@node9/proxy 1.19.3 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -99,12 +99,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
99
99
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
100
100
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
101
101
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
102
+ const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
102
103
  appendToLog(LOCAL_AUDIT_LOG, {
103
104
  ts: (/* @__PURE__ */ new Date()).toISOString(),
104
105
  tool: toolName,
105
106
  ...argsField,
106
107
  decision,
107
108
  checkedBy,
109
+ ...ruleNameField,
108
110
  ...testRun,
109
111
  agent: meta?.agent,
110
112
  mcpServer: meta?.mcpServer,
@@ -691,7 +693,12 @@ function analyzeFsOperationImpl(command) {
691
693
  for (const p of paths) {
692
694
  for (const sp of SENSITIVE_PATH_RULES) {
693
695
  if (sp.match(p)) {
694
- result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
696
+ result = {
697
+ ruleName: sp.rule,
698
+ verdict: sp.verdict ?? "block",
699
+ reason: sp.reason,
700
+ path: p
701
+ };
695
702
  return false;
696
703
  }
697
704
  }
@@ -1692,7 +1699,11 @@ function extractCanonicalFindings(call, ctx) {
1692
1699
  })
1693
1700
  );
1694
1701
  }
1695
- if (PRIVILEGE_ESCALATION_RE.test(command)) {
1702
+ const ast = analyzeShellCommand(command);
1703
+ const sudoVariant = ast.actions.includes("sudo") || ast.actions.includes("su");
1704
+ const chmodVariant = ast.actions.includes("chmod") && (ast.allTokens.includes("777") || ast.allTokens.includes("0777") || ast.allTokens.includes("+x"));
1705
+ const chownVariant = ast.actions.includes("chown") && ast.allTokens.includes("root");
1706
+ if (sudoVariant || chmodVariant || chownVariant) {
1696
1707
  out.push(
1697
1708
  makeFinding({
1698
1709
  type: "privilege-escalation",
@@ -1824,7 +1835,7 @@ function* stringValues(obj, depth = 0) {
1824
1835
  }
1825
1836
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
1826
1837
  }
1827
- var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, PRIVILEGE_ESCALATION_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE;
1838
+ var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE;
1828
1839
  var init_dist = __esm({
1829
1840
  "packages/policy-engine/dist/index.mjs"() {
1830
1841
  "use strict";
@@ -2341,15 +2352,50 @@ var init_dist = __esm({
2341
2352
  match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
2342
2353
  },
2343
2354
  {
2355
+ // Mirrors the JSON shield's `.env` pattern (project-jail.json's
2356
+ // review-read-env-any-tool) so the AST FS-op path catches the
2357
+ // same set the regex shield does — including Next.js / Vite's
2358
+ // `.env.<env>.local` double-suffix overrides which are commonly
2359
+ // gitignored AND commonly contain real secrets.
2360
+ //
2361
+ // Intentional non-matches (dev fixtures): .env.example, .env.sample,
2362
+ // .env.template, .env.test, .envrc. See shields.test.ts:983-995
2363
+ // for the canonical test-asserted contract.
2344
2364
  rule: "shield:project-jail:block-read-env",
2345
2365
  reason: "Reading .env files is blocked by project-jail shield",
2346
- match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
2366
+ match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
2367
+ p
2368
+ )
2347
2369
  },
2348
2370
  {
2349
- rule: "shield:project-jail:block-read-credentials",
2350
- reason: "Reading credential files is blocked by project-jail shield",
2351
- match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
2352
- p
2371
+ // verdict: 'review' (not 'block') is a deliberate design choice
2372
+ // documented in commit 29327a8. SSH keys and AWS credentials are
2373
+ // cryptographic material with no legitimate read use-case for
2374
+ // an AI agent → hard `block`. But .netrc / .npmrc / .docker /
2375
+ // .kube / gcloud are CONFIG files that hold tokens AND have
2376
+ // legitimate diagnostic reads ("which registry am I configured
2377
+ // for", "what cluster am I on"). Hard-blocking those creates
2378
+ // friction without much safety win because the review gate
2379
+ // still catches genuine exfiltration attempts.
2380
+ //
2381
+ // The review gate FAILS CLOSED on timeout (daemon.approvalTimeoutMs
2382
+ // returns a deny verdict via the orchestrator's timeout branch),
2383
+ // so a stuck or unattended approval does NOT silently grant
2384
+ // credential access. If the threat model demands strict block,
2385
+ // a future per-shield strict-mode toggle is the right fix —
2386
+ // not a regex-level upgrade here.
2387
+ rule: "shield:project-jail:review-read-credentials",
2388
+ reason: "Reading credential files requires approval (project-jail shield)",
2389
+ verdict: "review",
2390
+ match: (p) => (
2391
+ // .kube/config holds Kubernetes cluster credentials and was
2392
+ // flagged as missing by the node9-pr-agent review (the comment
2393
+ // above mentioned .kube but the regex didn't include it — a
2394
+ // textbook code-comment vs code drift). The JSON shield's
2395
+ // review-read-credentials-any-tool already had it. Now aligned.
2396
+ /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials|\.kube[\\/]config)$/i.test(
2397
+ p
2398
+ )
2353
2399
  )
2354
2400
  }
2355
2401
  ];
@@ -2365,7 +2411,7 @@ var init_dist = __esm({
2365
2411
  "shield:project-jail:block-read-ssh",
2366
2412
  "shield:project-jail:block-read-aws",
2367
2413
  "shield:project-jail:block-read-env",
2368
- "shield:project-jail:block-read-credentials"
2414
+ "shield:project-jail:review-read-credentials"
2369
2415
  ]);
2370
2416
  FS_OP_CACHE_MAX = 5e3;
2371
2417
  fsOpCache = /* @__PURE__ */ new Map();
@@ -3053,7 +3099,7 @@ var init_dist = __esm({
3053
3099
  {
3054
3100
  field: "command",
3055
3101
  op: "matches",
3056
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
3102
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
3057
3103
  flags: "i"
3058
3104
  }
3059
3105
  ],
@@ -3067,7 +3113,7 @@ var init_dist = __esm({
3067
3113
  {
3068
3114
  field: "command",
3069
3115
  op: "matches",
3070
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
3116
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
3071
3117
  flags: "i"
3072
3118
  }
3073
3119
  ],
@@ -3089,7 +3135,7 @@ var init_dist = __esm({
3089
3135
  reason: "Reading .env files is blocked by project-jail shield"
3090
3136
  },
3091
3137
  {
3092
- name: "shield:project-jail:block-read-credentials",
3138
+ name: "shield:project-jail:review-read-credentials",
3093
3139
  tool: "bash",
3094
3140
  conditions: [
3095
3141
  {
@@ -3099,8 +3145,64 @@ var init_dist = __esm({
3099
3145
  flags: "i"
3100
3146
  }
3101
3147
  ],
3148
+ verdict: "review",
3149
+ reason: "Reading credential files requires approval (project-jail shield)"
3150
+ },
3151
+ {
3152
+ name: "shield:project-jail:block-read-ssh-any-tool",
3153
+ tool: "*",
3154
+ conditions: [
3155
+ {
3156
+ field: "file_path",
3157
+ op: "matches",
3158
+ value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
3159
+ flags: "i"
3160
+ }
3161
+ ],
3162
+ verdict: "block",
3163
+ reason: "Reading SSH private keys is blocked by project-jail shield"
3164
+ },
3165
+ {
3166
+ name: "shield:project-jail:block-read-aws-any-tool",
3167
+ tool: "*",
3168
+ conditions: [
3169
+ {
3170
+ field: "file_path",
3171
+ op: "matches",
3172
+ value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
3173
+ flags: "i"
3174
+ }
3175
+ ],
3102
3176
  verdict: "block",
3103
- reason: "Reading credential files is blocked by project-jail shield"
3177
+ reason: "Reading AWS credentials is blocked by project-jail shield"
3178
+ },
3179
+ {
3180
+ name: "shield:project-jail:review-read-env-any-tool",
3181
+ tool: "*",
3182
+ conditions: [
3183
+ {
3184
+ field: "file_path",
3185
+ op: "matches",
3186
+ value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
3187
+ flags: "i"
3188
+ }
3189
+ ],
3190
+ verdict: "review",
3191
+ reason: "Reading .env files requires approval (project-jail shield)"
3192
+ },
3193
+ {
3194
+ name: "shield:project-jail:review-read-credentials-any-tool",
3195
+ tool: "*",
3196
+ conditions: [
3197
+ {
3198
+ field: "file_path",
3199
+ op: "matches",
3200
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
3201
+ flags: "i"
3202
+ }
3203
+ ],
3204
+ verdict: "review",
3205
+ reason: "Reading credential files requires approval (project-jail shield)"
3104
3206
  }
3105
3207
  ],
3106
3208
  dangerousWords: []
@@ -3224,7 +3326,6 @@ var init_dist = __esm({
3224
3326
  LOOP_THRESHOLD_FOR_WASTE = 3;
3225
3327
  COST_PER_LOOP_ITER_USD = 6e-3;
3226
3328
  DESTRUCTIVE_OP_RE = /\brm\s+-[rRf]+\b|\bDROP\s+(TABLE|DATABASE|COLLECTION|SCHEMA)\b|\bTRUNCATE\s+TABLE\b|\bgit\s+push\s+(--force|-f)\b|\bFLUSHALL\b|\bFLUSHDB\b|\bkubectl\s+delete\b|\bhelm\s+uninstall\b/i;
3227
- PRIVILEGE_ESCALATION_RE = /\b(sudo|su)\b\s+[a-z]|\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
3228
3329
  SENSITIVE_PATH_RE = /\.aws\/(credentials|config)\b|\.ssh\/(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b|\.env(\.|$|\b)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
3229
3330
  FILE_TOOLS = /* @__PURE__ */ new Set([
3230
3331
  "read",
@@ -3244,7 +3345,7 @@ var init_dist = __esm({
3244
3345
  PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
3245
3346
  PII_CC_RE = /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
3246
3347
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
3247
- CANONICAL_EXTRACTOR_VERSION = "canonical-v1";
3348
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
3248
3349
  DEDUPE_PREVIEW_LEN = 120;
3249
3350
  TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
3250
3351
  /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
@@ -4788,7 +4889,7 @@ async function waitForDaemonDecision(id, signal) {
4788
4889
  if (signal) signal.removeEventListener("abort", onAbort);
4789
4890
  }
4790
4891
  }
4791
- async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
4892
+ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent) {
4792
4893
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
4793
4894
  const res = await fetch(`${base}/check`, {
4794
4895
  method: "POST",
@@ -4799,7 +4900,12 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
4799
4900
  slackDelegated: true,
4800
4901
  agent: meta?.agent,
4801
4902
  mcpServer: meta?.mcpServer,
4802
- ...riskMetadata && { riskMetadata }
4903
+ ...riskMetadata && { riskMetadata },
4904
+ // fromCLI=true tells the daemon the CLI already sent the activity
4905
+ // event via socket. Same contract as registerDaemonEntry — without
4906
+ // it the daemon double-emits 'activity' for cloud-enforced flows.
4907
+ fromCLI: socketActivitySent !== false,
4908
+ activityId
4803
4909
  }),
4804
4910
  signal: AbortSignal.timeout(3e3)
4805
4911
  });
@@ -5762,7 +5868,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5762
5868
  args,
5763
5869
  "deny",
5764
5870
  "smart-rule-block-override",
5765
- meta,
5871
+ // Same rationale as the smart-rule-block path above —
5872
+ // pass the specific rule name so [2] SHIELDS can
5873
+ // attribute this override-block to its owning shield.
5874
+ { ...meta, ruleName: policyResult.ruleName },
5766
5875
  hashAuditArgs
5767
5876
  );
5768
5877
  if (approvers.cloud && creds?.apiKey)
@@ -5792,7 +5901,20 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5792
5901
  }
5793
5902
  } else {
5794
5903
  if (!isManual)
5795
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
5904
+ appendLocalAudit(
5905
+ toolName,
5906
+ args,
5907
+ "deny",
5908
+ "smart-rule-block",
5909
+ // Include policyResult.ruleName so the [2] Report SHIELDS
5910
+ // panel can attribute this block to its specific shield
5911
+ // (e.g. `shield:project-jail:block-read-ssh`) via the
5912
+ // rule→shield map. checkedBy stays as the generic
5913
+ // `smart-rule-block` for backward compat with existing
5914
+ // log readers.
5915
+ { ...meta, ruleName: policyResult.ruleName },
5916
+ hashAuditArgs
5917
+ );
5796
5918
  if (approvers.cloud && creds?.apiKey)
5797
5919
  auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
5798
5920
  ruleName: policyResult.ruleName,
@@ -5940,7 +6062,14 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5940
6062
  let daemonAllowCount = 1;
5941
6063
  if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
5942
6064
  if (cloudEnforced && cloudRequestId) {
5943
- const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
6065
+ const viewer = await notifyDaemonViewer(
6066
+ toolName,
6067
+ args,
6068
+ meta,
6069
+ riskMetadata,
6070
+ options?.activityId,
6071
+ options?.socketActivitySent
6072
+ ).catch(() => null);
5944
6073
  viewerId = viewer?.id ?? null;
5945
6074
  daemonEntryId = viewerId;
5946
6075
  if (viewer) daemonAllowCount = viewer.allowCount;
@@ -7606,9 +7735,61 @@ function computeLoopWaste(loops, totalToolCalls) {
7606
7735
  const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
7607
7736
  return { wastedCalls, wastePct };
7608
7737
  }
7738
+ function rollupByShield(sections, topRulesPerShield = 3) {
7739
+ const out = [];
7740
+ for (const section of sections) {
7741
+ if (section.sourceType !== "shield") continue;
7742
+ if (!section.shieldKey) continue;
7743
+ const totalCatches = section.blockedCount + section.reviewCount;
7744
+ const topRuleLabels = [...section.rules].sort((a, b) => b.findings.length - a.findings.length).slice(0, topRulesPerShield).map((r) => r.findings.length > 1 ? `${r.name} \xD7${r.findings.length}` : r.name);
7745
+ out.push({
7746
+ shieldName: section.shieldKey,
7747
+ totalCatches,
7748
+ blockCatches: section.blockedCount,
7749
+ reviewCatches: section.reviewCount,
7750
+ topRuleLabels
7751
+ });
7752
+ }
7753
+ return out.sort((a, b) => b.totalCatches - a.totalCatches);
7754
+ }
7755
+ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
7756
+ const inner = width - 4;
7757
+ const out = [];
7758
+ const titlePad = ` ${title} `;
7759
+ const titleSegment = titlePad.length <= inner ? titlePad : titlePad.slice(0, inner);
7760
+ const dashFill = "\u2500".repeat(Math.max(0, inner - titleSegment.length));
7761
+ out.push(chalk3.dim("\u256D\u2500") + chalk3.bold(titleSegment) + chalk3.dim(`${dashFill}\u2500\u256E`));
7762
+ for (const line of bodyLines) {
7763
+ const padding = " ".repeat(Math.max(0, inner - line.width));
7764
+ out.push(chalk3.dim("\u2502 ") + line.rendered + padding + chalk3.dim(" \u2502"));
7765
+ }
7766
+ out.push(chalk3.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
7767
+ return out;
7768
+ }
7769
+ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
7770
+ const t = new Date(timestamp).getTime();
7771
+ if (Number.isNaN(t)) return "?";
7772
+ const days = Math.floor((now.getTime() - t) / 864e5);
7773
+ if (days < 1) return "today";
7774
+ if (days > 90) return "90d+";
7775
+ return `${days}d`;
7776
+ }
7777
+ var PANEL_WIDTH;
7609
7778
  var init_scan_derive = __esm({
7610
7779
  "src/cli/render/scan-derive.ts"() {
7611
7780
  "use strict";
7781
+ PANEL_WIDTH = 76;
7782
+ }
7783
+ });
7784
+
7785
+ // src/protection.ts
7786
+ var PROTECTIVE_SHIELD_DISCOUNTS;
7787
+ var init_protection = __esm({
7788
+ "src/protection.ts"() {
7789
+ "use strict";
7790
+ PROTECTIVE_SHIELD_DISCOUNTS = {
7791
+ "project-jail": 0.7
7792
+ };
7612
7793
  }
7613
7794
  });
7614
7795
 
@@ -7798,6 +7979,7 @@ async function ensurePricingLoaded() {
7798
7979
  if (fromDisk && Object.keys(fromDisk).length > 0) {
7799
7980
  memCache = fromDisk;
7800
7981
  memCacheAt = Date.now();
7982
+ lookupCache.clear();
7801
7983
  return;
7802
7984
  }
7803
7985
  const fetched = await fetchLiteLLMPricing();
@@ -7805,30 +7987,42 @@ async function ensurePricingLoaded() {
7805
7987
  memCache = fetched;
7806
7988
  memCacheAt = Date.now();
7807
7989
  writeCache(fetched);
7990
+ lookupCache.clear();
7808
7991
  return;
7809
7992
  }
7810
7993
  memCache = { ...BUNDLED_PRICING };
7811
7994
  memCacheAt = Date.now();
7995
+ lookupCache.clear();
7812
7996
  }
7813
7997
  function pricingFor(model) {
7814
7998
  const norm = normalizeModel(model);
7999
+ const cached = lookupCache.get(norm);
8000
+ if (cached !== void 0) return cached;
7815
8001
  const sources = [];
7816
8002
  if (memCache) sources.push(memCache);
7817
8003
  sources.push(BUNDLED_PRICING);
8004
+ let resolved = null;
7818
8005
  for (const source of sources) {
7819
8006
  const exact = source[norm];
7820
- if (exact) return exact;
8007
+ if (exact) {
8008
+ resolved = exact;
8009
+ break;
8010
+ }
7821
8011
  let best = null;
7822
8012
  for (const key of Object.keys(source)) {
7823
8013
  if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
7824
8014
  best = key;
7825
8015
  }
7826
8016
  }
7827
- if (best) return source[best];
8017
+ if (best) {
8018
+ resolved = source[best];
8019
+ break;
8020
+ }
7828
8021
  }
7829
- return null;
8022
+ lookupCache.set(norm, resolved);
8023
+ return resolved;
7830
8024
  }
7831
- var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt;
8025
+ var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, lookupCache;
7832
8026
  var init_litellm = __esm({
7833
8027
  "src/pricing/litellm.ts"() {
7834
8028
  "use strict";
@@ -7862,6 +8056,7 @@ var init_litellm = __esm({
7862
8056
  TTL_MS = 24 * 60 * 60 * 1e3;
7863
8057
  memCache = null;
7864
8058
  memCacheAt = 0;
8059
+ lookupCache = /* @__PURE__ */ new Map();
7865
8060
  }
7866
8061
  });
7867
8062
 
@@ -7931,7 +8126,7 @@ function parseJSONLFile(filePath, fallbackWorkingDir) {
7931
8126
  }
7932
8127
  return daily;
7933
8128
  }
7934
- function collectEntries() {
8129
+ function collectEntries(sinceMs) {
7935
8130
  const projectsDir = path18.join(os15.homedir(), ".claude", "projects");
7936
8131
  if (!fs16.existsSync(projectsDir)) return [];
7937
8132
  const combined = /* @__PURE__ */ new Map();
@@ -7956,7 +8151,15 @@ function collectEntries() {
7956
8151
  }
7957
8152
  const fallbackWorkingDir = decodeProjectDirName(dir);
7958
8153
  for (const file of files) {
7959
- const entries = parseJSONLFile(path18.join(dirPath, file), fallbackWorkingDir);
8154
+ const filePath = path18.join(dirPath, file);
8155
+ if (sinceMs !== void 0) {
8156
+ try {
8157
+ if (fs16.statSync(filePath).mtimeMs < sinceMs) continue;
8158
+ } catch {
8159
+ continue;
8160
+ }
8161
+ }
8162
+ const entries = parseJSONLFile(filePath, fallbackWorkingDir);
7960
8163
  for (const [key, e] of entries) {
7961
8164
  const prev = combined.get(key);
7962
8165
  if (prev) {
@@ -8030,6 +8233,7 @@ __export(scan_watermark_exports, {
8030
8233
  markUploadComplete: () => markUploadComplete,
8031
8234
  saveWatermark: () => saveWatermark,
8032
8235
  scanDelta: () => scanDelta,
8236
+ tickForensicBroadcast: () => tickForensicBroadcast,
8033
8237
  tickScanWatcher: () => tickScanWatcher
8034
8238
  });
8035
8239
  import fs17 from "fs";
@@ -8237,6 +8441,25 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
8237
8441
  }
8238
8442
  return findings;
8239
8443
  }
8444
+ async function tickForensicBroadcast(offsets) {
8445
+ const out = [];
8446
+ const files = listJsonlFiles();
8447
+ for (const file of files) {
8448
+ const size = fileSize(file);
8449
+ const offset = offsets.get(file);
8450
+ if (offset === void 0) {
8451
+ offsets.set(file, size);
8452
+ continue;
8453
+ }
8454
+ if (size <= offset) continue;
8455
+ const sessionId = path19.basename(file, ".jsonl");
8456
+ const newOffset = await scanDelta(file, offset, (obj, lineIndex) => {
8457
+ out.push(...extractFindingsFromLine(obj, sessionId, lineIndex));
8458
+ });
8459
+ offsets.set(file, newOffset);
8460
+ }
8461
+ return out;
8462
+ }
8240
8463
  function markUploadComplete() {
8241
8464
  const state = loadWatermark();
8242
8465
  if (state.status === "schema-future") return;
@@ -8772,7 +8995,16 @@ function buildRecurringPatternSet(findings) {
8772
8995
  }
8773
8996
  return recurring;
8774
8997
  }
8775
- function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result) {
8998
+ function emptyScanDedup() {
8999
+ return { findingsKeys: /* @__PURE__ */ new Set(), dlpKeys: /* @__PURE__ */ new Set() };
9000
+ }
9001
+ function findingKey(ruleName, inputPreview, projLabel) {
9002
+ return `${ruleName ?? "<unnamed>"}|${inputPreview}|${projLabel}`;
9003
+ }
9004
+ function dlpKey(patternName, redactedSample, projLabel) {
9005
+ return `${patternName}|${redactedSample}|${projLabel}`;
9006
+ }
9007
+ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result, dedup) {
8776
9008
  const fsVerdict = analyzeFsOperation(command);
8777
9009
  if (!fsVerdict) return false;
8778
9010
  const synthRule = {
@@ -8795,10 +9027,9 @@ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sess
8795
9027
  rule: synthRule
8796
9028
  };
8797
9029
  const inputPreview = preview(input, 120);
8798
- const isDupe = result.findings.some(
8799
- (f) => f.source.rule.name === synthRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
8800
- );
8801
- if (!isDupe) {
9030
+ const k = findingKey(synthRule.name, inputPreview, projLabel);
9031
+ if (!dedup.findingsKeys.has(k)) {
9032
+ dedup.findingsKeys.add(k);
8802
9033
  result.findings.push({
8803
9034
  source: synthSource,
8804
9035
  toolName,
@@ -8883,22 +9114,15 @@ function buildRuleSources() {
8883
9114
  sources.push({ shieldName, shieldLabel: shieldName, sourceType: "shield", rule });
8884
9115
  }
8885
9116
  }
8886
- try {
8887
- const config = getConfig();
8888
- for (const rule of config.policy.smartRules) {
8889
- if (!rule.name) continue;
8890
- if (rule.name.startsWith("shield:")) continue;
8891
- const isCloud = rule.name.startsWith("cloud:");
8892
- const isDefault = DEFAULT_RULE_NAMES.has(rule.name);
8893
- const sourceType = isCloud ? "user" : isDefault ? "default" : "user";
8894
- sources.push({
8895
- shieldName: isCloud ? "cloud" : isDefault ? "default" : "custom",
8896
- shieldLabel: isCloud ? "Cloud Policy" : isDefault ? "Default Rules" : "Your Rules",
8897
- sourceType,
8898
- rule
8899
- });
8900
- }
8901
- } catch {
9117
+ for (const rule of DEFAULT_CONFIG.policy.smartRules) {
9118
+ if (!rule.name) continue;
9119
+ if (rule.name.startsWith("shield:")) continue;
9120
+ sources.push({
9121
+ shieldName: "default",
9122
+ shieldLabel: "Default Rules",
9123
+ sourceType: "default",
9124
+ rule
9125
+ });
8902
9126
  }
8903
9127
  return sources;
8904
9128
  }
@@ -8984,178 +9208,53 @@ function renderProgressBar(done, total, lines) {
8984
9208
  `\r ${chalk5.cyan("Scanning")} [${chalk5.cyan(bar)}] ${chalk5.dim(fileLabel)}${lineLabel} `
8985
9209
  );
8986
9210
  }
8987
- function scanClaudeHistory(startDate, onProgress, onLine) {
8988
- const projectsDir = path21.join(os18.homedir(), ".claude", "projects");
8989
- const result = {
8990
- filesScanned: 0,
8991
- sessions: 0,
8992
- totalToolCalls: 0,
8993
- bashCalls: 0,
8994
- findings: [],
8995
- dlpFindings: [],
8996
- loopFindings: [],
8997
- totalCostUSD: 0,
8998
- firstDate: null,
8999
- lastDate: null,
9000
- sessionsWithEarlySecrets: 0
9001
- };
9002
- if (!fs19.existsSync(projectsDir)) return result;
9003
- let projDirs;
9211
+ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, result, dedup, onProgress, onLine) {
9212
+ result.filesScanned++;
9213
+ result.sessions++;
9214
+ onProgress?.(result.filesScanned);
9215
+ const sessionId = file.replace(/\.jsonl$/, "");
9216
+ let raw;
9004
9217
  try {
9005
- projDirs = fs19.readdirSync(projectsDir);
9218
+ raw = fs19.readFileSync(path21.join(projPath, file), "utf-8");
9006
9219
  } catch {
9007
- return result;
9220
+ return;
9008
9221
  }
9009
- const ruleSources = buildRuleSources();
9010
- for (const proj of projDirs) {
9011
- const projPath = path21.join(projectsDir, proj);
9222
+ const sessionCalls = [];
9223
+ const toolUseFilePaths = /* @__PURE__ */ new Map();
9224
+ let firstDlpTs = null;
9225
+ let firstEditTs = null;
9226
+ for (const line of raw.split("\n")) {
9227
+ if (!line.trim()) continue;
9228
+ onLine?.();
9229
+ let entry;
9012
9230
  try {
9013
- if (!fs19.statSync(projPath).isDirectory()) continue;
9231
+ entry = JSON.parse(line);
9014
9232
  } catch {
9015
9233
  continue;
9016
9234
  }
9017
- const projLabel = stripTerminalEscapes(
9018
- decodeURIComponent(proj).replace(os18.homedir(), "~")
9019
- ).slice(0, 40);
9020
- let files;
9021
- try {
9022
- files = fs19.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
9023
- } catch {
9024
- continue;
9235
+ if (entry.type !== "assistant" && entry.type !== "user") continue;
9236
+ if (startDate && entry.timestamp) {
9237
+ if (new Date(entry.timestamp) < startDate) continue;
9025
9238
  }
9026
- for (const file of files) {
9027
- result.filesScanned++;
9028
- result.sessions++;
9029
- onProgress?.(result.filesScanned);
9030
- const sessionId = file.replace(/\.jsonl$/, "");
9031
- let raw;
9032
- try {
9033
- raw = fs19.readFileSync(path21.join(projPath, file), "utf-8");
9034
- } catch {
9035
- continue;
9036
- }
9037
- const sessionCalls = [];
9038
- const toolUseFilePaths = /* @__PURE__ */ new Map();
9039
- let firstDlpTs = null;
9040
- let firstEditTs = null;
9041
- for (const line of raw.split("\n")) {
9042
- if (!line.trim()) continue;
9043
- onLine?.();
9044
- let entry;
9045
- try {
9046
- entry = JSON.parse(line);
9047
- } catch {
9048
- continue;
9049
- }
9050
- if (entry.type !== "assistant" && entry.type !== "user") continue;
9051
- if (startDate && entry.timestamp) {
9052
- if (new Date(entry.timestamp) < startDate) continue;
9053
- }
9054
- if (entry.timestamp) {
9055
- if (!result.firstDate || entry.timestamp < result.firstDate)
9056
- result.firstDate = entry.timestamp;
9057
- if (!result.lastDate || entry.timestamp > result.lastDate)
9058
- result.lastDate = entry.timestamp;
9059
- }
9060
- if (entry.type === "user") {
9061
- const content2 = entry.message?.content;
9062
- if (Array.isArray(content2)) {
9063
- const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
9064
- if (text) {
9065
- const dlpMatch = scanArgs({ text });
9066
- if (dlpMatch) {
9067
- const isDupe = result.dlpFindings.some(
9068
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9069
- );
9070
- if (!isDupe) {
9071
- result.dlpFindings.push({
9072
- patternName: dlpMatch.patternName,
9073
- redactedSample: dlpMatch.redactedSample,
9074
- toolName: "user-prompt",
9075
- timestamp: entry.timestamp ?? "",
9076
- project: projLabel,
9077
- sessionId,
9078
- agent: "claude"
9079
- });
9080
- }
9081
- }
9082
- }
9083
- for (const block of content2) {
9084
- if (block.type !== "tool_result") continue;
9085
- const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
9086
- if (filePath) {
9087
- const ext = path21.extname(filePath).toLowerCase();
9088
- if (CODE_EXTENSIONS.has(ext)) continue;
9089
- }
9090
- const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
9091
- if (!resultText) continue;
9092
- if (isNode9SelfOutput(resultText)) continue;
9093
- const dlpMatch = scanArgs({ text: resultText });
9094
- if (dlpMatch) {
9095
- if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
9096
- if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9097
- const isDupe = result.dlpFindings.some(
9098
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9099
- );
9100
- if (!isDupe) {
9101
- result.dlpFindings.push({
9102
- patternName: dlpMatch.patternName,
9103
- redactedSample: dlpMatch.redactedSample,
9104
- toolName: "tool-result",
9105
- timestamp: entry.timestamp ?? "",
9106
- project: projLabel,
9107
- sessionId,
9108
- agent: "claude"
9109
- });
9110
- }
9111
- }
9112
- }
9113
- }
9114
- continue;
9115
- }
9116
- const usage = entry.message?.usage;
9117
- const model = entry.message?.model;
9118
- if (usage && model) {
9119
- const p = claudeModelPrice(model);
9120
- if (p) {
9121
- 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;
9122
- }
9123
- }
9124
- const content = entry.message?.content;
9125
- if (!Array.isArray(content)) continue;
9126
- for (const block of content) {
9127
- if (block.type !== "tool_use") continue;
9128
- result.totalToolCalls++;
9129
- const toolName = block.name ?? "";
9130
- const toolNameLower = toolName.toLowerCase();
9131
- const input = block.input ?? {};
9132
- if (block.id && typeof input.file_path === "string") {
9133
- toolUseFilePaths.set(block.id, input.file_path);
9134
- }
9135
- sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
9136
- if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
9137
- result.bashCalls++;
9138
- }
9139
- if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
9140
- firstEditTs = entry.timestamp ?? null;
9141
- }
9142
- const rawCmd = String(input.command ?? "").trimStart();
9143
- if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
9144
- continue;
9145
- const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
9146
- const inputFileExt = inputFilePath ? path21.extname(inputFilePath).toLowerCase() : "";
9147
- if (CODE_EXTENSIONS.has(inputFileExt)) continue;
9148
- const dlpMatch = scanArgs(input);
9239
+ if (entry.timestamp) {
9240
+ if (!result.firstDate || entry.timestamp < result.firstDate)
9241
+ result.firstDate = entry.timestamp;
9242
+ if (!result.lastDate || entry.timestamp > result.lastDate) result.lastDate = entry.timestamp;
9243
+ }
9244
+ if (entry.type === "user") {
9245
+ const content2 = entry.message?.content;
9246
+ if (Array.isArray(content2)) {
9247
+ const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
9248
+ if (text) {
9249
+ const dlpMatch = scanArgs({ text });
9149
9250
  if (dlpMatch) {
9150
- if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9151
- const isDupe = result.dlpFindings.some(
9152
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9153
- );
9154
- if (!isDupe) {
9251
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9252
+ if (!dedup.dlpKeys.has(k)) {
9253
+ dedup.dlpKeys.add(k);
9155
9254
  result.dlpFindings.push({
9156
9255
  patternName: dlpMatch.patternName,
9157
9256
  redactedSample: dlpMatch.redactedSample,
9158
- toolName,
9257
+ toolName: "user-prompt",
9159
9258
  timestamp: entry.timestamp ?? "",
9160
9259
  project: projLabel,
9161
9260
  sessionId,
@@ -9163,85 +9262,234 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
9163
9262
  });
9164
9263
  }
9165
9264
  }
9166
- let astFsMatched = false;
9167
- const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
9168
- if (astRanForBash) {
9169
- astFsMatched = pushFsOpAstFinding(
9170
- String(input.command ?? ""),
9171
- toolName,
9172
- input,
9173
- entry.timestamp ?? "",
9174
- projLabel,
9175
- sessionId,
9176
- "claude",
9177
- result
9178
- );
9265
+ }
9266
+ for (const block of content2) {
9267
+ if (block.type !== "tool_result") continue;
9268
+ const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
9269
+ if (filePath) {
9270
+ const ext = path21.extname(filePath).toLowerCase();
9271
+ if (CODE_EXTENSIONS.has(ext)) continue;
9179
9272
  }
9180
- let ruleMatched = astFsMatched;
9181
- for (const source of ruleSources) {
9182
- const { rule } = source;
9183
- if (rule.verdict === "allow") continue;
9184
- if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
9185
- if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9186
- if (!evaluateSmartConditions(input, rule)) continue;
9187
- const inputPreview = preview(input, 120);
9188
- const isDupe = result.findings.some(
9189
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9190
- );
9191
- if (!isDupe) {
9192
- result.findings.push({
9193
- source,
9194
- toolName,
9195
- input,
9273
+ const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
9274
+ if (!resultText) continue;
9275
+ if (isNode9SelfOutput(resultText)) continue;
9276
+ const dlpMatch = scanArgs({ text: resultText });
9277
+ if (dlpMatch) {
9278
+ if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
9279
+ if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9280
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9281
+ if (!dedup.dlpKeys.has(k)) {
9282
+ dedup.dlpKeys.add(k);
9283
+ result.dlpFindings.push({
9284
+ patternName: dlpMatch.patternName,
9285
+ redactedSample: dlpMatch.redactedSample,
9286
+ toolName: "tool-result",
9196
9287
  timestamp: entry.timestamp ?? "",
9197
9288
  project: projLabel,
9198
9289
  sessionId,
9199
9290
  agent: "claude"
9200
9291
  });
9201
9292
  }
9202
- ruleMatched = true;
9203
- break;
9204
- }
9205
- if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
9206
- const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
9207
- if (shellVerdict) {
9208
- const astRule = {
9209
- name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
9210
- tool: "bash",
9211
- conditions: [],
9212
- verdict: shellVerdict,
9213
- reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9214
- };
9215
- const inputPreview = preview(input, 120);
9216
- const isDupe = result.findings.some(
9217
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9218
- );
9219
- if (!isDupe) {
9220
- result.findings.push({
9221
- source: {
9222
- shieldName: "bash-safe",
9223
- shieldLabel: "bash-safe (AST)",
9224
- sourceType: "shield",
9225
- rule: astRule
9226
- },
9227
- toolName,
9228
- input,
9229
- timestamp: entry.timestamp ?? "",
9230
- project: projLabel,
9231
- sessionId,
9232
- agent: "claude"
9233
- });
9234
- }
9235
- }
9236
9293
  }
9237
9294
  }
9238
9295
  }
9239
- result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "claude"));
9240
- if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
9241
- result.sessionsWithEarlySecrets++;
9296
+ continue;
9297
+ }
9298
+ const usage = entry.message?.usage;
9299
+ const model = entry.message?.model;
9300
+ if (usage && model) {
9301
+ const p = claudeModelPrice(model);
9302
+ if (p) {
9303
+ result.totalCostUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
9242
9304
  }
9243
9305
  }
9244
- }
9306
+ const content = entry.message?.content;
9307
+ if (!Array.isArray(content)) continue;
9308
+ for (const block of content) {
9309
+ if (block.type !== "tool_use") continue;
9310
+ result.totalToolCalls++;
9311
+ const toolName = block.name ?? "";
9312
+ const toolNameLower = toolName.toLowerCase();
9313
+ const input = block.input ?? {};
9314
+ if (block.id && typeof input.file_path === "string") {
9315
+ toolUseFilePaths.set(block.id, input.file_path);
9316
+ }
9317
+ sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
9318
+ if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
9319
+ result.bashCalls++;
9320
+ }
9321
+ if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
9322
+ firstEditTs = entry.timestamp ?? null;
9323
+ }
9324
+ const rawCmd = String(input.command ?? "").trimStart();
9325
+ if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
9326
+ const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
9327
+ const inputFileExt = inputFilePath ? path21.extname(inputFilePath).toLowerCase() : "";
9328
+ if (CODE_EXTENSIONS.has(inputFileExt)) continue;
9329
+ const dlpMatch = scanArgs(input);
9330
+ if (dlpMatch) {
9331
+ if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9332
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9333
+ if (!dedup.dlpKeys.has(k)) {
9334
+ dedup.dlpKeys.add(k);
9335
+ result.dlpFindings.push({
9336
+ patternName: dlpMatch.patternName,
9337
+ redactedSample: dlpMatch.redactedSample,
9338
+ toolName,
9339
+ timestamp: entry.timestamp ?? "",
9340
+ project: projLabel,
9341
+ sessionId,
9342
+ agent: "claude"
9343
+ });
9344
+ }
9345
+ }
9346
+ let astFsMatched = false;
9347
+ const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
9348
+ if (astRanForBash) {
9349
+ astFsMatched = pushFsOpAstFinding(
9350
+ String(input.command ?? ""),
9351
+ toolName,
9352
+ input,
9353
+ entry.timestamp ?? "",
9354
+ projLabel,
9355
+ sessionId,
9356
+ "claude",
9357
+ result,
9358
+ dedup
9359
+ );
9360
+ }
9361
+ let ruleMatched = astFsMatched;
9362
+ for (const source of ruleSources) {
9363
+ const { rule } = source;
9364
+ if (rule.verdict === "allow") continue;
9365
+ if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
9366
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9367
+ if (!evaluateSmartConditions(input, rule)) continue;
9368
+ const inputPreview = preview(input, 120);
9369
+ const k = findingKey(rule.name, inputPreview, projLabel);
9370
+ if (!dedup.findingsKeys.has(k)) {
9371
+ dedup.findingsKeys.add(k);
9372
+ result.findings.push({
9373
+ source,
9374
+ toolName,
9375
+ input,
9376
+ timestamp: entry.timestamp ?? "",
9377
+ project: projLabel,
9378
+ sessionId,
9379
+ agent: "claude"
9380
+ });
9381
+ }
9382
+ ruleMatched = true;
9383
+ break;
9384
+ }
9385
+ if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
9386
+ const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
9387
+ if (shellVerdict) {
9388
+ const astRule = {
9389
+ name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
9390
+ tool: "bash",
9391
+ conditions: [],
9392
+ verdict: shellVerdict,
9393
+ reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9394
+ };
9395
+ const inputPreview = preview(input, 120);
9396
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9397
+ if (!dedup.findingsKeys.has(k)) {
9398
+ dedup.findingsKeys.add(k);
9399
+ result.findings.push({
9400
+ source: {
9401
+ shieldName: "bash-safe",
9402
+ shieldLabel: "bash-safe (AST)",
9403
+ sourceType: "shield",
9404
+ rule: astRule
9405
+ },
9406
+ toolName,
9407
+ input,
9408
+ timestamp: entry.timestamp ?? "",
9409
+ project: projLabel,
9410
+ sessionId,
9411
+ agent: "claude"
9412
+ });
9413
+ }
9414
+ }
9415
+ }
9416
+ }
9417
+ }
9418
+ result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "claude"));
9419
+ if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
9420
+ result.sessionsWithEarlySecrets++;
9421
+ }
9422
+ }
9423
+ function processClaudeProject(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
9424
+ const projPath = path21.join(projectsDir, proj);
9425
+ try {
9426
+ if (!fs19.statSync(projPath).isDirectory()) return;
9427
+ } catch {
9428
+ return;
9429
+ }
9430
+ const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os18.homedir(), "~")).slice(
9431
+ 0,
9432
+ 40
9433
+ );
9434
+ let files;
9435
+ try {
9436
+ files = fs19.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
9437
+ } catch {
9438
+ return;
9439
+ }
9440
+ for (const file of files) {
9441
+ processClaudeFile(
9442
+ file,
9443
+ projPath,
9444
+ projLabel,
9445
+ ruleSources,
9446
+ startDate,
9447
+ result,
9448
+ dedup,
9449
+ onProgress,
9450
+ onLine
9451
+ );
9452
+ }
9453
+ }
9454
+ function emptyClaudeScan() {
9455
+ return {
9456
+ filesScanned: 0,
9457
+ sessions: 0,
9458
+ totalToolCalls: 0,
9459
+ bashCalls: 0,
9460
+ findings: [],
9461
+ dlpFindings: [],
9462
+ loopFindings: [],
9463
+ totalCostUSD: 0,
9464
+ firstDate: null,
9465
+ lastDate: null,
9466
+ sessionsWithEarlySecrets: 0
9467
+ };
9468
+ }
9469
+ function scanClaudeHistory(startDate, onProgress, onLine) {
9470
+ const projectsDir = path21.join(os18.homedir(), ".claude", "projects");
9471
+ const result = emptyClaudeScan();
9472
+ if (!fs19.existsSync(projectsDir)) return result;
9473
+ let projDirs;
9474
+ try {
9475
+ projDirs = fs19.readdirSync(projectsDir);
9476
+ } catch {
9477
+ return result;
9478
+ }
9479
+ const ruleSources = buildRuleSources();
9480
+ const dedup = emptyScanDedup();
9481
+ for (const proj of projDirs) {
9482
+ processClaudeProject(
9483
+ proj,
9484
+ projectsDir,
9485
+ ruleSources,
9486
+ startDate,
9487
+ result,
9488
+ dedup,
9489
+ onProgress,
9490
+ onLine
9491
+ );
9492
+ }
9245
9493
  return result;
9246
9494
  }
9247
9495
  function scanGeminiHistory(startDate, onProgress, onLine) {
@@ -9259,6 +9507,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9259
9507
  lastDate: null,
9260
9508
  sessionsWithEarlySecrets: 0
9261
9509
  };
9510
+ const dedup = emptyScanDedup();
9262
9511
  if (!fs19.existsSync(tmpDir)) return result;
9263
9512
  let slugDirs;
9264
9513
  try {
@@ -9315,10 +9564,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9315
9564
  if (text) {
9316
9565
  const dlpMatch = scanArgs({ text });
9317
9566
  if (dlpMatch) {
9318
- const isDupe = result.dlpFindings.some(
9319
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9320
- );
9321
- if (!isDupe) {
9567
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9568
+ if (!dedup.dlpKeys.has(k)) {
9569
+ dedup.dlpKeys.add(k);
9322
9570
  result.dlpFindings.push({
9323
9571
  patternName: dlpMatch.patternName,
9324
9572
  redactedSample: dlpMatch.redactedSample,
@@ -9363,10 +9611,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9363
9611
  continue;
9364
9612
  const dlpMatch = scanArgs(input);
9365
9613
  if (dlpMatch) {
9366
- const isDupe = result.dlpFindings.some(
9367
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9368
- );
9369
- if (!isDupe) {
9614
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9615
+ if (!dedup.dlpKeys.has(k)) {
9616
+ dedup.dlpKeys.add(k);
9370
9617
  result.dlpFindings.push({
9371
9618
  patternName: dlpMatch.patternName,
9372
9619
  redactedSample: dlpMatch.redactedSample,
@@ -9389,7 +9636,8 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9389
9636
  projLabel,
9390
9637
  sessionId,
9391
9638
  "gemini",
9392
- result
9639
+ result,
9640
+ dedup
9393
9641
  );
9394
9642
  }
9395
9643
  let ruleMatched = astFsMatched;
@@ -9400,10 +9648,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9400
9648
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9401
9649
  if (!evaluateSmartConditions(input, rule)) continue;
9402
9650
  const inputPreview = preview(input, 120);
9403
- const isDupe = result.findings.some(
9404
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9405
- );
9406
- if (!isDupe) {
9651
+ const k = findingKey(rule.name, inputPreview, projLabel);
9652
+ if (!dedup.findingsKeys.has(k)) {
9653
+ dedup.findingsKeys.add(k);
9407
9654
  result.findings.push({
9408
9655
  source,
9409
9656
  toolName,
@@ -9431,10 +9678,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9431
9678
  reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9432
9679
  };
9433
9680
  const inputPreview = preview(input, 120);
9434
- const isDupe = result.findings.some(
9435
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9436
- );
9437
- if (!isDupe) {
9681
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9682
+ if (!dedup.findingsKeys.has(k)) {
9683
+ dedup.findingsKeys.add(k);
9438
9684
  result.findings.push({
9439
9685
  source: {
9440
9686
  shieldName: "bash-safe",
@@ -9474,6 +9720,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9474
9720
  lastDate: null,
9475
9721
  sessionsWithEarlySecrets: 0
9476
9722
  };
9723
+ const dedup = emptyScanDedup();
9477
9724
  if (!fs19.existsSync(sessionsBase)) return result;
9478
9725
  const jsonlFiles = [];
9479
9726
  try {
@@ -9555,10 +9802,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9555
9802
  if (text) {
9556
9803
  const dlpMatch2 = scanArgs({ text });
9557
9804
  if (dlpMatch2) {
9558
- const isDupe = result.dlpFindings.some(
9559
- (f) => f.patternName === dlpMatch2.patternName && f.redactedSample === dlpMatch2.redactedSample && f.project === projLabel
9560
- );
9561
- if (!isDupe) {
9805
+ const k = dlpKey(dlpMatch2.patternName, dlpMatch2.redactedSample, projLabel);
9806
+ if (!dedup.dlpKeys.has(k)) {
9807
+ dedup.dlpKeys.add(k);
9562
9808
  result.dlpFindings.push({
9563
9809
  patternName: dlpMatch2.patternName,
9564
9810
  redactedSample: dlpMatch2.redactedSample,
@@ -9600,10 +9846,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9600
9846
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
9601
9847
  const dlpMatch = scanArgs(input);
9602
9848
  if (dlpMatch) {
9603
- const isDupe = result.dlpFindings.some(
9604
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9605
- );
9606
- if (!isDupe) {
9849
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9850
+ if (!dedup.dlpKeys.has(k)) {
9851
+ dedup.dlpKeys.add(k);
9607
9852
  result.dlpFindings.push({
9608
9853
  patternName: dlpMatch.patternName,
9609
9854
  redactedSample: dlpMatch.redactedSample,
@@ -9626,7 +9871,8 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9626
9871
  projLabel,
9627
9872
  sessionId,
9628
9873
  "codex",
9629
- result
9874
+ result,
9875
+ dedup
9630
9876
  );
9631
9877
  }
9632
9878
  let ruleMatched = astFsMatched;
@@ -9638,10 +9884,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9638
9884
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9639
9885
  if (!evaluateSmartConditions(input, rule)) continue;
9640
9886
  const inputPreview = preview(input, 120);
9641
- const isDupe = result.findings.some(
9642
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9643
- );
9644
- if (!isDupe) {
9887
+ const k = findingKey(rule.name, inputPreview, projLabel);
9888
+ if (!dedup.findingsKeys.has(k)) {
9889
+ dedup.findingsKeys.add(k);
9645
9890
  result.findings.push({
9646
9891
  source,
9647
9892
  toolName,
@@ -9666,10 +9911,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9666
9911
  reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9667
9912
  };
9668
9913
  const inputPreview = preview(input, 120);
9669
- const isDupe = result.findings.some(
9670
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9671
- );
9672
- if (!isDupe) {
9914
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9915
+ if (!dedup.findingsKeys.has(k)) {
9916
+ dedup.findingsKeys.add(k);
9673
9917
  result.findings.push({
9674
9918
  source: {
9675
9919
  shieldName: "bash-safe",
@@ -9700,6 +9944,7 @@ function scanShellConfig() {
9700
9944
  (f) => path21.join(home, f)
9701
9945
  );
9702
9946
  const findings = [];
9947
+ const seen = /* @__PURE__ */ new Set();
9703
9948
  for (const filePath of configFiles) {
9704
9949
  if (!fs19.existsSync(filePath)) continue;
9705
9950
  let lines;
@@ -9714,10 +9959,9 @@ function scanShellConfig() {
9714
9959
  if (!trimmed || trimmed.startsWith("#")) continue;
9715
9960
  const dlpMatch = scanArgs({ text: trimmed });
9716
9961
  if (!dlpMatch) continue;
9717
- const isDupe = findings.some(
9718
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === shortPath
9719
- );
9720
- if (!isDupe) {
9962
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, shortPath);
9963
+ if (!seen.has(k)) {
9964
+ seen.add(k);
9721
9965
  findings.push({
9722
9966
  patternName: dlpMatch.patternName,
9723
9967
  redactedSample: dlpMatch.redactedSample,
@@ -9976,6 +10220,263 @@ function renderNarrativeScorecard(input) {
9976
10220
  console.log(chalk5.dim("\u2192 github.com/node9-ai/node9-proxy"));
9977
10221
  console.log("");
9978
10222
  }
10223
+ function mkLine(...parts) {
10224
+ let rendered = "";
10225
+ let width = 0;
10226
+ for (const [text, fmt] of parts) {
10227
+ rendered += fmt ? fmt(text) : text;
10228
+ width += text.length;
10229
+ }
10230
+ return { rendered, width };
10231
+ }
10232
+ function shortRule(name, width) {
10233
+ const stripped = name.replace(/^shield:[^:]+:/, "");
10234
+ if (stripped.length <= width) return stripped.padEnd(width);
10235
+ return stripped.slice(0, width - 1) + "\u2026";
10236
+ }
10237
+ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
10238
+ const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
10239
+ const topLines = [];
10240
+ if (scan.dlpFindings.length > 0) {
10241
+ const latest = scan.dlpFindings[0];
10242
+ const rel = relativeDate(latest.timestamp, now);
10243
+ const noun = `credential leak${scan.dlpFindings.length !== 1 ? "s" : ""}`;
10244
+ topLines.push(
10245
+ mkLine(
10246
+ ["\u{1F6A8} ", chalk5.red],
10247
+ [`${scan.dlpFindings.length} ${noun} in tool input `, chalk5.bold],
10248
+ [`(latest: ${rel} ago, ${latest.patternName})`, chalk5.dim]
10249
+ )
10250
+ );
10251
+ }
10252
+ if (blockedCount > 0) {
10253
+ const topBlocked = topRulesByVerdict(summary.sections, "block", 2).map(
10254
+ (r) => r.count > 1 ? `${shortRule(r.name, 20).trimEnd()} \xD7${r.count}` : shortRule(r.name, 20).trimEnd()
10255
+ ).join(", ");
10256
+ topLines.push(
10257
+ mkLine(
10258
+ ["\u{1F6D1} ", chalk5.red],
10259
+ [`${blockedCount} ops node9 would have blocked `, chalk5.bold],
10260
+ [`(${topBlocked})`, chalk5.dim]
10261
+ )
10262
+ );
10263
+ }
10264
+ if (scan.loopFindings.length > 0) {
10265
+ const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
10266
+ const byTool = /* @__PURE__ */ new Map();
10267
+ for (const f of scan.loopFindings) {
10268
+ byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + Math.max(0, f.count - 1));
10269
+ }
10270
+ const top = [...byTool.entries()].sort((a, b) => b[1] - a[1])[0];
10271
+ const wasteSuffix = wastePct > 0 ? `, ${wastePct}% wasted` : "";
10272
+ const detail = top ? `(${top[0]} dominates${wasteSuffix})` : "";
10273
+ topLines.push(
10274
+ mkLine(
10275
+ ["\u{1F501} ", chalk5.yellow],
10276
+ [`${scan.loopFindings.length} agent loops detected `, chalk5.bold],
10277
+ [detail, chalk5.dim]
10278
+ )
10279
+ );
10280
+ }
10281
+ if (blastExposures > 0) {
10282
+ const exposed2 = Math.max(0, 100 - blast.score);
10283
+ const pjDiscount = PROTECTIVE_SHIELD_DISCOUNTS["project-jail"] ?? 0;
10284
+ const pjBonus = Math.round(exposed2 * pjDiscount);
10285
+ const cta = pjBonus > 0 ? ` \u2192 enable project-jail (+${pjBonus} pts)` : "";
10286
+ topLines.push(
10287
+ mkLine(
10288
+ ["\u{1F52D} ", chalk5.red],
10289
+ [`${blastExposures} secrets reachable on disk`, chalk5.bold],
10290
+ [cta, chalk5.dim]
10291
+ )
10292
+ );
10293
+ }
10294
+ if (topLines.length > 0) {
10295
+ for (const ln of boxPanel("TOP FINDINGS", topLines)) console.log(" " + ln);
10296
+ console.log("");
10297
+ }
10298
+ if (summary.leaks.length > 0) {
10299
+ const leakLines = [];
10300
+ for (const leak of summary.leaks.slice(0, 5)) {
10301
+ const rel = relativeDate(leak.timestamp, now);
10302
+ leakLines.push(
10303
+ mkLine(
10304
+ [rel.padStart(4) + " ", chalk5.dim],
10305
+ [leak.patternName.padEnd(14), chalk5.red.bold],
10306
+ [" "],
10307
+ [leak.redactedSample.padEnd(20), chalk5.red],
10308
+ [" "],
10309
+ [`[${leak.toolName}]`.padEnd(15), chalk5.dim],
10310
+ [" "],
10311
+ [leak.agent, chalk5.dim]
10312
+ )
10313
+ );
10314
+ }
10315
+ const remaining = summary.leaks.length - 5;
10316
+ if (remaining > 0) {
10317
+ leakLines.push(mkLine([`\u2026 +${remaining} more`, chalk5.dim]));
10318
+ }
10319
+ const title = `LEAKS \xB7 ${summary.leaks.length} secret${summary.leaks.length !== 1 ? "s" : ""} in plain text`;
10320
+ for (const ln of boxPanel(title, leakLines)) console.log(" " + ln);
10321
+ console.log("");
10322
+ }
10323
+ if (blockedCount > 0) {
10324
+ const blockedLines = [];
10325
+ const ruleEntries = topRulesByVerdict(summary.sections, "block", 12);
10326
+ for (const r of ruleEntries) {
10327
+ const origin = originForRule(r.name, summary.sections);
10328
+ blockedLines.push(
10329
+ mkLine(
10330
+ ["\u2717 ", chalk5.red],
10331
+ [shortRule(r.name, 24), chalk5.bold],
10332
+ [" \xD7" + String(r.count).padEnd(4), chalk5.bold],
10333
+ [" "],
10334
+ [origin, chalk5.dim]
10335
+ )
10336
+ );
10337
+ }
10338
+ const title = `BLOCKED \xB7 ${blockedCount} ops node9 would have stopped`;
10339
+ for (const ln of boxPanel(title, blockedLines)) console.log(" " + ln);
10340
+ console.log("");
10341
+ }
10342
+ if (reviewCount > 0) {
10343
+ const reviewLines = [];
10344
+ const ruleEntries = topRulesByVerdict(summary.sections, "review", 12);
10345
+ for (const r of ruleEntries) {
10346
+ const origin = originForRule(r.name, summary.sections);
10347
+ reviewLines.push(
10348
+ mkLine(
10349
+ ["\u{1F441} ", chalk5.yellow],
10350
+ [shortRule(r.name, 24), chalk5.bold],
10351
+ [" \xD7" + String(r.count).padEnd(4), chalk5.bold],
10352
+ [" "],
10353
+ [origin, chalk5.dim]
10354
+ )
10355
+ );
10356
+ }
10357
+ const title = `REVIEW QUEUE \xB7 ${reviewCount} ops flagged for approval`;
10358
+ for (const ln of boxPanel(title, reviewLines)) console.log(" " + ln);
10359
+ console.log("");
10360
+ }
10361
+ if (scan.loopFindings.length > 0) {
10362
+ const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
10363
+ const byTool = /* @__PURE__ */ new Map();
10364
+ let totalRepeats = 0;
10365
+ for (const f of scan.loopFindings) {
10366
+ const repeats = Math.max(0, f.count - 1);
10367
+ byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
10368
+ totalRepeats += repeats;
10369
+ }
10370
+ const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
10371
+ const loopLines = [];
10372
+ for (const [tool, repeats] of toolEntries) {
10373
+ const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
10374
+ loopLines.push(
10375
+ mkLine(
10376
+ [tool.padEnd(10), chalk5.bold],
10377
+ [`\xD7${num(repeats)} repeats`.padEnd(16)],
10378
+ [`(${pct}%)`, chalk5.dim]
10379
+ )
10380
+ );
10381
+ }
10382
+ const topStuck = [...scan.loopFindings].sort((a, b) => b.count - a.count).slice(0, 3);
10383
+ if (topStuck.length > 0) {
10384
+ loopLines.push(mkLine([""]));
10385
+ loopLines.push(mkLine(["Top stuck patterns:", chalk5.dim]));
10386
+ for (const f of topStuck) {
10387
+ const raw = f.commandPreview || f.toolName;
10388
+ const target = raw.length > 60 ? "\u2026" + raw.slice(raw.length - 59) : raw.padEnd(60);
10389
+ loopLines.push(mkLine([`\xD7${num(f.count).padEnd(4)} `, chalk5.bold], [target, chalk5.dim]));
10390
+ }
10391
+ }
10392
+ const wasteSuffix = wastePct > 0 ? ` \xB7 ${wastePct}% wasted` : "";
10393
+ const title = `AGENT LOOPS \xB7 ${scan.loopFindings.length} repeated patterns${wasteSuffix}`;
10394
+ for (const ln of boxPanel(title, loopLines)) console.log(" " + ln);
10395
+ console.log("");
10396
+ }
10397
+ if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
10398
+ const blastLines = [];
10399
+ const DESC_W = 33;
10400
+ for (const r of blast.reachable.slice(0, 8)) {
10401
+ const trimmed = r.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
10402
+ const desc = trimmed.length > DESC_W ? trimmed.slice(0, DESC_W - 1) + "\u2026" : trimmed;
10403
+ blastLines.push(mkLine(["\u2717 ", chalk5.red], [r.label.padEnd(36)], [desc, chalk5.dim]));
10404
+ }
10405
+ for (const e of blast.envFindings.slice(0, 3)) {
10406
+ blastLines.push(
10407
+ mkLine(["\u26A0 ", chalk5.yellow], [`${e.key} `], [`(${e.patternName})`, chalk5.dim])
10408
+ );
10409
+ }
10410
+ const totalExposed = blast.reachable.length + blast.envFindings.length;
10411
+ if (totalExposed > 8) {
10412
+ blastLines.push(mkLine([`\u2026 +${totalExposed - 8} more`, chalk5.dim]));
10413
+ }
10414
+ const title = `BLAST RADIUS \xB7 ${totalExposed} path${totalExposed !== 1 ? "s" : ""} reachable right now`;
10415
+ for (const ln of boxPanel(title, blastLines)) console.log(" " + ln);
10416
+ console.log("");
10417
+ }
10418
+ const shieldImpacts = rollupByShield(summary.sections);
10419
+ const exposed = Math.max(0, 100 - blast.score);
10420
+ const shieldLines = [];
10421
+ const ranked = [...shieldImpacts].sort((a, b) => {
10422
+ const aDiscount = PROTECTIVE_SHIELD_DISCOUNTS[a.shieldName] ?? 0;
10423
+ const bDiscount = PROTECTIVE_SHIELD_DISCOUNTS[b.shieldName] ?? 0;
10424
+ if (aDiscount !== bDiscount) return bDiscount - aDiscount;
10425
+ return b.totalCatches - a.totalCatches;
10426
+ });
10427
+ for (const impact of ranked) {
10428
+ if (impact.totalCatches === 0) continue;
10429
+ const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
10430
+ const bonus = Math.round(exposed * discount);
10431
+ const icon = discount > 0 ? "\u{1F6E1} " : "\u2610 ";
10432
+ const wouldCatch = `would catch ${impact.totalCatches} op${impact.totalCatches !== 1 ? "s" : ""}`;
10433
+ const deltaSuffix = bonus > 0 ? ` \u2192 +${bonus} pts (${blast.score} \u2192 ${blast.score + bonus})` : "";
10434
+ shieldLines.push(
10435
+ mkLine(
10436
+ [icon, discount > 0 ? chalk5.cyan : chalk5.dim],
10437
+ [impact.shieldName.padEnd(14), chalk5.bold],
10438
+ [wouldCatch.padEnd(22), chalk5.dim],
10439
+ [deltaSuffix, bonus > 0 ? chalk5.green.bold : chalk5.dim]
10440
+ )
10441
+ );
10442
+ if (impact.topRuleLabels.length > 0) {
10443
+ const rules = impact.topRuleLabels.join(", ");
10444
+ shieldLines.push(mkLine([" ", chalk5.dim], [rules, chalk5.dim]));
10445
+ }
10446
+ }
10447
+ const hitShieldSet = new Set(
10448
+ shieldImpacts.filter((i) => i.totalCatches > 0).map((i) => i.shieldName)
10449
+ );
10450
+ const zeroHitBuiltins = Object.keys(SHIELDS).filter((name) => !hitShieldSet.has(name)).sort();
10451
+ if (zeroHitBuiltins.length > 0) {
10452
+ shieldLines.push(mkLine([""]));
10453
+ shieldLines.push(mkLine([zeroHitBuiltins.join(" \xB7 "), chalk5.dim]));
10454
+ shieldLines.push(mkLine([" no hits in your history \u2014 install proactively", chalk5.dim]));
10455
+ }
10456
+ const topRec = ranked.find(
10457
+ (r) => r.totalCatches > 0 && (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0
10458
+ );
10459
+ if (topRec) {
10460
+ const bonus = Math.round(exposed * (PROTECTIVE_SHIELD_DISCOUNTS[topRec.shieldName] ?? 0));
10461
+ const cta = `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 +${bonus} pts)`;
10462
+ shieldLines.push(mkLine([""]));
10463
+ shieldLines.push(mkLine([cta, chalk5.cyan]));
10464
+ }
10465
+ if (shieldLines.length > 0) {
10466
+ const title = "SHIELDS \xB7 install node9 + enable these to catch what we found";
10467
+ for (const ln of boxPanel(title, shieldLines)) console.log(" " + ln);
10468
+ console.log("");
10469
+ }
10470
+ }
10471
+ function originForRule(ruleName, sections) {
10472
+ for (const section of sections) {
10473
+ if (section.rules.some((r) => r.name === ruleName)) {
10474
+ if (section.sourceType === "default") return "default";
10475
+ if (section.sourceType === "shield") return `needs shield:${section.shieldKey ?? section.id}`;
10476
+ }
10477
+ }
10478
+ return "";
10479
+ }
9979
10480
  function registerScanCommand(program2) {
9980
10481
  program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option(
9981
10482
  "--json",
@@ -10207,7 +10708,7 @@ function registerScanCommand(program2) {
10207
10708
  " " + chalk5.dim("AI spend ") + chalk5.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? chalk5.dim(" \xB7 wasted on loops ") + chalk5.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
10208
10709
  );
10209
10710
  }
10210
- if (scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
10711
+ if (drillDown && scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
10211
10712
  console.log(
10212
10713
  " " + chalk5.dim(
10213
10714
  `${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
@@ -10215,6 +10716,26 @@ function registerScanCommand(program2) {
10215
10716
  );
10216
10717
  }
10217
10718
  console.log("");
10719
+ if (!drillDown) {
10720
+ renderPanelScorecard({
10721
+ scan,
10722
+ summary,
10723
+ blast,
10724
+ blastExposures,
10725
+ blockedCount,
10726
+ reviewCount
10727
+ });
10728
+ const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
10729
+ console.log(" " + chalk5.green(cta));
10730
+ console.log(
10731
+ " " + chalk5.dim("\u2192 ") + chalk5.cyan("node9 monitor") + chalk5.dim(" live dashboard")
10732
+ );
10733
+ console.log(
10734
+ " " + chalk5.dim("\u2192 ") + chalk5.cyan("node9 scan --drill-down") + chalk5.dim(" full commands + session IDs")
10735
+ );
10736
+ console.log("");
10737
+ return;
10738
+ }
10218
10739
  if (scan.dlpFindings.length > 0) {
10219
10740
  console.log(" " + chalk5.dim("\u2500".repeat(70)));
10220
10741
  console.log(
@@ -10403,7 +10924,7 @@ function registerScanCommand(program2) {
10403
10924
  }
10404
10925
  );
10405
10926
  }
10406
- var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, DEFAULT_RULE_NAMES, classifyRuleSeverity2, narrativeRuleLabel2;
10927
+ var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
10407
10928
  var init_scan = __esm({
10408
10929
  "src/cli/commands/scan.ts"() {
10409
10930
  "use strict";
@@ -10417,6 +10938,7 @@ var init_scan = __esm({
10417
10938
  init_setup();
10418
10939
  init_blast();
10419
10940
  init_scan_derive();
10941
+ init_protection();
10420
10942
  init_scan_json();
10421
10943
  init_scan_history();
10422
10944
  CLAUDE_PRICING = {
@@ -10499,9 +11021,6 @@ var init_scan = __esm({
10499
11021
  STUCK_TOOLS_LIMIT = 3;
10500
11022
  RECURRING_SESSION_THRESHOLD = 3;
10501
11023
  STALE_AGE_DAYS = 30;
10502
- DEFAULT_RULE_NAMES = new Set(
10503
- DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
10504
- );
10505
11024
  classifyRuleSeverity2 = classifyRuleSeverity;
10506
11025
  narrativeRuleLabel2 = narrativeRuleLabel;
10507
11026
  }
@@ -11006,6 +11525,19 @@ data: ${JSON.stringify(data)}
11006
11525
  }
11007
11526
  });
11008
11527
  }
11528
+ function broadcastForensic(finding) {
11529
+ const severity = CRITICAL_FORENSIC_CATEGORIES.has(finding.type) ? "critical" : "warning";
11530
+ const event = {
11531
+ type: "forensic",
11532
+ id: `fnd_${randomUUID3()}`,
11533
+ ts: Date.now(),
11534
+ sessionId: finding.sessionId,
11535
+ category: finding.type,
11536
+ severity
11537
+ };
11538
+ if (finding.patternName !== void 0) event.patternName = finding.patternName;
11539
+ broadcast("forensic", event);
11540
+ }
11009
11541
  function abandonPending() {
11010
11542
  setAbandonTimer(null);
11011
11543
  pending.forEach((entry, id) => {
@@ -11189,7 +11721,7 @@ function bindActivitySocket() {
11189
11721
  });
11190
11722
  activitySocketServer = unixServer;
11191
11723
  }
11192
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
11724
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, CRITICAL_FORENSIC_CATEGORIES, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
11193
11725
  var init_state2 = __esm({
11194
11726
  "src/daemon/state.ts"() {
11195
11727
  "use strict";
@@ -11234,6 +11766,11 @@ var init_state2 = __esm({
11234
11766
  INPUT_PRICE_PER_1M = 3;
11235
11767
  OUTPUT_PRICE_PER_1M = 15;
11236
11768
  BYTES_PER_TOKEN = 4;
11769
+ CRITICAL_FORENSIC_CATEGORIES = /* @__PURE__ */ new Set([
11770
+ "privilege-escalation",
11771
+ "destructive-op",
11772
+ "eval-of-remote"
11773
+ ]);
11237
11774
  WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
11238
11775
  "write",
11239
11776
  "write_file",
@@ -11578,7 +12115,26 @@ function startCloudSync() {
11578
12115
  const recurring = setInterval(() => void syncOnce(), intervalMs);
11579
12116
  recurring.unref();
11580
12117
  }
11581
- var FINDING_TO_SIGNAL3, rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS;
12118
+ function startForensicBroadcast() {
12119
+ const tick = async () => {
12120
+ try {
12121
+ const findings = await tickForensicBroadcast(forensicBroadcastOffsets);
12122
+ for (const f of findings) broadcastForensic(f);
12123
+ } catch (err2) {
12124
+ const msg = err2 instanceof Error ? err2.message : String(err2);
12125
+ appendToLog(HOOK_DEBUG_LOG, {
12126
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12127
+ kind: "forensic-broadcast-error",
12128
+ error: msg
12129
+ });
12130
+ }
12131
+ };
12132
+ const initial = setTimeout(() => void tick(), FORENSIC_INITIAL_DELAY_MS);
12133
+ initial.unref();
12134
+ const recurring = setInterval(() => void tick(), FORENSIC_BROADCAST_INTERVAL_MS);
12135
+ recurring.unref();
12136
+ }
12137
+ var FINDING_TO_SIGNAL3, rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
11582
12138
  var init_sync = __esm({
11583
12139
  "src/daemon/sync.ts"() {
11584
12140
  "use strict";
@@ -11586,6 +12142,8 @@ var init_sync = __esm({
11586
12142
  init_blast();
11587
12143
  init_dist();
11588
12144
  init_scan_watermark();
12145
+ init_state2();
12146
+ init_audit();
11589
12147
  FINDING_TO_SIGNAL3 = {
11590
12148
  dlp: "dlpFindings",
11591
12149
  pii: "piiFindings",
@@ -11602,6 +12160,9 @@ var init_sync = __esm({
11602
12160
  DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept/policies/sync";
11603
12161
  DEFAULT_INTERVAL_HOURS = 5;
11604
12162
  MIN_INTERVAL_HOURS = 1;
12163
+ FORENSIC_BROADCAST_INTERVAL_MS = 3e4;
12164
+ FORENSIC_INITIAL_DELAY_MS = 5e3;
12165
+ forensicBroadcastOffsets = /* @__PURE__ */ new Map();
11605
12166
  }
11606
12167
  });
11607
12168
 
@@ -11835,6 +12396,7 @@ import chalk6 from "chalk";
11835
12396
  function startDaemon() {
11836
12397
  startCostSync();
11837
12398
  startCloudSync();
12399
+ startForensicBroadcast();
11838
12400
  startDlpScanner();
11839
12401
  loadInsightCounts();
11840
12402
  const internalToken = randomUUID4();
@@ -13029,6 +13591,7 @@ var tail_exports = {};
13029
13591
  __export(tail_exports, {
13030
13592
  agentLabel: () => agentLabel,
13031
13593
  sessionTag: () => sessionTag,
13594
+ shortenPathSummary: () => shortenPathSummary,
13032
13595
  startTail: () => startTail
13033
13596
  });
13034
13597
  import http2 from "http";
@@ -13038,6 +13601,12 @@ import os41 from "os";
13038
13601
  import path47 from "path";
13039
13602
  import readline6 from "readline";
13040
13603
  import { spawn as spawn9 } from "child_process";
13604
+ function shortenPathSummary(s) {
13605
+ if (!s || !s.startsWith("/")) return s;
13606
+ const parts = s.split("/").filter(Boolean);
13607
+ if (parts.length <= 2) return s;
13608
+ return `\u2026/${parts.slice(-2).join("/")}`;
13609
+ }
13041
13610
  function getIcon(tool) {
13042
13611
  const t = tool.toLowerCase();
13043
13612
  for (const [k, v] of Object.entries(ICONS)) {
@@ -13791,7 +14360,8 @@ async function startTail(options = {}) {
13791
14360
  if (event === "snapshot") {
13792
14361
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
13793
14362
  const hash = data.hash ?? "";
13794
- const summary = data.argsSummary ?? data.tool;
14363
+ const rawSummary = data.argsSummary ?? data.tool;
14364
+ const summary = shortenPathSummary(rawSummary);
13795
14365
  const fileCount = data.fileCount ?? 0;
13796
14366
  const files = fileCount > 0 ? chalk30.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
13797
14367
  process.stdout.write(
@@ -16173,63 +16743,13 @@ function registerAuditCommand(program2) {
16173
16743
 
16174
16744
  // src/cli/commands/report.ts
16175
16745
  import chalk13 from "chalk";
16746
+
16747
+ // src/cli/aggregate/report-audit.ts
16748
+ init_costSync();
16749
+ init_litellm();
16176
16750
  import fs35 from "fs";
16177
- import path36 from "path";
16178
16751
  import os31 from "os";
16179
-
16180
- // src/cli/render/report-json.ts
16181
- function buildReportJson(input) {
16182
- const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
16183
- const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
16184
- const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
16185
- return {
16186
- schemaVersion: 1,
16187
- generatedAt: input.generatedAt,
16188
- period: input.period,
16189
- range: { start: input.start.toISOString(), end: input.end.toISOString() },
16190
- excludedTests: input.excludedTests,
16191
- totals: {
16192
- events: input.total,
16193
- blocked: totalBlocked,
16194
- blockRate,
16195
- userApproved: input.userApproved,
16196
- userDenied: input.userDenied,
16197
- timedOut: input.timedOut,
16198
- hardBlocked: input.hardBlocked,
16199
- dlpBlocked: input.dlpBlocked,
16200
- observeDlp: input.observeDlp,
16201
- loopHits: input.loopHits,
16202
- unackedDlp: input.unackedDlp
16203
- },
16204
- tests: {
16205
- passes: input.testPasses,
16206
- fails: input.testFails
16207
- },
16208
- cost: {
16209
- totalUSD: input.cost.claudeUSD + input.cost.codexUSD,
16210
- claudeUSD: input.cost.claudeUSD,
16211
- codexUSD: input.cost.codexUSD,
16212
- inputTokens: input.cost.inputTokens,
16213
- outputTokens: input.cost.outputTokens,
16214
- cacheWriteTokens: input.cost.cacheWriteTokens,
16215
- cacheReadTokens: input.cost.cacheReadTokens,
16216
- byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
16217
- byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
16218
- },
16219
- byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
16220
- byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
16221
- byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
16222
- byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
16223
- byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
16224
- byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
16225
- trend: {
16226
- priorBlockRate: input.priorBlockRate,
16227
- deltaPct
16228
- }
16229
- };
16230
- }
16231
-
16232
- // src/cli/commands/report.ts
16752
+ import path36 from "path";
16233
16753
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
16234
16754
  function buildTestTimestamps(allEntries) {
16235
16755
  const testTs = /* @__PURE__ */ new Set();
@@ -16254,8 +16774,7 @@ function isTestEntry(entry, testTs) {
16254
16774
  }
16255
16775
  return false;
16256
16776
  }
16257
- function getDateRange(period) {
16258
- const now = /* @__PURE__ */ new Date();
16777
+ function getDateRange(period, now) {
16259
16778
  const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
16260
16779
  const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
16261
16780
  switch (period) {
@@ -16271,6 +16790,11 @@ function getDateRange(period) {
16271
16790
  s.setDate(s.getDate() - 29);
16272
16791
  return { start: s, end };
16273
16792
  }
16793
+ case "90d": {
16794
+ const s = new Date(todayStart);
16795
+ s.setDate(s.getDate() - 89);
16796
+ return { start: s, end };
16797
+ }
16274
16798
  case "month":
16275
16799
  return { start: new Date(now.getFullYear(), now.getMonth(), 1), end };
16276
16800
  }
@@ -16293,40 +16817,6 @@ function isAllow(decision) {
16293
16817
  function isDlp(checkedBy) {
16294
16818
  return !!checkedBy?.includes("dlp");
16295
16819
  }
16296
- var BLOCK_REASON_LABELS = {
16297
- timeout: "Popup timeout",
16298
- "smart-rule-block": "Smart rule",
16299
- "observe-mode-dlp-would-block": "DLP (observe)",
16300
- "persistent-deny": "Persistent deny",
16301
- "local-decision": "User denied",
16302
- "dlp-block": "DLP block",
16303
- "loop-detected": "Loop detected"
16304
- };
16305
- function humanBlockReason(reason) {
16306
- return BLOCK_REASON_LABELS[reason] ?? reason;
16307
- }
16308
- function barStr(value, max, width) {
16309
- if (max === 0 || width <= 0) return "\u2591".repeat(width);
16310
- const filled = Math.max(1, Math.round(value / max * width));
16311
- return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
16312
- }
16313
- function colorBar(value, max, width) {
16314
- const s = barStr(value, max, width);
16315
- const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
16316
- return chalk13.cyan(s.slice(0, filled)) + chalk13.dim(s.slice(filled));
16317
- }
16318
- function fmtDate(d) {
16319
- const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
16320
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
16321
- }
16322
- function num2(n) {
16323
- return n.toLocaleString();
16324
- }
16325
- function fmtCost2(usd) {
16326
- if (usd < 1e-3) return "< $0.001";
16327
- if (usd < 1) return "$" + usd.toFixed(4);
16328
- return "$" + usd.toFixed(2);
16329
- }
16330
16820
  var CLAUDE_PRICING2 = {
16331
16821
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
16332
16822
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -16346,90 +16836,160 @@ function claudeModelPrice2(model) {
16346
16836
  }
16347
16837
  return null;
16348
16838
  }
16349
- function loadClaudeCost(start, end) {
16350
- const empty = {
16839
+ function emptyClaudeCostAccumulator() {
16840
+ return {
16351
16841
  total: 0,
16352
- byDay: /* @__PURE__ */ new Map(),
16353
- byModel: /* @__PURE__ */ new Map(),
16354
16842
  inputTokens: 0,
16355
16843
  outputTokens: 0,
16356
16844
  cacheWriteTokens: 0,
16357
- cacheReadTokens: 0
16845
+ cacheReadTokens: 0,
16846
+ byDay: /* @__PURE__ */ new Map(),
16847
+ byModel: /* @__PURE__ */ new Map(),
16848
+ byProject: /* @__PURE__ */ new Map()
16849
+ };
16850
+ }
16851
+ function freezeClaudeCost(acc) {
16852
+ return {
16853
+ total: acc.total,
16854
+ byDay: acc.byDay,
16855
+ byModel: acc.byModel,
16856
+ byProject: acc.byProject,
16857
+ inputTokens: acc.inputTokens,
16858
+ outputTokens: acc.outputTokens,
16859
+ cacheWriteTokens: acc.cacheWriteTokens,
16860
+ cacheReadTokens: acc.cacheReadTokens
16358
16861
  };
16359
- const projectsDir = path36.join(os31.homedir(), ".claude", "projects");
16360
- if (!fs35.existsSync(projectsDir)) return empty;
16862
+ }
16863
+ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
16864
+ const projPath = path36.join(projectsDir, proj);
16865
+ let files;
16866
+ try {
16867
+ const stat = fs35.statSync(projPath);
16868
+ if (!stat.isDirectory()) return;
16869
+ files = fs35.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
16870
+ } catch {
16871
+ return;
16872
+ }
16873
+ const startMs = start.getTime();
16874
+ for (const file of files) {
16875
+ const filePath = path36.join(projPath, file);
16876
+ try {
16877
+ if (fs35.statSync(filePath).mtimeMs < startMs) continue;
16878
+ } catch {
16879
+ continue;
16880
+ }
16881
+ try {
16882
+ const raw = fs35.readFileSync(filePath, "utf-8");
16883
+ for (const line of raw.split("\n")) {
16884
+ if (!line.trim()) continue;
16885
+ let entry;
16886
+ try {
16887
+ entry = JSON.parse(line);
16888
+ } catch {
16889
+ continue;
16890
+ }
16891
+ if (entry.type !== "assistant") continue;
16892
+ if (!entry.timestamp) continue;
16893
+ const ts = new Date(entry.timestamp);
16894
+ if (ts < start || ts > end) continue;
16895
+ const usage = entry.message?.usage;
16896
+ const model = entry.message?.model;
16897
+ if (!usage || !model) continue;
16898
+ const p = claudeModelPrice2(model);
16899
+ if (!p) continue;
16900
+ const inp = usage.input_tokens ?? 0;
16901
+ const out = usage.output_tokens ?? 0;
16902
+ const cw = usage.cache_creation_input_tokens ?? 0;
16903
+ const cr = usage.cache_read_input_tokens ?? 0;
16904
+ const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
16905
+ acc.total += cost;
16906
+ acc.inputTokens += inp;
16907
+ acc.outputTokens += out;
16908
+ acc.cacheWriteTokens += cw;
16909
+ acc.cacheReadTokens += cr;
16910
+ const dateKey = entry.timestamp.slice(0, 10);
16911
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
16912
+ const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
16913
+ acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
16914
+ const projectKey = decodeProjectDirName(proj);
16915
+ const projectRollup = acc.byProject.get(projectKey) ?? {
16916
+ cost: 0,
16917
+ inputTokens: 0,
16918
+ outputTokens: 0
16919
+ };
16920
+ projectRollup.cost += cost;
16921
+ projectRollup.inputTokens += inp;
16922
+ projectRollup.outputTokens += out;
16923
+ acc.byProject.set(projectKey, projectRollup);
16924
+ }
16925
+ } catch {
16926
+ continue;
16927
+ }
16928
+ }
16929
+ }
16930
+ function loadClaudeCost(start, end, projectsDir) {
16931
+ const acc = emptyClaudeCostAccumulator();
16932
+ if (!fs35.existsSync(projectsDir)) return freezeClaudeCost(acc);
16361
16933
  let dirs;
16362
16934
  try {
16363
16935
  dirs = fs35.readdirSync(projectsDir);
16364
16936
  } catch {
16365
- return empty;
16937
+ return freezeClaudeCost(acc);
16366
16938
  }
16367
- let total = 0;
16368
- let inputTokens = 0;
16369
- let outputTokens = 0;
16370
- let cacheWriteTokens = 0;
16371
- let cacheReadTokens = 0;
16372
- const byDay = /* @__PURE__ */ new Map();
16373
- const byModel = /* @__PURE__ */ new Map();
16374
16939
  for (const proj of dirs) {
16375
- const projPath = path36.join(projectsDir, proj);
16376
- let files;
16940
+ processClaudeCostProject(proj, projectsDir, start, end, acc);
16941
+ }
16942
+ return freezeClaudeCost(acc);
16943
+ }
16944
+ function processCodexCostFile(filePath, start, end, acc) {
16945
+ let lines;
16946
+ try {
16947
+ lines = fs35.readFileSync(filePath, "utf-8").split("\n");
16948
+ } catch {
16949
+ return;
16950
+ }
16951
+ let sessionStart2 = "";
16952
+ let lastTotalInput = 0;
16953
+ let lastTotalCached = 0;
16954
+ let lastTotalOutput = 0;
16955
+ let sessionToolCalls = 0;
16956
+ for (const line of lines) {
16957
+ if (!line.trim()) continue;
16958
+ let entry;
16377
16959
  try {
16378
- const stat = fs35.statSync(projPath);
16379
- if (!stat.isDirectory()) continue;
16380
- files = fs35.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
16960
+ entry = JSON.parse(line);
16381
16961
  } catch {
16382
16962
  continue;
16383
16963
  }
16384
- for (const file of files) {
16385
- try {
16386
- const raw = fs35.readFileSync(path36.join(projPath, file), "utf-8");
16387
- for (const line of raw.split("\n")) {
16388
- if (!line.trim()) continue;
16389
- let entry;
16390
- try {
16391
- entry = JSON.parse(line);
16392
- } catch {
16393
- continue;
16394
- }
16395
- if (entry.type !== "assistant") continue;
16396
- if (!entry.timestamp) continue;
16397
- const ts = new Date(entry.timestamp);
16398
- if (ts < start || ts > end) continue;
16399
- const usage = entry.message?.usage;
16400
- const model = entry.message?.model;
16401
- if (!usage || !model) continue;
16402
- const p = claudeModelPrice2(model);
16403
- if (!p) continue;
16404
- const inp = usage.input_tokens ?? 0;
16405
- const out = usage.output_tokens ?? 0;
16406
- const cw = usage.cache_creation_input_tokens ?? 0;
16407
- const cr = usage.cache_read_input_tokens ?? 0;
16408
- const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
16409
- total += cost;
16410
- inputTokens += inp;
16411
- outputTokens += out;
16412
- cacheWriteTokens += cw;
16413
- cacheReadTokens += cr;
16414
- const dateKey = entry.timestamp.slice(0, 10);
16415
- byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
16416
- const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
16417
- byModel.set(normModel, (byModel.get(normModel) ?? 0) + cost);
16418
- }
16419
- } catch {
16420
- continue;
16421
- }
16964
+ const p = entry.payload ?? {};
16965
+ if (entry.type === "session_meta") {
16966
+ sessionStart2 = String(p["timestamp"] ?? "");
16967
+ continue;
16968
+ }
16969
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
16970
+ const info = p["info"] ?? {};
16971
+ const usage = info["total_token_usage"] ?? {};
16972
+ lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
16973
+ lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
16974
+ lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
16975
+ }
16976
+ if (entry.type === "response_item" && p["type"] === "function_call") {
16977
+ sessionToolCalls++;
16422
16978
  }
16423
16979
  }
16424
- return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
16980
+ if (!sessionStart2) return;
16981
+ const ts = new Date(sessionStart2);
16982
+ if (ts < start || ts > end) return;
16983
+ const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
16984
+ const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
16985
+ acc.total += cost;
16986
+ acc.toolCalls += sessionToolCalls;
16987
+ const dateKey = sessionStart2.slice(0, 10);
16988
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
16425
16989
  }
16426
- function loadCodexCost(start, end) {
16427
- const sessionsBase = path36.join(os31.homedir(), ".codex", "sessions");
16428
- const byDay = /* @__PURE__ */ new Map();
16429
- let total = 0;
16430
- let toolCalls = 0;
16431
- if (!fs35.existsSync(sessionsBase)) return { total, byDay, toolCalls };
16990
+ function listCodexSessionFiles(sessionsBase) {
16432
16991
  const jsonlFiles = [];
16992
+ if (!fs35.existsSync(sessionsBase)) return jsonlFiles;
16433
16993
  try {
16434
16994
  for (const year of fs35.readdirSync(sessionsBase)) {
16435
16995
  const yearPath = path36.join(sessionsBase, year);
@@ -16459,495 +17019,742 @@ function loadCodexCost(start, end) {
16459
17019
  }
16460
17020
  }
16461
17021
  } catch {
16462
- return { total, byDay, toolCalls };
17022
+ return [];
16463
17023
  }
16464
- for (const filePath of jsonlFiles) {
16465
- let lines;
17024
+ return jsonlFiles;
17025
+ }
17026
+ function loadCodexCost(start, end, sessionsBase) {
17027
+ const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
17028
+ const files = listCodexSessionFiles(sessionsBase);
17029
+ for (const filePath of files) {
17030
+ processCodexCostFile(filePath, start, end, acc);
17031
+ }
17032
+ return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
17033
+ }
17034
+ var GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
17035
+ function geminiPriceFor(model) {
17036
+ let tuple = pricingFor(model);
17037
+ if (!tuple && /^gemini-/i.test(model)) {
17038
+ for (const proxy of GEMINI_FALLBACK_MODELS) {
17039
+ tuple = pricingFor(proxy);
17040
+ if (tuple) break;
17041
+ }
17042
+ }
17043
+ if (!tuple) return null;
17044
+ return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
17045
+ }
17046
+ function emptyGeminiAccumulator() {
17047
+ return {
17048
+ total: 0,
17049
+ inputTokens: 0,
17050
+ outputTokens: 0,
17051
+ cacheReadTokens: 0,
17052
+ byDay: /* @__PURE__ */ new Map(),
17053
+ byProject: /* @__PURE__ */ new Map()
17054
+ };
17055
+ }
17056
+ function freezeGeminiCost(acc) {
17057
+ return {
17058
+ total: acc.total,
17059
+ byDay: acc.byDay,
17060
+ byProject: acc.byProject,
17061
+ inputTokens: acc.inputTokens,
17062
+ outputTokens: acc.outputTokens,
17063
+ cacheReadTokens: acc.cacheReadTokens
17064
+ };
17065
+ }
17066
+ function processGeminiCostFile(filePath, projectKey, start, end, acc) {
17067
+ const startMs = start.getTime();
17068
+ try {
17069
+ if (fs35.statSync(filePath).mtimeMs < startMs) return;
17070
+ } catch {
17071
+ return;
17072
+ }
17073
+ let raw;
17074
+ try {
17075
+ raw = fs35.readFileSync(filePath, "utf-8");
17076
+ } catch {
17077
+ return;
17078
+ }
17079
+ const seenIds = /* @__PURE__ */ new Set();
17080
+ for (const line of raw.split("\n")) {
17081
+ if (!line.trim()) continue;
17082
+ let entry;
16466
17083
  try {
16467
- lines = fs35.readFileSync(filePath, "utf-8").split("\n");
17084
+ entry = JSON.parse(line);
16468
17085
  } catch {
16469
17086
  continue;
16470
17087
  }
16471
- let sessionStart2 = "";
16472
- let lastTotalInput = 0;
16473
- let lastTotalCached = 0;
16474
- let lastTotalOutput = 0;
16475
- let sessionToolCalls = 0;
16476
- for (const line of lines) {
16477
- if (!line.trim()) continue;
16478
- let entry;
16479
- try {
16480
- entry = JSON.parse(line);
16481
- } catch {
16482
- continue;
16483
- }
16484
- const p = entry.payload ?? {};
16485
- if (entry.type === "session_meta") {
16486
- sessionStart2 = String(p["timestamp"] ?? "");
16487
- continue;
16488
- }
16489
- if (entry.type === "event_msg" && p["type"] === "token_count") {
16490
- const info = p["info"] ?? {};
16491
- const usage = info["total_token_usage"] ?? {};
16492
- lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
16493
- lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
16494
- lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
16495
- }
16496
- if (entry.type === "response_item" && p["type"] === "function_call") {
16497
- sessionToolCalls++;
16498
- }
17088
+ if (entry.type !== "gemini") continue;
17089
+ if (!entry.tokens || !entry.model || !entry.timestamp) continue;
17090
+ if (entry.id) {
17091
+ if (seenIds.has(entry.id)) continue;
17092
+ seenIds.add(entry.id);
16499
17093
  }
16500
- if (!sessionStart2) continue;
16501
- const ts = new Date(sessionStart2);
17094
+ const ts = new Date(entry.timestamp);
16502
17095
  if (ts < start || ts > end) continue;
16503
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
16504
- const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
16505
- total += cost;
16506
- toolCalls += sessionToolCalls;
16507
- const dateKey = sessionStart2.slice(0, 10);
16508
- byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
17096
+ const price = geminiPriceFor(entry.model);
17097
+ if (!price) continue;
17098
+ const inp = entry.tokens.input ?? 0;
17099
+ const out = entry.tokens.output ?? 0;
17100
+ const cached = Math.min(entry.tokens.cached ?? 0, inp);
17101
+ const fresh = Math.max(0, inp - cached);
17102
+ const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
17103
+ acc.total += cost;
17104
+ acc.inputTokens += inp;
17105
+ acc.outputTokens += out;
17106
+ acc.cacheReadTokens += cached;
17107
+ const dateKey = entry.timestamp.slice(0, 10);
17108
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
17109
+ const rollup = acc.byProject.get(projectKey) ?? {
17110
+ cost: 0,
17111
+ inputTokens: 0,
17112
+ outputTokens: 0
17113
+ };
17114
+ rollup.cost += cost;
17115
+ rollup.inputTokens += inp;
17116
+ rollup.outputTokens += out;
17117
+ acc.byProject.set(projectKey, rollup);
16509
17118
  }
16510
- return { total, byDay, toolCalls };
17119
+ }
17120
+ function listGeminiSessionFiles(geminiTmpDir) {
17121
+ const out = [];
17122
+ let dirs;
17123
+ try {
17124
+ if (!fs35.statSync(geminiTmpDir).isDirectory()) return out;
17125
+ dirs = fs35.readdirSync(geminiTmpDir);
17126
+ } catch {
17127
+ return out;
17128
+ }
17129
+ for (const proj of dirs) {
17130
+ const chatsDir = path36.join(geminiTmpDir, proj, "chats");
17131
+ let files;
17132
+ try {
17133
+ if (!fs35.statSync(chatsDir).isDirectory()) continue;
17134
+ files = fs35.readdirSync(chatsDir);
17135
+ } catch {
17136
+ continue;
17137
+ }
17138
+ for (const f of files) {
17139
+ if (!f.endsWith(".jsonl")) continue;
17140
+ out.push({ projectKey: proj, file: path36.join(chatsDir, f) });
17141
+ }
17142
+ }
17143
+ return out;
17144
+ }
17145
+ function loadGeminiCost(start, end, geminiTmpDir) {
17146
+ const acc = emptyGeminiAccumulator();
17147
+ if (!fs35.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
17148
+ for (const { projectKey, file } of listGeminiSessionFiles(geminiTmpDir)) {
17149
+ processGeminiCostFile(file, projectKey, start, end, acc);
17150
+ }
17151
+ return freezeGeminiCost(acc);
17152
+ }
17153
+ function aggregateReportFromAudit(period, opts = {}) {
17154
+ const now = opts.now ?? /* @__PURE__ */ new Date();
17155
+ const auditLogPath = opts.auditLogPath ?? path36.join(os31.homedir(), ".node9", "audit.log");
17156
+ const claudeProjectsDir = opts.claudeProjectsDir ?? path36.join(os31.homedir(), ".claude", "projects");
17157
+ const codexSessionsDir = opts.codexSessionsDir ?? path36.join(os31.homedir(), ".codex", "sessions");
17158
+ const geminiTmpDir = opts.geminiTmpDir ?? path36.join(os31.homedir(), ".gemini", "tmp");
17159
+ const hasAuditFile = fs35.existsSync(auditLogPath);
17160
+ const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath);
17161
+ const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
17162
+ const { start, end } = getDateRange(period, now);
17163
+ const responseDlpEntries = allEntries.filter((e) => {
17164
+ if (e.source !== "response-dlp") return false;
17165
+ const ts = new Date(e.ts);
17166
+ return ts >= start && ts <= end;
17167
+ }).map((e) => {
17168
+ const raw = e;
17169
+ return {
17170
+ ts: e.ts,
17171
+ dlpPattern: typeof raw.dlpPattern === "string" ? raw.dlpPattern : void 0,
17172
+ dlpSample: typeof raw.dlpSample === "string" ? raw.dlpSample : void 0
17173
+ };
17174
+ });
17175
+ const claudeCost = opts.preloadedClaudeCost ?? loadClaudeCost(start, end, claudeProjectsDir);
17176
+ const codexCost = opts.preloadedCodexCost ?? loadCodexCost(start, end, codexSessionsDir);
17177
+ const geminiCost = opts.preloadedGeminiCost ?? loadGeminiCost(start, end, geminiTmpDir);
17178
+ for (const [day, c] of codexCost.byDay) {
17179
+ claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
17180
+ }
17181
+ for (const [day, c] of geminiCost.byDay) {
17182
+ claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
17183
+ }
17184
+ for (const [geminiKey, gRollup] of geminiCost.byProject) {
17185
+ let mergedInto = null;
17186
+ for (const claudeKey of claudeCost.byProject.keys()) {
17187
+ const claudeBase = claudeKey.match(/[^/\\]+$/)?.[0] ?? claudeKey;
17188
+ if (claudeBase === geminiKey) {
17189
+ mergedInto = claudeKey;
17190
+ break;
17191
+ }
17192
+ }
17193
+ const targetKey = mergedInto ?? geminiKey;
17194
+ const existing = claudeCost.byProject.get(targetKey) ?? {
17195
+ cost: 0,
17196
+ inputTokens: 0,
17197
+ outputTokens: 0
17198
+ };
17199
+ existing.cost += gRollup.cost;
17200
+ existing.inputTokens += gRollup.inputTokens;
17201
+ existing.outputTokens += gRollup.outputTokens;
17202
+ claudeCost.byProject.set(targetKey, existing);
17203
+ }
17204
+ const periodMs = end.getTime() - start.getTime();
17205
+ const priorEnd = new Date(start.getTime() - 1);
17206
+ const priorStart = new Date(start.getTime() - periodMs);
17207
+ const priorEntries = allEntries.filter((e) => {
17208
+ if (e.source === "post-hook") return false;
17209
+ const ts = new Date(e.ts);
17210
+ return ts >= priorStart && ts <= priorEnd;
17211
+ });
17212
+ const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
17213
+ const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
17214
+ const excludeTests = opts.excludeTests === true;
17215
+ const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
17216
+ let excludedTests = 0;
17217
+ const entries = allEntries.filter((e) => {
17218
+ if (e.source === "post-hook") return false;
17219
+ if (e.source === "response-dlp") return false;
17220
+ const ts = new Date(e.ts);
17221
+ if (ts < start || ts > end) return false;
17222
+ if (excludeTests && isTestEntry(e, testTs)) {
17223
+ excludedTests++;
17224
+ return false;
17225
+ }
17226
+ return true;
17227
+ });
17228
+ let userApproved = 0;
17229
+ let userDenied = 0;
17230
+ let timedOut = 0;
17231
+ let hardBlocked = 0;
17232
+ let dlpBlocked = 0;
17233
+ let observeDlp = 0;
17234
+ let loopHits = 0;
17235
+ let testPasses = 0;
17236
+ let testFails = 0;
17237
+ const toolMap = /* @__PURE__ */ new Map();
17238
+ const blockMap = /* @__PURE__ */ new Map();
17239
+ const ruleMap = /* @__PURE__ */ new Map();
17240
+ const agentMap = /* @__PURE__ */ new Map();
17241
+ const mcpMap = /* @__PURE__ */ new Map();
17242
+ const dailyMap = /* @__PURE__ */ new Map();
17243
+ const hourMap = /* @__PURE__ */ new Map();
17244
+ for (const e of entries) {
17245
+ const allow = isAllow(e.decision);
17246
+ const dateKey = e.ts.slice(0, 10);
17247
+ const userInteracted = e.source === "daemon";
17248
+ if (userInteracted) {
17249
+ if (allow) userApproved++;
17250
+ else userDenied++;
17251
+ } else if (!allow) {
17252
+ if (e.checkedBy === "timeout") timedOut++;
17253
+ else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
17254
+ else if (isDlp(e.checkedBy)) dlpBlocked++;
17255
+ else if (e.checkedBy !== "loop-detected") hardBlocked++;
17256
+ }
17257
+ if (e.checkedBy === "loop-detected") loopHits++;
17258
+ const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
17259
+ t.calls++;
17260
+ if (!allow) t.blocked++;
17261
+ toolMap.set(e.tool, t);
17262
+ if (!allow && e.checkedBy) {
17263
+ blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
17264
+ }
17265
+ if (!allow && e.ruleName) {
17266
+ ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
17267
+ }
17268
+ if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
17269
+ if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
17270
+ const hour = new Date(e.ts).getHours();
17271
+ hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
17272
+ const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
17273
+ d.calls++;
17274
+ if (!allow) d.blocked++;
17275
+ dailyMap.set(dateKey, d);
17276
+ }
17277
+ for (const e of allEntries) {
17278
+ if (e.source !== "test-result") continue;
17279
+ const ts = new Date(e.ts);
17280
+ if (ts < start || ts > end) continue;
17281
+ if (e.testResult === "pass") testPasses++;
17282
+ else if (e.testResult === "fail") testFails++;
17283
+ }
17284
+ if (codexCost.toolCalls > 0) {
17285
+ agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexCost.toolCalls);
17286
+ }
17287
+ const data = {
17288
+ period,
17289
+ start,
17290
+ end,
17291
+ excludedTests,
17292
+ total: entries.length,
17293
+ userApproved,
17294
+ userDenied,
17295
+ timedOut,
17296
+ hardBlocked,
17297
+ dlpBlocked,
17298
+ observeDlp,
17299
+ loopHits,
17300
+ testPasses,
17301
+ testFails,
17302
+ unackedDlp: unackedDlp.length,
17303
+ priorBlockRate,
17304
+ cost: {
17305
+ claudeUSD: claudeCost.total,
17306
+ codexUSD: codexCost.total,
17307
+ geminiUSD: geminiCost.total,
17308
+ inputTokens: claudeCost.inputTokens + geminiCost.inputTokens,
17309
+ outputTokens: claudeCost.outputTokens + geminiCost.outputTokens,
17310
+ cacheWriteTokens: claudeCost.cacheWriteTokens,
17311
+ cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
17312
+ byDay: claudeCost.byDay,
17313
+ byModel: claudeCost.byModel,
17314
+ byProject: claudeCost.byProject
17315
+ },
17316
+ toolMap,
17317
+ blockMap,
17318
+ ruleMap,
17319
+ agentMap,
17320
+ mcpMap,
17321
+ dailyMap,
17322
+ hourMap,
17323
+ generatedAt: now.toISOString()
17324
+ };
17325
+ return { data, hasAuditFile, responseDlpEntries };
17326
+ }
17327
+
17328
+ // src/cli/render/report-json.ts
17329
+ function buildReportJson(input) {
17330
+ const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
17331
+ const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
17332
+ const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
17333
+ return {
17334
+ schemaVersion: 1,
17335
+ generatedAt: input.generatedAt,
17336
+ period: input.period,
17337
+ range: { start: input.start.toISOString(), end: input.end.toISOString() },
17338
+ excludedTests: input.excludedTests,
17339
+ totals: {
17340
+ events: input.total,
17341
+ blocked: totalBlocked,
17342
+ blockRate,
17343
+ userApproved: input.userApproved,
17344
+ userDenied: input.userDenied,
17345
+ timedOut: input.timedOut,
17346
+ hardBlocked: input.hardBlocked,
17347
+ dlpBlocked: input.dlpBlocked,
17348
+ observeDlp: input.observeDlp,
17349
+ loopHits: input.loopHits,
17350
+ unackedDlp: input.unackedDlp
17351
+ },
17352
+ tests: {
17353
+ passes: input.testPasses,
17354
+ fails: input.testFails
17355
+ },
17356
+ cost: {
17357
+ totalUSD: input.cost.claudeUSD + input.cost.codexUSD + input.cost.geminiUSD,
17358
+ claudeUSD: input.cost.claudeUSD,
17359
+ codexUSD: input.cost.codexUSD,
17360
+ geminiUSD: input.cost.geminiUSD,
17361
+ inputTokens: input.cost.inputTokens,
17362
+ outputTokens: input.cost.outputTokens,
17363
+ cacheWriteTokens: input.cost.cacheWriteTokens,
17364
+ cacheReadTokens: input.cost.cacheReadTokens,
17365
+ byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
17366
+ byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
17367
+ },
17368
+ byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
17369
+ byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
17370
+ byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
17371
+ byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
17372
+ byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
17373
+ byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
17374
+ trend: {
17375
+ priorBlockRate: input.priorBlockRate,
17376
+ deltaPct
17377
+ }
17378
+ };
17379
+ }
17380
+
17381
+ // src/cli/commands/report.ts
17382
+ var BLOCK_REASON_LABELS = {
17383
+ timeout: "Approval timeout",
17384
+ "smart-rule-block": "Smart rule",
17385
+ "observe-mode-dlp-would-block": "DLP (observe)",
17386
+ "persistent-deny": "Persistent deny",
17387
+ "local-decision": "User denied",
17388
+ "dlp-block": "DLP block",
17389
+ "loop-detected": "Loop detected"
17390
+ };
17391
+ function humanBlockReason(reason) {
17392
+ return BLOCK_REASON_LABELS[reason] ?? reason;
17393
+ }
17394
+ function barStr(value, max, width) {
17395
+ if (max === 0 || width <= 0) return "\u2591".repeat(width);
17396
+ const filled = Math.max(1, Math.round(value / max * width));
17397
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
17398
+ }
17399
+ function colorBar(value, max, width) {
17400
+ const s = barStr(value, max, width);
17401
+ const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
17402
+ return chalk13.cyan(s.slice(0, filled)) + chalk13.dim(s.slice(filled));
17403
+ }
17404
+ function fmtDate(d) {
17405
+ const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
17406
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
17407
+ }
17408
+ function num2(n) {
17409
+ return n.toLocaleString();
17410
+ }
17411
+ function fmtCost2(usd) {
17412
+ if (usd < 1e-3) return "< $0.001";
17413
+ if (usd < 1) return "$" + usd.toFixed(4);
17414
+ return "$" + usd.toFixed(2);
16511
17415
  }
16512
17416
  function registerReportCommand(program2) {
16513
17417
  program2.command("report").description("Activity and security report \u2014 what Claude did, what was blocked").option("--period <period>", "today | 7d | 30d | month", "7d").option("--no-tests", "exclude test runner calls (npm test, vitest, pytest\u2026) from stats").option("--json", "Emit machine-readable JSON to stdout (suppresses renderer)").action((options) => {
16514
- const period = ["today", "7d", "30d", "month"].includes(
17418
+ const period = ["today", "7d", "30d", "90d", "month"].includes(
16515
17419
  options.period
16516
17420
  ) ? options.period : "7d";
16517
- const logPath = path36.join(os31.homedir(), ".node9", "audit.log");
16518
- const allEntries = parseAuditLog(logPath);
16519
- const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
16520
- if (unackedDlp.length > 0 && !options.json) {
17421
+ const excludeTests = options.tests === false;
17422
+ const { data, hasAuditFile, responseDlpEntries } = aggregateReportFromAudit(period, {
17423
+ excludeTests
17424
+ });
17425
+ if (data.unackedDlp > 0 && !options.json) {
16521
17426
  console.log("");
16522
17427
  console.log(
16523
17428
  chalk13.bgRed.white.bold(
16524
- ` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
17429
+ ` \u26A0\uFE0F DLP ALERT: ${data.unackedDlp} secret${data.unackedDlp !== 1 ? "s" : ""} found in Claude response text `
16525
17430
  ) + " " + chalk13.yellow("\u2192 run: node9 dlp")
16526
17431
  );
16527
17432
  }
16528
- if (allEntries.length === 0 && !options.json) {
17433
+ if (!hasAuditFile && !options.json) {
16529
17434
  console.log(
16530
17435
  chalk13.yellow("\n No audit data found. Run node9 with Claude Code to generate entries.\n")
16531
17436
  );
16532
17437
  return;
16533
17438
  }
16534
- const { start, end } = getDateRange(period);
16535
- const {
16536
- total: claudeCostUSD,
16537
- byDay: costByDay,
16538
- byModel: costByModel,
16539
- inputTokens: costInputTokens,
16540
- outputTokens: costOutputTokens,
16541
- cacheWriteTokens: costCacheWrite,
16542
- cacheReadTokens: costCacheRead
16543
- } = loadClaudeCost(start, end);
16544
- const {
16545
- total: codexCostUSD,
16546
- byDay: codexCostByDay,
16547
- toolCalls: codexToolCalls
16548
- } = loadCodexCost(start, end);
16549
- const costUSD = claudeCostUSD + codexCostUSD;
16550
- for (const [day, c] of codexCostByDay) {
16551
- costByDay.set(day, (costByDay.get(day) ?? 0) + c);
16552
- }
16553
- const periodMs = end.getTime() - start.getTime();
16554
- const priorEnd = new Date(start.getTime() - 1);
16555
- const priorStart = new Date(start.getTime() - periodMs);
16556
- const priorEntries = allEntries.filter((e) => {
16557
- if (e.source === "post-hook") return false;
16558
- const ts = new Date(e.ts);
16559
- return ts >= priorStart && ts <= priorEnd;
16560
- });
16561
- const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
16562
- const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
16563
- const excludeTests = options.tests === false;
16564
- const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
16565
- let filteredTestCount = 0;
16566
- const entries = allEntries.filter((e) => {
16567
- if (e.source === "post-hook") return false;
16568
- if (e.source === "response-dlp") return false;
16569
- const ts = new Date(e.ts);
16570
- if (ts < start || ts > end) return false;
16571
- if (excludeTests && isTestEntry(e, testTs)) {
16572
- filteredTestCount++;
16573
- return false;
16574
- }
16575
- return true;
16576
- });
16577
- if (entries.length === 0 && !options.json) {
17439
+ if (options.json) {
17440
+ const envelope = buildReportJson(data);
17441
+ process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
17442
+ return;
17443
+ }
17444
+ if (data.total === 0) {
16578
17445
  console.log(chalk13.yellow(`
16579
17446
  No activity for period "${period}".
16580
17447
  `));
16581
17448
  return;
16582
17449
  }
16583
- let userApproved = 0;
16584
- let userDenied = 0;
16585
- let timedOut = 0;
16586
- let hardBlocked = 0;
16587
- let dlpBlocked = 0;
16588
- let observeDlp = 0;
16589
- let loopHits = 0;
16590
- let testPasses = 0;
16591
- let testFails = 0;
16592
- const toolMap = /* @__PURE__ */ new Map();
16593
- const blockMap = /* @__PURE__ */ new Map();
16594
- const agentMap = /* @__PURE__ */ new Map();
16595
- const mcpMap = /* @__PURE__ */ new Map();
16596
- const dailyMap = /* @__PURE__ */ new Map();
16597
- const hourMap = /* @__PURE__ */ new Map();
16598
- for (const e of entries) {
16599
- const allow = isAllow(e.decision);
16600
- const dateKey = e.ts.slice(0, 10);
16601
- const userInteracted = e.source === "daemon";
16602
- if (userInteracted) {
16603
- if (allow) userApproved++;
16604
- else userDenied++;
16605
- } else if (!allow) {
16606
- if (e.checkedBy === "timeout") timedOut++;
16607
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
16608
- else if (isDlp(e.checkedBy)) dlpBlocked++;
16609
- else if (e.checkedBy !== "loop-detected") hardBlocked++;
16610
- }
16611
- if (e.checkedBy === "loop-detected") loopHits++;
16612
- const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
16613
- t.calls++;
16614
- if (!allow) t.blocked++;
16615
- toolMap.set(e.tool, t);
16616
- if (!allow && e.checkedBy) {
16617
- blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
16618
- }
16619
- if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
16620
- if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
16621
- const hour = new Date(e.ts).getHours();
16622
- hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
16623
- const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
16624
- d.calls++;
16625
- if (!allow) d.blocked++;
16626
- dailyMap.set(dateKey, d);
16627
- }
16628
- for (const e of allEntries) {
16629
- if (e.source !== "test-result") continue;
16630
- const ts = new Date(e.ts);
16631
- if (ts < start || ts > end) continue;
16632
- if (e.testResult === "pass") testPasses++;
16633
- else if (e.testResult === "fail") testFails++;
16634
- }
16635
- if (codexToolCalls > 0) agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexToolCalls);
16636
- if (options.json) {
16637
- const envelope = buildReportJson({
16638
- period,
16639
- start,
16640
- end,
16641
- excludedTests: filteredTestCount,
16642
- total: entries.length,
16643
- userApproved,
16644
- userDenied,
16645
- timedOut,
16646
- hardBlocked,
16647
- dlpBlocked,
16648
- observeDlp,
16649
- loopHits,
16650
- testPasses,
16651
- testFails,
16652
- unackedDlp: unackedDlp.length,
16653
- priorBlockRate,
16654
- cost: {
16655
- claudeUSD: claudeCostUSD,
16656
- codexUSD: codexCostUSD,
16657
- inputTokens: costInputTokens,
16658
- outputTokens: costOutputTokens,
16659
- cacheWriteTokens: costCacheWrite,
16660
- cacheReadTokens: costCacheRead,
16661
- byDay: costByDay,
16662
- byModel: costByModel
16663
- },
16664
- toolMap,
16665
- blockMap,
16666
- agentMap,
16667
- mcpMap,
16668
- dailyMap,
16669
- hourMap,
16670
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
16671
- });
16672
- process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
16673
- return;
16674
- }
16675
- const total = entries.length;
16676
- const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
16677
- const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
16678
- const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
16679
- const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
16680
- const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
16681
- const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
16682
- const W = Math.min(process.stdout.columns || 80, 100);
16683
- const INNER = W - 4;
16684
- const COL = Math.floor(INNER / 2) - 1;
16685
- const LABEL = 24;
16686
- const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
16687
- const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num2(v.calls).length), 1);
16688
- const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num2(v).length), 1);
16689
- const line = chalk13.dim("\u2500".repeat(W - 2));
16690
- const periodLabel = {
16691
- today: "Today",
16692
- "7d": "Last 7 Days",
16693
- "30d": "Last 30 Days",
16694
- month: "This Month"
16695
- };
17450
+ renderTerminalReport(data, responseDlpEntries, excludeTests);
17451
+ });
17452
+ }
17453
+ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
17454
+ const {
17455
+ period,
17456
+ start,
17457
+ end,
17458
+ total,
17459
+ excludedTests,
17460
+ userApproved,
17461
+ userDenied,
17462
+ timedOut,
17463
+ hardBlocked,
17464
+ dlpBlocked,
17465
+ observeDlp,
17466
+ loopHits,
17467
+ testPasses,
17468
+ testFails,
17469
+ priorBlockRate,
17470
+ cost: {
17471
+ claudeUSD,
17472
+ codexUSD,
17473
+ geminiUSD,
17474
+ inputTokens: costInputTokens,
17475
+ outputTokens: costOutputTokens,
17476
+ cacheWriteTokens: costCacheWrite,
17477
+ cacheReadTokens: costCacheRead,
17478
+ byDay: costByDay,
17479
+ byModel: costByModel
17480
+ },
17481
+ toolMap,
17482
+ blockMap,
17483
+ agentMap,
17484
+ mcpMap,
17485
+ dailyMap,
17486
+ hourMap
17487
+ } = data;
17488
+ const costUSD = claudeUSD + codexUSD + geminiUSD;
17489
+ const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
17490
+ const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
17491
+ const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
17492
+ const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
17493
+ const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
17494
+ const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
17495
+ const W = Math.min(process.stdout.columns || 80, 100);
17496
+ const INNER = W - 4;
17497
+ const COL = Math.floor(INNER / 2) - 1;
17498
+ const LABEL = 24;
17499
+ const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
17500
+ const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num2(v.calls).length), 1);
17501
+ const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num2(v).length), 1);
17502
+ const line = chalk13.dim("\u2500".repeat(W - 2));
17503
+ const periodLabel = {
17504
+ today: "Today",
17505
+ "7d": "Last 7 Days",
17506
+ "30d": "Last 30 Days",
17507
+ "90d": "Last 90 Days",
17508
+ month: "This Month"
17509
+ };
17510
+ console.log("");
17511
+ console.log(
17512
+ " " + chalk13.bold.cyan("\u{1F6E1} node9 Report") + chalk13.dim(" \xB7 ") + chalk13.white(periodLabel[period]) + chalk13.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + chalk13.dim(` ${num2(total)} events`) + (excludeTests ? chalk13.dim(` \u2013tests (\u2013${excludedTests})`) : "")
17513
+ );
17514
+ console.log(" " + line);
17515
+ const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
17516
+ const currentRate = total > 0 ? totalBlocked / total : 0;
17517
+ const trendLabel = (() => {
17518
+ if (priorBlockRate === null) return "";
17519
+ const delta = Math.round((currentRate - priorBlockRate) * 100);
17520
+ if (delta === 0) return "";
17521
+ return " " + (delta > 0 ? chalk13.red(`\u25B2${delta}%`) : chalk13.green(`\u25BC${Math.abs(delta)}%`)) + chalk13.dim(" vs prior");
17522
+ })();
17523
+ const reads = toolMap.get("Read")?.calls ?? 0;
17524
+ const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
17525
+ const ratioLabel = reads > 0 ? chalk13.dim(`edit/read ${(edits / reads).toFixed(1)}`) : chalk13.dim("edit/read \u2013");
17526
+ const testLabel = testPasses + testFails > 0 ? chalk13.dim("tests ") + chalk13.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + chalk13.red(`${testFails}\u2717`) : "") : chalk13.dim("tests \u2013");
17527
+ console.log("");
17528
+ console.log(" " + chalk13.bold("Protection Summary"));
17529
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
17530
+ console.log(
17531
+ " " + chalk13.dim("Intercepted") + " " + chalk13.white(num2(total)) + chalk13.dim(" tool calls")
17532
+ );
17533
+ console.log("");
17534
+ const COL1 = 18;
17535
+ const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
17536
+ const countStr = colorFn(num2(count));
17537
+ const noteStr = note ? chalk13.dim(" " + note) : "";
17538
+ console.log(" " + icon + " " + chalk13.white(label.padEnd(COL1)) + countStr + noteStr);
17539
+ };
17540
+ summaryRow(
17541
+ userApproved > 0 ? chalk13.green("\u2705") : chalk13.dim("\u2705"),
17542
+ "User approved",
17543
+ userApproved,
17544
+ userApproved === 0 ? "no popups this period" : void 0,
17545
+ userApproved > 0 ? (s) => chalk13.green(s) : (s) => chalk13.dim(s)
17546
+ );
17547
+ summaryRow(
17548
+ userDenied > 0 ? chalk13.red("\u{1F6AB}") : chalk13.dim("\u{1F6AB}"),
17549
+ "User denied",
17550
+ userDenied,
17551
+ void 0,
17552
+ userDenied > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
17553
+ );
17554
+ summaryRow(
17555
+ timedOut > 0 ? chalk13.yellow("\u23F1") : chalk13.dim("\u23F1"),
17556
+ "Timed out",
17557
+ timedOut,
17558
+ timedOut > 0 ? "no approval response" : void 0,
17559
+ timedOut > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
17560
+ );
17561
+ summaryRow(
17562
+ hardBlocked > 0 ? chalk13.red("\u{1F6D1}") : chalk13.dim("\u{1F6D1}"),
17563
+ "Auto-blocked",
17564
+ hardBlocked,
17565
+ void 0,
17566
+ hardBlocked > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
17567
+ );
17568
+ summaryRow(
17569
+ dlpBlocked > 0 ? chalk13.yellow("\u{1F6A8}") : chalk13.dim("\u{1F6A8}"),
17570
+ "DLP blocked",
17571
+ dlpBlocked,
17572
+ void 0,
17573
+ dlpBlocked > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
17574
+ );
17575
+ summaryRow(
17576
+ observeDlp > 0 ? chalk13.blue("\u{1F441}") : chalk13.dim("\u{1F441}"),
17577
+ "DLP (observe)",
17578
+ observeDlp,
17579
+ observeDlp > 0 ? "would-block in strict mode" : void 0,
17580
+ observeDlp > 0 ? (s) => chalk13.blue(s) : (s) => chalk13.dim(s)
17581
+ );
17582
+ summaryRow(
17583
+ loopHits > 0 ? chalk13.yellow("\u{1F504}") : chalk13.dim("\u{1F504}"),
17584
+ "Loops detected",
17585
+ loopHits,
17586
+ void 0,
17587
+ loopHits > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
17588
+ );
17589
+ if (trendLabel || ratioLabel || testPasses + testFails > 0) {
16696
17590
  console.log("");
16697
- console.log(
16698
- " " + chalk13.bold.cyan("\u{1F6E1} node9 Report") + chalk13.dim(" \xB7 ") + chalk13.white(periodLabel[period]) + chalk13.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + chalk13.dim(` ${num2(total)} events`) + (excludeTests ? chalk13.dim(` \u2013tests (\u2013${filteredTestCount})`) : "")
16699
- );
16700
- console.log(" " + line);
16701
- const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
16702
- const currentRate = total > 0 ? totalBlocked / total : 0;
16703
- const trendLabel = (() => {
16704
- if (priorBlockRate === null) return "";
16705
- const delta = Math.round((currentRate - priorBlockRate) * 100);
16706
- if (delta === 0) return "";
16707
- return " " + (delta > 0 ? chalk13.red(`\u25B2${delta}%`) : chalk13.green(`\u25BC${Math.abs(delta)}%`)) + chalk13.dim(" vs prior");
16708
- })();
16709
- const reads = toolMap.get("Read")?.calls ?? 0;
16710
- const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
16711
- const ratioLabel = reads > 0 ? chalk13.dim(`edit/read ${(edits / reads).toFixed(1)}`) : chalk13.dim("edit/read \u2013");
16712
- const testLabel = testPasses + testFails > 0 ? chalk13.dim("tests ") + chalk13.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + chalk13.red(`${testFails}\u2717`) : "") : chalk13.dim("tests \u2013");
17591
+ console.log(" " + ratioLabel + " " + testLabel + trendLabel);
17592
+ }
17593
+ console.log("");
17594
+ const toolHeaderRaw = "Top Tools";
17595
+ const blockHeaderRaw = "Top Blocks";
17596
+ console.log(
17597
+ " " + chalk13.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + chalk13.bold(blockHeaderRaw)
17598
+ );
17599
+ console.log(" " + chalk13.dim("\u2500".repeat(COL)) + " " + chalk13.dim("\u2500".repeat(COL)));
17600
+ const rows = Math.max(topTools.length, topBlocks.length, 1);
17601
+ for (let i = 0; i < rows; i++) {
17602
+ let leftStyled = " ".repeat(COL);
17603
+ if (i < topTools.length) {
17604
+ const [tool, { calls }] = topTools[i];
17605
+ const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
17606
+ const countStr = num2(calls).padStart(TOOL_COUNT_W);
17607
+ const b = colorBar(calls, maxTool, BAR);
17608
+ const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
17609
+ const pad = Math.max(0, COL - rawLen);
17610
+ leftStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(countStr) + " ".repeat(pad);
17611
+ }
17612
+ let rightStyled = "";
17613
+ if (i < topBlocks.length) {
17614
+ const [reason, count] = topBlocks[i];
17615
+ const readable = humanBlockReason(reason);
17616
+ const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
17617
+ const countStr = num2(count).padStart(BLOCK_COUNT_W);
17618
+ const b = colorBar(count, maxBlock, BAR);
17619
+ rightStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.red(countStr);
17620
+ }
17621
+ console.log(" " + leftStyled + " " + rightStyled);
17622
+ }
17623
+ if (topBlocks.length === 0) {
17624
+ console.log(" " + " ".repeat(COL) + " " + chalk13.dim("nothing blocked \u2713"));
17625
+ }
17626
+ if (agentMap.size >= 1) {
16713
17627
  console.log("");
16714
- console.log(" " + chalk13.bold("Protection Summary"));
17628
+ console.log(" " + chalk13.bold("Agents"));
16715
17629
  console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16716
- console.log(
16717
- " " + chalk13.dim("Intercepted") + " " + chalk13.white(num2(total)) + chalk13.dim(" tool calls")
16718
- );
16719
- console.log("");
16720
- const COL1 = 18;
16721
- const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
16722
- const countStr = colorFn(num2(count));
16723
- const noteStr = note ? chalk13.dim(" " + note) : "";
16724
- console.log(" " + icon + " " + chalk13.white(label.padEnd(COL1)) + countStr + noteStr);
16725
- };
16726
- summaryRow(
16727
- userApproved > 0 ? chalk13.green("\u2705") : chalk13.dim("\u2705"),
16728
- "User approved",
16729
- userApproved,
16730
- userApproved === 0 ? "no popups this period" : void 0,
16731
- userApproved > 0 ? (s) => chalk13.green(s) : (s) => chalk13.dim(s)
16732
- );
16733
- summaryRow(
16734
- userDenied > 0 ? chalk13.red("\u{1F6AB}") : chalk13.dim("\u{1F6AB}"),
16735
- "User denied",
16736
- userDenied,
16737
- void 0,
16738
- userDenied > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
16739
- );
16740
- summaryRow(
16741
- timedOut > 0 ? chalk13.yellow("\u23F1") : chalk13.dim("\u23F1"),
16742
- "Timed out",
16743
- timedOut,
16744
- timedOut > 0 ? "no approval response" : void 0,
16745
- timedOut > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
16746
- );
16747
- summaryRow(
16748
- hardBlocked > 0 ? chalk13.red("\u{1F6D1}") : chalk13.dim("\u{1F6D1}"),
16749
- "Auto-blocked",
16750
- hardBlocked,
16751
- void 0,
16752
- hardBlocked > 0 ? (s) => chalk13.red(s) : (s) => chalk13.dim(s)
16753
- );
16754
- summaryRow(
16755
- dlpBlocked > 0 ? chalk13.yellow("\u{1F6A8}") : chalk13.dim("\u{1F6A8}"),
16756
- "DLP blocked",
16757
- dlpBlocked,
16758
- void 0,
16759
- dlpBlocked > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
16760
- );
16761
- summaryRow(
16762
- observeDlp > 0 ? chalk13.blue("\u{1F441}") : chalk13.dim("\u{1F441}"),
16763
- "DLP (observe)",
16764
- observeDlp,
16765
- observeDlp > 0 ? "would-block in strict mode" : void 0,
16766
- observeDlp > 0 ? (s) => chalk13.blue(s) : (s) => chalk13.dim(s)
16767
- );
16768
- summaryRow(
16769
- loopHits > 0 ? chalk13.yellow("\u{1F504}") : chalk13.dim("\u{1F504}"),
16770
- "Loops detected",
16771
- loopHits,
16772
- void 0,
16773
- loopHits > 0 ? (s) => chalk13.yellow(s) : (s) => chalk13.dim(s)
16774
- );
16775
- if (trendLabel || ratioLabel || testPasses + testFails > 0) {
16776
- console.log("");
16777
- console.log(" " + ratioLabel + " " + testLabel + trendLabel);
17630
+ const maxAgent = Math.max(...agentMap.values(), 1);
17631
+ for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
17632
+ const label = agent.slice(0, LABEL - 1);
17633
+ const b = colorBar(count, maxAgent, BAR);
17634
+ console.log(" " + chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(num2(count)));
16778
17635
  }
17636
+ }
17637
+ if (mcpMap.size > 0) {
16779
17638
  console.log("");
16780
- const toolHeaderRaw = "Top Tools";
16781
- const blockHeaderRaw = "Top Blocks";
16782
- console.log(
16783
- " " + chalk13.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + chalk13.bold(blockHeaderRaw)
16784
- );
16785
- console.log(" " + chalk13.dim("\u2500".repeat(COL)) + " " + chalk13.dim("\u2500".repeat(COL)));
16786
- const rows = Math.max(topTools.length, topBlocks.length, 1);
16787
- for (let i = 0; i < rows; i++) {
16788
- let leftStyled = " ".repeat(COL);
16789
- if (i < topTools.length) {
16790
- const [tool, { calls }] = topTools[i];
16791
- const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
16792
- const countStr = num2(calls).padStart(TOOL_COUNT_W);
16793
- const b = colorBar(calls, maxTool, BAR);
16794
- const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
16795
- const pad = Math.max(0, COL - rawLen);
16796
- leftStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(countStr) + " ".repeat(pad);
16797
- }
16798
- let rightStyled = "";
16799
- if (i < topBlocks.length) {
16800
- const [reason, count] = topBlocks[i];
16801
- const readable = humanBlockReason(reason);
16802
- const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
16803
- const countStr = num2(count).padStart(BLOCK_COUNT_W);
16804
- const b = colorBar(count, maxBlock, BAR);
16805
- rightStyled = chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.red(countStr);
16806
- }
16807
- console.log(" " + leftStyled + " " + rightStyled);
16808
- }
16809
- if (topBlocks.length === 0) {
16810
- console.log(" " + " ".repeat(COL) + " " + chalk13.dim("nothing blocked \u2713"));
16811
- }
16812
- if (agentMap.size >= 1) {
16813
- console.log("");
16814
- console.log(" " + chalk13.bold("Agents"));
16815
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16816
- const maxAgent = Math.max(...agentMap.values(), 1);
16817
- for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
16818
- const label = agent.slice(0, LABEL - 1);
16819
- const b = colorBar(count, maxAgent, BAR);
16820
- console.log(" " + chalk13.white(label.padEnd(LABEL)) + b + " " + chalk13.white(num2(count)));
16821
- }
16822
- }
16823
- if (mcpMap.size > 0) {
16824
- console.log("");
16825
- console.log(" " + chalk13.bold("MCP Servers"));
16826
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16827
- const maxMcp = Math.max(...mcpMap.values(), 1);
16828
- for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
16829
- const label = server.slice(0, LABEL - 1).padEnd(LABEL);
16830
- const b = colorBar(count, maxMcp, BAR);
16831
- console.log(" " + chalk13.white(label) + b + " " + chalk13.white(num2(count)));
16832
- }
16833
- }
16834
- if (hourMap.size > 0) {
16835
- const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
16836
- const maxHour = Math.max(...hourMap.values(), 1);
16837
- const bar = Array.from({ length: 24 }, (_, h) => {
16838
- const v = hourMap.get(h) ?? 0;
16839
- return BLOCKS[Math.round(v / maxHour * 8)];
16840
- }).join("");
16841
- console.log("");
16842
- console.log(" " + chalk13.bold("Hour of Day") + chalk13.dim(" (local, 0h \u2013 23h)"));
16843
- console.log(" " + chalk13.cyan(bar));
16844
- console.log(" " + chalk13.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
16845
- }
16846
- if (dailyList.length > 1) {
16847
- console.log("");
16848
- console.log(" " + chalk13.bold("Daily Activity"));
16849
- console.log(" " + chalk13.dim("\u2500".repeat(W - 2)));
16850
- const DAY_BAR = Math.max(8, Math.min(30, W - 36));
16851
- for (const [dateKey, { calls, blocked: db }] of dailyList) {
16852
- const label = fmtDate(dateKey).padEnd(10);
16853
- const b = colorBar(calls, maxDaily, DAY_BAR);
16854
- const dayCost = costByDay.get(dateKey);
16855
- const costNote = dayCost ? chalk13.magenta(` ${fmtCost2(dayCost)}`) : "";
16856
- const blockNote = db > 0 ? chalk13.red(` ${db} blocked`) : "";
16857
- console.log(
16858
- " " + chalk13.dim(label) + " " + b + " " + chalk13.white(num2(calls)) + blockNote + costNote
16859
- );
16860
- }
16861
- }
16862
- const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
16863
- if (totalTokens > 0) {
16864
- const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
16865
- console.log("");
16866
- console.log(" " + chalk13.bold("Tokens") + " " + chalk13.dim(`${num2(totalTokens)} total`));
16867
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16868
- const TOK_BAR = Math.max(6, Math.min(20, W - 30));
16869
- const TOK_LABEL = 14;
16870
- const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
16871
- const nonCacheRows = [
16872
- ["Input", costInputTokens, chalk13.cyan(num2(costInputTokens))],
16873
- ["Output", costOutputTokens, chalk13.white(num2(costOutputTokens))],
16874
- ["Cache write", costCacheWrite, chalk13.yellow(num2(costCacheWrite))]
16875
- ];
16876
- for (const [label, count, colored] of nonCacheRows) {
16877
- if (count === 0) continue;
16878
- const b = colorBar(count, maxNonCache, TOK_BAR);
16879
- console.log(" " + chalk13.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
16880
- }
16881
- if (costCacheRead > 0) {
16882
- const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
16883
- const pct = cacheHitPct > 0 ? chalk13.dim(` ${cacheHitPct}% hit rate`) : "";
16884
- console.log(
16885
- " " + chalk13.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + chalk13.green(num2(costCacheRead)) + pct
16886
- );
16887
- }
16888
- }
16889
- if (costUSD > 0) {
16890
- const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
16891
- const avgPerDay = costUSD / periodDays;
16892
- const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
16893
- const costHeaderRight = [
16894
- chalk13.yellow(fmtCost2(costUSD)),
16895
- chalk13.dim(`avg ${fmtCost2(avgPerDay)}/day`),
16896
- cacheHitPct > 0 ? chalk13.dim(`${cacheHitPct}% cache hit`) : null
16897
- ].filter(Boolean).join(chalk13.dim(" \xB7 "));
16898
- console.log("");
16899
- console.log(" " + chalk13.bold("Cost") + " " + costHeaderRight);
16900
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
16901
- if (codexCostUSD > 0)
16902
- costByModel.set(
16903
- "codex (openai)",
16904
- (costByModel.get("codex (openai)") ?? 0) + codexCostUSD
16905
- );
16906
- const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
16907
- const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
16908
- const MODEL_LABEL = 22;
16909
- const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
16910
- for (const [model, cost] of modelList) {
16911
- const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
16912
- const b = colorBar(cost, maxModelCost, MODEL_BAR);
16913
- console.log(
16914
- " " + chalk13.white(label.padEnd(MODEL_LABEL)) + b + " " + chalk13.yellow(fmtCost2(cost))
16915
- );
16916
- }
17639
+ console.log(" " + chalk13.bold("MCP Servers"));
17640
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
17641
+ const maxMcp = Math.max(...mcpMap.values(), 1);
17642
+ for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
17643
+ const label = server.slice(0, LABEL - 1).padEnd(LABEL);
17644
+ const b = colorBar(count, maxMcp, BAR);
17645
+ console.log(" " + chalk13.white(label) + b + " " + chalk13.white(num2(count)));
17646
+ }
17647
+ }
17648
+ if (hourMap.size > 0) {
17649
+ const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
17650
+ const maxHour = Math.max(...hourMap.values(), 1);
17651
+ const bar = Array.from({ length: 24 }, (_, h) => {
17652
+ const v = hourMap.get(h) ?? 0;
17653
+ return BLOCKS[Math.round(v / maxHour * 8)];
17654
+ }).join("");
17655
+ console.log("");
17656
+ console.log(" " + chalk13.bold("Hour of Day") + chalk13.dim(" (local, 0h \u2013 23h)"));
17657
+ console.log(" " + chalk13.cyan(bar));
17658
+ console.log(" " + chalk13.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
17659
+ }
17660
+ if (dailyList.length > 1) {
17661
+ console.log("");
17662
+ console.log(" " + chalk13.bold("Daily Activity"));
17663
+ console.log(" " + chalk13.dim("\u2500".repeat(W - 2)));
17664
+ const DAY_BAR = Math.max(8, Math.min(30, W - 36));
17665
+ for (const [dateKey, { calls, blocked: db }] of dailyList) {
17666
+ const label = fmtDate(dateKey).padEnd(10);
17667
+ const b = colorBar(calls, maxDaily, DAY_BAR);
17668
+ const dayCost = costByDay.get(dateKey);
17669
+ const costNote = dayCost ? chalk13.magenta(` ${fmtCost2(dayCost)}`) : "";
17670
+ const blockNote = db > 0 ? chalk13.red(` ${db} blocked`) : "";
17671
+ console.log(
17672
+ " " + chalk13.dim(label) + " " + b + " " + chalk13.white(num2(calls)) + blockNote + costNote
17673
+ );
16917
17674
  }
16918
- const responseDlpEntries = allEntries.filter((e) => {
16919
- if (e.source !== "response-dlp") return false;
16920
- const ts = new Date(e.ts);
16921
- return ts >= start && ts <= end;
16922
- });
16923
- if (responseDlpEntries.length > 0) {
16924
- console.log("");
17675
+ }
17676
+ const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
17677
+ if (totalTokens > 0) {
17678
+ const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
17679
+ console.log("");
17680
+ console.log(" " + chalk13.bold("Tokens") + " " + chalk13.dim(`${num2(totalTokens)} total`));
17681
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
17682
+ const TOK_BAR = Math.max(6, Math.min(20, W - 30));
17683
+ const TOK_LABEL = 14;
17684
+ const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
17685
+ const nonCacheRows = [
17686
+ ["Input", costInputTokens, chalk13.cyan(num2(costInputTokens))],
17687
+ ["Output", costOutputTokens, chalk13.white(num2(costOutputTokens))],
17688
+ ["Cache write", costCacheWrite, chalk13.yellow(num2(costCacheWrite))]
17689
+ ];
17690
+ for (const [label, count, colored] of nonCacheRows) {
17691
+ if (count === 0) continue;
17692
+ const b = colorBar(count, maxNonCache, TOK_BAR);
17693
+ console.log(" " + chalk13.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
17694
+ }
17695
+ if (costCacheRead > 0) {
17696
+ const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
17697
+ const pct = cacheHitPct > 0 ? chalk13.dim(` ${cacheHitPct}% hit rate`) : "";
16925
17698
  console.log(
16926
- " " + chalk13.red.bold("\u26A0\uFE0F Response DLP") + chalk13.dim(" \xB7 ") + chalk13.red(
16927
- `${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
16928
- )
17699
+ " " + chalk13.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + chalk13.green(num2(costCacheRead)) + pct
16929
17700
  );
16930
- console.log(" " + chalk13.dim("\u2500".repeat(Math.min(60, W - 4))));
17701
+ }
17702
+ }
17703
+ if (costUSD > 0) {
17704
+ const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
17705
+ const avgPerDay = costUSD / periodDays;
17706
+ const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
17707
+ const costHeaderRight = [
17708
+ chalk13.yellow(fmtCost2(costUSD)),
17709
+ chalk13.dim(`avg ${fmtCost2(avgPerDay)}/day`),
17710
+ cacheHitPct > 0 ? chalk13.dim(`${cacheHitPct}% cache hit`) : null
17711
+ ].filter(Boolean).join(chalk13.dim(" \xB7 "));
17712
+ console.log("");
17713
+ console.log(" " + chalk13.bold("Cost") + " " + costHeaderRight);
17714
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(50, W - 4))));
17715
+ if (codexUSD > 0)
17716
+ costByModel.set("codex (openai)", (costByModel.get("codex (openai)") ?? 0) + codexUSD);
17717
+ if (geminiUSD > 0)
17718
+ costByModel.set("gemini (google)", (costByModel.get("gemini (google)") ?? 0) + geminiUSD);
17719
+ const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
17720
+ const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
17721
+ const MODEL_LABEL = 22;
17722
+ const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
17723
+ for (const [model, cost] of modelList) {
17724
+ const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
17725
+ const b = colorBar(cost, maxModelCost, MODEL_BAR);
16931
17726
  console.log(
16932
- " " + chalk13.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
17727
+ " " + chalk13.white(label.padEnd(MODEL_LABEL)) + b + " " + chalk13.yellow(fmtCost2(cost))
16933
17728
  );
16934
- console.log(" " + chalk13.yellow("Rotate affected keys immediately."));
16935
- for (const e of responseDlpEntries.slice(0, 5)) {
16936
- const ts = chalk13.dim(fmtDate(e.ts) + " ");
16937
- const pattern = chalk13.red(e.dlpPattern ?? "DLP");
16938
- const sample = chalk13.gray(e.dlpSample ?? "");
16939
- console.log(` ${ts}${pattern} ${sample}`);
16940
- }
16941
- if (responseDlpEntries.length > 5) {
16942
- console.log(chalk13.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
16943
- }
16944
17729
  }
17730
+ }
17731
+ if (responseDlpEntries.length > 0) {
16945
17732
  console.log("");
16946
17733
  console.log(
16947
- " " + chalk13.dim("node9 audit --deny") + chalk13.dim(" \xB7 ") + chalk13.dim("node9 report --period today|7d|30d|month --no-tests")
17734
+ " " + chalk13.red.bold("\u26A0\uFE0F Response DLP") + chalk13.dim(" \xB7 ") + chalk13.red(
17735
+ `${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
17736
+ )
16948
17737
  );
16949
- console.log("");
16950
- });
17738
+ console.log(" " + chalk13.dim("\u2500".repeat(Math.min(60, W - 4))));
17739
+ console.log(
17740
+ " " + chalk13.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
17741
+ );
17742
+ console.log(" " + chalk13.yellow("Rotate affected keys immediately."));
17743
+ for (const e of responseDlpEntries.slice(0, 5)) {
17744
+ const ts = chalk13.dim(fmtDate(e.ts) + " ");
17745
+ const pattern = chalk13.red(e.dlpPattern ?? "DLP");
17746
+ const sample = chalk13.gray(e.dlpSample ?? "");
17747
+ console.log(` ${ts}${pattern} ${sample}`);
17748
+ }
17749
+ if (responseDlpEntries.length > 5) {
17750
+ console.log(chalk13.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
17751
+ }
17752
+ }
17753
+ console.log("");
17754
+ console.log(
17755
+ " " + chalk13.dim("node9 audit --deny") + chalk13.dim(" \xB7 ") + chalk13.dim("node9 report --period today|7d|30d|month --no-tests")
17756
+ );
17757
+ console.log("");
16951
17758
  }
16952
17759
 
16953
17760
  // src/cli/commands/daemon-cmd.ts
@@ -20757,6 +21564,17 @@ program.command("tail").description("Stream live agent activity to the terminal"
20757
21564
  process.exit(1);
20758
21565
  }
20759
21566
  });
21567
+ program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
21568
+ try {
21569
+ const dashboardPath = path49.join(__dirname, "dashboard.mjs");
21570
+ const dynamicImport = new Function("id", "return import(id)");
21571
+ const mod = await dynamicImport(`file://${dashboardPath}`);
21572
+ await mod.startMonitor();
21573
+ } catch (err2) {
21574
+ console.error(chalk31.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
21575
+ process.exit(1);
21576
+ }
21577
+ });
20760
21578
  registerWatchCommand(program);
20761
21579
  registerMcpGatewayCommand(program);
20762
21580
  registerMcpServerCommand(program);