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.
- package/CLAUDE.md +280 -280
- package/IPFS_DOWNLOADER.md +277 -277
- package/TASK_2C_COMPLETION.md +334 -334
- package/agentgui.ico +0 -0
- package/bin/gmgui.cjs +54 -54
- package/build-portable.js +13 -42
- package/database.js +1422 -1406
- package/lib/claude-runner.js +1130 -1130
- package/lib/ipfs-downloader.js +459 -459
- package/lib/speech.js +159 -152
- package/package.json +1 -1
- package/readme.md +76 -76
- package/server.js +3787 -3794
- package/setup-npm-token.sh +68 -68
- package/static/app.js +773 -773
- package/static/event-rendering-showcase.html +708 -708
- package/static/index.html +3178 -3180
- package/static/js/agent-auth.js +298 -298
- package/static/js/audio-recorder-processor.js +18 -18
- package/static/js/client.js +2656 -2656
- package/static/js/conversations.js +583 -583
- package/static/js/dialogs.js +267 -267
- package/static/js/event-consolidator.js +101 -101
- package/static/js/event-filter.js +311 -311
- package/static/js/event-processor.js +452 -452
- package/static/js/features.js +413 -413
- package/static/js/kalman-filter.js +67 -67
- package/static/js/progress-dialog.js +130 -130
- package/static/js/script-runner.js +219 -219
- package/static/js/streaming-renderer.js +2123 -2120
- package/static/js/syntax-highlighter.js +269 -269
- package/static/js/tts-websocket-handler.js +152 -152
- package/static/js/ui-components.js +431 -431
- package/static/js/voice.js +849 -849
- package/static/js/websocket-manager.js +596 -596
- package/static/templates/INDEX.html +465 -465
- package/static/templates/README.md +190 -190
- package/static/templates/agent-capabilities.html +56 -56
- package/static/templates/agent-metadata-panel.html +44 -44
- package/static/templates/agent-status-badge.html +30 -30
- package/static/templates/code-annotation-panel.html +155 -155
- package/static/templates/code-suggestion-panel.html +184 -184
- package/static/templates/command-header.html +77 -77
- package/static/templates/command-output-scrollable.html +118 -118
- package/static/templates/elapsed-time.html +54 -54
- package/static/templates/error-alert.html +106 -106
- package/static/templates/error-history-timeline.html +160 -160
- package/static/templates/error-recovery-options.html +109 -109
- package/static/templates/error-stack-trace.html +95 -95
- package/static/templates/error-summary.html +80 -80
- package/static/templates/event-counter.html +48 -48
- package/static/templates/execution-actions.html +97 -97
- package/static/templates/execution-progress-bar.html +80 -80
- package/static/templates/execution-stepper.html +120 -120
- package/static/templates/file-breadcrumb.html +118 -118
- package/static/templates/file-diff-viewer.html +121 -121
- package/static/templates/file-metadata.html +133 -133
- package/static/templates/file-read-panel.html +66 -66
- package/static/templates/file-write-panel.html +120 -120
- package/static/templates/git-branch-remote.html +107 -107
- package/static/templates/git-diff-list.html +101 -101
- package/static/templates/git-log-visualization.html +153 -153
- package/static/templates/git-status-panel.html +115 -115
- package/static/templates/quality-metrics-display.html +170 -170
- package/static/templates/terminal-output-panel.html +87 -87
- package/static/templates/test-results-display.html +144 -144
- package/static/theme.js +72 -72
- package/test-download-progress.js +223 -223
- package/test-websocket-broadcast.js +147 -147
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
|
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
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
`
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
} catch (e) {
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
},
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
);
|
|
814
|
-
stmt.run(
|
|
815
|
-
},
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
);
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
);
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
);
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
);
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
return
|
|
1063
|
-
},
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
},
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
return
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
WHERE
|
|
1398
|
-
|
|
1399
|
-
stmt.
|
|
1400
|
-
},
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
const stmt = prep(
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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 };
|