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,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 };
|