@node9/proxy 1.4.0 → 1.5.1

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
@@ -30,7 +30,52 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  mod
31
31
  ));
32
32
 
33
+ // src/audit/hasher.ts
34
+ function canonicalise(value) {
35
+ return _canonicalise(value, /* @__PURE__ */ new WeakSet());
36
+ }
37
+ function _canonicalise(value, seen) {
38
+ if (value === null || typeof value !== "object") return value;
39
+ if (value instanceof Date) return value.toISOString();
40
+ if (value instanceof RegExp) return value.toString();
41
+ if (Buffer.isBuffer(value)) return value.toString("base64");
42
+ if (seen.has(value)) return "[Circular]";
43
+ seen.add(value);
44
+ let result;
45
+ if (Array.isArray(value)) {
46
+ result = value.map((v) => _canonicalise(v, seen));
47
+ } else {
48
+ const obj = value;
49
+ result = Object.fromEntries(
50
+ Object.keys(obj).sort().map((k) => [k, _canonicalise(obj[k], seen)])
51
+ );
52
+ }
53
+ seen.delete(value);
54
+ return result;
55
+ }
56
+ function hashArgs(args) {
57
+ const canonical = JSON.stringify(canonicalise(args) ?? null);
58
+ return (0, import_crypto.createHash)("sha256").update(canonical).digest("hex").slice(0, 32);
59
+ }
60
+ var import_crypto;
61
+ var init_hasher = __esm({
62
+ "src/audit/hasher.ts"() {
63
+ "use strict";
64
+ import_crypto = require("crypto");
65
+ }
66
+ });
67
+
33
68
  // src/audit/index.ts
69
+ var audit_exports = {};
70
+ __export(audit_exports, {
71
+ HOOK_DEBUG_LOG: () => HOOK_DEBUG_LOG,
72
+ LOCAL_AUDIT_LOG: () => LOCAL_AUDIT_LOG,
73
+ appendConfigAudit: () => appendConfigAudit,
74
+ appendHookDebug: () => appendHookDebug,
75
+ appendLocalAudit: () => appendLocalAudit,
76
+ appendToLog: () => appendToLog,
77
+ redactSecrets: () => redactSecrets
78
+ });
34
79
  function redactSecrets(text) {
35
80
  if (!text) return text;
36
81
  let redacted = text;
@@ -52,24 +97,24 @@ function appendToLog(logPath, entry) {
52
97
  } catch {
53
98
  }
54
99
  }
55
- function appendHookDebug(toolName, args, meta) {
56
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
100
+ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
101
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
57
102
  appendToLog(HOOK_DEBUG_LOG, {
58
103
  ts: (/* @__PURE__ */ new Date()).toISOString(),
59
104
  tool: toolName,
60
- args: safeArgs,
105
+ ...argsField,
61
106
  agent: meta?.agent,
62
107
  mcpServer: meta?.mcpServer,
63
108
  hostname: import_os.default.hostname(),
64
109
  cwd: process.cwd()
65
110
  });
66
111
  }
