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,374 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const zlib = require("zlib");
7
+
8
+ /**
9
+ * Zed's local AI agent (chat/thread) history.
10
+ *
11
+ * VERIFICATION STATUS: multi-source-corroborated-but-unverified. Zed is not
12
+ * installed on the machine this source was built on — checked directly:
13
+ * no /Applications/*.app matching Zed, no `dev.zed.Zed` bundle id via
14
+ * `mdfind`, no ~/Library/Application Support/Zed, no ~/.config/zed, no
15
+ * ~/.local/share/zed, no Homebrew cask. So none of this could be checked
16
+ * against a real, on-disk session — only against research. What that
17
+ * research consists of:
18
+ *
19
+ * 1. Zed's own current (`main` branch, fetched live off GitHub while
20
+ * building this source) open-source implementation:
21
+ * - crates/agent/src/db.rs — ThreadsDatabase::new() builds the exact
22
+ * path, the `CREATE TABLE`/`ALTER TABLE` statements below, and
23
+ * deserialize_thread()'s data_type dispatch (see SCHEMA below).
24
+ * - crates/paths/src/paths.rs — data_dir()'s per-OS logic (see
25
+ * PATHS below).
26
+ * This is a primary source, not a description of one — the literal
27
+ * code that will still be writing these files after this source
28
+ * ships, current as of the "main" branch fetched 2026-09-02.
29
+ * 2. A real user's own inspection, reported in a Zed GitHub discussion
30
+ * ("Where does Zed store Agent Conversation History?",
31
+ * github.com/zed-industries/zed/discussions/32335): confirms a real
32
+ * Linux install's conversation history sits under
33
+ * ~/.local/share/zed/threads/ as "a database... in binary format",
34
+ * and separately reports a Flatpak install's data root as
35
+ * ~/.var/app/dev.zed.Zed/data/zed/ instead of ~/.local/share/zed/.
36
+ * 3. zed-chat-export (lib.rs / crates.io), a maintained third-party Rust
37
+ * CLI that reads this same data for a living — its own docs describe,
38
+ * in one sentence, reading "Zed's internal SQLite schema" where
39
+ * conversations are stored "with Zstd-compressed message bodies,"
40
+ * matching source 1 exactly (SQLite, Zstd), and separately warn that
41
+ * schema is undocumented and can change between Zed releases — the
42
+ * same caution this file gives.
43
+ *
44
+ * One discrepancy worth stating plainly: source 2 described the Flatpak
45
+ * path with a "threads-db.1.mdb" (LMDB-shaped) name rather than a flat
46
+ * "threads.db" SQLite file. That is very likely a stale observation from an
47
+ * earlier Zed release — source 1, read directly off the current main
48
+ * branch, is unambiguous that thread persistence today is `sqlez` (a SQLite
49
+ * wrapper) writing one `threads.db` file, and nothing in Zed's current
50
+ * source constructs an ".mdb" path anywhere. Source 1 wins; the Flatpak
51
+ * candidate below still uses today's SQLite filename, not the older name.
52
+ *
53
+ * If you have Zed installed, the most useful thing you can do is run
54
+ * `residoo scan` and confirm `sourcesScanned`/`filesScanned` look right for
55
+ * what you know is actually on disk (a threads.db under one of the PATHS
56
+ * below), then report back either way — see CONTRIBUTING.md.
57
+ *
58
+ * SCHEMA (from source 1, crates/agent/src/db.rs — ThreadsDatabase::new() and
59
+ * deserialize_thread()):
60
+ * Single table `threads`, one row per conversation thread:
61
+ * id TEXT PRIMARY KEY, summary TEXT NOT NULL, updated_at TEXT NOT NULL,
62
+ * data_type TEXT NOT NULL, data BLOB NOT NULL
63
+ * (+ later ALTER-added columns — parent_id, folder_paths,
64
+ * folder_paths_order, created_at — irrelevant to scanning, not read here.)
65
+ * `summary` is the thread's title, plain text. `data` is the actual
66
+ * conversation payload (title + full message history, as JSON), encoded
67
+ * per `data_type`:
68
+ * - "zstd" (current default, per save_thread_sync()): Zstd-compressed
69
+ * UTF-8 JSON.
70
+ * - "json" (older/legacy rows, still readable by current Zed): raw
71
+ * UTF-8 JSON, uncompressed.
72
+ * deserialize_thread() treats any other data_type value as a hard error —
73
+ * Zed itself has never written a third kind, so this source doesn't guess
74
+ * at one either (see decodeThreadData() below).
75
+ *
76
+ * PATHS (from source 1's paths::data_dir(), joined with "threads/threads.db"
77
+ * by db.rs — APP_NAME is "Zed", APP_NAME_LOWERCASE is "zed"):
78
+ * macOS: ~/Library/Application Support/Zed/threads/threads.db
79
+ * Linux/FreeBSD: $XDG_DATA_HOME/zed/threads/threads.db
80
+ * (default ~/.local/share/zed/threads/threads.db)
81
+ * Windows: %LOCALAPPDATA%\Zed\threads\threads.db
82
+ * Flatpak (Linux only, checked in addition to the path above):
83
+ * ~/.var/app/dev.zed.Zed/data/zed/threads/threads.db
84
+ *
85
+ * Zstd decompression uses node:zlib's built-in zstd support
86
+ * (zlib.zstdDecompressSync) — a Node core API since v22.15.0 (still marked
87
+ * "Experimental" by Node's own docs as of this writing), not a new runtime
88
+ * dependency. Verified directly against this project's own Node: a
89
+ * synthetic threads.db was built (real node:sqlite writes, real
90
+ * zlib.zstdCompressSync-compressed rows, both "zstd" and "json" data_type
91
+ * rows) and read back through the exact logic below during development;
92
+ * that confirms the mechanics of this file, not a real Zed install — see
93
+ * VERIFICATION STATUS above for what remains unverified. Both node:sqlite
94
+ * and zlib's zstd support are feature-detected lazily/cheaply, same
95
+ * reasoning as cursor.js: don't require() node:sqlite (and risk its
96
+ * ExperimentalWarning) until Zed's own data directory is confirmed to exist
97
+ * on this machine.
98
+ */
99
+ function zedDataDirs() {
100
+ const home = os.homedir();
101
+ if (process.platform === "darwin") {
102
+ return [path.join(home, "Library", "Application Support", "Zed")];
103
+ }
104
+ if (process.platform === "win32") {
105
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
106
+ return [path.join(localAppData, "Zed")];
107
+ }
108
+ // Linux, FreeBSD, and other XDG-following unix platforms.
109
+ const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
110
+ return [
111
+ path.join(xdgDataHome, "zed"),
112
+ // Flatpak installs use a separate per-app data root instead of
113
+ // $XDG_DATA_HOME — see the module docstring for why this still targets
114
+ // today's SQLite filename rather than the older ".mdb" name some
115
+ // reports mention.
116
+ path.join(home, ".var", "app", "dev.zed.Zed", "data", "zed"),
117
+ ];
118
+ }
119
+
120
+ const ZED_DATA_DIRS = zedDataDirs();
121
+ const THREADS_DB_PATHS = ZED_DATA_DIRS.map((dir) => path.join(dir, "threads", "threads.db"));
122
+
123
+ // Bounds for readLines(), mirroring claude-code.js/cursor.js.
124
+ const MAX_DB_BYTES = 512 * 1024 * 1024; // generous backstop against a
125
+ // corrupted/pathological file — no
126
+ // real threads.db has been observed
127
+ // to size this against, same
128
+ // caveat cursor.js's MAX_DB_BYTES
129
+ // states for itself (no real
130
+ // install here to test against).
131
+ const READ_TIMEOUT_MS = 60_000;
132
+ const BUSY_TIMEOUT_MS = 5_000; // bound how long a read waits on a lock Zed itself may be holding
133
+ const YIELD_EVERY_N_ROWS = 500; // see readLines()'s docstring for why this exists
134
+
135
+ function id() { return "zed"; }
136
+ function label() { return "Zed"; }
137
+
138
+ function zedDataDirExists() {
139
+ return ZED_DATA_DIRS.some((dir) => {
140
+ try { return fs.statSync(dir).isDirectory(); } catch { return false; }
141
+ });
142
+ }
143
+
144
+ /**
145
+ * node:sqlite is a Node CORE module — see cursor.js's own getDatabaseSync()
146
+ * docstring for the full reasoning this mirrors exactly, including why the
147
+ * require() is deferred rather than done at module load time (an eager
148
+ * top-level require would print Node's ExperimentalWarning on every
149
+ * `residoo scan`, for every user, even the large majority who have never
150
+ * touched Zed).
151
+ */
152
+ let sqliteRequireAttempted = false;
153
+ let DatabaseSync = null;
154
+ function getDatabaseSync() {
155
+ if (!sqliteRequireAttempted) {
156
+ sqliteRequireAttempted = true;
157
+ try { ({ DatabaseSync } = require("node:sqlite")); }
158
+ catch { DatabaseSync = null; }
159
+ }
160
+ return DatabaseSync;
161
+ }
162
+
163
+ /**
164
+ * zlib.zstdDecompressSync — unlike node:sqlite, requiring the `zlib` module
165
+ * itself is not experimental and prints no warning (it's been a stable Node
166
+ * core module for years); only these specific zstd methods on it are new
167
+ * enough to be version-gated, so a plain feature-detect is all this needs
168
+ * (no lazy-require ceremony to avoid a warning, since there isn't one).
169
+ */
170
+ function getZstdDecompressSync() {
171
+ return typeof zlib.zstdDecompressSync === "function" ? zlib.zstdDecompressSync : null;
172
+ }
173
+
174
+ const NODE_SQLITE_REQUIREMENT = "needs Node.js 22.5+ (node:sqlite not present in this runtime)";
175
+ const NODE_ZSTD_REQUIREMENT = "needs Node.js 22.15+ (zlib.zstdDecompressSync not present in this runtime)";
176
+
177
+ function available() {
178
+ // Cheap fs check first, on purpose — same short-circuit reasoning as
179
+ // cursor.js's available(): the common case is Zed simply isn't installed,
180
+ // and answering "not available" for that reason alone must not cost
181
+ // requiring node:sqlite or evaluating zstd support.
182
+ return zedDataDirExists() && Boolean(getDatabaseSync()) && Boolean(getZstdDecompressSync());
183
+ }
184
+
185
+ /**
186
+ * Same optional, additive export cursor.js defines — see its own
187
+ * unavailableReason() docstring for the full contract (only cli.js's "why is
188
+ * a source missing" messaging calls this; scan.js/index.js never do).
189
+ *
190
+ * Returns a reason string only in the cases worth calling out specifically —
191
+ * Zed IS installed (its data directory is really there) but this Node
192
+ * runtime is missing node:sqlite and/or zlib's zstd support, so the source
193
+ * silently vanishing from "Sources checked" would read as "Zed isn't
194
+ * installed," which is false. Returns null for the ordinary "Zed just isn't
195
+ * here" case, and once available() is already true.
196
+ */
197
+ function unavailableReason() {
198
+ if (!zedDataDirExists()) return null;
199
+ const missing = [];
200
+ if (!getDatabaseSync()) missing.push(NODE_SQLITE_REQUIREMENT);
201
+ if (!getZstdDecompressSync()) missing.push(NODE_ZSTD_REQUIREMENT);
202
+ if (missing.length === 0) return null;
203
+ return `Zed detected but not scanned — ${missing.join("; ")}`;
204
+ }
205
+
206
+ /**
207
+ * Same defensive symlink-following pattern as cursor.js's statIfPresent —
208
+ * see that file's docstring for the full reasoning. Duplicated rather than
209
+ * imported: each source in this project is meant to be a small,
210
+ * self-contained file a reviewer can audit on its own — see CONTRIBUTING.md.
211
+ *
212
+ * A candidate path that simply does not exist yields nothing — normal for
213
+ * every candidate but whichever one matches this machine's actual OS/install
214
+ * (and normal even for that one, if Zed is installed but its agent panel has
215
+ * never been used). "broken" is reserved for a path that looked like it
216
+ * should resolve to a real file and didn't — chiefly a dangling symlink.
217
+ */
218
+ function* statIfPresent(dbPath) {
219
+ let lst;
220
+ try { lst = fs.lstatSync(dbPath); }
221
+ catch { return; }
222
+
223
+ if (lst.isSymbolicLink()) {
224
+ try {
225
+ const st = fs.statSync(dbPath); // follow the link
226
+ if (!st.isFile()) { yield { file: dbPath, broken: true }; return; }
227
+ yield { file: dbPath, mtimeMs: st.mtimeMs, sizeBytes: st.size, broken: false };
228
+ } catch {
229
+ yield { file: dbPath, broken: true }; // dangling symlink
230
+ }
231
+ return;
232
+ }
233
+
234
+ if (!lst.isFile()) return; // something unexpected sits at this path — out of scope, not broken
235
+ yield { file: dbPath, mtimeMs: lst.mtimeMs, sizeBytes: lst.size, broken: false };
236
+ }
237
+
238
+ /**
239
+ * Yield { file, mtimeMs, sizeBytes, broken } for every threads.db candidate
240
+ * path (see PATHS in the module docstring). Purely a filesystem walk + stat,
241
+ * same division of labour as cursor.js's files(): never opens the database,
242
+ * so it works even in a Node runtime where node:sqlite/zstd aren't available
243
+ * — only readLines() actually needs those.
244
+ */
245
+ function* files() {
246
+ for (const dbPath of THREADS_DB_PATHS) yield* statIfPresent(dbPath);
247
+ }
248
+
249
+ /**
250
+ * Decode one `threads` row's `data` BLOB into UTF-8 text, per its
251
+ * `data_type` column — mirrors deserialize_thread() in Zed's own db.rs
252
+ * exactly (see SCHEMA in the module docstring). Returns null when the value
253
+ * can't be turned into text — a corrupt/truncated blob, or a "zstd" row
254
+ * encountered without zstd support available (checked by available() before
255
+ * this source is ever scanned in the real path, but readLines() itself
256
+ * doesn't re-check its own available(), matching claude-code.js/cursor.js).
257
+ * A null here means nothing was ever successfully decoded, not that real
258
+ * content got discarded after being read — the distinction CONTRIBUTING.md's
259
+ * rule 5 (honest partial-read handling) cares about.
260
+ */
261
+ function decodeThreadData(dataType, data, zstdDecompressSync) {
262
+ if (!(data instanceof Uint8Array) || data.length === 0) return null;
263
+ const buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
264
+ try {
265
+ if (dataType === "zstd") {
266
+ if (!zstdDecompressSync) return null;
267
+ return zstdDecompressSync(buf).toString("utf-8");
268
+ }
269
+ // "json" (legacy, uncompressed) and any future/unrecognized data_type:
270
+ // best-effort treat the bytes as UTF-8 text directly — the same
271
+ // tolerant fallback cursor.js's valueToText() uses for a storage class
272
+ // it doesn't specifically recognize, and the only sane thing to do
273
+ // given Zed's own code has, to date, never written a third data_type.
274
+ return buf.toString("utf-8");
275
+ } catch {
276
+ return null; // corrupt/truncated blob — nothing decodable here
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Read one threads.db as an array of raw text "lines" — one per thread's
282
+ * `summary` (its title) plus one per thread's decoded `data` payload (the
283
+ * full message history as JSON). Returns { lines, status, bytesRead } with
284
+ * the same status vocabulary as claude-code.js/cursor.js: "complete",
285
+ * "partial", "too-large", "failed".
286
+ *
287
+ * Row-by-row iteration + periodic event-loop yield/deadline-check, for
288
+ * exactly the reason cursor.js's readLines() gives: node:sqlite is fully
289
+ * synchronous, so there is no event/AbortSignal to hook a real preemptive
290
+ * timeout onto once a native call has started — this bounds "too many rows
291
+ * taking too long," not a single pathological row's own decode time (same
292
+ * named, not silent, asymmetry cursor.js and claude-code.js each admit for
293
+ * their own analogous gaps).
294
+ */
295
+ async function readLines(file) {
296
+ // Unconditional — not gated on zedDataDirExists()/available() here,
297
+ // matching claude-code.js's and cursor.js's readLines(), which likewise
298
+ // never re-check their own available() before trying to read a file.
299
+ const DB = getDatabaseSync();
300
+ if (!DB) return { lines: [], status: "failed", bytesRead: 0 };
301
+
302
+ let stat;
303
+ try { stat = fs.statSync(file); }
304
+ catch { return { lines: [], status: "failed", bytesRead: 0 }; }
305
+ if (stat.size > MAX_DB_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
306
+
307
+ let db;
308
+ try {
309
+ db = new DB(file, { readOnly: true });
310
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
311
+ } catch {
312
+ // Covers: the file was deleted between files() and this call, a
313
+ // corrupt/non-SQLite file sitting at this path, or Zed holding a lock
314
+ // this readonly open can't get past even within BUSY_TIMEOUT_MS — all
315
+ // genuinely "could not read this," not "read it, found nothing."
316
+ return { lines: [], status: "failed", bytesRead: 0 };
317
+ }
318
+
319
+ let rows;
320
+ try {
321
+ rows = db.prepare("SELECT summary, data_type, data FROM threads").iterate();
322
+ } catch {
323
+ // Opened fine as SQLite but has no `threads` table matching the schema
324
+ // this source understands — wrong file, or a schema that has since
325
+ // moved on (Zed's own maintainers call this schema undocumented and
326
+ // subject to change — see the module docstring). A real "could not
327
+ // extract anything," not "extracted zero real rows."
328
+ try { db.close(); } catch { /* best-effort close */ }
329
+ return { lines: [], status: "failed", bytesRead: 0 };
330
+ }
331
+
332
+ const zstdDecompressSync = getZstdDecompressSync();
333
+ const lines = [];
334
+ let bytesRead = 0;
335
+ const deadline = Date.now() + READ_TIMEOUT_MS;
336
+ let timedOut = false;
337
+ let sawError = false;
338
+
339
+ try {
340
+ let n = 0;
341
+ for (const row of rows) {
342
+ if (typeof row.summary === "string" && row.summary.length > 0) {
343
+ lines.push(row.summary);
344
+ bytesRead += Buffer.byteLength(row.summary, "utf-8");
345
+ }
346
+
347
+ const text = decodeThreadData(row.data_type, row.data, zstdDecompressSync);
348
+ if (text) {
349
+ lines.push(text);
350
+ bytesRead += Buffer.byteLength(text, "utf-8");
351
+ }
352
+
353
+ n++;
354
+ if (n % YIELD_EVERY_N_ROWS === 0) {
355
+ await new Promise((resolve) => setImmediate(resolve));
356
+ if (Date.now() > deadline) { timedOut = true; break; }
357
+ }
358
+ }
359
+ } catch {
360
+ // A row iterator can itself throw partway (e.g. a corrupted page hit
361
+ // mid-scan) — whatever WAS read before that is real content, kept the
362
+ // same way claude-code.js/cursor.js keep a partial read rather than
363
+ // discarding it.
364
+ sawError = true;
365
+ }
366
+
367
+ try { db.close(); } catch { /* best-effort close; nothing left to do if this fails */ }
368
+
369
+ if (sawError && lines.length === 0) return { lines: [], status: "failed", bytesRead };
370
+ if (timedOut || sawError) return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
371
+ return { lines, status: "complete", bytesRead };
372
+ }
373
+
374
+ module.exports = { id, label, available, unavailableReason, files, readLines };