@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.
- package/dist/assets/index-CceDF8mT.css +32 -0
- package/dist/assets/index-Dlv06cpK.js +1245 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +44 -4
- package/package.json +4 -2
- package/public/sw.js +44 -4
- package/server/database/db.js +26 -16
- package/server/database/init.sql +2 -1
- package/server/database.js +861 -0
- package/server/db-indexer.js +401 -0
- package/server/external-session-detector.js +48 -392
- package/server/history-cache.js +354 -0
- package/server/index.js +457 -48
- package/server/logger.js +59 -0
- package/server/maintenance-scheduler.js +172 -0
- package/server/messages-cache.js +485 -0
- package/server/openai-codex.js +110 -102
- package/server/orchestrator/client.js +52 -0
- package/server/process-cache.js +513 -0
- package/server/projects.js +64 -47
- package/server/routes/auth.js +18 -12
- package/server/routes/sessions.js +59 -33
- package/server/session-lock.js +2 -10
- package/server/sessions-cache.js +16 -0
- package/dist/assets/index-BGneYLVE.css +0 -32
- package/dist/assets/index-DM1BeYBg.js +0 -1245
|
@@ -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
|
+
};
|