@node9/proxy 1.5.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +70 -4
  2. package/dist/cli.js +493 -140
  3. package/dist/cli.mjs +491 -138
  4. package/package.json +1 -1
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 path30 = issue.path.length > 0 ? issue.path.join(".") : "root";
143
- return ` \u2022 ${path30}: ${issue.message}`;
142
+ const path31 = issue.path.length > 0 ? issue.path.join(".") : "root";
143
+ return ` \u2022 ${path31}: ${issue.message}`;
144
144
  });
145
145
  return {
146
146
  sanitized,
@@ -297,9 +297,9 @@ function readShieldsFile() {
297
297
  (e) => typeof e === "string" && e.length > 0 && e in SHIELDS
298
298
  ) : [];
299
299
  return { active, overrides: validateOverrides(parsed.overrides) };
300
- } catch (err) {
301
- if (err.code !== "ENOENT") {
302
- process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
300
+ } catch (err2) {
301
+ if (err2.code !== "ENOENT") {
302
+ process.stderr.write(`[node9] Warning: could not read shields state: ${String(err2)}
303
303
  `);
304
304
  }
305
305
  return { active: [] };
@@ -714,8 +714,8 @@ function tryLoadConfig(filePath) {
714
714
  let raw;
715
715
  try {
716
716
  raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
717
- } catch (err) {
718
- const msg = err instanceof Error ? err.message : String(err);
717
+ } catch (err2) {
718
+ const msg = err2 instanceof Error ? err2.message : String(err2);
719
719
  process.stderr.write(
720
720
  `
721
721
  \u26A0\uFE0F Node9: Failed to parse ${filePath}
@@ -1043,10 +1043,10 @@ function getCompiledRegex(pattern, flags = "") {
1043
1043
  regexCache.set(key, cached);
1044
1044
  return cached;
1045
1045
  }
1046
- const err = validateRegex(pattern);
1047
- if (err) {
1046
+ const err2 = validateRegex(pattern);
1047
+ if (err2) {
1048
1048
  if (process.env.NODE9_DEBUG === "1")
1049
- console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
1049
+ console.error(`[Node9] Regex blocked: ${err2} \u2014 pattern: "${pattern}"`);
1050
1050
  return null;
1051
1051
  }
1052
1052
  try {
@@ -1081,8 +1081,8 @@ function scanFilePath(filePath, cwd = process.cwd()) {
1081
1081
  try {
1082
1082
  const absolute = path4.resolve(cwd, filePath);
1083
1083
  resolved = fs4.realpathSync.native(absolute);
1084
- } catch (err) {
1085
- const code = err.code;
1084
+ } catch (err2) {
1085
+ const code = err2.code;
1086
1086
  if (code === "ENOENT" || code === "ENOTDIR") {
1087
1087
  resolved = path4.resolve(cwd, filePath);
1088
1088
  } else {
@@ -1757,9 +1757,9 @@ function matchesPattern(text, patterns) {
1757
1757
  const withoutDotSlash = text.replace(/^\.\//, "");
1758
1758
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1759
1759
  }
1760
- function getNestedValue(obj, path30) {
1760
+ function getNestedValue(obj, path31) {
1761
1761
  if (!obj || typeof obj !== "object") return null;
1762
- return path30.split(".").reduce((prev, curr) => prev?.[curr], obj);
1762
+ return path31.split(".").reduce((prev, curr) => prev?.[curr], obj);
1763
1763
  }
1764
1764
  function shouldSnapshot(toolName, args, config) {
1765
1765
  if (!config.settings.enableUndo) return false;
@@ -2409,9 +2409,9 @@ function writeTrustSession(toolName, durationMs) {
2409
2409
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > now);
2410
2410
  trust.entries.push({ tool: toolName, expiry: now + durationMs });
2411
2411
  atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
2412
- } catch (err) {
2412
+ } catch (err2) {
2413
2413
  if (process.env.NODE9_DEBUG === "1") {
2414
- console.error("[Node9 Trust Error]:", err);
2414
+ console.error("[Node9 Trust Error]:", err2);
2415
2415
  }
2416
2416
  }
2417
2417
  }
@@ -2610,13 +2610,13 @@ async function checkTaint(paths) {
2610
2610
  signal: AbortSignal.timeout(2e3)
2611
2611
  });
2612
2612
  return await res.json();
2613
- } catch (err) {
2613
+ } catch (err2) {
2614
2614
  try {
2615
2615
  const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
2616
2616
  appendToLog2(HOOK_DEBUG_LOG2, {
2617
2617
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2618
2618
  event: "checkTaint-error",
2619
- error: String(err),
2619
+ error: String(err2),
2620
2620
  paths
2621
2621
  });
2622
2622
  } catch {
@@ -3109,10 +3109,10 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
3109
3109
  `
3110
3110
  );
3111
3111
  }
3112
- } catch (err) {
3112
+ } catch (err2) {
3113
3113
  fs10.appendFileSync(
3114
3114
  HOOK_DEBUG_LOG,
3115
- `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
3115
+ `[resolve-cloud] PATCH failed for ${requestId}: ${err2.message}
3116
3116
  `
3117
3117
  );
3118
3118
  }
@@ -3479,10 +3479,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3479
3479
  blockedBy: cloudResult.approved ? void 0 : "team-policy",
3480
3480
  blockedByLabel: "Organization Policy (SaaS)"
3481
3481
  };
3482
- } catch (err) {
3483
- const error = err;
3484
- if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err;
3485
- throw err;
3482
+ } catch (err2) {
3483
+ const error = err2;
3484
+ if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err2;
3485
+ throw err2;
3486
3486
  }
3487
3487
  })()
3488
3488
  );
@@ -3575,10 +3575,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3575
3575
  }
3576
3576
  };
3577
3577
  for (const p of racePromises) {
3578
- p.then(finish).catch((err) => {
3579
- if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
3578
+ p.then(finish).catch((err2) => {
3579
+ if (err2.name === "AbortError" || err2.message?.includes("canceled") || err2.message?.includes("Aborted"))
3580
3580
  return;
3581
- if (err.message === "Abandoned") {
3581
+ if (err2.message === "Abandoned") {
3582
3582
  finish({
3583
3583
  approved: false,
3584
3584
  reason: "Browser dashboard closed without making a decision.",
@@ -5598,21 +5598,21 @@ function atomicWriteSync2(filePath, data, options) {
5598
5598
  const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
5599
5599
  try {
5600
5600
  fs13.writeFileSync(tmpPath, data, options);
5601
- } catch (err) {
5601
+ } catch (err2) {
5602
5602
  try {
5603
5603
  fs13.unlinkSync(tmpPath);
5604
5604
  } catch {
5605
5605
  }
5606
- throw err;
5606
+ throw err2;
5607
5607
  }
5608
5608
  try {
5609
5609
  fs13.renameSync(tmpPath, filePath);
5610
- } catch (err) {
5610
+ } catch (err2) {
5611
5611
  try {
5612
5612
  fs13.unlinkSync(tmpPath);
5613
5613
  } catch {
5614
5614
  }
5615
- throw err;
5615
+ throw err2;
5616
5616
  }
5617
5617
  }
5618
5618
  function redactArgs(value) {
@@ -5791,6 +5791,16 @@ function startActivitySocket() {
5791
5791
  sessionHistory.recordTestFail(data.ts);
5792
5792
  return;
5793
5793
  }
5794
+ if (data.status === "snapshot") {
5795
+ broadcast("snapshot", {
5796
+ hash: data.hash,
5797
+ tool: data.tool,
5798
+ argsSummary: data.argsSummary,
5799
+ fileCount: data.fileCount,
5800
+ ts: data.ts
5801
+ });
5802
+ return;
5803
+ }
5794
5804
  if (data.status === "pending") {
5795
5805
  broadcast("activity", {
5796
5806
  id: data.id,
@@ -5922,21 +5932,21 @@ function patchConfig(configPath, patch) {
5922
5932
  const tmp = configPath + ".node9-tmp";
5923
5933
  try {
5924
5934
  fs14.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5925
- } catch (err) {
5935
+ } catch (err2) {
5926
5936
  try {
5927
5937
  fs14.unlinkSync(tmp);
5928
5938
  } catch {
5929
5939
  }
5930
- throw err;
5940
+ throw err2;
5931
5941
  }
5932
5942
  try {
5933
5943
  fs14.renameSync(tmp, configPath);
5934
- } catch (err) {
5944
+ } catch (err2) {
5935
5945
  try {
5936
5946
  fs14.unlinkSync(tmp);
5937
5947
  } catch {
5938
5948
  }
5939
- throw err;
5949
+ throw err2;
5940
5950
  }
5941
5951
  }
5942
5952
  var GLOBAL_CONFIG_PATH;
@@ -6191,11 +6201,11 @@ data: ${JSON.stringify(item.data)}
6191
6201
  e.earlyDecision = decision;
6192
6202
  e.earlyReason = result.reason;
6193
6203
  }
6194
- }).catch((err) => {
6204
+ }).catch((err2) => {
6195
6205
  const e = pending.get(id);
6196
6206
  if (!e) return;
6197
6207
  clearTimeout(e.timer);
6198
- const reason = err?.reason || "No response \u2014 request timed out";
6208
+ const reason = err2?.reason || "No response \u2014 request timed out";
6199
6209
  if (e.waiter) e.waiter("deny", reason);
6200
6210
  else {
6201
6211
  e.earlyDecision = "deny";
@@ -6322,8 +6332,8 @@ data: ${JSON.stringify(item.data)}
6322
6332
  const s = getGlobalSettings();
6323
6333
  res.writeHead(200, { "Content-Type": "application/json" });
6324
6334
  return res.end(JSON.stringify({ ...s, autoStarted }));
6325
- } catch (err) {
6326
- console.error(chalk2.red("[node9 daemon] GET /settings failed:"), err);
6335
+ } catch (err2) {
6336
+ console.error(chalk2.red("[node9 daemon] GET /settings failed:"), err2);
6327
6337
  res.writeHead(500, { "Content-Type": "application/json" });
6328
6338
  return res.end(JSON.stringify({ error: "internal" }));
6329
6339
  }
@@ -6347,8 +6357,8 @@ data: ${JSON.stringify(item.data)}
6347
6357
  };
6348
6358
  res.writeHead(200, { "Content-Type": "application/json" });
6349
6359
  return res.end(JSON.stringify(status));
6350
- } catch (err) {
6351
- console.error(chalk2.red("[node9 daemon] GET /status failed:"), err);
6360
+ } catch (err2) {
6361
+ console.error(chalk2.red("[node9 daemon] GET /status failed:"), err2);
6352
6362
  res.writeHead(500, { "Content-Type": "application/json" });
6353
6363
  return res.end(JSON.stringify({ error: "internal" }));
6354
6364
  }
@@ -6388,8 +6398,8 @@ data: ${JSON.stringify(item.data)}
6388
6398
  const s = getGlobalSettings();
6389
6399
  res.writeHead(200, { "Content-Type": "application/json" });
6390
6400
  return res.end(JSON.stringify({ hasKey: hasStoredSlackKey(), enabled: s.slackEnabled }));
6391
- } catch (err) {
6392
- console.error(chalk2.red("[node9 daemon] GET /slack-status failed:"), err);
6401
+ } catch (err2) {
6402
+ console.error(chalk2.red("[node9 daemon] GET /slack-status failed:"), err2);
6393
6403
  res.writeHead(500, { "Content-Type": "application/json" });
6394
6404
  return res.end(JSON.stringify({ error: "internal" }));
6395
6405
  }
@@ -6549,10 +6559,10 @@ data: ${JSON.stringify(item.data)}
6549
6559
  broadcast("suggestion:resolved", { id, status: "applied" });
6550
6560
  res.writeHead(200, { "Content-Type": "application/json" });
6551
6561
  return res.end(JSON.stringify({ ok: true }));
6552
- } catch (err) {
6553
- console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
6562
+ } catch (err2) {
6563
+ console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err2);
6554
6564
  res.writeHead(500, { "Content-Type": "application/json" });
6555
- return res.end(JSON.stringify({ error: String(err) }));
6565
+ return res.end(JSON.stringify({ error: String(err2) }));
6556
6566
  }
6557
6567
  }
6558
6568
  if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
@@ -6767,10 +6777,10 @@ __export(tail_exports, {
6767
6777
  });
6768
6778
  import http2 from "http";
6769
6779
  import chalk17 from "chalk";
6770
- import fs24 from "fs";
6771
- import os20 from "os";
6772
- import path27 from "path";
6773
- import readline4 from "readline";
6780
+ import fs25 from "fs";
6781
+ import os21 from "os";
6782
+ import path28 from "path";
6783
+ import readline5 from "readline";
6774
6784
  import { spawn as spawn9, execSync as execSync3 } from "child_process";
6775
6785
  function getIcon(tool) {
6776
6786
  const t = tool.toLowerCase();
@@ -6798,8 +6808,8 @@ function renderResult(activity, result) {
6798
6808
  status = chalk17.red("\u2717 BLOCK");
6799
6809
  }
6800
6810
  if (process.stdout.isTTY) {
6801
- readline4.clearLine(process.stdout, 0);
6802
- readline4.cursorTo(process.stdout, 0);
6811
+ readline5.clearLine(process.stdout, 0);
6812
+ readline5.cursorTo(process.stdout, 0);
6803
6813
  }
6804
6814
  console.log(`${base} ${status}`);
6805
6815
  }
@@ -6809,9 +6819,9 @@ function renderPending(activity) {
6809
6819
  }
6810
6820
  async function ensureDaemon() {
6811
6821
  let pidPort = null;
6812
- if (fs24.existsSync(PID_FILE)) {
6822
+ if (fs25.existsSync(PID_FILE)) {
6813
6823
  try {
6814
- const { port } = JSON.parse(fs24.readFileSync(PID_FILE, "utf-8"));
6824
+ const { port } = JSON.parse(fs25.readFileSync(PID_FILE, "utf-8"));
6815
6825
  pidPort = port;
6816
6826
  } catch {
6817
6827
  console.error(chalk17.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -6948,7 +6958,7 @@ async function startTail(options = {}) {
6948
6958
  res.resume();
6949
6959
  }
6950
6960
  );
6951
- req2.once("error", (err) => resolve({ ok: false, code: err.code }));
6961
+ req2.once("error", (err2) => resolve({ ok: false, code: err2.code }));
6952
6962
  req2.setTimeout(2e3, () => {
6953
6963
  resolve({ ok: false, code: "ETIMEDOUT" });
6954
6964
  req2.destroy();
@@ -6975,10 +6985,10 @@ async function startTail(options = {}) {
6975
6985
  let cancelActiveCard = null;
6976
6986
  const localAllowCounts = /* @__PURE__ */ new Map();
6977
6987
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
6978
- if (canApprove) readline4.emitKeypressEvents(process.stdin);
6988
+ if (canApprove) readline5.emitKeypressEvents(process.stdin);
6979
6989
  function clearCard() {
6980
6990
  if (cardLineCount > 0) {
6981
- readline4.moveCursor(process.stdout, 0, -cardLineCount);
6991
+ readline5.moveCursor(process.stdout, 0, -cardLineCount);
6982
6992
  process.stdout.write(ERASE_DOWN);
6983
6993
  cardLineCount = 0;
6984
6994
  }
@@ -7054,11 +7064,11 @@ async function startTail(options = {}) {
7054
7064
  } else {
7055
7065
  httpDecision = action;
7056
7066
  }
7057
- postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
7067
+ postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7058
7068
  try {
7059
- fs24.appendFileSync(
7060
- path27.join(os20.homedir(), ".node9", "hook-debug.log"),
7061
- `[tail] POST /decision failed: ${String(err)}
7069
+ fs25.appendFileSync(
7070
+ path28.join(os21.homedir(), ".node9", "hook-debug.log"),
7071
+ `[tail] POST /decision failed: ${String(err2)}
7062
7072
  `
7063
7073
  );
7064
7074
  } catch {
@@ -7156,8 +7166,8 @@ async function startTail(options = {}) {
7156
7166
  clearCard();
7157
7167
  process.stdout.write(SHOW_CURSOR);
7158
7168
  if (process.stdout.isTTY) {
7159
- readline4.clearLine(process.stdout, 0);
7160
- readline4.cursorTo(process.stdout, 0);
7169
+ readline5.clearLine(process.stdout, 0);
7170
+ readline5.cursorTo(process.stdout, 0);
7161
7171
  }
7162
7172
  console.log(chalk17.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7163
7173
  process.exit(0);
@@ -7172,7 +7182,7 @@ async function startTail(options = {}) {
7172
7182
  let currentData = "";
7173
7183
  res.on("error", () => {
7174
7184
  });
7175
- const rl = readline4.createInterface({ input: res, crlfDelay: Infinity });
7185
+ const rl = readline5.createInterface({ input: res, crlfDelay: Infinity });
7176
7186
  rl.on("error", () => {
7177
7187
  });
7178
7188
  rl.on("line", (line) => {
@@ -7192,8 +7202,8 @@ async function startTail(options = {}) {
7192
7202
  clearCard();
7193
7203
  process.stdout.write(SHOW_CURSOR);
7194
7204
  if (process.stdout.isTTY) {
7195
- readline4.clearLine(process.stdout, 0);
7196
- readline4.cursorTo(process.stdout, 0);
7205
+ readline5.clearLine(process.stdout, 0);
7206
+ readline5.cursorTo(process.stdout, 0);
7197
7207
  }
7198
7208
  console.log(chalk17.red("\n\u274C Daemon disconnected."));
7199
7209
  process.exit(1);
@@ -7271,6 +7281,18 @@ async function startTail(options = {}) {
7271
7281
  const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7272
7282
  if (slowTool) renderPending(data);
7273
7283
  }
7284
+ if (event === "snapshot") {
7285
+ const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
7286
+ const hash = data.hash ?? "";
7287
+ const summary = data.argsSummary ?? data.tool;
7288
+ const fileCount = data.fileCount ?? 0;
7289
+ const files = fileCount > 0 ? chalk17.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7290
+ process.stdout.write(
7291
+ `${chalk17.dim(time)} ${chalk17.cyan("\u{1F4F8} snapshot")} ${chalk17.dim(hash)} ${summary}${files}
7292
+ `
7293
+ );
7294
+ return;
7295
+ }
7274
7296
  if (event === "activity-result") {
7275
7297
  const original = activityPending.get(data.id);
7276
7298
  if (original) {
@@ -7279,8 +7301,8 @@ async function startTail(options = {}) {
7279
7301
  }
7280
7302
  }
7281
7303
  }
7282
- req.on("error", (err) => {
7283
- const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
7304
+ req.on("error", (err2) => {
7305
+ const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
7284
7306
  console.error(chalk17.red(`
7285
7307
  \u274C ${msg}`));
7286
7308
  process.exit(1);
@@ -7293,7 +7315,7 @@ var init_tail = __esm({
7293
7315
  init_daemon2();
7294
7316
  init_daemon();
7295
7317
  init_core();
7296
- PID_FILE = path27.join(os20.homedir(), ".node9", "daemon.pid");
7318
+ PID_FILE = path28.join(os21.homedir(), ".node9", "daemon.pid");
7297
7319
  ICONS = {
7298
7320
  bash: "\u{1F4BB}",
7299
7321
  shell: "\u{1F4BB}",
@@ -7332,9 +7354,9 @@ __export(hud_exports, {
7332
7354
  main: () => main,
7333
7355
  renderEnvironmentLine: () => renderEnvironmentLine
7334
7356
  });
7335
- import fs25 from "fs";
7336
- import path28 from "path";
7337
- import os21 from "os";
7357
+ import fs26 from "fs";
7358
+ import path29 from "path";
7359
+ import os22 from "os";
7338
7360
  import http3 from "http";
7339
7361
  async function readStdin() {
7340
7362
  const chunks = [];
@@ -7410,9 +7432,9 @@ function formatTimeLeft(resetsAt) {
7410
7432
  return ` (${m}m left)`;
7411
7433
  }
7412
7434
  function safeReadJson(filePath) {
7413
- if (!fs25.existsSync(filePath)) return null;
7435
+ if (!fs26.existsSync(filePath)) return null;
7414
7436
  try {
7415
- return JSON.parse(fs25.readFileSync(filePath, "utf-8"));
7437
+ return JSON.parse(fs26.readFileSync(filePath, "utf-8"));
7416
7438
  } catch {
7417
7439
  return null;
7418
7440
  }
@@ -7433,12 +7455,12 @@ function countHooksInFile(filePath) {
7433
7455
  return Object.keys(cfg.hooks).length;
7434
7456
  }
7435
7457
  function countRulesInDir(rulesDir) {
7436
- if (!fs25.existsSync(rulesDir)) return 0;
7458
+ if (!fs26.existsSync(rulesDir)) return 0;
7437
7459
  let count = 0;
7438
7460
  try {
7439
- for (const entry of fs25.readdirSync(rulesDir, { withFileTypes: true })) {
7461
+ for (const entry of fs26.readdirSync(rulesDir, { withFileTypes: true })) {
7440
7462
  if (entry.isDirectory()) {
7441
- count += countRulesInDir(path28.join(rulesDir, entry.name));
7463
+ count += countRulesInDir(path29.join(rulesDir, entry.name));
7442
7464
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
7443
7465
  count++;
7444
7466
  }
@@ -7449,46 +7471,46 @@ function countRulesInDir(rulesDir) {
7449
7471
  }
7450
7472
  function isSamePath(a, b) {
7451
7473
  try {
7452
- return path28.resolve(a) === path28.resolve(b);
7474
+ return path29.resolve(a) === path29.resolve(b);
7453
7475
  } catch {
7454
7476
  return false;
7455
7477
  }
7456
7478
  }
7457
7479
  function countConfigs(cwd) {
7458
- const homeDir2 = os21.homedir();
7459
- const claudeDir = path28.join(homeDir2, ".claude");
7480
+ const homeDir2 = os22.homedir();
7481
+ const claudeDir = path29.join(homeDir2, ".claude");
7460
7482
  let claudeMdCount = 0;
7461
7483
  let rulesCount = 0;
7462
7484
  let hooksCount = 0;
7463
7485
  const userMcpServers = /* @__PURE__ */ new Set();
7464
7486
  const projectMcpServers = /* @__PURE__ */ new Set();
7465
- if (fs25.existsSync(path28.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7466
- rulesCount += countRulesInDir(path28.join(claudeDir, "rules"));
7467
- const userSettings = path28.join(claudeDir, "settings.json");
7487
+ if (fs26.existsSync(path29.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7488
+ rulesCount += countRulesInDir(path29.join(claudeDir, "rules"));
7489
+ const userSettings = path29.join(claudeDir, "settings.json");
7468
7490
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7469
7491
  hooksCount += countHooksInFile(userSettings);
7470
- const userClaudeJson = path28.join(homeDir2, ".claude.json");
7492
+ const userClaudeJson = path29.join(homeDir2, ".claude.json");
7471
7493
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7472
7494
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7473
7495
  userMcpServers.delete(name);
7474
7496
  }
7475
7497
  if (cwd) {
7476
- if (fs25.existsSync(path28.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7477
- if (fs25.existsSync(path28.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7478
- const projectClaudeDir = path28.join(cwd, ".claude");
7498
+ if (fs26.existsSync(path29.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7499
+ if (fs26.existsSync(path29.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7500
+ const projectClaudeDir = path29.join(cwd, ".claude");
7479
7501
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7480
7502
  if (!overlapsUserScope) {
7481
- if (fs25.existsSync(path28.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7482
- rulesCount += countRulesInDir(path28.join(projectClaudeDir, "rules"));
7483
- const projSettings = path28.join(projectClaudeDir, "settings.json");
7503
+ if (fs26.existsSync(path29.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7504
+ rulesCount += countRulesInDir(path29.join(projectClaudeDir, "rules"));
7505
+ const projSettings = path29.join(projectClaudeDir, "settings.json");
7484
7506
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7485
7507
  hooksCount += countHooksInFile(projSettings);
7486
7508
  }
7487
- if (fs25.existsSync(path28.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7488
- const localSettings = path28.join(projectClaudeDir, "settings.local.json");
7509
+ if (fs26.existsSync(path29.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7510
+ const localSettings = path29.join(projectClaudeDir, "settings.local.json");
7489
7511
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7490
7512
  hooksCount += countHooksInFile(localSettings);
7491
- const mcpJsonServers = getMcpServerNames(path28.join(cwd, ".mcp.json"));
7513
+ const mcpJsonServers = getMcpServerNames(path29.join(cwd, ".mcp.json"));
7492
7514
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7493
7515
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7494
7516
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -7601,11 +7623,11 @@ async function main() {
7601
7623
  try {
7602
7624
  const cwd = stdin.cwd ?? process.cwd();
7603
7625
  for (const configPath of [
7604
- path28.join(cwd, "node9.config.json"),
7605
- path28.join(os21.homedir(), ".node9", "config.json")
7626
+ path29.join(cwd, "node9.config.json"),
7627
+ path29.join(os22.homedir(), ".node9", "config.json")
7606
7628
  ]) {
7607
- if (!fs25.existsSync(configPath)) continue;
7608
- const cfg = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
7629
+ if (!fs26.existsSync(configPath)) continue;
7630
+ const cfg = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
7609
7631
  const hud = cfg.settings?.hud;
7610
7632
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7611
7633
  }
@@ -7654,6 +7676,16 @@ import path14 from "path";
7654
7676
  import os10 from "os";
7655
7677
  import chalk from "chalk";
7656
7678
  import { confirm } from "@inquirer/prompts";
7679
+ var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7680
+ function hasNode9McpServer(servers) {
7681
+ const entry = servers["node9"];
7682
+ return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
7683
+ }
7684
+ function removeNode9McpServer(servers) {
7685
+ if (!hasNode9McpServer(servers)) return false;
7686
+ delete servers["node9"];
7687
+ return true;
7688
+ }
7657
7689
  function printDaemonTip() {
7658
7690
  console.log(
7659
7691
  chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk.white("\n To view your history or manage persistent rules, run:") + chalk.green("\n node9 daemon --openui")
@@ -7711,6 +7743,10 @@ function teardownClaude() {
7711
7743
  const claudeConfig = readJson(mcpPath);
7712
7744
  if (claudeConfig?.mcpServers) {
7713
7745
  let mcpChanged = false;
7746
+ if (removeNode9McpServer(claudeConfig.mcpServers)) {
7747
+ mcpChanged = true;
7748
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.claude.json"));
7749
+ }
7714
7750
  for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
7715
7751
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7716
7752
  const [originalCmd, ...originalArgs] = server.args;
@@ -7754,6 +7790,10 @@ function teardownGemini() {
7754
7790
  }
7755
7791
  }
7756
7792
  if (settings.mcpServers) {
7793
+ if (removeNode9McpServer(settings.mcpServers)) {
7794
+ changed = true;
7795
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.gemini/settings.json"));
7796
+ }
7757
7797
  for (const [name, server] of Object.entries(settings.mcpServers)) {
7758
7798
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7759
7799
  const [originalCmd, ...originalArgs] = server.args;
@@ -7782,6 +7822,10 @@ function teardownCursor() {
7782
7822
  return;
7783
7823
  }
7784
7824
  let changed = false;
7825
+ if (removeNode9McpServer(mcpConfig.mcpServers)) {
7826
+ changed = true;
7827
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.cursor/mcp.json"));
7828
+ }
7785
7829
  for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
7786
7830
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7787
7831
  const [originalCmd, ...originalArgs] = server.args;
@@ -7807,6 +7851,7 @@ async function setupClaude() {
7807
7851
  const claudeConfig = readJson(mcpPath) ?? {};
7808
7852
  const settings = readJson(hooksPath) ?? {};
7809
7853
  const servers = claudeConfig.mcpServers ?? {};
7854
+ let hooksChanged = false;
7810
7855
  let anythingChanged = false;
7811
7856
  if (!settings.hooks) settings.hooks = {};
7812
7857
  const hasPreHook = settings.hooks.PreToolUse?.some(
@@ -7819,6 +7864,7 @@ async function setupClaude() {
7819
7864
  hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
7820
7865
  });
7821
7866
  console.log(chalk.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
7867
+ hooksChanged = true;
7822
7868
  anythingChanged = true;
7823
7869
  }
7824
7870
  const hasPostHook = settings.hooks.PostToolUse?.some(
@@ -7831,9 +7877,17 @@ async function setupClaude() {
7831
7877
  hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
7832
7878
  });
7833
7879
  console.log(chalk.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
7880
+ hooksChanged = true;
7834
7881
  anythingChanged = true;
7835
7882
  }
7836
- if (anythingChanged) {
7883
+ if (!hasNode9McpServer(servers)) {
7884
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
7885
+ claudeConfig.mcpServers = servers;
7886
+ writeJson(mcpPath, claudeConfig);
7887
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
7888
+ anythingChanged = true;
7889
+ }
7890
+ if (hooksChanged) {
7837
7891
  writeJson(hooksPath, settings);
7838
7892
  console.log("");
7839
7893
  }
@@ -7881,6 +7935,7 @@ async function setupGemini() {
7881
7935
  const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
7882
7936
  const settings = readJson(settingsPath) ?? {};
7883
7937
  const servers = settings.mcpServers ?? {};
7938
+ let hooksChanged = false;
7884
7939
  let anythingChanged = false;
7885
7940
  if (!settings.hooks) settings.hooks = {};
7886
7941
  const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
@@ -7901,6 +7956,7 @@ async function setupGemini() {
7901
7956
  ]
7902
7957
  });
7903
7958
  console.log(chalk.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
7959
+ hooksChanged = true;
7904
7960
  anythingChanged = true;
7905
7961
  }
7906
7962
  const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
@@ -7914,9 +7970,17 @@ async function setupGemini() {
7914
7970
  hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
7915
7971
  });
7916
7972
  console.log(chalk.green(" \u2705 AfterTool hook added \u2192 node9 log"));
7973
+ hooksChanged = true;
7917
7974
  anythingChanged = true;
7918
7975
  }
7919
- if (anythingChanged) {
7976
+ if (!hasNode9McpServer(servers)) {
7977
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
7978
+ settings.mcpServers = servers;
7979
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
7980
+ hooksChanged = true;
7981
+ anythingChanged = true;
7982
+ }
7983
+ if (hooksChanged) {
7920
7984
  writeJson(settingsPath, settings);
7921
7985
  console.log("");
7922
7986
  }
@@ -7963,10 +8027,10 @@ function detectAgents(homeDir2 = os10.homedir()) {
7963
8027
  const exists = (p) => {
7964
8028
  try {
7965
8029
  return fs11.existsSync(p);
7966
- } catch (err) {
7967
- const code = err.code;
8030
+ } catch (err2) {
8031
+ const code = err2.code;
7968
8032
  if (code !== "ENOENT") {
7969
- process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
8033
+ process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err2)}
7970
8034
  `);
7971
8035
  }
7972
8036
  return false;
@@ -7984,6 +8048,13 @@ async function setupCursor() {
7984
8048
  const mcpConfig = readJson(mcpPath) ?? {};
7985
8049
  const servers = mcpConfig.mcpServers ?? {};
7986
8050
  let anythingChanged = false;
8051
+ if (!hasNode9McpServer(servers)) {
8052
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8053
+ mcpConfig.mcpServers = servers;
8054
+ writeJson(mcpPath, mcpConfig);
8055
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8056
+ anythingChanged = true;
8057
+ }
7987
8058
  const serversToWrap = [];
7988
8059
  for (const [name, server] of Object.entries(servers)) {
7989
8060
  if (!server.command || server.command === "node9") continue;
@@ -8083,9 +8154,9 @@ function teardownHud() {
8083
8154
  // src/cli.ts
8084
8155
  init_daemon2();
8085
8156
  import chalk18 from "chalk";
8086
- import fs26 from "fs";
8087
- import path29 from "path";
8088
- import os22 from "os";
8157
+ import fs27 from "fs";
8158
+ import path30 from "path";
8159
+ import os23 from "os";
8089
8160
  import { confirm as confirm2 } from "@inquirer/prompts";
8090
8161
 
8091
8162
  // src/utils/duration.ts
@@ -8320,8 +8391,29 @@ import os14 from "os";
8320
8391
  import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
8321
8392
  import crypto2 from "crypto";
8322
8393
  import fs17 from "fs";
8394
+ import net3 from "net";
8323
8395
  import path19 from "path";
8324
8396
  import os13 from "os";
8397
+ var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path19.join(os13.tmpdir(), "node9-activity.sock");
8398
+ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8399
+ try {
8400
+ const payload = JSON.stringify({
8401
+ status: "snapshot",
8402
+ hash,
8403
+ tool,
8404
+ argsSummary,
8405
+ fileCount,
8406
+ ts: Date.now()
8407
+ });
8408
+ const sock = net3.createConnection(ACTIVITY_SOCKET_PATH3);
8409
+ sock.on("connect", () => {
8410
+ sock.end(payload);
8411
+ });
8412
+ sock.on("error", () => {
8413
+ });
8414
+ } catch {
8415
+ }
8416
+ }
8325
8417
  var SNAPSHOT_STACK_PATH = path19.join(os13.homedir(), ".node9", "snapshots.json");
8326
8418
  var UNDO_LATEST_PATH = path19.join(os13.homedir(), ".node9", "undo_latest.txt");
8327
8419
  var MAX_SNAPSHOTS = 10;
@@ -8541,13 +8633,15 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8541
8633
  }
8542
8634
  if (cwdCount > MAX_SNAPSHOTS) stack.splice(oldestCwdIdx, 1);
8543
8635
  writeStack(stack);
8636
+ const entry = stack[stack.length - 1];
8637
+ notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
8544
8638
  fs17.writeFileSync(UNDO_LATEST_PATH, commitHash);
8545
8639
  if (shouldGc) {
8546
8640
  spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
8547
8641
  }
8548
8642
  return commitHash;
8549
- } catch (err) {
8550
- if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
8643
+ } catch (err2) {
8644
+ if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err2);
8551
8645
  return null;
8552
8646
  } finally {
8553
8647
  if (indexFile) {
@@ -8651,11 +8745,11 @@ function registerCheckCommand(program2) {
8651
8745
  let payload = JSON.parse(raw);
8652
8746
  try {
8653
8747
  payload = JSON.parse(raw);
8654
- } catch (err) {
8748
+ } catch (err2) {
8655
8749
  const tempConfig = getConfig();
8656
8750
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
8657
8751
  const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
8658
- const errMsg = err instanceof Error ? err.message : String(err);
8752
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
8659
8753
  fs18.appendFileSync(
8660
8754
  logPath,
8661
8755
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
@@ -8776,10 +8870,10 @@ RAW: ${raw}
8776
8870
  ...result,
8777
8871
  blockedByLabel: result.blockedByLabel
8778
8872
  });
8779
- } catch (err) {
8873
+ } catch (err2) {
8780
8874
  if (process.env.NODE9_DEBUG === "1") {
8781
8875
  const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
8782
- const errMsg = err instanceof Error ? err.message : String(err);
8876
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
8783
8877
  fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
8784
8878
  `);
8785
8879
  }
@@ -8925,8 +9019,8 @@ function registerLogCommand(program2) {
8925
9019
  if (shouldSnapshot(tool, {}, config)) {
8926
9020
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
8927
9021
  }
8928
- } catch (err) {
8929
- const msg = err instanceof Error ? err.message : String(err);
9022
+ } catch (err2) {
9023
+ const msg = err2 instanceof Error ? err2.message : String(err2);
8930
9024
  process.stderr.write(`[Node9] audit log error: ${msg}
8931
9025
  `);
8932
9026
  const debugPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
@@ -10312,6 +10406,264 @@ function registerMcpGatewayCommand(program2) {
10312
10406
  });
10313
10407
  }
10314
10408
 
10409
+ // src/mcp-server/index.ts
10410
+ import readline4 from "readline";
10411
+ import fs24 from "fs";
10412
+ import os20 from "os";
10413
+ import path27 from "path";
10414
+ init_core();
10415
+ init_daemon();
10416
+ init_shields();
10417
+ function ok(id, result) {
10418
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, result });
10419
+ }
10420
+ function err(id, code, message) {
10421
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
10422
+ }
10423
+ var TOOLS = [
10424
+ {
10425
+ name: "node9_status",
10426
+ description: "Show the current node9 protection status: mode, daemon state, undo engine, pause state, active shields, and whether agent hooks are wired. Use this to understand what protection is active before doing risky work.",
10427
+ inputSchema: { type: "object", properties: {}, required: [] }
10428
+ },
10429
+ {
10430
+ name: "node9_config_get",
10431
+ description: "Read the current node9 configuration: security mode, approver channels, timeouts, DLP settings, and the number of active smart rules. Returns the merged config (env > cloud > project > global > defaults).",
10432
+ inputSchema: { type: "object", properties: {}, required: [] }
10433
+ },
10434
+ {
10435
+ name: "node9_shield_list",
10436
+ description: "List all available node9 shields and which ones are currently active. Shields are pre-packaged rule sets for specific services (postgres, aws, github, filesystem).",
10437
+ inputSchema: { type: "object", properties: {}, required: [] }
10438
+ },
10439
+ {
10440
+ name: "node9_shield_enable",
10441
+ description: "Enable a node9 shield for a specific service. Shields only add protection \u2014 they cannot be used to weaken or bypass node9. Use node9_shield_list to see available shield names.",
10442
+ inputSchema: {
10443
+ type: "object",
10444
+ properties: {
10445
+ service: {
10446
+ type: "string",
10447
+ description: 'Shield name to enable (e.g. "postgres", "aws", "github", "filesystem").'
10448
+ }
10449
+ },
10450
+ required: ["service"]
10451
+ }
10452
+ },
10453
+ {
10454
+ name: "node9_undo_list",
10455
+ description: "List the node9 snapshot history. Each entry shows the git hash, tool that triggered it, a short summary, affected files, working directory, and timestamp. Use this to find a hash before calling node9_undo_revert.",
10456
+ inputSchema: { type: "object", properties: {}, required: [] }
10457
+ },
10458
+ {
10459
+ name: "node9_undo_revert",
10460
+ description: "Revert the working directory to a specific node9 snapshot. Call node9_undo_list first to find the hash you want to restore. WARNING: this overwrites current files \u2014 any unsaved work will be lost.",
10461
+ inputSchema: {
10462
+ type: "object",
10463
+ properties: {
10464
+ hash: {
10465
+ type: "string",
10466
+ description: "The full git commit hash from node9_undo_list."
10467
+ },
10468
+ cwd: {
10469
+ type: "string",
10470
+ description: "Absolute path to the project directory. Defaults to process.cwd()."
10471
+ }
10472
+ },
10473
+ required: ["hash"]
10474
+ }
10475
+ }
10476
+ ];
10477
+ function handleStatus() {
10478
+ const config = getConfig();
10479
+ const settings = config.settings;
10480
+ const paused = checkPause();
10481
+ const daemonUp = isDaemonRunning();
10482
+ const activeShields = readActiveShields();
10483
+ const lines = [];
10484
+ lines.push(`Mode: ${settings.mode}`);
10485
+ lines.push(`Daemon: ${daemonUp ? "running" : "stopped"}`);
10486
+ lines.push(`Undo engine: ${settings.enableUndo ? "enabled" : "disabled"}`);
10487
+ if (paused.paused) {
10488
+ const until = paused.expiresAt ? new Date(paused.expiresAt).toLocaleTimeString() : "indefinitely";
10489
+ lines.push(`PAUSED until ${until} \u2014 all tool calls currently allowed`);
10490
+ } else {
10491
+ lines.push(`Pause state: not paused`);
10492
+ }
10493
+ lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
10494
+ lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
10495
+ lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
10496
+ const projectConfig = path27.join(process.cwd(), "node9.config.json");
10497
+ const globalConfig = path27.join(os20.homedir(), ".node9", "config.json");
10498
+ lines.push(
10499
+ `Project config (node9.config.json): ${fs24.existsSync(projectConfig) ? "present" : "not found"}`
10500
+ );
10501
+ lines.push(
10502
+ `Global config (~/.node9/config.json): ${fs24.existsSync(globalConfig) ? "present" : "not found"}`
10503
+ );
10504
+ return lines.join("\n");
10505
+ }
10506
+ function handleConfigGet() {
10507
+ const config = getConfig();
10508
+ const s = config.settings;
10509
+ const lines = [
10510
+ `mode: ${s.mode}`,
10511
+ `enableUndo: ${s.enableUndo}`,
10512
+ `flightRecorder: ${s.flightRecorder}`,
10513
+ `approvalTimeoutMs: ${s.approvalTimeoutMs}`,
10514
+ `approvers:`,
10515
+ ` native: ${s.approvers.native}`,
10516
+ ` browser: ${s.approvers.browser}`,
10517
+ ` cloud: ${s.approvers.cloud}`,
10518
+ ` terminal: ${s.approvers.terminal}`,
10519
+ `dlp.enabled: ${config.policy.dlp?.enabled !== false}`,
10520
+ `dlp.scanIgnoredTools: ${config.policy.dlp?.scanIgnoredTools !== false}`,
10521
+ `smartRules: ${config.policy.smartRules.length} active`,
10522
+ `sandboxPaths: ${config.policy.sandboxPaths.length > 0 ? config.policy.sandboxPaths.join(", ") : "none"}`
10523
+ ];
10524
+ return lines.join("\n");
10525
+ }
10526
+ function handleShieldList() {
10527
+ const all = listShields();
10528
+ const active = new Set(readActiveShields());
10529
+ if (all.length === 0) return "No shields available.";
10530
+ const lines = all.map((shield) => {
10531
+ const on = active.has(shield.name);
10532
+ const ruleCount = shield.smartRules.length;
10533
+ return `${on ? "[active]" : "[off] "} ${shield.name.padEnd(12)} \u2014 ${shield.description ?? ""} (${ruleCount} rule${ruleCount === 1 ? "" : "s"})`;
10534
+ });
10535
+ lines.unshift(`${active.size} of ${all.length} shields active:
10536
+ `);
10537
+ return lines.join("\n");
10538
+ }
10539
+ function handleShieldEnable(args) {
10540
+ const service = args.service;
10541
+ if (typeof service !== "string" || !service) {
10542
+ throw new Error("service is required");
10543
+ }
10544
+ const name = resolveShieldName(service);
10545
+ if (!name) {
10546
+ throw new Error(
10547
+ `Unknown shield: "${service}". Run node9_shield_list to see available shields.`
10548
+ );
10549
+ }
10550
+ const active = readActiveShields();
10551
+ if (active.includes(name)) {
10552
+ return `Shield "${name}" is already active.`;
10553
+ }
10554
+ writeActiveShields([...active, name]);
10555
+ const shield = getShield(name);
10556
+ return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10557
+ }
10558
+ function handleUndoList() {
10559
+ const history = getSnapshotHistory();
10560
+ if (history.length === 0) {
10561
+ return "No snapshots found. Node9 captures snapshots automatically before file edits.";
10562
+ }
10563
+ const lines = history.slice().reverse().map((entry, i) => {
10564
+ const date = new Date(entry.timestamp).toLocaleString();
10565
+ const files = entry.files?.length ? `${entry.files.length} file(s)` : "unknown files";
10566
+ const summary = entry.argsSummary ? ` \u2014 ${entry.argsSummary}` : "";
10567
+ return `[${i + 1}] ${entry.hash.slice(0, 7)} ${date} ${entry.tool}${summary} (${files}) cwd: ${entry.cwd}
10568
+ full hash: ${entry.hash}`;
10569
+ });
10570
+ return lines.join("\n\n");
10571
+ }
10572
+ function handleUndoRevert(args) {
10573
+ const hash = args.hash;
10574
+ if (typeof hash !== "string" || !hash) {
10575
+ throw new Error("hash is required and must be a non-empty string");
10576
+ }
10577
+ if (!/^[0-9a-f]{7,40}$/i.test(hash)) {
10578
+ throw new Error(`Invalid hash format: ${hash}`);
10579
+ }
10580
+ const cwd = typeof args.cwd === "string" && args.cwd ? args.cwd : process.cwd();
10581
+ const success = applyUndo(hash, cwd);
10582
+ if (!success) {
10583
+ throw new Error(
10584
+ `Revert failed for hash ${hash}. The snapshot may not exist for this directory, or git encountered an error.`
10585
+ );
10586
+ }
10587
+ return `Successfully reverted to snapshot ${hash.slice(0, 7)} in ${cwd}.`;
10588
+ }
10589
+ function runMcpServer() {
10590
+ const rl = readline4.createInterface({ input: process.stdin, terminal: false });
10591
+ rl.on("line", (line) => {
10592
+ let msg;
10593
+ try {
10594
+ msg = JSON.parse(line);
10595
+ } catch {
10596
+ process.stdout.write(err(null, -32700, "Parse error") + "\n");
10597
+ return;
10598
+ }
10599
+ const { method, id, params } = msg;
10600
+ if (method === "initialize") {
10601
+ process.stdout.write(
10602
+ ok(id, {
10603
+ protocolVersion: "2024-11-05",
10604
+ serverInfo: { name: "node9", version: "1.0.0" },
10605
+ capabilities: { tools: {} }
10606
+ }) + "\n"
10607
+ );
10608
+ return;
10609
+ }
10610
+ if (id === void 0 || id === null) {
10611
+ return;
10612
+ }
10613
+ if (method === "tools/list") {
10614
+ process.stdout.write(ok(id, { tools: TOOLS }) + "\n");
10615
+ return;
10616
+ }
10617
+ if (method === "tools/call") {
10618
+ const p = params ?? {};
10619
+ const toolName = p.name;
10620
+ const toolArgs = p.arguments ?? {};
10621
+ try {
10622
+ let text;
10623
+ if (toolName === "node9_status") {
10624
+ text = handleStatus();
10625
+ } else if (toolName === "node9_config_get") {
10626
+ text = handleConfigGet();
10627
+ } else if (toolName === "node9_shield_list") {
10628
+ text = handleShieldList();
10629
+ } else if (toolName === "node9_shield_enable") {
10630
+ text = handleShieldEnable(toolArgs);
10631
+ } else if (toolName === "node9_undo_list") {
10632
+ text = handleUndoList();
10633
+ } else if (toolName === "node9_undo_revert") {
10634
+ text = handleUndoRevert(toolArgs);
10635
+ } else {
10636
+ process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
10637
+ return;
10638
+ }
10639
+ process.stdout.write(ok(id, { content: [{ type: "text", text }] }) + "\n");
10640
+ } catch (e) {
10641
+ const message = e instanceof Error ? e.message : String(e);
10642
+ process.stdout.write(
10643
+ ok(id, {
10644
+ content: [{ type: "text", text: `Error: ${message}` }],
10645
+ isError: true
10646
+ }) + "\n"
10647
+ );
10648
+ }
10649
+ return;
10650
+ }
10651
+ process.stdout.write(err(id, -32601, `Method not found: ${method}`) + "\n");
10652
+ });
10653
+ rl.on("close", () => {
10654
+ process.exit(0);
10655
+ });
10656
+ }
10657
+
10658
+ // src/cli/commands/mcp-server.ts
10659
+ function registerMcpServerCommand(program2) {
10660
+ program2.command("mcp-server").description(
10661
+ "Run the Node9 MCP server \u2014 exposes node9 tools (undo, rules, \u2026) to Claude, Cursor, and Gemini"
10662
+ ).action(() => {
10663
+ runMcpServer();
10664
+ });
10665
+ }
10666
+
10315
10667
  // src/cli/commands/trust.ts
10316
10668
  init_trusted_hosts();
10317
10669
  import chalk16 from "chalk";
@@ -10369,20 +10721,20 @@ function registerTrustCommand(program2) {
10369
10721
 
10370
10722
  // src/cli.ts
10371
10723
  var { version } = JSON.parse(
10372
- fs26.readFileSync(path29.join(__dirname, "../package.json"), "utf-8")
10724
+ fs27.readFileSync(path30.join(__dirname, "../package.json"), "utf-8")
10373
10725
  );
10374
10726
  var program = new Command();
10375
10727
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
10376
10728
  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) => {
10377
10729
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
10378
- const credPath = path29.join(os22.homedir(), ".node9", "credentials.json");
10379
- if (!fs26.existsSync(path29.dirname(credPath)))
10380
- fs26.mkdirSync(path29.dirname(credPath), { recursive: true });
10730
+ const credPath = path30.join(os23.homedir(), ".node9", "credentials.json");
10731
+ if (!fs27.existsSync(path30.dirname(credPath)))
10732
+ fs27.mkdirSync(path30.dirname(credPath), { recursive: true });
10381
10733
  const profileName = options.profile || "default";
10382
10734
  let existingCreds = {};
10383
10735
  try {
10384
- if (fs26.existsSync(credPath)) {
10385
- const raw = JSON.parse(fs26.readFileSync(credPath, "utf-8"));
10736
+ if (fs27.existsSync(credPath)) {
10737
+ const raw = JSON.parse(fs27.readFileSync(credPath, "utf-8"));
10386
10738
  if (raw.apiKey) {
10387
10739
  existingCreds = {
10388
10740
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -10394,13 +10746,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
10394
10746
  } catch {
10395
10747
  }
10396
10748
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
10397
- fs26.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10749
+ fs27.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10398
10750
  if (profileName === "default") {
10399
- const configPath = path29.join(os22.homedir(), ".node9", "config.json");
10751
+ const configPath = path30.join(os23.homedir(), ".node9", "config.json");
10400
10752
  let config = {};
10401
10753
  try {
10402
- if (fs26.existsSync(configPath))
10403
- config = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
10754
+ if (fs27.existsSync(configPath))
10755
+ config = JSON.parse(fs27.readFileSync(configPath, "utf-8"));
10404
10756
  } catch {
10405
10757
  }
10406
10758
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -10415,9 +10767,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
10415
10767
  approvers.cloud = false;
10416
10768
  }
10417
10769
  s.approvers = approvers;
10418
- if (!fs26.existsSync(path29.dirname(configPath)))
10419
- fs26.mkdirSync(path29.dirname(configPath), { recursive: true });
10420
- fs26.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
10770
+ if (!fs27.existsSync(path30.dirname(configPath)))
10771
+ fs27.mkdirSync(path30.dirname(configPath), { recursive: true });
10772
+ fs27.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
10421
10773
  }
10422
10774
  if (options.profile && profileName !== "default") {
10423
10775
  console.log(chalk18.green(`\u2705 Profile "${profileName}" saved`));
@@ -10477,8 +10829,8 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
10477
10829
  `));
10478
10830
  try {
10479
10831
  fn();
10480
- } catch (err) {
10481
- console.error(chalk18.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
10832
+ } catch (err2) {
10833
+ console.error(chalk18.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
10482
10834
  process.exit(1);
10483
10835
  }
10484
10836
  console.log(chalk18.gray("\n Restart the agent for changes to take effect."));
@@ -10501,25 +10853,25 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
10501
10853
  ]) {
10502
10854
  try {
10503
10855
  fn();
10504
- } catch (err) {
10856
+ } catch (err2) {
10505
10857
  teardownFailed = true;
10506
10858
  console.error(
10507
10859
  chalk18.red(
10508
- ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
10860
+ ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
10509
10861
  )
10510
10862
  );
10511
10863
  }
10512
10864
  }
10513
10865
  if (options.purge) {
10514
- const node9Dir = path29.join(os22.homedir(), ".node9");
10515
- if (fs26.existsSync(node9Dir)) {
10866
+ const node9Dir = path30.join(os23.homedir(), ".node9");
10867
+ if (fs27.existsSync(node9Dir)) {
10516
10868
  const confirmed = await confirm2({
10517
10869
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
10518
10870
  default: false
10519
10871
  });
10520
10872
  if (confirmed) {
10521
- fs26.rmSync(node9Dir, { recursive: true });
10522
- if (fs26.existsSync(node9Dir)) {
10873
+ fs27.rmSync(node9Dir, { recursive: true });
10874
+ if (fs27.existsSync(node9Dir)) {
10523
10875
  console.error(
10524
10876
  chalk18.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
10525
10877
  );
@@ -10626,13 +10978,14 @@ program.command("tail").description("Stream live agent activity to the terminal"
10626
10978
  const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
10627
10979
  try {
10628
10980
  await startTail2(options);
10629
- } catch (err) {
10630
- console.error(chalk18.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
10981
+ } catch (err2) {
10982
+ console.error(chalk18.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
10631
10983
  process.exit(1);
10632
10984
  }
10633
10985
  });
10634
10986
  registerWatchCommand(program);
10635
10987
  registerMcpGatewayCommand(program);
10988
+ registerMcpServerCommand(program);
10636
10989
  registerCheckCommand(program);
10637
10990
  registerLogCommand(program);
10638
10991
  program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
@@ -10735,9 +11088,9 @@ if (process.argv[2] !== "daemon") {
10735
11088
  const isCheckHook = process.argv[2] === "check";
10736
11089
  if (isCheckHook) {
10737
11090
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
10738
- const logPath = path29.join(os22.homedir(), ".node9", "hook-debug.log");
11091
+ const logPath = path30.join(os23.homedir(), ".node9", "hook-debug.log");
10739
11092
  const msg = reason instanceof Error ? reason.message : String(reason);
10740
- fs26.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
11093
+ fs27.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
10741
11094
  `);
10742
11095
  }
10743
11096
  process.exit(0);