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,361 @@
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
+ * Aider (github.com/Aider-AI/aider) session transcripts.
10
+ *
11
+ * VERIFICATION STATUS (read this before trusting anything below): the file
12
+ * names, on-disk format, and location logic below are corroborated across
13
+ * FOUR independent, current sources —
14
+ *
15
+ * 1. Aider's own official docs (aider.chat/docs/config/options.html),
16
+ * which document `--chat-history-file` (default `.aider.chat.history.md`)
17
+ * and `--input-history-file` (default `.aider.input.history`).
18
+ * 2. Aider's own current GitHub source (Aider-AI/aider, `aider/args.py`,
19
+ * main branch — fetched directly, not from a cache or a summary):
20
+ * default_input_history_file = os.path.join(git_root, ".aider.input.history")
21
+ * if git_root else ".aider.input.history"
22
+ * default_chat_history_file = os.path.join(git_root, ".aider.chat.history.md")
23
+ * if git_root else ".aider.chat.history.md"
24
+ * i.e. these are NOT under one fixed root the way Claude Code's or
25
+ * Cursor's storage is — they land at the root of whatever git repo the
26
+ * user ran `aider` inside (or the bare CWD if that wasn't a git repo).
27
+ * A real user independently ran into exactly this scattering, filing
28
+ * Aider-AI/aider#2684 ("history files accumulate ... outside git
29
+ * repos"), which corroborates the CWD/git-root behaviour from the
30
+ * outside, not just from reading the source.
31
+ * 3. A REAL, live `.aider.chat.history.md` from an actual aider user, who
32
+ * committed it to their own public repo (github.com/dfeldman/
33
+ * operation-conundrum.github.io, file `aider-chat-history.md`, 2628
34
+ * lines, dated 2023-05-26 in-content). Fetched and inspected directly.
35
+ * It matches the documented format exactly: sessions delimited by
36
+ * `# aider chat started at <timestamp>`, each user message as one or
37
+ * more `#### `-prefixed markdown lines, tool/system notices as `> `
38
+ * blockquote lines, assistant replies as plain markdown including
39
+ * fenced code blocks and aider's own `<<<<<<< ORIGINAL / ======= /
40
+ * >>>>>>> UPDATED` search-replace diff blocks.
41
+ * 4. `.aider.input.history` is not aider's own format at all — aider hands
42
+ * it straight to `prompt_toolkit.history.FileHistory`, a dependency of
43
+ * aider's. Fetched that library's own current source
44
+ * (python-prompt-toolkit, `src/prompt_toolkit/history.py`,
45
+ * `FileHistory.store_string`) directly: every stored input is appended
46
+ * as `\n# <datetime>\n` followed by that input's lines, each prefixed
47
+ * with a literal `+`. So this file is not free-form text so much as a
48
+ * well-known third-party library's fixed serialization — verified
49
+ * against that library's own code, not guessed.
50
+ *
51
+ * What none of the above is: a real Aider install on the machine this
52
+ * source was built on. Checked directly and thoroughly — `which aider`,
53
+ * `pip3 show aider-chat` / `python3 -m pip show aider-chat`, `brew list
54
+ * aider`, `pipx list`, common config locations (`~/.config`, `~/Library/
55
+ * Application Support`, `~/Library/Caches`, `~/Library/Preferences`), and a
56
+ * filesystem-wide `find`/`mdfind` for `.aider*` and `*aider-chat-history*`.
57
+ * All came back empty: aider is not installed here, and there is no real
58
+ * session history on this machine to genuinely verify the schema against.
59
+ * Per CONTRIBUTING.md's rule 3, this ships anyway because of the four
60
+ * corroborating sources above, but should be treated the same way cursor.js
61
+ * asks to be treated: real, but UNVERIFIED against a live install. If you
62
+ * have Aider installed, running `residoo scan` and checking the results
63
+ * against what you know is really in your `.aider.chat.history.md` /
64
+ * `.aider.input.history` files is the single most useful way to firm this
65
+ * up — please report back either way.
66
+ *
67
+ * WHERE THIS SOURCE LOOKS — the fundamentally different problem vs.
68
+ * claude-code.js / cursor.js:
69
+ *
70
+ * Both of those tools keep everything under one fixed, well-known directory
71
+ * this machine can enumerate directly (~/.claude/projects,
72
+ * .../Cursor/User). Aider, by its own design (see source citation #2
73
+ * above), has no such thing — its two history files can be sitting at the
74
+ * root of literally any git repository, or any bare directory, the user has
75
+ * ever run `aider` from. There is no manifest anywhere that lists which
76
+ * directories those were: `~/.aider/installs.json` (see AIDER_HOME below)
77
+ * only ever records `(version, python-executable)` pairs for "what's new"
78
+ * notes, and `~/.aider/analytics.json` records anonymized event counters,
79
+ * neither ever a path. Confirmed directly against aider's own
80
+ * `is_first_run_of_new_version()` in `main.py` and `Analytics` in
81
+ * `analytics.py`.
82
+ *
83
+ * So `files()` below does a bounded, best-effort walk of the user's home
84
+ * directory looking for the two exact filenames above (plus, opportunistically,
85
+ * the one non-default file named below) at any depth. This is an honest,
86
+ * named engineering tradeoff, not a guess about WHERE aider's format lives
87
+ * (that part is verified, see above) — it is a search-breadth compromise for
88
+ * a location that is, by the tool's own design, unbounded. The walk is
89
+ * gated behind available() (see below) so a user who has never touched
90
+ * aider pays nothing for it, and it is bounded (MAX_DEPTH, MAX_DIRS_VISITED)
91
+ * so a user who has pays a bounded, not unlimited, cost. Both bounds are
92
+ * generous enough to cover realistic project layouts but this is explicitly
93
+ * NOT an exhaustive filesystem search — a `.aider.chat.history.md` sitting
94
+ * deeper than MAX_DEPTH below $HOME, or reachable only through a symlinked
95
+ * directory (deliberately not followed — see walk()'s docstring), will be
96
+ * missed. That is a real, named limitation, the same spirit as
97
+ * claude-code.js's admitted peak-memory gap and cursor.js's admitted
98
+ * untested-on-a-real-install gap — not a silent one.
99
+ */
100
+
101
+ const HOME = os.homedir();
102
+
103
+ // Presence-only signal, NOT the location transcripts live in (see module
104
+ // docstring's "WHERE THIS SOURCE LOOKS" section). Aider writes into this
105
+ // directory on essentially every normal run — install-tracking
106
+ // (installs.json), opt-in anonymous analytics (analytics.json), and OAuth
107
+ // provider tokens (oauth-keys.env) — confirmed directly against
108
+ // `Path.home() / ".aider"` in aider's own main.py/analytics.py. Its mere
109
+ // existence is a reliable, cheap, fixed-path way to answer "has aider ever
110
+ // actually run on this machine" without doing the expensive home-directory
111
+ // walk files() needs for the transcripts themselves.
112
+ const AIDER_HOME = path.join(HOME, ".aider");
113
+
114
+ const CHAT_HISTORY_NAME = ".aider.chat.history.md";
115
+ const INPUT_HISTORY_NAME = ".aider.input.history";
116
+ // --llm-history-file has NO default (default=None in args.py — confirmed
117
+ // directly) — it only exists if a user explicitly opted in. It is included
118
+ // here purely opportunistically, using the exact filename aider's own
119
+ // --help text uses as its example ("for example, .aider.llm.history"): if a
120
+ // file with this exact name happens to exist alongside the other two, scan
121
+ // it too, since aider's own LLM history log is plausibly full of pasted
122
+ // code/secrets. This is NOT a verified default location the way the other
123
+ // two are — it is a zero-cost opportunistic check with no default to be
124
+ // wrong about, and its absence should never be read as "surely not opted
125
+ // in," just "not opted in under the example name."
126
+ const LLM_HISTORY_NAME = ".aider.llm.history";
127
+
128
+ const CANDIDATE_NAMES = new Set([CHAT_HISTORY_NAME, INPUT_HISTORY_NAME, LLM_HISTORY_NAME]);
129
+
130
+ function id() { return "aider"; }
131
+ function label() { return "Aider"; }
132
+
133
+ function available() {
134
+ try { return fs.statSync(AIDER_HOME).isDirectory(); } catch { return false; }
135
+ }
136
+
137
+ // Bounds for the home-directory walk in files() — see the module docstring
138
+ // for why this walk exists at all. Not calibrated against any real, large
139
+ // aider user's directory tree (no real install on this machine — see
140
+ // module docstring); chosen as a generous-but-bounded backstop the same way
141
+ // cursor.js's MAX_DB_BYTES is, not a measured real-world ceiling the way
142
+ // claude-code.js's MAX_BYTES is.
143
+ const MAX_DEPTH = 8; // levels below $HOME a candidate file can be found at
144
+ const MAX_DIRS_VISITED = 50_000; // circuit breaker on total directories read
145
+
146
+ // Directory names never worth descending into, at any depth: version
147
+ // control internals, dependency/build output, and language/tool caches.
148
+ // This is a performance optimization only, not a correctness boundary —
149
+ // MAX_DIRS_VISITED is what actually bounds worst-case cost; skipping these
150
+ // just spends that budget on directories far more likely to matter. None of
151
+ // aider's own history files are ever written inside any of these (they live
152
+ // at a git root or a bare CWD — never inside .git/, node_modules/, etc.),
153
+ // so skipping them cannot hide a real match.
154
+ const ALWAYS_SKIP_DIR_NAMES = new Set([
155
+ "node_modules", ".git", ".hg", ".svn", "vendor",
156
+ ".venv", "venv", "__pycache__", ".tox", ".mypy_cache", ".pytest_cache", ".ruff_cache",
157
+ ".next", ".nuxt", "dist", "build", "target", ".gradle", ".m2",
158
+ ".cargo", ".rustup", ".npm", ".yarn", ".pnpm-store", ".cache",
159
+ ".docker", ".orbstack", ".Trash", ".Trashes",
160
+ ".Spotlight-V100", ".fseventsd", ".DocumentRevisions-V100", ".TemporaryItems",
161
+ ]);
162
+
163
+ // Skipped ONLY as direct children of $HOME itself (depth 0), never at any
164
+ // deeper level — unlike the names above, these are ordinary, meaningful
165
+ // words a real project directory could legitimately be named (e.g. a repo
166
+ // literally called "build" or "Library"); they are only reliably "OS/user
167
+ // furniture, not a project" when sitting directly under the home directory.
168
+ const HOME_TOP_LEVEL_SKIP_DIR_NAMES = new Set([
169
+ "Library", "Applications", "Pictures", "Movies", "Music", "Public", "Desktop",
170
+ "AppData", // Windows counterpart to the above; harmless to check cross-platform
171
+ ]);
172
+
173
+ /**
174
+ * Same defensive symlink-following as claude-code.js's
175
+ * isFileFollowingSymlink — duplicated locally rather than imported, same
176
+ * reasoning cursor.js states: each source here is meant to be a small,
177
+ * self-contained file a reviewer can audit on its own.
178
+ *
179
+ * Used only for the three known candidate filenames themselves (a single,
180
+ * named entry) — see walk()'s docstring for why open-ended directory
181
+ * recursion below deliberately does NOT get the same symlink-following
182
+ * treatment.
183
+ */
184
+ function isFileFollowingSymlink(fullPath, dirent) {
185
+ if (dirent.isFile()) return true;
186
+ if (!dirent.isSymbolicLink()) return false;
187
+ try { return fs.statSync(fullPath).isFile(); } catch { return false; }
188
+ }
189
+
190
+ /**
191
+ * Resolve one candidate-named directory entry (`.aider.chat.history.md`,
192
+ * `.aider.input.history`, or `.aider.llm.history`) into zero or one files()
193
+ * entries, following a symlink with that exact name the same way
194
+ * claude-code.js follows a `*.jsonl`-named symlink. `broken: true` is
195
+ * reserved for a symlink with one of these exact names that fails to
196
+ * resolve — genuinely "this looked like an aider history file and wasn't
197
+ * readable," not the general "most directories we visit aren't
198
+ * aider-related at all" case walk() itself silently passes over (see its
199
+ * docstring).
200
+ */
201
+ function* candidateEntry(fullPath, dirent) {
202
+ if (!isFileFollowingSymlink(fullPath, dirent)) {
203
+ if (dirent.isSymbolicLink()) yield { file: fullPath, broken: true };
204
+ return; // e.g. a directory that happens to be named exactly this — out of scope, not broken
205
+ }
206
+ let stat;
207
+ try { stat = fs.statSync(fullPath); }
208
+ catch { yield { file: fullPath, broken: true }; return; }
209
+ yield { file: fullPath, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
210
+ }
211
+
212
+ /**
213
+ * Recursively walk `dir` (`depth` levels below $HOME) looking for the
214
+ * candidate filenames above, subject to `budget` (a shared { remaining }
215
+ * counter across the whole walk — see MAX_DIRS_VISITED).
216
+ *
217
+ * Two deliberate departures from claude-code.js/cursor.js's walking style,
218
+ * both because this walk is open-ended (an unbounded, unknown directory
219
+ * tree) rather than a listing of one specific, known, expected location:
220
+ *
221
+ * 1. Directory symlinks are NOT followed during recursion (only
222
+ * `dirent.isDirectory()`, lstat semantics). Following them here — unlike
223
+ * following a single, specific, known symlink such as
224
+ * ~/.claude/projects/<slug> — risks an infinite cycle (a symlinked
225
+ * directory pointing back at one of its own ancestors), which an
226
+ * open-ended walk has no other guard against. MAX_DIRS_VISITED still
227
+ * bounds worst case even if this reasoning has a gap, but not following
228
+ * directory symlinks is the primary defense.
229
+ * 2. A directory that fails to list (fs.readdirSync throws — permission
230
+ * denied, deleted mid-walk, etc.) is silently skipped, NOT reported via
231
+ * `broken: true`. claude-code.js reports that for a project directory
232
+ * under ~/.claude/projects because every entry there is a known,
233
+ * expected Claude Code project folder — a read failure is anomalous and
234
+ * worth surfacing. Here, the overwhelming majority of directories this
235
+ * function visits have nothing to do with aider at all (this is a
236
+ * speculative, exploratory walk of $HOME) — treating every
237
+ * permission-denied OS directory encountered along the way as a
238
+ * reportable "broken" entry would flood the report with noise carrying
239
+ * no actionable signal. `broken` stays reserved for the specific, named
240
+ * candidate files themselves (see candidateEntry above), exactly
241
+ * mirroring how claude-code.js/cursor.js already treat "some unrelated
242
+ * stray entry" as silently out of scope while treating a failure on a
243
+ * specifically-expected entry as reportable.
244
+ */
245
+ function* walk(dir, depth, budget) {
246
+ if (budget.remaining <= 0) return;
247
+ budget.remaining--;
248
+
249
+ let entries;
250
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
251
+ catch { return; }
252
+
253
+ const atHomeLevel = depth === 0;
254
+ for (const e of entries) {
255
+ const full = path.join(dir, e.name);
256
+
257
+ if (CANDIDATE_NAMES.has(e.name)) {
258
+ yield* candidateEntry(full, e);
259
+ continue;
260
+ }
261
+
262
+ if (depth >= MAX_DEPTH) continue;
263
+ if (!e.isDirectory()) continue; // no symlink-following in open-ended recursion — see docstring above
264
+ if (ALWAYS_SKIP_DIR_NAMES.has(e.name)) continue;
265
+ if (atHomeLevel && HOME_TOP_LEVEL_SKIP_DIR_NAMES.has(e.name)) continue;
266
+
267
+ yield* walk(full, depth + 1, budget);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Yield { file, mtimeMs, sizeBytes, broken } for every aider history file
273
+ * found under $HOME. See the module docstring's "WHERE THIS SOURCE LOOKS"
274
+ * section for what this walk is and is not guaranteed to cover.
275
+ */
276
+ function* files() {
277
+ // available() is the cheap gate on the (unrelated) ~/.aider directory —
278
+ // see that directory's own comment above for why checking it here, unlike
279
+ // in claude-code.js/cursor.js, is necessary rather than redundant: this
280
+ // function's walk root ($HOME) is not the same directory available()
281
+ // checks, so without this line every residoo user — aider or not — would
282
+ // pay for a full home-directory walk on every scan.
283
+ if (!available()) return;
284
+
285
+ const budget = { remaining: MAX_DIRS_VISITED };
286
+ yield* walk(HOME, 0, budget);
287
+ }
288
+
289
+ // Bounds for readLines() — same rationale and same numbers as
290
+ // claude-code.js, but NOT calibrated against a real large aider file the
291
+ // way claude-code.js's MAX_BYTES was (no real install — see module
292
+ // docstring). Markdown chat transcripts and prompt_toolkit's input-history
293
+ // format are both far more compact than JSONL tool-call payloads (no
294
+ // embedded base64, no repeated schema keys), so multi-gigabyte real files
295
+ // are less likely here than for claude-code.js — but with nothing real to
296
+ // measure, this stays a generous backstop rather than a measured ceiling.
297
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
298
+ const READ_TIMEOUT_MS = 60_000;
299
+
300
+ /**
301
+ * Read one aider history file as an array of raw text lines.
302
+ *
303
+ * Both known formats are already meaningfully line-oriented, so no
304
+ * reformatting is needed before pattern matching:
305
+ * - .aider.chat.history.md is Markdown — every line (a `#### ` prompt
306
+ * line, a `> ` tool-notice line, a fenced-code-block line, plain
307
+ * assistant prose) is exactly one scanned line, same as any other text
308
+ * file this codebase reads.
309
+ * - .aider.input.history is prompt_toolkit's FileHistory serialization —
310
+ * each stored input's lines are written back out one per file line,
311
+ * each prefixed with a literal `+` (plus interleaved `# <datetime>`
312
+ * comment lines) — see source citation #4 in the module docstring. The
313
+ * leading `+` is left in place rather than stripped: every pattern in
314
+ * src/patterns.js matches on `\b` word boundaries, never a `^`
315
+ * line-start anchor (checked directly against patterns.js), so a
316
+ * secret on a `+`-prefixed line is matched exactly as it would be
317
+ * without the prefix. Stripping it would be extra code with no
318
+ * detection benefit.
319
+ *
320
+ * Implementation (streaming via readline/promises, MAX_BYTES cap,
321
+ * READ_TIMEOUT_MS watchdog, partial-read lines kept rather than discarded)
322
+ * is deliberately identical in shape to claude-code.js's readLines() — see
323
+ * that file's docstring for the full reasoning on each of those choices,
324
+ * all of which apply here unchanged (this is plain line-delimited UTF-8
325
+ * text on disk either way, not a database or JSON blob needing cursor.js's
326
+ * different approach). Duplicated rather than imported, per this project's
327
+ * one-small-self-contained-file-per-source convention (see cursor.js's own
328
+ * docstring for the same point).
329
+ */
330
+ async function readLines(file) {
331
+ let stat;
332
+ try { stat = fs.statSync(file); }
333
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
334
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
335
+
336
+ const lines = [];
337
+ let bytesRead = 0;
338
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
339
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
340
+
341
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
342
+
343
+ try {
344
+ for await (const line of rl) {
345
+ lines.push(line);
346
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1; // +1 for the stripped newline
347
+ }
348
+ return { lines, status: "complete", bytesRead };
349
+ } catch {
350
+ // Whatever WAS read before the failure is real content and may contain
351
+ // a real secret — discarding it because the file didn't finish cleanly
352
+ // would be a silent false negative, same reasoning as claude-code.js.
353
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
354
+ } finally {
355
+ clearTimeout(timer);
356
+ rl.close();
357
+ stream.destroy();
358
+ }
359
+ }
360
+
361
+ module.exports = { id, label, available, files, readLines };
@@ -0,0 +1,199 @@
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
+ * Amazon Q Developer — the AWS chat/coding-assistant IDE plugin (VS Code via
10
+ * the AWS Toolkit / "Amazon Q" extension, and the JetBrains "Amazon Q"
11
+ * plugin; formerly CodeWhisperer).
12
+ *
13
+ * VERIFICATION STATUS (read this before trusting anything below):
14
+ * multi-source-corroborated-but-UNVERIFIED against a real install. Neither VS
15
+ * Code, JetBrains, nor any Amazon Q plugin is installed on the machine this
16
+ * adapter was built on (checked: no /Applications/*Code*.app, no `code` on
17
+ * PATH, no `~/.aws/amazonq` directory — `~/.aws` itself exists here only from
18
+ * an old, unrelated AWS CLI credentials setup, `config`/`credentials` only,
19
+ * no `amazonq` subdirectory). What IS unusually strong here, short of a real
20
+ * install: the storage mechanism and exact filenames were read directly out
21
+ * of AWS's own current shipped source for BOTH clients, not inferred from a
22
+ * blog post:
23
+ *
24
+ * 1. `aws/aws-toolkit-vscode` (the VS Code client), `main` branch,
25
+ * `packages/core/src/shared/db/chatDb/chatDb.ts`, fetched verbatim via
26
+ * `gh api repos/aws/aws-toolkit-vscode/contents/...` on 2026-09-02 —
27
+ * its own docstring states plainly: "The database is stored in the
28
+ * user's home directory under .aws/amazonq/history with a unique
29
+ * filename based on the workspace identifier," and the constructor
30
+ * confirms it exactly:
31
+ * `this.dbDirectory = path.join(fs.getUserHomeDir(), '.aws/amazonq/history')`
32
+ * `const dbName = \`chat-history-${workspaceId}.json\``
33
+ * where `getWorkspaceIdentifier()` (same file) is an MD5 hex hash of
34
+ * the open `.code-workspace` path, or of the sorted+joined multi-root
35
+ * folder paths, or of the single open folder path, or the literal
36
+ * string `'no-workspace'` when nothing is open — i.e. filenames are
37
+ * exactly `chat-history-<32-hex-md5>.json` or
38
+ * `chat-history-no-workspace.json`. The "database" itself is LokiJS
39
+ * (`import Loki from 'lokijs'`, `persistenceMethod: 'fs'`) — an
40
+ * embedded JS document store that serializes its entire collection set
41
+ * as ONE JSON document per file, not a real SQL database despite the
42
+ * `.json`-suffixed "chat-history-" naming — so this is ordinary,
43
+ * scannable JSON text on disk, no special binary/SQLite handling
44
+ * needed (unlike cursor.js/cody.js).
45
+ *
46
+ * 2. `Amazon-Q-Developer/language-servers`,
47
+ * `server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts`
48
+ * — the SAME class, byte-for-byte the same docstring and
49
+ * `.aws/amazonq/history` path, living in the shared "Flare" language
50
+ * server package both IDE clients embed. Confirmed the JetBrains client
51
+ * actually embeds this same language server (not a separate,
52
+ * JetBrains-native storage layer) by reading
53
+ * `Amazon-Q-Developer/amazon-q-jetbrains`,
54
+ * `plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt`
55
+ * (same fetch method, same date) — the JetBrains plugin is an LSP
56
+ * *client* to the identical CodeWhisperer/Q language server, so its chat
57
+ * history lands in the exact same home-directory path as VS Code's,
58
+ * not a JetBrains-specific config/plugins directory. This is why this
59
+ * one adapter covers both IDEs with a single, IDE-agnostic path — no
60
+ * per-editor branching needed, unlike copilot-chat.js/cody.js.
61
+ *
62
+ * Independent, non-AWS corroboration of the same exact path and filename
63
+ * pattern (cross-checked, all agree with each other and with the source
64
+ * above): a real user's own write-up at
65
+ * dev.to/aws/finding-and-recovering-your-amazon-q-developer-prompt-history-28j1
66
+ * ("In the ~/.aws/amazonq/ directory there is a history directory... json
67
+ * files" — names `chat-history-no-workspace.json` and several
68
+ * `chat-history-<hash>.json` examples verbatim); a third-party forensics
69
+ * tool, `ACandeias/AI-Forensicator`, `collectors/amazon_q.py`, whose own
70
+ * comment reads "~/.aws/amazonq/history/ -- chat history JSON files"; and
71
+ * a third-party agent-session spec, `YawLabs/ctxlint`,
72
+ * `agent-session-lint-rules.json`, recording
73
+ * `"historyLocation": "~/.aws/amazonq/history/chat-history-*.json"`.
74
+ *
75
+ * No per-OS path branching in the AWS source read above (`fs.getUserHomeDir()`
76
+ * joined with the same relative `.aws/amazonq/history` on every platform) —
77
+ * matching the long-standing, cross-platform AWS CLI/SDK convention of a
78
+ * single `~/.aws` (`%USERPROFILE%\.aws` on Windows) regardless of OS, unlike
79
+ * VS Code's own per-OS `Application Support`/`AppData`/XDG split that most
80
+ * other sources in this project have to branch on.
81
+ *
82
+ * Deliberately out of scope: any Amazon Q Developer usage OUTSIDE the IDE
83
+ * plugins covered here — the separate `q chat` CLI, the GitHub-hosted
84
+ * "Amazon Q Developer for GitHub" integration, and Kiro (a distinct AWS
85
+ * product that a third-party source above notes also happens to write into
86
+ * this same directory) are different products with their own storage
87
+ * questions, not verified here and not claimed by this adapter.
88
+ */
89
+ function historyDir() {
90
+ return path.join(os.homedir(), ".aws", "amazonq", "history");
91
+ }
92
+
93
+ const HISTORY_DIR = historyDir();
94
+
95
+ // Bounds for readLines() — same rationale and values as claude-code.js.
96
+ // Not backed by a real chat-history-*.json file this tool was tested
97
+ // against (no install to test with) — see the verification-status note
98
+ // above. A LokiJS-serialized history file is a single JSON document, so in
99
+ // practice this caps one very long "line," the same shape copilot-chat.js
100
+ // already handles for a flat (non-JSONL) chat session snapshot.
101
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
102
+ const READ_TIMEOUT_MS = 60_000;
103
+
104
+ function id() { return "amazon-q"; }
105
+ function label() { return "Amazon Q Developer"; }
106
+
107
+ function available() {
108
+ try { return fs.statSync(HISTORY_DIR).isDirectory(); } catch { return false; }
109
+ }
110
+
111
+ /**
112
+ * Same defensive symlink-following pattern as claude-code.js's
113
+ * isKindFollowingSymlink — see that file's docstring for the full reasoning.
114
+ * Duplicated rather than imported, matching this project's "small,
115
+ * self-contained file" convention.
116
+ */
117
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
118
+ if (checkFn(dirent)) return true;
119
+ if (!dirent.isSymbolicLink()) return false;
120
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
121
+ }
122
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
123
+
124
+ /**
125
+ * Yield { file, mtimeMs, sizeBytes, broken } for every `chat-history-*.json`
126
+ * file directly inside `~/.aws/amazonq/history/` (flat, not recursive —
127
+ * `chatDb.ts`'s `dbDirectory` is the immediate parent of every db file, no
128
+ * further nesting per the source read above).
129
+ *
130
+ * Not filtered to the exact `chat-history-` prefix: `.aws/amazonq/history`
131
+ * is a directory this adapter treats as fully Amazon-Q-owned (per the
132
+ * source above, nothing else writes there), so any `*.json` found there is
133
+ * scanned — the same "don't hard-code a filename pattern likely to drift"
134
+ * caution cursor.js documents for its own key-name filtering, applied here
135
+ * to filenames instead of SQLite keys.
136
+ */
137
+ function* files() {
138
+ let entries;
139
+ try { entries = fs.readdirSync(HISTORY_DIR, { withFileTypes: true }); }
140
+ catch { return; } // no history directory at all — Amazon Q never ran, or never opened chat
141
+
142
+ for (const e of entries) {
143
+ if (!e.name.endsWith(".json")) continue;
144
+ const file = path.join(HISTORY_DIR, e.name);
145
+ if (!isFileFollowingSymlink(file, e)) {
146
+ if (e.isSymbolicLink()) yield { file, broken: true };
147
+ continue;
148
+ }
149
+ let stat;
150
+ try { stat = fs.statSync(file); } catch { yield { file, broken: true }; continue; }
151
+ yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Read one chat-history-*.json file as an array of raw text lines.
157
+ *
158
+ * Streamed line-by-line via readline/promises, same as claude-code.js and
159
+ * copilot-chat.js — LokiJS's `fs` persistence adapter writes its entire
160
+ * collection set as one JSON document (typically not pretty-printed), so in
161
+ * the common case this yields exactly one long "line," bounded by
162
+ * MAX_BYTES; if a given LokiJS version ever pretty-prints or the file
163
+ * otherwise contains embedded newlines, per-line scanning still works
164
+ * unchanged. Status vocabulary matches every other source in this project.
165
+ */
166
+ async function readLines(file) {
167
+ let stat;
168
+ try { stat = fs.statSync(file); }
169
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
170
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
171
+
172
+ const lines = [];
173
+ let bytesRead = 0;
174
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
175
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
176
+
177
+ // Same rationale as claude-code.js: no natural timeout exists anywhere in
178
+ // Node's stream/readline stack, and a retargeted symlink can make the
179
+ // underlying open() block forever with no event ever firing.
180
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
181
+
182
+ try {
183
+ for await (const line of rl) {
184
+ lines.push(line);
185
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1; // +1 for the stripped newline
186
+ }
187
+ return { lines, status: "complete", bytesRead };
188
+ } catch {
189
+ // Whatever WAS read before the failure is real content and may contain
190
+ // a real secret — kept, not discarded, same as every other source here.
191
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
192
+ } finally {
193
+ clearTimeout(timer);
194
+ rl.close();
195
+ stream.destroy();
196
+ }
197
+ }
198
+
199
+ module.exports = { id, label, available, files, readLines };