@node9/proxy 1.5.5 → 1.7.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.
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}
@@ -777,7 +777,7 @@ var init_config = __esm({
777
777
  DEFAULT_CONFIG = {
778
778
  version: "1.0",
779
779
  settings: {
780
- mode: "audit",
780
+ mode: "standard",
781
781
  autoStartDaemon: true,
782
782
  enableUndo: true,
783
783
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
@@ -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
  }
@@ -3176,6 +3176,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
3176
3176
  await notifyActivity({
3177
3177
  id: actId,
3178
3178
  tool: toolName,
3179
+ args,
3179
3180
  ts: actTs,
3180
3181
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
3181
3182
  label: result.blockedByLabel,
@@ -3479,10 +3480,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3479
3480
  blockedBy: cloudResult.approved ? void 0 : "team-policy",
3480
3481
  blockedByLabel: "Organization Policy (SaaS)"
3481
3482
  };
3482
- } catch (err) {
3483
- const error = err;
3484
- if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err;
3485
- throw err;
3483
+ } catch (err2) {
3484
+ const error = err2;
3485
+ if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err2;
3486
+ throw err2;
3486
3487
  }
3487
3488
  })()
3488
3489
  );
@@ -3575,10 +3576,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3575
3576
  }
3576
3577
  };
3577
3578
  for (const p of racePromises) {
3578
- p.then(finish).catch((err) => {
3579
- if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
3579
+ p.then(finish).catch((err2) => {
3580
+ if (err2.name === "AbortError" || err2.message?.includes("canceled") || err2.message?.includes("Aborted"))
3580
3581
  return;
3581
- if (err.message === "Abandoned") {
3582
+ if (err2.message === "Abandoned") {
3582
3583
  finish({
3583
3584
  approved: false,
3584
3585
  reason: "Browser dashboard closed without making a decision.",
@@ -5453,6 +5454,7 @@ var init_session_counters = __esm({
5453
5454
  _blocked = 0;
5454
5455
  _dlpHits = 0;
5455
5456
  _wouldBlock = 0;
5457
+ _estimatedCost = 0;
5456
5458
  _lastRuleHit = null;
5457
5459
  _lastBlockedTool = null;
5458
5460
  incrementAllowed() {
@@ -5467,6 +5469,10 @@ var init_session_counters = __esm({
5467
5469
  incrementWouldBlock() {
5468
5470
  this._wouldBlock++;
5469
5471
  }
5472
+ addCost(amount) {
5473
+ if (!isFinite(amount) || amount < 0) return;
5474
+ this._estimatedCost += amount;
5475
+ }
5470
5476
  recordRuleHit(label) {
5471
5477
  this._lastRuleHit = label;
5472
5478
  }
@@ -5479,6 +5485,7 @@ var init_session_counters = __esm({
5479
5485
  blocked: this._blocked,
5480
5486
  dlpHits: this._dlpHits,
5481
5487
  wouldBlock: this._wouldBlock,
5488
+ estimatedCost: this._estimatedCost,
5482
5489
  lastRuleHit: this._lastRuleHit,
5483
5490
  lastBlockedTool: this._lastBlockedTool
5484
5491
  };
@@ -5488,6 +5495,7 @@ var init_session_counters = __esm({
5488
5495
  this._blocked = 0;
5489
5496
  this._dlpHits = 0;
5490
5497
  this._wouldBlock = 0;
5498
+ this._estimatedCost = 0;
5491
5499
  this._lastRuleHit = null;
5492
5500
  this._lastBlockedTool = null;
5493
5501
  }
@@ -5598,21 +5606,21 @@ function atomicWriteSync2(filePath, data, options) {
5598
5606
  const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
5599
5607
  try {
5600
5608
  fs13.writeFileSync(tmpPath, data, options);
5601
- } catch (err) {
5609
+ } catch (err2) {
5602
5610
  try {
5603
5611
  fs13.unlinkSync(tmpPath);
5604
5612
  } catch {
5605
5613
  }
5606
- throw err;
5614
+ throw err2;
5607
5615
  }
5608
5616
  try {
5609
5617
  fs13.renameSync(tmpPath, filePath);
5610
- } catch (err) {
5618
+ } catch (err2) {
5611
5619
  try {
5612
5620
  fs13.unlinkSync(tmpPath);
5613
5621
  } catch {
5614
5622
  }
5615
- throw err;
5623
+ throw err2;
5616
5624
  }
5617
5625
  }
5618
5626
  function redactArgs(value) {
@@ -5718,15 +5726,38 @@ function openBrowser(url) {
5718
5726
  } catch {
5719
5727
  }
5720
5728
  }
5729
+ function estimateToolCost(tool, args) {
5730
+ const a = args ?? {};
5731
+ const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
5732
+ if (t.includes("read") || t === "glob" || t === "grep") {
5733
+ const filePath = a.file_path ?? a.path;
5734
+ if (filePath) {
5735
+ try {
5736
+ const bytes = fs13.statSync(filePath).size;
5737
+ return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
5738
+ } catch {
5739
+ }
5740
+ }
5741
+ }
5742
+ if (t.includes("write")) {
5743
+ const content = a.content ?? "";
5744
+ return String(content).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5745
+ }
5746
+ if (t.includes("edit") || t === "str_replace_based_edit_tool") {
5747
+ const newStr = a.new_string ?? "";
5748
+ return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5749
+ }
5750
+ return void 0;
5751
+ }
5721
5752
  function broadcast(event, data) {
5722
5753
  if (event === "activity") {
5723
5754
  activityRing.push({ event, data });
5724
5755
  if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
5725
5756
  } else if (event === "activity-result") {
5726
- const { id, status, label } = data;
5757
+ const { id, status, label, costEstimate } = data;
5727
5758
  for (let i = activityRing.length - 1; i >= 0; i--) {
5728
5759
  if (activityRing[i].data.id === id) {
5729
- Object.assign(activityRing[i].data, { status, label });
5760
+ Object.assign(activityRing[i].data, { status, label, costEstimate });
5730
5761
  break;
5731
5762
  }
5732
5763
  }
@@ -5791,6 +5822,16 @@ function startActivitySocket() {
5791
5822
  sessionHistory.recordTestFail(data.ts);
5792
5823
  return;
5793
5824
  }
5825
+ if (data.status === "snapshot") {
5826
+ broadcast("snapshot", {
5827
+ hash: data.hash,
5828
+ tool: data.tool,
5829
+ argsSummary: data.argsSummary,
5830
+ fileCount: data.fileCount,
5831
+ ts: data.ts
5832
+ });
5833
+ return;
5834
+ }
5794
5835
  if (data.status === "pending") {
5795
5836
  broadcast("activity", {
5796
5837
  id: data.id,
@@ -5818,10 +5859,13 @@ function startActivitySocket() {
5818
5859
  sessionCounters.incrementBlocked();
5819
5860
  sessionCounters.recordBlockedTool(data.tool);
5820
5861
  }
5862
+ const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
5863
+ if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
5821
5864
  broadcast("activity-result", {
5822
5865
  id: data.id,
5823
5866
  status: data.status,
5824
- label: data.label
5867
+ label: data.label,
5868
+ costEstimate
5825
5869
  });
5826
5870
  }
5827
5871
  } catch {
@@ -5838,7 +5882,7 @@ function startActivitySocket() {
5838
5882
  }
5839
5883
  });
5840
5884
  }
5841
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, WRITE_TOOL_NAMES;
5885
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES;
5842
5886
  var init_state2 = __esm({
5843
5887
  "src/daemon/state.ts"() {
5844
5888
  "use strict";
@@ -5876,6 +5920,9 @@ var init_state2 = __esm({
5876
5920
  ACTIVITY_RING_SIZE = 100;
5877
5921
  activityRing = [];
5878
5922
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
5923
+ INPUT_PRICE_PER_1M = 3;
5924
+ OUTPUT_PRICE_PER_1M = 15;
5925
+ BYTES_PER_TOKEN = 4;
5879
5926
  WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5880
5927
  "write",
5881
5928
  "write_file",
@@ -5922,21 +5969,21 @@ function patchConfig(configPath, patch) {
5922
5969
  const tmp = configPath + ".node9-tmp";
5923
5970
  try {
5924
5971
  fs14.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5925
- } catch (err) {
5972
+ } catch (err2) {
5926
5973
  try {
5927
5974
  fs14.unlinkSync(tmp);
5928
5975
  } catch {
5929
5976
  }
5930
- throw err;
5977
+ throw err2;
5931
5978
  }
5932
5979
  try {
5933
5980
  fs14.renameSync(tmp, configPath);
5934
- } catch (err) {
5981
+ } catch (err2) {
5935
5982
  try {
5936
5983
  fs14.unlinkSync(tmp);
5937
5984
  } catch {
5938
5985
  }
5939
- throw err;
5986
+ throw err2;
5940
5987
  }
5941
5988
  }
5942
5989
  var GLOBAL_CONFIG_PATH;
@@ -6191,11 +6238,11 @@ data: ${JSON.stringify(item.data)}
6191
6238
  e.earlyDecision = decision;
6192
6239
  e.earlyReason = result.reason;
6193
6240
  }
6194
- }).catch((err) => {
6241
+ }).catch((err2) => {
6195
6242
  const e = pending.get(id);
6196
6243
  if (!e) return;
6197
6244
  clearTimeout(e.timer);
6198
- const reason = err?.reason || "No response \u2014 request timed out";
6245
+ const reason = err2?.reason || "No response \u2014 request timed out";
6199
6246
  if (e.waiter) e.waiter("deny", reason);
6200
6247
  else {
6201
6248
  e.earlyDecision = "deny";
@@ -6322,8 +6369,8 @@ data: ${JSON.stringify(item.data)}
6322
6369
  const s = getGlobalSettings();
6323
6370
  res.writeHead(200, { "Content-Type": "application/json" });
6324
6371
  return res.end(JSON.stringify({ ...s, autoStarted }));
6325
- } catch (err) {
6326
- console.error(chalk2.red("[node9 daemon] GET /settings failed:"), err);
6372
+ } catch (err2) {
6373
+ console.error(chalk2.red("[node9 daemon] GET /settings failed:"), err2);
6327
6374
  res.writeHead(500, { "Content-Type": "application/json" });
6328
6375
  return res.end(JSON.stringify({ error: "internal" }));
6329
6376
  }
@@ -6339,7 +6386,8 @@ data: ${JSON.stringify(item.data)}
6339
6386
  allowed: counters.allowed,
6340
6387
  blocked: counters.blocked,
6341
6388
  dlpHits: counters.dlpHits,
6342
- wouldBlock: counters.wouldBlock
6389
+ wouldBlock: counters.wouldBlock,
6390
+ estimatedCost: counters.estimatedCost
6343
6391
  },
6344
6392
  taintedCount: taintStore.list().length,
6345
6393
  lastRuleHit: counters.lastRuleHit,
@@ -6347,8 +6395,8 @@ data: ${JSON.stringify(item.data)}
6347
6395
  };
6348
6396
  res.writeHead(200, { "Content-Type": "application/json" });
6349
6397
  return res.end(JSON.stringify(status));
6350
- } catch (err) {
6351
- console.error(chalk2.red("[node9 daemon] GET /status failed:"), err);
6398
+ } catch (err2) {
6399
+ console.error(chalk2.red("[node9 daemon] GET /status failed:"), err2);
6352
6400
  res.writeHead(500, { "Content-Type": "application/json" });
6353
6401
  return res.end(JSON.stringify({ error: "internal" }));
6354
6402
  }
@@ -6388,8 +6436,8 @@ data: ${JSON.stringify(item.data)}
6388
6436
  const s = getGlobalSettings();
6389
6437
  res.writeHead(200, { "Content-Type": "application/json" });
6390
6438
  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);
6439
+ } catch (err2) {
6440
+ console.error(chalk2.red("[node9 daemon] GET /slack-status failed:"), err2);
6393
6441
  res.writeHead(500, { "Content-Type": "application/json" });
6394
6442
  return res.end(JSON.stringify({ error: "internal" }));
6395
6443
  }
@@ -6471,6 +6519,7 @@ data: ${JSON.stringify(item.data)}
6471
6519
  }
6472
6520
  if (req.method === "POST" && pathname === "/events/clear") {
6473
6521
  activityRing.length = 0;
6522
+ sessionCounters.reset();
6474
6523
  res.writeHead(200, { "Content-Type": "application/json" });
6475
6524
  return res.end(JSON.stringify({ ok: true }));
6476
6525
  }
@@ -6549,10 +6598,10 @@ data: ${JSON.stringify(item.data)}
6549
6598
  broadcast("suggestion:resolved", { id, status: "applied" });
6550
6599
  res.writeHead(200, { "Content-Type": "application/json" });
6551
6600
  return res.end(JSON.stringify({ ok: true }));
6552
- } catch (err) {
6553
- console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
6601
+ } catch (err2) {
6602
+ console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err2);
6554
6603
  res.writeHead(500, { "Content-Type": "application/json" });
6555
- return res.end(JSON.stringify({ error: String(err) }));
6604
+ return res.end(JSON.stringify({ error: String(err2) }));
6556
6605
  }
6557
6606
  }
6558
6607
  if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
@@ -6767,10 +6816,10 @@ __export(tail_exports, {
6767
6816
  });
6768
6817
  import http2 from "http";
6769
6818
  import chalk17 from "chalk";
6770
- import fs24 from "fs";
6771
- import os20 from "os";
6772
- import path27 from "path";
6773
- import readline4 from "readline";
6819
+ import fs25 from "fs";
6820
+ import os21 from "os";
6821
+ import path28 from "path";
6822
+ import readline5 from "readline";
6774
6823
  import { spawn as spawn9, execSync as execSync3 } from "child_process";
6775
6824
  function getIcon(tool) {
6776
6825
  const t = tool.toLowerCase();
@@ -6783,7 +6832,7 @@ function formatBase(activity) {
6783
6832
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
6784
6833
  const icon = getIcon(activity.tool);
6785
6834
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6786
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
6835
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os21.homedir(), "~");
6787
6836
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6788
6837
  return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
6789
6838
  }
@@ -6797,11 +6846,13 @@ function renderResult(activity, result) {
6797
6846
  } else {
6798
6847
  status = chalk17.red("\u2717 BLOCK");
6799
6848
  }
6849
+ const cost = result.costEstimate ?? activity.costEstimate;
6850
+ const costSuffix = cost == null ? "" : chalk17.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
6800
6851
  if (process.stdout.isTTY) {
6801
- readline4.clearLine(process.stdout, 0);
6802
- readline4.cursorTo(process.stdout, 0);
6852
+ readline5.clearLine(process.stdout, 0);
6853
+ readline5.cursorTo(process.stdout, 0);
6803
6854
  }
6804
- console.log(`${base} ${status}`);
6855
+ console.log(`${base} ${status}${costSuffix}`);
6805
6856
  }
6806
6857
  function renderPending(activity) {
6807
6858
  if (!process.stdout.isTTY) return;
@@ -6809,9 +6860,9 @@ function renderPending(activity) {
6809
6860
  }
6810
6861
  async function ensureDaemon() {
6811
6862
  let pidPort = null;
6812
- if (fs24.existsSync(PID_FILE)) {
6863
+ if (fs25.existsSync(PID_FILE)) {
6813
6864
  try {
6814
- const { port } = JSON.parse(fs24.readFileSync(PID_FILE, "utf-8"));
6865
+ const { port } = JSON.parse(fs25.readFileSync(PID_FILE, "utf-8"));
6815
6866
  pidPort = port;
6816
6867
  } catch {
6817
6868
  console.error(chalk17.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -6930,6 +6981,39 @@ function buildRecoveryCardLines(req) {
6930
6981
  ``
6931
6982
  ];
6932
6983
  }
6984
+ function readApproversFromDisk() {
6985
+ const configPath = path28.join(os21.homedir(), ".node9", "config.json");
6986
+ try {
6987
+ const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
6988
+ const settings = raw.settings ?? {};
6989
+ return settings.approvers ?? {};
6990
+ } catch {
6991
+ return {};
6992
+ }
6993
+ }
6994
+ function approverStatusLine() {
6995
+ const a = readApproversFromDisk();
6996
+ const fmt = (label, key) => {
6997
+ const on = a[key] !== false;
6998
+ return `[${key[0]}]${label.slice(1)} ${on ? chalk17.green("\u2713") : chalk17.dim("\u2717")}`;
6999
+ };
7000
+ return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
7001
+ }
7002
+ function toggleApprover(channel) {
7003
+ const configPath = path28.join(os21.homedir(), ".node9", "config.json");
7004
+ try {
7005
+ const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
7006
+ const settings = raw.settings ?? {};
7007
+ const approvers = settings.approvers ?? {};
7008
+ approvers[channel] = approvers[channel] === false;
7009
+ settings.approvers = approvers;
7010
+ raw.settings = settings;
7011
+ fs25.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7012
+ } catch (err2) {
7013
+ process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
7014
+ `);
7015
+ }
7016
+ }
6933
7017
  async function startTail(options = {}) {
6934
7018
  const port = await ensureDaemon();
6935
7019
  if (options.clear) {
@@ -6948,7 +7032,7 @@ async function startTail(options = {}) {
6948
7032
  res.resume();
6949
7033
  }
6950
7034
  );
6951
- req2.once("error", (err) => resolve({ ok: false, code: err.code }));
7035
+ req2.once("error", (err2) => resolve({ ok: false, code: err2.code }));
6952
7036
  req2.setTimeout(2e3, () => {
6953
7037
  resolve({ ok: false, code: "ETIMEDOUT" });
6954
7038
  req2.destroy();
@@ -6975,10 +7059,48 @@ async function startTail(options = {}) {
6975
7059
  let cancelActiveCard = null;
6976
7060
  const localAllowCounts = /* @__PURE__ */ new Map();
6977
7061
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
6978
- if (canApprove) readline4.emitKeypressEvents(process.stdin);
7062
+ if (canApprove) readline5.emitKeypressEvents(process.stdin);
7063
+ let idleKeypressHandler = null;
7064
+ function enterIdleMode() {
7065
+ if (!canApprove || idleKeypressHandler !== null) return;
7066
+ try {
7067
+ process.stdin.setRawMode(true);
7068
+ } catch {
7069
+ return;
7070
+ }
7071
+ process.stdin.resume();
7072
+ idleKeypressHandler = (_str, key) => {
7073
+ const name = key?.name ?? "";
7074
+ if (key?.ctrl && name === "c") {
7075
+ process.kill(process.pid, "SIGINT");
7076
+ return;
7077
+ }
7078
+ if (name === "q") {
7079
+ process.kill(process.pid, "SIGINT");
7080
+ return;
7081
+ }
7082
+ const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7083
+ if (channel) {
7084
+ toggleApprover(channel);
7085
+ console.log(chalk17.dim(` Approvers: ${approverStatusLine()}`));
7086
+ }
7087
+ };
7088
+ process.stdin.on("keypress", idleKeypressHandler);
7089
+ }
7090
+ function exitIdleMode() {
7091
+ if (idleKeypressHandler) {
7092
+ process.stdin.removeListener("keypress", idleKeypressHandler);
7093
+ idleKeypressHandler = null;
7094
+ }
7095
+ try {
7096
+ process.stdin.setRawMode(false);
7097
+ } catch {
7098
+ }
7099
+ process.stdin.pause();
7100
+ }
6979
7101
  function clearCard() {
6980
7102
  if (cardLineCount > 0) {
6981
- readline4.moveCursor(process.stdout, 0, -cardLineCount);
7103
+ readline5.moveCursor(process.stdout, 0, -cardLineCount);
6982
7104
  process.stdout.write(ERASE_DOWN);
6983
7105
  cardLineCount = 0;
6984
7106
  }
@@ -6994,10 +7116,12 @@ async function startTail(options = {}) {
6994
7116
  }
6995
7117
  function showNextCard() {
6996
7118
  if (cardActive || approvalQueue.length === 0 || !canApprove) return;
7119
+ exitIdleMode();
6997
7120
  try {
6998
7121
  process.stdin.setRawMode(true);
6999
7122
  } catch {
7000
7123
  cardActive = false;
7124
+ enterIdleMode();
7001
7125
  return;
7002
7126
  }
7003
7127
  cardActive = true;
@@ -7009,12 +7133,8 @@ async function startTail(options = {}) {
7009
7133
  const handler = onKeypress;
7010
7134
  onKeypress = null;
7011
7135
  if (handler) process.stdin.removeListener("keypress", handler);
7012
- try {
7013
- process.stdin.setRawMode(false);
7014
- } catch {
7015
- }
7016
- process.stdin.pause();
7017
7136
  cancelActiveCard = null;
7137
+ enterIdleMode();
7018
7138
  };
7019
7139
  const settle = (action) => {
7020
7140
  if (settled) return;
@@ -7054,11 +7174,11 @@ async function startTail(options = {}) {
7054
7174
  } else {
7055
7175
  httpDecision = action;
7056
7176
  }
7057
- postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
7177
+ postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7058
7178
  try {
7059
- fs24.appendFileSync(
7060
- path27.join(os20.homedir(), ".node9", "hook-debug.log"),
7061
- `[tail] POST /decision failed: ${String(err)}
7179
+ fs25.appendFileSync(
7180
+ path28.join(os21.homedir(), ".node9", "hook-debug.log"),
7181
+ `[tail] POST /decision failed: ${String(err2)}
7062
7182
  `
7063
7183
  );
7064
7184
  } catch {
@@ -7141,23 +7261,21 @@ async function startTail(options = {}) {
7141
7261
  console.log(chalk17.cyan.bold(`
7142
7262
  \u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
7143
7263
  if (canApprove) {
7144
- console.log(
7145
- chalk17.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
7146
- );
7264
+ console.log(chalk17.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7265
+ console.log(chalk17.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7147
7266
  }
7148
7267
  if (options.history) {
7149
- console.log(chalk17.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
7268
+ console.log(chalk17.dim("Showing history + live events.\n"));
7150
7269
  } else {
7151
- console.log(
7152
- chalk17.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
7153
- );
7270
+ console.log(chalk17.dim("Showing live events only. Use --history to include past.\n"));
7154
7271
  }
7155
7272
  process.on("SIGINT", () => {
7273
+ exitIdleMode();
7156
7274
  clearCard();
7157
7275
  process.stdout.write(SHOW_CURSOR);
7158
7276
  if (process.stdout.isTTY) {
7159
- readline4.clearLine(process.stdout, 0);
7160
- readline4.cursorTo(process.stdout, 0);
7277
+ readline5.clearLine(process.stdout, 0);
7278
+ readline5.cursorTo(process.stdout, 0);
7161
7279
  }
7162
7280
  console.log(chalk17.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7163
7281
  process.exit(0);
@@ -7168,11 +7286,12 @@ async function startTail(options = {}) {
7168
7286
  console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
7169
7287
  process.exit(1);
7170
7288
  }
7289
+ if (canApprove) enterIdleMode();
7171
7290
  let currentEvent = "";
7172
7291
  let currentData = "";
7173
7292
  res.on("error", () => {
7174
7293
  });
7175
- const rl = readline4.createInterface({ input: res, crlfDelay: Infinity });
7294
+ const rl = readline5.createInterface({ input: res, crlfDelay: Infinity });
7176
7295
  rl.on("error", () => {
7177
7296
  });
7178
7297
  rl.on("line", (line) => {
@@ -7192,8 +7311,8 @@ async function startTail(options = {}) {
7192
7311
  clearCard();
7193
7312
  process.stdout.write(SHOW_CURSOR);
7194
7313
  if (process.stdout.isTTY) {
7195
- readline4.clearLine(process.stdout, 0);
7196
- readline4.cursorTo(process.stdout, 0);
7314
+ readline5.clearLine(process.stdout, 0);
7315
+ readline5.cursorTo(process.stdout, 0);
7197
7316
  }
7198
7317
  console.log(chalk17.red("\n\u274C Daemon disconnected."));
7199
7318
  process.exit(1);
@@ -7271,6 +7390,18 @@ async function startTail(options = {}) {
7271
7390
  const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7272
7391
  if (slowTool) renderPending(data);
7273
7392
  }
7393
+ if (event === "snapshot") {
7394
+ const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
7395
+ const hash = data.hash ?? "";
7396
+ const summary = data.argsSummary ?? data.tool;
7397
+ const fileCount = data.fileCount ?? 0;
7398
+ const files = fileCount > 0 ? chalk17.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7399
+ process.stdout.write(
7400
+ `${chalk17.dim(time)} ${chalk17.cyan("\u{1F4F8} snapshot")} ${chalk17.dim(hash)} ${summary}${files}
7401
+ `
7402
+ );
7403
+ return;
7404
+ }
7274
7405
  if (event === "activity-result") {
7275
7406
  const original = activityPending.get(data.id);
7276
7407
  if (original) {
@@ -7279,8 +7410,8 @@ async function startTail(options = {}) {
7279
7410
  }
7280
7411
  }
7281
7412
  }
7282
- req.on("error", (err) => {
7283
- const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
7413
+ req.on("error", (err2) => {
7414
+ const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
7284
7415
  console.error(chalk17.red(`
7285
7416
  \u274C ${msg}`));
7286
7417
  process.exit(1);
@@ -7293,7 +7424,7 @@ var init_tail = __esm({
7293
7424
  init_daemon2();
7294
7425
  init_daemon();
7295
7426
  init_core();
7296
- PID_FILE = path27.join(os20.homedir(), ".node9", "daemon.pid");
7427
+ PID_FILE = path28.join(os21.homedir(), ".node9", "daemon.pid");
7297
7428
  ICONS = {
7298
7429
  bash: "\u{1F4BB}",
7299
7430
  shell: "\u{1F4BB}",
@@ -7332,9 +7463,9 @@ __export(hud_exports, {
7332
7463
  main: () => main,
7333
7464
  renderEnvironmentLine: () => renderEnvironmentLine
7334
7465
  });
7335
- import fs25 from "fs";
7336
- import path28 from "path";
7337
- import os21 from "os";
7466
+ import fs26 from "fs";
7467
+ import path29 from "path";
7468
+ import os22 from "os";
7338
7469
  import http3 from "http";
7339
7470
  async function readStdin() {
7340
7471
  const chunks = [];
@@ -7410,9 +7541,9 @@ function formatTimeLeft(resetsAt) {
7410
7541
  return ` (${m}m left)`;
7411
7542
  }
7412
7543
  function safeReadJson(filePath) {
7413
- if (!fs25.existsSync(filePath)) return null;
7544
+ if (!fs26.existsSync(filePath)) return null;
7414
7545
  try {
7415
- return JSON.parse(fs25.readFileSync(filePath, "utf-8"));
7546
+ return JSON.parse(fs26.readFileSync(filePath, "utf-8"));
7416
7547
  } catch {
7417
7548
  return null;
7418
7549
  }
@@ -7433,12 +7564,12 @@ function countHooksInFile(filePath) {
7433
7564
  return Object.keys(cfg.hooks).length;
7434
7565
  }
7435
7566
  function countRulesInDir(rulesDir) {
7436
- if (!fs25.existsSync(rulesDir)) return 0;
7567
+ if (!fs26.existsSync(rulesDir)) return 0;
7437
7568
  let count = 0;
7438
7569
  try {
7439
- for (const entry of fs25.readdirSync(rulesDir, { withFileTypes: true })) {
7570
+ for (const entry of fs26.readdirSync(rulesDir, { withFileTypes: true })) {
7440
7571
  if (entry.isDirectory()) {
7441
- count += countRulesInDir(path28.join(rulesDir, entry.name));
7572
+ count += countRulesInDir(path29.join(rulesDir, entry.name));
7442
7573
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
7443
7574
  count++;
7444
7575
  }
@@ -7449,46 +7580,46 @@ function countRulesInDir(rulesDir) {
7449
7580
  }
7450
7581
  function isSamePath(a, b) {
7451
7582
  try {
7452
- return path28.resolve(a) === path28.resolve(b);
7583
+ return path29.resolve(a) === path29.resolve(b);
7453
7584
  } catch {
7454
7585
  return false;
7455
7586
  }
7456
7587
  }
7457
7588
  function countConfigs(cwd) {
7458
- const homeDir2 = os21.homedir();
7459
- const claudeDir = path28.join(homeDir2, ".claude");
7589
+ const homeDir2 = os22.homedir();
7590
+ const claudeDir = path29.join(homeDir2, ".claude");
7460
7591
  let claudeMdCount = 0;
7461
7592
  let rulesCount = 0;
7462
7593
  let hooksCount = 0;
7463
7594
  const userMcpServers = /* @__PURE__ */ new Set();
7464
7595
  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");
7596
+ if (fs26.existsSync(path29.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7597
+ rulesCount += countRulesInDir(path29.join(claudeDir, "rules"));
7598
+ const userSettings = path29.join(claudeDir, "settings.json");
7468
7599
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7469
7600
  hooksCount += countHooksInFile(userSettings);
7470
- const userClaudeJson = path28.join(homeDir2, ".claude.json");
7601
+ const userClaudeJson = path29.join(homeDir2, ".claude.json");
7471
7602
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7472
7603
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7473
7604
  userMcpServers.delete(name);
7474
7605
  }
7475
7606
  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");
7607
+ if (fs26.existsSync(path29.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7608
+ if (fs26.existsSync(path29.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7609
+ const projectClaudeDir = path29.join(cwd, ".claude");
7479
7610
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7480
7611
  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");
7612
+ if (fs26.existsSync(path29.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7613
+ rulesCount += countRulesInDir(path29.join(projectClaudeDir, "rules"));
7614
+ const projSettings = path29.join(projectClaudeDir, "settings.json");
7484
7615
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7485
7616
  hooksCount += countHooksInFile(projSettings);
7486
7617
  }
7487
- if (fs25.existsSync(path28.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7488
- const localSettings = path28.join(projectClaudeDir, "settings.local.json");
7618
+ if (fs26.existsSync(path29.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7619
+ const localSettings = path29.join(projectClaudeDir, "settings.local.json");
7489
7620
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7490
7621
  hooksCount += countHooksInFile(localSettings);
7491
- const mcpJsonServers = getMcpServerNames(path28.join(cwd, ".mcp.json"));
7622
+ const mcpJsonServers = getMcpServerNames(path29.join(cwd, ".mcp.json"));
7492
7623
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7493
7624
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7494
7625
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -7546,6 +7677,11 @@ function renderSecurityLine(status) {
7546
7677
  parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7547
7678
  }
7548
7679
  }
7680
+ if (status.session.estimatedCost > 0) {
7681
+ const cost = status.session.estimatedCost;
7682
+ const costStr = cost >= 0.01 ? `$${cost.toFixed(2)}` : cost >= 1e-3 ? `$${cost.toFixed(3)}` : "<$0.001";
7683
+ parts.push(color(DIM, `~${costStr}`));
7684
+ }
7549
7685
  if (status.taintedCount > 0) {
7550
7686
  parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7551
7687
  }
@@ -7601,11 +7737,11 @@ async function main() {
7601
7737
  try {
7602
7738
  const cwd = stdin.cwd ?? process.cwd();
7603
7739
  for (const configPath of [
7604
- path28.join(cwd, "node9.config.json"),
7605
- path28.join(os21.homedir(), ".node9", "config.json")
7740
+ path29.join(cwd, "node9.config.json"),
7741
+ path29.join(os22.homedir(), ".node9", "config.json")
7606
7742
  ]) {
7607
- if (!fs25.existsSync(configPath)) continue;
7608
- const cfg = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
7743
+ if (!fs26.existsSync(configPath)) continue;
7744
+ const cfg = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
7609
7745
  const hud = cfg.settings?.hud;
7610
7746
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7611
7747
  }
@@ -7654,6 +7790,16 @@ import path14 from "path";
7654
7790
  import os10 from "os";
7655
7791
  import chalk from "chalk";
7656
7792
  import { confirm } from "@inquirer/prompts";
7793
+ var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7794
+ function hasNode9McpServer(servers) {
7795
+ const entry = servers["node9"];
7796
+ return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
7797
+ }
7798
+ function removeNode9McpServer(servers) {
7799
+ if (!hasNode9McpServer(servers)) return false;
7800
+ delete servers["node9"];
7801
+ return true;
7802
+ }
7657
7803
  function printDaemonTip() {
7658
7804
  console.log(
7659
7805
  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 +7857,10 @@ function teardownClaude() {
7711
7857
  const claudeConfig = readJson(mcpPath);
7712
7858
  if (claudeConfig?.mcpServers) {
7713
7859
  let mcpChanged = false;
7860
+ if (removeNode9McpServer(claudeConfig.mcpServers)) {
7861
+ mcpChanged = true;
7862
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.claude.json"));
7863
+ }
7714
7864
  for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
7715
7865
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7716
7866
  const [originalCmd, ...originalArgs] = server.args;
@@ -7754,6 +7904,10 @@ function teardownGemini() {
7754
7904
  }
7755
7905
  }
7756
7906
  if (settings.mcpServers) {
7907
+ if (removeNode9McpServer(settings.mcpServers)) {
7908
+ changed = true;
7909
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.gemini/settings.json"));
7910
+ }
7757
7911
  for (const [name, server] of Object.entries(settings.mcpServers)) {
7758
7912
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7759
7913
  const [originalCmd, ...originalArgs] = server.args;
@@ -7782,6 +7936,10 @@ function teardownCursor() {
7782
7936
  return;
7783
7937
  }
7784
7938
  let changed = false;
7939
+ if (removeNode9McpServer(mcpConfig.mcpServers)) {
7940
+ changed = true;
7941
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.cursor/mcp.json"));
7942
+ }
7785
7943
  for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
7786
7944
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7787
7945
  const [originalCmd, ...originalArgs] = server.args;
@@ -7807,6 +7965,7 @@ async function setupClaude() {
7807
7965
  const claudeConfig = readJson(mcpPath) ?? {};
7808
7966
  const settings = readJson(hooksPath) ?? {};
7809
7967
  const servers = claudeConfig.mcpServers ?? {};
7968
+ let hooksChanged = false;
7810
7969
  let anythingChanged = false;
7811
7970
  if (!settings.hooks) settings.hooks = {};
7812
7971
  const hasPreHook = settings.hooks.PreToolUse?.some(
@@ -7819,6 +7978,7 @@ async function setupClaude() {
7819
7978
  hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
7820
7979
  });
7821
7980
  console.log(chalk.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
7981
+ hooksChanged = true;
7822
7982
  anythingChanged = true;
7823
7983
  }
7824
7984
  const hasPostHook = settings.hooks.PostToolUse?.some(
@@ -7831,9 +7991,17 @@ async function setupClaude() {
7831
7991
  hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
7832
7992
  });
7833
7993
  console.log(chalk.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
7994
+ hooksChanged = true;
7834
7995
  anythingChanged = true;
7835
7996
  }
7836
- if (anythingChanged) {
7997
+ if (!hasNode9McpServer(servers)) {
7998
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
7999
+ claudeConfig.mcpServers = servers;
8000
+ writeJson(mcpPath, claudeConfig);
8001
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8002
+ anythingChanged = true;
8003
+ }
8004
+ if (hooksChanged) {
7837
8005
  writeJson(hooksPath, settings);
7838
8006
  console.log("");
7839
8007
  }
@@ -7881,6 +8049,7 @@ async function setupGemini() {
7881
8049
  const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
7882
8050
  const settings = readJson(settingsPath) ?? {};
7883
8051
  const servers = settings.mcpServers ?? {};
8052
+ let hooksChanged = false;
7884
8053
  let anythingChanged = false;
7885
8054
  if (!settings.hooks) settings.hooks = {};
7886
8055
  const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
@@ -7901,6 +8070,7 @@ async function setupGemini() {
7901
8070
  ]
7902
8071
  });
7903
8072
  console.log(chalk.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
8073
+ hooksChanged = true;
7904
8074
  anythingChanged = true;
7905
8075
  }
7906
8076
  const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
@@ -7914,9 +8084,17 @@ async function setupGemini() {
7914
8084
  hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
7915
8085
  });
7916
8086
  console.log(chalk.green(" \u2705 AfterTool hook added \u2192 node9 log"));
8087
+ hooksChanged = true;
7917
8088
  anythingChanged = true;
7918
8089
  }
7919
- if (anythingChanged) {
8090
+ if (!hasNode9McpServer(servers)) {
8091
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8092
+ settings.mcpServers = servers;
8093
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8094
+ hooksChanged = true;
8095
+ anythingChanged = true;
8096
+ }
8097
+ if (hooksChanged) {
7920
8098
  writeJson(settingsPath, settings);
7921
8099
  console.log("");
7922
8100
  }
@@ -7963,10 +8141,10 @@ function detectAgents(homeDir2 = os10.homedir()) {
7963
8141
  const exists = (p) => {
7964
8142
  try {
7965
8143
  return fs11.existsSync(p);
7966
- } catch (err) {
7967
- const code = err.code;
8144
+ } catch (err2) {
8145
+ const code = err2.code;
7968
8146
  if (code !== "ENOENT") {
7969
- process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
8147
+ process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err2)}
7970
8148
  `);
7971
8149
  }
7972
8150
  return false;
@@ -7984,6 +8162,13 @@ async function setupCursor() {
7984
8162
  const mcpConfig = readJson(mcpPath) ?? {};
7985
8163
  const servers = mcpConfig.mcpServers ?? {};
7986
8164
  let anythingChanged = false;
8165
+ if (!hasNode9McpServer(servers)) {
8166
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8167
+ mcpConfig.mcpServers = servers;
8168
+ writeJson(mcpPath, mcpConfig);
8169
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8170
+ anythingChanged = true;
8171
+ }
7987
8172
  const serversToWrap = [];
7988
8173
  for (const [name, server] of Object.entries(servers)) {
7989
8174
  if (!server.command || server.command === "node9") continue;
@@ -8083,9 +8268,9 @@ function teardownHud() {
8083
8268
  // src/cli.ts
8084
8269
  init_daemon2();
8085
8270
  import chalk18 from "chalk";
8086
- import fs26 from "fs";
8087
- import path29 from "path";
8088
- import os22 from "os";
8271
+ import fs27 from "fs";
8272
+ import path30 from "path";
8273
+ import os23 from "os";
8089
8274
  import { confirm as confirm2 } from "@inquirer/prompts";
8090
8275
 
8091
8276
  // src/utils/duration.ts
@@ -8320,8 +8505,29 @@ import os14 from "os";
8320
8505
  import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
8321
8506
  import crypto2 from "crypto";
8322
8507
  import fs17 from "fs";
8508
+ import net3 from "net";
8323
8509
  import path19 from "path";
8324
8510
  import os13 from "os";
8511
+ var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path19.join(os13.tmpdir(), "node9-activity.sock");
8512
+ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8513
+ try {
8514
+ const payload = JSON.stringify({
8515
+ status: "snapshot",
8516
+ hash,
8517
+ tool,
8518
+ argsSummary,
8519
+ fileCount,
8520
+ ts: Date.now()
8521
+ });
8522
+ const sock = net3.createConnection(ACTIVITY_SOCKET_PATH3);
8523
+ sock.on("connect", () => {
8524
+ sock.end(payload);
8525
+ });
8526
+ sock.on("error", () => {
8527
+ });
8528
+ } catch {
8529
+ }
8530
+ }
8325
8531
  var SNAPSHOT_STACK_PATH = path19.join(os13.homedir(), ".node9", "snapshots.json");
8326
8532
  var UNDO_LATEST_PATH = path19.join(os13.homedir(), ".node9", "undo_latest.txt");
8327
8533
  var MAX_SNAPSHOTS = 10;
@@ -8504,6 +8710,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8504
8710
  if (filesRes.status === 0) {
8505
8711
  capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8506
8712
  }
8713
+ if (capturedFiles.length === 0) {
8714
+ return prevEntry.hash;
8715
+ }
8507
8716
  const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
8508
8717
  env: shadowEnv,
8509
8718
  timeout: GIT_TIMEOUT
@@ -8541,13 +8750,15 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8541
8750
  }
8542
8751
  if (cwdCount > MAX_SNAPSHOTS) stack.splice(oldestCwdIdx, 1);
8543
8752
  writeStack(stack);
8753
+ const entry = stack[stack.length - 1];
8754
+ notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
8544
8755
  fs17.writeFileSync(UNDO_LATEST_PATH, commitHash);
8545
8756
  if (shouldGc) {
8546
8757
  spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
8547
8758
  }
8548
8759
  return commitHash;
8549
- } catch (err) {
8550
- if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
8760
+ } catch (err2) {
8761
+ if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err2);
8551
8762
  return null;
8552
8763
  } finally {
8553
8764
  if (indexFile) {
@@ -8651,11 +8862,11 @@ function registerCheckCommand(program2) {
8651
8862
  let payload = JSON.parse(raw);
8652
8863
  try {
8653
8864
  payload = JSON.parse(raw);
8654
- } catch (err) {
8865
+ } catch (err2) {
8655
8866
  const tempConfig = getConfig();
8656
8867
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
8657
8868
  const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
8658
- const errMsg = err instanceof Error ? err.message : String(err);
8869
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
8659
8870
  fs18.appendFileSync(
8660
8871
  logPath,
8661
8872
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
@@ -8776,10 +8987,10 @@ RAW: ${raw}
8776
8987
  ...result,
8777
8988
  blockedByLabel: result.blockedByLabel
8778
8989
  });
8779
- } catch (err) {
8990
+ } catch (err2) {
8780
8991
  if (process.env.NODE9_DEBUG === "1") {
8781
8992
  const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
8782
- const errMsg = err instanceof Error ? err.message : String(err);
8993
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
8783
8994
  fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
8784
8995
  `);
8785
8996
  }
@@ -8925,8 +9136,8 @@ function registerLogCommand(program2) {
8925
9136
  if (shouldSnapshot(tool, {}, config)) {
8926
9137
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
8927
9138
  }
8928
- } catch (err) {
8929
- const msg = err instanceof Error ? err.message : String(err);
9139
+ } catch (err2) {
9140
+ const msg = err2 instanceof Error ? err2.message : String(err2);
8930
9141
  process.stderr.write(`[Node9] audit log error: ${msg}
8931
9142
  `);
8932
9143
  const debugPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
@@ -9699,24 +9910,78 @@ import chalk11 from "chalk";
9699
9910
  import fs23 from "fs";
9700
9911
  import path25 from "path";
9701
9912
  import os19 from "os";
9913
+ import https from "https";
9914
+ function fireTelemetryPing(agents) {
9915
+ try {
9916
+ const body = JSON.stringify({
9917
+ event: "init_completed",
9918
+ agents_detected: agents,
9919
+ os: process.platform,
9920
+ node9_version: process.env.npm_package_version ?? "unknown"
9921
+ });
9922
+ const req = https.request(
9923
+ {
9924
+ hostname: "api.node9.ai",
9925
+ path: "/api/v1/telemetry",
9926
+ method: "POST",
9927
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
9928
+ timeout: 3e3
9929
+ },
9930
+ (res) => {
9931
+ res.resume();
9932
+ }
9933
+ );
9934
+ req.on("error", () => {
9935
+ });
9936
+ req.on("timeout", () => {
9937
+ req.destroy();
9938
+ });
9939
+ req.end(body);
9940
+ } catch {
9941
+ }
9942
+ }
9702
9943
  function registerInitCommand(program2) {
9703
9944
  program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
9704
9945
  console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
9946
+ let chosenMode = options.mode.toLowerCase();
9947
+ if (!["standard", "strict", "audit"].includes(chosenMode)) {
9948
+ chosenMode = DEFAULT_CONFIG.settings.mode;
9949
+ }
9950
+ {
9951
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
9952
+ const enableShields = await confirm3({
9953
+ message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
9954
+ default: true
9955
+ });
9956
+ if (enableShields) chosenMode = "standard";
9957
+ console.log("");
9958
+ }
9705
9959
  const configPath = path25.join(os19.homedir(), ".node9", "config.json");
9706
9960
  if (fs23.existsSync(configPath) && !options.force) {
9707
- console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
9961
+ try {
9962
+ const existing = JSON.parse(fs23.readFileSync(configPath, "utf-8"));
9963
+ const settings = existing.settings ?? {};
9964
+ if (settings.mode !== chosenMode) {
9965
+ settings.mode = chosenMode;
9966
+ existing.settings = settings;
9967
+ fs23.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
9968
+ console.log(chalk11.green(`\u2705 Mode updated: ${chosenMode}`));
9969
+ } else {
9970
+ console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
9971
+ }
9972
+ } catch {
9973
+ console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
9974
+ }
9708
9975
  } else {
9709
- const requestedMode = options.mode.toLowerCase();
9710
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
9711
9976
  const configToSave = {
9712
9977
  ...DEFAULT_CONFIG,
9713
- settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
9978
+ settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
9714
9979
  };
9715
9980
  const dir = path25.dirname(configPath);
9716
9981
  if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
9717
- fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
9982
+ fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
9718
9983
  console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
9719
- console.log(chalk11.gray(` Mode: ${safeMode}`));
9984
+ console.log(chalk11.gray(` Mode: ${chosenMode}`));
9720
9985
  }
9721
9986
  if (options.skipSetup) return;
9722
9987
  console.log("");
@@ -9743,14 +10008,20 @@ function registerInitCommand(program2) {
9743
10008
  else if (agent === "cursor") await setupCursor();
9744
10009
  console.log("");
9745
10010
  }
9746
- if (detected.claude) {
9747
- setupHud();
9748
- console.log(chalk11.green("\u2705 node9 HUD added to Claude Code statusline"));
9749
- console.log(chalk11.gray(" Restart Claude Code to activate the security statusline."));
10011
+ {
10012
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
10013
+ const sendTelemetry = await confirm3({
10014
+ message: "Send anonymous usage stats to help improve node9? (no code, no args)",
10015
+ default: true
10016
+ });
10017
+ if (sendTelemetry) fireTelemetryPing(found);
9750
10018
  console.log("");
9751
10019
  }
9752
10020
  console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
9753
- console.log(chalk11.gray(" Run: node9 daemon start"));
10021
+ console.log("");
10022
+ console.log(chalk11.white(" Start watching: ") + chalk11.cyan("node9 tail"));
10023
+ console.log(chalk11.white(" Browser view: ") + chalk11.cyan("node9 daemon --openui"));
10024
+ console.log(chalk11.white(" Cloud dashboard: ") + chalk11.cyan("node9.ai"));
9754
10025
  });
9755
10026
  }
9756
10027
 
@@ -10312,6 +10583,347 @@ function registerMcpGatewayCommand(program2) {
10312
10583
  });
10313
10584
  }
10314
10585
 
10586
+ // src/mcp-server/index.ts
10587
+ import readline4 from "readline";
10588
+ import fs24 from "fs";
10589
+ import os20 from "os";
10590
+ import path27 from "path";
10591
+ init_core();
10592
+ init_daemon();
10593
+ init_shields();
10594
+ function ok(id, result) {
10595
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, result });
10596
+ }
10597
+ function err(id, code, message) {
10598
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
10599
+ }
10600
+ var TOOLS = [
10601
+ {
10602
+ name: "node9_status",
10603
+ 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.",
10604
+ inputSchema: { type: "object", properties: {}, required: [] }
10605
+ },
10606
+ {
10607
+ name: "node9_config_get",
10608
+ 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).",
10609
+ inputSchema: { type: "object", properties: {}, required: [] }
10610
+ },
10611
+ {
10612
+ name: "node9_shield_list",
10613
+ 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).",
10614
+ inputSchema: { type: "object", properties: {}, required: [] }
10615
+ },
10616
+ {
10617
+ name: "node9_shield_enable",
10618
+ 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.",
10619
+ inputSchema: {
10620
+ type: "object",
10621
+ properties: {
10622
+ service: {
10623
+ type: "string",
10624
+ description: 'Shield name to enable (e.g. "postgres", "aws", "github", "filesystem").'
10625
+ }
10626
+ },
10627
+ required: ["service"]
10628
+ }
10629
+ },
10630
+ {
10631
+ name: "node9_approver_list",
10632
+ description: "List all node9 approver channels and their current enabled/disabled state. Approvers are the channels through which node9 asks a human to approve risky tool calls. Channels: native (OS popup), browser (web UI), cloud (team policy server), terminal (stdin).",
10633
+ inputSchema: { type: "object", properties: {}, required: [] }
10634
+ },
10635
+ {
10636
+ name: "node9_approver_set",
10637
+ description: "Enable or disable a specific node9 approver channel in the global config (~/.node9/config.json). Use this to turn individual channels on or off without touching other settings. Channels: native, browser, cloud, terminal. WARNING: disabling all approvers means node9 cannot prompt for human approval \u2014 use with care.",
10638
+ inputSchema: {
10639
+ type: "object",
10640
+ properties: {
10641
+ channel: {
10642
+ type: "string",
10643
+ enum: ["native", "browser", "cloud", "terminal"],
10644
+ description: "Approver channel to configure."
10645
+ },
10646
+ enabled: {
10647
+ type: "boolean",
10648
+ description: "true to enable the channel, false to disable it."
10649
+ }
10650
+ },
10651
+ required: ["channel", "enabled"]
10652
+ }
10653
+ },
10654
+ {
10655
+ name: "node9_undo_list",
10656
+ 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.",
10657
+ inputSchema: { type: "object", properties: {}, required: [] }
10658
+ },
10659
+ {
10660
+ name: "node9_undo_revert",
10661
+ 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.",
10662
+ inputSchema: {
10663
+ type: "object",
10664
+ properties: {
10665
+ hash: {
10666
+ type: "string",
10667
+ description: "The full git commit hash from node9_undo_list."
10668
+ },
10669
+ cwd: {
10670
+ type: "string",
10671
+ description: "Absolute path to the project directory. Defaults to process.cwd()."
10672
+ }
10673
+ },
10674
+ required: ["hash"]
10675
+ }
10676
+ }
10677
+ ];
10678
+ function handleStatus() {
10679
+ const config = getConfig();
10680
+ const settings = config.settings;
10681
+ const paused = checkPause();
10682
+ const daemonUp = isDaemonRunning();
10683
+ const activeShields = readActiveShields();
10684
+ const lines = [];
10685
+ lines.push(`Mode: ${settings.mode}`);
10686
+ lines.push(`Daemon: ${daemonUp ? "running" : "stopped"}`);
10687
+ lines.push(`Undo engine: ${settings.enableUndo ? "enabled" : "disabled"}`);
10688
+ if (paused.paused) {
10689
+ const until = paused.expiresAt ? new Date(paused.expiresAt).toLocaleTimeString() : "indefinitely";
10690
+ lines.push(`PAUSED until ${until} \u2014 all tool calls currently allowed`);
10691
+ } else {
10692
+ lines.push(`Pause state: not paused`);
10693
+ }
10694
+ lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
10695
+ lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
10696
+ lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
10697
+ const projectConfig = path27.join(process.cwd(), "node9.config.json");
10698
+ const globalConfig = path27.join(os20.homedir(), ".node9", "config.json");
10699
+ lines.push(
10700
+ `Project config (node9.config.json): ${fs24.existsSync(projectConfig) ? "present" : "not found"}`
10701
+ );
10702
+ lines.push(
10703
+ `Global config (~/.node9/config.json): ${fs24.existsSync(globalConfig) ? "present" : "not found"}`
10704
+ );
10705
+ return lines.join("\n");
10706
+ }
10707
+ function handleConfigGet() {
10708
+ const config = getConfig();
10709
+ const s = config.settings;
10710
+ const lines = [
10711
+ `mode: ${s.mode}`,
10712
+ `enableUndo: ${s.enableUndo}`,
10713
+ `flightRecorder: ${s.flightRecorder}`,
10714
+ `approvalTimeoutMs: ${s.approvalTimeoutMs}`,
10715
+ `approvers:`,
10716
+ ` native: ${s.approvers.native}`,
10717
+ ` browser: ${s.approvers.browser}`,
10718
+ ` cloud: ${s.approvers.cloud}`,
10719
+ ` terminal: ${s.approvers.terminal}`,
10720
+ `dlp.enabled: ${config.policy.dlp?.enabled !== false}`,
10721
+ `dlp.scanIgnoredTools: ${config.policy.dlp?.scanIgnoredTools !== false}`,
10722
+ `smartRules: ${config.policy.smartRules.length} active`,
10723
+ `sandboxPaths: ${config.policy.sandboxPaths.length > 0 ? config.policy.sandboxPaths.join(", ") : "none"}`
10724
+ ];
10725
+ return lines.join("\n");
10726
+ }
10727
+ function handleShieldList() {
10728
+ const all = listShields();
10729
+ const active = new Set(readActiveShields());
10730
+ if (all.length === 0) return "No shields available.";
10731
+ const lines = all.map((shield) => {
10732
+ const on = active.has(shield.name);
10733
+ const ruleCount = shield.smartRules.length;
10734
+ return `${on ? "[active]" : "[off] "} ${shield.name.padEnd(12)} \u2014 ${shield.description ?? ""} (${ruleCount} rule${ruleCount === 1 ? "" : "s"})`;
10735
+ });
10736
+ lines.unshift(`${active.size} of ${all.length} shields active:
10737
+ `);
10738
+ return lines.join("\n");
10739
+ }
10740
+ function handleShieldEnable(args) {
10741
+ const service = args.service;
10742
+ if (typeof service !== "string" || !service) {
10743
+ throw new Error("service is required");
10744
+ }
10745
+ const name = resolveShieldName(service);
10746
+ if (!name) {
10747
+ throw new Error(
10748
+ `Unknown shield: "${service}". Run node9_shield_list to see available shields.`
10749
+ );
10750
+ }
10751
+ const active = readActiveShields();
10752
+ if (active.includes(name)) {
10753
+ return `Shield "${name}" is already active.`;
10754
+ }
10755
+ writeActiveShields([...active, name]);
10756
+ const shield = getShield(name);
10757
+ return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10758
+ }
10759
+ var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
10760
+ var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
10761
+ function readGlobalConfigRaw() {
10762
+ try {
10763
+ if (fs24.existsSync(GLOBAL_CONFIG_PATH2)) {
10764
+ return JSON.parse(fs24.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
10765
+ }
10766
+ } catch {
10767
+ }
10768
+ return {};
10769
+ }
10770
+ function writeGlobalConfigRaw(data) {
10771
+ const dir = path27.dirname(GLOBAL_CONFIG_PATH2);
10772
+ if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
10773
+ fs24.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
10774
+ }
10775
+ function handleApproverList() {
10776
+ const config = getConfig();
10777
+ const approvers = config.settings.approvers;
10778
+ const lines = ["Approver channels:\n"];
10779
+ for (const ch of APPROVER_CHANNELS) {
10780
+ const on = approvers[ch];
10781
+ lines.push(` ${on ? "[enabled] " : "[disabled]"} ${ch}`);
10782
+ }
10783
+ const enabledCount = APPROVER_CHANNELS.filter((ch) => approvers[ch]).length;
10784
+ if (enabledCount === 0) {
10785
+ lines.push("\nWARNING: all approver channels are disabled \u2014 node9 cannot prompt for approval.");
10786
+ }
10787
+ return lines.join("\n");
10788
+ }
10789
+ function handleApproverSet(args) {
10790
+ const channel = args.channel;
10791
+ const enabled = args.enabled;
10792
+ if (!channel || !APPROVER_CHANNELS.includes(channel)) {
10793
+ throw new Error(
10794
+ `Invalid channel: "${channel}". Must be one of: ${APPROVER_CHANNELS.join(", ")}.`
10795
+ );
10796
+ }
10797
+ if (typeof enabled !== "boolean") {
10798
+ throw new Error("enabled must be a boolean (true or false).");
10799
+ }
10800
+ const raw = readGlobalConfigRaw();
10801
+ const settings = raw.settings ?? {};
10802
+ const approvers = settings.approvers ?? {};
10803
+ approvers[channel] = enabled;
10804
+ settings.approvers = approvers;
10805
+ raw.settings = settings;
10806
+ writeGlobalConfigRaw(raw);
10807
+ const currentApprovers = getConfig().settings.approvers;
10808
+ const anyEnabled = APPROVER_CHANNELS.some(
10809
+ (ch) => ch === channel ? enabled : currentApprovers[ch]
10810
+ );
10811
+ const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
10812
+ return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
10813
+ }
10814
+ function handleUndoList() {
10815
+ const history = getSnapshotHistory();
10816
+ if (history.length === 0) {
10817
+ return "No snapshots found. Node9 captures snapshots automatically before file edits.";
10818
+ }
10819
+ const lines = history.slice().reverse().map((entry, i) => {
10820
+ const date = new Date(entry.timestamp).toLocaleString();
10821
+ const files = entry.files?.length ? `${entry.files.length} file(s)` : "unknown files";
10822
+ const summary = entry.argsSummary ? ` \u2014 ${entry.argsSummary}` : "";
10823
+ return `[${i + 1}] ${entry.hash.slice(0, 7)} ${date} ${entry.tool}${summary} (${files}) cwd: ${entry.cwd}
10824
+ full hash: ${entry.hash}`;
10825
+ });
10826
+ return lines.join("\n\n");
10827
+ }
10828
+ function handleUndoRevert(args) {
10829
+ const hash = args.hash;
10830
+ if (typeof hash !== "string" || !hash) {
10831
+ throw new Error("hash is required and must be a non-empty string");
10832
+ }
10833
+ if (!/^[0-9a-f]{7,40}$/i.test(hash)) {
10834
+ throw new Error(`Invalid hash format: ${hash}`);
10835
+ }
10836
+ const cwd = typeof args.cwd === "string" && args.cwd ? args.cwd : process.cwd();
10837
+ const success = applyUndo(hash, cwd);
10838
+ if (!success) {
10839
+ throw new Error(
10840
+ `Revert failed for hash ${hash}. The snapshot may not exist for this directory, or git encountered an error.`
10841
+ );
10842
+ }
10843
+ return `Successfully reverted to snapshot ${hash.slice(0, 7)} in ${cwd}.`;
10844
+ }
10845
+ function runMcpServer() {
10846
+ const rl = readline4.createInterface({ input: process.stdin, terminal: false });
10847
+ rl.on("line", (line) => {
10848
+ let msg;
10849
+ try {
10850
+ msg = JSON.parse(line);
10851
+ } catch {
10852
+ process.stdout.write(err(null, -32700, "Parse error") + "\n");
10853
+ return;
10854
+ }
10855
+ const { method, id, params } = msg;
10856
+ if (method === "initialize") {
10857
+ process.stdout.write(
10858
+ ok(id, {
10859
+ protocolVersion: "2024-11-05",
10860
+ serverInfo: { name: "node9", version: "1.0.0" },
10861
+ capabilities: { tools: {} }
10862
+ }) + "\n"
10863
+ );
10864
+ return;
10865
+ }
10866
+ if (id === void 0 || id === null) {
10867
+ return;
10868
+ }
10869
+ if (method === "tools/list") {
10870
+ process.stdout.write(ok(id, { tools: TOOLS }) + "\n");
10871
+ return;
10872
+ }
10873
+ if (method === "tools/call") {
10874
+ const p = params ?? {};
10875
+ const toolName = p.name;
10876
+ const toolArgs = p.arguments ?? {};
10877
+ try {
10878
+ let text;
10879
+ if (toolName === "node9_status") {
10880
+ text = handleStatus();
10881
+ } else if (toolName === "node9_config_get") {
10882
+ text = handleConfigGet();
10883
+ } else if (toolName === "node9_shield_list") {
10884
+ text = handleShieldList();
10885
+ } else if (toolName === "node9_shield_enable") {
10886
+ text = handleShieldEnable(toolArgs);
10887
+ } else if (toolName === "node9_approver_list") {
10888
+ text = handleApproverList();
10889
+ } else if (toolName === "node9_approver_set") {
10890
+ text = handleApproverSet(toolArgs);
10891
+ } else if (toolName === "node9_undo_list") {
10892
+ text = handleUndoList();
10893
+ } else if (toolName === "node9_undo_revert") {
10894
+ text = handleUndoRevert(toolArgs);
10895
+ } else {
10896
+ process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
10897
+ return;
10898
+ }
10899
+ process.stdout.write(ok(id, { content: [{ type: "text", text }] }) + "\n");
10900
+ } catch (e) {
10901
+ const message = e instanceof Error ? e.message : String(e);
10902
+ process.stdout.write(
10903
+ ok(id, {
10904
+ content: [{ type: "text", text: `Error: ${message}` }],
10905
+ isError: true
10906
+ }) + "\n"
10907
+ );
10908
+ }
10909
+ return;
10910
+ }
10911
+ process.stdout.write(err(id, -32601, `Method not found: ${method}`) + "\n");
10912
+ });
10913
+ rl.on("close", () => {
10914
+ process.exit(0);
10915
+ });
10916
+ }
10917
+
10918
+ // src/cli/commands/mcp-server.ts
10919
+ function registerMcpServerCommand(program2) {
10920
+ program2.command("mcp-server").description(
10921
+ "Run the Node9 MCP server \u2014 exposes node9 tools (undo, rules, \u2026) to Claude, Cursor, and Gemini"
10922
+ ).action(() => {
10923
+ runMcpServer();
10924
+ });
10925
+ }
10926
+
10315
10927
  // src/cli/commands/trust.ts
10316
10928
  init_trusted_hosts();
10317
10929
  import chalk16 from "chalk";
@@ -10369,20 +10981,20 @@ function registerTrustCommand(program2) {
10369
10981
 
10370
10982
  // src/cli.ts
10371
10983
  var { version } = JSON.parse(
10372
- fs26.readFileSync(path29.join(__dirname, "../package.json"), "utf-8")
10984
+ fs27.readFileSync(path30.join(__dirname, "../package.json"), "utf-8")
10373
10985
  );
10374
10986
  var program = new Command();
10375
10987
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
10376
10988
  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
10989
  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 });
10990
+ const credPath = path30.join(os23.homedir(), ".node9", "credentials.json");
10991
+ if (!fs27.existsSync(path30.dirname(credPath)))
10992
+ fs27.mkdirSync(path30.dirname(credPath), { recursive: true });
10381
10993
  const profileName = options.profile || "default";
10382
10994
  let existingCreds = {};
10383
10995
  try {
10384
- if (fs26.existsSync(credPath)) {
10385
- const raw = JSON.parse(fs26.readFileSync(credPath, "utf-8"));
10996
+ if (fs27.existsSync(credPath)) {
10997
+ const raw = JSON.parse(fs27.readFileSync(credPath, "utf-8"));
10386
10998
  if (raw.apiKey) {
10387
10999
  existingCreds = {
10388
11000
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -10394,13 +11006,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
10394
11006
  } catch {
10395
11007
  }
10396
11008
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
10397
- fs26.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
11009
+ fs27.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10398
11010
  if (profileName === "default") {
10399
- const configPath = path29.join(os22.homedir(), ".node9", "config.json");
11011
+ const configPath = path30.join(os23.homedir(), ".node9", "config.json");
10400
11012
  let config = {};
10401
11013
  try {
10402
- if (fs26.existsSync(configPath))
10403
- config = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
11014
+ if (fs27.existsSync(configPath))
11015
+ config = JSON.parse(fs27.readFileSync(configPath, "utf-8"));
10404
11016
  } catch {
10405
11017
  }
10406
11018
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -10415,9 +11027,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
10415
11027
  approvers.cloud = false;
10416
11028
  }
10417
11029
  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 });
11030
+ if (!fs27.existsSync(path30.dirname(configPath)))
11031
+ fs27.mkdirSync(path30.dirname(configPath), { recursive: true });
11032
+ fs27.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
10421
11033
  }
10422
11034
  if (options.profile && profileName !== "default") {
10423
11035
  console.log(chalk18.green(`\u2705 Profile "${profileName}" saved`));
@@ -10477,8 +11089,8 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
10477
11089
  `));
10478
11090
  try {
10479
11091
  fn();
10480
- } catch (err) {
10481
- console.error(chalk18.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
11092
+ } catch (err2) {
11093
+ console.error(chalk18.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
10482
11094
  process.exit(1);
10483
11095
  }
10484
11096
  console.log(chalk18.gray("\n Restart the agent for changes to take effect."));
@@ -10501,25 +11113,25 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
10501
11113
  ]) {
10502
11114
  try {
10503
11115
  fn();
10504
- } catch (err) {
11116
+ } catch (err2) {
10505
11117
  teardownFailed = true;
10506
11118
  console.error(
10507
11119
  chalk18.red(
10508
- ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
11120
+ ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
10509
11121
  )
10510
11122
  );
10511
11123
  }
10512
11124
  }
10513
11125
  if (options.purge) {
10514
- const node9Dir = path29.join(os22.homedir(), ".node9");
10515
- if (fs26.existsSync(node9Dir)) {
11126
+ const node9Dir = path30.join(os23.homedir(), ".node9");
11127
+ if (fs27.existsSync(node9Dir)) {
10516
11128
  const confirmed = await confirm2({
10517
11129
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
10518
11130
  default: false
10519
11131
  });
10520
11132
  if (confirmed) {
10521
- fs26.rmSync(node9Dir, { recursive: true });
10522
- if (fs26.existsSync(node9Dir)) {
11133
+ fs27.rmSync(node9Dir, { recursive: true });
11134
+ if (fs27.existsSync(node9Dir)) {
10523
11135
  console.error(
10524
11136
  chalk18.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
10525
11137
  );
@@ -10626,13 +11238,14 @@ program.command("tail").description("Stream live agent activity to the terminal"
10626
11238
  const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
10627
11239
  try {
10628
11240
  await startTail2(options);
10629
- } catch (err) {
10630
- console.error(chalk18.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
11241
+ } catch (err2) {
11242
+ console.error(chalk18.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
10631
11243
  process.exit(1);
10632
11244
  }
10633
11245
  });
10634
11246
  registerWatchCommand(program);
10635
11247
  registerMcpGatewayCommand(program);
11248
+ registerMcpServerCommand(program);
10636
11249
  registerCheckCommand(program);
10637
11250
  registerLogCommand(program);
10638
11251
  program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
@@ -10735,9 +11348,9 @@ if (process.argv[2] !== "daemon") {
10735
11348
  const isCheckHook = process.argv[2] === "check";
10736
11349
  if (isCheckHook) {
10737
11350
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
10738
- const logPath = path29.join(os22.homedir(), ".node9", "hook-debug.log");
11351
+ const logPath = path30.join(os23.homedir(), ".node9", "hook-debug.log");
10739
11352
  const msg = reason instanceof Error ? reason.message : String(reason);
10740
- fs26.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
11353
+ fs27.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
10741
11354
  `);
10742
11355
  }
10743
11356
  process.exit(0);