residoo 0.1.0 → 0.2.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 (50) hide show
  1. package/README.md +225 -46
  2. package/SECURITY.md +29 -22
  3. package/package.json +1 -1
  4. package/src/cli.js +82 -16
  5. package/src/integrity.js +669 -0
  6. package/src/patterns.js +78 -5
  7. package/src/report.js +74 -7
  8. package/src/sources/agent-configs.js +308 -0
  9. package/src/sources/aider.js +361 -0
  10. package/src/sources/amazon-q.js +199 -0
  11. package/src/sources/antigravity-cli.js +155 -0
  12. package/src/sources/cline.js +208 -0
  13. package/src/sources/codebuff.js +295 -0
  14. package/src/sources/codex-cli.js +258 -0
  15. package/src/sources/cody.js +325 -0
  16. package/src/sources/continue.js +408 -0
  17. package/src/sources/copilot-chat.js +272 -0
  18. package/src/sources/copilot-cli.js +300 -0
  19. package/src/sources/crush.js +364 -0
  20. package/src/sources/cursor.js +374 -0
  21. package/src/sources/devin-cli.js +241 -0
  22. package/src/sources/factory-droid.js +153 -0
  23. package/src/sources/fx.js +136 -0
  24. package/src/sources/gemini-cli.js +242 -0
  25. package/src/sources/goose.js +366 -0
  26. package/src/sources/grok-cli.js +267 -0
  27. package/src/sources/hermes.js +282 -0
  28. package/src/sources/index.js +172 -8
  29. package/src/sources/jetbrains-ai-assistant.js +343 -0
  30. package/src/sources/jetbrains-junie.js +292 -0
  31. package/src/sources/kilo-code.js +430 -0
  32. package/src/sources/kimi-code.js +147 -0
  33. package/src/sources/kiro-cli.js +393 -0
  34. package/src/sources/kiro-ide.js +230 -0
  35. package/src/sources/llm.js +328 -0
  36. package/src/sources/mentat.js +143 -0
  37. package/src/sources/open-interpreter.js +224 -0
  38. package/src/sources/openclaw.js +218 -0
  39. package/src/sources/opencode.js +379 -0
  40. package/src/sources/openhands.js +181 -0
  41. package/src/sources/pearai.js +151 -0
  42. package/src/sources/pi-agent.js +130 -0
  43. package/src/sources/qodo-gen.js +189 -0
  44. package/src/sources/qwen-code.js +244 -0
  45. package/src/sources/roo-code.js +239 -0
  46. package/src/sources/trae.js +294 -0
  47. package/src/sources/void.js +273 -0
  48. package/src/sources/warp.js +395 -0
  49. package/src/sources/windsurf.js +256 -0
  50. package/src/sources/zed.js +374 -0
