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,151 @@
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
+ * PearAI's local chat session history.
10
+ *
11
+ * VERIFICATION STATUS (read this before trusting anything below): PearAI is
12
+ * an open-source VS Code fork (github.com/trypear/pearai-app) whose AI chat
13
+ * is powered by "pearai-submodule" — itself an open-source fork of
14
+ * Continue (github.com/continuedev/continue), bundled into the app rather
15
+ * than installed as a marketplace extension. This matters because it means
16
+ * PearAI does NOT use Cursor/VS Code's per-profile `state.vscdb` SQLite
17
+ * approach for chat content — it inherited Continue's own file-based session
18
+ * store instead. This was confirmed directly from two independent sources:
19
+ *
20
+ * 1. PearAI's own shipped source, `pearai-submodule/core/util/paths.ts`
21
+ * (fetched from trypear/pearai-submodule@main), which defines:
22
+ * const CONTINUE_GLOBAL_DIR =
23
+ * process.env.CONTINUE_GLOBAL_DIR ?? path.join(os.homedir(), ".pearai");
24
+ * and derives the sessions folder as `<CONTINUE_GLOBAL_DIR>/sessions`,
25
+ * individual session files as `<sessionId>.json`, and an index file at
26
+ * `sessions.json`. Note this path has NO per-OS branching — it is
27
+ * `os.homedir()/.pearai` on every platform, unlike VS Code-derived
28
+ * products (Cursor, Trae, Void) whose Application Support-style path
29
+ * differs per OS. (PearAI still ships a VS Code-derived Application
30
+ * Support/state.vscdb tree too, for ordinary editor/window state, but
31
+ * that is generic VS Code chrome, not where chat content lives — kept
32
+ * out of scope here the same way claude-code.js and cursor.js each stay
33
+ * scoped to where the actual transcript content lives, not every file
34
+ * the host editor happens to write.)
35
+ * 2. `claude-code-history-viewer` (github.com/jhlee0409/claude-code-history-viewer),
36
+ * an actively maintained, independently authored desktop app that reads
37
+ * this exact same layout — its `src-tauri/src/providers/pearai.rs`
38
+ * module doc reads (fetched verbatim): "PearAI is a fork of Continue
39
+ * that rebrands the global directory from ~/.continue to ~/.pearai. The
40
+ * session store format is identical (<sessionId>.json + sessions.json
41
+ * index)."
42
+ *
43
+ * Both sources agree exactly on the directory, the per-file naming, and the
44
+ * index file. What this source has NOT been checked against is a real
45
+ * PearAI install — PearAI is not installed on the machine this was built on
46
+ * (checked: not in /Applications, not in ~/Library/Application Support, no
47
+ * mdfind hits). If you have PearAI installed and have used its chat at
48
+ * least once, the most useful thing you can do is run `residoo scan` and
49
+ * confirm `sourcesScanned`/`filesScanned` for "pearai" look right against
50
+ * what you can see under ~/.pearai/sessions, then report back either way.
51
+ *
52
+ * Session files are plain JSON text on disk (not SQLite), so — like
53
+ * claude-code.js's JSONL files — they can be streamed and pattern-matched
54
+ * line by line with no parsing required: a pretty-printed session file
55
+ * naturally splits into one scannable line per field, and even a minified
56
+ * one degrades gracefully into a single long line, still fully scanned.
57
+ * `sessions.json` (the index) is included too, on the same "never
58
+ * cherry-pick which files might matter" principle the other sources follow.
59
+ */
60
+ const PEARAI_DIR = path.join(os.homedir(), ".pearai");
61
+ const SESSIONS_DIR = path.join(PEARAI_DIR, "sessions");
62
+
63
+ // Same bounds and same rationale as claude-code.js — no PearAI-specific
64
+ // large-file data point exists (no real install to measure against), so
65
+ // these are carried over unchanged as a generous, conservative backstop.
66
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
67
+ const READ_TIMEOUT_MS = 60_000;
68
+
69
+ function id() { return "pearai"; }
70
+ function label() { return "PearAI"; }
71
+
72
+ function available() {
73
+ try { return fs.statSync(SESSIONS_DIR).isDirectory(); } catch { return false; }
74
+ }
75
+
76
+ /**
77
+ * Same defensive symlink-following helper as claude-code.js — see that
78
+ * file's docstring for the full reasoning. Duplicated rather than shared,
79
+ * matching this project's "each source is a small, self-contained file"
80
+ * convention (stated explicitly in cursor.js).
81
+ */
82
+ function isFileFollowingSymlink(fullPath, dirent) {
83
+ if (dirent.isFile()) return true;
84
+ if (!dirent.isSymbolicLink()) return false;
85
+ try { return fs.statSync(fullPath).isFile(); } catch { return false; }
86
+ }
87
+
88
+ /**
89
+ * Yield { file, mtimeMs, sizeBytes, broken } for every session file found.
90
+ *
91
+ * Unlike claude-code.js's two-level walk (project dir -> transcripts), the
92
+ * confirmed layout here is flat: every `*.json` file directly inside
93
+ * `~/.pearai/sessions` — individual `<sessionId>.json` files plus the
94
+ * `sessions.json` index — so this is a single readdir, not a nested one.
95
+ */
96
+ function* files() {
97
+ let entries;
98
+ try { entries = fs.readdirSync(SESSIONS_DIR, { withFileTypes: true }); }
99
+ catch { return; }
100
+
101
+ for (const e of entries) {
102
+ if (!e.name.endsWith(".json")) continue;
103
+ const file = path.join(SESSIONS_DIR, e.name);
104
+ if (!e.isFile()) {
105
+ const resolved = isFileFollowingSymlink(file, e);
106
+ if (!resolved) {
107
+ if (e.isSymbolicLink()) yield { file, broken: true };
108
+ continue;
109
+ }
110
+ }
111
+ let stat;
112
+ try { stat = fs.statSync(file); } catch { yield { file, broken: true }; continue; }
113
+ yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Read one session file as an array of raw text lines. Same streamed,
119
+ * bounded, honest-partial-status approach as claude-code.js's readLines() —
120
+ * see that file's docstring for the full reasoning, which applies unchanged
121
+ * here since this is likewise a plain text file on disk.
122
+ */
123
+ async function readLines(file) {
124
+ let stat;
125
+ try { stat = fs.statSync(file); }
126
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
127
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
128
+
129
+ const lines = [];
130
+ let bytesRead = 0;
131
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
132
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
133
+
134
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
135
+
136
+ try {
137
+ for await (const line of rl) {
138
+ lines.push(line);
139
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1;
140
+ }
141
+ return { lines, status: "complete", bytesRead };
142
+ } catch {
143
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
144
+ } finally {
145
+ clearTimeout(timer);
146
+ rl.close();
147
+ stream.destroy();
148
+ }
149
+ }
150
+
151
+ module.exports = { id, label, available, files, readLines };
@@ -0,0 +1,130 @@
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
+ * "Pi" (earendil-works/pi on GitHub; installed as `@mariozechner/pi-coding-agent`
10
+ * from npm, run as the `pi` CLI) local session transcripts.
11
+ *
12
+ * VERIFICATION STATUS: corroborated directly from the project's own shipped
13
+ * documentation (fetched from the live repo during this source's research),
14
+ * but NOT checked against a real install on the machine this source was
15
+ * built on (no `~/.pi` directory exists there; see CONTRIBUTING.md).
16
+ *
17
+ * `packages/coding-agent/docs/sessions.md` in the `earendil-works/pi` repo —
18
+ * the project's own docs, not a third party's description of it — states
19
+ * plainly: "Sessions auto-save to `~/.pi/agent/sessions/`, organized by
20
+ * working directory. Each session is a JSONL file with a tree structure,"
21
+ * further describing entries with `id`/`parentId` fields (branching), model
22
+ * changes, thinking-level changes, labels, compactions, and branch summaries
23
+ * all living in the same JSONL stream. "Organized by working directory"
24
+ * means sessions live under per-project subdirectories rather than flat in
25
+ * `sessions/` itself — the exact subdirectory naming isn't spelled out in
26
+ * that doc, so this source walks recursively for `*.jsonl` rather than
27
+ * assuming a fixed depth, the same tolerance claude-code.js applies to
28
+ * project-slug directory names it doesn't try to decode either.
29
+ *
30
+ * Independently, jazzyalex/agent-sessions (github.com/jazzyalex/agent-sessions,
31
+ * 800+ stars, a real macOS app built specifically to parse local
32
+ * AI-coding-agent session history) lists Pi among the CLI agents whose local
33
+ * history it reads, corroborating that this is real, currently-scanned-by-
34
+ * someone-else session data rather than a doc describing an unshipped plan.
35
+ */
36
+ const HOME = os.homedir();
37
+ const ROOT = path.join(HOME, ".pi", "agent", "sessions");
38
+
39
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB — same backstop as claude-code.js.
40
+ const READ_TIMEOUT_MS = 60_000;
41
+ const MAX_WALK_DEPTH = 8;
42
+
43
+ function id() { return "pi-agent"; }
44
+ function label() { return "Pi"; }
45
+
46
+ function available() {
47
+ try { return fs.statSync(ROOT).isDirectory(); } catch { return false; }
48
+ }
49
+
50
+ /**
51
+ * Same defensive symlink-following helpers as claude-code.js — see that
52
+ * file's docstring. Duplicated rather than imported, per this project's
53
+ * self-contained-source-file convention (see cursor.js's docstring).
54
+ */
55
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
56
+ if (checkFn(dirent)) return true;
57
+ if (!dirent.isSymbolicLink()) return false;
58
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
59
+ }
60
+ const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
61
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
62
+
63
+ /**
64
+ * Recursively yield { file, mtimeMs, sizeBytes, broken } for every plain file
65
+ * under `dir` whose name passes `matchFn`, following symlinks and reporting
66
+ * one that resolves to neither a file nor a directory as `broken: true` — see
67
+ * factory-droid.js's walk() for the identical reasoning (this project
68
+ * duplicates this small helper per source file rather than sharing it; see
69
+ * cursor.js's docstring on why).
70
+ */
71
+ function* walk(dir, depth, matchFn) {
72
+ if (depth > MAX_WALK_DEPTH) return;
73
+ let entries;
74
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
75
+ catch { return; }
76
+
77
+ for (const e of entries) {
78
+ const full = path.join(dir, e.name);
79
+ if (isDirFollowingSymlink(full, e)) {
80
+ yield* walk(full, depth + 1, matchFn);
81
+ continue;
82
+ }
83
+ const isFile = isFileFollowingSymlink(full, e);
84
+ if (!isFile) {
85
+ if (e.isSymbolicLink()) yield { file: full, broken: true };
86
+ continue;
87
+ }
88
+ if (!matchFn(e.name)) continue;
89
+ let stat;
90
+ try { stat = fs.statSync(full); } catch { yield { file: full, broken: true }; continue; }
91
+ yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
92
+ }
93
+ }
94
+
95
+ function* files() {
96
+ yield* walk(ROOT, 0, (name) => name.endsWith(".jsonl"));
97
+ }
98
+
99
+ /**
100
+ * Read one JSONL session as raw text lines. Identical streaming/timeout/
101
+ * partial-read discipline to claude-code.js's readLines().
102
+ */
103
+ async function readLines(file) {
104
+ let stat;
105
+ try { stat = fs.statSync(file); }
106
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
107
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
108
+
109
+ const lines = [];
110
+ let bytesRead = 0;
111
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
112
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
113
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
114
+
115
+ try {
116
+ for await (const line of rl) {
117
+ lines.push(line);
118
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1;
119
+ }
120
+ return { lines, status: "complete", bytesRead };
121
+ } catch {
122
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
123
+ } finally {
124
+ clearTimeout(timer);
125
+ rl.close();
126
+ stream.destroy();
127
+ }
128
+ }
129
+
130
+ module.exports = { id, label, available, files, readLines };
@@ -0,0 +1,189 @@
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
+ * Qodo Gen (formerly CodiumAI / Codium) — the VS Code and JetBrains AI chat
10
+ * extension published by Qodo.
11
+ *
12
+ * VERIFICATION STATUS (read this before trusting anything below):
13
+ * multi-source-corroborated-but-UNVERIFIED against a real install, and
14
+ * meaningfully WEAKER corroboration than this project's other sources — read
15
+ * this whole note before trusting it. Neither VS Code, JetBrains, nor any
16
+ * Qodo extension is installed on the machine this adapter was built on
17
+ * (checked: no /Applications/*Code*.app, no `code` on PATH, no ~/.qodo
18
+ * directory, no ~/Library/Application Support/JetBrains/<product>/options
19
+ * containing anything Qodo-named). Qodo Gen is closed-source (the public
20
+ * `Codium-ai/codiumai-vscode-release` / `codiumai-jetbrains-release` repos
21
+ * are release-notes/changelog mirrors only, no extension source), so unlike
22
+ * cody.js and amazon-q.js in this project, this adapter's path claim could
23
+ * NOT be checked against the vendor's own source code — only its docs:
24
+ *
25
+ * 1. Qodo's own current documentation,
26
+ * docs.qodo.ai/qodo-documentation/qodo-gen/chat/chat-history (fetched
27
+ * 2026-09-02): "Qodo saves chat history locally in the user's home
28
+ * directory at `.qodo/history`. The file naming uses a hash of the
29
+ * workspace path to ensure uniqueness," plus: history not touched in
30
+ * 90+ days is auto-deleted; VS Code does NOT migrate history across
31
+ * extension upgrades (a fresh install loses it) while JetBrains DOES.
32
+ * 2. Qodo's own changelog, docs.qodo.ai/changelog, entry for "Qodo 1.0.8"
33
+ * (24 Apr 25): "The History file is now named `.qodo/history` and has
34
+ * been moved to the user folder for better file organization. The file
35
+ * is named using a hash of the workspace path to ensure uniqueness" —
36
+ * independently dated/versioned wording that agrees with (1), and
37
+ * implies this is a genuine change from an earlier, different layout
38
+ * (see the IDAHO-VAULT finding below for what that earlier layout
39
+ * likely looked like).
40
+ *
41
+ * Both of the above are the SAME vendor (Qodo's own docs site stating the
42
+ * same fact twice, once in reference docs and once in a changelog) — NOT
43
+ * two independent parties, despite counting as two fetches. Searching
44
+ * specifically for independent, third-party confirmation of the exact
45
+ * `~/.qodo/history` BASE DIRECTORY turned up nothing conclusive: the one
46
+ * real, concrete GitHub artifact found (`LAF-US/IDAHO-VAULT`,
47
+ * `GIT-REMOVAL-COMMANDS.txt`, a real user's own repo, commits from mid/late
48
+ * 2026, well after the v1.0.8 changelog date) shows a file
49
+ * `.qodo/history/<64-hex-hash>.json` committed INSIDE that repo's own
50
+ * project directory, not under that user's home directory — but on
51
+ * inspection this is very likely Qodo's separate automated code-review
52
+ * product (`qodo-ai/command`, the "Qodo Gen CLI" / PR-Agent lineage, whose
53
+ * own README describes exactly this "review your repo from the terminal,
54
+ * for CI/CD" use case) writing a per-repo review-report artifact, given the
55
+ * surrounding context ("Qodo review (...) flagged a medium-severity bug")
56
+ * reads as a CI/PR-review finding, not a chat transcript. That is a
57
+ * DIFFERENT Qodo product from the IDE chat extension this adapter targets,
58
+ * so it neither confirms nor contradicts the `~/.qodo/history` claim above
59
+ * for Qodo Gen specifically — it only corroborates that Qodo's tooling in
60
+ * general uses this exact `.qodo/history/<hash>.json` naming shape
61
+ * somewhere, which is real, but not the specific base-directory claim this
62
+ * adapter depends on.
63
+ *
64
+ * Built anyway, per CONTRIBUTING.md's allowance for a source with credible
65
+ * corroboration but no real install — but flagged here, honestly, as this
66
+ * project's single WEAKEST-verified source: one primary party (the vendor,
67
+ * stated twice) rather than genuinely independent agreement, and a live
68
+ * install is unusually likely to be needed to firm this up (there is no
69
+ * source code to fall back on the way cody.js/amazon-q.js could). If you
70
+ * have Qodo Gen installed, confirming `~/.qodo/history/*.json` actually
71
+ * exists and holds real chat content — or reporting exactly where it
72
+ * actually lives if not — is the single most useful thing you can do for
73
+ * this source (see CONTRIBUTING.md).
74
+ *
75
+ * No per-OS path branching in either Qodo source read above — the docs
76
+ * describe a plain home-directory dotfolder (`~/.qodo` /
77
+ * `%USERPROFILE%\.qodo` on Windows), consistent with it being deliberately
78
+ * IDE-agnostic: the same docs note JetBrains preserves this history across
79
+ * upgrades where VS Code's own extension-local storage would not, which only
80
+ * makes sense if both IDEs read/write ONE shared, non-IDE-specific location
81
+ * — the same reasoning amazon-q.js documents for `~/.aws/amazonq/history`
82
+ * and continue.js already documents for `~/.continue` in this project.
83
+ */
84
+ function historyDir() {
85
+ return path.join(os.homedir(), ".qodo", "history");
86
+ }
87
+
88
+ const HISTORY_DIR = historyDir();
89
+
90
+ // Bounds for readLines() — same rationale and values as claude-code.js.
91
+ // Not backed by a real Qodo Gen history file this tool was tested against
92
+ // (no install to test with) — see the verification-status note above.
93
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
94
+ const READ_TIMEOUT_MS = 60_000;
95
+
96
+ function id() { return "qodo-gen"; }
97
+ function label() { return "Qodo Gen"; }
98
+
99
+ function available() {
100
+ try { return fs.statSync(HISTORY_DIR).isDirectory(); } catch { return false; }
101
+ }
102
+
103
+ /**
104
+ * Same defensive symlink-following pattern as claude-code.js's
105
+ * isKindFollowingSymlink — see that file's docstring for the full reasoning.
106
+ * Duplicated rather than imported, matching this project's "small,
107
+ * self-contained file" convention.
108
+ */
109
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
110
+ if (checkFn(dirent)) return true;
111
+ if (!dirent.isSymbolicLink()) return false;
112
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
113
+ }
114
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
115
+
116
+ /**
117
+ * Yield { file, mtimeMs, sizeBytes, broken } for every `*.json` file
118
+ * directly inside `~/.qodo/history/` (flat, not recursive — every source
119
+ * describing this layout, vendor docs and the IDAHO-VAULT artifact alike,
120
+ * shows hash-named files as immediate children, no further nesting).
121
+ *
122
+ * Not filtered to a specific filename shape (e.g. requiring a hex-looking
123
+ * name): the exact hash algorithm/length isn't confirmed (docs say "a hash
124
+ * of the workspace path" without naming one), so — same caution cursor.js
125
+ * documents for not hard-coding a key-name allowlist likely to drift — any
126
+ * `*.json` found directly in this Qodo-owned directory is scanned rather
127
+ * than pattern-matched by filename.
128
+ */
129
+ function* files() {
130
+ let entries;
131
+ try { entries = fs.readdirSync(HISTORY_DIR, { withFileTypes: true }); }
132
+ catch { return; } // no history directory at all — Qodo Gen never ran, or never opened chat
133
+
134
+ for (const e of entries) {
135
+ if (!e.name.endsWith(".json")) continue;
136
+ const file = path.join(HISTORY_DIR, e.name);
137
+ if (!isFileFollowingSymlink(file, e)) {
138
+ if (e.isSymbolicLink()) yield { file, broken: true };
139
+ continue;
140
+ }
141
+ let stat;
142
+ try { stat = fs.statSync(file); } catch { yield { file, broken: true }; continue; }
143
+ yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Read one `.qodo/history/*.json` file as an array of raw text lines.
149
+ *
150
+ * Streamed line-by-line via readline/promises, same as claude-code.js and
151
+ * amazon-q.js — whether a given file turns out to be one flat JSON document
152
+ * or something line-delimited, per-line scanning handles both: a flat
153
+ * document just becomes one long "line," bounded by MAX_BYTES. Status
154
+ * vocabulary matches every other source in this project.
155
+ */
156
+ async function readLines(file) {
157
+ let stat;
158
+ try { stat = fs.statSync(file); }
159
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
160
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
161
+
162
+ const lines = [];
163
+ let bytesRead = 0;
164
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
165
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
166
+
167
+ // Same rationale as claude-code.js: no natural timeout exists anywhere in
168
+ // Node's stream/readline stack, and a retargeted symlink can make the
169
+ // underlying open() block forever with no event ever firing.
170
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
171
+
172
+ try {
173
+ for await (const line of rl) {
174
+ lines.push(line);
175
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1; // +1 for the stripped newline
176
+ }
177
+ return { lines, status: "complete", bytesRead };
178
+ } catch {
179
+ // Whatever WAS read before the failure is real content and may contain
180
+ // a real secret — kept, not discarded, same as every other source here.
181
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
182
+ } finally {
183
+ clearTimeout(timer);
184
+ rl.close();
185
+ stream.destroy();
186
+ }
187
+ }
188
+
189
+ module.exports = { id, label, available, files, readLines };