@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.js CHANGED
@@ -118,12 +118,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
118
118
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
119
119
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
120
120
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
121
+ const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
121
122
  appendToLog(LOCAL_AUDIT_LOG, {
122
123
  ts: (/* @__PURE__ */ new Date()).toISOString(),
123
124
  tool: toolName,
124
125
  ...argsField,
125
126
  decision,
126
127
  checkedBy,
128
+ ...ruleNameField,
127
129
  ...testRun,
128
130
  agent: meta?.agent,
129
131
  mcpServer: meta?.mcpServer,
@@ -707,7 +709,12 @@ function analyzeFsOperationImpl(command) {
707
709
  for (const p of paths) {
708
710
  for (const sp of SENSITIVE_PATH_RULES) {
709
711
  if (sp.match(p)) {
710
- result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
712
+ result = {
713
+ ruleName: sp.rule,
714
+ verdict: sp.verdict ?? "block",
715
+ reason: sp.reason,
716
+ path: p
717
+ };
711
718
  return false;
712
719
  }
713
720
  }
@@ -2367,15 +2374,50 @@ var init_dist = __esm({
2367
2374
  match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
2368
2375
  },
2369
2376
  {
2377
+ // Mirrors the JSON shield's `.env` pattern (project-jail.json's
2378
+ // review-read-env-any-tool) so the AST FS-op path catches the
2379
+ // same set the regex shield does — including Next.js / Vite's
2380
+ // `.env.<env>.local` double-suffix overrides which are commonly
2381
+ // gitignored AND commonly contain real secrets.
2382
+ //
2383
+ // Intentional non-matches (dev fixtures): .env.example, .env.sample,
2384
+ // .env.template, .env.test, .envrc. See shields.test.ts:983-995
2385
+ // for the canonical test-asserted contract.
2370
2386
  rule: "shield:project-jail:block-read-env",
2371
2387
  reason: "Reading .env files is blocked by project-jail shield",
2372
- match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
2388
+ match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
2389
+ p
2390
+ )
2373
2391
  },
2374
2392
  {
2375
- rule: "shield:project-jail:block-read-credentials",
2376
- reason: "Reading credential files is blocked by project-jail shield",
2377
- match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
2378
- p
2393
+ // verdict: 'review' (not 'block') is a deliberate design choice
2394
+ // documented in commit 29327a8. SSH keys and AWS credentials are
2395
+ // cryptographic material with no legitimate read use-case for
2396
+ // an AI agent → hard `block`. But .netrc / .npmrc / .docker /
2397
+ // .kube / gcloud are CONFIG files that hold tokens AND have
2398
+ // legitimate diagnostic reads ("which registry am I configured
2399
+ // for", "what cluster am I on"). Hard-blocking those creates
2400
+ // friction without much safety win because the review gate
2401
+ // still catches genuine exfiltration attempts.
2402
+ //
2403
+ // The review gate FAILS CLOSED on timeout (daemon.approvalTimeoutMs
2404
+ // returns a deny verdict via the orchestrator's timeout branch),
2405
+ // so a stuck or unattended approval does NOT silently grant
2406
+ // credential access. If the threat model demands strict block,
2407
+ // a future per-shield strict-mode toggle is the right fix —
2408
+ // not a regex-level upgrade here.
2409
+ rule: "shield:project-jail:review-read-credentials",
2410
+ reason: "Reading credential files requires approval (project-jail shield)",
2411
+ verdict: "review",
2412
+ match: (p) => (
2413
+ // .kube/config holds Kubernetes cluster credentials and was
2414
+ // flagged as missing by the node9-pr-agent review (the comment
2415
+ // above mentioned .kube but the regex didn't include it — a
2416
+ // textbook code-comment vs code drift). The JSON shield's
2417
+ // review-read-credentials-any-tool already had it. Now aligned.
2418
+ /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials|\.kube[\\/]config)$/i.test(
2419
+ p
2420
+ )
2379
2421
  )
2380
2422
  }
2381
2423
  ];
@@ -2391,7 +2433,7 @@ var init_dist = __esm({
2391
2433
  "shield:project-jail:block-read-ssh",
2392
2434
  "shield:project-jail:block-read-aws",
2393
2435
  "shield:project-jail:block-read-env",
2394
- "shield:project-jail:block-read-credentials"
2436
+ "shield:project-jail:review-read-credentials"
2395
2437
  ]);
2396
2438
  FS_OP_CACHE_MAX = 5e3;
2397
2439
  fsOpCache = /* @__PURE__ */ new Map();
@@ -3079,7 +3121,7 @@ var init_dist = __esm({
3079
3121
  {
3080
3122
  field: "command",
3081
3123
  op: "matches",
3082
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
3124
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
3083
3125
  flags: "i"
3084
3126
  }
3085
3127
  ],
@@ -3093,7 +3135,7 @@ var init_dist = __esm({
3093
3135
  {
3094
3136
  field: "command",
3095
3137
  op: "matches",
3096
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
3138
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
3097
3139
  flags: "i"
3098
3140
  }
3099
3141
  ],
@@ -3115,7 +3157,7 @@ var init_dist = __esm({
3115
3157
  reason: "Reading .env files is blocked by project-jail shield"
3116
3158
  },
3117
3159
  {
3118
- name: "shield:project-jail:block-read-credentials",
3160
+ name: "shield:project-jail:review-read-credentials",
3119
3161
  tool: "bash",
3120
3162
  conditions: [
3121
3163
  {
@@ -3125,8 +3167,64 @@ var init_dist = __esm({
3125
3167
  flags: "i"
3126
3168
  }
3127
3169
  ],
3170
+ verdict: "review",
3171
+ reason: "Reading credential files requires approval (project-jail shield)"
3172
+ },
3173
+ {
3174
+ name: "shield:project-jail:block-read-ssh-any-tool",
3175
+ tool: "*",
3176
+ conditions: [
3177
+ {
3178
+ field: "file_path",
3179
+ op: "matches",
3180
+ value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
3181
+ flags: "i"
3182
+ }
3183
+ ],
3184
+ verdict: "block",
3185
+ reason: "Reading SSH private keys is blocked by project-jail shield"
3186
+ },
3187
+ {
3188
+ name: "shield:project-jail:block-read-aws-any-tool",
3189
+ tool: "*",
3190
+ conditions: [
3191
+ {
3192
+ field: "file_path",
3193
+ op: "matches",
3194
+ value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
3195
+ flags: "i"
3196
+ }
3197
+ ],
3128
3198
  verdict: "block",
3129
- reason: "Reading credential files is blocked by project-jail shield"
3199
+ reason: "Reading AWS credentials is blocked by project-jail shield"
3200
+ },
3201
+ {
3202
+ name: "shield:project-jail:review-read-env-any-tool",
3203
+ tool: "*",
3204
+ conditions: [
3205
+ {
3206
+ field: "file_path",
3207
+ op: "matches",
3208
+ value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
3209
+ flags: "i"
3210
+ }
3211
+ ],
3212
+ verdict: "review",
3213
+ reason: "Reading .env files requires approval (project-jail shield)"
3214
+ },
3215
+ {
3216
+ name: "shield:project-jail:review-read-credentials-any-tool",
3217
+ tool: "*",
3218
+ conditions: [
3219
+ {
3220
+ field: "file_path",
3221
+ op: "matches",
3222
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
3223
+ flags: "i"
3224
+ }
3225
+ ],
3226
+ verdict: "review",
3227
+ reason: "Reading credential files requires approval (project-jail shield)"
3130
3228
  }
3131
3229
  ],
3132
3230
  dangerousWords: []
@@ -5793,7 +5891,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5793
5891
  args,
5794
5892
  "deny",
5795
5893
  "smart-rule-block-override",
5796
- meta,
5894
+ // Same rationale as the smart-rule-block path above —
5895
+ // pass the specific rule name so [2] SHIELDS can
5896
+ // attribute this override-block to its owning shield.
5897
+ { ...meta, ruleName: policyResult.ruleName },
5797
5898
  hashAuditArgs
5798
5899
  );
5799
5900
  if (approvers.cloud && creds?.apiKey)
@@ -5823,7 +5924,20 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5823
5924
  }
5824
5925
  } else {
5825
5926
  if (!isManual)
5826
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
5927
+ appendLocalAudit(
5928
+ toolName,
5929
+ args,
5930
+ "deny",
5931
+ "smart-rule-block",
5932
+ // Include policyResult.ruleName so the [2] Report SHIELDS
5933
+ // panel can attribute this block to its specific shield
5934
+ // (e.g. `shield:project-jail:block-read-ssh`) via the
5935
+ // rule→shield map. checkedBy stays as the generic
5936
+ // `smart-rule-block` for backward compat with existing
5937
+ // log readers.
5938
+ { ...meta, ruleName: policyResult.ruleName },
5939
+ hashAuditArgs
5940
+ );
5827
5941
  if (approvers.cloud && creds?.apiKey)
5828
5942
  auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
5829
5943
  ruleName: policyResult.ruleName,
@@ -7645,11 +7759,62 @@ function computeLoopWaste(loops, totalToolCalls) {
7645
7759
  const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
7646
7760
  return { wastedCalls, wastePct };
7647
7761
  }
7648
- var import_chalk3;
7762
+ function rollupByShield(sections, topRulesPerShield = 3) {
7763
+ const out = [];
7764
+ for (const section of sections) {
7765
+ if (section.sourceType !== "shield") continue;
7766
+ if (!section.shieldKey) continue;
7767
+ const totalCatches = section.blockedCount + section.reviewCount;
7768
+ 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);
7769
+ out.push({
7770
+ shieldName: section.shieldKey,
7771
+ totalCatches,
7772
+ blockCatches: section.blockedCount,
7773
+ reviewCatches: section.reviewCount,
7774
+ topRuleLabels
7775
+ });
7776
+ }
7777
+ return out.sort((a, b) => b.totalCatches - a.totalCatches);
7778
+ }
7779
+ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
7780
+ const inner = width - 4;
7781
+ const out = [];
7782
+ const titlePad = ` ${title} `;
7783
+ const titleSegment = titlePad.length <= inner ? titlePad : titlePad.slice(0, inner);
7784
+ const dashFill = "\u2500".repeat(Math.max(0, inner - titleSegment.length));
7785
+ out.push(import_chalk3.default.dim("\u256D\u2500") + import_chalk3.default.bold(titleSegment) + import_chalk3.default.dim(`${dashFill}\u2500\u256E`));
7786
+ for (const line of bodyLines) {
7787
+ const padding = " ".repeat(Math.max(0, inner - line.width));
7788
+ out.push(import_chalk3.default.dim("\u2502 ") + line.rendered + padding + import_chalk3.default.dim(" \u2502"));
7789
+ }
7790
+ out.push(import_chalk3.default.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
7791
+ return out;
7792
+ }
7793
+ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
7794
+ const t = new Date(timestamp).getTime();
7795
+ if (Number.isNaN(t)) return "?";
7796
+ const days = Math.floor((now.getTime() - t) / 864e5);
7797
+ if (days < 1) return "today";
7798
+ if (days > 90) return "90d+";
7799
+ return `${days}d`;
7800
+ }
7801
+ var import_chalk3, PANEL_WIDTH;
7649
7802
  var init_scan_derive = __esm({
7650
7803
  "src/cli/render/scan-derive.ts"() {
7651
7804
  "use strict";
7652
7805
  import_chalk3 = __toESM(require("chalk"));
7806
+ PANEL_WIDTH = 76;
7807
+ }
7808
+ });
7809
+
7810
+ // src/protection.ts
7811
+ var PROTECTIVE_SHIELD_DISCOUNTS;
7812
+ var init_protection = __esm({
7813
+ "src/protection.ts"() {
7814
+ "use strict";
7815
+ PROTECTIVE_SHIELD_DISCOUNTS = {
7816
+ "project-jail": 0.7
7817
+ };
7653
7818
  }
7654
7819
  });
7655
7820
 
@@ -7836,6 +8001,7 @@ async function ensurePricingLoaded() {
7836
8001
  if (fromDisk && Object.keys(fromDisk).length > 0) {
7837
8002
  memCache = fromDisk;
7838
8003
  memCacheAt = Date.now();
8004
+ lookupCache.clear();
7839
8005
  return;
7840
8006
  }
7841
8007
  const fetched = await fetchLiteLLMPricing();
@@ -7843,30 +8009,42 @@ async function ensurePricingLoaded() {
7843
8009
  memCache = fetched;
7844
8010
  memCacheAt = Date.now();
7845
8011
  writeCache(fetched);
8012
+ lookupCache.clear();
7846
8013
  return;
7847
8014
  }
7848
8015
  memCache = { ...BUNDLED_PRICING };
7849
8016
  memCacheAt = Date.now();
8017
+ lookupCache.clear();
7850
8018
  }
7851
8019
  function pricingFor(model) {
7852
8020
  const norm = normalizeModel(model);
8021
+ const cached = lookupCache.get(norm);
8022
+ if (cached !== void 0) return cached;
7853
8023
  const sources = [];
7854
8024
  if (memCache) sources.push(memCache);
7855
8025
  sources.push(BUNDLED_PRICING);
8026
+ let resolved = null;
7856
8027
  for (const source of sources) {
7857
8028
  const exact = source[norm];
7858
- if (exact) return exact;
8029
+ if (exact) {
8030
+ resolved = exact;
8031
+ break;
8032
+ }
7859
8033
  let best = null;
7860
8034
  for (const key of Object.keys(source)) {
7861
8035
  if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
7862
8036
  best = key;
7863
8037
  }
7864
8038
  }
7865
- if (best) return source[best];
8039
+ if (best) {
8040
+ resolved = source[best];
8041
+ break;
8042
+ }
7866
8043
  }
7867
- return null;
8044
+ lookupCache.set(norm, resolved);
8045
+ return resolved;
7868
8046
  }
7869
- var import_fs15, import_path17, import_os14, LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt;
8047
+ var import_fs15, import_path17, import_os14, LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, lookupCache;
7870
8048
  var init_litellm = __esm({
7871
8049
  "src/pricing/litellm.ts"() {
7872
8050
  "use strict";
@@ -7903,6 +8081,7 @@ var init_litellm = __esm({
7903
8081
  TTL_MS = 24 * 60 * 60 * 1e3;
7904
8082
  memCache = null;
7905
8083
  memCacheAt = 0;
8084
+ lookupCache = /* @__PURE__ */ new Map();
7906
8085
  }
7907
8086
  });
7908
8087
 
@@ -7969,7 +8148,7 @@ function parseJSONLFile(filePath, fallbackWorkingDir) {
7969
8148
  }
7970
8149
  return daily;
7971
8150
  }
7972
- function collectEntries() {
8151
+ function collectEntries(sinceMs) {
7973
8152
  const projectsDir = import_path18.default.join(import_os15.default.homedir(), ".claude", "projects");
7974
8153
  if (!import_fs16.default.existsSync(projectsDir)) return [];
7975
8154
  const combined = /* @__PURE__ */ new Map();
@@ -7994,7 +8173,15 @@ function collectEntries() {
7994
8173
  }
7995
8174
  const fallbackWorkingDir = decodeProjectDirName(dir);
7996
8175
  for (const file of files) {
7997
- const entries = parseJSONLFile(import_path18.default.join(dirPath, file), fallbackWorkingDir);
8176
+ const filePath = import_path18.default.join(dirPath, file);
8177
+ if (sinceMs !== void 0) {
8178
+ try {
8179
+ if (import_fs16.default.statSync(filePath).mtimeMs < sinceMs) continue;
8180
+ } catch {
8181
+ continue;
8182
+ }
8183
+ }
8184
+ const entries = parseJSONLFile(filePath, fallbackWorkingDir);
7998
8185
  for (const [key, e] of entries) {
7999
8186
  const prev = combined.get(key);
8000
8187
  if (prev) {
@@ -8829,7 +9016,16 @@ function buildRecurringPatternSet(findings) {
8829
9016
  }
8830
9017
  return recurring;
8831
9018
  }
8832
- function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result) {
9019
+ function emptyScanDedup() {
9020
+ return { findingsKeys: /* @__PURE__ */ new Set(), dlpKeys: /* @__PURE__ */ new Set() };
9021
+ }
9022
+ function findingKey(ruleName, inputPreview, projLabel) {
9023
+ return `${ruleName ?? "<unnamed>"}|${inputPreview}|${projLabel}`;
9024
+ }
9025
+ function dlpKey(patternName, redactedSample, projLabel) {
9026
+ return `${patternName}|${redactedSample}|${projLabel}`;
9027
+ }
9028
+ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result, dedup) {
8833
9029
  const fsVerdict = analyzeFsOperation(command);
8834
9030
  if (!fsVerdict) return false;
8835
9031
  const synthRule = {
@@ -8852,10 +9048,9 @@ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sess
8852
9048
  rule: synthRule
8853
9049
  };
8854
9050
  const inputPreview = preview(input, 120);
8855
- const isDupe = result.findings.some(
8856
- (f) => f.source.rule.name === synthRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
8857
- );
8858
- if (!isDupe) {
9051
+ const k = findingKey(synthRule.name, inputPreview, projLabel);
9052
+ if (!dedup.findingsKeys.has(k)) {
9053
+ dedup.findingsKeys.add(k);
8859
9054
  result.findings.push({
8860
9055
  source: synthSource,
8861
9056
  toolName,
@@ -8940,22 +9135,15 @@ function buildRuleSources() {
8940
9135
  sources.push({ shieldName, shieldLabel: shieldName, sourceType: "shield", rule });
8941
9136
  }
8942
9137
  }
8943
- try {
8944
- const config = getConfig();
8945
- for (const rule of config.policy.smartRules) {
8946
- if (!rule.name) continue;
8947
- if (rule.name.startsWith("shield:")) continue;
8948
- const isCloud = rule.name.startsWith("cloud:");
8949
- const isDefault = DEFAULT_RULE_NAMES.has(rule.name);
8950
- const sourceType = isCloud ? "user" : isDefault ? "default" : "user";
8951
- sources.push({
8952
- shieldName: isCloud ? "cloud" : isDefault ? "default" : "custom",
8953
- shieldLabel: isCloud ? "Cloud Policy" : isDefault ? "Default Rules" : "Your Rules",
8954
- sourceType,
8955
- rule
8956
- });
8957
- }
8958
- } catch {
9138
+ for (const rule of DEFAULT_CONFIG.policy.smartRules) {
9139
+ if (!rule.name) continue;
9140
+ if (rule.name.startsWith("shield:")) continue;
9141
+ sources.push({
9142
+ shieldName: "default",
9143
+ shieldLabel: "Default Rules",
9144
+ sourceType: "default",
9145
+ rule
9146
+ });
8959
9147
  }
8960
9148
  return sources;
8961
9149
  }
@@ -9041,178 +9229,53 @@ function renderProgressBar(done, total, lines) {
9041
9229
  `\r ${import_chalk5.default.cyan("Scanning")} [${import_chalk5.default.cyan(bar)}] ${import_chalk5.default.dim(fileLabel)}${lineLabel} `
9042
9230
  );
9043
9231
  }
9044
- function scanClaudeHistory(startDate, onProgress, onLine) {
9045
- const projectsDir = import_path21.default.join(import_os18.default.homedir(), ".claude", "projects");
9046
- const result = {
9047
- filesScanned: 0,
9048
- sessions: 0,
9049
- totalToolCalls: 0,
9050
- bashCalls: 0,
9051
- findings: [],
9052
- dlpFindings: [],
9053
- loopFindings: [],
9054
- totalCostUSD: 0,
9055
- firstDate: null,
9056
- lastDate: null,
9057
- sessionsWithEarlySecrets: 0
9058
- };
9059
- if (!import_fs19.default.existsSync(projectsDir)) return result;
9060
- let projDirs;
9232
+ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, result, dedup, onProgress, onLine) {
9233
+ result.filesScanned++;
9234
+ result.sessions++;
9235
+ onProgress?.(result.filesScanned);
9236
+ const sessionId = file.replace(/\.jsonl$/, "");
9237
+ let raw;
9061
9238
  try {
9062
- projDirs = import_fs19.default.readdirSync(projectsDir);
9239
+ raw = import_fs19.default.readFileSync(import_path21.default.join(projPath, file), "utf-8");
9063
9240
  } catch {
9064
- return result;
9241
+ return;
9065
9242
  }
9066
- const ruleSources = buildRuleSources();
9067
- for (const proj of projDirs) {
9068
- const projPath = import_path21.default.join(projectsDir, proj);
9243
+ const sessionCalls = [];
9244
+ const toolUseFilePaths = /* @__PURE__ */ new Map();
9245
+ let firstDlpTs = null;
9246
+ let firstEditTs = null;
9247
+ for (const line of raw.split("\n")) {
9248
+ if (!line.trim()) continue;
9249
+ onLine?.();
9250
+ let entry;
9069
9251
  try {
9070
- if (!import_fs19.default.statSync(projPath).isDirectory()) continue;
9252
+ entry = JSON.parse(line);
9071
9253
  } catch {
9072
9254
  continue;
9073
9255
  }
9074
- const projLabel = stripTerminalEscapes(
9075
- decodeURIComponent(proj).replace(import_os18.default.homedir(), "~")
9076
- ).slice(0, 40);
9077
- let files;
9078
- try {
9079
- files = import_fs19.default.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
9080
- } catch {
9081
- continue;
9256
+ if (entry.type !== "assistant" && entry.type !== "user") continue;
9257
+ if (startDate && entry.timestamp) {
9258
+ if (new Date(entry.timestamp) < startDate) continue;
9082
9259
  }
9083
- for (const file of files) {
9084
- result.filesScanned++;
9085
- result.sessions++;
9086
- onProgress?.(result.filesScanned);
9087
- const sessionId = file.replace(/\.jsonl$/, "");
9088
- let raw;
9089
- try {
9090
- raw = import_fs19.default.readFileSync(import_path21.default.join(projPath, file), "utf-8");
9091
- } catch {
9092
- continue;
9093
- }
9094
- const sessionCalls = [];
9095
- const toolUseFilePaths = /* @__PURE__ */ new Map();
9096
- let firstDlpTs = null;
9097
- let firstEditTs = null;
9098
- for (const line of raw.split("\n")) {
9099
- if (!line.trim()) continue;
9100
- onLine?.();
9101
- let entry;
9102
- try {
9103
- entry = JSON.parse(line);
9104
- } catch {
9105
- continue;
9106
- }
9107
- if (entry.type !== "assistant" && entry.type !== "user") continue;
9108
- if (startDate && entry.timestamp) {
9109
- if (new Date(entry.timestamp) < startDate) continue;
9110
- }
9111
- if (entry.timestamp) {
9112
- if (!result.firstDate || entry.timestamp < result.firstDate)
9113
- result.firstDate = entry.timestamp;
9114
- if (!result.lastDate || entry.timestamp > result.lastDate)
9115
- result.lastDate = entry.timestamp;
9116
- }
9117
- if (entry.type === "user") {
9118
- const content2 = entry.message?.content;
9119
- if (Array.isArray(content2)) {
9120
- const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
9121
- if (text) {
9122
- const dlpMatch = scanArgs({ text });
9123
- if (dlpMatch) {
9124
- const isDupe = result.dlpFindings.some(
9125
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9126
- );
9127
- if (!isDupe) {
9128
- result.dlpFindings.push({
9129
- patternName: dlpMatch.patternName,
9130
- redactedSample: dlpMatch.redactedSample,
9131
- toolName: "user-prompt",
9132
- timestamp: entry.timestamp ?? "",
9133
- project: projLabel,
9134
- sessionId,
9135
- agent: "claude"
9136
- });
9137
- }
9138
- }
9139
- }
9140
- for (const block of content2) {
9141
- if (block.type !== "tool_result") continue;
9142
- const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
9143
- if (filePath) {
9144
- const ext = import_path21.default.extname(filePath).toLowerCase();
9145
- if (CODE_EXTENSIONS.has(ext)) continue;
9146
- }
9147
- const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
9148
- if (!resultText) continue;
9149
- if (isNode9SelfOutput(resultText)) continue;
9150
- const dlpMatch = scanArgs({ text: resultText });
9151
- if (dlpMatch) {
9152
- if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
9153
- if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9154
- const isDupe = result.dlpFindings.some(
9155
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9156
- );
9157
- if (!isDupe) {
9158
- result.dlpFindings.push({
9159
- patternName: dlpMatch.patternName,
9160
- redactedSample: dlpMatch.redactedSample,
9161
- toolName: "tool-result",
9162
- timestamp: entry.timestamp ?? "",
9163
- project: projLabel,
9164
- sessionId,
9165
- agent: "claude"
9166
- });
9167
- }
9168
- }
9169
- }
9170
- }
9171
- continue;
9172
- }
9173
- const usage = entry.message?.usage;
9174
- const model = entry.message?.model;
9175
- if (usage && model) {
9176
- const p = claudeModelPrice(model);
9177
- if (p) {
9178
- 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;
9179
- }
9180
- }
9181
- const content = entry.message?.content;
9182
- if (!Array.isArray(content)) continue;
9183
- for (const block of content) {
9184
- if (block.type !== "tool_use") continue;
9185
- result.totalToolCalls++;
9186
- const toolName = block.name ?? "";
9187
- const toolNameLower = toolName.toLowerCase();
9188
- const input = block.input ?? {};
9189
- if (block.id && typeof input.file_path === "string") {
9190
- toolUseFilePaths.set(block.id, input.file_path);
9191
- }
9192
- sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
9193
- if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
9194
- result.bashCalls++;
9195
- }
9196
- if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
9197
- firstEditTs = entry.timestamp ?? null;
9198
- }
9199
- const rawCmd = String(input.command ?? "").trimStart();
9200
- if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
9201
- continue;
9202
- const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
9203
- const inputFileExt = inputFilePath ? import_path21.default.extname(inputFilePath).toLowerCase() : "";
9204
- if (CODE_EXTENSIONS.has(inputFileExt)) continue;
9205
- const dlpMatch = scanArgs(input);
9260
+ if (entry.timestamp) {
9261
+ if (!result.firstDate || entry.timestamp < result.firstDate)
9262
+ result.firstDate = entry.timestamp;
9263
+ if (!result.lastDate || entry.timestamp > result.lastDate) result.lastDate = entry.timestamp;
9264
+ }
9265
+ if (entry.type === "user") {
9266
+ const content2 = entry.message?.content;
9267
+ if (Array.isArray(content2)) {
9268
+ const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
9269
+ if (text) {
9270
+ const dlpMatch = scanArgs({ text });
9206
9271
  if (dlpMatch) {
9207
- if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9208
- const isDupe = result.dlpFindings.some(
9209
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9210
- );
9211
- if (!isDupe) {
9272
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9273
+ if (!dedup.dlpKeys.has(k)) {
9274
+ dedup.dlpKeys.add(k);
9212
9275
  result.dlpFindings.push({
9213
9276
  patternName: dlpMatch.patternName,
9214
9277
  redactedSample: dlpMatch.redactedSample,
9215
- toolName,
9278
+ toolName: "user-prompt",
9216
9279
  timestamp: entry.timestamp ?? "",
9217
9280
  project: projLabel,
9218
9281
  sessionId,
@@ -9220,102 +9283,252 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
9220
9283
  });
9221
9284
  }
9222
9285
  }
9223
- let astFsMatched = false;
9224
- const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
9225
- if (astRanForBash) {
9226
- astFsMatched = pushFsOpAstFinding(
9227
- String(input.command ?? ""),
9228
- toolName,
9229
- input,
9230
- entry.timestamp ?? "",
9231
- projLabel,
9232
- sessionId,
9233
- "claude",
9234
- result
9235
- );
9286
+ }
9287
+ for (const block of content2) {
9288
+ if (block.type !== "tool_result") continue;
9289
+ const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
9290
+ if (filePath) {
9291
+ const ext = import_path21.default.extname(filePath).toLowerCase();
9292
+ if (CODE_EXTENSIONS.has(ext)) continue;
9236
9293
  }
9237
- let ruleMatched = astFsMatched;
9238
- for (const source of ruleSources) {
9239
- const { rule } = source;
9240
- if (rule.verdict === "allow") continue;
9241
- if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
9242
- if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9243
- if (!evaluateSmartConditions(input, rule)) continue;
9244
- const inputPreview = preview(input, 120);
9245
- const isDupe = result.findings.some(
9246
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9247
- );
9248
- if (!isDupe) {
9249
- result.findings.push({
9250
- source,
9251
- toolName,
9252
- input,
9294
+ const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
9295
+ if (!resultText) continue;
9296
+ if (isNode9SelfOutput(resultText)) continue;
9297
+ const dlpMatch = scanArgs({ text: resultText });
9298
+ if (dlpMatch) {
9299
+ if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
9300
+ if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9301
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9302
+ if (!dedup.dlpKeys.has(k)) {
9303
+ dedup.dlpKeys.add(k);
9304
+ result.dlpFindings.push({
9305
+ patternName: dlpMatch.patternName,
9306
+ redactedSample: dlpMatch.redactedSample,
9307
+ toolName: "tool-result",
9253
9308
  timestamp: entry.timestamp ?? "",
9254
9309
  project: projLabel,
9255
9310
  sessionId,
9256
9311
  agent: "claude"
9257
9312
  });
9258
9313
  }
9259
- ruleMatched = true;
9260
- break;
9261
- }
9262
- if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
9263
- const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
9264
- if (shellVerdict) {
9265
- const astRule = {
9266
- name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
9267
- tool: "bash",
9268
- conditions: [],
9269
- verdict: shellVerdict,
9270
- reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9271
- };
9272
- const inputPreview = preview(input, 120);
9273
- const isDupe = result.findings.some(
9274
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9275
- );
9276
- if (!isDupe) {
9277
- result.findings.push({
9278
- source: {
9279
- shieldName: "bash-safe",
9280
- shieldLabel: "bash-safe (AST)",
9281
- sourceType: "shield",
9282
- rule: astRule
9283
- },
9284
- toolName,
9285
- input,
9286
- timestamp: entry.timestamp ?? "",
9287
- project: projLabel,
9288
- sessionId,
9289
- agent: "claude"
9290
- });
9291
- }
9292
- }
9293
9314
  }
9294
9315
  }
9295
9316
  }
9296
- result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "claude"));
9297
- if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
9298
- result.sessionsWithEarlySecrets++;
9317
+ continue;
9318
+ }
9319
+ const usage = entry.message?.usage;
9320
+ const model = entry.message?.model;
9321
+ if (usage && model) {
9322
+ const p = claudeModelPrice(model);
9323
+ if (p) {
9324
+ 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;
9299
9325
  }
9300
9326
  }
9301
- }
9302
- return result;
9303
- }
9304
- function scanGeminiHistory(startDate, onProgress, onLine) {
9305
- const tmpDir = import_path21.default.join(import_os18.default.homedir(), ".gemini", "tmp");
9306
- const result = {
9307
- filesScanned: 0,
9308
- sessions: 0,
9309
- totalToolCalls: 0,
9310
- bashCalls: 0,
9311
- findings: [],
9312
- dlpFindings: [],
9327
+ const content = entry.message?.content;
9328
+ if (!Array.isArray(content)) continue;
9329
+ for (const block of content) {
9330
+ if (block.type !== "tool_use") continue;
9331
+ result.totalToolCalls++;
9332
+ const toolName = block.name ?? "";
9333
+ const toolNameLower = toolName.toLowerCase();
9334
+ const input = block.input ?? {};
9335
+ if (block.id && typeof input.file_path === "string") {
9336
+ toolUseFilePaths.set(block.id, input.file_path);
9337
+ }
9338
+ sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
9339
+ if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
9340
+ result.bashCalls++;
9341
+ }
9342
+ if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
9343
+ firstEditTs = entry.timestamp ?? null;
9344
+ }
9345
+ const rawCmd = String(input.command ?? "").trimStart();
9346
+ if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
9347
+ const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
9348
+ const inputFileExt = inputFilePath ? import_path21.default.extname(inputFilePath).toLowerCase() : "";
9349
+ if (CODE_EXTENSIONS.has(inputFileExt)) continue;
9350
+ const dlpMatch = scanArgs(input);
9351
+ if (dlpMatch) {
9352
+ if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9353
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9354
+ if (!dedup.dlpKeys.has(k)) {
9355
+ dedup.dlpKeys.add(k);
9356
+ result.dlpFindings.push({
9357
+ patternName: dlpMatch.patternName,
9358
+ redactedSample: dlpMatch.redactedSample,
9359
+ toolName,
9360
+ timestamp: entry.timestamp ?? "",
9361
+ project: projLabel,
9362
+ sessionId,
9363
+ agent: "claude"
9364
+ });
9365
+ }
9366
+ }
9367
+ let astFsMatched = false;
9368
+ const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
9369
+ if (astRanForBash) {
9370
+ astFsMatched = pushFsOpAstFinding(
9371
+ String(input.command ?? ""),
9372
+ toolName,
9373
+ input,
9374
+ entry.timestamp ?? "",
9375
+ projLabel,
9376
+ sessionId,
9377
+ "claude",
9378
+ result,
9379
+ dedup
9380
+ );
9381
+ }
9382
+ let ruleMatched = astFsMatched;
9383
+ for (const source of ruleSources) {
9384
+ const { rule } = source;
9385
+ if (rule.verdict === "allow") continue;
9386
+ if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
9387
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9388
+ if (!evaluateSmartConditions(input, rule)) continue;
9389
+ const inputPreview = preview(input, 120);
9390
+ const k = findingKey(rule.name, inputPreview, projLabel);
9391
+ if (!dedup.findingsKeys.has(k)) {
9392
+ dedup.findingsKeys.add(k);
9393
+ result.findings.push({
9394
+ source,
9395
+ toolName,
9396
+ input,
9397
+ timestamp: entry.timestamp ?? "",
9398
+ project: projLabel,
9399
+ sessionId,
9400
+ agent: "claude"
9401
+ });
9402
+ }
9403
+ ruleMatched = true;
9404
+ break;
9405
+ }
9406
+ if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
9407
+ const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
9408
+ if (shellVerdict) {
9409
+ const astRule = {
9410
+ name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
9411
+ tool: "bash",
9412
+ conditions: [],
9413
+ verdict: shellVerdict,
9414
+ reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9415
+ };
9416
+ const inputPreview = preview(input, 120);
9417
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9418
+ if (!dedup.findingsKeys.has(k)) {
9419
+ dedup.findingsKeys.add(k);
9420
+ result.findings.push({
9421
+ source: {
9422
+ shieldName: "bash-safe",
9423
+ shieldLabel: "bash-safe (AST)",
9424
+ sourceType: "shield",
9425
+ rule: astRule
9426
+ },
9427
+ toolName,
9428
+ input,
9429
+ timestamp: entry.timestamp ?? "",
9430
+ project: projLabel,
9431
+ sessionId,
9432
+ agent: "claude"
9433
+ });
9434
+ }
9435
+ }
9436
+ }
9437
+ }
9438
+ }
9439
+ result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "claude"));
9440
+ if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
9441
+ result.sessionsWithEarlySecrets++;
9442
+ }
9443
+ }
9444
+ function processClaudeProject(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
9445
+ const projPath = import_path21.default.join(projectsDir, proj);
9446
+ try {
9447
+ if (!import_fs19.default.statSync(projPath).isDirectory()) return;
9448
+ } catch {
9449
+ return;
9450
+ }
9451
+ const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(import_os18.default.homedir(), "~")).slice(
9452
+ 0,
9453
+ 40
9454
+ );
9455
+ let files;
9456
+ try {
9457
+ files = import_fs19.default.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
9458
+ } catch {
9459
+ return;
9460
+ }
9461
+ for (const file of files) {
9462
+ processClaudeFile(
9463
+ file,
9464
+ projPath,
9465
+ projLabel,
9466
+ ruleSources,
9467
+ startDate,
9468
+ result,
9469
+ dedup,
9470
+ onProgress,
9471
+ onLine
9472
+ );
9473
+ }
9474
+ }
9475
+ function emptyClaudeScan() {
9476
+ return {
9477
+ filesScanned: 0,
9478
+ sessions: 0,
9479
+ totalToolCalls: 0,
9480
+ bashCalls: 0,
9481
+ findings: [],
9482
+ dlpFindings: [],
9483
+ loopFindings: [],
9484
+ totalCostUSD: 0,
9485
+ firstDate: null,
9486
+ lastDate: null,
9487
+ sessionsWithEarlySecrets: 0
9488
+ };
9489
+ }
9490
+ function scanClaudeHistory(startDate, onProgress, onLine) {
9491
+ const projectsDir = import_path21.default.join(import_os18.default.homedir(), ".claude", "projects");
9492
+ const result = emptyClaudeScan();
9493
+ if (!import_fs19.default.existsSync(projectsDir)) return result;
9494
+ let projDirs;
9495
+ try {
9496
+ projDirs = import_fs19.default.readdirSync(projectsDir);
9497
+ } catch {
9498
+ return result;
9499
+ }
9500
+ const ruleSources = buildRuleSources();
9501
+ const dedup = emptyScanDedup();
9502
+ for (const proj of projDirs) {
9503
+ processClaudeProject(
9504
+ proj,
9505
+ projectsDir,
9506
+ ruleSources,
9507
+ startDate,
9508
+ result,
9509
+ dedup,
9510
+ onProgress,
9511
+ onLine
9512
+ );
9513
+ }
9514
+ return result;
9515
+ }
9516
+ function scanGeminiHistory(startDate, onProgress, onLine) {
9517
+ const tmpDir = import_path21.default.join(import_os18.default.homedir(), ".gemini", "tmp");
9518
+ const result = {
9519
+ filesScanned: 0,
9520
+ sessions: 0,
9521
+ totalToolCalls: 0,
9522
+ bashCalls: 0,
9523
+ findings: [],
9524
+ dlpFindings: [],
9313
9525
  loopFindings: [],
9314
9526
  totalCostUSD: 0,
9315
9527
  firstDate: null,
9316
9528
  lastDate: null,
9317
9529
  sessionsWithEarlySecrets: 0
9318
9530
  };
9531
+ const dedup = emptyScanDedup();
9319
9532
  if (!import_fs19.default.existsSync(tmpDir)) return result;
9320
9533
  let slugDirs;
9321
9534
  try {
@@ -9372,10 +9585,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9372
9585
  if (text) {
9373
9586
  const dlpMatch = scanArgs({ text });
9374
9587
  if (dlpMatch) {
9375
- const isDupe = result.dlpFindings.some(
9376
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9377
- );
9378
- if (!isDupe) {
9588
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9589
+ if (!dedup.dlpKeys.has(k)) {
9590
+ dedup.dlpKeys.add(k);
9379
9591
  result.dlpFindings.push({
9380
9592
  patternName: dlpMatch.patternName,
9381
9593
  redactedSample: dlpMatch.redactedSample,
@@ -9420,10 +9632,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9420
9632
  continue;
9421
9633
  const dlpMatch = scanArgs(input);
9422
9634
  if (dlpMatch) {
9423
- const isDupe = result.dlpFindings.some(
9424
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9425
- );
9426
- if (!isDupe) {
9635
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9636
+ if (!dedup.dlpKeys.has(k)) {
9637
+ dedup.dlpKeys.add(k);
9427
9638
  result.dlpFindings.push({
9428
9639
  patternName: dlpMatch.patternName,
9429
9640
  redactedSample: dlpMatch.redactedSample,
@@ -9446,7 +9657,8 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9446
9657
  projLabel,
9447
9658
  sessionId,
9448
9659
  "gemini",
9449
- result
9660
+ result,
9661
+ dedup
9450
9662
  );
9451
9663
  }
9452
9664
  let ruleMatched = astFsMatched;
@@ -9457,10 +9669,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9457
9669
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9458
9670
  if (!evaluateSmartConditions(input, rule)) continue;
9459
9671
  const inputPreview = preview(input, 120);
9460
- const isDupe = result.findings.some(
9461
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9462
- );
9463
- if (!isDupe) {
9672
+ const k = findingKey(rule.name, inputPreview, projLabel);
9673
+ if (!dedup.findingsKeys.has(k)) {
9674
+ dedup.findingsKeys.add(k);
9464
9675
  result.findings.push({
9465
9676
  source,
9466
9677
  toolName,
@@ -9488,10 +9699,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9488
9699
  reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9489
9700
  };
9490
9701
  const inputPreview = preview(input, 120);
9491
- const isDupe = result.findings.some(
9492
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9493
- );
9494
- if (!isDupe) {
9702
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9703
+ if (!dedup.findingsKeys.has(k)) {
9704
+ dedup.findingsKeys.add(k);
9495
9705
  result.findings.push({
9496
9706
  source: {
9497
9707
  shieldName: "bash-safe",
@@ -9531,6 +9741,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9531
9741
  lastDate: null,
9532
9742
  sessionsWithEarlySecrets: 0
9533
9743
  };
9744
+ const dedup = emptyScanDedup();
9534
9745
  if (!import_fs19.default.existsSync(sessionsBase)) return result;
9535
9746
  const jsonlFiles = [];
9536
9747
  try {
@@ -9612,10 +9823,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9612
9823
  if (text) {
9613
9824
  const dlpMatch2 = scanArgs({ text });
9614
9825
  if (dlpMatch2) {
9615
- const isDupe = result.dlpFindings.some(
9616
- (f) => f.patternName === dlpMatch2.patternName && f.redactedSample === dlpMatch2.redactedSample && f.project === projLabel
9617
- );
9618
- if (!isDupe) {
9826
+ const k = dlpKey(dlpMatch2.patternName, dlpMatch2.redactedSample, projLabel);
9827
+ if (!dedup.dlpKeys.has(k)) {
9828
+ dedup.dlpKeys.add(k);
9619
9829
  result.dlpFindings.push({
9620
9830
  patternName: dlpMatch2.patternName,
9621
9831
  redactedSample: dlpMatch2.redactedSample,
@@ -9657,10 +9867,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9657
9867
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
9658
9868
  const dlpMatch = scanArgs(input);
9659
9869
  if (dlpMatch) {
9660
- const isDupe = result.dlpFindings.some(
9661
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9662
- );
9663
- if (!isDupe) {
9870
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9871
+ if (!dedup.dlpKeys.has(k)) {
9872
+ dedup.dlpKeys.add(k);
9664
9873
  result.dlpFindings.push({
9665
9874
  patternName: dlpMatch.patternName,
9666
9875
  redactedSample: dlpMatch.redactedSample,
@@ -9683,7 +9892,8 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9683
9892
  projLabel,
9684
9893
  sessionId,
9685
9894
  "codex",
9686
- result
9895
+ result,
9896
+ dedup
9687
9897
  );
9688
9898
  }
9689
9899
  let ruleMatched = astFsMatched;
@@ -9695,10 +9905,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9695
9905
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9696
9906
  if (!evaluateSmartConditions(input, rule)) continue;
9697
9907
  const inputPreview = preview(input, 120);
9698
- const isDupe = result.findings.some(
9699
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9700
- );
9701
- if (!isDupe) {
9908
+ const k = findingKey(rule.name, inputPreview, projLabel);
9909
+ if (!dedup.findingsKeys.has(k)) {
9910
+ dedup.findingsKeys.add(k);
9702
9911
  result.findings.push({
9703
9912
  source,
9704
9913
  toolName,
@@ -9723,10 +9932,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9723
9932
  reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9724
9933
  };
9725
9934
  const inputPreview = preview(input, 120);
9726
- const isDupe = result.findings.some(
9727
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9728
- );
9729
- if (!isDupe) {
9935
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9936
+ if (!dedup.findingsKeys.has(k)) {
9937
+ dedup.findingsKeys.add(k);
9730
9938
  result.findings.push({
9731
9939
  source: {
9732
9940
  shieldName: "bash-safe",
@@ -9757,6 +9965,7 @@ function scanShellConfig() {
9757
9965
  (f) => import_path21.default.join(home, f)
9758
9966
  );
9759
9967
  const findings = [];
9968
+ const seen = /* @__PURE__ */ new Set();
9760
9969
  for (const filePath of configFiles) {
9761
9970
  if (!import_fs19.default.existsSync(filePath)) continue;
9762
9971
  let lines;
@@ -9771,10 +9980,9 @@ function scanShellConfig() {
9771
9980
  if (!trimmed || trimmed.startsWith("#")) continue;
9772
9981
  const dlpMatch = scanArgs({ text: trimmed });
9773
9982
  if (!dlpMatch) continue;
9774
- const isDupe = findings.some(
9775
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === shortPath
9776
- );
9777
- if (!isDupe) {
9983
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, shortPath);
9984
+ if (!seen.has(k)) {
9985
+ seen.add(k);
9778
9986
  findings.push({
9779
9987
  patternName: dlpMatch.patternName,
9780
9988
  redactedSample: dlpMatch.redactedSample,
@@ -10033,6 +10241,263 @@ function renderNarrativeScorecard(input) {
10033
10241
  console.log(import_chalk5.default.dim("\u2192 github.com/node9-ai/node9-proxy"));
10034
10242
  console.log("");
10035
10243
  }
10244
+ function mkLine(...parts) {
10245
+ let rendered = "";
10246
+ let width = 0;
10247
+ for (const [text, fmt] of parts) {
10248
+ rendered += fmt ? fmt(text) : text;
10249
+ width += text.length;
10250
+ }
10251
+ return { rendered, width };
10252
+ }
10253
+ function shortRule(name, width) {
10254
+ const stripped = name.replace(/^shield:[^:]+:/, "");
10255
+ if (stripped.length <= width) return stripped.padEnd(width);
10256
+ return stripped.slice(0, width - 1) + "\u2026";
10257
+ }
10258
+ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
10259
+ const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
10260
+ const topLines = [];
10261
+ if (scan.dlpFindings.length > 0) {
10262
+ const latest = scan.dlpFindings[0];
10263
+ const rel = relativeDate(latest.timestamp, now);
10264
+ const noun = `credential leak${scan.dlpFindings.length !== 1 ? "s" : ""}`;
10265
+ topLines.push(
10266
+ mkLine(
10267
+ ["\u{1F6A8} ", import_chalk5.default.red],
10268
+ [`${scan.dlpFindings.length} ${noun} in tool input `, import_chalk5.default.bold],
10269
+ [`(latest: ${rel} ago, ${latest.patternName})`, import_chalk5.default.dim]
10270
+ )
10271
+ );
10272
+ }
10273
+ if (blockedCount > 0) {
10274
+ const topBlocked = topRulesByVerdict(summary.sections, "block", 2).map(
10275
+ (r) => r.count > 1 ? `${shortRule(r.name, 20).trimEnd()} \xD7${r.count}` : shortRule(r.name, 20).trimEnd()
10276
+ ).join(", ");
10277
+ topLines.push(
10278
+ mkLine(
10279
+ ["\u{1F6D1} ", import_chalk5.default.red],
10280
+ [`${blockedCount} ops node9 would have blocked `, import_chalk5.default.bold],
10281
+ [`(${topBlocked})`, import_chalk5.default.dim]
10282
+ )
10283
+ );
10284
+ }
10285
+ if (scan.loopFindings.length > 0) {
10286
+ const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
10287
+ const byTool = /* @__PURE__ */ new Map();
10288
+ for (const f of scan.loopFindings) {
10289
+ byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + Math.max(0, f.count - 1));
10290
+ }
10291
+ const top = [...byTool.entries()].sort((a, b) => b[1] - a[1])[0];
10292
+ const wasteSuffix = wastePct > 0 ? `, ${wastePct}% wasted` : "";
10293
+ const detail = top ? `(${top[0]} dominates${wasteSuffix})` : "";
10294
+ topLines.push(
10295
+ mkLine(
10296
+ ["\u{1F501} ", import_chalk5.default.yellow],
10297
+ [`${scan.loopFindings.length} agent loops detected `, import_chalk5.default.bold],
10298
+ [detail, import_chalk5.default.dim]
10299
+ )
10300
+ );
10301
+ }
10302
+ if (blastExposures > 0) {
10303
+ const exposed2 = Math.max(0, 100 - blast.score);
10304
+ const pjDiscount = PROTECTIVE_SHIELD_DISCOUNTS["project-jail"] ?? 0;
10305
+ const pjBonus = Math.round(exposed2 * pjDiscount);
10306
+ const cta = pjBonus > 0 ? ` \u2192 enable project-jail (+${pjBonus} pts)` : "";
10307
+ topLines.push(
10308
+ mkLine(
10309
+ ["\u{1F52D} ", import_chalk5.default.red],
10310
+ [`${blastExposures} secrets reachable on disk`, import_chalk5.default.bold],
10311
+ [cta, import_chalk5.default.dim]
10312
+ )
10313
+ );
10314
+ }
10315
+ if (topLines.length > 0) {
10316
+ for (const ln of boxPanel("TOP FINDINGS", topLines)) console.log(" " + ln);
10317
+ console.log("");
10318
+ }
10319
+ if (summary.leaks.length > 0) {
10320
+ const leakLines = [];
10321
+ for (const leak of summary.leaks.slice(0, 5)) {
10322
+ const rel = relativeDate(leak.timestamp, now);
10323
+ leakLines.push(
10324
+ mkLine(
10325
+ [rel.padStart(4) + " ", import_chalk5.default.dim],
10326
+ [leak.patternName.padEnd(14), import_chalk5.default.red.bold],
10327
+ [" "],
10328
+ [leak.redactedSample.padEnd(20), import_chalk5.default.red],
10329
+ [" "],
10330
+ [`[${leak.toolName}]`.padEnd(15), import_chalk5.default.dim],
10331
+ [" "],
10332
+ [leak.agent, import_chalk5.default.dim]
10333
+ )
10334
+ );
10335
+ }
10336
+ const remaining = summary.leaks.length - 5;
10337
+ if (remaining > 0) {
10338
+ leakLines.push(mkLine([`\u2026 +${remaining} more`, import_chalk5.default.dim]));
10339
+ }
10340
+ const title = `LEAKS \xB7 ${summary.leaks.length} secret${summary.leaks.length !== 1 ? "s" : ""} in plain text`;
10341
+ for (const ln of boxPanel(title, leakLines)) console.log(" " + ln);
10342
+ console.log("");
10343
+ }
10344
+ if (blockedCount > 0) {
10345
+ const blockedLines = [];
10346
+ const ruleEntries = topRulesByVerdict(summary.sections, "block", 12);
10347
+ for (const r of ruleEntries) {
10348
+ const origin = originForRule(r.name, summary.sections);
10349
+ blockedLines.push(
10350
+ mkLine(
10351
+ ["\u2717 ", import_chalk5.default.red],
10352
+ [shortRule(r.name, 24), import_chalk5.default.bold],
10353
+ [" \xD7" + String(r.count).padEnd(4), import_chalk5.default.bold],
10354
+ [" "],
10355
+ [origin, import_chalk5.default.dim]
10356
+ )
10357
+ );
10358
+ }
10359
+ const title = `BLOCKED \xB7 ${blockedCount} ops node9 would have stopped`;
10360
+ for (const ln of boxPanel(title, blockedLines)) console.log(" " + ln);
10361
+ console.log("");
10362
+ }
10363
+ if (reviewCount > 0) {
10364
+ const reviewLines = [];
10365
+ const ruleEntries = topRulesByVerdict(summary.sections, "review", 12);
10366
+ for (const r of ruleEntries) {
10367
+ const origin = originForRule(r.name, summary.sections);
10368
+ reviewLines.push(
10369
+ mkLine(
10370
+ ["\u{1F441} ", import_chalk5.default.yellow],
10371
+ [shortRule(r.name, 24), import_chalk5.default.bold],
10372
+ [" \xD7" + String(r.count).padEnd(4), import_chalk5.default.bold],
10373
+ [" "],
10374
+ [origin, import_chalk5.default.dim]
10375
+ )
10376
+ );
10377
+ }
10378
+ const title = `REVIEW QUEUE \xB7 ${reviewCount} ops flagged for approval`;
10379
+ for (const ln of boxPanel(title, reviewLines)) console.log(" " + ln);
10380
+ console.log("");
10381
+ }
10382
+ if (scan.loopFindings.length > 0) {
10383
+ const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
10384
+ const byTool = /* @__PURE__ */ new Map();
10385
+ let totalRepeats = 0;
10386
+ for (const f of scan.loopFindings) {
10387
+ const repeats = Math.max(0, f.count - 1);
10388
+ byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
10389
+ totalRepeats += repeats;
10390
+ }
10391
+ const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
10392
+ const loopLines = [];
10393
+ for (const [tool, repeats] of toolEntries) {
10394
+ const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
10395
+ loopLines.push(
10396
+ mkLine(
10397
+ [tool.padEnd(10), import_chalk5.default.bold],
10398
+ [`\xD7${num(repeats)} repeats`.padEnd(16)],
10399
+ [`(${pct}%)`, import_chalk5.default.dim]
10400
+ )
10401
+ );
10402
+ }
10403
+ const topStuck = [...scan.loopFindings].sort((a, b) => b.count - a.count).slice(0, 3);
10404
+ if (topStuck.length > 0) {
10405
+ loopLines.push(mkLine([""]));
10406
+ loopLines.push(mkLine(["Top stuck patterns:", import_chalk5.default.dim]));
10407
+ for (const f of topStuck) {
10408
+ const raw = f.commandPreview || f.toolName;
10409
+ const target = raw.length > 60 ? "\u2026" + raw.slice(raw.length - 59) : raw.padEnd(60);
10410
+ loopLines.push(mkLine([`\xD7${num(f.count).padEnd(4)} `, import_chalk5.default.bold], [target, import_chalk5.default.dim]));
10411
+ }
10412
+ }
10413
+ const wasteSuffix = wastePct > 0 ? ` \xB7 ${wastePct}% wasted` : "";
10414
+ const title = `AGENT LOOPS \xB7 ${scan.loopFindings.length} repeated patterns${wasteSuffix}`;
10415
+ for (const ln of boxPanel(title, loopLines)) console.log(" " + ln);
10416
+ console.log("");
10417
+ }
10418
+ if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
10419
+ const blastLines = [];
10420
+ const DESC_W = 33;
10421
+ for (const r of blast.reachable.slice(0, 8)) {
10422
+ const trimmed = r.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
10423
+ const desc = trimmed.length > DESC_W ? trimmed.slice(0, DESC_W - 1) + "\u2026" : trimmed;
10424
+ blastLines.push(mkLine(["\u2717 ", import_chalk5.default.red], [r.label.padEnd(36)], [desc, import_chalk5.default.dim]));
10425
+ }
10426
+ for (const e of blast.envFindings.slice(0, 3)) {
10427
+ blastLines.push(
10428
+ mkLine(["\u26A0 ", import_chalk5.default.yellow], [`${e.key} `], [`(${e.patternName})`, import_chalk5.default.dim])
10429
+ );
10430
+ }
10431
+ const totalExposed = blast.reachable.length + blast.envFindings.length;
10432
+ if (totalExposed > 8) {
10433
+ blastLines.push(mkLine([`\u2026 +${totalExposed - 8} more`, import_chalk5.default.dim]));
10434
+ }
10435
+ const title = `BLAST RADIUS \xB7 ${totalExposed} path${totalExposed !== 1 ? "s" : ""} reachable right now`;
10436
+ for (const ln of boxPanel(title, blastLines)) console.log(" " + ln);
10437
+ console.log("");
10438
+ }
10439
+ const shieldImpacts = rollupByShield(summary.sections);
10440
+ const exposed = Math.max(0, 100 - blast.score);
10441
+ const shieldLines = [];
10442
+ const ranked = [...shieldImpacts].sort((a, b) => {
10443
+ const aDiscount = PROTECTIVE_SHIELD_DISCOUNTS[a.shieldName] ?? 0;
10444
+ const bDiscount = PROTECTIVE_SHIELD_DISCOUNTS[b.shieldName] ?? 0;
10445
+ if (aDiscount !== bDiscount) return bDiscount - aDiscount;
10446
+ return b.totalCatches - a.totalCatches;
10447
+ });
10448
+ for (const impact of ranked) {
10449
+ if (impact.totalCatches === 0) continue;
10450
+ const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
10451
+ const bonus = Math.round(exposed * discount);
10452
+ const icon = discount > 0 ? "\u{1F6E1} " : "\u2610 ";
10453
+ const wouldCatch = `would catch ${impact.totalCatches} op${impact.totalCatches !== 1 ? "s" : ""}`;
10454
+ const deltaSuffix = bonus > 0 ? ` \u2192 +${bonus} pts (${blast.score} \u2192 ${blast.score + bonus})` : "";
10455
+ shieldLines.push(
10456
+ mkLine(
10457
+ [icon, discount > 0 ? import_chalk5.default.cyan : import_chalk5.default.dim],
10458
+ [impact.shieldName.padEnd(14), import_chalk5.default.bold],
10459
+ [wouldCatch.padEnd(22), import_chalk5.default.dim],
10460
+ [deltaSuffix, bonus > 0 ? import_chalk5.default.green.bold : import_chalk5.default.dim]
10461
+ )
10462
+ );
10463
+ if (impact.topRuleLabels.length > 0) {
10464
+ const rules = impact.topRuleLabels.join(", ");
10465
+ shieldLines.push(mkLine([" ", import_chalk5.default.dim], [rules, import_chalk5.default.dim]));
10466
+ }
10467
+ }
10468
+ const hitShieldSet = new Set(
10469
+ shieldImpacts.filter((i) => i.totalCatches > 0).map((i) => i.shieldName)
10470
+ );
10471
+ const zeroHitBuiltins = Object.keys(SHIELDS).filter((name) => !hitShieldSet.has(name)).sort();
10472
+ if (zeroHitBuiltins.length > 0) {
10473
+ shieldLines.push(mkLine([""]));
10474
+ shieldLines.push(mkLine([zeroHitBuiltins.join(" \xB7 "), import_chalk5.default.dim]));
10475
+ shieldLines.push(mkLine([" no hits in your history \u2014 install proactively", import_chalk5.default.dim]));
10476
+ }
10477
+ const topRec = ranked.find(
10478
+ (r) => r.totalCatches > 0 && (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0
10479
+ );
10480
+ if (topRec) {
10481
+ const bonus = Math.round(exposed * (PROTECTIVE_SHIELD_DISCOUNTS[topRec.shieldName] ?? 0));
10482
+ const cta = `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 +${bonus} pts)`;
10483
+ shieldLines.push(mkLine([""]));
10484
+ shieldLines.push(mkLine([cta, import_chalk5.default.cyan]));
10485
+ }
10486
+ if (shieldLines.length > 0) {
10487
+ const title = "SHIELDS \xB7 install node9 + enable these to catch what we found";
10488
+ for (const ln of boxPanel(title, shieldLines)) console.log(" " + ln);
10489
+ console.log("");
10490
+ }
10491
+ }
10492
+ function originForRule(ruleName, sections) {
10493
+ for (const section of sections) {
10494
+ if (section.rules.some((r) => r.name === ruleName)) {
10495
+ if (section.sourceType === "default") return "default";
10496
+ if (section.sourceType === "shield") return `needs shield:${section.shieldKey ?? section.id}`;
10497
+ }
10498
+ }
10499
+ return "";
10500
+ }
10036
10501
  function registerScanCommand(program2) {
10037
10502
  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(
10038
10503
  "--json",
@@ -10264,7 +10729,7 @@ function registerScanCommand(program2) {
10264
10729
  " " + import_chalk5.default.dim("AI spend ") + import_chalk5.default.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? import_chalk5.default.dim(" \xB7 wasted on loops ") + import_chalk5.default.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
10265
10730
  );
10266
10731
  }
10267
- if (scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
10732
+ if (drillDown && scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
10268
10733
  console.log(
10269
10734
  " " + import_chalk5.default.dim(
10270
10735
  `${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
@@ -10272,6 +10737,26 @@ function registerScanCommand(program2) {
10272
10737
  );
10273
10738
  }
10274
10739
  console.log("");
10740
+ if (!drillDown) {
10741
+ renderPanelScorecard({
10742
+ scan,
10743
+ summary,
10744
+ blast,
10745
+ blastExposures,
10746
+ blockedCount,
10747
+ reviewCount
10748
+ });
10749
+ const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
10750
+ console.log(" " + import_chalk5.default.green(cta));
10751
+ console.log(
10752
+ " " + import_chalk5.default.dim("\u2192 ") + import_chalk5.default.cyan("node9 monitor") + import_chalk5.default.dim(" live dashboard")
10753
+ );
10754
+ console.log(
10755
+ " " + import_chalk5.default.dim("\u2192 ") + import_chalk5.default.cyan("node9 scan --drill-down") + import_chalk5.default.dim(" full commands + session IDs")
10756
+ );
10757
+ console.log("");
10758
+ return;
10759
+ }
10275
10760
  if (scan.dlpFindings.length > 0) {
10276
10761
  console.log(" " + import_chalk5.default.dim("\u2500".repeat(70)));
10277
10762
  console.log(
@@ -10460,7 +10945,7 @@ function registerScanCommand(program2) {
10460
10945
  }
10461
10946
  );
10462
10947
  }
10463
- var import_chalk5, import_fs19, import_path21, import_os18, 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;
10948
+ var import_chalk5, import_fs19, import_path21, import_os18, 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;
10464
10949
  var init_scan = __esm({
10465
10950
  "src/cli/commands/scan.ts"() {
10466
10951
  "use strict";
@@ -10478,6 +10963,7 @@ var init_scan = __esm({
10478
10963
  init_setup();
10479
10964
  init_blast();
10480
10965
  init_scan_derive();
10966
+ init_protection();
10481
10967
  init_scan_json();
10482
10968
  init_scan_history();
10483
10969
  CLAUDE_PRICING = {
@@ -10560,9 +11046,6 @@ var init_scan = __esm({
10560
11046
  STUCK_TOOLS_LIMIT = 3;
10561
11047
  RECURRING_SESSION_THRESHOLD = 3;
10562
11048
  STALE_AGE_DAYS = 30;
10563
- DEFAULT_RULE_NAMES = new Set(
10564
- DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
10565
- );
10566
11049
  classifyRuleSeverity2 = classifyRuleSeverity;
10567
11050
  narrativeRuleLabel2 = narrativeRuleLabel;
10568
11051
  }
@@ -13135,8 +13618,15 @@ var tail_exports = {};
13135
13618
  __export(tail_exports, {
13136
13619
  agentLabel: () => agentLabel,
13137
13620
  sessionTag: () => sessionTag,
13621
+ shortenPathSummary: () => shortenPathSummary,
13138
13622
  startTail: () => startTail
13139
13623
  });
13624
+ function shortenPathSummary(s) {
13625
+ if (!s || !s.startsWith("/")) return s;
13626
+ const parts = s.split("/").filter(Boolean);
13627
+ if (parts.length <= 2) return s;
13628
+ return `\u2026/${parts.slice(-2).join("/")}`;
13629
+ }
13140
13630
  function getIcon(tool) {
13141
13631
  const t = tool.toLowerCase();
13142
13632
  for (const [k, v] of Object.entries(ICONS)) {
@@ -13890,7 +14380,8 @@ async function startTail(options = {}) {
13890
14380
  if (event === "snapshot") {
13891
14381
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
13892
14382
  const hash = data.hash ?? "";
13893
- const summary = data.argsSummary ?? data.tool;
14383
+ const rawSummary = data.argsSummary ?? data.tool;
14384
+ const summary = shortenPathSummary(rawSummary);
13894
14385
  const fileCount = data.fileCount ?? 0;
13895
14386
  const files = fileCount > 0 ? import_chalk30.default.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
13896
14387
  process.stdout.write(
@@ -16279,63 +16770,13 @@ function registerAuditCommand(program2) {
16279
16770
 
16280
16771
  // src/cli/commands/report.ts
16281
16772
  var import_chalk13 = __toESM(require("chalk"));
16773
+
16774
+ // src/cli/aggregate/report-audit.ts
16282
16775
  var import_fs35 = __toESM(require("fs"));
16283
- var import_path36 = __toESM(require("path"));
16284
16776
  var import_os31 = __toESM(require("os"));
16285
-
16286
- // src/cli/render/report-json.ts
16287
- function buildReportJson(input) {
16288
- const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
16289
- const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
16290
- const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
16291
- return {
16292
- schemaVersion: 1,
16293
- generatedAt: input.generatedAt,
16294
- period: input.period,
16295
- range: { start: input.start.toISOString(), end: input.end.toISOString() },
16296
- excludedTests: input.excludedTests,
16297
- totals: {
16298
- events: input.total,
16299
- blocked: totalBlocked,
16300
- blockRate,
16301
- userApproved: input.userApproved,
16302
- userDenied: input.userDenied,
16303
- timedOut: input.timedOut,
16304
- hardBlocked: input.hardBlocked,
16305
- dlpBlocked: input.dlpBlocked,
16306
- observeDlp: input.observeDlp,
16307
- loopHits: input.loopHits,
16308
- unackedDlp: input.unackedDlp
16309
- },
16310
- tests: {
16311
- passes: input.testPasses,
16312
- fails: input.testFails
16313
- },
16314
- cost: {
16315
- totalUSD: input.cost.claudeUSD + input.cost.codexUSD,
16316
- claudeUSD: input.cost.claudeUSD,
16317
- codexUSD: input.cost.codexUSD,
16318
- inputTokens: input.cost.inputTokens,
16319
- outputTokens: input.cost.outputTokens,
16320
- cacheWriteTokens: input.cost.cacheWriteTokens,
16321
- cacheReadTokens: input.cost.cacheReadTokens,
16322
- byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
16323
- byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
16324
- },
16325
- byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
16326
- byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
16327
- byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
16328
- byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
16329
- byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
16330
- byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
16331
- trend: {
16332
- priorBlockRate: input.priorBlockRate,
16333
- deltaPct
16334
- }
16335
- };
16336
- }
16337
-
16338
- // src/cli/commands/report.ts
16777
+ var import_path36 = __toESM(require("path"));
16778
+ init_costSync();
16779
+ init_litellm();
16339
16780
  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;
16340
16781
  function buildTestTimestamps(allEntries) {
16341
16782
  const testTs = /* @__PURE__ */ new Set();
@@ -16360,8 +16801,7 @@ function isTestEntry(entry, testTs) {
16360
16801
  }
16361
16802
  return false;
16362
16803
  }
16363
- function getDateRange(period) {
16364
- const now = /* @__PURE__ */ new Date();
16804
+ function getDateRange(period, now) {
16365
16805
  const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
16366
16806
  const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
16367
16807
  switch (period) {
@@ -16377,6 +16817,11 @@ function getDateRange(period) {
16377
16817
  s.setDate(s.getDate() - 29);
16378
16818
  return { start: s, end };
16379
16819
  }
16820
+ case "90d": {
16821
+ const s = new Date(todayStart);
16822
+ s.setDate(s.getDate() - 89);
16823
+ return { start: s, end };
16824
+ }
16380
16825
  case "month":
16381
16826
  return { start: new Date(now.getFullYear(), now.getMonth(), 1), end };
16382
16827
  }
@@ -16399,40 +16844,6 @@ function isAllow(decision) {
16399
16844
  function isDlp(checkedBy) {
16400
16845
  return !!checkedBy?.includes("dlp");
16401
16846
  }
16402
- var BLOCK_REASON_LABELS = {
16403
- timeout: "Popup timeout",
16404
- "smart-rule-block": "Smart rule",
16405
- "observe-mode-dlp-would-block": "DLP (observe)",
16406
- "persistent-deny": "Persistent deny",
16407
- "local-decision": "User denied",
16408
- "dlp-block": "DLP block",
16409
- "loop-detected": "Loop detected"
16410
- };
16411
- function humanBlockReason(reason) {
16412
- return BLOCK_REASON_LABELS[reason] ?? reason;
16413
- }
16414
- function barStr(value, max, width) {
16415
- if (max === 0 || width <= 0) return "\u2591".repeat(width);
16416
- const filled = Math.max(1, Math.round(value / max * width));
16417
- return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
16418
- }
16419
- function colorBar(value, max, width) {
16420
- const s = barStr(value, max, width);
16421
- const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
16422
- return import_chalk13.default.cyan(s.slice(0, filled)) + import_chalk13.default.dim(s.slice(filled));
16423
- }
16424
- function fmtDate(d) {
16425
- const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
16426
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
16427
- }
16428
- function num2(n) {
16429
- return n.toLocaleString();
16430
- }
16431
- function fmtCost2(usd) {
16432
- if (usd < 1e-3) return "< $0.001";
16433
- if (usd < 1) return "$" + usd.toFixed(4);
16434
- return "$" + usd.toFixed(2);
16435
- }
16436
16847
  var CLAUDE_PRICING2 = {
16437
16848
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
16438
16849
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -16452,90 +16863,160 @@ function claudeModelPrice2(model) {
16452
16863
  }
16453
16864
  return null;
16454
16865
  }
16455
- function loadClaudeCost(start, end) {
16456
- const empty = {
16866
+ function emptyClaudeCostAccumulator() {
16867
+ return {
16457
16868
  total: 0,
16458
- byDay: /* @__PURE__ */ new Map(),
16459
- byModel: /* @__PURE__ */ new Map(),
16460
16869
  inputTokens: 0,
16461
16870
  outputTokens: 0,
16462
16871
  cacheWriteTokens: 0,
16463
- cacheReadTokens: 0
16872
+ cacheReadTokens: 0,
16873
+ byDay: /* @__PURE__ */ new Map(),
16874
+ byModel: /* @__PURE__ */ new Map(),
16875
+ byProject: /* @__PURE__ */ new Map()
16464
16876
  };
16465
- const projectsDir = import_path36.default.join(import_os31.default.homedir(), ".claude", "projects");
16466
- if (!import_fs35.default.existsSync(projectsDir)) return empty;
16877
+ }
16878
+ function freezeClaudeCost(acc) {
16879
+ return {
16880
+ total: acc.total,
16881
+ byDay: acc.byDay,
16882
+ byModel: acc.byModel,
16883
+ byProject: acc.byProject,
16884
+ inputTokens: acc.inputTokens,
16885
+ outputTokens: acc.outputTokens,
16886
+ cacheWriteTokens: acc.cacheWriteTokens,
16887
+ cacheReadTokens: acc.cacheReadTokens
16888
+ };
16889
+ }
16890
+ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
16891
+ const projPath = import_path36.default.join(projectsDir, proj);
16892
+ let files;
16893
+ try {
16894
+ const stat = import_fs35.default.statSync(projPath);
16895
+ if (!stat.isDirectory()) return;
16896
+ files = import_fs35.default.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
16897
+ } catch {
16898
+ return;
16899
+ }
16900
+ const startMs = start.getTime();
16901
+ for (const file of files) {
16902
+ const filePath = import_path36.default.join(projPath, file);
16903
+ try {
16904
+ if (import_fs35.default.statSync(filePath).mtimeMs < startMs) continue;
16905
+ } catch {
16906
+ continue;
16907
+ }
16908
+ try {
16909
+ const raw = import_fs35.default.readFileSync(filePath, "utf-8");
16910
+ for (const line of raw.split("\n")) {
16911
+ if (!line.trim()) continue;
16912
+ let entry;
16913
+ try {
16914
+ entry = JSON.parse(line);
16915
+ } catch {
16916
+ continue;
16917
+ }
16918
+ if (entry.type !== "assistant") continue;
16919
+ if (!entry.timestamp) continue;
16920
+ const ts = new Date(entry.timestamp);
16921
+ if (ts < start || ts > end) continue;
16922
+ const usage = entry.message?.usage;
16923
+ const model = entry.message?.model;
16924
+ if (!usage || !model) continue;
16925
+ const p = claudeModelPrice2(model);
16926
+ if (!p) continue;
16927
+ const inp = usage.input_tokens ?? 0;
16928
+ const out = usage.output_tokens ?? 0;
16929
+ const cw = usage.cache_creation_input_tokens ?? 0;
16930
+ const cr = usage.cache_read_input_tokens ?? 0;
16931
+ const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
16932
+ acc.total += cost;
16933
+ acc.inputTokens += inp;
16934
+ acc.outputTokens += out;
16935
+ acc.cacheWriteTokens += cw;
16936
+ acc.cacheReadTokens += cr;
16937
+ const dateKey = entry.timestamp.slice(0, 10);
16938
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
16939
+ const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
16940
+ acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
16941
+ const projectKey = decodeProjectDirName(proj);
16942
+ const projectRollup = acc.byProject.get(projectKey) ?? {
16943
+ cost: 0,
16944
+ inputTokens: 0,
16945
+ outputTokens: 0
16946
+ };
16947
+ projectRollup.cost += cost;
16948
+ projectRollup.inputTokens += inp;
16949
+ projectRollup.outputTokens += out;
16950
+ acc.byProject.set(projectKey, projectRollup);
16951
+ }
16952
+ } catch {
16953
+ continue;
16954
+ }
16955
+ }
16956
+ }
16957
+ function loadClaudeCost(start, end, projectsDir) {
16958
+ const acc = emptyClaudeCostAccumulator();
16959
+ if (!import_fs35.default.existsSync(projectsDir)) return freezeClaudeCost(acc);
16467
16960
  let dirs;
16468
16961
  try {
16469
16962
  dirs = import_fs35.default.readdirSync(projectsDir);
16470
16963
  } catch {
16471
- return empty;
16964
+ return freezeClaudeCost(acc);
16472
16965
  }
16473
- let total = 0;
16474
- let inputTokens = 0;
16475
- let outputTokens = 0;
16476
- let cacheWriteTokens = 0;
16477
- let cacheReadTokens = 0;
16478
- const byDay = /* @__PURE__ */ new Map();
16479
- const byModel = /* @__PURE__ */ new Map();
16480
16966
  for (const proj of dirs) {
16481
- const projPath = import_path36.default.join(projectsDir, proj);
16482
- let files;
16967
+ processClaudeCostProject(proj, projectsDir, start, end, acc);
16968
+ }
16969
+ return freezeClaudeCost(acc);
16970
+ }
16971
+ function processCodexCostFile(filePath, start, end, acc) {
16972
+ let lines;
16973
+ try {
16974
+ lines = import_fs35.default.readFileSync(filePath, "utf-8").split("\n");
16975
+ } catch {
16976
+ return;
16977
+ }
16978
+ let sessionStart2 = "";
16979
+ let lastTotalInput = 0;
16980
+ let lastTotalCached = 0;
16981
+ let lastTotalOutput = 0;
16982
+ let sessionToolCalls = 0;
16983
+ for (const line of lines) {
16984
+ if (!line.trim()) continue;
16985
+ let entry;
16483
16986
  try {
16484
- const stat = import_fs35.default.statSync(projPath);
16485
- if (!stat.isDirectory()) continue;
16486
- files = import_fs35.default.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
16987
+ entry = JSON.parse(line);
16487
16988
  } catch {
16488
16989
  continue;
16489
16990
  }
16490
- for (const file of files) {
16491
- try {
16492
- const raw = import_fs35.default.readFileSync(import_path36.default.join(projPath, file), "utf-8");
16493
- for (const line of raw.split("\n")) {
16494
- if (!line.trim()) continue;
16495
- let entry;
16496
- try {
16497
- entry = JSON.parse(line);
16498
- } catch {
16499
- continue;
16500
- }
16501
- if (entry.type !== "assistant") continue;
16502
- if (!entry.timestamp) continue;
16503
- const ts = new Date(entry.timestamp);
16504
- if (ts < start || ts > end) continue;
16505
- const usage = entry.message?.usage;
16506
- const model = entry.message?.model;
16507
- if (!usage || !model) continue;
16508
- const p = claudeModelPrice2(model);
16509
- if (!p) continue;
16510
- const inp = usage.input_tokens ?? 0;
16511
- const out = usage.output_tokens ?? 0;
16512
- const cw = usage.cache_creation_input_tokens ?? 0;
16513
- const cr = usage.cache_read_input_tokens ?? 0;
16514
- const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
16515
- total += cost;
16516
- inputTokens += inp;
16517
- outputTokens += out;
16518
- cacheWriteTokens += cw;
16519
- cacheReadTokens += cr;
16520
- const dateKey = entry.timestamp.slice(0, 10);
16521
- byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
16522
- const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
16523
- byModel.set(normModel, (byModel.get(normModel) ?? 0) + cost);
16524
- }
16525
- } catch {
16526
- continue;
16527
- }
16991
+ const p = entry.payload ?? {};
16992
+ if (entry.type === "session_meta") {
16993
+ sessionStart2 = String(p["timestamp"] ?? "");
16994
+ continue;
16995
+ }
16996
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
16997
+ const info = p["info"] ?? {};
16998
+ const usage = info["total_token_usage"] ?? {};
16999
+ lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
17000
+ lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
17001
+ lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
17002
+ }
17003
+ if (entry.type === "response_item" && p["type"] === "function_call") {
17004
+ sessionToolCalls++;
16528
17005
  }
16529
17006
  }
16530
- return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
17007
+ if (!sessionStart2) return;
17008
+ const ts = new Date(sessionStart2);
17009
+ if (ts < start || ts > end) return;
17010
+ const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
17011
+ const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
17012
+ acc.total += cost;
17013
+ acc.toolCalls += sessionToolCalls;
17014
+ const dateKey = sessionStart2.slice(0, 10);
17015
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
16531
17016
  }
16532
- function loadCodexCost(start, end) {
16533
- const sessionsBase = import_path36.default.join(import_os31.default.homedir(), ".codex", "sessions");
16534
- const byDay = /* @__PURE__ */ new Map();
16535
- let total = 0;
16536
- let toolCalls = 0;
16537
- if (!import_fs35.default.existsSync(sessionsBase)) return { total, byDay, toolCalls };
17017
+ function listCodexSessionFiles(sessionsBase) {
16538
17018
  const jsonlFiles = [];
17019
+ if (!import_fs35.default.existsSync(sessionsBase)) return jsonlFiles;
16539
17020
  try {
16540
17021
  for (const year of import_fs35.default.readdirSync(sessionsBase)) {
16541
17022
  const yearPath = import_path36.default.join(sessionsBase, year);
@@ -16565,495 +17046,742 @@ function loadCodexCost(start, end) {
16565
17046
  }
16566
17047
  }
16567
17048
  } catch {
16568
- return { total, byDay, toolCalls };
17049
+ return [];
16569
17050
  }
16570
- for (const filePath of jsonlFiles) {
16571
- let lines;
17051
+ return jsonlFiles;
17052
+ }
17053
+ function loadCodexCost(start, end, sessionsBase) {
17054
+ const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
17055
+ const files = listCodexSessionFiles(sessionsBase);
17056
+ for (const filePath of files) {
17057
+ processCodexCostFile(filePath, start, end, acc);
17058
+ }
17059
+ return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
17060
+ }
17061
+ var GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
17062
+ function geminiPriceFor(model) {
17063
+ let tuple = pricingFor(model);
17064
+ if (!tuple && /^gemini-/i.test(model)) {
17065
+ for (const proxy of GEMINI_FALLBACK_MODELS) {
17066
+ tuple = pricingFor(proxy);
17067
+ if (tuple) break;
17068
+ }
17069
+ }
17070
+ if (!tuple) return null;
17071
+ return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
17072
+ }
17073
+ function emptyGeminiAccumulator() {
17074
+ return {
17075
+ total: 0,
17076
+ inputTokens: 0,
17077
+ outputTokens: 0,
17078
+ cacheReadTokens: 0,
17079
+ byDay: /* @__PURE__ */ new Map(),
17080
+ byProject: /* @__PURE__ */ new Map()
17081
+ };
17082
+ }
17083
+ function freezeGeminiCost(acc) {
17084
+ return {
17085
+ total: acc.total,
17086
+ byDay: acc.byDay,
17087
+ byProject: acc.byProject,
17088
+ inputTokens: acc.inputTokens,
17089
+ outputTokens: acc.outputTokens,
17090
+ cacheReadTokens: acc.cacheReadTokens
17091
+ };
17092
+ }
17093
+ function processGeminiCostFile(filePath, projectKey, start, end, acc) {
17094
+ const startMs = start.getTime();
17095
+ try {
17096
+ if (import_fs35.default.statSync(filePath).mtimeMs < startMs) return;
17097
+ } catch {
17098
+ return;
17099
+ }
17100
+ let raw;
17101
+ try {
17102
+ raw = import_fs35.default.readFileSync(filePath, "utf-8");
17103
+ } catch {
17104
+ return;
17105
+ }
17106
+ const seenIds = /* @__PURE__ */ new Set();
17107
+ for (const line of raw.split("\n")) {
17108
+ if (!line.trim()) continue;
17109
+ let entry;
16572
17110
  try {
16573
- lines = import_fs35.default.readFileSync(filePath, "utf-8").split("\n");
17111
+ entry = JSON.parse(line);
16574
17112
  } catch {
16575
17113
  continue;
16576
17114
  }
16577
- let sessionStart2 = "";
16578
- let lastTotalInput = 0;
16579
- let lastTotalCached = 0;
16580
- let lastTotalOutput = 0;
16581
- let sessionToolCalls = 0;
16582
- for (const line of lines) {
16583
- if (!line.trim()) continue;
16584
- let entry;
16585
- try {
16586
- entry = JSON.parse(line);
16587
- } catch {
16588
- continue;
16589
- }
16590
- const p = entry.payload ?? {};
16591
- if (entry.type === "session_meta") {
16592
- sessionStart2 = String(p["timestamp"] ?? "");
16593
- continue;
16594
- }
16595
- if (entry.type === "event_msg" && p["type"] === "token_count") {
16596
- const info = p["info"] ?? {};
16597
- const usage = info["total_token_usage"] ?? {};
16598
- lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
16599
- lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
16600
- lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
16601
- }
16602
- if (entry.type === "response_item" && p["type"] === "function_call") {
16603
- sessionToolCalls++;
16604
- }
17115
+ if (entry.type !== "gemini") continue;
17116
+ if (!entry.tokens || !entry.model || !entry.timestamp) continue;
17117
+ if (entry.id) {
17118
+ if (seenIds.has(entry.id)) continue;
17119
+ seenIds.add(entry.id);
16605
17120
  }
16606
- if (!sessionStart2) continue;
16607
- const ts = new Date(sessionStart2);
17121
+ const ts = new Date(entry.timestamp);
16608
17122
  if (ts < start || ts > end) continue;
16609
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
16610
- const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
16611
- total += cost;
16612
- toolCalls += sessionToolCalls;
16613
- const dateKey = sessionStart2.slice(0, 10);
16614
- byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
17123
+ const price = geminiPriceFor(entry.model);
17124
+ if (!price) continue;
17125
+ const inp = entry.tokens.input ?? 0;
17126
+ const out = entry.tokens.output ?? 0;
17127
+ const cached = Math.min(entry.tokens.cached ?? 0, inp);
17128
+ const fresh = Math.max(0, inp - cached);
17129
+ const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
17130
+ acc.total += cost;
17131
+ acc.inputTokens += inp;
17132
+ acc.outputTokens += out;
17133
+ acc.cacheReadTokens += cached;
17134
+ const dateKey = entry.timestamp.slice(0, 10);
17135
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
17136
+ const rollup = acc.byProject.get(projectKey) ?? {
17137
+ cost: 0,
17138
+ inputTokens: 0,
17139
+ outputTokens: 0
17140
+ };
17141
+ rollup.cost += cost;
17142
+ rollup.inputTokens += inp;
17143
+ rollup.outputTokens += out;
17144
+ acc.byProject.set(projectKey, rollup);
17145
+ }
17146
+ }
17147
+ function listGeminiSessionFiles(geminiTmpDir) {
17148
+ const out = [];
17149
+ let dirs;
17150
+ try {
17151
+ if (!import_fs35.default.statSync(geminiTmpDir).isDirectory()) return out;
17152
+ dirs = import_fs35.default.readdirSync(geminiTmpDir);
17153
+ } catch {
17154
+ return out;
17155
+ }
17156
+ for (const proj of dirs) {
17157
+ const chatsDir = import_path36.default.join(geminiTmpDir, proj, "chats");
17158
+ let files;
17159
+ try {
17160
+ if (!import_fs35.default.statSync(chatsDir).isDirectory()) continue;
17161
+ files = import_fs35.default.readdirSync(chatsDir);
17162
+ } catch {
17163
+ continue;
17164
+ }
17165
+ for (const f of files) {
17166
+ if (!f.endsWith(".jsonl")) continue;
17167
+ out.push({ projectKey: proj, file: import_path36.default.join(chatsDir, f) });
17168
+ }
16615
17169
  }
16616
- return { total, byDay, toolCalls };
17170
+ return out;
17171
+ }
17172
+ function loadGeminiCost(start, end, geminiTmpDir) {
17173
+ const acc = emptyGeminiAccumulator();
17174
+ if (!import_fs35.default.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
17175
+ for (const { projectKey, file } of listGeminiSessionFiles(geminiTmpDir)) {
17176
+ processGeminiCostFile(file, projectKey, start, end, acc);
17177
+ }
17178
+ return freezeGeminiCost(acc);
17179
+ }
17180
+ function aggregateReportFromAudit(period, opts = {}) {
17181
+ const now = opts.now ?? /* @__PURE__ */ new Date();
17182
+ const auditLogPath = opts.auditLogPath ?? import_path36.default.join(import_os31.default.homedir(), ".node9", "audit.log");
17183
+ const claudeProjectsDir = opts.claudeProjectsDir ?? import_path36.default.join(import_os31.default.homedir(), ".claude", "projects");
17184
+ const codexSessionsDir = opts.codexSessionsDir ?? import_path36.default.join(import_os31.default.homedir(), ".codex", "sessions");
17185
+ const geminiTmpDir = opts.geminiTmpDir ?? import_path36.default.join(import_os31.default.homedir(), ".gemini", "tmp");
17186
+ const hasAuditFile = import_fs35.default.existsSync(auditLogPath);
17187
+ const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath);
17188
+ const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
17189
+ const { start, end } = getDateRange(period, now);
17190
+ const responseDlpEntries = allEntries.filter((e) => {
17191
+ if (e.source !== "response-dlp") return false;
17192
+ const ts = new Date(e.ts);
17193
+ return ts >= start && ts <= end;
17194
+ }).map((e) => {
17195
+ const raw = e;
17196
+ return {
17197
+ ts: e.ts,
17198
+ dlpPattern: typeof raw.dlpPattern === "string" ? raw.dlpPattern : void 0,
17199
+ dlpSample: typeof raw.dlpSample === "string" ? raw.dlpSample : void 0
17200
+ };
17201
+ });
17202
+ const claudeCost = opts.preloadedClaudeCost ?? loadClaudeCost(start, end, claudeProjectsDir);
17203
+ const codexCost = opts.preloadedCodexCost ?? loadCodexCost(start, end, codexSessionsDir);
17204
+ const geminiCost = opts.preloadedGeminiCost ?? loadGeminiCost(start, end, geminiTmpDir);
17205
+ for (const [day, c] of codexCost.byDay) {
17206
+ claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
17207
+ }
17208
+ for (const [day, c] of geminiCost.byDay) {
17209
+ claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
17210
+ }
17211
+ for (const [geminiKey, gRollup] of geminiCost.byProject) {
17212
+ let mergedInto = null;
17213
+ for (const claudeKey of claudeCost.byProject.keys()) {
17214
+ const claudeBase = claudeKey.match(/[^/\\]+$/)?.[0] ?? claudeKey;
17215
+ if (claudeBase === geminiKey) {
17216
+ mergedInto = claudeKey;
17217
+ break;
17218
+ }
17219
+ }
17220
+ const targetKey = mergedInto ?? geminiKey;
17221
+ const existing = claudeCost.byProject.get(targetKey) ?? {
17222
+ cost: 0,
17223
+ inputTokens: 0,
17224
+ outputTokens: 0
17225
+ };
17226
+ existing.cost += gRollup.cost;
17227
+ existing.inputTokens += gRollup.inputTokens;
17228
+ existing.outputTokens += gRollup.outputTokens;
17229
+ claudeCost.byProject.set(targetKey, existing);
17230
+ }
17231
+ const periodMs = end.getTime() - start.getTime();
17232
+ const priorEnd = new Date(start.getTime() - 1);
17233
+ const priorStart = new Date(start.getTime() - periodMs);
17234
+ const priorEntries = allEntries.filter((e) => {
17235
+ if (e.source === "post-hook") return false;
17236
+ const ts = new Date(e.ts);
17237
+ return ts >= priorStart && ts <= priorEnd;
17238
+ });
17239
+ const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
17240
+ const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
17241
+ const excludeTests = opts.excludeTests === true;
17242
+ const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
17243
+ let excludedTests = 0;
17244
+ const entries = allEntries.filter((e) => {
17245
+ if (e.source === "post-hook") return false;
17246
+ if (e.source === "response-dlp") return false;
17247
+ const ts = new Date(e.ts);
17248
+ if (ts < start || ts > end) return false;
17249
+ if (excludeTests && isTestEntry(e, testTs)) {
17250
+ excludedTests++;
17251
+ return false;
17252
+ }
17253
+ return true;
17254
+ });
17255
+ let userApproved = 0;
17256
+ let userDenied = 0;
17257
+ let timedOut = 0;
17258
+ let hardBlocked = 0;
17259
+ let dlpBlocked = 0;
17260
+ let observeDlp = 0;
17261
+ let loopHits = 0;
17262
+ let testPasses = 0;
17263
+ let testFails = 0;
17264
+ const toolMap = /* @__PURE__ */ new Map();
17265
+ const blockMap = /* @__PURE__ */ new Map();
17266
+ const ruleMap = /* @__PURE__ */ new Map();
17267
+ const agentMap = /* @__PURE__ */ new Map();
17268
+ const mcpMap = /* @__PURE__ */ new Map();
17269
+ const dailyMap = /* @__PURE__ */ new Map();
17270
+ const hourMap = /* @__PURE__ */ new Map();
17271
+ for (const e of entries) {
17272
+ const allow = isAllow(e.decision);
17273
+ const dateKey = e.ts.slice(0, 10);
17274
+ const userInteracted = e.source === "daemon";
17275
+ if (userInteracted) {
17276
+ if (allow) userApproved++;
17277
+ else userDenied++;
17278
+ } else if (!allow) {
17279
+ if (e.checkedBy === "timeout") timedOut++;
17280
+ else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
17281
+ else if (isDlp(e.checkedBy)) dlpBlocked++;
17282
+ else if (e.checkedBy !== "loop-detected") hardBlocked++;
17283
+ }
17284
+ if (e.checkedBy === "loop-detected") loopHits++;
17285
+ const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
17286
+ t.calls++;
17287
+ if (!allow) t.blocked++;
17288
+ toolMap.set(e.tool, t);
17289
+ if (!allow && e.checkedBy) {
17290
+ blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
17291
+ }
17292
+ if (!allow && e.ruleName) {
17293
+ ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
17294
+ }
17295
+ if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
17296
+ if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
17297
+ const hour = new Date(e.ts).getHours();
17298
+ hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
17299
+ const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
17300
+ d.calls++;
17301
+ if (!allow) d.blocked++;
17302
+ dailyMap.set(dateKey, d);
17303
+ }
17304
+ for (const e of allEntries) {
17305
+ if (e.source !== "test-result") continue;
17306
+ const ts = new Date(e.ts);
17307
+ if (ts < start || ts > end) continue;
17308
+ if (e.testResult === "pass") testPasses++;
17309
+ else if (e.testResult === "fail") testFails++;
17310
+ }
17311
+ if (codexCost.toolCalls > 0) {
17312
+ agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexCost.toolCalls);
17313
+ }
17314
+ const data = {
17315
+ period,
17316
+ start,
17317
+ end,
17318
+ excludedTests,
17319
+ total: entries.length,
17320
+ userApproved,
17321
+ userDenied,
17322
+ timedOut,
17323
+ hardBlocked,
17324
+ dlpBlocked,
17325
+ observeDlp,
17326
+ loopHits,
17327
+ testPasses,
17328
+ testFails,
17329
+ unackedDlp: unackedDlp.length,
17330
+ priorBlockRate,
17331
+ cost: {
17332
+ claudeUSD: claudeCost.total,
17333
+ codexUSD: codexCost.total,
17334
+ geminiUSD: geminiCost.total,
17335
+ inputTokens: claudeCost.inputTokens + geminiCost.inputTokens,
17336
+ outputTokens: claudeCost.outputTokens + geminiCost.outputTokens,
17337
+ cacheWriteTokens: claudeCost.cacheWriteTokens,
17338
+ cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
17339
+ byDay: claudeCost.byDay,
17340
+ byModel: claudeCost.byModel,
17341
+ byProject: claudeCost.byProject
17342
+ },
17343
+ toolMap,
17344
+ blockMap,
17345
+ ruleMap,
17346
+ agentMap,
17347
+ mcpMap,
17348
+ dailyMap,
17349
+ hourMap,
17350
+ generatedAt: now.toISOString()
17351
+ };
17352
+ return { data, hasAuditFile, responseDlpEntries };
17353
+ }
17354
+
17355
+ // src/cli/render/report-json.ts
17356
+ function buildReportJson(input) {
17357
+ const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
17358
+ const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
17359
+ const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
17360
+ return {
17361
+ schemaVersion: 1,
17362
+ generatedAt: input.generatedAt,
17363
+ period: input.period,
17364
+ range: { start: input.start.toISOString(), end: input.end.toISOString() },
17365
+ excludedTests: input.excludedTests,
17366
+ totals: {
17367
+ events: input.total,
17368
+ blocked: totalBlocked,
17369
+ blockRate,
17370
+ userApproved: input.userApproved,
17371
+ userDenied: input.userDenied,
17372
+ timedOut: input.timedOut,
17373
+ hardBlocked: input.hardBlocked,
17374
+ dlpBlocked: input.dlpBlocked,
17375
+ observeDlp: input.observeDlp,
17376
+ loopHits: input.loopHits,
17377
+ unackedDlp: input.unackedDlp
17378
+ },
17379
+ tests: {
17380
+ passes: input.testPasses,
17381
+ fails: input.testFails
17382
+ },
17383
+ cost: {
17384
+ totalUSD: input.cost.claudeUSD + input.cost.codexUSD + input.cost.geminiUSD,
17385
+ claudeUSD: input.cost.claudeUSD,
17386
+ codexUSD: input.cost.codexUSD,
17387
+ geminiUSD: input.cost.geminiUSD,
17388
+ inputTokens: input.cost.inputTokens,
17389
+ outputTokens: input.cost.outputTokens,
17390
+ cacheWriteTokens: input.cost.cacheWriteTokens,
17391
+ cacheReadTokens: input.cost.cacheReadTokens,
17392
+ byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
17393
+ byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
17394
+ },
17395
+ byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
17396
+ byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
17397
+ byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
17398
+ byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
17399
+ byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
17400
+ byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
17401
+ trend: {
17402
+ priorBlockRate: input.priorBlockRate,
17403
+ deltaPct
17404
+ }
17405
+ };
17406
+ }
17407
+
17408
+ // src/cli/commands/report.ts
17409
+ var BLOCK_REASON_LABELS = {
17410
+ timeout: "Approval timeout",
17411
+ "smart-rule-block": "Smart rule",
17412
+ "observe-mode-dlp-would-block": "DLP (observe)",
17413
+ "persistent-deny": "Persistent deny",
17414
+ "local-decision": "User denied",
17415
+ "dlp-block": "DLP block",
17416
+ "loop-detected": "Loop detected"
17417
+ };
17418
+ function humanBlockReason(reason) {
17419
+ return BLOCK_REASON_LABELS[reason] ?? reason;
17420
+ }
17421
+ function barStr(value, max, width) {
17422
+ if (max === 0 || width <= 0) return "\u2591".repeat(width);
17423
+ const filled = Math.max(1, Math.round(value / max * width));
17424
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
17425
+ }
17426
+ function colorBar(value, max, width) {
17427
+ const s = barStr(value, max, width);
17428
+ const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
17429
+ return import_chalk13.default.cyan(s.slice(0, filled)) + import_chalk13.default.dim(s.slice(filled));
17430
+ }
17431
+ function fmtDate(d) {
17432
+ const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
17433
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
17434
+ }
17435
+ function num2(n) {
17436
+ return n.toLocaleString();
17437
+ }
17438
+ function fmtCost2(usd) {
17439
+ if (usd < 1e-3) return "< $0.001";
17440
+ if (usd < 1) return "$" + usd.toFixed(4);
17441
+ return "$" + usd.toFixed(2);
16617
17442
  }
16618
17443
  function registerReportCommand(program2) {
16619
17444
  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) => {
16620
- const period = ["today", "7d", "30d", "month"].includes(
17445
+ const period = ["today", "7d", "30d", "90d", "month"].includes(
16621
17446
  options.period
16622
17447
  ) ? options.period : "7d";
16623
- const logPath = import_path36.default.join(import_os31.default.homedir(), ".node9", "audit.log");
16624
- const allEntries = parseAuditLog(logPath);
16625
- const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
16626
- if (unackedDlp.length > 0 && !options.json) {
17448
+ const excludeTests = options.tests === false;
17449
+ const { data, hasAuditFile, responseDlpEntries } = aggregateReportFromAudit(period, {
17450
+ excludeTests
17451
+ });
17452
+ if (data.unackedDlp > 0 && !options.json) {
16627
17453
  console.log("");
16628
17454
  console.log(
16629
17455
  import_chalk13.default.bgRed.white.bold(
16630
- ` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
17456
+ ` \u26A0\uFE0F DLP ALERT: ${data.unackedDlp} secret${data.unackedDlp !== 1 ? "s" : ""} found in Claude response text `
16631
17457
  ) + " " + import_chalk13.default.yellow("\u2192 run: node9 dlp")
16632
17458
  );
16633
17459
  }
16634
- if (allEntries.length === 0 && !options.json) {
17460
+ if (!hasAuditFile && !options.json) {
16635
17461
  console.log(
16636
17462
  import_chalk13.default.yellow("\n No audit data found. Run node9 with Claude Code to generate entries.\n")
16637
17463
  );
16638
17464
  return;
16639
17465
  }
16640
- const { start, end } = getDateRange(period);
16641
- const {
16642
- total: claudeCostUSD,
16643
- byDay: costByDay,
16644
- byModel: costByModel,
16645
- inputTokens: costInputTokens,
16646
- outputTokens: costOutputTokens,
16647
- cacheWriteTokens: costCacheWrite,
16648
- cacheReadTokens: costCacheRead
16649
- } = loadClaudeCost(start, end);
16650
- const {
16651
- total: codexCostUSD,
16652
- byDay: codexCostByDay,
16653
- toolCalls: codexToolCalls
16654
- } = loadCodexCost(start, end);
16655
- const costUSD = claudeCostUSD + codexCostUSD;
16656
- for (const [day, c] of codexCostByDay) {
16657
- costByDay.set(day, (costByDay.get(day) ?? 0) + c);
16658
- }
16659
- const periodMs = end.getTime() - start.getTime();
16660
- const priorEnd = new Date(start.getTime() - 1);
16661
- const priorStart = new Date(start.getTime() - periodMs);
16662
- const priorEntries = allEntries.filter((e) => {
16663
- if (e.source === "post-hook") return false;
16664
- const ts = new Date(e.ts);
16665
- return ts >= priorStart && ts <= priorEnd;
16666
- });
16667
- const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
16668
- const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
16669
- const excludeTests = options.tests === false;
16670
- const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
16671
- let filteredTestCount = 0;
16672
- const entries = allEntries.filter((e) => {
16673
- if (e.source === "post-hook") return false;
16674
- if (e.source === "response-dlp") return false;
16675
- const ts = new Date(e.ts);
16676
- if (ts < start || ts > end) return false;
16677
- if (excludeTests && isTestEntry(e, testTs)) {
16678
- filteredTestCount++;
16679
- return false;
16680
- }
16681
- return true;
16682
- });
16683
- if (entries.length === 0 && !options.json) {
17466
+ if (options.json) {
17467
+ const envelope = buildReportJson(data);
17468
+ process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
17469
+ return;
17470
+ }
17471
+ if (data.total === 0) {
16684
17472
  console.log(import_chalk13.default.yellow(`
16685
17473
  No activity for period "${period}".
16686
17474
  `));
16687
17475
  return;
16688
17476
  }
16689
- let userApproved = 0;
16690
- let userDenied = 0;
16691
- let timedOut = 0;
16692
- let hardBlocked = 0;
16693
- let dlpBlocked = 0;
16694
- let observeDlp = 0;
16695
- let loopHits = 0;
16696
- let testPasses = 0;
16697
- let testFails = 0;
16698
- const toolMap = /* @__PURE__ */ new Map();
16699
- const blockMap = /* @__PURE__ */ new Map();
16700
- const agentMap = /* @__PURE__ */ new Map();
16701
- const mcpMap = /* @__PURE__ */ new Map();
16702
- const dailyMap = /* @__PURE__ */ new Map();
16703
- const hourMap = /* @__PURE__ */ new Map();
16704
- for (const e of entries) {
16705
- const allow = isAllow(e.decision);
16706
- const dateKey = e.ts.slice(0, 10);
16707
- const userInteracted = e.source === "daemon";
16708
- if (userInteracted) {
16709
- if (allow) userApproved++;
16710
- else userDenied++;
16711
- } else if (!allow) {
16712
- if (e.checkedBy === "timeout") timedOut++;
16713
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
16714
- else if (isDlp(e.checkedBy)) dlpBlocked++;
16715
- else if (e.checkedBy !== "loop-detected") hardBlocked++;
16716
- }
16717
- if (e.checkedBy === "loop-detected") loopHits++;
16718
- const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
16719
- t.calls++;
16720
- if (!allow) t.blocked++;
16721
- toolMap.set(e.tool, t);
16722
- if (!allow && e.checkedBy) {
16723
- blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
16724
- }
16725
- if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
16726
- if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
16727
- const hour = new Date(e.ts).getHours();
16728
- hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
16729
- const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
16730
- d.calls++;
16731
- if (!allow) d.blocked++;
16732
- dailyMap.set(dateKey, d);
16733
- }
16734
- for (const e of allEntries) {
16735
- if (e.source !== "test-result") continue;
16736
- const ts = new Date(e.ts);
16737
- if (ts < start || ts > end) continue;
16738
- if (e.testResult === "pass") testPasses++;
16739
- else if (e.testResult === "fail") testFails++;
16740
- }
16741
- if (codexToolCalls > 0) agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexToolCalls);
16742
- if (options.json) {
16743
- const envelope = buildReportJson({
16744
- period,
16745
- start,
16746
- end,
16747
- excludedTests: filteredTestCount,
16748
- total: entries.length,
16749
- userApproved,
16750
- userDenied,
16751
- timedOut,
16752
- hardBlocked,
16753
- dlpBlocked,
16754
- observeDlp,
16755
- loopHits,
16756
- testPasses,
16757
- testFails,
16758
- unackedDlp: unackedDlp.length,
16759
- priorBlockRate,
16760
- cost: {
16761
- claudeUSD: claudeCostUSD,
16762
- codexUSD: codexCostUSD,
16763
- inputTokens: costInputTokens,
16764
- outputTokens: costOutputTokens,
16765
- cacheWriteTokens: costCacheWrite,
16766
- cacheReadTokens: costCacheRead,
16767
- byDay: costByDay,
16768
- byModel: costByModel
16769
- },
16770
- toolMap,
16771
- blockMap,
16772
- agentMap,
16773
- mcpMap,
16774
- dailyMap,
16775
- hourMap,
16776
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
16777
- });
16778
- process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
16779
- return;
16780
- }
16781
- const total = entries.length;
16782
- const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
16783
- const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
16784
- const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
16785
- const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
16786
- const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
16787
- const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
16788
- const W = Math.min(process.stdout.columns || 80, 100);
16789
- const INNER = W - 4;
16790
- const COL = Math.floor(INNER / 2) - 1;
16791
- const LABEL = 24;
16792
- const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
16793
- const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num2(v.calls).length), 1);
16794
- const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num2(v).length), 1);
16795
- const line = import_chalk13.default.dim("\u2500".repeat(W - 2));
16796
- const periodLabel = {
16797
- today: "Today",
16798
- "7d": "Last 7 Days",
16799
- "30d": "Last 30 Days",
16800
- month: "This Month"
16801
- };
17477
+ renderTerminalReport(data, responseDlpEntries, excludeTests);
17478
+ });
17479
+ }
17480
+ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
17481
+ const {
17482
+ period,
17483
+ start,
17484
+ end,
17485
+ total,
17486
+ excludedTests,
17487
+ userApproved,
17488
+ userDenied,
17489
+ timedOut,
17490
+ hardBlocked,
17491
+ dlpBlocked,
17492
+ observeDlp,
17493
+ loopHits,
17494
+ testPasses,
17495
+ testFails,
17496
+ priorBlockRate,
17497
+ cost: {
17498
+ claudeUSD,
17499
+ codexUSD,
17500
+ geminiUSD,
17501
+ inputTokens: costInputTokens,
17502
+ outputTokens: costOutputTokens,
17503
+ cacheWriteTokens: costCacheWrite,
17504
+ cacheReadTokens: costCacheRead,
17505
+ byDay: costByDay,
17506
+ byModel: costByModel
17507
+ },
17508
+ toolMap,
17509
+ blockMap,
17510
+ agentMap,
17511
+ mcpMap,
17512
+ dailyMap,
17513
+ hourMap
17514
+ } = data;
17515
+ const costUSD = claudeUSD + codexUSD + geminiUSD;
17516
+ const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
17517
+ const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
17518
+ const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
17519
+ const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
17520
+ const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
17521
+ const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
17522
+ const W = Math.min(process.stdout.columns || 80, 100);
17523
+ const INNER = W - 4;
17524
+ const COL = Math.floor(INNER / 2) - 1;
17525
+ const LABEL = 24;
17526
+ const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
17527
+ const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num2(v.calls).length), 1);
17528
+ const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num2(v).length), 1);
17529
+ const line = import_chalk13.default.dim("\u2500".repeat(W - 2));
17530
+ const periodLabel = {
17531
+ today: "Today",
17532
+ "7d": "Last 7 Days",
17533
+ "30d": "Last 30 Days",
17534
+ "90d": "Last 90 Days",
17535
+ month: "This Month"
17536
+ };
17537
+ console.log("");
17538
+ console.log(
17539
+ " " + import_chalk13.default.bold.cyan("\u{1F6E1} node9 Report") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.white(periodLabel[period]) + import_chalk13.default.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + import_chalk13.default.dim(` ${num2(total)} events`) + (excludeTests ? import_chalk13.default.dim(` \u2013tests (\u2013${excludedTests})`) : "")
17540
+ );
17541
+ console.log(" " + line);
17542
+ const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
17543
+ const currentRate = total > 0 ? totalBlocked / total : 0;
17544
+ const trendLabel = (() => {
17545
+ if (priorBlockRate === null) return "";
17546
+ const delta = Math.round((currentRate - priorBlockRate) * 100);
17547
+ if (delta === 0) return "";
17548
+ return " " + (delta > 0 ? import_chalk13.default.red(`\u25B2${delta}%`) : import_chalk13.default.green(`\u25BC${Math.abs(delta)}%`)) + import_chalk13.default.dim(" vs prior");
17549
+ })();
17550
+ const reads = toolMap.get("Read")?.calls ?? 0;
17551
+ const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
17552
+ const ratioLabel = reads > 0 ? import_chalk13.default.dim(`edit/read ${(edits / reads).toFixed(1)}`) : import_chalk13.default.dim("edit/read \u2013");
17553
+ const testLabel = testPasses + testFails > 0 ? import_chalk13.default.dim("tests ") + import_chalk13.default.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + import_chalk13.default.red(`${testFails}\u2717`) : "") : import_chalk13.default.dim("tests \u2013");
17554
+ console.log("");
17555
+ console.log(" " + import_chalk13.default.bold("Protection Summary"));
17556
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
17557
+ console.log(
17558
+ " " + import_chalk13.default.dim("Intercepted") + " " + import_chalk13.default.white(num2(total)) + import_chalk13.default.dim(" tool calls")
17559
+ );
17560
+ console.log("");
17561
+ const COL1 = 18;
17562
+ const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
17563
+ const countStr = colorFn(num2(count));
17564
+ const noteStr = note ? import_chalk13.default.dim(" " + note) : "";
17565
+ console.log(" " + icon + " " + import_chalk13.default.white(label.padEnd(COL1)) + countStr + noteStr);
17566
+ };
17567
+ summaryRow(
17568
+ userApproved > 0 ? import_chalk13.default.green("\u2705") : import_chalk13.default.dim("\u2705"),
17569
+ "User approved",
17570
+ userApproved,
17571
+ userApproved === 0 ? "no popups this period" : void 0,
17572
+ userApproved > 0 ? (s) => import_chalk13.default.green(s) : (s) => import_chalk13.default.dim(s)
17573
+ );
17574
+ summaryRow(
17575
+ userDenied > 0 ? import_chalk13.default.red("\u{1F6AB}") : import_chalk13.default.dim("\u{1F6AB}"),
17576
+ "User denied",
17577
+ userDenied,
17578
+ void 0,
17579
+ userDenied > 0 ? (s) => import_chalk13.default.red(s) : (s) => import_chalk13.default.dim(s)
17580
+ );
17581
+ summaryRow(
17582
+ timedOut > 0 ? import_chalk13.default.yellow("\u23F1") : import_chalk13.default.dim("\u23F1"),
17583
+ "Timed out",
17584
+ timedOut,
17585
+ timedOut > 0 ? "no approval response" : void 0,
17586
+ timedOut > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
17587
+ );
17588
+ summaryRow(
17589
+ hardBlocked > 0 ? import_chalk13.default.red("\u{1F6D1}") : import_chalk13.default.dim("\u{1F6D1}"),
17590
+ "Auto-blocked",
17591
+ hardBlocked,
17592
+ void 0,
17593
+ hardBlocked > 0 ? (s) => import_chalk13.default.red(s) : (s) => import_chalk13.default.dim(s)
17594
+ );
17595
+ summaryRow(
17596
+ dlpBlocked > 0 ? import_chalk13.default.yellow("\u{1F6A8}") : import_chalk13.default.dim("\u{1F6A8}"),
17597
+ "DLP blocked",
17598
+ dlpBlocked,
17599
+ void 0,
17600
+ dlpBlocked > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
17601
+ );
17602
+ summaryRow(
17603
+ observeDlp > 0 ? import_chalk13.default.blue("\u{1F441}") : import_chalk13.default.dim("\u{1F441}"),
17604
+ "DLP (observe)",
17605
+ observeDlp,
17606
+ observeDlp > 0 ? "would-block in strict mode" : void 0,
17607
+ observeDlp > 0 ? (s) => import_chalk13.default.blue(s) : (s) => import_chalk13.default.dim(s)
17608
+ );
17609
+ summaryRow(
17610
+ loopHits > 0 ? import_chalk13.default.yellow("\u{1F504}") : import_chalk13.default.dim("\u{1F504}"),
17611
+ "Loops detected",
17612
+ loopHits,
17613
+ void 0,
17614
+ loopHits > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
17615
+ );
17616
+ if (trendLabel || ratioLabel || testPasses + testFails > 0) {
16802
17617
  console.log("");
16803
- console.log(
16804
- " " + import_chalk13.default.bold.cyan("\u{1F6E1} node9 Report") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.white(periodLabel[period]) + import_chalk13.default.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + import_chalk13.default.dim(` ${num2(total)} events`) + (excludeTests ? import_chalk13.default.dim(` \u2013tests (\u2013${filteredTestCount})`) : "")
16805
- );
16806
- console.log(" " + line);
16807
- const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
16808
- const currentRate = total > 0 ? totalBlocked / total : 0;
16809
- const trendLabel = (() => {
16810
- if (priorBlockRate === null) return "";
16811
- const delta = Math.round((currentRate - priorBlockRate) * 100);
16812
- if (delta === 0) return "";
16813
- return " " + (delta > 0 ? import_chalk13.default.red(`\u25B2${delta}%`) : import_chalk13.default.green(`\u25BC${Math.abs(delta)}%`)) + import_chalk13.default.dim(" vs prior");
16814
- })();
16815
- const reads = toolMap.get("Read")?.calls ?? 0;
16816
- const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
16817
- const ratioLabel = reads > 0 ? import_chalk13.default.dim(`edit/read ${(edits / reads).toFixed(1)}`) : import_chalk13.default.dim("edit/read \u2013");
16818
- const testLabel = testPasses + testFails > 0 ? import_chalk13.default.dim("tests ") + import_chalk13.default.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + import_chalk13.default.red(`${testFails}\u2717`) : "") : import_chalk13.default.dim("tests \u2013");
17618
+ console.log(" " + ratioLabel + " " + testLabel + trendLabel);
17619
+ }
17620
+ console.log("");
17621
+ const toolHeaderRaw = "Top Tools";
17622
+ const blockHeaderRaw = "Top Blocks";
17623
+ console.log(
17624
+ " " + import_chalk13.default.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + import_chalk13.default.bold(blockHeaderRaw)
17625
+ );
17626
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(COL)) + " " + import_chalk13.default.dim("\u2500".repeat(COL)));
17627
+ const rows = Math.max(topTools.length, topBlocks.length, 1);
17628
+ for (let i = 0; i < rows; i++) {
17629
+ let leftStyled = " ".repeat(COL);
17630
+ if (i < topTools.length) {
17631
+ const [tool, { calls }] = topTools[i];
17632
+ const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
17633
+ const countStr = num2(calls).padStart(TOOL_COUNT_W);
17634
+ const b = colorBar(calls, maxTool, BAR);
17635
+ const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
17636
+ const pad = Math.max(0, COL - rawLen);
17637
+ leftStyled = import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.white(countStr) + " ".repeat(pad);
17638
+ }
17639
+ let rightStyled = "";
17640
+ if (i < topBlocks.length) {
17641
+ const [reason, count] = topBlocks[i];
17642
+ const readable = humanBlockReason(reason);
17643
+ const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
17644
+ const countStr = num2(count).padStart(BLOCK_COUNT_W);
17645
+ const b = colorBar(count, maxBlock, BAR);
17646
+ rightStyled = import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.red(countStr);
17647
+ }
17648
+ console.log(" " + leftStyled + " " + rightStyled);
17649
+ }
17650
+ if (topBlocks.length === 0) {
17651
+ console.log(" " + " ".repeat(COL) + " " + import_chalk13.default.dim("nothing blocked \u2713"));
17652
+ }
17653
+ if (agentMap.size >= 1) {
16819
17654
  console.log("");
16820
- console.log(" " + import_chalk13.default.bold("Protection Summary"));
17655
+ console.log(" " + import_chalk13.default.bold("Agents"));
16821
17656
  console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
16822
- console.log(
16823
- " " + import_chalk13.default.dim("Intercepted") + " " + import_chalk13.default.white(num2(total)) + import_chalk13.default.dim(" tool calls")
16824
- );
16825
- console.log("");
16826
- const COL1 = 18;
16827
- const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
16828
- const countStr = colorFn(num2(count));
16829
- const noteStr = note ? import_chalk13.default.dim(" " + note) : "";
16830
- console.log(" " + icon + " " + import_chalk13.default.white(label.padEnd(COL1)) + countStr + noteStr);
16831
- };
16832
- summaryRow(
16833
- userApproved > 0 ? import_chalk13.default.green("\u2705") : import_chalk13.default.dim("\u2705"),
16834
- "User approved",
16835
- userApproved,
16836
- userApproved === 0 ? "no popups this period" : void 0,
16837
- userApproved > 0 ? (s) => import_chalk13.default.green(s) : (s) => import_chalk13.default.dim(s)
16838
- );
16839
- summaryRow(
16840
- userDenied > 0 ? import_chalk13.default.red("\u{1F6AB}") : import_chalk13.default.dim("\u{1F6AB}"),
16841
- "User denied",
16842
- userDenied,
16843
- void 0,
16844
- userDenied > 0 ? (s) => import_chalk13.default.red(s) : (s) => import_chalk13.default.dim(s)
16845
- );
16846
- summaryRow(
16847
- timedOut > 0 ? import_chalk13.default.yellow("\u23F1") : import_chalk13.default.dim("\u23F1"),
16848
- "Timed out",
16849
- timedOut,
16850
- timedOut > 0 ? "no approval response" : void 0,
16851
- timedOut > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
16852
- );
16853
- summaryRow(
16854
- hardBlocked > 0 ? import_chalk13.default.red("\u{1F6D1}") : import_chalk13.default.dim("\u{1F6D1}"),
16855
- "Auto-blocked",
16856
- hardBlocked,
16857
- void 0,
16858
- hardBlocked > 0 ? (s) => import_chalk13.default.red(s) : (s) => import_chalk13.default.dim(s)
16859
- );
16860
- summaryRow(
16861
- dlpBlocked > 0 ? import_chalk13.default.yellow("\u{1F6A8}") : import_chalk13.default.dim("\u{1F6A8}"),
16862
- "DLP blocked",
16863
- dlpBlocked,
16864
- void 0,
16865
- dlpBlocked > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
16866
- );
16867
- summaryRow(
16868
- observeDlp > 0 ? import_chalk13.default.blue("\u{1F441}") : import_chalk13.default.dim("\u{1F441}"),
16869
- "DLP (observe)",
16870
- observeDlp,
16871
- observeDlp > 0 ? "would-block in strict mode" : void 0,
16872
- observeDlp > 0 ? (s) => import_chalk13.default.blue(s) : (s) => import_chalk13.default.dim(s)
16873
- );
16874
- summaryRow(
16875
- loopHits > 0 ? import_chalk13.default.yellow("\u{1F504}") : import_chalk13.default.dim("\u{1F504}"),
16876
- "Loops detected",
16877
- loopHits,
16878
- void 0,
16879
- loopHits > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
16880
- );
16881
- if (trendLabel || ratioLabel || testPasses + testFails > 0) {
16882
- console.log("");
16883
- console.log(" " + ratioLabel + " " + testLabel + trendLabel);
17657
+ const maxAgent = Math.max(...agentMap.values(), 1);
17658
+ for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
17659
+ const label = agent.slice(0, LABEL - 1);
17660
+ const b = colorBar(count, maxAgent, BAR);
17661
+ console.log(" " + import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.white(num2(count)));
16884
17662
  }
17663
+ }
17664
+ if (mcpMap.size > 0) {
16885
17665
  console.log("");
16886
- const toolHeaderRaw = "Top Tools";
16887
- const blockHeaderRaw = "Top Blocks";
16888
- console.log(
16889
- " " + import_chalk13.default.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + import_chalk13.default.bold(blockHeaderRaw)
16890
- );
16891
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(COL)) + " " + import_chalk13.default.dim("\u2500".repeat(COL)));
16892
- const rows = Math.max(topTools.length, topBlocks.length, 1);
16893
- for (let i = 0; i < rows; i++) {
16894
- let leftStyled = " ".repeat(COL);
16895
- if (i < topTools.length) {
16896
- const [tool, { calls }] = topTools[i];
16897
- const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
16898
- const countStr = num2(calls).padStart(TOOL_COUNT_W);
16899
- const b = colorBar(calls, maxTool, BAR);
16900
- const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
16901
- const pad = Math.max(0, COL - rawLen);
16902
- leftStyled = import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.white(countStr) + " ".repeat(pad);
16903
- }
16904
- let rightStyled = "";
16905
- if (i < topBlocks.length) {
16906
- const [reason, count] = topBlocks[i];
16907
- const readable = humanBlockReason(reason);
16908
- const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
16909
- const countStr = num2(count).padStart(BLOCK_COUNT_W);
16910
- const b = colorBar(count, maxBlock, BAR);
16911
- rightStyled = import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.red(countStr);
16912
- }
16913
- console.log(" " + leftStyled + " " + rightStyled);
16914
- }
16915
- if (topBlocks.length === 0) {
16916
- console.log(" " + " ".repeat(COL) + " " + import_chalk13.default.dim("nothing blocked \u2713"));
16917
- }
16918
- if (agentMap.size >= 1) {
16919
- console.log("");
16920
- console.log(" " + import_chalk13.default.bold("Agents"));
16921
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
16922
- const maxAgent = Math.max(...agentMap.values(), 1);
16923
- for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
16924
- const label = agent.slice(0, LABEL - 1);
16925
- const b = colorBar(count, maxAgent, BAR);
16926
- console.log(" " + import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.white(num2(count)));
16927
- }
16928
- }
16929
- if (mcpMap.size > 0) {
16930
- console.log("");
16931
- console.log(" " + import_chalk13.default.bold("MCP Servers"));
16932
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
16933
- const maxMcp = Math.max(...mcpMap.values(), 1);
16934
- for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
16935
- const label = server.slice(0, LABEL - 1).padEnd(LABEL);
16936
- const b = colorBar(count, maxMcp, BAR);
16937
- console.log(" " + import_chalk13.default.white(label) + b + " " + import_chalk13.default.white(num2(count)));
16938
- }
16939
- }
16940
- if (hourMap.size > 0) {
16941
- const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
16942
- const maxHour = Math.max(...hourMap.values(), 1);
16943
- const bar = Array.from({ length: 24 }, (_, h) => {
16944
- const v = hourMap.get(h) ?? 0;
16945
- return BLOCKS[Math.round(v / maxHour * 8)];
16946
- }).join("");
16947
- console.log("");
16948
- console.log(" " + import_chalk13.default.bold("Hour of Day") + import_chalk13.default.dim(" (local, 0h \u2013 23h)"));
16949
- console.log(" " + import_chalk13.default.cyan(bar));
16950
- console.log(" " + import_chalk13.default.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
16951
- }
16952
- if (dailyList.length > 1) {
16953
- console.log("");
16954
- console.log(" " + import_chalk13.default.bold("Daily Activity"));
16955
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(W - 2)));
16956
- const DAY_BAR = Math.max(8, Math.min(30, W - 36));
16957
- for (const [dateKey, { calls, blocked: db }] of dailyList) {
16958
- const label = fmtDate(dateKey).padEnd(10);
16959
- const b = colorBar(calls, maxDaily, DAY_BAR);
16960
- const dayCost = costByDay.get(dateKey);
16961
- const costNote = dayCost ? import_chalk13.default.magenta(` ${fmtCost2(dayCost)}`) : "";
16962
- const blockNote = db > 0 ? import_chalk13.default.red(` ${db} blocked`) : "";
16963
- console.log(
16964
- " " + import_chalk13.default.dim(label) + " " + b + " " + import_chalk13.default.white(num2(calls)) + blockNote + costNote
16965
- );
16966
- }
16967
- }
16968
- const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
16969
- if (totalTokens > 0) {
16970
- const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
16971
- console.log("");
16972
- console.log(" " + import_chalk13.default.bold("Tokens") + " " + import_chalk13.default.dim(`${num2(totalTokens)} total`));
16973
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
16974
- const TOK_BAR = Math.max(6, Math.min(20, W - 30));
16975
- const TOK_LABEL = 14;
16976
- const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
16977
- const nonCacheRows = [
16978
- ["Input", costInputTokens, import_chalk13.default.cyan(num2(costInputTokens))],
16979
- ["Output", costOutputTokens, import_chalk13.default.white(num2(costOutputTokens))],
16980
- ["Cache write", costCacheWrite, import_chalk13.default.yellow(num2(costCacheWrite))]
16981
- ];
16982
- for (const [label, count, colored] of nonCacheRows) {
16983
- if (count === 0) continue;
16984
- const b = colorBar(count, maxNonCache, TOK_BAR);
16985
- console.log(" " + import_chalk13.default.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
16986
- }
16987
- if (costCacheRead > 0) {
16988
- const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
16989
- const pct = cacheHitPct > 0 ? import_chalk13.default.dim(` ${cacheHitPct}% hit rate`) : "";
16990
- console.log(
16991
- " " + import_chalk13.default.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + import_chalk13.default.green(num2(costCacheRead)) + pct
16992
- );
16993
- }
16994
- }
16995
- if (costUSD > 0) {
16996
- const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
16997
- const avgPerDay = costUSD / periodDays;
16998
- const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
16999
- const costHeaderRight = [
17000
- import_chalk13.default.yellow(fmtCost2(costUSD)),
17001
- import_chalk13.default.dim(`avg ${fmtCost2(avgPerDay)}/day`),
17002
- cacheHitPct > 0 ? import_chalk13.default.dim(`${cacheHitPct}% cache hit`) : null
17003
- ].filter(Boolean).join(import_chalk13.default.dim(" \xB7 "));
17004
- console.log("");
17005
- console.log(" " + import_chalk13.default.bold("Cost") + " " + costHeaderRight);
17006
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
17007
- if (codexCostUSD > 0)
17008
- costByModel.set(
17009
- "codex (openai)",
17010
- (costByModel.get("codex (openai)") ?? 0) + codexCostUSD
17011
- );
17012
- const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
17013
- const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
17014
- const MODEL_LABEL = 22;
17015
- const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
17016
- for (const [model, cost] of modelList) {
17017
- const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
17018
- const b = colorBar(cost, maxModelCost, MODEL_BAR);
17019
- console.log(
17020
- " " + import_chalk13.default.white(label.padEnd(MODEL_LABEL)) + b + " " + import_chalk13.default.yellow(fmtCost2(cost))
17021
- );
17022
- }
17666
+ console.log(" " + import_chalk13.default.bold("MCP Servers"));
17667
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
17668
+ const maxMcp = Math.max(...mcpMap.values(), 1);
17669
+ for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
17670
+ const label = server.slice(0, LABEL - 1).padEnd(LABEL);
17671
+ const b = colorBar(count, maxMcp, BAR);
17672
+ console.log(" " + import_chalk13.default.white(label) + b + " " + import_chalk13.default.white(num2(count)));
17673
+ }
17674
+ }
17675
+ if (hourMap.size > 0) {
17676
+ const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
17677
+ const maxHour = Math.max(...hourMap.values(), 1);
17678
+ const bar = Array.from({ length: 24 }, (_, h) => {
17679
+ const v = hourMap.get(h) ?? 0;
17680
+ return BLOCKS[Math.round(v / maxHour * 8)];
17681
+ }).join("");
17682
+ console.log("");
17683
+ console.log(" " + import_chalk13.default.bold("Hour of Day") + import_chalk13.default.dim(" (local, 0h \u2013 23h)"));
17684
+ console.log(" " + import_chalk13.default.cyan(bar));
17685
+ console.log(" " + import_chalk13.default.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
17686
+ }
17687
+ if (dailyList.length > 1) {
17688
+ console.log("");
17689
+ console.log(" " + import_chalk13.default.bold("Daily Activity"));
17690
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(W - 2)));
17691
+ const DAY_BAR = Math.max(8, Math.min(30, W - 36));
17692
+ for (const [dateKey, { calls, blocked: db }] of dailyList) {
17693
+ const label = fmtDate(dateKey).padEnd(10);
17694
+ const b = colorBar(calls, maxDaily, DAY_BAR);
17695
+ const dayCost = costByDay.get(dateKey);
17696
+ const costNote = dayCost ? import_chalk13.default.magenta(` ${fmtCost2(dayCost)}`) : "";
17697
+ const blockNote = db > 0 ? import_chalk13.default.red(` ${db} blocked`) : "";
17698
+ console.log(
17699
+ " " + import_chalk13.default.dim(label) + " " + b + " " + import_chalk13.default.white(num2(calls)) + blockNote + costNote
17700
+ );
17023
17701
  }
17024
- const responseDlpEntries = allEntries.filter((e) => {
17025
- if (e.source !== "response-dlp") return false;
17026
- const ts = new Date(e.ts);
17027
- return ts >= start && ts <= end;
17028
- });
17029
- if (responseDlpEntries.length > 0) {
17030
- console.log("");
17702
+ }
17703
+ const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
17704
+ if (totalTokens > 0) {
17705
+ const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
17706
+ console.log("");
17707
+ console.log(" " + import_chalk13.default.bold("Tokens") + " " + import_chalk13.default.dim(`${num2(totalTokens)} total`));
17708
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
17709
+ const TOK_BAR = Math.max(6, Math.min(20, W - 30));
17710
+ const TOK_LABEL = 14;
17711
+ const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
17712
+ const nonCacheRows = [
17713
+ ["Input", costInputTokens, import_chalk13.default.cyan(num2(costInputTokens))],
17714
+ ["Output", costOutputTokens, import_chalk13.default.white(num2(costOutputTokens))],
17715
+ ["Cache write", costCacheWrite, import_chalk13.default.yellow(num2(costCacheWrite))]
17716
+ ];
17717
+ for (const [label, count, colored] of nonCacheRows) {
17718
+ if (count === 0) continue;
17719
+ const b = colorBar(count, maxNonCache, TOK_BAR);
17720
+ console.log(" " + import_chalk13.default.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
17721
+ }
17722
+ if (costCacheRead > 0) {
17723
+ const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
17724
+ const pct = cacheHitPct > 0 ? import_chalk13.default.dim(` ${cacheHitPct}% hit rate`) : "";
17031
17725
  console.log(
17032
- " " + import_chalk13.default.red.bold("\u26A0\uFE0F Response DLP") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.red(
17033
- `${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
17034
- )
17726
+ " " + import_chalk13.default.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + import_chalk13.default.green(num2(costCacheRead)) + pct
17035
17727
  );
17036
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(60, W - 4))));
17728
+ }
17729
+ }
17730
+ if (costUSD > 0) {
17731
+ const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
17732
+ const avgPerDay = costUSD / periodDays;
17733
+ const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
17734
+ const costHeaderRight = [
17735
+ import_chalk13.default.yellow(fmtCost2(costUSD)),
17736
+ import_chalk13.default.dim(`avg ${fmtCost2(avgPerDay)}/day`),
17737
+ cacheHitPct > 0 ? import_chalk13.default.dim(`${cacheHitPct}% cache hit`) : null
17738
+ ].filter(Boolean).join(import_chalk13.default.dim(" \xB7 "));
17739
+ console.log("");
17740
+ console.log(" " + import_chalk13.default.bold("Cost") + " " + costHeaderRight);
17741
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
17742
+ if (codexUSD > 0)
17743
+ costByModel.set("codex (openai)", (costByModel.get("codex (openai)") ?? 0) + codexUSD);
17744
+ if (geminiUSD > 0)
17745
+ costByModel.set("gemini (google)", (costByModel.get("gemini (google)") ?? 0) + geminiUSD);
17746
+ const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
17747
+ const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
17748
+ const MODEL_LABEL = 22;
17749
+ const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
17750
+ for (const [model, cost] of modelList) {
17751
+ const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
17752
+ const b = colorBar(cost, maxModelCost, MODEL_BAR);
17037
17753
  console.log(
17038
- " " + import_chalk13.default.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
17754
+ " " + import_chalk13.default.white(label.padEnd(MODEL_LABEL)) + b + " " + import_chalk13.default.yellow(fmtCost2(cost))
17039
17755
  );
17040
- console.log(" " + import_chalk13.default.yellow("Rotate affected keys immediately."));
17041
- for (const e of responseDlpEntries.slice(0, 5)) {
17042
- const ts = import_chalk13.default.dim(fmtDate(e.ts) + " ");
17043
- const pattern = import_chalk13.default.red(e.dlpPattern ?? "DLP");
17044
- const sample = import_chalk13.default.gray(e.dlpSample ?? "");
17045
- console.log(` ${ts}${pattern} ${sample}`);
17046
- }
17047
- if (responseDlpEntries.length > 5) {
17048
- console.log(import_chalk13.default.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
17049
- }
17050
17756
  }
17757
+ }
17758
+ if (responseDlpEntries.length > 0) {
17051
17759
  console.log("");
17052
17760
  console.log(
17053
- " " + import_chalk13.default.dim("node9 audit --deny") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.dim("node9 report --period today|7d|30d|month --no-tests")
17761
+ " " + import_chalk13.default.red.bold("\u26A0\uFE0F Response DLP") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.red(
17762
+ `${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
17763
+ )
17054
17764
  );
17055
- console.log("");
17056
- });
17765
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(60, W - 4))));
17766
+ console.log(
17767
+ " " + import_chalk13.default.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
17768
+ );
17769
+ console.log(" " + import_chalk13.default.yellow("Rotate affected keys immediately."));
17770
+ for (const e of responseDlpEntries.slice(0, 5)) {
17771
+ const ts = import_chalk13.default.dim(fmtDate(e.ts) + " ");
17772
+ const pattern = import_chalk13.default.red(e.dlpPattern ?? "DLP");
17773
+ const sample = import_chalk13.default.gray(e.dlpSample ?? "");
17774
+ console.log(` ${ts}${pattern} ${sample}`);
17775
+ }
17776
+ if (responseDlpEntries.length > 5) {
17777
+ console.log(import_chalk13.default.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
17778
+ }
17779
+ }
17780
+ console.log("");
17781
+ console.log(
17782
+ " " + import_chalk13.default.dim("node9 audit --deny") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.dim("node9 report --period today|7d|30d|month --no-tests")
17783
+ );
17784
+ console.log("");
17057
17785
  }
17058
17786
 
17059
17787
  // src/cli/commands/daemon-cmd.ts