67
- function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
68
- const safeArgs = args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {};
112
+ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
113
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
69
114
  appendToLog(LOCAL_AUDIT_LOG, {
70
115
  ts: (/* @__PURE__ */ new Date()).toISOString(),
71
116
  tool: toolName,
72
- args: safeArgs,
117
+ ...argsField,
73
118
  decision,
74
119
  checkedBy,
75
120
  agent: meta?.agent,
@@ -91,6 +136,7 @@ var init_audit = __esm({
91
136
  import_fs = __toESM(require("fs"));
92
137
  import_path = __toESM(require("path"));
93
138
  import_os = __toESM(require("os"));
139
+ init_hasher();
94
140
  LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
95
141
  HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
96
142
  }
@@ -114,8 +160,8 @@ function sanitizeConfig(raw) {
114
160
  }
115
161
  }
116
162
  const lines = result.error.issues.map((issue) => {
117
- const path25 = issue.path.length > 0 ? issue.path.join(".") : "root";
118
- return ` \u2022 ${path25}: ${issue.message}`;
163
+ const path28 = issue.path.length > 0 ? issue.path.join(".") : "root";
164
+ return ` \u2022 ${path28}: ${issue.message}`;
119
165
  });
120
166
  return {
121
167
  sanitized,
@@ -188,7 +234,8 @@ var init_config_schema = __esm({
188
234
  environment: import_zod.z.string().optional(),
189
235
  slackEnabled: import_zod.z.boolean().optional(),
190
236
  enableTrustSessions: import_zod.z.boolean().optional(),
191
- allowGlobalPause: import_zod.z.boolean().optional()
237
+ allowGlobalPause: import_zod.z.boolean().optional(),
238
+ auditHashArgs: import_zod.z.boolean().optional()
192
239
  }).optional(),
193
240
  policy: import_zod.z.object({
194
241
  sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
@@ -269,7 +316,7 @@ function readShieldsFile() {
269
316
  }
270
317
  function writeShieldsFile(data) {
271
318
  import_fs2.default.mkdirSync(import_path2.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
272
- const tmp = `${SHIELDS_STATE_FILE}.${import_crypto.default.randomBytes(6).toString("hex")}.tmp`;
319
+ const tmp = `${SHIELDS_STATE_FILE}.${import_crypto2.default.randomBytes(6).toString("hex")}.tmp`;
273
320
  const toWrite = { active: data.active };
274
321
  if (data.overrides && Object.keys(data.overrides).length > 0) toWrite.overrides = data.overrides;
275
322
  import_fs2.default.writeFileSync(tmp, JSON.stringify(toWrite, null, 2), { mode: 384 });
@@ -318,14 +365,14 @@ function resolveShieldRule(shieldName, identifier) {
318
365
  }
319
366
  return null;
320
367
  }
321
- var import_fs2, import_path2, import_os2, import_crypto, SHIELDS, SHIELDS_STATE_FILE;
368
+ var import_fs2, import_path2, import_os2, import_crypto2, SHIELDS, SHIELDS_STATE_FILE;
322
369
  var init_shields = __esm({
323
370
  "src/shields.ts"() {
324
371
  "use strict";
325
372
  import_fs2 = __toESM(require("fs"));
326
373
  import_path2 = __toESM(require("path"));
327
374
  import_os2 = __toESM(require("os"));
328
- import_crypto = __toESM(require("crypto"));
375
+ import_crypto2 = __toESM(require("crypto"));
329
376
  SHIELDS = {
330
377
  postgres: {
331
378
  name: "postgres",
@@ -746,6 +793,7 @@ var init_config = __esm({
746
793
  approvalTimeoutMs: 12e4,
747
794
  // 120-second auto-deny timeout
748
795
  flightRecorder: true,
796
+ auditHashArgs: true,
749
797
  approvers: { native: true, browser: true, cloud: false, terminal: true }
750
798
  },
751
799
  policy: {
@@ -1623,17 +1671,44 @@ function readTrustedHosts() {
1623
1671
  return [];
1624
1672
  }
1625
1673
  }
1674
+ function getFileMtime() {
1675
+ try {
1676
+ return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
1677
+ } catch {
1678
+ return 0;
1679
+ }
1680
+ }
1681
+ function getCachedHosts() {
1682
+ const now = Date.now();
1683
+ if (_cache && now < _cache.expiry) {
1684
+ const mtime = getFileMtime();
1685
+ if (mtime === _cache.mtime) return _cache.hosts;
1686
+ }
1687
+ const hosts = readTrustedHosts();
1688
+ _cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
1689
+ return hosts;
1690
+ }
1626
1691
  function writeTrustedHosts(hosts) {
1627
1692
  const filePath = getTrustedHostsPath();
1628
1693
  import_fs6.default.mkdirSync(import_path7.default.dirname(filePath), { recursive: true });
1629
1694
  const tmp = filePath + ".node9-tmp";
1630
- import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2));
1695
+ import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
1631
1696
  import_fs6.default.renameSync(tmp, filePath);
1697
+ _cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
1632
1698
  }
1633
1699
  function addTrustedHost(host) {
1700
+ const normalized = normalizeHost(host);
1701
+ if (normalized.startsWith("*.")) {
1702
+ const base = normalized.slice(2);
1703
+ if (!base.includes(".")) {
1704
+ throw new Error(
1705
+ `Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
1706
+ );
1707
+ }
1708
+ }
1634
1709
  const hosts = readTrustedHosts();
1635
- if (hosts.some((h) => h.host === host)) return;
1636
- hosts.push({ host, addedAt: Date.now(), addedBy: "user" });
1710
+ if (hosts.some((h) => h.host === normalized)) return;
1711
+ hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
1637
1712
  writeTrustedHosts(hosts);
1638
1713
  }
1639
1714
  function removeTrustedHost(host) {
@@ -1648,22 +1723,24 @@ function normalizeHost(raw) {
1648
1723
  }
1649
1724
  function isTrustedHost(host) {
1650
1725
  const normalized = normalizeHost(host);
1651
- return readTrustedHosts().some((entry) => {
1726
+ return getCachedHosts().some((entry) => {
1652
1727
  const entryHost = entry.host.toLowerCase();
1653
1728
  if (entryHost.startsWith("*.")) {
1654
1729
  const domain = entryHost.slice(2);
1655
- return normalized === domain || normalized.endsWith("." + domain);
1730
+ return normalized.endsWith("." + domain);
1656
1731
  }
1657
1732
  return normalized === entryHost;
1658
1733
  });
1659
1734
  }
1660
- var import_fs6, import_path7, import_os5;
1735
+ var import_fs6, import_path7, import_os5, _cache, CACHE_TTL_MS;
1661
1736
  var init_trusted_hosts = __esm({
1662
1737
  "src/auth/trusted-hosts.ts"() {
1663
1738
  "use strict";
1664
1739
  import_fs6 = __toESM(require("fs"));
1665
1740
  import_path7 = __toESM(require("path"));
1666
1741
  import_os5 = __toESM(require("os"));
1742
+ _cache = null;
1743
+ CACHE_TTL_MS = 5e3;
1667
1744
  }
1668
1745
  });
1669
1746
 
@@ -1681,9 +1758,9 @@ function matchesPattern(text, patterns) {
1681
1758
  const withoutDotSlash = text.replace(/^\.\//, "");
1682
1759
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1683
1760
  }
1684
- function getNestedValue(obj, path25) {
1761
+ function getNestedValue(obj, path28) {
1685
1762
  if (!obj || typeof obj !== "object") return null;
1686
- return path25.split(".").reduce((prev, curr) => prev?.[curr], obj);
1763
+ return path28.split(".").reduce((prev, curr) => prev?.[curr], obj);
1687
1764
  }
1688
1765
  function shouldSnapshot(toolName, args, config) {
1689
1766
  if (!config.settings.enableUndo) return false;
@@ -1869,7 +1946,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1869
1946
  };
1870
1947
  }
1871
1948
  if (allTrusted) {
1872
- return { decision: "allow" };
1949
+ return {
1950
+ decision: "allow",
1951
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1952
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1953
+ tier: 3
1954
+ };
1873
1955
  }
1874
1956
  return {
1875
1957
  decision: "review",
@@ -2411,8 +2493,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2411
2493
  signal: ctrl.signal
2412
2494
  });
2413
2495
  if (!res.ok) throw new Error("Daemon fail");
2414
- const { id } = await res.json();
2415
- return id;
2496
+ const { id, allowCount } = await res.json();
2497
+ return { id, allowCount: allowCount ?? 1 };
2416
2498
  } finally {
2417
2499
  clearTimeout(timer);
2418
2500
  }
@@ -2451,15 +2533,67 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
2451
2533
  signal: AbortSignal.timeout(3e3)
2452
2534
  });
2453
2535
  if (!res.ok) throw new Error("Daemon unreachable");
2454
- const { id } = await res.json();
2455
- return id;
2536
+ const { id, allowCount } = await res.json();
2537
+ return { id, allowCount: allowCount ?? 1 };
2538
+ }
2539
+ async function notifyTaint(filePath, source) {
2540
+ if (!isDaemonRunning()) return;
2541
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2542
+ try {
2543
+ await fetch(`${base}/taint`, {
2544
+ method: "POST",
2545
+ headers: { "Content-Type": "application/json" },
2546
+ body: JSON.stringify({ path: filePath, source }),
2547
+ signal: AbortSignal.timeout(1e3)
2548
+ });
2549
+ } catch {
2550
+ }
2551
+ }
2552
+ async function notifyTaintPropagate(src, dest, clearSource = false) {
2553
+ if (!isDaemonRunning()) return;
2554
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2555
+ try {
2556
+ await fetch(`${base}/taint/propagate`, {
2557
+ method: "POST",
2558
+ headers: { "Content-Type": "application/json" },
2559
+ body: JSON.stringify({ src, dest, clearSource }),
2560
+ signal: AbortSignal.timeout(1e3)
2561
+ });
2562
+ } catch {
2563
+ }
2456
2564
  }
2457
- async function resolveViaDaemon(id, decision, internalToken) {
2565
+ async function checkTaint(paths) {
2566
+ if (paths.length === 0) return { tainted: false };
2567
+ if (!isDaemonRunning()) return { tainted: false, daemonUnavailable: true };
2568
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2569
+ try {
2570
+ const res = await fetch(`${base}/taint/check`, {
2571
+ method: "POST",
2572
+ headers: { "Content-Type": "application/json" },
2573
+ body: JSON.stringify({ paths }),
2574
+ signal: AbortSignal.timeout(2e3)
2575
+ });
2576
+ return await res.json();
2577
+ } catch (err) {
2578
+ try {
2579
+ const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
2580
+ appendToLog2(HOOK_DEBUG_LOG2, {
2581
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2582
+ event: "checkTaint-error",
2583
+ error: String(err),
2584
+ paths
2585
+ });
2586
+ } catch {
2587
+ }
2588
+ return { tainted: false, daemonUnavailable: true };
2589
+ }
2590
+ }
2591
+ async function resolveViaDaemon(id, decision, internalToken, source) {
2458
2592
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2459
2593
  await fetch(`${base}/resolve/${id}`, {
2460
2594
  method: "POST",
2461
2595
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
2462
- body: JSON.stringify({ decision }),
2596
+ body: JSON.stringify({ decision, ...source && { source } }),
2463
2597
  signal: AbortSignal.timeout(3e3)
2464
2598
  });
2465
2599
  }
@@ -2666,20 +2800,24 @@ ${smartTruncate(str, 500)}`
2666
2800
  function escapePango(text) {
2667
2801
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2668
2802
  }
2669
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2803
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2670
2804
  const lines = [];
2671
2805
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2672
2806
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2673
2807
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2674
2808
  lines.push("");
2675
2809
  lines.push(formattedArgs);
2810
+ if (allowCount >= 3) {
2811
+ lines.push("");
2812
+ lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
2813
+ }
2676
2814
  if (!locked) {
2677
2815
  lines.push("");
2678
2816
  lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
2679
2817
  }
2680
2818
  return lines.join("\n");
2681
2819
  }
2682
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2820
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2683
2821
  const lines = [];
2684
2822
  if (locked) {
2685
2823
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2691,6 +2829,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2691
2829
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2692
2830
  lines.push("");
2693
2831
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2832
+ if (allowCount >= 3) {
2833
+ lines.push("");
2834
+ lines.push(
2835
+ `<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
2836
+ );
2837
+ }
2694
2838
  if (!locked) {
2695
2839
  lines.push("");
2696
2840
  lines.push(
@@ -2699,12 +2843,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2699
2843
  }
2700
2844
  return lines.join("\n");
2701
2845
  }
2702
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
2846
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2703
2847
  if (isTestEnv()) return "deny";
2704
2848
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2705
2849
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
2706
2850
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
2707
- const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
2851
+ const message = buildPlainMessage(
2852
+ toolName,
2853
+ formattedArgs,
2854
+ agent,
2855
+ explainableLabel,
2856
+ locked,
2857
+ allowCount
2858
+ );
2708
2859
  return new Promise((resolve) => {
2709
2860
  let childProcess = null;
2710
2861
  const onAbort = () => {
@@ -2736,7 +2887,8 @@ end run`;
2736
2887
  formattedArgs,
2737
2888
  agent,
2738
2889
  explainableLabel,
2739
- locked
2890
+ locked,
2891
+ allowCount
2740
2892
  );
2741
2893
  const argsList = [
2742
2894
  locked ? "--info" : "--question",
@@ -2907,6 +3059,40 @@ var init_cloud = __esm({
2907
3059
  });
2908
3060
 
2909
3061
  // src/auth/orchestrator.ts
3062
+ function isWriteTool(toolName) {
3063
+ const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
3064
+ return WRITE_TOOLS.has(t);
3065
+ }
3066
+ function extractFilePaths(toolName, args) {
3067
+ const paths = [];
3068
+ if (!args || typeof args !== "object" || Array.isArray(args)) return paths;
3069
+ const a = args;
3070
+ for (const key of ["file_path", "path", "filename", "source", "src", "input"]) {
3071
+ if (typeof a[key] === "string" && a[key]) paths.push(a[key]);
3072
+ }
3073
+ const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
3074
+ if (cmd) {
3075
+ for (const m of cmd.matchAll(/(?:-T|--upload-file|--data(?:-binary)?)\s+@?(\S+)/g)) {
3076
+ paths.push(m[1]);
3077
+ }
3078
+ for (const m of cmd.matchAll(/\b(?:scp|rsync)\s+(?:-\S+\s+)*(\S+)\s+\S+@/g)) {
3079
+ paths.push(m[1]);
3080
+ }
3081
+ for (const m of cmd.matchAll(/<\s*(\S+)/g)) {
3082
+ paths.push(m[1]);
3083
+ }
3084
+ }
3085
+ return paths.filter(Boolean);
3086
+ }
3087
+ function isNetworkTool(toolName, args) {
3088
+ const t = toolName.toLowerCase();
3089
+ if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
3090
+ const a = args;
3091
+ const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
3092
+ return /\b(curl|wget|scp|rsync|nc|ncat|netcat|ssh)\b/.test(cmd);
3093
+ }
3094
+ return false;
3095
+ }
2910
3096
  function notifyActivity(data) {
2911
3097
  return new Promise((resolve) => {
2912
3098
  try {
@@ -2924,7 +3110,7 @@ function notifyActivity(data) {
2924
3110
  }
2925
3111
  async function authorizeHeadless(toolName, args, meta, options) {
2926
3112
  if (!options?.calledFromDaemon) {
2927
- const actId = (0, import_crypto2.randomUUID)();
3113
+ const actId = (0, import_crypto3.randomUUID)();
2928
3114
  const actTs = Date.now();
2929
3115
  await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
2930
3116
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
@@ -2936,7 +3122,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
2936
3122
  id: actId,
2937
3123
  tool: toolName,
2938
3124
  ts: actTs,
2939
- status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
3125
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
2940
3126
  label: result.blockedByLabel
2941
3127
  });
2942
3128
  }
@@ -2950,6 +3136,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2950
3136
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
2951
3137
  const creds = getCredentials();
2952
3138
  const config = getConfig(options?.cwd);
3139
+ const hashAuditArgs = config.settings.auditHashArgs === true;
2953
3140
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
2954
3141
  const approvers = {
2955
3142
  ...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
@@ -2960,13 +3147,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2960
3147
  approvers.terminal = false;
2961
3148
  }
2962
3149
  if (config.settings.enableHookLogDebug && !isTestEnv2) {
2963
- appendHookDebug(toolName, args, meta);
3150
+ appendHookDebug(toolName, args, meta, hashAuditArgs);
2964
3151
  }
2965
3152
  const isManual = meta?.agent === "Terminal";
2966
3153
  let explainableLabel = "Local Config";
2967
3154
  let policyMatchedField;
2968
3155
  let policyMatchedWord;
2969
3156
  let riskMetadata;
3157
+ let taintWarning = null;
3158
+ if (isNetworkTool(toolName, args)) {
3159
+ const filePaths = extractFilePaths(toolName, args);
3160
+ if (filePaths.length > 0) {
3161
+ const taintResult = await checkTaint(filePaths);
3162
+ if (taintResult.tainted && taintResult.record) {
3163
+ const { path: taintedPath, source: taintSource } = taintResult.record;
3164
+ taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
3165
+ } else if (taintResult.daemonUnavailable) {
3166
+ taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
3167
+ }
3168
+ }
3169
+ }
2970
3170
  if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
2971
3171
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
2972
3172
  const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
@@ -2974,7 +3174,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2974
3174
  if (dlpMatch) {
2975
3175
  const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
2976
3176
  if (dlpMatch.severity === "block") {
2977
- if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
3177
+ if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta, true);
3178
+ if (isWriteTool(toolName) && filePath) {
3179
+ await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
3180
+ }
2978
3181
  return {
2979
3182
  approved: false,
2980
3183
  reason: dlpReason,
@@ -2982,7 +3185,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2982
3185
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
2983
3186
  };
2984
3187
  }
2985
- if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
3188
+ if (!isManual)
3189
+ appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta, hashAuditArgs);
2986
3190
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
2987
3191
  }
2988
3192
  }
@@ -2990,7 +3194,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2990
3194
  if (!isIgnoredTool(toolName)) {
2991
3195
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
2992
3196
  if (policyResult.decision === "review") {
2993
- appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
3197
+ appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
2994
3198
  if (approvers.cloud && creds?.apiKey) {
2995
3199
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
2996
3200
  }
@@ -2998,22 +3202,23 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2998
3202
  }
2999
3203
  return { approved: true, checkedBy: "audit" };
3000
3204
  }
3001
- if (!isIgnoredTool(toolName)) {
3205
+ if (!taintWarning && !isIgnoredTool(toolName)) {
3002
3206
  if (getActiveTrustSession(toolName)) {
3003
3207
  if (approvers.cloud && creds?.apiKey)
3004
3208
  await auditLocalAllow(toolName, args, "trust", creds, meta);
3005
- if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
3209
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
3006
3210
  return { approved: true, checkedBy: "trust" };
3007
3211
  }
3008
3212
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
3009
3213
  if (policyResult.decision === "allow") {
3010
3214
  if (approvers.cloud && creds?.apiKey)
3011
3215
  auditLocalAllow(toolName, args, "local-policy", creds, meta);
3012
- if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
3216
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
3013
3217
  return { approved: true, checkedBy: "local-policy" };
3014
3218
  }
3015
3219
  if (policyResult.decision === "block") {
3016
- if (!isManual) appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta);
3220
+ if (!isManual)
3221
+ appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
3017
3222
  return {
3018
3223
  approved: false,
3019
3224
  reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
@@ -3032,15 +3237,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3032
3237
  policyMatchedWord,
3033
3238
  policyResult.ruleName
3034
3239
  );
3035
- const persistent = getPersistentDecision(toolName);
3240
+ const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
3036
3241
  if (persistent === "allow") {
3037
3242
  if (approvers.cloud && creds?.apiKey)
3038
3243
  await auditLocalAllow(toolName, args, "persistent", creds, meta);
3039
- if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
3244
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
3040
3245
  return { approved: true, checkedBy: "persistent" };
3041
3246
  }
3042
3247
  if (persistent === "deny") {
3043
- if (!isManual) appendLocalAudit(toolName, args, "deny", "persistent-deny", meta);
3248
+ if (!isManual)
3249
+ appendLocalAudit(toolName, args, "deny", "persistent-deny", meta, hashAuditArgs);
3044
3250
  return {
3045
3251
  approved: false,
3046
3252
  reason: `This tool ("${toolName}") is explicitly listed in your 'Always Deny' list.`,
@@ -3048,10 +3254,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3048
3254
  blockedByLabel: "Persistent User Rule"
3049
3255
  };
3050
3256
  }
3051
- } else {
3052
- if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
3257
+ } else if (!taintWarning) {
3258
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
3053
3259
  return { approved: true };
3054
3260
  }
3261
+ if (taintWarning) {
3262
+ explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
3263
+ riskMetadata = computeRiskMetadata(
3264
+ args,
3265
+ 7,
3266
+ explainableLabel,
3267
+ void 0,
3268
+ void 0,
3269
+ taintWarning
3270
+ );
3271
+ }
3055
3272
  let cloudRequestId = null;
3056
3273
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
3057
3274
  if (cloudEnforced) {
@@ -3070,7 +3287,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3070
3287
  };
3071
3288
  }
3072
3289
  cloudRequestId = initResult.requestId || null;
3073
- explainableLabel = "Organization Policy (SaaS)";
3290
+ if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3074
3291
  } catch {
3075
3292
  }
3076
3293
  }
@@ -3099,13 +3316,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3099
3316
  let viewerId = null;
3100
3317
  const internalToken = getInternalToken();
3101
3318
  let daemonEntryId = null;
3319
+ let daemonAllowCount = 1;
3102
3320
  if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
3103
3321
  if (cloudEnforced && cloudRequestId) {
3104
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3322
+ const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3323
+ viewerId = viewer?.id ?? null;
3105
3324
  daemonEntryId = viewerId;
3325
+ if (viewer) daemonAllowCount = viewer.allowCount;
3106
3326
  } else {
3107
3327
  try {
3108
- daemonEntryId = await registerDaemonEntry(
3328
+ const entry = await registerDaemonEntry(
3109
3329
  toolName,
3110
3330
  args,
3111
3331
  meta,
@@ -3113,6 +3333,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3113
3333
  options?.activityId,
3114
3334
  options?.cwd
3115
3335
  );
3336
+ daemonEntryId = entry.id;
3337
+ daemonAllowCount = entry.allowCount;
3116
3338
  } catch {
3117
3339
  }
3118
3340
  }
@@ -3148,7 +3370,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3148
3370
  false,
3149
3371
  signal,
3150
3372
  policyMatchedField,
3151
- policyMatchedWord
3373
+ policyMatchedWord,
3374
+ daemonAllowCount
3152
3375
  );
3153
3376
  if (decision === "always_allow") {
3154
3377
  writeTrustSession(toolName, 36e5);
@@ -3206,10 +3429,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3206
3429
  if (!resolved) {
3207
3430
  resolved = true;
3208
3431
  abortController.abort();
3209
- if (viewerId && internalToken) {
3210
- resolveViaDaemon(viewerId, res.approved ? "allow" : "deny", internalToken).catch(
3211
- () => null
3212
- );
3432
+ if (daemonEntryId && internalToken) {
3433
+ resolveViaDaemon(
3434
+ daemonEntryId,
3435
+ res.approved ? "allow" : "deny",
3436
+ internalToken,
3437
+ res.decisionSource
3438
+ ).catch(() => null);
3213
3439
  }
3214
3440
  resolve(res);
3215
3441
  }
@@ -3248,19 +3474,20 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3248
3474
  args,
3249
3475
  finalResult.approved ? "allow" : "deny",
3250
3476
  finalResult.checkedBy || finalResult.blockedBy || "unknown",
3251
- meta
3477
+ meta,
3478
+ hashAuditArgs
3252
3479
  );
3253
3480
  }
3254
3481
  return finalResult;
3255
3482
  }
3256
- var import_net, import_path13, import_os10, import_crypto2, ACTIVITY_SOCKET_PATH;
3483
+ var import_net, import_path13, import_os10, import_crypto3, WRITE_TOOLS, ACTIVITY_SOCKET_PATH;
3257
3484
  var init_orchestrator = __esm({
3258
3485
  "src/auth/orchestrator.ts"() {
3259
3486
  "use strict";
3260
3487
  import_net = __toESM(require("net"));
3261
3488
  import_path13 = __toESM(require("path"));
3262
3489
  import_os10 = __toESM(require("os"));
3263
- import_crypto2 = require("crypto");
3490
+ import_crypto3 = require("crypto");
3264
3491
  init_native();
3265
3492
  init_context_sniper();
3266
3493
  init_dlp();
@@ -3270,6 +3497,17 @@ var init_orchestrator = __esm({
3270
3497
  init_state();
3271
3498
  init_daemon();
3272
3499
  init_cloud();
3500
+ WRITE_TOOLS = /* @__PURE__ */ new Set([
3501
+ "write",
3502
+ "write_file",
3503
+ "create_file",
3504
+ "edit",
3505
+ "multiedit",
3506
+ "str_replace_based_edit_tool",
3507
+ "replace",
3508
+ "notebook_edit",
3509
+ "notebookedit"
3510
+ ]);
3273
3511
  ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os10.default.tmpdir(), "node9-activity.sock");
3274
3512
  }
3275
3513
  });
@@ -3566,6 +3804,15 @@ var init_ui = __esm({
3566
3804
  padding: 5px 10px;
3567
3805
  margin-bottom: 14px;
3568
3806
  }
3807
+ .insight-hint {
3808
+ font-size: 12px;
3809
+ color: #f0c040;
3810
+ background: rgba(240, 192, 64, 0.08);
3811
+ border: 1px solid rgba(240, 192, 64, 0.25);
3812
+ border-radius: 6px;
3813
+ padding: 6px 10px;
3814
+ margin-bottom: 12px;
3815
+ }
3569
3816
  pre {
3570
3817
  background: #0d1117;
3571
3818
  padding: 14px 16px;
@@ -4038,6 +4285,78 @@ var init_ui = __esm({
4038
4285
  color: var(--danger);
4039
4286
  }
4040
4287
 
4288
+ /* \u2500\u2500 Suggestion cards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4289
+ .suggestion-card {
4290
+ background: rgba(82, 130, 255, 0.06);
4291
+ border: 1px solid rgba(82, 130, 255, 0.25);
4292
+ border-radius: 8px;
4293
+ padding: 10px 12px;
4294
+ margin-bottom: 8px;
4295
+ }
4296
+ .suggestion-card:last-child {
4297
+ margin-bottom: 0;
4298
+ }
4299
+ .suggestion-header {
4300
+ display: flex;
4301
+ align-items: center;
4302
+ gap: 8px;
4303
+ margin-bottom: 6px;
4304
+ }
4305
+ .suggestion-tool {
4306
+ font-family: 'Fira Code', monospace;
4307
+ font-size: 11px;
4308
+ color: var(--text-bright);
4309
+ flex: 1;
4310
+ word-break: break-all;
4311
+ }
4312
+ .suggestion-count {
4313
+ font-size: 10px;
4314
+ color: var(--muted);
4315
+ white-space: nowrap;
4316
+ }
4317
+ .suggestion-rule {
4318
+ font-family: 'Fira Code', monospace;
4319
+ font-size: 10px;
4320
+ color: #79c0ff;
4321
+ background: rgba(0, 0, 0, 0.25);
4322
+ border-radius: 4px;
4323
+ padding: 4px 8px;
4324
+ margin-bottom: 8px;
4325
+ word-break: break-all;
4326
+ white-space: pre-wrap;
4327
+ }
4328
+ .suggestion-actions {
4329
+ display: flex;
4330
+ gap: 6px;
4331
+ }
4332
+ .btn-apply {
4333
+ background: rgba(52, 125, 57, 0.2);
4334
+ border: 1px solid rgba(87, 171, 90, 0.4);
4335
+ color: #57ab5a;
4336
+ padding: 4px 10px;
4337
+ font-size: 11px;
4338
+ border-radius: 5px;
4339
+ font-family: inherit;
4340
+ cursor: pointer;
4341
+ }
4342
+ .btn-apply:hover {
4343
+ background: rgba(52, 125, 57, 0.35);
4344
+ }
4345
+ .btn-dismiss-suggestion {
4346
+ background: transparent;
4347
+ border: 1px solid var(--border);
4348
+ color: var(--muted);
4349
+ padding: 4px 10px;
4350
+ font-size: 11px;
4351
+ border-radius: 5px;
4352
+ font-family: inherit;
4353
+ cursor: pointer;
4354
+ }
4355
+ .btn-dismiss-suggestion:hover {
4356
+ border-color: var(--danger);
4357
+ color: var(--danger);
4358
+ }
4359
+
4041
4360
  .modal-overlay {
4042
4361
  display: none;
4043
4362
  position: fixed;
@@ -4219,6 +4538,11 @@ var init_ui = __esm({
4219
4538
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
4220
4539
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
4221
4540
  </div>
4541
+
4542
+ <div class="panel" id="suggestionsPanel" style="display: none">
4543
+ <div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
4544
+ <div id="suggestionsList"></div>
4545
+ </div>
4222
4546
  </div>
4223
4547
  </div>
4224
4548
  </div>
@@ -4368,12 +4692,15 @@ var init_ui = __esm({
4368
4692
  const badgeClass = isEdit ? 'sniper-badge-edit' : 'sniper-badge-exec';
4369
4693
  const badgeLabel = isEdit ? '\u{1F4DD} Code Edit' : '\u{1F6D1} Execution';
4370
4694
  const tierLabel = \`Tier \${rm.tier} \xB7 \${esc(rm.blockedByLabel)}\`;
4695
+ const isTaint = rm.blockedByLabel?.includes('Taint');
4371
4696
  const fileLine =
4372
- isEdit && rm.editFilePath
4373
- ? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
4374
- : !isEdit && rm.matchedWord
4375
- ? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
4376
- : '';
4697
+ isTaint && rm.ruleName
4698
+ ? \`<div class="sniper-match">\u26A0\uFE0F \${esc(rm.ruleName)}</div>\`
4699
+ : isEdit && rm.editFilePath
4700
+ ? \`<div class="sniper-filepath">\u{1F4C2} \${esc(rm.editFilePath)}</div>\`
4701
+ : !isEdit && rm.matchedWord
4702
+ ? \`<div class="sniper-match">Matched: <code>\${esc(rm.matchedWord)}</code>\${rm.matchedField ? \` in <code>\${esc(rm.matchedField)}</code>\` : ''}</div>\`
4703
+ : '';
4377
4704
  const snippetHtml = rm.contextSnippet ? \`<pre>\${esc(rm.contextSnippet)}</pre>\` : '';
4378
4705
  return \`
4379
4706
  <div class="sniper-header">
@@ -4408,6 +4735,7 @@ var init_ui = __esm({
4408
4735
  </div>
4409
4736
  <div class="tool-chip">\${esc(req.toolName)}</div>
4410
4737
  \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
4738
+ \${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
4411
4739
  \${renderPayload(req)}
4412
4740
  <div class="actions" id="act-\${req.id}">
4413
4741
  <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
@@ -4474,6 +4802,14 @@ var init_ui = __esm({
4474
4802
  ev.addEventListener('shields-status', (e) => {
4475
4803
  renderShields(JSON.parse(e.data).shields);
4476
4804
  });
4805
+ ev.addEventListener('suggestion:new', (e) => {
4806
+ const s = JSON.parse(e.data);
4807
+ addSuggestionCard(s);
4808
+ });
4809
+ ev.addEventListener('suggestion:resolved', (e) => {
4810
+ const { id } = JSON.parse(e.data);
4811
+ removeSuggestionCard(id);
4812
+ });
4477
4813
 
4478
4814
  // \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4479
4815
  ev.addEventListener('activity', (e) => {
@@ -4723,6 +5059,74 @@ var init_ui = __esm({
4723
5059
  .then((r) => r.json())
4724
5060
  .then(renderDecisions)
4725
5061
  .catch(() => {});
5062
+
5063
+ // \u2500\u2500 Smart Rule Suggestions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5064
+ function rulePreview(suggestion) {
5065
+ const r = suggestion.suggestedRule;
5066
+ if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
5067
+ const cond = r.rule.conditions?.[0];
5068
+ const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
5069
+ return \`allow \${r.rule.tool}\${condStr}\`;
5070
+ }
5071
+
5072
+ function addSuggestionCard(s) {
5073
+ const panel = document.getElementById('suggestionsPanel');
5074
+ const list = document.getElementById('suggestionsList');
5075
+ panel.style.display = '';
5076
+
5077
+ const card = document.createElement('div');
5078
+ card.className = 'suggestion-card';
5079
+ card.id = 'sg-' + s.id;
5080
+ card.innerHTML = \`
5081
+ <div class="suggestion-header">
5082
+ <span class="suggestion-tool">\${esc(s.toolName)}</span>
5083
+ <span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
5084
+ </div>
5085
+ <div class="suggestion-rule">\${esc(rulePreview(s))}</div>
5086
+ <div class="suggestion-actions">
5087
+ <button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
5088
+ <button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
5089
+ </div>
5090
+ \`;
5091
+ list.appendChild(card);
5092
+ }
5093
+
5094
+ function removeSuggestionCard(id) {
5095
+ document.getElementById('sg-' + id)?.remove();
5096
+ const list = document.getElementById('suggestionsList');
5097
+ if (!list.querySelector('.suggestion-card')) {
5098
+ document.getElementById('suggestionsPanel').style.display = 'none';
5099
+ }
5100
+ }
5101
+
5102
+ function applySuggestion(id) {
5103
+ fetch('/suggestions/' + id + '/apply', {
5104
+ method: 'POST',
5105
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
5106
+ body: JSON.stringify({}),
5107
+ })
5108
+ .then((r) => {
5109
+ if (r.ok) removeSuggestionCard(id);
5110
+ })
5111
+ .catch(() => {});
5112
+ }
5113
+
5114
+ function dismissSuggestion(id) {
5115
+ fetch('/suggestions/' + id + '/dismiss', {
5116
+ method: 'POST',
5117
+ headers: { 'X-Node9-Token': CSRF_TOKEN },
5118
+ })
5119
+ .then((r) => {
5120
+ if (r.ok) removeSuggestionCard(id);
5121
+ })
5122
+ .catch(() => {});
5123
+ }
5124
+
5125
+ // Load any suggestions that survived a page reload (daemon still running)
5126
+ fetch('/suggestions')
5127
+ .then((r) => r.json())
5128
+ .then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
5129
+ .catch(() => {});
4726
5130
  </script>
4727
5131
  </body>
4728
5132
  </html>
@@ -4740,7 +5144,197 @@ var init_ui2 = __esm({
4740
5144
  }
4741
5145
  });
4742
5146
 
5147
+ // src/daemon/suggestion-tracker.ts
5148
+ function extractPath(args) {
5149
+ if (!args || typeof args !== "object") return null;
5150
+ const a = args;
5151
+ for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
5152
+ if (typeof a[key] === "string" && a[key]) return a[key];
5153
+ }
5154
+ return null;
5155
+ }
5156
+ function commonPathPrefix(paths) {
5157
+ if (paths.length < 2) return null;
5158
+ const dirParts = paths.map((p) => {
5159
+ const lastSlash = p.lastIndexOf("/");
5160
+ return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
5161
+ });
5162
+ const first = dirParts[0].split("/");
5163
+ const common = [];
5164
+ for (let i = 0; i < first.length; i++) {
5165
+ if (dirParts.every((d) => d.split("/")[i] === first[i])) {
5166
+ common.push(first[i]);
5167
+ } else {
5168
+ break;
5169
+ }
5170
+ }
5171
+ const prefix = common.join("/").replace(/\/?$/, "/");
5172
+ return prefix.length > 1 ? prefix : null;
5173
+ }
5174
+ var import_crypto4, SuggestionTracker;
5175
+ var init_suggestion_tracker = __esm({
5176
+ "src/daemon/suggestion-tracker.ts"() {
5177
+ "use strict";
5178
+ import_crypto4 = require("crypto");
5179
+ SuggestionTracker = class {
5180
+ events = /* @__PURE__ */ new Map();
5181
+ threshold;
5182
+ constructor(threshold = 3) {
5183
+ this.threshold = threshold;
5184
+ }
5185
+ /**
5186
+ * Record a human-allowed review for a tool.
5187
+ * Returns a Suggestion when the threshold is reached, null otherwise.
5188
+ */
5189
+ recordAllow(toolName, args) {
5190
+ const events = this.events.get(toolName) ?? [];
5191
+ events.push({ args, ts: Date.now() });
5192
+ this.events.set(toolName, events);
5193
+ if (events.length >= this.threshold) {
5194
+ this.events.delete(toolName);
5195
+ return this.generateSuggestion(toolName, events);
5196
+ }
5197
+ return null;
5198
+ }
5199
+ /**
5200
+ * Reset the counter for a tool (e.g. when the user clicks Deny —
5201
+ * don't suggest allowing something they just blocked).
5202
+ */
5203
+ resetTool(toolName) {
5204
+ this.events.delete(toolName);
5205
+ }
5206
+ /** Current allow count for a tool (for tests). */
5207
+ getCount(toolName) {
5208
+ return this.events.get(toolName)?.length ?? 0;
5209
+ }
5210
+ generateSuggestion(toolName, events) {
5211
+ const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
5212
+ const prefix = commonPathPrefix(paths);
5213
+ const suggestedRule = prefix ? {
5214
+ type: "smartRule",
5215
+ rule: {
5216
+ name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
5217
+ tool: toolName,
5218
+ conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
5219
+ verdict: "allow",
5220
+ reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
5221
+ }
5222
+ } : { type: "ignoredTool", toolName };
5223
+ return {
5224
+ id: (0, import_crypto4.randomUUID)(),
5225
+ toolName,
5226
+ allowCount: events.length,
5227
+ suggestedRule,
5228
+ status: "pending",
5229
+ createdAt: Date.now(),
5230
+ exampleArgs: events.slice(0, 3).map((e) => e.args)
5231
+ };
5232
+ }
5233
+ };
5234
+ }
5235
+ });
5236
+
5237
+ // src/daemon/taint-store.ts
5238
+ var import_fs12, import_path15, DEFAULT_TTL_MS, TaintStore;
5239
+ var init_taint_store = __esm({
5240
+ "src/daemon/taint-store.ts"() {
5241
+ "use strict";
5242
+ import_fs12 = __toESM(require("fs"));
5243
+ import_path15 = __toESM(require("path"));
5244
+ DEFAULT_TTL_MS = 60 * 60 * 1e3;
5245
+ TaintStore = class {
5246
+ records = /* @__PURE__ */ new Map();
5247
+ /** Add or refresh taint on an absolute path. */
5248
+ taint(filePath, source, ttlMs = DEFAULT_TTL_MS) {
5249
+ const resolved = this._resolve(filePath);
5250
+ const now = Date.now();
5251
+ this.records.set(resolved, {
5252
+ path: resolved,
5253
+ source,
5254
+ createdAt: now,
5255
+ expiresAt: now + ttlMs
5256
+ });
5257
+ }
5258
+ /**
5259
+ * Check whether a path is currently tainted.
5260
+ * Returns the TaintRecord if tainted (and not expired), null otherwise.
5261
+ * Expired records are pruned on access.
5262
+ */
5263
+ check(filePath) {
5264
+ const resolved = this._resolve(filePath);
5265
+ const record = this.records.get(resolved);
5266
+ if (!record) return null;
5267
+ if (Date.now() > record.expiresAt) {
5268
+ this.records.delete(resolved);
5269
+ return null;
5270
+ }
5271
+ return record;
5272
+ }
5273
+ /**
5274
+ * Propagate taint from sourcePath to destPath (e.g. cp, mv).
5275
+ * For mv semantics (clearSource=true) the source taint is removed.
5276
+ */
5277
+ propagate(sourcePath, destPath, clearSource = false) {
5278
+ const taintRecord = this.check(sourcePath);
5279
+ if (!taintRecord) return;
5280
+ const remainingMs = taintRecord.expiresAt - Date.now();
5281
+ if (remainingMs > 0) {
5282
+ const baseSource = taintRecord.source.replace(/^(propagated:)+/, "");
5283
+ this.taint(destPath, `propagated:${baseSource}`, remainingMs);
5284
+ }
5285
+ if (clearSource) {
5286
+ this.records.delete(this._resolve(sourcePath));
5287
+ }
5288
+ }
5289
+ /** Remove all expired records. Called periodically by the daemon. */
5290
+ prune() {
5291
+ const now = Date.now();
5292
+ for (const [key, record] of this.records) {
5293
+ if (now > record.expiresAt) this.records.delete(key);
5294
+ }
5295
+ }
5296
+ /** Return all non-expired taint records (for audit/debug). */
5297
+ list() {
5298
+ this.prune();
5299
+ return [...this.records.values()];
5300
+ }
5301
+ /** Remove all taint records atomically. Used by tests to reset state between runs. */
5302
+ clear() {
5303
+ this.records.clear();
5304
+ }
5305
+ /** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
5306
+ _resolve(filePath) {
5307
+ try {
5308
+ return import_fs12.default.realpathSync.native(import_path15.default.resolve(filePath));
5309
+ } catch {
5310
+ return import_path15.default.resolve(filePath);
5311
+ }
5312
+ }
5313
+ };
5314
+ }
5315
+ });
5316
+
4743
5317
  // src/daemon/state.ts
5318
+ function loadInsightCounts() {
5319
+ try {
5320
+ if (!import_fs13.default.existsSync(INSIGHT_COUNTS_FILE)) return;
5321
+ const data = JSON.parse(import_fs13.default.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
5322
+ for (const [tool, count] of Object.entries(data)) {
5323
+ if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
5324
+ }
5325
+ } catch {
5326
+ }
5327
+ }
5328
+ function saveInsightCounts() {
5329
+ try {
5330
+ const data = {};
5331
+ insightCounts.forEach((count, tool) => {
5332
+ data[tool] = count;
5333
+ });
5334
+ atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
5335
+ } catch {
5336
+ }
5337
+ }
4744
5338
  function getAbandonTimer() {
4745
5339
  return _abandonTimer;
4746
5340
  }
@@ -4763,11 +5357,27 @@ function markRejectionHandlerRegistered() {
4763
5357
  daemonRejectionHandlerRegistered = true;
4764
5358
  }
4765
5359
  function atomicWriteSync2(filePath, data, options) {
4766
- const dir = import_path15.default.dirname(filePath);
4767
- if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
4768
- const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
4769
- import_fs12.default.writeFileSync(tmpPath, data, options);
4770
- import_fs12.default.renameSync(tmpPath, filePath);
5360
+ const dir = import_path16.default.dirname(filePath);
5361
+ if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
5362
+ const tmpPath = `${filePath}.${(0, import_crypto5.randomUUID)()}.tmp`;
5363
+ try {
5364
+ import_fs13.default.writeFileSync(tmpPath, data, options);
5365
+ } catch (err) {
5366
+ try {
5367
+ import_fs13.default.unlinkSync(tmpPath);
5368
+ } catch {
5369
+ }
5370
+ throw err;
5371
+ }
5372
+ try {
5373
+ import_fs13.default.renameSync(tmpPath, filePath);
5374
+ } catch (err) {
5375
+ try {
5376
+ import_fs13.default.unlinkSync(tmpPath);
5377
+ } catch {
5378
+ }
5379
+ throw err;
5380
+ }
4771
5381
  }
4772
5382
  function redactArgs(value) {
4773
5383
  if (!value || typeof value !== "object") return value;
@@ -4787,16 +5397,16 @@ function appendAuditLog(data) {
4787
5397
  decision: data.decision,
4788
5398
  source: "daemon"
4789
5399
  };
4790
- const dir = import_path15.default.dirname(AUDIT_LOG_FILE);
4791
- if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
4792
- import_fs12.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
5400
+ const dir = import_path16.default.dirname(AUDIT_LOG_FILE);
5401
+ if (!import_fs13.default.existsSync(dir)) import_fs13.default.mkdirSync(dir, { recursive: true });
5402
+ import_fs13.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
4793
5403
  } catch {
4794
5404
  }
4795
5405
  }
4796
5406
  function getAuditHistory(limit = 20) {
4797
5407
  try {
4798
- if (!import_fs12.default.existsSync(AUDIT_LOG_FILE)) return [];
4799
- const lines = import_fs12.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
5408
+ if (!import_fs13.default.existsSync(AUDIT_LOG_FILE)) return [];
5409
+ const lines = import_fs13.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
4800
5410
  if (lines.length === 1 && lines[0] === "") return [];
4801
5411
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
4802
5412
  } catch {
@@ -4805,19 +5415,19 @@ function getAuditHistory(limit = 20) {
4805
5415
  }
4806
5416
  function getOrgName() {
4807
5417
  try {
4808
- if (import_fs12.default.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
5418
+ if (import_fs13.default.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
4809
5419
  } catch {
4810
5420
  }
4811
5421
  return null;
4812
5422
  }
4813
5423
  function hasStoredSlackKey() {
4814
- return import_fs12.default.existsSync(CREDENTIALS_FILE);
5424
+ return import_fs13.default.existsSync(CREDENTIALS_FILE);
4815
5425
  }
4816
5426
  function writeGlobalSetting(key, value) {
4817
5427
  let config = {};
4818
5428
  try {
4819
- if (import_fs12.default.existsSync(GLOBAL_CONFIG_FILE)) {
4820
- config = JSON.parse(import_fs12.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
5429
+ if (import_fs13.default.existsSync(GLOBAL_CONFIG_FILE)) {
5430
+ config = JSON.parse(import_fs13.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4821
5431
  }
4822
5432
  } catch {
4823
5433
  }
@@ -4829,8 +5439,8 @@ function writeTrustEntry(toolName, durationMs) {
4829
5439
  try {
4830
5440
  let trust = { entries: [] };
4831
5441
  try {
4832
- if (import_fs12.default.existsSync(TRUST_FILE2))
4833
- trust = JSON.parse(import_fs12.default.readFileSync(TRUST_FILE2, "utf-8"));
5442
+ if (import_fs13.default.existsSync(TRUST_FILE2))
5443
+ trust = JSON.parse(import_fs13.default.readFileSync(TRUST_FILE2, "utf-8"));
4834
5444
  } catch {
4835
5445
  }
4836
5446
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -4841,8 +5451,8 @@ function writeTrustEntry(toolName, durationMs) {
4841
5451
  }
4842
5452
  function readPersistentDecisions() {
4843
5453
  try {
4844
- if (import_fs12.default.existsSync(DECISIONS_FILE)) {
4845
- return JSON.parse(import_fs12.default.readFileSync(DECISIONS_FILE, "utf-8"));
5454
+ if (import_fs13.default.existsSync(DECISIONS_FILE)) {
5455
+ return JSON.parse(import_fs13.default.readFileSync(DECISIONS_FILE, "utf-8"));
4846
5456
  }
4847
5457
  } catch {
4848
5458
  }
@@ -4907,7 +5517,7 @@ function abandonPending() {
4907
5517
  });
4908
5518
  if (autoStarted) {
4909
5519
  try {
4910
- import_fs12.default.unlinkSync(DAEMON_PID_FILE);
5520
+ import_fs13.default.unlinkSync(DAEMON_PID_FILE);
4911
5521
  } catch {
4912
5522
  }
4913
5523
  setTimeout(() => {
@@ -4918,7 +5528,7 @@ function abandonPending() {
4918
5528
  }
4919
5529
  function startActivitySocket() {
4920
5530
  try {
4921
- import_fs12.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
5531
+ import_fs13.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4922
5532
  } catch {
4923
5533
  }
4924
5534
  const ACTIVITY_MAX_BYTES = 1024 * 1024;
@@ -4960,31 +5570,38 @@ function startActivitySocket() {
4960
5570
  unixServer.listen(ACTIVITY_SOCKET_PATH2);
4961
5571
  process.on("exit", () => {
4962
5572
  try {
4963
- import_fs12.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
5573
+ import_fs13.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4964
5574
  } catch {
4965
5575
  }
4966
5576
  });
4967
5577
  }
4968
- var import_net2, import_fs12, import_path15, import_os12, import_child_process3, import_crypto3, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, pending, sseClients, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
5578
+ var import_net2, import_fs13, import_path16, import_os12, 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;
4969
5579
  var init_state2 = __esm({
4970
5580
  "src/daemon/state.ts"() {
4971
5581
  "use strict";
4972
5582
  import_net2 = __toESM(require("net"));
4973
- import_fs12 = __toESM(require("fs"));
4974
- import_path15 = __toESM(require("path"));
5583
+ import_fs13 = __toESM(require("fs"));
5584
+ import_path16 = __toESM(require("path"));
4975
5585
  import_os12 = __toESM(require("os"));
4976
5586
  import_child_process3 = require("child_process");
4977
- import_crypto3 = require("crypto");
5587
+ import_crypto5 = require("crypto");
4978
5588
  init_daemon();
5589
+ init_suggestion_tracker();
5590
+ init_taint_store();
4979
5591
  homeDir = import_os12.default.homedir();
4980
- DAEMON_PID_FILE = import_path15.default.join(homeDir, ".node9", "daemon.pid");
4981
- DECISIONS_FILE = import_path15.default.join(homeDir, ".node9", "decisions.json");
4982
- AUDIT_LOG_FILE = import_path15.default.join(homeDir, ".node9", "audit.log");
4983
- TRUST_FILE2 = import_path15.default.join(homeDir, ".node9", "trust.json");
4984
- GLOBAL_CONFIG_FILE = import_path15.default.join(homeDir, ".node9", "config.json");
4985
- CREDENTIALS_FILE = import_path15.default.join(homeDir, ".node9", "credentials.json");
5592
+ DAEMON_PID_FILE = import_path16.default.join(homeDir, ".node9", "daemon.pid");
5593
+ DECISIONS_FILE = import_path16.default.join(homeDir, ".node9", "decisions.json");
5594
+ AUDIT_LOG_FILE = import_path16.default.join(homeDir, ".node9", "audit.log");
5595
+ TRUST_FILE2 = import_path16.default.join(homeDir, ".node9", "trust.json");
5596
+ GLOBAL_CONFIG_FILE = import_path16.default.join(homeDir, ".node9", "config.json");
5597
+ CREDENTIALS_FILE = import_path16.default.join(homeDir, ".node9", "credentials.json");
5598
+ INSIGHT_COUNTS_FILE = import_path16.default.join(homeDir, ".node9", "insight-counts.json");
4986
5599
  pending = /* @__PURE__ */ new Map();
4987
5600
  sseClients = /* @__PURE__ */ new Set();
5601
+ suggestionTracker = new SuggestionTracker(3);
5602
+ suggestions = /* @__PURE__ */ new Map();
5603
+ taintStore = new TaintStore();
5604
+ insightCounts = /* @__PURE__ */ new Map();
4988
5605
  _abandonTimer = null;
4989
5606
  _hadBrowserClient = false;
4990
5607
  _daemonServer = null;
@@ -4996,17 +5613,75 @@ var init_state2 = __esm({
4996
5613
  "2h": 2 * 60 * 6e4
4997
5614
  };
4998
5615
  autoStarted = process.env.NODE9_AUTO_STARTED === "1";
4999
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path15.default.join(import_os12.default.tmpdir(), "node9-activity.sock");
5616
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path16.default.join(import_os12.default.tmpdir(), "node9-activity.sock");
5000
5617
  ACTIVITY_RING_SIZE = 100;
5001
5618
  activityRing = [];
5002
5619
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
5003
5620
  }
5004
5621
  });
5005
5622
 
5623
+ // src/config/patch.ts
5624
+ function patchConfig(configPath, patch) {
5625
+ let config = {};
5626
+ try {
5627
+ if (import_fs14.default.existsSync(configPath)) {
5628
+ config = JSON.parse(import_fs14.default.readFileSync(configPath, "utf8"));
5629
+ }
5630
+ } catch {
5631
+ throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
5632
+ }
5633
+ if (!config.policy || typeof config.policy !== "object") config.policy = {};
5634
+ const policy = config.policy;
5635
+ if (patch.type === "smartRule") {
5636
+ if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
5637
+ const rules = policy.smartRules;
5638
+ if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
5639
+ rules.push(patch.rule);
5640
+ } else {
5641
+ if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
5642
+ const ignored = policy.ignoredTools;
5643
+ if (!ignored.includes(patch.toolName)) {
5644
+ ignored.push(patch.toolName);
5645
+ }
5646
+ }
5647
+ const dir = import_path17.default.dirname(configPath);
5648
+ import_fs14.default.mkdirSync(dir, { recursive: true });
5649
+ const tmp = configPath + ".node9-tmp";
5650
+ try {
5651
+ import_fs14.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5652
+ } catch (err) {
5653
+ try {
5654
+ import_fs14.default.unlinkSync(tmp);
5655
+ } catch {
5656
+ }
5657
+ throw err;
5658
+ }
5659
+ try {
5660
+ import_fs14.default.renameSync(tmp, configPath);
5661
+ } catch (err) {
5662
+ try {
5663
+ import_fs14.default.unlinkSync(tmp);
5664
+ } catch {
5665
+ }
5666
+ throw err;
5667
+ }
5668
+ }
5669
+ var import_fs14, import_path17, import_os13, GLOBAL_CONFIG_PATH;
5670
+ var init_patch = __esm({
5671
+ "src/config/patch.ts"() {
5672
+ "use strict";
5673
+ import_fs14 = __toESM(require("fs"));
5674
+ import_path17 = __toESM(require("path"));
5675
+ import_os13 = __toESM(require("os"));
5676
+ GLOBAL_CONFIG_PATH = import_path17.default.join(import_os13.default.homedir(), ".node9", "config.json");
5677
+ }
5678
+ });
5679
+
5006
5680
  // src/daemon/server.ts
5007
5681
  function startDaemon() {
5008
- const csrfToken = (0, import_crypto4.randomUUID)();
5009
- const internalToken = (0, import_crypto4.randomUUID)();
5682
+ loadInsightCounts();
5683
+ const csrfToken = (0, import_crypto6.randomUUID)();
5684
+ const internalToken = (0, import_crypto6.randomUUID)();
5010
5685
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
5011
5686
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
5012
5687
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
@@ -5019,7 +5694,7 @@ function startDaemon() {
5019
5694
  idleTimer = setTimeout(() => {
5020
5695
  if (autoStarted) {
5021
5696
  try {
5022
- import_fs13.default.unlinkSync(DAEMON_PID_FILE);
5697
+ import_fs15.default.unlinkSync(DAEMON_PID_FILE);
5023
5698
  } catch {
5024
5699
  }
5025
5700
  }
@@ -5028,8 +5703,14 @@ function startDaemon() {
5028
5703
  idleTimer.unref();
5029
5704
  }
5030
5705
  resetIdleTimer();
5706
+ const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
5031
5707
  const server = import_http.default.createServer(async (req, res) => {
5032
- const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
5708
+ const host = req.headers.host ?? "";
5709
+ if (!allowedHosts.has(host)) {
5710
+ res.writeHead(421, { "Content-Type": "text/plain" });
5711
+ return res.end("Misdirected Request");
5712
+ }
5713
+ const reqUrl = new URL(req.url || "/", `http://${host}`);
5033
5714
  const { pathname } = reqUrl;
5034
5715
  if (req.method === "GET" && pathname === "/") {
5035
5716
  res.writeHead(200, { "Content-Type": "text/html" });
@@ -5062,7 +5743,8 @@ data: ${JSON.stringify({
5062
5743
  slackDelegated: e.slackDelegated,
5063
5744
  timestamp: e.timestamp,
5064
5745
  agent: e.agent,
5065
- mcpServer: e.mcpServer
5746
+ mcpServer: e.mcpServer,
5747
+ allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
5066
5748
  })),
5067
5749
  orgName: getOrgName(),
5068
5750
  autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
@@ -5104,6 +5786,12 @@ data: ${JSON.stringify(item.data)}
5104
5786
  }
5105
5787
  });
5106
5788
  }
5789
+ if (req.method === "POST" && pathname === "/browser-opened") {
5790
+ if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
5791
+ browserOpened = true;
5792
+ res.writeHead(200).end();
5793
+ return;
5794
+ }
5107
5795
  if (req.method === "POST" && pathname === "/check") {
5108
5796
  try {
5109
5797
  resetIdleTimer();
@@ -5121,7 +5809,7 @@ data: ${JSON.stringify(item.data)}
5121
5809
  activityId,
5122
5810
  cwd
5123
5811
  } = JSON.parse(body);
5124
- const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto4.randomUUID)();
5812
+ const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto6.randomUUID)();
5125
5813
  const entry = {
5126
5814
  id,
5127
5815
  toolName,
@@ -5147,7 +5835,7 @@ data: ${JSON.stringify(item.data)}
5147
5835
  e.earlyReason = "No response \u2014 auto-denied after timeout";
5148
5836
  }
5149
5837
  pending.delete(id);
5150
- broadcast("remove", { id });
5838
+ broadcast("remove", { id, decision: "deny" });
5151
5839
  }
5152
5840
  }, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
5153
5841
  };
@@ -5161,7 +5849,7 @@ data: ${JSON.stringify(item.data)}
5161
5849
  status: "pending"
5162
5850
  });
5163
5851
  }
5164
- const projectCwd = typeof cwd === "string" && import_path16.default.isAbsolute(cwd) ? cwd : void 0;
5852
+ const projectCwd = typeof cwd === "string" && import_path18.default.isAbsolute(cwd) ? cwd : void 0;
5165
5853
  const projectConfig = getConfig(projectCwd);
5166
5854
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
5167
5855
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -5174,7 +5862,10 @@ data: ${JSON.stringify(item.data)}
5174
5862
  slackDelegated: entry.slackDelegated,
5175
5863
  agent: entry.agent,
5176
5864
  mcpServer: entry.mcpServer,
5177
- interactive: terminalEnabled
5865
+ interactive: terminalEnabled,
5866
+ // allowCount = what this count will be if the user allows.
5867
+ // Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
5868
+ allowCount: (insightCounts.get(toolName) ?? 0) + 1
5178
5869
  });
5179
5870
  const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
5180
5871
  if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
@@ -5183,7 +5874,7 @@ data: ${JSON.stringify(item.data)}
5183
5874
  }
5184
5875
  }
5185
5876
  res.writeHead(200, { "Content-Type": "application/json" });
5186
- res.end(JSON.stringify({ id }));
5877
+ res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
5187
5878
  if (slackDelegated) return;
5188
5879
  authorizeHeadless(
5189
5880
  toolName,
@@ -5210,7 +5901,7 @@ data: ${JSON.stringify(item.data)}
5210
5901
  if (e.waiter) {
5211
5902
  e.waiter(decision, result.reason);
5212
5903
  pending.delete(id);
5213
- broadcast("remove", { id });
5904
+ broadcast("remove", { id, decision });
5214
5905
  } else {
5215
5906
  e.earlyDecision = decision;
5216
5907
  e.earlyReason = result.reason;
@@ -5226,7 +5917,7 @@ data: ${JSON.stringify(item.data)}
5226
5917
  e.earlyReason = reason;
5227
5918
  }
5228
5919
  pending.delete(id);
5229
- broadcast("remove", { id });
5920
+ broadcast("remove", { id, decision: "deny" });
5230
5921
  });
5231
5922
  return;
5232
5923
  } catch {
@@ -5257,12 +5948,14 @@ data: ${JSON.stringify(item.data)}
5257
5948
  res.end(JSON.stringify(body));
5258
5949
  };
5259
5950
  req.on("close", () => {
5260
- const e = pending.get(id);
5261
- if (e && e.waiter && e.earlyDecision === null) {
5262
- clearTimeout(e.timer);
5263
- pending.delete(id);
5264
- broadcast("remove", { id });
5265
- }
5951
+ setTimeout(() => {
5952
+ const e = pending.get(id);
5953
+ if (e && e.waiter && e.earlyDecision === null) {
5954
+ clearTimeout(e.timer);
5955
+ pending.delete(id);
5956
+ broadcast("remove", { id });
5957
+ }
5958
+ }, 200);
5266
5959
  });
5267
5960
  return;
5268
5961
  }
@@ -5291,10 +5984,10 @@ data: ${JSON.stringify(item.data)}
5291
5984
  if (entry.waiter) {
5292
5985
  entry.waiter("allow");
5293
5986
  pending.delete(id);
5294
- broadcast("remove", { id });
5987
+ broadcast("remove", { id, decision: "allow" });
5295
5988
  } else {
5296
5989
  entry.earlyDecision = "allow";
5297
- broadcast("remove", { id });
5990
+ broadcast("remove", { id, decision: "allow" });
5298
5991
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5299
5992
  }
5300
5993
  res.writeHead(200);
@@ -5308,16 +6001,29 @@ data: ${JSON.stringify(item.data)}
5308
6001
  decision: resolvedDecision
5309
6002
  });
5310
6003
  clearTimeout(entry.timer);
6004
+ if (resolvedDecision === "allow" && !persist) {
6005
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
6006
+ saveInsightCounts();
6007
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
6008
+ if (suggestion) {
6009
+ suggestions.set(suggestion.id, suggestion);
6010
+ broadcast("suggestion:new", suggestion);
6011
+ }
6012
+ } else if (resolvedDecision === "deny") {
6013
+ insightCounts.delete(entry.toolName);
6014
+ saveInsightCounts();
6015
+ suggestionTracker.resetTool(entry.toolName);
6016
+ }
5311
6017
  const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
5312
6018
  if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
5313
6019
  if (entry.waiter) {
5314
6020
  entry.waiter(resolvedDecision, reason);
5315
6021
  pending.delete(id);
5316
- broadcast("remove", { id });
6022
+ broadcast("remove", { id, decision: resolvedDecision });
5317
6023
  } else {
5318
6024
  entry.earlyDecision = resolvedDecision;
5319
6025
  entry.earlyReason = reason;
5320
- broadcast("remove", { id });
6026
+ broadcast("remove", { id, decision: resolvedDecision });
5321
6027
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5322
6028
  }
5323
6029
  res.writeHead(200);
@@ -5405,13 +6111,38 @@ data: ${JSON.stringify(item.data)}
5405
6111
  const id = pathname.split("/").pop();
5406
6112
  const entry = pending.get(id);
5407
6113
  if (!entry) return res.writeHead(404).end();
5408
- const { decision } = JSON.parse(await readBody(req));
5409
- appendAuditLog({ toolName: entry.toolName, args: entry.args, decision });
6114
+ const { decision, source } = JSON.parse(await readBody(req));
6115
+ const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
6116
+ appendAuditLog({
6117
+ toolName: entry.toolName,
6118
+ args: entry.args,
6119
+ decision: resolvedResolveDecision
6120
+ });
5410
6121
  clearTimeout(entry.timer);
5411
- if (entry.waiter) entry.waiter(decision);
5412
- else entry.earlyDecision = decision;
6122
+ if (resolvedResolveDecision === "allow") {
6123
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
6124
+ saveInsightCounts();
6125
+ } else {
6126
+ insightCounts.delete(entry.toolName);
6127
+ saveInsightCounts();
6128
+ }
6129
+ if (!entry.slackDelegated) {
6130
+ if (resolvedResolveDecision === "allow") {
6131
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
6132
+ if (suggestion) {
6133
+ suggestions.set(suggestion.id, suggestion);
6134
+ broadcast("suggestion:new", suggestion);
6135
+ }
6136
+ } else {
6137
+ suggestionTracker.resetTool(entry.toolName);
6138
+ }
6139
+ }
6140
+ const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
6141
+ if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
6142
+ if (entry.waiter) entry.waiter(resolvedResolveDecision);
6143
+ else entry.earlyDecision = resolvedResolveDecision;
5413
6144
  pending.delete(id);
5414
- broadcast("remove", { id });
6145
+ broadcast("remove", { id, decision: resolvedResolveDecision });
5415
6146
  res.writeHead(200);
5416
6147
  return res.end(JSON.stringify({ ok: true }));
5417
6148
  } catch {
@@ -5459,20 +6190,136 @@ data: ${JSON.stringify(item.data)}
5459
6190
  res.writeHead(400).end();
5460
6191
  }
