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.
- package/README.md +334 -46
- package/SECURITY.md +29 -22
- package/package.json +1 -1
- package/src/cli.js +249 -17
- package/src/integrity.js +689 -0
- package/src/patterns.js +78 -5
- package/src/report.js +188 -8
- package/src/rotation.js +834 -0
- package/src/sources/agent-configs.js +308 -0
- package/src/sources/aider.js +361 -0
- package/src/sources/amazon-q.js +199 -0
- package/src/sources/antigravity-cli.js +155 -0
- package/src/sources/cline.js +208 -0
- package/src/sources/codebuff.js +295 -0
- package/src/sources/codex-cli.js +258 -0
- package/src/sources/cody.js +325 -0
- package/src/sources/continue.js +408 -0
- package/src/sources/copilot-chat.js +272 -0
- package/src/sources/copilot-cli.js +300 -0
- package/src/sources/crush.js +364 -0
- package/src/sources/cursor.js +374 -0
- package/src/sources/devin-cli.js +241 -0
- package/src/sources/factory-droid.js +153 -0
- package/src/sources/fx.js +136 -0
- package/src/sources/gemini-cli.js +242 -0
- package/src/sources/goose.js +366 -0
- package/src/sources/grok-cli.js +267 -0
- package/src/sources/hermes.js +282 -0
- package/src/sources/index.js +172 -8
- package/src/sources/jetbrains-ai-assistant.js +343 -0
- package/src/sources/jetbrains-junie.js +292 -0
- package/src/sources/kilo-code.js +430 -0
- package/src/sources/kimi-code.js +147 -0
- package/src/sources/kiro-cli.js +393 -0
- package/src/sources/kiro-ide.js +230 -0
- package/src/sources/llm.js +328 -0
- package/src/sources/mentat.js +143 -0
- package/src/sources/open-interpreter.js +224 -0
- package/src/sources/openclaw.js +218 -0
- package/src/sources/opencode.js +379 -0
- package/src/sources/openhands.js +181 -0
- package/src/sources/pearai.js +151 -0
- package/src/sources/pi-agent.js +130 -0
- package/src/sources/project-artifacts.js +355 -0
- package/src/sources/qodo-gen.js +189 -0
- package/src/sources/qwen-code.js +244 -0
- package/src/sources/roo-code.js +239 -0
- package/src/sources/trae.js +294 -0
- package/src/sources/void.js +273 -0
- package/src/sources/warp.js +395 -0
- package/src/sources/windsurf.js +256 -0
- 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 };
|