claudeck 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/db.js CHANGED
@@ -1,1645 +1,3 @@
1
- import Database from "better-sqlite3";
2
- import { createHash } from "crypto";
3
- import { dbPath } from "./server/paths.js";
4
-
5
- const db = new Database(dbPath);
6
-
7
- // Enable WAL mode for better concurrent performance
8
- db.pragma("journal_mode = WAL");
9
-
10
- // Create tables
11
- db.exec(`
12
- CREATE TABLE IF NOT EXISTS sessions (
13
- id TEXT PRIMARY KEY,
14
- claude_session_id TEXT,
15
- project_name TEXT,
16
- project_path TEXT,
17
- created_at INTEGER DEFAULT (unixepoch()),
18
- last_used_at INTEGER DEFAULT (unixepoch())
19
- );
20
-
21
- CREATE TABLE IF NOT EXISTS costs (
22
- id INTEGER PRIMARY KEY AUTOINCREMENT,
23
- session_id TEXT REFERENCES sessions(id),
24
- cost_usd REAL,
25
- duration_ms INTEGER,
26
- num_turns INTEGER,
27
- created_at INTEGER DEFAULT (unixepoch())
28
- );
29
-
30
- CREATE TABLE IF NOT EXISTS messages (
31
- id INTEGER PRIMARY KEY AUTOINCREMENT,
32
- session_id TEXT REFERENCES sessions(id),
33
- role TEXT NOT NULL,
34
- content TEXT NOT NULL,
35
- created_at INTEGER DEFAULT (unixepoch())
36
- );
37
-
38
- CREATE TABLE IF NOT EXISTS claude_sessions (
39
- session_id TEXT NOT NULL,
40
- chat_id TEXT NOT NULL DEFAULT '',
41
- claude_session_id TEXT NOT NULL,
42
- PRIMARY KEY (session_id, chat_id)
43
- );
44
-
45
- CREATE TABLE IF NOT EXISTS push_subscriptions (
46
- endpoint TEXT PRIMARY KEY,
47
- keys_p256dh TEXT NOT NULL,
48
- keys_auth TEXT NOT NULL,
49
- created_at INTEGER DEFAULT (unixepoch())
50
- );
51
-
52
- CREATE TABLE IF NOT EXISTS todos (
53
- id INTEGER PRIMARY KEY AUTOINCREMENT,
54
- text TEXT NOT NULL,
55
- done INTEGER DEFAULT 0,
56
- position INTEGER DEFAULT 0,
57
- created_at INTEGER DEFAULT (unixepoch()),
58
- updated_at INTEGER DEFAULT (unixepoch())
59
- );
60
- `);
61
-
62
- // Migrations
63
- try { db.exec(`ALTER TABLE messages ADD COLUMN chat_id TEXT DEFAULT NULL`); } catch { /* exists */ }
64
- try { db.exec(`ALTER TABLE sessions ADD COLUMN title TEXT DEFAULT NULL`); } catch { /* exists */ }
65
- try { db.exec(`ALTER TABLE sessions ADD COLUMN pinned INTEGER DEFAULT 0`); } catch { /* exists */ }
66
- try { db.exec(`ALTER TABLE costs ADD COLUMN input_tokens INTEGER DEFAULT 0`); } catch { /* exists */ }
67
- try { db.exec(`ALTER TABLE costs ADD COLUMN output_tokens INTEGER DEFAULT 0`); } catch { /* exists */ }
68
- // New columns for costs table
69
- try { db.exec(`ALTER TABLE costs ADD COLUMN model TEXT DEFAULT NULL`); } catch { /* exists */ }
70
- try { db.exec(`ALTER TABLE costs ADD COLUMN stop_reason TEXT DEFAULT NULL`); } catch { /* exists */ }
71
- try { db.exec(`ALTER TABLE costs ADD COLUMN is_error INTEGER DEFAULT 0`); } catch { /* exists */ }
72
- try { db.exec(`ALTER TABLE costs ADD COLUMN cache_read_tokens INTEGER DEFAULT 0`); } catch { /* exists */ }
73
- try { db.exec(`ALTER TABLE costs ADD COLUMN cache_creation_tokens INTEGER DEFAULT 0`); } catch { /* exists */ }
74
- // New columns for messages table (workflow metadata)
75
- try { db.exec(`ALTER TABLE messages ADD COLUMN workflow_id TEXT DEFAULT NULL`); } catch { /* exists */ }
76
- try { db.exec(`ALTER TABLE messages ADD COLUMN workflow_step_index INTEGER DEFAULT NULL`); } catch { /* exists */ }
77
- try { db.exec(`ALTER TABLE messages ADD COLUMN workflow_step_label TEXT DEFAULT NULL`); } catch { /* exists */ }
78
- // AI-generated session summary
79
- try { db.exec(`ALTER TABLE sessions ADD COLUMN summary TEXT DEFAULT NULL`); } catch { /* exists */ }
80
- // Todo archive
81
- try { db.exec(`ALTER TABLE todos ADD COLUMN archived INTEGER DEFAULT 0`); } catch { /* exists */ }
82
- // Todo priority (0=none, 1=low, 2=medium, 3=high)
83
- try { db.exec(`ALTER TABLE todos ADD COLUMN priority INTEGER DEFAULT 0`); } catch { /* exists */ }
84
- // Session branching / conversation forking
85
- try { db.exec(`ALTER TABLE sessions ADD COLUMN parent_session_id TEXT DEFAULT NULL`); } catch { /* exists */ }
86
- try { db.exec(`ALTER TABLE sessions ADD COLUMN fork_message_id INTEGER DEFAULT NULL`); } catch { /* exists */ }
87
-
88
- // Agent context (shared memory between agents in a chain/orchestration run)
89
- db.exec(`
90
- CREATE TABLE IF NOT EXISTS agent_context (
91
- id INTEGER PRIMARY KEY AUTOINCREMENT,
92
- run_id TEXT NOT NULL,
93
- agent_id TEXT NOT NULL,
94
- key TEXT NOT NULL,
95
- value TEXT NOT NULL,
96
- created_at INTEGER DEFAULT (unixepoch()),
97
- UNIQUE(run_id, agent_id, key)
98
- );
99
- CREATE INDEX IF NOT EXISTS idx_agent_context_run ON agent_context(run_id);
100
- `);
101
-
102
- // Agent runs table (monitoring dashboard)
103
- db.exec(`
104
- CREATE TABLE IF NOT EXISTS agent_runs (
105
- id INTEGER PRIMARY KEY AUTOINCREMENT,
106
- run_id TEXT NOT NULL,
107
- agent_id TEXT NOT NULL,
108
- agent_title TEXT NOT NULL,
109
- run_type TEXT NOT NULL DEFAULT 'single',
110
- parent_id TEXT,
111
- status TEXT NOT NULL DEFAULT 'running',
112
- turns INTEGER DEFAULT 0,
113
- cost_usd REAL DEFAULT 0,
114
- duration_ms INTEGER DEFAULT 0,
115
- input_tokens INTEGER DEFAULT 0,
116
- output_tokens INTEGER DEFAULT 0,
117
- error TEXT,
118
- started_at INTEGER DEFAULT (unixepoch()),
119
- completed_at INTEGER
120
- );
121
- CREATE INDEX IF NOT EXISTS idx_agent_runs_agent ON agent_runs(agent_id);
122
- CREATE INDEX IF NOT EXISTS idx_agent_runs_started ON agent_runs(started_at);
123
- CREATE INDEX IF NOT EXISTS idx_agent_runs_run_id ON agent_runs(run_id);
124
- `);
125
-
126
- // Persistent memories table (cross-session context)
127
- db.exec(`
128
- CREATE TABLE IF NOT EXISTS memories (
129
- id INTEGER PRIMARY KEY AUTOINCREMENT,
130
- project_path TEXT NOT NULL,
131
- category TEXT NOT NULL DEFAULT 'discovery',
132
- content TEXT NOT NULL,
133
- content_hash TEXT,
134
- source_session_id TEXT,
135
- source_agent_id TEXT,
136
- relevance_score REAL DEFAULT 1.0,
137
- created_at INTEGER DEFAULT (unixepoch()),
138
- accessed_at INTEGER DEFAULT (unixepoch()),
139
- expires_at INTEGER
140
- );
141
- CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_path);
142
- CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
143
- CREATE INDEX IF NOT EXISTS idx_memories_relevance ON memories(relevance_score DESC);
144
- `);
145
-
146
- // Migration: add content_hash column if missing (existing DBs)
147
- try { db.exec(`ALTER TABLE memories ADD COLUMN content_hash TEXT`); } catch { /* already exists */ }
148
- try { db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_hash ON memories(project_path, content_hash)`); } catch { /* already exists */ }
149
-
150
- // FTS5 full-text search for memories
151
- db.exec(`
152
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
153
- content,
154
- content='memories',
155
- content_rowid='id'
156
- );
157
- `);
158
-
159
- // Triggers to keep FTS in sync
160
- db.exec(`
161
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
162
- INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
163
- END;
164
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
165
- INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
166
- END;
167
- CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE OF content ON memories BEGIN
168
- INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
169
- INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
170
- END;
171
- `);
172
-
173
- // ── Notifications table ──────────────────────────────────
174
- db.exec(`
175
- CREATE TABLE IF NOT EXISTS notifications (
176
- id INTEGER PRIMARY KEY AUTOINCREMENT,
177
- type TEXT NOT NULL,
178
- title TEXT NOT NULL,
179
- body TEXT,
180
- metadata TEXT,
181
- source_session_id TEXT,
182
- source_agent_id TEXT,
183
- read_at INTEGER DEFAULT NULL,
184
- created_at INTEGER DEFAULT (unixepoch())
185
- );
186
- CREATE INDEX IF NOT EXISTS idx_notif_created ON notifications(created_at DESC);
187
- CREATE INDEX IF NOT EXISTS idx_notif_unread ON notifications(read_at) WHERE read_at IS NULL;
188
- `);
189
-
190
- // ── Worktrees table ──────────────────────────────────────
191
- db.exec(`
192
- CREATE TABLE IF NOT EXISTS worktrees (
193
- id TEXT PRIMARY KEY,
194
- session_id TEXT,
195
- project_path TEXT NOT NULL,
196
- worktree_path TEXT NOT NULL,
197
- branch_name TEXT NOT NULL,
198
- base_branch TEXT NOT NULL,
199
- status TEXT DEFAULT 'active',
200
- user_prompt TEXT,
201
- created_at INTEGER DEFAULT (unixepoch()),
202
- completed_at INTEGER DEFAULT NULL
203
- );
204
- CREATE INDEX IF NOT EXISTS idx_wt_project ON worktrees(project_path);
205
- CREATE INDEX IF NOT EXISTS idx_wt_status ON worktrees(status);
206
- `);
207
-
208
- // Backfill content_hash for existing rows
209
- const unhashed = db.prepare(`SELECT id, project_path, content FROM memories WHERE content_hash IS NULL`).all();
210
- if (unhashed.length > 0) {
211
- const backfill = db.prepare(`UPDATE memories SET content_hash = ? WHERE id = ?`);
212
- const backfillTx = db.transaction((rows) => {
213
- for (const row of rows) {
214
- const hash = createHash("sha256").update(`${row.project_path}:${row.content}`).digest("hex");
215
- backfill.run(hash, row.id);
216
- }
217
- });
218
- backfillTx(unhashed);
219
- }
220
-
221
- // Backfill FTS index for existing memories not yet indexed
222
- try {
223
- const ftsCount = db.prepare(`SELECT COUNT(*) as c FROM memories_fts`).get();
224
- const memCount = db.prepare(`SELECT COUNT(*) as c FROM memories`).get();
225
- if (ftsCount.c < memCount.c) {
226
- db.exec(`INSERT INTO memories_fts(memories_fts) VALUES ('rebuild')`);
227
- }
228
- } catch { /* ignore */ }
229
-
230
- // Brags table
231
- db.exec(`
232
- CREATE TABLE IF NOT EXISTS brags (
233
- id INTEGER PRIMARY KEY AUTOINCREMENT,
234
- todo_id INTEGER REFERENCES todos(id),
235
- text TEXT NOT NULL,
236
- summary TEXT NOT NULL,
237
- created_at INTEGER DEFAULT (unixepoch())
238
- );
239
- `);
240
-
241
- // Indexes for query performance
242
- db.exec(`
243
- CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id);
244
- CREATE INDEX IF NOT EXISTS idx_messages_session_chat ON messages(session_id, chat_id);
245
- CREATE INDEX IF NOT EXISTS idx_costs_session_id ON costs(session_id);
246
- CREATE INDEX IF NOT EXISTS idx_costs_created_at ON costs(created_at);
247
- CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path);
248
- CREATE INDEX IF NOT EXISTS idx_sessions_pinned_last_used ON sessions(pinned DESC, last_used_at DESC);
249
- CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id) WHERE parent_session_id IS NOT NULL;
250
- `);
251
-
252
- // Deduplicated mode CASE subquery — used in 4 session listing queries
253
- const MODE_CASE = `
254
- CASE
255
- WHEN EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id AND m.chat_id IS NOT NULL)
256
- AND EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id AND m.chat_id IS NULL)
257
- THEN 'both'
258
- WHEN EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id AND m.chat_id IS NOT NULL)
259
- THEN 'parallel'
260
- ELSE 'single'
261
- END AS mode`;
262
-
263
- // Prepared statements
264
- const stmts = {
265
- createSession: db.prepare(
266
- `INSERT OR IGNORE INTO sessions (id, claude_session_id, project_name, project_path)
267
- VALUES (?, ?, ?, ?)`
268
- ),
269
- updateClaudeSessionId: db.prepare(
270
- `UPDATE sessions SET claude_session_id = ? WHERE id = ?`
271
- ),
272
- getSession: db.prepare(`SELECT * FROM sessions WHERE id = ?`),
273
- listSessions: db.prepare(
274
- `SELECT s.*, ${MODE_CASE}
275
- FROM sessions s ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
276
- ),
277
- listSessionsByProject: db.prepare(
278
- `SELECT s.*, ${MODE_CASE}
279
- FROM sessions s WHERE s.project_path = ? ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
280
- ),
281
- touchSession: db.prepare(
282
- `UPDATE sessions SET last_used_at = unixepoch() WHERE id = ?`
283
- ),
284
- addCost: db.prepare(
285
- `INSERT INTO costs (session_id, cost_usd, duration_ms, num_turns, input_tokens, output_tokens, model, stop_reason, is_error, cache_read_tokens, cache_creation_tokens) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
286
- ),
287
- addMessage: db.prepare(
288
- `INSERT INTO messages (session_id, role, content, chat_id, workflow_id, workflow_step_index, workflow_step_label) VALUES (?, ?, ?, ?, ?, ?, ?)`
289
- ),
290
- getMessages: db.prepare(
291
- `SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC`
292
- ),
293
- getMessagesByChatId: db.prepare(
294
- `SELECT * FROM messages WHERE session_id = ? AND chat_id = ? ORDER BY id ASC`
295
- ),
296
- getMessagesNoChatId: db.prepare(
297
- `SELECT * FROM messages WHERE session_id = ? AND chat_id IS NULL ORDER BY id ASC`
298
- ),
299
- getTotalCost: db.prepare(`SELECT COALESCE(SUM(cost_usd), 0) AS total FROM costs`),
300
- getProjectCost: db.prepare(
301
- `SELECT COALESCE(SUM(c.cost_usd), 0) AS total
302
- FROM costs c JOIN sessions s ON c.session_id = s.id
303
- WHERE s.project_path = ?`
304
- ),
305
- setClaudeSession: db.prepare(
306
- `INSERT OR REPLACE INTO claude_sessions (session_id, chat_id, claude_session_id) VALUES (?, ?, ?)`
307
- ),
308
- getClaudeSessionId: db.prepare(
309
- `SELECT claude_session_id FROM claude_sessions WHERE session_id = ? AND chat_id = ?`
310
- ),
311
- allClaudeSessions: db.prepare(
312
- `SELECT * FROM claude_sessions`
313
- ),
314
- updateSessionTitle: db.prepare(
315
- `UPDATE sessions SET title = ? WHERE id = ?`
316
- ),
317
- toggleSessionPin: db.prepare(
318
- `UPDATE sessions SET pinned = CASE WHEN pinned = 1 THEN 0 ELSE 1 END WHERE id = ?`
319
- ),
320
- updateSessionSummary: db.prepare(
321
- `UPDATE sessions SET summary = ? WHERE id = ?`
322
- ),
323
- searchSessions: db.prepare(
324
- `SELECT s.*, ${MODE_CASE}
325
- FROM sessions s WHERE s.project_path = ? AND (s.title LIKE ? OR s.project_name LIKE ?) ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
326
- ),
327
- searchSessionsAll: db.prepare(
328
- `SELECT s.*, ${MODE_CASE}
329
- FROM sessions s WHERE (s.title LIKE ? OR s.project_name LIKE ?) ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
330
- ),
331
- // Session branching
332
- getMessagesByIdRange: db.prepare(
333
- `SELECT role, content, created_at FROM messages WHERE session_id = ? AND id <= ? AND chat_id IS NULL ORDER BY id ASC`
334
- ),
335
- getLastMessageId: db.prepare(
336
- `SELECT MAX(id) as maxId FROM messages WHERE session_id = ? AND chat_id IS NULL`
337
- ),
338
- getBranches: db.prepare(
339
- `SELECT s.*, ${MODE_CASE} FROM sessions s WHERE s.parent_session_id = ? ORDER BY s.created_at DESC`
340
- ),
341
- getBranchCount: db.prepare(
342
- `SELECT COUNT(*) as count FROM sessions WHERE parent_session_id = ?`
343
- ),
344
- orphanChildren: db.prepare(
345
- `UPDATE sessions SET parent_session_id = NULL WHERE parent_session_id = ?`
346
- ),
347
- getSessionCosts: db.prepare(
348
- `SELECT s.id, s.title, s.project_name, s.last_used_at,
349
- COALESCE(SUM(c.cost_usd), 0) AS total_cost,
350
- COALESCE(SUM(c.num_turns), 0) AS turns,
351
- COALESCE(SUM(c.input_tokens), 0) AS input_tokens,
352
- COALESCE(SUM(c.output_tokens), 0) AS output_tokens
353
- FROM sessions s
354
- LEFT JOIN costs c ON c.session_id = s.id
355
- WHERE s.project_path = ?
356
- GROUP BY s.id
357
- ORDER BY total_cost DESC`
358
- ),
359
- getSessionCostsAll: db.prepare(
360
- `SELECT s.id, s.title, s.project_name, s.last_used_at,
361
- COALESCE(SUM(c.cost_usd), 0) AS total_cost,
362
- COALESCE(SUM(c.num_turns), 0) AS turns,
363
- COALESCE(SUM(c.input_tokens), 0) AS input_tokens,
364
- COALESCE(SUM(c.output_tokens), 0) AS output_tokens
365
- FROM sessions s
366
- LEFT JOIN costs c ON c.session_id = s.id
367
- GROUP BY s.id
368
- ORDER BY total_cost DESC`
369
- ),
370
- getCostTimeline: db.prepare(
371
- `SELECT date(c.created_at, 'unixepoch') AS date,
372
- SUM(c.cost_usd) AS cost
373
- FROM costs c
374
- WHERE c.created_at >= unixepoch() - 30 * 86400
375
- GROUP BY date(c.created_at, 'unixepoch')
376
- ORDER BY date ASC`
377
- ),
378
- // Todo CRUD
379
- listTodos: db.prepare(`SELECT * FROM todos WHERE archived = 0 ORDER BY position ASC, id ASC`),
380
- listArchivedTodos: db.prepare(`SELECT * FROM todos WHERE archived = 1 ORDER BY updated_at DESC`),
381
- createTodo: db.prepare(`INSERT INTO todos (text, position) VALUES (?, (SELECT COALESCE(MAX(position),0)+1 FROM todos))`),
382
- updateTodo: db.prepare(`UPDATE todos SET text = COALESCE(?, text), done = COALESCE(?, done), priority = COALESCE(?, priority), updated_at = unixepoch() WHERE id = ?`),
383
- archiveTodo: db.prepare(`UPDATE todos SET archived = ?, updated_at = unixepoch() WHERE id = ?`),
384
- deleteTodo: db.prepare(`DELETE FROM todos WHERE id = ?`),
385
- todoCounts: db.prepare(`
386
- SELECT
387
- (SELECT COUNT(*) FROM todos WHERE archived = 0) AS active,
388
- (SELECT COUNT(*) FROM todos WHERE archived = 1) AS archived,
389
- (SELECT COUNT(*) FROM brags) AS brags
390
- `),
391
-
392
- // Brag CRUD
393
- createBrag: db.prepare(`INSERT INTO brags (todo_id, text, summary) VALUES (?, ?, ?)`),
394
- listBrags: db.prepare(`SELECT * FROM brags ORDER BY created_at DESC`),
395
- deleteBrag: db.prepare(`DELETE FROM brags WHERE id = ?`),
396
-
397
- yearlyActivity: db.prepare(
398
- `SELECT
399
- date(c.created_at, 'unixepoch') AS date,
400
- COUNT(DISTINCT c.session_id) AS sessions,
401
- COUNT(*) AS queries,
402
- COALESCE(SUM(c.cost_usd), 0) AS cost,
403
- COALESCE(SUM(c.input_tokens), 0) AS input_tokens,
404
- COALESCE(SUM(c.output_tokens), 0) AS output_tokens,
405
- COALESCE(SUM(c.num_turns), 0) AS turns
406
- FROM costs c
407
- WHERE c.created_at >= unixepoch() - 365 * 86400
408
- GROUP BY date(c.created_at, 'unixepoch')
409
- ORDER BY date ASC`
410
- ),
411
- getCostTimelineByProject: db.prepare(
412
- `SELECT date(c.created_at, 'unixepoch') AS date,
413
- SUM(c.cost_usd) AS cost
414
- FROM costs c
415
- JOIN sessions s ON c.session_id = s.id
416
- WHERE s.project_path = ? AND c.created_at >= unixepoch() - 30 * 86400
417
- GROUP BY date(c.created_at, 'unixepoch')
418
- ORDER BY date ASC`
419
- ),
420
- getTotalTokens: db.prepare(
421
- `SELECT COALESCE(SUM(input_tokens), 0) AS input_tokens,
422
- COALESCE(SUM(output_tokens), 0) AS output_tokens
423
- FROM costs`
424
- ),
425
- getProjectTokens: db.prepare(
426
- `SELECT COALESCE(SUM(c.input_tokens), 0) AS input_tokens,
427
- COALESCE(SUM(c.output_tokens), 0) AS output_tokens
428
- FROM costs c JOIN sessions s ON c.session_id = s.id
429
- WHERE s.project_path = ?`
430
- ),
431
- };
432
-
433
- export function createSession(id, claudeSessionId, projectName, projectPath) {
434
- stmts.createSession.run(id, claudeSessionId, projectName, projectPath);
435
- }
436
-
437
- export function updateClaudeSessionId(id, claudeSessionId) {
438
- stmts.updateClaudeSessionId.run(claudeSessionId, id);
439
- }
440
-
441
- export function getSession(id) {
442
- return stmts.getSession.get(id);
443
- }
444
-
445
- export function listSessions(limit = 20, projectPath) {
446
- if (projectPath) {
447
- return stmts.listSessionsByProject.all(projectPath, limit);
448
- }
449
- return stmts.listSessions.all(limit);
450
- }
451
-
452
- export function touchSession(id) {
453
- stmts.touchSession.run(id);
454
- }
455
-
456
- export function addCost(sessionId, costUsd, durationMs, numTurns, inputTokens = 0, outputTokens = 0, { model = null, stopReason = null, isError = 0, cacheReadTokens = 0, cacheCreationTokens = 0 } = {}) {
457
- stmts.addCost.run(sessionId, costUsd, durationMs, numTurns, inputTokens, outputTokens, model, stopReason, isError, cacheReadTokens, cacheCreationTokens);
458
- }
459
-
460
- export function getTotalCost() {
461
- return stmts.getTotalCost.get().total;
462
- }
463
-
464
- export function getProjectCost(projectPath) {
465
- return stmts.getProjectCost.get(projectPath).total;
466
- }
467
-
468
- export function addMessage(sessionId, role, content, chatId = null, workflowMeta = null) {
469
- stmts.addMessage.run(sessionId, role, content, chatId, workflowMeta?.workflowId ?? null, workflowMeta?.stepIndex ?? null, workflowMeta?.stepLabel ?? null);
470
- }
471
-
472
- export function getMessages(sessionId) {
473
- return stmts.getMessages.all(sessionId);
474
- }
475
-
476
- export function getMessagesByChatId(sessionId, chatId) {
477
- return stmts.getMessagesByChatId.all(sessionId, chatId);
478
- }
479
-
480
- export function getMessagesNoChatId(sessionId) {
481
- return stmts.getMessagesNoChatId.all(sessionId);
482
- }
483
-
484
- export function setClaudeSession(sessionId, chatId, claudeSessionId) {
485
- stmts.setClaudeSession.run(sessionId, chatId, claudeSessionId);
486
- }
487
-
488
- export function getClaudeSessionId(sessionId, chatId) {
489
- const row = stmts.getClaudeSessionId.get(sessionId, chatId);
490
- return row ? row.claude_session_id : null;
491
- }
492
-
493
- export function allClaudeSessions() {
494
- return stmts.allClaudeSessions.all();
495
- }
496
-
497
- export function updateSessionTitle(id, title) {
498
- stmts.updateSessionTitle.run(title, id);
499
- }
500
-
501
- export function toggleSessionPin(id) {
502
- stmts.toggleSessionPin.run(id);
503
- }
504
-
505
- export function updateSessionSummary(id, summary) {
506
- stmts.updateSessionSummary.run(summary, id);
507
- }
508
-
509
- export function searchSessions(query, limit = 20, projectPath) {
510
- const pattern = `%${query}%`;
511
- if (projectPath) {
512
- return stmts.searchSessions.all(projectPath, pattern, pattern, limit);
513
- }
514
- return stmts.searchSessionsAll.all(pattern, pattern, limit);
515
- }
516
-
517
- export const deleteSession = db.transaction((id) => {
518
- // Orphan child forks before deleting parent
519
- stmts.orphanChildren.run(id);
520
- db.prepare("DELETE FROM claude_sessions WHERE session_id = ?").run(id);
521
- db.prepare("DELETE FROM costs WHERE session_id = ?").run(id);
522
- db.prepare("DELETE FROM messages WHERE session_id = ?").run(id);
523
- db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
524
- });
525
-
526
- // ── Session Branching / Forking ─────────────────────────
527
- export const forkSession = db.transaction((parentSessionId, forkMessageId) => {
528
- const parent = stmts.getSession.get(parentSessionId);
529
- if (!parent) throw new Error("Session not found");
530
-
531
- if (!forkMessageId) {
532
- const last = stmts.getLastMessageId.get(parentSessionId);
533
- forkMessageId = last?.maxId;
534
- if (!forkMessageId) throw new Error("No messages to fork");
535
- }
536
-
537
- const newId = createHash("sha256")
538
- .update(parentSessionId + Date.now() + Math.random())
539
- .digest("hex")
540
- .slice(0, 36);
541
- const title = `Fork of: ${parent.title || parent.project_name || "Untitled"}`;
542
-
543
- db.prepare(
544
- `INSERT INTO sessions (id, project_name, project_path, title, parent_session_id, fork_message_id)
545
- VALUES (?, ?, ?, ?, ?, ?)`
546
- ).run(newId, parent.project_name, parent.project_path, title, parentSessionId, forkMessageId);
547
-
548
- const messages = stmts.getMessagesByIdRange.all(parentSessionId, forkMessageId);
549
- const insertMsg = db.prepare(
550
- "INSERT INTO messages (session_id, role, content, created_at) VALUES (?, ?, ?, ?)"
551
- );
552
- for (const msg of messages) {
553
- insertMsg.run(newId, msg.role, msg.content, msg.created_at);
554
- }
555
-
556
- return stmts.getSession.get(newId);
557
- });
558
-
559
- export function getSessionBranches(sessionId) {
560
- return stmts.getBranches.all(sessionId);
561
- }
562
-
563
- export function getSessionBranchCount(sessionId) {
564
- return stmts.getBranchCount.get(sessionId).count;
565
- }
566
-
567
- export function getSessionLineage(sessionId) {
568
- const ancestors = [];
569
- let current = stmts.getSession.get(sessionId);
570
- while (current && current.parent_session_id) {
571
- const parent = stmts.getSession.get(current.parent_session_id);
572
- if (!parent) break;
573
- ancestors.unshift(parent);
574
- current = parent;
575
- }
576
- // Get siblings (other forks of the same parent)
577
- const session = stmts.getSession.get(sessionId);
578
- let siblings = [];
579
- if (session?.parent_session_id) {
580
- siblings = stmts.getBranches.all(session.parent_session_id)
581
- .filter(s => s.id !== sessionId);
582
- }
583
- return { ancestors, siblings };
584
- }
585
-
586
- export function getSessionCosts(projectPath) {
587
- if (projectPath) {
588
- return stmts.getSessionCosts.all(projectPath);
589
- }
590
- return stmts.getSessionCostsAll.all();
591
- }
592
-
593
- export function getCostTimeline(projectPath) {
594
- if (projectPath) {
595
- return stmts.getCostTimelineByProject.all(projectPath);
596
- }
597
- return stmts.getCostTimeline.all();
598
- }
599
-
600
- export function getTotalTokens() {
601
- return stmts.getTotalTokens.get();
602
- }
603
-
604
- export function getProjectTokens(projectPath) {
605
- return stmts.getProjectTokens.get(projectPath);
606
- }
607
-
608
- // ── Error categorization CASE (reused in multiple queries) ────
609
- const ERROR_CATEGORY_CASE = `
610
- CASE
611
- WHEN json_extract(tr.content, '$.content') LIKE '%ENOENT%'
612
- OR json_extract(tr.content, '$.content') LIKE '%does not exist%'
613
- OR json_extract(tr.content, '$.content') LIKE '%No such file%'
614
- THEN 'File Not Found'
615
- WHEN json_extract(tr.content, '$.content') LIKE '%Denied by user%'
616
- OR json_extract(tr.content, '$.content') LIKE '%Aborted by user%'
617
- THEN 'User Denied'
618
- WHEN json_extract(tr.content, '$.content') LIKE '%timed out%'
619
- THEN 'Timeout'
620
- WHEN json_extract(tr.content, '$.content') LIKE '%File has not been read%'
621
- OR json_extract(tr.content, '$.content') LIKE '%File has been modified%'
622
- THEN 'File State Error'
623
- WHEN json_extract(tr.content, '$.content') LIKE '%EISDIR%'
624
- OR json_extract(tr.content, '$.content') LIKE '%illegal operation on a directory%'
625
- THEN 'Directory Error'
626
- WHEN json_extract(tr.content, '$.content') LIKE '%Found % matches%'
627
- THEN 'Multiple Matches'
628
- WHEN json_extract(tr.content, '$.content') LIKE '%command not found%'
629
- THEN 'Command Not Found'
630
- WHEN json_extract(tr.content, '$.content') LIKE '%npm error%'
631
- OR json_extract(tr.content, '$.content') LIKE '%SyntaxError%'
632
- OR json_extract(tr.content, '$.content') LIKE '%error TS%'
633
- THEN 'Build/Runtime Error'
634
- ELSE 'Other'
635
- END`;
636
-
637
- // ── Analytics queries ──────────────────────────────────────────
638
-
639
- const analyticsStmts = {
640
- overviewAll: db.prepare(`
641
- SELECT
642
- (SELECT COUNT(*) FROM sessions) AS sessions,
643
- COUNT(*) AS queries,
644
- COALESCE(SUM(cost_usd), 0) AS totalCost,
645
- COALESCE(SUM(num_turns), 0) AS totalTurns,
646
- COALESCE(SUM(output_tokens), 0) AS totalOutputTokens
647
- FROM costs
648
- `),
649
- overviewByProject: db.prepare(`
650
- SELECT
651
- COUNT(DISTINCT s.id) AS sessions,
652
- COUNT(c.id) AS queries,
653
- COALESCE(SUM(c.cost_usd), 0) AS totalCost,
654
- COALESCE(SUM(c.num_turns), 0) AS totalTurns,
655
- COALESCE(SUM(c.output_tokens), 0) AS totalOutputTokens
656
- FROM sessions s
657
- LEFT JOIN costs c ON c.session_id = s.id
658
- WHERE s.project_path = ?
659
- `),
660
- errorRateAll: db.prepare(`
661
- SELECT
662
- COUNT(CASE WHEN json_extract(content, '$.isError') = 1 THEN 1 END) AS errors,
663
- COUNT(*) AS total
664
- FROM messages WHERE role = 'tool_result'
665
- `),
666
- errorRateByProject: db.prepare(`
667
- SELECT
668
- COUNT(CASE WHEN json_extract(m.content, '$.isError') = 1 THEN 1 END) AS errors,
669
- COUNT(*) AS total
670
- FROM messages m
671
- JOIN sessions s ON m.session_id = s.id
672
- WHERE m.role = 'tool_result' AND s.project_path = ?
673
- `),
674
- dailyBreakdownAll: db.prepare(`
675
- SELECT
676
- date(c.created_at, 'unixepoch') AS date,
677
- COUNT(*) AS queries,
678
- SUM(c.cost_usd) AS cost,
679
- SUM(c.num_turns) AS turns,
680
- SUM(c.output_tokens) AS output_tok
681
- FROM costs c
682
- WHERE c.created_at >= unixepoch() - 30 * 86400
683
- GROUP BY date(c.created_at, 'unixepoch')
684
- ORDER BY date ASC
685
- `),
686
- dailyBreakdownByProject: db.prepare(`
687
- SELECT
688
- date(c.created_at, 'unixepoch') AS date,
689
- COUNT(*) AS queries,
690
- SUM(c.cost_usd) AS cost,
691
- SUM(c.num_turns) AS turns,
692
- SUM(c.output_tokens) AS output_tok
693
- FROM costs c
694
- JOIN sessions s ON c.session_id = s.id
695
- WHERE s.project_path = ? AND c.created_at >= unixepoch() - 30 * 86400
696
- GROUP BY date(c.created_at, 'unixepoch')
697
- ORDER BY date ASC
698
- `),
699
- hourlyActivityAll: db.prepare(`
700
- SELECT
701
- CAST(strftime('%H', c.created_at, 'unixepoch', 'localtime') AS INTEGER) AS hour,
702
- COUNT(*) AS queries,
703
- SUM(c.cost_usd) AS cost
704
- FROM costs c
705
- GROUP BY strftime('%H', c.created_at, 'unixepoch', 'localtime')
706
- ORDER BY hour ASC
707
- `),
708
- hourlyActivityByProject: db.prepare(`
709
- SELECT
710
- CAST(strftime('%H', c.created_at, 'unixepoch', 'localtime') AS INTEGER) AS hour,
711
- COUNT(*) AS queries,
712
- SUM(c.cost_usd) AS cost
713
- FROM costs c
714
- JOIN sessions s ON c.session_id = s.id
715
- WHERE s.project_path = ?
716
- GROUP BY strftime('%H', c.created_at, 'unixepoch', 'localtime')
717
- ORDER BY hour ASC
718
- `),
719
- projectBreakdown: db.prepare(`
720
- SELECT
721
- s.project_name AS name,
722
- s.project_path AS path,
723
- COUNT(DISTINCT s.id) AS sessions,
724
- COUNT(c.id) AS queries,
725
- COALESCE(SUM(c.cost_usd), 0) AS totalCost,
726
- CASE WHEN COUNT(DISTINCT s.id) > 0
727
- THEN COALESCE(SUM(c.cost_usd), 0) / COUNT(DISTINCT s.id)
728
- ELSE 0 END AS avgCost,
729
- CASE WHEN COUNT(DISTINCT s.id) > 0
730
- THEN COALESCE(SUM(c.num_turns), 0) / COUNT(DISTINCT s.id)
731
- ELSE 0 END AS avgTurns
732
- FROM sessions s
733
- LEFT JOIN costs c ON c.session_id = s.id
734
- GROUP BY s.project_path
735
- ORDER BY totalCost DESC
736
- `),
737
- topSessionsAll: db.prepare(`
738
- SELECT
739
- s.title,
740
- s.project_name AS project,
741
- COALESCE(SUM(c.cost_usd), 0) AS cost,
742
- COALESCE(SUM(c.num_turns), 0) AS turns,
743
- COUNT(c.id) AS queries,
744
- COALESCE(SUM(c.duration_ms), 0) / 60000.0 AS duration_min
745
- FROM sessions s
746
- LEFT JOIN costs c ON c.session_id = s.id
747
- GROUP BY s.id
748
- HAVING cost > 0
749
- ORDER BY cost DESC
750
- LIMIT 10
751
- `),
752
- topSessionsByProject: db.prepare(`
753
- SELECT
754
- s.title,
755
- s.project_name AS project,
756
- COALESCE(SUM(c.cost_usd), 0) AS cost,
757
- COALESCE(SUM(c.num_turns), 0) AS turns,
758
- COUNT(c.id) AS queries,
759
- COALESCE(SUM(c.duration_ms), 0) / 60000.0 AS duration_min
760
- FROM sessions s
761
- LEFT JOIN costs c ON c.session_id = s.id
762
- WHERE s.project_path = ?
763
- GROUP BY s.id
764
- HAVING cost > 0
765
- ORDER BY cost DESC
766
- LIMIT 10
767
- `),
768
- toolUsageAll: db.prepare(`
769
- SELECT
770
- json_extract(content, '$.name') AS name,
771
- COUNT(*) AS count
772
- FROM messages
773
- WHERE role = 'tool' AND json_extract(content, '$.name') IS NOT NULL
774
- GROUP BY json_extract(content, '$.name')
775
- ORDER BY count DESC
776
- `),
777
- toolUsageByProject: db.prepare(`
778
- SELECT
779
- json_extract(m.content, '$.name') AS name,
780
- COUNT(*) AS count
781
- FROM messages m
782
- JOIN sessions s ON m.session_id = s.id
783
- WHERE m.role = 'tool' AND s.project_path = ? AND json_extract(m.content, '$.name') IS NOT NULL
784
- GROUP BY json_extract(m.content, '$.name')
785
- ORDER BY count DESC
786
- `),
787
- toolErrorsAll: db.prepare(`
788
- SELECT
789
- json_extract(t.content, '$.name') AS name,
790
- COUNT(CASE WHEN json_extract(tr.content, '$.isError') = 1 THEN 1 END) AS errors,
791
- COUNT(*) AS total,
792
- CAST(COUNT(CASE WHEN json_extract(tr.content, '$.isError') = 1 THEN 1 END) AS REAL) / NULLIF(COUNT(*), 0) * 100 AS errorRate
793
- FROM messages t
794
- JOIN messages tr ON tr.session_id = t.session_id
795
- AND tr.role = 'tool_result'
796
- AND json_extract(tr.content, '$.toolUseId') = json_extract(t.content, '$.id')
797
- WHERE t.role = 'tool'
798
- GROUP BY json_extract(t.content, '$.name')
799
- HAVING errors > 0
800
- ORDER BY errors DESC
801
- `),
802
- toolErrorsByProject: db.prepare(`
803
- SELECT
804
- json_extract(t.content, '$.name') AS name,
805
- COUNT(CASE WHEN json_extract(tr.content, '$.isError') = 1 THEN 1 END) AS errors,
806
- COUNT(*) AS total,
807
- CAST(COUNT(CASE WHEN json_extract(tr.content, '$.isError') = 1 THEN 1 END) AS REAL) / NULLIF(COUNT(*), 0) * 100 AS errorRate
808
- FROM messages t
809
- JOIN messages tr ON tr.session_id = t.session_id
810
- AND tr.role = 'tool_result'
811
- AND json_extract(tr.content, '$.toolUseId') = json_extract(t.content, '$.id')
812
- JOIN sessions s ON t.session_id = s.id
813
- WHERE t.role = 'tool' AND s.project_path = ?
814
- GROUP BY json_extract(t.content, '$.name')
815
- HAVING errors > 0
816
- ORDER BY errors DESC
817
- `),
818
- sessionDepthAll: db.prepare(`
819
- SELECT
820
- CASE
821
- WHEN cnt = 1 THEN '1 query'
822
- WHEN cnt BETWEEN 2 AND 3 THEN '2-3'
823
- WHEN cnt BETWEEN 4 AND 6 THEN '4-6'
824
- WHEN cnt BETWEEN 7 AND 10 THEN '7-10'
825
- ELSE '10+'
826
- END AS bucket,
827
- COUNT(*) AS count,
828
- AVG(total_cost) AS avgCost
829
- FROM (
830
- SELECT s.id, COUNT(c.id) AS cnt, COALESCE(SUM(c.cost_usd), 0) AS total_cost
831
- FROM sessions s
832
- LEFT JOIN costs c ON c.session_id = s.id
833
- GROUP BY s.id
834
- HAVING cnt > 0
835
- )
836
- GROUP BY bucket
837
- ORDER BY MIN(cnt)
838
- `),
839
- sessionDepthByProject: db.prepare(`
840
- SELECT
841
- CASE
842
- WHEN cnt = 1 THEN '1 query'
843
- WHEN cnt BETWEEN 2 AND 3 THEN '2-3'
844
- WHEN cnt BETWEEN 4 AND 6 THEN '4-6'
845
- WHEN cnt BETWEEN 7 AND 10 THEN '7-10'
846
- ELSE '10+'
847
- END AS bucket,
848
- COUNT(*) AS count,
849
- AVG(total_cost) AS avgCost
850
- FROM (
851
- SELECT s.id, COUNT(c.id) AS cnt, COALESCE(SUM(c.cost_usd), 0) AS total_cost
852
- FROM sessions s
853
- LEFT JOIN costs c ON c.session_id = s.id
854
- WHERE s.project_path = ?
855
- GROUP BY s.id
856
- HAVING cnt > 0
857
- )
858
- GROUP BY bucket
859
- ORDER BY MIN(cnt)
860
- `),
861
- msgLengthAll: db.prepare(`
862
- SELECT
863
- CASE
864
- WHEN len < 100 THEN '<100'
865
- WHEN len BETWEEN 100 AND 499 THEN '100-499'
866
- WHEN len BETWEEN 500 AND 999 THEN '500-999'
867
- WHEN len BETWEEN 1000 AND 4999 THEN '1k-5k'
868
- ELSE '5k+'
869
- END AS bucket,
870
- COUNT(*) AS count,
871
- CAST(AVG(len) AS INTEGER) AS avgChars
872
- FROM (
873
- SELECT LENGTH(json_extract(content, '$.text')) AS len
874
- FROM messages
875
- WHERE role = 'user' AND json_extract(content, '$.text') IS NOT NULL
876
- )
877
- WHERE len > 0
878
- GROUP BY bucket
879
- ORDER BY MIN(len)
880
- `),
881
- msgLengthByProject: db.prepare(`
882
- SELECT
883
- CASE
884
- WHEN len < 100 THEN '<100'
885
- WHEN len BETWEEN 100 AND 499 THEN '100-499'
886
- WHEN len BETWEEN 500 AND 999 THEN '500-999'
887
- WHEN len BETWEEN 1000 AND 4999 THEN '1k-5k'
888
- ELSE '5k+'
889
- END AS bucket,
890
- COUNT(*) AS count,
891
- CAST(AVG(len) AS INTEGER) AS avgChars
892
- FROM (
893
- SELECT LENGTH(json_extract(m.content, '$.text')) AS len
894
- FROM messages m
895
- JOIN sessions s ON m.session_id = s.id
896
- WHERE m.role = 'user' AND s.project_path = ? AND json_extract(m.content, '$.text') IS NOT NULL
897
- )
898
- WHERE len > 0
899
- GROUP BY bucket
900
- ORDER BY MIN(len)
901
- `),
902
- topBashCommandsAll: db.prepare(`
903
- SELECT
904
- SUBSTR(json_extract(content, '$.input.command'), 1, 80) AS command,
905
- COUNT(*) AS count
906
- FROM messages
907
- WHERE role = 'tool' AND json_extract(content, '$.name') = 'Bash'
908
- AND json_extract(content, '$.input.command') IS NOT NULL
909
- GROUP BY SUBSTR(json_extract(content, '$.input.command'), 1, 80)
910
- ORDER BY count DESC
911
- LIMIT 10
912
- `),
913
- topBashCommandsByProject: db.prepare(`
914
- SELECT
915
- SUBSTR(json_extract(m.content, '$.input.command'), 1, 80) AS command,
916
- COUNT(*) AS count
917
- FROM messages m
918
- JOIN sessions s ON m.session_id = s.id
919
- WHERE m.role = 'tool' AND s.project_path = ? AND json_extract(m.content, '$.name') = 'Bash'
920
- AND json_extract(m.content, '$.input.command') IS NOT NULL
921
- GROUP BY SUBSTR(json_extract(m.content, '$.input.command'), 1, 80)
922
- ORDER BY count DESC
923
- LIMIT 10
924
- `),
925
- topFilesAll: db.prepare(`
926
- SELECT
927
- json_extract(content, '$.input.file_path') AS path,
928
- COUNT(*) AS count,
929
- json_extract(content, '$.name') AS tool
930
- FROM messages
931
- WHERE role = 'tool'
932
- AND json_extract(content, '$.name') IN ('Read', 'Write', 'Edit')
933
- AND json_extract(content, '$.input.file_path') IS NOT NULL
934
- GROUP BY json_extract(content, '$.input.file_path'), json_extract(content, '$.name')
935
- ORDER BY count DESC
936
- LIMIT 15
937
- `),
938
- topFilesByProject: db.prepare(`
939
- SELECT
940
- json_extract(m.content, '$.input.file_path') AS path,
941
- COUNT(*) AS count,
942
- json_extract(m.content, '$.name') AS tool
943
- FROM messages m
944
- JOIN sessions s ON m.session_id = s.id
945
- WHERE m.role = 'tool' AND s.project_path = ?
946
- AND json_extract(m.content, '$.name') IN ('Read', 'Write', 'Edit')
947
- AND json_extract(m.content, '$.input.file_path') IS NOT NULL
948
- GROUP BY json_extract(m.content, '$.input.file_path'), json_extract(m.content, '$.name')
949
- ORDER BY count DESC
950
- LIMIT 15
951
- `),
952
-
953
- // ── Error pattern analytics ──────────────────────────────────
954
- errorCategoriesAll: db.prepare(`
955
- SELECT ${ERROR_CATEGORY_CASE} AS category, COUNT(*) AS count
956
- FROM messages tr
957
- WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
958
- GROUP BY category
959
- ORDER BY count DESC
960
- `),
961
- errorCategoriesByProject: db.prepare(`
962
- SELECT ${ERROR_CATEGORY_CASE} AS category, COUNT(*) AS count
963
- FROM messages tr
964
- JOIN sessions s ON tr.session_id = s.id
965
- WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
966
- AND s.project_path = ?
967
- GROUP BY category
968
- ORDER BY count DESC
969
- `),
970
- errorTimelineAll: db.prepare(`
971
- SELECT date(tr.created_at, 'unixepoch') AS date, COUNT(*) AS errors
972
- FROM messages tr
973
- WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
974
- AND tr.created_at >= unixepoch() - 30 * 86400
975
- GROUP BY date(tr.created_at, 'unixepoch')
976
- ORDER BY date ASC
977
- `),
978
- errorTimelineByProject: db.prepare(`
979
- SELECT date(tr.created_at, 'unixepoch') AS date, COUNT(*) AS errors
980
- FROM messages tr
981
- JOIN sessions s ON tr.session_id = s.id
982
- WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
983
- AND s.project_path = ? AND tr.created_at >= unixepoch() - 30 * 86400
984
- GROUP BY date(tr.created_at, 'unixepoch')
985
- ORDER BY date ASC
986
- `),
987
- errorsByToolAll: db.prepare(`
988
- SELECT
989
- COALESCE(json_extract(t.content, '$.name'), 'Unknown') AS tool,
990
- ${ERROR_CATEGORY_CASE} AS category,
991
- COUNT(*) AS errors
992
- FROM messages tr
993
- LEFT JOIN messages t ON t.session_id = tr.session_id
994
- AND t.role = 'tool'
995
- AND json_extract(t.content, '$.id') = json_extract(tr.content, '$.toolUseId')
996
- WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
997
- GROUP BY tool, category
998
- ORDER BY errors DESC
999
- `),
1000
- errorsByToolByProject: db.prepare(`
1001
- SELECT
1002
- COALESCE(json_extract(t.content, '$.name'), 'Unknown') AS tool,
1003
- ${ERROR_CATEGORY_CASE} AS category,
1004
- COUNT(*) AS errors
1005
- FROM messages tr
1006
- JOIN sessions s ON tr.session_id = s.id
1007
- LEFT JOIN messages t ON t.session_id = tr.session_id
1008
- AND t.role = 'tool'
1009
- AND json_extract(t.content, '$.id') = json_extract(tr.content, '$.toolUseId')
1010
- WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
1011
- AND s.project_path = ?
1012
- GROUP BY tool, category
1013
- ORDER BY errors DESC
1014
- `),
1015
- recentErrorsAll: db.prepare(`
1016
- SELECT
1017
- COALESCE(json_extract(t.content, '$.name'), 'Unknown') AS tool,
1018
- SUBSTR(json_extract(tr.content, '$.content'), 1, 200) AS preview,
1019
- json_extract(tr.content, '$.content') AS full_content,
1020
- s.title AS session_title,
1021
- tr.created_at AS timestamp
1022
- FROM messages tr
1023
- JOIN sessions s ON tr.session_id = s.id
1024
- LEFT JOIN messages t ON t.session_id = tr.session_id
1025
- AND t.role = 'tool'
1026
- AND json_extract(t.content, '$.id') = json_extract(tr.content, '$.toolUseId')
1027
- WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
1028
- ORDER BY tr.created_at DESC
1029
- LIMIT 20
1030
- `),
1031
- recentErrorsByProject: db.prepare(`
1032
- SELECT
1033
- COALESCE(json_extract(t.content, '$.name'), 'Unknown') AS tool,
1034
- SUBSTR(json_extract(tr.content, '$.content'), 1, 200) AS preview,
1035
- json_extract(tr.content, '$.content') AS full_content,
1036
- s.title AS session_title,
1037
- tr.created_at AS timestamp
1038
- FROM messages tr
1039
- JOIN sessions s ON tr.session_id = s.id
1040
- LEFT JOIN messages t ON t.session_id = tr.session_id
1041
- AND t.role = 'tool'
1042
- AND json_extract(t.content, '$.id') = json_extract(tr.content, '$.toolUseId')
1043
- WHERE tr.role = 'tool_result' AND json_extract(tr.content, '$.isError') = 1
1044
- AND s.project_path = ?
1045
- ORDER BY tr.created_at DESC
1046
- LIMIT 20
1047
- `),
1048
-
1049
- // ── Model usage & cache efficiency ─────────────────────────
1050
- modelUsageAll: db.prepare(`
1051
- SELECT
1052
- COALESCE(model, 'unknown') AS model,
1053
- COUNT(*) AS count,
1054
- COALESCE(SUM(cost_usd), 0) AS cost,
1055
- COALESCE(SUM(input_tokens + output_tokens), 0) AS tokens
1056
- FROM costs
1057
- GROUP BY COALESCE(model, 'unknown')
1058
- ORDER BY cost DESC
1059
- `),
1060
- modelUsageByProject: db.prepare(`
1061
- SELECT
1062
- COALESCE(c.model, 'unknown') AS model,
1063
- COUNT(*) AS count,
1064
- COALESCE(SUM(c.cost_usd), 0) AS cost,
1065
- COALESCE(SUM(c.input_tokens + c.output_tokens), 0) AS tokens
1066
- FROM costs c
1067
- JOIN sessions s ON c.session_id = s.id
1068
- WHERE s.project_path = ?
1069
- GROUP BY COALESCE(c.model, 'unknown')
1070
- ORDER BY cost DESC
1071
- `),
1072
- cacheEfficiencyAll: db.prepare(`
1073
- SELECT
1074
- date(c.created_at, 'unixepoch') AS date,
1075
- COALESCE(SUM(c.cache_read_tokens), 0) AS cache_read,
1076
- COALESCE(SUM(c.cache_creation_tokens), 0) AS cache_creation,
1077
- COALESCE(SUM(c.input_tokens), 0) AS total_input
1078
- FROM costs c
1079
- WHERE c.created_at >= unixepoch() - 30 * 86400
1080
- GROUP BY date(c.created_at, 'unixepoch')
1081
- ORDER BY date ASC
1082
- `),
1083
- cacheEfficiencyByProject: db.prepare(`
1084
- SELECT
1085
- date(c.created_at, 'unixepoch') AS date,
1086
- COALESCE(SUM(c.cache_read_tokens), 0) AS cache_read,
1087
- COALESCE(SUM(c.cache_creation_tokens), 0) AS cache_creation,
1088
- COALESCE(SUM(c.input_tokens), 0) AS total_input
1089
- FROM costs c
1090
- JOIN sessions s ON c.session_id = s.id
1091
- WHERE s.project_path = ? AND c.created_at >= unixepoch() - 30 * 86400
1092
- GROUP BY date(c.created_at, 'unixepoch')
1093
- ORDER BY date ASC
1094
- `),
1095
- };
1096
-
1097
- export function getAnalyticsOverview(projectPath) {
1098
- const overview = projectPath
1099
- ? analyticsStmts.overviewByProject.get(projectPath)
1100
- : analyticsStmts.overviewAll.get();
1101
- const errors = projectPath
1102
- ? analyticsStmts.errorRateByProject.get(projectPath)
1103
- : analyticsStmts.errorRateAll.get();
1104
- return {
1105
- ...overview,
1106
- errorRate: errors.total > 0 ? (errors.errors / errors.total * 100) : 0,
1107
- };
1108
- }
1109
-
1110
- export function getDailyBreakdown(projectPath) {
1111
- return projectPath
1112
- ? analyticsStmts.dailyBreakdownByProject.all(projectPath)
1113
- : analyticsStmts.dailyBreakdownAll.all();
1114
- }
1115
-
1116
- export function getHourlyActivity(projectPath) {
1117
- return projectPath
1118
- ? analyticsStmts.hourlyActivityByProject.all(projectPath)
1119
- : analyticsStmts.hourlyActivityAll.all();
1120
- }
1121
-
1122
- export function getProjectBreakdown() {
1123
- return analyticsStmts.projectBreakdown.all();
1124
- }
1125
-
1126
- export function getTopSessionsByCost(projectPath) {
1127
- return projectPath
1128
- ? analyticsStmts.topSessionsByProject.all(projectPath)
1129
- : analyticsStmts.topSessionsAll.all();
1130
- }
1131
-
1132
- export function getToolUsage(projectPath) {
1133
- return projectPath
1134
- ? analyticsStmts.toolUsageByProject.all(projectPath)
1135
- : analyticsStmts.toolUsageAll.all();
1136
- }
1137
-
1138
- export function getToolErrors(projectPath) {
1139
- return projectPath
1140
- ? analyticsStmts.toolErrorsByProject.all(projectPath)
1141
- : analyticsStmts.toolErrorsAll.all();
1142
- }
1143
-
1144
- export function getSessionDepth(projectPath) {
1145
- return projectPath
1146
- ? analyticsStmts.sessionDepthByProject.all(projectPath)
1147
- : analyticsStmts.sessionDepthAll.all();
1148
- }
1149
-
1150
- export function getMsgLengthDistribution(projectPath) {
1151
- return projectPath
1152
- ? analyticsStmts.msgLengthByProject.all(projectPath)
1153
- : analyticsStmts.msgLengthAll.all();
1154
- }
1155
-
1156
- export function getTopBashCommands(projectPath) {
1157
- return projectPath
1158
- ? analyticsStmts.topBashCommandsByProject.all(projectPath)
1159
- : analyticsStmts.topBashCommandsAll.all();
1160
- }
1161
-
1162
- export function getTopFiles(projectPath) {
1163
- return projectPath
1164
- ? analyticsStmts.topFilesByProject.all(projectPath)
1165
- : analyticsStmts.topFilesAll.all();
1166
- }
1167
-
1168
- export function getErrorCategories(projectPath) {
1169
- return projectPath
1170
- ? analyticsStmts.errorCategoriesByProject.all(projectPath)
1171
- : analyticsStmts.errorCategoriesAll.all();
1172
- }
1173
-
1174
- export function getErrorTimeline(projectPath) {
1175
- return projectPath
1176
- ? analyticsStmts.errorTimelineByProject.all(projectPath)
1177
- : analyticsStmts.errorTimelineAll.all();
1178
- }
1179
-
1180
- export function getErrorsByTool(projectPath) {
1181
- return projectPath
1182
- ? analyticsStmts.errorsByToolByProject.all(projectPath)
1183
- : analyticsStmts.errorsByToolAll.all();
1184
- }
1185
-
1186
- export function getRecentErrors(projectPath) {
1187
- return projectPath
1188
- ? analyticsStmts.recentErrorsByProject.all(projectPath)
1189
- : analyticsStmts.recentErrorsAll.all();
1190
- }
1191
-
1192
- export function getModelUsage(projectPath) {
1193
- return projectPath
1194
- ? analyticsStmts.modelUsageByProject.all(projectPath)
1195
- : analyticsStmts.modelUsageAll.all();
1196
- }
1197
-
1198
- export function getYearlyActivity() {
1199
- return stmts.yearlyActivity.all();
1200
- }
1201
-
1202
- export function getCacheEfficiency(projectPath) {
1203
- return projectPath
1204
- ? analyticsStmts.cacheEfficiencyByProject.all(projectPath)
1205
- : analyticsStmts.cacheEfficiencyAll.all();
1206
- }
1207
-
1208
- // ── Todo CRUD ────────────────────────────────────────────────
1209
- export function listTodos(archived = false) {
1210
- return archived ? stmts.listArchivedTodos.all() : stmts.listTodos.all();
1211
- }
1212
- export function createTodo(text) { return stmts.createTodo.run(text); }
1213
- export function updateTodo(id, text, done, priority) { return stmts.updateTodo.run(text, done, priority, id); }
1214
- export function archiveTodo(id, archived) { return stmts.archiveTodo.run(archived ? 1 : 0, id); }
1215
- export function deleteTodo(id) { return stmts.deleteTodo.run(id); }
1216
-
1217
- export function getTodoCounts() { return stmts.todoCounts.get(); }
1218
-
1219
- // ── Brag CRUD ─────────────────────────────────────────────────
1220
- export function createBrag(todoId, text, summary) { return stmts.createBrag.run(todoId, text, summary); }
1221
- export function listBrags() { return stmts.listBrags.all(); }
1222
- export function deleteBrag(id) { return stmts.deleteBrag.run(id); }
1223
-
1224
- // ── Push subscription queries ────────────────────────────────
1225
- const pushStmts = {
1226
- upsert: db.prepare(
1227
- `INSERT INTO push_subscriptions (endpoint, keys_p256dh, keys_auth)
1228
- VALUES (?, ?, ?)
1229
- ON CONFLICT(endpoint) DO UPDATE SET keys_p256dh = excluded.keys_p256dh, keys_auth = excluded.keys_auth`
1230
- ),
1231
- delete: db.prepare(`DELETE FROM push_subscriptions WHERE endpoint = ?`),
1232
- getAll: db.prepare(`SELECT * FROM push_subscriptions`),
1233
- };
1234
-
1235
- export function upsertPushSubscription(endpoint, p256dh, auth) {
1236
- pushStmts.upsert.run(endpoint, p256dh, auth);
1237
- }
1238
-
1239
- export function deletePushSubscription(endpoint) {
1240
- pushStmts.delete.run(endpoint);
1241
- }
1242
-
1243
- export function getAllPushSubscriptions() {
1244
- return pushStmts.getAll.all();
1245
- }
1246
-
1247
- // ── Agent context (shared memory) ─────────────────────────
1248
- const ctxStmts = {
1249
- set: db.prepare(
1250
- `INSERT INTO agent_context (run_id, agent_id, key, value)
1251
- VALUES (?, ?, ?, ?)
1252
- ON CONFLICT(run_id, agent_id, key) DO UPDATE SET value = excluded.value`
1253
- ),
1254
- get: db.prepare(
1255
- `SELECT value FROM agent_context WHERE run_id = ? AND agent_id = ? AND key = ?`
1256
- ),
1257
- getAllForRun: db.prepare(
1258
- `SELECT agent_id, key, value, created_at FROM agent_context WHERE run_id = ? ORDER BY created_at ASC`
1259
- ),
1260
- getByKey: db.prepare(
1261
- `SELECT agent_id, value FROM agent_context WHERE run_id = ? AND key = ?`
1262
- ),
1263
- deleteRun: db.prepare(
1264
- `DELETE FROM agent_context WHERE run_id = ?`
1265
- ),
1266
- };
1267
-
1268
- export function setAgentContext(runId, agentId, key, value) {
1269
- ctxStmts.set.run(runId, agentId, key, typeof value === "string" ? value : JSON.stringify(value));
1270
- }
1271
-
1272
- export function getAgentContext(runId, agentId, key) {
1273
- const row = ctxStmts.get.get(runId, agentId, key);
1274
- return row ? row.value : null;
1275
- }
1276
-
1277
- export function getAllAgentContext(runId) {
1278
- return ctxStmts.getAllForRun.all(runId);
1279
- }
1280
-
1281
- export function getAgentContextByKey(runId, key) {
1282
- return ctxStmts.getByKey.all(runId, key);
1283
- }
1284
-
1285
- export function deleteAgentContext(runId) {
1286
- ctxStmts.deleteRun.run(runId);
1287
- }
1288
-
1289
- // ── Agent runs (monitoring) ────────────────────────────
1290
- const runStmts = {
1291
- insert: db.prepare(
1292
- `INSERT INTO agent_runs (run_id, agent_id, agent_title, run_type, parent_id, status)
1293
- VALUES (?, ?, ?, ?, ?, 'running')`
1294
- ),
1295
- complete: db.prepare(
1296
- `UPDATE agent_runs SET status = ?, turns = ?, cost_usd = ?, duration_ms = ?,
1297
- input_tokens = ?, output_tokens = ?, error = ?, completed_at = unixepoch()
1298
- WHERE run_id = ? AND agent_id = ?`
1299
- ),
1300
- listRecent: db.prepare(
1301
- `SELECT * FROM agent_runs ORDER BY started_at DESC LIMIT ?`
1302
- ),
1303
- agentSummary: db.prepare(
1304
- `SELECT
1305
- agent_id, agent_title,
1306
- COUNT(*) AS runs,
1307
- SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS successes,
1308
- SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS errors,
1309
- COALESCE(SUM(cost_usd), 0) AS total_cost,
1310
- COALESCE(AVG(CASE WHEN status = 'completed' THEN cost_usd END), 0) AS avg_cost,
1311
- COALESCE(AVG(CASE WHEN status = 'completed' THEN duration_ms END), 0) AS avg_duration,
1312
- COALESCE(AVG(CASE WHEN status = 'completed' THEN turns END), 0) AS avg_turns,
1313
- COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
1314
- COALESCE(SUM(output_tokens), 0) AS total_output_tokens
1315
- FROM agent_runs
1316
- GROUP BY agent_id
1317
- ORDER BY total_cost DESC`
1318
- ),
1319
- overview: db.prepare(
1320
- `SELECT
1321
- COUNT(*) AS total_runs,
1322
- SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
1323
- SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS errored,
1324
- COALESCE(SUM(cost_usd), 0) AS total_cost,
1325
- COALESCE(AVG(CASE WHEN status = 'completed' THEN duration_ms END), 0) AS avg_duration,
1326
- COALESCE(AVG(CASE WHEN status = 'completed' THEN turns END), 0) AS avg_turns,
1327
- COALESCE(SUM(input_tokens), 0) AS total_input_tokens,
1328
- COALESCE(SUM(output_tokens), 0) AS total_output_tokens
1329
- FROM agent_runs`
1330
- ),
1331
- byType: db.prepare(
1332
- `SELECT
1333
- run_type,
1334
- COUNT(*) AS runs,
1335
- COALESCE(SUM(cost_usd), 0) AS cost,
1336
- COALESCE(AVG(duration_ms), 0) AS avg_duration
1337
- FROM agent_runs
1338
- GROUP BY run_type
1339
- ORDER BY runs DESC`
1340
- ),
1341
- dailyRuns: db.prepare(
1342
- `SELECT
1343
- date(started_at, 'unixepoch') AS date,
1344
- COUNT(*) AS runs,
1345
- SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
1346
- SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS errored,
1347
- COALESCE(SUM(cost_usd), 0) AS cost
1348
- FROM agent_runs
1349
- WHERE started_at >= unixepoch() - 30 * 86400
1350
- GROUP BY date(started_at, 'unixepoch')
1351
- ORDER BY date ASC`
1352
- ),
1353
- };
1354
-
1355
- export function recordAgentRunStart(runId, agentId, agentTitle, runType = 'single', parentId = null) {
1356
- runStmts.insert.run(runId, agentId, agentTitle, runType, parentId);
1357
- }
1358
-
1359
- export function recordAgentRunComplete(runId, agentId, status, turns, costUsd, durationMs, inputTokens, outputTokens, error = null) {
1360
- runStmts.complete.run(status, turns, costUsd, durationMs, inputTokens, outputTokens, error, runId, agentId);
1361
- }
1362
-
1363
- export function getAgentRunsRecent(limit = 50) {
1364
- return runStmts.listRecent.all(limit);
1365
- }
1366
-
1367
- export function getAgentRunsSummary() {
1368
- return runStmts.agentSummary.all();
1369
- }
1370
-
1371
- export function getAgentRunsOverview() {
1372
- return runStmts.overview.get();
1373
- }
1374
-
1375
- export function getAgentRunsByType() {
1376
- return runStmts.byType.all();
1377
- }
1378
-
1379
- export function getAgentRunsDaily() {
1380
- return runStmts.dailyRuns.all();
1381
- }
1382
-
1383
- // ── Notifications ────────────────────────────────────────
1384
- const notifStmts = {
1385
- insert: db.prepare(
1386
- `INSERT INTO notifications (type, title, body, metadata, source_session_id, source_agent_id)
1387
- VALUES (?, ?, ?, ?, ?, ?)`
1388
- ),
1389
- history: db.prepare(
1390
- `SELECT * FROM notifications ORDER BY created_at DESC LIMIT ? OFFSET ?`
1391
- ),
1392
- historyUnread: db.prepare(
1393
- `SELECT * FROM notifications WHERE read_at IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ?`
1394
- ),
1395
- historyByType: db.prepare(
1396
- `SELECT * FROM notifications WHERE type = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
1397
- ),
1398
- historyByTypeUnread: db.prepare(
1399
- `SELECT * FROM notifications WHERE type = ? AND read_at IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ?`
1400
- ),
1401
- unreadCount: db.prepare(
1402
- `SELECT COUNT(*) as count FROM notifications WHERE read_at IS NULL`
1403
- ),
1404
- markRead: db.prepare(
1405
- `UPDATE notifications SET read_at = unixepoch() WHERE id = ? AND read_at IS NULL`
1406
- ),
1407
- markAllRead: db.prepare(
1408
- `UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL`
1409
- ),
1410
- markReadBefore: db.prepare(
1411
- `UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL AND created_at < ?`
1412
- ),
1413
- purgeOld: db.prepare(
1414
- `DELETE FROM notifications WHERE created_at < unixepoch() - (? * 86400)`
1415
- ),
1416
- markStaleRead: db.prepare(
1417
- `UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL AND created_at < unixepoch() - (7 * 86400)`
1418
- ),
1419
- };
1420
-
1421
- export function createNotification(type, title, body = null, metadata = null, sourceSessionId = null, sourceAgentId = null) {
1422
- const result = notifStmts.insert.run(type, title, body, metadata, sourceSessionId, sourceAgentId);
1423
- return {
1424
- id: result.lastInsertRowid,
1425
- type, title, body, metadata,
1426
- source_session_id: sourceSessionId,
1427
- source_agent_id: sourceAgentId,
1428
- read_at: null,
1429
- created_at: Math.floor(Date.now() / 1000),
1430
- };
1431
- }
1432
-
1433
- export function getNotificationHistory(limit = 20, offset = 0, unreadOnly = false, type = null) {
1434
- if (type && unreadOnly) return notifStmts.historyByTypeUnread.all(type, limit, offset);
1435
- if (type) return notifStmts.historyByType.all(type, limit, offset);
1436
- if (unreadOnly) return notifStmts.historyUnread.all(limit, offset);
1437
- return notifStmts.history.all(limit, offset);
1438
- }
1439
-
1440
- export function getUnreadNotificationCount() {
1441
- return notifStmts.unreadCount.get().count;
1442
- }
1443
-
1444
- export function markNotificationsRead(ids) {
1445
- const tx = db.transaction((idList) => {
1446
- for (const id of idList) notifStmts.markRead.run(id);
1447
- });
1448
- tx(ids);
1449
- }
1450
-
1451
- export function markAllNotificationsRead() {
1452
- notifStmts.markAllRead.run();
1453
- }
1454
-
1455
- export function markNotificationsReadBefore(timestamp) {
1456
- notifStmts.markReadBefore.run(timestamp);
1457
- }
1458
-
1459
- export function purgeOldNotifications(days = 90) {
1460
- notifStmts.markStaleRead.run();
1461
- notifStmts.purgeOld.run(days);
1462
- }
1463
-
1464
- // ── Worktrees ─────────────────────────────────────────────
1465
- const wtStmts = {
1466
- create: db.prepare(
1467
- `INSERT INTO worktrees (id, session_id, project_path, worktree_path, branch_name, base_branch, status, user_prompt)
1468
- VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`
1469
- ),
1470
- get: db.prepare(`SELECT * FROM worktrees WHERE id = ?`),
1471
- listByProject: db.prepare(
1472
- `SELECT * FROM worktrees WHERE project_path = ? ORDER BY created_at DESC`
1473
- ),
1474
- listActive: db.prepare(
1475
- `SELECT * FROM worktrees WHERE status IN ('active', 'completed') ORDER BY created_at DESC`
1476
- ),
1477
- updateStatus: db.prepare(
1478
- `UPDATE worktrees SET status = ?, completed_at = unixepoch() WHERE id = ?`
1479
- ),
1480
- updateSession: db.prepare(
1481
- `UPDATE worktrees SET session_id = ? WHERE id = ?`
1482
- ),
1483
- delete: db.prepare(`DELETE FROM worktrees WHERE id = ?`),
1484
- };
1485
-
1486
- export function createWorktreeRecord(id, sessionId, projectPath, worktreePath, branchName, baseBranch, userPrompt) {
1487
- wtStmts.create.run(id, sessionId, projectPath, worktreePath, branchName, baseBranch, userPrompt);
1488
- }
1489
-
1490
- export function getWorktreeRecord(id) {
1491
- return wtStmts.get.get(id);
1492
- }
1493
-
1494
- export function listWorktreesByProject(projectPath) {
1495
- return wtStmts.listByProject.all(projectPath);
1496
- }
1497
-
1498
- export function listActiveWorktrees() {
1499
- return wtStmts.listActive.all();
1500
- }
1501
-
1502
- export function updateWorktreeStatus(id, status) {
1503
- wtStmts.updateStatus.run(status, id);
1504
- }
1505
-
1506
- export function updateWorktreeSession(id, sessionId) {
1507
- wtStmts.updateSession.run(sessionId, id);
1508
- }
1509
-
1510
- export function deleteWorktreeRecord(id) {
1511
- wtStmts.delete.run(id);
1512
- }
1513
-
1514
- // ── Memories (persistent cross-session context) ──────────
1515
- function hashContent(projectPath, content) {
1516
- return createHash("sha256").update(`${projectPath}:${content}`).digest("hex");
1517
- }
1518
-
1519
- const memStmts = {
1520
- insert: db.prepare(
1521
- `INSERT OR IGNORE INTO memories (project_path, category, content, content_hash, source_session_id, source_agent_id)
1522
- VALUES (?, ?, ?, ?, ?, ?)`
1523
- ),
1524
- findByHash: db.prepare(
1525
- `SELECT id FROM memories WHERE project_path = ? AND content_hash = ?`
1526
- ),
1527
- list: db.prepare(
1528
- `SELECT * FROM memories WHERE project_path = ?
1529
- ORDER BY relevance_score DESC, accessed_at DESC`
1530
- ),
1531
- listByCategory: db.prepare(
1532
- `SELECT * FROM memories WHERE project_path = ? AND category = ?
1533
- ORDER BY relevance_score DESC, accessed_at DESC`
1534
- ),
1535
- searchFts: db.prepare(
1536
- `SELECT m.* FROM memories m
1537
- JOIN memories_fts fts ON fts.rowid = m.id
1538
- WHERE m.project_path = ? AND memories_fts MATCH ?
1539
- ORDER BY rank, m.relevance_score DESC LIMIT ?`
1540
- ),
1541
- searchLike: db.prepare(
1542
- `SELECT * FROM memories WHERE project_path = ? AND content LIKE ?
1543
- ORDER BY relevance_score DESC LIMIT ?`
1544
- ),
1545
- topRelevant: db.prepare(
1546
- `SELECT * FROM memories WHERE project_path = ?
1547
- ORDER BY relevance_score DESC, accessed_at DESC LIMIT ?`
1548
- ),
1549
- update: db.prepare(
1550
- `UPDATE memories SET content = ?, category = ? WHERE id = ?`
1551
- ),
1552
- touch: db.prepare(
1553
- `UPDATE memories SET accessed_at = unixepoch(),
1554
- relevance_score = MIN(relevance_score + 0.1, 2.0) WHERE id = ?`
1555
- ),
1556
- decay: db.prepare(
1557
- `UPDATE memories SET relevance_score = MAX(relevance_score * 0.95, 0.1)
1558
- WHERE project_path = ? AND accessed_at < unixepoch() - ?`
1559
- ),
1560
- delete: db.prepare(`DELETE FROM memories WHERE id = ?`),
1561
- deleteExpired: db.prepare(
1562
- `DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < unixepoch()`
1563
- ),
1564
- count: db.prepare(
1565
- `SELECT category, COUNT(*) as count FROM memories
1566
- WHERE project_path = ? GROUP BY category`
1567
- ),
1568
- stats: db.prepare(
1569
- `SELECT COUNT(*) as total,
1570
- SUM(CASE WHEN accessed_at > unixepoch() - 86400 THEN 1 ELSE 0 END) as accessed_today,
1571
- AVG(relevance_score) as avg_relevance
1572
- FROM memories WHERE project_path = ?`
1573
- ),
1574
- };
1575
-
1576
- export function createMemory(projectPath, category, content, sourceSessionId = null, sourceAgentId = null) {
1577
- const hash = hashContent(projectPath, content);
1578
- // Dedup: if identical content already exists, just touch it
1579
- const existing = memStmts.findByHash.get(projectPath, hash);
1580
- if (existing) {
1581
- memStmts.touch.run(existing.id);
1582
- return { lastInsertRowid: existing.id, changes: 0, isDuplicate: true };
1583
- }
1584
- return memStmts.insert.run(projectPath, category, content, hash, sourceSessionId, sourceAgentId);
1585
- }
1586
-
1587
- export function listMemories(projectPath, category = null) {
1588
- if (category) return memStmts.listByCategory.all(projectPath, category);
1589
- return memStmts.list.all(projectPath);
1590
- }
1591
-
1592
- export function searchMemories(projectPath, queryText, limit = 20) {
1593
- // Try FTS5 first, fall back to LIKE for non-FTS-compatible queries
1594
- try {
1595
- const ftsQuery = queryText.split(/\s+/).filter(Boolean).map(w => `"${w}"`).join(" OR ");
1596
- if (ftsQuery) {
1597
- return memStmts.searchFts.all(projectPath, ftsQuery, limit);
1598
- }
1599
- } catch {
1600
- // FTS parse error — fall back
1601
- }
1602
- return memStmts.searchLike.all(projectPath, `%${queryText}%`, limit);
1603
- }
1604
-
1605
- export function getTopMemories(projectPath, limit = 10) {
1606
- return memStmts.topRelevant.all(projectPath, limit);
1607
- }
1608
-
1609
- export function updateMemory(id, content, category) {
1610
- return memStmts.update.run(content, category, id);
1611
- }
1612
-
1613
- export function touchMemory(id) {
1614
- return memStmts.touch.run(id);
1615
- }
1616
-
1617
- export function decayMemories(projectPath, olderThanSecs = 604800) {
1618
- return memStmts.decay.run(projectPath, olderThanSecs);
1619
- }
1620
-
1621
- export function deleteMemory(id) {
1622
- return memStmts.delete.run(id);
1623
- }
1624
-
1625
- export function deleteExpiredMemories() {
1626
- return memStmts.deleteExpired.run();
1627
- }
1628
-
1629
- export function getMemoryCounts(projectPath) {
1630
- return memStmts.count.all(projectPath);
1631
- }
1632
-
1633
- export function getMemoryStats(projectPath) {
1634
- return memStmts.stats.get(projectPath);
1635
- }
1636
-
1637
- // Run decay + cleanup for a project (call on session start)
1638
- export function maintainMemories(projectPath) {
1639
- decayMemories(projectPath, 604800); // 7 days
1640
- deleteExpiredMemories();
1641
- }
1642
-
1643
- export function getDb() {
1644
- return db;
1645
- }
1
+ // Database adapter proxy — re-exports the active backend.
2
+ // Phase 1: SQLite only. Phase 2: read config to choose adapter.
3
+ export * from "./db/sqlite.js";