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