import { addNode } from '../store'; import { getCaptureConfig, CaptureConfig } from './config'; import { extractMemoryData, shouldCapture, ExtractedMemory } from './summarize'; import { checkDuplicate, mergeIntoNode, linkRelatedNode } from './dedupe'; import { Node } from '../../types'; export { getCaptureConfig, setCaptureConfig, CaptureMode, CaptureConfig } from './config'; export { extractMemoryData, shouldCapture, ExtractedMemory } from './summarize'; export { findSimilarNodes, checkDuplicate, mergeIntoNode } from './dedupe'; export interface CaptureInput { conversation: string; sessionId?: string; filesChanged?: string[]; source?: string; } export interface CaptureResult { captured: boolean; action: 'created' | 'merged' | 'linked' | 'skipped'; node?: Node; reason?: string; } export async function captureConversation(input: CaptureInput): Promise { const config = getCaptureConfig(); // Check if capture is enabled if (config.mode === 'off') { return { captured: false, action: 'skipped', reason: 'capture disabled' }; } // Check minimum length if (!shouldCapture(input.conversation, config.minLength)) { return { captured: false, action: 'skipped', reason: 'conversation too short or trivial' }; } // Check exclude patterns for (const pattern of config.excludePatterns) { try { if (new RegExp(pattern, 'i').test(input.conversation)) { return { captured: false, action: 'skipped', reason: `matched exclude pattern: ${pattern}` }; } } catch { // Invalid regex, skip } } // Extract memory data using Ollama const extracted = await extractMemoryData(input.conversation); if (!extracted) { return { captured: false, action: 'skipped', reason: 'failed to extract memory data' }; } // For "decisions" mode, only capture if decisions were found if (config.mode === 'decisions' && extracted.decisions.length === 0) { return { captured: false, action: 'skipped', reason: 'no decisions found (decisions mode)' }; } // Build content const contentParts: string[] = [extracted.summary]; if (extracted.decisions.length > 0) { contentParts.push('\n## Decisions'); for (const d of extracted.decisions) { contentParts.push(`- ${d}`); } } if (extracted.filesDiscussed.length > 0 || input.filesChanged?.length) { const files = [...new Set([...extracted.filesDiscussed, ...(input.filesChanged || [])])]; contentParts.push('\n## Files'); for (const f of files) { contentParts.push(`- ${f}`); } } const content = contentParts.join('\n'); // Check for duplicates const dedupeResult = await checkDuplicate(extracted.summary, content); // Build tags const tags = ['auto-capture']; if (config.autoTag && extracted.topics.length > 0) { tags.push(...extracted.topics); } if (input.source) { tags.push(`source:${input.source}`); } if (dedupeResult.action === 'merge' && dedupeResult.existingNode) { // Merge into existing node const merged = await mergeIntoNode( dedupeResult.existingNode.id, extracted.summary, content, tags ); return { captured: true, action: 'merged', node: merged || undefined, reason: `merged with existing node (similarity: ${(dedupeResult.similarity! * 100).toFixed(1)}%)`, }; } // Create new node const node = await addNode({ kind: 'memory', title: extracted.summary.slice(0, 100), content, tags, status: 'active', metadata: { sessionId: input.sessionId, filesChanged: input.filesChanged, source: input.source || 'claude-code', capturedAt: Date.now(), decisions: extracted.decisions, }, }); // Link to related node if found if (dedupeResult.action === 'link' && dedupeResult.existingNode && config.linkRelated) { await linkRelatedNode(node.id, dedupeResult.existingNode.id); return { captured: true, action: 'linked', node, reason: `linked to related node (similarity: ${(dedupeResult.similarity! * 100).toFixed(1)}%)`, }; } return { captured: true, action: 'created', node, }; } export async function captureText( text: string, options: { tags?: string[]; source?: string } = {} ): Promise { const config = getCaptureConfig(); if (config.mode === 'off') { return { captured: false, action: 'skipped', reason: 'capture disabled' }; } // Simple text capture - no summarization needed const dedupeResult = await checkDuplicate(text, text); const tags = ['manual-capture', ...(options.tags || [])]; if (options.source) { tags.push(`source:${options.source}`); } if (dedupeResult.action === 'merge' && dedupeResult.existingNode) { const merged = await mergeIntoNode( dedupeResult.existingNode.id, text.slice(0, 100), text, tags ); return { captured: true, action: 'merged', node: merged || undefined, }; } const node = await addNode({ kind: 'memory', title: text.slice(0, 100), content: text, tags, status: 'active', metadata: { source: options.source || 'manual', capturedAt: Date.now(), }, }); if (dedupeResult.action === 'link' && dedupeResult.existingNode && config.linkRelated) { await linkRelatedNode(node.id, dedupeResult.existingNode.id); return { captured: true, action: 'linked', node }; } return { captured: true, action: 'created', node }; }