@node9/proxy 1.19.4 → 1.20.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
@@ -99,12 +99,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
99
99
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
100
100
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
101
101
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
102
+ const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
102
103
  appendToLog(LOCAL_AUDIT_LOG, {
103
104
  ts: (/* @__PURE__ */ new Date()).toISOString(),
104
105
  tool: toolName,
105
106
  ...argsField,
106
107
  decision,
107
108
  checkedBy,
109
+ ...ruleNameField,
108
110
  ...testRun,
109
111
  agent: meta?.agent,
110
112
  mcpServer: meta?.mcpServer,
@@ -691,7 +693,12 @@ function analyzeFsOperationImpl(command) {
691
693
  for (const p of paths) {
692
694
  for (const sp of SENSITIVE_PATH_RULES) {
693
695
  if (sp.match(p)) {
694
- result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
696
+ result = {
697
+ ruleName: sp.rule,
698
+ verdict: sp.verdict ?? "block",
699
+ reason: sp.reason,
700
+ path: p
701
+ };
695
702
  return false;
696
703
  }
697
704
  }
@@ -2345,15 +2352,50 @@ var init_dist = __esm({
2345
2352
  match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
2346
2353
  },
2347
2354
  {
2355
+ // Mirrors the JSON shield's `.env` pattern (project-jail.json's
2356
+ // review-read-env-any-tool) so the AST FS-op path catches the
2357
+ // same set the regex shield does — including Next.js / Vite's
2358
+ // `.env.<env>.local` double-suffix overrides which are commonly
2359
+ // gitignored AND commonly contain real secrets.
2360
+ //
2361
+ // Intentional non-matches (dev fixtures): .env.example, .env.sample,
2362
+ // .env.template, .env.test, .envrc. See shields.test.ts:983-995
2363
+ // for the canonical test-asserted contract.
2348
2364
  rule: "shield:project-jail:block-read-env",
2349
2365
  reason: "Reading .env files is blocked by project-jail shield",
2350
- match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
2366
+ match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
2367
+ p
2368
+ )
2351
2369
  },
2352
2370
  {
2353
- rule: "shield:project-jail:block-read-credentials",
2354
- reason: "Reading credential files is blocked by project-jail shield",
2355
- match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
2356
- p
2371
+ // verdict: 'review' (not 'block') is a deliberate design choice
2372
+ // documented in commit 29327a8. SSH keys and AWS credentials are
2373
+ // cryptographic material with no legitimate read use-case for
2374
+ // an AI agent → hard `block`. But .netrc / .npmrc / .docker /
2375
+ // .kube / gcloud are CONFIG files that hold tokens AND have
2376
+ // legitimate diagnostic reads ("which registry am I configured
2377
+ // for", "what cluster am I on"). Hard-blocking those creates
2378
+ // friction without much safety win because the review gate
2379
+ // still catches genuine exfiltration attempts.
2380
+ //
2381
+ // The review gate FAILS CLOSED on timeout (daemon.approvalTimeoutMs
2382
+ // returns a deny verdict via the orchestrator's timeout branch),
2383
+ // so a stuck or unattended approval does NOT silently grant
2384
+ // credential access. If the threat model demands strict block,
2385
+ // a future per-shield strict-mode toggle is the right fix —
2386
+ // not a regex-level upgrade here.
2387
+ rule: "shield:project-jail:review-read-credentials",
2388
+ reason: "Reading credential files requires approval (project-jail shield)",
2389
+ verdict: "review",
2390
+ match: (p) => (
2391
+ // .kube/config holds Kubernetes cluster credentials and was
2392
+ // flagged as missing by the node9-pr-agent review (the comment
2393
+ // above mentioned .kube but the regex didn't include it — a
2394
+ // textbook code-comment vs code drift). The JSON shield's
2395
+ // review-read-credentials-any-tool already had it. Now aligned.
2396
+ /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials|\.kube[\\/]config)$/i.test(
2397
+ p
2398
+ )
2357
2399
  )
2358
2400
  }
2359
2401
  ];
@@ -2369,7 +2411,7 @@ var init_dist = __esm({
2369
2411
  "shield:project-jail:block-read-ssh",
2370
2412
  "shield:project-jail:block-read-aws",
2371
2413
  "shield:project-jail:block-read-env",
2372
- "shield:project-jail:block-read-credentials"
2414
+ "shield:project-jail:review-read-credentials"
2373
2415
  ]);
2374
2416
  FS_OP_CACHE_MAX = 5e3;
2375
2417
  fsOpCache = /* @__PURE__ */ new Map();
@@ -3057,7 +3099,7 @@ var init_dist = __esm({
3057
3099
  {
3058
3100
  field: "command",
3059
3101
  op: "matches",
3060
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
3102
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
3061
3103
  flags: "i"
3062
3104
  }
3063
3105
  ],
@@ -3071,7 +3113,7 @@ var init_dist = __esm({
3071
3113
  {
3072
3114
  field: "command",
3073
3115
  op: "matches",
3074
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
3116
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
3075
3117
  flags: "i"
3076
3118
  }
3077
3119
  ],
@@ -3093,7 +3135,7 @@ var init_dist = __esm({
3093
3135
  reason: "Reading .env files is blocked by project-jail shield"
3094
3136
  },
3095
3137
  {
3096
- name: "shield:project-jail:block-read-credentials",
3138
+ name: "shield:project-jail:review-read-credentials",
3097
3139
  tool: "bash",
3098
3140
  conditions: [
3099
3141
  {
@@ -3103,8 +3145,64 @@ var init_dist = __esm({
3103
3145
  flags: "i"
3104
3146
  }
3105
3147
  ],
3148
+ verdict: "review",
3149
+ reason: "Reading credential files requires approval (project-jail shield)"
3150
+ },
3151
+ {
3152
+ name: "shield:project-jail:block-read-ssh-any-tool",
3153
+ tool: "*",
3154
+ conditions: [
3155
+ {
3156
+ field: "file_path",
3157
+ op: "matches",
3158
+ value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
3159
+ flags: "i"
3160
+ }
3161
+ ],
3162
+ verdict: "block",
3163
+ reason: "Reading SSH private keys is blocked by project-jail shield"
3164
+ },
3165
+ {
3166
+ name: "shield:project-jail:block-read-aws-any-tool",
3167
+ tool: "*",
3168
+ conditions: [
3169
+ {
3170
+ field: "file_path",
3171
+ op: "matches",
3172
+ value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
3173
+ flags: "i"
3174
+ }
3175
+ ],
3106
3176
  verdict: "block",
3107
- reason: "Reading credential files is blocked by project-jail shield"
3177
+ reason: "Reading AWS credentials is blocked by project-jail shield"
3178
+ },
3179
+ {
3180
+ name: "shield:project-jail:review-read-env-any-tool",
3181
+ tool: "*",
3182
+ conditions: [
3183
+ {
3184
+ field: "file_path",
3185
+ op: "matches",
3186
+ value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
3187
+ flags: "i"
3188
+ }
3189
+ ],
3190
+ verdict: "review",
3191
+ reason: "Reading .env files requires approval (project-jail shield)"
3192
+ },
3193
+ {
3194
+ name: "shield:project-jail:review-read-credentials-any-tool",
3195
+ tool: "*",
3196
+ conditions: [
3197
+ {
3198
+ field: "file_path",
3199
+ op: "matches",
3200
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
3201
+ flags: "i"
3202
+ }
3203
+ ],
3204
+ verdict: "review",
3205
+ reason: "Reading credential files requires approval (project-jail shield)"
3108
3206
  }
3109
3207
  ],
3110
3208
  dangerousWords: []
@@ -5770,7 +5868,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5770
5868
  args,
5771
5869
  "deny",
5772
5870
  "smart-rule-block-override",
5773
- meta,
5871
+ // Same rationale as the smart-rule-block path above —
5872
+ // pass the specific rule name so [2] SHIELDS can
5873
+ // attribute this override-block to its owning shield.
5874
+ { ...meta, ruleName: policyResult.ruleName },
5774
5875
  hashAuditArgs
5775
5876
  );
5776
5877
  if (approvers.cloud && creds?.apiKey)
@@ -5800,7 +5901,20 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5800
5901
  }
5801
5902
  } else {
5802
5903
  if (!isManual)
5803
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
5904
+ appendLocalAudit(
5905
+ toolName,
5906
+ args,
5907
+ "deny",
5908
+ "smart-rule-block",
5909
+ // Include policyResult.ruleName so the [2] Report SHIELDS
5910
+ // panel can attribute this block to its specific shield
5911
+ // (e.g. `shield:project-jail:block-read-ssh`) via the
5912
+ // rule→shield map. checkedBy stays as the generic
5913
+ // `smart-rule-block` for backward compat with existing
5914
+ // log readers.
5915
+ { ...meta, ruleName: policyResult.ruleName },
5916
+ hashAuditArgs
5917
+ );
5804
5918
  if (approvers.cloud && creds?.apiKey)
5805
5919
  auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
5806
5920
  ruleName: policyResult.ruleName,
@@ -7621,9 +7735,61 @@ function computeLoopWaste(loops, totalToolCalls) {
7621
7735
  const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
7622
7736
  return { wastedCalls, wastePct };
7623
7737
  }
7738
+ function rollupByShield(sections, topRulesPerShield = 3) {
7739
+ const out = [];
7740
+ for (const section of sections) {
7741
+ if (section.sourceType !== "shield") continue;
7742
+ if (!section.shieldKey) continue;
7743
+ const totalCatches = section.blockedCount + section.reviewCount;
7744
+ const topRuleLabels = [...section.rules].sort((a, b) => b.findings.length - a.findings.length).slice(0, topRulesPerShield).map((r) => r.findings.length > 1 ? `${r.name} \xD7${r.findings.length}` : r.name);
7745
+ out.push({
7746
+ shieldName: section.shieldKey,
7747
+ totalCatches,
7748
+ blockCatches: section.blockedCount,
7749
+ reviewCatches: section.reviewCount,
7750
+ topRuleLabels
7751
+ });
7752
+ }
7753
+ return out.sort((a, b) => b.totalCatches - a.totalCatches);
7754
+ }
7755
+ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
7756
+ const inner = width - 4;
7757
+ const out = [];
7758
+ const titlePad = ` ${title} `;
7759
+ const titleSegment = titlePad.length <= inner ? titlePad : titlePad.slice(0, inner);
7760
+ const dashFill = "\u2500".repeat(Math.max(0, inner - titleSegment.length));
7761
+ out.push(chalk3.dim("\u256D\u2500") + chalk3.bold(titleSegment) + chalk3.dim(`${dashFill}\u2500\u256E`));
7762
+ for (const line of bodyLines) {
7763
+ const padding = " ".repeat(Math.max(0, inner - line.width));
7764
+ out.push(chalk3.dim("\u2502 ") + line.rendered + padding + chalk3.dim(" \u2502"));
7765
+ }
7766
+ out.push(chalk3.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
7767
+ return out;
7768
+ }
7769
+ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
7770
+ const t = new Date(timestamp).getTime();
7771
+ if (Number.isNaN(t)) return "?";
7772
+ const days = Math.floor((now.getTime() - t) / 864e5);
7773
+ if (days < 1) return "today";
7774
+ if (days > 90) return "90d+";
7775
+ return `${days}d`;
7776
+ }
7777
+ var PANEL_WIDTH;
7624
7778
  var init_scan_derive = __esm({
7625
7779
  "src/cli/render/scan-derive.ts"() {
7626
7780
  "use strict";
7781
+ PANEL_WIDTH = 76;
7782
+ }
7783
+ });
7784
+
7785
+ // src/protection.ts
7786
+ var PROTECTIVE_SHIELD_DISCOUNTS;
7787
+ var init_protection = __esm({
7788
+ "src/protection.ts"() {
7789
+ "use strict";
7790
+ PROTECTIVE_SHIELD_DISCOUNTS = {
7791
+ "project-jail": 0.7
7792
+ };
7627
7793
  }
7628
7794
  });
7629
7795
 
@@ -7813,6 +7979,7 @@ async function ensurePricingLoaded() {
7813
7979
  if (fromDisk && Object.keys(fromDisk).length > 0) {
7814
7980
  memCache = fromDisk;
7815
7981
  memCacheAt = Date.now();
7982
+ lookupCache.clear();
7816
7983
  return;
7817
7984
  }
7818
7985
  const fetched = await fetchLiteLLMPricing();
@@ -7820,30 +7987,42 @@ async function ensurePricingLoaded() {
7820
7987
  memCache = fetched;
7821
7988
  memCacheAt = Date.now();
7822
7989
  writeCache(fetched);
7990
+ lookupCache.clear();
7823
7991
  return;
7824
7992
  }
7825
7993
  memCache = { ...BUNDLED_PRICING };
7826
7994
  memCacheAt = Date.now();
7995
+ lookupCache.clear();
7827
7996
  }
7828
7997
  function pricingFor(model) {
7829
7998
  const norm = normalizeModel(model);
7999
+ const cached = lookupCache.get(norm);
8000
+ if (cached !== void 0) return cached;
7830
8001
  const sources = [];
7831
8002
  if (memCache) sources.push(memCache);
7832
8003
  sources.push(BUNDLED_PRICING);
8004
+ let resolved = null;
7833
8005
  for (const source of sources) {
7834
8006
  const exact = source[norm];
7835
- if (exact) return exact;
8007
+ if (exact) {
8008
+ resolved = exact;
8009
+ break;
8010
+ }
7836
8011
  let best = null;
7837
8012
  for (const key of Object.keys(source)) {
7838
8013
  if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
7839
8014
  best = key;
7840
8015
  }
7841
8016
  }
7842
- if (best) return source[best];
8017
+ if (best) {
8018
+ resolved = source[best];
8019
+ break;
8020
+ }
7843
8021
  }
7844
- return null;
8022
+ lookupCache.set(norm, resolved);
8023
+ return resolved;
7845
8024
  }
7846
- var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt;
8025
+ var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, lookupCache;
7847
8026
  var init_litellm = __esm({
7848
8027
  "src/pricing/litellm.ts"() {
7849
8028
  "use strict";
@@ -7877,6 +8056,7 @@ var init_litellm = __esm({
7877
8056
  TTL_MS = 24 * 60 * 60 * 1e3;
7878
8057
  memCache = null;
7879
8058
  memCacheAt = 0;
8059
+ lookupCache = /* @__PURE__ */ new Map();
7880
8060
  }
7881
8061
  });
7882
8062
 
@@ -7946,7 +8126,7 @@ function parseJSONLFile(filePath, fallbackWorkingDir) {
7946
8126
  }
7947
8127
  return daily;
7948
8128
  }
7949
- function collectEntries() {
8129
+ function collectEntries(sinceMs) {
7950
8130
  const projectsDir = path18.join(os15.homedir(), ".claude", "projects");
7951
8131
  if (!fs16.existsSync(projectsDir)) return [];
7952
8132
  const combined = /* @__PURE__ */ new Map();
@@ -7971,7 +8151,15 @@ function collectEntries() {
7971
8151
  }
7972
8152
  const fallbackWorkingDir = decodeProjectDirName(dir);
7973
8153
  for (const file of files) {
7974
- const entries = parseJSONLFile(path18.join(dirPath, file), fallbackWorkingDir);
8154
+ const filePath = path18.join(dirPath, file);
8155
+ if (sinceMs !== void 0) {
8156
+ try {
8157
+ if (fs16.statSync(filePath).mtimeMs < sinceMs) continue;
8158
+ } catch {
8159
+ continue;
8160
+ }
8161
+ }
8162
+ const entries = parseJSONLFile(filePath, fallbackWorkingDir);
7975
8163
  for (const [key, e] of entries) {
7976
8164
  const prev = combined.get(key);
7977
8165
  if (prev) {
@@ -8807,7 +8995,16 @@ function buildRecurringPatternSet(findings) {
8807
8995
  }
8808
8996
  return recurring;
8809
8997
  }
8810
- function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result) {
8998
+ function emptyScanDedup() {
8999
+ return { findingsKeys: /* @__PURE__ */ new Set(), dlpKeys: /* @__PURE__ */ new Set() };
9000
+ }
9001
+ function findingKey(ruleName, inputPreview, projLabel) {
9002
+ return `${ruleName ?? "<unnamed>"}|${inputPreview}|${projLabel}`;
9003
+ }
9004
+ function dlpKey(patternName, redactedSample, projLabel) {
9005
+ return `${patternName}|${redactedSample}|${projLabel}`;
9006
+ }
9007
+ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result, dedup) {
8811
9008
  const fsVerdict = analyzeFsOperation(command);
8812
9009
  if (!fsVerdict) return false;
8813
9010
  const synthRule = {
@@ -8830,10 +9027,9 @@ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sess
8830
9027
  rule: synthRule
8831
9028
  };
8832
9029
  const inputPreview = preview(input, 120);
8833
- const isDupe = result.findings.some(
8834
- (f) => f.source.rule.name === synthRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
8835
- );
8836
- if (!isDupe) {
9030
+ const k = findingKey(synthRule.name, inputPreview, projLabel);
9031
+ if (!dedup.findingsKeys.has(k)) {
9032
+ dedup.findingsKeys.add(k);
8837
9033
  result.findings.push({
8838
9034
  source: synthSource,
8839
9035
  toolName,
@@ -8918,22 +9114,15 @@ function buildRuleSources() {
8918
9114
  sources.push({ shieldName, shieldLabel: shieldName, sourceType: "shield", rule });
8919
9115
  }
8920
9116
  }
8921
- try {
8922
- const config = getConfig();
8923
- for (const rule of config.policy.smartRules) {
8924
- if (!rule.name) continue;
8925
- if (rule.name.startsWith("shield:")) continue;
8926
- const isCloud = rule.name.startsWith("cloud:");
8927
- const isDefault = DEFAULT_RULE_NAMES.has(rule.name);
8928
- const sourceType = isCloud ? "user" : isDefault ? "default" : "user";
8929
- sources.push({
8930
- shieldName: isCloud ? "cloud" : isDefault ? "default" : "custom",
8931
- shieldLabel: isCloud ? "Cloud Policy" : isDefault ? "Default Rules" : "Your Rules",
8932
- sourceType,
8933
- rule
8934
- });
8935
- }
8936
- } catch {
9117
+ for (const rule of DEFAULT_CONFIG.policy.smartRules) {
9118
+ if (!rule.name) continue;
9119
+ if (rule.name.startsWith("shield:")) continue;
9120
+ sources.push({
9121
+ shieldName: "default",
9122
+ shieldLabel: "Default Rules",
9123
+ sourceType: "default",
9124
+ rule
9125
+ });
8937
9126
  }
8938
9127
  return sources;
8939
9128
  }
@@ -9019,178 +9208,53 @@ function renderProgressBar(done, total, lines) {
9019
9208
  `\r ${chalk5.cyan("Scanning")} [${chalk5.cyan(bar)}] ${chalk5.dim(fileLabel)}${lineLabel} `
9020
9209
  );
9021
9210
  }
9022
- function scanClaudeHistory(startDate, onProgress, onLine) {
9023
- const projectsDir = path21.join(os18.homedir(), ".claude", "projects");
9024
- const result = {
9025
- filesScanned: 0,
9026
- sessions: 0,
9027
- totalToolCalls: 0,
9028
- bashCalls: 0,
9029
- findings: [],
9030
- dlpFindings: [],
9031
- loopFindings: [],
9032
- totalCostUSD: 0,
9033
- firstDate: null,
9034
- lastDate: null,
9035
- sessionsWithEarlySecrets: 0
9036
- };
9037
- if (!fs19.existsSync(projectsDir)) return result;
9038
- let projDirs;
9211
+ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, result, dedup, onProgress, onLine) {
9212
+ result.filesScanned++;
9213
+ result.sessions++;
9214
+ onProgress?.(result.filesScanned);
9215
+ const sessionId = file.replace(/\.jsonl$/, "");
9216
+ let raw;
9039
9217
  try {
9040
- projDirs = fs19.readdirSync(projectsDir);
9218
+ raw = fs19.readFileSync(path21.join(projPath, file), "utf-8");
9041
9219
  } catch {
9042
- return result;
9220
+ return;
9043
9221
  }
9044
- const ruleSources = buildRuleSources();
9045
- for (const proj of projDirs) {
9046
- const projPath = path21.join(projectsDir, proj);
9222
+ const sessionCalls = [];
9223
+ const toolUseFilePaths = /* @__PURE__ */ new Map();
9224
+ let firstDlpTs = null;
9225
+ let firstEditTs = null;
9226
+ for (const line of raw.split("\n")) {
9227
+ if (!line.trim()) continue;
9228
+ onLine?.();
9229
+ let entry;
9047
9230
  try {
9048
- if (!fs19.statSync(projPath).isDirectory()) continue;
9231
+ entry = JSON.parse(line);
9049
9232
  } catch {
9050
9233
  continue;
9051
9234
  }
9052
- const projLabel = stripTerminalEscapes(
9053
- decodeURIComponent(proj).replace(os18.homedir(), "~")
9054
- ).slice(0, 40);
9055
- let files;
9056
- try {
9057
- files = fs19.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
9058
- } catch {
9059
- continue;
9235
+ if (entry.type !== "assistant" && entry.type !== "user") continue;
9236
+ if (startDate && entry.timestamp) {
9237
+ if (new Date(entry.timestamp) < startDate) continue;
9060
9238
  }
9061
- for (const file of files) {
9062
- result.filesScanned++;
9063
- result.sessions++;
9064
- onProgress?.(result.filesScanned);
9065
- const sessionId = file.replace(/\.jsonl$/, "");
9066
- let raw;
9067
- try {
9068
- raw = fs19.readFileSync(path21.join(projPath, file), "utf-8");
9069
- } catch {
9070
- continue;
9071
- }
9072
- const sessionCalls = [];
9073
- const toolUseFilePaths = /* @__PURE__ */ new Map();
9074
- let firstDlpTs = null;
9075
- let firstEditTs = null;
9076
- for (const line of raw.split("\n")) {
9077
- if (!line.trim()) continue;
9078
- onLine?.();
9079
- let entry;
9080
- try {
9081
- entry = JSON.parse(line);
9082
- } catch {
9083
- continue;
9084
- }
9085
- if (entry.type !== "assistant" && entry.type !== "user") continue;
9086
- if (startDate && entry.timestamp) {
9087
- if (new Date(entry.timestamp) < startDate) continue;
9088
- }
9089
- if (entry.timestamp) {
9090
- if (!result.firstDate || entry.timestamp < result.firstDate)
9091
- result.firstDate = entry.timestamp;
9092
- if (!result.lastDate || entry.timestamp > result.lastDate)
9093
- result.lastDate = entry.timestamp;
9094
- }
9095
- if (entry.type === "user") {
9096
- const content2 = entry.message?.content;
9097
- if (Array.isArray(content2)) {
9098
- const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
9099
- if (text) {
9100
- const dlpMatch = scanArgs({ text });
9101
- if (dlpMatch) {
9102
- const isDupe = result.dlpFindings.some(
9103
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9104
- );
9105
- if (!isDupe) {
9106
- result.dlpFindings.push({
9107
- patternName: dlpMatch.patternName,
9108
- redactedSample: dlpMatch.redactedSample,
9109
- toolName: "user-prompt",
9110
- timestamp: entry.timestamp ?? "",
9111
- project: projLabel,
9112
- sessionId,
9113
- agent: "claude"
9114
- });
9115
- }
9116
- }
9117
- }
9118
- for (const block of content2) {
9119
- if (block.type !== "tool_result") continue;
9120
- const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
9121
- if (filePath) {
9122
- const ext = path21.extname(filePath).toLowerCase();
9123
- if (CODE_EXTENSIONS.has(ext)) continue;
9124
- }
9125
- const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
9126
- if (!resultText) continue;
9127
- if (isNode9SelfOutput(resultText)) continue;
9128
- const dlpMatch = scanArgs({ text: resultText });
9129
- if (dlpMatch) {
9130
- if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
9131
- if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9132
- const isDupe = result.dlpFindings.some(
9133
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9134
- );
9135
- if (!isDupe) {
9136
- result.dlpFindings.push({
9137
- patternName: dlpMatch.patternName,
9138
- redactedSample: dlpMatch.redactedSample,
9139
- toolName: "tool-result",
9140
- timestamp: entry.timestamp ?? "",
9141
- project: projLabel,
9142
- sessionId,
9143
- agent: "claude"
9144
- });
9145
- }
9146
- }
9147
- }
9148
- }
9149
- continue;
9150
- }
9151
- const usage = entry.message?.usage;
9152
- const model = entry.message?.model;
9153
- if (usage && model) {
9154
- const p = claudeModelPrice(model);
9155
- if (p) {
9156
- result.totalCostUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
9157
- }
9158
- }
9159
- const content = entry.message?.content;
9160
- if (!Array.isArray(content)) continue;
9161
- for (const block of content) {
9162
- if (block.type !== "tool_use") continue;
9163
- result.totalToolCalls++;
9164
- const toolName = block.name ?? "";
9165
- const toolNameLower = toolName.toLowerCase();
9166
- const input = block.input ?? {};
9167
- if (block.id && typeof input.file_path === "string") {
9168
- toolUseFilePaths.set(block.id, input.file_path);
9169
- }
9170
- sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
9171
- if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
9172
- result.bashCalls++;
9173
- }
9174
- if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
9175
- firstEditTs = entry.timestamp ?? null;
9176
- }
9177
- const rawCmd = String(input.command ?? "").trimStart();
9178
- if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
9179
- continue;
9180
- const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
9181
- const inputFileExt = inputFilePath ? path21.extname(inputFilePath).toLowerCase() : "";
9182
- if (CODE_EXTENSIONS.has(inputFileExt)) continue;
9183
- const dlpMatch = scanArgs(input);
9239
+ if (entry.timestamp) {
9240
+ if (!result.firstDate || entry.timestamp < result.firstDate)
9241
+ result.firstDate = entry.timestamp;
9242
+ if (!result.lastDate || entry.timestamp > result.lastDate) result.lastDate = entry.timestamp;
9243
+ }
9244
+ if (entry.type === "user") {
9245
+ const content2 = entry.message?.content;
9246
+ if (Array.isArray(content2)) {
9247
+ const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
9248
+ if (text) {
9249
+ const dlpMatch = scanArgs({ text });
9184
9250
  if (dlpMatch) {
9185
- if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9186
- const isDupe = result.dlpFindings.some(
9187
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9188
- );
9189
- if (!isDupe) {
9251
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9252
+ if (!dedup.dlpKeys.has(k)) {
9253
+ dedup.dlpKeys.add(k);
9190
9254
  result.dlpFindings.push({
9191
9255
  patternName: dlpMatch.patternName,
9192
9256
  redactedSample: dlpMatch.redactedSample,
9193
- toolName,
9257
+ toolName: "user-prompt",
9194
9258
  timestamp: entry.timestamp ?? "",
9195
9259
  project: projLabel,
9196
9260
  sessionId,
@@ -9198,102 +9262,252 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
9198
9262
  });
9199
9263
  }
9200
9264
  }
9201
- let astFsMatched = false;
9202
- const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
9203
- if (astRanForBash) {
9204
- astFsMatched = pushFsOpAstFinding(
9205
- String(input.command ?? ""),
9206
- toolName,
9207
- input,
9208
- entry.timestamp ?? "",
9209
- projLabel,
9210
- sessionId,
9211
- "claude",
9212
- result
9213
- );
9265
+ }
9266
+ for (const block of content2) {
9267
+ if (block.type !== "tool_result") continue;
9268
+ const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
9269
+ if (filePath) {
9270
+ const ext = path21.extname(filePath).toLowerCase();
9271
+ if (CODE_EXTENSIONS.has(ext)) continue;
9214
9272
  }
9215
- let ruleMatched = astFsMatched;
9216
- for (const source of ruleSources) {
9217
- const { rule } = source;
9218
- if (rule.verdict === "allow") continue;
9219
- if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
9220
- if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9221
- if (!evaluateSmartConditions(input, rule)) continue;
9222
- const inputPreview = preview(input, 120);
9223
- const isDupe = result.findings.some(
9224
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9225
- );
9226
- if (!isDupe) {
9227
- result.findings.push({
9228
- source,
9229
- toolName,
9230
- input,
9273
+ const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
9274
+ if (!resultText) continue;
9275
+ if (isNode9SelfOutput(resultText)) continue;
9276
+ const dlpMatch = scanArgs({ text: resultText });
9277
+ if (dlpMatch) {
9278
+ if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
9279
+ if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9280
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9281
+ if (!dedup.dlpKeys.has(k)) {
9282
+ dedup.dlpKeys.add(k);
9283
+ result.dlpFindings.push({
9284
+ patternName: dlpMatch.patternName,
9285
+ redactedSample: dlpMatch.redactedSample,
9286
+ toolName: "tool-result",
9231
9287
  timestamp: entry.timestamp ?? "",
9232
9288
  project: projLabel,
9233
9289
  sessionId,
9234
9290
  agent: "claude"
9235
9291
  });
9236
9292
  }
9237
- ruleMatched = true;
9238
- break;
9239
- }
9240
- if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
9241
- const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
9242
- if (shellVerdict) {
9243
- const astRule = {
9244
- name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
9245
- tool: "bash",
9246
- conditions: [],
9247
- verdict: shellVerdict,
9248
- reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9249
- };
9250
- const inputPreview = preview(input, 120);
9251
- const isDupe = result.findings.some(
9252
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9253
- );
9254
- if (!isDupe) {
9255
- result.findings.push({
9256
- source: {
9257
- shieldName: "bash-safe",
9258
- shieldLabel: "bash-safe (AST)",
9259
- sourceType: "shield",
9260
- rule: astRule
9261
- },
9262
- toolName,
9263
- input,
9264
- timestamp: entry.timestamp ?? "",
9265
- project: projLabel,
9266
- sessionId,
9267
- agent: "claude"
9268
- });
9269
- }
9270
- }
9271
9293
  }
9272
9294
  }
9273
9295
  }
9274
- result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "claude"));
9275
- if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
9276
- result.sessionsWithEarlySecrets++;
9296
+ continue;
9297
+ }
9298
+ const usage = entry.message?.usage;
9299
+ const model = entry.message?.model;
9300
+ if (usage && model) {
9301
+ const p = claudeModelPrice(model);
9302
+ if (p) {
9303
+ result.totalCostUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
9277
9304
  }
9278
9305
  }
9279
- }
9280
- return result;
9281
- }
9282
- function scanGeminiHistory(startDate, onProgress, onLine) {
9283
- const tmpDir = path21.join(os18.homedir(), ".gemini", "tmp");
9284
- const result = {
9285
- filesScanned: 0,
9286
- sessions: 0,
9287
- totalToolCalls: 0,
9288
- bashCalls: 0,
9289
- findings: [],
9290
- dlpFindings: [],
9306
+ const content = entry.message?.content;
9307
+ if (!Array.isArray(content)) continue;
9308
+ for (const block of content) {
9309
+ if (block.type !== "tool_use") continue;
9310
+ result.totalToolCalls++;
9311
+ const toolName = block.name ?? "";
9312
+ const toolNameLower = toolName.toLowerCase();
9313
+ const input = block.input ?? {};
9314
+ if (block.id && typeof input.file_path === "string") {
9315
+ toolUseFilePaths.set(block.id, input.file_path);
9316
+ }
9317
+ sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
9318
+ if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
9319
+ result.bashCalls++;
9320
+ }
9321
+ if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
9322
+ firstEditTs = entry.timestamp ?? null;
9323
+ }
9324
+ const rawCmd = String(input.command ?? "").trimStart();
9325
+ if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
9326
+ const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
9327
+ const inputFileExt = inputFilePath ? path21.extname(inputFilePath).toLowerCase() : "";
9328
+ if (CODE_EXTENSIONS.has(inputFileExt)) continue;
9329
+ const dlpMatch = scanArgs(input);
9330
+ if (dlpMatch) {
9331
+ if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9332
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9333
+ if (!dedup.dlpKeys.has(k)) {
9334
+ dedup.dlpKeys.add(k);
9335
+ result.dlpFindings.push({
9336
+ patternName: dlpMatch.patternName,
9337
+ redactedSample: dlpMatch.redactedSample,
9338
+ toolName,
9339
+ timestamp: entry.timestamp ?? "",
9340
+ project: projLabel,
9341
+ sessionId,
9342
+ agent: "claude"
9343
+ });
9344
+ }
9345
+ }
9346
+ let astFsMatched = false;
9347
+ const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
9348
+ if (astRanForBash) {
9349
+ astFsMatched = pushFsOpAstFinding(
9350
+ String(input.command ?? ""),
9351
+ toolName,
9352
+ input,
9353
+ entry.timestamp ?? "",
9354
+ projLabel,
9355
+ sessionId,
9356
+ "claude",
9357
+ result,
9358
+ dedup
9359
+ );
9360
+ }
9361
+ let ruleMatched = astFsMatched;
9362
+ for (const source of ruleSources) {
9363
+ const { rule } = source;
9364
+ if (rule.verdict === "allow") continue;
9365
+ if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
9366
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9367
+ if (!evaluateSmartConditions(input, rule)) continue;
9368
+ const inputPreview = preview(input, 120);
9369
+ const k = findingKey(rule.name, inputPreview, projLabel);
9370
+ if (!dedup.findingsKeys.has(k)) {
9371
+ dedup.findingsKeys.add(k);
9372
+ result.findings.push({
9373
+ source,
9374
+ toolName,
9375
+ input,
9376
+ timestamp: entry.timestamp ?? "",
9377
+ project: projLabel,
9378
+ sessionId,
9379
+ agent: "claude"
9380
+ });
9381
+ }
9382
+ ruleMatched = true;
9383
+ break;
9384
+ }
9385
+ if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
9386
+ const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
9387
+ if (shellVerdict) {
9388
+ const astRule = {
9389
+ name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
9390
+ tool: "bash",
9391
+ conditions: [],
9392
+ verdict: shellVerdict,
9393
+ reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9394
+ };
9395
+ const inputPreview = preview(input, 120);
9396
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9397
+ if (!dedup.findingsKeys.has(k)) {
9398
+ dedup.findingsKeys.add(k);
9399
+ result.findings.push({
9400
+ source: {
9401
+ shieldName: "bash-safe",
9402
+ shieldLabel: "bash-safe (AST)",
9403
+ sourceType: "shield",
9404
+ rule: astRule
9405
+ },
9406
+ toolName,
9407
+ input,
9408
+ timestamp: entry.timestamp ?? "",
9409
+ project: projLabel,
9410
+ sessionId,
9411
+ agent: "claude"
9412
+ });
9413
+ }
9414
+ }
9415
+ }
9416
+ }
9417
+ }
9418
+ result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "claude"));
9419
+ if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
9420
+ result.sessionsWithEarlySecrets++;
9421
+ }
9422
+ }
9423
+ function processClaudeProject(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
9424
+ const projPath = path21.join(projectsDir, proj);
9425
+ try {
9426
+ if (!fs19.statSync(projPath).isDirectory()) return;
9427
+ } catch {
9428
+ return;
9429
+ }
9430
+ const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os18.homedir(), "~")).slice(
9431
+ 0,
9432
+ 40
9433
+ );
9434
+ let files;
9435
+ try {
9436
+ files = fs19.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
9437
+ } catch {
9438
+ return;
9439
+ }
9440
+ for (const file of files) {
9441
+ processClaudeFile(
9442
+ file,
9443
+ projPath,
9444
+ projLabel,
9445
+ ruleSources,
9446
+ startDate,
9447
+ result,
9448
+ dedup,
9449
+ onProgress,
9450
+ onLine
9451
+ );
9452
+ }
9453
+ }
9454
+ function emptyClaudeScan() {
9455
+ return {
9456
+ filesScanned: 0,
9457
+ sessions: 0,
9458
+ totalToolCalls: 0,
9459
+ bashCalls: 0,
9460
+ findings: [],
9461
+ dlpFindings: [],
9462
+ loopFindings: [],
9463
+ totalCostUSD: 0,
9464
+ firstDate: null,
9465
+ lastDate: null,
9466
+ sessionsWithEarlySecrets: 0
9467
+ };
9468
+ }
9469
+ function scanClaudeHistory(startDate, onProgress, onLine) {
9470
+ const projectsDir = path21.join(os18.homedir(), ".claude", "projects");
9471
+ const result = emptyClaudeScan();
9472
+ if (!fs19.existsSync(projectsDir)) return result;
9473
+ let projDirs;
9474
+ try {
9475
+ projDirs = fs19.readdirSync(projectsDir);
9476
+ } catch {
9477
+ return result;
9478
+ }
9479
+ const ruleSources = buildRuleSources();
9480
+ const dedup = emptyScanDedup();
9481
+ for (const proj of projDirs) {
9482
+ processClaudeProject(
9483
+ proj,
9484
+ projectsDir,
9485
+ ruleSources,
9486
+ startDate,
9487
+ result,
9488
+ dedup,
9489
+ onProgress,
9490
+ onLine
9491
+ );
9492
+ }
9493
+ return result;
9494
+ }
9495
+ function scanGeminiHistory(startDate, onProgress, onLine) {
9496
+ const tmpDir = path21.join(os18.homedir(), ".gemini", "tmp");
9497
+ const result = {
9498
+ filesScanned: 0,
9499
+ sessions: 0,
9500
+ totalToolCalls: 0,
9501
+ bashCalls: 0,
9502
+ findings: [],
9503
+ dlpFindings: [],
9291
9504
  loopFindings: [],
9292
9505
  totalCostUSD: 0,
9293
9506
  firstDate: null,
9294
9507
  lastDate: null,
9295
9508
  sessionsWithEarlySecrets: 0
9296
9509
  };
9510
+ const dedup = emptyScanDedup();
9297
9511
  if (!fs19.existsSync(tmpDir)) return result;
9298
9512
  let slugDirs;
9299
9513
  try {
@@ -9350,10 +9564,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9350
9564
  if (text) {
9351
9565
  const dlpMatch = scanArgs({ text });
9352
9566
  if (dlpMatch) {
9353
- const isDupe = result.dlpFindings.some(
9354
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9355
- );
9356
- if (!isDupe) {
9567
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9568
+ if (!dedup.dlpKeys.has(k)) {
9569
+ dedup.dlpKeys.add(k);
9357
9570
  result.dlpFindings.push({
9358
9571
  patternName: dlpMatch.patternName,
9359
9572
  redactedSample: dlpMatch.redactedSample,
@@ -9398,10 +9611,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9398
9611
  continue;
9399
9612
  const dlpMatch = scanArgs(input);
9400
9613
  if (dlpMatch) {
9401
- const isDupe = result.dlpFindings.some(
9402
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9403
- );
9404
- if (!isDupe) {
9614
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9615
+ if (!dedup.dlpKeys.has(k)) {
9616
+ dedup.dlpKeys.add(k);
9405
9617
  result.dlpFindings.push({
9406
9618
  patternName: dlpMatch.patternName,
9407
9619
  redactedSample: dlpMatch.redactedSample,
@@ -9424,7 +9636,8 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9424
9636
  projLabel,
9425
9637
  sessionId,
9426
9638
  "gemini",
9427
- result
9639
+ result,
9640
+ dedup
9428
9641
  );
9429
9642
  }
9430
9643
  let ruleMatched = astFsMatched;
@@ -9435,10 +9648,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9435
9648
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9436
9649
  if (!evaluateSmartConditions(input, rule)) continue;
9437
9650
  const inputPreview = preview(input, 120);
9438
- const isDupe = result.findings.some(
9439
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9440
- );
9441
- if (!isDupe) {
9651
+ const k = findingKey(rule.name, inputPreview, projLabel);
9652
+ if (!dedup.findingsKeys.has(k)) {
9653
+ dedup.findingsKeys.add(k);
9442
9654
  result.findings.push({
9443
9655
  source,
9444
9656
  toolName,
@@ -9466,10 +9678,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9466
9678
  reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9467
9679
  };
9468
9680
  const inputPreview = preview(input, 120);
9469
- const isDupe = result.findings.some(
9470
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9471
- );
9472
- if (!isDupe) {
9681
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9682
+ if (!dedup.findingsKeys.has(k)) {
9683
+ dedup.findingsKeys.add(k);
9473
9684
  result.findings.push({
9474
9685
  source: {
9475
9686
  shieldName: "bash-safe",
@@ -9509,6 +9720,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9509
9720
  lastDate: null,
9510
9721
  sessionsWithEarlySecrets: 0
9511
9722
  };
9723
+ const dedup = emptyScanDedup();
9512
9724
  if (!fs19.existsSync(sessionsBase)) return result;
9513
9725
  const jsonlFiles = [];
9514
9726
  try {
@@ -9590,10 +9802,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9590
9802
  if (text) {
9591
9803
  const dlpMatch2 = scanArgs({ text });
9592
9804
  if (dlpMatch2) {
9593
- const isDupe = result.dlpFindings.some(
9594
- (f) => f.patternName === dlpMatch2.patternName && f.redactedSample === dlpMatch2.redactedSample && f.project === projLabel
9595
- );
9596
- if (!isDupe) {
9805
+ const k = dlpKey(dlpMatch2.patternName, dlpMatch2.redactedSample, projLabel);
9806
+ if (!dedup.dlpKeys.has(k)) {
9807
+ dedup.dlpKeys.add(k);
9597
9808
  result.dlpFindings.push({
9598
9809
  patternName: dlpMatch2.patternName,
9599
9810
  redactedSample: dlpMatch2.redactedSample,
@@ -9635,10 +9846,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9635
9846
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
9636
9847
  const dlpMatch = scanArgs(input);
9637
9848
  if (dlpMatch) {
9638
- const isDupe = result.dlpFindings.some(
9639
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9640
- );
9641
- if (!isDupe) {
9849
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9850
+ if (!dedup.dlpKeys.has(k)) {
9851
+ dedup.dlpKeys.add(k);
9642
9852
  result.dlpFindings.push({
9643
9853
  patternName: dlpMatch.patternName,
9644
9854
  redactedSample: dlpMatch.redactedSample,
@@ -9661,7 +9871,8 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9661
9871
  projLabel,
9662
9872
  sessionId,
9663
9873
  "codex",
9664
- result
9874
+ result,
9875
+ dedup
9665
9876
  );
9666
9877
  }
9667
9878
  let ruleMatched = astFsMatched;
@@ -9673,10 +9884,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9673
9884
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9674
9885
  if (!evaluateSmartConditions(input, rule)) continue;
9675
9886
  const inputPreview = preview(input, 120);
9676
- const isDupe = result.findings.some(
9677
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9678
- );
9679
- if (!isDupe) {
9887
+ const k = findingKey(rule.name, inputPreview, projLabel);
9888
+ if (!dedup.findingsKeys.has(k)) {
9889
+ dedup.findingsKeys.add(k);
9680
9890
  result.findings.push({
9681
9891
  source,
9682
9892
  toolName,
@@ -9701,10 +9911,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9701
9911
  reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9702
9912
  };
9703
9913
  const inputPreview = preview(input, 120);
9704
- const isDupe = result.findings.some(
9705
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9706
- );
9707
- if (!isDupe) {
9914
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9915
+ if (!dedup.findingsKeys.has(k)) {
9916
+ dedup.findingsKeys.add(k);
9708
9917
  result.findings.push({
9709
9918
  source: {
9710
9919
  shieldName: "bash-safe",
@@ -9735,6 +9944,7 @@ function scanShellConfig() {
9735
9944
  (f) => path21.join(home, f)
9736
9945
  );
9737
9946
  const findings = [];
9947
+ const seen = /* @__PURE__ */ new Set();
9738
9948
  for (const filePath of configFiles) {
9739
9949
  if (!fs19.existsSync(filePath)) continue;
9740
9950
  let lines;
@@ -9749,10 +9959,9 @@ function scanShellConfig() {
9749
9959
  if (!trimmed || trimmed.startsWith("#")) continue;
9750
9960
  const dlpMatch = scanArgs({ text: trimmed });
9751
9961
  if (!dlpMatch) continue;
9752
- const isDupe = findings.some(
9753
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === shortPath
9754
- );
9755
- if (!isDupe) {
9962
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, shortPath);
9963
+ if (!seen.has(k)) {
9964
+ seen.add(k);
9756
9965
  findings.push({
9757
9966
  patternName: dlpMatch.patternName,
9758
9967
  redactedSample: dlpMatch.redactedSample,
@@ -10011,6 +10220,263 @@ function renderNarrativeScorecard(input) {
10011
10220
  console.log(chalk5.dim("\u2192 github.com/node9-ai/node9-proxy"));
10012
10221
  console.log("");
10013
10222
  }
10223
+ function mkLine(...parts) {
10224
+ let rendered = "";
10225
+ let width = 0;
10226
+ for (const [text, fmt] of parts) {
10227
+ rendered += fmt ? fmt(text) : text;
10228
+ width += text.length;
10229
+ }
10230
+ return { rendered, width };
10231
+ }
10232
+ function shortRule(name, width) {
10233
+ const stripped = name.replace(/^shield:[^:]+:/, "");
10234
+ if (stripped.length <= width) return stripped.padEnd(width);
10235
+ return stripped.slice(0, width - 1) + "\u2026";
10236
+ }
10237
+ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
10238
+ const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
10239
+ const topLines = [];
10240
+ if (scan.dlpFindings.length > 0) {
10241
+ const latest = scan.dlpFindings[0];
10242
+ const rel = relativeDate(latest.timestamp, now);
10243
+ const noun = `credential leak${scan.dlpFindings.length !== 1 ? "s" : ""}`;
10244
+ topLines.push(
10245
+ mkLine(
10246
+ ["\u{1F6A8} ", chalk5.red],
10247
+ [`${scan.dlpFindings.length} ${noun} in tool input `, chalk5.bold],
10248
+ [`(latest: ${rel} ago, ${latest.patternName})`, chalk5.dim]
10249
+ )
10250
+ );
10251
+ }
10252
+ if (blockedCount > 0) {
10253
+ const topBlocked = topRulesByVerdict(summary.sections, "block", 2).map(
10254
+ (r) => r.count > 1 ? `${shortRule(r.name, 20).trimEnd()} \xD7${r.count}` : shortRule(r.name, 20).trimEnd()
10255
+ ).join(", ");
10256
+ topLines.push(
10257
+ mkLine(
10258
+ ["\u{1F6D1} ", chalk5.red],
10259
+ [`${blockedCount} ops node9 would have blocked `, chalk5.bold],
10260
+ [`(${topBlocked})`, chalk5.dim]
10261
+ )
10262
+ );
10263
+ }
10264
+ if (scan.loopFindings.length > 0) {
10265
+ const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
10266
+ const byTool = /* @__PURE__ */ new Map();
10267
+ for (const f of scan.loopFindings) {
10268
+ byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + Math.max(0, f.count - 1));
10269
+ }
10270
+ const top = [...byTool.entries()].sort((a, b) => b[1] - a[1])[0];
10271
+ const wasteSuffix = wastePct > 0 ? `, ${wastePct}% wasted` : "";
10272
+ const detail = top ? `(${top[0]} dominates${wasteSuffix})` : "";
10273
+ topLines.push(
10274
+ mkLine(
10275
+ ["\u{1F501} ", chalk5.yellow],
10276
+ [`${scan.loopFindings.length} agent loops detected `, chalk5.bold],
10277
+ [detail, chalk5.dim]
10278
+ )
10279
+ );
10280
+ }
10281
+ if (blastExposures > 0) {
10282
+ const exposed2 = Math.max(0, 100 - blast.score);
10283
+ const pjDiscount = PROTECTIVE_SHIELD_DISCOUNTS["project-jail"] ?? 0;
10284
+ const pjBonus = Math.round(exposed2 * pjDiscount);
10285
+ const cta = pjBonus > 0 ? ` \u2192 enable project-jail (+${pjBonus} pts)` : "";
10286
+ topLines.push(
10287
+ mkLine(
10288
+ ["\u{1F52D} ", chalk5.red],
10289
+ [`${blastExposures} secrets reachable on disk`, chalk5.bold],
10290
+ [cta, chalk5.dim]
10291
+ )
10292
+ );
10293
+ }
10294
+ if (topLines.length > 0) {
10295
+ for (const ln of boxPanel("TOP FINDINGS", topLines)) console.log(" " + ln);
10296
+ console.log("");
10297
+ }
10298
+ if (summary.leaks.length > 0) {
10299
+ const leakLines = [];
10300
+ for (const leak of summary.leaks.slice(0, 5)) {
10301
+ const rel = relativeDate(leak.timestamp, now);
10302
+ leakLines.push(
10303
+ mkLine(
10304
+ [rel.padStart(4) + " ", chalk5.dim],
10305
+ [leak.patternName.padEnd(14), chalk5.red.bold],
10306
+ [" "],
10307
+ [leak.redactedSample.padEnd(20), chalk5.red],
10308
+ [" "],
10309
+ [`[${leak.toolName}]`.padEnd(15), chalk5.dim],
10310
+ [" "],
10311
+ [leak.agent, chalk5.dim]
10312
+ )
10313
+ );
10314
+ }
10315
+ const remaining = summary.leaks.length - 5;
10316
+ if (remaining > 0) {
10317
+ leakLines.push(mkLine([`\u2026 +${remaining} more`, chalk5.dim]));
10318
+ }
10319
+ const title = `LEAKS \xB7 ${summary.leaks.length} secret${summary.leaks.length !== 1 ? "s" : ""} in plain text`;
10320
+ for (const ln of boxPanel(title, leakLines)) console.log(" " + ln);
10321
+ console.log("");
10322
+ }
10323
+ if (blockedCount > 0) {
10324
+ const blockedLines = [];
10325
+ const ruleEntries = topRulesByVerdict(summary.sections, "block", 12);
10326
+ for (const r of ruleEntries) {
10327
+ const origin = originForRule(r.name, summary.sections);
10328
+ blockedLines.push(
10329
+ mkLine(
10330
+ ["\u2717 ", chalk5.red],
10331
+ [shortRule(r.name, 24), chalk5.bold],
10332
+ [" \xD7" + String(r.count).padEnd(4), chalk5.bold],
10333
+ [" "],
10334
+ [origin, chalk5.dim]
10335
+ )
10336
+ );
10337
+ }
10338
+ const title = `BLOCKED \xB7 ${blockedCount} ops node9 would have stopped`;
10339
+ for (const ln of boxPanel(title, blockedLines)) console.log(" " + ln);
10340
+ console.log("");
10341
+ }
10342
+ if (reviewCount > 0) {
10343
+ const reviewLines = [];
10344
+ const ruleEntries = topRulesByVerdict(summary.sections, "review", 12);
10345
+ for (const r of ruleEntries) {
10346
+ const origin = originForRule(r.name, summary.sections);
10347
+ reviewLines.push(
10348
+ mkLine(
10349
+ ["\u{1F441} ", chalk5.yellow],
10350
+ [shortRule(r.name, 24), chalk5.bold],
10351
+ [" \xD7" + String(r.count).padEnd(4), chalk5.bold],
10352
+ [" "],
10353
+ [origin, chalk5.dim]
10354
+ )
10355
+ );
10356
+ }
10357
+ const title = `REVIEW QUEUE \xB7 ${reviewCount} ops flagged for approval`;
10358
+ for (const ln of boxPanel(title, reviewLines)) console.log(" " + ln);
10359
+ console.log("");
10360
+ }
10361
+ if (scan.loopFindings.length > 0) {
10362
+ const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
10363
+ const byTool = /* @__PURE__ */ new Map();
10364
+ let totalRepeats = 0;
10365
+ for (const f of scan.loopFindings) {
10366
+ const repeats = Math.max(0, f.count - 1);
10367
+ byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
10368
+ totalRepeats += repeats;
10369
+ }
10370
+ const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
10371
+ const loopLines = [];
10372
+ for (const [tool, repeats] of toolEntries) {
10373
+ const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
10374
+ loopLines.push(
10375
+ mkLine(
10376
+ [tool.padEnd(10), chalk5.bold],
10377
+ [`\xD7${num(repeats)} repeats`.padEnd(16)],
10378
+ [`(${pct}%)`, chalk5.dim]
10379
+ )
10380
+ );
10381
+ }
10382
+ const topStuck = [...scan.loopFindings].sort((a, b) => b.count - a.count).slice(0, 3);
10383
+ if (topStuck.length > 0) {
10384
+ loopLines.push(mkLine([""]));
10385
+ loopLines.push(mkLine(["Top stuck patterns:", chalk5.dim]));
10386
+ for (const f of topStuck) {
10387
+ const raw = f.commandPreview || f.toolName;
10388
+ const target = raw.length > 60 ? "\u2026" + raw.slice(raw.length - 59) : raw.padEnd(60);
10389
+ loopLines.push(mkLine([`\xD7${num(f.count).padEnd(4)} `, chalk5.bold], [target, chalk5.dim]));
10390
+ }
10391
+ }
10392
+ const wasteSuffix = wastePct > 0 ? ` \xB7 ${wastePct}% wasted` : "";
10393
+ const title = `AGENT LOOPS \xB7 ${scan.loopFindings.length} repeated patterns${wasteSuffix}`;
10394
+ for (const ln of boxPanel(title, loopLines)) console.log(" " + ln);
10395
+ console.log("");
10396
+ }
10397
+ if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
10398
+ const blastLines = [];
10399
+ const DESC_W = 33;
10400
+ for (const r of blast.reachable.slice(0, 8)) {
10401
+ const trimmed = r.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
10402
+ const desc = trimmed.length > DESC_W ? trimmed.slice(0, DESC_W - 1) + "\u2026" : trimmed;
10403
+ blastLines.push(mkLine(["\u2717 ", chalk5.red], [r.label.padEnd(36)], [desc, chalk5.dim]));
10404
+ }
10405
+ for (const e of blast.envFindings.slice(0, 3)) {
10406
+ blastLines.push(
10407
+ mkLine(["\u26A0 ", chalk5.yellow], [`${e.key} `], [`(${e.patternName})`, chalk5.dim])
10408
+ );
10409
+ }
10410
+ const totalExposed = blast.reachable.length + blast.envFindings.length;
10411
+ if (totalExposed > 8) {
10412
+ blastLines.push(mkLine([`\u2026 +${totalExposed - 8} more`, chalk5.dim]));
10413
+ }
10414
+ const title = `BLAST RADIUS \xB7 ${totalExposed} path${totalExposed !== 1 ? "s" : ""} reachable right now`;
10415
+ for (const ln of boxPanel(title, blastLines)) console.log(" " + ln);
10416
+ console.log("");
10417
+ }
10418
+ const shieldImpacts = rollupByShield(summary.sections);
10419
+ const exposed = Math.max(0, 100 - blast.score);
10420
+ const shieldLines = [];
10421
+ const ranked = [...shieldImpacts].sort((a, b) => {
10422
+ const aDiscount = PROTECTIVE_SHIELD_DISCOUNTS[a.shieldName] ?? 0;
10423
+ const bDiscount = PROTECTIVE_SHIELD_DISCOUNTS[b.shieldName] ?? 0;
10424
+ if (aDiscount !== bDiscount) return bDiscount - aDiscount;
10425
+ return b.totalCatches - a.totalCatches;
10426
+ });
10427
+ for (const impact of ranked) {
10428
+ if (impact.totalCatches === 0) continue;
10429
+ const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
10430
+ const bonus = Math.round(exposed * discount);
10431
+ const icon = discount > 0 ? "\u{1F6E1} " : "\u2610 ";
10432
+ const wouldCatch = `would catch ${impact.totalCatches} op${impact.totalCatches !== 1 ? "s" : ""}`;
10433
+ const deltaSuffix = bonus > 0 ? ` \u2192 +${bonus} pts (${blast.score} \u2192 ${blast.score + bonus})` : "";
10434
+ shieldLines.push(
10435
+ mkLine(
10436
+ [icon, discount > 0 ? chalk5.cyan : chalk5.dim],
10437
+ [impact.shieldName.padEnd(14), chalk5.bold],
10438
+ [wouldCatch.padEnd(22), chalk5.dim],
10439
+ [deltaSuffix, bonus > 0 ? chalk5.green.bold : chalk5.dim]
10440
+ )
10441
+ );
10442
+ if (impact.topRuleLabels.length > 0) {
10443
+ const rules = impact.topRuleLabels.join(", ");
10444
+ shieldLines.push(mkLine([" ", chalk5.dim], [rules, chalk5.dim]));
10445
+ }
10446
+ }
10447
+ const hitShieldSet = new Set(
10448
+ shieldImpacts.filter((i) => i.totalCatches > 0).map((i) => i.shieldName)
10449
+ );
10450
+ const zeroHitBuiltins = Object.keys(SHIELDS).filter((name) => !hitShieldSet.has(name)).sort();
10451
+ if (zeroHitBuiltins.length > 0) {
10452
+ shieldLines.push(mkLine([""]));
10453
+ shieldLines.push(mkLine([zeroHitBuiltins.join(" \xB7 "), chalk5.dim]));
10454
+ shieldLines.push(mkLine([" no hits in your history \u2014 install proactively", chalk5.dim]));
10455
+ }
10456
+ const topRec = ranked.find(
10457
+ (r) => r.totalCatches > 0 && (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0
10458
+ );
10459
+ if (topRec) {
10460
+ const bonus = Math.round(exposed * (PROTECTIVE_SHIELD_DISCOUNTS[topRec.shieldName] ?? 0));
10461
+ const cta = `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 +${bonus} pts)`;
10462
+ shieldLines.push(mkLine([""]));
10463
+ shieldLines.push(mkLine([cta, chalk5.cyan]));
10464
+ }
10465
+ if (shieldLines.length > 0) {
10466
+ const title = "SHIELDS \xB7 install node9 + enable these to catch what we found";
10467
+ for (const ln of boxPanel(title, shieldLines)) console.log(" " + ln);
10468
+ console.log("");
10469
+ }
10470
+ }
10471
+ function originForRule(ruleName, sections) {
10472
+ for (const section of sections) {
10473
+ if (section.rules.some((r) => r.name === ruleName)) {
10474
+ if (section.sourceType === "default") return "default";
10475
+ if (section.sourceType === "shield") return `needs shield:${section.shieldKey ?? section.id}`;
10476
+ }
10477
+ }
10478
+ return "";
10479
+ }
10014
10480
  function registerScanCommand(program2) {
10015
10481
  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 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").option(
10016
10482
  "--json",
@@ -10242,7 +10708,7 @@ function registerScanCommand(program2) {
10242
10708
  " " + chalk5.dim("AI spend ") + chalk5.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? chalk5.dim(" \xB7 wasted on loops ") + chalk5.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
10243
10709
  );
10244
10710
  }
10245
- if (scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
10711
+ if (drillDown && scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
10246
10712
  console.log(
10247
10713
  " " + chalk5.dim(
10248
10714
  `${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
@@ -10250,6 +10716,26 @@ function registerScanCommand(program2) {
10250
10716
  );
10251
10717
  }
10252
10718
  console.log("");
10719
+ if (!drillDown) {
10720
+ renderPanelScorecard({
10721
+ scan,
10722
+ summary,
10723
+ blast,
10724
+ blastExposures,
10725
+ blockedCount,
10726
+ reviewCount
10727
+ });
10728
+ const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
10729
+ console.log(" " + chalk5.green(cta));
10730
+ console.log(
10731
+ " " + chalk5.dim("\u2192 ") + chalk5.cyan("node9 monitor") + chalk5.dim(" live dashboard")
10732
+ );
10733
+ console.log(
10734
+ " " + chalk5.dim("\u2192 ") + chalk5.cyan("node9 scan --drill-down") + chalk5.dim(" full commands + session IDs")
10735
+ );
10736
+ console.log("");
10737
+ return;
10738
+ }
10253
10739
  if (scan.dlpFindings.length > 0) {
10254
10740
  console.log(" " + chalk5.dim("\u2500".repeat(70)));
10255
10741
  console.log(
@@ -10438,7 +10924,7 @@ function registerScanCommand(program2) {
10438
10924
  }
10439
10925
  );
10440
10926
  }
10441
- var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, DEFAULT_RULE_NAMES, classifyRuleSeverity2, narrativeRuleLabel2;
10927
+ var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
10442
10928
  var init_scan = __esm({
10443
10929
  "src/cli/commands/scan.ts"() {
10444
10930
  "use strict";
@@ -10452,6 +10938,7 @@ var init_scan = __esm({
10452
10938
  init_setup();
10453
10939
  init_blast();
10454
10940
  init_scan_derive();
10941
+ init_protection();
10455
10942
  init_scan_json();
10456
10943
  init_scan_history();
10457
10944
  CLAUDE_PRICING = {
@@ -10534,9 +11021,6 @@ var init_scan = __esm({
10534
11021
  STUCK_TOOLS_LIMIT = 3;
10535
11022
  RECURRING_SESSION_THRESHOLD = 3;
10536
11023
  STALE_AGE_DAYS = 30;
10537
- DEFAULT_RULE_NAMES = new Set(
10538
- DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
10539
- );
10540
11024
  classifyRuleSeverity2 = classifyRuleSeverity;
10541
11025
  narrativeRuleLabel2 = narrativeRuleLabel;
10542
11026
  }
@@ -13107,6 +13591,7 @@ var tail_exports = {};
13107
13591
  __export(tail_exports, {
13108
13592
  agentLabel: () => agentLabel,
13109
13593
  sessionTag: () => sessionTag,
13594
+ shortenPathSummary: () => shortenPathSummary,
13110
13595
  startTail: () => startTail
13111
13596
  });
13112
13597
  import http2 from "http";
@@ -13116,6 +13601,12 @@ import os41 from "os";
13116
13601
  import path47 from "path";
13117
13602
  import readline6 from "readline";
13118
13603
  import { spawn as spawn9 } from "child_process";
13604
+ function shortenPathSummary(s) {
13605
+ if (!s || !s.startsWith("/")) return s;
13606
+ const parts = s.split("/").filter(Boolean);
13607
+ if (parts.length <= 2) return s;
13608
+ return `\u2026/${parts.slice(-2).join("/")}`;
13609
+ }
13119
13610
  function getIcon(tool) {
13120
13611
  const t = tool.toLowerCase();
13121
13612
  for (const [k, v] of Object.entries(ICONS)) {
@@ -13869,7 +14360,8 @@ async function startTail(options = {}) {
13869
14360
  if (event === "snapshot") {
13870
14361
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
13871
14362
  const hash = data.hash ?? "";
13872
- const summary = data.argsSummary ?? data.tool;
14363
+ const rawSummary = data.argsSummary ?? data.tool;
14364
+ const summary = shortenPathSummary(rawSummary);
13873
14365
  const fileCount = data.fileCount ?? 0;
13874
14366
  const files = fileCount > 0 ? chalk30.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
13875
14367
  process.stdout.write(
@@ -16251,63 +16743,13 @@ function registerAuditCommand(program2) {
16251
16743
 
16252
16744
  // src/cli/commands/report.ts
16253
16745
  import chalk13 from "chalk";
16746
+
16747
+ // src/cli/aggregate/report-audit.ts
16748
+ init_costSync();
16749
+ init_litellm();
16254
16750
  import fs35 from "fs";
16255
- import path36 from "path";
16256
16751
  import os31 from "os";
16257
-
16258
- // src/cli/render/report-json.ts
16259
- function buildReportJson(input) {
16260
- const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
16261
- const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
16262
- const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
16263
- return {
16264
- schemaVersion: 1,
16265
- generatedAt: input.generatedAt,
16266
- period: input.period,
16267
- range: { start: input.start.toISOString(), end: input.end.toISOString() },
16268
- excludedTests: input.excludedTests,
16269
- totals: {
16270
- events: input.total,
16271
- blocked: totalBlocked,
16272
- blockRate,
16273
- userApproved: input.userApproved,
16274
- userDenied: input.userDenied,
16275
- timedOut: input.timedOut,
16276
- hardBlocked: input.hardBlocked,
16277
- dlpBlocked: input.dlpBlocked,
16278
- observeDlp: input.observeDlp,
16279
- loopHits: input.loopHits,
16280
- unackedDlp: input.unackedDlp
16281
- },
16282
- tests: {
16283
- passes: input.testPasses,
16284
- fails: input.testFails
16285
- },
16286
- cost: {
16287
- totalUSD: input.cost.claudeUSD + input.cost.codexUSD,
16288
- claudeUSD: input.cost.claudeUSD,
16289
- codexUSD: input.cost.codexUSD,
16290
- inputTokens: input.cost.inputTokens,
16291
- outputTokens: input.cost.outputTokens,
16292
- cacheWriteTokens: input.cost.cacheWriteTokens,
16293
- cacheReadTokens: input.cost.cacheReadTokens,
16294
- byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
16295
- byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
16296
- },
16297
- byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
16298
- byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
16299
- byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
16300
- byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
16301
- byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
16302
- byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
16303
- trend: {
16304
- priorBlockRate: input.priorBlockRate,
16305
- deltaPct
16306
- }
16307
- };
16308
- }
16309
-
16310
- // src/cli/commands/report.ts
16752
+ import path36 from "path";
16311
16753
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
16312
16754
  function buildTestTimestamps(allEntries) {
16313
16755
  const testTs = /* @__PURE__ */ new Set();
@@ -16332,8 +16774,7 @@ function isTestEntry(entry, testTs) {
16332
16774
  }
16333
16775
  return false;
16334
16776
  }
16335
- function getDateRange(period) {
16336
- const now = /* @__PURE__ */ new Date();
16777
+ function getDateRange(period, now) {
16337
16778
  const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
16338
16779
  const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
16339
16780
  switch (period) {
@@ -16349,6 +16790,11 @@ function getDateRange(period) {
16349
16790
  s.setDate(s.getDate() - 29);
16350
16791
  return { start: s, end };
16351
16792
  }
16793
+ case "90d": {
16794
+ const s = new Date(todayStart);
16795
+ s.setDate(s.getDate() - 89);
16796
+ return { start: s, end };
16797
+ }
16352
16798
  case "month":
16353
16799
  return { start: new Date(now.getFullYear(), now.getMonth(), 1), end };
16354
16800
  }
@@ -16371,40 +16817,6 @@ function isAllow(decision) {
16371
16817
  function isDlp(checkedBy) {
16372
16818
  return !!checkedBy?.includes("dlp");
16373
16819
  }
16374
- var BLOCK_REASON_LABELS = {
16375
- timeout: "Popup timeout",
16376
- "smart-rule-block": "Smart rule",
16377
- "observe-mode-dlp-would-block": "DLP (observe)",
16378
- "persistent-deny": "Persistent deny",
16379
- "local-decision": "User denied",
16380
- "dlp-block": "DLP block",
16381
- "loop-detected": "Loop detected"
16382
- };
16383
- function humanBlockReason(reason) {
16384
- return BLOCK_REASON_LABELS[reason] ?? reason;
16385
- }
16386
- function barStr(value, max, width) {
16387
- if (max === 0 || width <= 0) return "\u2591".repeat(width);
16388
- const filled = Math.max(1, Math.round(value / max * width));
16389
- return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
16390
- }
16391
- function colorBar(value, max, width) {
16392
- const s = barStr(value, max, width);
16393
- const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
16394
- return chalk13.cyan(s.slice(0, filled)) + chalk13.dim(s.slice(filled));
16395
- }
16396
- function fmtDate(d) {
16397
- const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
16398
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
16399
- }
16400
- function num2(n) {
16401
- return n.toLocaleString();
16402
- }
16403
- function fmtCost2(usd) {
16404
- if (usd < 1e-3) return "< $0.001";
16405
- if (usd < 1) return "$" + usd.toFixed(4);
16406
- return "$" + usd.toFixed(2);
16407
- }
16408
16820
  var CLAUDE_PRICING2 = {
16409
16821
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
16410
16822
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -16424,90 +16836,160 @@ function claudeModelPrice2(model) {
16424
16836
  }
16425
16837
  return null;
16426
16838
  }
16427
- function loadClaudeCost(start, end) {
16428
- const empty = {
16839
+ function emptyClaudeCostAccumulator() {
16840
+ return {
16429
16841
  total: 0,
16430
- byDay: /* @__PURE__ */ new Map(),
16431
- byModel: /* @__PURE__ */ new Map(),
16432
16842
  inputTokens: 0,
16433
16843
  outputTokens: 0,
16434
16844
  cacheWriteTokens: 0,
16435
- cacheReadTokens: 0
16845
+ cacheReadTokens: 0,
16846
+ byDay: /* @__PURE__ */ new Map(),
16847
+ byModel: /* @__PURE__ */ new Map(),
16848
+ byProject: /* @__PURE__ */ new Map()
16436
16849
  };
16437
- const projectsDir = path36.join(os31.homedir(), ".claude", "projects");
16438
- if (!fs35.existsSync(projectsDir)) return empty;
16850
+ }
16851
+ function freezeClaudeCost(acc) {
16852
+ return {
16853
+ total: acc.total,
16854
+ byDay: acc.byDay,
16855
+ byModel: acc.byModel,
16856
+ byProject: acc.byProject,
16857
+ inputTokens: acc.inputTokens,
16858
+ outputTokens: acc.outputTokens,
16859
+ cacheWriteTokens: acc.cacheWriteTokens,
16860
+ cacheReadTokens: acc.cacheReadTokens
16861
+ };
16862
+ }
16863
+ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
16864
+ const projPath = path36.join(projectsDir, proj);
16865
+ let files;
16866
+ try {
16867
+ const stat = fs35.statSync(projPath);
16868
+ if (!stat.isDirectory()) return;
16869
+ files = fs35.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
16870
+ } catch {
16871
+ return;
16872
+ }
16873
+ const startMs = start.getTime();
16874
+ for (const file of files) {
16875
+ const filePath = path36.join(projPath, file);
16876
+ try {
16877
+ if (fs35.statSync(filePath).mtimeMs < startMs) continue;
16878
+ } catch {
16879
+ continue;
16880
+ }
16881
+ try {
16882
+ const raw = fs35.readFileSync(filePath, "utf-8");
16883
+ for (const line of raw.split("\n")) {
16884
+ if (!line.trim()) continue;
16885
+ let entry;
16886
+ try {
16887
+ entry = JSON.parse(line);
16888
+ } catch {
16889
+ continue;
16890
+ }
16891
+ if (entry.type !== "assistant") continue;
16892
+ if (!entry.timestamp) continue;
16893
+ const ts = new Date(entry.timestamp);
16894
+ if (ts < start || ts > end) continue;
16895
+ const usage = entry.message?.usage;
16896
+ const model = entry.message?.model;
16897
+ if (!usage || !model) continue;
16898
+ const p = claudeModelPrice2(model);
16899
+ if (!p) continue;
16900
+ const inp = usage.input_tokens ?? 0;
16901
+ const out = usage.output_tokens ?? 0;
16902
+ const cw = usage.cache_creation_input_tokens ?? 0;
16903
+ const cr = usage.cache_read_input_tokens ?? 0;
16904
+ const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
16905
+ acc.total += cost;
16906
+ acc.inputTokens += inp;
16907
+ acc.outputTokens += out;
16908
+ acc.cacheWriteTokens += cw;
16909
+ acc.cacheReadTokens += cr;
16910
+ const dateKey = entry.timestamp.slice(0, 10);
16911
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
16912
+ const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
16913
+ acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
16914
+ const projectKey = decodeProjectDirName(proj);
16915
+ const projectRollup = acc.byProject.get(projectKey) ?? {
16916
+ cost: 0,
16917
+ inputTokens: 0,
16918
+ outputTokens: 0
16919
+ };
16920
+ projectRollup.cost += cost;
16921
+ projectRollup.inputTokens += inp;
16922
+ projectRollup.outputTokens += out;
16923
+ acc.byProject.set(projectKey, projectRollup);
16924
+ }
16925
+ } catch {
16926
+ continue;
16927
+ }
16928
+ }
16929
+ }
16930
+ function loadClaudeCost(start, end, projectsDir) {
16931
+ const acc = emptyClaudeCostAccumulator();
16932
+ if (!fs35.existsSync(projectsDir)) return freezeClaudeCost(acc);
16439
16933
  let dirs;
16440
16934
  try {
16441
16935
  dirs = fs35.readdirSync(projectsDir);
16442
16936
  } catch {
16443
- return empty;
16937
+ return freezeClaudeCost(acc);
16444
16938
  }
16445
- let total = 0;
16446
- let inputTokens = 0;
16447
- let outputTokens = 0;
16448
- let cacheWriteTokens = 0;
16449
- let cacheReadTokens = 0;
16450
- const byDay = /* @__PURE__ */ new Map();
16451
- const byModel = /* @__PURE__ */ new Map();
16452
16939
  for (const proj of dirs) {
16453
- const projPath = path36.join(projectsDir, proj);
16454
- let files;
16940
+ processClaudeCostProject(proj, projectsDir, start, end, acc);
16941
+ }
16942
+ return freezeClaudeCost(acc);
16943
+ }
16944
+ function processCodexCostFile(filePath, start, end, acc) {
16945
+ let lines;
16946
+ try {
16947
+ lines = fs35.readFileSync(filePath, "utf-8").split("\n");
16948
+ } catch {
16949
+ return;
16950
+ }
16951
+ let sessionStart2 = "";
16952
+ let lastTotalInput = 0;
16953
+ let lastTotalCached = 0;
16954
+ let lastTotalOutput = 0;
16955
+ let sessionToolCalls = 0;
16956
+ for (const line of lines) {
16957
+ if (!line.trim()) continue;
16958
+ let entry;
16455
16959
  try {
16456
- const stat = fs35.statSync(projPath);
16457
- if (!stat.isDirectory()) continue;
16458
- files = fs35.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
16960
+ entry = JSON.parse(line);
16459
16961
  } catch {
16460
16962
  continue;
16461
16963
  }
16462
- for (const file of files) {
16463
- try {
16464
- const raw = fs35.readFileSync(path36.join(projPath, file), "utf-8");
16465
- for (const line of raw.split("\n")) {
16466
- if (!line.trim()) continue;
16467
- let entry;
16468
- try {
16469
- entry = JSON.parse(line);
16470
- } catch {
16471
- continue;
16472
- }
16473
- if (entry.type !== "assistant") continue;
16474
- if (!entry.timestamp) continue;
16475
- const ts = new Date(entry.timestamp);
16476
- if (ts < start || ts > end) continue;
16477
- const usage = entry.message?.usage;
16478
- const model = entry.message?.model;
16479
- if (!usage || !model) continue;
16480
- const p = claudeModelPrice2(model);
16481
- if (!p) continue;
16482
- const inp = usage.input_tokens ?? 0;
16483
- const out = usage.output_tokens ?? 0;
16484
- const cw = usage.cache_creation_input_tokens ?? 0;
16485
- const cr = usage.cache_read_input_tokens ?? 0;
16486
- const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
16487
- total += cost;
16488
- inputTokens += inp;
16489
- outputTokens += out;
16490
- cacheWriteTokens += cw;
16491
- cacheReadTokens += cr;
16492
- const dateKey = entry.timestamp.slice(0, 10);
16493
- byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
16494
- const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
16495
- byModel.set(normModel, (byModel.get(normModel) ?? 0) + cost);
16496
- }
16497
- } catch {
16498
- continue;
16499
- }
16964
+ const p = entry.payload ?? {};
16965
+ if (entry.type === "session_meta") {
16966
+ sessionStart2 = String(p["timestamp"] ?? "");
16967
+ continue;
16968
+ }
16969
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
16970
+ const info = p["info"] ?? {};
16971
+ const usage = info["total_token_usage"] ?? {};
16972
+ lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
16973
+ lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
16974
+ lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
16975
+ }
16976
+ if (entry.type === "response_item" && p["type"] === "function_call") {
16977
+ sessionToolCalls++;
16500
16978
  }
16501
16979
  }
16502
- return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
16980
+ if (!sessionStart2) return;
16981
+ const ts = new Date(sessionStart2);
16982
+ if (ts < start || ts > end) return;
16983
+ const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
16984
+ const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
16985
+ acc.total += cost;
16986
+ acc.toolCalls += sessionToolCalls;
16987
+ const dateKey = sessionStart2.slice(0, 10);
16988
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
16503
16989
  }
16504
- function loadCodexCost(start, end) {
16505
- const sessionsBase = path36.join(os31.homedir(), ".codex", "sessions");
16506
- const byDay = /* @__PURE__ */ new Map();
16507
- let total = 0;
16508
- let toolCalls = 0;
16509
- if (!fs35.existsSync(sessionsBase)) return { total, byDay, toolCalls };
16990
+ function listCodexSessionFiles(sessionsBase) {
16510
16991
  const jsonlFiles = [];
16992
+ if (!fs35.existsSync(sessionsBase)) return jsonlFiles;
16511
16993
  try {
16512
16994
  for (const year of fs35.readdirSync(sessionsBase)) {
16513
16995
  const yearPath = path36.join(sessionsBase, year);
@@ -16537,495 +17019,742 @@ function loadCodexCost(start, end) {
16537
17019
  }
16538
17020
  }
16539
17021
  } catch {
16540
- return { total, byDay, toolCalls };
17022
+ return [];
16541
17023
  }
16542
- for (const filePath of jsonlFiles) {
16543
- let lines;
17024
+ return jsonlFiles;
17025
+ }
17026
+ function loadCodexCost(start, end, sessionsBase) {
17027
+ const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
17028
+ const files = listCodexSessionFiles(sessionsBase);
17029
+ for (const filePath of files) {
17030
+ processCodexCostFile(filePath, start, end, acc);
17031
+ }
17032
+ return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
17033
+ }
17034
+ var GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
17035
+ function geminiPriceFor(model) {
17036
+ let tuple = pricingFor(model);
17037
+ if (!tuple && /^gemini-/i.test(model)) {
17038
+ for (const proxy of GEMINI_FALLBACK_MODELS) {
17039
+ tuple = pricingFor(proxy);
17040
+ if (tuple) break;
17041
+ }
17042
+ }
17043
+ if (!tuple) return null;
17044
+ return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
17045
+ }
17046
+ function emptyGeminiAccumulator() {
17047
+ return {
17048
+ total: 0,
17049
+ inputTokens: 0,
17050
+ outputTokens: 0,
17051
+ cacheReadTokens: 0,
17052
+ byDay: /* @__PURE__ */ new Map(),
17053
+ byProject: /* @__PURE__ */ new Map()
17054
+ };
17055
+ }
17056
+ function freezeGeminiCost(acc) {
17057
+ return {
17058
+ total: acc.total,
17059
+ byDay: acc.byDay,
17060
+ byProject: acc.byProject,
17061
+ inputTokens: acc.inputTokens,
17062
+ outputTokens: acc.outputTokens,
17063
+ cacheReadTokens: acc.cacheReadTokens
17064
+ };
17065
+ }
17066
+ function processGeminiCostFile(filePath, projectKey, start, end, acc) {
17067
+ const startMs = start.getTime();
17068
+ try {
17069
+ if (fs35.statSync(filePath).mtimeMs < startMs) return;
17070
+ } catch {
17071
+ return;
17072
+ }
17073
+ let raw;
17074
+ try {
17075
+ raw = fs35.readFileSync(filePath, "utf-8");
17076
+ } catch {
17077
+ return;
17078
+ }
17079
+ const seenIds = /* @__PURE__ */ new Set();
17080
+ for (const line of raw.split("\n")) {
17081
+ if (!line.trim()) continue;
17082
+ let entry;
16544
17083
  try {
16545
- lines = fs35.readFileSync(filePath, "utf-8").split("\n");
17084
+ entry = JSON.parse(line);
16546
17085
  } catch {
16547
17086
  continue;
16548
17087
  }
16549
- let sessionStart2 = "";
16550
- let lastTotalInput = 0;
16551
- let lastTotalCached = 0;
16552
- let lastTotalOutput = 0;
16553
- let sessionToolCalls = 0;
16554
- for (const line of lines) {
16555
- if (!line.trim()) continue;
16556
- let entry;
16557
- try {
16558
- entry = JSON.parse(line);
16559
- } catch {
16560
- continue;
16561
- }
16562
- const p = entry.payload ?? {};
16563
- if (entry.type === "session_meta") {
16564
- sessionStart2 = String(p["timestamp"] ?? "");
16565
- continue;
16566
- }
16567
- if (entry.type === "event_msg" && p["type"] === "token_count") {
16568
- const info = p["info"] ?? {};
16569
- const usage = info["total_token_usage"] ?? {};
16570
- lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
16571
- lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
16572
- lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
16573
- }
16574
- if (entry.type === "response_item" && p["type"] === "function_call") {
16575
- sessionToolCalls++;
16576
- }
17088
+ if (entry.type !== "gemini") continue;
17089
+ if (!entry.tokens || !entry.model || !entry.timestamp) continue;
17090
+ if (entry.id) {
17091
+ if (seenIds.has(entry.id)) continue;
17092
+ seenIds.add(entry.id);
16577
17093
  }
16578
- if (!sessionStart2) continue;
16579
- const ts = new Date(sessionStart2);
17094
+ const ts = new Date(entry.timestamp);
16580
17095
  if (ts < start || ts > end) continue;
16581
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
16582
- const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
16583
- total += cost;
16584
- toolCalls += sessionToolCalls;
16585
- const dateKey = sessionStart2.slice(0, 10);
16586
- byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
17096
+ const price = geminiPriceFor(entry.model);
17097
+ if (!price) continue;
17098
+ const inp = entry.tokens.input ?? 0;
17099
+ const out = entry.tokens.output ?? 0;
17100
+ const cached = Math.min(entry.tokens.cached ?? 0, inp);
17101
+ const fresh = Math.max(0, inp - cached);
17102
+ const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
17103
+ acc.total += cost;
17104
+ acc.inputTokens += inp;
17105
+ acc.outputTokens += out;
17106
+ acc.cacheReadTokens += cached;
17107
+ const dateKey = entry.timestamp.slice(0, 10);
17108
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
17109
+ const rollup = acc.byProject.get(projectKey) ?? {
17110
+ cost: 0,
17111
+ inputTokens: 0,
17112
+ outputTokens: 0
17113
+ };
17114
+ rollup.cost += cost;
17115
+ rollup.inputTokens += inp;
17116
+ rollup.outputTokens += out;
17117
+ acc.byProject.set(projectKey, rollup);
17118
+ }
17119
+ }
17120
+ function listGeminiSessionFiles(geminiTmpDir) {
17121
+ const out = [];
17122
+ let dirs;
17123
+ try {
17124
+ if (!fs35.statSync(geminiTmpDir).isDirectory()) return out;
17125
+ dirs = fs35.readdirSync(geminiTmpDir);
17126
+ } catch {
17127
+ return out;
17128
+ }
17129
+ for (const proj of dirs) {
17130
+ const chatsDir = path36.join(geminiTmpDir, proj, "chats");
17131
+ let files;
17132
+ try {
17133
+ if (!fs35.statSync(chatsDir).isDirectory()) continue;
17134
+ files = fs35.readdirSync(chatsDir);
17135
+ } catch {
17136
+ continue;
17137
+ }
17138
+ for (const f of files) {
17139
+ if (!f.endsWith(".jsonl")) continue;
17140
+ out.push({ projectKey: proj, file: path36.join(chatsDir, f) });
17141
+ }
16587
17142
  }
16588
- return { total, byDay, toolCalls };
17143
+ return out;
17144
+ }
17145
+ function loadGeminiCost(start, end, geminiTmpDir) {
17146
+ const acc = emptyGeminiAccumulator();
17147
+ if (!fs35.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
17148
+ for (const { projectKey, file } of listGeminiSessionFiles(geminiTmpDir)) {
17149
+ processGeminiCostFile(file, projectKey, start, end, acc);
17150
+ }
17151
+ return freezeGeminiCost(acc);
17152
+ }
17153
+ function aggregateReportFromAudit(period, opts = {}) {
17154
+ const now = opts.now ?? /* @__PURE__ */ new Date();
17155
+ const auditLogPath = opts.auditLogPath ?? path36.join(os31.homedir(), ".node9", "audit.log");
17156
+ const claudeProjectsDir = opts.claudeProjectsDir ?? path36.join(os31.homedir(), ".claude", "projects");
17157
+ const codexSessionsDir = opts.codexSessionsDir ?? path36.join(os31.homedir(), ".codex", "sessions");
17158
+ const geminiTmpDir = opts.geminiTmpDir ?? path36.join(os31.homedir(), ".gemini", "tmp");
17159
+ const hasAuditFile = fs35.existsSync(auditLogPath);
17160
+ const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath);
17161
+ const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
17162
+ const { start, end } = getDateRange(period, now);
17163
+ const responseDlpEntries = allEntries.filter((e) => {
17164
+ if (e.source !== "response-dlp") return false;
17165
+ const ts = new Date(e.ts);
17166
+ return ts >= start && ts <= end;
17167
+ }).map((e) => {
17168
+ const raw = e;
17169
+ return {
17170
+ ts: e.ts,
17171
+ dlpPattern: typeof raw.dlpPattern === "string" ? raw.dlpPattern : void 0,
17172
+ dlpSample: typeof raw.dlpSample === "string" ? raw.dlpSample : void 0
17173
+ };
17174
+ });
17175
+ const claudeCost = opts.preloadedClaudeCost ?? loadClaudeCost(start, end, claudeProjectsDir);
17176
+ const codexCost = opts.preloadedCodexCost ?? loadCodexCost(start, end, codexSessionsDir);
17177
+ const geminiCost = opts.preloadedGeminiCost ?? loadGeminiCost(start, end, geminiTmpDir);
17178
+ for (const [day, c] of codexCost.byDay) {
17179
+ claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
17180
+ }
17181
+ for (const [day, c] of geminiCost.byDay) {
17182
+ claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
17183
+ }
17184
+ for (const [geminiKey, gRollup] of geminiCost.byProject) {
17185
+ let mergedInto = null;
17186
+ for (const claudeKey of claudeCost.byProject.keys()) {
17187
+ const claudeBase = claudeKey.match(/[^/\\]+$/)?.[0] ?? claudeKey;
17188
+ if (claudeBase === geminiKey) {
17189
+ mergedInto = claudeKey;
17190
+ break;
17191
+ }
17192
+ }
17193
+ const targetKey = mergedInto ?? geminiKey;
17194
+ const existing = claudeCost.byProject.get(targetKey) ?? {
17195
+ cost: 0,
17196
+ inputTokens: 0,
17197
+ outputTokens: 0
17198
+ };
17199
+ existing.cost += gRollup.cost;
17200
+ existing.inputTokens += gRollup.inputTokens;
17201
+ existing.outputTokens += gRollup.outputTokens;
17202
+ claudeCost.byProject.set(targetKey, existing);
17203
+ }
17204
+ const periodMs = end.getTime() - start.getTime();
17205
+ const priorEnd = new Date(start.getTime() - 1);
17206
+ const priorStart = new Date(start.getTime() - periodMs);
17207
+ const priorEntries = allEntries.filter((e) => {
17208
+ if (e.source === "post-hook") return false;
17209
+ const ts = new Date(e.ts);
17210
+ return ts >= priorStart && ts <= priorEnd;
17211
+ });
17212
+ const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
17213
+ const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
17214
+ const excludeTests = opts.excludeTests === true;
17215
+ const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
17216
+ let excludedTests = 0;
17217
+ const entries = allEntries.filter((e) => {
17218
+ if (e.source === "post-hook") return false;
17219
+ if (e.source === "response-dlp") return false;
17220
+ const ts = new Date(e.ts);
17221
+ if (ts < start || ts > end) return false;
17222
+ if (excludeTests && isTestEntry(e, testTs)) {
17223
+ excludedTests++;
17224
+ return false;
17225
+ }
17226
+ return true;
17227
+ });
17228
+ let userApproved = 0;
17229
+ let userDenied = 0;
17230
+ let timedOut = 0;
17231
+ let hardBlocked = 0;
17232
+ let dlpBlocked = 0;
17233
+ let observeDlp = 0;
17234
+ let loopHits = 0;
17235
+ let testPasses = 0;
17236
+ let testFails = 0;
17237
+ const toolMap = /* @__PURE__ */ new Map();
17238
+ const blockMap = /* @__PURE__ */ new Map();
17239
+ const ruleMap = /* @__PURE__ */ new Map();
17240
+ const agentMap = /* @__PURE__ */ new Map();
17241
+ const mcpMap = /* @__PURE__ */ new Map();
17242
+ const dailyMap = /* @__PURE__ */ new Map();
17243
+ const hourMap = /* @__PURE__ */ new Map();
17244
+ for (const e of entries) {
17245
+ const allow = isAllow(e.decision);
17246
+ const dateKey = e.ts.slice(0, 10);
17247
+ const userInteracted = e.source === "daemon";
17248
+ if (userInteracted) {
17249
+ if (allow) userApproved++;
17250
+ else userDenied++;
17251
+ } else if (!allow) {
17252
+ if (e.checkedBy === "timeout") timedOut++;
17253
+ else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
17254
+ else if (isDlp(e.checkedBy)) dlpBlocked++;
17255
+ else if (e.checkedBy !== "loop-detected") hardBlocked++;
17256
+ }
17257
+ if (e.checkedBy === "loop-detected") loopHits++;
17258
+ const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
17259
+ t.calls++;
17260
+ if (!allow) t.blocked++;
17261
+ toolMap.set(e.tool, t);
17262
+ if (!allow && e.checkedBy) {
17263
+ blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
17264
+ }
17265
+ if (!allow && e.ruleName) {
17266
+ ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
17267
+ }
17268
+ if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
17269
+ if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
17270
+ const hour = new Date(e.ts).getHours();
17271
+ hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
17272
+ const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
17273
+ d.calls++;
17274
+ if (!allow) d.blocked++;
17275
+ dailyMap.set(dateKey, d);
17276
+ }
17277
+ for (const e of allEntries) {
17278
+ if (e.source !== "test-result") continue;
17279
+ const ts = new Date(e.ts);
17280
+ if (ts < start || ts > end) continue;
17281
+ if (e.testResult === "pass") testPasses++;
17282
+ else if (e.testResult === "fail") testFails++;
17283
+ }
17284
+ if (codexCost.toolCalls > 0) {
17285
+ agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexCost.toolCalls);
17286
+ }
17287
+ const data = {
17288
+ period,
17289
+ start,
17290
+ end,
17291
+ excludedTests,
17292
+ total: entries.length,
17293
+ userApproved,
17294
+ userDenied,
17295
+ timedOut,
17296
+ hardBlocked,
17297
+ dlpBlocked,
17298
+ observeDlp,
17299
+ loopHits,
17300
+ testPasses,
17301
+ testFails,
17302
+ unackedDlp: unackedDlp.length,
17303
+ priorBlockRate,
17304
+ cost: {
17305
+ claudeUSD: claudeCost.total,
17306
+ codexUSD: codexCost.total,
17307
+ geminiUSD: geminiCost.total,
17308
+ inputTokens: claudeCost.inputTokens + geminiCost.inputTokens,
17309
+ outputTokens: claudeCost.outputTokens + geminiCost.outputTokens,
17310
+ cacheWriteTokens: claudeCost.cacheWriteTokens,
17311
+ cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
17312
+ byDay: claudeCost.byDay,
17313
+ byModel: claudeCost.byModel,
17314
+ byProject: claudeCost.byProject
17315
+ },
17316
+ toolMap,
17317
+ blockMap,
17318
+ ruleMap,
17319
+ agentMap,
17320
+ mcpMap,
17321
+ dailyMap,
17322
+ hourMap,
17323
+ generatedAt: now.toISOString()
17324
+ };
17325
+ return { data, hasAuditFile, responseDlpEntries };
17326
+ }
17327
+
17328
+ // src/cli/render/report-json.ts
17329
+ function buildReportJson(input) {
17330
+ const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
17331
+ const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
17332
+ const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
17333
+ return {
17334
+ schemaVersion: 1,
17335
+ generatedAt: input.generatedAt,
17336
+ period: input.period,
17337
+ range: { start: input.start.toISOString(), end: input.end.toISOString() },
17338
+ excludedTests: input.excludedTests,
17339
+ totals: {
17340
+ events: input.total,
17341
+ blocked: totalBlocked,
17342
+ blockRate,
17343
+ userApproved: input.userApproved,
17344
+ userDenied: input.userDenied,
17345
+ timedOut: input.timedOut,
17346
+ hardBlocked: input.hardBlocked,
17347
+ dlpBlocked: input.dlpBlocked,
17348
+ observeDlp: input.observeDlp,
17349
+ loopHits: input.loopHits,
17350
+ unackedDlp: input.unackedDlp
17351
+ },
17352
+ tests: {
17353
+ passes: input.testPasses,
17354
+ fails: input.testFails
17355
+ },
17356
+ cost: {
17357
+ totalUSD: input.cost.claudeUSD + input.cost.codexUSD + input.cost.geminiUSD,
17358
+ claudeUSD: input.cost.claudeUSD,
17359
+ codexUSD: input.cost.codexUSD,
17360
+ geminiUSD: input.cost.geminiUSD,
17361
+ inputTokens: input.cost.inputTokens,
17362
+ outputTokens: input.cost.outputTokens,
17363
+ cacheWriteTokens: input.cost.cacheWriteTokens,
17364
+ cacheReadTokens: input.cost.cacheReadTokens,
17365
+ byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
17366
+ byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
17367
+ },
17368
+ byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
17369
+ byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
17370
+ byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
17371
+ byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
17372
+ byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
17373
+ byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
17374
+ trend: {
17375
+ priorBlockRate: input.priorBlockRate,
17376
+ deltaPct
17377
+ }
17378
+ };
17379
+ }
17380
+
17381
+ // src/cli/commands/report.ts
17382
+ var BLOCK_REASON_LABELS = {
17383
+ timeout: "Approval timeout",
17384
+ "smart-rule-block": "Smart rule",
17385
+ "observe-mode-dlp-would-block": "DLP (observe)",
17386
+ "persistent-deny": "Persistent deny",
17387
+ "local-decision": "User denied",
17388
+ "dlp-block": "DLP block",
17389
+ "loop-detected": "Loop detected"
17390
+ };
17391
+ function humanBlockReason(reason) {
17392
+ return BLOCK_REASON_LABELS[reason] ?? reason;
17393
+ }
17394
+ function barStr(value, max, width) {
17395
+ if (max === 0 || width <= 0) return "\u2591".repeat(width);
17396
+ const filled = Math.max(1, Math.round(value / max * width));
17397
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
17398
+ }
17399
+ function colorBar(value, max, width) {
17400
+ const s = barStr(value, max, width);
17401
+ const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
17402
+ return chalk13.cyan(s.slice(0, filled)) + chalk13.dim(s.slice(filled));
17403
+ }
17404
+ function fmtDate(d) {
17405
+ const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
17406
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
17407
+ }
17408
+ function num2(n) {
17409
+ return n.toLocaleString();
17410
+ }
17411
+ function fmtCost2(usd) {
17412
+ if (usd < 1e-3) return "< $0.001";
17413
+ if (usd < 1) return "$" + usd.toFixed(4);
17414
+ return "$" + usd.toFixed(2);
16589
17415
  }
16590
17416
  function registerReportCommand(program2) {
16591
17417
  program2.command("report").description("Activity and security report \u2014 what Claude did, what was blocked").option("--period <period>", "today | 7d | 30d | month", "7d").option("--no-tests", "exclude test runner calls (npm test, vitest, pytest\u2026) from stats").option("--json", "Emit machine-readable JSON to stdout (suppresses renderer)").action((options) => {
16592
- const period = ["today", "7d", "30d", "month"].includes(
17418
+ const period = ["today", "7d", "30d", "90d", "month"].includes(
16593
17419
  options.period
16594
17420
  ) ? options.period : "7d";
16595
- const logPath = path36.join(os31.homedir(), ".node9", "audit.log");
16596
- const allEntries = parseAuditLog(logPath);
16597
- const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
16598
- if (unackedDlp.length > 0 && !options.json) {
17421
+ const excludeTests = options.tests === false;
17422
+ const { data, hasAuditFile, responseDlpEntries } = aggregateReportFromAudit(period, {
17423
+ excludeTests
17424
+ });
17425
+ if (data.unackedDlp > 0 && !options.json) {
16599
17426
  console.log("");
16600
17427
  console.log(
16601
17428
  chalk13.bgRed.white.bold(
16602
- ` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
17429
+ ` \u26A0\uFE0F DLP ALERT: ${data.unackedDlp} secret${data.unackedDlp !== 1 ? "s" : ""} found in Claude response text `
16603
17430
  ) + " " + chalk13.yellow("\u2192 run: node9 dlp")
16604
17431
  );
16605
17432
  }
16606
- if (allEntries.length === 0 && !options.json) {
17433
+ if (!hasAuditFile && !options.json) {
16607
17434
  console.log(
16608
17435
  chalk13.yellow("\n No audit data found. Run node9 with Claude Code to generate entries.\n")
16609
17436
  );
16610
17437
  return;
16611
17438
  }
16612
- const { start, end } = getDateRange(period);
16613
- const {
16614
- total: claudeCostUSD,
16615
- byDay: costByDay,
16616
- byModel: costByModel,
16617
- inputTokens: costInputTokens,
16618
- outputTokens: costOutputTokens,
16619
- cacheWriteTokens: costCacheWrite,
16620
- cacheReadTokens: costCacheRead
16621
- } = loadClaudeCost(start, end);
16622
- const {
16623
- total: codexCostUSD,
16624
- byDay: codexCostByDay,
16625
- toolCalls: codexToolCalls
16626
- } = loadCodexCost(start, end);
16627
- const costUSD = claudeCostUSD + codexCostUSD;
16628
- for (const [day, c] of codexCostByDay) {
16629
- costByDay.set(day, (costByDay.get(day) ?? 0) + c);
16630
- }
16631
- const periodMs = end.getTime() - start.getTime();
16632
- const priorEnd = new Date(start.getTime() - 1);
16633
- const priorStart = new Date(start.getTime() - periodMs);
16634
- const priorEntries = allEntries.filter((e) => {
16635
- if (e.source === "post-hook") return false;
16636
- const ts = new Date(e.ts);
16637
- return ts >= priorStart && ts <= priorEnd;
16638
- });
16639
- const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
16640
- const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
16641
- const excludeTests = options.tests === false;
16642
- const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
16643
- let filteredTestCount = 0;
16644
- const entries = allEntries.filter((e) => {
16645
- if (e.source === "post-hook") return false;
16646
- if (e.source === "response-dlp") return false;
16647
- const ts = new Date(e.ts);
16648
- if (ts < start || ts > end) return false;
16649
- if (excludeTests && isTestEntry(e, testTs)) {
16650
- filteredTestCount++;
16651
- return false;
16652
- }
16653
- return true;
16654
- });
16655
- if (entries.length === 0 && !options.json) {
17439
+ if (options.json) {
17440
+ const envelope = buildReportJson(data);
17441
+ process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
17442
+ return;
17443
+ }
17444
+ if (data.total === 0) {
16656
17445
  console.log(chalk13.yellow(`
16657
17446
  No activity for period "${period}".
16658
17447
  `));
16659
17448
  return;
16660
17449
  }
16661
- let userApproved = 0;
16662
- let userDenied = 0;
16663
- let timedOut = 0;
16664
- let hardBlocked = 0;
16665
- let dlpBlocked = 0;
16666
- let observeDlp = 0;
16667
- let loopHits = 0;
16668
- let testPasses = 0;
16669
- let testFails = 0;
16670
- const toolMap = /* @__PURE__ */ new Map();
16671
- const blockMap = /* @__PURE__ */ new Map();
16672
- const agentMap = /* @__PURE__ */ new Map();
16673
- const mcpMap = /* @__PURE__ */ new Map();
16674
- const dailyMap = /* @__PURE__ */ new Map();
16675
- const hourMap = /* @__PURE__ */ new Map();
16676
- for (const e of entries) {
16677
- const allow = isAllow(e.decision);
16678
- const dateKey = e.ts.slice(0, 10);
16679
- const userInteracted = e.source === "daemon";
16680
- if (userInteracted) {
16681
- if (allow) userApproved++;
16682
- else userDenied++;
16683
- } else if (!allow) {
16684
- if (e.checkedBy === "timeout") timedOut++;
16685
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
16686
- else if (isDlp(e.checkedBy)) dlpBlocked++;
16687
- else if (e.checkedBy !== "loop-detected") hardBlocked++;
16688
- }
16689
- if (e.checkedBy === "loop-detected") loopHits++;
16690
- const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
16691
- t.calls++;
16692
- if (!allow) t.blocked++;
16693
- toolMap.set(e.tool, t);
16694
- if (!allow && e.checkedBy) {
16695
- blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
16696
- }
16697
- if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
16698
- if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
16699
- const hour = new Date(e.ts).getHours();
16700
- hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
16701
- const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
16702
- d.calls++;
16703
- if (!allow) d.blocked++;
16704
- dailyMap.set(dateKey, d);
16705
- }
16706
- for (const e of allEntries) {
16707
- if (e.source !== "test-result") continue;
16708
- const ts = new Date(e.ts);
16709
- if (ts < start || ts > end) continue;
16710
- if (e.testResult === "pass") testPasses++;
16711
- else if (e.testResult === "fail") testFails++;
16712
- }
16713
- if (codexToolCalls > 0) agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexToolCalls);
16714
- if (options.json) {
16715
- const envelope = buildReportJson({
16716
- period,
16717
- start,
16718
- end,
16719
- excludedTests: filteredTestCount,
16720
- total: entries.length,
16721
- userApproved,
16722
- userDenied,
16723
- timedOut,
16724
- hardBlocked,
16725
- dlpBlocked,
16726
- observeDlp,
16727
- loopHits,
16728
- testPasses,
16729
- testFails,
16730
- unackedDlp: unackedDlp.length,
16731
- priorBlockRate,
16732
- cost: {
16733
- claudeUSD: claudeCostUSD,
16734
- codexUSD: codexCostUSD,
16735
- inputTokens: costInputTokens,
16736
- outputTokens: costOutputTokens,
16737
- cacheWriteTokens: costCacheWrite,
16738
- cacheReadTokens: costCacheRead,
16739
- byDay: costByDay,
16740
- byModel: costByModel
16741
- },
16742
- toolMap,
16743
- blockMap,
16744
- agentMap,
16745
- mcpMap,
16746
- dailyMap,
16747
- hourMap,
16748
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
16749
- });
16750
- process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
16751
- return;
16752
- }
16753
- const total = entries.length;
16754
- const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
16755
- const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
16756
- const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
16757
- const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
16758
- const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
16759
- const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
16760
- const W = Math.min(process.stdout.columns || 80, 100);
16761
- const INNER = W - 4;
16762
- const COL = Math.floor(INNER / 2) - 1;
16763
- const LABEL = 24;
16764
- const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
16765
- const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num2(v.calls).length), 1);
16766
- const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num2(v).length), 1);
16767
- const line = chalk13.dim("\u2500".repeat(W - 2));
16768
- const periodLabel = {
16769
- today: "Today",
16770
- "7d": "Last 7 Days",
16771
- "30d": "Last 30 Days",
16772
- month: "This Month"
16773
- };
17450
+ renderTerminalReport(data, responseDlpEntries, excludeTests);
17451
+ });
17452
+ }
17453
+ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
17454
+ const {
17455
+ period,
17456
+ start,
17457
+ end,
17458
+ total,
17459
+ excludedTests,
17460
+ userApproved,
17461
+ userDenied,
17462
+ timedOut,
17463
+ hardBlocked,
17464
+ dlpBlocked,
17465
+ observeDlp,
17466
+ loopHits,
17467
+ testPasses,
17468
+ testFails,
17469
+ priorBlockRate,
17470
+ cost: {
17471
+ claudeUSD,
17472
+ codexUSD,
17473
+ geminiUSD,
17474
+ inputTokens: costInputTokens,
17475
+ outputTokens: costOutputTokens,
17476
+ cacheWriteTokens: costCacheWrite,
17477
+ cacheReadTokens: costCacheRead,
17478
+ byDay: costByDay,
17479
+ byModel: costByModel
17480
+ },
17481
+ toolMap,
17482
+ blockMap,
17483
+ agentMap,
17484
+ mcpMap,
17485
+ dailyMap,
17486
+ hourMap
17487
+ } = data;
17488
+ const costUSD = claudeUSD + codexUSD + geminiUSD;
17489
+ const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
17490
+ const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
17491
+ const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
17492
+ const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
17493
+ const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
17494
+ const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
17495
+ const W = Math.min(process.stdout.columns || 80, 100);
17496
+ const INNER = W - 4;
17497
+ const COL = Math.floor(INNER / 2) - 1;
17498
+ const LABEL = 24;
17499
+ const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
17500
+ const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num2(v.calls).length), 1);
17501
+ const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num2(v).length), 1);
17502
+ const line = chalk13.dim("\u2500".repeat(W - 2));
17503
+ const periodLabel = {
17504
+ today: "Today",
17505
+ "7d": "Last 7 Days",
17506
+ "30d": "Last 30 Days",
17507
+ "90d": "Last 90 Days",
17508
+ month: "This Month"
17509
+ };
17510
+ console.log("");
17511
+ console.log(
17512
+ " " + chalk13.bold.cyan("\u{1F6E1} node9 Report") + chalk13.dim(" \xB7 ") + chalk13.white(periodLabel[period]) + chalk13.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + chalk13.dim(` ${num2(total)} events`) + (excludeTests ? chalk13.dim(` \u2013tests (\u2013${excludedTests})`) : "")
17513
+ );
17514
+ console.log(" " + line);
17515
+ const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
17516
+ const currentRate = total > 0 ? totalBlocked / total : 0;
17517
+ const trendLabel = (() => {
17518
+ if (priorBlockRate === null) return "";
17519
+ const delta = Math.round((currentRate - priorBlockRate) * 100);
17520
+ if (delta === 0) return "";
17521
+ return " " + (delta > 0 ? chalk13.red(`\u25B2${delta}%`) : chalk13.green(`\u25BC${Math.abs(delta)}%`)) + chalk13.dim(" vs prior");
17522
+ })();
17523
+ const reads = toolMap.get("Read")?.calls ?? 0;
17524
+ const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
17525
+ const ratioLabel = reads > 0 ? chalk13.dim(`edit/read ${(edits / reads).toFixed(1)}`) : chalk13.dim("edit/read \u2013");
17526
+ const testLabel = testPasses + testFails > 0 ? chalk13.dim("tests ") + chalk13.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + chalk13.red(`${testFails}\u2717`) : "") : chalk13.dim("tests \u2013");
17527
+ console.log("");
17528
+ console.log(" " + chalk13.bold("Protection Summary"));
17529
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
17530
+ console.log(
17531
+ " " + chalk13.dim("Intercepted") + " " + chalk13.white(num2(total)) + chalk13.dim(" tool calls")
17532
+ );
17533
+ console.log("");
17534
+ const COL1 = 18;
17535
+ const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
17536
+ const countStr = colorFn(num2(count));
17537
+ const noteStr = note ? chalk13.dim(" " + note) : "";
17538
+ console.log(" " + icon + " " + chalk13.white(label.padEnd(COL1)) + countStr + noteStr);
17539
+ };
17540
+ summaryRow(
17541
+ userApproved > 0 ? chalk13.green("\u2705") : chalk13.dim("\u2705"),
17542
+ "User approved",
17543
+ userApproved,
17544
+ userApproved === 0 ? "no popups this period" : void 0,
17545
+ userApproved > 0 ? (s) => chalk13.green(s) : (s) => chalk13.dim(s)
17546
+ );
17547
+ summaryRow(
17548
+ userDenied > 0 ? chalk13.red("\u{1F6AB}") : chalk13.dim("\u{1F6AB}"),
17549
+ "User denied",
17550
+ userDenied,
17551
+ void 0,
17552
+ userDenied > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
17553
+ );
17554
+ summaryRow(
17555
+ timedOut > 0 ? chalk13.yellow("\u23F1") : chalk13.dim("\u23F1"),
17556
+ "Timed out",
17557
+ timedOut,
17558
+ timedOut > 0 ? "no approval response" : void 0,
17559
+ timedOut > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
17560
+ );
17561
+ summaryRow(
17562
+ hardBlocked > 0 ? chalk13.red("\u{1F6D1}") : chalk13.dim("\u{1F6D1}"),
17563
+ "Auto-blocked",
17564
+ hardBlocked,
17565
+ void 0,
17566
+ hardBlocked > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
17567
+ );
17568
+ summaryRow(
17569
+ dlpBlocked > 0 ? chalk13.yellow("\u{1F6A8}") : chalk13.dim("\u{1F6A8}"),
17570
+ "DLP blocked",
17571
+ dlpBlocked,
17572
+ void 0,
17573
+ dlpBlocked > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
17574
+ );
17575
+ summaryRow(
17576
+ observeDlp > 0 ? chalk13.blue("\u{1F441}") : chalk13.dim("\u{1F441}"),
17577
+ "DLP (observe)",
17578
+ observeDlp,
17579
+ observeDlp > 0 ? "would-block in strict mode" : void 0,
17580
+ observeDlp > 0 ? (s) => chalk13.blue(s) : (s) => chalk13.dim(s)
17581
+ );
17582
+ summaryRow(
17583
+ loopHits > 0 ? chalk13.yellow("\u{1F504}") : chalk13.dim("\u{1F504}"),
17584
+ "Loops detected",
17585
+ loopHits,
17586
+ void 0,
17587
+ loopHits > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
17588
+ );
17589
+ if (trendLabel || ratioLabel || testPasses + testFails > 0) {
16774
17590
  console.log("");
16775
- console.log(
16776
- " " + chalk13.bold.cyan("\u{1F6E1} node9 Report") + chalk13.dim(" \xB7 ") + chalk13.white(periodLabel[period]) + chalk13.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + chalk13.dim(` ${num2(total)} events`) + (excludeTests ? chalk13.dim(` \u2013tests (\u2013${filteredTestCount})`) : "")
16777
- );
16778
- console.log(" " + line);
16779
- const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
16780
- const currentRate = total > 0 ? totalBlocked / total : 0;
16781
- const trendLabel = (() => {
16782
- if (priorBlockRate === null) return "";
16783
- const delta = Math.round((currentRate - priorBlockRate) * 100);
16784
- if (delta === 0) return "";
16785
- return " " + (delta > 0 ? chalk13.red(`\u25B2${delta}%`) : chalk13.green(`\u25BC${Math.abs(delta)}%`)) + chalk13.dim(" vs prior");
16786
- })();
16787
- const reads = toolMap.get("Read")?.calls ?? 0;
16788
- const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
16789
- const ratioLabel = reads > 0 ? chalk13.dim(`edit/read ${(edits / reads).toFixed(1)}`) : chalk13.dim("edit/read \u2013");
16790
- const testLabel = testPasses + testFails > 0 ? chalk13.dim("tests ") + chalk13.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + chalk13.red(`${testFails}\u2717`) : "") : chalk13.dim("tests \u2013");
17591
+ console.log(" " + ratioLabel + " " + testLabel + trendLabel);
17592
+ }
17593
+ console.log("");
17594
+ const toolHeaderRaw = "Top Tools";
17595
+ const blockHeaderRaw = "Top Blocks";
17596
+ console.log(
17597
+ " " + chalk13.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + chalk13.bold(blockHeaderRaw)
17598
+ );
17599
+ console.log(" " + chalk13.dim("\u2500".repeat(COL)) + " " + chalk13.dim("\u2500".repeat(COL)));
17600
+ const rows = Math.max(topTools.length, topBlocks.length, 1);
17601
+ for (let i = 0; i < rows; i++) {
17602
+ let leftStyled = " ".repeat(COL);
17603
+ if (i < topTools.length) {
17604
+ const [tool, { calls }] = topTools[i];
17605
+ const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
17606
+ const countStr = num2(calls).padStart(TOOL_COUNT_W);
17607
+ const b = colorBar(calls, maxTool, BAR);
17608
+ const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
17609
+ const pad = Math.max(0, COL - rawLen);
17610
+ leftStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(countStr) + " ".repeat(pad);
17611
+ }
17612
+ let rightStyled = "";
17613
+ if (i < topBlocks.length) {
17614
+ const [reason, count] = topBlocks[i];
17615
+ const readable = humanBlockReason(reason);
17616
+ const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
17617
+ const countStr = num2(count).padStart(BLOCK_COUNT_W);
17618
+ const b = colorBar(count, maxBlock, BAR);
17619
+ rightStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.red(countStr);
17620
+ }
17621
+ console.log(" " + leftStyled + " " + rightStyled);
17622
+ }
17623
+ if (topBlocks.length === 0) {
17624
+ console.log(" " + " ".repeat(COL) + " " + chalk13.dim("nothing blocked \u2713"));
17625
+ }
17626
+ if (agentMap.size >= 1) {
16791
17627
  console.log("");
16792
- console.log(" " + chalk13.bold("Protection Summary"));
17628
+ console.log(" " + chalk13.bold("Agents"));
16793
17629
  console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16794
- console.log(
16795
- " " + chalk13.dim("Intercepted") + " " + chalk13.white(num2(total)) + chalk13.dim(" tool calls")
16796
- );
16797
- console.log("");
16798
- const COL1 = 18;
16799
- const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
16800
- const countStr = colorFn(num2(count));
16801
- const noteStr = note ? chalk13.dim(" " + note) : "";
16802
- console.log(" " + icon + " " + chalk13.white(label.padEnd(COL1)) + countStr + noteStr);
16803
- };
16804
- summaryRow(
16805
- userApproved > 0 ? chalk13.green("\u2705") : chalk13.dim("\u2705"),
16806
- "User approved",
16807
- userApproved,
16808
- userApproved === 0 ? "no popups this period" : void 0,
16809
- userApproved > 0 ? (s) => chalk13.green(s) : (s) => chalk13.dim(s)
16810
- );
16811
- summaryRow(
16812
- userDenied > 0 ? chalk13.red("\u{1F6AB}") : chalk13.dim("\u{1F6AB}"),
16813
- "User denied",
16814
- userDenied,
16815
- void 0,
16816
- userDenied > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
16817
- );
16818
- summaryRow(
16819
- timedOut > 0 ? chalk13.yellow("\u23F1") : chalk13.dim("\u23F1"),
16820
- "Timed out",
16821
- timedOut,
16822
- timedOut > 0 ? "no approval response" : void 0,
16823
- timedOut > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
16824
- );
16825
- summaryRow(
16826
- hardBlocked > 0 ? chalk13.red("\u{1F6D1}") : chalk13.dim("\u{1F6D1}"),
16827
- "Auto-blocked",
16828
- hardBlocked,
16829
- void 0,
16830
- hardBlocked > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
16831
- );
16832
- summaryRow(
16833
- dlpBlocked > 0 ? chalk13.yellow("\u{1F6A8}") : chalk13.dim("\u{1F6A8}"),
16834
- "DLP blocked",
16835
- dlpBlocked,
16836
- void 0,
16837
- dlpBlocked > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
16838
- );
16839
- summaryRow(
16840
- observeDlp > 0 ? chalk13.blue("\u{1F441}") : chalk13.dim("\u{1F441}"),
16841
- "DLP (observe)",
16842
- observeDlp,
16843
- observeDlp > 0 ? "would-block in strict mode" : void 0,
16844
- observeDlp > 0 ? (s) => chalk13.blue(s) : (s) => chalk13.dim(s)
16845
- );
16846
- summaryRow(
16847
- loopHits > 0 ? chalk13.yellow("\u{1F504}") : chalk13.dim("\u{1F504}"),
16848
- "Loops detected",
16849
- loopHits,
16850
- void 0,
16851
- loopHits > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
16852
- );
16853
- if (trendLabel || ratioLabel || testPasses + testFails > 0) {
16854
- console.log("");
16855
- console.log(" " + ratioLabel + " " + testLabel + trendLabel);
17630
+ const maxAgent = Math.max(...agentMap.values(), 1);
17631
+ for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
17632
+ const label = agent.slice(0, LABEL - 1);
17633
+ const b = colorBar(count, maxAgent, BAR);
17634
+ console.log(" " + chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(num2(count)));
16856
17635
  }
17636
+ }
17637
+ if (mcpMap.size > 0) {
16857
17638
  console.log("");
16858
- const toolHeaderRaw = "Top Tools";
16859
- const blockHeaderRaw = "Top Blocks";
16860
- console.log(
16861
- " " + chalk13.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + chalk13.bold(blockHeaderRaw)
16862
- );
16863
- console.log(" " + chalk13.dim("\u2500".repeat(COL)) + " " + chalk13.dim("\u2500".repeat(COL)));
16864
- const rows = Math.max(topTools.length, topBlocks.length, 1);
16865
- for (let i = 0; i < rows; i++) {
16866
- let leftStyled = " ".repeat(COL);
16867
- if (i < topTools.length) {
16868
- const [tool, { calls }] = topTools[i];
16869
- const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
16870
- const countStr = num2(calls).padStart(TOOL_COUNT_W);
16871
- const b = colorBar(calls, maxTool, BAR);
16872
- const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
16873
- const pad = Math.max(0, COL - rawLen);
16874
- leftStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(countStr) + " ".repeat(pad);
16875
- }
16876
- let rightStyled = "";
16877
- if (i < topBlocks.length) {
16878
- const [reason, count] = topBlocks[i];
16879
- const readable = humanBlockReason(reason);
16880
- const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
16881
- const countStr = num2(count).padStart(BLOCK_COUNT_W);
16882
- const b = colorBar(count, maxBlock, BAR);
16883
- rightStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.red(countStr);
16884
- }
16885
- console.log(" " + leftStyled + " " + rightStyled);
16886
- }
16887
- if (topBlocks.length === 0) {
16888
- console.log(" " + " ".repeat(COL) + " " + chalk13.dim("nothing blocked \u2713"));
16889
- }
16890
- if (agentMap.size >= 1) {
16891
- console.log("");
16892
- console.log(" " + chalk13.bold("Agents"));
16893
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16894
- const maxAgent = Math.max(...agentMap.values(), 1);
16895
- for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
16896
- const label = agent.slice(0, LABEL - 1);
16897
- const b = colorBar(count, maxAgent, BAR);
16898
- console.log(" " + chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(num2(count)));
16899
- }
16900
- }
16901
- if (mcpMap.size > 0) {
16902
- console.log("");
16903
- console.log(" " + chalk13.bold("MCP Servers"));
16904
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16905
- const maxMcp = Math.max(...mcpMap.values(), 1);
16906
- for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
16907
- const label = server.slice(0, LABEL - 1).padEnd(LABEL);
16908
- const b = colorBar(count, maxMcp, BAR);
16909
- console.log(" " + chalk13.white(label) + b + " " + chalk13.white(num2(count)));
16910
- }
16911
- }
16912
- if (hourMap.size > 0) {
16913
- const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
16914
- const maxHour = Math.max(...hourMap.values(), 1);
16915
- const bar = Array.from({ length: 24 }, (_, h) => {
16916
- const v = hourMap.get(h) ?? 0;
16917
- return BLOCKS[Math.round(v / maxHour * 8)];
16918
- }).join("");
16919
- console.log("");
16920
- console.log(" " + chalk13.bold("Hour of Day") + chalk13.dim(" (local, 0h \u2013 23h)"));
16921
- console.log(" " + chalk13.cyan(bar));
16922
- console.log(" " + chalk13.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
16923
- }
16924
- if (dailyList.length > 1) {
16925
- console.log("");
16926
- console.log(" " + chalk13.bold("Daily Activity"));
16927
- console.log(" " + chalk13.dim("\u2500".repeat(W - 2)));
16928
- const DAY_BAR = Math.max(8, Math.min(30, W - 36));
16929
- for (const [dateKey, { calls, blocked: db }] of dailyList) {
16930
- const label = fmtDate(dateKey).padEnd(10);
16931
- const b = colorBar(calls, maxDaily, DAY_BAR);
16932
- const dayCost = costByDay.get(dateKey);
16933
- const costNote = dayCost ? chalk13.magenta(` ${fmtCost2(dayCost)}`) : "";
16934
- const blockNote = db > 0 ? chalk13.red(` ${db} blocked`) : "";
16935
- console.log(
16936
- " " + chalk13.dim(label) + " " + b + " " + chalk13.white(num2(calls)) + blockNote + costNote
16937
- );
16938
- }
16939
- }
16940
- const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
16941
- if (totalTokens > 0) {
16942
- const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
16943
- console.log("");
16944
- console.log(" " + chalk13.bold("Tokens") + " " + chalk13.dim(`${num2(totalTokens)} total`));
16945
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16946
- const TOK_BAR = Math.max(6, Math.min(20, W - 30));
16947
- const TOK_LABEL = 14;
16948
- const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
16949
- const nonCacheRows = [
16950
- ["Input", costInputTokens, chalk13.cyan(num2(costInputTokens))],
16951
- ["Output", costOutputTokens, chalk13.white(num2(costOutputTokens))],
16952
- ["Cache write", costCacheWrite, chalk13.yellow(num2(costCacheWrite))]
16953
- ];
16954
- for (const [label, count, colored] of nonCacheRows) {
16955
- if (count === 0) continue;
16956
- const b = colorBar(count, maxNonCache, TOK_BAR);
16957
- console.log(" " + chalk13.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
16958
- }
16959
- if (costCacheRead > 0) {
16960
- const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
16961
- const pct = cacheHitPct > 0 ? chalk13.dim(` ${cacheHitPct}% hit rate`) : "";
16962
- console.log(
16963
- " " + chalk13.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + chalk13.green(num2(costCacheRead)) + pct
16964
- );
16965
- }
16966
- }
16967
- if (costUSD > 0) {
16968
- const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
16969
- const avgPerDay = costUSD / periodDays;
16970
- const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
16971
- const costHeaderRight = [
16972
- chalk13.yellow(fmtCost2(costUSD)),
16973
- chalk13.dim(`avg ${fmtCost2(avgPerDay)}/day`),
16974
- cacheHitPct > 0 ? chalk13.dim(`${cacheHitPct}% cache hit`) : null
16975
- ].filter(Boolean).join(chalk13.dim(" \xB7 "));
16976
- console.log("");
16977
- console.log(" " + chalk13.bold("Cost") + " " + costHeaderRight);
16978
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16979
- if (codexCostUSD > 0)
16980
- costByModel.set(
16981
- "codex (openai)",
16982
- (costByModel.get("codex (openai)") ?? 0) + codexCostUSD
16983
- );
16984
- const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
16985
- const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
16986
- const MODEL_LABEL = 22;
16987
- const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
16988
- for (const [model, cost] of modelList) {
16989
- const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
16990
- const b = colorBar(cost, maxModelCost, MODEL_BAR);
16991
- console.log(
16992
- " " + chalk13.white(label.padEnd(MODEL_LABEL)) + b + " " + chalk13.yellow(fmtCost2(cost))
16993
- );
16994
- }
17639
+ console.log(" " + chalk13.bold("MCP Servers"));
17640
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
17641
+ const maxMcp = Math.max(...mcpMap.values(), 1);
17642
+ for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
17643
+ const label = server.slice(0, LABEL - 1).padEnd(LABEL);
17644
+ const b = colorBar(count, maxMcp, BAR);
17645
+ console.log(" " + chalk13.white(label) + b + " " + chalk13.white(num2(count)));
17646
+ }
17647
+ }
17648
+ if (hourMap.size > 0) {
17649
+ const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
17650
+ const maxHour = Math.max(...hourMap.values(), 1);
17651
+ const bar = Array.from({ length: 24 }, (_, h) => {
17652
+ const v = hourMap.get(h) ?? 0;
17653
+ return BLOCKS[Math.round(v / maxHour * 8)];
17654
+ }).join("");
17655
+ console.log("");
17656
+ console.log(" " + chalk13.bold("Hour of Day") + chalk13.dim(" (local, 0h \u2013 23h)"));
17657
+ console.log(" " + chalk13.cyan(bar));
17658
+ console.log(" " + chalk13.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
17659
+ }
17660
+ if (dailyList.length > 1) {
17661
+ console.log("");
17662
+ console.log(" " + chalk13.bold("Daily Activity"));
17663
+ console.log(" " + chalk13.dim("\u2500".repeat(W - 2)));
17664
+ const DAY_BAR = Math.max(8, Math.min(30, W - 36));
17665
+ for (const [dateKey, { calls, blocked: db }] of dailyList) {
17666
+ const label = fmtDate(dateKey).padEnd(10);
17667
+ const b = colorBar(calls, maxDaily, DAY_BAR);
17668
+ const dayCost = costByDay.get(dateKey);
17669
+ const costNote = dayCost ? chalk13.magenta(` ${fmtCost2(dayCost)}`) : "";
17670
+ const blockNote = db > 0 ? chalk13.red(` ${db} blocked`) : "";
17671
+ console.log(
17672
+ " " + chalk13.dim(label) + " " + b + " " + chalk13.white(num2(calls)) + blockNote + costNote
17673
+ );
16995
17674
  }
16996
- const responseDlpEntries = allEntries.filter((e) => {
16997
- if (e.source !== "response-dlp") return false;
16998
- const ts = new Date(e.ts);
16999
- return ts >= start && ts <= end;
17000
- });
17001
- if (responseDlpEntries.length > 0) {
17002
- console.log("");
17675
+ }
17676
+ const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
17677
+ if (totalTokens > 0) {
17678
+ const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
17679
+ console.log("");
17680
+ console.log(" " + chalk13.bold("Tokens") + " " + chalk13.dim(`${num2(totalTokens)} total`));
17681
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
17682
+ const TOK_BAR = Math.max(6, Math.min(20, W - 30));
17683
+ const TOK_LABEL = 14;
17684
+ const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
17685
+ const nonCacheRows = [
17686
+ ["Input", costInputTokens, chalk13.cyan(num2(costInputTokens))],
17687
+ ["Output", costOutputTokens, chalk13.white(num2(costOutputTokens))],
17688
+ ["Cache write", costCacheWrite, chalk13.yellow(num2(costCacheWrite))]
17689
+ ];
17690
+ for (const [label, count, colored] of nonCacheRows) {
17691
+ if (count === 0) continue;
17692
+ const b = colorBar(count, maxNonCache, TOK_BAR);
17693
+ console.log(" " + chalk13.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
17694
+ }
17695
+ if (costCacheRead > 0) {
17696
+ const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
17697
+ const pct = cacheHitPct > 0 ? chalk13.dim(` ${cacheHitPct}% hit rate`) : "";
17003
17698
  console.log(
17004
- " " + chalk13.red.bold("\u26A0\uFE0F Response DLP") + chalk13.dim(" \xB7 ") + chalk13.red(
17005
- `${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
17006
- )
17699
+ " " + chalk13.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + chalk13.green(num2(costCacheRead)) + pct
17007
17700
  );
17008
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(60, W - 4))));
17701
+ }
17702
+ }
17703
+ if (costUSD > 0) {
17704
+ const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
17705
+ const avgPerDay = costUSD / periodDays;
17706
+ const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
17707
+ const costHeaderRight = [
17708
+ chalk13.yellow(fmtCost2(costUSD)),
17709
+ chalk13.dim(`avg ${fmtCost2(avgPerDay)}/day`),
17710
+ cacheHitPct > 0 ? chalk13.dim(`${cacheHitPct}% cache hit`) : null
17711
+ ].filter(Boolean).join(chalk13.dim(" \xB7 "));
17712
+ console.log("");
17713
+ console.log(" " + chalk13.bold("Cost") + " " + costHeaderRight);
17714
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
17715
+ if (codexUSD > 0)
17716
+ costByModel.set("codex (openai)", (costByModel.get("codex (openai)") ?? 0) + codexUSD);
17717
+ if (geminiUSD > 0)
17718
+ costByModel.set("gemini (google)", (costByModel.get("gemini (google)") ?? 0) + geminiUSD);
17719
+ const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
17720
+ const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
17721
+ const MODEL_LABEL = 22;
17722
+ const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
17723
+ for (const [model, cost] of modelList) {
17724
+ const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
17725
+ const b = colorBar(cost, maxModelCost, MODEL_BAR);
17009
17726
  console.log(
17010
- " " + chalk13.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
17727
+ " " + chalk13.white(label.padEnd(MODEL_LABEL)) + b + " " + chalk13.yellow(fmtCost2(cost))
17011
17728
  );
17012
- console.log(" " + chalk13.yellow("Rotate affected keys immediately."));
17013
- for (const e of responseDlpEntries.slice(0, 5)) {
17014
- const ts = chalk13.dim(fmtDate(e.ts) + " ");
17015
- const pattern = chalk13.red(e.dlpPattern ?? "DLP");
17016
- const sample = chalk13.gray(e.dlpSample ?? "");
17017
- console.log(` ${ts}${pattern} ${sample}`);
17018
- }
17019
- if (responseDlpEntries.length > 5) {
17020
- console.log(chalk13.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
17021
- }
17022
17729
  }
17730
+ }
17731
+ if (responseDlpEntries.length > 0) {
17023
17732
  console.log("");
17024
17733
  console.log(
17025
- " " + chalk13.dim("node9 audit --deny") + chalk13.dim(" \xB7 ") + chalk13.dim("node9 report --period today|7d|30d|month --no-tests")
17734
+ " " + chalk13.red.bold("\u26A0\uFE0F Response DLP") + chalk13.dim(" \xB7 ") + chalk13.red(
17735
+ `${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
17736
+ )
17026
17737
  );
17027
- console.log("");
17028
- });
17738
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(60, W - 4))));
17739
+ console.log(
17740
+ " " + chalk13.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
17741
+ );
17742
+ console.log(" " + chalk13.yellow("Rotate affected keys immediately."));
17743
+ for (const e of responseDlpEntries.slice(0, 5)) {
17744
+ const ts = chalk13.dim(fmtDate(e.ts) + " ");
17745
+ const pattern = chalk13.red(e.dlpPattern ?? "DLP");
17746
+ const sample = chalk13.gray(e.dlpSample ?? "");
17747
+ console.log(` ${ts}${pattern} ${sample}`);
17748
+ }
17749
+ if (responseDlpEntries.length > 5) {
17750
+ console.log(chalk13.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
17751
+ }
17752
+ }
17753
+ console.log("");
17754
+ console.log(
17755
+ " " + chalk13.dim("node9 audit --deny") + chalk13.dim(" \xB7 ") + chalk13.dim("node9 report --period today|7d|30d|month --no-tests")
17756
+ );
17757
+ console.log("");
17029
17758
  }
17030
17759
 
17031
17760
  // src/cli/commands/daemon-cmd.ts