@node9/proxy 1.19.2 → 1.19.4

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
@@ -1692,7 +1692,11 @@ function extractCanonicalFindings(call, ctx) {
1692
1692
  })
1693
1693
  );
1694
1694
  }
1695
- if (PRIVILEGE_ESCALATION_RE.test(command)) {
1695
+ const ast = analyzeShellCommand(command);
1696
+ const sudoVariant = ast.actions.includes("sudo") || ast.actions.includes("su");
1697
+ const chmodVariant = ast.actions.includes("chmod") && (ast.allTokens.includes("777") || ast.allTokens.includes("0777") || ast.allTokens.includes("+x"));
1698
+ const chownVariant = ast.actions.includes("chown") && ast.allTokens.includes("root");
1699
+ if (sudoVariant || chmodVariant || chownVariant) {
1696
1700
  out.push(
1697
1701
  makeFinding({
1698
1702
  type: "privilege-escalation",
@@ -1824,7 +1828,7 @@ function* stringValues(obj, depth = 0) {
1824
1828
  }
1825
1829
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
1826
1830
  }
1827
- var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, PRIVILEGE_ESCALATION_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE;
1831
+ var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE;
1828
1832
  var init_dist = __esm({
1829
1833
  "packages/policy-engine/dist/index.mjs"() {
1830
1834
  "use strict";
@@ -3224,7 +3228,6 @@ var init_dist = __esm({
3224
3228
  LOOP_THRESHOLD_FOR_WASTE = 3;
3225
3229
  COST_PER_LOOP_ITER_USD = 6e-3;
3226
3230
  DESTRUCTIVE_OP_RE = /\brm\s+-[rRf]+\b|\bDROP\s+(TABLE|DATABASE|COLLECTION|SCHEMA)\b|\bTRUNCATE\s+TABLE\b|\bgit\s+push\s+(--force|-f)\b|\bFLUSHALL\b|\bFLUSHDB\b|\bkubectl\s+delete\b|\bhelm\s+uninstall\b/i;
3227
- PRIVILEGE_ESCALATION_RE = /\b(sudo|su)\b\s+[a-z]|\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
3228
3231
  SENSITIVE_PATH_RE = /\.aws\/(credentials|config)\b|\.ssh\/(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b|\.env(\.|$|\b)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
3229
3232
  FILE_TOOLS = /* @__PURE__ */ new Set([
3230
3233
  "read",
@@ -3244,7 +3247,7 @@ var init_dist = __esm({
3244
3247
  PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
3245
3248
  PII_CC_RE = /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
3246
3249
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
3247
- CANONICAL_EXTRACTOR_VERSION = "canonical-v1";
3250
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
3248
3251
  DEDUPE_PREVIEW_LEN = 120;
3249
3252
  TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
3250
3253
  /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
@@ -4651,7 +4654,6 @@ import fs9 from "fs";
4651
4654
  import net from "net";
4652
4655
  import path9 from "path";
4653
4656
  import os8 from "os";
4654
- import { spawnSync } from "child_process";
4655
4657
  function notifyActivitySocket(data) {
4656
4658
  return new Promise((resolve) => {
4657
4659
  try {
@@ -4721,19 +4723,16 @@ function isDaemonRunning() {
4721
4723
  }
4722
4724
  return false;
4723
4725
  }
4724
- const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
4725
- encoding: "utf8",
4726
- timeout: 300
4727
- });
4728
- if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) return true;
4729
- return false;
4726
+ return true;
4730
4727
  }
4728
+ return false;
4729
+ }
4730
+ async function isDaemonReachable(timeoutMs = 500) {
4731
4731
  try {
4732
- const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
4733
- encoding: "utf8",
4734
- timeout: 300
4732
+ const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/settings`, {
4733
+ signal: AbortSignal.timeout(timeoutMs)
4735
4734
  });
4736
- return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
4735
+ return res.ok;
4737
4736
  } catch {
4738
4737
  return false;
4739
4738
  }
@@ -4792,7 +4791,7 @@ async function waitForDaemonDecision(id, signal) {
4792
4791
  if (signal) signal.removeEventListener("abort", onAbort);
4793
4792
  }
4794
4793
  }
4795
- async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
4794
+ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent) {
4796
4795
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
4797
4796
  const res = await fetch(`${base}/check`, {
4798
4797
  method: "POST",
@@ -4803,7 +4802,12 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
4803
4802
  slackDelegated: true,
4804
4803
  agent: meta?.agent,
4805
4804
  mcpServer: meta?.mcpServer,
4806
- ...riskMetadata && { riskMetadata }
4805
+ ...riskMetadata && { riskMetadata },
4806
+ // fromCLI=true tells the daemon the CLI already sent the activity
4807
+ // event via socket. Same contract as registerDaemonEntry — without
4808
+ // it the daemon double-emits 'activity' for cloud-enforced flows.
4809
+ fromCLI: socketActivitySent !== false,
4810
+ activityId
4807
4811
  }),
4808
4812
  signal: AbortSignal.timeout(3e3)
4809
4813
  });
@@ -5944,7 +5948,14 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5944
5948
  let daemonAllowCount = 1;
5945
5949
  if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
5946
5950
  if (cloudEnforced && cloudRequestId) {
5947
- const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
5951
+ const viewer = await notifyDaemonViewer(
5952
+ toolName,
5953
+ args,
5954
+ meta,
5955
+ riskMetadata,
5956
+ options?.activityId,
5957
+ options?.socketActivitySent
5958
+ ).catch(() => null);
5948
5959
  viewerId = viewer?.id ?? null;
5949
5960
  daemonEntryId = viewerId;
5950
5961
  if (viewer) daemonAllowCount = viewer.allowCount;
@@ -8034,6 +8045,7 @@ __export(scan_watermark_exports, {
8034
8045
  markUploadComplete: () => markUploadComplete,
8035
8046
  saveWatermark: () => saveWatermark,
8036
8047
  scanDelta: () => scanDelta,
8048
+ tickForensicBroadcast: () => tickForensicBroadcast,
8037
8049
  tickScanWatcher: () => tickScanWatcher
8038
8050
  });
8039
8051
  import fs17 from "fs";
@@ -8241,6 +8253,25 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
8241
8253
  }
8242
8254
  return findings;
8243
8255
  }
8256
+ async function tickForensicBroadcast(offsets) {
8257
+ const out = [];
8258
+ const files = listJsonlFiles();
8259
+ for (const file of files) {
8260
+ const size = fileSize(file);
8261
+ const offset = offsets.get(file);
8262
+ if (offset === void 0) {
8263
+ offsets.set(file, size);
8264
+ continue;
8265
+ }
8266
+ if (size <= offset) continue;
8267
+ const sessionId = path19.basename(file, ".jsonl");
8268
+ const newOffset = await scanDelta(file, offset, (obj, lineIndex) => {
8269
+ out.push(...extractFindingsFromLine(obj, sessionId, lineIndex));
8270
+ });
8271
+ offsets.set(file, newOffset);
8272
+ }
8273
+ return out;
8274
+ }
8244
8275
  function markUploadComplete() {
8245
8276
  const state = loadWatermark();
8246
8277
  if (state.status === "schema-future") return;
@@ -11010,6 +11041,19 @@ data: ${JSON.stringify(data)}
11010
11041
  }
11011
11042
  });
11012
11043
  }
11044
+ function broadcastForensic(finding) {
11045
+ const severity = CRITICAL_FORENSIC_CATEGORIES.has(finding.type) ? "critical" : "warning";
11046
+ const event = {
11047
+ type: "forensic",
11048
+ id: `fnd_${randomUUID3()}`,
11049
+ ts: Date.now(),
11050
+ sessionId: finding.sessionId,
11051
+ category: finding.type,
11052
+ severity
11053
+ };
11054
+ if (finding.patternName !== void 0) event.patternName = finding.patternName;
11055
+ broadcast("forensic", event);
11056
+ }
11013
11057
  function abandonPending() {
11014
11058
  setAbandonTimer(null);
11015
11059
  pending.forEach((entry, id) => {
@@ -11193,7 +11237,7 @@ function bindActivitySocket() {
11193
11237
  });
11194
11238
  activitySocketServer = unixServer;
11195
11239
  }
11196
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
11240
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, CRITICAL_FORENSIC_CATEGORIES, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
11197
11241
  var init_state2 = __esm({
11198
11242
  "src/daemon/state.ts"() {
11199
11243
  "use strict";
@@ -11238,6 +11282,11 @@ var init_state2 = __esm({
11238
11282
  INPUT_PRICE_PER_1M = 3;
11239
11283
  OUTPUT_PRICE_PER_1M = 15;
11240
11284
  BYTES_PER_TOKEN = 4;
11285
+ CRITICAL_FORENSIC_CATEGORIES = /* @__PURE__ */ new Set([
11286
+ "privilege-escalation",
11287
+ "destructive-op",
11288
+ "eval-of-remote"
11289
+ ]);
11241
11290
  WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
11242
11291
  "write",
11243
11292
  "write_file",
@@ -11582,7 +11631,26 @@ function startCloudSync() {
11582
11631
  const recurring = setInterval(() => void syncOnce(), intervalMs);
11583
11632
  recurring.unref();
11584
11633
  }
11585
- var FINDING_TO_SIGNAL3, rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS;
11634
+ function startForensicBroadcast() {
11635
+ const tick = async () => {
11636
+ try {
11637
+ const findings = await tickForensicBroadcast(forensicBroadcastOffsets);
11638
+ for (const f of findings) broadcastForensic(f);
11639
+ } catch (err2) {
11640
+ const msg = err2 instanceof Error ? err2.message : String(err2);
11641
+ appendToLog(HOOK_DEBUG_LOG, {
11642
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
11643
+ kind: "forensic-broadcast-error",
11644
+ error: msg
11645
+ });
11646
+ }
11647
+ };
11648
+ const initial = setTimeout(() => void tick(), FORENSIC_INITIAL_DELAY_MS);
11649
+ initial.unref();
11650
+ const recurring = setInterval(() => void tick(), FORENSIC_BROADCAST_INTERVAL_MS);
11651
+ recurring.unref();
11652
+ }
11653
+ var FINDING_TO_SIGNAL3, rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
11586
11654
  var init_sync = __esm({
11587
11655
  "src/daemon/sync.ts"() {
11588
11656
  "use strict";
@@ -11590,6 +11658,8 @@ var init_sync = __esm({
11590
11658
  init_blast();
11591
11659
  init_dist();
11592
11660
  init_scan_watermark();
11661
+ init_state2();
11662
+ init_audit();
11593
11663
  FINDING_TO_SIGNAL3 = {
11594
11664
  dlp: "dlpFindings",
11595
11665
  pii: "piiFindings",
@@ -11606,6 +11676,9 @@ var init_sync = __esm({
11606
11676
  DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept/policies/sync";
11607
11677
  DEFAULT_INTERVAL_HOURS = 5;
11608
11678
  MIN_INTERVAL_HOURS = 1;
11679
+ FORENSIC_BROADCAST_INTERVAL_MS = 3e4;
11680
+ FORENSIC_INITIAL_DELAY_MS = 5e3;
11681
+ forensicBroadcastOffsets = /* @__PURE__ */ new Map();
11609
11682
  }
11610
11683
  });
11611
11684
 
@@ -11834,11 +11907,12 @@ import fs25 from "fs";
11834
11907
  import path27 from "path";
11835
11908
  import os23 from "os";
11836
11909
  import { randomUUID as randomUUID4 } from "crypto";
11837
- import { spawnSync as spawnSync2 } from "child_process";
11910
+ import { spawnSync } from "child_process";
11838
11911
  import chalk6 from "chalk";
11839
11912
  function startDaemon() {
11840
11913
  startCostSync();
11841
11914
  startCloudSync();
11915
+ startForensicBroadcast();
11842
11916
  startDlpScanner();
11843
11917
  loadInsightCounts();
11844
11918
  const internalToken = randomUUID4();
@@ -12644,13 +12718,26 @@ data: ${JSON.stringify(item.data)}
12644
12718
  }).then((res) => {
12645
12719
  if (res.ok) {
12646
12720
  try {
12647
- const r = spawnSync2("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
12721
+ let orphanPid = null;
12722
+ const ss = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
12648
12723
  encoding: "utf8",
12649
12724
  timeout: 1e3
12650
12725
  });
12651
- const match = r.stdout?.match(/pid=(\d+)/);
12652
- if (match) {
12653
- const orphanPid = parseInt(match[1], 10);
12726
+ if (!ss.error && ss.status === 0) {
12727
+ const m = ss.stdout?.match(/pid=(\d+)/);
12728
+ if (m) orphanPid = parseInt(m[1], 10);
12729
+ } else if (ss.error?.code === "ENOENT" || ss.status === null) {
12730
+ const lsof = spawnSync(
12731
+ "lsof",
12732
+ ["-nP", `-iTCP:${DAEMON_PORT}`, "-sTCP:LISTEN", "-t"],
12733
+ { encoding: "utf8", timeout: 1e3 }
12734
+ );
12735
+ if (!lsof.error && lsof.status === 0) {
12736
+ const first = (lsof.stdout ?? "").split("\n")[0].trim();
12737
+ if (/^\d+$/.test(first)) orphanPid = parseInt(first, 10);
12738
+ }
12739
+ }
12740
+ if (orphanPid !== null) {
12654
12741
  process.kill(orphanPid, 0);
12655
12742
  atomicWriteSync2(
12656
12743
  DAEMON_PID_FILE,
@@ -12685,7 +12772,7 @@ data: ${JSON.stringify(item.data)}
12685
12772
  JSON.stringify({ pid: process.pid, port: DAEMON_PORT, internalToken, autoStarted }),
12686
12773
  { mode: 384 }
12687
12774
  );
12688
- console.error(chalk6.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT}`));
12775
+ console.error(chalk6.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE on 127.0.0.1:${DAEMON_PORT}`));
12689
12776
  });
12690
12777
  if (watchMode) {
12691
12778
  console.error(chalk6.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
@@ -12711,7 +12798,7 @@ var init_server = __esm({
12711
12798
  import fs26 from "fs";
12712
12799
  import path28 from "path";
12713
12800
  import os24 from "os";
12714
- import { spawnSync as spawnSync3, execFileSync } from "child_process";
12801
+ import { spawnSync as spawnSync2, execFileSync } from "child_process";
12715
12802
  function resolveNode9Binary() {
12716
12803
  try {
12717
12804
  const script = process.argv[1];
@@ -12722,7 +12809,7 @@ function resolveNode9Binary() {
12722
12809
  }
12723
12810
  try {
12724
12811
  const cmd = process.platform === "win32" ? "where" : "which";
12725
- const r = spawnSync3(cmd, ["node9"], { encoding: "utf8", timeout: 3e3 });
12812
+ const r = spawnSync2(cmd, ["node9"], { encoding: "utf8", timeout: 3e3 });
12726
12813
  if (r.status === 0 && r.stdout.trim()) {
12727
12814
  return r.stdout.trim().split("\n")[0].trim();
12728
12815
  }
@@ -12774,8 +12861,8 @@ function installLaunchd(binaryPath) {
12774
12861
  const dir = path28.dirname(LAUNCHD_PLIST);
12775
12862
  if (!fs26.existsSync(dir)) fs26.mkdirSync(dir, { recursive: true });
12776
12863
  fs26.writeFileSync(LAUNCHD_PLIST, launchdPlist(binaryPath), "utf-8");
12777
- spawnSync3("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
12778
- const r = spawnSync3("launchctl", ["load", "-w", LAUNCHD_PLIST], {
12864
+ spawnSync2("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
12865
+ const r = spawnSync2("launchctl", ["load", "-w", LAUNCHD_PLIST], {
12779
12866
  encoding: "utf8",
12780
12867
  timeout: 5e3
12781
12868
  });
@@ -12785,7 +12872,7 @@ function installLaunchd(binaryPath) {
12785
12872
  }
12786
12873
  function uninstallLaunchd() {
12787
12874
  if (fs26.existsSync(LAUNCHD_PLIST)) {
12788
- spawnSync3("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
12875
+ spawnSync2("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
12789
12876
  fs26.unlinkSync(LAUNCHD_PLIST);
12790
12877
  }
12791
12878
  }
@@ -12817,15 +12904,15 @@ function installSystemd(binaryPath) {
12817
12904
  execFileSync("loginctl", ["enable-linger", os24.userInfo().username], { timeout: 3e3 });
12818
12905
  } catch {
12819
12906
  }
12820
- const reload = spawnSync3("systemctl", ["--user", "daemon-reload"], {
12907
+ const reload = spawnSync2("systemctl", ["--user", "daemon-reload"], {
12821
12908
  encoding: "utf8",
12822
12909
  timeout: 5e3
12823
12910
  });
12824
12911
  if (reload.status !== 0) {
12825
12912
  throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
12826
12913
  }
12827
- spawnSync3("systemctl", ["--user", "stop", "node9-daemon"], { encoding: "utf8", timeout: 3e3 });
12828
- const enable = spawnSync3("systemctl", ["--user", "enable", "--now", "node9-daemon"], {
12914
+ spawnSync2("systemctl", ["--user", "stop", "node9-daemon"], { encoding: "utf8", timeout: 3e3 });
12915
+ const enable = spawnSync2("systemctl", ["--user", "enable", "--now", "node9-daemon"], {
12829
12916
  encoding: "utf8",
12830
12917
  timeout: 5e3
12831
12918
  });
@@ -12835,11 +12922,11 @@ function installSystemd(binaryPath) {
12835
12922
  }
12836
12923
  function uninstallSystemd() {
12837
12924
  if (fs26.existsSync(SYSTEMD_UNIT)) {
12838
- spawnSync3("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
12925
+ spawnSync2("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
12839
12926
  encoding: "utf8",
12840
12927
  timeout: 5e3
12841
12928
  });
12842
- spawnSync3("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
12929
+ spawnSync2("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
12843
12930
  fs26.unlinkSync(SYSTEMD_UNIT);
12844
12931
  }
12845
12932
  }
@@ -12857,7 +12944,7 @@ function stopRunningDaemon() {
12857
12944
  try {
12858
12945
  process.kill(pid, "SIGTERM");
12859
12946
  const deadline = Date.now() + 3e3;
12860
- const pollStop = spawnSync3(
12947
+ const pollStop = spawnSync2(
12861
12948
  "sh",
12862
12949
  ["-c", `while kill -0 ${pid} 2>/dev/null; do sleep 0.1; done`],
12863
12950
  {
@@ -12889,7 +12976,7 @@ function installDaemonService() {
12889
12976
  return { ok: true, platform: "launchd", alreadyInstalled };
12890
12977
  }
12891
12978
  if (process.platform === "linux") {
12892
- const check = spawnSync3("systemctl", ["--user", "--version"], {
12979
+ const check = spawnSync2("systemctl", ["--user", "--version"], {
12893
12980
  encoding: "utf8",
12894
12981
  timeout: 2e3
12895
12982
  });
@@ -12954,7 +13041,6 @@ var init_service = __esm({
12954
13041
  // src/daemon/index.ts
12955
13042
  import fs27 from "fs";
12956
13043
  import chalk7 from "chalk";
12957
- import { spawnSync as spawnSync4 } from "child_process";
12958
13044
  function stopDaemon() {
12959
13045
  if (!fs27.existsSync(DAEMON_PID_FILE)) return console.log(chalk7.yellow("Not running."));
12960
13046
  try {
@@ -12996,15 +13082,7 @@ function daemonStatus() {
12996
13082
  processStatus = chalk7.yellow("not running (stale PID file)");
12997
13083
  }
12998
13084
  } else {
12999
- const r = spawnSync4("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
13000
- encoding: "utf8",
13001
- timeout: 500
13002
- });
13003
- if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
13004
- processStatus = chalk7.yellow(`running (orphaned \u2014 no PID file)`);
13005
- } else {
13006
- processStatus = chalk7.yellow("not running");
13007
- }
13085
+ processStatus = chalk7.yellow("not running");
13008
13086
  }
13009
13087
  console.log(`
13010
13088
  Process : ${processStatus}`);
@@ -14473,13 +14551,7 @@ async function autoStartDaemonAndWait() {
14473
14551
  for (let i = 0; i < 20; i++) {
14474
14552
  await new Promise((r) => setTimeout(r, 250));
14475
14553
  if (!isDaemonRunning()) continue;
14476
- try {
14477
- const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/settings`, {
14478
- signal: AbortSignal.timeout(500)
14479
- });
14480
- if (res.ok) return true;
14481
- } catch {
14482
- }
14554
+ if (await isDaemonReachable()) return true;
14483
14555
  }
14484
14556
  } catch {
14485
14557
  }
@@ -14498,7 +14570,7 @@ import path32 from "path";
14498
14570
  import os27 from "os";
14499
14571
 
14500
14572
  // src/undo.ts
14501
- import { spawnSync as spawnSync5, spawn as spawn4 } from "child_process";
14573
+ import { spawnSync as spawnSync3, spawn as spawn4 } from "child_process";
14502
14574
  import crypto3 from "crypto";
14503
14575
  import fs29 from "fs";
14504
14576
  import net3 from "net";
@@ -14611,7 +14683,7 @@ function ensureShadowRepo(shadowDir, cwd) {
14611
14683
  cleanOrphanedIndexFiles(shadowDir);
14612
14684
  const normalizedCwd = normalizeCwdForHash(cwd);
14613
14685
  const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
14614
- const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
14686
+ const check = spawnSync3("git", ["rev-parse", "--git-dir"], {
14615
14687
  env: shadowEnvBase,
14616
14688
  timeout: 3e3
14617
14689
  });
@@ -14637,17 +14709,17 @@ function ensureShadowRepo(shadowDir, cwd) {
14637
14709
  fs29.mkdirSync(shadowDir, { recursive: true });
14638
14710
  } catch {
14639
14711
  }
14640
- const init = spawnSync5("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
14712
+ const init = spawnSync3("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
14641
14713
  if (init.status !== 0 || init.error) {
14642
14714
  const reason = init.error ? init.error.message : init.stderr?.toString();
14643
14715
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
14644
14716
  return false;
14645
14717
  }
14646
14718
  const configFile = path30.join(shadowDir, "config");
14647
- spawnSync5("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
14719
+ spawnSync3("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
14648
14720
  timeout: 3e3
14649
14721
  });
14650
- spawnSync5("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
14722
+ spawnSync3("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
14651
14723
  timeout: 3e3
14652
14724
  });
14653
14725
  try {
@@ -14658,14 +14730,17 @@ function ensureShadowRepo(shadowDir, cwd) {
14658
14730
  }
14659
14731
  function buildGitEnv(cwd) {
14660
14732
  const shadowDir = getShadowRepoDir(cwd);
14661
- const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
14733
+ const check = spawnSync3("git", ["rev-parse", "--git-dir"], {
14662
14734
  env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
14663
14735
  timeout: 2e3
14664
14736
  });
14665
14737
  if (check.status === 0) {
14666
14738
  return { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
14667
14739
  }
14668
- return { ...process.env };
14740
+ const env = { ...process.env };
14741
+ delete env.GIT_DIR;
14742
+ delete env.GIT_WORK_TREE;
14743
+ return env;
14669
14744
  }
14670
14745
  async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
14671
14746
  let indexFile = null;
@@ -14683,11 +14758,11 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
14683
14758
  GIT_WORK_TREE: cwd,
14684
14759
  GIT_INDEX_FILE: indexFile
14685
14760
  };
14686
- spawnSync5("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
14687
- const treeRes = spawnSync5("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
14761
+ spawnSync3("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
14762
+ const treeRes = spawnSync3("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
14688
14763
  const treeHash = treeRes.stdout?.toString().trim();
14689
14764
  if (!treeHash || treeRes.status !== 0) return null;
14690
- const commitRes = spawnSync5(
14765
+ const commitRes = spawnSync3(
14691
14766
  "git",
14692
14767
  ["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
14693
14768
  { env: shadowEnv, timeout: GIT_TIMEOUT }
@@ -14699,7 +14774,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
14699
14774
  let capturedFiles = [];
14700
14775
  let capturedDiff = null;
14701
14776
  if (prevEntry) {
14702
- const filesRes = spawnSync5("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
14777
+ const filesRes = spawnSync3("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
14703
14778
  env: shadowEnv,
14704
14779
  timeout: GIT_TIMEOUT
14705
14780
  });
@@ -14709,7 +14784,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
14709
14784
  if (capturedFiles.length === 0) {
14710
14785
  return prevEntry.hash;
14711
14786
  }
14712
- const diffRes = spawnSync5("git", ["diff", prevEntry.hash, commitHash], {
14787
+ const diffRes = spawnSync3("git", ["diff", prevEntry.hash, commitHash], {
14713
14788
  env: shadowEnv,
14714
14789
  timeout: GIT_TIMEOUT
14715
14790
  });
@@ -14717,7 +14792,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
14717
14792
  capturedDiff = diffRes.stdout?.toString() || null;
14718
14793
  }
14719
14794
  } else {
14720
- const filesRes = spawnSync5("git", ["ls-tree", "-r", "--name-only", commitHash], {
14795
+ const filesRes = spawnSync3("git", ["ls-tree", "-r", "--name-only", commitHash], {
14721
14796
  env: shadowEnv,
14722
14797
  timeout: GIT_TIMEOUT
14723
14798
  });
@@ -14771,14 +14846,14 @@ function getSnapshotHistory() {
14771
14846
  function computeUndoDiff(hash, cwd) {
14772
14847
  try {
14773
14848
  const env = buildGitEnv(cwd);
14774
- const statRes = spawnSync5("git", ["diff", hash, "--stat", "--", "."], {
14849
+ const statRes = spawnSync3("git", ["diff", hash, "--stat", "--", "."], {
14775
14850
  cwd,
14776
14851
  env,
14777
14852
  timeout: GIT_TIMEOUT
14778
14853
  });
14779
14854
  const stat = statRes.stdout?.toString().trim();
14780
14855
  if (!stat || statRes.status !== 0) return null;
14781
- const diffRes = spawnSync5("git", ["diff", hash, "--", "."], {
14856
+ const diffRes = spawnSync3("git", ["diff", hash, "--", "."], {
14782
14857
  cwd,
14783
14858
  env,
14784
14859
  timeout: GIT_TIMEOUT
@@ -14797,7 +14872,7 @@ function applyUndo(hash, cwd) {
14797
14872
  try {
14798
14873
  const dir = cwd ?? process.cwd();
14799
14874
  const env = buildGitEnv(dir);
14800
- const restore = spawnSync5("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
14875
+ const restore = spawnSync3("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
14801
14876
  cwd: dir,
14802
14877
  env,
14803
14878
  timeout: GIT_TIMEOUT
@@ -14809,7 +14884,7 @@ function applyUndo(hash, cwd) {
14809
14884
  }
14810
14885
  return false;
14811
14886
  }
14812
- const lsTree = spawnSync5("git", ["ls-tree", "-r", "--name-only", hash], {
14887
+ const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], {
14813
14888
  cwd: dir,
14814
14889
  env,
14815
14890
  timeout: GIT_TIMEOUT
@@ -14828,8 +14903,8 @@ function applyUndo(hash, cwd) {
14828
14903
  `);
14829
14904
  return false;
14830
14905
  }
14831
- const tracked = spawnSync5("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
14832
- const untracked = spawnSync5("git", ["ls-files", "--others", "--exclude-standard"], {
14906
+ const tracked = spawnSync3("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
14907
+ const untracked = spawnSync3("git", ["ls-files", "--others", "--exclude-standard"], {
14833
14908
  cwd: dir,
14834
14909
  env,
14835
14910
  timeout: GIT_TIMEOUT
@@ -16078,10 +16153,12 @@ function registerDoctorCommand(program2, version2) {
16078
16153
  }
16079
16154
  section("Daemon (optional)");
16080
16155
  if (isDaemonRunning()) {
16081
- pass(`Browser dashboard running \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/`);
16156
+ pass(
16157
+ `Daemon running on ${DAEMON_HOST}:${DAEMON_PORT} \u2014 terminal & native approvals enabled`
16158
+ );
16082
16159
  } else {
16083
16160
  warn(
16084
- "Daemon not running \u2014 browser approvals unavailable",
16161
+ "Daemon not running \u2014 terminal & native approvals unavailable",
16085
16162
  "Run: node9 daemon --background"
16086
16163
  );
16087
16164
  }
@@ -17681,7 +17758,7 @@ function registerUndoCommand(program2) {
17681
17758
  // src/cli/commands/watch.ts
17682
17759
  init_daemon();
17683
17760
  import chalk19 from "chalk";
17684
- import { spawn as spawn7, spawnSync as spawnSync6 } from "child_process";
17761
+ import { spawn as spawn7, spawnSync as spawnSync4 } from "child_process";
17685
17762
  function registerWatchCommand(program2) {
17686
17763
  program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
17687
17764
  let port = DAEMON_PORT;
@@ -17727,7 +17804,7 @@ function registerWatchCommand(program2) {
17727
17804
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
17728
17805
  )
17729
17806
  );
17730
- const result = spawnSync6(cmd, args, {
17807
+ const result = spawnSync4(cmd, args, {
17731
17808
  stdio: "inherit",
17732
17809
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
17733
17810
  });
@@ -18274,7 +18351,7 @@ import readline5 from "readline";
18274
18351
  import fs39 from "fs";
18275
18352
  import os35 from "os";
18276
18353
  import path41 from "path";
18277
- import { spawnSync as spawnSync7 } from "child_process";
18354
+ import { spawnSync as spawnSync5 } from "child_process";
18278
18355
  init_core();
18279
18356
  init_daemon();
18280
18357
  init_shields();
@@ -18748,7 +18825,7 @@ function handleRuleAdd(args) {
18748
18825
  return `Rule "${name}" added to ~/.node9/config.json \u2014 verdict: ${verdict} when ${field} matches "${pattern}"`;
18749
18826
  }
18750
18827
  function runCliCommand(subArgs) {
18751
- const result = spawnSync7(process.execPath, [process.argv[1], ...subArgs], {
18828
+ const result = spawnSync5(process.execPath, [process.argv[1], ...subArgs], {
18752
18829
  encoding: "utf-8",
18753
18830
  timeout: 6e4,
18754
18831
  // Disable colors — stdout is piped (not a TTY), chalk auto-detects, but be explicit
@@ -20758,6 +20835,17 @@ program.command("tail").description("Stream live agent activity to the terminal"
20758
20835
  process.exit(1);
20759
20836
  }
20760
20837
  });
20838
+ program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
20839
+ try {
20840
+ const dashboardPath = path49.join(__dirname, "dashboard.mjs");
20841
+ const dynamicImport = new Function("id", "return import(id)");
20842
+ const mod = await dynamicImport(`file://${dashboardPath}`);
20843
+ await mod.startMonitor();
20844
+ } catch (err2) {
20845
+ console.error(chalk31.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
20846
+ process.exit(1);
20847
+ }
20848
+ });
20761
20849
  registerWatchCommand(program);
20762
20850
  registerMcpGatewayCommand(program);
20763
20851
  registerMcpServerCommand(program);