residoo 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -46
- package/SECURITY.md +29 -22
- package/package.json +1 -1
- package/src/cli.js +82 -16
- package/src/integrity.js +669 -0
- package/src/patterns.js +78 -5
- package/src/report.js +74 -7
- package/src/sources/agent-configs.js +308 -0
- package/src/sources/aider.js +361 -0
- package/src/sources/amazon-q.js +199 -0
- package/src/sources/antigravity-cli.js +155 -0
- package/src/sources/cline.js +208 -0
- package/src/sources/codebuff.js +295 -0
- package/src/sources/codex-cli.js +258 -0
- package/src/sources/cody.js +325 -0
- package/src/sources/continue.js +408 -0
- package/src/sources/copilot-chat.js +272 -0
- package/src/sources/copilot-cli.js +300 -0
- package/src/sources/crush.js +364 -0
- package/src/sources/cursor.js +374 -0
- package/src/sources/devin-cli.js +241 -0
- package/src/sources/factory-droid.js +153 -0
- package/src/sources/fx.js +136 -0
- package/src/sources/gemini-cli.js +242 -0
- package/src/sources/goose.js +366 -0
- package/src/sources/grok-cli.js +267 -0
- package/src/sources/hermes.js +282 -0
- package/src/sources/index.js +172 -8
- package/src/sources/jetbrains-ai-assistant.js +343 -0
- package/src/sources/jetbrains-junie.js +292 -0
- package/src/sources/kilo-code.js +430 -0
- package/src/sources/kimi-code.js +147 -0
- package/src/sources/kiro-cli.js +393 -0
- package/src/sources/kiro-ide.js +230 -0
- package/src/sources/llm.js +328 -0
- package/src/sources/mentat.js +143 -0
- package/src/sources/open-interpreter.js +224 -0
- package/src/sources/openclaw.js +218 -0
- package/src/sources/opencode.js +379 -0
- package/src/sources/openhands.js +181 -0
- package/src/sources/pearai.js +151 -0
- package/src/sources/pi-agent.js +130 -0
- package/src/sources/qodo-gen.js +189 -0
- package/src/sources/qwen-code.js +244 -0
- package/src/sources/roo-code.js +239 -0
- package/src/sources/trae.js +294 -0
- package/src/sources/void.js +273 -0
- package/src/sources/warp.js +395 -0
- package/src/sources/windsurf.js +256 -0
- package/src/sources/zed.js +374 -0
|
@@ -0,0 +1,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 };
|