@node9/proxy 1.33.0 → 1.35.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 +1257 -1097
  2. package/dist/cli.mjs +1255 -1094
  3. package/dist/dashboard.mjs +335 -174
  4. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -5869,6 +5869,57 @@ function validateApiUrl(raw) {
5869
5869
  }
5870
5870
  return null;
5871
5871
  }
5872
+ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
5873
+ const validated = validateApiUrl(creds.apiUrl);
5874
+ if (!validated) {
5875
+ try {
5876
+ fs10.appendFileSync(
5877
+ HOOK_DEBUG_LOG,
5878
+ `[audit] refused to send: invalid apiUrl scheme/host (got "${String(creds.apiUrl).slice(0, 200)}")
5879
+ `
5880
+ );
5881
+ } catch {
5882
+ }
5883
+ return Promise.resolve();
5884
+ }
5885
+ const safeArgs = containsSensitiveArgs ? { tool: toolName, redacted: true } : args;
5886
+ const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
5887
+ const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
5888
+ const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
5889
+ const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
5890
+ Object.entries(riskMetadata).filter(
5891
+ ([, v]) => typeof v === "string" && v.length > 0 || typeof v === "number" && Number.isFinite(v)
5892
+ )
5893
+ ) : void 0;
5894
+ const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
5895
+ return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
5896
+ method: "POST",
5897
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
5898
+ body: JSON.stringify({
5899
+ toolName,
5900
+ args: safeArgs,
5901
+ checkedBy: safeCheckedBy,
5902
+ ...dlpInfo && { dlpPattern, dlpSample },
5903
+ ...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
5904
+ // session_id (Claude Code + Gemini CLI) groups all audit rows from one
5905
+ // agent run; transcript_path is the authoritative pointer to the
5906
+ // session log (survives Gemini resume drift). Both optional —
5907
+ // unsupported agents (MCP-mediated) leave them undefined.
5908
+ ...meta?.sessionId && { runId: meta.sessionId },
5909
+ ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
5910
+ context: {
5911
+ agent: meta?.agent,
5912
+ mcpServer: meta?.mcpServer,
5913
+ hostname: os9.hostname(),
5914
+ cwd: process.cwd(),
5915
+ platform: os9.platform()
5916
+ }
5917
+ }),
5918
+ signal: AbortSignal.timeout(5e3)
5919
+ }).then(() => {
5920
+ }).catch(() => {
5921
+ });
5922
+ }
5872
5923
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
5873
5924
  const controller = new AbortController();
5874
5925
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -5992,10 +6043,44 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
5992
6043
  );
5993
6044
  }
5994
6045
  }
6046
+ var DLP_SAMPLE_MAX_LEN, DLP_PATTERN_MAX_LEN, KNOWN_CHECKED_BY;
5995
6047
  var init_cloud = __esm({
5996
6048
  "src/auth/cloud.ts"() {
5997
6049
  "use strict";
5998
6050
  init_audit();
6051
+ DLP_SAMPLE_MAX_LEN = 200;
6052
+ DLP_PATTERN_MAX_LEN = 100;
6053
+ KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
6054
+ "dlp-block",
6055
+ "observe-mode-dlp-would-block",
6056
+ "dlp-review-flagged",
6057
+ "loop-detected",
6058
+ "audit-mode",
6059
+ "local-policy",
6060
+ "smart-rule-block",
6061
+ // Smart-rule block was downgraded to review because the daemon was
6062
+ // running and we're not in CI. The block attempt is still recorded;
6063
+ // the user got a popup. Distinct from 'smart-rule-block' so the
6064
+ // dashboard can show "block rule overridden" separately from a hard
6065
+ // block that fired with no human in the loop.
6066
+ "smart-rule-block-override",
6067
+ "persistent",
6068
+ "trust",
6069
+ "observe-mode",
6070
+ "observe-mode-would-block",
6071
+ // MCP supply-chain: the gateway pinned a server's tool definitions and they
6072
+ // changed since (possible tool poisoning / rug pull). Emitted as a synthetic
6073
+ // audit row so the SaaS surfaces it as a blocked event. The firewall maps
6074
+ // this checkedBy to AUTO_BLOCKED. See doc/roadmap/active/saas-value-first.md
6075
+ // (workstream B-Tier2).
6076
+ "mcp-pin-mismatch",
6077
+ // MCP visibility (B-Tier2, informational — NOT blocks): the gateway
6078
+ // discovered a server's tool inventory (mcp-discovered) or saw an oversized
6079
+ // tool response that bloats the context window (mcp-large-response). Stored
6080
+ // AUTO_ALLOWED; carries mcpToolCount / mcpResponseBytes in riskMetadata.
6081
+ "mcp-discovered",
6082
+ "mcp-large-response"
6083
+ ]);
5999
6084
  }
6000
6085
  });
6001
6086
 
@@ -9252,1113 +9337,1139 @@ var init_setup = __esm({
9252
9337
  }
9253
9338
  });
9254
9339
 
