Release v3.9.9
Published from npm package build Source: https://github.com/thedotmack/claude-mem-source
This commit is contained in:
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Post Tool Use Hook - Streaming SDK Version
|
||||
*
|
||||
* Feeds tool responses to the streaming SDK session for real-time processing.
|
||||
* SDK decides what to store and calls bash commands directly.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { renderToolMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||
import { getProjectName } from './shared/path-resolver.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||
|
||||
function debugLog(message, data = {}) {
|
||||
if (process.env.CLAUDE_MEM_DEBUG === 'true') {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logLine = `[${timestamp}] HOOK DEBUG: ${message} ${JSON.stringify(data)}\n`;
|
||||
try {
|
||||
fs.appendFileSync(HOOKS_LOG, logLine);
|
||||
process.stderr.write(logLine);
|
||||
} catch (error) {
|
||||
// Silent fail on log errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removed: buildStreamingToolMessage function
|
||||
// Now using centralized config from hook-prompt-renderer.js
|
||||
|
||||
// =============================================================================
|
||||
// MAIN
|
||||
// =============================================================================
|
||||
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => { input += chunk; });
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
let payload;
|
||||
try {
|
||||
payload = input ? JSON.parse(input) : {};
|
||||
} catch (error) {
|
||||
debugLog('PostToolUse: JSON parse error', { error: error.message });
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { tool_name, tool_response, prompt, cwd, timestamp } = payload;
|
||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
||||
|
||||
// Return immediately - process async in background (don't block next tool)
|
||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
||||
|
||||
try {
|
||||
// Load SDK session info
|
||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
||||
if (!fs.existsSync(sessionFile)) {
|
||||
debugLog('PostToolUse: No streaming session found', { project });
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
||||
const sdkSessionId = sessionData.sdkSessionId;
|
||||
|
||||
// Convert tool response to string
|
||||
const toolResponseStr = typeof tool_response === 'string'
|
||||
? tool_response
|
||||
: JSON.stringify(tool_response);
|
||||
|
||||
// Build message for SDK using centralized config
|
||||
const message = renderToolMessage({
|
||||
toolName: tool_name,
|
||||
toolResponse: toolResponseStr,
|
||||
userPrompt: prompt || '',
|
||||
timestamp: timestamp || new Date().toISOString()
|
||||
});
|
||||
|
||||
// Send to SDK and wait for processing to complete using centralized config
|
||||
const response = query({
|
||||
prompt: message,
|
||||
options: {
|
||||
model: HOOK_CONFIG.sdk.model,
|
||||
resume: sdkSessionId,
|
||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
||||
maxTokens: HOOK_CONFIG.sdk.maxTokensTool,
|
||||
cwd // Must match where transcript was created
|
||||
}
|
||||
});
|
||||
|
||||
// Consume the stream to let SDK fully process
|
||||
for await (const msg of response) {
|
||||
debugLog('PostToolUse: SDK message', { type: msg.type, subtype: msg.subtype });
|
||||
|
||||
// SDK messages are structured differently than we expected
|
||||
// - type: 'assistant' contains the assistant's response with content blocks
|
||||
// - Content blocks can be text or tool_use
|
||||
// - type: 'user' contains tool results
|
||||
// - type: 'result' is the final summary
|
||||
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text') {
|
||||
debugLog('PostToolUse: SDK text', { text: block.text?.slice(0, 200) });
|
||||
} else if (block.type === 'tool_use') {
|
||||
debugLog('PostToolUse: SDK tool_use', {
|
||||
tool: block.name,
|
||||
input: JSON.stringify(block.input).slice(0, 200)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'user' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'tool_result') {
|
||||
debugLog('PostToolUse: SDK tool_result', {
|
||||
tool_use_id: block.tool_use_id,
|
||||
content: typeof block.content === 'string' ? block.content.slice(0, 300) : JSON.stringify(block.content).slice(0, 300)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'result') {
|
||||
debugLog('PostToolUse: SDK result', {
|
||||
subtype: msg.subtype,
|
||||
is_error: msg.is_error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('PostToolUse: SDK finished processing', { tool_name, sdkSessionId });
|
||||
} catch (error) {
|
||||
debugLog('PostToolUse: Error sending to SDK', { error: error.message });
|
||||
}
|
||||
|
||||
// Exit cleanly after async processing completes
|
||||
process.exit(0);
|
||||
});
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Session Start Hook (SDK Version)
|
||||
*
|
||||
* Calls the CLI to load relevant context from ChromaDB at session start.
|
||||
*/
|
||||
|
||||
import { createHookResponse, debugLog } from './shared/hook-helpers.js';
|
||||
|
||||
// Read stdin
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => {
|
||||
input += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
const payload = input ? JSON.parse(input) : {};
|
||||
|
||||
debugLog('SessionStart hook invoked (SDK version)', { cwd: payload.cwd });
|
||||
|
||||
const { cwd, source } = payload;
|
||||
|
||||
// Run on startup or /clear
|
||||
if (source !== 'startup' && source !== 'clear') {
|
||||
const response = createHookResponse('SessionStart', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
// Call the CLI to load context
|
||||
const { executeCliCommand } = await import('./shared/hook-helpers.js');
|
||||
|
||||
const result = await executeCliCommand('claude-mem', ['load-context', '--format', 'session-start']);
|
||||
|
||||
if (result.success && result.stdout) {
|
||||
// Use the CLI output directly as context (it's already formatted)
|
||||
const response = createHookResponse('SessionStart', true, {
|
||||
context: result.stdout
|
||||
});
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Return without context
|
||||
const response = createHookResponse('SessionStart', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue without context on error
|
||||
const response = createHookResponse('SessionStart', true);
|
||||
console.log(JSON.stringify(response));
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Shared configuration loader utility for Claude Memory hooks
|
||||
* Loads CLI command name from config.json with proper fallback handling
|
||||
*/
|
||||
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
|
||||
/**
|
||||
* Loads the CLI command name from the hooks config.json file
|
||||
* @returns {string} The CLI command name (defaults to 'claude-mem')
|
||||
*/
|
||||
export function loadCliCommand() {
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const configPath = join(__dirname, '..', 'config.json');
|
||||
|
||||
if (existsSync(configPath)) {
|
||||
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
||||
return config.cliCommand || 'claude-mem';
|
||||
}
|
||||
|
||||
return 'claude-mem';
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Hook Helper Functions
|
||||
*
|
||||
* This module provides JavaScript wrappers around the TypeScript PromptOrchestrator
|
||||
* and HookTemplates system, making them accessible to the JavaScript hook scripts.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* Creates a standardized hook response using the HookTemplates system
|
||||
* @param {string} hookType - Type of hook ('PreCompact' or 'SessionStart')
|
||||
* @param {boolean} success - Whether the operation was successful
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Object} Formatted hook response
|
||||
*/
|
||||
export function createHookResponse(hookType, success, options = {}) {
|
||||
if (hookType === 'PreCompact') {
|
||||
if (success) {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
continue: false,
|
||||
stopReason: options.reason || 'Pre-compact operation failed',
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (hookType === 'SessionStart') {
|
||||
if (success && options.context) {
|
||||
return {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'SessionStart',
|
||||
additionalContext: options.context
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (hookType === 'UserPromptSubmit' || hookType === 'PostToolUse') {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
if (hookType === 'Stop') {
|
||||
return {
|
||||
continue: true,
|
||||
suppressOutput: true
|
||||
};
|
||||
}
|
||||
|
||||
// Generic response for unknown hook types
|
||||
return {
|
||||
continue: success,
|
||||
suppressOutput: true,
|
||||
...(options.reason && !success ? { stopReason: options.reason } : {})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a session start context message using standardized templates
|
||||
* @param {Object} contextData - Context information
|
||||
* @returns {string} Formatted context message
|
||||
*/
|
||||
export function formatSessionStartContext(contextData) {
|
||||
const {
|
||||
projectName = 'unknown project',
|
||||
memoryCount = 0,
|
||||
lastSessionTime,
|
||||
recentComponents = [],
|
||||
recentDecisions = []
|
||||
} = contextData;
|
||||
|
||||
const timeInfo = lastSessionTime ? ` (last worked: ${lastSessionTime})` : '';
|
||||
const contextParts = [];
|
||||
|
||||
contextParts.push(`🧠 Loaded ${memoryCount} memories from previous sessions for ${projectName}${timeInfo}`);
|
||||
|
||||
if (recentComponents.length > 0) {
|
||||
contextParts.push(`\n🎯 Recent components: ${recentComponents.slice(0, 3).join(', ')}`);
|
||||
}
|
||||
|
||||
if (recentDecisions.length > 0) {
|
||||
contextParts.push(`\n🔄 Recent decisions: ${recentDecisions.slice(0, 2).join(', ')}`);
|
||||
}
|
||||
|
||||
if (memoryCount > 0) {
|
||||
contextParts.push('\n💡 Use search_nodes("keywords") to find related work or open_nodes(["entity_name"]) to load specific components');
|
||||
}
|
||||
|
||||
return contextParts.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a CLI command and returns the result
|
||||
* @param {string} command - CLI command to execute
|
||||
* @param {Array} args - Command arguments
|
||||
* @param {Object} options - Spawn options
|
||||
* @returns {Promise<{stdout: string, stderr: string, success: boolean}>}
|
||||
*/
|
||||
export async function executeCliCommand(command, args = [], options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const { input, ...spawnOptions } = options;
|
||||
const process = spawn(command, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
...spawnOptions
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
if (process.stdout) {
|
||||
process.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (process.stderr) {
|
||||
process.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
}
|
||||
|
||||
if (input && process.stdin) {
|
||||
process.stdin.write(input);
|
||||
process.stdin.end();
|
||||
} else if (process.stdin) {
|
||||
process.stdin.end();
|
||||
}
|
||||
|
||||
process.on('close', (code) => {
|
||||
resolve({
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
success: code === 0
|
||||
});
|
||||
});
|
||||
|
||||
process.on('error', (error) => {
|
||||
resolve({
|
||||
stdout: '',
|
||||
stderr: error.message,
|
||||
success: false
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses context data from CLI output
|
||||
* @param {string} output - Raw CLI output
|
||||
* @returns {Object} Parsed context data
|
||||
*/
|
||||
export function parseContextData(output) {
|
||||
if (!output || !output.trim()) {
|
||||
return {
|
||||
memoryCount: 0,
|
||||
recentComponents: [],
|
||||
recentDecisions: []
|
||||
};
|
||||
}
|
||||
|
||||
// Try to parse as JSON first (if CLI outputs structured data)
|
||||
try {
|
||||
const parsed = JSON.parse(output);
|
||||
return {
|
||||
memoryCount: parsed.memoryCount || 0,
|
||||
recentComponents: parsed.recentComponents || [],
|
||||
recentDecisions: parsed.recentDecisions || [],
|
||||
lastSessionTime: parsed.lastSessionTime
|
||||
};
|
||||
} catch (e) {
|
||||
// If not JSON, treat as plain text context
|
||||
const lines = output.split('\n').filter(line => line.trim());
|
||||
return {
|
||||
memoryCount: lines.length,
|
||||
recentComponents: [],
|
||||
recentDecisions: [],
|
||||
rawContext: output
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates hook payload structure
|
||||
* @param {Object} payload - Hook payload to validate
|
||||
* @param {string} expectedHookType - Expected hook event name
|
||||
* @returns {{valid: boolean, error?: string}} Validation result
|
||||
*/
|
||||
export function validateHookPayload(payload, expectedHookType) {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return { valid: false, error: 'Payload must be a valid object' };
|
||||
}
|
||||
|
||||
if (!payload.session_id || typeof payload.session_id !== 'string') {
|
||||
return { valid: false, error: 'Missing or invalid session_id' };
|
||||
}
|
||||
|
||||
if (!payload.transcript_path || typeof payload.transcript_path !== 'string') {
|
||||
return { valid: false, error: 'Missing or invalid transcript_path' };
|
||||
}
|
||||
|
||||
if (expectedHookType && payload.hook_event_name !== expectedHookType) {
|
||||
return { valid: false, error: `Expected hook_event_name to be ${expectedHookType}` };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs debug information if debug mode is enabled
|
||||
* @param {string} message - Debug message
|
||||
* @param {Object} data - Additional data to log
|
||||
*/
|
||||
export function debugLog(message, data = {}) {
|
||||
if (process.env.DEBUG === 'true' || process.env.CLAUDE_MEM_DEBUG === 'true') {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.error(`[${timestamp}] HOOK DEBUG: ${message}`, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// src/prompts/hook-prompts.config.ts
|
||||
var HOOK_CONFIG = {
|
||||
maxUserPromptLength: 200,
|
||||
maxToolResponseLength: 20000,
|
||||
sdk: {
|
||||
model: "claude-sonnet-4-5",
|
||||
allowedTools: ["Bash"],
|
||||
maxTokensSystem: 8192,
|
||||
maxTokensTool: 8192,
|
||||
maxTokensEnd: 2048
|
||||
}
|
||||
};
|
||||
var SYSTEM_PROMPT = `You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
|
||||
|
||||
# SESSION CONTEXT
|
||||
- Project: {{project}}
|
||||
- Session: {{sessionId}}
|
||||
- Date: {{date}}
|
||||
- User Request: "{{userPrompt}}"
|
||||
|
||||
# YOUR JOB
|
||||
|
||||
## FIRST: Generate Session Title
|
||||
|
||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
||||
|
||||
Use this bash command:
|
||||
\`\`\`bash
|
||||
claude-mem update-session-metadata \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--title "Short title (3-6 words)" \\
|
||||
--subtitle "One sentence description (max 20 words)"
|
||||
\`\`\`
|
||||
|
||||
Example for "Help me add dark mode to my app":
|
||||
- Title: "Dark Mode Implementation"
|
||||
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
|
||||
|
||||
## THEN: Process Tool Responses
|
||||
|
||||
You will receive a stream of tool responses. For each one:
|
||||
|
||||
1. ANALYZE: Does this contain information worth remembering?
|
||||
2. DECIDE: Should I store this or skip it?
|
||||
3. EXTRACT: What are the key semantic concepts?
|
||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
||||
5. STORE: Use bash to save the hierarchical memory
|
||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
||||
|
||||
# WHAT TO STORE
|
||||
|
||||
Store these:
|
||||
- File contents with logic, algorithms, or patterns
|
||||
- Search results revealing project structure
|
||||
- Build errors or test failures with context
|
||||
- Code revealing architecture or design decisions
|
||||
- Git diffs with significant changes
|
||||
- Command outputs showing system state
|
||||
|
||||
Skip these:
|
||||
- Simple status checks (git status with no changes)
|
||||
- Trivial edits (one-line config changes)
|
||||
- Repeated operations
|
||||
- Binary data or noise
|
||||
- Anything without semantic value
|
||||
|
||||
# HIERARCHICAL MEMORY FORMAT
|
||||
|
||||
Each memory has FOUR components:
|
||||
|
||||
## 1. TITLE (3-8 words)
|
||||
A scannable headline that captures the core action or topic.
|
||||
Examples:
|
||||
- "SDK Transcript Cleanup Implementation"
|
||||
- "Hook System Architecture Analysis"
|
||||
- "ChromaDB Migration Planning"
|
||||
|
||||
## 2. SUBTITLE (max 24 words)
|
||||
A concise, memorable summary that captures the essence of the change.
|
||||
Examples:
|
||||
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
|
||||
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
|
||||
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
|
||||
|
||||
Guidelines:
|
||||
- Clear and descriptive
|
||||
- Focus on the outcome or benefit
|
||||
- Use active voice when possible
|
||||
- Keep it professional and informative
|
||||
|
||||
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
|
||||
Individual, searchable statements that can be vector-embedded separately.
|
||||
Each fact is ONE specific piece of information.
|
||||
|
||||
Examples:
|
||||
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
|
||||
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
|
||||
- "Uses fs.unlink with graceful error handling for missing files"
|
||||
- "Checks two transcript path formats for backward compatibility"
|
||||
|
||||
Guidelines:
|
||||
- Start with filename or component when relevant
|
||||
- Be specific: include paths, function names, actual values
|
||||
- Each fact stands alone (no pronouns like "it" or "this")
|
||||
- 50-150 characters target
|
||||
- Focus on searchable technical details
|
||||
|
||||
## 4. NARRATIVE (512-1024 tokens, same as current format)
|
||||
The full contextual story for deep dives:
|
||||
|
||||
"In the {{project}} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
|
||||
|
||||
This is the detailed explanation for when someone needs full context.
|
||||
|
||||
# STORAGE COMMAND FORMAT
|
||||
|
||||
Store using this EXACT bash command structure:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "{{project}}_{{sessionId}}_{{date}}_001" \\
|
||||
--title "Your Title Here" \\
|
||||
--subtitle "Your concise subtitle here" \\
|
||||
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \\
|
||||
--concepts '["concept1", "concept2", "concept3"]' \\
|
||||
--files '["path/to/file1.js", "path/to/file2.ts"]' \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--date "{{date}}"
|
||||
\`\`\`
|
||||
|
||||
CRITICAL FORMATTING RULES:
|
||||
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
|
||||
- Use double quotes inside the JSON arrays: "item"
|
||||
- Use double quotes around simple string values: --title "Title"
|
||||
- Escape any quotes in the content properly
|
||||
- Sequential numbering: 001, 002, 003, etc.
|
||||
|
||||
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
|
||||
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
|
||||
|
||||
# EXAMPLE MEMORY
|
||||
|
||||
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
|
||||
|
||||
Your storage command:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "claude-mem_abc123_2025-10-01_001" \\
|
||||
--title "SDK Transcript Auto-Cleanup" \\
|
||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \\
|
||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \\
|
||||
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \\
|
||||
--files '["hooks/stop-streaming.js"]' \\
|
||||
--project "claude-mem" \\
|
||||
--session "abc123" \\
|
||||
--date "2025-10-01"
|
||||
\`\`\`
|
||||
|
||||
# STATE TRACKING
|
||||
|
||||
CRITICAL: Keep track of your memory counter across all tool messages.
|
||||
- Start at 001
|
||||
- Increment for each stored memory
|
||||
- Never repeat numbers
|
||||
- Each session has separate numbering
|
||||
|
||||
# SESSION END
|
||||
|
||||
At the end (when I send "SESSION ENDING"), generate an overview using:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "2-3 sentence overview"
|
||||
\`\`\`
|
||||
|
||||
# IMPORTANT REMINDERS
|
||||
|
||||
- You're processing a DIFFERENT Claude Code session (not your own)
|
||||
- Use Bash tool to call claude-mem commands
|
||||
- Keep subtitles clear and informative (max 24 words)
|
||||
- Each fact is ONE specific thing (not multiple ideas)
|
||||
- Be selective - quality over quantity
|
||||
- Always increment memory numbers
|
||||
- Facts should be searchable (specific file names, paths, functions)
|
||||
|
||||
Ready for tool responses.`;
|
||||
var TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
|
||||
|
||||
Tool: {{toolName}}
|
||||
User Context: "{{userPrompt}}"
|
||||
|
||||
\`\`\`
|
||||
{{toolResponse}}
|
||||
\`\`\`
|
||||
|
||||
Analyze and store if meaningful.`;
|
||||
var END_MESSAGE = `# SESSION ENDING
|
||||
|
||||
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
|
||||
|
||||
Store it using Bash:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "YOUR_OVERVIEW_HERE"
|
||||
\`\`\`
|
||||
|
||||
Focus on: what was done, current state, key decisions, outcomes.`;
|
||||
var PROMPTS = {
|
||||
system: SYSTEM_PROMPT,
|
||||
tool: TOOL_MESSAGE,
|
||||
end: END_MESSAGE
|
||||
};
|
||||
|
||||
// src/prompts/hook-prompt-renderer.ts
|
||||
function substituteVariables(template, variables) {
|
||||
let result = template;
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const placeholder = `{{${key}}}`;
|
||||
result = result.split(placeholder).join(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function truncate(text, maxLength) {
|
||||
if (text.length <= maxLength)
|
||||
return text;
|
||||
return text.slice(0, maxLength) + (text.length > maxLength ? "..." : "");
|
||||
}
|
||||
function formatTime(timestamp) {
|
||||
const timePart = timestamp.split("T")[1];
|
||||
if (!timePart)
|
||||
return "";
|
||||
return timePart.slice(0, 8);
|
||||
}
|
||||
function renderSystemPrompt(variables) {
|
||||
const userPromptTruncated = truncate(variables.userPrompt, HOOK_CONFIG.maxUserPromptLength);
|
||||
return substituteVariables(PROMPTS.system, {
|
||||
project: variables.project,
|
||||
sessionId: variables.sessionId,
|
||||
date: variables.date,
|
||||
userPrompt: userPromptTruncated
|
||||
});
|
||||
}
|
||||
function renderToolMessage(variables) {
|
||||
const userPromptTruncated = truncate(variables.userPrompt, HOOK_CONFIG.maxUserPromptLength);
|
||||
const toolResponseTruncated = truncate(variables.toolResponse, HOOK_CONFIG.maxToolResponseLength);
|
||||
const timeFormatted = formatTime(variables.timestamp);
|
||||
return substituteVariables(PROMPTS.tool, {
|
||||
toolName: variables.toolName,
|
||||
toolResponse: toolResponseTruncated,
|
||||
userPrompt: userPromptTruncated,
|
||||
timestamp: variables.timestamp,
|
||||
timeFormatted
|
||||
});
|
||||
}
|
||||
function renderEndMessage(variables) {
|
||||
return substituteVariables(PROMPTS.end, {
|
||||
project: variables.project,
|
||||
sessionId: variables.sessionId
|
||||
});
|
||||
}
|
||||
function renderPrompt(type, variables) {
|
||||
switch (type) {
|
||||
case "system":
|
||||
return renderSystemPrompt(variables);
|
||||
case "tool":
|
||||
return renderToolMessage(variables);
|
||||
case "end":
|
||||
return renderEndMessage(variables);
|
||||
default:
|
||||
throw new Error(`Unknown prompt type: ${type}`);
|
||||
}
|
||||
}
|
||||
export {
|
||||
renderToolMessage,
|
||||
renderSystemPrompt,
|
||||
renderPrompt,
|
||||
renderEndMessage,
|
||||
PROMPTS,
|
||||
HOOK_CONFIG
|
||||
};
|
||||
@@ -0,0 +1,217 @@
|
||||
// src/prompts/hook-prompts.config.ts
|
||||
var HOOK_CONFIG = {
|
||||
maxUserPromptLength: 200,
|
||||
maxToolResponseLength: 20000,
|
||||
sdk: {
|
||||
model: "claude-sonnet-4-5",
|
||||
allowedTools: ["Bash"],
|
||||
maxTokensSystem: 8192,
|
||||
maxTokensTool: 8192,
|
||||
maxTokensEnd: 2048
|
||||
}
|
||||
};
|
||||
var SYSTEM_PROMPT = `You are a semantic memory compressor for claude-mem. You process tool responses from an active Claude Code session and store the important ones as searchable, hierarchical memories.
|
||||
|
||||
# SESSION CONTEXT
|
||||
- Project: {{project}}
|
||||
- Session: {{sessionId}}
|
||||
- Date: {{date}}
|
||||
- User Request: "{{userPrompt}}"
|
||||
|
||||
# YOUR JOB
|
||||
|
||||
## FIRST: Generate Session Title
|
||||
|
||||
IMMEDIATELY generate a title and subtitle for this session based on the user request.
|
||||
|
||||
Use this bash command:
|
||||
\`\`\`bash
|
||||
claude-mem update-session-metadata \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--title "Short title (3-6 words)" \\
|
||||
--subtitle "One sentence description (max 20 words)"
|
||||
\`\`\`
|
||||
|
||||
Example for "Help me add dark mode to my app":
|
||||
- Title: "Dark Mode Implementation"
|
||||
- Subtitle: "Adding theme toggle and dark color scheme support to the application"
|
||||
|
||||
## THEN: Process Tool Responses
|
||||
|
||||
You will receive a stream of tool responses. For each one:
|
||||
|
||||
1. ANALYZE: Does this contain information worth remembering?
|
||||
2. DECIDE: Should I store this or skip it?
|
||||
3. EXTRACT: What are the key semantic concepts?
|
||||
4. DECOMPOSE: Break into title + subtitle + atomic facts + narrative
|
||||
5. STORE: Use bash to save the hierarchical memory
|
||||
6. TRACK: Keep count of stored memories (001, 002, 003...)
|
||||
|
||||
# WHAT TO STORE
|
||||
|
||||
Store these:
|
||||
- File contents with logic, algorithms, or patterns
|
||||
- Search results revealing project structure
|
||||
- Build errors or test failures with context
|
||||
- Code revealing architecture or design decisions
|
||||
- Git diffs with significant changes
|
||||
- Command outputs showing system state
|
||||
|
||||
Skip these:
|
||||
- Simple status checks (git status with no changes)
|
||||
- Trivial edits (one-line config changes)
|
||||
- Repeated operations
|
||||
- Binary data or noise
|
||||
- Anything without semantic value
|
||||
|
||||
# HIERARCHICAL MEMORY FORMAT
|
||||
|
||||
Each memory has FOUR components:
|
||||
|
||||
## 1. TITLE (3-8 words)
|
||||
A scannable headline that captures the core action or topic.
|
||||
Examples:
|
||||
- "SDK Transcript Cleanup Implementation"
|
||||
- "Hook System Architecture Analysis"
|
||||
- "ChromaDB Migration Planning"
|
||||
|
||||
## 2. SUBTITLE (max 24 words)
|
||||
A concise, memorable summary that captures the essence of the change.
|
||||
Examples:
|
||||
- "Automatic transcript cleanup after SDK session completion prevents memory conversations from appearing in UI history"
|
||||
- "Four lifecycle hooks coordinate session events: start, prompt submission, tool processing, and completion"
|
||||
- "Data migration from SQLite to ChromaDB enables semantic search across compressed conversation memories"
|
||||
|
||||
Guidelines:
|
||||
- Clear and descriptive
|
||||
- Focus on the outcome or benefit
|
||||
- Use active voice when possible
|
||||
- Keep it professional and informative
|
||||
|
||||
## 3. ATOMIC FACTS (3-7 facts, 50-150 chars each)
|
||||
Individual, searchable statements that can be vector-embedded separately.
|
||||
Each fact is ONE specific piece of information.
|
||||
|
||||
Examples:
|
||||
- "stop-streaming.js: Auto-deletes SDK transcripts after completion"
|
||||
- "Path format: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl"
|
||||
- "Uses fs.unlink with graceful error handling for missing files"
|
||||
- "Checks two transcript path formats for backward compatibility"
|
||||
|
||||
Guidelines:
|
||||
- Start with filename or component when relevant
|
||||
- Be specific: include paths, function names, actual values
|
||||
- Each fact stands alone (no pronouns like "it" or "this")
|
||||
- 50-150 characters target
|
||||
- Focus on searchable technical details
|
||||
|
||||
## 4. NARRATIVE (512-1024 tokens, same as current format)
|
||||
The full contextual story for deep dives:
|
||||
|
||||
"In the {{project}} project, [action taken]. [Technical details: files, functions, concepts]. [Why this matters]."
|
||||
|
||||
This is the detailed explanation for when someone needs full context.
|
||||
|
||||
# STORAGE COMMAND FORMAT
|
||||
|
||||
Store using this EXACT bash command structure:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "{{project}}_{{sessionId}}_{{date}}_001" \\
|
||||
--title "Your Title Here" \\
|
||||
--subtitle "Your concise subtitle here" \\
|
||||
--facts '["Fact 1 here", "Fact 2 here", "Fact 3 here"]' \\
|
||||
--concepts '["concept1", "concept2", "concept3"]' \\
|
||||
--files '["path/to/file1.js", "path/to/file2.ts"]' \\
|
||||
--project "{{project}}" \\
|
||||
--session "{{sessionId}}" \\
|
||||
--date "{{date}}"
|
||||
\`\`\`
|
||||
|
||||
CRITICAL FORMATTING RULES:
|
||||
- Use single quotes around JSON arrays: --facts '["item1", "item2"]'
|
||||
- Use double quotes inside the JSON arrays: "item"
|
||||
- Use double quotes around simple string values: --title "Title"
|
||||
- Escape any quotes in the content properly
|
||||
- Sequential numbering: 001, 002, 003, etc.
|
||||
|
||||
Concepts: 2-5 broad categories (e.g., "hooks", "storage", "async-processing")
|
||||
Files: Actual file paths touched (e.g., "hooks/stop-streaming.js")
|
||||
|
||||
# EXAMPLE MEMORY
|
||||
|
||||
Tool response shows: [Read file hooks/stop-streaming.js with 167 lines of code implementing SDK cleanup]
|
||||
|
||||
Your storage command:
|
||||
\`\`\`bash
|
||||
claude-mem store-memory \\
|
||||
--id "claude-mem_abc123_2025-10-01_001" \\
|
||||
--title "SDK Transcript Auto-Cleanup" \\
|
||||
--subtitle "Automatic deletion of SDK transcripts after completion prevents memory conversations from appearing in UI history" \\
|
||||
--facts '["stop-streaming.js: Deletes SDK transcript after overview generation", "Path: ~/.claude/projects/{sanitized-cwd}/{sessionId}.jsonl", "Uses fs.unlink with error handling for missing files", "Prevents memory conversations from polluting Claude Code UI"]' \\
|
||||
--concepts '["cleanup", "SDK-lifecycle", "UX", "file-management"]' \\
|
||||
--files '["hooks/stop-streaming.js"]' \\
|
||||
--project "claude-mem" \\
|
||||
--session "abc123" \\
|
||||
--date "2025-10-01"
|
||||
\`\`\`
|
||||
|
||||
# STATE TRACKING
|
||||
|
||||
CRITICAL: Keep track of your memory counter across all tool messages.
|
||||
- Start at 001
|
||||
- Increment for each stored memory
|
||||
- Never repeat numbers
|
||||
- Each session has separate numbering
|
||||
|
||||
# SESSION END
|
||||
|
||||
At the end (when I send "SESSION ENDING"), generate an overview using:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "2-3 sentence overview"
|
||||
\`\`\`
|
||||
|
||||
# IMPORTANT REMINDERS
|
||||
|
||||
- You're processing a DIFFERENT Claude Code session (not your own)
|
||||
- Use Bash tool to call claude-mem commands
|
||||
- Keep subtitles clear and informative (max 24 words)
|
||||
- Each fact is ONE specific thing (not multiple ideas)
|
||||
- Be selective - quality over quantity
|
||||
- Always increment memory numbers
|
||||
- Facts should be searchable (specific file names, paths, functions)
|
||||
|
||||
Ready for tool responses.`;
|
||||
var TOOL_MESSAGE = `# Tool Response {{timeFormatted}}
|
||||
|
||||
Tool: {{toolName}}
|
||||
User Context: "{{userPrompt}}"
|
||||
|
||||
\`\`\`
|
||||
{{toolResponse}}
|
||||
\`\`\`
|
||||
|
||||
Analyze and store if meaningful.`;
|
||||
var END_MESSAGE = `# SESSION ENDING
|
||||
|
||||
Review our entire conversation. Generate a concise 2-3 sentence overview of what was accomplished.
|
||||
|
||||
Store it using Bash:
|
||||
\`\`\`bash
|
||||
claude-mem store-overview --project "{{project}}" --session "{{sessionId}}" --content "YOUR_OVERVIEW_HERE"
|
||||
\`\`\`
|
||||
|
||||
Focus on: what was done, current state, key decisions, outcomes.`;
|
||||
var PROMPTS = {
|
||||
system: SYSTEM_PROMPT,
|
||||
tool: TOOL_MESSAGE,
|
||||
end: END_MESSAGE
|
||||
};
|
||||
export {
|
||||
TOOL_MESSAGE,
|
||||
SYSTEM_PROMPT,
|
||||
PROMPTS,
|
||||
HOOK_CONFIG,
|
||||
END_MESSAGE
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Path resolver utility for Claude Memory hooks
|
||||
* Provides proper path handling using environment variables
|
||||
*/
|
||||
|
||||
import { join, basename } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
/**
|
||||
* Gets the base data directory for claude-mem
|
||||
* @returns {string} Data directory path
|
||||
*/
|
||||
export function getDataDir() {
|
||||
return process.env.CLAUDE_MEM_DATA_DIR || join(homedir(), '.claude-mem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the settings file path
|
||||
* @returns {string} Settings file path
|
||||
*/
|
||||
export function getSettingsPath() {
|
||||
return join(getDataDir(), 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the archives directory path
|
||||
* @returns {string} Archives directory path
|
||||
*/
|
||||
export function getArchivesDir() {
|
||||
return process.env.CLAUDE_MEM_ARCHIVES_DIR || join(getDataDir(), 'archives');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the logs directory path
|
||||
* @returns {string} Logs directory path
|
||||
*/
|
||||
export function getLogsDir() {
|
||||
return process.env.CLAUDE_MEM_LOGS_DIR || join(getDataDir(), 'logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the compact flag file path
|
||||
* @returns {string} Compact flag file path
|
||||
*/
|
||||
export function getCompactFlagPath() {
|
||||
return join(getDataDir(), '.compact-running');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the claude-mem package root directory
|
||||
* @returns {Promise<string>} Package root path
|
||||
*/
|
||||
export async function getPackageRoot() {
|
||||
// Method 1: Check if we're running from development
|
||||
const devPath = join(homedir(), 'Scripts', 'claude-mem-source');
|
||||
const { existsSync } = await import('fs');
|
||||
if (existsSync(join(devPath, 'package.json'))) {
|
||||
return devPath;
|
||||
}
|
||||
|
||||
// Method 2: Follow the binary symlink
|
||||
try {
|
||||
const { execSync } = await import('child_process');
|
||||
const { realpathSync } = await import('fs');
|
||||
const binPath = execSync('which claude-mem', { encoding: 'utf8' }).trim();
|
||||
const realBinPath = realpathSync(binPath);
|
||||
// Binary is typically at package_root/dist/claude-mem.min.js
|
||||
return join(realBinPath, '../..');
|
||||
} catch {}
|
||||
|
||||
throw new Error('Cannot locate claude-mem package root');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the project root directory
|
||||
* Uses CLAUDE_PROJECT_DIR environment variable if available, otherwise falls back to cwd
|
||||
* @returns {string} Project root path
|
||||
*/
|
||||
export function getProjectRoot() {
|
||||
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives project name from CLAUDE_PROJECT_DIR or current working directory
|
||||
* Priority: CLAUDE_PROJECT_DIR > cwd parameter > process.cwd()
|
||||
* @param {string} [cwd] - Optional current working directory from hook payload
|
||||
* @returns {string} Project name (basename of project directory)
|
||||
*/
|
||||
export function getProjectName(cwd) {
|
||||
const projectRoot = process.env.CLAUDE_PROJECT_DIR || cwd || process.cwd();
|
||||
return basename(projectRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all common paths used by hooks
|
||||
* @returns {Object} Object containing all common paths
|
||||
*/
|
||||
export function getPaths() {
|
||||
return {
|
||||
dataDir: getDataDir(),
|
||||
settingsPath: getSettingsPath(),
|
||||
archivesDir: getArchivesDir(),
|
||||
logsDir: getLogsDir(),
|
||||
compactFlagPath: getCompactFlagPath()
|
||||
};
|
||||
}
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Stop Hook - Simple Orchestrator
|
||||
*
|
||||
* Signals session end to SDK, which generates and stores the overview via CLI.
|
||||
* Cleans up SDK transcript from UI.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { renderEndMessage, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||
import { getProjectName } from './shared/path-resolver.js';
|
||||
|
||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||
|
||||
function debugLog(message, data = {}) {
|
||||
if (process.env.CLAUDE_MEM_DEBUG === 'true') {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logLine = `[${timestamp}] HOOK DEBUG: ${message} ${JSON.stringify(data)}\n`;
|
||||
try {
|
||||
fs.appendFileSync(HOOKS_LOG, logLine);
|
||||
process.stderr.write(logLine);
|
||||
} catch (error) {
|
||||
// Silent fail on log errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN
|
||||
// =============================================================================
|
||||
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => { input += chunk; });
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
let payload;
|
||||
try {
|
||||
payload = input ? JSON.parse(input) : {};
|
||||
} catch (error) {
|
||||
debugLog('Stop: JSON parse error', { error: error.message });
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { cwd } = payload;
|
||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
||||
|
||||
// Return immediately with async mode
|
||||
console.log(JSON.stringify({ async: true, asyncTimeout: 180000 }));
|
||||
|
||||
try {
|
||||
// Load SDK session info
|
||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
||||
if (!fs.existsSync(sessionFile)) {
|
||||
debugLog('Stop: No streaming session found', { project });
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
||||
const sdkSessionId = sessionData.sdkSessionId;
|
||||
const claudeSessionId = sessionData.claudeSessionId;
|
||||
|
||||
debugLog('Stop: Ending SDK session', { sdkSessionId, claudeSessionId });
|
||||
|
||||
// Build end message - SDK will call `claude-mem store-overview` and `chroma_add_documents`
|
||||
const message = renderEndMessage({
|
||||
project,
|
||||
sessionId: claudeSessionId
|
||||
});
|
||||
|
||||
// Send end message and wait for SDK to complete
|
||||
const response = query({
|
||||
prompt: message,
|
||||
options: {
|
||||
model: HOOK_CONFIG.sdk.model,
|
||||
resume: sdkSessionId,
|
||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
||||
maxTokens: HOOK_CONFIG.sdk.maxTokensEnd,
|
||||
cwd // Must match where transcript was created
|
||||
}
|
||||
});
|
||||
|
||||
// Consume the response stream (wait for SDK to finish storing via CLI)
|
||||
for await (const msg of response) {
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'tool_use') {
|
||||
debugLog('Stop: SDK tool call', { tool: block.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('Stop: SDK session ended', { sdkSessionId });
|
||||
|
||||
// Delete SDK memories transcript from Claude Code UI
|
||||
const sanitizedCwd = cwd.replace(/\//g, '-');
|
||||
const projectsDir = path.join(process.env.HOME, '.claude', 'projects', sanitizedCwd);
|
||||
const memoriesTranscriptPath = path.join(projectsDir, `${sdkSessionId}.jsonl`);
|
||||
|
||||
if (fs.existsSync(memoriesTranscriptPath)) {
|
||||
fs.unlinkSync(memoriesTranscriptPath);
|
||||
debugLog('Stop: Cleaned up memories transcript', { memoriesTranscriptPath });
|
||||
}
|
||||
|
||||
// Clean up session file
|
||||
fs.unlinkSync(sessionFile);
|
||||
debugLog('Stop: Session ended and cleaned up', { project });
|
||||
|
||||
} catch (error) {
|
||||
debugLog('Stop: Error ending session', { error: error.message });
|
||||
}
|
||||
|
||||
// Exit cleanly after async processing completes
|
||||
process.exit(0);
|
||||
});
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* User Prompt Submit Hook - Streaming SDK Version
|
||||
*
|
||||
* Starts a streaming SDK session that will process tool responses in real-time.
|
||||
* Saves the SDK session ID for post-tool-use and stop hooks to resume.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { renderSystemPrompt, HOOK_CONFIG } from './shared/hook-prompt-renderer.js';
|
||||
import { getProjectName } from './shared/path-resolver.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const SESSION_DIR = path.join(process.env.HOME || '', '.claude-mem', 'sessions');
|
||||
const HOOKS_LOG = path.join(process.env.HOME || '', '.claude-mem', 'logs', 'hooks.log');
|
||||
|
||||
function debugLog(message, data = {}) {
|
||||
if (process.env.CLAUDE_MEM_DEBUG === 'true') {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logLine = `[${timestamp}] HOOK DEBUG: ${message} ${JSON.stringify(data)}\n`;
|
||||
try {
|
||||
fs.appendFileSync(HOOKS_LOG, logLine);
|
||||
process.stderr.write(logLine);
|
||||
} catch (error) {
|
||||
// Silent fail on log errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removed: buildStreamingSystemPrompt function
|
||||
// Now using centralized config from hook-prompt-renderer.js
|
||||
|
||||
// =============================================================================
|
||||
// MAIN
|
||||
// =============================================================================
|
||||
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => { input += chunk; });
|
||||
|
||||
process.stdin.on('end', async () => {
|
||||
let payload;
|
||||
try {
|
||||
payload = input ? JSON.parse(input) : {};
|
||||
} catch (error) {
|
||||
debugLog('UserPromptSubmit: JSON parse error', { error: error.message });
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { prompt, cwd, session_id, timestamp } = payload;
|
||||
const project = cwd ? getProjectName(cwd) : 'unknown';
|
||||
const date = timestamp ? timestamp.split('T')[0] : new Date().toISOString().split('T')[0];
|
||||
|
||||
debugLog('UserPromptSubmit: Starting streaming session', { project, session_id });
|
||||
|
||||
// Generate title and subtitle non-blocking
|
||||
if (prompt && session_id && project) {
|
||||
import('child_process').then(({ spawn }) => {
|
||||
const titleProcess = spawn('claude-mem', [
|
||||
'generate-title',
|
||||
'--save',
|
||||
'--project', project,
|
||||
'--session', session_id,
|
||||
prompt
|
||||
], {
|
||||
stdio: 'ignore',
|
||||
detached: true
|
||||
});
|
||||
titleProcess.unref();
|
||||
}).catch(error => {
|
||||
debugLog('UserPromptSubmit: Error spawning title generator', { error: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Build system prompt using centralized config
|
||||
const systemPrompt = renderSystemPrompt({
|
||||
project,
|
||||
sessionId: session_id,
|
||||
date,
|
||||
userPrompt: prompt || ''
|
||||
});
|
||||
|
||||
// Start SDK session using centralized config
|
||||
const response = query({
|
||||
prompt: systemPrompt,
|
||||
options: {
|
||||
model: HOOK_CONFIG.sdk.model,
|
||||
allowedTools: HOOK_CONFIG.sdk.allowedTools,
|
||||
maxTokens: HOOK_CONFIG.sdk.maxTokensSystem,
|
||||
cwd // SDK will save transcript in this directory
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for session ID from init message and consume entire stream
|
||||
let sdkSessionId = null;
|
||||
for await (const message of response) {
|
||||
if (message.type === 'system' && message.subtype === 'init') {
|
||||
sdkSessionId = message.session_id;
|
||||
debugLog('UserPromptSubmit: Got SDK session ID', { sdkSessionId });
|
||||
}
|
||||
// Don't break - consume entire stream so transcript gets written
|
||||
}
|
||||
|
||||
if (sdkSessionId) {
|
||||
// Save session info for other hooks
|
||||
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
||||
const sessionFile = path.join(SESSION_DIR, `${project}_streaming.json`);
|
||||
fs.writeFileSync(sessionFile, JSON.stringify({
|
||||
sdkSessionId,
|
||||
claudeSessionId: session_id,
|
||||
project,
|
||||
startedAt: timestamp,
|
||||
date
|
||||
}, null, 2));
|
||||
|
||||
debugLog('UserPromptSubmit: SDK session started', { sdkSessionId, sessionFile });
|
||||
}
|
||||
} catch (error) {
|
||||
debugLog('UserPromptSubmit: Error starting SDK session', { error: error.message });
|
||||
}
|
||||
|
||||
// Return success to Claude Code
|
||||
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
||||
process.exit(0);
|
||||
});
|
||||
Reference in New Issue
Block a user