@@ -1875,15 +1875,20 @@ function getKiloCodeDataRoots() {
18751875function getKiloCodeDatabaseFiles() {
18761876 const files = [];
18771877 const seen = new Set();
1878+ const push = (value) => {
1879+ if (typeof value !== 'string' || !value.trim()) return;
1880+ const full = expandHomePath(value.trim());
1881+ if (!full || seen.has(full) || !fs.existsSync(full)) return;
1882+ seen.add(full);
1883+ files.push(full);
1884+ };
1885+ push(process.env.KILO_DB);
18781886 for (const root of getKiloCodeDataRoots()) {
18791887 let entries = [];
18801888 try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch (_) { continue; }
18811889 for (const entry of entries) {
1882- if (!entry.isFile() || !/^kilo(?:-.+)?\.db$/i.test(entry.name)) continue;
1883- const full = path.join(root, entry.name);
1884- if (seen.has(full)) continue;
1885- seen.add(full);
1886- files.push(full);
1890+ if (!entry.isFile() || !/^(?:kilo|opencode)(?:-.+)?\.db$/i.test(entry.name)) continue;
1891+ push(path.join(root, entry.name));
18871892 }
18881893 }
18891894 return files;
@@ -5724,68 +5729,68 @@ function normalizeKiloCodeStructuredMessage(row) {
57245729 };
57255730}
57265731
5727- function runKiloCodeSqlitePython(dbPath, mode, options = {}) {
5732+ let g_kiloCodeSqliteBinding = undefined;
5733+
5734+ function loadKiloCodeSqliteBinding() {
5735+ if (g_kiloCodeSqliteBinding !== undefined) return g_kiloCodeSqliteBinding;
5736+ try {
5737+ g_kiloCodeSqliteBinding = require('better-sqlite3');
5738+ } catch (_) {
5739+ g_kiloCodeSqliteBinding = null;
5740+ }
5741+ return g_kiloCodeSqliteBinding;
5742+ }
5743+
5744+ function runKiloCodeSqlite(dbPath, mode, options = {}) {
57285745 if (!dbPath || !fs.existsSync(dbPath)) return null;
5729- const script = String.raw`
5730- import json, sqlite3, sys
5731-
5732- db_path, mode = sys.argv[1], sys.argv[2]
5733- session_id = sys.argv[3] if len(sys.argv) > 3 else ''
5734- limit = int(sys.argv[4]) if len(sys.argv) > 4 and sys.argv[4].isdigit() else 200
5735- conn = sqlite3.connect('file:' + db_path + '?mode=ro', uri=True)
5736- conn.row_factory = sqlite3.Row
5737- try:
5738- tables = {r['name'] for r in conn.execute("select name from sqlite_master where type='table'")}
5739- if 'session' not in tables or 'session_message' not in tables:
5740- print('[]')
5741- elif mode == 'list':
5742- rows = conn.execute("""
5743- select s.id, s.directory, s.path, s.title, s.model, s.cost,
5744- s.tokens_input, s.tokens_output, s.tokens_reasoning,
5745- s.tokens_cache_read, s.tokens_cache_write,
5746- s.time_created, s.time_updated,
5747- count(m.id) as message_count,
5748- max(coalesce(m.time_created, 0)) as last_message_time
5749- from session s
5750- left join session_message m on m.session_id = s.id
5751- where s.time_archived is null
5752- group by s.id
5753- order by max(coalesce(m.time_created, s.time_updated, s.time_created, 0)) desc
5754- limit ?
5755- """, (limit,)).fetchall()
5756- print(json.dumps([dict(r) for r in rows]))
5757- elif mode == 'messages':
5758- rows = conn.execute("""
5759- select id, session_id, type, seq, time_created, time_updated, data
5760- from session_message
5761- where session_id = ?
5762- order by coalesce(seq, time_created, 0), time_created, id
5763- """, (session_id,)).fetchall()
5764- print(json.dumps([dict(r) for r in rows]))
5765- elif mode == 'session':
5766- row = conn.execute("""
5767- select id, directory, path, title, model, cost,
5768- tokens_input, tokens_output, tokens_reasoning,
5769- tokens_cache_read, tokens_cache_write,
5770- time_created, time_updated
5771- from session
5772- where id = ?
5773- """, (session_id,)).fetchone()
5774- print(json.dumps(dict(row) if row else None))
5775- else:
5776- print('[]')
5777- finally:
5778- conn.close()
5779- `;
5746+ const Database = loadKiloCodeSqliteBinding();
5747+ if (!Database) return null;
5748+ let db;
57805749 try {
5781- const output = execFileSync('python3', ['-c', script, dbPath, mode, String(options.sessionId || ''), String(options.limit || 200)], {
5782- encoding: 'utf8',
5783- maxBuffer: 8 * 1024 * 1024,
5784- timeout: 15000
5785- });
5786- return JSON.parse(output || 'null');
5750+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
5751+ const tables = new Set(db.prepare("select name from sqlite_master where type='table'").all().map(row => row.name));
5752+ if (!tables.has('session') || !tables.has('session_message')) return [];
5753+ if (mode === 'list') {
5754+ return db.prepare(`
5755+ select s.id, s.directory, s.path, s.title, s.model, s.cost,
5756+ s.tokens_input, s.tokens_output, s.tokens_reasoning,
5757+ s.tokens_cache_read, s.tokens_cache_write,
5758+ s.time_created, s.time_updated,
5759+ count(m.id) as message_count,
5760+ max(coalesce(m.time_created, 0)) as last_message_time
5761+ from session s
5762+ left join session_message m on m.session_id = s.id and m.seq is not null
5763+ where s.time_archived is null
5764+ group by s.id
5765+ order by max(coalesce(m.time_created, s.time_updated, s.time_created, 0)) desc
5766+ limit ?
5767+ `).all(Math.max(1, Math.floor(Number(options.limit) || 200)));
5768+ }
5769+ if (mode === 'messages') {
5770+ return db.prepare(`
5771+ select id, session_id, type, seq, time_created, time_updated, data
5772+ from session_message
5773+ where session_id = ? and seq is not null
5774+ order by seq, time_created, id
5775+ `).all(String(options.sessionId || ''));
5776+ }
5777+ if (mode === 'session') {
5778+ return db.prepare(`
5779+ select id, directory, path, title, model, cost,
5780+ tokens_input, tokens_output, tokens_reasoning,
5781+ tokens_cache_read, tokens_cache_write,
5782+ time_created, time_updated
5783+ from session
5784+ where id = ?
5785+ `).get(String(options.sessionId || '')) || null;
5786+ }
5787+ return [];
57875788 } catch (_) {
57885789 return null;
5790+ } finally {
5791+ if (db) {
5792+ try { db.close(); } catch (_) { }
5793+ }
57895794 }
57905795}
57915796
@@ -5833,7 +5838,7 @@ function listKiloCodeDatabaseSessions(limit) {
58335838 const sessions = [];
58345839 const lookupStore = g_sessionFileLookupCache.kilocode;
58355840 for (const dbPath of getKiloCodeDatabaseFiles()) {
5836- const rows = runKiloCodeSqlitePython (dbPath, 'list', { limit: Math.max(limit * 2, 200) });
5841+ const rows = runKiloCodeSqlite (dbPath, 'list', { limit: Math.max(limit * 2, 200) });
58375842 if (!Array.isArray(rows)) continue;
58385843 for (const row of rows) {
58395844 const summary = toKiloCodeDbSessionSummary(row, dbPath);
@@ -5849,9 +5854,9 @@ function listKiloCodeDatabaseSessions(limit) {
58495854
58505855function readKiloCodeDatabaseSessionDetail(dbPath, sessionId, messageLimit = DEFAULT_SESSION_DETAIL_MESSAGES) {
58515856 if (!dbPath || !sessionId || !fs.existsSync(dbPath)) return null;
5852- const sessionRow = runKiloCodeSqlitePython (dbPath, 'session', { sessionId });
5857+ const sessionRow = runKiloCodeSqlite (dbPath, 'session', { sessionId });
58535858 if (!sessionRow || typeof sessionRow !== 'object') return null;
5854- const rows = runKiloCodeSqlitePython (dbPath, 'messages', { sessionId, limit: Math.max(messageLimit * 4, 200) });
5859+ const rows = runKiloCodeSqlite (dbPath, 'messages', { sessionId, limit: Math.max(messageLimit * 4, 200) });
58555860 if (!Array.isArray(rows)) return null;
58565861 const messages = rows.map(normalizeKiloCodeStructuredMessage).filter(Boolean);
58575862 const filtered = removeLeadingSystemMessage(messages);
0 commit comments