voice memos update the graph correctly

This commit is contained in:
Arjun 2026-02-03 14:30:27 +05:30
parent 81cbd1a891
commit 51f1ce43ae
5 changed files with 334 additions and 209 deletions

View file

@ -183,6 +183,7 @@ function VoiceNoteButton({ onNoteCreated }: { onNoteCreated?: (path: string) =>
const chunksRef = React.useRef<Blob[]>([])
const notePathRef = React.useRef<string | null>(null)
const timestampRef = React.useRef<string | null>(null)
const relativePathRef = React.useRef<string | null>(null)
const startRecording = async () => {
try {
@ -195,6 +196,9 @@ function VoiceNoteButton({ onNoteCreated }: { onNoteCreated?: (path: string) =>
timestampRef.current = timestamp
notePathRef.current = notePath
// Relative path for linking (from knowledge/ root, without .md extension)
const relativePath = `Voice Memos/${dateStr}/${noteName}`
relativePathRef.current = relativePath
// Create the note immediately with a "Recording..." placeholder
await window.ipc.invoke('workspace:mkdir', {
@ -206,6 +210,7 @@ function VoiceNoteButton({ onNoteCreated }: { onNoteCreated?: (path: string) =>
**Type:** voice memo
**Recorded:** ${now.toLocaleString()}
**Path:** ${relativePath}
## Transcript
@ -264,11 +269,13 @@ function VoiceNoteButton({ onNoteCreated }: { onNoteCreated?: (path: string) =>
// Update note to show transcribing status
const currentNotePath = notePathRef.current
if (currentNotePath) {
const currentRelativePath = relativePathRef.current
if (currentNotePath && currentRelativePath) {
const transcribingContent = `# Voice Memo
**Type:** voice memo
**Recorded:** ${new Date().toLocaleString()}
**Path:** ${currentRelativePath}
## Transcript
@ -283,12 +290,13 @@ function VoiceNoteButton({ onNoteCreated }: { onNoteCreated?: (path: string) =>
// Transcribe and update the note with the transcript
const transcript = await transcribeWithDeepgram(blob)
if (currentNotePath) {
if (currentNotePath && currentRelativePath) {
const finalContent = transcript
? `# Voice Memo
**Type:** voice memo
**Recorded:** ${new Date().toLocaleString()}
**Path:** ${currentRelativePath}
## Transcript
@ -298,6 +306,7 @@ ${transcript}
**Type:** voice memo
**Recorded:** ${new Date().toLocaleString()}
**Path:** ${currentRelativePath}
## Transcript

View file

@ -30,90 +30,87 @@ const SOURCE_FOLDERS = [
'granola_notes',
];
// Voice memos are handled separately - they get moved to knowledge/Voice Memos/<date>/
const VOICE_MEMOS_DIR = path.join(WorkDir, 'voice_memos');
// Voice memos are now created directly in knowledge/Voice Memos/<date>/
const VOICE_MEMOS_KNOWLEDGE_DIR = path.join(NOTES_OUTPUT_DIR, 'Voice Memos');
/**
* Extract date from voice memo filename
* Filename format: voice-memo-2026-02-03T04-56-53-182Z.txt
* Returns date string like "2026-02-03"
* Get unprocessed voice memo files from knowledge/Voice Memos/
* Voice memos are created directly in this directory by the UI.
* Returns paths to files that need entity extraction.
*/
function extractDateFromVoiceMemoFilename(filename: string): string {
// Match the date pattern: YYYY-MM-DD from voice-memo-YYYY-MM-DDTHH-MM-SS-MMMZ
const match = filename.match(/voice-memo-(\d{4}-\d{2}-\d{2})T/);
if (match) {
return match[1];
}
// Fallback to current date if pattern doesn't match
return new Date().toISOString().split('T')[0];
}
function getUnprocessedVoiceMemos(state: GraphState): string[] {
console.log(`[VoiceMemos] Checking directory: ${VOICE_MEMOS_KNOWLEDGE_DIR}`);
/**
* Move voice memo transcripts to knowledge/Voice Memos/<date>/
* Returns info about moved files for processing.
*/
function moveVoiceMemosToKnowledge(state: GraphState): { sourcePath: string; targetPath: string }[] {
if (!fs.existsSync(VOICE_MEMOS_DIR)) {
if (!fs.existsSync(VOICE_MEMOS_KNOWLEDGE_DIR)) {
console.log(`[VoiceMemos] Directory does not exist`);
return [];
}
const movedFiles: { sourcePath: string; targetPath: string }[] = [];
const entries = fs.readdirSync(VOICE_MEMOS_DIR);
const unprocessedFiles: string[] = [];
for (const entry of entries) {
// Only process .txt transcript files
if (!entry.endsWith('.txt')) {
// Scan date folders (e.g., 2026-02-03)
const dateFolders = fs.readdirSync(VOICE_MEMOS_KNOWLEDGE_DIR);
console.log(`[VoiceMemos] Found ${dateFolders.length} date folders: ${dateFolders.join(', ')}`);
for (const dateFolder of dateFolders) {
const dateFolderPath = path.join(VOICE_MEMOS_KNOWLEDGE_DIR, dateFolder);
// Skip if not a directory
try {
if (!fs.statSync(dateFolderPath).isDirectory()) {
continue;
}
} catch (err) {
console.log(`[VoiceMemos] Error checking ${dateFolderPath}:`, err);
continue;
}
const sourcePath = path.join(VOICE_MEMOS_DIR, entry);
// Scan markdown files in this date folder
const files = fs.readdirSync(dateFolderPath);
console.log(`[VoiceMemos] Found ${files.length} files in ${dateFolder}: ${files.join(', ')}`);
// Skip if already processed (check by source path)
if (state.processedFiles[sourcePath]) {
continue;
for (const file of files) {
// Only process voice memo markdown files
if (!file.endsWith('.md') || !file.startsWith('voice-memo-')) {
console.log(`[VoiceMemos] Skipping ${file} - not a voice memo file`);
continue;
}
const filePath = path.join(dateFolderPath, file);
// Skip if already processed
if (state.processedFiles[filePath]) {
console.log(`[VoiceMemos] Skipping ${file} - already processed`);
continue;
}
// Check if the file has actual content (not still recording/transcribing)
try {
const content = fs.readFileSync(filePath, 'utf-8');
// Skip files that are still recording or transcribing
if (content.includes('*Recording in progress...*')) {
console.log(`[VoiceMemos] Skipping ${file} - still recording`);
continue;
}
if (content.includes('*Transcribing...*')) {
console.log(`[VoiceMemos] Skipping ${file} - still transcribing`);
continue;
}
if (content.includes('*Transcription failed')) {
console.log(`[VoiceMemos] Skipping ${file} - transcription failed`);
continue;
}
console.log(`[VoiceMemos] Found unprocessed voice memo: ${file}`);
unprocessedFiles.push(filePath);
} catch (err) {
console.log(`[VoiceMemos] Error reading ${file}:`, err);
continue;
}
}
// Extract date and create target directory
const date = extractDateFromVoiceMemoFilename(entry);
const targetDir = path.join(VOICE_MEMOS_KNOWLEDGE_DIR, date);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
// Convert .txt to .md and create a proper markdown file
const baseName = entry.replace('.txt', '');
const targetPath = path.join(targetDir, `${baseName}.md`);
// Read original content and wrap in markdown format
const originalContent = fs.readFileSync(sourcePath, 'utf-8');
// Create a relative path for linking (from knowledge/ root)
const relativePath = `Voice Memos/${date}/${baseName}`;
const mdContent = `# Voice Memo - ${date}
**Type:** voice memo
**Recorded:** ${date}
**Path:** ${relativePath}
## Transcript
${originalContent}
`;
// Write to knowledge directory
fs.writeFileSync(targetPath, mdContent);
console.log(`[VoiceMemos] Moved transcript to ${targetPath}`);
// Delete the original .txt file (it's been moved)
fs.unlinkSync(sourcePath);
movedFiles.push({ sourcePath, targetPath });
}
return movedFiles;
console.log(`[VoiceMemos] Total unprocessed files: ${unprocessedFiles.length}`);
return unprocessedFiles;
}
/**
@ -272,23 +269,26 @@ export async function buildGraph(sourceDir: string): Promise<void> {
}
/**
* Process voice memos and run entity extraction on them
* Process voice memos from knowledge/Voice Memos/ and run entity extraction on them
* Voice memos are now created directly in the knowledge directory by the UI.
*/
async function processVoiceMemosForKnowledge(): Promise<boolean> {
console.log(`[VoiceMemos] Starting voice memo processing...`);
const state = loadState();
// Move voice memos to knowledge directory
const movedFiles = moveVoiceMemosToKnowledge(state);
// Get unprocessed voice memos from knowledge/Voice Memos/
const unprocessedFiles = getUnprocessedVoiceMemos(state);
if (movedFiles.length === 0) {
if (unprocessedFiles.length === 0) {
console.log(`[VoiceMemos] No unprocessed voice memos found`);
return false;
}
console.log(`[VoiceMemos] Processing ${movedFiles.length} voice memo transcripts for entity extraction...`);
console.log(`[VoiceMemos] Processing ${unprocessedFiles.length} voice memo transcripts for entity extraction...`);
console.log(`[VoiceMemos] Files to process: ${unprocessedFiles.map(f => path.basename(f)).join(', ')}`);
// Read the moved files
const targetPaths = movedFiles.map(f => f.targetPath);
const contentFiles = await readFileContents(targetPaths);
// Read the files
const contentFiles = await readFileContents(unprocessedFiles);
if (contentFiles.length === 0) {
return false;
@ -300,7 +300,6 @@ async function processVoiceMemosForKnowledge(): Promise<boolean> {
for (let i = 0; i < contentFiles.length; i += BATCH_SIZE) {
const batch = contentFiles.slice(i, i + BATCH_SIZE);
const batchMovedFiles = movedFiles.slice(i, i + BATCH_SIZE);
const batchNumber = Math.floor(i / BATCH_SIZE) + 1;
try {
@ -313,11 +312,9 @@ async function processVoiceMemosForKnowledge(): Promise<boolean> {
await createNotesFromBatch(batch, batchNumber, indexForPrompt);
console.log(`[VoiceMemos] Batch ${batchNumber}/${totalBatches} complete`);
// Mark files as processed using SOURCE path as key (to prevent reprocessing)
for (const { sourcePath, targetPath } of batchMovedFiles) {
markFileAsProcessed(targetPath, state);
// Also track by source path so we don't reprocess if a new file with same name appears
state.processedFiles[sourcePath] = state.processedFiles[targetPath];
// Mark files as processed
for (const file of batch) {
markFileAsProcessed(file.path, state);
}
// Save state after each batch
@ -392,7 +389,7 @@ async function processAllSources(): Promise<void> {
*/
export async function init() {
console.log('[GraphBuilder] Starting Knowledge Graph Builder Service...');
console.log(`[GraphBuilder] Monitoring folders: ${SOURCE_FOLDERS.join(', ')}, voice_memos`);
console.log(`[GraphBuilder] Monitoring folders: ${SOURCE_FOLDERS.join(', ')}, knowledge/Voice Memos`);
console.log(`[GraphBuilder] Will check for new content every ${SYNC_INTERVAL_MS / 1000} seconds`);
// Initial run

View file

@ -7,15 +7,21 @@ tools:
workspace-readFile:
type: builtin
name: workspace-readFile
workspace-edit:
type: builtin
name: workspace-edit
workspace-readdir:
type: builtin
name: workspace-readdir
workspace-mkdir:
type: builtin
name: workspace-mkdir
executeCommand:
workspace-grep:
type: builtin
name: executeCommand
name: workspace-grep
workspace-glob:
type: builtin
name: workspace-glob
---
# Task
@ -70,20 +76,51 @@ When you need to:
# Tools Available
You have access to \`executeCommand\` to run shell commands:
You have access to these tools:
**For reading files:**
\`\`\`
executeCommand("ls {path}") # List directory contents
executeCommand("cat {path}") # Read file contents
executeCommand("head -50 {path}") # Read first 50 lines
executeCommand("write {path} {content}") # Create or overwrite file
workspace-readFile({ path: "knowledge/People/Sarah Chen.md" })
\`\`\`
**Important:** Use shell escaping for paths with spaces:
**For creating NEW files:**
\`\`\`
executeCommand("cat 'knowledge_folder/People/Sarah Chen.md'")
workspace-writeFile({ path: "knowledge/People/Sarah Chen.md", data: "# Sarah Chen\\n\\n..." })
\`\`\`
**NOTE:** Do NOT use grep to search for entities. Use the provided knowledge_index instead.
**For editing EXISTING files (preferred for updates):**
\`\`\`
workspace-edit({
path: "knowledge/People/Sarah Chen.md",
oldString: "## Activity\\n",
newString: "## Activity\\n- **2026-02-03** (meeting): New activity entry\\n"
})
\`\`\`
**For listing directories:**
\`\`\`
workspace-readdir({ path: "knowledge/People" })
\`\`\`
**For creating directories:**
\`\`\`
workspace-mkdir({ path: "knowledge/Projects", recursive: true })
\`\`\`
**For searching files:**
\`\`\`
workspace-grep({ pattern: "Acme Corp", searchPath: "knowledge", fileGlob: "*.md" })
\`\`\`
**For finding files by pattern:**
\`\`\`
workspace-glob({ pattern: "**/*.md", cwd: "knowledge/People" })
\`\`\`
**IMPORTANT:**
- Use \`workspace-edit\` for updating existing notes (adding activity, updating fields)
- Use \`workspace-writeFile\` only for creating new notes
- Prefer the knowledge_index for entity resolution (it's faster than grep)
# Output
@ -113,7 +150,7 @@ Either:
Read the source file and determine if it's a meeting or email.
\`\`\`
executeCommand("cat '{source_file}'")
workspace-readFile({ path: "{source_file}" })
\`\`\`
**Meeting indicators:**
@ -307,9 +344,9 @@ If someone only appears in your memory as "CC'd on outreach emails from [Sender]
## Email-Specific Filtering
For emails, check if sender/recipients have existing notes:
\`\`\`bash
executeCommand("grep -r -i -l '{sender email}' '{knowledge_folder}/'")
executeCommand("grep -r -i -l '{sender name}' '{knowledge_folder}/People/'")
\`\`\`
workspace-grep({ pattern: "{sender email}", searchPath: "{knowledge_folder}" })
workspace-grep({ pattern: "{sender name}", searchPath: "{knowledge_folder}/People" })
\`\`\`
**If no existing note found:**
@ -349,7 +386,7 @@ If processing, continue to Step 2.
# Step 2: Read and Parse Source File
\`\`\`
executeCommand("cat '{source_file}'")
workspace-readFile({ path: "{source_file}" })
\`\`\`
Extract metadata:
@ -446,7 +483,7 @@ From index, find matches for:
Only read the full note content when you need details not in the index (e.g., activity logs, open items):
\`\`\`bash
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
workspace-readFile({ path: "{knowledge_folder}/People/Sarah Chen.md" })
\`\`\`
**Why read these notes:**
@ -530,27 +567,27 @@ Resolution Map:
When multiple candidates match a variant, disambiguate:
**By organization (strongest signal):**
\`\`\`bash
\`\`\`
# "David" could be David Kim or David Chen
executeCommand("grep -i 'Acme' '{knowledge_folder}/People/David Kim.md'")
workspace-grep({ pattern: "Acme", searchPath: "{knowledge_folder}/People/David Kim.md" })
# Output: **Organization:** [[Acme Corp]]
executeCommand("grep -i 'Acme' '{knowledge_folder}/People/David Chen.md'")
workspace-grep({ pattern: "Acme", searchPath: "{knowledge_folder}/People/David Chen.md" })
# Output: **Organization:** [[Other Corp]]
# Source is from Acme context "David" = "David Kim"
\`\`\`
**By email (definitive):**
\`\`\`bash
executeCommand("grep -i 'david@acme.com' '{knowledge_folder}/People/David Kim.md'")
\`\`\`
workspace-grep({ pattern: "david@acme.com", searchPath: "{knowledge_folder}/People/David Kim.md" })
# Exact email match is definitive
\`\`\`
**By role:**
\`\`\`bash
\`\`\`
# Source mentions "their CTO"
executeCommand("grep -r -i 'Role.*CTO' '{knowledge_folder}/People/'")
workspace-grep({ pattern: "Role.*CTO", searchPath: "{knowledge_folder}/People" })
# Filter results by organization context
\`\`\`
@ -979,8 +1016,8 @@ STATE CHANGES:
Before writing, compare extracted content against existing notes.
## Check Activity Log
\`\`\`bash
executeCommand("grep '2025-01-15' '{knowledge_folder}/People/Sarah Chen.md'")
\`\`\`
workspace-grep({ pattern: "2025-01-15", searchPath: "{knowledge_folder}/People/Sarah Chen.md" })
\`\`\`
If an entry for this date/source already exists, this may have been processed. Skip or verify different interaction.
@ -1010,28 +1047,28 @@ If new info contradicts existing:
**IMPORTANT: Write sequentially, one file at a time.**
- Generate content for exactly one note.
- Issue exactly one \`write\` command.
- Issue exactly one write/edit command.
- Wait for the tool to return before generating the next note.
- Do NOT batch multiple \`write\` commands in a single response.
- Do NOT batch multiple write commands in a single response.
**For new entities (meetings only):**
\`\`\`bash
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
**For NEW entities (use workspace-writeFile):**
\`\`\`
workspace-writeFile({
path: "{knowledge_folder}/People/Jennifer.md",
data: "# Jennifer\\n\\n## Summary\\n..."
})
\`\`\`
**For existing entities:**
- Read current content first
- Add activity entry at TOP of Activity section (reverse chronological)
- Update "Last seen" date
- Add new key facts (skip duplicates)
- Add new open items
- Add new decisions
- Add new relationships
- Update summary ONLY if significant new understanding
\`\`\`bash
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
# ... modify content ...
executeCommand("write '{knowledge_folder}/People/Sarah Chen.md' '{full_updated_content}'")
**For EXISTING entities (use workspace-edit):**
- Read current content first with workspace-readFile
- Use workspace-edit to add activity entry at TOP (reverse chronological)
- Update fields using targeted edits
\`\`\`
workspace-edit({
path: "{knowledge_folder}/People/Sarah Chen.md",
oldString: "## Activity\\n",
newString: "## Activity\\n- **2026-02-03** (meeting): Met to discuss project timeline\\n"
})
\`\`\`
## 9b: Emails Update Existing Notes Only
@ -1054,7 +1091,7 @@ For each state change identified in Step 7:
### Update Project Status
\`\`\`bash
# Read current project note
executeCommand("cat '{knowledge_folder}/Projects/Acme Integration.md'")
workspace-readFile({ path: "{knowledge_folder}/Projects/Acme Integration.md" })
# Update the Status field
# Change: **Status:** planning
@ -1064,7 +1101,7 @@ executeCommand("cat '{knowledge_folder}/Projects/Acme Integration.md'")
### Mark Open Items Complete
\`\`\`bash
# Read current note
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
workspace-readFile({ path: "{knowledge_folder}/People/Sarah Chen.md" })
# Find matching open item and update
# Change: - [ ] Send API documentation by Friday
@ -1074,7 +1111,7 @@ executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
### Update Role
\`\`\`bash
# Read current person note
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
workspace-readFile({ path: "{knowledge_folder}/People/Sarah Chen.md" })
# Update role field
# Change: **Role:** Engineering Lead
@ -1084,7 +1121,7 @@ executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
### Update Relationship
\`\`\`bash
# Read current org note
executeCommand("cat '{knowledge_folder}/Organizations/Acme Corp.md'")
workspace-readFile({ path: "{knowledge_folder}/Organizations/Acme Corp.md" })
# Update relationship field
# Change: **Relationship:** prospect
@ -1149,8 +1186,8 @@ This ensures:
## Check Each New Link
If you added \`[[People/Jennifer]]\` to \`Organizations/Acme Corp.md\`:
\`\`\`bash
executeCommand("grep 'Acme Corp' '{knowledge_folder}/People/Jennifer.md'")
\`\`\`
workspace-grep({ pattern: "Acme Corp", searchPath: "{knowledge_folder}/People/Jennifer.md" })
\`\`\`
If not found, update Jennifer.md to add the link.
@ -1384,11 +1421,11 @@ Not mass email, not automated. Continue.
- Variants: "Sarah Chen", "sarah@acme.com", "David Kim", "David", "Jennifer", "CTO", "Acme", "the pilot"
### Step 3: Search Existing Notes
\`\`\`bash
executeCommand("grep -r -i -l 'Sarah Chen' 'knowledge/'")
\`\`\`
workspace-grep({ pattern: "Sarah Chen", searchPath: "knowledge" })
# Output: (none)
executeCommand("grep -r -i -l 'acme' 'knowledge/'")
workspace-grep({ pattern: "acme", searchPath: "knowledge" })
# Output: (none)
\`\`\`
@ -1527,8 +1564,8 @@ VP Engineering, Acme Corp
### Step 1: Filter
Check for existing relationship:
\`\`\`bash
executeCommand("grep -r -i -l 'sarah@acme.com' 'knowledge/'")
\`\`\`
workspace-grep({ pattern: "sarah@acme.com", searchPath: "knowledge" })
# Output: notes/People/Sarah Chen.md
\`\`\`
@ -1663,11 +1700,11 @@ John Smith
### Step 1: Filter
Check for existing relationship:
\`\`\`bash
executeCommand("grep -r -i -l 'randomvendor' 'knowledge/'")
\`\`\`
workspace-grep({ pattern: "randomvendor", searchPath: "knowledge" })
# Output: (none)
executeCommand("grep -r -i -l 'John Smith' 'knowledge/'")
workspace-grep({ pattern: "John Smith", searchPath: "knowledge" })
# Output: (none)
\`\`\`
@ -1710,8 +1747,8 @@ David
### Step 1: Filter
Check for sender:
\`\`\`bash
executeCommand("grep -r -i -l 'david@friendly.vc' 'knowledge/'")
\`\`\`
workspace-grep({ pattern: "david@friendly.vc", searchPath: "knowledge" })
# Output: notes/People/David Park.md
\`\`\`

View file

@ -7,15 +7,21 @@ tools:
workspace-readFile:
type: builtin
name: workspace-readFile
workspace-edit:
type: builtin
name: workspace-edit
workspace-readdir:
type: builtin
name: workspace-readdir
workspace-mkdir:
type: builtin
name: workspace-mkdir
executeCommand:
workspace-grep:
type: builtin
name: executeCommand
name: workspace-grep
workspace-glob:
type: builtin
name: workspace-glob
---
# Task
@ -70,20 +76,51 @@ When you need to:
# Tools Available
You have access to \`executeCommand\` to run shell commands:
You have access to these tools:
**For reading files:**
\`\`\`
executeCommand("ls {path}") # List directory contents
executeCommand("cat {path}") # Read file contents
executeCommand("head -50 {path}") # Read first 50 lines
executeCommand("write {path} {content}") # Create or overwrite file
workspace-readFile({ path: "knowledge/People/Sarah Chen.md" })
\`\`\`
**Important:** Use shell escaping for paths with spaces:
**For creating NEW files:**
\`\`\`
executeCommand("cat 'knowledge_folder/People/Sarah Chen.md'")
workspace-writeFile({ path: "knowledge/People/Sarah Chen.md", data: "# Sarah Chen\\n\\n..." })
\`\`\`
**NOTE:** Do NOT use grep to search for entities. Use the provided knowledge_index instead.
**For editing EXISTING files (preferred for updates):**
\`\`\`
workspace-edit({
path: "knowledge/People/Sarah Chen.md",
oldString: "## Activity\\n",
newString: "## Activity\\n- **2026-02-03** (meeting): New activity entry\\n"
})
\`\`\`
**For listing directories:**
\`\`\`
workspace-readdir({ path: "knowledge/People" })
\`\`\`
**For creating directories:**
\`\`\`
workspace-mkdir({ path: "knowledge/Projects", recursive: true })
\`\`\`
**For searching files:**
\`\`\`
workspace-grep({ pattern: "Acme Corp", searchPath: "knowledge", fileGlob: "*.md" })
\`\`\`
**For finding files by pattern:**
\`\`\`
workspace-glob({ pattern: "**/*.md", cwd: "knowledge/People" })
\`\`\`
**IMPORTANT:**
- Use \`workspace-edit\` for updating existing notes (adding activity, updating fields)
- Use \`workspace-writeFile\` only for creating new notes
- Prefer the knowledge_index for entity resolution (it's faster than grep)
# Output
@ -120,7 +157,7 @@ This mode prioritizes comprehensive capture over selectivity. The goal is to nev
Read the source file and determine if it's a meeting or email.
\`\`\`
executeCommand("cat '{source_file}'")
workspace-readFile({ path: "{source_file}" })
\`\`\`
**Meeting indicators:**
@ -210,7 +247,7 @@ If processing, continue to Step 2.
# Step 2: Read and Parse Source File
\`\`\`
executeCommand("cat '{source_file}'")
workspace-readFile({ path: "{source_file}" })
\`\`\`
Extract metadata:
@ -297,8 +334,8 @@ From index, find matches for:
## 3d: Read Full Notes When Needed
Only read the full note content when you need details not in the index (e.g., activity logs, open items):
\`\`\`bash
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
\`\`\`
workspace-readFile({ path: "{knowledge_folder}/People/Sarah Chen.md" })
\`\`\`
**Why read these notes:**
@ -566,21 +603,29 @@ Before writing:
**IMPORTANT: Write sequentially, one file at a time.**
- Generate content for exactly one note.
- Issue exactly one \`write\` command.
- Issue exactly one write/edit command.
- Wait for the tool to return before generating the next note.
- Do NOT batch multiple \`write\` commands in a single response.
- Do NOT batch multiple write commands in a single response.
**For new entities:**
\`\`\`bash
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
**For NEW entities (use workspace-writeFile):**
\`\`\`
workspace-writeFile({
path: "{knowledge_folder}/People/Jennifer.md",
data: "# Jennifer\\n\\n## Summary\\n..."
})
\`\`\`
**For existing entities:**
- Read current content first
- Add activity entry at TOP (reverse chronological)
- Update "Last seen" date
- Add new key facts (skip duplicates)
- Add new open items
**For EXISTING entities (use workspace-edit):**
- Read current content first with workspace-readFile
- Use workspace-edit to add activity entry at TOP (reverse chronological)
- Update fields using targeted edits
\`\`\`
workspace-edit({
path: "{knowledge_folder}/People/Sarah Chen.md",
oldString: "## Activity\\n",
newString: "## Activity\\n- **2026-02-03** (meeting): Met to discuss project timeline\\n"
})
\`\`\`
## 9b: Apply State Changes

View file

@ -7,15 +7,21 @@ tools:
workspace-readFile:
type: builtin
name: workspace-readFile
workspace-edit:
type: builtin
name: workspace-edit
workspace-readdir:
type: builtin
name: workspace-readdir
workspace-mkdir:
type: builtin
name: workspace-mkdir
executeCommand:
workspace-grep:
type: builtin
name: executeCommand
name: workspace-grep
workspace-glob:
type: builtin
name: workspace-glob
---
# Task
@ -70,20 +76,51 @@ When you need to:
# Tools Available
You have access to \`executeCommand\` to run shell commands:
You have access to these tools:
**For reading files:**
\`\`\`
executeCommand("ls {path}") # List directory contents
executeCommand("cat {path}") # Read file contents
executeCommand("head -50 {path}") # Read first 50 lines
executeCommand("write {path} {content}") # Create or overwrite file
workspace-readFile({ path: "knowledge/People/Sarah Chen.md" })
\`\`\`
**Important:** Use shell escaping for paths with spaces:
**For creating NEW files:**
\`\`\`
executeCommand("cat 'knowledge_folder/People/Sarah Chen.md'")
workspace-writeFile({ path: "knowledge/People/Sarah Chen.md", data: "# Sarah Chen\\n\\n..." })
\`\`\`
**NOTE:** Do NOT use grep to search for entities. Use the provided knowledge_index instead.
**For editing EXISTING files (preferred for updates):**
\`\`\`
workspace-edit({
path: "knowledge/People/Sarah Chen.md",
oldString: "## Activity\\n",
newString: "## Activity\\n- **2026-02-03** (meeting): New activity entry\\n"
})
\`\`\`
**For listing directories:**
\`\`\`
workspace-readdir({ path: "knowledge/People" })
\`\`\`
**For creating directories:**
\`\`\`
workspace-mkdir({ path: "knowledge/Projects", recursive: true })
\`\`\`
**For searching files:**
\`\`\`
workspace-grep({ pattern: "Acme Corp", searchPath: "knowledge", fileGlob: "*.md" })
\`\`\`
**For finding files by pattern:**
\`\`\`
workspace-glob({ pattern: "**/*.md", cwd: "knowledge/People" })
\`\`\`
**IMPORTANT:**
- Use \`workspace-edit\` for updating existing notes (adding activity, updating fields)
- Use \`workspace-writeFile\` only for creating new notes
- Prefer the knowledge_index for entity resolution (it's faster than grep)
# Output
@ -119,7 +156,7 @@ Either:
Read the source file and determine if it's a meeting or email.
\`\`\`
executeCommand("cat '{source_file}'")
workspace-readFile({ path: "{source_file}" })
\`\`\`
**Meeting indicators:**
@ -350,7 +387,7 @@ If processing, continue to Step 2.
# Step 2: Read and Parse Source File
\`\`\`
executeCommand("cat '{source_file}'")
workspace-readFile({ path: "{source_file}" })
\`\`\`
Extract metadata:
@ -447,7 +484,7 @@ From index, find matches for:
Only read the full note content when you need details not in the index (e.g., activity logs, open items):
\`\`\`bash
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
workspace-readFile({ path: "{knowledge_folder}/People/Sarah Chen.md" })
\`\`\`
**Why read these notes:**
@ -531,27 +568,27 @@ Resolution Map:
When multiple candidates match a variant, disambiguate:
**By organization (strongest signal):**
\`\`\`bash
\`\`\`
# "David" could be David Kim or David Chen
executeCommand("grep -i 'Acme' '{knowledge_folder}/People/David Kim.md'")
workspace-grep({ pattern: "Acme", searchPath: "{knowledge_folder}/People/David Kim.md" })
# Output: **Organization:** [[Acme Corp]]
executeCommand("grep -i 'Acme' '{knowledge_folder}/People/David Chen.md'")
workspace-grep({ pattern: "Acme", searchPath: "{knowledge_folder}/People/David Chen.md" })
# Output: **Organization:** [[Other Corp]]
# Source is from Acme context "David" = "David Kim"
\`\`\`
**By email (definitive):**
\`\`\`bash
executeCommand("grep -i 'david@acme.com' '{knowledge_folder}/People/David Kim.md'")
\`\`\`
workspace-grep({ pattern: "david@acme.com", searchPath: "{knowledge_folder}/People/David Kim.md" })
# Exact email match is definitive
\`\`\`
**By role:**
\`\`\`bash
\`\`\`
# Source mentions "their CTO"
executeCommand("grep -r -i 'Role.*CTO' '{knowledge_folder}/People/'")
workspace-grep({ pattern: "Role.*CTO", searchPath: "{knowledge_folder}/People" })
# Filter results by organization context
\`\`\`
@ -888,8 +925,8 @@ STATE CHANGES:
Before writing, compare extracted content against existing notes.
## Check Activity Log
\`\`\`bash
executeCommand("grep '2025-01-15' '{knowledge_folder}/People/Sarah Chen.md'")
\`\`\`
workspace-grep({ pattern: "2025-01-15", searchPath: "{knowledge_folder}/People/Sarah Chen.md" })
\`\`\`
If an entry for this date/source already exists, this may have been processed. Skip or verify different interaction.
@ -919,28 +956,28 @@ If new info contradicts existing:
**IMPORTANT: Write sequentially, one file at a time.**
- Generate content for exactly one note.
- Issue exactly one \`write\` command.
- Issue exactly one write/edit command.
- Wait for the tool to return before generating the next note.
- Do NOT batch multiple \`write\` commands in a single response.
- Do NOT batch multiple write commands in a single response.
**For new entities (meetings and qualifying emails):**
\`\`\`bash
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
**For NEW entities (use workspace-writeFile):**
\`\`\`
workspace-writeFile({
path: "{knowledge_folder}/People/Jennifer.md",
data: "# Jennifer\\n\\n## Summary\\n..."
})
\`\`\`
**For existing entities:**
- Read current content first
- Add activity entry at TOP of Activity section (reverse chronological)
- Update "Last seen" date
- Add new key facts (skip duplicates)
- Add new open items
- Add new decisions
- Add new relationships
- Update summary ONLY if significant new understanding
\`\`\`bash
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
# ... modify content ...
executeCommand("write '{knowledge_folder}/People/Sarah Chen.md' '{full_updated_content}'")
**For EXISTING entities (use workspace-edit):**
- Read current content first with workspace-readFile
- Use workspace-edit to add activity entry at TOP (reverse chronological)
- Update fields using targeted edits
\`\`\`
workspace-edit({
path: "{knowledge_folder}/People/Sarah Chen.md",
oldString: "## Activity\\n",
newString: "## Activity\\n- **2026-02-03** (meeting): Met to discuss project timeline\\n"
})
\`\`\`
## 9b: Apply State Changes