@node9/proxy 1.11.1 → 1.11.3

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
@@ -116,7 +116,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
116
116
  }
117
117
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
118
118
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
119
- const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
119
+ const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
120
120
  appendToLog(LOCAL_AUDIT_LOG, {
121
121
  ts: (/* @__PURE__ */ new Date()).toISOString(),
122
122
  tool: toolName,
@@ -8647,10 +8647,10 @@ function bold(s) {
8647
8647
  function color(c, s) {
8648
8648
  return `${c}${s}${RESET3}`;
8649
8649
  }
8650
- function progressBar(pct2, warnAt = 70, critAt = 85) {
8651
- const filled = Math.round(Math.min(pct2, 100) / 100 * BAR_WIDTH);
8650
+ function progressBar(pct, warnAt = 70, critAt = 85) {
8651
+ const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
8652
8652
  const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
8653
- const c = pct2 >= critAt ? RED2 : pct2 >= warnAt ? YELLOW2 : GREEN2;
8653
+ const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
8654
8654
  return `${c}${bar}${RESET3}`;
8655
8655
  }
8656
8656
  function formatTimeLeft(resetsAt) {
@@ -8866,15 +8866,15 @@ function renderContextLine(stdin) {
8866
8866
  }
8867
8867
  const rl = stdin.rate_limits;
8868
8868
  if (rl?.five_hour?.used_percentage !== void 0) {
8869
- const pct2 = Math.round(rl.five_hour.used_percentage);
8870
- const bar = progressBar(pct2, 60, 80);
8869
+ const pct = Math.round(rl.five_hour.used_percentage);
8870
+ const bar = progressBar(pct, 60, 80);
8871
8871
  const left = formatTimeLeft(rl.five_hour.resets_at);
8872
- parts.push(`${dim("\u2502")} 5h ${bar} ${pct2}%${left}`);
8872
+ parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
8873
8873
  }
8874
8874
  if (rl?.seven_day?.used_percentage !== void 0) {
8875
- const pct2 = Math.round(rl.seven_day.used_percentage);
8876
- const bar = progressBar(pct2, 60, 80);
8877
- parts.push(`${dim("\u2502")} 7d ${bar} ${pct2}%`);
8875
+ const pct = Math.round(rl.seven_day.used_percentage);
8876
+ const bar = progressBar(pct, 60, 80);
8877
+ parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
8878
8878
  }
8879
8879
  if (parts.length === 0) return null;
8880
8880
  return parts.join(" ");
@@ -11747,8 +11747,8 @@ function buildTestTimestamps(allEntries) {
11747
11747
  return testTs;
11748
11748
  }
11749
11749
  function isTestEntry(entry, testTs) {
11750
- if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
11751
11750
  if (entry.testRun === true) return true;
11751
+ if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
11752
11752
  const cmd = entry.args?.command;
11753
11753
  if (typeof cmd === "string") return TEST_COMMAND_RE3.test(cmd);
11754
11754
  const t = new Date(entry.ts).getTime();
@@ -11806,10 +11806,6 @@ function colorBar(value, max, width) {
11806
11806
  const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
11807
11807
  return import_chalk9.default.cyan(s.slice(0, filled)) + import_chalk9.default.dim(s.slice(filled));
11808
11808
  }
11809
- function pct(num3, total) {
11810
- if (total === 0) return "\u2013";
11811
- return Math.round(num3 / total * 100) + "%";
11812
- }
11813
11809
  function fmtDate(d) {
11814
11810
  const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
11815
11811
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
@@ -11980,9 +11976,12 @@ function registerReportCommand(program2) {
11980
11976
  `));
11981
11977
  return;
11982
11978
  }
11983
- let allowed = 0;
11984
- let blocked = 0;
11985
- let dlpHits = 0;
11979
+ let userApproved = 0;
11980
+ let userDenied = 0;
11981
+ let timedOut = 0;
11982
+ let hardBlocked = 0;
11983
+ let dlpBlocked = 0;
11984
+ let observeDlp = 0;
11986
11985
  let loopHits = 0;
11987
11986
  let testPasses = 0;
11988
11987
  let testFails = 0;
@@ -11995,9 +11994,16 @@ function registerReportCommand(program2) {
11995
11994
  for (const e of entries) {
11996
11995
  const allow = isAllow(e.decision);
11997
11996
  const dateKey = e.ts.slice(0, 10);
11998
- if (allow) allowed++;
11999
- else blocked++;
12000
- if (isDlp(e.checkedBy)) dlpHits++;
11997
+ const userInteracted = e.source === "daemon";
11998
+ if (userInteracted) {
11999
+ if (allow) userApproved++;
12000
+ else userDenied++;
12001
+ } else if (!allow) {
12002
+ if (e.checkedBy === "timeout") timedOut++;
12003
+ else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
12004
+ else if (isDlp(e.checkedBy)) dlpBlocked++;
12005
+ else if (e.checkedBy !== "loop-detected") hardBlocked++;
12006
+ }
12001
12007
  if (e.checkedBy === "loop-detected") loopHits++;
12002
12008
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
12003
12009
  t.calls++;
@@ -12048,25 +12054,84 @@ function registerReportCommand(program2) {
12048
12054
  " " + import_chalk9.default.bold.cyan("\u{1F6E1} node9 Report") + import_chalk9.default.dim(" \xB7 ") + import_chalk9.default.white(periodLabel[period]) + import_chalk9.default.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + import_chalk9.default.dim(` ${num(total)} events`) + (excludeTests ? import_chalk9.default.dim(` \u2013tests (\u2013${filteredTestCount})`) : "")
12049
12055
  );
12050
12056
  console.log(" " + line);
12051
- console.log("");
12052
- const blockLabel = blocked > 0 ? import_chalk9.default.red(`\u{1F6D1} ${num(blocked)} blocked`) : import_chalk9.default.dim("\u{1F6D1} 0 blocked");
12053
- const dlpLabel = dlpHits > 0 ? import_chalk9.default.yellow(`\u{1F6A8} ${dlpHits} DLP hits`) : import_chalk9.default.dim("\u{1F6A8} 0 DLP hits");
12054
- const loopLabel = loopHits > 0 ? import_chalk9.default.yellow(`\u{1F504} ${loopHits} loops`) : import_chalk9.default.dim("\u{1F504} 0 loops");
12055
- const currentRate = total > 0 ? blocked / total : 0;
12057
+ const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
12058
+ const currentRate = total > 0 ? totalBlocked / total : 0;
12056
12059
  const trendLabel = (() => {
12057
- if (priorBlockRate === null) return import_chalk9.default.dim(`${pct(blocked, total)} block rate`);
12060
+ if (priorBlockRate === null) return "";
12058
12061
  const delta = Math.round((currentRate - priorBlockRate) * 100);
12059
- const arrow = delta > 0 ? import_chalk9.default.red(`\u25B2${delta}%`) : delta < 0 ? import_chalk9.default.green(`\u25BC${Math.abs(delta)}%`) : import_chalk9.default.dim("\u2013");
12060
- return import_chalk9.default.dim(`${pct(blocked, total)} block rate `) + arrow + import_chalk9.default.dim(" vs prior");
12062
+ if (delta === 0) return "";
12063
+ return " " + (delta > 0 ? import_chalk9.default.red(`\u25B2${delta}%`) : import_chalk9.default.green(`\u25BC${Math.abs(delta)}%`)) + import_chalk9.default.dim(" vs prior");
12061
12064
  })();
12062
12065
  const reads = toolMap.get("Read")?.calls ?? 0;
12063
12066
  const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
12064
12067
  const ratioLabel = reads > 0 ? import_chalk9.default.dim(`edit/read ${(edits / reads).toFixed(1)}`) : import_chalk9.default.dim("edit/read \u2013");
12065
12068
  const testLabel = testPasses + testFails > 0 ? import_chalk9.default.dim("tests ") + import_chalk9.default.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + import_chalk9.default.red(`${testFails}\u2717`) : "") : import_chalk9.default.dim("tests \u2013");
12069
+ console.log("");
12070
+ console.log(" " + import_chalk9.default.bold("Protection Summary"));
12071
+ console.log(" " + import_chalk9.default.dim("\u2500".repeat(Math.min(50, W - 4))));
12066
12072
  console.log(
12067
- " " + import_chalk9.default.green(`\u2705 ${num(allowed)} allowed`) + " " + blockLabel + " " + dlpLabel + " " + loopLabel + " " + trendLabel
12073
+ " " + import_chalk9.default.dim("Intercepted") + " " + import_chalk9.default.white(num(total)) + import_chalk9.default.dim(" tool calls")
12074
+ );
12075
+ console.log("");
12076
+ const COL1 = 18;
12077
+ const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
12078
+ const countStr = colorFn(num(count));
12079
+ const noteStr = note ? import_chalk9.default.dim(" " + note) : "";
12080
+ console.log(" " + icon + " " + import_chalk9.default.white(label.padEnd(COL1)) + countStr + noteStr);
12081
+ };
12082
+ summaryRow(
12083
+ userApproved > 0 ? import_chalk9.default.green("\u2705") : import_chalk9.default.dim("\u2705"),
12084
+ "User approved",
12085
+ userApproved,
12086
+ userApproved === 0 ? "no popups this period" : void 0,
12087
+ userApproved > 0 ? (s) => import_chalk9.default.green(s) : (s) => import_chalk9.default.dim(s)
12088
+ );
12089
+ summaryRow(
12090
+ userDenied > 0 ? import_chalk9.default.red("\u{1F6AB}") : import_chalk9.default.dim("\u{1F6AB}"),
12091
+ "User denied",
12092
+ userDenied,
12093
+ void 0,
12094
+ userDenied > 0 ? (s) => import_chalk9.default.red(s) : (s) => import_chalk9.default.dim(s)
12068
12095
  );
12069
- console.log(" " + ratioLabel + " " + testLabel);
12096
+ summaryRow(
12097
+ timedOut > 0 ? import_chalk9.default.yellow("\u23F1") : import_chalk9.default.dim("\u23F1"),
12098
+ "Timed out",
12099
+ timedOut,
12100
+ timedOut > 0 ? "no approval response" : void 0,
12101
+ timedOut > 0 ? (s) => import_chalk9.default.yellow(s) : (s) => import_chalk9.default.dim(s)
12102
+ );
12103
+ summaryRow(
12104
+ hardBlocked > 0 ? import_chalk9.default.red("\u{1F6D1}") : import_chalk9.default.dim("\u{1F6D1}"),
12105
+ "Auto-blocked",
12106
+ hardBlocked,
12107
+ void 0,
12108
+ hardBlocked > 0 ? (s) => import_chalk9.default.red(s) : (s) => import_chalk9.default.dim(s)
12109
+ );
12110
+ summaryRow(
12111
+ dlpBlocked > 0 ? import_chalk9.default.yellow("\u{1F6A8}") : import_chalk9.default.dim("\u{1F6A8}"),
12112
+ "DLP blocked",
12113
+ dlpBlocked,
12114
+ void 0,
12115
+ dlpBlocked > 0 ? (s) => import_chalk9.default.yellow(s) : (s) => import_chalk9.default.dim(s)
12116
+ );
12117
+ summaryRow(
12118
+ observeDlp > 0 ? import_chalk9.default.blue("\u{1F441}") : import_chalk9.default.dim("\u{1F441}"),
12119
+ "DLP (observe)",
12120
+ observeDlp,
12121
+ observeDlp > 0 ? "would-block in strict mode" : void 0,
12122
+ observeDlp > 0 ? (s) => import_chalk9.default.blue(s) : (s) => import_chalk9.default.dim(s)
12123
+ );
12124
+ summaryRow(
12125
+ loopHits > 0 ? import_chalk9.default.yellow("\u{1F504}") : import_chalk9.default.dim("\u{1F504}"),
12126
+ "Loops detected",
12127
+ loopHits,
12128
+ void 0,
12129
+ loopHits > 0 ? (s) => import_chalk9.default.yellow(s) : (s) => import_chalk9.default.dim(s)
12130
+ );
12131
+ if (trendLabel || ratioLabel || testPasses + testFails > 0) {
12132
+ console.log("");
12133
+ console.log(" " + ratioLabel + " " + testLabel + trendLabel);
12134
+ }
12070
12135
  console.log("");
12071
12136
  const toolHeaderRaw = "Top Tools";
12072
12137
  const blockHeaderRaw = "Top Blocks";
@@ -14247,6 +14312,22 @@ function claudeModelPrice2(model) {
14247
14312
  }
14248
14313
  return null;
14249
14314
  }
14315
+ var GEMINI_PRICING = {
14316
+ "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
14317
+ "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
14318
+ "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
14319
+ "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
14320
+ "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
14321
+ "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
14322
+ };
14323
+ function geminiModelPrice(model) {
14324
+ const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
14325
+ for (const [key, p] of Object.entries(GEMINI_PRICING)) {
14326
+ if (base === key || base.startsWith(key)) return p;
14327
+ }
14328
+ if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
14329
+ return null;
14330
+ }
14250
14331
  function num2(n) {
14251
14332
  return n.toLocaleString();
14252
14333
  }
@@ -14389,7 +14470,8 @@ function scanClaudeHistory(startDate) {
14389
14470
  redactedSample: dlpMatch.redactedSample,
14390
14471
  toolName,
14391
14472
  timestamp: entry.timestamp ?? "",
14392
- project: projLabel
14473
+ project: projLabel,
14474
+ agent: "claude"
14393
14475
  });
14394
14476
  }
14395
14477
  }
@@ -14408,7 +14490,135 @@ function scanClaudeHistory(startDate) {
14408
14490
  toolName,
14409
14491
  input,
14410
14492
  timestamp: entry.timestamp ?? "",
14411
- project: projLabel
14493
+ project: projLabel,
14494
+ agent: "claude"
14495
+ });
14496
+ }
14497
+ break;
14498
+ }
14499
+ }
14500
+ }
14501
+ }
14502
+ }
14503
+ return result;
14504
+ }
14505
+ function scanGeminiHistory(startDate) {
14506
+ const tmpDir = import_path36.default.join(import_os29.default.homedir(), ".gemini", "tmp");
14507
+ const result = {
14508
+ filesScanned: 0,
14509
+ sessions: 0,
14510
+ totalToolCalls: 0,
14511
+ bashCalls: 0,
14512
+ findings: [],
14513
+ dlpFindings: [],
14514
+ totalCostUSD: 0,
14515
+ firstDate: null,
14516
+ lastDate: null
14517
+ };
14518
+ if (!import_fs33.default.existsSync(tmpDir)) return result;
14519
+ let slugDirs;
14520
+ try {
14521
+ slugDirs = import_fs33.default.readdirSync(tmpDir);
14522
+ } catch {
14523
+ return result;
14524
+ }
14525
+ const ruleSources = buildRuleSources();
14526
+ for (const slug of slugDirs) {
14527
+ const slugPath = import_path36.default.join(tmpDir, slug);
14528
+ try {
14529
+ if (!import_fs33.default.statSync(slugPath).isDirectory()) continue;
14530
+ } catch {
14531
+ continue;
14532
+ }
14533
+ let projLabel = slug;
14534
+ try {
14535
+ projLabel = import_fs33.default.readFileSync(import_path36.default.join(slugPath, ".project_root"), "utf-8").trim().replace(import_os29.default.homedir(), "~").slice(0, 40);
14536
+ } catch {
14537
+ }
14538
+ const chatsDir = import_path36.default.join(slugPath, "chats");
14539
+ if (!import_fs33.default.existsSync(chatsDir)) continue;
14540
+ let chatFiles;
14541
+ try {
14542
+ chatFiles = import_fs33.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
14543
+ } catch {
14544
+ continue;
14545
+ }
14546
+ for (const chatFile of chatFiles) {
14547
+ result.filesScanned++;
14548
+ let raw;
14549
+ try {
14550
+ raw = import_fs33.default.readFileSync(import_path36.default.join(chatsDir, chatFile), "utf-8");
14551
+ } catch {
14552
+ continue;
14553
+ }
14554
+ let session;
14555
+ try {
14556
+ session = JSON.parse(raw);
14557
+ } catch {
14558
+ continue;
14559
+ }
14560
+ result.sessions++;
14561
+ for (const msg of session.messages ?? []) {
14562
+ if (msg.type !== "gemini") continue;
14563
+ if (startDate && msg.timestamp && new Date(msg.timestamp) < startDate) continue;
14564
+ if (msg.timestamp) {
14565
+ if (!result.firstDate || msg.timestamp < result.firstDate)
14566
+ result.firstDate = msg.timestamp;
14567
+ if (!result.lastDate || msg.timestamp > result.lastDate) result.lastDate = msg.timestamp;
14568
+ }
14569
+ const tokens = msg.tokens;
14570
+ const model = msg.model;
14571
+ if (tokens && model) {
14572
+ const p = geminiModelPrice(model);
14573
+ if (p) {
14574
+ const nonCached = Math.max(0, tokens.input - tokens.cached);
14575
+ result.totalCostUSD += nonCached * p.i + tokens.cached * p.cr + tokens.output * p.o;
14576
+ }
14577
+ }
14578
+ for (const tc of msg.toolCalls ?? []) {
14579
+ result.totalToolCalls++;
14580
+ const toolName = tc.name ?? "";
14581
+ const toolNameLower = toolName.toLowerCase();
14582
+ const input = tc.args ?? {};
14583
+ if (toolNameLower === "run_shell_command" || toolNameLower === "shell") {
14584
+ result.bashCalls++;
14585
+ }
14586
+ const rawCmd = String(input.command ?? "").trimStart();
14587
+ if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
14588
+ continue;
14589
+ const dlpMatch = scanArgs(input);
14590
+ if (dlpMatch) {
14591
+ const isDupe = result.dlpFindings.some(
14592
+ (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
14593
+ );
14594
+ if (!isDupe) {
14595
+ result.dlpFindings.push({
14596
+ patternName: dlpMatch.patternName,
14597
+ redactedSample: dlpMatch.redactedSample,
14598
+ toolName,
14599
+ timestamp: msg.timestamp ?? "",
14600
+ project: projLabel,
14601
+ agent: "gemini"
14602
+ });
14603
+ }
14604
+ }
14605
+ for (const source of ruleSources) {
14606
+ const { rule } = source;
14607
+ if (rule.verdict === "allow") continue;
14608
+ if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
14609
+ if (!evaluateSmartConditions(input, rule)) continue;
14610
+ const inputPreview = preview(input, 120);
14611
+ const isDupe = result.findings.some(
14612
+ (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
14613
+ );
14614
+ if (!isDupe) {
14615
+ result.findings.push({
14616
+ source,
14617
+ toolName,
14618
+ input,
14619
+ timestamp: msg.timestamp ?? "",
14620
+ project: projLabel,
14621
+ agent: "gemini"
14412
14622
  });
14413
14623
  }
14414
14624
  break;
@@ -14419,6 +14629,21 @@ function scanClaudeHistory(startDate) {
14419
14629
  }
14420
14630
  return result;
14421
14631
  }
14632
+ function mergeScans(a, b) {
14633
+ const dates = [a.firstDate, b.firstDate].filter(Boolean);
14634
+ const lastDates = [a.lastDate, b.lastDate].filter(Boolean);
14635
+ return {
14636
+ filesScanned: a.filesScanned + b.filesScanned,
14637
+ sessions: a.sessions + b.sessions,
14638
+ totalToolCalls: a.totalToolCalls + b.totalToolCalls,
14639
+ bashCalls: a.bashCalls + b.bashCalls,
14640
+ findings: [...a.findings, ...b.findings],
14641
+ dlpFindings: [...a.dlpFindings, ...b.dlpFindings],
14642
+ totalCostUSD: a.totalCostUSD + b.totalCostUSD,
14643
+ firstDate: dates.length ? dates.sort()[0] : null,
14644
+ lastDate: lastDates.length ? lastDates.sort().at(-1) : null
14645
+ };
14646
+ }
14422
14647
  function registerScanCommand(program2) {
14423
14648
  program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per shield", "5").action((options) => {
14424
14649
  const topN = Math.max(1, parseInt(options.top, 10) || 5);
@@ -14431,23 +14656,25 @@ function registerScanCommand(program2) {
14431
14656
  console.log("");
14432
14657
  console.log(import_chalk21.default.cyan.bold("\u{1F50D} node9 scan") + import_chalk21.default.dim(" \u2014 what would node9 catch?"));
14433
14658
  console.log("");
14434
- const projectsDir = import_path36.default.join(import_os29.default.homedir(), ".claude", "projects");
14435
- if (!import_fs33.default.existsSync(projectsDir)) {
14436
- console.log(import_chalk21.default.yellow(" No Claude history found at ~/.claude/projects/"));
14437
- console.log(import_chalk21.default.gray(" Install Claude Code, run a few sessions, then try again.\n"));
14438
- return;
14439
- }
14440
14659
  process.stdout.write(import_chalk21.default.dim(" Scanning\u2026"));
14441
- const scan = scanClaudeHistory(startDate);
14660
+ const claudeScan = scanClaudeHistory(startDate);
14661
+ const geminiScan = scanGeminiHistory(startDate);
14662
+ const scan = mergeScans(claudeScan, geminiScan);
14442
14663
  process.stdout.write("\r" + " ".repeat(20) + "\r");
14443
14664
  if (scan.filesScanned === 0) {
14444
- console.log(import_chalk21.default.yellow(" No JSONL session files found.\n"));
14665
+ console.log(import_chalk21.default.yellow(" No session history found."));
14666
+ console.log(
14667
+ import_chalk21.default.gray(
14668
+ " Supported: Claude Code (~/.claude/projects/) \xB7 Gemini CLI (~/.gemini/tmp/)\n"
14669
+ )
14670
+ );
14445
14671
  return;
14446
14672
  }
14447
14673
  const rangeLabel = options.all ? import_chalk21.default.dim("all time") : import_chalk21.default.dim(`last ${options.days ?? 90} days`);
14448
14674
  const dateRange = scan.firstDate && scan.lastDate ? import_chalk21.default.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
14675
+ const sessionBreakdown = claudeScan.sessions > 0 && geminiScan.sessions > 0 ? import_chalk21.default.dim("(") + import_chalk21.default.cyan(String(claudeScan.sessions)) + import_chalk21.default.dim(" Claude \xB7 ") + import_chalk21.default.blue(String(geminiScan.sessions)) + import_chalk21.default.dim(" Gemini)") : "";
14449
14676
  console.log(
14450
- " " + import_chalk21.default.white(num2(scan.sessions)) + import_chalk21.default.dim(" sessions ") + import_chalk21.default.white(num2(scan.totalToolCalls)) + import_chalk21.default.dim(" tool calls ") + import_chalk21.default.white(num2(scan.bashCalls)) + import_chalk21.default.dim(" bash commands ") + rangeLabel + dateRange
14677
+ " " + import_chalk21.default.white(num2(scan.sessions)) + import_chalk21.default.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + import_chalk21.default.white(num2(scan.totalToolCalls)) + import_chalk21.default.dim(" tool calls ") + import_chalk21.default.white(num2(scan.bashCalls)) + import_chalk21.default.dim(" bash commands ") + rangeLabel + dateRange
14451
14678
  );
14452
14679
  console.log("");
14453
14680
  const byShield = /* @__PURE__ */ new Map();
@@ -14499,8 +14726,9 @@ function registerScanCommand(program2) {
14499
14726
  for (const f of shown) {
14500
14727
  const ts = f.timestamp ? import_chalk21.default.dim(fmtTs(f.timestamp) + " ") : "";
14501
14728
  const proj = import_chalk21.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
14502
- const cmd = import_chalk21.default.gray(preview(f.input, 55));
14503
- console.log(` ${ts}${proj}${cmd}`);
14729
+ const agentBadge = f.agent === "gemini" ? import_chalk21.default.blue("[Gemini] ") : import_chalk21.default.cyan("[Claude] ");
14730
+ const cmd = import_chalk21.default.gray(preview(f.input, 45));
14731
+ console.log(` ${ts}${proj}${agentBadge}${cmd}`);
14504
14732
  }
14505
14733
  if (ruleFindings.length > topN) {
14506
14734
  console.log(
@@ -14524,8 +14752,9 @@ function registerScanCommand(program2) {
14524
14752
  for (const f of shownDlp) {
14525
14753
  const ts = f.timestamp ? import_chalk21.default.dim(fmtTs(f.timestamp) + " ") : "";
14526
14754
  const proj = import_chalk21.default.dim(f.project.slice(0, 22).padEnd(22) + " ");
14755
+ const agentBadge = f.agent === "gemini" ? import_chalk21.default.blue("[Gemini] ") : import_chalk21.default.cyan("[Claude] ");
14527
14756
  console.log(
14528
- ` ${ts}${proj}` + import_chalk21.default.yellow(f.patternName) + import_chalk21.default.dim(" ") + import_chalk21.default.gray(f.redactedSample)
14757
+ ` ${ts}${proj}${agentBadge}` + import_chalk21.default.yellow(f.patternName) + import_chalk21.default.dim(" ") + import_chalk21.default.gray(f.redactedSample)
14529
14758
  );
14530
14759
  }
14531
14760
  if (scan.dlpFindings.length > topN) {
@@ -14540,7 +14769,7 @@ function registerScanCommand(program2) {
14540
14769
  }
14541
14770
  if (scan.totalCostUSD > 0) {
14542
14771
  console.log(
14543
- " " + import_chalk21.default.bold("Claude spend:") + " " + import_chalk21.default.yellow(fmtCost2(scan.totalCostUSD)) + import_chalk21.default.dim(" (for per-period breakdown: node9 report)")
14772
+ " " + import_chalk21.default.bold("Agent spend:") + " " + import_chalk21.default.yellow(fmtCost2(scan.totalCostUSD)) + import_chalk21.default.dim(" (for per-period breakdown: node9 report)")
14544
14773
  );
14545
14774
  console.log("");
14546
14775
  }
@@ -14584,6 +14813,22 @@ function modelPrice(model) {
14584
14813
  }
14585
14814
  return null;
14586
14815
  }
14816
+ var GEMINI_PRICING2 = {
14817
+ "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
14818
+ "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
14819
+ "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
14820
+ "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
14821
+ "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
14822
+ "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
14823
+ };
14824
+ function geminiModelPrice2(model) {
14825
+ const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
14826
+ for (const [key, p] of Object.entries(GEMINI_PRICING2)) {
14827
+ if (base === key || base.startsWith(key)) return p;
14828
+ }
14829
+ if (base.includes("flash")) return GEMINI_PRICING2["gemini-2.0-flash"];
14830
+ return null;
14831
+ }
14587
14832
  function encodeProjectPath(projectPath) {
14588
14833
  return projectPath.replace(/\//g, "-");
14589
14834
  }
@@ -14699,6 +14944,123 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
14699
14944
  }
14700
14945
  return result;
14701
14946
  }
14947
+ function buildGeminiSessions(days, allAuditEntries) {
14948
+ const tmpDir = import_path37.default.join(import_os30.default.homedir(), ".gemini", "tmp");
14949
+ if (!import_fs34.default.existsSync(tmpDir)) return [];
14950
+ const cutoff = days !== null ? (() => {
14951
+ const d = /* @__PURE__ */ new Date();
14952
+ d.setDate(d.getDate() - days);
14953
+ d.setHours(0, 0, 0, 0);
14954
+ return d;
14955
+ })() : null;
14956
+ let slugDirs;
14957
+ try {
14958
+ slugDirs = import_fs34.default.readdirSync(tmpDir);
14959
+ } catch {
14960
+ return [];
14961
+ }
14962
+ const summaries = [];
14963
+ for (const slug of slugDirs) {
14964
+ const slugPath = import_path37.default.join(tmpDir, slug);
14965
+ try {
14966
+ if (!import_fs34.default.statSync(slugPath).isDirectory()) continue;
14967
+ } catch {
14968
+ continue;
14969
+ }
14970
+ let projectRoot = import_path37.default.join(import_os30.default.homedir(), slug);
14971
+ try {
14972
+ projectRoot = import_fs34.default.readFileSync(import_path37.default.join(slugPath, ".project_root"), "utf-8").trim();
14973
+ } catch {
14974
+ }
14975
+ const chatsDir = import_path37.default.join(slugPath, "chats");
14976
+ if (!import_fs34.default.existsSync(chatsDir)) continue;
14977
+ let chatFiles;
14978
+ try {
14979
+ chatFiles = import_fs34.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
14980
+ } catch {
14981
+ continue;
14982
+ }
14983
+ for (const chatFile of chatFiles) {
14984
+ let raw;
14985
+ try {
14986
+ raw = import_fs34.default.readFileSync(import_path37.default.join(chatsDir, chatFile), "utf-8");
14987
+ } catch {
14988
+ continue;
14989
+ }
14990
+ let session;
14991
+ try {
14992
+ session = JSON.parse(raw);
14993
+ } catch {
14994
+ continue;
14995
+ }
14996
+ const startTime = session.startTime ?? "";
14997
+ if (!startTime) continue;
14998
+ if (cutoff && new Date(startTime) < cutoff) continue;
14999
+ let firstPrompt = "";
15000
+ for (const msg of session.messages ?? []) {
15001
+ if (msg.type === "user") {
15002
+ const content = msg.content;
15003
+ if (Array.isArray(content) && content[0]?.text) {
15004
+ firstPrompt = content[0].text;
15005
+ } else if (typeof content === "string") {
15006
+ firstPrompt = content;
15007
+ }
15008
+ break;
15009
+ }
15010
+ }
15011
+ const toolCalls = [];
15012
+ let costUSD = 0;
15013
+ const modifiedFiles = [];
15014
+ const seenFiles = /* @__PURE__ */ new Set();
15015
+ let lastToolTs = "";
15016
+ for (const msg of session.messages ?? []) {
15017
+ if (msg.type !== "gemini") continue;
15018
+ const tokens = msg.tokens;
15019
+ const model = msg.model;
15020
+ if (tokens && model) {
15021
+ const p = geminiModelPrice2(model);
15022
+ if (p) {
15023
+ const nonCached = Math.max(0, tokens.input - tokens.cached);
15024
+ costUSD += nonCached * p.i + tokens.cached * p.cr + tokens.output * p.o;
15025
+ }
15026
+ }
15027
+ for (const tc of msg.toolCalls ?? []) {
15028
+ const tool = tc.name ?? "";
15029
+ const input = tc.args ?? {};
15030
+ const ts = msg.timestamp ?? "";
15031
+ toolCalls.push({ tool, input, timestamp: ts });
15032
+ if (ts > lastToolTs) lastToolTs = ts;
15033
+ const toolLower = tool.toLowerCase();
15034
+ if (toolLower === "write_file" || toolLower === "edit_file" || toolLower === "create_file" || toolLower === "overwrite_file") {
15035
+ const fp = input.file_path ?? input.path ?? input.filename;
15036
+ if (typeof fp === "string" && !seenFiles.has(fp)) {
15037
+ seenFiles.add(fp);
15038
+ modifiedFiles.push(fp);
15039
+ }
15040
+ }
15041
+ }
15042
+ }
15043
+ const windowEnd = new Date(
15044
+ Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
15045
+ ).toISOString();
15046
+ const blockedCalls = auditEntriesInWindow(allAuditEntries, startTime, windowEnd);
15047
+ summaries.push({
15048
+ sessionId: session.sessionId ?? chatFile.replace(".json", ""),
15049
+ project: projectRoot,
15050
+ projectLabel: projectLabel(projectRoot),
15051
+ firstPrompt,
15052
+ startTime,
15053
+ toolCalls,
15054
+ blockedCalls,
15055
+ costUSD,
15056
+ hasSnapshot: false,
15057
+ modifiedFiles,
15058
+ agent: "gemini"
15059
+ });
15060
+ }
15061
+ }
15062
+ return summaries;
15063
+ }
14702
15064
  function buildSessions(days, historyPath) {
14703
15065
  const hPath = historyPath ?? import_path37.default.join(import_os30.default.homedir(), ".claude", "history.jsonl");
14704
15066
  let historyRaw;
@@ -14749,9 +15111,13 @@ function buildSessions(days, historyPath) {
14749
15111
  blockedCalls,
14750
15112
  costUSD,
14751
15113
  hasSnapshot,
14752
- modifiedFiles
15114
+ modifiedFiles,
15115
+ agent: "claude"
14753
15116
  });
14754
15117
  }
15118
+ if (!historyPath) {
15119
+ summaries.push(...buildGeminiSessions(days, allAuditEntries));
15120
+ }
14755
15121
  summaries.sort((a, b) => a.startTime > b.startTime ? -1 : 1);
14756
15122
  return summaries;
14757
15123
  }
@@ -14863,9 +15229,9 @@ function renderSummary(summaries) {
14863
15229
  const maxGroup = Math.max(...Object.values(groups));
14864
15230
  for (const [label, count] of Object.entries(groups)) {
14865
15231
  if (count === 0) continue;
14866
- const pct2 = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
15232
+ const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
14867
15233
  console.log(
14868
- " " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + import_chalk22.default.white(String(count).padStart(4)) + import_chalk22.default.dim(` (${String(pct2)}%)`)
15234
+ " " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + import_chalk22.default.white(String(count).padStart(4)) + import_chalk22.default.dim(` (${String(pct)}%)`)
14869
15235
  );
14870
15236
  }
14871
15237
  console.log("");
@@ -14907,8 +15273,9 @@ function renderList(summaries, totalCost) {
14907
15273
  const cost = s.costUSD > 0 ? import_chalk22.default.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
14908
15274
  const blocked = s.blockedCalls.length > 0 ? import_chalk22.default.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
14909
15275
  const snap = s.hasSnapshot ? import_chalk22.default.green(" \u{1F4F8}") : "";
15276
+ const agentBadge = s.agent === "gemini" ? import_chalk22.default.blue(" [Gemini]") : import_chalk22.default.cyan(" [Claude]");
14910
15277
  const sid = import_chalk22.default.dim(" " + s.sessionId.slice(0, 8));
14911
- console.log(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${sid}`);
15278
+ console.log(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}`);
14912
15279
  }
14913
15280
  console.log("");
14914
15281
  console.log(
@@ -14923,6 +15290,10 @@ function renderDetail(s) {
14923
15290
  import_chalk22.default.bold(" Prompt ") + import_chalk22.default.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
14924
15291
  );
14925
15292
  console.log(import_chalk22.default.bold(" Project ") + import_chalk22.default.white(s.projectLabel));
15293
+ if (s.agent) {
15294
+ const agentLabel2 = s.agent === "gemini" ? import_chalk22.default.blue("Gemini CLI") : import_chalk22.default.cyan("Claude Code");
15295
+ console.log(import_chalk22.default.bold(" Agent ") + agentLabel2);
15296
+ }
14926
15297
  console.log(import_chalk22.default.bold(" When ") + import_chalk22.default.white(fmtDateTime(s.startTime)));
14927
15298
  if (s.costUSD > 0)
14928
15299
  console.log(import_chalk22.default.bold(" Cost ") + import_chalk22.default.yellow("~" + fmtCost3(s.costUSD)));
package/dist/cli.mjs CHANGED
@@ -97,7 +97,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
97
97
  }
98
98
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
99
99
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
100
- const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
100
+ const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
101
101
  appendToLog(LOCAL_AUDIT_LOG, {
102
102
  ts: (/* @__PURE__ */ new Date()).toISOString(),
103
103
  tool: toolName,
@@ -8627,10 +8627,10 @@ function bold(s) {
8627
8627
  function color(c, s) {
8628
8628
  return `${c}${s}${RESET3}`;
8629
8629
  }
8630
- function progressBar(pct2, warnAt = 70, critAt = 85) {
8631
- const filled = Math.round(Math.min(pct2, 100) / 100 * BAR_WIDTH);
8630
+ function progressBar(pct, warnAt = 70, critAt = 85) {
8631
+ const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
8632
8632
  const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
8633
- const c = pct2 >= critAt ? RED2 : pct2 >= warnAt ? YELLOW2 : GREEN2;
8633
+ const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
8634
8634
  return `${c}${bar}${RESET3}`;
8635
8635
  }
8636
8636
  function formatTimeLeft(resetsAt) {
@@ -8846,15 +8846,15 @@ function renderContextLine(stdin) {
8846
8846
  }
8847
8847
  const rl = stdin.rate_limits;
8848
8848
  if (rl?.five_hour?.used_percentage !== void 0) {
8849
- const pct2 = Math.round(rl.five_hour.used_percentage);
8850
- const bar = progressBar(pct2, 60, 80);
8849
+ const pct = Math.round(rl.five_hour.used_percentage);
8850
+ const bar = progressBar(pct, 60, 80);
8851
8851
  const left = formatTimeLeft(rl.five_hour.resets_at);
8852
- parts.push(`${dim("\u2502")} 5h ${bar} ${pct2}%${left}`);
8852
+ parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
8853
8853
  }
8854
8854
  if (rl?.seven_day?.used_percentage !== void 0) {
8855
- const pct2 = Math.round(rl.seven_day.used_percentage);
8856
- const bar = progressBar(pct2, 60, 80);
8857
- parts.push(`${dim("\u2502")} 7d ${bar} ${pct2}%`);
8855
+ const pct = Math.round(rl.seven_day.used_percentage);
8856
+ const bar = progressBar(pct, 60, 80);
8857
+ parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
8858
8858
  }
8859
8859
  if (parts.length === 0) return null;
8860
8860
  return parts.join(" ");
@@ -11723,8 +11723,8 @@ function buildTestTimestamps(allEntries) {
11723
11723
  return testTs;
11724
11724
  }
11725
11725
  function isTestEntry(entry, testTs) {
11726
- if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
11727
11726
  if (entry.testRun === true) return true;
11727
+ if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
11728
11728
  const cmd = entry.args?.command;
11729
11729
  if (typeof cmd === "string") return TEST_COMMAND_RE3.test(cmd);
11730
11730
  const t = new Date(entry.ts).getTime();
@@ -11782,10 +11782,6 @@ function colorBar(value, max, width) {
11782
11782
  const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
11783
11783
  return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
11784
11784
  }
11785
- function pct(num3, total) {
11786
- if (total === 0) return "\u2013";
11787
- return Math.round(num3 / total * 100) + "%";
11788
- }
11789
11785
  function fmtDate(d) {
11790
11786
  const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
11791
11787
  return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
@@ -11956,9 +11952,12 @@ function registerReportCommand(program2) {
11956
11952
  `));
11957
11953
  return;
11958
11954
  }
11959
- let allowed = 0;
11960
- let blocked = 0;
11961
- let dlpHits = 0;
11955
+ let userApproved = 0;
11956
+ let userDenied = 0;
11957
+ let timedOut = 0;
11958
+ let hardBlocked = 0;
11959
+ let dlpBlocked = 0;
11960
+ let observeDlp = 0;
11962
11961
  let loopHits = 0;
11963
11962
  let testPasses = 0;
11964
11963
  let testFails = 0;
@@ -11971,9 +11970,16 @@ function registerReportCommand(program2) {
11971
11970
  for (const e of entries) {
11972
11971
  const allow = isAllow(e.decision);
11973
11972
  const dateKey = e.ts.slice(0, 10);
11974
- if (allow) allowed++;
11975
- else blocked++;
11976
- if (isDlp(e.checkedBy)) dlpHits++;
11973
+ const userInteracted = e.source === "daemon";
11974
+ if (userInteracted) {
11975
+ if (allow) userApproved++;
11976
+ else userDenied++;
11977
+ } else if (!allow) {
11978
+ if (e.checkedBy === "timeout") timedOut++;
11979
+ else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
11980
+ else if (isDlp(e.checkedBy)) dlpBlocked++;
11981
+ else if (e.checkedBy !== "loop-detected") hardBlocked++;
11982
+ }
11977
11983
  if (e.checkedBy === "loop-detected") loopHits++;
11978
11984
  const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
11979
11985
  t.calls++;
@@ -12024,25 +12030,84 @@ function registerReportCommand(program2) {
12024
12030
  " " + chalk9.bold.cyan("\u{1F6E1} node9 Report") + chalk9.dim(" \xB7 ") + chalk9.white(periodLabel[period]) + chalk9.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + chalk9.dim(` ${num(total)} events`) + (excludeTests ? chalk9.dim(` \u2013tests (\u2013${filteredTestCount})`) : "")
12025
12031
  );
12026
12032
  console.log(" " + line);
12027
- console.log("");
12028
- const blockLabel = blocked > 0 ? chalk9.red(`\u{1F6D1} ${num(blocked)} blocked`) : chalk9.dim("\u{1F6D1} 0 blocked");
12029
- const dlpLabel = dlpHits > 0 ? chalk9.yellow(`\u{1F6A8} ${dlpHits} DLP hits`) : chalk9.dim("\u{1F6A8} 0 DLP hits");
12030
- const loopLabel = loopHits > 0 ? chalk9.yellow(`\u{1F504} ${loopHits} loops`) : chalk9.dim("\u{1F504} 0 loops");
12031
- const currentRate = total > 0 ? blocked / total : 0;
12033
+ const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
12034
+ const currentRate = total > 0 ? totalBlocked / total : 0;
12032
12035
  const trendLabel = (() => {
12033
- if (priorBlockRate === null) return chalk9.dim(`${pct(blocked, total)} block rate`);
12036
+ if (priorBlockRate === null) return "";
12034
12037
  const delta = Math.round((currentRate - priorBlockRate) * 100);
12035
- const arrow = delta > 0 ? chalk9.red(`\u25B2${delta}%`) : delta < 0 ? chalk9.green(`\u25BC${Math.abs(delta)}%`) : chalk9.dim("\u2013");
12036
- return chalk9.dim(`${pct(blocked, total)} block rate `) + arrow + chalk9.dim(" vs prior");
12038
+ if (delta === 0) return "";
12039
+ return " " + (delta > 0 ? chalk9.red(`\u25B2${delta}%`) : chalk9.green(`\u25BC${Math.abs(delta)}%`)) + chalk9.dim(" vs prior");
12037
12040
  })();
12038
12041
  const reads = toolMap.get("Read")?.calls ?? 0;
12039
12042
  const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
12040
12043
  const ratioLabel = reads > 0 ? chalk9.dim(`edit/read ${(edits / reads).toFixed(1)}`) : chalk9.dim("edit/read \u2013");
12041
12044
  const testLabel = testPasses + testFails > 0 ? chalk9.dim("tests ") + chalk9.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + chalk9.red(`${testFails}\u2717`) : "") : chalk9.dim("tests \u2013");
12045
+ console.log("");
12046
+ console.log(" " + chalk9.bold("Protection Summary"));
12047
+ console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
12042
12048
  console.log(
12043
- " " + chalk9.green(`\u2705 ${num(allowed)} allowed`) + " " + blockLabel + " " + dlpLabel + " " + loopLabel + " " + trendLabel
12049
+ " " + chalk9.dim("Intercepted") + " " + chalk9.white(num(total)) + chalk9.dim(" tool calls")
12050
+ );
12051
+ console.log("");
12052
+ const COL1 = 18;
12053
+ const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
12054
+ const countStr = colorFn(num(count));
12055
+ const noteStr = note ? chalk9.dim(" " + note) : "";
12056
+ console.log(" " + icon + " " + chalk9.white(label.padEnd(COL1)) + countStr + noteStr);
12057
+ };
12058
+ summaryRow(
12059
+ userApproved > 0 ? chalk9.green("\u2705") : chalk9.dim("\u2705"),
12060
+ "User approved",
12061
+ userApproved,
12062
+ userApproved === 0 ? "no popups this period" : void 0,
12063
+ userApproved > 0 ? (s) => chalk9.green(s) : (s) => chalk9.dim(s)
12064
+ );
12065
+ summaryRow(
12066
+ userDenied > 0 ? chalk9.red("\u{1F6AB}") : chalk9.dim("\u{1F6AB}"),
12067
+ "User denied",
12068
+ userDenied,
12069
+ void 0,
12070
+ userDenied > 0 ? (s) => chalk9.red(s) : (s) => chalk9.dim(s)
12044
12071
  );
12045
- console.log(" " + ratioLabel + " " + testLabel);
12072
+ summaryRow(
12073
+ timedOut > 0 ? chalk9.yellow("\u23F1") : chalk9.dim("\u23F1"),
12074
+ "Timed out",
12075
+ timedOut,
12076
+ timedOut > 0 ? "no approval response" : void 0,
12077
+ timedOut > 0 ? (s) => chalk9.yellow(s) : (s) => chalk9.dim(s)
12078
+ );
12079
+ summaryRow(
12080
+ hardBlocked > 0 ? chalk9.red("\u{1F6D1}") : chalk9.dim("\u{1F6D1}"),
12081
+ "Auto-blocked",
12082
+ hardBlocked,
12083
+ void 0,
12084
+ hardBlocked > 0 ? (s) => chalk9.red(s) : (s) => chalk9.dim(s)
12085
+ );
12086
+ summaryRow(
12087
+ dlpBlocked > 0 ? chalk9.yellow("\u{1F6A8}") : chalk9.dim("\u{1F6A8}"),
12088
+ "DLP blocked",
12089
+ dlpBlocked,
12090
+ void 0,
12091
+ dlpBlocked > 0 ? (s) => chalk9.yellow(s) : (s) => chalk9.dim(s)
12092
+ );
12093
+ summaryRow(
12094
+ observeDlp > 0 ? chalk9.blue("\u{1F441}") : chalk9.dim("\u{1F441}"),
12095
+ "DLP (observe)",
12096
+ observeDlp,
12097
+ observeDlp > 0 ? "would-block in strict mode" : void 0,
12098
+ observeDlp > 0 ? (s) => chalk9.blue(s) : (s) => chalk9.dim(s)
12099
+ );
12100
+ summaryRow(
12101
+ loopHits > 0 ? chalk9.yellow("\u{1F504}") : chalk9.dim("\u{1F504}"),
12102
+ "Loops detected",
12103
+ loopHits,
12104
+ void 0,
12105
+ loopHits > 0 ? (s) => chalk9.yellow(s) : (s) => chalk9.dim(s)
12106
+ );
12107
+ if (trendLabel || ratioLabel || testPasses + testFails > 0) {
12108
+ console.log("");
12109
+ console.log(" " + ratioLabel + " " + testLabel + trendLabel);
12110
+ }
12046
12111
  console.log("");
12047
12112
  const toolHeaderRaw = "Top Tools";
12048
12113
  const blockHeaderRaw = "Top Blocks";
@@ -14223,6 +14288,22 @@ function claudeModelPrice2(model) {
14223
14288
  }
14224
14289
  return null;
14225
14290
  }
14291
+ var GEMINI_PRICING = {
14292
+ "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
14293
+ "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
14294
+ "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
14295
+ "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
14296
+ "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
14297
+ "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
14298
+ };
14299
+ function geminiModelPrice(model) {
14300
+ const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
14301
+ for (const [key, p] of Object.entries(GEMINI_PRICING)) {
14302
+ if (base === key || base.startsWith(key)) return p;
14303
+ }
14304
+ if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
14305
+ return null;
14306
+ }
14226
14307
  function num2(n) {
14227
14308
  return n.toLocaleString();
14228
14309
  }
@@ -14365,7 +14446,8 @@ function scanClaudeHistory(startDate) {
14365
14446
  redactedSample: dlpMatch.redactedSample,
14366
14447
  toolName,
14367
14448
  timestamp: entry.timestamp ?? "",
14368
- project: projLabel
14449
+ project: projLabel,
14450
+ agent: "claude"
14369
14451
  });
14370
14452
  }
14371
14453
  }
