agentgui 1.0.274 → 1.0.276

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.
Files changed (70) hide show
  1. package/CLAUDE.md +280 -280
  2. package/IPFS_DOWNLOADER.md +277 -277
  3. package/TASK_2C_COMPLETION.md +334 -334
  4. package/agentgui.ico +0 -0
  5. package/bin/gmgui.cjs +54 -54
  6. package/build-portable.js +13 -42
  7. package/database.js +1422 -1406
  8. package/lib/claude-runner.js +1130 -1130
  9. package/lib/ipfs-downloader.js +459 -459
  10. package/lib/speech.js +159 -152
  11. package/package.json +1 -1
  12. package/readme.md +76 -76
  13. package/server.js +3787 -3794
  14. package/setup-npm-token.sh +68 -68
  15. package/static/app.js +773 -773
  16. package/static/event-rendering-showcase.html +708 -708
  17. package/static/index.html +3178 -3180
  18. package/static/js/agent-auth.js +298 -298
  19. package/static/js/audio-recorder-processor.js +18 -18
  20. package/static/js/client.js +2656 -2656
  21. package/static/js/conversations.js +583 -583
  22. package/static/js/dialogs.js +267 -267
  23. package/static/js/event-consolidator.js +101 -101
  24. package/static/js/event-filter.js +311 -311
  25. package/static/js/event-processor.js +452 -452
  26. package/static/js/features.js +413 -413
  27. package/static/js/kalman-filter.js +67 -67
  28. package/static/js/progress-dialog.js +130 -130
  29. package/static/js/script-runner.js +219 -219
  30. package/static/js/streaming-renderer.js +2123 -2120
  31. package/static/js/syntax-highlighter.js +269 -269
  32. package/static/js/tts-websocket-handler.js +152 -152
  33. package/static/js/ui-components.js +431 -431
  34. package/static/js/voice.js +849 -849
  35. package/static/js/websocket-manager.js +596 -596
  36. package/static/templates/INDEX.html +465 -465
  37. package/static/templates/README.md +190 -190
  38. package/static/templates/agent-capabilities.html +56 -56
  39. package/static/templates/agent-metadata-panel.html +44 -44
  40. package/static/templates/agent-status-badge.html +30 -30
  41. package/static/templates/code-annotation-panel.html +155 -155
  42. package/static/templates/code-suggestion-panel.html +184 -184
  43. package/static/templates/command-header.html +77 -77
  44. package/static/templates/command-output-scrollable.html +118 -118
  45. package/static/templates/elapsed-time.html +54 -54
  46. package/static/templates/error-alert.html +106 -106
  47. package/static/templates/error-history-timeline.html +160 -160
  48. package/static/templates/error-recovery-options.html +109 -109
  49. package/static/templates/error-stack-trace.html +95 -95
  50. package/static/templates/error-summary.html +80 -80
  51. package/static/templates/event-counter.html +48 -48
  52. package/static/templates/execution-actions.html +97 -97
  53. package/static/templates/execution-progress-bar.html +80 -80
  54. package/static/templates/execution-stepper.html +120 -120
  55. package/static/templates/file-breadcrumb.html +118 -118
  56. package/static/templates/file-diff-viewer.html +121 -121
  57. package/static/templates/file-metadata.html +133 -133
  58. package/static/templates/file-read-panel.html +66 -66
  59. package/static/templates/file-write-panel.html +120 -120
  60. package/static/templates/git-branch-remote.html +107 -107
  61. package/static/templates/git-diff-list.html +101 -101
  62. package/static/templates/git-log-visualization.html +153 -153
  63. package/static/templates/git-status-panel.html +115 -115
  64. package/static/templates/quality-metrics-display.html +170 -170
  65. package/static/templates/terminal-output-panel.html +87 -87
  66. package/static/templates/test-results-display.html +144 -144
  67. package/static/theme.js +72 -72
  68. package/test-download-progress.js +223 -223
  69. package/test-websocket-broadcast.js +147 -147
  70. package/tests/ipfs-downloader.test.js +370 -370
package/database.js CHANGED
@@ -4,1415 +4,1431 @@ import os from 'os';
4
4
  import { createRequire } from 'module';
5
5
 
6
6
  const require = createRequire(import.meta.url);
7
- const dbDir = path.join(os.homedir(), '.gmgui');
8
- const dbFilePath = path.join(dbDir, 'data.db');
9
- const oldJsonPath = path.join(dbDir, 'data.json');
10
-
11
- if (!fs.existsSync(dbDir)) {
12
- fs.mkdirSync(dbDir, { recursive: true });
13
- }
14
7
 
