@node9/proxy 1.20.1 → 1.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 +35 -36
- package/dist/cli.js +42 -16
- package/dist/cli.mjs +42 -16
- package/dist/scan-ink.mjs +1681 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,21 +8,33 @@
|
|
|
8
8
|
<a href="https://huggingface.co/spaces/Node9ai/node9-security-demo"><img src="https://huggingface.co/datasets/huggingface/badges/resolve/main/open-in-hf-spaces-sm.svg" alt="Try on HF Spaces" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
Node9 sits between your AI agent and the tools it can use —
|
|
11
|
+
Node9 sits between your AI agent and the tools it can use — **discover** what it's already been doing, **protect** against risky actions in real time, and **review** what happened over any time window.
|
|
12
12
|
|
|
13
13
|
Works with **Claude Code · Codex CLI · Gemini CLI · Cursor · Windsurf · any MCP server**.
|
|
14
14
|
|
|
15
15
|
## What Node9 does
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
17
|
+
- 🔍 **Discover** — scan every past AI session for credential leaks, agent loops, blocked operations, and every secret on disk an agent could reach right now
|
|
18
|
+
- 🛡 **Protect** — review or block risky commands before they run — `rm -rf`, `git push --force`, `DROP TABLE`, credential reads, `curl | bash`, AWS/GitHub/Stripe key leaks
|
|
19
|
+
- 📊 **Review** — period-windowed report (today / week / month / 90 days) — cost per agent, top tools, shields fired, blast radius
|
|
20
|
+
|
|
21
|
+
## Retrospective scan
|
|
22
|
+
|
|
23
|
+
This is my own machine — 90 days while building Node9. Score 25/100, 5 credential files an AI agent could reach right now.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx node9-ai scan # before installation, runs in ~10s, nothing uploads
|
|
27
|
+
node9 scan # after installation, same output
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="https://github.com/user-attachments/assets/7c5b30f1-1ca1-40b4-bfd5-d6671002e98e" width="720" alt="Node9 scan scorecard" />
|
|
32
|
+
</p>
|
|
21
33
|
|
|
22
34
|
## Live monitoring
|
|
23
35
|
|
|
24
36
|
<p align="center">
|
|
25
|
-
<img src="https://github.com/user-attachments/assets/
|
|
37
|
+
<img src="https://github.com/user-attachments/assets/4661da97-c174-4bae-ae54-4c52a1d69213" width="720" alt="Node9 monitor dashboard" />
|
|
26
38
|
</p>
|
|
27
39
|
|
|
28
40
|
`node9 monitor` opens an interactive terminal dashboard with two views:
|
|
@@ -30,31 +42,18 @@ Works with **Claude Code · Codex CLI · Gemini CLI · Cursor · Windsurf · any
|
|
|
30
42
|
- **`[1]` Realtime** — live activity, approvals, security alerts, current risk score
|
|
31
43
|
- **`[2]` Report** — period-windowed summary: cost, top tools, shields fired, blast radius
|
|
32
44
|
|
|
33
|
-
##
|
|
45
|
+
## Report
|
|
34
46
|
|
|
35
|
-
|
|
47
|
+
Press `[2]` in monitor for a period-windowed summary. Toggle the window with `[T]oday` · `[W]eek` · `[M]onth` · `[N]inety` — same panels as the scan above, driven by your post-install audit log.
|
|
36
48
|
|
|
37
49
|
<p align="center">
|
|
38
|
-
<img src="https://github.com/user-attachments/assets/
|
|
50
|
+
<img src="https://github.com/user-attachments/assets/66c02a72-e477-443d-807f-d65a21d096cd" width="720" alt="Node9 monitor [2] Report" />
|
|
39
51
|
</p>
|
|
40
52
|
|
|
53
|
+
```bash
|
|
54
|
+
node9 monitor # press [2] for Report view
|
|
55
|
+
node9 report --period 7d # CLI form, no TUI
|
|
41
56
|
```
|
|
42
|
-
🛡 Node9 Scan · 21 sessions · 8,114 tool calls · Apr 6 – May 1, 2026
|
|
43
|
-
|
|
44
|
-
Security Score: 25/100 · Critical
|
|
45
|
-
$3,789 AI spend · 62 risky operations
|
|
46
|
-
|
|
47
|
-
🔑 14 credential leak (Bearer Token ×4, GCP API Key ×4, JWT ×2)
|
|
48
|
-
🛑 15 would have blocked (force-push ×5, read-ssh ×4, read-aws ×4)
|
|
49
|
-
🔁 193 agent loops (18% wasted · ~$6.51)
|
|
50
|
-
👁 33 flagged for review (git-destructive ×19, rm ×9, sudo ×2)
|
|
51
|
-
|
|
52
|
-
🔭 Blast radius ssh × gcp × npm × other (5 exposures)
|
|
53
|
-
|
|
54
|
-
→ npx node9-ai scan run this on your machine
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Run it on yours — `npx node9-ai scan` finishes in ~10 seconds and runs entirely local. Nothing uploads.
|
|
58
57
|
|
|
59
58
|
## Install
|
|
60
59
|
|
|
@@ -141,17 +140,17 @@ node9 mcp pin reset # clear all pins
|
|
|
141
140
|
|
|
142
141
|
</details>
|
|
143
142
|
|
|
144
|
-
##
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
| `node9
|
|
151
|
-
| `node9
|
|
152
|
-
| `node9 sessions` | Session history with prompt, tool trace, cost, snapshot | Reviewing a handoff or past work
|
|
153
|
-
| `node9 dlp` | Credential-leak findings in Claude response text | Any time a DLP desktop alert fires
|
|
154
|
-
| `node9 mask` | Redact plaintext secrets from local session history files | After a DLP finding — cleans local disk
|
|
143
|
+
## Other commands
|
|
144
|
+
|
|
145
|
+
Beyond the three flow commands above (`scan` / `monitor` / `report`):
|
|
146
|
+
|
|
147
|
+
| Command | What it shows | When to use |
|
|
148
|
+
| ---------------- | --------------------------------------------------------- | --------------------------------------- |
|
|
149
|
+
| `node9 blast` | What an AI agent can reach right now — files, creds, env | First thing to run on any machine |
|
|
150
|
+
| `node9 tail` | Live stream of every tool call (text-only, no TUI) | Piping into other tools, CI, logs |
|
|
151
|
+
| `node9 sessions` | Session history with prompt, tool trace, cost, snapshot | Reviewing a handoff or past work |
|
|
152
|
+
| `node9 dlp` | Credential-leak findings in Claude response text | Any time a DLP desktop alert fires |
|
|
153
|
+
| `node9 mask` | Redact plaintext secrets from local session history files | After a DLP finding — cleans local disk |
|
|
155
154
|
|
|
156
155
|
Plus a **live HUD** in your Claude Code statusline:
|
|
157
156
|
|
package/dist/cli.js
CHANGED
|
@@ -10505,7 +10505,7 @@ function originForRule(ruleName, sections) {
|
|
|
10505
10505
|
return "";
|
|
10506
10506
|
}
|
|
10507
10507
|
function registerScanCommand(program2) {
|
|
10508
|
-
program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option(
|
|
10508
|
+
program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option("--classic", "Original chalk-based scorecard layout (default: new Ink-rendered view)").option(
|
|
10509
10509
|
"--json",
|
|
10510
10510
|
"Emit machine-readable JSON to stdout (suppresses banner, progress, and renderer)"
|
|
10511
10511
|
).option(
|
|
@@ -10539,7 +10539,8 @@ function registerScanCommand(program2) {
|
|
|
10539
10539
|
})();
|
|
10540
10540
|
const isWired = getAgentsStatus().some((a) => a.wired);
|
|
10541
10541
|
const screenshotMode = options.compact || options.narrative;
|
|
10542
|
-
const
|
|
10542
|
+
const useInk = !options.classic && !drillDown;
|
|
10543
|
+
const quiet = screenshotMode || options.json || useInk;
|
|
10543
10544
|
if (!quiet) {
|
|
10544
10545
|
console.log("");
|
|
10545
10546
|
if (!isWired) {
|
|
@@ -10677,6 +10678,7 @@ function registerScanCommand(program2) {
|
|
|
10677
10678
|
});
|
|
10678
10679
|
return;
|
|
10679
10680
|
}
|
|
10681
|
+
const useInkForHero = !options.classic && !drillDown;
|
|
10680
10682
|
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
10681
10683
|
console.log(import_chalk5.default.green(" \u2705 No risky operations found in your history."));
|
|
10682
10684
|
console.log(
|
|
@@ -10696,9 +10698,11 @@ function registerScanCommand(program2) {
|
|
|
10696
10698
|
const since = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo} days ago`;
|
|
10697
10699
|
return import_chalk5.default.dim(" \xB7 ") + arrow + import_chalk5.default.dim(` since ${since}`);
|
|
10698
10700
|
})();
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10701
|
+
if (!useInkForHero) {
|
|
10702
|
+
console.log(
|
|
10703
|
+
" " + (score.band === "critical" ? import_chalk5.default.red.bold("\u26A0 ") : "") + import_chalk5.default.bold("Security Score ") + score.color.bold(`${blast.score}/100`) + " " + severityDisplay + trendSuffix + import_chalk5.default.dim(" \xB7 ") + (totalRisky > 0 ? import_chalk5.default.red.bold(`${totalRisky} risky operation${totalRisky !== 1 ? "s" : ""}`) : import_chalk5.default.green("No risky operations"))
|
|
10704
|
+
);
|
|
10705
|
+
}
|
|
10702
10706
|
const cardParts = [];
|
|
10703
10707
|
if (scan.dlpFindings.length > 0) {
|
|
10704
10708
|
cardParts.push(
|
|
@@ -10727,10 +10731,10 @@ function registerScanCommand(program2) {
|
|
|
10727
10731
|
import_chalk5.default.red("\u{1F52D} ") + import_chalk5.default.red.bold(String(blastExposures)) + import_chalk5.default.dim(" exposures")
|
|
10728
10732
|
);
|
|
10729
10733
|
}
|
|
10730
|
-
if (cardParts.length > 0) {
|
|
10734
|
+
if (cardParts.length > 0 && !useInkForHero) {
|
|
10731
10735
|
console.log(" " + cardParts.join(import_chalk5.default.dim(" ")));
|
|
10732
10736
|
}
|
|
10733
|
-
if (scan.totalCostUSD > 0) {
|
|
10737
|
+
if (scan.totalCostUSD > 0 && !useInkForHero) {
|
|
10734
10738
|
console.log(
|
|
10735
10739
|
" " + import_chalk5.default.dim("AI spend ") + import_chalk5.default.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? import_chalk5.default.dim(" \xB7 wasted on loops ") + import_chalk5.default.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
|
|
10736
10740
|
);
|
|
@@ -10742,16 +10746,38 @@ function registerScanCommand(program2) {
|
|
|
10742
10746
|
)
|
|
10743
10747
|
);
|
|
10744
10748
|
}
|
|
10745
|
-
|
|
10749
|
+
if (!useInkForHero) {
|
|
10750
|
+
console.log("");
|
|
10751
|
+
}
|
|
10746
10752
|
if (!drillDown) {
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10753
|
+
const useInk2 = !options.classic;
|
|
10754
|
+
if (useInk2) {
|
|
10755
|
+
const scanInkPath = import_path21.default.join(__dirname, "scan-ink.mjs");
|
|
10756
|
+
const dynamicImport = new Function("id", "return import(id)");
|
|
10757
|
+
const mod = await dynamicImport(`file://${scanInkPath}`);
|
|
10758
|
+
const rangeLabel2 = options.all ? "all time" : `last ${options.days ?? 90} days`;
|
|
10759
|
+
mod.renderScanScorecardInk(
|
|
10760
|
+
{
|
|
10761
|
+
scan,
|
|
10762
|
+
summary,
|
|
10763
|
+
blast,
|
|
10764
|
+
blastExposures,
|
|
10765
|
+
blockedCount,
|
|
10766
|
+
reviewCount
|
|
10767
|
+
},
|
|
10768
|
+
rangeLabel2
|
|
10769
|
+
);
|
|
10770
|
+
console.log("");
|
|
10771
|
+
} else {
|
|
10772
|
+
renderPanelScorecard({
|
|
10773
|
+
scan,
|
|
10774
|
+
summary,
|
|
10775
|
+
blast,
|
|
10776
|
+
blastExposures,
|
|
10777
|
+
blockedCount,
|
|
10778
|
+
reviewCount
|
|
10779
|
+
});
|
|
10780
|
+
}
|
|
10755
10781
|
const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
|
|
10756
10782
|
console.log(" " + import_chalk5.default.green(cta));
|
|
10757
10783
|
console.log(
|
package/dist/cli.mjs
CHANGED
|
@@ -10485,7 +10485,7 @@ function originForRule(ruleName, sections) {
|
|
|
10485
10485
|
return "";
|
|
10486
10486
|
}
|
|
10487
10487
|
function registerScanCommand(program2) {
|
|
10488
|
-
program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option(
|
|
10488
|
+
program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option("--classic", "Original chalk-based scorecard layout (default: new Ink-rendered view)").option(
|
|
10489
10489
|
"--json",
|
|
10490
10490
|
"Emit machine-readable JSON to stdout (suppresses banner, progress, and renderer)"
|
|
10491
10491
|
).option(
|
|
@@ -10519,7 +10519,8 @@ function registerScanCommand(program2) {
|
|
|
10519
10519
|
})();
|
|
10520
10520
|
const isWired = getAgentsStatus().some((a) => a.wired);
|
|
10521
10521
|
const screenshotMode = options.compact || options.narrative;
|
|
10522
|
-
const
|
|
10522
|
+
const useInk = !options.classic && !drillDown;
|
|
10523
|
+
const quiet = screenshotMode || options.json || useInk;
|
|
10523
10524
|
if (!quiet) {
|
|
10524
10525
|
console.log("");
|
|
10525
10526
|
if (!isWired) {
|
|
@@ -10657,6 +10658,7 @@ function registerScanCommand(program2) {
|
|
|
10657
10658
|
});
|
|
10658
10659
|
return;
|
|
10659
10660
|
}
|
|
10661
|
+
const useInkForHero = !options.classic && !drillDown;
|
|
10660
10662
|
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
10661
10663
|
console.log(chalk5.green(" \u2705 No risky operations found in your history."));
|
|
10662
10664
|
console.log(
|
|
@@ -10676,9 +10678,11 @@ function registerScanCommand(program2) {
|
|
|
10676
10678
|
const since = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo} days ago`;
|
|
10677
10679
|
return chalk5.dim(" \xB7 ") + arrow + chalk5.dim(` since ${since}`);
|
|
10678
10680
|
})();
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10681
|
+
if (!useInkForHero) {
|
|
10682
|
+
console.log(
|
|
10683
|
+
" " + (score.band === "critical" ? chalk5.red.bold("\u26A0 ") : "") + chalk5.bold("Security Score ") + score.color.bold(`${blast.score}/100`) + " " + severityDisplay + trendSuffix + chalk5.dim(" \xB7 ") + (totalRisky > 0 ? chalk5.red.bold(`${totalRisky} risky operation${totalRisky !== 1 ? "s" : ""}`) : chalk5.green("No risky operations"))
|
|
10684
|
+
);
|
|
10685
|
+
}
|
|
10682
10686
|
const cardParts = [];
|
|
10683
10687
|
if (scan.dlpFindings.length > 0) {
|
|
10684
10688
|
cardParts.push(
|
|
@@ -10707,10 +10711,10 @@ function registerScanCommand(program2) {
|
|
|
10707
10711
|
chalk5.red("\u{1F52D} ") + chalk5.red.bold(String(blastExposures)) + chalk5.dim(" exposures")
|
|
10708
10712
|
);
|
|
10709
10713
|
}
|
|
10710
|
-
if (cardParts.length > 0) {
|
|
10714
|
+
if (cardParts.length > 0 && !useInkForHero) {
|
|
10711
10715
|
console.log(" " + cardParts.join(chalk5.dim(" ")));
|
|
10712
10716
|
}
|
|
10713
|
-
if (scan.totalCostUSD > 0) {
|
|
10717
|
+
if (scan.totalCostUSD > 0 && !useInkForHero) {
|
|
10714
10718
|
console.log(
|
|
10715
10719
|
" " + chalk5.dim("AI spend ") + chalk5.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? chalk5.dim(" \xB7 wasted on loops ") + chalk5.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
|
|
10716
10720
|
);
|
|
@@ -10722,16 +10726,38 @@ function registerScanCommand(program2) {
|
|
|
10722
10726
|
)
|
|
10723
10727
|
);
|
|
10724
10728
|
}
|
|
10725
|
-
|
|
10729
|
+
if (!useInkForHero) {
|
|
10730
|
+
console.log("");
|
|
10731
|
+
}
|
|
10726
10732
|
if (!drillDown) {
|
|
10727
|
-
|
|
10728
|
-
|
|
10729
|
-
|
|
10730
|
-
|
|
10731
|
-
|
|
10732
|
-
|
|
10733
|
-
|
|
10734
|
-
|
|
10733
|
+
const useInk2 = !options.classic;
|
|
10734
|
+
if (useInk2) {
|
|
10735
|
+
const scanInkPath = path21.join(__dirname, "scan-ink.mjs");
|
|
10736
|
+
const dynamicImport = new Function("id", "return import(id)");
|
|
10737
|
+
const mod = await dynamicImport(`file://${scanInkPath}`);
|
|
10738
|
+
const rangeLabel2 = options.all ? "all time" : `last ${options.days ?? 90} days`;
|
|
10739
|
+
mod.renderScanScorecardInk(
|
|
10740
|
+
{
|
|
10741
|
+
scan,
|
|
10742
|
+
summary,
|
|
10743
|
+
blast,
|
|
10744
|
+
blastExposures,
|
|
10745
|
+
blockedCount,
|
|
10746
|
+
reviewCount
|
|
10747
|
+
},
|
|
10748
|
+
rangeLabel2
|
|
10749
|
+
);
|
|
10750
|
+
console.log("");
|
|
10751
|
+
} else {
|
|
10752
|
+
renderPanelScorecard({
|
|
10753
|
+
scan,
|
|
10754
|
+
summary,
|
|
10755
|
+
blast,
|
|
10756
|
+
blastExposures,
|
|
10757
|
+
blockedCount,
|
|
10758
|
+
reviewCount
|
|
10759
|
+
});
|
|
10760
|
+
}
|
|
10735
10761
|
const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
|
|
10736
10762
|
console.log(" " + chalk5.green(cta));
|
|
10737
10763
|
console.log(
|