@node9/proxy 1.1.6 → 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.mjs CHANGED
@@ -120,7 +120,6 @@ var init_context_sniper = __esm({
120
120
  // src/ui/native.ts
121
121
  import { spawn } from "child_process";
122
122
  import path2 from "path";
123
- import chalk from "chalk";
124
123
  function formatArgs(args, matchedField, matchedWord) {
125
124
  if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
126
125
  let parsed = args;
@@ -199,21 +198,6 @@ ${smartTruncate(str, 500)}`
199
198
  }
200
199
  return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
201
200
  }
202
- function sendDesktopNotification(title, body) {
203
- if (isTestEnv()) return;
204
- try {
205
- if (process.platform === "darwin") {
206
- const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
207
- spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
208
- } else if (process.platform === "linux") {
209
- spawn("notify-send", [title, body, "--icon=dialog-warning"], {
210
- detached: true,
211
- stdio: "ignore"
212
- }).unref();
213
- }
214
- } catch {
215
- }
216
- }
217
201
  function escapePango(text) {
218
202
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
219
203
  }
@@ -256,9 +240,6 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
256
240
  const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
257
241
  const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
258
242
  const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
259
- process.stderr.write(chalk.yellow(`
260
- \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
261
- `));
262
243
  return new Promise((resolve) => {
263
244
  let childProcess = null;
264
245
  const onAbort = () => {
@@ -422,6 +403,7 @@ var init_config_schema = __esm({
422
403
  enableUndo: z.boolean().optional(),
423
404
  enableHookLogDebug: z.boolean().optional(),
424
405
  approvalTimeoutMs: z.number().nonnegative().optional(),
406
+ approvalTimeoutSeconds: z.number().nonnegative().optional(),
425
407
  flightRecorder: z.boolean().optional(),
426
408
  approvers: z.object({
427
409
  native: z.boolean().optional(),
@@ -890,8 +872,6 @@ var init_dlp = __esm({
890
872
  });
891
873
 
892
874
  // src/core.ts
893
- import chalk2 from "chalk";
894
- import { confirm } from "@inquirer/prompts";
895
875
  import fs3 from "fs";
896
876
  import path5 from "path";
897
877
  import os2 from "os";
@@ -1654,14 +1634,12 @@ function getPersistentDecision(toolName) {
1654
1634
  }
1655
1635
  return null;
1656
1636
  }
1657
- async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1637
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
1658
1638
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1659
- const checkCtrl = new AbortController();
1660
- const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
1661
- const onAbort = () => checkCtrl.abort();
1662
- if (signal) signal.addEventListener("abort", onAbort);
1639
+ const ctrl = new AbortController();
1640
+ const timer = setTimeout(() => ctrl.abort(), 5e3);
1663
1641
  try {
1664
- const checkRes = await fetch(`${base}/check`, {
1642
+ const res = await fetch(`${base}/check`, {
1665
1643
  method: "POST",
1666
1644
  headers: { "Content-Type": "application/json" },
1667
1645
  body: JSON.stringify({
@@ -1672,32 +1650,34 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId)
1672
1650
  fromCLI: true,
1673
1651
  // Pass the flight-recorder ID so the daemon uses the same UUID for
1674
1652
  // activity-result as the CLI used for the pending activity event.
1675
- // Without this, the two UUIDs never match and tail.ts never resolves
1676
- // the pending item.
1677
1653
  activityId,
1678
- ...riskMetadata && { riskMetadata }
1654
+ ...riskMetadata && { riskMetadata },
1655
+ ...cwd && { cwd }
1679
1656
  }),
1680
- signal: checkCtrl.signal
1657
+ signal: ctrl.signal
1681
1658
  });
1682
- if (!checkRes.ok) throw new Error("Daemon fail");
1683
- const { id } = await checkRes.json();
1684
- const waitCtrl = new AbortController();
1685
- const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
1686
- const onWaitAbort = () => waitCtrl.abort();
1687
- if (signal) signal.addEventListener("abort", onWaitAbort);
1688
- try {
1689
- const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
1690
- if (!waitRes.ok) return "deny";
1691
- const { decision } = await waitRes.json();
1692
- if (decision === "allow") return "allow";
1693
- if (decision === "abandoned") return "abandoned";
1694
- return "deny";
1695
- } finally {
1696
- clearTimeout(waitTimer);
1697
- if (signal) signal.removeEventListener("abort", onWaitAbort);
1698
- }
1659
+ if (!res.ok) throw new Error("Daemon fail");
1660
+ const { id } = await res.json();
1661
+ return id;
1662
+ } finally {
1663
+ clearTimeout(timer);
1664
+ }
1665
+ }
1666
+ async function waitForDaemonDecision(id, signal) {
1667
+ const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1668
+ const waitCtrl = new AbortController();
1669
+ const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
1670
+ const onAbort = () => waitCtrl.abort();
1671
+ if (signal) signal.addEventListener("abort", onAbort);
1672
+ try {
1673
+ const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
1674
+ if (!waitRes.ok) return { decision: "deny" };
1675
+ const { decision, source } = await waitRes.json();
1676
+ if (decision === "allow") return { decision: "allow", source };
1677
+ if (decision === "abandoned") return { decision: "abandoned", source };
1678
+ return { decision: "deny", source };
1699
1679
  } finally {
1700
- clearTimeout(checkTimer);
1680
+ clearTimeout(waitTimer);
1701
1681
  if (signal) signal.removeEventListener("abort", onAbort);
1702
1682
  }
1703
1683
  }
@@ -1744,12 +1724,12 @@ function notifyActivity(data) {
1744
1724
  }
1745
1725
  });
1746
1726
  }
1747
- async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1727
+ async function authorizeHeadless(toolName, args, meta, options) {
1748
1728
  if (!options?.calledFromDaemon) {
1749
1729
  const actId = randomUUID();
1750
1730
  const actTs = Date.now();
1751
1731
  await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1752
- const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1732
+ const result = await _authorizeHeadlessCore(toolName, args, meta, {
1753
1733
  ...options,
1754
1734
  activityId: actId
1755
1735
  });
@@ -1764,14 +1744,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1764
1744
  }
1765
1745
  return result;
1766
1746
  }
1767
- return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1747
+ return _authorizeHeadlessCore(toolName, args, meta, options);
1768
1748
  }
1769
- async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1749
+ async function _authorizeHeadlessCore(toolName, args, meta, options) {
1770
1750
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1771
1751
  const pauseState = checkPause();
1772
1752
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
1773
1753
  const creds = getCredentials();
1774
- const config = getConfig();
1754
+ const config = getConfig(options?.cwd);
1775
1755
  const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
1776
1756
  const approvers = {
1777
1757
  ...config.settings.approvers || { native: true, browser: true, cloud: true, terminal: true }
@@ -1816,10 +1796,6 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1816
1796
  if (approvers.cloud && creds?.apiKey) {
1817
1797
  await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
1818
1798
  }
1819
- sendDesktopNotification(
1820
- "Node9 Audit Mode",
1821
- `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
1822
- );
1823
1799
  }
1824
1800
  }
1825
1801
  return { approved: true, checkedBy: "audit" };
@@ -1879,23 +1855,12 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1879
1855
  return { approved: true };
1880
1856
  }
1881
1857
  let cloudRequestId = null;
1882
- let isRemoteLocked = false;
1883
1858
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
1884
1859
  if (cloudEnforced) {
1885
1860
  try {
1886
1861
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
1887
1862
  if (!initResult.pending) {
1888
1863
  if (initResult.shadowMode) {
1889
- console.error(
1890
- chalk2.yellow(
1891
- `
1892
- \u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
1893
- )
1894
- );
1895
- if (initResult.shadowReason) {
1896
- console.error(chalk2.dim(` Reason: ${initResult.shadowReason}
1897
- `));
1898
- }
1899
1864
  return { approved: true, checkedBy: "cloud" };
1900
1865
  }
1901
1866
  return {
@@ -1907,36 +1872,8 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1907
1872
  };
1908
1873
  }
1909
1874
  cloudRequestId = initResult.requestId || null;
1910
- isRemoteLocked = !!initResult.remoteApprovalOnly;
1911
1875
  explainableLabel = "Organization Policy (SaaS)";
1912
- } catch (err) {
1913
- const error = err;
1914
- const isAuthError = error.message.includes("401") || error.message.includes("403");
1915
- const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
1916
- 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;
1917
- console.error(
1918
- chalk2.yellow(`
1919
- \u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk2.dim(`
1920
- Falling back to local rules...
1921
- `)
1922
- );
1923
- }
1924
- }
1925
- if (!options?.calledFromDaemon) {
1926
- if (cloudEnforced && cloudRequestId) {
1927
- console.error(
1928
- chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
1929
- );
1930
- console.error(
1931
- chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n")
1932
- );
1933
- } else if (!cloudEnforced) {
1934
- const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
1935
- console.error(
1936
- chalk2.dim(`
1937
- \u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
1938
- `)
1939
- );
1876
+ } catch {
1940
1877
  }
1941
1878
  }
1942
1879
  const abortController = new AbortController();
@@ -1963,15 +1900,29 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1963
1900
  }
1964
1901
  let viewerId = null;
1965
1902
  const internalToken = getInternalToken();
1903
+ let daemonEntryId = null;
1904
+ if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
1905
+ if (cloudEnforced && cloudRequestId) {
1906
+ viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
1907
+ daemonEntryId = viewerId;
1908
+ } else {
1909
+ try {
1910
+ daemonEntryId = await registerDaemonEntry(
1911
+ toolName,
1912
+ args,
1913
+ meta,
1914
+ riskMetadata,
1915
+ options?.activityId,
1916
+ options?.cwd
1917
+ );
1918
+ } catch {
1919
+ }
1920
+ }
1921
+ }
1966
1922
  if (cloudEnforced && cloudRequestId) {
1967
1923
  racePromises.push(
1968
1924
  (async () => {
1969
1925
  try {
1970
- if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
1971
- viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
1972
- () => null
1973
- );
1974
- }
1975
1926
  const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
1976
1927
  return {
1977
1928
  approved: cloudResult.approved,
@@ -1996,7 +1947,7 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
1996
1947
  args,
1997
1948
  meta?.agent,
1998
1949
  explainableLabel,
1999
- isRemoteLocked,
1950
+ false,
2000
1951
  signal,
2001
1952
  policyMatchedField,
2002
1953
  policyMatchedWord
@@ -2011,96 +1962,31 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
2011
1962
  reason: isApproved ? void 0 : "The human user clicked 'Block' on the system dialog window.",
2012
1963
  checkedBy: isApproved ? "daemon" : void 0,
2013
1964
  blockedBy: isApproved ? void 0 : "local-decision",
2014
- blockedByLabel: "User Decision (Native)"
1965
+ blockedByLabel: "User Decision (Native)",
1966
+ decisionSource: "native"
2015
1967
  };
2016
1968
  })()
2017
1969
  );
2018
1970
  }
2019
- if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
1971
+ if (daemonEntryId && (approvers.browser || approvers.terminal)) {
2020
1972
  racePromises.push(
2021
1973
  (async () => {
2022
- try {
2023
- if (!approvers.native && !cloudEnforced) {
2024
- console.error(
2025
- chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
2026
- );
2027
- console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
2028
- `));
2029
- }
2030
- const daemonDecision = await askDaemon(
2031
- toolName,
2032
- args,
2033
- meta,
2034
- signal,
2035
- riskMetadata,
2036
- options?.activityId
2037
- );
2038
- if (daemonDecision === "abandoned") throw new Error("Abandoned");
2039
- const isApproved = daemonDecision === "allow";
2040
- return {
2041
- approved: isApproved,
2042
- reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
2043
- checkedBy: isApproved ? "daemon" : void 0,
2044
- blockedBy: isApproved ? void 0 : "local-decision",
2045
- blockedByLabel: "User Decision (Browser)"
2046
- };
2047
- } catch (err) {
2048
- throw err;
2049
- }
2050
- })()
2051
- );
2052
- }
2053
- if (approvers.terminal && allowTerminalFallback && process.stdout.isTTY) {
2054
- racePromises.push(
2055
- (async () => {
2056
- try {
2057
- if (explainableLabel.includes("DLP")) {
2058
- console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
2059
- console.log(
2060
- chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
2061
- );
2062
- } else {
2063
- console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
2064
- }
2065
- console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
2066
- console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
2067
- if (isRemoteLocked) {
2068
- console.log(chalk2.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
2069
- `));
2070
- await new Promise((_, reject) => {
2071
- signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
2072
- });
2073
- }
2074
- const TIMEOUT_MS = 6e4;
2075
- let timer;
2076
- const result = await new Promise((resolve, reject) => {
2077
- timer = setTimeout(() => reject(new Error("Terminal Timeout")), TIMEOUT_MS);
2078
- confirm(
2079
- { message: `Authorize? (auto-deny in ${TIMEOUT_MS / 1e3}s)`, default: false },
2080
- { signal }
2081
- ).then(resolve).catch(reject);
2082
- });
2083
- clearTimeout(timer);
2084
- return {
2085
- approved: result,
2086
- reason: result ? void 0 : "The human user typed 'N' in the terminal to reject this action.",
2087
- checkedBy: result ? "terminal" : void 0,
2088
- blockedBy: result ? void 0 : "local-decision",
2089
- blockedByLabel: "User Decision (Terminal)"
2090
- };
2091
- } catch (err) {
2092
- const error = err;
2093
- if (error.name === "AbortError" || error.message?.includes("Prompt was canceled") || error.message?.includes("Aborted by SaaS"))
2094
- throw err;
2095
- if (error.message === "Terminal Timeout") {
2096
- return {
2097
- approved: false,
2098
- reason: "The terminal prompt timed out without a human response.",
2099
- blockedBy: "local-decision"
2100
- };
2101
- }
2102
- throw err;
2103
- }
1974
+ const { decision: daemonDecision, source: decisionSource } = await waitForDaemonDecision(
1975
+ daemonEntryId,
1976
+ signal
1977
+ );
1978
+ if (daemonDecision === "abandoned") throw new Error("Abandoned");
1979
+ const isApproved = daemonDecision === "allow";
1980
+ const src = decisionSource === "terminal" || decisionSource === "browser" ? decisionSource : approvers.browser ? "browser" : "terminal";
1981
+ const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
1982
+ return {
1983
+ approved: isApproved,
1984
+ reason: isApproved ? void 0 : `The human user rejected this action via the Node9 ${via}.`,
1985
+ checkedBy: isApproved ? "daemon" : void 0,
1986
+ blockedBy: isApproved ? void 0 : "local-decision",
1987
+ blockedByLabel: `User Decision (${via})`,
1988
+ decisionSource: src
1989
+ };
2104
1990
  })()
2105
1991
  );
2106
1992
  }
@@ -2151,7 +2037,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
2151
2037
  }
2152
2038
  });
2153
2039
  if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
2154
- await resolveNode9SaaS(cloudRequestId, creds, finalResult.approved);
2040
+ await resolveNode9SaaS(
2041
+ cloudRequestId,
2042
+ creds,
2043
+ finalResult.approved,
2044
+ finalResult.decisionSource ?? finalResult.checkedBy ?? "local"
2045
+ );
2155
2046
  }
