residoo 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +334 -46
  2. package/SECURITY.md +29 -22
  3. package/package.json +1 -1
  4. package/src/cli.js +249 -17
  5. package/src/integrity.js +689 -0
  6. package/src/patterns.js +78 -5
  7. package/src/report.js +188 -8
  8. package/src/rotation.js +834 -0
  9. package/src/sources/agent-configs.js +308 -0
  10. package/src/sources/aider.js +361 -0
  11. package/src/sources/amazon-q.js +199 -0
  12. package/src/sources/antigravity-cli.js +155 -0
  13. package/src/sources/cline.js +208 -0
  14. package/src/sources/codebuff.js +295 -0
  15. package/src/sources/codex-cli.js +258 -0
  16. package/src/sources/cody.js +325 -0
  17. package/src/sources/continue.js +408 -0
  18. package/src/sources/copilot-chat.js +272 -0
  19. package/src/sources/copilot-cli.js +300 -0
  20. package/src/sources/crush.js +364 -0
  21. package/src/sources/cursor.js +374 -0
  22. package/src/sources/devin-cli.js +241 -0
  23. package/src/sources/factory-droid.js +153 -0
  24. package/src/sources/fx.js +136 -0
  25. package/src/sources/gemini-cli.js +242 -0
  26. package/src/sources/goose.js +366 -0
  27. package/src/sources/grok-cli.js +267 -0
  28. package/src/sources/hermes.js +282 -0
  29. package/src/sources/index.js +172 -8
  30. package/src/sources/jetbrains-ai-assistant.js +343 -0
  31. package/src/sources/jetbrains-junie.js +292 -0
  32. package/src/sources/kilo-code.js +430 -0
  33. package/src/sources/kimi-code.js +147 -0
  34. package/src/sources/kiro-cli.js +393 -0
  35. package/src/sources/kiro-ide.js +230 -0
  36. package/src/sources/llm.js +328 -0
  37. package/src/sources/mentat.js +143 -0
  38. package/src/sources/open-interpreter.js +224 -0
  39. package/src/sources/openclaw.js +218 -0
  40. package/src/sources/opencode.js +379 -0
  41. package/src/sources/openhands.js +181 -0
  42. package/src/sources/pearai.js +151 -0
  43. package/src/sources/pi-agent.js +130 -0
  44. package/src/sources/project-artifacts.js +355 -0
  45. package/src/sources/qodo-gen.js +189 -0
  46. package/src/sources/qwen-code.js +244 -0
  47. package/src/sources/roo-code.js +239 -0
  48. package/src/sources/trae.js +294 -0
  49. package/src/sources/void.js +273 -0
  50. package/src/sources/warp.js +395 -0
  51. package/src/sources/windsurf.js +256 -0
  52. package/src/sources/zed.js +374 -0
