residoo 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +334 -46
  2. package/SECURITY.md +29 -22
  3. package/package.json +1 -1
  4. package/src/cli.js +249 -17
  5. package/src/integrity.js +689 -0
  6. package/src/patterns.js +78 -5
  7. package/src/report.js +188 -8
  8. package/src/rotation.js +834 -0
  9. package/src/sources/agent-configs.js +308 -0
  10. package/src/sources/aider.js +361 -0
  11. package/src/sources/amazon-q.js +199 -0
  12. package/src/sources/antigravity-cli.js +155 -0
  13. package/src/sources/cline.js +208 -0
  14. package/src/sources/codebuff.js +295 -0
  15. package/src/sources/codex-cli.js +258 -0
  16. package/src/sources/cody.js +325 -0
  17. package/src/sources/continue.js +408 -0
  18. package/src/sources/copilot-chat.js +272 -0
  19. package/src/sources/copilot-cli.js +300 -0
  20. package/src/sources/crush.js +364 -0
  21. package/src/sources/cursor.js +374 -0
  22. package/src/sources/devin-cli.js +241 -0
  23. package/src/sources/factory-droid.js +153 -0
  24. package/src/sources/fx.js +136 -0
  25. package/src/sources/gemini-cli.js +242 -0
  26. package/src/sources/goose.js +366 -0
  27. package/src/sources/grok-cli.js +267 -0
  28. package/src/sources/hermes.js +282 -0
  29. package/src/sources/index.js +172 -8
  30. package/src/sources/jetbrains-ai-assistant.js +343 -0
  31. package/src/sources/jetbrains-junie.js +292 -0
  32. package/src/sources/kilo-code.js +430 -0
  33. package/src/sources/kimi-code.js +147 -0
  34. package/src/sources/kiro-cli.js +393 -0
  35. package/src/sources/kiro-ide.js +230 -0
  36. package/src/sources/llm.js +328 -0
  37. package/src/sources/mentat.js +143 -0
  38. package/src/sources/open-interpreter.js +224 -0
  39. package/src/sources/openclaw.js +218 -0
  40. package/src/sources/opencode.js +379 -0
  41. package/src/sources/openhands.js +181 -0
  42. package/src/sources/pearai.js +151 -0
  43. package/src/sources/pi-agent.js +130 -0
  44. package/src/sources/project-artifacts.js +355 -0
  45. package/src/sources/qodo-gen.js +189 -0
  46. package/src/sources/qwen-code.js +244 -0
  47. package/src/sources/roo-code.js +239 -0
  48. package/src/sources/trae.js +294 -0
  49. package/src/sources/void.js +273 -0
  50. package/src/sources/warp.js +395 -0
  51. package/src/sources/windsurf.js +256 -0
  52. package/src/sources/zed.js +374 -0
