@node9/proxy 1.1.5 → 1.1.7

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
@@ -217,21 +217,6 @@ ${smartTruncate(str, 500)}`
217
217
  }
218
218
  return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
219
219
  }
220
- function sendDesktopNotification(title, body) {
221
- if (isTestEnv()) return;
222
- try {
223
- if (process.platform === "darwin") {
224
- const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
225
- (0, import_child_process.spawn)("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
226
- } else if (process.platform === "linux") {
227
- (0, import_child_process.spawn)("notify-send", [title, body, "--icon=dialog-warning"], {
228
- detached: true,
229
- stdio: "ignore"
230
- }).unref();
231
- }
232
- } catch {
233
- }
234
- }
235
220
  function escapePango(text) {
236
221
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
237
222
  }
@@ -274,9 +259,6 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
274
259
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
275
260
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
276
261
  const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
277
- process.stderr.write(import_chalk.default.yellow(`
278
- \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
279
- `));
280
262
  return new Promise((resolve) => {
281
263
  let childProcess = null;
282
264
  const onAbort = () => {
@@ -348,13 +330,12 @@ end run`;
348
330
  }
349
331
  });
350
332
  }
351
- var import_child_process, import_path2, import_chalk, isTestEnv;
333
+ var import_child_process, import_path2, isTestEnv;
352
334
  var init_native = __esm({
353
335
  "src/ui/native.ts"() {
354
336
  "use strict";
355
337
  import_child_process = require("child_process");
356
338
  import_path2 = __toESM(require("path"));
357
- import_chalk = __toESM(require("chalk"));
358
339
  init_context_sniper();
359
340
  isTestEnv = () => {
360
341
  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";
@@ -443,6 +424,7 @@ var init_config_schema = __esm({
443
424
  enableUndo: import_zod.z.boolean().optional(),
444
425
  enableHookLogDebug: import_zod.z.boolean().optional(),
445
426
  approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
427
+ approvalTimeoutSeconds: import_zod.z.number().nonnegative().optional(),
446
428
  flightRecorder: import_zod.z.boolean().optional(),
447
429
  approvers: import_zod.z.object({
448
430
  native: import_zod.z.boolean().optional(),
@@ -896,9 +878,9 @@ var init_dlp = __esm({
896
878
  /[/\\][^/\\]+\.key$/i,
897
879
  /[/\\][^/\\]+\.p12$/i,
898
880
  /[/\\][^/\\]+\.pfx$/i,
899
- /^\/etc\/passwd$/,
900
- /^\/etc\/shadow$/,
901
- /^\/etc\/sudoers$/,
881
+ /^(?:[a-zA-Z]:)?\/etc\/passwd$/,
882
+ /^(?:[a-zA-Z]:)?\/etc\/shadow$/,
883
+ /^(?:[a-zA-Z]:)?\/etc\/sudoers$/,
902
884
  /[/\\]credentials\.json$/i,
903
885
  /[/\\]id_rsa$/i,
904
886
  /[/\\]id_ed25519$/i,
@@ -1664,14 +1646,12 @@ function getPersistentDecision(toolName) {
1664
1646
  }
1665
1647
  return null;
1666
1648
  }
1667
- async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1649
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
1668
1650
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1669
- const checkCtrl = new AbortController();
1670
- const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
1671
- const onAbort = () => checkCtrl.abort();
1672
- if (signal) signal.addEventListener("abort", onAbort);
1651
+ const ctrl = new AbortController();
1652
+ const timer = setTimeout(() => ctrl.abort(), 5e3);
1673
1653
  try {
1674
- const checkRes = await fetch(`${base}/check`, {
1654
+ const res = await fetch(`${base}/check`, {
1675
1655
  method: "POST",
1676
1656
  headers: { "Content-Type": "application/json" },
1677
1657
  body: JSON.stringify({
@@ -1682,32 +1662,34 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId)
1682
1662
  fromCLI: true,
1683
1663
  // Pass the flight-recorder ID so the daemon uses the same UUID for
1684
1664
  // activity-result as the CLI used for the pending activity event.
1685
- // Without this, the two UUIDs never match and tail.ts never resolves
1686
- // the pending item.
1687
1665
  activityId,
1688
- ...riskMetadata && { riskMetadata }
1666
+ ...riskMetadata && { riskMetadata },
1667
+ ...cwd && { cwd }
1689
1668
  }),
1690
- signal: checkCtrl.signal
1669
+ signal: ctrl.signal
1691
1670
  });
1692
- if (!checkRes.ok) throw new Error("Daemon fail");
1693
- const { id } = await checkRes.json();
1694
- const waitCtrl = new AbortController();
1695
- const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
1696
- const onWaitAbort = () => waitCtrl.abort();
1697
- if (signal) signal.addEventListener("abort", onWaitAbort);
1698
- try {
1699
- const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
1700
- if (!waitRes.ok) return "deny";
1701
- const { decision } = await waitRes.json();
1702
- if (decision === "allow") return "allow";
1703
- if (decision === "abandoned") return "abandoned";
1704
- return "deny";
1705
- } finally {
1706
- clearTimeout(waitTimer);
1707
- if (signal) signal.removeEventListener("abort", onWaitAbort);
1708
- }
1671
+ if (!res.ok) throw new Error("Daemon fail");
1672
+ const { id } = await res.json();
1673
+ return id;
1674
+ } finally {
1675
+ clearTimeout(timer);
1676
+ }
1677
+ }
1678
+ async function waitForDaemonDecision(id, signal) {
1679
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1680
+ const waitCtrl = new AbortController();
1681
+ const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
1682
+ const onAbort = () => waitCtrl.abort();
1683
+ if (signal) signal.addEventListener("abort", onAbort);
1684
+ try {
1685
+ const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
1686
+ if (!waitRes.ok) return { decision: "deny" };
1687
+ const { decision, source } = await waitRes.json();
1688
+ if (decision === "allow") return { decision: "allow", source };
1689
+ if (decision === "abandoned") return { decision: "abandoned", source };
1690
+ return { decision: "deny", source };
1709
1691
  } finally {
1710
- clearTimeout(checkTimer);
1692
+ clearTimeout(waitTimer);
1711
1693
  if (signal) signal.removeEventListener("abort", onAbort);
1712
1694
  }
1713
1695
  }
@@ -1754,12 +1736,12 @@ function notifyActivity(data) {
1754
1736
  }
1755
1737
  });
1756
1738
  }
1757
- async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1739
+ async function authorizeHeadless(toolName, args, meta, options) {
1758
1740
  if (!options?.calledFromDaemon) {
1759
1741
  const actId = (0, import_crypto2.randomUUID)();
1760
1742
  const actTs = Date.now();
1761
1743
  await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1762
- const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1744
+ const result = await _authorizeHeadlessCore(toolName, args, meta, {
1763
1745
  ...options,
1764
1746
  activityId: actId
1765
1747
  });
@@ -1774,14 +1756,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1774
1756
  }
1775
1757
  return result;
1776
1758
  }
1777
- return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1759
+ return _authorizeHeadlessCore(toolName, args, meta, options);
1778
1760
  }
1779
- async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1761
+ async function _authorizeHeadlessCore(toolName, args, meta, options) {
1780
1762
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1781
1763
  const pauseState = checkPause();
1782
1764
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
1783
1765
  const creds = getCredentials();
1784
- const config = getConfig();
1766
+ const config = getConfig(options?.cwd);
1785
1767
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
1786
1768
  const approvers = {
1787
1769
  ...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
@@ -1826,10 +1808,6 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1826
1808
  if (approvers.cloud && creds?.apiKey) {
1827
1809
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
1828
1810
  }
1829
- sendDesktopNotification(
1830
- "Node9 Audit Mode",
1831
- `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
1832
- );
1833
1811
  }
1834
1812
  }
1835
1813
  return { approved: true, checkedBy: "audit" };
@@ -1889,23 +1867,12 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1889
1867
  return { approved: true };
1890
1868
  }
1891
1869
  let cloudRequestId = null;
1892
- let isRemoteLocked = false;
1893
1870
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
1894
1871
  if (cloudEnforced) {
1895
1872
  try {
1896
1873
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
1897
1874
  if (!initResult.pending) {
1898
1875
  if (initResult.shadowMode) {
1899
- console.error(
1900
- import_chalk2.default.yellow(
1901
- `
1902
- \u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
1903
- )
1904
- );
1905
- if (initResult.shadowReason) {
1906
- console.error(import_chalk2.default.dim(` Reason: ${initResult.shadowReason}
1907
- `));
1908
- }
1909
1876
  return { approved: true, checkedBy: "cloud" };
1910
1877
  }
1911
1878
  return {
@@ -1917,36 +1884,8 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1917
1884
  };
1918
1885
  }
1919
1886
  cloudRequestId = initResult.requestId || null;
1920
- isRemoteLocked = !!initResult.remoteApprovalOnly;
1921
1887
  explainableLabel = "Organization Policy (SaaS)";
1922
- } catch (err) {
1923
- const error = err;
1924
- const isAuthError = error.message.includes("401") || error.message.includes("403");
1925
- const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
1926
- const reason = isAuthError ? "Invalid or missing API key. Run `node9 login` to generate a key (must start with n9_live_)." : isNetworkError ? "Could not reach the Node9 cloud. Check your network or API URL." : error.message;
1927
- console.error(
1928
- import_chalk2.default.yellow(`
1929
- \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + import_chalk2.default.dim(`
1930
- Falling back to local rules...
1931
- `)
1932
- );
1933
- }
1934
- }
1935
- if (!options?.calledFromDaemon) {
1936
- if (cloudEnforced && cloudRequestId) {
1937
- console.error(
1938
- import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
1939
- );
1940
- console.error(
1941
- import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n")
1942
- );
1943
- } else if (!cloudEnforced) {
1944
- const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
1945
- console.error(
1946
- import_chalk2.default.dim(`
1947
- \u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
1948
- `)
1949
- );
1888
+ } catch {
1950
1889
  }
1951
1890
  }
1952
1891
  const abortController = new AbortController();
@@ -1973,15 +1912,29 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1973
1912
  }
1974
1913
  let viewerId = null;
1975
1914
  const internalToken = getInternalToken();
1915
+ let daemonEntryId = null;
1916
+ if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
1917
+ if (cloudEnforced && cloudRequestId) {
1918
+ viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
1919
+ daemonEntryId = viewerId;
1920
+ } else {
1921
+ try {
1922
+ daemonEntryId = await registerDaemonEntry(
1923
+ toolName,
1924
+ args,
1925
+ meta,
1926
+ riskMetadata,
1927
+ options?.activityId,
1928
+ options?.cwd
1929
+ );
1930
+ } catch {
1931
+ }
1932
+ }
1933
+ }
1976
1934
  if (cloudEnforced && cloudRequestId) {
1977
1935
  racePromises.push(
1978
1936
  (async () => {
1979
1937
  try {
1980
- if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
1981
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
1982
- () => null
1983
- );
1984
- }
1985
1938
  const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
1986
1939
  return {
1987
1940
  approved: cloudResult.approved,
@@ -2006,7 +1959,7 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
2006
1959
  args,
2007
1960
  meta?.agent,
2008
1961
  explainableLabel,
2009
- isRemoteLocked,
1962
+ false,
2010
1963
  signal,
2011
1964
  policyMatchedField,
2012
1965
  policyMatchedWord
@@ -2021,96 +1974,31 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
2021
1974
  reason: isApproved ? void 0 : "The human user clicked 'Block' on the system dialog window.",
2022
1975
  checkedBy: isApproved ? "daemon" : void 0,
2023
1976
  blockedBy: isApproved ? void 0 : "local-decision",
2024
- blockedByLabel: "User Decision (Native)"
1977
+ blockedByLabel: "User Decision (Native)",
1978
+ decisionSource: "native"
2025
1979
  };
2026
1980
  })()
2027
1981
  );
2028
1982
  }
2029
- if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
2030
- racePromises.push(
2031
- (async () => {
2032
- try {
2033
- if (!approvers.native && !cloudEnforced) {
2034
- console.error(
2035
- import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
2036
- );
2037
- console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
2038
- `));
2039
- }
2040
- const daemonDecision = await askDaemon(
2041
- toolName,
2042
- args,
2043
- meta,
2044
- signal,
2045
- riskMetadata,
2046
- options?.activityId
2047
- );
2048
- if (daemonDecision === "abandoned") throw new Error("Abandoned");
2049
- const isApproved = daemonDecision === "allow";
2050
- return {
2051
- approved: isApproved,
2052
- reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
2053
- checkedBy: isApproved ? "daemon" : void 0,
2054
- blockedBy: isApproved ? void 0 : "local-decision",
2055
- blockedByLabel: "User Decision (Browser)"
2056
- };
2057
- } catch (err) {
2058
- throw err;
2059
- }
2060
- })()
2061
- );
2062
- }
2063
- if (approvers.terminal && allowTerminalFallback && process.stdout.isTTY) {
1983
+ if (daemonEntryId && (approvers.browser || approvers.terminal)) {
2064
1984
  racePromises.push(
2065
1985
  (async () => {
2066
- try {
2067
- if (explainableLabel.includes("DLP")) {
2068
- console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
2069
- console.log(
2070
- import_chalk2.default.red.bold(` A sensitive secret was detected in the tool arguments!`)
2071
- );
2072
- } else {
2073
- console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
2074
- }
2075
- console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
2076
- console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
2077
- if (isRemoteLocked) {
2078
- console.log(import_chalk2.default.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
2079
- `));
2080
- await new Promise((_, reject) => {
2081
- signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
2082
- });
2083
- }
2084
- const TIMEOUT_MS = 6e4;
2085
- let timer;
2086
- const result = await new Promise((resolve, reject) => {
2087
- timer = setTimeout(() => reject(new Error("Terminal Timeout")), TIMEOUT_MS);
2088
- (0, import_prompts.confirm)(
2089
- { message: `Authorize? (auto-deny in ${TIMEOUT_MS / 1e3}s)`, default: false },
2090
- { signal }
2091
- ).then(resolve).catch(reject);
2092
- });
2093
- clearTimeout(timer);
2094
- return {
2095
- approved: result,
2096
- reason: result ? void 0 : "The human user typed 'N' in the terminal to reject this action.",
2097
- checkedBy: result ? "terminal" : void 0,
2098
- blockedBy: result ? void 0 : "local-decision",
2099
- blockedByLabel: "User Decision (Terminal)"
2100
- };
2101
- } catch (err) {
2102
- const error = err;
2103
- if (error.name === "AbortError" || error.message?.includes("Prompt was canceled") || error.message?.includes("Aborted by SaaS"))
2104
- throw err;
2105
- if (error.message === "Terminal Timeout") {
2106
- return {
2107
- approved: false,
2108
- reason: "The terminal prompt timed out without a human response.",
2109
- blockedBy: "local-decision"
2110
- };
2111
- }
2112
- throw err;
2113
- }
1986
+ const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
1987
+ daemonEntryId,
1988
+ signal
1989
+ );
1990
+ if (daemonDecision === "abandoned") throw new Error("Abandoned");
1991
+ const isApproved = daemonDecision === "allow";
1992
+ const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
1993
+ const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
1994
+ return {
1995
+ approved: isApproved,
1996
+ reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
1997
+ checkedBy: isApproved ? "daemon" : void 0,
1998
+ blockedBy: isApproved ? void 0 : "local-decision",
1999
+ blockedByLabel: `User Decision (${via})`,
2000
+ decisionSource: src
2001
+ };
2114
2002
  })()
2115
2003
  );
2116
2004
  }
@@ -2161,7 +2049,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2161
2049
  }
2162
2050
  });
2163
2051
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
2164
- await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
2052
+ await resolveNode9SaaS(
2053
+ cloudRequestId,
2054
+ creds,
2055
+ finalResult.approved,
2056
+ finalResult.decisionSource ?? finalResult.checkedBy ?? "local"
2057
+ );
2165
2058
  }
2166
2059
  if (!isManual) {
2167
2060
  appendLocalAudit(
@@ -2209,6 +2102,8 @@ function getConfig(cwd) {
2209
2102
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
2210
2103
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
2211
2104
  if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
2105
+ if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
2106
+ mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
2212
2107
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
2213
2108
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
2214
2109
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -2433,11 +2328,9 @@ async function pollNode9SaaS(requestId, creds, signal) {
2433
2328
  if (!statusRes.ok) continue;
2434
2329
  const { status, reason } = await statusRes.json();
2435
2330
  if (status === "APPROVED") {
2436
- console.error(import_chalk2.default.green("\u2705 Approved via Cloud.\n"));
2437
2331
  return { approved: true, reason };
2438
2332
  }
2439
2333
  if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
2440
- console.error(import_chalk2.default.red("\u274C Denied via Cloud.\n"));
2441
2334
  return { approved: false, reason };
2442
2335
  }
2443
2336
  } catch {
@@ -2445,27 +2338,40 @@ async function pollNode9SaaS(requestId, creds, signal) {
2445
2338
  }
2446
2339
  return { approved: false, reason: "Cloud approval timed out after 10 minutes." };
2447
2340
  }
2448
- async function resolveNode9SaaS(requestId, creds, approved) {
2341
+ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2449
2342
  try {
2450
2343
  const resolveUrl = `${creds.apiUrl}/requests/${requestId}`;
2451
2344
  const ctrl = new AbortController();
2452
2345
  const timer = setTimeout(() => ctrl.abort(), 5e3);
2453
- await fetch(resolveUrl, {
2346
+ const res = await fetch(resolveUrl, {
2454
2347
  method: "PATCH",
2455
2348
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
2456
- body: JSON.stringify({ decision: approved ? "APPROVED" : "DENIED" }),
2349
+ body: JSON.stringify({
2350
+ decision: approved ? "APPROVED" : "DENIED",
2351
+ ...decidedBy && { decidedBy }
2352
+ }),
2457
2353
  signal: ctrl.signal
2458
2354
  });
2459
2355
  clearTimeout(timer);
2460
- } catch {
2356
+ if (!res.ok) {
2357
+ import_fs3.default.appendFileSync(
2358
+ HOOK_DEBUG_LOG,
2359
+ `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
2360
+ `
2361
+ );
2362
+ }
2363
+ } catch (err) {
2364
+ import_fs3.default.appendFileSync(
2365
+ HOOK_DEBUG_LOG,
2366
+ `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
2367
+ `
2368
+ );
2461
2369
  }
2462
2370
  }
2463
- var import_chalk2, import_prompts, import_fs3, import_path5, import_os2, import_net, import_crypto2, import_child_process2, import_picomatch, import_safe_regex2, import_sh_syntax, PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2371
+ var import_fs3, import_path5, import_os2, import_net, import_crypto2, import_child_process2, import_picomatch, import_safe_regex2, import_sh_syntax, PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2464
2372
  var init_core = __esm({
2465
2373
  "src/core.ts"() {
2466
2374
  "use strict";
2467
- import_chalk2 = __toESM(require("chalk"));
2468
- import_prompts = require("@inquirer/prompts");
2469
2375
  import_fs3 = __toESM(require("fs"));
2470
2376
  import_path5 = __toESM(require("path"));
2471
2377
  import_os2 = __toESM(require("os"));
@@ -2502,8 +2408,8 @@ var init_core = __esm({
2502
2408
  enableUndo: true,
2503
2409
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2504
2410
  enableHookLogDebug: true,
2505
- approvalTimeoutMs: 3e4,
2506
- // 30-second auto-deny timeout
2411
+ approvalTimeoutMs: 12e4,
2412
+ // 120-second auto-deny timeout
2507
2413
  flightRecorder: true,
2508
2414
  approvers: { native: true, browser: true, cloud: false, terminal: true }
2509
2415
  },
@@ -3767,7 +3673,7 @@ var init_ui = __esm({
3767
3673
  const res = await fetch('/decision/' + id, {
3768
3674
  method: 'POST',
3769
3675
  headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3770
- body: JSON.stringify({ decision, persist: !!persist }),
3676
+ body: JSON.stringify({ decision, persist: !!persist, source: 'browser' }),
3771
3677
  });
3772
3678
  if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3773
3679
  card?.remove();
@@ -3786,7 +3692,7 @@ var init_ui = __esm({
3786
3692
  const res = await fetch('/decision/' + id, {
3787
3693
  method: 'POST',
3788
3694
  headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3789
- body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3695
+ body: JSON.stringify({ decision: 'trust', trustDuration: duration, source: 'browser' }),
3790
3696
  });
3791
3697
  if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3792
3698
  card?.remove();
@@ -4308,7 +4214,7 @@ data: ${JSON.stringify(data)}
4308
4214
  `;
4309
4215
  sseClients.forEach((client) => {
4310
4216
  try {
4311
- client.write(msg);
4217
+ client.res.write(msg);
4312
4218
  } catch {
4313
4219
  sseClients.delete(client);
4314
4220
  }
@@ -4354,6 +4260,7 @@ function startDaemon() {
4354
4260
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
4355
4261
  const watchMode = process.env.NODE9_WATCH_MODE === "1";
4356
4262
  let idleTimer;
4263
+ let browserOpened = false;
4357
4264
  function resetIdleTimer() {
4358
4265
  if (watchMode) return;
4359
4266
  if (idleTimer) clearTimeout(idleTimer);
@@ -4370,12 +4277,15 @@ function startDaemon() {
4370
4277
  }
4371
4278
  resetIdleTimer();
4372
4279
  const server = import_http.default.createServer(async (req, res) => {
4373
- const { pathname } = new URL(req.url || "/", `http://${req.headers.host}`);
4280
+ const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
4281
+ const { pathname } = reqUrl;
4374
4282
  if (req.method === "GET" && pathname === "/") {
4375
4283
  res.writeHead(200, { "Content-Type": "text/html" });
4376
4284
  return res.end(UI_HTML);
4377
4285
  }
4378
4286
  if (req.method === "GET" && pathname === "/events") {
4287
+ const capParam = reqUrl.searchParams.get("capabilities") ?? "";
4288
+ const capabilities = capParam ? capParam.split(",").map((s) => s.trim()).filter(Boolean) : [];
4379
4289
  res.writeHead(200, {
4380
4290
  "Content-Type": "text/event-stream",
4381
4291
  "Cache-Control": "no-cache",
@@ -4386,7 +4296,8 @@ function startDaemon() {
4386
4296
  abandonTimer = null;
4387
4297
  }
4388
4298
  hadBrowserClient = true;
4389
- sseClients.add(res);
4299
+ const sseClient = { res, capabilities };
4300
+ sseClients.add(sseClient);
4390
4301
  res.write(
4391
4302
  `event: init
4392
4303
  data: ${JSON.stringify({
@@ -4401,7 +4312,7 @@ data: ${JSON.stringify({
4401
4312
  mcpServer: e.mcpServer
4402
4313
  })),
4403
4314
  orgName: getOrgName(),
4404
- autoDenyMs: AUTO_DENY_MS
4315
+ autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
4405
4316
  })}
4406
4317
 
4407
4318
  `
@@ -4423,6 +4334,10 @@ data: ${JSON.stringify({
4423
4334
 
4424
4335
  `
4425
4336
  );
4337
+ res.write(`event: csrf
4338
+ data: ${JSON.stringify({ token: csrfToken })}
4339
+
4340
+ `);
4426
4341
  for (const item of activityRing) {
4427
4342
  res.write(`event: ${item.event}
4428
4343
  data: ${JSON.stringify(item.data)}
@@ -4430,7 +4345,7 @@ data: ${JSON.stringify(item.data)}
4430
4345
  `);
4431
4346
  }
4432
4347
  return req.on("close", () => {
4433
- sseClients.delete(res);
4348
+ sseClients.delete(sseClient);
4434
4349
  if (sseClients.size === 0 && pending.size > 0) {
4435
4350
  abandonTimer = setTimeout(abandonPending, hadBrowserClient ? 1e4 : 15e3);
4436
4351
  }
@@ -4450,7 +4365,8 @@ data: ${JSON.stringify(item.data)}
4450
4365
  mcpServer,
4451
4366
  riskMetadata,
4452
4367
  fromCLI = false,
4453
- activityId
4368
+ activityId,
4369
+ cwd
4454
4370
  } = JSON.parse(body);
4455
4371
  const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto3.randomUUID)();
4456
4372
  const entry = {
@@ -4480,7 +4396,7 @@ data: ${JSON.stringify(item.data)}
4480
4396
  pending.delete(id);
4481
4397
  broadcast("remove", { id });
4482
4398
  }
4483
- }, AUTO_DENY_MS)
4399
+ }, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
4484
4400
  };
4485
4401
  pending.set(id, entry);
4486
4402
  if (!fromCLI) {
@@ -4492,8 +4408,11 @@ data: ${JSON.stringify(item.data)}
4492
4408
  status: "pending"
4493
4409
  });
4494
4410
  }
4495
- const browserEnabled = getConfig().settings.approvers?.browser !== false;
4496
- if (browserEnabled) {
4411
+ const projectCwd = typeof cwd === "string" && import_path7.default.isAbsolute(cwd) ? cwd : void 0;
4412
+ const projectConfig = getConfig(projectCwd);
4413
+ const browserEnabled = projectConfig.settings.approvers?.browser !== false;
4414
+ const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
4415
+ if (browserEnabled || terminalEnabled) {
4497
4416
  broadcast("add", {
4498
4417
  id,
4499
4418
  toolName,
@@ -4501,17 +4420,21 @@ data: ${JSON.stringify(item.data)}
4501
4420
  riskMetadata: entry.riskMetadata,
4502
4421
  slackDelegated: entry.slackDelegated,
4503
4422
  agent: entry.agent,
4504
- mcpServer: entry.mcpServer
4423
+ mcpServer: entry.mcpServer,
4424
+ interactive: terminalEnabled
4505
4425
  });
4506
- if (sseClients.size === 0 && !autoStarted)
4426
+ const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
4427
+ if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
4428
+ browserOpened = true;
4507
4429
  openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
4430
+ }
4508
4431
  }
4509
4432
  res.writeHead(200, { "Content-Type": "application/json" });
4510
4433
  res.end(JSON.stringify({ id }));
4434
+ if (slackDelegated) return;
4511
4435
  authorizeHeadless(
4512
4436
  toolName,
4513
4437
  args,
4514
- false,
4515
4438
  {
4516
4439
  agent: typeof agent === "string" ? agent : void 0,
4517
4440
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
@@ -4521,6 +4444,8 @@ data: ${JSON.stringify(item.data)}
4521
4444
  const e = pending.get(id);
4522
4445
  if (!e) return;
4523
4446
  if (result.noApprovalMechanism) return;
4447
+ if (result.checkedBy === "audit") return;
4448
+ if (e.earlyDecision !== null) return;
4524
4449
  broadcast("activity-result", {
4525
4450
  id,
4526
4451
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
@@ -4561,18 +4486,31 @@ data: ${JSON.stringify(item.data)}
4561
4486
  if (!entry) return res.writeHead(404).end();
4562
4487
  if (entry.earlyDecision) {
4563
4488
  clearTimeout(entry.timer);
4489
+ const source = entry.decisionSource;
4564
4490
  pending.delete(id);
4565
4491
  res.writeHead(200, { "Content-Type": "application/json" });
4566
- const body = { decision: entry.earlyDecision };
4492
+ const body = {
4493
+ decision: entry.earlyDecision
4494
+ };
4567
4495
  if (entry.earlyReason) body.reason = entry.earlyReason;
4496
+ if (source) body.source = source;
4568
4497
  return res.end(JSON.stringify(body));
4569
4498
  }
4570
4499
  entry.waiter = (d, reason) => {
4571
4500
  res.writeHead(200, { "Content-Type": "application/json" });
4572
4501
  const body = { decision: d };
4573
4502
  if (reason) body.reason = reason;
4503
+ if (entry.decisionSource) body.source = entry.decisionSource;
4574
4504
  res.end(JSON.stringify(body));
4575
4505
  };
4506
+ req.on("close", () => {
4507
+ const e = pending.get(id);
4508
+ if (e && e.waiter && e.earlyDecision === null) {
4509
+ clearTimeout(e.timer);
4510
+ pending.delete(id);
4511
+ broadcast("remove", { id });
4512
+ }
4513
+ });
4576
4514
  return;
4577
4515
  }
4578
4516
  if (req.method === "POST" && pathname.startsWith("/decision/")) {
@@ -4581,7 +4519,13 @@ data: ${JSON.stringify(item.data)}
4581
4519
  const id = pathname.split("/").pop();
4582
4520
  const entry = pending.get(id);
4583
4521
  if (!entry) return res.writeHead(404).end();
4584
- const { decision, persist, trustDuration, reason } = JSON.parse(await readBody(req));
4522
+ if (entry.earlyDecision !== null) {
4523
+ res.writeHead(409, { "Content-Type": "application/json" });
4524
+ return res.end(JSON.stringify({ conflict: true, decision: entry.earlyDecision }));
4525
+ }
4526
+ const { decision, persist, trustDuration, reason, source } = JSON.parse(
4527
+ await readBody(req)
4528
+ );
4585
4529
  if (decision === "trust" && trustDuration) {
4586
4530
  const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
4587
4531
  writeTrustEntry(entry.toolName, ms);
@@ -4611,6 +4555,8 @@ data: ${JSON.stringify(item.data)}
4611
4555
  decision: resolvedDecision
4612
4556
  });
4613
4557
  clearTimeout(entry.timer);
4558
+ const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
4559
+ if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
4614
4560
  if (entry.waiter) {
4615
4561
  entry.waiter(resolvedDecision, reason);
4616
4562
  pending.delete(id);
@@ -4633,7 +4579,7 @@ data: ${JSON.stringify(item.data)}
4633
4579
  res.writeHead(200, { "Content-Type": "application/json" });
4634
4580
  return res.end(JSON.stringify({ ...s, autoStarted }));
4635
4581
  } catch (err) {
4636
- console.error(import_chalk4.default.red("[node9 daemon] GET /settings failed:"), err);
4582
+ console.error(import_chalk2.default.red("[node9 daemon] GET /settings failed:"), err);
4637
4583
  res.writeHead(500, { "Content-Type": "application/json" });
4638
4584
  return res.end(JSON.stringify({ error: "internal" }));
4639
4585
  }
@@ -4664,7 +4610,7 @@ data: ${JSON.stringify(item.data)}
4664
4610
  res.writeHead(200, { "Content-Type": "application/json" });
4665
4611
  return res.end(JSON.stringify({ hasKey: hasStoredSlackKey(), enabled: s.slackEnabled }));
4666
4612
  } catch (err) {
4667
- console.error(import_chalk4.default.red("[node9 daemon] GET /slack-status failed:"), err);
4613
+ console.error(import_chalk2.default.red("[node9 daemon] GET /slack-status failed:"), err);
4668
4614
  res.writeHead(500, { "Content-Type": "application/json" });
4669
4615
  return res.end(JSON.stringify({ error: "internal" }));
4670
4616
  }
@@ -4813,14 +4759,14 @@ data: ${JSON.stringify(item.data)}
4813
4759
  });
4814
4760
  return;
4815
4761
  }
4816
- console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4762
+ console.error(import_chalk2.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4817
4763
  process.exit(1);
4818
4764
  });
4819
4765
  if (!daemonRejectionHandlerRegistered) {
4820
4766
  daemonRejectionHandlerRegistered = true;
4821
4767
  process.on("unhandledRejection", (reason) => {
4822
4768
  const stack = reason instanceof Error ? reason.stack : String(reason);
4823
- console.error(import_chalk4.default.red("[node9 daemon] unhandled rejection \u2014 keeping daemon alive:"), stack);
4769
+ console.error(import_chalk2.default.red("[node9 daemon] unhandled rejection \u2014 keeping daemon alive:"), stack);
4824
4770
  });
4825
4771
  }
4826
4772
  server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
@@ -4829,10 +4775,10 @@ data: ${JSON.stringify(item.data)}
4829
4775
  JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4830
4776
  { mode: 384 }
4831
4777
  );
4832
- console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4778
+ console.log(import_chalk2.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4833
4779
  });
4834
4780
  if (watchMode) {
4835
- console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4781
+ console.log(import_chalk2.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4836
4782
  }
4837
4783
  try {
4838
4784
  import_fs5.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
@@ -4883,13 +4829,13 @@ data: ${JSON.stringify(item.data)}
4883
4829
  });
4884
4830
  }
4885
4831
  function stopDaemon() {
4886
- if (!import_fs5.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
4832
+ if (!import_fs5.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk2.default.yellow("Not running."));
4887
4833
  try {
4888
4834
  const { pid } = JSON.parse(import_fs5.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4889
4835
  process.kill(pid, "SIGTERM");
4890
- console.log(import_chalk4.default.green("\u2705 Stopped."));
4836
+ console.log(import_chalk2.default.green("\u2705 Stopped."));
4891
4837
  } catch {
4892
- console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
4838
+ console.log(import_chalk2.default.gray("Cleaned up stale PID file."));
4893
4839
  } finally {
4894
4840
  try {
4895
4841
  import_fs5.default.unlinkSync(DAEMON_PID_FILE);
@@ -4902,10 +4848,10 @@ function daemonStatus() {
4902
4848
  try {
4903
4849
  const { pid } = JSON.parse(import_fs5.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4904
4850
  process.kill(pid, 0);
4905
- console.log(import_chalk4.default.green("Node9 daemon: running"));
4851
+ console.log(import_chalk2.default.green("Node9 daemon: running"));
4906
4852
  return;
4907
4853
  } catch {
4908
- console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
4854
+ console.log(import_chalk2.default.yellow("Node9 daemon: not running (stale PID)"));
4909
4855
  return;
4910
4856
  }
4911
4857
  }
@@ -4914,12 +4860,12 @@ function daemonStatus() {
4914
4860
  timeout: 500
4915
4861
  });
4916
4862
  if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT2}`)) {
4917
- console.log(import_chalk4.default.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
4863
+ console.log(import_chalk2.default.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
4918
4864
  } else {
4919
- console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4865
+ console.log(import_chalk2.default.yellow("Node9 daemon: not running"));
4920
4866
  }
4921
4867
  }
4922
- var import_http, import_net2, import_fs5, import_path7, import_os4, import_child_process3, import_crypto3, import_chalk4, daemonRejectionHandlerRegistered, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4868
+ var import_http, import_net2, import_fs5, import_path7, import_os4, import_child_process3, import_crypto3, import_chalk2, daemonRejectionHandlerRegistered, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4923
4869
  var init_daemon = __esm({
4924
4870
  "src/daemon/index.ts"() {
4925
4871
  "use strict";
@@ -4931,7 +4877,7 @@ var init_daemon = __esm({
4931
4877
  import_os4 = __toESM(require("os"));
4932
4878
  import_child_process3 = require("child_process");
4933
4879
  import_crypto3 = require("crypto");
4934
- import_chalk4 = __toESM(require("chalk"));
4880
+ import_chalk2 = __toESM(require("chalk"));
4935
4881
  init_core();
4936
4882
  init_shields();
4937
4883
  daemonRejectionHandlerRegistered = false;
@@ -4981,17 +4927,17 @@ function formatBase(activity) {
4981
4927
  const toolName = activity.tool.slice(0, 16).padEnd(16);
4982
4928
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
4983
4929
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
4984
- return `${import_chalk5.default.gray(time)} ${icon} ${import_chalk5.default.white.bold(toolName)} ${import_chalk5.default.dim(argsPreview)}`;
4930
+ return `${import_chalk3.default.gray(time)} ${icon} ${import_chalk3.default.white.bold(toolName)} ${import_chalk3.default.dim(argsPreview)}`;
4985
4931
  }
4986
4932
  function renderResult(activity, result) {
4987
4933
  const base = formatBase(activity);
4988
4934
  let status;
4989
4935
  if (result.status === "allow") {
4990
- status = import_chalk5.default.green("\u2713 ALLOW");
4936
+ status = import_chalk3.default.green("\u2713 ALLOW");
4991
4937
  } else if (result.status === "dlp") {
4992
- status = import_chalk5.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4938
+ status = import_chalk3.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4993
4939
  } else {
4994
- status = import_chalk5.default.red("\u2717 BLOCK");
4940
+ status = import_chalk3.default.red("\u2717 BLOCK");
4995
4941
  }
4996
4942
  if (process.stdout.isTTY) {
4997
4943
  import_readline.default.clearLine(process.stdout, 0);
@@ -5001,7 +4947,7 @@ function renderResult(activity, result) {
5001
4947
  }
5002
4948
  function renderPending(activity) {
5003
4949
  if (!process.stdout.isTTY) return;
5004
- process.stdout.write(`${formatBase(activity)} ${import_chalk5.default.yellow("\u25CF \u2026")}\r`);
4950
+ process.stdout.write(`${formatBase(activity)} ${import_chalk3.default.yellow("\u25CF \u2026")}\r`);
5005
4951
  }
5006
4952
  async function ensureDaemon() {
5007
4953
  let pidPort = null;
@@ -5010,7 +4956,7 @@ async function ensureDaemon() {
5010
4956
  const { port } = JSON.parse(import_fs7.default.readFileSync(PID_FILE, "utf-8"));
5011
4957
  pidPort = port;
5012
4958
  } catch {
5013
- console.error(import_chalk5.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
4959
+ console.error(import_chalk3.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
5014
4960
  }
5015
4961
  }
5016
4962
  const checkPort = pidPort ?? DAEMON_PORT2;
@@ -5021,7 +4967,7 @@ async function ensureDaemon() {
5021
4967
  if (res.ok) return checkPort;
5022
4968
  } catch {
5023
4969
  }
5024
- console.log(import_chalk5.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4970
+ console.log(import_chalk3.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
5025
4971
  const child = (0, import_child_process5.spawn)(process.execPath, [process.argv[1], "daemon"], {
5026
4972
  detached: true,
5027
4973
  stdio: "ignore",
@@ -5038,9 +4984,51 @@ async function ensureDaemon() {
5038
4984
  } catch {
5039
4985
  }
5040
4986
  }
5041
- console.error(import_chalk5.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
4987
+ console.error(import_chalk3.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
5042
4988
  process.exit(1);
5043
4989
  }
4990
+ function postDecisionHttp(id, decision, csrfToken, port) {
4991
+ return new Promise((resolve, reject) => {
4992
+ const body = JSON.stringify({ decision, source: "terminal" });
4993
+ const req = import_http2.default.request(
4994
+ {
4995
+ hostname: "127.0.0.1",
4996
+ port,
4997
+ path: `/decision/${id}`,
4998
+ method: "POST",
4999
+ headers: {
5000
+ "Content-Type": "application/json",
5001
+ "Content-Length": Buffer.byteLength(body),
5002
+ "X-Node9-Token": csrfToken
5003
+ }
5004
+ },
5005
+ (res) => {
5006
+ res.resume();
5007
+ if (res.statusCode === 200 || res.statusCode === 409) resolve();
5008
+ else reject(new Error(`POST /decision returned ${res.statusCode}`));
5009
+ }
5010
+ );
5011
+ req.on("error", reject);
5012
+ req.end(body);
5013
+ });
5014
+ }
5015
+ function buildCardLines(req) {
5016
+ const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
5017
+ const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
5018
+ 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`;
5019
+ const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
5020
+ return [
5021
+ ``,
5022
+ `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
5023
+ `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
5024
+ `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
5025
+ `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`,
5026
+ `${CYAN}\u255A${RESET}`,
5027
+ ``,
5028
+ ` ${BOLD}${GREEN}[A]${RESET} Allow ${BOLD}${RED}[D]${RESET} Deny`,
5029
+ ``
5030
+ ];
5031
+ }
5044
5032
  async function startTail(options = {}) {
5045
5033
  const port = await ensureDaemon();
5046
5034
  if (options.clear) {
@@ -5067,7 +5055,7 @@ async function startTail(options = {}) {
5067
5055
  req2.end();
5068
5056
  });
5069
5057
  if (result.ok) {
5070
- console.log(import_chalk5.default.green("\u2713 Flight Recorder buffer cleared."));
5058
+ console.log(import_chalk3.default.green("\u2713 Flight Recorder buffer cleared."));
5071
5059
  } else if (result.code === "ECONNREFUSED") {
5072
5060
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
5073
5061
  } else if (result.code === "ETIMEDOUT") {
@@ -5078,27 +5066,130 @@ async function startTail(options = {}) {
5078
5066
  return;
5079
5067
  }
5080
5068
  const connectionTime = Date.now();
5081
- const pending2 = /* @__PURE__ */ new Map();
5082
- console.log(import_chalk5.default.cyan.bold(`
5083
- \u{1F6F0}\uFE0F Node9 tail `) + import_chalk5.default.dim(`\u2192 localhost:${port}`));
5069
+ const activityPending = /* @__PURE__ */ new Map();
5070
+ let csrfToken = "";
5071
+ const approvalQueue = [];
5072
+ let cardActive = false;
5073
+ let cardLineCount = 0;
5074
+ let cancelActiveCard = null;
5075
+ const canApprove = process.stdout.isTTY && process.stdin.isTTY;
5076
+ if (canApprove) import_readline.default.emitKeypressEvents(process.stdin);
5077
+ function clearCard() {
5078
+ if (cardLineCount > 0) {
5079
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
5080
+ cardLineCount = 0;
5081
+ }
5082
+ }
5083
+ function printCard(req2) {
5084
+ process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
5085
+ const lines = buildCardLines(req2);
5086
+ for (const line of lines) process.stdout.write(line + "\n");
5087
+ cardLineCount = lines.length;
5088
+ }
5089
+ function showNextCard() {
5090
+ if (cardActive || approvalQueue.length === 0 || !canApprove) return;
5091
+ try {
5092
+ process.stdin.setRawMode(true);
5093
+ } catch {
5094
+ cardActive = false;
5095
+ return;
5096
+ }
5097
+ cardActive = true;
5098
+ const req2 = approvalQueue[0];
5099
+ printCard(req2);
5100
+ let settled = false;
5101
+ let onKeypress = null;
5102
+ const cleanup = () => {
5103
+ const handler = onKeypress;
5104
+ onKeypress = null;
5105
+ if (handler) process.stdin.removeListener("keypress", handler);
5106
+ try {
5107
+ process.stdin.setRawMode(false);
5108
+ } catch {
5109
+ }
5110
+ process.stdin.pause();
5111
+ cancelActiveCard = null;
5112
+ };
5113
+ const settle = (decision) => {
5114
+ if (settled) return;
5115
+ settled = true;
5116
+ cleanup();
5117
+ clearCard();
5118
+ process.stdout.write(SHOW_CURSOR);
5119
+ postDecisionHttp(req2.id, decision, csrfToken, port).catch((err) => {
5120
+ try {
5121
+ import_fs7.default.appendFileSync(
5122
+ import_path9.default.join(import_os6.default.homedir(), ".node9", "hook-debug.log"),
5123
+ `[tail] POST /decision failed: ${String(err)}
5124
+ `
5125
+ );
5126
+ } catch {
5127
+ }
5128
+ });
5129
+ const decisionLabel = decision === "allow" ? import_chalk3.default.green("\u2713 ALLOWED (terminal)") : import_chalk3.default.red("\u2717 DENIED (terminal)");
5130
+ console.log(`${import_chalk3.default.cyan("\u25C6")} ${import_chalk3.default.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
5131
+ approvalQueue.shift();
5132
+ cardActive = false;
5133
+ showNextCard();
5134
+ };
5135
+ cancelActiveCard = () => {
5136
+ if (settled) return;
5137
+ settled = true;
5138
+ cleanup();
5139
+ clearCard();
5140
+ process.stdout.write(SHOW_CURSOR);
5141
+ approvalQueue.shift();
5142
+ cardActive = false;
5143
+ showNextCard();
5144
+ };
5145
+ process.stdin.resume();
5146
+ onKeypress = (_str, key) => {
5147
+ const name = key?.name ?? "";
5148
+ if (name === "a") {
5149
+ settle("allow");
5150
+ } else if (name === "d" || name === "return" || name === "enter" || key?.ctrl && name === "c") {
5151
+ settle("deny");
5152
+ }
5153
+ };
5154
+ process.stdin.on("keypress", onKeypress);
5155
+ }
5156
+ const dashboardUrl = `http://127.0.0.1:${port}/`;
5157
+ try {
5158
+ const browserEnabled = getConfig().settings.approvers?.browser !== false;
5159
+ if (browserEnabled) {
5160
+ if (process.platform === "darwin") (0, import_child_process5.execSync)(`open "${dashboardUrl}"`, { stdio: "ignore" });
5161
+ else if (process.platform === "win32")
5162
+ (0, import_child_process5.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
5163
+ else (0, import_child_process5.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
5164
+ }
5165
+ } catch {
5166
+ }
5167
+ console.log(import_chalk3.default.cyan.bold(`
5168
+ \u{1F6F0}\uFE0F Node9 tail `) + import_chalk3.default.dim(`\u2192 ${dashboardUrl}`));
5169
+ if (canApprove) {
5170
+ console.log(import_chalk3.default.dim("Interactive approvals enabled. [A] Allow [D] Deny"));
5171
+ }
5084
5172
  if (options.history) {
5085
- console.log(import_chalk5.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
5173
+ console.log(import_chalk3.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
5086
5174
  } else {
5087
5175
  console.log(
5088
- import_chalk5.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
5176
+ import_chalk3.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
5089
5177
  );
5090
5178
  }
5091
5179
  process.on("SIGINT", () => {
5180
+ clearCard();
5181
+ process.stdout.write(SHOW_CURSOR);
5092
5182
  if (process.stdout.isTTY) {
5093
5183
  import_readline.default.clearLine(process.stdout, 0);
5094
5184
  import_readline.default.cursorTo(process.stdout, 0);
5095
5185
  }
5096
- console.log(import_chalk5.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
5186
+ console.log(import_chalk3.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
5097
5187
  process.exit(0);
5098
5188
  });
5099
- const req = import_http2.default.get(`http://127.0.0.1:${port}/events`, (res) => {
5189
+ const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
5190
+ const req = import_http2.default.get(sseUrl, (res) => {
5100
5191
  if (res.statusCode !== 200) {
5101
- console.error(import_chalk5.default.red(`Failed to connect: HTTP ${res.statusCode}`));
5192
+ console.error(import_chalk3.default.red(`Failed to connect: HTTP ${res.statusCode}`));
5102
5193
  process.exit(1);
5103
5194
  }
5104
5195
  let currentEvent = "";
@@ -5122,15 +5213,66 @@ async function startTail(options = {}) {
5122
5213
  }
5123
5214
  });
5124
5215
  rl.on("close", () => {
5216
+ clearCard();
5217
+ process.stdout.write(SHOW_CURSOR);
5125
5218
  if (process.stdout.isTTY) {
5126
5219
  import_readline.default.clearLine(process.stdout, 0);
5127
5220
  import_readline.default.cursorTo(process.stdout, 0);
5128
5221
  }
5129
- console.log(import_chalk5.default.red("\n\u274C Daemon disconnected."));
5222
+ console.log(import_chalk3.default.red("\n\u274C Daemon disconnected."));
5130
5223
  process.exit(1);
5131
5224
  });
5132
5225
  });
5133
5226
  function handleMessage(event, rawData) {
5227
+ if (event === "csrf") {
5228
+ try {
5229
+ const parsed = JSON.parse(rawData);
5230
+ if (parsed.token) csrfToken = parsed.token;
5231
+ } catch {
5232
+ }
5233
+ return;
5234
+ }
5235
+ if (event === "init") {
5236
+ try {
5237
+ const parsed = JSON.parse(rawData);
5238
+ if (canApprove && Array.isArray(parsed.requests)) {
5239
+ for (const r of parsed.requests) {
5240
+ approvalQueue.push(r);
5241
+ }
5242
+ showNextCard();
5243
+ }
5244
+ } catch {
5245
+ }
5246
+ return;
5247
+ }
5248
+ if (event === "add") {
5249
+ if (canApprove) {
5250
+ try {
5251
+ const parsed = JSON.parse(rawData);
5252
+ if (parsed.interactive !== false) {
5253
+ approvalQueue.push(parsed);
5254
+ showNextCard();
5255
+ }
5256
+ } catch {
5257
+ }
5258
+ }
5259
+ return;
5260
+ }
5261
+ if (event === "remove") {
5262
+ try {
5263
+ const { id } = JSON.parse(rawData);
5264
+ const idx = approvalQueue.findIndex((r) => r.id === id);
5265
+ if (idx !== -1) {
5266
+ if (idx === 0 && cardActive && cancelActiveCard) {
5267
+ cancelActiveCard();
5268
+ } else {
5269
+ approvalQueue.splice(idx, 1);
5270
+ }
5271
+ }
5272
+ } catch {
5273
+ }
5274
+ return;
5275
+ }
5134
5276
  let data;
5135
5277
  try {
5136
5278
  data = JSON.parse(rawData);
@@ -5143,37 +5285,38 @@ async function startTail(options = {}) {
5143
5285
  renderResult(data, data);
5144
5286
  return;
5145
5287
  }
5146
- pending2.set(data.id, data);
5288
+ activityPending.set(data.id, data);
5147
5289
  const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
5148
5290
  if (slowTool) renderPending(data);
5149
5291
  }
5150
5292
  if (event === "activity-result") {
5151
- const original = pending2.get(data.id);
5293
+ const original = activityPending.get(data.id);
5152
5294
  if (original) {
5153
5295
  renderResult(original, data);
5154
- pending2.delete(data.id);
5296
+ activityPending.delete(data.id);
5155
5297
  }
5156
5298
  }
5157
5299
  }
5158
5300
  req.on("error", (err) => {
5159
5301
  const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
5160
- console.error(import_chalk5.default.red(`
5302
+ console.error(import_chalk3.default.red(`
5161
5303
  \u274C ${msg}`));
5162
5304
  process.exit(1);
5163
5305
  });
5164
5306
  }
5165
- var import_http2, import_chalk5, import_fs7, import_os6, import_path9, import_readline, import_child_process5, PID_FILE, ICONS;
5307
+ var import_http2, import_chalk3, import_fs7, import_os6, import_path9, import_readline, import_child_process5, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
5166
5308
  var init_tail = __esm({
5167
5309
  "src/tui/tail.ts"() {
5168
5310
  "use strict";
5169
5311
  import_http2 = __toESM(require("http"));
5170
- import_chalk5 = __toESM(require("chalk"));
5312
+ import_chalk3 = __toESM(require("chalk"));
5171
5313
  import_fs7 = __toESM(require("fs"));
5172
5314
  import_os6 = __toESM(require("os"));
5173
5315
  import_path9 = __toESM(require("path"));
5174
5316
  import_readline = __toESM(require("readline"));
5175
5317
  import_child_process5 = require("child_process");
5176
5318
  init_daemon();
5319
+ init_core();
5177
5320
  PID_FILE = import_path9.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
5178
5321
  ICONS = {
5179
5322
  bash: "\u{1F4BB}",
@@ -5192,6 +5335,18 @@ var init_tail = __esm({
5192
5335
  delete: "\u{1F5D1}\uFE0F",
5193
5336
  web: "\u{1F310}"
5194
5337
  };
5338
+ RESET = "\x1B[0m";
5339
+ BOLD = "\x1B[1m";
5340
+ RED = "\x1B[31m";
5341
+ YELLOW = "\x1B[33m";
5342
+ CYAN = "\x1B[36m";
5343
+ GRAY = "\x1B[90m";
5344
+ GREEN = "\x1B[32m";
5345
+ HIDE_CURSOR = "\x1B[?25l";
5346
+ SHOW_CURSOR = "\x1B[?25h";
5347
+ ERASE_DOWN = "\x1B[J";
5348
+ SAVE_CURSOR = "\x1B7";
5349
+ RESTORE_CURSOR = "\x1B8";
5195
5350
  }
5196
5351
  });
5197
5352
 
@@ -5203,11 +5358,11 @@ init_core();
5203
5358
  var import_fs4 = __toESM(require("fs"));
5204
5359
  var import_path6 = __toESM(require("path"));
5205
5360
  var import_os3 = __toESM(require("os"));
5206
- var import_chalk3 = __toESM(require("chalk"));
5207
- var import_prompts2 = require("@inquirer/prompts");
5361
+ var import_chalk = __toESM(require("chalk"));
5362
+ var import_prompts = require("@inquirer/prompts");
5208
5363
  function printDaemonTip() {
5209
5364
  console.log(
5210
- import_chalk3.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk3.default.white("\n To view your history or manage persistent rules, run:") + import_chalk3.default.green("\n node9 daemon --openui")
5365
+ import_chalk.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk.default.white("\n To view your history or manage persistent rules, run:") + import_chalk.default.green("\n node9 daemon --openui")
5211
5366
  );
5212
5367
  }
5213
5368
  function fullPathCommand(subcommand) {
@@ -5253,10 +5408,10 @@ function teardownClaude() {
5253
5408
  if (changed) {
5254
5409
  writeJson(hooksPath, settings);
5255
5410
  console.log(
5256
- import_chalk3.default.green(" \u2705 Removed PreToolUse / PostToolUse hooks from ~/.claude/settings.json")
5411
+ import_chalk.default.green(" \u2705 Removed PreToolUse / PostToolUse hooks from ~/.claude/settings.json")
5257
5412
  );
5258
5413
  } else {
5259
- console.log(import_chalk3.default.blue(" \u2139\uFE0F No Node9 hooks found in ~/.claude/settings.json"));
5414
+ console.log(import_chalk.default.blue(" \u2139\uFE0F No Node9 hooks found in ~/.claude/settings.json"));
5260
5415
  }
5261
5416
  }
5262
5417
  const claudeConfig = readJson(mcpPath);
@@ -5273,7 +5428,7 @@ function teardownClaude() {
5273
5428
  mcpChanged = true;
5274
5429
  } else if (server.command === "node9") {
5275
5430
  console.warn(
5276
- import_chalk3.default.yellow(
5431
+ import_chalk.default.yellow(
5277
5432
  ` \u26A0\uFE0F Cannot unwrap MCP server "${name}" in ~/.claude.json \u2014 args is empty. Remove it manually.`
5278
5433
  )
5279
5434
  );
@@ -5281,7 +5436,7 @@ function teardownClaude() {
5281
5436
  }
5282
5437
  if (mcpChanged) {
5283
5438
  writeJson(mcpPath, claudeConfig);
5284
- console.log(import_chalk3.default.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
5439
+ console.log(import_chalk.default.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
5285
5440
  }
5286
5441
  }
5287
5442
  }
@@ -5290,7 +5445,7 @@ function teardownGemini() {
5290
5445
  const settingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
5291
5446
  const settings = readJson(settingsPath);
5292
5447
  if (!settings) {
5293
- console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
5448
+ console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
5294
5449
  return;
5295
5450
  }
5296
5451
  let changed = false;
@@ -5319,9 +5474,9 @@ function teardownGemini() {
5319
5474
  }
5320
5475
  if (changed) {
5321
5476
  writeJson(settingsPath, settings);
5322
- console.log(import_chalk3.default.green(" \u2705 Removed Node9 hooks from ~/.gemini/settings.json"));
5477
+ console.log(import_chalk.default.green(" \u2705 Removed Node9 hooks from ~/.gemini/settings.json"));
5323
5478
  } else {
5324
- console.log(import_chalk3.default.blue(" \u2139\uFE0F No Node9 hooks found in ~/.gemini/settings.json"));
5479
+ console.log(import_chalk.default.blue(" \u2139\uFE0F No Node9 hooks found in ~/.gemini/settings.json"));
5325
5480
  }
5326
5481
  }
5327
5482
  function teardownCursor() {
@@ -5329,7 +5484,7 @@ function teardownCursor() {
5329
5484
  const mcpPath = import_path6.default.join(homeDir2, ".cursor", "mcp.json");
5330
5485
  const mcpConfig = readJson(mcpPath);
5331
5486
  if (!mcpConfig?.mcpServers) {
5332
- console.log(import_chalk3.default.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
5487
+ console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
5333
5488
  return;
5334
5489
  }
5335
5490
  let changed = false;
@@ -5346,9 +5501,9 @@ function teardownCursor() {
5346
5501
  }
5347
5502
  if (changed) {
5348
5503
  writeJson(mcpPath, mcpConfig);
5349
- console.log(import_chalk3.default.green(" \u2705 Unwrapped MCP servers in ~/.cursor/mcp.json"));
5504
+ console.log(import_chalk.default.green(" \u2705 Unwrapped MCP servers in ~/.cursor/mcp.json"));
5350
5505
  } else {
5351
- console.log(import_chalk3.default.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.cursor/mcp.json"));
5506
+ console.log(import_chalk.default.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.cursor/mcp.json"));
5352
5507
  }
5353
5508
  }
5354
5509
  async function setupClaude() {
@@ -5369,7 +5524,7 @@ async function setupClaude() {
5369
5524
  matcher: ".*",
5370
5525
  hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
5371
5526
  });
5372
- console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
5527
+ console.log(import_chalk.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
5373
5528
  anythingChanged = true;
5374
5529
  }
5375
5530
  const hasPostHook = settings.hooks.PostToolUse?.some(
@@ -5381,7 +5536,7 @@ async function setupClaude() {
5381
5536
  matcher: ".*",
5382
5537
  hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
5383
5538
  });
5384
- console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
5539
+ console.log(import_chalk.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
5385
5540
  anythingChanged = true;
5386
5541
  }
5387
5542
  if (anythingChanged) {
@@ -5395,35 +5550,35 @@ async function setupClaude() {
5395
5550
  serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5396
5551
  }
5397
5552
  if (serversToWrap.length > 0) {
5398
- console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
5399
- console.log(import_chalk3.default.white(` ${mcpPath}`));
5553
+ console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
5554
+ console.log(import_chalk.default.white(` ${mcpPath}`));
5400
5555
  for (const { name, originalCmd } of serversToWrap) {
5401
- console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5556
+ console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5402
5557
  }
5403
5558
  console.log("");
5404
- const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
5559
+ const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
5405
5560
  if (proceed) {
5406
5561
  for (const { name, parts } of serversToWrap) {
5407
5562
  servers[name] = { ...servers[name], command: "node9", args: parts };
5408
5563
  }
5409
5564
  claudeConfig.mcpServers = servers;
5410
5565
  writeJson(mcpPath, claudeConfig);
5411
- console.log(import_chalk3.default.green(`
5566
+ console.log(import_chalk.default.green(`
5412
5567
  \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5413
5568
  anythingChanged = true;
5414
5569
  } else {
5415
- console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
5570
+ console.log(import_chalk.default.yellow(" Skipped MCP server wrapping."));
5416
5571
  }
5417
5572
  console.log("");
5418
5573
  }
5419
5574
  if (!anythingChanged && serversToWrap.length === 0) {
5420
- console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
5575
+ console.log(import_chalk.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
5421
5576
  printDaemonTip();
5422
5577
  return;
5423
5578
  }
5424
5579
  if (anythingChanged) {
5425
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
5426
- console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
5580
+ console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
5581
+ console.log(import_chalk.default.gray(" Restart Claude Code for changes to take effect."));
5427
5582
  printDaemonTip();
5428
5583
  }
5429
5584
  }
@@ -5451,7 +5606,7 @@ async function setupGemini() {
5451
5606
  }
5452
5607
  ]
5453
5608
  });
5454
- console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
5609
+ console.log(import_chalk.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
5455
5610
  anythingChanged = true;
5456
5611
  }
5457
5612
  const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
@@ -5464,7 +5619,7 @@ async function setupGemini() {
5464
5619
  matcher: ".*",
5465
5620
  hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
5466
5621
  });
5467
- console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
5622
+ console.log(import_chalk.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
5468
5623
  anythingChanged = true;
5469
5624
  }
5470
5625
  if (anythingChanged) {
@@ -5478,35 +5633,35 @@ async function setupGemini() {
5478
5633
  serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5479
5634
  }
5480
5635
  if (serversToWrap.length > 0) {
5481
- console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
5482
- console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
5636
+ console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
5637
+ console.log(import_chalk.default.white(` ${settingsPath} (mcpServers)`));
5483
5638
  for (const { name, originalCmd } of serversToWrap) {
5484
- console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5639
+ console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5485
5640
  }
5486
5641
  console.log("");
5487
- const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
5642
+ const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
5488
5643
  if (proceed) {
5489
5644
  for (const { name, parts } of serversToWrap) {
5490
5645
  servers[name] = { ...servers[name], command: "node9", args: parts };
5491
5646
  }
5492
5647
  settings.mcpServers = servers;
5493
5648
  writeJson(settingsPath, settings);
5494
- console.log(import_chalk3.default.green(`
5649
+ console.log(import_chalk.default.green(`
5495
5650
  \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5496
5651
  anythingChanged = true;
5497
5652
  } else {
5498
- console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
5653
+ console.log(import_chalk.default.yellow(" Skipped MCP server wrapping."));
5499
5654
  }
5500
5655
  console.log("");
5501
5656
  }
5502
5657
  if (!anythingChanged && serversToWrap.length === 0) {
5503
- console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
5658
+ console.log(import_chalk.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
5504
5659
  printDaemonTip();
5505
5660
  return;
5506
5661
  }
5507
5662
  if (anythingChanged) {
5508
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
5509
- console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
5663
+ console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
5664
+ console.log(import_chalk.default.gray(" Restart Gemini CLI for changes to take effect."));
5510
5665
  printDaemonTip();
5511
5666
  }
5512
5667
  }
@@ -5523,36 +5678,36 @@ async function setupCursor() {
5523
5678
  serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5524
5679
  }
5525
5680
  if (serversToWrap.length > 0) {
5526
- console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
5527
- console.log(import_chalk3.default.white(` ${mcpPath}`));
5681
+ console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
5682
+ console.log(import_chalk.default.white(` ${mcpPath}`));
5528
5683
  for (const { name, originalCmd } of serversToWrap) {
5529
- console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5684
+ console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5530
5685
  }
5531
5686
  console.log("");
5532
- const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
5687
+ const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
5533
5688
  if (proceed) {
5534
5689
  for (const { name, parts } of serversToWrap) {
5535
5690
  servers[name] = { ...servers[name], command: "node9", args: parts };
5536
5691
  }
5537
5692
  mcpConfig.mcpServers = servers;
5538
5693
  writeJson(mcpPath, mcpConfig);
5539
- console.log(import_chalk3.default.green(`
5694
+ console.log(import_chalk.default.green(`
5540
5695
  \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5541
5696
  anythingChanged = true;
5542
5697
  } else {
5543
- console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
5698
+ console.log(import_chalk.default.yellow(" Skipped MCP server wrapping."));
5544
5699
  }
5545
5700
  console.log("");
5546
5701
  }
5547
5702
  console.log(
5548
- import_chalk3.default.yellow(
5703
+ import_chalk.default.yellow(
5549
5704
  " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
5550
5705
  )
5551
5706
  );
5552
5707
  console.log("");
5553
5708
  if (!anythingChanged && serversToWrap.length === 0) {
5554
5709
  console.log(
5555
- import_chalk3.default.blue(
5710
+ import_chalk.default.blue(
5556
5711
  "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
5557
5712
  )
5558
5713
  );
@@ -5560,8 +5715,8 @@ async function setupCursor() {
5560
5715
  return;
5561
5716
  }
5562
5717
  if (anythingChanged) {
5563
- console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5564
- console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
5718
+ console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5719
+ console.log(import_chalk.default.gray(" Restart Cursor for changes to take effect."));
5565
5720
  printDaemonTip();
5566
5721
  }
5567
5722
  }
@@ -5571,7 +5726,7 @@ init_daemon();
5571
5726
  var import_child_process6 = require("child_process");
5572
5727
  var import_execa = require("execa");
5573
5728
  var import_execa2 = require("execa");
5574
- var import_chalk6 = __toESM(require("chalk"));
5729
+ var import_chalk4 = __toESM(require("chalk"));
5575
5730
  var import_readline2 = __toESM(require("readline"));
5576
5731
  var import_fs8 = __toESM(require("fs"));
5577
5732
  var import_path10 = __toESM(require("path"));
@@ -5838,7 +5993,7 @@ function applyUndo(hash, cwd) {
5838
5993
 
5839
5994
  // src/cli.ts
5840
5995
  init_shields();
5841
- var import_prompts3 = require("@inquirer/prompts");
5996
+ var import_prompts2 = require("@inquirer/prompts");
5842
5997
  var { version } = JSON.parse(
5843
5998
  import_fs8.default.readFileSync(import_path10.default.join(__dirname, "../package.json"), "utf-8")
5844
5999
  );
@@ -5936,10 +6091,12 @@ function openBrowserLocal() {
5936
6091
  }
5937
6092
  async function autoStartDaemonAndWait() {
5938
6093
  try {
5939
- const child = (0, import_child_process6.spawn)("node9", ["daemon"], {
6094
+ const child = (0, import_child_process6.spawn)(process.execPath, [process.argv[1], "daemon"], {
5940
6095
  detached: true,
5941
6096
  stdio: "ignore",
5942
- env: { ...process.env, NODE9_AUTO_STARTED: "1" }
6097
+ // NODE9_BROWSER_OPENED=1 tells the daemon we will open the browser ourselves
6098
+ // (openBrowserLocal below), so it must not open a duplicate tab on first approval.
6099
+ env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
5943
6100
  });
5944
6101
  child.unref();
5945
6102
  for (let i = 0; i < 20; i++) {
@@ -5972,7 +6129,7 @@ async function runProxy(targetCommand) {
5972
6129
  if (stdout) executable = stdout.trim();
5973
6130
  } catch {
5974
6131
  }
5975
- console.error(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
6132
+ console.error(import_chalk4.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5976
6133
  const child = (0, import_child_process6.spawn)(executable, args, {
5977
6134
  stdio: ["pipe", "pipe", "inherit"],
5978
6135
  // We control STDIN and STDOUT
@@ -5993,14 +6150,14 @@ async function runProxy(targetCommand) {
5993
6150
  try {
5994
6151
  const name = message.params?.name || message.params?.tool_name || "unknown";
5995
6152
  const toolArgs = message.params?.arguments || message.params?.tool_input || {};
5996
- const result = await authorizeHeadless(sanitize(name), toolArgs, true, {
6153
+ const result = await authorizeHeadless(sanitize(name), toolArgs, {
5997
6154
  agent: "Proxy/MCP"
5998
6155
  });
5999
6156
  if (!result.approved) {
6000
- console.error(import_chalk6.default.red(`
6157
+ console.error(import_chalk4.default.red(`
6001
6158
  \u{1F6D1} Node9 Sudo: Action Blocked`));
6002
- console.error(import_chalk6.default.gray(` Tool: ${name}`));
6003
- console.error(import_chalk6.default.gray(` Reason: ${result.reason || "Security Policy"}
6159
+ console.error(import_chalk4.default.gray(` Tool: ${name}`));
6160
+ console.error(import_chalk4.default.gray(` Reason: ${result.reason || "Security Policy"}
6004
6161
  `));
6005
6162
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
6006
6163
  const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -6087,31 +6244,31 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
6087
6244
  import_fs8.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
6088
6245
  }
6089
6246
  if (options.profile && profileName !== "default") {
6090
- console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
6091
- console.log(import_chalk6.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
6247
+ console.log(import_chalk4.default.green(`\u2705 Profile "${profileName}" saved`));
6248
+ console.log(import_chalk4.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
6092
6249
  } else if (options.local) {
6093
- console.log(import_chalk6.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
6094
- console.log(import_chalk6.default.gray(` All decisions stay on this machine.`));
6250
+ console.log(import_chalk4.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
6251
+ console.log(import_chalk4.default.gray(` All decisions stay on this machine.`));
6095
6252
  } else {
6096
- console.log(import_chalk6.default.green(`\u2705 Logged in \u2014 agent mode`));
6097
- console.log(import_chalk6.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
6253
+ console.log(import_chalk4.default.green(`\u2705 Logged in \u2014 agent mode`));
6254
+ console.log(import_chalk4.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
6098
6255
  }
6099
6256
  });
6100
6257
  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) => {
6101
6258
  if (target === "gemini") return await setupGemini();
6102
6259
  if (target === "claude") return await setupClaude();
6103
6260
  if (target === "cursor") return await setupCursor();
6104
- console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6261
+ console.error(import_chalk4.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6105
6262
  process.exit(1);
6106
6263
  });
6107
6264
  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) => {
6108
6265
  if (!target) {
6109
- console.log(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
6110
- console.log(" Usage: " + import_chalk6.default.white("node9 setup <target>") + "\n");
6266
+ console.log(import_chalk4.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
6267
+ console.log(" Usage: " + import_chalk4.default.white("node9 setup <target>") + "\n");
6111
6268
  console.log(" Targets:");
6112
- console.log(" " + import_chalk6.default.green("claude") + " \u2014 Claude Code (hook mode)");
6113
- console.log(" " + import_chalk6.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
6114
- console.log(" " + import_chalk6.default.green("cursor") + " \u2014 Cursor (hook mode)");
6269
+ console.log(" " + import_chalk4.default.green("claude") + " \u2014 Claude Code (hook mode)");
6270
+ console.log(" " + import_chalk4.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
6271
+ console.log(" " + import_chalk4.default.green("cursor") + " \u2014 Cursor (hook mode)");
6115
6272
  console.log("");
6116
6273
  return;
6117
6274
  }
@@ -6119,7 +6276,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
6119
6276
  if (t === "gemini") return await setupGemini();
6120
6277
  if (t === "claude") return await setupClaude();
6121
6278
  if (t === "cursor") return await setupCursor();
6122
- console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6279
+ console.error(import_chalk4.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6123
6280
  process.exit(1);
6124
6281
  });
6125
6282
  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) => {
@@ -6128,30 +6285,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
6128
6285
  else if (target === "gemini") fn = teardownGemini;
6129
6286
  else if (target === "cursor") fn = teardownCursor;
6130
6287
  else {
6131
- console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6288
+ console.error(import_chalk4.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6132
6289
  process.exit(1);
6133
6290
  }
6134
- console.log(import_chalk6.default.cyan(`
6291
+ console.log(import_chalk4.default.cyan(`
6135
6292
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
6136
6293
  `));
6137
6294
  try {
6138
6295
  fn();
6139
6296
  } catch (err) {
6140
- console.error(import_chalk6.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
6297
+ console.error(import_chalk4.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
6141
6298
  process.exit(1);
6142
6299
  }
6143
- console.log(import_chalk6.default.gray("\n Restart the agent for changes to take effect."));
6300
+ console.log(import_chalk4.default.gray("\n Restart the agent for changes to take effect."));
6144
6301
  });
6145
6302
  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) => {
6146
- console.log(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
6147
- console.log(import_chalk6.default.bold("Stopping daemon..."));
6303
+ console.log(import_chalk4.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
6304
+ console.log(import_chalk4.default.bold("Stopping daemon..."));
6148
6305
  try {
6149
6306
  stopDaemon();
6150
- console.log(import_chalk6.default.green(" \u2705 Daemon stopped"));
6307
+ console.log(import_chalk4.default.green(" \u2705 Daemon stopped"));
6151
6308
  } catch {
6152
- console.log(import_chalk6.default.blue(" \u2139\uFE0F Daemon was not running"));
6309
+ console.log(import_chalk4.default.blue(" \u2139\uFE0F Daemon was not running"));
6153
6310
  }
6154
- console.log(import_chalk6.default.bold("\nRemoving hooks..."));
6311
+ console.log(import_chalk4.default.bold("\nRemoving hooks..."));
6155
6312
  let teardownFailed = false;
6156
6313
  for (const [label, fn] of [
6157
6314
  ["Claude", teardownClaude],
@@ -6163,7 +6320,7 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
6163
6320
  } catch (err) {
6164
6321
  teardownFailed = true;
6165
6322
  console.error(
6166
- import_chalk6.default.red(
6323
+ import_chalk4.default.red(
6167
6324
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
6168
6325
  )
6169
6326
  );
@@ -6172,7 +6329,7 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
6172
6329
  if (options.purge) {
6173
6330
  const node9Dir = import_path10.default.join(import_os7.default.homedir(), ".node9");
6174
6331
  if (import_fs8.default.existsSync(node9Dir)) {
6175
- const confirmed = await (0, import_prompts3.confirm)({
6332
+ const confirmed = await (0, import_prompts2.confirm)({
6176
6333
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
6177
6334
  default: false
6178
6335
  });
@@ -6180,48 +6337,48 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
6180
6337
  import_fs8.default.rmSync(node9Dir, { recursive: true });
6181
6338
  if (import_fs8.default.existsSync(node9Dir)) {
6182
6339
  console.error(
6183
- import_chalk6.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
6340
+ import_chalk4.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
6184
6341
  );
6185
6342
  } else {
6186
- console.log(import_chalk6.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
6343
+ console.log(import_chalk4.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
6187
6344
  }
6188
6345
  } else {
6189
- console.log(import_chalk6.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
6346
+ console.log(import_chalk4.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
6190
6347
  }
6191
6348
  } else {
6192
- console.log(import_chalk6.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
6349
+ console.log(import_chalk4.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
6193
6350
  }
6194
6351
  } else {
6195
6352
  console.log(
6196
- import_chalk6.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
6353
+ import_chalk4.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
6197
6354
  );
6198
6355
  }
6199
6356
  if (teardownFailed) {
6200
- console.error(import_chalk6.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
6357
+ console.error(import_chalk4.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
6201
6358
  process.exit(1);
6202
6359
  }
6203
- console.log(import_chalk6.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
6204
- console.log(import_chalk6.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
6360
+ console.log(import_chalk4.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
6361
+ console.log(import_chalk4.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
6205
6362
  });
6206
6363
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
6207
6364
  const homeDir2 = import_os7.default.homedir();
6208
6365
  let failures = 0;
6209
6366
  function pass(msg) {
6210
- console.log(import_chalk6.default.green(" \u2705 ") + msg);
6367
+ console.log(import_chalk4.default.green(" \u2705 ") + msg);
6211
6368
  }
6212
6369
  function fail(msg, hint) {
6213
- console.log(import_chalk6.default.red(" \u274C ") + msg);
6214
- if (hint) console.log(import_chalk6.default.gray(" " + hint));
6370
+ console.log(import_chalk4.default.red(" \u274C ") + msg);
6371
+ if (hint) console.log(import_chalk4.default.gray(" " + hint));
6215
6372
  failures++;
6216
6373
  }
6217
6374
  function warn(msg, hint) {
6218
- console.log(import_chalk6.default.yellow(" \u26A0\uFE0F ") + msg);
6219
- if (hint) console.log(import_chalk6.default.gray(" " + hint));
6375
+ console.log(import_chalk4.default.yellow(" \u26A0\uFE0F ") + msg);
6376
+ if (hint) console.log(import_chalk4.default.gray(" " + hint));
6220
6377
  }
6221
6378
  function section(title) {
6222
- console.log("\n" + import_chalk6.default.bold(title));
6379
+ console.log("\n" + import_chalk4.default.bold(title));
6223
6380
  }
6224
- console.log(import_chalk6.default.cyan.bold(`
6381
+ console.log(import_chalk4.default.cyan.bold(`
6225
6382
  \u{1F6E1}\uFE0F Node9 Doctor v${version}
6226
6383
  `));
6227
6384
  section("Binary");
@@ -6336,9 +6493,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
6336
6493
  }
6337
6494
  console.log("");
6338
6495
  if (failures === 0) {
6339
- console.log(import_chalk6.default.green.bold(" All checks passed. Node9 is ready.\n"));
6496
+ console.log(import_chalk4.default.green.bold(" All checks passed. Node9 is ready.\n"));
6340
6497
  } else {
6341
- console.log(import_chalk6.default.red.bold(` ${failures} check(s) failed. See hints above.
6498
+ console.log(import_chalk4.default.red.bold(` ${failures} check(s) failed. See hints above.
6342
6499
  `));
6343
6500
  process.exit(1);
6344
6501
  }
@@ -6353,7 +6510,7 @@ program.command("explain").description(
6353
6510
  try {
6354
6511
  args = JSON.parse(trimmed);
6355
6512
  } catch {
6356
- console.error(import_chalk6.default.red(`
6513
+ console.error(import_chalk4.default.red(`
6357
6514
  \u274C Invalid JSON: ${trimmed}
6358
6515
  `));
6359
6516
  process.exit(1);
@@ -6364,54 +6521,54 @@ program.command("explain").description(
6364
6521
  }
6365
6522
  const result = await explainPolicy(tool, args);
6366
6523
  console.log("");
6367
- console.log(import_chalk6.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
6524
+ console.log(import_chalk4.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
6368
6525
  console.log("");
6369
- console.log(` ${import_chalk6.default.bold("Tool:")} ${import_chalk6.default.white(result.tool)}`);
6526
+ console.log(` ${import_chalk4.default.bold("Tool:")} ${import_chalk4.default.white(result.tool)}`);
6370
6527
  if (argsRaw) {
6371
6528
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
6372
- console.log(` ${import_chalk6.default.bold("Input:")} ${import_chalk6.default.gray(preview)}`);
6529
+ console.log(` ${import_chalk4.default.bold("Input:")} ${import_chalk4.default.gray(preview)}`);
6373
6530
  }
6374
6531
  console.log("");
6375
- console.log(import_chalk6.default.bold("Config Sources (Waterfall):"));
6532
+ console.log(import_chalk4.default.bold("Config Sources (Waterfall):"));
6376
6533
  for (const tier of result.waterfall) {
6377
- const num = import_chalk6.default.gray(` ${tier.tier}.`);
6534
+ const num = import_chalk4.default.gray(` ${tier.tier}.`);
6378
6535
  const label = tier.label.padEnd(16);
6379
6536
  let statusStr;
6380
6537
  if (tier.tier === 1) {
6381
- statusStr = import_chalk6.default.gray(tier.note ?? "");
6538
+ statusStr = import_chalk4.default.gray(tier.note ?? "");
6382
6539
  } else if (tier.status === "active") {
6383
- const loc = tier.path ? import_chalk6.default.gray(tier.path) : "";
6384
- const note = tier.note ? import_chalk6.default.gray(`(${tier.note})`) : "";
6385
- statusStr = import_chalk6.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
6540
+ const loc = tier.path ? import_chalk4.default.gray(tier.path) : "";
6541
+ const note = tier.note ? import_chalk4.default.gray(`(${tier.note})`) : "";
6542
+ statusStr = import_chalk4.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
6386
6543
  } else {
6387
- statusStr = import_chalk6.default.gray("\u25CB " + (tier.note ?? "not found"));
6544
+ statusStr = import_chalk4.default.gray("\u25CB " + (tier.note ?? "not found"));
6388
6545
  }
6389
- console.log(`${num} ${import_chalk6.default.white(label)} ${statusStr}`);
6546
+ console.log(`${num} ${import_chalk4.default.white(label)} ${statusStr}`);
6390
6547
  }
6391
6548
  console.log("");
6392
- console.log(import_chalk6.default.bold("Policy Evaluation:"));
6549
+ console.log(import_chalk4.default.bold("Policy Evaluation:"));
6393
6550
  for (const step of result.steps) {
6394
6551
  const isFinal = step.isFinal;
6395
6552
  let icon;
6396
- if (step.outcome === "allow") icon = import_chalk6.default.green(" \u2705");
6397
- else if (step.outcome === "review") icon = import_chalk6.default.red(" \u{1F534}");
6398
- else if (step.outcome === "skip") icon = import_chalk6.default.gray(" \u2500 ");
6399
- else icon = import_chalk6.default.gray(" \u25CB ");
6553
+ if (step.outcome === "allow") icon = import_chalk4.default.green(" \u2705");
6554
+ else if (step.outcome === "review") icon = import_chalk4.default.red(" \u{1F534}");
6555
+ else if (step.outcome === "skip") icon = import_chalk4.default.gray(" \u2500 ");
6556
+ else icon = import_chalk4.default.gray(" \u25CB ");
6400
6557
  const name = step.name.padEnd(18);
6401
- const nameStr = isFinal ? import_chalk6.default.white.bold(name) : import_chalk6.default.white(name);
6402
- const detail = isFinal ? import_chalk6.default.white(step.detail) : import_chalk6.default.gray(step.detail);
6403
- const arrow = isFinal ? import_chalk6.default.yellow(" \u2190 STOP") : "";
6558
+ const nameStr = isFinal ? import_chalk4.default.white.bold(name) : import_chalk4.default.white(name);
6559
+ const detail = isFinal ? import_chalk4.default.white(step.detail) : import_chalk4.default.gray(step.detail);
6560
+ const arrow = isFinal ? import_chalk4.default.yellow(" \u2190 STOP") : "";
6404
6561
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
6405
6562
  }
6406
6563
  console.log("");
6407
6564
  if (result.decision === "allow") {
6408
- console.log(import_chalk6.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk6.default.gray(" \u2014 no approval needed"));
6565
+ console.log(import_chalk4.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk4.default.gray(" \u2014 no approval needed"));
6409
6566
  } else {
6410
6567
  console.log(
6411
- import_chalk6.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk6.default.gray(" \u2014 human approval required")
6568
+ import_chalk4.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk4.default.gray(" \u2014 human approval required")
6412
6569
  );
6413
6570
  if (result.blockedByLabel) {
6414
- console.log(import_chalk6.default.gray(` Reason: ${result.blockedByLabel}`));
6571
+ console.log(import_chalk4.default.gray(` Reason: ${result.blockedByLabel}`));
6415
6572
  }
6416
6573
  }
6417
6574
  console.log("");
@@ -6419,8 +6576,8 @@ program.command("explain").description(
6419
6576
  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) => {
6420
6577
  const configPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "config.json");
6421
6578
  if (import_fs8.default.existsSync(configPath) && !options.force) {
6422
- console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
6423
- console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
6579
+ console.log(import_chalk4.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
6580
+ console.log(import_chalk4.default.gray(` Run with --force to overwrite.`));
6424
6581
  return;
6425
6582
  }
6426
6583
  const requestedMode = options.mode.toLowerCase();
@@ -6435,10 +6592,10 @@ program.command("init").description("Create ~/.node9/config.json with default po
6435
6592
  const dir = import_path10.default.dirname(configPath);
6436
6593
  if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
6437
6594
  import_fs8.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
6438
- console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
6439
- console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
6595
+ console.log(import_chalk4.default.green(`\u2705 Global config created: ${configPath}`));
6596
+ console.log(import_chalk4.default.cyan(` Mode set to: ${safeMode}`));
6440
6597
  console.log(
6441
- import_chalk6.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
6598
+ import_chalk4.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
6442
6599
  );
6443
6600
  });
6444
6601
  function formatRelativeTime(timestamp) {
@@ -6455,7 +6612,7 @@ program.command("audit").description("View local execution audit log").option("-
6455
6612
  const logPath = import_path10.default.join(import_os7.default.homedir(), ".node9", "audit.log");
6456
6613
  if (!import_fs8.default.existsSync(logPath)) {
6457
6614
  console.log(
6458
- import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
6615
+ import_chalk4.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
6459
6616
  );
6460
6617
  return;
6461
6618
  }
@@ -6481,31 +6638,31 @@ program.command("audit").description("View local execution audit log").option("-
6481
6638
  return;
6482
6639
  }
6483
6640
  if (entries.length === 0) {
6484
- console.log(import_chalk6.default.yellow("No matching audit entries."));
6641
+ console.log(import_chalk4.default.yellow("No matching audit entries."));
6485
6642
  return;
6486
6643
  }
6487
6644
  console.log(
6488
6645
  `
6489
- ${import_chalk6.default.bold("Node9 Audit Log")} ${import_chalk6.default.dim(`(${entries.length} entries)`)}`
6646
+ ${import_chalk4.default.bold("Node9 Audit Log")} ${import_chalk4.default.dim(`(${entries.length} entries)`)}`
6490
6647
  );
6491
- console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
6648
+ console.log(import_chalk4.default.dim(" " + "\u2500".repeat(65)));
6492
6649
  console.log(
6493
6650
  ` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
6494
6651
  );
6495
- console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
6652
+ console.log(import_chalk4.default.dim(" " + "\u2500".repeat(65)));
6496
6653
  for (const e of entries) {
6497
6654
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
6498
6655
  const tool = String(e.tool).slice(0, 17).padEnd(18);
6499
- const result = e.decision === "allow" ? import_chalk6.default.green("ALLOW".padEnd(10)) : import_chalk6.default.red("DENY".padEnd(10));
6656
+ const result = e.decision === "allow" ? import_chalk4.default.green("ALLOW".padEnd(10)) : import_chalk4.default.red("DENY".padEnd(10));
6500
6657
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
6501
6658
  const agent = String(e.agent || "unknown");
6502
6659
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
6503
6660
  }
6504
6661
  const allowed = entries.filter((e) => e.decision === "allow").length;
6505
6662
  const denied = entries.filter((e) => e.decision === "deny").length;
6506
- console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
6663
+ console.log(import_chalk4.default.dim(" " + "\u2500".repeat(65)));
6507
6664
  console.log(
6508
- ` ${entries.length} entries | ${import_chalk6.default.green(allowed + " allowed")} | ${import_chalk6.default.red(denied + " denied")}
6665
+ ` ${entries.length} entries | ${import_chalk4.default.green(allowed + " allowed")} | ${import_chalk4.default.red(denied + " denied")}
6509
6666
  `
6510
6667
  );
6511
6668
  });
@@ -6516,43 +6673,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
6516
6673
  const settings = mergedConfig.settings;
6517
6674
  console.log("");
6518
6675
  if (creds && settings.approvers.cloud) {
6519
- console.log(import_chalk6.default.green(" \u25CF Agent mode") + import_chalk6.default.gray(" \u2014 cloud team policy enforced"));
6676
+ console.log(import_chalk4.default.green(" \u25CF Agent mode") + import_chalk4.default.gray(" \u2014 cloud team policy enforced"));
6520
6677
  } else if (creds && !settings.approvers.cloud) {
6521
6678
  console.log(
6522
- import_chalk6.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 all decisions stay on this machine")
6679
+ import_chalk4.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk4.default.gray(" \u2014 all decisions stay on this machine")
6523
6680
  );
6524
6681
  } else {
6525
6682
  console.log(
6526
- import_chalk6.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 no API key (Local rules only)")
6683
+ import_chalk4.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk4.default.gray(" \u2014 no API key (Local rules only)")
6527
6684
  );
6528
6685
  }
6529
6686
  console.log("");
6530
6687
  if (daemonRunning) {
6531
6688
  console.log(
6532
- import_chalk6.default.green(" \u25CF Daemon running") + import_chalk6.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
6689
+ import_chalk4.default.green(" \u25CF Daemon running") + import_chalk4.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
6533
6690
  );
6534
6691
  } else {
6535
- console.log(import_chalk6.default.gray(" \u25CB Daemon stopped"));
6692
+ console.log(import_chalk4.default.gray(" \u25CB Daemon stopped"));
6536
6693
  }
6537
6694
  if (settings.enableUndo) {
6538
6695
  console.log(
6539
- import_chalk6.default.magenta(" \u25CF Undo Engine") + import_chalk6.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
6696
+ import_chalk4.default.magenta(" \u25CF Undo Engine") + import_chalk4.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
6540
6697
  );
6541
6698
  }
6542
6699
  console.log("");
6543
- const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
6700
+ const modeLabel = settings.mode === "audit" ? import_chalk4.default.blue("audit") : settings.mode === "strict" ? import_chalk4.default.red("strict") : import_chalk4.default.white("standard");
6544
6701
  console.log(` Mode: ${modeLabel}`);
6545
6702
  const projectConfig = import_path10.default.join(process.cwd(), "node9.config.json");
6546
6703
  const globalConfig = import_path10.default.join(import_os7.default.homedir(), ".node9", "config.json");
6547
6704
  console.log(
6548
- ` Local: ${import_fs8.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
6705
+ ` Local: ${import_fs8.default.existsSync(projectConfig) ? import_chalk4.default.green("Active (node9.config.json)") : import_chalk4.default.gray("Not present")}`
6549
6706
  );
6550
6707
  console.log(
6551
- ` Global: ${import_fs8.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
6708
+ ` Global: ${import_fs8.default.existsSync(globalConfig) ? import_chalk4.default.green("Active (~/.node9/config.json)") : import_chalk4.default.gray("Not present")}`
6552
6709
  );
6553
6710
  if (mergedConfig.policy.sandboxPaths.length > 0) {
6554
6711
  console.log(
6555
- ` Sandbox: ${import_chalk6.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
6712
+ ` Sandbox: ${import_chalk4.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
6556
6713
  );
6557
6714
  }
6558
6715
  const pauseState = checkPause();
@@ -6560,7 +6717,7 @@ program.command("status").description("Show current Node9 mode, policy source, a
6560
6717
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
6561
6718
  console.log("");
6562
6719
  console.log(
6563
- import_chalk6.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk6.default.gray(" \u2014 all tool calls allowed")
6720
+ import_chalk4.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk4.default.gray(" \u2014 all tool calls allowed")
6564
6721
  );
6565
6722
  }
6566
6723
  console.log("");
@@ -6574,14 +6731,14 @@ program.command("daemon").description("Run the local approval server").argument(
6574
6731
  if (cmd === "stop") return stopDaemon();
6575
6732
  if (cmd === "status") return daemonStatus();
6576
6733
  if (cmd !== "start" && action !== void 0) {
6577
- console.error(import_chalk6.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
6734
+ console.error(import_chalk4.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
6578
6735
  process.exit(1);
6579
6736
  }
6580
6737
  if (options.watch) {
6581
6738
  process.env.NODE9_WATCH_MODE = "1";
6582
6739
  setTimeout(() => {
6583
6740
  openBrowserLocal();
6584
- console.log(import_chalk6.default.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
6741
+ console.log(import_chalk4.default.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
6585
6742
  }, 600);
6586
6743
  startDaemon();
6587
6744
  return;
@@ -6589,24 +6746,30 @@ program.command("daemon").description("Run the local approval server").argument(
6589
6746
  if (options.openui) {
6590
6747
  if (isDaemonRunning()) {
6591
6748
  openBrowserLocal();
6592
- console.log(import_chalk6.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
6749
+ console.log(import_chalk4.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
6593
6750
  process.exit(0);
6594
6751
  }
6595
- const child = (0, import_child_process6.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
6752
+ const child = (0, import_child_process6.spawn)(process.execPath, [process.argv[1], "daemon"], {
6753
+ detached: true,
6754
+ stdio: "ignore"
6755
+ });
6596
6756
  child.unref();
6597
6757
  for (let i = 0; i < 12; i++) {
6598
6758
  await new Promise((r) => setTimeout(r, 250));
6599
6759
  if (isDaemonRunning()) break;
6600
6760
  }
6601
6761
  openBrowserLocal();
6602
- console.log(import_chalk6.default.green(`
6762
+ console.log(import_chalk4.default.green(`
6603
6763
  \u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
6604
6764
  process.exit(0);
6605
6765
  }
6606
6766
  if (options.background) {
6607
- const child = (0, import_child_process6.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
6767
+ const child = (0, import_child_process6.spawn)(process.execPath, [process.argv[1], "daemon"], {
6768
+ detached: true,
6769
+ stdio: "ignore"
6770
+ });
6608
6771
  child.unref();
6609
- console.log(import_chalk6.default.green(`
6772
+ console.log(import_chalk4.default.green(`
6610
6773
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
6611
6774
  process.exit(0);
6612
6775
  }
@@ -6618,9 +6781,63 @@ program.command("tail").description("Stream live agent activity to the terminal"
6618
6781
  try {
6619
6782
  await startTail2(options);
6620
6783
  } catch (err) {
6621
- console.error(import_chalk6.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
6784
+ console.error(import_chalk4.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
6785
+ process.exit(1);
6786
+ }
6787
+ });
6788
+ program.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
6789
+ let port = DAEMON_PORT2;
6790
+ try {
6791
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
6792
+ signal: AbortSignal.timeout(500)
6793
+ });
6794
+ if (res.ok) {
6795
+ const data = await res.json();
6796
+ if (typeof data.port === "number") port = data.port;
6797
+ } else {
6798
+ throw new Error("not running");
6799
+ }
6800
+ } catch {
6801
+ console.error(import_chalk4.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
6802
+ const child = (0, import_child_process6.spawn)(process.execPath, [process.argv[1], "daemon"], {
6803
+ detached: true,
6804
+ stdio: "ignore",
6805
+ env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_WATCH_MODE: "1" }
6806
+ });
6807
+ child.unref();
6808
+ let ready = false;
6809
+ for (let i = 0; i < 20; i++) {
6810
+ await new Promise((r) => setTimeout(r, 250));
6811
+ try {
6812
+ const r = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
6813
+ signal: AbortSignal.timeout(500)
6814
+ });
6815
+ if (r.ok) {
6816
+ ready = true;
6817
+ break;
6818
+ }
6819
+ } catch {
6820
+ }
6821
+ }
6822
+ if (!ready) {
6823
+ console.error(import_chalk4.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6824
+ process.exit(1);
6825
+ }
6826
+ }
6827
+ console.error(
6828
+ import_chalk4.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk4.default.dim(` \u2192 localhost:${port}`) + import_chalk4.default.dim(
6829
+ "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
6830
+ )
6831
+ );
6832
+ const result = (0, import_child_process6.spawnSync)(cmd, args, {
6833
+ stdio: "inherit",
6834
+ env: { ...process.env, NODE9_WATCH_MODE: "1" }
6835
+ });
6836
+ if (result.error) {
6837
+ console.error(import_chalk4.default.red(`\u274C Failed to run command: ${result.error.message}`));
6622
6838
  process.exit(1);
6623
6839
  }
6840
+ process.exit(result.status ?? 0);
6624
6841
  });
6625
6842
  program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
6626
6843
  const processPayload = async (raw) => {
@@ -6659,19 +6876,30 @@ RAW: ${raw}
6659
6876
  const sendBlock = (msg, result2) => {
6660
6877
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
6661
6878
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
6662
- if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
6663
- console.error(import_chalk6.default.bgRed.white.bold(`
6879
+ let ttyFd = null;
6880
+ try {
6881
+ ttyFd = import_fs8.default.openSync("/dev/tty", "w");
6882
+ const writeTty = (line) => import_fs8.default.writeSync(ttyFd, line + "\n");
6883
+ if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
6884
+ writeTty(import_chalk4.default.bgRed.white.bold(`
6664
6885
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
6665
- console.error(import_chalk6.default.red.bold(` A sensitive secret was found in the tool arguments!`));
6666
- } else {
6667
- console.error(import_chalk6.default.red(`
6886
+ writeTty(import_chalk4.default.red.bold(` A sensitive secret was found in the tool arguments!`));
6887
+ } else {
6888
+ writeTty(import_chalk4.default.red(`
6668
6889
  \u{1F6D1} Node9 blocked "${toolName}"`));
6890
+ }
6891
+ writeTty(import_chalk4.default.gray(` Triggered by: ${blockedByContext}`));
6892
+ if (result2?.changeHint) writeTty(import_chalk4.default.cyan(` To change: ${result2.changeHint}`));
6893
+ writeTty("");
6894
+ } catch {
6895
+ } finally {
6896
+ if (ttyFd !== null)
6897
+ try {
6898
+ import_fs8.default.closeSync(ttyFd);
6899
+ } catch {
6900
+ }
6669
6901
  }
6670
- console.error(import_chalk6.default.gray(` Triggered by: ${blockedByContext}`));
6671
- if (result2?.changeHint) console.error(import_chalk6.default.cyan(` To change: ${result2.changeHint}`));
6672
- console.error("");
6673
6902
  const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
6674
- console.error(import_chalk6.default.dim(` (Detailed instructions sent to AI agent)`));
6675
6903
  process.stdout.write(
6676
6904
  JSON.stringify({
6677
6905
  decision: "block",
@@ -6694,7 +6922,10 @@ RAW: ${raw}
6694
6922
  if (shouldSnapshot(toolName, toolInput, config)) {
6695
6923
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6696
6924
  }
6697
- const result = await authorizeHeadless(toolName, toolInput, false, meta);
6925
+ const safeCwdForAuth = typeof payload.cwd === "string" && import_path10.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
6926
+ const result = await authorizeHeadless(toolName, toolInput, meta, {
6927
+ cwd: safeCwdForAuth
6928
+ });
6698
6929
  if (result.approved) {
6699
6930
  if (result.checkedBy && process.env.NODE9_DEBUG === "1")
6700
6931
  process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
@@ -6702,10 +6933,20 @@ RAW: ${raw}
6702
6933
  process.exit(0);
6703
6934
  }
6704
6935
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
6705
- console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
6936
+ try {
6937
+ const tty = import_fs8.default.openSync("/dev/tty", "w");
6938
+ import_fs8.default.writeSync(
6939
+ tty,
6940
+ import_chalk4.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
6941
+ );
6942
+ import_fs8.default.closeSync(tty);
6943
+ } catch {
6944
+ }
6706
6945
  const daemonReady = await autoStartDaemonAndWait();
6707
6946
  if (daemonReady) {
6708
- const retry = await authorizeHeadless(toolName, toolInput, false, meta);
6947
+ const retry = await authorizeHeadless(toolName, toolInput, meta, {
6948
+ cwd: safeCwdForAuth
6949
+ });
6709
6950
  if (retry.approved) {
6710
6951
  if (retry.checkedBy && process.env.NODE9_DEBUG === "1")
6711
6952
  process.stderr.write(`\u2713 node9 [${retry.checkedBy}]: "${toolName}" allowed
@@ -6812,7 +7053,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
6812
7053
  const ms = parseDuration(options.duration);
6813
7054
  if (ms === null) {
6814
7055
  console.error(
6815
- import_chalk6.default.red(`
7056
+ import_chalk4.default.red(`
6816
7057
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
6817
7058
  `)
6818
7059
  );
@@ -6820,20 +7061,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
6820
7061
  }
6821
7062
  pauseNode9(ms, options.duration);
6822
7063
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
6823
- console.log(import_chalk6.default.yellow(`
7064
+ console.log(import_chalk4.default.yellow(`
6824
7065
  \u23F8 Node9 paused until ${expiresAt}`));
6825
- console.log(import_chalk6.default.gray(` All tool calls will be allowed without review.`));
6826
- console.log(import_chalk6.default.gray(` Run "node9 resume" to re-enable early.
7066
+ console.log(import_chalk4.default.gray(` All tool calls will be allowed without review.`));
7067
+ console.log(import_chalk4.default.gray(` Run "node9 resume" to re-enable early.
6827
7068
  `));
6828
7069
  });
6829
7070
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
6830
7071
  const { paused } = checkPause();
6831
7072
  if (!paused) {
6832
- console.log(import_chalk6.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
7073
+ console.log(import_chalk4.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
6833
7074
  return;
6834
7075
  }
6835
7076
  resumeNode9();
6836
- console.log(import_chalk6.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
7077
+ console.log(import_chalk4.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
6837
7078
  });
6838
7079
  var HOOK_BASED_AGENTS = {
6839
7080
  claude: "claude",
@@ -6846,37 +7087,50 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
6846
7087
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
6847
7088
  const target = HOOK_BASED_AGENTS[firstArg2];
6848
7089
  console.error(
6849
- import_chalk6.default.yellow(`
7090
+ import_chalk4.default.yellow(`
6850
7091
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
6851
7092
  );
6852
- console.error(import_chalk6.default.white(`
7093
+ console.error(import_chalk4.default.white(`
6853
7094
  "${target}" uses its own hook system. Use:`));
6854
7095
  console.error(
6855
- import_chalk6.default.green(` node9 addto ${target} `) + import_chalk6.default.gray("# one-time setup")
7096
+ import_chalk4.default.green(` node9 addto ${target} `) + import_chalk4.default.gray("# one-time setup")
6856
7097
  );
6857
- console.error(import_chalk6.default.green(` ${target} `) + import_chalk6.default.gray("# run normally"));
7098
+ console.error(import_chalk4.default.green(` ${target} `) + import_chalk4.default.gray("# run normally"));
6858
7099
  process.exit(1);
6859
7100
  }
6860
- const fullCommand = commandArgs.join(" ");
6861
- let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
6862
- agent: "Terminal"
6863
- });
7101
+ const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
7102
+ if (runArgs.length === 0) {
7103
+ program.help();
7104
+ return;
7105
+ }
7106
+ const fullCommand = runArgs.join(" ");
7107
+ let result = await authorizeHeadless(
7108
+ "shell",
7109
+ { command: fullCommand },
7110
+ {
7111
+ agent: "Terminal"
7112
+ }
7113
+ );
6864
7114
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
6865
- console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
7115
+ console.error(import_chalk4.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
6866
7116
  const daemonReady = await autoStartDaemonAndWait();
6867
7117
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
6868
7118
  }
6869
7119
  if (result.noApprovalMechanism && process.stdout.isTTY) {
6870
- result = await authorizeHeadless("shell", { command: fullCommand }, true);
7120
+ const approved = await (0, import_prompts2.confirm)({
7121
+ message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
7122
+ default: false
7123
+ });
7124
+ result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
6871
7125
  }
6872
7126
  if (!result.approved) {
6873
7127
  console.error(
6874
- import_chalk6.default.red(`
7128
+ import_chalk4.default.red(`
6875
7129
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
6876
7130
  );
6877
7131
  process.exit(1);
6878
7132
  }
6879
- console.error(import_chalk6.default.green("\n\u2705 Approved \u2014 running command...\n"));
7133
+ console.error(import_chalk4.default.green("\n\u2705 Approved \u2014 running command...\n"));
6880
7134
  await runProxy(fullCommand);
6881
7135
  } else {
6882
7136
  program.help();
@@ -6891,22 +7145,22 @@ program.command("undo").description(
6891
7145
  if (history.length === 0) {
6892
7146
  if (!options.all && allHistory.length > 0) {
6893
7147
  console.log(
6894
- import_chalk6.default.yellow(
7148
+ import_chalk4.default.yellow(
6895
7149
  `
6896
7150
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
6897
- Run ${import_chalk6.default.cyan("node9 undo --all")} to see snapshots from all projects.
7151
+ Run ${import_chalk4.default.cyan("node9 undo --all")} to see snapshots from all projects.
6898
7152
  `
6899
7153
  )
6900
7154
  );
6901
7155
  } else {
6902
- console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
7156
+ console.log(import_chalk4.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
6903
7157
  }
6904
7158
  return;
6905
7159
  }
6906
7160
  const idx = history.length - steps;
6907
7161
  if (idx < 0) {
6908
7162
  console.log(
6909
- import_chalk6.default.yellow(
7163
+ import_chalk4.default.yellow(
6910
7164
  `
6911
7165
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
6912
7166
  `
@@ -6917,18 +7171,18 @@ program.command("undo").description(
6917
7171
  const snapshot = history[idx];
6918
7172
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
6919
7173
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
6920
- console.log(import_chalk6.default.magenta.bold(`
7174
+ console.log(import_chalk4.default.magenta.bold(`
6921
7175
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
6922
7176
  console.log(
6923
- import_chalk6.default.white(
6924
- ` Tool: ${import_chalk6.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk6.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
7177
+ import_chalk4.default.white(
7178
+ ` Tool: ${import_chalk4.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk4.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
6925
7179
  )
6926
7180
  );
6927
- console.log(import_chalk6.default.white(` When: ${import_chalk6.default.gray(ageStr)}`));
6928
- console.log(import_chalk6.default.white(` Dir: ${import_chalk6.default.gray(snapshot.cwd)}`));
7181
+ console.log(import_chalk4.default.white(` When: ${import_chalk4.default.gray(ageStr)}`));
7182
+ console.log(import_chalk4.default.white(` Dir: ${import_chalk4.default.gray(snapshot.cwd)}`));
6929
7183
  if (steps > 1)
6930
7184
  console.log(
6931
- import_chalk6.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
7185
+ import_chalk4.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
6932
7186
  );
6933
7187
  console.log("");
6934
7188
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -6936,65 +7190,65 @@ program.command("undo").description(
6936
7190
  const lines = diff.split("\n");
6937
7191
  for (const line of lines) {
6938
7192
  if (line.startsWith("+++") || line.startsWith("---")) {
6939
- console.log(import_chalk6.default.bold(line));
7193
+ console.log(import_chalk4.default.bold(line));
6940
7194
  } else if (line.startsWith("+")) {
6941
- console.log(import_chalk6.default.green(line));
7195
+ console.log(import_chalk4.default.green(line));
6942
7196
  } else if (line.startsWith("-")) {
6943
- console.log(import_chalk6.default.red(line));
7197
+ console.log(import_chalk4.default.red(line));
6944
7198
  } else if (line.startsWith("@@")) {
6945
- console.log(import_chalk6.default.cyan(line));
7199
+ console.log(import_chalk4.default.cyan(line));
6946
7200
  } else {
6947
- console.log(import_chalk6.default.gray(line));
7201
+ console.log(import_chalk4.default.gray(line));
6948
7202
  }
6949
7203
  }
6950
7204
  console.log("");
6951
7205
  } else {
6952
7206
  console.log(
6953
- import_chalk6.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
7207
+ import_chalk4.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
6954
7208
  );
6955
7209
  }
6956
- const proceed = await (0, import_prompts3.confirm)({
7210
+ const proceed = await (0, import_prompts2.confirm)({
6957
7211
  message: `Revert to this snapshot?`,
6958
7212
  default: false
6959
7213
  });
6960
7214
  if (proceed) {
6961
7215
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
6962
- console.log(import_chalk6.default.green("\n\u2705 Reverted successfully.\n"));
7216
+ console.log(import_chalk4.default.green("\n\u2705 Reverted successfully.\n"));
6963
7217
  } else {
6964
- console.error(import_chalk6.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
7218
+ console.error(import_chalk4.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
6965
7219
  }
6966
7220
  } else {
6967
- console.log(import_chalk6.default.gray("\nCancelled.\n"));
7221
+ console.log(import_chalk4.default.gray("\nCancelled.\n"));
6968
7222
  }
6969
7223
  });
6970
7224
  var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
6971
7225
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
6972
7226
  const name = resolveShieldName(service);
6973
7227
  if (!name) {
6974
- console.error(import_chalk6.default.red(`
7228
+ console.error(import_chalk4.default.red(`
6975
7229
  \u274C Unknown shield: "${service}"
6976
7230
  `));
6977
- console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
7231
+ console.log(`Run ${import_chalk4.default.cyan("node9 shield list")} to see available shields.
6978
7232
  `);
6979
7233
  process.exit(1);
6980
7234
  }
6981
7235
  const shield = getShield(name);
6982
7236
  const active = readActiveShields();
6983
7237
  if (active.includes(name)) {
6984
- console.log(import_chalk6.default.yellow(`
7238
+ console.log(import_chalk4.default.yellow(`
6985
7239
  \u2139\uFE0F Shield "${name}" is already active.
6986
7240
  `));
6987
7241
  return;
6988
7242
  }
6989
7243
  writeActiveShields([...active, name]);
6990
- console.log(import_chalk6.default.green(`
7244
+ console.log(import_chalk4.default.green(`
6991
7245
  \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
6992
- console.log(import_chalk6.default.gray(` ${shield.smartRules.length} smart rules now active.`));
7246
+ console.log(import_chalk4.default.gray(` ${shield.smartRules.length} smart rules now active.`));
6993
7247
  if (shield.dangerousWords.length > 0)
6994
- console.log(import_chalk6.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
7248
+ console.log(import_chalk4.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
6995
7249
  if (name === "filesystem") {
6996
7250
  console.log(
6997
- import_chalk6.default.yellow(
7251
+ import_chalk4.default.yellow(
6998
7252
  `
6999
7253
  \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
7000
7254
  Tools like unlink, find -delete, or language-level file ops are not intercepted.`
@@ -7006,70 +7260,70 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
7006
7260
  shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
7007
7261
  const name = resolveShieldName(service);
7008
7262
  if (!name) {
7009
- console.error(import_chalk6.default.red(`
7263
+ console.error(import_chalk4.default.red(`
7010
7264
  \u274C Unknown shield: "${service}"
7011
7265
  `));
7012
- console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
7266
+ console.log(`Run ${import_chalk4.default.cyan("node9 shield list")} to see available shields.
7013
7267
  `);
7014
7268
  process.exit(1);
7015
7269
  }
7016
7270
  const active = readActiveShields();
7017
7271
  if (!active.includes(name)) {
7018
- console.log(import_chalk6.default.yellow(`
7272
+ console.log(import_chalk4.default.yellow(`
7019
7273
  \u2139\uFE0F Shield "${name}" is not active.
7020
7274
  `));
7021
7275
  return;
7022
7276
  }
7023
7277
  writeActiveShields(active.filter((s) => s !== name));
7024
- console.log(import_chalk6.default.green(`
7278
+ console.log(import_chalk4.default.green(`
7025
7279
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
7026
7280
  `));
7027
7281
  });
7028
7282
  shieldCmd.command("list").description("Show all available shields").action(() => {
7029
7283
  const active = new Set(readActiveShields());
7030
- console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
7284
+ console.log(import_chalk4.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
7031
7285
  for (const shield of listShields()) {
7032
- const status = active.has(shield.name) ? import_chalk6.default.green("\u25CF enabled") : import_chalk6.default.gray("\u25CB disabled");
7033
- console.log(` ${status} ${import_chalk6.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
7286
+ const status = active.has(shield.name) ? import_chalk4.default.green("\u25CF enabled") : import_chalk4.default.gray("\u25CB disabled");
7287
+ console.log(` ${status} ${import_chalk4.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
7034
7288
  if (shield.aliases.length > 0)
7035
- console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
7289
+ console.log(import_chalk4.default.gray(` aliases: ${shield.aliases.join(", ")}`));
7036
7290
  }
7037
7291
  console.log("");
7038
7292
  });
7039
7293
  shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
7040
7294
  const active = readActiveShields();
7041
7295
  if (active.length === 0) {
7042
- console.error(import_chalk6.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
7043
- console.error(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
7296
+ console.error(import_chalk4.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
7297
+ console.error(`Run ${import_chalk4.default.cyan("node9 shield list")} to see available shields.
7044
7298
  `);
7045
7299
  return;
7046
7300
  }
7047
7301
  const overrides = readShieldOverrides();
7048
- console.error(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
7302
+ console.error(import_chalk4.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
7049
7303
  for (const name of active) {
7050
7304
  const shield = getShield(name);
7051
7305
  if (!shield) continue;
7052
- console.error(` ${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)} \u2014 ${shield.description}`);
7306
+ console.error(` ${import_chalk4.default.green("\u25CF")} ${import_chalk4.default.cyan(name)} \u2014 ${shield.description}`);
7053
7307
  const ruleOverrides = overrides[name] ?? {};
7054
7308
  for (const rule of shield.smartRules) {
7055
7309
  const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
7056
7310
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
7057
7311
  const effectiveVerdict = overrideVerdict ?? rule.verdict;
7058
- const verdictLabel = effectiveVerdict === "block" ? import_chalk6.default.red("block ") : effectiveVerdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow ");
7059
- const overrideNote = overrideVerdict ? import_chalk6.default.gray(` \u2190 overridden (was: ${rule.verdict})`) : "";
7312
+ const verdictLabel = effectiveVerdict === "block" ? import_chalk4.default.red("block ") : effectiveVerdict === "review" ? import_chalk4.default.yellow("review") : import_chalk4.default.green("allow ");
7313
+ const overrideNote = overrideVerdict ? import_chalk4.default.gray(` \u2190 overridden (was: ${rule.verdict})`) : "";
7060
7314
  console.error(
7061
- ` ${verdictLabel} ${shortName.padEnd(24)} ${import_chalk6.default.gray(rule.reason ?? "")}${overrideNote}`
7315
+ ` ${verdictLabel} ${shortName.padEnd(24)} ${import_chalk4.default.gray(rule.reason ?? "")}${overrideNote}`
7062
7316
  );
7063
7317
  }
7064
7318
  if (shield.dangerousWords.length > 0) {
7065
- console.error(import_chalk6.default.gray(` words: ${shield.dangerousWords.join(", ")}`));
7319
+ console.error(import_chalk4.default.gray(` words: ${shield.dangerousWords.join(", ")}`));
7066
7320
  }
7067
7321
  console.error("");
7068
7322
  }
7069
7323
  if (Object.keys(overrides).length > 0) {
7070
7324
  console.error(
7071
- import_chalk6.default.gray(
7072
- ` Tip: run ${import_chalk6.default.cyan("node9 shield unset <shield> <rule>")} to remove an override.
7325
+ import_chalk4.default.gray(
7326
+ ` Tip: run ${import_chalk4.default.cyan("node9 shield unset <shield> <rule>")} to remove an override.
7073
7327
  `
7074
7328
  )
7075
7329
  );
@@ -7078,35 +7332,35 @@ shieldCmd.command("status").description("Show active shields and their individua
7078
7332
  shieldCmd.command("set <service> <rule> <verdict>").description("Override the verdict for a specific shield rule (block, review, or allow)").option("--force", "Required when setting verdict to allow (silences a block rule)").action((service, rule, verdict, opts) => {
7079
7333
  const name = resolveShieldName(service);
7080
7334
  if (!name) {
7081
- console.error(import_chalk6.default.red(`
7335
+ console.error(import_chalk4.default.red(`
7082
7336
  \u274C Unknown shield: "${service}"
7083
7337
  `));
7084
- console.error(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
7338
+ console.error(`Run ${import_chalk4.default.cyan("node9 shield list")} to see available shields.
7085
7339
  `);
7086
7340
  process.exit(1);
7087
7341
  }
7088
7342
  if (!readActiveShields().includes(name)) {
7089
- console.error(import_chalk6.default.red(`
7343
+ console.error(import_chalk4.default.red(`
7090
7344
  \u274C Shield "${name}" is not active. Enable it first:
7091
7345
  `));
7092
- console.error(` ${import_chalk6.default.cyan(`node9 shield enable ${name}`)}
7346
+ console.error(` ${import_chalk4.default.cyan(`node9 shield enable ${name}`)}
7093
7347
  `);
7094
7348
  process.exit(1);
7095
7349
  }
7096
7350
  if (!isShieldVerdict(verdict)) {
7097
- console.error(import_chalk6.default.red(`
7351
+ console.error(import_chalk4.default.red(`
7098
7352
  \u274C Invalid verdict "${verdict}". Use: block, review, or allow
7099
7353
  `));
7100
7354
  process.exit(1);
7101
7355
  }
7102
7356
  if (verdict === "allow" && !opts.force) {
7103
7357
  console.error(
7104
- import_chalk6.default.red(`
7358
+ import_chalk4.default.red(`
7105
7359
  \u26A0\uFE0F Setting a verdict to "allow" silences the rule entirely.
7106
- `) + import_chalk6.default.yellow(
7360
+ `) + import_chalk4.default.yellow(
7107
7361
  ` This disables a shield protection. If you are sure, re-run with --force:
7108
7362
  `
7109
- ) + import_chalk6.default.cyan(`
7363
+ ) + import_chalk4.default.cyan(`
7110
7364
  node9 shield set ${service} ${rule} allow --force
7111
7365
  `)
7112
7366
  );
@@ -7115,13 +7369,13 @@ shieldCmd.command("set <service> <rule> <verdict>").description("Override the ve
7115
7369
  const ruleName = resolveShieldRule(name, rule);
7116
7370
  if (!ruleName) {
7117
7371
  const shield = getShield(name);
7118
- console.error(import_chalk6.default.red(`
7372
+ console.error(import_chalk4.default.red(`
7119
7373
  \u274C Unknown rule "${rule}" for shield "${name}".
7120
7374
  `));
7121
7375
  console.error(" Available rules:");
7122
7376
  for (const r of shield?.smartRules ?? []) {
7123
7377
  const short = r.name ? r.name.replace(`shield:${name}:`, "") : "";
7124
- console.error(` ${import_chalk6.default.cyan(short)}`);
7378
+ console.error(` ${import_chalk4.default.cyan(short)}`);
7125
7379
  }
7126
7380
  console.error("");
7127
7381
  process.exit(1);
@@ -7131,33 +7385,33 @@ shieldCmd.command("set <service> <rule> <verdict>").description("Override the ve
7131
7385
  appendConfigAudit({ event: "shield-override-allow", shield: name, rule: ruleName });
7132
7386
  }
7133
7387
  const shortName = ruleName.replace(`shield:${name}:`, "");
7134
- const verdictLabel = verdict === "block" ? import_chalk6.default.red("block") : verdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow");
7388
+ const verdictLabel = verdict === "block" ? import_chalk4.default.red("block") : verdict === "review" ? import_chalk4.default.yellow("review") : import_chalk4.default.green("allow");
7135
7389
  if (verdict === "allow") {
7136
7390
  console.error(
7137
- import_chalk6.default.yellow(`
7138
- \u26A0\uFE0F ${name}/${shortName} \u2192 ${verdictLabel}`) + import_chalk6.default.gray(" (rule silenced \u2014 use `node9 shield unset` to restore)\n")
7391
+ import_chalk4.default.yellow(`
7392
+ \u26A0\uFE0F ${name}/${shortName} \u2192 ${verdictLabel}`) + import_chalk4.default.gray(" (rule silenced \u2014 use `node9 shield unset` to restore)\n")
7139
7393
  );
7140
7394
  } else {
7141
- console.error(import_chalk6.default.green(`
7395
+ console.error(import_chalk4.default.green(`
7142
7396
  \u2705 ${name}/${shortName} \u2192 ${verdictLabel}
7143
7397
  `));
7144
7398
  }
7145
7399
  console.error(
7146
- import_chalk6.default.gray(` Run ${import_chalk6.default.cyan("node9 shield status")} to see all active rules.
7400
+ import_chalk4.default.gray(` Run ${import_chalk4.default.cyan("node9 shield status")} to see all active rules.
7147
7401
  `)
7148
7402
  );
7149
7403
  });
7150
7404
  shieldCmd.command("unset <service> <rule>").description("Remove a verdict override, restoring the shield default").action((service, rule) => {
7151
7405
  const name = resolveShieldName(service);
7152
7406
  if (!name) {
7153
- console.error(import_chalk6.default.red(`
7407
+ console.error(import_chalk4.default.red(`
7154
7408
  \u274C Unknown shield: "${service}"
7155
7409
  `));
7156
7410
  process.exit(1);
7157
7411
  }
7158
7412
  const ruleName = resolveShieldRule(name, rule);
7159
7413
  if (!ruleName) {
7160
- console.error(import_chalk6.default.red(`
7414
+ console.error(import_chalk4.default.red(`
7161
7415
  \u274C Unknown rule "${rule}" for shield "${name}".
7162
7416
  `));
7163
7417
  process.exit(1);
@@ -7165,7 +7419,7 @@ shieldCmd.command("unset <service> <rule>").description("Remove a verdict overri
7165
7419
  clearShieldOverride(name, ruleName);
7166
7420
  const shortName = ruleName.replace(`shield:${name}:`, "");
7167
7421
  console.error(
7168
- import_chalk6.default.green(`
7422
+ import_chalk4.default.green(`
7169
7423
  \u2705 Override removed \u2014 ${name}/${shortName} restored to default.
7170
7424
  `)
7171
7425
  );
@@ -7174,32 +7428,32 @@ program.command("config show").description("Show the full effective runtime conf
7174
7428
  const config = getConfig();
7175
7429
  const active = readActiveShields();
7176
7430
  const overrides = readShieldOverrides();
7177
- console.error(import_chalk6.default.bold("\n\u{1F50D} Node9 Effective Configuration\n"));
7178
- const modeLabel = config.settings.mode === "audit" ? import_chalk6.default.blue("audit") : config.settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
7431
+ console.error(import_chalk4.default.bold("\n\u{1F50D} Node9 Effective Configuration\n"));
7432
+ const modeLabel = config.settings.mode === "audit" ? import_chalk4.default.blue("audit") : config.settings.mode === "strict" ? import_chalk4.default.red("strict") : import_chalk4.default.white("standard");
7179
7433
  console.error(` Mode: ${modeLabel}
7180
7434
  `);
7181
7435
  if (active.length > 0) {
7182
- console.error(import_chalk6.default.bold(" \u2500\u2500 Active Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7436
+ console.error(import_chalk4.default.bold(" \u2500\u2500 Active Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7183
7437
  for (const name of active) {
7184
7438
  const shield = getShield(name);
7185
7439
  if (!shield) continue;
7186
7440
  const ruleOverrides = overrides[name] ?? {};
7187
7441
  console.error(`
7188
- ${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)}`);
7442
+ ${import_chalk4.default.green("\u25CF")} ${import_chalk4.default.cyan(name)}`);
7189
7443
  for (const rule of shield.smartRules) {
7190
7444
  const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
7191
7445
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
7192
7446
  const effectiveVerdict = overrideVerdict ?? rule.verdict;
7193
- const vLabel = effectiveVerdict === "block" ? import_chalk6.default.red("block ") : effectiveVerdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow ");
7194
- const note = overrideVerdict ? import_chalk6.default.gray(` \u2190 overridden`) : "";
7447
+ const vLabel = effectiveVerdict === "block" ? import_chalk4.default.red("block ") : effectiveVerdict === "review" ? import_chalk4.default.yellow("review") : import_chalk4.default.green("allow ");
7448
+ const note = overrideVerdict ? import_chalk4.default.gray(` \u2190 overridden`) : "";
7195
7449
  console.error(` ${vLabel} ${shortName}${note}`);
7196
7450
  }
7197
7451
  }
7198
7452
  console.error("");
7199
7453
  } else {
7200
- console.error(import_chalk6.default.gray(" No shields active. Run `node9 shield list` to see options.\n"));
7454
+ console.error(import_chalk4.default.gray(" No shields active. Run `node9 shield list` to see options.\n"));
7201
7455
  }
7202
- console.error(import_chalk6.default.bold(" \u2500\u2500 Built-in Rules (always on) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7456
+ console.error(import_chalk4.default.bold(" \u2500\u2500 Built-in Rules (always on) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7203
7457
  for (const rule of config.policy.smartRules) {
7204
7458
  const isShieldRule = rule.name?.startsWith("shield:");
7205
7459
  const isAdvisory = [
@@ -7210,11 +7464,11 @@ program.command("config show").description("Show the full effective runtime conf
7210
7464
  "review-drop-column-sql"
7211
7465
  ].includes(rule.name ?? "");
7212
7466
  if (isShieldRule || isAdvisory) continue;
7213
- const vLabel = rule.verdict === "block" ? import_chalk6.default.red("block ") : rule.verdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow ");
7214
- console.error(` ${vLabel} ${import_chalk6.default.gray(rule.name ?? rule.tool)}`);
7467
+ const vLabel = rule.verdict === "block" ? import_chalk4.default.red("block ") : rule.verdict === "review" ? import_chalk4.default.yellow("review") : import_chalk4.default.green("allow ");
7468
+ console.error(` ${vLabel} ${import_chalk4.default.gray(rule.name ?? rule.tool)}`);
7215
7469
  }
7216
7470
  console.error("");
7217
- console.error(import_chalk6.default.bold(" \u2500\u2500 Safe by Default (advisory, overridable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7471
+ console.error(import_chalk4.default.bold(" \u2500\u2500 Safe by Default (advisory, overridable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7218
7472
  const advisoryNames = /* @__PURE__ */ new Set([
7219
7473
  "review-rm",
7220
7474
  "allow-rm-safe-paths",
@@ -7224,12 +7478,12 @@ program.command("config show").description("Show the full effective runtime conf
7224
7478
  ]);
7225
7479
  for (const rule of config.policy.smartRules) {
7226
7480
  if (!advisoryNames.has(rule.name ?? "")) continue;
7227
- const vLabel = rule.verdict === "block" ? import_chalk6.default.red("block ") : rule.verdict === "review" ? import_chalk6.default.yellow("review") : import_chalk6.default.green("allow ");
7228
- console.error(` ${vLabel} ${import_chalk6.default.gray(rule.name ?? rule.tool)}`);
7481
+ const vLabel = rule.verdict === "block" ? import_chalk4.default.red("block ") : rule.verdict === "review" ? import_chalk4.default.yellow("review") : import_chalk4.default.green("allow ");
7482
+ console.error(` ${vLabel} ${import_chalk4.default.gray(rule.name ?? rule.tool)}`);
7229
7483
  }
7230
7484
  console.error("");
7231
- console.error(import_chalk6.default.bold(" \u2500\u2500 Dangerous Words \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7232
- console.error(` ${import_chalk6.default.gray(config.policy.dangerousWords.join(", "))}
7485
+ console.error(import_chalk4.default.bold(" \u2500\u2500 Dangerous Words \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7486
+ console.error(` ${import_chalk4.default.gray(config.policy.dangerousWords.join(", "))}
7233
7487
  `);
7234
7488
  });
7235
7489
  if (process.argv[2] !== "daemon") {