e81ea69143
- Added CLI commands for context, new session, save observation, and summary. - Created HooksDatabase for managing SDK sessions and observations. - Implemented migration 004 to add new tables: sdk_sessions, observation_queue, observations, and session_summaries. - Developed hook functions for context display, session initialization, observation queuing, and session finalization. - Added comprehensive tests for database schema and hook functionality. - Documented Phase 1 implementation in PHASE1-COMPLETE.md.
265 lines
6.5 KiB
TypeScript
265 lines
6.5 KiB
TypeScript
import { Database } from 'bun:sqlite';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import { PathDiscovery } from '../path-discovery.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() {
|
|
const dataDir = PathDiscovery.getInstance().getDataDirectory();
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
|
|
const dbPath = path.join(dataDir, 'claude-mem.db');
|
|
this.db = new Database(dbPath, { create: true, readwrite: true });
|
|
|
|
// Ensure optimized settings
|
|
this.db.run('PRAGMA journal_mode = WAL');
|
|
this.db.run('PRAGMA synchronous = NORMAL');
|
|
this.db.run('PRAGMA foreign_keys = ON');
|
|
}
|
|
|
|
/**
|
|
* 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 query = this.db.query(`
|
|
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 query.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;
|
|
} | null {
|
|
const query = this.db.query(`
|
|
SELECT id, sdk_session_id, project
|
|
FROM sdk_sessions
|
|
WHERE claude_session_id = ? AND status = 'active'
|
|
LIMIT 1
|
|
`);
|
|
|
|
return query.get(claudeSessionId) as any || null;
|
|
}
|
|
|
|
/**
|
|
* Create a new SDK session
|
|
*/
|
|
createSDKSession(claudeSessionId: string, project: string, userPrompt: string): number {
|
|
const now = new Date();
|
|
const nowEpoch = now.getTime();
|
|
|
|
const query = this.db.query(`
|
|
INSERT INTO sdk_sessions
|
|
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
|
VALUES (?, ?, ?, ?, ?, 'active')
|
|
`);
|
|
|
|
query.run(claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
|
|
|
|
// Get the last inserted ID
|
|
const lastIdQuery = this.db.query('SELECT last_insert_rowid() as id');
|
|
const result = lastIdQuery.get() as { id: number };
|
|
return result.id;
|
|
}
|
|
|
|
/**
|
|
* Update SDK session ID (captured from init message)
|
|
*/
|
|
updateSDKSessionId(id: number, sdkSessionId: string): void {
|
|
const query = this.db.query(`
|
|
UPDATE sdk_sessions
|
|
SET sdk_session_id = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
query.run(sdkSessionId, id);
|
|
}
|
|
|
|
/**
|
|
* Queue an observation for SDK processing
|
|
*/
|
|
queueObservation(
|
|
sdkSessionId: string,
|
|
toolName: string,
|
|
toolInput: string,
|
|
toolOutput: string
|
|
): void {
|
|
const nowEpoch = Date.now();
|
|
|
|
const query = this.db.query(`
|
|
INSERT INTO observation_queue
|
|
(sdk_session_id, tool_name, tool_input, tool_output, created_at_epoch)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
query.run(sdkSessionId, toolName, toolInput, toolOutput, nowEpoch);
|
|
}
|
|
|
|
/**
|
|
* Get pending observations for SDK processing
|
|
*/
|
|
getPendingObservations(sdkSessionId: string, limit: number = 10): Array<{
|
|
id: number;
|
|
tool_name: string;
|
|
tool_input: string;
|
|
tool_output: string;
|
|
created_at_epoch: number;
|
|
}> {
|
|
const query = this.db.query(`
|
|
SELECT id, tool_name, tool_input, tool_output, created_at_epoch
|
|
FROM observation_queue
|
|
WHERE sdk_session_id = ? AND processed_at_epoch IS NULL
|
|
ORDER BY created_at_epoch ASC
|
|
LIMIT ?
|
|
`);
|
|
|
|
return query.all(sdkSessionId, limit) as any[];
|
|
}
|
|
|
|
/**
|
|
* Mark observation as processed
|
|
*/
|
|
markObservationProcessed(id: number): void {
|
|
const nowEpoch = Date.now();
|
|
|
|
const query = this.db.query(`
|
|
UPDATE observation_queue
|
|
SET processed_at_epoch = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
query.run(nowEpoch, id);
|
|
}
|
|
|
|
/**
|
|
* 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 query = this.db.query(`
|
|
INSERT INTO observations
|
|
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
query.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 query = this.db.query(`
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
query.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 query = this.db.query(`
|
|
UPDATE sdk_sessions
|
|
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
query.run(now.toISOString(), nowEpoch, id);
|
|
}
|
|
|
|
/**
|
|
* Mark SDK session as failed
|
|
*/
|
|
markSessionFailed(id: number): void {
|
|
const now = new Date();
|
|
const nowEpoch = now.getTime();
|
|
|
|
const query = this.db.query(`
|
|
UPDATE sdk_sessions
|
|
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
|
WHERE id = ?
|
|
`);
|
|
|
|
query.run(now.toISOString(), nowEpoch, id);
|
|
}
|
|
|
|
/**
|
|
* Close the database connection
|
|
*/
|
|
close(): void {
|
|
this.db.close();
|
|
}
|
|
}
|