9255
- // src/utils/hook-payload.ts
9256
- function extractToolName(payload, defaultValue = "") {
9257
- return payload.tool_name ?? payload.name ?? payload.toolCall?.name ?? defaultValue;
9340
+ // src/pricing/litellm.ts
9341
+ import fs14 from "fs";
9342
+ import path16 from "path";
9343
+ import os13 from "os";
9344
+ function normalizeModel(raw) {
9345
+ return raw.replace(/-\d{8}$/, "").toLowerCase();
9258
9346
  }
9259
- function extractToolInput(payload) {
9260
- return payload.tool_input ?? payload.args ?? payload.toolCall?.args ?? {};
9347
+ function readCache() {
9348
+ try {
9349
+ const raw = JSON.parse(fs14.readFileSync(CACHE_FILE(), "utf-8"));
9350
+ if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
9351
+ return null;
9352
+ }
9353
+ const ageMs = Date.now() - new Date(raw.fetchedAt).getTime();
9354
+ if (ageMs < 0 || ageMs > TTL_MS) return null;
9355
+ return raw.prices;
9356
+ } catch {
9357
+ return null;
9358
+ }
9261
9359
  }
9262
- function canonicalToolName(name) {
9263
- switch (name) {
9264
- // Hermes Agent
9265
- case "terminal":
9266
- return "Bash";
9267
- case "write_file":
9268
- return "Write";
9269
- case "patch":
9270
- return "Edit";
9271
- case "read_file":
9272
- return "Read";
9273
- case "search_files":
9274
- return "Grep";
9275
- // Antigravity (agy) — shell tool renamed from Gemini's run_shell_command
9276
- case "run_command":
9277
- return "Bash";
9278
- default:
9279
- return name;
9360
+ function writeCache(prices) {
9361
+ try {
9362
+ const target = CACHE_FILE();
9363
+ const dir = path16.dirname(target);
9364
+ if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
9365
+ const tmp = target + ".tmp";
9366
+ const body = {
9367
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
9368
+ prices
9369
+ };
9370
+ fs14.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
9371
+ fs14.renameSync(tmp, target);
9372
+ } catch (err2) {
9373
+ try {
9374
+ fs14.appendFileSync(
9375
+ HOOK_DEBUG_LOG,
9376
+ `[pricing] cache write failed: ${err2.message}
9377
+ `
9378
+ );
9379
+ } catch {
9380
+ }
9280
9381
  }
9281
9382
  }
9282
- function agentLabelFromFlag(flag) {
9283
- if (typeof flag !== "string") return void 0;
9284
- switch (flag.toLowerCase()) {
9285
- case "antigravity":
9286
- case "agy":
9287
- return "Antigravity";
9288
- case "copilot":
9289
- return "GitHub Copilot";
9290
- default:
9291
- return void 0;
9383
+ function tupleFromLiteLLM(entry) {
9384
+ if (!entry || typeof entry !== "object") return null;
9385
+ const e = entry;
9386
+ const num3 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
9387
+ const inCost = num3(e.input_cost_per_token);
9388
+ const outCost = num3(e.output_cost_per_token);
9389
+ if (inCost === 0 && outCost === 0) return null;
9390
+ return [
9391
+ inCost,
9392
+ outCost,
9393
+ num3(e.cache_creation_input_token_cost),
9394
+ num3(e.cache_read_input_token_cost)
9395
+ ];
9396
+ }
9397
+ async function fetchLiteLLMPricing() {
9398
+ try {
9399
+ const res = await fetch(LITELLM_URL, {
9400
+ signal: AbortSignal.timeout(15e3)
9401
+ });
9402
+ if (!res.ok) return null;
9403
+ const json = await res.json();
9404
+ if (!json || typeof json !== "object") return null;
9405
+ const out = {};
9406
+ for (const [key, value] of Object.entries(json)) {
9407
+ const tuple = tupleFromLiteLLM(value);
9408
+ if (tuple) out[key.toLowerCase()] = tuple;
9409
+ }
9410
+ if (Object.keys(out).length < 10) {
9411
+ return null;
9412
+ }
9413
+ return out;
9414
+ } catch {
9415
+ return null;
9292
9416
  }
9293
9417
  }
9294
- function canonicalToolInput(rawToolName, input) {
9295
- if (rawToolName !== "run_command") return input;
9296
- if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
9297
- const args = input;
9298
- if (typeof args.CommandLine !== "string") return input;
9299
- const { CommandLine, Cwd, ...rest } = args;
9300
- const canonical = { ...rest, command: CommandLine };
9301
- if (typeof Cwd === "string" && Cwd.length > 0) canonical.cwd = Cwd;
9302
- return canonical;
9418
+ async function ensurePricingLoaded() {
9419
+ if (memCache !== null && Date.now() - memCacheAt < TTL_MS) return;
9420
+ const fromDisk = readCache();
9421
+ if (fromDisk && Object.keys(fromDisk).length > 0) {
9422
+ memCache = fromDisk;
9423
+ memCacheAt = Date.now();
9424
+ lookupCache.clear();
9425
+ return;
9426
+ }
9427
+ const fetched = await fetchLiteLLMPricing();
9428
+ if (fetched && Object.keys(fetched).length > 0) {
9429
+ memCache = fetched;
9430
+ memCacheAt = Date.now();
9431
+ writeCache(fetched);
9432
+ lookupCache.clear();
9433
+ return;
9434
+ }
9435
+ memCache = { ...BUNDLED_PRICING };
9436
+ memCacheAt = Date.now();
9437
+ lookupCache.clear();
9303
9438
  }
9304
- var init_hook_payload = __esm({
9305
- "src/utils/hook-payload.ts"() {
9439
+ function pricingFor(model) {
9440
+ const norm = normalizeModel(model);
9441
+ const cached = lookupCache.get(norm);
9442
+ if (cached !== void 0) return cached;
9443
+ if (memCache === null && !diskChecked) {
9444
+ diskChecked = true;
9445
+ const disk = readCache();
9446
+ if (disk && Object.keys(disk).length > 0) {
9447
+ memCache = disk;
9448
+ memCacheAt = Date.now();
9449
+ }
9450
+ }
9451
+ const sources = [];
9452
+ if (memCache) sources.push(memCache);
9453
+ sources.push(BUNDLED_PRICING);
9454
+ let resolved = null;
9455
+ for (const source of sources) {
9456
+ const exact = source[norm];
9457
+ if (exact) {
9458
+ resolved = exact;
9459
+ break;
9460
+ }
9461
+ let best = null;
9462
+ for (const key of Object.keys(source)) {
9463
+ if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
9464
+ best = key;
9465
+ }
9466
+ }
9467
+ if (best) {
9468
+ resolved = source[best];
9469
+ break;
9470
+ }
9471
+ }
9472
+ lookupCache.set(norm, resolved);
9473
+ return resolved;
9474
+ }
9475
+ var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, diskChecked, lookupCache;
9476
+ var init_litellm = __esm({
9477
+ "src/pricing/litellm.ts"() {
9306
9478
  "use strict";
9479
+ init_audit();
9480
+ LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
9481
+ BUNDLED_PRICING = {
9482
+ // Anthropic
9483
+ "claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
9484
+ "claude-opus-4-1": [5e-6, 25e-6, 625e-8, 5e-7],
9485
+ "claude-opus-4-5": [5e-6, 25e-6, 625e-8, 5e-7],
9486
+ "claude-opus-4-6": [5e-6, 25e-6, 625e-8, 5e-7],
9487
+ "claude-opus-4-7": [5e-6, 25e-6, 625e-8, 5e-7],
9488
+ "claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
9489
+ "claude-sonnet-4-5": [3e-6, 15e-6, 375e-8, 3e-7],
9490
+ "claude-sonnet-4-6": [3e-6, 15e-6, 375e-8, 3e-7],
9491
+ "claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
9492
+ "claude-haiku-4-5": [8e-7, 4e-6, 1e-6, 8e-8],
9493
+ "claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
9494
+ "claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
9495
+ "claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
9496
+ "claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
9497
+ // OpenAI. gpt-5 family + o-series copied from the live LiteLLM table
9498
+ // (verified 2026-06-14) — the bundled gpt-5 was stale at $10/$30 vs the real
9499
+ // $1.25/$10, and Codex models (gpt-5-codex etc.) were absent, so the offline
9500
+ // fallback mispriced every Codex session. See cost-codex.codexPriceFor.
9501
+ "gpt-4o": [5e-6, 15e-6, 0, 25e-7],
9502
+ "gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
9503
+ "gpt-5": [125e-8, 1e-5, 0, 125e-9],
9504
+ "gpt-5-codex": [125e-8, 1e-5, 0, 125e-9],
9505
+ "gpt-5-mini": [25e-8, 2e-6, 0, 25e-9],
9506
+ o3: [2e-6, 8e-6, 0, 5e-7],
9507
+ "o4-mini": [11e-7, 44e-7, 0, 275e-9],
9508
+ // Google. Values copied from the live LiteLLM table (verified 2026-06-14)
9509
+ // so the bundled fallback prices the current Gemini tiers correctly offline
9510
+ // — the local cost readers were carrying a stale hardcoded copy where
9511
+ // gemini-2.5-flash read $0.15/$0.60 vs the real $0.30/$2.50 (~4× under on
9512
+ // output). See cost-gemini.geminiPriceFor (the single Gemini price source).
9513
+ "gemini-2.5-pro": [125e-8, 1e-5, 0, 125e-9],
9514
+ "gemini-2.5-flash": [3e-7, 25e-7, 0, 3e-8],
9515
+ "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
9516
+ "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
9517
+ };
9518
+ CACHE_FILE = () => path16.join(os13.homedir(), ".node9", "model-pricing.json");
9519
+ TTL_MS = 24 * 60 * 60 * 1e3;
9520
+ memCache = null;
9521
+ memCacheAt = 0;
9522
+ diskChecked = false;
9523
+ lookupCache = /* @__PURE__ */ new Map();
9307
9524
  }
9308
9525
  });
9309
9526
 
9310
- // src/scan-summary.ts
9311
- function agentDisplayName(agent) {
9312
- return AGENT_LONG[agent] ?? "Claude Code";
9527
+ // src/cost-gemini.ts
9528
+ import fs15 from "fs";
9529
+ import os14 from "os";
9530
+ import path17 from "path";
9531
+ function geminiTmpDir() {
9532
+ return path17.join(os14.homedir(), ".gemini", "tmp");
9313
9533
  }
9314
- function agentBadgeText(agent, width = 10) {
9315
- return `[${AGENT_SHORT[agent] ?? "Claude"}]`.padEnd(width);
9534
+ function geminiPriceFor(model) {
9535
+ let tuple = pricingFor(model);
9536
+ if (!tuple && /^gemini-/i.test(model)) {
9537
+ for (const proxy of GEMINI_FALLBACK_MODELS) {
9538
+ tuple = pricingFor(proxy);
9539
+ if (tuple) break;
9540
+ }
9541
+ }
9542
+ if (!tuple) return null;
9543
+ return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
9316
9544
  }
9317
- function agentColorName(agent) {
9318
- switch (agent) {
9319
- case "gemini":
9320
- return "blue";
9321
- case "codex":
9322
- return "magenta";
9323
- case "antigravity":
9324
- return "yellow";
9325
- case "copilot":
9326
- return "green";
9327
- case "shell":
9328
- return "yellow";
9329
- default:
9330
- return "cyan";
9545
+ function safeReaddir(dir) {
9546
+ try {
9547
+ return fs15.readdirSync(dir);
9548
+ } catch {
9549
+ return [];
9331
9550
  }
9332
9551
  }
9333
- function buildScanSummary(agents) {
9334
- const stats = {
9335
- sessions: 0,
9336
- totalToolCalls: 0,
9337
- bashCalls: 0,
9338
- totalCostUSD: 0,
9339
- firstDate: null,
9340
- lastDate: null
9341
- };
9342
- for (const a of agents) {
9343
- stats.sessions += a.scan.sessions;
9344
- stats.totalToolCalls += a.scan.totalToolCalls;
9345
- stats.bashCalls += a.scan.bashCalls;
9346
- stats.totalCostUSD += a.scan.totalCostUSD;
9347
- if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
9348
- stats.firstDate = a.scan.firstDate;
9349
- }
9350
- if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
9351
- stats.lastDate = a.scan.lastDate;
9352
- }
9353
- }
9354
- const allFindings = agents.flatMap((a) => a.scan.findings);
9355
- const allLeaks = agents.flatMap(
9356
- (a) => a.scan.dlpFindings.map((f) => ({
9357
- patternName: f.patternName,
9358
- redactedSample: f.redactedSample,
9359
- toolName: f.toolName,
9360
- timestamp: f.timestamp,
9361
- project: f.project,
9362
- sessionId: f.sessionId,
9363
- agent: f.agent
9364
- }))
9365
- );
9366
- const allLoops = agents.flatMap(
9367
- (a) => a.scan.loopFindings.map((f) => ({
9368
- toolName: f.toolName,
9369
- commandPreview: f.commandPreview,
9370
- count: f.count,
9371
- timestamp: f.timestamp,
9372
- project: f.project,
9373
- sessionId: f.sessionId,
9374
- agent: f.agent,
9375
- kind: f.kind
9376
- }))
9377
- );
9378
- const byVerdict = {
9379
- blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
9380
- supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
9381
- leaks: allLeaks.length,
9382
- loops: allLoops.length
9383
- };
9384
- const byAgent = agents.map((a) => ({
9385
- id: a.id,
9386
- label: a.label,
9387
- icon: a.icon,
9388
- sessions: a.scan.sessions,
9389
- findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
9390
- costUSD: a.scan.totalCostUSD
9391
- })).filter((s) => s.sessions > 0 || s.findings > 0);
9392
- const sections = buildSections(allFindings);
9393
- const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
9394
- const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
9395
- return {
9396
- stats,
9397
- byVerdict,
9398
- byAgent,
9399
- sections,
9400
- leaks: allLeaks,
9401
- loops: allLoops,
9402
- loopWastedUSD
9403
- };
9404
- }
9405
- function buildSections(findings) {
9406
- const sectionMap = /* @__PURE__ */ new Map();
9407
- function ensureSection(id, label, subtitle, sourceType, shieldKey) {
9408
- let s = sectionMap.get(id);
9409
- if (!s) {
9410
- s = {
9411
- id,
9412
- label,
9413
- subtitle,
9414
- sourceType,
9415
- shieldKey,
9416
- blockedCount: 0,
9417
- reviewCount: 0,
9418
- rules: []
9419
- };
9420
- sectionMap.set(id, s);
9421
- }
9422
- return s;
9423
- }
9424
- const ruleMap = /* @__PURE__ */ new Map();
9425
- for (const f of findings) {
9426
- const src = f.source;
9427
- const sourceType = src.sourceType;
9428
- const shieldName = src.shieldName;
9429
- const verdict = src.rule.verdict === "block" ? "block" : "review";
9430
- let sectionId;
9431
- let sectionLabel;
9432
- let sectionSubtitle;
9433
- let shieldKey;
9434
- if (sourceType === "default") {
9435
- sectionId = "default";
9436
- sectionLabel = "Default Rules";
9437
- sectionSubtitle = "built-in, always on";
9438
- } else if (sourceType === "shield") {
9439
- sectionId = `shield:${shieldName}`;
9440
- sectionLabel = shieldName;
9441
- sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
9442
- shieldKey = shieldName;
9443
- } else if (shieldName === "cloud") {
9444
- sectionId = "cloud";
9445
- sectionLabel = "Cloud Policy";
9446
- sectionSubtitle = "synced from node9 cloud";
9447
- } else {
9448
- sectionId = "user";
9449
- sectionLabel = "Your Rules";
9450
- sectionSubtitle = "added in node9.config.json";
9451
- }
9452
- const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
9453
- const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
9454
- const ruleKey = sectionId + "::" + ruleDisplayName;
9455
- let rule = ruleMap.get(ruleKey);
9456
- if (!rule) {
9457
- rule = {
9458
- name: ruleDisplayName,
9459
- verdict,
9460
- reason: src.rule.reason ?? "",
9461
- findings: []
9462
- };
9463
- ruleMap.set(ruleKey, rule);
9464
- section.rules.push(rule);
9465
- }
9466
- const cmdPreview = previewCommand(f.input, 120);
9467
- const fullCmd = fullCommandOf(f.input);
9468
- const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
9469
- if (!isDupe) {
9470
- rule.findings.push({
9471
- timestamp: f.timestamp ?? "",
9472
- command: cmdPreview,
9473
- fullCommand: fullCmd,
9474
- project: f.project,
9475
- sessionId: f.sessionId,
9476
- agent: f.agent,
9477
- toolName: f.toolName
9478
- });
9479
- }
9480
- if (verdict === "block") section.blockedCount++;
9481
- else section.reviewCount++;
9482
- }
9483
- const sections = [...sectionMap.values()];
9484
- sections.sort((a, b) => {
9485
- const aTotal = a.blockedCount + a.reviewCount;
9486
- const bTotal = b.blockedCount + b.reviewCount;
9487
- if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
9488
- return bTotal - aTotal;
9489
- });
9490
- for (const s of sections) {
9491
- s.rules.sort((a, b) => {
9492
- const aBlock = a.verdict === "block" ? 1 : 0;
9493
- const bBlock = b.verdict === "block" ? 1 : 0;
9494
- if (bBlock !== aBlock) return bBlock - aBlock;
9495
- return b.findings.length - a.findings.length;
9496
- });
9497
- }
9498
- return sections;
9499
- }
9500
- function previewCommand(input, max) {
9501
- const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9502
- const s = String(raw).replace(/\s+/g, " ").trim();
9503
- return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
9504
- }
9505
- function fullCommandOf(input) {
9506
- const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9507
- return String(raw).replace(/\s+/g, " ").trim();
9508
- }
9509
- var AGENT_SHORT, AGENT_LONG;
9510
- var init_scan_summary = __esm({
9511
- "src/scan-summary.ts"() {
9512
- "use strict";
9513
- init_shields();
9514
- init_dist();
9515
- init_dist();
9516
- AGENT_SHORT = {
9517
- claude: "Claude",
9518
- gemini: "Gemini",
9519
- codex: "Codex",
9520
- antigravity: "Agy",
9521
- copilot: "Copilot",
9522
- shell: "Shell"
9523
- };
9524
- AGENT_LONG = {
9525
- claude: "Claude Code",
9526
- gemini: "Gemini CLI",
9527
- codex: "Codex",
9528
- antigravity: "Antigravity",
9529
- copilot: "GitHub Copilot",
9530
- shell: "Shell"
9531
- };
9532
- }
9533
- });
9534
-
9535
- // src/cli/commands/blast.ts
9536
- import chalk2 from "chalk";
9537
- import fs14 from "fs";
9538
- import path16 from "path";
9539
- import os13 from "os";
9540
- function buildSensitivePaths(home, cwd) {
9541
- return [
9542
- {
9543
- full: path16.join(home, ".ssh", "id_rsa"),
9544
- label: "~/.ssh/id_rsa",
9545
- description: "RSA private key \u2014 grants SSH access to your servers",
9546
- score: 20
9547
- },
9548
- {
9549
- full: path16.join(home, ".ssh", "id_ed25519"),
9550
- label: "~/.ssh/id_ed25519",
9551
- description: "Ed25519 private key \u2014 grants SSH access to your servers",
9552
- score: 20
9553
- },
9554
- {
9555
- full: path16.join(home, ".ssh", "id_ecdsa"),
9556
- label: "~/.ssh/id_ecdsa",
9557
- description: "ECDSA private key \u2014 grants SSH access to your servers",
9558
- score: 20
9559
- },
9560
- {
9561
- full: path16.join(home, ".aws", "credentials"),
9562
- label: "~/.aws/credentials",
9563
- description: "AWS access keys \u2014 full cloud account access",
9564
- score: 20
9565
- },
9566
- {
9567
- full: path16.join(home, ".aws", "config"),
9568
- label: "~/.aws/config",
9569
- description: "AWS configuration \u2014 account and region settings",
9570
- score: 5
9571
- },
9572
- {
9573
- full: path16.join(home, ".config", "gcloud", "credentials.db"),
9574
- label: "~/.config/gcloud/credentials.db",
9575
- description: "Google Cloud credentials",
9576
- score: 15
9577
- },
9578
- {
9579
- full: path16.join(home, ".docker", "config.json"),
9580
- label: "~/.docker/config.json",
9581
- description: "Docker registry auth tokens",
9582
- score: 10
9583
- },
9584
- {
9585
- full: path16.join(home, ".netrc"),
9586
- label: "~/.netrc",
9587
- description: "FTP/HTTP credentials in plain text",
9588
- score: 15
9589
- },
9590
- {
9591
- full: path16.join(home, ".npmrc"),
9592
- label: "~/.npmrc",
9593
- description: "npm auth token \u2014 can publish packages as you",
9594
- score: 10
9595
- },
9596
- {
9597
- full: path16.join(home, ".node9", "credentials.json"),
9598
- label: "~/.node9/credentials.json",
9599
- description: "Node9 cloud API key",
9600
- score: 10
9601
- },
9602
- {
9603
- full: path16.join(cwd, ".env"),
9604
- label: ".env (current folder)",
9605
- description: "App secrets \u2014 database passwords, API keys",
9606
- score: 20
9607
- },
9608
- {
9609
- full: path16.join(cwd, ".env.local"),
9610
- label: ".env.local (current folder)",
9611
- description: "Local overrides \u2014 often contains real credentials",
9612
- score: 15
9613
- },
9614
- {
9615
- full: path16.join(cwd, ".env.production"),
9616
- label: ".env.production (current folder)",
9617
- description: "Production secrets",
9618
- score: 20
9619
- }
9620
- ];
9621
- }
9622
- function isReadable(filePath) {
9552
+ function isDir(p) {
9623
9553
  try {
9624
- fs14.accessSync(filePath, fs14.constants.R_OK);
9625
- return true;
9554
+ return fs15.statSync(p).isDirectory();
9626
9555
  } catch {
9627
9556
  return false;
9628
9557
  }
9629
9558
  }
9630
- function scoreLabel(score) {
9631
- if (score >= 80) return chalk2.green(`${score}/100 Good`);
9632
- if (score >= 50) return chalk2.yellow(`${score}/100 Moderate risk`);
9633
- if (score >= 25) return chalk2.red(`${score}/100 High risk`);
9634
- return chalk2.red.bold(`${score}/100 Critical`);
9635
- }
9636
- function runBlast() {
9637
- const home = os13.homedir();
9638
- const cwd = process.cwd();
9639
- const paths = buildSensitivePaths(home, cwd);
9640
- let scoreDeduction = 0;
9641
- const reachable = [];
9642
- for (const p of paths) {
9643
- if (fs14.existsSync(p.full) && isReadable(p.full)) {
9644
- reachable.push(p);
9645
- scoreDeduction += p.score;
9646
- }
9647
- }
9648
- const envFindings = [];
9649
- for (const [key, value] of Object.entries(process.env)) {
9650
- if (!value) continue;
9651
- const match = scanArgs({ [key]: value });
9652
- if (match) {
9653
- envFindings.push({ key, patternName: match.patternName });
9654
- scoreDeduction += 10;
9559
+ function listGeminiSessionFiles(base) {
9560
+ const out = [];
9561
+ for (const project of safeReaddir(base)) {
9562
+ const chats = path17.join(base, project, "chats");
9563
+ if (!isDir(chats)) continue;
9564
+ for (const f of safeReaddir(chats)) {
9565
+ if (f.startsWith("session-") && f.endsWith(".jsonl")) {
9566
+ out.push({ file: path17.join(chats, f), project });
9567
+ }
9655
9568
  }
9656
9569
  }
9657
- return { reachable, envFindings, score: Math.max(0, 100 - scoreDeduction) };
9570
+ return out;
9658
9571
  }
9659
- function registerBlastCommand(program2) {
9660
- program2.command("blast").description("Map what an AI agent can currently reach on this machine").action(() => {
9661
- const home = os13.homedir();
9662
- const cwd = process.cwd();
9663
- const { reachable, envFindings, score } = runBlast();
9664
- console.log("");
9665
- console.log(
9666
- chalk2.bold(" \u{1F52D} Node9 Blast Radius") + chalk2.dim(" \xB7 what an AI agent can reach from here")
9667
- );
9668
- console.log(chalk2.dim(" Running in: ") + chalk2.white(cwd.replace(home, "~")));
9669
- console.log("");
9670
- if (reachable.length > 0) {
9671
- console.log(" " + chalk2.red.bold("Sensitive files reachable:"));
9672
- for (const p of reachable) {
9673
- console.log(
9674
- " " + chalk2.red("\u2717 ") + chalk2.yellow(p.label.padEnd(38)) + chalk2.dim(p.description)
9675
- );
9676
- }
9677
- console.log("");
9572
+ function parseGeminiSession(lines, project) {
9573
+ const seenIds = /* @__PURE__ */ new Set();
9574
+ const byKey = /* @__PURE__ */ new Map();
9575
+ let runId = "";
9576
+ for (const raw of lines) {
9577
+ if (!raw.trim()) continue;
9578
+ let obj;
9579
+ try {
9580
+ obj = JSON.parse(raw);
9581
+ } catch {
9582
+ continue;
9678
9583
  }
9679
- if (envFindings.length > 0) {
9680
- console.log(" " + chalk2.red.bold("Secrets in active environment:"));
9681
- for (const f of envFindings) {
9682
- console.log(
9683
- " " + chalk2.red("\u2717 ") + chalk2.yellow(f.key.padEnd(38)) + chalk2.dim(f.patternName)
9684
- );
9685
- }
9686
- console.log("");
9584
+ if (typeof obj.sessionId === "string" && !runId) runId = obj.sessionId;
9585
+ if (!obj.tokens || !obj.model || !obj.timestamp) continue;
9586
+ if (obj.id) {
9587
+ if (seenIds.has(obj.id)) continue;
9588
+ seenIds.add(obj.id);
9687
9589
  }
9688
- console.log(" " + chalk2.dim("\u2500".repeat(70)));
9689
- if (reachable.length === 0 && envFindings.length === 0) {
9690
- console.log(" " + chalk2.green("\u2705 No sensitive files or environment secrets found."));
9691
- console.log(" Security Score: " + scoreLabel(score));
9590
+ const price = geminiPriceFor(obj.model);
9591
+ if (!price) continue;
9592
+ const inp = obj.tokens.input ?? 0;
9593
+ const out = obj.tokens.output ?? 0;
9594
+ const cached = Math.min(obj.tokens.cached ?? 0, inp);
9595
+ const fresh = Math.max(0, inp - cached);
9596
+ const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
9597
+ const date = obj.timestamp.slice(0, 10);
9598
+ const model = normalizeModel(obj.model);
9599
+ const key = `${date}::${model}`;
9600
+ const prev = byKey.get(key);
9601
+ if (prev) {
9602
+ prev.costUSD += cost;
9603
+ prev.inputTokens += fresh;
9604
+ prev.outputTokens += out;
9605
+ prev.cacheReadTokens += cached;
9692
9606
  } else {
9693
- console.log(
9694
- " Security Score: " + scoreLabel(score) + chalk2.dim(
9695
- ` (${reachable.length} file${reachable.length !== 1 ? "s" : ""}, ${envFindings.length} env var${envFindings.length !== 1 ? "s" : ""})`
9696
- )
9697
- );
9698
- console.log("");
9699
- console.log(
9700
- chalk2.dim(
9701
- " Every AI agent you start can read the files and env vars listed above.\n Run `node9 shield enable project-jail` to restrict agent file access.\n Run `node9 mask` to redact secrets from existing session history."
9702
- )
9703
- );
9607
+ byKey.set(key, {
9608
+ date,
9609
+ model,
9610
+ workingDir: project,
9611
+ runId,
9612
+ costUSD: cost,
9613
+ inputTokens: fresh,
9614
+ outputTokens: out,
9615
+ cacheReadTokens: cached,
9616
+ cacheWriteTokens: 0
9617
+ });
9704
9618
  }
9705
- console.log("");
9706
- });
9619
+ }
9620
+ if (runId) for (const e of byKey.values()) e.runId = runId;
9621
+ return [...byKey.values()];
9707
9622
  }
9708
- var init_blast = __esm({
9709
- "src/cli/commands/blast.ts"() {
9623
+ var GEMINI_FALLBACK_MODELS, geminiSource;
9624
+ var init_cost_gemini = __esm({
9625
+ "src/cost-gemini.ts"() {
9710
9626
  "use strict";
9711
- init_dlp();
9627
+ init_litellm();
9628
+ GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
9629
+ geminiSource = {
9630
+ id: "gemini",
9631
+ available() {
9632
+ try {
9633
+ return fs15.existsSync(geminiTmpDir());
9634
+ } catch {
9635
+ return false;
9636
+ }
9637
+ },
9638
+ collect(sinceMs) {
9639
+ const combined = /* @__PURE__ */ new Map();
9640
+ for (const { file, project } of listGeminiSessionFiles(geminiTmpDir())) {
9641
+ try {
9642
+ if (sinceMs !== void 0 && fs15.statSync(file).mtimeMs < sinceMs) continue;
9643
+ } catch {
9644
+ continue;
9645
+ }
9646
+ let content;
9647
+ try {
9648
+ content = fs15.readFileSync(file, "utf8");
9649
+ } catch {
9650
+ continue;
9651
+ }
9652
+ for (const e of parseGeminiSession(content.split("\n"), project)) {
9653
+ const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
9654
+ const prev = combined.get(key);
9655
+ if (prev) {
9656
+ prev.costUSD += e.costUSD;
9657
+ prev.inputTokens += e.inputTokens;
9658
+ prev.outputTokens += e.outputTokens;
9659
+ prev.cacheReadTokens += e.cacheReadTokens;
9660
+ prev.cacheWriteTokens += e.cacheWriteTokens;
9661
+ } else {
9662
+ combined.set(key, { ...e });
9663
+ }
9664
+ }
9665
+ }
9666
+ return [...combined.values()];
9667
+ }
9668
+ };
9712
9669
  }
9713
9670
  });
9714
9671
 
9715
- // src/cli/render/scan-derive.ts
9716
- import chalk3 from "chalk";
9717
- import stringWidth from "string-width";
9718
- function classifyScore(score) {
9719
- if (score >= 80) return { band: "good", label: "Good", color: chalk3.green };
9720
- if (score >= 50) return { band: "at-risk", label: "At Risk", color: chalk3.yellow };
9721
- return { band: "critical", label: "Critical", color: chalk3.red };
9722
- }
9723
- function topDlpPatterns(findings, n) {
9724
- const counts = /* @__PURE__ */ new Map();
9725
- for (const f of findings) {
9726
- counts.set(f.patternName, (counts.get(f.patternName) ?? 0) + 1);
9727
- }
9728
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([name, count]) => ({ name, count }));
9672
+ // src/cost-codex.ts
9673
+ import fs16 from "fs";
9674
+ import os15 from "os";
9675
+ import path18 from "path";
9676
+ function codexSessionsDir() {
9677
+ return path18.join(os15.homedir(), ".codex", "sessions");
9729
9678
  }
9730
- function topRulesByVerdict(sections, verdict, n) {
9731
- const matched = [];
9732
- for (const section of sections) {
9733
- for (const rule of section.rules) {
9734
- const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
9735
- if (matches) matched.push({ name: rule.name, count: rule.findings.length });
9736
- }
9737
- }
9738
- return matched.sort((a, b) => b.count - a.count).slice(0, n);
9679
+ function codexPriceFor(model) {
9680
+ return pricingFor(model) ?? CODEX_FALLBACK;
9739
9681
  }
9740
- function computeLoopWaste(loops, totalToolCalls) {
9741
- const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
9742
- const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
9743
- return { wastedCalls, wastePct };
9682
+ function codexSessionCost(model, tokens) {
9683
+ const nonCached = Math.max(0, tokens.input - tokens.cached);
9684
+ const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
9685
+ return nonCached * pin + tokens.cached * pcr + tokens.output * pout;
9744
9686
  }
9745
- function rollupByShield(sections, topRulesPerShield = 3) {
9687
+ function listCodexSessionFiles(base) {
9746
9688
  const out = [];
9747
- for (const section of sections) {
9748
- if (section.sourceType !== "shield") continue;
9749
- if (!section.shieldKey) continue;
9750
- const totalCatches = section.blockedCount + section.reviewCount;
9751
- const topRuleLabels = [...section.rules].sort((a, b) => b.findings.length - a.findings.length).slice(0, topRulesPerShield).map((r) => r.findings.length > 1 ? `${r.name} \xD7${r.findings.length}` : r.name);
9752
- out.push({
9753
- shieldName: section.shieldKey,
9754
- totalCatches,
9755
- blockCatches: section.blockedCount,
9756
- reviewCatches: section.reviewCount,
9757
- topRuleLabels
9758
- });
9689
+ for (const y of safeReaddir2(base)) {
9690
+ const yp = path18.join(base, y);
9691
+ if (!isDir2(yp)) continue;
9692
+ for (const m of safeReaddir2(yp)) {
9693
+ const mp = path18.join(yp, m);
9694
+ if (!isDir2(mp)) continue;
9695
+ for (const d of safeReaddir2(mp)) {
9696
+ const dp = path18.join(mp, d);
9697
+ if (!isDir2(dp)) continue;
9698
+ for (const f of safeReaddir2(dp)) {
9699
+ if (f.endsWith(".jsonl")) out.push(path18.join(dp, f));
9700
+ }
9701
+ }
9702
+ }
9759
9703
  }
9760
- return out.sort((a, b) => b.totalCatches - a.totalCatches);
9704
+ return out;
9761
9705
  }
9762
- function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
9763
- const inner = width - 4;
9764
- const out = [];
9765
- const titlePad = ` ${title} `;
9766
- const titleWidth = stringWidth(titlePad);
9767
- const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
9768
- const dashFill = "\u2500".repeat(Math.max(0, inner - stringWidth(titleSegment)));
9769
- out.push(chalk3.dim("\u256D\u2500") + chalk3.bold(titleSegment) + chalk3.dim(`${dashFill}\u2500\u256E`));
9770
- for (const line of bodyLines) {
9771
- const padding = " ".repeat(Math.max(0, inner - line.width));
9772
- out.push(chalk3.dim("\u2502 ") + line.rendered + padding + chalk3.dim(" \u2502"));
9706
+ function safeReaddir2(dir) {
9707
+ try {
9708
+ return fs16.readdirSync(dir);
9709
+ } catch {
9710
+ return [];
9773
9711
  }
9774
- out.push(chalk3.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
9775
- return out;
9776
9712
  }
9777
- function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
9778
- const t = new Date(timestamp).getTime();
9779
- if (Number.isNaN(t)) return "?";
9780
- const days = Math.floor((now.getTime() - t) / 864e5);
9781
- if (days < 1) return "today";
9782
- if (days > 90) return "90d+";
9783
- return `${days}d`;
9713
+ function isDir2(p) {
9714
+ try {
9715
+ return fs16.statSync(p).isDirectory();
9716
+ } catch {
9717
+ return false;
9718
+ }
9784
9719
  }
9785
- var PANEL_WIDTH;
9786
- var init_scan_derive = __esm({
9787
- "src/cli/render/scan-derive.ts"() {
9788
- "use strict";
9789
- PANEL_WIDTH = 76;
9720
+ function parseCodexSession(lines) {
9721
+ let sessionStart2 = "";
9722
+ let runId = "";
9723
+ let cwd = "";
9724
+ let model = "";
9725
+ let input = 0;
9726
+ let cached = 0;
9727
+ let output = 0;
9728
+ let sawUsage = false;
9729
+ for (const raw of lines) {
9730
+ if (!raw.trim()) continue;
9731
+ let entry;
9732
+ try {
9733
+ entry = JSON.parse(raw);
9734
+ } catch {
9735
+ continue;
9736
+ }
9737
+ const p = entry.payload ?? {};
9738
+ if (entry.type === "session_meta") {
9739
+ if (!sessionStart2 && typeof p["timestamp"] === "string") sessionStart2 = p["timestamp"];
9740
+ if (!runId && typeof p["id"] === "string") runId = p["id"];
9741
+ if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
9742
+ continue;
9743
+ }
9744
+ if (entry.type === "turn_context") {
9745
+ if (typeof p["model"] === "string") model = p["model"];
9746
+ if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
9747
+ continue;
9748
+ }
9749
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
9750
+ const info = p["info"] ?? {};
9751
+ const usage = info["total_token_usage"] ?? {};
9752
+ if (typeof usage["input_tokens"] === "number") input = usage["input_tokens"];
9753
+ if (typeof usage["cached_input_tokens"] === "number") cached = usage["cached_input_tokens"];
9754
+ if (typeof usage["output_tokens"] === "number") output = usage["output_tokens"];
9755
+ sawUsage = true;
9756
+ }
9790
9757
  }
9791
- });
9792
-
9793
- // src/protection.ts
9794
- var PROTECTIVE_SHIELD_DISCOUNTS;
9795
- var init_protection = __esm({
9796
- "src/protection.ts"() {
9758
+ if (!sessionStart2 || !sawUsage) return null;
9759
+ const nonCached = Math.max(0, input - cached);
9760
+ if (nonCached === 0 && output === 0 && cached === 0) return null;
9761
+ const norm = normalizeModel(model || "gpt-5");
9762
+ const costUSD = codexSessionCost(model, { input, cached, output });
9763
+ return {
9764
+ date: sessionStart2.slice(0, 10),
9765
+ model: norm,
9766
+ workingDir: cwd,
9767
+ runId,
9768
+ costUSD,
9769
+ inputTokens: nonCached,
9770
+ outputTokens: output,
9771
+ cacheReadTokens: cached,
9772
+ cacheWriteTokens: 0
9773
+ };
9774
+ }
9775
+ var CODEX_FALLBACK, codexSource;
9776
+ var init_cost_codex = __esm({
9777
+ "src/cost-codex.ts"() {
9797
9778
  "use strict";
9798
- PROTECTIVE_SHIELD_DISCOUNTS = {
9799
- "project-jail": 0.7
9779
+ init_litellm();
9780
+ CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
9781
+ codexSource = {
9782
+ id: "codex",
9783
+ available() {
9784
+ try {
9785
+ return fs16.existsSync(codexSessionsDir());
9786
+ } catch {
9787
+ return false;
9788
+ }
9789
+ },
9790
+ collect(sinceMs) {
9791
+ const base = codexSessionsDir();
9792
+ const combined = /* @__PURE__ */ new Map();
9793
+ for (const file of listCodexSessionFiles(base)) {
9794
+ try {
9795
+ if (sinceMs !== void 0 && fs16.statSync(file).mtimeMs < sinceMs) continue;
9796
+ } catch {
9797
+ continue;
9798
+ }
9799
+ let content;
9800
+ try {
9801
+ content = fs16.readFileSync(file, "utf8");
9802
+ } catch {
9803
+ continue;
9804
+ }
9805
+ const e = parseCodexSession(content.split("\n"));
9806
+ if (!e) continue;
9807
+ const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
9808
+ const prev = combined.get(key);
9809
+ if (prev) {
9810
+ prev.costUSD += e.costUSD;
9811
+ prev.inputTokens += e.inputTokens;
9812
+ prev.outputTokens += e.outputTokens;
9813
+ prev.cacheReadTokens += e.cacheReadTokens;
9814
+ prev.cacheWriteTokens += e.cacheWriteTokens;
9815
+ } else {
9816
+ combined.set(key, { ...e });
9817
+ }
9818
+ }
9819
+ return [...combined.values()];
9820
+ }
9800
9821
  };
9801
9822
  }
9802
9823
  });
9803
9824
 
9804
- // src/cli/render/scan-json.ts
9805
- function buildScanJson(input) {
9806
- const { summary, blast, isWired, generatedAt } = input;
9807
- const { band } = classifyScore(blast.score);
9808
- return {
9809
- schemaVersion: 1,
9810
- generatedAt,
9811
- isWired,
9812
- score: blast.score,
9813
- band,
9814
- totals: {
9815
- blocked: summary.byVerdict.blocked,
9816
- review: summary.byVerdict.supervised,
9817
- leaks: summary.byVerdict.leaks,
9818
- loops: summary.byVerdict.loops,
9819
- blastExposures: blast.reachable.length + blast.envFindings.length
9820
- },
9821
- summary,
9822
- blast: {
9823
- score: blast.score,
9824
- reachable: blast.reachable,
9825
- envFindings: blast.envFindings
9826
- }
9827
- };
9825
+ // src/utils/hook-payload.ts
9826
+ function extractToolName(payload, defaultValue = "") {
9827
+ return payload.tool_name ?? payload.name ?? payload.toolCall?.name ?? defaultValue;
9828
+ }
9829
+ function extractToolInput(payload) {
9830
+ return payload.tool_input ?? payload.args ?? payload.toolCall?.args ?? {};
9831
+ }
9832
+ function canonicalToolName(name) {
9833
+ switch (name) {
9834
+ // Hermes Agent
9835
+ case "terminal":
9836
+ return "Bash";
9837
+ case "write_file":
9838
+ return "Write";
9839
+ case "patch":
9840
+ return "Edit";
9841
+ case "read_file":
9842
+ return "Read";
9843
+ case "search_files":
9844
+ return "Grep";
9845
+ // Antigravity (agy) — shell tool renamed from Gemini's run_shell_command
9846
+ case "run_command":
9847
+ return "Bash";
9848
+ default:
9849
+ return name;
9850
+ }
9851
+ }
9852
+ function agentLabelFromFlag(flag) {
9853
+ if (typeof flag !== "string") return void 0;
9854
+ switch (flag.toLowerCase()) {
9855
+ case "antigravity":
9856
+ case "agy":
9857
+ return "Antigravity";
9858
+ case "copilot":
9859
+ return "GitHub Copilot";
9860
+ default:
9861
+ return void 0;
9862
+ }
9828
9863
  }
9829
- var init_scan_json = __esm({
9830
- "src/cli/render/scan-json.ts"() {
9864
+ function canonicalToolInput(rawToolName, input) {
9865
+ if (rawToolName !== "run_command") return input;
9866
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
9867
+ const args = input;
9868
+ if (typeof args.CommandLine !== "string") return input;
9869
+ const { CommandLine, Cwd, ...rest } = args;
9870
+ const canonical = { ...rest, command: CommandLine };
9871
+ if (typeof Cwd === "string" && Cwd.length > 0) canonical.cwd = Cwd;
9872
+ return canonical;
9873
+ }
9874
+ var init_hook_payload = __esm({
9875
+ "src/utils/hook-payload.ts"() {
9831
9876
  "use strict";
9832
- init_scan_derive();
9833
9877
  }
9834
9878
  });
9835
9879
 
9836
- // src/cli/render/scan-history.ts
9837
- import fs15 from "fs";
9838
- import path17 from "path";
9839
- import os14 from "os";
9840
- function defaultHistoryPath() {
9841
- return path17.join(os14.homedir(), ".node9", "scan-history.json");
9880
+ // src/scan-summary.ts
9881
+ function agentDisplayName(agent) {
9882
+ return AGENT_LONG[agent] ?? "Claude Code";
9842
9883
  }
9843
- function readPreviousScan(opts = {}) {
9844
- const filePath = opts.path ?? defaultHistoryPath();
9845
- try {
9846
- if (!fs15.existsSync(filePath)) return null;
9847
- const raw = fs15.readFileSync(filePath, "utf8");
9848
- const parsed = JSON.parse(raw);
9849
- if (!Array.isArray(parsed) || parsed.length === 0) return null;
9850
- const last = parsed[parsed.length - 1];
9851
- if (!isValidRecord(last)) return null;
9852
- return last;
9853
- } catch {
9854
- return null;
9884
+ function agentBadgeText(agent, width = 10) {
9885
+ return `[${AGENT_SHORT[agent] ?? "Claude"}]`.padEnd(width);
9886
+ }
9887
+ function agentColorName(agent) {
9888
+ switch (agent) {
9889
+ case "gemini":
9890
+ return "blue";
9891
+ case "codex":
9892
+ return "magenta";
9893
+ case "antigravity":
9894
+ return "yellow";
9895
+ case "copilot":
9896
+ return "green";
9897
+ case "shell":
9898
+ return "yellow";
9899
+ default:
9900
+ return "cyan";
9855
9901
  }
9856
9902
  }
9857
- function appendScanHistory(record, opts = {}) {
9858
- const filePath = opts.path ?? defaultHistoryPath();
9859
- const cap = opts.cap ?? SCAN_HISTORY_CAP;
9860
- try {
9861
- fs15.mkdirSync(path17.dirname(filePath), { recursive: true });
9862
- let history = [];
9863
- if (fs15.existsSync(filePath)) {
9864
- try {
9865
- const parsed = JSON.parse(fs15.readFileSync(filePath, "utf8"));
9866
- if (Array.isArray(parsed)) {
9867
- history = parsed.filter(isValidRecord);
9868
- }
9869
- } catch {
9870
- }
9903
+ function buildScanSummary(agents) {
9904
+ const stats = {
9905
+ sessions: 0,
9906
+ totalToolCalls: 0,
9907
+ bashCalls: 0,
9908
+ totalCostUSD: 0,
9909
+ firstDate: null,
9910
+ lastDate: null
9911
+ };
9912
+ for (const a of agents) {
9913
+ stats.sessions += a.scan.sessions;
9914
+ stats.totalToolCalls += a.scan.totalToolCalls;
9915
+ stats.bashCalls += a.scan.bashCalls;
9916
+ stats.totalCostUSD += a.scan.totalCostUSD;
9917
+ if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
9918
+ stats.firstDate = a.scan.firstDate;
9871
9919
  }
9872
- history.push(record);
9873
- if (history.length > cap) {
9874
- history = history.slice(history.length - cap);
9920
+ if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
9921
+ stats.lastDate = a.scan.lastDate;
9875
9922
  }
9876
- fs15.writeFileSync(filePath, JSON.stringify(history, null, 2));
9877
- } catch (err2) {
9878
- process.stderr.write(
9879
- `[node9] Warning: could not write scan-history.json: ${err2.message}
9880
- `
9881
- );
9882
9923
  }
9924
+ const allFindings = agents.flatMap((a) => a.scan.findings);
9925
+ const allLeaks = agents.flatMap(
9926
+ (a) => a.scan.dlpFindings.map((f) => ({
9927
+ patternName: f.patternName,
9928
+ redactedSample: f.redactedSample,
9929
+ toolName: f.toolName,
9930
+ timestamp: f.timestamp,
9931
+ project: f.project,
9932
+ sessionId: f.sessionId,
9933
+ agent: f.agent
9934
+ }))
9935
+ );
9936
+ const allLoops = agents.flatMap(
9937
+ (a) => a.scan.loopFindings.map((f) => ({
9938
+ toolName: f.toolName,
9939
+ commandPreview: f.commandPreview,
9940
+ count: f.count,
9941
+ timestamp: f.timestamp,
9942
+ project: f.project,
9943
+ sessionId: f.sessionId,
9944
+ agent: f.agent,
9945
+ kind: f.kind
9946
+ }))
9947
+ );
9948
+ const byVerdict = {
9949
+ blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
9950
+ supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
9951
+ leaks: allLeaks.length,
9952
+ loops: allLoops.length
9953
+ };
9954
+ const byAgent = agents.map((a) => ({
9955
+ id: a.id,
9956
+ label: a.label,
9957
+ icon: a.icon,
9958
+ sessions: a.scan.sessions,
9959
+ findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
9960
+ costUSD: a.scan.totalCostUSD
9961
+ })).filter((s) => s.sessions > 0 || s.findings > 0);
9962
+ const sections = buildSections(allFindings);
9963
+ const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
9964
+ const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
9965
+ return {
9966
+ stats,
9967
+ byVerdict,
9968
+ byAgent,
9969
+ sections,
9970
+ leaks: allLeaks,
9971
+ loops: allLoops,
9972
+ loopWastedUSD
9973
+ };
9883
9974
  }
9884
- function computeScanDelta(current, previous, now = Date.now()) {
9885
- if (!previous) return null;
9886
- const prevMs = Date.parse(previous.timestamp);
9887
- if (Number.isNaN(prevMs)) return null;
9888
- const scoreDelta = current.score - previous.score;
9889
- const daysAgo = Math.max(0, Math.floor((now - prevMs) / 864e5));
9890
- if (scoreDelta === 0 && daysAgo === 0) return null;
9891
- return { scoreDelta, daysAgo };
9975
+ function buildSections(findings) {
9976
+ const sectionMap = /* @__PURE__ */ new Map();
9977
+ function ensureSection(id, label, subtitle, sourceType, shieldKey) {
9978
+ let s = sectionMap.get(id);
9979
+ if (!s) {
9980
+ s = {
9981
+ id,
9982
+ label,
9983
+ subtitle,
9984
+ sourceType,
9985
+ shieldKey,
9986
+ blockedCount: 0,
9987
+ reviewCount: 0,
9988
+ rules: []
9989
+ };
9990
+ sectionMap.set(id, s);
9991
+ }
9992
+ return s;
9993
+ }
9994
+ const ruleMap = /* @__PURE__ */ new Map();
9995
+ for (const f of findings) {
9996
+ const src = f.source;
9997
+ const sourceType = src.sourceType;
9998
+ const shieldName = src.shieldName;
9999
+ const verdict = src.rule.verdict === "block" ? "block" : "review";
10000
+ let sectionId;
10001
+ let sectionLabel;
10002
+ let sectionSubtitle;
10003
+ let shieldKey;
10004
+ if (sourceType === "default") {
10005
+ sectionId = "default";
10006
+ sectionLabel = "Default Rules";
10007
+ sectionSubtitle = "built-in, always on";
10008
+ } else if (sourceType === "shield") {
10009
+ sectionId = `shield:${shieldName}`;
10010
+ sectionLabel = shieldName;
10011
+ sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
10012
+ shieldKey = shieldName;
10013
+ } else if (shieldName === "cloud") {
10014
+ sectionId = "cloud";
10015
+ sectionLabel = "Cloud Policy";
10016
+ sectionSubtitle = "synced from node9 cloud";
10017
+ } else {
10018
+ sectionId = "user";
10019
+ sectionLabel = "Your Rules";
10020
+ sectionSubtitle = "added in node9.config.json";
10021
+ }
10022
+ const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
10023
+ const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
10024
+ const ruleKey = sectionId + "::" + ruleDisplayName;
10025
+ let rule = ruleMap.get(ruleKey);
10026
+ if (!rule) {
10027
+ rule = {
10028
+ name: ruleDisplayName,
10029
+ verdict,
10030
+ reason: src.rule.reason ?? "",
10031
+ findings: []
10032
+ };
10033
+ ruleMap.set(ruleKey, rule);
10034
+ section.rules.push(rule);
10035
+ }
10036
+ const cmdPreview = previewCommand(f.input, 120);
10037
+ const fullCmd = fullCommandOf(f.input);
10038
+ const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
10039
+ if (!isDupe) {
10040
+ rule.findings.push({
10041
+ timestamp: f.timestamp ?? "",
10042
+ command: cmdPreview,
10043
+ fullCommand: fullCmd,
10044
+ project: f.project,
10045
+ sessionId: f.sessionId,
10046
+ agent: f.agent,
10047
+ toolName: f.toolName
10048
+ });
10049
+ }
10050
+ if (verdict === "block") section.blockedCount++;
10051
+ else section.reviewCount++;
10052
+ }
10053
+ const sections = [...sectionMap.values()];
10054
+ sections.sort((a, b) => {
10055
+ const aTotal = a.blockedCount + a.reviewCount;
10056
+ const bTotal = b.blockedCount + b.reviewCount;
10057
+ if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
10058
+ return bTotal - aTotal;
10059
+ });
10060
+ for (const s of sections) {
10061
+ s.rules.sort((a, b) => {
10062
+ const aBlock = a.verdict === "block" ? 1 : 0;
10063
+ const bBlock = b.verdict === "block" ? 1 : 0;
10064
+ if (bBlock !== aBlock) return bBlock - aBlock;
10065
+ return b.findings.length - a.findings.length;
10066
+ });
10067
+ }
10068
+ return sections;
9892
10069
  }
9893
- function isValidRecord(x) {
9894
- if (typeof x !== "object" || x === null) return false;
9895
- const r = x;
9896
- return typeof r.timestamp === "string" && typeof r.score === "number" && typeof r.blocked === "number" && typeof r.review === "number" && typeof r.leaks === "number" && typeof r.loops === "number" && typeof r.totalCalls === "number";
10070
+ function previewCommand(input, max) {
10071
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
10072
+ const s = String(raw).replace(/\s+/g, " ").trim();
10073
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
9897
10074
  }
9898
- var SCAN_HISTORY_CAP;
9899
- var init_scan_history = __esm({
9900
- "src/cli/render/scan-history.ts"() {
10075
+ function fullCommandOf(input) {
10076
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
10077
+ return String(raw).replace(/\s+/g, " ").trim();
10078
+ }
10079
+ var AGENT_SHORT, AGENT_LONG;
10080
+ var init_scan_summary = __esm({
10081
+ "src/scan-summary.ts"() {
9901
10082
  "use strict";
9902
- SCAN_HISTORY_CAP = 30;
10083
+ init_shields();
10084
+ init_dist();
10085
+ init_dist();
10086
+ AGENT_SHORT = {
10087
+ claude: "Claude",
10088
+ gemini: "Gemini",
10089
+ codex: "Codex",
10090
+ antigravity: "Agy",
10091
+ copilot: "Copilot",
10092
+ shell: "Shell"
10093
+ };
10094
+ AGENT_LONG = {
10095
+ claude: "Claude Code",
10096
+ gemini: "Gemini CLI",
10097
+ codex: "Codex",
10098
+ antigravity: "Antigravity",
10099
+ copilot: "GitHub Copilot",
10100
+ shell: "Shell"
10101
+ };
9903
10102
  }
9904
10103
  });
9905
10104
 
9906
- // src/pricing/litellm.ts
9907
- import fs16 from "fs";
9908
- import path18 from "path";
9909
- import os15 from "os";
9910
- function normalizeModel(raw) {
9911
- return raw.replace(/-\d{8}$/, "").toLowerCase();
9912
- }
9913
- function readCache() {
9914
- try {
9915
- const raw = JSON.parse(fs16.readFileSync(CACHE_FILE(), "utf-8"));
9916
- if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
9917
- return null;
9918
- }
9919
- const ageMs = Date.now() - new Date(raw.fetchedAt).getTime();
9920
- if (ageMs < 0 || ageMs > TTL_MS) return null;
9921
- return raw.prices;
9922
- } catch {
9923
- return null;
9924
- }
9925
- }
9926
- function writeCache(prices) {
9927
- try {
9928
- const target = CACHE_FILE();
9929
- const dir = path18.dirname(target);
9930
- if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
9931
- const tmp = target + ".tmp";
9932
- const body = {
9933
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
9934
- prices
9935
- };
9936
- fs16.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
9937
- fs16.renameSync(tmp, target);
9938
- } catch (err2) {
9939
- try {
9940
- fs16.appendFileSync(
9941
- HOOK_DEBUG_LOG,
9942
- `[pricing] cache write failed: ${err2.message}
9943
- `
9944
- );
9945
- } catch {
9946
- }
9947
- }
9948
- }
9949
- function tupleFromLiteLLM(entry) {
9950
- if (!entry || typeof entry !== "object") return null;
9951
- const e = entry;
9952
- const num3 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
9953
- const inCost = num3(e.input_cost_per_token);
9954
- const outCost = num3(e.output_cost_per_token);
9955
- if (inCost === 0 && outCost === 0) return null;
10105
+ // src/cli/commands/blast.ts
10106
+ import chalk2 from "chalk";
10107
+ import fs17 from "fs";
10108
+ import path19 from "path";
10109
+ import os16 from "os";
10110
+ function buildSensitivePaths(home, cwd) {
9956
10111
  return [
9957
- inCost,
9958
- outCost,
9959
- num3(e.cache_creation_input_token_cost),
9960
- num3(e.cache_read_input_token_cost)
10112
+ {
10113
+ full: path19.join(home, ".ssh", "id_rsa"),
10114
+ label: "~/.ssh/id_rsa",
10115
+ description: "RSA private key \u2014 grants SSH access to your servers",
10116
+ score: 20
10117
+ },
10118
+ {
10119
+ full: path19.join(home, ".ssh", "id_ed25519"),
10120
+ label: "~/.ssh/id_ed25519",
10121
+ description: "Ed25519 private key \u2014 grants SSH access to your servers",
10122
+ score: 20
10123
+ },
10124
+ {
10125
+ full: path19.join(home, ".ssh", "id_ecdsa"),
10126
+ label: "~/.ssh/id_ecdsa",
10127
+ description: "ECDSA private key \u2014 grants SSH access to your servers",
10128
+ score: 20
10129
+ },
10130
+ {
10131
+ full: path19.join(home, ".aws", "credentials"),
10132
+ label: "~/.aws/credentials",
10133
+ description: "AWS access keys \u2014 full cloud account access",
10134
+ score: 20
10135
+ },
10136
+ {
10137
+ full: path19.join(home, ".aws", "config"),
10138
+ label: "~/.aws/config",
10139
+ description: "AWS configuration \u2014 account and region settings",
10140
+ score: 5
10141
+ },
10142
+ {
10143
+ full: path19.join(home, ".config", "gcloud", "credentials.db"),
10144
+ label: "~/.config/gcloud/credentials.db",
10145
+ description: "Google Cloud credentials",
10146
+ score: 15
10147
+ },
10148
+ {
10149
+ full: path19.join(home, ".docker", "config.json"),
10150
+ label: "~/.docker/config.json",
10151
+ description: "Docker registry auth tokens",
10152
+ score: 10
10153
+ },
10154
+ {
10155
+ full: path19.join(home, ".netrc"),
10156
+ label: "~/.netrc",
10157
+ description: "FTP/HTTP credentials in plain text",
10158
+ score: 15
10159
+ },
10160
+ {
10161
+ full: path19.join(home, ".npmrc"),
10162
+ label: "~/.npmrc",
10163
+ description: "npm auth token \u2014 can publish packages as you",
10164
+ score: 10
10165
+ },
10166
+ {
10167
+ full: path19.join(home, ".node9", "credentials.json"),
10168
+ label: "~/.node9/credentials.json",
10169
+ description: "Node9 cloud API key",
10170
+ score: 10
10171
+ },
10172
+ {
10173
+ full: path19.join(cwd, ".env"),
10174
+ label: ".env (current folder)",
10175
+ description: "App secrets \u2014 database passwords, API keys",
10176
+ score: 20
10177
+ },
10178
+ {
10179
+ full: path19.join(cwd, ".env.local"),
10180
+ label: ".env.local (current folder)",
10181
+ description: "Local overrides \u2014 often contains real credentials",
10182
+ score: 15
10183
+ },
10184
+ {
10185
+ full: path19.join(cwd, ".env.production"),
10186
+ label: ".env.production (current folder)",
10187
+ description: "Production secrets",
10188
+ score: 20
10189
+ }
9961
10190
  ];
9962
10191
  }
9963
- async function fetchLiteLLMPricing() {
10192
+ function isReadable(filePath) {
9964
10193
  try {
9965
- const res = await fetch(LITELLM_URL, {
9966
- signal: AbortSignal.timeout(15e3)
9967
- });
9968
- if (!res.ok) return null;
9969
- const json = await res.json();
9970
- if (!json || typeof json !== "object") return null;
9971
- const out = {};
9972
- for (const [key, value] of Object.entries(json)) {
9973
- const tuple = tupleFromLiteLLM(value);
9974
- if (tuple) out[key.toLowerCase()] = tuple;
9975
- }
9976
- if (Object.keys(out).length < 10) {
9977
- return null;
9978
- }
9979
- return out;
10194
+ fs17.accessSync(filePath, fs17.constants.R_OK);
10195
+ return true;
9980
10196
  } catch {
9981
- return null;
10197
+ return false;
9982
10198
  }
9983
10199
  }
9984
- async function ensurePricingLoaded() {
9985
- if (memCache !== null && Date.now() - memCacheAt < TTL_MS) return;
9986
- const fromDisk = readCache();
9987
- if (fromDisk && Object.keys(fromDisk).length > 0) {
9988
- memCache = fromDisk;
9989
- memCacheAt = Date.now();
9990
- lookupCache.clear();
9991
- return;
10200
+ function scoreLabel(score) {
10201
+ if (score >= 80) return chalk2.green(`${score}/100 Good`);
10202
+ if (score >= 50) return chalk2.yellow(`${score}/100 Moderate risk`);
10203
+ if (score >= 25) return chalk2.red(`${score}/100 High risk`);
10204
+ return chalk2.red.bold(`${score}/100 Critical`);
10205
+ }
10206
+ function runBlast() {
10207
+ const home = os16.homedir();
10208
+ const cwd = process.cwd();
10209
+ const paths = buildSensitivePaths(home, cwd);
10210
+ let scoreDeduction = 0;
10211
+ const reachable = [];
10212
+ for (const p of paths) {
10213
+ if (fs17.existsSync(p.full) && isReadable(p.full)) {
10214
+ reachable.push(p);
10215
+ scoreDeduction += p.score;
10216
+ }
9992
10217
  }
9993
- const fetched = await fetchLiteLLMPricing();
9994
- if (fetched && Object.keys(fetched).length > 0) {
9995
- memCache = fetched;
9996
- memCacheAt = Date.now();
9997
- writeCache(fetched);
9998
- lookupCache.clear();
9999
- return;
10218
+ const envFindings = [];
10219
+ for (const [key, value] of Object.entries(process.env)) {
10220
+ if (!value) continue;
10221
+ const match = scanArgs({ [key]: value });
10222
+ if (match) {
10223
+ envFindings.push({ key, patternName: match.patternName });
10224
+ scoreDeduction += 10;
10225
+ }
10000
10226
  }
10001
- memCache = { ...BUNDLED_PRICING };
10002
- memCacheAt = Date.now();
10003
- lookupCache.clear();
10227
+ return { reachable, envFindings, score: Math.max(0, 100 - scoreDeduction) };
10004
10228
  }
10005
- function pricingFor(model) {
10006
- const norm = normalizeModel(model);
10007
- const cached = lookupCache.get(norm);
10008
- if (cached !== void 0) return cached;
10009
- const sources = [];
10010
- if (memCache) sources.push(memCache);
10011
- sources.push(BUNDLED_PRICING);
10012
- let resolved = null;
10013
- for (const source of sources) {
10014
- const exact = source[norm];
10015
- if (exact) {
10016
- resolved = exact;
10017
- break;
10229
+ function registerBlastCommand(program2) {
10230
+ program2.command("blast").description("Map what an AI agent can currently reach on this machine").action(() => {
10231
+ const home = os16.homedir();
10232
+ const cwd = process.cwd();
10233
+ const { reachable, envFindings, score } = runBlast();
10234
+ console.log("");
10235
+ console.log(
10236
+ chalk2.bold(" \u{1F52D} Node9 Blast Radius") + chalk2.dim(" \xB7 what an AI agent can reach from here")
10237
+ );
10238
+ console.log(chalk2.dim(" Running in: ") + chalk2.white(cwd.replace(home, "~")));
10239
+ console.log("");
10240
+ if (reachable.length > 0) {
10241
+ console.log(" " + chalk2.red.bold("Sensitive files reachable:"));
10242
+ for (const p of reachable) {
10243
+ console.log(
10244
+ " " + chalk2.red("\u2717 ") + chalk2.yellow(p.label.padEnd(38)) + chalk2.dim(p.description)
10245
+ );
10246
+ }
10247
+ console.log("");
10018
10248
  }
10019
- let best = null;
10020
- for (const key of Object.keys(source)) {
10021
- if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
10022
- best = key;
10249
+ if (envFindings.length > 0) {
10250
+ console.log(" " + chalk2.red.bold("Secrets in active environment:"));
10251
+ for (const f of envFindings) {
10252
+ console.log(
10253
+ " " + chalk2.red("\u2717 ") + chalk2.yellow(f.key.padEnd(38)) + chalk2.dim(f.patternName)
10254
+ );
10023
10255
  }
10256
+ console.log("");
10024
10257
  }
10025
- if (best) {
10026
- resolved = source[best];
10027
- break;
10258
+ console.log(" " + chalk2.dim("\u2500".repeat(70)));
10259
+ if (reachable.length === 0 && envFindings.length === 0) {
10260
+ console.log(" " + chalk2.green("\u2705 No sensitive files or environment secrets found."));
10261
+ console.log(" Security Score: " + scoreLabel(score));
10262
+ } else {
10263
+ console.log(
10264
+ " Security Score: " + scoreLabel(score) + chalk2.dim(
10265
+ ` (${reachable.length} file${reachable.length !== 1 ? "s" : ""}, ${envFindings.length} env var${envFindings.length !== 1 ? "s" : ""})`
10266
+ )
10267
+ );
10268
+ console.log("");
10269
+ console.log(
10270
+ chalk2.dim(
10271
+ " 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."
10272
+ )
10273
+ );
10028
10274
  }
10029
- }
10030
- lookupCache.set(norm, resolved);
10031
- return resolved;
10275
+ console.log("");
10276
+ });
10032
10277
  }
10033
- var LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, lookupCache;
10034
- var init_litellm = __esm({
10035
- "src/pricing/litellm.ts"() {
10278
+ var init_blast = __esm({
10279
+ "src/cli/commands/blast.ts"() {
10036
10280
  "use strict";
10037
- init_audit();
10038
- LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
10039
- BUNDLED_PRICING = {
10040
- // Anthropic
10041
- "claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
10042
- "claude-opus-4-1": [5e-6, 25e-6, 625e-8, 5e-7],
10043
- "claude-opus-4-5": [5e-6, 25e-6, 625e-8, 5e-7],
10044
- "claude-opus-4-6": [5e-6, 25e-6, 625e-8, 5e-7],
10045
- "claude-opus-4-7": [5e-6, 25e-6, 625e-8, 5e-7],
10046
- "claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
10047
- "claude-sonnet-4-5": [3e-6, 15e-6, 375e-8, 3e-7],
10048
- "claude-sonnet-4-6": [3e-6, 15e-6, 375e-8, 3e-7],
10049
- "claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
10050
- "claude-haiku-4-5": [8e-7, 4e-6, 1e-6, 8e-8],
10051
- "claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
10052
- "claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
10053
- "claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
10054
- "claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
10055
- // OpenAI
10056
- "gpt-4o": [5e-6, 15e-6, 0, 25e-7],
10057
- "gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
10058
- "gpt-5": [1e-5, 3e-5, 0, 5e-6],
10059
- // Google
10060
- "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
10061
- "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
10062
- };
10063
- CACHE_FILE = () => path18.join(os15.homedir(), ".node9", "model-pricing.json");
10064
- TTL_MS = 24 * 60 * 60 * 1e3;
10065
- memCache = null;
10066
- memCacheAt = 0;
10067
- lookupCache = /* @__PURE__ */ new Map();
10281
+ init_dlp();
10068
10282
  }
10069
10283
  });
10070
10284
 
10071
- // src/cost-codex.ts
10072
- import fs17 from "fs";
10073
- import os16 from "os";
10074
- import path19 from "path";
10075
- function codexSessionsDir() {
10076
- return path19.join(os16.homedir(), ".codex", "sessions");
10285
+ // src/cli/render/scan-derive.ts
10286
+ import chalk3 from "chalk";
10287
+ import stringWidth from "string-width";
10288
+ function classifyScore(score) {
10289
+ if (score >= 80) return { band: "good", label: "Good", color: chalk3.green };
10290
+ if (score >= 50) return { band: "at-risk", label: "At Risk", color: chalk3.yellow };
10291
+ return { band: "critical", label: "Critical", color: chalk3.red };
10077
10292
  }
10078
- function codexPriceFor(model) {
10079
- return pricingFor(model) ?? CODEX_FALLBACK;
10293
+ function topDlpPatterns(findings, n) {
10294
+ const counts = /* @__PURE__ */ new Map();
10295
+ for (const f of findings) {
10296
+ counts.set(f.patternName, (counts.get(f.patternName) ?? 0) + 1);
10297
+ }
10298
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([name, count]) => ({ name, count }));
10080
10299
  }
10081
- function listCodexSessionFiles(base) {
10082
- const out = [];
10083
- for (const y of safeReaddir(base)) {
10084
- const yp = path19.join(base, y);
10085
- if (!isDir(yp)) continue;
10086
- for (const m of safeReaddir(yp)) {
10087
- const mp = path19.join(yp, m);
10088
- if (!isDir(mp)) continue;
10089
- for (const d of safeReaddir(mp)) {
10090
- const dp = path19.join(mp, d);
10091
- if (!isDir(dp)) continue;
10092
- for (const f of safeReaddir(dp)) {
10093
- if (f.endsWith(".jsonl")) out.push(path19.join(dp, f));
10094
- }
10095
- }
10300
+ function topRulesByVerdict(sections, verdict, n) {
10301
+ const matched = [];
10302
+ for (const section of sections) {
10303
+ for (const rule of section.rules) {
10304
+ const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
10305
+ if (matches) matched.push({ name: rule.name, count: rule.findings.length });
10096
10306
  }
10097
10307
  }
10098
- return out;
10308
+ return matched.sort((a, b) => b.count - a.count).slice(0, n);
10099
10309
  }
10100
- function safeReaddir(dir) {
10101
- try {
10102
- return fs17.readdirSync(dir);
10103
- } catch {
10104
- return [];
10105
- }
10310
+ function computeLoopWaste(loops, totalToolCalls) {
10311
+ const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
10312
+ const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
10313
+ return { wastedCalls, wastePct };
10106
10314
  }
10107
- function isDir(p) {
10108
- try {
10109
- return fs17.statSync(p).isDirectory();
10110
- } catch {
10111
- return false;
10315
+ function rollupByShield(sections, topRulesPerShield = 3) {
10316
+ const out = [];
10317
+ for (const section of sections) {
10318
+ if (section.sourceType !== "shield") continue;
10319
+ if (!section.shieldKey) continue;
10320
+ const totalCatches = section.blockedCount + section.reviewCount;
10321
+ 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);
10322
+ out.push({
10323
+ shieldName: section.shieldKey,
10324
+ totalCatches,
10325
+ blockCatches: section.blockedCount,
10326
+ reviewCatches: section.reviewCount,
10327
+ topRuleLabels
10328
+ });
10112
10329
  }
10330
+ return out.sort((a, b) => b.totalCatches - a.totalCatches);
10113
10331
  }
10114
- function parseCodexSession(lines) {
10115
- let sessionStart2 = "";
10116
- let runId = "";
10117
- let cwd = "";
10118
- let model = "";
10119
- let input = 0;
10120
- let cached = 0;
10121
- let output = 0;
10122
- let sawUsage = false;
10123
- for (const raw of lines) {
10124
- if (!raw.trim()) continue;
10125
- let entry;
10126
- try {
10127
- entry = JSON.parse(raw);
10128
- } catch {
10129
- continue;
10130
- }
10131
- const p = entry.payload ?? {};
10132
- if (entry.type === "session_meta") {
10133
- if (!sessionStart2 && typeof p["timestamp"] === "string") sessionStart2 = p["timestamp"];
10134
- if (!runId && typeof p["id"] === "string") runId = p["id"];
10135
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
10136
- continue;
10137
- }
10138
- if (entry.type === "turn_context") {
10139
- if (typeof p["model"] === "string") model = p["model"];
10140
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
10141
- continue;
10142
- }
10143
- if (entry.type === "event_msg" && p["type"] === "token_count") {
10144
- const info = p["info"] ?? {};
10145
- const usage = info["total_token_usage"] ?? {};
10146
- if (typeof usage["input_tokens"] === "number") input = usage["input_tokens"];
10147
- if (typeof usage["cached_input_tokens"] === "number") cached = usage["cached_input_tokens"];
10148
- if (typeof usage["output_tokens"] === "number") output = usage["output_tokens"];
10149
- sawUsage = true;
10150
- }
10332
+ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
10333
+ const inner = width - 4;
10334
+ const out = [];
10335
+ const titlePad = ` ${title} `;
10336
+ const titleWidth = stringWidth(titlePad);
10337
+ const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
10338
+ const dashFill = "\u2500".repeat(Math.max(0, inner - stringWidth(titleSegment)));
10339
+ out.push(chalk3.dim("\u256D\u2500") + chalk3.bold(titleSegment) + chalk3.dim(`${dashFill}\u2500\u256E`));
10340
+ for (const line of bodyLines) {
10341
+ const padding = " ".repeat(Math.max(0, inner - line.width));
10342
+ out.push(chalk3.dim("\u2502 ") + line.rendered + padding + chalk3.dim(" \u2502"));
10151
10343
  }
10152
- if (!sessionStart2 || !sawUsage) return null;
10153
- const nonCached = Math.max(0, input - cached);
10154
- if (nonCached === 0 && output === 0 && cached === 0) return null;
10155
- const norm = normalizeModel(model || "gpt-5");
10156
- const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
10157
- const costUSD = nonCached * pin + output * pout + cached * pcr;
10158
- return {
10159
- date: sessionStart2.slice(0, 10),
10160
- model: norm,
10161
- workingDir: cwd,
10162
- runId,
10163
- costUSD,
10164
- inputTokens: nonCached,
10165
- outputTokens: output,
10166
- cacheReadTokens: cached,
10167
- cacheWriteTokens: 0
10168
- };
10344
+ out.push(chalk3.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
10345
+ return out;
10169
10346
  }
10170
- var CODEX_FALLBACK, codexSource;
10171
- var init_cost_codex = __esm({
10172
- "src/cost-codex.ts"() {
10347
+ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
10348
+ const t = new Date(timestamp).getTime();
10349
+ if (Number.isNaN(t)) return "?";
10350
+ const days = Math.floor((now.getTime() - t) / 864e5);
10351
+ if (days < 1) return "today";
10352
+ if (days > 90) return "90d+";
10353
+ return `${days}d`;
10354
+ }
10355
+ var PANEL_WIDTH;
10356
+ var init_scan_derive = __esm({
10357
+ "src/cli/render/scan-derive.ts"() {
10173
10358
  "use strict";
10174
- init_litellm();
10175
- CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
10176
- codexSource = {
10177
- id: "codex",
10178
- available() {
10179
- try {
10180
- return fs17.existsSync(codexSessionsDir());
10181
- } catch {
10182
- return false;
10183
- }
10184
- },
10185
- collect(sinceMs) {
10186
- const base = codexSessionsDir();
10187
- const combined = /* @__PURE__ */ new Map();
10188
- for (const file of listCodexSessionFiles(base)) {
10189
- try {
10190
- if (sinceMs !== void 0 && fs17.statSync(file).mtimeMs < sinceMs) continue;
10191
- } catch {
10192
- continue;
10193
- }
10194
- let content;
10195
- try {
10196
- content = fs17.readFileSync(file, "utf8");
10197
- } catch {
10198
- continue;
10199
- }
10200
- const e = parseCodexSession(content.split("\n"));
10201
- if (!e) continue;
10202
- const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
10203
- const prev = combined.get(key);
10204
- if (prev) {
10205
- prev.costUSD += e.costUSD;
10206
- prev.inputTokens += e.inputTokens;
10207
- prev.outputTokens += e.outputTokens;
10208
- prev.cacheReadTokens += e.cacheReadTokens;
10209
- prev.cacheWriteTokens += e.cacheWriteTokens;
10210
- } else {
10211
- combined.set(key, { ...e });
10212
- }
10213
- }
10214
- return [...combined.values()];
10215
- }
10359
+ PANEL_WIDTH = 76;
10360
+ }
10361
+ });
10362
+
10363
+ // src/protection.ts
10364
+ var PROTECTIVE_SHIELD_DISCOUNTS;
10365
+ var init_protection = __esm({
10366
+ "src/protection.ts"() {
10367
+ "use strict";
10368
+ PROTECTIVE_SHIELD_DISCOUNTS = {
10369
+ "project-jail": 0.7
10216
10370
  };
10217
10371
  }
10218
10372
  });
10219
10373
 
10220
- // src/cost-gemini.ts
10374
+ // src/cli/render/scan-json.ts
10375
+ function buildScanJson(input) {
10376
+ const { summary, blast, isWired, generatedAt } = input;
10377
+ const { band } = classifyScore(blast.score);
10378
+ return {
10379
+ schemaVersion: 1,
10380
+ generatedAt,
10381
+ isWired,
10382
+ score: blast.score,
10383
+ band,
10384
+ totals: {
10385
+ blocked: summary.byVerdict.blocked,
10386
+ review: summary.byVerdict.supervised,
10387
+ leaks: summary.byVerdict.leaks,
10388
+ loops: summary.byVerdict.loops,
10389
+ blastExposures: blast.reachable.length + blast.envFindings.length
10390
+ },
10391
+ summary,
10392
+ blast: {
10393
+ score: blast.score,
10394
+ reachable: blast.reachable,
10395
+ envFindings: blast.envFindings
10396
+ }
10397
+ };
10398
+ }
10399
+ var init_scan_json = __esm({
10400
+ "src/cli/render/scan-json.ts"() {
10401
+ "use strict";
10402
+ init_scan_derive();
10403
+ }
10404
+ });
10405
+
10406
+ // src/cli/render/scan-history.ts
10221
10407
  import fs18 from "fs";
10222
- import os17 from "os";
10223
10408
  import path20 from "path";
10224
- function geminiTmpDir() {
10225
- return path20.join(os17.homedir(), ".gemini", "tmp");
10226
- }
10227
- function geminiPriceFor(model) {
10228
- let tuple = pricingFor(model);
10229
- if (!tuple && /^gemini-/i.test(model)) {
10230
- for (const proxy of GEMINI_FALLBACK_MODELS) {
10231
- tuple = pricingFor(proxy);
10232
- if (tuple) break;
10233
- }
10234
- }
10235
- if (!tuple) return null;
10236
- return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
10409
+ import os17 from "os";
10410
+ function defaultHistoryPath() {
10411
+ return path20.join(os17.homedir(), ".node9", "scan-history.json");
10237
10412
  }
10238
- function safeReaddir2(dir) {
10413
+ function readPreviousScan(opts = {}) {
10414
+ const filePath = opts.path ?? defaultHistoryPath();
10239
10415
  try {
10240
- return fs18.readdirSync(dir);
10416
+ if (!fs18.existsSync(filePath)) return null;
10417
+ const raw = fs18.readFileSync(filePath, "utf8");
10418
+ const parsed = JSON.parse(raw);
10419
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
10420
+ const last = parsed[parsed.length - 1];
10421
+ if (!isValidRecord(last)) return null;
10422
+ return last;
10241
10423
  } catch {
10242
- return [];
10424
+ return null;
10243
10425
  }
10244
10426
  }
10245
- function isDir2(p) {
10427
+ function appendScanHistory(record, opts = {}) {
10428
+ const filePath = opts.path ?? defaultHistoryPath();
10429
+ const cap = opts.cap ?? SCAN_HISTORY_CAP;
10246
10430
  try {
10247
- return fs18.statSync(p).isDirectory();
10248
- } catch {
10249
- return false;
10250
- }
10251
- }
10252
- function listGeminiSessionFiles(base) {
10253
- const out = [];
10254
- for (const project of safeReaddir2(base)) {
10255
- const chats = path20.join(base, project, "chats");
10256
- if (!isDir2(chats)) continue;
10257
- for (const f of safeReaddir2(chats)) {
10258
- if (f.startsWith("session-") && f.endsWith(".jsonl")) {
10259
- out.push({ file: path20.join(chats, f), project });
10431
+ fs18.mkdirSync(path20.dirname(filePath), { recursive: true });
10432
+ let history = [];
10433
+ if (fs18.existsSync(filePath)) {
10434
+ try {
10435
+ const parsed = JSON.parse(fs18.readFileSync(filePath, "utf8"));
10436
+ if (Array.isArray(parsed)) {
10437
+ history = parsed.filter(isValidRecord);
10438
+ }
10439
+ } catch {
10260
10440
  }
10261
10441
  }
10262
- }
10263
- return out;
10264
- }
10265
- function parseGeminiSession(lines, project) {
10266
- const seenIds = /* @__PURE__ */ new Set();
10267
- const byKey = /* @__PURE__ */ new Map();
10268
- let runId = "";
10269
- for (const raw of lines) {
10270
- if (!raw.trim()) continue;
10271
- let obj;
10272
- try {
10273
- obj = JSON.parse(raw);
10274
- } catch {
10275
- continue;
10276
- }
10277
- if (typeof obj.sessionId === "string" && !runId) runId = obj.sessionId;
10278
- if (!obj.tokens || !obj.model || !obj.timestamp) continue;
10279
- if (obj.id) {
10280
- if (seenIds.has(obj.id)) continue;
10281
- seenIds.add(obj.id);
10282
- }
10283
- const price = geminiPriceFor(obj.model);
10284
- if (!price) continue;
10285
- const inp = obj.tokens.input ?? 0;
10286
- const out = obj.tokens.output ?? 0;
10287
- const cached = Math.min(obj.tokens.cached ?? 0, inp);
10288
- const fresh = Math.max(0, inp - cached);
10289
- const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
10290
- const date = obj.timestamp.slice(0, 10);
10291
- const model = normalizeModel(obj.model);
10292
- const key = `${date}::${model}`;
10293
- const prev = byKey.get(key);
10294
- if (prev) {
10295
- prev.costUSD += cost;
10296
- prev.inputTokens += fresh;
10297
- prev.outputTokens += out;
10298
- prev.cacheReadTokens += cached;
10299
- } else {
10300
- byKey.set(key, {
10301
- date,
10302
- model,
10303
- workingDir: project,
10304
- runId,
10305
- costUSD: cost,
10306
- inputTokens: fresh,
10307
- outputTokens: out,
10308
- cacheReadTokens: cached,
10309
- cacheWriteTokens: 0
10310
- });
10442
+ history.push(record);
10443
+ if (history.length > cap) {
10444
+ history = history.slice(history.length - cap);
10311
10445
  }
10446
+ fs18.writeFileSync(filePath, JSON.stringify(history, null, 2));
10447
+ } catch (err2) {
10448
+ process.stderr.write(
10449
+ `[node9] Warning: could not write scan-history.json: ${err2.message}
10450
+ `
10451
+ );
10312
10452
  }
10313
- if (runId) for (const e of byKey.values()) e.runId = runId;
10314
- return [...byKey.values()];
10315
10453
  }
10316
- var GEMINI_FALLBACK_MODELS, geminiSource;
10317
- var init_cost_gemini = __esm({
10318
- "src/cost-gemini.ts"() {
10454
+ function computeScanDelta(current, previous, now = Date.now()) {
10455
+ if (!previous) return null;
10456
+ const prevMs = Date.parse(previous.timestamp);
10457
+ if (Number.isNaN(prevMs)) return null;
10458
+ const scoreDelta = current.score - previous.score;
10459
+ const daysAgo = Math.max(0, Math.floor((now - prevMs) / 864e5));
10460
+ if (scoreDelta === 0 && daysAgo === 0) return null;
10461
+ return { scoreDelta, daysAgo };
10462
+ }
10463
+ function isValidRecord(x) {
10464
+ if (typeof x !== "object" || x === null) return false;
10465
+ const r = x;
10466
+ 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";
10467
+ }
10468
+ var SCAN_HISTORY_CAP;
10469
+ var init_scan_history = __esm({
10470
+ "src/cli/render/scan-history.ts"() {
10319
10471
  "use strict";
10320
- init_litellm();
10321
- GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
10322
- geminiSource = {
10323
- id: "gemini",
10324
- available() {
10325
- try {
10326
- return fs18.existsSync(geminiTmpDir());
10327
- } catch {
10328
- return false;
10329
- }
10330
- },
10331
- collect(sinceMs) {
10332
- const combined = /* @__PURE__ */ new Map();
10333
- for (const { file, project } of listGeminiSessionFiles(geminiTmpDir())) {
10334
- try {
10335
- if (sinceMs !== void 0 && fs18.statSync(file).mtimeMs < sinceMs) continue;
10336
- } catch {
10337
- continue;
10338
- }
10339
- let content;
10340
- try {
10341
- content = fs18.readFileSync(file, "utf8");
10342
- } catch {
10343
- continue;
10344
- }
10345
- for (const e of parseGeminiSession(content.split("\n"), project)) {
10346
- const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
10347
- const prev = combined.get(key);
10348
- if (prev) {
10349
- prev.costUSD += e.costUSD;
10350
- prev.inputTokens += e.inputTokens;
10351
- prev.outputTokens += e.outputTokens;
10352
- prev.cacheReadTokens += e.cacheReadTokens;
10353
- prev.cacheWriteTokens += e.cacheWriteTokens;
10354
- } else {
10355
- combined.set(key, { ...e });
10356
- }
10357
- }
10358
- }
10359
- return [...combined.values()];
10360
- }
10361
- };
10472
+ SCAN_HISTORY_CAP = 30;
10362
10473
  }
10363
10474
  });
10364
10475
 
@@ -11402,19 +11513,15 @@ import path25 from "path";
11402
11513
  import os22 from "os";
11403
11514
  import stringWidth2 from "string-width";
11404
11515
  function claudeModelPrice(model) {
11405
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
11406
- for (const [key, p] of Object.entries(CLAUDE_PRICING)) {
11407
- if (base === key || base.startsWith(key)) return p;
11408
- }
11409
- return null;
11516
+ const t = pricingFor(model);
11517
+ if (!t) return null;
11518
+ const [i, o, cw, cr] = t;
11519
+ return { i, o, cw, cr };
11410
11520
  }
11411
11521
  function geminiModelPrice(model) {
11412
- const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
11413
- for (const [key, p] of Object.entries(GEMINI_PRICING)) {
11414
- if (base === key || base.startsWith(key)) return p;
11415
- }
11416
- if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
11417
- return null;
11522
+ const p = geminiPriceFor(model);
11523
+ if (!p) return null;
11524
+ return { i: p.input, o: p.output, cr: p.cacheRead };
11418
11525
  }
11419
11526
  function isNode9SelfOutput(text) {
11420
11527
  let hits = 0;
@@ -12060,14 +12167,17 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
12060
12167
  if (!fs23.existsSync(chatsDir)) continue;
12061
12168
  let chatFiles;
12062
12169
  try {
12063
- chatFiles = fs23.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
12170
+ chatFiles = fs23.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
12064
12171
  } catch {
12065
12172
  continue;
12066
12173
  }
12174
+ const seenSessions = /* @__PURE__ */ new Set();
12067
12175
  for (const chatFile of chatFiles) {
12176
+ const sessionId = chatFile.replace(/\.jsonl?$/, "");
12177
+ if (seenSessions.has(sessionId)) continue;
12178
+ seenSessions.add(sessionId);
12068
12179
  result.filesScanned++;
12069
12180
  onProgress?.(result.filesScanned);
12070
- const sessionId = chatFile.replace(/\.json$/, "");
12071
12181
  let raw;
12072
12182
  try {
12073
12183
  raw = fs23.readFileSync(path25.join(chatsDir, chatFile), "utf-8");
@@ -12077,7 +12187,18 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
12077
12187
  const sessionCalls = [];
12078
12188
  let session;
12079
12189
  try {
12080
- session = JSON.parse(raw);
12190
+ if (chatFile.endsWith(".jsonl")) {
12191
+ const messages = raw.split("\n").filter((l) => l.trim()).map((l) => {
12192
+ try {
12193
+ return JSON.parse(l);
12194
+ } catch {
12195
+ return null;
12196
+ }
12197
+ }).filter((m) => m !== null);
12198
+ session = { messages };
12199
+ } else {
12200
+ session = JSON.parse(raw);
12201
+ }
12081
12202
  } catch {
12082
12203
  continue;
12083
12204
  }
@@ -12687,6 +12808,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12687
12808
  let lastTotalInput = 0;
12688
12809
  let lastTotalCached = 0;
12689
12810
  let lastTotalOutput = 0;
12811
+ let model = "";
12690
12812
  for (const line of lines) {
12691
12813
  if (!line.trim()) continue;
12692
12814
  onLine?.();
@@ -12704,6 +12826,10 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12704
12826
  projLabel = stripTerminalEscapes(cwd.replace(os22.homedir(), "~")).slice(0, 40);
12705
12827
  continue;
12706
12828
  }
12829
+ if (entry.type === "turn_context" && typeof payload["model"] === "string") {
12830
+ model = payload["model"];
12831
+ continue;
12832
+ }
12707
12833
  if (entry.type === "event_msg" && payload["type"] === "token_count") {
12708
12834
  const info = payload["info"];
12709
12835
  const usage = info?.["total_token_usage"] ?? {};
@@ -12847,8 +12973,11 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12847
12973
  }
12848
12974
  }
12849
12975
  }
12850
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
12851
- result.totalCostUSD += nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
12976
+ result.totalCostUSD += codexSessionCost(model, {
12977
+ input: lastTotalInput,
12978
+ cached: lastTotalCached,
12979
+ output: lastTotalOutput
12980
+ });
12852
12981
  result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "codex"));
12853
12982
  }
12854
12983
  return result;
@@ -13895,7 +14024,7 @@ function registerScanCommand(program2) {
13895
14024
  }
13896
14025
  );
13897
14026
  }
13898
- var CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
14027
+ var CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
13899
14028
  var init_scan = __esm({
13900
14029
  "src/cli/commands/scan.ts"() {
13901
14030
  "use strict";
@@ -13904,6 +14033,9 @@ var init_scan = __esm({
13904
14033
  init_policy();
13905
14034
  init_dist();
13906
14035
  init_dlp();
14036
+ init_litellm();
14037
+ init_cost_gemini();
14038
+ init_cost_codex();
13907
14039
  init_hook_payload();
13908
14040
  init_dist();
13909
14041
  init_scan_summary();
@@ -13913,26 +14045,6 @@ var init_scan = __esm({
13913
14045
  init_protection();
13914
14046
  init_scan_json();
13915
14047
  init_scan_history();
13916
- CLAUDE_PRICING = {
13917
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13918
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13919
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
13920
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13921
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13922
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13923
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13924
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13925
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
13926
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
13927
- };
13928
- GEMINI_PRICING = {
13929
- "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
13930
- "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
13931
- "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
13932
- "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
13933
- "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
13934
- "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
13935
- };
13936
14048
  CODE_EXTENSIONS = /* @__PURE__ */ new Set([
13937
14049
  ".ts",
13938
14050
  ".tsx",
@@ -20108,6 +20220,7 @@ import chalk13 from "chalk";
20108
20220
  // src/cli/aggregate/report-audit.ts
20109
20221
  init_costSync();
20110
20222
  init_litellm();
20223
+ init_cost_codex();
20111
20224
  import fs40 from "fs";
20112
20225
  import os36 from "os";
20113
20226
  import path41 from "path";
@@ -20207,24 +20320,11 @@ function isAllow(decision) {
20207
20320
  function isDlp(checkedBy) {
20208
20321
  return !!checkedBy?.includes("dlp");
20209
20322
  }
20210
- var CLAUDE_PRICING2 = {
20211
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
20212
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
20213
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
20214
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20215
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20216
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20217
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20218
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20219
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
20220
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
20221
- };
20222
20323
  function claudeModelPrice2(model) {
20223
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
20224
- for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
20225
- if (base === key || base.startsWith(key + "-") || base.startsWith(key)) return p;
20226
- }
20227
- return null;
20324
+ const t = pricingFor(model);
20325
+ if (!t) return null;
20326
+ const [i, o, cw, cr] = t;
20327
+ return { i, o, cw, cr };
20228
20328
  }
20229
20329
  function emptyClaudeCostAccumulator() {
20230
20330
  return {
@@ -20339,6 +20439,7 @@ function processCodexCostFile(filePath, start, end, acc) {
20339
20439
  return;
20340
20440
  }
20341
20441
  let sessionStart2 = "";
20442
+ let model = "";
20342
20443
  let lastTotalInput = 0;
20343
20444
  let lastTotalCached = 0;
20344
20445
  let lastTotalOutput = 0;
@@ -20356,6 +20457,10 @@ function processCodexCostFile(filePath, start, end, acc) {
20356
20457
  sessionStart2 = String(p["timestamp"] ?? "");
20357
20458
  continue;
20358
20459
  }
20460
+ if (entry.type === "turn_context" && typeof p["model"] === "string") {
20461
+ model = p["model"];
20462
+ continue;
20463
+ }
20359
20464
  if (entry.type === "event_msg" && p["type"] === "token_count") {
20360
20465
  const info = p["info"] ?? {};
20361
20466
  const usage = info["total_token_usage"] ?? {};
@@ -20370,12 +20475,17 @@ function processCodexCostFile(filePath, start, end, acc) {
20370
20475
  if (!sessionStart2) return;
20371
20476
  const ts = new Date(sessionStart2);
20372
20477
  if (ts < start || ts > end) return;
20373
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
20374
- const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
20478
+ const cost = codexSessionCost(model, {
20479
+ input: lastTotalInput,
20480
+ cached: lastTotalCached,
20481
+ output: lastTotalOutput
20482
+ });
20375
20483
  acc.total += cost;
20376
20484
  acc.toolCalls += sessionToolCalls;
20377
20485
  const dateKey = sessionStart2.slice(0, 10);
20378
20486
  acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
20487
+ const normModel = normalizeModel(model || "gpt-5");
20488
+ acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
20379
20489
  }
20380
20490
  function listCodexSessionFiles2(sessionsBase) {
20381
20491
  const jsonlFiles = [];
@@ -20413,13 +20523,25 @@ function listCodexSessionFiles2(sessionsBase) {
20413
20523
  }
20414
20524
  return jsonlFiles;
20415
20525
  }
20526
+ function mergeByModel(...maps) {
20527
+ const out = /* @__PURE__ */ new Map();
20528
+ for (const m of maps) {
20529
+ for (const [k, v] of m) out.set(k, (out.get(k) ?? 0) + v);
20530
+ }
20531
+ return out;
20532
+ }
20416
20533
  function loadCodexCost(start, end, sessionsBase) {
20417
- const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
20534
+ const acc = {
20535
+ total: 0,
20536
+ toolCalls: 0,
20537
+ byDay: /* @__PURE__ */ new Map(),
20538
+ byModel: /* @__PURE__ */ new Map()
20539
+ };
20418
20540
  const files = listCodexSessionFiles2(sessionsBase);
20419
20541
  for (const filePath of files) {
20420
20542
  processCodexCostFile(filePath, start, end, acc);
20421
20543
  }
20422
- return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
20544
+ return { total: acc.total, byDay: acc.byDay, byModel: acc.byModel, toolCalls: acc.toolCalls };
20423
20545
  }
20424
20546
  var GEMINI_FALLBACK_MODELS2 = ["gemini-2.5-flash", "gemini-2.0-flash"];
20425
20547
  function geminiPriceFor2(model) {
@@ -20708,7 +20830,7 @@ function aggregateReportFromAudit(period, opts = {}) {
20708
20830
  cacheWriteTokens: claudeCost.cacheWriteTokens,
20709
20831
  cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
20710
20832
  byDay: claudeCost.byDay,
20711
- byModel: claudeCost.byModel,
20833
+ byModel: mergeByModel(claudeCost.byModel, codexCost.byModel),
20712
20834
  byProject: claudeCost.byProject
20713
20835
  },
20714
20836
  toolMap,
@@ -21977,6 +22099,8 @@ function registerUndoCommand(program2) {
21977
22099
 
21978
22100
  // src/mcp-gateway/index.ts
21979
22101
  init_orchestrator();
22102
+ init_cloud();
22103
+ init_config();
21980
22104
  import readline4 from "readline";
21981
22105
  import chalk19 from "chalk";
21982
22106
  import { spawn as spawn7 } from "child_process";
@@ -22009,6 +22133,60 @@ function normalizeClientName(name) {
22009
22133
  const sanitized = sanitize4(name).slice(0, 40);
22010
22134
  return sanitized.length > 0 ? sanitized : void 0;
22011
22135
  }
22136
+ function reportPinMismatchToCloud(serverKey, agent) {
22137
+ try {
22138
+ const creds = getCredentials();
22139
+ if (!creds) return;
22140
+ void auditLocalAllow(
22141
+ `mcp-server:${serverKey}`,
22142
+ { serverKey, reason: "tool-pin-mismatch" },
22143
+ "mcp-pin-mismatch",
22144
+ creds,
22145
+ { mcpServer: serverKey, agent },
22146
+ void 0,
22147
+ false,
22148
+ {
22149
+ ruleName: "MCP tool definitions changed (possible rug pull)",
22150
+ ruleDescription: `The MCP server "${serverKey}" changed its tool definitions since they were pinned. This can indicate a supply-chain attack (tool poisoning). The session was quarantined. Review with: node9 mcp pin update ${serverKey}`
22151
+ }
22152
+ );
22153
+ } catch {
22154
+ }
22155
+ }
22156
+ function reportInventoryToCloud(serverKey, toolCount, agent) {
22157
+ try {
22158
+ const creds = getCredentials();
22159
+ if (!creds) return;
22160
+ void auditLocalAllow(
22161
+ `mcp-server:${serverKey}`,
22162
+ { serverKey, toolCount },
22163
+ "mcp-discovered",
22164
+ creds,
22165
+ { mcpServer: serverKey, agent },
22166
+ void 0,
22167
+ false,
22168
+ { mcpToolCount: toolCount }
22169
+ );
22170
+ } catch {
22171
+ }
22172
+ }
22173
+ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
22174
+ try {
22175
+ const creds = getCredentials();
22176
+ if (!creds) return;
22177
+ void auditLocalAllow(
22178
+ `mcp-server:${serverKey}`,
22179
+ { serverKey, responseBytes },
22180
+ "mcp-large-response",
22181
+ creds,
22182
+ { mcpServer: serverKey, agent },
22183
+ void 0,
22184
+ false,
22185
+ { mcpResponseBytes: responseBytes }
22186
+ );
22187
+ } catch {
22188
+ }
22189
+ }
22012
22190
  function tokenize4(cmd) {
22013
22191
  const tokens = [];
22014
22192
  let current = "";
@@ -22269,6 +22447,7 @@ async function runMcpGateway(upstreamCommand) {
22269
22447
  const currentHash = hashToolDefinitions(tools);
22270
22448
  const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
22271
22449
  const token = getInternalToken();
22450
+ reportInventoryToCloud(serverKey, tools.length, clientName);
22272
22451
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
22273
22452
  const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
22274
22453
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
@@ -22333,6 +22512,7 @@ async function runMcpGateway(upstreamCommand) {
22333
22512
  console.error(chalk19.red(" Session quarantined \u2014 all tool calls blocked."));
22334
22513
  console.error(chalk19.yellow(` Run: node9 mcp pin update ${serverKey}
22335
22514
  `));
22515
+ reportPinMismatchToCloud(serverKey, clientName);
22336
22516
  const errorResponse = {
22337
22517
  jsonrpc: "2.0",
22338
22518
  id: parsed.id,
@@ -22378,6 +22558,7 @@ async function runMcpGateway(upstreamCommand) {
22378
22558
  `\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
22379
22559
  )
22380
22560
  );
22561
+ reportLargeResponseToCloud(serverKey, line.length, clientName);
22381
22562
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
22382
22563
  const token = getInternalToken();
22383
22564
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/large-response`, {
@@ -23477,44 +23658,23 @@ init_scan();
23477
23658
 
23478
23659
  // src/cli/commands/sessions.ts
23479
23660
  init_scan_summary();
23661
+ init_litellm();
23662
+ init_cost_gemini();
23663
+ init_cost_codex();
23480
23664
  import chalk24 from "chalk";
23481
23665
  import fs45 from "fs";
23482
23666
  import path46 from "path";
23483
23667
  import os40 from "os";
23484
- var CLAUDE_PRICING3 = {
23485
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
23486
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
23487
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
23488
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23489
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23490
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23491
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23492
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23493
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
23494
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
23495
- };
23496
23668
  function modelPrice(model) {
23497
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
23498
- for (const [key, p] of Object.entries(CLAUDE_PRICING3)) {
23499
- if (base === key || base.startsWith(key)) return p;
23500
- }
23501
- return null;
23669
+ const t = pricingFor(model);
23670
+ if (!t) return null;
23671
+ const [i, o, cw, cr] = t;
23672
+ return { i, o, cw, cr };
23502
23673
  }
23503
- var GEMINI_PRICING2 = {
23504
- "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
23505
- "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
23506
- "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
23507
- "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
23508
- "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
23509
- "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
23510
- };
23511
23674
  function geminiModelPrice2(model) {
23512
- const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
23513
- for (const [key, p] of Object.entries(GEMINI_PRICING2)) {
23514
- if (base === key || base.startsWith(key)) return p;
23515
- }
23516
- if (base.includes("flash")) return GEMINI_PRICING2["gemini-2.0-flash"];
23517
- return null;
23675
+ const p = geminiPriceFor(model);
23676
+ if (!p) return null;
23677
+ return { i: p.input, o: p.output, cr: p.cacheRead };
23518
23678
  }
23519
23679
  function encodeProjectPath(projectPath) {
23520
23680
  return projectPath.replace(/\//g, "-");
@@ -23807,6 +23967,7 @@ function buildCodexSessions(days, allAuditEntries) {
23807
23967
  let lastTotalInput = 0;
23808
23968
  let lastTotalCached = 0;
23809
23969
  let lastTotalOutput = 0;
23970
+ let model = "";
23810
23971
  for (const line of lines) {
23811
23972
  if (!line.trim()) continue;
23812
23973
  let entry;
@@ -23822,6 +23983,10 @@ function buildCodexSessions(days, allAuditEntries) {
23822
23983
  cwd = String(p["cwd"] ?? "");
23823
23984
  continue;
23824
23985
  }
23986
+ if (entry.type === "turn_context" && typeof p["model"] === "string") {
23987
+ model = p["model"];
23988
+ continue;
23989
+ }
23825
23990
  if (entry.type === "event_msg" && p["type"] === "user_message" && !firstPrompt) {
23826
23991
  firstPrompt = String(p["message"] ?? "");
23827
23992
  continue;
@@ -23848,8 +24013,11 @@ function buildCodexSessions(days, allAuditEntries) {
23848
24013
  }
23849
24014
  if (!sessionId || !startTime) continue;
23850
24015
  if (cutoff && new Date(startTime) < cutoff) continue;
23851
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
23852
- const costUSD = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
24016
+ const costUSD = codexSessionCost(model, {
24017
+ input: lastTotalInput,
24018
+ cached: lastTotalCached,
24019
+ output: lastTotalOutput
24020
+ });
23853
24021
  const windowEnd = new Date(
23854
24022
  Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
23855
24023
  ).toISOString();
@@ -23873,11 +24041,10 @@ function buildCodexSessions(days, allAuditEntries) {
23873
24041
  }
23874
24042
  function buildSessions(days, historyPath) {
23875
24043
  const hPath = historyPath ?? path46.join(os40.homedir(), ".claude", "history.jsonl");
23876
- let historyRaw;
24044
+ let historyRaw = "";
23877
24045
  try {
23878
24046
  historyRaw = fs45.readFileSync(hPath, "utf-8");
23879
24047
  } catch {
23880
- return [];
23881
24048
  }
23882
24049
  const cutoff = days !== null ? (() => {
23883
24050
  const d = /* @__PURE__ */ new Date();
@@ -24168,12 +24335,6 @@ function registerSessionsCommand(program2) {
24168
24335
  console.log("");
24169
24336
  console.log(chalk24.cyan.bold("\u{1F4CB} node9 sessions") + chalk24.dim(" \u2014 what your AI agent did"));
24170
24337
  console.log("");
24171
- const historyPath = path46.join(os40.homedir(), ".claude", "history.jsonl");
24172
- if (!fs45.existsSync(historyPath)) {
24173
- console.log(chalk24.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
24174
- console.log(chalk24.gray(" Install Claude Code, run a few sessions, then try again.\n"));
24175
- return;
24176
- }
24177
24338
  const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
24178
24339
  const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
24179
24340
  console.log(chalk24.dim(" " + rangeLabel));