residoo 0.19.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 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
- 44 sources, real-install-verified for Claude Code and its config family,
255
- multi-source-corroborated for the rest (Cursor, Codex CLI, Cline, Windsurf,
256
- Gemini CLI, Copilot, and 30+ more). Full list, what "corroborated" means,
257
- and how to add one: [docs/sources.md](docs/sources.md).
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.19.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/cli.js CHANGED
@@ -186,8 +186,8 @@ Watch:
186
186
  same meaning as scan
187
187
  --no-notify skip the OS desktop notification watch fires for
188
188
  each genuinely new finding (macOS via osascript,
189
- Linux via notify-send if installed; no built-in
190
- mechanism on Windows -- disclosed, not attempted).
189
+ Linux via notify-send if installed, Windows via
190
+ System.Windows.Forms.NotifyIcon's balloon-tip API).
191
191
  On by default in human-readable mode: watch's own
192
192
  purpose is alerting you, and a background process
193
193
  nobody is watching a terminal for needs more than
package/src/notify.js CHANGED
@@ -8,13 +8,8 @@ const cp = require("child_process");
8
8
  * precedent `keychain.js`'s `security` and `ocr.js`'s `tesseract` already
9
9
  * set), Linux via `notify-send` (commonly present on a desktop session,
10
10
  * NOT guaranteed -- `watch` also runs on headless/server machines with no
11
- * notification daemon at all).
12
- *
13
- * Windows: no built-in, dependency-free mechanism was found that doesn't
14
- * either need an external module (BurntToast) or pop a blocking, modal
15
- * MessageBox in front of a background process -- a disclosed scope limit,
16
- * not silently assumed covered, the same posture `keychain.js` already
17
- * takes for its own Windows refusal.
11
+ * notification daemon at all), Windows via `System.Windows.Forms.NotifyIcon`'s
12
+ * balloon-tip API (see the Windows-specific docstring below).
18
13
  *
19
14
  * Decoration, never the report itself: `watch`'s own `emit()` already
20
15
  * writes every finding to stdout/stderr before this is ever called, so a
@@ -43,11 +38,63 @@ function notifyDesktop(title, message) {
43
38
  const child = cp.spawn("notify-send", [String(title), String(message)], { stdio: "ignore" });
44
39
  child.on("error", () => {});
45
40
  child.unref();
41
+ } else if (process.platform === "win32") {
42
+ notifyWindows(title, message);
46
43
  }
47
- // Windows and anything else: no-op, disclosed above, not attempted.
44
+ // Anything else: no-op, not attempted.
48
45
  } catch {
49
46
  // Never let a notification failure affect the caller.
50
47
  }
51
48
  }
52
49
 
50
+ /**
51
+ * Windows desktop notification via `System.Windows.Forms.NotifyIcon`'s
52
+ * balloon-tip API, shelled out to `powershell.exe` -- an earlier version
53
+ * of this module considered WinRT toast interop
54
+ * (`[Windows.UI.Notifications.ToastNotificationManager]`) instead and
55
+ * declined it after research found a real, disqualifying prerequisite:
56
+ * Microsoft's own docs make a Start-menu shortcut carrying a registered
57
+ * AppUserModelID a hard requirement for ANY desktop app's toast to
58
+ * display at all, explicitly including unpackaged/scripted apps. NotifyIcon
59
+ * has no such requirement -- confirmed against Microsoft's own current API
60
+ * reference (no [Obsolete] marker, listed through the windowsdesktop-10.0/
61
+ * 11.0 monikers) and multiple independently-converging technique
62
+ * write-ups, one of which states plainly it "requires no Start-menu
63
+ * shortcuts, AUMID registration, or external PowerShell modules." Not
64
+ * live-tested against a real Windows install, the same disclosed
65
+ * limitation `keychain.js`'s DPAPI functions and `integrity.js`'s Get-Acl
66
+ * check already carry.
67
+ *
68
+ * Two real caveats, disclosed rather than smoothed over: Windows ignores
69
+ * the millisecond value passed to `ShowBalloonTip` (actual on-screen
70
+ * duration is governed by the user's own accessibility settings, not this
71
+ * script), and the tray icon does NOT self-remove -- every reference
72
+ * implementation found demonstrates disposal via an interactive
73
+ * double-click handler, not an automatic one, which does not exist for a
74
+ * non-interactive script. This is why the script below explicitly
75
+ * `Start-Sleep`s before calling `.Dispose()` itself, inside the SAME
76
+ * spawned process: `notifyDesktop` never blocks its caller (the sleep
77
+ * happens in a detached, `unref()`'d child, exactly like the macOS/Linux
78
+ * branches above), but something has to keep the icon alive long enough
79
+ * to actually be seen before removing it, and nothing outside that one
80
+ * process is positioned to send a follow-up "now dispose" signal.
81
+ */
82
+ function notifyWindows(title, message) {
83
+ const esc = (s) => String(s).replace(/'/g, "''");
84
+ const script =
85
+ "Add-Type -AssemblyName System.Windows.Forms; " +
86
+ "Add-Type -AssemblyName System.Drawing; " +
87
+ "$ni = New-Object System.Windows.Forms.NotifyIcon; " +
88
+ "$ni.Icon = [System.Drawing.SystemIcons]::Information; " +
89
+ `$ni.BalloonTipTitle = '${esc(title)}'; ` +
90
+ `$ni.BalloonTipText = '${esc(message)}'; ` +
91
+ "$ni.Visible = $true; " +
92
+ "$ni.ShowBalloonTip(10000); " +
93
+ "Start-Sleep -Seconds 10; " +
94
+ "$ni.Dispose()";
95
+ const child = cp.spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script], { stdio: "ignore" });
96
+ child.on("error", () => {});
97
+ child.unref();
98
+ }
99
+
53
100
  module.exports = { notifyDesktop };
@@ -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 };