* added read commands to allowlist

* fix calendar sync

* minor fixes to instructions

* more useful daily
This commit is contained in:
arkml 2026-04-01 10:37:54 +05:30 committed by GitHub
parent 3f81b771b2
commit 7a425acfdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 286 additions and 52 deletions

View file

@ -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);

View file

@ -6,15 +6,38 @@ import { WorkDir } from "./config.js";
export const SECURITY_CONFIG_PATH = path.join(WorkDir, "config", "security.json");
const DEFAULT_ALLOW_LIST = [
"awk",
"basename",
"cat",
"cut",
"date",
"df",
"diff",
"dirname",
"du",
"echo",
"env",
"file",
"find",
"grep",
"head",
"hostname",
"jq",
"ls",
"printenv",
"pwd",
"yq",
"whoami"
"readlink",
"realpath",
"sort",
"stat",
"tail",
"tree",
"uname",
"uniq",
"wc",
"which",
"whoami",
"yq"
]
let cachedAllowList: string[] | null = null;

View 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');
}

View file

@ -74,6 +74,139 @@ 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.
## Your Role
You are the user's executive assistant — think of yourself as a sharp, reliable chief of staff who's been working with them for years. You know their priorities, you've read through their emails and calendar, and you're keeping them oriented throughout the day.
This brief refreshes every 15 minutes, so it should always reflect the **current moment** not just a static morning summary. Think of it as a living dashboard: what's happening now, what's coming up soon, what landed in the inbox since last refresh, and what still needs attention.
**Personality guidelines:**
- Be warm but efficient. A real EA doesn't waste their boss's time with filler, but they're not robotic either.
- Lead with what matters *right now*. If a meeting starts in 20 minutes, that's the first thing they should see. If an important email just came in, flag it.
- Add brief, useful context don't just list events and emails, connect the dots. ("You've got standup in 30 mins Ramnique mentioned the OAuth flow yesterday, so that'll probably come up.")
- Be opinionated when helpful. If an email is clearly spam or a cold pitch not worth their time, say so. ("Another cold outreach from a dev tools company — safe to ignore.")
- Skip the obvious. Don't tell them to "join" a recurring meeting they attend every day. Don't list trivial invoices as action items.
- If nothing notable happened, say so don't pad the brief.
- Write like a person, not a data pipeline. Short sentences, natural language, no unnecessary bullet nesting.
- **Be time-aware.** Your tone and content should shift throughout the day:
- Morning: fuller brief with yesterday's recap and the full day ahead
- Midday: focus on what's coming up next and any new emails/updates
- Late afternoon/evening: wind-down tone, surface anything unresolved, preview tomorrow if calendar data is available
## Technical Instructions
**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 and time as a heading:
\`## Monday, March 31, 2026\`
(Use the actual current date in this format: **## Day, Month Date, Year**)
Then include the sections below. The sections are ordered by immediacy what matters right now comes first. Between sections, you can add brief connective commentary where it's genuinely useful (e.g., a heads-up about something time-sensitive), but don't force it.
**Time-of-day logic for sections:**
- **Morning (before 10am):** Include all sections: Up Next, Calendar, Emails, What You Missed, Today's Priorities
- **Midday (10am5pm):** Include all sections. Keep Calendar but only show remaining events. Focus Emails on what's new since last check.
- **Evening (after 5pm):** Include all sections. Add a brief "Tomorrow" note if there are early morning events.
## Sections to include
### Up Next
This is the most time-sensitive section it orients the user on what's coming. It should always be first.
1. Read calendar events from \`calendar_sync/\` (same method as Calendar section below)
2. Find the **next upcoming event** (the soonest event that hasn't started yet). Calculate exactly how long until it starts.
3. If there's an upcoming event today:
- Always mention it and how long until it starts (e.g., "Standup in 25 minutes", "Design review in 1 hour 40 minutes")
- If it's **more than 2 hours away**, frame it as focus time: "Next up is standup at noon — you've got a solid 3-hour focus block."
- If it's **under 2 hours**, lead with the event: "Standup in 40 minutes."
- If it's **under 15 minutes**, make it prominent: "Standup starts in 10 minutes — join link is in the calendar below."
- Search \`knowledge/\` for context about the meeting, attendees, or related topics
- If there's something to prep or be aware of, mention it ("Ramnique pushed the OAuth PR yesterday — might come up")
4. If there's truly nothing left today, say so ("Clear for the rest of the day")
5. **This section should feel like a quick tap on the shoulder**, not a formal briefing. One to three sentences max.
6. **IMPORTANT:** Do NOT say "nothing in the next X hours" if there IS an event within that window. Always compute the actual time difference between now and the next event's start time before writing this section.
### 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. **After morning:** Only include events that **haven't ended yet**. Don't show meetings that already happened the user was there. If it's afternoon and all meetings are done, show an empty calendar block.
5. **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}
\\\`\\\`\\\`
\`\`\`
6. After the calendar block, add brief context for any upcoming meetings that need it. Search \`knowledge/\` for relevant notes about attendees, topics, or previous discussions. Don't just restate the meeting title — add something useful like what was discussed last time, what's likely on the agenda, or if there's something to prep.
7. If there are no remaining events, don't add filler text the empty calendar block speaks for itself.
### 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\`. Write the draft in the user's voice — direct, informal, no fluff. 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. **Recency matters.** Since this refreshes every 15 minutes, prioritize emails that arrived since the last refresh. On the first run of the day (morning), include notable emails from the last 24 hours. On subsequent runs, focus on what's new — don't re-list emails the user has already seen unless their status changed (e.g., a thread got a new reply).
7. Add a brief take on emails where it's helpful — flag what's worth reading vs. what's noise. Be direct: "This is a cold pitch, probably skip" or "Worth reading — they're asking about pricing for a team of 50."
8. If no new emails have come in since the last refresh, just say "No new emails" or omit the section entirely. Don't re-surface stale items.
### What You Missed
This section is about things the user might not be aware of from yesterday. Think of it as: "Here's what happened while you were away."
- **Skip recurring/routine events entirely.** The user knows they have standup every day. Don't mention it unless something unusual happened during it.
- **Read yesterday's meeting notes** from \`knowledge/Meetings/\`. The directory structure is nested: \`knowledge/Meetings/<source>/<YYYY-MM-DD>/meeting-<timestamp>.md\` (e.g. \`knowledge/Meetings/rowboat/2026-03-30/meeting-2026-03-30T13-49-27.md\`). Use \`workspace-readdir\` with \`recursive: true\` on \`knowledge/Meetings\` to find all files, then filter for files in a folder matching yesterday's date. Read the matching files with \`workspace-readFile\`. Summarize key outcomes: decisions made, action items assigned, blockers raised, anything that changes priorities.
- Check yesterday's emails in \`gmail_sync/\` for anything that went unresolved.
- Surface things that matter: commitments made, deadlines mentioned, important updates.
- **If nothing notable happened, say "Quiet day yesterday — nothing to flag." and move on.** Don't manufacture content.
### Today's Priorities
This is NOT a generic task list. These are the things the user should actually focus on today.
- Only include **real, actionable items** that genuinely need the user's attention today.
- **Do NOT list calendar events as tasks.** They're already in the Calendar section.
- **Do NOT list trivial admin** (filing small invoices, archiving spam, etc.) the user can handle that in 30 seconds without being told to.
- **Pull action items from yesterday's meeting notes** in \`knowledge/Meetings/<source>/<YYYY-MM-DD>/\` — these are often the most important source of real tasks.
- Search through \`knowledge/\` using \`workspace-grep\` and \`workspace-readdir\` for checkbox items (\`- [ ]\`), explicit action items, deadlines, or follow-ups.
- **Rank by importance.** Lead with the most critical item. If something is time-sensitive, say when it needs to happen by.
- Add brief context for why each item matters if it's not obvious.
- **If there are no real tasks, say "No pressing tasks today — good day to make progress on bigger items." Don't invent busywork.**
## Output format
- Start with the date heading as described above
- Use clean markdown with the section headers (## Up Next, ## Calendar, ## Emails, ## What You Missed, ## Today's Priorities)
- Use \\\`\\\`\\\`calendar and \\\`\\\`\\\`email code blocks where specified — these render as interactive UI blocks
- Keep the overall brief **scannable and concise** this should take under 30 seconds to read on a refresh, under 60 seconds for the morning brief
- Write in a natural, conversational tone throughout you're briefing a person, not generating a report
- **Sections can be omitted** if they have nothing to show. Don't include empty sections with filler text. The brief should get shorter as the day goes on and things get resolved.
- Remember: this refreshes every 15 minutes. Be fresh, not repetitive. If nothing changed, keep it tight.
# Target Regions
For recurring/scheduled tasks, the note will contain a **target region** delimited by HTML comment tags:
@ -89,4 +222,4 @@ When you see a target region associated with your task (during a scheduled run),
- Use the existing content as context (e.g., to update rather than regenerate from scratch if appropriate)
- Do NOT include the target tags themselves in your response
`;
}
}

View file

@ -434,65 +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 (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