residoo 0.3.7 → 0.3.9

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
@@ -121,6 +121,18 @@ won't be built into the tool that writes it.
121
121
  - Redacts everything in its own output. You get a shape and a first/last-4
122
122
  preview, never the real value, including in `--json` mode. A decoded or
123
123
  rejoined secret is redacted exactly like a plain one.
124
+ - On an interactive terminal, prints who it is and where it lives before
125
+ scanning starts (`residoo v0.3.9 · find secrets your AI coding agent left
126
+ on disk` plus the repo URL), then a live spinner naming the current file
127
+ as it scans. Every report also opens with the exact version and timestamp
128
+ it was run with (`residoo v0.3.9 · scanned 2026-01-01 12:00`; `--json`
129
+ carries the same as `residooVersion`/`scannedAt`), so a report pasted or
130
+ screenshotted later never leaves you guessing which build produced it.
131
+ When there are findings, the report closes with a "Next steps" pointer to
132
+ `--json` and `--seal`. All of the interactive chatter (the intro, the
133
+ spinner) goes to stderr only and is a complete no-op when stdout/stderr
134
+ are piped, redirected, or run in CI, so none of it can ever interleave
135
+ with `--json`/`--sarif` output.
124
136
  - `--sarif` emits SARIF 2.1.0 for GitHub code scanning's Security tab and
125
137
  inline pull-request annotations, the same format gitleaks/trufflehog/
126
138
  agentsweep already speak, so residoo's own Action and pre-commit hook plug
@@ -317,7 +329,7 @@ As a GitHub Action (this repository doubles as a composite action):
317
329
  ```yaml
318
330
  steps:
319
331
  - uses: actions/checkout@v4
320
- - uses: dandovdub/residoo@v0.3.7
332
+ - uses: dandovdub/residoo@v0.3.9
321
333
  ```
322
334
 
323
335
  As a pre-commit hook:
