Skip to main content
Glama
auto-capture-hook.js10.3 kB
#!/usr/bin/env node /** * Claude Code Auto-Capture Hook * * Automatically captures valuable conversation content after tool operations. * Uses pattern detection to identify decisions, errors, learnings, and implementations. * * Trigger: PostToolUse (Edit, Write, Bash) * Input: JSON via stdin with transcript_path and tool info * * @module auto-capture-hook * @version 1.0.0 */ 'use strict'; const fs = require('fs').promises; const path = require('path'); const http = require('http'); const https = require('https'); // Import pattern detection const { detectPatterns, hasUserOverride, generateTags, truncateContent, computeContentHash, extractProjectName, DEFAULT_CONFIG } = require('../utilities/auto-capture-patterns'); /** * Load hook configuration */ async function loadConfig() { try { const configPath = path.join(__dirname, '../config.json'); const configData = await fs.readFile(configPath, 'utf8'); const config = JSON.parse(configData); return { memoryService: config.memoryService || { http: { endpoint: 'http://127.0.0.1:8000', apiKey: '' } }, autoCapture: config.autoCapture || { enabled: true, minLength: 300, maxLength: 4000, patterns: ['decision', 'error', 'learning', 'implementation', 'important', 'code'], debugMode: false } }; } catch (error) { console.warn('[auto-capture] Using default configuration:', error.message); return { memoryService: { http: { endpoint: 'http://127.0.0.1:8000', apiKey: '' } }, autoCapture: { enabled: true, minLength: 300, maxLength: 4000, patterns: ['decision', 'error', 'learning', 'implementation', 'important', 'code'], debugMode: false } }; } } /** * Read input from stdin */ async function readStdin() { return new Promise((resolve, reject) => { let data = ''; const timeout = setTimeout(() => { resolve(data || '{}'); }, 1000); process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => data += chunk); process.stdin.on('end', () => { clearTimeout(timeout); resolve(data); }); process.stdin.on('error', reject); // Resume stdin in case it's paused process.stdin.resume(); }); } /** * Parse transcript file to extract last user and assistant messages */ async function parseTranscript(transcriptPath) { try { const content = await fs.readFile(transcriptPath, 'utf8'); const transcript = JSON.parse(content); if (!Array.isArray(transcript) || transcript.length === 0) { return null; } // Find last user and assistant messages let lastUser = null; let lastAssistant = null; for (let i = transcript.length - 1; i >= 0; i--) { const msg = transcript[i]; const role = msg.role || msg.type; if (!lastAssistant && role === 'assistant') { lastAssistant = extractTextContent(msg.content); } if (!lastUser && role === 'user') { lastUser = extractTextContent(msg.content); } if (lastUser && lastAssistant) break; } return { userMessage: lastUser || '', assistantMessage: lastAssistant || '', combined: `User: ${lastUser || '[no message]'}\n\nAssistant: ${lastAssistant || '[no response]'}` }; } catch (error) { console.error('[auto-capture] Failed to parse transcript:', error.message); return null; } } /** * Extract text content from various message formats */ function extractTextContent(content) { if (typeof content === 'string') { return content; } if (Array.isArray(content)) { return content .filter(item => item.type === 'text') .map(item => item.text) .join('\n'); } return ''; } /** * Store memory via HTTP API */ async function storeMemory(config, content, memoryType, tags) { const endpoint = config.memoryService.http.endpoint; const apiKey = config.memoryService.http.apiKey; const url = new URL('/api/memories', endpoint); const isHttps = url.protocol === 'https:'; const payload = JSON.stringify({ content: content, memory_type: memoryType, tags: tags, metadata: { source: 'auto-capture', hook: 'PostToolUse', captured_at: new Date().toISOString() } }); const options = { hostname: url.hostname, port: url.port || (isHttps ? 443 : 80), path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), ...(apiKey ? { 'X-API-Key': apiKey } : {}) }, timeout: 5000 }; return new Promise((resolve, reject) => { const client = isHttps ? https : http; const req = client.request(options, res => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) { try { resolve(JSON.parse(data)); } catch { resolve({ success: true, raw: data }); } } else { reject(new Error(`HTTP ${res.statusCode}: ${data}`)); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); }); req.write(payload); req.end(); }); } /** * Main hook execution */ async function main() { const startTime = Date.now(); try { // Load configuration const config = await loadConfig(); // Check if auto-capture is enabled if (!config.autoCapture.enabled) { if (config.autoCapture.debugMode) { console.log('[auto-capture] Disabled in configuration'); } process.exit(0); } // Read stdin input const stdinData = await readStdin(); let input = {}; try { input = JSON.parse(stdinData); } catch { // No valid input, might be empty if (config.autoCapture.debugMode) { console.log('[auto-capture] No valid stdin input'); } process.exit(0); } // Extract transcript path and cwd const transcriptPath = input.transcript_path || input.transcriptPath; const cwd = input.cwd || process.cwd(); if (!transcriptPath) { if (config.autoCapture.debugMode) { console.log('[auto-capture] No transcript path provided'); } process.exit(0); } // Parse transcript const transcript = await parseTranscript(transcriptPath); if (!transcript) { process.exit(0); } // Check user overrides const overrides = hasUserOverride(transcript.userMessage); if (overrides.forceSkip) { if (config.autoCapture.debugMode) { console.log('[auto-capture] Skipped by user override (#skip)'); } process.exit(0); } const content = transcript.combined; // Detect patterns (unless force remember) let detection; if (overrides.forceRemember) { detection = { isValuable: true, memoryType: 'Context', matchedPattern: 'user-override', confidence: 1.0 }; if (config.autoCapture.debugMode) { console.log('[auto-capture] Force remember by user override (#remember)'); } } else { detection = detectPatterns(content, { minLength: config.autoCapture.minLength, enabledPatterns: config.autoCapture.patterns, debugMode: config.autoCapture.debugMode }); } if (!detection.isValuable) { if (config.autoCapture.debugMode) { console.log(`[auto-capture] Not valuable: ${detection.reason}`); } process.exit(0); } // Prepare content for storage const truncatedContent = truncateContent(content, config.autoCapture.maxLength); const projectName = extractProjectName(cwd); const tags = generateTags(detection, projectName); // Store memory if (config.autoCapture.debugMode) { console.log(`[auto-capture] Storing ${detection.memoryType} memory...`); console.log(`[auto-capture] Pattern: ${detection.matchedPattern}`); console.log(`[auto-capture] Tags: ${tags.join(', ')}`); } const result = await storeMemory( config, truncatedContent, detection.memoryType, tags ); const elapsed = Date.now() - startTime; if (config.autoCapture.debugMode) { console.log(`[auto-capture] Stored successfully in ${elapsed}ms`); console.log(`[auto-capture] Hash: ${result.content_hash || 'unknown'}`); } process.exit(0); } catch (error) { const elapsed = Date.now() - startTime; console.error(`[auto-capture] Error after ${elapsed}ms:`, error.message); // Exit gracefully - don't block the user's workflow process.exit(0); } } // Run if executed directly if (require.main === module) { main(); } module.exports = { main, loadConfig, parseTranscript, storeMemory };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/doobidoo/mcp-memory-service'

If you have feedback or need assistance with the MCP directory API, please join our Discord server