@@ -14384,7 +14466,135 @@ function scanClaudeHistory(startDate) {
14384
14466
  toolName,
14385
14467
  input,
14386
14468
  timestamp: entry.timestamp ?? "",
14387
- project: projLabel
14469
+ project: projLabel,
14470
+ agent: "claude"
14471
+ });
14472
+ }
14473
+ break;
14474
+ }
14475
+ }
14476
+ }
14477
+ }
14478
+ }
14479
+ return result;
14480
+ }
14481
+ function scanGeminiHistory(startDate) {
14482
+ const tmpDir = path36.join(os29.homedir(), ".gemini", "tmp");
14483
+ const result = {
14484
+ filesScanned: 0,
14485
+ sessions: 0,
14486
+ totalToolCalls: 0,
14487
+ bashCalls: 0,
14488
+ findings: [],
14489
+ dlpFindings: [],
14490
+ totalCostUSD: 0,
14491
+ firstDate: null,
14492
+ lastDate: null
14493
+ };
14494
+ if (!fs33.existsSync(tmpDir)) return result;
14495
+ let slugDirs;
14496
+ try {
14497
+ slugDirs = fs33.readdirSync(tmpDir);
14498
+ } catch {
14499
+ return result;
14500
+ }
14501
+ const ruleSources = buildRuleSources();
14502
+ for (const slug of slugDirs) {
14503
+ const slugPath = path36.join(tmpDir, slug);
14504
+ try {
14505
+ if (!fs33.statSync(slugPath).isDirectory()) continue;
14506
+ } catch {
14507
+ continue;
14508
+ }
14509
+ let projLabel = slug;
14510
+ try {
14511
+ projLabel = fs33.readFileSync(path36.join(slugPath, ".project_root"), "utf-8").trim().replace(os29.homedir(), "~").slice(0, 40);
14512
+ } catch {
14513
+ }
14514
+ const chatsDir = path36.join(slugPath, "chats");
14515
+ if (!fs33.existsSync(chatsDir)) continue;
14516
+ let chatFiles;
14517
+ try {
14518
+ chatFiles = fs33.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
14519
+ } catch {
14520
+ continue;
14521
+ }
14522
+ for (const chatFile of chatFiles) {
14523
+ result.filesScanned++;
14524
+ let raw;
14525
+ try {
14526
+ raw = fs33.readFileSync(path36.join(chatsDir, chatFile), "utf-8");
14527
+ } catch {
14528
+ continue;
14529
+ }
14530
+ let session;
14531
+ try {
14532
+ session = JSON.parse(raw);
14533
+ } catch {
14534
+ continue;
14535
+ }
14536
+ result.sessions++;
14537
+ for (const msg of session.messages ?? []) {
14538
+ if (msg.type !== "gemini") continue;
14539
+ if (startDate && msg.timestamp && new Date(msg.timestamp) < startDate) continue;
14540
+ if (msg.timestamp) {
14541
+ if (!result.firstDate || msg.timestamp < result.firstDate)
14542
+ result.firstDate = msg.timestamp;
14543
+ if (!result.lastDate || msg.timestamp > result.lastDate) result.lastDate = msg.timestamp;
14544
+ }
14545
+ const tokens = msg.tokens;
14546
+ const model = msg.model;
14547
+ if (tokens && model) {
14548
+ const p = geminiModelPrice(model);
14549
+ if (p) {
14550
+ const nonCached = Math.max(0, tokens.input - tokens.cached);
14551
+ result.totalCostUSD += nonCached * p.i + tokens.cached * p.cr + tokens.output * p.o;
14552
+ }
14553
+ }
14554
+ for (const tc of msg.toolCalls ?? []) {
14555
+ result.totalToolCalls++;
14556
+ const toolName = tc.name ?? "";
14557
+ const toolNameLower = toolName.toLowerCase();
14558
+ const input = tc.args ?? {};
14559
+ if (toolNameLower === "run_shell_command" || toolNameLower === "shell") {
14560
+ result.bashCalls++;
14561
+ }
14562
+ const rawCmd = String(input.command ?? "").trimStart();
14563
+ if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
14564
+ continue;
14565
+ const dlpMatch = scanArgs(input);
14566
+ if (dlpMatch) {
14567
+ const isDupe = result.dlpFindings.some(
14568
+ (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
14569
+ );
14570
+ if (!isDupe) {
14571
+ result.dlpFindings.push({
14572
+ patternName: dlpMatch.patternName,
14573
+ redactedSample: dlpMatch.redactedSample,
14574
+ toolName,
14575
+ timestamp: msg.timestamp ?? "",
14576
+ project: projLabel,
14577
+ agent: "gemini"
14578
+ });
14579
+ }
14580
+ }
14581
+ for (const source of ruleSources) {
14582
+ const { rule } = source;
14583
+ if (rule.verdict === "allow") continue;
14584
+ if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
14585
+ if (!evaluateSmartConditions(input, rule)) continue;
14586
+ const inputPreview = preview(input, 120);
14587
+ const isDupe = result.findings.some(
14588
+ (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
14589
+ );
14590
+ if (!isDupe) {
14591
+ result.findings.push({
14592
+ source,
14593
+ toolName,
14594
+ input,
14595
+ timestamp: msg.timestamp ?? "",
14596
+ project: projLabel,
14597
+ agent: "gemini"
14388
14598
  });
14389
14599
  }
14390
14600
  break;
@@ -14395,6 +14605,21 @@ function scanClaudeHistory(startDate) {
14395
14605
  }
14396
14606
  return result;
14397
14607
  }
14608
+ function mergeScans(a, b) {
14609
+ const dates = [a.firstDate, b.firstDate].filter(Boolean);
14610
+ const lastDates = [a.lastDate, b.lastDate].filter(Boolean);
14611
+ return {
14612
+ filesScanned: a.filesScanned + b.filesScanned,
14613
+ sessions: a.sessions + b.sessions,
14614
+ totalToolCalls: a.totalToolCalls + b.totalToolCalls,
14615
+ bashCalls: a.bashCalls + b.bashCalls,
14616
+ findings: [...a.findings, ...b.findings],
14617
+ dlpFindings: [...a.dlpFindings, ...b.dlpFindings],
14618
+ totalCostUSD: a.totalCostUSD + b.totalCostUSD,
14619
+ firstDate: dates.length ? dates.sort()[0] : null,
14620
+ lastDate: lastDates.length ? lastDates.sort().at(-1) : null
14621
+ };
14622
+ }
14398
14623
  function registerScanCommand(program2) {
14399
14624
  program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per shield", "5").action((options) => {
14400
14625
  const topN = Math.max(1, parseInt(options.top, 10) || 5);
@@ -14407,23 +14632,25 @@ function registerScanCommand(program2) {
14407
14632
  console.log("");
14408
14633
  console.log(chalk21.cyan.bold("\u{1F50D} node9 scan") + chalk21.dim(" \u2014 what would node9 catch?"));
14409
14634
  console.log("");
14410
- const projectsDir = path36.join(os29.homedir(), ".claude", "projects");
14411
- if (!fs33.existsSync(projectsDir)) {
14412
- console.log(chalk21.yellow(" No Claude history found at ~/.claude/projects/"));
14413
- console.log(chalk21.gray(" Install Claude Code, run a few sessions, then try again.\n"));
14414
- return;
14415
- }
14416
14635
  process.stdout.write(chalk21.dim(" Scanning\u2026"));
14417
- const scan = scanClaudeHistory(startDate);
14636
+ const claudeScan = scanClaudeHistory(startDate);
14637
+ const geminiScan = scanGeminiHistory(startDate);
14638
+ const scan = mergeScans(claudeScan, geminiScan);
14418
14639
  process.stdout.write("\r" + " ".repeat(20) + "\r");
14419
14640
  if (scan.filesScanned === 0) {
14420
- console.log(chalk21.yellow(" No JSONL session files found.\n"));
14641
+ console.log(chalk21.yellow(" No session history found."));
14642
+ console.log(
14643
+ chalk21.gray(
14644
+ " Supported: Claude Code (~/.claude/projects/) \xB7 Gemini CLI (~/.gemini/tmp/)\n"
14645
+ )
14646
+ );
14421
14647
  return;
14422
14648
  }
14423
14649
  const rangeLabel = options.all ? chalk21.dim("all time") : chalk21.dim(`last ${options.days ?? 90} days`);
14424
14650
  const dateRange = scan.firstDate && scan.lastDate ? chalk21.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
14651
+ const sessionBreakdown = claudeScan.sessions > 0 && geminiScan.sessions > 0 ? chalk21.dim("(") + chalk21.cyan(String(claudeScan.sessions)) + chalk21.dim(" Claude \xB7 ") + chalk21.blue(String(geminiScan.sessions)) + chalk21.dim(" Gemini)") : "";
14425
14652
  console.log(
14426
- " " + chalk21.white(num2(scan.sessions)) + chalk21.dim(" sessions ") + chalk21.white(num2(scan.totalToolCalls)) + chalk21.dim(" tool calls ") + chalk21.white(num2(scan.bashCalls)) + chalk21.dim(" bash commands ") + rangeLabel + dateRange
14653
+ " " + chalk21.white(num2(scan.sessions)) + chalk21.dim(" sessions ") + sessionBreakdown + (sessionBreakdown ? " " : "") + chalk21.white(num2(scan.totalToolCalls)) + chalk21.dim(" tool calls ") + chalk21.white(num2(scan.bashCalls)) + chalk21.dim(" bash commands ") + rangeLabel + dateRange
14427
14654
  );
14428
14655
  console.log("");
14429
14656
  const byShield = /* @__PURE__ */ new Map();
@@ -14475,8 +14702,9 @@ function registerScanCommand(program2) {
14475
14702
  for (const f of shown) {
14476
14703
  const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
14477
14704
  const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
14478
- const cmd = chalk21.gray(preview(f.input, 55));
14479
- console.log(` ${ts}${proj}${cmd}`);
14705
+ const agentBadge = f.agent === "gemini" ? chalk21.blue("[Gemini] ") : chalk21.cyan("[Claude] ");
14706
+ const cmd = chalk21.gray(preview(f.input, 45));
14707
+ console.log(` ${ts}${proj}${agentBadge}${cmd}`);
14480
14708
  }
14481
14709
  if (ruleFindings.length > topN) {
14482
14710
  console.log(
@@ -14500,8 +14728,9 @@ function registerScanCommand(program2) {
14500
14728
  for (const f of shownDlp) {
14501
14729
  const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
14502
14730
  const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
14731
+ const agentBadge = f.agent === "gemini" ? chalk21.blue("[Gemini] ") : chalk21.cyan("[Claude] ");
14503
14732
  console.log(
14504
- ` ${ts}${proj}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample)
14733
+ ` ${ts}${proj}${agentBadge}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample)
14505
14734
  );
14506
14735
  }
14507
14736
  if (scan.dlpFindings.length > topN) {
@@ -14516,7 +14745,7 @@ function registerScanCommand(program2) {
14516
14745
  }
14517
14746
  if (scan.totalCostUSD > 0) {
14518
14747
  console.log(
14519
- " " + chalk21.bold("Claude spend:") + " " + chalk21.yellow(fmtCost2(scan.totalCostUSD)) + chalk21.dim(" (for per-period breakdown: node9 report)")
14748
+ " " + chalk21.bold("Agent spend:") + " " + chalk21.yellow(fmtCost2(scan.totalCostUSD)) + chalk21.dim(" (for per-period breakdown: node9 report)")
14520
14749
  );
14521
14750
  console.log("");
14522
14751
  }
@@ -14560,6 +14789,22 @@ function modelPrice(model) {
14560
14789
  }
14561
14790
  return null;
14562
14791
  }
14792
+ var GEMINI_PRICING2 = {
14793
+ "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
14794
+ "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
14795
+ "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
14796
+ "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
14797
+ "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
14798
+ "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
14799
+ };
14800
+ function geminiModelPrice2(model) {
14801
+ const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
14802
+ for (const [key, p] of Object.entries(GEMINI_PRICING2)) {
14803
+ if (base === key || base.startsWith(key)) return p;
14804
+ }
14805
+ if (base.includes("flash")) return GEMINI_PRICING2["gemini-2.0-flash"];
14806
+ return null;
14807
+ }
14563
14808
  function encodeProjectPath(projectPath) {
14564
14809
  return projectPath.replace(/\//g, "-");
14565
14810
  }
@@ -14675,6 +14920,123 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
14675
14920
  }
14676
14921
  return result;
14677
14922
  }
14923
+ function buildGeminiSessions(days, allAuditEntries) {
14924
+ const tmpDir = path37.join(os30.homedir(), ".gemini", "tmp");
14925
+ if (!fs34.existsSync(tmpDir)) return [];
14926
+ const cutoff = days !== null ? (() => {
14927
+ const d = /* @__PURE__ */ new Date();
14928
+ d.setDate(d.getDate() - days);
14929
+ d.setHours(0, 0, 0, 0);
14930
+ return d;
14931
+ })() : null;
14932
+ let slugDirs;
14933
+ try {
14934
+ slugDirs = fs34.readdirSync(tmpDir);
14935
+ } catch {
14936
+ return [];
14937
+ }
14938
+ const summaries = [];
14939
+ for (const slug of slugDirs) {
14940
+ const slugPath = path37.join(tmpDir, slug);
14941
+ try {
14942
+ if (!fs34.statSync(slugPath).isDirectory()) continue;
14943
+ } catch {
14944
+ continue;
14945
+ }
14946
+ let projectRoot = path37.join(os30.homedir(), slug);
14947
+ try {
14948
+ projectRoot = fs34.readFileSync(path37.join(slugPath, ".project_root"), "utf-8").trim();
14949
+ } catch {
14950
+ }
14951
+ const chatsDir = path37.join(slugPath, "chats");
14952
+ if (!fs34.existsSync(chatsDir)) continue;
14953
+ let chatFiles;
14954
+ try {
14955
+ chatFiles = fs34.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
14956
+ } catch {
14957
+ continue;
14958
+ }
14959
+ for (const chatFile of chatFiles) {
14960
+ let raw;
14961
+ try {
14962
+ raw = fs34.readFileSync(path37.join(chatsDir, chatFile), "utf-8");
14963
+ } catch {
14964
+ continue;
14965
+ }
14966
+ let session;
14967
+ try {
14968
+ session = JSON.parse(raw);
14969
+ } catch {
14970
+ continue;
14971
+ }
14972
+ const startTime = session.startTime ?? "";
14973
+ if (!startTime) continue;
14974
+ if (cutoff && new Date(startTime) < cutoff) continue;
14975
+ let firstPrompt = "";
14976
+ for (const msg of session.messages ?? []) {
14977
+ if (msg.type === "user") {
14978
+ const content = msg.content;
14979
+ if (Array.isArray(content) && content[0]?.text) {
14980
+ firstPrompt = content[0].text;
14981
+ } else if (typeof content === "string") {
14982
+ firstPrompt = content;
14983
+ }
14984
+ break;
14985
+ }
14986
+ }
14987
+ const toolCalls = [];
14988
+ let costUSD = 0;
14989
+ const modifiedFiles = [];
14990
+ const seenFiles = /* @__PURE__ */ new Set();
14991
+ let lastToolTs = "";
14992
+ for (const msg of session.messages ?? []) {
14993
+ if (msg.type !== "gemini") continue;
14994
+ const tokens = msg.tokens;
14995
+ const model = msg.model;
14996
+ if (tokens && model) {
14997
+ const p = geminiModelPrice2(model);
14998
+ if (p) {
14999
+ const nonCached = Math.max(0, tokens.input - tokens.cached);
15000
+ costUSD += nonCached * p.i + tokens.cached * p.cr + tokens.output * p.o;
15001
+ }
15002
+ }
15003
+ for (const tc of msg.toolCalls ?? []) {
15004
+ const tool = tc.name ?? "";
15005
+ const input = tc.args ?? {};
15006
+ const ts = msg.timestamp ?? "";
15007
+ toolCalls.push({ tool, input, timestamp: ts });
15008
+ if (ts > lastToolTs) lastToolTs = ts;
15009
+ const toolLower = tool.toLowerCase();
15010
+ if (toolLower === "write_file" || toolLower === "edit_file" || toolLower === "create_file" || toolLower === "overwrite_file") {
15011
+ const fp = input.file_path ?? input.path ?? input.filename;
15012
+ if (typeof fp === "string" && !seenFiles.has(fp)) {
15013
+ seenFiles.add(fp);
15014
+ modifiedFiles.push(fp);
15015
+ }
15016
+ }
15017
+ }
15018
+ }
15019
+ const windowEnd = new Date(
15020
+ Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
15021
+ ).toISOString();
15022
+ const blockedCalls = auditEntriesInWindow(allAuditEntries, startTime, windowEnd);
15023
+ summaries.push({
15024
+ sessionId: session.sessionId ?? chatFile.replace(".json", ""),
15025
+ project: projectRoot,
15026
+ projectLabel: projectLabel(projectRoot),
15027
+ firstPrompt,
15028
+ startTime,
15029
+ toolCalls,
15030
+ blockedCalls,
15031
+ costUSD,
15032
+ hasSnapshot: false,
15033
+ modifiedFiles,
15034
+ agent: "gemini"
15035
+ });
15036
+ }
15037
+ }
15038
+ return summaries;
15039
+ }
14678
15040
  function buildSessions(days, historyPath) {
14679
15041
  const hPath = historyPath ?? path37.join(os30.homedir(), ".claude", "history.jsonl");
14680
15042
  let historyRaw;
@@ -14725,9 +15087,13 @@ function buildSessions(days, historyPath) {
14725
15087
  blockedCalls,
14726
15088
  costUSD,
14727
15089
  hasSnapshot,
14728
- modifiedFiles
15090
+ modifiedFiles,
15091
+ agent: "claude"
14729
15092
  });
14730
15093
  }
15094
+ if (!historyPath) {
15095
+ summaries.push(...buildGeminiSessions(days, allAuditEntries));
15096
+ }
14731
15097
  summaries.sort((a, b) => a.startTime > b.startTime ? -1 : 1);
14732
15098
  return summaries;
14733
15099
  }
@@ -14839,9 +15205,9 @@ function renderSummary(summaries) {
14839
15205
  const maxGroup = Math.max(...Object.values(groups));
14840
15206
  for (const [label, count] of Object.entries(groups)) {
14841
15207
  if (count === 0) continue;
14842
- const pct2 = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
15208
+ const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
14843
15209
  console.log(
14844
- " " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk22.white(String(count).padStart(4)) + chalk22.dim(` (${String(pct2)}%)`)
15210
+ " " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk22.white(String(count).padStart(4)) + chalk22.dim(` (${String(pct)}%)`)
14845
15211
  );
14846
15212
  }
14847
15213
  console.log("");
@@ -14883,8 +15249,9 @@ function renderList(summaries, totalCost) {
14883
15249
  const cost = s.costUSD > 0 ? chalk22.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
14884
15250
  const blocked = s.blockedCalls.length > 0 ? chalk22.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
14885
15251
  const snap = s.hasSnapshot ? chalk22.green(" \u{1F4F8}") : "";
15252
+ const agentBadge = s.agent === "gemini" ? chalk22.blue(" [Gemini]") : chalk22.cyan(" [Claude]");
14886
15253
  const sid = chalk22.dim(" " + s.sessionId.slice(0, 8));
14887
- console.log(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${sid}`);
15254
+ console.log(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}`);
14888
15255
  }
14889
15256
  console.log("");
14890
15257
  console.log(
@@ -14899,6 +15266,10 @@ function renderDetail(s) {
14899
15266
  chalk22.bold(" Prompt ") + chalk22.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
14900
15267
  );
14901
15268
  console.log(chalk22.bold(" Project ") + chalk22.white(s.projectLabel));
15269
+ if (s.agent) {
15270
+ const agentLabel2 = s.agent === "gemini" ? chalk22.blue("Gemini CLI") : chalk22.cyan("Claude Code");
15271
+ console.log(chalk22.bold(" Agent ") + agentLabel2);
15272
+ }
14902
15273
  console.log(chalk22.bold(" When ") + chalk22.white(fmtDateTime(s.startTime)));
14903
15274
  if (s.costUSD > 0)
14904
15275
  console.log(chalk22.bold(" Cost ") + chalk22.yellow("~" + fmtCost3(s.costUSD)));
package/dist/index.js CHANGED
@@ -116,7 +116,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
116
116
  }
117
117
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
118
118
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
119
- const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
119
+ const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
120
120
  appendToLog(LOCAL_AUDIT_LOG, {
121
121
  ts: (/* @__PURE__ */ new Date()).toISOString(),
122
122
  tool: toolName,
package/dist/index.mjs CHANGED
@@ -96,7 +96,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
96
96
  }
97
97
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
98
98
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
99
- const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
99
+ const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
100
100
  appendToLog(LOCAL_AUDIT_LOG, {
101
101
  ts: (/* @__PURE__ */ new Date()).toISOString(),
102
102
  tool: toolName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.11.1",
3
+ "version": "1.11.3",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",