@node9/proxy 1.5.5 → 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.js CHANGED
@@ -160,8 +160,8 @@ function sanitizeConfig(raw) {
160
160
  }
161
161
  }
162
162
  const lines = result.error.issues.map((issue) => {
163
- const path30 = issue.path.length > 0 ? issue.path.join(".") : "root";
164
- return ` \u2022 ${path30}: ${issue.message}`;
163
+ const path31 = issue.path.length > 0 ? issue.path.join(".") : "root";
164
+ return ` \u2022 ${path31}: ${issue.message}`;
165
165
  });
166
166
  return {
167
167
  sanitized,
@@ -315,9 +315,9 @@ function readShieldsFile() {
315
315
  (e) => typeof e === "string" && e.length > 0 && e in SHIELDS
316
316
  ) : [];
317
317
  return { active, overrides: validateOverrides(parsed.overrides) };
318
- } catch (err) {
319
- if (err.code !== "ENOENT") {
320
- process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
318
+ } catch (err2) {
319
+ if (err2.code !== "ENOENT") {
320
+ process.stderr.write(`[node9] Warning: could not read shields state: ${String(err2)}
321
321
  `);
322
322
  }
323
323
  return { active: [] };
@@ -733,8 +733,8 @@ function tryLoadConfig(filePath) {
733
733
  let raw;
734
734
  try {
735
735
  raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
736
- } catch (err) {
737
- const msg = err instanceof Error ? err.message : String(err);
736
+ } catch (err2) {
737
+ const msg = err2 instanceof Error ? err2.message : String(err2);
738
738
  process.stderr.write(
739
739
  `
740
740
  \u26A0\uFE0F Node9: Failed to parse ${filePath}
@@ -1064,10 +1064,10 @@ function getCompiledRegex(pattern, flags = "") {
1064
1064
  regexCache.set(key, cached);
1065
1065
  return cached;
1066
1066
  }
1067
- const err = validateRegex(pattern);
1068
- if (err) {
1067
+ const err2 = validateRegex(pattern);
1068
+ if (err2) {
1069
1069
  if (process.env.NODE9_DEBUG === "1")
1070
- console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
1070
+ console.error(`[Node9] Regex blocked: ${err2} \u2014 pattern: "${pattern}"`);
1071
1071
  return null;
1072
1072
  }
1073
1073
  try {
@@ -1101,8 +1101,8 @@ function scanFilePath(filePath, cwd = process.cwd()) {
1101
1101
  try {
1102
1102
  const absolute = import_path4.default.resolve(cwd, filePath);
1103
1103
  resolved = import_fs4.default.realpathSync.native(absolute);
1104
- } catch (err) {
1105
- const code = err.code;
1104
+ } catch (err2) {
1105
+ const code = err2.code;
1106
1106
  if (code === "ENOENT" || code === "ENOTDIR") {
1107
1107
  resolved = import_path4.default.resolve(cwd, filePath);
1108
1108
  } else {
@@ -1774,9 +1774,9 @@ function matchesPattern(text, patterns) {
1774
1774
  const withoutDotSlash = text.replace(/^\.\//, "");
1775
1775
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1776
1776
  }
1777
- function getNestedValue(obj, path30) {
1777
+ function getNestedValue(obj, path31) {
1778
1778
  if (!obj || typeof obj !== "object") return null;
1779
- return path30.split(".").reduce((prev, curr) => prev?.[curr], obj);
1779
+ return path31.split(".").reduce((prev, curr) => prev?.[curr], obj);
1780
1780
  }
1781
1781
  function shouldSnapshot(toolName, args, config) {
1782
1782
  if (!config.settings.enableUndo) return false;
@@ -2428,9 +2428,9 @@ function writeTrustSession(toolName, durationMs) {
2428
2428
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > now);
2429
2429
  trust.entries.push({ tool: toolName, expiry: now + durationMs });
2430
2430
  atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
2431
- } catch (err) {
2431
+ } catch (err2) {
2432
2432
  if (process.env.NODE9_DEBUG === "1") {
2433
- console.error("[Node9 Trust Error]:", err);
2433
+ console.error("[Node9 Trust Error]:", err2);
2434
2434
  }
2435
2435
  }
2436
2436
  }
@@ -2627,13 +2627,13 @@ async function checkTaint(paths) {
2627
2627
  signal: AbortSignal.timeout(2e3)
2628
2628
  });
2629
2629
  return await res.json();
2630
- } catch (err) {
2630
+ } catch (err2) {
2631
2631
  try {
2632
2632
  const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
2633
2633
  appendToLog2(HOOK_DEBUG_LOG2, {
2634
2634
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2635
2635
  event: "checkTaint-error",
2636
- error: String(err),
2636
+ error: String(err2),
2637
2637
  paths
2638
2638
  });
2639
2639
  } catch {
@@ -3128,10 +3128,10 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
3128
3128
  `
3129
3129
  );
3130
3130
  }
3131
- } catch (err) {
3131
+ } catch (err2) {
3132
3132
  import_fs10.default.appendFileSync(
3133
3133
  HOOK_DEBUG_LOG,
3134
- `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
3134
+ `[resolve-cloud] PATCH failed for ${requestId}: ${err2.message}
3135
3135
  `
3136
3136
  );
3137
3137
  }
@@ -3501,10 +3501,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3501
3501
  blockedBy: cloudResult.approved ? void 0 : "team-policy",
3502
3502
  blockedByLabel: "Organization Policy (SaaS)"
3503
3503
  };
3504
- } catch (err) {
3505
- const error = err;
3506
- if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err;
3507
- throw err;
3504
+ } catch (err2) {
3505
+ const error = err2;
3506
+ if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err2;
3507
+ throw err2;
3508
3508
  }
3509
3509
  })()
3510
3510
  );
@@ -3597,10 +3597,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3597
3597
  }
3598
3598
  };
3599
3599
  for (const p of racePromises) {
3600
- p.then(finish).catch((err) => {
3601
- if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
3600
+ p.then(finish).catch((err2) => {
3601
+ if (err2.name === "AbortError" || err2.message?.includes("canceled") || err2.message?.includes("Aborted"))
3602
3602
  return;
3603
- if (err.message === "Abandoned") {
3603
+ if (err2.message === "Abandoned") {
3604
3604
  finish({
3605
3605
  approved: false,
3606
3606
  reason: "Browser dashboard closed without making a decision.",
@@ -5615,21 +5615,21 @@ function atomicWriteSync2(filePath, data, options) {
5615
5615
  const tmpPath = `${filePath}.${(0, import_crypto5.randomUUID)()}.tmp`;
5616
5616
  try {
5617
5617
  import_fs13.default.writeFileSync(tmpPath, data, options);
5618
- } catch (err) {
5618
+ } catch (err2) {
5619
5619
  try {
5620
5620
  import_fs13.default.unlinkSync(tmpPath);
5621
5621
  } catch {
5622
5622
  }
5623
- throw err;
5623
+ throw err2;
5624
5624
  }
5625
5625
  try {
5626
5626
  import_fs13.default.renameSync(tmpPath, filePath);
5627
- } catch (err) {
5627
+ } catch (err2) {
5628
5628
  try {
5629
5629
  import_fs13.default.unlinkSync(tmpPath);
5630
5630
  } catch {
5631
5631
  }
5632
- throw err;
5632
+ throw err2;
5633
5633
  }
5634
5634
  }
5635
5635
  function redactArgs(value) {
@@ -5808,6 +5808,16 @@ function startActivitySocket() {
5808
5808
  sessionHistory.recordTestFail(data.ts);
5809
5809
  return;
5810
5810
  }
5811
+ if (data.status === "snapshot") {
5812
+ broadcast("snapshot", {
5813
+ hash: data.hash,
5814
+ tool: data.tool,
5815
+ argsSummary: data.argsSummary,
5816
+ fileCount: data.fileCount,
5817
+ ts: data.ts
5818
+ });
5819
+ return;
5820
+ }
5811
5821
  if (data.status === "pending") {
5812
5822
  broadcast("activity", {
5813
5823
  id: data.id,
@@ -5942,21 +5952,21 @@ function patchConfig(configPath, patch) {
5942
5952
  const tmp = configPath + ".node9-tmp";
5943
5953
  try {
5944
5954
  import_fs14.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5945
- } catch (err) {
5955
+ } catch (err2) {
5946
5956
  try {
5947
5957
  import_fs14.default.unlinkSync(tmp);
5948
5958
  } catch {
5949
5959
  }
5950
- throw err;
5960
+ throw err2;
5951
5961
  }
5952
5962
  try {
5953
5963
  import_fs14.default.renameSync(tmp, configPath);
5954
- } catch (err) {
5964
+ } catch (err2) {
5955
5965
  try {
5956
5966
  import_fs14.default.unlinkSync(tmp);
5957
5967
  } catch {
5958
5968
  }
5959
- throw err;
5969
+ throw err2;
5960
5970
  }
5961
5971
  }
5962
5972
  var import_fs14, import_path17, import_os12, GLOBAL_CONFIG_PATH;
@@ -6208,11 +6218,11 @@ data: ${JSON.stringify(item.data)}
6208
6218
  e.earlyDecision = decision;
6209
6219
  e.earlyReason = result.reason;
6210
6220
  }
6211
- }).catch((err) => {
6221
+ }).catch((err2) => {
6212
6222
  const e = pending.get(id);
6213
6223
  if (!e) return;
6214
6224
  clearTimeout(e.timer);
6215
- const reason = err?.reason || "No response \u2014 request timed out";
6225
+ const reason = err2?.reason || "No response \u2014 request timed out";
6216
6226
  if (e.waiter) e.waiter("deny", reason);
6217
6227
  else {
6218
6228
  e.earlyDecision = "deny";
@@ -6339,8 +6349,8 @@ data: ${JSON.stringify(item.data)}
6339
6349
  const s = getGlobalSettings();
6340
6350
  res.writeHead(200, { "Content-Type": "application/json" });
6341
6351
  return res.end(JSON.stringify({ ...s, autoStarted }));
6342
- } catch (err) {
6343
- console.error(import_chalk2.default.red("[node9 daemon] GET /settings failed:"), err);
6352
+ } catch (err2) {
6353
+ console.error(import_chalk2.default.red("[node9 daemon] GET /settings failed:"), err2);
6344
6354
  res.writeHead(500, { "Content-Type": "application/json" });
6345
6355
  return res.end(JSON.stringify({ error: "internal" }));
6346
6356
  }
@@ -6364,8 +6374,8 @@ data: ${JSON.stringify(item.data)}
6364
6374
  };
6365
6375
  res.writeHead(200, { "Content-Type": "application/json" });
6366
6376
  return res.end(JSON.stringify(status));
6367
- } catch (err) {
6368
- console.error(import_chalk2.default.red("[node9 daemon] GET /status failed:"), err);
6377
+ } catch (err2) {
6378
+ console.error(import_chalk2.default.red("[node9 daemon] GET /status failed:"), err2);
6369
6379
  res.writeHead(500, { "Content-Type": "application/json" });
6370
6380
  return res.end(JSON.stringify({ error: "internal" }));
6371
6381
  }
@@ -6405,8 +6415,8 @@ data: ${JSON.stringify(item.data)}
6405
6415
  const s = getGlobalSettings();
6406
6416
  res.writeHead(200, { "Content-Type": "application/json" });
6407
6417
  return res.end(JSON.stringify({ hasKey: hasStoredSlackKey(), enabled: s.slackEnabled }));
6408
- } catch (err) {
6409
- console.error(import_chalk2.default.red("[node9 daemon] GET /slack-status failed:"), err);
6418
+ } catch (err2) {
6419
+ console.error(import_chalk2.default.red("[node9 daemon] GET /slack-status failed:"), err2);
6410
6420
  res.writeHead(500, { "Content-Type": "application/json" });
6411
6421
  return res.end(JSON.stringify({ error: "internal" }));
6412
6422
  }
@@ -6566,10 +6576,10 @@ data: ${JSON.stringify(item.data)}
6566
6576
  broadcast("suggestion:resolved", { id, status: "applied" });
6567
6577
  res.writeHead(200, { "Content-Type": "application/json" });
6568
6578
  return res.end(JSON.stringify({ ok: true }));
6569
- } catch (err) {
6570
- console.error(import_chalk2.default.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
6579
+ } catch (err2) {
6580
+ console.error(import_chalk2.default.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err2);
6571
6581
  res.writeHead(500, { "Content-Type": "application/json" });
6572
- return res.end(JSON.stringify({ error: String(err) }));
6582
+ return res.end(JSON.stringify({ error: String(err2) }));
6573
6583
  }
6574
6584
  }
6575
6585
  if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
@@ -6816,8 +6826,8 @@ function renderResult(activity, result) {
6816
6826
  status = import_chalk17.default.red("\u2717 BLOCK");
6817
6827
  }
6818
6828
  if (process.stdout.isTTY) {
6819
- import_readline4.default.clearLine(process.stdout, 0);
6820
- import_readline4.default.cursorTo(process.stdout, 0);
6829
+ import_readline5.default.clearLine(process.stdout, 0);
6830
+ import_readline5.default.cursorTo(process.stdout, 0);
6821
6831
  }
6822
6832
  console.log(`${base} ${status}`);
6823
6833
  }
@@ -6827,9 +6837,9 @@ function renderPending(activity) {
6827
6837
  }
6828
6838
  async function ensureDaemon() {
6829
6839
  let pidPort = null;
6830
- if (import_fs24.default.existsSync(PID_FILE)) {
6840
+ if (import_fs25.default.existsSync(PID_FILE)) {
6831
6841
  try {
6832
- const { port } = JSON.parse(import_fs24.default.readFileSync(PID_FILE, "utf-8"));
6842
+ const { port } = JSON.parse(import_fs25.default.readFileSync(PID_FILE, "utf-8"));
6833
6843
  pidPort = port;
6834
6844
  } catch {
6835
6845
  console.error(import_chalk17.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -6966,7 +6976,7 @@ async function startTail(options = {}) {
6966
6976
  res.resume();
6967
6977
  }
6968
6978
  );
6969
- req2.once("error", (err) => resolve({ ok: false, code: err.code }));
6979
+ req2.once("error", (err2) => resolve({ ok: false, code: err2.code }));
6970
6980
  req2.setTimeout(2e3, () => {
6971
6981
  resolve({ ok: false, code: "ETIMEDOUT" });
6972
6982
  req2.destroy();
@@ -6993,10 +7003,10 @@ async function startTail(options = {}) {
6993
7003
  let cancelActiveCard = null;
6994
7004
  const localAllowCounts = /* @__PURE__ */ new Map();
6995
7005
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
6996
- if (canApprove) import_readline4.default.emitKeypressEvents(process.stdin);
7006
+ if (canApprove) import_readline5.default.emitKeypressEvents(process.stdin);
6997
7007
  function clearCard() {
6998
7008
  if (cardLineCount > 0) {
6999
- import_readline4.default.moveCursor(process.stdout, 0, -cardLineCount);
7009
+ import_readline5.default.moveCursor(process.stdout, 0, -cardLineCount);
7000
7010
  process.stdout.write(ERASE_DOWN);
7001
7011
  cardLineCount = 0;
7002
7012
  }
@@ -7072,11 +7082,11 @@ async function startTail(options = {}) {
7072
7082
  } else {
7073
7083
  httpDecision = action;
7074
7084
  }
7075
- postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
7085
+ postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7076
7086
  try {
7077
- import_fs24.default.appendFileSync(
7078
- import_path27.default.join(import_os20.default.homedir(), ".node9", "hook-debug.log"),
7079
- `[tail] POST /decision failed: ${String(err)}
7087
+ import_fs25.default.appendFileSync(
7088
+ import_path28.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
7089
+ `[tail] POST /decision failed: ${String(err2)}
7080
7090
  `
7081
7091
  );
7082
7092
  } catch {
@@ -7174,8 +7184,8 @@ async function startTail(options = {}) {
7174
7184
  clearCard();
7175
7185
  process.stdout.write(SHOW_CURSOR);
7176
7186
  if (process.stdout.isTTY) {
7177
- import_readline4.default.clearLine(process.stdout, 0);
7178
- import_readline4.default.cursorTo(process.stdout, 0);
7187
+ import_readline5.default.clearLine(process.stdout, 0);
7188
+ import_readline5.default.cursorTo(process.stdout, 0);
7179
7189
  }
7180
7190
  console.log(import_chalk17.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7181
7191
  process.exit(0);
@@ -7190,7 +7200,7 @@ async function startTail(options = {}) {
7190
7200
  let currentData = "";
7191
7201
  res.on("error", () => {
7192
7202
  });
7193
- const rl = import_readline4.default.createInterface({ input: res, crlfDelay: Infinity });
7203
+ const rl = import_readline5.default.createInterface({ input: res, crlfDelay: Infinity });
7194
7204
  rl.on("error", () => {
7195
7205
  });
7196
7206
  rl.on("line", (line) => {
@@ -7210,8 +7220,8 @@ async function startTail(options = {}) {
7210
7220
  clearCard();
7211
7221
  process.stdout.write(SHOW_CURSOR);
7212
7222
  if (process.stdout.isTTY) {
7213
- import_readline4.default.clearLine(process.stdout, 0);
7214
- import_readline4.default.cursorTo(process.stdout, 0);
7223
+ import_readline5.default.clearLine(process.stdout, 0);
7224
+ import_readline5.default.cursorTo(process.stdout, 0);
7215
7225
  }
7216
7226
  console.log(import_chalk17.default.red("\n\u274C Daemon disconnected."));
7217
7227
  process.exit(1);
@@ -7289,6 +7299,18 @@ async function startTail(options = {}) {
7289
7299
  const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7290
7300
  if (slowTool) renderPending(data);
7291
7301
  }
7302
+ if (event === "snapshot") {
7303
+ const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
7304
+ const hash = data.hash ?? "";
7305
+ const summary = data.argsSummary ?? data.tool;
7306
+ const fileCount = data.fileCount ?? 0;
7307
+ const files = fileCount > 0 ? import_chalk17.default.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7308
+ process.stdout.write(
7309
+ `${import_chalk17.default.dim(time)} ${import_chalk17.default.cyan("\u{1F4F8} snapshot")} ${import_chalk17.default.dim(hash)} ${summary}${files}
7310
+ `
7311
+ );
7312
+ return;
7313
+ }
7292
7314
  if (event === "activity-result") {
7293
7315
  const original = activityPending.get(data.id);
7294
7316
  if (original) {
@@ -7297,28 +7319,28 @@ async function startTail(options = {}) {
7297
7319
  }
7298
7320
  }
7299
7321
  }
7300
- req.on("error", (err) => {
7301
- const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
7322
+ req.on("error", (err2) => {
7323
+ const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
7302
7324
  console.error(import_chalk17.default.red(`
7303
7325
  \u274C ${msg}`));
7304
7326
  process.exit(1);
7305
7327
  });
7306
7328
  }
7307
- var import_http2, import_chalk17, import_fs24, import_os20, import_path27, import_readline4, import_child_process13, PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
7329
+ var import_http2, import_chalk17, import_fs25, import_os21, import_path28, import_readline5, import_child_process13, PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
7308
7330
  var init_tail = __esm({
7309
7331
  "src/tui/tail.ts"() {
7310
7332
  "use strict";
7311
7333
  import_http2 = __toESM(require("http"));
7312
7334
  import_chalk17 = __toESM(require("chalk"));
7313
- import_fs24 = __toESM(require("fs"));
7314
- import_os20 = __toESM(require("os"));
7315
- import_path27 = __toESM(require("path"));
7316
- import_readline4 = __toESM(require("readline"));
7335
+ import_fs25 = __toESM(require("fs"));
7336
+ import_os21 = __toESM(require("os"));
7337
+ import_path28 = __toESM(require("path"));
7338
+ import_readline5 = __toESM(require("readline"));
7317
7339
  import_child_process13 = require("child_process");
7318
7340
  init_daemon2();
7319
7341
  init_daemon();
7320
7342
  init_core();
7321
- PID_FILE = import_path27.default.join(import_os20.default.homedir(), ".node9", "daemon.pid");
7343
+ PID_FILE = import_path28.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
7322
7344
  ICONS = {
7323
7345
  bash: "\u{1F4BB}",
7324
7346
  shell: "\u{1F4BB}",
@@ -7431,9 +7453,9 @@ function formatTimeLeft(resetsAt) {
7431
7453
  return ` (${m}m left)`;
7432
7454
  }
7433
7455
  function safeReadJson(filePath) {
7434
- if (!import_fs25.default.existsSync(filePath)) return null;
7456
+ if (!import_fs26.default.existsSync(filePath)) return null;
7435
7457
  try {
7436
- return JSON.parse(import_fs25.default.readFileSync(filePath, "utf-8"));
7458
+ return JSON.parse(import_fs26.default.readFileSync(filePath, "utf-8"));
7437
7459
  } catch {
7438
7460
  return null;
7439
7461
  }
@@ -7454,12 +7476,12 @@ function countHooksInFile(filePath) {
7454
7476
  return Object.keys(cfg.hooks).length;
7455
7477
  }
7456
7478
  function countRulesInDir(rulesDir) {
7457
- if (!import_fs25.default.existsSync(rulesDir)) return 0;
7479
+ if (!import_fs26.default.existsSync(rulesDir)) return 0;
7458
7480
  let count = 0;
7459
7481
  try {
7460
- for (const entry of import_fs25.default.readdirSync(rulesDir, { withFileTypes: true })) {
7482
+ for (const entry of import_fs26.default.readdirSync(rulesDir, { withFileTypes: true })) {
7461
7483
  if (entry.isDirectory()) {
7462
- count += countRulesInDir(import_path28.default.join(rulesDir, entry.name));
7484
+ count += countRulesInDir(import_path29.default.join(rulesDir, entry.name));
7463
7485
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
7464
7486
  count++;
7465
7487
  }
@@ -7470,46 +7492,46 @@ function countRulesInDir(rulesDir) {
7470
7492
  }
7471
7493
  function isSamePath(a, b) {
7472
7494
  try {
7473
- return import_path28.default.resolve(a) === import_path28.default.resolve(b);
7495
+ return import_path29.default.resolve(a) === import_path29.default.resolve(b);
7474
7496
  } catch {
7475
7497
  return false;
7476
7498
  }
7477
7499
  }
7478
7500
  function countConfigs(cwd) {
7479
- const homeDir2 = import_os21.default.homedir();
7480
- const claudeDir = import_path28.default.join(homeDir2, ".claude");
7501
+ const homeDir2 = import_os22.default.homedir();
7502
+ const claudeDir = import_path29.default.join(homeDir2, ".claude");
7481
7503
  let claudeMdCount = 0;
7482
7504
  let rulesCount = 0;
7483
7505
  let hooksCount = 0;
7484
7506
  const userMcpServers = /* @__PURE__ */ new Set();
7485
7507
  const projectMcpServers = /* @__PURE__ */ new Set();
7486
- if (import_fs25.default.existsSync(import_path28.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7487
- rulesCount += countRulesInDir(import_path28.default.join(claudeDir, "rules"));
7488
- const userSettings = import_path28.default.join(claudeDir, "settings.json");
7508
+ if (import_fs26.default.existsSync(import_path29.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7509
+ rulesCount += countRulesInDir(import_path29.default.join(claudeDir, "rules"));
7510
+ const userSettings = import_path29.default.join(claudeDir, "settings.json");
7489
7511
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7490
7512
  hooksCount += countHooksInFile(userSettings);
7491
- const userClaudeJson = import_path28.default.join(homeDir2, ".claude.json");
7513
+ const userClaudeJson = import_path29.default.join(homeDir2, ".claude.json");
7492
7514
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7493
7515
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7494
7516
  userMcpServers.delete(name);
7495
7517
  }
7496
7518
  if (cwd) {
7497
- if (import_fs25.default.existsSync(import_path28.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7498
- if (import_fs25.default.existsSync(import_path28.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7499
- const projectClaudeDir = import_path28.default.join(cwd, ".claude");
7519
+ if (import_fs26.default.existsSync(import_path29.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7520
+ if (import_fs26.default.existsSync(import_path29.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7521
+ const projectClaudeDir = import_path29.default.join(cwd, ".claude");
7500
7522
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7501
7523
  if (!overlapsUserScope) {
7502
- if (import_fs25.default.existsSync(import_path28.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7503
- rulesCount += countRulesInDir(import_path28.default.join(projectClaudeDir, "rules"));
7504
- const projSettings = import_path28.default.join(projectClaudeDir, "settings.json");
7524
+ if (import_fs26.default.existsSync(import_path29.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7525
+ rulesCount += countRulesInDir(import_path29.default.join(projectClaudeDir, "rules"));
7526
+ const projSettings = import_path29.default.join(projectClaudeDir, "settings.json");
7505
7527
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7506
7528
  hooksCount += countHooksInFile(projSettings);
7507
7529
  }
7508
- if (import_fs25.default.existsSync(import_path28.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7509
- const localSettings = import_path28.default.join(projectClaudeDir, "settings.local.json");
7530
+ if (import_fs26.default.existsSync(import_path29.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7531
+ const localSettings = import_path29.default.join(projectClaudeDir, "settings.local.json");
7510
7532
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7511
7533
  hooksCount += countHooksInFile(localSettings);
7512
- const mcpJsonServers = getMcpServerNames(import_path28.default.join(cwd, ".mcp.json"));
7534
+ const mcpJsonServers = getMcpServerNames(import_path29.default.join(cwd, ".mcp.json"));
7513
7535
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7514
7536
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7515
7537
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -7622,11 +7644,11 @@ async function main() {
7622
7644
  try {
7623
7645
  const cwd = stdin.cwd ?? process.cwd();
7624
7646
  for (const configPath of [
7625
- import_path28.default.join(cwd, "node9.config.json"),
7626
- import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json")
7647
+ import_path29.default.join(cwd, "node9.config.json"),
7648
+ import_path29.default.join(import_os22.default.homedir(), ".node9", "config.json")
7627
7649
  ]) {
7628
- if (!import_fs25.default.existsSync(configPath)) continue;
7629
- const cfg = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
7650
+ if (!import_fs26.default.existsSync(configPath)) continue;
7651
+ const cfg = JSON.parse(import_fs26.default.readFileSync(configPath, "utf-8"));
7630
7652
  const hud = cfg.settings?.hud;
7631
7653
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7632
7654
  }
@@ -7644,13 +7666,13 @@ async function main() {
7644
7666
  renderOffline();
7645
7667
  }
7646
7668
  }
7647
- var import_fs25, import_path28, import_os21, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
7669
+ 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;
7648
7670
  var init_hud = __esm({
7649
7671
  "src/cli/hud.ts"() {
7650
7672
  "use strict";
7651
- import_fs25 = __toESM(require("fs"));
7652
- import_path28 = __toESM(require("path"));
7653
- import_os21 = __toESM(require("os"));
7673
+ import_fs26 = __toESM(require("fs"));
7674
+ import_path29 = __toESM(require("path"));
7675
+ import_os22 = __toESM(require("os"));
7654
7676
  import_http3 = __toESM(require("http"));
7655
7677
  init_daemon();
7656
7678
  RESET3 = "\x1B[0m";
@@ -7679,6 +7701,16 @@ var import_path14 = __toESM(require("path"));
7679
7701
  var import_os10 = __toESM(require("os"));
7680
7702
  var import_chalk = __toESM(require("chalk"));
7681
7703
  var import_prompts = require("@inquirer/prompts");
7704
+ var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7705
+ function hasNode9McpServer(servers) {
7706
+ const entry = servers["node9"];
7707
+ return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
7708
+ }
7709
+ function removeNode9McpServer(servers) {
7710
+ if (!hasNode9McpServer(servers)) return false;
7711
+ delete servers["node9"];
7712
+ return true;
7713
+ }
7682
7714
  function printDaemonTip() {
7683
7715
  console.log(
7684
7716
  import_chalk.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk.default.white("\n To view your history or manage persistent rules, run:") + import_chalk.default.green("\n node9 daemon --openui")
@@ -7736,6 +7768,10 @@ function teardownClaude() {
7736
7768
  const claudeConfig = readJson(mcpPath);
7737
7769
  if (claudeConfig?.mcpServers) {
7738
7770
  let mcpChanged = false;
7771
+ if (removeNode9McpServer(claudeConfig.mcpServers)) {
7772
+ mcpChanged = true;
7773
+ console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.claude.json"));
7774
+ }
7739
7775
  for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
7740
7776
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7741
7777
  const [originalCmd, ...originalArgs] = server.args;
@@ -7779,6 +7815,10 @@ function teardownGemini() {
7779
7815
  }
7780
7816
  }
7781
7817
  if (settings.mcpServers) {
7818
+ if (removeNode9McpServer(settings.mcpServers)) {
7819
+ changed = true;
7820
+ console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.gemini/settings.json"));
7821
+ }
7782
7822
  for (const [name, server] of Object.entries(settings.mcpServers)) {
7783
7823
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7784
7824
  const [originalCmd, ...originalArgs] = server.args;
@@ -7807,6 +7847,10 @@ function teardownCursor() {
7807
7847
  return;
7808
7848
  }
7809
7849
  let changed = false;
7850
+ if (removeNode9McpServer(mcpConfig.mcpServers)) {
7851
+ changed = true;
7852
+ console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.cursor/mcp.json"));
7853
+ }
7810
7854
  for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
7811
7855
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7812
7856
  const [originalCmd, ...originalArgs] = server.args;
@@ -7832,6 +7876,7 @@ async function setupClaude() {
7832
7876
  const claudeConfig = readJson(mcpPath) ?? {};
7833
7877
  const settings = readJson(hooksPath) ?? {};
7834
7878
  const servers = claudeConfig.mcpServers ?? {};
7879
+ let hooksChanged = false;
7835
7880
  let anythingChanged = false;
7836
7881
  if (!settings.hooks) settings.hooks = {};
7837
7882
  const hasPreHook = settings.hooks.PreToolUse?.some(
@@ -7844,6 +7889,7 @@ async function setupClaude() {
7844
7889
  hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
7845
7890
  });
7846
7891
  console.log(import_chalk.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
7892
+ hooksChanged = true;
7847
7893
  anythingChanged = true;
7848
7894
  }
7849
7895
  const hasPostHook = settings.hooks.PostToolUse?.some(
@@ -7856,9 +7902,17 @@ async function setupClaude() {
7856
7902
  hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
7857
7903
  });
7858
7904
  console.log(import_chalk.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
7905
+ hooksChanged = true;
7859
7906
  anythingChanged = true;
7860
7907
  }
7861
- if (anythingChanged) {
7908
+ if (!hasNode9McpServer(servers)) {
7909
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
7910
+ claudeConfig.mcpServers = servers;
7911
+ writeJson(mcpPath, claudeConfig);
7912
+ console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
7913
+ anythingChanged = true;
7914
+ }
7915
+ if (hooksChanged) {
7862
7916
  writeJson(hooksPath, settings);
7863
7917
  console.log("");
7864
7918
  }
@@ -7906,6 +7960,7 @@ async function setupGemini() {
7906
7960
  const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
7907
7961
  const settings = readJson(settingsPath) ?? {};
7908
7962
  const servers = settings.mcpServers ?? {};
7963
+ let hooksChanged = false;
7909
7964
  let anythingChanged = false;
7910
7965
  if (!settings.hooks) settings.hooks = {};
7911
7966
  const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
@@ -7926,6 +7981,7 @@ async function setupGemini() {
7926
7981
  ]
7927
7982
  });
7928
7983
  console.log(import_chalk.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
7984
+ hooksChanged = true;
7929
7985
  anythingChanged = true;
7930
7986
  }
7931
7987
  const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
@@ -7939,9 +7995,17 @@ async function setupGemini() {
7939
7995
  hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
7940
7996
  });
7941
7997
  console.log(import_chalk.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
7998
+ hooksChanged = true;
7942
7999
  anythingChanged = true;
7943
8000
  }
7944
- if (anythingChanged) {
8001
+ if (!hasNode9McpServer(servers)) {
8002
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8003
+ settings.mcpServers = servers;
8004
+ console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8005
+ hooksChanged = true;
8006
+ anythingChanged = true;
8007
+ }
8008
+ if (hooksChanged) {
7945
8009
  writeJson(settingsPath, settings);
7946
8010
  console.log("");
7947
8011
  }
@@ -7988,10 +8052,10 @@ function detectAgents(homeDir2 = import_os10.default.homedir()) {
7988
8052
  const exists = (p) => {
7989
8053
  try {
7990
8054
  return import_fs11.default.existsSync(p);
7991
- } catch (err) {
7992
- const code = err.code;
8055
+ } catch (err2) {
8056
+ const code = err2.code;
7993
8057
  if (code !== "ENOENT") {
7994
- process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
8058
+ process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err2)}
7995
8059
  `);
7996
8060
  }
7997
8061
  return false;
@@ -8009,6 +8073,13 @@ async function setupCursor() {
8009
8073
  const mcpConfig = readJson(mcpPath) ?? {};
8010
8074
  const servers = mcpConfig.mcpServers ?? {};
8011
8075
  let anythingChanged = false;
8076
+ if (!hasNode9McpServer(servers)) {
8077
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8078
+ mcpConfig.mcpServers = servers;
8079
+ writeJson(mcpPath, mcpConfig);
8080
+ console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8081
+ anythingChanged = true;
8082
+ }
8012
8083
  const serversToWrap = [];
8013
8084
  for (const [name, server] of Object.entries(servers)) {
8014
8085
  if (!server.command || server.command === "node9") continue;
@@ -8108,9 +8179,9 @@ function teardownHud() {
8108
8179
  // src/cli.ts
8109
8180
  init_daemon2();
8110
8181
  var import_chalk18 = __toESM(require("chalk"));
8111
- var import_fs26 = __toESM(require("fs"));
8112
- var import_path29 = __toESM(require("path"));
8113
- var import_os22 = __toESM(require("os"));
8182
+ var import_fs27 = __toESM(require("fs"));
8183
+ var import_path30 = __toESM(require("path"));
8184
+ var import_os23 = __toESM(require("os"));
8114
8185
  var import_prompts2 = require("@inquirer/prompts");
8115
8186
 
8116
8187
  // src/utils/duration.ts
@@ -8345,8 +8416,29 @@ init_policy();
8345
8416
  var import_child_process8 = require("child_process");
8346
8417
  var import_crypto7 = __toESM(require("crypto"));
8347
8418
  var import_fs17 = __toESM(require("fs"));
8419
+ var import_net3 = __toESM(require("net"));
8348
8420
  var import_path19 = __toESM(require("path"));
8349
8421
  var import_os13 = __toESM(require("os"));
8422
+ var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path19.default.join(import_os13.default.tmpdir(), "node9-activity.sock");
8423
+ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8424
+ try {
8425
+ const payload = JSON.stringify({
8426
+ status: "snapshot",
8427
+ hash,
8428
+ tool,
8429
+ argsSummary,
8430
+ fileCount,
8431
+ ts: Date.now()
8432
+ });
8433
+ const sock = import_net3.default.createConnection(ACTIVITY_SOCKET_PATH3);
8434
+ sock.on("connect", () => {
8435
+ sock.end(payload);
8436
+ });
8437
+ sock.on("error", () => {
8438
+ });
8439
+ } catch {
8440
+ }
8441
+ }
8350
8442
  var SNAPSHOT_STACK_PATH = import_path19.default.join(import_os13.default.homedir(), ".node9", "snapshots.json");
8351
8443
  var UNDO_LATEST_PATH = import_path19.default.join(import_os13.default.homedir(), ".node9", "undo_latest.txt");
8352
8444
  var MAX_SNAPSHOTS = 10;
@@ -8566,13 +8658,15 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8566
8658
  }
8567
8659
  if (cwdCount > MAX_SNAPSHOTS) stack.splice(oldestCwdIdx, 1);
8568
8660
  writeStack(stack);
8661
+ const entry = stack[stack.length - 1];
8662
+ notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
8569
8663
  import_fs17.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
8570
8664
  if (shouldGc) {
8571
8665
  (0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
8572
8666
  }
8573
8667
  return commitHash;
8574
- } catch (err) {
8575
- if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
8668
+ } catch (err2) {
8669
+ if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err2);
8576
8670
  return null;
8577
8671
  } finally {
8578
8672
  if (indexFile) {
@@ -8676,11 +8770,11 @@ function registerCheckCommand(program2) {
8676
8770
  let payload = JSON.parse(raw);
8677
8771
  try {
8678
8772
  payload = JSON.parse(raw);
8679
- } catch (err) {
8773
+ } catch (err2) {
8680
8774
  const tempConfig = getConfig();
8681
8775
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
8682
8776
  const logPath = import_path20.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
8683
- const errMsg = err instanceof Error ? err.message : String(err);
8777
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
8684
8778
  import_fs18.default.appendFileSync(
8685
8779
  logPath,
8686
8780
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
@@ -8801,10 +8895,10 @@ RAW: ${raw}
8801
8895
  ...result,
8802
8896
  blockedByLabel: result.blockedByLabel
8803
8897
  });
8804
- } catch (err) {
8898
+ } catch (err2) {
8805
8899
  if (process.env.NODE9_DEBUG === "1") {
8806
8900
  const logPath = import_path20.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
8807
- const errMsg = err instanceof Error ? err.message : String(err);
8901
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
8808
8902
  import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
8809
8903
  `);
8810
8904
  }
@@ -8950,8 +9044,8 @@ function registerLogCommand(program2) {
8950
9044
  if (shouldSnapshot(tool, {}, config)) {
8951
9045
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
8952
9046
  }
8953
- } catch (err) {
8954
- const msg = err instanceof Error ? err.message : String(err);
9047
+ } catch (err2) {
9048
+ const msg = err2 instanceof Error ? err2.message : String(err2);
8955
9049
  process.stderr.write(`[Node9] audit log error: ${msg}
8956
9050
  `);
8957
9051
  const debugPath = import_path21.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
@@ -10337,6 +10431,264 @@ function registerMcpGatewayCommand(program2) {
10337
10431
  });
10338
10432
  }
10339
10433
 
10434
+ // src/mcp-server/index.ts
10435
+ var import_readline4 = __toESM(require("readline"));
10436
+ var import_fs24 = __toESM(require("fs"));
10437
+ var import_os20 = __toESM(require("os"));
10438
+ var import_path27 = __toESM(require("path"));
10439
+ init_core();
10440
+ init_daemon();
10441
+ init_shields();
10442
+ function ok(id, result) {
10443
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, result });
10444
+ }
10445
+ function err(id, code, message) {
10446
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
10447
+ }
10448
+ var TOOLS = [
10449
+ {
10450
+ name: "node9_status",
10451
+ 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.",
10452
+ inputSchema: { type: "object", properties: {}, required: [] }
10453
+ },
10454
+ {
10455
+ name: "node9_config_get",
10456
+ 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).",
10457
+ inputSchema: { type: "object", properties: {}, required: [] }
10458
+ },
10459
+ {
10460
+ name: "node9_shield_list",
10461
+ 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).",
10462
+ inputSchema: { type: "object", properties: {}, required: [] }
10463
+ },
10464
+ {
10465
+ name: "node9_shield_enable",
10466
+ 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.",
10467
+ inputSchema: {
10468
+ type: "object",
10469
+ properties: {
10470
+ service: {
10471
+ type: "string",
10472
+ description: 'Shield name to enable (e.g. "postgres", "aws", "github", "filesystem").'
10473
+ }
10474
+ },
10475
+ required: ["service"]
10476
+ }
10477
+ },
10478
+ {
10479
+ name: "node9_undo_list",
10480
+ 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.",
10481
+ inputSchema: { type: "object", properties: {}, required: [] }
10482
+ },
10483
+ {
10484
+ name: "node9_undo_revert",
10485
+ 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.",
10486
+ inputSchema: {
10487
+ type: "object",
10488
+ properties: {
10489
+ hash: {
10490
+ type: "string",
10491
+ description: "The full git commit hash from node9_undo_list."
10492
+ },
10493
+ cwd: {
10494
+ type: "string",
10495
+ description: "Absolute path to the project directory. Defaults to process.cwd()."
10496
+ }
10497
+ },
10498
+ required: ["hash"]
10499
+ }
10500
+ }
10501
+ ];
10502
+ function handleStatus() {
10503
+ const config = getConfig();
10504
+ const settings = config.settings;
10505
+ const paused = checkPause();
10506
+ const daemonUp = isDaemonRunning();
10507
+ const activeShields = readActiveShields();
10508
+ const lines = [];
10509
+ lines.push(`Mode: ${settings.mode}`);
10510
+ lines.push(`Daemon: ${daemonUp ? "running" : "stopped"}`);
10511
+ lines.push(`Undo engine: ${settings.enableUndo ? "enabled" : "disabled"}`);
10512
+ if (paused.paused) {
10513
+ const until = paused.expiresAt ? new Date(paused.expiresAt).toLocaleTimeString() : "indefinitely";
10514
+ lines.push(`PAUSED until ${until} \u2014 all tool calls currently allowed`);
10515
+ } else {
10516
+ lines.push(`Pause state: not paused`);
10517
+ }
10518
+ lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
10519
+ lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
10520
+ lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
10521
+ const projectConfig = import_path27.default.join(process.cwd(), "node9.config.json");
10522
+ const globalConfig = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
10523
+ lines.push(
10524
+ `Project config (node9.config.json): ${import_fs24.default.existsSync(projectConfig) ? "present" : "not found"}`
10525
+ );
10526
+ lines.push(
10527
+ `Global config (~/.node9/config.json): ${import_fs24.default.existsSync(globalConfig) ? "present" : "not found"}`
10528
+ );
10529
+ return lines.join("\n");
10530
+ }
10531
+ function handleConfigGet() {
10532
+ const config = getConfig();
10533
+ const s = config.settings;
10534
+ const lines = [
10535
+ `mode: ${s.mode}`,
10536
+ `enableUndo: ${s.enableUndo}`,
10537
+ `flightRecorder: ${s.flightRecorder}`,
10538
+ `approvalTimeoutMs: ${s.approvalTimeoutMs}`,
10539
+ `approvers:`,
10540
+ ` native: ${s.approvers.native}`,
10541
+ ` browser: ${s.approvers.browser}`,
10542
+ ` cloud: ${s.approvers.cloud}`,
10543
+ ` terminal: ${s.approvers.terminal}`,
10544
+ `dlp.enabled: ${config.policy.dlp?.enabled !== false}`,
10545
+ `dlp.scanIgnoredTools: ${config.policy.dlp?.scanIgnoredTools !== false}`,
10546
+ `smartRules: ${config.policy.smartRules.length} active`,
10547
+ `sandboxPaths: ${config.policy.sandboxPaths.length > 0 ? config.policy.sandboxPaths.join(", ") : "none"}`
10548
+ ];
10549
+ return lines.join("\n");
10550
+ }
10551
+ function handleShieldList() {
10552
+ const all = listShields();
10553
+ const active = new Set(readActiveShields());
10554
+ if (all.length === 0) return "No shields available.";
10555
+ const lines = all.map((shield) => {
10556
+ const on = active.has(shield.name);
10557
+ const ruleCount = shield.smartRules.length;
10558
+ return `${on ? "[active]" : "[off] "} ${shield.name.padEnd(12)} \u2014 ${shield.description ?? ""} (${ruleCount} rule${ruleCount === 1 ? "" : "s"})`;
10559
+ });
10560
+ lines.unshift(`${active.size} of ${all.length} shields active:
10561
+ `);
10562
+ return lines.join("\n");
10563
+ }
10564
+ function handleShieldEnable(args) {
10565
+ const service = args.service;
10566
+ if (typeof service !== "string" || !service) {
10567
+ throw new Error("service is required");
10568
+ }
10569
+ const name = resolveShieldName(service);
10570
+ if (!name) {
10571
+ throw new Error(
10572
+ `Unknown shield: "${service}". Run node9_shield_list to see available shields.`
10573
+ );
10574
+ }
10575
+ const active = readActiveShields();
10576
+ if (active.includes(name)) {
10577
+ return `Shield "${name}" is already active.`;
10578
+ }
10579
+ writeActiveShields([...active, name]);
10580
+ const shield = getShield(name);
10581
+ return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10582
+ }
10583
+ function handleUndoList() {
10584
+ const history = getSnapshotHistory();
10585
+ if (history.length === 0) {
10586
+ return "No snapshots found. Node9 captures snapshots automatically before file edits.";
10587
+ }
10588
+ const lines = history.slice().reverse().map((entry, i) => {
10589
+ const date = new Date(entry.timestamp).toLocaleString();
10590
+ const files = entry.files?.length ? `${entry.files.length} file(s)` : "unknown files";
10591
+ const summary = entry.argsSummary ? ` \u2014 ${entry.argsSummary}` : "";
10592
+ return `[${i + 1}] ${entry.hash.slice(0, 7)} ${date} ${entry.tool}${summary} (${files}) cwd: ${entry.cwd}
10593
+ full hash: ${entry.hash}`;
10594
+ });
10595
+ return lines.join("\n\n");
10596
+ }
10597
+ function handleUndoRevert(args) {
10598
+ const hash = args.hash;
10599
+ if (typeof hash !== "string" || !hash) {
10600
+ throw new Error("hash is required and must be a non-empty string");
10601
+ }
10602
+ if (!/^[0-9a-f]{7,40}$/i.test(hash)) {
10603
+ throw new Error(`Invalid hash format: ${hash}`);
10604
+ }
10605
+ const cwd = typeof args.cwd === "string" && args.cwd ? args.cwd : process.cwd();
10606
+ const success = applyUndo(hash, cwd);
10607
+ if (!success) {
10608
+ throw new Error(
10609
+ `Revert failed for hash ${hash}. The snapshot may not exist for this directory, or git encountered an error.`
10610
+ );
10611
+ }
10612
+ return `Successfully reverted to snapshot ${hash.slice(0, 7)} in ${cwd}.`;
10613
+ }
10614
+ function runMcpServer() {
10615
+ const rl = import_readline4.default.createInterface({ input: process.stdin, terminal: false });
10616
+ rl.on("line", (line) => {
10617
+ let msg;
10618
+ try {
10619
+ msg = JSON.parse(line);
10620
+ } catch {
10621
+ process.stdout.write(err(null, -32700, "Parse error") + "\n");
10622
+ return;
10623
+ }
10624
+ const { method, id, params } = msg;
10625
+ if (method === "initialize") {
10626
+ process.stdout.write(
10627
+ ok(id, {
10628
+ protocolVersion: "2024-11-05",
10629
+ serverInfo: { name: "node9", version: "1.0.0" },
10630
+ capabilities: { tools: {} }
10631
+ }) + "\n"
10632
+ );
10633
+ return;
10634
+ }
10635
+ if (id === void 0 || id === null) {
10636
+ return;
10637
+ }
10638
+ if (method === "tools/list") {
10639
+ process.stdout.write(ok(id, { tools: TOOLS }) + "\n");
10640
+ return;
10641
+ }
10642
+ if (method === "tools/call") {
10643
+ const p = params ?? {};
10644
+ const toolName = p.name;
10645
+ const toolArgs = p.arguments ?? {};
10646
+ try {
10647
+ let text;
10648
+ if (toolName === "node9_status") {
10649
+ text = handleStatus();
10650
+ } else if (toolName === "node9_config_get") {
10651
+ text = handleConfigGet();
10652
+ } else if (toolName === "node9_shield_list") {
10653
+ text = handleShieldList();
10654
+ } else if (toolName === "node9_shield_enable") {
10655
+ text = handleShieldEnable(toolArgs);
10656
+ } else if (toolName === "node9_undo_list") {
10657
+ text = handleUndoList();
10658
+ } else if (toolName === "node9_undo_revert") {
10659
+ text = handleUndoRevert(toolArgs);
10660
+ } else {
10661
+ process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
10662
+ return;
10663
+ }
10664
+ process.stdout.write(ok(id, { content: [{ type: "text", text }] }) + "\n");
10665
+ } catch (e) {
10666
+ const message = e instanceof Error ? e.message : String(e);
10667
+ process.stdout.write(
10668
+ ok(id, {
10669
+ content: [{ type: "text", text: `Error: ${message}` }],
10670
+ isError: true
10671
+ }) + "\n"
10672
+ );
10673
+ }
10674
+ return;
10675
+ }
10676
+ process.stdout.write(err(id, -32601, `Method not found: ${method}`) + "\n");
10677
+ });
10678
+ rl.on("close", () => {
10679
+ process.exit(0);
10680
+ });
10681
+ }
10682
+
10683
+ // src/cli/commands/mcp-server.ts
10684
+ function registerMcpServerCommand(program2) {
10685
+ program2.command("mcp-server").description(
10686
+ "Run the Node9 MCP server \u2014 exposes node9 tools (undo, rules, \u2026) to Claude, Cursor, and Gemini"
10687
+ ).action(() => {
10688
+ runMcpServer();
10689
+ });
10690
+ }
10691
+
10340
10692
  // src/cli/commands/trust.ts
10341
10693
  var import_chalk16 = __toESM(require("chalk"));
10342
10694
  init_trusted_hosts();
@@ -10394,20 +10746,20 @@ function registerTrustCommand(program2) {
10394
10746
 
10395
10747
  // src/cli.ts
10396
10748
  var { version } = JSON.parse(
10397
- import_fs26.default.readFileSync(import_path29.default.join(__dirname, "../package.json"), "utf-8")
10749
+ import_fs27.default.readFileSync(import_path30.default.join(__dirname, "../package.json"), "utf-8")
10398
10750
  );
10399
10751
  var program = new import_commander.Command();
10400
10752
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
10401
10753
  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) => {
10402
10754
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
10403
- const credPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
10404
- if (!import_fs26.default.existsSync(import_path29.default.dirname(credPath)))
10405
- import_fs26.default.mkdirSync(import_path29.default.dirname(credPath), { recursive: true });
10755
+ const credPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "credentials.json");
10756
+ if (!import_fs27.default.existsSync(import_path30.default.dirname(credPath)))
10757
+ import_fs27.default.mkdirSync(import_path30.default.dirname(credPath), { recursive: true });
10406
10758
  const profileName = options.profile || "default";
10407
10759
  let existingCreds = {};
10408
10760
  try {
10409
- if (import_fs26.default.existsSync(credPath)) {
10410
- const raw = JSON.parse(import_fs26.default.readFileSync(credPath, "utf-8"));
10761
+ if (import_fs27.default.existsSync(credPath)) {
10762
+ const raw = JSON.parse(import_fs27.default.readFileSync(credPath, "utf-8"));
10411
10763
  if (raw.apiKey) {
10412
10764
  existingCreds = {
10413
10765
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -10419,13 +10771,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
10419
10771
  } catch {
10420
10772
  }
10421
10773
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
10422
- import_fs26.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10774
+ import_fs27.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10423
10775
  if (profileName === "default") {
10424
- const configPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "config.json");
10776
+ const configPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "config.json");
10425
10777
  let config = {};
10426
10778
  try {
10427
- if (import_fs26.default.existsSync(configPath))
10428
- config = JSON.parse(import_fs26.default.readFileSync(configPath, "utf-8"));
10779
+ if (import_fs27.default.existsSync(configPath))
10780
+ config = JSON.parse(import_fs27.default.readFileSync(configPath, "utf-8"));
10429
10781
  } catch {
10430
10782
  }
10431
10783
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -10440,9 +10792,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
10440
10792
  approvers.cloud = false;
10441
10793
  }
10442
10794
  s.approvers = approvers;
10443
- if (!import_fs26.default.existsSync(import_path29.default.dirname(configPath)))
10444
- import_fs26.default.mkdirSync(import_path29.default.dirname(configPath), { recursive: true });
10445
- import_fs26.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
10795
+ if (!import_fs27.default.existsSync(import_path30.default.dirname(configPath)))
10796
+ import_fs27.default.mkdirSync(import_path30.default.dirname(configPath), { recursive: true });
10797
+ import_fs27.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
10446
10798
  }
10447
10799
  if (options.profile && profileName !== "default") {
10448
10800
  console.log(import_chalk18.default.green(`\u2705 Profile "${profileName}" saved`));
@@ -10502,8 +10854,8 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
10502
10854
  `));
10503
10855
  try {
10504
10856
  fn();
10505
- } catch (err) {
10506
- console.error(import_chalk18.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
10857
+ } catch (err2) {
10858
+ console.error(import_chalk18.default.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
10507
10859
  process.exit(1);
10508
10860
  }
10509
10861
  console.log(import_chalk18.default.gray("\n Restart the agent for changes to take effect."));
@@ -10526,25 +10878,25 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
10526
10878
  ]) {
10527
10879
  try {
10528
10880
  fn();
10529
- } catch (err) {
10881
+ } catch (err2) {
10530
10882
  teardownFailed = true;
10531
10883
  console.error(
10532
10884
  import_chalk18.default.red(
10533
- ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
10885
+ ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
10534
10886
  )
10535
10887
  );
10536
10888
  }
10537
10889
  }
10538
10890
  if (options.purge) {
10539
- const node9Dir = import_path29.default.join(import_os22.default.homedir(), ".node9");
10540
- if (import_fs26.default.existsSync(node9Dir)) {
10891
+ const node9Dir = import_path30.default.join(import_os23.default.homedir(), ".node9");
10892
+ if (import_fs27.default.existsSync(node9Dir)) {
10541
10893
  const confirmed = await (0, import_prompts2.confirm)({
10542
10894
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
10543
10895
  default: false
10544
10896
  });
10545
10897
  if (confirmed) {
10546
- import_fs26.default.rmSync(node9Dir, { recursive: true });
10547
- if (import_fs26.default.existsSync(node9Dir)) {
10898
+ import_fs27.default.rmSync(node9Dir, { recursive: true });
10899
+ if (import_fs27.default.existsSync(node9Dir)) {
10548
10900
  console.error(
10549
10901
  import_chalk18.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
10550
10902
  );
@@ -10651,13 +11003,14 @@ program.command("tail").description("Stream live agent activity to the terminal"
10651
11003
  const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
10652
11004
  try {
10653
11005
  await startTail2(options);
10654
- } catch (err) {
10655
- console.error(import_chalk18.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
11006
+ } catch (err2) {
11007
+ console.error(import_chalk18.default.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
10656
11008
  process.exit(1);
10657
11009
  }
10658
11010
  });
10659
11011
  registerWatchCommand(program);
10660
11012
  registerMcpGatewayCommand(program);
11013
+ registerMcpServerCommand(program);
10661
11014
  registerCheckCommand(program);
10662
11015
  registerLogCommand(program);
10663
11016
  program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
@@ -10760,9 +11113,9 @@ if (process.argv[2] !== "daemon") {
10760
11113
  const isCheckHook = process.argv[2] === "check";
10761
11114
  if (isCheckHook) {
10762
11115
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
10763
- const logPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
11116
+ const logPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "hook-debug.log");
10764
11117
  const msg = reason instanceof Error ? reason.message : String(reason);
10765
- import_fs26.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
11118
+ import_fs27.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
10766
11119
  `);
10767
11120
  }
10768
11121
  process.exit(0);