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,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 };