import { Command } from 'commander'; import chalk from 'chalk'; import { captureConversation, captureText, getCaptureConfig, setCaptureConfig, CaptureMode } from '../../core/capture'; const VALID_MODES: CaptureMode[] = ['always', 'manual', 'decisions', 'off']; export const captureCommand = new Command('capture') .description('Capture text or conversation as a memory node') .argument('[text]', 'Text to capture (or pipe via stdin)') .option('--tags ', 'Comma-separated tags') .option('--source ', 'Source identifier', 'manual') .action(async (text: string | undefined, opts) => { let content = text; // Read from stdin if no text provided if (!content) { content = await readStdin(); } if (!content || content.trim().length === 0) { console.error(chalk.red('No text provided. Pass text as argument or pipe via stdin.')); process.exit(1); } const tags = opts.tags ? opts.tags.split(',').map((t: string) => t.trim()) : []; const result = await captureText(content, { tags, source: opts.source }); if (!result.captured) { console.log(chalk.yellow(`⚠ Not captured: ${result.reason}`)); return; } console.log(chalk.green(`✓ Memory ${result.action}`)); if (result.node) { console.log(` ID: ${chalk.cyan(result.node.id)}`); console.log(` Title: ${result.node.title}`); if (result.node.tags.length) console.log(` Tags: ${result.node.tags.join(', ')}`); } if (result.reason) { console.log(` ${chalk.dim(result.reason)}`); } }); export const captureHookCommand = new Command('capture-hook') .description('Hook handler for Claude Code auto-capture (receives JSON on stdin)') .option('--session ', 'Session ID') .action(async (opts) => { const input = await readStdin(); if (!input) { // Silent exit - hook may be called with empty input process.exit(0); } let data: { conversation?: string; files_changed?: string[]; session_id?: string }; try { data = JSON.parse(input); } catch { // Not JSON, treat as plain conversation text data = { conversation: input }; } const conversation = data.conversation; if (!conversation) { process.exit(0); } const result = await captureConversation({ conversation, sessionId: data.session_id || opts.session, filesChanged: data.files_changed, source: 'claude-code', }); // Output result as JSON for hook system console.log(JSON.stringify({ captured: result.captured, action: result.action, nodeId: result.node?.id, reason: result.reason, })); }); export const configCommand = new Command('config') .description('Manage capture configuration') .argument('', 'Action: get, set, or list') .argument('[key]', 'Config key (for get/set)') .argument('[value]', 'Config value (for set)') .action(async (action: string, key?: string, value?: string) => { const config = getCaptureConfig(); if (action === 'list') { console.log(chalk.bold('Capture Configuration:')); console.log(` mode: ${chalk.cyan(config.mode)}`); console.log(` minLength: ${config.minLength}`); console.log(` excludePatterns: ${config.excludePatterns.length ? config.excludePatterns.join(', ') : chalk.dim('(none)')}`); console.log(` autoTag: ${config.autoTag}`); console.log(` linkRelated: ${config.linkRelated}`); console.log(` similarityThreshold: ${config.similarityThreshold}`); console.log(` mergeThreshold: ${config.mergeThreshold}`); return; } if (action === 'get') { if (!key) { console.error(chalk.red('Key required for get')); process.exit(1); } if (!(key in config)) { console.error(chalk.red(`Unknown config key: ${key}`)); process.exit(1); } const val = config[key as keyof typeof config]; console.log(Array.isArray(val) ? val.join(', ') : String(val)); return; } if (action === 'set') { if (!key || value === undefined) { console.error(chalk.red('Key and value required for set')); process.exit(1); } let parsedValue: any = value; // Parse value based on key type if (key === 'mode') { if (!VALID_MODES.includes(value as CaptureMode)) { console.error(chalk.red(`Invalid mode. Must be one of: ${VALID_MODES.join(', ')}`)); process.exit(1); } parsedValue = value; } else if (key === 'minLength' || key === 'similarityThreshold' || key === 'mergeThreshold') { parsedValue = parseFloat(value); if (isNaN(parsedValue)) { console.error(chalk.red('Value must be a number')); process.exit(1); } } else if (key === 'autoTag' || key === 'linkRelated') { parsedValue = value === 'true' || value === '1'; } else if (key === 'excludePatterns') { parsedValue = value.split(',').map(s => s.trim()).filter(Boolean); } else { console.error(chalk.red(`Unknown config key: ${key}`)); process.exit(1); } setCaptureConfig({ [key]: parsedValue }); console.log(chalk.green(`✓ Set ${key} = ${JSON.stringify(parsedValue)}`)); return; } console.error(chalk.red('Invalid action. Use: get, set, or list')); process.exit(1); }); async function readStdin(): Promise { return new Promise((resolve) => { // Check if stdin has data (non-TTY mode) if (process.stdin.isTTY) { resolve(''); return; } let data = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { data += chunk; }); process.stdin.on('end', () => { resolve(data.trim()); }); // Timeout after 100ms if no data setTimeout(() => { if (!data) resolve(''); }, 100); }); }