@node9/proxy 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4732,15 +4732,17 @@ async function authorizeHeadless(toolName, args, meta, options) {
4732
4732
  if (!options?.calledFromDaemon) {
4733
4733
  const actId = (0, import_crypto4.randomUUID)();
4734
4734
  const actTs = Date.now();
4735
+ const stripAnsi2 = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
4736
+ const sanitizedAgent = meta?.agent ? stripAnsi2(meta.agent).slice(0, 80) : void 0;
4737
+ const sanitizedMcpServer = meta?.mcpServer ? stripAnsi2(meta.mcpServer).slice(0, 40) : void 0;
4735
4738
  const socketOk = await notifyActivity({
4736
4739
  id: actId,
4737
4740
  ts: actTs,
4738
4741
  tool: toolName,
4739
4742
  args,
4740
4743
  status: "pending",
4741
- // Strip ANSI escape sequences — agent name comes from caller-supplied metadata
4742
- // and may be displayed in a terminal (node9 tail/watch), enabling injection.
4743
- agent: meta?.agent ? meta.agent.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80) : void 0
4744
+ agent: sanitizedAgent,
4745
+ mcpServer: sanitizedMcpServer
4744
4746
  });
4745
4747
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
4746
4748
  ...options,
@@ -4758,7 +4760,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
4758
4760
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
4759
4761
  label: result.blockedByLabel,
4760
4762
  ruleHit: result.ruleHit,
4761
- observeWouldBlock: result.observeWouldBlock
4763
+ observeWouldBlock: result.observeWouldBlock,
4764
+ agent: sanitizedAgent,
4765
+ mcpServer: sanitizedMcpServer
4762
4766
  });
4763
4767
  }
4764
4768
  return result;
@@ -10380,7 +10384,7 @@ function openBrowserLocal() {
10380
10384
  } catch {
10381
10385
  }
10382
10386
  }
10383
- async function autoStartDaemonAndWait(openBrowser2 = true) {
10387
+ async function autoStartDaemonAndWait(openBrowser = false) {
10384
10388
  if (isTestingMode()) return false;
10385
10389
  if (!import_path15.default.isAbsolute(process.argv[1])) return false;
10386
10390
  let resolvedArgv1;
@@ -10400,7 +10404,7 @@ async function autoStartDaemonAndWait(openBrowser2 = true) {
10400
10404
  env: {
10401
10405
  ...process.env,
10402
10406
  NODE9_AUTO_STARTED: "1",
10403
- ...openBrowser2 && { NODE9_BROWSER_OPENED: "1" }
10407
+ ...openBrowser && { NODE9_BROWSER_OPENED: "1" }
10404
10408
  }
10405
10409
  });
10406
10410
  child.unref();
@@ -10412,7 +10416,7 @@ async function autoStartDaemonAndWait(openBrowser2 = true) {
10412
10416
  signal: AbortSignal.timeout(500)
10413
10417
  });
10414
10418
  if (res.ok) {
10415
- if (openBrowser2) {
10419
+ if (openBrowser) {
10416
10420
  openBrowserLocal();
10417
10421
  }
10418
10422
  return true;
@@ -11801,370 +11805,681 @@ function printRuleGroup(rule, topN, drillDown, previewWidth) {
11801
11805
  );
11802
11806
  }
11803
11807
  }