@@ -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 };
@@ -0,0 +1,244 @@
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
+ * Qwen Code (QwenLM/qwen-code) session/chat transcripts.
10
+ *
11
+ * VERIFICATION STATUS (read this before trusting anything below): this
12
+ * source is corroborated by the actual current source code of the official
13
+ * QwenLM/qwen-code repository on GitHub (fetched and read directly) plus
14
+ * that project's own published docs and GitHub issues from real users
15
+ * describing real installs. It has NOT been checked against a real Qwen
16
+ * Code install or real transcript content — Qwen Code is not installed on
17
+ * the machine this adapter was built on (checked: no `qwen` on PATH, no
18
+ * `~/.qwen` directory, no `@qwen-code/qwen-code` in global npm packages).
19
+ * If you have Qwen Code installed, the most useful thing you can do is run
20
+ * `residoo scan` and confirm `sourcesScanned`/`filesScanned` look right for
21
+ * what you know is actually on disk under `~/.qwen`, then report back
22
+ * either way.
23
+ *
24
+ * Qwen Code began as a fork of google-gemini/gemini-cli (same author's
25
+ * `ChatRecordingService` naming, same general architecture) retargeted at
26
+ * Alibaba's Qwen models, but its storage layout has since diverged from
27
+ * gemini-cli's rather than staying a copy with the dotfolder renamed — this
28
+ * source was built by reading QwenLM/qwen-code's OWN code, not by assuming
29
+ * it still matches gemini-cli.js's layout.
30
+ *
31
+ * Storage location, confirmed directly from source
32
+ * (packages/core/src/config/storage.ts and
33
+ * packages/core/src/services/chatRecordingService.ts on QwenLM/qwen-code's
34
+ * `main` branch):
35
+ *
36
+ * - Base directory: `$QWEN_HOME/.qwen` if that env var is set (confirmed
37
+ * directly in `getGlobalQwenDir()`'s source: `process.env['QWEN_HOME']`,
38
+ * resolved and used as-is), otherwise `~/.qwen` (`QWEN_DIR = '.qwen'`
39
+ * joined onto `os.homedir()`, with a documented fallback to
40
+ * `os.tmpdir()/.qwen` if `os.homedir()` itself comes back empty — no
41
+ * other per-OS branching, same as gemini-cli).
42
+ * - Current (as of this research) chat storage:
43
+ * `~/.qwen/projects/<sanitizedCwd>/chats/<sessionId>.jsonl` —
44
+ * `PROJECT_DIR_NAME = 'projects'`, `getProjectDir()` joins it with
45
+ * `sanitizeCwd(projectRoot)`, and `ChatRecordingService` writes
46
+ * `path.join(storage.getProjectDir(), 'chats', sessionId + '.jsonl')`,
47
+ * one JSON record appended per line (metadata line first, then one
48
+ * record per turn) — confirmed directly in chatRecordingService.ts.
49
+ * Inactive sessions get moved into a `chats/archive/` subdirectory
50
+ * (per Qwen Code's own daemon/session-lifecycle docs) rather than
51
+ * deleted, so archived transcripts are still real, still-scannable
52
+ * content sitting one level deeper.
53
+ * - A SEPARATE, older-style temp dir also still exists in the current
54
+ * source: `~/.qwen/tmp/<hash>/` (`TMP_DIR_NAME = 'tmp'`,
55
+ * `getProjectTempDir()` using a SHA-256 `getProjectHash()`, the same
56
+ * shape gemini-cli.js's tmp dir uses) — but on the current codebase
57
+ * this is used for `checkpoints/`, not `chats/`. Because this project
58
+ * is a fork whose own history plausibly went through the same
59
+ * "everything under tmp/" phase gemini-cli.js's docstring describes
60
+ * before settling on `projects/`, this source defensively ALSO walks
61
+ * `tmp/<id>/chats/` if present — cheap when absent (a single failed
62
+ * readdir), and catches transcripts left behind by an older Qwen Code
63
+ * version without having to know exactly which version drew the line.
64
+ *
65
+ * Deliberately OUT OF SCOPE: an experimental PROJECT-LOCAL history mode
66
+ * referenced in qwen-code's own issue tracker — `getHistoryDir()` falling
67
+ * back to `<projectRoot>/.qwen/chat-history/` instead of a home-directory
68
+ * location when enabled. Every other source in residoo (including this
69
+ * one's primary path) scans one well-known location under the user's home
70
+ * directory; walking every project directory anywhere on disk looking for
71
+ * a stray `.qwen/chat-history` folder is a fundamentally different, far
72
+ * broader operation this source does not attempt. Also out of scope, for
73
+ * the same reasoning as gemini-cli.js: `<projectTempDir>/checkpoints/`.
74
+ *
75
+ * Sources consulted: QwenLM/qwen-code source on GitHub (storage.ts,
76
+ * chatRecordingService.ts, fetched from the `main` branch); Qwen Code's own
77
+ * published docs at qwenlm.github.io/qwen-code-docs (session-lifecycle and
78
+ * daemon-mode pages, confirming the `chats/` + `chats/archive/` structure
79
+ * and JSONL format at a description level); GitHub issues #2373, #1100 and
80
+ * #3606 on QwenLM/qwen-code (real users describing session resume/export
81
+ * behavior against their own installs).
82
+ */
83
+ function qwenHomeDir() {
84
+ const base = process.env.QWEN_HOME || os.homedir() || os.tmpdir();
85
+ return path.join(base, ".qwen");
86
+ }
87
+
88
+ const ROOT = qwenHomeDir();
89
+ const PROJECTS_DIR = path.join(ROOT, "projects");
90
+ const TMP_DIR = path.join(ROOT, "tmp"); // legacy/defensive, see module docstring
91
+ const CHAT_FILE_EXT = /\.(jsonl|json)$/i;
92
+
93
+ // See gemini-cli.js's identical constant for the reasoning — bounded rather
94
+ // than hard-coded at exactly the one nesting level (chats/archive/)
95
+ // confirmed from source, so a further nesting change gets scanned rather
96
+ // than silently missed, and bounded at all so a symlink cycle can't turn
97
+ // this into an infinite walk.
98
+ const MAX_CHATS_DEPTH = 4;
99
+
100
+ // Same caveat as gemini-cli.js's identical constant: a generous, untested
101
+ // backstop, not evidence from a real large Qwen Code transcript — no
102
+ // install was available to produce one.
103
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
104
+ const READ_TIMEOUT_MS = 60_000;
105
+
106
+ function id() { return "qwen-code"; }
107
+ function label() { return "Qwen Code"; }
108
+
109
+ function available() {
110
+ try { return fs.statSync(ROOT).isDirectory(); } catch { return false; }
111
+ }
112
+
113
+ /**
114
+ * Same defensive symlink-following pattern as claude-code.js — see that
115
+ * file's docstring for the full reasoning. Duplicated rather than imported:
116
+ * each source here is meant to be a small, self-contained file a reviewer
117
+ * can audit on its own (see CONTRIBUTING.md).
118
+ */
119
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
120
+ if (checkFn(dirent)) return true;
121
+ if (!dirent.isSymbolicLink()) return false;
122
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
123
+ }
124
+ const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
125
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
126
+
127
+ /**
128
+ * Walk a `chats/` directory (or one of its subdirectories, e.g. `archive/`)
129
+ * for candidate session files, up to MAX_CHATS_DEPTH levels deep. A missing
130
+ * directory yields nothing and is NOT reported broken — a project that has
131
+ * never recorded a chat, or a Qwen Code version that never wrote `tmp/`
132
+ * chats at all, is normal absence, same convention cursor.js's
133
+ * statIfPresent() uses. A directory that exists but can't be read, or a
134
+ * symlink that can't be resolved, IS reported broken.
135
+ */
136
+ function* walkChatFiles(dir, depth) {
137
+ let entries;
138
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
139
+ catch { return; }
140
+
141
+ for (const e of entries) {
142
+ const full = path.join(dir, e.name);
143
+
144
+ if (isDirFollowingSymlink(full, e)) {
145
+ if (depth < MAX_CHATS_DEPTH) yield* walkChatFiles(full, depth + 1);
146
+ continue;
147
+ }
148
+ if (isFileFollowingSymlink(full, e)) {
149
+ if (!CHAT_FILE_EXT.test(e.name)) continue;
150
+ let stat;
151
+ try { stat = fs.statSync(full); } catch { yield { file: full, broken: true }; continue; }
152
+ yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
153
+ continue;
154
+ }
155
+ // Neither resolves as a directory nor a file: a dangling symlink is the
156
+ // one case worth reporting.
157
+ if (e.isSymbolicLink()) yield { file: full, broken: true };
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Enumerate every immediate child of `parentDir` as a candidate project
163
+ * directory and walk its `chats/` subdirectory. Shared between the current
164
+ * `projects/<sanitizedCwd>/` layout and the legacy/defensive `tmp/<hash>/`
165
+ * one — both are "one directory per project, chats live in a `chats/`
166
+ * subdirectory of it," differing only in how the per-project name was
167
+ * derived, which this source does not try to recompute (see module
168
+ * docstring on `sanitizeCwd`/`getProjectHash`).
169
+ */
170
+ function* walkProjectDirs(parentDir) {
171
+ let projectDirs;
172
+ try { projectDirs = fs.readdirSync(parentDir, { withFileTypes: true }); }
173
+ catch { return; }
174
+
175
+ for (const projEnt of projectDirs) {
176
+ const projDir = path.join(parentDir, projEnt.name);
177
+ if (!isDirFollowingSymlink(projDir, projEnt)) {
178
+ if (projEnt.isSymbolicLink()) yield { file: projDir, broken: true };
179
+ continue;
180
+ }
181
+ yield* walkChatFiles(path.join(projDir, "chats"), 0);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Yield { file, mtimeMs, sizeBytes, broken } for every Qwen Code chat
187
+ * transcript found under `~/.qwen/projects/<id>/chats/` (current layout)
188
+ * and, defensively, `~/.qwen/tmp/<id>/chats/` (legacy layout — see module
189
+ * docstring). A project directory present in both would be enumerated
190
+ * independently under each root; in practice a given project's chats live
191
+ * under one or the other depending on which Qwen Code version wrote them,
192
+ * so this is not expected to double-count real files, only to widen which
193
+ * Qwen Code versions' transcripts get found.
194
+ */
195
+ function* files() {
196
+ yield* walkProjectDirs(PROJECTS_DIR);
197
+ yield* walkProjectDirs(TMP_DIR);
198
+ }
199
+
200
+ /**
201
+ * Read one transcript as an array of raw text lines. Identical approach to
202
+ * claude-code.js's readLines() — see that file's docstring for the full
203
+ * reasoning (streamed via readline/promises, a generous size cap, a hard
204
+ * read timeout since Node's stream/readline stack has none built in); not
205
+ * re-derived here since nothing about it is Qwen-Code-specific.
206
+ *
207
+ * Works the same whether `file` is JSONL (the current, and documented,
208
+ * format) or a legacy whole-session `.json` document — per the adapter
209
+ * contract, lines don't need to be valid JSON individually, they just need
210
+ * pattern-matching against.
211
+ */
212
+ async function readLines(file) {
213
+ let stat;
214
+ try { stat = fs.statSync(file); }
215
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
216
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
217
+
218
+ const lines = [];
219
+ let bytesRead = 0;
220
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
221
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
222
+
223
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
224
+
225
+ try {
226
+ for await (const line of rl) {
227
+ lines.push(line);
228
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1; // +1 for the stripped newline
229
+ }
230
+ return { lines, status: "complete", bytesRead };
231
+ } catch {
232
+ // Whatever WAS read before the failure is real content and may contain
233
+ // a real secret — discarding it because the file didn't finish cleanly
234
+ // would be a silent false negative, which is worse than an honest
235
+ // "partial" label.
236
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
237
+ } finally {
238
+ clearTimeout(timer);
239
+ rl.close();
240
+ stream.destroy();
241
+ }
242
+ }
243
+
244
+ module.exports = { id, label, available, files, readLines };
@@ -0,0 +1,239 @@
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
+ * Roo Code (VS Code extension, a community fork of Cline) session history.
10
+ *
11
+ * VERIFICATION STATUS: read directly out of Roo Code's own current source on
12
+ * GitHub (RooCodeInc/Roo-Code) during this source's research, not guessed
13
+ * from Cline's near-identical layout even though the two are in fact close:
14
+ * - src/package.json — `"name": "roo-cline"`, `"publisher":
15
+ * "RooVeterinaryInc"`. VS Code lowercases publisher.name when it names
16
+ * the on-disk globalStorage folder, so the real folder is
17
+ * `rooveterinaryinc.roo-cline`, not the mixed-case form the manifest
18
+ * declares — corroborated independently by a real user's own reported
19
+ * path in a GitHub issue about disk usage
20
+ * (`~/.vscode-server/data/User/globalStorage/rooveterinaryinc.roo-cline/tasks`,
21
+ * RooCodeInc/Roo-Code#4174), i.e. an actual observed path from an actual
22
+ * install, not just the manifest's declared casing.
23
+ * - src/shared/globalFileNames.ts — the per-task filenames below.
24
+ * - src/core/task-persistence/TaskHistoryStore.ts — its own doc comment
25
+ * states the exact layout verbatim: "Each task's HistoryItem is stored
26
+ * as an individual JSON file in its existing task directory
27
+ * (`globalStorage/tasks/<taskId>/history_item.json`). A single index
28
+ * file (`globalStorage/tasks/_index.json`) is maintained as a cache for
29
+ * fast list reads at startup."
30
+ * What this could NOT be checked against: a real Roo Code install on the
31
+ * machine this source was built on — VS Code itself isn't installed there.
32
+ * See CONTRIBUTING.md for what "verified" is supposed to mean and treat
33
+ * findings from this source accordingly until someone with Roo Code actually
34
+ * installed confirms it against real data.
35
+ *
36
+ * Roo Code writes one JSON file per concern into a per-task directory under
37
+ * its extension's VS Code globalStorage folder, plus one root-level index:
38
+ *
39
+ * <VS Code User dir>/globalStorage/rooveterinaryinc.roo-cline/tasks/
40
+ * _index.json - cache of every task's HistoryItem
41
+ * <taskId>/
42
+ * api_conversation_history.json - full message history sent to the model
43
+ * ui_messages.json - the rendered chat transcript
44
+ * history_item.json - this task's own HistoryItem record
45
+ * task_metadata.json - files touched, model/token usage
46
+ *
47
+ * Filenames inside a task directory are deliberately NOT allow-listed beyond
48
+ * "every *.json file directly inside tasks/<taskId>/": this fork has already
49
+ * added fields over time (history_item.json is a comparatively recent
50
+ * addition per its own source comment above) and hard-coding today's list is
51
+ * exactly the kind of thing likely to go stale the same way cursor.js's
52
+ * docstring describes for Cursor's own key names. `_index.json` is one level
53
+ * up, directly inside tasks/, so it is checked separately rather than by the
54
+ * per-task glob. A task's `checkpoints/` subdirectory (shadow-git snapshots
55
+ * used for file revert) is deliberately NOT walked — those are git object
56
+ * stores, not text transcripts.
57
+ *
58
+ * Base directory: VS Code has a portable/remote/Insiders/fork multiverse of
59
+ * possible per-profile "User" directories. This source checks the two by far
60
+ * most common ones on each OS — standard VS Code ("Code") and VS Code
61
+ * Insiders ("Code - Insiders") — and deliberately does NOT attempt every
62
+ * fork (VSCodium, etc.) or the separate ~/.vscode-server tree used by
63
+ * remote-SSH sessions (even though the corroborating GitHub issue above
64
+ * happens to be exactly that case) — a named, narrower scope rather than a
65
+ * guess at an exhaustive list.
66
+ *
67
+ * Also named rather than silently assumed: Roo Code supports a configurable
68
+ * custom storage base path (per open GitHub issues discussing it) that would
69
+ * move all of the above somewhere this source does not check. That is a
70
+ * real, acknowledged gap, not a silent one.
71
+ */
72
+ const EXT_ID = "rooveterinaryinc.roo-cline";
73
+
74
+ function vscodeUserDirs() {
75
+ const home = os.homedir();
76
+ const variants = ["Code", "Code - Insiders"];
77
+ if (process.platform === "darwin") {
78
+ return variants.map((v) => path.join(home, "Library", "Application Support", v, "User"));
79
+ }
80
+ if (process.platform === "win32") {
81
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
82
+ return variants.map((v) => path.join(appData, v, "User"));
83
+ }
84
+ // Linux and other XDG-following unix platforms.
85
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
86
+ return variants.map((v) => path.join(configHome, v, "User"));
87
+ }
88
+
89
+ function tasksDirs() {
90
+ return vscodeUserDirs().map((userDir) => path.join(userDir, "globalStorage", EXT_ID, "tasks"));
91
+ }
92
+
93
+ // Bounds for readLines() — same rationale and same values as claude-code.js.
94
+ // Not backed by a real Roo Code transcript this tool was tested against (no
95
+ // install to test with) — see the verification-status note above.
96
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
97
+ const READ_TIMEOUT_MS = 60_000;
98
+
99
+ function id() { return "roo-code"; }
100
+ function label() { return "Roo Code"; }
101
+
102
+ function available() {
103
+ return tasksDirs().some((dir) => {
104
+ try { return fs.statSync(dir).isDirectory(); } catch { return false; }
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Same defensive symlink-following pattern as claude-code.js's
110
+ * isDirFollowingSymlink/isFileFollowingSymlink — see that file's docstring
111
+ * for the full reasoning. Duplicated rather than imported: each source in
112
+ * this project is meant to be a small, self-contained file a reviewer can
113
+ * audit on its own (see CONTRIBUTING.md and cursor.js's own note on this).
114
+ */
115
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
116
+ if (checkFn(dirent)) return true;
117
+ if (!dirent.isSymbolicLink()) return false;
118
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
119
+ }
120
+ const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
121
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
122
+
123
+ /**
124
+ * Resolve one constructed file path (not a directory-listing Dirent) into
125
+ * zero or one files() entries — used for tasks/_index.json, which is a fixed
126
+ * filename rather than something discovered by listing a directory. Same
127
+ * convention as cursor.js's statIfPresent: a path that simply doesn't exist
128
+ * yields nothing (normal — e.g. an install old enough to predate this file),
129
+ * `broken: true` is reserved for a path that looked like it should resolve
130
+ * and didn't (a dangling symlink).
131
+ */
132
+ function* statIfPresent(filePath) {
133
+ let lst;
134
+ try { lst = fs.lstatSync(filePath); }
135
+ catch { return; }
136
+
137
+ if (lst.isSymbolicLink()) {
138
+ try {
139
+ const st = fs.statSync(filePath);
140
+ if (!st.isFile()) { yield { file: filePath, broken: true }; return; }
141
+ yield { file: filePath, mtimeMs: st.mtimeMs, sizeBytes: st.size, broken: false };
142
+ } catch {
143
+ yield { file: filePath, broken: true };
144
+ }
145
+ return;
146
+ }
147
+
148
+ if (!lst.isFile()) return;
149
+ yield { file: filePath, mtimeMs: lst.mtimeMs, sizeBytes: lst.size, broken: false };
150
+ }
151
+
152
+ /**
153
+ * Yield { file, mtimeMs, sizeBytes, broken } for tasks/_index.json and every
154
+ * *.json file directly inside every task directory, across every candidate
155
+ * VS Code User dir.
156
+ *
157
+ * broken:true marks a tasks/ entry or a *.json entry that looked like it
158
+ * should resolve (chiefly a dangling symlink) but didn't — never silently
159
+ * skipped, same convention as claude-code.js and cursor.js.
160
+ */
161
+ function* files() {
162
+ for (const tasksDir of tasksDirs()) {
163
+ yield* statIfPresent(path.join(tasksDir, "_index.json"));
164
+
165
+ let taskEntries;
166
+ try { taskEntries = fs.readdirSync(tasksDir, { withFileTypes: true }); }
167
+ catch { continue; } // this VS Code variant/profile simply has no Roo Code tasks dir — normal, not broken
168
+
169
+ for (const taskEntry of taskEntries) {
170
+ if (taskEntry.name === "_index.json") continue; // already handled above
171
+ const taskDir = path.join(tasksDir, taskEntry.name);
172
+ if (!isDirFollowingSymlink(taskDir, taskEntry)) {
173
+ if (taskEntry.isSymbolicLink()) yield { file: taskDir, broken: true };
174
+ continue; // a stray non-directory entry under tasks/ is out of scope, not broken
175
+ }
176
+
177
+ let fileEntries;
178
+ try { fileEntries = fs.readdirSync(taskDir, { withFileTypes: true }); }
179
+ catch { yield { file: taskDir, broken: true }; continue; }
180
+
181
+ for (const e of fileEntries) {
182
+ if (!e.name.endsWith(".json")) continue;
183
+ const file = path.join(taskDir, e.name);
184
+ if (!isFileFollowingSymlink(file, e)) {
185
+ if (e.isSymbolicLink()) yield { file, broken: true };
186
+ continue;
187
+ }
188
+ let stat;
189
+ try { stat = fs.statSync(file); } catch { yield { file, broken: true }; continue; }
190
+ yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Read one JSON file as an array of raw text lines.
198
+ *
199
+ * Roo Code writes these via a JSON-with-indentation writer (safeWriteJson,
200
+ * confirmed used by TaskHistoryStore.ts) — real multi-line, indented text,
201
+ * not a single giant line — so the same streamed readline/promises approach
202
+ * claude-code.js uses for JSONL applies here essentially unchanged, and gets
203
+ * the same benefits: no whole-file-as-one-string V8 string-length ceiling,
204
+ * and a partial read (the file started streaming but the read failed
205
+ * partway) still returns whatever lines WERE read rather than discarding
206
+ * real content.
207
+ *
208
+ * Status vocabulary matches every other source in this project: "complete",
209
+ * "partial", "too-large", "failed".
210
+ */
211
+ async function readLines(file) {
212
+ let stat;
213
+ try { stat = fs.statSync(file); }
214
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
215
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
216
+
217
+ const lines = [];
218
+ let bytesRead = 0;
219
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
220
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
221
+
222
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
223
+
224
+ try {
225
+ for await (const line of rl) {
226
+ lines.push(line);
227
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1; // +1 for the stripped newline
228
+ }
229
+ return { lines, status: "complete", bytesRead };
230
+ } catch {
231
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
232
+ } finally {
233
+ clearTimeout(timer);
234
+ rl.close();
235
+ stream.destroy();
236
+ }
237
+ }
238
+
239
+ module.exports = { id, label, available, files, readLines };