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,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const { createInterface } = require("readline/promises");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OpenAI's Codex CLI (the `codex` coding-agent binary — see
|
|
10
|
+
* https://github.com/openai/codex — NOT the retired "Codex" language model
|
|
11
|
+
* from 2021). Also referred to below as "Codex CLI" throughout to keep that
|
|
12
|
+
* distinction unambiguous.
|
|
13
|
+
*
|
|
14
|
+
* VERIFICATION STATUS: this source was NOT checked against a real Codex CLI
|
|
15
|
+
* install — `codex` is not installed on the machine this adapter was built
|
|
16
|
+
* on (checked: no `codex` on PATH, no `~/.codex`, no Homebrew/npm-global
|
|
17
|
+
* install, mdfind turned up nothing for a local CLI install — the only
|
|
18
|
+
* "codex" hits on this machine were the ChatGPT desktop app's own unrelated
|
|
19
|
+
* `com.openai.chat` local cache for its cloud-hosted "Codex" task feature,
|
|
20
|
+
* which is a different product with no local session transcripts of its
|
|
21
|
+
* own to scan; it runs in a remote container, not on this machine). Per
|
|
22
|
+
* CONTRIBUTING.md this ships anyway because it clears that bar a different
|
|
23
|
+
* way: multiple independent, credible, and largely recent sources agree
|
|
24
|
+
* with each other on the exact path and schema below, including official
|
|
25
|
+
* OpenAI documentation, the tool's own GitHub issue tracker describing this
|
|
26
|
+
* exact file layout as a live bug surface, and more than one third-party
|
|
27
|
+
* tool that reads these same files for a living. Specifically:
|
|
28
|
+
*
|
|
29
|
+
* - Official docs (developers.openai.com/codex/environment-variables,
|
|
30
|
+
* redirects to learn.chatgpt.com/docs/config-file/environment-variables):
|
|
31
|
+
* CODEX_HOME "sets the root directory for Codex state, including config,
|
|
32
|
+
* auth, logs, sessions, skills, and standalone package metadata,"
|
|
33
|
+
* defaulting to `~/.codex`.
|
|
34
|
+
* - openai/codex GitHub issue #21660 ("rollout: session JSONL files are
|
|
35
|
+
* created world-readable (mode 0644) on Unix") and issue #20864
|
|
36
|
+
* ("Codex Desktop App becomes laggy because it scans all
|
|
37
|
+
* `~/.codex/sessions` rollout files...") — both filed against the real
|
|
38
|
+
* tool, both independently naming this exact directory.
|
|
39
|
+
* - openai/codex GitHub Discussion #24042, a real, working, open-source
|
|
40
|
+
* native macOS viewer built specifically to read `~/.codex/sessions/
|
|
41
|
+
* *.jsonl`, and community tools codex-trace (PixelPaw-Labs) and
|
|
42
|
+
* codex-history-list (shinshin86) doing the same — the kind of "a
|
|
43
|
+
* maintained tool reads the same files" corroboration CONTRIBUTING.md
|
|
44
|
+
* calls out explicitly.
|
|
45
|
+
* - openai/codex GitHub issue #17000 ("Auto-archive and zstd-compress
|
|
46
|
+
* inactive local rollout files...") independently confirms the
|
|
47
|
+
* `rollout-*.jsonl` naming and the date-partitioned directory shape by
|
|
48
|
+
* proposing changes to it. NOTE: this issue also shows the zstd
|
|
49
|
+
* compression feature it proposes is NOT yet shipped as of this
|
|
50
|
+
* research — a compression detail that appeared in one AI-generated
|
|
51
|
+
* summary was traced back to this still-open proposal, not a shipped
|
|
52
|
+
* behavior, so no zstd decompression is assumed live here. It's still
|
|
53
|
+
* handled defensively below (see ZSTD_RE) in case that changes.
|
|
54
|
+
* - Independent write-ups (a dev.to reverse-engineering post showing real
|
|
55
|
+
* rollout JSONL line shapes, prismmd.app and betelgeuse.work blog posts,
|
|
56
|
+
* and a Codex-Knowledge-Base article on session archiving) all agree on
|
|
57
|
+
* the same `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl`
|
|
58
|
+
* shape and on a parallel `~/.codex/archived_sessions/` tree used when a
|
|
59
|
+
* session is archived (the rollout JSONL is moved, not transformed).
|
|
60
|
+
* - Multiple of the above also describe a flat `~/.codex/history.jsonl` —
|
|
61
|
+
* one line per user turn, holding just the raw text the user typed for
|
|
62
|
+
* that turn (not the full conversation) — which is real, secret-scanning
|
|
63
|
+
* -relevant content (a pasted key or token lands here) distinct from the
|
|
64
|
+
* per-session rollout files, so it's read too.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately NOT read: `~/.codex/session_index.jsonl`. Independent
|
|
67
|
+
* sources agree it is a lightweight metadata cache only (id, timestamp,
|
|
68
|
+
* cwd, model, status) that explicitly does NOT duplicate rollout content —
|
|
69
|
+
* scanning it would add file-walk cost with no realistic chance of a
|
|
70
|
+
* secret-bearing line. Also not read: anything under `~/.codex` that isn't
|
|
71
|
+
* one of the three content locations above — chiefly `config.toml`,
|
|
72
|
+
* `auth.json`, and `log/`, which are Codex's own config/credential/log
|
|
73
|
+
* files, not session transcript content, mirroring how claude-code.js and
|
|
74
|
+
* cursor.js each stay scoped to actual conversation data rather than a
|
|
75
|
+
* tool's entire state directory.
|
|
76
|
+
*
|
|
77
|
+
* If you have Codex CLI installed, the most useful thing you can do is run
|
|
78
|
+
* `residoo scan` and confirm `sourcesScanned`/`filesScanned` look right for
|
|
79
|
+
* what you know is actually on disk under `~/.codex`, then report back
|
|
80
|
+
* either way — see CONTRIBUTING.md.
|
|
81
|
+
*/
|
|
82
|
+
function codexHome() {
|
|
83
|
+
// Honoring CODEX_HOME (rather than hardcoding ~/.codex) mirrors how
|
|
84
|
+
// cursor.js honors XDG_CONFIG_HOME — the tool's own documented override,
|
|
85
|
+
// not a guess, and the official docs above are explicit that sessions,
|
|
86
|
+
// not just config, live under this root.
|
|
87
|
+
if (process.env.CODEX_HOME) return process.env.CODEX_HOME;
|
|
88
|
+
return path.join(os.homedir(), ".codex");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ROOT = codexHome();
|
|
92
|
+
const SESSIONS_DIR = path.join(ROOT, "sessions");
|
|
93
|
+
const ARCHIVED_SESSIONS_DIR = path.join(ROOT, "archived_sessions");
|
|
94
|
+
const HISTORY_FILE = path.join(ROOT, "history.jsonl");
|
|
95
|
+
|
|
96
|
+
// A rollout file this large has not been reported anywhere in this source's
|
|
97
|
+
// research; kept identical to claude-code.js's bound (same underlying
|
|
98
|
+
// concern — V8's whole-string ceiling doesn't apply here since this source
|
|
99
|
+
// also streams line-by-line, but a shared, already-reasoned-about number
|
|
100
|
+
// beats inventing a new one with no evidence behind it).
|
|
101
|
+
const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
|
|
102
|
+
const READ_TIMEOUT_MS = 60_000;
|
|
103
|
+
|
|
104
|
+
// Symlink-loop safety for the recursive sessions/archived_sessions walk.
|
|
105
|
+
// claude-code.js never needed a depth bound — its walk is exactly two
|
|
106
|
+
// levels (project dir, then files in it). Walking an arbitrary
|
|
107
|
+
// YYYY/MM/DD(/...)? tree that may itself contain a symlink is a genuinely
|
|
108
|
+
// new risk this source introduces, so it gets a guard claude-code.js didn't
|
|
109
|
+
// need. 12 gives generous headroom over the documented 3-level date
|
|
110
|
+
// partitioning while still bounding a pathological symlink cycle.
|
|
111
|
+
const MAX_WALK_DEPTH = 12;
|
|
112
|
+
|
|
113
|
+
// See the ZSTD note in the module docstring: not confirmed shipped, but
|
|
114
|
+
// handled honestly rather than assumed absent forever. A zero-dependency
|
|
115
|
+
// project has no built-in Zstandard decoder available across the supported
|
|
116
|
+
// Node range (>=18), so a matching file is surfaced as a normal, resolvable
|
|
117
|
+
// file entry (not "broken" — it's not unresolvable, just undecodable by
|
|
118
|
+
// this tool) and readLines() reports it "failed" rather than silently
|
|
119
|
+
// omitting it from the walk.
|
|
120
|
+
const ZSTD_RE = /\.zst$/i;
|
|
121
|
+
const JSONL_RE = /\.jsonl$/i;
|
|
122
|
+
|
|
123
|
+
function id() { return "codex-cli"; }
|
|
124
|
+
function label() { return "Codex CLI"; }
|
|
125
|
+
|
|
126
|
+
function available() {
|
|
127
|
+
try { return fs.statSync(ROOT).isDirectory(); } catch { return false; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Same lstat-vs-stat symlink-following pattern as claude-code.js's
|
|
132
|
+
* isKindFollowingSymlink — duplicated rather than imported, matching this
|
|
133
|
+
* project's convention (cursor.js's docstring on the same duplication:
|
|
134
|
+
* "each source in this project is meant to be a small, self-contained file
|
|
135
|
+
* a reviewer can audit on its own").
|
|
136
|
+
*/
|
|
137
|
+
function isKindFollowingSymlink(fullPath, dirent, checkFn) {
|
|
138
|
+
if (checkFn(dirent)) return true;
|
|
139
|
+
if (!dirent.isSymbolicLink()) return false;
|
|
140
|
+
try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
|
|
141
|
+
}
|
|
142
|
+
const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
|
|
143
|
+
const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Recursively walk one directory (sessions/ or archived_sessions/) yielding
|
|
147
|
+
* { file, mtimeMs, sizeBytes, broken } for every `*.jsonl` (scanned) and
|
|
148
|
+
* `*.zst` (surfaced, see ZSTD_RE above) file found at any depth, following
|
|
149
|
+
* symlinks the same way claude-code.js's files() does for project dirs and
|
|
150
|
+
* jsonl files, and reporting a dangling symlink as broken rather than
|
|
151
|
+
* skipping it silently. Any other file extension under this tree is out of
|
|
152
|
+
* scope, same as claude-code.js ignoring non-`.jsonl` entries.
|
|
153
|
+
*/
|
|
154
|
+
function* walkSessionDir(dir, depth) {
|
|
155
|
+
if (depth > MAX_WALK_DEPTH) return;
|
|
156
|
+
let entries;
|
|
157
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
158
|
+
catch { return; } // dir vanished or unreadable mid-walk — not reportable, nothing was ever yielded for it
|
|
159
|
+
|
|
160
|
+
for (const e of entries) {
|
|
161
|
+
const full = path.join(dir, e.name);
|
|
162
|
+
|
|
163
|
+
if (!e.isFile() && !e.isDirectory()) {
|
|
164
|
+
// Symlink (or other special entry) — resolve to find out which kind.
|
|
165
|
+
if (isDirFollowingSymlink(full, e)) { yield* walkSessionDir(full, depth + 1); continue; }
|
|
166
|
+
if (isFileFollowingSymlink(full, e)) {
|
|
167
|
+
if (!JSONL_RE.test(e.name) && !ZSTD_RE.test(e.name)) continue;
|
|
168
|
+
let stat;
|
|
169
|
+
try { stat = fs.statSync(full); } catch { yield { file: full, broken: true }; continue; }
|
|
170
|
+
yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Didn't resolve to either — a dangling symlink is the plausible
|
|
174
|
+
// real-world case (see claude-code.js's identical reasoning).
|
|
175
|
+
if (e.isSymbolicLink()) yield { file: full, broken: true };
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (e.isDirectory()) { yield* walkSessionDir(full, depth + 1); continue; }
|
|
180
|
+
|
|
181
|
+
if (!JSONL_RE.test(e.name) && !ZSTD_RE.test(e.name)) continue;
|
|
182
|
+
let stat;
|
|
183
|
+
try { stat = fs.statSync(full); } catch { yield { file: full, broken: true }; continue; }
|
|
184
|
+
yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Yield { file, mtimeMs, sizeBytes, broken } for every rollout file under
|
|
190
|
+
* sessions/ and archived_sessions/, plus the single flat history.jsonl —
|
|
191
|
+
* see the module docstring for why each of these three (and only these
|
|
192
|
+
* three) locations is read.
|
|
193
|
+
*/
|
|
194
|
+
function* files() {
|
|
195
|
+
yield* walkSessionDir(SESSIONS_DIR, 0);
|
|
196
|
+
yield* walkSessionDir(ARCHIVED_SESSIONS_DIR, 0);
|
|
197
|
+
|
|
198
|
+
// history.jsonl is a single fixed-name file, not a directory to walk —
|
|
199
|
+
// same lstat-first, follow-if-symlink handling as cursor.js's
|
|
200
|
+
// statIfPresent, and a path that simply doesn't exist (most installs,
|
|
201
|
+
// depending on version/config) is normal, not broken.
|
|
202
|
+
let lst;
|
|
203
|
+
try { lst = fs.lstatSync(HISTORY_FILE); }
|
|
204
|
+
catch { return; }
|
|
205
|
+
|
|
206
|
+
if (lst.isSymbolicLink()) {
|
|
207
|
+
try {
|
|
208
|
+
const st = fs.statSync(HISTORY_FILE);
|
|
209
|
+
if (!st.isFile()) { yield { file: HISTORY_FILE, broken: true }; return; }
|
|
210
|
+
yield { file: HISTORY_FILE, mtimeMs: st.mtimeMs, sizeBytes: st.size, broken: false };
|
|
211
|
+
} catch {
|
|
212
|
+
yield { file: HISTORY_FILE, broken: true };
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!lst.isFile()) return;
|
|
217
|
+
yield { file: HISTORY_FILE, mtimeMs: lst.mtimeMs, sizeBytes: lst.size, broken: false };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Read one file as an array of raw text lines. Identical streaming strategy
|
|
222
|
+
* to claude-code.js's readLines (see that file's docstring for the full
|
|
223
|
+
* reasoning on why streaming + a read timeout + honest partial-read
|
|
224
|
+
* handling all matter) — duplicated rather than shared, per this project's
|
|
225
|
+
* one-file-per-source convention. The one addition is the zstd short-circuit
|
|
226
|
+
* at the top; see ZSTD_RE above.
|
|
227
|
+
*/
|
|
228
|
+
async function readLines(file) {
|
|
229
|
+
if (ZSTD_RE.test(file)) return { lines: [], status: "failed", bytesRead: 0 };
|
|
230
|
+
|
|
231
|
+
let stat;
|
|
232
|
+
try { stat = fs.statSync(file); }
|
|
233
|
+
catch { return { lines: [], status: "failed", bytesRead: 0 }; }
|
|
234
|
+
if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
|
|
235
|
+
|
|
236
|
+
const lines = [];
|
|
237
|
+
let bytesRead = 0;
|
|
238
|
+
const stream = fs.createReadStream(file, { encoding: "utf-8" });
|
|
239
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
240
|
+
|
|
241
|
+
const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
for await (const line of rl) {
|
|
245
|
+
lines.push(line);
|
|
246
|
+
bytesRead += Buffer.byteLength(line, "utf-8") + 1;
|
|
247
|
+
}
|
|
248
|
+
return { lines, status: "complete", bytesRead };
|
|
249
|
+
} catch {
|
|
250
|
+
return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
|
|
251
|
+
} finally {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
rl.close();
|
|
254
|
+
stream.destroy();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = { id, label, available, files, readLines };
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sourcegraph Cody — the VS Code extension (publisher.name `sourcegraph.cody-ai`).
|
|
9
|
+
*
|
|
10
|
+
* VERIFICATION STATUS (read this before trusting anything below):
|
|
11
|
+
* multi-source-corroborated-but-UNVERIFIED against a real install. Neither VS
|
|
12
|
+
* Code nor the Cody extension is installed on the machine this adapter was
|
|
13
|
+
* built on (checked: no /Applications/*Code*.app, no `code`/`code-insiders`
|
|
14
|
+
* on PATH, no ~/Library/Application Support/Code, no
|
|
15
|
+
* ~/.vscode/extensions/sourcegraph.cody-ai-*, no
|
|
16
|
+
* ~/Library/Application Support/JetBrains/<product>/options/cody_history.xml).
|
|
17
|
+
* What
|
|
18
|
+
* IS unusually strong here, short of a real install: the storage mechanism
|
|
19
|
+
* was read directly out of two projects' own current shipped source — not
|
|
20
|
+
* inferred from a blog post — cross-checked against Sourcegraph's own docs:
|
|
21
|
+
*
|
|
22
|
+
* 1. CODY'S OWN SOURCE, read directly from
|
|
23
|
+
* github.com/sourcegraph/cody-public-snapshot (`main`,
|
|
24
|
+
* vscode/src/services/LocalStorageProvider.ts, fetched verbatim via
|
|
25
|
+
* `gh api repos/sourcegraph/cody-public-snapshot/contents/...` on
|
|
26
|
+
* 2026-09-02): the class keeps
|
|
27
|
+
* `protected readonly KEY_LOCAL_HISTORY = 'cody-local-chatHistory-v2'`
|
|
28
|
+
* and every read/write of chat history
|
|
29
|
+
* (getChatHistory/setChatHistory/deleteChatHistory) goes through
|
|
30
|
+
* `this.storage.get/update(this.KEY_LOCAL_HISTORY, ...)`, where
|
|
31
|
+
* `this.storage` is set, once, at extension activation via
|
|
32
|
+
* `localStorage.setStorage(context.globalState)` (see the same file,
|
|
33
|
+
* `activate()`/`initVSCodeStorage()` in `LocalStorageProvider.ts` and
|
|
34
|
+
* `extension.node.ts`) — i.e. the standard VS Code extension `Memento`
|
|
35
|
+
* API (`context.globalState`), not a bespoke file Cody writes itself.
|
|
36
|
+
* `vscode/package.json` (same repo, same fetch) confirms the extension's
|
|
37
|
+
* identity: `"name": "cody-ai"`, `"publisher": "sourcegraph"` — VS
|
|
38
|
+
* Code's own `getExtensionId(publisher, name)` (see next point) makes
|
|
39
|
+
* that `sourcegraph.cody-ai`, matching the `globalStorage/
|
|
40
|
+
* sourcegraph.cody-ai/` path fragment independently named in
|
|
41
|
+
* Sourcegraph's own troubleshooting docs
|
|
42
|
+
* (sourcegraph.com/docs/cody/troubleshooting, a real user's globalStorage
|
|
43
|
+
* resource path is quoted there verbatim).
|
|
44
|
+
*
|
|
45
|
+
* 2. VS CODE'S OWN SOURCE for what `context.globalState` actually resolves
|
|
46
|
+
* to on disk, read directly from github.com/microsoft/vscode (`main`,
|
|
47
|
+
* fetched the same way, 2026-09-02):
|
|
48
|
+
* - `src/vs/workbench/api/browser/mainThreadStorage.ts` —
|
|
49
|
+
* `$setValue(shared, key, value)` calls
|
|
50
|
+
* `extensionStorageService.setExtensionState(key, value, shared)`,
|
|
51
|
+
* where `key` here is the calling extension's id, not a
|
|
52
|
+
* caller-chosen storage key.
|
|
53
|
+
* - `src/vs/platform/extensionManagement/common/extensionStorage.ts`
|
|
54
|
+
* — `setExtensionState(extension, state, global)` resolves
|
|
55
|
+
* `extensionId = getExtensionId(extension)` and then does
|
|
56
|
+
* `storageService.store(extensionId, JSON.stringify(state),
|
|
57
|
+
* global ? StorageScope.PROFILE : StorageScope.WORKSPACE, ...)`.
|
|
58
|
+
* `getExtensionId(publisher, name)` (`extensionManagementUtil.ts`)
|
|
59
|
+
* is exactly `` `${publisher}.${name}` ``.
|
|
60
|
+
* - Net effect: EVERY globalState key an extension sets (Cody's
|
|
61
|
+
* `cody-local-chatHistory-v2` included) is merged into ONE JSON
|
|
62
|
+
* object, and that whole object is stored as a SINGLE row, keyed by
|
|
63
|
+
* the extension id itself, in the PROFILE-scope storage — which is
|
|
64
|
+
* the same shared, per-profile `state.vscdb` / `ItemTable` that
|
|
65
|
+
* cursor.js already reads for Cursor's own tables in this project
|
|
66
|
+
* (Cursor is a VS Code fork; this is the un-forked, upstream version
|
|
67
|
+
* of that exact mechanism). This is CORE VS Code behavior, not
|
|
68
|
+
* Cody-specific or version-fragile the way Cursor's own bespoke
|
|
69
|
+
* `cursorDiskKV` table naming has reportedly been (see cursor.js).
|
|
70
|
+
*
|
|
71
|
+
* Net path: `<VS Code User dir>/globalStorage/state.vscdb`, table
|
|
72
|
+
* `ItemTable`, one row with `key = 'sourcegraph.cody-ai'` whose `value` is a
|
|
73
|
+
* JSON object containing (among any other keys the extension has ever set)
|
|
74
|
+
* `cody-local-chatHistory-v2` — every chat transcript title, message, and any
|
|
75
|
+
* pasted code/output the user has had Cody see.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately scoped to VS Code only. Cody also ships a JetBrains plugin,
|
|
78
|
+
* but its chat history lives in a fundamentally different place: read
|
|
79
|
+
* directly from the same cody-public-snapshot repo,
|
|
80
|
+
* `jetbrains/src/main/kotlin/com/sourcegraph/cody/history/HistoryService.kt`
|
|
81
|
+
* declares `@State(name = "ChatHistory", storages =
|
|
82
|
+
* [Storage("cody_history.xml")])` at `@Service(Service.Level.PROJECT)` —
|
|
83
|
+
* PROJECT level, meaning one `cody_history.xml` per JetBrains project
|
|
84
|
+
* (typically under that project's own `.idea/` directory), not one file
|
|
85
|
+
* under a single, enumerable user-profile directory the way every other
|
|
86
|
+
* source in this project works. Finding those would mean either scanning the
|
|
87
|
+
* whole filesystem for `.idea/cody_history.xml` or trusting a JetBrains
|
|
88
|
+
* "recent projects" list — neither verified here and both a meaningfully
|
|
89
|
+
* different shape of problem — so it is left out rather than guessed at. See
|
|
90
|
+
* CONTRIBUTING.md.
|
|
91
|
+
*
|
|
92
|
+
* Only the DEFAULT VS Code profile is covered, for the same reason
|
|
93
|
+
* copilot-chat.js in this project already gives for itself: a non-default
|
|
94
|
+
* profile's globalStorage lives under `User/profiles/<profileId>/...`
|
|
95
|
+
* instead of `User/globalStorage` directly, per
|
|
96
|
+
* `IUserDataProfilesService.defaultProfile` in the same VS Code source read
|
|
97
|
+
* above.
|
|
98
|
+
*/
|
|
99
|
+
function vscodeUserDirs() {
|
|
100
|
+
const home = os.homedir();
|
|
101
|
+
const variants = ["Code", "Code - Insiders"];
|
|
102
|
+
if (process.platform === "darwin") {
|
|
103
|
+
return variants.map((v) => path.join(home, "Library", "Application Support", v, "User"));
|
|
104
|
+
}
|
|
105
|
+
if (process.platform === "win32") {
|
|
106
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
107
|
+
return variants.map((v) => path.join(appData, v, "User"));
|
|
108
|
+
}
|
|
109
|
+
// Linux and other XDG-following unix platforms.
|
|
110
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
111
|
+
return variants.map((v) => path.join(configHome, v, "User"));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function id() { return "cody"; }
|
|
115
|
+
function label() { return "Sourcegraph Cody"; }
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Same lazy-require pattern as cursor.js, for the same reason: index.js
|
|
119
|
+
* requires every source unconditionally, so an eager top-level
|
|
120
|
+
* `require("node:sqlite")` would print Node's one-per-process
|
|
121
|
+
* ExperimentalWarning for every user on Node 22.5+, even the large majority
|
|
122
|
+
* who have never installed VS Code at all. See cursor.js's own docstring on
|
|
123
|
+
* getDatabaseSync() for the full reasoning — duplicated here rather than
|
|
124
|
+
* imported, matching this project's "small, self-contained file" convention.
|
|
125
|
+
*/
|
|
126
|
+
const NODE_SQLITE_REQUIREMENT = "needs Node.js 22.5+ (node:sqlite not present in this runtime)";
|
|
127
|
+
let sqliteRequireAttempted = false;
|
|
128
|
+
let DatabaseSync = null;
|
|
129
|
+
|
|
130
|
+
function getDatabaseSync() {
|
|
131
|
+
if (!sqliteRequireAttempted) {
|
|
132
|
+
sqliteRequireAttempted = true;
|
|
133
|
+
try { ({ DatabaseSync } = require("node:sqlite")); }
|
|
134
|
+
catch { DatabaseSync = null; }
|
|
135
|
+
}
|
|
136
|
+
return DatabaseSync;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function anyVscodeUserDirExists() {
|
|
140
|
+
return vscodeUserDirs().some((dir) => {
|
|
141
|
+
try { return fs.statSync(dir).isDirectory(); } catch { return false; }
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function available() {
|
|
146
|
+
// Cheap fs check first, on purpose — see cursor.js's available() for why
|
|
147
|
+
// short-circuiting here matters (skip requiring node:sqlite, and its
|
|
148
|
+
// possible warning, when there is plainly nothing to read).
|
|
149
|
+
return anyVscodeUserDirExists() && Boolean(getDatabaseSync());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Same optional, additive contract as cursor.js's unavailableReason() — see
|
|
154
|
+
* that file's docstring. Only fires for the one case worth calling out: a VS
|
|
155
|
+
* Code User dir genuinely exists but this Node runtime is too old for
|
|
156
|
+
* node:sqlite, so silently vanishing from "Sources checked" would misread as
|
|
157
|
+
* "VS Code isn't installed," which would be false.
|
|
158
|
+
*/
|
|
159
|
+
function unavailableReason() {
|
|
160
|
+
if (!anyVscodeUserDirExists()) return null;
|
|
161
|
+
if (getDatabaseSync()) return null;
|
|
162
|
+
return `Sourcegraph Cody detected but not scanned — ${NODE_SQLITE_REQUIREMENT}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Same defensive symlink-following stat, duplicated from cursor.js's
|
|
167
|
+
* statIfPresent — see that file's docstring for the full reasoning. A path
|
|
168
|
+
* that doesn't exist yields nothing (normal: e.g. no "Code - Insiders" User
|
|
169
|
+
* dir because Insiders was never installed); a dangling symlink is reported
|
|
170
|
+
* `broken: true` rather than silently skipped.
|
|
171
|
+
*/
|
|
172
|
+
function* statIfPresent(dbPath) {
|
|
173
|
+
let lst;
|
|
174
|
+
try { lst = fs.lstatSync(dbPath); }
|
|
175
|
+
catch { return; }
|
|
176
|
+
|
|
177
|
+
if (lst.isSymbolicLink()) {
|
|
178
|
+
try {
|
|
179
|
+
const st = fs.statSync(dbPath); // follow the link
|
|
180
|
+
if (!st.isFile()) { yield { file: dbPath, broken: true }; return; }
|
|
181
|
+
yield { file: dbPath, mtimeMs: st.mtimeMs, sizeBytes: st.size, broken: false };
|
|
182
|
+
} catch {
|
|
183
|
+
yield { file: dbPath, broken: true }; // dangling symlink
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!lst.isFile()) return; // something unexpected sits at this path — out of scope, not broken
|
|
189
|
+
yield { file: dbPath, mtimeMs: lst.mtimeMs, sizeBytes: lst.size, broken: false };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Yield { file, mtimeMs, sizeBytes, broken } for the default profile's
|
|
194
|
+
* state.vscdb under every VS Code variant this adapter checks (standard +
|
|
195
|
+
* Insiders). Purely a filesystem walk + stat — never opens the database, so
|
|
196
|
+
* it works even in a Node runtime where node:sqlite isn't available (only
|
|
197
|
+
* readLines() actually needs it, same division of labour as cursor.js).
|
|
198
|
+
*/
|
|
199
|
+
function* files() {
|
|
200
|
+
for (const userDir of vscodeUserDirs()) {
|
|
201
|
+
yield* statIfPresent(path.join(userDir, "globalStorage", "state.vscdb"));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Not backed by a real Cody state.vscdb row this tool was tested against (no
|
|
206
|
+
// VS Code install to test with) — see the verification-status note above.
|
|
207
|
+
// Generous backstop against a corrupted/pathological file, same rationale as
|
|
208
|
+
// cursor.js's own MAX_DB_BYTES.
|
|
209
|
+
const MAX_DB_BYTES = 512 * 1024 * 1024;
|
|
210
|
+
const READ_TIMEOUT_MS = 60_000;
|
|
211
|
+
const BUSY_TIMEOUT_MS = 5_000;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Cody's extension id is `sourcegraph.cody-ai` (verified directly against
|
|
215
|
+
* vscode/package.json — see the module docstring). The LIKE clause is a
|
|
216
|
+
* deliberately narrow safety margin, not a guess-widening: it catches a
|
|
217
|
+
* differently-suffixed Sourcegraph Cody extension id (e.g. a possible future
|
|
218
|
+
* `sourcegraph.cody-ai-nightly`-style variant) without sweeping in any other
|
|
219
|
+
* publisher's or extension's state the way reading the whole ItemTable
|
|
220
|
+
* (cursor.js's approach, appropriate there because Cursor owns that entire
|
|
221
|
+
* file) would for a shared, multi-extension file like this one.
|
|
222
|
+
*/
|
|
223
|
+
const CODY_KEY_PATTERN = "sourcegraph.cody%";
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Same storage-class handling as cursor.js's valueToText — see that file's
|
|
227
|
+
* docstring. A PROFILE-scope row here is written via
|
|
228
|
+
* `JSON.stringify(state)` (see module docstring), i.e. always a JS string
|
|
229
|
+
* (TEXT storage class) in every real case this adapter's research found, but
|
|
230
|
+
* the BLOB fallback is kept for parity with cursor.js and cheap insurance
|
|
231
|
+
* against a SQLite storage-class surprise.
|
|
232
|
+
*/
|
|
233
|
+
function valueToText(value) {
|
|
234
|
+
if (typeof value === "string") return value;
|
|
235
|
+
if (value instanceof Uint8Array) return Buffer.from(value).toString("utf-8");
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Read one state.vscdb's Cody-owned row(s) as an array of raw text "lines" —
|
|
241
|
+
* one per matching ItemTable row's decoded value (in practice, at most one:
|
|
242
|
+
* a single row keyed `sourcegraph.cody-ai` holding a JSON object with every
|
|
243
|
+
* globalState key the extension has ever set, `cody-local-chatHistory-v2`
|
|
244
|
+
* included). Returns { lines, status, bytesRead } with the same status
|
|
245
|
+
* vocabulary as every other source in this project.
|
|
246
|
+
*
|
|
247
|
+
* Synchronous node:sqlite, same "no real preemptive timeout possible, so
|
|
248
|
+
* check a wall-clock deadline between statements" approach as cursor.js —
|
|
249
|
+
* see that file's docstring. With at most a handful of matching rows here
|
|
250
|
+
* (unlike cursor.js's tens-of-thousands-of-rows cursorDiskKV table), the
|
|
251
|
+
* per-row yield-to-event-loop machinery is far less likely to matter in
|
|
252
|
+
* practice, but the deadline check is kept for consistency and as insurance
|
|
253
|
+
* against a single pathologically large row's own decode time — the same
|
|
254
|
+
* named, un-bounded asymmetry cursor.js's own docstring admits.
|
|
255
|
+
*/
|
|
256
|
+
async function readLines(file) {
|
|
257
|
+
const DB = getDatabaseSync();
|
|
258
|
+
if (!DB) return { lines: [], status: "failed", bytesRead: 0 };
|
|
259
|
+
|
|
260
|
+
let stat;
|
|
261
|
+
try { stat = fs.statSync(file); }
|
|
262
|
+
catch { return { lines: [], status: "failed", bytesRead: 0 }; }
|
|
263
|
+
if (stat.size > MAX_DB_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
|
|
264
|
+
|
|
265
|
+
let db;
|
|
266
|
+
try {
|
|
267
|
+
db = new DB(file, { readOnly: true });
|
|
268
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
269
|
+
} catch {
|
|
270
|
+
// Deleted between files() and this call, a corrupt/non-SQLite file at
|
|
271
|
+
// this path, or VS Code holding a lock this readonly open can't get past
|
|
272
|
+
// within BUSY_TIMEOUT_MS — genuinely "could not read this," not "read it,
|
|
273
|
+
// found nothing." Status "failed" keeps the scan report honest about
|
|
274
|
+
// that difference (CONTRIBUTING.md rule 5).
|
|
275
|
+
return { lines: [], status: "failed", bytesRead: 0 };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let rows;
|
|
279
|
+
try {
|
|
280
|
+
rows = db.prepare("SELECT value FROM ItemTable WHERE key LIKE ?").iterate(CODY_KEY_PATTERN);
|
|
281
|
+
} catch {
|
|
282
|
+
// ItemTable itself doesn't exist — this file opened fine as SQLite but
|
|
283
|
+
// didn't match the schema this adapter understands, a real "could not
|
|
284
|
+
// extract anything," not "extracted zero real rows."
|
|
285
|
+
try { db.close(); } catch { /* best-effort */ }
|
|
286
|
+
return { lines: [], status: "failed", bytesRead: 0 };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const lines = [];
|
|
290
|
+
let bytesRead = 0;
|
|
291
|
+
const deadline = Date.now() + READ_TIMEOUT_MS;
|
|
292
|
+
let timedOut = false;
|
|
293
|
+
let sawError = false;
|
|
294
|
+
let n = 0;
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
for (const row of rows) {
|
|
298
|
+
const text = valueToText(row.value);
|
|
299
|
+
// One matching row becomes one scanned "line," using the value exactly
|
|
300
|
+
// as VS Code wrote it — not re-parsed/re-stringified — same reasoning
|
|
301
|
+
// as cursor.js: keeps every byte the regexes depend on intact, and
|
|
302
|
+
// sidesteps needing to track Cody's own nested key name
|
|
303
|
+
// (`cody-local-chatHistory-v2`) as it evolves across versions.
|
|
304
|
+
if (text) { lines.push(text); bytesRead += Buffer.byteLength(text, "utf-8"); }
|
|
305
|
+
n++;
|
|
306
|
+
if (n % 500 === 0) {
|
|
307
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
308
|
+
if (Date.now() > deadline) { timedOut = true; break; }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// Whatever WAS read before a mid-iteration failure (e.g. a corrupted
|
|
313
|
+
// page) is real content and may contain a real secret — kept, not
|
|
314
|
+
// discarded, same as claude-code.js/cursor.js.
|
|
315
|
+
sawError = true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try { db.close(); } catch { /* best-effort close */ }
|
|
319
|
+
|
|
320
|
+
if (sawError && lines.length === 0) return { lines: [], status: "failed", bytesRead };
|
|
321
|
+
if (timedOut || sawError) return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
|
|
322
|
+
return { lines, status: "complete", bytesRead };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = { id, label, available, unavailableReason, files, readLines };
|