15
- let db;
16
- try {
17
- const Database = (await import('bun:sqlite')).default;
18
- db = new Database(dbFilePath);
19
- db.run('PRAGMA journal_mode = WAL');
20
- db.run('PRAGMA foreign_keys = ON');
21
- db.run('PRAGMA encoding = "UTF-8"');
22
- db.run('PRAGMA synchronous = NORMAL');
23
- db.run('PRAGMA cache_size = -64000');
24
- db.run('PRAGMA mmap_size = 268435456');
25
- db.run('PRAGMA temp_store = MEMORY');
26
- } catch (e) {
27
- try {
28
- const sqlite3 = require('better-sqlite3');
29
- db = new sqlite3(dbFilePath);
30
- db.pragma('journal_mode = WAL');
31
- db.pragma('foreign_keys = ON');
32
- db.pragma('encoding = "UTF-8"');
33
- db.pragma('synchronous = NORMAL');
34
- db.pragma('cache_size = -64000');
35
- db.pragma('mmap_size = 268435456');
36
- db.pragma('temp_store = MEMORY');
37
- } catch (e2) {
38
- throw new Error('SQLite database is required. Please run with bun (recommended) or install better-sqlite3: npm install better-sqlite3');
8
+ function getDataDir() {
9
+ if (process.env.PORTABLE_DATA_DIR) {
10
+ return process.env.PORTABLE_DATA_DIR;
39
11
  }
40
- }
41
-
42
- function initSchema() {
43
- // Create table with minimal schema - columns will be added by migration
44
- db.exec(`
45
- CREATE TABLE IF NOT EXISTS conversations (
46
- id TEXT PRIMARY KEY,
47
- agentId TEXT NOT NULL,
48
- title TEXT,
49
- created_at INTEGER NOT NULL,
50
- updated_at INTEGER NOT NULL,
51
- status TEXT DEFAULT 'active'
52
- );
53
-
54
- CREATE INDEX IF NOT EXISTS idx_conversations_agent ON conversations(agentId);
55
- CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at DESC);
56
-
57
- CREATE TABLE IF NOT EXISTS messages (
58
- id TEXT PRIMARY KEY,
59
- conversationId TEXT NOT NULL,
60
- role TEXT NOT NULL,
61
- content TEXT NOT NULL,
62
- created_at INTEGER NOT NULL,
63
- FOREIGN KEY (conversationId) REFERENCES conversations(id)
64
- );
65
-
66
- CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversationId);
67
-
68
- CREATE TABLE IF NOT EXISTS sessions (
69
- id TEXT PRIMARY KEY,
70
- conversationId TEXT NOT NULL,
71
- status TEXT NOT NULL,
72
- started_at INTEGER NOT NULL,
73
- completed_at INTEGER,
74
- response TEXT,
75
- error TEXT,
76
- FOREIGN KEY (conversationId) REFERENCES conversations(id)
77
- );
78
-
79
- CREATE INDEX IF NOT EXISTS idx_sessions_conversation ON sessions(conversationId);
80
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(conversationId, status);
81
-
82
- CREATE TABLE IF NOT EXISTS events (
83
- id TEXT PRIMARY KEY,
84
- type TEXT NOT NULL,
85
- conversationId TEXT,
86
- sessionId TEXT,
87
- data TEXT NOT NULL,
88
- created_at INTEGER NOT NULL,
89
- FOREIGN KEY (conversationId) REFERENCES conversations(id),
90
- FOREIGN KEY (sessionId) REFERENCES sessions(id)
91
- );
92
-
93
- CREATE INDEX IF NOT EXISTS idx_events_conversation ON events(conversationId);
94
-
95
- CREATE TABLE IF NOT EXISTS idempotencyKeys (
96
- key TEXT PRIMARY KEY,
97
- value TEXT NOT NULL,
98
- created_at INTEGER NOT NULL,
99
- ttl INTEGER NOT NULL
100
- );
101
-
102
- CREATE INDEX IF NOT EXISTS idx_idempotency_created ON idempotencyKeys(created_at);
103
-
104
- CREATE TABLE IF NOT EXISTS stream_updates (
105
- id TEXT PRIMARY KEY,
106
- sessionId TEXT NOT NULL,
107
- conversationId TEXT NOT NULL,
108
- updateType TEXT NOT NULL,
109
- content TEXT NOT NULL,
110
- sequence INTEGER NOT NULL,
111
- created_at INTEGER NOT NULL,
112
- FOREIGN KEY (sessionId) REFERENCES sessions(id),
113
- FOREIGN KEY (conversationId) REFERENCES conversations(id)
114
- );
115
-
116
- CREATE INDEX IF NOT EXISTS idx_stream_updates_session ON stream_updates(sessionId);
117
- CREATE INDEX IF NOT EXISTS idx_stream_updates_created ON stream_updates(created_at);
118
-
119
- CREATE TABLE IF NOT EXISTS chunks (
120
- id TEXT PRIMARY KEY,
121
- sessionId TEXT NOT NULL,
122
- conversationId TEXT NOT NULL,
123
- sequence INTEGER NOT NULL,
124
- type TEXT NOT NULL,
125
- data BLOB NOT NULL,
126
- created_at INTEGER NOT NULL,
127
- FOREIGN KEY (sessionId) REFERENCES sessions(id),
128
- FOREIGN KEY (conversationId) REFERENCES conversations(id)
129
- );
130
-
131
- CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(sessionId, sequence);
132
- CREATE INDEX IF NOT EXISTS idx_chunks_conversation ON chunks(conversationId, sequence);
133
- CREATE UNIQUE INDEX IF NOT EXISTS idx_chunks_unique ON chunks(sessionId, sequence);
134
- CREATE INDEX IF NOT EXISTS idx_chunks_conv_created ON chunks(conversationId, created_at);
135
- CREATE INDEX IF NOT EXISTS idx_chunks_sess_created ON chunks(sessionId, created_at);
136
-
137
- CREATE TABLE IF NOT EXISTS ipfs_cids (
138
- id TEXT PRIMARY KEY,
139
- cid TEXT NOT NULL UNIQUE,
140
- modelName TEXT NOT NULL,
141
- modelType TEXT NOT NULL,
142
- modelHash TEXT,
143
- gatewayUrl TEXT,
144
- cached_at INTEGER NOT NULL,
145
- last_accessed_at INTEGER NOT NULL,
146
- success_count INTEGER DEFAULT 0,
147
- failure_count INTEGER DEFAULT 0
148
- );
149
-
150
- CREATE INDEX IF NOT EXISTS idx_ipfs_cids_model ON ipfs_cids(modelName);
151
- CREATE INDEX IF NOT EXISTS idx_ipfs_cids_type ON ipfs_cids(modelType);
152
- CREATE INDEX IF NOT EXISTS idx_ipfs_cids_hash ON ipfs_cids(modelHash);
153
-
154
- CREATE TABLE IF NOT EXISTS ipfs_downloads (
155
- id TEXT PRIMARY KEY,
156
- cidId TEXT NOT NULL,
157
- downloadPath TEXT NOT NULL,
158
- status TEXT DEFAULT 'pending',
159
- downloaded_bytes INTEGER DEFAULT 0,
160
- total_bytes INTEGER,
161
- error_message TEXT,
162
- started_at INTEGER NOT NULL,
163
- completed_at INTEGER,
164
- FOREIGN KEY (cidId) REFERENCES ipfs_cids(id)
165
- );
166
-
167
- CREATE INDEX IF NOT EXISTS idx_ipfs_downloads_cid ON ipfs_downloads(cidId);
168
- CREATE INDEX IF NOT EXISTS idx_ipfs_downloads_status ON ipfs_downloads(status);
169
- CREATE INDEX IF NOT EXISTS idx_ipfs_downloads_started ON ipfs_downloads(started_at);
170
- `);
171
- }
172
-
173
- function migrateFromJson() {
174
- if (!fs.existsSync(oldJsonPath)) return;
175
-
176
- try {
177
- const content = fs.readFileSync(oldJsonPath, 'utf-8');
178
- const data = JSON.parse(content);
179
-
180
- const migrationStmt = db.transaction(() => {
181
- if (data.conversations) {
182
- for (const id in data.conversations) {
183
- const conv = data.conversations[id];
184
- db.prepare(
185
- `INSERT OR REPLACE INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`
186
- ).run(conv.id, conv.agentId, conv.title || null, conv.created_at, conv.updated_at, conv.status || 'active');
187
- }
188
- }
189
-
190
- if (data.messages) {
191
- for (const id in data.messages) {
192
- const msg = data.messages[id];
193
- // Ensure content is always a string (stringify objects)
194
- const contentStr = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
195
- db.prepare(
196
- `INSERT OR REPLACE INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
197
- ).run(msg.id, msg.conversationId, msg.role, contentStr, msg.created_at);
198
- }
199
- }
200
-
201
- if (data.sessions) {
202
- for (const id in data.sessions) {
203
- const sess = data.sessions[id];
204
- // Ensure response and error are strings, not objects
205
- const responseStr = sess.response ? (typeof sess.response === 'string' ? sess.response : JSON.stringify(sess.response)) : null;
206
- const errorStr = sess.error ? (typeof sess.error === 'string' ? sess.error : JSON.stringify(sess.error)) : null;
207
- db.prepare(
208
- `INSERT OR REPLACE INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`
209
- ).run(sess.id, sess.conversationId, sess.status, sess.started_at, sess.completed_at || null, responseStr, errorStr);
210
- }
211
- }
212
-
213
- if (data.events) {
214
- for (const id in data.events) {
215
- const evt = data.events[id];
216
- // Ensure data is always valid JSON string
217
- const dataStr = typeof evt.data === 'string' ? evt.data : JSON.stringify(evt.data || {});
218
- db.prepare(
219
- `INSERT OR REPLACE INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`
220
- ).run(evt.id, evt.type, evt.conversationId || null, evt.sessionId || null, dataStr, evt.created_at);
221
- }
222
- }
223
-
224
- if (data.idempotencyKeys) {
225
- for (const key in data.idempotencyKeys) {
226
- const entry = data.idempotencyKeys[key];
227
- // Ensure value is always valid JSON string
228
- const valueStr = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value || {});
229
- // Ensure ttl is a number
230
- const ttl = typeof entry.ttl === 'number' ? entry.ttl : (entry.ttl ? parseInt(entry.ttl) : null);
231
- db.prepare(
232
- `INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)`
233
- ).run(key, valueStr, entry.created_at, ttl);
234
- }
235
- }
236
- });
237
-
238
- migrationStmt();
239
- fs.renameSync(oldJsonPath, `${oldJsonPath}.migrated`);
240
- console.log('Migrated data from JSON to SQLite');
241
- } catch (e) {
242
- console.error('Error during migration:', e.message);
243
- }
244
- }
245
-
246
- initSchema();
247
- migrateFromJson();
248
-
249
- // Migration: Add imported conversation columns if they don't exist
250
- try {
251
- const result = db.prepare("PRAGMA table_info(conversations)").all();
252
- const columnNames = result.map(r => r.name);
253
- const requiredColumns = {
254
- agentType: 'TEXT DEFAULT "claude-code"',
255
- source: 'TEXT DEFAULT "gui"',
256
- externalId: 'TEXT',
257
- firstPrompt: 'TEXT',
258
- messageCount: 'INTEGER DEFAULT 0',
259
- projectPath: 'TEXT',
260
- gitBranch: 'TEXT',
261
- sourcePath: 'TEXT',
262
- lastSyncedAt: 'INTEGER',
263
- workingDirectory: 'TEXT',
264
- claudeSessionId: 'TEXT',
265
- isStreaming: 'INTEGER DEFAULT 0',
266
- model: 'TEXT'
267
- };
268
-
269
- let addedColumns = false;
270
- for (const [colName, colDef] of Object.entries(requiredColumns)) {
271
- if (!columnNames.includes(colName)) {
272
- db.exec(`ALTER TABLE conversations ADD COLUMN ${colName} ${colDef}`);
273
- console.log(`[Migration] Added column ${colName} to conversations table`);
274
- addedColumns = true;
275
- }
276
- }
277
-
278
- // Add indexes for new columns
279
- if (addedColumns) {
280
- try {
281
- db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_external ON conversations(externalId)`);
282
- db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_agent_type ON conversations(agentType)`);
283
- db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_source ON conversations(source)`);
284
- } catch (e) {
285
- console.warn('[Migration] Index creation warning:', e.message);
286
- }
287
- }
288
- } catch (err) {
289
- console.error('[Migration] Error:', err.message);
290
- }
291
-
292
- // Migration: Add resume capability columns to ipfs_downloads if needed
293
- try {
294
- const result = db.prepare("PRAGMA table_info(ipfs_downloads)").all();
295
- const columnNames = result.map(r => r.name);
296
- const resumeColumns = {
297
- attempts: 'INTEGER DEFAULT 0',
298
- lastAttempt: 'INTEGER',
299
- currentSize: 'INTEGER DEFAULT 0',
300
- hash: 'TEXT'
301
- };
302
-
303
- for (const [colName, colDef] of Object.entries(resumeColumns)) {
304
- if (!columnNames.includes(colName)) {
305
- db.exec(`ALTER TABLE ipfs_downloads ADD COLUMN ${colName} ${colDef}`);
306
- console.log(`[Migration] Added column ${colName} to ipfs_downloads table`);
307
- }
308
- }
309
- } catch (err) {
310
- console.error('[Migration] IPFS schema update warning:', err.message);
311
- }
312
-
313
- // Register official IPFS CIDs for voice models
314
- try {
315
- const LIGHTHOUSE_GATEWAY = 'https://gateway.lighthouse.storage/ipfs';
316
- const WHISPER_CID = 'bafybeidyw252ecy4vs46bbmezrtw325gl2ymdltosmzqgx4edjsc3fbofy';
317
- const TTS_CID = 'bafybeidyw252ecy4vs46bbmezrtw325gl2ymdltosmzqgx4edjsc3fbofy';
318
-
319
- // Check if CIDs are already registered
320
- const existingWhisper = db.prepare('SELECT * FROM ipfs_cids WHERE modelName = ? AND modelType = ?').get('whisper-base', 'stt');
321
- if (!existingWhisper) {
322
- const cidId = `cid-${Date.now()}-whisper`;
323
- db.prepare(
324
- `INSERT INTO ipfs_cids (id, cid, modelName, modelType, modelHash, gatewayUrl, cached_at, last_accessed_at)
325
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
326
- ).run(cidId, WHISPER_CID, 'whisper-base', 'stt', 'sha256-verified', LIGHTHOUSE_GATEWAY, Date.now(), Date.now());
327
- console.log('[MODELS] Registered Whisper STT IPFS CID:', WHISPER_CID);
328
- }
329
-
330
- const existingTTS = db.prepare('SELECT * FROM ipfs_cids WHERE modelName = ? AND modelType = ?').get('tts', 'voice');
331
- if (!existingTTS) {
332
- const cidId = `cid-${Date.now()}-tts`;
333
- db.prepare(
334
- `INSERT INTO ipfs_cids (id, cid, modelName, modelType, modelHash, gatewayUrl, cached_at, last_accessed_at)
335
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
336
- ).run(cidId, TTS_CID, 'tts', 'voice', 'sha256-verified', LIGHTHOUSE_GATEWAY, Date.now(), Date.now());
337
- console.log('[MODELS] Registered TTS IPFS CID:', TTS_CID);
12
+ const exeDir = process.pkg?.path ? path.dirname(process.pkg.path) : null;
13
+ if (exeDir) {
14
+ return path.join(exeDir, 'data');
338
15
  }
339
- } catch (err) {
340
- console.warn('[MODELS] IPFS CID registration warning:', err.message);
341
- }
342
-
343
- const stmtCache = new Map();
344
- function prep(sql) {
345
- let s = stmtCache.get(sql);
346
- if (!s) {
347
- s = db.prepare(sql);
348
- stmtCache.set(sql, s);
16
+ if (process.env.BUN_BE_BUN && process.argv[1]) {
17
+ return path.join(path.dirname(process.argv[1]), 'data');
349
18
  }
350
- return s;
351
- }
352
-
353
- function generateId(prefix) {
354
- return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
19
+ return path.join(os.homedir(), '.gmgui');
355
20
  }
356
21
 
357
- export const queries = {
358
- _db: db,
359
- createConversation(agentType, title = null, workingDirectory = null, model = null) {
360
- const id = generateId('conv');
361
- const now = Date.now();
362
- const stmt = prep(
363
- `INSERT INTO conversations (id, agentId, agentType, title, created_at, updated_at, status, workingDirectory, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
364
- );
365
- stmt.run(id, agentType, agentType, title, now, now, 'active', workingDirectory, model);
366
-
367
- return {
368
- id,
369
- agentType,
370
- title,
371
- workingDirectory,
372
- model,
373
- created_at: now,
374
- updated_at: now,
375
- status: 'active'
376
- };
377
- },
378
-
379
- getConversation(id) {
380
- const stmt = prep('SELECT * FROM conversations WHERE id = ?');
381
- return stmt.get(id);
382
- },
383
-
384
- getAllConversations() {
385
- const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC');
386
- return stmt.all('deleted');
387
- },
388
-
389
- getConversationsList() {
390
- const stmt = prep(
391
- 'SELECT id, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model FROM conversations WHERE status != ? ORDER BY updated_at DESC'
392
- );
393
- return stmt.all('deleted');
394
- },
395
-
396
- getConversations() {
397
- const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC');
398
- return stmt.all('deleted');
399
- },
400
-
401
- updateConversation(id, data) {
402
- const conv = this.getConversation(id);
403
- if (!conv) return null;
404
-
405
- const now = Date.now();
406
- const title = data.title !== undefined ? data.title : conv.title;
407
- const status = data.status !== undefined ? data.status : conv.status;
408
-
409
- const stmt = prep(
410
- `UPDATE conversations SET title = ?, status = ?, updated_at = ? WHERE id = ?`
411
- );
412
- stmt.run(title, status, now, id);
413
-
414
- return {
415
- ...conv,
416
- title,
417
- status,
418
- updated_at: now
419
- };
420
- },
421
-
422
- setClaudeSessionId(conversationId, claudeSessionId) {
423
- const stmt = prep('UPDATE conversations SET claudeSessionId = ?, updated_at = ? WHERE id = ?');
424
- stmt.run(claudeSessionId, Date.now(), conversationId);
425
- },
426
-
427
- getClaudeSessionId(conversationId) {
428
- const stmt = prep('SELECT claudeSessionId FROM conversations WHERE id = ?');
429
- const row = stmt.get(conversationId);
430
- return row?.claudeSessionId || null;
431
- },
432
-
433
- setIsStreaming(conversationId, isStreaming) {
434
- const stmt = prep('UPDATE conversations SET isStreaming = ?, updated_at = ? WHERE id = ?');
435
- stmt.run(isStreaming ? 1 : 0, Date.now(), conversationId);
436
- },
437
-
438
- getIsStreaming(conversationId) {
439
- const stmt = prep('SELECT isStreaming FROM conversations WHERE id = ?');
440
- const row = stmt.get(conversationId);
441
- return row?.isStreaming === 1;
442
- },
443
-
444
- getStreamingConversations() {
445
- const stmt = prep('SELECT id, title, claudeSessionId, agentType FROM conversations WHERE isStreaming = 1');
446
- return stmt.all();
447
- },
448
-
449
- getResumableConversations() {
450
- const stmt = prep(
451
- "SELECT id, title, claudeSessionId, agentType, workingDirectory FROM conversations WHERE isStreaming = 1 AND claudeSessionId IS NOT NULL AND claudeSessionId != ''"
452
- );
453
- return stmt.all();
454
- },
455
-
456
- clearAllStreamingFlags() {
457
- const stmt = prep('UPDATE conversations SET isStreaming = 0 WHERE isStreaming = 1');
458
- return stmt.run().changes;
459
- },
460
-
461
- markSessionIncomplete(sessionId, errorMsg) {
462
- const stmt = prep('UPDATE sessions SET status = ?, error = ?, completed_at = ? WHERE id = ?');
463
- stmt.run('incomplete', errorMsg || 'unknown', Date.now(), sessionId);
464
- },
465
-
466
- getSessionsProcessingLongerThan(minutes) {
467
- const cutoff = Date.now() - (minutes * 60 * 1000);
468
- const stmt = prep("SELECT * FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
469
- return stmt.all(cutoff);
470
- },
471
-
472
- cleanupOrphanedSessions(days) {
473
- const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
474
- const stmt = prep("DELETE FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
475
- const result = stmt.run(cutoff);
476
- return result.changes || 0;
477
- },
478
-
479
- createMessage(conversationId, role, content, idempotencyKey = null) {
480
- if (idempotencyKey) {
481
- const cached = this.getIdempotencyKey(idempotencyKey);
482
- if (cached) return JSON.parse(cached);
483
- }
484
-
485
- const id = generateId('msg');
486
- const now = Date.now();
487
- const storedContent = typeof content === 'string' ? content : JSON.stringify(content);
488
-
489
- const stmt = prep(
490
- `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
491
- );
492
- stmt.run(id, conversationId, role, storedContent, now);
493
-
494
- const updateConvStmt = prep('UPDATE conversations SET updated_at = ? WHERE id = ?');
495
- updateConvStmt.run(now, conversationId);
496
-
497
- const message = {
498
- id,
499
- conversationId,
500
- role,
501
- content,
502
- created_at: now
503
- };
504
-
505
- if (idempotencyKey) {
506
- this.setIdempotencyKey(idempotencyKey, message);
507
- }
508
-
509
- return message;
510
- },
511
-
512
- getMessage(id) {
513
- const stmt = prep('SELECT * FROM messages WHERE id = ?');
514
- const msg = stmt.get(id);
515
- if (msg && typeof msg.content === 'string') {
516
- try {
517
- msg.content = JSON.parse(msg.content);
518
- } catch (_) {
519
- // If it's not JSON, leave it as string
520
- }
521
- }
522
- return msg;
523
- },
524
-
525
- getConversationMessages(conversationId) {
526
- const stmt = prep(
527
- 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC'
528
- );
529
- const messages = stmt.all(conversationId);
530
- return messages.map(msg => {
531
- if (typeof msg.content === 'string') {
532
- try {
533
- msg.content = JSON.parse(msg.content);
534
- } catch (_) {
535
- // If it's not JSON, leave it as string
536
- }
537
- }
538
- return msg;
539
- });
540
- },
541
-
542
- getLastUserMessage(conversationId) {
543
- const stmt = prep(
544
- "SELECT * FROM messages WHERE conversationId = ? AND role = 'user' ORDER BY created_at DESC LIMIT 1"
545
- );
546
- const msg = stmt.get(conversationId);
547
- if (msg && typeof msg.content === 'string') {
548
- try { msg.content = JSON.parse(msg.content); } catch (_) {}
549
- }
550
- return msg || null;
551
- },
552
-
553
- getPaginatedMessages(conversationId, limit = 50, offset = 0) {
554
- const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
555
- const total = countStmt.get(conversationId).count;
556
-
557
- const stmt = prep(
558
- 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC LIMIT ? OFFSET ?'
559
- );
560
- const messages = stmt.all(conversationId, limit, offset);
561
-
562
- return {
563
- messages: messages.map(msg => {
564
- if (typeof msg.content === 'string') {
565
- try {
566
- msg.content = JSON.parse(msg.content);
567
- } catch (_) {
568
- // If it's not JSON, leave it as string
569
- }
570
- }
571
- return msg;
572
- }),
573
- total,
574
- limit,
575
- offset,
576
- hasMore: offset + limit < total
577
- };
578
- },
579
-
580
- createSession(conversationId) {
581
- const id = generateId('sess');
582
- const now = Date.now();
583
-
584
- const stmt = prep(
585
- `INSERT INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`
586
- );
587
- stmt.run(id, conversationId, 'pending', now, null, null, null);
588
-
589
- return {
590
- id,
591
- conversationId,
592
- status: 'pending',
593
- started_at: now,
594
- completed_at: null,
595
- response: null,
596
- error: null
597
- };
598
- },
599
-
600
- getSession(id) {
601
- const stmt = prep('SELECT * FROM sessions WHERE id = ?');
602
- return stmt.get(id);
603
- },
604
-
605
- getConversationSessions(conversationId) {
606
- const stmt = prep(
607
- 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC'
608
- );
609
- return stmt.all(conversationId);
610
- },
611
-
612
- updateSession(id, data) {
613
- const session = this.getSession(id);
614
- if (!session) return null;
615
-
616
- const status = data.status !== undefined ? data.status : session.status;
617
- const rawResponse = data.response !== undefined ? data.response : session.response;
618
- const response = rawResponse && typeof rawResponse === 'object' ? JSON.stringify(rawResponse) : rawResponse;
619
- const error = data.error !== undefined ? data.error : session.error;
620
- const completed_at = data.completed_at !== undefined ? data.completed_at : session.completed_at;
621
-
622
- const stmt = prep(
623
- `UPDATE sessions SET status = ?, response = ?, error = ?, completed_at = ? WHERE id = ?`
624
- );
625
-
626
- try {
627
- stmt.run(status, response, error, completed_at, id);
628
- return {
629
- ...session,
630
- status,
631
- response,
632
- error,
633
- completed_at
634
- };
635
- } catch (e) {
636
- throw e;
637
- }
638
- },
639
-
640
- getLatestSession(conversationId) {
641
- const stmt = prep(
642
- 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT 1'
643
- );
644
- return stmt.get(conversationId) || null;
645
- },
646
-
647
- getSessionsByStatus(conversationId, status) {
648
- const stmt = prep(
649
- 'SELECT * FROM sessions WHERE conversationId = ? AND status = ? ORDER BY started_at DESC'
650
- );
651
- return stmt.all(conversationId, status);
652
- },
653
-
654
- getActiveSessions() {
655
- const stmt = prep(
656
- "SELECT * FROM sessions WHERE status IN ('active', 'pending') ORDER BY started_at DESC"
657
- );
658
- return stmt.all();
659
- },
660
-
661
- getSessionsByConversation(conversationId, limit = 10, offset = 0) {
662
- const stmt = prep(
663
- 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT ? OFFSET ?'
664
- );
665
- return stmt.all(conversationId, limit, offset);
666
- },
667
-
668
- getAllSessions(limit = 100) {
669
- const stmt = prep(
670
- 'SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?'
671
- );
672
- return stmt.all(limit);
673
- },
674
-
675
- deleteSession(id) {
676
- const stmt = prep('DELETE FROM sessions WHERE id = ?');
677
- const result = stmt.run(id);
678
- prep('DELETE FROM chunks WHERE sessionId = ?').run(id);
679
- prep('DELETE FROM events WHERE sessionId = ?').run(id);
680
- return result.changes || 0;
681
- },
682
-
683
- createEvent(type, data, conversationId = null, sessionId = null) {
684
- const id = generateId('evt');
685
- const now = Date.now();
686
-
687
- const stmt = prep(
688
- `INSERT INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`
689
- );
690
- stmt.run(id, type, conversationId, sessionId, JSON.stringify(data), now);
691
-
692
- return {
693
- id,
694
- type,
695
- conversationId,
696
- sessionId,
697
- data,
698
- created_at: now
699
- };
700
- },
701
-
702
- getEvent(id) {
703
- const stmt = prep('SELECT * FROM events WHERE id = ?');
704
- const row = stmt.get(id);
705
- if (row) {
706
- return {
707
- ...row,
708
- data: JSON.parse(row.data)
709
- };
710
- }
711
- return undefined;
712
- },
713
-
714
- getConversationEvents(conversationId) {
715
- const stmt = prep(
716
- 'SELECT * FROM events WHERE conversationId = ? ORDER BY created_at ASC'
717
- );
718
- const rows = stmt.all(conversationId);
719
- return rows.map(row => ({
720
- ...row,
721
- data: JSON.parse(row.data)
722
- }));
723
- },
724
-
725
- getSessionEvents(sessionId) {
726
- const stmt = prep(
727
- 'SELECT * FROM events WHERE sessionId = ? ORDER BY created_at ASC'
728
- );
729
- const rows = stmt.all(sessionId);
730
- return rows.map(row => ({
731
- ...row,
732
- data: JSON.parse(row.data)
733
- }));
734
- },
735
-
736
- deleteConversation(id) {
737
- const conv = this.getConversation(id);
738
- if (!conv) return false;
739
-
740
- // Delete associated Claude Code session file if it exists
741
- if (conv.claudeSessionId) {
742
- this.deleteClaudeSessionFile(conv.claudeSessionId);
743
- }
744
-
745
- const deleteStmt = db.transaction(() => {
746
- const sessionIds = prep('SELECT id FROM sessions WHERE conversationId = ?').all(id).map(r => r.id);
747
- prep('DELETE FROM stream_updates WHERE conversationId = ?').run(id);
748
- prep('DELETE FROM chunks WHERE conversationId = ?').run(id);
749
- prep('DELETE FROM events WHERE conversationId = ?').run(id);
750
- if (sessionIds.length > 0) {
751
- const placeholders = sessionIds.map(() => '?').join(',');
752
- db.prepare(`DELETE FROM stream_updates WHERE sessionId IN (${placeholders})`).run(...sessionIds);
753
- db.prepare(`DELETE FROM chunks WHERE sessionId IN (${placeholders})`).run(...sessionIds);
754
- db.prepare(`DELETE FROM events WHERE sessionId IN (${placeholders})`).run(...sessionIds);
755
- }
756
- prep('DELETE FROM sessions WHERE conversationId = ?').run(id);
757
- prep('DELETE FROM messages WHERE conversationId = ?').run(id);
758
- prep('DELETE FROM conversations WHERE id = ?').run(id);
759
- });
760
-
761
- deleteStmt();
762
- return true;
763
- },
764
-
765
- deleteClaudeSessionFile(sessionId) {
766
- try {
767
- const claudeDir = path.join(os.homedir(), '.claude');
768
- const projectsDir = path.join(claudeDir, 'projects');
769
-
770
- if (!fs.existsSync(projectsDir)) {
771
- return false;
772
- }
773
-
774
- // Search for session file in all project directories
775
- const projects = fs.readdirSync(projectsDir);
776
- for (const project of projects) {
777
- const projectPath = path.join(projectsDir, project);
778
- const sessionFile = path.join(projectPath, `${sessionId}.jsonl`);
779
-
780
- if (fs.existsSync(sessionFile)) {
781
- fs.unlinkSync(sessionFile);
782
- console.log(`[deleteClaudeSessionFile] Deleted Claude session: ${sessionFile}`);
783
- return true;
784
- }
785
- }
786
-
787
- return false;
788
- } catch (err) {
789
- console.error(`[deleteClaudeSessionFile] Error deleting session ${sessionId}:`, err.message);
790
- return false;
791
- }
792
- },
793
-
794
- cleanup() {
795
- const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
796
- const now = Date.now();
797
-
798
- const cleanupStmt = db.transaction(() => {
799
- prep('DELETE FROM events WHERE created_at < ?').run(thirtyDaysAgo);
800
- prep('DELETE FROM sessions WHERE completed_at IS NOT NULL AND completed_at < ?').run(thirtyDaysAgo);
801
- prep('DELETE FROM idempotencyKeys WHERE (created_at + ttl) < ?').run(now);
802
- });
803
-
804
- cleanupStmt();
805
- },
806
-
807
- setIdempotencyKey(key, value) {
808
- const now = Date.now();
809
- const ttl = 24 * 60 * 60 * 1000;
810
-
811
- const stmt = prep(
812
- 'INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)'
813
- );
814
- stmt.run(key, JSON.stringify(value), now, ttl);
815
- },
816
-
817
- getIdempotencyKey(key) {
818
- const stmt = prep('SELECT * FROM idempotencyKeys WHERE key = ?');
819
- const entry = stmt.get(key);
820
-
821
- if (!entry) return null;
822
-
823
- const isExpired = Date.now() - entry.created_at > entry.ttl;
824
- if (isExpired) {
825
- db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
826
- return null;
827
- }
828
-
829
- return entry.value;
830
- },
831
-
832
- clearIdempotencyKey(key) {
833
- db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
834
- },
835
-
836
- discoverClaudeCodeConversations() {
837
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
838
- if (!fs.existsSync(projectsDir)) return [];
839
-
840
- const discovered = [];
841
- try {
842
- const dirs = fs.readdirSync(projectsDir, { withFileTypes: true });
843
- for (const dir of dirs) {
844
- if (!dir.isDirectory()) continue;
845
- const dirPath = path.join(projectsDir, dir.name);
846
- const indexPath = path.join(dirPath, 'sessions-index.json');
847
- if (!fs.existsSync(indexPath)) continue;
848
-
849
- try {
850
- const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
851
- const projectPath = index.originalPath || dir.name.replace(/^-/, '/').replace(/-/g, '/');
852
- for (const entry of (index.entries || [])) {
853
- if (!entry.sessionId || entry.messageCount === 0) continue;
854
- discovered.push({
855
- id: entry.sessionId,
856
- jsonlPath: entry.fullPath || path.join(dirPath, `${entry.sessionId}.jsonl`),
857
- title: entry.summary || entry.firstPrompt || 'Claude Code Session',
858
- projectPath,
859
- created: entry.created ? new Date(entry.created).getTime() : entry.fileMtime,
860
- modified: entry.modified ? new Date(entry.modified).getTime() : entry.fileMtime,
861
- messageCount: entry.messageCount,
862
- gitBranch: entry.gitBranch,
863
- source: 'claude-code'
864
- });
865
- }
866
- } catch (e) {
867
- console.error(`Error reading index ${indexPath}:`, e.message);
868
- }
869
- }
870
- } catch (e) {
871
- console.error('Error discovering Claude Code conversations:', e.message);
872
- }
873
-
874
- return discovered;
875
- },
876
-
877
- parseJsonlMessages(jsonlPath) {
878
- if (!fs.existsSync(jsonlPath)) return [];
879
- const messages = [];
880
- try {
881
- const lines = fs.readFileSync(jsonlPath, 'utf-8').split('\n');
882
- for (const line of lines) {
883
- if (!line.trim()) continue;
884
- try {
885
- const obj = JSON.parse(line);
886
- if (obj.type === 'user' && obj.message?.content) {
887
- const content = typeof obj.message.content === 'string'
888
- ? obj.message.content
889
- : Array.isArray(obj.message.content)
890
- ? obj.message.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
891
- : JSON.stringify(obj.message.content);
892
- if (content && !content.startsWith('[{"tool_use_id"')) {
893
- messages.push({ id: obj.uuid || generateId('msg'), role: 'user', content, created_at: new Date(obj.timestamp).getTime() });
894
- }
895
- } else if (obj.type === 'assistant' && obj.message?.content) {
896
- let text = '';
897
- const content = obj.message.content;
898
- if (Array.isArray(content)) {
899
- // CRITICAL FIX: Join text blocks with newlines to preserve separation
900
- const textBlocks = [];
901
- for (const c of content) {
902
- if (c.type === 'text' && c.text) {
903
- textBlocks.push(c.text);
904
- }
905
- }
906
- // Join with double newline to preserve logical separation
907
- text = textBlocks.join('\n\n');
908
- } else if (typeof content === 'string') {
909
- text = content;
910
- }
911
- if (text) {
912
- messages.push({ id: obj.uuid || generateId('msg'), role: 'assistant', content: text, created_at: new Date(obj.timestamp).getTime() });
913
- }
914
- }
915
- } catch (_) {}
916
- }
917
- } catch (e) {
918
- console.error(`Error parsing JSONL ${jsonlPath}:`, e.message);
919
- }
920
- return messages;
921
- },
922
-
923
- importClaudeCodeConversations() {
924
- const discovered = this.discoverClaudeCodeConversations();
925
- const imported = [];
926
-
927
- for (const conv of discovered) {
928
- try {
929
- const existingConv = prep('SELECT id, status FROM conversations WHERE id = ?').get(conv.id);
930
- if (existingConv) {
931
- imported.push({ id: conv.id, status: 'skipped', reason: existingConv.status === 'deleted' ? 'deleted' : 'exists' });
932
- continue;
933
- }
934
-
935
- const projectName = conv.projectPath ? path.basename(conv.projectPath) : '';
936
- const title = conv.title || 'Claude Code Session';
937
- const displayTitle = projectName ? `[${projectName}] ${title}` : title;
938
-
939
- const messages = this.parseJsonlMessages(conv.jsonlPath);
940
-
941
- const importStmt = db.transaction(() => {
942
- prep(
943
- `INSERT INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`
944
- ).run(conv.id, 'claude-code', displayTitle, conv.created, conv.modified, 'active');
945
-
946
- for (const msg of messages) {
947
- try {
948
- prep(
949
- `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
950
- ).run(msg.id, conv.id, msg.role, msg.content, msg.created_at);
951
- } catch (_) {}
952
- }
953
- });
954
-
955
- importStmt();
956
- imported.push({ id: conv.id, status: 'imported', title: displayTitle, messages: messages.length });
957
- } catch (e) {
958
- imported.push({ id: conv.id, status: 'error', error: e.message });
959
- }
960
- }
961
-
962
- return imported;
963
- },
964
-
965
- createStreamUpdate(sessionId, conversationId, updateType, content) {
966
- const id = generateId('upd');
967
- const now = Date.now();
968
-
969
- // Use transaction to ensure atomic sequence number assignment
970
- const transaction = db.transaction(() => {
971
- const maxSequence = prep(
972
- 'SELECT MAX(sequence) as max FROM stream_updates WHERE sessionId = ?'
973
- ).get(sessionId);
974
- const sequence = (maxSequence?.max || -1) + 1;
975
-
976
- prep(
977
- `INSERT INTO stream_updates (id, sessionId, conversationId, updateType, content, sequence, created_at)
978
- VALUES (?, ?, ?, ?, ?, ?, ?)`
979
- ).run(id, sessionId, conversationId, updateType, JSON.stringify(content), sequence, now);
980
-
981
- return sequence;
982
- });
983
-
984
- const sequence = transaction();
985
-
986
- return {
987
- id,
988
- sessionId,
989
- conversationId,
990
- updateType,
991
- content,
992
- sequence,
993
- created_at: now
994
- };
995
- },
996
-
997
- getSessionStreamUpdates(sessionId) {
998
- const stmt = prep(
999
- `SELECT id, sessionId, conversationId, updateType, content, sequence, created_at
1000
- FROM stream_updates WHERE sessionId = ? ORDER BY sequence ASC`
1001
- );
1002
- const rows = stmt.all(sessionId);
1003
- return rows.map(row => ({
1004
- ...row,
1005
- content: JSON.parse(row.content)
1006
- }));
1007
- },
1008
-
1009
- clearSessionStreamUpdates(sessionId) {
1010
- const stmt = prep('DELETE FROM stream_updates WHERE sessionId = ?');
1011
- stmt.run(sessionId);
1012
- },
1013
-
1014
- createImportedConversation(data) {
1015
- const id = generateId('conv');
1016
- const now = Date.now();
1017
- const stmt = prep(
1018
- `INSERT INTO conversations (
1019
- id, agentId, title, created_at, updated_at, status,
1020
- agentType, source, externalId, firstPrompt, messageCount,
1021
- projectPath, gitBranch, sourcePath, lastSyncedAt
1022
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1023
- );
1024
- stmt.run(
1025
- id,
1026
- data.externalId || id,
1027
- data.title,
1028
- data.created || now,
1029
- data.modified || now,
1030
- 'active',
1031
- data.agentType || 'claude-code',
1032
- data.source || 'imported',
1033
- data.externalId,
1034
- data.firstPrompt,
1035
- data.messageCount || 0,
1036
- data.projectPath,
1037
- data.gitBranch,
1038
- data.sourcePath,
1039
- now
1040
- );
1041
- return { id, ...data };
1042
- },
1043
-
1044
- getConversationByExternalId(agentType, externalId) {
1045
- const stmt = prep(
1046
- 'SELECT * FROM conversations WHERE agentType = ? AND externalId = ?'
1047
- );
1048
- return stmt.get(agentType, externalId);
1049
- },
1050
-
1051
- getConversationsByAgentType(agentType) {
1052
- const stmt = prep(
1053
- 'SELECT * FROM conversations WHERE agentType = ? AND status != ? ORDER BY updated_at DESC'
1054
- );
1055
- return stmt.all(agentType, 'deleted');
1056
- },
1057
-
1058
- getImportedConversations() {
1059
- const stmt = prep(
1060
- 'SELECT * FROM conversations WHERE source = ? AND status != ? ORDER BY updated_at DESC'
1061
- );
1062
- return stmt.all('imported', 'deleted');
1063
- },
1064
-
1065
- importClaudeCodeConversations() {
1066
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
1067
- if (!fs.existsSync(projectsDir)) return [];
1068
-
1069
- const imported = [];
1070
- const projects = fs.readdirSync(projectsDir);
1071
-
1072
- for (const projectName of projects) {
1073
- const indexPath = path.join(projectsDir, projectName, 'sessions-index.json');
1074
- if (!fs.existsSync(indexPath)) continue;
1075
-
1076
- try {
1077
- const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
1078
- const entries = index.entries || [];
1079
-
1080
- for (const entry of entries) {
1081
- try {
1082
- const existing = this.getConversationByExternalId('claude-code', entry.sessionId);
1083
- if (existing) {
1084
- imported.push({ status: 'skipped', id: existing.id });
1085
- continue;
1086
- }
1087
-
1088
- this.createImportedConversation({
1089
- externalId: entry.sessionId,
1090
- agentType: 'claude-code',
1091
- title: entry.summary || entry.firstPrompt || `Conversation ${entry.sessionId.slice(0, 8)}`,
1092
- firstPrompt: entry.firstPrompt,
1093
- messageCount: entry.messageCount || 0,
1094
- created: new Date(entry.created).getTime(),
1095
- modified: new Date(entry.modified).getTime(),
1096
- projectPath: entry.projectPath,
1097
- gitBranch: entry.gitBranch,
1098
- sourcePath: entry.fullPath,
1099
- source: 'imported'
1100
- });
1101
-
1102
- imported.push({
1103
- status: 'imported',
1104
- id: entry.sessionId,
1105
- title: entry.summary || entry.firstPrompt
1106
- });
1107
- } catch (err) {
1108
- console.error(`[DB] Error importing session ${entry.sessionId}:`, err.message);
1109
- }
1110
- }
1111
- } catch (err) {
1112
- console.error(`[DB] Error reading ${indexPath}:`, err.message);
1113
- }
1114
- }
1115
-
1116
- return imported;
1117
- },
1118
-
1119
- createChunk(sessionId, conversationId, sequence, type, data) {
1120
- const id = generateId('chunk');
1121
- const now = Date.now();
1122
- const dataBlob = typeof data === 'string' ? data : JSON.stringify(data);
1123
-
1124
- const stmt = prep(
1125
- `INSERT INTO chunks (id, sessionId, conversationId, sequence, type, data, created_at)
1126
- VALUES (?, ?, ?, ?, ?, ?, ?)`
1127
- );
1128
- stmt.run(id, sessionId, conversationId, sequence, type, dataBlob, now);
1129
-
1130
- return {
1131
- id,
1132
- sessionId,
1133
- conversationId,
1134
- sequence,
1135
- type,
1136
- data,
1137
- created_at: now
1138
- };
1139
- },
1140
-
1141
- getChunk(id) {
1142
- const stmt = prep(
1143
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at FROM chunks WHERE id = ?`
1144
- );
1145
- const row = stmt.get(id);
1146
- if (!row) return null;
1147
-
1148
- try {
1149
- return {
1150
- ...row,
1151
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1152
- };
1153
- } catch (e) {
1154
- return row;
1155
- }
1156
- },
1157
-
1158
- getSessionChunks(sessionId) {
1159
- const stmt = prep(
1160
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1161
- FROM chunks WHERE sessionId = ? ORDER BY sequence ASC`
1162
- );
1163
- const rows = stmt.all(sessionId);
1164
- return rows.map(row => {
1165
- try {
1166
- return {
1167
- ...row,
1168
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1169
- };
1170
- } catch (e) {
1171
- return row;
1172
- }
1173
- });
1174
- },
1175
-
1176
- getConversationChunkCount(conversationId) {
1177
- const stmt = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ?');
1178
- return stmt.get(conversationId).count;
1179
- },
1180
-
1181
- getConversationChunks(conversationId) {
1182
- const stmt = prep(
1183
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1184
- FROM chunks WHERE conversationId = ? ORDER BY created_at ASC`
1185
- );
1186
- const rows = stmt.all(conversationId);
1187
- return rows.map(row => {
1188
- try {
1189
- return {
1190
- ...row,
1191
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1192
- };
1193
- } catch (e) {
1194
- return row;
1195
- }
1196
- });
1197
- },
1198
-
1199
- getRecentConversationChunks(conversationId, limit) {
1200
- const stmt = prep(
1201
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1202
- FROM chunks WHERE conversationId = ?
1203
- ORDER BY created_at DESC LIMIT ?`
1204
- );
1205
- const rows = stmt.all(conversationId, limit);
1206
- rows.reverse();
1207
- return rows.map(row => {
1208
- try {
1209
- return {
1210
- ...row,
1211
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1212
- };
1213
- } catch (e) {
1214
- return row;
1215
- }
1216
- });
1217
- },
1218
-
1219
- getChunksSince(sessionId, timestamp) {
1220
- const stmt = prep(
1221
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1222
- FROM chunks WHERE sessionId = ? AND created_at > ? ORDER BY sequence ASC`
1223
- );
1224
- const rows = stmt.all(sessionId, timestamp);
1225
- return rows.map(row => {
1226
- try {
1227
- return {
1228
- ...row,
1229
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1230
- };
1231
- } catch (e) {
1232
- return row;
1233
- }
1234
- });
1235
- },
1236
-
1237
- getChunksSinceSeq(sessionId, sinceSeq) {
1238
- const stmt = prep(
1239
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1240
- FROM chunks WHERE sessionId = ? AND sequence > ? ORDER BY sequence ASC`
1241
- );
1242
- const rows = stmt.all(sessionId, sinceSeq);
1243
- return rows.map(row => {
1244
- try {
1245
- return {
1246
- ...row,
1247
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1248
- };
1249
- } catch (e) {
1250
- return row;
1251
- }
1252
- });
1253
- },
1254
-
1255
- deleteSessionChunks(sessionId) {
1256
- const stmt = prep('DELETE FROM chunks WHERE sessionId = ?');
1257
- const result = stmt.run(sessionId);
1258
- return result.changes || 0;
1259
- },
1260
-
1261
- getMaxSequence(sessionId) {
1262
- const stmt = prep('SELECT MAX(sequence) as max FROM chunks WHERE sessionId = ?');
1263
- const result = stmt.get(sessionId);
1264
- return result?.max ?? -1;
1265
- },
1266
-
1267
- getEmptyConversations() {
1268
- const stmt = prep(`
1269
- SELECT c.* FROM conversations c
1270
- LEFT JOIN messages m ON c.id = m.conversationId
1271
- WHERE c.status != 'deleted'
1272
- GROUP BY c.id
1273
- HAVING COUNT(m.id) = 0
1274
- `);
1275
- return stmt.all();
1276
- },
1277
-
1278
- permanentlyDeleteConversation(id) {
1279
- const conv = this.getConversation(id);
1280
- if (!conv) return false;
1281
-
1282
- // Delete associated Claude Code session file if it exists
1283
- if (conv.claudeSessionId) {
1284
- this.deleteClaudeSessionFile(conv.claudeSessionId);
1285
- }
1286
-
1287
- const deleteStmt = db.transaction(() => {
1288
- prep('DELETE FROM stream_updates WHERE conversationId = ?').run(id);
1289
- prep('DELETE FROM chunks WHERE conversationId = ?').run(id);
1290
- prep('DELETE FROM events WHERE conversationId = ?').run(id);
1291
- prep('DELETE FROM sessions WHERE conversationId = ?').run(id);
1292
- prep('DELETE FROM messages WHERE conversationId = ?').run(id);
1293
- prep('DELETE FROM conversations WHERE id = ?').run(id);
1294
- });
1295
-
1296
- deleteStmt();
1297
- return true;
1298
- },
1299
-
1300
- cleanupEmptyConversations() {
1301
- const emptyConvs = this.getEmptyConversations();
1302
- let deletedCount = 0;
1303
-
1304
- for (const conv of emptyConvs) {
1305
- console.log(`[cleanup] Deleting empty conversation: ${conv.id} (${conv.title || 'Untitled'})`);
1306
- if (this.permanentlyDeleteConversation(conv.id)) {
1307
- deletedCount++;
1308
- }
1309
- }
1310
-
1311
- if (deletedCount > 0) {
1312
- console.log(`[cleanup] Deleted ${deletedCount} empty conversation(s)`);
1313
- }
1314
-
1315
- return deletedCount;
1316
- },
1317
-
1318
- recordIpfsCid(cid, modelName, modelType, modelHash, gatewayUrl) {
1319
- const id = generateId('ipfs');
1320
- const now = Date.now();
1321
- const stmt = prep(`
1322
- INSERT INTO ipfs_cids (id, cid, modelName, modelType, modelHash, gatewayUrl, cached_at, last_accessed_at)
1323
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1324
- ON CONFLICT(cid) DO UPDATE SET last_accessed_at = ?, success_count = success_count + 1
1325
- `);
1326
- stmt.run(id, cid, modelName, modelType, modelHash, gatewayUrl, now, now, now);
1327
- const record = this.getIpfsCid(cid);
1328
- return record ? record.id : id;
1329
- },
1330
-
1331
- getIpfsCid(cid) {
1332
- const stmt = prep('SELECT * FROM ipfs_cids WHERE cid = ?');
1333
- return stmt.get(cid);
1334
- },
1335
-
1336
- getIpfsCidByModel(modelName, modelType) {
1337
- const stmt = prep('SELECT * FROM ipfs_cids WHERE modelName = ? AND modelType = ? ORDER BY last_accessed_at DESC LIMIT 1');
1338
- return stmt.get(modelName, modelType);
1339
- },
1340
-
1341
- recordDownloadStart(cidId, downloadPath, totalBytes) {
1342
- const id = generateId('dl');
1343
- const stmt = prep(`
1344
- INSERT INTO ipfs_downloads (id, cidId, downloadPath, status, total_bytes, started_at)
1345
- VALUES (?, ?, ?, ?, ?, ?)
1346
- `);
1347
- stmt.run(id, cidId, downloadPath, 'in_progress', totalBytes, Date.now());
1348
- return id;
1349
- },
1350
-
1351
- updateDownloadProgress(downloadId, downloadedBytes) {
1352
- const stmt = prep(`
1353
- UPDATE ipfs_downloads SET downloaded_bytes = ? WHERE id = ?
1354
- `);
1355
- stmt.run(downloadedBytes, downloadId);
1356
- },
1357
-
1358
- completeDownload(downloadId, cidId) {
1359
- const now = Date.now();
1360
- prep(`
1361
- UPDATE ipfs_downloads SET status = ?, completed_at = ? WHERE id = ?
1362
- `).run('success', now, downloadId);
1363
- prep(`
1364
- UPDATE ipfs_cids SET last_accessed_at = ? WHERE id = ?
1365
- `).run(now, cidId);
1366
- },
1367
-
1368
- recordDownloadError(downloadId, cidId, errorMessage) {
1369
- const now = Date.now();
1370
- prep(`
1371
- UPDATE ipfs_downloads SET status = ?, error_message = ?, completed_at = ? WHERE id = ?
1372
- `).run('failed', errorMessage, now, downloadId);
1373
- prep(`
1374
- UPDATE ipfs_cids SET failure_count = failure_count + 1 WHERE id = ?
1375
- `).run(cidId);
1376
- },
1377
-
1378
- getDownload(downloadId) {
1379
- const stmt = prep('SELECT * FROM ipfs_downloads WHERE id = ?');
1380
- return stmt.get(downloadId);
1381
- },
1382
-
1383
- getDownloadsByCid(cidId) {
1384
- const stmt = prep('SELECT * FROM ipfs_downloads WHERE cidId = ? ORDER BY started_at DESC');
1385
- return stmt.all(cidId);
1386
- },
1387
-
1388
- getDownloadsByStatus(status) {
1389
- const stmt = prep('SELECT * FROM ipfs_downloads WHERE status = ? ORDER BY started_at DESC');
1390
- return stmt.all(status);
1391
- },
1392
-
1393
- updateDownloadResume(downloadId, currentSize, attempts, lastAttempt, status) {
1394
- const stmt = prep(`
1395
- UPDATE ipfs_downloads
1396
- SET downloaded_bytes = ?, attempts = ?, lastAttempt = ?, status = ?
1397
- WHERE id = ?
1398
- `);
1399
- stmt.run(currentSize, attempts, lastAttempt, status, downloadId);
1400
- },
1401
-
1402
- updateDownloadHash(downloadId, hash) {
1403
- const stmt = prep('UPDATE ipfs_downloads SET hash = ? WHERE id = ?');
1404
- stmt.run(hash, downloadId);
1405
- },
1406
-
1407
- markDownloadResuming(downloadId) {
1408
- const stmt = prep('UPDATE ipfs_downloads SET status = ?, lastAttempt = ? WHERE id = ?');
1409
- stmt.run('resuming', Date.now(), downloadId);
1410
- },
1411
-
1412
- markDownloadPaused(downloadId, errorMessage) {
1413
- const stmt = prep('UPDATE ipfs_downloads SET status = ?, error_message = ?, lastAttempt = ? WHERE id = ?');
1414
- stmt.run('paused', errorMessage, Date.now(), downloadId);
1415
- }
1416
- };
1417
-
1418
- export default { queries };
22
+ export const dataDir = getDataDir();
23
+ const dbDir = dataDir;
24
+ const dbFilePath = path.join(dbDir, 'data.db');
25
+ const oldJsonPath = path.join(dbDir, 'data.json');
26
+
27
+ if (!fs.existsSync(dbDir)) {
28
+ fs.mkdirSync(dbDir, { recursive: true });
29
+ }
30
+
31
+ let db;
32
+ try {
33
+ const Database = (await import('bun:sqlite')).default;
34
+ db = new Database(dbFilePath);
35
+ db.run('PRAGMA journal_mode = WAL');
36
+ db.run('PRAGMA foreign_keys = ON');
37
+ db.run('PRAGMA encoding = "UTF-8"');
38
+ db.run('PRAGMA synchronous = NORMAL');
39
+ db.run('PRAGMA cache_size = -64000');
40
+ db.run('PRAGMA mmap_size = 268435456');
41
+ db.run('PRAGMA temp_store = MEMORY');
42
+ } catch (e) {
43
+ try {
44
+ const sqlite3 = require('better-sqlite3');
45
+ db = new sqlite3(dbFilePath);
46
+ db.pragma('journal_mode = WAL');
47
+ db.pragma('foreign_keys = ON');
48
+ db.pragma('encoding = "UTF-8"');
49
+ db.pragma('synchronous = NORMAL');
50
+ db.pragma('cache_size = -64000');
51
+ db.pragma('mmap_size = 268435456');
52
+ db.pragma('temp_store = MEMORY');
53
+ } catch (e2) {
54
+ throw new Error('SQLite database is required. Please run with bun (recommended) or install better-sqlite3: npm install better-sqlite3');
55
+ }
56
+ }
57
+
58
+ function initSchema() {
59
+ // Create table with minimal schema - columns will be added by migration
60
+ db.exec(`
61
+ CREATE TABLE IF NOT EXISTS conversations (
62
+ id TEXT PRIMARY KEY,
63
+ agentId TEXT NOT NULL,
64
+ title TEXT,
65
+ created_at INTEGER NOT NULL,
66
+ updated_at INTEGER NOT NULL,
67
+ status TEXT DEFAULT 'active'
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_conversations_agent ON conversations(agentId);
71
+ CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at DESC);
72
+
73
+ CREATE TABLE IF NOT EXISTS messages (
74
+ id TEXT PRIMARY KEY,
75
+ conversationId TEXT NOT NULL,
76
+ role TEXT NOT NULL,
77
+ content TEXT NOT NULL,
78
+ created_at INTEGER NOT NULL,
79
+ FOREIGN KEY (conversationId) REFERENCES conversations(id)
80
+ );
81
+
82
+ CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversationId);
83
+
84
+ CREATE TABLE IF NOT EXISTS sessions (
85
+ id TEXT PRIMARY KEY,
86
+ conversationId TEXT NOT NULL,
87
+ status TEXT NOT NULL,
88
+ started_at INTEGER NOT NULL,
89
+ completed_at INTEGER,
90
+ response TEXT,
91
+ error TEXT,
92
+ FOREIGN KEY (conversationId) REFERENCES conversations(id)
93
+ );
94
+
95
+ CREATE INDEX IF NOT EXISTS idx_sessions_conversation ON sessions(conversationId);
96
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(conversationId, status);
97
+
98
+ CREATE TABLE IF NOT EXISTS events (
99
+ id TEXT PRIMARY KEY,
100
+ type TEXT NOT NULL,
101
+ conversationId TEXT,
102
+ sessionId TEXT,
103
+ data TEXT NOT NULL,
104
+ created_at INTEGER NOT NULL,
105
+ FOREIGN KEY (conversationId) REFERENCES conversations(id),
106
+ FOREIGN KEY (sessionId) REFERENCES sessions(id)
107
+ );
108
+
109
+ CREATE INDEX IF NOT EXISTS idx_events_conversation ON events(conversationId);
110
+
111
+ CREATE TABLE IF NOT EXISTS idempotencyKeys (
112
+ key TEXT PRIMARY KEY,
113
+ value TEXT NOT NULL,
114
+ created_at INTEGER NOT NULL,
115
+ ttl INTEGER NOT NULL
116
+ );
117
+
118
+ CREATE INDEX IF NOT EXISTS idx_idempotency_created ON idempotencyKeys(created_at);
119
+
120
+ CREATE TABLE IF NOT EXISTS stream_updates (
121
+ id TEXT PRIMARY KEY,
122
+ sessionId TEXT NOT NULL,
123
+ conversationId TEXT NOT NULL,
124
+ updateType TEXT NOT NULL,
125
+ content TEXT NOT NULL,
126
+ sequence INTEGER NOT NULL,
127
+ created_at INTEGER NOT NULL,
128
+ FOREIGN KEY (sessionId) REFERENCES sessions(id),
129
+ FOREIGN KEY (conversationId) REFERENCES conversations(id)
130
+ );
131
+
132
+ CREATE INDEX IF NOT EXISTS idx_stream_updates_session ON stream_updates(sessionId);
133
+ CREATE INDEX IF NOT EXISTS idx_stream_updates_created ON stream_updates(created_at);
134
+
135
+ CREATE TABLE IF NOT EXISTS chunks (
136
+ id TEXT PRIMARY KEY,
137
+ sessionId TEXT NOT NULL,
138
+ conversationId TEXT NOT NULL,
139
+ sequence INTEGER NOT NULL,
140
+ type TEXT NOT NULL,
141
+ data BLOB NOT NULL,
142
+ created_at INTEGER NOT NULL,
143
+ FOREIGN KEY (sessionId) REFERENCES sessions(id),
144
+ FOREIGN KEY (conversationId) REFERENCES conversations(id)
145
+ );
146
+
147
+ CREATE INDEX IF NOT EXISTS idx_chunks_session ON chunks(sessionId, sequence);
148
+ CREATE INDEX IF NOT EXISTS idx_chunks_conversation ON chunks(conversationId, sequence);
149
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_chunks_unique ON chunks(sessionId, sequence);
150
+ CREATE INDEX IF NOT EXISTS idx_chunks_conv_created ON chunks(conversationId, created_at);
151
+ CREATE INDEX IF NOT EXISTS idx_chunks_sess_created ON chunks(sessionId, created_at);
152
+
153
+ CREATE TABLE IF NOT EXISTS ipfs_cids (
154
+ id TEXT PRIMARY KEY,
155
+ cid TEXT NOT NULL UNIQUE,
156
+ modelName TEXT NOT NULL,
157
+ modelType TEXT NOT NULL,
158
+ modelHash TEXT,
159
+ gatewayUrl TEXT,
160
+ cached_at INTEGER NOT NULL,
161
+ last_accessed_at INTEGER NOT NULL,
162
+ success_count INTEGER DEFAULT 0,
163
+ failure_count INTEGER DEFAULT 0
164
+ );
165
+
166
+ CREATE INDEX IF NOT EXISTS idx_ipfs_cids_model ON ipfs_cids(modelName);
167
+ CREATE INDEX IF NOT EXISTS idx_ipfs_cids_type ON ipfs_cids(modelType);
168
+ CREATE INDEX IF NOT EXISTS idx_ipfs_cids_hash ON ipfs_cids(modelHash);
169
+
170
+ CREATE TABLE IF NOT EXISTS ipfs_downloads (
171
+ id TEXT PRIMARY KEY,
172
+ cidId TEXT NOT NULL,
173
+ downloadPath TEXT NOT NULL,
174
+ status TEXT DEFAULT 'pending',
175
+ downloaded_bytes INTEGER DEFAULT 0,
176
+ total_bytes INTEGER,
177
+ error_message TEXT,
178
+ started_at INTEGER NOT NULL,
179
+ completed_at INTEGER,
180
+ FOREIGN KEY (cidId) REFERENCES ipfs_cids(id)
181
+ );
182
+
183
+ CREATE INDEX IF NOT EXISTS idx_ipfs_downloads_cid ON ipfs_downloads(cidId);
184
+ CREATE INDEX IF NOT EXISTS idx_ipfs_downloads_status ON ipfs_downloads(status);
185
+ CREATE INDEX IF NOT EXISTS idx_ipfs_downloads_started ON ipfs_downloads(started_at);
186
+ `);
187
+ }
188
+
189
+ function migrateFromJson() {
190
+ if (!fs.existsSync(oldJsonPath)) return;
191
+
192
+ try {
193
+ const content = fs.readFileSync(oldJsonPath, 'utf-8');
194
+ const data = JSON.parse(content);
195
+
196
+ const migrationStmt = db.transaction(() => {
197
+ if (data.conversations) {
198
+ for (const id in data.conversations) {
199
+ const conv = data.conversations[id];
200
+ db.prepare(
201
+ `INSERT OR REPLACE INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`
202
+ ).run(conv.id, conv.agentId, conv.title || null, conv.created_at, conv.updated_at, conv.status || 'active');
203
+ }
204
+ }
205
+
206
+ if (data.messages) {
207
+ for (const id in data.messages) {
208
+ const msg = data.messages[id];
209
+ // Ensure content is always a string (stringify objects)
210
+ const contentStr = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
211
+ db.prepare(
212
+ `INSERT OR REPLACE INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
213
+ ).run(msg.id, msg.conversationId, msg.role, contentStr, msg.created_at);
214
+ }
215
+ }
216
+
217
+ if (data.sessions) {
218
+ for (const id in data.sessions) {
219
+ const sess = data.sessions[id];
220
+ // Ensure response and error are strings, not objects
221
+ const responseStr = sess.response ? (typeof sess.response === 'string' ? sess.response : JSON.stringify(sess.response)) : null;
222
+ const errorStr = sess.error ? (typeof sess.error === 'string' ? sess.error : JSON.stringify(sess.error)) : null;
223
+ db.prepare(
224
+ `INSERT OR REPLACE INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`
225
+ ).run(sess.id, sess.conversationId, sess.status, sess.started_at, sess.completed_at || null, responseStr, errorStr);
226
+ }
227
+ }
228
+
229
+ if (data.events) {
230
+ for (const id in data.events) {
231
+ const evt = data.events[id];
232
+ // Ensure data is always valid JSON string
233
+ const dataStr = typeof evt.data === 'string' ? evt.data : JSON.stringify(evt.data || {});
234
+ db.prepare(
235
+ `INSERT OR REPLACE INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`
236
+ ).run(evt.id, evt.type, evt.conversationId || null, evt.sessionId || null, dataStr, evt.created_at);
237
+ }
238
+ }
239
+
240
+ if (data.idempotencyKeys) {
241
+ for (const key in data.idempotencyKeys) {
242
+ const entry = data.idempotencyKeys[key];
243
+ // Ensure value is always valid JSON string
244
+ const valueStr = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value || {});
245
+ // Ensure ttl is a number
246
+ const ttl = typeof entry.ttl === 'number' ? entry.ttl : (entry.ttl ? parseInt(entry.ttl) : null);
247
+ db.prepare(
248
+ `INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)`
249
+ ).run(key, valueStr, entry.created_at, ttl);
250
+ }
251
+ }
252
+ });
253
+
254
+ migrationStmt();
255
+ fs.renameSync(oldJsonPath, `${oldJsonPath}.migrated`);
256
+ console.log('Migrated data from JSON to SQLite');
257
+ } catch (e) {
258
+ console.error('Error during migration:', e.message);
259
+ }
260
+ }
261
+
262
+ initSchema();
263
+ migrateFromJson();
264
+
265
+ // Migration: Add imported conversation columns if they don't exist
266
+ try {
267
+ const result = db.prepare("PRAGMA table_info(conversations)").all();
268
+ const columnNames = result.map(r => r.name);
269
+ const requiredColumns = {
270
+ agentType: 'TEXT DEFAULT "claude-code"',
271
+ source: 'TEXT DEFAULT "gui"',
272
+ externalId: 'TEXT',
273
+ firstPrompt: 'TEXT',
274
+ messageCount: 'INTEGER DEFAULT 0',
275
+ projectPath: 'TEXT',
276
+ gitBranch: 'TEXT',
277
+ sourcePath: 'TEXT',
278
+ lastSyncedAt: 'INTEGER',
279
+ workingDirectory: 'TEXT',
280
+ claudeSessionId: 'TEXT',
281
+ isStreaming: 'INTEGER DEFAULT 0',
282
+ model: 'TEXT'
283
+ };
284
+
285
+ let addedColumns = false;
286
+ for (const [colName, colDef] of Object.entries(requiredColumns)) {
287
+ if (!columnNames.includes(colName)) {
288
+ db.exec(`ALTER TABLE conversations ADD COLUMN ${colName} ${colDef}`);
289
+ console.log(`[Migration] Added column ${colName} to conversations table`);
290
+ addedColumns = true;
291
+ }
292
+ }
293
+
294
+ // Add indexes for new columns
295
+ if (addedColumns) {
296
+ try {
297
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_external ON conversations(externalId)`);
298
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_agent_type ON conversations(agentType)`);
299
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_source ON conversations(source)`);
300
+ } catch (e) {
301
+ console.warn('[Migration] Index creation warning:', e.message);
302
+ }
303
+ }
304
+ } catch (err) {
305
+ console.error('[Migration] Error:', err.message);
306
+ }
307
+
308
+ // Migration: Add resume capability columns to ipfs_downloads if needed
309
+ try {
310
+ const result = db.prepare("PRAGMA table_info(ipfs_downloads)").all();
311
+ const columnNames = result.map(r => r.name);
312
+ const resumeColumns = {
313
+ attempts: 'INTEGER DEFAULT 0',
314
+ lastAttempt: 'INTEGER',
315
+ currentSize: 'INTEGER DEFAULT 0',
316
+ hash: 'TEXT'
317
+ };
318
+
319
+ for (const [colName, colDef] of Object.entries(resumeColumns)) {
320
+ if (!columnNames.includes(colName)) {
321
+ db.exec(`ALTER TABLE ipfs_downloads ADD COLUMN ${colName} ${colDef}`);
322
+ console.log(`[Migration] Added column ${colName} to ipfs_downloads table`);
323
+ }
324
+ }
325
+ } catch (err) {
326
+ console.error('[Migration] IPFS schema update warning:', err.message);
327
+ }
328
+
329
+ // Register official IPFS CIDs for voice models
330
+ try {
331
+ const LIGHTHOUSE_GATEWAY = 'https://gateway.lighthouse.storage/ipfs';
332
+ const WHISPER_CID = 'bafybeidyw252ecy4vs46bbmezrtw325gl2ymdltosmzqgx4edjsc3fbofy';
333
+ const TTS_CID = 'bafybeidyw252ecy4vs46bbmezrtw325gl2ymdltosmzqgx4edjsc3fbofy';
334
+
335
+ // Check if CIDs are already registered
336
+ const existingWhisper = db.prepare('SELECT * FROM ipfs_cids WHERE modelName = ? AND modelType = ?').get('whisper-base', 'stt');
337
+ if (!existingWhisper) {
338
+ const cidId = `cid-${Date.now()}-whisper`;
339
+ db.prepare(
340
+ `INSERT INTO ipfs_cids (id, cid, modelName, modelType, modelHash, gatewayUrl, cached_at, last_accessed_at)
341
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
342
+ ).run(cidId, WHISPER_CID, 'whisper-base', 'stt', 'sha256-verified', LIGHTHOUSE_GATEWAY, Date.now(), Date.now());
343
+ console.log('[MODELS] Registered Whisper STT IPFS CID:', WHISPER_CID);
344
+ }
345
+
346
+ const existingTTS = db.prepare('SELECT * FROM ipfs_cids WHERE modelName = ? AND modelType = ?').get('tts', 'voice');
347
+ if (!existingTTS) {
348
+ const cidId = `cid-${Date.now()}-tts`;
349
+ db.prepare(
350
+ `INSERT INTO ipfs_cids (id, cid, modelName, modelType, modelHash, gatewayUrl, cached_at, last_accessed_at)
351
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
352
+ ).run(cidId, TTS_CID, 'tts', 'voice', 'sha256-verified', LIGHTHOUSE_GATEWAY, Date.now(), Date.now());
353
+ console.log('[MODELS] Registered TTS IPFS CID:', TTS_CID);
354
+ }
355
+ } catch (err) {
356
+ console.warn('[MODELS] IPFS CID registration warning:', err.message);
357
+ }
358
+
359
+ const stmtCache = new Map();
360
+ function prep(sql) {
361
+ let s = stmtCache.get(sql);
362
+ if (!s) {
363
+ s = db.prepare(sql);
364
+ stmtCache.set(sql, s);
365
+ }
366
+ return s;
367
+ }
368
+
369
+ function generateId(prefix) {
370
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
371
+ }
372
+
373
+ export const queries = {
374
+ _db: db,
375
+ createConversation(agentType, title = null, workingDirectory = null, model = null) {
376
+ const id = generateId('conv');
377
+ const now = Date.now();
378
+ const stmt = prep(
379
+ `INSERT INTO conversations (id, agentId, agentType, title, created_at, updated_at, status, workingDirectory, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
380
+ );
381
+ stmt.run(id, agentType, agentType, title, now, now, 'active', workingDirectory, model);
382
+
383
+ return {
384
+ id,
385
+ agentType,
386
+ title,
387
+ workingDirectory,
388
+ model,
389
+ created_at: now,
390
+ updated_at: now,
391
+ status: 'active'
392
+ };
393
+ },
394
+
395
+ getConversation(id) {
396
+ const stmt = prep('SELECT * FROM conversations WHERE id = ?');
397
+ return stmt.get(id);
398
+ },
399
+
400
+ getAllConversations() {
401
+ const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC');
402
+ return stmt.all('deleted');
403
+ },
404
+
405
+ getConversationsList() {
406
+ const stmt = prep(
407
+ 'SELECT id, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model FROM conversations WHERE status != ? ORDER BY updated_at DESC'
408
+ );
409
+ return stmt.all('deleted');
410
+ },
411
+
412
+ getConversations() {
413
+ const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC');
414
+ return stmt.all('deleted');
415
+ },
416
+
417
+ updateConversation(id, data) {
418
+ const conv = this.getConversation(id);
419
+ if (!conv) return null;
420
+
421
+ const now = Date.now();
422
+ const title = data.title !== undefined ? data.title : conv.title;
423
+ const status = data.status !== undefined ? data.status : conv.status;
424
+
425
+ const stmt = prep(
426
+ `UPDATE conversations SET title = ?, status = ?, updated_at = ? WHERE id = ?`
427
+ );
428
+ stmt.run(title, status, now, id);
429
+
430
+ return {
431
+ ...conv,
432
+ title,
433
+ status,
434
+ updated_at: now
435
+ };
436
+ },
437
+
438
+ setClaudeSessionId(conversationId, claudeSessionId) {
439
+ const stmt = prep('UPDATE conversations SET claudeSessionId = ?, updated_at = ? WHERE id = ?');
440
+ stmt.run(claudeSessionId, Date.now(), conversationId);
441
+ },
442
+
443
+ getClaudeSessionId(conversationId) {
444
+ const stmt = prep('SELECT claudeSessionId FROM conversations WHERE id = ?');
445
+ const row = stmt.get(conversationId);
446
+ return row?.claudeSessionId || null;
447
+ },
448
+
449
+ setIsStreaming(conversationId, isStreaming) {
450
+ const stmt = prep('UPDATE conversations SET isStreaming = ?, updated_at = ? WHERE id = ?');
451
+ stmt.run(isStreaming ? 1 : 0, Date.now(), conversationId);
452
+ },
453
+
454
+ getIsStreaming(conversationId) {
455
+ const stmt = prep('SELECT isStreaming FROM conversations WHERE id = ?');
456
+ const row = stmt.get(conversationId);
457
+ return row?.isStreaming === 1;
458
+ },
459
+
460
+ getStreamingConversations() {
461
+ const stmt = prep('SELECT id, title, claudeSessionId, agentType FROM conversations WHERE isStreaming = 1');
462
+ return stmt.all();
463
+ },
464
+
465
+ getResumableConversations() {
466
+ const stmt = prep(
467
+ "SELECT id, title, claudeSessionId, agentType, workingDirectory FROM conversations WHERE isStreaming = 1 AND claudeSessionId IS NOT NULL AND claudeSessionId != ''"
468
+ );
469
+ return stmt.all();
470
+ },
471
+
472
+ clearAllStreamingFlags() {
473
+ const stmt = prep('UPDATE conversations SET isStreaming = 0 WHERE isStreaming = 1');
474
+ return stmt.run().changes;
475
+ },
476
+
477
+ markSessionIncomplete(sessionId, errorMsg) {
478
+ const stmt = prep('UPDATE sessions SET status = ?, error = ?, completed_at = ? WHERE id = ?');
479
+ stmt.run('incomplete', errorMsg || 'unknown', Date.now(), sessionId);
480
+ },
481
+
482
+ getSessionsProcessingLongerThan(minutes) {
483
+ const cutoff = Date.now() - (minutes * 60 * 1000);
484
+ const stmt = prep("SELECT * FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
485
+ return stmt.all(cutoff);
486
+ },
487
+
488
+ cleanupOrphanedSessions(days) {
489
+ const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
490
+ const stmt = prep("DELETE FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
491
+ const result = stmt.run(cutoff);
492
+ return result.changes || 0;
493
+ },
494
+
495
+ createMessage(conversationId, role, content, idempotencyKey = null) {
496
+ if (idempotencyKey) {
497
+ const cached = this.getIdempotencyKey(idempotencyKey);
498
+ if (cached) return JSON.parse(cached);
499
+ }
500
+
501
+ const id = generateId('msg');
502
+ const now = Date.now();
503
+ const storedContent = typeof content === 'string' ? content : JSON.stringify(content);
504
+
505
+ const stmt = prep(
506
+ `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
507
+ );
508
+ stmt.run(id, conversationId, role, storedContent, now);
509
+
510
+ const updateConvStmt = prep('UPDATE conversations SET updated_at = ? WHERE id = ?');
511
+ updateConvStmt.run(now, conversationId);
512
+
513
+ const message = {
514
+ id,
515
+ conversationId,
516
+ role,
517
+ content,
518
+ created_at: now
519
+ };
520
+
521
+ if (idempotencyKey) {
522
+ this.setIdempotencyKey(idempotencyKey, message);
523
+ }
524
+
525
+ return message;
526
+ },
527
+
528
+ getMessage(id) {
529
+ const stmt = prep('SELECT * FROM messages WHERE id = ?');
530
+ const msg = stmt.get(id);
531
+ if (msg && typeof msg.content === 'string') {
532
+ try {
533
+ msg.content = JSON.parse(msg.content);
534
+ } catch (_) {
535
+ // If it's not JSON, leave it as string
536
+ }
537
+ }
538
+ return msg;
539
+ },
540
+
541
+ getConversationMessages(conversationId) {
542
+ const stmt = prep(
543
+ 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC'
544
+ );
545
+ const messages = stmt.all(conversationId);
546
+ return messages.map(msg => {
547
+ if (typeof msg.content === 'string') {
548
+ try {
549
+ msg.content = JSON.parse(msg.content);
550
+ } catch (_) {
551
+ // If it's not JSON, leave it as string
552
+ }
553
+ }
554
+ return msg;
555
+ });
556
+ },
557
+
558
+ getLastUserMessage(conversationId) {
559
+ const stmt = prep(
560
+ "SELECT * FROM messages WHERE conversationId = ? AND role = 'user' ORDER BY created_at DESC LIMIT 1"
561
+ );
562
+ const msg = stmt.get(conversationId);
563
+ if (msg && typeof msg.content === 'string') {
564
+ try { msg.content = JSON.parse(msg.content); } catch (_) {}
565
+ }
566
+ return msg || null;
567
+ },
568
+
569
+ getPaginatedMessages(conversationId, limit = 50, offset = 0) {
570
+ const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
571
+ const total = countStmt.get(conversationId).count;
572
+
573
+ const stmt = prep(
574
+ 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC LIMIT ? OFFSET ?'
575
+ );
576
+ const messages = stmt.all(conversationId, limit, offset);
577
+
578
+ return {
579
+ messages: messages.map(msg => {
580
+ if (typeof msg.content === 'string') {
581
+ try {
582
+ msg.content = JSON.parse(msg.content);
583
+ } catch (_) {
584
+ // If it's not JSON, leave it as string
585
+ }
586
+ }
587
+ return msg;
588
+ }),
589
+ total,
590
+ limit,
591
+ offset,
592
+ hasMore: offset + limit < total
593
+ };
594
+ },
595
+
596
+ createSession(conversationId) {
597
+ const id = generateId('sess');
598
+ const now = Date.now();
599
+
600
+ const stmt = prep(
601
+ `INSERT INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`
602
+ );
603
+ stmt.run(id, conversationId, 'pending', now, null, null, null);
604
+
605
+ return {
606
+ id,
607
+ conversationId,
608
+ status: 'pending',
609
+ started_at: now,
610
+ completed_at: null,
611
+ response: null,
612
+ error: null
613
+ };
614
+ },
615
+
616
+ getSession(id) {
617
+ const stmt = prep('SELECT * FROM sessions WHERE id = ?');
618
+ return stmt.get(id);
619
+ },
620
+
621
+ getConversationSessions(conversationId) {
622
+ const stmt = prep(
623
+ 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC'
624
+ );
625
+ return stmt.all(conversationId);
626
+ },
627
+
628
+ updateSession(id, data) {
629
+ const session = this.getSession(id);
630
+ if (!session) return null;
631
+
632
+ const status = data.status !== undefined ? data.status : session.status;
633
+ const rawResponse = data.response !== undefined ? data.response : session.response;
634
+ const response = rawResponse && typeof rawResponse === 'object' ? JSON.stringify(rawResponse) : rawResponse;
635
+ const error = data.error !== undefined ? data.error : session.error;
636
+ const completed_at = data.completed_at !== undefined ? data.completed_at : session.completed_at;
637
+
638
+ const stmt = prep(
639
+ `UPDATE sessions SET status = ?, response = ?, error = ?, completed_at = ? WHERE id = ?`
640
+ );
641
+
642
+ try {
643
+ stmt.run(status, response, error, completed_at, id);
644
+ return {
645
+ ...session,
646
+ status,
647
+ response,
648
+ error,
649
+ completed_at
650
+ };
651
+ } catch (e) {
652
+ throw e;
653
+ }
654
+ },
655
+
656
+ getLatestSession(conversationId) {
657
+ const stmt = prep(
658
+ 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT 1'
659
+ );
660
+ return stmt.get(conversationId) || null;
661
+ },
662
+
663
+ getSessionsByStatus(conversationId, status) {
664
+ const stmt = prep(
665
+ 'SELECT * FROM sessions WHERE conversationId = ? AND status = ? ORDER BY started_at DESC'
666
+ );
667
+ return stmt.all(conversationId, status);
668
+ },
669
+
670
+ getActiveSessions() {
671
+ const stmt = prep(
672
+ "SELECT * FROM sessions WHERE status IN ('active', 'pending') ORDER BY started_at DESC"
673
+ );
674
+ return stmt.all();
675
+ },
676
+
677
+ getSessionsByConversation(conversationId, limit = 10, offset = 0) {
678
+ const stmt = prep(
679
+ 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT ? OFFSET ?'
680
+ );
681
+ return stmt.all(conversationId, limit, offset);
682
+ },
683
+
684
+ getAllSessions(limit = 100) {
685
+ const stmt = prep(
686
+ 'SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?'
687
+ );
688
+ return stmt.all(limit);
689
+ },
690
+
691
+ deleteSession(id) {
692
+ const stmt = prep('DELETE FROM sessions WHERE id = ?');
693
+ const result = stmt.run(id);
694
+ prep('DELETE FROM chunks WHERE sessionId = ?').run(id);
695
+ prep('DELETE FROM events WHERE sessionId = ?').run(id);
696
+ return result.changes || 0;
697
+ },
698
+
699
+ createEvent(type, data, conversationId = null, sessionId = null) {
700
+ const id = generateId('evt');
701
+ const now = Date.now();
702
+
703
+ const stmt = prep(
704
+ `INSERT INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`
705
+ );
706
+ stmt.run(id, type, conversationId, sessionId, JSON.stringify(data), now);
707
+
708
+ return {
709
+ id,
710
+ type,
711
+ conversationId,
712
+ sessionId,
713
+ data,
714
+ created_at: now
715
+ };
716
+ },
717
+
718
+ getEvent(id) {
719
+ const stmt = prep('SELECT * FROM events WHERE id = ?');
720
+ const row = stmt.get(id);
721
+ if (row) {
722
+ return {
723
+ ...row,
724
+ data: JSON.parse(row.data)
725
+ };
726
+ }
727
+ return undefined;
728
+ },
729
+
730
+ getConversationEvents(conversationId) {
731
+ const stmt = prep(
732
+ 'SELECT * FROM events WHERE conversationId = ? ORDER BY created_at ASC'
733
+ );
734
+ const rows = stmt.all(conversationId);
735
+ return rows.map(row => ({
736
+ ...row,
737
+ data: JSON.parse(row.data)
738
+ }));
739
+ },
740
+
741
+ getSessionEvents(sessionId) {
742
+ const stmt = prep(
743
+ 'SELECT * FROM events WHERE sessionId = ? ORDER BY created_at ASC'
744
+ );
745
+ const rows = stmt.all(sessionId);
746
+ return rows.map(row => ({
747
+ ...row,
748
+ data: JSON.parse(row.data)
749
+ }));
750
+ },
751
+
752
+ deleteConversation(id) {
753
+ const conv = this.getConversation(id);
754
+ if (!conv) return false;
755
+
756
+ // Delete associated Claude Code session file if it exists
757
+ if (conv.claudeSessionId) {
758
+ this.deleteClaudeSessionFile(conv.claudeSessionId);
759
+ }
760
+
761
+ const deleteStmt = db.transaction(() => {
762
+ const sessionIds = prep('SELECT id FROM sessions WHERE conversationId = ?').all(id).map(r => r.id);
763
+ prep('DELETE FROM stream_updates WHERE conversationId = ?').run(id);
764
+ prep('DELETE FROM chunks WHERE conversationId = ?').run(id);
765
+ prep('DELETE FROM events WHERE conversationId = ?').run(id);
766
+ if (sessionIds.length > 0) {
767
+ const placeholders = sessionIds.map(() => '?').join(',');
768
+ db.prepare(`DELETE FROM stream_updates WHERE sessionId IN (${placeholders})`).run(...sessionIds);
769
+ db.prepare(`DELETE FROM chunks WHERE sessionId IN (${placeholders})`).run(...sessionIds);
770
+ db.prepare(`DELETE FROM events WHERE sessionId IN (${placeholders})`).run(...sessionIds);
771
+ }
772
+ prep('DELETE FROM sessions WHERE conversationId = ?').run(id);
773
+ prep('DELETE FROM messages WHERE conversationId = ?').run(id);
774
+ prep('DELETE FROM conversations WHERE id = ?').run(id);
775
+ });
776
+
777
+ deleteStmt();
778
+ return true;
779
+ },
780
+
781
+ deleteClaudeSessionFile(sessionId) {
782
+ try {
783
+ const claudeDir = path.join(os.homedir(), '.claude');
784
+ const projectsDir = path.join(claudeDir, 'projects');
785
+
786
+ if (!fs.existsSync(projectsDir)) {
787
+ return false;
788
+ }
789
+
790
+ // Search for session file in all project directories
791
+ const projects = fs.readdirSync(projectsDir);
792
+ for (const project of projects) {
793
+ const projectPath = path.join(projectsDir, project);
794
+ const sessionFile = path.join(projectPath, `${sessionId}.jsonl`);
795
+
796
+ if (fs.existsSync(sessionFile)) {
797
+ fs.unlinkSync(sessionFile);
798
+ console.log(`[deleteClaudeSessionFile] Deleted Claude session: ${sessionFile}`);
799
+ return true;
800
+ }
801
+ }
802
+
803
+ return false;
804
+ } catch (err) {
805
+ console.error(`[deleteClaudeSessionFile] Error deleting session ${sessionId}:`, err.message);
806
+ return false;
807
+ }
808
+ },
809
+
810
+ cleanup() {
811
+ const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
812
+ const now = Date.now();
813
+
814
+ const cleanupStmt = db.transaction(() => {
815
+ prep('DELETE FROM events WHERE created_at < ?').run(thirtyDaysAgo);
816
+ prep('DELETE FROM sessions WHERE completed_at IS NOT NULL AND completed_at < ?').run(thirtyDaysAgo);
817
+ prep('DELETE FROM idempotencyKeys WHERE (created_at + ttl) < ?').run(now);
818
+ });
819
+
820
+ cleanupStmt();
821
+ },
822
+
823
+ setIdempotencyKey(key, value) {
824
+ const now = Date.now();
825
+ const ttl = 24 * 60 * 60 * 1000;
826
+
827
+ const stmt = prep(
828
+ 'INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)'
829
+ );
830
+ stmt.run(key, JSON.stringify(value), now, ttl);
831
+ },
832
+
833
+ getIdempotencyKey(key) {
834
+ const stmt = prep('SELECT * FROM idempotencyKeys WHERE key = ?');
835
+ const entry = stmt.get(key);
836
+
837
+ if (!entry) return null;
838
+
839
+ const isExpired = Date.now() - entry.created_at > entry.ttl;
840
+ if (isExpired) {
841
+ db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
842
+ return null;
843
+ }
844
+
845
+ return entry.value;
846
+ },
847
+
848
+ clearIdempotencyKey(key) {
849
+ db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
850
+ },
851
+
852
+ discoverClaudeCodeConversations() {
853
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
854
+ if (!fs.existsSync(projectsDir)) return [];
855
+
856
+ const discovered = [];
857
+ try {
858
+ const dirs = fs.readdirSync(projectsDir, { withFileTypes: true });
859
+ for (const dir of dirs) {
860
+ if (!dir.isDirectory()) continue;
861
+ const dirPath = path.join(projectsDir, dir.name);
862
+ const indexPath = path.join(dirPath, 'sessions-index.json');
863
+ if (!fs.existsSync(indexPath)) continue;
864
+
865
+ try {
866
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
867
+ const projectPath = index.originalPath || dir.name.replace(/^-/, '/').replace(/-/g, '/');
868
+ for (const entry of (index.entries || [])) {
869
+ if (!entry.sessionId || entry.messageCount === 0) continue;
870
+ discovered.push({
871
+ id: entry.sessionId,
872
+ jsonlPath: entry.fullPath || path.join(dirPath, `${entry.sessionId}.jsonl`),
873
+ title: entry.summary || entry.firstPrompt || 'Claude Code Session',
874
+ projectPath,
875
+ created: entry.created ? new Date(entry.created).getTime() : entry.fileMtime,
876
+ modified: entry.modified ? new Date(entry.modified).getTime() : entry.fileMtime,
877
+ messageCount: entry.messageCount,
878
+ gitBranch: entry.gitBranch,
879
+ source: 'claude-code'
880
+ });
881
+ }
882
+ } catch (e) {
883
+ console.error(`Error reading index ${indexPath}:`, e.message);
884
+ }
885
+ }
886
+ } catch (e) {
887
+ console.error('Error discovering Claude Code conversations:', e.message);
888
+ }
889
+
890
+ return discovered;
891
+ },
892
+
893
+ parseJsonlMessages(jsonlPath) {
894
+ if (!fs.existsSync(jsonlPath)) return [];
895
+ const messages = [];
896
+ try {
897
+ const lines = fs.readFileSync(jsonlPath, 'utf-8').split('\n');
898
+ for (const line of lines) {
899
+ if (!line.trim()) continue;
900
+ try {
901
+ const obj = JSON.parse(line);
902
+ if (obj.type === 'user' && obj.message?.content) {
903
+ const content = typeof obj.message.content === 'string'
904
+ ? obj.message.content
905
+ : Array.isArray(obj.message.content)
906
+ ? obj.message.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
907
+ : JSON.stringify(obj.message.content);
908
+ if (content && !content.startsWith('[{"tool_use_id"')) {
909
+ messages.push({ id: obj.uuid || generateId('msg'), role: 'user', content, created_at: new Date(obj.timestamp).getTime() });
910
+ }
911
+ } else if (obj.type === 'assistant' && obj.message?.content) {
912
+ let text = '';
913
+ const content = obj.message.content;
914
+ if (Array.isArray(content)) {
915
+ // CRITICAL FIX: Join text blocks with newlines to preserve separation
916
+ const textBlocks = [];
917
+ for (const c of content) {
918
+ if (c.type === 'text' && c.text) {
919
+ textBlocks.push(c.text);
920
+ }
921
+ }
922
+ // Join with double newline to preserve logical separation
923
+ text = textBlocks.join('\n\n');
924
+ } else if (typeof content === 'string') {
925
+ text = content;
926
+ }
927
+ if (text) {
928
+ messages.push({ id: obj.uuid || generateId('msg'), role: 'assistant', content: text, created_at: new Date(obj.timestamp).getTime() });
929
+ }
930
+ }
931
+ } catch (_) {}
932
+ }
933
+ } catch (e) {
934
+ console.error(`Error parsing JSONL ${jsonlPath}:`, e.message);
935
+ }
936
+ return messages;
937
+ },
938
+
939
+ importClaudeCodeConversations() {
940
+ const discovered = this.discoverClaudeCodeConversations();
941
+ const imported = [];
942
+
943
+ for (const conv of discovered) {
944
+ try {
945
+ const existingConv = prep('SELECT id, status FROM conversations WHERE id = ?').get(conv.id);
946
+ if (existingConv) {
947
+ imported.push({ id: conv.id, status: 'skipped', reason: existingConv.status === 'deleted' ? 'deleted' : 'exists' });
948
+ continue;
949
+ }
950
+
951
+ const projectName = conv.projectPath ? path.basename(conv.projectPath) : '';
952
+ const title = conv.title || 'Claude Code Session';
953
+ const displayTitle = projectName ? `[${projectName}] ${title}` : title;
954
+
955
+ const messages = this.parseJsonlMessages(conv.jsonlPath);
956
+
957
+ const importStmt = db.transaction(() => {
958
+ prep(
959
+ `INSERT INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`
960
+ ).run(conv.id, 'claude-code', displayTitle, conv.created, conv.modified, 'active');
961
+
962
+ for (const msg of messages) {
963
+ try {
964
+ prep(
965
+ `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
966
+ ).run(msg.id, conv.id, msg.role, msg.content, msg.created_at);
967
+ } catch (_) {}
968
+ }
969
+ });
970
+
971
+ importStmt();
972
+ imported.push({ id: conv.id, status: 'imported', title: displayTitle, messages: messages.length });
973
+ } catch (e) {
974
+ imported.push({ id: conv.id, status: 'error', error: e.message });
975
+ }
976
+ }
977
+
978
+ return imported;
979
+ },
980
+
981
+ createStreamUpdate(sessionId, conversationId, updateType, content) {
982
+ const id = generateId('upd');
983
+ const now = Date.now();
984
+
985
+ // Use transaction to ensure atomic sequence number assignment
986
+ const transaction = db.transaction(() => {
987
+ const maxSequence = prep(
988
+ 'SELECT MAX(sequence) as max FROM stream_updates WHERE sessionId = ?'
989
+ ).get(sessionId);
990
+ const sequence = (maxSequence?.max || -1) + 1;
991
+
992
+ prep(
993
+ `INSERT INTO stream_updates (id, sessionId, conversationId, updateType, content, sequence, created_at)
994
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
995
+ ).run(id, sessionId, conversationId, updateType, JSON.stringify(content), sequence, now);
996
+
997
+ return sequence;
998
+ });
999
+
1000
+ const sequence = transaction();
1001
+
1002
+ return {
1003
+ id,
1004
+ sessionId,
1005
+ conversationId,
1006
+ updateType,
1007
+ content,
1008
+ sequence,
1009
+ created_at: now
1010
+ };
1011
+ },
1012
+
1013
+ getSessionStreamUpdates(sessionId) {
1014
+ const stmt = prep(
1015
+ `SELECT id, sessionId, conversationId, updateType, content, sequence, created_at
1016
+ FROM stream_updates WHERE sessionId = ? ORDER BY sequence ASC`
1017
+ );
1018
+ const rows = stmt.all(sessionId);
1019
+ return rows.map(row => ({
1020
+ ...row,
1021
+ content: JSON.parse(row.content)
1022
+ }));
1023
+ },
1024
+
1025
+ clearSessionStreamUpdates(sessionId) {
1026
+ const stmt = prep('DELETE FROM stream_updates WHERE sessionId = ?');
1027
+ stmt.run(sessionId);
1028
+ },
1029
+
1030
+ createImportedConversation(data) {
1031
+ const id = generateId('conv');
1032
+ const now = Date.now();
1033
+ const stmt = prep(
1034
+ `INSERT INTO conversations (
1035
+ id, agentId, title, created_at, updated_at, status,
1036
+ agentType, source, externalId, firstPrompt, messageCount,
1037
+ projectPath, gitBranch, sourcePath, lastSyncedAt
1038
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1039
+ );
1040
+ stmt.run(
1041
+ id,
1042
+ data.externalId || id,
1043
+ data.title,
1044
+ data.created || now,
1045
+ data.modified || now,
1046
+ 'active',
1047
+ data.agentType || 'claude-code',
1048
+ data.source || 'imported',
1049
+ data.externalId,
1050
+ data.firstPrompt,
1051
+ data.messageCount || 0,
1052
+ data.projectPath,
1053
+ data.gitBranch,
1054
+ data.sourcePath,
1055
+ now
1056
+ );
1057
+ return { id, ...data };
1058
+ },
1059
+
1060
+ getConversationByExternalId(agentType, externalId) {
1061
+ const stmt = prep(
1062
+ 'SELECT * FROM conversations WHERE agentType = ? AND externalId = ?'
1063
+ );
1064
+ return stmt.get(agentType, externalId);
1065
+ },
1066
+
1067
+ getConversationsByAgentType(agentType) {
1068
+ const stmt = prep(
1069
+ 'SELECT * FROM conversations WHERE agentType = ? AND status != ? ORDER BY updated_at DESC'
1070
+ );
1071
+ return stmt.all(agentType, 'deleted');
1072
+ },
1073
+
1074
+ getImportedConversations() {
1075
+ const stmt = prep(
1076
+ 'SELECT * FROM conversations WHERE source = ? AND status != ? ORDER BY updated_at DESC'
1077
+ );
1078
+ return stmt.all('imported', 'deleted');
1079
+ },
1080
+
1081
+ importClaudeCodeConversations() {
1082
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
1083
+ if (!fs.existsSync(projectsDir)) return [];
1084
+
1085
+ const imported = [];
1086
+ const projects = fs.readdirSync(projectsDir);
1087
+
1088
+ for (const projectName of projects) {
1089
+ const indexPath = path.join(projectsDir, projectName, 'sessions-index.json');
1090
+ if (!fs.existsSync(indexPath)) continue;
1091
+
1092
+ try {
1093
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
1094
+ const entries = index.entries || [];
1095
+
1096
+ for (const entry of entries) {
1097
+ try {
1098
+ const existing = this.getConversationByExternalId('claude-code', entry.sessionId);
1099
+ if (existing) {
1100
+ imported.push({ status: 'skipped', id: existing.id });
1101
+ continue;
1102
+ }
1103
+
1104
+ this.createImportedConversation({
1105
+ externalId: entry.sessionId,
1106
+ agentType: 'claude-code',
1107
+ title: entry.summary || entry.firstPrompt || `Conversation ${entry.sessionId.slice(0, 8)}`,
1108
+ firstPrompt: entry.firstPrompt,
1109
+ messageCount: entry.messageCount || 0,
1110
+ created: new Date(entry.created).getTime(),
1111
+ modified: new Date(entry.modified).getTime(),
1112
+ projectPath: entry.projectPath,
1113
+ gitBranch: entry.gitBranch,
1114
+ sourcePath: entry.fullPath,
1115
+ source: 'imported'
1116
+ });
1117
+
1118
+ imported.push({
1119
+ status: 'imported',
1120
+ id: entry.sessionId,
1121
+ title: entry.summary || entry.firstPrompt
1122
+ });
1123
+ } catch (err) {
1124
+ console.error(`[DB] Error importing session ${entry.sessionId}:`, err.message);
1125
+ }
1126
+ }
1127
+ } catch (err) {
1128
+ console.error(`[DB] Error reading ${indexPath}:`, err.message);
1129
+ }
1130
+ }
1131
+
1132
+ return imported;
1133
+ },
1134
+
1135
+ createChunk(sessionId, conversationId, sequence, type, data) {
1136
+ const id = generateId('chunk');
1137
+ const now = Date.now();
1138
+ const dataBlob = typeof data === 'string' ? data : JSON.stringify(data);
1139
+
1140
+ const stmt = prep(
1141
+ `INSERT INTO chunks (id, sessionId, conversationId, sequence, type, data, created_at)
1142
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
1143
+ );
1144
+ stmt.run(id, sessionId, conversationId, sequence, type, dataBlob, now);
1145
+
1146
+ return {
1147
+ id,
1148
+ sessionId,
1149
+ conversationId,
1150
+ sequence,
1151
+ type,
1152
+ data,
1153
+ created_at: now
1154
+ };
1155
+ },
1156
+
1157
+ getChunk(id) {
1158
+ const stmt = prep(
1159
+ `SELECT id, sessionId, conversationId, sequence, type, data, created_at FROM chunks WHERE id = ?`
1160
+ );
1161
+ const row = stmt.get(id);
1162
+ if (!row) return null;
1163
+
1164
+ try {
1165
+ return {
1166
+ ...row,
1167
+ data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1168
+ };
1169
+ } catch (e) {
1170
+ return row;
1171
+ }
1172
+ },
1173
+
1174
+ getSessionChunks(sessionId) {
1175
+ const stmt = prep(
1176
+ `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1177
+ FROM chunks WHERE sessionId = ? ORDER BY sequence ASC`
1178
+ );
1179
+ const rows = stmt.all(sessionId);
1180
+ return rows.map(row => {
1181
+ try {
1182
+ return {
1183
+ ...row,
1184
+ data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1185
+ };
1186
+ } catch (e) {
1187
+ return row;
1188
+ }
1189
+ });
1190
+ },
1191
+
1192
+ getConversationChunkCount(conversationId) {
1193
+ const stmt = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ?');
1194
+ return stmt.get(conversationId).count;
1195
+ },
1196
+
1197
+ getConversationChunks(conversationId) {
1198
+ const stmt = prep(
1199
+ `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1200
+ FROM chunks WHERE conversationId = ? ORDER BY created_at ASC`
1201
+ );
1202
+ const rows = stmt.all(conversationId);
1203
+ return rows.map(row => {
1204
+ try {
1205
+ return {
1206
+ ...row,
1207
+ data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1208
+ };
1209
+ } catch (e) {
1210
+ return row;
1211
+ }
1212
+ });
1213
+ },
1214
+
1215
+ getRecentConversationChunks(conversationId, limit) {
1216
+ const stmt = prep(
1217
+ `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1218
+ FROM chunks WHERE conversationId = ?
1219
+ ORDER BY created_at DESC LIMIT ?`
1220
+ );
1221
+ const rows = stmt.all(conversationId, limit);
1222
+ rows.reverse();
1223
+ return rows.map(row => {
1224
+ try {
1225
+ return {
1226
+ ...row,
1227
+ data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1228
+ };
1229
+ } catch (e) {
1230
+ return row;
1231
+ }
1232
+ });
1233
+ },
1234
+
1235
+ getChunksSince(sessionId, timestamp) {
1236
+ const stmt = prep(
1237
+ `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1238
+ FROM chunks WHERE sessionId = ? AND created_at > ? ORDER BY sequence ASC`
1239
+ );
1240
+ const rows = stmt.all(sessionId, timestamp);
1241
+ return rows.map(row => {
1242
+ try {
1243
+ return {
1244
+ ...row,
1245
+ data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1246
+ };
1247
+ } catch (e) {
1248
+ return row;
1249
+ }
1250
+ });
1251
+ },
1252
+
1253
+ getChunksSinceSeq(sessionId, sinceSeq) {
1254
+ const stmt = prep(
1255
+ `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1256
+ FROM chunks WHERE sessionId = ? AND sequence > ? ORDER BY sequence ASC`
1257
+ );
1258
+ const rows = stmt.all(sessionId, sinceSeq);
1259
+ return rows.map(row => {
1260
+ try {
1261
+ return {
1262
+ ...row,
1263
+ data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1264
+ };
1265
+ } catch (e) {
1266
+ return row;
1267
+ }
1268
+ });
1269
+ },
1270
+
1271
+ deleteSessionChunks(sessionId) {
1272
+ const stmt = prep('DELETE FROM chunks WHERE sessionId = ?');
1273
+ const result = stmt.run(sessionId);
1274
+ return result.changes || 0;
1275
+ },
1276
+
1277
+ getMaxSequence(sessionId) {
1278
+ const stmt = prep('SELECT MAX(sequence) as max FROM chunks WHERE sessionId = ?');
1279
+ const result = stmt.get(sessionId);
1280
+ return result?.max ?? -1;
1281
+ },
1282
+
1283
+ getEmptyConversations() {
1284
+ const stmt = prep(`
1285
+ SELECT c.* FROM conversations c
1286
+ LEFT JOIN messages m ON c.id = m.conversationId
1287
+ WHERE c.status != 'deleted'
1288
+ GROUP BY c.id
1289
+ HAVING COUNT(m.id) = 0
1290
+ `);
1291
+ return stmt.all();
1292
+ },
1293
+
1294
+ permanentlyDeleteConversation(id) {
1295
+ const conv = this.getConversation(id);
1296
+ if (!conv) return false;
1297
+
1298
+ // Delete associated Claude Code session file if it exists
1299
+ if (conv.claudeSessionId) {
1300
+ this.deleteClaudeSessionFile(conv.claudeSessionId);
1301
+ }
1302
+
1303
+ const deleteStmt = db.transaction(() => {
1304
+ prep('DELETE FROM stream_updates WHERE conversationId = ?').run(id);
1305
+ prep('DELETE FROM chunks WHERE conversationId = ?').run(id);
1306
+ prep('DELETE FROM events WHERE conversationId = ?').run(id);
1307
+ prep('DELETE FROM sessions WHERE conversationId = ?').run(id);
1308
+ prep('DELETE FROM messages WHERE conversationId = ?').run(id);
1309
+ prep('DELETE FROM conversations WHERE id = ?').run(id);
1310
+ });
1311
+
1312
+ deleteStmt();
1313
+ return true;
1314
+ },
1315
+
1316
+ cleanupEmptyConversations() {
1317
+ const emptyConvs = this.getEmptyConversations();
1318
+ let deletedCount = 0;
1319
+
1320
+ for (const conv of emptyConvs) {
1321
+ console.log(`[cleanup] Deleting empty conversation: ${conv.id} (${conv.title || 'Untitled'})`);
1322
+ if (this.permanentlyDeleteConversation(conv.id)) {
1323
+ deletedCount++;
1324
+ }
1325
+ }
1326
+
1327
+ if (deletedCount > 0) {
1328
+ console.log(`[cleanup] Deleted ${deletedCount} empty conversation(s)`);
1329
+ }
1330
+
1331
+ return deletedCount;
1332
+ },
1333
+
1334
+ recordIpfsCid(cid, modelName, modelType, modelHash, gatewayUrl) {
1335
+ const id = generateId('ipfs');
1336
+ const now = Date.now();
1337
+ const stmt = prep(`
1338
+ INSERT INTO ipfs_cids (id, cid, modelName, modelType, modelHash, gatewayUrl, cached_at, last_accessed_at)
1339
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1340
+ ON CONFLICT(cid) DO UPDATE SET last_accessed_at = ?, success_count = success_count + 1
1341
+ `);
1342
+ stmt.run(id, cid, modelName, modelType, modelHash, gatewayUrl, now, now, now);
1343
+ const record = this.getIpfsCid(cid);
1344
+ return record ? record.id : id;
1345
+ },
1346
+
1347
+ getIpfsCid(cid) {
1348
+ const stmt = prep('SELECT * FROM ipfs_cids WHERE cid = ?');
1349
+ return stmt.get(cid);
1350
+ },
1351
+
1352
+ getIpfsCidByModel(modelName, modelType) {
1353
+ const stmt = prep('SELECT * FROM ipfs_cids WHERE modelName = ? AND modelType = ? ORDER BY last_accessed_at DESC LIMIT 1');
1354
+ return stmt.get(modelName, modelType);
1355
+ },
1356
+
1357
+ recordDownloadStart(cidId, downloadPath, totalBytes) {
1358
+ const id = generateId('dl');
1359
+ const stmt = prep(`
1360
+ INSERT INTO ipfs_downloads (id, cidId, downloadPath, status, total_bytes, started_at)
1361
+ VALUES (?, ?, ?, ?, ?, ?)
1362
+ `);
1363
+ stmt.run(id, cidId, downloadPath, 'in_progress', totalBytes, Date.now());
1364
+ return id;
1365
+ },
1366
+
1367
+ updateDownloadProgress(downloadId, downloadedBytes) {
1368
+ const stmt = prep(`
1369
+ UPDATE ipfs_downloads SET downloaded_bytes = ? WHERE id = ?
1370
+ `);
1371
+ stmt.run(downloadedBytes, downloadId);
1372
+ },
1373
+
1374
+ completeDownload(downloadId, cidId) {
1375
+ const now = Date.now();
1376
+ prep(`
1377
+ UPDATE ipfs_downloads SET status = ?, completed_at = ? WHERE id = ?
1378
+ `).run('success', now, downloadId);
1379
+ prep(`
1380
+ UPDATE ipfs_cids SET last_accessed_at = ? WHERE id = ?
1381
+ `).run(now, cidId);
1382
+ },
1383
+
1384
+ recordDownloadError(downloadId, cidId, errorMessage) {
1385
+ const now = Date.now();
1386
+ prep(`
1387
+ UPDATE ipfs_downloads SET status = ?, error_message = ?, completed_at = ? WHERE id = ?
1388
+ `).run('failed', errorMessage, now, downloadId);
1389
+ prep(`
1390
+ UPDATE ipfs_cids SET failure_count = failure_count + 1 WHERE id = ?
1391
+ `).run(cidId);
1392
+ },
1393
+
1394
+ getDownload(downloadId) {
1395
+ const stmt = prep('SELECT * FROM ipfs_downloads WHERE id = ?');
1396
+ return stmt.get(downloadId);
1397
+ },
1398
+
1399
+ getDownloadsByCid(cidId) {
1400
+ const stmt = prep('SELECT * FROM ipfs_downloads WHERE cidId = ? ORDER BY started_at DESC');
1401
+ return stmt.all(cidId);
1402
+ },
1403
+
1404
+ getDownloadsByStatus(status) {
1405
+ const stmt = prep('SELECT * FROM ipfs_downloads WHERE status = ? ORDER BY started_at DESC');
1406
+ return stmt.all(status);
1407
+ },
1408
+
1409
+ updateDownloadResume(downloadId, currentSize, attempts, lastAttempt, status) {
1410
+ const stmt = prep(`
1411
+ UPDATE ipfs_downloads
1412
+ SET downloaded_bytes = ?, attempts = ?, lastAttempt = ?, status = ?
1413
+ WHERE id = ?
1414
+ `);
1415
+ stmt.run(currentSize, attempts, lastAttempt, status, downloadId);
1416
+ },
1417
+
1418
+ updateDownloadHash(downloadId, hash) {
1419
+ const stmt = prep('UPDATE ipfs_downloads SET hash = ? WHERE id = ?');
1420
+ stmt.run(hash, downloadId);
1421
+ },
1422
+
1423
+ markDownloadResuming(downloadId) {
1424
+ const stmt = prep('UPDATE ipfs_downloads SET status = ?, lastAttempt = ? WHERE id = ?');
1425
+ stmt.run('resuming', Date.now(), downloadId);
1426
+ },
1427
+
1428
+ markDownloadPaused(downloadId, errorMessage) {
1429
+ const stmt = prep('UPDATE ipfs_downloads SET status = ?, error_message = ?, lastAttempt = ? WHERE id = ?');
1430
+ stmt.run('paused', errorMessage, Date.now(), downloadId);
1431
+ }
1432
+ };
1433
+
1434
+ export default { queries };