@node9/proxy 1.9.0 → 1.9.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/dist/cli.mjs CHANGED
@@ -139,8 +139,8 @@ function sanitizeConfig(raw) {
139
139
  }
140
140
  }
141
141
  const lines = result.error.issues.map((issue) => {
142
- const path31 = issue.path.length > 0 ? issue.path.join(".") : "root";
143
- return ` \u2022 ${path31}: ${issue.message}`;
142
+ const path32 = issue.path.length > 0 ? issue.path.join(".") : "root";
143
+ return ` \u2022 ${path32}: ${issue.message}`;
144
144
  });
145
145
  return {
146
146
  sanitized,
@@ -806,7 +806,7 @@ var init_config = __esm({
806
806
  {
807
807
  field: "command",
808
808
  op: "matches",
809
- value: "^\\s*git\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
809
+ value: "\\bgit\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
810
810
  flags: "i"
811
811
  }
812
812
  ],
@@ -821,7 +821,7 @@ var init_config = __esm({
821
821
  {
822
822
  field: "command",
823
823
  op: "matches",
824
- value: "^\\s*git\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
824
+ value: "\\bgit\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
825
825
  flags: "i"
826
826
  }
827
827
  ],
@@ -836,7 +836,7 @@ var init_config = __esm({
836
836
  {
837
837
  field: "command",
838
838
  op: "matches",
839
- value: "^\\s*git\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
839
+ value: "\\bgit\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
840
840
  flags: "i"
841
841
  }
842
842
  ],
@@ -848,7 +848,7 @@ var init_config = __esm({
848
848
  {
849
849
  name: "review-sudo",
850
850
  tool: "bash",
851
- conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
851
+ conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
852
852
  conditionMode: "all",
853
853
  verdict: "review",
854
854
  reason: "Command requires elevated privileges"
@@ -1674,9 +1674,9 @@ function matchesPattern(text, patterns) {
1674
1674
  const withoutDotSlash = text.replace(/^\.\//, "");
1675
1675
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1676
1676
  }
1677
- function getNestedValue(obj, path31) {
1677
+ function getNestedValue(obj, path32) {
1678
1678
  if (!obj || typeof obj !== "object") return null;
1679
- return path31.split(".").reduce((prev, curr) => prev?.[curr], obj);
1679
+ return path32.split(".").reduce((prev, curr) => prev?.[curr], obj);
1680
1680
  }
1681
1681
  function shouldSnapshot(toolName, args, config) {
1682
1682
  if (!config.settings.enableUndo) return false;
@@ -6747,10 +6747,10 @@ __export(tail_exports, {
6747
6747
  startTail: () => startTail
6748
6748
  });
6749
6749
  import http2 from "http";
6750
- import chalk17 from "chalk";
6751
- import fs25 from "fs";
6752
- import os21 from "os";
6753
- import path28 from "path";
6750
+ import chalk18 from "chalk";
6751
+ import fs26 from "fs";
6752
+ import os22 from "os";
6753
+ import path29 from "path";
6754
6754
  import readline5 from "readline";
6755
6755
  import { spawn as spawn10, execSync as execSync3 } from "child_process";
6756
6756
  function getIcon(tool) {
@@ -6764,22 +6764,22 @@ function formatBase(activity) {
6764
6764
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
6765
6765
  const icon = getIcon(activity.tool);
6766
6766
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6767
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os21.homedir(), "~");
6767
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os22.homedir(), "~");
6768
6768
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6769
- return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
6769
+ return `${chalk18.gray(time)} ${icon} ${chalk18.white.bold(toolName)} ${chalk18.dim(argsPreview)}`;
6770
6770
  }
6771
6771
  function renderResult(activity, result) {
6772
6772
  const base = formatBase(activity);
6773
6773
  let status;
6774
6774
  if (result.status === "allow") {
6775
- status = chalk17.green("\u2713 ALLOW");
6775
+ status = chalk18.green("\u2713 ALLOW");
6776
6776
  } else if (result.status === "dlp") {
6777
- status = chalk17.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6777
+ status = chalk18.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6778
6778
  } else {
6779
- status = chalk17.red("\u2717 BLOCK");
6779
+ status = chalk18.red("\u2717 BLOCK");
6780
6780
  }
6781
6781
  const cost = result.costEstimate ?? activity.costEstimate;
6782
- const costSuffix = cost == null ? "" : chalk17.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
6782
+ const costSuffix = cost == null ? "" : chalk18.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
6783
6783
  if (process.stdout.isTTY) {
6784
6784
  readline5.clearLine(process.stdout, 0);
6785
6785
  readline5.cursorTo(process.stdout, 0);
@@ -6788,16 +6788,16 @@ function renderResult(activity, result) {
6788
6788
  }
6789
6789
  function renderPending(activity) {
6790
6790
  if (!process.stdout.isTTY) return;
6791
- process.stdout.write(`${formatBase(activity)} ${chalk17.yellow("\u25CF \u2026")}\r`);
6791
+ process.stdout.write(`${formatBase(activity)} ${chalk18.yellow("\u25CF \u2026")}\r`);
6792
6792
  }
6793
6793
  async function ensureDaemon() {
6794
6794
  let pidPort = null;
6795
- if (fs25.existsSync(PID_FILE)) {
6795
+ if (fs26.existsSync(PID_FILE)) {
6796
6796
  try {
6797
- const { port } = JSON.parse(fs25.readFileSync(PID_FILE, "utf-8"));
6797
+ const { port } = JSON.parse(fs26.readFileSync(PID_FILE, "utf-8"));
6798
6798
  pidPort = port;
6799
6799
  } catch {
6800
- console.error(chalk17.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6800
+ console.error(chalk18.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6801
6801
  }
6802
6802
  }
6803
6803
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -6808,7 +6808,7 @@ async function ensureDaemon() {
6808
6808
  if (res.ok) return checkPort;
6809
6809
  } catch {
6810
6810
  }
6811
- console.log(chalk17.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6811
+ console.log(chalk18.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6812
6812
  const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
6813
6813
  detached: true,
6814
6814
  stdio: "ignore",
@@ -6825,7 +6825,7 @@ async function ensureDaemon() {
6825
6825
  } catch {
6826
6826
  }
6827
6827
  }
6828
- console.error(chalk17.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6828
+ console.error(chalk18.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6829
6829
  process.exit(1);
6830
6830
  }
6831
6831
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
@@ -6914,9 +6914,9 @@ function buildRecoveryCardLines(req) {
6914
6914
  ];
6915
6915
  }
6916
6916
  function readApproversFromDisk() {
6917
- const configPath = path28.join(os21.homedir(), ".node9", "config.json");
6917
+ const configPath = path29.join(os22.homedir(), ".node9", "config.json");
6918
6918
  try {
6919
- const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
6919
+ const raw = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
6920
6920
  const settings = raw.settings ?? {};
6921
6921
  return settings.approvers ?? {};
6922
6922
  } catch {
@@ -6927,20 +6927,20 @@ function approverStatusLine() {
6927
6927
  const a = readApproversFromDisk();
6928
6928
  const fmt = (label, key) => {
6929
6929
  const on = a[key] !== false;
6930
- return `[${key[0]}]${label.slice(1)} ${on ? chalk17.green("\u2713") : chalk17.dim("\u2717")}`;
6930
+ return `[${key[0]}]${label.slice(1)} ${on ? chalk18.green("\u2713") : chalk18.dim("\u2717")}`;
6931
6931
  };
6932
6932
  return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
6933
6933
  }
6934
6934
  function toggleApprover(channel) {
6935
- const configPath = path28.join(os21.homedir(), ".node9", "config.json");
6935
+ const configPath = path29.join(os22.homedir(), ".node9", "config.json");
6936
6936
  try {
6937
- const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
6937
+ const raw = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
6938
6938
  const settings = raw.settings ?? {};
6939
6939
  const approvers = settings.approvers ?? {};
6940
6940
  approvers[channel] = approvers[channel] === false;
6941
6941
  settings.approvers = approvers;
6942
6942
  raw.settings = settings;
6943
- fs25.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
6943
+ fs26.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
6944
6944
  } catch (err2) {
6945
6945
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
6946
6946
  `);
@@ -6972,7 +6972,7 @@ async function startTail(options = {}) {
6972
6972
  req2.end();
6973
6973
  });
6974
6974
  if (result.ok) {
6975
- console.log(chalk17.green("\u2713 Flight Recorder buffer cleared."));
6975
+ console.log(chalk18.green("\u2713 Flight Recorder buffer cleared."));
6976
6976
  } else if (result.code === "ECONNREFUSED") {
6977
6977
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
6978
6978
  } else if (result.code === "ETIMEDOUT") {
@@ -7016,7 +7016,7 @@ async function startTail(options = {}) {
7016
7016
  const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7017
7017
  if (channel) {
7018
7018
  toggleApprover(channel);
7019
- console.log(chalk17.dim(` Approvers: ${approverStatusLine()}`));
7019
+ console.log(chalk18.dim(` Approvers: ${approverStatusLine()}`));
7020
7020
  }
7021
7021
  };
7022
7022
  process.stdin.on("keypress", idleKeypressHandler);
@@ -7082,7 +7082,7 @@ async function startTail(options = {}) {
7082
7082
  localAllowCounts.get(req2.toolName) ?? 0
7083
7083
  )
7084
7084
  );
7085
- const decisionStamp = action === "always-allow" ? chalk17.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk17.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk17.green("\u2713 ALLOWED") : action === "redirect" ? chalk17.yellow("\u21A9 REDIRECT AI") : chalk17.red("\u2717 DENIED");
7085
+ const decisionStamp = action === "always-allow" ? chalk18.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk18.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk18.green("\u2713 ALLOWED") : action === "redirect" ? chalk18.yellow("\u21A9 REDIRECT AI") : chalk18.red("\u2717 DENIED");
7086
7086
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
7087
7087
  for (const line of stampedLines) process.stdout.write(line + "\n");
7088
7088
  process.stdout.write(SHOW_CURSOR);
@@ -7110,8 +7110,8 @@ async function startTail(options = {}) {
7110
7110
  }
7111
7111
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7112
7112
  try {
7113
- fs25.appendFileSync(
7114
- path28.join(os21.homedir(), ".node9", "hook-debug.log"),
7113
+ fs26.appendFileSync(
7114
+ path29.join(os22.homedir(), ".node9", "hook-debug.log"),
7115
7115
  `[tail] POST /decision failed: ${String(err2)}
7116
7116
  `
7117
7117
  );
@@ -7133,7 +7133,7 @@ async function startTail(options = {}) {
7133
7133
  );
7134
7134
  const stampedLines = buildCardLines(req2, priorCount);
7135
7135
  if (externalDecision) {
7136
- const source = externalDecision === "allow" ? chalk17.green("\u2713 ALLOWED") : chalk17.red("\u2717 DENIED");
7136
+ const source = externalDecision === "allow" ? chalk18.green("\u2713 ALLOWED") : chalk18.red("\u2717 DENIED");
7137
7137
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
7138
7138
  }
7139
7139
  for (const line of stampedLines) process.stdout.write(line + "\n");
@@ -7192,16 +7192,16 @@ async function startTail(options = {}) {
7192
7192
  }
7193
7193
  } catch {
7194
7194
  }
7195
- console.log(chalk17.cyan.bold(`
7196
- \u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
7195
+ console.log(chalk18.cyan.bold(`
7196
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk18.dim(`\u2192 ${dashboardUrl}`));
7197
7197
  if (canApprove) {
7198
- console.log(chalk17.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7199
- console.log(chalk17.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7198
+ console.log(chalk18.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7199
+ console.log(chalk18.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7200
7200
  }
7201
7201
  if (options.history) {
7202
- console.log(chalk17.dim("Showing history + live events.\n"));
7202
+ console.log(chalk18.dim("Showing history + live events.\n"));
7203
7203
  } else {
7204
- console.log(chalk17.dim("Showing live events only. Use --history to include past.\n"));
7204
+ console.log(chalk18.dim("Showing live events only. Use --history to include past.\n"));
7205
7205
  }
7206
7206
  process.on("SIGINT", () => {
7207
7207
  exitIdleMode();
@@ -7211,13 +7211,13 @@ async function startTail(options = {}) {
7211
7211
  readline5.clearLine(process.stdout, 0);
7212
7212
  readline5.cursorTo(process.stdout, 0);
7213
7213
  }
7214
- console.log(chalk17.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7214
+ console.log(chalk18.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7215
7215
  process.exit(0);
7216
7216
  });
7217
7217
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
7218
7218
  const req = http2.get(sseUrl, (res) => {
7219
7219
  if (res.statusCode !== 200) {
7220
- console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
7220
+ console.error(chalk18.red(`Failed to connect: HTTP ${res.statusCode}`));
7221
7221
  process.exit(1);
7222
7222
  }
7223
7223
  if (canApprove) enterIdleMode();
@@ -7248,7 +7248,7 @@ async function startTail(options = {}) {
7248
7248
  readline5.clearLine(process.stdout, 0);
7249
7249
  readline5.cursorTo(process.stdout, 0);
7250
7250
  }
7251
- console.log(chalk17.red("\n\u274C Daemon disconnected."));
7251
+ console.log(chalk18.red("\n\u274C Daemon disconnected."));
7252
7252
  process.exit(1);
7253
7253
  });
7254
7254
  });
@@ -7340,9 +7340,9 @@ async function startTail(options = {}) {
7340
7340
  const hash = data.hash ?? "";
7341
7341
  const summary = data.argsSummary ?? data.tool;
7342
7342
  const fileCount = data.fileCount ?? 0;
7343
- const files = fileCount > 0 ? chalk17.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7343
+ const files = fileCount > 0 ? chalk18.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7344
7344
  process.stdout.write(
7345
- `${chalk17.dim(time)} ${chalk17.cyan("\u{1F4F8} snapshot")} ${chalk17.dim(hash)} ${summary}${files}
7345
+ `${chalk18.dim(time)} ${chalk18.cyan("\u{1F4F8} snapshot")} ${chalk18.dim(hash)} ${summary}${files}
7346
7346
  `
7347
7347
  );
7348
7348
  return;
@@ -7359,7 +7359,7 @@ async function startTail(options = {}) {
7359
7359
  }
7360
7360
  req.on("error", (err2) => {
7361
7361
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
7362
- console.error(chalk17.red(`
7362
+ console.error(chalk18.red(`
7363
7363
  \u274C ${msg}`));
7364
7364
  process.exit(1);
7365
7365
  });
@@ -7371,7 +7371,7 @@ var init_tail = __esm({
7371
7371
  init_daemon2();
7372
7372
  init_daemon();
7373
7373
  init_core();
7374
- PID_FILE = path28.join(os21.homedir(), ".node9", "daemon.pid");
7374
+ PID_FILE = path29.join(os22.homedir(), ".node9", "daemon.pid");
7375
7375
  ICONS = {
7376
7376
  bash: "\u{1F4BB}",
7377
7377
  shell: "\u{1F4BB}",
@@ -7410,9 +7410,9 @@ __export(hud_exports, {
7410
7410
  main: () => main,
7411
7411
  renderEnvironmentLine: () => renderEnvironmentLine
7412
7412
  });
7413
- import fs26 from "fs";
7414
- import path29 from "path";
7415
- import os22 from "os";
7413
+ import fs27 from "fs";
7414
+ import path30 from "path";
7415
+ import os23 from "os";
7416
7416
  import http3 from "http";
7417
7417
  async function readStdin() {
7418
7418
  const chunks = [];
@@ -7488,9 +7488,9 @@ function formatTimeLeft(resetsAt) {
7488
7488
  return ` (${m}m left)`;
7489
7489
  }
7490
7490
  function safeReadJson(filePath) {
7491
- if (!fs26.existsSync(filePath)) return null;
7491
+ if (!fs27.existsSync(filePath)) return null;
7492
7492
  try {
7493
- return JSON.parse(fs26.readFileSync(filePath, "utf-8"));
7493
+ return JSON.parse(fs27.readFileSync(filePath, "utf-8"));
7494
7494
  } catch {
7495
7495
  return null;
7496
7496
  }
@@ -7511,12 +7511,12 @@ function countHooksInFile(filePath) {
7511
7511
  return Object.keys(cfg.hooks).length;
7512
7512
  }
7513
7513
  function countRulesInDir(rulesDir) {
7514
- if (!fs26.existsSync(rulesDir)) return 0;
7514
+ if (!fs27.existsSync(rulesDir)) return 0;
7515
7515
  let count = 0;
7516
7516
  try {
7517
- for (const entry of fs26.readdirSync(rulesDir, { withFileTypes: true })) {
7517
+ for (const entry of fs27.readdirSync(rulesDir, { withFileTypes: true })) {
7518
7518
  if (entry.isDirectory()) {
7519
- count += countRulesInDir(path29.join(rulesDir, entry.name));
7519
+ count += countRulesInDir(path30.join(rulesDir, entry.name));
7520
7520
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
7521
7521
  count++;
7522
7522
  }
@@ -7527,46 +7527,46 @@ function countRulesInDir(rulesDir) {
7527
7527
  }
7528
7528
  function isSamePath(a, b) {
7529
7529
  try {
7530
- return path29.resolve(a) === path29.resolve(b);
7530
+ return path30.resolve(a) === path30.resolve(b);
7531
7531
  } catch {
7532
7532
  return false;
7533
7533
  }
7534
7534
  }
7535
7535
  function countConfigs(cwd) {
7536
- const homeDir2 = os22.homedir();
7537
- const claudeDir = path29.join(homeDir2, ".claude");
7536
+ const homeDir2 = os23.homedir();
7537
+ const claudeDir = path30.join(homeDir2, ".claude");
7538
7538
  let claudeMdCount = 0;
7539
7539
  let rulesCount = 0;
7540
7540
  let hooksCount = 0;
7541
7541
  const userMcpServers = /* @__PURE__ */ new Set();
7542
7542
  const projectMcpServers = /* @__PURE__ */ new Set();
7543
- if (fs26.existsSync(path29.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7544
- rulesCount += countRulesInDir(path29.join(claudeDir, "rules"));
7545
- const userSettings = path29.join(claudeDir, "settings.json");
7543
+ if (fs27.existsSync(path30.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7544
+ rulesCount += countRulesInDir(path30.join(claudeDir, "rules"));
7545
+ const userSettings = path30.join(claudeDir, "settings.json");
7546
7546
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7547
7547
  hooksCount += countHooksInFile(userSettings);
7548
- const userClaudeJson = path29.join(homeDir2, ".claude.json");
7548
+ const userClaudeJson = path30.join(homeDir2, ".claude.json");
7549
7549
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7550
7550
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7551
7551
  userMcpServers.delete(name);
7552
7552
  }
7553
7553
  if (cwd) {
7554
- if (fs26.existsSync(path29.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7555
- if (fs26.existsSync(path29.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7556
- const projectClaudeDir = path29.join(cwd, ".claude");
7554
+ if (fs27.existsSync(path30.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7555
+ if (fs27.existsSync(path30.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7556
+ const projectClaudeDir = path30.join(cwd, ".claude");
7557
7557
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7558
7558
  if (!overlapsUserScope) {
7559
- if (fs26.existsSync(path29.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7560
- rulesCount += countRulesInDir(path29.join(projectClaudeDir, "rules"));
7561
- const projSettings = path29.join(projectClaudeDir, "settings.json");
7559
+ if (fs27.existsSync(path30.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7560
+ rulesCount += countRulesInDir(path30.join(projectClaudeDir, "rules"));
7561
+ const projSettings = path30.join(projectClaudeDir, "settings.json");
7562
7562
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7563
7563
  hooksCount += countHooksInFile(projSettings);
7564
7564
  }
7565
- if (fs26.existsSync(path29.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7566
- const localSettings = path29.join(projectClaudeDir, "settings.local.json");
7565
+ if (fs27.existsSync(path30.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7566
+ const localSettings = path30.join(projectClaudeDir, "settings.local.json");
7567
7567
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7568
7568
  hooksCount += countHooksInFile(localSettings);
7569
- const mcpJsonServers = getMcpServerNames(path29.join(cwd, ".mcp.json"));
7569
+ const mcpJsonServers = getMcpServerNames(path30.join(cwd, ".mcp.json"));
7570
7570
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7571
7571
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7572
7572
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -7599,12 +7599,12 @@ function readActiveShieldsHud() {
7599
7599
  return shieldsCache.value;
7600
7600
  }
7601
7601
  try {
7602
- const shieldsPath = path29.join(os22.homedir(), ".node9", "shields.json");
7603
- if (!fs26.existsSync(shieldsPath)) {
7602
+ const shieldsPath = path30.join(os23.homedir(), ".node9", "shields.json");
7603
+ if (!fs27.existsSync(shieldsPath)) {
7604
7604
  shieldsCache = { value: [], ts: now };
7605
7605
  return [];
7606
7606
  }
7607
- const parsed = JSON.parse(fs26.readFileSync(shieldsPath, "utf-8"));
7607
+ const parsed = JSON.parse(fs27.readFileSync(shieldsPath, "utf-8"));
7608
7608
  if (!Array.isArray(parsed.active)) {
7609
7609
  shieldsCache = { value: [], ts: now };
7610
7610
  return [];
@@ -7706,17 +7706,17 @@ function renderContextLine(stdin) {
7706
7706
  async function main() {
7707
7707
  try {
7708
7708
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
7709
- if (fs26.existsSync(path29.join(os22.homedir(), ".node9", "hud-debug"))) {
7709
+ if (fs27.existsSync(path30.join(os23.homedir(), ".node9", "hud-debug"))) {
7710
7710
  try {
7711
- const logPath = path29.join(os22.homedir(), ".node9", "hud-debug.log");
7711
+ const logPath = path30.join(os23.homedir(), ".node9", "hud-debug.log");
7712
7712
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
7713
7713
  let size = 0;
7714
7714
  try {
7715
- size = fs26.statSync(logPath).size;
7715
+ size = fs27.statSync(logPath).size;
7716
7716
  } catch {
7717
7717
  }
7718
7718
  if (size < MAX_LOG_SIZE) {
7719
- fs26.appendFileSync(
7719
+ fs27.appendFileSync(
7720
7720
  logPath,
7721
7721
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
7722
7722
  );
@@ -7737,11 +7737,11 @@ async function main() {
7737
7737
  try {
7738
7738
  const cwd = stdin.cwd ?? process.cwd();
7739
7739
  for (const configPath of [
7740
- path29.join(cwd, "node9.config.json"),
7741
- path29.join(os22.homedir(), ".node9", "config.json")
7740
+ path30.join(cwd, "node9.config.json"),
7741
+ path30.join(os23.homedir(), ".node9", "config.json")
7742
7742
  ]) {
7743
- if (!fs26.existsSync(configPath)) continue;
7744
- const cfg = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
7743
+ if (!fs27.existsSync(configPath)) continue;
7744
+ const cfg = JSON.parse(fs27.readFileSync(configPath, "utf-8"));
7745
7745
  const hud = cfg.settings?.hud;
7746
7746
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7747
7747
  }
@@ -8370,10 +8370,10 @@ function teardownHud() {
8370
8370
 
8371
8371
  // src/cli.ts
8372
8372
  init_daemon2();
8373
- import chalk18 from "chalk";
8374
- import fs27 from "fs";
8375
- import path30 from "path";
8376
- import os23 from "os";
8373
+ import chalk19 from "chalk";
8374
+ import fs28 from "fs";
8375
+ import path31 from "path";
8376
+ import os24 from "os";
8377
8377
  import { confirm as confirm2 } from "@inquirer/prompts";
8378
8378
 
8379
8379
  // src/utils/duration.ts
@@ -10263,11 +10263,17 @@ function registerInitCommand(program2) {
10263
10263
  if (sendTelemetry) fireTelemetryPing(found);
10264
10264
  console.log("");
10265
10265
  }
10266
- console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
10266
+ const agentList = found.join(", ");
10267
+ console.log(chalk11.green.bold(`\u{1F6E1}\uFE0F Node9 is protecting ${agentList}!`));
10268
+ console.log("");
10269
+ console.log(chalk11.white(" Watch live: ") + chalk11.cyan("node9 tail"));
10270
+ console.log(chalk11.white(" Local UI: ") + chalk11.cyan("node9 daemon --openui"));
10267
10271
  console.log("");
10268
- console.log(chalk11.white(" Start watching: ") + chalk11.cyan("node9 tail"));
10269
- console.log(chalk11.white(" Browser view: ") + chalk11.cyan("node9 daemon --openui"));
10270
- console.log(chalk11.white(" Cloud dashboard: ") + chalk11.cyan("node9.ai"));
10272
+ console.log(chalk11.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10273
+ console.log(
10274
+ chalk11.white(" Team dashboard + full audit trail \u2192 ") + chalk11.cyan.bold("https://node9.ai")
10275
+ );
10276
+ console.log(chalk11.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
10271
10277
  });
10272
10278
  }
10273
10279
 
@@ -10625,6 +10631,90 @@ import chalk15 from "chalk";
10625
10631
  import { spawn as spawn9 } from "child_process";
10626
10632
  import { execa as execa2 } from "execa";
10627
10633
  init_provenance();
10634
+
10635
+ // src/mcp-pin.ts
10636
+ import fs24 from "fs";
10637
+ import path27 from "path";
10638
+ import os20 from "os";
10639
+ import crypto3 from "crypto";
10640
+ function getPinsFilePath() {
10641
+ return path27.join(os20.homedir(), ".node9", "mcp-pins.json");
10642
+ }
10643
+ function hashToolDefinitions(tools) {
10644
+ const sorted = [...tools].sort((a, b) => {
10645
+ const nameA = a.name ?? "";
10646
+ const nameB = b.name ?? "";
10647
+ return nameA.localeCompare(nameB);
10648
+ });
10649
+ const canonical = JSON.stringify(sorted);
10650
+ return crypto3.createHash("sha256").update(canonical).digest("hex");
10651
+ }
10652
+ function getServerKey(upstreamCommand) {
10653
+ return crypto3.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
10654
+ }
10655
+ function readMcpPinsSafe() {
10656
+ const filePath = getPinsFilePath();
10657
+ try {
10658
+ const raw = fs24.readFileSync(filePath, "utf-8");
10659
+ if (!raw.trim()) {
10660
+ return { ok: false, reason: "corrupt", detail: "empty file" };
10661
+ }
10662
+ const parsed = JSON.parse(raw);
10663
+ if (!parsed.servers || typeof parsed.servers !== "object" || Array.isArray(parsed.servers)) {
10664
+ return { ok: false, reason: "corrupt", detail: "invalid structure: missing servers object" };
10665
+ }
10666
+ return { ok: true, pins: { servers: parsed.servers } };
10667
+ } catch (err2) {
10668
+ if (err2.code === "ENOENT") {
10669
+ return { ok: false, reason: "missing" };
10670
+ }
10671
+ return { ok: false, reason: "corrupt", detail: String(err2) };
10672
+ }
10673
+ }
10674
+ function readMcpPins() {
10675
+ const result = readMcpPinsSafe();
10676
+ if (result.ok) return result.pins;
10677
+ if (result.reason === "missing") return { servers: {} };
10678
+ throw new Error(`[node9] MCP pin file is corrupt: ${result.detail}`);
10679
+ }
10680
+ function writeMcpPins(data) {
10681
+ const filePath = getPinsFilePath();
10682
+ fs24.mkdirSync(path27.dirname(filePath), { recursive: true });
10683
+ const tmp = `${filePath}.${crypto3.randomBytes(6).toString("hex")}.tmp`;
10684
+ fs24.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
10685
+ fs24.renameSync(tmp, filePath);
10686
+ }
10687
+ function checkPin(serverKey, currentHash) {
10688
+ const result = readMcpPinsSafe();
10689
+ if (!result.ok) {
10690
+ if (result.reason === "missing") return "new";
10691
+ return "corrupt";
10692
+ }
10693
+ const entry = result.pins.servers[serverKey];
10694
+ if (!entry) return "new";
10695
+ return entry.toolsHash === currentHash ? "match" : "mismatch";
10696
+ }
10697
+ function updatePin(serverKey, label, toolsHash, toolNames) {
10698
+ const pins = readMcpPins();
10699
+ pins.servers[serverKey] = {
10700
+ label,
10701
+ toolsHash,
10702
+ toolNames,
10703
+ toolCount: toolNames.length,
10704
+ pinnedAt: (/* @__PURE__ */ new Date()).toISOString()
10705
+ };
10706
+ writeMcpPins(pins);
10707
+ }
10708
+ function removePin(serverKey) {
10709
+ const pins = readMcpPins();
10710
+ delete pins.servers[serverKey];
10711
+ writeMcpPins(pins);
10712
+ }
10713
+ function clearAllPins() {
10714
+ writeMcpPins({ servers: {} });
10715
+ }
10716
+
10717
+ // src/mcp-gateway/index.ts
10628
10718
  function sanitize4(value) {
10629
10719
  return value.replace(/[\x00-\x1F\x7F]/g, "");
10630
10720
  }
@@ -10718,6 +10808,10 @@ async function runMcpGateway(upstreamCommand) {
10718
10808
  let authPending = false;
10719
10809
  let deferredExitCode = null;
10720
10810
  let deferredStdinEnd = false;
10811
+ const pendingToolsListIds = /* @__PURE__ */ new Set();
10812
+ const serverKey = getServerKey(upstreamCommand);
10813
+ let pinState = "pending";
10814
+ const pendingToolCalls = [];
10721
10815
  const agentIn = readline3.createInterface({ input: process.stdin, terminal: false });
10722
10816
  agentIn.on("line", async (line) => {
10723
10817
  let message;
@@ -10740,8 +10834,43 @@ async function runMcpGateway(upstreamCommand) {
10740
10834
  child.stdin.write(line + "\n");
10741
10835
  return;
10742
10836
  }
10837
+ if (message.method === "tools/list" && message.id !== void 0 && message.id !== null) {
10838
+ pendingToolsListIds.add(message.id);
10839
+ }
10743
10840
  if (message.method === "tools/call" || message.method === "call_tool" || message.method === "use_tool") {
10744
- agentIn.pause();
10841
+ if (pinState === "quarantined") {
10842
+ if (message.id === void 0 || message.id === null) return;
10843
+ const errorResponse = {
10844
+ jsonrpc: "2.0",
10845
+ id: message.id,
10846
+ error: {
10847
+ code: RPC_SERVER_ERROR,
10848
+ message: `Node9 Security: This MCP session is quarantined due to a tool definition mismatch or corrupt pin state. The human operator must review and approve changes before tool calls are allowed. Run: node9 mcp pin update ${serverKey}`,
10849
+ data: { reason: "pin-quarantine", serverKey, pinState }
10850
+ }
10851
+ };
10852
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
10853
+ return;
10854
+ }
10855
+ if (pinState === "pending") {
10856
+ if (pendingToolsListIds.size > 0) {
10857
+ pendingToolCalls.push(line);
10858
+ return;
10859
+ }
10860
+ if (message.id === void 0 || message.id === null) return;
10861
+ const errorResponse = {
10862
+ jsonrpc: "2.0",
10863
+ id: message.id,
10864
+ error: {
10865
+ code: RPC_SERVER_ERROR,
10866
+ message: "Node9 Security: Tool calls are blocked until MCP tool definitions have been verified. The client must issue a tools/list request before calling tools.",
10867
+ data: { reason: "pin-quarantine", serverKey, pinState }
10868
+ }
10869
+ };
10870
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
10871
+ return;
10872
+ }
10873
+ if (!deferredStdinEnd) agentIn.pause();
10745
10874
  authPending = true;
10746
10875
  try {
10747
10876
  const toolName = sanitize4(
@@ -10803,9 +10932,100 @@ async function runMcpGateway(upstreamCommand) {
10803
10932
  }
10804
10933
  child.stdin.write(line + "\n");
10805
10934
  });
10806
- child.stdout.pipe(process.stdout);
10935
+ function drainPendingToolCalls() {
10936
+ if (pendingToolCalls.length === 0) {
10937
+ if (deferredStdinEnd && !authPending) child.stdin.end();
10938
+ return;
10939
+ }
10940
+ const lines = pendingToolCalls.splice(0);
10941
+ for (const queuedLine of lines) {
10942
+ agentIn.emit("line", queuedLine);
10943
+ }
10944
+ if (deferredStdinEnd && !authPending) child.stdin.end();
10945
+ }
10946
+ const upstreamOut = readline3.createInterface({ input: child.stdout, terminal: false });
10947
+ upstreamOut.on("line", (line) => {
10948
+ let parsed;
10949
+ try {
10950
+ parsed = JSON.parse(line);
10951
+ } catch {
10952
+ }
10953
+ if (!parsed) {
10954
+ process.stdout.write(line + "\n");
10955
+ return;
10956
+ }
10957
+ if (parsed.id !== void 0 && pendingToolsListIds.has(parsed.id)) {
10958
+ pendingToolsListIds.delete(parsed.id);
10959
+ if (parsed.result && Array.isArray(parsed.result.tools)) {
10960
+ const tools = parsed.result.tools;
10961
+ const currentHash = hashToolDefinitions(tools);
10962
+ const pinStatus = checkPin(serverKey, currentHash);
10963
+ if (pinStatus === "new") {
10964
+ const toolNames = tools.map((t) => t.name ?? "unknown").sort();
10965
+ updatePin(serverKey, upstreamCommand, currentHash, toolNames);
10966
+ pinState = "validated";
10967
+ console.error(
10968
+ chalk15.green(
10969
+ `\u{1F512} Node9: Pinned ${toolNames.length} tool definition(s) for this MCP server`
10970
+ )
10971
+ );
10972
+ process.stdout.write(line + "\n");
10973
+ drainPendingToolCalls();
10974
+ } else if (pinStatus === "match") {
10975
+ pinState = "validated";
10976
+ process.stdout.write(line + "\n");
10977
+ drainPendingToolCalls();
10978
+ } else if (pinStatus === "corrupt") {
10979
+ pinState = "quarantined";
10980
+ console.error(
10981
+ chalk15.red("\n\u{1F6A8} Node9: MCP pin file is corrupt or unreadable \u2014 session quarantined!")
10982
+ );
10983
+ console.error(chalk15.red(" Tool calls are blocked until the pin file is repaired."));
10984
+ console.error(
10985
+ chalk15.yellow(` Run: node9 mcp pin reset (to clear and re-pin on next connect)
10986
+ `)
10987
+ );
10988
+ const errorResponse = {
10989
+ jsonrpc: "2.0",
10990
+ id: parsed.id,
10991
+ error: {
10992
+ code: RPC_SERVER_ERROR,
10993
+ message: "Node9 Security: MCP pin file is corrupt or unreadable. Tool definitions cannot be verified. Session quarantined. The human operator must repair or reset the pin file. Run: node9 mcp pin reset",
10994
+ data: { reason: "pin-file-corrupt", serverKey }
10995
+ }
10996
+ };
10997
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
10998
+ drainPendingToolCalls();
10999
+ } else {
11000
+ pinState = "quarantined";
11001
+ console.error(
11002
+ chalk15.red("\n\u{1F6A8} Node9: MCP tool definitions have changed since last verified!")
11003
+ );
11004
+ console.error(
11005
+ chalk15.red(" This could indicate a supply chain attack (tool poisoning / rug pull).")
11006
+ );
11007
+ console.error(chalk15.red(" Session quarantined \u2014 all tool calls blocked."));
11008
+ console.error(chalk15.yellow(` Run: node9 mcp pin update ${serverKey}
11009
+ `));
11010
+ const errorResponse = {
11011
+ jsonrpc: "2.0",
11012
+ id: parsed.id,
11013
+ error: {
11014
+ code: RPC_SERVER_ERROR,
11015
+ message: `Node9 Security: MCP server tool definitions have changed since they were last pinned. This could indicate a supply chain attack (tool poisoning / rug pull). Session quarantined \u2014 all tool calls are blocked. The human operator must review and approve the changes. Run: node9 mcp pin update ${serverKey}`,
11016
+ data: { reason: "tool-pin-mismatch", serverKey }
11017
+ }
11018
+ };
11019
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
11020
+ drainPendingToolCalls();
11021
+ }
11022
+ return;
11023
+ }
11024
+ }
11025
+ process.stdout.write(line + "\n");
11026
+ });
10807
11027
  process.stdin.on("close", () => {
10808
- if (authPending) {
11028
+ if (authPending || pendingToolCalls.length > 0) {
10809
11029
  deferredStdinEnd = true;
10810
11030
  } else {
10811
11031
  child.stdin.end();
@@ -10834,9 +11054,9 @@ function registerMcpGatewayCommand(program2) {
10834
11054
 
10835
11055
  // src/mcp-server/index.ts
10836
11056
  import readline4 from "readline";
10837
- import fs24 from "fs";
10838
- import os20 from "os";
10839
- import path27 from "path";
11057
+ import fs25 from "fs";
11058
+ import os21 from "os";
11059
+ import path28 from "path";
10840
11060
  init_core();
10841
11061
  init_daemon();
10842
11062
  init_shields();
@@ -10936,6 +11156,60 @@ var TOOLS = [
10936
11156
  },
10937
11157
  required: ["hash"]
10938
11158
  }
11159
+ },
11160
+ {
11161
+ name: "node9_audit_get",
11162
+ description: "Read recent entries from the node9 audit log (~/.node9/audit.log). Each entry shows timestamp, tool name, decision (allow/block/review), and agent. Use this to review what AI actions have been taken recently.",
11163
+ inputSchema: {
11164
+ type: "object",
11165
+ properties: {
11166
+ limit: {
11167
+ type: "number",
11168
+ description: "Number of recent entries to return (default: 20, max: 100)."
11169
+ }
11170
+ },
11171
+ required: []
11172
+ }
11173
+ },
11174
+ {
11175
+ name: "node9_policy_get",
11176
+ description: "Show all active smart rules in detail \u2014 name, tool, verdict, conditions, and reason. Includes default rules, shield rules, and any custom project rules. Use this to understand exactly what is being blocked or reviewed.",
11177
+ inputSchema: { type: "object", properties: {}, required: [] }
11178
+ },
11179
+ {
11180
+ name: "node9_rule_add",
11181
+ description: 'Add a new protective smart rule to the global node9 config (~/.node9/config.json). Rules can block or send dangerous commands for human review based on regex conditions. IMPORTANT: only "block" and "review" verdicts are permitted \u2014 "allow" rules are never accepted because they would weaken node9 security. Rules can only be added, never removed.',
11182
+ inputSchema: {
11183
+ type: "object",
11184
+ properties: {
11185
+ name: {
11186
+ type: "string",
11187
+ description: 'Unique rule name (e.g. "block-drop-prod-db").'
11188
+ },
11189
+ tool: {
11190
+ type: "string",
11191
+ description: 'Tool to match \u2014 "bash", "*", or a specific tool name.'
11192
+ },
11193
+ field: {
11194
+ type: "string",
11195
+ description: 'Field to inspect \u2014 "command" for bash, "sql" for database tools.'
11196
+ },
11197
+ pattern: {
11198
+ type: "string",
11199
+ description: "Regex pattern to match against the field."
11200
+ },
11201
+ verdict: {
11202
+ type: "string",
11203
+ enum: ["block", "review"],
11204
+ description: 'Action to take when the rule matches. Only "block" or "review" are allowed.'
11205
+ },
11206
+ reason: {
11207
+ type: "string",
11208
+ description: "Human-readable explanation shown when the rule triggers."
11209
+ }
11210
+ },
11211
+ required: ["name", "tool", "field", "pattern", "verdict", "reason"]
11212
+ }
10939
11213
  }
10940
11214
  ];
10941
11215
  function handleStatus() {
@@ -10957,13 +11231,13 @@ function handleStatus() {
10957
11231
  lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
10958
11232
  lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
10959
11233
  lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
10960
- const projectConfig = path27.join(process.cwd(), "node9.config.json");
10961
- const globalConfig = path27.join(os20.homedir(), ".node9", "config.json");
11234
+ const projectConfig = path28.join(process.cwd(), "node9.config.json");
11235
+ const globalConfig = path28.join(os21.homedir(), ".node9", "config.json");
10962
11236
  lines.push(
10963
- `Project config (node9.config.json): ${fs24.existsSync(projectConfig) ? "present" : "not found"}`
11237
+ `Project config (node9.config.json): ${fs25.existsSync(projectConfig) ? "present" : "not found"}`
10964
11238
  );
10965
11239
  lines.push(
10966
- `Global config (~/.node9/config.json): ${fs24.existsSync(globalConfig) ? "present" : "not found"}`
11240
+ `Global config (~/.node9/config.json): ${fs25.existsSync(globalConfig) ? "present" : "not found"}`
10967
11241
  );
10968
11242
  return lines.join("\n");
10969
11243
  }
@@ -11037,21 +11311,21 @@ function handleShieldDisable(args) {
11037
11311
  writeActiveShields(active.filter((s) => s !== name));
11038
11312
  return `Shield "${name}" disabled.`;
11039
11313
  }
11040
- var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
11314
+ var GLOBAL_CONFIG_PATH2 = path28.join(os21.homedir(), ".node9", "config.json");
11041
11315
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
11042
11316
  function readGlobalConfigRaw() {
11043
11317
  try {
11044
- if (fs24.existsSync(GLOBAL_CONFIG_PATH2)) {
11045
- return JSON.parse(fs24.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
11318
+ if (fs25.existsSync(GLOBAL_CONFIG_PATH2)) {
11319
+ return JSON.parse(fs25.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
11046
11320
  }
11047
11321
  } catch {
11048
11322
  }
11049
11323
  return {};
11050
11324
  }
11051
11325
  function writeGlobalConfigRaw(data) {
11052
- const dir = path27.dirname(GLOBAL_CONFIG_PATH2);
11053
- if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
11054
- fs24.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
11326
+ const dir = path28.dirname(GLOBAL_CONFIG_PATH2);
11327
+ if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
11328
+ fs25.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
11055
11329
  }
11056
11330
  function handleApproverList() {
11057
11331
  const config = getConfig();
@@ -11092,6 +11366,75 @@ function handleApproverSet(args) {
11092
11366
  const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
11093
11367
  return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
11094
11368
  }
11369
+ function handleAuditGet(args) {
11370
+ const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
11371
+ const auditPath = path28.join(os21.homedir(), ".node9", "audit.log");
11372
+ if (!fs25.existsSync(auditPath)) return "No audit log found.";
11373
+ const lines = fs25.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
11374
+ const recent = lines.slice(-limit);
11375
+ const entries = recent.map((line) => {
11376
+ try {
11377
+ const e = JSON.parse(line);
11378
+ return `${e.ts} ${String(e.tool).padEnd(20)} ${String(e.decision).padEnd(8)} ${e.agent ?? ""}`;
11379
+ } catch {
11380
+ return line;
11381
+ }
11382
+ });
11383
+ return `Last ${entries.length} audit entries:
11384
+
11385
+ ${entries.join("\n")}`;
11386
+ }
11387
+ function handlePolicyGet() {
11388
+ const config = getConfig();
11389
+ const rules = config.policy.smartRules;
11390
+ if (rules.length === 0) return "No smart rules active.";
11391
+ const lines = rules.map((r, i) => {
11392
+ const conditions = r.conditions.map((c) => `${c.field} ${c.op} "${c.value}"`).join(` ${r.conditionMode ?? "all"} `);
11393
+ return `[${i + 1}] ${r.name ?? "(unnamed)"} tool:${r.tool} verdict:${r.verdict}
11394
+ conditions: ${conditions}
11395
+ reason: ${r.reason ?? "\u2014"}`;
11396
+ });
11397
+ return `${rules.length} active smart rules:
11398
+
11399
+ ${lines.join("\n\n")}`;
11400
+ }
11401
+ function handleRuleAdd(args) {
11402
+ const name = args.name;
11403
+ const tool = args.tool;
11404
+ const field = args.field;
11405
+ const pattern = args.pattern;
11406
+ const verdict = args.verdict;
11407
+ const reason = args.reason;
11408
+ if (!["block", "review"].includes(verdict)) {
11409
+ throw new Error(
11410
+ 'verdict must be "block" or "review" \u2014 "allow" rules are not permitted as they would weaken node9 security'
11411
+ );
11412
+ }
11413
+ try {
11414
+ new RegExp(pattern);
11415
+ } catch {
11416
+ throw new Error(`Invalid regex pattern: ${pattern}`);
11417
+ }
11418
+ const raw = readGlobalConfigRaw();
11419
+ const policy = raw.policy ?? {};
11420
+ const smartRules = policy.smartRules ?? [];
11421
+ const existing = smartRules.find(
11422
+ (r) => typeof r === "object" && r !== null && r.name === name
11423
+ );
11424
+ if (existing) throw new Error(`A rule named "${name}" already exists.`);
11425
+ smartRules.push({
11426
+ name,
11427
+ tool,
11428
+ conditions: [{ field, op: "matches", value: pattern }],
11429
+ conditionMode: "all",
11430
+ verdict,
11431
+ reason
11432
+ });
11433
+ policy.smartRules = smartRules;
11434
+ raw.policy = policy;
11435
+ writeGlobalConfigRaw(raw);
11436
+ return `Rule "${name}" added to ~/.node9/config.json \u2014 verdict: ${verdict} when ${field} matches "${pattern}"`;
11437
+ }
11095
11438
  function handleUndoList() {
11096
11439
  const history = getSnapshotHistory();
11097
11440
  if (history.length === 0) {
@@ -11175,6 +11518,12 @@ function runMcpServer() {
11175
11518
  text = handleUndoList();
11176
11519
  } else if (toolName === "node9_undo_revert") {
11177
11520
  text = handleUndoRevert(toolArgs);
11521
+ } else if (toolName === "node9_audit_get") {
11522
+ text = handleAuditGet(toolArgs);
11523
+ } else if (toolName === "node9_policy_get") {
11524
+ text = handlePolicyGet();
11525
+ } else if (toolName === "node9_rule_add") {
11526
+ text = handleRuleAdd(toolArgs);
11178
11527
  } else {
11179
11528
  process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
11180
11529
  return;
@@ -11262,22 +11611,99 @@ function registerTrustCommand(program2) {
11262
11611
  });
11263
11612
  }
11264
11613
 
11614
+ // src/cli/commands/mcp-pin.ts
11615
+ import chalk17 from "chalk";
11616
+ function registerMcpPinCommand(program2) {
11617
+ const pinCmd = program2.command("mcp").description("Manage MCP server tool definition pinning (rug pull defense)");
11618
+ const pinSubCmd = pinCmd.command("pin").description("Manage pinned MCP server tool definitions");
11619
+ pinSubCmd.command("list").description("Show all pinned MCP servers and their tool definition hashes").action(() => {
11620
+ const result = readMcpPinsSafe();
11621
+ if (!result.ok) {
11622
+ if (result.reason === "missing") {
11623
+ console.log(chalk17.gray("\nNo MCP servers are pinned yet."));
11624
+ console.log(
11625
+ chalk17.gray("Pins are created automatically when the MCP gateway first connects.\n")
11626
+ );
11627
+ return;
11628
+ }
11629
+ console.error(chalk17.red(`
11630
+ \u274C Pin file is corrupt: ${result.detail}`));
11631
+ console.error(chalk17.yellow(" Run: node9 mcp pin reset\n"));
11632
+ process.exit(1);
11633
+ }
11634
+ const entries = Object.entries(result.pins.servers);
11635
+ if (entries.length === 0) {
11636
+ console.log(chalk17.gray("\nNo MCP servers are pinned yet."));
11637
+ console.log(
11638
+ chalk17.gray("Pins are created automatically when the MCP gateway first connects.\n")
11639
+ );
11640
+ return;
11641
+ }
11642
+ console.log(chalk17.bold("\n\u{1F512} Pinned MCP Servers\n"));
11643
+ for (const [key, entry] of entries) {
11644
+ console.log(` ${chalk17.cyan(key)} ${chalk17.gray(entry.label)}`);
11645
+ console.log(` Tools (${entry.toolCount}): ${chalk17.white(entry.toolNames.join(", "))}`);
11646
+ console.log(` Hash: ${chalk17.gray(entry.toolsHash.slice(0, 16))}...`);
11647
+ console.log(` Pinned: ${chalk17.gray(entry.pinnedAt)}`);
11648
+ console.log("");
11649
+ }
11650
+ });
11651
+ pinSubCmd.command("update <serverKey>").description(
11652
+ "Remove a pin so the next gateway connection re-pins with current tool definitions"
11653
+ ).action((serverKey) => {
11654
+ let pins;
11655
+ try {
11656
+ pins = readMcpPins();
11657
+ } catch {
11658
+ console.error(chalk17.red("\n\u274C Pin file is corrupt."));
11659
+ console.error(chalk17.yellow(" Run: node9 mcp pin reset\n"));
11660
+ process.exit(1);
11661
+ }
11662
+ if (!pins.servers[serverKey]) {
11663
+ console.error(chalk17.red(`
11664
+ \u274C No pin found for server key "${serverKey}"
11665
+ `));
11666
+ console.error(`Run ${chalk17.cyan("node9 mcp pin list")} to see pinned servers.
11667
+ `);
11668
+ process.exit(1);
11669
+ }
11670
+ const label = pins.servers[serverKey].label;
11671
+ removePin(serverKey);
11672
+ console.log(chalk17.green(`
11673
+ \u{1F513} Pin removed for ${chalk17.cyan(serverKey)}`));
11674
+ console.log(chalk17.gray(` Server: ${label}`));
11675
+ console.log(chalk17.gray(" Next connection will re-pin with current tool definitions.\n"));
11676
+ });
11677
+ pinSubCmd.command("reset").description("Clear all MCP pins (next connection to each server will re-pin)").action(() => {
11678
+ const result = readMcpPinsSafe();
11679
+ if (!result.ok && result.reason === "missing") {
11680
+ console.log(chalk17.gray("\nNo pins to clear.\n"));
11681
+ return;
11682
+ }
11683
+ const count = result.ok ? Object.keys(result.pins.servers).length : "?";
11684
+ clearAllPins();
11685
+ console.log(chalk17.green(`
11686
+ \u{1F513} Cleared ${count} MCP pin(s).`));
11687
+ console.log(chalk17.gray(" Next connection to each server will re-pin.\n"));
11688
+ });
11689
+ }
11690
+
11265
11691
  // src/cli.ts
11266
11692
  var { version } = JSON.parse(
11267
- fs27.readFileSync(path30.join(__dirname, "../package.json"), "utf-8")
11693
+ fs28.readFileSync(path31.join(__dirname, "../package.json"), "utf-8")
11268
11694
  );
11269
11695
  var program = new Command();
11270
11696
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
11271
11697
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
11272
11698
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
11273
- const credPath = path30.join(os23.homedir(), ".node9", "credentials.json");
11274
- if (!fs27.existsSync(path30.dirname(credPath)))
11275
- fs27.mkdirSync(path30.dirname(credPath), { recursive: true });
11699
+ const credPath = path31.join(os24.homedir(), ".node9", "credentials.json");
11700
+ if (!fs28.existsSync(path31.dirname(credPath)))
11701
+ fs28.mkdirSync(path31.dirname(credPath), { recursive: true });
11276
11702
  const profileName = options.profile || "default";
11277
11703
  let existingCreds = {};
11278
11704
  try {
11279
- if (fs27.existsSync(credPath)) {
11280
- const raw = JSON.parse(fs27.readFileSync(credPath, "utf-8"));
11705
+ if (fs28.existsSync(credPath)) {
11706
+ const raw = JSON.parse(fs28.readFileSync(credPath, "utf-8"));
11281
11707
  if (raw.apiKey) {
11282
11708
  existingCreds = {
11283
11709
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -11289,13 +11715,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
11289
11715
  } catch {
11290
11716
  }
11291
11717
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
11292
- fs27.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
11718
+ fs28.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
11293
11719
  if (profileName === "default") {
11294
- const configPath = path30.join(os23.homedir(), ".node9", "config.json");
11720
+ const configPath = path31.join(os24.homedir(), ".node9", "config.json");
11295
11721
  let config = {};
11296
11722
  try {
11297
- if (fs27.existsSync(configPath))
11298
- config = JSON.parse(fs27.readFileSync(configPath, "utf-8"));
11723
+ if (fs28.existsSync(configPath))
11724
+ config = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
11299
11725
  } catch {
11300
11726
  }
11301
11727
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -11310,19 +11736,19 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
11310
11736
  approvers.cloud = false;
11311
11737
  }
11312
11738
  s.approvers = approvers;
11313
- if (!fs27.existsSync(path30.dirname(configPath)))
11314
- fs27.mkdirSync(path30.dirname(configPath), { recursive: true });
11315
- fs27.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
11739
+ if (!fs28.existsSync(path31.dirname(configPath)))
11740
+ fs28.mkdirSync(path31.dirname(configPath), { recursive: true });
11741
+ fs28.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
11316
11742
  }
11317
11743
  if (options.profile && profileName !== "default") {
11318
- console.log(chalk18.green(`\u2705 Profile "${profileName}" saved`));
11319
- console.log(chalk18.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
11744
+ console.log(chalk19.green(`\u2705 Profile "${profileName}" saved`));
11745
+ console.log(chalk19.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
11320
11746
  } else if (options.local) {
11321
- console.log(chalk18.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
11322
- console.log(chalk18.gray(` All decisions stay on this machine.`));
11747
+ console.log(chalk19.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
11748
+ console.log(chalk19.gray(` All decisions stay on this machine.`));
11323
11749
  } else {
11324
- console.log(chalk18.green(`\u2705 Logged in \u2014 agent mode`));
11325
- console.log(chalk18.gray(` Team policy enforced for all calls via Node9 cloud.`));
11750
+ console.log(chalk19.green(`\u2705 Logged in \u2014 agent mode`));
11751
+ console.log(chalk19.gray(` Team policy enforced for all calls via Node9 cloud.`));
11326
11752
  }
11327
11753
  });
11328
11754
  program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("<target>", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
@@ -11330,19 +11756,19 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
11330
11756
  if (target === "claude") return await setupClaude();
11331
11757
  if (target === "cursor") return await setupCursor();
11332
11758
  if (target === "hud") return setupHud();
11333
- console.error(chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
11759
+ console.error(chalk19.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
11334
11760
  process.exit(1);
11335
11761
  });
11336
11762
  program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("[target]", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
11337
11763
  if (!target) {
11338
- console.log(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
11339
- console.log(" Usage: " + chalk18.white("node9 setup <target>") + "\n");
11764
+ console.log(chalk19.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
11765
+ console.log(" Usage: " + chalk19.white("node9 setup <target>") + "\n");
11340
11766
  console.log(" Targets:");
11341
- console.log(" " + chalk18.green("claude") + " \u2014 Claude Code (hook mode)");
11342
- console.log(" " + chalk18.green("gemini") + " \u2014 Gemini CLI (hook mode)");
11343
- console.log(" " + chalk18.green("cursor") + " \u2014 Cursor (hook mode)");
11767
+ console.log(" " + chalk19.green("claude") + " \u2014 Claude Code (hook mode)");
11768
+ console.log(" " + chalk19.green("gemini") + " \u2014 Gemini CLI (hook mode)");
11769
+ console.log(" " + chalk19.green("cursor") + " \u2014 Cursor (hook mode)");
11344
11770
  process.stdout.write(
11345
- " " + chalk18.green("hud") + " \u2014 Claude Code security statusline\n"
11771
+ " " + chalk19.green("hud") + " \u2014 Claude Code security statusline\n"
11346
11772
  );
11347
11773
  console.log("");
11348
11774
  return;
@@ -11352,7 +11778,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
11352
11778
  if (t === "claude") return await setupClaude();
11353
11779
  if (t === "cursor") return await setupCursor();
11354
11780
  if (t === "hud") return setupHud();
11355
- console.error(chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
11781
+ console.error(chalk19.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
11356
11782
  process.exit(1);
11357
11783
  });
11358
11784
  program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
@@ -11363,31 +11789,31 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
11363
11789
  else if (target === "hud") fn = teardownHud;
11364
11790
  else {
11365
11791
  console.error(
11366
- chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
11792
+ chalk19.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
11367
11793
  );
11368
11794
  process.exit(1);
11369
11795
  }
11370
- console.log(chalk18.cyan(`
11796
+ console.log(chalk19.cyan(`
11371
11797
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
11372
11798
  `));
11373
11799
  try {
11374
11800
  fn();
11375
11801
  } catch (err2) {
11376
- console.error(chalk18.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
11802
+ console.error(chalk19.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
11377
11803
  process.exit(1);
11378
11804
  }
11379
- console.log(chalk18.gray("\n Restart the agent for changes to take effect."));
11805
+ console.log(chalk19.gray("\n Restart the agent for changes to take effect."));
11380
11806
  });
11381
11807
  program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
11382
- console.log(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
11383
- console.log(chalk18.bold("Stopping daemon..."));
11808
+ console.log(chalk19.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
11809
+ console.log(chalk19.bold("Stopping daemon..."));
11384
11810
  try {
11385
11811
  stopDaemon();
11386
- console.log(chalk18.green(" \u2705 Daemon stopped"));
11812
+ console.log(chalk19.green(" \u2705 Daemon stopped"));
11387
11813
  } catch {
11388
- console.log(chalk18.blue(" \u2139\uFE0F Daemon was not running"));
11814
+ console.log(chalk19.blue(" \u2139\uFE0F Daemon was not running"));
11389
11815
  }
11390
- console.log(chalk18.bold("\nRemoving hooks..."));
11816
+ console.log(chalk19.bold("\nRemoving hooks..."));
11391
11817
  let teardownFailed = false;
11392
11818
  for (const [label, fn] of [
11393
11819
  ["Claude", teardownClaude],
@@ -11399,45 +11825,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
11399
11825
  } catch (err2) {
11400
11826
  teardownFailed = true;
11401
11827
  console.error(
11402
- chalk18.red(
11828
+ chalk19.red(
11403
11829
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
11404
11830
  )
11405
11831
  );
11406
11832
  }
11407
11833
  }
11408
11834
  if (options.purge) {
11409
- const node9Dir = path30.join(os23.homedir(), ".node9");
11410
- if (fs27.existsSync(node9Dir)) {
11835
+ const node9Dir = path31.join(os24.homedir(), ".node9");
11836
+ if (fs28.existsSync(node9Dir)) {
11411
11837
  const confirmed = await confirm2({
11412
11838
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
11413
11839
  default: false
11414
11840
  });
11415
11841
  if (confirmed) {
11416
- fs27.rmSync(node9Dir, { recursive: true });
11417
- if (fs27.existsSync(node9Dir)) {
11842
+ fs28.rmSync(node9Dir, { recursive: true });
11843
+ if (fs28.existsSync(node9Dir)) {
11418
11844
  console.error(
11419
- chalk18.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
11845
+ chalk19.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
11420
11846
  );
11421
11847
  } else {
11422
- console.log(chalk18.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
11848
+ console.log(chalk19.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
11423
11849
  }
11424
11850
  } else {
11425
- console.log(chalk18.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
11851
+ console.log(chalk19.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
11426
11852
  }
11427
11853
  } else {
11428
- console.log(chalk18.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
11854
+ console.log(chalk19.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
11429
11855
  }
11430
11856
  } else {
11431
11857
  console.log(
11432
- chalk18.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
11858
+ chalk19.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
11433
11859
  );
11434
11860
  }
11435
11861
  if (teardownFailed) {
11436
- console.error(chalk18.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
11862
+ console.error(chalk19.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
11437
11863
  process.exit(1);
11438
11864
  }
11439
- console.log(chalk18.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
11440
- console.log(chalk18.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
11865
+ console.log(chalk19.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
11866
+ console.log(chalk19.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
11441
11867
  });
11442
11868
  registerDoctorCommand(program, version);
11443
11869
  program.command("explain").description(
@@ -11450,7 +11876,7 @@ program.command("explain").description(
11450
11876
  try {
11451
11877
  args = JSON.parse(trimmed);
11452
11878
  } catch {
11453
- console.error(chalk18.red(`
11879
+ console.error(chalk19.red(`
11454
11880
  \u274C Invalid JSON: ${trimmed}
11455
11881
  `));
11456
11882
  process.exit(1);
@@ -11461,54 +11887,54 @@ program.command("explain").description(
11461
11887
  }
11462
11888
  const result = await explainPolicy(tool, args);
11463
11889
  console.log("");
11464
- console.log(chalk18.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
11890
+ console.log(chalk19.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
11465
11891
  console.log("");
11466
- console.log(` ${chalk18.bold("Tool:")} ${chalk18.white(result.tool)}`);
11892
+ console.log(` ${chalk19.bold("Tool:")} ${chalk19.white(result.tool)}`);
11467
11893
  if (argsRaw) {
11468
11894
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
11469
- console.log(` ${chalk18.bold("Input:")} ${chalk18.gray(preview)}`);
11895
+ console.log(` ${chalk19.bold("Input:")} ${chalk19.gray(preview)}`);
11470
11896
  }
11471
11897
  console.log("");
11472
- console.log(chalk18.bold("Config Sources (Waterfall):"));
11898
+ console.log(chalk19.bold("Config Sources (Waterfall):"));
11473
11899
  for (const tier of result.waterfall) {
11474
- const num = chalk18.gray(` ${tier.tier}.`);
11900
+ const num = chalk19.gray(` ${tier.tier}.`);
11475
11901
  const label = tier.label.padEnd(16);
11476
11902
  let statusStr;
11477
11903
  if (tier.tier === 1) {
11478
- statusStr = chalk18.gray(tier.note ?? "");
11904
+ statusStr = chalk19.gray(tier.note ?? "");
11479
11905
  } else if (tier.status === "active") {
11480
- const loc = tier.path ? chalk18.gray(tier.path) : "";
11481
- const note = tier.note ? chalk18.gray(`(${tier.note})`) : "";
11482
- statusStr = chalk18.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
11906
+ const loc = tier.path ? chalk19.gray(tier.path) : "";
11907
+ const note = tier.note ? chalk19.gray(`(${tier.note})`) : "";
11908
+ statusStr = chalk19.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
11483
11909
  } else {
11484
- statusStr = chalk18.gray("\u25CB " + (tier.note ?? "not found"));
11910
+ statusStr = chalk19.gray("\u25CB " + (tier.note ?? "not found"));
11485
11911
  }
11486
- console.log(`${num} ${chalk18.white(label)} ${statusStr}`);
11912
+ console.log(`${num} ${chalk19.white(label)} ${statusStr}`);
11487
11913
  }
11488
11914
  console.log("");
11489
- console.log(chalk18.bold("Policy Evaluation:"));
11915
+ console.log(chalk19.bold("Policy Evaluation:"));
11490
11916
  for (const step of result.steps) {
11491
11917
  const isFinal = step.isFinal;
11492
11918
  let icon;
11493
- if (step.outcome === "allow") icon = chalk18.green(" \u2705");
11494
- else if (step.outcome === "review") icon = chalk18.red(" \u{1F534}");
11495
- else if (step.outcome === "skip") icon = chalk18.gray(" \u2500 ");
11496
- else icon = chalk18.gray(" \u25CB ");
11919
+ if (step.outcome === "allow") icon = chalk19.green(" \u2705");
11920
+ else if (step.outcome === "review") icon = chalk19.red(" \u{1F534}");
11921
+ else if (step.outcome === "skip") icon = chalk19.gray(" \u2500 ");
11922
+ else icon = chalk19.gray(" \u25CB ");
11497
11923
  const name = step.name.padEnd(18);
11498
- const nameStr = isFinal ? chalk18.white.bold(name) : chalk18.white(name);
11499
- const detail = isFinal ? chalk18.white(step.detail) : chalk18.gray(step.detail);
11500
- const arrow = isFinal ? chalk18.yellow(" \u2190 STOP") : "";
11924
+ const nameStr = isFinal ? chalk19.white.bold(name) : chalk19.white(name);
11925
+ const detail = isFinal ? chalk19.white(step.detail) : chalk19.gray(step.detail);
11926
+ const arrow = isFinal ? chalk19.yellow(" \u2190 STOP") : "";
11501
11927
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
11502
11928
  }
11503
11929
  console.log("");
11504
11930
  if (result.decision === "allow") {
11505
- console.log(chalk18.green.bold(" Decision: \u2705 ALLOW") + chalk18.gray(" \u2014 no approval needed"));
11931
+ console.log(chalk19.green.bold(" Decision: \u2705 ALLOW") + chalk19.gray(" \u2014 no approval needed"));
11506
11932
  } else {
11507
11933
  console.log(
11508
- chalk18.red.bold(" Decision: \u{1F534} REVIEW") + chalk18.gray(" \u2014 human approval required")
11934
+ chalk19.red.bold(" Decision: \u{1F534} REVIEW") + chalk19.gray(" \u2014 human approval required")
11509
11935
  );
11510
11936
  if (result.blockedByLabel) {
11511
- console.log(chalk18.gray(` Reason: ${result.blockedByLabel}`));
11937
+ console.log(chalk19.gray(` Reason: ${result.blockedByLabel}`));
11512
11938
  }
11513
11939
  }
11514
11940
  console.log("");
@@ -11522,13 +11948,14 @@ program.command("tail").description("Stream live agent activity to the terminal"
11522
11948
  try {
11523
11949
  await startTail2(options);
11524
11950
  } catch (err2) {
11525
- console.error(chalk18.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
11951
+ console.error(chalk19.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
11526
11952
  process.exit(1);
11527
11953
  }
11528
11954
  });
11529
11955
  registerWatchCommand(program);
11530
11956
  registerMcpGatewayCommand(program);
11531
11957
  registerMcpServerCommand(program);
11958
+ registerMcpPinCommand(program);
11532
11959
  registerCheckCommand(program);
11533
11960
  registerLogCommand(program);
11534
11961
  program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
@@ -11553,14 +11980,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
11553
11980
  Run "node9 addto claude" to register it as the statusLine.`
11554
11981
  ).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
11555
11982
  if (subcommand === "debug") {
11556
- const flagFile = path30.join(os23.homedir(), ".node9", "hud-debug");
11983
+ const flagFile = path31.join(os24.homedir(), ".node9", "hud-debug");
11557
11984
  if (state === "on") {
11558
- fs27.mkdirSync(path30.dirname(flagFile), { recursive: true });
11559
- fs27.writeFileSync(flagFile, "");
11985
+ fs28.mkdirSync(path31.dirname(flagFile), { recursive: true });
11986
+ fs28.writeFileSync(flagFile, "");
11560
11987
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
11561
11988
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
11562
11989
  } else if (state === "off") {
11563
- if (fs27.existsSync(flagFile)) fs27.unlinkSync(flagFile);
11990
+ if (fs28.existsSync(flagFile)) fs28.unlinkSync(flagFile);
11564
11991
  console.log("HUD debug logging disabled.");
11565
11992
  } else {
11566
11993
  console.error("Usage: node9 hud debug on|off");
@@ -11575,7 +12002,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
11575
12002
  const ms = parseDuration(options.duration);
11576
12003
  if (ms === null) {
11577
12004
  console.error(
11578
- chalk18.red(`
12005
+ chalk19.red(`
11579
12006
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
11580
12007
  `)
11581
12008
  );
@@ -11583,20 +12010,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
11583
12010
  }
11584
12011
  pauseNode9(ms, options.duration);
11585
12012
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
11586
- console.log(chalk18.yellow(`
12013
+ console.log(chalk19.yellow(`
11587
12014
  \u23F8 Node9 paused until ${expiresAt}`));
11588
- console.log(chalk18.gray(` All tool calls will be allowed without review.`));
11589
- console.log(chalk18.gray(` Run "node9 resume" to re-enable early.
12015
+ console.log(chalk19.gray(` All tool calls will be allowed without review.`));
12016
+ console.log(chalk19.gray(` Run "node9 resume" to re-enable early.
11590
12017
  `));
11591
12018
  });
11592
12019
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
11593
12020
  const { paused } = checkPause();
11594
12021
  if (!paused) {
11595
- console.log(chalk18.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
12022
+ console.log(chalk19.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
11596
12023
  return;
11597
12024
  }
11598
12025
  resumeNode9();
11599
- console.log(chalk18.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
12026
+ console.log(chalk19.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
11600
12027
  });
11601
12028
  var HOOK_BASED_AGENTS = {
11602
12029
  claude: "claude",
@@ -11609,15 +12036,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
11609
12036
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
11610
12037
  const target = HOOK_BASED_AGENTS[firstArg2];
11611
12038
  console.error(
11612
- chalk18.yellow(`
12039
+ chalk19.yellow(`
11613
12040
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
11614
12041
  );
11615
- console.error(chalk18.white(`
12042
+ console.error(chalk19.white(`
11616
12043
  "${target}" uses its own hook system. Use:`));
11617
12044
  console.error(
11618
- chalk18.green(` node9 addto ${target} `) + chalk18.gray("# one-time setup")
12045
+ chalk19.green(` node9 addto ${target} `) + chalk19.gray("# one-time setup")
11619
12046
  );
11620
- console.error(chalk18.green(` ${target} `) + chalk18.gray("# run normally"));
12047
+ console.error(chalk19.green(` ${target} `) + chalk19.gray("# run normally"));
11621
12048
  process.exit(1);
11622
12049
  }
11623
12050
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -11634,7 +12061,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
11634
12061
  }
11635
12062
  );
11636
12063
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
11637
- console.error(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
12064
+ console.error(chalk19.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
11638
12065
  const daemonReady = await autoStartDaemonAndWait();
11639
12066
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
11640
12067
  }
@@ -11647,12 +12074,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
11647
12074
  }
11648
12075
  if (!result.approved) {
11649
12076
  console.error(
11650
- chalk18.red(`
12077
+ chalk19.red(`
11651
12078
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
11652
12079
  );
11653
12080
  process.exit(1);
11654
12081
  }
11655
- console.error(chalk18.green("\n\u2705 Approved \u2014 running command...\n"));
12082
+ console.error(chalk19.green("\n\u2705 Approved \u2014 running command...\n"));
11656
12083
  await runProxy(fullCommand);
11657
12084
  } else {
11658
12085
  program.help();
@@ -11667,9 +12094,9 @@ if (process.argv[2] !== "daemon") {
11667
12094
  const isCheckHook = process.argv[2] === "check";
11668
12095
  if (isCheckHook) {
11669
12096
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
11670
- const logPath = path30.join(os23.homedir(), ".node9", "hook-debug.log");
12097
+ const logPath = path31.join(os24.homedir(), ".node9", "hook-debug.log");
11671
12098
  const msg = reason instanceof Error ? reason.message : String(reason);
11672
- fs27.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
12099
+ fs28.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
11673
12100
  `);
11674
12101
  }
11675
12102
  process.exit(0);