2156
2047
  if (!isManual) {
2157
2048
  appendLocalAudit(
@@ -2199,6 +2090,8 @@ function getConfig(cwd) {
2199
2090
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
2200
2091
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
2201
2092
  if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
2093
+ if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
2094
+ mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
2202
2095
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
2203
2096
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
2204
2097
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -2423,11 +2316,9 @@ async function pollNode9SaaS(requestId, creds, signal) {
2423
2316
  if (!statusRes.ok) continue;
2424
2317
  const { status, reason } = await statusRes.json();
2425
2318
  if (status === "APPROVED") {
2426
- console.error(chalk2.green("\u2705 Approved via Cloud.\n"));
2427
2319
  return { approved: true, reason };
2428
2320
  }
2429
2321
  if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
2430
- console.error(chalk2.red("\u274C Denied via Cloud.\n"));
2431
2322
  return { approved: false, reason };
2432
2323
  }
2433
2324
  } catch {
@@ -2435,19 +2326,34 @@ async function pollNode9SaaS(requestId, creds, signal) {
2435
2326
  }
2436
2327
  return { approved: false, reason: "Cloud approval timed out after 10 minutes." };
2437
2328
  }
2438
- async function resolveNode9SaaS(requestId, creds, approved) {
2329
+ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
2439
2330
  try {
2440
2331
  const resolveUrl = `${creds.apiUrl}/requests/${requestId}`;
2441
2332
  const ctrl = new AbortController();
2442
2333
  const timer = setTimeout(() => ctrl.abort(), 5e3);
2443
- await fetch(resolveUrl, {
2334
+ const res = await fetch(resolveUrl, {
2444
2335
  method: "PATCH",
2445
2336
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
2446
- body: JSON.stringify({ decision: approved ? "APPROVED" : "DENIED" }),
2337
+ body: JSON.stringify({
2338
+ decision: approved ? "APPROVED" : "DENIED",
2339
+ ...decidedBy && { decidedBy }
2340
+ }),
2447
2341
  signal: ctrl.signal
2448
2342
  });
2449
2343
  clearTimeout(timer);
2450
- } catch {
2344
+ if (!res.ok) {
2345
+ fs3.appendFileSync(
2346
+ HOOK_DEBUG_LOG,
2347
+ `[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
2348
+ `
2349
+ );
2350
+ }
2351
+ } catch (err) {
2352
+ fs3.appendFileSync(
2353
+ HOOK_DEBUG_LOG,
2354
+ `[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
2355
+ `
2356
+ );
2451
2357
  }
2452
2358
  }
2453
2359
  var 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;
@@ -2481,8 +2387,8 @@ var init_core = __esm({
2481
2387
  enableUndo: true,
2482
2388
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2483
2389
  enableHookLogDebug: true,
2484
- approvalTimeoutMs: 3e4,
2485
- // 30-second auto-deny timeout
2390
+ approvalTimeoutMs: 12e4,
2391
+ // 120-second auto-deny timeout
2486
2392
  flightRecorder: true,
2487
2393
  approvers: { native: true, browser: true, cloud: false, terminal: true }
2488
2394
  },
@@ -3746,7 +3652,7 @@ var init_ui = __esm({
3746
3652
  const res = await fetch('/decision/' + id, {
3747
3653
  method: 'POST',
3748
3654
  headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3749
- body: JSON.stringify({ decision, persist: !!persist }),
3655
+ body: JSON.stringify({ decision, persist: !!persist, source: 'browser' }),
3750
3656
  });
3751
3657
  if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3752
3658
  card?.remove();
@@ -3765,7 +3671,7 @@ var init_ui = __esm({
3765
3671
  const res = await fetch('/decision/' + id, {
3766
3672
  method: 'POST',
3767
3673
  headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
3768
- body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
3674
+ body: JSON.stringify({ decision: 'trust', trustDuration: duration, source: 'browser' }),
3769
3675
  });
3770
3676
  if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
3771
3677
  card?.remove();
@@ -4176,7 +4082,7 @@ import path7 from "path";
4176
4082
  import os4 from "os";
4177
4083
  import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
4178
4084
  import { randomUUID as randomUUID2 } from "crypto";
4179
- import chalk4 from "chalk";
4085
+ import chalk2 from "chalk";
4180
4086
  function atomicWriteSync2(filePath, data, options) {
4181
4087
  const dir = path7.dirname(filePath);
4182
4088
  if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
@@ -4295,7 +4201,7 @@ data: ${JSON.stringify(data)}
4295
4201
  `;
4296
4202
  sseClients.forEach((client) => {
4297
4203
  try {
4298
- client.write(msg);
4204
+ client.res.write(msg);
4299
4205
  } catch {
4300
4206
  sseClients.delete(client);
4301
4207
  }
@@ -4341,6 +4247,7 @@ function startDaemon() {
4341
4247
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
4342
4248
  const watchMode = process.env.NODE9_WATCH_MODE === "1";
4343
4249
  let idleTimer;
4250
+ let browserOpened = false;
4344
4251
  function resetIdleTimer() {
4345
4252
  if (watchMode) return;
4346
4253
  if (idleTimer) clearTimeout(idleTimer);
@@ -4357,12 +4264,15 @@ function startDaemon() {
4357
4264
  }
4358
4265
  resetIdleTimer();
4359
4266
  const server = http.createServer(async (req, res) => {
4360
- const { pathname } = new URL(req.url || "/", `http://${req.headers.host}`);
4267
+ const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
4268
+ const { pathname } = reqUrl;
4361
4269
  if (req.method === "GET" && pathname === "/") {
4362
4270
  res.writeHead(200, { "Content-Type": "text/html" });
4363
4271
  return res.end(UI_HTML);
4364
4272
  }
4365
4273
  if (req.method === "GET" && pathname === "/events") {
4274
+ const capParam = reqUrl.searchParams.get("capabilities") ?? "";
4275
+ const capabilities = capParam ? capParam.split(",").map((s) => s.trim()).filter(Boolean) : [];
4366
4276
  res.writeHead(200, {
4367
4277
  "Content-Type": "text/event-stream",
4368
4278
  "Cache-Control": "no-cache",
@@ -4373,7 +4283,8 @@ function startDaemon() {
4373
4283
  abandonTimer = null;
4374
4284
  }
4375
4285
  hadBrowserClient = true;
4376
- sseClients.add(res);
4286
+ const sseClient = { res, capabilities };
4287
+ sseClients.add(sseClient);
4377
4288
  res.write(
4378
4289
  `event: init
4379
4290
  data: ${JSON.stringify({
@@ -4388,7 +4299,7 @@ data: ${JSON.stringify({
4388
4299
  mcpServer: e.mcpServer
4389
4300
  })),
4390
4301
  orgName: getOrgName(),
4391
- autoDenyMs: AUTO_DENY_MS
4302
+ autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
4392
4303
  })}
4393
4304
 
4394
4305
  `
@@ -4410,6 +4321,10 @@ data: ${JSON.stringify({
4410
4321
 
4411
4322
  `
4412
4323
  );
4324
+ res.write(`event: csrf
4325
+ data: ${JSON.stringify({ token: csrfToken })}
4326
+
4327
+ `);
4413
4328
  for (const item of activityRing) {
4414
4329
  res.write(`event: ${item.event}
4415
4330
  data: ${JSON.stringify(item.data)}
@@ -4417,7 +4332,7 @@ data: ${JSON.stringify(item.data)}
4417
4332
  `);
4418
4333
  }
4419
4334
  return req.on("close", () => {
4420
- sseClients.delete(res);
4335
+ sseClients.delete(sseClient);
4421
4336
  if (sseClients.size === 0 && pending.size > 0) {
4422
4337
  abandonTimer = setTimeout(abandonPending, hadBrowserClient ? 1e4 : 15e3);
4423
4338
  }
@@ -4437,7 +4352,8 @@ data: ${JSON.stringify(item.data)}
4437
4352
  mcpServer,
4438
4353
  riskMetadata,
4439
4354
  fromCLI = false,
4440
- activityId
4355
+ activityId,
4356
+ cwd
4441
4357
  } = JSON.parse(body);
4442
4358
  const id = fromCLI && typeof activityId === "string" && activityId || randomUUID2();
4443
4359
  const entry = {
@@ -4467,7 +4383,7 @@ data: ${JSON.stringify(item.data)}
4467
4383
  pending.delete(id);
4468
4384
  broadcast("remove", { id });
4469
4385
  }
4470
- }, AUTO_DENY_MS)
4386
+ }, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
4471
4387
  };
4472
4388
  pending.set(id, entry);
4473
4389
  if (!fromCLI) {
@@ -4479,8 +4395,11 @@ data: ${JSON.stringify(item.data)}
4479
4395
  status: "pending"
4480
4396
  });
4481
4397
  }
4482
- const browserEnabled = getConfig().settings.approvers?.browser !== false;
4483
- if (browserEnabled) {
4398
+ const projectCwd = typeof cwd === "string" && path7.isAbsolute(cwd) ? cwd : void 0;
4399
+ const projectConfig = getConfig(projectCwd);
4400
+ const browserEnabled = projectConfig.settings.approvers?.browser !== false;
4401
+ const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
4402
+ if (browserEnabled || terminalEnabled) {
4484
4403
  broadcast("add", {
4485
4404
  id,
4486
4405
  toolName,
@@ -4488,17 +4407,21 @@ data: ${JSON.stringify(item.data)}
4488
4407
  riskMetadata: entry.riskMetadata,
4489
4408
  slackDelegated: entry.slackDelegated,
4490
4409
  agent: entry.agent,
4491
- mcpServer: entry.mcpServer
4410
+ mcpServer: entry.mcpServer,
4411
+ interactive: terminalEnabled
4492
4412
  });
4493
- if (sseClients.size === 0 && !autoStarted)
4413
+ const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
4414
+ if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
4415
+ browserOpened = true;
4494
4416
  openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
4417
+ }
4495
4418
  }
4496
4419
  res.writeHead(200, { "Content-Type": "application/json" });
4497
4420
  res.end(JSON.stringify({ id }));
4421
+ if (slackDelegated) return;
4498
4422
  authorizeHeadless(
4499
4423
  toolName,
4500
4424
  args,
4501
- false,
4502
4425
  {
4503
4426
  agent: typeof agent === "string" ? agent : void 0,
4504
4427
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
@@ -4508,6 +4431,8 @@ data: ${JSON.stringify(item.data)}
4508
4431
  const e = pending.get(id);
4509
4432
  if (!e) return;
4510
4433
  if (result.noApprovalMechanism) return;
4434
+ if (result.checkedBy === "audit") return;
4435
+ if (e.earlyDecision !== null) return;
4511
4436
  broadcast("activity-result", {
4512
4437
  id,
4513
4438
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
@@ -4548,18 +4473,31 @@ data: ${JSON.stringify(item.data)}
4548
4473
  if (!entry) return res.writeHead(404).end();
4549
4474
  if (entry.earlyDecision) {
4550
4475
  clearTimeout(entry.timer);
4476
+ const source = entry.decisionSource;
4551
4477
  pending.delete(id);
4552
4478
  res.writeHead(200, { "Content-Type": "application/json" });
4553
- const body = { decision: entry.earlyDecision };
4479
+ const body = {
4480
+ decision: entry.earlyDecision
4481
+ };
4554
4482
  if (entry.earlyReason) body.reason = entry.earlyReason;
4483
+ if (source) body.source = source;
4555
4484
  return res.end(JSON.stringify(body));
4556
4485
  }
4557
4486
  entry.waiter = (d, reason) => {
4558
4487
  res.writeHead(200, { "Content-Type": "application/json" });
4559
4488
  const body = { decision: d };
4560
4489
  if (reason) body.reason = reason;
4490
+ if (entry.decisionSource) body.source = entry.decisionSource;
4561
4491
  res.end(JSON.stringify(body));
4562
4492
  };
4493
+ req.on("close", () => {
4494
+ const e = pending.get(id);
4495
+ if (e && e.waiter && e.earlyDecision === null) {
4496
+ clearTimeout(e.timer);
4497
+ pending.delete(id);
4498
+ broadcast("remove", { id });
4499
+ }
4500
+ });
4563
4501
  return;
4564
4502
  }
4565
4503
  if (req.method === "POST" && pathname.startsWith("/decision/")) {
@@ -4568,7 +4506,13 @@ data: ${JSON.stringify(item.data)}
4568
4506
  const id = pathname.split("/").pop();
4569
4507
  const entry = pending.get(id);
4570
4508
  if (!entry) return res.writeHead(404).end();
4571
- const { decision, persist, trustDuration, reason } = JSON.parse(await readBody(req));
4509
+ if (entry.earlyDecision !== null) {
4510
+ res.writeHead(409, { "Content-Type": "application/json" });
4511
+ return res.end(JSON.stringify({ conflict: true, decision: entry.earlyDecision }));
4512
+ }
4513
+ const { decision, persist, trustDuration, reason, source } = JSON.parse(
4514
+ await readBody(req)
4515
+ );
4572
4516
  if (decision === "trust" && trustDuration) {
4573
4517
  const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
4574
4518
  writeTrustEntry(entry.toolName, ms);
@@ -4598,6 +4542,8 @@ data: ${JSON.stringify(item.data)}
4598
4542
  decision: resolvedDecision
4599
4543
  });
4600
4544
  clearTimeout(entry.timer);
4545
+ const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
4546
+ if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
4601
4547
  if (entry.waiter) {
4602
4548
  entry.waiter(resolvedDecision, reason);
4603
4549
  pending.delete(id);
@@ -4620,7 +4566,7 @@ data: ${JSON.stringify(item.data)}
4620
4566
  res.writeHead(200, { "Content-Type": "application/json" });
4621
4567
  return res.end(JSON.stringify({ ...s, autoStarted }));
4622
4568
  } catch (err) {
4623
- console.error(chalk4.red("[node9 daemon] GET /settings failed:"), err);
4569
+ console.error(chalk2.red("[node9 daemon] GET /settings failed:"), err);
4624
4570
  res.writeHead(500, { "Content-Type": "application/json" });
4625
4571
  return res.end(JSON.stringify({ error: "internal" }));
4626
4572
  }
@@ -4651,7 +4597,7 @@ data: ${JSON.stringify(item.data)}
4651
4597
  res.writeHead(200, { "Content-Type": "application/json" });
4652
4598
  return res.end(JSON.stringify({ hasKey: hasStoredSlackKey(), enabled: s.slackEnabled }));
4653
4599
  } catch (err) {
4654
- console.error(chalk4.red("[node9 daemon] GET /slack-status failed:"), err);
4600
+ console.error(chalk2.red("[node9 daemon] GET /slack-status failed:"), err);
4655
4601
  res.writeHead(500, { "Content-Type": "application/json" });
4656
4602
  return res.end(JSON.stringify({ error: "internal" }));
4657
4603
  }
@@ -4800,14 +4746,14 @@ data: ${JSON.stringify(item.data)}
4800
4746
  });
4801
4747
  return;
4802
4748
  }
4803
- console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4749
+ console.error(chalk2.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4804
4750
  process.exit(1);
4805
4751
  });
4806
4752
  if (!daemonRejectionHandlerRegistered) {
4807
4753
  daemonRejectionHandlerRegistered = true;
4808
4754
  process.on("unhandledRejection", (reason) => {
4809
4755
  const stack = reason instanceof Error ? reason.stack : String(reason);
4810
- console.error(chalk4.red("[node9 daemon] unhandled rejection \u2014 keeping daemon alive:"), stack);
4756
+ console.error(chalk2.red("[node9 daemon] unhandled rejection \u2014 keeping daemon alive:"), stack);
4811
4757
  });
4812
4758
  }
4813
4759
  server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
@@ -4816,10 +4762,10 @@ data: ${JSON.stringify(item.data)}
4816
4762
  JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
4817
4763
  { mode: 384 }
4818
4764
  );
4819
- console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4765
+ console.log(chalk2.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
4820
4766
  });
4821
4767
  if (watchMode) {
4822
- console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4768
+ console.log(chalk2.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
4823
4769
  }
4824
4770
  try {
4825
4771
  fs5.unlinkSync(ACTIVITY_SOCKET_PATH2);
@@ -4870,13 +4816,13 @@ data: ${JSON.stringify(item.data)}
4870
4816
  });
4871
4817
  }
4872
4818
  function stopDaemon() {
4873
- if (!fs5.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
4819
+ if (!fs5.existsSync(DAEMON_PID_FILE)) return console.log(chalk2.yellow("Not running."));
4874
4820
  try {
4875
4821
  const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
4876
4822
  process.kill(pid, "SIGTERM");
4877
- console.log(chalk4.green("\u2705 Stopped."));
4823
+ console.log(chalk2.green("\u2705 Stopped."));
4878
4824
  } catch {
4879
- console.log(chalk4.gray("Cleaned up stale PID file."));
4825
+ console.log(chalk2.gray("Cleaned up stale PID file."));
4880
4826
  } finally {
4881
4827
  try {
4882
4828
  fs5.unlinkSync(DAEMON_PID_FILE);
@@ -4889,10 +4835,10 @@ function daemonStatus() {
4889
4835
  try {
4890
4836
  const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
4891
4837
  process.kill(pid, 0);
4892
- console.log(chalk4.green("Node9 daemon: running"));
4838
+ console.log(chalk2.green("Node9 daemon: running"));
4893
4839
  return;
4894
4840
  } catch {
4895
- console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
4841
+ console.log(chalk2.yellow("Node9 daemon: not running (stale PID)"));
4896
4842
  return;
4897
4843
  }
4898
4844
  }
@@ -4901,9 +4847,9 @@ function daemonStatus() {
4901
4847
  timeout: 500
4902
4848
  });
4903
4849
  if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT2}`)) {
4904
- console.log(chalk4.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
4850
+ console.log(chalk2.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
4905
4851
  } else {
4906
- console.log(chalk4.yellow("Node9 daemon: not running"));
4852
+ console.log(chalk2.yellow("Node9 daemon: not running"));
4907
4853
  }
4908
4854
  }
4909
4855
  var 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;
@@ -4948,12 +4894,12 @@ __export(tail_exports, {
4948
4894
  startTail: () => startTail
4949
4895
  });
4950
4896
  import http2 from "http";
4951
- import chalk5 from "chalk";
4897
+ import chalk3 from "chalk";
4952
4898
  import fs7 from "fs";
4953
4899
  import os6 from "os";
4954
4900
  import path9 from "path";
4955
4901
  import readline from "readline";
4956
- import { spawn as spawn4 } from "child_process";
4902
+ import { spawn as spawn4, execSync } from "child_process";
4957
4903
  function getIcon(tool) {
4958
4904
  const t = tool.toLowerCase();
4959
4905
  for (const [k, v] of Object.entries(ICONS)) {
@@ -4967,17 +4913,17 @@ function formatBase(activity) {
4967
4913
  const toolName = activity.tool.slice(0, 16).padEnd(16);
4968
4914
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
4969
4915
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
4970
- return `${chalk5.gray(time)} ${icon} ${chalk5.white.bold(toolName)} ${chalk5.dim(argsPreview)}`;
4916
+ return `${chalk3.gray(time)} ${icon} ${chalk3.white.bold(toolName)} ${chalk3.dim(argsPreview)}`;
4971
4917
  }
4972
4918
  function renderResult(activity, result) {
4973
4919
  const base = formatBase(activity);
4974
4920
  let status;
4975
4921
  if (result.status === "allow") {
4976
- status = chalk5.green("\u2713 ALLOW");
4922
+ status = chalk3.green("\u2713 ALLOW");
4977
4923
  } else if (result.status === "dlp") {
4978
- status = chalk5.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4924
+ status = chalk3.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
4979
4925
  } else {
4980
- status = chalk5.red("\u2717 BLOCK");
4926
+ status = chalk3.red("\u2717 BLOCK");
4981
4927
  }
4982
4928
  if (process.stdout.isTTY) {
4983
4929
  readline.clearLine(process.stdout, 0);
@@ -4987,7 +4933,7 @@ function renderResult(activity, result) {
4987
4933
  }
4988
4934
  function renderPending(activity) {
4989
4935
  if (!process.stdout.isTTY) return;
4990
- process.stdout.write(`${formatBase(activity)} ${chalk5.yellow("\u25CF \u2026")}\r`);
4936
+ process.stdout.write(`${formatBase(activity)} ${chalk3.yellow("\u25CF \u2026")}\r`);
4991
4937
  }
4992
4938
  async function ensureDaemon() {
4993
4939
  let pidPort = null;
@@ -4996,7 +4942,7 @@ async function ensureDaemon() {
4996
4942
  const { port } = JSON.parse(fs7.readFileSync(PID_FILE, "utf-8"));
4997
4943
  pidPort = port;
4998
4944
  } catch {
4999
- console.error(chalk5.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
4945
+ console.error(chalk3.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
5000
4946
  }
5001
4947
  }
5002
4948
  const checkPort = pidPort ?? DAEMON_PORT2;
@@ -5007,7 +4953,7 @@ async function ensureDaemon() {
5007
4953
  if (res.ok) return checkPort;
5008
4954
  } catch {
5009
4955
  }
5010
- console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4956
+ console.log(chalk3.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
5011
4957
  const child = spawn4(process.execPath, [process.argv[1], "daemon"], {
5012
4958
  detached: true,
5013
4959
  stdio: "ignore",
@@ -5024,9 +4970,51 @@ async function ensureDaemon() {
5024
4970
  } catch {
5025
4971
  }
5026
4972
  }
5027
- console.error(chalk5.red("\u274C Daemon failed to start. Try: node9 daemon start"));
4973
+ console.error(chalk3.red("\u274C Daemon failed to start. Try: node9 daemon start"));
5028
4974
  process.exit(1);
5029
4975
  }
4976
+ function postDecisionHttp(id, decision, csrfToken, port) {
4977
+ return new Promise((resolve, reject) => {
4978
+ const body = JSON.stringify({ decision, source: "terminal" });
4979
+ const req = http2.request(
4980
+ {
4981
+ hostname: "127.0.0.1",
4982
+ port,
4983
+ path: `/decision/${id}`,
4984
+ method: "POST",
4985
+ headers: {
4986
+ "Content-Type": "application/json",
4987
+ "Content-Length": Buffer.byteLength(body),
4988
+ "X-Node9-Token": csrfToken
4989
+ }
4990
+ },
4991
+ (res) => {
4992
+ res.resume();
4993
+ if (res.statusCode === 200 || res.statusCode === 409) resolve();
4994
+ else reject(new Error(`POST /decision returned ${res.statusCode}`));
4995
+ }
4996
+ );
4997
+ req.on("error", reject);
4998
+ req.end(body);
4999
+ });
5000
+ }
5001
+ function buildCardLines(req) {
5002
+ const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
5003
+ const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
5004
+ 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`;
5005
+ const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
5006
+ return [
5007
+ ``,
5008
+ `${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
5009
+ `${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
5010
+ `${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
5011
+ `${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`,
5012
+ `${CYAN}\u255A${RESET}`,
5013
+ ``,
5014
+ ` ${BOLD}${GREEN}[A]${RESET} Allow ${BOLD}${RED}[D]${RESET} Deny`,
5015
+ ``
5016
+ ];
5017
+ }
5030
5018
  async function startTail(options = {}) {
5031
5019
  const port = await ensureDaemon();
5032
5020
  if (options.clear) {
@@ -5053,7 +5041,7 @@ async function startTail(options = {}) {
5053
5041
  req2.end();
5054
5042
  });
5055
5043
  if (result.ok) {
5056
- console.log(chalk5.green("\u2713 Flight Recorder buffer cleared."));
5044
+ console.log(chalk3.green("\u2713 Flight Recorder buffer cleared."));
5057
5045
  } else if (result.code === "ECONNREFUSED") {
5058
5046
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
5059
5047
  } else if (result.code === "ETIMEDOUT") {
@@ -5064,27 +5052,130 @@ async function startTail(options = {}) {
5064
5052
  return;
5065
5053
  }
5066
5054
  const connectionTime = Date.now();
5067
- const pending2 = /* @__PURE__ */ new Map();
5068
- console.log(chalk5.cyan.bold(`
5069
- \u{1F6F0}\uFE0F Node9 tail `) + chalk5.dim(`\u2192 localhost:${port}`));
5055
+ const activityPending = /* @__PURE__ */ new Map();
5056
+ let csrfToken = "";
5057
+ const approvalQueue = [];
5058
+ let cardActive = false;
5059
+ let cardLineCount = 0;
5060
+ let cancelActiveCard = null;
5061
+ const canApprove = process.stdout.isTTY && process.stdin.isTTY;
5062
+ if (canApprove) readline.emitKeypressEvents(process.stdin);
5063
+ function clearCard() {
5064
+ if (cardLineCount > 0) {
5065
+ process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
5066
+ cardLineCount = 0;
5067
+ }
5068
+ }
5069
+ function printCard(req2) {
5070
+ process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
5071
+ const lines = buildCardLines(req2);
5072
+ for (const line of lines) process.stdout.write(line + "\n");
5073
+ cardLineCount = lines.length;
5074
+ }
5075
+ function showNextCard() {
5076
+ if (cardActive || approvalQueue.length === 0 || !canApprove) return;
5077
+ try {
5078
+ process.stdin.setRawMode(true);
5079
+ } catch {
5080
+ cardActive = false;
5081
+ return;
5082
+ }
5083
+ cardActive = true;
5084
+ const req2 = approvalQueue[0];
5085
+ printCard(req2);
5086
+ let settled = false;
5087
+ let onKeypress = null;
5088
+ const cleanup = () => {
5089
+ const handler = onKeypress;
5090
+ onKeypress = null;
5091
+ if (handler) process.stdin.removeListener("keypress", handler);
5092
+ try {
5093
+ process.stdin.setRawMode(false);
5094
+ } catch {
5095
+ }
5096
+ process.stdin.pause();
5097
+ cancelActiveCard = null;
5098
+ };
5099
+ const settle = (decision) => {
5100
+ if (settled) return;
5101
+ settled = true;
5102
+ cleanup();
5103
+ clearCard();
5104
+ process.stdout.write(SHOW_CURSOR);
5105
+ postDecisionHttp(req2.id, decision, csrfToken, port).catch((err) => {
5106
+ try {
5107
+ fs7.appendFileSync(
5108
+ path9.join(os6.homedir(), ".node9", "hook-debug.log"),
5109
+ `[tail] POST /decision failed: ${String(err)}
5110
+ `
5111
+ );
5112
+ } catch {
5113
+ }
5114
+ });
5115
+ const decisionLabel = decision === "allow" ? chalk3.green("\u2713 ALLOWED (terminal)") : chalk3.red("\u2717 DENIED (terminal)");
5116
+ console.log(`${chalk3.cyan("\u25C6")} ${chalk3.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
5117
+ approvalQueue.shift();
5118
+ cardActive = false;
5119
+ showNextCard();
5120
+ };
5121
+ cancelActiveCard = () => {
5122
+ if (settled) return;
5123
+ settled = true;
5124
+ cleanup();
5125
+ clearCard();
5126
+ process.stdout.write(SHOW_CURSOR);
5127
+ approvalQueue.shift();
5128
+ cardActive = false;
5129
+ showNextCard();
5130
+ };
5131
+ process.stdin.resume();
5132
+ onKeypress = (_str, key) => {
5133
+ const name = key?.name ?? "";
5134
+ if (name === "a") {
5135
+ settle("allow");
5136
+ } else if (name === "d" || name === "return" || name === "enter" || key?.ctrl && name === "c") {
5137
+ settle("deny");
5138
+ }
5139
+ };
5140
+ process.stdin.on("keypress", onKeypress);
5141
+ }
5142
+ const dashboardUrl = `http://127.0.0.1:${port}/`;
5143
+ try {
5144
+ const browserEnabled = getConfig().settings.approvers?.browser !== false;
5145
+ if (browserEnabled) {
5146
+ if (process.platform === "darwin") execSync(`open "${dashboardUrl}"`, { stdio: "ignore" });
5147
+ else if (process.platform === "win32")
5148
+ execSync(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
5149
+ else execSync(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
5150
+ }
5151
+ } catch {
5152
+ }
5153
+ console.log(chalk3.cyan.bold(`
5154
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk3.dim(`\u2192 ${dashboardUrl}`));
5155
+ if (canApprove) {
5156
+ console.log(chalk3.dim("Interactive approvals enabled. [A] Allow [D] Deny"));
5157
+ }
5070
5158
  if (options.history) {
5071
- console.log(chalk5.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
5159
+ console.log(chalk3.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
5072
5160
  } else {
5073
5161
  console.log(
5074
- chalk5.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
5162
+ chalk3.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
5075
5163
  );
5076
5164
  }
5077
5165
  process.on("SIGINT", () => {
5166
+ clearCard();
5167
+ process.stdout.write(SHOW_CURSOR);
5078
5168
  if (process.stdout.isTTY) {
5079
5169
  readline.clearLine(process.stdout, 0);
5080
5170
  readline.cursorTo(process.stdout, 0);
5081
5171
  }
5082
- console.log(chalk5.dim("\n\u{1F6F0}\uFE0F Disconnected."));
5172
+ console.log(chalk3.dim("\n\u{1F6F0}\uFE0F Disconnected."));
5083
5173
  process.exit(0);
5084
5174
  });
5085
- const req = http2.get(`http://127.0.0.1:${port}/events`, (res) => {
5175
+ const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
5176
+ const req = http2.get(sseUrl, (res) => {
5086
5177
  if (res.statusCode !== 200) {
5087
- console.error(chalk5.red(`Failed to connect: HTTP ${res.statusCode}`));
5178
+ console.error(chalk3.red(`Failed to connect: HTTP ${res.statusCode}`));
5088
5179
  process.exit(1);
5089
5180
  }
5090
5181
  let currentEvent = "";
@@ -5108,15 +5199,66 @@ async function startTail(options = {}) {
5108
5199
  }
5109
5200
  });
5110
5201
  rl.on("close", () => {
5202
+ clearCard();
5203
+ process.stdout.write(SHOW_CURSOR);
5111
5204
  if (process.stdout.isTTY) {
5112
5205
  readline.clearLine(process.stdout, 0);
5113
5206
  readline.cursorTo(process.stdout, 0);
5114
5207
  }
5115
- console.log(chalk5.red("\n\u274C Daemon disconnected."));
5208
+ console.log(chalk3.red("\n\u274C Daemon disconnected."));
5116
5209
  process.exit(1);
5117
5210
  });
5118
5211
  });
5119
5212
  function handleMessage(event, rawData) {
5213
+ if (event === "csrf") {
5214
+ try {
5215
+ const parsed = JSON.parse(rawData);
5216
+ if (parsed.token) csrfToken = parsed.token;
5217
+ } catch {
5218
+ }
5219
+ return;
5220
+ }
5221
+ if (event === "init") {
5222
+ try {
5223
+ const parsed = JSON.parse(rawData);
5224
+ if (canApprove && Array.isArray(parsed.requests)) {
5225
+ for (const r of parsed.requests) {
5226
+ approvalQueue.push(r);
5227
+ }
5228
+ showNextCard();
5229
+ }
5230
+ } catch {
5231
+ }
5232
+ return;
5233
+ }
5234
+ if (event === "add") {
5235
+ if (canApprove) {
5236
+ try {
5237
+ const parsed = JSON.parse(rawData);
5238
+ if (parsed.interactive !== false) {
5239
+ approvalQueue.push(parsed);
5240
+ showNextCard();
5241
+ }
5242
+ } catch {
5243
+ }
5244
+ }
5245
+ return;
5246
+ }
5247
+ if (event === "remove") {
5248
+ try {
5249
+ const { id } = JSON.parse(rawData);
5250
+ const idx = approvalQueue.findIndex((r) => r.id === id);
5251
+ if (idx !== -1) {
5252
+ if (idx === 0 && cardActive && cancelActiveCard) {
5253
+ cancelActiveCard();
5254
+ } else {
5255
+ approvalQueue.splice(idx, 1);
5256
+ }
5257
+ }
5258
+ } catch {
5259
+ }
5260
+ return;
5261
+ }
5120
5262
  let data;
5121
5263
  try {
5122
5264
  data = JSON.parse(rawData);
@@ -5129,30 +5271,31 @@ async function startTail(options = {}) {
5129
5271
  renderResult(data, data);
5130
5272
  return;
5131
5273
  }
5132
- pending2.set(data.id, data);
5274
+ activityPending.set(data.id, data);
5133
5275
  const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
5134
5276
  if (slowTool) renderPending(data);
5135
5277
  }
5136
5278
  if (event === "activity-result") {
5137
- const original = pending2.get(data.id);
5279
+ const original = activityPending.get(data.id);
5138
5280
  if (original) {
5139
5281
  renderResult(original, data);
5140
- pending2.delete(data.id);
5282
+ activityPending.delete(data.id);
5141
5283
  }
5142
5284
  }
5143
5285
  }
5144
5286
  req.on("error", (err) => {
5145
5287
  const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
5146
- console.error(chalk5.red(`
5288
+ console.error(chalk3.red(`
5147
5289
  \u274C ${msg}`));
5148
5290
  process.exit(1);
5149
5291
  });
5150
5292
  }
5151
- var PID_FILE, ICONS;
5293
+ var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
5152
5294
  var init_tail = __esm({
5153
5295
  "src/tui/tail.ts"() {
5154
5296
  "use strict";
5155
5297
  init_daemon();
5298
+ init_core();
5156
5299
  PID_FILE = path9.join(os6.homedir(), ".node9", "daemon.pid");
5157
5300
  ICONS = {
5158
5301
  bash: "\u{1F4BB}",
@@ -5171,6 +5314,18 @@ var init_tail = __esm({
5171
5314
  delete: "\u{1F5D1}\uFE0F",
5172
5315
  web: "\u{1F310}"
5173
5316
  };
5317
+ RESET = "\x1B[0m";
5318
+ BOLD = "\x1B[1m";
5319
+ RED = "\x1B[31m";
5320
+ YELLOW = "\x1B[33m";
5321
+ CYAN = "\x1B[36m";
5322
+ GRAY = "\x1B[90m";
5323
+ GREEN = "\x1B[32m";
5324
+ HIDE_CURSOR = "\x1B[?25l";
5325
+ SHOW_CURSOR = "\x1B[?25h";
5326
+ ERASE_DOWN = "\x1B[J";
5327
+ SAVE_CURSOR = "\x1B7";
5328
+ RESTORE_CURSOR = "\x1B8";
5174
5329
  }
5175
5330
  });
5176
5331
 
@@ -5182,11 +5337,11 @@ import { Command } from "commander";
5182
5337
  import fs4 from "fs";
5183
5338
  import path6 from "path";
5184
5339
  import os3 from "os";
5185
- import chalk3 from "chalk";
5186
- import { confirm as confirm2 } from "@inquirer/prompts";
5340
+ import chalk from "chalk";
5341
+ import { confirm } from "@inquirer/prompts";
5187
5342
  function printDaemonTip() {
5188
5343
  console.log(
5189
- chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
5344
+ chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk.white("\n To view your history or manage persistent rules, run:") + chalk.green("\n node9 daemon --openui")
5190
5345
  );
5191
5346
  }
5192
5347
  function fullPathCommand(subcommand) {
@@ -5232,10 +5387,10 @@ function teardownClaude() {
5232
5387
  if (changed) {
5233
5388
  writeJson(hooksPath, settings);
5234
5389
  console.log(
5235
- chalk3.green(" \u2705 Removed PreToolUse / PostToolUse hooks from ~/.claude/settings.json")
5390
+ chalk.green(" \u2705 Removed PreToolUse / PostToolUse hooks from ~/.claude/settings.json")
5236
5391
  );
5237
5392
  } else {
5238
- console.log(chalk3.blue(" \u2139\uFE0F No Node9 hooks found in ~/.claude/settings.json"));
5393
+ console.log(chalk.blue(" \u2139\uFE0F No Node9 hooks found in ~/.claude/settings.json"));
5239
5394
  }
5240
5395
  }
5241
5396
  const claudeConfig = readJson(mcpPath);
@@ -5252,7 +5407,7 @@ function teardownClaude() {
5252
5407
  mcpChanged = true;
5253
5408
  } else if (server.command === "node9") {
5254
5409
  console.warn(
5255
- chalk3.yellow(
5410
+ chalk.yellow(
5256
5411
  ` \u26A0\uFE0F Cannot unwrap MCP server "${name}" in ~/.claude.json \u2014 args is empty. Remove it manually.`
5257
5412
  )
5258
5413
  );
@@ -5260,7 +5415,7 @@ function teardownClaude() {
5260
5415
  }
5261
5416
  if (mcpChanged) {
5262
5417
  writeJson(mcpPath, claudeConfig);
5263
- console.log(chalk3.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
5418
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
5264
5419
  }
5265
5420
  }
5266
5421
  }
@@ -5269,7 +5424,7 @@ function teardownGemini() {
5269
5424
  const settingsPath = path6.join(homeDir2, ".gemini", "settings.json");
5270
5425
  const settings = readJson(settingsPath);
5271
5426
  if (!settings) {
5272
- console.log(chalk3.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
5427
+ console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
5273
5428
  return;
5274
5429
  }
5275
5430
  let changed = false;
@@ -5298,9 +5453,9 @@ function teardownGemini() {
5298
5453
  }
5299
5454
  if (changed) {
5300
5455
  writeJson(settingsPath, settings);
5301
- console.log(chalk3.green(" \u2705 Removed Node9 hooks from ~/.gemini/settings.json"));
5456
+ console.log(chalk.green(" \u2705 Removed Node9 hooks from ~/.gemini/settings.json"));
5302
5457
  } else {
5303
- console.log(chalk3.blue(" \u2139\uFE0F No Node9 hooks found in ~/.gemini/settings.json"));
5458
+ console.log(chalk.blue(" \u2139\uFE0F No Node9 hooks found in ~/.gemini/settings.json"));
5304
5459
  }
5305
5460
  }
5306
5461
  function teardownCursor() {
@@ -5308,7 +5463,7 @@ function teardownCursor() {
5308
5463
  const mcpPath = path6.join(homeDir2, ".cursor", "mcp.json");
5309
5464
  const mcpConfig = readJson(mcpPath);
5310
5465
  if (!mcpConfig?.mcpServers) {
5311
- console.log(chalk3.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
5466
+ console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
5312
5467
  return;
5313
5468
  }
5314
5469
  let changed = false;
@@ -5325,9 +5480,9 @@ function teardownCursor() {
5325
5480
  }
5326
5481
  if (changed) {
5327
5482
  writeJson(mcpPath, mcpConfig);
5328
- console.log(chalk3.green(" \u2705 Unwrapped MCP servers in ~/.cursor/mcp.json"));
5483
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.cursor/mcp.json"));
5329
5484
  } else {
5330
- console.log(chalk3.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.cursor/mcp.json"));
5485
+ console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.cursor/mcp.json"));
5331
5486
  }
5332
5487
  }
5333
5488
  async function setupClaude() {
@@ -5348,7 +5503,7 @@ async function setupClaude() {
5348
5503
  matcher: ".*",
5349
5504
  hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
5350
5505
  });
5351
- console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
5506
+ console.log(chalk.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
5352
5507
  anythingChanged = true;
5353
5508
  }
5354
5509
  const hasPostHook = settings.hooks.PostToolUse?.some(
@@ -5360,7 +5515,7 @@ async function setupClaude() {
5360
5515
  matcher: ".*",
5361
5516
  hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
5362
5517
  });
5363
- console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
5518
+ console.log(chalk.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
5364
5519
  anythingChanged = true;
5365
5520
  }
5366
5521
  if (anythingChanged) {
@@ -5374,35 +5529,35 @@ async function setupClaude() {
5374
5529
  serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5375
5530
  }
5376
5531
  if (serversToWrap.length > 0) {
5377
- console.log(chalk3.bold("The following existing entries will be modified:\n"));
5378
- console.log(chalk3.white(` ${mcpPath}`));
5532
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
5533
+ console.log(chalk.white(` ${mcpPath}`));
5379
5534
  for (const { name, originalCmd } of serversToWrap) {
5380
- console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5535
+ console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5381
5536
  }
5382
5537
  console.log("");
5383
- const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
5538
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
5384
5539
  if (proceed) {
5385
5540
  for (const { name, parts } of serversToWrap) {
5386
5541
  servers[name] = { ...servers[name], command: "node9", args: parts };
5387
5542
  }
5388
5543
  claudeConfig.mcpServers = servers;
5389
5544
  writeJson(mcpPath, claudeConfig);
5390
- console.log(chalk3.green(`
5545
+ console.log(chalk.green(`
5391
5546
  \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5392
5547
  anythingChanged = true;
5393
5548
  } else {
5394
- console.log(chalk3.yellow(" Skipped MCP server wrapping."));
5549
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
5395
5550
  }
5396
5551
  console.log("");
5397
5552
  }
5398
5553
  if (!anythingChanged && serversToWrap.length === 0) {
5399
- console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
5554
+ console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
5400
5555
  printDaemonTip();
5401
5556
  return;
5402
5557
  }
5403
5558
  if (anythingChanged) {
5404
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
5405
- console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
5559
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
5560
+ console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
5406
5561
  printDaemonTip();
5407
5562
  }
5408
5563
  }
@@ -5430,7 +5585,7 @@ async function setupGemini() {
5430
5585
  }
5431
5586
  ]
5432
5587
  });
5433
- console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
5588
+ console.log(chalk.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
5434
5589
  anythingChanged = true;
5435
5590
  }
5436
5591
  const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
@@ -5443,7 +5598,7 @@ async function setupGemini() {
5443
5598
  matcher: ".*",
5444
5599
  hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
5445
5600
  });
5446
- console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
5601
+ console.log(chalk.green(" \u2705 AfterTool hook added \u2192 node9 log"));
5447
5602
  anythingChanged = true;
5448
5603
  }
5449
5604
  if (anythingChanged) {
@@ -5457,35 +5612,35 @@ async function setupGemini() {
5457
5612
  serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5458
5613
  }
5459
5614
  if (serversToWrap.length > 0) {
5460
- console.log(chalk3.bold("The following existing entries will be modified:\n"));
5461
- console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
5615
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
5616
+ console.log(chalk.white(` ${settingsPath} (mcpServers)`));
5462
5617
  for (const { name, originalCmd } of serversToWrap) {
5463
- console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5618
+ console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5464
5619
  }
5465
5620
  console.log("");
5466
- const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
5621
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
5467
5622
  if (proceed) {
5468
5623
  for (const { name, parts } of serversToWrap) {
5469
5624
  servers[name] = { ...servers[name], command: "node9", args: parts };
5470
5625
  }
5471
5626
  settings.mcpServers = servers;
5472
5627
  writeJson(settingsPath, settings);
5473
- console.log(chalk3.green(`
5628
+ console.log(chalk.green(`
5474
5629
  \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5475
5630
  anythingChanged = true;
5476
5631
  } else {
5477
- console.log(chalk3.yellow(" Skipped MCP server wrapping."));
5632
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
5478
5633
  }
5479
5634
  console.log("");
5480
5635
  }
5481
5636
  if (!anythingChanged && serversToWrap.length === 0) {
5482
- console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
5637
+ console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
5483
5638
  printDaemonTip();
5484
5639
  return;
5485
5640
  }
5486
5641
  if (anythingChanged) {
5487
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
5488
- console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
5642
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
5643
+ console.log(chalk.gray(" Restart Gemini CLI for changes to take effect."));
5489
5644
  printDaemonTip();
5490
5645
  }
5491
5646
  }
@@ -5502,36 +5657,36 @@ async function setupCursor() {
5502
5657
  serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
5503
5658
  }
5504
5659
  if (serversToWrap.length > 0) {
5505
- console.log(chalk3.bold("The following existing entries will be modified:\n"));
5506
- console.log(chalk3.white(` ${mcpPath}`));
5660
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
5661
+ console.log(chalk.white(` ${mcpPath}`));
5507
5662
  for (const { name, originalCmd } of serversToWrap) {
5508
- console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5663
+ console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
5509
5664
  }
5510
5665
  console.log("");
5511
- const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
5666
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
5512
5667
  if (proceed) {
5513
5668
  for (const { name, parts } of serversToWrap) {
5514
5669
  servers[name] = { ...servers[name], command: "node9", args: parts };
5515
5670
  }
5516
5671
  mcpConfig.mcpServers = servers;
5517
5672
  writeJson(mcpPath, mcpConfig);
5518
- console.log(chalk3.green(`
5673
+ console.log(chalk.green(`
5519
5674
  \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
5520
5675
  anythingChanged = true;
5521
5676
  } else {
5522
- console.log(chalk3.yellow(" Skipped MCP server wrapping."));
5677
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
5523
5678
  }
5524
5679
  console.log("");
5525
5680
  }
5526
5681
  console.log(
5527
- chalk3.yellow(
5682
+ chalk.yellow(
5528
5683
  " \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
5529
5684
  )
5530
5685
  );
5531
5686
  console.log("");
5532
5687
  if (!anythingChanged && serversToWrap.length === 0) {
5533
5688
  console.log(
5534
- chalk3.blue(
5689
+ chalk.blue(
5535
5690
  "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
5536
5691
  )
5537
5692
  );
@@ -5539,18 +5694,18 @@ async function setupCursor() {
5539
5694
  return;
5540
5695
  }
5541
5696
  if (anythingChanged) {
5542
- console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5543
- console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
5697
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
5698
+ console.log(chalk.gray(" Restart Cursor for changes to take effect."));
5544
5699
  printDaemonTip();
5545
5700
  }
5546
5701
  }
5547
5702
 
5548
5703
  // src/cli.ts
5549
5704
  init_daemon();
5550
- import { spawn as spawn5, execSync } from "child_process";
5705
+ import { spawn as spawn5, execSync as execSync2, spawnSync as spawnSync4 } from "child_process";
5551
5706
  import { parseCommandString } from "execa";
5552
5707
  import { execa } from "execa";
5553
- import chalk6 from "chalk";
5708
+ import chalk4 from "chalk";
5554
5709
  import readline2 from "readline";
5555
5710
  import fs8 from "fs";
5556
5711
  import path10 from "path";
@@ -5817,7 +5972,7 @@ function applyUndo(hash, cwd) {
5817
5972
 
5818
5973
  // src/cli.ts
5819
5974
  init_shields();
5820
- import { confirm as confirm3 } from "@inquirer/prompts";
5975
+ import { confirm as confirm2 } from "@inquirer/prompts";
5821
5976
  var { version } = JSON.parse(
5822
5977
  fs8.readFileSync(path10.join(__dirname, "../package.json"), "utf-8")
5823
5978
  );
@@ -5907,9 +6062,9 @@ function openBrowserLocal() {
5907
6062
  const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
5908
6063
  try {
5909
6064
  const opts = { stdio: "ignore" };
5910
- if (process.platform === "darwin") execSync(`open "${url}"`, opts);
5911
- else if (process.platform === "win32") execSync(`cmd /c start "" "${url}"`, opts);
5912
- else execSync(`xdg-open "${url}"`, opts);
6065
+ if (process.platform === "darwin") execSync2(`open "${url}"`, opts);
6066
+ else if (process.platform === "win32") execSync2(`cmd /c start "" "${url}"`, opts);
6067
+ else execSync2(`xdg-open "${url}"`, opts);
5913
6068
  } catch {
5914
6069
  }
5915
6070
  }
@@ -5918,7 +6073,9 @@ async function autoStartDaemonAndWait() {
5918
6073
  const child = spawn5(process.execPath, [process.argv[1], "daemon"], {
5919
6074
  detached: true,
5920
6075
  stdio: "ignore",
5921
- env: { ...process.env, NODE9_AUTO_STARTED: "1" }
6076
+ // NODE9_BROWSER_OPENED=1 tells the daemon we will open the browser ourselves
6077
+ // (openBrowserLocal below), so it must not open a duplicate tab on first approval.
6078
+ env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
5922
6079
  });
5923
6080
  child.unref();
5924
6081
  for (let i = 0; i < 20; i++) {
@@ -5951,7 +6108,7 @@ async function runProxy(targetCommand) {
5951
6108
  if (stdout) executable = stdout.trim();
5952
6109
  } catch {
5953
6110
  }
5954
- console.error(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
6111
+ console.error(chalk4.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5955
6112
  const child = spawn5(executable, args, {
5956
6113
  stdio: ["pipe", "pipe", "inherit"],
5957
6114
  // We control STDIN and STDOUT
@@ -5972,14 +6129,14 @@ async function runProxy(targetCommand) {
5972
6129
  try {
5973
6130
  const name = message.params?.name || message.params?.tool_name || "unknown";
5974
6131
  const toolArgs = message.params?.arguments || message.params?.tool_input || {};
5975
- const result = await authorizeHeadless(sanitize(name), toolArgs, true, {
6132
+ const result = await authorizeHeadless(sanitize(name), toolArgs, {
5976
6133
  agent: "Proxy/MCP"
5977
6134
  });
5978
6135
  if (!result.approved) {
5979
- console.error(chalk6.red(`
6136
+ console.error(chalk4.red(`
5980
6137
  \u{1F6D1} Node9 Sudo: Action Blocked`));
5981
- console.error(chalk6.gray(` Tool: ${name}`));
5982
- console.error(chalk6.gray(` Reason: ${result.reason || "Security Policy"}
6138
+ console.error(chalk4.gray(` Tool: ${name}`));
6139
+ console.error(chalk4.gray(` Reason: ${result.reason || "Security Policy"}
5983
6140
  `));
5984
6141
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
5985
6142
  const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
@@ -6066,31 +6223,31 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
6066
6223
  fs8.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
6067
6224
  }
6068
6225
  if (options.profile && profileName !== "default") {
6069
- console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
6070
- console.log(chalk6.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
6226
+ console.log(chalk4.green(`\u2705 Profile "${profileName}" saved`));
6227
+ console.log(chalk4.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
6071
6228
  } else if (options.local) {
6072
- console.log(chalk6.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
6073
- console.log(chalk6.gray(` All decisions stay on this machine.`));
6229
+ console.log(chalk4.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
6230
+ console.log(chalk4.gray(` All decisions stay on this machine.`));
6074
6231
  } else {
6075
- console.log(chalk6.green(`\u2705 Logged in \u2014 agent mode`));
6076
- console.log(chalk6.gray(` Team policy enforced for all calls via Node9 cloud.`));
6232
+ console.log(chalk4.green(`\u2705 Logged in \u2014 agent mode`));
6233
+ console.log(chalk4.gray(` Team policy enforced for all calls via Node9 cloud.`));
6077
6234
  }
6078
6235
  });
6079
6236
  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) => {
6080
6237
  if (target === "gemini") return await setupGemini();
6081
6238
  if (target === "claude") return await setupClaude();
6082
6239
  if (target === "cursor") return await setupCursor();
6083
- console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6240
+ console.error(chalk4.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6084
6241
  process.exit(1);
6085
6242
  });
6086
6243
  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) => {
6087
6244
  if (!target) {
6088
- console.log(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
6089
- console.log(" Usage: " + chalk6.white("node9 setup <target>") + "\n");
6245
+ console.log(chalk4.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
6246
+ console.log(" Usage: " + chalk4.white("node9 setup <target>") + "\n");
6090
6247
  console.log(" Targets:");
6091
- console.log(" " + chalk6.green("claude") + " \u2014 Claude Code (hook mode)");
6092
- console.log(" " + chalk6.green("gemini") + " \u2014 Gemini CLI (hook mode)");
6093
- console.log(" " + chalk6.green("cursor") + " \u2014 Cursor (hook mode)");
6248
+ console.log(" " + chalk4.green("claude") + " \u2014 Claude Code (hook mode)");
6249
+ console.log(" " + chalk4.green("gemini") + " \u2014 Gemini CLI (hook mode)");
6250
+ console.log(" " + chalk4.green("cursor") + " \u2014 Cursor (hook mode)");
6094
6251
  console.log("");
6095
6252
  return;
6096
6253
  }
@@ -6098,7 +6255,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
6098
6255
  if (t === "gemini") return await setupGemini();
6099
6256
  if (t === "claude") return await setupClaude();
6100
6257
  if (t === "cursor") return await setupCursor();
6101
- console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6258
+ console.error(chalk4.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6102
6259
  process.exit(1);
6103
6260
  });
6104
6261
  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) => {
@@ -6107,30 +6264,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
6107
6264
  else if (target === "gemini") fn = teardownGemini;
6108
6265
  else if (target === "cursor") fn = teardownCursor;
6109
6266
  else {
6110
- console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6267
+ console.error(chalk4.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
6111
6268
  process.exit(1);
6112
6269
  }
6113
- console.log(chalk6.cyan(`
6270
+ console.log(chalk4.cyan(`
6114
6271
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
6115
6272
  `));
6116
6273
  try {
6117
6274
  fn();
6118
6275
  } catch (err) {
6119
- console.error(chalk6.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
6276
+ console.error(chalk4.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
6120
6277
  process.exit(1);
6121
6278
  }
6122
- console.log(chalk6.gray("\n Restart the agent for changes to take effect."));
6279
+ console.log(chalk4.gray("\n Restart the agent for changes to take effect."));
6123
6280
  });
6124
6281
  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) => {
6125
- console.log(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
6126
- console.log(chalk6.bold("Stopping daemon..."));
6282
+ console.log(chalk4.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
6283
+ console.log(chalk4.bold("Stopping daemon..."));
6127
6284
  try {
6128
6285
  stopDaemon();
6129
- console.log(chalk6.green(" \u2705 Daemon stopped"));
6286
+ console.log(chalk4.green(" \u2705 Daemon stopped"));
6130
6287
  } catch {
6131
- console.log(chalk6.blue(" \u2139\uFE0F Daemon was not running"));
6288
+ console.log(chalk4.blue(" \u2139\uFE0F Daemon was not running"));
6132
6289
  }
6133
- console.log(chalk6.bold("\nRemoving hooks..."));
6290
+ console.log(chalk4.bold("\nRemoving hooks..."));
6134
6291
  let teardownFailed = false;
6135
6292
  for (const [label, fn] of [
6136
6293
  ["Claude", teardownClaude],
@@ -6142,7 +6299,7 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
6142
6299
  } catch (err) {
6143
6300
  teardownFailed = true;
6144
6301
  console.error(
6145
- chalk6.red(
6302
+ chalk4.red(
6146
6303
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
6147
6304
  )
6148
6305
  );
@@ -6151,7 +6308,7 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
6151
6308
  if (options.purge) {
6152
6309
  const node9Dir = path10.join(os7.homedir(), ".node9");
6153
6310
  if (fs8.existsSync(node9Dir)) {
6154
- const confirmed = await confirm3({
6311
+ const confirmed = await confirm2({
6155
6312
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
6156
6313
  default: false
6157
6314
  });
@@ -6159,53 +6316,53 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
6159
6316
  fs8.rmSync(node9Dir, { recursive: true });
6160
6317
  if (fs8.existsSync(node9Dir)) {
6161
6318
  console.error(
6162
- chalk6.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
6319
+ chalk4.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
6163
6320
  );
6164
6321
  } else {
6165
- console.log(chalk6.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
6322
+ console.log(chalk4.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
6166
6323
  }
6167
6324
  } else {
6168
- console.log(chalk6.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
6325
+ console.log(chalk4.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
6169
6326
  }
6170
6327
  } else {
6171
- console.log(chalk6.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
6328
+ console.log(chalk4.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
6172
6329
  }
6173
6330
  } else {
6174
6331
  console.log(
6175
- chalk6.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
6332
+ chalk4.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
6176
6333
  );
6177
6334
  }
6178
6335
  if (teardownFailed) {
6179
- console.error(chalk6.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
6336
+ console.error(chalk4.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
6180
6337
  process.exit(1);
6181
6338
  }
6182
- console.log(chalk6.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
6183
- console.log(chalk6.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
6339
+ console.log(chalk4.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
6340
+ console.log(chalk4.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
6184
6341
  });
6185
6342
  program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
6186
6343
  const homeDir2 = os7.homedir();
6187
6344
  let failures = 0;
6188
6345
  function pass(msg) {
6189
- console.log(chalk6.green(" \u2705 ") + msg);
6346
+ console.log(chalk4.green(" \u2705 ") + msg);
6190
6347
  }
6191
6348
  function fail(msg, hint) {
6192
- console.log(chalk6.red(" \u274C ") + msg);
6193
- if (hint) console.log(chalk6.gray(" " + hint));
6349
+ console.log(chalk4.red(" \u274C ") + msg);
6350
+ if (hint) console.log(chalk4.gray(" " + hint));
6194
6351
  failures++;
6195
6352
  }
6196
6353
  function warn(msg, hint) {
6197
- console.log(chalk6.yellow(" \u26A0\uFE0F ") + msg);
6198
- if (hint) console.log(chalk6.gray(" " + hint));
6354
+ console.log(chalk4.yellow(" \u26A0\uFE0F ") + msg);
6355
+ if (hint) console.log(chalk4.gray(" " + hint));
6199
6356
  }
6200
6357
  function section(title) {
6201
- console.log("\n" + chalk6.bold(title));
6358
+ console.log("\n" + chalk4.bold(title));
6202
6359
  }
6203
- console.log(chalk6.cyan.bold(`
6360
+ console.log(chalk4.cyan.bold(`
6204
6361
  \u{1F6E1}\uFE0F Node9 Doctor v${version}
6205
6362
  `));
6206
6363
  section("Binary");
6207
6364
  try {
6208
- const which = execSync("which node9", { encoding: "utf-8" }).trim();
6365
+ const which = execSync2("which node9", { encoding: "utf-8" }).trim();
6209
6366
  pass(`node9 found at ${which}`);
6210
6367
  } catch {
6211
6368
  warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g @node9/proxy");
@@ -6220,7 +6377,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
6220
6377
  );
6221
6378
  }
6222
6379
  try {
6223
- const gitVersion = execSync("git --version", { encoding: "utf-8" }).trim();
6380
+ const gitVersion = execSync2("git --version", { encoding: "utf-8" }).trim();
6224
6381
  pass(gitVersion);
6225
6382
  } catch {
6226
6383
  warn(
@@ -6315,9 +6472,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
6315
6472
  }
6316
6473
  console.log("");
6317
6474
  if (failures === 0) {
6318
- console.log(chalk6.green.bold(" All checks passed. Node9 is ready.\n"));
6475
+ console.log(chalk4.green.bold(" All checks passed. Node9 is ready.\n"));
6319
6476
  } else {
6320
- console.log(chalk6.red.bold(` ${failures} check(s) failed. See hints above.
6477
+ console.log(chalk4.red.bold(` ${failures} check(s) failed. See hints above.
6321
6478
  `));
6322
6479
  process.exit(1);
6323
6480
  }
@@ -6332,7 +6489,7 @@ program.command("explain").description(
6332
6489
  try {
6333
6490
  args = JSON.parse(trimmed);
6334
6491
  } catch {
6335
- console.error(chalk6.red(`
6492
+ console.error(chalk4.red(`
6336
6493
  \u274C Invalid JSON: ${trimmed}
6337
6494
  `));
6338
6495
  process.exit(1);
@@ -6343,54 +6500,54 @@ program.command("explain").description(
6343
6500
  }
6344
6501
  const result = await explainPolicy(tool, args);
6345
6502
  console.log("");
6346
- console.log(chalk6.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
6503
+ console.log(chalk4.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
6347
6504
  console.log("");
6348
- console.log(` ${chalk6.bold("Tool:")} ${chalk6.white(result.tool)}`);
6505
+ console.log(` ${chalk4.bold("Tool:")} ${chalk4.white(result.tool)}`);
6349
6506
  if (argsRaw) {
6350
6507
  const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
6351
- console.log(` ${chalk6.bold("Input:")} ${chalk6.gray(preview)}`);
6508
+ console.log(` ${chalk4.bold("Input:")} ${chalk4.gray(preview)}`);
6352
6509
  }
6353
6510
  console.log("");
6354
- console.log(chalk6.bold("Config Sources (Waterfall):"));
6511
+ console.log(chalk4.bold("Config Sources (Waterfall):"));
6355
6512
  for (const tier of result.waterfall) {
6356
- const num = chalk6.gray(` ${tier.tier}.`);
6513
+ const num = chalk4.gray(` ${tier.tier}.`);
6357
6514
  const label = tier.label.padEnd(16);
6358
6515
  let statusStr;
6359
6516
  if (tier.tier === 1) {
6360
- statusStr = chalk6.gray(tier.note ?? "");
6517
+ statusStr = chalk4.gray(tier.note ?? "");
6361
6518
  } else if (tier.status === "active") {
6362
- const loc = tier.path ? chalk6.gray(tier.path) : "";
6363
- const note = tier.note ? chalk6.gray(`(${tier.note})`) : "";
6364
- statusStr = chalk6.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
6519
+ const loc = tier.path ? chalk4.gray(tier.path) : "";
6520
+ const note = tier.note ? chalk4.gray(`(${tier.note})`) : "";
6521
+ statusStr = chalk4.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
6365
6522
  } else {
6366
- statusStr = chalk6.gray("\u25CB " + (tier.note ?? "not found"));
6523
+ statusStr = chalk4.gray("\u25CB " + (tier.note ?? "not found"));
6367
6524
  }
6368
- console.log(`${num} ${chalk6.white(label)} ${statusStr}`);
6525
+ console.log(`${num} ${chalk4.white(label)} ${statusStr}`);
6369
6526
  }
6370
6527
  console.log("");
6371
- console.log(chalk6.bold("Policy Evaluation:"));
6528
+ console.log(chalk4.bold("Policy Evaluation:"));
6372
6529
  for (const step of result.steps) {
6373
6530
  const isFinal = step.isFinal;
6374
6531
  let icon;
6375
- if (step.outcome === "allow") icon = chalk6.green(" \u2705");
6376
- else if (step.outcome === "review") icon = chalk6.red(" \u{1F534}");
6377
- else if (step.outcome === "skip") icon = chalk6.gray(" \u2500 ");
6378
- else icon = chalk6.gray(" \u25CB ");
6532
+ if (step.outcome === "allow") icon = chalk4.green(" \u2705");
6533
+ else if (step.outcome === "review") icon = chalk4.red(" \u{1F534}");
6534
+ else if (step.outcome === "skip") icon = chalk4.gray(" \u2500 ");
6535
+ else icon = chalk4.gray(" \u25CB ");
6379
6536
  const name = step.name.padEnd(18);
6380
- const nameStr = isFinal ? chalk6.white.bold(name) : chalk6.white(name);
6381
- const detail = isFinal ? chalk6.white(step.detail) : chalk6.gray(step.detail);
6382
- const arrow = isFinal ? chalk6.yellow(" \u2190 STOP") : "";
6537
+ const nameStr = isFinal ? chalk4.white.bold(name) : chalk4.white(name);
6538
+ const detail = isFinal ? chalk4.white(step.detail) : chalk4.gray(step.detail);
6539
+ const arrow = isFinal ? chalk4.yellow(" \u2190 STOP") : "";
6383
6540
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
6384
6541
  }
6385
6542
  console.log("");
6386
6543
  if (result.decision === "allow") {
6387
- console.log(chalk6.green.bold(" Decision: \u2705 ALLOW") + chalk6.gray(" \u2014 no approval needed"));
6544
+ console.log(chalk4.green.bold(" Decision: \u2705 ALLOW") + chalk4.gray(" \u2014 no approval needed"));
6388
6545
  } else {
6389
6546
  console.log(
6390
- chalk6.red.bold(" Decision: \u{1F534} REVIEW") + chalk6.gray(" \u2014 human approval required")
6547
+ chalk4.red.bold(" Decision: \u{1F534} REVIEW") + chalk4.gray(" \u2014 human approval required")
6391
6548
  );
6392
6549
  if (result.blockedByLabel) {
6393
- console.log(chalk6.gray(` Reason: ${result.blockedByLabel}`));
6550
+ console.log(chalk4.gray(` Reason: ${result.blockedByLabel}`));
6394
6551
  }
6395
6552
  }
6396
6553
  console.log("");
@@ -6398,8 +6555,8 @@ program.command("explain").description(
6398
6555
  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) => {
6399
6556
  const configPath = path10.join(os7.homedir(), ".node9", "config.json");
6400
6557
  if (fs8.existsSync(configPath) && !options.force) {
6401
- console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
6402
- console.log(chalk6.gray(` Run with --force to overwrite.`));
6558
+ console.log(chalk4.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
6559
+ console.log(chalk4.gray(` Run with --force to overwrite.`));
6403
6560
  return;
6404
6561
  }
6405
6562
  const requestedMode = options.mode.toLowerCase();
@@ -6414,10 +6571,10 @@ program.command("init").description("Create ~/.node9/config.json with default po
6414
6571
  const dir = path10.dirname(configPath);
6415
6572
  if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
6416
6573
  fs8.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
6417
- console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
6418
- console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
6574
+ console.log(chalk4.green(`\u2705 Global config created: ${configPath}`));
6575
+ console.log(chalk4.cyan(` Mode set to: ${safeMode}`));
6419
6576
  console.log(
6420
- chalk6.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
6577
+ chalk4.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
6421
6578
  );
6422
6579
  });
6423
6580
  function formatRelativeTime(timestamp) {
@@ -6434,7 +6591,7 @@ program.command("audit").description("View local execution audit log").option("-
6434
6591
  const logPath = path10.join(os7.homedir(), ".node9", "audit.log");
6435
6592
  if (!fs8.existsSync(logPath)) {
6436
6593
  console.log(
6437
- chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
6594
+ chalk4.yellow("No audit logs found. Run node9 with an agent to generate entries.")
6438
6595
  );
6439
6596
  return;
6440
6597
  }
@@ -6460,31 +6617,31 @@ program.command("audit").description("View local execution audit log").option("-
6460
6617
  return;
6461
6618
  }
6462
6619
  if (entries.length === 0) {
6463
- console.log(chalk6.yellow("No matching audit entries."));
6620
+ console.log(chalk4.yellow("No matching audit entries."));
6464
6621
  return;
6465
6622
  }
6466
6623
  console.log(
6467
6624
  `
6468
- ${chalk6.bold("Node9 Audit Log")} ${chalk6.dim(`(${entries.length} entries)`)}`
6625
+ ${chalk4.bold("Node9 Audit Log")} ${chalk4.dim(`(${entries.length} entries)`)}`
6469
6626
  );
6470
- console.log(chalk6.dim(" " + "\u2500".repeat(65)));
6627
+ console.log(chalk4.dim(" " + "\u2500".repeat(65)));
6471
6628
  console.log(
6472
6629
  ` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
6473
6630
  );
6474
- console.log(chalk6.dim(" " + "\u2500".repeat(65)));
6631
+ console.log(chalk4.dim(" " + "\u2500".repeat(65)));
6475
6632
  for (const e of entries) {
6476
6633
  const time = formatRelativeTime(String(e.ts)).padEnd(12);
6477
6634
  const tool = String(e.tool).slice(0, 17).padEnd(18);
6478
- const result = e.decision === "allow" ? chalk6.green("ALLOW".padEnd(10)) : chalk6.red("DENY".padEnd(10));
6635
+ const result = e.decision === "allow" ? chalk4.green("ALLOW".padEnd(10)) : chalk4.red("DENY".padEnd(10));
6479
6636
  const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
6480
6637
  const agent = String(e.agent || "unknown");
6481
6638
  console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
6482
6639
  }
6483
6640
  const allowed = entries.filter((e) => e.decision === "allow").length;
6484
6641
  const denied = entries.filter((e) => e.decision === "deny").length;
6485
- console.log(chalk6.dim(" " + "\u2500".repeat(65)));
6642
+ console.log(chalk4.dim(" " + "\u2500".repeat(65)));
6486
6643
  console.log(
6487
- ` ${entries.length} entries | ${chalk6.green(allowed + " allowed")} | ${chalk6.red(denied + " denied")}
6644
+ ` ${entries.length} entries | ${chalk4.green(allowed + " allowed")} | ${chalk4.red(denied + " denied")}
6488
6645
  `
6489
6646
  );
6490
6647
  });
@@ -6495,43 +6652,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
6495
6652
  const settings = mergedConfig.settings;
6496
6653
  console.log("");
6497
6654
  if (creds && settings.approvers.cloud) {
6498
- console.log(chalk6.green(" \u25CF Agent mode") + chalk6.gray(" \u2014 cloud team policy enforced"));
6655
+ console.log(chalk4.green(" \u25CF Agent mode") + chalk4.gray(" \u2014 cloud team policy enforced"));
6499
6656
  } else if (creds && !settings.approvers.cloud) {
6500
6657
  console.log(
6501
- chalk6.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 all decisions stay on this machine")
6658
+ chalk4.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk4.gray(" \u2014 all decisions stay on this machine")
6502
6659
  );
6503
6660
  } else {
6504
6661
  console.log(
6505
- chalk6.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 no API key (Local rules only)")
6662
+ chalk4.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk4.gray(" \u2014 no API key (Local rules only)")
6506
6663
  );
6507
6664
  }
6508
6665
  console.log("");
6509
6666
  if (daemonRunning) {
6510
6667
  console.log(
6511
- chalk6.green(" \u25CF Daemon running") + chalk6.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
6668
+ chalk4.green(" \u25CF Daemon running") + chalk4.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
6512
6669
  );
6513
6670
  } else {
6514
- console.log(chalk6.gray(" \u25CB Daemon stopped"));
6671
+ console.log(chalk4.gray(" \u25CB Daemon stopped"));
6515
6672
  }
6516
6673
  if (settings.enableUndo) {
6517
6674
  console.log(
6518
- chalk6.magenta(" \u25CF Undo Engine") + chalk6.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
6675
+ chalk4.magenta(" \u25CF Undo Engine") + chalk4.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
6519
6676
  );
6520
6677
  }
6521
6678
  console.log("");
6522
- const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
6679
+ const modeLabel = settings.mode === "audit" ? chalk4.blue("audit") : settings.mode === "strict" ? chalk4.red("strict") : chalk4.white("standard");
6523
6680
  console.log(` Mode: ${modeLabel}`);
6524
6681
  const projectConfig = path10.join(process.cwd(), "node9.config.json");
6525
6682
  const globalConfig = path10.join(os7.homedir(), ".node9", "config.json");
6526
6683
  console.log(
6527
- ` Local: ${fs8.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
6684
+ ` Local: ${fs8.existsSync(projectConfig) ? chalk4.green("Active (node9.config.json)") : chalk4.gray("Not present")}`
6528
6685
  );
6529
6686
  console.log(
6530
- ` Global: ${fs8.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
6687
+ ` Global: ${fs8.existsSync(globalConfig) ? chalk4.green("Active (~/.node9/config.json)") : chalk4.gray("Not present")}`
6531
6688
  );
6532
6689
  if (mergedConfig.policy.sandboxPaths.length > 0) {
6533
6690
  console.log(
6534
- ` Sandbox: ${chalk6.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
6691
+ ` Sandbox: ${chalk4.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
6535
6692
  );
6536
6693
  }
6537
6694
  const pauseState = checkPause();
@@ -6539,7 +6696,7 @@ program.command("status").description("Show current Node9 mode, policy source, a
6539
6696
  const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
6540
6697
  console.log("");
6541
6698
  console.log(
6542
- chalk6.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk6.gray(" \u2014 all tool calls allowed")
6699
+ chalk4.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk4.gray(" \u2014 all tool calls allowed")
6543
6700
  );
6544
6701
  }
6545
6702
  console.log("");
@@ -6553,14 +6710,14 @@ program.command("daemon").description("Run the local approval server").argument(
6553
6710
  if (cmd === "stop") return stopDaemon();
6554
6711
  if (cmd === "status") return daemonStatus();
6555
6712
  if (cmd !== "start" && action !== void 0) {
6556
- console.error(chalk6.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
6713
+ console.error(chalk4.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
6557
6714
  process.exit(1);
6558
6715
  }
6559
6716
  if (options.watch) {
6560
6717
  process.env.NODE9_WATCH_MODE = "1";
6561
6718
  setTimeout(() => {
6562
6719
  openBrowserLocal();
6563
- console.log(chalk6.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
6720
+ console.log(chalk4.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
6564
6721
  }, 600);
6565
6722
  startDaemon();
6566
6723
  return;
@@ -6568,7 +6725,7 @@ program.command("daemon").description("Run the local approval server").argument(
6568
6725
  if (options.openui) {
6569
6726
  if (isDaemonRunning()) {
6570
6727
  openBrowserLocal();
6571
- console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
6728
+ console.log(chalk4.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
6572
6729
  process.exit(0);
6573
6730
  }
6574
6731
  const child = spawn5(process.execPath, [process.argv[1], "daemon"], {
@@ -6581,7 +6738,7 @@ program.command("daemon").description("Run the local approval server").argument(
6581
6738
  if (isDaemonRunning()) break;
6582
6739
  }
6583
6740
  openBrowserLocal();
6584
- console.log(chalk6.green(`
6741
+ console.log(chalk4.green(`
6585
6742
  \u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
6586
6743
  process.exit(0);
6587
6744
  }
@@ -6591,7 +6748,7 @@ program.command("daemon").description("Run the local approval server").argument(
6591
6748
  stdio: "ignore"
6592
6749
  });
6593
6750
  child.unref();
6594
- console.log(chalk6.green(`
6751
+ console.log(chalk4.green(`
6595
6752
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
6596
6753
  process.exit(0);
6597
6754
  }
@@ -6603,10 +6760,64 @@ program.command("tail").description("Stream live agent activity to the terminal"
6603
6760
  try {
6604
6761
  await startTail2(options);
6605
6762
  } catch (err) {
6606
- console.error(chalk6.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
6763
+ console.error(chalk4.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
6607
6764
  process.exit(1);
6608
6765
  }
6609
6766
  });
6767
+ 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) => {
6768
+ let port = DAEMON_PORT2;
6769
+ try {
6770
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
6771
+ signal: AbortSignal.timeout(500)
6772
+ });
6773
+ if (res.ok) {
6774
+ const data = await res.json();
6775
+ if (typeof data.port === "number") port = data.port;
6776
+ } else {
6777
+ throw new Error("not running");
6778
+ }
6779
+ } catch {
6780
+ console.error(chalk4.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
6781
+ const child = spawn5(process.execPath, [process.argv[1], "daemon"], {
6782
+ detached: true,
6783
+ stdio: "ignore",
6784
+ env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_WATCH_MODE: "1" }
6785
+ });
6786
+ child.unref();
6787
+ let ready = false;
6788
+ for (let i = 0; i < 20; i++) {
6789
+ await new Promise((r) => setTimeout(r, 250));
6790
+ try {
6791
+ const r = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
6792
+ signal: AbortSignal.timeout(500)
6793
+ });
6794
+ if (r.ok) {
6795
+ ready = true;
6796
+ break;
6797
+ }
6798
+ } catch {
6799
+ }
6800
+ }
6801
+ if (!ready) {
6802
+ console.error(chalk4.red("\u274C Daemon failed to start. Try: node9 daemon start"));
6803
+ process.exit(1);
6804
+ }
6805
+ }
6806
+ console.error(
6807
+ chalk4.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk4.dim(` \u2192 localhost:${port}`) + chalk4.dim(
6808
+ "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
6809
+ )
6810
+ );
6811
+ const result = spawnSync4(cmd, args, {
6812
+ stdio: "inherit",
6813
+ env: { ...process.env, NODE9_WATCH_MODE: "1" }
6814
+ });
6815
+ if (result.error) {
6816
+ console.error(chalk4.red(`\u274C Failed to run command: ${result.error.message}`));
6817
+ process.exit(1);
6818
+ }
6819
+ process.exit(result.status ?? 0);
6820
+ });
6610
6821
  program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
6611
6822
  const processPayload = async (raw) => {
6612
6823
  try {
@@ -6644,19 +6855,30 @@ RAW: ${raw}
6644
6855
  const sendBlock = (msg, result2) => {
6645
6856
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
6646
6857
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
6647
- if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
6648
- console.error(chalk6.bgRed.white.bold(`
6858
+ let ttyFd = null;
6859
+ try {
6860
+ ttyFd = fs8.openSync("/dev/tty", "w");
6861
+ const writeTty = (line) => fs8.writeSync(ttyFd, line + "\n");
6862
+ if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
6863
+ writeTty(chalk4.bgRed.white.bold(`
6649
6864
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
6650
- console.error(chalk6.red.bold(` A sensitive secret was found in the tool arguments!`));
6651
- } else {
6652
- console.error(chalk6.red(`
6865
+ writeTty(chalk4.red.bold(` A sensitive secret was found in the tool arguments!`));
6866
+ } else {
6867
+ writeTty(chalk4.red(`
6653
6868
  \u{1F6D1} Node9 blocked "${toolName}"`));
6869
+ }
6870
+ writeTty(chalk4.gray(` Triggered by: ${blockedByContext}`));
6871
+ if (result2?.changeHint) writeTty(chalk4.cyan(` To change: ${result2.changeHint}`));
6872
+ writeTty("");
6873
+ } catch {
6874
+ } finally {
6875
+ if (ttyFd !== null)
6876
+ try {
6877
+ fs8.closeSync(ttyFd);
6878
+ } catch {
6879
+ }
6654
6880
  }
6655
- console.error(chalk6.gray(` Triggered by: ${blockedByContext}`));
6656
- if (result2?.changeHint) console.error(chalk6.cyan(` To change: ${result2.changeHint}`));
6657
- console.error("");
6658
6881
  const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
6659
- console.error(chalk6.dim(` (Detailed instructions sent to AI agent)`));
6660
6882
  process.stdout.write(
6661
6883
  JSON.stringify({
6662
6884
  decision: "block",
@@ -6679,7 +6901,10 @@ RAW: ${raw}
6679
6901
  if (shouldSnapshot(toolName, toolInput, config)) {
6680
6902
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
6681
6903
  }
6682
- const result = await authorizeHeadless(toolName, toolInput, false, meta);
6904
+ const safeCwdForAuth = typeof payload.cwd === "string" && path10.isAbsolute(payload.cwd) ? payload.cwd : void 0;
6905
+ const result = await authorizeHeadless(toolName, toolInput, meta, {
6906
+ cwd: safeCwdForAuth
6907
+ });
6683
6908
  if (result.approved) {
6684
6909
  if (result.checkedBy && process.env.NODE9_DEBUG === "1")
6685
6910
  process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
@@ -6687,10 +6912,20 @@ RAW: ${raw}
6687
6912
  process.exit(0);
6688
6913
  }
6689
6914
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
6690
- console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
6915
+ try {
6916
+ const tty = fs8.openSync("/dev/tty", "w");
6917
+ fs8.writeSync(
6918
+ tty,
6919
+ chalk4.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
6920
+ );
6921
+ fs8.closeSync(tty);
6922
+ } catch {
6923
+ }
6691
6924
  const daemonReady = await autoStartDaemonAndWait();
6692
6925
  if (daemonReady) {
6693
- const retry = await authorizeHeadless(toolName, toolInput, false, meta);
6926
+ const retry = await authorizeHeadless(toolName, toolInput, meta, {
6927
+ cwd: safeCwdForAuth
6928
+ });
6694
6929
  if (retry.approved) {
6695
6930
  if (retry.checkedBy && process.env.NODE9_DEBUG === "1")
6696
6931
  process.stderr.write(`\u2713 node9 [${retry.checkedBy}]: "${toolName}" allowed
@@ -6797,7 +7032,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
6797
7032
  const ms = parseDuration(options.duration);
6798
7033
  if (ms === null) {
6799
7034
  console.error(
6800
- chalk6.red(`
7035
+ chalk4.red(`
6801
7036
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
6802
7037
  `)
6803
7038
  );
@@ -6805,20 +7040,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
6805
7040
  }
6806
7041
  pauseNode9(ms, options.duration);
6807
7042
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
6808
- console.log(chalk6.yellow(`
7043
+ console.log(chalk4.yellow(`
6809
7044
  \u23F8 Node9 paused until ${expiresAt}`));
6810
- console.log(chalk6.gray(` All tool calls will be allowed without review.`));
6811
- console.log(chalk6.gray(` Run "node9 resume" to re-enable early.
7045
+ console.log(chalk4.gray(` All tool calls will be allowed without review.`));
7046
+ console.log(chalk4.gray(` Run "node9 resume" to re-enable early.
6812
7047
  `));
6813
7048
  });
6814
7049
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
6815
7050
  const { paused } = checkPause();
6816
7051
  if (!paused) {
6817
- console.log(chalk6.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
7052
+ console.log(chalk4.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
6818
7053
  return;
6819
7054
  }
6820
7055
  resumeNode9();
6821
- console.log(chalk6.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
7056
+ console.log(chalk4.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
6822
7057
  });
6823
7058
  var HOOK_BASED_AGENTS = {
6824
7059
  claude: "claude",
@@ -6831,37 +7066,50 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
6831
7066
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
6832
7067
  const target = HOOK_BASED_AGENTS[firstArg2];
6833
7068
  console.error(
6834
- chalk6.yellow(`
7069
+ chalk4.yellow(`
6835
7070
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
6836
7071
  );
6837
- console.error(chalk6.white(`
7072
+ console.error(chalk4.white(`
6838
7073
  "${target}" uses its own hook system. Use:`));
6839
7074
  console.error(
6840
- chalk6.green(` node9 addto ${target} `) + chalk6.gray("# one-time setup")
7075
+ chalk4.green(` node9 addto ${target} `) + chalk4.gray("# one-time setup")
6841
7076
  );
6842
- console.error(chalk6.green(` ${target} `) + chalk6.gray("# run normally"));
7077
+ console.error(chalk4.green(` ${target} `) + chalk4.gray("# run normally"));
6843
7078
  process.exit(1);
6844
7079
  }
6845
- const fullCommand = commandArgs.join(" ");
6846
- let result = await authorizeHeadless("shell", { command: fullCommand }, true, {
6847
- agent: "Terminal"
6848
- });
7080
+ const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
7081
+ if (runArgs.length === 0) {
7082
+ program.help();
7083
+ return;
7084
+ }
7085
+ const fullCommand = runArgs.join(" ");
7086
+ let result = await authorizeHeadless(
7087
+ "shell",
7088
+ { command: fullCommand },
7089
+ {
7090
+ agent: "Terminal"
7091
+ }
7092
+ );
6849
7093
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
6850
- console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
7094
+ console.error(chalk4.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
6851
7095
  const daemonReady = await autoStartDaemonAndWait();
6852
7096
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
6853
7097
  }
6854
7098
  if (result.noApprovalMechanism && process.stdout.isTTY) {
6855
- result = await authorizeHeadless("shell", { command: fullCommand }, true);
7099
+ const approved = await confirm2({
7100
+ message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
7101
+ default: false
7102
+ });
7103
+ result = { approved, reason: approved ? void 0 : "Denied by user at terminal." };
6856
7104
  }
6857
7105
  if (!result.approved) {
6858
7106
  console.error(
6859
- chalk6.red(`
7107
+ chalk4.red(`
6860
7108
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
6861
7109
  );
6862
7110
  process.exit(1);
6863
7111
  }
6864
- console.error(chalk6.green("\n\u2705 Approved \u2014 running command...\n"));
7112
+ console.error(chalk4.green("\n\u2705 Approved \u2014 running command...\n"));
6865
7113
  await runProxy(fullCommand);
6866
7114
  } else {
6867
7115
  program.help();
@@ -6876,22 +7124,22 @@ program.command("undo").description(
6876
7124
  if (history.length === 0) {
6877
7125
  if (!options.all && allHistory.length > 0) {
6878
7126
  console.log(
6879
- chalk6.yellow(
7127
+ chalk4.yellow(
6880
7128
  `
6881
7129
  \u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
6882
- Run ${chalk6.cyan("node9 undo --all")} to see snapshots from all projects.
7130
+ Run ${chalk4.cyan("node9 undo --all")} to see snapshots from all projects.
6883
7131
  `
6884
7132
  )
6885
7133
  );
6886
7134
  } else {
6887
- console.log(chalk6.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
7135
+ console.log(chalk4.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
6888
7136
  }
6889
7137
  return;
6890
7138
  }
6891
7139
  const idx = history.length - steps;
6892
7140
  if (idx < 0) {
6893
7141
  console.log(
6894
- chalk6.yellow(
7142
+ chalk4.yellow(
6895
7143
  `
6896
7144
  \u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
6897
7145
  `
@@ -6902,18 +7150,18 @@ program.command("undo").description(
6902
7150
  const snapshot = history[idx];
6903
7151
  const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
6904
7152
  const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
6905
- console.log(chalk6.magenta.bold(`
7153
+ console.log(chalk4.magenta.bold(`
6906
7154
  \u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
6907
7155
  console.log(
6908
- chalk6.white(
6909
- ` Tool: ${chalk6.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk6.gray(" \u2192 " + snapshot.argsSummary) : ""}`
7156
+ chalk4.white(
7157
+ ` Tool: ${chalk4.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk4.gray(" \u2192 " + snapshot.argsSummary) : ""}`
6910
7158
  )
6911
7159
  );
6912
- console.log(chalk6.white(` When: ${chalk6.gray(ageStr)}`));
6913
- console.log(chalk6.white(` Dir: ${chalk6.gray(snapshot.cwd)}`));
7160
+ console.log(chalk4.white(` When: ${chalk4.gray(ageStr)}`));
7161
+ console.log(chalk4.white(` Dir: ${chalk4.gray(snapshot.cwd)}`));
6914
7162
  if (steps > 1)
6915
7163
  console.log(
6916
- chalk6.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
7164
+ chalk4.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
6917
7165
  );
6918
7166
  console.log("");
6919
7167
  const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
@@ -6921,65 +7169,65 @@ program.command("undo").description(
6921
7169
  const lines = diff.split("\n");
6922
7170
  for (const line of lines) {
6923
7171
  if (line.startsWith("+++") || line.startsWith("---")) {
6924
- console.log(chalk6.bold(line));
7172
+ console.log(chalk4.bold(line));
6925
7173
  } else if (line.startsWith("+")) {
6926
- console.log(chalk6.green(line));
7174
+ console.log(chalk4.green(line));
6927
7175
  } else if (line.startsWith("-")) {
6928
- console.log(chalk6.red(line));
7176
+ console.log(chalk4.red(line));
6929
7177
  } else if (line.startsWith("@@")) {
6930
- console.log(chalk6.cyan(line));
7178
+ console.log(chalk4.cyan(line));
6931
7179
  } else {
6932
- console.log(chalk6.gray(line));
7180
+ console.log(chalk4.gray(line));
6933
7181
  }
6934
7182
  }
6935
7183
  console.log("");
6936
7184
  } else {
6937
7185
  console.log(
6938
- chalk6.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
7186
+ chalk4.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
6939
7187
  );
6940
7188
  }
6941
- const proceed = await confirm3({
7189
+ const proceed = await confirm2({
6942
7190
  message: `Revert to this snapshot?`,
6943
7191
  default: false
6944
7192
  });
6945
7193
  if (proceed) {
6946
7194
  if (applyUndo(snapshot.hash, snapshot.cwd)) {
6947
- console.log(chalk6.green("\n\u2705 Reverted successfully.\n"));
7195
+ console.log(chalk4.green("\n\u2705 Reverted successfully.\n"));
6948
7196
  } else {
6949
- console.error(chalk6.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
7197
+ console.error(chalk4.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
6950
7198
  }
6951
7199
  } else {
6952
- console.log(chalk6.gray("\nCancelled.\n"));
7200
+ console.log(chalk4.gray("\nCancelled.\n"));
6953
7201
  }
6954
7202
  });
6955
7203
  var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
6956
7204
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
6957
7205
  const name = resolveShieldName(service);
6958
7206
  if (!name) {
6959
- console.error(chalk6.red(`
7207
+ console.error(chalk4.red(`
6960
7208
  \u274C Unknown shield: "${service}"
6961
7209
  `));
6962
- console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
7210
+ console.log(`Run ${chalk4.cyan("node9 shield list")} to see available shields.
6963
7211
  `);
6964
7212
  process.exit(1);
6965
7213
  }
6966
7214
  const shield = getShield(name);
6967
7215
  const active = readActiveShields();
6968
7216
  if (active.includes(name)) {
6969
- console.log(chalk6.yellow(`
7217
+ console.log(chalk4.yellow(`
6970
7218
  \u2139\uFE0F Shield "${name}" is already active.
6971
7219
  `));
6972
7220
  return;
6973
7221
  }
6974
7222
  writeActiveShields([...active, name]);
6975
- console.log(chalk6.green(`
7223
+ console.log(chalk4.green(`
6976
7224
  \u{1F6E1}\uFE0F Shield "${name}" enabled.`));
6977
- console.log(chalk6.gray(` ${shield.smartRules.length} smart rules now active.`));
7225
+ console.log(chalk4.gray(` ${shield.smartRules.length} smart rules now active.`));
6978
7226
  if (shield.dangerousWords.length > 0)
6979
- console.log(chalk6.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
7227
+ console.log(chalk4.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
6980
7228
  if (name === "filesystem") {
6981
7229
  console.log(
6982
- chalk6.yellow(
7230
+ chalk4.yellow(
6983
7231
  `
6984
7232
  \u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
6985
7233
  Tools like unlink, find -delete, or language-level file ops are not intercepted.`
@@ -6991,70 +7239,70 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
6991
7239
  shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
6992
7240
  const name = resolveShieldName(service);
6993
7241
  if (!name) {
6994
- console.error(chalk6.red(`
7242
+ console.error(chalk4.red(`
6995
7243
  \u274C Unknown shield: "${service}"
6996
7244
  `));
6997
- console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
7245
+ console.log(`Run ${chalk4.cyan("node9 shield list")} to see available shields.
6998
7246
  `);
6999
7247
  process.exit(1);
7000
7248
  }
7001
7249
  const active = readActiveShields();
7002
7250
  if (!active.includes(name)) {
7003
- console.log(chalk6.yellow(`
7251
+ console.log(chalk4.yellow(`
7004
7252
  \u2139\uFE0F Shield "${name}" is not active.
7005
7253
  `));
7006
7254
  return;
7007
7255
  }
7008
7256
  writeActiveShields(active.filter((s) => s !== name));
7009
- console.log(chalk6.green(`
7257
+ console.log(chalk4.green(`
7010
7258
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
7011
7259
  `));
7012
7260
  });
7013
7261
  shieldCmd.command("list").description("Show all available shields").action(() => {
7014
7262
  const active = new Set(readActiveShields());
7015
- console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
7263
+ console.log(chalk4.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
7016
7264
  for (const shield of listShields()) {
7017
- const status = active.has(shield.name) ? chalk6.green("\u25CF enabled") : chalk6.gray("\u25CB disabled");
7018
- console.log(` ${status} ${chalk6.cyan(shield.name.padEnd(12))} ${shield.description}`);
7265
+ const status = active.has(shield.name) ? chalk4.green("\u25CF enabled") : chalk4.gray("\u25CB disabled");
7266
+ console.log(` ${status} ${chalk4.cyan(shield.name.padEnd(12))} ${shield.description}`);
7019
7267
  if (shield.aliases.length > 0)
7020
- console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
7268
+ console.log(chalk4.gray(` aliases: ${shield.aliases.join(", ")}`));
7021
7269
  }
7022
7270
  console.log("");
7023
7271
  });
7024
7272
  shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
7025
7273
  const active = readActiveShields();
7026
7274
  if (active.length === 0) {
7027
- console.error(chalk6.yellow("\n\u2139\uFE0F No shields are active.\n"));
7028
- console.error(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
7275
+ console.error(chalk4.yellow("\n\u2139\uFE0F No shields are active.\n"));
7276
+ console.error(`Run ${chalk4.cyan("node9 shield list")} to see available shields.
7029
7277
  `);
7030
7278
  return;
7031
7279
  }
7032
7280
  const overrides = readShieldOverrides();
7033
- console.error(chalk6.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
7281
+ console.error(chalk4.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
7034
7282
  for (const name of active) {
7035
7283
  const shield = getShield(name);
7036
7284
  if (!shield) continue;
7037
- console.error(` ${chalk6.green("\u25CF")} ${chalk6.cyan(name)} \u2014 ${shield.description}`);
7285
+ console.error(` ${chalk4.green("\u25CF")} ${chalk4.cyan(name)} \u2014 ${shield.description}`);
7038
7286
  const ruleOverrides = overrides[name] ?? {};
7039
7287
  for (const rule of shield.smartRules) {
7040
7288
  const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
7041
7289
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
7042
7290
  const effectiveVerdict = overrideVerdict ?? rule.verdict;
7043
- const verdictLabel = effectiveVerdict === "block" ? chalk6.red("block ") : effectiveVerdict === "review" ? chalk6.yellow("review") : chalk6.green("allow ");
7044
- const overrideNote = overrideVerdict ? chalk6.gray(` \u2190 overridden (was: ${rule.verdict})`) : "";
7291
+ const verdictLabel = effectiveVerdict === "block" ? chalk4.red("block ") : effectiveVerdict === "review" ? chalk4.yellow("review") : chalk4.green("allow ");
7292
+ const overrideNote = overrideVerdict ? chalk4.gray(` \u2190 overridden (was: ${rule.verdict})`) : "";
7045
7293
  console.error(
7046
- ` ${verdictLabel} ${shortName.padEnd(24)} ${chalk6.gray(rule.reason ?? "")}${overrideNote}`
7294
+ ` ${verdictLabel} ${shortName.padEnd(24)} ${chalk4.gray(rule.reason ?? "")}${overrideNote}`
7047
7295
  );
7048
7296
  }
7049
7297
  if (shield.dangerousWords.length > 0) {
7050
- console.error(chalk6.gray(` words: ${shield.dangerousWords.join(", ")}`));
7298
+ console.error(chalk4.gray(` words: ${shield.dangerousWords.join(", ")}`));
7051
7299
  }
7052
7300
  console.error("");
7053
7301
  }
7054
7302
  if (Object.keys(overrides).length > 0) {
7055
7303
  console.error(
7056
- chalk6.gray(
7057
- ` Tip: run ${chalk6.cyan("node9 shield unset <shield> <rule>")} to remove an override.
7304
+ chalk4.gray(
7305
+ ` Tip: run ${chalk4.cyan("node9 shield unset <shield> <rule>")} to remove an override.
7058
7306
  `
7059
7307
  )
7060
7308
  );
@@ -7063,35 +7311,35 @@ shieldCmd.command("status").description("Show active shields and their individua
7063
7311
  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) => {
7064
7312
  const name = resolveShieldName(service);
7065
7313
  if (!name) {
7066
- console.error(chalk6.red(`
7314
+ console.error(chalk4.red(`
7067
7315
  \u274C Unknown shield: "${service}"
7068
7316
  `));
7069
- console.error(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
7317
+ console.error(`Run ${chalk4.cyan("node9 shield list")} to see available shields.
7070
7318
  `);
7071
7319
  process.exit(1);
7072
7320
  }
7073
7321
  if (!readActiveShields().includes(name)) {
7074
- console.error(chalk6.red(`
7322
+ console.error(chalk4.red(`
7075
7323
  \u274C Shield "${name}" is not active. Enable it first:
7076
7324
  `));
7077
- console.error(` ${chalk6.cyan(`node9 shield enable ${name}`)}
7325
+ console.error(` ${chalk4.cyan(`node9 shield enable ${name}`)}
7078
7326
  `);
7079
7327
  process.exit(1);
7080
7328
  }
7081
7329
  if (!isShieldVerdict(verdict)) {
7082
- console.error(chalk6.red(`
7330
+ console.error(chalk4.red(`
7083
7331
  \u274C Invalid verdict "${verdict}". Use: block, review, or allow
7084
7332
  `));
7085
7333
  process.exit(1);
7086
7334
  }
7087
7335
  if (verdict === "allow" && !opts.force) {
7088
7336
  console.error(
7089
- chalk6.red(`
7337
+ chalk4.red(`
7090
7338
  \u26A0\uFE0F Setting a verdict to "allow" silences the rule entirely.
7091
- `) + chalk6.yellow(
7339
+ `) + chalk4.yellow(
7092
7340
  ` This disables a shield protection. If you are sure, re-run with --force:
7093
7341
  `
7094
- ) + chalk6.cyan(`
7342
+ ) + chalk4.cyan(`
7095
7343
  node9 shield set ${service} ${rule} allow --force
7096
7344
  `)
7097
7345
  );
@@ -7100,13 +7348,13 @@ shieldCmd.command("set <service> <rule> <verdict>").description("Override the ve
7100
7348
  const ruleName = resolveShieldRule(name, rule);
7101
7349
  if (!ruleName) {
7102
7350
  const shield = getShield(name);
7103
- console.error(chalk6.red(`
7351
+ console.error(chalk4.red(`
7104
7352
  \u274C Unknown rule "${rule}" for shield "${name}".
7105
7353
  `));
7106
7354
  console.error(" Available rules:");
7107
7355
  for (const r of shield?.smartRules ?? []) {
7108
7356
  const short = r.name ? r.name.replace(`shield:${name}:`, "") : "";
7109
- console.error(` ${chalk6.cyan(short)}`);
7357
+ console.error(` ${chalk4.cyan(short)}`);
7110
7358
  }
7111
7359
  console.error("");
7112
7360
  process.exit(1);
@@ -7116,33 +7364,33 @@ shieldCmd.command("set <service> <rule> <verdict>").description("Override the ve
7116
7364
  appendConfigAudit({ event: "shield-override-allow", shield: name, rule: ruleName });
7117
7365
  }
7118
7366
  const shortName = ruleName.replace(`shield:${name}:`, "");
7119
- const verdictLabel = verdict === "block" ? chalk6.red("block") : verdict === "review" ? chalk6.yellow("review") : chalk6.green("allow");
7367
+ const verdictLabel = verdict === "block" ? chalk4.red("block") : verdict === "review" ? chalk4.yellow("review") : chalk4.green("allow");
7120
7368
  if (verdict === "allow") {
7121
7369
  console.error(
7122
- chalk6.yellow(`
7123
- \u26A0\uFE0F ${name}/${shortName} \u2192 ${verdictLabel}`) + chalk6.gray(" (rule silenced \u2014 use `node9 shield unset` to restore)\n")
7370
+ chalk4.yellow(`
7371
+ \u26A0\uFE0F ${name}/${shortName} \u2192 ${verdictLabel}`) + chalk4.gray(" (rule silenced \u2014 use `node9 shield unset` to restore)\n")
7124
7372
  );
7125
7373
  } else {
7126
- console.error(chalk6.green(`
7374
+ console.error(chalk4.green(`
7127
7375
  \u2705 ${name}/${shortName} \u2192 ${verdictLabel}
7128
7376
  `));
7129
7377
  }
7130
7378
  console.error(
7131
- chalk6.gray(` Run ${chalk6.cyan("node9 shield status")} to see all active rules.
7379
+ chalk4.gray(` Run ${chalk4.cyan("node9 shield status")} to see all active rules.
7132
7380
  `)
7133
7381
  );
7134
7382
  });
7135
7383
  shieldCmd.command("unset <service> <rule>").description("Remove a verdict override, restoring the shield default").action((service, rule) => {
7136
7384
  const name = resolveShieldName(service);
7137
7385
  if (!name) {
7138
- console.error(chalk6.red(`
7386
+ console.error(chalk4.red(`
7139
7387
  \u274C Unknown shield: "${service}"
7140
7388
  `));
7141
7389
  process.exit(1);
7142
7390
  }
7143
7391
  const ruleName = resolveShieldRule(name, rule);
7144
7392
  if (!ruleName) {
7145
- console.error(chalk6.red(`
7393
+ console.error(chalk4.red(`
7146
7394
  \u274C Unknown rule "${rule}" for shield "${name}".
7147
7395
  `));
7148
7396
  process.exit(1);
@@ -7150,7 +7398,7 @@ shieldCmd.command("unset <service> <rule>").description("Remove a verdict overri
7150
7398
  clearShieldOverride(name, ruleName);
7151
7399
  const shortName = ruleName.replace(`shield:${name}:`, "");
7152
7400
  console.error(
7153
- chalk6.green(`
7401
+ chalk4.green(`
7154
7402
  \u2705 Override removed \u2014 ${name}/${shortName} restored to default.
7155
7403
  `)
7156
7404
  );
@@ -7159,32 +7407,32 @@ program.command("config show").description("Show the full effective runtime conf
7159
7407
  const config = getConfig();
7160
7408
  const active = readActiveShields();
7161
7409
  const overrides = readShieldOverrides();
7162
- console.error(chalk6.bold("\n\u{1F50D} Node9 Effective Configuration\n"));
7163
- const modeLabel = config.settings.mode === "audit" ? chalk6.blue("audit") : config.settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
7410
+ console.error(chalk4.bold("\n\u{1F50D} Node9 Effective Configuration\n"));
7411
+ const modeLabel = config.settings.mode === "audit" ? chalk4.blue("audit") : config.settings.mode === "strict" ? chalk4.red("strict") : chalk4.white("standard");
7164
7412
  console.error(` Mode: ${modeLabel}
7165
7413
  `);
7166
7414
  if (active.length > 0) {
7167
- console.error(chalk6.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"));
7415
+ console.error(chalk4.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"));
7168
7416
  for (const name of active) {
7169
7417
  const shield = getShield(name);
7170
7418
  if (!shield) continue;
7171
7419
  const ruleOverrides = overrides[name] ?? {};
7172
7420
  console.error(`
7173
- ${chalk6.green("\u25CF")} ${chalk6.cyan(name)}`);
7421
+ ${chalk4.green("\u25CF")} ${chalk4.cyan(name)}`);
7174
7422
  for (const rule of shield.smartRules) {
7175
7423
  const shortName = rule.name ? rule.name.replace(`shield:${name}:`, "") : "(unnamed)";
7176
7424
  const overrideVerdict = rule.name ? ruleOverrides[rule.name] : void 0;
7177
7425
  const effectiveVerdict = overrideVerdict ?? rule.verdict;
7178
- const vLabel = effectiveVerdict === "block" ? chalk6.red("block ") : effectiveVerdict === "review" ? chalk6.yellow("review") : chalk6.green("allow ");
7179
- const note = overrideVerdict ? chalk6.gray(` \u2190 overridden`) : "";
7426
+ const vLabel = effectiveVerdict === "block" ? chalk4.red("block ") : effectiveVerdict === "review" ? chalk4.yellow("review") : chalk4.green("allow ");
7427
+ const note = overrideVerdict ? chalk4.gray(` \u2190 overridden`) : "";
7180
7428
  console.error(` ${vLabel} ${shortName}${note}`);
7181
7429
  }
7182
7430
  }
7183
7431
  console.error("");
7184
7432
  } else {
7185
- console.error(chalk6.gray(" No shields active. Run `node9 shield list` to see options.\n"));
7433
+ console.error(chalk4.gray(" No shields active. Run `node9 shield list` to see options.\n"));
7186
7434
  }
7187
- console.error(chalk6.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"));
7435
+ console.error(chalk4.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"));
7188
7436
  for (const rule of config.policy.smartRules) {
7189
7437
  const isShieldRule = rule.name?.startsWith("shield:");
7190
7438
  const isAdvisory = [
@@ -7195,11 +7443,11 @@ program.command("config show").description("Show the full effective runtime conf
7195
7443
  "review-drop-column-sql"
7196
7444
  ].includes(rule.name ?? "");
7197
7445
  if (isShieldRule || isAdvisory) continue;
7198
- const vLabel = rule.verdict === "block" ? chalk6.red("block ") : rule.verdict === "review" ? chalk6.yellow("review") : chalk6.green("allow ");
7199
- console.error(` ${vLabel} ${chalk6.gray(rule.name ?? rule.tool)}`);
7446
+ const vLabel = rule.verdict === "block" ? chalk4.red("block ") : rule.verdict === "review" ? chalk4.yellow("review") : chalk4.green("allow ");
7447
+ console.error(` ${vLabel} ${chalk4.gray(rule.name ?? rule.tool)}`);
7200
7448
  }
7201
7449
  console.error("");
7202
- console.error(chalk6.bold(" \u2500\u2500 Safe by Default (advisory, overridable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7450
+ console.error(chalk4.bold(" \u2500\u2500 Safe by Default (advisory, overridable) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
7203
7451
  const advisoryNames = /* @__PURE__ */ new Set([
7204
7452
  "review-rm",
7205
7453
  "allow-rm-safe-paths",
@@ -7209,12 +7457,12 @@ program.command("config show").description("Show the full effective runtime conf
7209
7457
  ]);
7210
7458
  for (const rule of config.policy.smartRules) {
7211
7459
  if (!advisoryNames.has(rule.name ?? "")) continue;
7212
- const vLabel = rule.verdict === "block" ? chalk6.red("block ") : rule.verdict === "review" ? chalk6.yellow("review") : chalk6.green("allow ");
7213
- console.error(` ${vLabel} ${chalk6.gray(rule.name ?? rule.tool)}`);
7460
+ const vLabel = rule.verdict === "block" ? chalk4.red("block ") : rule.verdict === "review" ? chalk4.yellow("review") : chalk4.green("allow ");
7461
+ console.error(` ${vLabel} ${chalk4.gray(rule.name ?? rule.tool)}`);
7214
7462
  }
7215
7463
  console.error("");
7216
- console.error(chalk6.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"));
7217
- console.error(` ${chalk6.gray(config.policy.dangerousWords.join(", "))}
7464
+ console.error(chalk4.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"));
7465
+ console.error(` ${chalk4.gray(config.policy.dangerousWords.join(", "))}
7218
7466
  `);
7219
7467
  });
7220
7468
  if (process.argv[2] !== "daemon") {