@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.js CHANGED
@@ -114,8 +114,8 @@ function sanitizeConfig(raw) {
114
114
  }
115
115
  }
116
116
  const lines = result.error.issues.map((issue) => {
117
- const path24 = issue.path.length > 0 ? issue.path.join(".") : "root";
118
- return ` \u2022 ${path24}: ${issue.message}`;
117
+ const path27 = issue.path.length > 0 ? issue.path.join(".") : "root";
118
+ return ` \u2022 ${path27}: ${issue.message}`;
119
119
  });
120
120
  return {
121
121
  sanitized,
@@ -1610,6 +1610,92 @@ var init_ssh_parser = __esm({
1610
1610
  }
1611
1611
  });
1612
1612
 
1613
+ // src/auth/trusted-hosts.ts
1614
+ function getTrustedHostsPath() {
1615
+ return import_path7.default.join(import_os5.default.homedir(), ".node9", "trusted-hosts.json");
1616
+ }
1617
+ function readTrustedHosts() {
1618
+ try {
1619
+ const raw = import_fs6.default.readFileSync(getTrustedHostsPath(), "utf8");
1620
+ const parsed = JSON.parse(raw);
1621
+ return Array.isArray(parsed.hosts) ? parsed.hosts : [];
1622
+ } catch {
1623
+ return [];
1624
+ }
1625
+ }
1626
+ function getFileMtime() {
1627
+ try {
1628
+ return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
1629
+ } catch {
1630
+ return 0;
1631
+ }
1632
+ }
1633
+ function getCachedHosts() {
1634
+ const now = Date.now();
1635
+ if (_cache && now < _cache.expiry) {
1636
+ const mtime = getFileMtime();
1637
+ if (mtime === _cache.mtime) return _cache.hosts;
1638
+ }
1639
+ const hosts = readTrustedHosts();
1640
+ _cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
1641
+ return hosts;
1642
+ }
1643
+ function writeTrustedHosts(hosts) {
1644
+ const filePath = getTrustedHostsPath();
1645
+ import_fs6.default.mkdirSync(import_path7.default.dirname(filePath), { recursive: true });
1646
+ const tmp = filePath + ".node9-tmp";
1647
+ import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
1648
+ import_fs6.default.renameSync(tmp, filePath);
1649
+ _cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
1650
+ }
1651
+ function addTrustedHost(host) {
1652
+ const normalized = normalizeHost(host);
1653
+ if (normalized.startsWith("*.")) {
1654
+ const base = normalized.slice(2);
1655
+ if (!base.includes(".")) {
1656
+ throw new Error(
1657
+ `Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
1658
+ );
1659
+ }
1660
+ }
1661
+ const hosts = readTrustedHosts();
1662
+ if (hosts.some((h) => h.host === normalized)) return;
1663
+ hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
1664
+ writeTrustedHosts(hosts);
1665
+ }
1666
+ function removeTrustedHost(host) {
1667
+ const hosts = readTrustedHosts();
1668
+ const filtered = hosts.filter((h) => h.host !== host);
1669
+ if (filtered.length === hosts.length) return false;
1670
+ writeTrustedHosts(filtered);
1671
+ return true;
1672
+ }
1673
+ function normalizeHost(raw) {
1674
+ return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
1675
+ }
1676
+ function isTrustedHost(host) {
1677
+ const normalized = normalizeHost(host);
1678
+ return getCachedHosts().some((entry) => {
1679
+ const entryHost = entry.host.toLowerCase();
1680
+ if (entryHost.startsWith("*.")) {
1681
+ const domain = entryHost.slice(2);
1682
+ return normalized.endsWith("." + domain);
1683
+ }
1684
+ return normalized === entryHost;
1685
+ });
1686
+ }
1687
+ var import_fs6, import_path7, import_os5, _cache, CACHE_TTL_MS;
1688
+ var init_trusted_hosts = __esm({
1689
+ "src/auth/trusted-hosts.ts"() {
1690
+ "use strict";
1691
+ import_fs6 = __toESM(require("fs"));
1692
+ import_path7 = __toESM(require("path"));
1693
+ import_os5 = __toESM(require("os"));
1694
+ _cache = null;
1695
+ CACHE_TTL_MS = 5e3;
1696
+ }
1697
+ });
1698
+
1613
1699
  // src/policy/index.ts
1614
1700
  function tokenize2(toolName) {
1615
1701
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
@@ -1624,9 +1710,9 @@ function matchesPattern(text, patterns) {
1624
1710
  const withoutDotSlash = text.replace(/^\.\//, "");
1625
1711
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1626
1712
  }
1627
- function getNestedValue(obj, path24) {
1713
+ function getNestedValue(obj, path27) {
1628
1714
  if (!obj || typeof obj !== "object") return null;
1629
- return path24.split(".").reduce((prev, curr) => prev?.[curr], obj);
1715
+ return path27.split(".").reduce((prev, curr) => prev?.[curr], obj);
1630
1716
  }
1631
1717
  function shouldSnapshot(toolName, args, config) {
1632
1718
  if (!config.settings.enableUndo) return false;
@@ -1792,23 +1878,39 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1792
1878
  return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
1793
1879
  }
1794
1880
  const pipeAnalysis = analyzePipeChain(shellCommand);
1795
- if (pipeAnalysis.isPipeline) {
1881
+ if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1882
+ const sinks = pipeAnalysis.sinkTargets;
1883
+ const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
1796
1884
  if (pipeAnalysis.risk === "critical") {
1885
+ if (allTrusted) {
1886
+ return {
1887
+ decision: "review",
1888
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1889
+ reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1890
+ tier: 3
1891
+ };
1892
+ }
1797
1893
  return {
1798
1894
  decision: "block",
1799
1895
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1800
- reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
1896
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1801
1897
  tier: 3
1802
1898
  };
1803
1899
  }
1804
- if (pipeAnalysis.risk === "high") {
1900
+ if (allTrusted) {
1805
1901
  return {
1806
- decision: "review",
1807
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1808
- reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${pipeAnalysis.sinkTargets.join(", ")}`,
1902
+ decision: "allow",
1903
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1904
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1809
1905
  tier: 3
1810
1906
  };
1811
1907
  }
1908
+ return {
1909
+ decision: "review",
1910
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1911
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1912
+ tier: 3
1913
+ };
1812
1914
  }
1813
1915
  const firstToken = analyzed.actions[0] ?? "";
1814
1916
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
@@ -1816,7 +1918,7 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1816
1918
  const sshHosts = extractAllSshHosts(rawTokens.slice(1));
1817
1919
  allTokens.push(...sshHosts);
1818
1920
  }
