@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.js CHANGED
@@ -114,8 +114,8 @@ function sanitizeConfig(raw) {
114
114
  }
115
115
  }
116
116
  const lines = result.error.issues.map((issue) => {
117
- const path25 = issue.path.length > 0 ? issue.path.join(".") : "root";
118
- return ` \u2022 ${path25}: ${issue.message}`;
117
+ const path27 = issue.path.length > 0 ? issue.path.join(".") : "root";
118
+ return ` \u2022 ${path27}: ${issue.message}`;
119
119
  });
120
120
  return {
121
121
  sanitized,
@@ -1623,17 +1623,44 @@ function readTrustedHosts() {
1623
1623
  return [];
1624
1624
  }
1625
1625
  }
1626
+ function getFileMtime() {
1627
+ try {
1628
+ return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
1629
+ } catch {
1630
+ return 0;
1631
+ }
1632
+ }
1633
+ function getCachedHosts() {
1634
+ const now = Date.now();
1635
+ if (_cache && now < _cache.expiry) {
1636
+ const mtime = getFileMtime();
1637
+ if (mtime === _cache.mtime) return _cache.hosts;
1638
+ }
1639
+ const hosts = readTrustedHosts();
1640
+ _cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
1641
+ return hosts;
1642
+ }
1626
1643
  function writeTrustedHosts(hosts) {
1627
1644
  const filePath = getTrustedHostsPath();
1628
1645
  import_fs6.default.mkdirSync(import_path7.default.dirname(filePath), { recursive: true });
1629
1646
  const tmp = filePath + ".node9-tmp";
1630
- import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2));
1647
+ import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
1631
1648
  import_fs6.default.renameSync(tmp, filePath);
1649
+ _cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
1632
1650
  }
