@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.js CHANGED
@@ -9278,1114 +9278,1140 @@ var init_setup = __esm({
9278
9278
  }
9279
9279
  });
9280
9280
 
9281
- // src/utils/hook-payload.ts
9282
- function extractToolName(payload, defaultValue = "") {
9283
- return payload.tool_name ?? payload.name ?? payload.toolCall?.name ?? defaultValue;
9281
+ // src/pricing/litellm.ts
9282
+ function normalizeModel(raw) {
9283
+ return raw.replace(/-\d{8}$/, "").toLowerCase();
9284
9284
  }
9285
- function extractToolInput(payload) {
9286
- return payload.tool_input ?? payload.args ?? payload.toolCall?.args ?? {};
9285
+ function readCache() {
9286
+ try {
9287
+ const raw = JSON.parse(import_fs14.default.readFileSync(CACHE_FILE(), "utf-8"));
9288
+ if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
9289
+ return null;
9290
+ }
9291
+ const ageMs = Date.now() - new Date(raw.fetchedAt).getTime();
9292
+ if (ageMs < 0 || ageMs > TTL_MS) return null;
9293
+ return raw.prices;
9294
+ } catch {
9295
+ return null;
9296
+ }
9287
9297
  }
9288
- function canonicalToolName(name) {
9289
- switch (name) {
9290
- // Hermes Agent
9291
- case "terminal":
9292
- return "Bash";
9293
- case "write_file":
9294
- return "Write";
9295
- case "patch":
9296
- return "Edit";
9297
- case "read_file":
9298
- return "Read";
9299
- case "search_files":
9300
- return "Grep";
9301
- // Antigravity (agy) — shell tool renamed from Gemini's run_shell_command
9302
- case "run_command":
9303
- return "Bash";
9304
- default:
9305
- return name;
9298
+ function writeCache(prices) {
9299
+ try {
9300
+ const target = CACHE_FILE();
9301
+ const dir = import_path16.default.dirname(target);
9302
+ if (!import_fs14.default.existsSync(dir)) import_fs14.default.mkdirSync(dir, { recursive: true });
9303
+ const tmp = target + ".tmp";
9304
+ const body = {
9305
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
9306
+ prices
9307
+ };
9308
+ import_fs14.default.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
9309
+ import_fs14.default.renameSync(tmp, target);
9310
+ } catch (err2) {
9311
+ try {
9312
+ import_fs14.default.appendFileSync(
9313
+ HOOK_DEBUG_LOG,
9314
+ `[pricing] cache write failed: ${err2.message}
9315
+ `
9316
+ );
9317
+ } catch {
9318
+ }
9306
9319
  }
9307
9320
  }
9308
- function agentLabelFromFlag(flag) {
9309
- if (typeof flag !== "string") return void 0;
9310
- switch (flag.toLowerCase()) {
9311
- case "antigravity":
9312
- case "agy":
9313
- return "Antigravity";
9314
- case "copilot":
9315
- return "GitHub Copilot";
9316
- default:
9317
- return void 0;
9321
+ function tupleFromLiteLLM(entry) {
9322
+ if (!entry || typeof entry !== "object") return null;
9323
+ const e = entry;
9324
+ const num3 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
9325
+ const inCost = num3(e.input_cost_per_token);
9326
+ const outCost = num3(e.output_cost_per_token);
9327
+ if (inCost === 0 && outCost === 0) return null;
9328
+ return [
9329
+ inCost,
9330
+ outCost,
9331
+ num3(e.cache_creation_input_token_cost),
9332
+ num3(e.cache_read_input_token_cost)
9333
+ ];
9334
+ }
9335
+ async function fetchLiteLLMPricing() {
9336
+ try {
9337
+ const res = await fetch(LITELLM_URL, {
9338
+ signal: AbortSignal.timeout(15e3)
9339
+ });
9340
+ if (!res.ok) return null;
9341
+ const json = await res.json();
9342
+ if (!json || typeof json !== "object") return null;
9343
+ const out = {};
9344
+ for (const [key, value] of Object.entries(json)) {
9345
+ const tuple = tupleFromLiteLLM(value);
9346
+ if (tuple) out[key.toLowerCase()] = tuple;
9347
+ }
9348
+ if (Object.keys(out).length < 10) {
9349
+ return null;
9350
+ }
9351
+ return out;
9352
+ } catch {
9353
+ return null;
9318
9354
  }
9319
9355
  }
9320
- function canonicalToolInput(rawToolName, input) {
9321
- if (rawToolName !== "run_command") return input;
9322
- if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
9323
- const args = input;
9324
- if (typeof args.CommandLine !== "string") return input;
9325
- const { CommandLine, Cwd, ...rest } = args;
9326
- const canonical = { ...rest, command: CommandLine };
9327
- if (typeof Cwd === "string" && Cwd.length > 0) canonical.cwd = Cwd;
9328
- return canonical;
9356
+ async function ensurePricingLoaded() {
9357
+ if (memCache !== null && Date.now() - memCacheAt < TTL_MS) return;
9358
+ const fromDisk = readCache();
9359
+ if (fromDisk && Object.keys(fromDisk).length > 0) {
9360
+ memCache = fromDisk;
9361
+ memCacheAt = Date.now();
9362
+ lookupCache.clear();
9363
+ return;
9364
+ }
9365
+ const fetched = await fetchLiteLLMPricing();
9366
+ if (fetched && Object.keys(fetched).length > 0) {
9367
+ memCache = fetched;
9368
+ memCacheAt = Date.now();
9369
+ writeCache(fetched);
9370
+ lookupCache.clear();
9371
+ return;
9372
+ }
9373
+ memCache = { ...BUNDLED_PRICING };
9374
+ memCacheAt = Date.now();
9375
+ lookupCache.clear();
9329
9376
  }
9330
- var init_hook_payload = __esm({
9331
- "src/utils/hook-payload.ts"() {
9377
+ function pricingFor(model) {
9378
+ const norm = normalizeModel(model);
9379
+ const cached = lookupCache.get(norm);
9380
+ if (cached !== void 0) return cached;
9381
+ if (memCache === null && !diskChecked) {
9382
+ diskChecked = true;
9383
+ const disk = readCache();
9384
+ if (disk && Object.keys(disk).length > 0) {
9385
+ memCache = disk;
9386
+ memCacheAt = Date.now();
9387
+ }
9388
+ }
9389
+ const sources = [];
9390
+ if (memCache) sources.push(memCache);
9391
+ sources.push(BUNDLED_PRICING);
9392
+ let resolved = null;
9393
+ for (const source of sources) {
9394
+ const exact = source[norm];
9395
+ if (exact) {
9396
+ resolved = exact;
9397
+ break;
9398
+ }
9399
+ let best = null;
9400
+ for (const key of Object.keys(source)) {
9401
+ if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
9402
+ best = key;
9403
+ }
9404
+ }
9405
+ if (best) {
9406
+ resolved = source[best];
9407
+ break;
9408
+ }
9409
+ }
9410
+ lookupCache.set(norm, resolved);
9411
+ return resolved;
9412
+ }
9413
+ var import_fs14, import_path16, import_os13, LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, diskChecked, lookupCache;
9414
+ var init_litellm = __esm({
9415
+ "src/pricing/litellm.ts"() {
9332
9416
  "use strict";
9417
+ import_fs14 = __toESM(require("fs"));
9418
+ import_path16 = __toESM(require("path"));
9419
+ import_os13 = __toESM(require("os"));
9420
+ init_audit();
9421
+ LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
9422
+ BUNDLED_PRICING = {
9423
+ // Anthropic
9424
+ "claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
9425
+ "claude-opus-4-1": [5e-6, 25e-6, 625e-8, 5e-7],
9426
+ "claude-opus-4-5": [5e-6, 25e-6, 625e-8, 5e-7],
9427
+ "claude-opus-4-6": [5e-6, 25e-6, 625e-8, 5e-7],
9428
+ "claude-opus-4-7": [5e-6, 25e-6, 625e-8, 5e-7],
9429
+ "claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
9430
+ "claude-sonnet-4-5": [3e-6, 15e-6, 375e-8, 3e-7],
9431
+ "claude-sonnet-4-6": [3e-6, 15e-6, 375e-8, 3e-7],
9432
+ "claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
9433
+ "claude-haiku-4-5": [8e-7, 4e-6, 1e-6, 8e-8],
9434
+ "claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
9435
+ "claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
9436
+ "claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
9437
+ "claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
9438
+ // OpenAI. gpt-5 family + o-series copied from the live LiteLLM table
9439
+ // (verified 2026-06-14) — the bundled gpt-5 was stale at $10/$30 vs the real
9440
+ // $1.25/$10, and Codex models (gpt-5-codex etc.) were absent, so the offline
9441
+ // fallback mispriced every Codex session. See cost-codex.codexPriceFor.
9442
+ "gpt-4o": [5e-6, 15e-6, 0, 25e-7],
9443
+ "gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
9444
+ "gpt-5": [125e-8, 1e-5, 0, 125e-9],
9445
+ "gpt-5-codex": [125e-8, 1e-5, 0, 125e-9],
9446
+ "gpt-5-mini": [25e-8, 2e-6, 0, 25e-9],
9447
+ o3: [2e-6, 8e-6, 0, 5e-7],
9448
+ "o4-mini": [11e-7, 44e-7, 0, 275e-9],
9449
+ // Google. Values copied from the live LiteLLM table (verified 2026-06-14)
9450
+ // so the bundled fallback prices the current Gemini tiers correctly offline
9451
+ // — the local cost readers were carrying a stale hardcoded copy where
9452
+ // gemini-2.5-flash read $0.15/$0.60 vs the real $0.30/$2.50 (~4× under on
9453
+ // output). See cost-gemini.geminiPriceFor (the single Gemini price source).
9454
+ "gemini-2.5-pro": [125e-8, 1e-5, 0, 125e-9],
9455
+ "gemini-2.5-flash": [3e-7, 25e-7, 0, 3e-8],
9456
+ "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
9457
+ "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
9458
+ };
9459
+ CACHE_FILE = () => import_path16.default.join(import_os13.default.homedir(), ".node9", "model-pricing.json");
9460
+ TTL_MS = 24 * 60 * 60 * 1e3;
9461
+ memCache = null;
9462
+ memCacheAt = 0;
9463
+ diskChecked = false;
9464
+ lookupCache = /* @__PURE__ */ new Map();
9333
9465
  }
9334
9466
  });
9335
9467
 
9336
- // src/scan-summary.ts
9337
- function agentDisplayName(agent) {
9338
- return AGENT_LONG[agent] ?? "Claude Code";
9468
+ // src/cost-gemini.ts
9469
+ function geminiTmpDir() {
9470
+ return import_path17.default.join(import_os14.default.homedir(), ".gemini", "tmp");
9339
9471
  }
9340
- function agentBadgeText(agent, width = 10) {
9341
- return `[${AGENT_SHORT[agent] ?? "Claude"}]`.padEnd(width);
9472
+ function geminiPriceFor(model) {
9473
+ let tuple = pricingFor(model);
9474
+ if (!tuple && /^gemini-/i.test(model)) {
9475
+ for (const proxy of GEMINI_FALLBACK_MODELS) {
9476
+ tuple = pricingFor(proxy);
9477
+ if (tuple) break;
9478
+ }
9479
+ }
9480
+ if (!tuple) return null;
9481
+ return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
9342
9482
  }
9343
- function agentColorName(agent) {
9344
- switch (agent) {
9345
- case "gemini":
9346
- return "blue";
9347
- case "codex":
9348
- return "magenta";
9349
- case "antigravity":
9350
- return "yellow";
9351
- case "copilot":
9352
- return "green";
9353
- case "shell":
9354
- return "yellow";
9355
- default:
9356
- return "cyan";
9483
+ function safeReaddir(dir) {
9484
+ try {
9485
+ return import_fs15.default.readdirSync(dir);
9486
+ } catch {
9487
+ return [];
9357
9488
  }
9358
9489
  }
9359
- function buildScanSummary(agents) {
9360
- const stats = {
9361
- sessions: 0,
9362
- totalToolCalls: 0,
9363
- bashCalls: 0,
9364
- totalCostUSD: 0,
9365
- firstDate: null,
9366
- lastDate: null
9367
- };
9368
- for (const a of agents) {
9369
- stats.sessions += a.scan.sessions;
9370
- stats.totalToolCalls += a.scan.totalToolCalls;
9371
- stats.bashCalls += a.scan.bashCalls;
9372
- stats.totalCostUSD += a.scan.totalCostUSD;
9373
- if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
9374
- stats.firstDate = a.scan.firstDate;
9375
- }
9376
- if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
9377
- stats.lastDate = a.scan.lastDate;
9490
+ function isDir(p) {
9491
+ try {
9492
+ return import_fs15.default.statSync(p).isDirectory();
9493
+ } catch {
9494
+ return false;
9495
+ }
9496
+ }
9497
+ function listGeminiSessionFiles(base) {
9498
+ const out = [];
9499
+ for (const project of safeReaddir(base)) {
9500
+ const chats = import_path17.default.join(base, project, "chats");
9501
+ if (!isDir(chats)) continue;
9502
+ for (const f of safeReaddir(chats)) {
9503
+ if (f.startsWith("session-") && f.endsWith(".jsonl")) {
9504
+ out.push({ file: import_path17.default.join(chats, f), project });
9505
+ }
9378
9506
  }
9379
9507
  }
9380
- const allFindings = agents.flatMap((a) => a.scan.findings);
9381
- const allLeaks = agents.flatMap(
9382
- (a) => a.scan.dlpFindings.map((f) => ({
9383
- patternName: f.patternName,
9384
- redactedSample: f.redactedSample,
9385
- toolName: f.toolName,
9386
- timestamp: f.timestamp,
9387
- project: f.project,
9388
- sessionId: f.sessionId,
9389
- agent: f.agent
9390
- }))
9391
- );
9392
- const allLoops = agents.flatMap(
9393
- (a) => a.scan.loopFindings.map((f) => ({
9394
- toolName: f.toolName,
9395
- commandPreview: f.commandPreview,
9396
- count: f.count,
9397
- timestamp: f.timestamp,
9398
- project: f.project,
9399
- sessionId: f.sessionId,
9400
- agent: f.agent,
9401
- kind: f.kind
9402
- }))
9403
- );
9404
- const byVerdict = {
9405
- blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
9406
- supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
9407
- leaks: allLeaks.length,
9408
- loops: allLoops.length
9409
- };
9410
- const byAgent = agents.map((a) => ({
9411
- id: a.id,
9412
- label: a.label,
9413
- icon: a.icon,
9414
- sessions: a.scan.sessions,
9415
- findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
9416
- costUSD: a.scan.totalCostUSD
9417
- })).filter((s) => s.sessions > 0 || s.findings > 0);
9418
- const sections = buildSections(allFindings);
9419
- const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
9420
- const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
9421
- return {
9422
- stats,
9423
- byVerdict,
9424
- byAgent,
9425
- sections,
9426
- leaks: allLeaks,
9427
- loops: allLoops,
9428
- loopWastedUSD
9429
- };
9508
+ return out;
9430
9509
  }
9431
- function buildSections(findings) {
9432
- const sectionMap = /* @__PURE__ */ new Map();
9433
- function ensureSection(id, label, subtitle, sourceType, shieldKey) {
9434
- let s = sectionMap.get(id);
9435
- if (!s) {
9436
- s = {
9437
- id,
9438
- label,
9439
- subtitle,
9440
- sourceType,
9441
- shieldKey,
9442
- blockedCount: 0,
9443
- reviewCount: 0,
9444
- rules: []
9445
- };
9446
- sectionMap.set(id, s);
9447
- }
9448
- return s;
9449
- }
9450
- const ruleMap = /* @__PURE__ */ new Map();
9451
- for (const f of findings) {
9452
- const src = f.source;
9453
- const sourceType = src.sourceType;
9454
- const shieldName = src.shieldName;
9455
- const verdict = src.rule.verdict === "block" ? "block" : "review";
9456
- let sectionId;
9457
- let sectionLabel;
9458
- let sectionSubtitle;
9459
- let shieldKey;
9460
- if (sourceType === "default") {
9461
- sectionId = "default";
9462
- sectionLabel = "Default Rules";
9463
- sectionSubtitle = "built-in, always on";
9464
- } else if (sourceType === "shield") {
9465
- sectionId = `shield:${shieldName}`;
9466
- sectionLabel = shieldName;
9467
- sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
9468
- shieldKey = shieldName;
9469
- } else if (shieldName === "cloud") {
9470
- sectionId = "cloud";
9471
- sectionLabel = "Cloud Policy";
9472
- sectionSubtitle = "synced from node9 cloud";
9473
- } else {
9474
- sectionId = "user";
9475
- sectionLabel = "Your Rules";
9476
- sectionSubtitle = "added in node9.config.json";
9510
+ function parseGeminiSession(lines, project) {
9511
+ const seenIds = /* @__PURE__ */ new Set();
9512
+ const byKey = /* @__PURE__ */ new Map();
9513
+ let runId = "";
9514
+ for (const raw of lines) {
9515
+ if (!raw.trim()) continue;
9516
+ let obj;
9517
+ try {
9518
+ obj = JSON.parse(raw);
9519
+ } catch {
9520
+ continue;
9477
9521
  }
9478
- const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
9479
- const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
9480
- const ruleKey = sectionId + "::" + ruleDisplayName;
9481
- let rule = ruleMap.get(ruleKey);
9482
- if (!rule) {
9483
- rule = {
9484
- name: ruleDisplayName,
9485
- verdict,
9486
- reason: src.rule.reason ?? "",
9487
- findings: []
9488
- };
9489
- ruleMap.set(ruleKey, rule);
9490
- section.rules.push(rule);
9522
+ if (typeof obj.sessionId === "string" && !runId) runId = obj.sessionId;
9523
+ if (!obj.tokens || !obj.model || !obj.timestamp) continue;
9524
+ if (obj.id) {
9525
+ if (seenIds.has(obj.id)) continue;
9526
+ seenIds.add(obj.id);
9491
9527
  }
9492
- const cmdPreview = previewCommand(f.input, 120);
9493
- const fullCmd = fullCommandOf(f.input);
9494
- const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
9495
- if (!isDupe) {
9496
- rule.findings.push({
9497
- timestamp: f.timestamp ?? "",
9498
- command: cmdPreview,
9499
- fullCommand: fullCmd,
9500
- project: f.project,
9501
- sessionId: f.sessionId,
9502
- agent: f.agent,
9503
- toolName: f.toolName
9528
+ const price = geminiPriceFor(obj.model);
9529
+ if (!price) continue;
9530
+ const inp = obj.tokens.input ?? 0;
9531
+ const out = obj.tokens.output ?? 0;
9532
+ const cached = Math.min(obj.tokens.cached ?? 0, inp);
9533
+ const fresh = Math.max(0, inp - cached);
9534
+ const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
9535
+ const date = obj.timestamp.slice(0, 10);
9536
+ const model = normalizeModel(obj.model);
9537
+ const key = `${date}::${model}`;
9538
+ const prev = byKey.get(key);
9539
+ if (prev) {
9540
+ prev.costUSD += cost;
9541
+ prev.inputTokens += fresh;
9542
+ prev.outputTokens += out;
9543
+ prev.cacheReadTokens += cached;
9544
+ } else {
9545
+ byKey.set(key, {
9546
+ date,
9547
+ model,
9548
+ workingDir: project,
9549
+ runId,
9550
+ costUSD: cost,
9551
+ inputTokens: fresh,
9552
+ outputTokens: out,
9553
+ cacheReadTokens: cached,
9554
+ cacheWriteTokens: 0
9504
9555
  });
9505
9556
  }
9506
- if (verdict === "block") section.blockedCount++;
9507
- else section.reviewCount++;
9508
9557
  }
9509
- const sections = [...sectionMap.values()];
9510
- sections.sort((a, b) => {
9511
- const aTotal = a.blockedCount + a.reviewCount;
9512
- const bTotal = b.blockedCount + b.reviewCount;
9513
- if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
9514
- return bTotal - aTotal;
9515
- });
9516
- for (const s of sections) {
9517
- s.rules.sort((a, b) => {
9518
- const aBlock = a.verdict === "block" ? 1 : 0;
9519
- const bBlock = b.verdict === "block" ? 1 : 0;
9520
- if (bBlock !== aBlock) return bBlock - aBlock;
9521
- return b.findings.length - a.findings.length;
9522
- });
9523
- }
9524
- return sections;
9525
- }
9526
- function previewCommand(input, max) {
9527
- const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9528
- const s = String(raw).replace(/\s+/g, " ").trim();
9529
- return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
9530
- }
9531
- function fullCommandOf(input) {
9532
- const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9533
- return String(raw).replace(/\s+/g, " ").trim();
9558
+ if (runId) for (const e of byKey.values()) e.runId = runId;
9559
+ return [...byKey.values()];
9534
9560
  }
9535
- var AGENT_SHORT, AGENT_LONG;
9536
- var init_scan_summary = __esm({
9537
- "src/scan-summary.ts"() {
9561
+ var import_fs15, import_os14, import_path17, GEMINI_FALLBACK_MODELS, geminiSource;
9562
+ var init_cost_gemini = __esm({
9563
+ "src/cost-gemini.ts"() {
9538
9564
  "use strict";
9539
- init_shields();
9540
- init_dist();
9541
- init_dist();
9542
- AGENT_SHORT = {
9543
- claude: "Claude",
9544
- gemini: "Gemini",
9545
- codex: "Codex",
9546
- antigravity: "Agy",
9547
- copilot: "Copilot",
9548
- shell: "Shell"
9549
- };
9550
- AGENT_LONG = {
9551
- claude: "Claude Code",
9552
- gemini: "Gemini CLI",
9553
- codex: "Codex",
9554
- antigravity: "Antigravity",
9555
- copilot: "GitHub Copilot",
9556
- shell: "Shell"
9557
- };
9558
- }
9559
- });
9560
-
9561
- // src/cli/commands/blast.ts
9562
- function buildSensitivePaths(home, cwd) {
9563
- return [
9564
- {
9565
- full: import_path16.default.join(home, ".ssh", "id_rsa"),
9566
- label: "~/.ssh/id_rsa",
9567
- description: "RSA private key \u2014 grants SSH access to your servers",
9568
- score: 20
9569
- },
9570
- {
9571
- full: import_path16.default.join(home, ".ssh", "id_ed25519"),
9572
- label: "~/.ssh/id_ed25519",
9573
- description: "Ed25519 private key \u2014 grants SSH access to your servers",
9574
- score: 20
9575
- },
9576
- {
9577
- full: import_path16.default.join(home, ".ssh", "id_ecdsa"),
9578
- label: "~/.ssh/id_ecdsa",
9579
- description: "ECDSA private key \u2014 grants SSH access to your servers",
9580
- score: 20
9581
- },
9582
- {
9583
- full: import_path16.default.join(home, ".aws", "credentials"),
9584
- label: "~/.aws/credentials",
9585
- description: "AWS access keys \u2014 full cloud account access",
9586
- score: 20
9587
- },
9588
- {
9589
- full: import_path16.default.join(home, ".aws", "config"),
9590
- label: "~/.aws/config",
9591
- description: "AWS configuration \u2014 account and region settings",
9592
- score: 5
9593
- },
9594
- {
9595
- full: import_path16.default.join(home, ".config", "gcloud", "credentials.db"),
9596
- label: "~/.config/gcloud/credentials.db",
9597
- description: "Google Cloud credentials",
9598
- score: 15
9599
- },
9600
- {
9601
- full: import_path16.default.join(home, ".docker", "config.json"),
9602
- label: "~/.docker/config.json",
9603
- description: "Docker registry auth tokens",
9604
- score: 10
9605
- },
9606
- {
9607
- full: import_path16.default.join(home, ".netrc"),
9608
- label: "~/.netrc",
9609
- description: "FTP/HTTP credentials in plain text",
9610
- score: 15
9611
- },
9612
- {
9613
- full: import_path16.default.join(home, ".npmrc"),
9614
- label: "~/.npmrc",
9615
- description: "npm auth token \u2014 can publish packages as you",
9616
- score: 10
9617
- },
9618
- {
9619
- full: import_path16.default.join(home, ".node9", "credentials.json"),
9620
- label: "~/.node9/credentials.json",
9621
- description: "Node9 cloud API key",
9622
- score: 10
9623
- },
9624
- {
9625
- full: import_path16.default.join(cwd, ".env"),
9626
- label: ".env (current folder)",
9627
- description: "App secrets \u2014 database passwords, API keys",
9628
- score: 20
9629
- },
9630
- {
9631
- full: import_path16.default.join(cwd, ".env.local"),
9632
- label: ".env.local (current folder)",
9633
- description: "Local overrides \u2014 often contains real credentials",
9634
- score: 15
9635
- },
9636
- {
9637
- full: import_path16.default.join(cwd, ".env.production"),
9638
- label: ".env.production (current folder)",
9639
- description: "Production secrets",
9640
- score: 20
9641
- }
9642
- ];
9643
- }
9644
- function isReadable(filePath) {
9645
- try {
9646
- import_fs14.default.accessSync(filePath, import_fs14.default.constants.R_OK);
9647
- return true;
9648
- } catch {
9649
- return false;
9650
- }
9651
- }
9652
- function scoreLabel(score) {
9653
- if (score >= 80) return import_chalk2.default.green(`${score}/100 Good`);
9654
- if (score >= 50) return import_chalk2.default.yellow(`${score}/100 Moderate risk`);
9655
- if (score >= 25) return import_chalk2.default.red(`${score}/100 High risk`);
9656
- return import_chalk2.default.red.bold(`${score}/100 Critical`);
9657
- }
9658
- function runBlast() {
9659
- const home = import_os13.default.homedir();
9660
- const cwd = process.cwd();
9661
- const paths = buildSensitivePaths(home, cwd);
9662
- let scoreDeduction = 0;
9663
- const reachable = [];
9664
- for (const p of paths) {
9665
- if (import_fs14.default.existsSync(p.full) && isReadable(p.full)) {
9666
- reachable.push(p);
9667
- scoreDeduction += p.score;
9668
- }
9669
- }
9670
- const envFindings = [];
9671
- for (const [key, value] of Object.entries(process.env)) {
9672
- if (!value) continue;
9673
- const match = scanArgs({ [key]: value });
9674
- if (match) {
9675
- envFindings.push({ key, patternName: match.patternName });
9676
- scoreDeduction += 10;
9677
- }
9678
- }
9679
- return { reachable, envFindings, score: Math.max(0, 100 - scoreDeduction) };
9680
- }
9681
- function registerBlastCommand(program2) {
9682
- program2.command("blast").description("Map what an AI agent can currently reach on this machine").action(() => {
9683
- const home = import_os13.default.homedir();
9684
- const cwd = process.cwd();
9685
- const { reachable, envFindings, score } = runBlast();
9686
- console.log("");
9687
- console.log(
9688
- import_chalk2.default.bold(" \u{1F52D} Node9 Blast Radius") + import_chalk2.default.dim(" \xB7 what an AI agent can reach from here")
9689
- );
9690
- console.log(import_chalk2.default.dim(" Running in: ") + import_chalk2.default.white(cwd.replace(home, "~")));
9691
- console.log("");
9692
- if (reachable.length > 0) {
9693
- console.log(" " + import_chalk2.default.red.bold("Sensitive files reachable:"));
9694
- for (const p of reachable) {
9695
- console.log(
9696
- " " + import_chalk2.default.red("\u2717 ") + import_chalk2.default.yellow(p.label.padEnd(38)) + import_chalk2.default.dim(p.description)
9697
- );
9698
- }
9699
- console.log("");
9700
- }
9701
- if (envFindings.length > 0) {
9702
- console.log(" " + import_chalk2.default.red.bold("Secrets in active environment:"));
9703
- for (const f of envFindings) {
9704
- console.log(
9705
- " " + import_chalk2.default.red("\u2717 ") + import_chalk2.default.yellow(f.key.padEnd(38)) + import_chalk2.default.dim(f.patternName)
9706
- );
9565
+ import_fs15 = __toESM(require("fs"));
9566
+ import_os14 = __toESM(require("os"));
9567
+ import_path17 = __toESM(require("path"));
9568
+ init_litellm();
9569
+ GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
9570
+ geminiSource = {
9571
+ id: "gemini",
9572
+ available() {
9573
+ try {
9574
+ return import_fs15.default.existsSync(geminiTmpDir());
9575
+ } catch {
9576
+ return false;
9577
+ }
9578
+ },
9579
+ collect(sinceMs) {
9580
+ const combined = /* @__PURE__ */ new Map();
9581
+ for (const { file, project } of listGeminiSessionFiles(geminiTmpDir())) {
9582
+ try {
9583
+ if (sinceMs !== void 0 && import_fs15.default.statSync(file).mtimeMs < sinceMs) continue;
9584
+ } catch {
9585
+ continue;
9586
+ }
9587
+ let content;
9588
+ try {
9589
+ content = import_fs15.default.readFileSync(file, "utf8");
9590
+ } catch {
9591
+ continue;
9592
+ }
9593
+ for (const e of parseGeminiSession(content.split("\n"), project)) {
9594
+ const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
9595
+ const prev = combined.get(key);
9596
+ if (prev) {
9597
+ prev.costUSD += e.costUSD;
9598
+ prev.inputTokens += e.inputTokens;
9599
+ prev.outputTokens += e.outputTokens;
9600
+ prev.cacheReadTokens += e.cacheReadTokens;
9601
+ prev.cacheWriteTokens += e.cacheWriteTokens;
9602
+ } else {
9603
+ combined.set(key, { ...e });
9604
+ }
9605
+ }
9606
+ }
9607
+ return [...combined.values()];
9707
9608
  }
9708
- console.log("");
9709
- }
9710
- console.log(" " + import_chalk2.default.dim("\u2500".repeat(70)));
9711
- if (reachable.length === 0 && envFindings.length === 0) {
9712
- console.log(" " + import_chalk2.default.green("\u2705 No sensitive files or environment secrets found."));
9713
- console.log(" Security Score: " + scoreLabel(score));
9714
- } else {
9715
- console.log(
9716
- " Security Score: " + scoreLabel(score) + import_chalk2.default.dim(
9717
- ` (${reachable.length} file${reachable.length !== 1 ? "s" : ""}, ${envFindings.length} env var${envFindings.length !== 1 ? "s" : ""})`
9718
- )
9719
- );
9720
- console.log("");
9721
- console.log(
9722
- import_chalk2.default.dim(
9723
- " 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."
9724
- )
9725
- );
9726
- }
9727
- console.log("");
9728
- });
9729
- }
9730
- var import_chalk2, import_fs14, import_path16, import_os13;
9731
- var init_blast = __esm({
9732
- "src/cli/commands/blast.ts"() {
9733
- "use strict";
9734
- import_chalk2 = __toESM(require("chalk"));
9735
- import_fs14 = __toESM(require("fs"));
9736
- import_path16 = __toESM(require("path"));
9737
- import_os13 = __toESM(require("os"));
9738
- init_dlp();
9609
+ };
9739
9610
  }
9740
9611
  });
9741
9612
 
9742
- // src/cli/render/scan-derive.ts
9743
- function classifyScore(score) {
9744
- if (score >= 80) return { band: "good", label: "Good", color: import_chalk3.default.green };
9745
- if (score >= 50) return { band: "at-risk", label: "At Risk", color: import_chalk3.default.yellow };
9746
- return { band: "critical", label: "Critical", color: import_chalk3.default.red };
9613
+ // src/cost-codex.ts
9614
+ function codexSessionsDir() {
9615
+ return import_path18.default.join(import_os15.default.homedir(), ".codex", "sessions");
9747
9616
  }
9748
- function topDlpPatterns(findings, n) {
9749
- const counts = /* @__PURE__ */ new Map();
9750
- for (const f of findings) {
9751
- counts.set(f.patternName, (counts.get(f.patternName) ?? 0) + 1);
9752
- }
9753
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([name, count]) => ({ name, count }));
9617
+ function codexPriceFor(model) {
9618
+ return pricingFor(model) ?? CODEX_FALLBACK;
9754
9619
  }
9755
- function topRulesByVerdict(sections, verdict, n) {
9756
- const matched = [];
9757
- for (const section of sections) {
9758
- for (const rule of section.rules) {
9759
- const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
9760
- if (matches) matched.push({ name: rule.name, count: rule.findings.length });
9620
+ function codexSessionCost(model, tokens) {
9621
+ const nonCached = Math.max(0, tokens.input - tokens.cached);
9622
+ const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
9623
+ return nonCached * pin + tokens.cached * pcr + tokens.output * pout;
9624
+ }
9625
+ function listCodexSessionFiles(base) {
9626
+ const out = [];
9627
+ for (const y of safeReaddir2(base)) {
9628
+ const yp = import_path18.default.join(base, y);
9629
+ if (!isDir2(yp)) continue;
9630
+ for (const m of safeReaddir2(yp)) {
9631
+ const mp = import_path18.default.join(yp, m);
9632
+ if (!isDir2(mp)) continue;
9633
+ for (const d of safeReaddir2(mp)) {
9634
+ const dp = import_path18.default.join(mp, d);
9635
+ if (!isDir2(dp)) continue;
9636
+ for (const f of safeReaddir2(dp)) {
9637
+ if (f.endsWith(".jsonl")) out.push(import_path18.default.join(dp, f));
9638
+ }
9639
+ }
9761
9640
  }
9762
9641
  }
9763
- return matched.sort((a, b) => b.count - a.count).slice(0, n);
9764
- }
9765
- function computeLoopWaste(loops, totalToolCalls) {
9766
- const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
9767
- const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
9768
- return { wastedCalls, wastePct };
9642
+ return out;
9769
9643
  }
9770
- function rollupByShield(sections, topRulesPerShield = 3) {
9771
- const out = [];
9772
- for (const section of sections) {
9773
- if (section.sourceType !== "shield") continue;
9774
- if (!section.shieldKey) continue;
9775
- const totalCatches = section.blockedCount + section.reviewCount;
9776
- 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);
9777
- out.push({
9778
- shieldName: section.shieldKey,
9779
- totalCatches,
9780
- blockCatches: section.blockedCount,
9781
- reviewCatches: section.reviewCount,
9782
- topRuleLabels
9783
- });
9644
+ function safeReaddir2(dir) {
9645
+ try {
9646
+ return import_fs16.default.readdirSync(dir);
9647
+ } catch {
9648
+ return [];
9784
9649
  }
9785
- return out.sort((a, b) => b.totalCatches - a.totalCatches);
9786
9650
  }
9787
- function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
9788
- const inner = width - 4;
9789
- const out = [];
9790
- const titlePad = ` ${title} `;
9791
- const titleWidth = (0, import_string_width.default)(titlePad);
9792
- const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
9793
- const dashFill = "\u2500".repeat(Math.max(0, inner - (0, import_string_width.default)(titleSegment)));
9794
- out.push(import_chalk3.default.dim("\u256D\u2500") + import_chalk3.default.bold(titleSegment) + import_chalk3.default.dim(`${dashFill}\u2500\u256E`));
9795
- for (const line of bodyLines) {
9796
- const padding = " ".repeat(Math.max(0, inner - line.width));
9797
- out.push(import_chalk3.default.dim("\u2502 ") + line.rendered + padding + import_chalk3.default.dim(" \u2502"));
9651
+ function isDir2(p) {
9652
+ try {
9653
+ return import_fs16.default.statSync(p).isDirectory();
9654
+ } catch {
9655
+ return false;
9798
9656
  }
9799
- out.push(import_chalk3.default.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
9800
- return out;
9801
9657
  }
9802
- function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
9803
- const t = new Date(timestamp).getTime();
9804
- if (Number.isNaN(t)) return "?";
9805
- const days = Math.floor((now.getTime() - t) / 864e5);
9806
- if (days < 1) return "today";
9807
- if (days > 90) return "90d+";
9808
- return `${days}d`;
9658
+ function parseCodexSession(lines) {
9659
+ let sessionStart2 = "";
9660
+ let runId = "";
9661
+ let cwd = "";
9662
+ let model = "";
9663
+ let input = 0;
9664
+ let cached = 0;
9665
+ let output = 0;
9666
+ let sawUsage = false;
9667
+ for (const raw of lines) {
9668
+ if (!raw.trim()) continue;
9669
+ let entry;
9670
+ try {
9671
+ entry = JSON.parse(raw);
9672
+ } catch {
9673
+ continue;
9674
+ }
9675
+ const p = entry.payload ?? {};
9676
+ if (entry.type === "session_meta") {
9677
+ if (!sessionStart2 && typeof p["timestamp"] === "string") sessionStart2 = p["timestamp"];
9678
+ if (!runId && typeof p["id"] === "string") runId = p["id"];
9679
+ if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
9680
+ continue;
9681
+ }
9682
+ if (entry.type === "turn_context") {
9683
+ if (typeof p["model"] === "string") model = p["model"];
9684
+ if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
9685
+ continue;
9686
+ }
9687
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
9688
+ const info = p["info"] ?? {};
9689
+ const usage = info["total_token_usage"] ?? {};
9690
+ if (typeof usage["input_tokens"] === "number") input = usage["input_tokens"];
9691
+ if (typeof usage["cached_input_tokens"] === "number") cached = usage["cached_input_tokens"];
9692
+ if (typeof usage["output_tokens"] === "number") output = usage["output_tokens"];
9693
+ sawUsage = true;
9694
+ }
9695
+ }
9696
+ if (!sessionStart2 || !sawUsage) return null;
9697
+ const nonCached = Math.max(0, input - cached);
9698
+ if (nonCached === 0 && output === 0 && cached === 0) return null;
9699
+ const norm = normalizeModel(model || "gpt-5");
9700
+ const costUSD = codexSessionCost(model, { input, cached, output });
9701
+ return {
9702
+ date: sessionStart2.slice(0, 10),
9703
+ model: norm,
9704
+ workingDir: cwd,
9705
+ runId,
9706
+ costUSD,
9707
+ inputTokens: nonCached,
9708
+ outputTokens: output,
9709
+ cacheReadTokens: cached,
9710
+ cacheWriteTokens: 0
9711
+ };
9809
9712
  }
9810
- var import_chalk3, import_string_width, PANEL_WIDTH;
9811
- var init_scan_derive = __esm({
9812
- "src/cli/render/scan-derive.ts"() {
9713
+ var import_fs16, import_os15, import_path18, CODEX_FALLBACK, codexSource;
9714
+ var init_cost_codex = __esm({
9715
+ "src/cost-codex.ts"() {
9813
9716
  "use strict";
9814
- import_chalk3 = __toESM(require("chalk"));
9815
- import_string_width = __toESM(require("string-width"));
9816
- PANEL_WIDTH = 76;
9717
+ import_fs16 = __toESM(require("fs"));
9718
+ import_os15 = __toESM(require("os"));
9719
+ import_path18 = __toESM(require("path"));
9720
+ init_litellm();
9721
+ CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
9722
+ codexSource = {
9723
+ id: "codex",
9724
+ available() {
9725
+ try {
9726
+ return import_fs16.default.existsSync(codexSessionsDir());
9727
+ } catch {
9728
+ return false;
9729
+ }
9730
+ },
9731
+ collect(sinceMs) {
9732
+ const base = codexSessionsDir();
9733
+ const combined = /* @__PURE__ */ new Map();
9734
+ for (const file of listCodexSessionFiles(base)) {
9735
+ try {
9736
+ if (sinceMs !== void 0 && import_fs16.default.statSync(file).mtimeMs < sinceMs) continue;
9737
+ } catch {
9738
+ continue;
9739
+ }
9740
+ let content;
9741
+ try {
9742
+ content = import_fs16.default.readFileSync(file, "utf8");
9743
+ } catch {
9744
+ continue;
9745
+ }
9746
+ const e = parseCodexSession(content.split("\n"));
9747
+ if (!e) continue;
9748
+ const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
9749
+ const prev = combined.get(key);
9750
+ if (prev) {
9751
+ prev.costUSD += e.costUSD;
9752
+ prev.inputTokens += e.inputTokens;
9753
+ prev.outputTokens += e.outputTokens;
9754
+ prev.cacheReadTokens += e.cacheReadTokens;
9755
+ prev.cacheWriteTokens += e.cacheWriteTokens;
9756
+ } else {
9757
+ combined.set(key, { ...e });
9758
+ }
9759
+ }
9760
+ return [...combined.values()];
9761
+ }
9762
+ };
9763
+ }
9764
+ });
9765
+
9766
+ // src/utils/hook-payload.ts
9767
+ function extractToolName(payload, defaultValue = "") {
9768
+ return payload.tool_name ?? payload.name ?? payload.toolCall?.name ?? defaultValue;
9769
+ }
9770
+ function extractToolInput(payload) {
9771
+ return payload.tool_input ?? payload.args ?? payload.toolCall?.args ?? {};
9772
+ }
9773
+ function canonicalToolName(name) {
9774
+ switch (name) {
9775
+ // Hermes Agent
9776
+ case "terminal":
9777
+ return "Bash";
9778
+ case "write_file":
9779
+ return "Write";
9780
+ case "patch":
9781
+ return "Edit";
9782
+ case "read_file":
9783
+ return "Read";
9784
+ case "search_files":
9785
+ return "Grep";
9786
+ // Antigravity (agy) — shell tool renamed from Gemini's run_shell_command
9787
+ case "run_command":
9788
+ return "Bash";
9789
+ default:
9790
+ return name;
9791
+ }
9792
+ }
9793
+ function agentLabelFromFlag(flag) {
9794
+ if (typeof flag !== "string") return void 0;
9795
+ switch (flag.toLowerCase()) {
9796
+ case "antigravity":
9797
+ case "agy":
9798
+ return "Antigravity";
9799
+ case "copilot":
9800
+ return "GitHub Copilot";
9801
+ default:
9802
+ return void 0;
9817
9803
  }
9818
- });
9819
-
9820
- // src/protection.ts
9821
- var PROTECTIVE_SHIELD_DISCOUNTS;
9822
- var init_protection = __esm({
9823
- "src/protection.ts"() {
9804
+ }
9805
+ function canonicalToolInput(rawToolName, input) {
9806
+ if (rawToolName !== "run_command") return input;
9807
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
9808
+ const args = input;
9809
+ if (typeof args.CommandLine !== "string") return input;
9810
+ const { CommandLine, Cwd, ...rest } = args;
9811
+ const canonical = { ...rest, command: CommandLine };
9812
+ if (typeof Cwd === "string" && Cwd.length > 0) canonical.cwd = Cwd;
9813
+ return canonical;
9814
+ }
9815
+ var init_hook_payload = __esm({
9816
+ "src/utils/hook-payload.ts"() {
9824
9817
  "use strict";
9825
- PROTECTIVE_SHIELD_DISCOUNTS = {
9826
- "project-jail": 0.7
9827
- };
9828
9818
  }
9829
9819
  });
9830
9820
 
9831
- // src/cli/render/scan-json.ts
9832
- function buildScanJson(input) {
9833
- const { summary, blast, isWired, generatedAt } = input;
9834
- const { band } = classifyScore(blast.score);
9835
- return {
9836
- schemaVersion: 1,
9837
- generatedAt,
9838
- isWired,
9839
- score: blast.score,
9840
- band,
9841
- totals: {
9842
- blocked: summary.byVerdict.blocked,
9843
- review: summary.byVerdict.supervised,
9844
- leaks: summary.byVerdict.leaks,
9845
- loops: summary.byVerdict.loops,
9846
- blastExposures: blast.reachable.length + blast.envFindings.length
9847
- },
9848
- summary,
9849
- blast: {
9850
- score: blast.score,
9851
- reachable: blast.reachable,
9852
- envFindings: blast.envFindings
9853
- }
9854
- };
9821
+ // src/scan-summary.ts
9822
+ function agentDisplayName(agent) {
9823
+ return AGENT_LONG[agent] ?? "Claude Code";
9855
9824
  }
9856
- var init_scan_json = __esm({
9857
- "src/cli/render/scan-json.ts"() {
9858
- "use strict";
9859
- init_scan_derive();
9825
+ function agentBadgeText(agent, width = 10) {
9826
+ return `[${AGENT_SHORT[agent] ?? "Claude"}]`.padEnd(width);
9827
+ }
9828
+ function agentColorName(agent) {
9829
+ switch (agent) {
9830
+ case "gemini":
9831
+ return "blue";
9832
+ case "codex":
9833
+ return "magenta";
9834
+ case "antigravity":
9835
+ return "yellow";
9836
+ case "copilot":
9837
+ return "green";
9838
+ case "shell":
9839
+ return "yellow";
9840
+ default:
9841
+ return "cyan";
9860
9842
  }
9861
- });
9862
-
9863
- // src/cli/render/scan-history.ts
9864
- function defaultHistoryPath() {
9865
- return import_path17.default.join(import_os14.default.homedir(), ".node9", "scan-history.json");
9866
9843
  }
9867
- function readPreviousScan(opts = {}) {
9868
- const filePath = opts.path ?? defaultHistoryPath();
9869
- try {
9870
- if (!import_fs15.default.existsSync(filePath)) return null;
9871
- const raw = import_fs15.default.readFileSync(filePath, "utf8");
9872
- const parsed = JSON.parse(raw);
9873
- if (!Array.isArray(parsed) || parsed.length === 0) return null;
9874
- const last = parsed[parsed.length - 1];
9875
- if (!isValidRecord(last)) return null;
9876
- return last;
9877
- } catch {
9878
- return null;
9844
+ function buildScanSummary(agents) {
9845
+ const stats = {
9846
+ sessions: 0,
9847
+ totalToolCalls: 0,
9848
+ bashCalls: 0,
9849
+ totalCostUSD: 0,
9850
+ firstDate: null,
9851
+ lastDate: null
9852
+ };
9853
+ for (const a of agents) {
9854
+ stats.sessions += a.scan.sessions;
9855
+ stats.totalToolCalls += a.scan.totalToolCalls;
9856
+ stats.bashCalls += a.scan.bashCalls;
9857
+ stats.totalCostUSD += a.scan.totalCostUSD;
9858
+ if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
9859
+ stats.firstDate = a.scan.firstDate;
9860
+ }
9861
+ if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
9862
+ stats.lastDate = a.scan.lastDate;
9863
+ }
9879
9864
  }
9865
+ const allFindings = agents.flatMap((a) => a.scan.findings);
9866
+ const allLeaks = agents.flatMap(
9867
+ (a) => a.scan.dlpFindings.map((f) => ({
9868
+ patternName: f.patternName,
9869
+ redactedSample: f.redactedSample,
9870
+ toolName: f.toolName,
9871
+ timestamp: f.timestamp,
9872
+ project: f.project,
9873
+ sessionId: f.sessionId,
9874
+ agent: f.agent
9875
+ }))
9876
+ );
9877
+ const allLoops = agents.flatMap(
9878
+ (a) => a.scan.loopFindings.map((f) => ({
9879
+ toolName: f.toolName,
9880
+ commandPreview: f.commandPreview,
9881
+ count: f.count,
9882
+ timestamp: f.timestamp,
9883
+ project: f.project,
9884
+ sessionId: f.sessionId,
9885
+ agent: f.agent,
9886
+ kind: f.kind
9887
+ }))
9888
+ );
9889
+ const byVerdict = {
9890
+ blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
9891
+ supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
9892
+ leaks: allLeaks.length,
9893
+ loops: allLoops.length
9894
+ };
9895
+ const byAgent = agents.map((a) => ({
9896
+ id: a.id,
9897
+ label: a.label,
9898
+ icon: a.icon,
9899
+ sessions: a.scan.sessions,
9900
+ findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
9901
+ costUSD: a.scan.totalCostUSD
9902
+ })).filter((s) => s.sessions > 0 || s.findings > 0);
9903
+ const sections = buildSections(allFindings);
9904
+ const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
9905
+ const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
9906
+ return {
9907
+ stats,
9908
+ byVerdict,
9909
+ byAgent,
9910
+ sections,
9911
+ leaks: allLeaks,
9912
+ loops: allLoops,
9913
+ loopWastedUSD
9914
+ };
9880
9915
  }
9881
- function appendScanHistory(record, opts = {}) {
9882
- const filePath = opts.path ?? defaultHistoryPath();
9883
- const cap = opts.cap ?? SCAN_HISTORY_CAP;
9884
- try {
9885
- import_fs15.default.mkdirSync(import_path17.default.dirname(filePath), { recursive: true });
9886
- let history = [];
9887
- if (import_fs15.default.existsSync(filePath)) {
9888
- try {
9889
- const parsed = JSON.parse(import_fs15.default.readFileSync(filePath, "utf8"));
9890
- if (Array.isArray(parsed)) {
9891
- history = parsed.filter(isValidRecord);
9892
- }
9893
- } catch {
9894
- }
9916
+ function buildSections(findings) {
9917
+ const sectionMap = /* @__PURE__ */ new Map();
9918
+ function ensureSection(id, label, subtitle, sourceType, shieldKey) {
9919
+ let s = sectionMap.get(id);
9920
+ if (!s) {
9921
+ s = {
9922
+ id,
9923
+ label,
9924
+ subtitle,
9925
+ sourceType,
9926
+ shieldKey,
9927
+ blockedCount: 0,
9928
+ reviewCount: 0,
9929
+ rules: []
9930
+ };
9931
+ sectionMap.set(id, s);
9895
9932
  }
9896
- history.push(record);
9897
- if (history.length > cap) {
9898
- history = history.slice(history.length - cap);
9933
+ return s;
9934
+ }
9935
+ const ruleMap = /* @__PURE__ */ new Map();
9936
+ for (const f of findings) {
9937
+ const src = f.source;
9938
+ const sourceType = src.sourceType;
9939
+ const shieldName = src.shieldName;
9940
+ const verdict = src.rule.verdict === "block" ? "block" : "review";
9941
+ let sectionId;
9942
+ let sectionLabel;
9943
+ let sectionSubtitle;
9944
+ let shieldKey;
9945
+ if (sourceType === "default") {
9946
+ sectionId = "default";
9947
+ sectionLabel = "Default Rules";
9948
+ sectionSubtitle = "built-in, always on";
9949
+ } else if (sourceType === "shield") {
9950
+ sectionId = `shield:${shieldName}`;
9951
+ sectionLabel = shieldName;
9952
+ sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
9953
+ shieldKey = shieldName;
9954
+ } else if (shieldName === "cloud") {
9955
+ sectionId = "cloud";
9956
+ sectionLabel = "Cloud Policy";
9957
+ sectionSubtitle = "synced from node9 cloud";
9958
+ } else {
9959
+ sectionId = "user";
9960
+ sectionLabel = "Your Rules";
9961
+ sectionSubtitle = "added in node9.config.json";
9899
9962
  }
9900
- import_fs15.default.writeFileSync(filePath, JSON.stringify(history, null, 2));
9901
- } catch (err2) {
9902
- process.stderr.write(
9903
- `[node9] Warning: could not write scan-history.json: ${err2.message}
9904
- `
9905
- );
9963
+ const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
9964
+ const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
9965
+ const ruleKey = sectionId + "::" + ruleDisplayName;
9966
+ let rule = ruleMap.get(ruleKey);
9967
+ if (!rule) {
9968
+ rule = {
9969
+ name: ruleDisplayName,
9970
+ verdict,
9971
+ reason: src.rule.reason ?? "",
9972
+ findings: []
9973
+ };
9974
+ ruleMap.set(ruleKey, rule);
9975
+ section.rules.push(rule);
9976
+ }
9977
+ const cmdPreview = previewCommand(f.input, 120);
9978
+ const fullCmd = fullCommandOf(f.input);
9979
+ const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
9980
+ if (!isDupe) {
9981
+ rule.findings.push({
9982
+ timestamp: f.timestamp ?? "",
9983
+ command: cmdPreview,
9984
+ fullCommand: fullCmd,
9985
+ project: f.project,
9986
+ sessionId: f.sessionId,
9987
+ agent: f.agent,
9988
+ toolName: f.toolName
9989
+ });
9990
+ }
9991
+ if (verdict === "block") section.blockedCount++;
9992
+ else section.reviewCount++;
9993
+ }
9994
+ const sections = [...sectionMap.values()];
9995
+ sections.sort((a, b) => {
9996
+ const aTotal = a.blockedCount + a.reviewCount;
9997
+ const bTotal = b.blockedCount + b.reviewCount;
9998
+ if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
9999
+ return bTotal - aTotal;
10000
+ });
10001
+ for (const s of sections) {
10002
+ s.rules.sort((a, b) => {
10003
+ const aBlock = a.verdict === "block" ? 1 : 0;
10004
+ const bBlock = b.verdict === "block" ? 1 : 0;
10005
+ if (bBlock !== aBlock) return bBlock - aBlock;
10006
+ return b.findings.length - a.findings.length;
10007
+ });
9906
10008
  }
10009
+ return sections;
9907
10010
  }
9908
- function computeScanDelta(current, previous, now = Date.now()) {
9909
- if (!previous) return null;
9910
- const prevMs = Date.parse(previous.timestamp);
9911
- if (Number.isNaN(prevMs)) return null;
9912
- const scoreDelta = current.score - previous.score;
9913
- const daysAgo = Math.max(0, Math.floor((now - prevMs) / 864e5));
9914
- if (scoreDelta === 0 && daysAgo === 0) return null;
9915
- return { scoreDelta, daysAgo };
10011
+ function previewCommand(input, max) {
10012
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
10013
+ const s = String(raw).replace(/\s+/g, " ").trim();
10014
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
9916
10015
  }
9917
- function isValidRecord(x) {
9918
- if (typeof x !== "object" || x === null) return false;
9919
- const r = x;
9920
- 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";
10016
+ function fullCommandOf(input) {
10017
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
10018
+ return String(raw).replace(/\s+/g, " ").trim();
9921
10019
  }
9922
- var import_fs15, import_path17, import_os14, SCAN_HISTORY_CAP;
9923
- var init_scan_history = __esm({
9924
- "src/cli/render/scan-history.ts"() {
10020
+ var AGENT_SHORT, AGENT_LONG;
10021
+ var init_scan_summary = __esm({
10022
+ "src/scan-summary.ts"() {
9925
10023
  "use strict";
9926
- import_fs15 = __toESM(require("fs"));
9927
- import_path17 = __toESM(require("path"));
9928
- import_os14 = __toESM(require("os"));
9929
- SCAN_HISTORY_CAP = 30;
10024
+ init_shields();
10025
+ init_dist();
10026
+ init_dist();
10027
+ AGENT_SHORT = {
10028
+ claude: "Claude",
10029
+ gemini: "Gemini",
10030
+ codex: "Codex",
10031
+ antigravity: "Agy",
10032
+ copilot: "Copilot",
10033
+ shell: "Shell"
10034
+ };
10035
+ AGENT_LONG = {
10036
+ claude: "Claude Code",
10037
+ gemini: "Gemini CLI",
10038
+ codex: "Codex",
10039
+ antigravity: "Antigravity",
10040
+ copilot: "GitHub Copilot",
10041
+ shell: "Shell"
10042
+ };
9930
10043
  }
9931
10044
  });
9932
10045
 
9933
- // src/pricing/litellm.ts
9934
- function normalizeModel(raw) {
9935
- return raw.replace(/-\d{8}$/, "").toLowerCase();
9936
- }
9937
- function readCache() {
9938
- try {
9939
- const raw = JSON.parse(import_fs16.default.readFileSync(CACHE_FILE(), "utf-8"));
9940
- if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
9941
- return null;
9942
- }
9943
- const ageMs = Date.now() - new Date(raw.fetchedAt).getTime();
9944
- if (ageMs < 0 || ageMs > TTL_MS) return null;
9945
- return raw.prices;
9946
- } catch {
9947
- return null;
9948
- }
9949
- }
9950
- function writeCache(prices) {
9951
- try {
9952
- const target = CACHE_FILE();
9953
- const dir = import_path18.default.dirname(target);
9954
- if (!import_fs16.default.existsSync(dir)) import_fs16.default.mkdirSync(dir, { recursive: true });
9955
- const tmp = target + ".tmp";
9956
- const body = {
9957
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
9958
- prices
9959
- };
9960
- import_fs16.default.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
9961
- import_fs16.default.renameSync(tmp, target);
9962
- } catch (err2) {
9963
- try {
9964
- import_fs16.default.appendFileSync(
9965
- HOOK_DEBUG_LOG,
9966
- `[pricing] cache write failed: ${err2.message}
9967
- `
9968
- );
9969
- } catch {
9970
- }
9971
- }
9972
- }
9973
- function tupleFromLiteLLM(entry) {
9974
- if (!entry || typeof entry !== "object") return null;
9975
- const e = entry;
9976
- const num3 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
9977
- const inCost = num3(e.input_cost_per_token);
9978
- const outCost = num3(e.output_cost_per_token);
9979
- if (inCost === 0 && outCost === 0) return null;
10046
+ // src/cli/commands/blast.ts
10047
+ function buildSensitivePaths(home, cwd) {
9980
10048
  return [
9981
- inCost,
9982
- outCost,
9983
- num3(e.cache_creation_input_token_cost),
9984
- num3(e.cache_read_input_token_cost)
10049
+ {
10050
+ full: import_path19.default.join(home, ".ssh", "id_rsa"),
10051
+ label: "~/.ssh/id_rsa",
10052
+ description: "RSA private key \u2014 grants SSH access to your servers",
10053
+ score: 20
10054
+ },
10055
+ {
10056
+ full: import_path19.default.join(home, ".ssh", "id_ed25519"),
10057
+ label: "~/.ssh/id_ed25519",
10058
+ description: "Ed25519 private key \u2014 grants SSH access to your servers",
10059
+ score: 20
10060
+ },
10061
+ {
10062
+ full: import_path19.default.join(home, ".ssh", "id_ecdsa"),
10063
+ label: "~/.ssh/id_ecdsa",
10064
+ description: "ECDSA private key \u2014 grants SSH access to your servers",
10065
+ score: 20
10066
+ },
10067
+ {
10068
+ full: import_path19.default.join(home, ".aws", "credentials"),
10069
+ label: "~/.aws/credentials",
10070
+ description: "AWS access keys \u2014 full cloud account access",
10071
+ score: 20
10072
+ },
10073
+ {
10074
+ full: import_path19.default.join(home, ".aws", "config"),
10075
+ label: "~/.aws/config",
10076
+ description: "AWS configuration \u2014 account and region settings",
10077
+ score: 5
10078
+ },
10079
+ {
10080
+ full: import_path19.default.join(home, ".config", "gcloud", "credentials.db"),
10081
+ label: "~/.config/gcloud/credentials.db",
10082
+ description: "Google Cloud credentials",
10083
+ score: 15
10084
+ },
10085
+ {
10086
+ full: import_path19.default.join(home, ".docker", "config.json"),
10087
+ label: "~/.docker/config.json",
10088
+ description: "Docker registry auth tokens",
10089
+ score: 10
10090
+ },
10091
+ {
10092
+ full: import_path19.default.join(home, ".netrc"),
10093
+ label: "~/.netrc",
10094
+ description: "FTP/HTTP credentials in plain text",
10095
+ score: 15
10096
+ },
10097
+ {
10098
+ full: import_path19.default.join(home, ".npmrc"),
10099
+ label: "~/.npmrc",
10100
+ description: "npm auth token \u2014 can publish packages as you",
10101
+ score: 10
10102
+ },
10103
+ {
10104
+ full: import_path19.default.join(home, ".node9", "credentials.json"),
10105
+ label: "~/.node9/credentials.json",
10106
+ description: "Node9 cloud API key",
10107
+ score: 10
10108
+ },
10109
+ {
10110
+ full: import_path19.default.join(cwd, ".env"),
10111
+ label: ".env (current folder)",
10112
+ description: "App secrets \u2014 database passwords, API keys",
10113
+ score: 20
10114
+ },
10115
+ {
10116
+ full: import_path19.default.join(cwd, ".env.local"),
10117
+ label: ".env.local (current folder)",
10118
+ description: "Local overrides \u2014 often contains real credentials",
10119
+ score: 15
10120
+ },
10121
+ {
10122
+ full: import_path19.default.join(cwd, ".env.production"),
10123
+ label: ".env.production (current folder)",
10124
+ description: "Production secrets",
10125
+ score: 20
10126
+ }
9985
10127
  ];
9986
10128
  }
9987
- async function fetchLiteLLMPricing() {
10129
+ function isReadable(filePath) {
9988
10130
  try {
9989
- const res = await fetch(LITELLM_URL, {
9990
- signal: AbortSignal.timeout(15e3)
9991
- });
9992
- if (!res.ok) return null;
9993
- const json = await res.json();
9994
- if (!json || typeof json !== "object") return null;
9995
- const out = {};
9996
- for (const [key, value] of Object.entries(json)) {
9997
- const tuple = tupleFromLiteLLM(value);
9998
- if (tuple) out[key.toLowerCase()] = tuple;
9999
- }
10000
- if (Object.keys(out).length < 10) {
10001
- return null;
10002
- }
10003
- return out;
10131
+ import_fs17.default.accessSync(filePath, import_fs17.default.constants.R_OK);
10132
+ return true;
10004
10133
  } catch {
10005
- return null;
10134
+ return false;
10006
10135
  }
10007
10136
  }
10008
- async function ensurePricingLoaded() {
10009
- if (memCache !== null && Date.now() - memCacheAt < TTL_MS) return;
10010
- const fromDisk = readCache();
10011
- if (fromDisk && Object.keys(fromDisk).length > 0) {
10012
- memCache = fromDisk;
10013
- memCacheAt = Date.now();
10014
- lookupCache.clear();
10015
- return;
10137
+ function scoreLabel(score) {
10138
+ if (score >= 80) return import_chalk2.default.green(`${score}/100 Good`);
10139
+ if (score >= 50) return import_chalk2.default.yellow(`${score}/100 Moderate risk`);
10140
+ if (score >= 25) return import_chalk2.default.red(`${score}/100 High risk`);
10141
+ return import_chalk2.default.red.bold(`${score}/100 Critical`);
10142
+ }
10143
+ function runBlast() {
10144
+ const home = import_os16.default.homedir();
10145
+ const cwd = process.cwd();
10146
+ const paths = buildSensitivePaths(home, cwd);
10147
+ let scoreDeduction = 0;
10148
+ const reachable = [];
10149
+ for (const p of paths) {
10150
+ if (import_fs17.default.existsSync(p.full) && isReadable(p.full)) {
10151
+ reachable.push(p);
10152
+ scoreDeduction += p.score;
10153
+ }
10016
10154
  }
10017
- const fetched = await fetchLiteLLMPricing();
10018
- if (fetched && Object.keys(fetched).length > 0) {
10019
- memCache = fetched;
10020
- memCacheAt = Date.now();
10021
- writeCache(fetched);
10022
- lookupCache.clear();
10023
- return;
10155
+ const envFindings = [];
10156
+ for (const [key, value] of Object.entries(process.env)) {
10157
+ if (!value) continue;
10158
+ const match = scanArgs({ [key]: value });
10159
+ if (match) {
10160
+ envFindings.push({ key, patternName: match.patternName });
10161
+ scoreDeduction += 10;
10162
+ }
10024
10163
  }
10025
- memCache = { ...BUNDLED_PRICING };
10026
- memCacheAt = Date.now();
10027
- lookupCache.clear();
10164
+ return { reachable, envFindings, score: Math.max(0, 100 - scoreDeduction) };
10028
10165
  }
10029
- function pricingFor(model) {
10030
- const norm = normalizeModel(model);
10031
- const cached = lookupCache.get(norm);
10032
- if (cached !== void 0) return cached;
10033
- const sources = [];
10034
- if (memCache) sources.push(memCache);
10035
- sources.push(BUNDLED_PRICING);
10036
- let resolved = null;
10037
- for (const source of sources) {
10038
- const exact = source[norm];
10039
- if (exact) {
10040
- resolved = exact;
10041
- break;
10166
+ function registerBlastCommand(program2) {
10167
+ program2.command("blast").description("Map what an AI agent can currently reach on this machine").action(() => {
10168
+ const home = import_os16.default.homedir();
10169
+ const cwd = process.cwd();
10170
+ const { reachable, envFindings, score } = runBlast();
10171
+ console.log("");
10172
+ console.log(
10173
+ import_chalk2.default.bold(" \u{1F52D} Node9 Blast Radius") + import_chalk2.default.dim(" \xB7 what an AI agent can reach from here")
10174
+ );
10175
+ console.log(import_chalk2.default.dim(" Running in: ") + import_chalk2.default.white(cwd.replace(home, "~")));
10176
+ console.log("");
10177
+ if (reachable.length > 0) {
10178
+ console.log(" " + import_chalk2.default.red.bold("Sensitive files reachable:"));
10179
+ for (const p of reachable) {
10180
+ console.log(
10181
+ " " + import_chalk2.default.red("\u2717 ") + import_chalk2.default.yellow(p.label.padEnd(38)) + import_chalk2.default.dim(p.description)
10182
+ );
10183
+ }
10184
+ console.log("");
10042
10185
  }
10043
- let best = null;
10044
- for (const key of Object.keys(source)) {
10045
- if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
10046
- best = key;
10186
+ if (envFindings.length > 0) {
10187
+ console.log(" " + import_chalk2.default.red.bold("Secrets in active environment:"));
10188
+ for (const f of envFindings) {
10189
+ console.log(
10190
+ " " + import_chalk2.default.red("\u2717 ") + import_chalk2.default.yellow(f.key.padEnd(38)) + import_chalk2.default.dim(f.patternName)
10191
+ );
10047
10192
  }
10193
+ console.log("");
10048
10194
  }
10049
- if (best) {
10050
- resolved = source[best];
10051
- break;
10195
+ console.log(" " + import_chalk2.default.dim("\u2500".repeat(70)));
10196
+ if (reachable.length === 0 && envFindings.length === 0) {
10197
+ console.log(" " + import_chalk2.default.green("\u2705 No sensitive files or environment secrets found."));
10198
+ console.log(" Security Score: " + scoreLabel(score));
10199
+ } else {
10200
+ console.log(
10201
+ " Security Score: " + scoreLabel(score) + import_chalk2.default.dim(
10202
+ ` (${reachable.length} file${reachable.length !== 1 ? "s" : ""}, ${envFindings.length} env var${envFindings.length !== 1 ? "s" : ""})`
10203
+ )
10204
+ );
10205
+ console.log("");
10206
+ console.log(
10207
+ import_chalk2.default.dim(
10208
+ " 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."
10209
+ )
10210
+ );
10052
10211
  }
10053
- }
10054
- lookupCache.set(norm, resolved);
10055
- return resolved;
10212
+ console.log("");
10213
+ });
10056
10214
  }
10057
- var import_fs16, import_path18, import_os15, LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, lookupCache;
10058
- var init_litellm = __esm({
10059
- "src/pricing/litellm.ts"() {
10215
+ var import_chalk2, import_fs17, import_path19, import_os16;
10216
+ var init_blast = __esm({
10217
+ "src/cli/commands/blast.ts"() {
10060
10218
  "use strict";
10061
- import_fs16 = __toESM(require("fs"));
10062
- import_path18 = __toESM(require("path"));
10063
- import_os15 = __toESM(require("os"));
10064
- init_audit();
10065
- LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
10066
- BUNDLED_PRICING = {
10067
- // Anthropic
10068
- "claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
10069
- "claude-opus-4-1": [5e-6, 25e-6, 625e-8, 5e-7],
10070
- "claude-opus-4-5": [5e-6, 25e-6, 625e-8, 5e-7],
10071
- "claude-opus-4-6": [5e-6, 25e-6, 625e-8, 5e-7],
10072
- "claude-opus-4-7": [5e-6, 25e-6, 625e-8, 5e-7],
10073
- "claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
10074
- "claude-sonnet-4-5": [3e-6, 15e-6, 375e-8, 3e-7],
10075
- "claude-sonnet-4-6": [3e-6, 15e-6, 375e-8, 3e-7],
10076
- "claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
10077
- "claude-haiku-4-5": [8e-7, 4e-6, 1e-6, 8e-8],
10078
- "claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
10079
- "claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
10080
- "claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
10081
- "claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
10082
- // OpenAI
10083
- "gpt-4o": [5e-6, 15e-6, 0, 25e-7],
10084
- "gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
10085
- "gpt-5": [1e-5, 3e-5, 0, 5e-6],
10086
- // Google
10087
- "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
10088
- "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
10089
- };
10090
- CACHE_FILE = () => import_path18.default.join(import_os15.default.homedir(), ".node9", "model-pricing.json");
10091
- TTL_MS = 24 * 60 * 60 * 1e3;
10092
- memCache = null;
10093
- memCacheAt = 0;
10094
- lookupCache = /* @__PURE__ */ new Map();
10219
+ import_chalk2 = __toESM(require("chalk"));
10220
+ import_fs17 = __toESM(require("fs"));
10221
+ import_path19 = __toESM(require("path"));
10222
+ import_os16 = __toESM(require("os"));
10223
+ init_dlp();
10095
10224
  }
10096
10225
  });
10097
10226
 
10098
- // src/cost-codex.ts
10099
- function codexSessionsDir() {
10100
- return import_path19.default.join(import_os16.default.homedir(), ".codex", "sessions");
10227
+ // src/cli/render/scan-derive.ts
10228
+ function classifyScore(score) {
10229
+ if (score >= 80) return { band: "good", label: "Good", color: import_chalk3.default.green };
10230
+ if (score >= 50) return { band: "at-risk", label: "At Risk", color: import_chalk3.default.yellow };
10231
+ return { band: "critical", label: "Critical", color: import_chalk3.default.red };
10101
10232
  }
10102
- function codexPriceFor(model) {
10103
- return pricingFor(model) ?? CODEX_FALLBACK;
10233
+ function topDlpPatterns(findings, n) {
10234
+ const counts = /* @__PURE__ */ new Map();
10235
+ for (const f of findings) {
10236
+ counts.set(f.patternName, (counts.get(f.patternName) ?? 0) + 1);
10237
+ }
10238
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([name, count]) => ({ name, count }));
10104
10239
  }
10105
- function listCodexSessionFiles(base) {
10106
- const out = [];
10107
- for (const y of safeReaddir(base)) {
10108
- const yp = import_path19.default.join(base, y);
10109
- if (!isDir(yp)) continue;
10110
- for (const m of safeReaddir(yp)) {
10111
- const mp = import_path19.default.join(yp, m);
10112
- if (!isDir(mp)) continue;
10113
- for (const d of safeReaddir(mp)) {
10114
- const dp = import_path19.default.join(mp, d);
10115
- if (!isDir(dp)) continue;
10116
- for (const f of safeReaddir(dp)) {
10117
- if (f.endsWith(".jsonl")) out.push(import_path19.default.join(dp, f));
10118
- }
10119
- }
10240
+ function topRulesByVerdict(sections, verdict, n) {
10241
+ const matched = [];
10242
+ for (const section of sections) {
10243
+ for (const rule of section.rules) {
10244
+ const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
10245
+ if (matches) matched.push({ name: rule.name, count: rule.findings.length });
10120
10246
  }
10121
10247
  }
10122
- return out;
10248
+ return matched.sort((a, b) => b.count - a.count).slice(0, n);
10123
10249
  }
10124
- function safeReaddir(dir) {
10125
- try {
10126
- return import_fs17.default.readdirSync(dir);
10127
- } catch {
10128
- return [];
10129
- }
10250
+ function computeLoopWaste(loops, totalToolCalls) {
10251
+ const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
10252
+ const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
10253
+ return { wastedCalls, wastePct };
10130
10254
  }
10131
- function isDir(p) {
10132
- try {
10133
- return import_fs17.default.statSync(p).isDirectory();
10134
- } catch {
10135
- return false;
10255
+ function rollupByShield(sections, topRulesPerShield = 3) {
10256
+ const out = [];
10257
+ for (const section of sections) {
10258
+ if (section.sourceType !== "shield") continue;
10259
+ if (!section.shieldKey) continue;
10260
+ const totalCatches = section.blockedCount + section.reviewCount;
10261
+ 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);
10262
+ out.push({
10263
+ shieldName: section.shieldKey,
10264
+ totalCatches,
10265
+ blockCatches: section.blockedCount,
10266
+ reviewCatches: section.reviewCount,
10267
+ topRuleLabels
10268
+ });
10136
10269
  }
10270
+ return out.sort((a, b) => b.totalCatches - a.totalCatches);
10137
10271
  }
10138
- function parseCodexSession(lines) {
10139
- let sessionStart2 = "";
10140
- let runId = "";
10141
- let cwd = "";
10142
- let model = "";
10143
- let input = 0;
10144
- let cached = 0;
10145
- let output = 0;
10146
- let sawUsage = false;
10147
- for (const raw of lines) {
10148
- if (!raw.trim()) continue;
10149
- let entry;
10150
- try {
10151
- entry = JSON.parse(raw);
10152
- } catch {
10153
- continue;
10154
- }
10155
- const p = entry.payload ?? {};
10156
- if (entry.type === "session_meta") {
10157
- if (!sessionStart2 && typeof p["timestamp"] === "string") sessionStart2 = p["timestamp"];
10158
- if (!runId && typeof p["id"] === "string") runId = p["id"];
10159
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
10160
- continue;
10161
- }
10162
- if (entry.type === "turn_context") {
10163
- if (typeof p["model"] === "string") model = p["model"];
10164
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
10165
- continue;
10166
- }
10167
- if (entry.type === "event_msg" && p["type"] === "token_count") {
10168
- const info = p["info"] ?? {};
10169
- const usage = info["total_token_usage"] ?? {};
10170
- if (typeof usage["input_tokens"] === "number") input = usage["input_tokens"];
10171
- if (typeof usage["cached_input_tokens"] === "number") cached = usage["cached_input_tokens"];
10172
- if (typeof usage["output_tokens"] === "number") output = usage["output_tokens"];
10173
- sawUsage = true;
10174
- }
10272
+ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
10273
+ const inner = width - 4;
10274
+ const out = [];
10275
+ const titlePad = ` ${title} `;
10276
+ const titleWidth = (0, import_string_width.default)(titlePad);
10277
+ const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
10278
+ const dashFill = "\u2500".repeat(Math.max(0, inner - (0, import_string_width.default)(titleSegment)));
10279
+ out.push(import_chalk3.default.dim("\u256D\u2500") + import_chalk3.default.bold(titleSegment) + import_chalk3.default.dim(`${dashFill}\u2500\u256E`));
10280
+ for (const line of bodyLines) {
10281
+ const padding = " ".repeat(Math.max(0, inner - line.width));
10282
+ out.push(import_chalk3.default.dim("\u2502 ") + line.rendered + padding + import_chalk3.default.dim(" \u2502"));
10175
10283
  }
10176
- if (!sessionStart2 || !sawUsage) return null;
10177
- const nonCached = Math.max(0, input - cached);
10178
- if (nonCached === 0 && output === 0 && cached === 0) return null;
10179
- const norm = normalizeModel(model || "gpt-5");
10180
- const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
10181
- const costUSD = nonCached * pin + output * pout + cached * pcr;
10182
- return {
10183
- date: sessionStart2.slice(0, 10),
10184
- model: norm,
10185
- workingDir: cwd,
10186
- runId,
10187
- costUSD,
10188
- inputTokens: nonCached,
10189
- outputTokens: output,
10190
- cacheReadTokens: cached,
10191
- cacheWriteTokens: 0
10192
- };
10284
+ out.push(import_chalk3.default.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
10285
+ return out;
10193
10286
  }
10194
- var import_fs17, import_os16, import_path19, CODEX_FALLBACK, codexSource;
10195
- var init_cost_codex = __esm({
10196
- "src/cost-codex.ts"() {
10197
- "use strict";
10198
- import_fs17 = __toESM(require("fs"));
10199
- import_os16 = __toESM(require("os"));
10200
- import_path19 = __toESM(require("path"));
10201
- init_litellm();
10202
- CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
10203
- codexSource = {
10204
- id: "codex",
10205
- available() {
10206
- try {
10207
- return import_fs17.default.existsSync(codexSessionsDir());
10208
- } catch {
10209
- return false;
10210
- }
10211
- },
10212
- collect(sinceMs) {
10213
- const base = codexSessionsDir();
10214
- const combined = /* @__PURE__ */ new Map();
10215
- for (const file of listCodexSessionFiles(base)) {
10216
- try {
10217
- if (sinceMs !== void 0 && import_fs17.default.statSync(file).mtimeMs < sinceMs) continue;
10218
- } catch {
10219
- continue;
10220
- }
10221
- let content;
10222
- try {
10223
- content = import_fs17.default.readFileSync(file, "utf8");
10224
- } catch {
10225
- continue;
10226
- }
10227
- const e = parseCodexSession(content.split("\n"));
10228
- if (!e) continue;
10229
- const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
10230
- const prev = combined.get(key);
10231
- if (prev) {
10232
- prev.costUSD += e.costUSD;
10233
- prev.inputTokens += e.inputTokens;
10234
- prev.outputTokens += e.outputTokens;
10235
- prev.cacheReadTokens += e.cacheReadTokens;
10236
- prev.cacheWriteTokens += e.cacheWriteTokens;
10237
- } else {
10238
- combined.set(key, { ...e });
10239
- }
10240
- }
10241
- return [...combined.values()];
10242
- }
10287
+ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
10288
+ const t = new Date(timestamp).getTime();
10289
+ if (Number.isNaN(t)) return "?";
10290
+ const days = Math.floor((now.getTime() - t) / 864e5);
10291
+ if (days < 1) return "today";
10292
+ if (days > 90) return "90d+";
10293
+ return `${days}d`;
10294
+ }
10295
+ var import_chalk3, import_string_width, PANEL_WIDTH;
10296
+ var init_scan_derive = __esm({
10297
+ "src/cli/render/scan-derive.ts"() {
10298
+ "use strict";
10299
+ import_chalk3 = __toESM(require("chalk"));
10300
+ import_string_width = __toESM(require("string-width"));
10301
+ PANEL_WIDTH = 76;
10302
+ }
10303
+ });
10304
+
10305
+ // src/protection.ts
10306
+ var PROTECTIVE_SHIELD_DISCOUNTS;
10307
+ var init_protection = __esm({
10308
+ "src/protection.ts"() {
10309
+ "use strict";
10310
+ PROTECTIVE_SHIELD_DISCOUNTS = {
10311
+ "project-jail": 0.7
10243
10312
  };
10244
10313
  }
10245
10314
  });
10246
10315
 
10247
- // src/cost-gemini.ts
10248
- function geminiTmpDir() {
10249
- return import_path20.default.join(import_os17.default.homedir(), ".gemini", "tmp");
10250
- }
10251
- function geminiPriceFor(model) {
10252
- let tuple = pricingFor(model);
10253
- if (!tuple && /^gemini-/i.test(model)) {
10254
- for (const proxy of GEMINI_FALLBACK_MODELS) {
10255
- tuple = pricingFor(proxy);
10256
- if (tuple) break;
10316
+ // src/cli/render/scan-json.ts
10317
+ function buildScanJson(input) {
10318
+ const { summary, blast, isWired, generatedAt } = input;
10319
+ const { band } = classifyScore(blast.score);
10320
+ return {
10321
+ schemaVersion: 1,
10322
+ generatedAt,
10323
+ isWired,
10324
+ score: blast.score,
10325
+ band,
10326
+ totals: {
10327
+ blocked: summary.byVerdict.blocked,
10328
+ review: summary.byVerdict.supervised,
10329
+ leaks: summary.byVerdict.leaks,
10330
+ loops: summary.byVerdict.loops,
10331
+ blastExposures: blast.reachable.length + blast.envFindings.length
10332
+ },
10333
+ summary,
10334
+ blast: {
10335
+ score: blast.score,
10336
+ reachable: blast.reachable,
10337
+ envFindings: blast.envFindings
10257
10338
  }
10258
- }
10259
- if (!tuple) return null;
10260
- return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
10339
+ };
10261
10340
  }
10262
- function safeReaddir2(dir) {
10263
- try {
10264
- return import_fs18.default.readdirSync(dir);
10265
- } catch {
10266
- return [];
10341
+ var init_scan_json = __esm({
10342
+ "src/cli/render/scan-json.ts"() {
10343
+ "use strict";
10344
+ init_scan_derive();
10267
10345
  }
10346
+ });
10347
+
10348
+ // src/cli/render/scan-history.ts
10349
+ function defaultHistoryPath() {
10350
+ return import_path20.default.join(import_os17.default.homedir(), ".node9", "scan-history.json");
10268
10351
  }
10269
- function isDir2(p) {
10352
+ function readPreviousScan(opts = {}) {
10353
+ const filePath = opts.path ?? defaultHistoryPath();
10270
10354
  try {
10271
- return import_fs18.default.statSync(p).isDirectory();
10355
+ if (!import_fs18.default.existsSync(filePath)) return null;
10356
+ const raw = import_fs18.default.readFileSync(filePath, "utf8");
10357
+ const parsed = JSON.parse(raw);
10358
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
10359
+ const last = parsed[parsed.length - 1];
10360
+ if (!isValidRecord(last)) return null;
10361
+ return last;
10272
10362
  } catch {
10273
- return false;
10363
+ return null;
10274
10364
  }
10275
10365
  }
10276
- function listGeminiSessionFiles(base) {
10277
- const out = [];
10278
- for (const project of safeReaddir2(base)) {
10279
- const chats = import_path20.default.join(base, project, "chats");
10280
- if (!isDir2(chats)) continue;
10281
- for (const f of safeReaddir2(chats)) {
10282
- if (f.startsWith("session-") && f.endsWith(".jsonl")) {
10283
- out.push({ file: import_path20.default.join(chats, f), project });
10366
+ function appendScanHistory(record, opts = {}) {
10367
+ const filePath = opts.path ?? defaultHistoryPath();
10368
+ const cap = opts.cap ?? SCAN_HISTORY_CAP;
10369
+ try {
10370
+ import_fs18.default.mkdirSync(import_path20.default.dirname(filePath), { recursive: true });
10371
+ let history = [];
10372
+ if (import_fs18.default.existsSync(filePath)) {
10373
+ try {
10374
+ const parsed = JSON.parse(import_fs18.default.readFileSync(filePath, "utf8"));
10375
+ if (Array.isArray(parsed)) {
10376
+ history = parsed.filter(isValidRecord);
10377
+ }
10378
+ } catch {
10284
10379
  }
10285
10380
  }
10286
- }
10287
- return out;
10288
- }
10289
- function parseGeminiSession(lines, project) {
10290
- const seenIds = /* @__PURE__ */ new Set();
10291
- const byKey = /* @__PURE__ */ new Map();
10292
- let runId = "";
10293
- for (const raw of lines) {
10294
- if (!raw.trim()) continue;
10295
- let obj;
10296
- try {
10297
- obj = JSON.parse(raw);
10298
- } catch {
10299
- continue;
10300
- }
10301
- if (typeof obj.sessionId === "string" && !runId) runId = obj.sessionId;
10302
- if (!obj.tokens || !obj.model || !obj.timestamp) continue;
10303
- if (obj.id) {
10304
- if (seenIds.has(obj.id)) continue;
10305
- seenIds.add(obj.id);
10306
- }
10307
- const price = geminiPriceFor(obj.model);
10308
- if (!price) continue;
10309
- const inp = obj.tokens.input ?? 0;
10310
- const out = obj.tokens.output ?? 0;
10311
- const cached = Math.min(obj.tokens.cached ?? 0, inp);
10312
- const fresh = Math.max(0, inp - cached);
10313
- const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
10314
- const date = obj.timestamp.slice(0, 10);
10315
- const model = normalizeModel(obj.model);
10316
- const key = `${date}::${model}`;
10317
- const prev = byKey.get(key);
10318
- if (prev) {
10319
- prev.costUSD += cost;
10320
- prev.inputTokens += fresh;
10321
- prev.outputTokens += out;
10322
- prev.cacheReadTokens += cached;
10323
- } else {
10324
- byKey.set(key, {
10325
- date,
10326
- model,
10327
- workingDir: project,
10328
- runId,
10329
- costUSD: cost,
10330
- inputTokens: fresh,
10331
- outputTokens: out,
10332
- cacheReadTokens: cached,
10333
- cacheWriteTokens: 0
10334
- });
10381
+ history.push(record);
10382
+ if (history.length > cap) {
10383
+ history = history.slice(history.length - cap);
10335
10384
  }
10385
+ import_fs18.default.writeFileSync(filePath, JSON.stringify(history, null, 2));
10386
+ } catch (err2) {
10387
+ process.stderr.write(
10388
+ `[node9] Warning: could not write scan-history.json: ${err2.message}
10389
+ `
10390
+ );
10336
10391
  }
10337
- if (runId) for (const e of byKey.values()) e.runId = runId;
10338
- return [...byKey.values()];
10339
10392
  }
10340
- var import_fs18, import_os17, import_path20, GEMINI_FALLBACK_MODELS, geminiSource;
10341
- var init_cost_gemini = __esm({
10342
- "src/cost-gemini.ts"() {
10393
+ function computeScanDelta(current, previous, now = Date.now()) {
10394
+ if (!previous) return null;
10395
+ const prevMs = Date.parse(previous.timestamp);
10396
+ if (Number.isNaN(prevMs)) return null;
10397
+ const scoreDelta = current.score - previous.score;
10398
+ const daysAgo = Math.max(0, Math.floor((now - prevMs) / 864e5));
10399
+ if (scoreDelta === 0 && daysAgo === 0) return null;
10400
+ return { scoreDelta, daysAgo };
10401
+ }
10402
+ function isValidRecord(x) {
10403
+ if (typeof x !== "object" || x === null) return false;
10404
+ const r = x;
10405
+ 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";
10406
+ }
10407
+ var import_fs18, import_path20, import_os17, SCAN_HISTORY_CAP;
10408
+ var init_scan_history = __esm({
10409
+ "src/cli/render/scan-history.ts"() {
10343
10410
  "use strict";
10344
10411
  import_fs18 = __toESM(require("fs"));
10345
- import_os17 = __toESM(require("os"));
10346
10412
  import_path20 = __toESM(require("path"));
10347
- init_litellm();
10348
- GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
10349
- geminiSource = {
10350
- id: "gemini",
10351
- available() {
10352
- try {
10353
- return import_fs18.default.existsSync(geminiTmpDir());
10354
- } catch {
10355
- return false;
10356
- }
10357
- },
10358
- collect(sinceMs) {
10359
- const combined = /* @__PURE__ */ new Map();
10360
- for (const { file, project } of listGeminiSessionFiles(geminiTmpDir())) {
10361
- try {
10362
- if (sinceMs !== void 0 && import_fs18.default.statSync(file).mtimeMs < sinceMs) continue;
10363
- } catch {
10364
- continue;
10365
- }
10366
- let content;
10367
- try {
10368
- content = import_fs18.default.readFileSync(file, "utf8");
10369
- } catch {
10370
- continue;
10371
- }
10372
- for (const e of parseGeminiSession(content.split("\n"), project)) {
10373
- const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
10374
- const prev = combined.get(key);
10375
- if (prev) {
10376
- prev.costUSD += e.costUSD;
10377
- prev.inputTokens += e.inputTokens;
10378
- prev.outputTokens += e.outputTokens;
10379
- prev.cacheReadTokens += e.cacheReadTokens;
10380
- prev.cacheWriteTokens += e.cacheWriteTokens;
10381
- } else {
10382
- combined.set(key, { ...e });
10383
- }
10384
- }
10385
- }
10386
- return [...combined.values()];
10387
- }
10388
- };
10413
+ import_os17 = __toESM(require("os"));
10414
+ SCAN_HISTORY_CAP = 30;
10389
10415
  }
10390
10416
  });
10391
10417
 
@@ -11424,19 +11450,15 @@ var init_scan_upload_history = __esm({
11424
11450
 
11425
11451
  // src/cli/commands/scan.ts
11426
11452
  function claudeModelPrice(model) {
11427
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
11428
- for (const [key, p] of Object.entries(CLAUDE_PRICING)) {
11429
- if (base === key || base.startsWith(key)) return p;
11430
- }
11431
- return null;
11453
+ const t = pricingFor(model);
11454
+ if (!t) return null;
11455
+ const [i, o, cw, cr] = t;
11456
+ return { i, o, cw, cr };
11432
11457
  }
11433
11458
  function geminiModelPrice(model) {
11434
- const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
11435
- for (const [key, p] of Object.entries(GEMINI_PRICING)) {
11436
- if (base === key || base.startsWith(key)) return p;
11437
- }
11438
- if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
11439
- return null;
11459
+ const p = geminiPriceFor(model);
11460
+ if (!p) return null;
11461
+ return { i: p.input, o: p.output, cr: p.cacheRead };
11440
11462
  }
11441
11463
  function isNode9SelfOutput(text) {
11442
11464
  let hits = 0;
@@ -12082,14 +12104,17 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
12082
12104
  if (!import_fs23.default.existsSync(chatsDir)) continue;
12083
12105
  let chatFiles;
12084
12106
  try {
12085
- chatFiles = import_fs23.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
12107
+ chatFiles = import_fs23.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
12086
12108
  } catch {
12087
12109
  continue;
12088
12110
  }
12111
+ const seenSessions = /* @__PURE__ */ new Set();
12089
12112
  for (const chatFile of chatFiles) {
12113
+ const sessionId = chatFile.replace(/\.jsonl?$/, "");
12114
+ if (seenSessions.has(sessionId)) continue;
12115
+ seenSessions.add(sessionId);
12090
12116
  result.filesScanned++;
12091
12117
  onProgress?.(result.filesScanned);
12092
- const sessionId = chatFile.replace(/\.json$/, "");
12093
12118
  let raw;
12094
12119
  try {
12095
12120
  raw = import_fs23.default.readFileSync(import_path25.default.join(chatsDir, chatFile), "utf-8");
@@ -12099,7 +12124,18 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
12099
12124
  const sessionCalls = [];
12100
12125
  let session;
12101
12126
  try {
12102
- session = JSON.parse(raw);
12127
+ if (chatFile.endsWith(".jsonl")) {
12128
+ const messages = raw.split("\n").filter((l) => l.trim()).map((l) => {
12129
+ try {
12130
+ return JSON.parse(l);
12131
+ } catch {
12132
+ return null;
12133
+ }
12134
+ }).filter((m) => m !== null);
12135
+ session = { messages };
12136
+ } else {
12137
+ session = JSON.parse(raw);
12138
+ }
12103
12139
  } catch {
12104
12140
  continue;
12105
12141
  }
@@ -12709,6 +12745,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12709
12745
  let lastTotalInput = 0;
12710
12746
  let lastTotalCached = 0;
12711
12747
  let lastTotalOutput = 0;
12748
+ let model = "";
12712
12749
  for (const line of lines) {
12713
12750
  if (!line.trim()) continue;
12714
12751
  onLine?.();
@@ -12726,6 +12763,10 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12726
12763
  projLabel = stripTerminalEscapes(cwd.replace(import_os22.default.homedir(), "~")).slice(0, 40);
12727
12764
  continue;
12728
12765
  }
12766
+ if (entry.type === "turn_context" && typeof payload["model"] === "string") {
12767
+ model = payload["model"];
12768
+ continue;
12769
+ }
12729
12770
  if (entry.type === "event_msg" && payload["type"] === "token_count") {
12730
12771
  const info = payload["info"];
12731
12772
  const usage = info?.["total_token_usage"] ?? {};
@@ -12869,8 +12910,11 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12869
12910
  }
12870
12911
  }
12871
12912
  }
12872
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
12873
- result.totalCostUSD += nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
12913
+ result.totalCostUSD += codexSessionCost(model, {
12914
+ input: lastTotalInput,
12915
+ cached: lastTotalCached,
12916
+ output: lastTotalOutput
12917
+ });
12874
12918
  result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "codex"));
12875
12919
  }
12876
12920
  return result;
@@ -13917,7 +13961,7 @@ function registerScanCommand(program2) {
13917
13961
  }
13918
13962
  );
13919
13963
  }
13920
- var import_chalk5, import_fs23, import_path25, import_os22, import_string_width2, 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;
13964
+ var import_chalk5, import_fs23, import_path25, import_os22, import_string_width2, 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;
13921
13965
  var init_scan = __esm({
13922
13966
  "src/cli/commands/scan.ts"() {
13923
13967
  "use strict";
@@ -13930,6 +13974,9 @@ var init_scan = __esm({
13930
13974
  init_policy();
13931
13975
  init_dist();
13932
13976
  init_dlp();
13977
+ init_litellm();
13978
+ init_cost_gemini();
13979
+ init_cost_codex();
13933
13980
  init_hook_payload();
13934
13981
  init_dist();
13935
13982
  init_scan_summary();
@@ -13940,26 +13987,6 @@ var init_scan = __esm({
13940
13987
  import_string_width2 = __toESM(require("string-width"));
13941
13988
  init_scan_json();
13942
13989
  init_scan_history();
13943
- CLAUDE_PRICING = {
13944
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13945
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13946
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
13947
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13948
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13949
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13950
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13951
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13952
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
13953
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
13954
- };
13955
- GEMINI_PRICING = {
13956
- "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
13957
- "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
13958
- "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
13959
- "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
13960
- "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
13961
- "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
13962
- };
13963
13990
  CODE_EXTENSIONS = /* @__PURE__ */ new Set([
13964
13991
  ".ts",
13965
13992
  ".tsx",
@@ -20140,6 +20167,7 @@ var import_os36 = __toESM(require("os"));
20140
20167
  var import_path41 = __toESM(require("path"));
20141
20168
  init_costSync();
20142
20169
  init_litellm();
20170
+ init_cost_codex();
20143
20171
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
20144
20172
  function buildTestTimestamps(allEntries) {
20145
20173
  const testTs = /* @__PURE__ */ new Set();
@@ -20236,24 +20264,11 @@ function isAllow(decision) {
20236
20264
  function isDlp(checkedBy) {
20237
20265
  return !!checkedBy?.includes("dlp");
20238
20266
  }
20239
- var CLAUDE_PRICING2 = {
20240
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
20241
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
20242
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
20243
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20244
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20245
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20246
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20247
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20248
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
20249
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
20250
- };
20251
20267
  function claudeModelPrice2(model) {
20252
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
20253
- for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
20254
- if (base === key || base.startsWith(key + "-") || base.startsWith(key)) return p;
20255
- }
20256
- return null;
20268
+ const t = pricingFor(model);
20269
+ if (!t) return null;
20270
+ const [i, o, cw, cr] = t;
20271
+ return { i, o, cw, cr };
20257
20272
  }
20258
20273
  function emptyClaudeCostAccumulator() {
20259
20274
  return {
@@ -20368,6 +20383,7 @@ function processCodexCostFile(filePath, start, end, acc) {
20368
20383
  return;
20369
20384
  }
20370
20385
  let sessionStart2 = "";
20386
+ let model = "";
20371
20387
  let lastTotalInput = 0;
20372
20388
  let lastTotalCached = 0;
20373
20389
  let lastTotalOutput = 0;
@@ -20385,6 +20401,10 @@ function processCodexCostFile(filePath, start, end, acc) {
20385
20401
  sessionStart2 = String(p["timestamp"] ?? "");
20386
20402
  continue;
20387
20403
  }
20404
+ if (entry.type === "turn_context" && typeof p["model"] === "string") {
20405
+ model = p["model"];
20406
+ continue;
20407
+ }
20388
20408
  if (entry.type === "event_msg" && p["type"] === "token_count") {
20389
20409
  const info = p["info"] ?? {};
20390
20410
  const usage = info["total_token_usage"] ?? {};
@@ -20399,12 +20419,17 @@ function processCodexCostFile(filePath, start, end, acc) {
20399
20419
  if (!sessionStart2) return;
20400
20420
  const ts = new Date(sessionStart2);
20401
20421
  if (ts < start || ts > end) return;
20402
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
20403
- const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
20422
+ const cost = codexSessionCost(model, {
20423
+ input: lastTotalInput,
20424
+ cached: lastTotalCached,
20425
+ output: lastTotalOutput
20426
+ });
20404
20427
  acc.total += cost;
20405
20428
  acc.toolCalls += sessionToolCalls;
20406
20429
  const dateKey = sessionStart2.slice(0, 10);
20407
20430
  acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
20431
+ const normModel = normalizeModel(model || "gpt-5");
20432
+ acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
20408
20433
  }
20409
20434
  function listCodexSessionFiles2(sessionsBase) {
20410
20435
  const jsonlFiles = [];
@@ -20442,13 +20467,25 @@ function listCodexSessionFiles2(sessionsBase) {
20442
20467
  }
20443
20468
  return jsonlFiles;
20444
20469
  }
20470
+ function mergeByModel(...maps) {
20471
+ const out = /* @__PURE__ */ new Map();
20472
+ for (const m of maps) {
20473
+ for (const [k, v] of m) out.set(k, (out.get(k) ?? 0) + v);
20474
+ }
20475
+ return out;
20476
+ }
20445
20477
  function loadCodexCost(start, end, sessionsBase) {
20446
- const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
20478
+ const acc = {
20479
+ total: 0,
20480
+ toolCalls: 0,
20481
+ byDay: /* @__PURE__ */ new Map(),
20482
+ byModel: /* @__PURE__ */ new Map()
20483
+ };
20447
20484
  const files = listCodexSessionFiles2(sessionsBase);
20448
20485
  for (const filePath of files) {
20449
20486
  processCodexCostFile(filePath, start, end, acc);
20450
20487
  }
20451
- return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
20488
+ return { total: acc.total, byDay: acc.byDay, byModel: acc.byModel, toolCalls: acc.toolCalls };
20452
20489
  }
20453
20490
  var GEMINI_FALLBACK_MODELS2 = ["gemini-2.5-flash", "gemini-2.0-flash"];
20454
20491
  function geminiPriceFor2(model) {
@@ -20737,7 +20774,7 @@ function aggregateReportFromAudit(period, opts = {}) {
20737
20774
  cacheWriteTokens: claudeCost.cacheWriteTokens,
20738
20775
  cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
20739
20776
  byDay: claudeCost.byDay,
20740
- byModel: claudeCost.byModel,
20777
+ byModel: mergeByModel(claudeCost.byModel, codexCost.byModel),
20741
20778
  byProject: claudeCost.byProject
20742
20779
  },
20743
20780
  toolMap,
@@ -23510,40 +23547,19 @@ var import_fs45 = __toESM(require("fs"));
23510
23547
  var import_path46 = __toESM(require("path"));
23511
23548
  var import_os40 = __toESM(require("os"));
23512
23549
  init_scan_summary();
23513
- var CLAUDE_PRICING3 = {
23514
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
23515
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
23516
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
23517
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23518
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23519
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23520
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23521
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23522
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
23523
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
23524
- };
23550
+ init_litellm();
23551
+ init_cost_gemini();
23552
+ init_cost_codex();
23525
23553
  function modelPrice(model) {
23526
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
23527
- for (const [key, p] of Object.entries(CLAUDE_PRICING3)) {
23528
- if (base === key || base.startsWith(key)) return p;
23529
- }
23530
- return null;
23554
+ const t = pricingFor(model);
23555
+ if (!t) return null;
23556
+ const [i, o, cw, cr] = t;
23557
+ return { i, o, cw, cr };
23531
23558
  }
23532
- var GEMINI_PRICING2 = {
23533
- "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
23534
- "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
23535
- "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
23536
- "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
23537
- "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
23538
- "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
23539
- };
23540
23559
  function geminiModelPrice2(model) {
23541
- const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
23542
- for (const [key, p] of Object.entries(GEMINI_PRICING2)) {
23543
- if (base === key || base.startsWith(key)) return p;
23544
- }
23545
- if (base.includes("flash")) return GEMINI_PRICING2["gemini-2.0-flash"];
23546
- return null;
23560
+ const p = geminiPriceFor(model);
23561
+ if (!p) return null;
23562
+ return { i: p.input, o: p.output, cr: p.cacheRead };
23547
23563
  }
23548
23564
  function encodeProjectPath(projectPath) {
23549
23565
  return projectPath.replace(/\//g, "-");
@@ -23836,6 +23852,7 @@ function buildCodexSessions(days, allAuditEntries) {
23836
23852
  let lastTotalInput = 0;
23837
23853
  let lastTotalCached = 0;
23838
23854
  let lastTotalOutput = 0;
23855
+ let model = "";
23839
23856
  for (const line of lines) {
23840
23857
  if (!line.trim()) continue;
23841
23858
  let entry;
@@ -23851,6 +23868,10 @@ function buildCodexSessions(days, allAuditEntries) {
23851
23868
  cwd = String(p["cwd"] ?? "");
23852
23869
  continue;
23853
23870
  }
23871
+ if (entry.type === "turn_context" && typeof p["model"] === "string") {
23872
+ model = p["model"];
23873
+ continue;
23874
+ }
23854
23875
  if (entry.type === "event_msg" && p["type"] === "user_message" && !firstPrompt) {
23855
23876
  firstPrompt = String(p["message"] ?? "");
23856
23877
  continue;
@@ -23877,8 +23898,11 @@ function buildCodexSessions(days, allAuditEntries) {
23877
23898
  }
23878
23899
  if (!sessionId || !startTime) continue;
23879
23900
  if (cutoff && new Date(startTime) < cutoff) continue;
23880
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
23881
- const costUSD = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
23901
+ const costUSD = codexSessionCost(model, {
23902
+ input: lastTotalInput,
23903
+ cached: lastTotalCached,
23904
+ output: lastTotalOutput
23905
+ });
23882
23906
  const windowEnd = new Date(
23883
23907
  Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
23884
23908
  ).toISOString();
@@ -23902,11 +23926,10 @@ function buildCodexSessions(days, allAuditEntries) {
23902
23926
  }
23903
23927
  function buildSessions(days, historyPath) {
23904
23928
  const hPath = historyPath ?? import_path46.default.join(import_os40.default.homedir(), ".claude", "history.jsonl");
23905
- let historyRaw;
23929
+ let historyRaw = "";
23906
23930
  try {
23907
23931
  historyRaw = import_fs45.default.readFileSync(hPath, "utf-8");
23908
23932
  } catch {
23909
- return [];
23910
23933
  }
23911
23934
  const cutoff = days !== null ? (() => {
23912
23935
  const d = /* @__PURE__ */ new Date();
@@ -24197,12 +24220,6 @@ function registerSessionsCommand(program2) {
24197
24220
  console.log("");
24198
24221
  console.log(import_chalk24.default.cyan.bold("\u{1F4CB} node9 sessions") + import_chalk24.default.dim(" \u2014 what your AI agent did"));
24199
24222
  console.log("");
24200
- const historyPath = import_path46.default.join(import_os40.default.homedir(), ".claude", "history.jsonl");
24201
- if (!import_fs45.default.existsSync(historyPath)) {
24202
- console.log(import_chalk24.default.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
24203
- console.log(import_chalk24.default.gray(" Install Claude Code, run a few sessions, then try again.\n"));
24204
- return;
24205
- }
24206
24223
  const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
24207
24224
  const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
24208
24225
  console.log(import_chalk24.default.dim(" " + rangeLabel));