residoo 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +225 -46
  2. package/SECURITY.md +29 -22
  3. package/package.json +1 -1
  4. package/src/cli.js +82 -16
  5. package/src/integrity.js +669 -0
  6. package/src/patterns.js +78 -5
  7. package/src/report.js +74 -7
  8. package/src/sources/agent-configs.js +308 -0
  9. package/src/sources/aider.js +361 -0
  10. package/src/sources/amazon-q.js +199 -0
  11. package/src/sources/antigravity-cli.js +155 -0
  12. package/src/sources/cline.js +208 -0
  13. package/src/sources/codebuff.js +295 -0
  14. package/src/sources/codex-cli.js +258 -0
  15. package/src/sources/cody.js +325 -0
  16. package/src/sources/continue.js +408 -0
  17. package/src/sources/copilot-chat.js +272 -0
  18. package/src/sources/copilot-cli.js +300 -0
  19. package/src/sources/crush.js +364 -0
  20. package/src/sources/cursor.js +374 -0
  21. package/src/sources/devin-cli.js +241 -0
  22. package/src/sources/factory-droid.js +153 -0
  23. package/src/sources/fx.js +136 -0
  24. package/src/sources/gemini-cli.js +242 -0
  25. package/src/sources/goose.js +366 -0
  26. package/src/sources/grok-cli.js +267 -0
  27. package/src/sources/hermes.js +282 -0
  28. package/src/sources/index.js +172 -8
  29. package/src/sources/jetbrains-ai-assistant.js +343 -0
  30. package/src/sources/jetbrains-junie.js +292 -0
  31. package/src/sources/kilo-code.js +430 -0
  32. package/src/sources/kimi-code.js +147 -0
  33. package/src/sources/kiro-cli.js +393 -0
  34. package/src/sources/kiro-ide.js +230 -0
  35. package/src/sources/llm.js +328 -0
  36. package/src/sources/mentat.js +143 -0
  37. package/src/sources/open-interpreter.js +224 -0
  38. package/src/sources/openclaw.js +218 -0
  39. package/src/sources/opencode.js +379 -0
  40. package/src/sources/openhands.js +181 -0
  41. package/src/sources/pearai.js +151 -0
  42. package/src/sources/pi-agent.js +130 -0
  43. package/src/sources/qodo-gen.js +189 -0
  44. package/src/sources/qwen-code.js +244 -0
  45. package/src/sources/roo-code.js +239 -0
  46. package/src/sources/trae.js +294 -0
  47. package/src/sources/void.js +273 -0
  48. package/src/sources/warp.js +395 -0
  49. package/src/sources/windsurf.js +256 -0
  50. package/src/sources/zed.js +374 -0
@@ -0,0 +1,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 };