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.
- package/README.md +334 -46
- package/SECURITY.md +29 -22
- package/package.json +1 -1
- package/src/cli.js +249 -17
- package/src/integrity.js +689 -0
- package/src/patterns.js +78 -5
- package/src/report.js +188 -8
- package/src/rotation.js +834 -0
- 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/project-artifacts.js +355 -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,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 };
|