1819
- if (firstToken && import_path7.default.posix.isAbsolute(firstToken)) {
1921
+ if (firstToken && import_path8.default.posix.isAbsolute(firstToken)) {
1820
1922
  const prov = checkProvenance(firstToken, cwd);
1821
1923
  if (prov.trustLevel === "suspect") {
1822
1924
  return {
@@ -1913,9 +2015,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
1913
2015
  }
1914
2016
  async function explainPolicy(toolName, args) {
1915
2017
  const steps = [];
1916
- const globalPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "config.json");
1917
- const projectPath = import_path7.default.join(process.cwd(), "node9.config.json");
1918
- const credsPath = import_path7.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
2018
+ const globalPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
2019
+ const projectPath = import_path8.default.join(process.cwd(), "node9.config.json");
2020
+ const credsPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "credentials.json");
1919
2021
  const waterfall = [
1920
2022
  {
1921
2023
  tier: 1,
@@ -1926,19 +2028,19 @@ async function explainPolicy(toolName, args) {
1926
2028
  {
1927
2029
  tier: 2,
1928
2030
  label: "Cloud policy",
1929
- status: import_fs6.default.existsSync(credsPath) ? "active" : "missing",
1930
- note: import_fs6.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
2031
+ status: import_fs7.default.existsSync(credsPath) ? "active" : "missing",
2032
+ note: import_fs7.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
1931
2033
  },
1932
2034
  {
1933
2035
  tier: 3,
1934
2036
  label: "Project config",
1935
- status: import_fs6.default.existsSync(projectPath) ? "active" : "missing",
2037
+ status: import_fs7.default.existsSync(projectPath) ? "active" : "missing",
1936
2038
  path: projectPath
1937
2039
  },
1938
2040
  {
1939
2041
  tier: 4,
1940
2042
  label: "Global config",
1941
- status: import_fs6.default.existsSync(globalPath) ? "active" : "missing",
2043
+ status: import_fs7.default.existsSync(globalPath) ? "active" : "missing",
1942
2044
  path: globalPath
1943
2045
  },
1944
2046
  {
@@ -2175,13 +2277,13 @@ function isIgnoredTool(toolName) {
2175
2277
  const config = getConfig();
2176
2278
  return matchesPattern(toolName, config.policy.ignoredTools);
2177
2279
  }
2178
- var import_fs6, import_path7, import_os5, import_picomatch, import_sh_syntax, SQL_DML_KEYWORDS;
2280
+ var import_fs7, import_path8, import_os6, import_picomatch, import_sh_syntax, SQL_DML_KEYWORDS;
2179
2281
  var init_policy = __esm({
2180
2282
  "src/policy/index.ts"() {
2181
2283
  "use strict";
2182
- import_fs6 = __toESM(require("fs"));
2183
- import_path7 = __toESM(require("path"));
2184
- import_os5 = __toESM(require("os"));
2284
+ import_fs7 = __toESM(require("fs"));
2285
+ import_path8 = __toESM(require("path"));
2286
+ import_os6 = __toESM(require("os"));
2185
2287
  import_picomatch = __toESM(require("picomatch"));
2186
2288
  import_sh_syntax = require("sh-syntax");
2187
2289
  init_dlp();
@@ -2190,6 +2292,7 @@ var init_policy = __esm({
2190
2292
  init_provenance();
2191
2293
  init_pipe_chain();
2192
2294
  init_ssh_parser();
2295
+ init_trusted_hosts();
2193
2296
  SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2194
2297
  }
2195
2298
  });
@@ -2197,11 +2300,11 @@ var init_policy = __esm({
2197
2300
  // src/auth/state.ts
2198
2301
  function checkPause() {
2199
2302
  try {
2200
- if (!import_fs7.default.existsSync(PAUSED_FILE)) return { paused: false };
2201
- const state = JSON.parse(import_fs7.default.readFileSync(PAUSED_FILE, "utf-8"));
2303
+ if (!import_fs8.default.existsSync(PAUSED_FILE)) return { paused: false };
2304
+ const state = JSON.parse(import_fs8.default.readFileSync(PAUSED_FILE, "utf-8"));
2202
2305
  if (state.expiry > 0 && Date.now() >= state.expiry) {
2203
2306
  try {
2204
- import_fs7.default.unlinkSync(PAUSED_FILE);
2307
+ import_fs8.default.unlinkSync(PAUSED_FILE);
2205
2308
  } catch {
2206
2309
  }
2207
2310
  return { paused: false };
@@ -2212,11 +2315,11 @@ function checkPause() {
2212
2315
  }
2213
2316
  }
2214
2317
  function atomicWriteSync(filePath, data, options) {
2215
- const dir = import_path8.default.dirname(filePath);
2216
- if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
2217
- const tmpPath = `${filePath}.${import_os6.default.hostname()}.${process.pid}.tmp`;
2218
- import_fs7.default.writeFileSync(tmpPath, data, options);
2219
- import_fs7.default.renameSync(tmpPath, filePath);
2318
+ const dir = import_path9.default.dirname(filePath);
2319
+ if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
2320
+ const tmpPath = `${filePath}.${import_os7.default.hostname()}.${process.pid}.tmp`;
2321
+ import_fs8.default.writeFileSync(tmpPath, data, options);
2322
+ import_fs8.default.renameSync(tmpPath, filePath);
2220
2323
  }
2221
2324
  function pauseNode9(durationMs, durationStr) {
2222
2325
  const state = { expiry: Date.now() + durationMs, duration: durationStr };
@@ -2224,18 +2327,18 @@ function pauseNode9(durationMs, durationStr) {
2224
2327
  }
2225
2328
  function resumeNode9() {
2226
2329
  try {
2227
- if (import_fs7.default.existsSync(PAUSED_FILE)) import_fs7.default.unlinkSync(PAUSED_FILE);
2330
+ if (import_fs8.default.existsSync(PAUSED_FILE)) import_fs8.default.unlinkSync(PAUSED_FILE);
2228
2331
  } catch {
2229
2332
  }
2230
2333
  }
2231
2334
  function getActiveTrustSession(toolName) {
2232
2335
  try {
2233
- if (!import_fs7.default.existsSync(TRUST_FILE)) return false;
2234
- const trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
2336
+ if (!import_fs8.default.existsSync(TRUST_FILE)) return false;
2337
+ const trust = JSON.parse(import_fs8.default.readFileSync(TRUST_FILE, "utf-8"));
2235
2338
  const now = Date.now();
2236
2339
  const active = trust.entries.filter((e) => e.expiry > now);
2237
2340
  if (active.length !== trust.entries.length) {
2238
- import_fs7.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
2341
+ import_fs8.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
2239
2342
  }
2240
2343
  return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
2241
2344
  } catch {
@@ -2246,8 +2349,8 @@ function writeTrustSession(toolName, durationMs) {
2246
2349
  try {
2247
2350
  let trust = { entries: [] };
2248
2351
  try {
2249
- if (import_fs7.default.existsSync(TRUST_FILE)) {
2250
- trust = JSON.parse(import_fs7.default.readFileSync(TRUST_FILE, "utf-8"));
2352
+ if (import_fs8.default.existsSync(TRUST_FILE)) {
2353
+ trust = JSON.parse(import_fs8.default.readFileSync(TRUST_FILE, "utf-8"));
2251
2354
  }
2252
2355
  } catch {
2253
2356
  }
@@ -2263,34 +2366,34 @@ function writeTrustSession(toolName, durationMs) {
2263
2366
  }
2264
2367
  function getPersistentDecision(toolName) {
2265
2368
  try {
2266
- const file = import_path8.default.join(import_os6.default.homedir(), ".node9", "decisions.json");
2267
- if (!import_fs7.default.existsSync(file)) return null;
2268
- const decisions = JSON.parse(import_fs7.default.readFileSync(file, "utf-8"));
2369
+ const file = import_path9.default.join(import_os7.default.homedir(), ".node9", "decisions.json");
2370
+ if (!import_fs8.default.existsSync(file)) return null;
2371
+ const decisions = JSON.parse(import_fs8.default.readFileSync(file, "utf-8"));
2269
2372
  const d = decisions[toolName];
2270
2373
  if (d === "allow" || d === "deny") return d;
2271
2374
  } catch {
2272
2375
  }
2273
2376
  return null;
2274
2377
  }
2275
- var import_fs7, import_path8, import_os6, PAUSED_FILE, TRUST_FILE;
2378
+ var import_fs8, import_path9, import_os7, PAUSED_FILE, TRUST_FILE;
2276
2379
  var init_state = __esm({
2277
2380
  "src/auth/state.ts"() {
2278
2381
  "use strict";
2279
- import_fs7 = __toESM(require("fs"));
2280
- import_path8 = __toESM(require("path"));
2281
- import_os6 = __toESM(require("os"));
2382
+ import_fs8 = __toESM(require("fs"));
2383
+ import_path9 = __toESM(require("path"));
2384
+ import_os7 = __toESM(require("os"));
2282
2385
  init_policy();
2283
- PAUSED_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "PAUSED");
2284
- TRUST_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "trust.json");
2386
+ PAUSED_FILE = import_path9.default.join(import_os7.default.homedir(), ".node9", "PAUSED");
2387
+ TRUST_FILE = import_path9.default.join(import_os7.default.homedir(), ".node9", "trust.json");
2285
2388
  }
2286
2389
  });
2287
2390
 
2288
2391
  // src/auth/daemon.ts
2289
2392
  function getInternalToken() {
2290
2393
  try {
2291
- const pidFile = import_path9.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
2292
- if (!import_fs8.default.existsSync(pidFile)) return null;
2293
- const data = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
2394
+ const pidFile = import_path10.default.join(import_os8.default.homedir(), ".node9", "daemon.pid");
2395
+ if (!import_fs9.default.existsSync(pidFile)) return null;
2396
+ const data = JSON.parse(import_fs9.default.readFileSync(pidFile, "utf-8"));
2294
2397
  process.kill(data.pid, 0);
2295
2398
  return data.internalToken ?? null;
2296
2399
  } catch {
@@ -2298,10 +2401,10 @@ function getInternalToken() {
2298
2401
  }
2299
2402
  }
2300
2403
  function isDaemonRunning() {
2301
- const pidFile = import_path9.default.join(import_os7.default.homedir(), ".node9", "daemon.pid");
2302
- if (import_fs8.default.existsSync(pidFile)) {
2404
+ const pidFile = import_path10.default.join(import_os8.default.homedir(), ".node9", "daemon.pid");
2405
+ if (import_fs9.default.existsSync(pidFile)) {
2303
2406
  try {
2304
- const { pid, port } = JSON.parse(import_fs8.default.readFileSync(pidFile, "utf-8"));
2407
+ const { pid, port } = JSON.parse(import_fs9.default.readFileSync(pidFile, "utf-8"));
2305
2408
  if (port !== DAEMON_PORT) return false;
2306
2409
  process.kill(pid, 0);
2307
2410
  return true;
@@ -2342,8 +2445,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2342
2445
  signal: ctrl.signal
2343
2446
  });
2344
2447
  if (!res.ok) throw new Error("Daemon fail");
2345
- const { id } = await res.json();
2346
- return id;
2448
+ const { id, allowCount } = await res.json();
2449
+ return { id, allowCount: allowCount ?? 1 };
2347
2450
  } finally {
2348
2451
  clearTimeout(timer);
2349
2452
  }
@@ -2382,25 +2485,25 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
2382
2485
  signal: AbortSignal.timeout(3e3)
2383
2486
  });
2384
2487
  if (!res.ok) throw new Error("Daemon unreachable");
2385
- const { id } = await res.json();
2386
- return id;
2488
+ const { id, allowCount } = await res.json();
2489
+ return { id, allowCount: allowCount ?? 1 };
2387
2490
  }
2388
- async function resolveViaDaemon(id, decision, internalToken) {
2491
+ async function resolveViaDaemon(id, decision, internalToken, source) {
2389
2492
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2390
2493
  await fetch(`${base}/resolve/${id}`, {
2391
2494
  method: "POST",
2392
2495
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
2393
- body: JSON.stringify({ decision }),
2496
+ body: JSON.stringify({ decision, ...source && { source } }),
2394
2497
  signal: AbortSignal.timeout(3e3)
2395
2498
  });
2396
2499
  }
2397
- var import_fs8, import_path9, import_os7, import_child_process, DAEMON_PORT, DAEMON_HOST;
2500
+ var import_fs9, import_path10, import_os8, import_child_process, DAEMON_PORT, DAEMON_HOST;
2398
2501
  var init_daemon = __esm({
2399
2502
  "src/auth/daemon.ts"() {
2400
2503
  "use strict";
2401
- import_fs8 = __toESM(require("fs"));
2402
- import_path9 = __toESM(require("path"));
2403
- import_os7 = __toESM(require("os"));
2504
+ import_fs9 = __toESM(require("fs"));
2505
+ import_path10 = __toESM(require("path"));
2506
+ import_os8 = __toESM(require("os"));
2404
2507
  import_child_process = require("child_process");
2405
2508
  DAEMON_PORT = 7391;
2406
2509
  DAEMON_HOST = "127.0.0.1";
@@ -2459,7 +2562,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
2459
2562
  intent = "EDIT";
2460
2563
  if (obj.file_path) {
2461
2564
  editFilePath = String(obj.file_path);
2462
- editFileName = import_path10.default.basename(editFilePath);
2565
+ editFileName = import_path11.default.basename(editFilePath);
2463
2566
  }
2464
2567
  const result = extractContext(String(obj.new_string), matchedWord);
2465
2568
  contextSnippet = result.snippet;
@@ -2491,11 +2594,11 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
2491
2594
  ...ruleName && { ruleName }
2492
2595
  };
2493
2596
  }
2494
- var import_path10, CODE_KEYS;
2597
+ var import_path11, CODE_KEYS;
2495
2598
  var init_context_sniper = __esm({
2496
2599
  "src/context-sniper.ts"() {
2497
2600
  "use strict";
2498
- import_path10 = __toESM(require("path"));
2601
+ import_path11 = __toESM(require("path"));
2499
2602
  CODE_KEYS = [
2500
2603
  "command",
2501
2604
  "cmd",
@@ -2534,7 +2637,7 @@ function formatArgs(args, matchedField, matchedWord) {
2534
2637
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
2535
2638
  const obj = parsed;
2536
2639
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
2537
- const file = obj.file_path ? import_path11.default.basename(String(obj.file_path)) : "file";
2640
+ const file = obj.file_path ? import_path12.default.basename(String(obj.file_path)) : "file";
2538
2641
  const oldPreview = smartTruncate(String(obj.old_string), 120);
2539
2642
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
2540
2643
  return {
@@ -2597,20 +2700,24 @@ ${smartTruncate(str, 500)}`
2597
2700
  function escapePango(text) {
2598
2701
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2599
2702
  }
2600
- function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2703
+ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2601
2704
  const lines = [];
2602
2705
  if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
2603
2706
  lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
2604
2707
  lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
2605
2708
  lines.push("");
2606
2709
  lines.push(formattedArgs);
2710
+ if (allowCount >= 3) {
2711
+ lines.push("");
2712
+ lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
2713
+ }
2607
2714
  if (!locked) {
2608
2715
  lines.push("");
2609
2716
  lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
2610
2717
  }
2611
2718
  return lines.join("\n");
2612
2719
  }
2613
- function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
2720
+ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
2614
2721
  const lines = [];
2615
2722
  if (locked) {
2616
2723
  lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
@@ -2622,6 +2729,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2622
2729
  lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
2623
2730
  lines.push("");
2624
2731
  lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
2732
+ if (allowCount >= 3) {
2733
+ lines.push("");
2734
+ lines.push(
2735
+ `<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
2736
+ );
2737
+ }
2625
2738
  if (!locked) {
2626
2739
  lines.push("");
2627
2740
  lines.push(
@@ -2630,12 +2743,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
2630
2743
  }
2631
2744
  return lines.join("\n");
2632
2745
  }
2633
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
2746
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
2634
2747
  if (isTestEnv()) return "deny";
2635
2748
  const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
2636
2749
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
2637
2750
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
2638
- const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
2751
+ const message = buildPlainMessage(
2752
+ toolName,
2753
+ formattedArgs,
2754
+ agent,
2755
+ explainableLabel,
2756
+ locked,
2757
+ allowCount
2758
+ );
2639
2759
  return new Promise((resolve) => {
2640
2760
  let childProcess = null;
2641
2761
  const onAbort = () => {
@@ -2667,7 +2787,8 @@ end run`;
2667
2787
  formattedArgs,
2668
2788
  agent,
2669
2789
  explainableLabel,
2670
- locked
2790
+ locked,
2791
+ allowCount
2671
2792
  );
2672
2793
  const argsList = [
2673
2794
  locked ? "--info" : "--question",
@@ -2707,12 +2828,12 @@ end run`;
2707
2828
  }
2708
2829
  });
2709
2830
  }
2710
- var import_child_process2, import_path11, isTestEnv;
2831
+ var import_child_process2, import_path12, isTestEnv;
2711
2832
  var init_native = __esm({
2712
2833
  "src/ui/native.ts"() {
2713
2834
  "use strict";
2714
2835
  import_child_process2 = require("child_process");
2715
- import_path11 = __toESM(require("path"));
2836
+ import_path12 = __toESM(require("path"));
2716
2837
  init_context_sniper();
2717
2838
  isTestEnv = () => {
2718
2839
  return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
@@ -2732,9 +2853,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
2732
2853
  context: {
2733
2854
  agent: meta?.agent,
2734
2855
  mcpServer: meta?.mcpServer,
2735
- hostname: import_os8.default.hostname(),
2856
+ hostname: import_os9.default.hostname(),
2736
2857
  cwd: process.cwd(),
2737
- platform: import_os8.default.platform()
2858
+ platform: import_os9.default.platform()
2738
2859
  }
2739
2860
  }),
2740
2861
  signal: AbortSignal.timeout(5e3)
@@ -2755,9 +2876,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
2755
2876
  context: {
2756
2877
  agent: meta?.agent,
2757
2878
  mcpServer: meta?.mcpServer,
2758
- hostname: import_os8.default.hostname(),
2879
+ hostname: import_os9.default.hostname(),
2759
2880
  cwd: process.cwd(),
2760
- platform: import_os8.default.platform()
2881
+ platform: import_os9.default.platform()
2761
2882
  },
2762
2883
  ...riskMetadata && { riskMetadata }
2763
2884
  }),
@@ -2813,26 +2934,26 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2813
2934
  });
2814
2935
  clearTimeout(timer);
2815
2936
  if (!res.ok) {
2816
- import_fs9.default.appendFileSync(
2937
+ import_fs10.default.appendFileSync(
2817
2938
  HOOK_DEBUG_LOG,
2818
2939
  `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
2819
2940
  `
2820
2941
  );
2821
2942
  }
2822
2943
  } catch (err) {
2823
- import_fs9.default.appendFileSync(
2944
+ import_fs10.default.appendFileSync(
2824
2945
  HOOK_DEBUG_LOG,
2825
2946
  `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
2826
2947
  `
2827
2948
  );
2828
2949
  }
2829
2950
  }
2830
- var import_fs9, import_os8;
2951
+ var import_fs10, import_os9;
2831
2952
  var init_cloud = __esm({
2832
2953
  "src/auth/cloud.ts"() {
2833
2954
  "use strict";
2834
- import_fs9 = __toESM(require("fs"));
2835
- import_os8 = __toESM(require("os"));
2955
+ import_fs10 = __toESM(require("fs"));
2956
+ import_os9 = __toESM(require("os"));
2836
2957
  init_audit();
2837
2958
  }
2838
2959
  });
@@ -3030,13 +3151,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3030
3151
  let viewerId = null;
3031
3152
  const internalToken = getInternalToken();
3032
3153
  let daemonEntryId = null;
3154
+ let daemonAllowCount = 1;
3033
3155
  if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
3034
3156
  if (cloudEnforced && cloudRequestId) {
3035
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3157
+ const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
3158
+ viewerId = viewer?.id ?? null;
3036
3159
  daemonEntryId = viewerId;
3160
+ if (viewer) daemonAllowCount = viewer.allowCount;
3037
3161
  } else {
3038
3162
  try {
3039
- daemonEntryId = await registerDaemonEntry(
3163
+ const entry = await registerDaemonEntry(
3040
3164
  toolName,
3041
3165
  args,
3042
3166
  meta,
@@ -3044,6 +3168,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3044
3168
  options?.activityId,
3045
3169
  options?.cwd
3046
3170
  );
3171
+ daemonEntryId = entry.id;
3172
+ daemonAllowCount = entry.allowCount;
3047
3173
  } catch {
3048
3174
  }
3049
3175
  }
@@ -3079,7 +3205,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3079
3205
  false,
3080
3206
  signal,
3081
3207
  policyMatchedField,
3082
- policyMatchedWord
3208
+ policyMatchedWord,
3209
+ daemonAllowCount
3083
3210
  );
3084
3211
  if (decision === "always_allow") {
3085
3212
  writeTrustSession(toolName, 36e5);
@@ -3137,10 +3264,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3137
3264
  if (!resolved) {
3138
3265
  resolved = true;
3139
3266
  abortController.abort();
3140
- if (viewerId && internalToken) {
3141
- resolveViaDaemon(viewerId, res.approved ? "allow" : "deny", internalToken).catch(
3142
- () => null
3143
- );
3267
+ if (daemonEntryId && internalToken) {
3268
+ resolveViaDaemon(
3269
+ daemonEntryId,
3270
+ res.approved ? "allow" : "deny",
3271
+ internalToken,
3272
+ res.decisionSource
3273
+ ).catch(() => null);
3144
3274
  }
3145
3275
  resolve(res);
3146
3276
  }
@@ -3184,13 +3314,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
3184
3314
  }
3185
3315
  return finalResult;
3186
3316
  }
3187
- var import_net, import_path12, import_os9, import_crypto2, ACTIVITY_SOCKET_PATH;
3317
+ var import_net, import_path13, import_os10, import_crypto2, ACTIVITY_SOCKET_PATH;
3188
3318
  var init_orchestrator = __esm({
3189
3319
  "src/auth/orchestrator.ts"() {
3190
3320
  "use strict";
3191
3321
  import_net = __toESM(require("net"));
3192
- import_path12 = __toESM(require("path"));
3193
- import_os9 = __toESM(require("os"));
3322
+ import_path13 = __toESM(require("path"));
3323
+ import_os10 = __toESM(require("os"));
3194
3324
  import_crypto2 = require("crypto");
3195
3325
  init_native();
3196
3326
  init_context_sniper();
@@ -3201,7 +3331,7 @@ var init_orchestrator = __esm({
3201
3331
  init_state();
3202
3332
  init_daemon();
3203
3333
  init_cloud();
3204
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path12.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
3334
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os10.default.tmpdir(), "node9-activity.sock");
3205
3335
  }
3206
3336
  });
3207
3337
 
@@ -3497,6 +3627,15 @@ var init_ui = __esm({
3497
3627
  padding: 5px 10px;
3498
3628
  margin-bottom: 14px;
3499
3629
  }
3630
+ .insight-hint {
3631
+ font-size: 12px;
3632
+ color: #f0c040;
3633
+ background: rgba(240, 192, 64, 0.08);
3634
+ border: 1px solid rgba(240, 192, 64, 0.25);
3635
+ border-radius: 6px;
3636
+ padding: 6px 10px;
3637
+ margin-bottom: 12px;
3638
+ }
3500
3639
  pre {
3501
3640
  background: #0d1117;
3502
3641
  padding: 14px 16px;
@@ -3969,6 +4108,78 @@ var init_ui = __esm({
3969
4108
  color: var(--danger);
3970
4109
  }
3971
4110
 
4111
+ /* \u2500\u2500 Suggestion cards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4112
+ .suggestion-card {
4113
+ background: rgba(82, 130, 255, 0.06);
4114
+ border: 1px solid rgba(82, 130, 255, 0.25);
4115
+ border-radius: 8px;
4116
+ padding: 10px 12px;
4117
+ margin-bottom: 8px;
4118
+ }
4119
+ .suggestion-card:last-child {
4120
+ margin-bottom: 0;
4121
+ }
4122
+ .suggestion-header {
4123
+ display: flex;
4124
+ align-items: center;
4125
+ gap: 8px;
4126
+ margin-bottom: 6px;
4127
+ }
4128
+ .suggestion-tool {
4129
+ font-family: 'Fira Code', monospace;
4130
+ font-size: 11px;
4131
+ color: var(--text-bright);
4132
+ flex: 1;
4133
+ word-break: break-all;
4134
+ }
4135
+ .suggestion-count {
4136
+ font-size: 10px;
4137
+ color: var(--muted);
4138
+ white-space: nowrap;
4139
+ }
4140
+ .suggestion-rule {
4141
+ font-family: 'Fira Code', monospace;
4142
+ font-size: 10px;
4143
+ color: #79c0ff;
4144
+ background: rgba(0, 0, 0, 0.25);
4145
+ border-radius: 4px;
4146
+ padding: 4px 8px;
4147
+ margin-bottom: 8px;
4148
+ word-break: break-all;
4149
+ white-space: pre-wrap;
4150
+ }
4151
+ .suggestion-actions {
4152
+ display: flex;
4153
+ gap: 6px;
4154
+ }
4155
+ .btn-apply {
4156
+ background: rgba(52, 125, 57, 0.2);
4157
+ border: 1px solid rgba(87, 171, 90, 0.4);
4158
+ color: #57ab5a;
4159
+ padding: 4px 10px;
4160
+ font-size: 11px;
4161
+ border-radius: 5px;
4162
+ font-family: inherit;
4163
+ cursor: pointer;
4164
+ }
4165
+ .btn-apply:hover {
4166
+ background: rgba(52, 125, 57, 0.35);
4167
+ }
4168
+ .btn-dismiss-suggestion {
4169
+ background: transparent;
4170
+ border: 1px solid var(--border);
4171
+ color: var(--muted);
4172
+ padding: 4px 10px;
4173
+ font-size: 11px;
4174
+ border-radius: 5px;
4175
+ font-family: inherit;
4176
+ cursor: pointer;
4177
+ }
4178
+ .btn-dismiss-suggestion:hover {
4179
+ border-color: var(--danger);
4180
+ color: var(--danger);
4181
+ }
4182
+
3972
4183
  .modal-overlay {
3973
4184
  display: none;
3974
4185
  position: fixed;
@@ -4150,6 +4361,11 @@ var init_ui = __esm({
4150
4361
  <div class="panel-title">\u{1F4CB} Persistent Decisions</div>
4151
4362
  <div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
4152
4363
  </div>
4364
+
4365
+ <div class="panel" id="suggestionsPanel" style="display: none">
4366
+ <div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
4367
+ <div id="suggestionsList"></div>
4368
+ </div>
4153
4369
  </div>
4154
4370
  </div>
4155
4371
  </div>
@@ -4339,6 +4555,7 @@ var init_ui = __esm({
4339
4555
  </div>
4340
4556
  <div class="tool-chip">\${esc(req.toolName)}</div>
4341
4557
  \${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
4558
+ \${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
4342
4559
  \${renderPayload(req)}
4343
4560
  <div class="actions" id="act-\${req.id}">
4344
4561
  <button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
@@ -4405,6 +4622,14 @@ var init_ui = __esm({
4405
4622
  ev.addEventListener('shields-status', (e) => {
4406
4623
  renderShields(JSON.parse(e.data).shields);
4407
4624
  });
4625
+ ev.addEventListener('suggestion:new', (e) => {
4626
+ const s = JSON.parse(e.data);
4627
+ addSuggestionCard(s);
4628
+ });
4629
+ ev.addEventListener('suggestion:resolved', (e) => {
4630
+ const { id } = JSON.parse(e.data);
4631
+ removeSuggestionCard(id);
4632
+ });
4408
4633
 
4409
4634
  // \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4410
4635
  ev.addEventListener('activity', (e) => {
@@ -4654,6 +4879,74 @@ var init_ui = __esm({
4654
4879
  .then((r) => r.json())
4655
4880
  .then(renderDecisions)
4656
4881
  .catch(() => {});
4882
+
4883
+ // \u2500\u2500 Smart Rule Suggestions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4884
+ function rulePreview(suggestion) {
4885
+ const r = suggestion.suggestedRule;
4886
+ if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
4887
+ const cond = r.rule.conditions?.[0];
4888
+ const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
4889
+ return \`allow \${r.rule.tool}\${condStr}\`;
4890
+ }
4891
+
4892
+ function addSuggestionCard(s) {
4893
+ const panel = document.getElementById('suggestionsPanel');
4894
+ const list = document.getElementById('suggestionsList');
4895
+ panel.style.display = '';
4896
+
4897
+ const card = document.createElement('div');
4898
+ card.className = 'suggestion-card';
4899
+ card.id = 'sg-' + s.id;
4900
+ card.innerHTML = \`
4901
+ <div class="suggestion-header">
4902
+ <span class="suggestion-tool">\${esc(s.toolName)}</span>
4903
+ <span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
4904
+ </div>
4905
+ <div class="suggestion-rule">\${esc(rulePreview(s))}</div>
4906
+ <div class="suggestion-actions">
4907
+ <button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
4908
+ <button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
4909
+ </div>
4910
+ \`;
4911
+ list.appendChild(card);
4912
+ }
4913
+
4914
+ function removeSuggestionCard(id) {
4915
+ document.getElementById('sg-' + id)?.remove();
4916
+ const list = document.getElementById('suggestionsList');
4917
+ if (!list.querySelector('.suggestion-card')) {
4918
+ document.getElementById('suggestionsPanel').style.display = 'none';
4919
+ }
4920
+ }
4921
+
4922
+ function applySuggestion(id) {
4923
+ fetch('/suggestions/' + id + '/apply', {
4924
+ method: 'POST',
4925
+ headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
4926
+ body: JSON.stringify({}),
4927
+ })
4928
+ .then((r) => {
4929
+ if (r.ok) removeSuggestionCard(id);
4930
+ })
4931
+ .catch(() => {});
4932
+ }
4933
+
4934
+ function dismissSuggestion(id) {
4935
+ fetch('/suggestions/' + id + '/dismiss', {
4936
+ method: 'POST',
4937
+ headers: { 'X-Node9-Token': CSRF_TOKEN },
4938
+ })
4939
+ .then((r) => {
4940
+ if (r.ok) removeSuggestionCard(id);
4941
+ })
4942
+ .catch(() => {});
4943
+ }
4944
+
4945
+ // Load any suggestions that survived a page reload (daemon still running)
4946
+ fetch('/suggestions')
4947
+ .then((r) => r.json())
4948
+ .then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
4949
+ .catch(() => {});
4657
4950
  </script>
4658
4951
  </body>
4659
4952
  </html>
@@ -4671,7 +4964,117 @@ var init_ui2 = __esm({
4671
4964
  }
4672
4965
  });
4673
4966
 
4967
+ // src/daemon/suggestion-tracker.ts
4968
+ function extractPath(args) {
4969
+ if (!args || typeof args !== "object") return null;
4970
+ const a = args;
4971
+ for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
4972
+ if (typeof a[key] === "string" && a[key]) return a[key];
4973
+ }
4974
+ return null;
4975
+ }
4976
+ function commonPathPrefix(paths) {
4977
+ if (paths.length < 2) return null;
4978
+ const dirParts = paths.map((p) => {
4979
+ const lastSlash = p.lastIndexOf("/");
4980
+ return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
4981
+ });
4982
+ const first = dirParts[0].split("/");
4983
+ const common = [];
4984
+ for (let i = 0; i < first.length; i++) {
4985
+ if (dirParts.every((d) => d.split("/")[i] === first[i])) {
4986
+ common.push(first[i]);
4987
+ } else {
4988
+ break;
4989
+ }
4990
+ }
4991
+ const prefix = common.join("/").replace(/\/?$/, "/");
4992
+ return prefix.length > 1 ? prefix : null;
4993
+ }
4994
+ var import_crypto3, SuggestionTracker;
4995
+ var init_suggestion_tracker = __esm({
4996
+ "src/daemon/suggestion-tracker.ts"() {
4997
+ "use strict";
4998
+ import_crypto3 = require("crypto");
4999
+ SuggestionTracker = class {
5000
+ events = /* @__PURE__ */ new Map();
5001
+ threshold;
5002
+ constructor(threshold = 3) {
5003
+ this.threshold = threshold;
5004
+ }
5005
+ /**
5006
+ * Record a human-allowed review for a tool.
5007
+ * Returns a Suggestion when the threshold is reached, null otherwise.
5008
+ */
5009
+ recordAllow(toolName, args) {
5010
+ const events = this.events.get(toolName) ?? [];
5011
+ events.push({ args, ts: Date.now() });
5012
+ this.events.set(toolName, events);
5013
+ if (events.length >= this.threshold) {
5014
+ this.events.delete(toolName);
5015
+ return this.generateSuggestion(toolName, events);
5016
+ }
5017
+ return null;
5018
+ }
5019
+ /**
5020
+ * Reset the counter for a tool (e.g. when the user clicks Deny —
5021
+ * don't suggest allowing something they just blocked).
5022
+ */
5023
+ resetTool(toolName) {
5024
+ this.events.delete(toolName);
5025
+ }
5026
+ /** Current allow count for a tool (for tests). */
5027
+ getCount(toolName) {
5028
+ return this.events.get(toolName)?.length ?? 0;
5029
+ }
5030
+ generateSuggestion(toolName, events) {
5031
+ const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
5032
+ const prefix = commonPathPrefix(paths);
5033
+ const suggestedRule = prefix ? {
5034
+ type: "smartRule",
5035
+ rule: {
5036
+ name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
5037
+ tool: toolName,
5038
+ conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
5039
+ verdict: "allow",
5040
+ reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
5041
+ }
5042
+ } : { type: "ignoredTool", toolName };
5043
+ return {
5044
+ id: (0, import_crypto3.randomUUID)(),
5045
+ toolName,
5046
+ allowCount: events.length,
5047
+ suggestedRule,
5048
+ status: "pending",
5049
+ createdAt: Date.now(),
5050
+ exampleArgs: events.slice(0, 3).map((e) => e.args)
5051
+ };
5052
+ }
5053
+ };
5054
+ }
5055
+ });
5056
+
4674
5057
  // src/daemon/state.ts
5058
+ function loadInsightCounts() {
5059
+ try {
5060
+ if (!import_fs12.default.existsSync(INSIGHT_COUNTS_FILE)) return;
5061
+ const data = JSON.parse(import_fs12.default.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
5062
+ for (const [tool, count] of Object.entries(data)) {
5063
+ if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
5064
+ }
5065
+ } catch {
5066
+ }
5067
+ }
5068
+ function saveInsightCounts() {
5069
+ try {
5070
+ const data = {};
5071
+ insightCounts.forEach((count, tool) => {
5072
+ data[tool] = count;
5073
+ });
5074
+ atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
5075
+ } catch {
5076
+ }
5077
+ }
4675
5078
  function getAbandonTimer() {
4676
5079
  return _abandonTimer;
4677
5080
  }
@@ -4694,11 +5097,27 @@ function markRejectionHandlerRegistered() {
4694
5097
  daemonRejectionHandlerRegistered = true;
4695
5098
  }
4696
5099
  function atomicWriteSync2(filePath, data, options) {
4697
- const dir = import_path14.default.dirname(filePath);
4698
- if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
4699
- const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
4700
- import_fs11.default.writeFileSync(tmpPath, data, options);
4701
- import_fs11.default.renameSync(tmpPath, filePath);
5100
+ const dir = import_path15.default.dirname(filePath);
5101
+ if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
5102
+ const tmpPath = `${filePath}.${(0, import_crypto4.randomUUID)()}.tmp`;
5103
+ try {
5104
+ import_fs12.default.writeFileSync(tmpPath, data, options);
5105
+ } catch (err) {
5106
+ try {
5107
+ import_fs12.default.unlinkSync(tmpPath);
5108
+ } catch {
5109
+ }
5110
+ throw err;
5111
+ }
5112
+ try {
5113
+ import_fs12.default.renameSync(tmpPath, filePath);
5114
+ } catch (err) {
5115
+ try {
5116
+ import_fs12.default.unlinkSync(tmpPath);
5117
+ } catch {
5118
+ }
5119
+ throw err;
5120
+ }
4702
5121
  }
4703
5122
  function redactArgs(value) {
4704
5123
  if (!value || typeof value !== "object") return value;
@@ -4718,16 +5137,16 @@ function appendAuditLog(data) {
4718
5137
  decision: data.decision,
4719
5138
  source: "daemon"
4720
5139
  };
4721
- const dir = import_path14.default.dirname(AUDIT_LOG_FILE);
4722
- if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
4723
- import_fs11.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
5140
+ const dir = import_path15.default.dirname(AUDIT_LOG_FILE);
5141
+ if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
5142
+ import_fs12.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
4724
5143
  } catch {
4725
5144
  }
4726
5145
  }
4727
5146
  function getAuditHistory(limit = 20) {
4728
5147
  try {
4729
- if (!import_fs11.default.existsSync(AUDIT_LOG_FILE)) return [];
4730
- const lines = import_fs11.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
5148
+ if (!import_fs12.default.existsSync(AUDIT_LOG_FILE)) return [];
5149
+ const lines = import_fs12.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
4731
5150
  if (lines.length === 1 && lines[0] === "") return [];
4732
5151
  return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
4733
5152
  } catch {
@@ -4736,19 +5155,19 @@ function getAuditHistory(limit = 20) {
4736
5155
  }
4737
5156
  function getOrgName() {
4738
5157
  try {
4739
- if (import_fs11.default.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
5158
+ if (import_fs12.default.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
4740
5159
  } catch {
4741
5160
  }
4742
5161
  return null;
4743
5162
  }
4744
5163
  function hasStoredSlackKey() {
4745
- return import_fs11.default.existsSync(CREDENTIALS_FILE);
5164
+ return import_fs12.default.existsSync(CREDENTIALS_FILE);
4746
5165
  }
4747
5166
  function writeGlobalSetting(key, value) {
4748
5167
  let config = {};
4749
5168
  try {
4750
- if (import_fs11.default.existsSync(GLOBAL_CONFIG_FILE)) {
4751
- config = JSON.parse(import_fs11.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
5169
+ if (import_fs12.default.existsSync(GLOBAL_CONFIG_FILE)) {
5170
+ config = JSON.parse(import_fs12.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
4752
5171
  }
4753
5172
  } catch {
4754
5173
  }
@@ -4760,8 +5179,8 @@ function writeTrustEntry(toolName, durationMs) {
4760
5179
  try {
4761
5180
  let trust = { entries: [] };
4762
5181
  try {
4763
- if (import_fs11.default.existsSync(TRUST_FILE2))
4764
- trust = JSON.parse(import_fs11.default.readFileSync(TRUST_FILE2, "utf-8"));
5182
+ if (import_fs12.default.existsSync(TRUST_FILE2))
5183
+ trust = JSON.parse(import_fs12.default.readFileSync(TRUST_FILE2, "utf-8"));
4765
5184
  } catch {
4766
5185
  }
4767
5186
  trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
@@ -4772,8 +5191,8 @@ function writeTrustEntry(toolName, durationMs) {
4772
5191
  }
4773
5192
  function readPersistentDecisions() {
4774
5193
  try {
4775
- if (import_fs11.default.existsSync(DECISIONS_FILE)) {
4776
- return JSON.parse(import_fs11.default.readFileSync(DECISIONS_FILE, "utf-8"));
5194
+ if (import_fs12.default.existsSync(DECISIONS_FILE)) {
5195
+ return JSON.parse(import_fs12.default.readFileSync(DECISIONS_FILE, "utf-8"));
4777
5196
  }
4778
5197
  } catch {
4779
5198
  }
@@ -4838,7 +5257,7 @@ function abandonPending() {
4838
5257
  });
4839
5258
  if (autoStarted) {
4840
5259
  try {
4841
- import_fs11.default.unlinkSync(DAEMON_PID_FILE);
5260
+ import_fs12.default.unlinkSync(DAEMON_PID_FILE);
4842
5261
  } catch {
4843
5262
  }
4844
5263
  setTimeout(() => {
@@ -4849,7 +5268,7 @@ function abandonPending() {
4849
5268
  }
4850
5269
  function startActivitySocket() {
4851
5270
  try {
4852
- import_fs11.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
5271
+ import_fs12.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4853
5272
  } catch {
4854
5273
  }
4855
5274
  const ACTIVITY_MAX_BYTES = 1024 * 1024;
@@ -4891,31 +5310,36 @@ function startActivitySocket() {
4891
5310
  unixServer.listen(ACTIVITY_SOCKET_PATH2);
4892
5311
  process.on("exit", () => {
4893
5312
  try {
4894
- import_fs11.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
5313
+ import_fs12.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
4895
5314
  } catch {
4896
5315
  }
4897
5316
  });
4898
5317
  }
4899
- var import_net2, import_fs11, import_path14, import_os11, import_child_process3, import_crypto3, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, pending, sseClients, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
5318
+ var import_net2, import_fs12, import_path15, import_os12, import_child_process3, import_crypto4, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
4900
5319
  var init_state2 = __esm({
4901
5320
  "src/daemon/state.ts"() {
4902
5321
  "use strict";
4903
5322
  import_net2 = __toESM(require("net"));
4904
- import_fs11 = __toESM(require("fs"));
4905
- import_path14 = __toESM(require("path"));
4906
- import_os11 = __toESM(require("os"));
5323
+ import_fs12 = __toESM(require("fs"));
5324
+ import_path15 = __toESM(require("path"));
5325
+ import_os12 = __toESM(require("os"));
4907
5326
  import_child_process3 = require("child_process");
4908
- import_crypto3 = require("crypto");
5327
+ import_crypto4 = require("crypto");
4909
5328
  init_daemon();
4910
- homeDir = import_os11.default.homedir();
4911
- DAEMON_PID_FILE = import_path14.default.join(homeDir, ".node9", "daemon.pid");
4912
- DECISIONS_FILE = import_path14.default.join(homeDir, ".node9", "decisions.json");
4913
- AUDIT_LOG_FILE = import_path14.default.join(homeDir, ".node9", "audit.log");
4914
- TRUST_FILE2 = import_path14.default.join(homeDir, ".node9", "trust.json");
4915
- GLOBAL_CONFIG_FILE = import_path14.default.join(homeDir, ".node9", "config.json");
4916
- CREDENTIALS_FILE = import_path14.default.join(homeDir, ".node9", "credentials.json");
5329
+ init_suggestion_tracker();
5330
+ homeDir = import_os12.default.homedir();
5331
+ DAEMON_PID_FILE = import_path15.default.join(homeDir, ".node9", "daemon.pid");
5332
+ DECISIONS_FILE = import_path15.default.join(homeDir, ".node9", "decisions.json");
5333
+ AUDIT_LOG_FILE = import_path15.default.join(homeDir, ".node9", "audit.log");
5334
+ TRUST_FILE2 = import_path15.default.join(homeDir, ".node9", "trust.json");
5335
+ GLOBAL_CONFIG_FILE = import_path15.default.join(homeDir, ".node9", "config.json");
5336
+ CREDENTIALS_FILE = import_path15.default.join(homeDir, ".node9", "credentials.json");
5337
+ INSIGHT_COUNTS_FILE = import_path15.default.join(homeDir, ".node9", "insight-counts.json");
4917
5338
  pending = /* @__PURE__ */ new Map();
4918
5339
  sseClients = /* @__PURE__ */ new Set();
5340
+ suggestionTracker = new SuggestionTracker(3);
5341
+ suggestions = /* @__PURE__ */ new Map();
5342
+ insightCounts = /* @__PURE__ */ new Map();
4919
5343
  _abandonTimer = null;
4920
5344
  _hadBrowserClient = false;
4921
5345
  _daemonServer = null;
@@ -4927,17 +5351,75 @@ var init_state2 = __esm({
4927
5351
  "2h": 2 * 60 * 6e4
4928
5352
  };
4929
5353
  autoStarted = process.env.NODE9_AUTO_STARTED === "1";
4930
- ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path14.default.join(import_os11.default.tmpdir(), "node9-activity.sock");
5354
+ ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path15.default.join(import_os12.default.tmpdir(), "node9-activity.sock");
4931
5355
  ACTIVITY_RING_SIZE = 100;
4932
5356
  activityRing = [];
4933
5357
  SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
4934
5358
  }
4935
5359
  });
4936
5360
 
5361
+ // src/config/patch.ts
5362
+ function patchConfig(configPath, patch) {
5363
+ let config = {};
5364
+ try {
5365
+ if (import_fs13.default.existsSync(configPath)) {
5366
+ config = JSON.parse(import_fs13.default.readFileSync(configPath, "utf8"));
5367
+ }
5368
+ } catch {
5369
+ throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
5370
+ }
5371
+ if (!config.policy || typeof config.policy !== "object") config.policy = {};
5372
+ const policy = config.policy;
5373
+ if (patch.type === "smartRule") {
5374
+ if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
5375
+ const rules = policy.smartRules;
5376
+ if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
5377
+ rules.push(patch.rule);
5378
+ } else {
5379
+ if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
5380
+ const ignored = policy.ignoredTools;
5381
+ if (!ignored.includes(patch.toolName)) {
5382
+ ignored.push(patch.toolName);
5383
+ }
5384
+ }
5385
+ const dir = import_path16.default.dirname(configPath);
5386
+ import_fs13.default.mkdirSync(dir, { recursive: true });
5387
+ const tmp = configPath + ".node9-tmp";
5388
+ try {
5389
+ import_fs13.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
5390
+ } catch (err) {
5391
+ try {
5392
+ import_fs13.default.unlinkSync(tmp);
5393
+ } catch {
5394
+ }
5395
+ throw err;
5396
+ }
5397
+ try {
5398
+ import_fs13.default.renameSync(tmp, configPath);
5399
+ } catch (err) {
5400
+ try {
5401
+ import_fs13.default.unlinkSync(tmp);
5402
+ } catch {
5403
+ }
5404
+ throw err;
5405
+ }
5406
+ }
5407
+ var import_fs13, import_path16, import_os13, GLOBAL_CONFIG_PATH;
5408
+ var init_patch = __esm({
5409
+ "src/config/patch.ts"() {
5410
+ "use strict";
5411
+ import_fs13 = __toESM(require("fs"));
5412
+ import_path16 = __toESM(require("path"));
5413
+ import_os13 = __toESM(require("os"));
5414
+ GLOBAL_CONFIG_PATH = import_path16.default.join(import_os13.default.homedir(), ".node9", "config.json");
5415
+ }
5416
+ });
5417
+
4937
5418
  // src/daemon/server.ts
4938
5419
  function startDaemon() {
4939
- const csrfToken = (0, import_crypto4.randomUUID)();
4940
- const internalToken = (0, import_crypto4.randomUUID)();
5420
+ loadInsightCounts();
5421
+ const csrfToken = (0, import_crypto5.randomUUID)();
5422
+ const internalToken = (0, import_crypto5.randomUUID)();
4941
5423
  const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
4942
5424
  const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
4943
5425
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
@@ -4950,7 +5432,7 @@ function startDaemon() {
4950
5432
  idleTimer = setTimeout(() => {
4951
5433
  if (autoStarted) {
4952
5434
  try {
4953
- import_fs12.default.unlinkSync(DAEMON_PID_FILE);
5435
+ import_fs14.default.unlinkSync(DAEMON_PID_FILE);
4954
5436
  } catch {
4955
5437
  }
4956
5438
  }
@@ -4959,8 +5441,14 @@ function startDaemon() {
4959
5441
  idleTimer.unref();
4960
5442
  }
4961
5443
  resetIdleTimer();
5444
+ const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
4962
5445
  const server = import_http.default.createServer(async (req, res) => {
4963
- const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
5446
+ const host = req.headers.host ?? "";
5447
+ if (!allowedHosts.has(host)) {
5448
+ res.writeHead(421, { "Content-Type": "text/plain" });
5449
+ return res.end("Misdirected Request");
5450
+ }
5451
+ const reqUrl = new URL(req.url || "/", `http://${host}`);
4964
5452
  const { pathname } = reqUrl;
4965
5453
  if (req.method === "GET" && pathname === "/") {
4966
5454
  res.writeHead(200, { "Content-Type": "text/html" });
@@ -4993,7 +5481,8 @@ data: ${JSON.stringify({
4993
5481
  slackDelegated: e.slackDelegated,
4994
5482
  timestamp: e.timestamp,
4995
5483
  agent: e.agent,
4996
- mcpServer: e.mcpServer
5484
+ mcpServer: e.mcpServer,
5485
+ allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
4997
5486
  })),
4998
5487
  orgName: getOrgName(),
4999
5488
  autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
@@ -5035,6 +5524,12 @@ data: ${JSON.stringify(item.data)}
5035
5524
  }
5036
5525
  });
5037
5526
  }
5527
+ if (req.method === "POST" && pathname === "/browser-opened") {
5528
+ if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
5529
+ browserOpened = true;
5530
+ res.writeHead(200).end();
5531
+ return;
5532
+ }
5038
5533
  if (req.method === "POST" && pathname === "/check") {
5039
5534
  try {
5040
5535
  resetIdleTimer();
@@ -5052,7 +5547,7 @@ data: ${JSON.stringify(item.data)}
5052
5547
  activityId,
5053
5548
  cwd
5054
5549
  } = JSON.parse(body);
5055
- const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto4.randomUUID)();
5550
+ const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto5.randomUUID)();
5056
5551
  const entry = {
5057
5552
  id,
5058
5553
  toolName,
@@ -5078,7 +5573,7 @@ data: ${JSON.stringify(item.data)}
5078
5573
  e.earlyReason = "No response \u2014 auto-denied after timeout";
5079
5574
  }
5080
5575
  pending.delete(id);
5081
- broadcast("remove", { id });
5576
+ broadcast("remove", { id, decision: "deny" });
5082
5577
  }
5083
5578
  }, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
5084
5579
  };
@@ -5092,7 +5587,7 @@ data: ${JSON.stringify(item.data)}
5092
5587
  status: "pending"
5093
5588
  });
5094
5589
  }
5095
- const projectCwd = typeof cwd === "string" && import_path15.default.isAbsolute(cwd) ? cwd : void 0;
5590
+ const projectCwd = typeof cwd === "string" && import_path17.default.isAbsolute(cwd) ? cwd : void 0;
5096
5591
  const projectConfig = getConfig(projectCwd);
5097
5592
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
5098
5593
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -5105,7 +5600,10 @@ data: ${JSON.stringify(item.data)}
5105
5600
  slackDelegated: entry.slackDelegated,
5106
5601
  agent: entry.agent,
5107
5602
  mcpServer: entry.mcpServer,
5108
- interactive: terminalEnabled
5603
+ interactive: terminalEnabled,
5604
+ // allowCount = what this count will be if the user allows.
5605
+ // Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
5606
+ allowCount: (insightCounts.get(toolName) ?? 0) + 1
5109
5607
  });
5110
5608
  const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
5111
5609
  if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
@@ -5114,7 +5612,7 @@ data: ${JSON.stringify(item.data)}
5114
5612
  }
5115
5613
  }
5116
5614
  res.writeHead(200, { "Content-Type": "application/json" });
5117
- res.end(JSON.stringify({ id }));
5615
+ res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
5118
5616
  if (slackDelegated) return;
5119
5617
  authorizeHeadless(
5120
5618
  toolName,
@@ -5141,7 +5639,7 @@ data: ${JSON.stringify(item.data)}
5141
5639
  if (e.waiter) {
5142
5640
  e.waiter(decision, result.reason);
5143
5641
  pending.delete(id);
5144
- broadcast("remove", { id });
5642
+ broadcast("remove", { id, decision });
5145
5643
  } else {
5146
5644
  e.earlyDecision = decision;
5147
5645
  e.earlyReason = result.reason;
@@ -5157,7 +5655,7 @@ data: ${JSON.stringify(item.data)}
5157
5655
  e.earlyReason = reason;
5158
5656
  }
5159
5657
  pending.delete(id);
5160
- broadcast("remove", { id });
5658
+ broadcast("remove", { id, decision: "deny" });
5161
5659
  });
5162
5660
  return;
5163
5661
  } catch {
@@ -5188,12 +5686,14 @@ data: ${JSON.stringify(item.data)}
5188
5686
  res.end(JSON.stringify(body));
5189
5687
  };
5190
5688
  req.on("close", () => {
5191
- const e = pending.get(id);
5192
- if (e && e.waiter && e.earlyDecision === null) {
5193
- clearTimeout(e.timer);
5194
- pending.delete(id);
5195
- broadcast("remove", { id });
5196
- }
5689
+ setTimeout(() => {
5690
+ const e = pending.get(id);
5691
+ if (e && e.waiter && e.earlyDecision === null) {
5692
+ clearTimeout(e.timer);
5693
+ pending.delete(id);
5694
+ broadcast("remove", { id });
5695
+ }
5696
+ }, 200);
5197
5697
  });
5198
5698
  return;
5199
5699
  }
@@ -5222,10 +5722,10 @@ data: ${JSON.stringify(item.data)}
5222
5722
  if (entry.waiter) {
5223
5723
  entry.waiter("allow");
5224
5724
  pending.delete(id);
5225
- broadcast("remove", { id });
5725
+ broadcast("remove", { id, decision: "allow" });
5226
5726
  } else {
5227
5727
  entry.earlyDecision = "allow";
5228
- broadcast("remove", { id });
5728
+ broadcast("remove", { id, decision: "allow" });
5229
5729
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5230
5730
  }
5231
5731
  res.writeHead(200);
@@ -5239,16 +5739,29 @@ data: ${JSON.stringify(item.data)}
5239
5739
  decision: resolvedDecision
5240
5740
  });
5241
5741
  clearTimeout(entry.timer);
5742
+ if (resolvedDecision === "allow" && !persist) {
5743
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
5744
+ saveInsightCounts();
5745
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
5746
+ if (suggestion) {
5747
+ suggestions.set(suggestion.id, suggestion);
5748
+ broadcast("suggestion:new", suggestion);
5749
+ }
5750
+ } else if (resolvedDecision === "deny") {
5751
+ insightCounts.delete(entry.toolName);
5752
+ saveInsightCounts();
5753
+ suggestionTracker.resetTool(entry.toolName);
5754
+ }
5242
5755
  const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
5243
5756
  if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
5244
5757
  if (entry.waiter) {
5245
5758
  entry.waiter(resolvedDecision, reason);
5246
5759
  pending.delete(id);
5247
- broadcast("remove", { id });
5760
+ broadcast("remove", { id, decision: resolvedDecision });
5248
5761
  } else {
5249
5762
  entry.earlyDecision = resolvedDecision;
5250
5763
  entry.earlyReason = reason;
5251
- broadcast("remove", { id });
5764
+ broadcast("remove", { id, decision: resolvedDecision });
5252
5765
  entry.timer = setTimeout(() => pending.delete(id), 3e4);
5253
5766
  }
5254
5767
  res.writeHead(200);
@@ -5336,13 +5849,38 @@ data: ${JSON.stringify(item.data)}
5336
5849
  const id = pathname.split("/").pop();
5337
5850
  const entry = pending.get(id);
5338
5851
  if (!entry) return res.writeHead(404).end();
5339
- const { decision } = JSON.parse(await readBody(req));
5340
- appendAuditLog({ toolName: entry.toolName, args: entry.args, decision });
5852
+ const { decision, source } = JSON.parse(await readBody(req));
5853
+ const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
5854
+ appendAuditLog({
5855
+ toolName: entry.toolName,
5856
+ args: entry.args,
5857
+ decision: resolvedResolveDecision
5858
+ });
5341
5859
  clearTimeout(entry.timer);
5342
- if (entry.waiter) entry.waiter(decision);
5343
- else entry.earlyDecision = decision;
5860
+ if (resolvedResolveDecision === "allow") {
5861
+ insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
5862
+ saveInsightCounts();
5863
+ } else {
5864
+ insightCounts.delete(entry.toolName);
5865
+ saveInsightCounts();
5866
+ }
5867
+ if (!entry.slackDelegated) {
5868
+ if (resolvedResolveDecision === "allow") {
5869
+ const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
5870
+ if (suggestion) {
5871
+ suggestions.set(suggestion.id, suggestion);
5872
+ broadcast("suggestion:new", suggestion);
5873
+ }
5874
+ } else {
5875
+ suggestionTracker.resetTool(entry.toolName);
5876
+ }
5877
+ }
5878
+ const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
5879
+ if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
5880
+ if (entry.waiter) entry.waiter(resolvedResolveDecision);
5881
+ else entry.earlyDecision = resolvedResolveDecision;
5344
5882
  pending.delete(id);
5345
- broadcast("remove", { id });
5883
+ broadcast("remove", { id, decision: resolvedResolveDecision });
5346
5884
  res.writeHead(200);
5347
5885
  return res.end(JSON.stringify({ ok: true }));
5348
5886
  } catch {
@@ -5390,20 +5928,79 @@ data: ${JSON.stringify(item.data)}
5390
5928
  res.writeHead(400).end();
5391
5929
  }
5392
5930
  }
5931
+ if (req.method === "GET" && pathname === "/suggestions") {
5932
+ res.writeHead(200, { "Content-Type": "application/json" });
5933
+ return res.end(JSON.stringify([...suggestions.values()]));
5934
+ }
5935
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
5936
+ if (!validToken(req)) return res.writeHead(403).end();
5937
+ try {
5938
+ const body = await readBody(req);
5939
+ const data = body ? JSON.parse(body) : {};
5940
+ const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
5941
+ const node9Dir = import_path17.default.dirname(GLOBAL_CONFIG_PATH);
5942
+ if (!import_path17.default.resolve(configPath).startsWith(node9Dir + import_path17.default.sep)) {
5943
+ res.writeHead(400, { "Content-Type": "application/json" });
5944
+ return res.end(
5945
+ JSON.stringify({ error: "configPath must be within the node9 config directory" })
5946
+ );
5947
+ }
5948
+ const id = pathname.split("/")[2];
5949
+ const suggestion = suggestions.get(id);
5950
+ if (!suggestion) return res.writeHead(404).end();
5951
+ let patch;
5952
+ if (data.rule !== void 0) {
5953
+ const parsed = SmartRuleSchema.safeParse(data.rule);
5954
+ if (!parsed.success) {
5955
+ res.writeHead(400, { "Content-Type": "application/json" });
5956
+ return res.end(JSON.stringify({ error: parsed.error.message }));
5957
+ }
5958
+ patch = { type: "smartRule", rule: parsed.data };
5959
+ } else {
5960
+ patch = suggestion.suggestedRule;
5961
+ }
5962
+ patchConfig(configPath, patch);
5963
+ _resetConfigCache();
5964
+ insightCounts.delete(suggestion.toolName);
5965
+ saveInsightCounts();
5966
+ suggestion.status = "applied";
5967
+ broadcast("suggestion:resolved", { id, status: "applied" });
5968
+ res.writeHead(200, { "Content-Type": "application/json" });
5969
+ return res.end(JSON.stringify({ ok: true }));
5970
+ } catch (err) {
5971
+ console.error(import_chalk2.default.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
5972
+ res.writeHead(500, { "Content-Type": "application/json" });
5973
+ return res.end(JSON.stringify({ error: String(err) }));
5974
+ }
5975
+ }
5976
+ if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
5977
+ if (!validToken(req)) return res.writeHead(403).end();
5978
+ try {
5979
+ const id = pathname.split("/")[2];
5980
+ const suggestion = suggestions.get(id);
5981
+ if (!suggestion) return res.writeHead(404).end();
5982
+ suggestion.status = "dismissed";
5983
+ broadcast("suggestion:resolved", { id, status: "dismissed" });
5984
+ res.writeHead(200, { "Content-Type": "application/json" });
5985
+ return res.end(JSON.stringify({ ok: true }));
5986
+ } catch {
5987
+ res.writeHead(400).end();
5988
+ }
5989
+ }
5393
5990
  res.writeHead(404).end();
5394
5991
  });
5395
5992
  setDaemonServer(server);
5396
5993
  server.on("error", (e) => {
5397
5994
  if (e.code === "EADDRINUSE") {
5398
5995
  try {
5399
- if (import_fs12.default.existsSync(DAEMON_PID_FILE)) {
5400
- const { pid } = JSON.parse(import_fs12.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5996
+ if (import_fs14.default.existsSync(DAEMON_PID_FILE)) {
5997
+ const { pid } = JSON.parse(import_fs14.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5401
5998
  process.kill(pid, 0);
5402
5999
  return process.exit(0);
5403
6000
  }
5404
6001
  } catch {
5405
6002
  try {
5406
- import_fs12.default.unlinkSync(DAEMON_PID_FILE);
6003
+ import_fs14.default.unlinkSync(DAEMON_PID_FILE);
5407
6004
  } catch {
5408
6005
  }
5409
6006
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -5462,43 +6059,45 @@ data: ${JSON.stringify(item.data)}
5462
6059
  }
5463
6060
  startActivitySocket();
5464
6061
  }
5465
- var import_http, import_fs12, import_path15, import_crypto4, import_child_process4, import_chalk2;
6062
+ var import_http, import_fs14, import_path17, import_crypto5, import_child_process4, import_chalk2;
5466
6063
  var init_server = __esm({
5467
6064
  "src/daemon/server.ts"() {
5468
6065
  "use strict";
5469
6066
  import_http = __toESM(require("http"));
5470
- import_fs12 = __toESM(require("fs"));
5471
- import_path15 = __toESM(require("path"));
5472
- import_crypto4 = require("crypto");
6067
+ import_fs14 = __toESM(require("fs"));
6068
+ import_path17 = __toESM(require("path"));
6069
+ import_crypto5 = require("crypto");
5473
6070
  import_child_process4 = require("child_process");
5474
6071
  import_chalk2 = __toESM(require("chalk"));
5475
6072
  init_core();
5476
6073
  init_shields();
5477
6074
  init_ui2();
5478
6075
  init_state2();
6076
+ init_patch();
6077
+ init_config_schema();
5479
6078
  }
5480
6079
  });
5481
6080
 
5482
6081
  // src/daemon/index.ts
5483
6082
  function stopDaemon() {
5484
- if (!import_fs13.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
6083
+ if (!import_fs15.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
5485
6084
  try {
5486
- const { pid } = JSON.parse(import_fs13.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6085
+ const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5487
6086
  process.kill(pid, "SIGTERM");
5488
6087
  console.log(import_chalk3.default.green("\u2705 Stopped."));
5489
6088
  } catch {
5490
6089
  console.log(import_chalk3.default.gray("Cleaned up stale PID file."));
5491
6090
  } finally {
5492
6091
  try {
5493
- import_fs13.default.unlinkSync(DAEMON_PID_FILE);
6092
+ import_fs15.default.unlinkSync(DAEMON_PID_FILE);
5494
6093
  } catch {
5495
6094
  }
5496
6095
  }
5497
6096
  }
5498
6097
  function daemonStatus() {
5499
- if (import_fs13.default.existsSync(DAEMON_PID_FILE)) {
6098
+ if (import_fs15.default.existsSync(DAEMON_PID_FILE)) {
5500
6099
  try {
5501
- const { pid } = JSON.parse(import_fs13.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
6100
+ const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
5502
6101
  process.kill(pid, 0);
5503
6102
  console.log(import_chalk3.default.green("Node9 daemon: running"));
5504
6103
  return;
@@ -5517,11 +6116,11 @@ function daemonStatus() {
5517
6116
  console.log(import_chalk3.default.yellow("Node9 daemon: not running"));
5518
6117
  }
5519
6118
  }
5520
- var import_fs13, import_chalk3, import_child_process5;
6119
+ var import_fs15, import_chalk3, import_child_process5;
5521
6120
  var init_daemon2 = __esm({
5522
6121
  "src/daemon/index.ts"() {
5523
6122
  "use strict";
5524
- import_fs13 = __toESM(require("fs"));
6123
+ import_fs15 = __toESM(require("fs"));
5525
6124
  import_chalk3 = __toESM(require("chalk"));
5526
6125
  import_child_process5 = require("child_process");
5527
6126
  init_server();
@@ -5548,17 +6147,17 @@ function formatBase(activity) {
5548
6147
  const toolName = activity.tool.slice(0, 16).padEnd(16);
5549
6148
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
5550
6149
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
5551
- return `${import_chalk14.default.gray(time)} ${icon} ${import_chalk14.default.white.bold(toolName)} ${import_chalk14.default.dim(argsPreview)}`;
6150
+ return `${import_chalk16.default.gray(time)} ${icon} ${import_chalk16.default.white.bold(toolName)} ${import_chalk16.default.dim(argsPreview)}`;
5552
6151
  }
5553
6152
  function renderResult(activity, result) {
5554
6153
  const base = formatBase(activity);
5555
6154
  let status;
5556
6155
  if (result.status === "allow") {
5557
- status = import_chalk14.default.green("\u2713 ALLOW");
6156
+ status = import_chalk16.default.green("\u2713 ALLOW");
5558
6157
  } else if (result.status === "dlp") {
5559
- status = import_chalk14.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
6158
+ status = import_chalk16.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
5560
6159
  } else {
5561
- status = import_chalk14.default.red("\u2717 BLOCK");
6160
+ status = import_chalk16.default.red("\u2717 BLOCK");
5562
6161
  }
5563
6162
  if (process.stdout.isTTY) {
5564
6163
  import_readline3.default.clearLine(process.stdout, 0);
@@ -5568,16 +6167,16 @@ function renderResult(activity, result) {
5568
6167
  }
5569
6168
  function renderPending(activity) {
5570
6169
  if (!process.stdout.isTTY) return;
5571
- process.stdout.write(`${formatBase(activity)} ${import_chalk14.default.yellow("\u25CF \u2026")}\r`);
6170
+ process.stdout.write(`${formatBase(activity)} ${import_chalk16.default.yellow("\u25CF \u2026")}\r`);
5572
6171
  }
5573
6172
  async function ensureDaemon() {
5574
6173
  let pidPort = null;
5575
- if (import_fs20.default.existsSync(PID_FILE)) {
6174
+ if (import_fs23.default.existsSync(PID_FILE)) {
5576
6175
  try {
5577
- const { port } = JSON.parse(import_fs20.default.readFileSync(PID_FILE, "utf-8"));
6176
+ const { port } = JSON.parse(import_fs23.default.readFileSync(PID_FILE, "utf-8"));
5578
6177
  pidPort = port;
5579
6178
  } catch {
5580
- console.error(import_chalk14.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
6179
+ console.error(import_chalk16.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
5581
6180
  }
5582
6181
  }
5583
6182
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -5588,7 +6187,7 @@ async function ensureDaemon() {
5588
6187
  if (res.ok) return checkPort;
5589
6188
  } catch {
5590
6189
  }
5591
- console.log(import_chalk14.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6190
+ console.log(import_chalk16.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
5592
6191
  const child = (0, import_child_process13.spawn)(process.execPath, [process.argv[1], "daemon"], {
5593
6192
  detached: true,
5594
6193
  stdio: "ignore",
@@ -5605,12 +6204,15 @@ async function ensureDaemon() {
5605
6204
  } catch {
5606
6205
  }
5607
6206
  }
5608
- console.error(import_chalk14.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6207
+ console.error(import_chalk16.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
5609
6208
  process.exit(1);
5610
6209
  }
5611
- function postDecisionHttp(id, decision, csrfToken, port) {
6210
+ function postDecisionHttp(id, decision, csrfToken, port, opts) {
5612
6211
  return new Promise((resolve, reject) => {
5613
- const body = JSON.stringify({ decision, source: "terminal" });
6212
+ const bodyObj = { decision, source: "terminal" };
6213
+ if (opts?.persist) bodyObj.persist = true;
6214
+ if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
6215
+ const body = JSON.stringify(bodyObj);
5614
6216
  const req = import_http2.default.request(
5615
6217
  {
5616
6218
  hostname: "127.0.0.1",
@@ -5633,22 +6235,30 @@ function postDecisionHttp(id, decision, csrfToken, port) {
5633
6235
  req.end(body);
5634
6236
  });
5635
6237
  }
5636
- function buildCardLines(req) {
6238
+ function buildCardLines(req, localCount = 0) {
5637
6239
  const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
5638
6240
  const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
5639
6241
  const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
5640
6242
  const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
5641
- return [
6243
+ const lines = [
5642
6244
  ``,
5643
6245
  `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
5644
6246
  `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
5645
6247
  `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
5646
- `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`,
6248
+ `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`
6249
+ ];
6250
+ if (localCount >= 2) {
6251
+ lines.push(
6252
+ `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
6253
+ );
6254
+ }
6255
+ lines.push(
5647
6256
  `${CYAN}\u255A${RESET}`,
5648
6257
  ``,
5649
- ` ${BOLD}${GREEN}[A]${RESET} Allow ${BOLD}${RED}[D]${RESET} Deny`,
6258
+ ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
5650
6259
  ``
5651
- ];
6260
+ );
6261
+ return lines;
5652
6262
  }
5653
6263
  async function startTail(options = {}) {
5654
6264
  const port = await ensureDaemon();
@@ -5676,7 +6286,7 @@ async function startTail(options = {}) {
5676
6286
  req2.end();
5677
6287
  });
5678
6288
  if (result.ok) {
5679
- console.log(import_chalk14.default.green("\u2713 Flight Recorder buffer cleared."));
6289
+ console.log(import_chalk16.default.green("\u2713 Flight Recorder buffer cleared."));
5680
6290
  } else if (result.code === "ECONNREFUSED") {
5681
6291
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
5682
6292
  } else if (result.code === "ETIMEDOUT") {
@@ -5693,6 +6303,7 @@ async function startTail(options = {}) {
5693
6303
  let cardActive = false;
5694
6304
  let cardLineCount = 0;
5695
6305
  let cancelActiveCard = null;
6306
+ const localAllowCounts = /* @__PURE__ */ new Map();
5696
6307
  const canApprove = process.stdout.isTTY && process.stdin.isTTY;
5697
6308
  if (canApprove) import_readline3.default.emitKeypressEvents(process.stdin);
5698
6309
  function clearCard() {
@@ -5703,7 +6314,10 @@ async function startTail(options = {}) {
5703
6314
  }
5704
6315
  function printCard(req2) {
5705
6316
  process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
5706
- const lines = buildCardLines(req2);
6317
+ const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
6318
+ const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
6319
+ const priorCount = Math.max(daemonPrior, localPrior);
6320
+ const lines = buildCardLines(req2, priorCount);
5707
6321
  for (const line of lines) process.stdout.write(line + "\n");
5708
6322
  cardLineCount = lines.length;
5709
6323
  }
@@ -5731,34 +6345,70 @@ async function startTail(options = {}) {
5731
6345
  process.stdin.pause();
5732
6346
  cancelActiveCard = null;
5733
6347
  };
5734
- const settle = (decision) => {
6348
+ const settle = (action) => {
5735
6349
  if (settled) return;
5736
6350
  settled = true;
5737
6351
  cleanup();
5738
- clearCard();
6352
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6353
+ const stampedLines = buildCardLines(
6354
+ req2,
6355
+ Math.max(
6356
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6357
+ localAllowCounts.get(req2.toolName) ?? 0
6358
+ )
6359
+ );
6360
+ const decisionStamp = action === "always-allow" ? import_chalk16.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk16.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
6361
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
6362
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5739
6363
  process.stdout.write(SHOW_CURSOR);
5740
- postDecisionHttp(req2.id, decision, csrfToken, port).catch((err) => {
6364
+ cardLineCount = 0;
6365
+ if (action === "allow" || action === "always-allow" || action === "trust") {
6366
+ localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
6367
+ } else if (action === "deny") {
6368
+ localAllowCounts.delete(req2.toolName);
6369
+ }
6370
+ let httpDecision;
6371
+ let httpOpts;
6372
+ if (action === "always-allow") {
6373
+ httpDecision = "allow";
6374
+ httpOpts = { persist: true };
6375
+ } else if (action === "trust") {
6376
+ httpDecision = "trust";
6377
+ httpOpts = { trustDuration: "30m" };
6378
+ } else {
6379
+ httpDecision = action;
6380
+ }
6381
+ postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
5741
6382
  try {
5742
- import_fs20.default.appendFileSync(
5743
- import_path22.default.join(import_os18.default.homedir(), ".node9", "hook-debug.log"),
6383
+ import_fs23.default.appendFileSync(
6384
+ import_path25.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
5744
6385
  `[tail] POST /decision failed: ${String(err)}
5745
6386
  `
5746
6387
  );
5747
6388
  } catch {
5748
6389
  }
5749
6390
  });
5750
- const decisionLabel = decision === "allow" ? import_chalk14.default.green("\u2713 ALLOWED (terminal)") : import_chalk14.default.red("\u2717 DENIED (terminal)");
5751
- console.log(`${import_chalk14.default.cyan("\u25C6")} ${import_chalk14.default.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
5752
6391
  approvalQueue.shift();
5753
6392
  cardActive = false;
5754
6393
  showNextCard();
5755
6394
  };
5756
- cancelActiveCard = () => {
6395
+ cancelActiveCard = (externalDecision) => {
5757
6396
  if (settled) return;
5758
6397
  settled = true;
5759
6398
  cleanup();
5760
- clearCard();
6399
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
6400
+ const priorCount = Math.max(
6401
+ req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
6402
+ localAllowCounts.get(req2.toolName) ?? 0
6403
+ );
6404
+ const stampedLines = buildCardLines(req2, priorCount);
6405
+ if (externalDecision) {
6406
+ const source = externalDecision === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
6407
+ stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
6408
+ }
6409
+ for (const line of stampedLines) process.stdout.write(line + "\n");
5761
6410
  process.stdout.write(SHOW_CURSOR);
6411
+ cardLineCount = 0;
5762
6412
  approvalQueue.shift();
5763
6413
  cardActive = false;
5764
6414
  showNextCard();
@@ -5766,10 +6416,14 @@ async function startTail(options = {}) {
5766
6416
  process.stdin.resume();
5767
6417
  onKeypress = (_str, key) => {
5768
6418
  const name = key?.name ?? "";
5769
- if (name === "a") {
6419
+ if (name === "y" || name === "return") {
5770
6420
  settle("allow");
5771
- } else if (name === "d" || name === "return" || name === "enter" || key?.ctrl && name === "c") {
6421
+ } else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
5772
6422
  settle("deny");
6423
+ } else if (name === "a") {
6424
+ settle("always-allow");
6425
+ } else if (name === "t") {
6426
+ settle("trust");
5773
6427
  }
5774
6428
  };
5775
6429
  process.stdin.on("keypress", onKeypress);
@@ -5782,19 +6436,27 @@ async function startTail(options = {}) {
5782
6436
  else if (process.platform === "win32")
5783
6437
  (0, import_child_process13.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
5784
6438
  else (0, import_child_process13.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
6439
+ const intToken = getInternalToken();
6440
+ fetch(`http://127.0.0.1:${port}/browser-opened`, {
6441
+ method: "POST",
6442
+ headers: intToken ? { "X-Node9-Internal": intToken } : {}
6443
+ }).catch(() => {
6444
+ });
5785
6445
  }
5786
6446
  } catch {
5787
6447
  }
5788
- console.log(import_chalk14.default.cyan.bold(`
5789
- \u{1F6F0}\uFE0F Node9 tail `) + import_chalk14.default.dim(`\u2192 ${dashboardUrl}`));
6448
+ console.log(import_chalk16.default.cyan.bold(`
6449
+ \u{1F6F0}\uFE0F Node9 tail `) + import_chalk16.default.dim(`\u2192 ${dashboardUrl}`));
5790
6450
  if (canApprove) {
5791
- console.log(import_chalk14.default.dim("Interactive approvals enabled. [A] Allow [D] Deny"));
6451
+ console.log(
6452
+ import_chalk16.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
6453
+ );
5792
6454
  }
5793
6455
  if (options.history) {
5794
- console.log(import_chalk14.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
6456
+ console.log(import_chalk16.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
5795
6457
  } else {
5796
6458
  console.log(
5797
- import_chalk14.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
6459
+ import_chalk16.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
5798
6460
  );
5799
6461
  }
5800
6462
  process.on("SIGINT", () => {
@@ -5804,13 +6466,13 @@ async function startTail(options = {}) {
5804
6466
  import_readline3.default.clearLine(process.stdout, 0);
5805
6467
  import_readline3.default.cursorTo(process.stdout, 0);
5806
6468
  }
5807
- console.log(import_chalk14.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
6469
+ console.log(import_chalk16.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
5808
6470
  process.exit(0);
5809
6471
  });
5810
6472
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
5811
6473
  const req = import_http2.default.get(sseUrl, (res) => {
5812
6474
  if (res.statusCode !== 200) {
5813
- console.error(import_chalk14.default.red(`Failed to connect: HTTP ${res.statusCode}`));
6475
+ console.error(import_chalk16.default.red(`Failed to connect: HTTP ${res.statusCode}`));
5814
6476
  process.exit(1);
5815
6477
  }
5816
6478
  let currentEvent = "";
@@ -5840,7 +6502,7 @@ async function startTail(options = {}) {
5840
6502
  import_readline3.default.clearLine(process.stdout, 0);
5841
6503
  import_readline3.default.cursorTo(process.stdout, 0);
5842
6504
  }
5843
- console.log(import_chalk14.default.red("\n\u274C Daemon disconnected."));
6505
+ console.log(import_chalk16.default.red("\n\u274C Daemon disconnected."));
5844
6506
  process.exit(1);
5845
6507
  });
5846
6508
  });
@@ -5881,11 +6543,17 @@ async function startTail(options = {}) {
5881
6543
  }
5882
6544
  if (event === "remove") {
5883
6545
  try {
5884
- const { id } = JSON.parse(rawData);
6546
+ const { id, decision } = JSON.parse(rawData);
5885
6547
  const idx = approvalQueue.findIndex((r) => r.id === id);
5886
6548
  if (idx !== -1) {
5887
6549
  if (idx === 0 && cardActive && cancelActiveCard) {
5888
- cancelActiveCard();
6550
+ const toolName = approvalQueue[0].toolName;
6551
+ if (decision === "allow") {
6552
+ localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
6553
+ } else if (decision === "deny") {
6554
+ localAllowCounts.delete(toolName);
6555
+ }
6556
+ cancelActiveCard(decision);
5889
6557
  } else {
5890
6558
  approvalQueue.splice(idx, 1);
5891
6559
  }
@@ -5920,25 +6588,26 @@ async function startTail(options = {}) {
5920
6588
  }
5921
6589
  req.on("error", (err) => {
5922
6590
  const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
5923
- console.error(import_chalk14.default.red(`
6591
+ console.error(import_chalk16.default.red(`
5924
6592
  \u274C ${msg}`));
5925
6593
  process.exit(1);
5926
6594
  });
5927
6595
  }
5928
- var import_http2, import_chalk14, import_fs20, import_os18, import_path22, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
6596
+ var import_http2, import_chalk16, import_fs23, import_os21, import_path25, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
5929
6597
  var init_tail = __esm({
5930
6598
  "src/tui/tail.ts"() {
5931
6599
  "use strict";
5932
6600
  import_http2 = __toESM(require("http"));
5933
- import_chalk14 = __toESM(require("chalk"));
5934
- import_fs20 = __toESM(require("fs"));
5935
- import_os18 = __toESM(require("os"));
5936
- import_path22 = __toESM(require("path"));
6601
+ import_chalk16 = __toESM(require("chalk"));
6602
+ import_fs23 = __toESM(require("fs"));
6603
+ import_os21 = __toESM(require("os"));
6604
+ import_path25 = __toESM(require("path"));
5937
6605
  import_readline3 = __toESM(require("readline"));
5938
6606
  import_child_process13 = require("child_process");
5939
6607
  init_daemon2();
6608
+ init_daemon();
5940
6609
  init_core();
5941
- PID_FILE = import_path22.default.join(import_os18.default.homedir(), ".node9", "daemon.pid");
6610
+ PID_FILE = import_path25.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
5942
6611
  ICONS = {
5943
6612
  bash: "\u{1F4BB}",
5944
6613
  shell: "\u{1F4BB}",
@@ -5976,9 +6645,9 @@ var import_commander = require("commander");
5976
6645
  init_core();
5977
6646
 
5978
6647
  // src/setup.ts
5979
- var import_fs10 = __toESM(require("fs"));
5980
- var import_path13 = __toESM(require("path"));
5981
- var import_os10 = __toESM(require("os"));
6648
+ var import_fs11 = __toESM(require("fs"));
6649
+ var import_path14 = __toESM(require("path"));
6650
+ var import_os11 = __toESM(require("os"));
5982
6651
  var import_chalk = __toESM(require("chalk"));
5983
6652
  var import_prompts = require("@inquirer/prompts");
5984
6653
  function printDaemonTip() {
@@ -5995,26 +6664,26 @@ function fullPathCommand(subcommand) {
5995
6664
  }
5996
6665
  function readJson(filePath) {
5997
6666
  try {
5998
- if (import_fs10.default.existsSync(filePath)) {
5999
- return JSON.parse(import_fs10.default.readFileSync(filePath, "utf-8"));
6667
+ if (import_fs11.default.existsSync(filePath)) {
6668
+ return JSON.parse(import_fs11.default.readFileSync(filePath, "utf-8"));
6000
6669
  }
6001
6670
  } catch {
6002
6671
  }
6003
6672
  return null;
6004
6673
  }
6005
6674
  function writeJson(filePath, data) {
6006
- const dir = import_path13.default.dirname(filePath);
6007
- if (!import_fs10.default.existsSync(dir)) import_fs10.default.mkdirSync(dir, { recursive: true });
6008
- import_fs10.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
6675
+ const dir = import_path14.default.dirname(filePath);
6676
+ if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
6677
+ import_fs11.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
6009
6678
  }
6010
6679
  function isNode9Hook(cmd) {
6011
6680
  if (!cmd) return false;
6012
6681
  return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
6013
6682
  }
6014
6683
  function teardownClaude() {
6015
- const homeDir2 = import_os10.default.homedir();
6016
- const hooksPath = import_path13.default.join(homeDir2, ".claude", "settings.json");
6017
- const mcpPath = import_path13.default.join(homeDir2, ".claude.json");
6684
+ const homeDir2 = import_os11.default.homedir();
6685
+ const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
6686
+ const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
6018
6687
  let changed = false;
6019
6688
  const settings = readJson(hooksPath);
6020
6689
  if (settings?.hooks) {
@@ -6062,8 +6731,8 @@ function teardownClaude() {
6062
6731
  }
6063
6732
  }
6064
6733
  function teardownGemini() {
6065
- const homeDir2 = import_os10.default.homedir();
6066
- const settingsPath = import_path13.default.join(homeDir2, ".gemini", "settings.json");
6734
+ const homeDir2 = import_os11.default.homedir();
6735
+ const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
6067
6736
  const settings = readJson(settingsPath);
6068
6737
  if (!settings) {
6069
6738
  console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
@@ -6101,8 +6770,8 @@ function teardownGemini() {
6101
6770
  }
6102
6771
  }
6103
6772
  function teardownCursor() {
6104
- const homeDir2 = import_os10.default.homedir();
6105
- const mcpPath = import_path13.default.join(homeDir2, ".cursor", "mcp.json");
6773
+ const homeDir2 = import_os11.default.homedir();
6774
+ const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
6106
6775
  const mcpConfig = readJson(mcpPath);
6107
6776
  if (!mcpConfig?.mcpServers) {
6108
6777
  console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
@@ -6128,9 +6797,9 @@ function teardownCursor() {
6128
6797
  }
6129
6798
  }
6130
6799
  async function setupClaude() {
6131
- const homeDir2 = import_os10.default.homedir();
6132
- const mcpPath = import_path13.default.join(homeDir2, ".claude.json");
6133
- const hooksPath = import_path13.default.join(homeDir2, ".claude", "settings.json");
6800
+ const homeDir2 = import_os11.default.homedir();
6801
+ const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
6802
+ const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
6134
6803
  const claudeConfig = readJson(mcpPath) ?? {};
6135
6804
  const settings = readJson(hooksPath) ?? {};
6136
6805
  const servers = claudeConfig.mcpServers ?? {};
@@ -6204,8 +6873,8 @@ async function setupClaude() {
6204
6873
  }
6205
6874
  }
6206
6875
  async function setupGemini() {
6207
- const homeDir2 = import_os10.default.homedir();
6208
- const settingsPath = import_path13.default.join(homeDir2, ".gemini", "settings.json");
6876
+ const homeDir2 = import_os11.default.homedir();
6877
+ const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
6209
6878
  const settings = readJson(settingsPath) ?? {};
6210
6879
  const servers = settings.mcpServers ?? {};
6211
6880
  let anythingChanged = false;
@@ -6286,9 +6955,28 @@ async function setupGemini() {
6286
6955
  printDaemonTip();
6287
6956
  }
6288
6957
  }
6958
+ function detectAgents(homeDir2 = import_os11.default.homedir()) {
6959
+ const exists = (p) => {
6960
+ try {
6961
+ return import_fs11.default.existsSync(p);
6962
+ } catch (err) {
6963
+ const code = err.code;
6964
+ if (code !== "ENOENT") {
6965
+ process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
6966
+ `);
6967
+ }
6968
+ return false;
6969
+ }
6970
+ };
6971
+ return {
6972
+ claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
6973
+ gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
6974
+ cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
6975
+ };
6976
+ }
6289
6977
  async function setupCursor() {
6290
- const homeDir2 = import_os10.default.homedir();
6291
- const mcpPath = import_path13.default.join(homeDir2, ".cursor", "mcp.json");
6978
+ const homeDir2 = import_os11.default.homedir();
6979
+ const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
6292
6980
  const mcpConfig = readJson(mcpPath) ?? {};
6293
6981
  const servers = mcpConfig.mcpServers ?? {};
6294
6982
  let anythingChanged = false;
@@ -6344,10 +7032,10 @@ async function setupCursor() {
6344
7032
 
6345
7033
  // src/cli.ts
6346
7034
  init_daemon2();
6347
- var import_chalk15 = __toESM(require("chalk"));
6348
- var import_fs21 = __toESM(require("fs"));
6349
- var import_path23 = __toESM(require("path"));
6350
- var import_os19 = __toESM(require("os"));
7035
+ var import_chalk17 = __toESM(require("chalk"));
7036
+ var import_fs24 = __toESM(require("fs"));
7037
+ var import_path26 = __toESM(require("path"));
7038
+ var import_os22 = __toESM(require("os"));
6351
7039
  var import_prompts3 = require("@inquirer/prompts");
6352
7040
 
6353
7041
  // src/utils/duration.ts
@@ -6568,9 +7256,9 @@ async function autoStartDaemonAndWait() {
6568
7256
 
6569
7257
  // src/cli/commands/check.ts
6570
7258
  var import_chalk5 = __toESM(require("chalk"));
6571
- var import_fs15 = __toESM(require("fs"));
6572
- var import_path17 = __toESM(require("path"));
6573
- var import_os13 = __toESM(require("os"));
7259
+ var import_fs17 = __toESM(require("fs"));
7260
+ var import_path19 = __toESM(require("path"));
7261
+ var import_os15 = __toESM(require("os"));
6574
7262
  init_orchestrator();
6575
7263
  init_daemon();
6576
7264
  init_config();
@@ -6578,26 +7266,26 @@ init_policy();
6578
7266
 
6579
7267
  // src/undo.ts
6580
7268
  var import_child_process8 = require("child_process");
6581
- var import_crypto5 = __toESM(require("crypto"));
6582
- var import_fs14 = __toESM(require("fs"));
6583
- var import_path16 = __toESM(require("path"));
6584
- var import_os12 = __toESM(require("os"));
6585
- var SNAPSHOT_STACK_PATH = import_path16.default.join(import_os12.default.homedir(), ".node9", "snapshots.json");
6586
- var UNDO_LATEST_PATH = import_path16.default.join(import_os12.default.homedir(), ".node9", "undo_latest.txt");
7269
+ var import_crypto6 = __toESM(require("crypto"));
7270
+ var import_fs16 = __toESM(require("fs"));
7271
+ var import_path18 = __toESM(require("path"));
7272
+ var import_os14 = __toESM(require("os"));
7273
+ var SNAPSHOT_STACK_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots.json");
7274
+ var UNDO_LATEST_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "undo_latest.txt");
6587
7275
  var MAX_SNAPSHOTS = 10;
6588
7276
  var GIT_TIMEOUT = 15e3;
6589
7277
  function readStack() {
6590
7278
  try {
6591
- if (import_fs14.default.existsSync(SNAPSHOT_STACK_PATH))
6592
- return JSON.parse(import_fs14.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
7279
+ if (import_fs16.default.existsSync(SNAPSHOT_STACK_PATH))
7280
+ return JSON.parse(import_fs16.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
6593
7281
  } catch {
6594
7282
  }
6595
7283
  return [];
6596
7284
  }
6597
7285
  function writeStack(stack) {
6598
- const dir = import_path16.default.dirname(SNAPSHOT_STACK_PATH);
6599
- if (!import_fs14.default.existsSync(dir)) import_fs14.default.mkdirSync(dir, { recursive: true });
6600
- import_fs14.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
7286
+ const dir = import_path18.default.dirname(SNAPSHOT_STACK_PATH);
7287
+ if (!import_fs16.default.existsSync(dir)) import_fs16.default.mkdirSync(dir, { recursive: true });
7288
+ import_fs16.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
6601
7289
  }
6602
7290
  function buildArgsSummary(tool, args) {
6603
7291
  if (!args || typeof args !== "object") return "";
@@ -6613,7 +7301,7 @@ function buildArgsSummary(tool, args) {
6613
7301
  function normalizeCwdForHash(cwd) {
6614
7302
  let normalized;
6615
7303
  try {
6616
- normalized = import_fs14.default.realpathSync(cwd);
7304
+ normalized = import_fs16.default.realpathSync(cwd);
6617
7305
  } catch {
6618
7306
  normalized = cwd;
6619
7307
  }
@@ -6622,17 +7310,17 @@ function normalizeCwdForHash(cwd) {
6622
7310
  return normalized;
6623
7311
  }
6624
7312
  function getShadowRepoDir(cwd) {
6625
- const hash = import_crypto5.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
6626
- return import_path16.default.join(import_os12.default.homedir(), ".node9", "snapshots", hash);
7313
+ const hash = import_crypto6.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
7314
+ return import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots", hash);
6627
7315
  }
6628
7316
  function cleanOrphanedIndexFiles(shadowDir) {
6629
7317
  try {
6630
7318
  const cutoff = Date.now() - 6e4;
6631
- for (const f of import_fs14.default.readdirSync(shadowDir)) {
7319
+ for (const f of import_fs16.default.readdirSync(shadowDir)) {
6632
7320
  if (f.startsWith("index_")) {
6633
- const fp = import_path16.default.join(shadowDir, f);
7321
+ const fp = import_path18.default.join(shadowDir, f);
6634
7322
  try {
6635
- if (import_fs14.default.statSync(fp).mtimeMs < cutoff) import_fs14.default.unlinkSync(fp);
7323
+ if (import_fs16.default.statSync(fp).mtimeMs < cutoff) import_fs16.default.unlinkSync(fp);
6636
7324
  } catch {
6637
7325
  }
6638
7326
  }
@@ -6644,7 +7332,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
6644
7332
  const hardcoded = [".git", ".node9"];
6645
7333
  const lines = [...hardcoded, ...ignorePaths].join("\n");
6646
7334
  try {
6647
- import_fs14.default.writeFileSync(import_path16.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
7335
+ import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
6648
7336
  } catch {
6649
7337
  }
6650
7338
  }
@@ -6657,25 +7345,25 @@ function ensureShadowRepo(shadowDir, cwd) {
6657
7345
  timeout: 3e3
6658
7346
  });
6659
7347
  if (check.status === 0) {
6660
- const ptPath = import_path16.default.join(shadowDir, "project-path.txt");
7348
+ const ptPath = import_path18.default.join(shadowDir, "project-path.txt");
6661
7349
  try {
6662
- const stored = import_fs14.default.readFileSync(ptPath, "utf8").trim();
7350
+ const stored = import_fs16.default.readFileSync(ptPath, "utf8").trim();
6663
7351
  if (stored === normalizedCwd) return true;
6664
7352
  if (process.env.NODE9_DEBUG === "1")
6665
7353
  console.error(
6666
7354
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
6667
7355
  );
6668
- import_fs14.default.rmSync(shadowDir, { recursive: true, force: true });
7356
+ import_fs16.default.rmSync(shadowDir, { recursive: true, force: true });
6669
7357
  } catch {
6670
7358
  try {
6671
- import_fs14.default.writeFileSync(ptPath, normalizedCwd, "utf8");
7359
+ import_fs16.default.writeFileSync(ptPath, normalizedCwd, "utf8");
6672
7360
  } catch {
6673
7361
  }
6674
7362
  return true;
6675
7363
  }
6676
7364
  }
6677
7365
  try {
6678
- import_fs14.default.mkdirSync(shadowDir, { recursive: true });
7366
+ import_fs16.default.mkdirSync(shadowDir, { recursive: true });
6679
7367
  } catch {
6680
7368
  }
6681
7369
  const init = (0, import_child_process8.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
@@ -6684,7 +7372,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6684
7372
  console.error("[Node9] git init --bare failed:", init.stderr?.toString());
6685
7373
  return false;
6686
7374
  }
6687
- const configFile = import_path16.default.join(shadowDir, "config");
7375
+ const configFile = import_path18.default.join(shadowDir, "config");
6688
7376
  (0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
6689
7377
  timeout: 3e3
6690
7378
  });
@@ -6692,7 +7380,7 @@ function ensureShadowRepo(shadowDir, cwd) {
6692
7380
  timeout: 3e3
6693
7381
  });
6694
7382
  try {
6695
- import_fs14.default.writeFileSync(import_path16.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
7383
+ import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
6696
7384
  } catch {
6697
7385
  }
6698
7386
  return true;
@@ -6715,7 +7403,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6715
7403
  const shadowDir = getShadowRepoDir(cwd);
6716
7404
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
6717
7405
  writeShadowExcludes(shadowDir, ignorePaths);
6718
- indexFile = import_path16.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
7406
+ indexFile = import_path18.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
6719
7407
  const shadowEnv = {
6720
7408
  ...process.env,
6721
7409
  GIT_DIR: shadowDir,
@@ -6744,7 +7432,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6744
7432
  const shouldGc = stack.length % 5 === 0;
6745
7433
  if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
6746
7434
  writeStack(stack);
6747
- import_fs14.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
7435
+ import_fs16.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
6748
7436
  if (shouldGc) {
6749
7437
  (0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
6750
7438
  }
@@ -6755,7 +7443,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
6755
7443
  } finally {
6756
7444
  if (indexFile) {
6757
7445
  try {
6758
- import_fs14.default.unlinkSync(indexFile);
7446
+ import_fs16.default.unlinkSync(indexFile);
6759
7447
  } catch {
6760
7448
  }
6761
7449
  }
@@ -6824,9 +7512,9 @@ function applyUndo(hash, cwd) {
6824
7512
  timeout: GIT_TIMEOUT
6825
7513
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
6826
7514
  for (const file of [...tracked, ...untracked]) {
6827
- const fullPath = import_path16.default.join(dir, file);
6828
- if (!snapshotFiles.has(file) && import_fs14.default.existsSync(fullPath)) {
6829
- import_fs14.default.unlinkSync(fullPath);
7515
+ const fullPath = import_path18.default.join(dir, file);
7516
+ if (!snapshotFiles.has(file) && import_fs16.default.existsSync(fullPath)) {
7517
+ import_fs16.default.unlinkSync(fullPath);
6830
7518
  }
6831
7519
  }
6832
7520
  return true;
@@ -6850,9 +7538,9 @@ function registerCheckCommand(program2) {
6850
7538
  } catch (err) {
6851
7539
  const tempConfig = getConfig();
6852
7540
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
6853
- const logPath = import_path17.default.join(import_os13.default.homedir(), ".node9", "hook-debug.log");
7541
+ const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
6854
7542
  const errMsg = err instanceof Error ? err.message : String(err);
6855
- import_fs15.default.appendFileSync(
7543
+ import_fs17.default.appendFileSync(
6856
7544
  logPath,
6857
7545
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
6858
7546
  RAW: ${raw}
@@ -6863,10 +7551,10 @@ RAW: ${raw}
6863
7551
  }
6864
7552
  const config = getConfig(payload.cwd || void 0);
6865
7553
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
6866
- const logPath = import_path17.default.join(import_os13.default.homedir(), ".node9", "hook-debug.log");
6867
- if (!import_fs15.default.existsSync(import_path17.default.dirname(logPath)))
6868
- import_fs15.default.mkdirSync(import_path17.default.dirname(logPath), { recursive: true });
6869
- import_fs15.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
7554
+ const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
7555
+ if (!import_fs17.default.existsSync(import_path19.default.dirname(logPath)))
7556
+ import_fs17.default.mkdirSync(import_path19.default.dirname(logPath), { recursive: true });
7557
+ import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
6870
7558
  `);
6871
7559
  }
6872
7560
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -6879,8 +7567,8 @@ RAW: ${raw}
6879
7567
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
6880
7568
  let ttyFd = null;
6881
7569
  try {
6882
- ttyFd = import_fs15.default.openSync("/dev/tty", "w");
6883
- const writeTty = (line) => import_fs15.default.writeSync(ttyFd, line + "\n");
7570
+ ttyFd = import_fs17.default.openSync("/dev/tty", "w");
7571
+ const writeTty = (line) => import_fs17.default.writeSync(ttyFd, line + "\n");
6884
7572
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
6885
7573
  writeTty(import_chalk5.default.bgRed.white.bold(`
6886
7574
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -6896,7 +7584,7 @@ RAW: ${raw}
6896
7584
  } finally {
6897
7585
  if (ttyFd !== null)
6898
7586
  try {
6899
- import_fs15.default.closeSync(ttyFd);
7587
+ import_fs17.default.closeSync(ttyFd);
6900
7588
  } catch {
6901
7589
  }
6902
7590
  }
@@ -6927,7 +7615,7 @@ RAW: ${raw}
6927
7615
  if (shouldSnapshot(toolName, toolInput, config)) {
6928
7616
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6929
7617
  }
6930
- const safeCwdForAuth = typeof payload.cwd === "string" && import_path17.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7618
+ const safeCwdForAuth = typeof payload.cwd === "string" && import_path19.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
6931
7619
  const result = await authorizeHeadless(toolName, toolInput, meta, {
6932
7620
  cwd: safeCwdForAuth
6933
7621
  });
@@ -6939,12 +7627,12 @@ RAW: ${raw}
6939
7627
  }
6940
7628
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
6941
7629
  try {
6942
- const tty = import_fs15.default.openSync("/dev/tty", "w");
6943
- import_fs15.default.writeSync(
7630
+ const tty = import_fs17.default.openSync("/dev/tty", "w");
7631
+ import_fs17.default.writeSync(
6944
7632
  tty,
6945
7633
  import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
6946
7634
  );
6947
- import_fs15.default.closeSync(tty);
7635
+ import_fs17.default.closeSync(tty);
6948
7636
  } catch {
6949
7637
  }
6950
7638
  const daemonReady = await autoStartDaemonAndWait();
@@ -6971,9 +7659,9 @@ RAW: ${raw}
6971
7659
  });
6972
7660
  } catch (err) {
6973
7661
  if (process.env.NODE9_DEBUG === "1") {
6974
- const logPath = import_path17.default.join(import_os13.default.homedir(), ".node9", "hook-debug.log");
7662
+ const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
6975
7663
  const errMsg = err instanceof Error ? err.message : String(err);
6976
- import_fs15.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
7664
+ import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
6977
7665
  `);
6978
7666
  }
6979
7667
  process.exit(0);
@@ -7007,9 +7695,9 @@ RAW: ${raw}
7007
7695
  }
7008
7696
 
7009
7697
  // src/cli/commands/log.ts
7010
- var import_fs16 = __toESM(require("fs"));
7011
- var import_path18 = __toESM(require("path"));
7012
- var import_os14 = __toESM(require("os"));
7698
+ var import_fs18 = __toESM(require("fs"));
7699
+ var import_path20 = __toESM(require("path"));
7700
+ var import_os16 = __toESM(require("os"));
7013
7701
  init_audit();
7014
7702
  init_config();
7015
7703
  init_policy();
@@ -7031,11 +7719,11 @@ function registerLogCommand(program2) {
7031
7719
  decision: "allowed",
7032
7720
  source: "post-hook"
7033
7721
  };
7034
- const logPath = import_path18.default.join(import_os14.default.homedir(), ".node9", "audit.log");
7035
- if (!import_fs16.default.existsSync(import_path18.default.dirname(logPath)))
7036
- import_fs16.default.mkdirSync(import_path18.default.dirname(logPath), { recursive: true });
7037
- import_fs16.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7038
- const safeCwd = typeof payload.cwd === "string" && import_path18.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7722
+ const logPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "audit.log");
7723
+ if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
7724
+ import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
7725
+ import_fs18.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
7726
+ const safeCwd = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
7039
7727
  const config = getConfig(safeCwd);
7040
7728
  if (shouldSnapshot(tool, {}, config)) {
7041
7729
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -7044,9 +7732,9 @@ function registerLogCommand(program2) {
7044
7732
  const msg = err instanceof Error ? err.message : String(err);
7045
7733
  process.stderr.write(`[Node9] audit log error: ${msg}
7046
7734
  `);
7047
- const debugPath = import_path18.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
7735
+ const debugPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
7048
7736
  try {
7049
- import_fs16.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7737
+ import_fs18.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
7050
7738
  `);
7051
7739
  } catch {
7052
7740
  }
@@ -7350,14 +8038,14 @@ function registerConfigShowCommand(program2) {
7350
8038
 
7351
8039
  // src/cli/commands/doctor.ts
7352
8040
  var import_chalk7 = __toESM(require("chalk"));
7353
- var import_fs17 = __toESM(require("fs"));
7354
- var import_path19 = __toESM(require("path"));
7355
- var import_os15 = __toESM(require("os"));
8041
+ var import_fs19 = __toESM(require("fs"));
8042
+ var import_path21 = __toESM(require("path"));
8043
+ var import_os17 = __toESM(require("os"));
7356
8044
  var import_child_process9 = require("child_process");
7357
8045
  init_daemon();
7358
8046
  function registerDoctorCommand(program2, version2) {
7359
8047
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
7360
- const homeDir2 = import_os15.default.homedir();
8048
+ const homeDir2 = import_os17.default.homedir();
7361
8049
  let failures = 0;
7362
8050
  function pass(msg) {
7363
8051
  console.log(import_chalk7.default.green(" \u2705 ") + msg);
@@ -7406,10 +8094,10 @@ function registerDoctorCommand(program2, version2) {
7406
8094
  );
7407
8095
  }
7408
8096
  section("Configuration");
7409
- const globalConfigPath = import_path19.default.join(homeDir2, ".node9", "config.json");
7410
- if (import_fs17.default.existsSync(globalConfigPath)) {
8097
+ const globalConfigPath = import_path21.default.join(homeDir2, ".node9", "config.json");
8098
+ if (import_fs19.default.existsSync(globalConfigPath)) {
7411
8099
  try {
7412
- JSON.parse(import_fs17.default.readFileSync(globalConfigPath, "utf-8"));
8100
+ JSON.parse(import_fs19.default.readFileSync(globalConfigPath, "utf-8"));
7413
8101
  pass("~/.node9/config.json found and valid");
7414
8102
  } catch {
7415
8103
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -7417,10 +8105,10 @@ function registerDoctorCommand(program2, version2) {
7417
8105
  } else {
7418
8106
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
7419
8107
  }
7420
- const projectConfigPath = import_path19.default.join(process.cwd(), "node9.config.json");
7421
- if (import_fs17.default.existsSync(projectConfigPath)) {
8108
+ const projectConfigPath = import_path21.default.join(process.cwd(), "node9.config.json");
8109
+ if (import_fs19.default.existsSync(projectConfigPath)) {
7422
8110
  try {
7423
- JSON.parse(import_fs17.default.readFileSync(projectConfigPath, "utf-8"));
8111
+ JSON.parse(import_fs19.default.readFileSync(projectConfigPath, "utf-8"));
7424
8112
  pass("node9.config.json found and valid (project)");
7425
8113
  } catch {
7426
8114
  fail(
@@ -7429,8 +8117,8 @@ function registerDoctorCommand(program2, version2) {
7429
8117
  );
7430
8118
  }
7431
8119
  }
7432
- const credsPath = import_path19.default.join(homeDir2, ".node9", "credentials.json");
7433
- if (import_fs17.default.existsSync(credsPath)) {
8120
+ const credsPath = import_path21.default.join(homeDir2, ".node9", "credentials.json");
8121
+ if (import_fs19.default.existsSync(credsPath)) {
7434
8122
  pass("Cloud credentials found (~/.node9/credentials.json)");
7435
8123
  } else {
7436
8124
  warn(
@@ -7439,10 +8127,10 @@ function registerDoctorCommand(program2, version2) {
7439
8127
  );
7440
8128
  }
7441
8129
  section("Agent Hooks");
7442
- const claudeSettingsPath = import_path19.default.join(homeDir2, ".claude", "settings.json");
7443
- if (import_fs17.default.existsSync(claudeSettingsPath)) {
8130
+ const claudeSettingsPath = import_path21.default.join(homeDir2, ".claude", "settings.json");
8131
+ if (import_fs19.default.existsSync(claudeSettingsPath)) {
7444
8132
  try {
7445
- const cs = JSON.parse(import_fs17.default.readFileSync(claudeSettingsPath, "utf-8"));
8133
+ const cs = JSON.parse(import_fs19.default.readFileSync(claudeSettingsPath, "utf-8"));
7446
8134
  const hasHook = cs.hooks?.PreToolUse?.some(
7447
8135
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7448
8136
  );
@@ -7458,10 +8146,10 @@ function registerDoctorCommand(program2, version2) {
7458
8146
  } else {
7459
8147
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
7460
8148
  }
7461
- const geminiSettingsPath = import_path19.default.join(homeDir2, ".gemini", "settings.json");
7462
- if (import_fs17.default.existsSync(geminiSettingsPath)) {
8149
+ const geminiSettingsPath = import_path21.default.join(homeDir2, ".gemini", "settings.json");
8150
+ if (import_fs19.default.existsSync(geminiSettingsPath)) {
7463
8151
  try {
7464
- const gs = JSON.parse(import_fs17.default.readFileSync(geminiSettingsPath, "utf-8"));
8152
+ const gs = JSON.parse(import_fs19.default.readFileSync(geminiSettingsPath, "utf-8"));
7465
8153
  const hasHook = gs.hooks?.BeforeTool?.some(
7466
8154
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
7467
8155
  );
@@ -7477,10 +8165,10 @@ function registerDoctorCommand(program2, version2) {
7477
8165
  } else {
7478
8166
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
7479
8167
  }
7480
- const cursorHooksPath = import_path19.default.join(homeDir2, ".cursor", "hooks.json");
7481
- if (import_fs17.default.existsSync(cursorHooksPath)) {
8168
+ const cursorHooksPath = import_path21.default.join(homeDir2, ".cursor", "hooks.json");
8169
+ if (import_fs19.default.existsSync(cursorHooksPath)) {
7482
8170
  try {
7483
- const cur = JSON.parse(import_fs17.default.readFileSync(cursorHooksPath, "utf-8"));
8171
+ const cur = JSON.parse(import_fs19.default.readFileSync(cursorHooksPath, "utf-8"));
7484
8172
  const hasHook = cur.hooks?.preToolUse?.some(
7485
8173
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
7486
8174
  );
@@ -7518,9 +8206,9 @@ function registerDoctorCommand(program2, version2) {
7518
8206
 
7519
8207
  // src/cli/commands/audit.ts
7520
8208
  var import_chalk8 = __toESM(require("chalk"));
7521
- var import_fs18 = __toESM(require("fs"));
7522
- var import_path20 = __toESM(require("path"));
7523
- var import_os16 = __toESM(require("os"));
8209
+ var import_fs20 = __toESM(require("fs"));
8210
+ var import_path22 = __toESM(require("path"));
8211
+ var import_os18 = __toESM(require("os"));
7524
8212
  function formatRelativeTime(timestamp) {
7525
8213
  const diff = Date.now() - new Date(timestamp).getTime();
7526
8214
  const sec = Math.floor(diff / 1e3);
@@ -7533,14 +8221,14 @@ function formatRelativeTime(timestamp) {
7533
8221
  }
7534
8222
  function registerAuditCommand(program2) {
7535
8223
  program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
7536
- const logPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "audit.log");
7537
- if (!import_fs18.default.existsSync(logPath)) {
8224
+ const logPath = import_path22.default.join(import_os18.default.homedir(), ".node9", "audit.log");
8225
+ if (!import_fs20.default.existsSync(logPath)) {
7538
8226
  console.log(
7539
8227
  import_chalk8.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
7540
8228
  );
7541
8229
  return;
7542
8230
  }
7543
- const raw = import_fs18.default.readFileSync(logPath, "utf-8");
8231
+ const raw = import_fs20.default.readFileSync(logPath, "utf-8");
7544
8232
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
7545
8233
  let entries = lines.flatMap((line) => {
7546
8234
  try {
@@ -7658,11 +8346,44 @@ function registerDaemonCommand(program2) {
7658
8346
 
7659
8347
  // src/cli/commands/status.ts
7660
8348
  var import_chalk10 = __toESM(require("chalk"));
7661
- var import_fs19 = __toESM(require("fs"));
7662
- var import_path21 = __toESM(require("path"));
7663
- var import_os17 = __toESM(require("os"));
8349
+ var import_fs21 = __toESM(require("fs"));
8350
+ var import_path23 = __toESM(require("path"));
8351
+ var import_os19 = __toESM(require("os"));
7664
8352
  init_core();
7665
8353
  init_daemon();
8354
+ function readJson2(filePath) {
8355
+ try {
8356
+ if (import_fs21.default.existsSync(filePath)) return JSON.parse(import_fs21.default.readFileSync(filePath, "utf-8"));
8357
+ } catch {
8358
+ }
8359
+ return null;
8360
+ }
8361
+ function isNode9Hook2(cmd) {
8362
+ if (!cmd) return false;
8363
+ return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
8364
+ }
8365
+ function wrappedMcpServers(servers) {
8366
+ if (!servers) return [];
8367
+ return Object.entries(servers).filter(([, s]) => s.command === "node9" && Array.isArray(s.args) && s.args.length > 0).map(([name, s]) => `${name} \u2192 ${s.args.join(" ")}`);
8368
+ }
8369
+ function printAgentSection(label, hookPairs, wrapped) {
8370
+ console.log(import_chalk10.default.bold(` ${label}`));
8371
+ for (const { name, present } of hookPairs) {
8372
+ if (present) {
8373
+ console.log(import_chalk10.default.green(` \u2713 ${name}`));
8374
+ } else {
8375
+ console.log(import_chalk10.default.red(` \u2717 ${name}`) + import_chalk10.default.gray(" (not wired)"));
8376
+ }
8377
+ }
8378
+ if (wrapped.length > 0) {
8379
+ console.log(import_chalk10.default.cyan(` MCP proxied:`));
8380
+ for (const entry of wrapped) {
8381
+ console.log(import_chalk10.default.gray(` \u2022 ${entry}`));
8382
+ }
8383
+ } else {
8384
+ console.log(import_chalk10.default.gray(` MCP proxied: none`));
8385
+ }
8386
+ }
7666
8387
  function registerStatusCommand(program2) {
7667
8388
  program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
7668
8389
  const creds = getCredentials();
@@ -7697,19 +8418,72 @@ function registerStatusCommand(program2) {
7697
8418
  console.log("");
7698
8419
  const modeLabel = settings.mode === "audit" ? import_chalk10.default.blue("audit") : settings.mode === "strict" ? import_chalk10.default.red("strict") : import_chalk10.default.white("standard");
7699
8420
  console.log(` Mode: ${modeLabel}`);
7700
- const projectConfig = import_path21.default.join(process.cwd(), "node9.config.json");
7701
- const globalConfig = import_path21.default.join(import_os17.default.homedir(), ".node9", "config.json");
8421
+ const projectConfig = import_path23.default.join(process.cwd(), "node9.config.json");
8422
+ const globalConfig = import_path23.default.join(import_os19.default.homedir(), ".node9", "config.json");
7702
8423
  console.log(
7703
- ` Local: ${import_fs19.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
8424
+ ` Local: ${import_fs21.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
7704
8425
  );
7705
8426
  console.log(
7706
- ` Global: ${import_fs19.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
8427
+ ` Global: ${import_fs21.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
7707
8428
  );
7708
8429
  if (mergedConfig.policy.sandboxPaths.length > 0) {
7709
8430
  console.log(
7710
8431
  ` Sandbox: ${import_chalk10.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
7711
8432
  );
7712
8433
  }
8434
+ const homeDir2 = import_os19.default.homedir();
8435
+ const claudeSettings = readJson2(
8436
+ import_path23.default.join(homeDir2, ".claude", "settings.json")
8437
+ );
8438
+ const claudeConfig = readJson2(import_path23.default.join(homeDir2, ".claude.json"));
8439
+ const geminiSettings = readJson2(
8440
+ import_path23.default.join(homeDir2, ".gemini", "settings.json")
8441
+ );
8442
+ const cursorConfig = readJson2(import_path23.default.join(homeDir2, ".cursor", "mcp.json"));
8443
+ const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
8444
+ if (agentFound) {
8445
+ console.log("");
8446
+ console.log(import_chalk10.default.bold(" Agent Wiring:"));
8447
+ console.log("");
8448
+ if (claudeSettings || claudeConfig) {
8449
+ const preHook = claudeSettings?.hooks?.PreToolUse?.some(
8450
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8451
+ ) ?? false;
8452
+ const postHook = claudeSettings?.hooks?.PostToolUse?.some(
8453
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8454
+ ) ?? false;
8455
+ printAgentSection(
8456
+ "Claude Code",
8457
+ [
8458
+ { name: "PreToolUse (node9 check)", present: preHook },
8459
+ { name: "PostToolUse (node9 log)", present: postHook }
8460
+ ],
8461
+ wrappedMcpServers(claudeConfig?.mcpServers)
8462
+ );
8463
+ console.log("");
8464
+ }
8465
+ if (geminiSettings) {
8466
+ const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
8467
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8468
+ ) ?? false;
8469
+ const afterHook = geminiSettings.hooks?.AfterTool?.some(
8470
+ (m) => m.hooks.some((h) => isNode9Hook2(h.command))
8471
+ ) ?? false;
8472
+ printAgentSection(
8473
+ "Gemini CLI",
8474
+ [
8475
+ { name: "BeforeTool (node9 check)", present: beforeHook },
8476
+ { name: "AfterTool (node9 log)", present: afterHook }
8477
+ ],
8478
+ wrappedMcpServers(geminiSettings.mcpServers)
8479
+ );
8480
+ console.log("");
8481
+ }
8482
+ if (cursorConfig) {
8483
+ printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
8484
+ console.log("");
8485
+ }
8486
+ }
7713
8487
  const pauseState = checkPause();
7714
8488
  if (pauseState.paused) {
7715
8489
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
@@ -7722,8 +8496,63 @@ function registerStatusCommand(program2) {
7722
8496
  });
7723
8497
  }
7724
8498
 
7725
- // src/cli/commands/undo.ts
8499
+ // src/cli/commands/init.ts
7726
8500
  var import_chalk11 = __toESM(require("chalk"));
8501
+ var import_fs22 = __toESM(require("fs"));
8502
+ var import_path24 = __toESM(require("path"));
8503
+ var import_os20 = __toESM(require("os"));
8504
+ init_core();
8505
+ function registerInitCommand(program2) {
8506
+ program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
8507
+ console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
8508
+ const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
8509
+ if (import_fs22.default.existsSync(configPath) && !options.force) {
8510
+ console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
8511
+ } else {
8512
+ const requestedMode = options.mode.toLowerCase();
8513
+ const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8514
+ const configToSave = {
8515
+ ...DEFAULT_CONFIG,
8516
+ settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
8517
+ };
8518
+ const dir = import_path24.default.dirname(configPath);
8519
+ if (!import_fs22.default.existsSync(dir)) import_fs22.default.mkdirSync(dir, { recursive: true });
8520
+ import_fs22.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8521
+ console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
8522
+ console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
8523
+ }
8524
+ if (options.skipSetup) return;
8525
+ console.log("");
8526
+ const detected = detectAgents();
8527
+ const found = Object.keys(detected).filter(
8528
+ (k) => detected[k]
8529
+ );
8530
+ if (found.length === 0) {
8531
+ console.log(
8532
+ import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
8533
+ );
8534
+ console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
8535
+ return;
8536
+ }
8537
+ console.log(import_chalk11.default.bold("Detected agents:"));
8538
+ for (const agent of found) {
8539
+ console.log(import_chalk11.default.green(` \u2713 ${agent}`));
8540
+ }
8541
+ console.log("");
8542
+ for (const agent of found) {
8543
+ console.log(import_chalk11.default.bold(`Wiring ${agent}...`));
8544
+ if (agent === "claude") await setupClaude();
8545
+ else if (agent === "gemini") await setupGemini();
8546
+ else if (agent === "cursor") await setupCursor();
8547
+ console.log("");
8548
+ }
8549
+ console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
8550
+ console.log(import_chalk11.default.gray(" Run: node9 daemon start"));
8551
+ });
8552
+ }
8553
+
8554
+ // src/cli/commands/undo.ts
8555
+ var import_chalk12 = __toESM(require("chalk"));
7727
8556
  var import_prompts2 = require("@inquirer/prompts");
7728
8557
  function registerUndoCommand(program2) {
7729
8558
  program2.command("undo").description(
@@ -7735,22 +8564,22 @@ function registerUndoCommand(program2) {
7735
8564
  if (history.length === 0) {
7736
8565
  if (!options.all && allHistory.length > 0) {
7737
8566
  console.log(
7738
- import_chalk11.default.yellow(
8567
+ import_chalk12.default.yellow(
7739
8568
  `
7740
8569
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
7741
- Run ${import_chalk11.default.cyan("node9 undo --all")} to see snapshots from all projects.
8570
+ Run ${import_chalk12.default.cyan("node9 undo --all")} to see snapshots from all projects.
7742
8571
  `
7743
8572
  )
7744
8573
  );
7745
8574
  } else {
7746
- console.log(import_chalk11.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
8575
+ console.log(import_chalk12.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
7747
8576
  }
7748
8577
  return;
7749
8578
  }
7750
8579
  const idx = history.length - steps;
7751
8580
  if (idx < 0) {
7752
8581
  console.log(
7753
- import_chalk11.default.yellow(
8582
+ import_chalk12.default.yellow(
7754
8583
  `
7755
8584
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
7756
8585
  `
@@ -7762,19 +8591,19 @@ function registerUndoCommand(program2) {
7762
8591
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
7763
8592
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
7764
8593
  console.log(
7765
- import_chalk11.default.magenta.bold(`
8594
+ import_chalk12.default.magenta.bold(`
7766
8595
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
7767
8596
  );
7768
8597
  console.log(
7769
- import_chalk11.default.white(
7770
- ` Tool: ${import_chalk11.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk11.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
8598
+ import_chalk12.default.white(
8599
+ ` Tool: ${import_chalk12.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk12.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
7771
8600
  )
7772
8601
  );
7773
- console.log(import_chalk11.default.white(` When: ${import_chalk11.default.gray(ageStr)}`));
7774
- console.log(import_chalk11.default.white(` Dir: ${import_chalk11.default.gray(snapshot.cwd)}`));
8602
+ console.log(import_chalk12.default.white(` When: ${import_chalk12.default.gray(ageStr)}`));
8603
+ console.log(import_chalk12.default.white(` Dir: ${import_chalk12.default.gray(snapshot.cwd)}`));
7775
8604
  if (steps > 1)
7776
8605
  console.log(
7777
- import_chalk11.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
8606
+ import_chalk12.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
7778
8607
  );
7779
8608
  console.log("");
7780
8609
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -7782,21 +8611,21 @@ function registerUndoCommand(program2) {
7782
8611
  const lines = diff.split("\n");
7783
8612
  for (const line of lines) {
7784
8613
  if (line.startsWith("+++") || line.startsWith("---")) {
7785
- console.log(import_chalk11.default.bold(line));
8614
+ console.log(import_chalk12.default.bold(line));
7786
8615
  } else if (line.startsWith("+")) {
7787
- console.log(import_chalk11.default.green(line));
8616
+ console.log(import_chalk12.default.green(line));
7788
8617
  } else if (line.startsWith("-")) {
7789
- console.log(import_chalk11.default.red(line));
8618
+ console.log(import_chalk12.default.red(line));
7790
8619
  } else if (line.startsWith("@@")) {
7791
- console.log(import_chalk11.default.cyan(line));
8620
+ console.log(import_chalk12.default.cyan(line));
7792
8621
  } else {
7793
- console.log(import_chalk11.default.gray(line));
8622
+ console.log(import_chalk12.default.gray(line));
7794
8623
  }
7795
8624
  }
7796
8625
  console.log("");
7797
8626
  } else {
7798
8627
  console.log(
7799
- import_chalk11.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
8628
+ import_chalk12.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
7800
8629
  );
7801
8630
  }
7802
8631
  const proceed = await (0, import_prompts2.confirm)({
@@ -7805,18 +8634,18 @@ function registerUndoCommand(program2) {
7805
8634
  });
7806
8635
  if (proceed) {
7807
8636
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
7808
- console.log(import_chalk11.default.green("\n\u2705 Reverted successfully.\n"));
8637
+ console.log(import_chalk12.default.green("\n\u2705 Reverted successfully.\n"));
7809
8638
  } else {
7810
- console.error(import_chalk11.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
8639
+ console.error(import_chalk12.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
7811
8640
  }
7812
8641
  } else {
7813
- console.log(import_chalk11.default.gray("\nCancelled.\n"));
8642
+ console.log(import_chalk12.default.gray("\nCancelled.\n"));
7814
8643
  }
7815
8644
  });
7816
8645
  }
7817
8646
 
7818
8647
  // src/cli/commands/watch.ts
7819
- var import_chalk12 = __toESM(require("chalk"));
8648
+ var import_chalk13 = __toESM(require("chalk"));
7820
8649
  var import_child_process11 = require("child_process");
7821
8650
  init_daemon();
7822
8651
  function registerWatchCommand(program2) {
@@ -7833,7 +8662,7 @@ function registerWatchCommand(program2) {
7833
8662
  throw new Error("not running");
7834
8663
  }
7835
8664
  } catch {
7836
- console.error(import_chalk12.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
8665
+ console.error(import_chalk13.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
7837
8666
  const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
7838
8667
  detached: true,
7839
8668
  stdio: "ignore",
@@ -7855,12 +8684,12 @@ function registerWatchCommand(program2) {
7855
8684
  }
7856
8685
  }
7857
8686
  if (!ready) {
7858
- console.error(import_chalk12.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
8687
+ console.error(import_chalk13.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7859
8688
  process.exit(1);
7860
8689
  }
7861
8690
  }
7862
8691
  console.error(
7863
- import_chalk12.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk12.default.dim(` \u2192 localhost:${port}`) + import_chalk12.default.dim(
8692
+ import_chalk13.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk13.default.dim(` \u2192 localhost:${port}`) + import_chalk13.default.dim(
7864
8693
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
7865
8694
  )
7866
8695
  );
@@ -7869,7 +8698,7 @@ function registerWatchCommand(program2) {
7869
8698
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
7870
8699
  });
7871
8700
  if (result.error) {
7872
- console.error(import_chalk12.default.red(`\u274C Failed to run command: ${result.error.message}`));
8701
+ console.error(import_chalk13.default.red(`\u274C Failed to run command: ${result.error.message}`));
7873
8702
  process.exit(1);
7874
8703
  }
7875
8704
  process.exit(result.status ?? 0);
@@ -7878,7 +8707,7 @@ function registerWatchCommand(program2) {
7878
8707
 
7879
8708
  // src/mcp-gateway/index.ts
7880
8709
  var import_readline2 = __toESM(require("readline"));
7881
- var import_chalk13 = __toESM(require("chalk"));
8710
+ var import_chalk14 = __toESM(require("chalk"));
7882
8711
  var import_child_process12 = require("child_process");
7883
8712
  var import_execa3 = require("execa");
7884
8713
  init_orchestrator();
@@ -7942,13 +8771,13 @@ async function runMcpGateway(upstreamCommand) {
7942
8771
  const prov = checkProvenance(executable);
7943
8772
  if (prov.trustLevel === "suspect") {
7944
8773
  console.error(
7945
- import_chalk13.default.red(
8774
+ import_chalk14.default.red(
7946
8775
  `\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
7947
8776
  )
7948
8777
  );
7949
- console.error(import_chalk13.default.red(" Verify this binary is trusted before proceeding."));
8778
+ console.error(import_chalk14.default.red(" Verify this binary is trusted before proceeding."));
7950
8779
  }
7951
- console.error(import_chalk13.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
8780
+ console.error(import_chalk14.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
7952
8781
  const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
7953
8782
  "NODE_OPTIONS",
7954
8783
  "NODE_PATH",
@@ -8012,10 +8841,10 @@ async function runMcpGateway(upstreamCommand) {
8012
8841
  mcpServer
8013
8842
  });
8014
8843
  if (!result.approved) {
8015
- console.error(import_chalk13.default.red(`
8844
+ console.error(import_chalk14.default.red(`
8016
8845
  \u{1F6D1} Node9 MCP Gateway: Action Blocked`));
8017
- console.error(import_chalk13.default.gray(` Tool: ${toolName}`));
8018
- console.error(import_chalk13.default.gray(` Reason: ${result.reason ?? "Security Policy"}
8846
+ console.error(import_chalk14.default.gray(` Tool: ${toolName}`));
8847
+ console.error(import_chalk14.default.gray(` Reason: ${result.reason ?? "Security Policy"}
8019
8848
  `));
8020
8849
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
8021
8850
  const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -8087,22 +8916,77 @@ function registerMcpGatewayCommand(program2) {
8087
8916
  });
8088
8917
  }
8089
8918
 
8919
+ // src/cli/commands/trust.ts
8920
+ var import_chalk15 = __toESM(require("chalk"));
8921
+ init_trusted_hosts();
8922
+ function isValidHost(host) {
8923
+ return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
8924
+ }
8925
+ function registerTrustCommand(program2) {
8926
+ const trustCmd = program2.command("trust").description("Manage trusted network hosts (reduces approval friction for known destinations)");
8927
+ trustCmd.command("add <host>").description("Add a trusted host \u2014 pipe-chain blocks targeting this host are downgraded").action((host) => {
8928
+ const normalized = normalizeHost(host.trim());
8929
+ if (!isValidHost(normalized)) {
8930
+ console.error(
8931
+ import_chalk15.default.red(`
8932
+ \u274C Invalid host: "${host}"
8933
+ `) + import_chalk15.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
8934
+ );
8935
+ process.exit(1);
8936
+ }
8937
+ addTrustedHost(normalized);
8938
+ console.log(import_chalk15.default.green(`
8939
+ \u2705 ${normalized} added to trusted hosts.`));
8940
+ console.log(
8941
+ import_chalk15.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
8942
+ );
8943
+ });
8944
+ trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
8945
+ const normalized = normalizeHost(host.trim());
8946
+ const removed = removeTrustedHost(normalized);
8947
+ if (!removed) {
8948
+ console.error(import_chalk15.default.yellow(`
8949
+ \u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
8950
+ `));
8951
+ process.exit(1);
8952
+ }
8953
+ console.log(import_chalk15.default.green(`
8954
+ \u2705 ${normalized} removed from trusted hosts.
8955
+ `));
8956
+ });
8957
+ trustCmd.command("list").description("Show all trusted hosts").action(() => {
8958
+ const hosts = readTrustedHosts();
8959
+ if (hosts.length === 0) {
8960
+ console.log(import_chalk15.default.gray("\n No trusted hosts configured.\n"));
8961
+ console.log(` Add one: ${import_chalk15.default.cyan("node9 trust add api.mycompany.com")}
8962
+ `);
8963
+ return;
8964
+ }
8965
+ console.log(import_chalk15.default.bold("\n\u{1F513} Trusted Hosts\n"));
8966
+ for (const entry of hosts) {
8967
+ const date = new Date(entry.addedAt).toLocaleDateString();
8968
+ console.log(` ${import_chalk15.default.cyan(entry.host.padEnd(40))} ${import_chalk15.default.gray(`added ${date}`)}`);
8969
+ }
8970
+ console.log("");
8971
+ });
8972
+ }
8973
+
8090
8974
  // src/cli.ts
8091
8975
  var { version } = JSON.parse(
8092
- import_fs21.default.readFileSync(import_path23.default.join(__dirname, "../package.json"), "utf-8")
8976
+ import_fs24.default.readFileSync(import_path26.default.join(__dirname, "../package.json"), "utf-8")
8093
8977
  );
8094
8978
  var program = new import_commander.Command();
8095
8979
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
8096
8980
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
8097
8981
  const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
8098
- const credPath = import_path23.default.join(import_os19.default.homedir(), ".node9", "credentials.json");
8099
- if (!import_fs21.default.existsSync(import_path23.default.dirname(credPath)))
8100
- import_fs21.default.mkdirSync(import_path23.default.dirname(credPath), { recursive: true });
8982
+ const credPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
8983
+ if (!import_fs24.default.existsSync(import_path26.default.dirname(credPath)))
8984
+ import_fs24.default.mkdirSync(import_path26.default.dirname(credPath), { recursive: true });
8101
8985
  const profileName = options.profile || "default";
8102
8986
  let existingCreds = {};
8103
8987
  try {
8104
- if (import_fs21.default.existsSync(credPath)) {
8105
- const raw = JSON.parse(import_fs21.default.readFileSync(credPath, "utf-8"));
8988
+ if (import_fs24.default.existsSync(credPath)) {
8989
+ const raw = JSON.parse(import_fs24.default.readFileSync(credPath, "utf-8"));
8106
8990
  if (raw.apiKey) {
8107
8991
  existingCreds = {
8108
8992
  default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
@@ -8114,13 +8998,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8114
8998
  } catch {
8115
8999
  }
8116
9000
  existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
8117
- import_fs21.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
9001
+ import_fs24.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
8118
9002
  if (profileName === "default") {
8119
- const configPath = import_path23.default.join(import_os19.default.homedir(), ".node9", "config.json");
9003
+ const configPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "config.json");
8120
9004
  let config = {};
8121
9005
  try {
8122
- if (import_fs21.default.existsSync(configPath))
8123
- config = JSON.parse(import_fs21.default.readFileSync(configPath, "utf-8"));
9006
+ if (import_fs24.default.existsSync(configPath))
9007
+ config = JSON.parse(import_fs24.default.readFileSync(configPath, "utf-8"));
8124
9008
  } catch {
8125
9009
  }
8126
9010
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -8135,36 +9019,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
8135
9019
  approvers.cloud = false;
8136
9020
  }
8137
9021
  s.approvers = approvers;
8138
- if (!import_fs21.default.existsSync(import_path23.default.dirname(configPath)))
8139
- import_fs21.default.mkdirSync(import_path23.default.dirname(configPath), { recursive: true });
8140
- import_fs21.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
9022
+ if (!import_fs24.default.existsSync(import_path26.default.dirname(configPath)))
9023
+ import_fs24.default.mkdirSync(import_path26.default.dirname(configPath), { recursive: true });
9024
+ import_fs24.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
8141
9025
  }
8142
9026
  if (options.profile && profileName !== "default") {
8143
- console.log(import_chalk15.default.green(`\u2705 Profile "${profileName}" saved`));
8144
- console.log(import_chalk15.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
9027
+ console.log(import_chalk17.default.green(`\u2705 Profile "${profileName}" saved`));
9028
+ console.log(import_chalk17.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
8145
9029
  } else if (options.local) {
8146
- console.log(import_chalk15.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
8147
- console.log(import_chalk15.default.gray(` All decisions stay on this machine.`));
9030
+ console.log(import_chalk17.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
9031
+ console.log(import_chalk17.default.gray(` All decisions stay on this machine.`));
8148
9032
  } else {
8149
- console.log(import_chalk15.default.green(`\u2705 Logged in \u2014 agent mode`));
8150
- console.log(import_chalk15.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
9033
+ console.log(import_chalk17.default.green(`\u2705 Logged in \u2014 agent mode`));
9034
+ console.log(import_chalk17.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
8151
9035
  }
8152
9036
  });
8153
9037
  program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
8154
9038
  if (target === "gemini") return await setupGemini();
8155
9039
  if (target === "claude") return await setupClaude();
8156
9040
  if (target === "cursor") return await setupCursor();
8157
- console.error(import_chalk15.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9041
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8158
9042
  process.exit(1);
8159
9043
  });
8160
9044
  program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
8161
9045
  if (!target) {
8162
- console.log(import_chalk15.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
8163
- console.log(" Usage: " + import_chalk15.default.white("node9 setup <target>") + "\n");
9046
+ console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
9047
+ console.log(" Usage: " + import_chalk17.default.white("node9 setup <target>") + "\n");
8164
9048
  console.log(" Targets:");
8165
- console.log(" " + import_chalk15.default.green("claude") + " \u2014 Claude Code (hook mode)");
8166
- console.log(" " + import_chalk15.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
8167
- console.log(" " + import_chalk15.default.green("cursor") + " \u2014 Cursor (hook mode)");
9049
+ console.log(" " + import_chalk17.default.green("claude") + " \u2014 Claude Code (hook mode)");
9050
+ console.log(" " + import_chalk17.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
9051
+ console.log(" " + import_chalk17.default.green("cursor") + " \u2014 Cursor (hook mode)");
8168
9052
  console.log("");
8169
9053
  return;
8170
9054
  }
@@ -8172,7 +9056,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
8172
9056
  if (t === "gemini") return await setupGemini();
8173
9057
  if (t === "claude") return await setupClaude();
8174
9058
  if (t === "cursor") return await setupCursor();
8175
- console.error(import_chalk15.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9059
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8176
9060
  process.exit(1);
8177
9061
  });
8178
9062
  program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
@@ -8181,30 +9065,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
8181
9065
  else if (target === "gemini") fn = teardownGemini;
8182
9066
  else if (target === "cursor") fn = teardownCursor;
8183
9067
  else {
8184
- console.error(import_chalk15.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
9068
+ console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
8185
9069
  process.exit(1);
8186
9070
  }
8187
- console.log(import_chalk15.default.cyan(`
9071
+ console.log(import_chalk17.default.cyan(`
8188
9072
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
8189
9073
  `));
8190
9074
  try {
8191
9075
  fn();
8192
9076
  } catch (err) {
8193
- console.error(import_chalk15.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
9077
+ console.error(import_chalk17.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
8194
9078
  process.exit(1);
8195
9079
  }
8196
- console.log(import_chalk15.default.gray("\n Restart the agent for changes to take effect."));
9080
+ console.log(import_chalk17.default.gray("\n Restart the agent for changes to take effect."));
8197
9081
  });
8198
9082
  program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
8199
- console.log(import_chalk15.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
8200
- console.log(import_chalk15.default.bold("Stopping daemon..."));
9083
+ console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
9084
+ console.log(import_chalk17.default.bold("Stopping daemon..."));
8201
9085
  try {
8202
9086
  stopDaemon();
8203
- console.log(import_chalk15.default.green(" \u2705 Daemon stopped"));
9087
+ console.log(import_chalk17.default.green(" \u2705 Daemon stopped"));
8204
9088
  } catch {
8205
- console.log(import_chalk15.default.blue(" \u2139\uFE0F Daemon was not running"));
9089
+ console.log(import_chalk17.default.blue(" \u2139\uFE0F Daemon was not running"));
8206
9090
  }
8207
- console.log(import_chalk15.default.bold("\nRemoving hooks..."));
9091
+ console.log(import_chalk17.default.bold("\nRemoving hooks..."));
8208
9092
  let teardownFailed = false;
8209
9093
  for (const [label, fn] of [
8210
9094
  ["Claude", teardownClaude],
@@ -8216,45 +9100,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
8216
9100
  } catch (err) {
8217
9101
  teardownFailed = true;
8218
9102
  console.error(
8219
- import_chalk15.default.red(
9103
+ import_chalk17.default.red(
8220
9104
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
8221
9105
  )
8222
9106
  );
8223
9107
  }
8224
9108
  }
8225
9109
  if (options.purge) {
8226
- const node9Dir = import_path23.default.join(import_os19.default.homedir(), ".node9");
8227
- if (import_fs21.default.existsSync(node9Dir)) {
9110
+ const node9Dir = import_path26.default.join(import_os22.default.homedir(), ".node9");
9111
+ if (import_fs24.default.existsSync(node9Dir)) {
8228
9112
  const confirmed = await (0, import_prompts3.confirm)({
8229
9113
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
8230
9114
  default: false
8231
9115
  });
8232
9116
  if (confirmed) {
8233
- import_fs21.default.rmSync(node9Dir, { recursive: true });
8234
- if (import_fs21.default.existsSync(node9Dir)) {
9117
+ import_fs24.default.rmSync(node9Dir, { recursive: true });
9118
+ if (import_fs24.default.existsSync(node9Dir)) {
8235
9119
  console.error(
8236
- import_chalk15.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
9120
+ import_chalk17.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
8237
9121
  );
8238
9122
  } else {
8239
- console.log(import_chalk15.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
9123
+ console.log(import_chalk17.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
8240
9124
  }
8241
9125
  } else {
8242
- console.log(import_chalk15.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
9126
+ console.log(import_chalk17.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
8243
9127
  }
8244
9128
  } else {
8245
- console.log(import_chalk15.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
9129
+ console.log(import_chalk17.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
8246
9130
  }
8247
9131
  } else {
8248
9132
  console.log(
8249
- import_chalk15.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
9133
+ import_chalk17.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
8250
9134
  );
8251
9135
  }
8252
9136
  if (teardownFailed) {
8253
- console.error(import_chalk15.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
9137
+ console.error(import_chalk17.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
8254
9138
  process.exit(1);
8255
9139
  }
8256
- console.log(import_chalk15.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
8257
- console.log(import_chalk15.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
9140
+ console.log(import_chalk17.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
9141
+ console.log(import_chalk17.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
8258
9142
  });
8259
9143
  registerDoctorCommand(program, version);
8260
9144
  program.command("explain").description(
@@ -8267,7 +9151,7 @@ program.command("explain").description(
8267
9151
  try {
8268
9152
  args = JSON.parse(trimmed);
8269
9153
  } catch {
8270
- console.error(import_chalk15.default.red(`
9154
+ console.error(import_chalk17.default.red(`
8271
9155
  \u274C Invalid JSON: ${trimmed}
8272
9156
  `));
8273
9157
  process.exit(1);
@@ -8278,83 +9162,59 @@ program.command("explain").description(
8278
9162
  }
8279
9163
  const result = await explainPolicy(tool, args);
8280
9164
  console.log("");
8281
- console.log(import_chalk15.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
9165
+ console.log(import_chalk17.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
8282
9166
  console.log("");
8283
- console.log(` ${import_chalk15.default.bold("Tool:")} ${import_chalk15.default.white(result.tool)}`);
9167
+ console.log(` ${import_chalk17.default.bold("Tool:")} ${import_chalk17.default.white(result.tool)}`);
8284
9168
  if (argsRaw) {
8285
9169
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
8286
- console.log(` ${import_chalk15.default.bold("Input:")} ${import_chalk15.default.gray(preview)}`);
9170
+ console.log(` ${import_chalk17.default.bold("Input:")} ${import_chalk17.default.gray(preview)}`);
8287
9171
  }
8288
9172
  console.log("");
8289
- console.log(import_chalk15.default.bold("Config Sources (Waterfall):"));
9173
+ console.log(import_chalk17.default.bold("Config Sources (Waterfall):"));
8290
9174
  for (const tier of result.waterfall) {
8291
- const num = import_chalk15.default.gray(` ${tier.tier}.`);
9175
+ const num = import_chalk17.default.gray(` ${tier.tier}.`);
8292
9176
  const label = tier.label.padEnd(16);
8293
9177
  let statusStr;
8294
9178
  if (tier.tier === 1) {
8295
- statusStr = import_chalk15.default.gray(tier.note ?? "");
9179
+ statusStr = import_chalk17.default.gray(tier.note ?? "");
8296
9180
  } else if (tier.status === "active") {
8297
- const loc = tier.path ? import_chalk15.default.gray(tier.path) : "";
8298
- const note = tier.note ? import_chalk15.default.gray(`(${tier.note})`) : "";
8299
- statusStr = import_chalk15.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
9181
+ const loc = tier.path ? import_chalk17.default.gray(tier.path) : "";
9182
+ const note = tier.note ? import_chalk17.default.gray(`(${tier.note})`) : "";
9183
+ statusStr = import_chalk17.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
8300
9184
  } else {
8301
- statusStr = import_chalk15.default.gray("\u25CB " + (tier.note ?? "not found"));
9185
+ statusStr = import_chalk17.default.gray("\u25CB " + (tier.note ?? "not found"));
8302
9186
  }
8303
- console.log(`${num} ${import_chalk15.default.white(label)} ${statusStr}`);
9187
+ console.log(`${num} ${import_chalk17.default.white(label)} ${statusStr}`);
8304
9188
  }
8305
9189
  console.log("");
8306
- console.log(import_chalk15.default.bold("Policy Evaluation:"));
9190
+ console.log(import_chalk17.default.bold("Policy Evaluation:"));
8307
9191
  for (const step of result.steps) {
8308
9192
  const isFinal = step.isFinal;
8309
9193
  let icon;
8310
- if (step.outcome === "allow") icon = import_chalk15.default.green(" \u2705");
8311
- else if (step.outcome === "review") icon = import_chalk15.default.red(" \u{1F534}");
8312
- else if (step.outcome === "skip") icon = import_chalk15.default.gray(" \u2500 ");
8313
- else icon = import_chalk15.default.gray(" \u25CB ");
9194
+ if (step.outcome === "allow") icon = import_chalk17.default.green(" \u2705");
9195
+ else if (step.outcome === "review") icon = import_chalk17.default.red(" \u{1F534}");
9196
+ else if (step.outcome === "skip") icon = import_chalk17.default.gray(" \u2500 ");
9197
+ else icon = import_chalk17.default.gray(" \u25CB ");
8314
9198
  const name = step.name.padEnd(18);
8315
- const nameStr = isFinal ? import_chalk15.default.white.bold(name) : import_chalk15.default.white(name);
8316
- const detail = isFinal ? import_chalk15.default.white(step.detail) : import_chalk15.default.gray(step.detail);
8317
- const arrow = isFinal ? import_chalk15.default.yellow(" \u2190 STOP") : "";
9199
+ const nameStr = isFinal ? import_chalk17.default.white.bold(name) : import_chalk17.default.white(name);
9200
+ const detail = isFinal ? import_chalk17.default.white(step.detail) : import_chalk17.default.gray(step.detail);
9201
+ const arrow = isFinal ? import_chalk17.default.yellow(" \u2190 STOP") : "";
8318
9202
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
8319
9203
  }
8320
9204
  console.log("");
8321
9205
  if (result.decision === "allow") {
8322
- console.log(import_chalk15.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk15.default.gray(" \u2014 no approval needed"));
9206
+ console.log(import_chalk17.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk17.default.gray(" \u2014 no approval needed"));
8323
9207
  } else {
8324
9208
  console.log(
8325
- import_chalk15.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk15.default.gray(" \u2014 human approval required")
9209
+ import_chalk17.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk17.default.gray(" \u2014 human approval required")
8326
9210
  );
8327
9211
  if (result.blockedByLabel) {
8328
- console.log(import_chalk15.default.gray(` Reason: ${result.blockedByLabel}`));
9212
+ console.log(import_chalk17.default.gray(` Reason: ${result.blockedByLabel}`));
8329
9213
  }
8330
9214
  }
8331
9215
  console.log("");
8332
9216
  });
8333
- 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) => {
8334
- const configPath = import_path23.default.join(import_os19.default.homedir(), ".node9", "config.json");
8335
- if (import_fs21.default.existsSync(configPath) && !options.force) {
8336
- console.log(import_chalk15.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
8337
- console.log(import_chalk15.default.gray(` Run with --force to overwrite.`));
8338
- return;
8339
- }
8340
- const requestedMode = options.mode.toLowerCase();
8341
- const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
8342
- const configToSave = {
8343
- ...DEFAULT_CONFIG,
8344
- settings: {
8345
- ...DEFAULT_CONFIG.settings,
8346
- mode: safeMode
8347
- }
8348
- };
8349
- const dir = import_path23.default.dirname(configPath);
8350
- if (!import_fs21.default.existsSync(dir)) import_fs21.default.mkdirSync(dir, { recursive: true });
8351
- import_fs21.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
8352
- console.log(import_chalk15.default.green(`\u2705 Global config created: ${configPath}`));
8353
- console.log(import_chalk15.default.cyan(` Mode set to: ${safeMode}`));
8354
- console.log(
8355
- import_chalk15.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
8356
- );
8357
- });
9217
+ registerInitCommand(program);
8358
9218
  registerAuditCommand(program);
8359
9219
  registerStatusCommand(program);
8360
9220
  registerDaemonCommand(program);
@@ -8363,7 +9223,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
8363
9223
  try {
8364
9224
  await startTail2(options);
8365
9225
  } catch (err) {
8366
- console.error(import_chalk15.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
9226
+ console.error(import_chalk17.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
8367
9227
  process.exit(1);
8368
9228
  }
8369
9229
  });
@@ -8375,7 +9235,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8375
9235
  const ms = parseDuration(options.duration);
8376
9236
  if (ms === null) {
8377
9237
  console.error(
8378
- import_chalk15.default.red(`
9238
+ import_chalk17.default.red(`
8379
9239
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
8380
9240
  `)
8381
9241
  );
@@ -8383,20 +9243,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
8383
9243
  }
8384
9244
  pauseNode9(ms, options.duration);
8385
9245
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
8386
- console.log(import_chalk15.default.yellow(`
9246
+ console.log(import_chalk17.default.yellow(`
8387
9247
  \u23F8 Node9 paused until ${expiresAt}`));
8388
- console.log(import_chalk15.default.gray(` All tool calls will be allowed without review.`));
8389
- console.log(import_chalk15.default.gray(` Run "node9 resume" to re-enable early.
9248
+ console.log(import_chalk17.default.gray(` All tool calls will be allowed without review.`));
9249
+ console.log(import_chalk17.default.gray(` Run "node9 resume" to re-enable early.
8390
9250
  `));
8391
9251
  });
8392
9252
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
8393
9253
  const { paused } = checkPause();
8394
9254
  if (!paused) {
8395
- console.log(import_chalk15.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
9255
+ console.log(import_chalk17.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
8396
9256
  return;
8397
9257
  }
8398
9258
  resumeNode9();
8399
- console.log(import_chalk15.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
9259
+ console.log(import_chalk17.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
8400
9260
  });
8401
9261
  var HOOK_BASED_AGENTS = {
8402
9262
  claude: "claude",
@@ -8409,15 +9269,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8409
9269
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
8410
9270
  const target = HOOK_BASED_AGENTS[firstArg2];
8411
9271
  console.error(
8412
- import_chalk15.default.yellow(`
9272
+ import_chalk17.default.yellow(`
8413
9273
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
8414
9274
  );
8415
- console.error(import_chalk15.default.white(`
9275
+ console.error(import_chalk17.default.white(`
8416
9276
  "${target}" uses its own hook system. Use:`));
8417
9277
  console.error(
8418
- import_chalk15.default.green(` node9 addto ${target} `) + import_chalk15.default.gray("# one-time setup")
9278
+ import_chalk17.default.green(` node9 addto ${target} `) + import_chalk17.default.gray("# one-time setup")
8419
9279
  );
8420
- console.error(import_chalk15.default.green(` ${target} `) + import_chalk15.default.gray("# run normally"));
9280
+ console.error(import_chalk17.default.green(` ${target} `) + import_chalk17.default.gray("# run normally"));
8421
9281
  process.exit(1);
8422
9282
  }
8423
9283
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -8434,7 +9294,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8434
9294
  }
8435
9295
  );
8436
9296
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
8437
- console.error(import_chalk15.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
9297
+ console.error(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
8438
9298
  const daemonReady = await autoStartDaemonAndWait();
8439
9299
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
8440
9300
  }
@@ -8447,12 +9307,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8447
9307
  }
8448
9308
  if (!result.approved) {
8449
9309
  console.error(
8450
- import_chalk15.default.red(`
9310
+ import_chalk17.default.red(`
8451
9311
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
8452
9312
  );
8453
9313
  process.exit(1);
8454
9314
  }
8455
- console.error(import_chalk15.default.green("\n\u2705 Approved \u2014 running command...\n"));
9315
+ console.error(import_chalk17.default.green("\n\u2705 Approved \u2014 running command...\n"));
8456
9316
  await runProxy(fullCommand);
8457
9317
  } else {
8458
9318
  program.help();
@@ -8461,14 +9321,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
8461
9321
  registerUndoCommand(program);
8462
9322
  registerShieldCommand(program);
8463
9323
  registerConfigShowCommand(program);
9324
+ registerTrustCommand(program);
8464
9325
  if (process.argv[2] !== "daemon") {
8465
9326
  process.on("unhandledRejection", (reason) => {
8466
9327
  const isCheckHook = process.argv[2] === "check";
8467
9328
  if (isCheckHook) {
8468
9329
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
8469
- const logPath = import_path23.default.join(import_os19.default.homedir(), ".node9", "hook-debug.log");
9330
+ const logPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
8470
9331
  const msg = reason instanceof Error ? reason.message : String(reason);
8471
- import_fs21.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
9332
+ import_fs24.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
8472
9333
  `);
8473
9334
  }
8474
9335
  process.exit(0);