@node9/proxy 1.20.0 โ 1.20.1
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 +9 -9
- package/dist/cli.js +15 -8
- package/dist/cli.mjs +13 -6
- package/dist/dashboard.mjs +2 -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,13 +8,13 @@
|
|
|
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 โ recording every action,
|
|
11
|
+
Node9 sits between your AI agent and the tools it can use โ recording every action, intervening on risky ones, and showing you what happened both live and in retrospect.
|
|
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
|
-
-
|
|
17
|
+
- ๐ก **Review or block** risky commands before they run โ `rm -rf`, `git push --force`, `DROP TABLE`, credential reads, `curl | bash`
|
|
18
18
|
- ๐ **Scan** what your AI has already been doing โ loops, leaked secrets, blocked operations across every session
|
|
19
19
|
- ๐ **Catch credential leaks** โ AWS keys, GitHub tokens, JWTs, GCP API keys, PEM private keys flagged in tool args, file contents, and shell config
|
|
20
20
|
- ๐ญ **Map your blast radius** โ every SSH key, AWS credential, and `.env` file an AI agent on this machine could reach right now
|
|
@@ -22,7 +22,7 @@ Works with **Claude Code ยท Codex CLI ยท Gemini CLI ยท Cursor ยท Windsurf ยท any
|
|
|
22
22
|
## Live monitoring
|
|
23
23
|
|
|
24
24
|
<p align="center">
|
|
25
|
-
<img src="https://github.com/user-attachments/assets/
|
|
25
|
+
<img src="https://github.com/user-attachments/assets/997b7b42-b251-4046-b9c5-e000f8b5a481" width="720" alt="Node9 monitor dashboard" />
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
28
|
`node9 monitor` opens an interactive terminal dashboard with two views:
|
|
@@ -97,10 +97,10 @@ node9 shield list # show all shields + status
|
|
|
97
97
|
|
|
98
98
|
## Always on โ no config needed
|
|
99
99
|
|
|
100
|
-
- **Git** โ
|
|
101
|
-
- **SQL** โ
|
|
102
|
-
- **Shell** โ
|
|
103
|
-
- **DLP** โ
|
|
100
|
+
- **Git** โ catches `git push --force`, `git reset --hard`, `git clean -fd`
|
|
101
|
+
- **SQL** โ catches `DELETE` / `UPDATE` without `WHERE`, `DROP TABLE`, `TRUNCATE`
|
|
102
|
+
- **Shell** โ catches `curl | bash`, unauthorized `sudo`
|
|
103
|
+
- **DLP** โ flags AWS keys, GitHub tokens, Stripe keys, PEM private keys in any tool argument, file contents, or shell config (`~/.zshrc`, `~/.bashrc`)
|
|
104
104
|
- **Response DLP** โ background scanner reads Claude's conversation history and alerts you if Claude _wrote_ a secret in its response text
|
|
105
105
|
- **Auto-undo** โ git snapshot before every AI file edit โ `node9 undo` to revert
|
|
106
106
|
- **Skills pinning** โ SHA-256 verification of installed Claude skills / plugins between sessions
|
|
@@ -167,7 +167,7 @@ Node9 surfaces the signal. Here are the patterns worth knowing:
|
|
|
167
167
|
|
|
168
168
|
| Signal | Likely meaning |
|
|
169
169
|
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
170
|
-
| `Would have blocked` โฅ 5 in a week | Agent is attempting
|
|
170
|
+
| `Would have blocked` โฅ 5 in a week | Agent is attempting high-impact ops; shields are worth reviewing |
|
|
171
171
|
| Single `review-git-push` rule >50% of findings | Your own rule is firing as intended โ not a risk, just supervision |
|
|
172
172
|
| DLP finding in `user-prompt` tool | You pasted a secret into your own prompt โ rotate the key |
|
|
173
173
|
| 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(
|
|
@@ -10945,7 +10951,7 @@ function registerScanCommand(program2) {
|
|
|
10945
10951
|
}
|
|
10946
10952
|
);
|
|
10947
10953
|
}
|
|
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;
|
|
10954
|
+
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
10955
|
var init_scan = __esm({
|
|
10950
10956
|
"src/cli/commands/scan.ts"() {
|
|
10951
10957
|
"use strict";
|
|
@@ -10964,6 +10970,7 @@ var init_scan = __esm({
|
|
|
10964
10970
|
init_blast();
|
|
10965
10971
|
init_scan_derive();
|
|
10966
10972
|
init_protection();
|
|
10973
|
+
import_string_width2 = __toESM(require("string-width"));
|
|
10967
10974
|
init_scan_json();
|
|
10968
10975
|
init_scan_history();
|
|
10969
10976
|
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(
|
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)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.1",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"react": "^19.2.6",
|
|
84
84
|
"safe-regex2": "^5.1.0",
|
|
85
85
|
"smol-toml": "^1.6.1",
|
|
86
|
+
"string-width": "^4.2.3",
|
|
86
87
|
"zod": "^3.25.76"
|
|
87
88
|
},
|
|
88
89
|
"bundleDependencies": [
|