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.
- package/README.md +225 -46
- package/SECURITY.md +29 -22
- package/package.json +1 -1
- package/src/cli.js +82 -16
- package/src/integrity.js +669 -0
- package/src/patterns.js +78 -5
- package/src/report.js +74 -7
- 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/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,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 };
|