5461
6192
  }
6193
+ if (req.method === "GET" && pathname === "/suggestions") {
6194
+ res.writeHead(200, { "Content-Type": "application/json" });
6195
+ return res.end(JSON.stringify([...suggestions.values()]));
6196
+ }
6197
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
6198
+ if (!validToken(req)) return res.writeHead(403).end();
6199
+ try {
6200
+ const body = await readBody(req);
6201
+ const data = body ? JSON.parse(body) : {};
6202
+ const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
6203
+ const node9Dir = import_path18.default.dirname(GLOBAL_CONFIG_PATH);
6204
+ if (!import_path18.default.resolve(configPath).startsWith(node9Dir + import_path18.default.sep)) {
6205
+ res.writeHead(400, { "Content-Type": "application/json" });
6206
+ return res.end(
6207
+ JSON.stringify({ error: "configPath must be within the node9 config directory" })
6208
+ );
6209
+ }
6210
+ const id = pathname.split("/")[2];
6211
+ const suggestion = suggestions.get(id);
6212
+ if (!suggestion) return res.writeHead(404).end();
6213
+ let patch;
6214
+ if (data.rule !== void 0) {
6215
+ const parsed = SmartRuleSchema.safeParse(data.rule);
6216
+ if (!parsed.success) {
6217
+ res.writeHead(400, { "Content-Type": "application/json" });
6218
+ return res.end(JSON.stringify({ error: parsed.error.message }));
6219
+ }
6220
+ patch = { type: "smartRule", rule: parsed.data };
6221
+ } else {
6222
+ patch = suggestion.suggestedRule;
6223
+ }
6224
+ patchConfig(configPath, patch);
6225
+ _resetConfigCache();
6226
+ insightCounts.delete(suggestion.toolName);
6227
+ saveInsightCounts();
6228
+ suggestion.status = "applied";
6229
+ broadcast("suggestion:resolved", { id, status: "applied" });
6230
+ res.writeHead(200, { "Content-Type": "application/json" });
6231
+ return res.end(JSON.stringify({ ok: true }));
6232
+ } catch (err) {
6233
+ console.error(import_chalk2.default.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
6234
+ res.writeHead(500, { "Content-Type": "application/json" });
6235
+ return res.end(JSON.stringify({ error: String(err) }));
6236
+ }
6237
+ }
6238
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
6239
+ if (!validToken(req)) return res.writeHead(403).end();
6240
+ try {
6241
+ const id = pathname.split("/")[2];
6242
+ const suggestion = suggestions.get(id);
6243
+ if (!suggestion) return res.writeHead(404).end();
6244
+ suggestion.status = "dismissed";
6245
+ broadcast("suggestion:resolved", { id, status: "dismissed" });
6246
+ res.writeHead(200, { "Content-Type": "application/json" });
6247
+ return res.end(JSON.stringify({ ok: true }));
6248
+ } catch {
6249
+ res.writeHead(400).end();
6250
+ }
6251
+ }
6252
+ if (req.method === "POST" && pathname === "/taint") {
6253
+ try {
6254
+ const body = JSON.parse(await readBody(req));
6255
+ if (typeof body.path !== "string" || typeof body.source !== "string") {
6256
+ res.writeHead(400, { "Content-Type": "application/json" });
6257
+ return res.end(JSON.stringify({ error: "path and source are required strings" }));
6258
+ }
6259
+ const ttlMs = typeof body.ttlMs === "number" ? body.ttlMs : void 0;
6260
+ taintStore.taint(body.path, body.source, ttlMs);
6261
+ res.writeHead(200, { "Content-Type": "application/json" });
6262
+ return res.end(JSON.stringify({ ok: true }));
6263
+ } catch {
6264
+ res.writeHead(400).end();
6265
+ return;
6266
+ }
6267
+ }
6268
+ if (req.method === "POST" && pathname === "/taint/check") {
6269
+ try {
6270
+ const body = JSON.parse(await readBody(req));
6271
+ if (!Array.isArray(body.paths)) {
6272
+ res.writeHead(400, { "Content-Type": "application/json" });
6273
+ return res.end(JSON.stringify({ error: "paths must be an array" }));
6274
+ }
6275
+ if (body.paths.some((p) => typeof p !== "string")) {
6276
+ res.writeHead(400, { "Content-Type": "application/json" });
6277
+ return res.end(JSON.stringify({ error: "all paths must be strings" }));
6278
+ }
6279
+ for (const p of body.paths) {
6280
+ const record = taintStore.check(p);
6281
+ if (record) {
6282
+ res.writeHead(200, { "Content-Type": "application/json" });
6283
+ return res.end(JSON.stringify({ tainted: true, record }));
6284
+ }
6285
+ }
6286
+ res.writeHead(200, { "Content-Type": "application/json" });
6287
+ return res.end(JSON.stringify({ tainted: false }));
6288
+ } catch {
6289
+ res.writeHead(400).end();
6290
+ return;
6291
+ }
6292
+ }
6293
+ if (req.method === "POST" && pathname === "/taint/propagate") {
6294
+ try {
6295
+ const body = JSON.parse(await readBody(req));
6296
+ if (typeof body.src !== "string" || typeof body.dest !== "string") {
6297
+ res.writeHead(400, { "Content-Type": "application/json" });
6298
+ return res.end(JSON.stringify({ error: "src and dest are required strings" }));
6299
+ }
6300
+ const clearSource = body.clearSource === true;
6301
+ taintStore.propagate(body.src, body.dest, clearSource);
6302
+ res.writeHead(200, { "Content-Type": "application/json" });
6303
+ return res.end(JSON.stringify({ ok: true }));
6304
+ } catch {
6305
+ res.writeHead(400).end();
6306
+ return;
6307
+ }
6308
+ }
5462
6309
  res.writeHead(404).end();
