@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.js CHANGED
@@ -118,12 +118,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
118
118
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
119
119
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
120
120
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
121
+ const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
121
122
  appendToLog(LOCAL_AUDIT_LOG, {
122
123
  ts: (/* @__PURE__ */ new Date()).toISOString(),
123
124
  tool: toolName,
124
125
  ...argsField,
125
126
  decision,
126
127
  checkedBy,
128
+ ...ruleNameField,
127
129
  ...testRun,
128
130
  agent: meta?.agent,
129
131
  mcpServer: meta?.mcpServer,
@@ -707,7 +709,12 @@ function analyzeFsOperationImpl(command) {
707
709
  for (const p of paths) {
708
710
  for (const sp of SENSITIVE_PATH_RULES) {
709
711
  if (sp.match(p)) {
710
- result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
712
+ result = {
713
+ ruleName: sp.rule,
714
+ verdict: sp.verdict ?? "block",
715
+ reason: sp.reason,
716
+ path: p
717
+ };
711
718
  return false;
712
719
  }
713
720
  }
@@ -1708,7 +1715,11 @@ function extractCanonicalFindings(call, ctx) {
1708
1715
  })
1709
1716
  );
1710
1717
  }
1711
- if (PRIVILEGE_ESCALATION_RE.test(command)) {
1718
+ const ast = analyzeShellCommand(command);
1719
+ const sudoVariant = ast.actions.includes("sudo") || ast.actions.includes("su");
1720
+ const chmodVariant = ast.actions.includes("chmod") && (ast.allTokens.includes("777") || ast.allTokens.includes("0777") || ast.allTokens.includes("+x"));
1721
+ const chownVariant = ast.actions.includes("chown") && ast.allTokens.includes("root");
1722
+ if (sudoVariant || chmodVariant || chownVariant) {
1712
1723
  out.push(
1713
1724
  makeFinding({
1714
1725
  type: "privilege-escalation",
@@ -1840,7 +1851,7 @@ function* stringValues(obj, depth = 0) {
1840
1851
  }
1841
1852
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
1842
1853
  }
1843
- var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto2, 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;
1854
+ var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto2, 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;
1844
1855
  var init_dist = __esm({
1845
1856
  "packages/policy-engine/dist/index.mjs"() {
1846
1857
  "use strict";
@@ -2363,15 +2374,50 @@ var init_dist = __esm({
2363
2374
  match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
2364
2375
  },
2365
2376
  {
2377
+ // Mirrors the JSON shield's `.env` pattern (project-jail.json's
2378
+ // review-read-env-any-tool) so the AST FS-op path catches the
2379
+ // same set the regex shield does — including Next.js / Vite's
2380
+ // `.env.<env>.local` double-suffix overrides which are commonly
2381
+ // gitignored AND commonly contain real secrets.
2382
+ //
2383
+ // Intentional non-matches (dev fixtures): .env.example, .env.sample,
2384
+ // .env.template, .env.test, .envrc. See shields.test.ts:983-995
2385
+ // for the canonical test-asserted contract.
2366
2386
  rule: "shield:project-jail:block-read-env",
2367
2387
  reason: "Reading .env files is blocked by project-jail shield",
2368
- match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
2388
+ match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
2389
+ p
2390
+ )
2369
2391
  },
2370
2392
  {
2371
- rule: "shield:project-jail:block-read-credentials",
2372
- reason: "Reading credential files is blocked by project-jail shield",
2373
- match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
2374
- p
2393
+ // verdict: 'review' (not 'block') is a deliberate design choice
2394
+ // documented in commit 29327a8. SSH keys and AWS credentials are
2395
+ // cryptographic material with no legitimate read use-case for
2396
+ // an AI agent → hard `block`. But .netrc / .npmrc / .docker /
2397
+ // .kube / gcloud are CONFIG files that hold tokens AND have
2398
+ // legitimate diagnostic reads ("which registry am I configured
2399
+ // for", "what cluster am I on"). Hard-blocking those creates
2400
+ // friction without much safety win because the review gate
2401
+ // still catches genuine exfiltration attempts.
2402
+ //
2403
+ // The review gate FAILS CLOSED on timeout (daemon.approvalTimeoutMs
2404
+ // returns a deny verdict via the orchestrator's timeout branch),
2405
+ // so a stuck or unattended approval does NOT silently grant
2406
+ // credential access. If the threat model demands strict block,
2407
+ // a future per-shield strict-mode toggle is the right fix —
2408
+ // not a regex-level upgrade here.
2409
+ rule: "shield:project-jail:review-read-credentials",
2410
+ reason: "Reading credential files requires approval (project-jail shield)",
2411
+ verdict: "review",
2412
+ match: (p) => (
2413
+ // .kube/config holds Kubernetes cluster credentials and was
2414
+ // flagged as missing by the node9-pr-agent review (the comment
2415
+ // above mentioned .kube but the regex didn't include it — a
2416
+ // textbook code-comment vs code drift). The JSON shield's
2417
+ // review-read-credentials-any-tool already had it. Now aligned.
2418
+ /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials|\.kube[\\/]config)$/i.test(
2419
+ p
2420
+ )
2375
2421
  )
2376
2422
  }
2377
2423
  ];
@@ -2387,7 +2433,7 @@ var init_dist = __esm({
2387
2433
  "shield:project-jail:block-read-ssh",
2388
2434
  "shield:project-jail:block-read-aws",
2389
2435
  "shield:project-jail:block-read-env",
2390
- "shield:project-jail:block-read-credentials"
2436
+ "shield:project-jail:review-read-credentials"
2391
2437
  ]);
2392
2438
  FS_OP_CACHE_MAX = 5e3;
2393
2439
  fsOpCache = /* @__PURE__ */ new Map();
@@ -3075,7 +3121,7 @@ var init_dist = __esm({
3075
3121
  {
3076
3122
  field: "command",
3077
3123
  op: "matches",
3078
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.ssh[\\/\\\\]",
3124
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.ssh[\\/\\\\]",
3079
3125
  flags: "i"
3080
3126
  }
3081
3127
  ],
@@ -3089,7 +3135,7 @@ var init_dist = __esm({
3089
3135
  {
3090
3136
  field: "command",
3091
3137
  op: "matches",
3092
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*[\\/\\\\]\\.aws[\\/\\\\]",
3138
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\\s+.*?\\.aws[\\/\\\\]",
3093
3139
  flags: "i"
3094
3140
  }
3095
3141
  ],
@@ -3111,7 +3157,7 @@ var init_dist = __esm({
3111
3157
  reason: "Reading .env files is blocked by project-jail shield"
3112
3158
  },
3113
3159
  {
3114
- name: "shield:project-jail:block-read-credentials",
3160
+ name: "shield:project-jail:review-read-credentials",
3115
3161
  tool: "bash",
3116
3162
  conditions: [
3117
3163
  {
@@ -3121,8 +3167,64 @@ var init_dist = __esm({
3121
3167
  flags: "i"
3122
3168
  }
3123
3169
  ],
3170
+ verdict: "review",
3171
+ reason: "Reading credential files requires approval (project-jail shield)"
3172
+ },
3173
+ {
3174
+ name: "shield:project-jail:block-read-ssh-any-tool",
3175
+ tool: "*",
3176
+ conditions: [
3177
+ {
3178
+ field: "file_path",
3179
+ op: "matches",
3180
+ value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
3181
+ flags: "i"
3182
+ }
3183
+ ],
3184
+ verdict: "block",
3185
+ reason: "Reading SSH private keys is blocked by project-jail shield"
3186
+ },
3187
+ {
3188
+ name: "shield:project-jail:block-read-aws-any-tool",
3189
+ tool: "*",
3190
+ conditions: [
3191
+ {
3192
+ field: "file_path",
3193
+ op: "matches",
3194
+ value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
3195
+ flags: "i"
3196
+ }
3197
+ ],
3124
3198
  verdict: "block",
3125
- reason: "Reading credential files is blocked by project-jail shield"
3199
+ reason: "Reading AWS credentials is blocked by project-jail shield"
3200
+ },
3201
+ {
3202
+ name: "shield:project-jail:review-read-env-any-tool",
3203
+ tool: "*",
3204
+ conditions: [
3205
+ {
3206
+ field: "file_path",
3207
+ op: "matches",
3208
+ value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
3209
+ flags: "i"
3210
+ }
3211
+ ],
3212
+ verdict: "review",
3213
+ reason: "Reading .env files requires approval (project-jail shield)"
3214
+ },
3215
+ {
3216
+ name: "shield:project-jail:review-read-credentials-any-tool",
3217
+ tool: "*",
3218
+ conditions: [
3219
+ {
3220
+ field: "file_path",
3221
+ op: "matches",
3222
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
3223
+ flags: "i"
3224
+ }
3225
+ ],
3226
+ verdict: "review",
3227
+ reason: "Reading credential files requires approval (project-jail shield)"
3126
3228
  }
3127
3229
  ],
3128
3230
  dangerousWords: []
@@ -3246,7 +3348,6 @@ var init_dist = __esm({
3246
3348
  LOOP_THRESHOLD_FOR_WASTE = 3;
3247
3349
  COST_PER_LOOP_ITER_USD = 6e-3;
3248
3350
  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;
3249
- PRIVILEGE_ESCALATION_RE = /\b(sudo|su)\b\s+[a-z]|\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
3250
3351
  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;
3251
3352
  FILE_TOOLS = /* @__PURE__ */ new Set([
3252
3353
  "read",
@@ -3266,7 +3367,7 @@ var init_dist = __esm({
3266
3367
  PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
3267
3368
  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/;
3268
3369
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
3269
- CANONICAL_EXTRACTOR_VERSION = "canonical-v1";
3370
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v4";
3270
3371
  DEDUPE_PREVIEW_LEN = 120;
3271
3372
  TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
3272
3373
  /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
@@ -4807,7 +4908,7 @@ async function waitForDaemonDecision(id, signal) {
4807
4908
  if (signal) signal.removeEventListener("abort", onAbort);
4808
4909
  }
4809
4910
  }
4810
- async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
4911
+ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent) {
4811
4912
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
4812
4913
  const res = await fetch(`${base}/check`, {
4813
4914
  method: "POST",
@@ -4818,7 +4919,12 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
4818
4919
  slackDelegated: true,
4819
4920
  agent: meta?.agent,
4820
4921
  mcpServer: meta?.mcpServer,
4821
- ...riskMetadata && { riskMetadata }
4922
+ ...riskMetadata && { riskMetadata },
4923
+ // fromCLI=true tells the daemon the CLI already sent the activity
4924
+ // event via socket. Same contract as registerDaemonEntry — without
4925
+ // it the daemon double-emits 'activity' for cloud-enforced flows.
4926
+ fromCLI: socketActivitySent !== false,
4927
+ activityId
4822
4928
  }),
4823
4929
  signal: AbortSignal.timeout(3e3)
4824
4930
  });
@@ -5785,7 +5891,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5785
5891
  args,
5786
5892
  "deny",
5787
5893
  "smart-rule-block-override",
5788
- meta,
5894
+ // Same rationale as the smart-rule-block path above —
5895
+ // pass the specific rule name so [2] SHIELDS can
5896
+ // attribute this override-block to its owning shield.
5897
+ { ...meta, ruleName: policyResult.ruleName },
5789
5898
  hashAuditArgs
5790
5899
  );
5791
5900
  if (approvers.cloud && creds?.apiKey)
@@ -5815,7 +5924,20 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5815
5924
  }
5816
5925
  } else {
5817
5926
  if (!isManual)
5818
- appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
5927
+ appendLocalAudit(
5928
+ toolName,
5929
+ args,
5930
+ "deny",
5931
+ "smart-rule-block",
5932
+ // Include policyResult.ruleName so the [2] Report SHIELDS
5933
+ // panel can attribute this block to its specific shield
5934
+ // (e.g. `shield:project-jail:block-read-ssh`) via the
5935
+ // rule→shield map. checkedBy stays as the generic
5936
+ // `smart-rule-block` for backward compat with existing
5937
+ // log readers.
5938
+ { ...meta, ruleName: policyResult.ruleName },
5939
+ hashAuditArgs
5940
+ );
5819
5941
  if (approvers.cloud && creds?.apiKey)
5820
5942
  auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
5821
5943
  ruleName: policyResult.ruleName,
@@ -5963,7 +6085,14 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
5963
6085
  let daemonAllowCount = 1;
5964
6086
  if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
5965
6087
  if (cloudEnforced && cloudRequestId) {
5966
- const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
6088
+ const viewer = await notifyDaemonViewer(
6089
+ toolName,
6090
+ args,
6091
+ meta,
6092
+ riskMetadata,
6093
+ options?.activityId,
6094
+ options?.socketActivitySent
6095
+ ).catch(() => null);
5967
6096
  viewerId = viewer?.id ?? null;
5968
6097
  daemonEntryId = viewerId;
5969
6098
  if (viewer) daemonAllowCount = viewer.allowCount;
@@ -7630,11 +7759,62 @@ function computeLoopWaste(loops, totalToolCalls) {
7630
7759
  const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
7631
7760
  return { wastedCalls, wastePct };
7632
7761
  }
7633
- var import_chalk3;
7762
+ function rollupByShield(sections, topRulesPerShield = 3) {
7763
+ const out = [];
7764
+ for (const section of sections) {
7765
+ if (section.sourceType !== "shield") continue;
7766
+ if (!section.shieldKey) continue;
7767
+ const totalCatches = section.blockedCount + section.reviewCount;
7768
+ const topRuleLabels = [...section.rules].sort((a, b) => b.findings.length - a.findings.length).slice(0, topRulesPerShield).map((r) => r.findings.length > 1 ? `${r.name} \xD7${r.findings.length}` : r.name);
7769
+ out.push({
7770
+ shieldName: section.shieldKey,
7771
+ totalCatches,
7772
+ blockCatches: section.blockedCount,
7773
+ reviewCatches: section.reviewCount,
7774
+ topRuleLabels
7775
+ });
7776
+ }
7777
+ return out.sort((a, b) => b.totalCatches - a.totalCatches);
7778
+ }
7779
+ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
7780
+ const inner = width - 4;
7781
+ const out = [];
7782
+ const titlePad = ` ${title} `;
7783
+ const titleSegment = titlePad.length <= inner ? titlePad : titlePad.slice(0, inner);
7784
+ const dashFill = "\u2500".repeat(Math.max(0, inner - titleSegment.length));
7785
+ out.push(import_chalk3.default.dim("\u256D\u2500") + import_chalk3.default.bold(titleSegment) + import_chalk3.default.dim(`${dashFill}\u2500\u256E`));
7786
+ for (const line of bodyLines) {
7787
+ const padding = " ".repeat(Math.max(0, inner - line.width));
7788
+ out.push(import_chalk3.default.dim("\u2502 ") + line.rendered + padding + import_chalk3.default.dim(" \u2502"));
7789
+ }
7790
+ out.push(import_chalk3.default.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
7791
+ return out;
7792
+ }
7793
+ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
7794
+ const t = new Date(timestamp).getTime();
7795
+ if (Number.isNaN(t)) return "?";
7796
+ const days = Math.floor((now.getTime() - t) / 864e5);
7797
+ if (days < 1) return "today";
7798
+ if (days > 90) return "90d+";
7799
+ return `${days}d`;
7800
+ }
7801
+ var import_chalk3, PANEL_WIDTH;
7634
7802
  var init_scan_derive = __esm({
7635
7803
  "src/cli/render/scan-derive.ts"() {
7636
7804
  "use strict";
7637
7805
  import_chalk3 = __toESM(require("chalk"));
7806
+ PANEL_WIDTH = 76;
7807
+ }
7808
+ });
7809
+
7810
+ // src/protection.ts
7811
+ var PROTECTIVE_SHIELD_DISCOUNTS;
7812
+ var init_protection = __esm({
7813
+ "src/protection.ts"() {
7814
+ "use strict";
7815
+ PROTECTIVE_SHIELD_DISCOUNTS = {
7816
+ "project-jail": 0.7
7817
+ };
7638
7818
  }
7639
7819
  });
7640
7820
 
@@ -7821,6 +8001,7 @@ async function ensurePricingLoaded() {
7821
8001
  if (fromDisk && Object.keys(fromDisk).length > 0) {
7822
8002
  memCache = fromDisk;
7823
8003
  memCacheAt = Date.now();
8004
+ lookupCache.clear();
7824
8005
  return;
7825
8006
  }
7826
8007
  const fetched = await fetchLiteLLMPricing();
@@ -7828,30 +8009,42 @@ async function ensurePricingLoaded() {
7828
8009
  memCache = fetched;
7829
8010
  memCacheAt = Date.now();
7830
8011
  writeCache(fetched);
8012
+ lookupCache.clear();
7831
8013
  return;
7832
8014
  }
7833
8015
  memCache = { ...BUNDLED_PRICING };
7834
8016
  memCacheAt = Date.now();
8017
+ lookupCache.clear();
7835
8018
  }
7836
8019
  function pricingFor(model) {
7837
8020
  const norm = normalizeModel(model);
8021
+ const cached = lookupCache.get(norm);
8022
+ if (cached !== void 0) return cached;
7838
8023
  const sources = [];
7839
8024
  if (memCache) sources.push(memCache);
7840
8025
  sources.push(BUNDLED_PRICING);
8026
+ let resolved = null;
7841
8027
  for (const source of sources) {
7842
8028
  const exact = source[norm];
7843
- if (exact) return exact;
8029
+ if (exact) {
8030
+ resolved = exact;
8031
+ break;
8032
+ }
7844
8033
  let best = null;
7845
8034
  for (const key of Object.keys(source)) {
7846
8035
  if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
7847
8036
  best = key;
7848
8037
  }
7849
8038
  }
7850
- if (best) return source[best];
8039
+ if (best) {
8040
+ resolved = source[best];
8041
+ break;
8042
+ }
7851
8043
  }
7852
- return null;
8044
+ lookupCache.set(norm, resolved);
8045
+ return resolved;
7853
8046
  }
7854
- var import_fs15, import_path17, import_os14, LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt;
8047
+ var import_fs15, import_path17, import_os14, LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, lookupCache;
7855
8048
  var init_litellm = __esm({
7856
8049
  "src/pricing/litellm.ts"() {
7857
8050
  "use strict";
@@ -7888,6 +8081,7 @@ var init_litellm = __esm({
7888
8081
  TTL_MS = 24 * 60 * 60 * 1e3;
7889
8082
  memCache = null;
7890
8083
  memCacheAt = 0;
8084
+ lookupCache = /* @__PURE__ */ new Map();
7891
8085
  }
7892
8086
  });
7893
8087
 
@@ -7954,7 +8148,7 @@ function parseJSONLFile(filePath, fallbackWorkingDir) {
7954
8148
  }
7955
8149
  return daily;
7956
8150
  }
7957
- function collectEntries() {
8151
+ function collectEntries(sinceMs) {
7958
8152
  const projectsDir = import_path18.default.join(import_os15.default.homedir(), ".claude", "projects");
7959
8153
  if (!import_fs16.default.existsSync(projectsDir)) return [];
7960
8154
  const combined = /* @__PURE__ */ new Map();
@@ -7979,7 +8173,15 @@ function collectEntries() {
7979
8173
  }
7980
8174
  const fallbackWorkingDir = decodeProjectDirName(dir);
7981
8175
  for (const file of files) {
7982
- const entries = parseJSONLFile(import_path18.default.join(dirPath, file), fallbackWorkingDir);
8176
+ const filePath = import_path18.default.join(dirPath, file);
8177
+ if (sinceMs !== void 0) {
8178
+ try {
8179
+ if (import_fs16.default.statSync(filePath).mtimeMs < sinceMs) continue;
8180
+ } catch {
8181
+ continue;
8182
+ }
8183
+ }
8184
+ const entries = parseJSONLFile(filePath, fallbackWorkingDir);
7983
8185
  for (const [key, e] of entries) {
7984
8186
  const prev = combined.get(key);
7985
8187
  if (prev) {
@@ -8056,6 +8258,7 @@ __export(scan_watermark_exports, {
8056
8258
  markUploadComplete: () => markUploadComplete,
8057
8259
  saveWatermark: () => saveWatermark,
8058
8260
  scanDelta: () => scanDelta,
8261
+ tickForensicBroadcast: () => tickForensicBroadcast,
8059
8262
  tickScanWatcher: () => tickScanWatcher
8060
8263
  });
8061
8264
  function freshWatermark() {
@@ -8259,6 +8462,25 @@ function extractFindingsFromLine(line, sessionId, lineIndex) {
8259
8462
  }
8260
8463
  return findings;
8261
8464
  }
8465
+ async function tickForensicBroadcast(offsets) {
8466
+ const out = [];
8467
+ const files = listJsonlFiles();
8468
+ for (const file of files) {
8469
+ const size = fileSize(file);
8470
+ const offset = offsets.get(file);
8471
+ if (offset === void 0) {
8472
+ offsets.set(file, size);
8473
+ continue;
8474
+ }
8475
+ if (size <= offset) continue;
8476
+ const sessionId = import_path19.default.basename(file, ".jsonl");
8477
+ const newOffset = await scanDelta(file, offset, (obj, lineIndex) => {
8478
+ out.push(...extractFindingsFromLine(obj, sessionId, lineIndex));
8479
+ });
8480
+ offsets.set(file, newOffset);
8481
+ }
8482
+ return out;
8483
+ }
8262
8484
  function markUploadComplete() {
8263
8485
  const state = loadWatermark();
8264
8486
  if (state.status === "schema-future") return;
@@ -8794,7 +9016,16 @@ function buildRecurringPatternSet(findings) {
8794
9016
  }
8795
9017
  return recurring;
8796
9018
  }
8797
- function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result) {
9019
+ function emptyScanDedup() {
9020
+ return { findingsKeys: /* @__PURE__ */ new Set(), dlpKeys: /* @__PURE__ */ new Set() };
9021
+ }
9022
+ function findingKey(ruleName, inputPreview, projLabel) {
9023
+ return `${ruleName ?? "<unnamed>"}|${inputPreview}|${projLabel}`;
9024
+ }
9025
+ function dlpKey(patternName, redactedSample, projLabel) {
9026
+ return `${patternName}|${redactedSample}|${projLabel}`;
9027
+ }
9028
+ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sessionId, agent, result, dedup) {
8798
9029
  const fsVerdict = analyzeFsOperation(command);
8799
9030
  if (!fsVerdict) return false;
8800
9031
  const synthRule = {
@@ -8817,10 +9048,9 @@ function pushFsOpAstFinding(command, toolName, input, timestamp, projLabel, sess
8817
9048
  rule: synthRule
8818
9049
  };
8819
9050
  const inputPreview = preview(input, 120);
8820
- const isDupe = result.findings.some(
8821
- (f) => f.source.rule.name === synthRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
8822
- );
8823
- if (!isDupe) {
9051
+ const k = findingKey(synthRule.name, inputPreview, projLabel);
9052
+ if (!dedup.findingsKeys.has(k)) {
9053
+ dedup.findingsKeys.add(k);
8824
9054
  result.findings.push({
8825
9055
  source: synthSource,
8826
9056
  toolName,
@@ -8905,22 +9135,15 @@ function buildRuleSources() {
8905
9135
  sources.push({ shieldName, shieldLabel: shieldName, sourceType: "shield", rule });
8906
9136
  }
8907
9137
  }
8908
- try {
8909
- const config = getConfig();
8910
- for (const rule of config.policy.smartRules) {
8911
- if (!rule.name) continue;
8912
- if (rule.name.startsWith("shield:")) continue;
8913
- const isCloud = rule.name.startsWith("cloud:");
8914
- const isDefault = DEFAULT_RULE_NAMES.has(rule.name);
8915
- const sourceType = isCloud ? "user" : isDefault ? "default" : "user";
8916
- sources.push({
8917
- shieldName: isCloud ? "cloud" : isDefault ? "default" : "custom",
8918
- shieldLabel: isCloud ? "Cloud Policy" : isDefault ? "Default Rules" : "Your Rules",
8919
- sourceType,
8920
- rule
8921
- });
8922
- }
8923
- } catch {
9138
+ for (const rule of DEFAULT_CONFIG.policy.smartRules) {
9139
+ if (!rule.name) continue;
9140
+ if (rule.name.startsWith("shield:")) continue;
9141
+ sources.push({
9142
+ shieldName: "default",
9143
+ shieldLabel: "Default Rules",
9144
+ sourceType: "default",
9145
+ rule
9146
+ });
8924
9147
  }
8925
9148
  return sources;
8926
9149
  }
@@ -9006,178 +9229,53 @@ function renderProgressBar(done, total, lines) {
9006
9229
  `\r ${import_chalk5.default.cyan("Scanning")} [${import_chalk5.default.cyan(bar)}] ${import_chalk5.default.dim(fileLabel)}${lineLabel} `
9007
9230
  );
9008
9231
  }
9009
- function scanClaudeHistory(startDate, onProgress, onLine) {
9010
- const projectsDir = import_path21.default.join(import_os18.default.homedir(), ".claude", "projects");
9011
- const result = {
9012
- filesScanned: 0,
9013
- sessions: 0,
9014
- totalToolCalls: 0,
9015
- bashCalls: 0,
9016
- findings: [],
9017
- dlpFindings: [],
9018
- loopFindings: [],
9019
- totalCostUSD: 0,
9020
- firstDate: null,
9021
- lastDate: null,
9022
- sessionsWithEarlySecrets: 0
9023
- };
9024
- if (!import_fs19.default.existsSync(projectsDir)) return result;
9025
- let projDirs;
9232
+ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, result, dedup, onProgress, onLine) {
9233
+ result.filesScanned++;
9234
+ result.sessions++;
9235
+ onProgress?.(result.filesScanned);
9236
+ const sessionId = file.replace(/\.jsonl$/, "");
9237
+ let raw;
9026
9238
  try {
9027
- projDirs = import_fs19.default.readdirSync(projectsDir);
9239
+ raw = import_fs19.default.readFileSync(import_path21.default.join(projPath, file), "utf-8");
9028
9240
  } catch {
9029
- return result;
9241
+ return;
9030
9242
  }
9031
- const ruleSources = buildRuleSources();
9032
- for (const proj of projDirs) {
9033
- const projPath = import_path21.default.join(projectsDir, proj);
9243
+ const sessionCalls = [];
9244
+ const toolUseFilePaths = /* @__PURE__ */ new Map();
9245
+ let firstDlpTs = null;
9246
+ let firstEditTs = null;
9247
+ for (const line of raw.split("\n")) {
9248
+ if (!line.trim()) continue;
9249
+ onLine?.();
9250
+ let entry;
9034
9251
  try {
9035
- if (!import_fs19.default.statSync(projPath).isDirectory()) continue;
9252
+ entry = JSON.parse(line);
9036
9253
  } catch {
9037
9254
  continue;
9038
9255
  }
9039
- const projLabel = stripTerminalEscapes(
9040
- decodeURIComponent(proj).replace(import_os18.default.homedir(), "~")
9041
- ).slice(0, 40);
9042
- let files;
9043
- try {
9044
- files = import_fs19.default.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
9045
- } catch {
9046
- continue;
9256
+ if (entry.type !== "assistant" && entry.type !== "user") continue;
9257
+ if (startDate && entry.timestamp) {
9258
+ if (new Date(entry.timestamp) < startDate) continue;
9047
9259
  }
9048
- for (const file of files) {
9049
- result.filesScanned++;
9050
- result.sessions++;
9051
- onProgress?.(result.filesScanned);
9052
- const sessionId = file.replace(/\.jsonl$/, "");
9053
- let raw;
9054
- try {
9055
- raw = import_fs19.default.readFileSync(import_path21.default.join(projPath, file), "utf-8");
9056
- } catch {
9057
- continue;
9058
- }
9059
- const sessionCalls = [];
9060
- const toolUseFilePaths = /* @__PURE__ */ new Map();
9061
- let firstDlpTs = null;
9062
- let firstEditTs = null;
9063
- for (const line of raw.split("\n")) {
9064
- if (!line.trim()) continue;
9065
- onLine?.();
9066
- let entry;
9067
- try {
9068
- entry = JSON.parse(line);
9069
- } catch {
9070
- continue;
9071
- }
9072
- if (entry.type !== "assistant" && entry.type !== "user") continue;
9073
- if (startDate && entry.timestamp) {
9074
- if (new Date(entry.timestamp) < startDate) continue;
9075
- }
9076
- if (entry.timestamp) {
9077
- if (!result.firstDate || entry.timestamp < result.firstDate)
9078
- result.firstDate = entry.timestamp;
9079
- if (!result.lastDate || entry.timestamp > result.lastDate)
9080
- result.lastDate = entry.timestamp;
9081
- }
9082
- if (entry.type === "user") {
9083
- const content2 = entry.message?.content;
9084
- if (Array.isArray(content2)) {
9085
- const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
9086
- if (text) {
9087
- const dlpMatch = scanArgs({ text });
9088
- if (dlpMatch) {
9089
- const isDupe = result.dlpFindings.some(
9090
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9091
- );
9092
- if (!isDupe) {
9093
- result.dlpFindings.push({
9094
- patternName: dlpMatch.patternName,
9095
- redactedSample: dlpMatch.redactedSample,
9096
- toolName: "user-prompt",
9097
- timestamp: entry.timestamp ?? "",
9098
- project: projLabel,
9099
- sessionId,
9100
- agent: "claude"
9101
- });
9102
- }
9103
- }
9104
- }
9105
- for (const block of content2) {
9106
- if (block.type !== "tool_result") continue;
9107
- const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
9108
- if (filePath) {
9109
- const ext = import_path21.default.extname(filePath).toLowerCase();
9110
- if (CODE_EXTENSIONS.has(ext)) continue;
9111
- }
9112
- const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
9113
- if (!resultText) continue;
9114
- if (isNode9SelfOutput(resultText)) continue;
9115
- const dlpMatch = scanArgs({ text: resultText });
9116
- if (dlpMatch) {
9117
- if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
9118
- if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9119
- const isDupe = result.dlpFindings.some(
9120
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9121
- );
9122
- if (!isDupe) {
9123
- result.dlpFindings.push({
9124
- patternName: dlpMatch.patternName,
9125
- redactedSample: dlpMatch.redactedSample,
9126
- toolName: "tool-result",
9127
- timestamp: entry.timestamp ?? "",
9128
- project: projLabel,
9129
- sessionId,
9130
- agent: "claude"
9131
- });
9132
- }
9133
- }
9134
- }
9135
- }
9136
- continue;
9137
- }
9138
- const usage = entry.message?.usage;
9139
- const model = entry.message?.model;
9140
- if (usage && model) {
9141
- const p = claudeModelPrice(model);
9142
- if (p) {
9143
- 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;
9144
- }
9145
- }
9146
- const content = entry.message?.content;
9147
- if (!Array.isArray(content)) continue;
9148
- for (const block of content) {
9149
- if (block.type !== "tool_use") continue;
9150
- result.totalToolCalls++;
9151
- const toolName = block.name ?? "";
9152
- const toolNameLower = toolName.toLowerCase();
9153
- const input = block.input ?? {};
9154
- if (block.id && typeof input.file_path === "string") {
9155
- toolUseFilePaths.set(block.id, input.file_path);
9156
- }
9157
- sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
9158
- if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
9159
- result.bashCalls++;
9160
- }
9161
- if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
9162
- firstEditTs = entry.timestamp ?? null;
9163
- }
9164
- const rawCmd = String(input.command ?? "").trimStart();
9165
- if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
9166
- continue;
9167
- const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
9168
- const inputFileExt = inputFilePath ? import_path21.default.extname(inputFilePath).toLowerCase() : "";
9169
- if (CODE_EXTENSIONS.has(inputFileExt)) continue;
9170
- const dlpMatch = scanArgs(input);
9260
+ if (entry.timestamp) {
9261
+ if (!result.firstDate || entry.timestamp < result.firstDate)
9262
+ result.firstDate = entry.timestamp;
9263
+ if (!result.lastDate || entry.timestamp > result.lastDate) result.lastDate = entry.timestamp;
9264
+ }
9265
+ if (entry.type === "user") {
9266
+ const content2 = entry.message?.content;
9267
+ if (Array.isArray(content2)) {
9268
+ const text = content2.filter((b) => b.type === "text").map((b) => b["text"] ?? "").join("\n");
9269
+ if (text) {
9270
+ const dlpMatch = scanArgs({ text });
9171
9271
  if (dlpMatch) {
9172
- if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9173
- const isDupe = result.dlpFindings.some(
9174
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9175
- );
9176
- if (!isDupe) {
9272
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9273
+ if (!dedup.dlpKeys.has(k)) {
9274
+ dedup.dlpKeys.add(k);
9177
9275
  result.dlpFindings.push({
9178
9276
  patternName: dlpMatch.patternName,
9179
9277
  redactedSample: dlpMatch.redactedSample,
9180
- toolName,
9278
+ toolName: "user-prompt",
9181
9279
  timestamp: entry.timestamp ?? "",
9182
9280
  project: projLabel,
9183
9281
  sessionId,
@@ -9185,85 +9283,234 @@ function scanClaudeHistory(startDate, onProgress, onLine) {
9185
9283
  });
9186
9284
  }
9187
9285
  }
9188
- let astFsMatched = false;
9189
- const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
9190
- if (astRanForBash) {
9191
- astFsMatched = pushFsOpAstFinding(
9192
- String(input.command ?? ""),
9193
- toolName,
9194
- input,
9195
- entry.timestamp ?? "",
9196
- projLabel,
9197
- sessionId,
9198
- "claude",
9199
- result
9200
- );
9286
+ }
9287
+ for (const block of content2) {
9288
+ if (block.type !== "tool_result") continue;
9289
+ const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
9290
+ if (filePath) {
9291
+ const ext = import_path21.default.extname(filePath).toLowerCase();
9292
+ if (CODE_EXTENSIONS.has(ext)) continue;
9201
9293
  }
9202
- let ruleMatched = astFsMatched;
9203
- for (const source of ruleSources) {
9204
- const { rule } = source;
9205
- if (rule.verdict === "allow") continue;
9206
- if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
9207
- if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9208
- if (!evaluateSmartConditions(input, rule)) continue;
9209
- const inputPreview = preview(input, 120);
9210
- const isDupe = result.findings.some(
9211
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9212
- );
9213
- if (!isDupe) {
9214
- result.findings.push({
9215
- source,
9216
- toolName,
9217
- input,
9294
+ const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
9295
+ if (!resultText) continue;
9296
+ if (isNode9SelfOutput(resultText)) continue;
9297
+ const dlpMatch = scanArgs({ text: resultText });
9298
+ if (dlpMatch) {
9299
+ if (looksLikeFixtureToken(dlpMatch.redactedSample)) continue;
9300
+ if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9301
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9302
+ if (!dedup.dlpKeys.has(k)) {
9303
+ dedup.dlpKeys.add(k);
9304
+ result.dlpFindings.push({
9305
+ patternName: dlpMatch.patternName,
9306
+ redactedSample: dlpMatch.redactedSample,
9307
+ toolName: "tool-result",
9218
9308
  timestamp: entry.timestamp ?? "",
9219
9309
  project: projLabel,
9220
9310
  sessionId,
9221
9311
  agent: "claude"
9222
9312
  });
9223
9313
  }
9224
- ruleMatched = true;
9225
- break;
9226
- }
9227
- if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
9228
- const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
9229
- if (shellVerdict) {
9230
- const astRule = {
9231
- name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
9232
- tool: "bash",
9233
- conditions: [],
9234
- verdict: shellVerdict,
9235
- reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9236
- };
9237
- const inputPreview = preview(input, 120);
9238
- const isDupe = result.findings.some(
9239
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9240
- );
9241
- if (!isDupe) {
9242
- result.findings.push({
9243
- source: {
9244
- shieldName: "bash-safe",
9245
- shieldLabel: "bash-safe (AST)",
9246
- sourceType: "shield",
9247
- rule: astRule
9248
- },
9249
- toolName,
9250
- input,
9251
- timestamp: entry.timestamp ?? "",
9252
- project: projLabel,
9253
- sessionId,
9254
- agent: "claude"
9255
- });
9256
- }
9257
- }
9258
9314
  }
9259
9315
  }
9260
9316
  }
9261
- result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "claude"));
9262
- if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
9263
- result.sessionsWithEarlySecrets++;
9317
+ continue;
9318
+ }
9319
+ const usage = entry.message?.usage;
9320
+ const model = entry.message?.model;
9321
+ if (usage && model) {
9322
+ const p = claudeModelPrice(model);
9323
+ if (p) {
9324
+ result.totalCostUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
9264
9325
  }
9265
9326
  }
9266
- }
9327
+ const content = entry.message?.content;
9328
+ if (!Array.isArray(content)) continue;
9329
+ for (const block of content) {
9330
+ if (block.type !== "tool_use") continue;
9331
+ result.totalToolCalls++;
9332
+ const toolName = block.name ?? "";
9333
+ const toolNameLower = toolName.toLowerCase();
9334
+ const input = block.input ?? {};
9335
+ if (block.id && typeof input.file_path === "string") {
9336
+ toolUseFilePaths.set(block.id, input.file_path);
9337
+ }
9338
+ sessionCalls.push({ toolName, input, timestamp: entry.timestamp ?? "" });
9339
+ if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
9340
+ result.bashCalls++;
9341
+ }
9342
+ if (firstEditTs === null && (toolNameLower === "edit" || toolNameLower === "write" || toolNameLower === "write_file" || toolNameLower === "edit_file" || toolNameLower === "multiedit")) {
9343
+ firstEditTs = entry.timestamp ?? null;
9344
+ }
9345
+ const rawCmd = String(input.command ?? "").trimStart();
9346
+ if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
9347
+ const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
9348
+ const inputFileExt = inputFilePath ? import_path21.default.extname(inputFilePath).toLowerCase() : "";
9349
+ if (CODE_EXTENSIONS.has(inputFileExt)) continue;
9350
+ const dlpMatch = scanArgs(input);
9351
+ if (dlpMatch) {
9352
+ if (firstDlpTs === null) firstDlpTs = entry.timestamp ?? null;
9353
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9354
+ if (!dedup.dlpKeys.has(k)) {
9355
+ dedup.dlpKeys.add(k);
9356
+ result.dlpFindings.push({
9357
+ patternName: dlpMatch.patternName,
9358
+ redactedSample: dlpMatch.redactedSample,
9359
+ toolName,
9360
+ timestamp: entry.timestamp ?? "",
9361
+ project: projLabel,
9362
+ sessionId,
9363
+ agent: "claude"
9364
+ });
9365
+ }
9366
+ }
9367
+ let astFsMatched = false;
9368
+ const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
9369
+ if (astRanForBash) {
9370
+ astFsMatched = pushFsOpAstFinding(
9371
+ String(input.command ?? ""),
9372
+ toolName,
9373
+ input,
9374
+ entry.timestamp ?? "",
9375
+ projLabel,
9376
+ sessionId,
9377
+ "claude",
9378
+ result,
9379
+ dedup
9380
+ );
9381
+ }
9382
+ let ruleMatched = astFsMatched;
9383
+ for (const source of ruleSources) {
9384
+ const { rule } = source;
9385
+ if (rule.verdict === "allow") continue;
9386
+ if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
9387
+ if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9388
+ if (!evaluateSmartConditions(input, rule)) continue;
9389
+ const inputPreview = preview(input, 120);
9390
+ const k = findingKey(rule.name, inputPreview, projLabel);
9391
+ if (!dedup.findingsKeys.has(k)) {
9392
+ dedup.findingsKeys.add(k);
9393
+ result.findings.push({
9394
+ source,
9395
+ toolName,
9396
+ input,
9397
+ timestamp: entry.timestamp ?? "",
9398
+ project: projLabel,
9399
+ sessionId,
9400
+ agent: "claude"
9401
+ });
9402
+ }
9403
+ ruleMatched = true;
9404
+ break;
9405
+ }
9406
+ if (!ruleMatched && (toolNameLower === "bash" || toolNameLower === "execute_bash")) {
9407
+ const shellVerdict = detectDangerousShellExec(String(input.command ?? ""));
9408
+ if (shellVerdict) {
9409
+ const astRule = {
9410
+ name: `ast:bash-safe:${shellVerdict}-shell-exec-remote`,
9411
+ tool: "bash",
9412
+ conditions: [],
9413
+ verdict: shellVerdict,
9414
+ reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9415
+ };
9416
+ const inputPreview = preview(input, 120);
9417
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9418
+ if (!dedup.findingsKeys.has(k)) {
9419
+ dedup.findingsKeys.add(k);
9420
+ result.findings.push({
9421
+ source: {
9422
+ shieldName: "bash-safe",
9423
+ shieldLabel: "bash-safe (AST)",
9424
+ sourceType: "shield",
9425
+ rule: astRule
9426
+ },
9427
+ toolName,
9428
+ input,
9429
+ timestamp: entry.timestamp ?? "",
9430
+ project: projLabel,
9431
+ sessionId,
9432
+ agent: "claude"
9433
+ });
9434
+ }
9435
+ }
9436
+ }
9437
+ }
9438
+ }
9439
+ result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "claude"));
9440
+ if (firstDlpTs !== null && (firstEditTs === null || firstDlpTs < firstEditTs)) {
9441
+ result.sessionsWithEarlySecrets++;
9442
+ }
9443
+ }
9444
+ function processClaudeProject(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
9445
+ const projPath = import_path21.default.join(projectsDir, proj);
9446
+ try {
9447
+ if (!import_fs19.default.statSync(projPath).isDirectory()) return;
9448
+ } catch {
9449
+ return;
9450
+ }
9451
+ const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(import_os18.default.homedir(), "~")).slice(
9452
+ 0,
9453
+ 40
9454
+ );
9455
+ let files;
9456
+ try {
9457
+ files = import_fs19.default.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
9458
+ } catch {
9459
+ return;
9460
+ }
9461
+ for (const file of files) {
9462
+ processClaudeFile(
9463
+ file,
9464
+ projPath,
9465
+ projLabel,
9466
+ ruleSources,
9467
+ startDate,
9468
+ result,
9469
+ dedup,
9470
+ onProgress,
9471
+ onLine
9472
+ );
9473
+ }
9474
+ }
9475
+ function emptyClaudeScan() {
9476
+ return {
9477
+ filesScanned: 0,
9478
+ sessions: 0,
9479
+ totalToolCalls: 0,
9480
+ bashCalls: 0,
9481
+ findings: [],
9482
+ dlpFindings: [],
9483
+ loopFindings: [],
9484
+ totalCostUSD: 0,
9485
+ firstDate: null,
9486
+ lastDate: null,
9487
+ sessionsWithEarlySecrets: 0
9488
+ };
9489
+ }
9490
+ function scanClaudeHistory(startDate, onProgress, onLine) {
9491
+ const projectsDir = import_path21.default.join(import_os18.default.homedir(), ".claude", "projects");
9492
+ const result = emptyClaudeScan();
9493
+ if (!import_fs19.default.existsSync(projectsDir)) return result;
9494
+ let projDirs;
9495
+ try {
9496
+ projDirs = import_fs19.default.readdirSync(projectsDir);
9497
+ } catch {
9498
+ return result;
9499
+ }
9500
+ const ruleSources = buildRuleSources();
9501
+ const dedup = emptyScanDedup();
9502
+ for (const proj of projDirs) {
9503
+ processClaudeProject(
9504
+ proj,
9505
+ projectsDir,
9506
+ ruleSources,
9507
+ startDate,
9508
+ result,
9509
+ dedup,
9510
+ onProgress,
9511
+ onLine
9512
+ );
9513
+ }
9267
9514
  return result;
9268
9515
  }
9269
9516
  function scanGeminiHistory(startDate, onProgress, onLine) {
@@ -9281,6 +9528,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9281
9528
  lastDate: null,
9282
9529
  sessionsWithEarlySecrets: 0
9283
9530
  };
9531
+ const dedup = emptyScanDedup();
9284
9532
  if (!import_fs19.default.existsSync(tmpDir)) return result;
9285
9533
  let slugDirs;
9286
9534
  try {
@@ -9337,10 +9585,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9337
9585
  if (text) {
9338
9586
  const dlpMatch = scanArgs({ text });
9339
9587
  if (dlpMatch) {
9340
- const isDupe = result.dlpFindings.some(
9341
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9342
- );
9343
- if (!isDupe) {
9588
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9589
+ if (!dedup.dlpKeys.has(k)) {
9590
+ dedup.dlpKeys.add(k);
9344
9591
  result.dlpFindings.push({
9345
9592
  patternName: dlpMatch.patternName,
9346
9593
  redactedSample: dlpMatch.redactedSample,
@@ -9385,10 +9632,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9385
9632
  continue;
9386
9633
  const dlpMatch = scanArgs(input);
9387
9634
  if (dlpMatch) {
9388
- const isDupe = result.dlpFindings.some(
9389
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9390
- );
9391
- if (!isDupe) {
9635
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9636
+ if (!dedup.dlpKeys.has(k)) {
9637
+ dedup.dlpKeys.add(k);
9392
9638
  result.dlpFindings.push({
9393
9639
  patternName: dlpMatch.patternName,
9394
9640
  redactedSample: dlpMatch.redactedSample,
@@ -9411,7 +9657,8 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9411
9657
  projLabel,
9412
9658
  sessionId,
9413
9659
  "gemini",
9414
- result
9660
+ result,
9661
+ dedup
9415
9662
  );
9416
9663
  }
9417
9664
  let ruleMatched = astFsMatched;
@@ -9422,10 +9669,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9422
9669
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9423
9670
  if (!evaluateSmartConditions(input, rule)) continue;
9424
9671
  const inputPreview = preview(input, 120);
9425
- const isDupe = result.findings.some(
9426
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9427
- );
9428
- if (!isDupe) {
9672
+ const k = findingKey(rule.name, inputPreview, projLabel);
9673
+ if (!dedup.findingsKeys.has(k)) {
9674
+ dedup.findingsKeys.add(k);
9429
9675
  result.findings.push({
9430
9676
  source,
9431
9677
  toolName,
@@ -9453,10 +9699,9 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
9453
9699
  reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9454
9700
  };
9455
9701
  const inputPreview = preview(input, 120);
9456
- const isDupe = result.findings.some(
9457
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9458
- );
9459
- if (!isDupe) {
9702
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9703
+ if (!dedup.findingsKeys.has(k)) {
9704
+ dedup.findingsKeys.add(k);
9460
9705
  result.findings.push({
9461
9706
  source: {
9462
9707
  shieldName: "bash-safe",
@@ -9496,6 +9741,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9496
9741
  lastDate: null,
9497
9742
  sessionsWithEarlySecrets: 0
9498
9743
  };
9744
+ const dedup = emptyScanDedup();
9499
9745
  if (!import_fs19.default.existsSync(sessionsBase)) return result;
9500
9746
  const jsonlFiles = [];
9501
9747
  try {
@@ -9577,10 +9823,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9577
9823
  if (text) {
9578
9824
  const dlpMatch2 = scanArgs({ text });
9579
9825
  if (dlpMatch2) {
9580
- const isDupe = result.dlpFindings.some(
9581
- (f) => f.patternName === dlpMatch2.patternName && f.redactedSample === dlpMatch2.redactedSample && f.project === projLabel
9582
- );
9583
- if (!isDupe) {
9826
+ const k = dlpKey(dlpMatch2.patternName, dlpMatch2.redactedSample, projLabel);
9827
+ if (!dedup.dlpKeys.has(k)) {
9828
+ dedup.dlpKeys.add(k);
9584
9829
  result.dlpFindings.push({
9585
9830
  patternName: dlpMatch2.patternName,
9586
9831
  redactedSample: dlpMatch2.redactedSample,
@@ -9622,10 +9867,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9622
9867
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
9623
9868
  const dlpMatch = scanArgs(input);
9624
9869
  if (dlpMatch) {
9625
- const isDupe = result.dlpFindings.some(
9626
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
9627
- );
9628
- if (!isDupe) {
9870
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, projLabel);
9871
+ if (!dedup.dlpKeys.has(k)) {
9872
+ dedup.dlpKeys.add(k);
9629
9873
  result.dlpFindings.push({
9630
9874
  patternName: dlpMatch.patternName,
9631
9875
  redactedSample: dlpMatch.redactedSample,
@@ -9648,7 +9892,8 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9648
9892
  projLabel,
9649
9893
  sessionId,
9650
9894
  "codex",
9651
- result
9895
+ result,
9896
+ dedup
9652
9897
  );
9653
9898
  }
9654
9899
  let ruleMatched = astFsMatched;
@@ -9660,10 +9905,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9660
9905
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
9661
9906
  if (!evaluateSmartConditions(input, rule)) continue;
9662
9907
  const inputPreview = preview(input, 120);
9663
- const isDupe = result.findings.some(
9664
- (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9665
- );
9666
- if (!isDupe) {
9908
+ const k = findingKey(rule.name, inputPreview, projLabel);
9909
+ if (!dedup.findingsKeys.has(k)) {
9910
+ dedup.findingsKeys.add(k);
9667
9911
  result.findings.push({
9668
9912
  source,
9669
9913
  toolName,
@@ -9688,10 +9932,9 @@ function scanCodexHistory(startDate, onProgress, onLine) {
9688
9932
  reason: `Shell execution of remote download detected by AST analysis (bash-safe)`
9689
9933
  };
9690
9934
  const inputPreview = preview(input, 120);
9691
- const isDupe = result.findings.some(
9692
- (f) => f.source.rule.name === astRule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
9693
- );
9694
- if (!isDupe) {
9935
+ const k = findingKey(astRule.name, inputPreview, projLabel);
9936
+ if (!dedup.findingsKeys.has(k)) {
9937
+ dedup.findingsKeys.add(k);
9695
9938
  result.findings.push({
9696
9939
  source: {
9697
9940
  shieldName: "bash-safe",
@@ -9722,6 +9965,7 @@ function scanShellConfig() {
9722
9965
  (f) => import_path21.default.join(home, f)
9723
9966
  );
9724
9967
  const findings = [];
9968
+ const seen = /* @__PURE__ */ new Set();
9725
9969
  for (const filePath of configFiles) {
9726
9970
  if (!import_fs19.default.existsSync(filePath)) continue;
9727
9971
  let lines;
@@ -9736,10 +9980,9 @@ function scanShellConfig() {
9736
9980
  if (!trimmed || trimmed.startsWith("#")) continue;
9737
9981
  const dlpMatch = scanArgs({ text: trimmed });
9738
9982
  if (!dlpMatch) continue;
9739
- const isDupe = findings.some(
9740
- (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === shortPath
9741
- );
9742
- if (!isDupe) {
9983
+ const k = dlpKey(dlpMatch.patternName, dlpMatch.redactedSample, shortPath);
9984
+ if (!seen.has(k)) {
9985
+ seen.add(k);
9743
9986
  findings.push({
9744
9987
  patternName: dlpMatch.patternName,
9745
9988
  redactedSample: dlpMatch.redactedSample,
@@ -9998,6 +10241,263 @@ function renderNarrativeScorecard(input) {
9998
10241
  console.log(import_chalk5.default.dim("\u2192 github.com/node9-ai/node9-proxy"));
9999
10242
  console.log("");
10000
10243
  }
10244
+ function mkLine(...parts) {
10245
+ let rendered = "";
10246
+ let width = 0;
10247
+ for (const [text, fmt] of parts) {
10248
+ rendered += fmt ? fmt(text) : text;
10249
+ width += text.length;
10250
+ }
10251
+ return { rendered, width };
10252
+ }
10253
+ function shortRule(name, width) {
10254
+ const stripped = name.replace(/^shield:[^:]+:/, "");
10255
+ if (stripped.length <= width) return stripped.padEnd(width);
10256
+ return stripped.slice(0, width - 1) + "\u2026";
10257
+ }
10258
+ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
10259
+ const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
10260
+ const topLines = [];
10261
+ if (scan.dlpFindings.length > 0) {
10262
+ const latest = scan.dlpFindings[0];
10263
+ const rel = relativeDate(latest.timestamp, now);
10264
+ const noun = `credential leak${scan.dlpFindings.length !== 1 ? "s" : ""}`;
10265
+ topLines.push(
10266
+ mkLine(
10267
+ ["\u{1F6A8} ", import_chalk5.default.red],
10268
+ [`${scan.dlpFindings.length} ${noun} in tool input `, import_chalk5.default.bold],
10269
+ [`(latest: ${rel} ago, ${latest.patternName})`, import_chalk5.default.dim]
10270
+ )
10271
+ );
10272
+ }
10273
+ if (blockedCount > 0) {
10274
+ const topBlocked = topRulesByVerdict(summary.sections, "block", 2).map(
10275
+ (r) => r.count > 1 ? `${shortRule(r.name, 20).trimEnd()} \xD7${r.count}` : shortRule(r.name, 20).trimEnd()
10276
+ ).join(", ");
10277
+ topLines.push(
10278
+ mkLine(
10279
+ ["\u{1F6D1} ", import_chalk5.default.red],
10280
+ [`${blockedCount} ops node9 would have blocked `, import_chalk5.default.bold],
10281
+ [`(${topBlocked})`, import_chalk5.default.dim]
10282
+ )
10283
+ );
10284
+ }
10285
+ if (scan.loopFindings.length > 0) {
10286
+ const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
10287
+ const byTool = /* @__PURE__ */ new Map();
10288
+ for (const f of scan.loopFindings) {
10289
+ byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + Math.max(0, f.count - 1));
10290
+ }
10291
+ const top = [...byTool.entries()].sort((a, b) => b[1] - a[1])[0];
10292
+ const wasteSuffix = wastePct > 0 ? `, ${wastePct}% wasted` : "";
10293
+ const detail = top ? `(${top[0]} dominates${wasteSuffix})` : "";
10294
+ topLines.push(
10295
+ mkLine(
10296
+ ["\u{1F501} ", import_chalk5.default.yellow],
10297
+ [`${scan.loopFindings.length} agent loops detected `, import_chalk5.default.bold],
10298
+ [detail, import_chalk5.default.dim]
10299
+ )
10300
+ );
10301
+ }
10302
+ if (blastExposures > 0) {
10303
+ const exposed2 = Math.max(0, 100 - blast.score);
10304
+ const pjDiscount = PROTECTIVE_SHIELD_DISCOUNTS["project-jail"] ?? 0;
10305
+ const pjBonus = Math.round(exposed2 * pjDiscount);
10306
+ const cta = pjBonus > 0 ? ` \u2192 enable project-jail (+${pjBonus} pts)` : "";
10307
+ topLines.push(
10308
+ mkLine(
10309
+ ["\u{1F52D} ", import_chalk5.default.red],
10310
+ [`${blastExposures} secrets reachable on disk`, import_chalk5.default.bold],
10311
+ [cta, import_chalk5.default.dim]
10312
+ )
10313
+ );
10314
+ }
10315
+ if (topLines.length > 0) {
10316
+ for (const ln of boxPanel("TOP FINDINGS", topLines)) console.log(" " + ln);
10317
+ console.log("");
10318
+ }
10319
+ if (summary.leaks.length > 0) {
10320
+ const leakLines = [];
10321
+ for (const leak of summary.leaks.slice(0, 5)) {
10322
+ const rel = relativeDate(leak.timestamp, now);
10323
+ leakLines.push(
10324
+ mkLine(
10325
+ [rel.padStart(4) + " ", import_chalk5.default.dim],
10326
+ [leak.patternName.padEnd(14), import_chalk5.default.red.bold],
10327
+ [" "],
10328
+ [leak.redactedSample.padEnd(20), import_chalk5.default.red],
10329
+ [" "],
10330
+ [`[${leak.toolName}]`.padEnd(15), import_chalk5.default.dim],
10331
+ [" "],
10332
+ [leak.agent, import_chalk5.default.dim]
10333
+ )
10334
+ );
10335
+ }
10336
+ const remaining = summary.leaks.length - 5;
10337
+ if (remaining > 0) {
10338
+ leakLines.push(mkLine([`\u2026 +${remaining} more`, import_chalk5.default.dim]));
10339
+ }
10340
+ const title = `LEAKS \xB7 ${summary.leaks.length} secret${summary.leaks.length !== 1 ? "s" : ""} in plain text`;
10341
+ for (const ln of boxPanel(title, leakLines)) console.log(" " + ln);
10342
+ console.log("");
10343
+ }
10344
+ if (blockedCount > 0) {
10345
+ const blockedLines = [];
10346
+ const ruleEntries = topRulesByVerdict(summary.sections, "block", 12);
10347
+ for (const r of ruleEntries) {
10348
+ const origin = originForRule(r.name, summary.sections);
10349
+ blockedLines.push(
10350
+ mkLine(
10351
+ ["\u2717 ", import_chalk5.default.red],
10352
+ [shortRule(r.name, 24), import_chalk5.default.bold],
10353
+ [" \xD7" + String(r.count).padEnd(4), import_chalk5.default.bold],
10354
+ [" "],
10355
+ [origin, import_chalk5.default.dim]
10356
+ )
10357
+ );
10358
+ }
10359
+ const title = `BLOCKED \xB7 ${blockedCount} ops node9 would have stopped`;
10360
+ for (const ln of boxPanel(title, blockedLines)) console.log(" " + ln);
10361
+ console.log("");
10362
+ }
10363
+ if (reviewCount > 0) {
10364
+ const reviewLines = [];
10365
+ const ruleEntries = topRulesByVerdict(summary.sections, "review", 12);
10366
+ for (const r of ruleEntries) {
10367
+ const origin = originForRule(r.name, summary.sections);
10368
+ reviewLines.push(
10369
+ mkLine(
10370
+ ["\u{1F441} ", import_chalk5.default.yellow],
10371
+ [shortRule(r.name, 24), import_chalk5.default.bold],
10372
+ [" \xD7" + String(r.count).padEnd(4), import_chalk5.default.bold],
10373
+ [" "],
10374
+ [origin, import_chalk5.default.dim]
10375
+ )
10376
+ );
10377
+ }
10378
+ const title = `REVIEW QUEUE \xB7 ${reviewCount} ops flagged for approval`;
10379
+ for (const ln of boxPanel(title, reviewLines)) console.log(" " + ln);
10380
+ console.log("");
10381
+ }
10382
+ if (scan.loopFindings.length > 0) {
10383
+ const { wastePct } = computeLoopWaste(scan.loopFindings, scan.totalToolCalls);
10384
+ const byTool = /* @__PURE__ */ new Map();
10385
+ let totalRepeats = 0;
10386
+ for (const f of scan.loopFindings) {
10387
+ const repeats = Math.max(0, f.count - 1);
10388
+ byTool.set(f.toolName, (byTool.get(f.toolName) ?? 0) + repeats);
10389
+ totalRepeats += repeats;
10390
+ }
10391
+ const toolEntries = [...byTool.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
10392
+ const loopLines = [];
10393
+ for (const [tool, repeats] of toolEntries) {
10394
+ const pct = totalRepeats > 0 ? Math.round(repeats / totalRepeats * 100) : 0;
10395
+ loopLines.push(
10396
+ mkLine(
10397
+ [tool.padEnd(10), import_chalk5.default.bold],
10398
+ [`\xD7${num(repeats)} repeats`.padEnd(16)],
10399
+ [`(${pct}%)`, import_chalk5.default.dim]
10400
+ )
10401
+ );
10402
+ }
10403
+ const topStuck = [...scan.loopFindings].sort((a, b) => b.count - a.count).slice(0, 3);
10404
+ if (topStuck.length > 0) {
10405
+ loopLines.push(mkLine([""]));
10406
+ loopLines.push(mkLine(["Top stuck patterns:", import_chalk5.default.dim]));
10407
+ for (const f of topStuck) {
10408
+ const raw = f.commandPreview || f.toolName;
10409
+ const target = raw.length > 60 ? "\u2026" + raw.slice(raw.length - 59) : raw.padEnd(60);
10410
+ loopLines.push(mkLine([`\xD7${num(f.count).padEnd(4)} `, import_chalk5.default.bold], [target, import_chalk5.default.dim]));
10411
+ }
10412
+ }
10413
+ const wasteSuffix = wastePct > 0 ? ` \xB7 ${wastePct}% wasted` : "";
10414
+ const title = `AGENT LOOPS \xB7 ${scan.loopFindings.length} repeated patterns${wasteSuffix}`;
10415
+ for (const ln of boxPanel(title, loopLines)) console.log(" " + ln);
10416
+ console.log("");
10417
+ }
10418
+ if (blast.reachable.length > 0 || blast.envFindings.length > 0) {
10419
+ const blastLines = [];
10420
+ const DESC_W = 33;
10421
+ for (const r of blast.reachable.slice(0, 8)) {
10422
+ const trimmed = r.description.split(" \u2014 ")[0].split(/—|--/)[0].trim();
10423
+ const desc = trimmed.length > DESC_W ? trimmed.slice(0, DESC_W - 1) + "\u2026" : trimmed;
10424
+ blastLines.push(mkLine(["\u2717 ", import_chalk5.default.red], [r.label.padEnd(36)], [desc, import_chalk5.default.dim]));
10425
+ }
10426
+ for (const e of blast.envFindings.slice(0, 3)) {
10427
+ blastLines.push(
10428
+ mkLine(["\u26A0 ", import_chalk5.default.yellow], [`${e.key} `], [`(${e.patternName})`, import_chalk5.default.dim])
10429
+ );
10430
+ }
10431
+ const totalExposed = blast.reachable.length + blast.envFindings.length;
10432
+ if (totalExposed > 8) {
10433
+ blastLines.push(mkLine([`\u2026 +${totalExposed - 8} more`, import_chalk5.default.dim]));
10434
+ }
10435
+ const title = `BLAST RADIUS \xB7 ${totalExposed} path${totalExposed !== 1 ? "s" : ""} reachable right now`;
10436
+ for (const ln of boxPanel(title, blastLines)) console.log(" " + ln);
10437
+ console.log("");
10438
+ }
10439
+ const shieldImpacts = rollupByShield(summary.sections);
10440
+ const exposed = Math.max(0, 100 - blast.score);
10441
+ const shieldLines = [];
10442
+ const ranked = [...shieldImpacts].sort((a, b) => {
10443
+ const aDiscount = PROTECTIVE_SHIELD_DISCOUNTS[a.shieldName] ?? 0;
10444
+ const bDiscount = PROTECTIVE_SHIELD_DISCOUNTS[b.shieldName] ?? 0;
10445
+ if (aDiscount !== bDiscount) return bDiscount - aDiscount;
10446
+ return b.totalCatches - a.totalCatches;
10447
+ });
10448
+ for (const impact of ranked) {
10449
+ if (impact.totalCatches === 0) continue;
10450
+ const discount = PROTECTIVE_SHIELD_DISCOUNTS[impact.shieldName] ?? 0;
10451
+ const bonus = Math.round(exposed * discount);
10452
+ const icon = discount > 0 ? "\u{1F6E1} " : "\u2610 ";
10453
+ const wouldCatch = `would catch ${impact.totalCatches} op${impact.totalCatches !== 1 ? "s" : ""}`;
10454
+ const deltaSuffix = bonus > 0 ? ` \u2192 +${bonus} pts (${blast.score} \u2192 ${blast.score + bonus})` : "";
10455
+ shieldLines.push(
10456
+ mkLine(
10457
+ [icon, discount > 0 ? import_chalk5.default.cyan : import_chalk5.default.dim],
10458
+ [impact.shieldName.padEnd(14), import_chalk5.default.bold],
10459
+ [wouldCatch.padEnd(22), import_chalk5.default.dim],
10460
+ [deltaSuffix, bonus > 0 ? import_chalk5.default.green.bold : import_chalk5.default.dim]
10461
+ )
10462
+ );
10463
+ if (impact.topRuleLabels.length > 0) {
10464
+ const rules = impact.topRuleLabels.join(", ");
10465
+ shieldLines.push(mkLine([" ", import_chalk5.default.dim], [rules, import_chalk5.default.dim]));
10466
+ }
10467
+ }
10468
+ const hitShieldSet = new Set(
10469
+ shieldImpacts.filter((i) => i.totalCatches > 0).map((i) => i.shieldName)
10470
+ );
10471
+ const zeroHitBuiltins = Object.keys(SHIELDS).filter((name) => !hitShieldSet.has(name)).sort();
10472
+ if (zeroHitBuiltins.length > 0) {
10473
+ shieldLines.push(mkLine([""]));
10474
+ shieldLines.push(mkLine([zeroHitBuiltins.join(" \xB7 "), import_chalk5.default.dim]));
10475
+ shieldLines.push(mkLine([" no hits in your history \u2014 install proactively", import_chalk5.default.dim]));
10476
+ }
10477
+ const topRec = ranked.find(
10478
+ (r) => r.totalCatches > 0 && (PROTECTIVE_SHIELD_DISCOUNTS[r.shieldName] ?? 0) > 0
10479
+ );
10480
+ if (topRec) {
10481
+ const bonus = Math.round(exposed * (PROTECTIVE_SHIELD_DISCOUNTS[topRec.shieldName] ?? 0));
10482
+ const cta = `\u2192 node9 shield enable ${topRec.shieldName} (start here \u2014 +${bonus} pts)`;
10483
+ shieldLines.push(mkLine([""]));
10484
+ shieldLines.push(mkLine([cta, import_chalk5.default.cyan]));
10485
+ }
10486
+ if (shieldLines.length > 0) {
10487
+ const title = "SHIELDS \xB7 install node9 + enable these to catch what we found";
10488
+ for (const ln of boxPanel(title, shieldLines)) console.log(" " + ln);
10489
+ console.log("");
10490
+ }
10491
+ }
10492
+ function originForRule(ruleName, sections) {
10493
+ for (const section of sections) {
10494
+ if (section.rules.some((r) => r.name === ruleName)) {
10495
+ if (section.sourceType === "default") return "default";
10496
+ if (section.sourceType === "shield") return `needs shield:${section.shieldKey ?? section.id}`;
10497
+ }
10498
+ }
10499
+ return "";
10500
+ }
10001
10501
  function registerScanCommand(program2) {
10002
10502
  program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per rule (default: 5)", "5").option("--drill-down", "Show all findings with full commands and session IDs").option("--compact", "Compact one-screen scorecard \u2014 for screenshots and sharing").option("--narrative", "Severity-grouped report \u2014 for video / dramatic sharing").option(
10003
10503
  "--json",
@@ -10229,7 +10729,7 @@ function registerScanCommand(program2) {
10229
10729
  " " + import_chalk5.default.dim("AI spend ") + import_chalk5.default.bold(fmtCost(scan.totalCostUSD)) + (summary.loopWastedUSD > 0 ? import_chalk5.default.dim(" \xB7 wasted on loops ") + import_chalk5.default.yellow("~" + fmtCost(summary.loopWastedUSD)) : "")
10230
10730
  );
10231
10731
  }
10232
- if (scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
10732
+ if (drillDown && scan.dlpFindings.length > 0 && scan.sessionsWithEarlySecrets > 0) {
10233
10733
  console.log(
10234
10734
  " " + import_chalk5.default.dim(
10235
10735
  `${scan.sessionsWithEarlySecrets} session${scan.sessionsWithEarlySecrets !== 1 ? "s" : ""} loaded secrets before first edit`
@@ -10237,6 +10737,26 @@ function registerScanCommand(program2) {
10237
10737
  );
10238
10738
  }
10239
10739
  console.log("");
10740
+ if (!drillDown) {
10741
+ renderPanelScorecard({
10742
+ scan,
10743
+ summary,
10744
+ blast,
10745
+ blastExposures,
10746
+ blockedCount,
10747
+ reviewCount
10748
+ });
10749
+ const cta = isWired ? "\u2705 node9 is active" : "\u2192 install node9 to enable protection";
10750
+ console.log(" " + import_chalk5.default.green(cta));
10751
+ console.log(
10752
+ " " + import_chalk5.default.dim("\u2192 ") + import_chalk5.default.cyan("node9 monitor") + import_chalk5.default.dim(" live dashboard")
10753
+ );
10754
+ console.log(
10755
+ " " + import_chalk5.default.dim("\u2192 ") + import_chalk5.default.cyan("node9 scan --drill-down") + import_chalk5.default.dim(" full commands + session IDs")
10756
+ );
10757
+ console.log("");
10758
+ return;
10759
+ }
10240
10760
  if (scan.dlpFindings.length > 0) {
10241
10761
  console.log(" " + import_chalk5.default.dim("\u2500".repeat(70)));
10242
10762
  console.log(
@@ -10425,7 +10945,7 @@ function registerScanCommand(program2) {
10425
10945
  }
10426
10946
  );
10427
10947
  }
10428
- var import_chalk5, import_fs19, import_path21, import_os18, CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, DEFAULT_RULE_NAMES, classifyRuleSeverity2, narrativeRuleLabel2;
10948
+ var import_chalk5, import_fs19, import_path21, import_os18, CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
10429
10949
  var init_scan = __esm({
10430
10950
  "src/cli/commands/scan.ts"() {
10431
10951
  "use strict";
@@ -10443,6 +10963,7 @@ var init_scan = __esm({
10443
10963
  init_setup();
10444
10964
  init_blast();
10445
10965
  init_scan_derive();
10966
+ init_protection();
10446
10967
  init_scan_json();
10447
10968
  init_scan_history();
10448
10969
  CLAUDE_PRICING = {
@@ -10525,9 +11046,6 @@ var init_scan = __esm({
10525
11046
  STUCK_TOOLS_LIMIT = 3;
10526
11047
  RECURRING_SESSION_THRESHOLD = 3;
10527
11048
  STALE_AGE_DAYS = 30;
10528
- DEFAULT_RULE_NAMES = new Set(
10529
- DEFAULT_CONFIG.policy.smartRules.map((r) => r.name).filter(Boolean)
10530
- );
10531
11049
  classifyRuleSeverity2 = classifyRuleSeverity;
10532
11050
  narrativeRuleLabel2 = narrativeRuleLabel;
10533
11051
  }
@@ -11027,6 +11545,19 @@ data: ${JSON.stringify(data)}
11027
11545
  }
11028
11546
  });
11029
11547
  }
11548
+ function broadcastForensic(finding) {
11549
+ const severity = CRITICAL_FORENSIC_CATEGORIES.has(finding.type) ? "critical" : "warning";
11550
+ const event = {
11551
+ type: "forensic",
11552
+ id: `fnd_${(0, import_crypto6.randomUUID)()}`,
11553
+ ts: Date.now(),
11554
+ sessionId: finding.sessionId,
11555
+ category: finding.type,
11556
+ severity
11557
+ };
11558
+ if (finding.patternName !== void 0) event.patternName = finding.patternName;
11559
+ broadcast("forensic", event);
11560
+ }
11030
11561
  function abandonPending() {
11031
11562
  setAbandonTimer(null);
11032
11563
  pending.forEach((entry, id) => {
@@ -11210,7 +11741,7 @@ function bindActivitySocket() {
11210
11741
  });
11211
11742
  activitySocketServer = unixServer;
11212
11743
  }
11213
- var import_net2, import_fs21, import_path23, import_os19, import_crypto6, 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;
11744
+ var import_net2, import_fs21, import_path23, import_os19, import_crypto6, 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;
11214
11745
  var init_state2 = __esm({
11215
11746
  "src/daemon/state.ts"() {
11216
11747
  "use strict";
@@ -11260,6 +11791,11 @@ var init_state2 = __esm({
11260
11791
  INPUT_PRICE_PER_1M = 3;
11261
11792
  OUTPUT_PRICE_PER_1M = 15;
11262
11793
  BYTES_PER_TOKEN = 4;
11794
+ CRITICAL_FORENSIC_CATEGORIES = /* @__PURE__ */ new Set([
11795
+ "privilege-escalation",
11796
+ "destructive-op",
11797
+ "eval-of-remote"
11798
+ ]);
11263
11799
  WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
11264
11800
  "write",
11265
11801
  "write_file",
@@ -11600,7 +12136,26 @@ function startCloudSync() {
11600
12136
  const recurring = setInterval(() => void syncOnce(), intervalMs);
11601
12137
  recurring.unref();
11602
12138
  }
11603
- var import_fs22, import_https2, import_os20, import_path24, FINDING_TO_SIGNAL3, rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS;
12139
+ function startForensicBroadcast() {
12140
+ const tick = async () => {
12141
+ try {
12142
+ const findings = await tickForensicBroadcast(forensicBroadcastOffsets);
12143
+ for (const f of findings) broadcastForensic(f);
12144
+ } catch (err2) {
12145
+ const msg = err2 instanceof Error ? err2.message : String(err2);
12146
+ appendToLog(HOOK_DEBUG_LOG, {
12147
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
12148
+ kind: "forensic-broadcast-error",
12149
+ error: msg
12150
+ });
12151
+ }
12152
+ };
12153
+ const initial = setTimeout(() => void tick(), FORENSIC_INITIAL_DELAY_MS);
12154
+ initial.unref();
12155
+ const recurring = setInterval(() => void tick(), FORENSIC_BROADCAST_INTERVAL_MS);
12156
+ recurring.unref();
12157
+ }
12158
+ var import_fs22, import_https2, import_os20, import_path24, FINDING_TO_SIGNAL3, rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS, FORENSIC_BROADCAST_INTERVAL_MS, FORENSIC_INITIAL_DELAY_MS, forensicBroadcastOffsets;
11604
12159
  var init_sync = __esm({
11605
12160
  "src/daemon/sync.ts"() {
11606
12161
  "use strict";
@@ -11612,6 +12167,8 @@ var init_sync = __esm({
11612
12167
  init_blast();
11613
12168
  init_dist();
11614
12169
  init_scan_watermark();
12170
+ init_state2();
12171
+ init_audit();
11615
12172
  FINDING_TO_SIGNAL3 = {
11616
12173
  dlp: "dlpFindings",
11617
12174
  pii: "piiFindings",
@@ -11628,6 +12185,9 @@ var init_sync = __esm({
11628
12185
  DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept/policies/sync";
11629
12186
  DEFAULT_INTERVAL_HOURS = 5;
11630
12187
  MIN_INTERVAL_HOURS = 1;
12188
+ FORENSIC_BROADCAST_INTERVAL_MS = 3e4;
12189
+ FORENSIC_INITIAL_DELAY_MS = 5e3;
12190
+ forensicBroadcastOffsets = /* @__PURE__ */ new Map();
11631
12191
  }
11632
12192
  });
11633
12193
 
@@ -11855,6 +12415,7 @@ var init_mcp_tools = __esm({
11855
12415
  function startDaemon() {
11856
12416
  startCostSync();
11857
12417
  startCloudSync();
12418
+ startForensicBroadcast();
11858
12419
  startDlpScanner();
11859
12420
  loadInsightCounts();
11860
12421
  const internalToken = (0, import_crypto7.randomUUID)();
@@ -13057,8 +13618,15 @@ var tail_exports = {};
13057
13618
  __export(tail_exports, {
13058
13619
  agentLabel: () => agentLabel,
13059
13620
  sessionTag: () => sessionTag,
13621
+ shortenPathSummary: () => shortenPathSummary,
13060
13622
  startTail: () => startTail
13061
13623
  });
13624
+ function shortenPathSummary(s) {
13625
+ if (!s || !s.startsWith("/")) return s;
13626
+ const parts = s.split("/").filter(Boolean);
13627
+ if (parts.length <= 2) return s;
13628
+ return `\u2026/${parts.slice(-2).join("/")}`;
13629
+ }
13062
13630
  function getIcon(tool) {
13063
13631
  const t = tool.toLowerCase();
13064
13632
  for (const [k, v] of Object.entries(ICONS)) {
@@ -13812,7 +14380,8 @@ async function startTail(options = {}) {
13812
14380
  if (event === "snapshot") {
13813
14381
  const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
13814
14382
  const hash = data.hash ?? "";
13815
- const summary = data.argsSummary ?? data.tool;
14383
+ const rawSummary = data.argsSummary ?? data.tool;
14384
+ const summary = shortenPathSummary(rawSummary);
13816
14385
  const fileCount = data.fileCount ?? 0;
13817
14386
  const files = fileCount > 0 ? import_chalk30.default.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
13818
14387
  process.stdout.write(
@@ -16201,63 +16770,13 @@ function registerAuditCommand(program2) {
16201
16770
 
16202
16771
  // src/cli/commands/report.ts
16203
16772
  var import_chalk13 = __toESM(require("chalk"));
16773
+
16774
+ // src/cli/aggregate/report-audit.ts
16204
16775
  var import_fs35 = __toESM(require("fs"));
16205
- var import_path36 = __toESM(require("path"));
16206
16776
  var import_os31 = __toESM(require("os"));
16207
-
16208
- // src/cli/render/report-json.ts
16209
- function buildReportJson(input) {
16210
- const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
16211
- const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
16212
- const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
16213
- return {
16214
- schemaVersion: 1,
16215
- generatedAt: input.generatedAt,
16216
- period: input.period,
16217
- range: { start: input.start.toISOString(), end: input.end.toISOString() },
16218
- excludedTests: input.excludedTests,
16219
- totals: {
16220
- events: input.total,
16221
- blocked: totalBlocked,
16222
- blockRate,
16223
- userApproved: input.userApproved,
16224
- userDenied: input.userDenied,
16225
- timedOut: input.timedOut,
16226
- hardBlocked: input.hardBlocked,
16227
- dlpBlocked: input.dlpBlocked,
16228
- observeDlp: input.observeDlp,
16229
- loopHits: input.loopHits,
16230
- unackedDlp: input.unackedDlp
16231
- },
16232
- tests: {
16233
- passes: input.testPasses,
16234
- fails: input.testFails
16235
- },
16236
- cost: {
16237
- totalUSD: input.cost.claudeUSD + input.cost.codexUSD,
16238
- claudeUSD: input.cost.claudeUSD,
16239
- codexUSD: input.cost.codexUSD,
16240
- inputTokens: input.cost.inputTokens,
16241
- outputTokens: input.cost.outputTokens,
16242
- cacheWriteTokens: input.cost.cacheWriteTokens,
16243
- cacheReadTokens: input.cost.cacheReadTokens,
16244
- byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
16245
- byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
16246
- },
16247
- byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
16248
- byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
16249
- byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
16250
- byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
16251
- byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
16252
- byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
16253
- trend: {
16254
- priorBlockRate: input.priorBlockRate,
16255
- deltaPct
16256
- }
16257
- };
16258
- }
16259
-
16260
- // src/cli/commands/report.ts
16777
+ var import_path36 = __toESM(require("path"));
16778
+ init_costSync();
16779
+ init_litellm();
16261
16780
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
16262
16781
  function buildTestTimestamps(allEntries) {
16263
16782
  const testTs = /* @__PURE__ */ new Set();
@@ -16282,8 +16801,7 @@ function isTestEntry(entry, testTs) {
16282
16801
  }
16283
16802
  return false;
16284
16803
  }
16285
- function getDateRange(period) {
16286
- const now = /* @__PURE__ */ new Date();
16804
+ function getDateRange(period, now) {
16287
16805
  const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
16288
16806
  const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
16289
16807
  switch (period) {
@@ -16299,6 +16817,11 @@ function getDateRange(period) {
16299
16817
  s.setDate(s.getDate() - 29);
16300
16818
  return { start: s, end };
16301
16819
  }
16820
+ case "90d": {
16821
+ const s = new Date(todayStart);
16822
+ s.setDate(s.getDate() - 89);
16823
+ return { start: s, end };
16824
+ }
16302
16825
  case "month":
16303
16826
  return { start: new Date(now.getFullYear(), now.getMonth(), 1), end };
16304
16827
  }
@@ -16321,40 +16844,6 @@ function isAllow(decision) {
16321
16844
  function isDlp(checkedBy) {
16322
16845
  return !!checkedBy?.includes("dlp");
16323
16846
  }
16324
- var BLOCK_REASON_LABELS = {
16325
- timeout: "Popup timeout",
16326
- "smart-rule-block": "Smart rule",
16327
- "observe-mode-dlp-would-block": "DLP (observe)",
16328
- "persistent-deny": "Persistent deny",
16329
- "local-decision": "User denied",
16330
- "dlp-block": "DLP block",
16331
- "loop-detected": "Loop detected"
16332
- };
16333
- function humanBlockReason(reason) {
16334
- return BLOCK_REASON_LABELS[reason] ?? reason;
16335
- }
16336
- function barStr(value, max, width) {
16337
- if (max === 0 || width <= 0) return "\u2591".repeat(width);
16338
- const filled = Math.max(1, Math.round(value / max * width));
16339
- return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
16340
- }
16341
- function colorBar(value, max, width) {
16342
- const s = barStr(value, max, width);
16343
- const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
16344
- return import_chalk13.default.cyan(s.slice(0, filled)) + import_chalk13.default.dim(s.slice(filled));
16345
- }
16346
- function fmtDate(d) {
16347
- const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
16348
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
16349
- }
16350
- function num2(n) {
16351
- return n.toLocaleString();
16352
- }
16353
- function fmtCost2(usd) {
16354
- if (usd < 1e-3) return "< $0.001";
16355
- if (usd < 1) return "$" + usd.toFixed(4);
16356
- return "$" + usd.toFixed(2);
16357
- }
16358
16847
  var CLAUDE_PRICING2 = {
16359
16848
  "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
16360
16849
  "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
@@ -16374,90 +16863,160 @@ function claudeModelPrice2(model) {
16374
16863
  }
16375
16864
  return null;
16376
16865
  }
16377
- function loadClaudeCost(start, end) {
16378
- const empty = {
16866
+ function emptyClaudeCostAccumulator() {
16867
+ return {
16379
16868
  total: 0,
16380
- byDay: /* @__PURE__ */ new Map(),
16381
- byModel: /* @__PURE__ */ new Map(),
16382
16869
  inputTokens: 0,
16383
16870
  outputTokens: 0,
16384
16871
  cacheWriteTokens: 0,
16385
- cacheReadTokens: 0
16872
+ cacheReadTokens: 0,
16873
+ byDay: /* @__PURE__ */ new Map(),
16874
+ byModel: /* @__PURE__ */ new Map(),
16875
+ byProject: /* @__PURE__ */ new Map()
16876
+ };
16877
+ }
16878
+ function freezeClaudeCost(acc) {
16879
+ return {
16880
+ total: acc.total,
16881
+ byDay: acc.byDay,
16882
+ byModel: acc.byModel,
16883
+ byProject: acc.byProject,
16884
+ inputTokens: acc.inputTokens,
16885
+ outputTokens: acc.outputTokens,
16886
+ cacheWriteTokens: acc.cacheWriteTokens,
16887
+ cacheReadTokens: acc.cacheReadTokens
16386
16888
  };
16387
- const projectsDir = import_path36.default.join(import_os31.default.homedir(), ".claude", "projects");
16388
- if (!import_fs35.default.existsSync(projectsDir)) return empty;
16889
+ }
16890
+ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
16891
+ const projPath = import_path36.default.join(projectsDir, proj);
16892
+ let files;
16893
+ try {
16894
+ const stat = import_fs35.default.statSync(projPath);
16895
+ if (!stat.isDirectory()) return;
16896
+ files = import_fs35.default.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
16897
+ } catch {
16898
+ return;
16899
+ }
16900
+ const startMs = start.getTime();
16901
+ for (const file of files) {
16902
+ const filePath = import_path36.default.join(projPath, file);
16903
+ try {
16904
+ if (import_fs35.default.statSync(filePath).mtimeMs < startMs) continue;
16905
+ } catch {
16906
+ continue;
16907
+ }
16908
+ try {
16909
+ const raw = import_fs35.default.readFileSync(filePath, "utf-8");
16910
+ for (const line of raw.split("\n")) {
16911
+ if (!line.trim()) continue;
16912
+ let entry;
16913
+ try {
16914
+ entry = JSON.parse(line);
16915
+ } catch {
16916
+ continue;
16917
+ }
16918
+ if (entry.type !== "assistant") continue;
16919
+ if (!entry.timestamp) continue;
16920
+ const ts = new Date(entry.timestamp);
16921
+ if (ts < start || ts > end) continue;
16922
+ const usage = entry.message?.usage;
16923
+ const model = entry.message?.model;
16924
+ if (!usage || !model) continue;
16925
+ const p = claudeModelPrice2(model);
16926
+ if (!p) continue;
16927
+ const inp = usage.input_tokens ?? 0;
16928
+ const out = usage.output_tokens ?? 0;
16929
+ const cw = usage.cache_creation_input_tokens ?? 0;
16930
+ const cr = usage.cache_read_input_tokens ?? 0;
16931
+ const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
16932
+ acc.total += cost;
16933
+ acc.inputTokens += inp;
16934
+ acc.outputTokens += out;
16935
+ acc.cacheWriteTokens += cw;
16936
+ acc.cacheReadTokens += cr;
16937
+ const dateKey = entry.timestamp.slice(0, 10);
16938
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
16939
+ const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
16940
+ acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
16941
+ const projectKey = decodeProjectDirName(proj);
16942
+ const projectRollup = acc.byProject.get(projectKey) ?? {
16943
+ cost: 0,
16944
+ inputTokens: 0,
16945
+ outputTokens: 0
16946
+ };
16947
+ projectRollup.cost += cost;
16948
+ projectRollup.inputTokens += inp;
16949
+ projectRollup.outputTokens += out;
16950
+ acc.byProject.set(projectKey, projectRollup);
16951
+ }
16952
+ } catch {
16953
+ continue;
16954
+ }
16955
+ }
16956
+ }
16957
+ function loadClaudeCost(start, end, projectsDir) {
16958
+ const acc = emptyClaudeCostAccumulator();
16959
+ if (!import_fs35.default.existsSync(projectsDir)) return freezeClaudeCost(acc);
16389
16960
  let dirs;
16390
16961
  try {
16391
16962
  dirs = import_fs35.default.readdirSync(projectsDir);
16392
16963
  } catch {
16393
- return empty;
16964
+ return freezeClaudeCost(acc);
16394
16965
  }
16395
- let total = 0;
16396
- let inputTokens = 0;
16397
- let outputTokens = 0;
16398
- let cacheWriteTokens = 0;
16399
- let cacheReadTokens = 0;
16400
- const byDay = /* @__PURE__ */ new Map();
16401
- const byModel = /* @__PURE__ */ new Map();
16402
16966
  for (const proj of dirs) {
16403
- const projPath = import_path36.default.join(projectsDir, proj);
16404
- let files;
16967
+ processClaudeCostProject(proj, projectsDir, start, end, acc);
16968
+ }
16969
+ return freezeClaudeCost(acc);
16970
+ }
16971
+ function processCodexCostFile(filePath, start, end, acc) {
16972
+ let lines;
16973
+ try {
16974
+ lines = import_fs35.default.readFileSync(filePath, "utf-8").split("\n");
16975
+ } catch {
16976
+ return;
16977
+ }
16978
+ let sessionStart2 = "";
16979
+ let lastTotalInput = 0;
16980
+ let lastTotalCached = 0;
16981
+ let lastTotalOutput = 0;
16982
+ let sessionToolCalls = 0;
16983
+ for (const line of lines) {
16984
+ if (!line.trim()) continue;
16985
+ let entry;
16405
16986
  try {
16406
- const stat = import_fs35.default.statSync(projPath);
16407
- if (!stat.isDirectory()) continue;
16408
- files = import_fs35.default.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
16987
+ entry = JSON.parse(line);
16409
16988
  } catch {
16410
16989
  continue;
16411
16990
  }
16412
- for (const file of files) {
16413
- try {
16414
- const raw = import_fs35.default.readFileSync(import_path36.default.join(projPath, file), "utf-8");
16415
- for (const line of raw.split("\n")) {
16416
- if (!line.trim()) continue;
16417
- let entry;
16418
- try {
16419
- entry = JSON.parse(line);
16420
- } catch {
16421
- continue;
16422
- }
16423
- if (entry.type !== "assistant") continue;
16424
- if (!entry.timestamp) continue;
16425
- const ts = new Date(entry.timestamp);
16426
- if (ts < start || ts > end) continue;
16427
- const usage = entry.message?.usage;
16428
- const model = entry.message?.model;
16429
- if (!usage || !model) continue;
16430
- const p = claudeModelPrice2(model);
16431
- if (!p) continue;
16432
- const inp = usage.input_tokens ?? 0;
16433
- const out = usage.output_tokens ?? 0;
16434
- const cw = usage.cache_creation_input_tokens ?? 0;
16435
- const cr = usage.cache_read_input_tokens ?? 0;
16436
- const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
16437
- total += cost;
16438
- inputTokens += inp;
16439
- outputTokens += out;
16440
- cacheWriteTokens += cw;
16441
- cacheReadTokens += cr;
16442
- const dateKey = entry.timestamp.slice(0, 10);
16443
- byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
16444
- const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
16445
- byModel.set(normModel, (byModel.get(normModel) ?? 0) + cost);
16446
- }
16447
- } catch {
16448
- continue;
16449
- }
16991
+ const p = entry.payload ?? {};
16992
+ if (entry.type === "session_meta") {
16993
+ sessionStart2 = String(p["timestamp"] ?? "");
16994
+ continue;
16995
+ }
16996
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
16997
+ const info = p["info"] ?? {};
16998
+ const usage = info["total_token_usage"] ?? {};
16999
+ lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
17000
+ lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
17001
+ lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
17002
+ }
17003
+ if (entry.type === "response_item" && p["type"] === "function_call") {
17004
+ sessionToolCalls++;
16450
17005
  }
16451
17006
  }
16452
- return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
17007
+ if (!sessionStart2) return;
17008
+ const ts = new Date(sessionStart2);
17009
+ if (ts < start || ts > end) return;
17010
+ const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
17011
+ const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
17012
+ acc.total += cost;
17013
+ acc.toolCalls += sessionToolCalls;
17014
+ const dateKey = sessionStart2.slice(0, 10);
17015
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
16453
17016
  }
16454
- function loadCodexCost(start, end) {
16455
- const sessionsBase = import_path36.default.join(import_os31.default.homedir(), ".codex", "sessions");
16456
- const byDay = /* @__PURE__ */ new Map();
16457
- let total = 0;
16458
- let toolCalls = 0;
16459
- if (!import_fs35.default.existsSync(sessionsBase)) return { total, byDay, toolCalls };
17017
+ function listCodexSessionFiles(sessionsBase) {
16460
17018
  const jsonlFiles = [];
17019
+ if (!import_fs35.default.existsSync(sessionsBase)) return jsonlFiles;
16461
17020
  try {
16462
17021
  for (const year of import_fs35.default.readdirSync(sessionsBase)) {
16463
17022
  const yearPath = import_path36.default.join(sessionsBase, year);
@@ -16487,495 +17046,742 @@ function loadCodexCost(start, end) {
16487
17046
  }
16488
17047
  }
16489
17048
  } catch {
16490
- return { total, byDay, toolCalls };
17049
+ return [];
16491
17050
  }
16492
- for (const filePath of jsonlFiles) {
16493
- let lines;
17051
+ return jsonlFiles;
17052
+ }
17053
+ function loadCodexCost(start, end, sessionsBase) {
17054
+ const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
17055
+ const files = listCodexSessionFiles(sessionsBase);
17056
+ for (const filePath of files) {
17057
+ processCodexCostFile(filePath, start, end, acc);
17058
+ }
17059
+ return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
17060
+ }
17061
+ var GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
17062
+ function geminiPriceFor(model) {
17063
+ let tuple = pricingFor(model);
17064
+ if (!tuple && /^gemini-/i.test(model)) {
17065
+ for (const proxy of GEMINI_FALLBACK_MODELS) {
17066
+ tuple = pricingFor(proxy);
17067
+ if (tuple) break;
17068
+ }
17069
+ }
17070
+ if (!tuple) return null;
17071
+ return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
17072
+ }
17073
+ function emptyGeminiAccumulator() {
17074
+ return {
17075
+ total: 0,
17076
+ inputTokens: 0,
17077
+ outputTokens: 0,
17078
+ cacheReadTokens: 0,
17079
+ byDay: /* @__PURE__ */ new Map(),
17080
+ byProject: /* @__PURE__ */ new Map()
17081
+ };
17082
+ }
17083
+ function freezeGeminiCost(acc) {
17084
+ return {
17085
+ total: acc.total,
17086
+ byDay: acc.byDay,
17087
+ byProject: acc.byProject,
17088
+ inputTokens: acc.inputTokens,
17089
+ outputTokens: acc.outputTokens,
17090
+ cacheReadTokens: acc.cacheReadTokens
17091
+ };
17092
+ }
17093
+ function processGeminiCostFile(filePath, projectKey, start, end, acc) {
17094
+ const startMs = start.getTime();
17095
+ try {
17096
+ if (import_fs35.default.statSync(filePath).mtimeMs < startMs) return;
17097
+ } catch {
17098
+ return;
17099
+ }
17100
+ let raw;
17101
+ try {
17102
+ raw = import_fs35.default.readFileSync(filePath, "utf-8");
17103
+ } catch {
17104
+ return;
17105
+ }
17106
+ const seenIds = /* @__PURE__ */ new Set();
17107
+ for (const line of raw.split("\n")) {
17108
+ if (!line.trim()) continue;
17109
+ let entry;
16494
17110
  try {
16495
- lines = import_fs35.default.readFileSync(filePath, "utf-8").split("\n");
17111
+ entry = JSON.parse(line);
16496
17112
  } catch {
16497
17113
  continue;
16498
17114
  }
16499
- let sessionStart2 = "";
16500
- let lastTotalInput = 0;
16501
- let lastTotalCached = 0;
16502
- let lastTotalOutput = 0;
16503
- let sessionToolCalls = 0;
16504
- for (const line of lines) {
16505
- if (!line.trim()) continue;
16506
- let entry;
16507
- try {
16508
- entry = JSON.parse(line);
16509
- } catch {
16510
- continue;
16511
- }
16512
- const p = entry.payload ?? {};
16513
- if (entry.type === "session_meta") {
16514
- sessionStart2 = String(p["timestamp"] ?? "");
16515
- continue;
16516
- }
16517
- if (entry.type === "event_msg" && p["type"] === "token_count") {
16518
- const info = p["info"] ?? {};
16519
- const usage = info["total_token_usage"] ?? {};
16520
- lastTotalInput = usage["input_tokens"] ?? lastTotalInput;
16521
- lastTotalCached = usage["cached_input_tokens"] ?? lastTotalCached;
16522
- lastTotalOutput = usage["output_tokens"] ?? lastTotalOutput;
16523
- }
16524
- if (entry.type === "response_item" && p["type"] === "function_call") {
16525
- sessionToolCalls++;
16526
- }
17115
+ if (entry.type !== "gemini") continue;
17116
+ if (!entry.tokens || !entry.model || !entry.timestamp) continue;
17117
+ if (entry.id) {
17118
+ if (seenIds.has(entry.id)) continue;
17119
+ seenIds.add(entry.id);
16527
17120
  }
16528
- if (!sessionStart2) continue;
16529
- const ts = new Date(sessionStart2);
17121
+ const ts = new Date(entry.timestamp);
16530
17122
  if (ts < start || ts > end) continue;
16531
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
16532
- const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
16533
- total += cost;
16534
- toolCalls += sessionToolCalls;
16535
- const dateKey = sessionStart2.slice(0, 10);
16536
- byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
17123
+ const price = geminiPriceFor(entry.model);
17124
+ if (!price) continue;
17125
+ const inp = entry.tokens.input ?? 0;
17126
+ const out = entry.tokens.output ?? 0;
17127
+ const cached = Math.min(entry.tokens.cached ?? 0, inp);
17128
+ const fresh = Math.max(0, inp - cached);
17129
+ const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
17130
+ acc.total += cost;
17131
+ acc.inputTokens += inp;
17132
+ acc.outputTokens += out;
17133
+ acc.cacheReadTokens += cached;
17134
+ const dateKey = entry.timestamp.slice(0, 10);
17135
+ acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
17136
+ const rollup = acc.byProject.get(projectKey) ?? {
17137
+ cost: 0,
17138
+ inputTokens: 0,
17139
+ outputTokens: 0
17140
+ };
17141
+ rollup.cost += cost;
17142
+ rollup.inputTokens += inp;
17143
+ rollup.outputTokens += out;
17144
+ acc.byProject.set(projectKey, rollup);
16537
17145
  }
16538
- return { total, byDay, toolCalls };
17146
+ }
17147
+ function listGeminiSessionFiles(geminiTmpDir) {
17148
+ const out = [];
17149
+ let dirs;
17150
+ try {
17151
+ if (!import_fs35.default.statSync(geminiTmpDir).isDirectory()) return out;
17152
+ dirs = import_fs35.default.readdirSync(geminiTmpDir);
17153
+ } catch {
17154
+ return out;
17155
+ }
17156
+ for (const proj of dirs) {
17157
+ const chatsDir = import_path36.default.join(geminiTmpDir, proj, "chats");
17158
+ let files;
17159
+ try {
17160
+ if (!import_fs35.default.statSync(chatsDir).isDirectory()) continue;
17161
+ files = import_fs35.default.readdirSync(chatsDir);
17162
+ } catch {
17163
+ continue;
17164
+ }
17165
+ for (const f of files) {
17166
+ if (!f.endsWith(".jsonl")) continue;
17167
+ out.push({ projectKey: proj, file: import_path36.default.join(chatsDir, f) });
17168
+ }
17169
+ }
17170
+ return out;
17171
+ }
17172
+ function loadGeminiCost(start, end, geminiTmpDir) {
17173
+ const acc = emptyGeminiAccumulator();
17174
+ if (!import_fs35.default.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
17175
+ for (const { projectKey, file } of listGeminiSessionFiles(geminiTmpDir)) {
17176
+ processGeminiCostFile(file, projectKey, start, end, acc);
17177
+ }
17178
+ return freezeGeminiCost(acc);
17179
+ }
17180
+ function aggregateReportFromAudit(period, opts = {}) {
17181
+ const now = opts.now ?? /* @__PURE__ */ new Date();
17182
+ const auditLogPath = opts.auditLogPath ?? import_path36.default.join(import_os31.default.homedir(), ".node9", "audit.log");
17183
+ const claudeProjectsDir = opts.claudeProjectsDir ?? import_path36.default.join(import_os31.default.homedir(), ".claude", "projects");
17184
+ const codexSessionsDir = opts.codexSessionsDir ?? import_path36.default.join(import_os31.default.homedir(), ".codex", "sessions");
17185
+ const geminiTmpDir = opts.geminiTmpDir ?? import_path36.default.join(import_os31.default.homedir(), ".gemini", "tmp");
17186
+ const hasAuditFile = import_fs35.default.existsSync(auditLogPath);
17187
+ const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath);
17188
+ const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
17189
+ const { start, end } = getDateRange(period, now);
17190
+ const responseDlpEntries = allEntries.filter((e) => {
17191
+ if (e.source !== "response-dlp") return false;
17192
+ const ts = new Date(e.ts);
17193
+ return ts >= start && ts <= end;
17194
+ }).map((e) => {
17195
+ const raw = e;
17196
+ return {
17197
+ ts: e.ts,
17198
+ dlpPattern: typeof raw.dlpPattern === "string" ? raw.dlpPattern : void 0,
17199
+ dlpSample: typeof raw.dlpSample === "string" ? raw.dlpSample : void 0
17200
+ };
17201
+ });
17202
+ const claudeCost = opts.preloadedClaudeCost ?? loadClaudeCost(start, end, claudeProjectsDir);
17203
+ const codexCost = opts.preloadedCodexCost ?? loadCodexCost(start, end, codexSessionsDir);
17204
+ const geminiCost = opts.preloadedGeminiCost ?? loadGeminiCost(start, end, geminiTmpDir);
17205
+ for (const [day, c] of codexCost.byDay) {
17206
+ claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
17207
+ }
17208
+ for (const [day, c] of geminiCost.byDay) {
17209
+ claudeCost.byDay.set(day, (claudeCost.byDay.get(day) ?? 0) + c);
17210
+ }
17211
+ for (const [geminiKey, gRollup] of geminiCost.byProject) {
17212
+ let mergedInto = null;
17213
+ for (const claudeKey of claudeCost.byProject.keys()) {
17214
+ const claudeBase = claudeKey.match(/[^/\\]+$/)?.[0] ?? claudeKey;
17215
+ if (claudeBase === geminiKey) {
17216
+ mergedInto = claudeKey;
17217
+ break;
17218
+ }
17219
+ }
17220
+ const targetKey = mergedInto ?? geminiKey;
17221
+ const existing = claudeCost.byProject.get(targetKey) ?? {
17222
+ cost: 0,
17223
+ inputTokens: 0,
17224
+ outputTokens: 0
17225
+ };
17226
+ existing.cost += gRollup.cost;
17227
+ existing.inputTokens += gRollup.inputTokens;
17228
+ existing.outputTokens += gRollup.outputTokens;
17229
+ claudeCost.byProject.set(targetKey, existing);
17230
+ }
17231
+ const periodMs = end.getTime() - start.getTime();
17232
+ const priorEnd = new Date(start.getTime() - 1);
17233
+ const priorStart = new Date(start.getTime() - periodMs);
17234
+ const priorEntries = allEntries.filter((e) => {
17235
+ if (e.source === "post-hook") return false;
17236
+ const ts = new Date(e.ts);
17237
+ return ts >= priorStart && ts <= priorEnd;
17238
+ });
17239
+ const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
17240
+ const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
17241
+ const excludeTests = opts.excludeTests === true;
17242
+ const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
17243
+ let excludedTests = 0;
17244
+ const entries = allEntries.filter((e) => {
17245
+ if (e.source === "post-hook") return false;
17246
+ if (e.source === "response-dlp") return false;
17247
+ const ts = new Date(e.ts);
17248
+ if (ts < start || ts > end) return false;
17249
+ if (excludeTests && isTestEntry(e, testTs)) {
17250
+ excludedTests++;
17251
+ return false;
17252
+ }
17253
+ return true;
17254
+ });
17255
+ let userApproved = 0;
17256
+ let userDenied = 0;
17257
+ let timedOut = 0;
17258
+ let hardBlocked = 0;
17259
+ let dlpBlocked = 0;
17260
+ let observeDlp = 0;
17261
+ let loopHits = 0;
17262
+ let testPasses = 0;
17263
+ let testFails = 0;
17264
+ const toolMap = /* @__PURE__ */ new Map();
17265
+ const blockMap = /* @__PURE__ */ new Map();
17266
+ const ruleMap = /* @__PURE__ */ new Map();
17267
+ const agentMap = /* @__PURE__ */ new Map();
17268
+ const mcpMap = /* @__PURE__ */ new Map();
17269
+ const dailyMap = /* @__PURE__ */ new Map();
17270
+ const hourMap = /* @__PURE__ */ new Map();
17271
+ for (const e of entries) {
17272
+ const allow = isAllow(e.decision);
17273
+ const dateKey = e.ts.slice(0, 10);
17274
+ const userInteracted = e.source === "daemon";
17275
+ if (userInteracted) {
17276
+ if (allow) userApproved++;
17277
+ else userDenied++;
17278
+ } else if (!allow) {
17279
+ if (e.checkedBy === "timeout") timedOut++;
17280
+ else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
17281
+ else if (isDlp(e.checkedBy)) dlpBlocked++;
17282
+ else if (e.checkedBy !== "loop-detected") hardBlocked++;
17283
+ }
17284
+ if (e.checkedBy === "loop-detected") loopHits++;
17285
+ const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
17286
+ t.calls++;
17287
+ if (!allow) t.blocked++;
17288
+ toolMap.set(e.tool, t);
17289
+ if (!allow && e.checkedBy) {
17290
+ blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
17291
+ }
17292
+ if (!allow && e.ruleName) {
17293
+ ruleMap.set(e.ruleName, (ruleMap.get(e.ruleName) ?? 0) + 1);
17294
+ }
17295
+ if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
17296
+ if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
17297
+ const hour = new Date(e.ts).getHours();
17298
+ hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
17299
+ const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
17300
+ d.calls++;
17301
+ if (!allow) d.blocked++;
17302
+ dailyMap.set(dateKey, d);
17303
+ }
17304
+ for (const e of allEntries) {
17305
+ if (e.source !== "test-result") continue;
17306
+ const ts = new Date(e.ts);
17307
+ if (ts < start || ts > end) continue;
17308
+ if (e.testResult === "pass") testPasses++;
17309
+ else if (e.testResult === "fail") testFails++;
17310
+ }
17311
+ if (codexCost.toolCalls > 0) {
17312
+ agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexCost.toolCalls);
17313
+ }
17314
+ const data = {
17315
+ period,
17316
+ start,
17317
+ end,
17318
+ excludedTests,
17319
+ total: entries.length,
17320
+ userApproved,
17321
+ userDenied,
17322
+ timedOut,
17323
+ hardBlocked,
17324
+ dlpBlocked,
17325
+ observeDlp,
17326
+ loopHits,
17327
+ testPasses,
17328
+ testFails,
17329
+ unackedDlp: unackedDlp.length,
17330
+ priorBlockRate,
17331
+ cost: {
17332
+ claudeUSD: claudeCost.total,
17333
+ codexUSD: codexCost.total,
17334
+ geminiUSD: geminiCost.total,
17335
+ inputTokens: claudeCost.inputTokens + geminiCost.inputTokens,
17336
+ outputTokens: claudeCost.outputTokens + geminiCost.outputTokens,
17337
+ cacheWriteTokens: claudeCost.cacheWriteTokens,
17338
+ cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
17339
+ byDay: claudeCost.byDay,
17340
+ byModel: claudeCost.byModel,
17341
+ byProject: claudeCost.byProject
17342
+ },
17343
+ toolMap,
17344
+ blockMap,
17345
+ ruleMap,
17346
+ agentMap,
17347
+ mcpMap,
17348
+ dailyMap,
17349
+ hourMap,
17350
+ generatedAt: now.toISOString()
17351
+ };
17352
+ return { data, hasAuditFile, responseDlpEntries };
17353
+ }
17354
+
17355
+ // src/cli/render/report-json.ts
17356
+ function buildReportJson(input) {
17357
+ const totalBlocked = input.timedOut + input.hardBlocked + input.dlpBlocked + input.loopHits + input.userDenied;
17358
+ const blockRate = input.total > 0 ? totalBlocked / input.total : 0;
17359
+ const deltaPct = input.priorBlockRate === null ? null : Math.round((blockRate - input.priorBlockRate) * 100);
17360
+ return {
17361
+ schemaVersion: 1,
17362
+ generatedAt: input.generatedAt,
17363
+ period: input.period,
17364
+ range: { start: input.start.toISOString(), end: input.end.toISOString() },
17365
+ excludedTests: input.excludedTests,
17366
+ totals: {
17367
+ events: input.total,
17368
+ blocked: totalBlocked,
17369
+ blockRate,
17370
+ userApproved: input.userApproved,
17371
+ userDenied: input.userDenied,
17372
+ timedOut: input.timedOut,
17373
+ hardBlocked: input.hardBlocked,
17374
+ dlpBlocked: input.dlpBlocked,
17375
+ observeDlp: input.observeDlp,
17376
+ loopHits: input.loopHits,
17377
+ unackedDlp: input.unackedDlp
17378
+ },
17379
+ tests: {
17380
+ passes: input.testPasses,
17381
+ fails: input.testFails
17382
+ },
17383
+ cost: {
17384
+ totalUSD: input.cost.claudeUSD + input.cost.codexUSD + input.cost.geminiUSD,
17385
+ claudeUSD: input.cost.claudeUSD,
17386
+ codexUSD: input.cost.codexUSD,
17387
+ geminiUSD: input.cost.geminiUSD,
17388
+ inputTokens: input.cost.inputTokens,
17389
+ outputTokens: input.cost.outputTokens,
17390
+ cacheWriteTokens: input.cost.cacheWriteTokens,
17391
+ cacheReadTokens: input.cost.cacheReadTokens,
17392
+ byDay: [...input.cost.byDay.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, usd]) => ({ day, usd })),
17393
+ byModel: [...input.cost.byModel.entries()].sort((a, b) => b[1] - a[1]).map(([model, usd]) => ({ model, usd }))
17394
+ },
17395
+ byTool: [...input.toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).map(([tool, v]) => ({ tool, calls: v.calls, blocked: v.blocked })),
17396
+ byBlock: [...input.blockMap.entries()].sort((a, b) => b[1] - a[1]).map(([rule, count]) => ({ rule, count })),
17397
+ byAgent: [...input.agentMap.entries()].sort((a, b) => b[1] - a[1]).map(([agent, calls]) => ({ agent, calls })),
17398
+ byMcp: [...input.mcpMap.entries()].sort((a, b) => b[1] - a[1]).map(([server, calls]) => ({ server, calls })),
17399
+ byDay: [...input.dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([day, v]) => ({ day, calls: v.calls, blocked: v.blocked })),
17400
+ byHour: [...input.hourMap.entries()].sort((a, b) => a[0] - b[0]).map(([hour, calls]) => ({ hour, calls })),
17401
+ trend: {
17402
+ priorBlockRate: input.priorBlockRate,
17403
+ deltaPct
17404
+ }
17405
+ };
17406
+ }
17407
+
17408
+ // src/cli/commands/report.ts
17409
+ var BLOCK_REASON_LABELS = {
17410
+ timeout: "Approval timeout",
17411
+ "smart-rule-block": "Smart rule",
17412
+ "observe-mode-dlp-would-block": "DLP (observe)",
17413
+ "persistent-deny": "Persistent deny",
17414
+ "local-decision": "User denied",
17415
+ "dlp-block": "DLP block",
17416
+ "loop-detected": "Loop detected"
17417
+ };
17418
+ function humanBlockReason(reason) {
17419
+ return BLOCK_REASON_LABELS[reason] ?? reason;
17420
+ }
17421
+ function barStr(value, max, width) {
17422
+ if (max === 0 || width <= 0) return "\u2591".repeat(width);
17423
+ const filled = Math.max(1, Math.round(value / max * width));
17424
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
17425
+ }
17426
+ function colorBar(value, max, width) {
17427
+ const s = barStr(value, max, width);
17428
+ const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
17429
+ return import_chalk13.default.cyan(s.slice(0, filled)) + import_chalk13.default.dim(s.slice(filled));
17430
+ }
17431
+ function fmtDate(d) {
17432
+ const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
17433
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
17434
+ }
17435
+ function num2(n) {
17436
+ return n.toLocaleString();
17437
+ }
17438
+ function fmtCost2(usd) {
17439
+ if (usd < 1e-3) return "< $0.001";
17440
+ if (usd < 1) return "$" + usd.toFixed(4);
17441
+ return "$" + usd.toFixed(2);
16539
17442
  }
16540
17443
  function registerReportCommand(program2) {
16541
17444
  program2.command("report").description("Activity and security report \u2014 what Claude did, what was blocked").option("--period <period>", "today | 7d | 30d | month", "7d").option("--no-tests", "exclude test runner calls (npm test, vitest, pytest\u2026) from stats").option("--json", "Emit machine-readable JSON to stdout (suppresses renderer)").action((options) => {
16542
- const period = ["today", "7d", "30d", "month"].includes(
17445
+ const period = ["today", "7d", "30d", "90d", "month"].includes(
16543
17446
  options.period
16544
17447
  ) ? options.period : "7d";
16545
- const logPath = import_path36.default.join(import_os31.default.homedir(), ".node9", "audit.log");
16546
- const allEntries = parseAuditLog(logPath);
16547
- const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
16548
- if (unackedDlp.length > 0 && !options.json) {
17448
+ const excludeTests = options.tests === false;
17449
+ const { data, hasAuditFile, responseDlpEntries } = aggregateReportFromAudit(period, {
17450
+ excludeTests
17451
+ });
17452
+ if (data.unackedDlp > 0 && !options.json) {
16549
17453
  console.log("");
16550
17454
  console.log(
16551
17455
  import_chalk13.default.bgRed.white.bold(
16552
- ` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
17456
+ ` \u26A0\uFE0F DLP ALERT: ${data.unackedDlp} secret${data.unackedDlp !== 1 ? "s" : ""} found in Claude response text `
16553
17457
  ) + " " + import_chalk13.default.yellow("\u2192 run: node9 dlp")
16554
17458
  );
16555
17459
  }
16556
- if (allEntries.length === 0 && !options.json) {
17460
+ if (!hasAuditFile && !options.json) {
16557
17461
  console.log(
16558
17462
  import_chalk13.default.yellow("\n No audit data found. Run node9 with Claude Code to generate entries.\n")
16559
17463
  );
16560
17464
  return;
16561
17465
  }
16562
- const { start, end } = getDateRange(period);
16563
- const {
16564
- total: claudeCostUSD,
16565
- byDay: costByDay,
16566
- byModel: costByModel,
16567
- inputTokens: costInputTokens,
16568
- outputTokens: costOutputTokens,
16569
- cacheWriteTokens: costCacheWrite,
16570
- cacheReadTokens: costCacheRead
16571
- } = loadClaudeCost(start, end);
16572
- const {
16573
- total: codexCostUSD,
16574
- byDay: codexCostByDay,
16575
- toolCalls: codexToolCalls
16576
- } = loadCodexCost(start, end);
16577
- const costUSD = claudeCostUSD + codexCostUSD;
16578
- for (const [day, c] of codexCostByDay) {
16579
- costByDay.set(day, (costByDay.get(day) ?? 0) + c);
16580
- }
16581
- const periodMs = end.getTime() - start.getTime();
16582
- const priorEnd = new Date(start.getTime() - 1);
16583
- const priorStart = new Date(start.getTime() - periodMs);
16584
- const priorEntries = allEntries.filter((e) => {
16585
- if (e.source === "post-hook") return false;
16586
- const ts = new Date(e.ts);
16587
- return ts >= priorStart && ts <= priorEnd;
16588
- });
16589
- const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
16590
- const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
16591
- const excludeTests = options.tests === false;
16592
- const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
16593
- let filteredTestCount = 0;
16594
- const entries = allEntries.filter((e) => {
16595
- if (e.source === "post-hook") return false;
16596
- if (e.source === "response-dlp") return false;
16597
- const ts = new Date(e.ts);
16598
- if (ts < start || ts > end) return false;
16599
- if (excludeTests && isTestEntry(e, testTs)) {
16600
- filteredTestCount++;
16601
- return false;
16602
- }
16603
- return true;
16604
- });
16605
- if (entries.length === 0 && !options.json) {
17466
+ if (options.json) {
17467
+ const envelope = buildReportJson(data);
17468
+ process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
17469
+ return;
17470
+ }
17471
+ if (data.total === 0) {
16606
17472
  console.log(import_chalk13.default.yellow(`
16607
17473
  No activity for period "${period}".
16608
17474
  `));
16609
17475
  return;
16610
17476
  }
16611
- let userApproved = 0;
16612
- let userDenied = 0;
16613
- let timedOut = 0;
16614
- let hardBlocked = 0;
16615
- let dlpBlocked = 0;
16616
- let observeDlp = 0;
16617
- let loopHits = 0;
16618
- let testPasses = 0;
16619
- let testFails = 0;
16620
- const toolMap = /* @__PURE__ */ new Map();
16621
- const blockMap = /* @__PURE__ */ new Map();
16622
- const agentMap = /* @__PURE__ */ new Map();
16623
- const mcpMap = /* @__PURE__ */ new Map();
16624
- const dailyMap = /* @__PURE__ */ new Map();
16625
- const hourMap = /* @__PURE__ */ new Map();
16626
- for (const e of entries) {
16627
- const allow = isAllow(e.decision);
16628
- const dateKey = e.ts.slice(0, 10);
16629
- const userInteracted = e.source === "daemon";
16630
- if (userInteracted) {
16631
- if (allow) userApproved++;
16632
- else userDenied++;
16633
- } else if (!allow) {
16634
- if (e.checkedBy === "timeout") timedOut++;
16635
- else if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
16636
- else if (isDlp(e.checkedBy)) dlpBlocked++;
16637
- else if (e.checkedBy !== "loop-detected") hardBlocked++;
16638
- }
16639
- if (e.checkedBy === "loop-detected") loopHits++;
16640
- const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
16641
- t.calls++;
16642
- if (!allow) t.blocked++;
16643
- toolMap.set(e.tool, t);
16644
- if (!allow && e.checkedBy) {
16645
- blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
16646
- }
16647
- if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
16648
- if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
16649
- const hour = new Date(e.ts).getHours();
16650
- hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
16651
- const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
16652
- d.calls++;
16653
- if (!allow) d.blocked++;
16654
- dailyMap.set(dateKey, d);
16655
- }
16656
- for (const e of allEntries) {
16657
- if (e.source !== "test-result") continue;
16658
- const ts = new Date(e.ts);
16659
- if (ts < start || ts > end) continue;
16660
- if (e.testResult === "pass") testPasses++;
16661
- else if (e.testResult === "fail") testFails++;
16662
- }
16663
- if (codexToolCalls > 0) agentMap.set("Codex", (agentMap.get("Codex") ?? 0) + codexToolCalls);
16664
- if (options.json) {
16665
- const envelope = buildReportJson({
16666
- period,
16667
- start,
16668
- end,
16669
- excludedTests: filteredTestCount,
16670
- total: entries.length,
16671
- userApproved,
16672
- userDenied,
16673
- timedOut,
16674
- hardBlocked,
16675
- dlpBlocked,
16676
- observeDlp,
16677
- loopHits,
16678
- testPasses,
16679
- testFails,
16680
- unackedDlp: unackedDlp.length,
16681
- priorBlockRate,
16682
- cost: {
16683
- claudeUSD: claudeCostUSD,
16684
- codexUSD: codexCostUSD,
16685
- inputTokens: costInputTokens,
16686
- outputTokens: costOutputTokens,
16687
- cacheWriteTokens: costCacheWrite,
16688
- cacheReadTokens: costCacheRead,
16689
- byDay: costByDay,
16690
- byModel: costByModel
16691
- },
16692
- toolMap,
16693
- blockMap,
16694
- agentMap,
16695
- mcpMap,
16696
- dailyMap,
16697
- hourMap,
16698
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
16699
- });
16700
- process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
16701
- return;
16702
- }
16703
- const total = entries.length;
16704
- const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
16705
- const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
16706
- const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
16707
- const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
16708
- const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
16709
- const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
16710
- const W = Math.min(process.stdout.columns || 80, 100);
16711
- const INNER = W - 4;
16712
- const COL = Math.floor(INNER / 2) - 1;
16713
- const LABEL = 24;
16714
- const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
16715
- const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num2(v.calls).length), 1);
16716
- const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num2(v).length), 1);
16717
- const line = import_chalk13.default.dim("\u2500".repeat(W - 2));
16718
- const periodLabel = {
16719
- today: "Today",
16720
- "7d": "Last 7 Days",
16721
- "30d": "Last 30 Days",
16722
- month: "This Month"
16723
- };
17477
+ renderTerminalReport(data, responseDlpEntries, excludeTests);
17478
+ });
17479
+ }
17480
+ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
17481
+ const {
17482
+ period,
17483
+ start,
17484
+ end,
17485
+ total,
17486
+ excludedTests,
17487
+ userApproved,
17488
+ userDenied,
17489
+ timedOut,
17490
+ hardBlocked,
17491
+ dlpBlocked,
17492
+ observeDlp,
17493
+ loopHits,
17494
+ testPasses,
17495
+ testFails,
17496
+ priorBlockRate,
17497
+ cost: {
17498
+ claudeUSD,
17499
+ codexUSD,
17500
+ geminiUSD,
17501
+ inputTokens: costInputTokens,
17502
+ outputTokens: costOutputTokens,
17503
+ cacheWriteTokens: costCacheWrite,
17504
+ cacheReadTokens: costCacheRead,
17505
+ byDay: costByDay,
17506
+ byModel: costByModel
17507
+ },
17508
+ toolMap,
17509
+ blockMap,
17510
+ agentMap,
17511
+ mcpMap,
17512
+ dailyMap,
17513
+ hourMap
17514
+ } = data;
17515
+ const costUSD = claudeUSD + codexUSD + geminiUSD;
17516
+ const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
17517
+ const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
17518
+ const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
17519
+ const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
17520
+ const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
17521
+ const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
17522
+ const W = Math.min(process.stdout.columns || 80, 100);
17523
+ const INNER = W - 4;
17524
+ const COL = Math.floor(INNER / 2) - 1;
17525
+ const LABEL = 24;
17526
+ const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
17527
+ const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num2(v.calls).length), 1);
17528
+ const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num2(v).length), 1);
17529
+ const line = import_chalk13.default.dim("\u2500".repeat(W - 2));
17530
+ const periodLabel = {
17531
+ today: "Today",
17532
+ "7d": "Last 7 Days",
17533
+ "30d": "Last 30 Days",
17534
+ "90d": "Last 90 Days",
17535
+ month: "This Month"
17536
+ };
17537
+ console.log("");
17538
+ console.log(
17539
+ " " + import_chalk13.default.bold.cyan("\u{1F6E1} node9 Report") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.white(periodLabel[period]) + import_chalk13.default.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + import_chalk13.default.dim(` ${num2(total)} events`) + (excludeTests ? import_chalk13.default.dim(` \u2013tests (\u2013${excludedTests})`) : "")
17540
+ );
17541
+ console.log(" " + line);
17542
+ const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
17543
+ const currentRate = total > 0 ? totalBlocked / total : 0;
17544
+ const trendLabel = (() => {
17545
+ if (priorBlockRate === null) return "";
17546
+ const delta = Math.round((currentRate - priorBlockRate) * 100);
17547
+ if (delta === 0) return "";
17548
+ return " " + (delta > 0 ? import_chalk13.default.red(`\u25B2${delta}%`) : import_chalk13.default.green(`\u25BC${Math.abs(delta)}%`)) + import_chalk13.default.dim(" vs prior");
17549
+ })();
17550
+ const reads = toolMap.get("Read")?.calls ?? 0;
17551
+ const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
17552
+ const ratioLabel = reads > 0 ? import_chalk13.default.dim(`edit/read ${(edits / reads).toFixed(1)}`) : import_chalk13.default.dim("edit/read \u2013");
17553
+ const testLabel = testPasses + testFails > 0 ? import_chalk13.default.dim("tests ") + import_chalk13.default.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + import_chalk13.default.red(`${testFails}\u2717`) : "") : import_chalk13.default.dim("tests \u2013");
17554
+ console.log("");
17555
+ console.log(" " + import_chalk13.default.bold("Protection Summary"));
17556
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
17557
+ console.log(
17558
+ " " + import_chalk13.default.dim("Intercepted") + " " + import_chalk13.default.white(num2(total)) + import_chalk13.default.dim(" tool calls")
17559
+ );
17560
+ console.log("");
17561
+ const COL1 = 18;
17562
+ const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
17563
+ const countStr = colorFn(num2(count));
17564
+ const noteStr = note ? import_chalk13.default.dim(" " + note) : "";
17565
+ console.log(" " + icon + " " + import_chalk13.default.white(label.padEnd(COL1)) + countStr + noteStr);
17566
+ };
17567
+ summaryRow(
17568
+ userApproved > 0 ? import_chalk13.default.green("\u2705") : import_chalk13.default.dim("\u2705"),
17569
+ "User approved",
17570
+ userApproved,
17571
+ userApproved === 0 ? "no popups this period" : void 0,
17572
+ userApproved > 0 ? (s) => import_chalk13.default.green(s) : (s) => import_chalk13.default.dim(s)
17573
+ );
17574
+ summaryRow(
17575
+ userDenied > 0 ? import_chalk13.default.red("\u{1F6AB}") : import_chalk13.default.dim("\u{1F6AB}"),
17576
+ "User denied",
17577
+ userDenied,
17578
+ void 0,
17579
+ userDenied > 0 ? (s) => import_chalk13.default.red(s) : (s) => import_chalk13.default.dim(s)
17580
+ );
17581
+ summaryRow(
17582
+ timedOut > 0 ? import_chalk13.default.yellow("\u23F1") : import_chalk13.default.dim("\u23F1"),
17583
+ "Timed out",
17584
+ timedOut,
17585
+ timedOut > 0 ? "no approval response" : void 0,
17586
+ timedOut > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
17587
+ );
17588
+ summaryRow(
17589
+ hardBlocked > 0 ? import_chalk13.default.red("\u{1F6D1}") : import_chalk13.default.dim("\u{1F6D1}"),
17590
+ "Auto-blocked",
17591
+ hardBlocked,
17592
+ void 0,
17593
+ hardBlocked > 0 ? (s) => import_chalk13.default.red(s) : (s) => import_chalk13.default.dim(s)
17594
+ );
17595
+ summaryRow(
17596
+ dlpBlocked > 0 ? import_chalk13.default.yellow("\u{1F6A8}") : import_chalk13.default.dim("\u{1F6A8}"),
17597
+ "DLP blocked",
17598
+ dlpBlocked,
17599
+ void 0,
17600
+ dlpBlocked > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
17601
+ );
17602
+ summaryRow(
17603
+ observeDlp > 0 ? import_chalk13.default.blue("\u{1F441}") : import_chalk13.default.dim("\u{1F441}"),
17604
+ "DLP (observe)",
17605
+ observeDlp,
17606
+ observeDlp > 0 ? "would-block in strict mode" : void 0,
17607
+ observeDlp > 0 ? (s) => import_chalk13.default.blue(s) : (s) => import_chalk13.default.dim(s)
17608
+ );
17609
+ summaryRow(
17610
+ loopHits > 0 ? import_chalk13.default.yellow("\u{1F504}") : import_chalk13.default.dim("\u{1F504}"),
17611
+ "Loops detected",
17612
+ loopHits,
17613
+ void 0,
17614
+ loopHits > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
17615
+ );
17616
+ if (trendLabel || ratioLabel || testPasses + testFails > 0) {
16724
17617
  console.log("");
16725
- console.log(
16726
- " " + import_chalk13.default.bold.cyan("\u{1F6E1} node9 Report") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.white(periodLabel[period]) + import_chalk13.default.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + import_chalk13.default.dim(` ${num2(total)} events`) + (excludeTests ? import_chalk13.default.dim(` \u2013tests (\u2013${filteredTestCount})`) : "")
16727
- );
16728
- console.log(" " + line);
16729
- const totalBlocked = timedOut + hardBlocked + dlpBlocked + loopHits + userDenied;
16730
- const currentRate = total > 0 ? totalBlocked / total : 0;
16731
- const trendLabel = (() => {
16732
- if (priorBlockRate === null) return "";
16733
- const delta = Math.round((currentRate - priorBlockRate) * 100);
16734
- if (delta === 0) return "";
16735
- return " " + (delta > 0 ? import_chalk13.default.red(`\u25B2${delta}%`) : import_chalk13.default.green(`\u25BC${Math.abs(delta)}%`)) + import_chalk13.default.dim(" vs prior");
16736
- })();
16737
- const reads = toolMap.get("Read")?.calls ?? 0;
16738
- const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
16739
- const ratioLabel = reads > 0 ? import_chalk13.default.dim(`edit/read ${(edits / reads).toFixed(1)}`) : import_chalk13.default.dim("edit/read \u2013");
16740
- const testLabel = testPasses + testFails > 0 ? import_chalk13.default.dim("tests ") + import_chalk13.default.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + import_chalk13.default.red(`${testFails}\u2717`) : "") : import_chalk13.default.dim("tests \u2013");
17618
+ console.log(" " + ratioLabel + " " + testLabel + trendLabel);
17619
+ }
17620
+ console.log("");
17621
+ const toolHeaderRaw = "Top Tools";
17622
+ const blockHeaderRaw = "Top Blocks";
17623
+ console.log(
17624
+ " " + import_chalk13.default.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + import_chalk13.default.bold(blockHeaderRaw)
17625
+ );
17626
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(COL)) + " " + import_chalk13.default.dim("\u2500".repeat(COL)));
17627
+ const rows = Math.max(topTools.length, topBlocks.length, 1);
17628
+ for (let i = 0; i < rows; i++) {
17629
+ let leftStyled = " ".repeat(COL);
17630
+ if (i < topTools.length) {
17631
+ const [tool, { calls }] = topTools[i];
17632
+ const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
17633
+ const countStr = num2(calls).padStart(TOOL_COUNT_W);
17634
+ const b = colorBar(calls, maxTool, BAR);
17635
+ const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
17636
+ const pad = Math.max(0, COL - rawLen);
17637
+ leftStyled = import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.white(countStr) + " ".repeat(pad);
17638
+ }
17639
+ let rightStyled = "";
17640
+ if (i < topBlocks.length) {
17641
+ const [reason, count] = topBlocks[i];
17642
+ const readable = humanBlockReason(reason);
17643
+ const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
17644
+ const countStr = num2(count).padStart(BLOCK_COUNT_W);
17645
+ const b = colorBar(count, maxBlock, BAR);
17646
+ rightStyled = import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.red(countStr);
17647
+ }
17648
+ console.log(" " + leftStyled + " " + rightStyled);
17649
+ }
17650
+ if (topBlocks.length === 0) {
17651
+ console.log(" " + " ".repeat(COL) + " " + import_chalk13.default.dim("nothing blocked \u2713"));
17652
+ }
17653
+ if (agentMap.size >= 1) {
16741
17654
  console.log("");
16742
- console.log(" " + import_chalk13.default.bold("Protection Summary"));
17655
+ console.log(" " + import_chalk13.default.bold("Agents"));
16743
17656
  console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
16744
- console.log(
16745
- " " + import_chalk13.default.dim("Intercepted") + " " + import_chalk13.default.white(num2(total)) + import_chalk13.default.dim(" tool calls")
16746
- );
16747
- console.log("");
16748
- const COL1 = 18;
16749
- const summaryRow = (icon, label, count, note, colorFn = (s) => s) => {
16750
- const countStr = colorFn(num2(count));
16751
- const noteStr = note ? import_chalk13.default.dim(" " + note) : "";
16752
- console.log(" " + icon + " " + import_chalk13.default.white(label.padEnd(COL1)) + countStr + noteStr);
16753
- };
16754
- summaryRow(
16755
- userApproved > 0 ? import_chalk13.default.green("\u2705") : import_chalk13.default.dim("\u2705"),
16756
- "User approved",
16757
- userApproved,
16758
- userApproved === 0 ? "no popups this period" : void 0,
16759
- userApproved > 0 ? (s) => import_chalk13.default.green(s) : (s) => import_chalk13.default.dim(s)
16760
- );
16761
- summaryRow(
16762
- userDenied > 0 ? import_chalk13.default.red("\u{1F6AB}") : import_chalk13.default.dim("\u{1F6AB}"),
16763
- "User denied",
16764
- userDenied,
16765
- void 0,
16766
- userDenied > 0 ? (s) => import_chalk13.default.red(s) : (s) => import_chalk13.default.dim(s)
16767
- );
16768
- summaryRow(
16769
- timedOut > 0 ? import_chalk13.default.yellow("\u23F1") : import_chalk13.default.dim("\u23F1"),
16770
- "Timed out",
16771
- timedOut,
16772
- timedOut > 0 ? "no approval response" : void 0,
16773
- timedOut > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
16774
- );
16775
- summaryRow(
16776
- hardBlocked > 0 ? import_chalk13.default.red("\u{1F6D1}") : import_chalk13.default.dim("\u{1F6D1}"),
16777
- "Auto-blocked",
16778
- hardBlocked,
16779
- void 0,
16780
- hardBlocked > 0 ? (s) => import_chalk13.default.red(s) : (s) => import_chalk13.default.dim(s)
16781
- );
16782
- summaryRow(
16783
- dlpBlocked > 0 ? import_chalk13.default.yellow("\u{1F6A8}") : import_chalk13.default.dim("\u{1F6A8}"),
16784
- "DLP blocked",
16785
- dlpBlocked,
16786
- void 0,
16787
- dlpBlocked > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
16788
- );
16789
- summaryRow(
16790
- observeDlp > 0 ? import_chalk13.default.blue("\u{1F441}") : import_chalk13.default.dim("\u{1F441}"),
16791
- "DLP (observe)",
16792
- observeDlp,
16793
- observeDlp > 0 ? "would-block in strict mode" : void 0,
16794
- observeDlp > 0 ? (s) => import_chalk13.default.blue(s) : (s) => import_chalk13.default.dim(s)
16795
- );
16796
- summaryRow(
16797
- loopHits > 0 ? import_chalk13.default.yellow("\u{1F504}") : import_chalk13.default.dim("\u{1F504}"),
16798
- "Loops detected",
16799
- loopHits,
16800
- void 0,
16801
- loopHits > 0 ? (s) => import_chalk13.default.yellow(s) : (s) => import_chalk13.default.dim(s)
16802
- );
16803
- if (trendLabel || ratioLabel || testPasses + testFails > 0) {
16804
- console.log("");
16805
- console.log(" " + ratioLabel + " " + testLabel + trendLabel);
17657
+ const maxAgent = Math.max(...agentMap.values(), 1);
17658
+ for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
17659
+ const label = agent.slice(0, LABEL - 1);
17660
+ const b = colorBar(count, maxAgent, BAR);
17661
+ console.log(" " + import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.white(num2(count)));
16806
17662
  }
17663
+ }
17664
+ if (mcpMap.size > 0) {
16807
17665
  console.log("");
16808
- const toolHeaderRaw = "Top Tools";
16809
- const blockHeaderRaw = "Top Blocks";
16810
- console.log(
16811
- " " + import_chalk13.default.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + import_chalk13.default.bold(blockHeaderRaw)
16812
- );
16813
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(COL)) + " " + import_chalk13.default.dim("\u2500".repeat(COL)));
16814
- const rows = Math.max(topTools.length, topBlocks.length, 1);
16815
- for (let i = 0; i < rows; i++) {
16816
- let leftStyled = " ".repeat(COL);
16817
- if (i < topTools.length) {
16818
- const [tool, { calls }] = topTools[i];
16819
- const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
16820
- const countStr = num2(calls).padStart(TOOL_COUNT_W);
16821
- const b = colorBar(calls, maxTool, BAR);
16822
- const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
16823
- const pad = Math.max(0, COL - rawLen);
16824
- leftStyled = import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.white(countStr) + " ".repeat(pad);
16825
- }
16826
- let rightStyled = "";
16827
- if (i < topBlocks.length) {
16828
- const [reason, count] = topBlocks[i];
16829
- const readable = humanBlockReason(reason);
16830
- const label = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
16831
- const countStr = num2(count).padStart(BLOCK_COUNT_W);
16832
- const b = colorBar(count, maxBlock, BAR);
16833
- rightStyled = import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.red(countStr);
16834
- }
16835
- console.log(" " + leftStyled + " " + rightStyled);
16836
- }
16837
- if (topBlocks.length === 0) {
16838
- console.log(" " + " ".repeat(COL) + " " + import_chalk13.default.dim("nothing blocked \u2713"));
16839
- }
16840
- if (agentMap.size >= 1) {
16841
- console.log("");
16842
- console.log(" " + import_chalk13.default.bold("Agents"));
16843
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
16844
- const maxAgent = Math.max(...agentMap.values(), 1);
16845
- for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
16846
- const label = agent.slice(0, LABEL - 1);
16847
- const b = colorBar(count, maxAgent, BAR);
16848
- console.log(" " + import_chalk13.default.white(label.padEnd(LABEL)) + b + " " + import_chalk13.default.white(num2(count)));
16849
- }
16850
- }
16851
- if (mcpMap.size > 0) {
16852
- console.log("");
16853
- console.log(" " + import_chalk13.default.bold("MCP Servers"));
16854
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
16855
- const maxMcp = Math.max(...mcpMap.values(), 1);
16856
- for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
16857
- const label = server.slice(0, LABEL - 1).padEnd(LABEL);
16858
- const b = colorBar(count, maxMcp, BAR);
16859
- console.log(" " + import_chalk13.default.white(label) + b + " " + import_chalk13.default.white(num2(count)));
16860
- }
16861
- }
16862
- if (hourMap.size > 0) {
16863
- const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
16864
- const maxHour = Math.max(...hourMap.values(), 1);
16865
- const bar = Array.from({ length: 24 }, (_, h) => {
16866
- const v = hourMap.get(h) ?? 0;
16867
- return BLOCKS[Math.round(v / maxHour * 8)];
16868
- }).join("");
16869
- console.log("");
16870
- console.log(" " + import_chalk13.default.bold("Hour of Day") + import_chalk13.default.dim(" (local, 0h \u2013 23h)"));
16871
- console.log(" " + import_chalk13.default.cyan(bar));
16872
- console.log(" " + import_chalk13.default.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
16873
- }
16874
- if (dailyList.length > 1) {
16875
- console.log("");
16876
- console.log(" " + import_chalk13.default.bold("Daily Activity"));
16877
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(W - 2)));
16878
- const DAY_BAR = Math.max(8, Math.min(30, W - 36));
16879
- for (const [dateKey, { calls, blocked: db }] of dailyList) {
16880
- const label = fmtDate(dateKey).padEnd(10);
16881
- const b = colorBar(calls, maxDaily, DAY_BAR);
16882
- const dayCost = costByDay.get(dateKey);
16883
- const costNote = dayCost ? import_chalk13.default.magenta(` ${fmtCost2(dayCost)}`) : "";
16884
- const blockNote = db > 0 ? import_chalk13.default.red(` ${db} blocked`) : "";
16885
- console.log(
16886
- " " + import_chalk13.default.dim(label) + " " + b + " " + import_chalk13.default.white(num2(calls)) + blockNote + costNote
16887
- );
16888
- }
16889
- }
16890
- const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
16891
- if (totalTokens > 0) {
16892
- const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
16893
- console.log("");
16894
- console.log(" " + import_chalk13.default.bold("Tokens") + " " + import_chalk13.default.dim(`${num2(totalTokens)} total`));
16895
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
16896
- const TOK_BAR = Math.max(6, Math.min(20, W - 30));
16897
- const TOK_LABEL = 14;
16898
- const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
16899
- const nonCacheRows = [
16900
- ["Input", costInputTokens, import_chalk13.default.cyan(num2(costInputTokens))],
16901
- ["Output", costOutputTokens, import_chalk13.default.white(num2(costOutputTokens))],
16902
- ["Cache write", costCacheWrite, import_chalk13.default.yellow(num2(costCacheWrite))]
16903
- ];
16904
- for (const [label, count, colored] of nonCacheRows) {
16905
- if (count === 0) continue;
16906
- const b = colorBar(count, maxNonCache, TOK_BAR);
16907
- console.log(" " + import_chalk13.default.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
16908
- }
16909
- if (costCacheRead > 0) {
16910
- const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
16911
- const pct = cacheHitPct > 0 ? import_chalk13.default.dim(` ${cacheHitPct}% hit rate`) : "";
16912
- console.log(
16913
- " " + import_chalk13.default.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + import_chalk13.default.green(num2(costCacheRead)) + pct
16914
- );
16915
- }
16916
- }
16917
- if (costUSD > 0) {
16918
- const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
16919
- const avgPerDay = costUSD / periodDays;
16920
- const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
16921
- const costHeaderRight = [
16922
- import_chalk13.default.yellow(fmtCost2(costUSD)),
16923
- import_chalk13.default.dim(`avg ${fmtCost2(avgPerDay)}/day`),
16924
- cacheHitPct > 0 ? import_chalk13.default.dim(`${cacheHitPct}% cache hit`) : null
16925
- ].filter(Boolean).join(import_chalk13.default.dim(" \xB7 "));
16926
- console.log("");
16927
- console.log(" " + import_chalk13.default.bold("Cost") + " " + costHeaderRight);
16928
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
16929
- if (codexCostUSD > 0)
16930
- costByModel.set(
16931
- "codex (openai)",
16932
- (costByModel.get("codex (openai)") ?? 0) + codexCostUSD
16933
- );
16934
- const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
16935
- const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
16936
- const MODEL_LABEL = 22;
16937
- const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
16938
- for (const [model, cost] of modelList) {
16939
- const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
16940
- const b = colorBar(cost, maxModelCost, MODEL_BAR);
16941
- console.log(
16942
- " " + import_chalk13.default.white(label.padEnd(MODEL_LABEL)) + b + " " + import_chalk13.default.yellow(fmtCost2(cost))
16943
- );
16944
- }
17666
+ console.log(" " + import_chalk13.default.bold("MCP Servers"));
17667
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
17668
+ const maxMcp = Math.max(...mcpMap.values(), 1);
17669
+ for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
17670
+ const label = server.slice(0, LABEL - 1).padEnd(LABEL);
17671
+ const b = colorBar(count, maxMcp, BAR);
17672
+ console.log(" " + import_chalk13.default.white(label) + b + " " + import_chalk13.default.white(num2(count)));
17673
+ }
17674
+ }
17675
+ if (hourMap.size > 0) {
17676
+ const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
17677
+ const maxHour = Math.max(...hourMap.values(), 1);
17678
+ const bar = Array.from({ length: 24 }, (_, h) => {
17679
+ const v = hourMap.get(h) ?? 0;
17680
+ return BLOCKS[Math.round(v / maxHour * 8)];
17681
+ }).join("");
17682
+ console.log("");
17683
+ console.log(" " + import_chalk13.default.bold("Hour of Day") + import_chalk13.default.dim(" (local, 0h \u2013 23h)"));
17684
+ console.log(" " + import_chalk13.default.cyan(bar));
17685
+ console.log(" " + import_chalk13.default.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
17686
+ }
17687
+ if (dailyList.length > 1) {
17688
+ console.log("");
17689
+ console.log(" " + import_chalk13.default.bold("Daily Activity"));
17690
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(W - 2)));
17691
+ const DAY_BAR = Math.max(8, Math.min(30, W - 36));
17692
+ for (const [dateKey, { calls, blocked: db }] of dailyList) {
17693
+ const label = fmtDate(dateKey).padEnd(10);
17694
+ const b = colorBar(calls, maxDaily, DAY_BAR);
17695
+ const dayCost = costByDay.get(dateKey);
17696
+ const costNote = dayCost ? import_chalk13.default.magenta(` ${fmtCost2(dayCost)}`) : "";
17697
+ const blockNote = db > 0 ? import_chalk13.default.red(` ${db} blocked`) : "";
17698
+ console.log(
17699
+ " " + import_chalk13.default.dim(label) + " " + b + " " + import_chalk13.default.white(num2(calls)) + blockNote + costNote
17700
+ );
16945
17701
  }
16946
- const responseDlpEntries = allEntries.filter((e) => {
16947
- if (e.source !== "response-dlp") return false;
16948
- const ts = new Date(e.ts);
16949
- return ts >= start && ts <= end;
16950
- });
16951
- if (responseDlpEntries.length > 0) {
16952
- console.log("");
17702
+ }
17703
+ const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
17704
+ if (totalTokens > 0) {
17705
+ const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
17706
+ console.log("");
17707
+ console.log(" " + import_chalk13.default.bold("Tokens") + " " + import_chalk13.default.dim(`${num2(totalTokens)} total`));
17708
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
17709
+ const TOK_BAR = Math.max(6, Math.min(20, W - 30));
17710
+ const TOK_LABEL = 14;
17711
+ const maxNonCache = Math.max(costInputTokens, costOutputTokens, costCacheWrite, 1);
17712
+ const nonCacheRows = [
17713
+ ["Input", costInputTokens, import_chalk13.default.cyan(num2(costInputTokens))],
17714
+ ["Output", costOutputTokens, import_chalk13.default.white(num2(costOutputTokens))],
17715
+ ["Cache write", costCacheWrite, import_chalk13.default.yellow(num2(costCacheWrite))]
17716
+ ];
17717
+ for (const [label, count, colored] of nonCacheRows) {
17718
+ if (count === 0) continue;
17719
+ const b = colorBar(count, maxNonCache, TOK_BAR);
17720
+ console.log(" " + import_chalk13.default.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
17721
+ }
17722
+ if (costCacheRead > 0) {
17723
+ const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
17724
+ const pct = cacheHitPct > 0 ? import_chalk13.default.dim(` ${cacheHitPct}% hit rate`) : "";
16953
17725
  console.log(
16954
- " " + import_chalk13.default.red.bold("\u26A0\uFE0F Response DLP") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.red(
16955
- `${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
16956
- )
17726
+ " " + import_chalk13.default.white("Cache read".padEnd(TOK_LABEL)) + cacheBar + " " + import_chalk13.default.green(num2(costCacheRead)) + pct
16957
17727
  );
16958
- console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(60, W - 4))));
17728
+ }
17729
+ }
17730
+ if (costUSD > 0) {
17731
+ const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
17732
+ const avgPerDay = costUSD / periodDays;
17733
+ const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
17734
+ const costHeaderRight = [
17735
+ import_chalk13.default.yellow(fmtCost2(costUSD)),
17736
+ import_chalk13.default.dim(`avg ${fmtCost2(avgPerDay)}/day`),
17737
+ cacheHitPct > 0 ? import_chalk13.default.dim(`${cacheHitPct}% cache hit`) : null
17738
+ ].filter(Boolean).join(import_chalk13.default.dim(" \xB7 "));
17739
+ console.log("");
17740
+ console.log(" " + import_chalk13.default.bold("Cost") + " " + costHeaderRight);
17741
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
17742
+ if (codexUSD > 0)
17743
+ costByModel.set("codex (openai)", (costByModel.get("codex (openai)") ?? 0) + codexUSD);
17744
+ if (geminiUSD > 0)
17745
+ costByModel.set("gemini (google)", (costByModel.get("gemini (google)") ?? 0) + geminiUSD);
17746
+ const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
17747
+ const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
17748
+ const MODEL_LABEL = 22;
17749
+ const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
17750
+ for (const [model, cost] of modelList) {
17751
+ const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
17752
+ const b = colorBar(cost, maxModelCost, MODEL_BAR);
16959
17753
  console.log(
16960
- " " + import_chalk13.default.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
17754
+ " " + import_chalk13.default.white(label.padEnd(MODEL_LABEL)) + b + " " + import_chalk13.default.yellow(fmtCost2(cost))
16961
17755
  );
16962
- console.log(" " + import_chalk13.default.yellow("Rotate affected keys immediately."));
16963
- for (const e of responseDlpEntries.slice(0, 5)) {
16964
- const ts = import_chalk13.default.dim(fmtDate(e.ts) + " ");
16965
- const pattern = import_chalk13.default.red(e.dlpPattern ?? "DLP");
16966
- const sample = import_chalk13.default.gray(e.dlpSample ?? "");
16967
- console.log(` ${ts}${pattern} ${sample}`);
16968
- }
16969
- if (responseDlpEntries.length > 5) {
16970
- console.log(import_chalk13.default.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
16971
- }
16972
17756
  }
17757
+ }
17758
+ if (responseDlpEntries.length > 0) {
16973
17759
  console.log("");
16974
17760
  console.log(
16975
- " " + import_chalk13.default.dim("node9 audit --deny") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.dim("node9 report --period today|7d|30d|month --no-tests")
17761
+ " " + import_chalk13.default.red.bold("\u26A0\uFE0F Response DLP") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.red(
17762
+ `${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
17763
+ )
16976
17764
  );
16977
- console.log("");
16978
- });
17765
+ console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(60, W - 4))));
17766
+ console.log(
17767
+ " " + import_chalk13.default.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
17768
+ );
17769
+ console.log(" " + import_chalk13.default.yellow("Rotate affected keys immediately."));
17770
+ for (const e of responseDlpEntries.slice(0, 5)) {
17771
+ const ts = import_chalk13.default.dim(fmtDate(e.ts) + " ");
17772
+ const pattern = import_chalk13.default.red(e.dlpPattern ?? "DLP");
17773
+ const sample = import_chalk13.default.gray(e.dlpSample ?? "");
17774
+ console.log(` ${ts}${pattern} ${sample}`);
17775
+ }
17776
+ if (responseDlpEntries.length > 5) {
17777
+ console.log(import_chalk13.default.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
17778
+ }
17779
+ }
17780
+ console.log("");
17781
+ console.log(
17782
+ " " + import_chalk13.default.dim("node9 audit --deny") + import_chalk13.default.dim(" \xB7 ") + import_chalk13.default.dim("node9 report --period today|7d|30d|month --no-tests")
17783
+ );
17784
+ console.log("");
16979
17785
  }
16980
17786
 
16981
17787
  // src/cli/commands/daemon-cmd.ts
@@ -20785,6 +21591,17 @@ program.command("tail").description("Stream live agent activity to the terminal"
20785
21591
  process.exit(1);
20786
21592
  }
20787
21593
  });
21594
+ program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
21595
+ try {
21596
+ const dashboardPath = import_path49.default.join(__dirname, "dashboard.mjs");
21597
+ const dynamicImport = new Function("id", "return import(id)");
21598
+ const mod = await dynamicImport(`file://${dashboardPath}`);
21599
+ await mod.startMonitor();
21600
+ } catch (err2) {
21601
+ console.error(import_chalk31.default.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
21602
+ process.exit(1);
21603
+ }
21604
+ });
20788
21605
  registerWatchCommand(program);
20789
21606
  registerMcpGatewayCommand(program);
20790
21607
  registerMcpServerCommand(program);