Skip to content

Commit ac97001

Browse files
committed
feat(sessions): use npm SQLite reader for KiloCode
1 parent 5cab7f9 commit ac97001

5 files changed

Lines changed: 824 additions & 89 deletions

File tree

cli.js

Lines changed: 71 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1875,15 +1875,20 @@ function getKiloCodeDataRoots() {
18751875
function 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

58505855
function 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

Comments
 (0)