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,294 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ /**
8
+ * Trae's local chat/composer history.
9
+ *
10
+ * Trae (ByteDance) is a fork of Code OSS (VS Code's open-source base), and
11
+ * it inherited VS Code's per-profile SQLite storage approach wholesale: a
12
+ * `state.vscdb` file containing a table called `ItemTable`, both under
13
+ * `globalStorage` (one per profile) and per-project under
14
+ * `workspaceStorage/<hash>` — same physical shape as Cursor's, see
15
+ * cursor.js's own header for the general mechanism. Unlike Cursor, nothing
16
+ * found in this source's research indicates Trae adds a second, custom
17
+ * table alongside `ItemTable` — its AI chat/agent content instead appears to
18
+ * live inside `ItemTable` itself, under specially-prefixed keys (see below).
19
+ *
20
+ * VERIFICATION STATUS (read this before trusting anything below): this is
21
+ * corroborated by two independent, real, currently-maintained community
22
+ * tools that read these exact files, not a single guess:
23
+ *
24
+ * 1. `ai-data-extraction` (github.com/0xSero/ai-data-extraction) — a real
25
+ * extraction tool covering Cursor, Codex, Claude Code, Windsurf, and
26
+ * Trae — documents Trae's search paths as `~/.trae` and
27
+ * `~/Library/Application Support/Trae`, formats "JSONL and SQLite
28
+ * databases".
29
+ * 2. `claude-code-history-viewer` (github.com/jhlee0409/claude-code-history-viewer)
30
+ * — a separate, independently authored, actively maintained desktop
31
+ * app — has a dedicated Trae provider module
32
+ * (`src-tauri/src/providers/trae.rs`) whose contents were fetched and
33
+ * read directly. It resolves the platform config directory then
34
+ * appends `Trae/User/workspaceStorage` — i.e. macOS
35
+ * `~/Library/Application Support/Trae/User/workspaceStorage`, Linux
36
+ * `~/.config/Trae/User/workspaceStorage`, Windows
37
+ * `%APPDATA%/Trae/User/workspaceStorage` — each `<hash>/state.vscdb`
38
+ * holding a single table `ItemTable (key TEXT UNIQUE ON CONFLICT
39
+ * REPLACE, value TEXT)`, with **no decompression applied to values —
40
+ * parsed directly as JSON strings**.
41
+ *
42
+ * These two sources agree with each other on the directory layout, the
43
+ * table name, and the column shape, and that layout matches the
44
+ * already-established vanilla-VS-Code / Cursor pattern this project already
45
+ * trusts (see cursor.js) — which is why `globalStorage/state.vscdb` is also
46
+ * included below even though source #2's own description only walked
47
+ * `workspaceStorage` explicitly: every VS Code-family storage service
48
+ * maintains both a per-profile globalStorage and a per-project
49
+ * workspaceStorage side by side, and stated plainly, that specific half
50
+ * (globalStorage existing for Trae too) rests on the general VS Code
51
+ * architecture fact rather than a source naming Trae's globalStorage by name.
52
+ *
53
+ * What is explicitly NOT trusted here: source #2's own documentation flags
54
+ * the specific KEY NAMES it looks for for chat/agent content — things like
55
+ * `memento/icube-ai-agent-storage`, `ChatStore`,
56
+ * `memento/icube-ai-chat-storage-*` (Trae's internal AI subsystem appears to
57
+ * be code-named "icube") — with its own explicit warning: "Unlike the other
58
+ * providers, this schema is NOT from official source — it is
59
+ * reverse-engineered" and "has not been verified against a real Trae
60
+ * install." Rather than bake in a specific key allowlist that even its own
61
+ * author calls provisional, this source follows cursor.js's own precedent
62
+ * exactly: no key-name filtering at all. Every row's value in `ItemTable` is
63
+ * turned into one scanned line, regardless of which key it's stored under.
64
+ * That sidesteps needing the exact icube key names to be right, at the cost
65
+ * of also picking up ordinary editor/window state alongside chat content —
66
+ * the same tradeoff cursor.js already makes and explains in its own header.
67
+ *
68
+ * This source has NOT been checked against a real Trae install — Trae is
69
+ * not installed on the machine this was built on (checked: not in
70
+ * /Applications, not in ~/Library/Application Support, no mdfind hits). If
71
+ * you have Trae installed, running `residoo scan` and confirming
72
+ * `sourcesScanned`/`filesScanned` for "trae" look right against what you
73
+ * know is on disk is the single most useful way to firm this up.
74
+ *
75
+ * Deliberately NOT covered here: `~/.trae` (JSONL files) mentioned by source
76
+ * #1 above. That path is ambiguous — ByteDance also ships a separate,
77
+ * genuinely open-source CLI tool, `trae-agent` (github.com/bytedance/trae-agent,
78
+ * a different product from the Trae IDE this source targets), whose own
79
+ * README documents trajectory files saved to a `trajectories/` directory
80
+ * *relative to wherever the CLI was run*, not a fixed home-directory path —
81
+ * meaning `~/.trae` may not exist at all, or may hold something unrelated to
82
+ * what source #1's short description implied. Rather than guess which it
83
+ * is, this source only covers the `state.vscdb` layout that two independent
84
+ * tools corroborate in matching, specific detail.
85
+ */
86
+ function traeUserDir() {
87
+ const home = os.homedir();
88
+ if (process.platform === "darwin") {
89
+ return path.join(home, "Library", "Application Support", "Trae", "User");
90
+ }
91
+ if (process.platform === "win32") {
92
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
93
+ return path.join(appData, "Trae", "User");
94
+ }
95
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
96
+ return path.join(configHome, "Trae", "User");
97
+ }
98
+
99
+ const USER_DIR = traeUserDir();
100
+ const GLOBAL_STORAGE_DB = path.join(USER_DIR, "globalStorage", "state.vscdb");
101
+ const WORKSPACE_STORAGE_DIR = path.join(USER_DIR, "workspaceStorage");
102
+
103
+ // Same lazy-require pattern as cursor.js, for the same reason — see that
104
+ // file's docstring for the full explanation of why this is deferred until
105
+ // Trae's own directory is confirmed to exist, and why that matters for the
106
+ // zero-runtime-dependency rule and the ExperimentalWarning node:sqlite prints.
107
+ const NODE_SQLITE_REQUIREMENT = "needs Node.js 22.5+ (node:sqlite not present in this runtime)";
108
+ let sqliteRequireAttempted = false;
109
+ let DatabaseSync = null;
110
+
111
+ function getDatabaseSync() {
112
+ if (!sqliteRequireAttempted) {
113
+ sqliteRequireAttempted = true;
114
+ try { ({ DatabaseSync } = require("node:sqlite")); }
115
+ catch { DatabaseSync = null; }
116
+ }
117
+ return DatabaseSync;
118
+ }
119
+
120
+ function id() { return "trae"; }
121
+ function label() { return "Trae"; }
122
+
123
+ function userDirExists() {
124
+ try { return fs.statSync(USER_DIR).isDirectory(); } catch { return false; }
125
+ }
126
+
127
+ function available() {
128
+ return userDirExists() && Boolean(getDatabaseSync());
129
+ }
130
+
131
+ /**
132
+ * Same optional, additive export as cursor.js's unavailableReason() — see
133
+ * that file's docstring for why this exists and when callers should use it.
134
+ */
135
+ function unavailableReason() {
136
+ if (!userDirExists()) return null;
137
+ if (getDatabaseSync()) return null;
138
+ return `Trae detected but not scanned — ${NODE_SQLITE_REQUIREMENT}`;
139
+ }
140
+
141
+ /**
142
+ * Same defensive symlink-following pattern as cursor.js's own
143
+ * isDirFollowingSymlink — duplicated rather than imported, matching this
144
+ * project's "small, self-contained file" convention.
145
+ */
146
+ function isDirFollowingSymlink(fullPath, dirent) {
147
+ if (dirent.isDirectory()) return true;
148
+ if (!dirent.isSymbolicLink()) return false;
149
+ try { return fs.statSync(fullPath).isDirectory(); } catch { return false; }
150
+ }
151
+
152
+ /**
153
+ * Resolve one candidate `state.vscdb` path into zero or one files() entries.
154
+ * Identical logic and identical broken-vs-absent convention to cursor.js's
155
+ * statIfPresent() — see that file's docstring for the reasoning.
156
+ */
157
+ function* statIfPresent(dbPath) {
158
+ let lst;
159
+ try { lst = fs.lstatSync(dbPath); }
160
+ catch { return; }
161
+
162
+ if (lst.isSymbolicLink()) {
163
+ try {
164
+ const st = fs.statSync(dbPath);
165
+ if (!st.isFile()) { yield { file: dbPath, broken: true }; return; }
166
+ yield { file: dbPath, mtimeMs: st.mtimeMs, sizeBytes: st.size, broken: false };
167
+ } catch {
168
+ yield { file: dbPath, broken: true };
169
+ }
170
+ return;
171
+ }
172
+
173
+ if (!lst.isFile()) return;
174
+ yield { file: dbPath, mtimeMs: lst.mtimeMs, sizeBytes: lst.size, broken: false };
175
+ }
176
+
177
+ /**
178
+ * Yield { file, mtimeMs, sizeBytes, broken } for every state.vscdb found —
179
+ * one for globalStorage, one per workspaceStorage/<hash> directory. Purely
180
+ * a filesystem walk + stat, same division of labour as cursor.js's files().
181
+ */
182
+ function* files() {
183
+ yield* statIfPresent(GLOBAL_STORAGE_DB);
184
+
185
+ let workspaceDirs;
186
+ try { workspaceDirs = fs.readdirSync(WORKSPACE_STORAGE_DIR, { withFileTypes: true }); }
187
+ catch { return; }
188
+
189
+ for (const ws of workspaceDirs) {
190
+ const wsDir = path.join(WORKSPACE_STORAGE_DIR, ws.name);
191
+ if (!isDirFollowingSymlink(wsDir, ws)) {
192
+ if (ws.isSymbolicLink()) yield { file: wsDir, broken: true };
193
+ continue;
194
+ }
195
+ yield* statIfPresent(path.join(wsDir, "state.vscdb"));
196
+ }
197
+ }
198
+
199
+ // No Trae-specific large-file data point exists (no real install to measure
200
+ // against) — carried over unchanged from cursor.js as a generous,
201
+ // conservative backstop, not a figure backed by an observed real file here.
202
+ const MAX_DB_BYTES = 512 * 1024 * 1024;
203
+ const READ_TIMEOUT_MS = 60_000;
204
+ const BUSY_TIMEOUT_MS = 5_000;
205
+ const YIELD_EVERY_N_ROWS = 500;
206
+
207
+ /**
208
+ * Same value-decoding rule as cursor.js's valueToText() — a TEXT-stored
209
+ * value comes back as a JS string, a BLOB-stored one as a Uint8Array. The
210
+ * schema found for Trae's `ItemTable` declares `value TEXT` (rather than
211
+ * Cursor's BLOB-affinity column), so the string branch is expected to cover
212
+ * the normal case here — the Uint8Array branch is kept anyway, defensively,
213
+ * exactly the way cursor.js keeps it for whatever storage class an
214
+ * individual row actually used, regardless of the column's declared type.
215
+ */
216
+ function valueToText(value) {
217
+ if (typeof value === "string") return value;
218
+ if (value instanceof Uint8Array) return Buffer.from(value).toString("utf-8");
219
+ return null;
220
+ }
221
+
222
+ /**
223
+ * Read one state.vscdb as an array of raw text "lines" — one per row's
224
+ * decoded value from `ItemTable`. Same synchronous-native-call constraint,
225
+ * same cooperative-yield-plus-deadline timeout approach, and same status
226
+ * vocabulary as cursor.js's readLines() — see that file's docstring for the
227
+ * full reasoning, which applies unchanged here.
228
+ *
229
+ * Only one table is queried (`ItemTable`) — unlike cursor.js's two
230
+ * (`ItemTable` and Cursor's own `cursorDiskKV`), because no second,
231
+ * Trae-specific table name is corroborated by more than one source (see the
232
+ * module docstring above on why the "icube" key names found in a single
233
+ * source are deliberately not hard-coded anywhere in this file, including
234
+ * here as a table name).
235
+ */
236
+ async function readLines(file) {
237
+ const DB = getDatabaseSync();
238
+ if (!DB) return { lines: [], status: "failed", bytesRead: 0 };
239
+
240
+ let stat;
241
+ try { stat = fs.statSync(file); }
242
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
243
+ if (stat.size > MAX_DB_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
244
+
245
+ let db;
246
+ try {
247
+ db = new DB(file, { readOnly: true });
248
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
249
+ } catch {
250
+ return { lines: [], status: "failed", bytesRead: 0 };
251
+ }
252
+
253
+ const lines = [];
254
+ let bytesRead = 0;
255
+ const deadline = Date.now() + READ_TIMEOUT_MS;
256
+ let timedOut = false;
257
+ let sawError = false;
258
+ let foundAnyTable = false;
259
+
260
+ for (const table of ["ItemTable"]) {
261
+ let rows;
262
+ try {
263
+ rows = db.prepare(`SELECT key, value FROM ${table}`).iterate();
264
+ } catch {
265
+ continue;
266
+ }
267
+ foundAnyTable = true;
268
+
269
+ let n = 0;
270
+ try {
271
+ for (const row of rows) {
272
+ const text = valueToText(row.value);
273
+ if (text) { lines.push(text); bytesRead += Buffer.byteLength(text, "utf-8"); }
274
+ n++;
275
+ if (n % YIELD_EVERY_N_ROWS === 0) {
276
+ await new Promise((resolve) => setImmediate(resolve));
277
+ if (Date.now() > deadline) { timedOut = true; break; }
278
+ }
279
+ }
280
+ } catch {
281
+ sawError = true;
282
+ }
283
+ if (timedOut) break;
284
+ }
285
+
286
+ try { db.close(); } catch { /* best-effort close */ }
287
+
288
+ if (!foundAnyTable) return { lines: [], status: "failed", bytesRead: 0 };
289
+ if (sawError && lines.length === 0) return { lines: [], status: "failed", bytesRead };
290
+ if (timedOut || sawError) return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
291
+ return { lines, status: "complete", bytesRead };
292
+ }
293
+
294
+ module.exports = { id, label, available, unavailableReason, files, readLines };
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ /**
8
+ * Void's local chat thread history.
9
+ *
10
+ * Void (voideditor/void, Apache-2.0) is an open-source fork of VS Code, and
11
+ * it inherited VS Code's own storage service wholesale — it does NOT add a
12
+ * Cursor-style second custom table. Its AI chat is stored as ordinary VS
13
+ * Code application state: one row in the standard `ItemTable` of the
14
+ * standard per-profile `state.vscdb`, under a Void-specific key.
15
+ *
16
+ * VERIFICATION STATUS (read this before trusting anything below): unlike
17
+ * pearai.js and trae.js, which lean on independent third-party tools, this
18
+ * was confirmed by reading Void's own shipped TypeScript source directly
19
+ * (voideditor/void@main, fetched verbatim from GitHub):
20
+ *
21
+ * - `src/vs/workbench/contrib/void/browser/chatThreadService.ts` persists
22
+ * every chat thread in one call:
23
+ * this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads),
24
+ * StorageScope.APPLICATION, StorageTarget.USER);
25
+ * i.e. the *entire* set of chat threads, serialized as one JSON blob, at
26
+ * APPLICATION scope (VS Code's storage service backs APPLICATION-scope
27
+ * keys with the per-profile globalStorage `state.vscdb`'s `ItemTable` —
28
+ * the same mechanism, and the same physical file, Cursor's own
29
+ * non-custom-table state uses; see cursor.js's header for that
30
+ * mechanism generally).
31
+ * - `src/vs/workbench/contrib/void/common/storageKeys.ts` defines the
32
+ * literal key: `THREAD_STORAGE_KEY = 'void.chatThreadStorageII'` (plus
33
+ * `VOID_SETTINGS_STORAGE_KEY = 'void.settingsServiceStorageII'` and
34
+ * `OPT_OUT_KEY = 'void.app.optOutAll'`, both also APPLICATION-scoped by
35
+ * the same mechanism).
36
+ *
37
+ * This is direct confirmation of the actual mechanism, not a third party's
38
+ * inference — but it does NOT by itself confirm the per-OS Application
39
+ * Support-style folder NAME Void's Electron shell actually writes to on a
40
+ * real machine: no independent blog post, forum thread, or community tool
41
+ * naming that exact path (e.g. `~/Library/Application Support/Void`
42
+ * specifically) was found in this source's research. That folder name is
43
+ * derived instead from `product.json`'s `nameShort`/`nameLong`, both
44
+ * `"Void"` (fetched directly from voideditor/void's own product.json), via
45
+ * the same VS Code-family convention already relied on for Cursor in this
46
+ * project (`~/Library/Application Support/Cursor`, nameShort `"Cursor"`)
47
+ * and independently cross-checked against VSCodium, whose product.json sets
48
+ * nameShort to `"VSCodium"` and which is independently documented as
49
+ * writing to `~/Library/Application Support/VSCodium` — i.e. this is a
50
+ * consistent, multi-example VS Code-fork naming rule, not a Void-specific
51
+ * guess, applied to a Void-specific value read from Void's own source.
52
+ *
53
+ * Net effect: HIGH confidence in the storage mechanism and key (read
54
+ * straight from shipped source), MODERATE-plus confidence in the exact
55
+ * folder name (derived from a well-established, cross-checked convention
56
+ * rather than independently witnessed for Void by a third party). Neither
57
+ * substitutes for a real install: Void is not installed on the machine this
58
+ * was built on (checked: not in /Applications, not in ~/Library/Application
59
+ * Support, no mdfind hits), so this has NOT been checked against a real
60
+ * Void install. If you have Void installed, running `residoo scan` and
61
+ * confirming `sourcesScanned`/`filesScanned` for "void" look right against
62
+ * what you know is on disk is the single most useful way to firm this up.
63
+ *
64
+ * As with Trae (see trae.js), no key-name filtering is done here even
65
+ * though the exact chat-thread key IS known (`void.chatThreadStorageII`) —
66
+ * every row in `ItemTable` is turned into one scanned line regardless of
67
+ * key, matching cursor.js's own stated reasoning: key names are exactly the
68
+ * kind of detail known to drift across versions, and scan.js already
69
+ * tolerates raw, unfiltered text. `workspaceStorage/<hash>/state.vscdb` is
70
+ * included alongside `globalStorage` on the same general-VS-Code-architecture
71
+ * basis cursor.js and trae.js both use, even though the specific thread
72
+ * storage read above is APPLICATION-scoped (i.e. global, not per-workspace)
73
+ * — other state (Void's or any installed extension's) may still legitimately
74
+ * live at workspace scope, and this project's existing sources don't
75
+ * cherry-pick which storage tier might matter.
76
+ */
77
+ function voidUserDir() {
78
+ const home = os.homedir();
79
+ if (process.platform === "darwin") {
80
+ return path.join(home, "Library", "Application Support", "Void", "User");
81
+ }
82
+ if (process.platform === "win32") {
83
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
84
+ return path.join(appData, "Void", "User");
85
+ }
86
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
87
+ return path.join(configHome, "Void", "User");
88
+ }
89
+
90
+ const USER_DIR = voidUserDir();
91
+ const GLOBAL_STORAGE_DB = path.join(USER_DIR, "globalStorage", "state.vscdb");
92
+ const WORKSPACE_STORAGE_DIR = path.join(USER_DIR, "workspaceStorage");
93
+
94
+ // Same lazy-require pattern as cursor.js and trae.js, for the same reason —
95
+ // see cursor.js's docstring for the full explanation.
96
+ const NODE_SQLITE_REQUIREMENT = "needs Node.js 22.5+ (node:sqlite not present in this runtime)";
97
+ let sqliteRequireAttempted = false;
98
+ let DatabaseSync = null;
99
+
100
+ function getDatabaseSync() {
101
+ if (!sqliteRequireAttempted) {
102
+ sqliteRequireAttempted = true;
103
+ try { ({ DatabaseSync } = require("node:sqlite")); }
104
+ catch { DatabaseSync = null; }
105
+ }
106
+ return DatabaseSync;
107
+ }
108
+
109
+ function id() { return "void"; }
110
+ function label() { return "Void"; }
111
+
112
+ function userDirExists() {
113
+ try { return fs.statSync(USER_DIR).isDirectory(); } catch { return false; }
114
+ }
115
+
116
+ function available() {
117
+ return userDirExists() && Boolean(getDatabaseSync());
118
+ }
119
+
120
+ /**
121
+ * Same optional, additive export as cursor.js's unavailableReason().
122
+ */
123
+ function unavailableReason() {
124
+ if (!userDirExists()) return null;
125
+ if (getDatabaseSync()) return null;
126
+ return `Void detected but not scanned — ${NODE_SQLITE_REQUIREMENT}`;
127
+ }
128
+
129
+ /**
130
+ * Same defensive symlink-following pattern as cursor.js's
131
+ * isDirFollowingSymlink — duplicated rather than imported, matching this
132
+ * project's "small, self-contained file" convention.
133
+ */
134
+ function isDirFollowingSymlink(fullPath, dirent) {
135
+ if (dirent.isDirectory()) return true;
136
+ if (!dirent.isSymbolicLink()) return false;
137
+ try { return fs.statSync(fullPath).isDirectory(); } catch { return false; }
138
+ }
139
+
140
+ /**
141
+ * Resolve one candidate `state.vscdb` path into zero or one files() entries.
142
+ * Identical logic and identical broken-vs-absent convention to cursor.js's
143
+ * statIfPresent().
144
+ */
145
+ function* statIfPresent(dbPath) {
146
+ let lst;
147
+ try { lst = fs.lstatSync(dbPath); }
148
+ catch { return; }
149
+
150
+ if (lst.isSymbolicLink()) {
151
+ try {
152
+ const st = fs.statSync(dbPath);
153
+ if (!st.isFile()) { yield { file: dbPath, broken: true }; return; }
154
+ yield { file: dbPath, mtimeMs: st.mtimeMs, sizeBytes: st.size, broken: false };
155
+ } catch {
156
+ yield { file: dbPath, broken: true };
157
+ }
158
+ return;
159
+ }
160
+
161
+ if (!lst.isFile()) return;
162
+ yield { file: dbPath, mtimeMs: lst.mtimeMs, sizeBytes: lst.size, broken: false };
163
+ }
164
+
165
+ /**
166
+ * Yield { file, mtimeMs, sizeBytes, broken } for every state.vscdb found —
167
+ * one for globalStorage, one per workspaceStorage/<hash> directory.
168
+ */
169
+ function* files() {
170
+ yield* statIfPresent(GLOBAL_STORAGE_DB);
171
+
172
+ let workspaceDirs;
173
+ try { workspaceDirs = fs.readdirSync(WORKSPACE_STORAGE_DIR, { withFileTypes: true }); }
174
+ catch { return; }
175
+
176
+ for (const ws of workspaceDirs) {
177
+ const wsDir = path.join(WORKSPACE_STORAGE_DIR, ws.name);
178
+ if (!isDirFollowingSymlink(wsDir, ws)) {
179
+ if (ws.isSymbolicLink()) yield { file: wsDir, broken: true };
180
+ continue;
181
+ }
182
+ yield* statIfPresent(path.join(wsDir, "state.vscdb"));
183
+ }
184
+ }
185
+
186
+ // No Void-specific large-file data point exists (no real install to measure
187
+ // against) — carried over unchanged from cursor.js as a generous,
188
+ // conservative backstop, not a figure backed by an observed real file here.
189
+ const MAX_DB_BYTES = 512 * 1024 * 1024;
190
+ const READ_TIMEOUT_MS = 60_000;
191
+ const BUSY_TIMEOUT_MS = 5_000;
192
+ const YIELD_EVERY_N_ROWS = 500;
193
+
194
+ /**
195
+ * Same value-decoding rule as cursor.js's valueToText().
196
+ */
197
+ function valueToText(value) {
198
+ if (typeof value === "string") return value;
199
+ if (value instanceof Uint8Array) return Buffer.from(value).toString("utf-8");
200
+ return null;
201
+ }
202
+
203
+ /**
204
+ * Read one state.vscdb as an array of raw text "lines" — one per row's
205
+ * decoded value from `ItemTable`. Same synchronous-native-call constraint,
206
+ * same cooperative-yield-plus-deadline timeout approach, and same status
207
+ * vocabulary as cursor.js's readLines() — see that file's docstring for the
208
+ * full reasoning, which applies unchanged here.
209
+ *
210
+ * Only `ItemTable` is queried — no second, Void-specific table exists per
211
+ * this source's research (see module docstring: Void's chat persistence
212
+ * goes through the plain IStorageService, not a custom SQLite table the way
213
+ * Cursor's `cursorDiskKV` is).
214
+ */
215
+ async function readLines(file) {
216
+ const DB = getDatabaseSync();
217
+ if (!DB) return { lines: [], status: "failed", bytesRead: 0 };
218
+
219
+ let stat;
220
+ try { stat = fs.statSync(file); }
221
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
222
+ if (stat.size > MAX_DB_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
223
+
224
+ let db;
225
+ try {
226
+ db = new DB(file, { readOnly: true });
227
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
228
+ } catch {
229
+ return { lines: [], status: "failed", bytesRead: 0 };
230
+ }
231
+
232
+ const lines = [];
233
+ let bytesRead = 0;
234
+ const deadline = Date.now() + READ_TIMEOUT_MS;
235
+ let timedOut = false;
236
+ let sawError = false;
237
+ let foundAnyTable = false;
238
+
239
+ for (const table of ["ItemTable"]) {
240
+ let rows;
241
+ try {
242
+ rows = db.prepare(`SELECT key, value FROM ${table}`).iterate();
243
+ } catch {
244
+ continue;
245
+ }
246
+ foundAnyTable = true;
247
+
248
+ let n = 0;
249
+ try {
250
+ for (const row of rows) {
251
+ const text = valueToText(row.value);
252
+ if (text) { lines.push(text); bytesRead += Buffer.byteLength(text, "utf-8"); }
253
+ n++;
254
+ if (n % YIELD_EVERY_N_ROWS === 0) {
255
+ await new Promise((resolve) => setImmediate(resolve));
256
+ if (Date.now() > deadline) { timedOut = true; break; }
257
+ }
258
+ }
259
+ } catch {
260
+ sawError = true;
261
+ }
262
+ if (timedOut) break;
263
+ }
264
+
265
+ try { db.close(); } catch { /* best-effort close */ }
266
+
267
+ if (!foundAnyTable) return { lines: [], status: "failed", bytesRead: 0 };
268
+ if (sawError && lines.length === 0) return { lines: [], status: "failed", bytesRead };
269
+ if (timedOut || sawError) return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
270
+ return { lines, status: "complete", bytesRead };
271
+ }
272
+
273
+ module.exports = { id, label, available, unavailableReason, files, readLines };