mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 17:06:23 +02:00
feat: voice notes with instant transcription and knowledge graph integration
- Voice memos now create notes immediately in knowledge/Voice Memos/<date>/ - Transcription shows directly in the note (Recording... → Transcribing... → transcript) - Graph builder processes voice memos from knowledge directory - Note creation agents now use workspace tools (writeFile, edit, grep, glob) instead of executeCommand - Removes executeCommand dependency - no more permission prompts blocking knowledge updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3e2ed4cbc4
commit
d7b84f87d0
6 changed files with 720 additions and 142 deletions
|
|
@ -1461,6 +1461,29 @@ function AppContent({ auth }: { auth: ReturnType<typeof useRowboatAuth> }) {
|
||||||
},
|
},
|
||||||
}), [tree, selectedPath, workspaceRoot, collectDirPaths])
|
}), [tree, selectedPath, workspaceRoot, collectDirPaths])
|
||||||
|
|
||||||
|
// Handler for when a voice note is created/updated
|
||||||
|
const handleVoiceNoteCreated = useCallback(async (notePath: string) => {
|
||||||
|
// Refresh the tree to show the new file/folder
|
||||||
|
const newTree = await loadDirectory()
|
||||||
|
setTree(newTree)
|
||||||
|
|
||||||
|
// Expand parent directories to show the file
|
||||||
|
const parts = notePath.split('/')
|
||||||
|
const parentPaths: string[] = []
|
||||||
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
parentPaths.push(parts.slice(0, i).join('/'))
|
||||||
|
}
|
||||||
|
setExpandedPaths(prev => {
|
||||||
|
const newSet = new Set(prev)
|
||||||
|
parentPaths.forEach(p => newSet.add(p))
|
||||||
|
return newSet
|
||||||
|
})
|
||||||
|
|
||||||
|
// Select the file to show it in the editor
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setSelectedPath(notePath)
|
||||||
|
}, [loadDirectory])
|
||||||
|
|
||||||
const ensureWikiFile = useCallback(async (wikiPath: string) => {
|
const ensureWikiFile = useCallback(async (wikiPath: string) => {
|
||||||
const resolvedPath = toKnowledgePath(wikiPath)
|
const resolvedPath = toKnowledgePath(wikiPath)
|
||||||
if (!resolvedPath) return null
|
if (!resolvedPath) return null
|
||||||
|
|
@ -1707,6 +1730,7 @@ function AppContent({ auth }: { auth: ReturnType<typeof useRowboatAuth> }) {
|
||||||
expandedPaths={expandedPaths}
|
expandedPaths={expandedPaths}
|
||||||
onSelectFile={toggleExpand}
|
onSelectFile={toggleExpand}
|
||||||
knowledgeActions={knowledgeActions}
|
knowledgeActions={knowledgeActions}
|
||||||
|
onVoiceNoteCreated={handleVoiceNoteCreated}
|
||||||
runs={runs}
|
runs={runs}
|
||||||
currentRunId={runId}
|
currentRunId={runId}
|
||||||
tasksActions={{
|
tasksActions={{
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ import {
|
||||||
Folder,
|
Folder,
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
|
Mic,
|
||||||
Network,
|
Network,
|
||||||
Pencil,
|
Pencil,
|
||||||
|
Square,
|
||||||
SquarePen,
|
SquarePen,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
@ -88,6 +90,7 @@ type SidebarContentPanelProps = {
|
||||||
expandedPaths: Set<string>
|
expandedPaths: Set<string>
|
||||||
onSelectFile: (path: string, kind: "file" | "dir") => void
|
onSelectFile: (path: string, kind: "file" | "dir") => void
|
||||||
knowledgeActions: KnowledgeActions
|
knowledgeActions: KnowledgeActions
|
||||||
|
onVoiceNoteCreated?: (path: string) => void
|
||||||
runs?: RunListItem[]
|
runs?: RunListItem[]
|
||||||
currentRunId?: string | null
|
currentRunId?: string | null
|
||||||
tasksActions?: TasksActions
|
tasksActions?: TasksActions
|
||||||
|
|
@ -104,6 +107,7 @@ export function SidebarContentPanel({
|
||||||
expandedPaths,
|
expandedPaths,
|
||||||
onSelectFile,
|
onSelectFile,
|
||||||
knowledgeActions,
|
knowledgeActions,
|
||||||
|
onVoiceNoteCreated,
|
||||||
runs = [],
|
runs = [],
|
||||||
currentRunId,
|
currentRunId,
|
||||||
tasksActions,
|
tasksActions,
|
||||||
|
|
@ -126,6 +130,7 @@ export function SidebarContentPanel({
|
||||||
expandedPaths={expandedPaths}
|
expandedPaths={expandedPaths}
|
||||||
onSelectFile={onSelectFile}
|
onSelectFile={onSelectFile}
|
||||||
actions={knowledgeActions}
|
actions={knowledgeActions}
|
||||||
|
onVoiceNoteCreated={onVoiceNoteCreated}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeSection === "tasks" && (
|
{activeSection === "tasks" && (
|
||||||
|
|
@ -141,6 +146,227 @@ export function SidebarContentPanel({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function transcribeWithDeepgram(audioBlob: Blob): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const configResult = await window.ipc.invoke('workspace:readFile', {
|
||||||
|
path: 'config/deepgram.json',
|
||||||
|
encoding: 'utf8',
|
||||||
|
})
|
||||||
|
const { apiKey } = JSON.parse(configResult.data) as { apiKey: string }
|
||||||
|
if (!apiKey) throw new Error('No apiKey in deepgram.json')
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.deepgram.com/v1/listen?model=nova-2&smart_format=true',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Token ${apiKey}`,
|
||||||
|
'Content-Type': audioBlob.type,
|
||||||
|
},
|
||||||
|
body: audioBlob,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`Deepgram API error: ${response.status}`)
|
||||||
|
const result = await response.json()
|
||||||
|
return result.results?.channels?.[0]?.alternatives?.[0]?.transcript ?? null
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Deepgram transcription failed:', err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Voice Note Recording Button
|
||||||
|
function VoiceNoteButton({ onNoteCreated }: { onNoteCreated?: (path: string) => void }) {
|
||||||
|
const [isRecording, setIsRecording] = React.useState(false)
|
||||||
|
const mediaRecorderRef = React.useRef<MediaRecorder | null>(null)
|
||||||
|
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 {
|
||||||
|
// Generate timestamp and paths immediately
|
||||||
|
const now = new Date()
|
||||||
|
const timestamp = now.toISOString().replace(/[:.]/g, '-')
|
||||||
|
const dateStr = now.toISOString().split('T')[0] // YYYY-MM-DD
|
||||||
|
const noteName = `voice-memo-${timestamp}`
|
||||||
|
const notePath = `knowledge/Voice Memos/${dateStr}/${noteName}.md`
|
||||||
|
|
||||||
|
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', {
|
||||||
|
path: `knowledge/Voice Memos/${dateStr}`,
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialContent = `# Voice Memo
|
||||||
|
|
||||||
|
**Type:** voice memo
|
||||||
|
**Recorded:** ${now.toLocaleString()}
|
||||||
|
**Path:** ${relativePath}
|
||||||
|
|
||||||
|
## Transcript
|
||||||
|
|
||||||
|
*Recording in progress...*
|
||||||
|
`
|
||||||
|
await window.ipc.invoke('workspace:writeFile', {
|
||||||
|
path: notePath,
|
||||||
|
data: initialContent,
|
||||||
|
opts: { encoding: 'utf8' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Select the note so the user can see it
|
||||||
|
onNoteCreated?.(notePath)
|
||||||
|
|
||||||
|
// Start actual recording
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
|
const mimeType = MediaRecorder.isTypeSupported('audio/mp4')
|
||||||
|
? 'audio/mp4'
|
||||||
|
: 'audio/webm'
|
||||||
|
const recorder = new MediaRecorder(stream, { mimeType })
|
||||||
|
chunksRef.current = []
|
||||||
|
|
||||||
|
recorder.ondataavailable = (e) => {
|
||||||
|
if (e.data.size > 0) chunksRef.current.push(e.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder.onstop = async () => {
|
||||||
|
stream.getTracks().forEach((t) => t.stop())
|
||||||
|
const blob = new Blob(chunksRef.current, { type: mimeType })
|
||||||
|
const ext = mimeType === 'audio/mp4' ? 'm4a' : 'webm'
|
||||||
|
const audioFilename = `voice-memo-${timestampRef.current}.${ext}`
|
||||||
|
|
||||||
|
// Save audio file to voice_memos folder (for backup/reference)
|
||||||
|
try {
|
||||||
|
await window.ipc.invoke('workspace:mkdir', {
|
||||||
|
path: 'voice_memos',
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const arrayBuffer = await blob.arrayBuffer()
|
||||||
|
const base64 = btoa(
|
||||||
|
new Uint8Array(arrayBuffer).reduce(
|
||||||
|
(data, byte) => data + String.fromCharCode(byte),
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await window.ipc.invoke('workspace:writeFile', {
|
||||||
|
path: `voice_memos/${audioFilename}`,
|
||||||
|
data: base64,
|
||||||
|
opts: { encoding: 'base64' },
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to save audio file')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update note to show transcribing status
|
||||||
|
const currentNotePath = notePathRef.current
|
||||||
|
const currentRelativePath = relativePathRef.current
|
||||||
|
if (currentNotePath && currentRelativePath) {
|
||||||
|
const transcribingContent = `# Voice Memo
|
||||||
|
|
||||||
|
**Type:** voice memo
|
||||||
|
**Recorded:** ${new Date().toLocaleString()}
|
||||||
|
**Path:** ${currentRelativePath}
|
||||||
|
|
||||||
|
## Transcript
|
||||||
|
|
||||||
|
*Transcribing...*
|
||||||
|
`
|
||||||
|
await window.ipc.invoke('workspace:writeFile', {
|
||||||
|
path: currentNotePath,
|
||||||
|
data: transcribingContent,
|
||||||
|
opts: { encoding: 'utf8' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transcribe and update the note with the transcript
|
||||||
|
const transcript = await transcribeWithDeepgram(blob)
|
||||||
|
if (currentNotePath && currentRelativePath) {
|
||||||
|
const finalContent = transcript
|
||||||
|
? `# Voice Memo
|
||||||
|
|
||||||
|
**Type:** voice memo
|
||||||
|
**Recorded:** ${new Date().toLocaleString()}
|
||||||
|
**Path:** ${currentRelativePath}
|
||||||
|
|
||||||
|
## Transcript
|
||||||
|
|
||||||
|
${transcript}
|
||||||
|
`
|
||||||
|
: `# Voice Memo
|
||||||
|
|
||||||
|
**Type:** voice memo
|
||||||
|
**Recorded:** ${new Date().toLocaleString()}
|
||||||
|
**Path:** ${currentRelativePath}
|
||||||
|
|
||||||
|
## Transcript
|
||||||
|
|
||||||
|
*Transcription failed. Please try again.*
|
||||||
|
`
|
||||||
|
await window.ipc.invoke('workspace:writeFile', {
|
||||||
|
path: currentNotePath,
|
||||||
|
data: finalContent,
|
||||||
|
opts: { encoding: 'utf8' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Re-select to trigger refresh
|
||||||
|
onNoteCreated?.(currentNotePath)
|
||||||
|
|
||||||
|
if (transcript) {
|
||||||
|
toast('Voice note transcribed', 'success')
|
||||||
|
} else {
|
||||||
|
toast('Transcription failed', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder.start()
|
||||||
|
mediaRecorderRef.current = recorder
|
||||||
|
setIsRecording(true)
|
||||||
|
toast('Recording started', 'success')
|
||||||
|
} catch {
|
||||||
|
toast('Could not access microphone', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopRecording = () => {
|
||||||
|
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
|
||||||
|
mediaRecorderRef.current.stop()
|
||||||
|
}
|
||||||
|
mediaRecorderRef.current = null
|
||||||
|
setIsRecording(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={isRecording ? stopRecording : startRecording}
|
||||||
|
className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
|
||||||
|
>
|
||||||
|
{isRecording ? (
|
||||||
|
<Square className="size-4 fill-red-500 text-red-500 animate-pulse" />
|
||||||
|
) : (
|
||||||
|
<Mic className="size-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
{isRecording ? 'Stop Recording' : 'New Voice Note'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Knowledge Section
|
// Knowledge Section
|
||||||
function KnowledgeSection({
|
function KnowledgeSection({
|
||||||
tree,
|
tree,
|
||||||
|
|
@ -148,15 +374,17 @@ function KnowledgeSection({
|
||||||
expandedPaths,
|
expandedPaths,
|
||||||
onSelectFile,
|
onSelectFile,
|
||||||
actions,
|
actions,
|
||||||
|
onVoiceNoteCreated,
|
||||||
}: {
|
}: {
|
||||||
tree: TreeNode[]
|
tree: TreeNode[]
|
||||||
selectedPath: string | null
|
selectedPath: string | null
|
||||||
expandedPaths: Set<string>
|
expandedPaths: Set<string>
|
||||||
onSelectFile: (path: string, kind: "file" | "dir") => void
|
onSelectFile: (path: string, kind: "file" | "dir") => void
|
||||||
actions: KnowledgeActions
|
actions: KnowledgeActions
|
||||||
|
onVoiceNoteCreated?: (path: string) => void
|
||||||
}) {
|
}) {
|
||||||
const isExpanded = expandedPaths.size > 0
|
const isExpanded = expandedPaths.size > 0
|
||||||
|
|
||||||
const quickActions = [
|
const quickActions = [
|
||||||
{ icon: FilePlus, label: "New Note", action: () => actions.createNote() },
|
{ icon: FilePlus, label: "New Note", action: () => actions.createNote() },
|
||||||
{ icon: FolderPlus, label: "New Folder", action: () => actions.createFolder() },
|
{ icon: FolderPlus, label: "New Folder", action: () => actions.createFolder() },
|
||||||
|
|
@ -181,6 +409,7 @@ function KnowledgeSection({
|
||||||
<TooltipContent side="bottom">{action.label}</TooltipContent>
|
<TooltipContent side="bottom">{action.label}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
|
<VoiceNoteButton onNoteCreated={onVoiceNoteCreated} />
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,89 @@ const SOURCE_FOLDERS = [
|
||||||
'fireflies_transcripts',
|
'fireflies_transcripts',
|
||||||
'granola_notes',
|
'granola_notes',
|
||||||
];
|
];
|
||||||
const MAX_CONCURRENT_BATCHES = 1; // Process only 1 batch at a time to avoid overwhelming the agent
|
|
||||||
|
// Voice memos are now created directly in knowledge/Voice Memos/<date>/
|
||||||
|
const VOICE_MEMOS_KNOWLEDGE_DIR = path.join(NOTES_OUTPUT_DIR, 'Voice Memos');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 getUnprocessedVoiceMemos(state: GraphState): string[] {
|
||||||
|
console.log(`[GraphBuilder] Checking directory: ${VOICE_MEMOS_KNOWLEDGE_DIR}`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(VOICE_MEMOS_KNOWLEDGE_DIR)) {
|
||||||
|
console.log(`[GraphBuilder] Directory does not exist`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const unprocessedFiles: string[] = [];
|
||||||
|
|
||||||
|
// Scan date folders (e.g., 2026-02-03)
|
||||||
|
const dateFolders = fs.readdirSync(VOICE_MEMOS_KNOWLEDGE_DIR);
|
||||||
|
console.log(`[GraphBuilder] 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(`[GraphBuilder] Error checking ${dateFolderPath}:`, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan markdown files in this date folder
|
||||||
|
const files = fs.readdirSync(dateFolderPath);
|
||||||
|
console.log(`[GraphBuilder] Found ${files.length} files in ${dateFolder}: ${files.join(', ')}`);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Only process voice memo markdown files
|
||||||
|
if (!file.endsWith('.md') || !file.startsWith('voice-memo-')) {
|
||||||
|
console.log(`[GraphBuilder] Skipping ${file} - not a voice memo file`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(dateFolderPath, file);
|
||||||
|
|
||||||
|
// Skip if already processed
|
||||||
|
if (state.processedFiles[filePath]) {
|
||||||
|
console.log(`[GraphBuilder] 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(`[GraphBuilder] Skipping ${file} - still recording`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (content.includes('*Transcribing...*')) {
|
||||||
|
console.log(`[GraphBuilder] Skipping ${file} - still transcribing`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (content.includes('*Transcription failed')) {
|
||||||
|
console.log(`[GraphBuilder] Skipping ${file} - transcription failed`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log(`[GraphBuilder] Found unprocessed voice memo: ${file}`);
|
||||||
|
unprocessedFiles.push(filePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[GraphBuilder] Error reading ${file}:`, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[GraphBuilder] Total unprocessed files: ${unprocessedFiles.length}`);
|
||||||
|
return unprocessedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read content for specific files
|
* Read content for specific files
|
||||||
|
|
@ -186,6 +268,69 @@ export async function buildGraph(sourceDir: string): Promise<void> {
|
||||||
console.log(`Knowledge graph build complete. Processed ${processedFiles.length} files.`);
|
console.log(`Knowledge graph build complete. Processed ${processedFiles.length} files.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(`[GraphBuilder] Starting voice memo processing...`);
|
||||||
|
const state = loadState();
|
||||||
|
|
||||||
|
// Get unprocessed voice memos from knowledge/Voice Memos/
|
||||||
|
const unprocessedFiles = getUnprocessedVoiceMemos(state);
|
||||||
|
|
||||||
|
if (unprocessedFiles.length === 0) {
|
||||||
|
console.log(`[GraphBuilder] No unprocessed voice memos found`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[GraphBuilder] Processing ${unprocessedFiles.length} voice memo transcripts for entity extraction...`);
|
||||||
|
console.log(`[GraphBuilder] Files to process: ${unprocessedFiles.map(f => path.basename(f)).join(', ')}`);
|
||||||
|
|
||||||
|
// Read the files
|
||||||
|
const contentFiles = await readFileContents(unprocessedFiles);
|
||||||
|
|
||||||
|
if (contentFiles.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process in batches like other sources
|
||||||
|
const BATCH_SIZE = 10;
|
||||||
|
const totalBatches = Math.ceil(contentFiles.length / BATCH_SIZE);
|
||||||
|
|
||||||
|
for (let i = 0; i < contentFiles.length; i += BATCH_SIZE) {
|
||||||
|
const batch = contentFiles.slice(i, i + BATCH_SIZE);
|
||||||
|
const batchNumber = Math.floor(i / BATCH_SIZE) + 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build knowledge index
|
||||||
|
console.log(`[GraphBuilder] Building knowledge index for batch ${batchNumber}...`);
|
||||||
|
const index = buildKnowledgeIndex();
|
||||||
|
const indexForPrompt = formatIndexForPrompt(index);
|
||||||
|
|
||||||
|
console.log(`[GraphBuilder] Processing batch ${batchNumber}/${totalBatches} (${batch.length} files)...`);
|
||||||
|
await createNotesFromBatch(batch, batchNumber, indexForPrompt);
|
||||||
|
console.log(`[GraphBuilder] Batch ${batchNumber}/${totalBatches} complete`);
|
||||||
|
|
||||||
|
// Mark files as processed
|
||||||
|
for (const file of batch) {
|
||||||
|
markFileAsProcessed(file.path, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save state after each batch
|
||||||
|
saveState(state);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[GraphBuilder] Error processing batch ${batchNumber}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last build time
|
||||||
|
state.lastBuildTime = new Date().toISOString();
|
||||||
|
saveState(state);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process all configured source directories
|
* Process all configured source directories
|
||||||
*/
|
*/
|
||||||
|
|
@ -197,6 +342,16 @@ async function processAllSources(): Promise<void> {
|
||||||
|
|
||||||
let anyFilesProcessed = false;
|
let anyFilesProcessed = false;
|
||||||
|
|
||||||
|
// Process voice memos first (they get moved to knowledge/)
|
||||||
|
try {
|
||||||
|
const voiceMemosProcessed = await processVoiceMemosForKnowledge();
|
||||||
|
if (voiceMemosProcessed) {
|
||||||
|
anyFilesProcessed = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[GraphBuilder] Error processing voice memos:', error);
|
||||||
|
}
|
||||||
|
|
||||||
for (const folder of SOURCE_FOLDERS) {
|
for (const folder of SOURCE_FOLDERS) {
|
||||||
const sourceDir = path.join(WorkDir, folder);
|
const sourceDir = path.join(WorkDir, folder);
|
||||||
|
|
||||||
|
|
@ -234,7 +389,7 @@ async function processAllSources(): Promise<void> {
|
||||||
*/
|
*/
|
||||||
export async function init() {
|
export async function init() {
|
||||||
console.log('[GraphBuilder] Starting Knowledge Graph Builder Service...');
|
console.log('[GraphBuilder] Starting Knowledge Graph Builder Service...');
|
||||||
console.log(`[GraphBuilder] Monitoring folders: ${SOURCE_FOLDERS.join(', ')}`);
|
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`);
|
console.log(`[GraphBuilder] Will check for new content every ${SYNC_INTERVAL_MS / 1000} seconds`);
|
||||||
|
|
||||||
// Initial run
|
// Initial run
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,25 @@ tools:
|
||||||
workspace-readFile:
|
workspace-readFile:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: workspace-readFile
|
name: workspace-readFile
|
||||||
|
workspace-edit:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-edit
|
||||||
workspace-readdir:
|
workspace-readdir:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: workspace-readdir
|
name: workspace-readdir
|
||||||
workspace-mkdir:
|
workspace-mkdir:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: workspace-mkdir
|
name: workspace-mkdir
|
||||||
executeCommand:
|
workspace-grep:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: executeCommand
|
name: workspace-grep
|
||||||
|
workspace-glob:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-glob
|
||||||
---
|
---
|
||||||
# Task
|
# Task
|
||||||
|
|
||||||
You are a memory agent. Given a single source file (email or meeting transcript), you will:
|
You are a memory agent. Given a single source file (email, meeting transcript, or voice memo), you will:
|
||||||
|
|
||||||
1. **Determine source type (meeting or email)**
|
1. **Determine source type (meeting or email)**
|
||||||
2. **Evaluate if the source is worth processing**
|
2. **Evaluate if the source is worth processing**
|
||||||
|
|
@ -31,7 +37,7 @@ You are a memory agent. Given a single source file (email or meeting transcript)
|
||||||
8. Create new notes (meetings only) or update existing notes
|
8. Create new notes (meetings only) or update existing notes
|
||||||
9. **Apply state changes to existing notes**
|
9. **Apply state changes to existing notes**
|
||||||
|
|
||||||
The core rule: **Meetings create notes. Emails enrich them.**
|
The core rule: **Meetings and voice memos create notes. Emails enrich them.**
|
||||||
|
|
||||||
You have full read access to the existing knowledge directory. Use this extensively to:
|
You have full read access to the existing knowledge directory. Use this extensively to:
|
||||||
- Find existing notes for people, organizations, projects mentioned
|
- Find existing notes for people, organizations, projects mentioned
|
||||||
|
|
@ -70,20 +76,51 @@ When you need to:
|
||||||
|
|
||||||
# Tools Available
|
# 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
|
workspace-readFile({ path: "knowledge/People/Sarah Chen.md" })
|
||||||
executeCommand("cat {path}") # Read file contents
|
|
||||||
executeCommand("head -50 {path}") # Read first 50 lines
|
|
||||||
executeCommand("write {path} {content}") # Create or overwrite file
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**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
|
# Output
|
||||||
|
|
||||||
|
|
@ -113,7 +150,7 @@ Either:
|
||||||
|
|
||||||
Read the source file and determine if it's a meeting or email.
|
Read the source file and determine if it's a meeting or email.
|
||||||
\`\`\`
|
\`\`\`
|
||||||
executeCommand("cat '{source_file}'")
|
workspace-readFile({ path: "{source_file}" })
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Meeting indicators:**
|
**Meeting indicators:**
|
||||||
|
|
@ -126,9 +163,15 @@ executeCommand("cat '{source_file}'")
|
||||||
- Has \`Subject:\` field
|
- Has \`Subject:\` field
|
||||||
- Email signature
|
- Email signature
|
||||||
|
|
||||||
|
**Voice memo indicators:**
|
||||||
|
- Has \`**Type:** voice memo\` field
|
||||||
|
- Has \`**Path:**\` field with path like \`Voice Memos/YYYY-MM-DD/...\`
|
||||||
|
- Has \`## Transcript\` section
|
||||||
|
|
||||||
**Set processing mode:**
|
**Set processing mode:**
|
||||||
- \`source_type = "meeting"\` → Can create new notes
|
- \`source_type = "meeting"\` → Can create new notes
|
||||||
- \`source_type = "email"\` → Can only update existing notes
|
- \`source_type = "email"\` → Can only update existing notes
|
||||||
|
- \`source_type = "voice_memo"\` → Can create new notes (treat like meetings)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -301,9 +344,9 @@ If someone only appears in your memory as "CC'd on outreach emails from [Sender]
|
||||||
## Email-Specific Filtering
|
## Email-Specific Filtering
|
||||||
|
|
||||||
For emails, check if sender/recipients have existing notes:
|
For emails, check if sender/recipients have existing notes:
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
executeCommand("grep -r -i -l '{sender email}' '{knowledge_folder}/'")
|
workspace-grep({ pattern: "{sender email}", searchPath: "{knowledge_folder}" })
|
||||||
executeCommand("grep -r -i -l '{sender name}' '{knowledge_folder}/People/'")
|
workspace-grep({ pattern: "{sender name}", searchPath: "{knowledge_folder}/People" })
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**If no existing note found:**
|
**If no existing note found:**
|
||||||
|
|
@ -343,7 +386,7 @@ If processing, continue to Step 2.
|
||||||
|
|
||||||
# Step 2: Read and Parse Source File
|
# Step 2: Read and Parse Source File
|
||||||
\`\`\`
|
\`\`\`
|
||||||
executeCommand("cat '{source_file}'")
|
workspace-readFile({ path: "{source_file}" })
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Extract metadata:
|
Extract metadata:
|
||||||
|
|
@ -440,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):
|
Only read the full note content when you need details not in the index (e.g., activity logs, open items):
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
|
workspace-readFile({ path: "{knowledge_folder}/People/Sarah Chen.md" })
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Why read these notes:**
|
**Why read these notes:**
|
||||||
|
|
@ -524,27 +567,27 @@ Resolution Map:
|
||||||
When multiple candidates match a variant, disambiguate:
|
When multiple candidates match a variant, disambiguate:
|
||||||
|
|
||||||
**By organization (strongest signal):**
|
**By organization (strongest signal):**
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
# "David" could be David Kim or David Chen
|
# "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]]
|
# 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]]
|
# Output: **Organization:** [[Other Corp]]
|
||||||
|
|
||||||
# Source is from Acme context → "David" = "David Kim"
|
# Source is from Acme context → "David" = "David Kim"
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**By email (definitive):**
|
**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
|
# Exact email match is definitive
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**By role:**
|
**By role:**
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
# Source mentions "their CTO"
|
# 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
|
# Filter results by organization context
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
|
@ -844,7 +887,12 @@ If role is unknown but context suggests it, say so:
|
||||||
|
|
||||||
One line summarizing this source's relevance to the entity:
|
One line summarizing this source's relevance to the entity:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
**{YYYY-MM-DD}** ({meeting|email}): {Summary with [[links]]}
|
**{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[links]]}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**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]]
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Important:** Use canonical names with absolute paths from resolution map in all summaries:
|
**Important:** Use canonical names with absolute paths from resolution map in all summaries:
|
||||||
|
|
@ -968,8 +1016,8 @@ STATE CHANGES:
|
||||||
Before writing, compare extracted content against existing notes.
|
Before writing, compare extracted content against existing notes.
|
||||||
|
|
||||||
## Check Activity Log
|
## 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.
|
If an entry for this date/source already exists, this may have been processed. Skip or verify different interaction.
|
||||||
|
|
@ -999,28 +1047,28 @@ If new info contradicts existing:
|
||||||
|
|
||||||
**IMPORTANT: Write sequentially, one file at a time.**
|
**IMPORTANT: Write sequentially, one file at a time.**
|
||||||
- Generate content for exactly one note.
|
- 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.
|
- 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):**
|
**For NEW entities (use workspace-writeFile):**
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
workspace-writeFile({
|
||||||
|
path: "{knowledge_folder}/People/Jennifer.md",
|
||||||
|
data: "# Jennifer\\n\\n## Summary\\n..."
|
||||||
|
})
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**For existing entities:**
|
**For EXISTING entities (use workspace-edit):**
|
||||||
- Read current content first
|
- Read current content first with workspace-readFile
|
||||||
- Add activity entry at TOP of Activity section (reverse chronological)
|
- Use workspace-edit to add activity entry at TOP (reverse chronological)
|
||||||
- Update "Last seen" date
|
- Update fields using targeted edits
|
||||||
- Add new key facts (skip duplicates)
|
\`\`\`
|
||||||
- Add new open items
|
workspace-edit({
|
||||||
- Add new decisions
|
path: "{knowledge_folder}/People/Sarah Chen.md",
|
||||||
- Add new relationships
|
oldString: "## Activity\\n",
|
||||||
- Update summary ONLY if significant new understanding
|
newString: "## Activity\\n- **2026-02-03** (meeting): Met to discuss project timeline\\n"
|
||||||
\`\`\`bash
|
})
|
||||||
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
|
|
||||||
# ... modify content ...
|
|
||||||
executeCommand("write '{knowledge_folder}/People/Sarah Chen.md' '{full_updated_content}'")
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## 9b: Emails — Update Existing Notes Only
|
## 9b: Emails — Update Existing Notes Only
|
||||||
|
|
@ -1043,7 +1091,7 @@ For each state change identified in Step 7:
|
||||||
### Update Project Status
|
### Update Project Status
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# Read current project note
|
# 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
|
# Update the Status field
|
||||||
# Change: **Status:** planning
|
# Change: **Status:** planning
|
||||||
|
|
@ -1053,7 +1101,7 @@ executeCommand("cat '{knowledge_folder}/Projects/Acme Integration.md'")
|
||||||
### Mark Open Items Complete
|
### Mark Open Items Complete
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# Read current note
|
# 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
|
# Find matching open item and update
|
||||||
# Change: - [ ] Send API documentation — by Friday
|
# Change: - [ ] Send API documentation — by Friday
|
||||||
|
|
@ -1063,7 +1111,7 @@ executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
|
||||||
### Update Role
|
### Update Role
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# Read current person note
|
# Read current person note
|
||||||
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
|
workspace-readFile({ path: "{knowledge_folder}/People/Sarah Chen.md" })
|
||||||
|
|
||||||
# Update role field
|
# Update role field
|
||||||
# Change: **Role:** Engineering Lead
|
# Change: **Role:** Engineering Lead
|
||||||
|
|
@ -1073,7 +1121,7 @@ executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
|
||||||
### Update Relationship
|
### Update Relationship
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
# Read current org note
|
# Read current org note
|
||||||
executeCommand("cat '{knowledge_folder}/Organizations/Acme Corp.md'")
|
workspace-readFile({ path: "{knowledge_folder}/Organizations/Acme Corp.md" })
|
||||||
|
|
||||||
# Update relationship field
|
# Update relationship field
|
||||||
# Change: **Relationship:** prospect
|
# Change: **Relationship:** prospect
|
||||||
|
|
@ -1138,8 +1186,8 @@ This ensures:
|
||||||
## Check Each New Link
|
## Check Each New Link
|
||||||
|
|
||||||
If you added \`[[People/Jennifer]]\` to \`Organizations/Acme Corp.md\`:
|
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.
|
If not found, update Jennifer.md to add the link.
|
||||||
|
|
@ -1179,7 +1227,7 @@ If not found, update Jennifer.md to add the link.
|
||||||
- [[Projects/{Project}]] — {role}
|
- [[Projects/{Project}]] — {role}
|
||||||
|
|
||||||
## Activity
|
## Activity
|
||||||
- **{YYYY-MM-DD}** ({meeting|email}): {Summary with [[Folder/Name]] links} {[State changes if any]}
|
- **{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[Folder/Name]] links} {[State changes if any]}
|
||||||
|
|
||||||
## Key facts
|
## Key facts
|
||||||
{Substantive facts only. Leave empty if none. Never include data gap commentary.}
|
{Substantive facts only. Leave empty if none. Never include data gap commentary.}
|
||||||
|
|
@ -1216,7 +1264,7 @@ If not found, update Jennifer.md to add the link.
|
||||||
- [[Projects/{Project}]] — {relationship}
|
- [[Projects/{Project}]] — {relationship}
|
||||||
|
|
||||||
## Activity
|
## Activity
|
||||||
- **{YYYY-MM-DD}** ({meeting|email}): {Summary with [[Folder/Name]] links} {[State changes if any]}
|
- **{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[Folder/Name]] links} {[State changes if any]}
|
||||||
|
|
||||||
## Key facts
|
## Key facts
|
||||||
{Substantive facts only. Leave empty if none.}
|
{Substantive facts only. Leave empty if none.}
|
||||||
|
|
@ -1373,11 +1421,11 @@ Not mass email, not automated. Continue.
|
||||||
- Variants: "Sarah Chen", "sarah@acme.com", "David Kim", "David", "Jennifer", "CTO", "Acme", "the pilot"
|
- Variants: "Sarah Chen", "sarah@acme.com", "David Kim", "David", "Jennifer", "CTO", "Acme", "the pilot"
|
||||||
|
|
||||||
### Step 3: Search Existing Notes
|
### Step 3: Search Existing Notes
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
executeCommand("grep -r -i -l 'Sarah Chen' 'knowledge/'")
|
workspace-grep({ pattern: "Sarah Chen", searchPath: "knowledge" })
|
||||||
# Output: (none)
|
# Output: (none)
|
||||||
|
|
||||||
executeCommand("grep -r -i -l 'acme' 'knowledge/'")
|
workspace-grep({ pattern: "acme", searchPath: "knowledge" })
|
||||||
# Output: (none)
|
# Output: (none)
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
|
@ -1516,8 +1564,8 @@ VP Engineering, Acme Corp
|
||||||
### Step 1: Filter
|
### Step 1: Filter
|
||||||
|
|
||||||
Check for existing relationship:
|
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
|
# Output: notes/People/Sarah Chen.md
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
|
@ -1652,11 +1700,11 @@ John Smith
|
||||||
### Step 1: Filter
|
### Step 1: Filter
|
||||||
|
|
||||||
Check for existing relationship:
|
Check for existing relationship:
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
executeCommand("grep -r -i -l 'randomvendor' 'knowledge/'")
|
workspace-grep({ pattern: "randomvendor", searchPath: "knowledge" })
|
||||||
# Output: (none)
|
# Output: (none)
|
||||||
|
|
||||||
executeCommand("grep -r -i -l 'John Smith' 'knowledge/'")
|
workspace-grep({ pattern: "John Smith", searchPath: "knowledge" })
|
||||||
# Output: (none)
|
# Output: (none)
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
|
@ -1699,8 +1747,8 @@ David
|
||||||
### Step 1: Filter
|
### Step 1: Filter
|
||||||
|
|
||||||
Check for sender:
|
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
|
# Output: notes/People/David Park.md
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
|
@ -1803,10 +1851,16 @@ Business banking provider. Account setup completed January 2025.
|
||||||
| Source Type | Creates Notes? | Updates Notes? | Detects State Changes? |
|
| Source Type | Creates Notes? | Updates Notes? | Detects State Changes? |
|
||||||
|-------------|---------------|----------------|------------------------|
|
|-------------|---------------|----------------|------------------------|
|
||||||
| Meeting | Yes | Yes | Yes |
|
| Meeting | Yes | Yes | Yes |
|
||||||
|
| Voice memo | Yes | Yes | Yes |
|
||||||
| Email (known contact) | No | Yes | Yes |
|
| Email (known contact) | No | Yes | Yes |
|
||||||
| Email (unknown contact) | No | No (SKIP) | No |
|
| Email (unknown contact) | No | No (SKIP) | No |
|
||||||
| Email (warm intro) | Yes (exception) | Yes | Yes |
|
| Email (warm intro) | Yes (exception) | Yes | Yes |
|
||||||
|
|
||||||
|
**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-...]]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# State Change Reference
|
# State Change Reference
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,25 @@ tools:
|
||||||
workspace-readFile:
|
workspace-readFile:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: workspace-readFile
|
name: workspace-readFile
|
||||||
|
workspace-edit:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-edit
|
||||||
workspace-readdir:
|
workspace-readdir:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: workspace-readdir
|
name: workspace-readdir
|
||||||
workspace-mkdir:
|
workspace-mkdir:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: workspace-mkdir
|
name: workspace-mkdir
|
||||||
executeCommand:
|
workspace-grep:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: executeCommand
|
name: workspace-grep
|
||||||
|
workspace-glob:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-glob
|
||||||
---
|
---
|
||||||
# Task
|
# Task
|
||||||
|
|
||||||
You are a memory agent. Given a single source file (email or meeting transcript), you will:
|
You are a memory agent. Given a single source file (email, meeting transcript, or voice memo), you will:
|
||||||
|
|
||||||
1. **Determine source type (meeting or email)**
|
1. **Determine source type (meeting or email)**
|
||||||
2. **Evaluate if the source is worth processing**
|
2. **Evaluate if the source is worth processing**
|
||||||
|
|
@ -31,7 +37,7 @@ You are a memory agent. Given a single source file (email or meeting transcript)
|
||||||
8. Create new notes or update existing notes
|
8. Create new notes or update existing notes
|
||||||
9. **Apply state changes to existing notes**
|
9. **Apply state changes to existing notes**
|
||||||
|
|
||||||
The core rule: **Capture broadly. Both meetings and emails create notes for most external contacts.**
|
The core rule: **Capture broadly. Meetings, voice memos, and emails create notes for most external contacts.**
|
||||||
|
|
||||||
You have full read access to the existing knowledge directory. Use this extensively to:
|
You have full read access to the existing knowledge directory. Use this extensively to:
|
||||||
- Find existing notes for people, organizations, projects mentioned
|
- Find existing notes for people, organizations, projects mentioned
|
||||||
|
|
@ -70,20 +76,51 @@ When you need to:
|
||||||
|
|
||||||
# Tools Available
|
# 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
|
workspace-readFile({ path: "knowledge/People/Sarah Chen.md" })
|
||||||
executeCommand("cat {path}") # Read file contents
|
|
||||||
executeCommand("head -50 {path}") # Read first 50 lines
|
|
||||||
executeCommand("write {path} {content}") # Create or overwrite file
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**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
|
# 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.
|
Read the source file and determine if it's a meeting or email.
|
||||||
\`\`\`
|
\`\`\`
|
||||||
executeCommand("cat '{source_file}'")
|
workspace-readFile({ path: "{source_file}" })
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Meeting indicators:**
|
**Meeting indicators:**
|
||||||
|
|
@ -133,9 +170,15 @@ executeCommand("cat '{source_file}'")
|
||||||
- Has \`Subject:\` field
|
- Has \`Subject:\` field
|
||||||
- Email signature
|
- Email signature
|
||||||
|
|
||||||
|
**Voice memo indicators:**
|
||||||
|
- Has \`**Type:** voice memo\` field
|
||||||
|
- Has \`**Path:**\` field with path like \`Voice Memos/YYYY-MM-DD/...\`
|
||||||
|
- Has \`## Transcript\` section
|
||||||
|
|
||||||
**Set processing mode:**
|
**Set processing mode:**
|
||||||
- \`source_type = "meeting"\` → Create notes for all external attendees
|
- \`source_type = "meeting"\` → Create notes for all external attendees
|
||||||
- \`source_type = "email"\` → Create notes for sender if identifiable human
|
- \`source_type = "email"\` → Create notes for sender if identifiable human
|
||||||
|
- \`source_type = "voice_memo"\` → Create notes for all mentioned entities (treat like meetings)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -204,7 +247,7 @@ If processing, continue to Step 2.
|
||||||
|
|
||||||
# Step 2: Read and Parse Source File
|
# Step 2: Read and Parse Source File
|
||||||
\`\`\`
|
\`\`\`
|
||||||
executeCommand("cat '{source_file}'")
|
workspace-readFile({ path: "{source_file}" })
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Extract metadata:
|
Extract metadata:
|
||||||
|
|
@ -291,8 +334,8 @@ From index, find matches for:
|
||||||
## 3d: Read Full Notes When Needed
|
## 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):
|
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:**
|
**Why read these notes:**
|
||||||
|
|
@ -509,7 +552,12 @@ Write 2-3 sentences covering their role/function, context of the relationship, a
|
||||||
|
|
||||||
One line summarizing this source's relevance to the entity:
|
One line summarizing this source's relevance to the entity:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
**{YYYY-MM-DD}** ({meeting|email}): {Summary with [[links]]}
|
**{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[links]]}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**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]]
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -555,21 +603,29 @@ Before writing:
|
||||||
|
|
||||||
**IMPORTANT: Write sequentially, one file at a time.**
|
**IMPORTANT: Write sequentially, one file at a time.**
|
||||||
- Generate content for exactly one note.
|
- 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.
|
- 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:**
|
**For NEW entities (use workspace-writeFile):**
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
workspace-writeFile({
|
||||||
|
path: "{knowledge_folder}/People/Jennifer.md",
|
||||||
|
data: "# Jennifer\\n\\n## Summary\\n..."
|
||||||
|
})
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**For existing entities:**
|
**For EXISTING entities (use workspace-edit):**
|
||||||
- Read current content first
|
- Read current content first with workspace-readFile
|
||||||
- Add activity entry at TOP (reverse chronological)
|
- Use workspace-edit to add activity entry at TOP (reverse chronological)
|
||||||
- Update "Last seen" date
|
- Update fields using targeted edits
|
||||||
- Add new key facts (skip duplicates)
|
\`\`\`
|
||||||
- Add new open items
|
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
|
## 9b: Apply State Changes
|
||||||
|
|
||||||
|
|
@ -638,7 +694,7 @@ After writing, verify links go both ways.
|
||||||
- [[Projects/{Project}]] — {role}
|
- [[Projects/{Project}]] — {role}
|
||||||
|
|
||||||
## Activity
|
## Activity
|
||||||
- **{YYYY-MM-DD}** ({meeting|email}): {Summary with [[Folder/Name]] links}
|
- **{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[Folder/Name]] links}
|
||||||
|
|
||||||
## Key facts
|
## Key facts
|
||||||
{Substantive facts only. Leave empty if none.}
|
{Substantive facts only. Leave empty if none.}
|
||||||
|
|
@ -673,7 +729,7 @@ After writing, verify links go both ways.
|
||||||
- [[Projects/{Project}]] — {relationship}
|
- [[Projects/{Project}]] — {relationship}
|
||||||
|
|
||||||
## Activity
|
## Activity
|
||||||
- **{YYYY-MM-DD}** ({meeting|email}): {Summary}
|
- **{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary}
|
||||||
|
|
||||||
## Key facts
|
## Key facts
|
||||||
{Substantive facts only. Leave empty if none.}
|
{Substantive facts only. Leave empty if none.}
|
||||||
|
|
@ -705,7 +761,7 @@ After writing, verify links go both ways.
|
||||||
- [[Topics/{Topic}]] — {relationship}
|
- [[Topics/{Topic}]] — {relationship}
|
||||||
|
|
||||||
## Timeline
|
## Timeline
|
||||||
**{YYYY-MM-DD}** ({meeting|email})
|
**{YYYY-MM-DD}** ({meeting|email|voice memo})
|
||||||
{What happened.}
|
{What happened.}
|
||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
@ -756,9 +812,15 @@ After writing, verify links go both ways.
|
||||||
| Source Type | Creates Notes? | Updates Notes? | Detects State Changes? |
|
| Source Type | Creates Notes? | Updates Notes? | Detects State Changes? |
|
||||||
|-------------|---------------|----------------|------------------------|
|
|-------------|---------------|----------------|------------------------|
|
||||||
| Meeting | Yes — ALL external attendees | Yes | Yes |
|
| Meeting | Yes — ALL external attendees | Yes | Yes |
|
||||||
|
| Voice memo | Yes — all mentioned entities | Yes | Yes |
|
||||||
| Email (any human sender) | Yes | Yes | Yes |
|
| Email (any human sender) | Yes | Yes | Yes |
|
||||||
| Email (automated/newsletter) | No (SKIP) | No | No |
|
| Email (automated/newsletter) | No (SKIP) | No | No |
|
||||||
|
|
||||||
|
**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-...]]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
**Philosophy:** Capture broadly, filter later if needed.
|
**Philosophy:** Capture broadly, filter later if needed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,25 @@ tools:
|
||||||
workspace-readFile:
|
workspace-readFile:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: workspace-readFile
|
name: workspace-readFile
|
||||||
|
workspace-edit:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-edit
|
||||||
workspace-readdir:
|
workspace-readdir:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: workspace-readdir
|
name: workspace-readdir
|
||||||
workspace-mkdir:
|
workspace-mkdir:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: workspace-mkdir
|
name: workspace-mkdir
|
||||||
executeCommand:
|
workspace-grep:
|
||||||
type: builtin
|
type: builtin
|
||||||
name: executeCommand
|
name: workspace-grep
|
||||||
|
workspace-glob:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-glob
|
||||||
---
|
---
|
||||||
# Task
|
# Task
|
||||||
|
|
||||||
You are a memory agent. Given a single source file (email or meeting transcript), you will:
|
You are a memory agent. Given a single source file (email, meeting transcript, or voice memo), you will:
|
||||||
|
|
||||||
1. **Determine source type (meeting or email)**
|
1. **Determine source type (meeting or email)**
|
||||||
2. **Evaluate if the source is worth processing**
|
2. **Evaluate if the source is worth processing**
|
||||||
|
|
@ -70,20 +76,51 @@ When you need to:
|
||||||
|
|
||||||
# Tools Available
|
# 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
|
workspace-readFile({ path: "knowledge/People/Sarah Chen.md" })
|
||||||
executeCommand("cat {path}") # Read file contents
|
|
||||||
executeCommand("head -50 {path}") # Read first 50 lines
|
|
||||||
executeCommand("write {path} {content}") # Create or overwrite file
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**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
|
# Output
|
||||||
|
|
||||||
|
|
@ -119,7 +156,7 @@ Either:
|
||||||
|
|
||||||
Read the source file and determine if it's a meeting or email.
|
Read the source file and determine if it's a meeting or email.
|
||||||
\`\`\`
|
\`\`\`
|
||||||
executeCommand("cat '{source_file}'")
|
workspace-readFile({ path: "{source_file}" })
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Meeting indicators:**
|
**Meeting indicators:**
|
||||||
|
|
@ -132,9 +169,15 @@ executeCommand("cat '{source_file}'")
|
||||||
- Has \`Subject:\` field
|
- Has \`Subject:\` field
|
||||||
- Email signature
|
- Email signature
|
||||||
|
|
||||||
|
**Voice memo indicators:**
|
||||||
|
- Has \`**Type:** voice memo\` field
|
||||||
|
- Has \`**Path:**\` field with path like \`Voice Memos/YYYY-MM-DD/...\`
|
||||||
|
- Has \`## Transcript\` section
|
||||||
|
|
||||||
**Set processing mode:**
|
**Set processing mode:**
|
||||||
- \`source_type = "meeting"\` → Can create new notes
|
- \`source_type = "meeting"\` → Can create new notes
|
||||||
- \`source_type = "email"\` → Can create notes if personalized and relevant
|
- \`source_type = "email"\` → Can create notes if personalized and relevant
|
||||||
|
- \`source_type = "voice_memo"\` → Can create new notes (treat like meetings)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -344,7 +387,7 @@ If processing, continue to Step 2.
|
||||||
|
|
||||||
# Step 2: Read and Parse Source File
|
# Step 2: Read and Parse Source File
|
||||||
\`\`\`
|
\`\`\`
|
||||||
executeCommand("cat '{source_file}'")
|
workspace-readFile({ path: "{source_file}" })
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Extract metadata:
|
Extract metadata:
|
||||||
|
|
@ -441,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):
|
Only read the full note content when you need details not in the index (e.g., activity logs, open items):
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
|
workspace-readFile({ path: "{knowledge_folder}/People/Sarah Chen.md" })
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Why read these notes:**
|
**Why read these notes:**
|
||||||
|
|
@ -525,27 +568,27 @@ Resolution Map:
|
||||||
When multiple candidates match a variant, disambiguate:
|
When multiple candidates match a variant, disambiguate:
|
||||||
|
|
||||||
**By organization (strongest signal):**
|
**By organization (strongest signal):**
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
# "David" could be David Kim or David Chen
|
# "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]]
|
# 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]]
|
# Output: **Organization:** [[Other Corp]]
|
||||||
|
|
||||||
# Source is from Acme context → "David" = "David Kim"
|
# Source is from Acme context → "David" = "David Kim"
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**By email (definitive):**
|
**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
|
# Exact email match is definitive
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**By role:**
|
**By role:**
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
# Source mentions "their CTO"
|
# 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
|
# Filter results by organization context
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
|
@ -784,7 +827,12 @@ The summary should answer: **"Who is this person and why do I know them?"**
|
||||||
|
|
||||||
One line summarizing this source's relevance to the entity:
|
One line summarizing this source's relevance to the entity:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
**{YYYY-MM-DD}** ({meeting|email}): {Summary with [[links]]}
|
**{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[links]]}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**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]]
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Important:** Use canonical names with absolute paths from resolution map in all summaries:
|
**Important:** Use canonical names with absolute paths from resolution map in all summaries:
|
||||||
|
|
@ -877,8 +925,8 @@ STATE CHANGES:
|
||||||
Before writing, compare extracted content against existing notes.
|
Before writing, compare extracted content against existing notes.
|
||||||
|
|
||||||
## Check Activity Log
|
## 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.
|
If an entry for this date/source already exists, this may have been processed. Skip or verify different interaction.
|
||||||
|
|
@ -908,28 +956,28 @@ If new info contradicts existing:
|
||||||
|
|
||||||
**IMPORTANT: Write sequentially, one file at a time.**
|
**IMPORTANT: Write sequentially, one file at a time.**
|
||||||
- Generate content for exactly one note.
|
- 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.
|
- 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):**
|
**For NEW entities (use workspace-writeFile):**
|
||||||
\`\`\`bash
|
\`\`\`
|
||||||
executeCommand("write '{knowledge_folder}/People/Jennifer.md' '{content}'")
|
workspace-writeFile({
|
||||||
|
path: "{knowledge_folder}/People/Jennifer.md",
|
||||||
|
data: "# Jennifer\\n\\n## Summary\\n..."
|
||||||
|
})
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**For existing entities:**
|
**For EXISTING entities (use workspace-edit):**
|
||||||
- Read current content first
|
- Read current content first with workspace-readFile
|
||||||
- Add activity entry at TOP of Activity section (reverse chronological)
|
- Use workspace-edit to add activity entry at TOP (reverse chronological)
|
||||||
- Update "Last seen" date
|
- Update fields using targeted edits
|
||||||
- Add new key facts (skip duplicates)
|
\`\`\`
|
||||||
- Add new open items
|
workspace-edit({
|
||||||
- Add new decisions
|
path: "{knowledge_folder}/People/Sarah Chen.md",
|
||||||
- Add new relationships
|
oldString: "## Activity\\n",
|
||||||
- Update summary ONLY if significant new understanding
|
newString: "## Activity\\n- **2026-02-03** (meeting): Met to discuss project timeline\\n"
|
||||||
\`\`\`bash
|
})
|
||||||
executeCommand("cat '{knowledge_folder}/People/Sarah Chen.md'")
|
|
||||||
# ... modify content ...
|
|
||||||
executeCommand("write '{knowledge_folder}/People/Sarah Chen.md' '{full_updated_content}'")
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## 9b: Apply State Changes
|
## 9b: Apply State Changes
|
||||||
|
|
@ -1000,7 +1048,7 @@ After writing, verify links go both ways.
|
||||||
- [[Projects/{Project}]] — {role}
|
- [[Projects/{Project}]] — {role}
|
||||||
|
|
||||||
## Activity
|
## Activity
|
||||||
- **{YYYY-MM-DD}** ({meeting|email}): {Summary with [[Folder/Name]] links}
|
- **{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[Folder/Name]] links}
|
||||||
|
|
||||||
## Key facts
|
## Key facts
|
||||||
{Substantive facts only. Leave empty if none.}
|
{Substantive facts only. Leave empty if none.}
|
||||||
|
|
@ -1035,7 +1083,7 @@ After writing, verify links go both ways.
|
||||||
- [[Projects/{Project}]] — {relationship}
|
- [[Projects/{Project}]] — {relationship}
|
||||||
|
|
||||||
## Activity
|
## Activity
|
||||||
- **{YYYY-MM-DD}** ({meeting|email}): {Summary with [[Folder/Name]] links}
|
- **{YYYY-MM-DD}** ({meeting|email|voice memo}): {Summary with [[Folder/Name]] links}
|
||||||
|
|
||||||
## Key facts
|
## Key facts
|
||||||
{Substantive facts only. Leave empty if none.}
|
{Substantive facts only. Leave empty if none.}
|
||||||
|
|
@ -1119,11 +1167,17 @@ After writing, verify links go both ways.
|
||||||
| Source Type | Creates Notes? | Updates Notes? | Detects State Changes? |
|
| Source Type | Creates Notes? | Updates Notes? | Detects State Changes? |
|
||||||
|-------------|---------------|----------------|------------------------|
|
|-------------|---------------|----------------|------------------------|
|
||||||
| Meeting | Yes | Yes | Yes |
|
| Meeting | Yes | Yes | Yes |
|
||||||
|
| Voice memo | Yes | Yes | Yes |
|
||||||
| Email (personalized, business-relevant) | Yes | Yes | Yes |
|
| Email (personalized, business-relevant) | Yes | Yes | Yes |
|
||||||
| Email (mass/automated/consumer) | No (SKIP) | No | No |
|
| Email (mass/automated/consumer) | No (SKIP) | No | No |
|
||||||
| Email (cold outreach with personalization) | Yes | Yes | Yes |
|
| Email (cold outreach with personalization) | Yes | Yes | Yes |
|
||||||
| Email (generic cold outreach) | No | No | No |
|
| Email (generic cold outreach) | No | No | No |
|
||||||
|
|
||||||
|
**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-...]]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Error Handling
|
# Error Handling
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue