coding-tool-x 3.8.0 → 3.8.2
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/CHANGELOG.md +2 -0
- package/README.md +4 -6
- package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
- package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
- package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
- package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
- package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
- package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
- package/dist/web/assets/index-D5hyHPj7.js +2 -0
- package/dist/web/index.html +1 -1
- package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
- package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
- package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
- package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
- package/package.json +2 -2
- package/src/commands/update.js +1 -1
- package/src/config/paths.js +1 -0
- package/src/server/api/codex-sessions.js +14 -14
- package/src/server/api/gemini-sessions.js +52 -14
- package/src/server/api/omp-projects.js +2 -2
- package/src/server/api/omp-sessions.js +61 -23
- package/src/server/api/sessions.js +44 -2
- package/src/server/opencode-proxy-server.js +55 -1037
- package/src/server/services/claude-wire.js +694 -0
- package/src/server/services/codex-sessions.js +41 -233
- package/src/server/services/codex-wire.js +219 -0
- package/src/server/services/dashboard-snapshot-worker.js +5 -5
- package/src/server/services/gemini-sessions.js +67 -157
- package/src/server/services/gemini-wire.js +759 -0
- package/src/server/services/model-detector.js +49 -131
- package/src/server/services/omp-channels.js +23 -4
- package/src/server/services/omp-gateway.js +29 -1
- package/src/server/services/omp-sessions.js +102 -105
- package/src/server/services/omp-settings-manager.js +27 -3
- package/src/server/services/opencode-gateway-adapters.js +22 -1147
- package/src/server/services/opencode-normalization.js +546 -0
- package/src/server/services/opencode-sessions.js +339 -546
- package/src/server/services/session-converter.js +4 -4
- package/src/server/services/session-history-adapters/claude.js +235 -0
- package/src/server/services/session-history-adapters/codex.js +158 -0
- package/src/server/services/session-history-adapters/gemini.js +292 -0
- package/src/server/services/session-history-adapters/index.js +20 -0
- package/src/server/services/session-history-adapters/omp.js +256 -0
- package/src/server/services/session-history-index.js +930 -0
- package/src/server/services/session-history-worker.js +99 -0
- package/src/server/services/session-snapshot-worker.js +7 -7
- package/src/server/services/sessions.js +167 -775
- package/src/server/services/speed-test.js +85 -303
- package/src/server/services/sqlite-connection.js +78 -0
- package/src/server/services/workspace-service.js +3 -3
- package/dist/web/assets/index-DIWJmyL6.js +0 -2
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { openDatabase, closeDatabase } = require('./sqlite-connection');
|
|
4
|
+
const { PATHS } = require('../../config/paths');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Constants
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** @type {number} Maximum time a source inventory is considered fresh */
|
|
13
|
+
const INDEX_INVENTORY_TTL_MS = 30000;
|
|
14
|
+
|
|
15
|
+
/** @type {number} Max wait for stale-ok on a cold source */
|
|
16
|
+
const COLD_STALE_WAIT_MS = 2500;
|
|
17
|
+
|
|
18
|
+
/** @type {number} Hard timeout for worker processes */
|
|
19
|
+
const WORKER_TIMEOUT_MS = 180000;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Schema
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const SCHEMA_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
const SCHEMA_SQL = `
|
|
28
|
+
CREATE TABLE IF NOT EXISTS source_state (
|
|
29
|
+
source TEXT PRIMARY KEY,
|
|
30
|
+
last_inventory_ms INTEGER,
|
|
31
|
+
last_error TEXT
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS session_file (
|
|
35
|
+
source TEXT NOT NULL,
|
|
36
|
+
file_path TEXT NOT NULL,
|
|
37
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
session_id TEXT NOT NULL,
|
|
40
|
+
project_name TEXT NOT NULL,
|
|
41
|
+
project_display_name TEXT,
|
|
42
|
+
project_full_path TEXT,
|
|
43
|
+
first_message TEXT,
|
|
44
|
+
git_branch TEXT,
|
|
45
|
+
provider TEXT,
|
|
46
|
+
model TEXT,
|
|
47
|
+
started_at INTEGER,
|
|
48
|
+
updated_at INTEGER,
|
|
49
|
+
message_count INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
usage_json TEXT,
|
|
51
|
+
extra_json TEXT,
|
|
52
|
+
PRIMARY KEY (source, file_path),
|
|
53
|
+
UNIQUE (source, session_id)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_session_file_project
|
|
57
|
+
ON session_file(source, project_name, updated_at DESC);
|
|
58
|
+
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_session_file_sid
|
|
60
|
+
ON session_file(source, session_id);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS session_message (
|
|
63
|
+
source TEXT NOT NULL,
|
|
64
|
+
session_id TEXT NOT NULL,
|
|
65
|
+
ordinal INTEGER NOT NULL,
|
|
66
|
+
message_id TEXT,
|
|
67
|
+
role TEXT,
|
|
68
|
+
type TEXT,
|
|
69
|
+
subtype TEXT,
|
|
70
|
+
content TEXT,
|
|
71
|
+
timestamp INTEGER,
|
|
72
|
+
model TEXT,
|
|
73
|
+
provider TEXT,
|
|
74
|
+
user_message_number INTEGER,
|
|
75
|
+
extra_json TEXT,
|
|
76
|
+
PRIMARY KEY (source, session_id, ordinal),
|
|
77
|
+
FOREIGN KEY (source, session_id) REFERENCES session_file(source, session_id)
|
|
78
|
+
ON DELETE CASCADE
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_session_message_sid_ord
|
|
82
|
+
ON session_message(source, session_id, ordinal);
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const FTS_SETUP_SQL = `
|
|
86
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_message_fts USING fts5(
|
|
87
|
+
content,
|
|
88
|
+
content='session_message',
|
|
89
|
+
content_rowid='rowid',
|
|
90
|
+
tokenize='trigram'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
CREATE TRIGGER IF NOT EXISTS session_message_fts_insert AFTER INSERT ON session_message BEGIN
|
|
94
|
+
INSERT INTO session_message_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
95
|
+
END;
|
|
96
|
+
|
|
97
|
+
CREATE TRIGGER IF NOT EXISTS session_message_fts_delete AFTER DELETE ON session_message BEGIN
|
|
98
|
+
INSERT INTO session_message_fts(session_message_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
|
|
99
|
+
END;
|
|
100
|
+
|
|
101
|
+
CREATE TRIGGER IF NOT EXISTS session_message_fts_update AFTER UPDATE ON session_message BEGIN
|
|
102
|
+
INSERT INTO session_message_fts(session_message_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
|
|
103
|
+
INSERT INTO session_message_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
104
|
+
END;
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Helpers
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
function _detectFts5(db) {
|
|
115
|
+
try {
|
|
116
|
+
const rows = db.prepare('SELECT 1 FROM pragma_compile_options WHERE compile_options = ?').all('ENABLE_FTS5');
|
|
117
|
+
return rows.length > 0;
|
|
118
|
+
} catch (_err) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Normalize page/limit/order defaults matching current route behavior.
|
|
125
|
+
* @param {{ page?: number, limit?: number, order?: string }} options
|
|
126
|
+
* @returns {{ page: number, limit: number, order: 'ASC'|'DESC' }}
|
|
127
|
+
*/
|
|
128
|
+
function _normalizePageOpts(options = {}) {
|
|
129
|
+
let page = Number(options.page) || 1;
|
|
130
|
+
let limit = Number(options.limit) || 20;
|
|
131
|
+
const order = options.order === 'asc' ? 'ASC' : 'DESC';
|
|
132
|
+
if (page < 1) page = 1;
|
|
133
|
+
if (limit < 1) limit = 1;
|
|
134
|
+
if (limit > 200) limit = 200;
|
|
135
|
+
return { page, limit, order };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Double embedded quotes in a string for FTS5 MATCH.
|
|
140
|
+
* @param {string} s
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
function _ftsQuote(s) {
|
|
144
|
+
return '"' + String(s).replace(/"/g, '""') + '"';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Core: createSessionHistoryIndex
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {object} [opts]
|
|
153
|
+
* @param {string} [opts.dbPath]
|
|
154
|
+
* @param {object} [opts.adapterRegistry] - source → { inventory, parse }
|
|
155
|
+
* @param {Function} [opts.workerRunner] - (source, indexDbPath) => Promise<void>
|
|
156
|
+
* @param {boolean|null} [opts.ftsEnabledOverride] - override FTS detection for tests
|
|
157
|
+
* @returns {object} index API
|
|
158
|
+
*/
|
|
159
|
+
function createSessionHistoryIndex(opts = {}) {
|
|
160
|
+
const dbPath = opts.dbPath || PATHS?.sessionHistoryIndex || path.join(PATHS?.base || process.cwd(), 'session-history.sqlite');
|
|
161
|
+
const adapters = opts.adapterRegistry || require('./session-history-adapters');
|
|
162
|
+
const workerRunner = opts.workerRunner || _defaultWorkerRunner;
|
|
163
|
+
const ftsEnabled = opts.ftsEnabledOverride !== undefined
|
|
164
|
+
? opts.ftsEnabledOverride
|
|
165
|
+
: null;
|
|
166
|
+
const shouldUseWorker = process.env.NODE_ENV !== 'test' && process.env.CC_TOOL_SESSION_HISTORY_CHILD !== '1';
|
|
167
|
+
let _db = null;
|
|
168
|
+
let _ftsAvailable = null;
|
|
169
|
+
/** @type {Map<string, Promise<void>>} */
|
|
170
|
+
const _inflight = new Map();
|
|
171
|
+
|
|
172
|
+
function _getDb() {
|
|
173
|
+
if (_db) return _db;
|
|
174
|
+
_db = openDatabase(dbPath);
|
|
175
|
+
_initSchema(_db);
|
|
176
|
+
if (ftsEnabled !== null) {
|
|
177
|
+
_ftsAvailable = ftsEnabled;
|
|
178
|
+
} else if (_ftsAvailable === null) {
|
|
179
|
+
_ftsAvailable = _detectFts5(_db);
|
|
180
|
+
}
|
|
181
|
+
if (_ftsAvailable && ftsEnabled !== false) {
|
|
182
|
+
_initFts(_db);
|
|
183
|
+
}
|
|
184
|
+
return _db;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _initSchema(db) {
|
|
188
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
189
|
+
db.exec(SCHEMA_SQL);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function _initFts(db) {
|
|
193
|
+
try {
|
|
194
|
+
db.exec(FTS_SETUP_SQL);
|
|
195
|
+
} catch (_err) {
|
|
196
|
+
_ftsAvailable = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {string} source
|
|
202
|
+
* @param {{ force?: boolean, consistency?: string }} [options]
|
|
203
|
+
* @returns {Promise<void>}
|
|
204
|
+
*/
|
|
205
|
+
async function ensureSourceIndexed(source, options = {}) {
|
|
206
|
+
const consistency = options.consistency || 'stale-ok';
|
|
207
|
+
const force = options.force === true;
|
|
208
|
+
|
|
209
|
+
const key = `ensure:${source}`;
|
|
210
|
+
if (_inflight.has(key)) {
|
|
211
|
+
if (consistency === 'complete') {
|
|
212
|
+
return _inflight.get(key);
|
|
213
|
+
}
|
|
214
|
+
const inflight = _inflight.get(key);
|
|
215
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve('timeout'), COLD_STALE_WAIT_MS));
|
|
216
|
+
const result = await Promise.race([inflight, timeout]);
|
|
217
|
+
if (result === 'timeout') return;
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const promise = shouldUseWorker
|
|
222
|
+
? workerRunner(source, dbPath, { force })
|
|
223
|
+
: _runInventory(source, { force });
|
|
224
|
+
_inflight.set(key, promise);
|
|
225
|
+
promise.finally(() => {
|
|
226
|
+
if (_inflight.get(key) === promise) {
|
|
227
|
+
_inflight.delete(key);
|
|
228
|
+
}
|
|
229
|
+
}).catch(() => {});
|
|
230
|
+
|
|
231
|
+
if (consistency === 'stale-ok') {
|
|
232
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve('timeout'), COLD_STALE_WAIT_MS));
|
|
233
|
+
const result = await Promise.race([promise, timeout]);
|
|
234
|
+
if (result === 'timeout') {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await promise;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function _runInventory(source, { force = false } = {}) {
|
|
243
|
+
const db = _getDb();
|
|
244
|
+
const adapter = adapters[source];
|
|
245
|
+
if (!adapter || !adapter.inventory) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!force) {
|
|
250
|
+
const row = db.prepare('SELECT last_inventory_ms FROM source_state WHERE source = ?').get(source);
|
|
251
|
+
if (row && row.last_inventory_ms && (Date.now() - row.last_inventory_ms) < INDEX_INVENTORY_TTL_MS) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const descriptors = await adapter.inventory();
|
|
257
|
+
if (!Array.isArray(descriptors)) return;
|
|
258
|
+
|
|
259
|
+
const seen = new Map();
|
|
260
|
+
for (const d of descriptors) {
|
|
261
|
+
const prev = seen.get(d.sessionId);
|
|
262
|
+
if (!prev || d.mtimeMs > prev.mtimeMs || (d.mtimeMs === prev.mtimeMs && d.filePath < prev.filePath)) {
|
|
263
|
+
seen.set(d.sessionId, d);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const indexedFiles = new Map();
|
|
268
|
+
const indexedRows = db.prepare('SELECT file_path, size, mtime_ms FROM session_file WHERE source = ?').all(source);
|
|
269
|
+
for (const r of indexedRows) {
|
|
270
|
+
indexedFiles.set(r.file_path, r);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const toParse = [];
|
|
274
|
+
const activePaths = new Set();
|
|
275
|
+
for (const d of seen.values()) {
|
|
276
|
+
activePaths.add(d.filePath);
|
|
277
|
+
const idx = indexedFiles.get(d.filePath);
|
|
278
|
+
if (!idx || idx.size !== d.size || idx.mtime_ms !== d.mtimeMs) {
|
|
279
|
+
toParse.push(d);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const filePath of indexedFiles.keys()) {
|
|
284
|
+
if (!activePaths.has(filePath)) {
|
|
285
|
+
db.prepare('DELETE FROM session_file WHERE source = ? AND file_path = ?').run(source, filePath);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let errorMsg = null;
|
|
290
|
+
for (const d of toParse) {
|
|
291
|
+
try {
|
|
292
|
+
const preStat = { size: d.size, mtimeMs: d.mtimeMs };
|
|
293
|
+
const { session, messages } = await adapter.parse(d);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const postStat = fs.statSync(d.filePath);
|
|
297
|
+
if (postStat.size !== preStat.size || postStat.mtimeMs !== preStat.mtimeMs) {
|
|
298
|
+
const retry = await adapter.parse(d);
|
|
299
|
+
const retryStat = fs.statSync(d.filePath);
|
|
300
|
+
if (retryStat.size !== postStat.size || retryStat.mtimeMs !== postStat.mtimeMs) {
|
|
301
|
+
const existing = db.prepare('SELECT 1 FROM session_file WHERE source = ? AND file_path = ?').get(source, d.filePath);
|
|
302
|
+
if (existing) continue;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
_upsertSession(db, source, d, retry.session, retry.messages);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
} catch (_statErr) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_upsertSession(db, source, d, session, messages);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
const existing = db.prepare('SELECT 1 FROM session_file WHERE source = ? AND file_path = ?').get(source, d.filePath);
|
|
315
|
+
if (!existing) {
|
|
316
|
+
errorMsg = (errorMsg ? errorMsg + '; ' : '') + `${d.filePath}: ${err.message}`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
db.prepare(
|
|
322
|
+
'INSERT INTO source_state(source, last_inventory_ms, last_error) VALUES(?, ?, ?) ON CONFLICT(source) DO UPDATE SET last_inventory_ms = excluded.last_inventory_ms, last_error = excluded.last_error'
|
|
323
|
+
).run(source, Date.now(), errorMsg);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function _upsertSession(db, source, descriptor, session, messages) {
|
|
327
|
+
try {
|
|
328
|
+
db.exec('BEGIN IMMEDIATE');
|
|
329
|
+
db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?').run(source, session.sessionId);
|
|
330
|
+
|
|
331
|
+
db.prepare(`
|
|
332
|
+
INSERT INTO session_file(
|
|
333
|
+
source, file_path, size, mtime_ms, session_id,
|
|
334
|
+
project_name, project_display_name, project_full_path,
|
|
335
|
+
first_message, git_branch, provider, model,
|
|
336
|
+
started_at, updated_at, message_count, usage_json, extra_json
|
|
337
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
338
|
+
`).run(
|
|
339
|
+
source,
|
|
340
|
+
descriptor.filePath,
|
|
341
|
+
descriptor.size,
|
|
342
|
+
descriptor.mtimeMs,
|
|
343
|
+
session.sessionId,
|
|
344
|
+
session.projectName || '',
|
|
345
|
+
session.projectDisplayName || null,
|
|
346
|
+
session.projectFullPath || null,
|
|
347
|
+
session.firstMessage || null,
|
|
348
|
+
session.gitBranch || null,
|
|
349
|
+
session.provider || null,
|
|
350
|
+
session.model || null,
|
|
351
|
+
session.startedAt || null,
|
|
352
|
+
session.updatedAt || null,
|
|
353
|
+
messages.length,
|
|
354
|
+
session.usageJson || null,
|
|
355
|
+
session.extraJson || null
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (messages.length > 0) {
|
|
359
|
+
const stmt = db.prepare(`
|
|
360
|
+
INSERT INTO session_message(
|
|
361
|
+
source, session_id, ordinal, message_id, role, type, subtype,
|
|
362
|
+
content, timestamp, model, provider, user_message_number, extra_json
|
|
363
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
364
|
+
`);
|
|
365
|
+
|
|
366
|
+
let ordinal = 0;
|
|
367
|
+
for (const msg of messages) {
|
|
368
|
+
stmt.run(
|
|
369
|
+
source,
|
|
370
|
+
session.sessionId,
|
|
371
|
+
ordinal,
|
|
372
|
+
msg.messageId || null,
|
|
373
|
+
msg.role || null,
|
|
374
|
+
msg.type || null,
|
|
375
|
+
msg.subtype || null,
|
|
376
|
+
msg.content || null,
|
|
377
|
+
msg.timestamp || null,
|
|
378
|
+
msg.model || null,
|
|
379
|
+
msg.provider || null,
|
|
380
|
+
msg.userMessageNumber != null ? msg.userMessageNumber : null,
|
|
381
|
+
msg.extraJson || null
|
|
382
|
+
);
|
|
383
|
+
ordinal++;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
db.exec('COMMIT');
|
|
388
|
+
} catch (err) {
|
|
389
|
+
try { db.exec('ROLLBACK'); } catch (_) {}
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
// ---- Public query methods ----
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @param {string} source
|
|
400
|
+
* @param {object} [options]
|
|
401
|
+
* @returns {Promise<Array<{name, displayName, fullPath, path, sessionCount, lastUsed, latestSession, source}>>}
|
|
402
|
+
*/
|
|
403
|
+
async function listProjects(source, options = {}) {
|
|
404
|
+
await ensureSourceIndexed(source, { consistency: options.consistency || 'stale-ok' });
|
|
405
|
+
const db = _getDb();
|
|
406
|
+
|
|
407
|
+
const rows = db.prepare(`
|
|
408
|
+
SELECT project_name, project_display_name, project_full_path,
|
|
409
|
+
COUNT(*) AS session_count,
|
|
410
|
+
MAX(updated_at) AS last_used,
|
|
411
|
+
(SELECT session_id FROM session_file sf2
|
|
412
|
+
WHERE sf2.source = ? AND sf2.project_name = sf.project_name
|
|
413
|
+
ORDER BY updated_at DESC LIMIT 1) AS latest_session
|
|
414
|
+
FROM session_file sf
|
|
415
|
+
WHERE source = ?
|
|
416
|
+
GROUP BY project_name
|
|
417
|
+
ORDER BY last_used DESC
|
|
418
|
+
`).all(source, source);
|
|
419
|
+
|
|
420
|
+
return rows.map(r => ({
|
|
421
|
+
name: r.project_name,
|
|
422
|
+
displayName: r.project_display_name || r.project_name,
|
|
423
|
+
fullPath: r.project_full_path || '',
|
|
424
|
+
path: r.project_full_path || '',
|
|
425
|
+
sessionCount: r.session_count,
|
|
426
|
+
lastUsed: r.last_used,
|
|
427
|
+
latestSession: r.latest_session,
|
|
428
|
+
source
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @param {string} source
|
|
434
|
+
* @param {string} projectName
|
|
435
|
+
* @param {object} [options]
|
|
436
|
+
* @returns {Promise<Array>}
|
|
437
|
+
*/
|
|
438
|
+
async function listSessions(source, projectName, options = {}) {
|
|
439
|
+
await ensureSourceIndexed(source, { consistency: 'stale-ok' });
|
|
440
|
+
|
|
441
|
+
const db = _getDb();
|
|
442
|
+
const rows = db.prepare(`
|
|
443
|
+
SELECT * FROM session_file
|
|
444
|
+
WHERE source = ? AND project_name = ?
|
|
445
|
+
ORDER BY updated_at DESC
|
|
446
|
+
`).all(source, projectName);
|
|
447
|
+
|
|
448
|
+
return rows.map(r => ({
|
|
449
|
+
sessionId: r.session_id,
|
|
450
|
+
filePath: r.file_path,
|
|
451
|
+
size: r.size,
|
|
452
|
+
mtime: r.mtime_ms,
|
|
453
|
+
firstMessage: r.first_message,
|
|
454
|
+
gitBranch: r.git_branch,
|
|
455
|
+
provider: r.provider,
|
|
456
|
+
model: r.model,
|
|
457
|
+
messageCount: r.message_count,
|
|
458
|
+
tokens: r.usage_json ? JSON.parse(r.usage_json) : null,
|
|
459
|
+
extra: r.extra_json ? JSON.parse(r.extra_json) : {},
|
|
460
|
+
source: r.source,
|
|
461
|
+
projectName: r.project_name,
|
|
462
|
+
projectDisplayName: r.project_display_name,
|
|
463
|
+
projectFullPath: r.project_full_path,
|
|
464
|
+
startedAt: r.started_at,
|
|
465
|
+
updatedAt: r.updated_at
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @param {string} source
|
|
471
|
+
* @param {string} sessionId
|
|
472
|
+
* @param {object} [options]
|
|
473
|
+
* @returns {Promise<{sessionId, lastModified, size, filePath}|null>}
|
|
474
|
+
*/
|
|
475
|
+
async function getSessionStatus(source, sessionId, options = {}) {
|
|
476
|
+
await ensureSourceIndexed(source, { consistency: 'stale-ok' });
|
|
477
|
+
const db = _getDb();
|
|
478
|
+
const row = db.prepare(
|
|
479
|
+
'SELECT session_id, mtime_ms, size, file_path FROM session_file WHERE source = ? AND session_id = ?'
|
|
480
|
+
).get(source, sessionId);
|
|
481
|
+
|
|
482
|
+
if (!row) return null;
|
|
483
|
+
|
|
484
|
+
// Verify file still exists
|
|
485
|
+
try {
|
|
486
|
+
await fs.promises.stat(row.file_path);
|
|
487
|
+
} catch (_) {
|
|
488
|
+
// File gone - invalidate and return null
|
|
489
|
+
db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?').run(source, sessionId);
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
sessionId: row.session_id,
|
|
495
|
+
lastModified: row.mtime_ms,
|
|
496
|
+
size: row.size,
|
|
497
|
+
filePath: row.file_path
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @param {string} source
|
|
503
|
+
* @param {string} sessionId
|
|
504
|
+
* @param {object} [options]
|
|
505
|
+
* @returns {Promise<{sessionId, items: Array}|null>}
|
|
506
|
+
*/
|
|
507
|
+
async function getSessionOutline(source, sessionId, options = {}) {
|
|
508
|
+
await _ensureSessionCurrent(source, sessionId);
|
|
509
|
+
const db = _getDb();
|
|
510
|
+
|
|
511
|
+
const rows = db.prepare(`
|
|
512
|
+
SELECT user_message_number, content, timestamp
|
|
513
|
+
FROM session_message
|
|
514
|
+
WHERE source = ? AND session_id = ? AND user_message_number IS NOT NULL
|
|
515
|
+
ORDER BY ordinal ASC
|
|
516
|
+
`).all(source, sessionId);
|
|
517
|
+
|
|
518
|
+
if (rows.length === 0) {
|
|
519
|
+
// Check session exists at all
|
|
520
|
+
const sf = db.prepare('SELECT 1 FROM session_file WHERE source = ? AND session_id = ?').get(source, sessionId);
|
|
521
|
+
if (!sf) return null;
|
|
522
|
+
return { sessionId, items: [] };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
sessionId,
|
|
527
|
+
items: rows.map(r => ({
|
|
528
|
+
userMessageNumber: r.user_message_number,
|
|
529
|
+
preview: _buildPreview(r.content),
|
|
530
|
+
timestamp: r.timestamp
|
|
531
|
+
}))
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* @param {string} source
|
|
537
|
+
* @param {string} sessionId
|
|
538
|
+
* @param {object} [options]
|
|
539
|
+
* @returns {Promise<{messages: Array, metadata: object, pagination: object}|null>}
|
|
540
|
+
*/
|
|
541
|
+
async function getMessagePage(source, sessionId, options = {}) {
|
|
542
|
+
await _ensureSessionCurrent(source, sessionId);
|
|
543
|
+
const db = _getDb();
|
|
544
|
+
const { page, limit, order } = _normalizePageOpts(options);
|
|
545
|
+
|
|
546
|
+
// Check session exists
|
|
547
|
+
const sf = db.prepare('SELECT * FROM session_file WHERE source = ? AND session_id = ?').get(source, sessionId);
|
|
548
|
+
if (!sf) return null;
|
|
549
|
+
|
|
550
|
+
const total = Number(sf.message_count);
|
|
551
|
+
if (total === 0) {
|
|
552
|
+
return {
|
|
553
|
+
messages: [],
|
|
554
|
+
metadata: _buildMessageMetadata(sf),
|
|
555
|
+
pagination: { page, limit, total: 0, hasMore: false }
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const offset = (page - 1) * limit;
|
|
560
|
+
const rows = db.prepare(`
|
|
561
|
+
SELECT * FROM session_message
|
|
562
|
+
WHERE source = ? AND session_id = ?
|
|
563
|
+
ORDER BY ordinal ${order}
|
|
564
|
+
LIMIT ? OFFSET ?
|
|
565
|
+
`).all(source, sessionId, limit, offset);
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
messages: rows.map(r => ({
|
|
569
|
+
messageId: r.message_id,
|
|
570
|
+
role: r.role,
|
|
571
|
+
type: r.type,
|
|
572
|
+
subtype: r.subtype,
|
|
573
|
+
content: r.content,
|
|
574
|
+
timestamp: r.timestamp,
|
|
575
|
+
model: r.model,
|
|
576
|
+
provider: r.provider,
|
|
577
|
+
userMessageNumber: r.user_message_number,
|
|
578
|
+
extra: r.extra_json ? JSON.parse(r.extra_json) : {}
|
|
579
|
+
})),
|
|
580
|
+
metadata: _buildMessageMetadata(sf),
|
|
581
|
+
pagination: {
|
|
582
|
+
page,
|
|
583
|
+
limit,
|
|
584
|
+
total,
|
|
585
|
+
hasMore: offset + limit < total
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* @param {string} source
|
|
592
|
+
* @param {number} [limit=5]
|
|
593
|
+
* @param {object} [options]
|
|
594
|
+
* @returns {Promise<Array>}
|
|
595
|
+
*/
|
|
596
|
+
async function getRecentSessions(source, limit = 5, options = {}) {
|
|
597
|
+
await ensureSourceIndexed(source, { consistency: 'stale-ok' });
|
|
598
|
+
const db = _getDb();
|
|
599
|
+
|
|
600
|
+
const rows = db.prepare(`
|
|
601
|
+
SELECT * FROM session_file
|
|
602
|
+
WHERE source = ?
|
|
603
|
+
ORDER BY updated_at DESC
|
|
604
|
+
LIMIT ?
|
|
605
|
+
`).all(source, Math.max(1, Math.min(limit, 100)));
|
|
606
|
+
|
|
607
|
+
return rows.map(r => ({
|
|
608
|
+
sessionId: r.session_id,
|
|
609
|
+
filePath: r.file_path,
|
|
610
|
+
size: r.size,
|
|
611
|
+
mtime: r.mtime_ms,
|
|
612
|
+
firstMessage: r.first_message,
|
|
613
|
+
gitBranch: r.git_branch,
|
|
614
|
+
provider: r.provider,
|
|
615
|
+
model: r.model,
|
|
616
|
+
messageCount: r.message_count,
|
|
617
|
+
projectName: r.project_name,
|
|
618
|
+
projectDisplayName: r.project_display_name,
|
|
619
|
+
projectFullPath: r.project_full_path,
|
|
620
|
+
source: r.source,
|
|
621
|
+
updatedAt: r.updated_at
|
|
622
|
+
}));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* @param {string} source
|
|
627
|
+
* @param {string} keyword
|
|
628
|
+
* @param {object} [options]
|
|
629
|
+
* @returns {Promise<Array>}
|
|
630
|
+
*/
|
|
631
|
+
async function searchSessions(source, keyword, options = {}) {
|
|
632
|
+
await ensureSourceIndexed(source, { consistency: options.consistency || 'complete' });
|
|
633
|
+
const db = _getDb();
|
|
634
|
+
const contextLength = options.contextLength || 35;
|
|
635
|
+
const projectName = options.projectName || null;
|
|
636
|
+
|
|
637
|
+
// Build candidate query
|
|
638
|
+
let candidates;
|
|
639
|
+
if (_ftsAvailable) {
|
|
640
|
+
// FTS5 trigram search for needles >= 3 chars
|
|
641
|
+
const needle = String(keyword);
|
|
642
|
+
if ([...needle].length >= 3) {
|
|
643
|
+
const ftsRows = db.prepare(`
|
|
644
|
+
SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp
|
|
645
|
+
FROM session_message_fts fts
|
|
646
|
+
JOIN session_message sm ON sm.rowid = fts.rowid
|
|
647
|
+
JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
|
|
648
|
+
WHERE sm.source = ? AND session_message_fts MATCH ?
|
|
649
|
+
${projectName ? 'AND sf.project_name = ?' : ''}
|
|
650
|
+
ORDER BY sf.updated_at DESC
|
|
651
|
+
LIMIT 500
|
|
652
|
+
`);
|
|
653
|
+
const params = [source, _ftsQuote(needle)];
|
|
654
|
+
if (projectName) params.push(projectName);
|
|
655
|
+
candidates = ftsRows.all(...params);
|
|
656
|
+
} else {
|
|
657
|
+
// Short needle: scan all messages for this source
|
|
658
|
+
candidates = _scanMessagesRelational(db, source, keyword, projectName);
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
candidates = _scanMessagesRelational(db, source, keyword, projectName);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (candidates.length === 0) return [];
|
|
665
|
+
|
|
666
|
+
// Final matching with JS toLowerCase/indexOf (preserves CJK, Unicode case)
|
|
667
|
+
const lowerKeyword = String(keyword).toLocaleLowerCase();
|
|
668
|
+
const matched = [];
|
|
669
|
+
const matchMap = new Map(); // sessionId → { session, matches[] }
|
|
670
|
+
|
|
671
|
+
for (const c of candidates) {
|
|
672
|
+
const content = c.content || '';
|
|
673
|
+
const lowerContent = content.toLocaleLowerCase();
|
|
674
|
+
let idx = 0;
|
|
675
|
+
let count = 0;
|
|
676
|
+
const positions = [];
|
|
677
|
+
while ((idx = lowerContent.indexOf(lowerKeyword, idx)) !== -1) {
|
|
678
|
+
positions.push(idx);
|
|
679
|
+
count++;
|
|
680
|
+
idx += lowerKeyword.length;
|
|
681
|
+
}
|
|
682
|
+
if (count === 0) continue;
|
|
683
|
+
|
|
684
|
+
if (!matchMap.has(c.session_id)) {
|
|
685
|
+
const sf = db.prepare('SELECT * FROM session_file WHERE source = ? AND session_id = ?').get(source, c.session_id);
|
|
686
|
+
if (!sf) continue;
|
|
687
|
+
matchMap.set(c.session_id, {
|
|
688
|
+
session: sf,
|
|
689
|
+
matchCount: 0,
|
|
690
|
+
messages: []
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const entry = matchMap.get(c.session_id);
|
|
695
|
+
// Claude: count every occurrence; Codex/Gemini/OMP: at most 1 per message
|
|
696
|
+
entry.matchCount += (source === 'claude') ? count : 1;
|
|
697
|
+
entry.messages.push({
|
|
698
|
+
ordinal: c.ordinal,
|
|
699
|
+
content,
|
|
700
|
+
positions,
|
|
701
|
+
role: c.role || 'unknown',
|
|
702
|
+
type: c.type || c.role || 'unknown',
|
|
703
|
+
timestamp: c.timestamp,
|
|
704
|
+
context: _extractContext(content, positions[0], contextLength, lowerKeyword.length)
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Flatten to results
|
|
709
|
+
const results = [];
|
|
710
|
+
for (const [_sid, entry] of matchMap) {
|
|
711
|
+
results.push({
|
|
712
|
+
sessionId: entry.session.session_id,
|
|
713
|
+
projectName: entry.session.project_name,
|
|
714
|
+
projectDisplayName: entry.session.project_display_name,
|
|
715
|
+
projectFullPath: entry.session.project_full_path,
|
|
716
|
+
filePath: entry.session.file_path,
|
|
717
|
+
firstMessage: entry.session.first_message,
|
|
718
|
+
updatedAt: entry.session.updated_at,
|
|
719
|
+
matchCount: entry.matchCount,
|
|
720
|
+
source: entry.session.source,
|
|
721
|
+
matches: entry.messages.slice(0, 5)
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Sort: matchCount DESC, then updated_at DESC
|
|
726
|
+
results.sort((a, b) => {
|
|
727
|
+
const cm = b.matchCount - a.matchCount;
|
|
728
|
+
if (cm !== 0) return cm;
|
|
729
|
+
const aRow = matchMap.get(a.sessionId);
|
|
730
|
+
const bRow = matchMap.get(b.sessionId);
|
|
731
|
+
const au = (aRow && aRow.session.updated_at) || 0;
|
|
732
|
+
const bu = (bRow && bRow.session.updated_at) || 0;
|
|
733
|
+
return bu - au;
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
return results;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function _scanMessagesRelational(db, source, keyword, projectName = null) {
|
|
740
|
+
const lowerKeyword = String(keyword).toLocaleLowerCase();
|
|
741
|
+
// Use instr for case-insensitive substring matching
|
|
742
|
+
let sql = `
|
|
743
|
+
SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp
|
|
744
|
+
FROM session_message sm
|
|
745
|
+
JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
|
|
746
|
+
WHERE sm.source = ? AND instr(lower(sm.content), lower(?)) > 0
|
|
747
|
+
${projectName ? 'AND sf.project_name = ?' : ''}
|
|
748
|
+
ORDER BY sf.updated_at DESC
|
|
749
|
+
LIMIT 500
|
|
750
|
+
`;
|
|
751
|
+
const params = [source, keyword];
|
|
752
|
+
if (projectName) params.push(projectName);
|
|
753
|
+
return db.prepare(sql).all(...params);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function _extractContext(content, position, contextLength, keywordLength) {
|
|
757
|
+
const start = Math.max(0, position - contextLength);
|
|
758
|
+
const end = Math.min(content.length, position + keywordLength + contextLength);
|
|
759
|
+
let ctx = content.slice(start, end);
|
|
760
|
+
if (start > 0) ctx = '...' + ctx;
|
|
761
|
+
if (end < content.length) ctx = ctx + '...';
|
|
762
|
+
return ctx;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function _buildPreview(content) {
|
|
766
|
+
if (!content) return '(空消息)';
|
|
767
|
+
const firstLine = String(content)
|
|
768
|
+
.split('\n')
|
|
769
|
+
.map(l => l.trim())
|
|
770
|
+
.find(Boolean) || '(空消息)';
|
|
771
|
+
return firstLine.length > 42 ? firstLine.slice(0, 42) + '...' : firstLine;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function _buildMessageMetadata(sf) {
|
|
775
|
+
return {
|
|
776
|
+
sessionId: sf.session_id,
|
|
777
|
+
gitBranch: sf.git_branch,
|
|
778
|
+
provider: sf.provider,
|
|
779
|
+
model: sf.model,
|
|
780
|
+
messageCount: sf.message_count,
|
|
781
|
+
usage: sf.usage_json ? JSON.parse(sf.usage_json) : null,
|
|
782
|
+
extra: sf.extra_json ? JSON.parse(sf.extra_json) : {}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Ensure the indexed data for a session is current by checking file mtime.
|
|
788
|
+
*/
|
|
789
|
+
async function _ensureSessionCurrent(source, sessionId) {
|
|
790
|
+
await ensureSourceIndexed(source, { consistency: 'stale-ok' });
|
|
791
|
+
const db = _getDb();
|
|
792
|
+
const row = db.prepare(
|
|
793
|
+
'SELECT file_path, size, mtime_ms FROM session_file WHERE source = ? AND session_id = ?'
|
|
794
|
+
).get(source, sessionId);
|
|
795
|
+
if (!row) return;
|
|
796
|
+
|
|
797
|
+
let currentStat;
|
|
798
|
+
try {
|
|
799
|
+
currentStat = await fs.promises.stat(row.file_path);
|
|
800
|
+
} catch (_) {
|
|
801
|
+
// File gone, session stays indexed as-is
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (currentStat.size !== row.size || currentStat.mtimeMs !== row.mtime_ms) {
|
|
806
|
+
// File changed; force re-index
|
|
807
|
+
await ensureSourceIndexed(source, { force: true, consistency: 'complete' });
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* @param {string} source
|
|
813
|
+
* @param {object} [options]
|
|
814
|
+
*/
|
|
815
|
+
function invalidateSource(source, options = {}) {
|
|
816
|
+
const db = _getDb ? _getDb() : null;
|
|
817
|
+
if (!db) return;
|
|
818
|
+
|
|
819
|
+
if (options.deleted && options.sessionId) {
|
|
820
|
+
db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?').run(source, options.sessionId);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Mark state as stale
|
|
824
|
+
db.prepare(
|
|
825
|
+
'UPDATE source_state SET last_inventory_ms = NULL WHERE source = ?'
|
|
826
|
+
).run(source);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function closeSessionHistoryIndex() {
|
|
830
|
+
closeDatabase(dbPath);
|
|
831
|
+
_db = null;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Build the API object
|
|
835
|
+
const api = {
|
|
836
|
+
ensureSourceIndexed,
|
|
837
|
+
listProjects,
|
|
838
|
+
listSessions,
|
|
839
|
+
getSessionStatus,
|
|
840
|
+
getSessionOutline,
|
|
841
|
+
getMessagePage,
|
|
842
|
+
getRecentSessions,
|
|
843
|
+
searchSessions,
|
|
844
|
+
invalidateSource,
|
|
845
|
+
closeSessionHistoryIndex,
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// Lazy-init helpers used internally
|
|
849
|
+
api._getDb = _getDb;
|
|
850
|
+
api._initSchema = _initSchema;
|
|
851
|
+
|
|
852
|
+
return api;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ---------------------------------------------------------------------------
|
|
856
|
+
// Default singleton
|
|
857
|
+
// ---------------------------------------------------------------------------
|
|
858
|
+
|
|
859
|
+
let _defaultIndex = null;
|
|
860
|
+
|
|
861
|
+
function _getDefaultIndex() {
|
|
862
|
+
if (!_defaultIndex) {
|
|
863
|
+
_defaultIndex = createSessionHistoryIndex();
|
|
864
|
+
}
|
|
865
|
+
return _defaultIndex;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function ensureSourceIndexed(source, options) {
|
|
869
|
+
return _getDefaultIndex().ensureSourceIndexed(source, options);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async function listProjects(source, options) {
|
|
873
|
+
return _getDefaultIndex().listProjects(source, options);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async function listSessions(source, projectName, options) {
|
|
877
|
+
return _getDefaultIndex().listSessions(source, projectName, options);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function getSessionStatus(source, sessionId, options) {
|
|
881
|
+
return _getDefaultIndex().getSessionStatus(source, sessionId, options);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function getSessionOutline(source, sessionId, options) {
|
|
885
|
+
return _getDefaultIndex().getSessionOutline(source, sessionId, options);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async function getMessagePage(source, sessionId, options) {
|
|
889
|
+
return _getDefaultIndex().getMessagePage(source, sessionId, options);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
async function getRecentSessions(source, limit, options) {
|
|
893
|
+
return _getDefaultIndex().getRecentSessions(source, limit, options);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async function searchSessions(source, keyword, options) {
|
|
897
|
+
return _getDefaultIndex().searchSessions(source, keyword, options);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function invalidateSource(source, options) {
|
|
901
|
+
_getDefaultIndex().invalidateSource(source, options);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function closeSessionHistoryIndex() {
|
|
905
|
+
if (_defaultIndex) {
|
|
906
|
+
_defaultIndex.closeSessionHistoryIndex();
|
|
907
|
+
_defaultIndex = null;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ---------------------------------------------------------------------------
|
|
912
|
+
// Default worker runner uses a child process in production.
|
|
913
|
+
async function _defaultWorkerRunner(source, indexDbPath, options = {}) {
|
|
914
|
+
const { runInventoryWorker } = require('./session-history-worker');
|
|
915
|
+
return runInventoryWorker(source, indexDbPath, options);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
module.exports = {
|
|
919
|
+
createSessionHistoryIndex,
|
|
920
|
+
ensureSourceIndexed,
|
|
921
|
+
listProjects,
|
|
922
|
+
listSessions,
|
|
923
|
+
getSessionStatus,
|
|
924
|
+
getSessionOutline,
|
|
925
|
+
getMessagePage,
|
|
926
|
+
getRecentSessions,
|
|
927
|
+
searchSessions,
|
|
928
|
+
invalidateSource,
|
|
929
|
+
closeSessionHistoryIndex
|
|
930
|
+
};
|