@@ -325,7 +337,7 @@ As a pre-commit hook:
325
337
  ```yaml
326
338
  repos:
327
339
  - repo: https://github.com/dandovdub/residoo
328
- rev: v0.3.7
340
+ rev: v0.3.9
329
341
  hooks:
330
342
  - id: residoo
331
343
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "residoo",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
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
@@ -5,7 +5,7 @@ const fs = require("fs");
5
5
  const crypto = require("crypto");
6
6
  const { availableSources, ALL_SOURCES } = require("./sources");
7
7
  const { scan, emptyResult } = require("./scan");
8
- const { render, renderIntegrity, renderJson, renderSarif } = require("./report");
8
+ const { render, renderIntegrity, renderJson, renderSarif, makeProgressReporter, printIntro } = require("./report");
9
9
  const { checkIntegrity } = require("./integrity");
10
10
  const {
11
11
  ROTATION_GUIDANCE, guidanceFor, loadAcks, ackFinding, renderRotation,
@@ -404,6 +404,7 @@ async function main(argv) {
404
404
  // mutated env var would leak past this one invocation and silently kill
405
405
  // color for a later call that never asked for that.
406
406
  const noColor = args.includes("--no-color");
407
+ printIntro(noColor);
407
408
 
408
409
  // Integrity runs by default: a scan that reports "no secrets leaked" while
409
410
  // a planted SessionStart hook sits ready to re-leak them next session is
@@ -494,7 +495,9 @@ async function main(argv) {
494
495
  return failOnFind && integrityWarnCount(integrity) > 0 ? 1 : 0;
495
496
  }
496
497
 
497
- const result = await scan({ sources, includeNoisy, includeSuppressed });
498
+ const progress = makeProgressReporter(noColor);
499
+ const result = await scan({ sources, includeNoisy, includeSuppressed, onProgress: progress.onProgress });
500
+ progress.stop();
498
501
  const integrity = wantsIntegrity ? runIntegrity() : null;
499
502
  const rotation = renderRotation(result.findings, acks);
500
503
  process.stdout.write((wantsSarif
package/src/report.js CHANGED
@@ -29,6 +29,66 @@ function ageDays(mtimeMs) {
29
29
  return Math.max(0, Math.floor((Date.now() - mtimeMs) / 86400000));
30
30
  }
31
31
 
32
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
33
+
34
+ /**
35
+ * Printed once, before scanning starts: what this is, what version, where
36
+ * it lives. Answers exactly the question a first-time (or every-time)
37
+ * reader has before results ever appear, without waiting for the report.
38
+ * Same TTY gate and same stream as the progress spinner below and for the
39
+ * same reason: stderr only, so a --json/--sarif consumer's stdout is
40
+ * untouched, and a complete no-op under redirection, piping, or CI.
41
+ */
42
+ function printIntro(noColor) {
43
+ if (!process.stderr.isTTY) return;
44
+ const paint = makePaint(noColor);
45
+ const { version } = require("../package.json");
46
+ process.stderr.write(
47
+ paint(c.bold + c.cyan, `residoo v${version}`) +
48
+ paint(c.dim, " · find secrets your AI coding agent left on disk\n") +
49
+ paint(c.dim, "https://github.com/dandovdub/residoo\n\n")
50
+ );
51
+ }
52
+
53
+ /**
54
+ * A progress indicator for the scan phase, wired to scan()'s own
55
+ * onProgress callback. Writes to STDERR only, never stdout: --json/--sarif
56
+ * consumers pipe stdout into a parser, and a spinner corrupting that would
57
+ * be a much worse bug than not having one. Gated on stderr actually being a
58
+ * TTY, so it is a complete no-op under redirection, piping, or CI, exactly
59
+ * the contexts where carriage-return spam in a captured log would be
60
+ * useless or actively annoying, not merely invisible. Shows the actual
61
+ * current file (basename only, through the same safeBasename() every other
62
+ * displayed path in this report goes through — control bytes stripped,
63
+ * invisible code points made visible), not just a running count: real
64
+ * signal, not just motion, and genuinely useful if a scan stalls on one
65
+ * huge file. `stop()` clears the line so whatever prints next starts
66
+ * clean.
67
+ */
68
+ function makeProgressReporter(noColor) {
69
+ if (!process.stderr.isTTY) return { onProgress: null, stop() {} };
70
+ const paint = makePaint(noColor);
71
+ let count = 0;
72
+ let lastWriteMs = 0;
73
+ let lastLineLen = 0;
74
+ let frame = 0;
75
+ const write = (s, visibleLen) => {
76
+ process.stderr.write("\r" + " ".repeat(lastLineLen) + "\r" + s);
77
+ lastLineLen = visibleLen;
78
+ };
79
+ const onProgress = ({ source, file }) => {
80
+ count++;
81
+ const now = Date.now();
82
+ if (now - lastWriteMs < 80) return; // throttled: avoid flicker on a fast scan
83
+ lastWriteMs = now;
84
+ frame = (frame + 1) % SPINNER_FRAMES.length;
85
+ const label = `scanning ${source}… ${count} file${count === 1 ? "" : "s"} ${safeBasename(file)}`;
86
+ write(paint(c.bold + c.cyan, SPINNER_FRAMES[frame]) + " " + paint(c.dim, label), 2 + label.length);
87
+ };
88
+ const stop = () => { if (lastLineLen > 0) process.stderr.write("\r" + " ".repeat(lastLineLen) + "\r"); };
89
+ return { onProgress, stop };
90
+ }
91
+
32
92
  // File NAMES are attacker-controllable text headed for a terminal: in
33
93
  // --project mode a hostile checkout chooses its own filenames, and a name
34
94
  // carrying raw ESC bytes could clear the screen or overwrite the findings
@@ -160,11 +220,23 @@ function renderIntegrity(integrity, { noColor = false } = {}) {
160
220
  return lines.join("\n");
161
221
  }
162
222
 
223
+ /** "YYYY-MM-DD HH:MM" in local time — matches the user's own system clock, not UTC. */
224
+ function localTimestamp(d) {
225
+ const p2 = (n) => String(n).padStart(2, "0");
226
+ return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}`;
227
+ }
228
+
163
229
  function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppressedCount = 0, distinctCounts = {}, unreadableFiles = [] }, { noColor = false, integrity = null, rotation = null } = {}) {
164
230
  const paint = makePaint(noColor);
165
231
  const lines = [];
166
232
  const push = (s = "") => lines.push(s);
167
233
 
234
+ // Which build ran and when, up front: a report pasted or screenshotted
235
+ // hours later (or a "why don't I see feature X" question) should never
236
+ // require asking "what version were you even running."
237
+ const { version } = require("../package.json");
238
+ push(paint(c.dim, `residoo v${version} · scanned ${localTimestamp(new Date())}`));
239
+
168
240
  const suppressedNote = suppressedCount > 0
169
241
  ? paint(c.dim, ` (${suppressedCount} more matched but looked like placeholder/example text; see --include-suppressed)`)
170
242
  : "";
@@ -255,9 +327,13 @@ function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppress
255
327
  push(renderIntegrity(integrity, { noColor }));
256
328
  }
257
329
 
330
+ push();
331
+ push(paint(c.bold, "Next steps:"));
332
+ push(` residoo scan --json ${paint(c.dim, "machine-readable output, full detail")}`);
333
+ push(` residoo scan --seal ${paint(c.dim, "encrypt the affected files into a local vault (originals untouched)")}`);
258
334
  push();
259
335
  push(paint(c.dim, "Values are redacted in this report (first/last 4 characters only). Nothing scanned"));
260
- push(paint(c.dim, "here left your machine; residoo makes no network calls. Run with --json for full detail."));
336
+ push(paint(c.dim, "here left your machine; residoo makes no network calls."));
261
337
 
262
338
  return lines.join("\n");
263
339
  }
@@ -271,8 +347,11 @@ function render({ findings, filesScanned, sourcesScanned, bytesScanned, suppress
271
347
  // way, since it is derived from already-redacted material and is what
272
348
  // "residoo ack" takes.
273
349
  function renderJson(result, integrity = null, rotation = null) {
350
+ const { version } = require("../package.json");
274
351
  return JSON.stringify(
275
352
  {
353
+ residooVersion: version,
354
+ scannedAt: new Date().toISOString(),
276
355
  summary: {
277
356
  findingCount: result.findings.length,
278
357
  filesScanned: result.filesScanned,
@@ -398,4 +477,4 @@ function renderSarif(result) {
398
477
  }, null, 2);
399
478
  }
400
479
 
401
- module.exports = { render, renderIntegrity, renderRotationSection, renderJson, renderSarif };
480
+ module.exports = { render, renderIntegrity, renderRotationSection, renderJson, renderSarif, makeProgressReporter, printIntro };