@node9/proxy 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -94,8 +94,8 @@ function sanitizeConfig(raw) {
94
94
  }
95
95
  }
96
96
  const lines = result.error.issues.map((issue) => {
97
- const path25 = issue.path.length > 0 ? issue.path.join(".") : "root";
98
- return ` \u2022 ${path25}: ${issue.message}`;
97
+ const path27 = issue.path.length > 0 ? issue.path.join(".") : "root";
98
+ return ` \u2022 ${path27}: ${issue.message}`;
99
99
  });
100
100
  return {
101
101
  sanitized,
@@ -1605,17 +1605,44 @@ function readTrustedHosts() {
1605
1605
  return [];
1606
1606
  }
1607
1607
  }
1608
+ function getFileMtime() {
1609
+ try {
1610
+ return fs6.statSync(getTrustedHostsPath()).mtimeMs;
1611
+ } catch {
1612
+ return 0;
1613
+ }
1614
+ }
1615
+ function getCachedHosts() {
1616
+ const now = Date.now();
1617
+ if (_cache && now < _cache.expiry) {
1618
+ const mtime = getFileMtime();
1619
+ if (mtime === _cache.mtime) return _cache.hosts;
1620
+ }
1621
+ const hosts = readTrustedHosts();
1622
+ _cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
1623
+ return hosts;
1624
+ }
1608
1625
  function writeTrustedHosts(hosts) {
1609
1626
  const filePath = getTrustedHostsPath();
1610
1627
  fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
1611
1628
  const tmp = filePath + ".node9-tmp";
1612
- fs6.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2));
1629
+ fs6.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
1613
1630
  fs6.renameSync(tmp, filePath);
1631
+ _cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
1614
1632
  }
