@node9/proxy 1.16.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.mjs CHANGED
@@ -4709,15 +4709,17 @@ async function authorizeHeadless(toolName, args, meta, options) {
4709
4709
  if (!options?.calledFromDaemon) {
4710
4710
  const actId = randomUUID();
4711
4711
  const actTs = Date.now();
4712
+ const stripAnsi2 = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
4713
+ const sanitizedAgent = meta?.agent ? stripAnsi2(meta.agent).slice(0, 80) : void 0;
4714
+ const sanitizedMcpServer = meta?.mcpServer ? stripAnsi2(meta.mcpServer).slice(0, 40) : void 0;
4712
4715
  const socketOk = await notifyActivity({
4713
4716
  id: actId,
4714
4717
  ts: actTs,
4715
4718
  tool: toolName,
4716
4719
  args,
4717
4720
  status: "pending",
4718
- // Strip ANSI escape sequences — agent name comes from caller-supplied metadata
4719
- // and may be displayed in a terminal (node9 tail/watch), enabling injection.
4720
- agent: meta?.agent ? meta.agent.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80) : void 0
4721
+ agent: sanitizedAgent,
4722
+ mcpServer: sanitizedMcpServer
4721
4723
  });
4722
4724
  const result = await _authorizeHeadlessCore(toolName, args, meta, {
4723
4725
  ...options,
@@ -4735,7 +4737,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
4735
4737
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
4736
4738
  label: result.blockedByLabel,
4737
4739
  ruleHit: result.ruleHit,
4738
- observeWouldBlock: result.observeWouldBlock
4740
+ observeWouldBlock: result.observeWouldBlock,
4741
+ agent: sanitizedAgent,
4742
+ mcpServer: sanitizedMcpServer
4739
4743
  });
4740
4744
  }
4741
4745
  return result;
@@ -10359,7 +10363,7 @@ function openBrowserLocal() {
10359
10363
  } catch {
10360
10364
  }
10361
10365
  }
10362
- async function autoStartDaemonAndWait(openBrowser2 = true) {
10366
+ async function autoStartDaemonAndWait(openBrowser = false) {
10363
10367
  if (isTestingMode()) return false;
10364
10368
  if (!path15.isAbsolute(process.argv[1])) return false;
10365
10369
  let resolvedArgv1;
@@ -10379,7 +10383,7 @@ async function autoStartDaemonAndWait(openBrowser2 = true) {
10379
10383
  env: {
10380
10384
  ...process.env,
10381
10385
  NODE9_AUTO_STARTED: "1",
10382
- ...openBrowser2 && { NODE9_BROWSER_OPENED: "1" }
10386
+ ...openBrowser && { NODE9_BROWSER_OPENED: "1" }
10383
10387
  }
10384
10388
  });
10385
10389
  child.unref();
@@ -10391,7 +10395,7 @@ async function autoStartDaemonAndWait(openBrowser2 = true) {
10391
10395
  signal: AbortSignal.timeout(500)
10392
10396
  });
10393
10397
  if (res.ok) {
10394
- if (openBrowser2) {
10398
+ if (openBrowser) {
10395
10399
  openBrowserLocal();
10396
10400
  }
10397
10401
  return true;
@@ -11779,370 +11783,681 @@ function printRuleGroup(rule, topN, drillDown, previewWidth) {
11779
11783
  );
11780
11784
  }
11781
11785
  }