5463
6310
  });
5464
6311
  setDaemonServer(server);
5465
6312
  server.on("error", (e) => {
5466
6313
  if (e.code === "EADDRINUSE") {
5467
6314
  try {
5468
- if (import_fs13.default.existsSync(DAEMON_PID_FILE)) {
5469
- const { pid } = JSON.parse(import_fs13.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6315
+ if (import_fs15.default.existsSync(DAEMON_PID_FILE)) {
6316
+ const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5470
6317
  process.kill(pid, 0);
5471
6318
  return process.exit(0);
5472
6319
  }
5473
6320
  } catch {
5474
6321
  try {
5475
- import_fs13.default.unlinkSync(DAEMON_PID_FILE);
6322
+ import_fs15.default.unlinkSync(DAEMON_PID_FILE);
5476
6323
  } catch {
5477
6324
  }
5478
6325
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -5531,43 +6378,45 @@ data: ${JSON.stringify(item.data)}
5531
6378
  }
5532
6379
  startActivitySocket();
5533
6380
  }
5534
- var import_http, import_fs13, import_path16, import_crypto4, import_child_process4, import_chalk2;
6381
+ var import_http, import_fs15, import_path18, import_crypto6, import_child_process4, import_chalk2;
5535
6382
  var init_server = __esm({
5536
6383
  "src/daemon/server.ts"() {
5537
6384
  "use strict";
5538
6385
  import_http = __toESM(require("http"));
5539
- import_fs13 = __toESM(require("fs"));
5540
- import_path16 = __toESM(require("path"));
5541
- import_crypto4 = require("crypto");
6386
+ import_fs15 = __toESM(require("fs"));
6387
+ import_path18 = __toESM(require("path"));
6388
+ import_crypto6 = require("crypto");
5542
6389
  import_child_process4 = require("child_process");
5543
6390
  import_chalk2 = __toESM(require("chalk"));
5544
6391
  init_core();
5545
6392
  init_shields();
5546
6393
  init_ui2();
5547
6394
  init_state2();
6395
+ init_patch();
6396
+ init_config_schema();
5548
6397
  }
5549
6398
  });
5550
6399
 
5551
6400
  // src/daemon/index.ts
5552
6401
  function stopDaemon() {
5553
- if (!import_fs14.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
6402
+ if (!import_fs16.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
5554
6403
  try {
5555
- const { pid } = JSON.parse(import_fs14.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6404
+ const { pid } = JSON.parse(import_fs16.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5556
6405
  process.kill(pid, "SIGTERM");
5557
6406
  console.log(import_chalk3.default.green("\u2705 Stopped."));
5558
6407
  } catch {
5559
6408
  console.log(import_chalk3.default.gray("Cleaned up stale PID file."));
5560
6409
  } finally {
5561
6410
  try {
5562
- import_fs14.default.unlinkSync(DAEMON_PID_FILE);
6411
+ import_fs16.default.unlinkSync(DAEMON_PID_FILE);
5563
6412
  } catch {
5564
6413
  }
5565
6414
  }
5566
6415
  }
5567
6416
  function daemonStatus() {
5568
- if (import_fs14.default.existsSync(DAEMON_PID_FILE)) {
6417
+ if (import_fs16.default.existsSync(DAEMON_PID_FILE)) {
5569
6418
  try {
5570
- const { pid } = JSON.parse(import_fs14.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6419
+ const { pid } = JSON.parse(import_fs16.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5571
6420
  process.kill(pid, 0);
5572
6421
  console.log(import_chalk3.default.green("Node9 daemon: running"));
5573
6422
  return;
@@ -5586,11 +6435,11 @@ function daemonStatus() {
5586
6435
  console.log(import_chalk3.default.yellow("Node9 daemon: not running"));
5587
6436
  }
5588
6437
  }
5589
- var import_fs14, import_chalk3, import_child_process5;
6438
+ var import_fs16, import_chalk3, import_child_process5;
5590
6439
  var init_daemon2 = __esm({
5591
6440
  "src/daemon/index.ts"() {
5592
6441
  "use strict";
5593
- import_fs14 = __toESM(require("fs"));
6442
+ import_fs16 = __toESM(require("fs"));
5594
6443
  import_chalk3 = __toESM(require("chalk"));
5595
6444
  import_child_process5 = require("child_process");
5596
6445
  init_server();
@@ -5617,17 +6466,17 @@ function formatBase(activity) {
5617
6466
  const toolName = activity.tool.slice(0, 16).padEnd(16);
5618
6467
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
5619
6468
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
5620
- return `${import_chalk15.default.gray(time)} ${icon} ${import_chalk15.default.white.bold(toolName)} ${import_chalk15.default.dim(argsPreview)}`;
6469
+ return `${import_chalk16.default.gray(time)} ${icon} ${import_chalk16.default.white.bold(toolName)} ${import_chalk16.default.dim(argsPreview)}`;
5621
6470
  }
5622
6471
  function renderResult(activity, result) {
5623
6472
  const base = formatBase(activity);
5624
6473
  let status;
5625
6474
  if (result.status === "allow") {
5626
- status = import_chalk15.default.green("\u2713 ALLOW");
6475
+ status = import_chalk16.default.green("\u2713 ALLOW");
5627
6476
  } else if (result.status === "dlp") {
5628
- status = import_chalk15.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6477
+ status = import_chalk16.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
5629
6478
  } else {
5630
- status = import_chalk15.default.red("\u2717 BLOCK");
6479
+ status = import_chalk16.default.red("\u2717 BLOCK");
5631
6480
  }
5632
6481
  if (process.stdout.isTTY) {
5633
6482
  import_readline3.default.clearLine(process.stdout, 0);
@@ -5637,16 +6486,16 @@ function renderResult(activity, result) {
5637
6486
  }
5638
6487
  function renderPending(activity) {
5639
6488
  if (!process.stdout.isTTY) return;
5640
- process.stdout.write(`${formatBase(activity)} ${import_chalk15.default.yellow("\u25CF \u2026")}\r`);
6489
+ process.stdout.write(`${formatBase(activity)} ${import_chalk16.default.yellow("\u25CF \u2026")}\r`);
5641
6490
  }
5642
6491
  async function ensureDaemon() {
5643
6492
  let pidPort = null;
5644
- if (import_fs21.default.existsSync(PID_FILE)) {
6493
+ if (import_fs24.default.existsSync(PID_FILE)) {
5645
6494
  try {
5646
- const { port } = JSON.parse(import_fs21.default.readFileSync(PID_FILE, "utf-8"));
6495
+ const { port } = JSON.parse(import_fs24.default.readFileSync(PID_FILE, "utf-8"));
5647
6496
  pidPort = port;
5648
6497
  } catch {
5649
- console.error(import_chalk15.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6498
+ console.error(import_chalk16.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
5650
6499
  }
5651
6500
  }
5652
6501
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -5657,7 +6506,7 @@ async function ensureDaemon() {
5657
6506
  if (res.ok) return checkPort;
5658
6507
  } catch {
5659
6508
  }
5660
- console.log(import_chalk15.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6509
+ console.log(import_chalk16.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
5661
6510
  const child = (0, import_child_process13.spawn)(process.execPath, [process.argv[1], "daemon"], {
5662
6511
  detached: true,
5663
6512
  stdio: "ignore",
@@ -5674,12 +6523,15 @@ async function ensureDaemon() {
5674
6523
  } catch {
5675
6524
  }
5676
6525
  }
5677
- console.error(import_chalk15.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6526
+ console.error(import_chalk16.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
5678
6527
  process.exit(1);
5679
6528
  }
5680
- function postDecisionHttp(id, decision, csrfToken, port) {
6529
+ function postDecisionHttp(id, decision, csrfToken, port, opts) {
5681
6530
  return new Promise((resolve, reject) => {
5682
- const body = JSON.stringify({ decision, source: "terminal" });
6531
+ const bodyObj = { decision, source: "terminal" };
6532
+ if (opts?.persist) bodyObj.persist = true;
6533
+ if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
6534
+ const body = JSON.stringify(bodyObj);
5683
6535
  const req = import_http2.default.request(
5684
6536
  {
5685
6537
  hostname: "127.0.0.1",
@@ -5702,22 +6554,33 @@ function postDecisionHttp(id, decision, csrfToken, port) {
5702
6554
  req.end(body);
5703
6555
  });
5704
6556
  }
5705
- function buildCardLines(req) {
6557
+ function buildCardLines(req, localCount = 0) {
5706
6558
  const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
5707
6559
  const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
5708
6560
  const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
5709
6561
  const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
5710
- return [
6562
+ const lines = [
5711
6563
  ``,
5712
6564
  `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
5713
6565
  `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
5714
- `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
5715
- `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`,
6566
+ `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`
6567
+ ];
6568
+ if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
6569
+ lines.push(`${CYAN}\u2551${RESET} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET}`);
6570
+ }
6571
+ lines.push(`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`);
6572
+ if (localCount >= 2) {
6573
+ lines.push(
6574
+ `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
6575
+ );
6576
+ }
6577
+ lines.push(
5716
6578
  `${CYAN}\u255A${RESET}`,
5717
6579
  ``,
5718
- ` ${BOLD}${GREEN}[A]${RESET} Allow ${BOLD}${RED}[D]${RESET} Deny`,
6580
+ ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
5719
6581
  ``
5720
- ];
6582
+ );
6583
+ return lines;
5721
6584
  }
5722
6585
  async function startTail(options = {}) {
5723
6586
  const port = await ensureDaemon();
@@ -5745,7 +6608,7 @@ async function startTail(options = {}) {
5745
6608
  req2.end();
5746
6609
  });
5747
6610
  if (result.ok) {
5748
- console.log(import_chalk15.default.green("\u2713 Flight Recorder buffer cleared."));
6611
+ console.log(import_chalk16.default.green("\u2713 Flight Recorder buffer cleared."));
5749
6612
  } else if (result.code === "ECONNREFUSED") {
5750
6613
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
5751
6614
  } else if (result.code === "ETIMEDOUT") {
@@ -5762,17 +6625,22 @@ async function startTail(options = {}) {
5762
6625
  let cardActive = false;
5763
6626
  let cardLineCount = 0;
5764
6627
  let cancelActiveCard = null;
6628
+ const localAllowCounts = /* @__PURE__ */ new Map();
5765
6629
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
5766
6630
  if (canApprove) import_readline3.default.emitKeypressEvents(process.stdin);
5767
6631
  function clearCard() {
5768
6632
  if (cardLineCount > 0) {
5769
- process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6633
+ import_readline3.default.moveCursor(process.stdout, 0, -cardLineCount);
6634
+ process.stdout.write(ERASE_DOWN);
5770
6635
  cardLineCount = 0;
5771
6636
  }
5772
6637
  }
5773
6638
  function printCard(req2) {
5774
- process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
5775
- const lines = buildCardLines(req2);
6639
+ process.stdout.write(HIDE_CURSOR);
6640
+ const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
6641
+ const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
6642
+ const priorCount = Math.max(daemonPrior, localPrior);
6643
+ const lines = buildCardLines(req2, priorCount);
5776
6644
  for (const line of lines) process.stdout.write(line + "\n");
5777
6645
  cardLineCount = lines.length;
5778
6646
  }
@@ -5800,34 +6668,70 @@ async function startTail(options = {}) {
5800
6668
  process.stdin.pause();
5801
6669
  cancelActiveCard = null;
5802
6670
  };
5803
- const settle = (decision) => {
6671
+ const settle = (action) => {
5804
6672
  if (settled) return;
5805
6673
  settled = true;
5806
6674
  cleanup();
5807
6675
  clearCard();
6676
+ const stampedLines = buildCardLines(
6677
+ req2,
6678
+ Math.max(
6679
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6680
+ localAllowCounts.get(req2.toolName) ?? 0
6681
+ )
6682
+ );
6683
+ const decisionStamp = action === "always-allow" ? import_chalk16.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk16.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
6684
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
6685
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5808
6686
  process.stdout.write(SHOW_CURSOR);
5809
- postDecisionHttp(req2.id, decision, csrfToken, port).catch((err) => {
6687
+ cardLineCount = 0;
6688
+ if (action === "allow" || action === "always-allow" || action === "trust") {
6689
+ localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
6690
+ } else if (action === "deny") {
6691
+ localAllowCounts.delete(req2.toolName);
6692
+ }
6693
+ let httpDecision;
6694
+ let httpOpts;
6695
+ if (action === "always-allow") {
6696
+ httpDecision = "allow";
6697
+ httpOpts = { persist: true };
6698
+ } else if (action === "trust") {
6699
+ httpDecision = "trust";
6700
+ httpOpts = { trustDuration: "30m" };
6701
+ } else {
6702
+ httpDecision = action;
6703
+ }
6704
+ postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
5810
6705
  try {
5811
- import_fs21.default.appendFileSync(
5812
- import_path23.default.join(import_os19.default.homedir(), ".node9", "hook-debug.log"),
6706
+ import_fs24.default.appendFileSync(
6707
+ import_path26.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
5813
6708
  `[tail] POST /decision failed: ${String(err)}
5814
6709
  `
5815
6710
  );
5816
6711
  } catch {
5817
6712
  }
5818
6713
  });
5819
- const decisionLabel = decision === "allow" ? import_chalk15.default.green("\u2713 ALLOWED (terminal)") : import_chalk15.default.red("\u2717 DENIED (terminal)");
5820
- console.log(`${import_chalk15.default.cyan("\u25C6")} ${import_chalk15.default.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
5821
6714
  approvalQueue.shift();
5822
6715
  cardActive = false;
5823
6716
  showNextCard();
5824
6717
  };
5825
- cancelActiveCard = () => {
6718
+ cancelActiveCard = (externalDecision) => {
5826
6719
  if (settled) return;
5827
6720
  settled = true;
5828
6721
  cleanup();
5829
6722
  clearCard();
6723
+ const priorCount = Math.max(
6724
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6725
+ localAllowCounts.get(req2.toolName) ?? 0
6726
+ );
6727
+ const stampedLines = buildCardLines(req2, priorCount);
6728
+ if (externalDecision) {
6729
+ const source = externalDecision === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
6730
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
6731
+ }
6732
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5830
6733
  process.stdout.write(SHOW_CURSOR);
6734
+ cardLineCount = 0;
5831
6735
  approvalQueue.shift();
5832
6736
  cardActive = false;
5833
6737
  showNextCard();
@@ -5835,10 +6739,14 @@ async function startTail(options = {}) {
5835
6739
  process.stdin.resume();
5836
6740
  onKeypress = (_str, key) => {
5837
6741
  const name = key?.name ?? "";
5838
- if (name === "a") {
6742
+ if (name === "y" || name === "return") {
5839
6743
  settle("allow");
5840
- } else if (name === "d" || name === "return" || name === "enter" || key?.ctrl && name === "c") {
6744
+ } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
5841
6745
  settle("deny");
6746
+ } else if (name === "a") {
6747
+ settle("always-allow");
6748
+ } else if (name === "t") {
6749
+ settle("trust");
5842
6750
  }
5843
6751
  };
5844
6752
  process.stdin.on("keypress", onKeypress);
@@ -5851,19 +6759,27 @@ async function startTail(options = {}) {
5851
6759
  else if (process.platform === "win32")
5852
6760
  (0, import_child_process13.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
5853
6761
  else (0, import_child_process13.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
6762
+ const intToken = getInternalToken();
6763
+ fetch(`http://127.0.0.1:${port}/browser-opened`, {
6764
+ method: "POST",
6765
+ headers: intToken ? { "X-Node9-Internal": intToken } : {}
6766
+ }).catch(() => {
6767
+ });
5854
6768
  }
5855
6769
  } catch {
5856
6770
  }
5857
- console.log(import_chalk15.default.cyan.bold(`
5858
- \u{1F6F0}\uFE0F Node9 tail `) + import_chalk15.default.dim(`\u2192 ${dashboardUrl}`));
6771
+ console.log(import_chalk16.default.cyan.bold(`
6772
+ \u{1F6F0}\uFE0F Node9 tail `) + import_chalk16.default.dim(`\u2192 ${dashboardUrl}`));
5859
6773
  if (canApprove) {
5860
- console.log(import_chalk15.default.dim("Interactive approvals enabled. [A] Allow [D] Deny"));
6774
+ console.log(
6775
+ import_chalk16.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
6776
+ );
5861
6777
  }
5862
6778
  if (options.history) {
5863
- console.log(import_chalk15.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
6779
+ console.log(import_chalk16.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
5864
6780
  } else {
5865
6781
  console.log(
5866
- import_chalk15.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
6782
+ import_chalk16.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
5867
6783
  );
5868
6784
  }
5869
6785
  process.on("SIGINT", () => {
@@ -5873,13 +6789,13 @@ async function startTail(options = {}) {
5873
6789
  import_readline3.default.clearLine(process.stdout, 0);
5874
6790
  import_readline3.default.cursorTo(process.stdout, 0);
5875
6791
  }
5876
- console.log(import_chalk15.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
6792
+ console.log(import_chalk16.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
5877
6793
  process.exit(0);
5878
6794
  });
5879
6795
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
5880
6796
  const req = import_http2.default.get(sseUrl, (res) => {
5881
6797
  if (res.statusCode !== 200) {
5882
- console.error(import_chalk15.default.red(`Failed to connect: HTTP ${res.statusCode}`));
6798
+ console.error(import_chalk16.default.red(`Failed to connect: HTTP ${res.statusCode}`));
5883
6799
  process.exit(1);
5884
6800
  }
5885
6801
  let currentEvent = "";
@@ -5909,7 +6825,7 @@ async function startTail(options = {}) {
5909
6825
  import_readline3.default.clearLine(process.stdout, 0);
5910
6826
  import_readline3.default.cursorTo(process.stdout, 0);
5911
6827
  }
5912
- console.log(import_chalk15.default.red("\n\u274C Daemon disconnected."));
6828
+ console.log(import_chalk16.default.red("\n\u274C Daemon disconnected."));
5913
6829
  process.exit(1);
5914
6830
  });
5915
6831
  });
@@ -5950,11 +6866,17 @@ async function startTail(options = {}) {
5950
6866
  }
5951
6867
  if (event === "remove") {
5952
6868
  try {
5953
- const { id } = JSON.parse(rawData);
6869
+ const { id, decision } = JSON.parse(rawData);
5954
6870
  const idx = approvalQueue.findIndex((r) => r.id === id);
5955
6871
  if (idx !== -1) {
5956
6872
  if (idx === 0 && cardActive && cancelActiveCard) {
5957
- cancelActiveCard();
6873
+ const toolName = approvalQueue[0].toolName;
6874
+ if (decision === "allow") {
6875
+ localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
6876
+ } else if (decision === "deny") {
6877
+ localAllowCounts.delete(toolName);
6878
+ }
6879
+ cancelActiveCard(decision);
5958
6880
  } else {
5959
6881
  approvalQueue.splice(idx, 1);
5960
6882
  }
@@ -5989,25 +6911,26 @@ async function startTail(options = {}) {
5989
6911
  }
5990
6912
  req.on("error", (err) => {
5991
6913
  const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
5992
- console.error(import_chalk15.default.red(`
6914
+ console.error(import_chalk16.default.red(`
5993
6915
  \u274C ${msg}`));
5994
6916
  process.exit(1);
5995
6917
  });
5996
6918
  }
5997
- var import_http2, import_chalk15, import_fs21, import_os19, import_path23, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
6919
+ var import_http2, import_chalk16, import_fs24, import_os21, import_path26, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN;
5998
6920
  var init_tail = __esm({
5999
6921
  "src/tui/tail.ts"() {
6000
6922
  "use strict";
6001
6923
  import_http2 = __toESM(require("http"));
6002
- import_chalk15 = __toESM(require("chalk"));
6003
- import_fs21 = __toESM(require("fs"));
6004
- import_os19 = __toESM(require("os"));
6005
- import_path23 = __toESM(require("path"));
6924
+ import_chalk16 = __toESM(require("chalk"));
6925
+ import_fs24 = __toESM(require("fs"));
6926
+ import_os21 = __toESM(require("os"));
6927
+ import_path26 = __toESM(require("path"));
6006
6928
  import_readline3 = __toESM(require("readline"));
6007
6929
  import_child_process13 = require("child_process");
6008
6930
  init_daemon2();
6931
+ init_daemon();
6009
6932
  init_core();
6010
- PID_FILE = import_path23.default.join(import_os19.default.homedir(), ".node9", "daemon.pid");
6933
+ PID_FILE = import_path26.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
6011
6934
  ICONS = {
6012
6935
  bash: "\u{1F4BB}",
6013
6936
  shell: "\u{1F4BB}",
@@ -6035,8 +6958,6 @@ var init_tail = __esm({
6035
6958
  HIDE_CURSOR = "\x1B[?25l";
6036
6959
  SHOW_CURSOR = "\x1B[?25h";
6037
6960
  ERASE_DOWN = "\x1B[J";
6038
- SAVE_CURSOR = "\x1B7";
6039
- RESTORE_CURSOR = "\x1B8";
6040
6961
  }
6041
6962
  });
6042
6963
 
@@ -6355,6 +7276,25 @@ async function setupGemini() {
6355
7276
  printDaemonTip();
6356
7277
  }
6357
7278
  }
7279
+ function detectAgents(homeDir2 = import_os11.default.homedir()) {
7280
+ const exists = (p) => {
7281
+ try {
7282
+ return import_fs11.default.existsSync(p);
7283
+ } catch (err) {
7284
+ const code = err.code;
7285
+ if (code !== "ENOENT") {
7286
+ process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
7287
+ `);
7288
+ }
7289
+ return false;
7290
+ }
7291
+ };
7292
+ return {
7293
+ claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
7294
+ gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
7295
+ cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
7296
+ };
7297
+ }
6358
7298
  async function setupCursor() {
6359
7299
  const homeDir2 = import_os11.default.homedir();
6360
7300
  const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
@@ -6413,10 +7353,10 @@ async function setupCursor() {
6413
7353
 
6414
7354
  // src/cli.ts
6415
7355
  init_daemon2();
6416
- var import_chalk16 = __toESM(require("chalk"));
6417
- var import_fs22 = __toESM(require("fs"));
6418
- var import_path24 = __toESM(require("path"));
6419
- var import_os20 = __toESM(require("os"));
7356
+ var import_chalk17 = __toESM(require("chalk"));
7357
+ var import_fs25 = __toESM(require("fs"));
7358
+ var import_path27 = __toESM(require("path"));
7359
+ var import_os22 = __toESM(require("os"));
6420
7360
  var import_prompts3 = require("@inquirer/prompts");
6421
7361
 
6422
7362
  // src/utils/duration.ts
@@ -6637,9 +7577,9 @@ async function autoStartDaemonAndWait() {
6637
7577
 
6638
7578
  // src/cli/commands/check.ts
6639
7579
  var import_chalk5 = __toESM(require("chalk"));
6640
- var import_fs16 = __toESM(require("fs"));
6641
- var import_path18 = __toESM(require("path"));
6642
- var import_os14 = __toESM(require("os"));
7580
+ var import_fs18 = __toESM(require("fs"));
7581
+ var import_path20 = __toESM(require("path"));
7582
+ var import_os15 = __toESM(require("os"));
6643
7583
  init_orchestrator();
6644
7584
  init_daemon();
6645
7585
  init_config();
@@ -6647,26 +7587,26 @@ init_policy();
6647
7587
 
6648
7588
  // src/undo.ts
6649
7589
  var import_child_process8 = require("child_process");
6650
- var import_crypto5 = __toESM(require("crypto"));
6651
- var import_fs15 = __toESM(require("fs"));
6652
- var import_path17 = __toESM(require("path"));
6653
- var import_os13 = __toESM(require("os"));
6654
- var SNAPSHOT_STACK_PATH = import_path17.default.join(import_os13.default.homedir(), ".node9", "snapshots.json");
6655
- var UNDO_LATEST_PATH = import_path17.default.join(import_os13.default.homedir(), ".node9", "undo_latest.txt");
7590
+ var import_crypto7 = __toESM(require("crypto"));
7591
+ var import_fs17 = __toESM(require("fs"));
7592
+ var import_path19 = __toESM(require("path"));
7593
+ var import_os14 = __toESM(require("os"));
7594
+ var SNAPSHOT_STACK_PATH = import_path19.default.join(import_os14.default.homedir(), ".node9", "snapshots.json");
7595
+ var UNDO_LATEST_PATH = import_path19.default.join(import_os14.default.homedir(), ".node9", "undo_latest.txt");
6656
7596
  var MAX_SNAPSHOTS = 10;
6657
7597
  var GIT_TIMEOUT = 15e3;
6658
7598
  function readStack() {
6659
7599
  try {
6660
- if (import_fs15.default.existsSync(SNAPSHOT_STACK_PATH))
6661
- return JSON.parse(import_fs15.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
7600
+ if (import_fs17.default.existsSync(SNAPSHOT_STACK_PATH))
7601
+ return JSON.parse(import_fs17.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
6662
7602
  } catch {
6663
7603
  }
6664
7604
  return [];
6665
7605
  }
6666
7606
  function writeStack(stack) {
6667
- const dir = import_path17.default.dirname(SNAPSHOT_STACK_PATH);
6668
- if (!import_fs15.default.existsSync(dir)) import_fs15.default.mkdirSync(dir, { recursive: true });
6669
- import_fs15.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7607
+ const dir = import_path19.default.dirname(SNAPSHOT_STACK_PATH);
7608
+ if (!import_fs17.default.existsSync(dir)) import_fs17.default.mkdirSync(dir, { recursive: true });
7609
+ import_fs17.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
6670
7610
  }
6671
7611
  function buildArgsSummary(tool, args) {
6672
7612
  if (!args || typeof args !== "object") return "";
@@ -6682,7 +7622,7 @@ function buildArgsSummary(tool, args) {
6682
7622
  function normalizeCwdForHash(cwd) {
6683
7623
  let normalized;
6684
7624
  try {
6685
- normalized = import_fs15.default.realpathSync(cwd);
7625
+ normalized = import_fs17.default.realpathSync(cwd);
6686
7626
  } catch {
6687
7627
  normalized = cwd;
6688
7628
  }
@@ -6691,17 +7631,17 @@ function normalizeCwdForHash(cwd) {
6691
7631
  return normalized;
6692
7632
  }
6693
7633
  function getShadowRepoDir(cwd) {
6694
- const hash = import_crypto5.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
6695
- return import_path17.default.join(import_os13.default.homedir(), ".node9", "snapshots", hash);
7634
+ const hash = import_crypto7.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
7635
+ return import_path19.default.join(import_os14.default.homedir(), ".node9", "snapshots", hash);
6696
7636
  }
6697
7637
  function cleanOrphanedIndexFiles(shadowDir) {
6698
7638
  try {
6699
7639
  const cutoff = Date.now() - 6e4;
6700
- for (const f of import_fs15.default.readdirSync(shadowDir)) {
7640
+ for (const f of import_fs17.default.readdirSync(shadowDir)) {
6701
7641
  if (f.startsWith("index_")) {
6702
- const fp = import_path17.default.join(shadowDir, f);
7642
+ const fp = import_path19.default.join(shadowDir, f);
6703
7643
  try {
6704
- if (import_fs15.default.statSync(fp).mtimeMs < cutoff) import_fs15.default.unlinkSync(fp);
7644
+ if (import_fs17.default.statSync(fp).mtimeMs < cutoff) import_fs17.default.unlinkSync(fp);
6705
7645
  } catch {
6706
7646
  }
6707
7647
  }
@@ -6713,7 +7653,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
6713
7653
  const hardcoded = [".git", ".node9"];
6714
7654
  const lines = [...hardcoded, ...ignorePaths].join("\n");
6715
7655
  try {
6716
- import_fs15.default.writeFileSync(import_path17.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
7656
+ import_fs17.default.writeFileSync(import_path19.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
6717
7657
  } catch {
6718
7658
  }
6719
7659
  }
@@ -6726,25 +7666,25 @@ function ensureShadowRepo(shadowDir, cwd) {
6726
7666
  timeout: 3e3
6727
7667
  });
6728
7668
  if (check.status === 0) {
6729
- const ptPath = import_path17.default.join(shadowDir, "project-path.txt");
7669
+ const ptPath = import_path19.default.join(shadowDir, "project-path.txt");
6730
7670
  try {
6731
- const stored = import_fs15.default.readFileSync(ptPath, "utf8").trim();
7671
+ const stored = import_fs17.default.readFileSync(ptPath, "utf8").trim();
6732
7672
  if (stored === normalizedCwd) return true;
6733
7673
  if (process.env.NODE9_DEBUG === "1")
6734
7674
  console.error(
6735
7675
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
6736
7676
  );
6737
- import_fs15.default.rmSync(shadowDir, { recursive: true, force: true });
7677
+ import_fs17.default.rmSync(shadowDir, { recursive: true, force: true });
6738
7678
  } catch {
6739
7679
  try {
6740
- import_fs15.default.writeFileSync(ptPath, normalizedCwd, "utf8");
7680
+ import_fs17.default.writeFileSync(ptPath, normalizedCwd, "utf8");
6741
7681
  } catch {
6742
7682
  }
6743
7683
  return true;
6744
7684
  }
6745
7685
  }
6746
7686
  try {
6747
- import_fs15.default.mkdirSync(shadowDir, { recursive: true });
7687
+ import_fs17.default.mkdirSync(shadowDir, { recursive: true });
6748
7688
  } catch {
6749
7689
  }
6750
7690
  const init = (0, import_child_process8.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
@@ -6753,7 +7693,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6753
7693
  console.error("[Node9] git init --bare failed:", init.stderr?.toString());
6754
7694
  return false;
6755
7695
  }
6756
- const configFile = import_path17.default.join(shadowDir, "config");
7696
+ const configFile = import_path19.default.join(shadowDir, "config");
6757
7697
  (0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
6758
7698
  timeout: 3e3
6759
7699
  });
@@ -6761,7 +7701,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6761
7701
  timeout: 3e3
6762
7702
  });
6763
7703
  try {
6764
- import_fs15.default.writeFileSync(import_path17.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
7704
+ import_fs17.default.writeFileSync(import_path19.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
6765
7705
  } catch {
6766
7706
  }
6767
7707
  return true;
@@ -6784,7 +7724,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6784
7724
  const shadowDir = getShadowRepoDir(cwd);
6785
7725
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
6786
7726
  writeShadowExcludes(shadowDir, ignorePaths);
6787
- indexFile = import_path17.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
7727
+ indexFile = import_path19.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
6788
7728
  const shadowEnv = {
6789
7729
  ...process.env,
6790
7730
  GIT_DIR: shadowDir,
@@ -6813,7 +7753,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6813
7753
  const shouldGc = stack.length % 5 === 0;
6814
7754
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
6815
7755
  writeStack(stack);
6816
- import_fs15.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
7756
+ import_fs17.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
6817
7757
  if (shouldGc) {
6818
7758
  (0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
6819
7759
  }
@@ -6824,7 +7764,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6824
7764
  } finally {
6825
7765
  if (indexFile) {
6826
7766
  try {
6827
- import_fs15.default.unlinkSync(indexFile);
7767
+ import_fs17.default.unlinkSync(indexFile);
6828
7768
  } catch {
6829
7769
  }
6830
7770
  }
@@ -6893,9 +7833,9 @@ function applyUndo(hash, cwd) {
6893
7833
  timeout: GIT_TIMEOUT
6894
7834
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
6895
7835
  for (const file of [...tracked, ...untracked]) {
6896
- const fullPath = import_path17.default.join(dir, file);
6897
- if (!snapshotFiles.has(file) && import_fs15.default.existsSync(fullPath)) {
6898
- import_fs15.default.unlinkSync(fullPath);
7836
+ const fullPath = import_path19.default.join(dir, file);
7837
+ if (!snapshotFiles.has(file) && import_fs17.default.existsSync(fullPath)) {
7838
+ import_fs17.default.unlinkSync(fullPath);
6899
7839
  }
6900
7840
  }
6901
7841
  return true;
@@ -6919,9 +7859,9 @@ function registerCheckCommand(program2) {
6919
7859
  } catch (err) {
6920
7860
  const tempConfig = getConfig();
6921
7861
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
6922
- const logPath = import_path18.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7862
+ const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
6923
7863
  const errMsg = err instanceof Error ? err.message : String(err);
6924
- import_fs16.default.appendFileSync(
7864
+ import_fs18.default.appendFileSync(
6925
7865
  logPath,
6926
7866
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
6927
7867
  RAW: ${raw}
@@ -6932,10 +7872,10 @@ RAW: ${raw}
6932
7872
  }
6933
7873
  const config = getConfig(payload.cwd || void 0);
6934
7874
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
6935
- const logPath = import_path18.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
6936
- if (!import_fs16.default.existsSync(import_path18.default.dirname(logPath)))
6937
- import_fs16.default.mkdirSync(import_path18.default.dirname(logPath), { recursive: true });
6938
- import_fs16.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
7875
+ const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7876
+ if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
7877
+ import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
7878
+ import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6939
7879
  `);
6940
7880
  }
6941
7881
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -6948,8 +7888,8 @@ RAW: ${raw}
6948
7888
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
6949
7889
  let ttyFd = null;
6950
7890
  try {
6951
- ttyFd = import_fs16.default.openSync("/dev/tty", "w");
6952
- const writeTty = (line) => import_fs16.default.writeSync(ttyFd, line + "\n");
7891
+ ttyFd = import_fs18.default.openSync("/dev/tty", "w");
7892
+ const writeTty = (line) => import_fs18.default.writeSync(ttyFd, line + "\n");
6953
7893
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
6954
7894
  writeTty(import_chalk5.default.bgRed.white.bold(`
6955
7895
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -6965,7 +7905,7 @@ RAW: ${raw}
6965
7905
  } finally {
6966
7906
  if (ttyFd !== null)
6967
7907
  try {
6968
- import_fs16.default.closeSync(ttyFd);
7908
+ import_fs18.default.closeSync(ttyFd);
6969
7909
  } catch {
6970
7910
  }
6971
7911
  }
@@ -6996,7 +7936,7 @@ RAW: ${raw}
6996
7936
  if (shouldSnapshot(toolName, toolInput, config)) {
6997
7937
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6998
7938
  }
6999
- const safeCwdForAuth = typeof payload.cwd === "string" && import_path18.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7939
+ const safeCwdForAuth = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7000
7940
  const result = await authorizeHeadless(toolName, toolInput, meta, {
7001
7941
  cwd: safeCwdForAuth
7002
7942
  });
@@ -7008,12 +7948,12 @@ RAW: ${raw}
7008
7948
  }
7009
7949
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
7010
7950
  try {
7011
- const tty = import_fs16.default.openSync("/dev/tty", "w");
7012
- import_fs16.default.writeSync(
7951
+ const tty = import_fs18.default.openSync("/dev/tty", "w");
7952
+ import_fs18.default.writeSync(
7013
7953
  tty,
7014
7954
  import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
7015
7955
  );
7016
- import_fs16.default.closeSync(tty);
7956
+ import_fs18.default.closeSync(tty);
7017
7957
  } catch {
7018
7958
  }
7019
7959
  const daemonReady = await autoStartDaemonAndWait();
@@ -7040,9 +7980,9 @@ RAW: ${raw}
7040
7980
  });
7041
7981
  } catch (err) {
7042
7982
  if (process.env.NODE9_DEBUG === "1") {
7043
- const logPath = import_path18.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7983
+ const logPath = import_path20.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7044
7984
  const errMsg = err instanceof Error ? err.message : String(err);
7045
- import_fs16.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7985
+ import_fs18.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7046
7986
  `);
7047
7987
  }
7048
7988
  process.exit(0);
@@ -7076,12 +8016,52 @@ RAW: ${raw}
7076
8016
  }
7077
8017
 
7078
8018
  // src/cli/commands/log.ts
7079
- var import_fs17 = __toESM(require("fs"));
7080
- var import_path19 = __toESM(require("path"));
7081
- var import_os15 = __toESM(require("os"));
8019
+ var import_fs19 = __toESM(require("fs"));
8020
+ var import_path21 = __toESM(require("path"));
8021
+ var import_os16 = __toESM(require("os"));
7082
8022
  init_audit();
7083
8023
  init_config();
7084
8024
  init_policy();
8025
+ init_daemon();
8026
+
8027
+ // src/utils/cp-mv-parser.ts
8028
+ function parseCpMvOp(command) {
8029
+ const trimmed = command.trim();
8030
+ const tokens = trimmed.split(/\s+/);
8031
+ if (tokens.length < 3) return null;
8032
+ const [cmd, ...rest] = tokens;
8033
+ const base = cmd.split("/").pop() ?? cmd;
8034
+ if (base !== "cp" && base !== "mv") return null;
8035
+ const args = [];
8036
+ for (const tok of rest) {
8037
+ if (tok === "--") {
8038
+ args.push(...rest.slice(rest.indexOf("--") + 1));
8039
+ break;
8040
+ }
8041
+ if (tok === "-t" || tok === "--target-directory") return null;
8042
+ if (tok.startsWith("--target-directory=")) return null;
8043
+ if (tok.startsWith("-") && !tok.startsWith("--")) {
8044
+ if (tok.includes("t")) return null;
8045
+ continue;
8046
+ }
8047
+ if (tok.startsWith("--")) {
8048
+ continue;
8049
+ }
8050
+ args.push(tok);
8051
+ }
8052
+ if (args.length !== 2) return null;
8053
+ const [src, dest] = args;
8054
+ if (!src || !dest) return null;
8055
+ if (containsShellMetachar(src) || containsShellMetachar(dest)) return null;
8056
+ return { src, dest, clearSource: base === "mv" };
8057
+ }
8058
+ function containsShellMetachar(token) {
8059
+ if (/[$`{;*?]/.test(token)) return true;
8060
+ if (token.includes("\0")) return true;
8061
+ return false;
8062
+ }
8063
+
8064
+ // src/cli/commands/log.ts
7085
8065
  function sanitize3(value) {
7086
8066
  return value.replace(/[\x00-\x1F\x7F]/g, "");
7087
8067
  }
@@ -7100,11 +8080,20 @@ function registerLogCommand(program2) {
7100
8080
  decision: "allowed",
7101
8081
  source: "post-hook"
7102
8082
  };
7103
- const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "audit.log");
7104
- if (!import_fs17.default.existsSync(import_path19.default.dirname(logPath)))
7105
- import_fs17.default.mkdirSync(import_path19.default.dirname(logPath), { recursive: true });
7106
- import_fs17.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7107
- const safeCwd = typeof payload.cwd === "string" && import_path19.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
8083
+ const logPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "audit.log");
8084
+ if (!import_fs19.default.existsSync(import_path21.default.dirname(logPath)))
8085
+ import_fs19.default.mkdirSync(import_path21.default.dirname(logPath), { recursive: true });
8086
+ import_fs19.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
8087
+ if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
8088
+ const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
8089
+ if (command) {
8090
+ const op = parseCpMvOp(command);
8091
+ if (op) {
8092
+ await notifyTaintPropagate(op.src, op.dest, op.clearSource);
8093
+ }
8094
+ }
8095
+ }
8096
+ const safeCwd = typeof payload.cwd === "string" && import_path21.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7108
8097
  const config = getConfig(safeCwd);
7109
8098
  if (shouldSnapshot(tool, {}, config)) {
7110
8099
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -7113,9 +8102,9 @@ function registerLogCommand(program2) {
7113
8102
  const msg = err instanceof Error ? err.message : String(err);
7114
8103
  process.stderr.write(`[Node9] audit log error: ${msg}
7115
8104
  `);
7116
- const debugPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
8105
+ const debugPath = import_path21.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
7117
8106
  try {
7118
- import_fs17.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
8107
+ import_fs19.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7119
8108
  `);
7120
8109
  } catch {
7121
8110
  }
@@ -7419,14 +8408,14 @@ function registerConfigShowCommand(program2) {
7419
8408
 
7420
8409
  // src/cli/commands/doctor.ts
7421
8410
  var import_chalk7 = __toESM(require("chalk"));
7422
- var import_fs18 = __toESM(require("fs"));
7423
- var import_path20 = __toESM(require("path"));
7424
- var import_os16 = __toESM(require("os"));
8411
+ var import_fs20 = __toESM(require("fs"));
8412
+ var import_path22 = __toESM(require("path"));
8413
+ var import_os17 = __toESM(require("os"));
7425
8414
  var import_child_process9 = require("child_process");
7426
8415
  init_daemon();
7427
8416
  function registerDoctorCommand(program2, version2) {
7428
8417
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
7429
- const homeDir2 = import_os16.default.homedir();
8418
+ const homeDir2 = import_os17.default.homedir();
7430
8419
  let failures = 0;
7431
8420
  function pass(msg) {
7432
8421
  console.log(import_chalk7.default.green(" \u2705 ") + msg);
@@ -7475,10 +8464,10 @@ function registerDoctorCommand(program2, version2) {
7475
8464
  );
7476
8465
  }
7477
8466
  section("Configuration");
7478
- const globalConfigPath = import_path20.default.join(homeDir2, ".node9", "config.json");
7479
- if (import_fs18.default.existsSync(globalConfigPath)) {
8467
+ const globalConfigPath = import_path22.default.join(homeDir2, ".node9", "config.json");
8468
+ if (import_fs20.default.existsSync(globalConfigPath)) {
7480
8469
  try {
7481
- JSON.parse(import_fs18.default.readFileSync(globalConfigPath, "utf-8"));
8470
+ JSON.parse(import_fs20.default.readFileSync(globalConfigPath, "utf-8"));
7482
8471
  pass("~/.node9/config.json found and valid");
7483
8472
  } catch {
7484
8473
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -7486,10 +8475,10 @@ function registerDoctorCommand(program2, version2) {
7486
8475
  } else {
7487
8476
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
7488
8477
  }
7489
- const projectConfigPath = import_path20.default.join(process.cwd(), "node9.config.json");
7490
- if (import_fs18.default.existsSync(projectConfigPath)) {
8478
+ const projectConfigPath = import_path22.default.join(process.cwd(), "node9.config.json");
8479
+ if (import_fs20.default.existsSync(projectConfigPath)) {
7491
8480
  try {
7492
- JSON.parse(import_fs18.default.readFileSync(projectConfigPath, "utf-8"));
8481
+ JSON.parse(import_fs20.default.readFileSync(projectConfigPath, "utf-8"));
7493
8482
  pass("node9.config.json found and valid (project)");
7494
8483
  } catch {
7495
8484
  fail(
@@ -7498,8 +8487,8 @@ function registerDoctorCommand(program2, version2) {
7498
8487
  );
7499
8488
  }
7500
8489
  }
7501
- const credsPath = import_path20.default.join(homeDir2, ".node9", "credentials.json");
7502
- if (import_fs18.default.existsSync(credsPath)) {
8490
+ const credsPath = import_path22.default.join(homeDir2, ".node9", "credentials.json");
8491
+ if (import_fs20.default.existsSync(credsPath)) {
7503
8492
  pass("Cloud credentials found (~/.node9/credentials.json)");
7504
8493
  } else {
7505
8494
  warn(
@@ -7508,10 +8497,10 @@ function registerDoctorCommand(program2, version2) {
7508
8497
  );
7509
8498
  }
7510
8499
  section("Agent Hooks");
7511
- const claudeSettingsPath = import_path20.default.join(homeDir2, ".claude", "settings.json");
7512
- if (import_fs18.default.existsSync(claudeSettingsPath)) {
8500
+ const claudeSettingsPath = import_path22.default.join(homeDir2, ".claude", "settings.json");
8501
+ if (import_fs20.default.existsSync(claudeSettingsPath)) {
7513
8502
  try {
7514
- const cs = JSON.parse(import_fs18.default.readFileSync(claudeSettingsPath, "utf-8"));
8503
+ const cs = JSON.parse(import_fs20.default.readFileSync(claudeSettingsPath, "utf-8"));
7515
8504
  const hasHook = cs.hooks?.PreToolUse?.some(
7516
8505
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7517
8506
  );
@@ -7527,10 +8516,10 @@ function registerDoctorCommand(program2, version2) {
7527
8516
  } else {
7528
8517
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
7529
8518
  }
7530
- const geminiSettingsPath = import_path20.default.join(homeDir2, ".gemini", "settings.json");
7531
- if (import_fs18.default.existsSync(geminiSettingsPath)) {
8519
+ const geminiSettingsPath = import_path22.default.join(homeDir2, ".gemini", "settings.json");
8520
+ if (import_fs20.default.existsSync(geminiSettingsPath)) {
7532
8521
  try {
7533
- const gs = JSON.parse(import_fs18.default.readFileSync(geminiSettingsPath, "utf-8"));
8522
+ const gs = JSON.parse(import_fs20.default.readFileSync(geminiSettingsPath, "utf-8"));
7534
8523
  const hasHook = gs.hooks?.BeforeTool?.some(
7535
8524
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7536
8525
  );
@@ -7546,10 +8535,10 @@ function registerDoctorCommand(program2, version2) {
7546
8535
  } else {
7547
8536
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
7548
8537
  }
7549
- const cursorHooksPath = import_path20.default.join(homeDir2, ".cursor", "hooks.json");
7550
- if (import_fs18.default.existsSync(cursorHooksPath)) {
8538
+ const cursorHooksPath = import_path22.default.join(homeDir2, ".cursor", "hooks.json");
8539
+ if (import_fs20.default.existsSync(cursorHooksPath)) {
7551
8540
  try {
7552
- const cur = JSON.parse(import_fs18.default.readFileSync(cursorHooksPath, "utf-8"));
8541
+ const cur = JSON.parse(import_fs20.default.readFileSync(cursorHooksPath, "utf-8"));
7553
8542
  const hasHook = cur.hooks?.preToolUse?.some(
7554
8543
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
7555
8544
  );
@@ -7587,9 +8576,9 @@ function registerDoctorCommand(program2, version2) {
7587
8576
 
7588
8577
  // src/cli/commands/audit.ts
7589
8578
  var import_chalk8 = __toESM(require("chalk"));
7590
- var import_fs19 = __toESM(require("fs"));
7591
- var import_path21 = __toESM(require("path"));
7592
- var import_os17 = __toESM(require("os"));
8579
+ var import_fs21 = __toESM(require("fs"));
8580
+ var import_path23 = __toESM(require("path"));
8581
+ var import_os18 = __toESM(require("os"));
7593
8582
  function formatRelativeTime(timestamp) {
7594
8583
  const diff = Date.now() - new Date(timestamp).getTime();
7595
8584
  const sec = Math.floor(diff / 1e3);
@@ -7602,14 +8591,14 @@ function formatRelativeTime(timestamp) {
7602
8591
  }
7603
8592
  function registerAuditCommand(program2) {
7604
8593
  program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
7605
- const logPath = import_path21.default.join(import_os17.default.homedir(), ".node9", "audit.log");
7606
- if (!import_fs19.default.existsSync(logPath)) {
8594
+ const logPath = import_path23.default.join(import_os18.default.homedir(), ".node9", "audit.log");
8595
+ if (!import_fs21.default.existsSync(logPath)) {
7607
8596
  console.log(
7608
8597
  import_chalk8.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
7609
8598
  );
7610
8599
  return;
7611
8600
  }
7612
- const raw = import_fs19.default.readFileSync(logPath, "utf-8");
8601
+ const raw = import_fs21.default.readFileSync(logPath, "utf-8");
7613
8602
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
7614
8603
  let entries = lines.flatMap((line) => {
7615
8604
  try {
@@ -7727,11 +8716,44 @@ function registerDaemonCommand(program2) {
7727
8716
 
7728
8717
  // src/cli/commands/status.ts
7729
8718
  var import_chalk10 = __toESM(require("chalk"));
7730
- var import_fs20 = __toESM(require("fs"));
7731
- var import_path22 = __toESM(require("path"));
7732
- var import_os18 = __toESM(require("os"));
8719
+ var import_fs22 = __toESM(require("fs"));
8720
+ var import_path24 = __toESM(require("path"));
8721
+ var import_os19 = __toESM(require("os"));
7733
8722
  init_core();
7734
8723
  init_daemon();
8724
+ function readJson2(filePath) {
8725
+ try {
8726
+ if (import_fs22.default.existsSync(filePath)) return JSON.parse(import_fs22.default.readFileSync(filePath, "utf-8"));
8727
+ } catch {
8728
+ }
8729
+ return null;
8730
+ }
8731
+ function isNode9Hook2(cmd) {
8732
+ if (!cmd) return false;
8733
+ return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
8734
+ }
8735
+ function wrappedMcpServers(servers) {
8736
+ if (!servers) return [];
8737
+ return Object.entries(servers).filter(([, s]) => s.command === "node9" && Array.isArray(s.args) && s.args.length > 0).map(([name, s]) => `${name} \u2192 ${s.args.join(" ")}`);
8738
+ }
8739
+ function printAgentSection(label, hookPairs, wrapped) {
8740
+ console.log(import_chalk10.default.bold(` ${label}`));
8741
+ for (const { name, present } of hookPairs) {
8742
+ if (present) {
8743
+ console.log(import_chalk10.default.green(` \u2713 ${name}`));
8744
+ } else {
8745
+ console.log(import_chalk10.default.red(` \u2717 ${name}`) + import_chalk10.default.gray(" (not wired)"));
8746
+ }
8747
+ }
8748
+ if (wrapped.length > 0) {
8749
+ console.log(import_chalk10.default.cyan(` MCP proxied:`));
8750
+ for (const entry of wrapped) {
8751
+ console.log(import_chalk10.default.gray(` \u2022 ${entry}`));
8752
+ }
8753
+ } else {
8754
+ console.log(import_chalk10.default.gray(` MCP proxied: none`));
8755
+ }
8756
+ }
7735
8757
  function registerStatusCommand(program2) {
7736
8758
  program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
7737
8759
  const creds = getCredentials();
@@ -7766,19 +8788,72 @@ function registerStatusCommand(program2) {
7766
8788
  console.log("");
7767
8789
  const modeLabel = settings.mode === "audit" ? import_chalk10.default.blue("audit") : settings.mode === "strict" ? import_chalk10.default.red("strict") : import_chalk10.default.white("standard");
7768
8790
  console.log(` Mode: ${modeLabel}`);
7769
- const projectConfig = import_path22.default.join(process.cwd(), "node9.config.json");
7770
- const globalConfig = import_path22.default.join(import_os18.default.homedir(), ".node9", "config.json");
8791
+ const projectConfig = import_path24.default.join(process.cwd(), "node9.config.json");
8792
+ const globalConfig = import_path24.default.join(import_os19.default.homedir(), ".node9", "config.json");
7771
8793
  console.log(
7772
- ` Local: ${import_fs20.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
8794
+ ` Local: ${import_fs22.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
7773
8795
  );
7774
8796
  console.log(
7775
- ` Global: ${import_fs20.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
8797
+ ` Global: ${import_fs22.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
7776
8798
  );
7777
8799
  if (mergedConfig.policy.sandboxPaths.length > 0) {
7778
8800
  console.log(
7779
8801
  ` Sandbox: ${import_chalk10.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
7780
8802
  );
7781
8803
  }
8804
+ const homeDir2 = import_os19.default.homedir();
8805
+ const claudeSettings = readJson2(
8806
+ import_path24.default.join(homeDir2, ".claude", "settings.json")
8807
+ );
8808
+ const claudeConfig = readJson2(import_path24.default.join(homeDir2, ".claude.json"));
8809
+ const geminiSettings = readJson2(
8810
+ import_path24.default.join(homeDir2, ".gemini", "settings.json")
8811
+ );
8812
+ const cursorConfig = readJson2(import_path24.default.join(homeDir2, ".cursor", "mcp.json"));
8813
+ const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
8814
+ if (agentFound) {
8815
+ console.log("");
8816
+ console.log(import_chalk10.default.bold(" Agent Wiring:"));
8817
+ console.log("");
8818
+ if (claudeSettings || claudeConfig) {
8819
+ const preHook = claudeSettings?.hooks?.PreToolUse?.some(
8820
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8821
+ ) ?? false;
8822
+ const postHook = claudeSettings?.hooks?.PostToolUse?.some(
8823
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8824
+ ) ?? false;
8825
+ printAgentSection(
8826
+ "Claude Code",
8827
+ [
8828
+ { name: "PreToolUse (node9 check)", present: preHook },
8829
+ { name: "PostToolUse (node9 log)", present: postHook }
8830
+ ],
8831
+ wrappedMcpServers(claudeConfig?.mcpServers)
8832
+ );
8833
+ console.log("");
8834
+ }
8835
+ if (geminiSettings) {
8836
+ const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
8837
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8838
+ ) ?? false;
8839
+ const afterHook = geminiSettings.hooks?.AfterTool?.some(
8840
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8841
+ ) ?? false;
8842
+ printAgentSection(
8843
+ "Gemini CLI",
8844
+ [
8845
+ { name: "BeforeTool (node9 check)", present: beforeHook },
8846
+ { name: "AfterTool (node9 log)", present: afterHook }
8847
+ ],
8848
+ wrappedMcpServers(geminiSettings.mcpServers)
8849
+ );
8850
+ console.log("");
8851
+ }
8852
+ if (cursorConfig) {
8853
+ printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
8854
+ console.log("");
8855
+ }
8856
+ }
7782
8857
  const pauseState = checkPause();
7783
8858
  if (pauseState.paused) {
7784
8859
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
@@ -7791,8 +8866,63 @@ function registerStatusCommand(program2) {
7791
8866
  });
7792
8867
  }
7793
8868
 
7794
- // src/cli/commands/undo.ts
8869
+ // src/cli/commands/init.ts
7795
8870
  var import_chalk11 = __toESM(require("chalk"));
8871
+ var import_fs23 = __toESM(require("fs"));
8872
+ var import_path25 = __toESM(require("path"));
8873
+ var import_os20 = __toESM(require("os"));
8874
+ init_core();
8875
+ function registerInitCommand(program2) {
8876
+ 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) => {
8877
+ console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
8878
+ const configPath = import_path25.default.join(import_os20.default.homedir(), ".node9", "config.json");
8879
+ if (import_fs23.default.existsSync(configPath) && !options.force) {
8880
+ console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8881
+ } else {
8882
+ const requestedMode = options.mode.toLowerCase();
8883
+ const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8884
+ const configToSave = {
8885
+ ...DEFAULT_CONFIG,
8886
+ settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
8887
+ };
8888
+ const dir = import_path25.default.dirname(configPath);
8889
+ if (!import_fs23.default.existsSync(dir)) import_fs23.default.mkdirSync(dir, { recursive: true });
8890
+ import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8891
+ console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
8892
+ console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
8893
+ }
8894
+ if (options.skipSetup) return;
8895
+ console.log("");
8896
+ const detected = detectAgents();
8897
+ const found = Object.keys(detected).filter(
8898
+ (k) => detected[k]
8899
+ );
8900
+ if (found.length === 0) {
8901
+ console.log(
8902
+ import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
8903
+ );
8904
+ console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
8905
+ return;
8906
+ }
8907
+ console.log(import_chalk11.default.bold("Detected agents:"));
8908
+ for (const agent of found) {
8909
+ console.log(import_chalk11.default.green(` \u2713 ${agent}`));
8910
+ }
8911
+ console.log("");
8912
+ for (const agent of found) {
8913
+ console.log(import_chalk11.default.bold(`Wiring ${agent}...`));
8914
+ if (agent === "claude") await setupClaude();
8915
+ else if (agent === "gemini") await setupGemini();
8916
+ else if (agent === "cursor") await setupCursor();
8917
+ console.log("");
8918
+ }
8919
+ console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
8920
+ console.log(import_chalk11.default.gray(" Run: node9 daemon start"));
8921
+ });
8922
+ }
8923
+
8924
+ // src/cli/commands/undo.ts
8925
+ var import_chalk12 = __toESM(require("chalk"));
7796
8926
  var import_prompts2 = require("@inquirer/prompts");
7797
8927
  function registerUndoCommand(program2) {
7798
8928
  program2.command("undo").description(
@@ -7804,22 +8934,22 @@ function registerUndoCommand(program2) {
7804
8934
  if (history.length === 0) {
7805
8935
  if (!options.all && allHistory.length > 0) {
7806
8936
  console.log(
7807
- import_chalk11.default.yellow(
8937
+ import_chalk12.default.yellow(
7808
8938
  `
7809
8939
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
7810
- Run ${import_chalk11.default.cyan("node9 undo --all")} to see snapshots from all projects.
8940
+ Run ${import_chalk12.default.cyan("node9 undo --all")} to see snapshots from all projects.
7811
8941
  `
7812
8942
  )
7813
8943
  );
7814
8944
  } else {
7815
- console.log(import_chalk11.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
8945
+ console.log(import_chalk12.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
7816
8946
  }
7817
8947
  return;
7818
8948
  }
7819
8949
  const idx = history.length - steps;
7820
8950
  if (idx < 0) {
7821
8951
  console.log(
7822
- import_chalk11.default.yellow(
8952
+ import_chalk12.default.yellow(
7823
8953
  `
7824
8954
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
7825
8955
  `
@@ -7831,19 +8961,19 @@ function registerUndoCommand(program2) {
7831
8961
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
7832
8962
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
7833
8963
  console.log(
7834
- import_chalk11.default.magenta.bold(`
8964
+ import_chalk12.default.magenta.bold(`
7835
8965
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
7836
8966
  );
7837
8967
  console.log(
7838
- import_chalk11.default.white(
7839
- ` Tool: ${import_chalk11.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk11.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
8968
+ import_chalk12.default.white(
8969
+ ` Tool: ${import_chalk12.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk12.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
7840
8970
  )
7841
8971
  );
7842
- console.log(import_chalk11.default.white(` When: ${import_chalk11.default.gray(ageStr)}`));
7843
- console.log(import_chalk11.default.white(` Dir: ${import_chalk11.default.gray(snapshot.cwd)}`));
8972
+ console.log(import_chalk12.default.white(` When: ${import_chalk12.default.gray(ageStr)}`));
8973
+ console.log(import_chalk12.default.white(` Dir: ${import_chalk12.default.gray(snapshot.cwd)}`));
7844
8974
  if (steps > 1)
7845
8975
  console.log(
7846
- import_chalk11.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
8976
+ import_chalk12.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
7847
8977
  );
7848
8978
  console.log("");
7849
8979
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -7851,21 +8981,21 @@ function registerUndoCommand(program2) {
7851
8981
  const lines = diff.split("\n");
7852
8982
  for (const line of lines) {
7853
8983
  if (line.startsWith("+++") || line.startsWith("---")) {
7854
- console.log(import_chalk11.default.bold(line));
8984
+ console.log(import_chalk12.default.bold(line));
7855
8985
  } else if (line.startsWith("+")) {
7856
- console.log(import_chalk11.default.green(line));
8986
+ console.log(import_chalk12.default.green(line));
7857
8987
  } else if (line.startsWith("-")) {
7858
- console.log(import_chalk11.default.red(line));
8988
+ console.log(import_chalk12.default.red(line));
7859
8989
  } else if (line.startsWith("@@")) {
7860
- console.log(import_chalk11.default.cyan(line));
8990
+ console.log(import_chalk12.default.cyan(line));
7861
8991
  } else {
7862
- console.log(import_chalk11.default.gray(line));
8992
+ console.log(import_chalk12.default.gray(line));
7863
8993
  }
7864
8994
  }
7865
8995
  console.log("");
7866
8996
  } else {
7867
8997
  console.log(
7868
- import_chalk11.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
8998
+ import_chalk12.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
7869
8999
  );
7870
9000
  }
7871
9001
  const proceed = await (0, import_prompts2.confirm)({
@@ -7874,18 +9004,18 @@ function registerUndoCommand(program2) {
7874
9004
  });
7875
9005
  if (proceed) {
7876
9006
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
7877
- console.log(import_chalk11.default.green("\n\u2705 Reverted successfully.\n"));
9007
+ console.log(import_chalk12.default.green("\n\u2705 Reverted successfully.\n"));
7878
9008
  } else {
7879
- console.error(import_chalk11.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
9009
+ console.error(import_chalk12.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
7880
9010
  }
7881
9011
  } else {
7882
- console.log(import_chalk11.default.gray("\nCancelled.\n"));
9012
+ console.log(import_chalk12.default.gray("\nCancelled.\n"));
7883
9013
  }
7884
9014
  });
7885
9015
  }
7886
9016
 
7887
9017
  // src/cli/commands/watch.ts
7888
- var import_chalk12 = __toESM(require("chalk"));
9018
+ var import_chalk13 = __toESM(require("chalk"));
7889
9019
  var import_child_process11 = require("child_process");
7890
9020
  init_daemon();
7891
9021
  function registerWatchCommand(program2) {
@@ -7902,7 +9032,7 @@ function registerWatchCommand(program2) {
7902
9032
  throw new Error("not running");
7903
9033
  }
7904
9034
  } catch {
7905
- console.error(import_chalk12.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
9035
+ console.error(import_chalk13.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
7906
9036
  const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
7907
9037
  detached: true,
7908
9038
  stdio: "ignore",
@@ -7924,12 +9054,12 @@ function registerWatchCommand(program2) {
7924
9054
  }
7925
9055
  }
7926
9056
  if (!ready) {
7927
- console.error(import_chalk12.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
9057
+ console.error(import_chalk13.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7928
9058
  process.exit(1);
7929
9059
  }
7930
9060
  }
7931
9061
  console.error(
7932
- import_chalk12.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk12.default.dim(` \u2192 localhost:${port}`) + import_chalk12.default.dim(
9062
+ import_chalk13.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk13.default.dim(` \u2192 localhost:${port}`) + import_chalk13.default.dim(
7933
9063
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
7934
9064
  )
7935
9065
  );
@@ -7938,7 +9068,7 @@ function registerWatchCommand(program2) {
7938
9068
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
7939
9069
  });
7940
9070
  if (result.error) {
7941
- console.error(import_chalk12.default.red(`\u274C Failed to run command: ${result.error.message}`));
9071
+ console.error(import_chalk13.default.red(`\u274C Failed to run command: ${result.error.message}`));
7942
9072
  process.exit(1);
7943
9073
  }
7944
9074
  process.exit(result.status ?? 0);
@@ -7947,7 +9077,7 @@ function registerWatchCommand(program2) {
7947
9077
 
7948
9078
  // src/mcp-gateway/index.ts
7949
9079
  var import_readline2 = __toESM(require("readline"));
7950
- var import_chalk13 = __toESM(require("chalk"));
9080
+ var import_chalk14 = __toESM(require("chalk"));
7951
9081
  var import_child_process12 = require("child_process");
7952
9082
  var import_execa3 = require("execa");
7953
9083
  init_orchestrator();
@@ -8011,13 +9141,13 @@ async function runMcpGateway(upstreamCommand) {
8011
9141
  const prov = checkProvenance(executable);
8012
9142
  if (prov.trustLevel === "suspect") {
8013
9143
  console.error(
8014
- import_chalk13.default.red(
9144
+ import_chalk14.default.red(
8015
9145
  `\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
8016
9146
  )
8017
9147
  );
8018
- console.error(import_chalk13.default.red(" Verify this binary is trusted before proceeding."));
9148
+ console.error(import_chalk14.default.red(" Verify this binary is trusted before proceeding."));
8019
9149
  }
8020
- console.error(import_chalk13.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
9150
+ console.error(import_chalk14.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
8021
9151
  const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
8022
9152
  "NODE_OPTIONS",
8023
9153
  "NODE_PATH",
@@ -8081,10 +9211,10 @@ async function runMcpGateway(upstreamCommand) {
8081
9211
  mcpServer
8082
9212
  });
8083
9213
  if (!result.approved) {
8084
- console.error(import_chalk13.default.red(`
9214
+ console.error(import_chalk14.default.red(`
8085
9215
  \u{1F6D1} Node9 MCP Gateway: Action Blocked`));
8086
- console.error(import_chalk13.default.gray(` Tool: ${toolName}`));
8087
- console.error(import_chalk13.default.gray(` Reason: ${result.reason ?? "Security Policy"}
9216
+ console.error(import_chalk14.default.gray(` Tool: ${toolName}`));
9217
+ console.error(import_chalk14.default.gray(` Reason: ${result.reason ?? "Security Policy"}
8088
9218
  `));
8089
9219
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
8090
9220
  const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -8157,7 +9287,7 @@ function registerMcpGatewayCommand(program2) {
8157
9287
  }
8158
9288
 
8159
9289
  // src/cli/commands/trust.ts
8160
- var import_chalk14 = __toESM(require("chalk"));
9290
+ var import_chalk15 = __toESM(require("chalk"));
8161
9291
  init_trusted_hosts();
8162
9292
  function isValidHost(host) {
8163
9293
  return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
@@ -8168,44 +9298,44 @@ function registerTrustCommand(program2) {
8168
9298
  const normalized = normalizeHost(host.trim());
8169
9299
  if (!isValidHost(normalized)) {
8170
9300
  console.error(
8171
- import_chalk14.default.red(`
9301
+ import_chalk15.default.red(`
8172
9302
  \u274C Invalid host: "${host}"
8173
- `) + import_chalk14.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
9303
+ `) + import_chalk15.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
8174
9304
  );
8175
9305
  process.exit(1);
8176
9306
  }
8177
9307
  addTrustedHost(normalized);
8178
- console.log(import_chalk14.default.green(`
9308
+ console.log(import_chalk15.default.green(`
8179
9309
  \u2705 ${normalized} added to trusted hosts.`));
8180
9310
  console.log(
8181
- import_chalk14.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
9311
+ import_chalk15.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
8182
9312
  );
8183
9313
  });
8184
9314
  trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
8185
9315
  const normalized = normalizeHost(host.trim());
8186
9316
  const removed = removeTrustedHost(normalized);
8187
9317
  if (!removed) {
8188
- console.error(import_chalk14.default.yellow(`
9318
+ console.error(import_chalk15.default.yellow(`
8189
9319
  \u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
8190
9320
  `));
8191
9321
  process.exit(1);
8192
9322
  }
8193
- console.log(import_chalk14.default.green(`
9323
+ console.log(import_chalk15.default.green(`
8194
9324
  \u2705 ${normalized} removed from trusted hosts.
8195
9325
  `));
8196
9326
  });
8197
9327
  trustCmd.command("list").description("Show all trusted hosts").action(() => {
8198
9328
  const hosts = readTrustedHosts();
8199
9329
  if (hosts.length === 0) {
8200
- console.log(import_chalk14.default.gray("\n No trusted hosts configured.\n"));
8201
- console.log(` Add one: ${import_chalk14.default.cyan("node9 trust add api.mycompany.com")}
9330
+ console.log(import_chalk15.default.gray("\n No trusted hosts configured.\n"));
9331
+ console.log(` Add one: ${import_chalk15.default.cyan("node9 trust add api.mycompany.com")}
8202
9332
  `);
8203
9333
  return;
8204
9334
  }
8205
- console.log(import_chalk14.default.bold("\n\u{1F513} Trusted Hosts\n"));
9335
+ console.log(import_chalk15.default.bold("\n\u{1F513} Trusted Hosts\n"));
8206
9336
  for (const entry of hosts) {
8207
9337
  const date = new Date(entry.addedAt).toLocaleDateString();
8208
- console.log(` ${import_chalk14.default.cyan(entry.host.padEnd(40))} ${import_chalk14.default.gray(`added ${date}`)}`);
9338
+ console.log(` ${import_chalk15.default.cyan(entry.host.padEnd(40))} ${import_chalk15.default.gray(`added ${date}`)}`);
8209
9339
  }
8210
9340
  console.log("");
8211
9341
  });
@@ -8213,20 +9343,20 @@ function registerTrustCommand(program2) {
8213
9343
 
8214
9344
  // src/cli.ts
8215
9345
  var { version } = JSON.parse(
8216
- import_fs22.default.readFileSync(import_path24.default.join(__dirname, "../package.json"), "utf-8")
9346
+ import_fs25.default.readFileSync(import_path27.default.join(__dirname, "../package.json"), "utf-8")
8217
9347
  );
8218
9348
  var program = new import_commander.Command();
8219
9349
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
8220
9350
  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) => {
8221
9351
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
8222
- const credPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "credentials.json");
8223
- if (!import_fs22.default.existsSync(import_path24.default.dirname(credPath)))
8224
- import_fs22.default.mkdirSync(import_path24.default.dirname(credPath), { recursive: true });
9352
+ const credPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
9353
+ if (!import_fs25.default.existsSync(import_path27.default.dirname(credPath)))
9354
+ import_fs25.default.mkdirSync(import_path27.default.dirname(credPath), { recursive: true });
8225
9355
  const profileName = options.profile || "default";
8226
9356
  let existingCreds = {};
8227
9357
  try {
8228
- if (import_fs22.default.existsSync(credPath)) {
8229
- const raw = JSON.parse(import_fs22.default.readFileSync(credPath, "utf-8"));
9358
+ if (import_fs25.default.existsSync(credPath)) {
9359
+ const raw = JSON.parse(import_fs25.default.readFileSync(credPath, "utf-8"));
8230
9360
  if (raw.apiKey) {
8231
9361
  existingCreds = {
8232
9362
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -8238,13 +9368,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8238
9368
  } catch {
8239
9369
  }
8240
9370
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
8241
- import_fs22.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
9371
+ import_fs25.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
8242
9372
  if (profileName === "default") {
8243
- const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
9373
+ const configPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "config.json");
8244
9374
  let config = {};
8245
9375
  try {
8246
- if (import_fs22.default.existsSync(configPath))
8247
- config = JSON.parse(import_fs22.default.readFileSync(configPath, "utf-8"));
9376
+ if (import_fs25.default.existsSync(configPath))
9377
+ config = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
8248
9378
  } catch {
8249
9379
  }
8250
9380
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -8259,36 +9389,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8259
9389
  approvers.cloud = false;
8260
9390
  }
8261
9391
  s.approvers = approvers;
8262
- if (!import_fs22.default.existsSync(import_path24.default.dirname(configPath)))
8263
- import_fs22.default.mkdirSync(import_path24.default.dirname(configPath), { recursive: true });
8264
- import_fs22.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
9392
+ if (!import_fs25.default.existsSync(import_path27.default.dirname(configPath)))
9393
+ import_fs25.default.mkdirSync(import_path27.default.dirname(configPath), { recursive: true });
9394
+ import_fs25.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
8265
9395
  }
8266
9396
  if (options.profile && profileName !== "default") {
8267
- console.log(import_chalk16.default.green(`\u2705 Profile "${profileName}" saved`));
8268
- console.log(import_chalk16.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
9397
+ console.log(import_chalk17.default.green(`\u2705 Profile "${profileName}" saved`));
9398
+ console.log(import_chalk17.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
8269
9399
  } else if (options.local) {
8270
- console.log(import_chalk16.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
8271
- console.log(import_chalk16.default.gray(` All decisions stay on this machine.`));
9400
+ console.log(import_chalk17.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
9401
+ console.log(import_chalk17.default.gray(` All decisions stay on this machine.`));
8272
9402
  } else {
8273
- console.log(import_chalk16.default.green(`\u2705 Logged in \u2014 agent mode`));
8274
- console.log(import_chalk16.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
9403
+ console.log(import_chalk17.default.green(`\u2705 Logged in \u2014 agent mode`));
9404
+ console.log(import_chalk17.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
8275
9405
  }
8276
9406
  });
8277
9407
  program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
8278
9408
  if (target === "gemini") return await setupGemini();
8279
9409
  if (target === "claude") return await setupClaude();
8280
9410
  if (target === "cursor") return await setupCursor();
8281
- console.error(import_chalk16.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9411
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8282
9412
  process.exit(1);
8283
9413
  });
8284
9414
  program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
8285
9415
  if (!target) {
8286
- console.log(import_chalk16.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
8287
- console.log(" Usage: " + import_chalk16.default.white("node9 setup <target>") + "\n");
9416
+ console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
9417
+ console.log(" Usage: " + import_chalk17.default.white("node9 setup <target>") + "\n");
8288
9418
  console.log(" Targets:");
8289
- console.log(" " + import_chalk16.default.green("claude") + " \u2014 Claude Code (hook mode)");
8290
- console.log(" " + import_chalk16.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
8291
- console.log(" " + import_chalk16.default.green("cursor") + " \u2014 Cursor (hook mode)");
9419
+ console.log(" " + import_chalk17.default.green("claude") + " \u2014 Claude Code (hook mode)");
9420
+ console.log(" " + import_chalk17.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
9421
+ console.log(" " + import_chalk17.default.green("cursor") + " \u2014 Cursor (hook mode)");
8292
9422
  console.log("");
8293
9423
  return;
8294
9424
  }
@@ -8296,7 +9426,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
8296
9426
  if (t === "gemini") return await setupGemini();
8297
9427
  if (t === "claude") return await setupClaude();
8298
9428
  if (t === "cursor") return await setupCursor();
8299
- console.error(import_chalk16.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9429
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8300
9430
  process.exit(1);
8301
9431
  });
8302
9432
  program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
@@ -8305,30 +9435,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
8305
9435
  else if (target === "gemini") fn = teardownGemini;
8306
9436
  else if (target === "cursor") fn = teardownCursor;
8307
9437
  else {
8308
- console.error(import_chalk16.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9438
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8309
9439
  process.exit(1);
8310
9440
  }
8311
- console.log(import_chalk16.default.cyan(`
9441
+ console.log(import_chalk17.default.cyan(`
8312
9442
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
8313
9443
  `));
8314
9444
  try {
8315
9445
  fn();
8316
9446
  } catch (err) {
8317
- console.error(import_chalk16.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
9447
+ console.error(import_chalk17.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
8318
9448
  process.exit(1);
8319
9449
  }
8320
- console.log(import_chalk16.default.gray("\n Restart the agent for changes to take effect."));
9450
+ console.log(import_chalk17.default.gray("\n Restart the agent for changes to take effect."));
8321
9451
  });
8322
9452
  program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
8323
- console.log(import_chalk16.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
8324
- console.log(import_chalk16.default.bold("Stopping daemon..."));
9453
+ console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
9454
+ console.log(import_chalk17.default.bold("Stopping daemon..."));
8325
9455
  try {
8326
9456
  stopDaemon();
8327
- console.log(import_chalk16.default.green(" \u2705 Daemon stopped"));
9457
+ console.log(import_chalk17.default.green(" \u2705 Daemon stopped"));
8328
9458
  } catch {
8329
- console.log(import_chalk16.default.blue(" \u2139\uFE0F Daemon was not running"));
9459
+ console.log(import_chalk17.default.blue(" \u2139\uFE0F Daemon was not running"));
8330
9460
  }
8331
- console.log(import_chalk16.default.bold("\nRemoving hooks..."));
9461
+ console.log(import_chalk17.default.bold("\nRemoving hooks..."));
8332
9462
  let teardownFailed = false;
8333
9463
  for (const [label, fn] of [
8334
9464
  ["Claude", teardownClaude],
@@ -8340,45 +9470,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
8340
9470
  } catch (err) {
8341
9471
  teardownFailed = true;
8342
9472
  console.error(
8343
- import_chalk16.default.red(
9473
+ import_chalk17.default.red(
8344
9474
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
8345
9475
  )
8346
9476
  );
8347
9477
  }
8348
9478
  }
8349
9479
  if (options.purge) {
8350
- const node9Dir = import_path24.default.join(import_os20.default.homedir(), ".node9");
8351
- if (import_fs22.default.existsSync(node9Dir)) {
9480
+ const node9Dir = import_path27.default.join(import_os22.default.homedir(), ".node9");
9481
+ if (import_fs25.default.existsSync(node9Dir)) {
8352
9482
  const confirmed = await (0, import_prompts3.confirm)({
8353
9483
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
8354
9484
  default: false
8355
9485
  });
8356
9486
  if (confirmed) {
8357
- import_fs22.default.rmSync(node9Dir, { recursive: true });
8358
- if (import_fs22.default.existsSync(node9Dir)) {
9487
+ import_fs25.default.rmSync(node9Dir, { recursive: true });
9488
+ if (import_fs25.default.existsSync(node9Dir)) {
8359
9489
  console.error(
8360
- import_chalk16.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9490
+ import_chalk17.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
8361
9491
  );
8362
9492
  } else {
8363
- console.log(import_chalk16.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
9493
+ console.log(import_chalk17.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
8364
9494
  }
8365
9495
  } else {
8366
- console.log(import_chalk16.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
9496
+ console.log(import_chalk17.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
8367
9497
  }
8368
9498
  } else {
8369
- console.log(import_chalk16.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
9499
+ console.log(import_chalk17.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
8370
9500
  }
8371
9501
  } else {
8372
9502
  console.log(
8373
- import_chalk16.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
9503
+ import_chalk17.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
8374
9504
  );
8375
9505
  }
8376
9506
  if (teardownFailed) {
8377
- console.error(import_chalk16.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
9507
+ console.error(import_chalk17.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
8378
9508
  process.exit(1);
8379
9509
  }
8380
- console.log(import_chalk16.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
8381
- console.log(import_chalk16.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
9510
+ console.log(import_chalk17.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
9511
+ console.log(import_chalk17.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
8382
9512
  });
8383
9513
  registerDoctorCommand(program, version);
8384
9514
  program.command("explain").description(
@@ -8391,7 +9521,7 @@ program.command("explain").description(
8391
9521
  try {
8392
9522
  args = JSON.parse(trimmed);
8393
9523
  } catch {
8394
- console.error(import_chalk16.default.red(`
9524
+ console.error(import_chalk17.default.red(`
8395
9525
  \u274C Invalid JSON: ${trimmed}
8396
9526
  `));
8397
9527
  process.exit(1);
@@ -8402,83 +9532,59 @@ program.command("explain").description(
8402
9532
  }
8403
9533
  const result = await explainPolicy(tool, args);
8404
9534
  console.log("");
8405
- console.log(import_chalk16.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
9535
+ console.log(import_chalk17.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
8406
9536
  console.log("");
8407
- console.log(` ${import_chalk16.default.bold("Tool:")} ${import_chalk16.default.white(result.tool)}`);
9537
+ console.log(` ${import_chalk17.default.bold("Tool:")} ${import_chalk17.default.white(result.tool)}`);
8408
9538
  if (argsRaw) {
8409
9539
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
8410
- console.log(` ${import_chalk16.default.bold("Input:")} ${import_chalk16.default.gray(preview)}`);
9540
+ console.log(` ${import_chalk17.default.bold("Input:")} ${import_chalk17.default.gray(preview)}`);
8411
9541
  }
8412
9542
  console.log("");
8413
- console.log(import_chalk16.default.bold("Config Sources (Waterfall):"));
9543
+ console.log(import_chalk17.default.bold("Config Sources (Waterfall):"));
8414
9544
  for (const tier of result.waterfall) {
8415
- const num = import_chalk16.default.gray(` ${tier.tier}.`);
9545
+ const num = import_chalk17.default.gray(` ${tier.tier}.`);
8416
9546
  const label = tier.label.padEnd(16);
8417
9547
  let statusStr;
8418
9548
  if (tier.tier === 1) {
8419
- statusStr = import_chalk16.default.gray(tier.note ?? "");
9549
+ statusStr = import_chalk17.default.gray(tier.note ?? "");
8420
9550
  } else if (tier.status === "active") {
8421
- const loc = tier.path ? import_chalk16.default.gray(tier.path) : "";
8422
- const note = tier.note ? import_chalk16.default.gray(`(${tier.note})`) : "";
8423
- statusStr = import_chalk16.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
9551
+ const loc = tier.path ? import_chalk17.default.gray(tier.path) : "";
9552
+ const note = tier.note ? import_chalk17.default.gray(`(${tier.note})`) : "";
9553
+ statusStr = import_chalk17.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
8424
9554
  } else {
8425
- statusStr = import_chalk16.default.gray("\u25CB " + (tier.note ?? "not found"));
9555
+ statusStr = import_chalk17.default.gray("\u25CB " + (tier.note ?? "not found"));
8426
9556
  }
8427
- console.log(`${num} ${import_chalk16.default.white(label)} ${statusStr}`);
9557
+ console.log(`${num} ${import_chalk17.default.white(label)} ${statusStr}`);
8428
9558
  }
8429
9559
  console.log("");
8430
- console.log(import_chalk16.default.bold("Policy Evaluation:"));
9560
+ console.log(import_chalk17.default.bold("Policy Evaluation:"));
8431
9561
  for (const step of result.steps) {
8432
9562
  const isFinal = step.isFinal;
8433
9563
  let icon;
8434
- if (step.outcome === "allow") icon = import_chalk16.default.green(" \u2705");
8435
- else if (step.outcome === "review") icon = import_chalk16.default.red(" \u{1F534}");
8436
- else if (step.outcome === "skip") icon = import_chalk16.default.gray(" \u2500 ");
8437
- else icon = import_chalk16.default.gray(" \u25CB ");
9564
+ if (step.outcome === "allow") icon = import_chalk17.default.green(" \u2705");
9565
+ else if (step.outcome === "review") icon = import_chalk17.default.red(" \u{1F534}");
9566
+ else if (step.outcome === "skip") icon = import_chalk17.default.gray(" \u2500 ");
9567
+ else icon = import_chalk17.default.gray(" \u25CB ");
8438
9568
  const name = step.name.padEnd(18);
8439
- const nameStr = isFinal ? import_chalk16.default.white.bold(name) : import_chalk16.default.white(name);
8440
- const detail = isFinal ? import_chalk16.default.white(step.detail) : import_chalk16.default.gray(step.detail);
8441
- const arrow = isFinal ? import_chalk16.default.yellow(" \u2190 STOP") : "";
9569
+ const nameStr = isFinal ? import_chalk17.default.white.bold(name) : import_chalk17.default.white(name);
9570
+ const detail = isFinal ? import_chalk17.default.white(step.detail) : import_chalk17.default.gray(step.detail);
9571
+ const arrow = isFinal ? import_chalk17.default.yellow(" \u2190 STOP") : "";
8442
9572
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
8443
9573
  }
8444
9574
  console.log("");
8445
9575
  if (result.decision === "allow") {
8446
- console.log(import_chalk16.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk16.default.gray(" \u2014 no approval needed"));
9576
+ console.log(import_chalk17.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk17.default.gray(" \u2014 no approval needed"));
8447
9577
  } else {
8448
9578
  console.log(
8449
- import_chalk16.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk16.default.gray(" \u2014 human approval required")
9579
+ import_chalk17.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk17.default.gray(" \u2014 human approval required")
8450
9580
  );
8451
9581
  if (result.blockedByLabel) {
8452
- console.log(import_chalk16.default.gray(` Reason: ${result.blockedByLabel}`));
9582
+ console.log(import_chalk17.default.gray(` Reason: ${result.blockedByLabel}`));
8453
9583
  }
8454
9584
  }
8455
9585
  console.log("");
8456
9586
  });
8457
- program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
8458
- const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
8459
- if (import_fs22.default.existsSync(configPath) && !options.force) {
8460
- console.log(import_chalk16.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
8461
- console.log(import_chalk16.default.gray(` Run with --force to overwrite.`));
8462
- return;
8463
- }
8464
- const requestedMode = options.mode.toLowerCase();
8465
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8466
- const configToSave = {
8467
- ...DEFAULT_CONFIG,
8468
- settings: {
8469
- ...DEFAULT_CONFIG.settings,
8470
- mode: safeMode
8471
- }
8472
- };
8473
- const dir = import_path24.default.dirname(configPath);
8474
- if (!import_fs22.default.existsSync(dir)) import_fs22.default.mkdirSync(dir, { recursive: true });
8475
- import_fs22.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8476
- console.log(import_chalk16.default.green(`\u2705 Global config created: ${configPath}`));
8477
- console.log(import_chalk16.default.cyan(` Mode set to: ${safeMode}`));
8478
- console.log(
8479
- import_chalk16.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
8480
- );
8481
- });
9587
+ registerInitCommand(program);
8482
9588
  registerAuditCommand(program);
8483
9589
  registerStatusCommand(program);
8484
9590
  registerDaemonCommand(program);
@@ -8487,7 +9593,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
8487
9593
  try {
8488
9594
  await startTail2(options);
8489
9595
  } catch (err) {
8490
- console.error(import_chalk16.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
9596
+ console.error(import_chalk17.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
8491
9597
  process.exit(1);
8492
9598
  }
8493
9599
  });
@@ -8499,7 +9605,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8499
9605
  const ms = parseDuration(options.duration);
8500
9606
  if (ms === null) {
8501
9607
  console.error(
8502
- import_chalk16.default.red(`
9608
+ import_chalk17.default.red(`
8503
9609
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
8504
9610
  `)
8505
9611
  );
@@ -8507,20 +9613,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8507
9613
  }
8508
9614
  pauseNode9(ms, options.duration);
8509
9615
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
8510
- console.log(import_chalk16.default.yellow(`
9616
+ console.log(import_chalk17.default.yellow(`
8511
9617
  \u23F8 Node9 paused until ${expiresAt}`));
8512
- console.log(import_chalk16.default.gray(` All tool calls will be allowed without review.`));
8513
- console.log(import_chalk16.default.gray(` Run "node9 resume" to re-enable early.
9618
+ console.log(import_chalk17.default.gray(` All tool calls will be allowed without review.`));
9619
+ console.log(import_chalk17.default.gray(` Run "node9 resume" to re-enable early.
8514
9620
  `));
8515
9621
  });
8516
9622
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
8517
9623
  const { paused } = checkPause();
8518
9624
  if (!paused) {
8519
- console.log(import_chalk16.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
9625
+ console.log(import_chalk17.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
8520
9626
  return;
8521
9627
  }
8522
9628
  resumeNode9();
8523
- console.log(import_chalk16.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
9629
+ console.log(import_chalk17.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
8524
9630
  });
8525
9631
  var HOOK_BASED_AGENTS = {
8526
9632
  claude: "claude",
@@ -8533,15 +9639,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8533
9639
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
8534
9640
  const target = HOOK_BASED_AGENTS[firstArg2];
8535
9641
  console.error(
8536
- import_chalk16.default.yellow(`
9642
+ import_chalk17.default.yellow(`
8537
9643
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
8538
9644
  );
8539
- console.error(import_chalk16.default.white(`
9645
+ console.error(import_chalk17.default.white(`
8540
9646
  "${target}" uses its own hook system. Use:`));
8541
9647
  console.error(
8542
- import_chalk16.default.green(` node9 addto ${target} `) + import_chalk16.default.gray("# one-time setup")
9648
+ import_chalk17.default.green(` node9 addto ${target} `) + import_chalk17.default.gray("# one-time setup")
8543
9649
  );
8544
- console.error(import_chalk16.default.green(` ${target} `) + import_chalk16.default.gray("# run normally"));
9650
+ console.error(import_chalk17.default.green(` ${target} `) + import_chalk17.default.gray("# run normally"));
8545
9651
  process.exit(1);
8546
9652
  }
8547
9653
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -8558,7 +9664,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8558
9664
  }
8559
9665
  );
8560
9666
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
8561
- console.error(import_chalk16.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
9667
+ console.error(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
8562
9668
  const daemonReady = await autoStartDaemonAndWait();
8563
9669
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
8564
9670
  }
@@ -8571,12 +9677,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8571
9677
  }
8572
9678
  if (!result.approved) {
8573
9679
  console.error(
8574
- import_chalk16.default.red(`
9680
+ import_chalk17.default.red(`
8575
9681
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
8576
9682
  );
8577
9683
  process.exit(1);
8578
9684
  }
8579
- console.error(import_chalk16.default.green("\n\u2705 Approved \u2014 running command...\n"));
9685
+ console.error(import_chalk17.default.green("\n\u2705 Approved \u2014 running command...\n"));
8580
9686
  await runProxy(fullCommand);
8581
9687
  } else {
8582
9688
  program.help();
@@ -8591,9 +9697,9 @@ if (process.argv[2] !== "daemon") {
8591
9697
  const isCheckHook = process.argv[2] === "check";
8592
9698
  if (isCheckHook) {
8593
9699
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
8594
- const logPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "hook-debug.log");
9700
+ const logPath = import_path27.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
8595
9701
  const msg = reason instanceof Error ? reason.message : String(reason);
8596
- import_fs22.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9702
+ import_fs25.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
8597
9703
  `);
8598
9704
  }
8599
9705
  process.exit(0);