residoo 0.24.0 → 0.26.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 +11 -0
- package/package.json +1 -1
- package/src/cli.js +34 -2
- package/src/notify.js +86 -1
package/README.md
CHANGED
|
@@ -206,6 +206,17 @@ brew install residoo
|
|
|
206
206
|
The Homebrew formula installs the exact tarball published to npm (sha256
|
|
207
207
|
verified): same bits, not a second build.
|
|
208
208
|
|
|
209
|
+
**macOS, no terminal needed for install or uninstall:** download
|
|
210
|
+
`residoo-<version>.pkg` from the
|
|
211
|
+
[latest release](https://github.com/dandovdub/residoo/releases/latest)
|
|
212
|
+
and double-click it to install, `residoo-uninstall-<version>.pkg` to
|
|
213
|
+
remove it. Both are unsigned (no Apple Developer ID -- right-click >
|
|
214
|
+
Open once to get past Gatekeeper's "unidentified developer" warning),
|
|
215
|
+
and installing still needs Node.js present on the machine (it runs
|
|
216
|
+
`npm install -g residoo` on your behalf, it doesn't bundle a Node
|
|
217
|
+
runtime) -- see [packaging/macos-pkg](packaging/macos-pkg/README.md) for
|
|
218
|
+
exactly what each one does and how both were verified.
|
|
219
|
+
|
|
209
220
|
Requires Node.js 18+ (22.5+ for the SQLite-backed sources listed in
|
|
210
221
|
[docs/sources.md](docs/sources.md); residoo still runs fine without it).
|
|
211
222
|
Zero runtime dependencies: check `package.json` rather than take that on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
} = require("./rotation");
|
|
14
14
|
const { startWatch, isTailable } = require("./watch");
|
|
15
15
|
const { startDashboardServer, openBrowser } = require("./dashboard");
|
|
16
|
+
const { startWindowsTray } = require("./notify");
|
|
16
17
|
const { startMcpServer } = require("./mcp");
|
|
17
18
|
const { buildTools } = require("./mcpTools");
|
|
18
19
|
const { runGuard: runGuardEngine, buildHookConfig } = require("./guard");
|
|
@@ -216,8 +217,17 @@ Watch:
|
|
|
216
217
|
nobody is watching a terminal for needs more than
|
|
217
218
|
a printed line. Never fires for a re-exposure of
|
|
218
219
|
something already seen, and never in --json mode.
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
--tray Windows only: a persistent "residoo is watching"
|
|
221
|
+
tray icon for the life of this session, separate
|
|
222
|
+
from --no-notify's own per-finding balloon-tip
|
|
223
|
+
alerts (which still fire independently). No
|
|
224
|
+
equivalent stock mechanism exists on macOS/Linux
|
|
225
|
+
yet -- disclosed with a one-line message there,
|
|
226
|
+
not silently ignored. Right-click > Hide icon to
|
|
227
|
+
dismiss it without stopping the watch itself.
|
|
228
|
+
Ctrl+C stops cleanly, closes the tray icon if one was shown, and prints a
|
|
229
|
+
session summary (skipped with --json, where the same information is one
|
|
230
|
+
final NDJSON event).
|
|
221
231
|
|
|
222
232
|
Dashboard:
|
|
223
233
|
residoo dashboard the exact "scan --html" report, served live at
|
|
@@ -823,6 +833,7 @@ async function runWatch(args) {
|
|
|
823
833
|
const includePii = args.includes("--include-pii");
|
|
824
834
|
const includeInjection = args.includes("--include-injection");
|
|
825
835
|
const noNotify = args.includes("--no-notify");
|
|
836
|
+
const wantsTray = args.includes("--tray");
|
|
826
837
|
|
|
827
838
|
let intervalSeconds = 5;
|
|
828
839
|
const intervalArg = argValue(args, "--interval");
|
|
@@ -846,6 +857,22 @@ async function runWatch(args) {
|
|
|
846
857
|
|
|
847
858
|
if (!wantsJson) printWatchBanner(sources);
|
|
848
859
|
|
|
860
|
+
// --tray: a PERSISTENT status-presence icon, deliberately decoupled from
|
|
861
|
+
// the per-finding balloon-tip alerts --no-notify already controls (see
|
|
862
|
+
// notify.js's startWindowsTray docstring for why merging them would need
|
|
863
|
+
// real inter-process communication this project has no way to verify).
|
|
864
|
+
// Windows-only, disclosed rather than silently ignored elsewhere: a real
|
|
865
|
+
// stock mechanism exists there (PowerShell + .NET's own NotifyIcon); macOS
|
|
866
|
+
// has none without a compiled app, and this flag doesn't attempt one.
|
|
867
|
+
let trayProcess = null;
|
|
868
|
+
if (wantsTray) {
|
|
869
|
+
if (process.platform === "win32") {
|
|
870
|
+
trayProcess = startWindowsTray("residoo is watching");
|
|
871
|
+
} else if (!wantsJson) {
|
|
872
|
+
process.stderr.write("--tray is Windows-only (no equivalent stock mechanism on this platform yet) -- ignored on this run.\n");
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
849
876
|
const { promise, stop } = startWatch({
|
|
850
877
|
sources,
|
|
851
878
|
options: { includeNoisy, includeSuppressed, verify, noColor, includePii, includeInjection, noNotify, json: wantsJson, pollMs: intervalSeconds * 1000 },
|
|
@@ -863,6 +890,10 @@ async function runWatch(args) {
|
|
|
863
890
|
const onSignal = () => {
|
|
864
891
|
if (signalled) return;
|
|
865
892
|
signalled = true;
|
|
893
|
+
// Not .unref()'d when spawned (see startWindowsTray's own docstring),
|
|
894
|
+
// specifically so this kill() can still reach it here -- a spawned
|
|
895
|
+
// child Node has already lost its handle to can never be stopped again.
|
|
896
|
+
if (trayProcess) trayProcess.kill();
|
|
866
897
|
printFinalSummary(stop());
|
|
867
898
|
};
|
|
868
899
|
process.once("SIGINT", onSignal);
|
|
@@ -871,6 +902,7 @@ async function runWatch(args) {
|
|
|
871
902
|
const stats = await promise;
|
|
872
903
|
process.removeListener("SIGINT", onSignal);
|
|
873
904
|
process.removeListener("SIGTERM", onSignal);
|
|
905
|
+
if (trayProcess && !signalled) trayProcess.kill();
|
|
874
906
|
// promise can also resolve because something ELSE called stop() (not
|
|
875
907
|
// possible from outside this function today, but the contract allows
|
|
876
908
|
// it) -- print the summary exactly once regardless of which path got here.
|
package/src/notify.js
CHANGED
|
@@ -97,4 +97,89 @@ function notifyWindows(title, message) {
|
|
|
97
97
|
child.unref();
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
/**
|
|
101
|
+
* A PERSISTENT Windows tray icon -- genuinely different from `notifyWindows`
|
|
102
|
+
* above, not a variant of it: that function is fire-and-forget (one balloon,
|
|
103
|
+
* then the process disposes itself and exits); this one stays alive and
|
|
104
|
+
* visible for as long as the CALLER wants it to (`residoo watch --tray`,
|
|
105
|
+
* the standing-presence "residoo is watching" indicator this project's own
|
|
106
|
+
* platform-scope.md records as a real, scoped follow-up). The two are
|
|
107
|
+
* deliberately DECOUPLED, not merged into one mechanism: this function only
|
|
108
|
+
* shows a static icon + tooltip; actual per-finding alerts keep firing
|
|
109
|
+
* through the existing, already-proven `notifyWindows` balloon-tip path,
|
|
110
|
+
* completely independently. Combining them into one process would mean
|
|
111
|
+
* finding a way to push live UPDATES into an already-running PowerShell
|
|
112
|
+
* process (a named pipe, a polled state file) -- real inter-process-
|
|
113
|
+
* communication complexity this project has no way to verify without a
|
|
114
|
+
* real Windows machine, so it was deliberately left out of scope rather
|
|
115
|
+
* than shipped unverified. A static presence icon needs no such channel.
|
|
116
|
+
*
|
|
117
|
+
* API surface verified directly against Microsoft's own current docs
|
|
118
|
+
* (learn.microsoft.com, fetched 2026-09-07), the same bar every other
|
|
119
|
+
* Windows-specific function in this project holds to, and likewise NOT
|
|
120
|
+
* live-tested against a real Windows install:
|
|
121
|
+
* - `NotifyIcon.ContextMenuStrip` (not the older, pre-.NET-2.0
|
|
122
|
+
* `ContextMenu`/`MenuItem` classes) is the current, non-deprecated
|
|
123
|
+
* property, listed through the windowsdesktop-11.0 moniker.
|
|
124
|
+
* - `SystemIcons.Shield` is a real, current static property ("an Icon
|
|
125
|
+
* object that contains the shield icon") -- used here instead of
|
|
126
|
+
* bundling a custom .ico file, matching zero-dependency the same way
|
|
127
|
+
* `notifyWindows` reuses `SystemIcons.Information`.
|
|
128
|
+
* - `NotifyIcon.Text` (the tooltip) has a REAL, documented, THROWING
|
|
129
|
+
* limit: Microsoft's own docs give an exact table -- 63 characters
|
|
130
|
+
* for .NET Framework and .NET 5/Core 3.0-3.1, 127 for .NET 6+. Windows
|
|
131
|
+
* PowerShell (`powershell.exe`, what this project shells out to
|
|
132
|
+
* everywhere, never `pwsh.exe`) runs on .NET Framework, so 63 is the
|
|
133
|
+
* applicable limit, and exceeding it throws `ArgumentException` --
|
|
134
|
+
* not a cosmetic detail, a real crash this function truncates against
|
|
135
|
+
* before it can happen.
|
|
136
|
+
*
|
|
137
|
+
* Lifecycle, the one place this genuinely departs from every other spawn
|
|
138
|
+
* in this file: NOT `.unref()`'d, because the caller (`watch.js`) needs
|
|
139
|
+
* to `.kill()` this exact child process on its own SIGINT/SIGTERM
|
|
140
|
+
* shutdown -- an unref'd handle a caller has already discarded can't be
|
|
141
|
+
* reached again later. `[System.Windows.Forms.Application]::Run()` blocks
|
|
142
|
+
* the spawned PowerShell process in its own message loop for as long as
|
|
143
|
+
* the icon should stay visible; the "Hide icon" context-menu item calls
|
|
144
|
+
* `Application.Exit()` so a user can dismiss it independently of whether
|
|
145
|
+
* `residoo watch` itself keeps running.
|
|
146
|
+
*/
|
|
147
|
+
function startWindowsTray(tooltip) {
|
|
148
|
+
if (process.platform !== "win32") return null;
|
|
149
|
+
// Unlike notifyDesktop's callers, nothing wraps a call to this function
|
|
150
|
+
// in an outer try/catch -- runWatch calls it directly, and this returns
|
|
151
|
+
// a real value (the child, or null) callers branch on, so the "never
|
|
152
|
+
// throw" contract has to be enforced right here, not borrowed from a
|
|
153
|
+
// caller the way notifyWindows borrows notifyDesktop's. The whole body
|
|
154
|
+
// is inside this one try, not just the spawn call: `String(tooltip)`
|
|
155
|
+
// itself can throw -- an object whose own `toString` property isn't a
|
|
156
|
+
// function fails JavaScript's ToPrimitive coercion before anything else
|
|
157
|
+
// runs, the exact real (fast-check-found, not hypothetical) shape
|
|
158
|
+
// cve.js's parseVersion hit and was fixed for earlier this same
|
|
159
|
+
// project -- confirmed directly here too before shipping, not assumed
|
|
160
|
+
// fixed by analogy.
|
|
161
|
+
try {
|
|
162
|
+
const esc = (s) => String(s).replace(/'/g, "''");
|
|
163
|
+
const truncated = String(tooltip).slice(0, 63); // NotifyIcon.Text's real, throwing limit on .NET Framework -- see docstring
|
|
164
|
+
const script =
|
|
165
|
+
"Add-Type -AssemblyName System.Windows.Forms; " +
|
|
166
|
+
"Add-Type -AssemblyName System.Drawing; " +
|
|
167
|
+
"$ni = New-Object System.Windows.Forms.NotifyIcon; " +
|
|
168
|
+
"$ni.Icon = [System.Drawing.SystemIcons]::Shield; " +
|
|
169
|
+
`$ni.Text = '${esc(truncated)}'; ` +
|
|
170
|
+
"$ni.Visible = $true; " +
|
|
171
|
+
"$menu = New-Object System.Windows.Forms.ContextMenuStrip; " +
|
|
172
|
+
"$hideItem = New-Object System.Windows.Forms.ToolStripMenuItem 'Hide icon'; " +
|
|
173
|
+
"$hideItem.add_Click({ $ni.Visible = $false; $ni.Dispose(); [System.Windows.Forms.Application]::Exit() }); " +
|
|
174
|
+
"[void]$menu.Items.Add($hideItem); " +
|
|
175
|
+
"$ni.ContextMenuStrip = $menu; " +
|
|
176
|
+
"[System.Windows.Forms.Application]::Run()";
|
|
177
|
+
const child = cp.spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script], { stdio: "ignore" });
|
|
178
|
+
child.on("error", () => {}); // binary missing or spawn failed: never throw
|
|
179
|
+
return child;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { notifyDesktop, startWindowsTray };
|