feat: Implement Worker Service with session management and SDK integration

- Added WorkerService to handle long-running HTTP service with session management.
- Implemented endpoints for initializing, observing, finalizing, checking status, and deleting sessions.
- Integrated with Claude SDK for processing observations and generating responses.
- Added port allocator utility to dynamically find available ports for the service.
- Configured TypeScript settings for the project.
This commit is contained in:
Alex Newman
2025-10-17 15:59:36 -04:00
parent d6462919cb
commit 372854948c
57 changed files with 7055 additions and 6649 deletions
+63 -15
View File
@@ -1,4 +1,3 @@
import { spawn } from 'child_process';
import path from 'path';
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import { createHookResponse } from './hook-response.js';
@@ -11,10 +10,32 @@ export interface UserPromptSubmitInput {
}
/**
* New Hook - UserPromptSubmit
* Initializes SDK memory session in background
* Get worker service port from file
*/
export function newHook(input?: UserPromptSubmitInput): void {
async function getWorkerPort(): Promise<number | null> {
const { readFileSync, existsSync } = await import('fs');
const { join } = await import('path');
const { homedir } = await import('os');
const portFile = join(homedir(), '.claude-mem', 'worker.port');
if (!existsSync(portFile)) {
return null;
}
try {
const portStr = readFileSync(portFile, 'utf8').trim();
return parseInt(portStr, 10);
} catch {
return null;
}
}
/**
* New Hook - UserPromptSubmit
* Initializes SDK memory session via HTTP POST to worker service
*/
export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
if (!input) {
throw new Error('newHook requires input');
}
@@ -24,30 +45,57 @@ export function newHook(input?: UserPromptSubmitInput): void {
const db = new HooksDatabase();
try {
const existing = db.findActiveSDKSession(session_id);
// Check for any existing session (active, failed, or completed)
let existing = db.findActiveSDKSession(session_id);
let sessionDbId: number;
if (existing) {
// Session already active, just continue
sessionDbId = existing.id;
console.log(createHookResponse('UserPromptSubmit', true));
return;
}
const sessionId = db.createSDKSession(session_id, project, prompt);
// Check for inactive sessions we can reuse
const inactive = db.findAnySDKSession(session_id);
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
if (!pluginRoot) {
throw new Error('CLAUDE_PLUGIN_ROOT not set');
if (inactive) {
// Reactivate the existing session
sessionDbId = inactive.id;
db.reactivateSession(sessionDbId, prompt);
console.error(`[new-hook] Reactivated session ${sessionDbId} for Claude session ${session_id}`);
} else {
// Create new session
sessionDbId = db.createSDKSession(session_id, project, prompt);
console.error(`[new-hook] Created new session ${sessionDbId} for Claude session ${session_id}`);
}
const workerPath = path.join(pluginRoot, 'scripts', 'hooks', 'worker.js');
const child = spawn('bun', [workerPath, sessionId.toString()], {
detached: true,
stdio: 'ignore'
// Find worker service port
const port = await getWorkerPort();
if (!port) {
console.error('[new-hook] Worker service not running. Start with: npm run worker:start');
console.log(createHookResponse('UserPromptSubmit', true)); // Don't block Claude
return;
}
// Initialize session via HTTP
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project, userPrompt: prompt }),
signal: AbortSignal.timeout(5000)
});
child.unref();
if (!response.ok) {
console.error('[new-hook] Failed to init session:', await response.text());
}
console.log(createHookResponse('UserPromptSubmit', true));
} catch (error: any) {
console.error('[new-hook] FATAL ERROR:', error.message);
console.error('[new-hook] Stack:', error.stack);
console.error('[new-hook] Full error:', JSON.stringify(error, Object.getOwnPropertyNames(error)));
console.log(createHookResponse('UserPromptSubmit', true)); // Don't block Claude
} finally {
db.close();
}