@node9/proxy 1.6.0 → 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/README.md CHANGED
@@ -32,10 +32,6 @@ While others try to _guess_ if a prompt is malicious (Semantic Security), Node9
32
32
 
33
33
  **AIs are literal.** When you ask an agent to "Fix my disk space," it might decide to run `docker system prune -af`.
34
34
 
35
- <p align="center">
36
- <img src="https://github.com/user-attachments/assets/afae9caa-0605-4cac-929a-c14198383169" width="100%">
37
- </p>
38
-
39
35
  **With Node9, the interaction looks like this:**
40
36
 
41
37
  1. **🤖 AI attempts a "Nuke":** `Bash("docker system prune -af --volumes")`
package/dist/cli.js CHANGED
@@ -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
@@ -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,
@@ -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
  }
@@ -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
  }
@@ -5845,10 +5876,13 @@ function startActivitySocket() {
5845
5876
  sessionCounters.incrementBlocked();
5846
5877
  sessionCounters.recordBlockedTool(data.tool);
5847
5878
  }
5879
+ const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
5880
+ if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
5848
5881
  broadcast("activity-result", {
5849
5882
  id: data.id,
5850
5883
  status: data.status,
5851
- label: data.label
5884
+ label: data.label,
5885
+ costEstimate
5852
5886
  });
5853
5887
  }
5854
5888
  } catch {
@@ -5865,7 +5899,7 @@ function startActivitySocket() {
5865
5899
  }
5866
5900
  });
5867
5901
  }
