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,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const { createInterface } = require("readline/promises");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* PearAI's local chat session history.
|
|
10
|
+
*
|
|
11
|
+
* VERIFICATION STATUS (read this before trusting anything below): PearAI is
|
|
12
|
+
* an open-source VS Code fork (github.com/trypear/pearai-app) whose AI chat
|
|
13
|
+
* is powered by "pearai-submodule" — itself an open-source fork of
|
|
14
|
+
* Continue (github.com/continuedev/continue), bundled into the app rather
|
|
15
|
+
* than installed as a marketplace extension. This matters because it means
|
|
16
|
+
* PearAI does NOT use Cursor/VS Code's per-profile `state.vscdb` SQLite
|
|
17
|
+
* approach for chat content — it inherited Continue's own file-based session
|
|
18
|
+
* store instead. This was confirmed directly from two independent sources:
|
|
19
|
+
*
|
|
20
|
+
* 1. PearAI's own shipped source, `pearai-submodule/core/util/paths.ts`
|
|
21
|
+
* (fetched from trypear/pearai-submodule@main), which defines:
|
|
22
|
+
* const CONTINUE_GLOBAL_DIR =
|
|
23
|
+
* process.env.CONTINUE_GLOBAL_DIR ?? path.join(os.homedir(), ".pearai");
|
|
24
|
+
* and derives the sessions folder as `<CONTINUE_GLOBAL_DIR>/sessions`,
|
|
25
|
+
* individual session files as `<sessionId>.json`, and an index file at
|
|
26
|
+
* `sessions.json`. Note this path has NO per-OS branching — it is
|
|
27
|
+
* `os.homedir()/.pearai` on every platform, unlike VS Code-derived
|
|
28
|
+
* products (Cursor, Trae, Void) whose Application Support-style path
|
|
29
|
+
* differs per OS. (PearAI still ships a VS Code-derived Application
|
|
30
|
+
* Support/state.vscdb tree too, for ordinary editor/window state, but
|
|
31
|
+
* that is generic VS Code chrome, not where chat content lives — kept
|
|
32
|
+
* out of scope here the same way claude-code.js and cursor.js each stay
|
|
33
|
+
* scoped to where the actual transcript content lives, not every file
|
|
34
|
+
* the host editor happens to write.)
|
|
35
|
+
* 2. `claude-code-history-viewer` (github.com/jhlee0409/claude-code-history-viewer),
|
|
36
|
+
* an actively maintained, independently authored desktop app that reads
|
|
37
|
+
* this exact same layout — its `src-tauri/src/providers/pearai.rs`
|
|
38
|
+
* module doc reads (fetched verbatim): "PearAI is a fork of Continue
|
|
39
|
+
* that rebrands the global directory from ~/.continue to ~/.pearai. The
|
|
40
|
+
* session store format is identical (<sessionId>.json + sessions.json
|
|
41
|
+
* index)."
|
|
42
|
+
*
|
|
43
|
+
* Both sources agree exactly on the directory, the per-file naming, and the
|
|
44
|
+
* index file. What this source has NOT been checked against is a real
|
|
45
|
+
* PearAI install — PearAI is not installed on the machine this was built on
|
|
46
|
+
* (checked: not in /Applications, not in ~/Library/Application Support, no
|
|
47
|
+
* mdfind hits). If you have PearAI installed and have used its chat at
|
|
48
|
+
* least once, the most useful thing you can do is run `residoo scan` and
|
|
49
|
+
* confirm `sourcesScanned`/`filesScanned` for "pearai" look right against
|
|
50
|
+
* what you can see under ~/.pearai/sessions, then report back either way.
|
|
51
|
+
*
|
|
52
|
+
* Session files are plain JSON text on disk (not SQLite), so — like
|
|
53
|
+
* claude-code.js's JSONL files — they can be streamed and pattern-matched
|
|
54
|
+
* line by line with no parsing required: a pretty-printed session file
|
|
55
|
+
* naturally splits into one scannable line per field, and even a minified
|
|
56
|
+
* one degrades gracefully into a single long line, still fully scanned.
|
|
57
|
+
* `sessions.json` (the index) is included too, on the same "never
|
|
58
|
+
* cherry-pick which files might matter" principle the other sources follow.
|
|
59
|
+
*/
|
|
60
|
+
const PEARAI_DIR = path.join(os.homedir(), ".pearai");
|
|
61
|
+
const SESSIONS_DIR = path.join(PEARAI_DIR, "sessions");
|
|
62
|
+
|
|
63
|
+
// Same bounds and same rationale as claude-code.js — no PearAI-specific
|
|
64
|
+
// large-file data point exists (no real install to measure against), so
|
|
65
|
+
// these are carried over unchanged as a generous, conservative backstop.
|
|
66
|
+
const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
|
|
67
|
+
const READ_TIMEOUT_MS = 60_000;
|
|
68
|
+
|
|
69
|
+
function id() { return "pearai"; }
|
|
70
|
+
function label() { return "PearAI"; }
|
|
71
|
+
|
|
72
|
+
function available() {
|
|
73
|
+
try { return fs.statSync(SESSIONS_DIR).isDirectory(); } catch { return false; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Same defensive symlink-following helper as claude-code.js — see that
|
|
78
|
+
* file's docstring for the full reasoning. Duplicated rather than shared,
|
|
79
|
+
* matching this project's "each source is a small, self-contained file"
|
|
80
|
+
* convention (stated explicitly in cursor.js).
|
|
81
|
+
*/
|
|
82
|
+
function isFileFollowingSymlink(fullPath, dirent) {
|
|
83
|
+
if (dirent.isFile()) return true;
|
|
84
|
+
if (!dirent.isSymbolicLink()) return false;
|
|
85
|
+
try { return fs.statSync(fullPath).isFile(); } catch { return false; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Yield { file, mtimeMs, sizeBytes, broken } for every session file found.
|
|
90
|
+
*
|
|
91
|
+
* Unlike claude-code.js's two-level walk (project dir -> transcripts), the
|
|
92
|
+
* confirmed layout here is flat: every `*.json` file directly inside
|
|
93
|
+
* `~/.pearai/sessions` — individual `<sessionId>.json` files plus the
|
|
94
|
+
* `sessions.json` index — so this is a single readdir, not a nested one.
|
|
95
|
+
*/
|
|
96
|
+
function* files() {
|
|
97
|
+
let entries;
|
|
98
|
+
try { entries = fs.readdirSync(SESSIONS_DIR, { withFileTypes: true }); }
|
|
99
|
+
catch { return; }
|
|
100
|
+
|
|
101
|
+
for (const e of entries) {
|
|
102
|
+
if (!e.name.endsWith(".json")) continue;
|
|
103
|
+
const file = path.join(SESSIONS_DIR, e.name);
|
|
104
|
+
if (!e.isFile()) {
|
|
105
|
+
const resolved = isFileFollowingSymlink(file, e);
|
|
106
|
+
if (!resolved) {
|
|
107
|
+
if (e.isSymbolicLink()) yield { file, broken: true };
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let stat;
|
|
112
|
+
try { stat = fs.statSync(file); } catch { yield { file, broken: true }; continue; }
|
|
113
|
+
yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read one session file as an array of raw text lines. Same streamed,
|
|
119
|
+
* bounded, honest-partial-status approach as claude-code.js's readLines() —
|
|
120
|
+
* see that file's docstring for the full reasoning, which applies unchanged
|
|
121
|
+
* here since this is likewise a plain text file on disk.
|
|
122
|
+
*/
|
|
123
|
+
async function readLines(file) {
|
|
124
|
+
let stat;
|
|
125
|
+
try { stat = fs.statSync(file); }
|
|
126
|
+
catch { return { lines: [], status: "failed", bytesRead: 0 }; }
|
|
127
|
+
if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
|
|
128
|
+
|
|
129
|
+
const lines = [];
|
|
130
|
+
let bytesRead = 0;
|
|
131
|
+
const stream = fs.createReadStream(file, { encoding: "utf-8" });
|
|
132
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
133
|
+
|
|
134
|
+
const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
for await (const line of rl) {
|
|
138
|
+
lines.push(line);
|
|
139
|
+
bytesRead += Buffer.byteLength(line, "utf-8") + 1;
|
|
140
|
+
}
|
|
141
|
+
return { lines, status: "complete", bytesRead };
|
|
142
|
+
} catch {
|
|
143
|
+
return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
|
|
144
|
+
} finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
rl.close();
|
|
147
|
+
stream.destroy();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { id, label, available, files, readLines };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const { createInterface } = require("readline/promises");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* "Pi" (earendil-works/pi on GitHub; installed as `@mariozechner/pi-coding-agent`
|
|
10
|
+
* from npm, run as the `pi` CLI) local session transcripts.
|
|
11
|
+
*
|
|
12
|
+
* VERIFICATION STATUS: corroborated directly from the project's own shipped
|
|
13
|
+
* documentation (fetched from the live repo during this source's research),
|
|
14
|
+
* but NOT checked against a real install on the machine this source was
|
|
15
|
+
* built on (no `~/.pi` directory exists there; see CONTRIBUTING.md).
|
|
16
|
+
*
|
|
17
|
+
* `packages/coding-agent/docs/sessions.md` in the `earendil-works/pi` repo —
|
|
18
|
+
* the project's own docs, not a third party's description of it — states
|
|
19
|
+
* plainly: "Sessions auto-save to `~/.pi/agent/sessions/`, organized by
|
|
20
|
+
* working directory. Each session is a JSONL file with a tree structure,"
|
|
21
|
+
* further describing entries with `id`/`parentId` fields (branching), model
|
|
22
|
+
* changes, thinking-level changes, labels, compactions, and branch summaries
|
|
23
|
+
* all living in the same JSONL stream. "Organized by working directory"
|
|
24
|
+
* means sessions live under per-project subdirectories rather than flat in
|
|
25
|
+
* `sessions/` itself — the exact subdirectory naming isn't spelled out in
|
|
26
|
+
* that doc, so this source walks recursively for `*.jsonl` rather than
|
|
27
|
+
* assuming a fixed depth, the same tolerance claude-code.js applies to
|
|
28
|
+
* project-slug directory names it doesn't try to decode either.
|
|
29
|
+
*
|
|
30
|
+
* Independently, jazzyalex/agent-sessions (github.com/jazzyalex/agent-sessions,
|
|
31
|
+
* 800+ stars, a real macOS app built specifically to parse local
|
|
32
|
+
* AI-coding-agent session history) lists Pi among the CLI agents whose local
|
|
33
|
+
* history it reads, corroborating that this is real, currently-scanned-by-
|
|
34
|
+
* someone-else session data rather than a doc describing an unshipped plan.
|
|
35
|
+
*/
|
|
36
|
+
const HOME = os.homedir();
|
|
37
|
+
const ROOT = path.join(HOME, ".pi", "agent", "sessions");
|
|
38
|
+
|
|
39
|
+
const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB — same backstop as claude-code.js.
|
|
40
|
+
const READ_TIMEOUT_MS = 60_000;
|
|
41
|
+
const MAX_WALK_DEPTH = 8;
|
|
42
|
+
|
|
43
|
+
function id() { return "pi-agent"; }
|
|
44
|
+
function label() { return "Pi"; }
|
|
45
|
+
|
|
46
|
+
function available() {
|
|
47
|
+
try { return fs.statSync(ROOT).isDirectory(); } catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Same defensive symlink-following helpers as claude-code.js — see that
|
|
52
|
+
* file's docstring. Duplicated rather than imported, per this project's
|
|
53
|
+
* self-contained-source-file convention (see cursor.js's docstring).
|
|
54
|
+
*/
|
|
55
|
+
function isKindFollowingSymlink(fullPath, dirent, checkFn) {
|
|
56
|
+
if (checkFn(dirent)) return true;
|
|
57
|
+
if (!dirent.isSymbolicLink()) return false;
|
|
58
|
+
try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
|
|
59
|
+
}
|
|
60
|
+
const isDirFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isDirectory());
|
|
61
|
+
const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Recursively yield { file, mtimeMs, sizeBytes, broken } for every plain file
|
|
65
|
+
* under `dir` whose name passes `matchFn`, following symlinks and reporting
|
|
66
|
+
* one that resolves to neither a file nor a directory as `broken: true` — see
|
|
67
|
+
* factory-droid.js's walk() for the identical reasoning (this project
|
|
68
|
+
* duplicates this small helper per source file rather than sharing it; see
|
|
69
|
+
* cursor.js's docstring on why).
|
|
70
|
+
*/
|
|
71
|
+
function* walk(dir, depth, matchFn) {
|
|
72
|
+
if (depth > MAX_WALK_DEPTH) return;
|
|
73
|
+
let entries;
|
|
74
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
75
|
+
catch { return; }
|
|
76
|
+
|
|
77
|
+
for (const e of entries) {
|
|
78
|
+
const full = path.join(dir, e.name);
|
|
79
|
+
if (isDirFollowingSymlink(full, e)) {
|
|
80
|
+
yield* walk(full, depth + 1, matchFn);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const isFile = isFileFollowingSymlink(full, e);
|
|
84
|
+
if (!isFile) {
|
|
85
|
+
if (e.isSymbolicLink()) yield { file: full, broken: true };
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!matchFn(e.name)) continue;
|
|
89
|
+
let stat;
|
|
90
|
+
try { stat = fs.statSync(full); } catch { yield { file: full, broken: true }; continue; }
|
|
91
|
+
yield { file: full, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function* files() {
|
|
96
|
+
yield* walk(ROOT, 0, (name) => name.endsWith(".jsonl"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read one JSONL session as raw text lines. Identical streaming/timeout/
|
|
101
|
+
* partial-read discipline to claude-code.js's readLines().
|
|
102
|
+
*/
|
|
103
|
+
async function readLines(file) {
|
|
104
|
+
let stat;
|
|
105
|
+
try { stat = fs.statSync(file); }
|
|
106
|
+
catch { return { lines: [], status: "failed", bytesRead: 0 }; }
|
|
107
|
+
if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
|
|
108
|
+
|
|
109
|
+
const lines = [];
|
|
110
|
+
let bytesRead = 0;
|
|
111
|
+
const stream = fs.createReadStream(file, { encoding: "utf-8" });
|
|
112
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
113
|
+
const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
for await (const line of rl) {
|
|
117
|
+
lines.push(line);
|
|
118
|
+
bytesRead += Buffer.byteLength(line, "utf-8") + 1;
|
|
119
|
+
}
|
|
120
|
+
return { lines, status: "complete", bytesRead };
|
|
121
|
+
} catch {
|
|
122
|
+
return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
rl.close();
|
|
126
|
+
stream.destroy();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { id, label, available, files, readLines };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const { createInterface } = require("readline/promises");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Qodo Gen (formerly CodiumAI / Codium) — the VS Code and JetBrains AI chat
|
|
10
|
+
* extension published by Qodo.
|
|
11
|
+
*
|
|
12
|
+
* VERIFICATION STATUS (read this before trusting anything below):
|
|
13
|
+
* multi-source-corroborated-but-UNVERIFIED against a real install, and
|
|
14
|
+
* meaningfully WEAKER corroboration than this project's other sources — read
|
|
15
|
+
* this whole note before trusting it. Neither VS Code, JetBrains, nor any
|
|
16
|
+
* Qodo extension is installed on the machine this adapter was built on
|
|
17
|
+
* (checked: no /Applications/*Code*.app, no `code` on PATH, no ~/.qodo
|
|
18
|
+
* directory, no ~/Library/Application Support/JetBrains/<product>/options
|
|
19
|
+
* containing anything Qodo-named). Qodo Gen is closed-source (the public
|
|
20
|
+
* `Codium-ai/codiumai-vscode-release` / `codiumai-jetbrains-release` repos
|
|
21
|
+
* are release-notes/changelog mirrors only, no extension source), so unlike
|
|
22
|
+
* cody.js and amazon-q.js in this project, this adapter's path claim could
|
|
23
|
+
* NOT be checked against the vendor's own source code — only its docs:
|
|
24
|
+
*
|
|
25
|
+
* 1. Qodo's own current documentation,
|
|
26
|
+
* docs.qodo.ai/qodo-documentation/qodo-gen/chat/chat-history (fetched
|
|
27
|
+
* 2026-09-02): "Qodo saves chat history locally in the user's home
|
|
28
|
+
* directory at `.qodo/history`. The file naming uses a hash of the
|
|
29
|
+
* workspace path to ensure uniqueness," plus: history not touched in
|
|
30
|
+
* 90+ days is auto-deleted; VS Code does NOT migrate history across
|
|
31
|
+
* extension upgrades (a fresh install loses it) while JetBrains DOES.
|
|
32
|
+
* 2. Qodo's own changelog, docs.qodo.ai/changelog, entry for "Qodo 1.0.8"
|
|
33
|
+
* (24 Apr 25): "The History file is now named `.qodo/history` and has
|
|
34
|
+
* been moved to the user folder for better file organization. The file
|
|
35
|
+
* is named using a hash of the workspace path to ensure uniqueness" —
|
|
36
|
+
* independently dated/versioned wording that agrees with (1), and
|
|
37
|
+
* implies this is a genuine change from an earlier, different layout
|
|
38
|
+
* (see the IDAHO-VAULT finding below for what that earlier layout
|
|
39
|
+
* likely looked like).
|
|
40
|
+
*
|
|
41
|
+
* Both of the above are the SAME vendor (Qodo's own docs site stating the
|
|
42
|
+
* same fact twice, once in reference docs and once in a changelog) — NOT
|
|
43
|
+
* two independent parties, despite counting as two fetches. Searching
|
|
44
|
+
* specifically for independent, third-party confirmation of the exact
|
|
45
|
+
* `~/.qodo/history` BASE DIRECTORY turned up nothing conclusive: the one
|
|
46
|
+
* real, concrete GitHub artifact found (`LAF-US/IDAHO-VAULT`,
|
|
47
|
+
* `GIT-REMOVAL-COMMANDS.txt`, a real user's own repo, commits from mid/late
|
|
48
|
+
* 2026, well after the v1.0.8 changelog date) shows a file
|
|
49
|
+
* `.qodo/history/<64-hex-hash>.json` committed INSIDE that repo's own
|
|
50
|
+
* project directory, not under that user's home directory — but on
|
|
51
|
+
* inspection this is very likely Qodo's separate automated code-review
|
|
52
|
+
* product (`qodo-ai/command`, the "Qodo Gen CLI" / PR-Agent lineage, whose
|
|
53
|
+
* own README describes exactly this "review your repo from the terminal,
|
|
54
|
+
* for CI/CD" use case) writing a per-repo review-report artifact, given the
|
|
55
|
+
* surrounding context ("Qodo review (...) flagged a medium-severity bug")
|
|
56
|
+
* reads as a CI/PR-review finding, not a chat transcript. That is a
|
|
57
|
+
* DIFFERENT Qodo product from the IDE chat extension this adapter targets,
|
|
58
|
+
* so it neither confirms nor contradicts the `~/.qodo/history` claim above
|
|
59
|
+
* for Qodo Gen specifically — it only corroborates that Qodo's tooling in
|
|
60
|
+
* general uses this exact `.qodo/history/<hash>.json` naming shape
|
|
61
|
+
* somewhere, which is real, but not the specific base-directory claim this
|
|
62
|
+
* adapter depends on.
|
|
63
|
+
*
|
|
64
|
+
* Built anyway, per CONTRIBUTING.md's allowance for a source with credible
|
|
65
|
+
* corroboration but no real install — but flagged here, honestly, as this
|
|
66
|
+
* project's single WEAKEST-verified source: one primary party (the vendor,
|
|
67
|
+
* stated twice) rather than genuinely independent agreement, and a live
|
|
68
|
+
* install is unusually likely to be needed to firm this up (there is no
|
|
69
|
+
* source code to fall back on the way cody.js/amazon-q.js could). If you
|
|
70
|
+
* have Qodo Gen installed, confirming `~/.qodo/history/*.json` actually
|
|
71
|
+
* exists and holds real chat content — or reporting exactly where it
|
|
72
|
+
* actually lives if not — is the single most useful thing you can do for
|
|
73
|
+
* this source (see CONTRIBUTING.md).
|
|
74
|
+
*
|
|
75
|
+
* No per-OS path branching in either Qodo source read above — the docs
|
|
76
|
+
* describe a plain home-directory dotfolder (`~/.qodo` /
|
|
77
|
+
* `%USERPROFILE%\.qodo` on Windows), consistent with it being deliberately
|
|
78
|
+
* IDE-agnostic: the same docs note JetBrains preserves this history across
|
|
79
|
+
* upgrades where VS Code's own extension-local storage would not, which only
|
|
80
|
+
* makes sense if both IDEs read/write ONE shared, non-IDE-specific location
|
|
81
|
+
* — the same reasoning amazon-q.js documents for `~/.aws/amazonq/history`
|
|
82
|
+
* and continue.js already documents for `~/.continue` in this project.
|
|
83
|
+
*/
|
|
84
|
+
function historyDir() {
|
|
85
|
+
return path.join(os.homedir(), ".qodo", "history");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const HISTORY_DIR = historyDir();
|
|
89
|
+
|
|
90
|
+
// Bounds for readLines() — same rationale and values as claude-code.js.
|
|
91
|
+
// Not backed by a real Qodo Gen history file this tool was tested against
|
|
92
|
+
// (no install to test with) — see the verification-status note above.
|
|
93
|
+
const MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2GB
|
|
94
|
+
const READ_TIMEOUT_MS = 60_000;
|
|
95
|
+
|
|
96
|
+
function id() { return "qodo-gen"; }
|
|
97
|
+
function label() { return "Qodo Gen"; }
|
|
98
|
+
|
|
99
|
+
function available() {
|
|
100
|
+
try { return fs.statSync(HISTORY_DIR).isDirectory(); } catch { return false; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Same defensive symlink-following pattern as claude-code.js's
|
|
105
|
+
* isKindFollowingSymlink — see that file's docstring for the full reasoning.
|
|
106
|
+
* Duplicated rather than imported, matching this project's "small,
|
|
107
|
+
* self-contained file" convention.
|
|
108
|
+
*/
|
|
109
|
+
function isKindFollowingSymlink(fullPath, dirent, checkFn) {
|
|
110
|
+
if (checkFn(dirent)) return true;
|
|
111
|
+
if (!dirent.isSymbolicLink()) return false;
|
|
112
|
+
try { return checkFn(fs.statSync(fullPath)); } catch { return false; }
|
|
113
|
+
}
|
|
114
|
+
const isFileFollowingSymlink = (p, d) => isKindFollowingSymlink(p, d, (x) => x.isFile());
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Yield { file, mtimeMs, sizeBytes, broken } for every `*.json` file
|
|
118
|
+
* directly inside `~/.qodo/history/` (flat, not recursive — every source
|
|
119
|
+
* describing this layout, vendor docs and the IDAHO-VAULT artifact alike,
|
|
120
|
+
* shows hash-named files as immediate children, no further nesting).
|
|
121
|
+
*
|
|
122
|
+
* Not filtered to a specific filename shape (e.g. requiring a hex-looking
|
|
123
|
+
* name): the exact hash algorithm/length isn't confirmed (docs say "a hash
|
|
124
|
+
* of the workspace path" without naming one), so — same caution cursor.js
|
|
125
|
+
* documents for not hard-coding a key-name allowlist likely to drift — any
|
|
126
|
+
* `*.json` found directly in this Qodo-owned directory is scanned rather
|
|
127
|
+
* than pattern-matched by filename.
|
|
128
|
+
*/
|
|
129
|
+
function* files() {
|
|
130
|
+
let entries;
|
|
131
|
+
try { entries = fs.readdirSync(HISTORY_DIR, { withFileTypes: true }); }
|
|
132
|
+
catch { return; } // no history directory at all — Qodo Gen never ran, or never opened chat
|
|
133
|
+
|
|
134
|
+
for (const e of entries) {
|
|
135
|
+
if (!e.name.endsWith(".json")) continue;
|
|
136
|
+
const file = path.join(HISTORY_DIR, e.name);
|
|
137
|
+
if (!isFileFollowingSymlink(file, e)) {
|
|
138
|
+
if (e.isSymbolicLink()) yield { file, broken: true };
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
let stat;
|
|
142
|
+
try { stat = fs.statSync(file); } catch { yield { file, broken: true }; continue; }
|
|
143
|
+
yield { file, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, broken: false };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read one `.qodo/history/*.json` file as an array of raw text lines.
|
|
149
|
+
*
|
|
150
|
+
* Streamed line-by-line via readline/promises, same as claude-code.js and
|
|
151
|
+
* amazon-q.js — whether a given file turns out to be one flat JSON document
|
|
152
|
+
* or something line-delimited, per-line scanning handles both: a flat
|
|
153
|
+
* document just becomes one long "line," bounded by MAX_BYTES. Status
|
|
154
|
+
* vocabulary matches every other source in this project.
|
|
155
|
+
*/
|
|
156
|
+
async function readLines(file) {
|
|
157
|
+
let stat;
|
|
158
|
+
try { stat = fs.statSync(file); }
|
|
159
|
+
catch { return { lines: [], status: "failed", bytesRead: 0 }; }
|
|
160
|
+
if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
|
|
161
|
+
|
|
162
|
+
const lines = [];
|
|
163
|
+
let bytesRead = 0;
|
|
164
|
+
const stream = fs.createReadStream(file, { encoding: "utf-8" });
|
|
165
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
166
|
+
|
|
167
|
+
// Same rationale as claude-code.js: no natural timeout exists anywhere in
|
|
168
|
+
// Node's stream/readline stack, and a retargeted symlink can make the
|
|
169
|
+
// underlying open() block forever with no event ever firing.
|
|
170
|
+
const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
for await (const line of rl) {
|
|
174
|
+
lines.push(line);
|
|
175
|
+
bytesRead += Buffer.byteLength(line, "utf-8") + 1; // +1 for the stripped newline
|
|
176
|
+
}
|
|
177
|
+
return { lines, status: "complete", bytesRead };
|
|
178
|
+
} catch {
|
|
179
|
+
// Whatever WAS read before the failure is real content and may contain
|
|
180
|
+
// a real secret — kept, not discarded, same as every other source here.
|
|
181
|
+
return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
|
|
182
|
+
} finally {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
rl.close();
|
|
185
|
+
stream.destroy();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { id, label, available, files, readLines };
|