1615
1633
  function addTrustedHost(host) {
1634
+ const normalized = normalizeHost(host);
1635
+ if (normalized.startsWith("*.")) {
1636
+ const base = normalized.slice(2);
1637
+ if (!base.includes(".")) {
1638
+ throw new Error(
1639
+ `Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
1640
+ );
1641
+ }
1642
+ }
1616
1643
  const hosts = readTrustedHosts();
1617
- if (hosts.some((h) => h.host === host)) return;
1618
- hosts.push({ host, addedAt: Date.now(), addedBy: "user" });
1644
+ if (hosts.some((h) => h.host === normalized)) return;
1645
+ hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
1619
1646
  writeTrustedHosts(hosts);
1620
1647
  }
1621
1648
  function removeTrustedHost(host) {
@@ -1630,18 +1657,21 @@ function normalizeHost(raw) {
1630
1657
  }
1631
1658
  function isTrustedHost(host) {
1632
1659
  const normalized = normalizeHost(host);
1633
- return readTrustedHosts().some((entry) => {
1660
+ return getCachedHosts().some((entry) => {
1634
1661
  const entryHost = entry.host.toLowerCase();
1635
1662
  if (entryHost.startsWith("*.")) {
1636
1663
  const domain = entryHost.slice(2);
1637
- return normalized === domain || normalized.endsWith("." + domain);
1664
+ return normalized.endsWith("." + domain);
1638
1665
  }
1639
1666
  return normalized === entryHost;
1640
1667
  });
1641
1668
  }
1669
+ var _cache, CACHE_TTL_MS;
1642
1670
  var init_trusted_hosts = __esm({
1643
1671
  "src/auth/trusted-hosts.ts"() {
1644
1672
  "use strict";
1673
+ _cache = null;
1674
+ CACHE_TTL_MS = 5e3;
1645
1675
  }
1646
1676
  });
1647
1677
 
@@ -1664,9 +1694,9 @@ function matchesPattern(text, patterns) {
1664
1694
  const withoutDotSlash = text.replace(/^\.\//, "");
1665
1695
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1666
1696
  }
1667
- function getNestedValue(obj, path25) {
1697
+ function getNestedValue(obj, path27) {
1668
1698
  if (!obj || typeof obj !== "object") return null;
1669
- return path25.split(".").reduce((prev, curr) => prev?.[curr], obj);
1699
+ return path27.split(".").reduce((prev, curr) => prev?.[curr], obj);
1670
1700
  }
1671
1701
  function shouldSnapshot(toolName, args, config) {
1672
1702
  if (!config.settings.enableUndo) return false;
@@ -1852,7 +1882,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1852
1882
  };
1853
1883
  }
1854
1884
  if (allTrusted) {
1855
- return { decision: "allow" };
1885
+ return {
1886
+ decision: "allow",
1887
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1888
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1889
+ tier: 3
1890
+ };
1856
1891
  }
1857
1892
  return {
1858
1893
  decision: "review",
@@ -2393,8 +2428,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2393
2428
  signal: ctrl.signal
2394
2429
  });
2395
2430
  if (!res.ok) throw new Error("Daemon fail");
2396
- const { id } = await res.json();
2397
- return id;
2431
+ const { id, allowCount } = await res.json();
2432
+ return { id, allowCount: allowCount ?? 1 };
2398
2433
  } finally {
2399
2434
  clearTimeout(timer);
2400
2435
  }
@@ -2433,15 +2468,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
2433
2468
  signal: AbortSignal.timeout(3e3)
2434
2469
  });
2435
2470
  if (!res.ok) throw new Error("Daemon unreachable");
2436
- const { id } = await res.json();
2437
- return id;
2471
+ const { id, allowCount } = await res.json();
2472
+ return { id, allowCount: allowCount ?? 1 };
2438
2473
  }
2439
- async function resolveViaDaemon(id, decision, internalToken) {
2474
+ async function resolveViaDaemon(id, decision, internalToken, source) {
2440
2475
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2441
2476
  await fetch(`${base}/resolve/${id}`, {
2442
2477
  method: "POST",
2443
2478
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
2444
- body: JSON.stringify({ decision }),
2479
+ body: JSON.stringify({ decision, ...source && { source } }),
2445
2480
  signal: AbortSignal.timeout(3e3)
2446
2481
  });
2447
2482
  }
@@ -2646,20 +2681,24 @@ ${smartTruncate(str, 500)}`
2646
2681
  function escapePango(text) {
2647
2682
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2648
2683
  }
2649
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2684
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2650
2685
  const lines = [];
2651
2686
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2652
2687
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2653
2688
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2654
2689
  lines.push("");
2655
2690
  lines.push(formattedArgs);
2691
+ if (allowCount >= 3) {
2692
+ lines.push("");
2693
+ lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
2694
+ }
2656
2695
  if (!locked) {
2657
2696
  lines.push("");
2658
2697
  lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
2659
2698
  }
2660
2699
  return lines.join("\n");
2661
2700
  }
2662
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2701
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2663
2702
  const lines = [];
2664
2703
  if (locked) {
2665
2704
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2671,6 +2710,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2671
2710
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2672
2711
  lines.push("");
2673
2712
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2713
+ if (allowCount >= 3) {
2714
+ lines.push("");
2715
+ lines.push(
2716
+ `<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
2717
+ );
2718
+ }
2674
2719
  if (!locked) {
2675
2720
  lines.push("");
2676
2721
  lines.push(
@@ -2679,12 +2724,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2679
2724
  }
2680
2725
  return lines.join("\n");
2681
2726
  }
2682
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
2727
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2683
2728
  if (isTestEnv()) return "deny";
2684
2729
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2685
2730
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
2686
2731
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
2687
- const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
2732
+ const message = buildPlainMessage(
2733
+ toolName,
2734
+ formattedArgs,
2735
+ agent,
2736
+ explainableLabel,
2737
+ locked,
2738
+ allowCount
2739
+ );
2688
2740
  return new Promise((resolve) => {
2689
2741
  let childProcess = null;
2690
2742
  const onAbort = () => {
@@ -2716,7 +2768,8 @@ end run`;
2716
2768
  formattedArgs,
2717
2769
  agent,
2718
2770
  explainableLabel,
2719
- locked
2771
+ locked,
2772
+ allowCount
2720
2773
  );
2721
2774
  const argsList = [
2722
2775
  locked ? "--info" : "--question",
@@ -3080,13 +3133,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3080
3133
  let viewerId = null;
3081
3134
  const internalToken = getInternalToken();
3082
3135
  let daemonEntryId = null;
3136
+ let daemonAllowCount = 1;
3083
3137
  if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
3084
3138
  if (cloudEnforced && cloudRequestId) {
3085
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3139
+ const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3140
+ viewerId = viewer?.id ?? null;
3086
3141
  daemonEntryId = viewerId;
3142
+ if (viewer) daemonAllowCount = viewer.allowCount;
3087
3143
  } else {
3088
3144
  try {
3089
- daemonEntryId = await registerDaemonEntry(
3145
+ const entry = await registerDaemonEntry(
3090
3146
  toolName,
3091
3147
  args,
3092
3148
  meta,
@@ -3094,6 +3150,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3094
3150
  options?.activityId,
3095
3151
  options?.cwd
3096
3152
  );
3153
+ daemonEntryId = entry.id;
3154
+ daemonAllowCount = entry.allowCount;
3097
3155
  } catch {
3098
3156
  }
3099
3157
  }
@@ -3129,7 +3187,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3129
3187
  false,
3130
3188
  signal,
3131
3189
  policyMatchedField,
3132
- policyMatchedWord
3190
+ policyMatchedWord,
3191
+ daemonAllowCount
3133
3192
  );
3134
3193
  if (decision === "always_allow") {
3135
3194
  writeTrustSession(toolName, 36e5);
@@ -3187,10 +3246,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3187
3246
  if (!resolved) {
3188
3247
  resolved = true;
3189
3248
  abortController.abort();
3190
- if (viewerId && internalToken) {
3191
- resolveViaDaemon(viewerId, res.approved ? "allow" : "deny", internalToken).catch(
3192
- () => null
3193
- );
3249
+ if (daemonEntryId && internalToken) {
3250
+ resolveViaDaemon(
3251
+ daemonEntryId,
3252
+ res.approved ? "allow" : "deny",
3253
+ internalToken,
3254
+ res.decisionSource
3255
+ ).catch(() => null);
3194
3256
  }
3195
3257
  resolve(res);
3196
3258
  }
@@ -3543,6 +3605,15 @@ var init_ui = __esm({
3543
3605
  padding: 5px 10px;
3544
3606
  margin-bottom: 14px;
3545
3607
  }
3608
+ .insight-hint {
3609
+ font-size: 12px;
3610
+ color: #f0c040;
3611
+ background: rgba(240, 192, 64, 0.08);
3612
+ border: 1px solid rgba(240, 192, 64, 0.25);
3613
+ border-radius: 6px;
3614
+ padding: 6px 10px;
3615
+ margin-bottom: 12px;
3616
+ }
3546
3617
  pre {
3547
3618
  background: #0d1117;
3548
3619
  padding: 14px 16px;
@@ -4015,6 +4086,78 @@ var init_ui = __esm({
4015
4086
  color: var(--danger);
4016
4087
  }
4017
4088
 
4089
+ /* \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 */
4090
+ .suggestion-card {
4091
+ background: rgba(82, 130, 255, 0.06);
4092
+ border: 1px solid rgba(82, 130, 255, 0.25);
4093
+ border-radius: 8px;
4094
+ padding: 10px 12px;
4095
+ margin-bottom: 8px;
4096
+ }
4097
+ .suggestion-card:last-child {
4098
+ margin-bottom: 0;
4099
+ }
4100
+ .suggestion-header {
4101
+ display: flex;
4102
+ align-items: center;
4103
+ gap: 8px;
4104
+ margin-bottom: 6px;
4105
+ }
4106
+ .suggestion-tool {
4107
+ font-family: 'Fira Code', monospace;
4108
+ font-size: 11px;
4109
+ color: var(--text-bright);
4110
+ flex: 1;
4111
+ word-break: break-all;
4112
+ }
4113
+ .suggestion-count {
4114
+ font-size: 10px;
4115
+ color: var(--muted);
4116
+ white-space: nowrap;
4117
+ }
4118
+ .suggestion-rule {
4119
+ font-family: 'Fira Code', monospace;
4120
+ font-size: 10px;
4121
+ color: #79c0ff;
4122
+ background: rgba(0, 0, 0, 0.25);
4123
+ border-radius: 4px;
4124
+ padding: 4px 8px;
4125
+ margin-bottom: 8px;
4126
+ word-break: break-all;
4127
+ white-space: pre-wrap;
4128
+ }
4129
+ .suggestion-actions {
4130
+ display: flex;
4131
+ gap: 6px;
4132
+ }
4133
+ .btn-apply {
4134
+ background: rgba(52, 125, 57, 0.2);
4135
+ border: 1px solid rgba(87, 171, 90, 0.4);
4136
+ color: #57ab5a;
4137
+ padding: 4px 10px;
4138
+ font-size: 11px;
4139
+ border-radius: 5px;
4140
+ font-family: inherit;
4141
+ cursor: pointer;
4142
+ }
4143
+ .btn-apply:hover {
4144
+ background: rgba(52, 125, 57, 0.35);
4145
+ }
4146
+ .btn-dismiss-suggestion {
4147
+ background: transparent;
4148
+ border: 1px solid var(--border);
4149
+ color: var(--muted);
4150
+ padding: 4px 10px;
4151
+ font-size: 11px;
4152
+ border-radius: 5px;
4153
+ font-family: inherit;
4154
+ cursor: pointer;
4155
+ }
4156
+ .btn-dismiss-suggestion:hover {
4157
+ border-color: var(--danger);
4158
+ color: var(--danger);
4159
+ }
4160
+
4018
4161
  .modal-overlay {
4019
4162
  display: none;
4020
4163
  position: fixed;
@@ -4196,6 +4339,11 @@ var init_ui = __esm({
4196
4339
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
4197
4340
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
4198
4341
  </div>
4342
+
4343
+ <div class="panel" id="suggestionsPanel" style="display: none">
4344
+ <div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
4345
+ <div id="suggestionsList"></div>
4346
+ </div>
4199
4347
  </div>
4200
4348
  </div>
4201
4349
  </div>
@@ -4385,6 +4533,7 @@ var init_ui = __esm({
4385
4533
  </div>
4386
4534
  <div class="tool-chip">\${esc(req.toolName)}</div>
4387
4535
  \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
4536
+ \${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
4388
4537
  \${renderPayload(req)}
4389
4538
  <div class="actions" id="act-\${req.id}">
4390
4539
  <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
@@ -4451,6 +4600,14 @@ var init_ui = __esm({
4451
4600
  ev.addEventListener('shields-status', (e) => {
4452
4601
  renderShields(JSON.parse(e.data).shields);
4453
4602
  });
4603
+ ev.addEventListener('suggestion:new', (e) => {
4604
+ const s = JSON.parse(e.data);
4605
+ addSuggestionCard(s);
4606
+ });
4607
+ ev.addEventListener('suggestion:resolved', (e) => {
4608
+ const { id } = JSON.parse(e.data);
4609
+ removeSuggestionCard(id);
4610
+ });
4454
4611
 
4455
4612
  // \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
4456
4613
  ev.addEventListener('activity', (e) => {
@@ -4700,6 +4857,74 @@ var init_ui = __esm({
4700
4857
  .then((r) => r.json())
4701
4858
  .then(renderDecisions)
4702
4859
  .catch(() => {});
4860
+
4861
+ // \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
4862
+ function rulePreview(suggestion) {
4863
+ const r = suggestion.suggestedRule;
4864
+ if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
4865
+ const cond = r.rule.conditions?.[0];
4866
+ const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
4867
+ return \`allow \${r.rule.tool}\${condStr}\`;
4868
+ }
4869
+
4870
+ function addSuggestionCard(s) {
4871
+ const panel = document.getElementById('suggestionsPanel');
4872
+ const list = document.getElementById('suggestionsList');
4873
+ panel.style.display = '';
4874
+
4875
+ const card = document.createElement('div');
4876
+ card.className = 'suggestion-card';
4877
+ card.id = 'sg-' + s.id;
4878
+ card.innerHTML = \`
4879
+ <div class="suggestion-header">
4880
+ <span class="suggestion-tool">\${esc(s.toolName)}</span>
4881
+ <span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
4882
+ </div>
4883
+ <div class="suggestion-rule">\${esc(rulePreview(s))}</div>
4884
+ <div class="suggestion-actions">
4885
+ <button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
4886
+ <button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
4887
+ </div>
4888
+ \`;
4889
+ list.appendChild(card);
4890
+ }
4891
+
4892
+ function removeSuggestionCard(id) {
4893
+ document.getElementById('sg-' + id)?.remove();
4894
+ const list = document.getElementById('suggestionsList');
4895
+ if (!list.querySelector('.suggestion-card')) {
4896
+ document.getElementById('suggestionsPanel').style.display = 'none';
4897
+ }
4898
+ }
4899
+
4900
+ function applySuggestion(id) {
4901
+ fetch('/suggestions/' + id + '/apply', {
4902
+ method: 'POST',
4903
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
4904
+ body: JSON.stringify({}),
4905
+ })
4906
+ .then((r) => {
4907
+ if (r.ok) removeSuggestionCard(id);
4908
+ })
4909
+ .catch(() => {});
4910
+ }
4911
+
4912
+ function dismissSuggestion(id) {
4913
+ fetch('/suggestions/' + id + '/dismiss', {
4914
+ method: 'POST',
4915
+ headers: { 'X-Node9-Token': CSRF_TOKEN },
4916
+ })
4917
+ .then((r) => {
4918
+ if (r.ok) removeSuggestionCard(id);
4919
+ })
4920
+ .catch(() => {});
4921
+ }
4922
+
4923
+ // Load any suggestions that survived a page reload (daemon still running)
4924
+ fetch('/suggestions')
4925
+ .then((r) => r.json())
4926
+ .then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
4927
+ .catch(() => {});
4703
4928
  </script>
4704
4929
  </body>
4705
4930
  </html>
@@ -4717,13 +4942,123 @@ var init_ui2 = __esm({
4717
4942
  }
4718
4943
  });
4719
4944
 
4945
+ // src/daemon/suggestion-tracker.ts
4946
+ import { randomUUID as randomUUID2 } from "crypto";
4947
+ function extractPath(args) {
4948
+ if (!args || typeof args !== "object") return null;
4949
+ const a = args;
4950
+ for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
4951
+ if (typeof a[key] === "string" && a[key]) return a[key];
4952
+ }
4953
+ return null;
4954
+ }
4955
+ function commonPathPrefix(paths) {
4956
+ if (paths.length < 2) return null;
4957
+ const dirParts = paths.map((p) => {
4958
+ const lastSlash = p.lastIndexOf("/");
4959
+ return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
4960
+ });
4961
+ const first = dirParts[0].split("/");
4962
+ const common = [];
4963
+ for (let i = 0; i < first.length; i++) {
4964
+ if (dirParts.every((d) => d.split("/")[i] === first[i])) {
4965
+ common.push(first[i]);
4966
+ } else {
4967
+ break;
4968
+ }
4969
+ }
4970
+ const prefix = common.join("/").replace(/\/?$/, "/");
4971
+ return prefix.length > 1 ? prefix : null;
4972
+ }
4973
+ var SuggestionTracker;
4974
+ var init_suggestion_tracker = __esm({
4975
+ "src/daemon/suggestion-tracker.ts"() {
4976
+ "use strict";
4977
+ SuggestionTracker = class {
4978
+ events = /* @__PURE__ */ new Map();
4979
+ threshold;
4980
+ constructor(threshold = 3) {
4981
+ this.threshold = threshold;
4982
+ }
4983
+ /**
4984
+ * Record a human-allowed review for a tool.
4985
+ * Returns a Suggestion when the threshold is reached, null otherwise.
4986
+ */
4987
+ recordAllow(toolName, args) {
4988
+ const events = this.events.get(toolName) ?? [];
4989
+ events.push({ args, ts: Date.now() });
4990
+ this.events.set(toolName, events);
4991
+ if (events.length >= this.threshold) {
4992
+ this.events.delete(toolName);
4993
+ return this.generateSuggestion(toolName, events);
4994
+ }
4995
+ return null;
4996
+ }
4997
+ /**
4998
+ * Reset the counter for a tool (e.g. when the user clicks Deny —
4999
+ * don't suggest allowing something they just blocked).
5000
+ */
5001
+ resetTool(toolName) {
5002
+ this.events.delete(toolName);
5003
+ }
5004
+ /** Current allow count for a tool (for tests). */
5005
+ getCount(toolName) {
5006
+ return this.events.get(toolName)?.length ?? 0;
5007
+ }
5008
+ generateSuggestion(toolName, events) {
5009
+ const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
5010
+ const prefix = commonPathPrefix(paths);
5011
+ const suggestedRule = prefix ? {
5012
+ type: "smartRule",
5013
+ rule: {
5014
+ name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
5015
+ tool: toolName,
5016
+ conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
5017
+ verdict: "allow",
5018
+ reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
5019
+ }
5020
+ } : { type: "ignoredTool", toolName };
5021
+ return {
5022
+ id: randomUUID2(),
5023
+ toolName,
5024
+ allowCount: events.length,
5025
+ suggestedRule,
5026
+ status: "pending",
5027
+ createdAt: Date.now(),
5028
+ exampleArgs: events.slice(0, 3).map((e) => e.args)
5029
+ };
5030
+ }
5031
+ };
5032
+ }
5033
+ });
5034
+
4720
5035
  // src/daemon/state.ts
4721
5036
  import net2 from "net";
4722
5037
  import fs12 from "fs";
4723
5038
  import path15 from "path";
4724
5039
  import os12 from "os";
4725
5040
  import { spawn as spawn2 } from "child_process";
4726
- import { randomUUID as randomUUID2 } from "crypto";
5041
+ import { randomUUID as randomUUID3 } from "crypto";
5042
+ function loadInsightCounts() {
5043
+ try {
5044
+ if (!fs12.existsSync(INSIGHT_COUNTS_FILE)) return;
5045
+ const data = JSON.parse(fs12.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
5046
+ for (const [tool, count] of Object.entries(data)) {
5047
+ if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
5048
+ }
5049
+ } catch {
5050
+ }
5051
+ }
5052
+ function saveInsightCounts() {
5053
+ try {
5054
+ const data = {};
5055
+ insightCounts.forEach((count, tool) => {
5056
+ data[tool] = count;
5057
+ });
5058
+ atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
5059
+ } catch {
5060
+ }
5061
+ }
4727
5062
  function getAbandonTimer() {
4728
5063
  return _abandonTimer;
4729
5064
  }
@@ -4748,9 +5083,25 @@ function markRejectionHandlerRegistered() {
4748
5083
  function atomicWriteSync2(filePath, data, options) {
4749
5084
  const dir = path15.dirname(filePath);
4750
5085
  if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
4751
- const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
4752
- fs12.writeFileSync(tmpPath, data, options);
4753
- fs12.renameSync(tmpPath, filePath);
5086
+ const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
5087
+ try {
5088
+ fs12.writeFileSync(tmpPath, data, options);
5089
+ } catch (err) {
5090
+ try {
5091
+ fs12.unlinkSync(tmpPath);
5092
+ } catch {
5093
+ }
5094
+ throw err;
5095
+ }
5096
+ try {
5097
+ fs12.renameSync(tmpPath, filePath);
5098
+ } catch (err) {
5099
+ try {
5100
+ fs12.unlinkSync(tmpPath);
5101
+ } catch {
5102
+ }
5103
+ throw err;
5104
+ }
4754
5105
  }
4755
5106
  function redactArgs(value) {
4756
5107
  if (!value || typeof value !== "object") return value;
@@ -4948,11 +5299,12 @@ function startActivitySocket() {
4948
5299
  }
4949
5300
  });
4950
5301
  }
4951
- var 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;
5302
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
4952
5303
  var init_state2 = __esm({
4953
5304
  "src/daemon/state.ts"() {
4954
5305
  "use strict";
4955
5306
  init_daemon();
5307
+ init_suggestion_tracker();
4956
5308
  homeDir = os12.homedir();
4957
5309
  DAEMON_PID_FILE = path15.join(homeDir, ".node9", "daemon.pid");
4958
5310
  DECISIONS_FILE = path15.join(homeDir, ".node9", "decisions.json");
@@ -4960,8 +5312,12 @@ var init_state2 = __esm({
4960
5312
  TRUST_FILE2 = path15.join(homeDir, ".node9", "trust.json");
4961
5313
  GLOBAL_CONFIG_FILE = path15.join(homeDir, ".node9", "config.json");
4962
5314
  CREDENTIALS_FILE = path15.join(homeDir, ".node9", "credentials.json");
5315
+ INSIGHT_COUNTS_FILE = path15.join(homeDir, ".node9", "insight-counts.json");
4963
5316
  pending = /* @__PURE__ */ new Map();
4964
5317
  sseClients = /* @__PURE__ */ new Set();
5318
+ suggestionTracker = new SuggestionTracker(3);
5319
+ suggestions = /* @__PURE__ */ new Map();
5320
+ insightCounts = /* @__PURE__ */ new Map();
4965
5321
  _abandonTimer = null;
4966
5322
  _hadBrowserClient = false;
4967
5323
  _daemonServer = null;
@@ -4980,16 +5336,74 @@ var init_state2 = __esm({
4980
5336
  }
4981
5337
  });
4982
5338
 
4983
- // src/daemon/server.ts
4984
- import http from "http";
5339
+ // src/config/patch.ts
4985
5340
  import fs13 from "fs";
4986
5341
  import path16 from "path";
4987
- import { randomUUID as randomUUID3 } from "crypto";
5342
+ import os13 from "os";
5343
+ function patchConfig(configPath, patch) {
5344
+ let config = {};
5345
+ try {
5346
+ if (fs13.existsSync(configPath)) {
5347
+ config = JSON.parse(fs13.readFileSync(configPath, "utf8"));
5348
+ }
5349
+ } catch {
5350
+ throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
5351
+ }
5352
+ if (!config.policy || typeof config.policy !== "object") config.policy = {};
5353
+ const policy = config.policy;
5354
+ if (patch.type === "smartRule") {
5355
+ if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
5356
+ const rules = policy.smartRules;
5357
+ if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
5358
+ rules.push(patch.rule);
5359
+ } else {
5360
+ if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
5361
+ const ignored = policy.ignoredTools;
5362
+ if (!ignored.includes(patch.toolName)) {
5363
+ ignored.push(patch.toolName);
5364
+ }
5365
+ }
5366
+ const dir = path16.dirname(configPath);
5367
+ fs13.mkdirSync(dir, { recursive: true });
5368
+ const tmp = configPath + ".node9-tmp";
5369
+ try {
5370
+ fs13.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5371
+ } catch (err) {
5372
+ try {
5373
+ fs13.unlinkSync(tmp);
5374
+ } catch {
5375
+ }
5376
+ throw err;
5377
+ }
5378
+ try {
5379
+ fs13.renameSync(tmp, configPath);
5380
+ } catch (err) {
5381
+ try {
5382
+ fs13.unlinkSync(tmp);
5383
+ } catch {
5384
+ }
5385
+ throw err;
5386
+ }
5387
+ }
5388
+ var GLOBAL_CONFIG_PATH;
5389
+ var init_patch = __esm({
5390
+ "src/config/patch.ts"() {
5391
+ "use strict";
5392
+ GLOBAL_CONFIG_PATH = path16.join(os13.homedir(), ".node9", "config.json");
5393
+ }
5394
+ });
5395
+
5396
+ // src/daemon/server.ts
5397
+ import http from "http";
5398
+ import fs14 from "fs";
5399
+ import path17 from "path";
5400
+ import { randomUUID as randomUUID4 } from "crypto";
4988
5401
  import { spawnSync as spawnSync2 } from "child_process";
4989
5402
  import chalk2 from "chalk";
4990
5403
  function startDaemon() {
4991
- const csrfToken = randomUUID3();
4992
- const internalToken = randomUUID3();
5404
+ loadInsightCounts();
5405
+ const csrfToken = randomUUID4();
5406
+ const internalToken = randomUUID4();
4993
5407
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
4994
5408
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
4995
5409
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
@@ -5002,7 +5416,7 @@ function startDaemon() {
5002
5416
  idleTimer = setTimeout(() => {
5003
5417
  if (autoStarted) {
5004
5418
  try {
5005
- fs13.unlinkSync(DAEMON_PID_FILE);
5419
+ fs14.unlinkSync(DAEMON_PID_FILE);
5006
5420
  } catch {
5007
5421
  }
5008
5422
  }
@@ -5011,8 +5425,14 @@ function startDaemon() {
5011
5425
  idleTimer.unref();
5012
5426
  }
5013
5427
  resetIdleTimer();
5428
+ const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
5014
5429
  const server = http.createServer(async (req, res) => {
5015
- const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
5430
+ const host = req.headers.host ?? "";
5431
+ if (!allowedHosts.has(host)) {
5432
+ res.writeHead(421, { "Content-Type": "text/plain" });
5433
+ return res.end("Misdirected Request");
5434
+ }
5435
+ const reqUrl = new URL(req.url || "/", `http://${host}`);
5016
5436
  const { pathname } = reqUrl;
5017
5437
  if (req.method === "GET" && pathname === "/") {
5018
5438
  res.writeHead(200, { "Content-Type": "text/html" });
@@ -5045,7 +5465,8 @@ data: ${JSON.stringify({
5045
5465
  slackDelegated: e.slackDelegated,
5046
5466
  timestamp: e.timestamp,
5047
5467
  agent: e.agent,
5048
- mcpServer: e.mcpServer
5468
+ mcpServer: e.mcpServer,
5469
+ allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
5049
5470
  })),
5050
5471
  orgName: getOrgName(),
5051
5472
  autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
@@ -5087,6 +5508,12 @@ data: ${JSON.stringify(item.data)}
5087
5508
  }
5088
5509
  });
5089
5510
  }
5511
+ if (req.method === "POST" && pathname === "/browser-opened") {
5512
+ if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
5513
+ browserOpened = true;
5514
+ res.writeHead(200).end();
5515
+ return;
5516
+ }
5090
5517
  if (req.method === "POST" && pathname === "/check") {
5091
5518
  try {
5092
5519
  resetIdleTimer();
@@ -5104,7 +5531,7 @@ data: ${JSON.stringify(item.data)}
5104
5531
  activityId,
5105
5532
  cwd
5106
5533
  } = JSON.parse(body);
5107
- const id = fromCLI && typeof activityId === "string" && activityId || randomUUID3();
5534
+ const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
5108
5535
  const entry = {
5109
5536
  id,
5110
5537
  toolName,
@@ -5130,7 +5557,7 @@ data: ${JSON.stringify(item.data)}
5130
5557
  e.earlyReason = "No response \u2014 auto-denied after timeout";
5131
5558
  }
5132
5559
  pending.delete(id);
5133
- broadcast("remove", { id });
5560
+ broadcast("remove", { id, decision: "deny" });
5134
5561
  }
5135
5562
  }, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
5136
5563
  };
@@ -5144,7 +5571,7 @@ data: ${JSON.stringify(item.data)}
5144
5571
  status: "pending"
5145
5572
  });
5146
5573
  }
5147
- const projectCwd = typeof cwd === "string" && path16.isAbsolute(cwd) ? cwd : void 0;
5574
+ const projectCwd = typeof cwd === "string" && path17.isAbsolute(cwd) ? cwd : void 0;
5148
5575
  const projectConfig = getConfig(projectCwd);
5149
5576
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
5150
5577
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -5157,7 +5584,10 @@ data: ${JSON.stringify(item.data)}
5157
5584
  slackDelegated: entry.slackDelegated,
5158
5585
  agent: entry.agent,
5159
5586
  mcpServer: entry.mcpServer,
5160
- interactive: terminalEnabled
5587
+ interactive: terminalEnabled,
5588
+ // allowCount = what this count will be if the user allows.
5589
+ // Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
5590
+ allowCount: (insightCounts.get(toolName) ?? 0) + 1
5161
5591
  });
5162
5592
  const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
5163
5593
  if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
@@ -5166,7 +5596,7 @@ data: ${JSON.stringify(item.data)}
5166
5596
  }
5167
5597
  }
5168
5598
  res.writeHead(200, { "Content-Type": "application/json" });
5169
- res.end(JSON.stringify({ id }));
5599
+ res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
5170
5600
  if (slackDelegated) return;
5171
5601
  authorizeHeadless(
5172
5602
  toolName,
@@ -5193,7 +5623,7 @@ data: ${JSON.stringify(item.data)}
5193
5623
  if (e.waiter) {
5194
5624
  e.waiter(decision, result.reason);
5195
5625
  pending.delete(id);
5196
- broadcast("remove", { id });
5626
+ broadcast("remove", { id, decision });
5197
5627
  } else {
5198
5628
  e.earlyDecision = decision;
5199
5629
  e.earlyReason = result.reason;
@@ -5209,7 +5639,7 @@ data: ${JSON.stringify(item.data)}
5209
5639
  e.earlyReason = reason;
5210
5640
  }
5211
5641
  pending.delete(id);
5212
- broadcast("remove", { id });
5642
+ broadcast("remove", { id, decision: "deny" });
5213
5643
  });
5214
5644
  return;
5215
5645
  } catch {
@@ -5240,12 +5670,14 @@ data: ${JSON.stringify(item.data)}
5240
5670
  res.end(JSON.stringify(body));
5241
5671
  };
5242
5672
  req.on("close", () => {
5243
- const e = pending.get(id);
5244
- if (e && e.waiter && e.earlyDecision === null) {
5245
- clearTimeout(e.timer);
5246
- pending.delete(id);
5247
- broadcast("remove", { id });
5248
- }
5673
+ setTimeout(() => {
5674
+ const e = pending.get(id);
5675
+ if (e && e.waiter && e.earlyDecision === null) {
5676
+ clearTimeout(e.timer);
5677
+ pending.delete(id);
5678
+ broadcast("remove", { id });
5679
+ }
5680
+ }, 200);
5249
5681
  });
5250
5682
  return;
5251
5683
  }
@@ -5274,10 +5706,10 @@ data: ${JSON.stringify(item.data)}
5274
5706
  if (entry.waiter) {
5275
5707
  entry.waiter("allow");
5276
5708
  pending.delete(id);
5277
- broadcast("remove", { id });
5709
+ broadcast("remove", { id, decision: "allow" });
5278
5710
  } else {
5279
5711
  entry.earlyDecision = "allow";
5280
- broadcast("remove", { id });
5712
+ broadcast("remove", { id, decision: "allow" });
5281
5713
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5282
5714
  }
5283
5715
  res.writeHead(200);
@@ -5291,16 +5723,29 @@ data: ${JSON.stringify(item.data)}
5291
5723
  decision: resolvedDecision
5292
5724
  });
5293
5725
  clearTimeout(entry.timer);
5726
+ if (resolvedDecision === "allow" && !persist) {
5727
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
5728
+ saveInsightCounts();
5729
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
5730
+ if (suggestion) {
5731
+ suggestions.set(suggestion.id, suggestion);
5732
+ broadcast("suggestion:new", suggestion);
5733
+ }
5734
+ } else if (resolvedDecision === "deny") {
5735
+ insightCounts.delete(entry.toolName);
5736
+ saveInsightCounts();
5737
+ suggestionTracker.resetTool(entry.toolName);
5738
+ }
5294
5739
  const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
5295
5740
  if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
5296
5741
  if (entry.waiter) {
5297
5742
  entry.waiter(resolvedDecision, reason);
5298
5743
  pending.delete(id);
5299
- broadcast("remove", { id });
5744
+ broadcast("remove", { id, decision: resolvedDecision });
5300
5745
  } else {
5301
5746
  entry.earlyDecision = resolvedDecision;
5302
5747
  entry.earlyReason = reason;
5303
- broadcast("remove", { id });
5748
+ broadcast("remove", { id, decision: resolvedDecision });
5304
5749
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5305
5750
  }
5306
5751
  res.writeHead(200);
@@ -5388,13 +5833,38 @@ data: ${JSON.stringify(item.data)}
5388
5833
  const id = pathname.split("/").pop();
5389
5834
  const entry = pending.get(id);
5390
5835
  if (!entry) return res.writeHead(404).end();
5391
- const { decision } = JSON.parse(await readBody(req));
5392
- appendAuditLog({ toolName: entry.toolName, args: entry.args, decision });
5836
+ const { decision, source } = JSON.parse(await readBody(req));
5837
+ const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
5838
+ appendAuditLog({
5839
+ toolName: entry.toolName,
5840
+ args: entry.args,
5841
+ decision: resolvedResolveDecision
5842
+ });
5393
5843
  clearTimeout(entry.timer);
5394
- if (entry.waiter) entry.waiter(decision);
5395
- else entry.earlyDecision = decision;
5844
+ if (resolvedResolveDecision === "allow") {
5845
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
5846
+ saveInsightCounts();
5847
+ } else {
5848
+ insightCounts.delete(entry.toolName);
5849
+ saveInsightCounts();
5850
+ }
5851
+ if (!entry.slackDelegated) {
5852
+ if (resolvedResolveDecision === "allow") {
5853
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
5854
+ if (suggestion) {
5855
+ suggestions.set(suggestion.id, suggestion);
5856
+ broadcast("suggestion:new", suggestion);
5857
+ }
5858
+ } else {
5859
+ suggestionTracker.resetTool(entry.toolName);
5860
+ }
5861
+ }
5862
+ const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
5863
+ if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
5864
+ if (entry.waiter) entry.waiter(resolvedResolveDecision);
5865
+ else entry.earlyDecision = resolvedResolveDecision;
5396
5866
  pending.delete(id);
5397
- broadcast("remove", { id });
5867
+ broadcast("remove", { id, decision: resolvedResolveDecision });
5398
5868
  res.writeHead(200);
5399
5869
  return res.end(JSON.stringify({ ok: true }));
5400
5870
  } catch {
@@ -5442,20 +5912,79 @@ data: ${JSON.stringify(item.data)}
5442
5912
  res.writeHead(400).end();
5443
5913
  }
5444
5914
  }
5915
+ if (req.method === "GET" && pathname === "/suggestions") {
5916
+ res.writeHead(200, { "Content-Type": "application/json" });
5917
+ return res.end(JSON.stringify([...suggestions.values()]));
5918
+ }
5919
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
5920
+ if (!validToken(req)) return res.writeHead(403).end();
5921
+ try {
5922
+ const body = await readBody(req);
5923
+ const data = body ? JSON.parse(body) : {};
5924
+ const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
5925
+ const node9Dir = path17.dirname(GLOBAL_CONFIG_PATH);
5926
+ if (!path17.resolve(configPath).startsWith(node9Dir + path17.sep)) {
5927
+ res.writeHead(400, { "Content-Type": "application/json" });
5928
+ return res.end(
5929
+ JSON.stringify({ error: "configPath must be within the node9 config directory" })
5930
+ );
5931
+ }
5932
+ const id = pathname.split("/")[2];
5933
+ const suggestion = suggestions.get(id);
5934
+ if (!suggestion) return res.writeHead(404).end();
5935
+ let patch;
5936
+ if (data.rule !== void 0) {
5937
+ const parsed = SmartRuleSchema.safeParse(data.rule);
5938
+ if (!parsed.success) {
5939
+ res.writeHead(400, { "Content-Type": "application/json" });
5940
+ return res.end(JSON.stringify({ error: parsed.error.message }));
5941
+ }
5942
+ patch = { type: "smartRule", rule: parsed.data };
5943
+ } else {
5944
+ patch = suggestion.suggestedRule;
5945
+ }
5946
+ patchConfig(configPath, patch);
5947
+ _resetConfigCache();
5948
+ insightCounts.delete(suggestion.toolName);
5949
+ saveInsightCounts();
5950
+ suggestion.status = "applied";
5951
+ broadcast("suggestion:resolved", { id, status: "applied" });
5952
+ res.writeHead(200, { "Content-Type": "application/json" });
5953
+ return res.end(JSON.stringify({ ok: true }));
5954
+ } catch (err) {
5955
+ console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
5956
+ res.writeHead(500, { "Content-Type": "application/json" });
5957
+ return res.end(JSON.stringify({ error: String(err) }));
5958
+ }
5959
+ }
5960
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
5961
+ if (!validToken(req)) return res.writeHead(403).end();
5962
+ try {
5963
+ const id = pathname.split("/")[2];
5964
+ const suggestion = suggestions.get(id);
5965
+ if (!suggestion) return res.writeHead(404).end();
5966
+ suggestion.status = "dismissed";
5967
+ broadcast("suggestion:resolved", { id, status: "dismissed" });
5968
+ res.writeHead(200, { "Content-Type": "application/json" });
5969
+ return res.end(JSON.stringify({ ok: true }));
5970
+ } catch {
5971
+ res.writeHead(400).end();
5972
+ }
5973
+ }
5445
5974
  res.writeHead(404).end();
5446
5975
  });
5447
5976
  setDaemonServer(server);
5448
5977
  server.on("error", (e) => {
5449
5978
  if (e.code === "EADDRINUSE") {
5450
5979
  try {
5451
- if (fs13.existsSync(DAEMON_PID_FILE)) {
5452
- const { pid } = JSON.parse(fs13.readFileSync(DAEMON_PID_FILE, "utf-8"));
5980
+ if (fs14.existsSync(DAEMON_PID_FILE)) {
5981
+ const { pid } = JSON.parse(fs14.readFileSync(DAEMON_PID_FILE, "utf-8"));
5453
5982
  process.kill(pid, 0);
5454
5983
  return process.exit(0);
5455
5984
  }
5456
5985
  } catch {
5457
5986
  try {
5458
- fs13.unlinkSync(DAEMON_PID_FILE);
5987
+ fs14.unlinkSync(DAEMON_PID_FILE);
5459
5988
  } catch {
5460
5989
  }
5461
5990
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -5521,32 +6050,34 @@ var init_server = __esm({
5521
6050
  init_shields();
5522
6051
  init_ui2();
5523
6052
  init_state2();
6053
+ init_patch();
6054
+ init_config_schema();
5524
6055
  }
5525
6056
  });
5526
6057
 
5527
6058
  // src/daemon/index.ts
5528
- import fs14 from "fs";
6059
+ import fs15 from "fs";
5529
6060
  import chalk3 from "chalk";
5530
6061
  import { spawnSync as spawnSync3 } from "child_process";
5531
6062
  function stopDaemon() {
5532
- if (!fs14.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
6063
+ if (!fs15.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
5533
6064
  try {
5534
- const { pid } = JSON.parse(fs14.readFileSync(DAEMON_PID_FILE, "utf-8"));
6065
+ const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
5535
6066
  process.kill(pid, "SIGTERM");
5536
6067
  console.log(chalk3.green("\u2705 Stopped."));
5537
6068
  } catch {
5538
6069
  console.log(chalk3.gray("Cleaned up stale PID file."));
5539
6070
  } finally {
5540
6071
  try {
5541
- fs14.unlinkSync(DAEMON_PID_FILE);
6072
+ fs15.unlinkSync(DAEMON_PID_FILE);
5542
6073
  } catch {
5543
6074
  }
5544
6075
  }
5545
6076
  }
5546
6077
  function daemonStatus() {
5547
- if (fs14.existsSync(DAEMON_PID_FILE)) {
6078
+ if (fs15.existsSync(DAEMON_PID_FILE)) {
5548
6079
  try {
5549
- const { pid } = JSON.parse(fs14.readFileSync(DAEMON_PID_FILE, "utf-8"));
6080
+ const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
5550
6081
  process.kill(pid, 0);
5551
6082
  console.log(chalk3.green("Node9 daemon: running"));
5552
6083
  return;
@@ -5580,10 +6111,10 @@ __export(tail_exports, {
5580
6111
  startTail: () => startTail
5581
6112
  });
5582
6113
  import http2 from "http";
5583
- import chalk15 from "chalk";
5584
- import fs21 from "fs";
5585
- import os19 from "os";
5586
- import path23 from "path";
6114
+ import chalk16 from "chalk";
6115
+ import fs23 from "fs";
6116
+ import os21 from "os";
6117
+ import path25 from "path";
5587
6118
  import readline3 from "readline";
5588
6119
  import { spawn as spawn9, execSync as execSync3 } from "child_process";
5589
6120
  function getIcon(tool) {
@@ -5599,17 +6130,17 @@ function formatBase(activity) {
5599
6130
  const toolName = activity.tool.slice(0, 16).padEnd(16);
5600
6131
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
5601
6132
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
5602
- return `${chalk15.gray(time)} ${icon} ${chalk15.white.bold(toolName)} ${chalk15.dim(argsPreview)}`;
6133
+ return `${chalk16.gray(time)} ${icon} ${chalk16.white.bold(toolName)} ${chalk16.dim(argsPreview)}`;
5603
6134
  }
5604
6135
  function renderResult(activity, result) {
5605
6136
  const base = formatBase(activity);
5606
6137
  let status;
5607
6138
  if (result.status === "allow") {
5608
- status = chalk15.green("\u2713 ALLOW");
6139
+ status = chalk16.green("\u2713 ALLOW");
5609
6140
  } else if (result.status === "dlp") {
5610
- status = chalk15.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6141
+ status = chalk16.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
5611
6142
  } else {
5612
- status = chalk15.red("\u2717 BLOCK");
6143
+ status = chalk16.red("\u2717 BLOCK");
5613
6144
  }
5614
6145
  if (process.stdout.isTTY) {
5615
6146
  readline3.clearLine(process.stdout, 0);
@@ -5619,16 +6150,16 @@ function renderResult(activity, result) {
5619
6150
  }
5620
6151
  function renderPending(activity) {
5621
6152
  if (!process.stdout.isTTY) return;
5622
- process.stdout.write(`${formatBase(activity)} ${chalk15.yellow("\u25CF \u2026")}\r`);
6153
+ process.stdout.write(`${formatBase(activity)} ${chalk16.yellow("\u25CF \u2026")}\r`);
5623
6154
  }
5624
6155
  async function ensureDaemon() {
5625
6156
  let pidPort = null;
5626
- if (fs21.existsSync(PID_FILE)) {
6157
+ if (fs23.existsSync(PID_FILE)) {
5627
6158
  try {
5628
- const { port } = JSON.parse(fs21.readFileSync(PID_FILE, "utf-8"));
6159
+ const { port } = JSON.parse(fs23.readFileSync(PID_FILE, "utf-8"));
5629
6160
  pidPort = port;
5630
6161
  } catch {
5631
- console.error(chalk15.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6162
+ console.error(chalk16.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
5632
6163
  }
5633
6164
  }
5634
6165
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -5639,7 +6170,7 @@ async function ensureDaemon() {
5639
6170
  if (res.ok) return checkPort;
5640
6171
  } catch {
5641
6172
  }
5642
- console.log(chalk15.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6173
+ console.log(chalk16.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
5643
6174
  const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
5644
6175
  detached: true,
5645
6176
  stdio: "ignore",
@@ -5656,12 +6187,15 @@ async function ensureDaemon() {
5656
6187
  } catch {
5657
6188
  }
5658
6189
  }
5659
- console.error(chalk15.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6190
+ console.error(chalk16.red("\u274C Daemon failed to start. Try: node9 daemon start"));
5660
6191
  process.exit(1);
5661
6192
  }
5662
- function postDecisionHttp(id, decision, csrfToken, port) {
6193
+ function postDecisionHttp(id, decision, csrfToken, port, opts) {
5663
6194
  return new Promise((resolve, reject) => {
5664
- const body = JSON.stringify({ decision, source: "terminal" });
6195
+ const bodyObj = { decision, source: "terminal" };
6196
+ if (opts?.persist) bodyObj.persist = true;
6197
+ if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
6198
+ const body = JSON.stringify(bodyObj);
5665
6199
  const req = http2.request(
5666
6200
  {
5667
6201
  hostname: "127.0.0.1",
@@ -5684,22 +6218,30 @@ function postDecisionHttp(id, decision, csrfToken, port) {
5684
6218
  req.end(body);
5685
6219
  });
5686
6220
  }
5687
- function buildCardLines(req) {
6221
+ function buildCardLines(req, localCount = 0) {
5688
6222
  const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
5689
6223
  const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
5690
6224
  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`;
5691
6225
  const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
5692
- return [
6226
+ const lines = [
5693
6227
  ``,
5694
6228
  `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
5695
6229
  `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
5696
6230
  `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
5697
- `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`,
6231
+ `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`
6232
+ ];
6233
+ if (localCount >= 2) {
6234
+ lines.push(
6235
+ `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
6236
+ );
6237
+ }
6238
+ lines.push(
5698
6239
  `${CYAN}\u255A${RESET}`,
5699
6240
  ``,
5700
- ` ${BOLD}${GREEN}[A]${RESET} Allow ${BOLD}${RED}[D]${RESET} Deny`,
6241
+ ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
5701
6242
  ``
5702
- ];
6243
+ );
6244
+ return lines;
5703
6245
  }
5704
6246
  async function startTail(options = {}) {
5705
6247
  const port = await ensureDaemon();
@@ -5727,7 +6269,7 @@ async function startTail(options = {}) {
5727
6269
  req2.end();
5728
6270
  });
5729
6271
  if (result.ok) {
5730
- console.log(chalk15.green("\u2713 Flight Recorder buffer cleared."));
6272
+ console.log(chalk16.green("\u2713 Flight Recorder buffer cleared."));
5731
6273
  } else if (result.code === "ECONNREFUSED") {
5732
6274
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
5733
6275
  } else if (result.code === "ETIMEDOUT") {
@@ -5744,6 +6286,7 @@ async function startTail(options = {}) {
5744
6286
  let cardActive = false;
5745
6287
  let cardLineCount = 0;
5746
6288
  let cancelActiveCard = null;
6289
+ const localAllowCounts = /* @__PURE__ */ new Map();
5747
6290
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
5748
6291
  if (canApprove) readline3.emitKeypressEvents(process.stdin);
5749
6292
  function clearCard() {
@@ -5754,7 +6297,10 @@ async function startTail(options = {}) {
5754
6297
  }
5755
6298
  function printCard(req2) {
5756
6299
  process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
5757
- const lines = buildCardLines(req2);
6300
+ const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
6301
+ const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
6302
+ const priorCount = Math.max(daemonPrior, localPrior);
6303
+ const lines = buildCardLines(req2, priorCount);
5758
6304
  for (const line of lines) process.stdout.write(line + "\n");
5759
6305
  cardLineCount = lines.length;
5760
6306
  }
@@ -5782,34 +6328,70 @@ async function startTail(options = {}) {
5782
6328
  process.stdin.pause();
5783
6329
  cancelActiveCard = null;
5784
6330
  };
5785
- const settle = (decision) => {
6331
+ const settle = (action) => {
5786
6332
  if (settled) return;
5787
6333
  settled = true;
5788
6334
  cleanup();
5789
- clearCard();
6335
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6336
+ const stampedLines = buildCardLines(
6337
+ req2,
6338
+ Math.max(
6339
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6340
+ localAllowCounts.get(req2.toolName) ?? 0
6341
+ )
6342
+ );
6343
+ const decisionStamp = action === "always-allow" ? chalk16.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk16.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
6344
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
6345
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5790
6346
  process.stdout.write(SHOW_CURSOR);
5791
- postDecisionHttp(req2.id, decision, csrfToken, port).catch((err) => {
6347
+ cardLineCount = 0;
6348
+ if (action === "allow" || action === "always-allow" || action === "trust") {
6349
+ localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
6350
+ } else if (action === "deny") {
6351
+ localAllowCounts.delete(req2.toolName);
6352
+ }
6353
+ let httpDecision;
6354
+ let httpOpts;
6355
+ if (action === "always-allow") {
6356
+ httpDecision = "allow";
6357
+ httpOpts = { persist: true };
6358
+ } else if (action === "trust") {
6359
+ httpDecision = "trust";
6360
+ httpOpts = { trustDuration: "30m" };
6361
+ } else {
6362
+ httpDecision = action;
6363
+ }
6364
+ postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
5792
6365
  try {
5793
- fs21.appendFileSync(
5794
- path23.join(os19.homedir(), ".node9", "hook-debug.log"),
6366
+ fs23.appendFileSync(
6367
+ path25.join(os21.homedir(), ".node9", "hook-debug.log"),
5795
6368
  `[tail] POST /decision failed: ${String(err)}
5796
6369
  `
5797
6370
  );
5798
6371
  } catch {
5799
6372
  }
5800
6373
  });
5801
- const decisionLabel = decision === "allow" ? chalk15.green("\u2713 ALLOWED (terminal)") : chalk15.red("\u2717 DENIED (terminal)");
5802
- console.log(`${chalk15.cyan("\u25C6")} ${chalk15.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
5803
6374
  approvalQueue.shift();
5804
6375
  cardActive = false;
5805
6376
  showNextCard();
5806
6377
  };
5807
- cancelActiveCard = () => {
6378
+ cancelActiveCard = (externalDecision) => {
5808
6379
  if (settled) return;
5809
6380
  settled = true;
5810
6381
  cleanup();
5811
- clearCard();
6382
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6383
+ const priorCount = Math.max(
6384
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6385
+ localAllowCounts.get(req2.toolName) ?? 0
6386
+ );
6387
+ const stampedLines = buildCardLines(req2, priorCount);
6388
+ if (externalDecision) {
6389
+ const source = externalDecision === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
6390
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
6391
+ }
6392
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5812
6393
  process.stdout.write(SHOW_CURSOR);
6394
+ cardLineCount = 0;
5813
6395
  approvalQueue.shift();
5814
6396
  cardActive = false;
5815
6397
  showNextCard();
@@ -5817,10 +6399,14 @@ async function startTail(options = {}) {
5817
6399
  process.stdin.resume();
5818
6400
  onKeypress = (_str, key) => {
5819
6401
  const name = key?.name ?? "";
5820
- if (name === "a") {
6402
+ if (name === "y" || name === "return") {
5821
6403
  settle("allow");
5822
- } else if (name === "d" || name === "return" || name === "enter" || key?.ctrl && name === "c") {
6404
+ } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
5823
6405
  settle("deny");
6406
+ } else if (name === "a") {
6407
+ settle("always-allow");
6408
+ } else if (name === "t") {
6409
+ settle("trust");
5824
6410
  }
5825
6411
  };
5826
6412
  process.stdin.on("keypress", onKeypress);
@@ -5833,19 +6419,27 @@ async function startTail(options = {}) {
5833
6419
  else if (process.platform === "win32")
5834
6420
  execSync3(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
5835
6421
  else execSync3(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
6422
+ const intToken = getInternalToken();
6423
+ fetch(`http://127.0.0.1:${port}/browser-opened`, {
6424
+ method: "POST",
6425
+ headers: intToken ? { "X-Node9-Internal": intToken } : {}
6426
+ }).catch(() => {
6427
+ });
5836
6428
  }
5837
6429
  } catch {
5838
6430
  }
5839
- console.log(chalk15.cyan.bold(`
5840
- \u{1F6F0}\uFE0F Node9 tail `) + chalk15.dim(`\u2192 ${dashboardUrl}`));
6431
+ console.log(chalk16.cyan.bold(`
6432
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk16.dim(`\u2192 ${dashboardUrl}`));
5841
6433
  if (canApprove) {
5842
- console.log(chalk15.dim("Interactive approvals enabled. [A] Allow [D] Deny"));
6434
+ console.log(
6435
+ chalk16.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
6436
+ );
5843
6437
  }
5844
6438
  if (options.history) {
5845
- console.log(chalk15.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
6439
+ console.log(chalk16.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
5846
6440
  } else {
5847
6441
  console.log(
5848
- chalk15.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
6442
+ chalk16.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
5849
6443
  );
5850
6444
  }
5851
6445
  process.on("SIGINT", () => {
@@ -5855,13 +6449,13 @@ async function startTail(options = {}) {
5855
6449
  readline3.clearLine(process.stdout, 0);
5856
6450
  readline3.cursorTo(process.stdout, 0);
5857
6451
  }
5858
- console.log(chalk15.dim("\n\u{1F6F0}\uFE0F Disconnected."));
6452
+ console.log(chalk16.dim("\n\u{1F6F0}\uFE0F Disconnected."));
5859
6453
  process.exit(0);
5860
6454
  });
5861
6455
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
5862
6456
  const req = http2.get(sseUrl, (res) => {
5863
6457
  if (res.statusCode !== 200) {
5864
- console.error(chalk15.red(`Failed to connect: HTTP ${res.statusCode}`));
6458
+ console.error(chalk16.red(`Failed to connect: HTTP ${res.statusCode}`));
5865
6459
  process.exit(1);
5866
6460
  }
5867
6461
  let currentEvent = "";
@@ -5891,7 +6485,7 @@ async function startTail(options = {}) {
5891
6485
  readline3.clearLine(process.stdout, 0);
5892
6486
  readline3.cursorTo(process.stdout, 0);
5893
6487
  }
5894
- console.log(chalk15.red("\n\u274C Daemon disconnected."));
6488
+ console.log(chalk16.red("\n\u274C Daemon disconnected."));
5895
6489
  process.exit(1);
5896
6490
  });
5897
6491
  });
@@ -5932,11 +6526,17 @@ async function startTail(options = {}) {
5932
6526
  }
5933
6527
  if (event === "remove") {
5934
6528
  try {
5935
- const { id } = JSON.parse(rawData);
6529
+ const { id, decision } = JSON.parse(rawData);
5936
6530
  const idx = approvalQueue.findIndex((r) => r.id === id);
5937
6531
  if (idx !== -1) {
5938
6532
  if (idx === 0 && cardActive && cancelActiveCard) {
5939
- cancelActiveCard();
6533
+ const toolName = approvalQueue[0].toolName;
6534
+ if (decision === "allow") {
6535
+ localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
6536
+ } else if (decision === "deny") {
6537
+ localAllowCounts.delete(toolName);
6538
+ }
6539
+ cancelActiveCard(decision);
5940
6540
  } else {
5941
6541
  approvalQueue.splice(idx, 1);
5942
6542
  }
@@ -5971,7 +6571,7 @@ async function startTail(options = {}) {
5971
6571
  }
5972
6572
  req.on("error", (err) => {
5973
6573
  const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
5974
- console.error(chalk15.red(`
6574
+ console.error(chalk16.red(`
5975
6575
  \u274C ${msg}`));
5976
6576
  process.exit(1);
5977
6577
  });
@@ -5981,8 +6581,9 @@ var init_tail = __esm({
5981
6581
  "src/tui/tail.ts"() {
5982
6582
  "use strict";
5983
6583
  init_daemon2();
6584
+ init_daemon();
5984
6585
  init_core();
5985
- PID_FILE = path23.join(os19.homedir(), ".node9", "daemon.pid");
6586
+ PID_FILE = path25.join(os21.homedir(), ".node9", "daemon.pid");
5986
6587
  ICONS = {
5987
6588
  bash: "\u{1F4BB}",
5988
6589
  shell: "\u{1F4BB}",
@@ -6330,6 +6931,25 @@ async function setupGemini() {
6330
6931
  printDaemonTip();
6331
6932
  }
6332
6933
  }
6934
+ function detectAgents(homeDir2 = os11.homedir()) {
6935
+ const exists = (p) => {
6936
+ try {
6937
+ return fs11.existsSync(p);
6938
+ } catch (err) {
6939
+ const code = err.code;
6940
+ if (code !== "ENOENT") {
6941
+ process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
6942
+ `);
6943
+ }
6944
+ return false;
6945
+ }
6946
+ };
6947
+ return {
6948
+ claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
6949
+ gemini: exists(path14.join(homeDir2, ".gemini")),
6950
+ cursor: exists(path14.join(homeDir2, ".cursor"))
6951
+ };
6952
+ }
6333
6953
  async function setupCursor() {
6334
6954
  const homeDir2 = os11.homedir();
6335
6955
  const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
@@ -6388,10 +7008,10 @@ async function setupCursor() {
6388
7008
 
6389
7009
  // src/cli.ts
6390
7010
  init_daemon2();
6391
- import chalk16 from "chalk";
6392
- import fs22 from "fs";
6393
- import path24 from "path";
6394
- import os20 from "os";
7011
+ import chalk17 from "chalk";
7012
+ import fs24 from "fs";
7013
+ import path26 from "path";
7014
+ import os22 from "os";
6395
7015
  import { confirm as confirm3 } from "@inquirer/prompts";
6396
7016
 
6397
7017
  // src/utils/duration.ts
@@ -6616,32 +7236,32 @@ init_daemon();
6616
7236
  init_config();
6617
7237
  init_policy();
6618
7238
  import chalk5 from "chalk";
6619
- import fs16 from "fs";
6620
- import path18 from "path";
6621
- import os14 from "os";
7239
+ import fs17 from "fs";
7240
+ import path19 from "path";
7241
+ import os15 from "os";
6622
7242
 
6623
7243
  // src/undo.ts
6624
7244
  import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
6625
7245
  import crypto2 from "crypto";
6626
- import fs15 from "fs";
6627
- import path17 from "path";
6628
- import os13 from "os";
6629
- var SNAPSHOT_STACK_PATH = path17.join(os13.homedir(), ".node9", "snapshots.json");
6630
- var UNDO_LATEST_PATH = path17.join(os13.homedir(), ".node9", "undo_latest.txt");
7246
+ import fs16 from "fs";
7247
+ import path18 from "path";
7248
+ import os14 from "os";
7249
+ var SNAPSHOT_STACK_PATH = path18.join(os14.homedir(), ".node9", "snapshots.json");
7250
+ var UNDO_LATEST_PATH = path18.join(os14.homedir(), ".node9", "undo_latest.txt");
6631
7251
  var MAX_SNAPSHOTS = 10;
6632
7252
  var GIT_TIMEOUT = 15e3;
6633
7253
  function readStack() {
6634
7254
  try {
6635
- if (fs15.existsSync(SNAPSHOT_STACK_PATH))
6636
- return JSON.parse(fs15.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
7255
+ if (fs16.existsSync(SNAPSHOT_STACK_PATH))
7256
+ return JSON.parse(fs16.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
6637
7257
  } catch {
6638
7258
  }
6639
7259
  return [];
6640
7260
  }
6641
7261
  function writeStack(stack) {
6642
- const dir = path17.dirname(SNAPSHOT_STACK_PATH);
6643
- if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
6644
- fs15.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7262
+ const dir = path18.dirname(SNAPSHOT_STACK_PATH);
7263
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
7264
+ fs16.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
6645
7265
  }
6646
7266
  function buildArgsSummary(tool, args) {
6647
7267
  if (!args || typeof args !== "object") return "";
@@ -6657,7 +7277,7 @@ function buildArgsSummary(tool, args) {
6657
7277
  function normalizeCwdForHash(cwd) {
6658
7278
  let normalized;
6659
7279
  try {
6660
- normalized = fs15.realpathSync(cwd);
7280
+ normalized = fs16.realpathSync(cwd);
6661
7281
  } catch {
6662
7282
  normalized = cwd;
6663
7283
  }
@@ -6667,16 +7287,16 @@ function normalizeCwdForHash(cwd) {
6667
7287
  }
6668
7288
  function getShadowRepoDir(cwd) {
6669
7289
  const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
6670
- return path17.join(os13.homedir(), ".node9", "snapshots", hash);
7290
+ return path18.join(os14.homedir(), ".node9", "snapshots", hash);
6671
7291
  }
6672
7292
  function cleanOrphanedIndexFiles(shadowDir) {
6673
7293
  try {
6674
7294
  const cutoff = Date.now() - 6e4;
6675
- for (const f of fs15.readdirSync(shadowDir)) {
7295
+ for (const f of fs16.readdirSync(shadowDir)) {
6676
7296
  if (f.startsWith("index_")) {
6677
- const fp = path17.join(shadowDir, f);
7297
+ const fp = path18.join(shadowDir, f);
6678
7298
  try {
6679
- if (fs15.statSync(fp).mtimeMs < cutoff) fs15.unlinkSync(fp);
7299
+ if (fs16.statSync(fp).mtimeMs < cutoff) fs16.unlinkSync(fp);
6680
7300
  } catch {
6681
7301
  }
6682
7302
  }
@@ -6688,7 +7308,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
6688
7308
  const hardcoded = [".git", ".node9"];
6689
7309
  const lines = [...hardcoded, ...ignorePaths].join("\n");
6690
7310
  try {
6691
- fs15.writeFileSync(path17.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
7311
+ fs16.writeFileSync(path18.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
6692
7312
  } catch {
6693
7313
  }
6694
7314
  }
@@ -6701,25 +7321,25 @@ function ensureShadowRepo(shadowDir, cwd) {
6701
7321
  timeout: 3e3
6702
7322
  });
6703
7323
  if (check.status === 0) {
6704
- const ptPath = path17.join(shadowDir, "project-path.txt");
7324
+ const ptPath = path18.join(shadowDir, "project-path.txt");
6705
7325
  try {
6706
- const stored = fs15.readFileSync(ptPath, "utf8").trim();
7326
+ const stored = fs16.readFileSync(ptPath, "utf8").trim();
6707
7327
  if (stored === normalizedCwd) return true;
6708
7328
  if (process.env.NODE9_DEBUG === "1")
6709
7329
  console.error(
6710
7330
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
6711
7331
  );
6712
- fs15.rmSync(shadowDir, { recursive: true, force: true });
7332
+ fs16.rmSync(shadowDir, { recursive: true, force: true });
6713
7333
  } catch {
6714
7334
  try {
6715
- fs15.writeFileSync(ptPath, normalizedCwd, "utf8");
7335
+ fs16.writeFileSync(ptPath, normalizedCwd, "utf8");
6716
7336
  } catch {
6717
7337
  }
6718
7338
  return true;
6719
7339
  }
6720
7340
  }
6721
7341
  try {
6722
- fs15.mkdirSync(shadowDir, { recursive: true });
7342
+ fs16.mkdirSync(shadowDir, { recursive: true });
6723
7343
  } catch {
6724
7344
  }
6725
7345
  const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
@@ -6728,7 +7348,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6728
7348
  console.error("[Node9] git init --bare failed:", init.stderr?.toString());
6729
7349
  return false;
6730
7350
  }
6731
- const configFile = path17.join(shadowDir, "config");
7351
+ const configFile = path18.join(shadowDir, "config");
6732
7352
  spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
6733
7353
  timeout: 3e3
6734
7354
  });
@@ -6736,7 +7356,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6736
7356
  timeout: 3e3
6737
7357
  });
6738
7358
  try {
6739
- fs15.writeFileSync(path17.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
7359
+ fs16.writeFileSync(path18.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
6740
7360
  } catch {
6741
7361
  }
6742
7362
  return true;
@@ -6759,7 +7379,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6759
7379
  const shadowDir = getShadowRepoDir(cwd);
6760
7380
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
6761
7381
  writeShadowExcludes(shadowDir, ignorePaths);
6762
- indexFile = path17.join(shadowDir, `index_${process.pid}_${Date.now()}`);
7382
+ indexFile = path18.join(shadowDir, `index_${process.pid}_${Date.now()}`);
6763
7383
  const shadowEnv = {
6764
7384
  ...process.env,
6765
7385
  GIT_DIR: shadowDir,
@@ -6788,7 +7408,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6788
7408
  const shouldGc = stack.length % 5 === 0;
6789
7409
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
6790
7410
  writeStack(stack);
6791
- fs15.writeFileSync(UNDO_LATEST_PATH, commitHash);
7411
+ fs16.writeFileSync(UNDO_LATEST_PATH, commitHash);
6792
7412
  if (shouldGc) {
6793
7413
  spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
6794
7414
  }
@@ -6799,7 +7419,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6799
7419
  } finally {
6800
7420
  if (indexFile) {
6801
7421
  try {
6802
- fs15.unlinkSync(indexFile);
7422
+ fs16.unlinkSync(indexFile);
6803
7423
  } catch {
6804
7424
  }
6805
7425
  }
@@ -6868,9 +7488,9 @@ function applyUndo(hash, cwd) {
6868
7488
  timeout: GIT_TIMEOUT
6869
7489
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
6870
7490
  for (const file of [...tracked, ...untracked]) {
6871
- const fullPath = path17.join(dir, file);
6872
- if (!snapshotFiles.has(file) && fs15.existsSync(fullPath)) {
6873
- fs15.unlinkSync(fullPath);
7491
+ const fullPath = path18.join(dir, file);
7492
+ if (!snapshotFiles.has(file) && fs16.existsSync(fullPath)) {
7493
+ fs16.unlinkSync(fullPath);
6874
7494
  }
6875
7495
  }
6876
7496
  return true;
@@ -6894,9 +7514,9 @@ function registerCheckCommand(program2) {
6894
7514
  } catch (err) {
6895
7515
  const tempConfig = getConfig();
6896
7516
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
6897
- const logPath = path18.join(os14.homedir(), ".node9", "hook-debug.log");
7517
+ const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
6898
7518
  const errMsg = err instanceof Error ? err.message : String(err);
6899
- fs16.appendFileSync(
7519
+ fs17.appendFileSync(
6900
7520
  logPath,
6901
7521
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
6902
7522
  RAW: ${raw}
@@ -6907,10 +7527,10 @@ RAW: ${raw}
6907
7527
  }
6908
7528
  const config = getConfig(payload.cwd || void 0);
6909
7529
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
6910
- const logPath = path18.join(os14.homedir(), ".node9", "hook-debug.log");
6911
- if (!fs16.existsSync(path18.dirname(logPath)))
6912
- fs16.mkdirSync(path18.dirname(logPath), { recursive: true });
6913
- fs16.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
7530
+ const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
7531
+ if (!fs17.existsSync(path19.dirname(logPath)))
7532
+ fs17.mkdirSync(path19.dirname(logPath), { recursive: true });
7533
+ fs17.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6914
7534
  `);
6915
7535
  }
6916
7536
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -6923,8 +7543,8 @@ RAW: ${raw}
6923
7543
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
6924
7544
  let ttyFd = null;
6925
7545
  try {
6926
- ttyFd = fs16.openSync("/dev/tty", "w");
6927
- const writeTty = (line) => fs16.writeSync(ttyFd, line + "\n");
7546
+ ttyFd = fs17.openSync("/dev/tty", "w");
7547
+ const writeTty = (line) => fs17.writeSync(ttyFd, line + "\n");
6928
7548
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
6929
7549
  writeTty(chalk5.bgRed.white.bold(`
6930
7550
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -6940,7 +7560,7 @@ RAW: ${raw}
6940
7560
  } finally {
6941
7561
  if (ttyFd !== null)
6942
7562
  try {
6943
- fs16.closeSync(ttyFd);
7563
+ fs17.closeSync(ttyFd);
6944
7564
  } catch {
6945
7565
  }
6946
7566
  }
@@ -6971,7 +7591,7 @@ RAW: ${raw}
6971
7591
  if (shouldSnapshot(toolName, toolInput, config)) {
6972
7592
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6973
7593
  }
6974
- const safeCwdForAuth = typeof payload.cwd === "string" && path18.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7594
+ const safeCwdForAuth = typeof payload.cwd === "string" && path19.isAbsolute(payload.cwd) ? payload.cwd : void 0;
6975
7595
  const result = await authorizeHeadless(toolName, toolInput, meta, {
6976
7596
  cwd: safeCwdForAuth
6977
7597
  });
@@ -6983,12 +7603,12 @@ RAW: ${raw}
6983
7603
  }
6984
7604
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
6985
7605
  try {
6986
- const tty = fs16.openSync("/dev/tty", "w");
6987
- fs16.writeSync(
7606
+ const tty = fs17.openSync("/dev/tty", "w");
7607
+ fs17.writeSync(
6988
7608
  tty,
6989
7609
  chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
6990
7610
  );
6991
- fs16.closeSync(tty);
7611
+ fs17.closeSync(tty);
6992
7612
  } catch {
6993
7613
  }
6994
7614
  const daemonReady = await autoStartDaemonAndWait();
@@ -7015,9 +7635,9 @@ RAW: ${raw}
7015
7635
  });
7016
7636
  } catch (err) {
7017
7637
  if (process.env.NODE9_DEBUG === "1") {
7018
- const logPath = path18.join(os14.homedir(), ".node9", "hook-debug.log");
7638
+ const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
7019
7639
  const errMsg = err instanceof Error ? err.message : String(err);
7020
- fs16.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7640
+ fs17.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7021
7641
  `);
7022
7642
  }
7023
7643
  process.exit(0);
@@ -7054,9 +7674,9 @@ RAW: ${raw}
7054
7674
  init_audit();
7055
7675
  init_config();
7056
7676
  init_policy();
7057
- import fs17 from "fs";
7058
- import path19 from "path";
7059
- import os15 from "os";
7677
+ import fs18 from "fs";
7678
+ import path20 from "path";
7679
+ import os16 from "os";
7060
7680
  function sanitize3(value) {
7061
7681
  return value.replace(/[\x00-\x1F\x7F]/g, "");
7062
7682
  }
@@ -7075,11 +7695,11 @@ function registerLogCommand(program2) {
7075
7695
  decision: "allowed",
7076
7696
  source: "post-hook"
7077
7697
  };
7078
- const logPath = path19.join(os15.homedir(), ".node9", "audit.log");
7079
- if (!fs17.existsSync(path19.dirname(logPath)))
7080
- fs17.mkdirSync(path19.dirname(logPath), { recursive: true });
7081
- fs17.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7082
- const safeCwd = typeof payload.cwd === "string" && path19.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7698
+ const logPath = path20.join(os16.homedir(), ".node9", "audit.log");
7699
+ if (!fs18.existsSync(path20.dirname(logPath)))
7700
+ fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
7701
+ fs18.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7702
+ const safeCwd = typeof payload.cwd === "string" && path20.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7083
7703
  const config = getConfig(safeCwd);
7084
7704
  if (shouldSnapshot(tool, {}, config)) {
7085
7705
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -7088,9 +7708,9 @@ function registerLogCommand(program2) {
7088
7708
  const msg = err instanceof Error ? err.message : String(err);
7089
7709
  process.stderr.write(`[Node9] audit log error: ${msg}
7090
7710
  `);
7091
- const debugPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
7711
+ const debugPath = path20.join(os16.homedir(), ".node9", "hook-debug.log");
7092
7712
  try {
7093
- fs17.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7713
+ fs18.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7094
7714
  `);
7095
7715
  } catch {
7096
7716
  }
@@ -7395,13 +8015,13 @@ function registerConfigShowCommand(program2) {
7395
8015
  // src/cli/commands/doctor.ts
7396
8016
  init_daemon();
7397
8017
  import chalk7 from "chalk";
7398
- import fs18 from "fs";
7399
- import path20 from "path";
7400
- import os16 from "os";
8018
+ import fs19 from "fs";
8019
+ import path21 from "path";
8020
+ import os17 from "os";
7401
8021
  import { execSync as execSync2 } from "child_process";
7402
8022
  function registerDoctorCommand(program2, version2) {
7403
8023
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
7404
- const homeDir2 = os16.homedir();
8024
+ const homeDir2 = os17.homedir();
7405
8025
  let failures = 0;
7406
8026
  function pass(msg) {
7407
8027
  console.log(chalk7.green(" \u2705 ") + msg);
@@ -7450,10 +8070,10 @@ function registerDoctorCommand(program2, version2) {
7450
8070
  );
7451
8071
  }
7452
8072
  section("Configuration");
7453
- const globalConfigPath = path20.join(homeDir2, ".node9", "config.json");
7454
- if (fs18.existsSync(globalConfigPath)) {
8073
+ const globalConfigPath = path21.join(homeDir2, ".node9", "config.json");
8074
+ if (fs19.existsSync(globalConfigPath)) {
7455
8075
  try {
7456
- JSON.parse(fs18.readFileSync(globalConfigPath, "utf-8"));
8076
+ JSON.parse(fs19.readFileSync(globalConfigPath, "utf-8"));
7457
8077
  pass("~/.node9/config.json found and valid");
7458
8078
  } catch {
7459
8079
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -7461,10 +8081,10 @@ function registerDoctorCommand(program2, version2) {
7461
8081
  } else {
7462
8082
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
7463
8083
  }
7464
- const projectConfigPath = path20.join(process.cwd(), "node9.config.json");
7465
- if (fs18.existsSync(projectConfigPath)) {
8084
+ const projectConfigPath = path21.join(process.cwd(), "node9.config.json");
8085
+ if (fs19.existsSync(projectConfigPath)) {
7466
8086
  try {
7467
- JSON.parse(fs18.readFileSync(projectConfigPath, "utf-8"));
8087
+ JSON.parse(fs19.readFileSync(projectConfigPath, "utf-8"));
7468
8088
  pass("node9.config.json found and valid (project)");
7469
8089
  } catch {
7470
8090
  fail(
@@ -7473,8 +8093,8 @@ function registerDoctorCommand(program2, version2) {
7473
8093
  );
7474
8094
  }
7475
8095
  }
7476
- const credsPath = path20.join(homeDir2, ".node9", "credentials.json");
7477
- if (fs18.existsSync(credsPath)) {
8096
+ const credsPath = path21.join(homeDir2, ".node9", "credentials.json");
8097
+ if (fs19.existsSync(credsPath)) {
7478
8098
  pass("Cloud credentials found (~/.node9/credentials.json)");
7479
8099
  } else {
7480
8100
  warn(
@@ -7483,10 +8103,10 @@ function registerDoctorCommand(program2, version2) {
7483
8103
  );
7484
8104
  }
7485
8105
  section("Agent Hooks");
7486
- const claudeSettingsPath = path20.join(homeDir2, ".claude", "settings.json");
7487
- if (fs18.existsSync(claudeSettingsPath)) {
8106
+ const claudeSettingsPath = path21.join(homeDir2, ".claude", "settings.json");
8107
+ if (fs19.existsSync(claudeSettingsPath)) {
7488
8108
  try {
7489
- const cs = JSON.parse(fs18.readFileSync(claudeSettingsPath, "utf-8"));
8109
+ const cs = JSON.parse(fs19.readFileSync(claudeSettingsPath, "utf-8"));
7490
8110
  const hasHook = cs.hooks?.PreToolUse?.some(
7491
8111
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7492
8112
  );
@@ -7502,10 +8122,10 @@ function registerDoctorCommand(program2, version2) {
7502
8122
  } else {
7503
8123
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
7504
8124
  }
7505
- const geminiSettingsPath = path20.join(homeDir2, ".gemini", "settings.json");
7506
- if (fs18.existsSync(geminiSettingsPath)) {
8125
+ const geminiSettingsPath = path21.join(homeDir2, ".gemini", "settings.json");
8126
+ if (fs19.existsSync(geminiSettingsPath)) {
7507
8127
  try {
7508
- const gs = JSON.parse(fs18.readFileSync(geminiSettingsPath, "utf-8"));
8128
+ const gs = JSON.parse(fs19.readFileSync(geminiSettingsPath, "utf-8"));
7509
8129
  const hasHook = gs.hooks?.BeforeTool?.some(
7510
8130
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7511
8131
  );
@@ -7521,10 +8141,10 @@ function registerDoctorCommand(program2, version2) {
7521
8141
  } else {
7522
8142
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
7523
8143
  }
7524
- const cursorHooksPath = path20.join(homeDir2, ".cursor", "hooks.json");
7525
- if (fs18.existsSync(cursorHooksPath)) {
8144
+ const cursorHooksPath = path21.join(homeDir2, ".cursor", "hooks.json");
8145
+ if (fs19.existsSync(cursorHooksPath)) {
7526
8146
  try {
7527
- const cur = JSON.parse(fs18.readFileSync(cursorHooksPath, "utf-8"));
8147
+ const cur = JSON.parse(fs19.readFileSync(cursorHooksPath, "utf-8"));
7528
8148
  const hasHook = cur.hooks?.preToolUse?.some(
7529
8149
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
7530
8150
  );
@@ -7562,9 +8182,9 @@ function registerDoctorCommand(program2, version2) {
7562
8182
 
7563
8183
  // src/cli/commands/audit.ts
7564
8184
  import chalk8 from "chalk";
7565
- import fs19 from "fs";
7566
- import path21 from "path";
7567
- import os17 from "os";
8185
+ import fs20 from "fs";
8186
+ import path22 from "path";
8187
+ import os18 from "os";
7568
8188
  function formatRelativeTime(timestamp) {
7569
8189
  const diff = Date.now() - new Date(timestamp).getTime();
7570
8190
  const sec = Math.floor(diff / 1e3);
@@ -7577,14 +8197,14 @@ function formatRelativeTime(timestamp) {
7577
8197
  }
7578
8198
  function registerAuditCommand(program2) {
7579
8199
  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) => {
7580
- const logPath = path21.join(os17.homedir(), ".node9", "audit.log");
7581
- if (!fs19.existsSync(logPath)) {
8200
+ const logPath = path22.join(os18.homedir(), ".node9", "audit.log");
8201
+ if (!fs20.existsSync(logPath)) {
7582
8202
  console.log(
7583
8203
  chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
7584
8204
  );
7585
8205
  return;
7586
8206
  }
7587
- const raw = fs19.readFileSync(logPath, "utf-8");
8207
+ const raw = fs20.readFileSync(logPath, "utf-8");
7588
8208
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
7589
8209
  let entries = lines.flatMap((line) => {
7590
8210
  try {
@@ -7704,9 +8324,42 @@ function registerDaemonCommand(program2) {
7704
8324
  init_core();
7705
8325
  init_daemon();
7706
8326
  import chalk10 from "chalk";
7707
- import fs20 from "fs";
7708
- import path22 from "path";
7709
- import os18 from "os";
8327
+ import fs21 from "fs";
8328
+ import path23 from "path";
8329
+ import os19 from "os";
8330
+ function readJson2(filePath) {
8331
+ try {
8332
+ if (fs21.existsSync(filePath)) return JSON.parse(fs21.readFileSync(filePath, "utf-8"));
8333
+ } catch {
8334
+ }
8335
+ return null;
8336
+ }
8337
+ function isNode9Hook2(cmd) {
8338
+ if (!cmd) return false;
8339
+ return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
8340
+ }
8341
+ function wrappedMcpServers(servers) {
8342
+ if (!servers) return [];
8343
+ 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(" ")}`);
8344
+ }
8345
+ function printAgentSection(label, hookPairs, wrapped) {
8346
+ console.log(chalk10.bold(` ${label}`));
8347
+ for (const { name, present } of hookPairs) {
8348
+ if (present) {
8349
+ console.log(chalk10.green(` \u2713 ${name}`));
8350
+ } else {
8351
+ console.log(chalk10.red(` \u2717 ${name}`) + chalk10.gray(" (not wired)"));
8352
+ }
8353
+ }
8354
+ if (wrapped.length > 0) {
8355
+ console.log(chalk10.cyan(` MCP proxied:`));
8356
+ for (const entry of wrapped) {
8357
+ console.log(chalk10.gray(` \u2022 ${entry}`));
8358
+ }
8359
+ } else {
8360
+ console.log(chalk10.gray(` MCP proxied: none`));
8361
+ }
8362
+ }
7710
8363
  function registerStatusCommand(program2) {
7711
8364
  program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
7712
8365
  const creds = getCredentials();
@@ -7741,19 +8394,72 @@ function registerStatusCommand(program2) {
7741
8394
  console.log("");
7742
8395
  const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
7743
8396
  console.log(` Mode: ${modeLabel}`);
7744
- const projectConfig = path22.join(process.cwd(), "node9.config.json");
7745
- const globalConfig = path22.join(os18.homedir(), ".node9", "config.json");
8397
+ const projectConfig = path23.join(process.cwd(), "node9.config.json");
8398
+ const globalConfig = path23.join(os19.homedir(), ".node9", "config.json");
7746
8399
  console.log(
7747
- ` Local: ${fs20.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
8400
+ ` Local: ${fs21.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
7748
8401
  );
7749
8402
  console.log(
7750
- ` Global: ${fs20.existsSync(globalConfig) ? chalk10.green("Active (~/.node9/config.json)") : chalk10.gray("Not present")}`
8403
+ ` Global: ${fs21.existsSync(globalConfig) ? chalk10.green("Active (~/.node9/config.json)") : chalk10.gray("Not present")}`
7751
8404
  );
7752
8405
  if (mergedConfig.policy.sandboxPaths.length > 0) {
7753
8406
  console.log(
7754
8407
  ` Sandbox: ${chalk10.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
7755
8408
  );
7756
8409
  }
8410
+ const homeDir2 = os19.homedir();
8411
+ const claudeSettings = readJson2(
8412
+ path23.join(homeDir2, ".claude", "settings.json")
8413
+ );
8414
+ const claudeConfig = readJson2(path23.join(homeDir2, ".claude.json"));
8415
+ const geminiSettings = readJson2(
8416
+ path23.join(homeDir2, ".gemini", "settings.json")
8417
+ );
8418
+ const cursorConfig = readJson2(path23.join(homeDir2, ".cursor", "mcp.json"));
8419
+ const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
8420
+ if (agentFound) {
8421
+ console.log("");
8422
+ console.log(chalk10.bold(" Agent Wiring:"));
8423
+ console.log("");
8424
+ if (claudeSettings || claudeConfig) {
8425
+ const preHook = claudeSettings?.hooks?.PreToolUse?.some(
8426
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8427
+ ) ?? false;
8428
+ const postHook = claudeSettings?.hooks?.PostToolUse?.some(
8429
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8430
+ ) ?? false;
8431
+ printAgentSection(
8432
+ "Claude Code",
8433
+ [
8434
+ { name: "PreToolUse (node9 check)", present: preHook },
8435
+ { name: "PostToolUse (node9 log)", present: postHook }
8436
+ ],
8437
+ wrappedMcpServers(claudeConfig?.mcpServers)
8438
+ );
8439
+ console.log("");
8440
+ }
8441
+ if (geminiSettings) {
8442
+ const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
8443
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8444
+ ) ?? false;
8445
+ const afterHook = geminiSettings.hooks?.AfterTool?.some(
8446
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8447
+ ) ?? false;
8448
+ printAgentSection(
8449
+ "Gemini CLI",
8450
+ [
8451
+ { name: "BeforeTool (node9 check)", present: beforeHook },
8452
+ { name: "AfterTool (node9 log)", present: afterHook }
8453
+ ],
8454
+ wrappedMcpServers(geminiSettings.mcpServers)
8455
+ );
8456
+ console.log("");
8457
+ }
8458
+ if (cursorConfig) {
8459
+ printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
8460
+ console.log("");
8461
+ }
8462
+ }
7757
8463
  const pauseState = checkPause();
7758
8464
  if (pauseState.paused) {
7759
8465
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
@@ -7766,8 +8472,63 @@ function registerStatusCommand(program2) {
7766
8472
  });
7767
8473
  }
7768
8474
 
7769
- // src/cli/commands/undo.ts
8475
+ // src/cli/commands/init.ts
8476
+ init_core();
7770
8477
  import chalk11 from "chalk";
8478
+ import fs22 from "fs";
8479
+ import path24 from "path";
8480
+ import os20 from "os";
8481
+ function registerInitCommand(program2) {
8482
+ 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) => {
8483
+ console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
8484
+ const configPath = path24.join(os20.homedir(), ".node9", "config.json");
8485
+ if (fs22.existsSync(configPath) && !options.force) {
8486
+ console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8487
+ } else {
8488
+ const requestedMode = options.mode.toLowerCase();
8489
+ const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8490
+ const configToSave = {
8491
+ ...DEFAULT_CONFIG,
8492
+ settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
8493
+ };
8494
+ const dir = path24.dirname(configPath);
8495
+ if (!fs22.existsSync(dir)) fs22.mkdirSync(dir, { recursive: true });
8496
+ fs22.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8497
+ console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
8498
+ console.log(chalk11.gray(` Mode: ${safeMode}`));
8499
+ }
8500
+ if (options.skipSetup) return;
8501
+ console.log("");
8502
+ const detected = detectAgents();
8503
+ const found = Object.keys(detected).filter(
8504
+ (k) => detected[k]
8505
+ );
8506
+ if (found.length === 0) {
8507
+ console.log(
8508
+ chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
8509
+ );
8510
+ console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
8511
+ return;
8512
+ }
8513
+ console.log(chalk11.bold("Detected agents:"));
8514
+ for (const agent of found) {
8515
+ console.log(chalk11.green(` \u2713 ${agent}`));
8516
+ }
8517
+ console.log("");
8518
+ for (const agent of found) {
8519
+ console.log(chalk11.bold(`Wiring ${agent}...`));
8520
+ if (agent === "claude") await setupClaude();
8521
+ else if (agent === "gemini") await setupGemini();
8522
+ else if (agent === "cursor") await setupCursor();
8523
+ console.log("");
8524
+ }
8525
+ console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
8526
+ console.log(chalk11.gray(" Run: node9 daemon start"));
8527
+ });
8528
+ }
8529
+
8530
+ // src/cli/commands/undo.ts
8531
+ import chalk12 from "chalk";
7771
8532
  import { confirm as confirm2 } from "@inquirer/prompts";
7772
8533
  function registerUndoCommand(program2) {
7773
8534
  program2.command("undo").description(
@@ -7779,22 +8540,22 @@ function registerUndoCommand(program2) {
7779
8540
  if (history.length === 0) {
7780
8541
  if (!options.all && allHistory.length > 0) {
7781
8542
  console.log(
7782
- chalk11.yellow(
8543
+ chalk12.yellow(
7783
8544
  `
7784
8545
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
7785
- Run ${chalk11.cyan("node9 undo --all")} to see snapshots from all projects.
8546
+ Run ${chalk12.cyan("node9 undo --all")} to see snapshots from all projects.
7786
8547
  `
7787
8548
  )
7788
8549
  );
7789
8550
  } else {
7790
- console.log(chalk11.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
8551
+ console.log(chalk12.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
7791
8552
  }
7792
8553
  return;
7793
8554
  }
7794
8555
  const idx = history.length - steps;
7795
8556
  if (idx < 0) {
7796
8557
  console.log(
7797
- chalk11.yellow(
8558
+ chalk12.yellow(
7798
8559
  `
7799
8560
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
7800
8561
  `
@@ -7806,19 +8567,19 @@ function registerUndoCommand(program2) {
7806
8567
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
7807
8568
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
7808
8569
  console.log(
7809
- chalk11.magenta.bold(`
8570
+ chalk12.magenta.bold(`
7810
8571
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
7811
8572
  );
7812
8573
  console.log(
7813
- chalk11.white(
7814
- ` Tool: ${chalk11.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk11.gray(" \u2192 " + snapshot.argsSummary) : ""}`
8574
+ chalk12.white(
8575
+ ` Tool: ${chalk12.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk12.gray(" \u2192 " + snapshot.argsSummary) : ""}`
7815
8576
  )
7816
8577
  );
7817
- console.log(chalk11.white(` When: ${chalk11.gray(ageStr)}`));
7818
- console.log(chalk11.white(` Dir: ${chalk11.gray(snapshot.cwd)}`));
8578
+ console.log(chalk12.white(` When: ${chalk12.gray(ageStr)}`));
8579
+ console.log(chalk12.white(` Dir: ${chalk12.gray(snapshot.cwd)}`));
7819
8580
  if (steps > 1)
7820
8581
  console.log(
7821
- chalk11.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
8582
+ chalk12.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
7822
8583
  );
7823
8584
  console.log("");
7824
8585
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -7826,21 +8587,21 @@ function registerUndoCommand(program2) {
7826
8587
  const lines = diff.split("\n");
7827
8588
  for (const line of lines) {
7828
8589
  if (line.startsWith("+++") || line.startsWith("---")) {
7829
- console.log(chalk11.bold(line));
8590
+ console.log(chalk12.bold(line));
7830
8591
  } else if (line.startsWith("+")) {
7831
- console.log(chalk11.green(line));
8592
+ console.log(chalk12.green(line));
7832
8593
  } else if (line.startsWith("-")) {
7833
- console.log(chalk11.red(line));
8594
+ console.log(chalk12.red(line));
7834
8595
  } else if (line.startsWith("@@")) {
7835
- console.log(chalk11.cyan(line));
8596
+ console.log(chalk12.cyan(line));
7836
8597
  } else {
7837
- console.log(chalk11.gray(line));
8598
+ console.log(chalk12.gray(line));
7838
8599
  }
7839
8600
  }
7840
8601
  console.log("");
7841
8602
  } else {
7842
8603
  console.log(
7843
- chalk11.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
8604
+ chalk12.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
7844
8605
  );
7845
8606
  }
7846
8607
  const proceed = await confirm2({
@@ -7849,19 +8610,19 @@ function registerUndoCommand(program2) {
7849
8610
  });
7850
8611
  if (proceed) {
7851
8612
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
7852
- console.log(chalk11.green("\n\u2705 Reverted successfully.\n"));
8613
+ console.log(chalk12.green("\n\u2705 Reverted successfully.\n"));
7853
8614
  } else {
7854
- console.error(chalk11.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
8615
+ console.error(chalk12.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
7855
8616
  }
7856
8617
  } else {
7857
- console.log(chalk11.gray("\nCancelled.\n"));
8618
+ console.log(chalk12.gray("\nCancelled.\n"));
7858
8619
  }
7859
8620
  });
7860
8621
  }
7861
8622
 
7862
8623
  // src/cli/commands/watch.ts
7863
8624
  init_daemon();
7864
- import chalk12 from "chalk";
8625
+ import chalk13 from "chalk";
7865
8626
  import { spawn as spawn7, spawnSync as spawnSync5 } from "child_process";
7866
8627
  function registerWatchCommand(program2) {
7867
8628
  program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
@@ -7877,7 +8638,7 @@ function registerWatchCommand(program2) {
7877
8638
  throw new Error("not running");
7878
8639
  }
7879
8640
  } catch {
7880
- console.error(chalk12.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
8641
+ console.error(chalk13.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
7881
8642
  const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
7882
8643
  detached: true,
7883
8644
  stdio: "ignore",
@@ -7899,12 +8660,12 @@ function registerWatchCommand(program2) {
7899
8660
  }
7900
8661
  }
7901
8662
  if (!ready) {
7902
- console.error(chalk12.red("\u274C Daemon failed to start. Try: node9 daemon start"));
8663
+ console.error(chalk13.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7903
8664
  process.exit(1);
7904
8665
  }
7905
8666
  }
7906
8667
  console.error(
7907
- chalk12.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk12.dim(` \u2192 localhost:${port}`) + chalk12.dim(
8668
+ chalk13.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk13.dim(` \u2192 localhost:${port}`) + chalk13.dim(
7908
8669
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
7909
8670
  )
7910
8671
  );
@@ -7913,7 +8674,7 @@ function registerWatchCommand(program2) {
7913
8674
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
7914
8675
  });
7915
8676
  if (result.error) {
7916
- console.error(chalk12.red(`\u274C Failed to run command: ${result.error.message}`));
8677
+ console.error(chalk13.red(`\u274C Failed to run command: ${result.error.message}`));
7917
8678
  process.exit(1);
7918
8679
  }
7919
8680
  process.exit(result.status ?? 0);
@@ -7923,7 +8684,7 @@ function registerWatchCommand(program2) {
7923
8684
  // src/mcp-gateway/index.ts
7924
8685
  init_orchestrator();
7925
8686
  import readline2 from "readline";
7926
- import chalk13 from "chalk";
8687
+ import chalk14 from "chalk";
7927
8688
  import { spawn as spawn8 } from "child_process";
7928
8689
  import { execa as execa2 } from "execa";
7929
8690
  init_provenance();
@@ -7986,13 +8747,13 @@ async function runMcpGateway(upstreamCommand) {
7986
8747
  const prov = checkProvenance(executable);
7987
8748
  if (prov.trustLevel === "suspect") {
7988
8749
  console.error(
7989
- chalk13.red(
8750
+ chalk14.red(
7990
8751
  `\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
7991
8752
  )
7992
8753
  );
7993
- console.error(chalk13.red(" Verify this binary is trusted before proceeding."));
8754
+ console.error(chalk14.red(" Verify this binary is trusted before proceeding."));
7994
8755
  }
7995
- console.error(chalk13.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
8756
+ console.error(chalk14.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
7996
8757
  const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
7997
8758
  "NODE_OPTIONS",
7998
8759
  "NODE_PATH",
@@ -8056,10 +8817,10 @@ async function runMcpGateway(upstreamCommand) {
8056
8817
  mcpServer
8057
8818
  });
8058
8819
  if (!result.approved) {
8059
- console.error(chalk13.red(`
8820
+ console.error(chalk14.red(`
8060
8821
  \u{1F6D1} Node9 MCP Gateway: Action Blocked`));
8061
- console.error(chalk13.gray(` Tool: ${toolName}`));
8062
- console.error(chalk13.gray(` Reason: ${result.reason ?? "Security Policy"}
8822
+ console.error(chalk14.gray(` Tool: ${toolName}`));
8823
+ console.error(chalk14.gray(` Reason: ${result.reason ?? "Security Policy"}
8063
8824
  `));
8064
8825
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
8065
8826
  const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -8133,7 +8894,7 @@ function registerMcpGatewayCommand(program2) {
8133
8894
 
8134
8895
  // src/cli/commands/trust.ts
8135
8896
  init_trusted_hosts();
8136
- import chalk14 from "chalk";
8897
+ import chalk15 from "chalk";
8137
8898
  function isValidHost(host) {
8138
8899
  return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
8139
8900
  }
@@ -8143,44 +8904,44 @@ function registerTrustCommand(program2) {
8143
8904
  const normalized = normalizeHost(host.trim());
8144
8905
  if (!isValidHost(normalized)) {
8145
8906
  console.error(
8146
- chalk14.red(`
8907
+ chalk15.red(`
8147
8908
  \u274C Invalid host: "${host}"
8148
- `) + chalk14.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
8909
+ `) + chalk15.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
8149
8910
  );
8150
8911
  process.exit(1);
8151
8912
  }
8152
8913
  addTrustedHost(normalized);
8153
- console.log(chalk14.green(`
8914
+ console.log(chalk15.green(`
8154
8915
  \u2705 ${normalized} added to trusted hosts.`));
8155
8916
  console.log(
8156
- chalk14.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
8917
+ chalk15.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
8157
8918
  );
8158
8919
  });
8159
8920
  trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
8160
8921
  const normalized = normalizeHost(host.trim());
8161
8922
  const removed = removeTrustedHost(normalized);
8162
8923
  if (!removed) {
8163
- console.error(chalk14.yellow(`
8924
+ console.error(chalk15.yellow(`
8164
8925
  \u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
8165
8926
  `));
8166
8927
  process.exit(1);
8167
8928
  }
8168
- console.log(chalk14.green(`
8929
+ console.log(chalk15.green(`
8169
8930
  \u2705 ${normalized} removed from trusted hosts.
8170
8931
  `));
8171
8932
  });
8172
8933
  trustCmd.command("list").description("Show all trusted hosts").action(() => {
8173
8934
  const hosts = readTrustedHosts();
8174
8935
  if (hosts.length === 0) {
8175
- console.log(chalk14.gray("\n No trusted hosts configured.\n"));
8176
- console.log(` Add one: ${chalk14.cyan("node9 trust add api.mycompany.com")}
8936
+ console.log(chalk15.gray("\n No trusted hosts configured.\n"));
8937
+ console.log(` Add one: ${chalk15.cyan("node9 trust add api.mycompany.com")}
8177
8938
  `);
8178
8939
  return;
8179
8940
  }
8180
- console.log(chalk14.bold("\n\u{1F513} Trusted Hosts\n"));
8941
+ console.log(chalk15.bold("\n\u{1F513} Trusted Hosts\n"));
8181
8942
  for (const entry of hosts) {
8182
8943
  const date = new Date(entry.addedAt).toLocaleDateString();
8183
- console.log(` ${chalk14.cyan(entry.host.padEnd(40))} ${chalk14.gray(`added ${date}`)}`);
8944
+ console.log(` ${chalk15.cyan(entry.host.padEnd(40))} ${chalk15.gray(`added ${date}`)}`);
8184
8945
  }
8185
8946
  console.log("");
8186
8947
  });
@@ -8188,20 +8949,20 @@ function registerTrustCommand(program2) {
8188
8949
 
8189
8950
  // src/cli.ts
8190
8951
  var { version } = JSON.parse(
8191
- fs22.readFileSync(path24.join(__dirname, "../package.json"), "utf-8")
8952
+ fs24.readFileSync(path26.join(__dirname, "../package.json"), "utf-8")
8192
8953
  );
8193
8954
  var program = new Command();
8194
8955
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
8195
8956
  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) => {
8196
8957
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
8197
- const credPath = path24.join(os20.homedir(), ".node9", "credentials.json");
8198
- if (!fs22.existsSync(path24.dirname(credPath)))
8199
- fs22.mkdirSync(path24.dirname(credPath), { recursive: true });
8958
+ const credPath = path26.join(os22.homedir(), ".node9", "credentials.json");
8959
+ if (!fs24.existsSync(path26.dirname(credPath)))
8960
+ fs24.mkdirSync(path26.dirname(credPath), { recursive: true });
8200
8961
  const profileName = options.profile || "default";
8201
8962
  let existingCreds = {};
8202
8963
  try {
8203
- if (fs22.existsSync(credPath)) {
8204
- const raw = JSON.parse(fs22.readFileSync(credPath, "utf-8"));
8964
+ if (fs24.existsSync(credPath)) {
8965
+ const raw = JSON.parse(fs24.readFileSync(credPath, "utf-8"));
8205
8966
  if (raw.apiKey) {
8206
8967
  existingCreds = {
8207
8968
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -8213,13 +8974,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8213
8974
  } catch {
8214
8975
  }
8215
8976
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
8216
- fs22.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
8977
+ fs24.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
8217
8978
  if (profileName === "default") {
8218
- const configPath = path24.join(os20.homedir(), ".node9", "config.json");
8979
+ const configPath = path26.join(os22.homedir(), ".node9", "config.json");
8219
8980
  let config = {};
8220
8981
  try {
8221
- if (fs22.existsSync(configPath))
8222
- config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
8982
+ if (fs24.existsSync(configPath))
8983
+ config = JSON.parse(fs24.readFileSync(configPath, "utf-8"));
8223
8984
  } catch {
8224
8985
  }
8225
8986
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -8234,36 +8995,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8234
8995
  approvers.cloud = false;
8235
8996
  }
8236
8997
  s.approvers = approvers;
8237
- if (!fs22.existsSync(path24.dirname(configPath)))
8238
- fs22.mkdirSync(path24.dirname(configPath), { recursive: true });
8239
- fs22.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
8998
+ if (!fs24.existsSync(path26.dirname(configPath)))
8999
+ fs24.mkdirSync(path26.dirname(configPath), { recursive: true });
9000
+ fs24.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
8240
9001
  }
8241
9002
  if (options.profile && profileName !== "default") {
8242
- console.log(chalk16.green(`\u2705 Profile "${profileName}" saved`));
8243
- console.log(chalk16.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
9003
+ console.log(chalk17.green(`\u2705 Profile "${profileName}" saved`));
9004
+ console.log(chalk17.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
8244
9005
  } else if (options.local) {
8245
- console.log(chalk16.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
8246
- console.log(chalk16.gray(` All decisions stay on this machine.`));
9006
+ console.log(chalk17.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
9007
+ console.log(chalk17.gray(` All decisions stay on this machine.`));
8247
9008
  } else {
8248
- console.log(chalk16.green(`\u2705 Logged in \u2014 agent mode`));
8249
- console.log(chalk16.gray(` Team policy enforced for all calls via Node9 cloud.`));
9009
+ console.log(chalk17.green(`\u2705 Logged in \u2014 agent mode`));
9010
+ console.log(chalk17.gray(` Team policy enforced for all calls via Node9 cloud.`));
8250
9011
  }
8251
9012
  });
8252
9013
  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) => {
8253
9014
  if (target === "gemini") return await setupGemini();
8254
9015
  if (target === "claude") return await setupClaude();
8255
9016
  if (target === "cursor") return await setupCursor();
8256
- console.error(chalk16.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9017
+ console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8257
9018
  process.exit(1);
8258
9019
  });
8259
9020
  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) => {
8260
9021
  if (!target) {
8261
- console.log(chalk16.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
8262
- console.log(" Usage: " + chalk16.white("node9 setup <target>") + "\n");
9022
+ console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
9023
+ console.log(" Usage: " + chalk17.white("node9 setup <target>") + "\n");
8263
9024
  console.log(" Targets:");
8264
- console.log(" " + chalk16.green("claude") + " \u2014 Claude Code (hook mode)");
8265
- console.log(" " + chalk16.green("gemini") + " \u2014 Gemini CLI (hook mode)");
8266
- console.log(" " + chalk16.green("cursor") + " \u2014 Cursor (hook mode)");
9025
+ console.log(" " + chalk17.green("claude") + " \u2014 Claude Code (hook mode)");
9026
+ console.log(" " + chalk17.green("gemini") + " \u2014 Gemini CLI (hook mode)");
9027
+ console.log(" " + chalk17.green("cursor") + " \u2014 Cursor (hook mode)");
8267
9028
  console.log("");
8268
9029
  return;
8269
9030
  }
@@ -8271,7 +9032,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
8271
9032
  if (t === "gemini") return await setupGemini();
8272
9033
  if (t === "claude") return await setupClaude();
8273
9034
  if (t === "cursor") return await setupCursor();
8274
- console.error(chalk16.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9035
+ console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8275
9036
  process.exit(1);
8276
9037
  });
8277
9038
  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) => {
@@ -8280,30 +9041,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
8280
9041
  else if (target === "gemini") fn = teardownGemini;
8281
9042
  else if (target === "cursor") fn = teardownCursor;
8282
9043
  else {
8283
- console.error(chalk16.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9044
+ console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8284
9045
  process.exit(1);
8285
9046
  }
8286
- console.log(chalk16.cyan(`
9047
+ console.log(chalk17.cyan(`
8287
9048
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
8288
9049
  `));
8289
9050
  try {
8290
9051
  fn();
8291
9052
  } catch (err) {
8292
- console.error(chalk16.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
9053
+ console.error(chalk17.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
8293
9054
  process.exit(1);
8294
9055
  }
8295
- console.log(chalk16.gray("\n Restart the agent for changes to take effect."));
9056
+ console.log(chalk17.gray("\n Restart the agent for changes to take effect."));
8296
9057
  });
8297
9058
  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) => {
8298
- console.log(chalk16.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
8299
- console.log(chalk16.bold("Stopping daemon..."));
9059
+ console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
9060
+ console.log(chalk17.bold("Stopping daemon..."));
8300
9061
  try {
8301
9062
  stopDaemon();
8302
- console.log(chalk16.green(" \u2705 Daemon stopped"));
9063
+ console.log(chalk17.green(" \u2705 Daemon stopped"));
8303
9064
  } catch {
8304
- console.log(chalk16.blue(" \u2139\uFE0F Daemon was not running"));
9065
+ console.log(chalk17.blue(" \u2139\uFE0F Daemon was not running"));
8305
9066
  }
8306
- console.log(chalk16.bold("\nRemoving hooks..."));
9067
+ console.log(chalk17.bold("\nRemoving hooks..."));
8307
9068
  let teardownFailed = false;
8308
9069
  for (const [label, fn] of [
8309
9070
  ["Claude", teardownClaude],
@@ -8315,45 +9076,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
8315
9076
  } catch (err) {
8316
9077
  teardownFailed = true;
8317
9078
  console.error(
8318
- chalk16.red(
9079
+ chalk17.red(
8319
9080
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
8320
9081
  )
8321
9082
  );
8322
9083
  }
8323
9084
  }
8324
9085
  if (options.purge) {
8325
- const node9Dir = path24.join(os20.homedir(), ".node9");
8326
- if (fs22.existsSync(node9Dir)) {
9086
+ const node9Dir = path26.join(os22.homedir(), ".node9");
9087
+ if (fs24.existsSync(node9Dir)) {
8327
9088
  const confirmed = await confirm3({
8328
9089
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
8329
9090
  default: false
8330
9091
  });
8331
9092
  if (confirmed) {
8332
- fs22.rmSync(node9Dir, { recursive: true });
8333
- if (fs22.existsSync(node9Dir)) {
9093
+ fs24.rmSync(node9Dir, { recursive: true });
9094
+ if (fs24.existsSync(node9Dir)) {
8334
9095
  console.error(
8335
- chalk16.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9096
+ chalk17.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
8336
9097
  );
8337
9098
  } else {
8338
- console.log(chalk16.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
9099
+ console.log(chalk17.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
8339
9100
  }
8340
9101
  } else {
8341
- console.log(chalk16.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
9102
+ console.log(chalk17.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
8342
9103
  }
8343
9104
  } else {
8344
- console.log(chalk16.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
9105
+ console.log(chalk17.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
8345
9106
  }
8346
9107
  } else {
8347
9108
  console.log(
8348
- chalk16.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
9109
+ chalk17.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
8349
9110
  );
8350
9111
  }
8351
9112
  if (teardownFailed) {
8352
- console.error(chalk16.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
9113
+ console.error(chalk17.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
8353
9114
  process.exit(1);
8354
9115
  }
8355
- console.log(chalk16.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
8356
- console.log(chalk16.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
9116
+ console.log(chalk17.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
9117
+ console.log(chalk17.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
8357
9118
  });
8358
9119
  registerDoctorCommand(program, version);
8359
9120
  program.command("explain").description(
@@ -8366,7 +9127,7 @@ program.command("explain").description(
8366
9127
  try {
8367
9128
  args = JSON.parse(trimmed);
8368
9129
  } catch {
8369
- console.error(chalk16.red(`
9130
+ console.error(chalk17.red(`
8370
9131
  \u274C Invalid JSON: ${trimmed}
8371
9132
  `));
8372
9133
  process.exit(1);
@@ -8377,83 +9138,59 @@ program.command("explain").description(
8377
9138
  }
8378
9139
  const result = await explainPolicy(tool, args);
8379
9140
  console.log("");
8380
- console.log(chalk16.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
9141
+ console.log(chalk17.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
8381
9142
  console.log("");
8382
- console.log(` ${chalk16.bold("Tool:")} ${chalk16.white(result.tool)}`);
9143
+ console.log(` ${chalk17.bold("Tool:")} ${chalk17.white(result.tool)}`);
8383
9144
  if (argsRaw) {
8384
9145
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
8385
- console.log(` ${chalk16.bold("Input:")} ${chalk16.gray(preview)}`);
9146
+ console.log(` ${chalk17.bold("Input:")} ${chalk17.gray(preview)}`);
8386
9147
  }
8387
9148
  console.log("");
8388
- console.log(chalk16.bold("Config Sources (Waterfall):"));
9149
+ console.log(chalk17.bold("Config Sources (Waterfall):"));
8389
9150
  for (const tier of result.waterfall) {
8390
- const num = chalk16.gray(` ${tier.tier}.`);
9151
+ const num = chalk17.gray(` ${tier.tier}.`);
8391
9152
  const label = tier.label.padEnd(16);
8392
9153
  let statusStr;
8393
9154
  if (tier.tier === 1) {
8394
- statusStr = chalk16.gray(tier.note ?? "");
9155
+ statusStr = chalk17.gray(tier.note ?? "");
8395
9156
  } else if (tier.status === "active") {
8396
- const loc = tier.path ? chalk16.gray(tier.path) : "";
8397
- const note = tier.note ? chalk16.gray(`(${tier.note})`) : "";
8398
- statusStr = chalk16.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
9157
+ const loc = tier.path ? chalk17.gray(tier.path) : "";
9158
+ const note = tier.note ? chalk17.gray(`(${tier.note})`) : "";
9159
+ statusStr = chalk17.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
8399
9160
  } else {
8400
- statusStr = chalk16.gray("\u25CB " + (tier.note ?? "not found"));
9161
+ statusStr = chalk17.gray("\u25CB " + (tier.note ?? "not found"));
8401
9162
  }
8402
- console.log(`${num} ${chalk16.white(label)} ${statusStr}`);
9163
+ console.log(`${num} ${chalk17.white(label)} ${statusStr}`);
8403
9164
  }
8404
9165
  console.log("");
8405
- console.log(chalk16.bold("Policy Evaluation:"));
9166
+ console.log(chalk17.bold("Policy Evaluation:"));
8406
9167
  for (const step of result.steps) {
8407
9168
  const isFinal = step.isFinal;
8408
9169
  let icon;
8409
- if (step.outcome === "allow") icon = chalk16.green(" \u2705");
8410
- else if (step.outcome === "review") icon = chalk16.red(" \u{1F534}");
8411
- else if (step.outcome === "skip") icon = chalk16.gray(" \u2500 ");
8412
- else icon = chalk16.gray(" \u25CB ");
9170
+ if (step.outcome === "allow") icon = chalk17.green(" \u2705");
9171
+ else if (step.outcome === "review") icon = chalk17.red(" \u{1F534}");
9172
+ else if (step.outcome === "skip") icon = chalk17.gray(" \u2500 ");
9173
+ else icon = chalk17.gray(" \u25CB ");
8413
9174
  const name = step.name.padEnd(18);
8414
- const nameStr = isFinal ? chalk16.white.bold(name) : chalk16.white(name);
8415
- const detail = isFinal ? chalk16.white(step.detail) : chalk16.gray(step.detail);
8416
- const arrow = isFinal ? chalk16.yellow(" \u2190 STOP") : "";
9175
+ const nameStr = isFinal ? chalk17.white.bold(name) : chalk17.white(name);
9176
+ const detail = isFinal ? chalk17.white(step.detail) : chalk17.gray(step.detail);
9177
+ const arrow = isFinal ? chalk17.yellow(" \u2190 STOP") : "";
8417
9178
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
8418
9179
  }
8419
9180
  console.log("");
8420
9181
  if (result.decision === "allow") {
8421
- console.log(chalk16.green.bold(" Decision: \u2705 ALLOW") + chalk16.gray(" \u2014 no approval needed"));
9182
+ console.log(chalk17.green.bold(" Decision: \u2705 ALLOW") + chalk17.gray(" \u2014 no approval needed"));
8422
9183
  } else {
8423
9184
  console.log(
8424
- chalk16.red.bold(" Decision: \u{1F534} REVIEW") + chalk16.gray(" \u2014 human approval required")
9185
+ chalk17.red.bold(" Decision: \u{1F534} REVIEW") + chalk17.gray(" \u2014 human approval required")
8425
9186
  );
8426
9187
  if (result.blockedByLabel) {
8427
- console.log(chalk16.gray(` Reason: ${result.blockedByLabel}`));
9188
+ console.log(chalk17.gray(` Reason: ${result.blockedByLabel}`));
8428
9189
  }
8429
9190
  }
8430
9191
  console.log("");
8431
9192
  });
8432
- 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) => {
8433
- const configPath = path24.join(os20.homedir(), ".node9", "config.json");
8434
- if (fs22.existsSync(configPath) && !options.force) {
8435
- console.log(chalk16.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
8436
- console.log(chalk16.gray(` Run with --force to overwrite.`));
8437
- return;
8438
- }
8439
- const requestedMode = options.mode.toLowerCase();
8440
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8441
- const configToSave = {
8442
- ...DEFAULT_CONFIG,
8443
- settings: {
8444
- ...DEFAULT_CONFIG.settings,
8445
- mode: safeMode
8446
- }
8447
- };
8448
- const dir = path24.dirname(configPath);
8449
- if (!fs22.existsSync(dir)) fs22.mkdirSync(dir, { recursive: true });
8450
- fs22.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8451
- console.log(chalk16.green(`\u2705 Global config created: ${configPath}`));
8452
- console.log(chalk16.cyan(` Mode set to: ${safeMode}`));
8453
- console.log(
8454
- chalk16.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
8455
- );
8456
- });
9193
+ registerInitCommand(program);
8457
9194
  registerAuditCommand(program);
8458
9195
  registerStatusCommand(program);
8459
9196
  registerDaemonCommand(program);
@@ -8462,7 +9199,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
8462
9199
  try {
8463
9200
  await startTail2(options);
8464
9201
  } catch (err) {
8465
- console.error(chalk16.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
9202
+ console.error(chalk17.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
8466
9203
  process.exit(1);
8467
9204
  }
8468
9205
  });
@@ -8474,7 +9211,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8474
9211
  const ms = parseDuration(options.duration);
8475
9212
  if (ms === null) {
8476
9213
  console.error(
8477
- chalk16.red(`
9214
+ chalk17.red(`
8478
9215
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
8479
9216
  `)
8480
9217
  );
@@ -8482,20 +9219,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8482
9219
  }
8483
9220
  pauseNode9(ms, options.duration);
8484
9221
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
8485
- console.log(chalk16.yellow(`
9222
+ console.log(chalk17.yellow(`
8486
9223
  \u23F8 Node9 paused until ${expiresAt}`));
8487
- console.log(chalk16.gray(` All tool calls will be allowed without review.`));
8488
- console.log(chalk16.gray(` Run "node9 resume" to re-enable early.
9224
+ console.log(chalk17.gray(` All tool calls will be allowed without review.`));
9225
+ console.log(chalk17.gray(` Run "node9 resume" to re-enable early.
8489
9226
  `));
8490
9227
  });
8491
9228
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
8492
9229
  const { paused } = checkPause();
8493
9230
  if (!paused) {
8494
- console.log(chalk16.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
9231
+ console.log(chalk17.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
8495
9232
  return;
8496
9233
  }
8497
9234
  resumeNode9();
8498
- console.log(chalk16.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
9235
+ console.log(chalk17.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
8499
9236
  });
8500
9237
  var HOOK_BASED_AGENTS = {
8501
9238
  claude: "claude",
@@ -8508,15 +9245,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8508
9245
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
8509
9246
  const target = HOOK_BASED_AGENTS[firstArg2];
8510
9247
  console.error(
8511
- chalk16.yellow(`
9248
+ chalk17.yellow(`
8512
9249
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
8513
9250
  );
8514
- console.error(chalk16.white(`
9251
+ console.error(chalk17.white(`
8515
9252
  "${target}" uses its own hook system. Use:`));
8516
9253
  console.error(
8517
- chalk16.green(` node9 addto ${target} `) + chalk16.gray("# one-time setup")
9254
+ chalk17.green(` node9 addto ${target} `) + chalk17.gray("# one-time setup")
8518
9255
  );
8519
- console.error(chalk16.green(` ${target} `) + chalk16.gray("# run normally"));
9256
+ console.error(chalk17.green(` ${target} `) + chalk17.gray("# run normally"));
8520
9257
  process.exit(1);
8521
9258
  }
8522
9259
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -8533,7 +9270,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8533
9270
  }
8534
9271
  );
8535
9272
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
8536
- console.error(chalk16.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
9273
+ console.error(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
8537
9274
  const daemonReady = await autoStartDaemonAndWait();
8538
9275
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
8539
9276
  }
@@ -8546,12 +9283,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8546
9283
  }
8547
9284
  if (!result.approved) {
8548
9285
  console.error(
8549
- chalk16.red(`
9286
+ chalk17.red(`
8550
9287
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
8551
9288
  );
8552
9289
  process.exit(1);
8553
9290
  }
8554
- console.error(chalk16.green("\n\u2705 Approved \u2014 running command...\n"));
9291
+ console.error(chalk17.green("\n\u2705 Approved \u2014 running command...\n"));
8555
9292
  await runProxy(fullCommand);
8556
9293
  } else {
8557
9294
  program.help();
@@ -8566,9 +9303,9 @@ if (process.argv[2] !== "daemon") {
8566
9303
  const isCheckHook = process.argv[2] === "check";
8567
9304
  if (isCheckHook) {
8568
9305
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
8569
- const logPath = path24.join(os20.homedir(), ".node9", "hook-debug.log");
9306
+ const logPath = path26.join(os22.homedir(), ".node9", "hook-debug.log");
8570
9307
  const msg = reason instanceof Error ? reason.message : String(reason);
8571
- fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9308
+ fs24.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
8572
9309
  `);
8573
9310
  }
8574
9311
  process.exit(0);