@node9/proxy 1.33.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.
Files changed (4) hide show
  1. package/dist/cli.js +1116 -1099
  2. package/dist/cli.mjs +1114 -1097
  3. package/dist/dashboard.mjs +335 -174
  4. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -9252,1113 +9252,1139 @@ var init_setup = __esm({
9252
9252
  }
9253
9253
  });
9254
9254
 
9255
- // src/utils/hook-payload.ts
9256
- function extractToolName(payload, defaultValue = "") {
9257
- return payload.tool_name ?? payload.name ?? payload.toolCall?.name ?? defaultValue;
9255
+ // src/pricing/litellm.ts
9256
+ import fs14 from "fs";
9257
+ import path16 from "path";
9258
+ import os13 from "os";
9259
+ function normalizeModel(raw) {
9260
+ return raw.replace(/-\d{8}$/, "").toLowerCase();
9258
9261
  }
9259
- function extractToolInput(payload) {
9260
- return payload.tool_input ?? payload.args ?? payload.toolCall?.args ?? {};
9262
+ function readCache() {
9263
+ try {
9264
+ const raw = JSON.parse(fs14.readFileSync(CACHE_FILE(), "utf-8"));
9265
+ if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
9266
+ return null;
9267
+ }
9268
+ const ageMs = Date.now() - new Date(raw.fetchedAt).getTime();
9269
+ if (ageMs < 0 || ageMs > TTL_MS) return null;
9270
+ return raw.prices;
9271
+ } catch {
9272
+ return null;
9273
+ }
9261
9274
  }
9262
- function canonicalToolName(name) {
9263
- switch (name) {
9264
- // Hermes Agent
9265
- case "terminal":
9266
- return "Bash";
9267
- case "write_file":
9268
- return "Write";
9269
- case "patch":
9270
- return "Edit";
9271
- case "read_file":
9272
- return "Read";
9273
- case "search_files":
9274
- return "Grep";
9275
- // Antigravity (agy) — shell tool renamed from Gemini's run_shell_command
9276
- case "run_command":
9277
- return "Bash";
9278
- default:
9279
- return name;
9275
+ function writeCache(prices) {
9276
+ try {
9277
+ const target = CACHE_FILE();
9278
+ const dir = path16.dirname(target);
9279
+ if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
9280
+ const tmp = target + ".tmp";
9281
+ const body = {
9282
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
9283
+ prices
9284
+ };
9285
+ fs14.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
9286
+ fs14.renameSync(tmp, target);
9287
+ } catch (err2) {
9288
+ try {
9289
+ fs14.appendFileSync(
9290
+ HOOK_DEBUG_LOG,
9291
+ `[pricing] cache write failed: ${err2.message}
9292
+ `
9293
+ );
9294
+ } catch {
9295
+ }
9280
9296
  }
9281
9297
  }
9282
- function agentLabelFromFlag(flag) {
9283
- if (typeof flag !== "string") return void 0;
9284
- switch (flag.toLowerCase()) {
9285
- case "antigravity":
9286
- case "agy":
9287
- return "Antigravity";
9288
- case "copilot":
9289
- return "GitHub Copilot";
9290
- default:
9291
- return void 0;
9298
+ function tupleFromLiteLLM(entry) {
9299
+ if (!entry || typeof entry !== "object") return null;
9300
+ const e = entry;
9301
+ const num3 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
9302
+ const inCost = num3(e.input_cost_per_token);
9303
+ const outCost = num3(e.output_cost_per_token);
9304
+ if (inCost === 0 && outCost === 0) return null;
9305
+ return [
9306
+ inCost,
9307
+ outCost,
9308
+ num3(e.cache_creation_input_token_cost),
9309
+ num3(e.cache_read_input_token_cost)
9310
+ ];
9311
+ }
9312
+ async function fetchLiteLLMPricing() {
9313
+ try {
9314
+ const res = await fetch(LITELLM_URL, {
9315
+ signal: AbortSignal.timeout(15e3)
9316
+ });
9317
+ if (!res.ok) return null;
9318
+ const json = await res.json();
9319
+ if (!json || typeof json !== "object") return null;
9320
+ const out = {};
9321
+ for (const [key, value] of Object.entries(json)) {
9322
+ const tuple = tupleFromLiteLLM(value);
9323
+ if (tuple) out[key.toLowerCase()] = tuple;
9324
+ }
9325
+ if (Object.keys(out).length < 10) {
9326
+ return null;
9327
+ }
9328
+ return out;
9329
+ } catch {
9330
+ return null;
9292
9331
  }
9293
9332
  }
9294
- function canonicalToolInput(rawToolName, input) {
9295
- if (rawToolName !== "run_command") return input;
9296
- if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
9297
- const args = input;
9298
- if (typeof args.CommandLine !== "string") return input;
9299
- const { CommandLine, Cwd, ...rest } = args;
9300
- const canonical = { ...rest, command: CommandLine };
9301
- if (typeof Cwd === "string" && Cwd.length > 0) canonical.cwd = Cwd;
9302
- return canonical;
9333
+ async function ensurePricingLoaded() {
9334
+ if (memCache !== null && Date.now() - memCacheAt < TTL_MS) return;
9335
+ const fromDisk = readCache();
9336
+ if (fromDisk && Object.keys(fromDisk).length > 0) {
9337
+ memCache = fromDisk;
9338
+ memCacheAt = Date.now();
9339
+ lookupCache.clear();
9340
+ return;
9341
+ }
9342
+ const fetched = await fetchLiteLLMPricing();
9343
+ if (fetched && Object.keys(fetched).length > 0) {
9344
+ memCache = fetched;
9345
+ memCacheAt = Date.now();
9346
+ writeCache(fetched);
9347
+ lookupCache.clear();
9348
+ return;
9349
+ }
9350
+ memCache = { ...BUNDLED_PRICING };
9351
+ memCacheAt = Date.now();
9352
+ lookupCache.clear();
9303
9353
  }
9304
- var init_hook_payload = __esm({
9305
- "src/utils/hook-payload.ts"() {
9354
+ function pricingFor(model) {
9355
+ const norm = normalizeModel(model);
9356
+ const cached = lookupCache.get(norm);
9357
+ if (cached !== void 0) return cached;
9358
+ if (memCache === null && !diskChecked) {
9359
+ diskChecked = true;
9360
+ const disk = readCache();
9361
+ if (disk && Object.keys(disk).length > 0) {
9362
+ memCache = disk;
9363
+ memCacheAt = Date.now();
9364
+ }
9365
+ }
9366
+ const sources = [];
9367
+ if (memCache) sources.push(memCache);
9368
+ sources.push(BUNDLED_PRICING);
9369
+ let resolved = null;
9370
+ for (const source of sources) {
9371
+ const exact = source[norm];
9372
+ if (exact) {
9373
+ resolved = exact;
9374
+ break;
9375
+ }
9376
+ let best = null;
9377
+ for (const key of Object.keys(source)) {
9378
+ if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
9379
+ best = key;
9380
+ }
9381
+ }
9382
+ if (best) {
9383
+ resolved = source[best];
9384
+ break;
9385
+ }
9386
+ }
9387
+ lookupCache.set(norm, resolved);
9388
+ return resolved;
9389
+ }
9390
+ var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, diskChecked, lookupCache;
9391
+ var init_litellm = __esm({
9392
+ "src/pricing/litellm.ts"() {
9306
9393
  "use strict";
9394
+ init_audit();
9395
+ LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
9396
+ BUNDLED_PRICING = {
9397
+ // Anthropic
9398
+ "claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
9399
+ "claude-opus-4-1": [5e-6, 25e-6, 625e-8, 5e-7],
9400
+ "claude-opus-4-5": [5e-6, 25e-6, 625e-8, 5e-7],
9401
+ "claude-opus-4-6": [5e-6, 25e-6, 625e-8, 5e-7],
9402
+ "claude-opus-4-7": [5e-6, 25e-6, 625e-8, 5e-7],
9403
+ "claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
9404
+ "claude-sonnet-4-5": [3e-6, 15e-6, 375e-8, 3e-7],
9405
+ "claude-sonnet-4-6": [3e-6, 15e-6, 375e-8, 3e-7],
9406
+ "claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
9407
+ "claude-haiku-4-5": [8e-7, 4e-6, 1e-6, 8e-8],
9408
+ "claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
9409
+ "claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
9410
+ "claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
9411
+ "claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
9412
+ // OpenAI. gpt-5 family + o-series copied from the live LiteLLM table
9413
+ // (verified 2026-06-14) — the bundled gpt-5 was stale at $10/$30 vs the real
9414
+ // $1.25/$10, and Codex models (gpt-5-codex etc.) were absent, so the offline
9415
+ // fallback mispriced every Codex session. See cost-codex.codexPriceFor.
9416
+ "gpt-4o": [5e-6, 15e-6, 0, 25e-7],
9417
+ "gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
9418
+ "gpt-5": [125e-8, 1e-5, 0, 125e-9],
9419
+ "gpt-5-codex": [125e-8, 1e-5, 0, 125e-9],
9420
+ "gpt-5-mini": [25e-8, 2e-6, 0, 25e-9],
9421
+ o3: [2e-6, 8e-6, 0, 5e-7],
9422
+ "o4-mini": [11e-7, 44e-7, 0, 275e-9],
9423
+ // Google. Values copied from the live LiteLLM table (verified 2026-06-14)
9424
+ // so the bundled fallback prices the current Gemini tiers correctly offline
9425
+ // — the local cost readers were carrying a stale hardcoded copy where
9426
+ // gemini-2.5-flash read $0.15/$0.60 vs the real $0.30/$2.50 (~4× under on
9427
+ // output). See cost-gemini.geminiPriceFor (the single Gemini price source).
9428
+ "gemini-2.5-pro": [125e-8, 1e-5, 0, 125e-9],
9429
+ "gemini-2.5-flash": [3e-7, 25e-7, 0, 3e-8],
9430
+ "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
9431
+ "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
9432
+ };
9433
+ CACHE_FILE = () => path16.join(os13.homedir(), ".node9", "model-pricing.json");
9434
+ TTL_MS = 24 * 60 * 60 * 1e3;
9435
+ memCache = null;
9436
+ memCacheAt = 0;
9437
+ diskChecked = false;
9438
+ lookupCache = /* @__PURE__ */ new Map();
9307
9439
  }
9308
9440
  });
9309
9441
 
9310
- // src/scan-summary.ts
9311
- function agentDisplayName(agent) {
9312
- return AGENT_LONG[agent] ?? "Claude Code";
9442
+ // src/cost-gemini.ts
9443
+ import fs15 from "fs";
9444
+ import os14 from "os";
9445
+ import path17 from "path";
9446
+ function geminiTmpDir() {
9447
+ return path17.join(os14.homedir(), ".gemini", "tmp");
9313
9448
  }
9314
- function agentBadgeText(agent, width = 10) {
9315
- return `[${AGENT_SHORT[agent] ?? "Claude"}]`.padEnd(width);
9449
+ function geminiPriceFor(model) {
9450
+ let tuple = pricingFor(model);
9451
+ if (!tuple && /^gemini-/i.test(model)) {
9452
+ for (const proxy of GEMINI_FALLBACK_MODELS) {
9453
+ tuple = pricingFor(proxy);
9454
+ if (tuple) break;
9455
+ }
9456
+ }
9457
+ if (!tuple) return null;
9458
+ return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
9316
9459
  }
9317
- function agentColorName(agent) {
9318
- switch (agent) {
9319
- case "gemini":
9320
- return "blue";
9321
- case "codex":
9322
- return "magenta";
9323
- case "antigravity":
9324
- return "yellow";
9325
- case "copilot":
9326
- return "green";
9327
- case "shell":
9328
- return "yellow";
9329
- default:
9330
- return "cyan";
9460
+ function safeReaddir(dir) {
9461
+ try {
9462
+ return fs15.readdirSync(dir);
9463
+ } catch {
9464
+ return [];
9331
9465
  }
9332
9466
  }
9333
- function buildScanSummary(agents) {
9334
- const stats = {
9335
- sessions: 0,
9336
- totalToolCalls: 0,
9337
- bashCalls: 0,
9338
- totalCostUSD: 0,
9339
- firstDate: null,
9340
- lastDate: null
9341
- };
9342
- for (const a of agents) {
9343
- stats.sessions += a.scan.sessions;
9344
- stats.totalToolCalls += a.scan.totalToolCalls;
9345
- stats.bashCalls += a.scan.bashCalls;
9346
- stats.totalCostUSD += a.scan.totalCostUSD;
9347
- if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
9348
- stats.firstDate = a.scan.firstDate;
9349
- }
9350
- if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
9351
- stats.lastDate = a.scan.lastDate;
9467
+ function isDir(p) {
9468
+ try {
9469
+ return fs15.statSync(p).isDirectory();
9470
+ } catch {
9471
+ return false;
9472
+ }
9473
+ }
9474
+ function listGeminiSessionFiles(base) {
9475
+ const out = [];
9476
+ for (const project of safeReaddir(base)) {
9477
+ const chats = path17.join(base, project, "chats");
9478
+ if (!isDir(chats)) continue;
9479
+ for (const f of safeReaddir(chats)) {
9480
+ if (f.startsWith("session-") && f.endsWith(".jsonl")) {
9481
+ out.push({ file: path17.join(chats, f), project });
9482
+ }
9352
9483
  }
9353
9484
  }
9354
- const allFindings = agents.flatMap((a) => a.scan.findings);
9355
- const allLeaks = agents.flatMap(
9356
- (a) => a.scan.dlpFindings.map((f) => ({
9357
- patternName: f.patternName,
9358
- redactedSample: f.redactedSample,
9359
- toolName: f.toolName,
9360
- timestamp: f.timestamp,
9361
- project: f.project,
9362
- sessionId: f.sessionId,
9363
- agent: f.agent
9364
- }))
9365
- );
9366
- const allLoops = agents.flatMap(
9367
- (a) => a.scan.loopFindings.map((f) => ({
9368
- toolName: f.toolName,
9369
- commandPreview: f.commandPreview,
9370
- count: f.count,
9371
- timestamp: f.timestamp,
9372
- project: f.project,
9373
- sessionId: f.sessionId,
9374
- agent: f.agent,
9375
- kind: f.kind
9376
- }))
9377
- );
9378
- const byVerdict = {
9379
- blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
9380
- supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
9381
- leaks: allLeaks.length,
9382
- loops: allLoops.length
9383
- };
9384
- const byAgent = agents.map((a) => ({
9385
- id: a.id,
9386
- label: a.label,
9387
- icon: a.icon,
9388
- sessions: a.scan.sessions,
9389
- findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
9390
- costUSD: a.scan.totalCostUSD
9391
- })).filter((s) => s.sessions > 0 || s.findings > 0);
9392
- const sections = buildSections(allFindings);
9393
- const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
9394
- const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
9395
- return {
9396
- stats,
9397
- byVerdict,
9398
- byAgent,
9399
- sections,
9400
- leaks: allLeaks,
9401
- loops: allLoops,
9402
- loopWastedUSD
9403
- };
9485
+ return out;
9404
9486
  }
9405
- function buildSections(findings) {
9406
- const sectionMap = /* @__PURE__ */ new Map();
9407
- function ensureSection(id, label, subtitle, sourceType, shieldKey) {
9408
- let s = sectionMap.get(id);
9409
- if (!s) {
9410
- s = {
9411
- id,
9412
- label,
9413
- subtitle,
9414
- sourceType,
9415
- shieldKey,
9416
- blockedCount: 0,
9417
- reviewCount: 0,
9418
- rules: []
9419
- };
9420
- sectionMap.set(id, s);
9421
- }
9422
- return s;
9423
- }
9424
- const ruleMap = /* @__PURE__ */ new Map();
9425
- for (const f of findings) {
9426
- const src = f.source;
9427
- const sourceType = src.sourceType;
9428
- const shieldName = src.shieldName;
9429
- const verdict = src.rule.verdict === "block" ? "block" : "review";
9430
- let sectionId;
9431
- let sectionLabel;
9432
- let sectionSubtitle;
9433
- let shieldKey;
9434
- if (sourceType === "default") {
9435
- sectionId = "default";
9436
- sectionLabel = "Default Rules";
9437
- sectionSubtitle = "built-in, always on";
9438
- } else if (sourceType === "shield") {
9439
- sectionId = `shield:${shieldName}`;
9440
- sectionLabel = shieldName;
9441
- sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
9442
- shieldKey = shieldName;
9443
- } else if (shieldName === "cloud") {
9444
- sectionId = "cloud";
9445
- sectionLabel = "Cloud Policy";
9446
- sectionSubtitle = "synced from node9 cloud";
9447
- } else {
9448
- sectionId = "user";
9449
- sectionLabel = "Your Rules";
9450
- sectionSubtitle = "added in node9.config.json";
9487
+ function parseGeminiSession(lines, project) {
9488
+ const seenIds = /* @__PURE__ */ new Set();
9489
+ const byKey = /* @__PURE__ */ new Map();
9490
+ let runId = "";
9491
+ for (const raw of lines) {
9492
+ if (!raw.trim()) continue;
9493
+ let obj;
9494
+ try {
9495
+ obj = JSON.parse(raw);
9496
+ } catch {
9497
+ continue;
9451
9498
  }
9452
- const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
9453
- const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
9454
- const ruleKey = sectionId + "::" + ruleDisplayName;
9455
- let rule = ruleMap.get(ruleKey);
9456
- if (!rule) {
9457
- rule = {
9458
- name: ruleDisplayName,
9459
- verdict,
9460
- reason: src.rule.reason ?? "",
9461
- findings: []
9462
- };
9463
- ruleMap.set(ruleKey, rule);
9464
- section.rules.push(rule);
9499
+ if (typeof obj.sessionId === "string" && !runId) runId = obj.sessionId;
9500
+ if (!obj.tokens || !obj.model || !obj.timestamp) continue;
9501
+ if (obj.id) {
9502
+ if (seenIds.has(obj.id)) continue;
9503
+ seenIds.add(obj.id);
9465
9504
  }
9466
- const cmdPreview = previewCommand(f.input, 120);
9467
- const fullCmd = fullCommandOf(f.input);
9468
- const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
9469
- if (!isDupe) {
9470
- rule.findings.push({
9471
- timestamp: f.timestamp ?? "",
9472
- command: cmdPreview,
9473
- fullCommand: fullCmd,
9474
- project: f.project,
9475
- sessionId: f.sessionId,
9476
- agent: f.agent,
9477
- toolName: f.toolName
9505
+ const price = geminiPriceFor(obj.model);
9506
+ if (!price) continue;
9507
+ const inp = obj.tokens.input ?? 0;
9508
+ const out = obj.tokens.output ?? 0;
9509
+ const cached = Math.min(obj.tokens.cached ?? 0, inp);
9510
+ const fresh = Math.max(0, inp - cached);
9511
+ const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
9512
+ const date = obj.timestamp.slice(0, 10);
9513
+ const model = normalizeModel(obj.model);
9514
+ const key = `${date}::${model}`;
9515
+ const prev = byKey.get(key);
9516
+ if (prev) {
9517
+ prev.costUSD += cost;
9518
+ prev.inputTokens += fresh;
9519
+ prev.outputTokens += out;
9520
+ prev.cacheReadTokens += cached;
9521
+ } else {
9522
+ byKey.set(key, {
9523
+ date,
9524
+ model,
9525
+ workingDir: project,
9526
+ runId,
9527
+ costUSD: cost,
9528
+ inputTokens: fresh,
9529
+ outputTokens: out,
9530
+ cacheReadTokens: cached,
9531
+ cacheWriteTokens: 0
9478
9532
  });
9479
9533
  }
9480
- if (verdict === "block") section.blockedCount++;
9481
- else section.reviewCount++;
9482
9534
  }
9483
- const sections = [...sectionMap.values()];
9484
- sections.sort((a, b) => {
9485
- const aTotal = a.blockedCount + a.reviewCount;
9486
- const bTotal = b.blockedCount + b.reviewCount;
9487
- if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
9488
- return bTotal - aTotal;
9489
- });
9490
- for (const s of sections) {
9491
- s.rules.sort((a, b) => {
9492
- const aBlock = a.verdict === "block" ? 1 : 0;
9493
- const bBlock = b.verdict === "block" ? 1 : 0;
9494
- if (bBlock !== aBlock) return bBlock - aBlock;
9495
- return b.findings.length - a.findings.length;
9496
- });
9497
- }
9498
- return sections;
9499
- }
9500
- function previewCommand(input, max) {
9501
- const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9502
- const s = String(raw).replace(/\s+/g, " ").trim();
9503
- return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
9504
- }
9505
- function fullCommandOf(input) {
9506
- const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9507
- return String(raw).replace(/\s+/g, " ").trim();
9535
+ if (runId) for (const e of byKey.values()) e.runId = runId;
9536
+ return [...byKey.values()];
9508
9537
  }
9509
- var AGENT_SHORT, AGENT_LONG;
9510
- var init_scan_summary = __esm({
9511
- "src/scan-summary.ts"() {
9538
+ var GEMINI_FALLBACK_MODELS, geminiSource;
9539
+ var init_cost_gemini = __esm({
9540
+ "src/cost-gemini.ts"() {
9512
9541
  "use strict";
9513
- init_shields();
9514
- init_dist();
9515
- init_dist();
9516
- AGENT_SHORT = {
9517
- claude: "Claude",
9518
- gemini: "Gemini",
9519
- codex: "Codex",
9520
- antigravity: "Agy",
9521
- copilot: "Copilot",
9522
- shell: "Shell"
9523
- };
9524
- AGENT_LONG = {
9525
- claude: "Claude Code",
9526
- gemini: "Gemini CLI",
9527
- codex: "Codex",
9528
- antigravity: "Antigravity",
9529
- copilot: "GitHub Copilot",
9530
- shell: "Shell"
9531
- };
9532
- }
9533
- });
9534
-
9535
- // src/cli/commands/blast.ts
9536
- import chalk2 from "chalk";
9537
- import fs14 from "fs";
9538
- import path16 from "path";
9539
- import os13 from "os";
9540
- function buildSensitivePaths(home, cwd) {
9541
- return [
9542
- {
9543
- full: path16.join(home, ".ssh", "id_rsa"),
9544
- label: "~/.ssh/id_rsa",
9545
- description: "RSA private key \u2014 grants SSH access to your servers",
9546
- score: 20
9547
- },
9548
- {
9549
- full: path16.join(home, ".ssh", "id_ed25519"),
9550
- label: "~/.ssh/id_ed25519",
9551
- description: "Ed25519 private key \u2014 grants SSH access to your servers",
9552
- score: 20
9553
- },
9554
- {
9555
- full: path16.join(home, ".ssh", "id_ecdsa"),
9556
- label: "~/.ssh/id_ecdsa",
9557
- description: "ECDSA private key \u2014 grants SSH access to your servers",
9558
- score: 20
9559
- },
9560
- {
9561
- full: path16.join(home, ".aws", "credentials"),
9562
- label: "~/.aws/credentials",
9563
- description: "AWS access keys \u2014 full cloud account access",
9564
- score: 20
9565
- },
9566
- {
9567
- full: path16.join(home, ".aws", "config"),
9568
- label: "~/.aws/config",
9569
- description: "AWS configuration \u2014 account and region settings",
9570
- score: 5
9571
- },
9572
- {
9573
- full: path16.join(home, ".config", "gcloud", "credentials.db"),
9574
- label: "~/.config/gcloud/credentials.db",
9575
- description: "Google Cloud credentials",
9576
- score: 15
9577
- },
9578
- {
9579
- full: path16.join(home, ".docker", "config.json"),
9580
- label: "~/.docker/config.json",
9581
- description: "Docker registry auth tokens",
9582
- score: 10
9583
- },
9584
- {
9585
- full: path16.join(home, ".netrc"),
9586
- label: "~/.netrc",
9587
- description: "FTP/HTTP credentials in plain text",
9588
- score: 15
9589
- },
9590
- {
9591
- full: path16.join(home, ".npmrc"),
9592
- label: "~/.npmrc",
9593
- description: "npm auth token \u2014 can publish packages as you",
9594
- score: 10
9595
- },
9596
- {
9597
- full: path16.join(home, ".node9", "credentials.json"),
9598
- label: "~/.node9/credentials.json",
9599
- description: "Node9 cloud API key",
9600
- score: 10
9601
- },
9602
- {
9603
- full: path16.join(cwd, ".env"),
9604
- label: ".env (current folder)",
9605
- description: "App secrets \u2014 database passwords, API keys",
9606
- score: 20
9607
- },
9608
- {
9609
- full: path16.join(cwd, ".env.local"),
9610
- label: ".env.local (current folder)",
9611
- description: "Local overrides \u2014 often contains real credentials",
9612
- score: 15
9613
- },
9614
- {
9615
- full: path16.join(cwd, ".env.production"),
9616
- label: ".env.production (current folder)",
9617
- description: "Production secrets",
9618
- score: 20
9619
- }
9620
- ];
9621
- }
9622
- function isReadable(filePath) {
9623
- try {
9624
- fs14.accessSync(filePath, fs14.constants.R_OK);
9625
- return true;
9626
- } catch {
9627
- return false;
9628
- }
9629
- }
9630
- function scoreLabel(score) {
9631
- if (score >= 80) return chalk2.green(`${score}/100 Good`);
9632
- if (score >= 50) return chalk2.yellow(`${score}/100 Moderate risk`);
9633
- if (score >= 25) return chalk2.red(`${score}/100 High risk`);
9634
- return chalk2.red.bold(`${score}/100 Critical`);
9635
- }
9636
- function runBlast() {
9637
- const home = os13.homedir();
9638
- const cwd = process.cwd();
9639
- const paths = buildSensitivePaths(home, cwd);
9640
- let scoreDeduction = 0;
9641
- const reachable = [];
9642
- for (const p of paths) {
9643
- if (fs14.existsSync(p.full) && isReadable(p.full)) {
9644
- reachable.push(p);
9645
- scoreDeduction += p.score;
9646
- }
9647
- }
9648
- const envFindings = [];
9649
- for (const [key, value] of Object.entries(process.env)) {
9650
- if (!value) continue;
9651
- const match = scanArgs({ [key]: value });
9652
- if (match) {
9653
- envFindings.push({ key, patternName: match.patternName });
9654
- scoreDeduction += 10;
9655
- }
9656
- }
9657
- return { reachable, envFindings, score: Math.max(0, 100 - scoreDeduction) };
9658
- }
9659
- function registerBlastCommand(program2) {
9660
- program2.command("blast").description("Map what an AI agent can currently reach on this machine").action(() => {
9661
- const home = os13.homedir();
9662
- const cwd = process.cwd();
9663
- const { reachable, envFindings, score } = runBlast();
9664
- console.log("");
9665
- console.log(
9666
- chalk2.bold(" \u{1F52D} Node9 Blast Radius") + chalk2.dim(" \xB7 what an AI agent can reach from here")
9667
- );
9668
- console.log(chalk2.dim(" Running in: ") + chalk2.white(cwd.replace(home, "~")));
9669
- console.log("");
9670
- if (reachable.length > 0) {
9671
- console.log(" " + chalk2.red.bold("Sensitive files reachable:"));
9672
- for (const p of reachable) {
9673
- console.log(
9674
- " " + chalk2.red("\u2717 ") + chalk2.yellow(p.label.padEnd(38)) + chalk2.dim(p.description)
9675
- );
9676
- }
9677
- console.log("");
9678
- }
9679
- if (envFindings.length > 0) {
9680
- console.log(" " + chalk2.red.bold("Secrets in active environment:"));
9681
- for (const f of envFindings) {
9682
- console.log(
9683
- " " + chalk2.red("\u2717 ") + chalk2.yellow(f.key.padEnd(38)) + chalk2.dim(f.patternName)
9684
- );
9542
+ init_litellm();
9543
+ GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
9544
+ geminiSource = {
9545
+ id: "gemini",
9546
+ available() {
9547
+ try {
9548
+ return fs15.existsSync(geminiTmpDir());
9549
+ } catch {
9550
+ return false;
9551
+ }
9552
+ },
9553
+ collect(sinceMs) {
9554
+ const combined = /* @__PURE__ */ new Map();
9555
+ for (const { file, project } of listGeminiSessionFiles(geminiTmpDir())) {
9556
+ try {
9557
+ if (sinceMs !== void 0 && fs15.statSync(file).mtimeMs < sinceMs) continue;
9558
+ } catch {
9559
+ continue;
9560
+ }
9561
+ let content;
9562
+ try {
9563
+ content = fs15.readFileSync(file, "utf8");
9564
+ } catch {
9565
+ continue;
9566
+ }
9567
+ for (const e of parseGeminiSession(content.split("\n"), project)) {
9568
+ const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
9569
+ const prev = combined.get(key);
9570
+ if (prev) {
9571
+ prev.costUSD += e.costUSD;
9572
+ prev.inputTokens += e.inputTokens;
9573
+ prev.outputTokens += e.outputTokens;
9574
+ prev.cacheReadTokens += e.cacheReadTokens;
9575
+ prev.cacheWriteTokens += e.cacheWriteTokens;
9576
+ } else {
9577
+ combined.set(key, { ...e });
9578
+ }
9579
+ }
9580
+ }
9581
+ return [...combined.values()];
9685
9582
  }
9686
- console.log("");
9687
- }
9688
- console.log(" " + chalk2.dim("\u2500".repeat(70)));
9689
- if (reachable.length === 0 && envFindings.length === 0) {
9690
- console.log(" " + chalk2.green("\u2705 No sensitive files or environment secrets found."));
9691
- console.log(" Security Score: " + scoreLabel(score));
9692
- } else {
9693
- console.log(
9694
- " Security Score: " + scoreLabel(score) + chalk2.dim(
9695
- ` (${reachable.length} file${reachable.length !== 1 ? "s" : ""}, ${envFindings.length} env var${envFindings.length !== 1 ? "s" : ""})`
9696
- )
9697
- );
9698
- console.log("");
9699
- console.log(
9700
- chalk2.dim(
9701
- " Every AI agent you start can read the files and env vars listed above.\n Run `node9 shield enable project-jail` to restrict agent file access.\n Run `node9 mask` to redact secrets from existing session history."
9702
- )
9703
- );
9704
- }
9705
- console.log("");
9706
- });
9707
- }
9708
- var init_blast = __esm({
9709
- "src/cli/commands/blast.ts"() {
9710
- "use strict";
9711
- init_dlp();
9583
+ };
9712
9584
  }
9713
9585
  });
9714
9586
 
9715
- // src/cli/render/scan-derive.ts
9716
- import chalk3 from "chalk";
9717
- import stringWidth from "string-width";
9718
- function classifyScore(score) {
9719
- if (score >= 80) return { band: "good", label: "Good", color: chalk3.green };
9720
- if (score >= 50) return { band: "at-risk", label: "At Risk", color: chalk3.yellow };
9721
- return { band: "critical", label: "Critical", color: chalk3.red };
9587
+ // src/cost-codex.ts
9588
+ import fs16 from "fs";
9589
+ import os15 from "os";
9590
+ import path18 from "path";
9591
+ function codexSessionsDir() {
9592
+ return path18.join(os15.homedir(), ".codex", "sessions");
9722
9593
  }
9723
- function topDlpPatterns(findings, n) {
9724
- const counts = /* @__PURE__ */ new Map();
9725
- for (const f of findings) {
9726
- counts.set(f.patternName, (counts.get(f.patternName) ?? 0) + 1);
9727
- }
9728
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([name, count]) => ({ name, count }));
9594
+ function codexPriceFor(model) {
9595
+ return pricingFor(model) ?? CODEX_FALLBACK;
9729
9596
  }
9730
- function topRulesByVerdict(sections, verdict, n) {
9731
- const matched = [];
9732
- for (const section of sections) {
9733
- for (const rule of section.rules) {
9734
- const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
9735
- if (matches) matched.push({ name: rule.name, count: rule.findings.length });
9597
+ function codexSessionCost(model, tokens) {
9598
+ const nonCached = Math.max(0, tokens.input - tokens.cached);
9599
+ const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
9600
+ return nonCached * pin + tokens.cached * pcr + tokens.output * pout;
9601
+ }
9602
+ function listCodexSessionFiles(base) {
9603
+ const out = [];
9604
+ for (const y of safeReaddir2(base)) {
9605
+ const yp = path18.join(base, y);
9606
+ if (!isDir2(yp)) continue;
9607
+ for (const m of safeReaddir2(yp)) {
9608
+ const mp = path18.join(yp, m);
9609
+ if (!isDir2(mp)) continue;
9610
+ for (const d of safeReaddir2(mp)) {
9611
+ const dp = path18.join(mp, d);
9612
+ if (!isDir2(dp)) continue;
9613
+ for (const f of safeReaddir2(dp)) {
9614
+ if (f.endsWith(".jsonl")) out.push(path18.join(dp, f));
9615
+ }
9616
+ }
9736
9617
  }
9737
9618
  }
9738
- return matched.sort((a, b) => b.count - a.count).slice(0, n);
9739
- }
9740
- function computeLoopWaste(loops, totalToolCalls) {
9741
- const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
9742
- const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
9743
- return { wastedCalls, wastePct };
9619
+ return out;
9744
9620
  }
9745
- function rollupByShield(sections, topRulesPerShield = 3) {
9746
- const out = [];
9747
- for (const section of sections) {
9748
- if (section.sourceType !== "shield") continue;
9749
- if (!section.shieldKey) continue;
9750
- const totalCatches = section.blockedCount + section.reviewCount;
9751
- 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);
9752
- out.push({
9753
- shieldName: section.shieldKey,
9754
- totalCatches,
9755
- blockCatches: section.blockedCount,
9756
- reviewCatches: section.reviewCount,
9757
- topRuleLabels
9758
- });
9621
+ function safeReaddir2(dir) {
9622
+ try {
9623
+ return fs16.readdirSync(dir);
9624
+ } catch {
9625
+ return [];
9759
9626
  }
9760
- return out.sort((a, b) => b.totalCatches - a.totalCatches);
9761
9627
  }
9762
- function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
9763
- const inner = width - 4;
9764
- const out = [];
9765
- const titlePad = ` ${title} `;
9766
- const titleWidth = stringWidth(titlePad);
9767
- const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
9768
- const dashFill = "\u2500".repeat(Math.max(0, inner - stringWidth(titleSegment)));
9769
- out.push(chalk3.dim("\u256D\u2500") + chalk3.bold(titleSegment) + chalk3.dim(`${dashFill}\u2500\u256E`));
9770
- for (const line of bodyLines) {
9771
- const padding = " ".repeat(Math.max(0, inner - line.width));
9772
- out.push(chalk3.dim("\u2502 ") + line.rendered + padding + chalk3.dim(" \u2502"));
9628
+ function isDir2(p) {
9629
+ try {
9630
+ return fs16.statSync(p).isDirectory();
9631
+ } catch {
9632
+ return false;
9773
9633
  }
9774
- out.push(chalk3.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
9775
- return out;
9776
9634
  }
9777
- function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
9778
- const t = new Date(timestamp).getTime();
9779
- if (Number.isNaN(t)) return "?";
9780
- const days = Math.floor((now.getTime() - t) / 864e5);
9781
- if (days < 1) return "today";
9782
- if (days > 90) return "90d+";
9783
- return `${days}d`;
9635
+ function parseCodexSession(lines) {
9636
+ let sessionStart2 = "";
9637
+ let runId = "";
9638
+ let cwd = "";
9639
+ let model = "";
9640
+ let input = 0;
9641
+ let cached = 0;
9642
+ let output = 0;
9643
+ let sawUsage = false;
9644
+ for (const raw of lines) {
9645
+ if (!raw.trim()) continue;
9646
+ let entry;
9647
+ try {
9648
+ entry = JSON.parse(raw);
9649
+ } catch {
9650
+ continue;
9651
+ }
9652
+ const p = entry.payload ?? {};
9653
+ if (entry.type === "session_meta") {
9654
+ if (!sessionStart2 && typeof p["timestamp"] === "string") sessionStart2 = p["timestamp"];
9655
+ if (!runId && typeof p["id"] === "string") runId = p["id"];
9656
+ if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
9657
+ continue;
9658
+ }
9659
+ if (entry.type === "turn_context") {
9660
+ if (typeof p["model"] === "string") model = p["model"];
9661
+ if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
9662
+ continue;
9663
+ }
9664
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
9665
+ const info = p["info"] ?? {};
9666
+ const usage = info["total_token_usage"] ?? {};
9667
+ if (typeof usage["input_tokens"] === "number") input = usage["input_tokens"];
9668
+ if (typeof usage["cached_input_tokens"] === "number") cached = usage["cached_input_tokens"];
9669
+ if (typeof usage["output_tokens"] === "number") output = usage["output_tokens"];
9670
+ sawUsage = true;
9671
+ }
9672
+ }
9673
+ if (!sessionStart2 || !sawUsage) return null;
9674
+ const nonCached = Math.max(0, input - cached);
9675
+ if (nonCached === 0 && output === 0 && cached === 0) return null;
9676
+ const norm = normalizeModel(model || "gpt-5");
9677
+ const costUSD = codexSessionCost(model, { input, cached, output });
9678
+ return {
9679
+ date: sessionStart2.slice(0, 10),
9680
+ model: norm,
9681
+ workingDir: cwd,
9682
+ runId,
9683
+ costUSD,
9684
+ inputTokens: nonCached,
9685
+ outputTokens: output,
9686
+ cacheReadTokens: cached,
9687
+ cacheWriteTokens: 0
9688
+ };
9784
9689
  }
9785
- var PANEL_WIDTH;
9786
- var init_scan_derive = __esm({
9787
- "src/cli/render/scan-derive.ts"() {
9690
+ var CODEX_FALLBACK, codexSource;
9691
+ var init_cost_codex = __esm({
9692
+ "src/cost-codex.ts"() {
9788
9693
  "use strict";
9789
- PANEL_WIDTH = 76;
9694
+ init_litellm();
9695
+ CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
9696
+ codexSource = {
9697
+ id: "codex",
9698
+ available() {
9699
+ try {
9700
+ return fs16.existsSync(codexSessionsDir());
9701
+ } catch {
9702
+ return false;
9703
+ }
9704
+ },
9705
+ collect(sinceMs) {
9706
+ const base = codexSessionsDir();
9707
+ const combined = /* @__PURE__ */ new Map();
9708
+ for (const file of listCodexSessionFiles(base)) {
9709
+ try {
9710
+ if (sinceMs !== void 0 && fs16.statSync(file).mtimeMs < sinceMs) continue;
9711
+ } catch {
9712
+ continue;
9713
+ }
9714
+ let content;
9715
+ try {
9716
+ content = fs16.readFileSync(file, "utf8");
9717
+ } catch {
9718
+ continue;
9719
+ }
9720
+ const e = parseCodexSession(content.split("\n"));
9721
+ if (!e) continue;
9722
+ const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
9723
+ const prev = combined.get(key);
9724
+ if (prev) {
9725
+ prev.costUSD += e.costUSD;
9726
+ prev.inputTokens += e.inputTokens;
9727
+ prev.outputTokens += e.outputTokens;
9728
+ prev.cacheReadTokens += e.cacheReadTokens;
9729
+ prev.cacheWriteTokens += e.cacheWriteTokens;
9730
+ } else {
9731
+ combined.set(key, { ...e });
9732
+ }
9733
+ }
9734
+ return [...combined.values()];
9735
+ }
9736
+ };
9737
+ }
9738
+ });
9739
+
9740
+ // src/utils/hook-payload.ts
9741
+ function extractToolName(payload, defaultValue = "") {
9742
+ return payload.tool_name ?? payload.name ?? payload.toolCall?.name ?? defaultValue;
9743
+ }
9744
+ function extractToolInput(payload) {
9745
+ return payload.tool_input ?? payload.args ?? payload.toolCall?.args ?? {};
9746
+ }
9747
+ function canonicalToolName(name) {
9748
+ switch (name) {
9749
+ // Hermes Agent
9750
+ case "terminal":
9751
+ return "Bash";
9752
+ case "write_file":
9753
+ return "Write";
9754
+ case "patch":
9755
+ return "Edit";
9756
+ case "read_file":
9757
+ return "Read";
9758
+ case "search_files":
9759
+ return "Grep";
9760
+ // Antigravity (agy) — shell tool renamed from Gemini's run_shell_command
9761
+ case "run_command":
9762
+ return "Bash";
9763
+ default:
9764
+ return name;
9765
+ }
9766
+ }
9767
+ function agentLabelFromFlag(flag) {
9768
+ if (typeof flag !== "string") return void 0;
9769
+ switch (flag.toLowerCase()) {
9770
+ case "antigravity":
9771
+ case "agy":
9772
+ return "Antigravity";
9773
+ case "copilot":
9774
+ return "GitHub Copilot";
9775
+ default:
9776
+ return void 0;
9790
9777
  }
9791
- });
9792
-
9793
- // src/protection.ts
9794
- var PROTECTIVE_SHIELD_DISCOUNTS;
9795
- var init_protection = __esm({
9796
- "src/protection.ts"() {
9778
+ }
9779
+ function canonicalToolInput(rawToolName, input) {
9780
+ if (rawToolName !== "run_command") return input;
9781
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
9782
+ const args = input;
9783
+ if (typeof args.CommandLine !== "string") return input;
9784
+ const { CommandLine, Cwd, ...rest } = args;
9785
+ const canonical = { ...rest, command: CommandLine };
9786
+ if (typeof Cwd === "string" && Cwd.length > 0) canonical.cwd = Cwd;
9787
+ return canonical;
9788
+ }
9789
+ var init_hook_payload = __esm({
9790
+ "src/utils/hook-payload.ts"() {
9797
9791
  "use strict";
9798
- PROTECTIVE_SHIELD_DISCOUNTS = {
9799
- "project-jail": 0.7
9800
- };
9801
9792
  }
9802
9793
  });
9803
9794
 
9804
- // src/cli/render/scan-json.ts
9805
- function buildScanJson(input) {
9806
- const { summary, blast, isWired, generatedAt } = input;
9807
- const { band } = classifyScore(blast.score);
9808
- return {
9809
- schemaVersion: 1,
9810
- generatedAt,
9811
- isWired,
9812
- score: blast.score,
9813
- band,
9814
- totals: {
9815
- blocked: summary.byVerdict.blocked,
9816
- review: summary.byVerdict.supervised,
9817
- leaks: summary.byVerdict.leaks,
9818
- loops: summary.byVerdict.loops,
9819
- blastExposures: blast.reachable.length + blast.envFindings.length
9820
- },
9821
- summary,
9822
- blast: {
9823
- score: blast.score,
9824
- reachable: blast.reachable,
9825
- envFindings: blast.envFindings
9826
- }
9827
- };
9795
+ // src/scan-summary.ts
9796
+ function agentDisplayName(agent) {
9797
+ return AGENT_LONG[agent] ?? "Claude Code";
9828
9798
  }
9829
- var init_scan_json = __esm({
9830
- "src/cli/render/scan-json.ts"() {
9831
- "use strict";
9832
- init_scan_derive();
9799
+ function agentBadgeText(agent, width = 10) {
9800
+ return `[${AGENT_SHORT[agent] ?? "Claude"}]`.padEnd(width);
9801
+ }
9802
+ function agentColorName(agent) {
9803
+ switch (agent) {
9804
+ case "gemini":
9805
+ return "blue";
9806
+ case "codex":
9807
+ return "magenta";
9808
+ case "antigravity":
9809
+ return "yellow";
9810
+ case "copilot":
9811
+ return "green";
9812
+ case "shell":
9813
+ return "yellow";
9814
+ default:
9815
+ return "cyan";
9833
9816
  }
9834
- });
9835
-
9836
- // src/cli/render/scan-history.ts
9837
- import fs15 from "fs";
9838
- import path17 from "path";
9839
- import os14 from "os";
9840
- function defaultHistoryPath() {
9841
- return path17.join(os14.homedir(), ".node9", "scan-history.json");
9842
9817
  }
9843
- function readPreviousScan(opts = {}) {
9844
- const filePath = opts.path ?? defaultHistoryPath();
9845
- try {
9846
- if (!fs15.existsSync(filePath)) return null;
9847
- const raw = fs15.readFileSync(filePath, "utf8");
9848
- const parsed = JSON.parse(raw);
9849
- if (!Array.isArray(parsed) || parsed.length === 0) return null;
9850
- const last = parsed[parsed.length - 1];
9851
- if (!isValidRecord(last)) return null;
9852
- return last;
9853
- } catch {
9854
- return null;
9818
+ function buildScanSummary(agents) {
9819
+ const stats = {
9820
+ sessions: 0,
9821
+ totalToolCalls: 0,
9822
+ bashCalls: 0,
9823
+ totalCostUSD: 0,
9824
+ firstDate: null,
9825
+ lastDate: null
9826
+ };
9827
+ for (const a of agents) {
9828
+ stats.sessions += a.scan.sessions;
9829
+ stats.totalToolCalls += a.scan.totalToolCalls;
9830
+ stats.bashCalls += a.scan.bashCalls;
9831
+ stats.totalCostUSD += a.scan.totalCostUSD;
9832
+ if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
9833
+ stats.firstDate = a.scan.firstDate;
9834
+ }
9835
+ if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
9836
+ stats.lastDate = a.scan.lastDate;
9837
+ }
9855
9838
  }
9839
+ const allFindings = agents.flatMap((a) => a.scan.findings);
9840
+ const allLeaks = agents.flatMap(
9841
+ (a) => a.scan.dlpFindings.map((f) => ({
9842
+ patternName: f.patternName,
9843
+ redactedSample: f.redactedSample,
9844
+ toolName: f.toolName,
9845
+ timestamp: f.timestamp,
9846
+ project: f.project,
9847
+ sessionId: f.sessionId,
9848
+ agent: f.agent
9849
+ }))
9850
+ );
9851
+ const allLoops = agents.flatMap(
9852
+ (a) => a.scan.loopFindings.map((f) => ({
9853
+ toolName: f.toolName,
9854
+ commandPreview: f.commandPreview,
9855
+ count: f.count,
9856
+ timestamp: f.timestamp,
9857
+ project: f.project,
9858
+ sessionId: f.sessionId,
9859
+ agent: f.agent,
9860
+ kind: f.kind
9861
+ }))
9862
+ );
9863
+ const byVerdict = {
9864
+ blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
9865
+ supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
9866
+ leaks: allLeaks.length,
9867
+ loops: allLoops.length
9868
+ };
9869
+ const byAgent = agents.map((a) => ({
9870
+ id: a.id,
9871
+ label: a.label,
9872
+ icon: a.icon,
9873
+ sessions: a.scan.sessions,
9874
+ findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
9875
+ costUSD: a.scan.totalCostUSD
9876
+ })).filter((s) => s.sessions > 0 || s.findings > 0);
9877
+ const sections = buildSections(allFindings);
9878
+ const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
9879
+ const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
9880
+ return {
9881
+ stats,
9882
+ byVerdict,
9883
+ byAgent,
9884
+ sections,
9885
+ leaks: allLeaks,
9886
+ loops: allLoops,
9887
+ loopWastedUSD
9888
+ };
9856
9889
  }
9857
- function appendScanHistory(record, opts = {}) {
9858
- const filePath = opts.path ?? defaultHistoryPath();
9859
- const cap = opts.cap ?? SCAN_HISTORY_CAP;
9860
- try {
9861
- fs15.mkdirSync(path17.dirname(filePath), { recursive: true });
9862
- let history = [];
9863
- if (fs15.existsSync(filePath)) {
9864
- try {
9865
- const parsed = JSON.parse(fs15.readFileSync(filePath, "utf8"));
9866
- if (Array.isArray(parsed)) {
9867
- history = parsed.filter(isValidRecord);
9868
- }
9869
- } catch {
9870
- }
9890
+ function buildSections(findings) {
9891
+ const sectionMap = /* @__PURE__ */ new Map();
9892
+ function ensureSection(id, label, subtitle, sourceType, shieldKey) {
9893
+ let s = sectionMap.get(id);
9894
+ if (!s) {
9895
+ s = {
9896
+ id,
9897
+ label,
9898
+ subtitle,
9899
+ sourceType,
9900
+ shieldKey,
9901
+ blockedCount: 0,
9902
+ reviewCount: 0,
9903
+ rules: []
9904
+ };
9905
+ sectionMap.set(id, s);
9871
9906
  }
9872
- history.push(record);
9873
- if (history.length > cap) {
9874
- history = history.slice(history.length - cap);
9907
+ return s;
9908
+ }
9909
+ const ruleMap = /* @__PURE__ */ new Map();
9910
+ for (const f of findings) {
9911
+ const src = f.source;
9912
+ const sourceType = src.sourceType;
9913
+ const shieldName = src.shieldName;
9914
+ const verdict = src.rule.verdict === "block" ? "block" : "review";
9915
+ let sectionId;
9916
+ let sectionLabel;
9917
+ let sectionSubtitle;
9918
+ let shieldKey;
9919
+ if (sourceType === "default") {
9920
+ sectionId = "default";
9921
+ sectionLabel = "Default Rules";
9922
+ sectionSubtitle = "built-in, always on";
9923
+ } else if (sourceType === "shield") {
9924
+ sectionId = `shield:${shieldName}`;
9925
+ sectionLabel = shieldName;
9926
+ sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
9927
+ shieldKey = shieldName;
9928
+ } else if (shieldName === "cloud") {
9929
+ sectionId = "cloud";
9930
+ sectionLabel = "Cloud Policy";
9931
+ sectionSubtitle = "synced from node9 cloud";
9932
+ } else {
9933
+ sectionId = "user";
9934
+ sectionLabel = "Your Rules";
9935
+ sectionSubtitle = "added in node9.config.json";
9875
9936
  }
9876
- fs15.writeFileSync(filePath, JSON.stringify(history, null, 2));
9877
- } catch (err2) {
9878
- process.stderr.write(
9879
- `[node9] Warning: could not write scan-history.json: ${err2.message}
9880
- `
9881
- );
9937
+ const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
9938
+ const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
9939
+ const ruleKey = sectionId + "::" + ruleDisplayName;
9940
+ let rule = ruleMap.get(ruleKey);
9941
+ if (!rule) {
9942
+ rule = {
9943
+ name: ruleDisplayName,
9944
+ verdict,
9945
+ reason: src.rule.reason ?? "",
9946
+ findings: []
9947
+ };
9948
+ ruleMap.set(ruleKey, rule);
9949
+ section.rules.push(rule);
9950
+ }
9951
+ const cmdPreview = previewCommand(f.input, 120);
9952
+ const fullCmd = fullCommandOf(f.input);
9953
+ const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
9954
+ if (!isDupe) {
9955
+ rule.findings.push({
9956
+ timestamp: f.timestamp ?? "",
9957
+ command: cmdPreview,
9958
+ fullCommand: fullCmd,
9959
+ project: f.project,
9960
+ sessionId: f.sessionId,
9961
+ agent: f.agent,
9962
+ toolName: f.toolName
9963
+ });
9964
+ }
9965
+ if (verdict === "block") section.blockedCount++;
9966
+ else section.reviewCount++;
9882
9967
  }
9968
+ const sections = [...sectionMap.values()];
9969
+ sections.sort((a, b) => {
9970
+ const aTotal = a.blockedCount + a.reviewCount;
9971
+ const bTotal = b.blockedCount + b.reviewCount;
9972
+ if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
9973
+ return bTotal - aTotal;
9974
+ });
9975
+ for (const s of sections) {
9976
+ s.rules.sort((a, b) => {
9977
+ const aBlock = a.verdict === "block" ? 1 : 0;
9978
+ const bBlock = b.verdict === "block" ? 1 : 0;
9979
+ if (bBlock !== aBlock) return bBlock - aBlock;
9980
+ return b.findings.length - a.findings.length;
9981
+ });
9982
+ }
9983
+ return sections;
9883
9984
  }
9884
- function computeScanDelta(current, previous, now = Date.now()) {
9885
- if (!previous) return null;
9886
- const prevMs = Date.parse(previous.timestamp);
9887
- if (Number.isNaN(prevMs)) return null;
9888
- const scoreDelta = current.score - previous.score;
9889
- const daysAgo = Math.max(0, Math.floor((now - prevMs) / 864e5));
9890
- if (scoreDelta === 0 && daysAgo === 0) return null;
9891
- return { scoreDelta, daysAgo };
9985
+ function previewCommand(input, max) {
9986
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9987
+ const s = String(raw).replace(/\s+/g, " ").trim();
9988
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
9892
9989
  }
9893
- function isValidRecord(x) {
9894
- if (typeof x !== "object" || x === null) return false;
9895
- const r = x;
9896
- return typeof r.timestamp === "string" && typeof r.score === "number" && typeof r.blocked === "number" && typeof r.review === "number" && typeof r.leaks === "number" && typeof r.loops === "number" && typeof r.totalCalls === "number";
9990
+ function fullCommandOf(input) {
9991
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9992
+ return String(raw).replace(/\s+/g, " ").trim();
9897
9993
  }
9898
- var SCAN_HISTORY_CAP;
9899
- var init_scan_history = __esm({
9900
- "src/cli/render/scan-history.ts"() {
9994
+ var AGENT_SHORT, AGENT_LONG;
9995
+ var init_scan_summary = __esm({
9996
+ "src/scan-summary.ts"() {
9901
9997
  "use strict";
9902
- SCAN_HISTORY_CAP = 30;
9998
+ init_shields();
9999
+ init_dist();
10000
+ init_dist();
10001
+ AGENT_SHORT = {
10002
+ claude: "Claude",
10003
+ gemini: "Gemini",
10004
+ codex: "Codex",
10005
+ antigravity: "Agy",
10006
+ copilot: "Copilot",
10007
+ shell: "Shell"
10008
+ };
10009
+ AGENT_LONG = {
10010
+ claude: "Claude Code",
10011
+ gemini: "Gemini CLI",
10012
+ codex: "Codex",
10013
+ antigravity: "Antigravity",
10014
+ copilot: "GitHub Copilot",
10015
+ shell: "Shell"
10016
+ };
9903
10017
  }
9904
10018
  });
9905
10019
 
9906
- // src/pricing/litellm.ts
9907
- import fs16 from "fs";
9908
- import path18 from "path";
9909
- import os15 from "os";
9910
- function normalizeModel(raw) {
9911
- return raw.replace(/-\d{8}$/, "").toLowerCase();
9912
- }
9913
- function readCache() {
9914
- try {
9915
- const raw = JSON.parse(fs16.readFileSync(CACHE_FILE(), "utf-8"));
9916
- if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
9917
- return null;
9918
- }
9919
- const ageMs = Date.now() - new Date(raw.fetchedAt).getTime();
9920
- if (ageMs < 0 || ageMs > TTL_MS) return null;
9921
- return raw.prices;
9922
- } catch {
9923
- return null;
9924
- }
9925
- }
9926
- function writeCache(prices) {
9927
- try {
9928
- const target = CACHE_FILE();
9929
- const dir = path18.dirname(target);
9930
- if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
9931
- const tmp = target + ".tmp";
9932
- const body = {
9933
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
9934
- prices
9935
- };
9936
- fs16.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
9937
- fs16.renameSync(tmp, target);
9938
- } catch (err2) {
9939
- try {
9940
- fs16.appendFileSync(
9941
- HOOK_DEBUG_LOG,
9942
- `[pricing] cache write failed: ${err2.message}
9943
- `
9944
- );
9945
- } catch {
9946
- }
9947
- }
9948
- }
9949
- function tupleFromLiteLLM(entry) {
9950
- if (!entry || typeof entry !== "object") return null;
9951
- const e = entry;
9952
- const num3 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
9953
- const inCost = num3(e.input_cost_per_token);
9954
- const outCost = num3(e.output_cost_per_token);
9955
- if (inCost === 0 && outCost === 0) return null;
10020
+ // src/cli/commands/blast.ts
10021
+ import chalk2 from "chalk";
10022
+ import fs17 from "fs";
10023
+ import path19 from "path";
10024
+ import os16 from "os";
10025
+ function buildSensitivePaths(home, cwd) {
9956
10026
  return [
9957
- inCost,
9958
- outCost,
9959
- num3(e.cache_creation_input_token_cost),
9960
- num3(e.cache_read_input_token_cost)
10027
+ {
10028
+ full: path19.join(home, ".ssh", "id_rsa"),
10029
+ label: "~/.ssh/id_rsa",
10030
+ description: "RSA private key \u2014 grants SSH access to your servers",
10031
+ score: 20
10032
+ },
10033
+ {
10034
+ full: path19.join(home, ".ssh", "id_ed25519"),
10035
+ label: "~/.ssh/id_ed25519",
10036
+ description: "Ed25519 private key \u2014 grants SSH access to your servers",
10037
+ score: 20
10038
+ },
10039
+ {
10040
+ full: path19.join(home, ".ssh", "id_ecdsa"),
10041
+ label: "~/.ssh/id_ecdsa",
10042
+ description: "ECDSA private key \u2014 grants SSH access to your servers",
10043
+ score: 20
10044
+ },
10045
+ {
10046
+ full: path19.join(home, ".aws", "credentials"),
10047
+ label: "~/.aws/credentials",
10048
+ description: "AWS access keys \u2014 full cloud account access",
10049
+ score: 20
10050
+ },
10051
+ {
10052
+ full: path19.join(home, ".aws", "config"),
10053
+ label: "~/.aws/config",
10054
+ description: "AWS configuration \u2014 account and region settings",
10055
+ score: 5
10056
+ },
10057
+ {
10058
+ full: path19.join(home, ".config", "gcloud", "credentials.db"),
10059
+ label: "~/.config/gcloud/credentials.db",
10060
+ description: "Google Cloud credentials",
10061
+ score: 15
10062
+ },
10063
+ {
10064
+ full: path19.join(home, ".docker", "config.json"),
10065
+ label: "~/.docker/config.json",
10066
+ description: "Docker registry auth tokens",
10067
+ score: 10
10068
+ },
10069
+ {
10070
+ full: path19.join(home, ".netrc"),
10071
+ label: "~/.netrc",
10072
+ description: "FTP/HTTP credentials in plain text",
10073
+ score: 15
10074
+ },
10075
+ {
10076
+ full: path19.join(home, ".npmrc"),
10077
+ label: "~/.npmrc",
10078
+ description: "npm auth token \u2014 can publish packages as you",
10079
+ score: 10
10080
+ },
10081
+ {
10082
+ full: path19.join(home, ".node9", "credentials.json"),
10083
+ label: "~/.node9/credentials.json",
10084
+ description: "Node9 cloud API key",
10085
+ score: 10
10086
+ },
10087
+ {
10088
+ full: path19.join(cwd, ".env"),
10089
+ label: ".env (current folder)",
10090
+ description: "App secrets \u2014 database passwords, API keys",
10091
+ score: 20
10092
+ },
10093
+ {
10094
+ full: path19.join(cwd, ".env.local"),
10095
+ label: ".env.local (current folder)",
10096
+ description: "Local overrides \u2014 often contains real credentials",
10097
+ score: 15
10098
+ },
10099
+ {
10100
+ full: path19.join(cwd, ".env.production"),
10101
+ label: ".env.production (current folder)",
10102
+ description: "Production secrets",
10103
+ score: 20
10104
+ }
9961
10105
  ];
9962
10106
  }
9963
- async function fetchLiteLLMPricing() {
10107
+ function isReadable(filePath) {
9964
10108
  try {
9965
- const res = await fetch(LITELLM_URL, {
9966
- signal: AbortSignal.timeout(15e3)
9967
- });
9968
- if (!res.ok) return null;
9969
- const json = await res.json();
9970
- if (!json || typeof json !== "object") return null;
9971
- const out = {};
9972
- for (const [key, value] of Object.entries(json)) {
9973
- const tuple = tupleFromLiteLLM(value);
9974
- if (tuple) out[key.toLowerCase()] = tuple;
9975
- }
9976
- if (Object.keys(out).length < 10) {
9977
- return null;
9978
- }
9979
- return out;
10109
+ fs17.accessSync(filePath, fs17.constants.R_OK);
10110
+ return true;
9980
10111
  } catch {
9981
- return null;
10112
+ return false;
9982
10113
  }
9983
10114
  }
9984
- async function ensurePricingLoaded() {
9985
- if (memCache !== null && Date.now() - memCacheAt < TTL_MS) return;
9986
- const fromDisk = readCache();
9987
- if (fromDisk && Object.keys(fromDisk).length > 0) {
9988
- memCache = fromDisk;
9989
- memCacheAt = Date.now();
9990
- lookupCache.clear();
9991
- return;
10115
+ function scoreLabel(score) {
10116
+ if (score >= 80) return chalk2.green(`${score}/100 Good`);
10117
+ if (score >= 50) return chalk2.yellow(`${score}/100 Moderate risk`);
10118
+ if (score >= 25) return chalk2.red(`${score}/100 High risk`);
10119
+ return chalk2.red.bold(`${score}/100 Critical`);
10120
+ }
10121
+ function runBlast() {
10122
+ const home = os16.homedir();
10123
+ const cwd = process.cwd();
10124
+ const paths = buildSensitivePaths(home, cwd);
10125
+ let scoreDeduction = 0;
10126
+ const reachable = [];
10127
+ for (const p of paths) {
10128
+ if (fs17.existsSync(p.full) && isReadable(p.full)) {
10129
+ reachable.push(p);
10130
+ scoreDeduction += p.score;
10131
+ }
9992
10132
  }
9993
- const fetched = await fetchLiteLLMPricing();
9994
- if (fetched && Object.keys(fetched).length > 0) {
9995
- memCache = fetched;
9996
- memCacheAt = Date.now();
9997
- writeCache(fetched);
9998
- lookupCache.clear();
9999
- return;
10133
+ const envFindings = [];
10134
+ for (const [key, value] of Object.entries(process.env)) {
10135
+ if (!value) continue;
10136
+ const match = scanArgs({ [key]: value });
10137
+ if (match) {
10138
+ envFindings.push({ key, patternName: match.patternName });
10139
+ scoreDeduction += 10;
10140
+ }
10000
10141
  }
10001
- memCache = { ...BUNDLED_PRICING };
10002
- memCacheAt = Date.now();
10003
- lookupCache.clear();
10142
+ return { reachable, envFindings, score: Math.max(0, 100 - scoreDeduction) };
10004
10143
  }
10005
- function pricingFor(model) {
10006
- const norm = normalizeModel(model);
10007
- const cached = lookupCache.get(norm);
10008
- if (cached !== void 0) return cached;
10009
- const sources = [];
10010
- if (memCache) sources.push(memCache);
10011
- sources.push(BUNDLED_PRICING);
10012
- let resolved = null;
10013
- for (const source of sources) {
10014
- const exact = source[norm];
10015
- if (exact) {
10016
- resolved = exact;
10017
- break;
10144
+ function registerBlastCommand(program2) {
10145
+ program2.command("blast").description("Map what an AI agent can currently reach on this machine").action(() => {
10146
+ const home = os16.homedir();
10147
+ const cwd = process.cwd();
10148
+ const { reachable, envFindings, score } = runBlast();
10149
+ console.log("");
10150
+ console.log(
10151
+ chalk2.bold(" \u{1F52D} Node9 Blast Radius") + chalk2.dim(" \xB7 what an AI agent can reach from here")
10152
+ );
10153
+ console.log(chalk2.dim(" Running in: ") + chalk2.white(cwd.replace(home, "~")));
10154
+ console.log("");
10155
+ if (reachable.length > 0) {
10156
+ console.log(" " + chalk2.red.bold("Sensitive files reachable:"));
10157
+ for (const p of reachable) {
10158
+ console.log(
10159
+ " " + chalk2.red("\u2717 ") + chalk2.yellow(p.label.padEnd(38)) + chalk2.dim(p.description)
10160
+ );
10161
+ }
10162
+ console.log("");
10018
10163
  }
10019
- let best = null;
10020
- for (const key of Object.keys(source)) {
10021
- if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
10022
- best = key;
10164
+ if (envFindings.length > 0) {
10165
+ console.log(" " + chalk2.red.bold("Secrets in active environment:"));
10166
+ for (const f of envFindings) {
10167
+ console.log(
10168
+ " " + chalk2.red("\u2717 ") + chalk2.yellow(f.key.padEnd(38)) + chalk2.dim(f.patternName)
10169
+ );
10023
10170
  }
10171
+ console.log("");
10024
10172
  }
10025
- if (best) {
10026
- resolved = source[best];
10027
- break;
10173
+ console.log(" " + chalk2.dim("\u2500".repeat(70)));
10174
+ if (reachable.length === 0 && envFindings.length === 0) {
10175
+ console.log(" " + chalk2.green("\u2705 No sensitive files or environment secrets found."));
10176
+ console.log(" Security Score: " + scoreLabel(score));
10177
+ } else {
10178
+ console.log(
10179
+ " Security Score: " + scoreLabel(score) + chalk2.dim(
10180
+ ` (${reachable.length} file${reachable.length !== 1 ? "s" : ""}, ${envFindings.length} env var${envFindings.length !== 1 ? "s" : ""})`
10181
+ )
10182
+ );
10183
+ console.log("");
10184
+ console.log(
10185
+ chalk2.dim(
10186
+ " Every AI agent you start can read the files and env vars listed above.\n Run `node9 shield enable project-jail` to restrict agent file access.\n Run `node9 mask` to redact secrets from existing session history."
10187
+ )
10188
+ );
10028
10189
  }
10029
- }
10030
- lookupCache.set(norm, resolved);
10031
- return resolved;
10190
+ console.log("");
10191
+ });
10032
10192
  }
10033
- var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, lookupCache;
10034
- var init_litellm = __esm({
10035
- "src/pricing/litellm.ts"() {
10193
+ var init_blast = __esm({
10194
+ "src/cli/commands/blast.ts"() {
10036
10195
  "use strict";
10037
- init_audit();
10038
- LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
10039
- BUNDLED_PRICING = {
10040
- // Anthropic
10041
- "claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
10042
- "claude-opus-4-1": [5e-6, 25e-6, 625e-8, 5e-7],
10043
- "claude-opus-4-5": [5e-6, 25e-6, 625e-8, 5e-7],
10044
- "claude-opus-4-6": [5e-6, 25e-6, 625e-8, 5e-7],
10045
- "claude-opus-4-7": [5e-6, 25e-6, 625e-8, 5e-7],
10046
- "claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
10047
- "claude-sonnet-4-5": [3e-6, 15e-6, 375e-8, 3e-7],
10048
- "claude-sonnet-4-6": [3e-6, 15e-6, 375e-8, 3e-7],
10049
- "claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
10050
- "claude-haiku-4-5": [8e-7, 4e-6, 1e-6, 8e-8],
10051
- "claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
10052
- "claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
10053
- "claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
10054
- "claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
10055
- // OpenAI
10056
- "gpt-4o": [5e-6, 15e-6, 0, 25e-7],
10057
- "gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
10058
- "gpt-5": [1e-5, 3e-5, 0, 5e-6],
10059
- // Google
10060
- "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
10061
- "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
10062
- };
10063
- CACHE_FILE = () => path18.join(os15.homedir(), ".node9", "model-pricing.json");
10064
- TTL_MS = 24 * 60 * 60 * 1e3;
10065
- memCache = null;
10066
- memCacheAt = 0;
10067
- lookupCache = /* @__PURE__ */ new Map();
10196
+ init_dlp();
10068
10197
  }
10069
10198
  });
10070
10199
 
10071
- // src/cost-codex.ts
10072
- import fs17 from "fs";
10073
- import os16 from "os";
10074
- import path19 from "path";
10075
- function codexSessionsDir() {
10076
- return path19.join(os16.homedir(), ".codex", "sessions");
10200
+ // src/cli/render/scan-derive.ts
10201
+ import chalk3 from "chalk";
10202
+ import stringWidth from "string-width";
10203
+ function classifyScore(score) {
10204
+ if (score >= 80) return { band: "good", label: "Good", color: chalk3.green };
10205
+ if (score >= 50) return { band: "at-risk", label: "At Risk", color: chalk3.yellow };
10206
+ return { band: "critical", label: "Critical", color: chalk3.red };
10077
10207
  }
10078
- function codexPriceFor(model) {
10079
- return pricingFor(model) ?? CODEX_FALLBACK;
10208
+ function topDlpPatterns(findings, n) {
10209
+ const counts = /* @__PURE__ */ new Map();
10210
+ for (const f of findings) {
10211
+ counts.set(f.patternName, (counts.get(f.patternName) ?? 0) + 1);
10212
+ }
10213
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([name, count]) => ({ name, count }));
10080
10214
  }
10081
- function listCodexSessionFiles(base) {
10082
- const out = [];
10083
- for (const y of safeReaddir(base)) {
10084
- const yp = path19.join(base, y);
10085
- if (!isDir(yp)) continue;
10086
- for (const m of safeReaddir(yp)) {
10087
- const mp = path19.join(yp, m);
10088
- if (!isDir(mp)) continue;
10089
- for (const d of safeReaddir(mp)) {
10090
- const dp = path19.join(mp, d);
10091
- if (!isDir(dp)) continue;
10092
- for (const f of safeReaddir(dp)) {
10093
- if (f.endsWith(".jsonl")) out.push(path19.join(dp, f));
10094
- }
10095
- }
10215
+ function topRulesByVerdict(sections, verdict, n) {
10216
+ const matched = [];
10217
+ for (const section of sections) {
10218
+ for (const rule of section.rules) {
10219
+ const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
10220
+ if (matches) matched.push({ name: rule.name, count: rule.findings.length });
10096
10221
  }
10097
10222
  }
10098
- return out;
10223
+ return matched.sort((a, b) => b.count - a.count).slice(0, n);
10099
10224
  }
10100
- function safeReaddir(dir) {
10101
- try {
10102
- return fs17.readdirSync(dir);
10103
- } catch {
10104
- return [];
10225
+ function computeLoopWaste(loops, totalToolCalls) {
10226
+ const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
10227
+ const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
10228
+ return { wastedCalls, wastePct };
10229
+ }
10230
+ function rollupByShield(sections, topRulesPerShield = 3) {
10231
+ const out = [];
10232
+ for (const section of sections) {
10233
+ if (section.sourceType !== "shield") continue;
10234
+ if (!section.shieldKey) continue;
10235
+ const totalCatches = section.blockedCount + section.reviewCount;
10236
+ 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);
10237
+ out.push({
10238
+ shieldName: section.shieldKey,
10239
+ totalCatches,
10240
+ blockCatches: section.blockedCount,
10241
+ reviewCatches: section.reviewCount,
10242
+ topRuleLabels
10243
+ });
10105
10244
  }
10245
+ return out.sort((a, b) => b.totalCatches - a.totalCatches);
10106
10246
  }
10107
- function isDir(p) {
10108
- try {
10109
- return fs17.statSync(p).isDirectory();
10110
- } catch {
10111
- return false;
10247
+ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
10248
+ const inner = width - 4;
10249
+ const out = [];
10250
+ const titlePad = ` ${title} `;
10251
+ const titleWidth = stringWidth(titlePad);
10252
+ const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
10253
+ const dashFill = "\u2500".repeat(Math.max(0, inner - stringWidth(titleSegment)));
10254
+ out.push(chalk3.dim("\u256D\u2500") + chalk3.bold(titleSegment) + chalk3.dim(`${dashFill}\u2500\u256E`));
10255
+ for (const line of bodyLines) {
10256
+ const padding = " ".repeat(Math.max(0, inner - line.width));
10257
+ out.push(chalk3.dim("\u2502 ") + line.rendered + padding + chalk3.dim(" \u2502"));
10112
10258
  }
10259
+ out.push(chalk3.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
10260
+ return out;
10113
10261
  }
10114
- function parseCodexSession(lines) {
10115
- let sessionStart2 = "";
10116
- let runId = "";
10117
- let cwd = "";
10118
- let model = "";
10119
- let input = 0;
10120
- let cached = 0;
10121
- let output = 0;
10122
- let sawUsage = false;
10123
- for (const raw of lines) {
10124
- if (!raw.trim()) continue;
10125
- let entry;
10126
- try {
10127
- entry = JSON.parse(raw);
10128
- } catch {
10129
- continue;
10130
- }
10131
- const p = entry.payload ?? {};
10132
- if (entry.type === "session_meta") {
10133
- if (!sessionStart2 && typeof p["timestamp"] === "string") sessionStart2 = p["timestamp"];
10134
- if (!runId && typeof p["id"] === "string") runId = p["id"];
10135
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
10136
- continue;
10137
- }
10138
- if (entry.type === "turn_context") {
10139
- if (typeof p["model"] === "string") model = p["model"];
10140
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
10141
- continue;
10142
- }
10143
- if (entry.type === "event_msg" && p["type"] === "token_count") {
10144
- const info = p["info"] ?? {};
10145
- const usage = info["total_token_usage"] ?? {};
10146
- if (typeof usage["input_tokens"] === "number") input = usage["input_tokens"];
10147
- if (typeof usage["cached_input_tokens"] === "number") cached = usage["cached_input_tokens"];
10148
- if (typeof usage["output_tokens"] === "number") output = usage["output_tokens"];
10149
- sawUsage = true;
10150
- }
10262
+ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
10263
+ const t = new Date(timestamp).getTime();
10264
+ if (Number.isNaN(t)) return "?";
10265
+ const days = Math.floor((now.getTime() - t) / 864e5);
10266
+ if (days < 1) return "today";
10267
+ if (days > 90) return "90d+";
10268
+ return `${days}d`;
10269
+ }
10270
+ var PANEL_WIDTH;
10271
+ var init_scan_derive = __esm({
10272
+ "src/cli/render/scan-derive.ts"() {
10273
+ "use strict";
10274
+ PANEL_WIDTH = 76;
10151
10275
  }
10152
- if (!sessionStart2 || !sawUsage) return null;
10153
- const nonCached = Math.max(0, input - cached);
10154
- if (nonCached === 0 && output === 0 && cached === 0) return null;
10155
- const norm = normalizeModel(model || "gpt-5");
10156
- const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
10157
- const costUSD = nonCached * pin + output * pout + cached * pcr;
10276
+ });
10277
+
10278
+ // src/protection.ts
10279
+ var PROTECTIVE_SHIELD_DISCOUNTS;
10280
+ var init_protection = __esm({
10281
+ "src/protection.ts"() {
10282
+ "use strict";
10283
+ PROTECTIVE_SHIELD_DISCOUNTS = {
10284
+ "project-jail": 0.7
10285
+ };
10286
+ }
10287
+ });
10288
+
10289
+ // src/cli/render/scan-json.ts
10290
+ function buildScanJson(input) {
10291
+ const { summary, blast, isWired, generatedAt } = input;
10292
+ const { band } = classifyScore(blast.score);
10158
10293
  return {
10159
- date: sessionStart2.slice(0, 10),
10160
- model: norm,
10161
- workingDir: cwd,
10162
- runId,
10163
- costUSD,
10164
- inputTokens: nonCached,
10165
- outputTokens: output,
10166
- cacheReadTokens: cached,
10167
- cacheWriteTokens: 0
10294
+ schemaVersion: 1,
10295
+ generatedAt,
10296
+ isWired,
10297
+ score: blast.score,
10298
+ band,
10299
+ totals: {
10300
+ blocked: summary.byVerdict.blocked,
10301
+ review: summary.byVerdict.supervised,
10302
+ leaks: summary.byVerdict.leaks,
10303
+ loops: summary.byVerdict.loops,
10304
+ blastExposures: blast.reachable.length + blast.envFindings.length
10305
+ },
10306
+ summary,
10307
+ blast: {
10308
+ score: blast.score,
10309
+ reachable: blast.reachable,
10310
+ envFindings: blast.envFindings
10311
+ }
10168
10312
  };
10169
10313
  }
10170
- var CODEX_FALLBACK, codexSource;
10171
- var init_cost_codex = __esm({
10172
- "src/cost-codex.ts"() {
10314
+ var init_scan_json = __esm({
10315
+ "src/cli/render/scan-json.ts"() {
10173
10316
  "use strict";
10174
- init_litellm();
10175
- CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
10176
- codexSource = {
10177
- id: "codex",
10178
- available() {
10179
- try {
10180
- return fs17.existsSync(codexSessionsDir());
10181
- } catch {
10182
- return false;
10183
- }
10184
- },
10185
- collect(sinceMs) {
10186
- const base = codexSessionsDir();
10187
- const combined = /* @__PURE__ */ new Map();
10188
- for (const file of listCodexSessionFiles(base)) {
10189
- try {
10190
- if (sinceMs !== void 0 && fs17.statSync(file).mtimeMs < sinceMs) continue;
10191
- } catch {
10192
- continue;
10193
- }
10194
- let content;
10195
- try {
10196
- content = fs17.readFileSync(file, "utf8");
10197
- } catch {
10198
- continue;
10199
- }
10200
- const e = parseCodexSession(content.split("\n"));
10201
- if (!e) continue;
10202
- const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
10203
- const prev = combined.get(key);
10204
- if (prev) {
10205
- prev.costUSD += e.costUSD;
10206
- prev.inputTokens += e.inputTokens;
10207
- prev.outputTokens += e.outputTokens;
10208
- prev.cacheReadTokens += e.cacheReadTokens;
10209
- prev.cacheWriteTokens += e.cacheWriteTokens;
10210
- } else {
10211
- combined.set(key, { ...e });
10212
- }
10213
- }
10214
- return [...combined.values()];
10215
- }
10216
- };
10317
+ init_scan_derive();
10217
10318
  }
10218
10319
  });
10219
10320
 
10220
- // src/cost-gemini.ts
10321
+ // src/cli/render/scan-history.ts
10221
10322
  import fs18 from "fs";
10222
- import os17 from "os";
10223
10323
  import path20 from "path";
10224
- function geminiTmpDir() {
10225
- return path20.join(os17.homedir(), ".gemini", "tmp");
10226
- }
10227
- function geminiPriceFor(model) {
10228
- let tuple = pricingFor(model);
10229
- if (!tuple && /^gemini-/i.test(model)) {
10230
- for (const proxy of GEMINI_FALLBACK_MODELS) {
10231
- tuple = pricingFor(proxy);
10232
- if (tuple) break;
10233
- }
10234
- }
10235
- if (!tuple) return null;
10236
- return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
10324
+ import os17 from "os";
10325
+ function defaultHistoryPath() {
10326
+ return path20.join(os17.homedir(), ".node9", "scan-history.json");
10237
10327
  }
10238
- function safeReaddir2(dir) {
10328
+ function readPreviousScan(opts = {}) {
10329
+ const filePath = opts.path ?? defaultHistoryPath();
10239
10330
  try {
10240
- return fs18.readdirSync(dir);
10331
+ if (!fs18.existsSync(filePath)) return null;
10332
+ const raw = fs18.readFileSync(filePath, "utf8");
10333
+ const parsed = JSON.parse(raw);
10334
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
10335
+ const last = parsed[parsed.length - 1];
10336
+ if (!isValidRecord(last)) return null;
10337
+ return last;
10241
10338
  } catch {
10242
- return [];
10339
+ return null;
10243
10340
  }
10244
10341
  }
10245
- function isDir2(p) {
10342
+ function appendScanHistory(record, opts = {}) {
10343
+ const filePath = opts.path ?? defaultHistoryPath();
10344
+ const cap = opts.cap ?? SCAN_HISTORY_CAP;
10246
10345
  try {
10247
- return fs18.statSync(p).isDirectory();
10248
- } catch {
10249
- return false;
10250
- }
10251
- }
10252
- function listGeminiSessionFiles(base) {
10253
- const out = [];
10254
- for (const project of safeReaddir2(base)) {
10255
- const chats = path20.join(base, project, "chats");
10256
- if (!isDir2(chats)) continue;
10257
- for (const f of safeReaddir2(chats)) {
10258
- if (f.startsWith("session-") && f.endsWith(".jsonl")) {
10259
- out.push({ file: path20.join(chats, f), project });
10346
+ fs18.mkdirSync(path20.dirname(filePath), { recursive: true });
10347
+ let history = [];
10348
+ if (fs18.existsSync(filePath)) {
10349
+ try {
10350
+ const parsed = JSON.parse(fs18.readFileSync(filePath, "utf8"));
10351
+ if (Array.isArray(parsed)) {
10352
+ history = parsed.filter(isValidRecord);
10353
+ }
10354
+ } catch {
10260
10355
  }
10261
10356
  }
10262
- }
10263
- return out;
10264
- }
10265
- function parseGeminiSession(lines, project) {
10266
- const seenIds = /* @__PURE__ */ new Set();
10267
- const byKey = /* @__PURE__ */ new Map();
10268
- let runId = "";
10269
- for (const raw of lines) {
10270
- if (!raw.trim()) continue;
10271
- let obj;
10272
- try {
10273
- obj = JSON.parse(raw);
10274
- } catch {
10275
- continue;
10276
- }
10277
- if (typeof obj.sessionId === "string" && !runId) runId = obj.sessionId;
10278
- if (!obj.tokens || !obj.model || !obj.timestamp) continue;
10279
- if (obj.id) {
10280
- if (seenIds.has(obj.id)) continue;
10281
- seenIds.add(obj.id);
10282
- }
10283
- const price = geminiPriceFor(obj.model);
10284
- if (!price) continue;
10285
- const inp = obj.tokens.input ?? 0;
10286
- const out = obj.tokens.output ?? 0;
10287
- const cached = Math.min(obj.tokens.cached ?? 0, inp);
10288
- const fresh = Math.max(0, inp - cached);
10289
- const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
10290
- const date = obj.timestamp.slice(0, 10);
10291
- const model = normalizeModel(obj.model);
10292
- const key = `${date}::${model}`;
10293
- const prev = byKey.get(key);
10294
- if (prev) {
10295
- prev.costUSD += cost;
10296
- prev.inputTokens += fresh;
10297
- prev.outputTokens += out;
10298
- prev.cacheReadTokens += cached;
10299
- } else {
10300
- byKey.set(key, {
10301
- date,
10302
- model,
10303
- workingDir: project,
10304
- runId,
10305
- costUSD: cost,
10306
- inputTokens: fresh,
10307
- outputTokens: out,
10308
- cacheReadTokens: cached,
10309
- cacheWriteTokens: 0
10310
- });
10357
+ history.push(record);
10358
+ if (history.length > cap) {
10359
+ history = history.slice(history.length - cap);
10311
10360
  }
10361
+ fs18.writeFileSync(filePath, JSON.stringify(history, null, 2));
10362
+ } catch (err2) {
10363
+ process.stderr.write(
10364
+ `[node9] Warning: could not write scan-history.json: ${err2.message}
10365
+ `
10366
+ );
10312
10367
  }
10313
- if (runId) for (const e of byKey.values()) e.runId = runId;
10314
- return [...byKey.values()];
10315
10368
  }
10316
- var GEMINI_FALLBACK_MODELS, geminiSource;
10317
- var init_cost_gemini = __esm({
10318
- "src/cost-gemini.ts"() {
10369
+ function computeScanDelta(current, previous, now = Date.now()) {
10370
+ if (!previous) return null;
10371
+ const prevMs = Date.parse(previous.timestamp);
10372
+ if (Number.isNaN(prevMs)) return null;
10373
+ const scoreDelta = current.score - previous.score;
10374
+ const daysAgo = Math.max(0, Math.floor((now - prevMs) / 864e5));
10375
+ if (scoreDelta === 0 && daysAgo === 0) return null;
10376
+ return { scoreDelta, daysAgo };
10377
+ }
10378
+ function isValidRecord(x) {
10379
+ if (typeof x !== "object" || x === null) return false;
10380
+ const r = x;
10381
+ return typeof r.timestamp === "string" && typeof r.score === "number" && typeof r.blocked === "number" && typeof r.review === "number" && typeof r.leaks === "number" && typeof r.loops === "number" && typeof r.totalCalls === "number";
10382
+ }
10383
+ var SCAN_HISTORY_CAP;
10384
+ var init_scan_history = __esm({
10385
+ "src/cli/render/scan-history.ts"() {
10319
10386
  "use strict";
10320
- init_litellm();
10321
- GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
10322
- geminiSource = {
10323
- id: "gemini",
10324
- available() {
10325
- try {
10326
- return fs18.existsSync(geminiTmpDir());
10327
- } catch {
10328
- return false;
10329
- }
10330
- },
10331
- collect(sinceMs) {
10332
- const combined = /* @__PURE__ */ new Map();
10333
- for (const { file, project } of listGeminiSessionFiles(geminiTmpDir())) {
10334
- try {
10335
- if (sinceMs !== void 0 && fs18.statSync(file).mtimeMs < sinceMs) continue;
10336
- } catch {
10337
- continue;
10338
- }
10339
- let content;
10340
- try {
10341
- content = fs18.readFileSync(file, "utf8");
10342
- } catch {
10343
- continue;
10344
- }
10345
- for (const e of parseGeminiSession(content.split("\n"), project)) {
10346
- const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
10347
- const prev = combined.get(key);
10348
- if (prev) {
10349
- prev.costUSD += e.costUSD;
10350
- prev.inputTokens += e.inputTokens;
10351
- prev.outputTokens += e.outputTokens;
10352
- prev.cacheReadTokens += e.cacheReadTokens;
10353
- prev.cacheWriteTokens += e.cacheWriteTokens;
10354
- } else {
10355
- combined.set(key, { ...e });
10356
- }
10357
- }
10358
- }
10359
- return [...combined.values()];
10360
- }
10361
- };
10387
+ SCAN_HISTORY_CAP = 30;
10362
10388
  }
10363
10389
  });
10364
10390
 
@@ -11402,19 +11428,15 @@ import path25 from "path";
11402
11428
  import os22 from "os";
11403
11429
  import stringWidth2 from "string-width";
11404
11430
  function claudeModelPrice(model) {
11405
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
11406
- for (const [key, p] of Object.entries(CLAUDE_PRICING)) {
11407
- if (base === key || base.startsWith(key)) return p;
11408
- }
11409
- return null;
11431
+ const t = pricingFor(model);
11432
+ if (!t) return null;
11433
+ const [i, o, cw, cr] = t;
11434
+ return { i, o, cw, cr };
11410
11435
  }
11411
11436
  function geminiModelPrice(model) {
11412
- const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
11413
- for (const [key, p] of Object.entries(GEMINI_PRICING)) {
11414
- if (base === key || base.startsWith(key)) return p;
11415
- }
11416
- if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
11417
- return null;
11437
+ const p = geminiPriceFor(model);
11438
+ if (!p) return null;
11439
+ return { i: p.input, o: p.output, cr: p.cacheRead };
11418
11440
  }
11419
11441
  function isNode9SelfOutput(text) {
11420
11442
  let hits = 0;
@@ -12060,14 +12082,17 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
12060
12082
  if (!fs23.existsSync(chatsDir)) continue;
12061
12083
  let chatFiles;
12062
12084
  try {
12063
- chatFiles = fs23.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
12085
+ chatFiles = fs23.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
12064
12086
  } catch {
12065
12087
  continue;
12066
12088
  }
12089
+ const seenSessions = /* @__PURE__ */ new Set();
12067
12090
  for (const chatFile of chatFiles) {
12091
+ const sessionId = chatFile.replace(/\.jsonl?$/, "");
12092
+ if (seenSessions.has(sessionId)) continue;
12093
+ seenSessions.add(sessionId);
12068
12094
  result.filesScanned++;
12069
12095
  onProgress?.(result.filesScanned);
12070
- const sessionId = chatFile.replace(/\.json$/, "");
12071
12096
  let raw;
12072
12097
  try {
12073
12098
  raw = fs23.readFileSync(path25.join(chatsDir, chatFile), "utf-8");
@@ -12077,7 +12102,18 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
12077
12102
  const sessionCalls = [];
12078
12103
  let session;
12079
12104
  try {
12080
- session = JSON.parse(raw);
12105
+ if (chatFile.endsWith(".jsonl")) {
12106
+ const messages = raw.split("\n").filter((l) => l.trim()).map((l) => {
12107
+ try {
12108
+ return JSON.parse(l);
12109
+ } catch {
12110
+ return null;
12111
+ }
12112
+ }).filter((m) => m !== null);
12113
+ session = { messages };
12114
+ } else {
12115
+ session = JSON.parse(raw);
12116
+ }
12081
12117
  } catch {
12082
12118
  continue;
12083
12119
  }
@@ -12687,6 +12723,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12687
12723
  let lastTotalInput = 0;
12688
12724
  let lastTotalCached = 0;
12689
12725
  let lastTotalOutput = 0;
12726
+ let model = "";
12690
12727
  for (const line of lines) {
12691
12728
  if (!line.trim()) continue;
12692
12729
  onLine?.();
@@ -12704,6 +12741,10 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12704
12741
  projLabel = stripTerminalEscapes(cwd.replace(os22.homedir(), "~")).slice(0, 40);
12705
12742
  continue;
12706
12743
  }
12744
+ if (entry.type === "turn_context" && typeof payload["model"] === "string") {
12745
+ model = payload["model"];
12746
+ continue;
12747
+ }
12707
12748
  if (entry.type === "event_msg" && payload["type"] === "token_count") {
12708
12749
  const info = payload["info"];
12709
12750
  const usage = info?.["total_token_usage"] ?? {};
@@ -12847,8 +12888,11 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12847
12888
  }
12848
12889
  }
12849
12890
  }
12850
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
12851
- result.totalCostUSD += nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
12891
+ result.totalCostUSD += codexSessionCost(model, {
12892
+ input: lastTotalInput,
12893
+ cached: lastTotalCached,
12894
+ output: lastTotalOutput
12895
+ });
12852
12896
  result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "codex"));
12853
12897
  }
12854
12898
  return result;
@@ -13895,7 +13939,7 @@ function registerScanCommand(program2) {
13895
13939
  }
13896
13940
  );
13897
13941
  }
13898
- var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
13942
+ var 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;
13899
13943
  var init_scan = __esm({
13900
13944
  "src/cli/commands/scan.ts"() {
13901
13945
  "use strict";
@@ -13904,6 +13948,9 @@ var init_scan = __esm({
13904
13948
  init_policy();
13905
13949
  init_dist();
13906
13950
  init_dlp();
13951
+ init_litellm();
13952
+ init_cost_gemini();
13953
+ init_cost_codex();
13907
13954
  init_hook_payload();
13908
13955
  init_dist();
13909
13956
  init_scan_summary();
@@ -13913,26 +13960,6 @@ var init_scan = __esm({
13913
13960
  init_protection();
13914
13961
  init_scan_json();
13915
13962
  init_scan_history();
13916
- CLAUDE_PRICING = {
13917
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13918
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13919
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
13920
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13921
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13922
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13923
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13924
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13925
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
13926
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
13927
- };
13928
- GEMINI_PRICING = {
13929
- "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
13930
- "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
13931
- "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
13932
- "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
13933
- "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
13934
- "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
13935
- };
13936
13963
  CODE_EXTENSIONS = /* @__PURE__ */ new Set([
13937
13964
  ".ts",
13938
13965
  ".tsx",
@@ -20108,6 +20135,7 @@ import chalk13 from "chalk";
20108
20135
  // src/cli/aggregate/report-audit.ts
20109
20136
  init_costSync();
20110
20137
  init_litellm();
20138
+ init_cost_codex();
20111
20139
  import fs40 from "fs";
20112
20140
  import os36 from "os";
20113
20141
  import path41 from "path";
@@ -20207,24 +20235,11 @@ function isAllow(decision) {
20207
20235
  function isDlp(checkedBy) {
20208
20236
  return !!checkedBy?.includes("dlp");
20209
20237
  }
20210
- var CLAUDE_PRICING2 = {
20211
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
20212
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
20213
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
20214
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20215
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20216
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20217
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20218
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20219
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
20220
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
20221
- };
20222
20238
  function claudeModelPrice2(model) {
20223
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
20224
- for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
20225
- if (base === key || base.startsWith(key + "-") || base.startsWith(key)) return p;
20226
- }
20227
- return null;
20239
+ const t = pricingFor(model);
20240
+ if (!t) return null;
20241
+ const [i, o, cw, cr] = t;
20242
+ return { i, o, cw, cr };
20228
20243
  }
20229
20244
  function emptyClaudeCostAccumulator() {
20230
20245
  return {
@@ -20339,6 +20354,7 @@ function processCodexCostFile(filePath, start, end, acc) {
20339
20354
  return;
20340
20355
  }
20341
20356
  let sessionStart2 = "";
20357
+ let model = "";
20342
20358
  let lastTotalInput = 0;
20343
20359
  let lastTotalCached = 0;
20344
20360
  let lastTotalOutput = 0;
@@ -20356,6 +20372,10 @@ function processCodexCostFile(filePath, start, end, acc) {
20356
20372
  sessionStart2 = String(p["timestamp"] ?? "");
20357
20373
  continue;
20358
20374
  }
20375
+ if (entry.type === "turn_context" && typeof p["model"] === "string") {
20376
+ model = p["model"];
20377
+ continue;
20378
+ }
20359
20379
  if (entry.type === "event_msg" && p["type"] === "token_count") {
20360
20380
  const info = p["info"] ?? {};
20361
20381
  const usage = info["total_token_usage"] ?? {};
@@ -20370,12 +20390,17 @@ function processCodexCostFile(filePath, start, end, acc) {
20370
20390
  if (!sessionStart2) return;
20371
20391
  const ts = new Date(sessionStart2);
20372
20392
  if (ts < start || ts > end) return;
20373
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
20374
- const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
20393
+ const cost = codexSessionCost(model, {
20394
+ input: lastTotalInput,
20395
+ cached: lastTotalCached,
20396
+ output: lastTotalOutput
20397
+ });
20375
20398
  acc.total += cost;
20376
20399
  acc.toolCalls += sessionToolCalls;
20377
20400
  const dateKey = sessionStart2.slice(0, 10);
20378
20401
  acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
20402
+ const normModel = normalizeModel(model || "gpt-5");
20403
+ acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
20379
20404
  }
20380
20405
  function listCodexSessionFiles2(sessionsBase) {
20381
20406
  const jsonlFiles = [];
@@ -20413,13 +20438,25 @@ function listCodexSessionFiles2(sessionsBase) {
20413
20438
  }
20414
20439
  return jsonlFiles;
20415
20440
  }
20441
+ function mergeByModel(...maps) {
20442
+ const out = /* @__PURE__ */ new Map();
20443
+ for (const m of maps) {
20444
+ for (const [k, v] of m) out.set(k, (out.get(k) ?? 0) + v);
20445
+ }
20446
+ return out;
20447
+ }
20416
20448
  function loadCodexCost(start, end, sessionsBase) {
20417
- const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
20449
+ const acc = {
20450
+ total: 0,
20451
+ toolCalls: 0,
20452
+ byDay: /* @__PURE__ */ new Map(),
20453
+ byModel: /* @__PURE__ */ new Map()
20454
+ };
20418
20455
  const files = listCodexSessionFiles2(sessionsBase);
20419
20456
  for (const filePath of files) {
20420
20457
  processCodexCostFile(filePath, start, end, acc);
20421
20458
  }
20422
- return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
20459
+ return { total: acc.total, byDay: acc.byDay, byModel: acc.byModel, toolCalls: acc.toolCalls };
20423
20460
  }
20424
20461
  var GEMINI_FALLBACK_MODELS2 = ["gemini-2.5-flash", "gemini-2.0-flash"];
20425
20462
  function geminiPriceFor2(model) {
@@ -20708,7 +20745,7 @@ function aggregateReportFromAudit(period, opts = {}) {
20708
20745
  cacheWriteTokens: claudeCost.cacheWriteTokens,
20709
20746
  cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
20710
20747
  byDay: claudeCost.byDay,
20711
- byModel: claudeCost.byModel,
20748
+ byModel: mergeByModel(claudeCost.byModel, codexCost.byModel),
20712
20749
  byProject: claudeCost.byProject
20713
20750
  },
20714
20751
  toolMap,
@@ -23477,44 +23514,23 @@ init_scan();
23477
23514
 
23478
23515
  // src/cli/commands/sessions.ts
23479
23516
  init_scan_summary();
23517
+ init_litellm();
23518
+ init_cost_gemini();
23519
+ init_cost_codex();
23480
23520
  import chalk24 from "chalk";
23481
23521
  import fs45 from "fs";
23482
23522
  import path46 from "path";
23483
23523
  import os40 from "os";
23484
- var CLAUDE_PRICING3 = {
23485
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
23486
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
23487
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
23488
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23489
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23490
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23491
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23492
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23493
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
23494
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
23495
- };
23496
23524
  function modelPrice(model) {
23497
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
23498
- for (const [key, p] of Object.entries(CLAUDE_PRICING3)) {
23499
- if (base === key || base.startsWith(key)) return p;
23500
- }
23501
- return null;
23525
+ const t = pricingFor(model);
23526
+ if (!t) return null;
23527
+ const [i, o, cw, cr] = t;
23528
+ return { i, o, cw, cr };
23502
23529
  }
23503
- var GEMINI_PRICING2 = {
23504
- "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
23505
- "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
23506
- "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
23507
- "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
23508
- "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
23509
- "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
23510
- };
23511
23530
  function geminiModelPrice2(model) {
23512
- const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
23513
- for (const [key, p] of Object.entries(GEMINI_PRICING2)) {
23514
- if (base === key || base.startsWith(key)) return p;
23515
- }
23516
- if (base.includes("flash")) return GEMINI_PRICING2["gemini-2.0-flash"];
23517
- return null;
23531
+ const p = geminiPriceFor(model);
23532
+ if (!p) return null;
23533
+ return { i: p.input, o: p.output, cr: p.cacheRead };
23518
23534
  }
23519
23535
  function encodeProjectPath(projectPath) {
23520
23536
  return projectPath.replace(/\//g, "-");
@@ -23807,6 +23823,7 @@ function buildCodexSessions(days, allAuditEntries) {
23807
23823
  let lastTotalInput = 0;
23808
23824
  let lastTotalCached = 0;
23809
23825
  let lastTotalOutput = 0;
23826
+ let model = "";
23810
23827
  for (const line of lines) {
23811
23828
  if (!line.trim()) continue;
23812
23829
  let entry;
@@ -23822,6 +23839,10 @@ function buildCodexSessions(days, allAuditEntries) {
23822
23839
  cwd = String(p["cwd"] ?? "");
23823
23840
  continue;
23824
23841
  }
23842
+ if (entry.type === "turn_context" && typeof p["model"] === "string") {
23843
+ model = p["model"];
23844
+ continue;
23845
+ }
23825
23846
  if (entry.type === "event_msg" && p["type"] === "user_message" && !firstPrompt) {
23826
23847
  firstPrompt = String(p["message"] ?? "");
23827
23848
  continue;
@@ -23848,8 +23869,11 @@ function buildCodexSessions(days, allAuditEntries) {
23848
23869
  }
23849
23870
  if (!sessionId || !startTime) continue;
23850
23871
  if (cutoff && new Date(startTime) < cutoff) continue;
23851
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
23852
- const costUSD = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
23872
+ const costUSD = codexSessionCost(model, {
23873
+ input: lastTotalInput,
23874
+ cached: lastTotalCached,
23875
+ output: lastTotalOutput
23876
+ });
23853
23877
  const windowEnd = new Date(
23854
23878
  Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
23855
23879
  ).toISOString();
@@ -23873,11 +23897,10 @@ function buildCodexSessions(days, allAuditEntries) {
23873
23897
  }
23874
23898
  function buildSessions(days, historyPath) {
23875
23899
  const hPath = historyPath ?? path46.join(os40.homedir(), ".claude", "history.jsonl");
23876
- let historyRaw;
23900
+ let historyRaw = "";
23877
23901
  try {
23878
23902
  historyRaw = fs45.readFileSync(hPath, "utf-8");
23879
23903
  } catch {
23880
- return [];
23881
23904
  }
23882
23905
  const cutoff = days !== null ? (() => {
23883
23906
  const d = /* @__PURE__ */ new Date();
@@ -24168,12 +24191,6 @@ function registerSessionsCommand(program2) {
24168
24191
  console.log("");
24169
24192
  console.log(chalk24.cyan.bold("\u{1F4CB} node9 sessions") + chalk24.dim(" \u2014 what your AI agent did"));
24170
24193
  console.log("");
24171
- const historyPath = path46.join(os40.homedir(), ".claude", "history.jsonl");
24172
- if (!fs45.existsSync(historyPath)) {
24173
- console.log(chalk24.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
24174
- console.log(chalk24.gray(" Install Claude Code, run a few sessions, then try again.\n"));
24175
- return;
24176
- }
24177
24194
  const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
24178
24195
  const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
24179
24196
  console.log(chalk24.dim(" " + rangeLabel));