1633
1651
  function addTrustedHost(host) {
1652
+ const normalized = normalizeHost(host);
1653
+ if (normalized.startsWith("*.")) {
1654
+ const base = normalized.slice(2);
1655
+ if (!base.includes(".")) {
1656
+ throw new Error(
1657
+ `Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
1658
+ );
1659
+ }
1660
+ }
1634
1661
  const hosts = readTrustedHosts();
1635
- if (hosts.some((h) => h.host === host)) return;
1636
- hosts.push({ host, addedAt: Date.now(), addedBy: "user" });
1662
+ if (hosts.some((h) => h.host === normalized)) return;
1663
+ hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
1637
1664
  writeTrustedHosts(hosts);
1638
1665
  }
1639
1666
  function removeTrustedHost(host) {
@@ -1648,22 +1675,24 @@ function normalizeHost(raw) {
1648
1675
  }
1649
1676
  function isTrustedHost(host) {
1650
1677
  const normalized = normalizeHost(host);
1651
- return readTrustedHosts().some((entry) => {
1678
+ return getCachedHosts().some((entry) => {
1652
1679
  const entryHost = entry.host.toLowerCase();
1653
1680
  if (entryHost.startsWith("*.")) {
1654
1681
  const domain = entryHost.slice(2);
1655
- return normalized === domain || normalized.endsWith("." + domain);
1682
+ return normalized.endsWith("." + domain);
1656
1683
  }
1657
1684
  return normalized === entryHost;
1658
1685
  });
1659
1686
  }
1660
- var import_fs6, import_path7, import_os5;
1687
+ var import_fs6, import_path7, import_os5, _cache, CACHE_TTL_MS;
1661
1688
  var init_trusted_hosts = __esm({
1662
1689
  "src/auth/trusted-hosts.ts"() {
1663
1690
  "use strict";
1664
1691
  import_fs6 = __toESM(require("fs"));
1665
1692
  import_path7 = __toESM(require("path"));
1666
1693
  import_os5 = __toESM(require("os"));
1694
+ _cache = null;
1695
+ CACHE_TTL_MS = 5e3;
1667
1696
  }
1668
1697
  });
1669
1698
 
@@ -1681,9 +1710,9 @@ function matchesPattern(text, patterns) {
1681
1710
  const withoutDotSlash = text.replace(/^\.\//, "");
1682
1711
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1683
1712
  }
1684
- function getNestedValue(obj, path25) {
1713
+ function getNestedValue(obj, path27) {
1685
1714
  if (!obj || typeof obj !== "object") return null;
1686
- return path25.split(".").reduce((prev, curr) => prev?.[curr], obj);
1715
+ return path27.split(".").reduce((prev, curr) => prev?.[curr], obj);
1687
1716
  }
1688
1717
  function shouldSnapshot(toolName, args, config) {
1689
1718
  if (!config.settings.enableUndo) return false;
@@ -1869,7 +1898,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1869
1898
  };
1870
1899
  }
1871
1900
  if (allTrusted) {
1872
- return { decision: "allow" };
1901
+ return {
1902
+ decision: "allow",
1903
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1904
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1905
+ tier: 3
1906
+ };
1873
1907
  }
1874
1908
  return {
1875
1909
  decision: "review",
@@ -2411,8 +2445,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2411
2445
  signal: ctrl.signal
2412
2446
  });
2413
2447
  if (!res.ok) throw new Error("Daemon fail");
2414
- const { id } = await res.json();
2415
- return id;
2448
+ const { id, allowCount } = await res.json();
2449
+ return { id, allowCount: allowCount ?? 1 };
2416
2450
  } finally {
2417
2451
  clearTimeout(timer);
2418
2452
  }
@@ -2451,15 +2485,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
2451
2485
  signal: AbortSignal.timeout(3e3)
2452
2486
  });
2453
2487
  if (!res.ok) throw new Error("Daemon unreachable");
2454
- const { id } = await res.json();
2455
- return id;
2488
+ const { id, allowCount } = await res.json();
2489
+ return { id, allowCount: allowCount ?? 1 };
2456
2490
  }
2457
- async function resolveViaDaemon(id, decision, internalToken) {
2491
+ async function resolveViaDaemon(id, decision, internalToken, source) {
2458
2492
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2459
2493
  await fetch(`${base}/resolve/${id}`, {
2460
2494
  method: "POST",
2461
2495
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
2462
- body: JSON.stringify({ decision }),
2496
+ body: JSON.stringify({ decision, ...source && { source } }),
2463
2497
  signal: AbortSignal.timeout(3e3)
2464
2498
  });
2465
2499
  }
@@ -2666,20 +2700,24 @@ ${smartTruncate(str, 500)}`
2666
2700
  function escapePango(text) {
2667
2701
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2668
2702
  }
2669
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2703
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2670
2704
  const lines = [];
2671
2705
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2672
2706
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2673
2707
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2674
2708
  lines.push("");
2675
2709
  lines.push(formattedArgs);
2710
+ if (allowCount >= 3) {
2711
+ lines.push("");
2712
+ lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
2713
+ }
2676
2714
  if (!locked) {
2677
2715
  lines.push("");
2678
2716
  lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
2679
2717
  }
2680
2718
  return lines.join("\n");
2681
2719
  }
2682
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2720
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2683
2721
  const lines = [];
2684
2722
  if (locked) {
2685
2723
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2691,6 +2729,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2691
2729
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2692
2730
  lines.push("");
2693
2731
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2732
+ if (allowCount >= 3) {
2733
+ lines.push("");
2734
+ lines.push(
2735
+ `<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
2736
+ );
2737
+ }
2694
2738
  if (!locked) {
2695
2739
  lines.push("");
2696
2740
  lines.push(
@@ -2699,12 +2743,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2699
2743
  }
2700
2744
  return lines.join("\n");
2701
2745
  }
2702
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
2746
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2703
2747
  if (isTestEnv()) return "deny";
2704
2748
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2705
2749
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
2706
2750
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
2707
- const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
2751
+ const message = buildPlainMessage(
2752
+ toolName,
2753
+ formattedArgs,
2754
+ agent,
2755
+ explainableLabel,
2756
+ locked,
2757
+ allowCount
2758
+ );
2708
2759
  return new Promise((resolve) => {
2709
2760
  let childProcess = null;
2710
2761
  const onAbort = () => {
@@ -2736,7 +2787,8 @@ end run`;
2736
2787
  formattedArgs,
2737
2788
  agent,
2738
2789
  explainableLabel,
2739
- locked
2790
+ locked,
2791
+ allowCount
2740
2792
  );
2741
2793
  const argsList = [
2742
2794
  locked ? "--info" : "--question",
@@ -3099,13 +3151,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3099
3151
  let viewerId = null;
3100
3152
  const internalToken = getInternalToken();
3101
3153
  let daemonEntryId = null;
3154
+ let daemonAllowCount = 1;
3102
3155
  if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
3103
3156
  if (cloudEnforced && cloudRequestId) {
3104
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3157
+ const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3158
+ viewerId = viewer?.id ?? null;
3105
3159
  daemonEntryId = viewerId;
3160
+ if (viewer) daemonAllowCount = viewer.allowCount;
3106
3161
  } else {
3107
3162
  try {
3108
- daemonEntryId = await registerDaemonEntry(
3163
+ const entry = await registerDaemonEntry(
3109
3164
  toolName,
3110
3165
  args,
3111
3166
  meta,
@@ -3113,6 +3168,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3113
3168
  options?.activityId,
3114
3169
  options?.cwd
3115
3170
  );
3171
+ daemonEntryId = entry.id;
3172
+ daemonAllowCount = entry.allowCount;
3116
3173
  } catch {
3117
3174
  }
3118
3175
  }
@@ -3148,7 +3205,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3148
3205
  false,
3149
3206
  signal,
3150
3207
  policyMatchedField,
3151
- policyMatchedWord
3208
+ policyMatchedWord,
3209
+ daemonAllowCount
3152
3210
  );
3153
3211
  if (decision === "always_allow") {
3154
3212
  writeTrustSession(toolName, 36e5);
@@ -3206,10 +3264,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3206
3264
  if (!resolved) {
3207
3265
  resolved = true;
3208
3266
  abortController.abort();
3209
- if (viewerId && internalToken) {
3210
- resolveViaDaemon(viewerId, res.approved ? "allow" : "deny", internalToken).catch(
3211
- () => null
3212
- );
3267
+ if (daemonEntryId && internalToken) {
3268
+ resolveViaDaemon(
3269
+ daemonEntryId,
3270
+ res.approved ? "allow" : "deny",
3271
+ internalToken,
3272
+ res.decisionSource
3273
+ ).catch(() => null);
3213
3274
  }
3214
3275
  resolve(res);
3215
3276
  }
@@ -3566,6 +3627,15 @@ var init_ui = __esm({
3566
3627
  padding: 5px 10px;
3567
3628
  margin-bottom: 14px;
3568
3629
  }
3630
+ .insight-hint {
3631
+ font-size: 12px;
3632
+ color: #f0c040;
3633
+ background: rgba(240, 192, 64, 0.08);
3634
+ border: 1px solid rgba(240, 192, 64, 0.25);
3635
+ border-radius: 6px;
3636
+ padding: 6px 10px;
3637
+ margin-bottom: 12px;
3638
+ }
3569
3639
  pre {
3570
3640
  background: #0d1117;
3571
3641
  padding: 14px 16px;
@@ -4038,6 +4108,78 @@ var init_ui = __esm({
4038
4108
  color: var(--danger);
4039
4109
  }
4040
4110
 
4111
+ /* \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 */
4112
+ .suggestion-card {
4113
+ background: rgba(82, 130, 255, 0.06);
4114
+ border: 1px solid rgba(82, 130, 255, 0.25);
4115
+ border-radius: 8px;
4116
+ padding: 10px 12px;
4117
+ margin-bottom: 8px;
4118
+ }
4119
+ .suggestion-card:last-child {
4120
+ margin-bottom: 0;
4121
+ }
4122
+ .suggestion-header {
4123
+ display: flex;
4124
+ align-items: center;
4125
+ gap: 8px;
4126
+ margin-bottom: 6px;
4127
+ }
4128
+ .suggestion-tool {
4129
+ font-family: 'Fira Code', monospace;
4130
+ font-size: 11px;
4131
+ color: var(--text-bright);
4132
+ flex: 1;
4133
+ word-break: break-all;
4134
+ }
4135
+ .suggestion-count {
4136
+ font-size: 10px;
4137
+ color: var(--muted);
4138
+ white-space: nowrap;
4139
+ }
4140
+ .suggestion-rule {
4141
+ font-family: 'Fira Code', monospace;
4142
+ font-size: 10px;
4143
+ color: #79c0ff;
4144
+ background: rgba(0, 0, 0, 0.25);
4145
+ border-radius: 4px;
4146
+ padding: 4px 8px;
4147
+ margin-bottom: 8px;
4148
+ word-break: break-all;
4149
+ white-space: pre-wrap;
4150
+ }
4151
+ .suggestion-actions {
4152
+ display: flex;
4153
+ gap: 6px;
4154
+ }
4155
+ .btn-apply {
4156
+ background: rgba(52, 125, 57, 0.2);
4157
+ border: 1px solid rgba(87, 171, 90, 0.4);
4158
+ color: #57ab5a;
4159
+ padding: 4px 10px;
4160
+ font-size: 11px;
4161
+ border-radius: 5px;
4162
+ font-family: inherit;
4163
+ cursor: pointer;
4164
+ }
4165
+ .btn-apply:hover {
4166
+ background: rgba(52, 125, 57, 0.35);
4167
+ }
4168
+ .btn-dismiss-suggestion {
4169
+ background: transparent;
4170
+ border: 1px solid var(--border);
4171
+ color: var(--muted);
4172
+ padding: 4px 10px;
4173
+ font-size: 11px;
4174
+ border-radius: 5px;
4175
+ font-family: inherit;
4176
+ cursor: pointer;
4177
+ }
4178
+ .btn-dismiss-suggestion:hover {
4179
+ border-color: var(--danger);
4180
+ color: var(--danger);
4181
+ }
4182
+
4041
4183
  .modal-overlay {
4042
4184
  display: none;
4043
4185
  position: fixed;
@@ -4219,6 +4361,11 @@ var init_ui = __esm({
4219
4361
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
4220
4362
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
4221
4363
  </div>
4364
+
4365
+ <div class="panel" id="suggestionsPanel" style="display: none">
4366
+ <div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
4367
+ <div id="suggestionsList"></div>
4368
+ </div>
4222
4369
  </div>
4223
4370
  </div>
4224
4371
  </div>
@@ -4408,6 +4555,7 @@ var init_ui = __esm({
4408
4555
  </div>
4409
4556
  <div class="tool-chip">\${esc(req.toolName)}</div>
4410
4557
  \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
4558
+ \${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
4411
4559
  \${renderPayload(req)}
4412
4560
  <div class="actions" id="act-\${req.id}">
4413
4561
  <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
@@ -4474,6 +4622,14 @@ var init_ui = __esm({
4474
4622
  ev.addEventListener('shields-status', (e) => {
4475
4623
  renderShields(JSON.parse(e.data).shields);
4476
4624
  });
4625
+ ev.addEventListener('suggestion:new', (e) => {
4626
+ const s = JSON.parse(e.data);
4627
+ addSuggestionCard(s);
4628
+ });
4629
+ ev.addEventListener('suggestion:resolved', (e) => {
4630
+ const { id } = JSON.parse(e.data);
4631
+ removeSuggestionCard(id);
4632
+ });
4477
4633
 
4478
4634
  // \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4479
4635
  ev.addEventListener('activity', (e) => {
@@ -4723,6 +4879,74 @@ var init_ui = __esm({
4723
4879
  .then((r) => r.json())
4724
4880
  .then(renderDecisions)
4725
4881
  .catch(() => {});
4882
+
4883
+ // \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
4884
+ function rulePreview(suggestion) {
4885
+ const r = suggestion.suggestedRule;
4886
+ if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
4887
+ const cond = r.rule.conditions?.[0];
4888
+ const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
4889
+ return \`allow \${r.rule.tool}\${condStr}\`;
4890
+ }
4891
+
4892
+ function addSuggestionCard(s) {
4893
+ const panel = document.getElementById('suggestionsPanel');
4894
+ const list = document.getElementById('suggestionsList');
4895
+ panel.style.display = '';
4896
+
4897
+ const card = document.createElement('div');
4898
+ card.className = 'suggestion-card';
4899
+ card.id = 'sg-' + s.id;
4900
+ card.innerHTML = \`
4901
+ <div class="suggestion-header">
4902
+ <span class="suggestion-tool">\${esc(s.toolName)}</span>
4903
+ <span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
4904
+ </div>
4905
+ <div class="suggestion-rule">\${esc(rulePreview(s))}</div>
4906
+ <div class="suggestion-actions">
4907
+ <button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
4908
+ <button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
4909
+ </div>
4910
+ \`;
4911
+ list.appendChild(card);
4912
+ }
4913
+
4914
+ function removeSuggestionCard(id) {
4915
+ document.getElementById('sg-' + id)?.remove();
4916
+ const list = document.getElementById('suggestionsList');
4917
+ if (!list.querySelector('.suggestion-card')) {
4918
+ document.getElementById('suggestionsPanel').style.display = 'none';
4919
+ }
4920
+ }
4921
+
4922
+ function applySuggestion(id) {
4923
+ fetch('/suggestions/' + id + '/apply', {
4924
+ method: 'POST',
4925
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
4926
+ body: JSON.stringify({}),
4927
+ })
4928
+ .then((r) => {
4929
+ if (r.ok) removeSuggestionCard(id);
4930
+ })
4931
+ .catch(() => {});
4932
+ }
4933
+
4934
+ function dismissSuggestion(id) {
4935
+ fetch('/suggestions/' + id + '/dismiss', {
4936
+ method: 'POST',
4937
+ headers: { 'X-Node9-Token': CSRF_TOKEN },
4938
+ })
4939
+ .then((r) => {
4940
+ if (r.ok) removeSuggestionCard(id);
4941
+ })
4942
+ .catch(() => {});
4943
+ }
4944
+
4945
+ // Load any suggestions that survived a page reload (daemon still running)
4946
+ fetch('/suggestions')
4947
+ .then((r) => r.json())
4948
+ .then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
4949
+ .catch(() => {});
4726
4950
  </script>
4727
4951
  </body>
4728
4952
  </html>
@@ -4740,7 +4964,117 @@ var init_ui2 = __esm({
4740
4964
  }
4741
4965
  });
4742
4966
 
4967
+ // src/daemon/suggestion-tracker.ts
4968
+ function extractPath(args) {
4969
+ if (!args || typeof args !== "object") return null;
4970
+ const a = args;
4971
+ for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
4972
+ if (typeof a[key] === "string" && a[key]) return a[key];
4973
+ }
4974
+ return null;
4975
+ }
4976
+ function commonPathPrefix(paths) {
4977
+ if (paths.length < 2) return null;
4978
+ const dirParts = paths.map((p) => {
4979
+ const lastSlash = p.lastIndexOf("/");
4980
+ return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
4981
+ });
4982
+ const first = dirParts[0].split("/");
4983
+ const common = [];
4984
+ for (let i = 0; i < first.length; i++) {
4985
+ if (dirParts.every((d) => d.split("/")[i] === first[i])) {
4986
+ common.push(first[i]);
4987
+ } else {
4988
+ break;
4989
+ }
4990
+ }
4991
+ const prefix = common.join("/").replace(/\/?$/, "/");
4992
+ return prefix.length > 1 ? prefix : null;
4993
+ }
4994
+ var import_crypto3, SuggestionTracker;
4995
+ var init_suggestion_tracker = __esm({
4996
+ "src/daemon/suggestion-tracker.ts"() {
4997
+ "use strict";
4998
+ import_crypto3 = require("crypto");
4999
+ SuggestionTracker = class {
5000
+ events = /* @__PURE__ */ new Map();
5001
+ threshold;
5002
+ constructor(threshold = 3) {
5003
+ this.threshold = threshold;
5004
+ }
5005
+ /**
5006
+ * Record a human-allowed review for a tool.
5007
+ * Returns a Suggestion when the threshold is reached, null otherwise.
5008
+ */
5009
+ recordAllow(toolName, args) {
5010
+ const events = this.events.get(toolName) ?? [];
5011
+ events.push({ args, ts: Date.now() });
5012
+ this.events.set(toolName, events);
5013
+ if (events.length >= this.threshold) {
5014
+ this.events.delete(toolName);
5015
+ return this.generateSuggestion(toolName, events);
5016
+ }
5017
+ return null;
5018
+ }
5019
+ /**
5020
+ * Reset the counter for a tool (e.g. when the user clicks Deny —
5021
+ * don't suggest allowing something they just blocked).
5022
+ */
5023
+ resetTool(toolName) {
5024
+ this.events.delete(toolName);
5025
+ }
5026
+ /** Current allow count for a tool (for tests). */
5027
+ getCount(toolName) {
5028
+ return this.events.get(toolName)?.length ?? 0;
5029
+ }
5030
+ generateSuggestion(toolName, events) {
5031
+ const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
5032
+ const prefix = commonPathPrefix(paths);
5033
+ const suggestedRule = prefix ? {
5034
+ type: "smartRule",
5035
+ rule: {
5036
+ name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
5037
+ tool: toolName,
5038
+ conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
5039
+ verdict: "allow",
5040
+ reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
5041
+ }
5042
+ } : { type: "ignoredTool", toolName };
5043
+ return {
5044
+ id: (0, import_crypto3.randomUUID)(),
5045
+ toolName,
5046
+ allowCount: events.length,
5047
+ suggestedRule,
5048
+ status: "pending",
5049
+ createdAt: Date.now(),
5050
+ exampleArgs: events.slice(0, 3).map((e) => e.args)
5051
+ };
5052
+ }
5053
+ };
5054
+ }
5055
+ });
5056
+
4743
5057
  // src/daemon/state.ts
5058
+ function loadInsightCounts() {
5059
+ try {
5060
+ if (!import_fs12.default.existsSync(INSIGHT_COUNTS_FILE)) return;
5061
+ const data = JSON.parse(import_fs12.default.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
5062
+ for (const [tool, count] of Object.entries(data)) {
5063
+ if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
5064
+ }
5065
+ } catch {
5066
+ }
5067
+ }
5068
+ function saveInsightCounts() {
5069
+ try {
5070
+ const data = {};
5071
+ insightCounts.forEach((count, tool) => {
5072
+ data[tool] = count;
5073
+ });
5074
+ atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
5075
+ } catch {
5076
+ }
5077
+ }
4744
5078
  function getAbandonTimer() {
4745
5079
  return _abandonTimer;
4746
5080
  }
@@ -4765,9 +5099,25 @@ function markRejectionHandlerRegistered() {
4765
5099
  function atomicWriteSync2(filePath, data, options) {
4766
5100
  const dir = import_path15.default.dirname(filePath);
4767
5101
  if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
4768
- const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
4769
- import_fs12.default.writeFileSync(tmpPath, data, options);
4770
- import_fs12.default.renameSync(tmpPath, filePath);
5102
+ const tmpPath = `${filePath}.${(0, import_crypto4.randomUUID)()}.tmp`;
5103
+ try {
5104
+ import_fs12.default.writeFileSync(tmpPath, data, options);
5105
+ } catch (err) {
5106
+ try {
5107
+ import_fs12.default.unlinkSync(tmpPath);
5108
+ } catch {
5109
+ }
5110
+ throw err;
5111
+ }
5112
+ try {
5113
+ import_fs12.default.renameSync(tmpPath, filePath);
5114
+ } catch (err) {
5115
+ try {
5116
+ import_fs12.default.unlinkSync(tmpPath);
5117
+ } catch {
5118
+ }
5119
+ throw err;
5120
+ }
4771
5121
  }
4772
5122
  function redactArgs(value) {
4773
5123
  if (!value || typeof value !== "object") return value;
@@ -4965,7 +5315,7 @@ function startActivitySocket() {
4965
5315
  }
4966
5316
  });
4967
5317
  }
4968
- var import_net2, import_fs12, import_path15, import_os12, import_child_process3, import_crypto3, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, pending, sseClients, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
5318
+ var import_net2, import_fs12, import_path15, import_os12, import_child_process3, import_crypto4, 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;
4969
5319
  var init_state2 = __esm({
4970
5320
  "src/daemon/state.ts"() {
4971
5321
  "use strict";
@@ -4974,8 +5324,9 @@ var init_state2 = __esm({
4974
5324
  import_path15 = __toESM(require("path"));
4975
5325
  import_os12 = __toESM(require("os"));
4976
5326
  import_child_process3 = require("child_process");
4977
- import_crypto3 = require("crypto");
5327
+ import_crypto4 = require("crypto");
4978
5328
  init_daemon();
5329
+ init_suggestion_tracker();
4979
5330
  homeDir = import_os12.default.homedir();
4980
5331
  DAEMON_PID_FILE = import_path15.default.join(homeDir, ".node9", "daemon.pid");
4981
5332
  DECISIONS_FILE = import_path15.default.join(homeDir, ".node9", "decisions.json");
@@ -4983,8 +5334,12 @@ var init_state2 = __esm({
4983
5334
  TRUST_FILE2 = import_path15.default.join(homeDir, ".node9", "trust.json");
4984
5335
  GLOBAL_CONFIG_FILE = import_path15.default.join(homeDir, ".node9", "config.json");
4985
5336
  CREDENTIALS_FILE = import_path15.default.join(homeDir, ".node9", "credentials.json");
5337
+ INSIGHT_COUNTS_FILE = import_path15.default.join(homeDir, ".node9", "insight-counts.json");
4986
5338
  pending = /* @__PURE__ */ new Map();
4987
5339
  sseClients = /* @__PURE__ */ new Set();
5340
+ suggestionTracker = new SuggestionTracker(3);
5341
+ suggestions = /* @__PURE__ */ new Map();
5342
+ insightCounts = /* @__PURE__ */ new Map();
4988
5343
  _abandonTimer = null;
4989
5344
  _hadBrowserClient = false;
4990
5345
  _daemonServer = null;
@@ -5003,10 +5358,68 @@ var init_state2 = __esm({
5003
5358
  }
5004
5359
  });
5005
5360
 
5361
+ // src/config/patch.ts
5362
+ function patchConfig(configPath, patch) {
5363
+ let config = {};
5364
+ try {
5365
+ if (import_fs13.default.existsSync(configPath)) {
5366
+ config = JSON.parse(import_fs13.default.readFileSync(configPath, "utf8"));
5367
+ }
5368
+ } catch {
5369
+ throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
5370
+ }
5371
+ if (!config.policy || typeof config.policy !== "object") config.policy = {};
5372
+ const policy = config.policy;
5373
+ if (patch.type === "smartRule") {
5374
+ if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
5375
+ const rules = policy.smartRules;
5376
+ if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
5377
+ rules.push(patch.rule);
5378
+ } else {
5379
+ if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
5380
+ const ignored = policy.ignoredTools;
5381
+ if (!ignored.includes(patch.toolName)) {
5382
+ ignored.push(patch.toolName);
5383
+ }
5384
+ }
5385
+ const dir = import_path16.default.dirname(configPath);
5386
+ import_fs13.default.mkdirSync(dir, { recursive: true });
5387
+ const tmp = configPath + ".node9-tmp";
5388
+ try {
5389
+ import_fs13.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5390
+ } catch (err) {
5391
+ try {
5392
+ import_fs13.default.unlinkSync(tmp);
5393
+ } catch {
5394
+ }
5395
+ throw err;
5396
+ }
5397
+ try {
5398
+ import_fs13.default.renameSync(tmp, configPath);
5399
+ } catch (err) {
5400
+ try {
5401
+ import_fs13.default.unlinkSync(tmp);
5402
+ } catch {
5403
+ }
5404
+ throw err;
5405
+ }
5406
+ }
5407
+ var import_fs13, import_path16, import_os13, GLOBAL_CONFIG_PATH;
5408
+ var init_patch = __esm({
5409
+ "src/config/patch.ts"() {
5410
+ "use strict";
5411
+ import_fs13 = __toESM(require("fs"));
5412
+ import_path16 = __toESM(require("path"));
5413
+ import_os13 = __toESM(require("os"));
5414
+ GLOBAL_CONFIG_PATH = import_path16.default.join(import_os13.default.homedir(), ".node9", "config.json");
5415
+ }
5416
+ });
5417
+
5006
5418
  // src/daemon/server.ts
5007
5419
  function startDaemon() {
5008
- const csrfToken = (0, import_crypto4.randomUUID)();
5009
- const internalToken = (0, import_crypto4.randomUUID)();
5420
+ loadInsightCounts();
5421
+ const csrfToken = (0, import_crypto5.randomUUID)();
5422
+ const internalToken = (0, import_crypto5.randomUUID)();
5010
5423
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
5011
5424
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
5012
5425
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
@@ -5019,7 +5432,7 @@ function startDaemon() {
5019
5432
  idleTimer = setTimeout(() => {
5020
5433
  if (autoStarted) {
5021
5434
  try {
5022
- import_fs13.default.unlinkSync(DAEMON_PID_FILE);
5435
+ import_fs14.default.unlinkSync(DAEMON_PID_FILE);
5023
5436
  } catch {
5024
5437
  }
5025
5438
  }
@@ -5028,8 +5441,14 @@ function startDaemon() {
5028
5441
  idleTimer.unref();
5029
5442
  }
5030
5443
  resetIdleTimer();
5444
+ const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
5031
5445
  const server = import_http.default.createServer(async (req, res) => {
5032
- const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
5446
+ const host = req.headers.host ?? "";
5447
+ if (!allowedHosts.has(host)) {
5448
+ res.writeHead(421, { "Content-Type": "text/plain" });
5449
+ return res.end("Misdirected Request");
5450
+ }
5451
+ const reqUrl = new URL(req.url || "/", `http://${host}`);
5033
5452
  const { pathname } = reqUrl;
5034
5453
  if (req.method === "GET" && pathname === "/") {
5035
5454
  res.writeHead(200, { "Content-Type": "text/html" });
@@ -5062,7 +5481,8 @@ data: ${JSON.stringify({
5062
5481
  slackDelegated: e.slackDelegated,
5063
5482
  timestamp: e.timestamp,
5064
5483
  agent: e.agent,
5065
- mcpServer: e.mcpServer
5484
+ mcpServer: e.mcpServer,
5485
+ allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
5066
5486
  })),
5067
5487
  orgName: getOrgName(),
5068
5488
  autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
@@ -5104,6 +5524,12 @@ data: ${JSON.stringify(item.data)}
5104
5524
  }
5105
5525
  });
5106
5526
  }
5527
+ if (req.method === "POST" && pathname === "/browser-opened") {
5528
+ if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
5529
+ browserOpened = true;
5530
+ res.writeHead(200).end();
5531
+ return;
5532
+ }
5107
5533
  if (req.method === "POST" && pathname === "/check") {
5108
5534
  try {
5109
5535
  resetIdleTimer();
@@ -5121,7 +5547,7 @@ data: ${JSON.stringify(item.data)}
5121
5547
  activityId,
5122
5548
  cwd
5123
5549
  } = JSON.parse(body);
5124
- const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto4.randomUUID)();
5550
+ const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto5.randomUUID)();
5125
5551
  const entry = {
5126
5552
  id,
5127
5553
  toolName,
@@ -5147,7 +5573,7 @@ data: ${JSON.stringify(item.data)}
5147
5573
  e.earlyReason = "No response \u2014 auto-denied after timeout";
5148
5574
  }
5149
5575
  pending.delete(id);
5150
- broadcast("remove", { id });
5576
+ broadcast("remove", { id, decision: "deny" });
5151
5577
  }
5152
5578
  }, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
5153
5579
  };
@@ -5161,7 +5587,7 @@ data: ${JSON.stringify(item.data)}
5161
5587
  status: "pending"
5162
5588
  });
5163
5589
  }
5164
- const projectCwd = typeof cwd === "string" && import_path16.default.isAbsolute(cwd) ? cwd : void 0;
5590
+ const projectCwd = typeof cwd === "string" && import_path17.default.isAbsolute(cwd) ? cwd : void 0;
5165
5591
  const projectConfig = getConfig(projectCwd);
5166
5592
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
5167
5593
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -5174,7 +5600,10 @@ data: ${JSON.stringify(item.data)}
5174
5600
  slackDelegated: entry.slackDelegated,
5175
5601
  agent: entry.agent,
5176
5602
  mcpServer: entry.mcpServer,
5177
- interactive: terminalEnabled
5603
+ interactive: terminalEnabled,
5604
+ // allowCount = what this count will be if the user allows.
5605
+ // Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
5606
+ allowCount: (insightCounts.get(toolName) ?? 0) + 1
5178
5607
  });
5179
5608
  const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
5180
5609
  if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
@@ -5183,7 +5612,7 @@ data: ${JSON.stringify(item.data)}
5183
5612
  }
5184
5613
  }
5185
5614
  res.writeHead(200, { "Content-Type": "application/json" });
5186
- res.end(JSON.stringify({ id }));
5615
+ res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
5187
5616
  if (slackDelegated) return;
5188
5617
  authorizeHeadless(
5189
5618
  toolName,
@@ -5210,7 +5639,7 @@ data: ${JSON.stringify(item.data)}
5210
5639
  if (e.waiter) {
5211
5640
  e.waiter(decision, result.reason);
5212
5641
  pending.delete(id);
5213
- broadcast("remove", { id });
5642
+ broadcast("remove", { id, decision });
5214
5643
  } else {
5215
5644
  e.earlyDecision = decision;
5216
5645
  e.earlyReason = result.reason;
@@ -5226,7 +5655,7 @@ data: ${JSON.stringify(item.data)}
5226
5655
  e.earlyReason = reason;
5227
5656
  }
5228
5657
  pending.delete(id);
5229
- broadcast("remove", { id });
5658
+ broadcast("remove", { id, decision: "deny" });
5230
5659
  });
5231
5660
  return;
5232
5661
  } catch {
@@ -5257,12 +5686,14 @@ data: ${JSON.stringify(item.data)}
5257
5686
  res.end(JSON.stringify(body));
5258
5687
  };
5259
5688
  req.on("close", () => {
5260
- const e = pending.get(id);
5261
- if (e && e.waiter && e.earlyDecision === null) {
5262
- clearTimeout(e.timer);
5263
- pending.delete(id);
5264
- broadcast("remove", { id });
5265
- }
5689
+ setTimeout(() => {
5690
+ const e = pending.get(id);
5691
+ if (e && e.waiter && e.earlyDecision === null) {
5692
+ clearTimeout(e.timer);
5693
+ pending.delete(id);
5694
+ broadcast("remove", { id });
5695
+ }
5696
+ }, 200);
5266
5697
  });
5267
5698
  return;
5268
5699
  }
@@ -5291,10 +5722,10 @@ data: ${JSON.stringify(item.data)}
5291
5722
  if (entry.waiter) {
5292
5723
  entry.waiter("allow");
5293
5724
  pending.delete(id);
5294
- broadcast("remove", { id });
5725
+ broadcast("remove", { id, decision: "allow" });
5295
5726
  } else {
5296
5727
  entry.earlyDecision = "allow";
5297
- broadcast("remove", { id });
5728
+ broadcast("remove", { id, decision: "allow" });
5298
5729
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5299
5730
  }
5300
5731
  res.writeHead(200);
@@ -5308,16 +5739,29 @@ data: ${JSON.stringify(item.data)}
5308
5739
  decision: resolvedDecision
5309
5740
  });
5310
5741
  clearTimeout(entry.timer);
5742
+ if (resolvedDecision === "allow" && !persist) {
5743
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
5744
+ saveInsightCounts();
5745
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
5746
+ if (suggestion) {
5747
+ suggestions.set(suggestion.id, suggestion);
5748
+ broadcast("suggestion:new", suggestion);
5749
+ }
5750
+ } else if (resolvedDecision === "deny") {
5751
+ insightCounts.delete(entry.toolName);
5752
+ saveInsightCounts();
5753
+ suggestionTracker.resetTool(entry.toolName);
5754
+ }
5311
5755
  const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
5312
5756
  if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
5313
5757
  if (entry.waiter) {
5314
5758
  entry.waiter(resolvedDecision, reason);
5315
5759
  pending.delete(id);
5316
- broadcast("remove", { id });
5760
+ broadcast("remove", { id, decision: resolvedDecision });
5317
5761
  } else {
5318
5762
  entry.earlyDecision = resolvedDecision;
5319
5763
  entry.earlyReason = reason;
5320
- broadcast("remove", { id });
5764
+ broadcast("remove", { id, decision: resolvedDecision });
5321
5765
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5322
5766
  }
5323
5767
  res.writeHead(200);
@@ -5405,13 +5849,38 @@ data: ${JSON.stringify(item.data)}
5405
5849
  const id = pathname.split("/").pop();
5406
5850
  const entry = pending.get(id);
5407
5851
  if (!entry) return res.writeHead(404).end();
5408
- const { decision } = JSON.parse(await readBody(req));
5409
- appendAuditLog({ toolName: entry.toolName, args: entry.args, decision });
5852
+ const { decision, source } = JSON.parse(await readBody(req));
5853
+ const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
5854
+ appendAuditLog({
5855
+ toolName: entry.toolName,
5856
+ args: entry.args,
5857
+ decision: resolvedResolveDecision
5858
+ });
5410
5859
  clearTimeout(entry.timer);
5411
- if (entry.waiter) entry.waiter(decision);
5412
- else entry.earlyDecision = decision;
5860
+ if (resolvedResolveDecision === "allow") {
5861
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
5862
+ saveInsightCounts();
5863
+ } else {
5864
+ insightCounts.delete(entry.toolName);
5865
+ saveInsightCounts();
5866
+ }
5867
+ if (!entry.slackDelegated) {
5868
+ if (resolvedResolveDecision === "allow") {
5869
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
5870
+ if (suggestion) {
5871
+ suggestions.set(suggestion.id, suggestion);
5872
+ broadcast("suggestion:new", suggestion);
5873
+ }
5874
+ } else {
5875
+ suggestionTracker.resetTool(entry.toolName);
5876
+ }
5877
+ }
5878
+ const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
5879
+ if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
5880
+ if (entry.waiter) entry.waiter(resolvedResolveDecision);
5881
+ else entry.earlyDecision = resolvedResolveDecision;
5413
5882
  pending.delete(id);
5414
- broadcast("remove", { id });
5883
+ broadcast("remove", { id, decision: resolvedResolveDecision });
5415
5884
  res.writeHead(200);
5416
5885
  return res.end(JSON.stringify({ ok: true }));
5417
5886
  } catch {
@@ -5459,20 +5928,79 @@ data: ${JSON.stringify(item.data)}
5459
5928
  res.writeHead(400).end();
5460
5929
  }
5461
5930
  }
5931
+ if (req.method === "GET" && pathname === "/suggestions") {
5932
+ res.writeHead(200, { "Content-Type": "application/json" });
5933
+ return res.end(JSON.stringify([...suggestions.values()]));
5934
+ }
5935
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
5936
+ if (!validToken(req)) return res.writeHead(403).end();
5937
+ try {
5938
+ const body = await readBody(req);
5939
+ const data = body ? JSON.parse(body) : {};
5940
+ const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
5941
+ const node9Dir = import_path17.default.dirname(GLOBAL_CONFIG_PATH);
5942
+ if (!import_path17.default.resolve(configPath).startsWith(node9Dir + import_path17.default.sep)) {
5943
+ res.writeHead(400, { "Content-Type": "application/json" });
5944
+ return res.end(
5945
+ JSON.stringify({ error: "configPath must be within the node9 config directory" })
5946
+ );
5947
+ }
5948
+ const id = pathname.split("/")[2];
5949
+ const suggestion = suggestions.get(id);
5950
+ if (!suggestion) return res.writeHead(404).end();
5951
+ let patch;
5952
+ if (data.rule !== void 0) {
5953
+ const parsed = SmartRuleSchema.safeParse(data.rule);
5954
+ if (!parsed.success) {
5955
+ res.writeHead(400, { "Content-Type": "application/json" });
5956
+ return res.end(JSON.stringify({ error: parsed.error.message }));
5957
+ }
5958
+ patch = { type: "smartRule", rule: parsed.data };
5959
+ } else {
5960
+ patch = suggestion.suggestedRule;
5961
+ }
5962
+ patchConfig(configPath, patch);
5963
+ _resetConfigCache();
5964
+ insightCounts.delete(suggestion.toolName);
5965
+ saveInsightCounts();
5966
+ suggestion.status = "applied";
5967
+ broadcast("suggestion:resolved", { id, status: "applied" });
5968
+ res.writeHead(200, { "Content-Type": "application/json" });
5969
+ return res.end(JSON.stringify({ ok: true }));
5970
+ } catch (err) {
5971
+ console.error(import_chalk2.default.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
5972
+ res.writeHead(500, { "Content-Type": "application/json" });
5973
+ return res.end(JSON.stringify({ error: String(err) }));
5974
+ }
5975
+ }
5976
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
5977
+ if (!validToken(req)) return res.writeHead(403).end();
5978
+ try {
5979
+ const id = pathname.split("/")[2];
5980
+ const suggestion = suggestions.get(id);
5981
+ if (!suggestion) return res.writeHead(404).end();
5982
+ suggestion.status = "dismissed";
5983
+ broadcast("suggestion:resolved", { id, status: "dismissed" });
5984
+ res.writeHead(200, { "Content-Type": "application/json" });
5985
+ return res.end(JSON.stringify({ ok: true }));
5986
+ } catch {
5987
+ res.writeHead(400).end();
5988
+ }
5989
+ }
5462
5990
  res.writeHead(404).end();
5463
5991
  });
