@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.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}
@@ -799,7 +799,7 @@ var init_config = __esm({
799
799
  DEFAULT_CONFIG = {
800
800
  version: "1.0",
801
801
  settings: {
802
- mode: "audit",
802
+ mode: "standard",
803
803
  autoStartDaemon: true,
804
804
  enableUndo: true,
805
805
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
@@ -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
  }
@@ -3198,6 +3198,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
3198
3198
  await notifyActivity({
3199
3199
  id: actId,
3200
3200
  tool: toolName,
3201
+ args,
3201
3202
  ts: actTs,
3202
3203
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
3203
3204
  label: result.blockedByLabel,
@@ -3501,10 +3502,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3501
3502
  blockedBy: cloudResult.approved ? void 0 : "team-policy",
3502
3503
  blockedByLabel: "Organization Policy (SaaS)"
3503
3504
  };
3504
- } catch (err) {
3505
- const error = err;
3506
- if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err;
3507
- throw err;
3505
+ } catch (err2) {
3506
+ const error = err2;
3507
+ if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err2;
3508
+ throw err2;
3508
3509
  }
3509
3510
  })()
3510
3511
  );
@@ -3597,10 +3598,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3597
3598
  }
3598
3599
  };
3599
3600
  for (const p of racePromises) {
3600
- p.then(finish).catch((err) => {
3601
- if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
3601
+ p.then(finish).catch((err2) => {
3602
+ if (err2.name === "AbortError" || err2.message?.includes("canceled") || err2.message?.includes("Aborted"))
3602
3603
  return;
3603
- if (err.message === "Abandoned") {
3604
+ if (err2.message === "Abandoned") {
3604
3605
  finish({
3605
3606
  approved: false,
3606
3607
  reason: "Browser dashboard closed without making a decision.",
@@ -5476,6 +5477,7 @@ var init_session_counters = __esm({
5476
5477
  _blocked = 0;
5477
5478
  _dlpHits = 0;
5478
5479
  _wouldBlock = 0;
5480
+ _estimatedCost = 0;
5479
5481
  _lastRuleHit = null;
5480
5482
  _lastBlockedTool = null;
5481
5483
  incrementAllowed() {
@@ -5490,6 +5492,10 @@ var init_session_counters = __esm({
5490
5492
  incrementWouldBlock() {
5491
5493
  this._wouldBlock++;
5492
5494
  }
5495
+ addCost(amount) {
5496
+ if (!isFinite(amount) || amount < 0) return;
5497
+ this._estimatedCost += amount;
5498
+ }
5493
5499
  recordRuleHit(label) {
5494
5500
  this._lastRuleHit = label;
5495
5501
  }
@@ -5502,6 +5508,7 @@ var init_session_counters = __esm({
5502
5508
  blocked: this._blocked,
5503
5509
  dlpHits: this._dlpHits,
5504
5510
  wouldBlock: this._wouldBlock,
5511
+ estimatedCost: this._estimatedCost,
5505
5512
  lastRuleHit: this._lastRuleHit,
5506
5513
  lastBlockedTool: this._lastBlockedTool
5507
5514
  };
@@ -5511,6 +5518,7 @@ var init_session_counters = __esm({
5511
5518
  this._blocked = 0;
5512
5519
  this._dlpHits = 0;
5513
5520
  this._wouldBlock = 0;
5521
+ this._estimatedCost = 0;
5514
5522
  this._lastRuleHit = null;
5515
5523
  this._lastBlockedTool = null;
5516
5524
  }
@@ -5615,21 +5623,21 @@ function atomicWriteSync2(filePath, data, options) {
5615
5623
  const tmpPath = `${filePath}.${(0, import_crypto5.randomUUID)()}.tmp`;
5616
5624
  try {
5617
5625
  import_fs13.default.writeFileSync(tmpPath, data, options);
5618
- } catch (err) {
5626
+ } catch (err2) {
5619
5627
  try {
5620
5628
  import_fs13.default.unlinkSync(tmpPath);
5621
5629
  } catch {
5622
5630
  }
5623
- throw err;
5631
+ throw err2;
5624
5632
  }
5625
5633
  try {
5626
5634
  import_fs13.default.renameSync(tmpPath, filePath);
5627
- } catch (err) {
5635
+ } catch (err2) {
5628
5636
  try {
5629
5637
  import_fs13.default.unlinkSync(tmpPath);
5630
5638
  } catch {
5631
5639
  }
5632
- throw err;
5640
+ throw err2;
5633
5641
  }
5634
5642
  }
5635
5643
  function redactArgs(value) {
@@ -5735,15 +5743,38 @@ function openBrowser(url) {
5735
5743
  } catch {
5736
5744
  }
5737
5745
  }
5746
+ function estimateToolCost(tool, args) {
5747
+ const a = args ?? {};
5748
+ const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
5749
+ if (t.includes("read") || t === "glob" || t === "grep") {
5750
+ const filePath = a.file_path ?? a.path;
5751
+ if (filePath) {
5752
+ try {
5753
+ const bytes = import_fs13.default.statSync(filePath).size;
5754
+ return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
5755
+ } catch {
5756
+ }
5757
+ }
5758
+ }
5759
+ if (t.includes("write")) {
5760
+ const content = a.content ?? "";
5761
+ return String(content).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5762
+ }
5763
+ if (t.includes("edit") || t === "str_replace_based_edit_tool") {
5764
+ const newStr = a.new_string ?? "";
5765
+ return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5766
+ }
5767
+ return void 0;
5768
+ }
5738
5769
  function broadcast(event, data) {
5739
5770
  if (event === "activity") {
5740
5771
  activityRing.push({ event, data });
5741
5772
  if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
5742
5773
  } else if (event === "activity-result") {
5743
- const { id, status, label } = data;
5774
+ const { id, status, label, costEstimate } = data;
5744
5775
  for (let i = activityRing.length - 1; i >= 0; i--) {
5745
5776
  if (activityRing[i].data.id === id) {
5746
- Object.assign(activityRing[i].data, { status, label });
5777
+ Object.assign(activityRing[i].data, { status, label, costEstimate });
5747
5778
  break;
5748
5779
  }
5749
5780
  }
@@ -5808,6 +5839,16 @@ function startActivitySocket() {
5808
5839
  sessionHistory.recordTestFail(data.ts);
5809
5840
  return;
5810
5841
  }
5842
+ if (data.status === "snapshot") {
5843
+ broadcast("snapshot", {
5844
+ hash: data.hash,
5845
+ tool: data.tool,
5846
+ argsSummary: data.argsSummary,
5847
+ fileCount: data.fileCount,
5848
+ ts: data.ts
5849
+ });
5850
+ return;
5851
+ }
5811
5852
  if (data.status === "pending") {
5812
5853
  broadcast("activity", {
5813
5854
  id: data.id,
@@ -5835,10 +5876,13 @@ function startActivitySocket() {
5835
5876
  sessionCounters.incrementBlocked();
5836
5877
  sessionCounters.recordBlockedTool(data.tool);
5837
5878
  }
5879
+ const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
5880
+ if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
5838
5881
  broadcast("activity-result", {
5839
5882
  id: data.id,
5840
5883
  status: data.status,
5841
- label: data.label
5884
+ label: data.label,
5885
+ costEstimate
5842
5886
  });
5843
5887
  }
5844
5888
  } catch {
@@ -5855,7 +5899,7 @@ function startActivitySocket() {
5855
5899
  }
5856
5900
  });
5857
5901
  }
5858
- var import_net2, import_fs13, import_path16, import_os11, import_child_process3, import_crypto5, 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;
5902
+ var import_net2, import_fs13, import_path16, import_os11, import_child_process3, import_crypto5, 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;
5859
5903
  var init_state2 = __esm({
5860
5904
  "src/daemon/state.ts"() {
5861
5905
  "use strict";
@@ -5899,6 +5943,9 @@ var init_state2 = __esm({
5899
5943
  ACTIVITY_RING_SIZE = 100;
5900
5944
  activityRing = [];
5901
5945
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
5946
+ INPUT_PRICE_PER_1M = 3;
5947
+ OUTPUT_PRICE_PER_1M = 15;
5948
+ BYTES_PER_TOKEN = 4;
5902
5949
  WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5903
5950
  "write",
5904
5951
  "write_file",
@@ -5942,21 +5989,21 @@ function patchConfig(configPath, patch) {
5942
5989
  const tmp = configPath + ".node9-tmp";
5943
5990
  try {
5944
5991
  import_fs14.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5945
- } catch (err) {
5992
+ } catch (err2) {
5946
5993
  try {
5947
5994
  import_fs14.default.unlinkSync(tmp);
5948
5995
  } catch {
5949
5996
  }
5950
- throw err;
5997
+ throw err2;
5951
5998
  }
5952
5999
  try {
5953
6000
  import_fs14.default.renameSync(tmp, configPath);
5954
- } catch (err) {
6001
+ } catch (err2) {
5955
6002
  try {
5956
6003
  import_fs14.default.unlinkSync(tmp);
5957
6004
  } catch {
5958
6005
  }
5959
- throw err;
6006
+ throw err2;
5960
6007
  }
5961
6008
  }
5962
6009
  var import_fs14, import_path17, import_os12, GLOBAL_CONFIG_PATH;
@@ -6208,11 +6255,11 @@ data: ${JSON.stringify(item.data)}
6208
6255
  e.earlyDecision = decision;
6209
6256
  e.earlyReason = result.reason;
6210
6257
  }
6211
- }).catch((err) => {
6258
+ }).catch((err2) => {
6212
6259
  const e = pending.get(id);
6213
6260
  if (!e) return;
6214
6261
  clearTimeout(e.timer);
6215
- const reason = err?.reason || "No response \u2014 request timed out";
6262
+ const reason = err2?.reason || "No response \u2014 request timed out";
6216
6263
  if (e.waiter) e.waiter("deny", reason);
6217
6264
  else {
6218
6265
  e.earlyDecision = "deny";
@@ -6339,8 +6386,8 @@ data: ${JSON.stringify(item.data)}
6339
6386
  const s = getGlobalSettings();
6340
6387
  res.writeHead(200, { "Content-Type": "application/json" });
6341
6388
  return res.end(JSON.stringify({ ...s, autoStarted }));
6342
- } catch (err) {
6343
- console.error(import_chalk2.default.red("[node9 daemon] GET /settings failed:"), err);
6389
+ } catch (err2) {
6390
+ console.error(import_chalk2.default.red("[node9 daemon] GET /settings failed:"), err2);
6344
6391
  res.writeHead(500, { "Content-Type": "application/json" });
6345
6392
  return res.end(JSON.stringify({ error: "internal" }));
6346
6393
  }
@@ -6356,7 +6403,8 @@ data: ${JSON.stringify(item.data)}
6356
6403
  allowed: counters.allowed,
6357
6404
  blocked: counters.blocked,
6358
6405
  dlpHits: counters.dlpHits,
6359
- wouldBlock: counters.wouldBlock
6406
+ wouldBlock: counters.wouldBlock,
6407
+ estimatedCost: counters.estimatedCost
6360
6408
  },
6361
6409
  taintedCount: taintStore.list().length,
6362
6410
  lastRuleHit: counters.lastRuleHit,
@@ -6364,8 +6412,8 @@ data: ${JSON.stringify(item.data)}
6364
6412
  };
6365
6413
  res.writeHead(200, { "Content-Type": "application/json" });
6366
6414
  return res.end(JSON.stringify(status));
6367
- } catch (err) {
6368
- console.error(import_chalk2.default.red("[node9 daemon] GET /status failed:"), err);
6415
+ } catch (err2) {
6416
+ console.error(import_chalk2.default.red("[node9 daemon] GET /status failed:"), err2);
6369
6417
  res.writeHead(500, { "Content-Type": "application/json" });
6370
6418
  return res.end(JSON.stringify({ error: "internal" }));
6371
6419
  }
@@ -6405,8 +6453,8 @@ data: ${JSON.stringify(item.data)}
6405
6453
  const s = getGlobalSettings();
6406
6454
  res.writeHead(200, { "Content-Type": "application/json" });
6407
6455
  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);
6456
+ } catch (err2) {
6457
+ console.error(import_chalk2.default.red("[node9 daemon] GET /slack-status failed:"), err2);
6410
6458
  res.writeHead(500, { "Content-Type": "application/json" });
6411
6459
  return res.end(JSON.stringify({ error: "internal" }));
6412
6460
  }
@@ -6488,6 +6536,7 @@ data: ${JSON.stringify(item.data)}
6488
6536
  }
6489
6537
  if (req.method === "POST" && pathname === "/events/clear") {
6490
6538
  activityRing.length = 0;
6539
+ sessionCounters.reset();
6491
6540
  res.writeHead(200, { "Content-Type": "application/json" });
6492
6541
  return res.end(JSON.stringify({ ok: true }));
6493
6542
  }
@@ -6566,10 +6615,10 @@ data: ${JSON.stringify(item.data)}
6566
6615
  broadcast("suggestion:resolved", { id, status: "applied" });
6567
6616
  res.writeHead(200, { "Content-Type": "application/json" });
6568
6617
  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);
6618
+ } catch (err2) {
6619
+ console.error(import_chalk2.default.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err2);
6571
6620
  res.writeHead(500, { "Content-Type": "application/json" });
6572
- return res.end(JSON.stringify({ error: String(err) }));
6621
+ return res.end(JSON.stringify({ error: String(err2) }));
6573
6622
  }
6574
6623
  }
6575
6624
  if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
@@ -6801,7 +6850,7 @@ function formatBase(activity) {
6801
6850
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
6802
6851
  const icon = getIcon(activity.tool);
6803
6852
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6804
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
6853
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os21.default.homedir(), "~");
6805
6854
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6806
6855
  return `${import_chalk17.default.gray(time)} ${icon} ${import_chalk17.default.white.bold(toolName)} ${import_chalk17.default.dim(argsPreview)}`;
6807
6856
  }
@@ -6815,11 +6864,13 @@ function renderResult(activity, result) {
6815
6864
  } else {
6816
6865
  status = import_chalk17.default.red("\u2717 BLOCK");
6817
6866
  }
6867
+ const cost = result.costEstimate ?? activity.costEstimate;
6868
+ const costSuffix = cost == null ? "" : import_chalk17.default.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
6818
6869
  if (process.stdout.isTTY) {
6819
- import_readline4.default.clearLine(process.stdout, 0);
6820
- import_readline4.default.cursorTo(process.stdout, 0);
6870
+ import_readline5.default.clearLine(process.stdout, 0);
6871
+ import_readline5.default.cursorTo(process.stdout, 0);
6821
6872
  }
6822
- console.log(`${base} ${status}`);
6873
+ console.log(`${base} ${status}${costSuffix}`);
6823
6874
  }
6824
6875
  function renderPending(activity) {
6825
6876
  if (!process.stdout.isTTY) return;
@@ -6827,9 +6878,9 @@ function renderPending(activity) {
6827
6878
  }
6828
6879
  async function ensureDaemon() {
6829
6880
  let pidPort = null;
6830
- if (import_fs24.default.existsSync(PID_FILE)) {
6881
+ if (import_fs25.default.existsSync(PID_FILE)) {
6831
6882
  try {
6832
- const { port } = JSON.parse(import_fs24.default.readFileSync(PID_FILE, "utf-8"));
6883
+ const { port } = JSON.parse(import_fs25.default.readFileSync(PID_FILE, "utf-8"));
6833
6884
  pidPort = port;
6834
6885
  } catch {
6835
6886
  console.error(import_chalk17.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
@@ -6948,6 +6999,39 @@ function buildRecoveryCardLines(req) {
6948
6999
  ``
6949
7000
  ];
6950
7001
  }
7002
+ function readApproversFromDisk() {
7003
+ const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
7004
+ try {
7005
+ const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
7006
+ const settings = raw.settings ?? {};
7007
+ return settings.approvers ?? {};
7008
+ } catch {
7009
+ return {};
7010
+ }
7011
+ }
7012
+ function approverStatusLine() {
7013
+ const a = readApproversFromDisk();
7014
+ const fmt = (label, key) => {
7015
+ const on = a[key] !== false;
7016
+ return `[${key[0]}]${label.slice(1)} ${on ? import_chalk17.default.green("\u2713") : import_chalk17.default.dim("\u2717")}`;
7017
+ };
7018
+ return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
7019
+ }
7020
+ function toggleApprover(channel) {
7021
+ const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
7022
+ try {
7023
+ const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
7024
+ const settings = raw.settings ?? {};
7025
+ const approvers = settings.approvers ?? {};
7026
+ approvers[channel] = approvers[channel] === false;
7027
+ settings.approvers = approvers;
7028
+ raw.settings = settings;
7029
+ import_fs25.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7030
+ } catch (err2) {
7031
+ process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
7032
+ `);
7033
+ }
7034
+ }
6951
7035
  async function startTail(options = {}) {
6952
7036
  const port = await ensureDaemon();
6953
7037
  if (options.clear) {
@@ -6966,7 +7050,7 @@ async function startTail(options = {}) {
6966
7050
  res.resume();
6967
7051
  }
6968
7052
  );
6969
- req2.once("error", (err) => resolve({ ok: false, code: err.code }));
7053
+ req2.once("error", (err2) => resolve({ ok: false, code: err2.code }));
6970
7054
  req2.setTimeout(2e3, () => {
6971
7055
  resolve({ ok: false, code: "ETIMEDOUT" });
6972
7056
  req2.destroy();
@@ -6993,10 +7077,48 @@ async function startTail(options = {}) {
6993
7077
  let cancelActiveCard = null;
6994
7078
  const localAllowCounts = /* @__PURE__ */ new Map();
6995
7079
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
6996
- if (canApprove) import_readline4.default.emitKeypressEvents(process.stdin);
7080
+ if (canApprove) import_readline5.default.emitKeypressEvents(process.stdin);
7081
+ let idleKeypressHandler = null;
7082
+ function enterIdleMode() {
7083
+ if (!canApprove || idleKeypressHandler !== null) return;
7084
+ try {
7085
+ process.stdin.setRawMode(true);
7086
+ } catch {
7087
+ return;
7088
+ }
7089
+ process.stdin.resume();
7090
+ idleKeypressHandler = (_str, key) => {
7091
+ const name = key?.name ?? "";
7092
+ if (key?.ctrl && name === "c") {
7093
+ process.kill(process.pid, "SIGINT");
7094
+ return;
7095
+ }
7096
+ if (name === "q") {
7097
+ process.kill(process.pid, "SIGINT");
7098
+ return;
7099
+ }
7100
+ const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7101
+ if (channel) {
7102
+ toggleApprover(channel);
7103
+ console.log(import_chalk17.default.dim(` Approvers: ${approverStatusLine()}`));
7104
+ }
7105
+ };
7106
+ process.stdin.on("keypress", idleKeypressHandler);
7107
+ }
7108
+ function exitIdleMode() {
7109
+ if (idleKeypressHandler) {
7110
+ process.stdin.removeListener("keypress", idleKeypressHandler);
7111
+ idleKeypressHandler = null;
7112
+ }
7113
+ try {
7114
+ process.stdin.setRawMode(false);
7115
+ } catch {
7116
+ }
7117
+ process.stdin.pause();
7118
+ }
6997
7119
  function clearCard() {
6998
7120
  if (cardLineCount > 0) {
6999
- import_readline4.default.moveCursor(process.stdout, 0, -cardLineCount);
7121
+ import_readline5.default.moveCursor(process.stdout, 0, -cardLineCount);
7000
7122
  process.stdout.write(ERASE_DOWN);
7001
7123
  cardLineCount = 0;
7002
7124
  }
@@ -7012,10 +7134,12 @@ async function startTail(options = {}) {
7012
7134
  }
7013
7135
  function showNextCard() {
7014
7136
  if (cardActive || approvalQueue.length === 0 || !canApprove) return;
7137
+ exitIdleMode();
7015
7138
  try {
7016
7139
  process.stdin.setRawMode(true);
7017
7140
  } catch {
7018
7141
  cardActive = false;
7142
+ enterIdleMode();
7019
7143
  return;
7020
7144
  }
7021
7145
  cardActive = true;
@@ -7027,12 +7151,8 @@ async function startTail(options = {}) {
7027
7151
  const handler = onKeypress;
7028
7152
  onKeypress = null;
7029
7153
  if (handler) process.stdin.removeListener("keypress", handler);
7030
- try {
7031
- process.stdin.setRawMode(false);
7032
- } catch {
7033
- }
7034
- process.stdin.pause();
7035
7154
  cancelActiveCard = null;
7155
+ enterIdleMode();
7036
7156
  };
7037
7157
  const settle = (action) => {
7038
7158
  if (settled) return;
@@ -7072,11 +7192,11 @@ async function startTail(options = {}) {
7072
7192
  } else {
7073
7193
  httpDecision = action;
7074
7194
  }
7075
- postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
7195
+ postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7076
7196
  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)}
7197
+ import_fs25.default.appendFileSync(
7198
+ import_path28.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
7199
+ `[tail] POST /decision failed: ${String(err2)}
7080
7200
  `
7081
7201
  );
7082
7202
  } catch {
@@ -7159,23 +7279,21 @@ async function startTail(options = {}) {
7159
7279
  console.log(import_chalk17.default.cyan.bold(`
7160
7280
  \u{1F6F0}\uFE0F Node9 tail `) + import_chalk17.default.dim(`\u2192 ${dashboardUrl}`));
7161
7281
  if (canApprove) {
7162
- console.log(
7163
- import_chalk17.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
7164
- );
7282
+ console.log(import_chalk17.default.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7283
+ console.log(import_chalk17.default.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7165
7284
  }
7166
7285
  if (options.history) {
7167
- console.log(import_chalk17.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
7286
+ console.log(import_chalk17.default.dim("Showing history + live events.\n"));
7168
7287
  } else {
7169
- console.log(
7170
- import_chalk17.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
7171
- );
7288
+ console.log(import_chalk17.default.dim("Showing live events only. Use --history to include past.\n"));
7172
7289
  }
7173
7290
  process.on("SIGINT", () => {
7291
+ exitIdleMode();
7174
7292
  clearCard();
7175
7293
  process.stdout.write(SHOW_CURSOR);
7176
7294
  if (process.stdout.isTTY) {
7177
- import_readline4.default.clearLine(process.stdout, 0);
7178
- import_readline4.default.cursorTo(process.stdout, 0);
7295
+ import_readline5.default.clearLine(process.stdout, 0);
7296
+ import_readline5.default.cursorTo(process.stdout, 0);
7179
7297
  }
7180
7298
  console.log(import_chalk17.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7181
7299
  process.exit(0);
@@ -7186,11 +7304,12 @@ async function startTail(options = {}) {
7186
7304
  console.error(import_chalk17.default.red(`Failed to connect: HTTP ${res.statusCode}`));
7187
7305
  process.exit(1);
7188
7306
  }
7307
+ if (canApprove) enterIdleMode();
7189
7308
  let currentEvent = "";
7190
7309
  let currentData = "";
7191
7310
  res.on("error", () => {
7192
7311
  });
7193
- const rl = import_readline4.default.createInterface({ input: res, crlfDelay: Infinity });
7312
+ const rl = import_readline5.default.createInterface({ input: res, crlfDelay: Infinity });
7194
7313
  rl.on("error", () => {
7195
7314
  });
7196
7315
  rl.on("line", (line) => {
@@ -7210,8 +7329,8 @@ async function startTail(options = {}) {
7210
7329
  clearCard();
7211
7330
  process.stdout.write(SHOW_CURSOR);
7212
7331
  if (process.stdout.isTTY) {
7213
- import_readline4.default.clearLine(process.stdout, 0);
7214
- import_readline4.default.cursorTo(process.stdout, 0);
7332
+ import_readline5.default.clearLine(process.stdout, 0);
7333
+ import_readline5.default.cursorTo(process.stdout, 0);
7215
7334
  }
7216
7335
  console.log(import_chalk17.default.red("\n\u274C Daemon disconnected."));
7217
7336
  process.exit(1);
@@ -7289,6 +7408,18 @@ async function startTail(options = {}) {
7289
7408
  const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
7290
7409
  if (slowTool) renderPending(data);
7291
7410
  }
7411
+ if (event === "snapshot") {
7412
+ const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
7413
+ const hash = data.hash ?? "";
7414
+ const summary = data.argsSummary ?? data.tool;
7415
+ const fileCount = data.fileCount ?? 0;
7416
+ const files = fileCount > 0 ? import_chalk17.default.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7417
+ process.stdout.write(
7418
+ `${import_chalk17.default.dim(time)} ${import_chalk17.default.cyan("\u{1F4F8} snapshot")} ${import_chalk17.default.dim(hash)} ${summary}${files}
7419
+ `
7420
+ );
7421
+ return;
7422
+ }
7292
7423
  if (event === "activity-result") {
7293
7424
  const original = activityPending.get(data.id);
7294
7425
  if (original) {
@@ -7297,28 +7428,28 @@ async function startTail(options = {}) {
7297
7428
  }
7298
7429
  }
7299
7430
  }
7300
- req.on("error", (err) => {
7301
- const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
7431
+ req.on("error", (err2) => {
7432
+ const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
7302
7433
  console.error(import_chalk17.default.red(`
7303
7434
  \u274C ${msg}`));
7304
7435
  process.exit(1);
7305
7436
  });
7306
7437
  }
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;
7438
+ 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
7439
  var init_tail = __esm({
7309
7440
  "src/tui/tail.ts"() {
7310
7441
  "use strict";
7311
7442
  import_http2 = __toESM(require("http"));
7312
7443
  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"));
7444
+ import_fs25 = __toESM(require("fs"));
7445
+ import_os21 = __toESM(require("os"));
7446
+ import_path28 = __toESM(require("path"));
7447
+ import_readline5 = __toESM(require("readline"));
7317
7448
  import_child_process13 = require("child_process");
7318
7449
  init_daemon2();
7319
7450
  init_daemon();
7320
7451
  init_core();
7321
- PID_FILE = import_path27.default.join(import_os20.default.homedir(), ".node9", "daemon.pid");
7452
+ PID_FILE = import_path28.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
7322
7453
  ICONS = {
7323
7454
  bash: "\u{1F4BB}",
7324
7455
  shell: "\u{1F4BB}",
@@ -7431,9 +7562,9 @@ function formatTimeLeft(resetsAt) {
7431
7562
  return ` (${m}m left)`;
7432
7563
  }
7433
7564
  function safeReadJson(filePath) {
7434
- if (!import_fs25.default.existsSync(filePath)) return null;
7565
+ if (!import_fs26.default.existsSync(filePath)) return null;
7435
7566
  try {
7436
- return JSON.parse(import_fs25.default.readFileSync(filePath, "utf-8"));
7567
+ return JSON.parse(import_fs26.default.readFileSync(filePath, "utf-8"));
7437
7568
  } catch {
7438
7569
  return null;
7439
7570
  }
@@ -7454,12 +7585,12 @@ function countHooksInFile(filePath) {
7454
7585
  return Object.keys(cfg.hooks).length;
7455
7586
  }
7456
7587
  function countRulesInDir(rulesDir) {
7457
- if (!import_fs25.default.existsSync(rulesDir)) return 0;
7588
+ if (!import_fs26.default.existsSync(rulesDir)) return 0;
7458
7589
  let count = 0;
7459
7590
  try {
7460
- for (const entry of import_fs25.default.readdirSync(rulesDir, { withFileTypes: true })) {
7591
+ for (const entry of import_fs26.default.readdirSync(rulesDir, { withFileTypes: true })) {
7461
7592
  if (entry.isDirectory()) {
7462
- count += countRulesInDir(import_path28.default.join(rulesDir, entry.name));
7593
+ count += countRulesInDir(import_path29.default.join(rulesDir, entry.name));
7463
7594
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
7464
7595
  count++;
7465
7596
  }
@@ -7470,46 +7601,46 @@ function countRulesInDir(rulesDir) {
7470
7601
  }
7471
7602
  function isSamePath(a, b) {
7472
7603
  try {
7473
- return import_path28.default.resolve(a) === import_path28.default.resolve(b);
7604
+ return import_path29.default.resolve(a) === import_path29.default.resolve(b);
7474
7605
  } catch {
7475
7606
  return false;
7476
7607
  }
7477
7608
  }
7478
7609
  function countConfigs(cwd) {
7479
- const homeDir2 = import_os21.default.homedir();
7480
- const claudeDir = import_path28.default.join(homeDir2, ".claude");
7610
+ const homeDir2 = import_os22.default.homedir();
7611
+ const claudeDir = import_path29.default.join(homeDir2, ".claude");
7481
7612
  let claudeMdCount = 0;
7482
7613
  let rulesCount = 0;
7483
7614
  let hooksCount = 0;
7484
7615
  const userMcpServers = /* @__PURE__ */ new Set();
7485
7616
  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");
7617
+ if (import_fs26.default.existsSync(import_path29.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7618
+ rulesCount += countRulesInDir(import_path29.default.join(claudeDir, "rules"));
7619
+ const userSettings = import_path29.default.join(claudeDir, "settings.json");
7489
7620
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7490
7621
  hooksCount += countHooksInFile(userSettings);
7491
- const userClaudeJson = import_path28.default.join(homeDir2, ".claude.json");
7622
+ const userClaudeJson = import_path29.default.join(homeDir2, ".claude.json");
7492
7623
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7493
7624
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7494
7625
  userMcpServers.delete(name);
7495
7626
  }
7496
7627
  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");
7628
+ if (import_fs26.default.existsSync(import_path29.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7629
+ if (import_fs26.default.existsSync(import_path29.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7630
+ const projectClaudeDir = import_path29.default.join(cwd, ".claude");
7500
7631
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7501
7632
  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");
7633
+ if (import_fs26.default.existsSync(import_path29.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7634
+ rulesCount += countRulesInDir(import_path29.default.join(projectClaudeDir, "rules"));
7635
+ const projSettings = import_path29.default.join(projectClaudeDir, "settings.json");
7505
7636
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7506
7637
  hooksCount += countHooksInFile(projSettings);
7507
7638
  }
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");
7639
+ if (import_fs26.default.existsSync(import_path29.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7640
+ const localSettings = import_path29.default.join(projectClaudeDir, "settings.local.json");
7510
7641
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7511
7642
  hooksCount += countHooksInFile(localSettings);
7512
- const mcpJsonServers = getMcpServerNames(import_path28.default.join(cwd, ".mcp.json"));
7643
+ const mcpJsonServers = getMcpServerNames(import_path29.default.join(cwd, ".mcp.json"));
7513
7644
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7514
7645
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7515
7646
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -7567,6 +7698,11 @@ function renderSecurityLine(status) {
7567
7698
  parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7568
7699
  }
7569
7700
  }
7701
+ if (status.session.estimatedCost > 0) {
7702
+ const cost = status.session.estimatedCost;
7703
+ const costStr = cost >= 0.01 ? `$${cost.toFixed(2)}` : cost >= 1e-3 ? `$${cost.toFixed(3)}` : "<$0.001";
7704
+ parts.push(color(DIM, `~${costStr}`));
7705
+ }
7570
7706
  if (status.taintedCount > 0) {
7571
7707
  parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7572
7708
  }
@@ -7622,11 +7758,11 @@ async function main() {
7622
7758
  try {
7623
7759
  const cwd = stdin.cwd ?? process.cwd();
7624
7760
  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")
7761
+ import_path29.default.join(cwd, "node9.config.json"),
7762
+ import_path29.default.join(import_os22.default.homedir(), ".node9", "config.json")
7627
7763
  ]) {
7628
- if (!import_fs25.default.existsSync(configPath)) continue;
7629
- const cfg = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
7764
+ if (!import_fs26.default.existsSync(configPath)) continue;
7765
+ const cfg = JSON.parse(import_fs26.default.readFileSync(configPath, "utf-8"));
7630
7766
  const hud = cfg.settings?.hud;
7631
7767
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
7632
7768
  }
@@ -7644,13 +7780,13 @@ async function main() {
7644
7780
  renderOffline();
7645
7781
  }
7646
7782
  }
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;
7783
+ var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
7648
7784
  var init_hud = __esm({
7649
7785
  "src/cli/hud.ts"() {
7650
7786
  "use strict";
7651
- import_fs25 = __toESM(require("fs"));
7652
- import_path28 = __toESM(require("path"));
7653
- import_os21 = __toESM(require("os"));
7787
+ import_fs26 = __toESM(require("fs"));
7788
+ import_path29 = __toESM(require("path"));
7789
+ import_os22 = __toESM(require("os"));
7654
7790
  import_http3 = __toESM(require("http"));
7655
7791
  init_daemon();
7656
7792
  RESET3 = "\x1B[0m";
@@ -7679,6 +7815,16 @@ var import_path14 = __toESM(require("path"));
7679
7815
  var import_os10 = __toESM(require("os"));
7680
7816
  var import_chalk = __toESM(require("chalk"));
7681
7817
  var import_prompts = require("@inquirer/prompts");
7818
+ var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7819
+ function hasNode9McpServer(servers) {
7820
+ const entry = servers["node9"];
7821
+ return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
7822
+ }
7823
+ function removeNode9McpServer(servers) {
7824
+ if (!hasNode9McpServer(servers)) return false;
7825
+ delete servers["node9"];
7826
+ return true;
7827
+ }
7682
7828
  function printDaemonTip() {
7683
7829
  console.log(
7684
7830
  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 +7882,10 @@ function teardownClaude() {
7736
7882
  const claudeConfig = readJson(mcpPath);
7737
7883
  if (claudeConfig?.mcpServers) {
7738
7884
  let mcpChanged = false;
7885
+ if (removeNode9McpServer(claudeConfig.mcpServers)) {
7886
+ mcpChanged = true;
7887
+ console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.claude.json"));
7888
+ }
7739
7889
  for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
7740
7890
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7741
7891
  const [originalCmd, ...originalArgs] = server.args;
@@ -7779,6 +7929,10 @@ function teardownGemini() {
7779
7929
  }
7780
7930
  }
7781
7931
  if (settings.mcpServers) {
7932
+ if (removeNode9McpServer(settings.mcpServers)) {
7933
+ changed = true;
7934
+ console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.gemini/settings.json"));
7935
+ }
7782
7936
  for (const [name, server] of Object.entries(settings.mcpServers)) {
7783
7937
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7784
7938
  const [originalCmd, ...originalArgs] = server.args;
@@ -7807,6 +7961,10 @@ function teardownCursor() {
7807
7961
  return;
7808
7962
  }
7809
7963
  let changed = false;
7964
+ if (removeNode9McpServer(mcpConfig.mcpServers)) {
7965
+ changed = true;
7966
+ console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.cursor/mcp.json"));
7967
+ }
7810
7968
  for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
7811
7969
  if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7812
7970
  const [originalCmd, ...originalArgs] = server.args;
@@ -7832,6 +7990,7 @@ async function setupClaude() {
7832
7990
  const claudeConfig = readJson(mcpPath) ?? {};
7833
7991
  const settings = readJson(hooksPath) ?? {};
7834
7992
  const servers = claudeConfig.mcpServers ?? {};
7993
+ let hooksChanged = false;
7835
7994
  let anythingChanged = false;
7836
7995
  if (!settings.hooks) settings.hooks = {};
7837
7996
  const hasPreHook = settings.hooks.PreToolUse?.some(
@@ -7844,6 +8003,7 @@ async function setupClaude() {
7844
8003
  hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
7845
8004
  });
7846
8005
  console.log(import_chalk.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
8006
+ hooksChanged = true;
7847
8007
  anythingChanged = true;
7848
8008
  }
7849
8009
  const hasPostHook = settings.hooks.PostToolUse?.some(
@@ -7856,9 +8016,17 @@ async function setupClaude() {
7856
8016
  hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
7857
8017
  });
7858
8018
  console.log(import_chalk.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
8019
+ hooksChanged = true;
7859
8020
  anythingChanged = true;
7860
8021
  }
7861
- if (anythingChanged) {
8022
+ if (!hasNode9McpServer(servers)) {
8023
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8024
+ claudeConfig.mcpServers = servers;
8025
+ writeJson(mcpPath, claudeConfig);
8026
+ console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8027
+ anythingChanged = true;
8028
+ }
8029
+ if (hooksChanged) {
7862
8030
  writeJson(hooksPath, settings);
7863
8031
  console.log("");
7864
8032
  }
@@ -7906,6 +8074,7 @@ async function setupGemini() {
7906
8074
  const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
7907
8075
  const settings = readJson(settingsPath) ?? {};
7908
8076
  const servers = settings.mcpServers ?? {};
8077
+ let hooksChanged = false;
7909
8078
  let anythingChanged = false;
7910
8079
  if (!settings.hooks) settings.hooks = {};
7911
8080
  const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
@@ -7926,6 +8095,7 @@ async function setupGemini() {
7926
8095
  ]
7927
8096
  });
7928
8097
  console.log(import_chalk.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
8098
+ hooksChanged = true;
7929
8099
  anythingChanged = true;
7930
8100
  }
7931
8101
  const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
@@ -7939,9 +8109,17 @@ async function setupGemini() {
7939
8109
  hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
7940
8110
  });
7941
8111
  console.log(import_chalk.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
8112
+ hooksChanged = true;
7942
8113
  anythingChanged = true;
7943
8114
  }
7944
- if (anythingChanged) {
8115
+ if (!hasNode9McpServer(servers)) {
8116
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8117
+ settings.mcpServers = servers;
8118
+ console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8119
+ hooksChanged = true;
8120
+ anythingChanged = true;
8121
+ }
8122
+ if (hooksChanged) {
7945
8123
  writeJson(settingsPath, settings);
7946
8124
  console.log("");
7947
8125
  }
@@ -7988,10 +8166,10 @@ function detectAgents(homeDir2 = import_os10.default.homedir()) {
7988
8166
  const exists = (p) => {
7989
8167
  try {
7990
8168
  return import_fs11.default.existsSync(p);
7991
- } catch (err) {
7992
- const code = err.code;
8169
+ } catch (err2) {
8170
+ const code = err2.code;
7993
8171
  if (code !== "ENOENT") {
7994
- process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
8172
+ process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err2)}
7995
8173
  `);
7996
8174
  }
7997
8175
  return false;
@@ -8009,6 +8187,13 @@ async function setupCursor() {
8009
8187
  const mcpConfig = readJson(mcpPath) ?? {};
8010
8188
  const servers = mcpConfig.mcpServers ?? {};
8011
8189
  let anythingChanged = false;
8190
+ if (!hasNode9McpServer(servers)) {
8191
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
8192
+ mcpConfig.mcpServers = servers;
8193
+ writeJson(mcpPath, mcpConfig);
8194
+ console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8195
+ anythingChanged = true;
8196
+ }
8012
8197
  const serversToWrap = [];
8013
8198
  for (const [name, server] of Object.entries(servers)) {
8014
8199
  if (!server.command || server.command === "node9") continue;
@@ -8108,9 +8293,9 @@ function teardownHud() {
8108
8293
  // src/cli.ts
8109
8294
  init_daemon2();
8110
8295
  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"));
8296
+ var import_fs27 = __toESM(require("fs"));
8297
+ var import_path30 = __toESM(require("path"));
8298
+ var import_os23 = __toESM(require("os"));
8114
8299
  var import_prompts2 = require("@inquirer/prompts");
8115
8300
 
8116
8301
  // src/utils/duration.ts
@@ -8345,8 +8530,29 @@ init_policy();
8345
8530
  var import_child_process8 = require("child_process");
8346
8531
  var import_crypto7 = __toESM(require("crypto"));
8347
8532
  var import_fs17 = __toESM(require("fs"));
8533
+ var import_net3 = __toESM(require("net"));
8348
8534
  var import_path19 = __toESM(require("path"));
8349
8535
  var import_os13 = __toESM(require("os"));
8536
+ var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path19.default.join(import_os13.default.tmpdir(), "node9-activity.sock");
8537
+ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8538
+ try {
8539
+ const payload = JSON.stringify({
8540
+ status: "snapshot",
8541
+ hash,
8542
+ tool,
8543
+ argsSummary,
8544
+ fileCount,
8545
+ ts: Date.now()
8546
+ });
8547
+ const sock = import_net3.default.createConnection(ACTIVITY_SOCKET_PATH3);
8548
+ sock.on("connect", () => {
8549
+ sock.end(payload);
8550
+ });
8551
+ sock.on("error", () => {
8552
+ });
8553
+ } catch {
8554
+ }
8555
+ }
8350
8556
  var SNAPSHOT_STACK_PATH = import_path19.default.join(import_os13.default.homedir(), ".node9", "snapshots.json");
8351
8557
  var UNDO_LATEST_PATH = import_path19.default.join(import_os13.default.homedir(), ".node9", "undo_latest.txt");
8352
8558
  var MAX_SNAPSHOTS = 10;
@@ -8529,6 +8735,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8529
8735
  if (filesRes.status === 0) {
8530
8736
  capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8531
8737
  }
8738
+ if (capturedFiles.length === 0) {
8739
+ return prevEntry.hash;
8740
+ }
8532
8741
  const diffRes = (0, import_child_process8.spawnSync)("git", ["diff", prevEntry.hash, commitHash], {
8533
8742
  env: shadowEnv,
8534
8743
  timeout: GIT_TIMEOUT
@@ -8566,13 +8775,15 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8566
8775
  }
8567
8776
  if (cwdCount > MAX_SNAPSHOTS) stack.splice(oldestCwdIdx, 1);
8568
8777
  writeStack(stack);
8778
+ const entry = stack[stack.length - 1];
8779
+ notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
8569
8780
  import_fs17.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
8570
8781
  if (shouldGc) {
8571
8782
  (0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
8572
8783
  }
8573
8784
  return commitHash;
8574
- } catch (err) {
8575
- if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
8785
+ } catch (err2) {
8786
+ if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err2);
8576
8787
  return null;
8577
8788
  } finally {
8578
8789
  if (indexFile) {
@@ -8676,11 +8887,11 @@ function registerCheckCommand(program2) {
8676
8887
  let payload = JSON.parse(raw);
8677
8888
  try {
8678
8889
  payload = JSON.parse(raw);
8679
- } catch (err) {
8890
+ } catch (err2) {
8680
8891
  const tempConfig = getConfig();
8681
8892
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
8682
8893
  const logPath = import_path20.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
8683
- const errMsg = err instanceof Error ? err.message : String(err);
8894
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
8684
8895
  import_fs18.default.appendFileSync(
8685
8896
  logPath,
8686
8897
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
@@ -8801,10 +9012,10 @@ RAW: ${raw}
8801
9012
  ...result,
8802
9013
  blockedByLabel: result.blockedByLabel
8803
9014
  });
8804
- } catch (err) {
9015
+ } catch (err2) {
8805
9016
  if (process.env.NODE9_DEBUG === "1") {
8806
9017
  const logPath = import_path20.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
8807
- const errMsg = err instanceof Error ? err.message : String(err);
9018
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
8808
9019
  import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
8809
9020
  `);
8810
9021
  }
@@ -8950,8 +9161,8 @@ function registerLogCommand(program2) {
8950
9161
  if (shouldSnapshot(tool, {}, config)) {
8951
9162
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
8952
9163
  }
8953
- } catch (err) {
8954
- const msg = err instanceof Error ? err.message : String(err);
9164
+ } catch (err2) {
9165
+ const msg = err2 instanceof Error ? err2.message : String(err2);
8955
9166
  process.stderr.write(`[Node9] audit log error: ${msg}
8956
9167
  `);
8957
9168
  const debugPath = import_path21.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
@@ -9723,25 +9934,79 @@ var import_chalk11 = __toESM(require("chalk"));
9723
9934
  var import_fs23 = __toESM(require("fs"));
9724
9935
  var import_path25 = __toESM(require("path"));
9725
9936
  var import_os19 = __toESM(require("os"));
9937
+ var import_https = __toESM(require("https"));
9726
9938
  init_core();
9939
+ function fireTelemetryPing(agents) {
9940
+ try {
9941
+ const body = JSON.stringify({
9942
+ event: "init_completed",
9943
+ agents_detected: agents,
9944
+ os: process.platform,
9945
+ node9_version: process.env.npm_package_version ?? "unknown"
9946
+ });
9947
+ const req = import_https.default.request(
9948
+ {
9949
+ hostname: "api.node9.ai",
9950
+ path: "/api/v1/telemetry",
9951
+ method: "POST",
9952
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
9953
+ timeout: 3e3
9954
+ },
9955
+ (res) => {
9956
+ res.resume();
9957
+ }
9958
+ );
9959
+ req.on("error", () => {
9960
+ });
9961
+ req.on("timeout", () => {
9962
+ req.destroy();
9963
+ });
9964
+ req.end(body);
9965
+ } catch {
9966
+ }
9967
+ }
9727
9968
  function registerInitCommand(program2) {
9728
9969
  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) => {
9729
9970
  console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
9971
+ let chosenMode = options.mode.toLowerCase();
9972
+ if (!["standard", "strict", "audit"].includes(chosenMode)) {
9973
+ chosenMode = DEFAULT_CONFIG.settings.mode;
9974
+ }
9975
+ {
9976
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
9977
+ const enableShields = await confirm3({
9978
+ message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
9979
+ default: true
9980
+ });
9981
+ if (enableShields) chosenMode = "standard";
9982
+ console.log("");
9983
+ }
9730
9984
  const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
9731
9985
  if (import_fs23.default.existsSync(configPath) && !options.force) {
9732
- console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
9986
+ try {
9987
+ const existing = JSON.parse(import_fs23.default.readFileSync(configPath, "utf-8"));
9988
+ const settings = existing.settings ?? {};
9989
+ if (settings.mode !== chosenMode) {
9990
+ settings.mode = chosenMode;
9991
+ existing.settings = settings;
9992
+ import_fs23.default.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
9993
+ console.log(import_chalk11.default.green(`\u2705 Mode updated: ${chosenMode}`));
9994
+ } else {
9995
+ console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
9996
+ }
9997
+ } catch {
9998
+ console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
9999
+ }
9733
10000
  } else {
9734
- const requestedMode = options.mode.toLowerCase();
9735
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
9736
10001
  const configToSave = {
9737
10002
  ...DEFAULT_CONFIG,
9738
- settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
10003
+ settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
9739
10004
  };
9740
10005
  const dir = import_path25.default.dirname(configPath);
9741
10006
  if (!import_fs23.default.existsSync(dir)) import_fs23.default.mkdirSync(dir, { recursive: true });
9742
- import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
10007
+ import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
9743
10008
  console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
9744
- console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
10009
+ console.log(import_chalk11.default.gray(` Mode: ${chosenMode}`));
9745
10010
  }
9746
10011
  if (options.skipSetup) return;
9747
10012
  console.log("");
@@ -9768,14 +10033,20 @@ function registerInitCommand(program2) {
9768
10033
  else if (agent === "cursor") await setupCursor();
9769
10034
  console.log("");
9770
10035
  }
9771
- if (detected.claude) {
9772
- setupHud();
9773
- console.log(import_chalk11.default.green("\u2705 node9 HUD added to Claude Code statusline"));
9774
- console.log(import_chalk11.default.gray(" Restart Claude Code to activate the security statusline."));
10036
+ {
10037
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
10038
+ const sendTelemetry = await confirm3({
10039
+ message: "Send anonymous usage stats to help improve node9? (no code, no args)",
10040
+ default: true
10041
+ });
10042
+ if (sendTelemetry) fireTelemetryPing(found);
9775
10043
  console.log("");
9776
10044
  }
9777
10045
  console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
9778
- console.log(import_chalk11.default.gray(" Run: node9 daemon start"));
10046
+ console.log("");
10047
+ console.log(import_chalk11.default.white(" Start watching: ") + import_chalk11.default.cyan("node9 tail"));
10048
+ console.log(import_chalk11.default.white(" Browser view: ") + import_chalk11.default.cyan("node9 daemon --openui"));
10049
+ console.log(import_chalk11.default.white(" Cloud dashboard: ") + import_chalk11.default.cyan("node9.ai"));
9779
10050
  });
9780
10051
  }
9781
10052
 
@@ -10337,6 +10608,347 @@ function registerMcpGatewayCommand(program2) {
10337
10608
  });
10338
10609
  }
10339
10610
 
10611
+ // src/mcp-server/index.ts
10612
+ var import_readline4 = __toESM(require("readline"));
10613
+ var import_fs24 = __toESM(require("fs"));
10614
+ var import_os20 = __toESM(require("os"));
10615
+ var import_path27 = __toESM(require("path"));
10616
+ init_core();
10617
+ init_daemon();
10618
+ init_shields();
10619
+ function ok(id, result) {
10620
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, result });
10621
+ }
10622
+ function err(id, code, message) {
10623
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
10624
+ }
10625
+ var TOOLS = [
10626
+ {
10627
+ name: "node9_status",
10628
+ 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.",
10629
+ inputSchema: { type: "object", properties: {}, required: [] }
10630
+ },
10631
+ {
10632
+ name: "node9_config_get",
10633
+ 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).",
10634
+ inputSchema: { type: "object", properties: {}, required: [] }
10635
+ },
10636
+ {
10637
+ name: "node9_shield_list",
10638
+ 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).",
10639
+ inputSchema: { type: "object", properties: {}, required: [] }
10640
+ },
10641
+ {
10642
+ name: "node9_shield_enable",
10643
+ 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.",
10644
+ inputSchema: {
10645
+ type: "object",
10646
+ properties: {
10647
+ service: {
10648
+ type: "string",
10649
+ description: 'Shield name to enable (e.g. "postgres", "aws", "github", "filesystem").'
10650
+ }
10651
+ },
10652
+ required: ["service"]
10653
+ }
10654
+ },
10655
+ {
10656
+ name: "node9_approver_list",
10657
+ 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).",
10658
+ inputSchema: { type: "object", properties: {}, required: [] }
10659
+ },
10660
+ {
10661
+ name: "node9_approver_set",
10662
+ 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.",
10663
+ inputSchema: {
10664
+ type: "object",
10665
+ properties: {
10666
+ channel: {
10667
+ type: "string",
10668
+ enum: ["native", "browser", "cloud", "terminal"],
10669
+ description: "Approver channel to configure."
10670
+ },
10671
+ enabled: {
10672
+ type: "boolean",
10673
+ description: "true to enable the channel, false to disable it."
10674
+ }
10675
+ },
10676
+ required: ["channel", "enabled"]
10677
+ }
10678
+ },
10679
+ {
10680
+ name: "node9_undo_list",
10681
+ 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.",
10682
+ inputSchema: { type: "object", properties: {}, required: [] }
10683
+ },
10684
+ {
10685
+ name: "node9_undo_revert",
10686
+ 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.",
10687
+ inputSchema: {
10688
+ type: "object",
10689
+ properties: {
10690
+ hash: {
10691
+ type: "string",
10692
+ description: "The full git commit hash from node9_undo_list."
10693
+ },
10694
+ cwd: {
10695
+ type: "string",
10696
+ description: "Absolute path to the project directory. Defaults to process.cwd()."
10697
+ }
10698
+ },
10699
+ required: ["hash"]
10700
+ }
10701
+ }
10702
+ ];
10703
+ function handleStatus() {
10704
+ const config = getConfig();
10705
+ const settings = config.settings;
10706
+ const paused = checkPause();
10707
+ const daemonUp = isDaemonRunning();
10708
+ const activeShields = readActiveShields();
10709
+ const lines = [];
10710
+ lines.push(`Mode: ${settings.mode}`);
10711
+ lines.push(`Daemon: ${daemonUp ? "running" : "stopped"}`);
10712
+ lines.push(`Undo engine: ${settings.enableUndo ? "enabled" : "disabled"}`);
10713
+ if (paused.paused) {
10714
+ const until = paused.expiresAt ? new Date(paused.expiresAt).toLocaleTimeString() : "indefinitely";
10715
+ lines.push(`PAUSED until ${until} \u2014 all tool calls currently allowed`);
10716
+ } else {
10717
+ lines.push(`Pause state: not paused`);
10718
+ }
10719
+ lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
10720
+ lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
10721
+ lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
10722
+ const projectConfig = import_path27.default.join(process.cwd(), "node9.config.json");
10723
+ const globalConfig = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
10724
+ lines.push(
10725
+ `Project config (node9.config.json): ${import_fs24.default.existsSync(projectConfig) ? "present" : "not found"}`
10726
+ );
10727
+ lines.push(
10728
+ `Global config (~/.node9/config.json): ${import_fs24.default.existsSync(globalConfig) ? "present" : "not found"}`
10729
+ );
10730
+ return lines.join("\n");
10731
+ }
10732
+ function handleConfigGet() {
10733
+ const config = getConfig();
10734
+ const s = config.settings;
10735
+ const lines = [
10736
+ `mode: ${s.mode}`,
10737
+ `enableUndo: ${s.enableUndo}`,
10738
+ `flightRecorder: ${s.flightRecorder}`,
10739
+ `approvalTimeoutMs: ${s.approvalTimeoutMs}`,
10740
+ `approvers:`,
10741
+ ` native: ${s.approvers.native}`,
10742
+ ` browser: ${s.approvers.browser}`,
10743
+ ` cloud: ${s.approvers.cloud}`,
10744
+ ` terminal: ${s.approvers.terminal}`,
10745
+ `dlp.enabled: ${config.policy.dlp?.enabled !== false}`,
10746
+ `dlp.scanIgnoredTools: ${config.policy.dlp?.scanIgnoredTools !== false}`,
10747
+ `smartRules: ${config.policy.smartRules.length} active`,
10748
+ `sandboxPaths: ${config.policy.sandboxPaths.length > 0 ? config.policy.sandboxPaths.join(", ") : "none"}`
10749
+ ];
10750
+ return lines.join("\n");
10751
+ }
10752
+ function handleShieldList() {
10753
+ const all = listShields();
10754
+ const active = new Set(readActiveShields());
10755
+ if (all.length === 0) return "No shields available.";
10756
+ const lines = all.map((shield) => {
10757
+ const on = active.has(shield.name);
10758
+ const ruleCount = shield.smartRules.length;
10759
+ return `${on ? "[active]" : "[off] "} ${shield.name.padEnd(12)} \u2014 ${shield.description ?? ""} (${ruleCount} rule${ruleCount === 1 ? "" : "s"})`;
10760
+ });
10761
+ lines.unshift(`${active.size} of ${all.length} shields active:
10762
+ `);
10763
+ return lines.join("\n");
10764
+ }
10765
+ function handleShieldEnable(args) {
10766
+ const service = args.service;
10767
+ if (typeof service !== "string" || !service) {
10768
+ throw new Error("service is required");
10769
+ }
10770
+ const name = resolveShieldName(service);
10771
+ if (!name) {
10772
+ throw new Error(
10773
+ `Unknown shield: "${service}". Run node9_shield_list to see available shields.`
10774
+ );
10775
+ }
10776
+ const active = readActiveShields();
10777
+ if (active.includes(name)) {
10778
+ return `Shield "${name}" is already active.`;
10779
+ }
10780
+ writeActiveShields([...active, name]);
10781
+ const shield = getShield(name);
10782
+ return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10783
+ }
10784
+ var GLOBAL_CONFIG_PATH2 = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
10785
+ var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
10786
+ function readGlobalConfigRaw() {
10787
+ try {
10788
+ if (import_fs24.default.existsSync(GLOBAL_CONFIG_PATH2)) {
10789
+ return JSON.parse(import_fs24.default.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
10790
+ }
10791
+ } catch {
10792
+ }
10793
+ return {};
10794
+ }
10795
+ function writeGlobalConfigRaw(data) {
10796
+ const dir = import_path27.default.dirname(GLOBAL_CONFIG_PATH2);
10797
+ if (!import_fs24.default.existsSync(dir)) import_fs24.default.mkdirSync(dir, { recursive: true });
10798
+ import_fs24.default.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
10799
+ }
10800
+ function handleApproverList() {
10801
+ const config = getConfig();
10802
+ const approvers = config.settings.approvers;
10803
+ const lines = ["Approver channels:\n"];
10804
+ for (const ch of APPROVER_CHANNELS) {
10805
+ const on = approvers[ch];
10806
+ lines.push(` ${on ? "[enabled] " : "[disabled]"} ${ch}`);
10807
+ }
10808
+ const enabledCount = APPROVER_CHANNELS.filter((ch) => approvers[ch]).length;
10809
+ if (enabledCount === 0) {
10810
+ lines.push("\nWARNING: all approver channels are disabled \u2014 node9 cannot prompt for approval.");
10811
+ }
10812
+ return lines.join("\n");
10813
+ }
10814
+ function handleApproverSet(args) {
10815
+ const channel = args.channel;
10816
+ const enabled = args.enabled;
10817
+ if (!channel || !APPROVER_CHANNELS.includes(channel)) {
10818
+ throw new Error(
10819
+ `Invalid channel: "${channel}". Must be one of: ${APPROVER_CHANNELS.join(", ")}.`
10820
+ );
10821
+ }
10822
+ if (typeof enabled !== "boolean") {
10823
+ throw new Error("enabled must be a boolean (true or false).");
10824
+ }
10825
+ const raw = readGlobalConfigRaw();
10826
+ const settings = raw.settings ?? {};
10827
+ const approvers = settings.approvers ?? {};
10828
+ approvers[channel] = enabled;
10829
+ settings.approvers = approvers;
10830
+ raw.settings = settings;
10831
+ writeGlobalConfigRaw(raw);
10832
+ const currentApprovers = getConfig().settings.approvers;
10833
+ const anyEnabled = APPROVER_CHANNELS.some(
10834
+ (ch) => ch === channel ? enabled : currentApprovers[ch]
10835
+ );
10836
+ const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
10837
+ return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
10838
+ }
10839
+ function handleUndoList() {
10840
+ const history = getSnapshotHistory();
10841
+ if (history.length === 0) {
10842
+ return "No snapshots found. Node9 captures snapshots automatically before file edits.";
10843
+ }
10844
+ const lines = history.slice().reverse().map((entry, i) => {
10845
+ const date = new Date(entry.timestamp).toLocaleString();
10846
+ const files = entry.files?.length ? `${entry.files.length} file(s)` : "unknown files";
10847
+ const summary = entry.argsSummary ? ` \u2014 ${entry.argsSummary}` : "";
10848
+ return `[${i + 1}] ${entry.hash.slice(0, 7)} ${date} ${entry.tool}${summary} (${files}) cwd: ${entry.cwd}
10849
+ full hash: ${entry.hash}`;
10850
+ });
10851
+ return lines.join("\n\n");
10852
+ }
10853
+ function handleUndoRevert(args) {
10854
+ const hash = args.hash;
10855
+ if (typeof hash !== "string" || !hash) {
10856
+ throw new Error("hash is required and must be a non-empty string");
10857
+ }
10858
+ if (!/^[0-9a-f]{7,40}$/i.test(hash)) {
10859
+ throw new Error(`Invalid hash format: ${hash}`);
10860
+ }
10861
+ const cwd = typeof args.cwd === "string" && args.cwd ? args.cwd : process.cwd();
10862
+ const success = applyUndo(hash, cwd);
10863
+ if (!success) {
10864
+ throw new Error(
10865
+ `Revert failed for hash ${hash}. The snapshot may not exist for this directory, or git encountered an error.`
10866
+ );
10867
+ }
10868
+ return `Successfully reverted to snapshot ${hash.slice(0, 7)} in ${cwd}.`;
10869
+ }
10870
+ function runMcpServer() {
10871
+ const rl = import_readline4.default.createInterface({ input: process.stdin, terminal: false });
10872
+ rl.on("line", (line) => {
10873
+ let msg;
10874
+ try {
10875
+ msg = JSON.parse(line);
10876
+ } catch {
10877
+ process.stdout.write(err(null, -32700, "Parse error") + "\n");
10878
+ return;
10879
+ }
10880
+ const { method, id, params } = msg;
10881
+ if (method === "initialize") {
10882
+ process.stdout.write(
10883
+ ok(id, {
10884
+ protocolVersion: "2024-11-05",
10885
+ serverInfo: { name: "node9", version: "1.0.0" },
10886
+ capabilities: { tools: {} }
10887
+ }) + "\n"
10888
+ );
10889
+ return;
10890
+ }
10891
+ if (id === void 0 || id === null) {
10892
+ return;
10893
+ }
10894
+ if (method === "tools/list") {
10895
+ process.stdout.write(ok(id, { tools: TOOLS }) + "\n");
10896
+ return;
10897
+ }
10898
+ if (method === "tools/call") {
10899
+ const p = params ?? {};
10900
+ const toolName = p.name;
10901
+ const toolArgs = p.arguments ?? {};
10902
+ try {
10903
+ let text;
10904
+ if (toolName === "node9_status") {
10905
+ text = handleStatus();
10906
+ } else if (toolName === "node9_config_get") {
10907
+ text = handleConfigGet();
10908
+ } else if (toolName === "node9_shield_list") {
10909
+ text = handleShieldList();
10910
+ } else if (toolName === "node9_shield_enable") {
10911
+ text = handleShieldEnable(toolArgs);
10912
+ } else if (toolName === "node9_approver_list") {
10913
+ text = handleApproverList();
10914
+ } else if (toolName === "node9_approver_set") {
10915
+ text = handleApproverSet(toolArgs);
10916
+ } else if (toolName === "node9_undo_list") {
10917
+ text = handleUndoList();
10918
+ } else if (toolName === "node9_undo_revert") {
10919
+ text = handleUndoRevert(toolArgs);
10920
+ } else {
10921
+ process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
10922
+ return;
10923
+ }
10924
+ process.stdout.write(ok(id, { content: [{ type: "text", text }] }) + "\n");
10925
+ } catch (e) {
10926
+ const message = e instanceof Error ? e.message : String(e);
10927
+ process.stdout.write(
10928
+ ok(id, {
10929
+ content: [{ type: "text", text: `Error: ${message}` }],
10930
+ isError: true
10931
+ }) + "\n"
10932
+ );
10933
+ }
10934
+ return;
10935
+ }
10936
+ process.stdout.write(err(id, -32601, `Method not found: ${method}`) + "\n");
10937
+ });
10938
+ rl.on("close", () => {
10939
+ process.exit(0);
10940
+ });
10941
+ }
10942
+
10943
+ // src/cli/commands/mcp-server.ts
10944
+ function registerMcpServerCommand(program2) {
10945
+ program2.command("mcp-server").description(
10946
+ "Run the Node9 MCP server \u2014 exposes node9 tools (undo, rules, \u2026) to Claude, Cursor, and Gemini"
10947
+ ).action(() => {
10948
+ runMcpServer();
10949
+ });
10950
+ }
10951
+
10340
10952
  // src/cli/commands/trust.ts
10341
10953
  var import_chalk16 = __toESM(require("chalk"));
10342
10954
  init_trusted_hosts();
@@ -10394,20 +11006,20 @@ function registerTrustCommand(program2) {
10394
11006
 
10395
11007
  // src/cli.ts
10396
11008
  var { version } = JSON.parse(
10397
- import_fs26.default.readFileSync(import_path29.default.join(__dirname, "../package.json"), "utf-8")
11009
+ import_fs27.default.readFileSync(import_path30.default.join(__dirname, "../package.json"), "utf-8")
10398
11010
  );
10399
11011
  var program = new import_commander.Command();
10400
11012
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
10401
11013
  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
11014
  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 });
11015
+ const credPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "credentials.json");
11016
+ if (!import_fs27.default.existsSync(import_path30.default.dirname(credPath)))
11017
+ import_fs27.default.mkdirSync(import_path30.default.dirname(credPath), { recursive: true });
10406
11018
  const profileName = options.profile || "default";
10407
11019
  let existingCreds = {};
10408
11020
  try {
10409
- if (import_fs26.default.existsSync(credPath)) {
10410
- const raw = JSON.parse(import_fs26.default.readFileSync(credPath, "utf-8"));
11021
+ if (import_fs27.default.existsSync(credPath)) {
11022
+ const raw = JSON.parse(import_fs27.default.readFileSync(credPath, "utf-8"));
10411
11023
  if (raw.apiKey) {
10412
11024
  existingCreds = {
10413
11025
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -10419,13 +11031,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
10419
11031
  } catch {
10420
11032
  }
10421
11033
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
10422
- import_fs26.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
11034
+ import_fs27.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
10423
11035
  if (profileName === "default") {
10424
- const configPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "config.json");
11036
+ const configPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "config.json");
10425
11037
  let config = {};
10426
11038
  try {
10427
- if (import_fs26.default.existsSync(configPath))
10428
- config = JSON.parse(import_fs26.default.readFileSync(configPath, "utf-8"));
11039
+ if (import_fs27.default.existsSync(configPath))
11040
+ config = JSON.parse(import_fs27.default.readFileSync(configPath, "utf-8"));
10429
11041
  } catch {
10430
11042
  }
10431
11043
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -10440,9 +11052,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
10440
11052
  approvers.cloud = false;
10441
11053
  }
10442
11054
  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 });
11055
+ if (!import_fs27.default.existsSync(import_path30.default.dirname(configPath)))
11056
+ import_fs27.default.mkdirSync(import_path30.default.dirname(configPath), { recursive: true });
11057
+ import_fs27.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
10446
11058
  }
10447
11059
  if (options.profile && profileName !== "default") {
10448
11060
  console.log(import_chalk18.default.green(`\u2705 Profile "${profileName}" saved`));
@@ -10502,8 +11114,8 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
10502
11114
  `));
10503
11115
  try {
10504
11116
  fn();
10505
- } catch (err) {
10506
- console.error(import_chalk18.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
11117
+ } catch (err2) {
11118
+ console.error(import_chalk18.default.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
10507
11119
  process.exit(1);
10508
11120
  }
10509
11121
  console.log(import_chalk18.default.gray("\n Restart the agent for changes to take effect."));
@@ -10526,25 +11138,25 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
10526
11138
  ]) {
10527
11139
  try {
10528
11140
  fn();
10529
- } catch (err) {
11141
+ } catch (err2) {
10530
11142
  teardownFailed = true;
10531
11143
  console.error(
10532
11144
  import_chalk18.default.red(
10533
- ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
11145
+ ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
10534
11146
  )
10535
11147
  );
10536
11148
  }
10537
11149
  }
10538
11150
  if (options.purge) {
10539
- const node9Dir = import_path29.default.join(import_os22.default.homedir(), ".node9");
10540
- if (import_fs26.default.existsSync(node9Dir)) {
11151
+ const node9Dir = import_path30.default.join(import_os23.default.homedir(), ".node9");
11152
+ if (import_fs27.default.existsSync(node9Dir)) {
10541
11153
  const confirmed = await (0, import_prompts2.confirm)({
10542
11154
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
10543
11155
  default: false
10544
11156
  });
10545
11157
  if (confirmed) {
10546
- import_fs26.default.rmSync(node9Dir, { recursive: true });
10547
- if (import_fs26.default.existsSync(node9Dir)) {
11158
+ import_fs27.default.rmSync(node9Dir, { recursive: true });
11159
+ if (import_fs27.default.existsSync(node9Dir)) {
10548
11160
  console.error(
10549
11161
  import_chalk18.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
10550
11162
  );
@@ -10651,13 +11263,14 @@ program.command("tail").description("Stream live agent activity to the terminal"
10651
11263
  const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
10652
11264
  try {
10653
11265
  await startTail2(options);
10654
- } catch (err) {
10655
- console.error(import_chalk18.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
11266
+ } catch (err2) {
11267
+ console.error(import_chalk18.default.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
10656
11268
  process.exit(1);
10657
11269
  }
10658
11270
  });
10659
11271
  registerWatchCommand(program);
10660
11272
  registerMcpGatewayCommand(program);
11273
+ registerMcpServerCommand(program);
10661
11274
  registerCheckCommand(program);
10662
11275
  registerLogCommand(program);
10663
11276
  program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
@@ -10760,9 +11373,9 @@ if (process.argv[2] !== "daemon") {
10760
11373
  const isCheckHook = process.argv[2] === "check";
10761
11374
  if (isCheckHook) {
10762
11375
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
10763
- const logPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
11376
+ const logPath = import_path30.default.join(import_os23.default.homedir(), ".node9", "hook-debug.log");
10764
11377
  const msg = reason instanceof Error ? reason.message : String(reason);
10765
- import_fs26.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
11378
+ import_fs27.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
10766
11379
  `);
10767
11380
  }
10768
11381
  process.exit(0);