@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 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, and stop the dangerous stuff.</strong></p>
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, blocking the dangerous ones, and showing you what happened both live and in retrospect.
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
- - ๐Ÿ›‘ **Block** dangerous AI actions before they run โ€” `rm -rf`, `git push --force`, `DROP TABLE`, credential reads, `curl | bash`
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/25c601db-221d-4553-8b8c-34af85ab30c8" width="720" alt="Node9 monitor dashboard" />
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** โ€” blocks `git push --force`, `git reset --hard`, `git clean -fd`
101
- - **SQL** โ€” blocks `DELETE` / `UPDATE` without `WHERE`, `DROP TABLE`, `TRUNCATE`
102
- - **Shell** โ€” blocks `curl | bash`, unauthorized `sudo`
103
- - **DLP** โ€” blocks AWS keys, GitHub tokens, Stripe keys, PEM private keys in any tool argument, file contents, or shell config (`~/.zshrc`, `~/.bashrc`)
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 destructive ops; shields need review |
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 titleSegment = titlePad.length <= inner ? titlePad : titlePad.slice(0, inner);
7784
- const dashFill = "\u2500".repeat(Math.max(0, inner - titleSegment.length));
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.length;
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
- ["\u{1F441} ", import_chalk5.default.yellow],
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 titleSegment = titlePad.length <= inner ? titlePad : titlePad.slice(0, inner);
7760
- const dashFill = "\u2500".repeat(Math.max(0, inner - titleSegment.length));
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.length;
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
- ["\u{1F441} ", chalk5.yellow],
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(
@@ -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.0",
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": [