residoo 0.20.0 → 0.21.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 +5 -4
- package/package.json +1 -1
- package/src/sources/index.js +8 -0
- package/src/sources/shell-history.js +241 -0
package/README.md
CHANGED
|
@@ -251,10 +251,11 @@ prompt. There is no recovery if you lose it, so pick one you keep.
|
|
|
251
251
|
|
|
252
252
|
## Sources supported today
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
multi-source-corroborated for the rest
|
|
256
|
-
Gemini CLI, Copilot, and 30+ more).
|
|
257
|
-
and how to add one:
|
|
254
|
+
45 sources, real-install-verified for Claude Code, its config family, and
|
|
255
|
+
bash/Python-REPL shell history, multi-source-corroborated for the rest
|
|
256
|
+
(Cursor, Codex CLI, Cline, Windsurf, Gemini CLI, Copilot, and 30+ more).
|
|
257
|
+
Full list, what "corroborated" means, and how to add one:
|
|
258
|
+
[docs/sources.md](docs/sources.md).
|
|
258
259
|
|
|
259
260
|
## License
|
|
260
261
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Find secrets leaking through your AI coding agent's session history. Zero network calls in the scan path, zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "CloudRoam (https://cloudroam.io)",
|
package/src/sources/index.js
CHANGED
|
@@ -140,6 +140,13 @@ const atlassianRovoDev = require("./atlassian-rovo-dev");
|
|
|
140
140
|
// cache/offline copy of thread content is documented anywhere found — the
|
|
141
141
|
// same cloud-only reasoning as Augment Code/CodeGPT above, not missed.
|
|
142
142
|
|
|
143
|
+
// Not a transcript store either — interactive shell/REPL history (bash,
|
|
144
|
+
// zsh, fish, psql, mysql, Python, Node.js). See shell-history.js's own
|
|
145
|
+
// header for why this is in scope despite not being literally "an AI
|
|
146
|
+
// agent's session history," and for exactly which paths are
|
|
147
|
+
// real-install-verified versus documented-but-unverified on this machine.
|
|
148
|
+
const shellHistory = require("./shell-history");
|
|
149
|
+
|
|
143
150
|
const ALL_SOURCES = [
|
|
144
151
|
claudeCode,
|
|
145
152
|
agentConfigs,
|
|
@@ -185,6 +192,7 @@ const ALL_SOURCES = [
|
|
|
185
192
|
kimiCode,
|
|
186
193
|
fx,
|
|
187
194
|
atlassianRovoDev,
|
|
195
|
+
shellHistory,
|
|
188
196
|
];
|
|
189
197
|
|
|
190
198
|
function availableSources() {
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const { createInterface } = require("readline/promises");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interactive shell and REPL/DB-client history files.
|
|
10
|
+
*
|
|
11
|
+
* SCOPE, stated plainly because this is the second source in this project
|
|
12
|
+
* (after agent-configs.js) that is not literally "an AI agent's session
|
|
13
|
+
* history": these files record what the DEVELOPER typed at a real
|
|
14
|
+
* interactive prompt, not what an agent wrote to disk on their behalf. It's
|
|
15
|
+
* included because it's the same failure mode, one hop away from what this
|
|
16
|
+
* tool already covers: a developer tests a curl call with a bearer token
|
|
17
|
+
* before pasting the working version into an agent prompt, connects to a
|
|
18
|
+
* database with a password embedded in the URI, or exports a token into a
|
|
19
|
+
* REPL to try a client library — plaintext, indefinitely, in a file almost
|
|
20
|
+
* nobody thinks to check, the exact description this project's own README
|
|
21
|
+
* opens with. It is also a real, disclosed, competitor-named gap: Medusa
|
|
22
|
+
* (see docs/comparison.md's Medusa section) already scans exactly
|
|
23
|
+
* bash/zsh/fish/psql/mysql/python-REPL history and residoo did not.
|
|
24
|
+
*
|
|
25
|
+
* Every path below is a documented default or a documented override
|
|
26
|
+
* environment variable from that tool's own primary docs, fetched directly
|
|
27
|
+
* (not assumed by analogy to a similar tool) on 2026-09-05:
|
|
28
|
+
*
|
|
29
|
+
* - **bash**: `~/.bash_history` is bash's own long-standing built-in
|
|
30
|
+
* default (its manual page).
|
|
31
|
+
* - **zsh**: NOT a shell-level default the way bash's is — zsh's own
|
|
32
|
+
* manual (zsh.sourceforge.io/Doc/Release/Parameters.html) states
|
|
33
|
+
* plainly that if `HISTFILE` is unset, "the history is not saved" at
|
|
34
|
+
* all. `~/.zsh_history` is checked anyway because it's the exact path
|
|
35
|
+
* zsh's own bundled `zsh-newuser-install` script offers a new user who
|
|
36
|
+
* accepts history saving, and the default both Oh My Zsh's and
|
|
37
|
+
* Prezto's stock templates set — the de facto convention on most real
|
|
38
|
+
* machines, not a shell-level guarantee. `$HISTFILE`, when set, is
|
|
39
|
+
* checked once for both bash and zsh: a set value can't be attributed
|
|
40
|
+
* to one shell over the other from outside the shell itself.
|
|
41
|
+
* - **fish**: `$XDG_DATA_HOME/fish/fish_history`, defaulting to
|
|
42
|
+
* `~/.local/share/fish/fish_history` when that variable is unset —
|
|
43
|
+
* fish's own docs (fishshell.com/docs/current/interactive.html) state
|
|
44
|
+
* this exact default and XDG override.
|
|
45
|
+
* - **psql**: `~/.psql_history` (Unix) or
|
|
46
|
+
* `%APPDATA%\postgresql\psql_history` (Windows) — PostgreSQL's own
|
|
47
|
+
* psql docs (postgresql.org/docs/current/app-psql.html) state both
|
|
48
|
+
* paths as the default. No environment-variable override is
|
|
49
|
+
* documented; psql's own `\set HISTFILE ...` is an in-session psql
|
|
50
|
+
* variable, not a process environment variable residoo can read from
|
|
51
|
+
* outside the running psql process.
|
|
52
|
+
* - **mysql**: `$MYSQL_HISTFILE`, defaulting to `~/.mysql_history` —
|
|
53
|
+
* MySQL's own reference manual (dev.mysql.com/doc/refman/8.4/en/
|
|
54
|
+
* mysql-logging.html) documents both, and its own text recommends
|
|
55
|
+
* restricting this file's permissions because it "may contain
|
|
56
|
+
* sensitive information" — a vendor admission of exactly the failure
|
|
57
|
+
* mode this source exists to catch.
|
|
58
|
+
* - **Python** (interactive interpreter): `$PYTHON_HISTORY`, defaulting
|
|
59
|
+
* to `~/.python_history` — Python's own docs (docs.python.org/3/using/
|
|
60
|
+
* cmdline.html). The environment variable is Python 3.13+ only (added
|
|
61
|
+
* that release); reading it on an older interpreter simply finds it
|
|
62
|
+
* unset and falls through to the same fixed default, so no version
|
|
63
|
+
* check is needed here.
|
|
64
|
+
* - **Node.js REPL**: `$NODE_REPL_HISTORY`, defaulting to
|
|
65
|
+
* `~/.node_repl_history` — Node's own docs (nodejs.org/api/repl.html),
|
|
66
|
+
* which also document that an empty or whitespace-only value means the
|
|
67
|
+
* user explicitly disabled persistent history; honored here exactly as
|
|
68
|
+
* documented rather than treated as "unset."
|
|
69
|
+
*
|
|
70
|
+
* VERIFICATION STATUS: `~/.bash_history` and `~/.python_history` are
|
|
71
|
+
* REAL-INSTALL-VERIFIED — both exist on this project's own build machine
|
|
72
|
+
* with genuine, non-empty content (confirmed directly, read-only, before
|
|
73
|
+
* this source was written). zsh/fish/psql/mysql/Node history are
|
|
74
|
+
* MULTI-SOURCE-CORROBORATED-BUT-UNVERIFIED: each path above comes from
|
|
75
|
+
* that tool's own primary documentation, but none of those five files
|
|
76
|
+
* exist on the machine this was built on, so the schema (there isn't
|
|
77
|
+
* one — every one of these is already plain line-delimited text) is
|
|
78
|
+
* unverified against real content the same way most of this project's
|
|
79
|
+
* other sources are. If you use zsh, fish, psql, mysql, or the Node REPL
|
|
80
|
+
* with a populated history file, running `residoo scan` and confirming
|
|
81
|
+
* `filesScanned` looks right is the single most useful way to firm this
|
|
82
|
+
* up (see CONTRIBUTING.md).
|
|
83
|
+
*
|
|
84
|
+
* FORMAT: every one of these files is already plain line-delimited text —
|
|
85
|
+
* zsh's optional "extended history" format (`: <ts>:<secs>;<command>`) and
|
|
86
|
+
* fish's YAML-ish `- cmd: ...` / ` when: ...` records still carry the
|
|
87
|
+
* actual command as a contiguous substring of one line, and every pattern
|
|
88
|
+
* in `src/patterns.js` matches on `\b` word boundaries, never a `^`
|
|
89
|
+
* line-start anchor — so no format-specific parsing is needed before
|
|
90
|
+
* pattern matching, the same reasoning agent-configs.js's readLines()
|
|
91
|
+
* docstring states for JSON/TOML config lines, and decode.js's own header
|
|
92
|
+
* already anticipates this exact case ("plain-text chat logs, shell
|
|
93
|
+
* history... used as-is").
|
|
94
|
+
*
|
|
95
|
+
* NOT covered, and why: shell history for any shell/tool not named above
|
|
96
|
+
* (fish's own `fish_history` predecessor formats, csh/tcsh, sqlite3's
|
|
97
|
+
* `.sqlite_history`, R's `.Rhistory`, IPython's separate SQLite-backed
|
|
98
|
+
* history database) — each would need its own documented default and
|
|
99
|
+
* schema check to the same bar as the seven above, not guessed by
|
|
100
|
+
* analogy. A welcome follow-up PR, per CONTRIBUTING.md.
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
const HOME = os.homedir();
|
|
104
|
+
|
|
105
|
+
function id() { return "shell-history"; }
|
|
106
|
+
function label() { return "Shell & REPL history"; }
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Every candidate path this source checks, deduplicated (a customized
|
|
110
|
+
* override that happens to equal a default above would otherwise be
|
|
111
|
+
* checked twice). Order has no behavioral meaning — statIfPresent handles
|
|
112
|
+
* each independently.
|
|
113
|
+
*/
|
|
114
|
+
function candidatePaths() {
|
|
115
|
+
const xdgDataHome = process.env.XDG_DATA_HOME || path.join(HOME, ".local", "share");
|
|
116
|
+
const nodeReplHistory = process.env.NODE_REPL_HISTORY;
|
|
117
|
+
const nodeReplDisabled = nodeReplHistory !== undefined && nodeReplHistory.trim() === "";
|
|
118
|
+
|
|
119
|
+
const paths = [
|
|
120
|
+
path.join(HOME, ".bash_history"),
|
|
121
|
+
path.join(HOME, ".zsh_history"),
|
|
122
|
+
...(process.env.HISTFILE ? [process.env.HISTFILE] : []),
|
|
123
|
+
path.join(xdgDataHome, "fish", "fish_history"),
|
|
124
|
+
process.platform === "win32"
|
|
125
|
+
? path.join(process.env.APPDATA || path.join(HOME, "AppData", "Roaming"), "postgresql", "psql_history")
|
|
126
|
+
: path.join(HOME, ".psql_history"),
|
|
127
|
+
process.env.MYSQL_HISTFILE || path.join(HOME, ".mysql_history"),
|
|
128
|
+
process.env.PYTHON_HISTORY || path.join(HOME, ".python_history"),
|
|
129
|
+
...(nodeReplDisabled ? [] : [nodeReplHistory || path.join(HOME, ".node_repl_history")]),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
return [...new Set(paths)];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve one fixed candidate path into zero or one files() entries.
|
|
137
|
+
* Duplicated from agent-configs.js's statIfPresent rather than imported,
|
|
138
|
+
* per this project's one-small-self-contained-file-per-source convention
|
|
139
|
+
* (see cursor.js's own docstring for the same point). Absence (ENOENT/
|
|
140
|
+
* ENOTDIR) is the normal, expected case for a tool the user doesn't use
|
|
141
|
+
* and yields nothing; anything else that stops the path resolving (a
|
|
142
|
+
* dangling symlink, a permission error) is a broken entry, not silent
|
|
143
|
+
* absence — the same never-a-false-all-clear reasoning as every other
|
|
144
|
+
* source here.
|
|
145
|
+
*/
|
|
146
|
+
function* statIfPresent(p) {
|
|
147
|
+
let lst;
|
|
148
|
+
try { lst = fs.lstatSync(p); }
|
|
149
|
+
catch (err) {
|
|
150
|
+
if (err && (err.code === "ENOENT" || err.code === "ENOTDIR")) return;
|
|
151
|
+
yield { file: p, broken: true };
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (lst.isSymbolicLink()) {
|
|
156
|
+
try {
|
|
157
|
+
const st = fs.statSync(p);
|
|
158
|
+
if (!st.isFile()) { yield { file: p, broken: true }; return; }
|
|
159
|
+
yield { file: p, mtimeMs: st.mtimeMs, sizeBytes: st.size, broken: false };
|
|
160
|
+
} catch {
|
|
161
|
+
yield { file: p, broken: true };
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!lst.isFile()) return;
|
|
167
|
+
yield { file: p, mtimeMs: lst.mtimeMs, sizeBytes: lst.size, broken: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* True when any candidate history file actually exists. Unlike
|
|
172
|
+
* agent-configs.js (which gates on a per-tool ROOT directory so an
|
|
173
|
+
* installed-but-empty tool still shows as checked), none of the files
|
|
174
|
+
* here have a natural "installed" signal separate from the file's own
|
|
175
|
+
* existence — there is no `~/.bash/` directory to check instead. A
|
|
176
|
+
* machine with none of these files present correctly doesn't list this
|
|
177
|
+
* source at all, the same as a brand-new machine with no shell history
|
|
178
|
+
* yet would have nothing meaningful to report either way.
|
|
179
|
+
*/
|
|
180
|
+
function available() {
|
|
181
|
+
for (const p of candidatePaths()) {
|
|
182
|
+
for (const _ of statIfPresent(p)) return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Yield { file, mtimeMs, sizeBytes, broken } for every candidate present.
|
|
189
|
+
*/
|
|
190
|
+
function* files() {
|
|
191
|
+
for (const p of candidatePaths()) yield* statIfPresent(p);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Real observations on this project's own build machine: ~40KB
|
|
195
|
+
// (~.bash_history, years of use) and ~6.6KB (~.python_history). 256MB is a
|
|
196
|
+
// generous, uncalibrated backstop against a corrupted or pathological file
|
|
197
|
+
// (the same caveat cursor.js states for its own size bound), not a measured
|
|
198
|
+
// ceiling — a file over it is surfaced as "too-large", never silently
|
|
199
|
+
// skipped.
|
|
200
|
+
const MAX_BYTES = 256 * 1024 * 1024;
|
|
201
|
+
const READ_TIMEOUT_MS = 60_000;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Read one history file as an array of raw text lines. Identical streaming
|
|
205
|
+
* shape (readline/promises, MAX_BYTES cap, READ_TIMEOUT_MS watchdog,
|
|
206
|
+
* partial-read lines kept rather than discarded) to every other source
|
|
207
|
+
* here — see claude-code.js's readLines() docstring for the full
|
|
208
|
+
* reasoning, all of which applies unchanged since this is plain
|
|
209
|
+
* line-delimited UTF-8 text on disk (see the module docstring's FORMAT
|
|
210
|
+
* section for why no per-tool parsing is needed first).
|
|
211
|
+
*/
|
|
212
|
+
async function readLines(file) {
|
|
213
|
+
let stat;
|
|
214
|
+
try { stat = fs.statSync(file); }
|
|
215
|
+
catch { return { lines: [], status: "failed", bytesRead: 0 }; }
|
|
216
|
+
if (stat.size > MAX_BYTES) return { lines: [], status: "too-large", bytesRead: 0 };
|
|
217
|
+
|
|
218
|
+
const lines = [];
|
|
219
|
+
let bytesRead = 0;
|
|
220
|
+
const stream = fs.createReadStream(file, { encoding: "utf-8" });
|
|
221
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
222
|
+
const timer = setTimeout(() => stream.destroy(new Error("read timed out")), READ_TIMEOUT_MS);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
for await (const line of rl) {
|
|
226
|
+
lines.push(line);
|
|
227
|
+
bytesRead += Buffer.byteLength(line, "utf-8") + 1; // +1 for the stripped newline
|
|
228
|
+
}
|
|
229
|
+
return { lines, status: "complete", bytesRead };
|
|
230
|
+
} catch {
|
|
231
|
+
// Lines read before the failure are real content and may hold a real
|
|
232
|
+
// secret -- an honest "partial" beats a silent false negative.
|
|
233
|
+
return { lines, status: lines.length > 0 ? "partial" : "failed", bytesRead };
|
|
234
|
+
} finally {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
rl.close();
|
|
237
|
+
stream.destroy();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { id, label, available, files, readLines };
|