11804
- function registerScanCommand(program2) {
11805
- program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 30 days)").option("--days <n>", "Scan last N days of history", "30").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").action(async (options) => {
11806
- const drillDown = options.drillDown ?? false;
11807
- const topN = drillDown ? Infinity : Math.max(1, parseInt(options.top, 10) || 5);
11808
- const previewWidth = 70;
11809
- const startDate = options.all ? null : (() => {
11810
- const d = /* @__PURE__ */ new Date();
11811
- d.setDate(d.getDate() - (parseInt(options.days, 10) || 30));
11812
- d.setHours(0, 0, 0, 0);
11813
- return d;
11814
- })();
11815
- const isWired = getAgentsStatus().some((a) => a.wired);
11816
- console.log("");
11817
- if (!isWired) {
11818
- console.log(
11819
- import_chalk3.default.bold("\u{1F6E1} node9") + import_chalk3.default.dim(" \u2014 security layer for AI coding agents")
11820
- );
11821
- console.log(
11822
- import_chalk3.default.dim(" Intercepts dangerous tool calls before they execute. No config needed.")
11823
- );
11824
- console.log("");
11808
+ function compactRuleLabel(name) {
11809
+ let label = name.replace(/^shield:[^:]+:/, "");
11810
+ label = label.replace(/^(block|review|allow)-/, "");
11811
+ return label.replace(/-+/g, "-");
11812
+ }
11813
+ function renderCompactScorecard(input) {
11814
+ const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
11815
+ const totalRisky = scan.findings.length + scan.dlpFindings.length;
11816
+ const dateRange = scan.firstDate && scan.lastDate ? `${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}` : "";
11817
+ console.log(
11818
+ import_chalk3.default.bold("\u{1F6E1} Node9 Scan") + import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.white(num(scan.sessions)) + import_chalk3.default.dim(" sessions \xB7 ") + import_chalk3.default.white(num(scan.totalToolCalls)) + import_chalk3.default.dim(" tool calls") + (dateRange ? import_chalk3.default.dim(" \xB7 " + dateRange) : "")
11819
+ );
11820
+ console.log("");
11821
+ const scoreColor = blast.score >= 80 ? import_chalk3.default.green : blast.score >= 50 ? import_chalk3.default.yellow : import_chalk3.default.red;
11822
+ const scoreSeverity = blast.score >= 80 ? "Good" : blast.score >= 50 ? "At Risk" : "Critical";
11823
+ console.log(
11824
+ import_chalk3.default.bold("Security Score: ") + scoreColor.bold(`${blast.score}/100`) + import_chalk3.default.dim(" \xB7 ") + scoreColor(scoreSeverity)
11825
+ );
11826
+ if (scan.totalCostUSD > 0) {
11827
+ console.log(
11828
+ import_chalk3.default.bold(fmtCost(scan.totalCostUSD)) + import_chalk3.default.dim(" AI spend \xB7 ") + import_chalk3.default.bold(`${totalRisky}`) + import_chalk3.default.dim(` risky operation${totalRisky !== 1 ? "s" : ""}`)
11829
+ );
11830
+ }
11831
+ console.log("");
11832
+ if (scan.dlpFindings.length > 0) {
11833
+ const patternCounts = /* @__PURE__ */ new Map();
11834
+ for (const f of scan.dlpFindings) {
11835
+ patternCounts.set(f.patternName, (patternCounts.get(f.patternName) ?? 0) + 1);
11825
11836
  }
11837
+ const topPatterns = [...patternCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name, count]) => count > 1 ? `${name} \xD7${count}` : name).join(", ");
11826
11838
  console.log(
11827
- import_chalk3.default.cyan.bold("\u{1F50D} Scanning your AI history") + import_chalk3.default.dim(" \u2014 what would node9 have caught?")
11839
+ import_chalk3.default.red("\u{1F511} ") + import_chalk3.default.red.bold(String(scan.dlpFindings.length).padEnd(4)) + import_chalk3.default.dim("credential leak".padEnd(20)) + import_chalk3.default.dim(`(${topPatterns})`)
11828
11840
  );
11829
- console.log("");
11830
- const useTTY = process.stdout.isTTY === true && process.env.NODE9_WRAPPER !== "1";
11831
- if (!useTTY) {
11832
- process.stdout.write(
11833
- " " + import_chalk3.default.dim("Scanning your history \u2014 this may take a moment...\n")
11834
- );
11841
+ }
11842
+ if (blockedCount > 0) {
11843
+ const blockedRules = [];
11844
+ for (const section of summary.sections) {
11845
+ for (const rule of section.rules) {
11846
+ if (rule.verdict === "block") {
11847
+ blockedRules.push({ name: rule.name, count: rule.findings.length });
11848
+ }
11849
+ }
11835
11850
  }
11836
- const totalFiles = countScanFiles();
11837
- let filesScanned = 0;
11838
- let linesScanned = 0;
11839
- let lastRender = 0;
11840
- const onProgress = (done) => {
11841
- filesScanned = done;
11842
- if (useTTY) renderProgressBar(filesScanned, totalFiles, linesScanned);
11843
- lastRender = Date.now();
11844
- };
11845
- const onLine = () => {
11846
- linesScanned++;
11847
- const now = Date.now();
11848
- if (useTTY && now - lastRender >= 80) {
11849
- lastRender = now;
11850
- renderProgressBar(filesScanned, totalFiles, linesScanned);
11851
+ const topBlocked = blockedRules.sort((a, b) => b.count - a.count).slice(0, 3).map(
11852
+ (r) => r.count > 1 ? `${compactRuleLabel(r.name)} \xD7${r.count}` : compactRuleLabel(r.name)
11853
+ ).join(", ");
11854
+ console.log(
11855
+ import_chalk3.default.red("\u{1F6D1} ") + import_chalk3.default.red.bold(String(blockedCount).padEnd(4)) + import_chalk3.default.dim("would have blocked".padEnd(20)) + import_chalk3.default.dim(`(${topBlocked})`)
11856
+ );
11857
+ }
11858
+ if (scan.loopFindings.length > 0) {
11859
+ const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
11860
+ const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
11861
+ const wasteParts = [];
11862
+ if (wastePct > 0) wasteParts.push(`${wastePct}% wasted`);
11863
+ if (summary.loopWastedUSD > 0) wasteParts.push("~" + fmtCost(summary.loopWastedUSD));
11864
+ const wasteSummary = wasteParts.length ? `(${wasteParts.join(" \xB7 ")})` : "";
11865
+ console.log(
11866
+ import_chalk3.default.yellow("\u{1F501} ") + import_chalk3.default.yellow.bold(String(scan.loopFindings.length).padEnd(4)) + import_chalk3.default.dim("agent loops".padEnd(20)) + import_chalk3.default.dim(wasteSummary)
11867
+ );
11868
+ }
11869
+ if (reviewCount > 0) {
11870
+ const reviewRules = [];
11871
+ for (const section of summary.sections) {
11872
+ for (const rule of section.rules) {
11873
+ if (rule.verdict !== "block") {
11874
+ reviewRules.push({ name: rule.name, count: rule.findings.length });
11875
+ }
11851
11876
  }
11852
- };
11853
- if (useTTY) renderProgressBar(0, totalFiles, 0);
11854
- const claudeScan = scanClaudeHistory(startDate, onProgress, onLine);
11855
- const geminiScan = scanGeminiHistory(
11856
- startDate,
11857
- (done) => onProgress(claudeScan.filesScanned + done),
11858
- onLine
11877
+ }
11878
+ const topReview = reviewRules.sort((a, b) => b.count - a.count).slice(0, 3).map(
11879
+ (r) => r.count > 1 ? `${compactRuleLabel(r.name)} \xD7${r.count}` : compactRuleLabel(r.name)
11880
+ ).join(", ");
11881
+ console.log(
11882
+ import_chalk3.default.yellow("\u{1F441} ") + import_chalk3.default.yellow.bold(String(reviewCount).padEnd(4)) + import_chalk3.default.dim("flagged for review".padEnd(20)) + import_chalk3.default.dim(`(${topReview})`)
11859
11883
  );
11860
- const codexScan = scanCodexHistory(
11861
- startDate,
11862
- (done) => onProgress(claudeScan.filesScanned + geminiScan.filesScanned + done),
11863
- onLine
11884
+ }
11885
+ console.log("");
11886
+ if (blastExposures > 0) {
11887
+ const categories = /* @__PURE__ */ new Set();
11888
+ for (const r of blast.reachable) {
11889
+ const lower = r.label.toLowerCase();
11890
+ if (lower.includes("ssh")) categories.add("ssh");
11891
+ else if (lower.includes("aws")) categories.add("aws");
11892
+ else if (lower.includes("gcloud") || lower.includes("gcp")) categories.add("gcp");
11893
+ else if (lower.includes("docker")) categories.add("docker");
11894
+ else if (lower.includes("netrc")) categories.add("netrc");
11895
+ else if (lower.includes("kube")) categories.add("k8s");
11896
+ else if (lower.includes("npmrc")) categories.add("npm");
11897
+ else categories.add("other");
11898
+ }
11899
+ if (blast.envFindings.length > 0) categories.add("env");
11900
+ const catList = [...categories].slice(0, 6).join(" \xD7 ");
11901
+ console.log(
11902
+ import_chalk3.default.red("\u{1F52D} ") + import_chalk3.default.dim("Blast radius".padEnd(24)) + import_chalk3.default.dim(`${catList} (${blastExposures} exposure${blastExposures !== 1 ? "s" : ""})`)
11864
11903
  );
11865
- const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
11866
- scan.dlpFindings.push(...scanShellConfig());
11867
- const summary = buildScanSummary([
11868
- { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
11869
- { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
11870
- { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
11871
- ]);
11872
- if (useTTY) process.stdout.write("\r" + " ".repeat(60) + "\r");
11873
- if (scan.filesScanned === 0) {
11874
- console.log(import_chalk3.default.yellow(" No session history found."));
11875
- console.log(
11876
- import_chalk3.default.gray(
11877
- " Supported: Claude Code (~/.claude/projects/) \xB7 Gemini CLI (~/.gemini/tmp/)\n"
11878
- )
11879
- );
11880
- return;
11904
+ console.log("");
11905
+ }
11906
+ console.log(
11907
+ import_chalk3.default.dim("\u2192 ") + import_chalk3.default.cyan("npx node9-ai scan") + import_chalk3.default.dim(" run this on your machine")
11908
+ );
11909
+ console.log(import_chalk3.default.dim("\u2192 github.com/node9-ai/node9-proxy"));
11910
+ console.log("");
11911
+ }
11912
+ function classifyRuleSeverity(name, verdict) {
11913
+ const n = name.toLowerCase();
11914
+ const criticalPatterns = [
11915
+ "rm-rf",
11916
+ "eval-remote",
11917
+ "eval-curl",
11918
+ "read-aws",
11919
+ "read-ssh",
11920
+ "read-gcp",
11921
+ "read-cred",
11922
+ "delete-repo",
11923
+ "helm-uninstall",
11924
+ "drop-table",
11925
+ "drop-database",
11926
+ "drop-collection",
11927
+ "truncate",
11928
+ "flushall",
11929
+ "flushdb",
11930
+ "pipe-shell"
11931
+ ];
11932
+ if (criticalPatterns.some((p) => n.includes(p))) return "critical";
11933
+ const highPatterns = [
11934
+ "force-push",
11935
+ "force_push",
11936
+ "git-destructive",
11937
+ "reset-hard",
11938
+ "rebase",
11939
+ "delete-branch",
11940
+ "delete-remote"
11941
+ ];
11942
+ if (highPatterns.some((p) => n.includes(p))) return "high";
11943
+ if (verdict === "block") return "high";
11944
+ return "medium";
11945
+ }
11946
+ function narrativeRuleLabel(name) {
11947
+ const stripped = compactRuleLabel(name);
11948
+ const map = {
11949
+ "read-aws": "AWS credentials read",
11950
+ "read-ssh": "SSH private key read",
11951
+ "read-gcp": "GCP credentials read",
11952
+ "read-cred": "credential file read",
11953
+ "delete-repo": "GitHub repository deletion",
11954
+ "helm-uninstall": "helm uninstall",
11955
+ "rm-rf-home": "rm -rf on home directory",
11956
+ "eval-remote": "eval of remote download",
11957
+ "pipe-shell": "curl | bash",
11958
+ "drop-table": "DROP TABLE",
11959
+ "drop-database": "DROP DATABASE",
11960
+ truncate: "TRUNCATE",
11961
+ flushall: "Redis FLUSHALL",
11962
+ flushdb: "Redis FLUSHDB",
11963
+ "force-push": "force pushes",
11964
+ "git-destructive": "destructive git operations",
11965
+ rm: "rm calls",
11966
+ sudo: "sudo calls",
11967
+ "eval-dynamic": "dynamic eval",
11968
+ "config-set": "Redis CONFIG SET"
11969
+ };
11970
+ for (const [key, label] of Object.entries(map)) {
11971
+ if (stripped.includes(key)) return label;
11972
+ }
11973
+ return stripped;
11974
+ }
11975
+ function renderNarrativeScorecard(input) {
11976
+ const { scan, summary, blast, blastExposures } = input;
11977
+ const critical = [];
11978
+ const high = [];
11979
+ const medium = [];
11980
+ if (scan.dlpFindings.length > 0) {
11981
+ const patterns = /* @__PURE__ */ new Map();
11982
+ for (const f of scan.dlpFindings) {
11983
+ patterns.set(f.patternName, (patterns.get(f.patternName) ?? 0) + 1);
11984
+ }
11985
+ const top = [...patterns.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name, n]) => n > 1 ? `${name} \xD7${n}` : name).join(", ");
11986
+ critical.push({
11987
+ label: `${scan.dlpFindings.length} credential leak${scan.dlpFindings.length !== 1 ? "s" : ""} (${top})`,
11988
+ count: scan.dlpFindings.length
11989
+ });
11990
+ }
11991
+ for (const section of summary.sections) {
11992
+ for (const rule of section.rules) {
11993
+ const sev = classifyRuleSeverity(rule.name, rule.verdict);
11994
+ const label = narrativeRuleLabel(rule.name);
11995
+ const count = rule.findings.length;
11996
+ const display = count > 1 ? `${label} \xD7${count}` : label;
11997
+ const entry = { label: display, count };
11998
+ if (sev === "critical") critical.push(entry);
11999
+ else if (sev === "high") high.push(entry);
12000
+ else medium.push(entry);
12001
+ }
12002
+ }
12003
+ if (blastExposures > 0) {
12004
+ high.push({
12005
+ label: `${blastExposures} credential file${blastExposures !== 1 ? "s" : ""} reachable on disk`,
12006
+ count: blastExposures
12007
+ });
12008
+ }
12009
+ if (scan.loopFindings.length > 0) {
12010
+ const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
12011
+ const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
12012
+ const cost = summary.loopWastedUSD > 0 ? `, ~${fmtCost(summary.loopWastedUSD)} wasted` : "";
12013
+ medium.push({
12014
+ label: `${scan.loopFindings.length} agent loops (${wastePct}% of calls${cost})`,
12015
+ count: scan.loopFindings.length
12016
+ });
12017
+ }
12018
+ const sortByCount = (a, b) => b.count - a.count;
12019
+ critical.sort(sortByCount);
12020
+ high.sort(sortByCount);
12021
+ medium.sort(sortByCount);
12022
+ const criticalCount = critical.reduce((s, e) => s + e.count, 0);
12023
+ const highCount = high.reduce((s, e) => s + e.count, 0);
12024
+ const mediumCount = medium.reduce((s, e) => s + e.count, 0);
12025
+ const dateRange = scan.firstDate && scan.lastDate ? `${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}` : "";
12026
+ console.log(
12027
+ import_chalk3.default.bold("\u{1F6E1} Node9 Scan") + import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.white(num(scan.sessions)) + import_chalk3.default.dim(" sessions") + (scan.totalCostUSD > 0 ? import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.bold(fmtCost(scan.totalCostUSD)) + import_chalk3.default.dim(" spend") : "") + (dateRange ? import_chalk3.default.dim(" \xB7 " + dateRange) : "")
12028
+ );
12029
+ console.log("");
12030
+ const scoreColor = blast.score >= 80 ? import_chalk3.default.green : blast.score >= 50 ? import_chalk3.default.yellow : import_chalk3.default.red;
12031
+ const scoreSeverity = blast.score >= 80 ? "Good" : blast.score >= 50 ? "At Risk" : "Critical";
12032
+ console.log(
12033
+ (blast.score < 50 ? import_chalk3.default.red.bold("\u26A0 ") : "") + import_chalk3.default.bold("Security Score: ") + scoreColor.bold(`${blast.score}/100`) + import_chalk3.default.dim(" \xB7 ") + scoreColor(scoreSeverity)
12034
+ );
12035
+ console.log("");
12036
+ if (criticalCount > 0) {
12037
+ console.log(
12038
+ import_chalk3.default.red.bold(" \u{1F534} CRITICAL ") + import_chalk3.default.red(`${criticalCount} finding${criticalCount !== 1 ? "s" : ""}`)
12039
+ );
12040
+ for (const entry of critical.slice(0, 5)) {
12041
+ console.log(import_chalk3.default.dim(" \u2022 ") + import_chalk3.default.red(entry.label));
12042
+ }
12043
+ if (critical.length > 5) {
12044
+ const remaining = critical.length - 5;
12045
+ console.log(import_chalk3.default.dim(` \u2022 \u2026 and ${remaining} more`));
11881
12046
  }
11882
- const rangeLabel = options.all ? import_chalk3.default.dim("all time") : import_chalk3.default.dim(`last ${options.days ?? 30} days`);
11883
- const dateRange = scan.firstDate && scan.lastDate ? import_chalk3.default.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
11884
- const breakdownParts = [];
11885
- if (claudeScan.sessions > 0)
11886
- breakdownParts.push(import_chalk3.default.cyan(String(claudeScan.sessions)) + import_chalk3.default.dim(" Claude"));
11887
- if (geminiScan.sessions > 0)
11888
- breakdownParts.push(import_chalk3.default.blue(String(geminiScan.sessions)) + import_chalk3.default.dim(" Gemini"));
11889
- if (codexScan.sessions > 0)
11890
- breakdownParts.push(import_chalk3.default.magenta(String(codexScan.sessions)) + import_chalk3.default.dim(" Codex"));
11891
- const sessionBreakdown = breakdownParts.length > 1 ? import_chalk3.default.dim("(") + breakdownParts.join(import_chalk3.default.dim(" \xB7 ")) + import_chalk3.default.dim(")") : "";
12047
+ console.log("");
12048
+ }
12049
+ if (highCount > 0) {
11892
12050
  console.log(
11893
- " " + import_chalk3.default.white(num(scan.sessions)) + import_chalk3.default.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + import_chalk3.default.white(num(scan.totalToolCalls)) + import_chalk3.default.dim(" tool calls ") + import_chalk3.default.white(num(scan.bashCalls)) + import_chalk3.default.dim(" bash commands ") + rangeLabel + dateRange
12051
+ import_chalk3.default.yellow.bold(" \u{1F7E1} HIGH ") + import_chalk3.default.yellow(`${highCount} finding${highCount !== 1 ? "s" : ""}`)
11894
12052
  );
12053
+ for (const entry of high.slice(0, 5)) {
12054
+ console.log(import_chalk3.default.dim(" \u2022 ") + import_chalk3.default.yellow(entry.label));
12055
+ }
12056
+ if (high.length > 5) {
12057
+ const remaining = high.length - 5;
12058
+ console.log(import_chalk3.default.dim(` \u2022 \u2026 and ${remaining} more`));
12059
+ }
11895
12060
  console.log("");
11896
- const totalFindings = scan.findings.length;
11897
- const blockedCount = scan.findings.filter((f) => f.source.rule.verdict === "block").length;
11898
- const reviewCount = totalFindings - blockedCount;
11899
- if (totalFindings === 0 && scan.dlpFindings.length === 0) {
11900
- console.log(import_chalk3.default.green(" \u2705 No risky operations found in your history."));
11901
- console.log(
11902
- import_chalk3.default.dim(" node9 is still worth running \u2014 it monitors every tool call in real time.\n")
11903
- );
11904
- } else {
11905
- const totalRisky = totalFindings + scan.dlpFindings.length;
11906
- const heroLine = isWired ? import_chalk3.default.bold(
11907
- ` Found ${import_chalk3.default.yellow(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} in your history`
11908
- ) : import_chalk3.default.bold(
11909
- ` ${import_chalk3.default.red.bold(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} found \u2014 none were blocked`
11910
- );
11911
- console.log(heroLine);
11912
- console.log("");
11913
- if (scan.totalCostUSD > 0) {
12061
+ }
12062
+ if (mediumCount > 0) {
12063
+ console.log(
12064
+ import_chalk3.default.bold(" \u{1F7E2} MEDIUM ") + import_chalk3.default.dim(`${mediumCount} finding${mediumCount !== 1 ? "s" : ""}`)
12065
+ );
12066
+ for (const entry of medium.slice(0, 5)) {
12067
+ console.log(import_chalk3.default.dim(" \u2022 ") + import_chalk3.default.dim(entry.label));
12068
+ }
12069
+ if (medium.length > 5) {
12070
+ const remaining = medium.length - 5;
12071
+ console.log(import_chalk3.default.dim(` \u2022 \u2026 and ${remaining} more`));
12072
+ }
12073
+ console.log("");
12074
+ }
12075
+ console.log(
12076
+ import_chalk3.default.dim("\u2192 ") + import_chalk3.default.cyan("npx node9-ai scan") + import_chalk3.default.dim(" run this on your machine")
12077
+ );
12078
+ console.log(import_chalk3.default.dim("\u2192 github.com/node9-ai/node9-proxy"));
12079
+ console.log("");
12080
+ }
12081
+ function registerScanCommand(program2) {
12082
+ program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 30 days)").option("--days <n>", "Scan last N days of history", "30").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").action(
12083
+ async (options) => {
12084
+ const drillDown = options.drillDown ?? false;
12085
+ const topN = drillDown ? Infinity : Math.max(1, parseInt(options.top, 10) || 5);
12086
+ const previewWidth = 70;
12087
+ const startDate = options.all ? null : (() => {
12088
+ const d = /* @__PURE__ */ new Date();
12089
+ d.setDate(d.getDate() - (parseInt(options.days, 10) || 30));
12090
+ d.setHours(0, 0, 0, 0);
12091
+ return d;
12092
+ })();
12093
+ const isWired = getAgentsStatus().some((a) => a.wired);
12094
+ const screenshotMode = options.compact || options.narrative;
12095
+ if (!screenshotMode) {
12096
+ console.log("");
12097
+ if (!isWired) {
12098
+ console.log(
12099
+ import_chalk3.default.bold("\u{1F6E1} node9") + import_chalk3.default.dim(" \u2014 security layer for AI coding agents")
12100
+ );
12101
+ console.log(
12102
+ import_chalk3.default.dim(" Intercepts dangerous tool calls before they execute. No config needed.")
12103
+ );
12104
+ console.log("");
12105
+ }
11914
12106
  console.log(
11915
- " " + import_chalk3.default.bold(fmtCost(scan.totalCostUSD)) + import_chalk3.default.dim(" AI spend \xB7 ") + import_chalk3.default.dim(`${totalRisky} risky operations`)
12107
+ import_chalk3.default.cyan.bold("\u{1F50D} Scanning your AI history") + import_chalk3.default.dim(" \u2014 what would node9 have caught?")
11916
12108
  );
12109
+ console.log("");
11917
12110
  }
11918
- if (scan.dlpFindings.length > 0) {
11919
- const earlyLabel = scan.sessionsWithEarlySecrets > 0 ? import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.red(
11920
- `${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
11921
- ) : "";
11922
- console.log(
11923
- " " + import_chalk3.default.red("\u{1F511} Credential leak") + " " + import_chalk3.default.red.bold(String(scan.dlpFindings.length).padStart(5)) + import_chalk3.default.dim(" secret detected in history or shell config") + earlyLabel
12111
+ const useTTY = process.stdout.isTTY === true && process.env.NODE9_WRAPPER !== "1";
12112
+ if (!useTTY && !screenshotMode) {
12113
+ process.stdout.write(
12114
+ " " + import_chalk3.default.dim("Scanning your history \u2014 this may take a moment...\n")
11924
12115
  );
11925
12116
  }
11926
- if (blockedCount > 0) {
12117
+ const totalFiles = countScanFiles();
12118
+ let filesScanned = 0;
12119
+ let linesScanned = 0;
12120
+ let lastRender = 0;
12121
+ const onProgress = (done) => {
12122
+ filesScanned = done;
12123
+ if (useTTY) renderProgressBar(filesScanned, totalFiles, linesScanned);
12124
+ lastRender = Date.now();
12125
+ };
12126
+ const onLine = () => {
12127
+ linesScanned++;
12128
+ const now = Date.now();
12129
+ if (useTTY && now - lastRender >= 80) {
12130
+ lastRender = now;
12131
+ renderProgressBar(filesScanned, totalFiles, linesScanned);
12132
+ }
12133
+ };
12134
+ if (useTTY) renderProgressBar(0, totalFiles, 0);
12135
+ const claudeScan = scanClaudeHistory(startDate, onProgress, onLine);
12136
+ const geminiScan = scanGeminiHistory(
12137
+ startDate,
12138
+ (done) => onProgress(claudeScan.filesScanned + done),
12139
+ onLine
12140
+ );
12141
+ const codexScan = scanCodexHistory(
12142
+ startDate,
12143
+ (done) => onProgress(claudeScan.filesScanned + geminiScan.filesScanned + done),
12144
+ onLine
12145
+ );
12146
+ const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
12147
+ scan.dlpFindings.push(...scanShellConfig());
12148
+ const summary = buildScanSummary([
12149
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
12150
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
12151
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
12152
+ ]);
12153
+ if (useTTY) process.stdout.write("\r" + " ".repeat(60) + "\r");
12154
+ if (scan.filesScanned === 0) {
12155
+ console.log(import_chalk3.default.yellow(" No session history found."));
11927
12156
  console.log(
11928
- " " + import_chalk3.default.red("\u{1F6D1} Would have blocked") + " " + import_chalk3.default.red.bold(String(blockedCount).padStart(5)) + import_chalk3.default.dim(" operations stopped before execution")
12157
+ import_chalk3.default.gray(
12158
+ " Supported: Claude Code (~/.claude/projects/) \xB7 Gemini CLI (~/.gemini/tmp/)\n"
12159
+ )
11929
12160
  );
12161
+ return;
11930
12162
  }
11931
- if (scan.loopFindings.length > 0) {
11932
- const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
11933
- const loopRatio = scan.totalToolCalls > 0 ? import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.yellow(
11934
- Math.round(wastedCalls / scan.totalToolCalls * 100) + "% of calls wasted"
11935
- ) : "";
11936
- const loopCost = summary.loopWastedUSD > 0 ? import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.yellow("~" + fmtCost(summary.loopWastedUSD)) : "";
12163
+ const rangeLabel = options.all ? import_chalk3.default.dim("all time") : import_chalk3.default.dim(`last ${options.days ?? 30} days`);
12164
+ const dateRange = scan.firstDate && scan.lastDate ? import_chalk3.default.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
12165
+ const breakdownParts = [];
12166
+ if (claudeScan.sessions > 0)
12167
+ breakdownParts.push(import_chalk3.default.cyan(String(claudeScan.sessions)) + import_chalk3.default.dim(" Claude"));
12168
+ if (geminiScan.sessions > 0)
12169
+ breakdownParts.push(import_chalk3.default.blue(String(geminiScan.sessions)) + import_chalk3.default.dim(" Gemini"));
12170
+ if (codexScan.sessions > 0)
12171
+ breakdownParts.push(import_chalk3.default.magenta(String(codexScan.sessions)) + import_chalk3.default.dim(" Codex"));
12172
+ const sessionBreakdown = breakdownParts.length > 1 ? import_chalk3.default.dim("(") + breakdownParts.join(import_chalk3.default.dim(" \xB7 ")) + import_chalk3.default.dim(")") : "";
12173
+ if (!screenshotMode) {
11937
12174
  console.log(
11938
- " " + import_chalk3.default.yellow("\u{1F501} Loop detected") + " " + import_chalk3.default.yellow.bold(String(scan.loopFindings.length).padStart(5)) + import_chalk3.default.dim(" repeated tool call patterns found") + loopRatio + loopCost
12175
+ " " + import_chalk3.default.white(num(scan.sessions)) + import_chalk3.default.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + import_chalk3.default.white(num(scan.totalToolCalls)) + import_chalk3.default.dim(" tool calls ") + import_chalk3.default.white(num(scan.bashCalls)) + import_chalk3.default.dim(" bash commands ") + rangeLabel + dateRange
11939
12176
  );
12177
+ console.log("");
11940
12178
  }
11941
- if (reviewCount > 0) {
11942
- console.log(
11943
- " " + import_chalk3.default.yellow("\u{1F441} Would have flagged") + " " + import_chalk3.default.yellow.bold(String(reviewCount).padStart(5)) + import_chalk3.default.dim(" sent to you for approval")
11944
- );
12179
+ const totalFindings = scan.findings.length;
12180
+ const blockedCount = scan.findings.filter((f) => f.source.rule.verdict === "block").length;
12181
+ const reviewCount = totalFindings - blockedCount;
12182
+ const blast = runBlast();
12183
+ const blastExposures = blast.reachable.length + blast.envFindings.length;
12184
+ if (options.compact) {
12185
+ renderCompactScorecard({
12186
+ scan,
12187
+ summary,
12188
+ blast,
12189
+ blastExposures,
12190
+ blockedCount,
12191
+ reviewCount
12192
+ });
12193
+ return;
11945
12194
  }
11946
- console.log("");
11947
- if (scan.dlpFindings.length > 0) {
11948
- console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
12195
+ if (options.narrative) {
12196
+ renderNarrativeScorecard({
12197
+ scan,
12198
+ summary,
12199
+ blast,
12200
+ blastExposures,
12201
+ blockedCount,
12202
+ reviewCount
12203
+ });
12204
+ return;
12205
+ }
12206
+ if (totalFindings === 0 && scan.dlpFindings.length === 0) {
12207
+ console.log(import_chalk3.default.green(" \u2705 No risky operations found in your history."));
11949
12208
  console.log(
11950
- " " + import_chalk3.default.red.bold("\u{1F511} Credential Leaks") + import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.red(
11951
- `${num(scan.dlpFindings.length)} secret${scan.dlpFindings.length !== 1 ? "s" : ""} found in plain text`
12209
+ import_chalk3.default.dim(
12210
+ " node9 is still worth running \u2014 it monitors every tool call in real time.\n"
11952
12211
  )
11953
12212
  );
11954
- const sortedDlp = sortDlpFindingsByPriority(scan.dlpFindings);
11955
- const recurringPatterns = buildRecurringPatternSet(scan.dlpFindings);
11956
- const shownDlp = drillDown ? sortedDlp : sortedDlp.slice(0, topN);
11957
- for (const f of shownDlp) {
11958
- const stale = isStaleFinding(f.timestamp);
11959
- const ts = f.timestamp ? import_chalk3.default.dim(fmtTs(f.timestamp) + " ") : "";
11960
- const proj = import_chalk3.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
11961
- const agentBadge = f.agent === "gemini" ? import_chalk3.default.blue("[Gemini] ") : f.agent === "codex" ? import_chalk3.default.magenta("[Codex] ") : f.agent === "shell" ? import_chalk3.default.yellow("[Shell] ") : import_chalk3.default.cyan("[Claude] ");
11962
- const sessionSuffix = f.sessionId ? import_chalk3.default.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
11963
- const recurringBadge = recurringPatterns.has(f.patternName) ? import_chalk3.default.red.bold(" \u26A0\uFE0F recurring ") : "";
11964
- const patternDisplay = stale ? import_chalk3.default.dim(f.patternName) : import_chalk3.default.yellow(f.patternName);
11965
- const sampleDisplay = stale ? import_chalk3.default.dim(f.redactedSample) : import_chalk3.default.gray(f.redactedSample);
11966
- const entryBadge = import_chalk3.default.dim(` [${entryPathLabel(f.toolName)}]`);
11967
- const leadIcon = stale ? import_chalk3.default.dim("\u{1F6A8}") : "\u{1F6A8}";
12213
+ } else {
12214
+ const totalRisky = totalFindings + scan.dlpFindings.length;
12215
+ const scoreSeverity = blast.score >= 80 ? import_chalk3.default.green("Good") : blast.score >= 50 ? import_chalk3.default.yellow("At Risk") : import_chalk3.default.red.bold("Critical");
12216
+ const scoreColor = blast.score >= 80 ? import_chalk3.default.green : blast.score >= 50 ? import_chalk3.default.yellow : import_chalk3.default.red;
12217
+ console.log(
12218
+ " " + (blast.score < 50 ? import_chalk3.default.red.bold("\u26A0 ") : "") + import_chalk3.default.bold("Security Score ") + scoreColor.bold(`${blast.score}/100`) + " " + scoreSeverity + import_chalk3.default.dim(" \xB7 ") + (totalRisky > 0 ? import_chalk3.default.red.bold(`${totalRisky} risky operation${totalRisky !== 1 ? "s" : ""}`) : import_chalk3.default.green("No risky operations"))
12219
+ );
12220
+ const cardParts = [];
12221
+ if (scan.dlpFindings.length > 0) {
12222
+ cardParts.push(
12223
+ import_chalk3.default.red("\u{1F511} ") + import_chalk3.default.red.bold(String(scan.dlpFindings.length)) + import_chalk3.default.dim(` leak${scan.dlpFindings.length !== 1 ? "s" : ""}`)
12224
+ );
12225
+ }
12226
+ if (blockedCount > 0) {
12227
+ cardParts.push(
12228
+ import_chalk3.default.red("\u{1F6D1} ") + import_chalk3.default.red.bold(String(blockedCount)) + import_chalk3.default.dim(" blocked")
12229
+ );
12230
+ }
12231
+ if (scan.loopFindings.length > 0) {
12232
+ const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
12233
+ const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
12234
+ const wasteSuffix = wastePct > 0 ? import_chalk3.default.dim(` (${wastePct}% wasted)`) : "";
12235
+ cardParts.push(
12236
+ import_chalk3.default.yellow("\u{1F501} ") + import_chalk3.default.yellow.bold(String(scan.loopFindings.length)) + import_chalk3.default.dim(" loops") + wasteSuffix
12237
+ );
12238
+ }
12239
+ if (reviewCount > 0) {
12240
+ cardParts.push(
12241
+ import_chalk3.default.yellow("\u{1F441} ") + import_chalk3.default.yellow.bold(String(reviewCount)) + import_chalk3.default.dim(" flagged")
12242
+ );
12243
+ }
12244
+ if (blastExposures > 0) {
12245
+ cardParts.push(
12246
+ import_chalk3.default.red("\u{1F52D} ") + import_chalk3.default.red.bold(String(blastExposures)) + import_chalk3.default.dim(" exposures")
12247
+ );
12248
+ }
12249
+ if (cardParts.length > 0) {
12250
+ console.log(" " + cardParts.join(import_chalk3.default.dim(" ")));
12251
+ }
12252
+ if (scan.totalCostUSD > 0) {
11968
12253
  console.log(
11969
- ` ${leadIcon} ${ts}${proj}${agentBadge}` + patternDisplay + recurringBadge + import_chalk3.default.dim(" ") + sampleDisplay + entryBadge + sessionSuffix
12254
+ " " + import_chalk3.default.dim("AI spend ") + import_chalk3.default.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? import_chalk3.default.dim(" \xB7 wasted on loops ") + import_chalk3.default.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
11970
12255
  );
11971
12256
  }
11972
- if (!drillDown && scan.dlpFindings.length > topN) {
12257
+ if (scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
11973
12258
  console.log(
11974
- import_chalk3.default.dim(
11975
- ` \u2026 and ${scan.dlpFindings.length - topN} more (--drill-down for full list)`
12259
+ " " + import_chalk3.default.dim(
12260
+ `${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
11976
12261
  )
11977
12262
  );
11978
12263
  }
11979
12264
  console.log("");
11980
- }
11981
- const blockedRuleSections = summary.sections.map((s) => ({ ...s, rules: s.rules.filter((r) => r.verdict === "block") })).filter((s) => s.rules.length > 0);
11982
- if (blockedRuleSections.length > 0) {
11983
- console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
11984
- console.log(
11985
- " " + import_chalk3.default.red.bold("\u{1F6D1} Blocked") + import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.red(
11986
- `${blockedCount} operation${blockedCount !== 1 ? "s" : ""} node9 would have stopped`
11987
- )
11988
- );
11989
- for (const section of blockedRuleSections) {
11990
- for (const rule of section.rules) {
11991
- printRuleGroup(rule, topN, drillDown, previewWidth);
11992
- }
11993
- }
11994
- console.log("");
11995
- }
11996
- if (scan.loopFindings.length > 0) {
11997
- console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
11998
- const loopCostLabel = summary.loopWastedUSD > 0 ? import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.yellow("~" + fmtCost(summary.loopWastedUSD) + " wasted") : "";
11999
- console.log(
12000
- " " + import_chalk3.default.yellow.bold("\u{1F501} Agent Loops") + import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.yellow(
12001
- `${num(scan.loopFindings.length)} repeated pattern${scan.loopFindings.length !== 1 ? "s" : ""} found`
12002
- ) + loopCostLabel
12003
- );
12004
- const shownLoops = drillDown ? scan.loopFindings : scan.loopFindings.slice(0, topN);
12005
- for (const f of shownLoops) {
12006
- const ts = f.timestamp ? import_chalk3.default.dim(fmtTs(f.timestamp) + " ") : "";
12007
- const proj = import_chalk3.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
12008
- const agentBadge = f.agent === "gemini" ? import_chalk3.default.blue("[Gemini] ") : f.agent === "codex" ? import_chalk3.default.magenta("[Codex] ") : import_chalk3.default.cyan("[Claude] ");
12009
- const sessionSuffix = f.sessionId ? import_chalk3.default.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
12265
+ if (scan.dlpFindings.length > 0) {
12266
+ console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
12010
12267
  console.log(
12011
- ` ${ts}${proj}${agentBadge}` + import_chalk3.default.yellow(f.toolName) + import_chalk3.default.dim(` \xD7${f.count} `) + import_chalk3.default.gray(f.commandPreview) + sessionSuffix
12268
+ " " + import_chalk3.default.red.bold("\u{1F511} Credential Leaks") + import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.red(
12269
+ `${num(scan.dlpFindings.length)} secret${scan.dlpFindings.length !== 1 ? "s" : ""} found in plain text`
12270
+ )
12012
12271
  );
12272
+ const sortedDlp = sortDlpFindingsByPriority(scan.dlpFindings);
12273
+ const recurringPatterns = buildRecurringPatternSet(scan.dlpFindings);
12274
+ const shownDlp = drillDown ? sortedDlp : sortedDlp.slice(0, topN);
12275
+ for (const f of shownDlp) {
12276
+ const stale = isStaleFinding(f.timestamp);
12277
+ const ts = f.timestamp ? import_chalk3.default.dim(fmtTs(f.timestamp) + " ") : "";
12278
+ const proj = import_chalk3.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
12279
+ const agentBadge = f.agent === "gemini" ? import_chalk3.default.blue("[Gemini] ") : f.agent === "codex" ? import_chalk3.default.magenta("[Codex] ") : f.agent === "shell" ? import_chalk3.default.yellow("[Shell] ") : import_chalk3.default.cyan("[Claude] ");
12280
+ const sessionSuffix = f.sessionId ? import_chalk3.default.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
12281
+ const recurringBadge = recurringPatterns.has(f.patternName) ? import_chalk3.default.red.bold(" \u26A0\uFE0F recurring ") : "";
12282
+ const patternDisplay = stale ? import_chalk3.default.dim(f.patternName) : import_chalk3.default.yellow(f.patternName);
12283
+ const sampleDisplay = stale ? import_chalk3.default.dim(f.redactedSample) : import_chalk3.default.gray(f.redactedSample);
12284
+ const entryBadge = import_chalk3.default.dim(` [${entryPathLabel(f.toolName)}]`);
12285
+ const leadIcon = stale ? import_chalk3.default.dim("\u{1F6A8}") : "\u{1F6A8}";
12286
+ console.log(
12287
+ ` ${leadIcon} ${ts}${proj}${agentBadge}` + patternDisplay + recurringBadge + import_chalk3.default.dim(" ") + sampleDisplay + entryBadge + sessionSuffix
12288
+ );
12289
+ }
12290
+ if (!drillDown && scan.dlpFindings.length > topN) {
12291
+ console.log(
12292
+ import_chalk3.default.dim(
12293
+ ` \u2026 and ${scan.dlpFindings.length - topN} more (--drill-down for full list)`
12294
+ )
12295
+ );
12296
+ }
12297
+ console.log("");
12013
12298
  }
12014
- if (!drillDown && scan.loopFindings.length > topN) {
12299
+ const blockedRuleSections = summary.sections.map((s) => ({ ...s, rules: s.rules.filter((r) => r.verdict === "block") })).filter((s) => s.rules.length > 0);
12300
+ if (blockedRuleSections.length > 0) {
12301
+ console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
12015
12302
  console.log(
12016
- import_chalk3.default.dim(
12017
- ` \u2026 and ${scan.loopFindings.length - topN} more (--drill-down for full list)`
12303
+ " " + import_chalk3.default.red.bold("\u{1F6D1} Blocked") + import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.red(
12304
+ `${blockedCount} operation${blockedCount !== 1 ? "s" : ""} node9 would have stopped`
12018
12305
  )
12019
12306
  );
12020
- }
12021
- const stuckTools = computeStuckTools(scan.loopFindings);
12022
- if (stuckTools.length > 0) {
12307
+ for (const section of blockedRuleSections) {
12308
+ for (const rule of section.rules) {
12309
+ printRuleGroup(rule, topN, drillDown, previewWidth);
12310
+ }
12311
+ }
12023
12312
  console.log("");
12024
- console.log(" " + import_chalk3.default.dim("Most stuck tools:"));
12025
- for (const t of stuckTools) {
12313
+ }
12314
+ if (scan.loopFindings.length > 0) {
12315
+ console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
12316
+ const loopCostLabel = summary.loopWastedUSD > 0 ? import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.yellow("~" + fmtCost(summary.loopWastedUSD) + " wasted") : "";
12317
+ console.log(
12318
+ " " + import_chalk3.default.yellow.bold("\u{1F501} Agent Loops") + import_chalk3.default.dim(" \xB7 ") + import_chalk3.default.yellow(
12319
+ `${num(scan.loopFindings.length)} repeated pattern${scan.loopFindings.length !== 1 ? "s" : ""} found`
12320
+ ) + loopCostLabel
12321
+ );
12322
+ const shownLoops = drillDown ? scan.loopFindings : scan.loopFindings.slice(0, topN);
12323
+ for (const f of shownLoops) {
12324
+ const ts = f.timestamp ? import_chalk3.default.dim(fmtTs(f.timestamp) + " ") : "";
12325
+ const proj = import_chalk3.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
12326
+ const agentBadge = f.agent === "gemini" ? import_chalk3.default.blue("[Gemini] ") : f.agent === "codex" ? import_chalk3.default.magenta("[Codex] ") : import_chalk3.default.cyan("[Claude] ");
12327
+ const sessionSuffix = f.sessionId ? import_chalk3.default.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
12328
+ console.log(
12329
+ ` ${ts}${proj}${agentBadge}` + import_chalk3.default.yellow(f.toolName) + import_chalk3.default.dim(` \xD7${f.count} `) + import_chalk3.default.gray(f.commandPreview) + sessionSuffix
12330
+ );
12331
+ }
12332
+ if (!drillDown && scan.loopFindings.length > topN) {
12026
12333
  console.log(
12027
- import_chalk3.default.dim(" ") + import_chalk3.default.yellow(t.toolName.padEnd(8)) + import_chalk3.default.dim(" ") + import_chalk3.default.dim(`\xD7${t.waste} repeats`.padEnd(14)) + import_chalk3.default.dim(` (${t.pct}%)`)
12334
+ import_chalk3.default.dim(
12335
+ ` \u2026 and ${scan.loopFindings.length - topN} more (--drill-down for full list)`
12336
+ )
12028
12337
  );
12029
12338
  }
12339
+ const stuckTools = computeStuckTools(scan.loopFindings);
12340
+ if (stuckTools.length > 0) {
12341
+ console.log("");
12342
+ console.log(" " + import_chalk3.default.dim("Most stuck tools:"));
12343
+ for (const t of stuckTools) {
12344
+ console.log(
12345
+ import_chalk3.default.dim(" ") + import_chalk3.default.yellow(t.toolName.padEnd(8)) + import_chalk3.default.dim(" ") + import_chalk3.default.dim(`\xD7${t.waste} repeats`.padEnd(14)) + import_chalk3.default.dim(` (${t.pct}%)`)
12346
+ );
12347
+ }
12348
+ }
12349
+ console.log("");
12350
+ }
12351
+ for (const section of summary.sections) {
12352
+ const reviewRules = section.rules.filter((r) => r.verdict !== "block");
12353
+ if (reviewRules.length === 0) continue;
12354
+ const enableHint = section.shieldKey ? import_chalk3.default.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
12355
+ console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
12356
+ console.log(
12357
+ " " + import_chalk3.default.bold(section.label) + (section.subtitle ? import_chalk3.default.dim(` \xB7 ${section.subtitle}`) : "") + " " + import_chalk3.default.yellow(`${section.reviewCount} review`) + enableHint
12358
+ );
12359
+ for (const rule of reviewRules) {
12360
+ printRuleGroup(rule, topN, drillDown, previewWidth);
12361
+ }
12362
+ console.log("");
12363
+ }
12364
+ const activeShieldIds = new Set(
12365
+ summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
12366
+ );
12367
+ const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
12368
+ if (emptyShields.length > 0) {
12369
+ console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
12370
+ console.log(
12371
+ " " + import_chalk3.default.bold("\u{1F6E1} Inactive Shields") + import_chalk3.default.dim(" \xB7 enable for more coverage")
12372
+ );
12373
+ console.log(" " + import_chalk3.default.dim(emptyShields.join(" \xB7 ")));
12374
+ console.log(" " + import_chalk3.default.dim("\u2192 node9 shield enable <name> to activate"));
12375
+ console.log("");
12030
12376
  }
12031
- console.log("");
12032
12377
  }
12033
- for (const section of summary.sections) {
12034
- const reviewRules = section.rules.filter((r) => r.verdict !== "block");
12035
- if (reviewRules.length === 0) continue;
12036
- const enableHint = section.shieldKey ? import_chalk3.default.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
12378
+ if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
12037
12379
  console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
12038
12380
  console.log(
12039
- " " + import_chalk3.default.bold(section.label) + (section.subtitle ? import_chalk3.default.dim(` \xB7 ${section.subtitle}`) : "") + " " + import_chalk3.default.yellow(`${section.reviewCount} review`) + enableHint
12381
+ " " + import_chalk3.default.bold("\u{1F52D} Blast Radius") + import_chalk3.default.dim(
12382
+ ` \xB7 ${blastExposures} exposure${blastExposures !== 1 ? "s" : ""} an AI agent can reach right now`
12383
+ )
12040
12384
  );
12041
- for (const rule of reviewRules) {
12042
- printRuleGroup(rule, topN, drillDown, previewWidth);
12385
+ console.log("");
12386
+ if (blast.reachable.length > 0) {
12387
+ for (const p of blast.reachable) {
12388
+ console.log(
12389
+ " " + import_chalk3.default.red("\u2717 ") + import_chalk3.default.yellow(p.label.padEnd(38)) + import_chalk3.default.dim(p.description)
12390
+ );
12391
+ }
12392
+ }
12393
+ if (blast.envFindings.length > 0) {
12394
+ for (const f of blast.envFindings) {
12395
+ console.log(
12396
+ " " + import_chalk3.default.red("\u2717 ") + import_chalk3.default.yellow(f.key.padEnd(38)) + import_chalk3.default.dim(f.patternName + " in environment")
12397
+ );
12398
+ }
12043
12399
  }
12044
12400
  console.log("");
12045
- }
12046
- const activeShieldIds = new Set(
12047
- summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
12048
- );
12049
- const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
12050
- if (emptyShields.length > 0) {
12051
- console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
12052
12401
  console.log(
12053
- " " + import_chalk3.default.bold("\u{1F6E1} Inactive Shields") + import_chalk3.default.dim(" \xB7 enable for more coverage")
12402
+ import_chalk3.default.dim(
12403
+ " \u2192 Run `node9 shield enable project-jail` to block agent access to these files."
12404
+ )
12054
12405
  );
12055
- console.log(" " + import_chalk3.default.dim(emptyShields.join(" \xB7 ")));
12056
- console.log(" " + import_chalk3.default.dim("\u2192 node9 shield enable <name> to activate"));
12057
12406
  console.log("");
12058
12407
  }
12059
- }
12060
- const blast = runBlast();
12061
- if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
12062
- console.log(" " + import_chalk3.default.dim("\u2500".repeat(70)));
12063
- console.log(
12064
- " " + import_chalk3.default.bold("\u{1F52D} Blast Radius") + import_chalk3.default.dim(" \xB7 what an AI agent can reach right now")
12065
- );
12066
- console.log("");
12067
- if (blast.reachable.length > 0) {
12068
- for (const p of blast.reachable) {
12408
+ if (isWired) {
12409
+ console.log(import_chalk3.default.green(" \u2705 node9 is active \u2014 your future sessions are protected."));
12410
+ console.log(
12411
+ import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan("node9 report") + import_chalk3.default.dim(" to see live protection stats.")
12412
+ );
12413
+ if (drillDown) {
12069
12414
  console.log(
12070
- " " + import_chalk3.default.red("\u2717 ") + import_chalk3.default.yellow(p.label.padEnd(38)) + import_chalk3.default.dim(p.description)
12415
+ import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan("node9 sessions --detail <session-id>") + import_chalk3.default.dim(" to see the full conversation for any session above.")
12071
12416
  );
12072
- }
12073
- }
12074
- if (blast.envFindings.length > 0) {
12075
- for (const f of blast.envFindings) {
12417
+ } else {
12076
12418
  console.log(
12077
- " " + import_chalk3.default.red("\u2717 ") + import_chalk3.default.yellow(f.key.padEnd(38)) + import_chalk3.default.dim(f.patternName + " in environment")
12419
+ import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan("node9 scan --drill-down") + import_chalk3.default.dim(" to see full commands and session IDs.")
12078
12420
  );
12079
12421
  }
12080
- }
12081
- console.log("");
12082
- console.log(
12083
- " Security Score: " + scoreLabel(blast.score) + import_chalk3.default.dim(
12084
- ` (${blast.reachable.length + blast.envFindings.length} exposure${blast.reachable.length + blast.envFindings.length !== 1 ? "s" : ""})`
12085
- )
12086
- );
12087
- console.log(
12088
- import_chalk3.default.dim(
12089
- "\n Run `node9 shield enable project-jail` to block agent access to these files."
12090
- )
12091
- );
12092
- console.log("");
12093
- }
12094
- if (isWired) {
12095
- console.log(import_chalk3.default.green(" \u2705 node9 is active \u2014 your future sessions are protected."));
12096
- console.log(
12097
- import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan("node9 report") + import_chalk3.default.dim(" to see live protection stats.")
12098
- );
12099
- if (drillDown) {
12100
- console.log(
12101
- import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan("node9 sessions --detail <session-id>") + import_chalk3.default.dim(" to see the full conversation for any session above.")
12102
- );
12103
12422
  } else {
12423
+ const riskySummary = totalFindings + scan.dlpFindings.length;
12424
+ if (riskySummary > 0) {
12425
+ console.log(
12426
+ import_chalk3.default.yellow.bold(
12427
+ ` \u26A1 ${riskySummary} operation${riskySummary !== 1 ? "s" : ""} ran unprotected.`
12428
+ ) + import_chalk3.default.dim(" node9 would have caught them.")
12429
+ );
12430
+ console.log("");
12431
+ }
12432
+ console.log(import_chalk3.default.bold(" Enable real-time protection:"));
12433
+ console.log("");
12104
12434
  console.log(
12105
- import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan("node9 scan --drill-down") + import_chalk3.default.dim(" to see full commands and session IDs.")
12435
+ " " + import_chalk3.default.cyan("npm install -g @node9/proxy") + import_chalk3.default.dim(" && ") + import_chalk3.default.cyan("node9 init --recommended")
12106
12436
  );
12107
- }
12108
- } else {
12109
- const riskySummary = totalFindings + scan.dlpFindings.length;
12110
- if (riskySummary > 0) {
12437
+ console.log("");
12111
12438
  console.log(
12112
- import_chalk3.default.yellow.bold(
12113
- ` \u26A1 ${riskySummary} operation${riskySummary !== 1 ? "s" : ""} ran unprotected.`
12114
- ) + import_chalk3.default.dim(" node9 would have caught them.")
12439
+ import_chalk3.default.dim(
12440
+ " Hooks into Claude Code automatically. Every tool call checked before it runs."
12441
+ )
12115
12442
  );
12443
+ console.log(" " + import_chalk3.default.dim("\u2192 ") + import_chalk3.default.underline("https://node9.ai"));
12116
12444
  }
12117
12445
  console.log("");
12118
- console.log(import_chalk3.default.bold(" Protect your next session in 30 seconds:"));
12119
- console.log("");
12120
- console.log(" " + import_chalk3.default.cyan("npm install -g @node9/proxy"));
12121
- console.log(" " + import_chalk3.default.cyan("node9 init"));
12122
- console.log("");
12123
- console.log(import_chalk3.default.dim(" node9 hooks into Claude Code automatically."));
12124
- console.log(
12125
- import_chalk3.default.dim(" Every tool call is checked before it runs \u2014 no proxy, no latency.")
12126
- );
12127
- console.log("");
12128
- console.log(" " + import_chalk3.default.dim("\u2192 ") + import_chalk3.default.underline("https://node9.ai"));
12129
- }
12130
- console.log("");
12131
- if (!isTestingMode()) {
12132
- if (isWired) {
12133
- const url = `http://${DAEMON_HOST}:${DAEMON_PORT}/?openscan=1`;
12134
- if (isDaemonRunning()) {
12135
- const internalToken = getInternalToken();
12136
- if (internalToken) {
12137
- try {
12138
- const pushSummary = buildScanSummary([
12139
- { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
12140
- { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
12141
- { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
12142
- ]);
12143
- await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
12144
- method: "POST",
12145
- headers: {
12146
- "Content-Type": "application/json",
12147
- "x-node9-internal": internalToken
12148
- },
12149
- body: JSON.stringify({ status: "complete", summary: pushSummary }),
12150
- signal: AbortSignal.timeout(3e3)
12151
- });
12152
- openBrowserLocal();
12153
- } catch {
12446
+ if (!isTestingMode()) {
12447
+ if (isWired) {
12448
+ const url = `http://${DAEMON_HOST}:${DAEMON_PORT}/?openscan=1`;
12449
+ if (isDaemonRunning()) {
12450
+ const internalToken = getInternalToken();
12451
+ if (internalToken) {
12452
+ try {
12453
+ const pushSummary = buildScanSummary([
12454
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
12455
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
12456
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
12457
+ ]);
12458
+ await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
12459
+ method: "POST",
12460
+ headers: {
12461
+ "Content-Type": "application/json",
12462
+ "x-node9-internal": internalToken
12463
+ },
12464
+ body: JSON.stringify({ status: "complete", summary: pushSummary }),
12465
+ signal: AbortSignal.timeout(3e3)
12466
+ });
12467
+ } catch {
12468
+ }
12154
12469
  }
12155
12470
  }
12471
+ if (isDaemonRunning()) {
12472
+ console.log(" " + import_chalk3.default.cyan("\u{1F310} View in browser:") + " " + import_chalk3.default.underline(url));
12473
+ } else {
12474
+ console.log(
12475
+ " " + import_chalk3.default.dim("\u{1F4CA} To view in browser, start the daemon: ") + import_chalk3.default.cyan("node9 daemon --background")
12476
+ );
12477
+ }
12478
+ console.log("");
12156
12479
  }
12157
- if (isDaemonRunning()) {
12158
- console.log(" " + import_chalk3.default.cyan("\u{1F310} View in browser:") + " " + import_chalk3.default.underline(url));
12159
- } else {
12160
- console.log(
12161
- " " + import_chalk3.default.dim("\u{1F4CA} To view in browser, start the daemon: ") + import_chalk3.default.cyan("node9 daemon --background")
12162
- );
12163
- }
12164
- console.log("");
12165
12480
  }
12166
12481
  }
12167
- });
12482
+ );
12168
12483
  }
12169
12484
  var import_chalk3, import_fs15, import_path17, import_os13, CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, DEFAULT_RULE_NAMES;
12170
12485
  var init_scan = __esm({
@@ -12700,14 +13015,6 @@ function readBody(req) {
12700
13015
  req.on("end", () => resolve(body));
12701
13016
  });
12702
13017
  }
12703
- function openBrowser(url) {
12704
- if (process.env.NODE9_TESTING === "1") return;
12705
- try {
12706
- const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
12707
- (0, import_child_process4.spawn)(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
12708
- } catch {
12709
- }
12710
- }
12711
13018
  function estimateToolCost(tool, args) {
12712
13019
  const a = args ?? {};
12713
13020
  const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
@@ -12820,6 +13127,18 @@ function startActivitySocket() {
12820
13127
  });
12821
13128
  return;
12822
13129
  }
13130
+ if (data.status === "execution-completed") {
13131
+ broadcast("execution-result", {
13132
+ id: data.id,
13133
+ ts: data.ts,
13134
+ tool: data.tool,
13135
+ agent: data.agent,
13136
+ mcpServer: data.mcpServer,
13137
+ durationMs: data.durationMs,
13138
+ isError: data.isError
13139
+ });
13140
+ return;
13141
+ }
12823
13142
  if (data.status === "pending") {
12824
13143
  broadcast("activity", {
12825
13144
  id: data.id,
@@ -12827,7 +13146,8 @@ function startActivitySocket() {
12827
13146
  tool: data.tool,
12828
13147
  args: redactArgs(data.args),
12829
13148
  status: "pending",
12830
- agent: data.agent
13149
+ agent: data.agent,
13150
+ mcpServer: data.mcpServer
12831
13151
  });
12832
13152
  } else {
12833
13153
  if (data.status === "allow") {
@@ -12854,7 +13174,9 @@ function startActivitySocket() {
12854
13174
  id: data.id,
12855
13175
  status: data.status,
12856
13176
  label: data.label,
12857
- costEstimate
13177
+ costEstimate,
13178
+ agent: data.agent,
13179
+ mcpServer: data.mcpServer
12858
13180
  });
12859
13181
  }
12860
13182
  } catch {
@@ -12871,7 +13193,7 @@ function startActivitySocket() {
12871
13193
  }
12872
13194
  });
12873
13195
  }
12874
- var import_net2, import_fs17, import_path19, import_os14, import_child_process4, import_crypto6, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, 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;
13196
+ var import_net2, import_fs17, import_path19, import_os14, import_crypto6, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, 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;
12875
13197
  var init_state2 = __esm({
12876
13198
  "src/daemon/state.ts"() {
12877
13199
  "use strict";
@@ -12879,7 +13201,6 @@ var init_state2 = __esm({
12879
13201
  import_fs17 = __toESM(require("fs"));
12880
13202
  import_path19 = __toESM(require("path"));
12881
13203
  import_os14 = __toESM(require("os"));
12882
- import_child_process4 = require("child_process");
12883
13204
  import_crypto6 = require("crypto");
12884
13205
  init_daemon();
12885
13206
  init_suggestion_tracker();
@@ -13536,7 +13857,6 @@ function startDaemon() {
13536
13857
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
13537
13858
  const watchMode = process.env.NODE9_WATCH_MODE === "1";
13538
13859
  let idleTimer;
13539
- let browserOpened = false;
13540
13860
  function resetIdleTimer() {
13541
13861
  if (watchMode) return;
13542
13862
  if (idleTimer) clearTimeout(idleTimer);
@@ -13645,12 +13965,6 @@ data: ${JSON.stringify(item.data)}
13645
13965
  }
13646
13966
  });
13647
13967
  }
13648
- if (req.method === "POST" && pathname === "/browser-opened") {
13649
- if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
13650
- browserOpened = true;
13651
- res.writeHead(200).end();
13652
- return;
13653
- }
13654
13968
  if (req.method === "POST" && pathname === "/check") {
13655
13969
  try {
13656
13970
  resetIdleTimer();
@@ -13711,7 +14025,9 @@ data: ${JSON.stringify(item.data)}
13711
14025
  ts: entry.timestamp,
13712
14026
  tool: toolName,
13713
14027
  args: redactArgs(args),
13714
- status: "pending"
14028
+ status: "pending",
14029
+ agent: entry.agent,
14030
+ mcpServer: entry.mcpServer
13715
14031
  });
13716
14032
  }
13717
14033
  const projectCwd = typeof cwd === "string" && import_path25.default.isAbsolute(cwd) ? cwd : void 0;
@@ -13734,11 +14050,6 @@ data: ${JSON.stringify(item.data)}
13734
14050
  // Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
13735
14051
  allowCount: (insightCounts.get(toolName) ?? 0) + 1
13736
14052
  });
13737
- const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
13738
- if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
13739
- browserOpened = true;
13740
- openBrowser(`http://127.0.0.1:${DAEMON_PORT}/`);
13741
- }
13742
14053
  }
13743
14054
  res.writeHead(200, { "Content-Type": "application/json" });
13744
14055
  res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
@@ -13760,7 +14071,9 @@ data: ${JSON.stringify(item.data)}
13760
14071
  broadcast("activity-result", {
13761
14072
  id,
13762
14073
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
13763
- label: result.blockedByLabel
14074
+ label: result.blockedByLabel,
14075
+ agent: e.agent,
14076
+ mcpServer: e.mcpServer
13764
14077
  });
13765
14078
  clearTimeout(e.timer);
13766
14079
  const decision = result.approved ? "allow" : "deny";
@@ -14534,7 +14847,7 @@ data: ${JSON.stringify(item.data)}
14534
14847
  }).then((res) => {
14535
14848
  if (res.ok) {
14536
14849
  try {
14537
- const r = (0, import_child_process5.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
14850
+ const r = (0, import_child_process4.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
14538
14851
  encoding: "utf8",
14539
14852
  timeout: 1e3
14540
14853
  });
@@ -14582,7 +14895,7 @@ data: ${JSON.stringify(item.data)}
14582
14895
  }
14583
14896
  startActivitySocket();
14584
14897
  }
14585
- var import_http, import_fs23, import_path25, import_os20, import_crypto7, import_child_process5, import_chalk4;
14898
+ var import_http, import_fs23, import_path25, import_os20, import_crypto7, import_child_process4, import_chalk4;
14586
14899
  var init_server = __esm({
14587
14900
  "src/daemon/server.ts"() {
14588
14901
  "use strict";
@@ -14591,7 +14904,7 @@ var init_server = __esm({
14591
14904
  import_path25 = __toESM(require("path"));
14592
14905
  import_os20 = __toESM(require("os"));
14593
14906
  import_crypto7 = require("crypto");
14594
- import_child_process5 = require("child_process");
14907
+ import_child_process4 = require("child_process");
14595
14908
  import_chalk4 = __toESM(require("chalk"));
14596
14909
  init_core();
14597
14910
  init_shields();
@@ -14620,7 +14933,7 @@ function resolveNode9Binary() {
14620
14933
  }
14621
14934
  try {
14622
14935
  const cmd = process.platform === "win32" ? "where" : "which";
14623
- const r = (0, import_child_process6.spawnSync)(cmd, ["node9"], { encoding: "utf8", timeout: 3e3 });
14936
+ const r = (0, import_child_process5.spawnSync)(cmd, ["node9"], { encoding: "utf8", timeout: 3e3 });
14624
14937
  if (r.status === 0 && r.stdout.trim()) {
14625
14938
  return r.stdout.trim().split("\n")[0].trim();
14626
14939
  }
@@ -14674,8 +14987,8 @@ function installLaunchd(binaryPath) {
14674
14987
  const dir = import_path26.default.dirname(LAUNCHD_PLIST);
14675
14988
  if (!import_fs24.default.existsSync(dir)) import_fs24.default.mkdirSync(dir, { recursive: true });
14676
14989
  import_fs24.default.writeFileSync(LAUNCHD_PLIST, launchdPlist(binaryPath), "utf-8");
14677
- (0, import_child_process6.spawnSync)("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
14678
- const r = (0, import_child_process6.spawnSync)("launchctl", ["load", "-w", LAUNCHD_PLIST], {
14990
+ (0, import_child_process5.spawnSync)("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
14991
+ const r = (0, import_child_process5.spawnSync)("launchctl", ["load", "-w", LAUNCHD_PLIST], {
14679
14992
  encoding: "utf8",
14680
14993
  timeout: 5e3
14681
14994
  });
@@ -14685,7 +14998,7 @@ function installLaunchd(binaryPath) {
14685
14998
  }
14686
14999
  function uninstallLaunchd() {
14687
15000
  if (import_fs24.default.existsSync(LAUNCHD_PLIST)) {
14688
- (0, import_child_process6.spawnSync)("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
15001
+ (0, import_child_process5.spawnSync)("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
14689
15002
  import_fs24.default.unlinkSync(LAUNCHD_PLIST);
14690
15003
  }
14691
15004
  }
@@ -14715,18 +15028,18 @@ function installSystemd(binaryPath) {
14715
15028
  }
14716
15029
  import_fs24.default.writeFileSync(SYSTEMD_UNIT, systemdUnit(binaryPath), "utf-8");
14717
15030
  try {
14718
- (0, import_child_process6.execFileSync)("loginctl", ["enable-linger", import_os21.default.userInfo().username], { timeout: 3e3 });
15031
+ (0, import_child_process5.execFileSync)("loginctl", ["enable-linger", import_os21.default.userInfo().username], { timeout: 3e3 });
14719
15032
  } catch {
14720
15033
  }
14721
- const reload = (0, import_child_process6.spawnSync)("systemctl", ["--user", "daemon-reload"], {
15034
+ const reload = (0, import_child_process5.spawnSync)("systemctl", ["--user", "daemon-reload"], {
14722
15035
  encoding: "utf8",
14723
15036
  timeout: 5e3
14724
15037
  });
14725
15038
  if (reload.status !== 0) {
14726
15039
  throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
14727
15040
  }
14728
- (0, import_child_process6.spawnSync)("systemctl", ["--user", "stop", "node9-daemon"], { encoding: "utf8", timeout: 3e3 });
14729
- const enable = (0, import_child_process6.spawnSync)("systemctl", ["--user", "enable", "--now", "node9-daemon"], {
15041
+ (0, import_child_process5.spawnSync)("systemctl", ["--user", "stop", "node9-daemon"], { encoding: "utf8", timeout: 3e3 });
15042
+ const enable = (0, import_child_process5.spawnSync)("systemctl", ["--user", "enable", "--now", "node9-daemon"], {
14730
15043
  encoding: "utf8",
14731
15044
  timeout: 5e3
14732
15045
  });
@@ -14736,11 +15049,11 @@ function installSystemd(binaryPath) {
14736
15049
  }
14737
15050
  function uninstallSystemd() {
14738
15051
  if (import_fs24.default.existsSync(SYSTEMD_UNIT)) {
14739
- (0, import_child_process6.spawnSync)("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
15052
+ (0, import_child_process5.spawnSync)("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
14740
15053
  encoding: "utf8",
14741
15054
  timeout: 5e3
14742
15055
  });
14743
- (0, import_child_process6.spawnSync)("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
15056
+ (0, import_child_process5.spawnSync)("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
14744
15057
  import_fs24.default.unlinkSync(SYSTEMD_UNIT);
14745
15058
  }
14746
15059
  }
@@ -14758,7 +15071,7 @@ function stopRunningDaemon() {
14758
15071
  try {
14759
15072
  process.kill(pid, "SIGTERM");
14760
15073
  const deadline = Date.now() + 3e3;
14761
- const pollStop = (0, import_child_process6.spawnSync)(
15074
+ const pollStop = (0, import_child_process5.spawnSync)(
14762
15075
  "sh",
14763
15076
  ["-c", `while kill -0 ${pid} 2>/dev/null; do sleep 0.1; done`],
14764
15077
  {
@@ -14790,7 +15103,7 @@ function installDaemonService() {
14790
15103
  return { ok: true, platform: "launchd", alreadyInstalled };
14791
15104
  }
14792
15105
  if (process.platform === "linux") {
14793
- const check = (0, import_child_process6.spawnSync)("systemctl", ["--user", "--version"], {
15106
+ const check = (0, import_child_process5.spawnSync)("systemctl", ["--user", "--version"], {
14794
15107
  encoding: "utf8",
14795
15108
  timeout: 2e3
14796
15109
  });
@@ -14841,14 +15154,14 @@ function isDaemonServiceInstalled() {
14841
15154
  if (process.platform === "linux") return isSystemdInstalled();
14842
15155
  return false;
14843
15156
  }
14844
- var import_fs24, import_path26, import_os21, import_child_process6, LAUNCHD_LABEL, LAUNCHD_PLIST, SYSTEMD_UNIT_DIR, SYSTEMD_UNIT;
15157
+ var import_fs24, import_path26, import_os21, import_child_process5, LAUNCHD_LABEL, LAUNCHD_PLIST, SYSTEMD_UNIT_DIR, SYSTEMD_UNIT;
14845
15158
  var init_service = __esm({
14846
15159
  "src/daemon/service.ts"() {
14847
15160
  "use strict";
14848
15161
  import_fs24 = __toESM(require("fs"));
14849
15162
  import_path26 = __toESM(require("path"));
14850
15163
  import_os21 = __toESM(require("os"));
14851
- import_child_process6 = require("child_process");
15164
+ import_child_process5 = require("child_process");
14852
15165
  LAUNCHD_LABEL = "ai.node9.daemon";
14853
15166
  LAUNCHD_PLIST = import_path26.default.join(import_os21.default.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
14854
15167
  SYSTEMD_UNIT_DIR = import_path26.default.join(import_os21.default.homedir(), ".config", "systemd", "user");
@@ -14898,7 +15211,7 @@ function daemonStatus() {
14898
15211
  processStatus = import_chalk5.default.yellow("not running (stale PID file)");
14899
15212
  }
14900
15213
  } else {
14901
- const r = (0, import_child_process7.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
15214
+ const r = (0, import_child_process6.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
14902
15215
  encoding: "utf8",
14903
15216
  timeout: 500
14904
15217
  });
@@ -14913,13 +15226,13 @@ function daemonStatus() {
14913
15226
  console.log(` Service : ${serviceLabel}
14914
15227
  `);
14915
15228
  }
14916
- var import_fs25, import_chalk5, import_child_process7, MAX_PID;
15229
+ var import_fs25, import_chalk5, import_child_process6, MAX_PID;
14917
15230
  var init_daemon2 = __esm({
14918
15231
  "src/daemon/index.ts"() {
14919
15232
  "use strict";
14920
15233
  import_fs25 = __toESM(require("fs"));
14921
15234
  import_chalk5 = __toESM(require("chalk"));
14922
- import_child_process7 = require("child_process");
15235
+ import_child_process6 = require("child_process");
14923
15236
  init_server();
14924
15237
  init_state2();
14925
15238
  init_service();
@@ -15018,10 +15331,13 @@ function wrappedLineCount(text) {
15018
15331
  const len = visibleLength(text);
15019
15332
  return Math.max(1, Math.ceil(len / cols));
15020
15333
  }
15021
- function agentLabel(agent) {
15022
- if (!agent || agent === "Terminal") return "";
15334
+ function agentLabel(agent, mcpServer) {
15335
+ if (!agent || agent === "Terminal") {
15336
+ return mcpServer ? import_chalk27.default.dim(`[\u2192 ${mcpServer}] `) : "";
15337
+ }
15023
15338
  const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
15024
- return short ? import_chalk27.default.dim(`[${short}] `) : "";
15339
+ if (!short) return mcpServer ? import_chalk27.default.dim(`[\u2192 ${mcpServer}] `) : "";
15340
+ return mcpServer ? import_chalk27.default.dim(`[${short} \u2192 ${mcpServer}] `) : import_chalk27.default.dim(`[${short}] `);
15025
15341
  }
15026
15342
  function formatBase(activity) {
15027
15343
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
@@ -15029,7 +15345,7 @@ function formatBase(activity) {
15029
15345
  const toolName = activity.tool.slice(0, 16).padEnd(16);
15030
15346
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os37.default.homedir(), "~");
15031
15347
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
15032
- return `${import_chalk27.default.gray(time)} ${icon} ${agentLabel(activity.agent)}${import_chalk27.default.white.bold(toolName)} ${import_chalk27.default.dim(argsPreview)}`;
15348
+ return `${import_chalk27.default.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer)}${import_chalk27.default.white.bold(toolName)} ${import_chalk27.default.dim(argsPreview)}`;
15033
15349
  }
15034
15350
  function renderResult(activity, result) {
15035
15351
  const base = formatBase(activity);
@@ -15083,7 +15399,7 @@ async function ensureDaemon() {
15083
15399
  } catch {
15084
15400
  }
15085
15401
  console.log(import_chalk27.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
15086
- const child = (0, import_child_process16.spawn)(process.execPath, [process.argv[1], "daemon"], {
15402
+ const child = (0, import_child_process15.spawn)(process.execPath, [process.argv[1], "daemon"], {
15087
15403
  detached: true,
15088
15404
  stdio: "ignore",
15089
15405
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -15483,22 +15799,6 @@ async function startTail(options = {}) {
15483
15799
  process.stdin.on("keypress", onKeypress);
15484
15800
  }
15485
15801
  const dashboardUrl = `http://127.0.0.1:${port}/`;
15486
- try {
15487
- const browserEnabled = getConfig().settings.approvers?.browser !== false;
15488
- if (browserEnabled) {
15489
- if (process.platform === "darwin") (0, import_child_process16.execSync)(`open "${dashboardUrl}"`, { stdio: "ignore" });
15490
- else if (process.platform === "win32")
15491
- (0, import_child_process16.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
15492
- else (0, import_child_process16.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
15493
- const intToken = getInternalToken();
15494
- fetch(`http://127.0.0.1:${port}/browser-opened`, {
15495
- method: "POST",
15496
- headers: intToken ? { "X-Node9-Internal": intToken } : {}
15497
- }).catch(() => {
15498
- });
15499
- }
15500
- } catch {
15501
- }
15502
15802
  const auditLog = import_path43.default.join(import_os37.default.homedir(), ".node9", "audit.log");
15503
15803
  try {
15504
15804
  const unackedDlp = import_fs41.default.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
@@ -15678,6 +15978,17 @@ async function startTail(options = {}) {
15678
15978
  orphanedResults.set(data.id, data);
15679
15979
  }
15680
15980
  }
15981
+ if (event === "execution-result") {
15982
+ const exec = data;
15983
+ const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
15984
+ const arrow = exec.isError ? import_chalk27.default.red(" \u21B3 \u2717") : import_chalk27.default.green(" \u21B3 \u2713");
15985
+ const label = agentLabel(exec.agent, exec.mcpServer);
15986
+ const tool = (exec.tool ?? "").slice(0, 16);
15987
+ const duration = typeof exec.durationMs === "number" ? import_chalk27.default.dim(` (${exec.durationMs}ms)`) : "";
15988
+ console.log(
15989
+ `${import_chalk27.default.gray(time)} ${arrow} ${label}${import_chalk27.default.dim(tool)}${import_chalk27.default.dim(" completed")}${duration}`
15990
+ );
15991
+ }
15681
15992
  }
15682
15993
  req.on("error", (err2) => {
15683
15994
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
@@ -15686,7 +15997,7 @@ async function startTail(options = {}) {
15686
15997
  process.exit(1);
15687
15998
  });
15688
15999
  }
15689
- var import_http2, import_chalk27, import_fs41, import_os37, import_path43, import_readline5, import_child_process16, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
16000
+ var import_http2, import_chalk27, import_fs41, import_os37, import_path43, import_readline5, import_child_process15, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
15690
16001
  var init_tail = __esm({
15691
16002
  "src/tui/tail.ts"() {
15692
16003
  "use strict";
@@ -15696,10 +16007,8 @@ var init_tail = __esm({
15696
16007
  import_os37 = __toESM(require("os"));
15697
16008
  import_path43 = __toESM(require("path"));
15698
16009
  import_readline5 = __toESM(require("readline"));
15699
- import_child_process16 = require("child_process");
16010
+ import_child_process15 = require("child_process");
15700
16011
  init_daemon2();
15701
- init_daemon();
15702
- init_core();
15703
16012
  PID_FILE = import_path43.default.join(import_os37.default.homedir(), ".node9", "daemon.pid");
15704
16013
  ICONS = {
15705
16014
  bash: "\u{1F4BB}",
@@ -16153,7 +16462,7 @@ function parseDuration(str) {
16153
16462
  // src/proxy/index.ts
16154
16463
  var import_readline = __toESM(require("readline"));
16155
16464
  var import_chalk6 = __toESM(require("chalk"));
16156
- var import_child_process8 = require("child_process");
16465
+ var import_child_process7 = require("child_process");
16157
16466
  var import_execa = require("execa");
16158
16467
  var import_execa2 = require("execa");
16159
16468
  init_orchestrator();
@@ -16241,11 +16550,11 @@ async function runProxy(targetCommand) {
16241
16550
  }
16242
16551
  console.error(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
16243
16552
  const spawnEnv = { ...process.env, FORCE_COLOR: "1" };
16244
- const child = useShell ? (0, import_child_process8.spawn)("/bin/bash", ["-c", targetCommand], {
16553
+ const child = useShell ? (0, import_child_process7.spawn)("/bin/bash", ["-c", targetCommand], {
16245
16554
  stdio: ["pipe", "pipe", "inherit"],
16246
16555
  shell: false,
16247
16556
  env: spawnEnv
16248
- }) : (0, import_child_process8.spawn)(executable, args, { stdio: ["pipe", "pipe", "inherit"], shell: false, env: spawnEnv });
16557
+ }) : (0, import_child_process7.spawn)(executable, args, { stdio: ["pipe", "pipe", "inherit"], shell: false, env: spawnEnv });
16249
16558
  const agentIn = import_readline.default.createInterface({ input: process.stdin, terminal: false });
16250
16559
  agentIn.on("line", async (line) => {
16251
16560
  let message;
@@ -16314,7 +16623,7 @@ init_daemon_starter();
16314
16623
  // src/cli/commands/check.ts
16315
16624
  var import_chalk7 = __toESM(require("chalk"));
16316
16625
  var import_fs28 = __toESM(require("fs"));
16317
- var import_child_process10 = require("child_process");
16626
+ var import_child_process9 = require("child_process");
16318
16627
  var import_path29 = __toESM(require("path"));
16319
16628
  var import_os24 = __toESM(require("os"));
16320
16629
  init_orchestrator();
@@ -16323,7 +16632,7 @@ init_config();
16323
16632
  init_policy();
16324
16633
 
16325
16634
  // src/undo.ts
16326
- var import_child_process9 = require("child_process");
16635
+ var import_child_process8 = require("child_process");
16327
16636
  var import_crypto8 = __toESM(require("crypto"));
16328
16637
  var import_fs26 = __toESM(require("fs"));
16329
16638
  var import_net3 = __toESM(require("net"));
@@ -16436,7 +16745,7 @@ function ensureShadowRepo(shadowDir, cwd) {
16436
16745
  cleanOrphanedIndexFiles(shadowDir);
16437
16746
  const normalizedCwd = normalizeCwdForHash(cwd);
16438
16747
  const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
16439
- const check = (0, import_child_process9.spawnSync)("git", ["rev-parse", "--git-dir"], {
16748
+ const check = (0, import_child_process8.spawnSync)("git", ["rev-parse", "--git-dir"], {
16440
16749
  env: shadowEnvBase,
16441
16750
  timeout: 3e3
16442
16751
  });
@@ -16462,17 +16771,17 @@ function ensureShadowRepo(shadowDir, cwd) {
16462
16771
  import_fs26.default.mkdirSync(shadowDir, { recursive: true });
16463
16772
  } catch {
16464
16773
  }
16465
- const init = (0, import_child_process9.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
16774
+ const init = (0, import_child_process8.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
16466
16775
  if (init.status !== 0 || init.error) {
16467
16776
  const reason = init.error ? init.error.message : init.stderr?.toString();
16468
16777
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
16469
16778
  return false;
16470
16779
  }
16471
16780
  const configFile = import_path27.default.join(shadowDir, "config");
16472
- (0, import_child_process9.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
16781
+ (0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
16473
16782
  timeout: 3e3
16474
16783
  });
16475
- (0, import_child_process9.spawnSync)("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
16784
+ (0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
16476
16785
  timeout: 3e3
16477
16786
  });
16478
16787
  try {
@@ -16483,7 +16792,7 @@ function ensureShadowRepo(shadowDir, cwd) {
16483
16792
  }
16484
16793
  function buildGitEnv(cwd) {
16485
16794
  const shadowDir = getShadowRepoDir(cwd);
16486
- const check = (0, import_child_process9.spawnSync)("git", ["rev-parse", "--git-dir"], {
16795
+ const check = (0, import_child_process8.spawnSync)("git", ["rev-parse", "--git-dir"], {
16487
16796
  env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
16488
16797
  timeout: 2e3
16489
16798
  });
@@ -16508,11 +16817,11 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
16508
16817
  GIT_WORK_TREE: cwd,
16509
16818
  GIT_INDEX_FILE: indexFile
16510
16819
  };
16511
- (0, import_child_process9.spawnSync)("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
16512
- const treeRes = (0, import_child_process9.spawnSync)("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
16820
+ (0, import_child_process8.spawnSync)("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
16821
+ const treeRes = (0, import_child_process8.spawnSync)("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
16513
16822
  const treeHash = treeRes.stdout?.toString().trim();
16514
16823
  if (!treeHash || treeRes.status !== 0) return null;
16515
- const commitRes = (0, import_child_process9.spawnSync)(
16824
+ const commitRes = (0, import_child_process8.spawnSync)(
16516
16825
  "git",
16517
16826
  ["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
16518
16827
  { env: shadowEnv, timeout: GIT_TIMEOUT }
@@ -16524,7 +16833,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
16524
16833
  let capturedFiles = [];
16525
16834
  let capturedDiff = null;
16526
16835
  if (prevEntry) {
16527
- const filesRes = (0, import_child_process9.spawnSync)("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
16836
+ const filesRes = (0, import_child_process8.spawnSync)("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
16528
16837
  env: shadowEnv,
16529
16838
  timeout: GIT_TIMEOUT
16530
16839
  });
@@ -16534,7 +16843,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
16534
16843
  if (capturedFiles.length === 0) {
16535
16844
  return prevEntry.hash;
16536
16845
  }
16537
- const diffRes = (0, import_child_process9.spawnSync)("git", ["diff", prevEntry.hash, commitHash], {
16846
+ const diffRes = (0, import_child_process8.spawnSync)("git", ["diff", prevEntry.hash, commitHash], {
16538
16847
  env: shadowEnv,
16539
16848
  timeout: GIT_TIMEOUT
16540
16849
  });
@@ -16542,7 +16851,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
16542
16851
  capturedDiff = diffRes.stdout?.toString() || null;
16543
16852
  }
16544
16853
  } else {
16545
- const filesRes = (0, import_child_process9.spawnSync)("git", ["ls-tree", "-r", "--name-only", commitHash], {
16854
+ const filesRes = (0, import_child_process8.spawnSync)("git", ["ls-tree", "-r", "--name-only", commitHash], {
16546
16855
  env: shadowEnv,
16547
16856
  timeout: GIT_TIMEOUT
16548
16857
  });
@@ -16575,7 +16884,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
16575
16884
  notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
16576
16885
  import_fs26.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
16577
16886
  if (shouldGc) {
16578
- (0, import_child_process9.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
16887
+ (0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
16579
16888
  }
16580
16889
  return commitHash;
16581
16890
  } catch (err2) {
@@ -16596,14 +16905,14 @@ function getSnapshotHistory() {
16596
16905
  function computeUndoDiff(hash, cwd) {
16597
16906
  try {
16598
16907
  const env = buildGitEnv(cwd);
16599
- const statRes = (0, import_child_process9.spawnSync)("git", ["diff", hash, "--stat", "--", "."], {
16908
+ const statRes = (0, import_child_process8.spawnSync)("git", ["diff", hash, "--stat", "--", "."], {
16600
16909
  cwd,
16601
16910
  env,
16602
16911
  timeout: GIT_TIMEOUT
16603
16912
  });
16604
16913
  const stat = statRes.stdout?.toString().trim();
16605
16914
  if (!stat || statRes.status !== 0) return null;
16606
- const diffRes = (0, import_child_process9.spawnSync)("git", ["diff", hash, "--", "."], {
16915
+ const diffRes = (0, import_child_process8.spawnSync)("git", ["diff", hash, "--", "."], {
16607
16916
  cwd,
16608
16917
  env,
16609
16918
  timeout: GIT_TIMEOUT
@@ -16622,7 +16931,7 @@ function applyUndo(hash, cwd) {
16622
16931
  try {
16623
16932
  const dir = cwd ?? process.cwd();
16624
16933
  const env = buildGitEnv(dir);
16625
- const restore = (0, import_child_process9.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
16934
+ const restore = (0, import_child_process8.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
16626
16935
  cwd: dir,
16627
16936
  env,
16628
16937
  timeout: GIT_TIMEOUT
@@ -16634,7 +16943,7 @@ function applyUndo(hash, cwd) {
16634
16943
  }
16635
16944
  return false;
16636
16945
  }
16637
- const lsTree = (0, import_child_process9.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], {
16946
+ const lsTree = (0, import_child_process8.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], {
16638
16947
  cwd: dir,
16639
16948
  env,
16640
16949
  timeout: GIT_TIMEOUT
@@ -16653,8 +16962,8 @@ function applyUndo(hash, cwd) {
16653
16962
  `);
16654
16963
  return false;
16655
16964
  }
16656
- const tracked = (0, import_child_process9.spawnSync)("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
16657
- const untracked = (0, import_child_process9.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], {
16965
+ const tracked = (0, import_child_process8.spawnSync)("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
16966
+ const untracked = (0, import_child_process8.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], {
16658
16967
  cwd: dir,
16659
16968
  env,
16660
16969
  timeout: GIT_TIMEOUT
@@ -16904,7 +17213,7 @@ RAW: ${raw}
16904
17213
  ]) {
16905
17214
  delete safeEnv[key];
16906
17215
  }
16907
- const d = (0, import_child_process10.spawn)(process.execPath, [scriptPath, "daemon"], {
17216
+ const d = (0, import_child_process9.spawn)(process.execPath, [scriptPath, "daemon"], {
16908
17217
  detached: true,
16909
17218
  stdio: "ignore",
16910
17219
  env: { ...safeEnv, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
@@ -17736,7 +18045,7 @@ var import_chalk9 = __toESM(require("chalk"));
17736
18045
  var import_fs30 = __toESM(require("fs"));
17737
18046
  var import_path31 = __toESM(require("path"));
17738
18047
  var import_os26 = __toESM(require("os"));
17739
- var import_child_process11 = require("child_process");
18048
+ var import_child_process10 = require("child_process");
17740
18049
  init_daemon();
17741
18050
  function registerDoctorCommand(program2, version2) {
17742
18051
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
@@ -17762,7 +18071,7 @@ function registerDoctorCommand(program2, version2) {
17762
18071
  `));
17763
18072
  section("Binary");
17764
18073
  try {
17765
- const which = (0, import_child_process11.execSync)("which node9", { encoding: "utf-8", timeout: 3e3 }).trim();
18074
+ const which = (0, import_child_process10.execSync)("which node9", { encoding: "utf-8", timeout: 3e3 }).trim();
17766
18075
  pass(`node9 found at ${which}`);
17767
18076
  } catch {
17768
18077
  warn(
@@ -17780,7 +18089,7 @@ function registerDoctorCommand(program2, version2) {
17780
18089
  );
17781
18090
  }
17782
18091
  try {
17783
- const gitVersion = (0, import_child_process11.execSync)("git --version", { encoding: "utf-8", timeout: 3e3 }).trim();
18092
+ const gitVersion = (0, import_child_process10.execSync)("git --version", { encoding: "utf-8", timeout: 3e3 }).trim();
17784
18093
  pass(gitVersion);
17785
18094
  } catch {
17786
18095
  warn(
@@ -18663,7 +18972,7 @@ function registerReportCommand(program2) {
18663
18972
 
18664
18973
  // src/cli/commands/daemon-cmd.ts
18665
18974
  var import_chalk12 = __toESM(require("chalk"));
18666
- var import_child_process12 = require("child_process");
18975
+ var import_child_process11 = require("child_process");
18667
18976
  init_daemon2();
18668
18977
  init_daemon();
18669
18978
  init_daemon_starter();
@@ -18704,7 +19013,7 @@ function registerDaemonCommand(program2) {
18704
19013
  if (cmd === "restart") {
18705
19014
  stopDaemon();
18706
19015
  await new Promise((r) => setTimeout(r, 500));
18707
- const child = (0, import_child_process12.spawn)(process.execPath, [process.argv[1], "daemon"], {
19016
+ const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
18708
19017
  detached: true,
18709
19018
  stdio: "ignore",
18710
19019
  env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
@@ -18738,7 +19047,7 @@ function registerDaemonCommand(program2) {
18738
19047
  console.log(import_chalk12.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
18739
19048
  process.exit(0);
18740
19049
  }
18741
- const child = (0, import_child_process12.spawn)(process.execPath, [process.argv[1], "daemon"], {
19050
+ const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
18742
19051
  detached: true,
18743
19052
  stdio: "ignore"
18744
19053
  });
@@ -18753,7 +19062,7 @@ function registerDaemonCommand(program2) {
18753
19062
  process.exit(0);
18754
19063
  }
18755
19064
  if (options.background) {
18756
- const child = (0, import_child_process12.spawn)(process.execPath, [process.argv[1], "daemon"], {
19065
+ const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
18757
19066
  detached: true,
18758
19067
  stdio: "ignore"
18759
19068
  });
@@ -18961,14 +19270,18 @@ function fireTelemetryPing(agents) {
18961
19270
  }
18962
19271
  }
18963
19272
  function registerInitCommand(program2) {
18964
- program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").option(
19273
+ program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option(
19274
+ "-m, --mode <mode>",
19275
+ "Initial security mode: standard | strict | audit | observe (logs would-block, never blocks)",
19276
+ "standard"
19277
+ ).option("--skip-setup", "Only create config \u2014 do not wire AI agents").option(
18965
19278
  "--recommended",
18966
19279
  "Non-interactive: enable bash-safe + filesystem + project-jail shields without prompting"
18967
19280
  ).action(
18968
19281
  async (options) => {
18969
19282
  console.log(import_chalk14.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
18970
19283
  let chosenMode = options.mode.toLowerCase();
18971
- if (!["standard", "strict", "audit"].includes(chosenMode)) {
19284
+ if (!["standard", "strict", "audit", "observe"].includes(chosenMode)) {
18972
19285
  chosenMode = DEFAULT_CONFIG.settings.mode;
18973
19286
  }
18974
19287
  {
@@ -19409,7 +19722,7 @@ function registerUndoCommand(program2) {
19409
19722
 
19410
19723
  // src/cli/commands/watch.ts
19411
19724
  var import_chalk17 = __toESM(require("chalk"));
19412
- var import_child_process13 = require("child_process");
19725
+ var import_child_process12 = require("child_process");
19413
19726
  init_daemon();
19414
19727
  function registerWatchCommand(program2) {
19415
19728
  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) => {
@@ -19426,7 +19739,7 @@ function registerWatchCommand(program2) {
19426
19739
  }
19427
19740
  } catch {
19428
19741
  console.error(import_chalk17.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
19429
- const child = (0, import_child_process13.spawn)(process.execPath, [process.argv[1], "daemon"], {
19742
+ const child = (0, import_child_process12.spawn)(process.execPath, [process.argv[1], "daemon"], {
19430
19743
  detached: true,
19431
19744
  stdio: "ignore",
19432
19745
  env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_WATCH_MODE: "1" }
@@ -19456,7 +19769,7 @@ function registerWatchCommand(program2) {
19456
19769
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
19457
19770
  )
19458
19771
  );
19459
- const result = (0, import_child_process13.spawnSync)(cmd, args, {
19772
+ const result = (0, import_child_process12.spawnSync)(cmd, args, {
19460
19773
  stdio: "inherit",
19461
19774
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
19462
19775
  });
@@ -19471,7 +19784,7 @@ function registerWatchCommand(program2) {
19471
19784
  // src/mcp-gateway/index.ts
19472
19785
  var import_readline3 = __toESM(require("readline"));
19473
19786
  var import_chalk18 = __toESM(require("chalk"));
19474
- var import_child_process14 = require("child_process");
19787
+ var import_child_process13 = require("child_process");
19475
19788
  var import_execa3 = require("execa");
19476
19789
  init_orchestrator();
19477
19790
  init_provenance();
@@ -19593,6 +19906,18 @@ function extractMcpServer(toolName) {
19593
19906
  const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
19594
19907
  return match?.[1];
19595
19908
  }
19909
+ function normalizeClientName(name) {
19910
+ if (typeof name !== "string" || name.length === 0) return void 0;
19911
+ const lower = name.toLowerCase();
19912
+ if (lower.includes("claude")) return "Claude";
19913
+ if (lower.includes("cursor")) return "Cursor";
19914
+ if (lower.includes("codex")) return "Codex";
19915
+ if (lower.includes("gemini")) return "Gemini";
19916
+ if (lower.includes("cline")) return "Cline";
19917
+ if (lower.includes("continue")) return "Continue";
19918
+ const sanitized = sanitize4(name).slice(0, 40);
19919
+ return sanitized.length > 0 ? sanitized : void 0;
19920
+ }
19596
19921
  function tokenize4(cmd) {
19597
19922
  const tokens = [];
19598
19923
  let current = "";
@@ -19665,7 +19990,7 @@ async function runMcpGateway(upstreamCommand) {
19665
19990
  const safeEnv = Object.fromEntries(
19666
19991
  Object.entries(process.env).filter(([k]) => !UPSTREAM_INJECTOR_VARS.has(k))
19667
19992
  );
19668
- const child = (0, import_child_process14.spawn)(executable, cmdArgs, {
19993
+ const child = (0, import_child_process13.spawn)(executable, cmdArgs, {
19669
19994
  stdio: ["pipe", "pipe", "inherit"],
19670
19995
  // control stdin/stdout; inherit stderr
19671
19996
  shell: false,
@@ -19679,6 +20004,8 @@ async function runMcpGateway(upstreamCommand) {
19679
20004
  let pinState = "pending";
19680
20005
  const pendingToolCalls = [];
19681
20006
  const pendingCallNames = /* @__PURE__ */ new Map();
20007
+ const pendingExecutions = /* @__PURE__ */ new Map();
20008
+ let clientName;
19682
20009
  const agentIn = import_readline3.default.createInterface({ input: process.stdin, terminal: false });
19683
20010
  agentIn.on("line", async (line) => {
19684
20011
  let message;
@@ -19701,6 +20028,13 @@ async function runMcpGateway(upstreamCommand) {
19701
20028
  child.stdin.write(line + "\n");
19702
20029
  return;
19703
20030
  }
20031
+ if (message.method === "initialize") {
20032
+ clientName = normalizeClientName(
20033
+ message.params?.clientInfo?.name
20034
+ );
20035
+ child.stdin.write(line + "\n");
20036
+ return;
20037
+ }
19704
20038
  if (message.method === "tools/list" && message.id !== void 0 && message.id !== null) {
19705
20039
  pendingToolsListIds.add(message.id);
19706
20040
  }
@@ -19746,7 +20080,7 @@ async function runMcpGateway(upstreamCommand) {
19746
20080
  const toolArgs = message.params?.arguments ?? message.params?.tool_input ?? {};
19747
20081
  const mcpServer = extractMcpServer(toolName);
19748
20082
  const result = await authorizeHeadless(toolName, toolArgs, {
19749
- agent: "MCP-Gateway",
20083
+ agent: clientName ?? "MCP-Gateway",
19750
20084
  mcpServer
19751
20085
  });
19752
20086
  if (!result.approved) {
@@ -19776,6 +20110,12 @@ async function runMcpGateway(upstreamCommand) {
19776
20110
  }
19777
20111
  if (message.id !== void 0 && message.id !== null) {
19778
20112
  pendingCallNames.set(message.id, toolName);
20113
+ pendingExecutions.set(message.id, {
20114
+ ts: Date.now(),
20115
+ toolName,
20116
+ agent: clientName,
20117
+ mcpServer
20118
+ });
19779
20119
  }
19780
20120
  child.stdin.write(line + "\n");
19781
20121
  } catch {
@@ -19917,6 +20257,26 @@ async function runMcpGateway(upstreamCommand) {
19917
20257
  return;
19918
20258
  }
19919
20259
  }
20260
+ const respId = parsed?.id;
20261
+ if (respId !== void 0 && respId !== null) {
20262
+ const exec = pendingExecutions.get(respId);
20263
+ if (exec) {
20264
+ pendingExecutions.delete(respId);
20265
+ const durationMs = Date.now() - exec.ts;
20266
+ const isError = parsed?.error !== void 0;
20267
+ notifyActivitySocket({
20268
+ id: String(respId),
20269
+ ts: Date.now(),
20270
+ tool: exec.toolName,
20271
+ status: "execution-completed",
20272
+ durationMs,
20273
+ agent: exec.agent,
20274
+ mcpServer: exec.mcpServer,
20275
+ isError
20276
+ }).catch(() => {
20277
+ });
20278
+ }
20279
+ }
19920
20280
  const LARGE_RESPONSE_THRESHOLD = 5e5;
19921
20281
  if (parsed?.result && line.length > LARGE_RESPONSE_THRESHOLD) {
19922
20282
  const callId = parsed.id;
@@ -19977,7 +20337,7 @@ var import_readline4 = __toESM(require("readline"));
19977
20337
  var import_fs36 = __toESM(require("fs"));
19978
20338
  var import_os32 = __toESM(require("os"));
19979
20339
  var import_path38 = __toESM(require("path"));
19980
- var import_child_process15 = require("child_process");
20340
+ var import_child_process14 = require("child_process");
19981
20341
  init_core();
19982
20342
  init_daemon();
19983
20343
  init_shields();
@@ -20451,7 +20811,7 @@ function handleRuleAdd(args) {
20451
20811
  return `Rule "${name}" added to ~/.node9/config.json \u2014 verdict: ${verdict} when ${field} matches "${pattern}"`;
20452
20812
  }
20453
20813
  function runCliCommand(subArgs) {
20454
- const result = (0, import_child_process15.spawnSync)(process.execPath, [process.argv[1], ...subArgs], {
20814
+ const result = (0, import_child_process14.spawnSync)(process.execPath, [process.argv[1], ...subArgs], {
20455
20815
  encoding: "utf-8",
20456
20816
  timeout: 6e4,
20457
20817
  // Disable colors — stdout is piped (not a TTY), chalk auto-detects, but be explicit