@node9/proxy 1.32.0 → 1.34.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.
@@ -244,6 +244,8 @@ function normalizeCommandForPolicyImpl(command) {
244
244
  if (f === PARSE_FAIL) return command;
245
245
  try {
246
246
  const strips = [];
247
+ const rewrites = [];
248
+ const msgSpans = /* @__PURE__ */ new Set();
247
249
  syntax.Walk(f, (node) => {
248
250
  if (!node) return false;
249
251
  const n = node;
@@ -259,25 +261,46 @@ function normalizeCommandForPolicyImpl(command) {
259
261
  if (nextParts.length !== 1) continue;
260
262
  const quotedNode = nextParts[0];
261
263
  const nt = syntax.NodeType(quotedNode);
264
+ const markStrip = () => {
265
+ const s = next.Pos().Offset();
266
+ const e = next.End().Offset();
267
+ strips.push([s, e]);
268
+ msgSpans.add(`${s}:${e}`);
269
+ };
262
270
  if (nt === "SglQuoted") {
263
- strips.push([next.Pos().Offset(), next.End().Offset()]);
271
+ markStrip();
264
272
  } else if (nt === "DblQuoted") {
265
273
  const innerParts = quotedNode.Parts || [];
266
274
  const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
267
275
  if (allLit) {
268
- strips.push([next.Pos().Offset(), next.End().Offset()]);
276
+ markStrip();
269
277
  } else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
270
- strips.push([next.Pos().Offset(), next.End().Offset()]);
278
+ markStrip();
271
279
  }
272
280
  }
273
281
  }
282
+ for (const arg of args) {
283
+ const s = arg.Pos().Offset();
284
+ const e = arg.End().Offset();
285
+ if (msgSpans.has(`${s}:${e}`)) continue;
286
+ const resolved = resolveWordLiteral(arg);
287
+ if (resolved === null) continue;
288
+ const source = command.slice(s, e);
289
+ if (resolved === source) continue;
290
+ if (resolved === "" || /\s/.test(resolved)) continue;
291
+ rewrites.push([s, e, resolved]);
292
+ }
274
293
  return true;
275
294
  });
276
- if (strips.length === 0) return command;
277
- strips.sort((a, b) => b[0] - a[0]);
295
+ const edits = [
296
+ ...strips.map(([s, e]) => [s, e, '""']),
297
+ ...rewrites
298
+ ];
299
+ if (edits.length === 0) return command;
300
+ edits.sort((a, b) => b[0] - a[0]);
278
301
  let result = command;
279
- for (const [start, end] of strips) {
280
- result = result.slice(0, start) + '""' + result.slice(end);
302
+ for (const [s, e, rep] of edits) {
303
+ result = result.slice(0, s) + rep + result.slice(e);
281
304
  }
282
305
  return result;
283
306
  } catch {
@@ -386,20 +409,38 @@ function extractLiteralArgs(callExpr) {
386
409
  }
387
410
  return { name, flags, paths };
388
411
  }
412
+ function resolveWordLiteral(w) {
413
+ const parts = w?.Parts || [];
414
+ let s = "";
415
+ for (const p of parts) {
416
+ const t = syntax.NodeType(p);
417
+ if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
418
+ else if (t === "SglQuoted") s += p.Value ?? "";
419
+ else if (t === "DblQuoted") {
420
+ const inner = p.Parts || [];
421
+ if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
422
+ s += inner.map((ip) => ip.Value ?? "").join("");
423
+ } else {
424
+ return null;
425
+ }
426
+ }
427
+ return s;
428
+ }
389
429
  function analyzeFsOperation(command) {
390
- if (!FS_OP_PRESCREEN_RE.test(command)) return null;
391
- if (fsOpCache.has(command)) {
392
- const hit = fsOpCache.get(command) ?? null;
393
- fsOpCache.delete(command);
394
- fsOpCache.set(command, hit);
430
+ const normalized = normalizeCommandForPolicy(command);
431
+ if (!FS_OP_PRESCREEN_RE.test(normalized)) return null;
432
+ if (fsOpCache.has(normalized)) {
433
+ const hit = fsOpCache.get(normalized) ?? null;
434
+ fsOpCache.delete(normalized);
435
+ fsOpCache.set(normalized, hit);
395
436
  return hit;
396
437
  }
397
- const computed = analyzeFsOperationImpl(command);
438
+ const computed = analyzeFsOperationImpl(normalized);
398
439
  if (fsOpCache.size >= FS_OP_CACHE_MAX) {
399
440
  const oldest = fsOpCache.keys().next().value;
400
441
  if (oldest !== void 0) fsOpCache.delete(oldest);
401
442
  }
402
- fsOpCache.set(command, computed);
443
+ fsOpCache.set(normalized, computed);
403
444
  return computed;
404
445
  }
405
446
  function analyzeFsOperationImpl(command) {
@@ -506,9 +547,9 @@ function matchesPattern(text, patterns) {
506
547
  const withoutDotSlash = text.replace(/^\.\//, "");
507
548
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
508
549
  }
509
- function getNestedValue(obj, path9) {
550
+ function getNestedValue(obj, path10) {
510
551
  if (!obj || typeof obj !== "object") return null;
511
- const segments = path9.split(".");
552
+ const segments = path10.split(".");
512
553
  for (const seg of segments) {
513
554
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
514
555
  }
@@ -1219,7 +1260,11 @@ var init_dist = __esm({
1219
1260
  "shield:project-jail:block-read-ssh",
1220
1261
  "shield:project-jail:block-read-aws",
1221
1262
  "shield:project-jail:block-read-env",
1222
- "shield:project-jail:review-read-credentials"
1263
+ "shield:project-jail:review-read-credentials",
1264
+ // SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
1265
+ // raw-regex smart rule is suppressed for bash — its cond1 read a grep
1266
+ // alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
1267
+ "review-drop-truncate-shell"
1223
1268
  ]);
1224
1269
  FS_OP_CACHE_MAX = 5e3;
1225
1270
  fsOpCache = /* @__PURE__ */ new Map();
@@ -2585,13 +2630,116 @@ var init_audit = __esm({
2585
2630
  });
2586
2631
 
2587
2632
  // src/pricing/litellm.ts
2633
+ import fs4 from "fs";
2634
+ import path5 from "path";
2635
+ import os5 from "os";
2588
2636
  function normalizeModel(raw) {
2589
2637
  return raw.replace(/-\d{8}$/, "").toLowerCase();
2590
2638
  }
2639
+ function readCache() {
2640
+ try {
2641
+ const raw = JSON.parse(fs4.readFileSync(CACHE_FILE(), "utf-8"));
2642
+ if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
2643
+ return null;
2644
+ }
2645
+ const ageMs = Date.now() - new Date(raw.fetchedAt).getTime();
2646
+ if (ageMs < 0 || ageMs > TTL_MS) return null;
2647
+ return raw.prices;
2648
+ } catch {
2649
+ return null;
2650
+ }
2651
+ }
2652
+ function writeCache(prices) {
2653
+ try {
2654
+ const target = CACHE_FILE();
2655
+ const dir = path5.dirname(target);
2656
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
2657
+ const tmp = target + ".tmp";
2658
+ const body = {
2659
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2660
+ prices
2661
+ };
2662
+ fs4.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
2663
+ fs4.renameSync(tmp, target);
2664
+ } catch (err) {
2665
+ try {
2666
+ fs4.appendFileSync(
2667
+ HOOK_DEBUG_LOG,
2668
+ `[pricing] cache write failed: ${err.message}
2669
+ `
2670
+ );
2671
+ } catch {
2672
+ }
2673
+ }
2674
+ }
2675
+ function tupleFromLiteLLM(entry) {
2676
+ if (!entry || typeof entry !== "object") return null;
2677
+ const e = entry;
2678
+ const num2 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
2679
+ const inCost = num2(e.input_cost_per_token);
2680
+ const outCost = num2(e.output_cost_per_token);
2681
+ if (inCost === 0 && outCost === 0) return null;
2682
+ return [
2683
+ inCost,
2684
+ outCost,
2685
+ num2(e.cache_creation_input_token_cost),
2686
+ num2(e.cache_read_input_token_cost)
2687
+ ];
2688
+ }
2689
+ async function fetchLiteLLMPricing() {
2690
+ try {
2691
+ const res = await fetch(LITELLM_URL, {
2692
+ signal: AbortSignal.timeout(15e3)
2693
+ });
2694
+ if (!res.ok) return null;
2695
+ const json = await res.json();
2696
+ if (!json || typeof json !== "object") return null;
2697
+ const out = {};
2698
+ for (const [key, value] of Object.entries(json)) {
2699
+ const tuple = tupleFromLiteLLM(value);
2700
+ if (tuple) out[key.toLowerCase()] = tuple;
2701
+ }
2702
+ if (Object.keys(out).length < 10) {
2703
+ return null;
2704
+ }
2705
+ return out;
2706
+ } catch {
2707
+ return null;
2708
+ }
2709
+ }
2710
+ async function ensurePricingLoaded() {
2711
+ if (memCache !== null && Date.now() - memCacheAt < TTL_MS) return;
2712
+ const fromDisk = readCache();
2713
+ if (fromDisk && Object.keys(fromDisk).length > 0) {
2714
+ memCache = fromDisk;
2715
+ memCacheAt = Date.now();
2716
+ lookupCache.clear();
2717
+ return;
2718
+ }
2719
+ const fetched = await fetchLiteLLMPricing();
2720
+ if (fetched && Object.keys(fetched).length > 0) {
2721
+ memCache = fetched;
2722
+ memCacheAt = Date.now();
2723
+ writeCache(fetched);
2724
+ lookupCache.clear();
2725
+ return;
2726
+ }
2727
+ memCache = { ...BUNDLED_PRICING };
2728
+ memCacheAt = Date.now();
2729
+ lookupCache.clear();
2730
+ }
2591
2731
  function pricingFor(model) {
2592
2732
  const norm = normalizeModel(model);
2593
2733
  const cached = lookupCache.get(norm);
2594
2734
  if (cached !== void 0) return cached;
2735
+ if (memCache === null && !diskChecked) {
2736
+ diskChecked = true;
2737
+ const disk = readCache();
2738
+ if (disk && Object.keys(disk).length > 0) {
2739
+ memCache = disk;
2740
+ memCacheAt = Date.now();
2741
+ }
2742
+ }
2595
2743
  const sources = [];
2596
2744
  if (memCache) sources.push(memCache);
2597
2745
  sources.push(BUNDLED_PRICING);
@@ -2616,11 +2764,12 @@ function pricingFor(model) {
2616
2764
  lookupCache.set(norm, resolved);
2617
2765
  return resolved;
2618
2766
  }
2619
- var BUNDLED_PRICING, TTL_MS, memCache, lookupCache;
2767
+ var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, diskChecked, lookupCache;
2620
2768
  var init_litellm = __esm({
2621
2769
  "src/pricing/litellm.ts"() {
2622
2770
  "use strict";
2623
2771
  init_audit();
2772
+ LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
2624
2773
  BUNDLED_PRICING = {
2625
2774
  // Anthropic
2626
2775
  "claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
@@ -2637,25 +2786,80 @@ var init_litellm = __esm({
2637
2786
  "claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
2638
2787
  "claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
2639
2788
  "claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
2640
- // OpenAI
2789
+ // OpenAI. gpt-5 family + o-series copied from the live LiteLLM table
2790
+ // (verified 2026-06-14) — the bundled gpt-5 was stale at $10/$30 vs the real
2791
+ // $1.25/$10, and Codex models (gpt-5-codex etc.) were absent, so the offline
2792
+ // fallback mispriced every Codex session. See cost-codex.codexPriceFor.
2641
2793
  "gpt-4o": [5e-6, 15e-6, 0, 25e-7],
2642
2794
  "gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
2643
- "gpt-5": [1e-5, 3e-5, 0, 5e-6],
2644
- // Google
2795
+ "gpt-5": [125e-8, 1e-5, 0, 125e-9],
2796
+ "gpt-5-codex": [125e-8, 1e-5, 0, 125e-9],
2797
+ "gpt-5-mini": [25e-8, 2e-6, 0, 25e-9],
2798
+ o3: [2e-6, 8e-6, 0, 5e-7],
2799
+ "o4-mini": [11e-7, 44e-7, 0, 275e-9],
2800
+ // Google. Values copied from the live LiteLLM table (verified 2026-06-14)
2801
+ // so the bundled fallback prices the current Gemini tiers correctly offline
2802
+ // — the local cost readers were carrying a stale hardcoded copy where
2803
+ // gemini-2.5-flash read $0.15/$0.60 vs the real $0.30/$2.50 (~4× under on
2804
+ // output). See cost-gemini.geminiPriceFor (the single Gemini price source).
2805
+ "gemini-2.5-pro": [125e-8, 1e-5, 0, 125e-9],
2806
+ "gemini-2.5-flash": [3e-7, 25e-7, 0, 3e-8],
2645
2807
  "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
2646
2808
  "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
2647
2809
  };
2810
+ CACHE_FILE = () => path5.join(os5.homedir(), ".node9", "model-pricing.json");
2648
2811
  TTL_MS = 24 * 60 * 60 * 1e3;
2649
2812
  memCache = null;
2813
+ memCacheAt = 0;
2814
+ diskChecked = false;
2650
2815
  lookupCache = /* @__PURE__ */ new Map();
2651
2816
  }
2652
2817
  });
2653
2818
 
2654
2819
  // src/cost-codex.ts
2820
+ function codexPriceFor(model) {
2821
+ return pricingFor(model) ?? CODEX_FALLBACK;
2822
+ }
2823
+ function codexSessionCost(model, tokens) {
2824
+ const nonCached = Math.max(0, tokens.input - tokens.cached);
2825
+ const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
2826
+ return nonCached * pin + tokens.cached * pcr + tokens.output * pout;
2827
+ }
2828
+ var CODEX_FALLBACK;
2655
2829
  var init_cost_codex = __esm({
2656
2830
  "src/cost-codex.ts"() {
2657
2831
  "use strict";
2658
2832
  init_litellm();
2833
+ CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
2834
+ }
2835
+ });
2836
+
2837
+ // src/cost-gemini.ts
2838
+ function geminiPriceFor(model) {
2839
+ let tuple = pricingFor(model);
2840
+ if (!tuple && /^gemini-/i.test(model)) {
2841
+ for (const proxy of GEMINI_FALLBACK_MODELS) {
2842
+ tuple = pricingFor(proxy);
2843
+ if (tuple) break;
2844
+ }
2845
+ }
2846
+ if (!tuple) return null;
2847
+ return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
2848
+ }
2849
+ var GEMINI_FALLBACK_MODELS;
2850
+ var init_cost_gemini = __esm({
2851
+ "src/cost-gemini.ts"() {
2852
+ "use strict";
2853
+ init_litellm();
2854
+ GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
2855
+ }
2856
+ });
2857
+
2858
+ // src/cost-copilot.ts
2859
+ var init_cost_copilot = __esm({
2860
+ "src/cost-copilot.ts"() {
2861
+ "use strict";
2862
+ init_litellm();
2659
2863
  }
2660
2864
  });
2661
2865
 
@@ -2671,6 +2875,8 @@ var init_costSync = __esm({
2671
2875
  init_audit();
2672
2876
  init_litellm();
2673
2877
  init_cost_codex();
2878
+ init_cost_gemini();
2879
+ init_cost_copilot();
2674
2880
  SYNC_INTERVAL_MS = 10 * 60 * 1e3;
2675
2881
  }
2676
2882
  });
@@ -2687,9 +2893,9 @@ var init_scan_watermark = __esm({
2687
2893
  });
2688
2894
 
2689
2895
  // src/cli/aggregate/report-audit.ts
2690
- import fs4 from "fs";
2691
- import os5 from "os";
2692
- import path5 from "path";
2896
+ import fs5 from "fs";
2897
+ import os6 from "os";
2898
+ import path6 from "path";
2693
2899
  function buildTestTimestamps(allEntries) {
2694
2900
  const testTs = /* @__PURE__ */ new Set();
2695
2901
  for (const e of allEntries) {
@@ -2770,8 +2976,8 @@ function getDateRange(period, now) {
2770
2976
  }
2771
2977
  }
2772
2978
  function parseAuditLog(logPath) {
2773
- if (!fs4.existsSync(logPath)) return [];
2774
- const raw = fs4.readFileSync(logPath, "utf-8");
2979
+ if (!fs5.existsSync(logPath)) return [];
2980
+ const raw = fs5.readFileSync(logPath, "utf-8");
2775
2981
  return raw.split("\n").flatMap((line) => {
2776
2982
  if (!line.trim()) return [];
2777
2983
  try {
@@ -2788,11 +2994,10 @@ function isDlp(checkedBy) {
2788
2994
  return !!checkedBy?.includes("dlp");
2789
2995
  }
2790
2996
  function claudeModelPrice(model) {
2791
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
2792
- for (const [key, p] of Object.entries(CLAUDE_PRICING)) {
2793
- if (base === key || base.startsWith(key + "-") || base.startsWith(key)) return p;
2794
- }
2795
- return null;
2997
+ const t = pricingFor(model);
2998
+ if (!t) return null;
2999
+ const [i, o, cw, cr] = t;
3000
+ return { i, o, cw, cr };
2796
3001
  }
2797
3002
  function emptyClaudeCostAccumulator() {
2798
3003
  return {
@@ -2819,25 +3024,25 @@ function freezeClaudeCost(acc) {
2819
3024
  };
2820
3025
  }
2821
3026
  function processClaudeCostProject(proj, projectsDir, start, end, acc) {
2822
- const projPath = path5.join(projectsDir, proj);
3027
+ const projPath = path6.join(projectsDir, proj);
2823
3028
  let files;
2824
3029
  try {
2825
- const stat = fs4.statSync(projPath);
3030
+ const stat = fs5.statSync(projPath);
2826
3031
  if (!stat.isDirectory()) return;
2827
- files = fs4.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
3032
+ files = fs5.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
2828
3033
  } catch {
2829
3034
  return;
2830
3035
  }
2831
3036
  const startMs = start.getTime();
2832
3037
  for (const file of files) {
2833
- const filePath = path5.join(projPath, file);
3038
+ const filePath = path6.join(projPath, file);
2834
3039
  try {
2835
- if (fs4.statSync(filePath).mtimeMs < startMs) continue;
3040
+ if (fs5.statSync(filePath).mtimeMs < startMs) continue;
2836
3041
  } catch {
2837
3042
  continue;
2838
3043
  }
2839
3044
  try {
2840
- const raw = fs4.readFileSync(filePath, "utf-8");
3045
+ const raw = fs5.readFileSync(filePath, "utf-8");
2841
3046
  for (const line of raw.split("\n")) {
2842
3047
  if (!line.trim()) continue;
2843
3048
  let entry;
@@ -2887,10 +3092,10 @@ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
2887
3092
  }
2888
3093
  function loadClaudeCost(start, end, projectsDir) {
2889
3094
  const acc = emptyClaudeCostAccumulator();
2890
- if (!fs4.existsSync(projectsDir)) return freezeClaudeCost(acc);
3095
+ if (!fs5.existsSync(projectsDir)) return freezeClaudeCost(acc);
2891
3096
  let dirs;
2892
3097
  try {
2893
- dirs = fs4.readdirSync(projectsDir);
3098
+ dirs = fs5.readdirSync(projectsDir);
2894
3099
  } catch {
2895
3100
  return freezeClaudeCost(acc);
2896
3101
  }
@@ -2901,10 +3106,10 @@ function loadClaudeCost(start, end, projectsDir) {
2901
3106
  }
2902
3107
  async function loadClaudeCostAsync(start, end, projectsDir) {
2903
3108
  const acc = emptyClaudeCostAccumulator();
2904
- if (!fs4.existsSync(projectsDir)) return freezeClaudeCost(acc);
3109
+ if (!fs5.existsSync(projectsDir)) return freezeClaudeCost(acc);
2905
3110
  let dirs;
2906
3111
  try {
2907
- dirs = fs4.readdirSync(projectsDir);
3112
+ dirs = fs5.readdirSync(projectsDir);
2908
3113
  } catch {
2909
3114
  return freezeClaudeCost(acc);
2910
3115
  }
@@ -2917,11 +3122,12 @@ async function loadClaudeCostAsync(start, end, projectsDir) {
2917
3122
  function processCodexCostFile(filePath, start, end, acc) {
2918
3123
  let lines;
2919
3124
  try {
2920
- lines = fs4.readFileSync(filePath, "utf-8").split("\n");
3125
+ lines = fs5.readFileSync(filePath, "utf-8").split("\n");
2921
3126
  } catch {
2922
3127
  return;
2923
3128
  }
2924
3129
  let sessionStart = "";
3130
+ let model = "";
2925
3131
  let lastTotalInput = 0;
2926
3132
  let lastTotalCached = 0;
2927
3133
  let lastTotalOutput = 0;
@@ -2939,6 +3145,10 @@ function processCodexCostFile(filePath, start, end, acc) {
2939
3145
  sessionStart = String(p["timestamp"] ?? "");
2940
3146
  continue;
2941
3147
  }
3148
+ if (entry.type === "turn_context" && typeof p["model"] === "string") {
3149
+ model = p["model"];
3150
+ continue;
3151
+ }
2942
3152
  if (entry.type === "event_msg" && p["type"] === "token_count") {
2943
3153
  const info = p["info"] ?? {};
2944
3154
  const usage = info["total_token_usage"] ?? {};
@@ -2953,40 +3163,45 @@ function processCodexCostFile(filePath, start, end, acc) {
2953
3163
  if (!sessionStart) return;
2954
3164
  const ts = new Date(sessionStart);
2955
3165
  if (ts < start || ts > end) return;
2956
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
2957
- const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
3166
+ const cost = codexSessionCost(model, {
3167
+ input: lastTotalInput,
3168
+ cached: lastTotalCached,
3169
+ output: lastTotalOutput
3170
+ });
2958
3171
  acc.total += cost;
2959
3172
  acc.toolCalls += sessionToolCalls;
2960
3173
  const dateKey = sessionStart.slice(0, 10);
2961
3174
  acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
3175
+ const normModel = normalizeModel(model || "gpt-5");
3176
+ acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
2962
3177
  }
2963
3178
  function listCodexSessionFiles(sessionsBase) {
2964
3179
  const jsonlFiles = [];
2965
- if (!fs4.existsSync(sessionsBase)) return jsonlFiles;
3180
+ if (!fs5.existsSync(sessionsBase)) return jsonlFiles;
2966
3181
  try {
2967
- for (const year of fs4.readdirSync(sessionsBase)) {
2968
- const yearPath = path5.join(sessionsBase, year);
3182
+ for (const year of fs5.readdirSync(sessionsBase)) {
3183
+ const yearPath = path6.join(sessionsBase, year);
2969
3184
  try {
2970
- if (!fs4.statSync(yearPath).isDirectory()) continue;
3185
+ if (!fs5.statSync(yearPath).isDirectory()) continue;
2971
3186
  } catch {
2972
3187
  continue;
2973
3188
  }
2974
- for (const month of fs4.readdirSync(yearPath)) {
2975
- const monthPath = path5.join(yearPath, month);
3189
+ for (const month of fs5.readdirSync(yearPath)) {
3190
+ const monthPath = path6.join(yearPath, month);
2976
3191
  try {
2977
- if (!fs4.statSync(monthPath).isDirectory()) continue;
3192
+ if (!fs5.statSync(monthPath).isDirectory()) continue;
2978
3193
  } catch {
2979
3194
  continue;
2980
3195
  }
2981
- for (const day of fs4.readdirSync(monthPath)) {
2982
- const dayPath = path5.join(monthPath, day);
3196
+ for (const day of fs5.readdirSync(monthPath)) {
3197
+ const dayPath = path6.join(monthPath, day);
2983
3198
  try {
2984
- if (!fs4.statSync(dayPath).isDirectory()) continue;
3199
+ if (!fs5.statSync(dayPath).isDirectory()) continue;
2985
3200
  } catch {
2986
3201
  continue;
2987
3202
  }
2988
- for (const file of fs4.readdirSync(dayPath)) {
2989
- if (file.endsWith(".jsonl")) jsonlFiles.push(path5.join(dayPath, file));
3203
+ for (const file of fs5.readdirSync(dayPath)) {
3204
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path6.join(dayPath, file));
2990
3205
  }
2991
3206
  }
2992
3207
  }
@@ -2996,16 +3211,33 @@ function listCodexSessionFiles(sessionsBase) {
2996
3211
  }
2997
3212
  return jsonlFiles;
2998
3213
  }
3214
+ function mergeByModel(...maps) {
3215
+ const out = /* @__PURE__ */ new Map();
3216
+ for (const m of maps) {
3217
+ for (const [k, v] of m) out.set(k, (out.get(k) ?? 0) + v);
3218
+ }
3219
+ return out;
3220
+ }
2999
3221
  function loadCodexCost(start, end, sessionsBase) {
3000
- const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
3222
+ const acc = {
3223
+ total: 0,
3224
+ toolCalls: 0,
3225
+ byDay: /* @__PURE__ */ new Map(),
3226
+ byModel: /* @__PURE__ */ new Map()
3227
+ };
3001
3228
  const files = listCodexSessionFiles(sessionsBase);
3002
3229
  for (const filePath of files) {
3003
3230
  processCodexCostFile(filePath, start, end, acc);
3004
3231
  }
3005
- return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
3232
+ return { total: acc.total, byDay: acc.byDay, byModel: acc.byModel, toolCalls: acc.toolCalls };
3006
3233
  }
3007
3234
  async function loadCodexCostAsync(start, end, sessionsBase) {
3008
- const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
3235
+ const acc = {
3236
+ total: 0,
3237
+ toolCalls: 0,
3238
+ byDay: /* @__PURE__ */ new Map(),
3239
+ byModel: /* @__PURE__ */ new Map()
3240
+ };
3009
3241
  const files = listCodexSessionFiles(sessionsBase);
3010
3242
  const CHUNK_SIZE = 5;
3011
3243
  for (let i = 0; i < files.length; i++) {
@@ -3014,12 +3246,12 @@ async function loadCodexCostAsync(start, end, sessionsBase) {
3014
3246
  await new Promise((resolve) => setImmediate(resolve));
3015
3247
  }
3016
3248
  }
3017
- return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
3249
+ return { total: acc.total, byDay: acc.byDay, byModel: acc.byModel, toolCalls: acc.toolCalls };
3018
3250
  }
3019
- function geminiPriceFor(model) {
3251
+ function geminiPriceFor2(model) {
3020
3252
  let tuple = pricingFor(model);
3021
3253
  if (!tuple && /^gemini-/i.test(model)) {
3022
- for (const proxy of GEMINI_FALLBACK_MODELS) {
3254
+ for (const proxy of GEMINI_FALLBACK_MODELS2) {
3023
3255
  tuple = pricingFor(proxy);
3024
3256
  if (tuple) break;
3025
3257
  }
@@ -3050,13 +3282,13 @@ function freezeGeminiCost(acc) {
3050
3282
  function processGeminiCostFile(filePath, projectKey, start, end, acc) {
3051
3283
  const startMs = start.getTime();
3052
3284
  try {
3053
- if (fs4.statSync(filePath).mtimeMs < startMs) return;
3285
+ if (fs5.statSync(filePath).mtimeMs < startMs) return;
3054
3286
  } catch {
3055
3287
  return;
3056
3288
  }
3057
3289
  let raw;
3058
3290
  try {
3059
- raw = fs4.readFileSync(filePath, "utf-8");
3291
+ raw = fs5.readFileSync(filePath, "utf-8");
3060
3292
  } catch {
3061
3293
  return;
3062
3294
  }
@@ -3077,7 +3309,7 @@ function processGeminiCostFile(filePath, projectKey, start, end, acc) {
3077
3309
  }
3078
3310
  const ts = new Date(entry.timestamp);
3079
3311
  if (ts < start || ts > end) continue;
3080
- const price = geminiPriceFor(entry.model);
3312
+ const price = geminiPriceFor2(entry.model);
3081
3313
  if (!price) continue;
3082
3314
  const inp = entry.tokens.input ?? 0;
3083
3315
  const out = entry.tokens.output ?? 0;
@@ -3105,30 +3337,30 @@ function listGeminiSessionFiles(geminiTmpDir) {
3105
3337
  const out = [];
3106
3338
  let dirs;
3107
3339
  try {
3108
- if (!fs4.statSync(geminiTmpDir).isDirectory()) return out;
3109
- dirs = fs4.readdirSync(geminiTmpDir);
3340
+ if (!fs5.statSync(geminiTmpDir).isDirectory()) return out;
3341
+ dirs = fs5.readdirSync(geminiTmpDir);
3110
3342
  } catch {
3111
3343
  return out;
3112
3344
  }
3113
3345
  for (const proj of dirs) {
3114
- const chatsDir = path5.join(geminiTmpDir, proj, "chats");
3346
+ const chatsDir = path6.join(geminiTmpDir, proj, "chats");
3115
3347
  let files;
3116
3348
  try {
3117
- if (!fs4.statSync(chatsDir).isDirectory()) continue;
3118
- files = fs4.readdirSync(chatsDir);
3349
+ if (!fs5.statSync(chatsDir).isDirectory()) continue;
3350
+ files = fs5.readdirSync(chatsDir);
3119
3351
  } catch {
3120
3352
  continue;
3121
3353
  }
3122
3354
  for (const f of files) {
3123
3355
  if (!f.endsWith(".jsonl")) continue;
3124
- out.push({ projectKey: proj, file: path5.join(chatsDir, f) });
3356
+ out.push({ projectKey: proj, file: path6.join(chatsDir, f) });
3125
3357
  }
3126
3358
  }
3127
3359
  return out;
3128
3360
  }
3129
3361
  function loadGeminiCost(start, end, geminiTmpDir) {
3130
3362
  const acc = emptyGeminiAccumulator();
3131
- if (!fs4.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
3363
+ if (!fs5.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
3132
3364
  for (const { projectKey, file } of listGeminiSessionFiles(geminiTmpDir)) {
3133
3365
  processGeminiCostFile(file, projectKey, start, end, acc);
3134
3366
  }
@@ -3136,7 +3368,7 @@ function loadGeminiCost(start, end, geminiTmpDir) {
3136
3368
  }
3137
3369
  async function loadGeminiCostAsync(start, end, geminiTmpDir) {
3138
3370
  const acc = emptyGeminiAccumulator();
3139
- if (!fs4.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
3371
+ if (!fs5.existsSync(geminiTmpDir)) return freezeGeminiCost(acc);
3140
3372
  const files = listGeminiSessionFiles(geminiTmpDir);
3141
3373
  const CHUNK_SIZE = 5;
3142
3374
  for (let i = 0; i < files.length; i++) {
@@ -3149,11 +3381,11 @@ async function loadGeminiCostAsync(start, end, geminiTmpDir) {
3149
3381
  }
3150
3382
  function aggregateReportFromAudit(period, opts = {}) {
3151
3383
  const now = opts.now ?? /* @__PURE__ */ new Date();
3152
- const auditLogPath2 = opts.auditLogPath ?? path5.join(os5.homedir(), ".node9", "audit.log");
3153
- const claudeProjectsDir = opts.claudeProjectsDir ?? path5.join(os5.homedir(), ".claude", "projects");
3154
- const codexSessionsDir = opts.codexSessionsDir ?? path5.join(os5.homedir(), ".codex", "sessions");
3155
- const geminiTmpDir = opts.geminiTmpDir ?? path5.join(os5.homedir(), ".gemini", "tmp");
3156
- const hasAuditFile = fs4.existsSync(auditLogPath2);
3384
+ const auditLogPath2 = opts.auditLogPath ?? path6.join(os6.homedir(), ".node9", "audit.log");
3385
+ const claudeProjectsDir = opts.claudeProjectsDir ?? path6.join(os6.homedir(), ".claude", "projects");
3386
+ const codexSessionsDir = opts.codexSessionsDir ?? path6.join(os6.homedir(), ".codex", "sessions");
3387
+ const geminiTmpDir = opts.geminiTmpDir ?? path6.join(os6.homedir(), ".gemini", "tmp");
3388
+ const hasAuditFile = fs5.existsSync(auditLogPath2);
3157
3389
  const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath2);
3158
3390
  const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
3159
3391
  const { start, end } = getDateRange(period, now);
@@ -3315,7 +3547,7 @@ function aggregateReportFromAudit(period, opts = {}) {
3315
3547
  cacheWriteTokens: claudeCost.cacheWriteTokens,
3316
3548
  cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
3317
3549
  byDay: claudeCost.byDay,
3318
- byModel: claudeCost.byModel,
3550
+ byModel: mergeByModel(claudeCost.byModel, codexCost.byModel),
3319
3551
  byProject: claudeCost.byProject
3320
3552
  },
3321
3553
  toolMap,
@@ -3329,43 +3561,32 @@ function aggregateReportFromAudit(period, opts = {}) {
3329
3561
  };
3330
3562
  return { data, hasAuditFile, responseDlpEntries };
3331
3563
  }
3332
- var TEST_COMMAND_RE, SUPERSEDE_WINDOW_MS, CLAUDE_PRICING, GEMINI_FALLBACK_MODELS;
3564
+ var TEST_COMMAND_RE, SUPERSEDE_WINDOW_MS, GEMINI_FALLBACK_MODELS2;
3333
3565
  var init_report_audit = __esm({
3334
3566
  "src/cli/aggregate/report-audit.ts"() {
3335
3567
  "use strict";
3336
3568
  init_costSync();
3337
3569
  init_litellm();
3570
+ init_cost_codex();
3338
3571
  TEST_COMMAND_RE = /(?:^|\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;
3339
3572
  SUPERSEDE_WINDOW_MS = 6e4;
3340
- CLAUDE_PRICING = {
3341
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
3342
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
3343
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
3344
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
3345
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
3346
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
3347
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
3348
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
3349
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
3350
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
3351
- };
3352
- GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
3573
+ GEMINI_FALLBACK_MODELS2 = ["gemini-2.5-flash", "gemini-2.0-flash"];
3353
3574
  }
3354
3575
  });
3355
3576
 
3356
3577
  // src/utils/provenance.ts
3357
- import path6 from "path";
3358
- import os6 from "os";
3578
+ import path7 from "path";
3579
+ import os7 from "os";
3359
3580
  var USER_PREFIXES;
3360
3581
  var init_provenance = __esm({
3361
3582
  "src/utils/provenance.ts"() {
3362
3583
  "use strict";
3363
3584
  USER_PREFIXES = [
3364
- path6.join(os6.homedir(), "bin"),
3365
- path6.join(os6.homedir(), ".local", "bin"),
3366
- path6.join(os6.homedir(), ".cargo", "bin"),
3367
- path6.join(os6.homedir(), ".npm-global", "bin"),
3368
- path6.join(os6.homedir(), ".volta", "bin")
3585
+ path7.join(os7.homedir(), "bin"),
3586
+ path7.join(os7.homedir(), ".local", "bin"),
3587
+ path7.join(os7.homedir(), ".cargo", "bin"),
3588
+ path7.join(os7.homedir(), ".npm-global", "bin"),
3589
+ path7.join(os7.homedir(), ".volta", "bin")
3369
3590
  ];
3370
3591
  }
3371
3592
  });
@@ -3469,24 +3690,20 @@ var init_scan_history = __esm({
3469
3690
 
3470
3691
  // src/cli/commands/scan.ts
3471
3692
  import chalk4 from "chalk";
3472
- import fs5 from "fs";
3473
- import path7 from "path";
3474
- import os7 from "os";
3693
+ import fs6 from "fs";
3694
+ import path8 from "path";
3695
+ import os8 from "os";
3475
3696
  import stringWidth2 from "string-width";
3476
3697
  function claudeModelPrice2(model) {
3477
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
3478
- for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
3479
- if (base === key || base.startsWith(key)) return p;
3480
- }
3481
- return null;
3698
+ const t = pricingFor(model);
3699
+ if (!t) return null;
3700
+ const [i, o, cw, cr] = t;
3701
+ return { i, o, cw, cr };
3482
3702
  }
3483
3703
  function geminiModelPrice(model) {
3484
- const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
3485
- for (const [key, p] of Object.entries(GEMINI_PRICING)) {
3486
- if (base === key || base.startsWith(key)) return p;
3487
- }
3488
- if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
3489
- return null;
3704
+ const p = geminiPriceFor(model);
3705
+ if (!p) return null;
3706
+ return { i: p.input, o: p.output, cr: p.cacheRead };
3490
3707
  }
3491
3708
  function isNode9SelfOutput(text) {
3492
3709
  let hits = 0;
@@ -3624,7 +3841,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
3624
3841
  const sessionId = file.replace(/\.jsonl$/, "");
3625
3842
  let raw;
3626
3843
  try {
3627
- raw = fs5.readFileSync(path7.join(projPath, file), "utf-8");
3844
+ raw = fs6.readFileSync(path8.join(projPath, file), "utf-8");
3628
3845
  } catch {
3629
3846
  return;
3630
3847
  }
@@ -3676,7 +3893,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
3676
3893
  if (block.type !== "tool_result") continue;
3677
3894
  const filePath = block.tool_use_id ? toolUseFilePaths.get(block.tool_use_id) : void 0;
3678
3895
  if (filePath) {
3679
- const ext = path7.extname(filePath).toLowerCase();
3896
+ const ext = path8.extname(filePath).toLowerCase();
3680
3897
  if (CODE_EXTENSIONS.has(ext)) continue;
3681
3898
  }
3682
3899
  const resultText = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("\n") : null;
@@ -3733,7 +3950,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
3733
3950
  const rawCmd = String(input.command ?? "").trimStart();
3734
3951
  if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd)) continue;
3735
3952
  const inputFilePath = typeof input.file_path === "string" ? input.file_path : "";
3736
- const inputFileExt = inputFilePath ? path7.extname(inputFilePath).toLowerCase() : "";
3953
+ const inputFileExt = inputFilePath ? path8.extname(inputFilePath).toLowerCase() : "";
3737
3954
  if (CODE_EXTENSIONS.has(inputFileExt)) continue;
3738
3955
  const dlpMatch = scanArgs(input);
3739
3956
  if (dlpMatch) {
@@ -3830,19 +4047,19 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
3830
4047
  }
3831
4048
  }
3832
4049
  async function processClaudeProjectAsync(proj, projectsDir, ruleSources, startDate, result, dedup, onProgress, onLine) {
3833
- const projPath = path7.join(projectsDir, proj);
4050
+ const projPath = path8.join(projectsDir, proj);
3834
4051
  try {
3835
- if (!fs5.statSync(projPath).isDirectory()) return;
4052
+ if (!fs6.statSync(projPath).isDirectory()) return;
3836
4053
  } catch {
3837
4054
  return;
3838
4055
  }
3839
- const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os7.homedir(), "~")).slice(
4056
+ const projLabel = stripTerminalEscapes(decodeURIComponent(proj).replace(os8.homedir(), "~")).slice(
3840
4057
  0,
3841
4058
  40
3842
4059
  );
3843
4060
  let files;
3844
4061
  try {
3845
- files = fs5.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
4062
+ files = fs6.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
3846
4063
  } catch {
3847
4064
  return;
3848
4065
  }
@@ -3880,12 +4097,12 @@ function emptyClaudeScan() {
3880
4097
  };
3881
4098
  }
3882
4099
  async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
3883
- const projectsDir = path7.join(os7.homedir(), ".claude", "projects");
4100
+ const projectsDir = path8.join(os8.homedir(), ".claude", "projects");
3884
4101
  const result = emptyClaudeScan();
3885
- if (!fs5.existsSync(projectsDir)) return result;
4102
+ if (!fs6.existsSync(projectsDir)) return result;
3886
4103
  let projDirs;
3887
4104
  try {
3888
- projDirs = fs5.readdirSync(projectsDir);
4105
+ projDirs = fs6.readdirSync(projectsDir);
3889
4106
  } catch {
3890
4107
  return result;
3891
4108
  }
@@ -3906,7 +4123,7 @@ async function scanClaudeHistoryAsync(startDate, onProgress, onLine) {
3906
4123
  return result;
3907
4124
  }
3908
4125
  function scanGeminiHistory(startDate, onProgress, onLine) {
3909
- const tmpDir = path7.join(os7.homedir(), ".gemini", "tmp");
4126
+ const tmpDir = path8.join(os8.homedir(), ".gemini", "tmp");
3910
4127
  const result = {
3911
4128
  filesScanned: 0,
3912
4129
  sessions: 0,
@@ -3921,50 +4138,64 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
3921
4138
  sessionsWithEarlySecrets: 0
3922
4139
  };
3923
4140
  const dedup = emptyScanDedup();
3924
- if (!fs5.existsSync(tmpDir)) return result;
4141
+ if (!fs6.existsSync(tmpDir)) return result;
3925
4142
  let slugDirs;
3926
4143
  try {
3927
- slugDirs = fs5.readdirSync(tmpDir);
4144
+ slugDirs = fs6.readdirSync(tmpDir);
3928
4145
  } catch {
3929
4146
  return result;
3930
4147
  }
3931
4148
  const ruleSources = buildRuleSources();
3932
4149
  for (const slug of slugDirs) {
3933
- const slugPath = path7.join(tmpDir, slug);
4150
+ const slugPath = path8.join(tmpDir, slug);
3934
4151
  try {
3935
- if (!fs5.statSync(slugPath).isDirectory()) continue;
4152
+ if (!fs6.statSync(slugPath).isDirectory()) continue;
3936
4153
  } catch {
3937
4154
  continue;
3938
4155
  }
3939
4156
  let projLabel = stripTerminalEscapes(slug).slice(0, 40);
3940
4157
  try {
3941
4158
  projLabel = stripTerminalEscapes(
3942
- fs5.readFileSync(path7.join(slugPath, ".project_root"), "utf-8").trim()
3943
- ).replace(os7.homedir(), "~").slice(0, 40);
4159
+ fs6.readFileSync(path8.join(slugPath, ".project_root"), "utf-8").trim()
4160
+ ).replace(os8.homedir(), "~").slice(0, 40);
3944
4161
  } catch {
3945
4162
  }
3946
- const chatsDir = path7.join(slugPath, "chats");
3947
- if (!fs5.existsSync(chatsDir)) continue;
4163
+ const chatsDir = path8.join(slugPath, "chats");
4164
+ if (!fs6.existsSync(chatsDir)) continue;
3948
4165
  let chatFiles;
3949
4166
  try {
3950
- chatFiles = fs5.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
4167
+ chatFiles = fs6.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
3951
4168
  } catch {
3952
4169
  continue;
3953
4170
  }
4171
+ const seenSessions = /* @__PURE__ */ new Set();
3954
4172
  for (const chatFile of chatFiles) {
4173
+ const sessionId = chatFile.replace(/\.jsonl?$/, "");
4174
+ if (seenSessions.has(sessionId)) continue;
4175
+ seenSessions.add(sessionId);
3955
4176
  result.filesScanned++;
3956
4177
  onProgress?.(result.filesScanned);
3957
- const sessionId = chatFile.replace(/\.json$/, "");
3958
4178
  let raw;
3959
4179
  try {
3960
- raw = fs5.readFileSync(path7.join(chatsDir, chatFile), "utf-8");
4180
+ raw = fs6.readFileSync(path8.join(chatsDir, chatFile), "utf-8");
3961
4181
  } catch {
3962
4182
  continue;
3963
4183
  }
3964
4184
  const sessionCalls = [];
3965
4185
  let session;
3966
4186
  try {
3967
- session = JSON.parse(raw);
4187
+ if (chatFile.endsWith(".jsonl")) {
4188
+ const messages = raw.split("\n").filter((l) => l.trim()).map((l) => {
4189
+ try {
4190
+ return JSON.parse(l);
4191
+ } catch {
4192
+ return null;
4193
+ }
4194
+ }).filter((m) => m !== null);
4195
+ session = { messages };
4196
+ } else {
4197
+ session = JSON.parse(raw);
4198
+ }
3968
4199
  } catch {
3969
4200
  continue;
3970
4201
  }
@@ -4119,7 +4350,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
4119
4350
  return result;
4120
4351
  }
4121
4352
  function scanCodexHistory(startDate, onProgress, onLine) {
4122
- const sessionsBase = path7.join(os7.homedir(), ".codex", "sessions");
4353
+ const sessionsBase = path8.join(os8.homedir(), ".codex", "sessions");
4123
4354
  const result = {
4124
4355
  filesScanned: 0,
4125
4356
  sessions: 0,
@@ -4134,32 +4365,32 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4134
4365
  sessionsWithEarlySecrets: 0
4135
4366
  };
4136
4367
  const dedup = emptyScanDedup();
4137
- if (!fs5.existsSync(sessionsBase)) return result;
4368
+ if (!fs6.existsSync(sessionsBase)) return result;
4138
4369
  const jsonlFiles = [];
4139
4370
  try {
4140
- for (const year of fs5.readdirSync(sessionsBase)) {
4141
- const yearPath = path7.join(sessionsBase, year);
4371
+ for (const year of fs6.readdirSync(sessionsBase)) {
4372
+ const yearPath = path8.join(sessionsBase, year);
4142
4373
  try {
4143
- if (!fs5.statSync(yearPath).isDirectory()) continue;
4374
+ if (!fs6.statSync(yearPath).isDirectory()) continue;
4144
4375
  } catch {
4145
4376
  continue;
4146
4377
  }
4147
- for (const month of fs5.readdirSync(yearPath)) {
4148
- const monthPath = path7.join(yearPath, month);
4378
+ for (const month of fs6.readdirSync(yearPath)) {
4379
+ const monthPath = path8.join(yearPath, month);
4149
4380
  try {
4150
- if (!fs5.statSync(monthPath).isDirectory()) continue;
4381
+ if (!fs6.statSync(monthPath).isDirectory()) continue;
4151
4382
  } catch {
4152
4383
  continue;
4153
4384
  }
4154
- for (const day of fs5.readdirSync(monthPath)) {
4155
- const dayPath = path7.join(monthPath, day);
4385
+ for (const day of fs6.readdirSync(monthPath)) {
4386
+ const dayPath = path8.join(monthPath, day);
4156
4387
  try {
4157
- if (!fs5.statSync(dayPath).isDirectory()) continue;
4388
+ if (!fs6.statSync(dayPath).isDirectory()) continue;
4158
4389
  } catch {
4159
4390
  continue;
4160
4391
  }
4161
- for (const file of fs5.readdirSync(dayPath)) {
4162
- if (file.endsWith(".jsonl")) jsonlFiles.push(path7.join(dayPath, file));
4392
+ for (const file of fs6.readdirSync(dayPath)) {
4393
+ if (file.endsWith(".jsonl")) jsonlFiles.push(path8.join(dayPath, file));
4163
4394
  }
4164
4395
  }
4165
4396
  }
@@ -4173,7 +4404,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4173
4404
  onProgress?.(result.filesScanned);
4174
4405
  let lines;
4175
4406
  try {
4176
- lines = fs5.readFileSync(filePath, "utf-8").split("\n");
4407
+ lines = fs6.readFileSync(filePath, "utf-8").split("\n");
4177
4408
  } catch {
4178
4409
  continue;
4179
4410
  }
@@ -4185,6 +4416,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4185
4416
  let lastTotalInput = 0;
4186
4417
  let lastTotalCached = 0;
4187
4418
  let lastTotalOutput = 0;
4419
+ let model = "";
4188
4420
  for (const line of lines) {
4189
4421
  if (!line.trim()) continue;
4190
4422
  onLine?.();
@@ -4199,7 +4431,11 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4199
4431
  sessionId = String(payload["id"] ?? filePath);
4200
4432
  startTime = String(payload["timestamp"] ?? "");
4201
4433
  const cwd = String(payload["cwd"] ?? "");
4202
- projLabel = stripTerminalEscapes(cwd.replace(os7.homedir(), "~")).slice(0, 40);
4434
+ projLabel = stripTerminalEscapes(cwd.replace(os8.homedir(), "~")).slice(0, 40);
4435
+ continue;
4436
+ }
4437
+ if (entry.type === "turn_context" && typeof payload["model"] === "string") {
4438
+ model = payload["model"];
4203
4439
  continue;
4204
4440
  }
4205
4441
  if (entry.type === "event_msg" && payload["type"] === "token_count") {
@@ -4345,13 +4581,16 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4345
4581
  }
4346
4582
  }
4347
4583
  }
4348
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
4349
- result.totalCostUSD += nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
4584
+ result.totalCostUSD += codexSessionCost(model, {
4585
+ input: lastTotalInput,
4586
+ cached: lastTotalCached,
4587
+ output: lastTotalOutput
4588
+ });
4350
4589
  result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "codex"));
4351
4590
  }
4352
4591
  return result;
4353
4592
  }
4354
- var CLAUDE_PRICING2, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
4593
+ var CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
4355
4594
  var init_scan = __esm({
4356
4595
  "src/cli/commands/scan.ts"() {
4357
4596
  "use strict";
@@ -4360,6 +4599,9 @@ var init_scan = __esm({
4360
4599
  init_policy();
4361
4600
  init_dist();
4362
4601
  init_dlp();
4602
+ init_litellm();
4603
+ init_cost_gemini();
4604
+ init_cost_codex();
4363
4605
  init_hook_payload();
4364
4606
  init_dist();
4365
4607
  init_scan_summary();
@@ -4369,26 +4611,6 @@ var init_scan = __esm({
4369
4611
  init_protection();
4370
4612
  init_scan_json();
4371
4613
  init_scan_history();
4372
- CLAUDE_PRICING2 = {
4373
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
4374
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
4375
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
4376
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
4377
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
4378
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
4379
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
4380
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
4381
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
4382
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
4383
- };
4384
- GEMINI_PRICING = {
4385
- "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
4386
- "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
4387
- "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
4388
- "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
4389
- "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
4390
- "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
4391
- };
4392
4614
  CODE_EXTENSIONS = /* @__PURE__ */ new Set([
4393
4615
  ".ts",
4394
4616
  ".tsx",
@@ -4451,23 +4673,23 @@ var init_scan = __esm({
4451
4673
  });
4452
4674
 
4453
4675
  // src/tui/dashboard/data.ts
4454
- import fs6 from "fs";
4455
- import os8 from "os";
4456
- import path8 from "path";
4676
+ import fs7 from "fs";
4677
+ import os9 from "os";
4678
+ import path9 from "path";
4457
4679
  import http from "http";
4458
4680
  function auditLogPath() {
4459
- return path8.join(os8.homedir(), ".node9", "audit.log");
4681
+ return path9.join(os9.homedir(), ".node9", "audit.log");
4460
4682
  }
4461
4683
  function readAuditEntriesAsync(chunkSize = 1e3, customPath) {
4462
4684
  return new Promise((resolve) => {
4463
4685
  const p = customPath ?? auditLogPath();
4464
- if (!fs6.existsSync(p)) {
4686
+ if (!fs7.existsSync(p)) {
4465
4687
  resolve([]);
4466
4688
  return;
4467
4689
  }
4468
4690
  let raw;
4469
4691
  try {
4470
- raw = fs6.readFileSync(p, "utf8");
4692
+ raw = fs7.readFileSync(p, "utf8");
4471
4693
  } catch {
4472
4694
  resolve([]);
4473
4695
  return;
@@ -4596,15 +4818,16 @@ function loadBlast() {
4596
4818
  }
4597
4819
  }
4598
4820
  function shortenPath(p) {
4599
- const home = os8.homedir();
4821
+ const home = os9.homedir();
4600
4822
  return p.startsWith(home) ? p.replace(home, "~") : p;
4601
4823
  }
4602
4824
  async function loadReportAuditAsync(period) {
4603
- const claudeProjectsDir = path8.join(os8.homedir(), ".claude", "projects");
4604
- const codexSessionsDir = path8.join(os8.homedir(), ".codex", "sessions");
4605
- const geminiTmpDir = path8.join(os8.homedir(), ".gemini", "tmp");
4825
+ const claudeProjectsDir = path9.join(os9.homedir(), ".claude", "projects");
4826
+ const codexSessionsDir = path9.join(os9.homedir(), ".codex", "sessions");
4827
+ const geminiTmpDir = path9.join(os9.homedir(), ".gemini", "tmp");
4606
4828
  const { start, end } = getDateRange(period, /* @__PURE__ */ new Date());
4607
4829
  const entries = await readAuditEntriesAsync();
4830
+ void ensurePricingLoaded();
4608
4831
  const claudeCost = await loadClaudeCostAsync(start, end, claudeProjectsDir);
4609
4832
  const codexCost = await loadCodexCostAsync(start, end, codexSessionsDir);
4610
4833
  const geminiCost = await loadGeminiCostAsync(start, end, geminiTmpDir);
@@ -4945,6 +5168,7 @@ var init_data = __esm({
4945
5168
  init_shields();
4946
5169
  init_scan_watermark();
4947
5170
  init_report_audit();
5171
+ init_litellm();
4948
5172
  init_scan();
4949
5173
  init_protection();
4950
5174
  TEST_TOOLS = /* @__PURE__ */ new Set(["Bash", "bash"]);
@@ -5706,8 +5930,8 @@ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-run
5706
5930
  function TopToolsProjects({ audit }) {
5707
5931
  const data = audit?.data;
5708
5932
  const tools = data ? [...data.toolMap.entries()].sort(([, a], [, b]) => b.calls - a.calls).slice(0, ROW_LIMIT) : [];
5709
- const projects = data ? [...data.cost.byProject.entries()].map(([path9, r]) => ({
5710
- name: basenameOf(path9),
5933
+ const projects = data ? [...data.cost.byProject.entries()].map(([path10, r]) => ({
5934
+ name: basenameOf(path10),
5711
5935
  cost: r.cost,
5712
5936
  tokens: r.inputTokens + r.outputTokens
5713
5937
  })).sort((a, b) => b.cost - a.cost).slice(0, ROW_LIMIT) : [];
@@ -6144,8 +6368,8 @@ function pickTopLoopFile(loops) {
6144
6368
  map.set(k, (map.get(k) ?? 0) + (l.count ?? 0));
6145
6369
  }
6146
6370
  if (map.size === 0) return void 0;
6147
- const [path9, count] = [...map.entries()].sort((a, b) => b[1] - a[1])[0];
6148
- return { path: path9, count };
6371
+ const [path10, count] = [...map.entries()].sort((a, b) => b[1] - a[1])[0];
6372
+ return { path: path10, count };
6149
6373
  }
6150
6374
  var EMPTY_FILTERED_SCAN;
6151
6375
  var init_derive = __esm({