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,199 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const { createInterface } = require("readline/promises");
5
+ const path = require("path");
6
+ const os = require("os");
7
+
8
+ /**
9
+ * Amazon Q Developer — the AWS chat/coding-assistant IDE plugin (VS Code via
10
+ * the AWS Toolkit / "Amazon Q" extension, and the JetBrains "Amazon Q"
11
+ * plugin; formerly CodeWhisperer).
12
+ *
13
+ * VERIFICATION STATUS (read this before trusting anything below):
14
+ * multi-source-corroborated-but-UNVERIFIED against a real install. Neither VS
15
+ * Code, JetBrains, nor any Amazon Q plugin is installed on the machine this
16
+ * adapter was built on (checked: no /Applications/*Code*.app, no `code` on
17
+ * PATH, no `~/.aws/amazonq` directory — `~/.aws` itself exists here only from
18
+ * an old, unrelated AWS CLI credentials setup, `config`/`credentials` only,
19
+ * no `amazonq` subdirectory). What IS unusually strong here, short of a real
20
+ * install: the storage mechanism and exact filenames were read directly out
21
+ * of AWS's own current shipped source for BOTH clients, not inferred from a
22
+ * blog post:
23
+ *
24
+ * 1. `aws/aws-toolkit-vscode` (the VS Code client), `main` branch,
25
+ * `packages/core/src/shared/db/chatDb/chatDb.ts`, fetched verbatim via
26
+ * `gh api repos/aws/aws-toolkit-vscode/contents/...` on 2026-09-02 —
27
+ * its own docstring states plainly: "The database is stored in the
28
+ * user's home directory under .aws/amazonq/history with a unique
29
+ * filename based on the workspace identifier," and the constructor
30
+ * confirms it exactly:
31
+ * `this.dbDirectory = path.join(fs.getUserHomeDir(), '.aws/amazonq/history')`
32
+ * `const dbName = \`chat-history-${workspaceId}.json\``
33
+ * where `getWorkspaceIdentifier()` (same file) is an MD5 hex hash of
34
+ * the open `.code-workspace` path, or of the sorted+joined multi-root
35
+ * folder paths, or of the single open folder path, or the literal
36
+ * string `'no-workspace'` when nothing is open — i.e. filenames are
37
+ * exactly `chat-history-<32-hex-md5>.json` or
38
+ * `chat-history-no-workspace.json`. The "database" itself is LokiJS
39
+ * (`import Loki from 'lokijs'`, `persistenceMethod: 'fs'`) — an
40
+ * embedded JS document store that serializes its entire collection set
41
+ * as ONE JSON document per file, not a real SQL database despite the
42
+ * `.json`-suffixed "chat-history-" naming — so this is ordinary,
43
+ * scannable JSON text on disk, no special binary/SQLite handling
44
+ * needed (unlike cursor.js/cody.js).
45
+ *
46
+ * 2. `Amazon-Q-Developer/language-servers`,
47
+ * `server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts`
48
+ * — the SAME class, byte-for-byte the same docstring and
49
+ * `.aws/amazonq/history` path, living in the shared "Flare" language
50
+ * server package both IDE clients embed. Confirmed the JetBrains client
51
+ * actually embeds this same language server (not a separate,
52
+ * JetBrains-native storage layer) by reading
53
+ * `Amazon-Q-Developer/amazon-q-jetbrains`,
54
+ * `plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt`
55
+ * (same fetch method, same date) — the JetBrains plugin is an LSP
56
+ * *client* to the identical CodeWhisperer/Q language server, so its chat
57
+ * history lands in the exact same home-directory path as VS Code's,
58
+ * not a JetBrains-specific config/plugins directory. This is why this
59
+ * one adapter covers both IDEs with a single, IDE-agnostic path — no
60
+ * per-editor branching needed, unlike copilot-chat.js/cody.js.
61
+ *
62
+ * Independent, non-AWS corroboration of the same exact path and filename
63
+ * pattern (cross-checked, all agree with each other and with the source
64
+ * above): a real user's own write-up at
65
+ * dev.to/aws/finding-and-recovering-your-amazon-q-developer-prompt-history-28j1
66
+ * ("In the ~/.aws/amazonq/ directory there is a history directory... json
67
+ * files" — names `chat-history-no-workspace.json` and several
68
+ * `chat-history-<hash>.json` examples verbatim); a third-party forensics
69
+ * tool, `ACandeias/AI-Forensicator`, `collectors/amazon_q.py`, whose own
70
+ * comment reads "~/.aws/amazonq/history/ -- chat history JSON files"; and
71
+ * a third-party agent-session spec, `YawLabs/ctxlint`,
72
+ * `agent-session-lint-rules.json`, recording
73
+ * `"historyLocation": "~/.aws/amazonq/history/chat-history-*.json"`.
74
+ *
75
+ * No per-OS path branching in the AWS source read above (`fs.getUserHomeDir()`
76
+ * joined with the same relative `.aws/amazonq/history` on every platform) —
77
+ * matching the long-standing, cross-platform AWS CLI/SDK convention of a
78
+ * single `~/.aws` (`%USERPROFILE%\.aws` on Windows) regardless of OS, unlike
79
+ * VS Code's own per-OS `Application Support`/`AppData`/XDG split that most
80
+ * other sources in this project have to branch on.
81
+ *
82
+ * Deliberately out of scope: any Amazon Q Developer usage OUTSIDE the IDE
83
+ * plugins covered here — the separate `q chat` CLI, the GitHub-hosted
84
+ * "Amazon Q Developer for GitHub" integration, and Kiro (a distinct AWS
85
+ * product that a third-party source above notes also happens to write into
86
+ * this same directory) are different products with their own storage
87
+ * questions, not verified here and not claimed by this adapter.
88
+ */
89
+ function historyDir() {
90
+ return path.join(os.homedir(), ".aws", "amazonq", "history");
91
+ }
92
+
93
+ const HISTORY_DIR = historyDir();
94
+
95
+ // Bounds for readLines() — same rationale and values as claude-code.js.
96
+ // Not backed by a real chat-history-*.json file this tool was tested
97
+ // against (no install to test with) — see the verification-status note
98
+ // above. A LokiJS-serialized history file is a single JSON document, so in
99
+ // practice this caps one very long "line," the same shape copilot-chat.js
100
+ // already handles for a flat (non-JSONL) chat session snapshot.
101
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
102
+ const READ_TIMEOUT_MS = 60_000;
103
+
104
+ function id() { return "amazon-q"; }
105
+ function label() { return "Amazon Q Developer"; }
106
+
107
+ function available() {
108
+ try { return fs.statSync(HISTORY_DIR).isDirectory(); } catch { return false; }
109
+ }
110
+
111
+ /**
112
+ * Same defensive symlink-following pattern as claude-code.js's
113
+ * isKindFollowingSymlink — see that file's docstring for the full reasoning.
114
+ * Duplicated rather than imported, matching this project's "small,
115
+ * self-contained file" convention.
116
+ */
117
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
118
+ if (checkFn(dirent)) return true;
119
+ if (!dirent.isSymbolicLink()) return false;
120
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
121
+ }
122
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
123
+
124
+ /**
125
+ * Yield { file, mtimeMs, sizeBytes, broken } for every `chat-history-*.json`
126
+ * file directly inside `~/.aws/amazonq/history/` (flat, not recursive —
127
+ * `chatDb.ts`'s `dbDirectory` is the immediate parent of every db file, no
128
+ * further nesting per the source read above).
129
+ *
130
+ * Not filtered to the exact `chat-history-` prefix: `.aws/amazonq/history`
131
+ * is a directory this adapter treats as fully Amazon-Q-owned (per the
132
+ * source above, nothing else writes there), so any `*.json` found there is
133
+ * scanned — the same "don't hard-code a filename pattern likely to drift"
134
+ * caution cursor.js documents for its own key-name filtering, applied here
135
+ * to filenames instead of SQLite keys.
136
+ */
137
+ function* files() {
138
+ let entries;
139
+ try { entries = fs.readdirSync(HISTORY_DIR, { withFileTypes: true }); }
140
+ catch { return; } // no history directory at all — Amazon Q never ran, or never opened chat
141
+
142
+ for (const e of entries) {
143
+ if (!e.name.endsWith(".json")) continue;
144
+ const file = path.join(HISTORY_DIR, e.name);
145
+ if (!isFileFollowingSymlink(file, e)) {
146
+ if (e.isSymbolicLink()) yield { file, broken: true };
147
+ continue;
148
+ }
149
+ let stat;
150
+ try { stat = fs.statSync(file); } catch { yield { file, broken: true }; continue; }
151
+ yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Read one chat-history-*.json file as an array of raw text lines.
157
+ *
158
+ * Streamed line-by-line via readline/promises, same as claude-code.js and
159
+ * copilot-chat.js — LokiJS's `fs` persistence adapter writes its entire
160
+ * collection set as one JSON document (typically not pretty-printed), so in
161
+ * the common case this yields exactly one long "line," bounded by
162
+ * MAX_BYTES; if a given LokiJS version ever pretty-prints or the file
163
+ * otherwise contains embedded newlines, per-line scanning still works
164
+ * unchanged. Status vocabulary matches every other source in this project.
165
+ */
166
+ async function readLines(file) {
167
+ let stat;
168
+ try { stat = fs.statSync(file); }
169
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
170
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
171
+
172
+ const lines = [];
173
+ let bytesRead = 0;
174
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
175
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
176
+
177
+ // Same rationale as claude-code.js: no natural timeout exists anywhere in
178
+ // Node's stream/readline stack, and a retargeted symlink can make the
179
+ // underlying open() block forever with no event ever firing.
180
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
181
+
182
+ try {
183
+ for await (const line of rl) {
184
+ lines.push(line);
185
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1; // +1 for the stripped newline
186
+ }
187
+ return { lines, status: "complete", bytesRead };
188
+ } catch {
189
+ // Whatever WAS read before the failure is real content and may contain
190
+ // a real secret — kept, not discarded, same as every other source here.
191
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
192
+ } finally {
193
+ clearTimeout(timer);
194
+ rl.close();
195
+ stream.destroy();
196
+ }
197
+ }
198
+
199
+ module.exports = { id, label, available, files, readLines };
@@ -0,0 +1,155 @@
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
+ * Google Antigravity (antigravity.google) — the agentic development platform
10
+ * Google launched at I/O 2026, whose CLI (built in Go, replacing Gemini CLI
11
+ * for agentic use) and desktop editor both write local session state under
12
+ * `~/.gemini/antigravity*`.
13
+ *
14
+ * VERIFICATION STATUS: corroborated by one detailed, credible source — NOT
15
+ * by two independent sources of the exact schema, and NOT checked against a
16
+ * real install on the machine this source was built on (no `~/.gemini`
17
+ * directory exists there; see CONTRIBUTING.md). Ship this with that
18
+ * explicitly weaker standing in mind relative to this project's other new
19
+ * sources.
20
+ *
21
+ * The source: jazzyalex/agent-sessions (github.com/jazzyalex/agent-sessions,
22
+ * 800+ stars) — a real, actively maintained macOS app built specifically to
23
+ * parse local AI-coding-agent session history for browsing/search — ships a
24
+ * dedicated guide page, "Antigravity CLI local history: transcripts and
25
+ * brain artifacts under `~/.gemini`," documenting (as this tool's own
26
+ * behavior, i.e. code the maintainer wrote and presumably ran against a real
27
+ * Antigravity install, not a secondhand description):
28
+ * - CLI transcripts: `~/.gemini/antigravity-cli/brain/<conversation-id>/
29
+ * .system_generated/logs/transcript.jsonl` (one step per line: fields
30
+ * including step_index, source, type, status, created_at, content,
31
+ * tool_calls, thinking, truncated_fields) plus a sibling
32
+ * `transcript_full.jsonl` restoring content the primary file truncates.
33
+ * - Editor artifacts: `~/.gemini/antigravity/brain/<conversation-id>/`
34
+ * holding per-artifact Markdown (`task.md`, `implementation_plan.md`,
35
+ * `walkthrough.md`, `proposal.md`) each paired with a `.metadata.json`.
36
+ * - The same guide notes upgraded installs may also carry
37
+ * `~/.gemini/antigravity-ide/brain` and `~/.gemini/antigravity-backup/
38
+ * brain`, but says its own tool only scans the first two — this source
39
+ * follows that same, narrower choice rather than guessing at the other
40
+ * two directory names' internal shape.
41
+ *
42
+ * Real-world significance: Antigravity is a first-party Google product
43
+ * (Google I/O 2026 launch, "the only tool in this group not built on VS
44
+ * Code," free public preview with Gemini 3 Pro access as of that launch),
45
+ * so — schema-verification caveat above notwithstanding — this is exactly
46
+ * the kind of tool this project's coverage would be conspicuously incomplete
47
+ * without.
48
+ *
49
+ * This source walks both brain roots recursively for `.jsonl`, `.json`, and
50
+ * `.md` files (skipping the `.system_generated/logs` vs top-level distinction
51
+ * rather than hard-coding it) — the same "match by extension, don't pin the
52
+ * exact depth" tolerance claude-code.js applies to its own project-slug
53
+ * directories, since a screenshot or other binary asset the CLI writes
54
+ * alongside these would not match any of those three extensions anyway.
55
+ */
56
+ const HOME = os.homedir();
57
+ const GEMINI_DIR = path.join(HOME, ".gemini");
58
+ const CLI_BRAIN_ROOT = path.join(GEMINI_DIR, "antigravity-cli", "brain");
59
+ const EDITOR_BRAIN_ROOT = path.join(GEMINI_DIR, "antigravity", "brain");
60
+
61
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB — same backstop as claude-code.js.
62
+ const READ_TIMEOUT_MS = 60_000;
63
+ const MAX_WALK_DEPTH = 8;
64
+
65
+ function id() { return "antigravity"; }
66
+ function label() { return "Google Antigravity"; }
67
+
68
+ function available() {
69
+ for (const root of [CLI_BRAIN_ROOT, EDITOR_BRAIN_ROOT]) {
70
+ try { if (fs.statSync(root).isDirectory()) return true; } catch { /* try the next root */ }
71
+ }
72
+ return false;
73
+ }
74
+
75
+ /**
76
+ * Same defensive symlink-following helpers as claude-code.js — see that
77
+ * file's docstring. Duplicated rather than imported, per this project's
78
+ * self-contained-source-file convention (see cursor.js's docstring).
79
+ */
80
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
81
+ if (checkFn(dirent)) return true;
82
+ if (!dirent.isSymbolicLink()) return false;
83
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
84
+ }
85
+ const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
86
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
87
+
88
+ const TEXT_EXTENSIONS = new Set([".jsonl", ".json", ".md"]);
89
+
90
+ /**
91
+ * Recursively yield { file, mtimeMs, sizeBytes, broken } for every plain
92
+ * text-like (see TEXT_EXTENSIONS) file under `dir` — see factory-droid.js's
93
+ * walk() for the identical symlink-handling reasoning.
94
+ */
95
+ function* walk(dir, depth) {
96
+ if (depth > MAX_WALK_DEPTH) return;
97
+ let entries;
98
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
99
+ catch { return; }
100
+
101
+ for (const e of entries) {
102
+ const full = path.join(dir, e.name);
103
+ if (isDirFollowingSymlink(full, e)) {
104
+ yield* walk(full, depth + 1);
105
+ continue;
106
+ }
107
+ const isFile = isFileFollowingSymlink(full, e);
108
+ if (!isFile) {
109
+ if (e.isSymbolicLink()) yield { file: full, broken: true };
110
+ continue;
111
+ }
112
+ if (!TEXT_EXTENSIONS.has(path.extname(e.name))) continue; // e.g. a captured screenshot — out of scope
113
+ let stat;
114
+ try { stat = fs.statSync(full); } catch { yield { file: full, broken: true }; continue; }
115
+ yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
116
+ }
117
+ }
118
+
119
+ function* files() {
120
+ yield* walk(CLI_BRAIN_ROOT, 0);
121
+ yield* walk(EDITOR_BRAIN_ROOT, 0);
122
+ }
123
+
124
+ /**
125
+ * Read one transcript/artifact file as raw text lines. Identical streaming/
126
+ * timeout/partial-read discipline to claude-code.js's readLines().
127
+ */
128
+ async function readLines(file) {
129
+ let stat;
130
+ try { stat = fs.statSync(file); }
131
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
132
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
133
+
134
+ const lines = [];
135
+ let bytesRead = 0;
136
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
137
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
138
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
139
+
140
+ try {
141
+ for await (const line of rl) {
142
+ lines.push(line);
143
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1;
144
+ }
145
+ return { lines, status: "complete", bytesRead };
146
+ } catch {
147
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
148
+ } finally {
149
+ clearTimeout(timer);
150
+ rl.close();
151
+ stream.destroy();
152
+ }
153
+ }
154
+
155
+ module.exports = { id, label, available, files, readLines };
@@ -0,0 +1,208 @@
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
+ * Cline (VS Code extension, historically shipped as "Claude Dev") session
10
+ * history.
11
+ *
12
+ * VERIFICATION STATUS: the path and file layout below were read directly out
13
+ * of Cline's own current source on GitHub (cline/cline) during this source's
14
+ * research — not inferred from a blog post and not guessed from Roo Code's
15
+ * near-identical fork, even though the two are in fact near-identical.
16
+ * Specifically:
17
+ * - apps/vscode/src/core/storage/disk.ts — GlobalFileNames constants and
18
+ * ensureTaskDirectoryExists()/getGlobalStorageDir(), which resolve
19
+ * through the VS Code extension's own `globalStorageUri` (NOT
20
+ * workspaceStorage) into `tasks/<taskId>/`.
21
+ * - apps/vscode/package.json — `"name": "claude-dev"`, `"publisher":
22
+ * "saoudrizwan"`, confirming the on-disk globalStorage folder is really
23
+ * still `saoudrizwan.claude-dev` (Cline shipped as "Claude Dev" and kept
24
+ * its original package.json identity across the product's later rename
25
+ * to Cline — this is NOT the same as the current "Cline" display name,
26
+ * and guessing `cline.cline` or similar would have been wrong).
27
+ * What this could NOT be checked against: a real Cline install on the
28
+ * machine this source was built on — VS Code itself isn't installed there.
29
+ * See CONTRIBUTING.md for what "verified" is supposed to mean and treat
30
+ * findings from this source accordingly until someone with Cline actually
31
+ * installed confirms it against real data.
32
+ *
33
+ * Cline writes one JSON file per concern into a per-task directory under its
34
+ * extension's VS Code globalStorage folder:
35
+ *
36
+ * <VS Code User dir>/globalStorage/saoudrizwan.claude-dev/tasks/<taskId>/
37
+ * api_conversation_history.json - full message history sent to the model
38
+ * ui_messages.json - the rendered chat transcript
39
+ * context_history.json - context-window bookkeeping
40
+ * task_metadata.json - files touched, model/token usage
41
+ * settings.json - a per-task settings snapshot
42
+ *
43
+ * Filenames are deliberately NOT allow-listed beyond "every *.json file
44
+ * directly inside tasks/<taskId>/": GlobalFileNames in Cline's own source has
45
+ * gained entries over time (context_history.json is a relatively recent
46
+ * addition) and hard-coding today's list is exactly the kind of thing likely
47
+ * to go stale the same way cursor.js's docstring describes for Cursor's own
48
+ * key names. A task's `checkpoints/` subdirectory (shadow-git snapshots used
49
+ * for file revert) is deliberately NOT walked — those are git object stores,
50
+ * not text transcripts.
51
+ *
52
+ * Base directory: VS Code has a portable/remote/Insiders/fork multiverse of
53
+ * possible per-profile "User" directories. This source checks the two by far
54
+ * most common ones on each OS — standard VS Code ("Code") and VS Code
55
+ * Insiders ("Code - Insiders") — and deliberately does NOT attempt every
56
+ * fork (VSCodium, etc.) or the separate ~/.vscode-server tree used by
57
+ * remote-SSH sessions: a named, narrower scope rather than a guess at an
58
+ * exhaustive list.
59
+ *
60
+ * Also out of scope, named rather than silently skipped: Cline's task-title
61
+ * history INDEX (the array a "History" panel is built from) is not a plain
62
+ * file — it is read via VS Code's own `context.globalState` API, which
63
+ * persists into a *different*, central per-profile database
64
+ * (globalStorage/state.vscdb, shared by every installed extension, keyed by
65
+ * extension id) rather than anywhere under this extension's own
66
+ * globalStorage folder. Parsing that shared, VS-Code-owned database is out of
67
+ * scope for this source; the per-task JSON files above are where actual
68
+ * conversation content — and anything pasted into it — lives.
69
+ */
70
+ const EXT_ID = "saoudrizwan.claude-dev";
71
+
72
+ function vscodeUserDirs() {
73
+ const home = os.homedir();
74
+ const variants = ["Code", "Code - Insiders"];
75
+ if (process.platform === "darwin") {
76
+ return variants.map((v) => path.join(home, "Library", "Application Support", v, "User"));
77
+ }
78
+ if (process.platform === "win32") {
79
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
80
+ return variants.map((v) => path.join(appData, v, "User"));
81
+ }
82
+ // Linux and other XDG-following unix platforms.
83
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
84
+ return variants.map((v) => path.join(configHome, v, "User"));
85
+ }
86
+
87
+ function tasksDirs() {
88
+ return vscodeUserDirs().map((userDir) => path.join(userDir, "globalStorage", EXT_ID, "tasks"));
89
+ }
90
+
91
+ // Bounds for readLines() — same rationale and same values as claude-code.js:
92
+ // generous headroom over any real transcript, plus a hard stop against a
93
+ // hung read. Not backed by a real Cline transcript this tool was tested
94
+ // against (no install to test with) — see the verification-status note above.
95
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
96
+ const READ_TIMEOUT_MS = 60_000;
97
+
98
+ function id() { return "cline"; }
99
+ function label() { return "Cline"; }
100
+
101
+ function available() {
102
+ return tasksDirs().some((dir) => {
103
+ try { return fs.statSync(dir).isDirectory(); } catch { return false; }
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Same defensive symlink-following pattern as claude-code.js's
109
+ * isDirFollowingSymlink/isFileFollowingSymlink — see that file's docstring
110
+ * for the full reasoning. Duplicated rather than imported: each source in
111
+ * this project is meant to be a small, self-contained file a reviewer can
112
+ * audit on its own (see CONTRIBUTING.md and cursor.js's own note on this).
113
+ */
114
+ function isKindFollowingSymlink(fullPath, dirent, checkFn) {
115
+ if (checkFn(dirent)) return true;
116
+ if (!dirent.isSymbolicLink()) return false;
117
+ try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
118
+ }
119
+ const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
120
+ const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
121
+
122
+ /**
123
+ * Yield { file, mtimeMs, sizeBytes, broken } for every *.json file directly
124
+ * inside every task directory, across every candidate VS Code User dir.
125
+ *
126
+ * broken:true marks a tasks/ entry or a *.json entry that looked like it
127
+ * should resolve (chiefly a dangling symlink) but didn't — never silently
128
+ * skipped, same convention as claude-code.js and cursor.js.
129
+ */
130
+ function* files() {
131
+ for (const tasksDir of tasksDirs()) {
132
+ let taskEntries;
133
+ try { taskEntries = fs.readdirSync(tasksDir, { withFileTypes: true }); }
134
+ catch { continue; } // this VS Code variant/profile simply has no Cline tasks dir — normal, not broken
135
+
136
+ for (const taskEntry of taskEntries) {
137
+ const taskDir = path.join(tasksDir, taskEntry.name);
138
+ if (!isDirFollowingSymlink(taskDir, taskEntry)) {
139
+ if (taskEntry.isSymbolicLink()) yield { file: taskDir, broken: true };
140
+ continue; // a stray non-directory entry under tasks/ is out of scope, not broken
141
+ }
142
+
143
+ let fileEntries;
144
+ try { fileEntries = fs.readdirSync(taskDir, { withFileTypes: true }); }
145
+ catch { yield { file: taskDir, broken: true }; continue; }
146
+
147
+ for (const e of fileEntries) {
148
+ if (!e.name.endsWith(".json")) continue;
149
+ const file = path.join(taskDir, e.name);
150
+ if (!isFileFollowingSymlink(file, e)) {
151
+ if (e.isSymbolicLink()) yield { file, broken: true };
152
+ continue;
153
+ }
154
+ let stat;
155
+ try { stat = fs.statSync(file); } catch { yield { file, broken: true }; continue; }
156
+ yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Read one JSON file as an array of raw text lines.
164
+ *
165
+ * Cline writes these via `JSON.stringify(value, null, 2)` (confirmed from
166
+ * disk.ts) — real multi-line, indented text, not a single giant line — so
167
+ * the same streamed readline/promises approach claude-code.js uses for JSONL
168
+ * applies here essentially unchanged, and gets the same benefits: no
169
+ * whole-file-as-one-string V8 string-length ceiling, and a partial read (the
170
+ * file started streaming but the read failed partway) still returns
171
+ * whatever lines WERE read rather than discarding real content.
172
+ *
173
+ * Status vocabulary matches every other source in this project: "complete",
174
+ * "partial", "too-large", "failed".
175
+ */
176
+ async function readLines(file) {
177
+ let stat;
178
+ try { stat = fs.statSync(file); }
179
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
180
+ if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
181
+
182
+ const lines = [];
183
+ let bytesRead = 0;
184
+ const stream = fs.createReadStream(file, { encoding: "utf-8" });
185
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
186
+
187
+ // Same rationale as claude-code.js: no natural timeout exists anywhere in
188
+ // Node's stream/readline stack, and a retargeted symlink can make the
189
+ // underlying open() block forever with no event ever firing. Destroying
190
+ // the stream is what actually unblocks that.
191
+ const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
192
+
193
+ try {
194
+ for await (const line of rl) {
195
+ lines.push(line);
196
+ bytesRead += Buffer.byteLength(line, "utf-8") + 1; // +1 for the stripped newline
197
+ }
198
+ return { lines, status: "complete", bytesRead };
199
+ } catch {
200
+ return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
201
+ } finally {
202
+ clearTimeout(timer);
203
+ rl.close();
204
+ stream.destroy();
205
+ }
206
+ }
207
+
208
+ module.exports = { id, label, available, files, readLines };