5464
5992
  setDaemonServer(server);
5465
5993
  server.on("error", (e) => {
5466
5994
  if (e.code === "EADDRINUSE") {
5467
5995
  try {
5468
- if (import_fs13.default.existsSync(DAEMON_PID_FILE)) {
5469
- const { pid } = JSON.parse(import_fs13.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5996
+ if (import_fs14.default.existsSync(DAEMON_PID_FILE)) {
5997
+ const { pid } = JSON.parse(import_fs14.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5470
5998
  process.kill(pid, 0);
5471
5999
  return process.exit(0);
5472
6000
  }
5473
6001
  } catch {
5474
6002
  try {
5475
- import_fs13.default.unlinkSync(DAEMON_PID_FILE);
6003
+ import_fs14.default.unlinkSync(DAEMON_PID_FILE);
5476
6004
  } catch {
5477
6005
  }
5478
6006
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -5531,43 +6059,45 @@ data: ${JSON.stringify(item.data)}
5531
6059
  }
5532
6060
  startActivitySocket();
5533
6061
  }
5534
- var import_http, import_fs13, import_path16, import_crypto4, import_child_process4, import_chalk2;
6062
+ var import_http, import_fs14, import_path17, import_crypto5, import_child_process4, import_chalk2;
5535
6063
  var init_server = __esm({
5536
6064
  "src/daemon/server.ts"() {
5537
6065
  "use strict";
5538
6066
  import_http = __toESM(require("http"));
5539
- import_fs13 = __toESM(require("fs"));
5540
- import_path16 = __toESM(require("path"));
5541
- import_crypto4 = require("crypto");
6067
+ import_fs14 = __toESM(require("fs"));
6068
+ import_path17 = __toESM(require("path"));
6069
+ import_crypto5 = require("crypto");
5542
6070
  import_child_process4 = require("child_process");
5543
6071
  import_chalk2 = __toESM(require("chalk"));
5544
6072
  init_core();
5545
6073
  init_shields();
5546
6074
  init_ui2();
5547
6075
  init_state2();
6076
+ init_patch();
6077
+ init_config_schema();
5548
6078
  }
5549
6079
  });
5550
6080
 
5551
6081
  // src/daemon/index.ts
5552
6082
  function stopDaemon() {
5553
- if (!import_fs14.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
6083
+ if (!import_fs15.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
5554
6084
  try {
5555
- const { pid } = JSON.parse(import_fs14.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6085
+ const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5556
6086
  process.kill(pid, "SIGTERM");
5557
6087
  console.log(import_chalk3.default.green("\u2705 Stopped."));
5558
6088
  } catch {
5559
6089
  console.log(import_chalk3.default.gray("Cleaned up stale PID file."));
5560
6090
  } finally {
5561
6091
  try {
5562
- import_fs14.default.unlinkSync(DAEMON_PID_FILE);
6092
+ import_fs15.default.unlinkSync(DAEMON_PID_FILE);
5563
6093
  } catch {
5564
6094
  }
5565
6095
  }
5566
6096
  }
5567
6097
  function daemonStatus() {
5568
- if (import_fs14.default.existsSync(DAEMON_PID_FILE)) {
6098
+ if (import_fs15.default.existsSync(DAEMON_PID_FILE)) {
5569
6099
  try {
5570
- const { pid } = JSON.parse(import_fs14.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6100
+ const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5571
6101
  process.kill(pid, 0);
5572
6102
  console.log(import_chalk3.default.green("Node9 daemon: running"));
5573
6103
  return;
@@ -5586,11 +6116,11 @@ function daemonStatus() {
5586
6116
  console.log(import_chalk3.default.yellow("Node9 daemon: not running"));
5587
6117
  }
5588
6118
  }
5589
- var import_fs14, import_chalk3, import_child_process5;
6119
+ var import_fs15, import_chalk3, import_child_process5;
5590
6120
  var init_daemon2 = __esm({
5591
6121
  "src/daemon/index.ts"() {
5592
6122
  "use strict";
5593
- import_fs14 = __toESM(require("fs"));
6123
+ import_fs15 = __toESM(require("fs"));
5594
6124
  import_chalk3 = __toESM(require("chalk"));
5595
6125
  import_child_process5 = require("child_process");
5596
6126
  init_server();
@@ -5617,17 +6147,17 @@ function formatBase(activity) {
5617
6147
  const toolName = activity.tool.slice(0, 16).padEnd(16);
5618
6148
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
5619
6149
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
5620
- return `${import_chalk15.default.gray(time)} ${icon} ${import_chalk15.default.white.bold(toolName)} ${import_chalk15.default.dim(argsPreview)}`;
6150
+ return `${import_chalk16.default.gray(time)} ${icon} ${import_chalk16.default.white.bold(toolName)} ${import_chalk16.default.dim(argsPreview)}`;
5621
6151
  }
5622
6152
  function renderResult(activity, result) {
5623
6153
  const base = formatBase(activity);
5624
6154
  let status;
5625
6155
  if (result.status === "allow") {
5626
- status = import_chalk15.default.green("\u2713 ALLOW");
6156
+ status = import_chalk16.default.green("\u2713 ALLOW");
5627
6157
  } else if (result.status === "dlp") {
5628
- status = import_chalk15.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6158
+ status = import_chalk16.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
5629
6159
  } else {
5630
- status = import_chalk15.default.red("\u2717 BLOCK");
6160
+ status = import_chalk16.default.red("\u2717 BLOCK");
5631
6161
  }
5632
6162
  if (process.stdout.isTTY) {
5633
6163
  import_readline3.default.clearLine(process.stdout, 0);
@@ -5637,16 +6167,16 @@ function renderResult(activity, result) {
5637
6167
  }
5638
6168
  function renderPending(activity) {
5639
6169
  if (!process.stdout.isTTY) return;
5640
- process.stdout.write(`${formatBase(activity)} ${import_chalk15.default.yellow("\u25CF \u2026")}\r`);
6170
+ process.stdout.write(`${formatBase(activity)} ${import_chalk16.default.yellow("\u25CF \u2026")}\r`);
5641
6171
  }
5642
6172
  async function ensureDaemon() {
5643
6173
  let pidPort = null;
5644
- if (import_fs21.default.existsSync(PID_FILE)) {
6174
+ if (import_fs23.default.existsSync(PID_FILE)) {
5645
6175
  try {
5646
- const { port } = JSON.parse(import_fs21.default.readFileSync(PID_FILE, "utf-8"));
6176
+ const { port } = JSON.parse(import_fs23.default.readFileSync(PID_FILE, "utf-8"));
5647
6177
  pidPort = port;
5648
6178
  } catch {
5649
- console.error(import_chalk15.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6179
+ console.error(import_chalk16.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
5650
6180
  }
5651
6181
  }
5652
6182
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -5657,7 +6187,7 @@ async function ensureDaemon() {
5657
6187
  if (res.ok) return checkPort;
5658
6188
  } catch {
5659
6189
  }
5660
- console.log(import_chalk15.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6190
+ console.log(import_chalk16.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
5661
6191
  const child = (0, import_child_process13.spawn)(process.execPath, [process.argv[1], "daemon"], {
5662
6192
  detached: true,
5663
6193
  stdio: "ignore",
@@ -5674,12 +6204,15 @@ async function ensureDaemon() {
5674
6204
  } catch {
5675
6205
  }
5676
6206
  }
5677
- console.error(import_chalk15.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6207
+ console.error(import_chalk16.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
5678
6208
  process.exit(1);
5679
6209
  }
5680
- function postDecisionHttp(id, decision, csrfToken, port) {
6210
+ function postDecisionHttp(id, decision, csrfToken, port, opts) {
5681
6211
  return new Promise((resolve, reject) => {
5682
- const body = JSON.stringify({ decision, source: "terminal" });
6212
+ const bodyObj = { decision, source: "terminal" };
6213
+ if (opts?.persist) bodyObj.persist = true;
6214
+ if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
6215
+ const body = JSON.stringify(bodyObj);
5683
6216
  const req = import_http2.default.request(
5684
6217
  {
5685
6218
  hostname: "127.0.0.1",
@@ -5702,22 +6235,30 @@ function postDecisionHttp(id, decision, csrfToken, port) {
5702
6235
  req.end(body);
5703
6236
  });
5704
6237
  }
5705
- function buildCardLines(req) {
6238
+ function buildCardLines(req, localCount = 0) {
5706
6239
  const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
5707
6240
  const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
5708
6241
  const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
5709
6242
  const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
5710
- return [
6243
+ const lines = [
5711
6244
  ``,
5712
6245
  `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
5713
6246
  `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
5714
6247
  `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
5715
- `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`,
6248
+ `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`
6249
+ ];
6250
+ if (localCount >= 2) {
6251
+ lines.push(
6252
+ `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
6253
+ );
6254
+ }
6255
+ lines.push(
5716
6256
  `${CYAN}\u255A${RESET}`,
5717
6257
  ``,
5718
- ` ${BOLD}${GREEN}[A]${RESET} Allow ${BOLD}${RED}[D]${RESET} Deny`,
6258
+ ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
5719
6259
  ``
5720
- ];
6260
+ );
6261
+ return lines;
5721
6262
  }
5722
6263
  async function startTail(options = {}) {
5723
6264
  const port = await ensureDaemon();
@@ -5745,7 +6286,7 @@ async function startTail(options = {}) {
5745
6286
  req2.end();
5746
6287
  });
5747
6288
  if (result.ok) {
5748
- console.log(import_chalk15.default.green("\u2713 Flight Recorder buffer cleared."));
6289
+ console.log(import_chalk16.default.green("\u2713 Flight Recorder buffer cleared."));
5749
6290
  } else if (result.code === "ECONNREFUSED") {
5750
6291
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
5751
6292
  } else if (result.code === "ETIMEDOUT") {
@@ -5762,6 +6303,7 @@ async function startTail(options = {}) {
5762
6303
  let cardActive = false;
5763
6304
  let cardLineCount = 0;
5764
6305
  let cancelActiveCard = null;
6306
+ const localAllowCounts = /* @__PURE__ */ new Map();
5765
6307
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
5766
6308
  if (canApprove) import_readline3.default.emitKeypressEvents(process.stdin);
5767
6309
  function clearCard() {
@@ -5772,7 +6314,10 @@ async function startTail(options = {}) {
5772
6314
  }
5773
6315
  function printCard(req2) {
5774
6316
  process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
5775
- const lines = buildCardLines(req2);
6317
+ const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
6318
+ const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
6319
+ const priorCount = Math.max(daemonPrior, localPrior);
6320
+ const lines = buildCardLines(req2, priorCount);
5776
6321
  for (const line of lines) process.stdout.write(line + "\n");
5777
6322
  cardLineCount = lines.length;
5778
6323
  }
@@ -5800,34 +6345,70 @@ async function startTail(options = {}) {
5800
6345
  process.stdin.pause();
5801
6346
  cancelActiveCard = null;
5802
6347
  };
5803
- const settle = (decision) => {
6348
+ const settle = (action) => {
5804
6349
  if (settled) return;
5805
6350
  settled = true;
5806
6351
  cleanup();
5807
- clearCard();
6352
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6353
+ const stampedLines = buildCardLines(
6354
+ req2,
6355
+ Math.max(
6356
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6357
+ localAllowCounts.get(req2.toolName) ?? 0
6358
+ )
6359
+ );
6360
+ const decisionStamp = action === "always-allow" ? import_chalk16.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk16.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
6361
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
6362
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5808
6363
  process.stdout.write(SHOW_CURSOR);
5809
- postDecisionHttp(req2.id, decision, csrfToken, port).catch((err) => {
6364
+ cardLineCount = 0;
6365
+ if (action === "allow" || action === "always-allow" || action === "trust") {
6366
+ localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
6367
+ } else if (action === "deny") {
6368
+ localAllowCounts.delete(req2.toolName);
6369
+ }
6370
+ let httpDecision;
6371
+ let httpOpts;
6372
+ if (action === "always-allow") {
6373
+ httpDecision = "allow";
6374
+ httpOpts = { persist: true };
6375
+ } else if (action === "trust") {
6376
+ httpDecision = "trust";
6377
+ httpOpts = { trustDuration: "30m" };
6378
+ } else {
6379
+ httpDecision = action;
6380
+ }
6381
+ postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
5810
6382
  try {
5811
- import_fs21.default.appendFileSync(
5812
- import_path23.default.join(import_os19.default.homedir(), ".node9", "hook-debug.log"),
6383
+ import_fs23.default.appendFileSync(
6384
+ import_path25.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
5813
6385
  `[tail] POST /decision failed: ${String(err)}
5814
6386
  `
5815
6387
  );
5816
6388
  } catch {
5817
6389
  }
5818
6390
  });
5819
- const decisionLabel = decision === "allow" ? import_chalk15.default.green("\u2713 ALLOWED (terminal)") : import_chalk15.default.red("\u2717 DENIED (terminal)");
5820
- console.log(`${import_chalk15.default.cyan("\u25C6")} ${import_chalk15.default.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
5821
6391
  approvalQueue.shift();
5822
6392
  cardActive = false;
5823
6393
  showNextCard();
5824
6394
  };
5825
- cancelActiveCard = () => {
6395
+ cancelActiveCard = (externalDecision) => {
5826
6396
  if (settled) return;
5827
6397
  settled = true;
5828
6398
  cleanup();
5829
- clearCard();
6399
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6400
+ const priorCount = Math.max(
6401
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6402
+ localAllowCounts.get(req2.toolName) ?? 0
6403
+ );
6404
+ const stampedLines = buildCardLines(req2, priorCount);
6405
+ if (externalDecision) {
6406
+ const source = externalDecision === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
6407
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
6408
+ }
6409
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5830
6410
  process.stdout.write(SHOW_CURSOR);
6411
+ cardLineCount = 0;
5831
6412
  approvalQueue.shift();
5832
6413
  cardActive = false;
5833
6414
  showNextCard();
@@ -5835,10 +6416,14 @@ async function startTail(options = {}) {
5835
6416
  process.stdin.resume();
5836
6417
  onKeypress = (_str, key) => {
5837
6418
  const name = key?.name ?? "";
5838
- if (name === "a") {
6419
+ if (name === "y" || name === "return") {
5839
6420
  settle("allow");
5840
- } else if (name === "d" || name === "return" || name === "enter" || key?.ctrl && name === "c") {
6421
+ } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
5841
6422
  settle("deny");
6423
+ } else if (name === "a") {
6424
+ settle("always-allow");
6425
+ } else if (name === "t") {
6426
+ settle("trust");
5842
6427
  }
5843
6428
  };
5844
6429
  process.stdin.on("keypress", onKeypress);
@@ -5851,19 +6436,27 @@ async function startTail(options = {}) {
5851
6436
  else if (process.platform === "win32")
5852
6437
  (0, import_child_process13.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
5853
6438
  else (0, import_child_process13.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
6439
+ const intToken = getInternalToken();
6440
+ fetch(`http://127.0.0.1:${port}/browser-opened`, {
6441
+ method: "POST",
6442
+ headers: intToken ? { "X-Node9-Internal": intToken } : {}
6443
+ }).catch(() => {
6444
+ });
5854
6445
  }
5855
6446
  } catch {
5856
6447
  }
5857
- console.log(import_chalk15.default.cyan.bold(`
5858
- \u{1F6F0}\uFE0F Node9 tail `) + import_chalk15.default.dim(`\u2192 ${dashboardUrl}`));
6448
+ console.log(import_chalk16.default.cyan.bold(`
6449
+ \u{1F6F0}\uFE0F Node9 tail `) + import_chalk16.default.dim(`\u2192 ${dashboardUrl}`));
5859
6450
  if (canApprove) {
5860
- console.log(import_chalk15.default.dim("Interactive approvals enabled. [A] Allow [D] Deny"));
6451
+ console.log(
6452
+ import_chalk16.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
6453
+ );
5861
6454
  }
5862
6455
  if (options.history) {
5863
- console.log(import_chalk15.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
6456
+ console.log(import_chalk16.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
5864
6457
  } else {
5865
6458
  console.log(
5866
- import_chalk15.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
6459
+ import_chalk16.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
5867
6460
  );
5868
6461
  }
5869
6462
  process.on("SIGINT", () => {
@@ -5873,13 +6466,13 @@ async function startTail(options = {}) {
5873
6466
  import_readline3.default.clearLine(process.stdout, 0);
5874
6467
  import_readline3.default.cursorTo(process.stdout, 0);
5875
6468
  }
5876
- console.log(import_chalk15.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
6469
+ console.log(import_chalk16.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
5877
6470
  process.exit(0);
5878
6471
  });
5879
6472
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
5880
6473
  const req = import_http2.default.get(sseUrl, (res) => {
5881
6474
  if (res.statusCode !== 200) {
5882
- console.error(import_chalk15.default.red(`Failed to connect: HTTP ${res.statusCode}`));
6475
+ console.error(import_chalk16.default.red(`Failed to connect: HTTP ${res.statusCode}`));
5883
6476
  process.exit(1);
5884
6477
  }
5885
6478
  let currentEvent = "";
@@ -5909,7 +6502,7 @@ async function startTail(options = {}) {
5909
6502
  import_readline3.default.clearLine(process.stdout, 0);
5910
6503
  import_readline3.default.cursorTo(process.stdout, 0);
5911
6504
  }
5912
- console.log(import_chalk15.default.red("\n\u274C Daemon disconnected."));
6505
+ console.log(import_chalk16.default.red("\n\u274C Daemon disconnected."));
5913
6506
  process.exit(1);
5914
6507
  });
5915
6508
  });
@@ -5950,11 +6543,17 @@ async function startTail(options = {}) {
5950
6543
  }
5951
6544
  if (event === "remove") {
5952
6545
  try {
5953
- const { id } = JSON.parse(rawData);
6546
+ const { id, decision } = JSON.parse(rawData);
5954
6547
  const idx = approvalQueue.findIndex((r) => r.id === id);
5955
6548
  if (idx !== -1) {
5956
6549
  if (idx === 0 && cardActive && cancelActiveCard) {
5957
- cancelActiveCard();
6550
+ const toolName = approvalQueue[0].toolName;
6551
+ if (decision === "allow") {
6552
+ localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
6553
+ } else if (decision === "deny") {
6554
+ localAllowCounts.delete(toolName);
6555
+ }
6556
+ cancelActiveCard(decision);
5958
6557
  } else {
5959
6558
  approvalQueue.splice(idx, 1);
5960
6559
  }
@@ -5989,25 +6588,26 @@ async function startTail(options = {}) {
5989
6588
  }
5990
6589
  req.on("error", (err) => {
5991
6590
  const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
5992
- console.error(import_chalk15.default.red(`
6591
+ console.error(import_chalk16.default.red(`
5993
6592
  \u274C ${msg}`));
5994
6593
  process.exit(1);
5995
6594
  });
5996
6595
  }
5997
- var import_http2, import_chalk15, import_fs21, import_os19, import_path23, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
6596
+ var import_http2, import_chalk16, import_fs23, import_os21, import_path25, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
5998
6597
  var init_tail = __esm({
5999
6598
  "src/tui/tail.ts"() {
6000
6599
  "use strict";
6001
6600
  import_http2 = __toESM(require("http"));
6002
- import_chalk15 = __toESM(require("chalk"));
6003
- import_fs21 = __toESM(require("fs"));
6004
- import_os19 = __toESM(require("os"));
6005
- import_path23 = __toESM(require("path"));
6601
+ import_chalk16 = __toESM(require("chalk"));
6602
+ import_fs23 = __toESM(require("fs"));
6603
+ import_os21 = __toESM(require("os"));
6604
+ import_path25 = __toESM(require("path"));
6006
6605
  import_readline3 = __toESM(require("readline"));
6007
6606
  import_child_process13 = require("child_process");
6008
6607
  init_daemon2();
6608
+ init_daemon();
6009
6609
  init_core();
6010
- PID_FILE = import_path23.default.join(import_os19.default.homedir(), ".node9", "daemon.pid");
6610
+ PID_FILE = import_path25.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
6011
6611
  ICONS = {
6012
6612
  bash: "\u{1F4BB}",
6013
6613
  shell: "\u{1F4BB}",
@@ -6355,6 +6955,25 @@ async function setupGemini() {
6355
6955
  printDaemonTip();
6356
6956
  }
6357
6957
  }
6958
+ function detectAgents(homeDir2 = import_os11.default.homedir()) {
6959
+ const exists = (p) => {
6960
+ try {
6961
+ return import_fs11.default.existsSync(p);
6962
+ } catch (err) {
6963
+ const code = err.code;
6964
+ if (code !== "ENOENT") {
6965
+ process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
6966
+ `);
6967
+ }
6968
+ return false;
6969
+ }
6970
+ };
6971
+ return {
6972
+ claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
6973
+ gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
6974
+ cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
6975
+ };
6976
+ }
6358
6977
  async function setupCursor() {
6359
6978
  const homeDir2 = import_os11.default.homedir();
6360
6979
  const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
@@ -6413,10 +7032,10 @@ async function setupCursor() {
6413
7032
 
6414
7033
  // src/cli.ts
6415
7034
  init_daemon2();
6416
- var import_chalk16 = __toESM(require("chalk"));
6417
- var import_fs22 = __toESM(require("fs"));
6418
- var import_path24 = __toESM(require("path"));
6419
- var import_os20 = __toESM(require("os"));
7035
+ var import_chalk17 = __toESM(require("chalk"));
7036
+ var import_fs24 = __toESM(require("fs"));
7037
+ var import_path26 = __toESM(require("path"));
7038
+ var import_os22 = __toESM(require("os"));
6420
7039
  var import_prompts3 = require("@inquirer/prompts");
6421
7040
 
6422
7041
  // src/utils/duration.ts
@@ -6637,9 +7256,9 @@ async function autoStartDaemonAndWait() {
6637
7256
 
6638
7257
  // src/cli/commands/check.ts
6639
7258
  var import_chalk5 = __toESM(require("chalk"));
6640
- var import_fs16 = __toESM(require("fs"));
6641
- var import_path18 = __toESM(require("path"));
6642
- var import_os14 = __toESM(require("os"));
7259
+ var import_fs17 = __toESM(require("fs"));
7260
+ var import_path19 = __toESM(require("path"));
7261
+ var import_os15 = __toESM(require("os"));
6643
7262
  init_orchestrator();
6644
7263
  init_daemon();
6645
7264
  init_config();
@@ -6647,26 +7266,26 @@ init_policy();
6647
7266
 
6648
7267
  // src/undo.ts
6649
7268
  var import_child_process8 = require("child_process");
6650
- var import_crypto5 = __toESM(require("crypto"));
6651
- var import_fs15 = __toESM(require("fs"));
6652
- var import_path17 = __toESM(require("path"));
6653
- var import_os13 = __toESM(require("os"));
6654
- var SNAPSHOT_STACK_PATH = import_path17.default.join(import_os13.default.homedir(), ".node9", "snapshots.json");
6655
- var UNDO_LATEST_PATH = import_path17.default.join(import_os13.default.homedir(), ".node9", "undo_latest.txt");
7269
+ var import_crypto6 = __toESM(require("crypto"));
7270
+ var import_fs16 = __toESM(require("fs"));
7271
+ var import_path18 = __toESM(require("path"));
7272
+ var import_os14 = __toESM(require("os"));
7273
+ var SNAPSHOT_STACK_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots.json");
7274
+ var UNDO_LATEST_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "undo_latest.txt");
6656
7275
  var MAX_SNAPSHOTS = 10;
6657
7276
  var GIT_TIMEOUT = 15e3;
6658
7277
  function readStack() {
6659
7278
  try {
6660
- if (import_fs15.default.existsSync(SNAPSHOT_STACK_PATH))
6661
- return JSON.parse(import_fs15.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
7279
+ if (import_fs16.default.existsSync(SNAPSHOT_STACK_PATH))
7280
+ return JSON.parse(import_fs16.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
6662
7281
  } catch {
6663
7282
  }
6664
7283
  return [];
6665
7284
  }
6666
7285
  function writeStack(stack) {
6667
- const dir = import_path17.default.dirname(SNAPSHOT_STACK_PATH);
6668
- if (!import_fs15.default.existsSync(dir)) import_fs15.default.mkdirSync(dir, { recursive: true });
6669
- import_fs15.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7286
+ const dir = import_path18.default.dirname(SNAPSHOT_STACK_PATH);
7287
+ if (!import_fs16.default.existsSync(dir)) import_fs16.default.mkdirSync(dir, { recursive: true });
7288
+ import_fs16.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
6670
7289
  }
6671
7290
  function buildArgsSummary(tool, args) {
6672
7291
  if (!args || typeof args !== "object") return "";
@@ -6682,7 +7301,7 @@ function buildArgsSummary(tool, args) {
6682
7301
  function normalizeCwdForHash(cwd) {
6683
7302
  let normalized;
6684
7303
  try {
6685
- normalized = import_fs15.default.realpathSync(cwd);
7304
+ normalized = import_fs16.default.realpathSync(cwd);
6686
7305
  } catch {
6687
7306
  normalized = cwd;
6688
7307
  }
@@ -6691,17 +7310,17 @@ function normalizeCwdForHash(cwd) {
6691
7310
  return normalized;
6692
7311
  }
6693
7312
  function getShadowRepoDir(cwd) {
6694
- const hash = import_crypto5.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
6695
- return import_path17.default.join(import_os13.default.homedir(), ".node9", "snapshots", hash);
7313
+ const hash = import_crypto6.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
7314
+ return import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots", hash);
6696
7315
  }
6697
7316
  function cleanOrphanedIndexFiles(shadowDir) {
6698
7317
  try {
6699
7318
  const cutoff = Date.now() - 6e4;
6700
- for (const f of import_fs15.default.readdirSync(shadowDir)) {
7319
+ for (const f of import_fs16.default.readdirSync(shadowDir)) {
6701
7320
  if (f.startsWith("index_")) {
6702
- const fp = import_path17.default.join(shadowDir, f);
7321
+ const fp = import_path18.default.join(shadowDir, f);
6703
7322
  try {
6704
- if (import_fs15.default.statSync(fp).mtimeMs < cutoff) import_fs15.default.unlinkSync(fp);
7323
+ if (import_fs16.default.statSync(fp).mtimeMs < cutoff) import_fs16.default.unlinkSync(fp);
6705
7324
  } catch {
6706
7325
  }
6707
7326
  }
@@ -6713,7 +7332,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
6713
7332
  const hardcoded = [".git", ".node9"];
6714
7333
  const lines = [...hardcoded, ...ignorePaths].join("\n");
6715
7334
  try {
6716
- import_fs15.default.writeFileSync(import_path17.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
7335
+ import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
6717
7336
  } catch {
6718
7337
  }
6719
7338
  }
@@ -6726,25 +7345,25 @@ function ensureShadowRepo(shadowDir, cwd) {
6726
7345
  timeout: 3e3
6727
7346
  });
6728
7347
  if (check.status === 0) {
6729
- const ptPath = import_path17.default.join(shadowDir, "project-path.txt");
7348
+ const ptPath = import_path18.default.join(shadowDir, "project-path.txt");
6730
7349
  try {
6731
- const stored = import_fs15.default.readFileSync(ptPath, "utf8").trim();
7350
+ const stored = import_fs16.default.readFileSync(ptPath, "utf8").trim();
6732
7351
  if (stored === normalizedCwd) return true;
6733
7352
  if (process.env.NODE9_DEBUG === "1")
6734
7353
  console.error(
6735
7354
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
6736
7355
  );
6737
- import_fs15.default.rmSync(shadowDir, { recursive: true, force: true });
7356
+ import_fs16.default.rmSync(shadowDir, { recursive: true, force: true });
6738
7357
  } catch {
6739
7358
  try {
6740
- import_fs15.default.writeFileSync(ptPath, normalizedCwd, "utf8");
7359
+ import_fs16.default.writeFileSync(ptPath, normalizedCwd, "utf8");
6741
7360
  } catch {
6742
7361
  }
6743
7362
  return true;
6744
7363
  }
6745
7364
  }
6746
7365
  try {
6747
- import_fs15.default.mkdirSync(shadowDir, { recursive: true });
7366
+ import_fs16.default.mkdirSync(shadowDir, { recursive: true });
6748
7367
  } catch {
6749
7368
  }
6750
7369
  const init = (0, import_child_process8.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
@@ -6753,7 +7372,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6753
7372
  console.error("[Node9] git init --bare failed:", init.stderr?.toString());
6754
7373
  return false;
6755
7374
  }
6756
- const configFile = import_path17.default.join(shadowDir, "config");
7375
+ const configFile = import_path18.default.join(shadowDir, "config");
6757
7376
  (0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
6758
7377
  timeout: 3e3
6759
7378
  });
@@ -6761,7 +7380,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6761
7380
  timeout: 3e3
6762
7381
  });
6763
7382
  try {
6764
- import_fs15.default.writeFileSync(import_path17.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
7383
+ import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
6765
7384
  } catch {
6766
7385
  }
6767
7386
  return true;
@@ -6784,7 +7403,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6784
7403
  const shadowDir = getShadowRepoDir(cwd);
6785
7404
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
6786
7405
  writeShadowExcludes(shadowDir, ignorePaths);
6787
- indexFile = import_path17.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
7406
+ indexFile = import_path18.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
6788
7407
  const shadowEnv = {
6789
7408
  ...process.env,
6790
7409
  GIT_DIR: shadowDir,
@@ -6813,7 +7432,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6813
7432
  const shouldGc = stack.length % 5 === 0;
6814
7433
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
6815
7434
  writeStack(stack);
6816
- import_fs15.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
7435
+ import_fs16.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
6817
7436
  if (shouldGc) {
6818
7437
  (0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
6819
7438
  }
@@ -6824,7 +7443,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6824
7443
  } finally {
6825
7444
  if (indexFile) {
6826
7445
  try {
6827
- import_fs15.default.unlinkSync(indexFile);
7446
+ import_fs16.default.unlinkSync(indexFile);
6828
7447
  } catch {
6829
7448
  }
6830
7449
  }
@@ -6893,9 +7512,9 @@ function applyUndo(hash, cwd) {
6893
7512
  timeout: GIT_TIMEOUT
6894
7513
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
6895
7514
  for (const file of [...tracked, ...untracked]) {
6896
- const fullPath = import_path17.default.join(dir, file);
6897
- if (!snapshotFiles.has(file) && import_fs15.default.existsSync(fullPath)) {
6898
- import_fs15.default.unlinkSync(fullPath);
7515
+ const fullPath = import_path18.default.join(dir, file);
7516
+ if (!snapshotFiles.has(file) && import_fs16.default.existsSync(fullPath)) {
7517
+ import_fs16.default.unlinkSync(fullPath);
6899
7518
  }
6900
7519
  }
6901
7520
  return true;
@@ -6919,9 +7538,9 @@ function registerCheckCommand(program2) {
6919
7538
  } catch (err) {
6920
7539
  const tempConfig = getConfig();
6921
7540
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
6922
- const logPath = import_path18.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7541
+ const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
6923
7542
  const errMsg = err instanceof Error ? err.message : String(err);
6924
- import_fs16.default.appendFileSync(
7543
+ import_fs17.default.appendFileSync(
6925
7544
  logPath,
6926
7545
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
6927
7546
  RAW: ${raw}
@@ -6932,10 +7551,10 @@ RAW: ${raw}
6932
7551
  }
6933
7552
  const config = getConfig(payload.cwd || void 0);
6934
7553
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
6935
- const logPath = import_path18.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
6936
- if (!import_fs16.default.existsSync(import_path18.default.dirname(logPath)))
6937
- import_fs16.default.mkdirSync(import_path18.default.dirname(logPath), { recursive: true });
6938
- import_fs16.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
7554
+ const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7555
+ if (!import_fs17.default.existsSync(import_path19.default.dirname(logPath)))
7556
+ import_fs17.default.mkdirSync(import_path19.default.dirname(logPath), { recursive: true });
7557
+ import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6939
7558
  `);
6940
7559
  }
6941
7560
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -6948,8 +7567,8 @@ RAW: ${raw}
6948
7567
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
6949
7568
  let ttyFd = null;
6950
7569
  try {
6951
- ttyFd = import_fs16.default.openSync("/dev/tty", "w");
6952
- const writeTty = (line) => import_fs16.default.writeSync(ttyFd, line + "\n");
7570
+ ttyFd = import_fs17.default.openSync("/dev/tty", "w");
7571
+ const writeTty = (line) => import_fs17.default.writeSync(ttyFd, line + "\n");
6953
7572
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
6954
7573
  writeTty(import_chalk5.default.bgRed.white.bold(`
6955
7574
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -6965,7 +7584,7 @@ RAW: ${raw}
6965
7584
  } finally {
6966
7585
  if (ttyFd !== null)
6967
7586
  try {
6968
- import_fs16.default.closeSync(ttyFd);
7587
+ import_fs17.default.closeSync(ttyFd);
6969
7588
  } catch {
6970
7589
  }
6971
7590
  }
@@ -6996,7 +7615,7 @@ RAW: ${raw}
6996
7615
  if (shouldSnapshot(toolName, toolInput, config)) {
6997
7616
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6998
7617
  }
6999
- const safeCwdForAuth = typeof payload.cwd === "string" && import_path18.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7618
+ const safeCwdForAuth = typeof payload.cwd === "string" && import_path19.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7000
7619
  const result = await authorizeHeadless(toolName, toolInput, meta, {
7001
7620
  cwd: safeCwdForAuth
7002
7621
  });
@@ -7008,12 +7627,12 @@ RAW: ${raw}
7008
7627
  }
7009
7628
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
7010
7629
  try {
7011
- const tty = import_fs16.default.openSync("/dev/tty", "w");
7012
- import_fs16.default.writeSync(
7630
+ const tty = import_fs17.default.openSync("/dev/tty", "w");
7631
+ import_fs17.default.writeSync(
7013
7632
  tty,
7014
7633
  import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
7015
7634
  );
7016
- import_fs16.default.closeSync(tty);
7635
+ import_fs17.default.closeSync(tty);
7017
7636
  } catch {
7018
7637
  }
7019
7638
  const daemonReady = await autoStartDaemonAndWait();
@@ -7040,9 +7659,9 @@ RAW: ${raw}
7040
7659
  });
7041
7660
  } catch (err) {
7042
7661
  if (process.env.NODE9_DEBUG === "1") {
7043
- const logPath = import_path18.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7662
+ const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7044
7663
  const errMsg = err instanceof Error ? err.message : String(err);
7045
- import_fs16.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7664
+ import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7046
7665
  `);
7047
7666
  }
7048
7667
  process.exit(0);
@@ -7076,9 +7695,9 @@ RAW: ${raw}
7076
7695
  }
7077
7696
 
7078
7697
  // src/cli/commands/log.ts
7079
- var import_fs17 = __toESM(require("fs"));
7080
- var import_path19 = __toESM(require("path"));
7081
- var import_os15 = __toESM(require("os"));
7698
+ var import_fs18 = __toESM(require("fs"));
7699
+ var import_path20 = __toESM(require("path"));
7700
+ var import_os16 = __toESM(require("os"));
7082
7701
  init_audit();
7083
7702
  init_config();
7084
7703
  init_policy();
@@ -7100,11 +7719,11 @@ function registerLogCommand(program2) {
7100
7719
  decision: "allowed",
7101
7720
  source: "post-hook"
7102
7721
  };
7103
- const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "audit.log");
7104
- if (!import_fs17.default.existsSync(import_path19.default.dirname(logPath)))
7105
- import_fs17.default.mkdirSync(import_path19.default.dirname(logPath), { recursive: true });
7106
- import_fs17.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7107
- const safeCwd = typeof payload.cwd === "string" && import_path19.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7722
+ const logPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "audit.log");
7723
+ if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
7724
+ import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
7725
+ import_fs18.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7726
+ const safeCwd = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7108
7727
  const config = getConfig(safeCwd);
7109
7728
  if (shouldSnapshot(tool, {}, config)) {
7110
7729
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -7113,9 +7732,9 @@ function registerLogCommand(program2) {
7113
7732
  const msg = err instanceof Error ? err.message : String(err);
7114
7733
  process.stderr.write(`[Node9] audit log error: ${msg}
7115
7734
  `);
7116
- const debugPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7735
+ const debugPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
7117
7736
  try {
7118
- import_fs17.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7737
+ import_fs18.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7119
7738
  `);
7120
7739
  } catch {
7121
7740
  }
@@ -7419,14 +8038,14 @@ function registerConfigShowCommand(program2) {
7419
8038
 
7420
8039
  // src/cli/commands/doctor.ts
7421
8040
  var import_chalk7 = __toESM(require("chalk"));
7422
- var import_fs18 = __toESM(require("fs"));
7423
- var import_path20 = __toESM(require("path"));
7424
- var import_os16 = __toESM(require("os"));
8041
+ var import_fs19 = __toESM(require("fs"));
8042
+ var import_path21 = __toESM(require("path"));
8043
+ var import_os17 = __toESM(require("os"));
7425
8044
  var import_child_process9 = require("child_process");
7426
8045
  init_daemon();
7427
8046
  function registerDoctorCommand(program2, version2) {
7428
8047
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
7429
- const homeDir2 = import_os16.default.homedir();
8048
+ const homeDir2 = import_os17.default.homedir();
7430
8049
  let failures = 0;
7431
8050
  function pass(msg) {
7432
8051
  console.log(import_chalk7.default.green(" \u2705 ") + msg);
@@ -7475,10 +8094,10 @@ function registerDoctorCommand(program2, version2) {
7475
8094
  );
7476
8095
  }
7477
8096
  section("Configuration");
7478
- const globalConfigPath = import_path20.default.join(homeDir2, ".node9", "config.json");
7479
- if (import_fs18.default.existsSync(globalConfigPath)) {
8097
+ const globalConfigPath = import_path21.default.join(homeDir2, ".node9", "config.json");
8098
+ if (import_fs19.default.existsSync(globalConfigPath)) {
7480
8099
  try {
7481
- JSON.parse(import_fs18.default.readFileSync(globalConfigPath, "utf-8"));
8100
+ JSON.parse(import_fs19.default.readFileSync(globalConfigPath, "utf-8"));
7482
8101
  pass("~/.node9/config.json found and valid");
7483
8102
  } catch {
7484
8103
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -7486,10 +8105,10 @@ function registerDoctorCommand(program2, version2) {
7486
8105
  } else {
7487
8106
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
7488
8107
  }
7489
- const projectConfigPath = import_path20.default.join(process.cwd(), "node9.config.json");
7490
- if (import_fs18.default.existsSync(projectConfigPath)) {
8108
+ const projectConfigPath = import_path21.default.join(process.cwd(), "node9.config.json");
8109
+ if (import_fs19.default.existsSync(projectConfigPath)) {
7491
8110
  try {
7492
- JSON.parse(import_fs18.default.readFileSync(projectConfigPath, "utf-8"));
8111
+ JSON.parse(import_fs19.default.readFileSync(projectConfigPath, "utf-8"));
7493
8112
  pass("node9.config.json found and valid (project)");
7494
8113
  } catch {
7495
8114
  fail(
@@ -7498,8 +8117,8 @@ function registerDoctorCommand(program2, version2) {
7498
8117
  );
7499
8118
  }
7500
8119
  }
7501
- const credsPath = import_path20.default.join(homeDir2, ".node9", "credentials.json");
7502
- if (import_fs18.default.existsSync(credsPath)) {
8120
+ const credsPath = import_path21.default.join(homeDir2, ".node9", "credentials.json");
8121
+ if (import_fs19.default.existsSync(credsPath)) {
7503
8122
  pass("Cloud credentials found (~/.node9/credentials.json)");
7504
8123
  } else {
7505
8124
  warn(
@@ -7508,10 +8127,10 @@ function registerDoctorCommand(program2, version2) {
7508
8127
  );
7509
8128
  }
7510
8129
  section("Agent Hooks");
7511
- const claudeSettingsPath = import_path20.default.join(homeDir2, ".claude", "settings.json");
7512
- if (import_fs18.default.existsSync(claudeSettingsPath)) {
8130
+ const claudeSettingsPath = import_path21.default.join(homeDir2, ".claude", "settings.json");
8131
+ if (import_fs19.default.existsSync(claudeSettingsPath)) {
7513
8132
  try {
7514
- const cs = JSON.parse(import_fs18.default.readFileSync(claudeSettingsPath, "utf-8"));
8133
+ const cs = JSON.parse(import_fs19.default.readFileSync(claudeSettingsPath, "utf-8"));
7515
8134
  const hasHook = cs.hooks?.PreToolUse?.some(
7516
8135
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7517
8136
  );
@@ -7527,10 +8146,10 @@ function registerDoctorCommand(program2, version2) {
7527
8146
  } else {
7528
8147
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
7529
8148
  }
7530
- const geminiSettingsPath = import_path20.default.join(homeDir2, ".gemini", "settings.json");
7531
- if (import_fs18.default.existsSync(geminiSettingsPath)) {
8149
+ const geminiSettingsPath = import_path21.default.join(homeDir2, ".gemini", "settings.json");
8150
+ if (import_fs19.default.existsSync(geminiSettingsPath)) {
7532
8151
  try {
7533
- const gs = JSON.parse(import_fs18.default.readFileSync(geminiSettingsPath, "utf-8"));
8152
+ const gs = JSON.parse(import_fs19.default.readFileSync(geminiSettingsPath, "utf-8"));
7534
8153
  const hasHook = gs.hooks?.BeforeTool?.some(
7535
8154
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7536
8155
  );
@@ -7546,10 +8165,10 @@ function registerDoctorCommand(program2, version2) {
7546
8165
  } else {
7547
8166
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
7548
8167
  }
7549
- const cursorHooksPath = import_path20.default.join(homeDir2, ".cursor", "hooks.json");
7550
- if (import_fs18.default.existsSync(cursorHooksPath)) {
8168
+ const cursorHooksPath = import_path21.default.join(homeDir2, ".cursor", "hooks.json");
8169
+ if (import_fs19.default.existsSync(cursorHooksPath)) {
7551
8170
  try {
7552
- const cur = JSON.parse(import_fs18.default.readFileSync(cursorHooksPath, "utf-8"));
8171
+ const cur = JSON.parse(import_fs19.default.readFileSync(cursorHooksPath, "utf-8"));
7553
8172
  const hasHook = cur.hooks?.preToolUse?.some(
7554
8173
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
7555
8174
  );
@@ -7587,9 +8206,9 @@ function registerDoctorCommand(program2, version2) {
7587
8206
 
7588
8207
  // src/cli/commands/audit.ts
7589
8208
  var import_chalk8 = __toESM(require("chalk"));
7590
- var import_fs19 = __toESM(require("fs"));
7591
- var import_path21 = __toESM(require("path"));
7592
- var import_os17 = __toESM(require("os"));
8209
+ var import_fs20 = __toESM(require("fs"));
8210
+ var import_path22 = __toESM(require("path"));
8211
+ var import_os18 = __toESM(require("os"));
7593
8212
  function formatRelativeTime(timestamp) {
7594
8213
  const diff = Date.now() - new Date(timestamp).getTime();
7595
8214
  const sec = Math.floor(diff / 1e3);
@@ -7602,14 +8221,14 @@ function formatRelativeTime(timestamp) {
7602
8221
  }
7603
8222
  function registerAuditCommand(program2) {
7604
8223
  program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
7605
- const logPath = import_path21.default.join(import_os17.default.homedir(), ".node9", "audit.log");
7606
- if (!import_fs19.default.existsSync(logPath)) {
8224
+ const logPath = import_path22.default.join(import_os18.default.homedir(), ".node9", "audit.log");
8225
+ if (!import_fs20.default.existsSync(logPath)) {
7607
8226
  console.log(
7608
8227
  import_chalk8.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
7609
8228
  );
7610
8229
  return;
7611
8230
  }
7612
- const raw = import_fs19.default.readFileSync(logPath, "utf-8");
8231
+ const raw = import_fs20.default.readFileSync(logPath, "utf-8");
7613
8232
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
7614
8233
  let entries = lines.flatMap((line) => {
7615
8234
  try {
@@ -7727,11 +8346,44 @@ function registerDaemonCommand(program2) {
7727
8346
 
7728
8347
  // src/cli/commands/status.ts
7729
8348
  var import_chalk10 = __toESM(require("chalk"));
7730
- var import_fs20 = __toESM(require("fs"));
7731
- var import_path22 = __toESM(require("path"));
7732
- var import_os18 = __toESM(require("os"));
8349
+ var import_fs21 = __toESM(require("fs"));
8350
+ var import_path23 = __toESM(require("path"));
8351
+ var import_os19 = __toESM(require("os"));
7733
8352
  init_core();
7734
8353
  init_daemon();
8354
+ function readJson2(filePath) {
8355
+ try {
8356
+ if (import_fs21.default.existsSync(filePath)) return JSON.parse(import_fs21.default.readFileSync(filePath, "utf-8"));
8357
+ } catch {
8358
+ }
8359
+ return null;
8360
+ }
8361
+ function isNode9Hook2(cmd) {
8362
+ if (!cmd) return false;
8363
+ return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
8364
+ }
8365
+ function wrappedMcpServers(servers) {
8366
+ if (!servers) return [];
8367
+ 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(" ")}`);
8368
+ }
8369
+ function printAgentSection(label, hookPairs, wrapped) {
8370
+ console.log(import_chalk10.default.bold(` ${label}`));
8371
+ for (const { name, present } of hookPairs) {
8372
+ if (present) {
8373
+ console.log(import_chalk10.default.green(` \u2713 ${name}`));
8374
+ } else {
8375
+ console.log(import_chalk10.default.red(` \u2717 ${name}`) + import_chalk10.default.gray(" (not wired)"));
8376
+ }
8377
+ }
8378
+ if (wrapped.length > 0) {
8379
+ console.log(import_chalk10.default.cyan(` MCP proxied:`));
8380
+ for (const entry of wrapped) {
8381
+ console.log(import_chalk10.default.gray(` \u2022 ${entry}`));
8382
+ }
8383
+ } else {
8384
+ console.log(import_chalk10.default.gray(` MCP proxied: none`));
8385
+ }
8386
+ }
7735
8387
  function registerStatusCommand(program2) {
7736
8388
  program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
7737
8389
  const creds = getCredentials();
@@ -7766,19 +8418,72 @@ function registerStatusCommand(program2) {
7766
8418
  console.log("");
7767
8419
  const modeLabel = settings.mode === "audit" ? import_chalk10.default.blue("audit") : settings.mode === "strict" ? import_chalk10.default.red("strict") : import_chalk10.default.white("standard");
7768
8420
  console.log(` Mode: ${modeLabel}`);
7769
- const projectConfig = import_path22.default.join(process.cwd(), "node9.config.json");
7770
- const globalConfig = import_path22.default.join(import_os18.default.homedir(), ".node9", "config.json");
8421
+ const projectConfig = import_path23.default.join(process.cwd(), "node9.config.json");
8422
+ const globalConfig = import_path23.default.join(import_os19.default.homedir(), ".node9", "config.json");
7771
8423
  console.log(
7772
- ` Local: ${import_fs20.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
8424
+ ` Local: ${import_fs21.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
7773
8425
  );
7774
8426
  console.log(
7775
- ` Global: ${import_fs20.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
8427
+ ` Global: ${import_fs21.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
7776
8428
  );
7777
8429
  if (mergedConfig.policy.sandboxPaths.length > 0) {
7778
8430
  console.log(
7779
8431
  ` Sandbox: ${import_chalk10.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
7780
8432
  );
7781
8433
  }
8434
+ const homeDir2 = import_os19.default.homedir();
8435
+ const claudeSettings = readJson2(
8436
+ import_path23.default.join(homeDir2, ".claude", "settings.json")
8437
+ );
8438
+ const claudeConfig = readJson2(import_path23.default.join(homeDir2, ".claude.json"));
8439
+ const geminiSettings = readJson2(
8440
+ import_path23.default.join(homeDir2, ".gemini", "settings.json")
8441
+ );
8442
+ const cursorConfig = readJson2(import_path23.default.join(homeDir2, ".cursor", "mcp.json"));
8443
+ const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
8444
+ if (agentFound) {
8445
+ console.log("");
8446
+ console.log(import_chalk10.default.bold(" Agent Wiring:"));
8447
+ console.log("");
8448
+ if (claudeSettings || claudeConfig) {
8449
+ const preHook = claudeSettings?.hooks?.PreToolUse?.some(
8450
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8451
+ ) ?? false;
8452
+ const postHook = claudeSettings?.hooks?.PostToolUse?.some(
8453
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8454
+ ) ?? false;
8455
+ printAgentSection(
8456
+ "Claude Code",
8457
+ [
8458
+ { name: "PreToolUse (node9 check)", present: preHook },
8459
+ { name: "PostToolUse (node9 log)", present: postHook }
8460
+ ],
8461
+ wrappedMcpServers(claudeConfig?.mcpServers)
8462
+ );
8463
+ console.log("");
8464
+ }
8465
+ if (geminiSettings) {
8466
+ const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
8467
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8468
+ ) ?? false;
8469
+ const afterHook = geminiSettings.hooks?.AfterTool?.some(
8470
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8471
+ ) ?? false;
8472
+ printAgentSection(
8473
+ "Gemini CLI",
8474
+ [
8475
+ { name: "BeforeTool (node9 check)", present: beforeHook },
8476
+ { name: "AfterTool (node9 log)", present: afterHook }
8477
+ ],
8478
+ wrappedMcpServers(geminiSettings.mcpServers)
8479
+ );
8480
+ console.log("");
8481
+ }
8482
+ if (cursorConfig) {
8483
+ printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
8484
+ console.log("");
8485
+ }
8486
+ }
7782
8487
  const pauseState = checkPause();
7783
8488
  if (pauseState.paused) {
7784
8489
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
@@ -7791,8 +8496,63 @@ function registerStatusCommand(program2) {
7791
8496
  });
7792
8497
  }
7793
8498
 
7794
- // src/cli/commands/undo.ts
8499
+ // src/cli/commands/init.ts
7795
8500
  var import_chalk11 = __toESM(require("chalk"));
8501
+ var import_fs22 = __toESM(require("fs"));
8502
+ var import_path24 = __toESM(require("path"));
8503
+ var import_os20 = __toESM(require("os"));
8504
+ init_core();
8505
+ function registerInitCommand(program2) {
8506
+ 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) => {
8507
+ console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
8508
+ const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
8509
+ if (import_fs22.default.existsSync(configPath) && !options.force) {
8510
+ console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8511
+ } else {
8512
+ const requestedMode = options.mode.toLowerCase();
8513
+ const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8514
+ const configToSave = {
8515
+ ...DEFAULT_CONFIG,
8516
+ settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
8517
+ };
8518
+ const dir = import_path24.default.dirname(configPath);
8519
+ if (!import_fs22.default.existsSync(dir)) import_fs22.default.mkdirSync(dir, { recursive: true });
8520
+ import_fs22.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8521
+ console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
8522
+ console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
8523
+ }
8524
+ if (options.skipSetup) return;
8525
+ console.log("");
8526
+ const detected = detectAgents();
8527
+ const found = Object.keys(detected).filter(
8528
+ (k) => detected[k]
8529
+ );
8530
+ if (found.length === 0) {
8531
+ console.log(
8532
+ import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
8533
+ );
8534
+ console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
8535
+ return;
8536
+ }
8537
+ console.log(import_chalk11.default.bold("Detected agents:"));
8538
+ for (const agent of found) {
8539
+ console.log(import_chalk11.default.green(` \u2713 ${agent}`));
8540
+ }
8541
+ console.log("");
8542
+ for (const agent of found) {
8543
+ console.log(import_chalk11.default.bold(`Wiring ${agent}...`));
8544
+ if (agent === "claude") await setupClaude();
8545
+ else if (agent === "gemini") await setupGemini();
8546
+ else if (agent === "cursor") await setupCursor();
8547
+ console.log("");
8548
+ }
8549
+ console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
8550
+ console.log(import_chalk11.default.gray(" Run: node9 daemon start"));
8551
+ });
8552
+ }
8553
+
8554
+ // src/cli/commands/undo.ts
8555
+ var import_chalk12 = __toESM(require("chalk"));
7796
8556
  var import_prompts2 = require("@inquirer/prompts");
7797
8557
  function registerUndoCommand(program2) {
7798
8558
  program2.command("undo").description(
@@ -7804,22 +8564,22 @@ function registerUndoCommand(program2) {
7804
8564
  if (history.length === 0) {
7805
8565
  if (!options.all && allHistory.length > 0) {
7806
8566
  console.log(
7807
- import_chalk11.default.yellow(
8567
+ import_chalk12.default.yellow(
7808
8568
  `
7809
8569
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
7810
- Run ${import_chalk11.default.cyan("node9 undo --all")} to see snapshots from all projects.
8570
+ Run ${import_chalk12.default.cyan("node9 undo --all")} to see snapshots from all projects.
7811
8571
  `
7812
8572
  )
7813
8573
  );
7814
8574
  } else {
7815
- console.log(import_chalk11.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
8575
+ console.log(import_chalk12.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
7816
8576
  }
7817
8577
  return;
7818
8578
  }
7819
8579
  const idx = history.length - steps;
7820
8580
  if (idx < 0) {
7821
8581
  console.log(
7822
- import_chalk11.default.yellow(
8582
+ import_chalk12.default.yellow(
7823
8583
  `
7824
8584
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
7825
8585
  `
@@ -7831,19 +8591,19 @@ function registerUndoCommand(program2) {
7831
8591
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
7832
8592
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
7833
8593
  console.log(
7834
- import_chalk11.default.magenta.bold(`
8594
+ import_chalk12.default.magenta.bold(`
7835
8595
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
7836
8596
  );
7837
8597
  console.log(
7838
- import_chalk11.default.white(
7839
- ` Tool: ${import_chalk11.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk11.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
8598
+ import_chalk12.default.white(
8599
+ ` Tool: ${import_chalk12.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk12.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
7840
8600
  )
7841
8601
  );
7842
- console.log(import_chalk11.default.white(` When: ${import_chalk11.default.gray(ageStr)}`));
7843
- console.log(import_chalk11.default.white(` Dir: ${import_chalk11.default.gray(snapshot.cwd)}`));
8602
+ console.log(import_chalk12.default.white(` When: ${import_chalk12.default.gray(ageStr)}`));
8603
+ console.log(import_chalk12.default.white(` Dir: ${import_chalk12.default.gray(snapshot.cwd)}`));
7844
8604
  if (steps > 1)
7845
8605
  console.log(
7846
- import_chalk11.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
8606
+ import_chalk12.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
7847
8607
  );
7848
8608
  console.log("");
7849
8609
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -7851,21 +8611,21 @@ function registerUndoCommand(program2) {
7851
8611
  const lines = diff.split("\n");
7852
8612
  for (const line of lines) {
7853
8613
  if (line.startsWith("+++") || line.startsWith("---")) {
7854
- console.log(import_chalk11.default.bold(line));
8614
+ console.log(import_chalk12.default.bold(line));
7855
8615
  } else if (line.startsWith("+")) {
7856
- console.log(import_chalk11.default.green(line));
8616
+ console.log(import_chalk12.default.green(line));
7857
8617
  } else if (line.startsWith("-")) {
7858
- console.log(import_chalk11.default.red(line));
8618
+ console.log(import_chalk12.default.red(line));
7859
8619
  } else if (line.startsWith("@@")) {
7860
- console.log(import_chalk11.default.cyan(line));
8620
+ console.log(import_chalk12.default.cyan(line));
7861
8621
  } else {
7862
- console.log(import_chalk11.default.gray(line));
8622
+ console.log(import_chalk12.default.gray(line));
7863
8623
  }
7864
8624
  }
7865
8625
  console.log("");
7866
8626
  } else {
7867
8627
  console.log(
7868
- import_chalk11.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
8628
+ import_chalk12.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
7869
8629
  );
7870
8630
  }
7871
8631
  const proceed = await (0, import_prompts2.confirm)({
@@ -7874,18 +8634,18 @@ function registerUndoCommand(program2) {
7874
8634
  });
7875
8635
  if (proceed) {
7876
8636
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
7877
- console.log(import_chalk11.default.green("\n\u2705 Reverted successfully.\n"));
8637
+ console.log(import_chalk12.default.green("\n\u2705 Reverted successfully.\n"));
7878
8638
  } else {
7879
- console.error(import_chalk11.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
8639
+ console.error(import_chalk12.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
7880
8640
  }
7881
8641
  } else {
7882
- console.log(import_chalk11.default.gray("\nCancelled.\n"));
8642
+ console.log(import_chalk12.default.gray("\nCancelled.\n"));
7883
8643
  }
7884
8644
  });
7885
8645
  }
7886
8646
 
7887
8647
  // src/cli/commands/watch.ts
7888
- var import_chalk12 = __toESM(require("chalk"));
8648
+ var import_chalk13 = __toESM(require("chalk"));
7889
8649
  var import_child_process11 = require("child_process");
7890
8650
  init_daemon();
7891
8651
  function registerWatchCommand(program2) {
@@ -7902,7 +8662,7 @@ function registerWatchCommand(program2) {
7902
8662
  throw new Error("not running");
7903
8663
  }
7904
8664
  } catch {
7905
- console.error(import_chalk12.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
8665
+ console.error(import_chalk13.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
7906
8666
  const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
7907
8667
  detached: true,
7908
8668
  stdio: "ignore",
@@ -7924,12 +8684,12 @@ function registerWatchCommand(program2) {
7924
8684
  }
7925
8685
  }
7926
8686
  if (!ready) {
7927
- console.error(import_chalk12.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
8687
+ console.error(import_chalk13.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7928
8688
  process.exit(1);
7929
8689
  }
7930
8690
  }
7931
8691
  console.error(
7932
- import_chalk12.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk12.default.dim(` \u2192 localhost:${port}`) + import_chalk12.default.dim(
8692
+ import_chalk13.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk13.default.dim(` \u2192 localhost:${port}`) + import_chalk13.default.dim(
7933
8693
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
7934
8694
  )
7935
8695
  );
@@ -7938,7 +8698,7 @@ function registerWatchCommand(program2) {
7938
8698
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
7939
8699
  });
7940
8700
  if (result.error) {
7941
- console.error(import_chalk12.default.red(`\u274C Failed to run command: ${result.error.message}`));
8701
+ console.error(import_chalk13.default.red(`\u274C Failed to run command: ${result.error.message}`));
7942
8702
  process.exit(1);
7943
8703
  }
7944
8704
  process.exit(result.status ?? 0);
@@ -7947,7 +8707,7 @@ function registerWatchCommand(program2) {
7947
8707
 
7948
8708
  // src/mcp-gateway/index.ts
7949
8709
  var import_readline2 = __toESM(require("readline"));
7950
- var import_chalk13 = __toESM(require("chalk"));
8710
+ var import_chalk14 = __toESM(require("chalk"));
7951
8711
  var import_child_process12 = require("child_process");
7952
8712
  var import_execa3 = require("execa");
7953
8713
  init_orchestrator();
@@ -8011,13 +8771,13 @@ async function runMcpGateway(upstreamCommand) {
8011
8771
  const prov = checkProvenance(executable);
8012
8772
  if (prov.trustLevel === "suspect") {
8013
8773
  console.error(
8014
- import_chalk13.default.red(
8774
+ import_chalk14.default.red(
8015
8775
  `\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
8016
8776
  )
8017
8777
  );
8018
- console.error(import_chalk13.default.red(" Verify this binary is trusted before proceeding."));
8778
+ console.error(import_chalk14.default.red(" Verify this binary is trusted before proceeding."));
8019
8779
  }
8020
- console.error(import_chalk13.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
8780
+ console.error(import_chalk14.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
8021
8781
  const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
8022
8782
  "NODE_OPTIONS",
8023
8783
  "NODE_PATH",
@@ -8081,10 +8841,10 @@ async function runMcpGateway(upstreamCommand) {
8081
8841
  mcpServer
8082
8842
  });
8083
8843
  if (!result.approved) {
8084
- console.error(import_chalk13.default.red(`
8844
+ console.error(import_chalk14.default.red(`
8085
8845
  \u{1F6D1} Node9 MCP Gateway: Action Blocked`));
8086
- console.error(import_chalk13.default.gray(` Tool: ${toolName}`));
8087
- console.error(import_chalk13.default.gray(` Reason: ${result.reason ?? "Security Policy"}
8846
+ console.error(import_chalk14.default.gray(` Tool: ${toolName}`));
8847
+ console.error(import_chalk14.default.gray(` Reason: ${result.reason ?? "Security Policy"}
8088
8848
  `));
8089
8849
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
8090
8850
  const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -8157,7 +8917,7 @@ function registerMcpGatewayCommand(program2) {
8157
8917
  }
8158
8918
 
8159
8919
  // src/cli/commands/trust.ts
8160
- var import_chalk14 = __toESM(require("chalk"));
8920
+ var import_chalk15 = __toESM(require("chalk"));
8161
8921
  init_trusted_hosts();
8162
8922
  function isValidHost(host) {
8163
8923
  return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
@@ -8168,44 +8928,44 @@ function registerTrustCommand(program2) {
8168
8928
  const normalized = normalizeHost(host.trim());
8169
8929
  if (!isValidHost(normalized)) {
8170
8930
  console.error(
8171
- import_chalk14.default.red(`
8931
+ import_chalk15.default.red(`
8172
8932
  \u274C Invalid host: "${host}"
8173
- `) + import_chalk14.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
8933
+ `) + import_chalk15.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
8174
8934
  );
8175
8935
  process.exit(1);
8176
8936
  }
8177
8937
  addTrustedHost(normalized);
8178
- console.log(import_chalk14.default.green(`
8938
+ console.log(import_chalk15.default.green(`
8179
8939
  \u2705 ${normalized} added to trusted hosts.`));
8180
8940
  console.log(
8181
- import_chalk14.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
8941
+ import_chalk15.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
8182
8942
  );
8183
8943
  });
8184
8944
  trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
8185
8945
  const normalized = normalizeHost(host.trim());
8186
8946
  const removed = removeTrustedHost(normalized);
8187
8947
  if (!removed) {
8188
- console.error(import_chalk14.default.yellow(`
8948
+ console.error(import_chalk15.default.yellow(`
8189
8949
  \u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
8190
8950
  `));
8191
8951
  process.exit(1);
8192
8952
  }
8193
- console.log(import_chalk14.default.green(`
8953
+ console.log(import_chalk15.default.green(`
8194
8954
  \u2705 ${normalized} removed from trusted hosts.
8195
8955
  `));
8196
8956
  });
8197
8957
  trustCmd.command("list").description("Show all trusted hosts").action(() => {
8198
8958
  const hosts = readTrustedHosts();
8199
8959
  if (hosts.length === 0) {
8200
- console.log(import_chalk14.default.gray("\n No trusted hosts configured.\n"));
8201
- console.log(` Add one: ${import_chalk14.default.cyan("node9 trust add api.mycompany.com")}
8960
+ console.log(import_chalk15.default.gray("\n No trusted hosts configured.\n"));
8961
+ console.log(` Add one: ${import_chalk15.default.cyan("node9 trust add api.mycompany.com")}
8202
8962
  `);
8203
8963
  return;
8204
8964
  }
8205
- console.log(import_chalk14.default.bold("\n\u{1F513} Trusted Hosts\n"));
8965
+ console.log(import_chalk15.default.bold("\n\u{1F513} Trusted Hosts\n"));
8206
8966
  for (const entry of hosts) {
8207
8967
  const date = new Date(entry.addedAt).toLocaleDateString();
8208
- console.log(` ${import_chalk14.default.cyan(entry.host.padEnd(40))} ${import_chalk14.default.gray(`added ${date}`)}`);
8968
+ console.log(` ${import_chalk15.default.cyan(entry.host.padEnd(40))} ${import_chalk15.default.gray(`added ${date}`)}`);
8209
8969
  }
8210
8970
  console.log("");
8211
8971
  });
@@ -8213,20 +8973,20 @@ function registerTrustCommand(program2) {
8213
8973
 
8214
8974
  // src/cli.ts
8215
8975
  var { version } = JSON.parse(
8216
- import_fs22.default.readFileSync(import_path24.default.join(__dirname, "../package.json"), "utf-8")
8976
+ import_fs24.default.readFileSync(import_path26.default.join(__dirname, "../package.json"), "utf-8")
8217
8977
  );
8218
8978
  var program = new import_commander.Command();
8219
8979
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
8220
8980
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
8221
8981
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
8222
- const credPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "credentials.json");
8223
- if (!import_fs22.default.existsSync(import_path24.default.dirname(credPath)))
8224
- import_fs22.default.mkdirSync(import_path24.default.dirname(credPath), { recursive: true });
8982
+ const credPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
8983
+ if (!import_fs24.default.existsSync(import_path26.default.dirname(credPath)))
8984
+ import_fs24.default.mkdirSync(import_path26.default.dirname(credPath), { recursive: true });
8225
8985
  const profileName = options.profile || "default";
8226
8986
  let existingCreds = {};
8227
8987
  try {
8228
- if (import_fs22.default.existsSync(credPath)) {
8229
- const raw = JSON.parse(import_fs22.default.readFileSync(credPath, "utf-8"));
8988
+ if (import_fs24.default.existsSync(credPath)) {
8989
+ const raw = JSON.parse(import_fs24.default.readFileSync(credPath, "utf-8"));
8230
8990
  if (raw.apiKey) {
8231
8991
  existingCreds = {
8232
8992
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -8238,13 +8998,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8238
8998
  } catch {
8239
8999
  }
8240
9000
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
8241
- import_fs22.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
9001
+ import_fs24.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
8242
9002
  if (profileName === "default") {
8243
- const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
9003
+ const configPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "config.json");
8244
9004
  let config = {};
8245
9005
  try {
8246
- if (import_fs22.default.existsSync(configPath))
8247
- config = JSON.parse(import_fs22.default.readFileSync(configPath, "utf-8"));
9006
+ if (import_fs24.default.existsSync(configPath))
9007
+ config = JSON.parse(import_fs24.default.readFileSync(configPath, "utf-8"));
8248
9008
  } catch {
8249
9009
  }
8250
9010
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -8259,36 +9019,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8259
9019
  approvers.cloud = false;
8260
9020
  }
8261
9021
  s.approvers = approvers;
8262
- if (!import_fs22.default.existsSync(import_path24.default.dirname(configPath)))
8263
- import_fs22.default.mkdirSync(import_path24.default.dirname(configPath), { recursive: true });
8264
- import_fs22.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
9022
+ if (!import_fs24.default.existsSync(import_path26.default.dirname(configPath)))
9023
+ import_fs24.default.mkdirSync(import_path26.default.dirname(configPath), { recursive: true });
9024
+ import_fs24.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
8265
9025
  }
8266
9026
  if (options.profile && profileName !== "default") {
8267
- console.log(import_chalk16.default.green(`\u2705 Profile "${profileName}" saved`));
8268
- console.log(import_chalk16.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
9027
+ console.log(import_chalk17.default.green(`\u2705 Profile "${profileName}" saved`));
9028
+ console.log(import_chalk17.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
8269
9029
  } else if (options.local) {
8270
- console.log(import_chalk16.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
8271
- console.log(import_chalk16.default.gray(` All decisions stay on this machine.`));
9030
+ console.log(import_chalk17.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
9031
+ console.log(import_chalk17.default.gray(` All decisions stay on this machine.`));
8272
9032
  } else {
8273
- console.log(import_chalk16.default.green(`\u2705 Logged in \u2014 agent mode`));
8274
- console.log(import_chalk16.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
9033
+ console.log(import_chalk17.default.green(`\u2705 Logged in \u2014 agent mode`));
9034
+ console.log(import_chalk17.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
8275
9035
  }
8276
9036
  });
8277
9037
  program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
8278
9038
  if (target === "gemini") return await setupGemini();
8279
9039
  if (target === "claude") return await setupClaude();
8280
9040
  if (target === "cursor") return await setupCursor();
8281
- console.error(import_chalk16.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9041
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8282
9042
  process.exit(1);
8283
9043
  });
8284
9044
  program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
8285
9045
  if (!target) {
8286
- console.log(import_chalk16.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
8287
- console.log(" Usage: " + import_chalk16.default.white("node9 setup <target>") + "\n");
9046
+ console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
9047
+ console.log(" Usage: " + import_chalk17.default.white("node9 setup <target>") + "\n");
8288
9048
  console.log(" Targets:");
8289
- console.log(" " + import_chalk16.default.green("claude") + " \u2014 Claude Code (hook mode)");
8290
- console.log(" " + import_chalk16.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
8291
- console.log(" " + import_chalk16.default.green("cursor") + " \u2014 Cursor (hook mode)");
9049
+ console.log(" " + import_chalk17.default.green("claude") + " \u2014 Claude Code (hook mode)");
9050
+ console.log(" " + import_chalk17.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
9051
+ console.log(" " + import_chalk17.default.green("cursor") + " \u2014 Cursor (hook mode)");
8292
9052
  console.log("");
8293
9053
  return;
8294
9054
  }
@@ -8296,7 +9056,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
8296
9056
  if (t === "gemini") return await setupGemini();
8297
9057
  if (t === "claude") return await setupClaude();
8298
9058
  if (t === "cursor") return await setupCursor();
8299
- console.error(import_chalk16.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9059
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8300
9060
  process.exit(1);
8301
9061
  });
8302
9062
  program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
@@ -8305,30 +9065,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
8305
9065
  else if (target === "gemini") fn = teardownGemini;
8306
9066
  else if (target === "cursor") fn = teardownCursor;
8307
9067
  else {
8308
- console.error(import_chalk16.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9068
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8309
9069
  process.exit(1);
8310
9070
  }
8311
- console.log(import_chalk16.default.cyan(`
9071
+ console.log(import_chalk17.default.cyan(`
8312
9072
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
8313
9073
  `));
8314
9074
  try {
8315
9075
  fn();
8316
9076
  } catch (err) {
8317
- console.error(import_chalk16.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
9077
+ console.error(import_chalk17.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
8318
9078
  process.exit(1);
8319
9079
  }
8320
- console.log(import_chalk16.default.gray("\n Restart the agent for changes to take effect."));
9080
+ console.log(import_chalk17.default.gray("\n Restart the agent for changes to take effect."));
8321
9081
  });
8322
9082
  program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
8323
- console.log(import_chalk16.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
8324
- console.log(import_chalk16.default.bold("Stopping daemon..."));
9083
+ console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
9084
+ console.log(import_chalk17.default.bold("Stopping daemon..."));
8325
9085
  try {
8326
9086
  stopDaemon();
8327
- console.log(import_chalk16.default.green(" \u2705 Daemon stopped"));
9087
+ console.log(import_chalk17.default.green(" \u2705 Daemon stopped"));
8328
9088
  } catch {
8329
- console.log(import_chalk16.default.blue(" \u2139\uFE0F Daemon was not running"));
9089
+ console.log(import_chalk17.default.blue(" \u2139\uFE0F Daemon was not running"));
8330
9090
  }
8331
- console.log(import_chalk16.default.bold("\nRemoving hooks..."));
9091
+ console.log(import_chalk17.default.bold("\nRemoving hooks..."));
8332
9092
  let teardownFailed = false;
8333
9093
  for (const [label, fn] of [
8334
9094
  ["Claude", teardownClaude],
@@ -8340,45 +9100,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
8340
9100
  } catch (err) {
8341
9101
  teardownFailed = true;
8342
9102
  console.error(
8343
- import_chalk16.default.red(
9103
+ import_chalk17.default.red(
8344
9104
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
8345
9105
  )
8346
9106
  );
8347
9107
  }
8348
9108
  }
8349
9109
  if (options.purge) {
8350
- const node9Dir = import_path24.default.join(import_os20.default.homedir(), ".node9");
8351
- if (import_fs22.default.existsSync(node9Dir)) {
9110
+ const node9Dir = import_path26.default.join(import_os22.default.homedir(), ".node9");
9111
+ if (import_fs24.default.existsSync(node9Dir)) {
8352
9112
  const confirmed = await (0, import_prompts3.confirm)({
8353
9113
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
8354
9114
  default: false
8355
9115
  });
8356
9116
  if (confirmed) {
8357
- import_fs22.default.rmSync(node9Dir, { recursive: true });
8358
- if (import_fs22.default.existsSync(node9Dir)) {
9117
+ import_fs24.default.rmSync(node9Dir, { recursive: true });
9118
+ if (import_fs24.default.existsSync(node9Dir)) {
8359
9119
  console.error(
8360
- import_chalk16.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9120
+ import_chalk17.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
8361
9121
  );
8362
9122
  } else {
8363
- console.log(import_chalk16.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
9123
+ console.log(import_chalk17.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
8364
9124
  }
8365
9125
  } else {
8366
- console.log(import_chalk16.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
9126
+ console.log(import_chalk17.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
8367
9127
  }
8368
9128
  } else {
8369
- console.log(import_chalk16.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
9129
+ console.log(import_chalk17.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
8370
9130
  }
8371
9131
  } else {
8372
9132
  console.log(
8373
- import_chalk16.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
9133
+ import_chalk17.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
8374
9134
  );
8375
9135
  }
8376
9136
  if (teardownFailed) {
8377
- console.error(import_chalk16.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
9137
+ console.error(import_chalk17.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
8378
9138
  process.exit(1);
8379
9139
  }
8380
- console.log(import_chalk16.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
8381
- console.log(import_chalk16.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
9140
+ console.log(import_chalk17.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
9141
+ console.log(import_chalk17.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
8382
9142
  });
8383
9143
  registerDoctorCommand(program, version);
8384
9144
  program.command("explain").description(
@@ -8391,7 +9151,7 @@ program.command("explain").description(
8391
9151
  try {
8392
9152
  args = JSON.parse(trimmed);
8393
9153
  } catch {
8394
- console.error(import_chalk16.default.red(`
9154
+ console.error(import_chalk17.default.red(`
8395
9155
  \u274C Invalid JSON: ${trimmed}
8396
9156
  `));
8397
9157
  process.exit(1);
@@ -8402,83 +9162,59 @@ program.command("explain").description(
8402
9162
  }
8403
9163
  const result = await explainPolicy(tool, args);
8404
9164
  console.log("");
8405
- console.log(import_chalk16.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
9165
+ console.log(import_chalk17.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
8406
9166
  console.log("");
8407
- console.log(` ${import_chalk16.default.bold("Tool:")} ${import_chalk16.default.white(result.tool)}`);
9167
+ console.log(` ${import_chalk17.default.bold("Tool:")} ${import_chalk17.default.white(result.tool)}`);
8408
9168
  if (argsRaw) {
8409
9169
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
8410
- console.log(` ${import_chalk16.default.bold("Input:")} ${import_chalk16.default.gray(preview)}`);
9170
+ console.log(` ${import_chalk17.default.bold("Input:")} ${import_chalk17.default.gray(preview)}`);
8411
9171
  }
8412
9172
  console.log("");
8413
- console.log(import_chalk16.default.bold("Config Sources (Waterfall):"));
9173
+ console.log(import_chalk17.default.bold("Config Sources (Waterfall):"));
8414
9174
  for (const tier of result.waterfall) {
8415
- const num = import_chalk16.default.gray(` ${tier.tier}.`);
9175
+ const num = import_chalk17.default.gray(` ${tier.tier}.`);
8416
9176
  const label = tier.label.padEnd(16);
8417
9177
  let statusStr;
8418
9178
  if (tier.tier === 1) {
8419
- statusStr = import_chalk16.default.gray(tier.note ?? "");
9179
+ statusStr = import_chalk17.default.gray(tier.note ?? "");
8420
9180
  } else if (tier.status === "active") {
8421
- const loc = tier.path ? import_chalk16.default.gray(tier.path) : "";
8422
- const note = tier.note ? import_chalk16.default.gray(`(${tier.note})`) : "";
8423
- statusStr = import_chalk16.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
9181
+ const loc = tier.path ? import_chalk17.default.gray(tier.path) : "";
9182
+ const note = tier.note ? import_chalk17.default.gray(`(${tier.note})`) : "";
9183
+ statusStr = import_chalk17.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
8424
9184
  } else {
8425
- statusStr = import_chalk16.default.gray("\u25CB " + (tier.note ?? "not found"));
9185
+ statusStr = import_chalk17.default.gray("\u25CB " + (tier.note ?? "not found"));
8426
9186
  }
8427
- console.log(`${num} ${import_chalk16.default.white(label)} ${statusStr}`);
9187
+ console.log(`${num} ${import_chalk17.default.white(label)} ${statusStr}`);
8428
9188
  }
8429
9189
  console.log("");
8430
- console.log(import_chalk16.default.bold("Policy Evaluation:"));
9190
+ console.log(import_chalk17.default.bold("Policy Evaluation:"));
8431
9191
  for (const step of result.steps) {
8432
9192
  const isFinal = step.isFinal;
8433
9193
  let icon;
8434
- if (step.outcome === "allow") icon = import_chalk16.default.green(" \u2705");
8435
- else if (step.outcome === "review") icon = import_chalk16.default.red(" \u{1F534}");
8436
- else if (step.outcome === "skip") icon = import_chalk16.default.gray(" \u2500 ");
8437
- else icon = import_chalk16.default.gray(" \u25CB ");
9194
+ if (step.outcome === "allow") icon = import_chalk17.default.green(" \u2705");
9195
+ else if (step.outcome === "review") icon = import_chalk17.default.red(" \u{1F534}");
9196
+ else if (step.outcome === "skip") icon = import_chalk17.default.gray(" \u2500 ");
9197
+ else icon = import_chalk17.default.gray(" \u25CB ");
8438
9198
  const name = step.name.padEnd(18);
8439
- const nameStr = isFinal ? import_chalk16.default.white.bold(name) : import_chalk16.default.white(name);
8440
- const detail = isFinal ? import_chalk16.default.white(step.detail) : import_chalk16.default.gray(step.detail);
8441
- const arrow = isFinal ? import_chalk16.default.yellow(" \u2190 STOP") : "";
9199
+ const nameStr = isFinal ? import_chalk17.default.white.bold(name) : import_chalk17.default.white(name);
9200
+ const detail = isFinal ? import_chalk17.default.white(step.detail) : import_chalk17.default.gray(step.detail);
9201
+ const arrow = isFinal ? import_chalk17.default.yellow(" \u2190 STOP") : "";
8442
9202
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
8443
9203
  }
8444
9204
  console.log("");
8445
9205
  if (result.decision === "allow") {
8446
- console.log(import_chalk16.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk16.default.gray(" \u2014 no approval needed"));
9206
+ console.log(import_chalk17.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk17.default.gray(" \u2014 no approval needed"));
8447
9207
  } else {
8448
9208
  console.log(
8449
- import_chalk16.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk16.default.gray(" \u2014 human approval required")
9209
+ import_chalk17.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk17.default.gray(" \u2014 human approval required")
8450
9210
  );
8451
9211
  if (result.blockedByLabel) {
8452
- console.log(import_chalk16.default.gray(` Reason: ${result.blockedByLabel}`));
9212
+ console.log(import_chalk17.default.gray(` Reason: ${result.blockedByLabel}`));
8453
9213
  }
8454
9214
  }
8455
9215
  console.log("");
8456
9216
  });
8457
- program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
8458
- const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
8459
- if (import_fs22.default.existsSync(configPath) && !options.force) {
8460
- console.log(import_chalk16.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
8461
- console.log(import_chalk16.default.gray(` Run with --force to overwrite.`));
8462
- return;
8463
- }
8464
- const requestedMode = options.mode.toLowerCase();
8465
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8466
- const configToSave = {
8467
- ...DEFAULT_CONFIG,
8468
- settings: {
8469
- ...DEFAULT_CONFIG.settings,
8470
- mode: safeMode
8471
- }
8472
- };
8473
- const dir = import_path24.default.dirname(configPath);
8474
- if (!import_fs22.default.existsSync(dir)) import_fs22.default.mkdirSync(dir, { recursive: true });
8475
- import_fs22.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8476
- console.log(import_chalk16.default.green(`\u2705 Global config created: ${configPath}`));
8477
- console.log(import_chalk16.default.cyan(` Mode set to: ${safeMode}`));
8478
- console.log(
8479
- import_chalk16.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
8480
- );
8481
- });
9217
+ registerInitCommand(program);
8482
9218
  registerAuditCommand(program);
8483
9219
  registerStatusCommand(program);
8484
9220
  registerDaemonCommand(program);
@@ -8487,7 +9223,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
8487
9223
  try {
8488
9224
  await startTail2(options);
8489
9225
  } catch (err) {
8490
- console.error(import_chalk16.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
9226
+ console.error(import_chalk17.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
8491
9227
  process.exit(1);
8492
9228
  }
8493
9229
  });
@@ -8499,7 +9235,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8499
9235
  const ms = parseDuration(options.duration);
8500
9236
  if (ms === null) {
8501
9237
  console.error(
8502
- import_chalk16.default.red(`
9238
+ import_chalk17.default.red(`
8503
9239
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
8504
9240
  `)
8505
9241
  );
@@ -8507,20 +9243,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8507
9243
  }
8508
9244
  pauseNode9(ms, options.duration);
8509
9245
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
8510
- console.log(import_chalk16.default.yellow(`
9246
+ console.log(import_chalk17.default.yellow(`
8511
9247
  \u23F8 Node9 paused until ${expiresAt}`));
8512
- console.log(import_chalk16.default.gray(` All tool calls will be allowed without review.`));
8513
- console.log(import_chalk16.default.gray(` Run "node9 resume" to re-enable early.
9248
+ console.log(import_chalk17.default.gray(` All tool calls will be allowed without review.`));
9249
+ console.log(import_chalk17.default.gray(` Run "node9 resume" to re-enable early.
8514
9250
  `));
8515
9251
  });
8516
9252
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
8517
9253
  const { paused } = checkPause();
8518
9254
  if (!paused) {
8519
- console.log(import_chalk16.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
9255
+ console.log(import_chalk17.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
8520
9256
  return;
8521
9257
  }
8522
9258
  resumeNode9();
8523
- console.log(import_chalk16.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
9259
+ console.log(import_chalk17.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
8524
9260
  });
8525
9261
  var HOOK_BASED_AGENTS = {
8526
9262
  claude: "claude",
@@ -8533,15 +9269,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8533
9269
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
8534
9270
  const target = HOOK_BASED_AGENTS[firstArg2];
8535
9271
  console.error(
8536
- import_chalk16.default.yellow(`
9272
+ import_chalk17.default.yellow(`
8537
9273
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
8538
9274
  );
8539
- console.error(import_chalk16.default.white(`
9275
+ console.error(import_chalk17.default.white(`
8540
9276
  "${target}" uses its own hook system. Use:`));
8541
9277
  console.error(
8542
- import_chalk16.default.green(` node9 addto ${target} `) + import_chalk16.default.gray("# one-time setup")
9278
+ import_chalk17.default.green(` node9 addto ${target} `) + import_chalk17.default.gray("# one-time setup")
8543
9279
  );
8544
- console.error(import_chalk16.default.green(` ${target} `) + import_chalk16.default.gray("# run normally"));
9280
+ console.error(import_chalk17.default.green(` ${target} `) + import_chalk17.default.gray("# run normally"));
8545
9281
  process.exit(1);
8546
9282
  }
8547
9283
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -8558,7 +9294,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8558
9294
  }
8559
9295
  );
8560
9296
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
8561
- console.error(import_chalk16.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
9297
+ console.error(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
8562
9298
  const daemonReady = await autoStartDaemonAndWait();
8563
9299
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
8564
9300
  }
@@ -8571,12 +9307,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8571
9307
  }
8572
9308
  if (!result.approved) {
8573
9309
  console.error(
8574
- import_chalk16.default.red(`
9310
+ import_chalk17.default.red(`
8575
9311
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
8576
9312
  );
8577
9313
  process.exit(1);
8578
9314
  }
8579
- console.error(import_chalk16.default.green("\n\u2705 Approved \u2014 running command...\n"));
9315
+ console.error(import_chalk17.default.green("\n\u2705 Approved \u2014 running command...\n"));
8580
9316
  await runProxy(fullCommand);
8581
9317
  } else {
8582
9318
  program.help();
@@ -8591,9 +9327,9 @@ if (process.argv[2] !== "daemon") {
8591
9327
  const isCheckHook = process.argv[2] === "check";
8592
9328
  if (isCheckHook) {
8593
9329
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
8594
- const logPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "hook-debug.log");
9330
+ const logPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
8595
9331
  const msg = reason instanceof Error ? reason.message : String(reason);
8596
- import_fs22.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9332
+ import_fs24.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
8597
9333
  `);
8598
9334
  }
8599
9335
  process.exit(0);