11782
- function registerScanCommand(program2) {
11783
- 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) => {
11784
- const drillDown = options.drillDown ?? false;
11785
- const topN = drillDown ? Infinity : Math.max(1, parseInt(options.top, 10) || 5);
11786
- const previewWidth = 70;
11787
- const startDate = options.all ? null : (() => {
11788
- const d = /* @__PURE__ */ new Date();
11789
- d.setDate(d.getDate() - (parseInt(options.days, 10) || 30));
11790
- d.setHours(0, 0, 0, 0);
11791
- return d;
11792
- })();
11793
- const isWired = getAgentsStatus().some((a) => a.wired);
11794
- console.log("");
11795
- if (!isWired) {
11796
- console.log(
11797
- chalk3.bold("\u{1F6E1} node9") + chalk3.dim(" \u2014 security layer for AI coding agents")
11798
- );
11799
- console.log(
11800
- chalk3.dim(" Intercepts dangerous tool calls before they execute. No config needed.")
11801
- );
11802
- console.log("");
11786
+ function compactRuleLabel(name) {
11787
+ let label = name.replace(/^shield:[^:]+:/, "");
11788
+ label = label.replace(/^(block|review|allow)-/, "");
11789
+ return label.replace(/-+/g, "-");
11790
+ }
11791
+ function renderCompactScorecard(input) {
11792
+ const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
11793
+ const totalRisky = scan.findings.length + scan.dlpFindings.length;
11794
+ const dateRange = scan.firstDate && scan.lastDate ? `${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}` : "";
11795
+ console.log(
11796
+ chalk3.bold("\u{1F6E1} Node9 Scan") + chalk3.dim(" \xB7 ") + chalk3.white(num(scan.sessions)) + chalk3.dim(" sessions \xB7 ") + chalk3.white(num(scan.totalToolCalls)) + chalk3.dim(" tool calls") + (dateRange ? chalk3.dim(" \xB7 " + dateRange) : "")
11797
+ );
11798
+ console.log("");
11799
+ const scoreColor = blast.score >= 80 ? chalk3.green : blast.score >= 50 ? chalk3.yellow : chalk3.red;
11800
+ const scoreSeverity = blast.score >= 80 ? "Good" : blast.score >= 50 ? "At Risk" : "Critical";
11801
+ console.log(
11802
+ chalk3.bold("Security Score: ") + scoreColor.bold(`${blast.score}/100`) + chalk3.dim(" \xB7 ") + scoreColor(scoreSeverity)
11803
+ );
11804
+ if (scan.totalCostUSD > 0) {
11805
+ console.log(
11806
+ chalk3.bold(fmtCost(scan.totalCostUSD)) + chalk3.dim(" AI spend \xB7 ") + chalk3.bold(`${totalRisky}`) + chalk3.dim(` risky operation${totalRisky !== 1 ? "s" : ""}`)
11807
+ );
11808
+ }
11809
+ console.log("");
11810
+ if (scan.dlpFindings.length > 0) {
11811
+ const patternCounts = /* @__PURE__ */ new Map();
11812
+ for (const f of scan.dlpFindings) {
11813
+ patternCounts.set(f.patternName, (patternCounts.get(f.patternName) ?? 0) + 1);
11803
11814
  }
11815
+ const topPatterns = [...patternCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name, count]) => count > 1 ? `${name} \xD7${count}` : name).join(", ");
11804
11816
  console.log(
11805
- chalk3.cyan.bold("\u{1F50D} Scanning your AI history") + chalk3.dim(" \u2014 what would node9 have caught?")
11817
+ chalk3.red("\u{1F511} ") + chalk3.red.bold(String(scan.dlpFindings.length).padEnd(4)) + chalk3.dim("credential leak".padEnd(20)) + chalk3.dim(`(${topPatterns})`)
11806
11818
  );
11807
- console.log("");
11808
- const useTTY = process.stdout.isTTY === true && process.env.NODE9_WRAPPER !== "1";
11809
- if (!useTTY) {
11810
- process.stdout.write(
11811
- " " + chalk3.dim("Scanning your history \u2014 this may take a moment...\n")
11812
- );
11819
+ }
11820
+ if (blockedCount > 0) {
11821
+ const blockedRules = [];
11822
+ for (const section of summary.sections) {
11823
+ for (const rule of section.rules) {
11824
+ if (rule.verdict === "block") {
11825
+ blockedRules.push({ name: rule.name, count: rule.findings.length });
11826
+ }
11827
+ }
11813
11828
  }
11814
- const totalFiles = countScanFiles();
11815
- let filesScanned = 0;
11816
- let linesScanned = 0;
11817
- let lastRender = 0;
11818
- const onProgress = (done) => {
11819
- filesScanned = done;
11820
- if (useTTY) renderProgressBar(filesScanned, totalFiles, linesScanned);
11821
- lastRender = Date.now();
11822
- };
11823
- const onLine = () => {
11824
- linesScanned++;
11825
- const now = Date.now();
11826
- if (useTTY && now - lastRender >= 80) {
11827
- lastRender = now;
11828
- renderProgressBar(filesScanned, totalFiles, linesScanned);
11829
+ const topBlocked = blockedRules.sort((a, b) => b.count - a.count).slice(0, 3).map(
11830
+ (r) => r.count > 1 ? `${compactRuleLabel(r.name)} \xD7${r.count}` : compactRuleLabel(r.name)
11831
+ ).join(", ");
11832
+ console.log(
11833
+ chalk3.red("\u{1F6D1} ") + chalk3.red.bold(String(blockedCount).padEnd(4)) + chalk3.dim("would have blocked".padEnd(20)) + chalk3.dim(`(${topBlocked})`)
11834
+ );
11835
+ }
11836
+ if (scan.loopFindings.length > 0) {
11837
+ const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
11838
+ const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
11839
+ const wasteParts = [];
11840
+ if (wastePct > 0) wasteParts.push(`${wastePct}% wasted`);
11841
+ if (summary.loopWastedUSD > 0) wasteParts.push("~" + fmtCost(summary.loopWastedUSD));
11842
+ const wasteSummary = wasteParts.length ? `(${wasteParts.join(" \xB7 ")})` : "";
11843
+ console.log(
11844
+ chalk3.yellow("\u{1F501} ") + chalk3.yellow.bold(String(scan.loopFindings.length).padEnd(4)) + chalk3.dim("agent loops".padEnd(20)) + chalk3.dim(wasteSummary)
11845
+ );
11846
+ }
11847
+ if (reviewCount > 0) {
11848
+ const reviewRules = [];
11849
+ for (const section of summary.sections) {
11850
+ for (const rule of section.rules) {
11851
+ if (rule.verdict !== "block") {
11852
+ reviewRules.push({ name: rule.name, count: rule.findings.length });
11853
+ }
11829
11854
  }
11830
- };
11831
- if (useTTY) renderProgressBar(0, totalFiles, 0);
11832
- const claudeScan = scanClaudeHistory(startDate, onProgress, onLine);
11833
- const geminiScan = scanGeminiHistory(
11834
- startDate,
11835
- (done) => onProgress(claudeScan.filesScanned + done),
11836
- onLine
11855
+ }
11856
+ const topReview = reviewRules.sort((a, b) => b.count - a.count).slice(0, 3).map(
11857
+ (r) => r.count > 1 ? `${compactRuleLabel(r.name)} \xD7${r.count}` : compactRuleLabel(r.name)
11858
+ ).join(", ");
11859
+ console.log(
11860
+ chalk3.yellow("\u{1F441} ") + chalk3.yellow.bold(String(reviewCount).padEnd(4)) + chalk3.dim("flagged for review".padEnd(20)) + chalk3.dim(`(${topReview})`)
11837
11861
  );
11838
- const codexScan = scanCodexHistory(
11839
- startDate,
11840
- (done) => onProgress(claudeScan.filesScanned + geminiScan.filesScanned + done),
11841
- onLine
11862
+ }
11863
+ console.log("");
11864
+ if (blastExposures > 0) {
11865
+ const categories = /* @__PURE__ */ new Set();
11866
+ for (const r of blast.reachable) {
11867
+ const lower = r.label.toLowerCase();
11868
+ if (lower.includes("ssh")) categories.add("ssh");
11869
+ else if (lower.includes("aws")) categories.add("aws");
11870
+ else if (lower.includes("gcloud") || lower.includes("gcp")) categories.add("gcp");
11871
+ else if (lower.includes("docker")) categories.add("docker");
11872
+ else if (lower.includes("netrc")) categories.add("netrc");
11873
+ else if (lower.includes("kube")) categories.add("k8s");
11874
+ else if (lower.includes("npmrc")) categories.add("npm");
11875
+ else categories.add("other");
11876
+ }
11877
+ if (blast.envFindings.length > 0) categories.add("env");
11878
+ const catList = [...categories].slice(0, 6).join(" \xD7 ");
11879
+ console.log(
11880
+ chalk3.red("\u{1F52D} ") + chalk3.dim("Blast radius".padEnd(24)) + chalk3.dim(`${catList} (${blastExposures} exposure${blastExposures !== 1 ? "s" : ""})`)
11842
11881
  );
11843
- const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
11844
- scan.dlpFindings.push(...scanShellConfig());
11845
- const summary = buildScanSummary([
11846
- { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
11847
- { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
11848
- { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
11849
- ]);
11850
- if (useTTY) process.stdout.write("\r" + " ".repeat(60) + "\r");
11851
- if (scan.filesScanned === 0) {
11852
- console.log(chalk3.yellow(" No session history found."));
11853
- console.log(
11854
- chalk3.gray(
11855
- " Supported: Claude Code (~/.claude/projects/) \xB7 Gemini CLI (~/.gemini/tmp/)\n"
11856
- )
11857
- );
11858
- return;
11882
+ console.log("");
11883
+ }
11884
+ console.log(
11885
+ chalk3.dim("\u2192 ") + chalk3.cyan("npx node9-ai scan") + chalk3.dim(" run this on your machine")
11886
+ );
11887
+ console.log(chalk3.dim("\u2192 github.com/node9-ai/node9-proxy"));
11888
+ console.log("");
11889
+ }
11890
+ function classifyRuleSeverity(name, verdict) {
11891
+ const n = name.toLowerCase();
11892
+ const criticalPatterns = [
11893
+ "rm-rf",
11894
+ "eval-remote",
11895
+ "eval-curl",
11896
+ "read-aws",
11897
+ "read-ssh",
11898
+ "read-gcp",
11899
+ "read-cred",
11900
+ "delete-repo",
11901
+ "helm-uninstall",
11902
+ "drop-table",
11903
+ "drop-database",
11904
+ "drop-collection",
11905
+ "truncate",
11906
+ "flushall",
11907
+ "flushdb",
11908
+ "pipe-shell"
11909
+ ];
11910
+ if (criticalPatterns.some((p) => n.includes(p))) return "critical";
11911
+ const highPatterns = [
11912
+ "force-push",
11913
+ "force_push",
11914
+ "git-destructive",
11915
+ "reset-hard",
11916
+ "rebase",
11917
+ "delete-branch",
11918
+ "delete-remote"
11919
+ ];
11920
+ if (highPatterns.some((p) => n.includes(p))) return "high";
11921
+ if (verdict === "block") return "high";
11922
+ return "medium";
11923
+ }
11924
+ function narrativeRuleLabel(name) {
11925
+ const stripped = compactRuleLabel(name);
11926
+ const map = {
11927
+ "read-aws": "AWS credentials read",
11928
+ "read-ssh": "SSH private key read",
11929
+ "read-gcp": "GCP credentials read",
11930
+ "read-cred": "credential file read",
11931
+ "delete-repo": "GitHub repository deletion",
11932
+ "helm-uninstall": "helm uninstall",
11933
+ "rm-rf-home": "rm -rf on home directory",
11934
+ "eval-remote": "eval of remote download",
11935
+ "pipe-shell": "curl | bash",
11936
+ "drop-table": "DROP TABLE",
11937
+ "drop-database": "DROP DATABASE",
11938
+ truncate: "TRUNCATE",
11939
+ flushall: "Redis FLUSHALL",
11940
+ flushdb: "Redis FLUSHDB",
11941
+ "force-push": "force pushes",
11942
+ "git-destructive": "destructive git operations",
11943
+ rm: "rm calls",
11944
+ sudo: "sudo calls",
11945
+ "eval-dynamic": "dynamic eval",
11946
+ "config-set": "Redis CONFIG SET"
11947
+ };
11948
+ for (const [key, label] of Object.entries(map)) {
11949
+ if (stripped.includes(key)) return label;
11950
+ }
11951
+ return stripped;
11952
+ }
11953
+ function renderNarrativeScorecard(input) {
11954
+ const { scan, summary, blast, blastExposures } = input;
11955
+ const critical = [];
11956
+ const high = [];
11957
+ const medium = [];
11958
+ if (scan.dlpFindings.length > 0) {
11959
+ const patterns = /* @__PURE__ */ new Map();
11960
+ for (const f of scan.dlpFindings) {
11961
+ patterns.set(f.patternName, (patterns.get(f.patternName) ?? 0) + 1);
11962
+ }
11963
+ const top = [...patterns.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name, n]) => n > 1 ? `${name} \xD7${n}` : name).join(", ");
11964
+ critical.push({
11965
+ label: `${scan.dlpFindings.length} credential leak${scan.dlpFindings.length !== 1 ? "s" : ""} (${top})`,
11966
+ count: scan.dlpFindings.length
11967
+ });
11968
+ }
11969
+ for (const section of summary.sections) {
11970
+ for (const rule of section.rules) {
11971
+ const sev = classifyRuleSeverity(rule.name, rule.verdict);
11972
+ const label = narrativeRuleLabel(rule.name);
11973
+ const count = rule.findings.length;
11974
+ const display = count > 1 ? `${label} \xD7${count}` : label;
11975
+ const entry = { label: display, count };
11976
+ if (sev === "critical") critical.push(entry);
11977
+ else if (sev === "high") high.push(entry);
11978
+ else medium.push(entry);
11979
+ }
11980
+ }
11981
+ if (blastExposures > 0) {
11982
+ high.push({
11983
+ label: `${blastExposures} credential file${blastExposures !== 1 ? "s" : ""} reachable on disk`,
11984
+ count: blastExposures
11985
+ });
11986
+ }
11987
+ if (scan.loopFindings.length > 0) {
11988
+ const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
11989
+ const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
11990
+ const cost = summary.loopWastedUSD > 0 ? `, ~${fmtCost(summary.loopWastedUSD)} wasted` : "";
11991
+ medium.push({
11992
+ label: `${scan.loopFindings.length} agent loops (${wastePct}% of calls${cost})`,
11993
+ count: scan.loopFindings.length
11994
+ });
11995
+ }
11996
+ const sortByCount = (a, b) => b.count - a.count;
11997
+ critical.sort(sortByCount);
11998
+ high.sort(sortByCount);
11999
+ medium.sort(sortByCount);
12000
+ const criticalCount = critical.reduce((s, e) => s + e.count, 0);
12001
+ const highCount = high.reduce((s, e) => s + e.count, 0);
12002
+ const mediumCount = medium.reduce((s, e) => s + e.count, 0);
12003
+ const dateRange = scan.firstDate && scan.lastDate ? `${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}` : "";
12004
+ console.log(
12005
+ chalk3.bold("\u{1F6E1} Node9 Scan") + chalk3.dim(" \xB7 ") + chalk3.white(num(scan.sessions)) + chalk3.dim(" sessions") + (scan.totalCostUSD > 0 ? chalk3.dim(" \xB7 ") + chalk3.bold(fmtCost(scan.totalCostUSD)) + chalk3.dim(" spend") : "") + (dateRange ? chalk3.dim(" \xB7 " + dateRange) : "")
12006
+ );
12007
+ console.log("");
12008
+ const scoreColor = blast.score >= 80 ? chalk3.green : blast.score >= 50 ? chalk3.yellow : chalk3.red;
12009
+ const scoreSeverity = blast.score >= 80 ? "Good" : blast.score >= 50 ? "At Risk" : "Critical";
12010
+ console.log(
12011
+ (blast.score < 50 ? chalk3.red.bold("\u26A0 ") : "") + chalk3.bold("Security Score: ") + scoreColor.bold(`${blast.score}/100`) + chalk3.dim(" \xB7 ") + scoreColor(scoreSeverity)
12012
+ );
12013
+ console.log("");
12014
+ if (criticalCount > 0) {
12015
+ console.log(
12016
+ chalk3.red.bold(" \u{1F534} CRITICAL ") + chalk3.red(`${criticalCount} finding${criticalCount !== 1 ? "s" : ""}`)
12017
+ );
12018
+ for (const entry of critical.slice(0, 5)) {
12019
+ console.log(chalk3.dim(" \u2022 ") + chalk3.red(entry.label));
11859
12020
  }
11860
- const rangeLabel = options.all ? chalk3.dim("all time") : chalk3.dim(`last ${options.days ?? 30} days`);
11861
- const dateRange = scan.firstDate && scan.lastDate ? chalk3.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
11862
- const breakdownParts = [];
11863
- if (claudeScan.sessions > 0)
11864
- breakdownParts.push(chalk3.cyan(String(claudeScan.sessions)) + chalk3.dim(" Claude"));
11865
- if (geminiScan.sessions > 0)
11866
- breakdownParts.push(chalk3.blue(String(geminiScan.sessions)) + chalk3.dim(" Gemini"));
11867
- if (codexScan.sessions > 0)
11868
- breakdownParts.push(chalk3.magenta(String(codexScan.sessions)) + chalk3.dim(" Codex"));
11869
- const sessionBreakdown = breakdownParts.length > 1 ? chalk3.dim("(") + breakdownParts.join(chalk3.dim(" \xB7 ")) + chalk3.dim(")") : "";
12021
+ if (critical.length > 5) {
12022
+ const remaining = critical.length - 5;
12023
+ console.log(chalk3.dim(` \u2022 \u2026 and ${remaining} more`));
12024
+ }
12025
+ console.log("");
12026
+ }
12027
+ if (highCount > 0) {
11870
12028
  console.log(
11871
- " " + chalk3.white(num(scan.sessions)) + chalk3.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + chalk3.white(num(scan.totalToolCalls)) + chalk3.dim(" tool calls ") + chalk3.white(num(scan.bashCalls)) + chalk3.dim(" bash commands ") + rangeLabel + dateRange
12029
+ chalk3.yellow.bold(" \u{1F7E1} HIGH ") + chalk3.yellow(`${highCount} finding${highCount !== 1 ? "s" : ""}`)
11872
12030
  );
12031
+ for (const entry of high.slice(0, 5)) {
12032
+ console.log(chalk3.dim(" \u2022 ") + chalk3.yellow(entry.label));
12033
+ }
12034
+ if (high.length > 5) {
12035
+ const remaining = high.length - 5;
12036
+ console.log(chalk3.dim(` \u2022 \u2026 and ${remaining} more`));
12037
+ }
11873
12038
  console.log("");
11874
- const totalFindings = scan.findings.length;
11875
- const blockedCount = scan.findings.filter((f) => f.source.rule.verdict === "block").length;
11876
- const reviewCount = totalFindings - blockedCount;
11877
- if (totalFindings === 0 && scan.dlpFindings.length === 0) {
11878
- console.log(chalk3.green(" \u2705 No risky operations found in your history."));
11879
- console.log(
11880
- chalk3.dim(" node9 is still worth running \u2014 it monitors every tool call in real time.\n")
11881
- );
11882
- } else {
11883
- const totalRisky = totalFindings + scan.dlpFindings.length;
11884
- const heroLine = isWired ? chalk3.bold(
11885
- ` Found ${chalk3.yellow(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} in your history`
11886
- ) : chalk3.bold(
11887
- ` ${chalk3.red.bold(String(totalRisky))} risky operation${totalRisky !== 1 ? "s" : ""} found \u2014 none were blocked`
11888
- );
11889
- console.log(heroLine);
11890
- console.log("");
11891
- if (scan.totalCostUSD > 0) {
12039
+ }
12040
+ if (mediumCount > 0) {
12041
+ console.log(
12042
+ chalk3.bold(" \u{1F7E2} MEDIUM ") + chalk3.dim(`${mediumCount} finding${mediumCount !== 1 ? "s" : ""}`)
12043
+ );
12044
+ for (const entry of medium.slice(0, 5)) {
12045
+ console.log(chalk3.dim(" \u2022 ") + chalk3.dim(entry.label));
12046
+ }
12047
+ if (medium.length > 5) {
12048
+ const remaining = medium.length - 5;
12049
+ console.log(chalk3.dim(` \u2022 \u2026 and ${remaining} more`));
12050
+ }
12051
+ console.log("");
12052
+ }
12053
+ console.log(
12054
+ chalk3.dim("\u2192 ") + chalk3.cyan("npx node9-ai scan") + chalk3.dim(" run this on your machine")
12055
+ );
12056
+ console.log(chalk3.dim("\u2192 github.com/node9-ai/node9-proxy"));
12057
+ console.log("");
12058
+ }
12059
+ function registerScanCommand(program2) {
12060
+ 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(
12061
+ async (options) => {
12062
+ const drillDown = options.drillDown ?? false;
12063
+ const topN = drillDown ? Infinity : Math.max(1, parseInt(options.top, 10) || 5);
12064
+ const previewWidth = 70;
12065
+ const startDate = options.all ? null : (() => {
12066
+ const d = /* @__PURE__ */ new Date();
12067
+ d.setDate(d.getDate() - (parseInt(options.days, 10) || 30));
12068
+ d.setHours(0, 0, 0, 0);
12069
+ return d;
12070
+ })();
12071
+ const isWired = getAgentsStatus().some((a) => a.wired);
12072
+ const screenshotMode = options.compact || options.narrative;
12073
+ if (!screenshotMode) {
12074
+ console.log("");
12075
+ if (!isWired) {
12076
+ console.log(
12077
+ chalk3.bold("\u{1F6E1} node9") + chalk3.dim(" \u2014 security layer for AI coding agents")
12078
+ );
12079
+ console.log(
12080
+ chalk3.dim(" Intercepts dangerous tool calls before they execute. No config needed.")
12081
+ );
12082
+ console.log("");
12083
+ }
11892
12084
  console.log(
11893
- " " + chalk3.bold(fmtCost(scan.totalCostUSD)) + chalk3.dim(" AI spend \xB7 ") + chalk3.dim(`${totalRisky} risky operations`)
12085
+ chalk3.cyan.bold("\u{1F50D} Scanning your AI history") + chalk3.dim(" \u2014 what would node9 have caught?")
11894
12086
  );
12087
+ console.log("");
11895
12088
  }
11896
- if (scan.dlpFindings.length > 0) {
11897
- const earlyLabel = scan.sessionsWithEarlySecrets > 0 ? chalk3.dim(" \xB7 ") + chalk3.red(
11898
- `${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
11899
- ) : "";
11900
- console.log(
11901
- " " + chalk3.red("\u{1F511} Credential leak") + " " + chalk3.red.bold(String(scan.dlpFindings.length).padStart(5)) + chalk3.dim(" secret detected in history or shell config") + earlyLabel
12089
+ const useTTY = process.stdout.isTTY === true && process.env.NODE9_WRAPPER !== "1";
12090
+ if (!useTTY && !screenshotMode) {
12091
+ process.stdout.write(
12092
+ " " + chalk3.dim("Scanning your history \u2014 this may take a moment...\n")
11902
12093
  );
11903
12094
  }
11904
- if (blockedCount > 0) {
12095
+ const totalFiles = countScanFiles();
12096
+ let filesScanned = 0;
12097
+ let linesScanned = 0;
12098
+ let lastRender = 0;
12099
+ const onProgress = (done) => {
12100
+ filesScanned = done;
12101
+ if (useTTY) renderProgressBar(filesScanned, totalFiles, linesScanned);
12102
+ lastRender = Date.now();
12103
+ };
12104
+ const onLine = () => {
12105
+ linesScanned++;
12106
+ const now = Date.now();
12107
+ if (useTTY && now - lastRender >= 80) {
12108
+ lastRender = now;
12109
+ renderProgressBar(filesScanned, totalFiles, linesScanned);
12110
+ }
12111
+ };
12112
+ if (useTTY) renderProgressBar(0, totalFiles, 0);
12113
+ const claudeScan = scanClaudeHistory(startDate, onProgress, onLine);
12114
+ const geminiScan = scanGeminiHistory(
12115
+ startDate,
12116
+ (done) => onProgress(claudeScan.filesScanned + done),
12117
+ onLine
12118
+ );
12119
+ const codexScan = scanCodexHistory(
12120
+ startDate,
12121
+ (done) => onProgress(claudeScan.filesScanned + geminiScan.filesScanned + done),
12122
+ onLine
12123
+ );
12124
+ const scan = mergeScans(mergeScans(claudeScan, geminiScan), codexScan);
12125
+ scan.dlpFindings.push(...scanShellConfig());
12126
+ const summary = buildScanSummary([
12127
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
12128
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
12129
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
12130
+ ]);
12131
+ if (useTTY) process.stdout.write("\r" + " ".repeat(60) + "\r");
12132
+ if (scan.filesScanned === 0) {
12133
+ console.log(chalk3.yellow(" No session history found."));
11905
12134
  console.log(
11906
- " " + chalk3.red("\u{1F6D1} Would have blocked") + " " + chalk3.red.bold(String(blockedCount).padStart(5)) + chalk3.dim(" operations stopped before execution")
12135
+ chalk3.gray(
12136
+ " Supported: Claude Code (~/.claude/projects/) \xB7 Gemini CLI (~/.gemini/tmp/)\n"
12137
+ )
11907
12138
  );
12139
+ return;
11908
12140
  }
11909
- if (scan.loopFindings.length > 0) {
11910
- const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
11911
- const loopRatio = scan.totalToolCalls > 0 ? chalk3.dim(" \xB7 ") + chalk3.yellow(
11912
- Math.round(wastedCalls / scan.totalToolCalls * 100) + "% of calls wasted"
11913
- ) : "";
11914
- const loopCost = summary.loopWastedUSD > 0 ? chalk3.dim(" \xB7 ") + chalk3.yellow("~" + fmtCost(summary.loopWastedUSD)) : "";
12141
+ const rangeLabel = options.all ? chalk3.dim("all time") : chalk3.dim(`last ${options.days ?? 30} days`);
12142
+ const dateRange = scan.firstDate && scan.lastDate ? chalk3.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
12143
+ const breakdownParts = [];
12144
+ if (claudeScan.sessions > 0)
12145
+ breakdownParts.push(chalk3.cyan(String(claudeScan.sessions)) + chalk3.dim(" Claude"));
12146
+ if (geminiScan.sessions > 0)
12147
+ breakdownParts.push(chalk3.blue(String(geminiScan.sessions)) + chalk3.dim(" Gemini"));
12148
+ if (codexScan.sessions > 0)
12149
+ breakdownParts.push(chalk3.magenta(String(codexScan.sessions)) + chalk3.dim(" Codex"));
12150
+ const sessionBreakdown = breakdownParts.length > 1 ? chalk3.dim("(") + breakdownParts.join(chalk3.dim(" \xB7 ")) + chalk3.dim(")") : "";
12151
+ if (!screenshotMode) {
11915
12152
  console.log(
11916
- " " + chalk3.yellow("\u{1F501} Loop detected") + " " + chalk3.yellow.bold(String(scan.loopFindings.length).padStart(5)) + chalk3.dim(" repeated tool call patterns found") + loopRatio + loopCost
12153
+ " " + chalk3.white(num(scan.sessions)) + chalk3.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + chalk3.white(num(scan.totalToolCalls)) + chalk3.dim(" tool calls ") + chalk3.white(num(scan.bashCalls)) + chalk3.dim(" bash commands ") + rangeLabel + dateRange
11917
12154
  );
12155
+ console.log("");
11918
12156
  }
11919
- if (reviewCount > 0) {
11920
- console.log(
11921
- " " + chalk3.yellow("\u{1F441} Would have flagged") + " " + chalk3.yellow.bold(String(reviewCount).padStart(5)) + chalk3.dim(" sent to you for approval")
11922
- );
12157
+ const totalFindings = scan.findings.length;
12158
+ const blockedCount = scan.findings.filter((f) => f.source.rule.verdict === "block").length;
12159
+ const reviewCount = totalFindings - blockedCount;
12160
+ const blast = runBlast();
12161
+ const blastExposures = blast.reachable.length + blast.envFindings.length;
12162
+ if (options.compact) {
12163
+ renderCompactScorecard({
12164
+ scan,
12165
+ summary,
12166
+ blast,
12167
+ blastExposures,
12168
+ blockedCount,
12169
+ reviewCount
12170
+ });
12171
+ return;
11923
12172
  }
11924
- console.log("");
11925
- if (scan.dlpFindings.length > 0) {
11926
- console.log(" " + chalk3.dim("\u2500".repeat(70)));
12173
+ if (options.narrative) {
12174
+ renderNarrativeScorecard({
12175
+ scan,
12176
+ summary,
12177
+ blast,
12178
+ blastExposures,
12179
+ blockedCount,
12180
+ reviewCount
12181
+ });
12182
+ return;
12183
+ }
12184
+ if (totalFindings === 0 && scan.dlpFindings.length === 0) {
12185
+ console.log(chalk3.green(" \u2705 No risky operations found in your history."));
11927
12186
  console.log(
11928
- " " + chalk3.red.bold("\u{1F511} Credential Leaks") + chalk3.dim(" \xB7 ") + chalk3.red(
11929
- `${num(scan.dlpFindings.length)} secret${scan.dlpFindings.length !== 1 ? "s" : ""} found in plain text`
12187
+ chalk3.dim(
12188
+ " node9 is still worth running \u2014 it monitors every tool call in real time.\n"
11930
12189
  )
11931
12190
  );
11932
- const sortedDlp = sortDlpFindingsByPriority(scan.dlpFindings);
11933
- const recurringPatterns = buildRecurringPatternSet(scan.dlpFindings);
11934
- const shownDlp = drillDown ? sortedDlp : sortedDlp.slice(0, topN);
11935
- for (const f of shownDlp) {
11936
- const stale = isStaleFinding(f.timestamp);
11937
- const ts = f.timestamp ? chalk3.dim(fmtTs(f.timestamp) + " ") : "";
11938
- const proj = chalk3.dim(f.project.slice(0, 22).padEnd(22) + " ");
11939
- const agentBadge = f.agent === "gemini" ? chalk3.blue("[Gemini] ") : f.agent === "codex" ? chalk3.magenta("[Codex] ") : f.agent === "shell" ? chalk3.yellow("[Shell] ") : chalk3.cyan("[Claude] ");
11940
- const sessionSuffix = f.sessionId ? chalk3.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
11941
- const recurringBadge = recurringPatterns.has(f.patternName) ? chalk3.red.bold(" \u26A0\uFE0F recurring ") : "";
11942
- const patternDisplay = stale ? chalk3.dim(f.patternName) : chalk3.yellow(f.patternName);
11943
- const sampleDisplay = stale ? chalk3.dim(f.redactedSample) : chalk3.gray(f.redactedSample);
11944
- const entryBadge = chalk3.dim(` [${entryPathLabel(f.toolName)}]`);
11945
- const leadIcon = stale ? chalk3.dim("\u{1F6A8}") : "\u{1F6A8}";
12191
+ } else {
12192
+ const totalRisky = totalFindings + scan.dlpFindings.length;
12193
+ const scoreSeverity = blast.score >= 80 ? chalk3.green("Good") : blast.score >= 50 ? chalk3.yellow("At Risk") : chalk3.red.bold("Critical");
12194
+ const scoreColor = blast.score >= 80 ? chalk3.green : blast.score >= 50 ? chalk3.yellow : chalk3.red;
12195
+ console.log(
12196
+ " " + (blast.score < 50 ? chalk3.red.bold("\u26A0 ") : "") + chalk3.bold("Security Score ") + scoreColor.bold(`${blast.score}/100`) + " " + scoreSeverity + chalk3.dim(" \xB7 ") + (totalRisky > 0 ? chalk3.red.bold(`${totalRisky} risky operation${totalRisky !== 1 ? "s" : ""}`) : chalk3.green("No risky operations"))
12197
+ );
12198
+ const cardParts = [];
12199
+ if (scan.dlpFindings.length > 0) {
12200
+ cardParts.push(
12201
+ chalk3.red("\u{1F511} ") + chalk3.red.bold(String(scan.dlpFindings.length)) + chalk3.dim(` leak${scan.dlpFindings.length !== 1 ? "s" : ""}`)
12202
+ );
12203
+ }
12204
+ if (blockedCount > 0) {
12205
+ cardParts.push(
12206
+ chalk3.red("\u{1F6D1} ") + chalk3.red.bold(String(blockedCount)) + chalk3.dim(" blocked")
12207
+ );
12208
+ }
12209
+ if (scan.loopFindings.length > 0) {
12210
+ const wastedCalls = scan.loopFindings.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
12211
+ const wastePct = scan.totalToolCalls > 0 ? Math.round(wastedCalls / scan.totalToolCalls * 100) : 0;
12212
+ const wasteSuffix = wastePct > 0 ? chalk3.dim(` (${wastePct}% wasted)`) : "";
12213
+ cardParts.push(
12214
+ chalk3.yellow("\u{1F501} ") + chalk3.yellow.bold(String(scan.loopFindings.length)) + chalk3.dim(" loops") + wasteSuffix
12215
+ );
12216
+ }
12217
+ if (reviewCount > 0) {
12218
+ cardParts.push(
12219
+ chalk3.yellow("\u{1F441} ") + chalk3.yellow.bold(String(reviewCount)) + chalk3.dim(" flagged")
12220
+ );
12221
+ }
12222
+ if (blastExposures > 0) {
12223
+ cardParts.push(
12224
+ chalk3.red("\u{1F52D} ") + chalk3.red.bold(String(blastExposures)) + chalk3.dim(" exposures")
12225
+ );
12226
+ }
12227
+ if (cardParts.length > 0) {
12228
+ console.log(" " + cardParts.join(chalk3.dim(" ")));
12229
+ }
12230
+ if (scan.totalCostUSD > 0) {
11946
12231
  console.log(
11947
- ` ${leadIcon} ${ts}${proj}${agentBadge}` + patternDisplay + recurringBadge + chalk3.dim(" ") + sampleDisplay + entryBadge + sessionSuffix
12232
+ " " + chalk3.dim("AI spend ") + chalk3.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? chalk3.dim(" \xB7 wasted on loops ") + chalk3.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
11948
12233
  );
11949
12234
  }
11950
- if (!drillDown && scan.dlpFindings.length > topN) {
12235
+ if (scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
11951
12236
  console.log(
11952
- chalk3.dim(
11953
- ` \u2026 and ${scan.dlpFindings.length - topN} more (--drill-down for full list)`
12237
+ " " + chalk3.dim(
12238
+ `${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
11954
12239
  )
11955
12240
  );
11956
12241
  }
11957
12242
  console.log("");
11958
- }
11959
- const blockedRuleSections = summary.sections.map((s) => ({ ...s, rules: s.rules.filter((r) => r.verdict === "block") })).filter((s) => s.rules.length > 0);
11960
- if (blockedRuleSections.length > 0) {
11961
- console.log(" " + chalk3.dim("\u2500".repeat(70)));
11962
- console.log(
11963
- " " + chalk3.red.bold("\u{1F6D1} Blocked") + chalk3.dim(" \xB7 ") + chalk3.red(
11964
- `${blockedCount} operation${blockedCount !== 1 ? "s" : ""} node9 would have stopped`
11965
- )
11966
- );
11967
- for (const section of blockedRuleSections) {
11968
- for (const rule of section.rules) {
11969
- printRuleGroup(rule, topN, drillDown, previewWidth);
11970
- }
11971
- }
11972
- console.log("");
11973
- }
11974
- if (scan.loopFindings.length > 0) {
11975
- console.log(" " + chalk3.dim("\u2500".repeat(70)));
11976
- const loopCostLabel = summary.loopWastedUSD > 0 ? chalk3.dim(" \xB7 ") + chalk3.yellow("~" + fmtCost(summary.loopWastedUSD) + " wasted") : "";
11977
- console.log(
11978
- " " + chalk3.yellow.bold("\u{1F501} Agent Loops") + chalk3.dim(" \xB7 ") + chalk3.yellow(
11979
- `${num(scan.loopFindings.length)} repeated pattern${scan.loopFindings.length !== 1 ? "s" : ""} found`
11980
- ) + loopCostLabel
11981
- );
11982
- const shownLoops = drillDown ? scan.loopFindings : scan.loopFindings.slice(0, topN);
11983
- for (const f of shownLoops) {
11984
- const ts = f.timestamp ? chalk3.dim(fmtTs(f.timestamp) + " ") : "";
11985
- const proj = chalk3.dim(f.project.slice(0, 22).padEnd(22) + " ");
11986
- const agentBadge = f.agent === "gemini" ? chalk3.blue("[Gemini] ") : f.agent === "codex" ? chalk3.magenta("[Codex] ") : chalk3.cyan("[Claude] ");
11987
- const sessionSuffix = f.sessionId ? chalk3.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
12243
+ if (scan.dlpFindings.length > 0) {
12244
+ console.log(" " + chalk3.dim("\u2500".repeat(70)));
11988
12245
  console.log(
11989
- ` ${ts}${proj}${agentBadge}` + chalk3.yellow(f.toolName) + chalk3.dim(` \xD7${f.count} `) + chalk3.gray(f.commandPreview) + sessionSuffix
12246
+ " " + chalk3.red.bold("\u{1F511} Credential Leaks") + chalk3.dim(" \xB7 ") + chalk3.red(
12247
+ `${num(scan.dlpFindings.length)} secret${scan.dlpFindings.length !== 1 ? "s" : ""} found in plain text`
12248
+ )
11990
12249
  );
12250
+ const sortedDlp = sortDlpFindingsByPriority(scan.dlpFindings);
12251
+ const recurringPatterns = buildRecurringPatternSet(scan.dlpFindings);
12252
+ const shownDlp = drillDown ? sortedDlp : sortedDlp.slice(0, topN);
12253
+ for (const f of shownDlp) {
12254
+ const stale = isStaleFinding(f.timestamp);
12255
+ const ts = f.timestamp ? chalk3.dim(fmtTs(f.timestamp) + " ") : "";
12256
+ const proj = chalk3.dim(f.project.slice(0, 22).padEnd(22) + " ");
12257
+ const agentBadge = f.agent === "gemini" ? chalk3.blue("[Gemini] ") : f.agent === "codex" ? chalk3.magenta("[Codex] ") : f.agent === "shell" ? chalk3.yellow("[Shell] ") : chalk3.cyan("[Claude] ");
12258
+ const sessionSuffix = f.sessionId ? chalk3.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
12259
+ const recurringBadge = recurringPatterns.has(f.patternName) ? chalk3.red.bold(" \u26A0\uFE0F recurring ") : "";
12260
+ const patternDisplay = stale ? chalk3.dim(f.patternName) : chalk3.yellow(f.patternName);
12261
+ const sampleDisplay = stale ? chalk3.dim(f.redactedSample) : chalk3.gray(f.redactedSample);
12262
+ const entryBadge = chalk3.dim(` [${entryPathLabel(f.toolName)}]`);
12263
+ const leadIcon = stale ? chalk3.dim("\u{1F6A8}") : "\u{1F6A8}";
12264
+ console.log(
12265
+ ` ${leadIcon} ${ts}${proj}${agentBadge}` + patternDisplay + recurringBadge + chalk3.dim(" ") + sampleDisplay + entryBadge + sessionSuffix
12266
+ );
12267
+ }
12268
+ if (!drillDown && scan.dlpFindings.length > topN) {
12269
+ console.log(
12270
+ chalk3.dim(
12271
+ ` \u2026 and ${scan.dlpFindings.length - topN} more (--drill-down for full list)`
12272
+ )
12273
+ );
12274
+ }
12275
+ console.log("");
11991
12276
  }
11992
- if (!drillDown && scan.loopFindings.length > topN) {
12277
+ const blockedRuleSections = summary.sections.map((s) => ({ ...s, rules: s.rules.filter((r) => r.verdict === "block") })).filter((s) => s.rules.length > 0);
12278
+ if (blockedRuleSections.length > 0) {
12279
+ console.log(" " + chalk3.dim("\u2500".repeat(70)));
11993
12280
  console.log(
11994
- chalk3.dim(
11995
- ` \u2026 and ${scan.loopFindings.length - topN} more (--drill-down for full list)`
12281
+ " " + chalk3.red.bold("\u{1F6D1} Blocked") + chalk3.dim(" \xB7 ") + chalk3.red(
12282
+ `${blockedCount} operation${blockedCount !== 1 ? "s" : ""} node9 would have stopped`
11996
12283
  )
11997
12284
  );
11998
- }
11999
- const stuckTools = computeStuckTools(scan.loopFindings);
12000
- if (stuckTools.length > 0) {
12285
+ for (const section of blockedRuleSections) {
12286
+ for (const rule of section.rules) {
12287
+ printRuleGroup(rule, topN, drillDown, previewWidth);
12288
+ }
12289
+ }
12001
12290
  console.log("");
12002
- console.log(" " + chalk3.dim("Most stuck tools:"));
12003
- for (const t of stuckTools) {
12291
+ }
12292
+ if (scan.loopFindings.length > 0) {
12293
+ console.log(" " + chalk3.dim("\u2500".repeat(70)));
12294
+ const loopCostLabel = summary.loopWastedUSD > 0 ? chalk3.dim(" \xB7 ") + chalk3.yellow("~" + fmtCost(summary.loopWastedUSD) + " wasted") : "";
12295
+ console.log(
12296
+ " " + chalk3.yellow.bold("\u{1F501} Agent Loops") + chalk3.dim(" \xB7 ") + chalk3.yellow(
12297
+ `${num(scan.loopFindings.length)} repeated pattern${scan.loopFindings.length !== 1 ? "s" : ""} found`
12298
+ ) + loopCostLabel
12299
+ );
12300
+ const shownLoops = drillDown ? scan.loopFindings : scan.loopFindings.slice(0, topN);
12301
+ for (const f of shownLoops) {
12302
+ const ts = f.timestamp ? chalk3.dim(fmtTs(f.timestamp) + " ") : "";
12303
+ const proj = chalk3.dim(f.project.slice(0, 22).padEnd(22) + " ");
12304
+ const agentBadge = f.agent === "gemini" ? chalk3.blue("[Gemini] ") : f.agent === "codex" ? chalk3.magenta("[Codex] ") : chalk3.cyan("[Claude] ");
12305
+ const sessionSuffix = f.sessionId ? chalk3.dim(` \u2192 ${f.sessionId.slice(0, 8)}`) : "";
12306
+ console.log(
12307
+ ` ${ts}${proj}${agentBadge}` + chalk3.yellow(f.toolName) + chalk3.dim(` \xD7${f.count} `) + chalk3.gray(f.commandPreview) + sessionSuffix
12308
+ );
12309
+ }
12310
+ if (!drillDown && scan.loopFindings.length > topN) {
12004
12311
  console.log(
12005
- chalk3.dim(" ") + chalk3.yellow(t.toolName.padEnd(8)) + chalk3.dim(" ") + chalk3.dim(`\xD7${t.waste} repeats`.padEnd(14)) + chalk3.dim(` (${t.pct}%)`)
12312
+ chalk3.dim(
12313
+ ` \u2026 and ${scan.loopFindings.length - topN} more (--drill-down for full list)`
12314
+ )
12006
12315
  );
12007
12316
  }
12317
+ const stuckTools = computeStuckTools(scan.loopFindings);
12318
+ if (stuckTools.length > 0) {
12319
+ console.log("");
12320
+ console.log(" " + chalk3.dim("Most stuck tools:"));
12321
+ for (const t of stuckTools) {
12322
+ console.log(
12323
+ chalk3.dim(" ") + chalk3.yellow(t.toolName.padEnd(8)) + chalk3.dim(" ") + chalk3.dim(`\xD7${t.waste} repeats`.padEnd(14)) + chalk3.dim(` (${t.pct}%)`)
12324
+ );
12325
+ }
12326
+ }
12327
+ console.log("");
12328
+ }
12329
+ for (const section of summary.sections) {
12330
+ const reviewRules = section.rules.filter((r) => r.verdict !== "block");
12331
+ if (reviewRules.length === 0) continue;
12332
+ const enableHint = section.shieldKey ? chalk3.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
12333
+ console.log(" " + chalk3.dim("\u2500".repeat(70)));
12334
+ console.log(
12335
+ " " + chalk3.bold(section.label) + (section.subtitle ? chalk3.dim(` \xB7 ${section.subtitle}`) : "") + " " + chalk3.yellow(`${section.reviewCount} review`) + enableHint
12336
+ );
12337
+ for (const rule of reviewRules) {
12338
+ printRuleGroup(rule, topN, drillDown, previewWidth);
12339
+ }
12340
+ console.log("");
12341
+ }
12342
+ const activeShieldIds = new Set(
12343
+ summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
12344
+ );
12345
+ const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
12346
+ if (emptyShields.length > 0) {
12347
+ console.log(" " + chalk3.dim("\u2500".repeat(70)));
12348
+ console.log(
12349
+ " " + chalk3.bold("\u{1F6E1} Inactive Shields") + chalk3.dim(" \xB7 enable for more coverage")
12350
+ );
12351
+ console.log(" " + chalk3.dim(emptyShields.join(" \xB7 ")));
12352
+ console.log(" " + chalk3.dim("\u2192 node9 shield enable <name> to activate"));
12353
+ console.log("");
12008
12354
  }
12009
- console.log("");
12010
12355
  }
12011
- for (const section of summary.sections) {
12012
- const reviewRules = section.rules.filter((r) => r.verdict !== "block");
12013
- if (reviewRules.length === 0) continue;
12014
- const enableHint = section.shieldKey ? chalk3.dim(` \u2192 node9 shield enable ${section.shieldKey}`) : "";
12356
+ if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
12015
12357
  console.log(" " + chalk3.dim("\u2500".repeat(70)));
12016
12358
  console.log(
12017
- " " + chalk3.bold(section.label) + (section.subtitle ? chalk3.dim(` \xB7 ${section.subtitle}`) : "") + " " + chalk3.yellow(`${section.reviewCount} review`) + enableHint
12359
+ " " + chalk3.bold("\u{1F52D} Blast Radius") + chalk3.dim(
12360
+ ` \xB7 ${blastExposures} exposure${blastExposures !== 1 ? "s" : ""} an AI agent can reach right now`
12361
+ )
12018
12362
  );
12019
- for (const rule of reviewRules) {
12020
- printRuleGroup(rule, topN, drillDown, previewWidth);
12363
+ console.log("");
12364
+ if (blast.reachable.length > 0) {
12365
+ for (const p of blast.reachable) {
12366
+ console.log(
12367
+ " " + chalk3.red("\u2717 ") + chalk3.yellow(p.label.padEnd(38)) + chalk3.dim(p.description)
12368
+ );
12369
+ }
12370
+ }
12371
+ if (blast.envFindings.length > 0) {
12372
+ for (const f of blast.envFindings) {
12373
+ console.log(
12374
+ " " + chalk3.red("\u2717 ") + chalk3.yellow(f.key.padEnd(38)) + chalk3.dim(f.patternName + " in environment")
12375
+ );
12376
+ }
12021
12377
  }
12022
12378
  console.log("");
12023
- }
12024
- const activeShieldIds = new Set(
12025
- summary.sections.filter((s) => s.sourceType === "shield" && s.shieldKey).map((s) => s.shieldKey)
12026
- );
12027
- const emptyShields = Object.keys(SHIELDS).filter((n) => !activeShieldIds.has(n)).sort();
12028
- if (emptyShields.length > 0) {
12029
- console.log(" " + chalk3.dim("\u2500".repeat(70)));
12030
12379
  console.log(
12031
- " " + chalk3.bold("\u{1F6E1} Inactive Shields") + chalk3.dim(" \xB7 enable for more coverage")
12380
+ chalk3.dim(
12381
+ " \u2192 Run `node9 shield enable project-jail` to block agent access to these files."
12382
+ )
12032
12383
  );
12033
- console.log(" " + chalk3.dim(emptyShields.join(" \xB7 ")));
12034
- console.log(" " + chalk3.dim("\u2192 node9 shield enable <name> to activate"));
12035
12384
  console.log("");
12036
12385
  }
12037
- }
12038
- const blast = runBlast();
12039
- if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
12040
- console.log(" " + chalk3.dim("\u2500".repeat(70)));
12041
- console.log(
12042
- " " + chalk3.bold("\u{1F52D} Blast Radius") + chalk3.dim(" \xB7 what an AI agent can reach right now")
12043
- );
12044
- console.log("");
12045
- if (blast.reachable.length > 0) {
12046
- for (const p of blast.reachable) {
12386
+ if (isWired) {
12387
+ console.log(chalk3.green(" \u2705 node9 is active \u2014 your future sessions are protected."));
12388
+ console.log(
12389
+ chalk3.dim(" Run ") + chalk3.cyan("node9 report") + chalk3.dim(" to see live protection stats.")
12390
+ );
12391
+ if (drillDown) {
12047
12392
  console.log(
12048
- " " + chalk3.red("\u2717 ") + chalk3.yellow(p.label.padEnd(38)) + chalk3.dim(p.description)
12393
+ chalk3.dim(" Run ") + chalk3.cyan("node9 sessions --detail <session-id>") + chalk3.dim(" to see the full conversation for any session above.")
12049
12394
  );
12050
- }
12051
- }
12052
- if (blast.envFindings.length > 0) {
12053
- for (const f of blast.envFindings) {
12395
+ } else {
12054
12396
  console.log(
12055
- " " + chalk3.red("\u2717 ") + chalk3.yellow(f.key.padEnd(38)) + chalk3.dim(f.patternName + " in environment")
12397
+ chalk3.dim(" Run ") + chalk3.cyan("node9 scan --drill-down") + chalk3.dim(" to see full commands and session IDs.")
12056
12398
  );
12057
12399
  }
12058
- }
12059
- console.log("");
12060
- console.log(
12061
- " Security Score: " + scoreLabel(blast.score) + chalk3.dim(
12062
- ` (${blast.reachable.length + blast.envFindings.length} exposure${blast.reachable.length + blast.envFindings.length !== 1 ? "s" : ""})`
12063
- )
12064
- );
12065
- console.log(
12066
- chalk3.dim(
12067
- "\n Run `node9 shield enable project-jail` to block agent access to these files."
12068
- )
12069
- );
12070
- console.log("");
12071
- }
12072
- if (isWired) {
12073
- console.log(chalk3.green(" \u2705 node9 is active \u2014 your future sessions are protected."));
12074
- console.log(
12075
- chalk3.dim(" Run ") + chalk3.cyan("node9 report") + chalk3.dim(" to see live protection stats.")
12076
- );
12077
- if (drillDown) {
12078
- console.log(
12079
- chalk3.dim(" Run ") + chalk3.cyan("node9 sessions --detail <session-id>") + chalk3.dim(" to see the full conversation for any session above.")
12080
- );
12081
12400
  } else {
12401
+ const riskySummary = totalFindings + scan.dlpFindings.length;
12402
+ if (riskySummary > 0) {
12403
+ console.log(
12404
+ chalk3.yellow.bold(
12405
+ ` \u26A1 ${riskySummary} operation${riskySummary !== 1 ? "s" : ""} ran unprotected.`
12406
+ ) + chalk3.dim(" node9 would have caught them.")
12407
+ );
12408
+ console.log("");
12409
+ }
12410
+ console.log(chalk3.bold(" Enable real-time protection:"));
12411
+ console.log("");
12082
12412
  console.log(
12083
- chalk3.dim(" Run ") + chalk3.cyan("node9 scan --drill-down") + chalk3.dim(" to see full commands and session IDs.")
12413
+ " " + chalk3.cyan("npm install -g @node9/proxy") + chalk3.dim(" && ") + chalk3.cyan("node9 init --recommended")
12084
12414
  );
12085
- }
12086
- } else {
12087
- const riskySummary = totalFindings + scan.dlpFindings.length;
12088
- if (riskySummary > 0) {
12415
+ console.log("");
12089
12416
  console.log(
12090
- chalk3.yellow.bold(
12091
- ` \u26A1 ${riskySummary} operation${riskySummary !== 1 ? "s" : ""} ran unprotected.`
12092
- ) + chalk3.dim(" node9 would have caught them.")
12417
+ chalk3.dim(
12418
+ " Hooks into Claude Code automatically. Every tool call checked before it runs."
12419
+ )
12093
12420
  );
12421
+ console.log(" " + chalk3.dim("\u2192 ") + chalk3.underline("https://node9.ai"));
12094
12422
  }
12095
12423
  console.log("");
12096
- console.log(chalk3.bold(" Protect your next session in 30 seconds:"));
12097
- console.log("");
12098
- console.log(" " + chalk3.cyan("npm install -g @node9/proxy"));
12099
- console.log(" " + chalk3.cyan("node9 init"));
12100
- console.log("");
12101
- console.log(chalk3.dim(" node9 hooks into Claude Code automatically."));
12102
- console.log(
12103
- chalk3.dim(" Every tool call is checked before it runs \u2014 no proxy, no latency.")
12104
- );
12105
- console.log("");
12106
- console.log(" " + chalk3.dim("\u2192 ") + chalk3.underline("https://node9.ai"));
12107
- }
12108
- console.log("");
12109
- if (!isTestingMode()) {
12110
- if (isWired) {
12111
- const url = `http://${DAEMON_HOST}:${DAEMON_PORT}/?openscan=1`;
12112
- if (isDaemonRunning()) {
12113
- const internalToken = getInternalToken();
12114
- if (internalToken) {
12115
- try {
12116
- const pushSummary = buildScanSummary([
12117
- { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
12118
- { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
12119
- { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
12120
- ]);
12121
- await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
12122
- method: "POST",
12123
- headers: {
12124
- "Content-Type": "application/json",
12125
- "x-node9-internal": internalToken
12126
- },
12127
- body: JSON.stringify({ status: "complete", summary: pushSummary }),
12128
- signal: AbortSignal.timeout(3e3)
12129
- });
12130
- openBrowserLocal();
12131
- } catch {
12424
+ if (!isTestingMode()) {
12425
+ if (isWired) {
12426
+ const url = `http://${DAEMON_HOST}:${DAEMON_PORT}/?openscan=1`;
12427
+ if (isDaemonRunning()) {
12428
+ const internalToken = getInternalToken();
12429
+ if (internalToken) {
12430
+ try {
12431
+ const pushSummary = buildScanSummary([
12432
+ { id: "claude", label: "Claude", icon: "\u{1F916}", scan: claudeScan },
12433
+ { id: "gemini", label: "Gemini", icon: "\u264A", scan: geminiScan },
12434
+ { id: "codex", label: "Codex", icon: "\u{1F52E}", scan: codexScan }
12435
+ ]);
12436
+ await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/scan/push`, {
12437
+ method: "POST",
12438
+ headers: {
12439
+ "Content-Type": "application/json",
12440
+ "x-node9-internal": internalToken
12441
+ },
12442
+ body: JSON.stringify({ status: "complete", summary: pushSummary }),
12443
+ signal: AbortSignal.timeout(3e3)
12444
+ });
12445
+ } catch {
12446
+ }
12132
12447
  }
12133
12448
  }
12449
+ if (isDaemonRunning()) {
12450
+ console.log(" " + chalk3.cyan("\u{1F310} View in browser:") + " " + chalk3.underline(url));
12451
+ } else {
12452
+ console.log(
12453
+ " " + chalk3.dim("\u{1F4CA} To view in browser, start the daemon: ") + chalk3.cyan("node9 daemon --background")
12454
+ );
12455
+ }
12456
+ console.log("");
12134
12457
  }
12135
- if (isDaemonRunning()) {
12136
- console.log(" " + chalk3.cyan("\u{1F310} View in browser:") + " " + chalk3.underline(url));
12137
- } else {
12138
- console.log(
12139
- " " + chalk3.dim("\u{1F4CA} To view in browser, start the daemon: ") + chalk3.cyan("node9 daemon --background")
12140
- );
12141
- }
12142
- console.log("");
12143
12458
  }
12144
12459
  }
12145
- });
12460
+ );
12146
12461
  }
12147
12462
  var 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;
12148
12463
  var init_scan = __esm({
@@ -12509,7 +12824,6 @@ import net2 from "net";
12509
12824
  import fs17 from "fs";
12510
12825
  import path19 from "path";
12511
12826
  import os14 from "os";
12512
- import { spawn as spawn3 } from "child_process";
12513
12827
  import { randomUUID as randomUUID3 } from "crypto";
12514
12828
  function loadInsightCounts() {
12515
12829
  try {
@@ -12680,14 +12994,6 @@ function readBody(req) {
12680
12994
  req.on("end", () => resolve(body));
12681
12995
  });
12682
12996
  }
12683
- function openBrowser(url) {
12684
- if (process.env.NODE9_TESTING === "1") return;
12685
- try {
12686
- const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
12687
- spawn3(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
12688
- } catch {
12689
- }
12690
- }
12691
12997
  function estimateToolCost(tool, args) {
12692
12998
  const a = args ?? {};
12693
12999
  const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
@@ -12800,6 +13106,18 @@ function startActivitySocket() {
12800
13106
  });
12801
13107
  return;
12802
13108
  }
13109
+ if (data.status === "execution-completed") {
13110
+ broadcast("execution-result", {
13111
+ id: data.id,
13112
+ ts: data.ts,
13113
+ tool: data.tool,
13114
+ agent: data.agent,
13115
+ mcpServer: data.mcpServer,
13116
+ durationMs: data.durationMs,
13117
+ isError: data.isError
13118
+ });
13119
+ return;
13120
+ }
12803
13121
  if (data.status === "pending") {
12804
13122
  broadcast("activity", {
12805
13123
  id: data.id,
@@ -12807,7 +13125,8 @@ function startActivitySocket() {
12807
13125
  tool: data.tool,
12808
13126
  args: redactArgs(data.args),
12809
13127
  status: "pending",
12810
- agent: data.agent
13128
+ agent: data.agent,
13129
+ mcpServer: data.mcpServer
12811
13130
  });
12812
13131
  } else {
12813
13132
  if (data.status === "allow") {
@@ -12834,7 +13153,9 @@ function startActivitySocket() {
12834
13153
  id: data.id,
12835
13154
  status: data.status,
12836
13155
  label: data.label,
12837
- costEstimate
13156
+ costEstimate,
13157
+ agent: data.agent,
13158
+ mcpServer: data.mcpServer
12838
13159
  });
12839
13160
  }
12840
13161
  } catch {
@@ -13516,7 +13837,6 @@ function startDaemon() {
13516
13837
  const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
13517
13838
  const watchMode = process.env.NODE9_WATCH_MODE === "1";
13518
13839
  let idleTimer;
13519
- let browserOpened = false;
13520
13840
  function resetIdleTimer() {
13521
13841
  if (watchMode) return;
13522
13842
  if (idleTimer) clearTimeout(idleTimer);
@@ -13625,12 +13945,6 @@ data: ${JSON.stringify(item.data)}
13625
13945
  }
13626
13946
  });
13627
13947
  }
13628
- if (req.method === "POST" && pathname === "/browser-opened") {
13629
- if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
13630
- browserOpened = true;
13631
- res.writeHead(200).end();
13632
- return;
13633
- }
13634
13948
  if (req.method === "POST" && pathname === "/check") {
13635
13949
  try {
13636
13950
  resetIdleTimer();
@@ -13691,7 +14005,9 @@ data: ${JSON.stringify(item.data)}
13691
14005
  ts: entry.timestamp,
13692
14006
  tool: toolName,
13693
14007
  args: redactArgs(args),
13694
- status: "pending"
14008
+ status: "pending",
14009
+ agent: entry.agent,
14010
+ mcpServer: entry.mcpServer
13695
14011
  });
13696
14012
  }
13697
14013
  const projectCwd = typeof cwd === "string" && path25.isAbsolute(cwd) ? cwd : void 0;
@@ -13714,11 +14030,6 @@ data: ${JSON.stringify(item.data)}
13714
14030
  // Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
13715
14031
  allowCount: (insightCounts.get(toolName) ?? 0) + 1
13716
14032
  });
13717
- const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
13718
- if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
13719
- browserOpened = true;
13720
- openBrowser(`http://127.0.0.1:${DAEMON_PORT}/`);
13721
- }
13722
14033
  }
13723
14034
  res.writeHead(200, { "Content-Type": "application/json" });
13724
14035
  res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
@@ -13740,7 +14051,9 @@ data: ${JSON.stringify(item.data)}
13740
14051
  broadcast("activity-result", {
13741
14052
  id,
13742
14053
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
13743
- label: result.blockedByLabel
14054
+ label: result.blockedByLabel,
14055
+ agent: e.agent,
14056
+ mcpServer: e.mcpServer
13744
14057
  });
13745
14058
  clearTimeout(e.timer);
13746
14059
  const decision = result.approved ? "allow" : "deny";
@@ -14912,7 +15225,7 @@ import fs41 from "fs";
14912
15225
  import os37 from "os";
14913
15226
  import path43 from "path";
14914
15227
  import readline5 from "readline";
14915
- import { spawn as spawn10, execSync as execSync3 } from "child_process";
15228
+ import { spawn as spawn9 } from "child_process";
14916
15229
  function getIcon(tool) {
14917
15230
  const t = tool.toLowerCase();
14918
15231
  for (const [k, v] of Object.entries(ICONS)) {
@@ -14997,10 +15310,13 @@ function wrappedLineCount(text) {
14997
15310
  const len = visibleLength(text);
14998
15311
  return Math.max(1, Math.ceil(len / cols));
14999
15312
  }
15000
- function agentLabel(agent) {
15001
- if (!agent || agent === "Terminal") return "";
15313
+ function agentLabel(agent, mcpServer) {
15314
+ if (!agent || agent === "Terminal") {
15315
+ return mcpServer ? chalk27.dim(`[\u2192 ${mcpServer}] `) : "";
15316
+ }
15002
15317
  const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
15003
- return short ? chalk27.dim(`[${short}] `) : "";
15318
+ if (!short) return mcpServer ? chalk27.dim(`[\u2192 ${mcpServer}] `) : "";
15319
+ return mcpServer ? chalk27.dim(`[${short} \u2192 ${mcpServer}] `) : chalk27.dim(`[${short}] `);
15004
15320
  }
15005
15321
  function formatBase(activity) {
15006
15322
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
@@ -15008,7 +15324,7 @@ function formatBase(activity) {
15008
15324
  const toolName = activity.tool.slice(0, 16).padEnd(16);
15009
15325
  const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os37.homedir(), "~");
15010
15326
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
15011
- return `${chalk27.gray(time)} ${icon} ${agentLabel(activity.agent)}${chalk27.white.bold(toolName)} ${chalk27.dim(argsPreview)}`;
15327
+ return `${chalk27.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer)}${chalk27.white.bold(toolName)} ${chalk27.dim(argsPreview)}`;
15012
15328
  }
15013
15329
  function renderResult(activity, result) {
15014
15330
  const base = formatBase(activity);
@@ -15062,7 +15378,7 @@ async function ensureDaemon() {
15062
15378
  } catch {
15063
15379
  }
15064
15380
  console.log(chalk27.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
15065
- const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
15381
+ const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
15066
15382
  detached: true,
15067
15383
  stdio: "ignore",
15068
15384
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -15462,22 +15778,6 @@ async function startTail(options = {}) {
15462
15778
  process.stdin.on("keypress", onKeypress);
15463
15779
  }
15464
15780
  const dashboardUrl = `http://127.0.0.1:${port}/`;
15465
- try {
15466
- const browserEnabled = getConfig().settings.approvers?.browser !== false;
15467
- if (browserEnabled) {
15468
- if (process.platform === "darwin") execSync3(`open "${dashboardUrl}"`, { stdio: "ignore" });
15469
- else if (process.platform === "win32")
15470
- execSync3(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
15471
- else execSync3(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
15472
- const intToken = getInternalToken();
15473
- fetch(`http://127.0.0.1:${port}/browser-opened`, {
15474
- method: "POST",
15475
- headers: intToken ? { "X-Node9-Internal": intToken } : {}
15476
- }).catch(() => {
15477
- });
15478
- }
15479
- } catch {
15480
- }
15481
15781
  const auditLog = path43.join(os37.homedir(), ".node9", "audit.log");
15482
15782
  try {
15483
15783
  const unackedDlp = fs41.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
@@ -15657,6 +15957,17 @@ async function startTail(options = {}) {
15657
15957
  orphanedResults.set(data.id, data);
15658
15958
  }
15659
15959
  }
15960
+ if (event === "execution-result") {
15961
+ const exec = data;
15962
+ const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
15963
+ const arrow = exec.isError ? chalk27.red(" \u21B3 \u2717") : chalk27.green(" \u21B3 \u2713");
15964
+ const label = agentLabel(exec.agent, exec.mcpServer);
15965
+ const tool = (exec.tool ?? "").slice(0, 16);
15966
+ const duration = typeof exec.durationMs === "number" ? chalk27.dim(` (${exec.durationMs}ms)`) : "";
15967
+ console.log(
15968
+ `${chalk27.gray(time)} ${arrow} ${label}${chalk27.dim(tool)}${chalk27.dim(" completed")}${duration}`
15969
+ );
15970
+ }
15660
15971
  }
15661
15972
  req.on("error", (err2) => {
15662
15973
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
@@ -15670,8 +15981,6 @@ var init_tail = __esm({
15670
15981
  "src/tui/tail.ts"() {
15671
15982
  "use strict";
15672
15983
  init_daemon2();
15673
- init_daemon();
15674
- init_core();
15675
15984
  PID_FILE = path43.join(os37.homedir(), ".node9", "daemon.pid");
15676
15985
  ICONS = {
15677
15986
  bash: "\u{1F4BB}",
@@ -16126,7 +16435,7 @@ function parseDuration(str) {
16126
16435
  init_orchestrator();
16127
16436
  import readline from "readline";
16128
16437
  import chalk6 from "chalk";
16129
- import { spawn as spawn4 } from "child_process";
16438
+ import { spawn as spawn3 } from "child_process";
16130
16439
  import { execa } from "execa";
16131
16440
  import { parseCommandString } from "execa";
16132
16441
 
@@ -16213,11 +16522,11 @@ async function runProxy(targetCommand) {
16213
16522
  }
16214
16523
  console.error(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
16215
16524
  const spawnEnv = { ...process.env, FORCE_COLOR: "1" };
16216
- const child = useShell ? spawn4("/bin/bash", ["-c", targetCommand], {
16525
+ const child = useShell ? spawn3("/bin/bash", ["-c", targetCommand], {
16217
16526
  stdio: ["pipe", "pipe", "inherit"],
16218
16527
  shell: false,
16219
16528
  env: spawnEnv
16220
- }) : spawn4(executable, args, { stdio: ["pipe", "pipe", "inherit"], shell: false, env: spawnEnv });
16529
+ }) : spawn3(executable, args, { stdio: ["pipe", "pipe", "inherit"], shell: false, env: spawnEnv });
16221
16530
  const agentIn = readline.createInterface({ input: process.stdin, terminal: false });
16222
16531
  agentIn.on("line", async (line) => {
16223
16532
  let message;
@@ -16290,12 +16599,12 @@ init_config();
16290
16599
  init_policy();
16291
16600
  import chalk7 from "chalk";
16292
16601
  import fs28 from "fs";
16293
- import { spawn as spawn6 } from "child_process";
16602
+ import { spawn as spawn5 } from "child_process";
16294
16603
  import path29 from "path";
16295
16604
  import os24 from "os";
16296
16605
 
16297
16606
  // src/undo.ts
16298
- import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
16607
+ import { spawnSync as spawnSync5, spawn as spawn4 } from "child_process";
16299
16608
  import crypto3 from "crypto";
16300
16609
  import fs26 from "fs";
16301
16610
  import net3 from "net";
@@ -16547,7 +16856,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
16547
16856
  notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
16548
16857
  fs26.writeFileSync(UNDO_LATEST_PATH, commitHash);
16549
16858
  if (shouldGc) {
16550
- spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
16859
+ spawn4("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
16551
16860
  }
16552
16861
  return commitHash;
16553
16862
  } catch (err2) {
@@ -16876,7 +17185,7 @@ RAW: ${raw}
16876
17185
  ]) {
16877
17186
  delete safeEnv[key];
16878
17187
  }
16879
- const d = spawn6(process.execPath, [scriptPath, "daemon"], {
17188
+ const d = spawn5(process.execPath, [scriptPath, "daemon"], {
16880
17189
  detached: true,
16881
17190
  stdio: "ignore",
16882
17191
  env: { ...safeEnv, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
@@ -18638,7 +18947,7 @@ init_daemon2();
18638
18947
  init_daemon();
18639
18948
  init_daemon_starter();
18640
18949
  import chalk12 from "chalk";
18641
- import { spawn as spawn7 } from "child_process";
18950
+ import { spawn as spawn6 } from "child_process";
18642
18951
  var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
18643
18952
  function registerDaemonCommand(program2) {
18644
18953
  program2.command("daemon").description("Manage the local approval daemon").argument("[action]", `${VALID_ACTIONS} (default: start)`).option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
@@ -18676,7 +18985,7 @@ function registerDaemonCommand(program2) {
18676
18985
  if (cmd === "restart") {
18677
18986
  stopDaemon();
18678
18987
  await new Promise((r) => setTimeout(r, 500));
18679
- const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
18988
+ const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
18680
18989
  detached: true,
18681
18990
  stdio: "ignore",
18682
18991
  env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
@@ -18710,7 +19019,7 @@ function registerDaemonCommand(program2) {
18710
19019
  console.log(chalk12.green(`\u{1F310} Opened browser: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
18711
19020
  process.exit(0);
18712
19021
  }
18713
- const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
19022
+ const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
18714
19023
  detached: true,
18715
19024
  stdio: "ignore"
18716
19025
  });
@@ -18725,7 +19034,7 @@ function registerDaemonCommand(program2) {
18725
19034
  process.exit(0);
18726
19035
  }
18727
19036
  if (options.background) {
18728
- const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
19037
+ const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
18729
19038
  detached: true,
18730
19039
  stdio: "ignore"
18731
19040
  });
@@ -18933,14 +19242,18 @@ function fireTelemetryPing(agents) {
18933
19242
  }
18934
19243
  }
18935
19244
  function registerInitCommand(program2) {
18936
- 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(
19245
+ program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option(
19246
+ "-m, --mode <mode>",
19247
+ "Initial security mode: standard | strict | audit | observe (logs would-block, never blocks)",
19248
+ "standard"
19249
+ ).option("--skip-setup", "Only create config \u2014 do not wire AI agents").option(
18937
19250
  "--recommended",
18938
19251
  "Non-interactive: enable bash-safe + filesystem + project-jail shields without prompting"
18939
19252
  ).action(
18940
19253
  async (options) => {
18941
19254
  console.log(chalk14.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
18942
19255
  let chosenMode = options.mode.toLowerCase();
18943
- if (!["standard", "strict", "audit"].includes(chosenMode)) {
19256
+ if (!["standard", "strict", "audit", "observe"].includes(chosenMode)) {
18944
19257
  chosenMode = DEFAULT_CONFIG.settings.mode;
18945
19258
  }
18946
19259
  {
@@ -19382,7 +19695,7 @@ function registerUndoCommand(program2) {
19382
19695
  // src/cli/commands/watch.ts
19383
19696
  init_daemon();
19384
19697
  import chalk17 from "chalk";
19385
- import { spawn as spawn8, spawnSync as spawnSync6 } from "child_process";
19698
+ import { spawn as spawn7, spawnSync as spawnSync6 } from "child_process";
19386
19699
  function registerWatchCommand(program2) {
19387
19700
  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) => {
19388
19701
  let port = DAEMON_PORT;
@@ -19398,7 +19711,7 @@ function registerWatchCommand(program2) {
19398
19711
  }
19399
19712
  } catch {
19400
19713
  console.error(chalk17.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
19401
- const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
19714
+ const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
19402
19715
  detached: true,
19403
19716
  stdio: "ignore",
19404
19717
  env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_WATCH_MODE: "1" }
@@ -19444,7 +19757,7 @@ function registerWatchCommand(program2) {
19444
19757
  init_orchestrator();
19445
19758
  import readline3 from "readline";
19446
19759
  import chalk18 from "chalk";
19447
- import { spawn as spawn9 } from "child_process";
19760
+ import { spawn as spawn8 } from "child_process";
19448
19761
  import { execa as execa2 } from "execa";
19449
19762
  init_provenance();
19450
19763
 
@@ -19565,6 +19878,18 @@ function extractMcpServer(toolName) {
19565
19878
  const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
19566
19879
  return match?.[1];
19567
19880
  }
19881
+ function normalizeClientName(name) {
19882
+ if (typeof name !== "string" || name.length === 0) return void 0;
19883
+ const lower = name.toLowerCase();
19884
+ if (lower.includes("claude")) return "Claude";
19885
+ if (lower.includes("cursor")) return "Cursor";
19886
+ if (lower.includes("codex")) return "Codex";
19887
+ if (lower.includes("gemini")) return "Gemini";
19888
+ if (lower.includes("cline")) return "Cline";
19889
+ if (lower.includes("continue")) return "Continue";
19890
+ const sanitized = sanitize4(name).slice(0, 40);
19891
+ return sanitized.length > 0 ? sanitized : void 0;
19892
+ }
19568
19893
  function tokenize4(cmd) {
19569
19894
  const tokens = [];
19570
19895
  let current = "";
@@ -19637,7 +19962,7 @@ async function runMcpGateway(upstreamCommand) {
19637
19962
  const safeEnv = Object.fromEntries(
19638
19963
  Object.entries(process.env).filter(([k]) => !UPSTREAM_INJECTOR_VARS.has(k))
19639
19964
  );
19640
- const child = spawn9(executable, cmdArgs, {
19965
+ const child = spawn8(executable, cmdArgs, {
19641
19966
  stdio: ["pipe", "pipe", "inherit"],
19642
19967
  // control stdin/stdout; inherit stderr
19643
19968
  shell: false,
@@ -19651,6 +19976,8 @@ async function runMcpGateway(upstreamCommand) {
19651
19976
  let pinState = "pending";
19652
19977
  const pendingToolCalls = [];
19653
19978
  const pendingCallNames = /* @__PURE__ */ new Map();
19979
+ const pendingExecutions = /* @__PURE__ */ new Map();
19980
+ let clientName;
19654
19981
  const agentIn = readline3.createInterface({ input: process.stdin, terminal: false });
19655
19982
  agentIn.on("line", async (line) => {
19656
19983
  let message;
@@ -19673,6 +20000,13 @@ async function runMcpGateway(upstreamCommand) {
19673
20000
  child.stdin.write(line + "\n");
19674
20001
  return;
19675
20002
  }
20003
+ if (message.method === "initialize") {
20004
+ clientName = normalizeClientName(
20005
+ message.params?.clientInfo?.name
20006
+ );
20007
+ child.stdin.write(line + "\n");
20008
+ return;
20009
+ }
19676
20010
  if (message.method === "tools/list" && message.id !== void 0 && message.id !== null) {
19677
20011
  pendingToolsListIds.add(message.id);
19678
20012
  }
@@ -19718,7 +20052,7 @@ async function runMcpGateway(upstreamCommand) {
19718
20052
  const toolArgs = message.params?.arguments ?? message.params?.tool_input ?? {};
19719
20053
  const mcpServer = extractMcpServer(toolName);
19720
20054
  const result = await authorizeHeadless(toolName, toolArgs, {
19721
- agent: "MCP-Gateway",
20055
+ agent: clientName ?? "MCP-Gateway",
19722
20056
  mcpServer
19723
20057
  });
19724
20058
  if (!result.approved) {
@@ -19748,6 +20082,12 @@ async function runMcpGateway(upstreamCommand) {
19748
20082
  }
19749
20083
  if (message.id !== void 0 && message.id !== null) {
19750
20084
  pendingCallNames.set(message.id, toolName);
20085
+ pendingExecutions.set(message.id, {
20086
+ ts: Date.now(),
20087
+ toolName,
20088
+ agent: clientName,
20089
+ mcpServer
20090
+ });
19751
20091
  }
19752
20092
  child.stdin.write(line + "\n");
19753
20093
  } catch {
@@ -19889,6 +20229,26 @@ async function runMcpGateway(upstreamCommand) {
19889
20229
  return;
19890
20230
  }
19891
20231
  }
20232
+ const respId = parsed?.id;
20233
+ if (respId !== void 0 && respId !== null) {
20234
+ const exec = pendingExecutions.get(respId);
20235
+ if (exec) {
20236
+ pendingExecutions.delete(respId);
20237
+ const durationMs = Date.now() - exec.ts;
20238
+ const isError = parsed?.error !== void 0;
20239
+ notifyActivitySocket({
20240
+ id: String(respId),
20241
+ ts: Date.now(),
20242
+ tool: exec.toolName,
20243
+ status: "execution-completed",
20244
+ durationMs,
20245
+ agent: exec.agent,
20246
+ mcpServer: exec.mcpServer,
20247
+ isError
20248
+ }).catch(() => {
20249
+ });
20250
+ }
20251
+ }
19892
20252
  const LARGE_RESPONSE_THRESHOLD = 5e5;
19893
20253
  if (parsed?.result && line.length > LARGE_RESPONSE_THRESHOLD) {
19894
20254
  const callId = parsed.id;