@@ -0,0 +1,393 @@
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
+ * Kiro CLI (AWS/Kiro's terminal coding agent, binary `kiro-cli` / `kiro`)
10
+ * session history. Distinct from Kiro IDE (see kiro-ide.js) — same product
11
+ * family, different application, different storage.
12
+ *
13
+ * VERIFICATION STATUS (read this before trusting anything below): the
14
+ * general shape ("SQLite database in ~/.kiro/", per-directory scoping) is
15
+ * confirmed directly from Kiro's own official docs
16
+ * (https://kiro.dev/docs/cli/chat/session-management/, "Technical details"
17
+ * section — fetched as raw HTML and grepped for the literal text). That
18
+ * page turns out to be imprecise about the exact path, though (see below) —
19
+ * caught only by going one step further to a real, actively-maintained,
20
+ * read-only community tool built specifically to read this data:
21
+ * github.com/prabhugr/kiro-cli-history. Its own README states plainly "This
22
+ * tool never writes to or modifies your Kiro CLI session data" and opens
23
+ * the database "in read-only mode" — and this source fetched and read its
24
+ * actual Python source (`kiro_history.py`), not just its README prose, the
25
+ * same standard cursor.js's own research applied to its community source.
26
+ * This has NOT been checked against a real Kiro CLI install — it is not
27
+ * installed on the machine this adapter was built on (checked: no `kiro` or
28
+ * `kiro-cli` on PATH, no `~/.kiro` directory). If you have Kiro CLI
29
+ * installed, the most useful thing you can do is run `residoo scan` and
30
+ * confirm `sourcesScanned`/`filesScanned` look right for what you know is
31
+ * actually on disk, then report back either way.
32
+ *
33
+ * THREE STORAGE FORMATS. `kiro_history.py`'s own module docstring names
34
+ * three, and its actual path constants and SQL confirm each one, quoted
35
+ * near-verbatim:
36
+ *
37
+ * 1. v3 (JSONL, used by `kiro-cli --classic`):
38
+ * `~/.kiro/sessions/cli/<session-id>.json` (metadata: session_id,
39
+ * title, cwd, created_at, updated_at) plus a companion
40
+ * `~/.kiro/sessions/cli/<session-id>.jsonl` (the actual conversation —
41
+ * one JSON record per line, each with a `kind` field — `"Prompt"` or
42
+ * `"AssistantMessage"` observed — and the message text nested at
43
+ * `data.content[].data` where `data.content[].kind === "text"`).
44
+ * Both files sit FLAT directly in `sessions/cli/`, not nested per
45
+ * project — confirmed directly from source (`SESSIONS_DIR.glob("*.json")`,
46
+ * a non-recursive glob).
47
+ * 2. v2 (SQLite, used by the current default TUI mode):
48
+ * `~/Library/Application Support/kiro-cli/data.sqlite3` (macOS path —
49
+ * this specific tool is macOS-only per its own README), table
50
+ * `conversations_v2` — confirmed from its actual query:
51
+ * `SELECT key, conversation_id, value, created_at, updated_at FROM
52
+ * conversations_v2 ORDER BY updated_at DESC`, where `value` is a JSON
53
+ * blob holding the conversation.
54
+ * 3. v1 (SQLite, legacy): same database file, table `conversations` —
55
+ * confirmed from source: `SELECT key, value FROM conversations`, where
56
+ * `value` is again a JSON blob (`conversation_id` is read back out of
57
+ * the parsed JSON itself here, not a separate column — the schema
58
+ * visibly grew a dedicated column between v1 and v2).
59
+ *
60
+ * That a real, working tool had to reverse-engineer THREE evolving formats
61
+ * to stay useful is itself the reason this source discovers tables from
62
+ * `sqlite_master` at scan time (see readSqliteLines() below) rather than
63
+ * hardcoding only `conversations`/`conversations_v2` — the same schema-drift
64
+ * reasoning warp.js documents at length for a comparable situation. A v3-era
65
+ * SQLite table this source doesn't yet know the name of would still get
66
+ * scanned.
67
+ *
68
+ * WHERE KIRO'S OWN DOCS ARE IMPRECISE: they say simply "SQLite database in
69
+ * ~/.kiro/" — true of the JSONL sessions (which really do live under
70
+ * `~/.kiro/`) but not of the SQLite database itself, which in fact lives in
71
+ * the OS-native local-data directory, a level of precision only the
72
+ * community tool's actual, working source code provided. This is exactly
73
+ * the class of error CONTRIBUTING.md's guessed-path warning describes,
74
+ * caught the same way this project caught an analogous imprecision in a
75
+ * secondary source for Crush (see crush.js's docstring) — by going to a
76
+ * source that had to actually work against the real file, not just describe
77
+ * it.
78
+ *
79
+ * PER-OS SQLITE PATH: only the macOS path above is independently confirmed
80
+ * (kiro-cli-history is explicitly macOS-only). Linux and Windows below are
81
+ * filled in by structural analogy to the OS-native "local data directory"
82
+ * convention (Rust's `dirs::data_local_dir()`) — the exact convention this
83
+ * source separately confirmed, from actual source
84
+ * (`crates/chat-cli/src/util/paths.rs`), that the sibling AWS product Q
85
+ * Developer CLI uses for its own `<data-dir>/amazon-q/data.sqlite3` (Kiro
86
+ * CLI is documented — see kiro.dev's own CLI intro — as building on Q
87
+ * Developer CLI's agent engine, with `amazon-q` swapped for `kiro-cli` in
88
+ * exactly the position confirmed on macOS). Flagged as analogy, not
89
+ * independently quoted, same honesty standard as this project's other
90
+ * per-OS gaps (e.g. warp.js's Preview-channel filename).
91
+ */
92
+ function homeDir() { return os.homedir(); }
93
+
94
+ const KIRO_HOME = path.join(homeDir(), ".kiro");
95
+ const JSONL_SESSIONS_DIR = path.join(KIRO_HOME, "sessions", "cli");
96
+
97
+ function sqliteDbPath() {
98
+ if (process.platform === "darwin") {
99
+ return path.join(homeDir(), "Library", "Application Support", "kiro-cli", "data.sqlite3");
100
+ }
101
+ if (process.platform === "win32") {
102
+ const localAppData = process.env.LOCALAPPDATA || path.join(homeDir(), "AppData", "Local");
103
+ return path.join(localAppData, "kiro-cli", "data.sqlite3");
104
+ }
105
+ // Linux and other XDG-following unix platforms — not independently
106
+ // confirmed for Kiro CLI specifically; see module docstring.
107
+ const xdgDataHome = process.env.XDG_DATA_HOME || path.join(homeDir(), ".local", "share");
108
+ return path.join(xdgDataHome, "kiro-cli", "data.sqlite3");
109
+ }
110
+
111
+ const SQLITE_DB = sqliteDbPath();
112
+
113
+ /**
114
+ * Same lazy-require, feature-detected node:sqlite pattern as cursor.js,
115
+ * crush.js, and warp.js — see cursor.js's docstring for the full reasoning.
116
+ * Duplicated rather than shared: each source here is meant to be a small,
117
+ * self-contained file a reviewer can audit on its own (CONTRIBUTING.md).
118
+ */
119
+ const NODE_SQLITE_REQUIREMENT = "needs Node.js 22.5+ (node:sqlite not present in this runtime)";
120
+ let sqliteRequireAttempted = false;
121
+ let DatabaseSync = null;
122
+
123
+ function getDatabaseSync() {
124
+ if (!sqliteRequireAttempted) {
125
+ sqliteRequireAttempted = true;
126
+ try { ({ DatabaseSync } = require("node:sqlite")); }
127
+ catch { DatabaseSync = null; }
128
+ }
129
+ return DatabaseSync;
130
+ }
131
+
132
+ function id() { return "kiro-cli"; }
133
+ function label() { return "Kiro CLI"; }
134
+
135
+ function jsonlDirExists() {
136
+ try { return fs.statSync(JSONL_SESSIONS_DIR).isDirectory(); } catch { return false; }
137
+ }
138
+
139
+ function sqliteDbExists() {
140
+ try { return fs.statSync(SQLITE_DB).isFile(); } catch { return false; }
141
+ }
142
+
143
+ /**
144
+ * True when either store is present. The JSONL store needs no node:sqlite
145
+ * at all, so this must not require it merely because the SQLite store also
146
+ * happens not to exist — same short-circuit-order reasoning as cursor.js's
147
+ * available(), applied across two independent stores instead of one.
148
+ */
149
+ function available() {
150
+ if (jsonlDirExists()) return true;
151
+ return sqliteDbExists() && Boolean(getDatabaseSync());
152
+ }
153
+
154
+ /**
155
+ * Same additive, optional export as cursor.js's/crush.js's/warp.js's
156
+ * unavailableReason(). Only fires the one case worth calling out
157
+ * specifically: the SQLite store is the only thing present, and this Node
158
+ * runtime can't read it. If the JSONL store is present, available() is
159
+ * already true and there's nothing to explain.
160
+ */
161
+ function unavailableReason() {
162
+ if (jsonlDirExists()) return null;
163
+ if (!sqliteDbExists()) return null;
164
+ if (getDatabaseSync()) return null;
165
+ return `Kiro CLI detected but not scanned — ${NODE_SQLITE_REQUIREMENT}`;
166
+ }
167
+
168
+ // Bounds for readLines() on the JSONL store — same shape as claude-code.js's.
169
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
170
+ const READ_TIMEOUT_MS = 60_000;
171
+
172
+ /**
173
+ * Yield { file, mtimeMs, sizeBytes, broken } for every `.json`/`.jsonl` file
174
+ * directly under `~/.kiro/sessions/cli/` — flat, non-recursive, matching
175
+ * kiro-cli-history's own confirmed `SESSIONS_DIR.glob("*.json")` walk (the
176
+ * companion `.jsonl` files sit alongside, same directory, so a plain
177
+ * extension filter over one readdir catches both).
178
+ */
179
+ function* jsonlFiles() {
180
+ let entries;
181
+ try { entries = fs.readdirSync(JSONL_SESSIONS_DIR, { withFileTypes: true }); }
182
+ catch { return; }
183
+
184
+ for (const e of entries) {
185
+ if (!/\.(jsonl|json)$/i.test(e.name)) continue;
186
+ const full = path.join(JSONL_SESSIONS_DIR, e.name);
187
+
188
+ if (e.isFile()) {
189
+ let stat;
190
+ try { stat = fs.statSync(full); } catch { yield { file: full, broken: true }; continue; }
191
+ yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
192
+ continue;
193
+ }
194
+ if (e.isSymbolicLink()) {
195
+ try {
196
+ const stat = fs.statSync(full); // follow the link
197
+ if (!stat.isFile()) { yield { file: full, broken: true }; continue; }
198
+ yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
199
+ } catch {
200
+ yield { file: full, broken: true }; // dangling symlink
201
+ }
202
+ }
203
+ // Anything else (a directory named *.json, a FIFO, ...) was never a
204
+ // scannable transcript in the first place — out of scope, not broken.
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Yield zero or one files() entries for the SQLite database. Identical
210
+ * shape and reasoning to cursor.js's/warp.js's statIfPresent(): a
211
+ * constructed, not discovered, path, so lstat is used directly. Missing
212
+ * entirely (this mode of Kiro CLI never used, or an older/newer version
213
+ * with a different filename) yields nothing and is NOT broken; a dangling
214
+ * symlink IS.
215
+ */
216
+ function* sqliteFileEntry() {
217
+ let lst;
218
+ try { lst = fs.lstatSync(SQLITE_DB); }
219
+ catch { return; }
220
+
221
+ if (lst.isSymbolicLink()) {
222
+ try {
223
+ const st = fs.statSync(SQLITE_DB);
224
+ if (!st.isFile()) { yield { file: SQLITE_DB, broken: true }; return; }
225
+ yield { file: SQLITE_DB, mtimeMs: st.mtimeMs, sizeBytes: st.size, broken: false };
226
+ } catch {
227
+ yield { file: SQLITE_DB, broken: true };
228
+ }
229
+ return;
230
+ }
231
+
232
+ if (!lst.isFile()) return;
233
+ yield { file: SQLITE_DB, mtimeMs: lst.mtimeMs, sizeBytes: lst.size, broken: false };
234
+ }
235
+
236
+ function* files() {
237
+ yield* jsonlFiles();
238
+ yield* sqliteFileEntry();
239
+ }
240
+
241
+ /**
242
+ * Read one JSONL/JSON transcript file as raw text lines. Identical approach
243
+ * to claude-code.js's readLines() — see that file's docstring for the full
244
+ * reasoning; not re-derived here since nothing about it is Kiro-specific.
245
+ */
246
+ async function readJsonlLines(file) {
247
+ let stat;
248
+ try { stat = fs.statSync(file); }
249
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
250
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
251
+
252
+ const lines = [];
253
+ let bytesRead = 0;
254
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
255
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
256
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
257
+
258
+ try {
259
+ for await (const line of rl) {
260
+ lines.push(line);
261
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1;
262
+ }
263
+ return { lines, status: "complete", bytesRead };
264
+ } catch {
265
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
266
+ } finally {
267
+ clearTimeout(timer);
268
+ rl.close();
269
+ stream.destroy();
270
+ }
271
+ }
272
+
273
+ // Bounds for the SQLite store's readLines() — same shape as cursor.js's/
274
+ // warp.js's. Not backed by a real large data.sqlite3 this tool was tested
275
+ // against; a generous, honestly-labeled backstop.
276
+ const MAX_DB_BYTES = 512 * 1024 * 1024;
277
+ const READ_TIMEOUT_MS_DB = 60_000;
278
+ const BUSY_TIMEOUT_MS = 5_000;
279
+ const YIELD_EVERY_N_ROWS = 500;
280
+
281
+ function quoteIdent(name) {
282
+ return '"' + name.replace(/"/g, '""') + '"';
283
+ }
284
+
285
+ /**
286
+ * List every real, user-created table in the database, discovered from
287
+ * `sqlite_master` rather than hardcoded to `conversations`/`conversations_v2`
288
+ * — see the module docstring for why (a real community tool already had to
289
+ * chase this schema through three revisions).
290
+ */
291
+ function discoverTableNames(db) {
292
+ try {
293
+ const rows = db.prepare(
294
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'"
295
+ ).all();
296
+ return rows.map((r) => r.name).filter((n) => typeof n === "string" && n.length > 0);
297
+ } catch {
298
+ return [];
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Turn one row into one scanned text line. Same Uint8Array/BigInt handling
304
+ * as crush.js's rowToLine() and warp.js's rowToLine() — see either for the
305
+ * full reasoning; not re-derived here.
306
+ */
307
+ function rowToLine(row) {
308
+ const clean = {};
309
+ for (const [k, v] of Object.entries(row)) {
310
+ if (v instanceof Uint8Array) clean[k] = Buffer.from(v).toString("utf-8");
311
+ else if (typeof v === "bigint") clean[k] = v.toString();
312
+ else clean[k] = v;
313
+ }
314
+ return JSON.stringify(clean);
315
+ }
316
+
317
+ /**
318
+ * Read the SQLite store as raw text lines — one per row, across every
319
+ * discovered table. Same status vocabulary and periodic-yield-plus-deadline
320
+ * approach as cursor.js's/crush.js's/warp.js's readLines(), for the same
321
+ * reason: node:sqlite is fully synchronous. See cursor.js's own docstring
322
+ * for the full reasoning.
323
+ */
324
+ async function readSqliteLines(file) {
325
+ const DB = getDatabaseSync();
326
+ if (!DB) return { lines: [], status: "failed", bytesRead: 0 };
327
+
328
+ let stat;
329
+ try { stat = fs.statSync(file); }
330
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
331
+ if (stat.size > MAX_DB_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
332
+
333
+ let db;
334
+ try {
335
+ db = new DB(file, { readOnly: true });
336
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
337
+ } catch {
338
+ return { lines: [], status: "failed", bytesRead: 0 };
339
+ }
340
+
341
+ const tables = discoverTableNames(db);
342
+
343
+ const lines = [];
344
+ let bytesRead = 0;
345
+ const deadline = Date.now() + READ_TIMEOUT_MS_DB;
346
+ let timedOut = false;
347
+ let sawError = false;
348
+
349
+ for (const table of tables) {
350
+ let rows;
351
+ try {
352
+ rows = db.prepare(`SELECT * FROM ${quoteIdent(table)}`).iterate();
353
+ } catch {
354
+ continue;
355
+ }
356
+
357
+ let n = 0;
358
+ try {
359
+ for (const row of rows) {
360
+ const text = rowToLine(row);
361
+ lines.push(text);
362
+ bytesRead += Buffer.byteLength(text, "utf-8");
363
+ n++;
364
+ if (n % YIELD_EVERY_N_ROWS === 0) {
365
+ await new Promise((resolve) => setImmediate(resolve));
366
+ if (Date.now() > deadline) { timedOut = true; break; }
367
+ }
368
+ }
369
+ } catch {
370
+ sawError = true;
371
+ }
372
+ if (timedOut) break;
373
+ }
374
+
375
+ try { db.close(); } catch { /* best-effort close */ }
376
+
377
+ if (tables.length === 0) return { lines: [], status: "failed", bytesRead: 0 };
378
+ if (sawError && lines.length === 0) return { lines: [], status: "failed", bytesRead };
379
+ if (timedOut || sawError) return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
380
+ return { lines, status: "complete", bytesRead };
381
+ }
382
+
383
+ /**
384
+ * Dispatch to the JSONL or SQLite reader by matching `file` against the one
385
+ * known SQLite path — everything else (every entry jsonlFiles() can ever
386
+ * yield) goes through the plain text reader.
387
+ */
388
+ async function readLines(file) {
389
+ if (file === SQLITE_DB) return readSqliteLines(file);
390
+ return readJsonlLines(file);
391
+ }
392
+
393
+ module.exports = { id, label, available, unavailableReason, files, readLines };
@@ -0,0 +1,230 @@
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
+ * Kiro IDE (AWS/Kiro's VS Code–based desktop IDE) agent chat history.
10
+ * Distinct from Kiro CLI (see kiro-cli.js) — same product family, different
11
+ * application, different storage.
12
+ *
13
+ * VERIFICATION STATUS (read this before trusting anything below): the exact
14
+ * path and directory shape below are corroborated by THREE independent,
15
+ * real-world sources, none of them official documentation (Kiro's own docs
16
+ * do not appear to document this path at all — this source found none):
17
+ *
18
+ * 1. github.com/kirodotdev/Kiro issue #5469 — a real user's own `du -sh`
19
+ * output against their live install: `~/Library/Application
20
+ * Support/kiro/User/globalStorage/kiro.kiroagent` (13GB, broken down
21
+ * into per-workspace-hash subdirectories like
22
+ * `d1c95acd1215dbe372efb48819c04345/`).
23
+ * 2. github.com/kirodotdev/Kiro issue #4165 — a SECOND, independent real
24
+ * user reporting the identical shape from their own install (6,422
25
+ * files, ~8.2GB), explicitly describing the content: "Each file
26
+ * contains the full context of a spec generation run, including your
27
+ * prompts and environment context" — i.e. real prompt/response text,
28
+ * exactly the "AI-block" content this source was scoped to find.
29
+ * 3. github.com/kirodotdev/Kiro PR #5755 (open, unmerged, but written
30
+ * against a real install by a third independent contributor) — a
31
+ * cache-cleanup script whose actual shell logic this source fetched
32
+ * and read directly (`gh pr diff 5755`), which is more precise than
33
+ * either issue: it fixes the exact base path (with correct
34
+ * capitalization — `Kiro`, not `kiro`, resolving a casing
35
+ * inconsistency between the two issues above) and identifies which
36
+ * files under it are the real conversation content:
37
+ * `~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent/`
38
+ * containing per-workspace hex-hash directories, each holding:
39
+ * - one or more `*.chat` files — "Chat Files (.chat): Conversation
40
+ * history with Kiro" (the script's own "WHAT GETS DELETED"
41
+ * section) — confirmed as FILES, not directories, by its actual
42
+ * deletion command: `find . -name "*.chat" -type f`.
43
+ * - other subdirectories holding "File Version Cache: Subdirectories
44
+ * containing snapshots of project files used for diff/restore
45
+ * operations."
46
+ * The companion script in the same PR (`clean-kiro-ide-sessions.sh`)
47
+ * further confirms two SIBLING directories exist —
48
+ * `~/Library/Application Support/Kiro/Session Storage` (its own
49
+ * description: "LevelDB files... window positions, open tabs,
50
+ * navigation history") and `.../Kiro/Workspaces` (workspace
51
+ * settings/state) — and that neither holds conversation content. This
52
+ * mirrors exactly what cursor.js already established for a sibling VS
53
+ * Code fork: the generic Electron/VS-Code-shell state (Session
54
+ * Storage, Workspaces) is separate from, and uninteresting compared
55
+ * to, the extension-specific data under `User/globalStorage/<extension-id>/`
56
+ * — here `kiro.kiroagent`, Kiro's own agent extension ID. Deliberately
57
+ * OUT OF SCOPE for the same reason gemini-cli.js excludes its
58
+ * tool's checkpoints/logs directories: `Session Storage` and
59
+ * `Workspaces` are confirmed to hold window/UI state, not agent
60
+ * conversation content.
61
+ *
62
+ * None of this has been checked against a real Kiro IDE install — it is not
63
+ * installed on the machine this adapter was built on (checked: no `Kiro.app`
64
+ * under /Applications, no matching directory under `~/Library/Application
65
+ * Support`, not findable via `mdfind`). If you have Kiro IDE installed, the
66
+ * most useful thing you can do is run `residoo scan` and confirm
67
+ * `sourcesScanned`/`filesScanned` look right for what you know is actually
68
+ * on disk under the path above, then report back either way.
69
+ *
70
+ * INTERNAL FORMAT OF `.chat` FILES: not independently confirmed — none of
71
+ * the three sources above open or describe one's actual bytes, only its
72
+ * name, size, and general subject ("conversation history", "prompts and
73
+ * environment context"). This source treats `.chat` files as plain text
74
+ * (streamed the same way claude-code.js treats `.jsonl`), which is
75
+ * consistent with every comparable VS-Code-fork/agent-tool store this
76
+ * project has examined (Cursor's cursorDiskKV values, Warp's blob columns,
77
+ * Crush's `content` column — all JSON text, none binary) but is, honestly,
78
+ * an inference from that pattern rather than a confirmed fact about this
79
+ * specific file format. Per the adapter contract this is safe either way:
80
+ * a line-by-line text read against a file that happens to be binary just
81
+ * yields lines that don't pattern-match anything, not a crash — see
82
+ * claude-code.js's own docstring on this exact point.
83
+ *
84
+ * PER-OS PATH: only the macOS path is independently confirmed (all three
85
+ * sources are macOS reports). Windows and Linux below are filled in by
86
+ * direct structural analogy to cursor.js's own per-OS `<App>/User/` layout
87
+ * — the same convention this project already relies on for a sibling VS
88
+ * Code fork, applied here because Kiro IDE is itself documented as
89
+ * "VS Code-based" (kirodotdev/Kiro's own repo description). Flagged as
90
+ * analogy, not independently quoted, same honesty standard as this
91
+ * project's other per-OS gaps.
92
+ */
93
+ function kiroUserDir() {
94
+ const home = os.homedir();
95
+ if (process.platform === "darwin") {
96
+ return path.join(home, "Library", "Application Support", "Kiro", "User");
97
+ }
98
+ if (process.platform === "win32") {
99
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
100
+ return path.join(appData, "Kiro", "User");
101
+ }
102
+ // Linux and other XDG-following unix platforms.
103
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
104
+ return path.join(configHome, "Kiro", "User");
105
+ }
106
+
107
+ const USER_DIR = kiroUserDir();
108
+ const AGENT_STORAGE_DIR = path.join(USER_DIR, "globalStorage", "kiro.kiroagent");
109
+
110
+ // How many levels deep files() will descend under kiro.kiroagent/ looking
111
+ // for `*.chat` files. The confirmed shape is one workspace-hash level
112
+ // (`<hash>/*.chat`), but the cleanup script's own recursive `find . -name
113
+ // "*.chat"` (no depth limit, run from the kiro.kiroagent root) implies
114
+ // *.chat files are not guaranteed to sit at exactly that one depth — kept
115
+ // bounded rather than hard-coded at exactly 1 for the same reason
116
+ // gemini-cli.js's MAX_CHATS_DEPTH is bounded rather than fixed: a deeper
117
+ // nesting should still get scanned, and a symlink cycle must not turn this
118
+ // into an infinite walk.
119
+ const MAX_WALK_DEPTH = 4;
120
+
121
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB — see claude-code.js; not independently
122
+ // measured against a real .chat file here.
123
+ const READ_TIMEOUT_MS = 60_000;
124
+
125
+ function id() { return "kiro-ide"; }
126
+ function label() { return "Kiro IDE"; }
127
+
128
+ function available() {
129
+ try { return fs.statSync(AGENT_STORAGE_DIR).isDirectory(); } catch { return false; }
130
+ }
131
+
132
+ /**
133
+ * Same defensive symlink-following pattern as claude-code.js and cursor.js
134
+ * — see claude-code.js's docstring for the full reasoning. Duplicated
135
+ * rather than imported: each source here is meant to be a small,
136
+ * self-contained file a reviewer can audit on its own (CONTRIBUTING.md).
137
+ */
138
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
139
+ if (checkFn(dirent)) return true;
140
+ if (!dirent.isSymbolicLink()) return false;
141
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
142
+ }
143
+ const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
144
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
145
+
146
+ /**
147
+ * Walk kiro.kiroagent/ (or one of its subdirectories) for `*.chat` files, up
148
+ * to MAX_WALK_DEPTH levels deep. The confirmed "file version cache"
149
+ * subdirectories (diff/restore snapshots, unconfirmed internal format) are
150
+ * still descended into by this walk — they are not excluded — because a
151
+ * cache directory could itself, in principle, hold a nested `*.chat` file
152
+ * under some Kiro version this source has no visibility into; only files
153
+ * that don't match the `.chat` name pattern are skipped, not whole
154
+ * directories. See the module docstring for why cache-directory CONTENTS
155
+ * themselves (i.e. files inside them that aren't named `*.chat`) are out of
156
+ * scope: their format was never confirmed.
157
+ */
158
+ function* walkChatFiles(dir, depth) {
159
+ let entries;
160
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
161
+ catch { return; }
162
+
163
+ for (const e of entries) {
164
+ const full = path.join(dir, e.name);
165
+
166
+ if (isDirFollowingSymlink(full, e)) {
167
+ if (depth < MAX_WALK_DEPTH) yield* walkChatFiles(full, depth + 1);
168
+ continue;
169
+ }
170
+ if (isFileFollowingSymlink(full, e)) {
171
+ if (!e.name.endsWith(".chat")) continue;
172
+ let stat;
173
+ try { stat = fs.statSync(full); } catch { yield { file: full, broken: true }; continue; }
174
+ yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
175
+ continue;
176
+ }
177
+ // Neither resolves as a directory nor a file: a dangling symlink is the
178
+ // one case worth reporting.
179
+ if (e.isSymbolicLink()) yield { file: full, broken: true };
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Yield { file, mtimeMs, sizeBytes, broken } for every `*.chat` file found
185
+ * under `.../User/globalStorage/kiro.kiroagent/`.
186
+ */
187
+ function* files() {
188
+ yield* walkChatFiles(AGENT_STORAGE_DIR, 0);
189
+ }
190
+
191
+ /**
192
+ * Read one `.chat` file as an array of raw text lines. Identical approach to
193
+ * claude-code.js's readLines() — streamed via readline/promises (not
194
+ * readFileSync+split, for the same V8 string-length-ceiling reason), a
195
+ * generous size cap, and a hard read timeout since Node's stream/readline
196
+ * stack has no built-in one. See claude-code.js's own docstring for the
197
+ * full reasoning; not re-derived here since nothing about it is
198
+ * Kiro-specific. Whether `.chat` is JSON, JSONL, or something else, lines
199
+ * don't need to be valid JSON individually per the adapter contract — they
200
+ * just need pattern-matching against.
201
+ */
202
+ async function readLines(file) {
203
+ let stat;
204
+ try { stat = fs.statSync(file); }
205
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
206
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
207
+
208
+ const lines = [];
209
+ let bytesRead = 0;
210
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
211
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
212
+
213
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
214
+
215
+ try {
216
+ for await (const line of rl) {
217
+ lines.push(line);
218
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1;
219
+ }
220
+ return { lines, status: "complete", bytesRead };
221
+ } catch {
222
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
223
+ } finally {
224
+ clearTimeout(timer);
225
+ rl.close();
226
+ stream.destroy();
227
+ }
228
+ }
229
+
230
+ module.exports = { id, label, available, files, readLines };