refactor: Convert all hooks to HTTP clients (remove all SQL)

Architecture transformation: Hooks → HTTP → Worker → Database

**context-hook.ts** (843 → 104 lines, 88% reduction)
- Remove all database imports and raw SQL queries
- HTTP GET to /api/context/inject
- Returns both formatted (stderr) and unformatted (stdout) context
- Dual output: colored display for users, plain text for model

**user-message-hook.ts** (updated, 113 lines)
- HTTP GET to /api/context/inject with colors=true
- Displays formatted context to users via stderr
- No database dependencies

**save-hook.ts** (418 → 99 lines, 76% reduction)
- Remove all SessionStore database methods
- HTTP POST to /api/sessions/observations
- Worker handles privacy checks and observation creation
- Fire-and-forget pattern with 2s timeout

**summary-hook.ts** (435 → 200 lines, 54% reduction)
- Remove all SessionStore database methods
- Keep local transcript parsing (hook has file access)
- HTTP POST to /api/sessions/summarize
- Worker handles privacy checks and summary generation

**cleanup-hook.ts** (414 → 90 lines, 78% reduction)
- Remove all SessionStore database methods
- HTTP POST to /api/sessions/complete
- Worker handles session completion and DB cleanup
- Non-fatal if worker unavailable

**Benefits:**
- Zero native module dependencies in hooks (Node.js or Bun compatible)
- Hooks can run in any runtime without recompilation
- All database operations centralized in worker service
- Simpler, more maintainable hook code
- Complete separation of concerns: I/O vs business logic
This commit is contained in:
Alex Newman
2025-12-05 19:10:51 -05:00
parent d3aaef926b
commit 8e0b1ee4e1
5 changed files with 128 additions and 968 deletions
+12 -63
View File
@@ -1,15 +1,15 @@
/**
* Save Hook - PostToolUse
* Consolidated entry point + logic
*
* Pure HTTP client - sends data to worker, worker handles all database operations
* including privacy checks. This allows the hook to run under any runtime
* (Node.js or Bun) since it has no native module dependencies.
*/
import { stdin } from 'process';
import { SessionStore } from '../services/sqlite/SessionStore.js';
import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { silentDebug } from '../utils/silent-debug.js';
import { stripMemoryTagsFromJson } from '../utils/tag-stripping.js';
export interface PostToolUseInput {
session_id: string;
@@ -29,9 +29,8 @@ const SKIP_TOOLS = new Set([
'AskUserQuestion' // User interaction, not substantive work
]);
/**
* Save Hook Main Logic
* Save Hook Main Logic - Fire-and-forget HTTP client
*/
async function saveHook(input?: PostToolUseInput): Promise<void> {
if (!input) {
@@ -48,72 +47,24 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
// Ensure worker is running
await ensureWorkerRunning();
const db = new SessionStore();
// Get or create session
const sessionDbId = db.createSDKSession(session_id, '', '');
const promptNumber = db.getPromptCounter(sessionDbId);
// Skip observation if user prompt was entirely private
// This respects the user's intent: if they marked the entire prompt as <private>,
// they don't want ANY observations from that interaction
const userPrompt = db.getUserPrompt(session_id, promptNumber);
if (!userPrompt || userPrompt.trim() === '') {
silentDebug('[save-hook] Skipping observation - user prompt was entirely private', {
session_id,
promptNumber,
tool_name
});
db.close();
console.log(createHookResponse('PostToolUse', true));
return;
}
db.close();
const port = getWorkerPort();
const toolStr = logger.formatTool(tool_name, tool_input);
const port = getWorkerPort();
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
sessionId: sessionDbId,
workerPort: port
});
try {
// Serialize and strip memory tags from tool_input and tool_response
// This prevents recursive storage of context and respects <private> tags
let cleanedToolInput = '{}';
let cleanedToolResponse = '{}';
try {
cleanedToolInput = tool_input !== undefined
? stripMemoryTagsFromJson(JSON.stringify(tool_input))
: '{}';
} catch (error) {
// Handle circular references or other JSON.stringify errors
silentDebug('[save-hook] Failed to stringify tool_input:', { error, tool_name });
cleanedToolInput = '{"error": "Failed to serialize tool_input"}';
}
try {
cleanedToolResponse = tool_response !== undefined
? stripMemoryTagsFromJson(JSON.stringify(tool_response))
: '{}';
} catch (error) {
// Handle circular references or other JSON.stringify errors
silentDebug('[save-hook] Failed to stringify tool_response:', { error, tool_name });
cleanedToolResponse = '{"error": "Failed to serialize tool_response"}';
}
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/observations`, {
// Send to worker - worker handles privacy check and database operations
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
claudeSessionId: session_id,
tool_name,
tool_input: cleanedToolInput,
tool_response: cleanedToolResponse,
prompt_number: promptNumber,
tool_input,
tool_response,
cwd: cwd || ''
}),
signal: AbortSignal.timeout(2000)
@@ -122,19 +73,17 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
if (!response.ok) {
const errorText = await response.text();
logger.failure('HOOK', 'Failed to send observation', {
sessionId: sessionDbId,
status: response.status
}, errorText);
throw new Error(`Failed to send observation to worker: ${response.status} ${errorText}`);
}
logger.debug('HOOK', 'Observation sent successfully', { sessionId: sessionDbId, toolName: tool_name });
logger.debug('HOOK', 'Observation sent successfully', { toolName: tool_name });
} catch (error: any) {
// Only show restart message for connection errors, not HTTP errors
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
}
// Re-throw HTTP errors and other errors as-is
throw error;
}