@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.js CHANGED
@@ -160,8 +160,8 @@ function sanitizeConfig(raw) {
160
160
  }
161
161
  }
162
162
  const lines = result.error.issues.map((issue) => {
163
- const path31 = issue.path.length > 0 ? issue.path.join(".") : "root";
164
- return ` \u2022 ${path31}: ${issue.message}`;
163
+ const path32 = issue.path.length > 0 ? issue.path.join(".") : "root";
164
+ return ` \u2022 ${path32}: ${issue.message}`;
165
165
  });
166
166
  return {
167
167
  sanitized,
@@ -828,7 +828,7 @@ var init_config = __esm({
828
828
  {
829
829
  field: "command",
830
830
  op: "matches",
831
- value: "^\\s*git\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
831
+ value: "\\bgit\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
832
832
  flags: "i"
833
833
  }
834
834
  ],
@@ -843,7 +843,7 @@ var init_config = __esm({
843
843
  {
844
844
  field: "command",
845
845
  op: "matches",
846
- value: "^\\s*git\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
846
+ value: "\\bgit\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
847
847
  flags: "i"
848
848
  }
849
849
  ],
@@ -858,7 +858,7 @@ var init_config = __esm({
858
858
  {
859
859
  field: "command",
860
860
  op: "matches",
861
- value: "^\\s*git\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
861
+ value: "\\bgit\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
862
862
  flags: "i"
863
863
  }
864
864
  ],
@@ -870,7 +870,7 @@ var init_config = __esm({
870
870
  {
871
871
  name: "review-sudo",
872
872
  tool: "bash",
873
- conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
873
+ conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
874
874
  conditionMode: "all",
875
875
  verdict: "review",
876
876
  reason: "Command requires elevated privileges"
@@ -1691,9 +1691,9 @@ function matchesPattern(text, patterns) {
1691
1691
  const withoutDotSlash = text.replace(/^\.\//, "");
1692
1692
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1693
1693
  }
1694
- function getNestedValue(obj, path31) {
1694
+ function getNestedValue(obj, path32) {
1695
1695
  if (!obj || typeof obj !== "object") return null;
1696
- return path31.split(".").reduce((prev, curr) => prev?.[curr], obj);
1696
+ return path32.split(".").reduce((prev, curr) => prev?.[curr], obj);
1697
1697
  }
1698
1698
  function shouldSnapshot(toolName, args, config) {
1699
1699
  if (!config.settings.enableUndo) return false;
@@ -6782,22 +6782,22 @@ function formatBase(activity) {
6782
6782
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
6783
6783
  const icon = getIcon(activity.tool);
6784
6784
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6785
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os21.default.homedir(), "~");
6785
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os22.default.homedir(), "~");
6786
6786
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6787
- return `${import_chalk17.default.gray(time)} ${icon} ${import_chalk17.default.white.bold(toolName)} ${import_chalk17.default.dim(argsPreview)}`;
6787
+ return `${import_chalk18.default.gray(time)} ${icon} ${import_chalk18.default.white.bold(toolName)} ${import_chalk18.default.dim(argsPreview)}`;
6788
6788
  }
6789
6789
  function renderResult(activity, result) {
6790
6790
  const base = formatBase(activity);
6791
6791
  let status;
6792
6792
  if (result.status === "allow") {
6793
- status = import_chalk17.default.green("\u2713 ALLOW");
6793
+ status = import_chalk18.default.green("\u2713 ALLOW");
6794
6794
  } else if (result.status === "dlp") {
6795
- status = import_chalk17.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6795
+ status = import_chalk18.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6796
6796
  } else {
6797
- status = import_chalk17.default.red("\u2717 BLOCK");
6797
+ status = import_chalk18.default.red("\u2717 BLOCK");
6798
6798
  }
6799
6799
  const cost = result.costEstimate ?? activity.costEstimate;
6800
- const costSuffix = cost == null ? "" : import_chalk17.default.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
6800
+ const costSuffix = cost == null ? "" : import_chalk18.default.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
6801
6801
  if (process.stdout.isTTY) {
6802
6802
  import_readline5.default.clearLine(process.stdout, 0);
6803
6803
  import_readline5.default.cursorTo(process.stdout, 0);
@@ -6806,16 +6806,16 @@ function renderResult(activity, result) {
6806
6806
  }
6807
6807
  function renderPending(activity) {
6808
6808
  if (!process.stdout.isTTY) return;
6809
- process.stdout.write(`${formatBase(activity)} ${import_chalk17.default.yellow("\u25CF \u2026")}\r`);
6809
+ process.stdout.write(`${formatBase(activity)} ${import_chalk18.default.yellow("\u25CF \u2026")}\r`);
6810
6810
  }
6811
6811
  async function ensureDaemon() {
6812
6812
  let pidPort = null;
6813
- if (import_fs25.default.existsSync(PID_FILE)) {
6813
+ if (import_fs26.default.existsSync(PID_FILE)) {
6814
6814
  try {
6815
- const { port } = JSON.parse(import_fs25.default.readFileSync(PID_FILE, "utf-8"));
6815
+ const { port } = JSON.parse(import_fs26.default.readFileSync(PID_FILE, "utf-8"));
6816
6816
  pidPort = port;
6817
6817
  } catch {
6818
- console.error(import_chalk17.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6818
+ console.error(import_chalk18.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6819
6819
  }
6820
6820
  }
6821
6821
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -6826,7 +6826,7 @@ async function ensureDaemon() {
6826
6826
  if (res.ok) return checkPort;
6827
6827
  } catch {
6828
6828
  }
6829
- console.log(import_chalk17.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6829
+ console.log(import_chalk18.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6830
6830
  const child = (0, import_child_process14.spawn)(process.execPath, [process.argv[1], "daemon"], {
6831
6831
  detached: true,
6832
6832
  stdio: "ignore",
@@ -6843,7 +6843,7 @@ async function ensureDaemon() {
6843
6843
  } catch {
6844
6844
  }
6845
6845
  }
6846
- console.error(import_chalk17.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6846
+ console.error(import_chalk18.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6847
6847
  process.exit(1);
6848
6848
  }
6849
6849
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
@@ -6932,9 +6932,9 @@ function buildRecoveryCardLines(req) {
6932
6932
  ];
6933
6933
  }
6934
6934
  function readApproversFromDisk() {
6935
- const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
6935
+ const configPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "config.json");
6936
6936
  try {
6937
- const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
6937
+ const raw = JSON.parse(import_fs26.default.readFileSync(configPath, "utf-8"));
6938
6938
  const settings = raw.settings ?? {};
6939
6939
  return settings.approvers ?? {};
6940
6940
  } catch {
@@ -6945,20 +6945,20 @@ function approverStatusLine() {
6945
6945
  const a = readApproversFromDisk();
6946
6946
  const fmt = (label, key) => {
6947
6947
  const on = a[key] !== false;
6948
- return `[${key[0]}]${label.slice(1)} ${on ? import_chalk17.default.green("\u2713") : import_chalk17.default.dim("\u2717")}`;
6948
+ return `[${key[0]}]${label.slice(1)} ${on ? import_chalk18.default.green("\u2713") : import_chalk18.default.dim("\u2717")}`;
6949
6949
  };
6950
6950
  return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
6951
6951
  }
6952
6952
  function toggleApprover(channel) {
6953
- const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
6953
+ const configPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "config.json");
6954
6954
  try {
6955
- const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
6955
+ const raw = JSON.parse(import_fs26.default.readFileSync(configPath, "utf-8"));
6956
6956
  const settings = raw.settings ?? {};
6957
6957
  const approvers = settings.approvers ?? {};
6958
6958
  approvers[channel] = approvers[channel] === false;
6959
6959
  settings.approvers = approvers;
6960
6960
  raw.settings = settings;
6961
- import_fs25.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
6961
+ import_fs26.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
6962
6962
  } catch (err2) {
6963
6963
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
6964
6964
  `);
@@ -6990,7 +6990,7 @@ async function startTail(options = {}) {
6990
6990
  req2.end();
6991
6991
  });
6992
6992
  if (result.ok) {
6993
- console.log(import_chalk17.default.green("\u2713 Flight Recorder buffer cleared."));
6993
+ console.log(import_chalk18.default.green("\u2713 Flight Recorder buffer cleared."));
6994
6994
  } else if (result.code === "ECONNREFUSED") {
6995
6995
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
6996
6996
  } else if (result.code === "ETIMEDOUT") {
@@ -7034,7 +7034,7 @@ async function startTail(options = {}) {
7034
7034
  const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7035
7035
  if (channel) {
7036
7036
  toggleApprover(channel);
7037
- console.log(import_chalk17.default.dim(` Approvers: ${approverStatusLine()}`));
7037
+ console.log(import_chalk18.default.dim(` Approvers: ${approverStatusLine()}`));
7038
7038
  }
7039
7039
  };
7040
7040
  process.stdin.on("keypress", idleKeypressHandler);
@@ -7100,7 +7100,7 @@ async function startTail(options = {}) {
7100
7100
  localAllowCounts.get(req2.toolName) ?? 0
7101
7101
  )
7102
7102
  );
7103
- const decisionStamp = action === "always-allow" ? import_chalk17.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk17.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk17.default.green("\u2713 ALLOWED") : action === "redirect" ? import_chalk17.default.yellow("\u21A9 REDIRECT AI") : import_chalk17.default.red("\u2717 DENIED");
7103
+ const decisionStamp = action === "always-allow" ? import_chalk18.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk18.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk18.default.green("\u2713 ALLOWED") : action === "redirect" ? import_chalk18.default.yellow("\u21A9 REDIRECT AI") : import_chalk18.default.red("\u2717 DENIED");
7104
7104
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
7105
7105
  for (const line of stampedLines) process.stdout.write(line + "\n");
7106
7106
  process.stdout.write(SHOW_CURSOR);
@@ -7128,8 +7128,8 @@ async function startTail(options = {}) {
7128
7128
  }
7129
7129
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7130
7130
  try {
7131
- import_fs25.default.appendFileSync(
7132
- import_path28.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
7131
+ import_fs26.default.appendFileSync(
7132
+ import_path29.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log"),
7133
7133
  `[tail] POST /decision failed: ${String(err2)}
7134
7134
  `
7135
7135
  );
@@ -7151,7 +7151,7 @@ async function startTail(options = {}) {
7151
7151
  );
7152
7152
  const stampedLines = buildCardLines(req2, priorCount);
7153
7153
  if (externalDecision) {
7154
- const source = externalDecision === "allow" ? import_chalk17.default.green("\u2713 ALLOWED") : import_chalk17.default.red("\u2717 DENIED");
7154
+ const source = externalDecision === "allow" ? import_chalk18.default.green("\u2713 ALLOWED") : import_chalk18.default.red("\u2717 DENIED");
7155
7155
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
7156
7156
  }
7157
7157
  for (const line of stampedLines) process.stdout.write(line + "\n");
@@ -7210,16 +7210,16 @@ async function startTail(options = {}) {
7210
7210
  }
7211
7211
  } catch {
7212
7212
  }
7213
- console.log(import_chalk17.default.cyan.bold(`
7214
- \u{1F6F0}\uFE0F Node9 tail `) + import_chalk17.default.dim(`\u2192 ${dashboardUrl}`));
7213
+ console.log(import_chalk18.default.cyan.bold(`
7214
+ \u{1F6F0}\uFE0F Node9 tail `) + import_chalk18.default.dim(`\u2192 ${dashboardUrl}`));
7215
7215
  if (canApprove) {
7216
- console.log(import_chalk17.default.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7217
- console.log(import_chalk17.default.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7216
+ console.log(import_chalk18.default.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7217
+ console.log(import_chalk18.default.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7218
7218
  }
7219
7219
  if (options.history) {
7220
- console.log(import_chalk17.default.dim("Showing history + live events.\n"));
7220
+ console.log(import_chalk18.default.dim("Showing history + live events.\n"));
7221
7221
  } else {
7222
- console.log(import_chalk17.default.dim("Showing live events only. Use --history to include past.\n"));
7222
+ console.log(import_chalk18.default.dim("Showing live events only. Use --history to include past.\n"));
7223
7223
  }
7224
7224
  process.on("SIGINT", () => {
7225
7225
  exitIdleMode();
@@ -7229,13 +7229,13 @@ async function startTail(options = {}) {
7229
7229
  import_readline5.default.clearLine(process.stdout, 0);
7230
7230
  import_readline5.default.cursorTo(process.stdout, 0);
7231
7231
  }
7232
- console.log(import_chalk17.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7232
+ console.log(import_chalk18.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7233
7233
  process.exit(0);
7234
7234
  });
7235
7235
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
7236
7236
  const req = import_http2.default.get(sseUrl, (res) => {
7237
7237
  if (res.statusCode !== 200) {
7238
- console.error(import_chalk17.default.red(`Failed to connect: HTTP ${res.statusCode}`));
7238
+ console.error(import_chalk18.default.red(`Failed to connect: HTTP ${res.statusCode}`));
7239
7239
  process.exit(1);
7240
7240
  }
7241
7241
  if (canApprove) enterIdleMode();
@@ -7266,7 +7266,7 @@ async function startTail(options = {}) {
7266
7266
  import_readline5.default.clearLine(process.stdout, 0);
7267
7267
  import_readline5.default.cursorTo(process.stdout, 0);
7268
7268
  }
7269
- console.log(import_chalk17.default.red("\n\u274C Daemon disconnected."));
7269
+ console.log(import_chalk18.default.red("\n\u274C Daemon disconnected."));
7270
7270
  process.exit(1);
7271
7271
  });
7272
7272
  });
@@ -7358,9 +7358,9 @@ async function startTail(options = {}) {
7358
7358
  const hash = data.hash ?? "";
7359
7359
  const summary = data.argsSummary ?? data.tool;
7360
7360
  const fileCount = data.fileCount ?? 0;
7361
- const files = fileCount > 0 ? import_chalk17.default.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7361
+ const files = fileCount > 0 ? import_chalk18.default.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7362
7362
  process.stdout.write(
7363
- `${import_chalk17.default.dim(time)} ${import_chalk17.default.cyan("\u{1F4F8} snapshot")} ${import_chalk17.default.dim(hash)} ${summary}${files}
7363
+ `${import_chalk18.default.dim(time)} ${import_chalk18.default.cyan("\u{1F4F8} snapshot")} ${import_chalk18.default.dim(hash)} ${summary}${files}
7364
7364
  `
7365
7365
  );
7366
7366
  return;
@@ -7377,26 +7377,26 @@ async function startTail(options = {}) {
7377
7377
  }
7378
7378
  req.on("error", (err2) => {
7379
7379
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
7380
- console.error(import_chalk17.default.red(`
7380
+ console.error(import_chalk18.default.red(`
7381
7381
  \u274C ${msg}`));
7382
7382
  process.exit(1);
7383
7383
  });
7384
7384
  }
7385
- var import_http2, import_chalk17, import_fs25, import_os21, import_path28, import_readline5, import_child_process14, PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
7385
+ var import_http2, import_chalk18, import_fs26, import_os22, import_path29, import_readline5, import_child_process14, PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
7386
7386
  var init_tail = __esm({
7387
7387
  "src/tui/tail.ts"() {
7388
7388
  "use strict";
7389
7389
  import_http2 = __toESM(require("http"));
7390
- import_chalk17 = __toESM(require("chalk"));
7391
- import_fs25 = __toESM(require("fs"));
7392
- import_os21 = __toESM(require("os"));
7393
- import_path28 = __toESM(require("path"));
7390
+ import_chalk18 = __toESM(require("chalk"));
7391
+ import_fs26 = __toESM(require("fs"));
7392
+ import_os22 = __toESM(require("os"));
7393
+ import_path29 = __toESM(require("path"));
7394
7394
  import_readline5 = __toESM(require("readline"));
7395
7395
  import_child_process14 = require("child_process");
7396
7396
  init_daemon2();
7397
7397
  init_daemon();
7398
7398
  init_core();
7399
- PID_FILE = import_path28.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
7399
+ PID_FILE = import_path29.default.join(import_os22.default.homedir(), ".node9", "daemon.pid");
7400
7400
  ICONS = {
7401
7401
  bash: "\u{1F4BB}",
7402
7402
  shell: "\u{1F4BB}",
@@ -7509,9 +7509,9 @@ function formatTimeLeft(resetsAt) {
7509
7509
  return ` (${m}m left)`;
7510
7510
  }
7511
7511
  function safeReadJson(filePath) {
7512
- if (!import_fs26.default.existsSync(filePath)) return null;
7512
+ if (!import_fs27.default.existsSync(filePath)) return null;
7513
7513
  try {
7514
- return JSON.parse(import_fs26.default.readFileSync(filePath, "utf-8"));
7514
+ return JSON.parse(import_fs27.default.readFileSync(filePath, "utf-8"));
7515
7515
  } catch {
7516
7516
  return null;
7517
7517
  }
@@ -7532,12 +7532,12 @@ function countHooksInFile(filePath) {
7532
7532
  return Object.keys(cfg.hooks).length;
7533
7533
  }
7534
7534
  function countRulesInDir(rulesDir) {
7535
- if (!import_fs26.default.existsSync(rulesDir)) return 0;
7535
+ if (!import_fs27.default.existsSync(rulesDir)) return 0;
7536
7536
  let count = 0;
7537
7537
  try {
7538
- for (const entry of import_fs26.default.readdirSync(rulesDir, { withFileTypes: true })) {
7538
+ for (const entry of import_fs27.default.readdirSync(rulesDir, { withFileTypes: true })) {
7539
7539
  if (entry.isDirectory()) {
7540
- count += countRulesInDir(import_path29.default.join(rulesDir, entry.name));
7540
+ count += countRulesInDir(import_path30.default.join(rulesDir, entry.name));
7541
7541
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
7542
7542
  count++;
7543
7543
  }
@@ -7548,46 +7548,46 @@ function countRulesInDir(rulesDir) {
7548
7548
  }
7549
7549
  function isSamePath(a, b) {
7550
7550
  try {
7551
- return import_path29.default.resolve(a) === import_path29.default.resolve(b);
7551
+ return import_path30.default.resolve(a) === import_path30.default.resolve(b);
7552
7552
  } catch {
7553
7553
  return false;
7554
7554
  }
7555
7555
  }
7556
7556
  function countConfigs(cwd) {
7557
- const homeDir2 = import_os22.default.homedir();
7558
- const claudeDir = import_path29.default.join(homeDir2, ".claude");
7557
+ const homeDir2 = import_os23.default.homedir();
7558
+ const claudeDir = import_path30.default.join(homeDir2, ".claude");
7559
7559
  let claudeMdCount = 0;
7560
7560
  let rulesCount = 0;
7561
7561
  let hooksCount = 0;
7562
7562
  const userMcpServers = /* @__PURE__ */ new Set();
7563
7563
  const projectMcpServers = /* @__PURE__ */ new Set();
7564
- if (import_fs26.default.existsSync(import_path29.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7565
- rulesCount += countRulesInDir(import_path29.default.join(claudeDir, "rules"));
7566
- const userSettings = import_path29.default.join(claudeDir, "settings.json");
7564
+ if (import_fs27.default.existsSync(import_path30.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7565
+ rulesCount += countRulesInDir(import_path30.default.join(claudeDir, "rules"));
7566
+ const userSettings = import_path30.default.join(claudeDir, "settings.json");
7567
7567
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7568
7568
  hooksCount += countHooksInFile(userSettings);
7569
- const userClaudeJson = import_path29.default.join(homeDir2, ".claude.json");
7569
+ const userClaudeJson = import_path30.default.join(homeDir2, ".claude.json");
7570
7570
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7571
7571
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7572
7572
  userMcpServers.delete(name);
7573
7573
  }
7574
7574
  if (cwd) {
7575
- if (import_fs26.default.existsSync(import_path29.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7576
- if (import_fs26.default.existsSync(import_path29.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7577
- const projectClaudeDir = import_path29.default.join(cwd, ".claude");
7575
+ if (import_fs27.default.existsSync(import_path30.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7576
+ if (import_fs27.default.existsSync(import_path30.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7577
+ const projectClaudeDir = import_path30.default.join(cwd, ".claude");
7578
7578
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7579
7579
  if (!overlapsUserScope) {
7580
- if (import_fs26.default.existsSync(import_path29.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7581
- rulesCount += countRulesInDir(import_path29.default.join(projectClaudeDir, "rules"));
7582
- const projSettings = import_path29.default.join(projectClaudeDir, "settings.json");
7580
+ if (import_fs27.default.existsSync(import_path30.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7581
+ rulesCount += countRulesInDir(import_path30.default.join(projectClaudeDir, "rules"));
7582
+ const projSettings = import_path30.default.join(projectClaudeDir, "settings.json");
7583
7583
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7584
7584
  hooksCount += countHooksInFile(projSettings);
7585
7585
  }
7586
- if (import_fs26.default.existsSync(import_path29.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7587
- const localSettings = import_path29.default.join(projectClaudeDir, "settings.local.json");
7586
+ if (import_fs27.default.existsSync(import_path30.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7587
+ const localSettings = import_path30.default.join(projectClaudeDir, "settings.local.json");
7588
7588
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7589
7589
  hooksCount += countHooksInFile(localSettings);
7590
- const mcpJsonServers = getMcpServerNames(import_path29.default.join(cwd, ".mcp.json"));
7590
+ const mcpJsonServers = getMcpServerNames(import_path30.default.join(cwd, ".mcp.json"));
7591
7591
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7592
7592
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7593
7593
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -7620,12 +7620,12 @@ function readActiveShieldsHud() {
7620
7620
  return shieldsCache.value;
7621
7621
  }
7622
7622
  try {
7623
- const shieldsPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "shields.json");
7624
- if (!import_fs26.default.existsSync(shieldsPath)) {
7623
+ const shieldsPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "shields.json");
7624
+ if (!import_fs27.default.existsSync(shieldsPath)) {
7625
7625
  shieldsCache = { value: [], ts: now };
7626
7626
  return [];
7627
7627
  }
7628
- const parsed = JSON.parse(import_fs26.default.readFileSync(shieldsPath, "utf-8"));
7628
+ const parsed = JSON.parse(import_fs27.default.readFileSync(shieldsPath, "utf-8"));
7629
7629
  if (!Array.isArray(parsed.active)) {
7630
7630
  shieldsCache = { value: [], ts: now };
7631
7631
  return [];
@@ -7727,17 +7727,17 @@ function renderContextLine(stdin) {
7727
7727
  async function main() {
7728
7728
  try {
7729
7729
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
7730
- if (import_fs26.default.existsSync(import_path29.default.join(import_os22.default.homedir(), ".node9", "hud-debug"))) {
7730
+ if (import_fs27.default.existsSync(import_path30.default.join(import_os23.default.homedir(), ".node9", "hud-debug"))) {
7731
7731
  try {
7732
- const logPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "hud-debug.log");
7732
+ const logPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "hud-debug.log");
7733
7733
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
7734
7734
  let size = 0;
7735
7735
  try {
7736
- size = import_fs26.default.statSync(logPath).size;
7736
+ size = import_fs27.default.statSync(logPath).size;
7737
7737
  } catch {
7738
7738
  }
7739
7739
  if (size < MAX_LOG_SIZE) {
7740
- import_fs26.default.appendFileSync(
7740
+ import_fs27.default.appendFileSync(
7741
7741
  logPath,
7742
7742
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
7743
7743
  );
@@ -7758,11 +7758,11 @@ async function main() {
7758
7758
  try {
7759
7759
  const cwd = stdin.cwd ?? process.cwd();
7760
7760
  for (const configPath of [
7761
- import_path29.default.join(cwd, "node9.config.json"),
7762
- import_path29.default.join(import_os22.default.homedir(), ".node9", "config.json")
7761
+ import_path30.default.join(cwd, "node9.config.json"),
7762
+ import_path30.default.join(import_os23.default.homedir(), ".node9", "config.json")
7763
7763
  ]) {
7764
- if (!import_fs26.default.existsSync(configPath)) continue;
7765
- const cfg = JSON.parse(import_fs26.default.readFileSync(configPath, "utf-8"));
7764
+ if (!import_fs27.default.existsSync(configPath)) continue;
7765
+ const cfg = JSON.parse(import_fs27.default.readFileSync(configPath, "utf-8"));
7766
7766
  const hud = cfg.settings?.hud;
7767
7767
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7768
7768
  }
@@ -7780,13 +7780,13 @@ async function main() {
7780
7780
  renderOffline();
7781
7781
  }
7782
7782
  }
7783
- var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
7783
+ var import_fs27, import_path30, import_os23, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
7784
7784
  var init_hud = __esm({
7785
7785
  "src/cli/hud.ts"() {
7786
7786
  "use strict";
7787
- import_fs26 = __toESM(require("fs"));
7788
- import_path29 = __toESM(require("path"));
7789
- import_os22 = __toESM(require("os"));
7787
+ import_fs27 = __toESM(require("fs"));
7788
+ import_path30 = __toESM(require("path"));
7789
+ import_os23 = __toESM(require("os"));
7790
7790
  import_http3 = __toESM(require("http"));
7791
7791
  init_daemon();
7792
7792
  RESET3 = "\x1B[0m";
@@ -8395,10 +8395,10 @@ function teardownHud() {
8395
8395
 
8396
8396
  // src/cli.ts
8397
8397
  init_daemon2();
8398
- var import_chalk18 = __toESM(require("chalk"));
8399
- var import_fs27 = __toESM(require("fs"));
8400
- var import_path30 = __toESM(require("path"));
8401
- var import_os23 = __toESM(require("os"));
8398
+ var import_chalk19 = __toESM(require("chalk"));
8399
+ var import_fs28 = __toESM(require("fs"));
8400
+ var import_path31 = __toESM(require("path"));
8401
+ var import_os24 = __toESM(require("os"));
8402
8402
  var import_prompts2 = require("@inquirer/prompts");
8403
8403
 
8404
8404
  // src/utils/duration.ts
@@ -10288,11 +10288,17 @@ function registerInitCommand(program2) {
10288
10288
  if (sendTelemetry) fireTelemetryPing(found);
10289
10289
  console.log("");
10290
10290
  }
10291
- console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
10291
+ const agentList = found.join(", ");
10292
+ console.log(import_chalk11.default.green.bold(`\u{1F6E1}\uFE0F Node9 is protecting ${agentList}!`));
10293
+ console.log("");
10294
+ console.log(import_chalk11.default.white(" Watch live: ") + import_chalk11.default.cyan("node9 tail"));
10295
+ console.log(import_chalk11.default.white(" Local UI: ") + import_chalk11.default.cyan("node9 daemon --openui"));
10292
10296
  console.log("");
10293
- console.log(import_chalk11.default.white(" Start watching: ") + import_chalk11.default.cyan("node9 tail"));
10294
- console.log(import_chalk11.default.white(" Browser view: ") + import_chalk11.default.cyan("node9 daemon --openui"));
10295
- console.log(import_chalk11.default.white(" Cloud dashboard: ") + import_chalk11.default.cyan("node9.ai"));
10297
+ console.log(import_chalk11.default.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"));
10298
+ console.log(
10299
+ import_chalk11.default.white(" Team dashboard + full audit trail \u2192 ") + import_chalk11.default.cyan.bold("https://node9.ai")
10300
+ );
10301
+ console.log(import_chalk11.default.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"));
10296
10302
  });
10297
10303
  }
10298
10304
 
@@ -10650,6 +10656,90 @@ var import_child_process13 = require("child_process");
10650
10656
  var import_execa3 = require("execa");
10651
10657
  init_orchestrator();
10652
10658
  init_provenance();
10659
+
10660
+ // src/mcp-pin.ts
10661
+ var import_fs24 = __toESM(require("fs"));
10662
+ var import_path27 = __toESM(require("path"));
10663
+ var import_os20 = __toESM(require("os"));
10664
+ var import_crypto8 = __toESM(require("crypto"));
10665
+ function getPinsFilePath() {
10666
+ return import_path27.default.join(import_os20.default.homedir(), ".node9", "mcp-pins.json");
10667
+ }
10668
+ function hashToolDefinitions(tools) {
10669
+ const sorted = [...tools].sort((a, b) => {
10670
+ const nameA = a.name ?? "";
10671
+ const nameB = b.name ?? "";
10672
+ return nameA.localeCompare(nameB);
10673
+ });
10674
+ const canonical = JSON.stringify(sorted);
10675
+ return import_crypto8.default.createHash("sha256").update(canonical).digest("hex");
10676
+ }
10677
+ function getServerKey(upstreamCommand) {
10678
+ return import_crypto8.default.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
10679
+ }
10680
+ function readMcpPinsSafe() {
10681
+ const filePath = getPinsFilePath();
10682
+ try {
10683
+ const raw = import_fs24.default.readFileSync(filePath, "utf-8");
10684
+ if (!raw.trim()) {
10685
+ return { ok: false, reason: "corrupt", detail: "empty file" };
10686
+ }
10687
+ const parsed = JSON.parse(raw);
10688
+ if (!parsed.servers || typeof parsed.servers !== "object" || Array.isArray(parsed.servers)) {
10689
+ return { ok: false, reason: "corrupt", detail: "invalid structure: missing servers object" };
10690
+ }
10691
+ return { ok: true, pins: { servers: parsed.servers } };
10692
+ } catch (err2) {
10693
+ if (err2.code === "ENOENT") {
10694
+ return { ok: false, reason: "missing" };
10695
+ }
10696
+ return { ok: false, reason: "corrupt", detail: String(err2) };
10697
+ }
10698
+ }
10699
+ function readMcpPins() {
10700
+ const result = readMcpPinsSafe();
10701
+ if (result.ok) return result.pins;
10702
+ if (result.reason === "missing") return { servers: {} };
10703
+ throw new Error(`[node9] MCP pin file is corrupt: ${result.detail}`);
10704
+ }
10705
+ function writeMcpPins(data) {
10706
+ const filePath = getPinsFilePath();
10707
+ import_fs24.default.mkdirSync(import_path27.default.dirname(filePath), { recursive: true });
10708
+ const tmp = `${filePath}.${import_crypto8.default.randomBytes(6).toString("hex")}.tmp`;
10709
+ import_fs24.default.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
10710
+ import_fs24.default.renameSync(tmp, filePath);
10711
+ }
10712
+ function checkPin(serverKey, currentHash) {
10713
+ const result = readMcpPinsSafe();
10714
+ if (!result.ok) {
10715
+ if (result.reason === "missing") return "new";
10716
+ return "corrupt";
10717
+ }
10718
+ const entry = result.pins.servers[serverKey];
10719
+ if (!entry) return "new";
10720
+ return entry.toolsHash === currentHash ? "match" : "mismatch";
10721
+ }
10722
+ function updatePin(serverKey, label, toolsHash, toolNames) {
10723
+ const pins = readMcpPins();
10724
+ pins.servers[serverKey] = {
10725
+ label,
10726
+ toolsHash,
10727
+ toolNames,
10728
+ toolCount: toolNames.length,
10729
+ pinnedAt: (/* @__PURE__ */ new Date()).toISOString()
10730
+ };
10731
+ writeMcpPins(pins);
10732
+ }
10733
+ function removePin(serverKey) {
10734
+ const pins = readMcpPins();
10735
+ delete pins.servers[serverKey];
10736
+ writeMcpPins(pins);
10737
+ }
10738
+ function clearAllPins() {
10739
+ writeMcpPins({ servers: {} });
10740
+ }
10741
+
10742
+ // src/mcp-gateway/index.ts
10653
10743
  function sanitize4(value) {
10654
10744
  return value.replace(/[\x00-\x1F\x7F]/g, "");
10655
10745
  }
@@ -10743,6 +10833,10 @@ async function runMcpGateway(upstreamCommand) {
10743
10833
  let authPending = false;
10744
10834
  let deferredExitCode = null;
10745
10835
  let deferredStdinEnd = false;
10836
+ const pendingToolsListIds = /* @__PURE__ */ new Set();
10837
+ const serverKey = getServerKey(upstreamCommand);
10838
+ let pinState = "pending";
10839
+ const pendingToolCalls = [];
10746
10840
  const agentIn = import_readline3.default.createInterface({ input: process.stdin, terminal: false });
10747
10841
  agentIn.on("line", async (line) => {
10748
10842
  let message;
@@ -10765,8 +10859,43 @@ async function runMcpGateway(upstreamCommand) {
10765
10859
  child.stdin.write(line + "\n");
10766
10860
  return;
10767
10861
  }
10862
+ if (message.method === "tools/list" && message.id !== void 0 && message.id !== null) {
10863
+ pendingToolsListIds.add(message.id);
10864
+ }
10768
10865
  if (message.method === "tools/call" || message.method === "call_tool" || message.method === "use_tool") {
10769
- agentIn.pause();
10866
+ if (pinState === "quarantined") {
10867
+ if (message.id === void 0 || message.id === null) return;
10868
+ const errorResponse = {
10869
+ jsonrpc: "2.0",
10870
+ id: message.id,
10871
+ error: {
10872
+ code: RPC_SERVER_ERROR,
10873
+ 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}`,
10874
+ data: { reason: "pin-quarantine", serverKey, pinState }
10875
+ }
10876
+ };
10877
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
10878
+ return;
10879
+ }
10880
+ if (pinState === "pending") {
10881
+ if (pendingToolsListIds.size > 0) {
10882
+ pendingToolCalls.push(line);
10883
+ return;
10884
+ }
10885
+ if (message.id === void 0 || message.id === null) return;
10886
+ const errorResponse = {
10887
+ jsonrpc: "2.0",
10888
+ id: message.id,
10889
+ error: {
10890
+ code: RPC_SERVER_ERROR,
10891
+ 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.",
10892
+ data: { reason: "pin-quarantine", serverKey, pinState }
10893
+ }
10894
+ };
10895
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
10896
+ return;
10897
+ }
10898
+ if (!deferredStdinEnd) agentIn.pause();
10770
10899
  authPending = true;
10771
10900
  try {
10772
10901
  const toolName = sanitize4(
@@ -10828,9 +10957,100 @@ async function runMcpGateway(upstreamCommand) {
10828
10957
  }
10829
10958
  child.stdin.write(line + "\n");
10830
10959
  });
10831
- child.stdout.pipe(process.stdout);
10960
+ function drainPendingToolCalls() {
10961
+ if (pendingToolCalls.length === 0) {
10962
+ if (deferredStdinEnd && !authPending) child.stdin.end();
10963
+ return;
10964
+ }
10965
+ const lines = pendingToolCalls.splice(0);
10966
+ for (const queuedLine of lines) {
10967
+ agentIn.emit("line", queuedLine);
10968
+ }
10969
+ if (deferredStdinEnd && !authPending) child.stdin.end();
10970
+ }
10971
+ const upstreamOut = import_readline3.default.createInterface({ input: child.stdout, terminal: false });
10972
+ upstreamOut.on("line", (line) => {
10973
+ let parsed;
10974
+ try {
10975
+ parsed = JSON.parse(line);
10976
+ } catch {
10977
+ }
10978
+ if (!parsed) {
10979
+ process.stdout.write(line + "\n");
10980
+ return;
10981
+ }
10982
+ if (parsed.id !== void 0 && pendingToolsListIds.has(parsed.id)) {
10983
+ pendingToolsListIds.delete(parsed.id);
10984
+ if (parsed.result && Array.isArray(parsed.result.tools)) {
10985
+ const tools = parsed.result.tools;
10986
+ const currentHash = hashToolDefinitions(tools);
10987
+ const pinStatus = checkPin(serverKey, currentHash);
10988
+ if (pinStatus === "new") {
10989
+ const toolNames = tools.map((t) => t.name ?? "unknown").sort();
10990
+ updatePin(serverKey, upstreamCommand, currentHash, toolNames);
10991
+ pinState = "validated";
10992
+ console.error(
10993
+ import_chalk15.default.green(
10994
+ `\u{1F512} Node9: Pinned ${toolNames.length} tool definition(s) for this MCP server`
10995
+ )
10996
+ );
10997
+ process.stdout.write(line + "\n");
10998
+ drainPendingToolCalls();
10999
+ } else if (pinStatus === "match") {
11000
+ pinState = "validated";
11001
+ process.stdout.write(line + "\n");
11002
+ drainPendingToolCalls();
11003
+ } else if (pinStatus === "corrupt") {
11004
+ pinState = "quarantined";
11005
+ console.error(
11006
+ import_chalk15.default.red("\n\u{1F6A8} Node9: MCP pin file is corrupt or unreadable \u2014 session quarantined!")
11007
+ );
11008
+ console.error(import_chalk15.default.red(" Tool calls are blocked until the pin file is repaired."));
11009
+ console.error(
11010
+ import_chalk15.default.yellow(` Run: node9 mcp pin reset (to clear and re-pin on next connect)
11011
+ `)
11012
+ );
11013
+ const errorResponse = {
11014
+ jsonrpc: "2.0",
11015
+ id: parsed.id,
11016
+ error: {
11017
+ code: RPC_SERVER_ERROR,
11018
+ 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",
11019
+ data: { reason: "pin-file-corrupt", serverKey }
11020
+ }
11021
+ };
11022
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
11023
+ drainPendingToolCalls();
11024
+ } else {
11025
+ pinState = "quarantined";
11026
+ console.error(
11027
+ import_chalk15.default.red("\n\u{1F6A8} Node9: MCP tool definitions have changed since last verified!")
11028
+ );
11029
+ console.error(
11030
+ import_chalk15.default.red(" This could indicate a supply chain attack (tool poisoning / rug pull).")
11031
+ );
11032
+ console.error(import_chalk15.default.red(" Session quarantined \u2014 all tool calls blocked."));
11033
+ console.error(import_chalk15.default.yellow(` Run: node9 mcp pin update ${serverKey}
11034
+ `));
11035
+ const errorResponse = {
11036
+ jsonrpc: "2.0",
11037
+ id: parsed.id,
11038
+ error: {
11039
+ code: RPC_SERVER_ERROR,
11040
+ 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}`,
11041
+ data: { reason: "tool-pin-mismatch", serverKey }
11042
+ }
11043
+ };
11044
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
11045
+ drainPendingToolCalls();
11046
+ }
11047
+ return;
11048
+ }
11049
+ }
11050
+ process.stdout.write(line + "\n");
11051
+ });
10832
11052
  process.stdin.on("close", () => {
10833
- if (authPending) {
11053
+ if (authPending || pendingToolCalls.length > 0) {
10834
11054
  deferredStdinEnd = true;
10835
11055
  } else {
10836
11056
  child.stdin.end();
@@ -10859,9 +11079,9 @@ function registerMcpGatewayCommand(program2) {
10859
11079
 
10860
11080
  // src/mcp-server/index.ts
10861
11081
  var import_readline4 = __toESM(require("readline"));
10862
- var import_fs24 = __toESM(require("fs"));
10863
- var import_os20 = __toESM(require("os"));
10864
- var import_path27 = __toESM(require("path"));
11082
+ var import_fs25 = __toESM(require("fs"));
11083
+ var import_os21 = __toESM(require("os"));
11084
+ var import_path28 = __toESM(require("path"));
10865
11085
  init_core();
10866
11086
  init_daemon();
10867
11087
  init_shields();
@@ -10961,6 +11181,60 @@ var TOOLS = [
10961
11181
  },
10962
11182
  required: ["hash"]
10963
11183
  }
11184
+ },
11185
+ {
11186
+ name: "node9_audit_get",
11187
+ 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.",
11188
+ inputSchema: {
11189
+ type: "object",
11190
+ properties: {
11191
+ limit: {
11192
+ type: "number",
11193
+ description: "Number of recent entries to return (default: 20, max: 100)."
11194
+ }
11195
+ },
11196
+ required: []
11197
+ }
11198
+ },
11199
+ {
11200
+ name: "node9_policy_get",
11201
+ 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.",
11202
+ inputSchema: { type: "object", properties: {}, required: [] }
11203
+ },
11204
+ {
11205
+ name: "node9_rule_add",
11206
+ 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.',
11207
+ inputSchema: {
11208
+ type: "object",
11209
+ properties: {
11210
+ name: {
11211
+ type: "string",
11212
+ description: 'Unique rule name (e.g. "block-drop-prod-db").'
11213
+ },
11214
+ tool: {
11215
+ type: "string",
11216
+ description: 'Tool to match \u2014 "bash", "*", or a specific tool name.'
11217
+ },
11218
+ field: {
11219
+ type: "string",
11220
+ description: 'Field to inspect \u2014 "command" for bash, "sql" for database tools.'
11221
+ },
11222
+ pattern: {
11223
+ type: "string",
11224
+ description: "Regex pattern to match against the field."
11225
+ },
11226
+ verdict: {
11227
+ type: "string",
11228
+ enum: ["block", "review"],
11229
+ description: 'Action to take when the rule matches. Only "block" or "review" are allowed.'
11230
+ },
11231
+ reason: {
11232
+ type: "string",
11233
+ description: "Human-readable explanation shown when the rule triggers."
11234
+ }
11235
+ },
11236
+ required: ["name", "tool", "field", "pattern", "verdict", "reason"]
11237
+ }
10964
11238
  }
10965
11239
  ];
10966
11240
  function handleStatus() {
@@ -10982,13 +11256,13 @@ function handleStatus() {
10982
11256
  lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
10983
11257
  lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
10984
11258
  lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
10985
- const projectConfig = import_path27.default.join(process.cwd(), "node9.config.json");
10986
- const globalConfig = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
11259
+ const projectConfig = import_path28.default.join(process.cwd(), "node9.config.json");
11260
+ const globalConfig = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
10987
11261
  lines.push(
10988
- `Project config (node9.config.json): ${import_fs24.default.existsSync(projectConfig) ? "present" : "not found"}`
11262
+ `Project config (node9.config.json): ${import_fs25.default.existsSync(projectConfig) ? "present" : "not found"}`
10989
11263
  );
10990
11264
  lines.push(
10991
- `Global config (~/.node9/config.json): ${import_fs24.default.existsSync(globalConfig) ? "present" : "not found"}`
11265
+ `Global config (~/.node9/config.json): ${import_fs25.default.existsSync(globalConfig) ? "present" : "not found"}`
10992
11266
  );
10993
11267
  return lines.join("\n");
10994
11268
  }
@@ -11062,21 +11336,21 @@ function handleShieldDisable(args) {
11062
11336
  writeActiveShields(active.filter((s) => s !== name));
11063
11337
  return `Shield "${name}" disabled.`;
11064
11338
  }
11065
- var GLOBAL_CONFIG_PATH2 = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
11339
+ var GLOBAL_CONFIG_PATH2 = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
11066
11340
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
11067
11341
  function readGlobalConfigRaw() {
11068
11342
  try {
11069
- if (import_fs24.default.existsSync(GLOBAL_CONFIG_PATH2)) {
11070
- return JSON.parse(import_fs24.default.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
11343
+ if (import_fs25.default.existsSync(GLOBAL_CONFIG_PATH2)) {
11344
+ return JSON.parse(import_fs25.default.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
11071
11345
  }
11072
11346
  } catch {
11073
11347
  }
11074
11348
  return {};
11075
11349
  }
11076
11350
  function writeGlobalConfigRaw(data) {
11077
- const dir = import_path27.default.dirname(GLOBAL_CONFIG_PATH2);
11078
- if (!import_fs24.default.existsSync(dir)) import_fs24.default.mkdirSync(dir, { recursive: true });
11079
- import_fs24.default.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
11351
+ const dir = import_path28.default.dirname(GLOBAL_CONFIG_PATH2);
11352
+ if (!import_fs25.default.existsSync(dir)) import_fs25.default.mkdirSync(dir, { recursive: true });
11353
+ import_fs25.default.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
11080
11354
  }
11081
11355
  function handleApproverList() {
11082
11356
  const config = getConfig();
@@ -11117,6 +11391,75 @@ function handleApproverSet(args) {
11117
11391
  const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
11118
11392
  return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
11119
11393
  }
11394
+ function handleAuditGet(args) {
11395
+ const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
11396
+ const auditPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "audit.log");
11397
+ if (!import_fs25.default.existsSync(auditPath)) return "No audit log found.";
11398
+ const lines = import_fs25.default.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
11399
+ const recent = lines.slice(-limit);
11400
+ const entries = recent.map((line) => {
11401
+ try {
11402
+ const e = JSON.parse(line);
11403
+ return `${e.ts} ${String(e.tool).padEnd(20)} ${String(e.decision).padEnd(8)} ${e.agent ?? ""}`;
11404
+ } catch {
11405
+ return line;
11406
+ }
11407
+ });
11408
+ return `Last ${entries.length} audit entries:
11409
+
11410
+ ${entries.join("\n")}`;
11411
+ }
11412
+ function handlePolicyGet() {
11413
+ const config = getConfig();
11414
+ const rules = config.policy.smartRules;
11415
+ if (rules.length === 0) return "No smart rules active.";
11416
+ const lines = rules.map((r, i) => {
11417
+ const conditions = r.conditions.map((c) => `${c.field} ${c.op} "${c.value}"`).join(` ${r.conditionMode ?? "all"} `);
11418
+ return `[${i + 1}] ${r.name ?? "(unnamed)"} tool:${r.tool} verdict:${r.verdict}
11419
+ conditions: ${conditions}
11420
+ reason: ${r.reason ?? "\u2014"}`;
11421
+ });
11422
+ return `${rules.length} active smart rules:
11423
+
11424
+ ${lines.join("\n\n")}`;
11425
+ }
11426
+ function handleRuleAdd(args) {
11427
+ const name = args.name;
11428
+ const tool = args.tool;
11429
+ const field = args.field;
11430
+ const pattern = args.pattern;
11431
+ const verdict = args.verdict;
11432
+ const reason = args.reason;
11433
+ if (!["block", "review"].includes(verdict)) {
11434
+ throw new Error(
11435
+ 'verdict must be "block" or "review" \u2014 "allow" rules are not permitted as they would weaken node9 security'
11436
+ );
11437
+ }
11438
+ try {
11439
+ new RegExp(pattern);
11440
+ } catch {
11441
+ throw new Error(`Invalid regex pattern: ${pattern}`);
11442
+ }
11443
+ const raw = readGlobalConfigRaw();
11444
+ const policy = raw.policy ?? {};
11445
+ const smartRules = policy.smartRules ?? [];
11446
+ const existing = smartRules.find(
11447
+ (r) => typeof r === "object" && r !== null && r.name === name
11448
+ );
11449
+ if (existing) throw new Error(`A rule named "${name}" already exists.`);
11450
+ smartRules.push({
11451
+ name,
11452
+ tool,
11453
+ conditions: [{ field, op: "matches", value: pattern }],
11454
+ conditionMode: "all",
11455
+ verdict,
11456
+ reason
11457
+ });
11458
+ policy.smartRules = smartRules;
11459
+ raw.policy = policy;
11460
+ writeGlobalConfigRaw(raw);
11461
+ return `Rule "${name}" added to ~/.node9/config.json \u2014 verdict: ${verdict} when ${field} matches "${pattern}"`;
11462
+ }
11120
11463
  function handleUndoList() {
11121
11464
  const history = getSnapshotHistory();
11122
11465
  if (history.length === 0) {
@@ -11200,6 +11543,12 @@ function runMcpServer() {
11200
11543
  text = handleUndoList();
11201
11544
  } else if (toolName === "node9_undo_revert") {
11202
11545
  text = handleUndoRevert(toolArgs);
11546
+ } else if (toolName === "node9_audit_get") {
11547
+ text = handleAuditGet(toolArgs);
11548
+ } else if (toolName === "node9_policy_get") {
11549
+ text = handlePolicyGet();
11550
+ } else if (toolName === "node9_rule_add") {
11551
+ text = handleRuleAdd(toolArgs);
11203
11552
  } else {
11204
11553
  process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
11205
11554
  return;
@@ -11287,22 +11636,99 @@ function registerTrustCommand(program2) {
11287
11636
  });
11288
11637
  }
11289
11638
 
11639
+ // src/cli/commands/mcp-pin.ts
11640
+ var import_chalk17 = __toESM(require("chalk"));
11641
+ function registerMcpPinCommand(program2) {
11642
+ const pinCmd = program2.command("mcp").description("Manage MCP server tool definition pinning (rug pull defense)");
11643
+ const pinSubCmd = pinCmd.command("pin").description("Manage pinned MCP server tool definitions");
11644
+ pinSubCmd.command("list").description("Show all pinned MCP servers and their tool definition hashes").action(() => {
11645
+ const result = readMcpPinsSafe();
11646
+ if (!result.ok) {
11647
+ if (result.reason === "missing") {
11648
+ console.log(import_chalk17.default.gray("\nNo MCP servers are pinned yet."));
11649
+ console.log(
11650
+ import_chalk17.default.gray("Pins are created automatically when the MCP gateway first connects.\n")
11651
+ );
11652
+ return;
11653
+ }
11654
+ console.error(import_chalk17.default.red(`
11655
+ \u274C Pin file is corrupt: ${result.detail}`));
11656
+ console.error(import_chalk17.default.yellow(" Run: node9 mcp pin reset\n"));
11657
+ process.exit(1);
11658
+ }
11659
+ const entries = Object.entries(result.pins.servers);
11660
+ if (entries.length === 0) {
11661
+ console.log(import_chalk17.default.gray("\nNo MCP servers are pinned yet."));
11662
+ console.log(
11663
+ import_chalk17.default.gray("Pins are created automatically when the MCP gateway first connects.\n")
11664
+ );
11665
+ return;
11666
+ }
11667
+ console.log(import_chalk17.default.bold("\n\u{1F512} Pinned MCP Servers\n"));
11668
+ for (const [key, entry] of entries) {
11669
+ console.log(` ${import_chalk17.default.cyan(key)} ${import_chalk17.default.gray(entry.label)}`);
11670
+ console.log(` Tools (${entry.toolCount}): ${import_chalk17.default.white(entry.toolNames.join(", "))}`);
11671
+ console.log(` Hash: ${import_chalk17.default.gray(entry.toolsHash.slice(0, 16))}...`);
11672
+ console.log(` Pinned: ${import_chalk17.default.gray(entry.pinnedAt)}`);
11673
+ console.log("");
11674
+ }
11675
+ });
11676
+ pinSubCmd.command("update <serverKey>").description(
11677
+ "Remove a pin so the next gateway connection re-pins with current tool definitions"
11678
+ ).action((serverKey) => {
11679
+ let pins;
11680
+ try {
11681
+ pins = readMcpPins();
11682
+ } catch {
11683
+ console.error(import_chalk17.default.red("\n\u274C Pin file is corrupt."));
11684
+ console.error(import_chalk17.default.yellow(" Run: node9 mcp pin reset\n"));
11685
+ process.exit(1);
11686
+ }
11687
+ if (!pins.servers[serverKey]) {
11688
+ console.error(import_chalk17.default.red(`
11689
+ \u274C No pin found for server key "${serverKey}"
11690
+ `));
11691
+ console.error(`Run ${import_chalk17.default.cyan("node9 mcp pin list")} to see pinned servers.
11692
+ `);
11693
+ process.exit(1);
11694
+ }
11695
+ const label = pins.servers[serverKey].label;
11696
+ removePin(serverKey);
11697
+ console.log(import_chalk17.default.green(`
11698
+ \u{1F513} Pin removed for ${import_chalk17.default.cyan(serverKey)}`));
11699
+ console.log(import_chalk17.default.gray(` Server: ${label}`));
11700
+ console.log(import_chalk17.default.gray(" Next connection will re-pin with current tool definitions.\n"));
11701
+ });
11702
+ pinSubCmd.command("reset").description("Clear all MCP pins (next connection to each server will re-pin)").action(() => {
11703
+ const result = readMcpPinsSafe();
11704
+ if (!result.ok && result.reason === "missing") {
11705
+ console.log(import_chalk17.default.gray("\nNo pins to clear.\n"));
11706
+ return;
11707
+ }
11708
+ const count = result.ok ? Object.keys(result.pins.servers).length : "?";
11709
+ clearAllPins();
11710
+ console.log(import_chalk17.default.green(`
11711
+ \u{1F513} Cleared ${count} MCP pin(s).`));
11712
+ console.log(import_chalk17.default.gray(" Next connection to each server will re-pin.\n"));
11713
+ });
11714
+ }
11715
+
11290
11716
  // src/cli.ts
11291
11717
  var { version } = JSON.parse(
11292
- import_fs27.default.readFileSync(import_path30.default.join(__dirname, "../package.json"), "utf-8")
11718
+ import_fs28.default.readFileSync(import_path31.default.join(__dirname, "../package.json"), "utf-8")
11293
11719
  );
11294
11720
  var program = new import_commander.Command();
11295
11721
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
11296
11722
  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) => {
11297
11723
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
11298
- const credPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "credentials.json");
11299
- if (!import_fs27.default.existsSync(import_path30.default.dirname(credPath)))
11300
- import_fs27.default.mkdirSync(import_path30.default.dirname(credPath), { recursive: true });
11724
+ const credPath = import_path31.default.join(import_os24.default.homedir(), ".node9", "credentials.json");
11725
+ if (!import_fs28.default.existsSync(import_path31.default.dirname(credPath)))
11726
+ import_fs28.default.mkdirSync(import_path31.default.dirname(credPath), { recursive: true });
11301
11727
  const profileName = options.profile || "default";
11302
11728
  let existingCreds = {};
11303
11729
  try {
11304
- if (import_fs27.default.existsSync(credPath)) {
11305
- const raw = JSON.parse(import_fs27.default.readFileSync(credPath, "utf-8"));
11730
+ if (import_fs28.default.existsSync(credPath)) {
11731
+ const raw = JSON.parse(import_fs28.default.readFileSync(credPath, "utf-8"));
11306
11732
  if (raw.apiKey) {
11307
11733
  existingCreds = {
11308
11734
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -11314,13 +11740,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
11314
11740
  } catch {
11315
11741
  }
11316
11742
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
11317
- import_fs27.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
11743
+ import_fs28.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
11318
11744
  if (profileName === "default") {
11319
- const configPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "config.json");
11745
+ const configPath = import_path31.default.join(import_os24.default.homedir(), ".node9", "config.json");
11320
11746
  let config = {};
11321
11747
  try {
11322
- if (import_fs27.default.existsSync(configPath))
11323
- config = JSON.parse(import_fs27.default.readFileSync(configPath, "utf-8"));
11748
+ if (import_fs28.default.existsSync(configPath))
11749
+ config = JSON.parse(import_fs28.default.readFileSync(configPath, "utf-8"));
11324
11750
  } catch {
11325
11751
  }
11326
11752
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -11335,19 +11761,19 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
11335
11761
  approvers.cloud = false;
11336
11762
  }
11337
11763
  s.approvers = approvers;
11338
- if (!import_fs27.default.existsSync(import_path30.default.dirname(configPath)))
11339
- import_fs27.default.mkdirSync(import_path30.default.dirname(configPath), { recursive: true });
11340
- import_fs27.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
11764
+ if (!import_fs28.default.existsSync(import_path31.default.dirname(configPath)))
11765
+ import_fs28.default.mkdirSync(import_path31.default.dirname(configPath), { recursive: true });
11766
+ import_fs28.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
11341
11767
  }
11342
11768
  if (options.profile && profileName !== "default") {
11343
- console.log(import_chalk18.default.green(`\u2705 Profile "${profileName}" saved`));
11344
- console.log(import_chalk18.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
11769
+ console.log(import_chalk19.default.green(`\u2705 Profile "${profileName}" saved`));
11770
+ console.log(import_chalk19.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
11345
11771
  } else if (options.local) {
11346
- console.log(import_chalk18.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
11347
- console.log(import_chalk18.default.gray(` All decisions stay on this machine.`));
11772
+ console.log(import_chalk19.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
11773
+ console.log(import_chalk19.default.gray(` All decisions stay on this machine.`));
11348
11774
  } else {
11349
- console.log(import_chalk18.default.green(`\u2705 Logged in \u2014 agent mode`));
11350
- console.log(import_chalk18.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
11775
+ console.log(import_chalk19.default.green(`\u2705 Logged in \u2014 agent mode`));
11776
+ console.log(import_chalk19.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
11351
11777
  }
11352
11778
  });
11353
11779
  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) => {
@@ -11355,19 +11781,19 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
11355
11781
  if (target === "claude") return await setupClaude();
11356
11782
  if (target === "cursor") return await setupCursor();
11357
11783
  if (target === "hud") return setupHud();
11358
- console.error(import_chalk18.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
11784
+ console.error(import_chalk19.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
11359
11785
  process.exit(1);
11360
11786
  });
11361
11787
  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) => {
11362
11788
  if (!target) {
11363
- console.log(import_chalk18.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
11364
- console.log(" Usage: " + import_chalk18.default.white("node9 setup <target>") + "\n");
11789
+ console.log(import_chalk19.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
11790
+ console.log(" Usage: " + import_chalk19.default.white("node9 setup <target>") + "\n");
11365
11791
  console.log(" Targets:");
11366
- console.log(" " + import_chalk18.default.green("claude") + " \u2014 Claude Code (hook mode)");
11367
- console.log(" " + import_chalk18.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
11368
- console.log(" " + import_chalk18.default.green("cursor") + " \u2014 Cursor (hook mode)");
11792
+ console.log(" " + import_chalk19.default.green("claude") + " \u2014 Claude Code (hook mode)");
11793
+ console.log(" " + import_chalk19.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
11794
+ console.log(" " + import_chalk19.default.green("cursor") + " \u2014 Cursor (hook mode)");
11369
11795
  process.stdout.write(
11370
- " " + import_chalk18.default.green("hud") + " \u2014 Claude Code security statusline\n"
11796
+ " " + import_chalk19.default.green("hud") + " \u2014 Claude Code security statusline\n"
11371
11797
  );
11372
11798
  console.log("");
11373
11799
  return;
@@ -11377,7 +11803,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
11377
11803
  if (t === "claude") return await setupClaude();
11378
11804
  if (t === "cursor") return await setupCursor();
11379
11805
  if (t === "hud") return setupHud();
11380
- console.error(import_chalk18.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
11806
+ console.error(import_chalk19.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
11381
11807
  process.exit(1);
11382
11808
  });
11383
11809
  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) => {
@@ -11388,31 +11814,31 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
11388
11814
  else if (target === "hud") fn = teardownHud;
11389
11815
  else {
11390
11816
  console.error(
11391
- import_chalk18.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
11817
+ import_chalk19.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
11392
11818
  );
11393
11819
  process.exit(1);
11394
11820
  }
11395
- console.log(import_chalk18.default.cyan(`
11821
+ console.log(import_chalk19.default.cyan(`
11396
11822
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
11397
11823
  `));
11398
11824
  try {
11399
11825
  fn();
11400
11826
  } catch (err2) {
11401
- console.error(import_chalk18.default.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
11827
+ console.error(import_chalk19.default.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
11402
11828
  process.exit(1);
11403
11829
  }
11404
- console.log(import_chalk18.default.gray("\n Restart the agent for changes to take effect."));
11830
+ console.log(import_chalk19.default.gray("\n Restart the agent for changes to take effect."));
11405
11831
  });
11406
11832
  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) => {
11407
- console.log(import_chalk18.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
11408
- console.log(import_chalk18.default.bold("Stopping daemon..."));
11833
+ console.log(import_chalk19.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
11834
+ console.log(import_chalk19.default.bold("Stopping daemon..."));
11409
11835
  try {
11410
11836
  stopDaemon();
11411
- console.log(import_chalk18.default.green(" \u2705 Daemon stopped"));
11837
+ console.log(import_chalk19.default.green(" \u2705 Daemon stopped"));
11412
11838
  } catch {
11413
- console.log(import_chalk18.default.blue(" \u2139\uFE0F Daemon was not running"));
11839
+ console.log(import_chalk19.default.blue(" \u2139\uFE0F Daemon was not running"));
11414
11840
  }
11415
- console.log(import_chalk18.default.bold("\nRemoving hooks..."));
11841
+ console.log(import_chalk19.default.bold("\nRemoving hooks..."));
11416
11842
  let teardownFailed = false;
11417
11843
  for (const [label, fn] of [
11418
11844
  ["Claude", teardownClaude],
@@ -11424,45 +11850,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
11424
11850
  } catch (err2) {
11425
11851
  teardownFailed = true;
11426
11852
  console.error(
11427
- import_chalk18.default.red(
11853
+ import_chalk19.default.red(
11428
11854
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
11429
11855
  )
11430
11856
  );
11431
11857
  }
11432
11858
  }
11433
11859
  if (options.purge) {
11434
- const node9Dir = import_path30.default.join(import_os23.default.homedir(), ".node9");
11435
- if (import_fs27.default.existsSync(node9Dir)) {
11860
+ const node9Dir = import_path31.default.join(import_os24.default.homedir(), ".node9");
11861
+ if (import_fs28.default.existsSync(node9Dir)) {
11436
11862
  const confirmed = await (0, import_prompts2.confirm)({
11437
11863
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
11438
11864
  default: false
11439
11865
  });
11440
11866
  if (confirmed) {
11441
- import_fs27.default.rmSync(node9Dir, { recursive: true });
11442
- if (import_fs27.default.existsSync(node9Dir)) {
11867
+ import_fs28.default.rmSync(node9Dir, { recursive: true });
11868
+ if (import_fs28.default.existsSync(node9Dir)) {
11443
11869
  console.error(
11444
- import_chalk18.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
11870
+ import_chalk19.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
11445
11871
  );
11446
11872
  } else {
11447
- console.log(import_chalk18.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
11873
+ console.log(import_chalk19.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
11448
11874
  }
11449
11875
  } else {
11450
- console.log(import_chalk18.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
11876
+ console.log(import_chalk19.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
11451
11877
  }
11452
11878
  } else {
11453
- console.log(import_chalk18.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
11879
+ console.log(import_chalk19.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
11454
11880
  }
11455
11881
  } else {
11456
11882
  console.log(
11457
- import_chalk18.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
11883
+ import_chalk19.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
11458
11884
  );
11459
11885
  }
11460
11886
  if (teardownFailed) {
11461
- console.error(import_chalk18.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
11887
+ console.error(import_chalk19.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
11462
11888
  process.exit(1);
11463
11889
  }
11464
- console.log(import_chalk18.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
11465
- console.log(import_chalk18.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
11890
+ console.log(import_chalk19.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
11891
+ console.log(import_chalk19.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
11466
11892
  });
11467
11893
  registerDoctorCommand(program, version);
11468
11894
  program.command("explain").description(
@@ -11475,7 +11901,7 @@ program.command("explain").description(
11475
11901
  try {
11476
11902
  args = JSON.parse(trimmed);
11477
11903
  } catch {
11478
- console.error(import_chalk18.default.red(`
11904
+ console.error(import_chalk19.default.red(`
11479
11905
  \u274C Invalid JSON: ${trimmed}
11480
11906
  `));
11481
11907
  process.exit(1);
@@ -11486,54 +11912,54 @@ program.command("explain").description(
11486
11912
  }
11487
11913
  const result = await explainPolicy(tool, args);
11488
11914
  console.log("");
11489
- console.log(import_chalk18.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
11915
+ console.log(import_chalk19.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
11490
11916
  console.log("");
11491
- console.log(` ${import_chalk18.default.bold("Tool:")} ${import_chalk18.default.white(result.tool)}`);
11917
+ console.log(` ${import_chalk19.default.bold("Tool:")} ${import_chalk19.default.white(result.tool)}`);
11492
11918
  if (argsRaw) {
11493
11919
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
11494
- console.log(` ${import_chalk18.default.bold("Input:")} ${import_chalk18.default.gray(preview)}`);
11920
+ console.log(` ${import_chalk19.default.bold("Input:")} ${import_chalk19.default.gray(preview)}`);
11495
11921
  }
11496
11922
  console.log("");
11497
- console.log(import_chalk18.default.bold("Config Sources (Waterfall):"));
11923
+ console.log(import_chalk19.default.bold("Config Sources (Waterfall):"));
11498
11924
  for (const tier of result.waterfall) {
11499
- const num = import_chalk18.default.gray(` ${tier.tier}.`);
11925
+ const num = import_chalk19.default.gray(` ${tier.tier}.`);
11500
11926
  const label = tier.label.padEnd(16);
11501
11927
  let statusStr;
11502
11928
  if (tier.tier === 1) {
11503
- statusStr = import_chalk18.default.gray(tier.note ?? "");
11929
+ statusStr = import_chalk19.default.gray(tier.note ?? "");
11504
11930
  } else if (tier.status === "active") {
11505
- const loc = tier.path ? import_chalk18.default.gray(tier.path) : "";
11506
- const note = tier.note ? import_chalk18.default.gray(`(${tier.note})`) : "";
11507
- statusStr = import_chalk18.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
11931
+ const loc = tier.path ? import_chalk19.default.gray(tier.path) : "";
11932
+ const note = tier.note ? import_chalk19.default.gray(`(${tier.note})`) : "";
11933
+ statusStr = import_chalk19.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
11508
11934
  } else {
11509
- statusStr = import_chalk18.default.gray("\u25CB " + (tier.note ?? "not found"));
11935
+ statusStr = import_chalk19.default.gray("\u25CB " + (tier.note ?? "not found"));
11510
11936
  }
11511
- console.log(`${num} ${import_chalk18.default.white(label)} ${statusStr}`);
11937
+ console.log(`${num} ${import_chalk19.default.white(label)} ${statusStr}`);
11512
11938
  }
11513
11939
  console.log("");
11514
- console.log(import_chalk18.default.bold("Policy Evaluation:"));
11940
+ console.log(import_chalk19.default.bold("Policy Evaluation:"));
11515
11941
  for (const step of result.steps) {
11516
11942
  const isFinal = step.isFinal;
11517
11943
  let icon;
11518
- if (step.outcome === "allow") icon = import_chalk18.default.green(" \u2705");
11519
- else if (step.outcome === "review") icon = import_chalk18.default.red(" \u{1F534}");
11520
- else if (step.outcome === "skip") icon = import_chalk18.default.gray(" \u2500 ");
11521
- else icon = import_chalk18.default.gray(" \u25CB ");
11944
+ if (step.outcome === "allow") icon = import_chalk19.default.green(" \u2705");
11945
+ else if (step.outcome === "review") icon = import_chalk19.default.red(" \u{1F534}");
11946
+ else if (step.outcome === "skip") icon = import_chalk19.default.gray(" \u2500 ");
11947
+ else icon = import_chalk19.default.gray(" \u25CB ");
11522
11948
  const name = step.name.padEnd(18);
11523
- const nameStr = isFinal ? import_chalk18.default.white.bold(name) : import_chalk18.default.white(name);
11524
- const detail = isFinal ? import_chalk18.default.white(step.detail) : import_chalk18.default.gray(step.detail);
11525
- const arrow = isFinal ? import_chalk18.default.yellow(" \u2190 STOP") : "";
11949
+ const nameStr = isFinal ? import_chalk19.default.white.bold(name) : import_chalk19.default.white(name);
11950
+ const detail = isFinal ? import_chalk19.default.white(step.detail) : import_chalk19.default.gray(step.detail);
11951
+ const arrow = isFinal ? import_chalk19.default.yellow(" \u2190 STOP") : "";
11526
11952
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
11527
11953
  }
11528
11954
  console.log("");
11529
11955
  if (result.decision === "allow") {
11530
- console.log(import_chalk18.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk18.default.gray(" \u2014 no approval needed"));
11956
+ console.log(import_chalk19.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk19.default.gray(" \u2014 no approval needed"));
11531
11957
  } else {
11532
11958
  console.log(
11533
- import_chalk18.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk18.default.gray(" \u2014 human approval required")
11959
+ import_chalk19.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk19.default.gray(" \u2014 human approval required")
11534
11960
  );
11535
11961
  if (result.blockedByLabel) {
11536
- console.log(import_chalk18.default.gray(` Reason: ${result.blockedByLabel}`));
11962
+ console.log(import_chalk19.default.gray(` Reason: ${result.blockedByLabel}`));
11537
11963
  }
11538
11964
  }
11539
11965
  console.log("");
@@ -11547,13 +11973,14 @@ program.command("tail").description("Stream live agent activity to the terminal"
11547
11973
  try {
11548
11974
  await startTail2(options);
11549
11975
  } catch (err2) {
11550
- console.error(import_chalk18.default.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
11976
+ console.error(import_chalk19.default.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
11551
11977
  process.exit(1);
11552
11978
  }
11553
11979
  });
11554
11980
  registerWatchCommand(program);
11555
11981
  registerMcpGatewayCommand(program);
11556
11982
  registerMcpServerCommand(program);
11983
+ registerMcpPinCommand(program);
11557
11984
  registerCheckCommand(program);
11558
11985
  registerLogCommand(program);
11559
11986
  program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
@@ -11578,14 +12005,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
11578
12005
  Run "node9 addto claude" to register it as the statusLine.`
11579
12006
  ).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
11580
12007
  if (subcommand === "debug") {
11581
- const flagFile = import_path30.default.join(import_os23.default.homedir(), ".node9", "hud-debug");
12008
+ const flagFile = import_path31.default.join(import_os24.default.homedir(), ".node9", "hud-debug");
11582
12009
  if (state === "on") {
11583
- import_fs27.default.mkdirSync(import_path30.default.dirname(flagFile), { recursive: true });
11584
- import_fs27.default.writeFileSync(flagFile, "");
12010
+ import_fs28.default.mkdirSync(import_path31.default.dirname(flagFile), { recursive: true });
12011
+ import_fs28.default.writeFileSync(flagFile, "");
11585
12012
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
11586
12013
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
11587
12014
  } else if (state === "off") {
11588
- if (import_fs27.default.existsSync(flagFile)) import_fs27.default.unlinkSync(flagFile);
12015
+ if (import_fs28.default.existsSync(flagFile)) import_fs28.default.unlinkSync(flagFile);
11589
12016
  console.log("HUD debug logging disabled.");
11590
12017
  } else {
11591
12018
  console.error("Usage: node9 hud debug on|off");
@@ -11600,7 +12027,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
11600
12027
  const ms = parseDuration(options.duration);
11601
12028
  if (ms === null) {
11602
12029
  console.error(
11603
- import_chalk18.default.red(`
12030
+ import_chalk19.default.red(`
11604
12031
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
11605
12032
  `)
11606
12033
  );
@@ -11608,20 +12035,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
11608
12035
  }
11609
12036
  pauseNode9(ms, options.duration);
11610
12037
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
11611
- console.log(import_chalk18.default.yellow(`
12038
+ console.log(import_chalk19.default.yellow(`
11612
12039
  \u23F8 Node9 paused until ${expiresAt}`));
11613
- console.log(import_chalk18.default.gray(` All tool calls will be allowed without review.`));
11614
- console.log(import_chalk18.default.gray(` Run "node9 resume" to re-enable early.
12040
+ console.log(import_chalk19.default.gray(` All tool calls will be allowed without review.`));
12041
+ console.log(import_chalk19.default.gray(` Run "node9 resume" to re-enable early.
11615
12042
  `));
11616
12043
  });
11617
12044
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
11618
12045
  const { paused } = checkPause();
11619
12046
  if (!paused) {
11620
- console.log(import_chalk18.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
12047
+ console.log(import_chalk19.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
11621
12048
  return;
11622
12049
  }
11623
12050
  resumeNode9();
11624
- console.log(import_chalk18.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
12051
+ console.log(import_chalk19.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
11625
12052
  });
11626
12053
  var HOOK_BASED_AGENTS = {
11627
12054
  claude: "claude",
@@ -11634,15 +12061,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
11634
12061
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
11635
12062
  const target = HOOK_BASED_AGENTS[firstArg2];
11636
12063
  console.error(
11637
- import_chalk18.default.yellow(`
12064
+ import_chalk19.default.yellow(`
11638
12065
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
11639
12066
  );
11640
- console.error(import_chalk18.default.white(`
12067
+ console.error(import_chalk19.default.white(`
11641
12068
  "${target}" uses its own hook system. Use:`));
11642
12069
  console.error(
11643
- import_chalk18.default.green(` node9 addto ${target} `) + import_chalk18.default.gray("# one-time setup")
12070
+ import_chalk19.default.green(` node9 addto ${target} `) + import_chalk19.default.gray("# one-time setup")
11644
12071
  );
11645
- console.error(import_chalk18.default.green(` ${target} `) + import_chalk18.default.gray("# run normally"));
12072
+ console.error(import_chalk19.default.green(` ${target} `) + import_chalk19.default.gray("# run normally"));
11646
12073
  process.exit(1);
11647
12074
  }
11648
12075
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -11659,7 +12086,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
11659
12086
  }
11660
12087
  );
11661
12088
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
11662
- console.error(import_chalk18.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
12089
+ console.error(import_chalk19.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
11663
12090
  const daemonReady = await autoStartDaemonAndWait();
11664
12091
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
11665
12092
  }
@@ -11672,12 +12099,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
11672
12099
  }
11673
12100
  if (!result.approved) {
11674
12101
  console.error(
11675
- import_chalk18.default.red(`
12102
+ import_chalk19.default.red(`
11676
12103
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
11677
12104
  );
11678
12105
  process.exit(1);
11679
12106
  }
11680
- console.error(import_chalk18.default.green("\n\u2705 Approved \u2014 running command...\n"));
12107
+ console.error(import_chalk19.default.green("\n\u2705 Approved \u2014 running command...\n"));
11681
12108
  await runProxy(fullCommand);
11682
12109
  } else {
11683
12110
  program.help();
@@ -11692,9 +12119,9 @@ if (process.argv[2] !== "daemon") {
11692
12119
  const isCheckHook = process.argv[2] === "check";
11693
12120
  if (isCheckHook) {
11694
12121
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
11695
- const logPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "hook-debug.log");
12122
+ const logPath = import_path31.default.join(import_os24.default.homedir(), ".node9", "hook-debug.log");
11696
12123
  const msg = reason instanceof Error ? reason.message : String(reason);
11697
- import_fs27.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
12124
+ import_fs28.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
11698
12125
  `);
11699
12126
  }
11700
12127
  process.exit(0);