@node9/proxy 1.20.0 โ 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 +40 -41
- package/dist/cli.js +57 -24
- package/dist/cli.mjs +55 -22
- package/dist/dashboard.mjs +2 -0
- package/dist/scan-ink.mjs +1681 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<h1 align="center">๐ก๏ธ Node9</h1>
|
|
2
|
-
<p align="center"><strong>What did your AI agent actually do? Find out
|
|
2
|
+
<p align="center"><strong>What did your AI agent actually do? Find out.</strong></p>
|
|
3
3
|
<p align="center">
|
|
4
4
|
<a href="https://www.npmjs.com/package/node9-ai"><img src="https://img.shields.io/npm/v/node9-ai.svg" alt="npm version" /></a>
|
|
5
5
|
<a href="https://www.npmjs.com/package/node9-ai"><img src="https://img.shields.io/npm/dm/node9-ai.svg" alt="monthly downloads" /></a>
|
|
@@ -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,32 +42,19 @@ 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
|
|
|
41
|
-
```
|
|
42
|
-
|
|
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
|
|
53
|
+
```bash
|
|
54
|
+
node9 monitor # press [2] for Report view
|
|
55
|
+
node9 report --period 7d # CLI form, no TUI
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
Run it on yours โ `npx node9-ai scan` finishes in ~10 seconds and runs entirely local. Nothing uploads.
|
|
58
|
-
|
|
59
58
|
## Install
|
|
60
59
|
|
|
61
60
|
```bash
|
|
@@ -97,10 +96,10 @@ node9 shield list # show all shields + status
|
|
|
97
96
|
|
|
98
97
|
## Always on โ no config needed
|
|
99
98
|
|
|
100
|
-
- **Git** โ
|
|
101
|
-
- **SQL** โ
|
|
102
|
-
- **Shell** โ
|
|
103
|
-
- **DLP** โ
|
|
99
|
+
- **Git** โ catches `git push --force`, `git reset --hard`, `git clean -fd`
|
|
100
|
+
- **SQL** โ catches `DELETE` / `UPDATE` without `WHERE`, `DROP TABLE`, `TRUNCATE`
|
|
101
|
+
- **Shell** โ catches `curl | bash`, unauthorized `sudo`
|
|
102
|
+
- **DLP** โ flags AWS keys, GitHub tokens, Stripe keys, PEM private keys in any tool argument, file contents, or shell config (`~/.zshrc`, `~/.bashrc`)
|
|
104
103
|
- **Response DLP** โ background scanner reads Claude's conversation history and alerts you if Claude _wrote_ a secret in its response text
|
|
105
104
|
- **Auto-undo** โ git snapshot before every AI file edit โ `node9 undo` to revert
|
|
106
105
|
- **Skills pinning** โ SHA-256 verification of installed Claude skills / plugins between sessions
|
|
@@ -141,17 +140,17 @@ node9 mcp pin reset # clear all pins
|
|
|
141
140
|
|
|
142
141
|
</details>
|
|
143
142
|
|
|
144
|
-
##
|
|
143
|
+
## Other commands
|
|
144
|
+
|
|
145
|
+
Beyond the three flow commands above (`scan` / `monitor` / `report`):
|
|
145
146
|
|
|
146
|
-
| Command | What it shows | When to use
|
|
147
|
-
| ---------------- | --------------------------------------------------------- |
|
|
148
|
-
| `node9 blast` | What an AI agent can reach right now โ files, creds, env | First thing to run on any machine
|
|
149
|
-
| `node9
|
|
150
|
-
| `node9
|
|
151
|
-
| `node9
|
|
152
|
-
| `node9
|
|
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 |
|
|
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
|
|
|
@@ -167,7 +166,7 @@ Node9 surfaces the signal. Here are the patterns worth knowing:
|
|
|
167
166
|
|
|
168
167
|
| Signal | Likely meaning |
|
|
169
168
|
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
170
|
-
| `Would have blocked` โฅ 5 in a week | Agent is attempting
|
|
169
|
+
| `Would have blocked` โฅ 5 in a week | Agent is attempting high-impact ops; shields are worth reviewing |
|
|
171
170
|
| Single `review-git-push` rule >50% of findings | Your own rule is firing as intended โ not a risk, just supervision |
|
|
172
171
|
| DLP finding in `user-prompt` tool | You pasted a secret into your own prompt โ rotate the key |
|
|
173
172
|
| Agent Loop ร50+ on same file | Agent stuck in edit/test/fix cycle โ check context or slow down |
|
package/dist/cli.js
CHANGED
|
@@ -7780,8 +7780,9 @@ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
|
|
|
7780
7780
|
const inner = width - 4;
|
|
7781
7781
|
const out = [];
|
|
7782
7782
|
const titlePad = ` ${title} `;
|
|
7783
|
-
const
|
|
7784
|
-
const
|
|
7783
|
+
const titleWidth = (0, import_string_width.default)(titlePad);
|
|
7784
|
+
const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
|
|
7785
|
+
const dashFill = "\u2500".repeat(Math.max(0, inner - (0, import_string_width.default)(titleSegment)));
|
|
7785
7786
|
out.push(import_chalk3.default.dim("\u256D\u2500") + import_chalk3.default.bold(titleSegment) + import_chalk3.default.dim(`${dashFill}\u2500\u256E`));
|
|
7786
7787
|
for (const line of bodyLines) {
|
|
7787
7788
|
const padding = " ".repeat(Math.max(0, inner - line.width));
|
|
@@ -7798,11 +7799,12 @@ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
|
|
|
7798
7799
|
if (days > 90) return "90d+";
|
|
7799
7800
|
return `${days}d`;
|
|
7800
7801
|
}
|
|
7801
|
-
var import_chalk3, PANEL_WIDTH;
|
|
7802
|
+
var import_chalk3, import_string_width, PANEL_WIDTH;
|
|
7802
7803
|
var init_scan_derive = __esm({
|
|
7803
7804
|
"src/cli/render/scan-derive.ts"() {
|
|
7804
7805
|
"use strict";
|
|
7805
7806
|
import_chalk3 = __toESM(require("chalk"));
|
|
7807
|
+
import_string_width = __toESM(require("string-width"));
|
|
7806
7808
|
PANEL_WIDTH = 76;
|
|
7807
7809
|
}
|
|
7808
7810
|
});
|
|
@@ -10246,7 +10248,7 @@ function mkLine(...parts) {
|
|
|
10246
10248
|
let width = 0;
|
|
10247
10249
|
for (const [text, fmt] of parts) {
|
|
10248
10250
|
rendered += fmt ? fmt(text) : text;
|
|
10249
|
-
width += text
|
|
10251
|
+
width += (0, import_string_width2.default)(text);
|
|
10250
10252
|
}
|
|
10251
10253
|
return { rendered, width };
|
|
10252
10254
|
}
|
|
@@ -10367,7 +10369,11 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
|
|
|
10367
10369
|
const origin = originForRule(r.name, summary.sections);
|
|
10368
10370
|
reviewLines.push(
|
|
10369
10371
|
mkLine(
|
|
10370
|
-
|
|
10372
|
+
// VS-16 (U+FE0F) forces emoji-presentation so string-width
|
|
10373
|
+
// returns 2 cells (matching how modern terminals actually
|
|
10374
|
+
// render it). Without VS-16 string-width says 1 cell โ and
|
|
10375
|
+
// the right border drifts off. Same applies to ๐ก / โ below.
|
|
10376
|
+
["\u{1F441}\uFE0F ", import_chalk5.default.yellow],
|
|
10371
10377
|
[shortRule(r.name, 24), import_chalk5.default.bold],
|
|
10372
10378
|
[" \xD7" + String(r.count).padEnd(4), import_chalk5.default.bold],
|
|
10373
10379
|
[" "],
|
|
@@ -10425,7 +10431,7 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
|
|
|
10425
10431
|
}
|
|
10426
10432
|
for (const e of blast.envFindings.slice(0, 3)) {
|
|
10427
10433
|
blastLines.push(
|
|
10428
|
-
mkLine(["\u26A0 ", import_chalk5.default.yellow], [`${e.key} `], [`(${e.patternName})`, import_chalk5.default.dim])
|
|
10434
|
+
mkLine(["\u26A0\uFE0F ", import_chalk5.default.yellow], [`${e.key} `], [`(${e.patternName})`, import_chalk5.default.dim])
|
|
10429
10435
|
);
|
|
10430
10436
|
}
|
|
10431
10437
|
const totalExposed = blast.reachable.length + blast.envFindings.length;
|
|
@@ -10449,7 +10455,7 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
|
|
|
10449
10455
|
if (impact.totalCatches === 0) continue;
|
|
10450
10456
|
const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
|
|
10451
10457
|
const bonus = Math.round(exposed * discount);
|
|
10452
|
-
const icon = discount > 0 ? "\u{1F6E1} " : "\u2610 ";
|
|
10458
|
+
const icon = discount > 0 ? "\u{1F6E1}\uFE0F " : "\u2610 ";
|
|
10453
10459
|
const wouldCatch = `would catch ${impact.totalCatches} op${impact.totalCatches !== 1 ? "s" : ""}`;
|
|
10454
10460
|
const deltaSuffix = bonus > 0 ? ` \u2192 +${bonus} pts (${blast.score} \u2192 ${blast.score + bonus})` : "";
|
|
10455
10461
|
shieldLines.push(
|
|
@@ -10499,7 +10505,7 @@ function originForRule(ruleName, sections) {
|
|
|
10499
10505
|
return "";
|
|
10500
10506
|
}
|
|
10501
10507
|
function registerScanCommand(program2) {
|
|
10502
|
-
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(
|
|
10503
10509
|
"--json",
|
|
10504
10510
|
"Emit machine-readable JSON to stdout (suppresses banner, progress, and renderer)"
|
|
10505
10511
|
).option(
|
|
@@ -10533,7 +10539,8 @@ function registerScanCommand(program2) {
|
|
|
10533
10539
|
})();
|
|
10534
10540
|
const isWired = getAgentsStatus().some((a) => a.wired);
|
|
10535
10541
|
const screenshotMode = options.compact || options.narrative;
|
|
10536
|
-
const
|
|
10542
|
+
const useInk = !options.classic && !drillDown;
|
|
10543
|
+
const quiet = screenshotMode || options.json || useInk;
|
|
10537
10544
|
if (!quiet) {
|
|
10538
10545
|
console.log("");
|
|
10539
10546
|
if (!isWired) {
|
|
@@ -10671,6 +10678,7 @@ function registerScanCommand(program2) {
|
|
|
10671
10678
|
});
|
|
10672
10679
|
return;
|
|
10673
10680
|
}
|
|
10681
|
+
const useInkForHero = !options.classic && !drillDown;
|
|
10674
10682
|
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
10675
10683
|
console.log(import_chalk5.default.green(" \u2705 No risky operations found in your history."));
|
|
10676
10684
|
console.log(
|
|
@@ -10690,9 +10698,11 @@ function registerScanCommand(program2) {
|
|
|
10690
10698
|
const since = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo} days ago`;
|
|
10691
10699
|
return import_chalk5.default.dim(" \xB7 ") + arrow + import_chalk5.default.dim(` since ${since}`);
|
|
10692
10700
|
})();
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
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
|
+
}
|
|
10696
10706
|
const cardParts = [];
|
|
10697
10707
|
if (scan.dlpFindings.length > 0) {
|
|
10698
10708
|
cardParts.push(
|
|
@@ -10721,10 +10731,10 @@ function registerScanCommand(program2) {
|
|
|
10721
10731
|
import_chalk5.default.red("\u{1F52D} ") + import_chalk5.default.red.bold(String(blastExposures)) + import_chalk5.default.dim(" exposures")
|
|
10722
10732
|
);
|
|
10723
10733
|
}
|
|
10724
|
-
if (cardParts.length > 0) {
|
|
10734
|
+
if (cardParts.length > 0 && !useInkForHero) {
|
|
10725
10735
|
console.log(" " + cardParts.join(import_chalk5.default.dim(" ")));
|
|
10726
10736
|
}
|
|
10727
|
-
if (scan.totalCostUSD > 0) {
|
|
10737
|
+
if (scan.totalCostUSD > 0 && !useInkForHero) {
|
|
10728
10738
|
console.log(
|
|
10729
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)) : "")
|
|
10730
10740
|
);
|
|
@@ -10736,16 +10746,38 @@ function registerScanCommand(program2) {
|
|
|
10736
10746
|
)
|
|
10737
10747
|
);
|
|
10738
10748
|
}
|
|
10739
|
-
|
|
10749
|
+
if (!useInkForHero) {
|
|
10750
|
+
console.log("");
|
|
10751
|
+
}
|
|
10740
10752
|
if (!drillDown) {
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
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
|
+
}
|
|
10749
10781
|
const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
|
|
10750
10782
|
console.log(" " + import_chalk5.default.green(cta));
|
|
10751
10783
|
console.log(
|
|
@@ -10945,7 +10977,7 @@ function registerScanCommand(program2) {
|
|
|
10945
10977
|
}
|
|
10946
10978
|
);
|
|
10947
10979
|
}
|
|
10948
|
-
var import_chalk5, import_fs19, import_path21, import_os18, CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
|
|
10980
|
+
var import_chalk5, import_fs19, import_path21, import_os18, import_string_width2, CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
|
|
10949
10981
|
var init_scan = __esm({
|
|
10950
10982
|
"src/cli/commands/scan.ts"() {
|
|
10951
10983
|
"use strict";
|
|
@@ -10964,6 +10996,7 @@ var init_scan = __esm({
|
|
|
10964
10996
|
init_blast();
|
|
10965
10997
|
init_scan_derive();
|
|
10966
10998
|
init_protection();
|
|
10999
|
+
import_string_width2 = __toESM(require("string-width"));
|
|
10967
11000
|
init_scan_json();
|
|
10968
11001
|
init_scan_history();
|
|
10969
11002
|
CLAUDE_PRICING = {
|
package/dist/cli.mjs
CHANGED
|
@@ -7708,6 +7708,7 @@ var init_blast = __esm({
|
|
|
7708
7708
|
|
|
7709
7709
|
// src/cli/render/scan-derive.ts
|
|
7710
7710
|
import chalk3 from "chalk";
|
|
7711
|
+
import stringWidth from "string-width";
|
|
7711
7712
|
function classifyScore(score) {
|
|
7712
7713
|
if (score >= 80) return { band: "good", label: "Good", color: chalk3.green };
|
|
7713
7714
|
if (score >= 50) return { band: "at-risk", label: "At Risk", color: chalk3.yellow };
|
|
@@ -7756,8 +7757,9 @@ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
|
|
|
7756
7757
|
const inner = width - 4;
|
|
7757
7758
|
const out = [];
|
|
7758
7759
|
const titlePad = ` ${title} `;
|
|
7759
|
-
const
|
|
7760
|
-
const
|
|
7760
|
+
const titleWidth = stringWidth(titlePad);
|
|
7761
|
+
const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
|
|
7762
|
+
const dashFill = "\u2500".repeat(Math.max(0, inner - stringWidth(titleSegment)));
|
|
7761
7763
|
out.push(chalk3.dim("\u256D\u2500") + chalk3.bold(titleSegment) + chalk3.dim(`${dashFill}\u2500\u256E`));
|
|
7762
7764
|
for (const line of bodyLines) {
|
|
7763
7765
|
const padding = " ".repeat(Math.max(0, inner - line.width));
|
|
@@ -8903,6 +8905,7 @@ import chalk5 from "chalk";
|
|
|
8903
8905
|
import fs19 from "fs";
|
|
8904
8906
|
import path21 from "path";
|
|
8905
8907
|
import os18 from "os";
|
|
8908
|
+
import stringWidth2 from "string-width";
|
|
8906
8909
|
function claudeModelPrice(model) {
|
|
8907
8910
|
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
8908
8911
|
for (const [key, p] of Object.entries(CLAUDE_PRICING)) {
|
|
@@ -10225,7 +10228,7 @@ function mkLine(...parts) {
|
|
|
10225
10228
|
let width = 0;
|
|
10226
10229
|
for (const [text, fmt] of parts) {
|
|
10227
10230
|
rendered += fmt ? fmt(text) : text;
|
|
10228
|
-
width += text
|
|
10231
|
+
width += stringWidth2(text);
|
|
10229
10232
|
}
|
|
10230
10233
|
return { rendered, width };
|
|
10231
10234
|
}
|
|
@@ -10346,7 +10349,11 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
|
|
|
10346
10349
|
const origin = originForRule(r.name, summary.sections);
|
|
10347
10350
|
reviewLines.push(
|
|
10348
10351
|
mkLine(
|
|
10349
|
-
|
|
10352
|
+
// VS-16 (U+FE0F) forces emoji-presentation so string-width
|
|
10353
|
+
// returns 2 cells (matching how modern terminals actually
|
|
10354
|
+
// render it). Without VS-16 string-width says 1 cell โ and
|
|
10355
|
+
// the right border drifts off. Same applies to ๐ก / โ below.
|
|
10356
|
+
["\u{1F441}\uFE0F ", chalk5.yellow],
|
|
10350
10357
|
[shortRule(r.name, 24), chalk5.bold],
|
|
10351
10358
|
[" \xD7" + String(r.count).padEnd(4), chalk5.bold],
|
|
10352
10359
|
[" "],
|
|
@@ -10404,7 +10411,7 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
|
|
|
10404
10411
|
}
|
|
10405
10412
|
for (const e of blast.envFindings.slice(0, 3)) {
|
|
10406
10413
|
blastLines.push(
|
|
10407
|
-
mkLine(["\u26A0 ", chalk5.yellow], [`${e.key} `], [`(${e.patternName})`, chalk5.dim])
|
|
10414
|
+
mkLine(["\u26A0\uFE0F ", chalk5.yellow], [`${e.key} `], [`(${e.patternName})`, chalk5.dim])
|
|
10408
10415
|
);
|
|
10409
10416
|
}
|
|
10410
10417
|
const totalExposed = blast.reachable.length + blast.envFindings.length;
|
|
@@ -10428,7 +10435,7 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
|
|
|
10428
10435
|
if (impact.totalCatches === 0) continue;
|
|
10429
10436
|
const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
|
|
10430
10437
|
const bonus = Math.round(exposed * discount);
|
|
10431
|
-
const icon = discount > 0 ? "\u{1F6E1} " : "\u2610 ";
|
|
10438
|
+
const icon = discount > 0 ? "\u{1F6E1}\uFE0F " : "\u2610 ";
|
|
10432
10439
|
const wouldCatch = `would catch ${impact.totalCatches} op${impact.totalCatches !== 1 ? "s" : ""}`;
|
|
10433
10440
|
const deltaSuffix = bonus > 0 ? ` \u2192 +${bonus} pts (${blast.score} \u2192 ${blast.score + bonus})` : "";
|
|
10434
10441
|
shieldLines.push(
|
|
@@ -10478,7 +10485,7 @@ function originForRule(ruleName, sections) {
|
|
|
10478
10485
|
return "";
|
|
10479
10486
|
}
|
|
10480
10487
|
function registerScanCommand(program2) {
|
|
10481
|
-
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(
|
|
10482
10489
|
"--json",
|
|
10483
10490
|
"Emit machine-readable JSON to stdout (suppresses banner, progress, and renderer)"
|
|
10484
10491
|
).option(
|
|
@@ -10512,7 +10519,8 @@ function registerScanCommand(program2) {
|
|
|
10512
10519
|
})();
|
|
10513
10520
|
const isWired = getAgentsStatus().some((a) => a.wired);
|
|
10514
10521
|
const screenshotMode = options.compact || options.narrative;
|
|
10515
|
-
const
|
|
10522
|
+
const useInk = !options.classic && !drillDown;
|
|
10523
|
+
const quiet = screenshotMode || options.json || useInk;
|
|
10516
10524
|
if (!quiet) {
|
|
10517
10525
|
console.log("");
|
|
10518
10526
|
if (!isWired) {
|
|
@@ -10650,6 +10658,7 @@ function registerScanCommand(program2) {
|
|
|
10650
10658
|
});
|
|
10651
10659
|
return;
|
|
10652
10660
|
}
|
|
10661
|
+
const useInkForHero = !options.classic && !drillDown;
|
|
10653
10662
|
if (totalFindings === 0 && scan.dlpFindings.length === 0) {
|
|
10654
10663
|
console.log(chalk5.green(" \u2705 No risky operations found in your history."));
|
|
10655
10664
|
console.log(
|
|
@@ -10669,9 +10678,11 @@ function registerScanCommand(program2) {
|
|
|
10669
10678
|
const since = daysAgo === 0 ? "today" : daysAgo === 1 ? "yesterday" : `${daysAgo} days ago`;
|
|
10670
10679
|
return chalk5.dim(" \xB7 ") + arrow + chalk5.dim(` since ${since}`);
|
|
10671
10680
|
})();
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
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
|
+
}
|
|
10675
10686
|
const cardParts = [];
|
|
10676
10687
|
if (scan.dlpFindings.length > 0) {
|
|
10677
10688
|
cardParts.push(
|
|
@@ -10700,10 +10711,10 @@ function registerScanCommand(program2) {
|
|
|
10700
10711
|
chalk5.red("\u{1F52D} ") + chalk5.red.bold(String(blastExposures)) + chalk5.dim(" exposures")
|
|
10701
10712
|
);
|
|
10702
10713
|
}
|
|
10703
|
-
if (cardParts.length > 0) {
|
|
10714
|
+
if (cardParts.length > 0 && !useInkForHero) {
|
|
10704
10715
|
console.log(" " + cardParts.join(chalk5.dim(" ")));
|
|
10705
10716
|
}
|
|
10706
|
-
if (scan.totalCostUSD > 0) {
|
|
10717
|
+
if (scan.totalCostUSD > 0 && !useInkForHero) {
|
|
10707
10718
|
console.log(
|
|
10708
10719
|
" " + chalk5.dim("AI spend ") + chalk5.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? chalk5.dim(" \xB7 wasted on loops ") + chalk5.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
|
|
10709
10720
|
);
|
|
@@ -10715,16 +10726,38 @@ function registerScanCommand(program2) {
|
|
|
10715
10726
|
)
|
|
10716
10727
|
);
|
|
10717
10728
|
}
|
|
10718
|
-
|
|
10729
|
+
if (!useInkForHero) {
|
|
10730
|
+
console.log("");
|
|
10731
|
+
}
|
|
10719
10732
|
if (!drillDown) {
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
|
|
10723
|
-
|
|
10724
|
-
|
|
10725
|
-
|
|
10726
|
-
|
|
10727
|
-
|
|
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
|
+
}
|
|
10728
10761
|
const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
|
|
10729
10762
|
console.log(" " + chalk5.green(cta));
|
|
10730
10763
|
console.log(
|
package/dist/dashboard.mjs
CHANGED
|
@@ -3308,6 +3308,7 @@ var init_setup = __esm({
|
|
|
3308
3308
|
|
|
3309
3309
|
// src/cli/render/scan-derive.ts
|
|
3310
3310
|
import chalk3 from "chalk";
|
|
3311
|
+
import stringWidth from "string-width";
|
|
3311
3312
|
var init_scan_derive = __esm({
|
|
3312
3313
|
"src/cli/render/scan-derive.ts"() {
|
|
3313
3314
|
"use strict";
|
|
@@ -3334,6 +3335,7 @@ import chalk4 from "chalk";
|
|
|
3334
3335
|
import fs5 from "fs";
|
|
3335
3336
|
import path7 from "path";
|
|
3336
3337
|
import os7 from "os";
|
|
3338
|
+
import stringWidth2 from "string-width";
|
|
3337
3339
|
function claudeModelPrice2(model) {
|
|
3338
3340
|
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
3339
3341
|
for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
|