feat: Implement Phase 1 of SDK agent architecture with hook integration

- 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.
This commit is contained in:
Alex Newman
2025-10-15 19:06:51 -04:00
parent 917ab9740c
commit e81ea69143
12 changed files with 1294 additions and 129 deletions
+264
View File
@@ -0,0 +1,264 @@
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();
}
}