5868
- 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;
5869
5903
  var init_state2 = __esm({
5870
5904
  "src/daemon/state.ts"() {
5871
5905
  "use strict";
@@ -5909,6 +5943,9 @@ var init_state2 = __esm({
5909
5943
  ACTIVITY_RING_SIZE = 100;
5910
5944
  activityRing = [];
5911
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;
5912
5949
  WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5913
5950
  "write",
5914
5951
  "write_file",
@@ -6366,7 +6403,8 @@ data: ${JSON.stringify(item.data)}
6366
6403
  allowed: counters.allowed,
6367
6404
  blocked: counters.blocked,
6368
6405
  dlpHits: counters.dlpHits,
6369
- wouldBlock: counters.wouldBlock
6406
+ wouldBlock: counters.wouldBlock,
6407
+ estimatedCost: counters.estimatedCost
6370
6408
  },
6371
6409
  taintedCount: taintStore.list().length,
6372
6410
  lastRuleHit: counters.lastRuleHit,
@@ -6498,6 +6536,7 @@ data: ${JSON.stringify(item.data)}
6498
6536
  }
6499
6537
  if (req.method === "POST" && pathname === "/events/clear") {
6500
6538
  activityRing.length = 0;
6539
+ sessionCounters.reset();
6501
6540
  res.writeHead(200, { "Content-Type": "application/json" });
6502
6541
  return res.end(JSON.stringify({ ok: true }));
6503
6542
  }
@@ -6811,7 +6850,7 @@ function formatBase(activity) {
6811
6850
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
6812
6851
  const icon = getIcon(activity.tool);
6813
6852
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6814
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
6853
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os21.default.homedir(), "~");
6815
6854
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6816
6855
  return `${import_chalk17.default.gray(time)} ${icon} ${import_chalk17.default.white.bold(toolName)} ${import_chalk17.default.dim(argsPreview)}`;
6817
6856
  }
@@ -6825,11 +6864,13 @@ function renderResult(activity, result) {
6825
6864
  } else {
6826
6865
  status = import_chalk17.default.red("\u2717 BLOCK");
6827
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"}`);
6828
6869
  if (process.stdout.isTTY) {
6829
6870
  import_readline5.default.clearLine(process.stdout, 0);
6830
6871
  import_readline5.default.cursorTo(process.stdout, 0);
6831
6872
  }
6832
- console.log(`${base} ${status}`);
6873
+ console.log(`${base} ${status}${costSuffix}`);
6833
6874
  }
6834
6875
  function renderPending(activity) {
6835
6876
  if (!process.stdout.isTTY) return;
@@ -6958,6 +6999,39 @@ function buildRecoveryCardLines(req) {
6958
6999
  ``
6959
7000
  ];
6960
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
+ }
6961
7035
  async function startTail(options = {}) {
6962
7036
  const port = await ensureDaemon();
6963
7037
  if (options.clear) {
@@ -7004,6 +7078,44 @@ async function startTail(options = {}) {
7004
7078
  const localAllowCounts = /* @__PURE__ */ new Map();
7005
7079
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
7006
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
+ }
7007
7119
  function clearCard() {
7008
7120
  if (cardLineCount > 0) {
7009
7121
  import_readline5.default.moveCursor(process.stdout, 0, -cardLineCount);
@@ -7022,10 +7134,12 @@ async function startTail(options = {}) {
7022
7134
  }
7023
7135
  function showNextCard() {
7024
7136
  if (cardActive || approvalQueue.length === 0 || !canApprove) return;
7137
+ exitIdleMode();
7025
7138
  try {
7026
7139
  process.stdin.setRawMode(true);
7027
7140
  } catch {
7028
7141
  cardActive = false;
7142
+ enterIdleMode();
7029
7143
  return;
7030
7144
  }
7031
7145
  cardActive = true;
@@ -7037,12 +7151,8 @@ async function startTail(options = {}) {
7037
7151
  const handler = onKeypress;
7038
7152
  onKeypress = null;
7039
7153
  if (handler) process.stdin.removeListener("keypress", handler);
7040
- try {
7041
- process.stdin.setRawMode(false);
7042
- } catch {
7043
- }
7044
- process.stdin.pause();
7045
7154
  cancelActiveCard = null;
7155
+ enterIdleMode();
7046
7156
  };
7047
7157
  const settle = (action) => {
7048
7158
  if (settled) return;
@@ -7169,18 +7279,16 @@ async function startTail(options = {}) {
7169
7279
  console.log(import_chalk17.default.cyan.bold(`
7170
7280
  \u{1F6F0}\uFE0F Node9 tail `) + import_chalk17.default.dim(`\u2192 ${dashboardUrl}`));
7171
7281
  if (canApprove) {
7172
- console.log(
7173
- import_chalk17.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
7174
- );
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`));
7175
7284
  }
7176
7285
  if (options.history) {
7177
- 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"));
7178
7287
  } else {
7179
- console.log(
7180
- import_chalk17.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
7181
- );
7288
+ console.log(import_chalk17.default.dim("Showing live events only. Use --history to include past.\n"));
7182
7289
  }
7183
7290
  process.on("SIGINT", () => {
7291
+ exitIdleMode();
7184
7292
  clearCard();
7185
7293
  process.stdout.write(SHOW_CURSOR);
7186
7294
  if (process.stdout.isTTY) {
@@ -7196,6 +7304,7 @@ async function startTail(options = {}) {
7196
7304
  console.error(import_chalk17.default.red(`Failed to connect: HTTP ${res.statusCode}`));
7197
7305
  process.exit(1);
7198
7306
  }
7307
+ if (canApprove) enterIdleMode();
7199
7308
  let currentEvent = "";
7200
7309
  let currentData = "";
7201
7310
  res.on("error", () => {
@@ -7589,6 +7698,11 @@ function renderSecurityLine(status) {
7589
7698
  parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7590
7699
  }
7591
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
+ }
7592
7706
  if (status.taintedCount > 0) {
7593
7707
  parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7594
7708
  }
@@ -8621,6 +8735,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8621
8735
  if (filesRes.status === 0) {
8622
8736
  capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8623
8737
  }
8738
+ if (capturedFiles.length === 0) {
8739
+ return prevEntry.hash;
8740
+ }
8624
8741
  const diffRes = (0, import_child_process8.spawnSync)("git", ["diff", prevEntry.hash, commitHash], {
8625
8742
  env: shadowEnv,
8626
8743
  timeout: GIT_TIMEOUT
@@ -9817,25 +9934,79 @@ var import_chalk11 = __toESM(require("chalk"));
9817
9934
  var import_fs23 = __toESM(require("fs"));
9818
9935
  var import_path25 = __toESM(require("path"));
9819
9936
  var import_os19 = __toESM(require("os"));
9937
+ var import_https = __toESM(require("https"));
9820
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
+ }
9821
9968
  function registerInitCommand(program2) {
9822
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) => {
9823
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
+ }
9824
9984
  const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
9825
9985
  if (import_fs23.default.existsSync(configPath) && !options.force) {
9826
- 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
+ }
9827
10000
  } else {
9828
- const requestedMode = options.mode.toLowerCase();
9829
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
9830
10001
  const configToSave = {
9831
10002
  ...DEFAULT_CONFIG,
9832
- settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
10003
+ settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
9833
10004
  };
9834
10005
  const dir = import_path25.default.dirname(configPath);
9835
10006
  if (!import_fs23.default.existsSync(dir)) import_fs23.default.mkdirSync(dir, { recursive: true });
9836
- import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
10007
+ import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
9837
10008
  console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
9838
- console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
10009
+ console.log(import_chalk11.default.gray(` Mode: ${chosenMode}`));
9839
10010
  }
9840
10011
  if (options.skipSetup) return;
9841
10012
  console.log("");
@@ -9862,14 +10033,20 @@ function registerInitCommand(program2) {
9862
10033
  else if (agent === "cursor") await setupCursor();
9863
10034
  console.log("");
9864
10035
  }
9865
- if (detected.claude) {
9866
- setupHud();
9867
- console.log(import_chalk11.default.green("\u2705 node9 HUD added to Claude Code statusline"));
9868
- 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);
9869
10043
  console.log("");
9870
10044
  }
9871
10045
  console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
9872
- 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"));
9873
10050
  });
9874
10051
  }
9875
10052
 
@@ -10475,6 +10652,30 @@ var TOOLS = [
10475
10652
  required: ["service"]
10476
10653
  }
10477
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
+ },
10478
10679
  {
10479
10680
  name: "node9_undo_list",
10480
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.",
@@ -10580,6 +10781,61 @@ function handleShieldEnable(args) {
10580
10781
  const shield = getShield(name);
10581
10782
  return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10582
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
+ }
10583
10839
  function handleUndoList() {
10584
10840
  const history = getSnapshotHistory();
10585
10841
  if (history.length === 0) {
@@ -10653,6 +10909,10 @@ function runMcpServer() {
10653
10909
  text = handleShieldList();
10654
10910
  } else if (toolName === "node9_shield_enable") {
10655
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);
10656
10916
  } else if (toolName === "node9_undo_list") {
10657
10917
  text = handleUndoList();
10658
10918
  } else if (toolName === "node9_undo_revert") {
package/dist/cli.mjs CHANGED
@@ -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
@@ -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,
@@ -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
  }
@@ -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
  }
@@ -5828,10 +5859,13 @@ function startActivitySocket() {
5828
5859
  sessionCounters.incrementBlocked();
5829
5860
  sessionCounters.recordBlockedTool(data.tool);
5830
5861
  }
5862
+ const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
5863
+ if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
5831
5864
  broadcast("activity-result", {
5832
5865
  id: data.id,
5833
5866
  status: data.status,
5834
- label: data.label
5867
+ label: data.label,
5868
+ costEstimate
5835
5869
  });
5836
5870
  }
5837
5871
  } catch {
@@ -5848,7 +5882,7 @@ function startActivitySocket() {
5848
5882
  }
5849
5883
  });
5850
5884
  }
5851
- 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;
5852
5886
  var init_state2 = __esm({
5853
5887
  "src/daemon/state.ts"() {
5854
5888
  "use strict";
@@ -5886,6 +5920,9 @@ var init_state2 = __esm({
5886
5920
  ACTIVITY_RING_SIZE = 100;
5887
5921
  activityRing = [];
5888
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;
5889
5926
  WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
5890
5927
  "write",
5891
5928
  "write_file",
@@ -6349,7 +6386,8 @@ data: ${JSON.stringify(item.data)}
6349
6386
  allowed: counters.allowed,
6350
6387
  blocked: counters.blocked,
6351
6388
  dlpHits: counters.dlpHits,
6352
- wouldBlock: counters.wouldBlock
6389
+ wouldBlock: counters.wouldBlock,
6390
+ estimatedCost: counters.estimatedCost
6353
6391
  },
6354
6392
  taintedCount: taintStore.list().length,
6355
6393
  lastRuleHit: counters.lastRuleHit,
@@ -6481,6 +6519,7 @@ data: ${JSON.stringify(item.data)}
6481
6519
  }
6482
6520
  if (req.method === "POST" && pathname === "/events/clear") {
6483
6521
  activityRing.length = 0;
6522
+ sessionCounters.reset();
6484
6523
  res.writeHead(200, { "Content-Type": "application/json" });
6485
6524
  return res.end(JSON.stringify({ ok: true }));
6486
6525
  }
@@ -6793,7 +6832,7 @@ function formatBase(activity) {
6793
6832
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
6794
6833
  const icon = getIcon(activity.tool);
6795
6834
  const toolName = activity.tool.slice(0, 16).padEnd(16);
6796
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
6835
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os21.homedir(), "~");
6797
6836
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
6798
6837
  return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
6799
6838
  }
@@ -6807,11 +6846,13 @@ function renderResult(activity, result) {
6807
6846
  } else {
6808
6847
  status = chalk17.red("\u2717 BLOCK");
6809
6848
  }
6849
+ const cost = result.costEstimate ?? activity.costEstimate;
6850
+ const costSuffix = cost == null ? "" : chalk17.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
6810
6851
  if (process.stdout.isTTY) {
6811
6852
  readline5.clearLine(process.stdout, 0);
6812
6853
  readline5.cursorTo(process.stdout, 0);
6813
6854
  }
6814
- console.log(`${base} ${status}`);
6855
+ console.log(`${base} ${status}${costSuffix}`);
6815
6856
  }
6816
6857
  function renderPending(activity) {
6817
6858
  if (!process.stdout.isTTY) return;
@@ -6940,6 +6981,39 @@ function buildRecoveryCardLines(req) {
6940
6981
  ``
6941
6982
  ];
6942
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
+ }
6943
7017
  async function startTail(options = {}) {
6944
7018
  const port = await ensureDaemon();
6945
7019
  if (options.clear) {
@@ -6986,6 +7060,44 @@ async function startTail(options = {}) {
6986
7060
  const localAllowCounts = /* @__PURE__ */ new Map();
6987
7061
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
6988
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
+ }
6989
7101
  function clearCard() {
6990
7102
  if (cardLineCount > 0) {
6991
7103
  readline5.moveCursor(process.stdout, 0, -cardLineCount);
@@ -7004,10 +7116,12 @@ async function startTail(options = {}) {
7004
7116
  }
7005
7117
  function showNextCard() {
7006
7118
  if (cardActive || approvalQueue.length === 0 || !canApprove) return;
7119
+ exitIdleMode();
7007
7120
  try {
7008
7121
  process.stdin.setRawMode(true);
7009
7122
  } catch {
7010
7123
  cardActive = false;
7124
+ enterIdleMode();
7011
7125
  return;
7012
7126
  }
7013
7127
  cardActive = true;
@@ -7019,12 +7133,8 @@ async function startTail(options = {}) {
7019
7133
  const handler = onKeypress;
7020
7134
  onKeypress = null;
7021
7135
  if (handler) process.stdin.removeListener("keypress", handler);
7022
- try {
7023
- process.stdin.setRawMode(false);
7024
- } catch {
7025
- }
7026
- process.stdin.pause();
7027
7136
  cancelActiveCard = null;
7137
+ enterIdleMode();
7028
7138
  };
7029
7139
  const settle = (action) => {
7030
7140
  if (settled) return;
@@ -7151,18 +7261,16 @@ async function startTail(options = {}) {
7151
7261
  console.log(chalk17.cyan.bold(`
7152
7262
  \u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
7153
7263
  if (canApprove) {
7154
- console.log(
7155
- chalk17.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
7156
- );
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`));
7157
7266
  }
7158
7267
  if (options.history) {
7159
- console.log(chalk17.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
7268
+ console.log(chalk17.dim("Showing history + live events.\n"));
7160
7269
  } else {
7161
- console.log(
7162
- chalk17.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
7163
- );
7270
+ console.log(chalk17.dim("Showing live events only. Use --history to include past.\n"));
7164
7271
  }
7165
7272
  process.on("SIGINT", () => {
7273
+ exitIdleMode();
7166
7274
  clearCard();
7167
7275
  process.stdout.write(SHOW_CURSOR);
7168
7276
  if (process.stdout.isTTY) {
@@ -7178,6 +7286,7 @@ async function startTail(options = {}) {
7178
7286
  console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
7179
7287
  process.exit(1);
7180
7288
  }
7289
+ if (canApprove) enterIdleMode();
7181
7290
  let currentEvent = "";
7182
7291
  let currentData = "";
7183
7292
  res.on("error", () => {
@@ -7568,6 +7677,11 @@ function renderSecurityLine(status) {
7568
7677
  parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
7569
7678
  }
7570
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
+ }
7571
7685
  if (status.taintedCount > 0) {
7572
7686
  parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
7573
7687
  }
@@ -8596,6 +8710,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
8596
8710
  if (filesRes.status === 0) {
8597
8711
  capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
8598
8712
  }
8713
+ if (capturedFiles.length === 0) {
8714
+ return prevEntry.hash;
8715
+ }
8599
8716
  const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
8600
8717
  env: shadowEnv,
8601
8718
  timeout: GIT_TIMEOUT
@@ -9793,24 +9910,78 @@ import chalk11 from "chalk";
9793
9910
  import fs23 from "fs";
9794
9911
  import path25 from "path";
9795
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
+ }
9796
9943
  function registerInitCommand(program2) {
9797
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) => {
9798
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
+ }
9799
9959
  const configPath = path25.join(os19.homedir(), ".node9", "config.json");
9800
9960
  if (fs23.existsSync(configPath) && !options.force) {
9801
- 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
+ }
9802
9975
  } else {
9803
- const requestedMode = options.mode.toLowerCase();
9804
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
9805
9976
  const configToSave = {
9806
9977
  ...DEFAULT_CONFIG,
9807
- settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
9978
+ settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
9808
9979
  };
9809
9980
  const dir = path25.dirname(configPath);
9810
9981
  if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
9811
- fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
9982
+ fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
9812
9983
  console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
9813
- console.log(chalk11.gray(` Mode: ${safeMode}`));
9984
+ console.log(chalk11.gray(` Mode: ${chosenMode}`));
9814
9985
  }
9815
9986
  if (options.skipSetup) return;
9816
9987
  console.log("");
@@ -9837,14 +10008,20 @@ function registerInitCommand(program2) {
9837
10008
  else if (agent === "cursor") await setupCursor();
9838
10009
  console.log("");
9839
10010
  }
9840
- if (detected.claude) {
9841
- setupHud();
9842
- console.log(chalk11.green("\u2705 node9 HUD added to Claude Code statusline"));
9843
- 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);
9844
10018
  console.log("");
9845
10019
  }
9846
10020
  console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
9847
- 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"));
9848
10025
  });
9849
10026
  }
9850
10027
 
@@ -10450,6 +10627,30 @@ var TOOLS = [
10450
10627
  required: ["service"]
10451
10628
  }
10452
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
+ },
10453
10654
  {
10454
10655
  name: "node9_undo_list",
10455
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.",
@@ -10555,6 +10756,61 @@ function handleShieldEnable(args) {
10555
10756
  const shield = getShield(name);
10556
10757
  return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
10557
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
+ }
10558
10814
  function handleUndoList() {
10559
10815
  const history = getSnapshotHistory();
10560
10816
  if (history.length === 0) {
@@ -10628,6 +10884,10 @@ function runMcpServer() {
10628
10884
  text = handleShieldList();
10629
10885
  } else if (toolName === "node9_shield_enable") {
10630
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);
10631
10891
  } else if (toolName === "node9_undo_list") {
10632
10892
  text = handleUndoList();
10633
10893
  } else if (toolName === "node9_undo_revert") {
package/dist/index.js CHANGED
@@ -517,7 +517,7 @@ var DANGEROUS_WORDS = [
517
517
  var DEFAULT_CONFIG = {
518
518
  version: "1.0",
519
519
  settings: {
520
- mode: "audit",
520
+ mode: "standard",
521
521
  autoStartDaemon: true,
522
522
  enableUndo: true,
523
523
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
@@ -2721,6 +2721,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
2721
2721
  await notifyActivity({
2722
2722
  id: actId,
2723
2723
  tool: toolName,
2724
+ args,
2724
2725
  ts: actTs,
2725
2726
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
2726
2727
  label: result.blockedByLabel,
package/dist/index.mjs CHANGED
@@ -487,7 +487,7 @@ var DANGEROUS_WORDS = [
487
487
  var DEFAULT_CONFIG = {
488
488
  version: "1.0",
489
489
  settings: {
490
- mode: "audit",
490
+ mode: "standard",
491
491
  autoStartDaemon: true,
492
492
  enableUndo: true,
493
493
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
@@ -2691,6 +2691,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
2691
2691
  await notifyActivity({
2692
2692
  id: actId,
2693
2693
  tool: toolName,
2694
+ args,
2694
2695
  ts: actTs,
2695
2696
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
2696
2697
  label: result.blockedByLabel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",