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:
+12
-63
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user