@node9/proxy 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -94,8 +94,8 @@ function sanitizeConfig(raw) {
94
94
  }
95
95
  }
96
96
  const lines = result.error.issues.map((issue) => {
97
- const path24 = issue.path.length > 0 ? issue.path.join(".") : "root";
98
- return ` \u2022 ${path24}: ${issue.message}`;
97
+ const path27 = issue.path.length > 0 ? issue.path.join(".") : "root";
98
+ return ` \u2022 ${path27}: ${issue.message}`;
99
99
  });
100
100
  return {
101
101
  sanitized,
@@ -1589,10 +1589,96 @@ var init_ssh_parser = __esm({
1589
1589
  }
1590
1590
  });
1591
1591
 
1592
- // src/policy/index.ts
1592
+ // src/auth/trusted-hosts.ts
1593
1593
  import fs6 from "fs";
1594
1594
  import path7 from "path";
1595
1595
  import os5 from "os";
1596
+ function getTrustedHostsPath() {
1597
+ return path7.join(os5.homedir(), ".node9", "trusted-hosts.json");
1598
+ }
1599
+ function readTrustedHosts() {
1600
+ try {
1601
+ const raw = fs6.readFileSync(getTrustedHostsPath(), "utf8");
1602
+ const parsed = JSON.parse(raw);
1603
+ return Array.isArray(parsed.hosts) ? parsed.hosts : [];
1604
+ } catch {
1605
+ return [];
1606
+ }
1607
+ }
1608
+ function getFileMtime() {
1609
+ try {
1610
+ return fs6.statSync(getTrustedHostsPath()).mtimeMs;
1611
+ } catch {
1612
+ return 0;
1613
+ }
1614
+ }
1615
+ function getCachedHosts() {
1616
+ const now = Date.now();
1617
+ if (_cache && now < _cache.expiry) {
1618
+ const mtime = getFileMtime();
1619
+ if (mtime === _cache.mtime) return _cache.hosts;
1620
+ }
1621
+ const hosts = readTrustedHosts();
1622
+ _cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
1623
+ return hosts;
1624
+ }
1625
+ function writeTrustedHosts(hosts) {
1626
+ const filePath = getTrustedHostsPath();
1627
+ fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
1628
+ const tmp = filePath + ".node9-tmp";
1629
+ fs6.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
1630
+ fs6.renameSync(tmp, filePath);
1631
+ _cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
1632
+ }
1633
+ function addTrustedHost(host) {
1634
+ const normalized = normalizeHost(host);
1635
+ if (normalized.startsWith("*.")) {
1636
+ const base = normalized.slice(2);
1637
+ if (!base.includes(".")) {
1638
+ throw new Error(
1639
+ `Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
1640
+ );
1641
+ }
1642
+ }
1643
+ const hosts = readTrustedHosts();
1644
+ if (hosts.some((h) => h.host === normalized)) return;
1645
+ hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
1646
+ writeTrustedHosts(hosts);
1647
+ }
1648
+ function removeTrustedHost(host) {
1649
+ const hosts = readTrustedHosts();
1650
+ const filtered = hosts.filter((h) => h.host !== host);
1651
+ if (filtered.length === hosts.length) return false;
1652
+ writeTrustedHosts(filtered);
1653
+ return true;
1654
+ }
1655
+ function normalizeHost(raw) {
1656
+ return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
1657
+ }
1658
+ function isTrustedHost(host) {
1659
+ const normalized = normalizeHost(host);
1660
+ return getCachedHosts().some((entry) => {
1661
+ const entryHost = entry.host.toLowerCase();
1662
+ if (entryHost.startsWith("*.")) {
1663
+ const domain = entryHost.slice(2);
1664
+ return normalized.endsWith("." + domain);
1665
+ }
1666
+ return normalized === entryHost;
1667
+ });
1668
+ }
1669
+ var _cache, CACHE_TTL_MS;
1670
+ var init_trusted_hosts = __esm({
1671
+ "src/auth/trusted-hosts.ts"() {
1672
+ "use strict";
1673
+ _cache = null;
1674
+ CACHE_TTL_MS = 5e3;
1675
+ }
1676
+ });
1677
+
1678
+ // src/policy/index.ts
1679
+ import fs7 from "fs";
1680
+ import path8 from "path";
1681
+ import os6 from "os";
1596
1682
  import pm from "picomatch";
1597
1683
  import { parse } from "sh-syntax";
1598
1684
  function tokenize2(toolName) {
@@ -1608,9 +1694,9 @@ function matchesPattern(text, patterns) {
1608
1694
  const withoutDotSlash = text.replace(/^\.\//, "");
1609
1695
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1610
1696
  }
1611
- function getNestedValue(obj, path24) {
1697
+ function getNestedValue(obj, path27) {
1612
1698
  if (!obj || typeof obj !== "object") return null;
1613
- return path24.split(".").reduce((prev, curr) => prev?.[curr], obj);
1699
+ return path27.split(".").reduce((prev, curr) => prev?.[curr], obj);
1614
1700
  }
1615
1701
  function shouldSnapshot(toolName, args, config) {
1616
1702
  if (!config.settings.enableUndo) return false;
@@ -1776,23 +1862,39 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1776
1862
  return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
1777
1863
  }
1778
1864
  const pipeAnalysis = analyzePipeChain(shellCommand);
1779
- if (pipeAnalysis.isPipeline) {
1865
+ if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1866
+ const sinks = pipeAnalysis.sinkTargets;
1867
+ const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
1780
1868
  if (pipeAnalysis.risk === "critical") {
1869
+ if (allTrusted) {
1870
+ return {
1871
+ decision: "review",
1872
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1873
+ reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1874
+ tier: 3
1875
+ };
1876
+ }
1781
1877
  return {
1782
1878
  decision: "block",
1783
1879
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1784
- reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
1880
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1785
1881
  tier: 3
1786
1882
  };
1787
1883
  }
1788
- if (pipeAnalysis.risk === "high") {
1884
+ if (allTrusted) {
1789
1885
  return {
1790
- decision: "review",
1791
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1792
- reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
1886
+ decision: "allow",
1887
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1888
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1793
1889
  tier: 3
1794
1890
  };
1795
1891
  }
1892
+ return {
1893
+ decision: "review",
1894
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1895
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1896
+ tier: 3
1897
+ };
1796
1898
  }
1797
1899
  const firstToken = analyzed.actions[0] ?? "";
1798
1900
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
@@ -1800,7 +1902,7 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1800
1902
  const sshHosts = extractAllSshHosts(rawTokens.slice(1));
1801
1903
  allTokens.push(...sshHosts);
1802
1904
  }
1803
- if (firstToken && path7.posix.isAbsolute(firstToken)) {
1905
+ if (firstToken && path8.posix.isAbsolute(firstToken)) {
1804
1906
  const prov = checkProvenance(firstToken, cwd);
1805
1907
  if (prov.trustLevel === "suspect") {
1806
1908
  return {
@@ -1897,9 +1999,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1897
1999
  }
1898
2000
  async function explainPolicy(toolName, args) {
1899
2001
  const steps = [];
1900
- const globalPath = path7.join(os5.homedir(), ".node9", "config.json");
1901
- const projectPath = path7.join(process.cwd(), "node9.config.json");
1902
- const credsPath = path7.join(os5.homedir(), ".node9", "credentials.json");
2002
+ const globalPath = path8.join(os6.homedir(), ".node9", "config.json");
2003
+ const projectPath = path8.join(process.cwd(), "node9.config.json");
2004
+ const credsPath = path8.join(os6.homedir(), ".node9", "credentials.json");
1903
2005
  const waterfall = [
1904
2006
  {
1905
2007
  tier: 1,
@@ -1910,19 +2012,19 @@ async function explainPolicy(toolName, args) {
1910
2012
  {
1911
2013
  tier: 2,
1912
2014
  label: "Cloud policy",
1913
- status: fs6.existsSync(credsPath) ? "active" : "missing",
1914
- note: fs6.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
2015
+ status: fs7.existsSync(credsPath) ? "active" : "missing",
2016
+ note: fs7.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1915
2017
  },
1916
2018
  {
1917
2019
  tier: 3,
1918
2020
  label: "Project config",
1919
- status: fs6.existsSync(projectPath) ? "active" : "missing",
2021
+ status: fs7.existsSync(projectPath) ? "active" : "missing",
1920
2022
  path: projectPath
1921
2023
  },
1922
2024
  {
1923
2025
  tier: 4,
1924
2026
  label: "Global config",
1925
- status: fs6.existsSync(globalPath) ? "active" : "missing",
2027
+ status: fs7.existsSync(globalPath) ? "active" : "missing",
1926
2028
  path: globalPath
1927
2029
  },
1928
2030
  {
@@ -2169,21 +2271,22 @@ var init_policy = __esm({
2169
2271
  init_provenance();
2170
2272
  init_pipe_chain();
2171
2273
  init_ssh_parser();
2274
+ init_trusted_hosts();
2172
2275
  SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2173
2276
  }
2174
2277
  });
2175
2278
 
2176
2279
  // src/auth/state.ts
2177
- import fs7 from "fs";
2178
- import path8 from "path";
2179
- import os6 from "os";
2280
+ import fs8 from "fs";
2281
+ import path9 from "path";
2282
+ import os7 from "os";
2180
2283
  function checkPause() {
2181
2284
  try {
2182
- if (!fs7.existsSync(PAUSED_FILE)) return { paused: false };
2183
- const state = JSON.parse(fs7.readFileSync(PAUSED_FILE, "utf-8"));
2285
+ if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
2286
+ const state = JSON.parse(fs8.readFileSync(PAUSED_FILE, "utf-8"));
2184
2287
  if (state.expiry > 0 && Date.now() >= state.expiry) {
2185
2288
  try {
2186
- fs7.unlinkSync(PAUSED_FILE);
2289
+ fs8.unlinkSync(PAUSED_FILE);
2187
2290
  } catch {
2188
2291
  }
2189
2292
  return { paused: false };
@@ -2194,11 +2297,11 @@ function checkPause() {
2194
2297
  }
2195
2298
  }
2196
2299
  function atomicWriteSync(filePath, data, options) {
2197
- const dir = path8.dirname(filePath);
2198
- if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
2199
- const tmpPath = `${filePath}.${os6.hostname()}.${process.pid}.tmp`;
2200
- fs7.writeFileSync(tmpPath, data, options);
2201
- fs7.renameSync(tmpPath, filePath);
2300
+ const dir = path9.dirname(filePath);
2301
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
2302
+ const tmpPath = `${filePath}.${os7.hostname()}.${process.pid}.tmp`;
2303
+ fs8.writeFileSync(tmpPath, data, options);
2304
+ fs8.renameSync(tmpPath, filePath);
2202
2305
  }
2203
2306
  function pauseNode9(durationMs, durationStr) {
2204
2307
  const state = { expiry: Date.now() + durationMs, duration: durationStr };
@@ -2206,18 +2309,18 @@ function pauseNode9(durationMs, durationStr) {
2206
2309
  }
2207
2310
  function resumeNode9() {
2208
2311
  try {
2209
- if (fs7.existsSync(PAUSED_FILE)) fs7.unlinkSync(PAUSED_FILE);
2312
+ if (fs8.existsSync(PAUSED_FILE)) fs8.unlinkSync(PAUSED_FILE);
2210
2313
  } catch {
2211
2314
  }
2212
2315
  }
2213
2316
  function getActiveTrustSession(toolName) {
2214
2317
  try {
2215
- if (!fs7.existsSync(TRUST_FILE)) return false;
2216
- const trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
2318
+ if (!fs8.existsSync(TRUST_FILE)) return false;
2319
+ const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
2217
2320
  const now = Date.now();
2218
2321
  const active = trust.entries.filter((e) => e.expiry > now);
2219
2322
  if (active.length !== trust.entries.length) {
2220
- fs7.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
2323
+ fs8.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
2221
2324
  }
2222
2325
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
2223
2326
  } catch {
@@ -2228,8 +2331,8 @@ function writeTrustSession(toolName, durationMs) {
2228
2331
  try {
2229
2332
  let trust = { entries: [] };
2230
2333
  try {
2231
- if (fs7.existsSync(TRUST_FILE)) {
2232
- trust = JSON.parse(fs7.readFileSync(TRUST_FILE, "utf-8"));
2334
+ if (fs8.existsSync(TRUST_FILE)) {
2335
+ trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
2233
2336
  }
2234
2337
  } catch {
2235
2338
  }
@@ -2245,9 +2348,9 @@ function writeTrustSession(toolName, durationMs) {
2245
2348
  }
2246
2349
  function getPersistentDecision(toolName) {
2247
2350
  try {
2248
- const file = path8.join(os6.homedir(), ".node9", "decisions.json");
2249
- if (!fs7.existsSync(file)) return null;
2250
- const decisions = JSON.parse(fs7.readFileSync(file, "utf-8"));
2351
+ const file = path9.join(os7.homedir(), ".node9", "decisions.json");
2352
+ if (!fs8.existsSync(file)) return null;
2353
+ const decisions = JSON.parse(fs8.readFileSync(file, "utf-8"));
2251
2354
  const d = decisions[toolName];
2252
2355
  if (d === "allow" || d === "deny") return d;
2253
2356
  } catch {
@@ -2259,21 +2362,21 @@ var init_state = __esm({
2259
2362
  "src/auth/state.ts"() {
2260
2363
  "use strict";
2261
2364
  init_policy();
2262
- PAUSED_FILE = path8.join(os6.homedir(), ".node9", "PAUSED");
2263
- TRUST_FILE = path8.join(os6.homedir(), ".node9", "trust.json");
2365
+ PAUSED_FILE = path9.join(os7.homedir(), ".node9", "PAUSED");
2366
+ TRUST_FILE = path9.join(os7.homedir(), ".node9", "trust.json");
2264
2367
  }
2265
2368
  });
2266
2369
 
2267
2370
  // src/auth/daemon.ts
2268
- import fs8 from "fs";
2269
- import path9 from "path";
2270
- import os7 from "os";
2371
+ import fs9 from "fs";
2372
+ import path10 from "path";
2373
+ import os8 from "os";
2271
2374
  import { spawnSync } from "child_process";
2272
2375
  function getInternalToken() {
2273
2376
  try {
2274
- const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
2275
- if (!fs8.existsSync(pidFile)) return null;
2276
- const data = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
2377
+ const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
2378
+ if (!fs9.existsSync(pidFile)) return null;
2379
+ const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
2277
2380
  process.kill(data.pid, 0);
2278
2381
  return data.internalToken ?? null;
2279
2382
  } catch {
@@ -2281,10 +2384,10 @@ function getInternalToken() {
2281
2384
  }
2282
2385
  }
2283
2386
  function isDaemonRunning() {
2284
- const pidFile = path9.join(os7.homedir(), ".node9", "daemon.pid");
2285
- if (fs8.existsSync(pidFile)) {
2387
+ const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
2388
+ if (fs9.existsSync(pidFile)) {
2286
2389
  try {
2287
- const { pid, port } = JSON.parse(fs8.readFileSync(pidFile, "utf-8"));
2390
+ const { pid, port } = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
2288
2391
  if (port !== DAEMON_PORT) return false;
2289
2392
  process.kill(pid, 0);
2290
2393
  return true;
@@ -2325,8 +2428,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2325
2428
  signal: ctrl.signal
2326
2429
  });
2327
2430
  if (!res.ok) throw new Error("Daemon fail");
2328
- const { id } = await res.json();
2329
- return id;
2431
+ const { id, allowCount } = await res.json();
2432
+ return { id, allowCount: allowCount ?? 1 };
2330
2433
  } finally {
2331
2434
  clearTimeout(timer);
2332
2435
  }
@@ -2365,15 +2468,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
2365
2468
  signal: AbortSignal.timeout(3e3)
2366
2469
  });
2367
2470
  if (!res.ok) throw new Error("Daemon unreachable");
2368
- const { id } = await res.json();
2369
- return id;
2471
+ const { id, allowCount } = await res.json();
2472
+ return { id, allowCount: allowCount ?? 1 };
2370
2473
  }
2371
- async function resolveViaDaemon(id, decision, internalToken) {
2474
+ async function resolveViaDaemon(id, decision, internalToken, source) {
2372
2475
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2373
2476
  await fetch(`${base}/resolve/${id}`, {
2374
2477
  method: "POST",
2375
2478
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
2376
- body: JSON.stringify({ decision }),
2479
+ body: JSON.stringify({ decision, ...source && { source } }),
2377
2480
  signal: AbortSignal.timeout(3e3)
2378
2481
  });
2379
2482
  }
@@ -2387,7 +2490,7 @@ var init_daemon = __esm({
2387
2490
  });
2388
2491
 
2389
2492
  // src/context-sniper.ts
2390
- import path10 from "path";
2493
+ import path11 from "path";
2391
2494
  function smartTruncate(str, maxLen = 500) {
2392
2495
  if (str.length <= maxLen) return str;
2393
2496
  const edge = Math.floor(maxLen / 2) - 3;
@@ -2439,7 +2542,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
2439
2542
  intent = "EDIT";
2440
2543
  if (obj.file_path) {
2441
2544
  editFilePath = String(obj.file_path);
2442
- editFileName = path10.basename(editFilePath);
2545
+ editFileName = path11.basename(editFilePath);
2443
2546
  }
2444
2547
  const result = extractContext(String(obj.new_string), matchedWord);
2445
2548
  contextSnippet = result.snippet;
@@ -2496,7 +2599,7 @@ var init_context_sniper = __esm({
2496
2599
 
2497
2600
  // src/ui/native.ts
2498
2601
  import { spawn } from "child_process";
2499
- import path11 from "path";
2602
+ import path12 from "path";
2500
2603
  function formatArgs(args, matchedField, matchedWord) {
2501
2604
  if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
2502
2605
  let parsed = args;
@@ -2515,7 +2618,7 @@ function formatArgs(args, matchedField, matchedWord) {
2515
2618
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
2516
2619
  const obj = parsed;
2517
2620
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
2518
- const file = obj.file_path ? path11.basename(String(obj.file_path)) : "file";
2621
+ const file = obj.file_path ? path12.basename(String(obj.file_path)) : "file";
2519
2622
  const oldPreview = smartTruncate(String(obj.old_string), 120);
2520
2623
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
2521
2624
  return {
@@ -2578,20 +2681,24 @@ ${smartTruncate(str, 500)}`
2578
2681
  function escapePango(text) {
2579
2682
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2580
2683
  }
2581
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2684
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2582
2685
  const lines = [];
2583
2686
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2584
2687
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2585
2688
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2586
2689
  lines.push("");
2587
2690
  lines.push(formattedArgs);
2691
+ if (allowCount >= 3) {
2692
+ lines.push("");
2693
+ lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
2694
+ }
2588
2695
  if (!locked) {
2589
2696
  lines.push("");
2590
2697
  lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
2591
2698
  }
2592
2699
  return lines.join("\n");
2593
2700
  }
2594
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2701
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2595
2702
  const lines = [];
2596
2703
  if (locked) {
2597
2704
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2603,6 +2710,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2603
2710
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2604
2711
  lines.push("");
2605
2712
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2713
+ if (allowCount >= 3) {
2714
+ lines.push("");
2715
+ lines.push(
2716
+ `<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
2717
+ );
2718
+ }
2606
2719
  if (!locked) {
2607
2720
  lines.push("");
2608
2721
  lines.push(
@@ -2611,12 +2724,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2611
2724
  }
2612
2725
  return lines.join("\n");
2613
2726
  }
2614
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
2727
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2615
2728
  if (isTestEnv()) return "deny";
2616
2729
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2617
2730
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
2618
2731
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
2619
- const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
2732
+ const message = buildPlainMessage(
2733
+ toolName,
2734
+ formattedArgs,
2735
+ agent,
2736
+ explainableLabel,
2737
+ locked,
2738
+ allowCount
2739
+ );
2620
2740
  return new Promise((resolve) => {
2621
2741
  let childProcess = null;
2622
2742
  const onAbort = () => {
@@ -2648,7 +2768,8 @@ end run`;
2648
2768
  formattedArgs,
2649
2769
  agent,
2650
2770
  explainableLabel,
2651
- locked
2771
+ locked,
2772
+ allowCount
2652
2773
  );
2653
2774
  const argsList = [
2654
2775
  locked ? "--info" : "--question",
@@ -2700,8 +2821,8 @@ var init_native = __esm({
2700
2821
  });
2701
2822
 
2702
2823
  // src/auth/cloud.ts
2703
- import fs9 from "fs";
2704
- import os8 from "os";
2824
+ import fs10 from "fs";
2825
+ import os9 from "os";
2705
2826
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2706
2827
  return fetch(`${creds.apiUrl}/audit`, {
2707
2828
  method: "POST",
@@ -2713,9 +2834,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2713
2834
  context: {
2714
2835
  agent: meta?.agent,
2715
2836
  mcpServer: meta?.mcpServer,
2716
- hostname: os8.hostname(),
2837
+ hostname: os9.hostname(),
2717
2838
  cwd: process.cwd(),
2718
- platform: os8.platform()
2839
+ platform: os9.platform()
2719
2840
  }
2720
2841
  }),
2721
2842
  signal: AbortSignal.timeout(5e3)
@@ -2736,9 +2857,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2736
2857
  context: {
2737
2858
  agent: meta?.agent,
2738
2859
  mcpServer: meta?.mcpServer,
2739
- hostname: os8.hostname(),
2860
+ hostname: os9.hostname(),
2740
2861
  cwd: process.cwd(),
2741
- platform: os8.platform()
2862
+ platform: os9.platform()
2742
2863
  },
2743
2864
  ...riskMetadata && { riskMetadata }
2744
2865
  }),
@@ -2794,14 +2915,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2794
2915
  });
2795
2916
  clearTimeout(timer);
2796
2917
  if (!res.ok) {
2797
- fs9.appendFileSync(
2918
+ fs10.appendFileSync(
2798
2919
  HOOK_DEBUG_LOG,
2799
2920
  `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
2800
2921
  `
2801
2922
  );
2802
2923
  }
2803
2924
  } catch (err) {
2804
- fs9.appendFileSync(
2925
+ fs10.appendFileSync(
2805
2926
  HOOK_DEBUG_LOG,
2806
2927
  `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
2807
2928
  `
@@ -2817,8 +2938,8 @@ var init_cloud = __esm({
2817
2938
 
2818
2939
  // src/auth/orchestrator.ts
2819
2940
  import net from "net";
2820
- import path12 from "path";
2821
- import os9 from "os";
2941
+ import path13 from "path";
2942
+ import os10 from "os";
2822
2943
  import { randomUUID } from "crypto";
2823
2944
  function notifyActivity(data) {
2824
2945
  return new Promise((resolve) => {
@@ -3012,13 +3133,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3012
3133
  let viewerId = null;
3013
3134
  const internalToken = getInternalToken();
3014
3135
  let daemonEntryId = null;
3136
+ let daemonAllowCount = 1;
3015
3137
  if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
3016
3138
  if (cloudEnforced && cloudRequestId) {
3017
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3139
+ const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3140
+ viewerId = viewer?.id ?? null;
3018
3141
  daemonEntryId = viewerId;
3142
+ if (viewer) daemonAllowCount = viewer.allowCount;
3019
3143
  } else {
3020
3144
  try {
3021
- daemonEntryId = await registerDaemonEntry(
3145
+ const entry = await registerDaemonEntry(
3022
3146
  toolName,
3023
3147
  args,
3024
3148
  meta,
@@ -3026,6 +3150,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3026
3150
  options?.activityId,
3027
3151
  options?.cwd
3028
3152
  );
3153
+ daemonEntryId = entry.id;
3154
+ daemonAllowCount = entry.allowCount;
3029
3155
  } catch {
3030
3156
  }
3031
3157
  }
@@ -3061,7 +3187,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3061
3187
  false,
3062
3188
  signal,
3063
3189
  policyMatchedField,
3064
- policyMatchedWord
3190
+ policyMatchedWord,
3191
+ daemonAllowCount
3065
3192
  );
3066
3193
  if (decision === "always_allow") {
3067
3194
  writeTrustSession(toolName, 36e5);
@@ -3119,10 +3246,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3119
3246
  if (!resolved) {
3120
3247
  resolved = true;
3121
3248
  abortController.abort();
3122
- if (viewerId && internalToken) {
3123
- resolveViaDaemon(viewerId, res.approved ? "allow" : "deny", internalToken).catch(
3124
- () => null
3125
- );
3249
+ if (daemonEntryId && internalToken) {
3250
+ resolveViaDaemon(
3251
+ daemonEntryId,
3252
+ res.approved ? "allow" : "deny",
3253
+ internalToken,
3254
+ res.decisionSource
3255
+ ).catch(() => null);
3126
3256
  }
3127
3257
  resolve(res);
3128
3258
  }
@@ -3179,7 +3309,7 @@ var init_orchestrator = __esm({
3179
3309
  init_state();
3180
3310
  init_daemon();
3181
3311
  init_cloud();
3182
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path12.join(os9.tmpdir(), "node9-activity.sock");
3312
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os10.tmpdir(), "node9-activity.sock");
3183
3313
  }
3184
3314
  });
3185
3315
 
@@ -3475,6 +3605,15 @@ var init_ui = __esm({
3475
3605
  padding: 5px 10px;
3476
3606
  margin-bottom: 14px;
3477
3607
  }
3608
+ .insight-hint {
3609
+ font-size: 12px;
3610
+ color: #f0c040;
3611
+ background: rgba(240, 192, 64, 0.08);
3612
+ border: 1px solid rgba(240, 192, 64, 0.25);
3613
+ border-radius: 6px;
3614
+ padding: 6px 10px;
3615
+ margin-bottom: 12px;
3616
+ }
3478
3617
  pre {
3479
3618
  background: #0d1117;
3480
3619
  padding: 14px 16px;
@@ -3947,6 +4086,78 @@ var init_ui = __esm({
3947
4086
  color: var(--danger);
3948
4087
  }
3949
4088
 
4089
+ /* \u2500\u2500 Suggestion cards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4090
+ .suggestion-card {
4091
+ background: rgba(82, 130, 255, 0.06);
4092
+ border: 1px solid rgba(82, 130, 255, 0.25);
4093
+ border-radius: 8px;
4094
+ padding: 10px 12px;
4095
+ margin-bottom: 8px;
4096
+ }
4097
+ .suggestion-card:last-child {
4098
+ margin-bottom: 0;
4099
+ }
4100
+ .suggestion-header {
4101
+ display: flex;
4102
+ align-items: center;
4103
+ gap: 8px;
4104
+ margin-bottom: 6px;
4105
+ }
4106
+ .suggestion-tool {
4107
+ font-family: 'Fira Code', monospace;
4108
+ font-size: 11px;
4109
+ color: var(--text-bright);
4110
+ flex: 1;
4111
+ word-break: break-all;
4112
+ }
4113
+ .suggestion-count {
4114
+ font-size: 10px;
4115
+ color: var(--muted);
4116
+ white-space: nowrap;
4117
+ }
4118
+ .suggestion-rule {
4119
+ font-family: 'Fira Code', monospace;
4120
+ font-size: 10px;
4121
+ color: #79c0ff;
4122
+ background: rgba(0, 0, 0, 0.25);
4123
+ border-radius: 4px;
4124
+ padding: 4px 8px;
4125
+ margin-bottom: 8px;
4126
+ word-break: break-all;
4127
+ white-space: pre-wrap;
4128
+ }
4129
+ .suggestion-actions {
4130
+ display: flex;
4131
+ gap: 6px;
4132
+ }
4133
+ .btn-apply {
4134
+ background: rgba(52, 125, 57, 0.2);
4135
+ border: 1px solid rgba(87, 171, 90, 0.4);
4136
+ color: #57ab5a;
4137
+ padding: 4px 10px;
4138
+ font-size: 11px;
4139
+ border-radius: 5px;
4140
+ font-family: inherit;
4141
+ cursor: pointer;
4142
+ }
4143
+ .btn-apply:hover {
4144
+ background: rgba(52, 125, 57, 0.35);
4145
+ }
4146
+ .btn-dismiss-suggestion {
4147
+ background: transparent;
4148
+ border: 1px solid var(--border);
4149
+ color: var(--muted);
4150
+ padding: 4px 10px;
4151
+ font-size: 11px;
4152
+ border-radius: 5px;
4153
+ font-family: inherit;
4154
+ cursor: pointer;
4155
+ }
4156
+ .btn-dismiss-suggestion:hover {
4157
+ border-color: var(--danger);
4158
+ color: var(--danger);
4159
+ }
4160
+
3950
4161
  .modal-overlay {
3951
4162
  display: none;
3952
4163
  position: fixed;
@@ -4128,6 +4339,11 @@ var init_ui = __esm({
4128
4339
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
4129
4340
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
4130
4341
  </div>
4342
+
4343
+ <div class="panel" id="suggestionsPanel" style="display: none">
4344
+ <div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
4345
+ <div id="suggestionsList"></div>
4346
+ </div>
4131
4347
  </div>
4132
4348
  </div>
4133
4349
  </div>
@@ -4317,6 +4533,7 @@ var init_ui = __esm({
4317
4533
  </div>
4318
4534
  <div class="tool-chip">\${esc(req.toolName)}</div>
4319
4535
  \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
4536
+ \${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
4320
4537
  \${renderPayload(req)}
4321
4538
  <div class="actions" id="act-\${req.id}">
4322
4539
  <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
@@ -4383,6 +4600,14 @@ var init_ui = __esm({
4383
4600
  ev.addEventListener('shields-status', (e) => {
4384
4601
  renderShields(JSON.parse(e.data).shields);
4385
4602
  });
4603
+ ev.addEventListener('suggestion:new', (e) => {
4604
+ const s = JSON.parse(e.data);
4605
+ addSuggestionCard(s);
4606
+ });
4607
+ ev.addEventListener('suggestion:resolved', (e) => {
4608
+ const { id } = JSON.parse(e.data);
4609
+ removeSuggestionCard(id);
4610
+ });
4386
4611
 
4387
4612
  // \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4388
4613
  ev.addEventListener('activity', (e) => {
@@ -4632,6 +4857,74 @@ var init_ui = __esm({
4632
4857
  .then((r) => r.json())
4633
4858
  .then(renderDecisions)
4634
4859
  .catch(() => {});
4860
+
4861
+ // \u2500\u2500 Smart Rule Suggestions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4862
+ function rulePreview(suggestion) {
4863
+ const r = suggestion.suggestedRule;
4864
+ if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
4865
+ const cond = r.rule.conditions?.[0];
4866
+ const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
4867
+ return \`allow \${r.rule.tool}\${condStr}\`;
4868
+ }
4869
+
4870
+ function addSuggestionCard(s) {
4871
+ const panel = document.getElementById('suggestionsPanel');
4872
+ const list = document.getElementById('suggestionsList');
4873
+ panel.style.display = '';
4874
+
4875
+ const card = document.createElement('div');
4876
+ card.className = 'suggestion-card';
4877
+ card.id = 'sg-' + s.id;
4878
+ card.innerHTML = \`
4879
+ <div class="suggestion-header">
4880
+ <span class="suggestion-tool">\${esc(s.toolName)}</span>
4881
+ <span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
4882
+ </div>
4883
+ <div class="suggestion-rule">\${esc(rulePreview(s))}</div>
4884
+ <div class="suggestion-actions">
4885
+ <button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
4886
+ <button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
4887
+ </div>
4888
+ \`;
4889
+ list.appendChild(card);
4890
+ }
4891
+
4892
+ function removeSuggestionCard(id) {
4893
+ document.getElementById('sg-' + id)?.remove();
4894
+ const list = document.getElementById('suggestionsList');
4895
+ if (!list.querySelector('.suggestion-card')) {
4896
+ document.getElementById('suggestionsPanel').style.display = 'none';
4897
+ }
4898
+ }
4899
+
4900
+ function applySuggestion(id) {
4901
+ fetch('/suggestions/' + id + '/apply', {
4902
+ method: 'POST',
4903
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
4904
+ body: JSON.stringify({}),
4905
+ })
4906
+ .then((r) => {
4907
+ if (r.ok) removeSuggestionCard(id);
4908
+ })
4909
+ .catch(() => {});
4910
+ }
4911
+
4912
+ function dismissSuggestion(id) {
4913
+ fetch('/suggestions/' + id + '/dismiss', {
4914
+ method: 'POST',
4915
+ headers: { 'X-Node9-Token': CSRF_TOKEN },
4916
+ })
4917
+ .then((r) => {
4918
+ if (r.ok) removeSuggestionCard(id);
4919
+ })
4920
+ .catch(() => {});
4921
+ }
4922
+
4923
+ // Load any suggestions that survived a page reload (daemon still running)
4924
+ fetch('/suggestions')
4925
+ .then((r) => r.json())
4926
+ .then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
4927
+ .catch(() => {});
4635
4928
  </script>
4636
4929
  </body>
4637
4930
  </html>
@@ -4649,13 +4942,123 @@ var init_ui2 = __esm({
4649
4942
  }
4650
4943
  });
4651
4944
 
4945
+ // src/daemon/suggestion-tracker.ts
4946
+ import { randomUUID as randomUUID2 } from "crypto";
4947
+ function extractPath(args) {
4948
+ if (!args || typeof args !== "object") return null;
4949
+ const a = args;
4950
+ for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
4951
+ if (typeof a[key] === "string" && a[key]) return a[key];
4952
+ }
4953
+ return null;
4954
+ }
4955
+ function commonPathPrefix(paths) {
4956
+ if (paths.length < 2) return null;
4957
+ const dirParts = paths.map((p) => {
4958
+ const lastSlash = p.lastIndexOf("/");
4959
+ return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
4960
+ });
4961
+ const first = dirParts[0].split("/");
4962
+ const common = [];
4963
+ for (let i = 0; i < first.length; i++) {
4964
+ if (dirParts.every((d) => d.split("/")[i] === first[i])) {
4965
+ common.push(first[i]);
4966
+ } else {
4967
+ break;
4968
+ }
4969
+ }
4970
+ const prefix = common.join("/").replace(/\/?$/, "/");
4971
+ return prefix.length > 1 ? prefix : null;
4972
+ }
4973
+ var SuggestionTracker;
4974
+ var init_suggestion_tracker = __esm({
4975
+ "src/daemon/suggestion-tracker.ts"() {
4976
+ "use strict";
4977
+ SuggestionTracker = class {
4978
+ events = /* @__PURE__ */ new Map();
4979
+ threshold;
4980
+ constructor(threshold = 3) {
4981
+ this.threshold = threshold;
4982
+ }
4983
+ /**
4984
+ * Record a human-allowed review for a tool.
4985
+ * Returns a Suggestion when the threshold is reached, null otherwise.
4986
+ */
4987
+ recordAllow(toolName, args) {
4988
+ const events = this.events.get(toolName) ?? [];
4989
+ events.push({ args, ts: Date.now() });
4990
+ this.events.set(toolName, events);
4991
+ if (events.length >= this.threshold) {
4992
+ this.events.delete(toolName);
4993
+ return this.generateSuggestion(toolName, events);
4994
+ }
4995
+ return null;
4996
+ }
4997
+ /**
4998
+ * Reset the counter for a tool (e.g. when the user clicks Deny —
4999
+ * don't suggest allowing something they just blocked).
5000
+ */
5001
+ resetTool(toolName) {
5002
+ this.events.delete(toolName);
5003
+ }
5004
+ /** Current allow count for a tool (for tests). */
5005
+ getCount(toolName) {
5006
+ return this.events.get(toolName)?.length ?? 0;
5007
+ }
5008
+ generateSuggestion(toolName, events) {
5009
+ const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
5010
+ const prefix = commonPathPrefix(paths);
5011
+ const suggestedRule = prefix ? {
5012
+ type: "smartRule",
5013
+ rule: {
5014
+ name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
5015
+ tool: toolName,
5016
+ conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
5017
+ verdict: "allow",
5018
+ reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
5019
+ }
5020
+ } : { type: "ignoredTool", toolName };
5021
+ return {
5022
+ id: randomUUID2(),
5023
+ toolName,
5024
+ allowCount: events.length,
5025
+ suggestedRule,
5026
+ status: "pending",
5027
+ createdAt: Date.now(),
5028
+ exampleArgs: events.slice(0, 3).map((e) => e.args)
5029
+ };
5030
+ }
5031
+ };
5032
+ }
5033
+ });
5034
+
4652
5035
  // src/daemon/state.ts
4653
5036
  import net2 from "net";
4654
- import fs11 from "fs";
4655
- import path14 from "path";
4656
- import os11 from "os";
5037
+ import fs12 from "fs";
5038
+ import path15 from "path";
5039
+ import os12 from "os";
4657
5040
  import { spawn as spawn2 } from "child_process";
4658
- import { randomUUID as randomUUID2 } from "crypto";
5041
+ import { randomUUID as randomUUID3 } from "crypto";
5042
+ function loadInsightCounts() {
5043
+ try {
5044
+ if (!fs12.existsSync(INSIGHT_COUNTS_FILE)) return;
5045
+ const data = JSON.parse(fs12.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
5046
+ for (const [tool, count] of Object.entries(data)) {
5047
+ if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
5048
+ }
5049
+ } catch {
5050
+ }
5051
+ }
5052
+ function saveInsightCounts() {
5053
+ try {
5054
+ const data = {};
5055
+ insightCounts.forEach((count, tool) => {
5056
+ data[tool] = count;
5057
+ });
5058
+ atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
5059
+ } catch {
5060
+ }
5061
+ }
4659
5062
  function getAbandonTimer() {
4660
5063
  return _abandonTimer;
4661
5064
  }
@@ -4678,11 +5081,27 @@ function markRejectionHandlerRegistered() {
4678
5081
  daemonRejectionHandlerRegistered = true;
4679
5082
  }
4680
5083
  function atomicWriteSync2(filePath, data, options) {
4681
- const dir = path14.dirname(filePath);
4682
- if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
4683
- const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
4684
- fs11.writeFileSync(tmpPath, data, options);
4685
- fs11.renameSync(tmpPath, filePath);
5084
+ const dir = path15.dirname(filePath);
5085
+ if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
5086
+ const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
5087
+ try {
5088
+ fs12.writeFileSync(tmpPath, data, options);
5089
+ } catch (err) {
5090
+ try {
5091
+ fs12.unlinkSync(tmpPath);
5092
+ } catch {
5093
+ }
5094
+ throw err;
5095
+ }
5096
+ try {
5097
+ fs12.renameSync(tmpPath, filePath);
5098
+ } catch (err) {
5099
+ try {
5100
+ fs12.unlinkSync(tmpPath);
5101
+ } catch {
5102
+ }
5103
+ throw err;
5104
+ }
4686
5105
  }
4687
5106
  function redactArgs(value) {
4688
5107
  if (!value || typeof value !== "object") return value;
@@ -4702,16 +5121,16 @@ function appendAuditLog(data) {
4702
5121
  decision: data.decision,
4703
5122
  source: "daemon"
4704
5123
  };
4705
- const dir = path14.dirname(AUDIT_LOG_FILE);
4706
- if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
4707
- fs11.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
5124
+ const dir = path15.dirname(AUDIT_LOG_FILE);
5125
+ if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
5126
+ fs12.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
4708
5127
  } catch {
4709
5128
  }
4710
5129
  }
4711
5130
  function getAuditHistory(limit = 20) {
4712
5131
  try {
4713
- if (!fs11.existsSync(AUDIT_LOG_FILE)) return [];
4714
- const lines = fs11.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
5132
+ if (!fs12.existsSync(AUDIT_LOG_FILE)) return [];
5133
+ const lines = fs12.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
4715
5134
  if (lines.length === 1 && lines[0] === "") return [];
4716
5135
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
4717
5136
  } catch {
@@ -4720,19 +5139,19 @@ function getAuditHistory(limit = 20) {
4720
5139
  }
4721
5140
  function getOrgName() {
4722
5141
  try {
4723
- if (fs11.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
5142
+ if (fs12.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
4724
5143
  } catch {
4725
5144
  }
4726
5145
  return null;
4727
5146
  }
4728
5147
  function hasStoredSlackKey() {
4729
- return fs11.existsSync(CREDENTIALS_FILE);
5148
+ return fs12.existsSync(CREDENTIALS_FILE);
4730
5149
  }
4731
5150
  function writeGlobalSetting(key, value) {
4732
5151
  let config = {};
4733
5152
  try {
4734
- if (fs11.existsSync(GLOBAL_CONFIG_FILE)) {
4735
- config = JSON.parse(fs11.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
5153
+ if (fs12.existsSync(GLOBAL_CONFIG_FILE)) {
5154
+ config = JSON.parse(fs12.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4736
5155
  }
4737
5156
  } catch {
4738
5157
  }
@@ -4744,8 +5163,8 @@ function writeTrustEntry(toolName, durationMs) {
4744
5163
  try {
4745
5164
  let trust = { entries: [] };
4746
5165
  try {
4747
- if (fs11.existsSync(TRUST_FILE2))
4748
- trust = JSON.parse(fs11.readFileSync(TRUST_FILE2, "utf-8"));
5166
+ if (fs12.existsSync(TRUST_FILE2))
5167
+ trust = JSON.parse(fs12.readFileSync(TRUST_FILE2, "utf-8"));
4749
5168
  } catch {
4750
5169
  }
4751
5170
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -4756,8 +5175,8 @@ function writeTrustEntry(toolName, durationMs) {
4756
5175
  }
4757
5176
  function readPersistentDecisions() {
4758
5177
  try {
4759
- if (fs11.existsSync(DECISIONS_FILE)) {
4760
- return JSON.parse(fs11.readFileSync(DECISIONS_FILE, "utf-8"));
5178
+ if (fs12.existsSync(DECISIONS_FILE)) {
5179
+ return JSON.parse(fs12.readFileSync(DECISIONS_FILE, "utf-8"));
4761
5180
  }
4762
5181
  } catch {
4763
5182
  }
@@ -4822,7 +5241,7 @@ function abandonPending() {
4822
5241
  });
4823
5242
  if (autoStarted) {
4824
5243
  try {
4825
- fs11.unlinkSync(DAEMON_PID_FILE);
5244
+ fs12.unlinkSync(DAEMON_PID_FILE);
4826
5245
  } catch {
4827
5246
  }
4828
5247
  setTimeout(() => {
@@ -4833,7 +5252,7 @@ function abandonPending() {
4833
5252
  }
4834
5253
  function startActivitySocket() {
4835
5254
  try {
4836
- fs11.unlinkSync(ACTIVITY_SOCKET_PATH2);
5255
+ fs12.unlinkSync(ACTIVITY_SOCKET_PATH2);
4837
5256
  } catch {
4838
5257
  }
4839
5258
  const ACTIVITY_MAX_BYTES = 1024 * 1024;
@@ -4875,25 +5294,30 @@ function startActivitySocket() {
4875
5294
  unixServer.listen(ACTIVITY_SOCKET_PATH2);
4876
5295
  process.on("exit", () => {
4877
5296
  try {
4878
- fs11.unlinkSync(ACTIVITY_SOCKET_PATH2);
5297
+ fs12.unlinkSync(ACTIVITY_SOCKET_PATH2);
4879
5298
  } catch {
4880
5299
  }
4881
5300
  });
4882
5301
  }
4883
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, pending, sseClients, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
5302
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
4884
5303
  var init_state2 = __esm({
4885
5304
  "src/daemon/state.ts"() {
4886
5305
  "use strict";
4887
5306
  init_daemon();
4888
- homeDir = os11.homedir();
4889
- DAEMON_PID_FILE = path14.join(homeDir, ".node9", "daemon.pid");
4890
- DECISIONS_FILE = path14.join(homeDir, ".node9", "decisions.json");
4891
- AUDIT_LOG_FILE = path14.join(homeDir, ".node9", "audit.log");
4892
- TRUST_FILE2 = path14.join(homeDir, ".node9", "trust.json");
4893
- GLOBAL_CONFIG_FILE = path14.join(homeDir, ".node9", "config.json");
4894
- CREDENTIALS_FILE = path14.join(homeDir, ".node9", "credentials.json");
5307
+ init_suggestion_tracker();
5308
+ homeDir = os12.homedir();
5309
+ DAEMON_PID_FILE = path15.join(homeDir, ".node9", "daemon.pid");
5310
+ DECISIONS_FILE = path15.join(homeDir, ".node9", "decisions.json");
5311
+ AUDIT_LOG_FILE = path15.join(homeDir, ".node9", "audit.log");
5312
+ TRUST_FILE2 = path15.join(homeDir, ".node9", "trust.json");
5313
+ GLOBAL_CONFIG_FILE = path15.join(homeDir, ".node9", "config.json");
5314
+ CREDENTIALS_FILE = path15.join(homeDir, ".node9", "credentials.json");
5315
+ INSIGHT_COUNTS_FILE = path15.join(homeDir, ".node9", "insight-counts.json");
4895
5316
  pending = /* @__PURE__ */ new Map();
4896
5317
  sseClients = /* @__PURE__ */ new Set();
5318
+ suggestionTracker = new SuggestionTracker(3);
5319
+ suggestions = /* @__PURE__ */ new Map();
5320
+ insightCounts = /* @__PURE__ */ new Map();
4897
5321
  _abandonTimer = null;
4898
5322
  _hadBrowserClient = false;
4899
5323
  _daemonServer = null;
@@ -4905,23 +5329,81 @@ var init_state2 = __esm({
4905
5329
  "2h": 2 * 60 * 6e4
4906
5330
  };
4907
5331
  autoStarted = process.env.NODE9_AUTO_STARTED === "1";
4908
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path14.join(os11.tmpdir(), "node9-activity.sock");
5332
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path15.join(os12.tmpdir(), "node9-activity.sock");
4909
5333
  ACTIVITY_RING_SIZE = 100;
4910
5334
  activityRing = [];
4911
5335
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
4912
5336
  }
4913
5337
  });
4914
5338
 
5339
+ // src/config/patch.ts
5340
+ import fs13 from "fs";
5341
+ import path16 from "path";
5342
+ import os13 from "os";
5343
+ function patchConfig(configPath, patch) {
5344
+ let config = {};
5345
+ try {
5346
+ if (fs13.existsSync(configPath)) {
5347
+ config = JSON.parse(fs13.readFileSync(configPath, "utf8"));
5348
+ }
5349
+ } catch {
5350
+ throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
5351
+ }
5352
+ if (!config.policy || typeof config.policy !== "object") config.policy = {};
5353
+ const policy = config.policy;
5354
+ if (patch.type === "smartRule") {
5355
+ if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
5356
+ const rules = policy.smartRules;
5357
+ if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
5358
+ rules.push(patch.rule);
5359
+ } else {
5360
+ if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
5361
+ const ignored = policy.ignoredTools;
5362
+ if (!ignored.includes(patch.toolName)) {
5363
+ ignored.push(patch.toolName);
5364
+ }
5365
+ }
5366
+ const dir = path16.dirname(configPath);
5367
+ fs13.mkdirSync(dir, { recursive: true });
5368
+ const tmp = configPath + ".node9-tmp";
5369
+ try {
5370
+ fs13.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5371
+ } catch (err) {
5372
+ try {
5373
+ fs13.unlinkSync(tmp);
5374
+ } catch {
5375
+ }
5376
+ throw err;
5377
+ }
5378
+ try {
5379
+ fs13.renameSync(tmp, configPath);
5380
+ } catch (err) {
5381
+ try {
5382
+ fs13.unlinkSync(tmp);
5383
+ } catch {
5384
+ }
5385
+ throw err;
5386
+ }
5387
+ }
5388
+ var GLOBAL_CONFIG_PATH;
5389
+ var init_patch = __esm({
5390
+ "src/config/patch.ts"() {
5391
+ "use strict";
5392
+ GLOBAL_CONFIG_PATH = path16.join(os13.homedir(), ".node9", "config.json");
5393
+ }
5394
+ });
5395
+
4915
5396
  // src/daemon/server.ts
4916
5397
  import http from "http";
4917
- import fs12 from "fs";
4918
- import path15 from "path";
4919
- import { randomUUID as randomUUID3 } from "crypto";
5398
+ import fs14 from "fs";
5399
+ import path17 from "path";
5400
+ import { randomUUID as randomUUID4 } from "crypto";
4920
5401
  import { spawnSync as spawnSync2 } from "child_process";
4921
5402
  import chalk2 from "chalk";
4922
5403
  function startDaemon() {
4923
- const csrfToken = randomUUID3();
4924
- const internalToken = randomUUID3();
5404
+ loadInsightCounts();
5405
+ const csrfToken = randomUUID4();
5406
+ const internalToken = randomUUID4();
4925
5407
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
4926
5408
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
4927
5409
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
@@ -4934,7 +5416,7 @@ function startDaemon() {
4934
5416
  idleTimer = setTimeout(() => {
4935
5417
  if (autoStarted) {
4936
5418
  try {
4937
- fs12.unlinkSync(DAEMON_PID_FILE);
5419
+ fs14.unlinkSync(DAEMON_PID_FILE);
4938
5420
  } catch {
4939
5421
  }
4940
5422
  }
@@ -4943,8 +5425,14 @@ function startDaemon() {
4943
5425
  idleTimer.unref();
4944
5426
  }
4945
5427
  resetIdleTimer();
5428
+ const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
4946
5429
  const server = http.createServer(async (req, res) => {
4947
- const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
5430
+ const host = req.headers.host ?? "";
5431
+ if (!allowedHosts.has(host)) {
5432
+ res.writeHead(421, { "Content-Type": "text/plain" });
5433
+ return res.end("Misdirected Request");
5434
+ }
5435
+ const reqUrl = new URL(req.url || "/", `http://${host}`);
4948
5436
  const { pathname } = reqUrl;
4949
5437
  if (req.method === "GET" && pathname === "/") {
4950
5438
  res.writeHead(200, { "Content-Type": "text/html" });
@@ -4977,7 +5465,8 @@ data: ${JSON.stringify({
4977
5465
  slackDelegated: e.slackDelegated,
4978
5466
  timestamp: e.timestamp,
4979
5467
  agent: e.agent,
4980
- mcpServer: e.mcpServer
5468
+ mcpServer: e.mcpServer,
5469
+ allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
4981
5470
  })),
4982
5471
  orgName: getOrgName(),
4983
5472
  autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
@@ -5019,6 +5508,12 @@ data: ${JSON.stringify(item.data)}
5019
5508
  }
5020
5509
  });
5021
5510
  }
5511
+ if (req.method === "POST" && pathname === "/browser-opened") {
5512
+ if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
5513
+ browserOpened = true;
5514
+ res.writeHead(200).end();
5515
+ return;
5516
+ }
5022
5517
  if (req.method === "POST" && pathname === "/check") {
5023
5518
  try {
5024
5519
  resetIdleTimer();
@@ -5036,7 +5531,7 @@ data: ${JSON.stringify(item.data)}
5036
5531
  activityId,
5037
5532
  cwd
5038
5533
  } = JSON.parse(body);
5039
- const id = fromCLI && typeof activityId === "string" && activityId || randomUUID3();
5534
+ const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
5040
5535
  const entry = {
5041
5536
  id,
5042
5537
  toolName,
@@ -5062,7 +5557,7 @@ data: ${JSON.stringify(item.data)}
5062
5557
  e.earlyReason = "No response \u2014 auto-denied after timeout";
5063
5558
  }
5064
5559
  pending.delete(id);
5065
- broadcast("remove", { id });
5560
+ broadcast("remove", { id, decision: "deny" });
5066
5561
  }
5067
5562
  }, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
5068
5563
  };
@@ -5076,7 +5571,7 @@ data: ${JSON.stringify(item.data)}
5076
5571
  status: "pending"
5077
5572
  });
5078
5573
  }
5079
- const projectCwd = typeof cwd === "string" && path15.isAbsolute(cwd) ? cwd : void 0;
5574
+ const projectCwd = typeof cwd === "string" && path17.isAbsolute(cwd) ? cwd : void 0;
5080
5575
  const projectConfig = getConfig(projectCwd);
5081
5576
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
5082
5577
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -5089,7 +5584,10 @@ data: ${JSON.stringify(item.data)}
5089
5584
  slackDelegated: entry.slackDelegated,
5090
5585
  agent: entry.agent,
5091
5586
  mcpServer: entry.mcpServer,
5092
- interactive: terminalEnabled
5587
+ interactive: terminalEnabled,
5588
+ // allowCount = what this count will be if the user allows.
5589
+ // Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
5590
+ allowCount: (insightCounts.get(toolName) ?? 0) + 1
5093
5591
  });
5094
5592
  const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
5095
5593
  if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
@@ -5098,7 +5596,7 @@ data: ${JSON.stringify(item.data)}
5098
5596
  }
5099
5597
  }
5100
5598
  res.writeHead(200, { "Content-Type": "application/json" });
5101
- res.end(JSON.stringify({ id }));
5599
+ res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
5102
5600
  if (slackDelegated) return;
5103
5601
  authorizeHeadless(
5104
5602
  toolName,
@@ -5125,7 +5623,7 @@ data: ${JSON.stringify(item.data)}
5125
5623
  if (e.waiter) {
5126
5624
  e.waiter(decision, result.reason);
5127
5625
  pending.delete(id);
5128
- broadcast("remove", { id });
5626
+ broadcast("remove", { id, decision });
5129
5627
  } else {
5130
5628
  e.earlyDecision = decision;
5131
5629
  e.earlyReason = result.reason;
@@ -5141,7 +5639,7 @@ data: ${JSON.stringify(item.data)}
5141
5639
  e.earlyReason = reason;
5142
5640
  }
5143
5641
  pending.delete(id);
5144
- broadcast("remove", { id });
5642
+ broadcast("remove", { id, decision: "deny" });
5145
5643
  });
5146
5644
  return;
5147
5645
  } catch {
@@ -5172,12 +5670,14 @@ data: ${JSON.stringify(item.data)}
5172
5670
  res.end(JSON.stringify(body));
5173
5671
  };
5174
5672
  req.on("close", () => {
5175
- const e = pending.get(id);
5176
- if (e && e.waiter && e.earlyDecision === null) {
5177
- clearTimeout(e.timer);
5178
- pending.delete(id);
5179
- broadcast("remove", { id });
5180
- }
5673
+ setTimeout(() => {
5674
+ const e = pending.get(id);
5675
+ if (e && e.waiter && e.earlyDecision === null) {
5676
+ clearTimeout(e.timer);
5677
+ pending.delete(id);
5678
+ broadcast("remove", { id });
5679
+ }
5680
+ }, 200);
5181
5681
  });
5182
5682
  return;
5183
5683
  }
@@ -5206,10 +5706,10 @@ data: ${JSON.stringify(item.data)}
5206
5706
  if (entry.waiter) {
5207
5707
  entry.waiter("allow");
5208
5708
  pending.delete(id);
5209
- broadcast("remove", { id });
5709
+ broadcast("remove", { id, decision: "allow" });
5210
5710
  } else {
5211
5711
  entry.earlyDecision = "allow";
5212
- broadcast("remove", { id });
5712
+ broadcast("remove", { id, decision: "allow" });
5213
5713
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5214
5714
  }
5215
5715
  res.writeHead(200);
@@ -5223,16 +5723,29 @@ data: ${JSON.stringify(item.data)}
5223
5723
  decision: resolvedDecision
5224
5724
  });
5225
5725
  clearTimeout(entry.timer);
5726
+ if (resolvedDecision === "allow" && !persist) {
5727
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
5728
+ saveInsightCounts();
5729
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
5730
+ if (suggestion) {
5731
+ suggestions.set(suggestion.id, suggestion);
5732
+ broadcast("suggestion:new", suggestion);
5733
+ }
5734
+ } else if (resolvedDecision === "deny") {
5735
+ insightCounts.delete(entry.toolName);
5736
+ saveInsightCounts();
5737
+ suggestionTracker.resetTool(entry.toolName);
5738
+ }
5226
5739
  const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
5227
5740
  if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
5228
5741
  if (entry.waiter) {
5229
5742
  entry.waiter(resolvedDecision, reason);
5230
5743
  pending.delete(id);
5231
- broadcast("remove", { id });
5744
+ broadcast("remove", { id, decision: resolvedDecision });
5232
5745
  } else {
5233
5746
  entry.earlyDecision = resolvedDecision;
5234
5747
  entry.earlyReason = reason;
5235
- broadcast("remove", { id });
5748
+ broadcast("remove", { id, decision: resolvedDecision });
5236
5749
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5237
5750
  }
5238
5751
  res.writeHead(200);
@@ -5320,13 +5833,38 @@ data: ${JSON.stringify(item.data)}
5320
5833
  const id = pathname.split("/").pop();
5321
5834
  const entry = pending.get(id);
5322
5835
  if (!entry) return res.writeHead(404).end();
5323
- const { decision } = JSON.parse(await readBody(req));
5324
- appendAuditLog({ toolName: entry.toolName, args: entry.args, decision });
5836
+ const { decision, source } = JSON.parse(await readBody(req));
5837
+ const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
5838
+ appendAuditLog({
5839
+ toolName: entry.toolName,
5840
+ args: entry.args,
5841
+ decision: resolvedResolveDecision
5842
+ });
5325
5843
  clearTimeout(entry.timer);
5326
- if (entry.waiter) entry.waiter(decision);
5327
- else entry.earlyDecision = decision;
5844
+ if (resolvedResolveDecision === "allow") {
5845
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
5846
+ saveInsightCounts();
5847
+ } else {
5848
+ insightCounts.delete(entry.toolName);
5849
+ saveInsightCounts();
5850
+ }
5851
+ if (!entry.slackDelegated) {
5852
+ if (resolvedResolveDecision === "allow") {
5853
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
5854
+ if (suggestion) {
5855
+ suggestions.set(suggestion.id, suggestion);
5856
+ broadcast("suggestion:new", suggestion);
5857
+ }
5858
+ } else {
5859
+ suggestionTracker.resetTool(entry.toolName);
5860
+ }
5861
+ }
5862
+ const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
5863
+ if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
5864
+ if (entry.waiter) entry.waiter(resolvedResolveDecision);
5865
+ else entry.earlyDecision = resolvedResolveDecision;
5328
5866
  pending.delete(id);
5329
- broadcast("remove", { id });
5867
+ broadcast("remove", { id, decision: resolvedResolveDecision });
5330
5868
  res.writeHead(200);
5331
5869
  return res.end(JSON.stringify({ ok: true }));
5332
5870
  } catch {
@@ -5374,20 +5912,79 @@ data: ${JSON.stringify(item.data)}
5374
5912
  res.writeHead(400).end();
5375
5913
  }
5376
5914
  }
5915
+ if (req.method === "GET" && pathname === "/suggestions") {
5916
+ res.writeHead(200, { "Content-Type": "application/json" });
5917
+ return res.end(JSON.stringify([...suggestions.values()]));
5918
+ }
5919
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
5920
+ if (!validToken(req)) return res.writeHead(403).end();
5921
+ try {
5922
+ const body = await readBody(req);
5923
+ const data = body ? JSON.parse(body) : {};
5924
+ const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
5925
+ const node9Dir = path17.dirname(GLOBAL_CONFIG_PATH);
5926
+ if (!path17.resolve(configPath).startsWith(node9Dir + path17.sep)) {
5927
+ res.writeHead(400, { "Content-Type": "application/json" });
5928
+ return res.end(
5929
+ JSON.stringify({ error: "configPath must be within the node9 config directory" })
5930
+ );
5931
+ }
5932
+ const id = pathname.split("/")[2];
5933
+ const suggestion = suggestions.get(id);
5934
+ if (!suggestion) return res.writeHead(404).end();
5935
+ let patch;
5936
+ if (data.rule !== void 0) {
5937
+ const parsed = SmartRuleSchema.safeParse(data.rule);
5938
+ if (!parsed.success) {
5939
+ res.writeHead(400, { "Content-Type": "application/json" });
5940
+ return res.end(JSON.stringify({ error: parsed.error.message }));
5941
+ }
5942
+ patch = { type: "smartRule", rule: parsed.data };
5943
+ } else {
5944
+ patch = suggestion.suggestedRule;
5945
+ }
5946
+ patchConfig(configPath, patch);
5947
+ _resetConfigCache();
5948
+ insightCounts.delete(suggestion.toolName);
5949
+ saveInsightCounts();
5950
+ suggestion.status = "applied";
5951
+ broadcast("suggestion:resolved", { id, status: "applied" });
5952
+ res.writeHead(200, { "Content-Type": "application/json" });
5953
+ return res.end(JSON.stringify({ ok: true }));
5954
+ } catch (err) {
5955
+ console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
5956
+ res.writeHead(500, { "Content-Type": "application/json" });
5957
+ return res.end(JSON.stringify({ error: String(err) }));
5958
+ }
5959
+ }
5960
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
5961
+ if (!validToken(req)) return res.writeHead(403).end();
5962
+ try {
5963
+ const id = pathname.split("/")[2];
5964
+ const suggestion = suggestions.get(id);
5965
+ if (!suggestion) return res.writeHead(404).end();
5966
+ suggestion.status = "dismissed";
5967
+ broadcast("suggestion:resolved", { id, status: "dismissed" });
5968
+ res.writeHead(200, { "Content-Type": "application/json" });
5969
+ return res.end(JSON.stringify({ ok: true }));
5970
+ } catch {
5971
+ res.writeHead(400).end();
5972
+ }
5973
+ }
5377
5974
  res.writeHead(404).end();
5378
5975
  });
5379
5976
  setDaemonServer(server);
5380
5977
  server.on("error", (e) => {
5381
5978
  if (e.code === "EADDRINUSE") {
5382
5979
  try {
5383
- if (fs12.existsSync(DAEMON_PID_FILE)) {
5384
- const { pid } = JSON.parse(fs12.readFileSync(DAEMON_PID_FILE, "utf-8"));
5980
+ if (fs14.existsSync(DAEMON_PID_FILE)) {
5981
+ const { pid } = JSON.parse(fs14.readFileSync(DAEMON_PID_FILE, "utf-8"));
5385
5982
  process.kill(pid, 0);
5386
5983
  return process.exit(0);
5387
5984
  }
5388
5985
  } catch {
5389
5986
  try {
5390
- fs12.unlinkSync(DAEMON_PID_FILE);
5987
+ fs14.unlinkSync(DAEMON_PID_FILE);
5391
5988
  } catch {
5392
5989
  }
5393
5990
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -5453,32 +6050,34 @@ var init_server = __esm({
5453
6050
  init_shields();
5454
6051
  init_ui2();
5455
6052
  init_state2();
6053
+ init_patch();
6054
+ init_config_schema();
5456
6055
  }
5457
6056
  });
5458
6057
 
5459
6058
  // src/daemon/index.ts
5460
- import fs13 from "fs";
6059
+ import fs15 from "fs";
5461
6060
  import chalk3 from "chalk";
5462
6061
  import { spawnSync as spawnSync3 } from "child_process";
5463
6062
  function stopDaemon() {
5464
- if (!fs13.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
6063
+ if (!fs15.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
5465
6064
  try {
5466
- const { pid } = JSON.parse(fs13.readFileSync(DAEMON_PID_FILE, "utf-8"));
6065
+ const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
5467
6066
  process.kill(pid, "SIGTERM");
5468
6067
  console.log(chalk3.green("\u2705 Stopped."));
5469
6068
  } catch {
5470
6069
  console.log(chalk3.gray("Cleaned up stale PID file."));
5471
6070
  } finally {
5472
6071
  try {
5473
- fs13.unlinkSync(DAEMON_PID_FILE);
6072
+ fs15.unlinkSync(DAEMON_PID_FILE);
5474
6073
  } catch {
5475
6074
  }
5476
6075
  }
5477
6076
  }
5478
6077
  function daemonStatus() {
5479
- if (fs13.existsSync(DAEMON_PID_FILE)) {
6078
+ if (fs15.existsSync(DAEMON_PID_FILE)) {
5480
6079
  try {
5481
- const { pid } = JSON.parse(fs13.readFileSync(DAEMON_PID_FILE, "utf-8"));
6080
+ const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
5482
6081
  process.kill(pid, 0);
5483
6082
  console.log(chalk3.green("Node9 daemon: running"));
5484
6083
  return;
@@ -5512,10 +6111,10 @@ __export(tail_exports, {
5512
6111
  startTail: () => startTail
5513
6112
  });
5514
6113
  import http2 from "http";
5515
- import chalk14 from "chalk";
5516
- import fs20 from "fs";
5517
- import os18 from "os";
5518
- import path22 from "path";
6114
+ import chalk16 from "chalk";
6115
+ import fs23 from "fs";
6116
+ import os21 from "os";
6117
+ import path25 from "path";
5519
6118
  import readline3 from "readline";
5520
6119
  import { spawn as spawn9, execSync as execSync3 } from "child_process";
5521
6120
  function getIcon(tool) {
@@ -5531,17 +6130,17 @@ function formatBase(activity) {
5531
6130
  const toolName = activity.tool.slice(0, 16).padEnd(16);
5532
6131
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
5533
6132
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
5534
- return `${chalk14.gray(time)} ${icon} ${chalk14.white.bold(toolName)} ${chalk14.dim(argsPreview)}`;
6133
+ return `${chalk16.gray(time)} ${icon} ${chalk16.white.bold(toolName)} ${chalk16.dim(argsPreview)}`;
5535
6134
  }
5536
6135
  function renderResult(activity, result) {
5537
6136
  const base = formatBase(activity);
5538
6137
  let status;
5539
6138
  if (result.status === "allow") {
5540
- status = chalk14.green("\u2713 ALLOW");
6139
+ status = chalk16.green("\u2713 ALLOW");
5541
6140
  } else if (result.status === "dlp") {
5542
- status = chalk14.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6141
+ status = chalk16.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
5543
6142
  } else {
5544
- status = chalk14.red("\u2717 BLOCK");
6143
+ status = chalk16.red("\u2717 BLOCK");
5545
6144
  }
5546
6145
  if (process.stdout.isTTY) {
5547
6146
  readline3.clearLine(process.stdout, 0);
@@ -5551,16 +6150,16 @@ function renderResult(activity, result) {
5551
6150
  }
5552
6151
  function renderPending(activity) {
5553
6152
  if (!process.stdout.isTTY) return;
5554
- process.stdout.write(`${formatBase(activity)} ${chalk14.yellow("\u25CF \u2026")}\r`);
6153
+ process.stdout.write(`${formatBase(activity)} ${chalk16.yellow("\u25CF \u2026")}\r`);
5555
6154
  }
5556
6155
  async function ensureDaemon() {
5557
6156
  let pidPort = null;
5558
- if (fs20.existsSync(PID_FILE)) {
6157
+ if (fs23.existsSync(PID_FILE)) {
5559
6158
  try {
5560
- const { port } = JSON.parse(fs20.readFileSync(PID_FILE, "utf-8"));
6159
+ const { port } = JSON.parse(fs23.readFileSync(PID_FILE, "utf-8"));
5561
6160
  pidPort = port;
5562
6161
  } catch {
5563
- console.error(chalk14.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6162
+ console.error(chalk16.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
5564
6163
  }
5565
6164
  }
5566
6165
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -5571,7 +6170,7 @@ async function ensureDaemon() {
5571
6170
  if (res.ok) return checkPort;
5572
6171
  } catch {
5573
6172
  }
5574
- console.log(chalk14.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6173
+ console.log(chalk16.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
5575
6174
  const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
5576
6175
  detached: true,
5577
6176
  stdio: "ignore",
@@ -5588,12 +6187,15 @@ async function ensureDaemon() {
5588
6187
  } catch {
5589
6188
  }
5590
6189
  }
5591
- console.error(chalk14.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6190
+ console.error(chalk16.red("\u274C Daemon failed to start. Try: node9 daemon start"));
5592
6191
  process.exit(1);
5593
6192
  }
5594
- function postDecisionHttp(id, decision, csrfToken, port) {
6193
+ function postDecisionHttp(id, decision, csrfToken, port, opts) {
5595
6194
  return new Promise((resolve, reject) => {
5596
- const body = JSON.stringify({ decision, source: "terminal" });
6195
+ const bodyObj = { decision, source: "terminal" };
6196
+ if (opts?.persist) bodyObj.persist = true;
6197
+ if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
6198
+ const body = JSON.stringify(bodyObj);
5597
6199
  const req = http2.request(
5598
6200
  {
5599
6201
  hostname: "127.0.0.1",
@@ -5616,22 +6218,30 @@ function postDecisionHttp(id, decision, csrfToken, port) {
5616
6218
  req.end(body);
5617
6219
  });
5618
6220
  }
5619
- function buildCardLines(req) {
6221
+ function buildCardLines(req, localCount = 0) {
5620
6222
  const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
5621
6223
  const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
5622
6224
  const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
5623
6225
  const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
5624
- return [
6226
+ const lines = [
5625
6227
  ``,
5626
6228
  `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
5627
6229
  `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
5628
6230
  `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
5629
- `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`,
6231
+ `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`
6232
+ ];
6233
+ if (localCount >= 2) {
6234
+ lines.push(
6235
+ `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
6236
+ );
6237
+ }
6238
+ lines.push(
5630
6239
  `${CYAN}\u255A${RESET}`,
5631
6240
  ``,
5632
- ` ${BOLD}${GREEN}[A]${RESET} Allow ${BOLD}${RED}[D]${RESET} Deny`,
6241
+ ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
5633
6242
  ``
5634
- ];
6243
+ );
6244
+ return lines;
5635
6245
  }
5636
6246
  async function startTail(options = {}) {
5637
6247
  const port = await ensureDaemon();
@@ -5659,7 +6269,7 @@ async function startTail(options = {}) {
5659
6269
  req2.end();
5660
6270
  });
5661
6271
  if (result.ok) {
5662
- console.log(chalk14.green("\u2713 Flight Recorder buffer cleared."));
6272
+ console.log(chalk16.green("\u2713 Flight Recorder buffer cleared."));
5663
6273
  } else if (result.code === "ECONNREFUSED") {
5664
6274
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
5665
6275
  } else if (result.code === "ETIMEDOUT") {
@@ -5676,6 +6286,7 @@ async function startTail(options = {}) {
5676
6286
  let cardActive = false;
5677
6287
  let cardLineCount = 0;
5678
6288
  let cancelActiveCard = null;
6289
+ const localAllowCounts = /* @__PURE__ */ new Map();
5679
6290
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
5680
6291
  if (canApprove) readline3.emitKeypressEvents(process.stdin);
5681
6292
  function clearCard() {
@@ -5686,7 +6297,10 @@ async function startTail(options = {}) {
5686
6297
  }
5687
6298
  function printCard(req2) {
5688
6299
  process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
5689
- const lines = buildCardLines(req2);
6300
+ const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
6301
+ const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
6302
+ const priorCount = Math.max(daemonPrior, localPrior);
6303
+ const lines = buildCardLines(req2, priorCount);
5690
6304
  for (const line of lines) process.stdout.write(line + "\n");
5691
6305
  cardLineCount = lines.length;
5692
6306
  }
@@ -5714,34 +6328,70 @@ async function startTail(options = {}) {
5714
6328
  process.stdin.pause();
5715
6329
  cancelActiveCard = null;
5716
6330
  };
5717
- const settle = (decision) => {
6331
+ const settle = (action) => {
5718
6332
  if (settled) return;
5719
6333
  settled = true;
5720
6334
  cleanup();
5721
- clearCard();
6335
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6336
+ const stampedLines = buildCardLines(
6337
+ req2,
6338
+ Math.max(
6339
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6340
+ localAllowCounts.get(req2.toolName) ?? 0
6341
+ )
6342
+ );
6343
+ const decisionStamp = action === "always-allow" ? chalk16.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk16.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
6344
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
6345
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5722
6346
  process.stdout.write(SHOW_CURSOR);
5723
- postDecisionHttp(req2.id, decision, csrfToken, port).catch((err) => {
6347
+ cardLineCount = 0;
6348
+ if (action === "allow" || action === "always-allow" || action === "trust") {
6349
+ localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
6350
+ } else if (action === "deny") {
6351
+ localAllowCounts.delete(req2.toolName);
6352
+ }
6353
+ let httpDecision;
6354
+ let httpOpts;
6355
+ if (action === "always-allow") {
6356
+ httpDecision = "allow";
6357
+ httpOpts = { persist: true };
6358
+ } else if (action === "trust") {
6359
+ httpDecision = "trust";
6360
+ httpOpts = { trustDuration: "30m" };
6361
+ } else {
6362
+ httpDecision = action;
6363
+ }
6364
+ postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
5724
6365
  try {
5725
- fs20.appendFileSync(
5726
- path22.join(os18.homedir(), ".node9", "hook-debug.log"),
6366
+ fs23.appendFileSync(
6367
+ path25.join(os21.homedir(), ".node9", "hook-debug.log"),
5727
6368
  `[tail] POST /decision failed: ${String(err)}
5728
6369
  `
5729
6370
  );
5730
6371
  } catch {
5731
6372
  }
5732
6373
  });
5733
- const decisionLabel = decision === "allow" ? chalk14.green("\u2713 ALLOWED (terminal)") : chalk14.red("\u2717 DENIED (terminal)");
5734
- console.log(`${chalk14.cyan("\u25C6")} ${chalk14.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
5735
6374
  approvalQueue.shift();
5736
6375
  cardActive = false;
5737
6376
  showNextCard();
5738
6377
  };
5739
- cancelActiveCard = () => {
6378
+ cancelActiveCard = (externalDecision) => {
5740
6379
  if (settled) return;
5741
6380
  settled = true;
5742
6381
  cleanup();
5743
- clearCard();
6382
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6383
+ const priorCount = Math.max(
6384
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6385
+ localAllowCounts.get(req2.toolName) ?? 0
6386
+ );
6387
+ const stampedLines = buildCardLines(req2, priorCount);
6388
+ if (externalDecision) {
6389
+ const source = externalDecision === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
6390
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
6391
+ }
6392
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5744
6393
  process.stdout.write(SHOW_CURSOR);
6394
+ cardLineCount = 0;
5745
6395
  approvalQueue.shift();
5746
6396
  cardActive = false;
5747
6397
  showNextCard();
@@ -5749,10 +6399,14 @@ async function startTail(options = {}) {
5749
6399
  process.stdin.resume();
5750
6400
  onKeypress = (_str, key) => {
5751
6401
  const name = key?.name ?? "";
5752
- if (name === "a") {
6402
+ if (name === "y" || name === "return") {
5753
6403
  settle("allow");
5754
- } else if (name === "d" || name === "return" || name === "enter" || key?.ctrl && name === "c") {
6404
+ } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
5755
6405
  settle("deny");
6406
+ } else if (name === "a") {
6407
+ settle("always-allow");
6408
+ } else if (name === "t") {
6409
+ settle("trust");
5756
6410
  }
5757
6411
  };
5758
6412
  process.stdin.on("keypress", onKeypress);
@@ -5765,19 +6419,27 @@ async function startTail(options = {}) {
5765
6419
  else if (process.platform === "win32")
5766
6420
  execSync3(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
5767
6421
  else execSync3(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
6422
+ const intToken = getInternalToken();
6423
+ fetch(`http://127.0.0.1:${port}/browser-opened`, {
6424
+ method: "POST",
6425
+ headers: intToken ? { "X-Node9-Internal": intToken } : {}
6426
+ }).catch(() => {
6427
+ });
5768
6428
  }
5769
6429
  } catch {
5770
6430
  }
5771
- console.log(chalk14.cyan.bold(`
5772
- \u{1F6F0}\uFE0F Node9 tail `) + chalk14.dim(`\u2192 ${dashboardUrl}`));
6431
+ console.log(chalk16.cyan.bold(`
6432
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk16.dim(`\u2192 ${dashboardUrl}`));
5773
6433
  if (canApprove) {
5774
- console.log(chalk14.dim("Interactive approvals enabled. [A] Allow [D] Deny"));
6434
+ console.log(
6435
+ chalk16.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
6436
+ );
5775
6437
  }
5776
6438
  if (options.history) {
5777
- console.log(chalk14.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
6439
+ console.log(chalk16.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
5778
6440
  } else {
5779
6441
  console.log(
5780
- chalk14.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
6442
+ chalk16.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
5781
6443
  );
5782
6444
  }
5783
6445
  process.on("SIGINT", () => {
@@ -5787,13 +6449,13 @@ async function startTail(options = {}) {
5787
6449
  readline3.clearLine(process.stdout, 0);
5788
6450
  readline3.cursorTo(process.stdout, 0);
5789
6451
  }
5790
- console.log(chalk14.dim("\n\u{1F6F0}\uFE0F Disconnected."));
6452
+ console.log(chalk16.dim("\n\u{1F6F0}\uFE0F Disconnected."));
5791
6453
  process.exit(0);
5792
6454
  });
5793
6455
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
5794
6456
  const req = http2.get(sseUrl, (res) => {
5795
6457
  if (res.statusCode !== 200) {
5796
- console.error(chalk14.red(`Failed to connect: HTTP ${res.statusCode}`));
6458
+ console.error(chalk16.red(`Failed to connect: HTTP ${res.statusCode}`));
5797
6459
  process.exit(1);
5798
6460
  }
5799
6461
  let currentEvent = "";
@@ -5823,7 +6485,7 @@ async function startTail(options = {}) {
5823
6485
  readline3.clearLine(process.stdout, 0);
5824
6486
  readline3.cursorTo(process.stdout, 0);
5825
6487
  }
5826
- console.log(chalk14.red("\n\u274C Daemon disconnected."));
6488
+ console.log(chalk16.red("\n\u274C Daemon disconnected."));
5827
6489
  process.exit(1);
5828
6490
  });
5829
6491
  });
@@ -5864,11 +6526,17 @@ async function startTail(options = {}) {
5864
6526
  }
5865
6527
  if (event === "remove") {
5866
6528
  try {
5867
- const { id } = JSON.parse(rawData);
6529
+ const { id, decision } = JSON.parse(rawData);
5868
6530
  const idx = approvalQueue.findIndex((r) => r.id === id);
5869
6531
  if (idx !== -1) {
5870
6532
  if (idx === 0 && cardActive && cancelActiveCard) {
5871
- cancelActiveCard();
6533
+ const toolName = approvalQueue[0].toolName;
6534
+ if (decision === "allow") {
6535
+ localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
6536
+ } else if (decision === "deny") {
6537
+ localAllowCounts.delete(toolName);
6538
+ }
6539
+ cancelActiveCard(decision);
5872
6540
  } else {
5873
6541
  approvalQueue.splice(idx, 1);
5874
6542
  }
@@ -5903,7 +6571,7 @@ async function startTail(options = {}) {
5903
6571
  }
5904
6572
  req.on("error", (err) => {
5905
6573
  const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
5906
- console.error(chalk14.red(`
6574
+ console.error(chalk16.red(`
5907
6575
  \u274C ${msg}`));
5908
6576
  process.exit(1);
5909
6577
  });
@@ -5913,8 +6581,9 @@ var init_tail = __esm({
5913
6581
  "src/tui/tail.ts"() {
5914
6582
  "use strict";
5915
6583
  init_daemon2();
6584
+ init_daemon();
5916
6585
  init_core();
5917
- PID_FILE = path22.join(os18.homedir(), ".node9", "daemon.pid");
6586
+ PID_FILE = path25.join(os21.homedir(), ".node9", "daemon.pid");
5918
6587
  ICONS = {
5919
6588
  bash: "\u{1F4BB}",
5920
6589
  shell: "\u{1F4BB}",
@@ -5952,9 +6621,9 @@ init_core();
5952
6621
  import { Command } from "commander";
5953
6622
 
5954
6623
  // src/setup.ts
5955
- import fs10 from "fs";
5956
- import path13 from "path";
5957
- import os10 from "os";
6624
+ import fs11 from "fs";
6625
+ import path14 from "path";
6626
+ import os11 from "os";
5958
6627
  import chalk from "chalk";
5959
6628
  import { confirm } from "@inquirer/prompts";
5960
6629
  function printDaemonTip() {
@@ -5971,26 +6640,26 @@ function fullPathCommand(subcommand) {
5971
6640
  }
5972
6641
  function readJson(filePath) {
5973
6642
  try {
5974
- if (fs10.existsSync(filePath)) {
5975
- return JSON.parse(fs10.readFileSync(filePath, "utf-8"));
6643
+ if (fs11.existsSync(filePath)) {
6644
+ return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
5976
6645
  }
5977
6646
  } catch {
5978
6647
  }
5979
6648
  return null;
5980
6649
  }
5981
6650
  function writeJson(filePath, data) {
5982
- const dir = path13.dirname(filePath);
5983
- if (!fs10.existsSync(dir)) fs10.mkdirSync(dir, { recursive: true });
5984
- fs10.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
6651
+ const dir = path14.dirname(filePath);
6652
+ if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
6653
+ fs11.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
5985
6654
  }
5986
6655
  function isNode9Hook(cmd) {
5987
6656
  if (!cmd) return false;
5988
6657
  return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
5989
6658
  }
5990
6659
  function teardownClaude() {
5991
- const homeDir2 = os10.homedir();
5992
- const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
5993
- const mcpPath = path13.join(homeDir2, ".claude.json");
6660
+ const homeDir2 = os11.homedir();
6661
+ const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
6662
+ const mcpPath = path14.join(homeDir2, ".claude.json");
5994
6663
  let changed = false;
5995
6664
  const settings = readJson(hooksPath);
5996
6665
  if (settings?.hooks) {
@@ -6038,8 +6707,8 @@ function teardownClaude() {
6038
6707
  }
6039
6708
  }
6040
6709
  function teardownGemini() {
6041
- const homeDir2 = os10.homedir();
6042
- const settingsPath = path13.join(homeDir2, ".gemini", "settings.json");
6710
+ const homeDir2 = os11.homedir();
6711
+ const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
6043
6712
  const settings = readJson(settingsPath);
6044
6713
  if (!settings) {
6045
6714
  console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
@@ -6077,8 +6746,8 @@ function teardownGemini() {
6077
6746
  }
6078
6747
  }
6079
6748
  function teardownCursor() {
6080
- const homeDir2 = os10.homedir();
6081
- const mcpPath = path13.join(homeDir2, ".cursor", "mcp.json");
6749
+ const homeDir2 = os11.homedir();
6750
+ const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
6082
6751
  const mcpConfig = readJson(mcpPath);
6083
6752
  if (!mcpConfig?.mcpServers) {
6084
6753
  console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
@@ -6104,9 +6773,9 @@ function teardownCursor() {
6104
6773
  }
6105
6774
  }
6106
6775
  async function setupClaude() {
6107
- const homeDir2 = os10.homedir();
6108
- const mcpPath = path13.join(homeDir2, ".claude.json");
6109
- const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
6776
+ const homeDir2 = os11.homedir();
6777
+ const mcpPath = path14.join(homeDir2, ".claude.json");
6778
+ const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
6110
6779
  const claudeConfig = readJson(mcpPath) ?? {};
6111
6780
  const settings = readJson(hooksPath) ?? {};
6112
6781
  const servers = claudeConfig.mcpServers ?? {};
@@ -6180,8 +6849,8 @@ async function setupClaude() {
6180
6849
  }
6181
6850
  }
6182
6851
  async function setupGemini() {
6183
- const homeDir2 = os10.homedir();
6184
- const settingsPath = path13.join(homeDir2, ".gemini", "settings.json");
6852
+ const homeDir2 = os11.homedir();
6853
+ const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
6185
6854
  const settings = readJson(settingsPath) ?? {};
6186
6855
  const servers = settings.mcpServers ?? {};
6187
6856
  let anythingChanged = false;
@@ -6262,9 +6931,28 @@ async function setupGemini() {
6262
6931
  printDaemonTip();
6263
6932
  }
6264
6933
  }
6934
+ function detectAgents(homeDir2 = os11.homedir()) {
6935
+ const exists = (p) => {
6936
+ try {
6937
+ return fs11.existsSync(p);
6938
+ } catch (err) {
6939
+ const code = err.code;
6940
+ if (code !== "ENOENT") {
6941
+ process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
6942
+ `);
6943
+ }
6944
+ return false;
6945
+ }
6946
+ };
6947
+ return {
6948
+ claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
6949
+ gemini: exists(path14.join(homeDir2, ".gemini")),
6950
+ cursor: exists(path14.join(homeDir2, ".cursor"))
6951
+ };
6952
+ }
6265
6953
  async function setupCursor() {
6266
- const homeDir2 = os10.homedir();
6267
- const mcpPath = path13.join(homeDir2, ".cursor", "mcp.json");
6954
+ const homeDir2 = os11.homedir();
6955
+ const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
6268
6956
  const mcpConfig = readJson(mcpPath) ?? {};
6269
6957
  const servers = mcpConfig.mcpServers ?? {};
6270
6958
  let anythingChanged = false;
@@ -6320,10 +7008,10 @@ async function setupCursor() {
6320
7008
 
6321
7009
  // src/cli.ts
6322
7010
  init_daemon2();
6323
- import chalk15 from "chalk";
6324
- import fs21 from "fs";
6325
- import path23 from "path";
6326
- import os19 from "os";
7011
+ import chalk17 from "chalk";
7012
+ import fs24 from "fs";
7013
+ import path26 from "path";
7014
+ import os22 from "os";
6327
7015
  import { confirm as confirm3 } from "@inquirer/prompts";
6328
7016
 
6329
7017
  // src/utils/duration.ts
@@ -6548,32 +7236,32 @@ init_daemon();
6548
7236
  init_config();
6549
7237
  init_policy();
6550
7238
  import chalk5 from "chalk";
6551
- import fs15 from "fs";
6552
- import path17 from "path";
6553
- import os13 from "os";
7239
+ import fs17 from "fs";
7240
+ import path19 from "path";
7241
+ import os15 from "os";
6554
7242
 
6555
7243
  // src/undo.ts
6556
7244
  import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
6557
7245
  import crypto2 from "crypto";
6558
- import fs14 from "fs";
6559
- import path16 from "path";
6560
- import os12 from "os";
6561
- var SNAPSHOT_STACK_PATH = path16.join(os12.homedir(), ".node9", "snapshots.json");
6562
- var UNDO_LATEST_PATH = path16.join(os12.homedir(), ".node9", "undo_latest.txt");
7246
+ import fs16 from "fs";
7247
+ import path18 from "path";
7248
+ import os14 from "os";
7249
+ var SNAPSHOT_STACK_PATH = path18.join(os14.homedir(), ".node9", "snapshots.json");
7250
+ var UNDO_LATEST_PATH = path18.join(os14.homedir(), ".node9", "undo_latest.txt");
6563
7251
  var MAX_SNAPSHOTS = 10;
6564
7252
  var GIT_TIMEOUT = 15e3;
6565
7253
  function readStack() {
6566
7254
  try {
6567
- if (fs14.existsSync(SNAPSHOT_STACK_PATH))
6568
- return JSON.parse(fs14.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
7255
+ if (fs16.existsSync(SNAPSHOT_STACK_PATH))
7256
+ return JSON.parse(fs16.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
6569
7257
  } catch {
6570
7258
  }
6571
7259
  return [];
6572
7260
  }
6573
7261
  function writeStack(stack) {
6574
- const dir = path16.dirname(SNAPSHOT_STACK_PATH);
6575
- if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
6576
- fs14.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7262
+ const dir = path18.dirname(SNAPSHOT_STACK_PATH);
7263
+ if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
7264
+ fs16.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
6577
7265
  }
6578
7266
  function buildArgsSummary(tool, args) {
6579
7267
  if (!args || typeof args !== "object") return "";
@@ -6589,7 +7277,7 @@ function buildArgsSummary(tool, args) {
6589
7277
  function normalizeCwdForHash(cwd) {
6590
7278
  let normalized;
6591
7279
  try {
6592
- normalized = fs14.realpathSync(cwd);
7280
+ normalized = fs16.realpathSync(cwd);
6593
7281
  } catch {
6594
7282
  normalized = cwd;
6595
7283
  }
@@ -6599,16 +7287,16 @@ function normalizeCwdForHash(cwd) {
6599
7287
  }
6600
7288
  function getShadowRepoDir(cwd) {
6601
7289
  const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
6602
- return path16.join(os12.homedir(), ".node9", "snapshots", hash);
7290
+ return path18.join(os14.homedir(), ".node9", "snapshots", hash);
6603
7291
  }
6604
7292
  function cleanOrphanedIndexFiles(shadowDir) {
6605
7293
  try {
6606
7294
  const cutoff = Date.now() - 6e4;
6607
- for (const f of fs14.readdirSync(shadowDir)) {
7295
+ for (const f of fs16.readdirSync(shadowDir)) {
6608
7296
  if (f.startsWith("index_")) {
6609
- const fp = path16.join(shadowDir, f);
7297
+ const fp = path18.join(shadowDir, f);
6610
7298
  try {
6611
- if (fs14.statSync(fp).mtimeMs < cutoff) fs14.unlinkSync(fp);
7299
+ if (fs16.statSync(fp).mtimeMs < cutoff) fs16.unlinkSync(fp);
6612
7300
  } catch {
6613
7301
  }
6614
7302
  }
@@ -6620,7 +7308,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
6620
7308
  const hardcoded = [".git", ".node9"];
6621
7309
  const lines = [...hardcoded, ...ignorePaths].join("\n");
6622
7310
  try {
6623
- fs14.writeFileSync(path16.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
7311
+ fs16.writeFileSync(path18.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
6624
7312
  } catch {
6625
7313
  }
6626
7314
  }
@@ -6633,25 +7321,25 @@ function ensureShadowRepo(shadowDir, cwd) {
6633
7321
  timeout: 3e3
6634
7322
  });
6635
7323
  if (check.status === 0) {
6636
- const ptPath = path16.join(shadowDir, "project-path.txt");
7324
+ const ptPath = path18.join(shadowDir, "project-path.txt");
6637
7325
  try {
6638
- const stored = fs14.readFileSync(ptPath, "utf8").trim();
7326
+ const stored = fs16.readFileSync(ptPath, "utf8").trim();
6639
7327
  if (stored === normalizedCwd) return true;
6640
7328
  if (process.env.NODE9_DEBUG === "1")
6641
7329
  console.error(
6642
7330
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
6643
7331
  );
6644
- fs14.rmSync(shadowDir, { recursive: true, force: true });
7332
+ fs16.rmSync(shadowDir, { recursive: true, force: true });
6645
7333
  } catch {
6646
7334
  try {
6647
- fs14.writeFileSync(ptPath, normalizedCwd, "utf8");
7335
+ fs16.writeFileSync(ptPath, normalizedCwd, "utf8");
6648
7336
  } catch {
6649
7337
  }
6650
7338
  return true;
6651
7339
  }
6652
7340
  }
6653
7341
  try {
6654
- fs14.mkdirSync(shadowDir, { recursive: true });
7342
+ fs16.mkdirSync(shadowDir, { recursive: true });
6655
7343
  } catch {
6656
7344
  }
6657
7345
  const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
@@ -6660,7 +7348,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6660
7348
  console.error("[Node9] git init --bare failed:", init.stderr?.toString());
6661
7349
  return false;
6662
7350
  }
6663
- const configFile = path16.join(shadowDir, "config");
7351
+ const configFile = path18.join(shadowDir, "config");
6664
7352
  spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
6665
7353
  timeout: 3e3
6666
7354
  });
@@ -6668,7 +7356,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6668
7356
  timeout: 3e3
6669
7357
  });
6670
7358
  try {
6671
- fs14.writeFileSync(path16.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
7359
+ fs16.writeFileSync(path18.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
6672
7360
  } catch {
6673
7361
  }
6674
7362
  return true;
@@ -6691,7 +7379,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6691
7379
  const shadowDir = getShadowRepoDir(cwd);
6692
7380
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
6693
7381
  writeShadowExcludes(shadowDir, ignorePaths);
6694
- indexFile = path16.join(shadowDir, `index_${process.pid}_${Date.now()}`);
7382
+ indexFile = path18.join(shadowDir, `index_${process.pid}_${Date.now()}`);
6695
7383
  const shadowEnv = {
6696
7384
  ...process.env,
6697
7385
  GIT_DIR: shadowDir,
@@ -6720,7 +7408,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6720
7408
  const shouldGc = stack.length % 5 === 0;
6721
7409
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
6722
7410
  writeStack(stack);
6723
- fs14.writeFileSync(UNDO_LATEST_PATH, commitHash);
7411
+ fs16.writeFileSync(UNDO_LATEST_PATH, commitHash);
6724
7412
  if (shouldGc) {
6725
7413
  spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
6726
7414
  }
@@ -6731,7 +7419,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6731
7419
  } finally {
6732
7420
  if (indexFile) {
6733
7421
  try {
6734
- fs14.unlinkSync(indexFile);
7422
+ fs16.unlinkSync(indexFile);
6735
7423
  } catch {
6736
7424
  }
6737
7425
  }
@@ -6800,9 +7488,9 @@ function applyUndo(hash, cwd) {
6800
7488
  timeout: GIT_TIMEOUT
6801
7489
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
6802
7490
  for (const file of [...tracked, ...untracked]) {
6803
- const fullPath = path16.join(dir, file);
6804
- if (!snapshotFiles.has(file) && fs14.existsSync(fullPath)) {
6805
- fs14.unlinkSync(fullPath);
7491
+ const fullPath = path18.join(dir, file);
7492
+ if (!snapshotFiles.has(file) && fs16.existsSync(fullPath)) {
7493
+ fs16.unlinkSync(fullPath);
6806
7494
  }
6807
7495
  }
6808
7496
  return true;
@@ -6826,9 +7514,9 @@ function registerCheckCommand(program2) {
6826
7514
  } catch (err) {
6827
7515
  const tempConfig = getConfig();
6828
7516
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
6829
- const logPath = path17.join(os13.homedir(), ".node9", "hook-debug.log");
7517
+ const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
6830
7518
  const errMsg = err instanceof Error ? err.message : String(err);
6831
- fs15.appendFileSync(
7519
+ fs17.appendFileSync(
6832
7520
  logPath,
6833
7521
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
6834
7522
  RAW: ${raw}
@@ -6839,10 +7527,10 @@ RAW: ${raw}
6839
7527
  }
6840
7528
  const config = getConfig(payload.cwd || void 0);
6841
7529
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
6842
- const logPath = path17.join(os13.homedir(), ".node9", "hook-debug.log");
6843
- if (!fs15.existsSync(path17.dirname(logPath)))
6844
- fs15.mkdirSync(path17.dirname(logPath), { recursive: true });
6845
- fs15.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
7530
+ const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
7531
+ if (!fs17.existsSync(path19.dirname(logPath)))
7532
+ fs17.mkdirSync(path19.dirname(logPath), { recursive: true });
7533
+ fs17.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6846
7534
  `);
6847
7535
  }
6848
7536
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -6855,8 +7543,8 @@ RAW: ${raw}
6855
7543
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
6856
7544
  let ttyFd = null;
6857
7545
  try {
6858
- ttyFd = fs15.openSync("/dev/tty", "w");
6859
- const writeTty = (line) => fs15.writeSync(ttyFd, line + "\n");
7546
+ ttyFd = fs17.openSync("/dev/tty", "w");
7547
+ const writeTty = (line) => fs17.writeSync(ttyFd, line + "\n");
6860
7548
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
6861
7549
  writeTty(chalk5.bgRed.white.bold(`
6862
7550
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -6872,7 +7560,7 @@ RAW: ${raw}
6872
7560
  } finally {
6873
7561
  if (ttyFd !== null)
6874
7562
  try {
6875
- fs15.closeSync(ttyFd);
7563
+ fs17.closeSync(ttyFd);
6876
7564
  } catch {
6877
7565
  }
6878
7566
  }
@@ -6903,7 +7591,7 @@ RAW: ${raw}
6903
7591
  if (shouldSnapshot(toolName, toolInput, config)) {
6904
7592
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6905
7593
  }
6906
- const safeCwdForAuth = typeof payload.cwd === "string" && path17.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7594
+ const safeCwdForAuth = typeof payload.cwd === "string" && path19.isAbsolute(payload.cwd) ? payload.cwd : void 0;
6907
7595
  const result = await authorizeHeadless(toolName, toolInput, meta, {
6908
7596
  cwd: safeCwdForAuth
6909
7597
  });
@@ -6915,12 +7603,12 @@ RAW: ${raw}
6915
7603
  }
6916
7604
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
6917
7605
  try {
6918
- const tty = fs15.openSync("/dev/tty", "w");
6919
- fs15.writeSync(
7606
+ const tty = fs17.openSync("/dev/tty", "w");
7607
+ fs17.writeSync(
6920
7608
  tty,
6921
7609
  chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
6922
7610
  );
6923
- fs15.closeSync(tty);
7611
+ fs17.closeSync(tty);
6924
7612
  } catch {
6925
7613
  }
6926
7614
  const daemonReady = await autoStartDaemonAndWait();
@@ -6947,9 +7635,9 @@ RAW: ${raw}
6947
7635
  });
6948
7636
  } catch (err) {
6949
7637
  if (process.env.NODE9_DEBUG === "1") {
6950
- const logPath = path17.join(os13.homedir(), ".node9", "hook-debug.log");
7638
+ const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
6951
7639
  const errMsg = err instanceof Error ? err.message : String(err);
6952
- fs15.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7640
+ fs17.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6953
7641
  `);
6954
7642
  }
6955
7643
  process.exit(0);
@@ -6986,9 +7674,9 @@ RAW: ${raw}
6986
7674
  init_audit();
6987
7675
  init_config();
6988
7676
  init_policy();
6989
- import fs16 from "fs";
6990
- import path18 from "path";
6991
- import os14 from "os";
7677
+ import fs18 from "fs";
7678
+ import path20 from "path";
7679
+ import os16 from "os";
6992
7680
  function sanitize3(value) {
6993
7681
  return value.replace(/[\x00-\x1F\x7F]/g, "");
6994
7682
  }
@@ -7007,11 +7695,11 @@ function registerLogCommand(program2) {
7007
7695
  decision: "allowed",
7008
7696
  source: "post-hook"
7009
7697
  };
7010
- const logPath = path18.join(os14.homedir(), ".node9", "audit.log");
7011
- if (!fs16.existsSync(path18.dirname(logPath)))
7012
- fs16.mkdirSync(path18.dirname(logPath), { recursive: true });
7013
- fs16.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7014
- const safeCwd = typeof payload.cwd === "string" && path18.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7698
+ const logPath = path20.join(os16.homedir(), ".node9", "audit.log");
7699
+ if (!fs18.existsSync(path20.dirname(logPath)))
7700
+ fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
7701
+ fs18.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7702
+ const safeCwd = typeof payload.cwd === "string" && path20.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7015
7703
  const config = getConfig(safeCwd);
7016
7704
  if (shouldSnapshot(tool, {}, config)) {
7017
7705
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -7020,9 +7708,9 @@ function registerLogCommand(program2) {
7020
7708
  const msg = err instanceof Error ? err.message : String(err);
7021
7709
  process.stderr.write(`[Node9] audit log error: ${msg}
7022
7710
  `);
7023
- const debugPath = path18.join(os14.homedir(), ".node9", "hook-debug.log");
7711
+ const debugPath = path20.join(os16.homedir(), ".node9", "hook-debug.log");
7024
7712
  try {
7025
- fs16.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7713
+ fs18.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7026
7714
  `);
7027
7715
  } catch {
7028
7716
  }
@@ -7327,13 +8015,13 @@ function registerConfigShowCommand(program2) {
7327
8015
  // src/cli/commands/doctor.ts
7328
8016
  init_daemon();
7329
8017
  import chalk7 from "chalk";
7330
- import fs17 from "fs";
7331
- import path19 from "path";
7332
- import os15 from "os";
8018
+ import fs19 from "fs";
8019
+ import path21 from "path";
8020
+ import os17 from "os";
7333
8021
  import { execSync as execSync2 } from "child_process";
7334
8022
  function registerDoctorCommand(program2, version2) {
7335
8023
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
7336
- const homeDir2 = os15.homedir();
8024
+ const homeDir2 = os17.homedir();
7337
8025
  let failures = 0;
7338
8026
  function pass(msg) {
7339
8027
  console.log(chalk7.green(" \u2705 ") + msg);
@@ -7382,10 +8070,10 @@ function registerDoctorCommand(program2, version2) {
7382
8070
  );
7383
8071
  }
7384
8072
  section("Configuration");
7385
- const globalConfigPath = path19.join(homeDir2, ".node9", "config.json");
7386
- if (fs17.existsSync(globalConfigPath)) {
8073
+ const globalConfigPath = path21.join(homeDir2, ".node9", "config.json");
8074
+ if (fs19.existsSync(globalConfigPath)) {
7387
8075
  try {
7388
- JSON.parse(fs17.readFileSync(globalConfigPath, "utf-8"));
8076
+ JSON.parse(fs19.readFileSync(globalConfigPath, "utf-8"));
7389
8077
  pass("~/.node9/config.json found and valid");
7390
8078
  } catch {
7391
8079
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -7393,10 +8081,10 @@ function registerDoctorCommand(program2, version2) {
7393
8081
  } else {
7394
8082
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
7395
8083
  }
7396
- const projectConfigPath = path19.join(process.cwd(), "node9.config.json");
7397
- if (fs17.existsSync(projectConfigPath)) {
8084
+ const projectConfigPath = path21.join(process.cwd(), "node9.config.json");
8085
+ if (fs19.existsSync(projectConfigPath)) {
7398
8086
  try {
7399
- JSON.parse(fs17.readFileSync(projectConfigPath, "utf-8"));
8087
+ JSON.parse(fs19.readFileSync(projectConfigPath, "utf-8"));
7400
8088
  pass("node9.config.json found and valid (project)");
7401
8089
  } catch {
7402
8090
  fail(
@@ -7405,8 +8093,8 @@ function registerDoctorCommand(program2, version2) {
7405
8093
  );
7406
8094
  }
7407
8095
  }
7408
- const credsPath = path19.join(homeDir2, ".node9", "credentials.json");
7409
- if (fs17.existsSync(credsPath)) {
8096
+ const credsPath = path21.join(homeDir2, ".node9", "credentials.json");
8097
+ if (fs19.existsSync(credsPath)) {
7410
8098
  pass("Cloud credentials found (~/.node9/credentials.json)");
7411
8099
  } else {
7412
8100
  warn(
@@ -7415,10 +8103,10 @@ function registerDoctorCommand(program2, version2) {
7415
8103
  );
7416
8104
  }
7417
8105
  section("Agent Hooks");
7418
- const claudeSettingsPath = path19.join(homeDir2, ".claude", "settings.json");
7419
- if (fs17.existsSync(claudeSettingsPath)) {
8106
+ const claudeSettingsPath = path21.join(homeDir2, ".claude", "settings.json");
8107
+ if (fs19.existsSync(claudeSettingsPath)) {
7420
8108
  try {
7421
- const cs = JSON.parse(fs17.readFileSync(claudeSettingsPath, "utf-8"));
8109
+ const cs = JSON.parse(fs19.readFileSync(claudeSettingsPath, "utf-8"));
7422
8110
  const hasHook = cs.hooks?.PreToolUse?.some(
7423
8111
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7424
8112
  );
@@ -7434,10 +8122,10 @@ function registerDoctorCommand(program2, version2) {
7434
8122
  } else {
7435
8123
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
7436
8124
  }
7437
- const geminiSettingsPath = path19.join(homeDir2, ".gemini", "settings.json");
7438
- if (fs17.existsSync(geminiSettingsPath)) {
8125
+ const geminiSettingsPath = path21.join(homeDir2, ".gemini", "settings.json");
8126
+ if (fs19.existsSync(geminiSettingsPath)) {
7439
8127
  try {
7440
- const gs = JSON.parse(fs17.readFileSync(geminiSettingsPath, "utf-8"));
8128
+ const gs = JSON.parse(fs19.readFileSync(geminiSettingsPath, "utf-8"));
7441
8129
  const hasHook = gs.hooks?.BeforeTool?.some(
7442
8130
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7443
8131
  );
@@ -7453,10 +8141,10 @@ function registerDoctorCommand(program2, version2) {
7453
8141
  } else {
7454
8142
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
7455
8143
  }
7456
- const cursorHooksPath = path19.join(homeDir2, ".cursor", "hooks.json");
7457
- if (fs17.existsSync(cursorHooksPath)) {
8144
+ const cursorHooksPath = path21.join(homeDir2, ".cursor", "hooks.json");
8145
+ if (fs19.existsSync(cursorHooksPath)) {
7458
8146
  try {
7459
- const cur = JSON.parse(fs17.readFileSync(cursorHooksPath, "utf-8"));
8147
+ const cur = JSON.parse(fs19.readFileSync(cursorHooksPath, "utf-8"));
7460
8148
  const hasHook = cur.hooks?.preToolUse?.some(
7461
8149
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
7462
8150
  );
@@ -7494,9 +8182,9 @@ function registerDoctorCommand(program2, version2) {
7494
8182
 
7495
8183
  // src/cli/commands/audit.ts
7496
8184
  import chalk8 from "chalk";
7497
- import fs18 from "fs";
7498
- import path20 from "path";
7499
- import os16 from "os";
8185
+ import fs20 from "fs";
8186
+ import path22 from "path";
8187
+ import os18 from "os";
7500
8188
  function formatRelativeTime(timestamp) {
7501
8189
  const diff = Date.now() - new Date(timestamp).getTime();
7502
8190
  const sec = Math.floor(diff / 1e3);
@@ -7509,14 +8197,14 @@ function formatRelativeTime(timestamp) {
7509
8197
  }
7510
8198
  function registerAuditCommand(program2) {
7511
8199
  program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
7512
- const logPath = path20.join(os16.homedir(), ".node9", "audit.log");
7513
- if (!fs18.existsSync(logPath)) {
8200
+ const logPath = path22.join(os18.homedir(), ".node9", "audit.log");
8201
+ if (!fs20.existsSync(logPath)) {
7514
8202
  console.log(
7515
8203
  chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
7516
8204
  );
7517
8205
  return;
7518
8206
  }
7519
- const raw = fs18.readFileSync(logPath, "utf-8");
8207
+ const raw = fs20.readFileSync(logPath, "utf-8");
7520
8208
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
7521
8209
  let entries = lines.flatMap((line) => {
7522
8210
  try {
@@ -7636,9 +8324,42 @@ function registerDaemonCommand(program2) {
7636
8324
  init_core();
7637
8325
  init_daemon();
7638
8326
  import chalk10 from "chalk";
7639
- import fs19 from "fs";
7640
- import path21 from "path";
7641
- import os17 from "os";
8327
+ import fs21 from "fs";
8328
+ import path23 from "path";
8329
+ import os19 from "os";
8330
+ function readJson2(filePath) {
8331
+ try {
8332
+ if (fs21.existsSync(filePath)) return JSON.parse(fs21.readFileSync(filePath, "utf-8"));
8333
+ } catch {
8334
+ }
8335
+ return null;
8336
+ }
8337
+ function isNode9Hook2(cmd) {
8338
+ if (!cmd) return false;
8339
+ return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
8340
+ }
8341
+ function wrappedMcpServers(servers) {
8342
+ if (!servers) return [];
8343
+ return Object.entries(servers).filter(([, s]) => s.command === "node9" && Array.isArray(s.args) && s.args.length > 0).map(([name, s]) => `${name} \u2192 ${s.args.join(" ")}`);
8344
+ }
8345
+ function printAgentSection(label, hookPairs, wrapped) {
8346
+ console.log(chalk10.bold(` ${label}`));
8347
+ for (const { name, present } of hookPairs) {
8348
+ if (present) {
8349
+ console.log(chalk10.green(` \u2713 ${name}`));
8350
+ } else {
8351
+ console.log(chalk10.red(` \u2717 ${name}`) + chalk10.gray(" (not wired)"));
8352
+ }
8353
+ }
8354
+ if (wrapped.length > 0) {
8355
+ console.log(chalk10.cyan(` MCP proxied:`));
8356
+ for (const entry of wrapped) {
8357
+ console.log(chalk10.gray(` \u2022 ${entry}`));
8358
+ }
8359
+ } else {
8360
+ console.log(chalk10.gray(` MCP proxied: none`));
8361
+ }
8362
+ }
7642
8363
  function registerStatusCommand(program2) {
7643
8364
  program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
7644
8365
  const creds = getCredentials();
@@ -7673,19 +8394,72 @@ function registerStatusCommand(program2) {
7673
8394
  console.log("");
7674
8395
  const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
7675
8396
  console.log(` Mode: ${modeLabel}`);
7676
- const projectConfig = path21.join(process.cwd(), "node9.config.json");
7677
- const globalConfig = path21.join(os17.homedir(), ".node9", "config.json");
8397
+ const projectConfig = path23.join(process.cwd(), "node9.config.json");
8398
+ const globalConfig = path23.join(os19.homedir(), ".node9", "config.json");
7678
8399
  console.log(
7679
- ` Local: ${fs19.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
8400
+ ` Local: ${fs21.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
7680
8401
  );
7681
8402
  console.log(
7682
- ` Global: ${fs19.existsSync(globalConfig) ? chalk10.green("Active (~/.node9/config.json)") : chalk10.gray("Not present")}`
8403
+ ` Global: ${fs21.existsSync(globalConfig) ? chalk10.green("Active (~/.node9/config.json)") : chalk10.gray("Not present")}`
7683
8404
  );
7684
8405
  if (mergedConfig.policy.sandboxPaths.length > 0) {
7685
8406
  console.log(
7686
8407
  ` Sandbox: ${chalk10.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
7687
8408
  );
7688
8409
  }
8410
+ const homeDir2 = os19.homedir();
8411
+ const claudeSettings = readJson2(
8412
+ path23.join(homeDir2, ".claude", "settings.json")
8413
+ );
8414
+ const claudeConfig = readJson2(path23.join(homeDir2, ".claude.json"));
8415
+ const geminiSettings = readJson2(
8416
+ path23.join(homeDir2, ".gemini", "settings.json")
8417
+ );
8418
+ const cursorConfig = readJson2(path23.join(homeDir2, ".cursor", "mcp.json"));
8419
+ const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
8420
+ if (agentFound) {
8421
+ console.log("");
8422
+ console.log(chalk10.bold(" Agent Wiring:"));
8423
+ console.log("");
8424
+ if (claudeSettings || claudeConfig) {
8425
+ const preHook = claudeSettings?.hooks?.PreToolUse?.some(
8426
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8427
+ ) ?? false;
8428
+ const postHook = claudeSettings?.hooks?.PostToolUse?.some(
8429
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8430
+ ) ?? false;
8431
+ printAgentSection(
8432
+ "Claude Code",
8433
+ [
8434
+ { name: "PreToolUse (node9 check)", present: preHook },
8435
+ { name: "PostToolUse (node9 log)", present: postHook }
8436
+ ],
8437
+ wrappedMcpServers(claudeConfig?.mcpServers)
8438
+ );
8439
+ console.log("");
8440
+ }
8441
+ if (geminiSettings) {
8442
+ const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
8443
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8444
+ ) ?? false;
8445
+ const afterHook = geminiSettings.hooks?.AfterTool?.some(
8446
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8447
+ ) ?? false;
8448
+ printAgentSection(
8449
+ "Gemini CLI",
8450
+ [
8451
+ { name: "BeforeTool (node9 check)", present: beforeHook },
8452
+ { name: "AfterTool (node9 log)", present: afterHook }
8453
+ ],
8454
+ wrappedMcpServers(geminiSettings.mcpServers)
8455
+ );
8456
+ console.log("");
8457
+ }
8458
+ if (cursorConfig) {
8459
+ printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
8460
+ console.log("");
8461
+ }
8462
+ }
7689
8463
  const pauseState = checkPause();
7690
8464
  if (pauseState.paused) {
7691
8465
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
@@ -7698,8 +8472,63 @@ function registerStatusCommand(program2) {
7698
8472
  });
7699
8473
  }
7700
8474
 
7701
- // src/cli/commands/undo.ts
8475
+ // src/cli/commands/init.ts
8476
+ init_core();
7702
8477
  import chalk11 from "chalk";
8478
+ import fs22 from "fs";
8479
+ import path24 from "path";
8480
+ import os20 from "os";
8481
+ function registerInitCommand(program2) {
8482
+ program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
8483
+ console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
8484
+ const configPath = path24.join(os20.homedir(), ".node9", "config.json");
8485
+ if (fs22.existsSync(configPath) && !options.force) {
8486
+ console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8487
+ } else {
8488
+ const requestedMode = options.mode.toLowerCase();
8489
+ const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8490
+ const configToSave = {
8491
+ ...DEFAULT_CONFIG,
8492
+ settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
8493
+ };
8494
+ const dir = path24.dirname(configPath);
8495
+ if (!fs22.existsSync(dir)) fs22.mkdirSync(dir, { recursive: true });
8496
+ fs22.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8497
+ console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
8498
+ console.log(chalk11.gray(` Mode: ${safeMode}`));
8499
+ }
8500
+ if (options.skipSetup) return;
8501
+ console.log("");
8502
+ const detected = detectAgents();
8503
+ const found = Object.keys(detected).filter(
8504
+ (k) => detected[k]
8505
+ );
8506
+ if (found.length === 0) {
8507
+ console.log(
8508
+ chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
8509
+ );
8510
+ console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
8511
+ return;
8512
+ }
8513
+ console.log(chalk11.bold("Detected agents:"));
8514
+ for (const agent of found) {
8515
+ console.log(chalk11.green(` \u2713 ${agent}`));
8516
+ }
8517
+ console.log("");
8518
+ for (const agent of found) {
8519
+ console.log(chalk11.bold(`Wiring ${agent}...`));
8520
+ if (agent === "claude") await setupClaude();
8521
+ else if (agent === "gemini") await setupGemini();
8522
+ else if (agent === "cursor") await setupCursor();
8523
+ console.log("");
8524
+ }
8525
+ console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
8526
+ console.log(chalk11.gray(" Run: node9 daemon start"));
8527
+ });
8528
+ }
8529
+
8530
+ // src/cli/commands/undo.ts
8531
+ import chalk12 from "chalk";
7703
8532
  import { confirm as confirm2 } from "@inquirer/prompts";
7704
8533
  function registerUndoCommand(program2) {
7705
8534
  program2.command("undo").description(
@@ -7711,22 +8540,22 @@ function registerUndoCommand(program2) {
7711
8540
  if (history.length === 0) {
7712
8541
  if (!options.all && allHistory.length > 0) {
7713
8542
  console.log(
7714
- chalk11.yellow(
8543
+ chalk12.yellow(
7715
8544
  `
7716
8545
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
7717
- Run ${chalk11.cyan("node9 undo --all")} to see snapshots from all projects.
8546
+ Run ${chalk12.cyan("node9 undo --all")} to see snapshots from all projects.
7718
8547
  `
7719
8548
  )
7720
8549
  );
7721
8550
  } else {
7722
- console.log(chalk11.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
8551
+ console.log(chalk12.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
7723
8552
  }
7724
8553
  return;
7725
8554
  }
7726
8555
  const idx = history.length - steps;
7727
8556
  if (idx < 0) {
7728
8557
  console.log(
7729
- chalk11.yellow(
8558
+ chalk12.yellow(
7730
8559
  `
7731
8560
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
7732
8561
  `
@@ -7738,19 +8567,19 @@ function registerUndoCommand(program2) {
7738
8567
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
7739
8568
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
7740
8569
  console.log(
7741
- chalk11.magenta.bold(`
8570
+ chalk12.magenta.bold(`
7742
8571
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
7743
8572
  );
7744
8573
  console.log(
7745
- chalk11.white(
7746
- ` Tool: ${chalk11.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk11.gray(" \u2192 " + snapshot.argsSummary) : ""}`
8574
+ chalk12.white(
8575
+ ` Tool: ${chalk12.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk12.gray(" \u2192 " + snapshot.argsSummary) : ""}`
7747
8576
  )
7748
8577
  );
7749
- console.log(chalk11.white(` When: ${chalk11.gray(ageStr)}`));
7750
- console.log(chalk11.white(` Dir: ${chalk11.gray(snapshot.cwd)}`));
8578
+ console.log(chalk12.white(` When: ${chalk12.gray(ageStr)}`));
8579
+ console.log(chalk12.white(` Dir: ${chalk12.gray(snapshot.cwd)}`));
7751
8580
  if (steps > 1)
7752
8581
  console.log(
7753
- chalk11.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
8582
+ chalk12.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
7754
8583
  );
7755
8584
  console.log("");
7756
8585
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -7758,21 +8587,21 @@ function registerUndoCommand(program2) {
7758
8587
  const lines = diff.split("\n");
7759
8588
  for (const line of lines) {
7760
8589
  if (line.startsWith("+++") || line.startsWith("---")) {
7761
- console.log(chalk11.bold(line));
8590
+ console.log(chalk12.bold(line));
7762
8591
  } else if (line.startsWith("+")) {
7763
- console.log(chalk11.green(line));
8592
+ console.log(chalk12.green(line));
7764
8593
  } else if (line.startsWith("-")) {
7765
- console.log(chalk11.red(line));
8594
+ console.log(chalk12.red(line));
7766
8595
  } else if (line.startsWith("@@")) {
7767
- console.log(chalk11.cyan(line));
8596
+ console.log(chalk12.cyan(line));
7768
8597
  } else {
7769
- console.log(chalk11.gray(line));
8598
+ console.log(chalk12.gray(line));
7770
8599
  }
7771
8600
  }
7772
8601
  console.log("");
7773
8602
  } else {
7774
8603
  console.log(
7775
- chalk11.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
8604
+ chalk12.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
7776
8605
  );
7777
8606
  }
7778
8607
  const proceed = await confirm2({
@@ -7781,19 +8610,19 @@ function registerUndoCommand(program2) {
7781
8610
  });
7782
8611
  if (proceed) {
7783
8612
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
7784
- console.log(chalk11.green("\n\u2705 Reverted successfully.\n"));
8613
+ console.log(chalk12.green("\n\u2705 Reverted successfully.\n"));
7785
8614
  } else {
7786
- console.error(chalk11.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
8615
+ console.error(chalk12.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
7787
8616
  }
7788
8617
  } else {
7789
- console.log(chalk11.gray("\nCancelled.\n"));
8618
+ console.log(chalk12.gray("\nCancelled.\n"));
7790
8619
  }
7791
8620
  });
7792
8621
  }
7793
8622
 
7794
8623
  // src/cli/commands/watch.ts
7795
8624
  init_daemon();
7796
- import chalk12 from "chalk";
8625
+ import chalk13 from "chalk";
7797
8626
  import { spawn as spawn7, spawnSync as spawnSync5 } from "child_process";
7798
8627
  function registerWatchCommand(program2) {
7799
8628
  program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
@@ -7809,7 +8638,7 @@ function registerWatchCommand(program2) {
7809
8638
  throw new Error("not running");
7810
8639
  }
7811
8640
  } catch {
7812
- console.error(chalk12.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
8641
+ console.error(chalk13.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
7813
8642
  const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
7814
8643
  detached: true,
7815
8644
  stdio: "ignore",
@@ -7831,12 +8660,12 @@ function registerWatchCommand(program2) {
7831
8660
  }
7832
8661
  }
7833
8662
  if (!ready) {
7834
- console.error(chalk12.red("\u274C Daemon failed to start. Try: node9 daemon start"));
8663
+ console.error(chalk13.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7835
8664
  process.exit(1);
7836
8665
  }
7837
8666
  }
7838
8667
  console.error(
7839
- chalk12.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk12.dim(` \u2192 localhost:${port}`) + chalk12.dim(
8668
+ chalk13.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk13.dim(` \u2192 localhost:${port}`) + chalk13.dim(
7840
8669
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
7841
8670
  )
7842
8671
  );
@@ -7845,7 +8674,7 @@ function registerWatchCommand(program2) {
7845
8674
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
7846
8675
  });
7847
8676
  if (result.error) {
7848
- console.error(chalk12.red(`\u274C Failed to run command: ${result.error.message}`));
8677
+ console.error(chalk13.red(`\u274C Failed to run command: ${result.error.message}`));
7849
8678
  process.exit(1);
7850
8679
  }
7851
8680
  process.exit(result.status ?? 0);
@@ -7855,7 +8684,7 @@ function registerWatchCommand(program2) {
7855
8684
  // src/mcp-gateway/index.ts
7856
8685
  init_orchestrator();
7857
8686
  import readline2 from "readline";
7858
- import chalk13 from "chalk";
8687
+ import chalk14 from "chalk";
7859
8688
  import { spawn as spawn8 } from "child_process";
7860
8689
  import { execa as execa2 } from "execa";
7861
8690
  init_provenance();
@@ -7918,13 +8747,13 @@ async function runMcpGateway(upstreamCommand) {
7918
8747
  const prov = checkProvenance(executable);
7919
8748
  if (prov.trustLevel === "suspect") {
7920
8749
  console.error(
7921
- chalk13.red(
8750
+ chalk14.red(
7922
8751
  `\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
7923
8752
  )
7924
8753
  );
7925
- console.error(chalk13.red(" Verify this binary is trusted before proceeding."));
8754
+ console.error(chalk14.red(" Verify this binary is trusted before proceeding."));
7926
8755
  }
7927
- console.error(chalk13.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
8756
+ console.error(chalk14.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
7928
8757
  const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
7929
8758
  "NODE_OPTIONS",
7930
8759
  "NODE_PATH",
@@ -7988,10 +8817,10 @@ async function runMcpGateway(upstreamCommand) {
7988
8817
  mcpServer
7989
8818
  });
7990
8819
  if (!result.approved) {
7991
- console.error(chalk13.red(`
8820
+ console.error(chalk14.red(`
7992
8821
  \u{1F6D1} Node9 MCP Gateway: Action Blocked`));
7993
- console.error(chalk13.gray(` Tool: ${toolName}`));
7994
- console.error(chalk13.gray(` Reason: ${result.reason ?? "Security Policy"}
8822
+ console.error(chalk14.gray(` Tool: ${toolName}`));
8823
+ console.error(chalk14.gray(` Reason: ${result.reason ?? "Security Policy"}
7995
8824
  `));
7996
8825
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
7997
8826
  const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -8063,22 +8892,77 @@ function registerMcpGatewayCommand(program2) {
8063
8892
  });
8064
8893
  }
8065
8894
 
8895
+ // src/cli/commands/trust.ts
8896
+ init_trusted_hosts();
8897
+ import chalk15 from "chalk";
8898
+ function isValidHost(host) {
8899
+ return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
8900
+ }
8901
+ function registerTrustCommand(program2) {
8902
+ const trustCmd = program2.command("trust").description("Manage trusted network hosts (reduces approval friction for known destinations)");
8903
+ trustCmd.command("add <host>").description("Add a trusted host \u2014 pipe-chain blocks targeting this host are downgraded").action((host) => {
8904
+ const normalized = normalizeHost(host.trim());
8905
+ if (!isValidHost(normalized)) {
8906
+ console.error(
8907
+ chalk15.red(`
8908
+ \u274C Invalid host: "${host}"
8909
+ `) + chalk15.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
8910
+ );
8911
+ process.exit(1);
8912
+ }
8913
+ addTrustedHost(normalized);
8914
+ console.log(chalk15.green(`
8915
+ \u2705 ${normalized} added to trusted hosts.`));
8916
+ console.log(
8917
+ chalk15.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
8918
+ );
8919
+ });
8920
+ trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
8921
+ const normalized = normalizeHost(host.trim());
8922
+ const removed = removeTrustedHost(normalized);
8923
+ if (!removed) {
8924
+ console.error(chalk15.yellow(`
8925
+ \u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
8926
+ `));
8927
+ process.exit(1);
8928
+ }
8929
+ console.log(chalk15.green(`
8930
+ \u2705 ${normalized} removed from trusted hosts.
8931
+ `));
8932
+ });
8933
+ trustCmd.command("list").description("Show all trusted hosts").action(() => {
8934
+ const hosts = readTrustedHosts();
8935
+ if (hosts.length === 0) {
8936
+ console.log(chalk15.gray("\n No trusted hosts configured.\n"));
8937
+ console.log(` Add one: ${chalk15.cyan("node9 trust add api.mycompany.com")}
8938
+ `);
8939
+ return;
8940
+ }
8941
+ console.log(chalk15.bold("\n\u{1F513} Trusted Hosts\n"));
8942
+ for (const entry of hosts) {
8943
+ const date = new Date(entry.addedAt).toLocaleDateString();
8944
+ console.log(` ${chalk15.cyan(entry.host.padEnd(40))} ${chalk15.gray(`added ${date}`)}`);
8945
+ }
8946
+ console.log("");
8947
+ });
8948
+ }
8949
+
8066
8950
  // src/cli.ts
8067
8951
  var { version } = JSON.parse(
8068
- fs21.readFileSync(path23.join(__dirname, "../package.json"), "utf-8")
8952
+ fs24.readFileSync(path26.join(__dirname, "../package.json"), "utf-8")
8069
8953
  );
8070
8954
  var program = new Command();
8071
8955
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
8072
8956
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
8073
8957
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
8074
- const credPath = path23.join(os19.homedir(), ".node9", "credentials.json");
8075
- if (!fs21.existsSync(path23.dirname(credPath)))
8076
- fs21.mkdirSync(path23.dirname(credPath), { recursive: true });
8958
+ const credPath = path26.join(os22.homedir(), ".node9", "credentials.json");
8959
+ if (!fs24.existsSync(path26.dirname(credPath)))
8960
+ fs24.mkdirSync(path26.dirname(credPath), { recursive: true });
8077
8961
  const profileName = options.profile || "default";
8078
8962
  let existingCreds = {};
8079
8963
  try {
8080
- if (fs21.existsSync(credPath)) {
8081
- const raw = JSON.parse(fs21.readFileSync(credPath, "utf-8"));
8964
+ if (fs24.existsSync(credPath)) {
8965
+ const raw = JSON.parse(fs24.readFileSync(credPath, "utf-8"));
8082
8966
  if (raw.apiKey) {
8083
8967
  existingCreds = {
8084
8968
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -8090,13 +8974,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8090
8974
  } catch {
8091
8975
  }
8092
8976
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
8093
- fs21.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
8977
+ fs24.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
8094
8978
  if (profileName === "default") {
8095
- const configPath = path23.join(os19.homedir(), ".node9", "config.json");
8979
+ const configPath = path26.join(os22.homedir(), ".node9", "config.json");
8096
8980
  let config = {};
8097
8981
  try {
8098
- if (fs21.existsSync(configPath))
8099
- config = JSON.parse(fs21.readFileSync(configPath, "utf-8"));
8982
+ if (fs24.existsSync(configPath))
8983
+ config = JSON.parse(fs24.readFileSync(configPath, "utf-8"));
8100
8984
  } catch {
8101
8985
  }
8102
8986
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -8111,36 +8995,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8111
8995
  approvers.cloud = false;
8112
8996
  }
8113
8997
  s.approvers = approvers;
8114
- if (!fs21.existsSync(path23.dirname(configPath)))
8115
- fs21.mkdirSync(path23.dirname(configPath), { recursive: true });
8116
- fs21.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
8998
+ if (!fs24.existsSync(path26.dirname(configPath)))
8999
+ fs24.mkdirSync(path26.dirname(configPath), { recursive: true });
9000
+ fs24.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
8117
9001
  }
8118
9002
  if (options.profile && profileName !== "default") {
8119
- console.log(chalk15.green(`\u2705 Profile "${profileName}" saved`));
8120
- console.log(chalk15.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
9003
+ console.log(chalk17.green(`\u2705 Profile "${profileName}" saved`));
9004
+ console.log(chalk17.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
8121
9005
  } else if (options.local) {
8122
- console.log(chalk15.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
8123
- console.log(chalk15.gray(` All decisions stay on this machine.`));
9006
+ console.log(chalk17.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
9007
+ console.log(chalk17.gray(` All decisions stay on this machine.`));
8124
9008
  } else {
8125
- console.log(chalk15.green(`\u2705 Logged in \u2014 agent mode`));
8126
- console.log(chalk15.gray(` Team policy enforced for all calls via Node9 cloud.`));
9009
+ console.log(chalk17.green(`\u2705 Logged in \u2014 agent mode`));
9010
+ console.log(chalk17.gray(` Team policy enforced for all calls via Node9 cloud.`));
8127
9011
  }
8128
9012
  });
8129
9013
  program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
8130
9014
  if (target === "gemini") return await setupGemini();
8131
9015
  if (target === "claude") return await setupClaude();
8132
9016
  if (target === "cursor") return await setupCursor();
8133
- console.error(chalk15.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9017
+ console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8134
9018
  process.exit(1);
8135
9019
  });
8136
9020
  program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
8137
9021
  if (!target) {
8138
- console.log(chalk15.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
8139
- console.log(" Usage: " + chalk15.white("node9 setup <target>") + "\n");
9022
+ console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
9023
+ console.log(" Usage: " + chalk17.white("node9 setup <target>") + "\n");
8140
9024
  console.log(" Targets:");
8141
- console.log(" " + chalk15.green("claude") + " \u2014 Claude Code (hook mode)");
8142
- console.log(" " + chalk15.green("gemini") + " \u2014 Gemini CLI (hook mode)");
8143
- console.log(" " + chalk15.green("cursor") + " \u2014 Cursor (hook mode)");
9025
+ console.log(" " + chalk17.green("claude") + " \u2014 Claude Code (hook mode)");
9026
+ console.log(" " + chalk17.green("gemini") + " \u2014 Gemini CLI (hook mode)");
9027
+ console.log(" " + chalk17.green("cursor") + " \u2014 Cursor (hook mode)");
8144
9028
  console.log("");
8145
9029
  return;
8146
9030
  }
@@ -8148,7 +9032,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
8148
9032
  if (t === "gemini") return await setupGemini();
8149
9033
  if (t === "claude") return await setupClaude();
8150
9034
  if (t === "cursor") return await setupCursor();
8151
- console.error(chalk15.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9035
+ console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8152
9036
  process.exit(1);
8153
9037
  });
8154
9038
  program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
@@ -8157,30 +9041,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
8157
9041
  else if (target === "gemini") fn = teardownGemini;
8158
9042
  else if (target === "cursor") fn = teardownCursor;
8159
9043
  else {
8160
- console.error(chalk15.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9044
+ console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8161
9045
  process.exit(1);
8162
9046
  }
8163
- console.log(chalk15.cyan(`
9047
+ console.log(chalk17.cyan(`
8164
9048
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
8165
9049
  `));
8166
9050
  try {
8167
9051
  fn();
8168
9052
  } catch (err) {
8169
- console.error(chalk15.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
9053
+ console.error(chalk17.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
8170
9054
  process.exit(1);
8171
9055
  }
8172
- console.log(chalk15.gray("\n Restart the agent for changes to take effect."));
9056
+ console.log(chalk17.gray("\n Restart the agent for changes to take effect."));
8173
9057
  });
8174
9058
  program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
8175
- console.log(chalk15.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
8176
- console.log(chalk15.bold("Stopping daemon..."));
9059
+ console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
9060
+ console.log(chalk17.bold("Stopping daemon..."));
8177
9061
  try {
8178
9062
  stopDaemon();
8179
- console.log(chalk15.green(" \u2705 Daemon stopped"));
9063
+ console.log(chalk17.green(" \u2705 Daemon stopped"));
8180
9064
  } catch {
8181
- console.log(chalk15.blue(" \u2139\uFE0F Daemon was not running"));
9065
+ console.log(chalk17.blue(" \u2139\uFE0F Daemon was not running"));
8182
9066
  }
8183
- console.log(chalk15.bold("\nRemoving hooks..."));
9067
+ console.log(chalk17.bold("\nRemoving hooks..."));
8184
9068
  let teardownFailed = false;
8185
9069
  for (const [label, fn] of [
8186
9070
  ["Claude", teardownClaude],
@@ -8192,45 +9076,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
8192
9076
  } catch (err) {
8193
9077
  teardownFailed = true;
8194
9078
  console.error(
8195
- chalk15.red(
9079
+ chalk17.red(
8196
9080
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
8197
9081
  )
8198
9082
  );
8199
9083
  }
8200
9084
  }
8201
9085
  if (options.purge) {
8202
- const node9Dir = path23.join(os19.homedir(), ".node9");
8203
- if (fs21.existsSync(node9Dir)) {
9086
+ const node9Dir = path26.join(os22.homedir(), ".node9");
9087
+ if (fs24.existsSync(node9Dir)) {
8204
9088
  const confirmed = await confirm3({
8205
9089
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
8206
9090
  default: false
8207
9091
  });
8208
9092
  if (confirmed) {
8209
- fs21.rmSync(node9Dir, { recursive: true });
8210
- if (fs21.existsSync(node9Dir)) {
9093
+ fs24.rmSync(node9Dir, { recursive: true });
9094
+ if (fs24.existsSync(node9Dir)) {
8211
9095
  console.error(
8212
- chalk15.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9096
+ chalk17.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
8213
9097
  );
8214
9098
  } else {
8215
- console.log(chalk15.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
9099
+ console.log(chalk17.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
8216
9100
  }
8217
9101
  } else {
8218
- console.log(chalk15.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
9102
+ console.log(chalk17.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
8219
9103
  }
8220
9104
  } else {
8221
- console.log(chalk15.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
9105
+ console.log(chalk17.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
8222
9106
  }
8223
9107
  } else {
8224
9108
  console.log(
8225
- chalk15.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
9109
+ chalk17.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
8226
9110
  );
8227
9111
  }
8228
9112
  if (teardownFailed) {
8229
- console.error(chalk15.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
9113
+ console.error(chalk17.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
8230
9114
  process.exit(1);
8231
9115
  }
8232
- console.log(chalk15.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
8233
- console.log(chalk15.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
9116
+ console.log(chalk17.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
9117
+ console.log(chalk17.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
8234
9118
  });
8235
9119
  registerDoctorCommand(program, version);
8236
9120
  program.command("explain").description(
@@ -8243,7 +9127,7 @@ program.command("explain").description(
8243
9127
  try {
8244
9128
  args = JSON.parse(trimmed);
8245
9129
  } catch {
8246
- console.error(chalk15.red(`
9130
+ console.error(chalk17.red(`
8247
9131
  \u274C Invalid JSON: ${trimmed}
8248
9132
  `));
8249
9133
  process.exit(1);
@@ -8254,83 +9138,59 @@ program.command("explain").description(
8254
9138
  }
8255
9139
  const result = await explainPolicy(tool, args);
8256
9140
  console.log("");
8257
- console.log(chalk15.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
9141
+ console.log(chalk17.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
8258
9142
  console.log("");
8259
- console.log(` ${chalk15.bold("Tool:")} ${chalk15.white(result.tool)}`);
9143
+ console.log(` ${chalk17.bold("Tool:")} ${chalk17.white(result.tool)}`);
8260
9144
  if (argsRaw) {
8261
9145
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
8262
- console.log(` ${chalk15.bold("Input:")} ${chalk15.gray(preview)}`);
9146
+ console.log(` ${chalk17.bold("Input:")} ${chalk17.gray(preview)}`);
8263
9147
  }
8264
9148
  console.log("");
8265
- console.log(chalk15.bold("Config Sources (Waterfall):"));
9149
+ console.log(chalk17.bold("Config Sources (Waterfall):"));
8266
9150
  for (const tier of result.waterfall) {
8267
- const num = chalk15.gray(` ${tier.tier}.`);
9151
+ const num = chalk17.gray(` ${tier.tier}.`);
8268
9152
  const label = tier.label.padEnd(16);
8269
9153
  let statusStr;
8270
9154
  if (tier.tier === 1) {
8271
- statusStr = chalk15.gray(tier.note ?? "");
9155
+ statusStr = chalk17.gray(tier.note ?? "");
8272
9156
  } else if (tier.status === "active") {
8273
- const loc = tier.path ? chalk15.gray(tier.path) : "";
8274
- const note = tier.note ? chalk15.gray(`(${tier.note})`) : "";
8275
- statusStr = chalk15.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
9157
+ const loc = tier.path ? chalk17.gray(tier.path) : "";
9158
+ const note = tier.note ? chalk17.gray(`(${tier.note})`) : "";
9159
+ statusStr = chalk17.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
8276
9160
  } else {
8277
- statusStr = chalk15.gray("\u25CB " + (tier.note ?? "not found"));
9161
+ statusStr = chalk17.gray("\u25CB " + (tier.note ?? "not found"));
8278
9162
  }
8279
- console.log(`${num} ${chalk15.white(label)} ${statusStr}`);
9163
+ console.log(`${num} ${chalk17.white(label)} ${statusStr}`);
8280
9164
  }
8281
9165
  console.log("");
8282
- console.log(chalk15.bold("Policy Evaluation:"));
9166
+ console.log(chalk17.bold("Policy Evaluation:"));
8283
9167
  for (const step of result.steps) {
8284
9168
  const isFinal = step.isFinal;
8285
9169
  let icon;
8286
- if (step.outcome === "allow") icon = chalk15.green(" \u2705");
8287
- else if (step.outcome === "review") icon = chalk15.red(" \u{1F534}");
8288
- else if (step.outcome === "skip") icon = chalk15.gray(" \u2500 ");
8289
- else icon = chalk15.gray(" \u25CB ");
9170
+ if (step.outcome === "allow") icon = chalk17.green(" \u2705");
9171
+ else if (step.outcome === "review") icon = chalk17.red(" \u{1F534}");
9172
+ else if (step.outcome === "skip") icon = chalk17.gray(" \u2500 ");
9173
+ else icon = chalk17.gray(" \u25CB ");
8290
9174
  const name = step.name.padEnd(18);
8291
- const nameStr = isFinal ? chalk15.white.bold(name) : chalk15.white(name);
8292
- const detail = isFinal ? chalk15.white(step.detail) : chalk15.gray(step.detail);
8293
- const arrow = isFinal ? chalk15.yellow(" \u2190 STOP") : "";
9175
+ const nameStr = isFinal ? chalk17.white.bold(name) : chalk17.white(name);
9176
+ const detail = isFinal ? chalk17.white(step.detail) : chalk17.gray(step.detail);
9177
+ const arrow = isFinal ? chalk17.yellow(" \u2190 STOP") : "";
8294
9178
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
8295
9179
  }
8296
9180
  console.log("");
8297
9181
  if (result.decision === "allow") {
8298
- console.log(chalk15.green.bold(" Decision: \u2705 ALLOW") + chalk15.gray(" \u2014 no approval needed"));
9182
+ console.log(chalk17.green.bold(" Decision: \u2705 ALLOW") + chalk17.gray(" \u2014 no approval needed"));
8299
9183
  } else {
8300
9184
  console.log(
8301
- chalk15.red.bold(" Decision: \u{1F534} REVIEW") + chalk15.gray(" \u2014 human approval required")
9185
+ chalk17.red.bold(" Decision: \u{1F534} REVIEW") + chalk17.gray(" \u2014 human approval required")
8302
9186
  );
8303
9187
  if (result.blockedByLabel) {
8304
- console.log(chalk15.gray(` Reason: ${result.blockedByLabel}`));
9188
+ console.log(chalk17.gray(` Reason: ${result.blockedByLabel}`));
8305
9189
  }
8306
9190
  }
8307
9191
  console.log("");
8308
9192
  });
8309
- 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) => {
8310
- const configPath = path23.join(os19.homedir(), ".node9", "config.json");
8311
- if (fs21.existsSync(configPath) && !options.force) {
8312
- console.log(chalk15.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
8313
- console.log(chalk15.gray(` Run with --force to overwrite.`));
8314
- return;
8315
- }
8316
- const requestedMode = options.mode.toLowerCase();
8317
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8318
- const configToSave = {
8319
- ...DEFAULT_CONFIG,
8320
- settings: {
8321
- ...DEFAULT_CONFIG.settings,
8322
- mode: safeMode
8323
- }
8324
- };
8325
- const dir = path23.dirname(configPath);
8326
- if (!fs21.existsSync(dir)) fs21.mkdirSync(dir, { recursive: true });
8327
- fs21.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8328
- console.log(chalk15.green(`\u2705 Global config created: ${configPath}`));
8329
- console.log(chalk15.cyan(` Mode set to: ${safeMode}`));
8330
- console.log(
8331
- chalk15.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
8332
- );
8333
- });
9193
+ registerInitCommand(program);
8334
9194
  registerAuditCommand(program);
8335
9195
  registerStatusCommand(program);
8336
9196
  registerDaemonCommand(program);
@@ -8339,7 +9199,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
8339
9199
  try {
8340
9200
  await startTail2(options);
8341
9201
  } catch (err) {
8342
- console.error(chalk15.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
9202
+ console.error(chalk17.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
8343
9203
  process.exit(1);
8344
9204
  }
8345
9205
  });
@@ -8351,7 +9211,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8351
9211
  const ms = parseDuration(options.duration);
8352
9212
  if (ms === null) {
8353
9213
  console.error(
8354
- chalk15.red(`
9214
+ chalk17.red(`
8355
9215
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
8356
9216
  `)
8357
9217
  );
@@ -8359,20 +9219,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8359
9219
  }
8360
9220
  pauseNode9(ms, options.duration);
8361
9221
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
8362
- console.log(chalk15.yellow(`
9222
+ console.log(chalk17.yellow(`
8363
9223
  \u23F8 Node9 paused until ${expiresAt}`));
8364
- console.log(chalk15.gray(` All tool calls will be allowed without review.`));
8365
- console.log(chalk15.gray(` Run "node9 resume" to re-enable early.
9224
+ console.log(chalk17.gray(` All tool calls will be allowed without review.`));
9225
+ console.log(chalk17.gray(` Run "node9 resume" to re-enable early.
8366
9226
  `));
8367
9227
  });
8368
9228
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
8369
9229
  const { paused } = checkPause();
8370
9230
  if (!paused) {
8371
- console.log(chalk15.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
9231
+ console.log(chalk17.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
8372
9232
  return;
8373
9233
  }
8374
9234
  resumeNode9();
8375
- console.log(chalk15.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
9235
+ console.log(chalk17.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
8376
9236
  });
8377
9237
  var HOOK_BASED_AGENTS = {
8378
9238
  claude: "claude",
@@ -8385,15 +9245,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8385
9245
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
8386
9246
  const target = HOOK_BASED_AGENTS[firstArg2];
8387
9247
  console.error(
8388
- chalk15.yellow(`
9248
+ chalk17.yellow(`
8389
9249
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
8390
9250
  );
8391
- console.error(chalk15.white(`
9251
+ console.error(chalk17.white(`
8392
9252
  "${target}" uses its own hook system. Use:`));
8393
9253
  console.error(
8394
- chalk15.green(` node9 addto ${target} `) + chalk15.gray("# one-time setup")
9254
+ chalk17.green(` node9 addto ${target} `) + chalk17.gray("# one-time setup")
8395
9255
  );
8396
- console.error(chalk15.green(` ${target} `) + chalk15.gray("# run normally"));
9256
+ console.error(chalk17.green(` ${target} `) + chalk17.gray("# run normally"));
8397
9257
  process.exit(1);
8398
9258
  }
8399
9259
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -8410,7 +9270,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8410
9270
  }
8411
9271
  );
8412
9272
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
8413
- console.error(chalk15.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
9273
+ console.error(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
8414
9274
  const daemonReady = await autoStartDaemonAndWait();
8415
9275
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
8416
9276
  }
@@ -8423,12 +9283,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8423
9283
  }
8424
9284
  if (!result.approved) {
8425
9285
  console.error(
8426
- chalk15.red(`
9286
+ chalk17.red(`
8427
9287
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
8428
9288
  );
8429
9289
  process.exit(1);
8430
9290
  }
8431
- console.error(chalk15.green("\n\u2705 Approved \u2014 running command...\n"));
9291
+ console.error(chalk17.green("\n\u2705 Approved \u2014 running command...\n"));
8432
9292
  await runProxy(fullCommand);
8433
9293
  } else {
8434
9294
  program.help();
@@ -8437,14 +9297,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8437
9297
  registerUndoCommand(program);
8438
9298
  registerShieldCommand(program);
8439
9299
  registerConfigShowCommand(program);
9300
+ registerTrustCommand(program);
8440
9301
  if (process.argv[2] !== "daemon") {
8441
9302
  process.on("unhandledRejection", (reason) => {
8442
9303
  const isCheckHook = process.argv[2] === "check";
8443
9304
  if (isCheckHook) {
8444
9305
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
8445
- const logPath = path23.join(os19.homedir(), ".node9", "hook-debug.log");
9306
+ const logPath = path26.join(os22.homedir(), ".node9", "hook-debug.log");
8446
9307
  const msg = reason instanceof Error ? reason.message : String(reason);
8447
- fs21.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9308
+ fs24.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
8448
9309
  `);
8449
9310
  }
8450
9311
  process.exit(0);