@@ -0,0 +1,430 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const { createInterface } = require("readline/promises");
5
+ const path = require("path");
6
+ const os = require("os");
7
+
8
+ /**
9
+ * Kilo Code (VS Code extension, originally a Roo Code fork) session history.
10
+ *
11
+ * VERIFICATION STATUS AND WHY THIS FILE IS UNUSUAL: the brief for this
12
+ * cluster assumed Kilo Code's local storage would be "very likely
13
+ * near-identical" to Cline/Roo Code's, since it started as a Roo Code fork.
14
+ * That assumption turned out to be WRONG for the currently-shipped
15
+ * extension — Kilo Code has since been rebuilt on top of an embedded fork of
16
+ * OpenCode (sst/opencode; the vendored copy lives at packages/opencode in
17
+ * Kilo Code's own monorepo) and now persists sessions in a SQLite database
18
+ * outside VS Code's storage model entirely, not as per-task JSON files
19
+ * inside globalStorage. The OLD per-task-JSON-file layout still exists, but
20
+ * only as a migration SOURCE the current extension reads from and imports
21
+ * out of — not where new data is written. Both are handled below, because
22
+ * both are real: an install that hasn't been migrated yet (or was migrated
23
+ * without deleting the originals — nothing in the migration code path found
24
+ * during this source's research deletes the old files) can have real
25
+ * content in either place, or both.
26
+ *
27
+ * Sourced directly from Kilo Code's own current source and docs on GitHub
28
+ * (Kilo-Org/kilocode) during this source's research:
29
+ * - packages/kilo-vscode/package.json — `"name": "kilo-code"`,
30
+ * `"publisher": "kilocode"`, confirming the legacy on-disk globalStorage
31
+ * folder id `kilocode.kilo-code` is still current, unchanged by the
32
+ * OpenCode rewrite.
33
+ * - packages/kilo-docs/pages/code-with-ai/agents/session-history.md — this
34
+ * is Kilo Code's own *published, official* documentation, and states the
35
+ * new database's default path per OS verbatim in a table: macOS/Linux
36
+ * `~/.local/share/kilo/kilo.db`, Windows
37
+ * `%USERPROFILE%\.local\share\kilo\kilo.db` (yes, also a dotfile path on
38
+ * Windows — this is confirmed as real, current, and NOT a typo by an
39
+ * open upstream OpenCode issue, sst/opencode#8235, describing the exact
40
+ * same behavior: "Config and Data directories follow the Linux XDG
41
+ * standard even on windows"). The same doc gives the `session`,
42
+ * `message`, `part` table/column names used below (e.g. its own example
43
+ * query joins them and reads `json_extract(p.data, '$.text')`).
44
+ * - packages/core/src/global.ts — the actual construction of that path:
45
+ * `app = "kilo"`; `data = path.join(xdgData, app)`, where `xdgData` comes
46
+ * from the `xdg-basedir` npm package. That package (confirmed by reading
47
+ * ITS source directly) does NOT special-case macOS or Windows the way
48
+ * most XDG-inspired Node tools do — every platform falls back to
49
+ * `path.join(os.homedir(), ".local", "share")` when `$XDG_DATA_HOME`
50
+ * isn't set. This is exactly what the official doc above independently
51
+ * confirms, and exactly the kind of easy-to-get-wrong-by-assumption
52
+ * detail this project's verification rule exists for.
53
+ * - packages/opencode/src/storage/db.ts — `getChannelPath()`: the database
54
+ * filename is `kilo.db` for the "latest"/"beta"/"prod" install channels
55
+ * (what a normal Marketplace install uses), or `kilo-<channel>.db` for
56
+ * any other channel (nightly/dev builds), with a fallback check for a
57
+ * same-named `opencode-<channel>.db` left over from before the
58
+ * product's own rename. `db.node.ts` confirms the VS Code extension
59
+ * itself opens this via `node:sqlite`'s `DatabaseSync` — the same
60
+ * built-in module cursor.js already relies on, not a new dependency.
61
+ * - packages/core/src/session/sql.ts — the Drizzle table definitions used
62
+ * to build the table list below.
63
+ * - Kilo-Org/kilocode-legacy (an archived, but real, sibling repo Kilo-Org
64
+ * split the pre-rewrite extension into) — its own
65
+ * docs/legacy-ides/getting-started/file-locations.md documents the
66
+ * legacy globalStorage path per OS, and its
67
+ * src/shared/globalFileNames.ts (real source, not a guess) gives the
68
+ * legacy per-task filenames, which match Roo Code's naming exactly (Kilo
69
+ * Code's legacy extension was, at the source level, a Roo Code fork).
70
+ *
71
+ * What this could NOT be checked against: a real Kilo Code install on the
72
+ * machine this source was built on — VS Code itself isn't installed there.
73
+ * Despite the unusually deep source/docs corroboration above, treat findings
74
+ * from this source accordingly until someone with Kilo Code actually
75
+ * installed confirms it against real data (in particular: confirm which of
76
+ * the two formats below actually has content for a given real install, and
77
+ * whether `kilo db path` on that machine agrees with the default path this
78
+ * source assumes).
79
+ *
80
+ * ---- Format 1: the new SQLite database (current, primary) ----
81
+ * <XDG data dir>/kilo/kilo.db (or kilo-<channel>.db / opencode-<channel>.db)
82
+ * Tables scanned: session, message, part, session_message, session_input,
83
+ * todo — the ones that can hold actual conversation/task text. Deliberately
84
+ * NOT scanned: account / account_state / control_account (Kilo's own stored
85
+ * provider credentials — a different concern than a transcript source, and
86
+ * out of scope the same way claude-code.js never reads Claude Code's own
87
+ * settings/credentials files outside ~/.claude/projects), project,
88
+ * session_share, workspace (control-plane/identity records, not transcript
89
+ * content). This mirrors how claude-code.js scopes itself to the transcripts
90
+ * directory rather than all of ~/.claude.
91
+ *
92
+ * ---- Format 2: legacy per-task JSON files (migration source only) ----
93
+ * <VS Code User dir>/globalStorage/kilocode.kilo-code/tasks/<taskId>/
94
+ * api_conversation_history.json / ui_messages.json / task_metadata.json
95
+ * Same VS Code User dir scope (standard + Insiders only, see cline.js/
96
+ * roo-code.js for the same named limitation) and the same "glob every *.json
97
+ * in the task dir rather than allow-list filenames" reasoning as those two
98
+ * sibling sources.
99
+ */
100
+ const LEGACY_EXT_ID = "kilocode.kilo-code";
101
+ const DB_NAME_RE = /^(kilo|opencode)(-[A-Za-z0-9._-]+)?\.db$/;
102
+
103
+ function vscodeUserDirs() {
104
+ const home = os.homedir();
105
+ const variants = ["Code", "Code - Insiders"];
106
+ if (process.platform === "darwin") {
107
+ return variants.map((v) => path.join(home, "Library", "Application Support", v, "User"));
108
+ }
109
+ if (process.platform === "win32") {
110
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
111
+ return variants.map((v) => path.join(appData, v, "User"));
112
+ }
113
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
114
+ return variants.map((v) => path.join(configHome, v, "User"));
115
+ }
116
+
117
+ function legacyTasksDirs() {
118
+ return vscodeUserDirs().map((userDir) => path.join(userDir, "globalStorage", LEGACY_EXT_ID, "tasks"));
119
+ }
120
+
121
+ /**
122
+ * `xdg-basedir`'s own behavior (verified by reading its source): every
123
+ * platform, not just Linux, falls back to `~/.local/share` when
124
+ * `$XDG_DATA_HOME` isn't set — there is no macOS/Windows special case. See
125
+ * the docstring above for the corroborating official Kilo Code doc and
126
+ * upstream OpenCode issue.
127
+ */
128
+ function kiloDataDir() {
129
+ const home = os.homedir();
130
+ const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
131
+ return path.join(xdgDataHome, "kilo");
132
+ }
133
+
134
+ // Bounds for the legacy JSON files — same rationale/values as claude-code.js.
135
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
136
+ const READ_TIMEOUT_MS = 60_000;
137
+
138
+ // Bounds for the SQLite database — same rationale/values as cursor.js: not
139
+ // backed by a real kilo.db this tool was tested against, only a generous
140
+ // backstop.
141
+ const MAX_DB_BYTES = 512 * 1024 * 1024;
142
+ const DB_READ_TIMEOUT_MS = 60_000;
143
+ const BUSY_TIMEOUT_MS = 5_000;
144
+ const YIELD_EVERY_N_ROWS = 500;
145
+ const SESSION_TABLES = ["session", "message", "part", "session_message", "session_input", "todo"];
146
+
147
+ /**
148
+ * node:sqlite, loaded lazily — same reasoning as cursor.js: it's a Node core
149
+ * module (no package.json/lockfile footprint, satisfies CONTRIBUTING.md rule
150
+ * 1), but require()-ing it eagerly would print Node's ExperimentalWarning to
151
+ * stderr on every `residoo scan` for every user, even the large majority who
152
+ * have never touched Kilo Code. Deferred until there is already a concrete
153
+ * reason to make the require (Kilo's own data directory actually exists).
154
+ */
155
+ const NODE_SQLITE_REQUIREMENT = "needs Node.js 22.5+ (node:sqlite not present in this runtime)";
156
+ let sqliteRequireAttempted = false;
157
+ let DatabaseSync = null;
158
+ function getDatabaseSync() {
159
+ if (!sqliteRequireAttempted) {
160
+ sqliteRequireAttempted = true;
161
+ try { ({ DatabaseSync } = require("node:sqlite")); }
162
+ catch { DatabaseSync = null; }
163
+ }
164
+ return DatabaseSync;
165
+ }
166
+
167
+ function id() { return "kilo-code"; }
168
+ function label() { return "Kilo Code"; }
169
+
170
+ function legacyDirExists() {
171
+ return legacyTasksDirs().some((dir) => {
172
+ try { return fs.statSync(dir).isDirectory(); } catch { return false; }
173
+ });
174
+ }
175
+
176
+ function dbCandidates() {
177
+ const dataDir = kiloDataDir();
178
+ let entries;
179
+ try { entries = fs.readdirSync(dataDir, { withFileTypes: true }); }
180
+ catch { return []; }
181
+ return entries
182
+ .filter((e) => (e.isFile() || e.isSymbolicLink()) && DB_NAME_RE.test(e.name))
183
+ .map((e) => path.join(dataDir, e.name));
184
+ }
185
+
186
+ /**
187
+ * Available whenever there is at least one thing this source could attempt
188
+ * to scan. The legacy JSON path needs nothing beyond plain fs access; the
189
+ * new SQLite path additionally needs node:sqlite to be loadable. This is
190
+ * deliberately looser than cursor.js's available() (which is all-or-nothing
191
+ * because Cursor's ENTIRE content is SQLite-only) — here the legacy format
192
+ * alone is real, scannable content even when node:sqlite is unavailable.
193
+ */
194
+ function available() {
195
+ if (legacyDirExists()) return true;
196
+ const candidates = dbCandidates();
197
+ return candidates.length > 0 && Boolean(getDatabaseSync());
198
+ }
199
+
200
+ /**
201
+ * Same purpose as cursor.js's unavailableReason(): the one case worth
202
+ * calling out by name is "a kilo.db was found but this Node runtime can't
203
+ * open it and there's no legacy data either" — otherwise this source would
204
+ * silently vanish from "Sources checked," reading as "Kilo Code isn't
205
+ * installed," which would be false.
206
+ */
207
+ function unavailableReason() {
208
+ if (legacyDirExists()) return null; // available() already covers this source correctly
209
+ const candidates = dbCandidates();
210
+ if (candidates.length === 0) return null; // ordinary "Kilo Code just isn't here" case
211
+ if (getDatabaseSync()) return null;
212
+ return `Kilo Code detected but not scanned — ${NODE_SQLITE_REQUIREMENT}`;
213
+ }
214
+
215
+ /**
216
+ * Same defensive symlink-following pattern as claude-code.js's
217
+ * isDirFollowingSymlink/isFileFollowingSymlink — see that file's docstring
218
+ * for the full reasoning. Duplicated rather than imported: each source in
219
+ * this project is meant to be a small, self-contained file a reviewer can
220
+ * audit on its own (see CONTRIBUTING.md and cursor.js's own note on this).
221
+ */
222
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
223
+ if (checkFn(dirent)) return true;
224
+ if (!dirent.isSymbolicLink()) return false;
225
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
226
+ }
227
+ const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
228
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
229
+
230
+ function* legacyFiles() {
231
+ for (const tasksDir of legacyTasksDirs()) {
232
+ let taskEntries;
233
+ try { taskEntries = fs.readdirSync(tasksDir, { withFileTypes: true }); }
234
+ catch { continue; } // this VS Code variant/profile has no legacy Kilo Code tasks dir — normal, not broken
235
+
236
+ for (const taskEntry of taskEntries) {
237
+ const taskDir = path.join(tasksDir, taskEntry.name);
238
+ if (!isDirFollowingSymlink(taskDir, taskEntry)) {
239
+ if (taskEntry.isSymbolicLink()) yield { file: taskDir, broken: true };
240
+ continue;
241
+ }
242
+
243
+ let fileEntries;
244
+ try { fileEntries = fs.readdirSync(taskDir, { withFileTypes: true }); }
245
+ catch { yield { file: taskDir, broken: true }; continue; }
246
+
247
+ for (const e of fileEntries) {
248
+ if (!e.name.endsWith(".json")) continue;
249
+ const file = path.join(taskDir, e.name);
250
+ if (!isFileFollowingSymlink(file, e)) {
251
+ if (e.isSymbolicLink()) yield { file, broken: true };
252
+ continue;
253
+ }
254
+ let stat;
255
+ try { stat = fs.statSync(file); } catch { yield { file, broken: true }; continue; }
256
+ yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Yield the kilo.db candidate(s) directly inside the Kilo data directory.
264
+ * Uses lstat directly (not a Dirent) because dbCandidates() already resolved
265
+ * the directory listing; this just re-stats each already-known path right
266
+ * before scan-time, same TOCTOU-narrowing rationale cursor.js's
267
+ * statIfPresent gives for globalStorage/state.vscdb.
268
+ */
269
+ function* dbFiles() {
270
+ for (const dbPath of dbCandidates()) {
271
+ let lst;
272
+ try { lst = fs.lstatSync(dbPath); }
273
+ catch { continue; } // vanished between the directory listing above and now
274
+
275
+ if (lst.isSymbolicLink()) {
276
+ try {
277
+ const st = fs.statSync(dbPath);
278
+ if (!st.isFile()) { yield { file: dbPath, broken: true }; continue; }
279
+ yield { file: dbPath, mtimeMs: st.mtimeMs, sizeBytes: st.size, broken: false };
280
+ } catch {
281
+ yield { file: dbPath, broken: true }; // dangling symlink
282
+ }
283
+ continue;
284
+ }
285
+
286
+ if (!lst.isFile()) continue;
287
+ yield { file: dbPath, mtimeMs: lst.mtimeMs, sizeBytes: lst.size, broken: false };
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Yield { file, mtimeMs, sizeBytes, broken } for every legacy per-task *.json
293
+ * file AND every kilo*.db candidate. readLines() below tells the two apart
294
+ * by file extension. broken:true marks an entry that looked like it should
295
+ * resolve (chiefly a dangling symlink) but didn't — never silently skipped,
296
+ * same convention as every other source in this project.
297
+ */
298
+ function* files() {
299
+ yield* legacyFiles();
300
+ yield* dbFiles();
301
+ }
302
+
303
+ /**
304
+ * Read one legacy per-task JSON file as an array of raw text lines. Kilo
305
+ * Code's legacy extension (a Roo Code fork at the source level) writes these
306
+ * with real indentation/newlines, same as roo-code.js's readLines() — see
307
+ * that file's docstring for the full rationale; this is the same
308
+ * implementation.
309
+ */
310
+ async function readJsonLines(file) {
311
+ let stat;
312
+ try { stat = fs.statSync(file); }
313
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
314
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
315
+
316
+ const lines = [];
317
+ let bytesRead = 0;
318
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
319
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
320
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
321
+
322
+ try {
323
+ for await (const line of rl) {
324
+ lines.push(line);
325
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1;
326
+ }
327
+ return { lines, status: "complete", bytesRead };
328
+ } catch {
329
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
330
+ } finally {
331
+ clearTimeout(timer);
332
+ rl.close();
333
+ stream.destroy();
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Read one kilo.db as an array of raw text "lines" — one per row, across
339
+ * SESSION_TABLES. Same synchronous-SQLite/no-preemptive-timeout situation
340
+ * cursor.js documents at length for state.vscdb: node:sqlite's
341
+ * DatabaseSync/StatementSync are fully synchronous, so this iterates
342
+ * row-by-row via StatementSync#iterate() and yields to the event loop (plus
343
+ * checks a wall-clock deadline) every YIELD_EVERY_N_ROWS rows.
344
+ *
345
+ * Each row becomes `JSON.stringify(row)` — the whole row, every column, not
346
+ * just the JSON-encoded `data` column most of these tables have — because
347
+ * unlike cursor.js's single opaque `value` column, several columns here
348
+ * (session.title, session.directory, todo.content, ...) can independently
349
+ * hold real text with no single column to special-case. This is the same
350
+ * "turn each row into one line via JSON.stringify" approach this project's
351
+ * own adapter contract describes for non-line-delimited sources. The one
352
+ * named cost: JSON.stringify re-escapes the already-JSON-encoded `data`
353
+ * column's own quotes/backslashes as it nests that string inside the outer
354
+ * object — harmless for the vendor-prefixed literal token shapes this
355
+ * project's PATTERNS match (e.g. `sk-ant-…`, `AKIA…`), since none of them
356
+ * depend on quote-adjacency, but worth naming rather than leaving implicit.
357
+ */
358
+ async function readDbLines(file) {
359
+ const DB = getDatabaseSync();
360
+ if (!DB) return { lines: [], status: "failed", bytesRead: 0 };
361
+
362
+ let stat;
363
+ try { stat = fs.statSync(file); }
364
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
365
+ if (stat.size > MAX_DB_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
366
+
367
+ let db;
368
+ try {
369
+ db = new DB(file, { readOnly: true });
370
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
371
+ } catch {
372
+ // Deleted between files() and this call, a corrupt/non-SQLite file at
373
+ // this path, or Kilo Code holding a lock this readonly open can't get
374
+ // past within BUSY_TIMEOUT_MS — genuinely "could not read this."
375
+ return { lines: [], status: "failed", bytesRead: 0 };
376
+ }
377
+
378
+ const lines = [];
379
+ let bytesRead = 0;
380
+ const deadline = Date.now() + DB_READ_TIMEOUT_MS;
381
+ let timedOut = false;
382
+ let sawError = false;
383
+ let foundAnyTable = false;
384
+
385
+ for (const table of SESSION_TABLES) {
386
+ let rows;
387
+ try {
388
+ rows = db.prepare(`SELECT * FROM ${table}`).iterate();
389
+ } catch {
390
+ // This particular table genuinely doesn't exist in this file's schema
391
+ // (older/newer Kilo Code version) — not a read failure for the other
392
+ // tables, so just move on rather than aborting the whole file.
393
+ continue;
394
+ }
395
+ foundAnyTable = true;
396
+
397
+ let n = 0;
398
+ try {
399
+ for (const row of rows) {
400
+ let text;
401
+ try { text = JSON.stringify(row); } catch { text = null; }
402
+ if (text) { lines.push(text); bytesRead += Buffer.byteLength(text, "utf-8"); }
403
+ n++;
404
+ if (n % YIELD_EVERY_N_ROWS === 0) {
405
+ await new Promise((resolve) => setImmediate(resolve));
406
+ if (Date.now() > deadline) { timedOut = true; break; }
407
+ }
408
+ }
409
+ } catch {
410
+ // A row iterator can itself throw partway (e.g. a corrupted page hit
411
+ // mid-scan) — whatever WAS read before that is real content, kept the
412
+ // same way claude-code.js/cursor.js keep a partial read.
413
+ sawError = true;
414
+ }
415
+ if (timedOut) break;
416
+ }
417
+
418
+ try { db.close(); } catch { /* best-effort close */ }
419
+
420
+ if (!foundAnyTable) return { lines: [], status: "failed", bytesRead: 0 };
421
+ if (sawError && lines.length === 0) return { lines: [], status: "failed", bytesRead };
422
+ if (timedOut || sawError) return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
423
+ return { lines, status: "complete", bytesRead };
424
+ }
425
+
426
+ async function readLines(file) {
427
+ return DB_NAME_RE.test(path.basename(file)) ? readDbLines(file) : readJsonLines(file);
428
+ }
429
+
430
+ module.exports = { id, label, available, unavailableReason, files, readLines };
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const { createInterface } = require("readline/promises");
5
+ const path = require("path");
6
+ const os = require("os");
7
+
8
+ /**
9
+ * Moonshot AI's Kimi Code CLI (github.com/MoonshotAI/kimi-code, 7,200+
10
+ * stars; also mirrored/aliased as github.com/MoonshotAI/kimi-cli, 11,300+
11
+ * stars) local session transcripts.
12
+ *
13
+ * VERIFICATION STATUS: read directly from the actual, current shipped source
14
+ * code of the MoonshotAI/kimi-code monorepo (fetched from GitHub during this
15
+ * source's research, not inferred from docs or a blog post) — but NOT
16
+ * checked against a real install on the machine this source was built on (no
17
+ * `~/.kimi-code` directory exists there; see CONTRIBUTING.md). This is the
18
+ * same confidence tier as cursor.js's SQLite-schema verification: real
19
+ * source, not a real install.
20
+ *
21
+ * The chain of evidence, file by file, all from MoonshotAI/kimi-code@main:
22
+ * - `packages/agent-core/src/config/path.ts`:
23
+ * `resolveKimiHome()` returns `$KIMI_CODE_HOME`, else
24
+ * `join(homedir(), '.kimi-code')` — i.e. ROOT below.
25
+ * - `apps/kimi-code/src/cli/telemetry.ts`'s `createCliTelemetryBootstrap()`
26
+ * calls `resolveKimiHome()` for the `homeDir` the CLI actually wires
27
+ * into its session store (via `apps/kimi-code/src/cli/sub/session.ts`'s
28
+ * `createDefaultSessionListDeps()`), confirming this is the real runtime
29
+ * value, not just an unused helper.
30
+ * - `packages/agent-core/src/session/store/session-store.ts`'s
31
+ * `SessionStore` class: `this.sessionsDir = join(homeDir, 'sessions')`,
32
+ * and each session's directory is
33
+ * `sessionsDir/<bucket>/<sessionId>` where `<bucket>` comes from
34
+ * `encodeWorkDirKey()` (`packages/agent-core/src/session/store/
35
+ * workdir-key.ts`): `wd_<slug>_<sha256(workdir).slice(0,12)>`.
36
+ * - `packages/agent-core/src/services/message/transcript.ts`'s own
37
+ * docstring: "rebuilds the FULL message history of a session agent from
38
+ * its `wire.jsonl` record log... The wire log... keeps every record,"
39
+ * confirming `wire.jsonl` (via `FileSystemAgentRecordPersistence`, an
40
+ * append-only JSONL writer in `packages/agent-core/src/agent/records/
41
+ * persistence.ts`) as the actual on-disk transcript file inside each
42
+ * session directory.
43
+ *
44
+ * Full path: `~/.kimi-code/sessions/<bucket>/<sessionId>/wire.jsonl`. This
45
+ * source does not attempt to recompute `<bucket>` (the slug+hash scheme
46
+ * above) — like claude-code.js not decoding Claude Code's own project-slug
47
+ * naming, it simply walks the tree recursively for `wire.jsonl`, which needs
48
+ * no knowledge of how the intermediate directory names are derived.
49
+ *
50
+ * Deliberately not honored: the `KIMI_CODE_HOME` environment-variable
51
+ * override confirmed above. Neither claude-code.js nor cursor.js chases an
52
+ * equivalent override for its own tool, and the plain default path is what
53
+ * the overwhelming majority of installs actually use.
54
+ */
55
+ const HOME = os.homedir();
56
+ const KIMI_HOME = path.join(HOME, ".kimi-code");
57
+ const SESSIONS_ROOT = path.join(KIMI_HOME, "sessions");
58
+
59
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB — same backstop as claude-code.js.
60
+ const READ_TIMEOUT_MS = 60_000;
61
+ const MAX_WALK_DEPTH = 8;
62
+
63
+ function id() { return "kimi-code"; }
64
+ function label() { return "Kimi Code"; }
65
+
66
+ function available() {
67
+ try { return fs.statSync(SESSIONS_ROOT).isDirectory(); } catch { return false; }
68
+ }
69
+
70
+ /**
71
+ * Same defensive symlink-following helpers as claude-code.js — see that
72
+ * file's docstring. Duplicated rather than imported, per this project's
73
+ * self-contained-source-file convention (see cursor.js's docstring).
74
+ */
75
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
76
+ if (checkFn(dirent)) return true;
77
+ if (!dirent.isSymbolicLink()) return false;
78
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
79
+ }
80
+ const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
81
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
82
+
83
+ /**
84
+ * Recursively yield { file, mtimeMs, sizeBytes, broken } for every plain file
85
+ * under `dir` whose name passes `matchFn` — see factory-droid.js's walk()
86
+ * for the identical reasoning.
87
+ */
88
+ function* walk(dir, depth, matchFn) {
89
+ if (depth > MAX_WALK_DEPTH) return;
90
+ let entries;
91
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
92
+ catch { return; }
93
+
94
+ for (const e of entries) {
95
+ const full = path.join(dir, e.name);
96
+ if (isDirFollowingSymlink(full, e)) {
97
+ yield* walk(full, depth + 1, matchFn);
98
+ continue;
99
+ }
100
+ const isFile = isFileFollowingSymlink(full, e);
101
+ if (!isFile) {
102
+ if (e.isSymbolicLink()) yield { file: full, broken: true };
103
+ continue;
104
+ }
105
+ if (!matchFn(e.name)) continue;
106
+ let stat;
107
+ try { stat = fs.statSync(full); } catch { yield { file: full, broken: true }; continue; }
108
+ yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
109
+ }
110
+ }
111
+
112
+ function* files() {
113
+ yield* walk(SESSIONS_ROOT, 0, (name) => name === "wire.jsonl");
114
+ }
115
+
116
+ /**
117
+ * Read one wire.jsonl transcript as raw text lines. Identical streaming/
118
+ * timeout/partial-read discipline to claude-code.js's readLines().
119
+ */
120
+ async function readLines(file) {
121
+ let stat;
122
+ try { stat = fs.statSync(file); }
123
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
124
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
125
+
126
+ const lines = [];
127
+ let bytesRead = 0;
128
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
129
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
130
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
131
+
132
+ try {
133
+ for await (const line of rl) {
134
+ lines.push(line);
135
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1;
136
+ }
137
+ return { lines, status: "complete", bytesRead };
138
+ } catch {
139
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
140
+ } finally {
141
+ clearTimeout(timer);
142
+ rl.close();
143
+ stream.destroy();
144
+ }
145
+ }
146
+
147
+ module.exports = { id, label, available, files, readLines };