372854948c
- 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.
313 lines
7.7 KiB
TypeScript
313 lines
7.7 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
|
|
|
|
/**
|
|
* Lightweight database interface for hooks
|
|
* Provides simple, synchronous operations for hook commands
|
|
* No complex logic - just basic CRUD operations
|
|
*/
|
|
export class HooksDatabase {
|
|
private db: Database;
|
|
|
|
constructor() {
|
|
ensureDir(DATA_DIR);
|
|
this.db = new Database(DB_PATH);
|
|
|
|
// Ensure optimized settings
|
|
this.db.pragma('journal_mode = WAL');
|
|
this.db.pragma('synchronous = NORMAL');
|
|
this.db.pragma('foreign_keys = ON');
|
|
|
|
// Run migration to add worker_port column if it doesn't exist
|
|
this.ensureWorkerPortColumn();
|
|
}
|
|
|
|
/**
|
|
* Ensure worker_port column exists (migration)
|
|
*/
|
|
private ensureWorkerPortColumn(): void {
|
|
try {
|
|
// Check if column exists
|
|
const tableInfo = this.db.pragma('table_info(sdk_sessions)');
|
|
const hasWorkerPort = (tableInfo as any[]).some((col: any) => col.name === 'worker_port');
|
|
|
|
if (!hasWorkerPort) {
|
|
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER');
|
|
console.error('[HooksDatabase] Added worker_port column to sdk_sessions table');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('[HooksDatabase] Migration error:', error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get recent session summaries for a project
|
|
*/
|
|
getRecentSummaries(project: string, limit: number = 10): Array<{
|
|
request: string | null;
|
|
investigated: string | null;
|
|
learned: string | null;
|
|
completed: string | null;
|
|
next_steps: string | null;
|
|
files_read: string | null;
|
|
files_edited: string | null;
|
|
notes: string | null;
|
|
created_at: string;
|
|
}> {
|
|
const stmt = this.db.prepare(`
|
|
SELECT
|
|
request, investigated, learned, completed, next_steps,
|
|
files_read, files_edited, notes, created_at
|
|
FROM session_summaries
|
|
WHERE project = ?
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
`);
|
|
|
|
return stmt.all(project, limit) as any[];
|
|
}
|
|
|
|
/**
|
|
* Get recent observations for a project
|
|
*/
|
|
getRecentObservations(project: string, limit: number = 20): Array<{
|
|
type: string;
|
|
text: string;
|
|
created_at: string;
|
|
}> {
|
|
const stmt = this.db.prepare(`
|
|
SELECT type, text, created_at
|
|
FROM observations
|
|
WHERE project = ?
|
|
ORDER BY created_at_epoch DESC
|
|
LIMIT ?
|
|
`);
|
|
|
|
return stmt.all(project, limit) as any[];
|
|
}
|
|
|
|
/**
|
|
* Find active SDK session for a Claude session
|
|
*/
|
|
findActiveSDKSession(claudeSessionId: string): {
|
|
id: number;
|
|
sdk_session_id: string | null;
|
|
project: string;
|
|
worker_port: number | null;
|
|
} | null {
|
|
const stmt = this.db.prepare(`
|
|
SELECT id, sdk_session_id, project, worker_port
|
|
FROM sdk_sessions
|
|
WHERE claude_session_id = ? AND status = 'active'
|
|
LIMIT 1
|
|
`);
|
|
|
|
return stmt.get(claudeSessionId) as any || null;
|
|
}
|
|
|
|
/**
|
|
* Find any SDK session for a Claude session (active, failed, or completed)
|
|
*/
|
|
findAnySDKSession(claudeSessionId: string): { id: number } | null {
|
|
const stmt = this.db.prepare(`
|
|
SELECT id
|
|
FROM sdk_sessions
|
|
WHERE claude_session_id = ?
|
|
LIMIT 1
|
|
`);
|
|
|
|
return stmt.get(claudeSessionId) as any || null;
|
|
}
|
|
|
|
/**
|
|
* Reactivate an existing session
|
|
*/
|
|
reactivateSession(id: number, userPrompt: string): void {
|
|
const stmt = this.db.prepare(`
|
|
UPDATE sdk_sessions
|
|
SET status = 'active', user_prompt = ?, worker_port = NULL
|
|
WHERE id = ?
|
|
`);
|
|
|
|
stmt.run(userPrompt, id);
|
|
}
|
|
|
|
/**
|
|
* Create a new SDK session
|
|
*/
|
|
createSDKSession(claudeSessionId: string, project: string, userPrompt: string): number {
|
|
const now = new Date();
|
|
const nowEpoch = now.getTime();
|
|
|
|
const stmt = this.db.prepare(`
|
|
INSERT INTO sdk_sessions
|
|
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
|
VALUES (?, ?, ?, ?, ?, 'active')
|
|
`);
|
|
|
|
const result = stmt.run(claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
|
|
return result.lastInsertRowid as number;
|
|
}
|
|
|
|
/**
|
|
* Update SDK session ID (captured from init message)
|
|
*/
|
|
updateSDKSessionId(id: number, sdkSessionId: string): void {
|
|
const stmt = this.db.prepare(`
|
|
UPDATE sdk_sessions
|
|
SET sdk_session_id = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
stmt.run(sdkSessionId, id);
|
|
}
|
|
|
|
/**
|
|
* Set worker port for a session
|
|
*/
|
|
setWorkerPort(id: number, port: number): void {
|
|
const stmt = this.db.prepare(`
|
|
UPDATE sdk_sessions
|
|
SET worker_port = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
stmt.run(port, id);
|
|
}
|
|
|
|
/**
|
|
* Get worker port for a session
|
|
*/
|
|
getWorkerPort(id: number): number | null {
|
|
const stmt = this.db.prepare(`
|
|
SELECT worker_port
|
|
FROM sdk_sessions
|
|
WHERE id = ?
|
|
LIMIT 1
|
|
`);
|
|
|
|
const result = stmt.get(id) as { worker_port: number | null } | undefined;
|
|
return result?.worker_port || null;
|
|
}
|
|
|
|
/**
|
|
* Store an observation (from SDK parsing)
|
|
*/
|
|
storeObservation(
|
|
sdkSessionId: string,
|
|
project: string,
|
|
type: string,
|
|
text: string
|
|
): void {
|
|
const now = new Date();
|
|
const nowEpoch = now.getTime();
|
|
|
|
const stmt = this.db.prepare(`
|
|
INSERT INTO observations
|
|
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
stmt.run(sdkSessionId, project, text, type, now.toISOString(), nowEpoch);
|
|
}
|
|
|
|
/**
|
|
* Store a session summary (from SDK parsing)
|
|
*/
|
|
storeSummary(
|
|
sdkSessionId: string,
|
|
project: string,
|
|
summary: {
|
|
request?: string;
|
|
investigated?: string;
|
|
learned?: string;
|
|
completed?: string;
|
|
next_steps?: string;
|
|
files_read?: string;
|
|
files_edited?: string;
|
|
notes?: string;
|
|
}
|
|
): void {
|
|
const now = new Date();
|
|
const nowEpoch = now.getTime();
|
|
|
|
const stmt = this.db.prepare(`
|
|
INSERT INTO session_summaries
|
|
(sdk_session_id, project, request, investigated, learned, completed,
|
|
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
stmt.run(
|
|
sdkSessionId,
|
|
project,
|
|
summary.request || null,
|
|
summary.investigated || null,
|
|
summary.learned || null,
|
|
summary.completed || null,
|
|
summary.next_steps || null,
|
|
summary.files_read || null,
|
|
summary.files_edited || null,
|
|
summary.notes || null,
|
|
now.toISOString(),
|
|
nowEpoch
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Mark SDK session as completed
|
|
*/
|
|
markSessionCompleted(id: number): void {
|
|
const now = new Date();
|
|
const nowEpoch = now.getTime();
|
|
|
|
const stmt = this.db.prepare(`
|
|
UPDATE sdk_sessions
|
|
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
stmt.run(now.toISOString(), nowEpoch, id);
|
|
}
|
|
|
|
/**
|
|
* Mark SDK session as failed
|
|
*/
|
|
markSessionFailed(id: number): void {
|
|
const now = new Date();
|
|
const nowEpoch = now.getTime();
|
|
|
|
const stmt = this.db.prepare(`
|
|
UPDATE sdk_sessions
|
|
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
stmt.run(now.toISOString(), nowEpoch, id);
|
|
}
|
|
|
|
/**
|
|
* Clean up orphaned active sessions (called on worker startup)
|
|
*/
|
|
cleanupOrphanedSessions(): number {
|
|
const now = new Date();
|
|
const nowEpoch = now.getTime();
|
|
|
|
const stmt = this.db.prepare(`
|
|
UPDATE sdk_sessions
|
|
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
|
WHERE status = 'active'
|
|
`);
|
|
|
|
const result = stmt.run(now.toISOString(), nowEpoch);
|
|
return result.changes;
|
|
}
|
|
|
|
/**
|
|
* Close the database connection
|
|
*/
|
|
close(): void {
|
|
this.db.close();
|
|
}
|
|
}
|