diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 45044f5f..ff6d8dc1 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -3514,7 +3514,11 @@ function App() { return ( - + { + if (section === 'knowledge' && !selectedPath && !isGraphOpen) { + void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH }) + } + }}>
{/* Content sidebar with SidebarProvider for collapse functionality */} / @@ -192,7 +192,9 @@ async function createNotesFromBatch( // Add each file's content message += `# Source Files to Process\n\n`; files.forEach((file, idx) => { - message += `## Source File ${idx + 1}: ${path.basename(file.path)}\n\n`; + // Pass workspace-relative path so the agent can link back to meeting notes + const relativePath = path.relative(WorkDir, file.path); + message += `## Source File ${idx + 1}: ${relativePath}\n\n`; message += file.content; message += `\n\n---\n\n`; }); diff --git a/apps/x/packages/core/src/knowledge/granola/sync.ts b/apps/x/packages/core/src/knowledge/granola/sync.ts index f03a8e06..2c647c9a 100644 --- a/apps/x/packages/core/src/knowledge/granola/sync.ts +++ b/apps/x/packages/core/src/knowledge/granola/sync.ts @@ -17,13 +17,14 @@ import { const GRANOLA_CLIENT_VERSION = '6.462.1'; const GRANOLA_API_BASE = 'https://api.granola.ai'; const GRANOLA_CONFIG_PATH = path.join(homedir(), 'Library', 'Application Support', 'Granola', 'supabase.json'); -const SYNC_DIR = path.join(WorkDir, 'granola_notes'); +const SYNC_DIR = path.join(WorkDir, 'knowledge', 'Meetings', 'granola'); const STATE_FILE = path.join(SYNC_DIR, 'sync_state.json'); const SYNC_INTERVAL_MS = 5 * 60 * 1000; // Check every 5 minutes const API_DELAY_MS = 1000; // 1 second delay between API calls const RATE_LIMIT_RETRY_DELAY_MS = 60 * 1000; // Wait 1 minute on rate limit const MAX_RETRIES = 3; // Maximum retries for rate-limited requests const MAX_BATCH_SIZE = 10; // Process max 10 documents per folder per sync +const LOOKBACK_DAYS = 30; // Only sync documents from the last 30 days // --- Wake Signal for Immediate Sync Trigger --- let wakeResolve: (() => void) | null = null; @@ -370,6 +371,10 @@ async function syncNotes(): Promise { let hasMore = true; const changedTitles: string[] = []; + // Calculate lookback cutoff date + const lookbackCutoff = new Date(); + lookbackCutoff.setDate(lookbackCutoff.getDate() - LOOKBACK_DAYS); + // Fetch documents with pagination while (hasMore) { // Delay before API call (except first) @@ -390,7 +395,16 @@ async function syncNotes(): Promise { } // Process each document + let foundOldDoc = false; for (const doc of docsResponse.docs) { + // Skip documents outside the lookback period + const docDate = new Date(doc.created_at); + if (docDate < lookbackCutoff) { + console.log(`[Granola] Document "${doc.title}" is older than ${LOOKBACK_DAYS} days, stopping pagination`); + foundOldDoc = true; + break; + } + const docUpdatedAt = doc.updated_at || doc.created_at; const lastSyncedAt = state.syncedDocs[doc.id]; @@ -407,8 +421,15 @@ async function syncNotes(): Promise { // Convert to markdown and save const markdown = documentToMarkdown(doc); - const filename = `${doc.id}_${cleanFilename(docTitle)}.md`; - const filePath = path.join(SYNC_DIR, filename); + const dateDir = path.join( + SYNC_DIR, + String(docDate.getFullYear()), + String(docDate.getMonth() + 1).padStart(2, '0'), + String(docDate.getDate()).padStart(2, '0') + ); + ensureDir(dateDir); + const filename = `${cleanFilename(docTitle)}.md`; + const filePath = path.join(dateDir, filename); fs.writeFileSync(filePath, markdown); @@ -424,6 +445,12 @@ async function syncNotes(): Promise { state.syncedDocs[doc.id] = docUpdatedAt; } + // Stop if we hit a document outside the lookback period + if (foundOldDoc) { + hasMore = false; + break; + } + // Move to next page offset += docsResponse.docs.length; diff --git a/apps/x/packages/core/src/knowledge/note_creation.ts b/apps/x/packages/core/src/knowledge/note_creation.ts index 2d7a6b58..bec0dd21 100644 --- a/apps/x/packages/core/src/knowledge/note_creation.ts +++ b/apps/x/packages/core/src/knowledge/note_creation.ts @@ -157,6 +157,7 @@ workspace-readFile({ path: "{source_file}" }) - Has \`Attendees:\` field - Has \`Meeting:\` title - Transcript format with speaker labels +- Source file path is under \`knowledge/Meetings/\` (e.g. \`knowledge/Meetings/granola/...\` or \`knowledge/Meetings/fireflies/...\`) **Email indicators:** - Has \`From:\` and \`To:\` fields @@ -680,6 +681,16 @@ One line summarizing this source's relevance to the entity: **{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[links]]} \`\`\` +**For meetings:** Include a link to the source meeting note. Derive the wiki-link path from the source file path (strip the \`.md\` extension): +\`\`\` +**2025-01-15** (meeting): Discussed [[Projects/Acme Integration]] timeline with [[People/David Kim]]. See [[Meetings/granola/abc123_Weekly Sync]] +\`\`\` + +**For emails:** Include a Gmail web link to the thread. Extract the thread ID from the \`**Thread ID:**\` field in the email source file, then construct the URL as \`https://mail.google.com/mail/#inbox/{threadId}\`: +\`\`\` +**2025-01-15** (email): [[People/Sarah Chen]] sent pricing proposal for [[Projects/Acme Integration]]. [View thread](https://mail.google.com/mail/#inbox/18d5a3b2c1e4f567) +\`\`\` + **For voice memos:** Include a link to the voice memo file using the Path field: \`\`\` **2025-01-15** (voice memo): Discussed [[Projects/Acme Integration]] timeline. See [[Voice Memos/2025-01-15/voice-memo-2025-01-15T10-30-00-000Z]] @@ -687,11 +698,13 @@ One line summarizing this source's relevance to the entity: **Important:** Use canonical names with absolute paths from resolution map in all summaries: \`\`\` -# Correct (uses absolute paths): -**2025-01-15** (meeting): [[People/Sarah Chen]] confirmed timeline with [[People/David Kim]]. Blocked on [[Topics/Security Compliance]]. +# Correct (uses absolute paths and source links): +**2025-01-15** (meeting): [[People/Sarah Chen]] confirmed timeline with [[People/David Kim]]. Blocked on [[Topics/Security Compliance]]. See [[Meetings/fireflies/abc_Team Sync]] +**2025-01-15** (email): [[People/Sarah Chen]] shared the contract draft. [View thread](https://mail.google.com/mail/#inbox/18d5a3b2c1e4f567) -# Incorrect (uses variants or relative links): +# Incorrect (uses variants or relative links, missing source links): **2025-01-15** (meeting): Sarah confirmed timeline with David. Blocked on SOC 2. +**2025-01-15** (email): Sarah shared the contract draft. \`\`\` --- @@ -888,6 +901,16 @@ ${renderNoteTypesBlock()} | Email (has create label) | Yes | Yes | Yes | | Email (only skip labels) | No (SKIP) | No | No | +**Meeting activity format:** Always include a link to the source meeting note: +\`\`\` +**2025-01-15** (meeting): Discussed project timeline with [[People/Sarah Chen]]. See [[Meetings/granola/abc123_Weekly Sync]] +\`\`\` + +**Email activity format:** Always include a Gmail web link using the Thread ID from the source: +\`\`\` +**2025-01-15** (email): [[People/Sarah Chen]] sent pricing proposal. [View thread](https://mail.google.com/mail/#inbox/18d5a3b2c1e4f567) +\`\`\` + **Voice memo activity format:** Always include a link to the source voice memo: \`\`\` **2025-01-15** (voice memo): Discussed project timeline with [[People/Sarah Chen]]. See [[Voice Memos/2025-01-15/voice-memo-...]] diff --git a/apps/x/packages/core/src/knowledge/note_system.ts b/apps/x/packages/core/src/knowledge/note_system.ts index 210d3501..39cf2695 100644 --- a/apps/x/packages/core/src/knowledge/note_system.ts +++ b/apps/x/packages/core/src/knowledge/note_system.ts @@ -153,6 +153,13 @@ const DEFAULT_NOTE_TYPE_DEFINITIONS: NoteTypeDefinition[] = [ extractionGuide: "Look for: topic name, keywords, related people/orgs/projects, decisions, key facts", }, + { + type: "Meetings", + folder: "Meetings", + template: "", + extractionGuide: + "Look for: meeting title, date, attendees, source (granola or fireflies), duration, topics discussed", + }, ]; // ── Disk-backed config with mtime caching ────────────────────────────────── diff --git a/apps/x/packages/core/src/knowledge/note_tagging_agent.ts b/apps/x/packages/core/src/knowledge/note_tagging_agent.ts index 94cd5016..8238e40a 100644 --- a/apps/x/packages/core/src/knowledge/note_tagging_agent.ts +++ b/apps/x/packages/core/src/knowledge/note_tagging_agent.ts @@ -16,14 +16,14 @@ tools: --- # Task -You are a note tagging agent. Given a batch of knowledge notes (People, Organizations, Projects, Topics), you will classify each note and prepend YAML frontmatter with categorized tags and Info attributes. +You are a note tagging agent. Given a batch of knowledge notes (People, Organizations, Projects, Topics, Meetings), you will classify each note and prepend YAML frontmatter with categorized tags and Info/metadata attributes. # Instructions 1. For each note file provided in the message, read its content carefully. -2. Determine the note type from its folder path (People/, Organizations/, Projects/, Topics/). +2. Determine the note type from its folder path (People/, Organizations/, Projects/, Topics/, Meetings/). 3. Classify the note using the Rowboat Tag System (Note Tags section) appended below. -4. Extract attributes from the note's \`## Info\` section (or \`## About\` for Topics). +4. Extract attributes from the note's \`## Info\` section (or \`## About\` for Topics). For Meetings, extract metadata from the note content and file path (see Meeting extraction rules below). 5. Use \`workspace-edit\` to prepend YAML frontmatter to the file. The oldString should be the first line of the file (the \`# Title\` heading), and the newString should be the frontmatter followed by that same first line. 6. If the note already has frontmatter (starts with \`---\`), skip it. @@ -97,6 +97,12 @@ Extract all \`**Key:** value\` fields from the \`## Info\` (or \`## About\`) sec - **Organizations**: type, industry, relationship, domain, aliases, first_met, last_seen - **Projects**: type, status, started, last_activity - **Topics** (from \`## About\`): keywords, aliases, first_mentioned, last_mentioned +- **Meetings**: Extract from the note content and file path: + - \`date\`: meeting date (from the file path \`Meetings/{source}/YYYY/MM/DD/\` or from \`created_at\`/\`Date:\` in content) + - \`source\`: \`granola\` or \`fireflies\` (from the file path) + - \`attendees\`: list of attendee names (from \`Attendees:\` field or participant list) + - \`title\`: meeting title + - \`topic\`: relevant topic tags based on meeting content Note: For Organizations, the Info \`**Relationship:**\` field is separate from the \`relationship\` tag category. Include both — the Info field as \`info_relationship\` and the tag as \`relationship\`. @@ -122,7 +128,11 @@ Note: For Organizations, the Info \`**Relationship:**\` field is separate from t 7. **For Topic notes**, include: - The relevant topic tag - Source tags -8. **Only use tags from the Rowboat Tag System** — do not invent new tags. +8. **For Meeting notes**, include: + - \`source: meeting\` + - Topic tags based on what was discussed + - The \`date\`, \`attendees\`, and \`title\` fields extracted from content +9. **Only use tags from the Rowboat Tag System** — do not invent new tags. 9. Process all files in the batch. Do not skip any unless they already have frontmatter. --- diff --git a/apps/x/packages/core/src/knowledge/sync_fireflies.ts b/apps/x/packages/core/src/knowledge/sync_fireflies.ts index 5e0cca07..1244b2dd 100644 --- a/apps/x/packages/core/src/knowledge/sync_fireflies.ts +++ b/apps/x/packages/core/src/knowledge/sync_fireflies.ts @@ -6,7 +6,7 @@ import { serviceLogger, type ServiceRunContext } from '../services/service_logge import { limitEventItems } from './limit_event_items.js'; // Configuration -const SYNC_DIR = path.join(WorkDir, 'fireflies_transcripts'); +const SYNC_DIR = path.join(WorkDir, 'knowledge', 'Meetings', 'fireflies'); const SYNC_INTERVAL_MS = 30 * 60 * 1000; // Check every 30 minutes (reduced from 1 minute) const STATE_FILE = path.join(SYNC_DIR, 'sync_state.json'); const LOOKBACK_DAYS = 30; // Last 1 month @@ -569,8 +569,16 @@ async function syncMeetings() { // Convert to markdown and save const markdown = meetingToMarkdown(meetingData); - const filename = `${meetingId}_${cleanFilename(meetingData.title || 'untitled')}.md`; - const filePath = path.join(SYNC_DIR, filename); + const meetingDate = new Date(meetingData.dateString || meetingData.date || Date.now()); + const dateDir = path.join( + SYNC_DIR, + String(meetingDate.getFullYear()), + String(meetingDate.getMonth() + 1).padStart(2, '0'), + String(meetingDate.getDate()).padStart(2, '0') + ); + fs.mkdirSync(dateDir, { recursive: true }); + const filename = `${cleanFilename(meetingData.title || 'untitled')}.md`; + const filePath = path.join(dateDir, filename); fs.writeFileSync(filePath, markdown); console.log(`[Fireflies] Saved: ${filename}`); diff --git a/apps/x/packages/core/src/knowledge/tag_notes.ts b/apps/x/packages/core/src/knowledge/tag_notes.ts index 95934b03..086a3bb5 100644 --- a/apps/x/packages/core/src/knowledge/tag_notes.ts +++ b/apps/x/packages/core/src/knowledge/tag_notes.ts @@ -30,17 +30,17 @@ function getUntaggedNotes(state: NoteTaggingState): string[] { const untagged: string[] = []; const noteFolders = getNoteTypeDefinitions().map(d => d.folder); - for (const folder of noteFolders) { - const folderPath = path.join(KNOWLEDGE_DIR, folder); - if (!fs.existsSync(folderPath)) { - continue; - } - - const entries = fs.readdirSync(folderPath); + function scanDir(dir: string) { + const entries = fs.readdirSync(dir); for (const entry of entries) { - const fullPath = path.join(folderPath, entry); + const fullPath = path.join(dir, entry); const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + scanDir(fullPath); + continue; + } + if (!stat.isFile() || !entry.endsWith('.md')) { continue; } @@ -64,6 +64,14 @@ function getUntaggedNotes(state: NoteTaggingState): string[] { } } + for (const folder of noteFolders) { + const folderPath = path.join(KNOWLEDGE_DIR, folder); + if (!fs.existsSync(folderPath)) { + continue; + } + scanDir(folderPath); + } + return untagged; }