mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
fix calendar sync and daily
This commit is contained in:
parent
5a354049cc
commit
19b51e974f
4 changed files with 204 additions and 56 deletions
|
|
@ -32,6 +32,11 @@ function ensureDefaultConfigs() {
|
|||
ensureDirs();
|
||||
ensureDefaultConfigs();
|
||||
|
||||
// Ensure default knowledge files exist
|
||||
import('../knowledge/ensure_daily_note.js').then(m => m.ensureDailyNote()).catch(err => {
|
||||
console.error('[DailyNote] Failed to ensure daily note:', err);
|
||||
});
|
||||
|
||||
// Initialize version history repo (async, fire-and-forget on startup)
|
||||
import('../knowledge/version_history.js').then(m => m.initRepo()).catch(err => {
|
||||
console.error('[VersionHistory] Failed to init repo:', err);
|
||||
|
|
|
|||
48
apps/x/packages/core/src/knowledge/ensure_daily_note.ts
Normal file
48
apps/x/packages/core/src/knowledge/ensure_daily_note.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { WorkDir } from '../config/config.js';
|
||||
|
||||
const KNOWLEDGE_DIR = path.join(WorkDir, 'knowledge');
|
||||
const DAILY_NOTE_PATH = path.join(KNOWLEDGE_DIR, 'Today.md');
|
||||
const TARGET_ID = 'dailybrief';
|
||||
|
||||
function buildDailyNoteContent(): string {
|
||||
const now = new Date();
|
||||
const startDate = now.toISOString();
|
||||
const endDate = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
const instruction = 'Create a daily brief for me';
|
||||
|
||||
const taskBlock = JSON.stringify({
|
||||
instruction,
|
||||
schedule: {
|
||||
type: 'cron',
|
||||
expression: '*/15 * * * *',
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
'schedule-label': 'runs every 15 minutes',
|
||||
targetId: TARGET_ID,
|
||||
});
|
||||
|
||||
return [
|
||||
'---',
|
||||
'live_note: true',
|
||||
'---',
|
||||
'# Today',
|
||||
'',
|
||||
'```task',
|
||||
taskBlock,
|
||||
'```',
|
||||
'',
|
||||
`<!--task-target:${TARGET_ID}-->`,
|
||||
`<!--/task-target:${TARGET_ID}-->`,
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function ensureDailyNote(): void {
|
||||
if (fs.existsSync(DAILY_NOTE_PATH)) return;
|
||||
fs.writeFileSync(DAILY_NOTE_PATH, buildDailyNoteContent(), 'utf-8');
|
||||
console.log('[DailyNote] Created today.md');
|
||||
}
|
||||
|
|
@ -74,6 +74,83 @@ Current UTC time: ${nowISO}
|
|||
- For scheduled tasks: output ONLY the two markers (schedule + instruction), nothing else.
|
||||
- Do not modify the original note file — the system handles all insertions.
|
||||
|
||||
# Daily Brief
|
||||
|
||||
When the instruction is to "create a daily brief" (or similar), generate a comprehensive daily briefing.
|
||||
|
||||
**IMPORTANT:** All workspace tools (workspace-readdir, workspace-readFile, workspace-grep, etc.) take paths **relative to the workspace root**. Use paths like \`calendar_sync/\`, \`gmail_sync/\`, \`knowledge/\` — NOT absolute paths.
|
||||
|
||||
**IMPORTANT:** Check the current date. If the date has changed since the content was last generated, clear everything and start fresh for the new day.
|
||||
|
||||
## Output structure
|
||||
|
||||
Your output MUST start with the current date as a heading:
|
||||
|
||||
\`## Monday, March 31, 2026\`
|
||||
|
||||
(Use the actual current date in this format: **## Day, Month Date, Year**)
|
||||
|
||||
Then include each section below.
|
||||
|
||||
## Sections to include
|
||||
|
||||
### Calendar
|
||||
1. Use \`workspace-readdir\` with path \`calendar_sync\` to list files
|
||||
2. Use \`workspace-readFile\` to read each \`.json\` event file (e.g. \`calendar_sync/eventid123.json\`)
|
||||
3. Filter for events happening **today** (compare the event's start dateTime or date to the current date)
|
||||
4. **Always** output a \\\`\\\`\\\`calendar block — even if there are no events today. If no events, output an empty events array:
|
||||
|
||||
\`\`\`
|
||||
\\\`\\\`\\\`calendar
|
||||
{"title":"Today's Meetings","events":[],"showJoinButton":false}
|
||||
\\\`\\\`\\\`
|
||||
\`\`\`
|
||||
|
||||
If there are events, include them:
|
||||
|
||||
\`\`\`
|
||||
\\\`\\\`\\\`calendar
|
||||
{"title":"Today's Meetings","events":[{"summary":"Weekly Sync","start":{"dateTime":"2026-04-01T10:00:00+05:30"},"end":{"dateTime":"2026-04-01T11:00:00+05:30"},"location":"Google Meet","htmlLink":"...","conferenceLink":"..."}],"showJoinButton":true}
|
||||
\\\`\\\`\\\`
|
||||
\`\`\`
|
||||
|
||||
5. For each upcoming meeting, add a brief note below the calendar block summarizing what the meeting is about. Use \`workspace-grep\` to search knowledge notes for relevant context about attendees or topics.
|
||||
6. Do NOT add any text like "No meetings found" — the empty calendar block is sufficient.
|
||||
|
||||
### Emails
|
||||
1. Use \`workspace-readdir\` with path \`gmail_sync\` to list files (skip \`sync_state.json\` and \`attachments/\`)
|
||||
2. Use \`workspace-readFile\` to read the email markdown files (e.g. \`gmail_sync/threadid123.md\`)
|
||||
3. Check the frontmatter \`action\` field — emails with \`action: reply\` or \`action: respond\` need a response
|
||||
4. For emails needing a response, output \\\`\\\`\\\`email blocks with a \`draft_response\`. Example:
|
||||
|
||||
\`\`\`
|
||||
\\\`\\\`\\\`email
|
||||
{"threadId":"abc123","summary":"Payment confirmation","subject":"Google services payment","from":"Sender <sender@example.com>","date":"2026-04-01T11:28:39+05:30","latest_email":"Hi, I've made the payment...","draft_response":"Thanks for confirming. I'll update our records."}
|
||||
\\\`\\\`\\\`
|
||||
\`\`\`
|
||||
|
||||
5. For other important/recent emails, output \\\`\\\`\\\`email blocks without \`draft_response\` as FYI items
|
||||
6. Focus on emails from the last 24 hours
|
||||
|
||||
### Yesterday's Summary
|
||||
- Check yesterday's calendar events from \`calendar_sync/\` for meetings that occurred
|
||||
- Check emails from yesterday in \`gmail_sync/\`
|
||||
- Use \`workspace-grep\` to search \`knowledge/\` for any updates from yesterday
|
||||
- Keep concise — a few bullet points
|
||||
|
||||
### Tasks for Today
|
||||
- Search through \`knowledge/\` using \`workspace-grep\` and \`workspace-readdir\` for tasks, todos, or action items
|
||||
- Look for checkbox items (\`- [ ]\`), "TODO", "action item", or similar patterns
|
||||
- Look at recently updated notes for context on current work
|
||||
- List relevant items as a markdown checklist
|
||||
|
||||
## Output format
|
||||
- Start with the date heading as described above
|
||||
- Use clean markdown with the section headers (## Calendar, ## Emails, etc.)
|
||||
- Use \\\`\\\`\\\`calendar and \\\`\\\`\\\`email code blocks where specified — these render as interactive UI blocks
|
||||
- Do NOT add filler text or commentary when sections are empty — just show the empty block
|
||||
- Keep the overall brief scannable and concise
|
||||
|
||||
# Target Regions
|
||||
|
||||
For recurring/scheduled tasks, the note will contain a **target region** delimited by HTML comment tags:
|
||||
|
|
|
|||
|
|
@ -434,72 +434,90 @@ async function performSyncComposio() {
|
|||
};
|
||||
|
||||
try {
|
||||
const result = await executeAction(
|
||||
'GOOGLECALENDAR_FIND_EVENT',
|
||||
{
|
||||
connected_account_id: connectedAccountId,
|
||||
user_id: 'rowboat-user',
|
||||
version: 'latest',
|
||||
arguments: {
|
||||
calendar_id: 'primary',
|
||||
time_min: timeMin,
|
||||
time_max: timeMax,
|
||||
single_events: true,
|
||||
order_by: 'startTime',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.successful || !result.data) {
|
||||
console.error('[Calendar] Failed to list events via Composio:', result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = result.data as Record<string, unknown>;
|
||||
// Composio may return events in different structures
|
||||
let events: Array<Record<string, unknown>> = [];
|
||||
|
||||
if (Array.isArray(data.items)) {
|
||||
events = data.items as Array<Record<string, unknown>>;
|
||||
} else if (Array.isArray(data.events)) {
|
||||
events = data.events as Array<Record<string, unknown>>;
|
||||
} else if (data.event_data && typeof data.event_data === 'object') {
|
||||
const nested = data.event_data as Record<string, unknown>;
|
||||
if (Array.isArray(nested.event_data)) {
|
||||
events = nested.event_data as Array<Record<string, unknown>>;
|
||||
} else if (Array.isArray(data.event_data)) {
|
||||
events = data.event_data as Array<Record<string, unknown>>;
|
||||
}
|
||||
} else if (Array.isArray(data)) {
|
||||
events = data as unknown as Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
const currentEventIds = new Set<string>();
|
||||
let newCount = 0;
|
||||
let updatedCount = 0;
|
||||
const changedTitles: string[] = [];
|
||||
let pageToken: string | null = null;
|
||||
const MAX_PAGES = 20;
|
||||
|
||||
if (events.length === 0) {
|
||||
console.log('[Calendar] No events found in this window.');
|
||||
} else {
|
||||
console.log(`[Calendar] Found ${events.length} events.`);
|
||||
for (const event of events) {
|
||||
const eventId = event.id as string | undefined;
|
||||
if (eventId) {
|
||||
const saveResult = saveComposioEvent(event, SYNC_DIR);
|
||||
currentEventIds.add(eventId);
|
||||
for (let page = 0; page < MAX_PAGES; page++) {
|
||||
const args: Record<string, unknown> = {
|
||||
calendar_id: 'primary',
|
||||
time_min: timeMin,
|
||||
time_max: timeMax,
|
||||
single_events: true,
|
||||
order_by: 'startTime',
|
||||
};
|
||||
if (pageToken) {
|
||||
args.page_token = pageToken;
|
||||
}
|
||||
|
||||
if (saveResult.changed) {
|
||||
await ensureRun();
|
||||
changedTitles.push(saveResult.title);
|
||||
if (saveResult.isNew) {
|
||||
newCount++;
|
||||
} else {
|
||||
updatedCount++;
|
||||
const result = await executeAction(
|
||||
'GOOGLECALENDAR_FIND_EVENT',
|
||||
{
|
||||
connected_account_id: connectedAccountId,
|
||||
user_id: 'rowboat-user',
|
||||
version: 'latest',
|
||||
arguments: args,
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.successful || !result.data) {
|
||||
console.error('[Calendar] Failed to list events via Composio:', result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = result.data as Record<string, unknown>;
|
||||
// Composio may return events in different structures
|
||||
let events: Array<Record<string, unknown>> = [];
|
||||
|
||||
if (Array.isArray(data.items)) {
|
||||
events = data.items as Array<Record<string, unknown>>;
|
||||
} else if (Array.isArray(data.events)) {
|
||||
events = data.events as Array<Record<string, unknown>>;
|
||||
} else if (data.event_data && typeof data.event_data === 'object') {
|
||||
const nested = data.event_data as Record<string, unknown>;
|
||||
if (Array.isArray(nested.event_data)) {
|
||||
events = nested.event_data as Array<Record<string, unknown>>;
|
||||
} else if (Array.isArray(data.event_data)) {
|
||||
events = data.event_data as Array<Record<string, unknown>>;
|
||||
}
|
||||
} else if (Array.isArray(data)) {
|
||||
events = data as unknown as Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
if (events.length === 0 && page === 0) {
|
||||
console.log('[Calendar] No events found in this window.');
|
||||
} else if (events.length > 0) {
|
||||
console.log(`[Calendar] Page ${page + 1}: found ${events.length} events.`);
|
||||
for (const event of events) {
|
||||
const eventId = event.id as string | undefined;
|
||||
if (eventId) {
|
||||
const saveResult = saveComposioEvent(event, SYNC_DIR);
|
||||
currentEventIds.add(eventId);
|
||||
|
||||
if (saveResult.changed) {
|
||||
await ensureRun();
|
||||
changedTitles.push(saveResult.title);
|
||||
if (saveResult.isNew) {
|
||||
newCount++;
|
||||
} else {
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for next page
|
||||
const nextToken = data.nextPageToken as string | undefined;
|
||||
if (nextToken) {
|
||||
pageToken = nextToken;
|
||||
console.log(`[Calendar] Fetching next page...`);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up events no longer in the window
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue