@epiphytic/claudecodeui 1.2.2 → 1.3.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.
@@ -0,0 +1,861 @@
1
+ /**
2
+ * SQLite Database Module
3
+ *
4
+ * Single source of truth for all session/project data.
5
+ * Replaces multiple in-memory caches with efficient SQLite storage.
6
+ */
7
+
8
+ import Database from "better-sqlite3";
9
+ import path from "path";
10
+ import os from "os";
11
+ import fs from "fs";
12
+ import { createLogger } from "./logger.js";
13
+
14
+ const log = createLogger("database");
15
+
16
+ // Database location
17
+ const DB_DIR = path.join(os.homedir(), ".claude", "claudecodeui");
18
+ const DB_PATH = path.join(DB_DIR, "cache.db");
19
+
20
+ // Database instance (singleton)
21
+ let db = null;
22
+
23
+ /**
24
+ * Initialize the database with schema
25
+ */
26
+ function initializeSchema(database) {
27
+ database.exec(`
28
+ -- Track file processing state for incremental updates
29
+ CREATE TABLE IF NOT EXISTS file_state (
30
+ file_path TEXT PRIMARY KEY,
31
+ last_byte_offset INTEGER DEFAULT 0,
32
+ last_mtime REAL,
33
+ last_processed_at INTEGER,
34
+ file_size INTEGER DEFAULT 0
35
+ );
36
+
37
+ -- Projects metadata
38
+ CREATE TABLE IF NOT EXISTS projects (
39
+ name TEXT PRIMARY KEY,
40
+ display_name TEXT,
41
+ full_path TEXT,
42
+ session_count INTEGER DEFAULT 0,
43
+ last_activity INTEGER,
44
+ has_claude_sessions INTEGER DEFAULT 0,
45
+ has_cursor_sessions INTEGER DEFAULT 0,
46
+ has_codex_sessions INTEGER DEFAULT 0,
47
+ has_taskmaster INTEGER DEFAULT 0,
48
+ updated_at INTEGER
49
+ );
50
+
51
+ -- Sessions metadata (lightweight)
52
+ CREATE TABLE IF NOT EXISTS sessions (
53
+ id TEXT PRIMARY KEY,
54
+ project_name TEXT NOT NULL,
55
+ summary TEXT DEFAULT 'New Session',
56
+ message_count INTEGER DEFAULT 0,
57
+ last_activity INTEGER,
58
+ cwd TEXT,
59
+ provider TEXT DEFAULT 'claude',
60
+ is_grouped INTEGER DEFAULT 0,
61
+ group_id TEXT,
62
+ file_path TEXT,
63
+ updated_at INTEGER
64
+ );
65
+
66
+ -- Message index (byte offsets for on-demand loading)
67
+ CREATE TABLE IF NOT EXISTS message_index (
68
+ session_id TEXT NOT NULL,
69
+ message_number INTEGER NOT NULL,
70
+ uuid TEXT,
71
+ type TEXT,
72
+ timestamp INTEGER,
73
+ byte_offset INTEGER NOT NULL,
74
+ file_path TEXT NOT NULL,
75
+ PRIMARY KEY (session_id, message_number)
76
+ );
77
+
78
+ -- UUID mapping for timeline detection
79
+ CREATE TABLE IF NOT EXISTS uuid_mapping (
80
+ uuid TEXT PRIMARY KEY,
81
+ session_id TEXT NOT NULL,
82
+ parent_uuid TEXT,
83
+ type TEXT
84
+ );
85
+
86
+ -- History prompts (from history.jsonl)
87
+ CREATE TABLE IF NOT EXISTS history_prompts (
88
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
89
+ session_id TEXT NOT NULL,
90
+ prompt TEXT,
91
+ timestamp INTEGER,
92
+ project_path TEXT
93
+ );
94
+
95
+ -- Indexes for common queries
96
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_name);
97
+ CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(last_activity DESC);
98
+ CREATE INDEX IF NOT EXISTS idx_sessions_provider ON sessions(provider);
99
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON message_index(session_id);
100
+ CREATE INDEX IF NOT EXISTS idx_uuid_session ON uuid_mapping(session_id);
101
+ CREATE INDEX IF NOT EXISTS idx_uuid_parent ON uuid_mapping(parent_uuid);
102
+ CREATE INDEX IF NOT EXISTS idx_history_session ON history_prompts(session_id);
103
+ CREATE INDEX IF NOT EXISTS idx_projects_activity ON projects(last_activity DESC);
104
+
105
+ -- Version tracking for cache invalidation
106
+ CREATE TABLE IF NOT EXISTS cache_version (
107
+ key TEXT PRIMARY KEY,
108
+ version INTEGER DEFAULT 0,
109
+ updated_at INTEGER
110
+ );
111
+ `);
112
+
113
+ // Initialize version counters
114
+ database
115
+ .prepare(
116
+ `
117
+ INSERT OR IGNORE INTO cache_version (key, version, updated_at)
118
+ VALUES ('sessions', 0, ?), ('projects', 0, ?), ('messages', 0, ?)
119
+ `,
120
+ )
121
+ .run(Date.now(), Date.now(), Date.now());
122
+ }
123
+
124
+ /**
125
+ * Get or create database instance
126
+ */
127
+ function getDatabase() {
128
+ if (db) return db;
129
+
130
+ // Ensure directory exists
131
+ if (!fs.existsSync(DB_DIR)) {
132
+ fs.mkdirSync(DB_DIR, { recursive: true });
133
+ }
134
+
135
+ try {
136
+ db = new Database(DB_PATH);
137
+ db.pragma("journal_mode = WAL"); // Better concurrent access
138
+ db.pragma("synchronous = NORMAL"); // Good balance of safety/speed
139
+ db.pragma("cache_size = -64000"); // 64MB page cache
140
+ db.pragma("temp_store = MEMORY");
141
+
142
+ initializeSchema(db);
143
+ log.info({ path: DB_PATH }, "Database initialized");
144
+
145
+ return db;
146
+ } catch (error) {
147
+ log.error(
148
+ { error: error.message, path: DB_PATH },
149
+ "Failed to open database",
150
+ );
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Close database connection
157
+ */
158
+ function closeDatabase() {
159
+ if (db) {
160
+ db.close();
161
+ db = null;
162
+ log.info("Database closed");
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Increment version counter for a cache type
168
+ */
169
+ function incrementVersion(key) {
170
+ const database = getDatabase();
171
+ database
172
+ .prepare(
173
+ `
174
+ UPDATE cache_version SET version = version + 1, updated_at = ? WHERE key = ?
175
+ `,
176
+ )
177
+ .run(Date.now(), key);
178
+ }
179
+
180
+ /**
181
+ * Get current version for a cache type
182
+ */
183
+ function getVersion(key) {
184
+ const database = getDatabase();
185
+ const row = database
186
+ .prepare(
187
+ `
188
+ SELECT version, updated_at FROM cache_version WHERE key = ?
189
+ `,
190
+ )
191
+ .get(key);
192
+ return row || { version: 0, updated_at: 0 };
193
+ }
194
+
195
+ // ============================================================
196
+ // File State Management
197
+ // ============================================================
198
+
199
+ /**
200
+ * Get the processing state for a file
201
+ */
202
+ function getFileState(filePath) {
203
+ const database = getDatabase();
204
+ return database
205
+ .prepare(
206
+ `
207
+ SELECT last_byte_offset, last_mtime, last_processed_at, file_size
208
+ FROM file_state WHERE file_path = ?
209
+ `,
210
+ )
211
+ .get(filePath);
212
+ }
213
+
214
+ /**
215
+ * Update file processing state
216
+ */
217
+ function updateFileState(filePath, byteOffset, mtime, fileSize) {
218
+ const database = getDatabase();
219
+ database
220
+ .prepare(
221
+ `
222
+ INSERT OR REPLACE INTO file_state (file_path, last_byte_offset, last_mtime, last_processed_at, file_size)
223
+ VALUES (?, ?, ?, ?, ?)
224
+ `,
225
+ )
226
+ .run(filePath, byteOffset, mtime, Date.now(), fileSize);
227
+ }
228
+
229
+ /**
230
+ * Reset file state (force re-index)
231
+ */
232
+ function resetFileState(filePath) {
233
+ const database = getDatabase();
234
+ database.prepare(`DELETE FROM file_state WHERE file_path = ?`).run(filePath);
235
+ }
236
+
237
+ // ============================================================
238
+ // Project Operations
239
+ // ============================================================
240
+
241
+ /**
242
+ * Upsert a project
243
+ */
244
+ function upsertProject(project) {
245
+ const database = getDatabase();
246
+ database
247
+ .prepare(
248
+ `
249
+ INSERT INTO projects (name, display_name, full_path, session_count, last_activity,
250
+ has_claude_sessions, has_cursor_sessions, has_codex_sessions,
251
+ has_taskmaster, updated_at)
252
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
253
+ ON CONFLICT(name) DO UPDATE SET
254
+ display_name = excluded.display_name,
255
+ full_path = excluded.full_path,
256
+ session_count = excluded.session_count,
257
+ last_activity = CASE WHEN excluded.last_activity > last_activity
258
+ THEN excluded.last_activity ELSE last_activity END,
259
+ has_claude_sessions = excluded.has_claude_sessions OR has_claude_sessions,
260
+ has_cursor_sessions = excluded.has_cursor_sessions OR has_cursor_sessions,
261
+ has_codex_sessions = excluded.has_codex_sessions OR has_codex_sessions,
262
+ has_taskmaster = excluded.has_taskmaster OR has_taskmaster,
263
+ updated_at = excluded.updated_at
264
+ `,
265
+ )
266
+ .run(
267
+ project.name,
268
+ project.displayName || project.name,
269
+ project.fullPath || "",
270
+ project.sessionCount || 0,
271
+ project.lastActivity ? new Date(project.lastActivity).getTime() : null,
272
+ project.hasClaudeSessions ? 1 : 0,
273
+ project.hasCursorSessions ? 1 : 0,
274
+ project.hasCodexSessions ? 1 : 0,
275
+ project.hasTaskmaster ? 1 : 0,
276
+ Date.now(),
277
+ );
278
+ incrementVersion("projects");
279
+ }
280
+
281
+ /**
282
+ * Get all projects with optional timeframe filter
283
+ */
284
+ function getProjectsFromDb(timeframMs = null) {
285
+ const database = getDatabase();
286
+ let query = `
287
+ SELECT name, display_name as displayName, full_path as fullPath,
288
+ session_count as sessionCount, last_activity as lastActivity,
289
+ has_claude_sessions as hasClaudeSessions,
290
+ has_cursor_sessions as hasCursorSessions,
291
+ has_codex_sessions as hasCodexSessions,
292
+ has_taskmaster as hasTaskmaster
293
+ FROM projects
294
+ `;
295
+
296
+ if (timeframMs) {
297
+ const cutoff = Date.now() - timeframMs;
298
+ query += ` WHERE last_activity >= ${cutoff}`;
299
+ }
300
+
301
+ query += ` ORDER BY last_activity DESC`;
302
+
303
+ return database
304
+ .prepare(query)
305
+ .all()
306
+ .map((row) => ({
307
+ ...row,
308
+ lastActivity: row.lastActivity
309
+ ? new Date(row.lastActivity).toISOString()
310
+ : null,
311
+ hasClaudeSessions: !!row.hasClaudeSessions,
312
+ hasCursorSessions: !!row.hasCursorSessions,
313
+ hasCodexSessions: !!row.hasCodexSessions,
314
+ hasTaskmaster: !!row.hasTaskmaster,
315
+ }));
316
+ }
317
+
318
+ /**
319
+ * Update project session count
320
+ */
321
+ function updateProjectSessionCount(projectName) {
322
+ const database = getDatabase();
323
+ const count = database
324
+ .prepare(
325
+ `
326
+ SELECT COUNT(*) as count FROM sessions WHERE project_name = ?
327
+ `,
328
+ )
329
+ .get(projectName);
330
+
331
+ database
332
+ .prepare(
333
+ `
334
+ UPDATE projects SET session_count = ?, updated_at = ? WHERE name = ?
335
+ `,
336
+ )
337
+ .run(count.count, Date.now(), projectName);
338
+ }
339
+
340
+ // ============================================================
341
+ // Session Operations
342
+ // ============================================================
343
+
344
+ /**
345
+ * Upsert a session
346
+ */
347
+ function upsertSession(session) {
348
+ const database = getDatabase();
349
+ database
350
+ .prepare(
351
+ `
352
+ INSERT INTO sessions (id, project_name, summary, message_count, last_activity,
353
+ cwd, provider, is_grouped, group_id, file_path, updated_at)
354
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
355
+ ON CONFLICT(id) DO UPDATE SET
356
+ summary = CASE WHEN excluded.summary != 'New Session' THEN excluded.summary ELSE summary END,
357
+ message_count = CASE WHEN excluded.message_count > message_count
358
+ THEN excluded.message_count ELSE message_count END,
359
+ last_activity = CASE WHEN excluded.last_activity > last_activity
360
+ THEN excluded.last_activity ELSE last_activity END,
361
+ cwd = COALESCE(excluded.cwd, cwd),
362
+ file_path = COALESCE(excluded.file_path, file_path),
363
+ updated_at = excluded.updated_at
364
+ `,
365
+ )
366
+ .run(
367
+ session.id,
368
+ session.projectName,
369
+ session.summary || "New Session",
370
+ session.messageCount || 0,
371
+ session.lastActivity ? new Date(session.lastActivity).getTime() : null,
372
+ session.cwd || null,
373
+ session.provider || "claude",
374
+ session.isGrouped ? 1 : 0,
375
+ session.groupId || null,
376
+ session.filePath || null,
377
+ Date.now(),
378
+ );
379
+ incrementVersion("sessions");
380
+ }
381
+
382
+ /**
383
+ * Get sessions with optional filters
384
+ */
385
+ function getSessions(options = {}) {
386
+ const database = getDatabase();
387
+ const {
388
+ projectName,
389
+ timeframMs,
390
+ limit = 100,
391
+ offset = 0,
392
+ provider,
393
+ } = options;
394
+
395
+ let query = `
396
+ SELECT s.id, s.project_name as projectName, s.summary, s.message_count as messageCount,
397
+ s.last_activity as lastActivity, s.cwd, s.provider, s.is_grouped as isGrouped,
398
+ s.group_id as groupId,
399
+ p.display_name as projectDisplayName, p.full_path as projectFullPath
400
+ FROM sessions s
401
+ LEFT JOIN projects p ON s.project_name = p.name
402
+ WHERE 1=1
403
+ `;
404
+
405
+ const params = [];
406
+
407
+ if (projectName) {
408
+ query += ` AND s.project_name = ?`;
409
+ params.push(projectName);
410
+ }
411
+
412
+ if (timeframMs) {
413
+ const cutoff = Date.now() - timeframMs;
414
+ query += ` AND s.last_activity >= ?`;
415
+ params.push(cutoff);
416
+ }
417
+
418
+ if (provider) {
419
+ query += ` AND s.provider = ?`;
420
+ params.push(provider);
421
+ }
422
+
423
+ query += ` ORDER BY s.last_activity DESC LIMIT ? OFFSET ?`;
424
+ params.push(limit, offset);
425
+
426
+ return database
427
+ .prepare(query)
428
+ .all(...params)
429
+ .map((row) => ({
430
+ ...row,
431
+ lastActivity: row.lastActivity
432
+ ? new Date(row.lastActivity).toISOString()
433
+ : null,
434
+ isGrouped: !!row.isGrouped,
435
+ project: {
436
+ name: row.projectName,
437
+ displayName: row.projectDisplayName,
438
+ fullPath: row.projectFullPath,
439
+ },
440
+ }));
441
+ }
442
+
443
+ /**
444
+ * Get session count
445
+ */
446
+ function getSessionCount(options = {}) {
447
+ const database = getDatabase();
448
+ const { projectName, timeframMs, provider } = options;
449
+
450
+ let query = `SELECT COUNT(*) as count FROM sessions WHERE 1=1`;
451
+ const params = [];
452
+
453
+ if (projectName) {
454
+ query += ` AND project_name = ?`;
455
+ params.push(projectName);
456
+ }
457
+
458
+ if (timeframMs) {
459
+ const cutoff = Date.now() - timeframMs;
460
+ query += ` AND last_activity >= ?`;
461
+ params.push(cutoff);
462
+ }
463
+
464
+ if (provider) {
465
+ query += ` AND provider = ?`;
466
+ params.push(provider);
467
+ }
468
+
469
+ return database.prepare(query).get(...params).count;
470
+ }
471
+
472
+ /**
473
+ * Get session by ID
474
+ */
475
+ function getSession(sessionId) {
476
+ const database = getDatabase();
477
+ const row = database
478
+ .prepare(
479
+ `
480
+ SELECT s.*, p.display_name as projectDisplayName, p.full_path as projectFullPath
481
+ FROM sessions s
482
+ LEFT JOIN projects p ON s.project_name = p.name
483
+ WHERE s.id = ?
484
+ `,
485
+ )
486
+ .get(sessionId);
487
+
488
+ if (!row) return null;
489
+
490
+ return {
491
+ id: row.id,
492
+ projectName: row.project_name,
493
+ summary: row.summary,
494
+ messageCount: row.message_count,
495
+ lastActivity: row.last_activity
496
+ ? new Date(row.last_activity).toISOString()
497
+ : null,
498
+ cwd: row.cwd,
499
+ provider: row.provider,
500
+ filePath: row.file_path,
501
+ project: {
502
+ name: row.project_name,
503
+ displayName: row.projectDisplayName,
504
+ fullPath: row.projectFullPath,
505
+ },
506
+ };
507
+ }
508
+
509
+ /**
510
+ * Update session summary
511
+ */
512
+ function updateSessionSummary(sessionId, summary) {
513
+ const database = getDatabase();
514
+ database
515
+ .prepare(
516
+ `
517
+ UPDATE sessions SET summary = ?, updated_at = ? WHERE id = ?
518
+ `,
519
+ )
520
+ .run(summary, Date.now(), sessionId);
521
+ incrementVersion("sessions");
522
+ }
523
+
524
+ // ============================================================
525
+ // Message Index Operations
526
+ // ============================================================
527
+
528
+ /**
529
+ * Insert message index entry
530
+ */
531
+ function insertMessageIndex(entry) {
532
+ const database = getDatabase();
533
+ database
534
+ .prepare(
535
+ `
536
+ INSERT OR REPLACE INTO message_index (session_id, message_number, uuid, type, timestamp, byte_offset, file_path)
537
+ VALUES (?, ?, ?, ?, ?, ?, ?)
538
+ `,
539
+ )
540
+ .run(
541
+ entry.sessionId,
542
+ entry.messageNumber,
543
+ entry.uuid || null,
544
+ entry.type || null,
545
+ entry.timestamp ? new Date(entry.timestamp).getTime() : null,
546
+ entry.byteOffset,
547
+ entry.filePath,
548
+ );
549
+ }
550
+
551
+ /**
552
+ * Bulk insert message indexes (for efficiency)
553
+ */
554
+ function insertMessageIndexBatch(entries) {
555
+ const database = getDatabase();
556
+ const insert = database.prepare(`
557
+ INSERT OR REPLACE INTO message_index (session_id, message_number, uuid, type, timestamp, byte_offset, file_path)
558
+ VALUES (?, ?, ?, ?, ?, ?, ?)
559
+ `);
560
+
561
+ const insertMany = database.transaction((entries) => {
562
+ for (const entry of entries) {
563
+ insert.run(
564
+ entry.sessionId,
565
+ entry.messageNumber,
566
+ entry.uuid || null,
567
+ entry.type || null,
568
+ entry.timestamp ? new Date(entry.timestamp).getTime() : null,
569
+ entry.byteOffset,
570
+ entry.filePath,
571
+ );
572
+ }
573
+ });
574
+
575
+ insertMany(entries);
576
+ incrementVersion("messages");
577
+ }
578
+
579
+ /**
580
+ * Get message index entry
581
+ */
582
+ function getMessageIndex(sessionId, messageNumber) {
583
+ const database = getDatabase();
584
+ return database
585
+ .prepare(
586
+ `
587
+ SELECT * FROM message_index WHERE session_id = ? AND message_number = ?
588
+ `,
589
+ )
590
+ .get(sessionId, messageNumber);
591
+ }
592
+
593
+ /**
594
+ * Get message list for a session
595
+ */
596
+ function getMessageListFromDb(sessionId) {
597
+ const database = getDatabase();
598
+ return database
599
+ .prepare(
600
+ `
601
+ SELECT message_number as number, uuid as id, type, timestamp
602
+ FROM message_index
603
+ WHERE session_id = ?
604
+ ORDER BY message_number ASC
605
+ `,
606
+ )
607
+ .all(sessionId)
608
+ .map((row) => ({
609
+ ...row,
610
+ timestamp: row.timestamp ? new Date(row.timestamp).toISOString() : null,
611
+ }));
612
+ }
613
+
614
+ /**
615
+ * Get message count for a session
616
+ */
617
+ function getMessageCountFromDb(sessionId) {
618
+ const database = getDatabase();
619
+ const row = database
620
+ .prepare(
621
+ `
622
+ SELECT COUNT(*) as count FROM message_index WHERE session_id = ?
623
+ `,
624
+ )
625
+ .get(sessionId);
626
+ return row ? row.count : 0;
627
+ }
628
+
629
+ /**
630
+ * Delete message indexes for a session (for re-indexing)
631
+ */
632
+ function deleteSessionMessageIndexes(sessionId) {
633
+ const database = getDatabase();
634
+ database
635
+ .prepare(`DELETE FROM message_index WHERE session_id = ?`)
636
+ .run(sessionId);
637
+ }
638
+
639
+ // ============================================================
640
+ // UUID Mapping Operations
641
+ // ============================================================
642
+
643
+ /**
644
+ * Insert UUID mapping
645
+ */
646
+ function insertUuidMapping(uuid, sessionId, parentUuid, type) {
647
+ const database = getDatabase();
648
+ database
649
+ .prepare(
650
+ `
651
+ INSERT OR REPLACE INTO uuid_mapping (uuid, session_id, parent_uuid, type)
652
+ VALUES (?, ?, ?, ?)
653
+ `,
654
+ )
655
+ .run(uuid, sessionId, parentUuid, type);
656
+ }
657
+
658
+ /**
659
+ * Bulk insert UUID mappings
660
+ */
661
+ function insertUuidMappingBatch(mappings) {
662
+ const database = getDatabase();
663
+ const insert = database.prepare(`
664
+ INSERT OR REPLACE INTO uuid_mapping (uuid, session_id, parent_uuid, type)
665
+ VALUES (?, ?, ?, ?)
666
+ `);
667
+
668
+ const insertMany = database.transaction((mappings) => {
669
+ for (const m of mappings) {
670
+ insert.run(m.uuid, m.sessionId, m.parentUuid || null, m.type || null);
671
+ }
672
+ });
673
+
674
+ insertMany(mappings);
675
+ }
676
+
677
+ /**
678
+ * Get session ID for a UUID
679
+ */
680
+ function getSessionIdForUuid(uuid) {
681
+ const database = getDatabase();
682
+ const row = database
683
+ .prepare(
684
+ `
685
+ SELECT session_id FROM uuid_mapping WHERE uuid = ?
686
+ `,
687
+ )
688
+ .get(uuid);
689
+ return row ? row.session_id : null;
690
+ }
691
+
692
+ /**
693
+ * Get first user messages (for timeline grouping)
694
+ */
695
+ function getFirstUserMessages(projectName = null) {
696
+ const database = getDatabase();
697
+ let query = `
698
+ SELECT u.uuid, u.session_id as sessionId
699
+ FROM uuid_mapping u
700
+ WHERE u.parent_uuid IS NULL AND u.type = 'user'
701
+ `;
702
+
703
+ if (projectName) {
704
+ query += `
705
+ AND EXISTS (SELECT 1 FROM sessions s WHERE s.id = u.session_id AND s.project_name = ?)
706
+ `;
707
+ return database.prepare(query).all(projectName);
708
+ }
709
+
710
+ return database.prepare(query).all();
711
+ }
712
+
713
+ // ============================================================
714
+ // History Prompts Operations
715
+ // ============================================================
716
+
717
+ /**
718
+ * Insert history prompt
719
+ */
720
+ function insertHistoryPrompt(sessionId, prompt, timestamp, projectPath) {
721
+ const database = getDatabase();
722
+ database
723
+ .prepare(
724
+ `
725
+ INSERT INTO history_prompts (session_id, prompt, timestamp, project_path)
726
+ VALUES (?, ?, ?, ?)
727
+ `,
728
+ )
729
+ .run(sessionId, prompt, timestamp, projectPath);
730
+ }
731
+
732
+ /**
733
+ * Get prompts for a session
734
+ */
735
+ function getSessionPromptsFromDb(sessionId) {
736
+ const database = getDatabase();
737
+ return database
738
+ .prepare(
739
+ `
740
+ SELECT prompt, timestamp, project_path as projectPath
741
+ FROM history_prompts
742
+ WHERE session_id = ?
743
+ ORDER BY timestamp ASC
744
+ `,
745
+ )
746
+ .all(sessionId);
747
+ }
748
+
749
+ /**
750
+ * Clear history prompts (for re-indexing)
751
+ */
752
+ function clearHistoryPrompts() {
753
+ const database = getDatabase();
754
+ database.prepare(`DELETE FROM history_prompts`).run();
755
+ }
756
+
757
+ // ============================================================
758
+ // Utility Operations
759
+ // ============================================================
760
+
761
+ /**
762
+ * Get a project's cwd from its sessions
763
+ * Useful as a fallback when session files are skipped during indexing
764
+ */
765
+ function getProjectCwdFromSessions(projectName) {
766
+ const database = getDatabase();
767
+ const row = database
768
+ .prepare(
769
+ `SELECT cwd FROM sessions WHERE project_name = ? AND cwd IS NOT NULL AND cwd != '' LIMIT 1`,
770
+ )
771
+ .get(projectName);
772
+ return row ? row.cwd : null;
773
+ }
774
+
775
+ /**
776
+ * Get database statistics
777
+ */
778
+ function getStats() {
779
+ const database = getDatabase();
780
+ return {
781
+ projects: database.prepare(`SELECT COUNT(*) as count FROM projects`).get()
782
+ .count,
783
+ sessions: database.prepare(`SELECT COUNT(*) as count FROM sessions`).get()
784
+ .count,
785
+ messageIndexes: database
786
+ .prepare(`SELECT COUNT(*) as count FROM message_index`)
787
+ .get().count,
788
+ uuidMappings: database
789
+ .prepare(`SELECT COUNT(*) as count FROM uuid_mapping`)
790
+ .get().count,
791
+ historyPrompts: database
792
+ .prepare(`SELECT COUNT(*) as count FROM history_prompts`)
793
+ .get().count,
794
+ versions: {
795
+ sessions: getVersion("sessions"),
796
+ projects: getVersion("projects"),
797
+ messages: getVersion("messages"),
798
+ },
799
+ };
800
+ }
801
+
802
+ /**
803
+ * Clear all data (for testing or reset)
804
+ */
805
+ function clearAllData() {
806
+ const database = getDatabase();
807
+ database.exec(`
808
+ DELETE FROM message_index;
809
+ DELETE FROM uuid_mapping;
810
+ DELETE FROM history_prompts;
811
+ DELETE FROM sessions;
812
+ DELETE FROM projects;
813
+ DELETE FROM file_state;
814
+ `);
815
+ database
816
+ .prepare(`UPDATE cache_version SET version = 0, updated_at = ?`)
817
+ .run(Date.now());
818
+ log.info("All data cleared");
819
+ }
820
+
821
+ export {
822
+ getDatabase,
823
+ closeDatabase,
824
+ incrementVersion,
825
+ getVersion,
826
+ // File state
827
+ getFileState,
828
+ updateFileState,
829
+ resetFileState,
830
+ // Projects
831
+ upsertProject,
832
+ getProjectsFromDb,
833
+ updateProjectSessionCount,
834
+ // Sessions
835
+ upsertSession,
836
+ getSessions,
837
+ getSessionCount,
838
+ getSession,
839
+ updateSessionSummary,
840
+ getProjectCwdFromSessions,
841
+ // Message index
842
+ insertMessageIndex,
843
+ insertMessageIndexBatch,
844
+ getMessageIndex,
845
+ getMessageListFromDb,
846
+ getMessageCountFromDb,
847
+ deleteSessionMessageIndexes,
848
+ // UUID mapping
849
+ insertUuidMapping,
850
+ insertUuidMappingBatch,
851
+ getSessionIdForUuid,
852
+ getFirstUserMessages,
853
+ // History prompts
854
+ insertHistoryPrompt,
855
+ getSessionPromptsFromDb,
856
+ clearHistoryPrompts,
857
+ // Utility
858
+ getStats,
859
+ clearAllData,
860
+ DB_PATH,
861
+ };