@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.js CHANGED
@@ -5889,6 +5889,57 @@ function validateApiUrl(raw) {
5889
5889
  }
5890
5890
  return null;
5891
5891
  }
5892
+ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
5893
+ const validated = validateApiUrl(creds.apiUrl);
5894
+ if (!validated) {
5895
+ try {
5896
+ import_fs10.default.appendFileSync(
5897
+ HOOK_DEBUG_LOG,
5898
+ `[audit] refused to send: invalid apiUrl scheme/host (got "${String(creds.apiUrl).slice(0, 200)}")
5899
+ `
5900
+ );
5901
+ } catch {
5902
+ }
5903
+ return Promise.resolve();
5904
+ }
5905
+ const safeArgs = containsSensitiveArgs ? { tool: toolName, redacted: true } : args;
5906
+ const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
5907
+ const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
5908
+ const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
5909
+ const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
5910
+ Object.entries(riskMetadata).filter(
5911
+ ([, v]) => typeof v === "string" && v.length > 0 || typeof v === "number" && Number.isFinite(v)
5912
+ )
5913
+ ) : void 0;
5914
+ const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
5915
+ return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
5916
+ method: "POST",
5917
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
5918
+ body: JSON.stringify({
5919
+ toolName,
5920
+ args: safeArgs,
5921
+ checkedBy: safeCheckedBy,
5922
+ ...dlpInfo && { dlpPattern, dlpSample },
5923
+ ...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
5924
+ // session_id (Claude Code + Gemini CLI) groups all audit rows from one
5925
+ // agent run; transcript_path is the authoritative pointer to the
5926
+ // session log (survives Gemini resume drift). Both optional —
5927
+ // unsupported agents (MCP-mediated) leave them undefined.
5928
+ ...meta?.sessionId && { runId: meta.sessionId },
5929
+ ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
5930
+ context: {
5931
+ agent: meta?.agent,
5932
+ mcpServer: meta?.mcpServer,
5933
+ hostname: import_os9.default.hostname(),
5934
+ cwd: process.cwd(),
5935
+ platform: import_os9.default.platform()
5936
+ }
5937
+ }),
5938
+ signal: AbortSignal.timeout(5e3)
5939
+ }).then(() => {
5940
+ }).catch(() => {
5941
+ });
5942
+ }
5892
5943
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
5893
5944
  const controller = new AbortController();
5894
5945
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -6012,7 +6063,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
6012
6063
  );
6013
6064
  }
6014
6065
  }
6015
- var import_fs10, import_os9, import_path12;
6066
+ var import_fs10, import_os9, import_path12, DLP_SAMPLE_MAX_LEN, DLP_PATTERN_MAX_LEN, KNOWN_CHECKED_BY;
6016
6067
  var init_cloud = __esm({
6017
6068
  "src/auth/cloud.ts"() {
6018
6069
  "use strict";
@@ -6020,6 +6071,39 @@ var init_cloud = __esm({
6020
6071
  import_os9 = __toESM(require("os"));
6021
6072
  import_path12 = __toESM(require("path"));
6022
6073
  init_audit();
6074
+ DLP_SAMPLE_MAX_LEN = 200;
6075
+ DLP_PATTERN_MAX_LEN = 100;
6076
+ KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
6077
+ "dlp-block",
6078
+ "observe-mode-dlp-would-block",
6079
+ "dlp-review-flagged",
6080
+ "loop-detected",
6081
+ "audit-mode",
6082
+ "local-policy",
6083
+ "smart-rule-block",
6084
+ // Smart-rule block was downgraded to review because the daemon was
6085
+ // running and we're not in CI. The block attempt is still recorded;
6086
+ // the user got a popup. Distinct from 'smart-rule-block' so the
6087
+ // dashboard can show "block rule overridden" separately from a hard
6088
+ // block that fired with no human in the loop.
6089
+ "smart-rule-block-override",
6090
+ "persistent",
6091
+ "trust",
6092
+ "observe-mode",
6093
+ "observe-mode-would-block",
6094
+ // MCP supply-chain: the gateway pinned a server's tool definitions and they
6095
+ // changed since (possible tool poisoning / rug pull). Emitted as a synthetic
6096
+ // audit row so the SaaS surfaces it as a blocked event. The firewall maps
6097
+ // this checkedBy to AUTO_BLOCKED. See doc/roadmap/active/saas-value-first.md
6098
+ // (workstream B-Tier2).
6099
+ "mcp-pin-mismatch",
6100
+ // MCP visibility (B-Tier2, informational — NOT blocks): the gateway
6101
+ // discovered a server's tool inventory (mcp-discovered) or saw an oversized
6102
+ // tool response that bloats the context window (mcp-large-response). Stored
6103
+ // AUTO_ALLOWED; carries mcpToolCount / mcpResponseBytes in riskMetadata.
6104
+ "mcp-discovered",
6105
+ "mcp-large-response"
6106
+ ]);
6023
6107
  }
6024
6108
  });
6025
6109
 
@@ -9278,1114 +9362,1140 @@ var init_setup = __esm({
9278
9362
  }
9279
9363
  });
9280
9364
 
9281
- // src/utils/hook-payload.ts
9282
- function extractToolName(payload, defaultValue = "") {
9283
- return payload.tool_name ?? payload.name ?? payload.toolCall?.name ?? defaultValue;
9365
+ // src/pricing/litellm.ts
9366
+ function normalizeModel(raw) {
9367
+ return raw.replace(/-\d{8}$/, "").toLowerCase();
9284
9368
  }
9285
- function extractToolInput(payload) {
9286
- return payload.tool_input ?? payload.args ?? payload.toolCall?.args ?? {};
9369
+ function readCache() {
9370
+ try {
9371
+ const raw = JSON.parse(import_fs14.default.readFileSync(CACHE_FILE(), "utf-8"));
9372
+ if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
9373
+ return null;
9374
+ }
9375
+ const ageMs = Date.now() - new Date(raw.fetchedAt).getTime();
9376
+ if (ageMs < 0 || ageMs > TTL_MS) return null;
9377
+ return raw.prices;
9378
+ } catch {
9379
+ return null;
9380
+ }
9287
9381
  }
9288
- function canonicalToolName(name) {
9289
- switch (name) {
9290
- // Hermes Agent
9291
- case "terminal":
9292
- return "Bash";
9293
- case "write_file":
9294
- return "Write";
9295
- case "patch":
9296
- return "Edit";
9297
- case "read_file":
9298
- return "Read";
9299
- case "search_files":
9300
- return "Grep";
9301
- // Antigravity (agy) — shell tool renamed from Gemini's run_shell_command
9302
- case "run_command":
9303
- return "Bash";
9304
- default:
9305
- return name;
9382
+ function writeCache(prices) {
9383
+ try {
9384
+ const target = CACHE_FILE();
9385
+ const dir = import_path16.default.dirname(target);
9386
+ if (!import_fs14.default.existsSync(dir)) import_fs14.default.mkdirSync(dir, { recursive: true });
9387
+ const tmp = target + ".tmp";
9388
+ const body = {
9389
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
9390
+ prices
9391
+ };
9392
+ import_fs14.default.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
9393
+ import_fs14.default.renameSync(tmp, target);
9394
+ } catch (err2) {
9395
+ try {
9396
+ import_fs14.default.appendFileSync(
9397
+ HOOK_DEBUG_LOG,
9398
+ `[pricing] cache write failed: ${err2.message}
9399
+ `
9400
+ );
9401
+ } catch {
9402
+ }
9306
9403
  }
9307
9404
  }
9308
- function agentLabelFromFlag(flag) {
9309
- if (typeof flag !== "string") return void 0;
9310
- switch (flag.toLowerCase()) {
9311
- case "antigravity":
9312
- case "agy":
9313
- return "Antigravity";
9314
- case "copilot":
9315
- return "GitHub Copilot";
9316
- default:
9317
- return void 0;
9405
+ function tupleFromLiteLLM(entry) {
9406
+ if (!entry || typeof entry !== "object") return null;
9407
+ const e = entry;
9408
+ const num3 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
9409
+ const inCost = num3(e.input_cost_per_token);
9410
+ const outCost = num3(e.output_cost_per_token);
9411
+ if (inCost === 0 && outCost === 0) return null;
9412
+ return [
9413
+ inCost,
9414
+ outCost,
9415
+ num3(e.cache_creation_input_token_cost),
9416
+ num3(e.cache_read_input_token_cost)
9417
+ ];
9418
+ }
9419
+ async function fetchLiteLLMPricing() {
9420
+ try {
9421
+ const res = await fetch(LITELLM_URL, {
9422
+ signal: AbortSignal.timeout(15e3)
9423
+ });
9424
+ if (!res.ok) return null;
9425
+ const json = await res.json();
9426
+ if (!json || typeof json !== "object") return null;
9427
+ const out = {};
9428
+ for (const [key, value] of Object.entries(json)) {
9429
+ const tuple = tupleFromLiteLLM(value);
9430
+ if (tuple) out[key.toLowerCase()] = tuple;
9431
+ }
9432
+ if (Object.keys(out).length < 10) {
9433
+ return null;
9434
+ }
9435
+ return out;
9436
+ } catch {
9437
+ return null;
9318
9438
  }
9319
9439
  }
9320
- function canonicalToolInput(rawToolName, input) {
9321
- if (rawToolName !== "run_command") return input;
9322
- if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
9323
- const args = input;
9324
- if (typeof args.CommandLine !== "string") return input;
9325
- const { CommandLine, Cwd, ...rest } = args;
9326
- const canonical = { ...rest, command: CommandLine };
9327
- if (typeof Cwd === "string" && Cwd.length > 0) canonical.cwd = Cwd;
9328
- return canonical;
9440
+ async function ensurePricingLoaded() {
9441
+ if (memCache !== null && Date.now() - memCacheAt < TTL_MS) return;
9442
+ const fromDisk = readCache();
9443
+ if (fromDisk && Object.keys(fromDisk).length > 0) {
9444
+ memCache = fromDisk;
9445
+ memCacheAt = Date.now();
9446
+ lookupCache.clear();
9447
+ return;
9448
+ }
9449
+ const fetched = await fetchLiteLLMPricing();
9450
+ if (fetched && Object.keys(fetched).length > 0) {
9451
+ memCache = fetched;
9452
+ memCacheAt = Date.now();
9453
+ writeCache(fetched);
9454
+ lookupCache.clear();
9455
+ return;
9456
+ }
9457
+ memCache = { ...BUNDLED_PRICING };
9458
+ memCacheAt = Date.now();
9459
+ lookupCache.clear();
9329
9460
  }
9330
- var init_hook_payload = __esm({
9331
- "src/utils/hook-payload.ts"() {
9461
+ function pricingFor(model) {
9462
+ const norm = normalizeModel(model);
9463
+ const cached = lookupCache.get(norm);
9464
+ if (cached !== void 0) return cached;
9465
+ if (memCache === null && !diskChecked) {
9466
+ diskChecked = true;
9467
+ const disk = readCache();
9468
+ if (disk && Object.keys(disk).length > 0) {
9469
+ memCache = disk;
9470
+ memCacheAt = Date.now();
9471
+ }
9472
+ }
9473
+ const sources = [];
9474
+ if (memCache) sources.push(memCache);
9475
+ sources.push(BUNDLED_PRICING);
9476
+ let resolved = null;
9477
+ for (const source of sources) {
9478
+ const exact = source[norm];
9479
+ if (exact) {
9480
+ resolved = exact;
9481
+ break;
9482
+ }
9483
+ let best = null;
9484
+ for (const key of Object.keys(source)) {
9485
+ if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
9486
+ best = key;
9487
+ }
9488
+ }
9489
+ if (best) {
9490
+ resolved = source[best];
9491
+ break;
9492
+ }
9493
+ }
9494
+ lookupCache.set(norm, resolved);
9495
+ return resolved;
9496
+ }
9497
+ var import_fs14, import_path16, import_os13, LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, diskChecked, lookupCache;
9498
+ var init_litellm = __esm({
9499
+ "src/pricing/litellm.ts"() {
9332
9500
  "use strict";
9501
+ import_fs14 = __toESM(require("fs"));
9502
+ import_path16 = __toESM(require("path"));
9503
+ import_os13 = __toESM(require("os"));
9504
+ init_audit();
9505
+ LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
9506
+ BUNDLED_PRICING = {
9507
+ // Anthropic
9508
+ "claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
9509
+ "claude-opus-4-1": [5e-6, 25e-6, 625e-8, 5e-7],
9510
+ "claude-opus-4-5": [5e-6, 25e-6, 625e-8, 5e-7],
9511
+ "claude-opus-4-6": [5e-6, 25e-6, 625e-8, 5e-7],
9512
+ "claude-opus-4-7": [5e-6, 25e-6, 625e-8, 5e-7],
9513
+ "claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
9514
+ "claude-sonnet-4-5": [3e-6, 15e-6, 375e-8, 3e-7],
9515
+ "claude-sonnet-4-6": [3e-6, 15e-6, 375e-8, 3e-7],
9516
+ "claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
9517
+ "claude-haiku-4-5": [8e-7, 4e-6, 1e-6, 8e-8],
9518
+ "claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
9519
+ "claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
9520
+ "claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
9521
+ "claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
9522
+ // OpenAI. gpt-5 family + o-series copied from the live LiteLLM table
9523
+ // (verified 2026-06-14) — the bundled gpt-5 was stale at $10/$30 vs the real
9524
+ // $1.25/$10, and Codex models (gpt-5-codex etc.) were absent, so the offline
9525
+ // fallback mispriced every Codex session. See cost-codex.codexPriceFor.
9526
+ "gpt-4o": [5e-6, 15e-6, 0, 25e-7],
9527
+ "gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
9528
+ "gpt-5": [125e-8, 1e-5, 0, 125e-9],
9529
+ "gpt-5-codex": [125e-8, 1e-5, 0, 125e-9],
9530
+ "gpt-5-mini": [25e-8, 2e-6, 0, 25e-9],
9531
+ o3: [2e-6, 8e-6, 0, 5e-7],
9532
+ "o4-mini": [11e-7, 44e-7, 0, 275e-9],
9533
+ // Google. Values copied from the live LiteLLM table (verified 2026-06-14)
9534
+ // so the bundled fallback prices the current Gemini tiers correctly offline
9535
+ // — the local cost readers were carrying a stale hardcoded copy where
9536
+ // gemini-2.5-flash read $0.15/$0.60 vs the real $0.30/$2.50 (~4× under on
9537
+ // output). See cost-gemini.geminiPriceFor (the single Gemini price source).
9538
+ "gemini-2.5-pro": [125e-8, 1e-5, 0, 125e-9],
9539
+ "gemini-2.5-flash": [3e-7, 25e-7, 0, 3e-8],
9540
+ "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
9541
+ "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
9542
+ };
9543
+ CACHE_FILE = () => import_path16.default.join(import_os13.default.homedir(), ".node9", "model-pricing.json");
9544
+ TTL_MS = 24 * 60 * 60 * 1e3;
9545
+ memCache = null;
9546
+ memCacheAt = 0;
9547
+ diskChecked = false;
9548
+ lookupCache = /* @__PURE__ */ new Map();
9333
9549
  }
9334
9550
  });
9335
9551
 
9336
- // src/scan-summary.ts
9337
- function agentDisplayName(agent) {
9338
- return AGENT_LONG[agent] ?? "Claude Code";
9552
+ // src/cost-gemini.ts
9553
+ function geminiTmpDir() {
9554
+ return import_path17.default.join(import_os14.default.homedir(), ".gemini", "tmp");
9339
9555
  }
9340
- function agentBadgeText(agent, width = 10) {
9341
- return `[${AGENT_SHORT[agent] ?? "Claude"}]`.padEnd(width);
9556
+ function geminiPriceFor(model) {
9557
+ let tuple = pricingFor(model);
9558
+ if (!tuple && /^gemini-/i.test(model)) {
9559
+ for (const proxy of GEMINI_FALLBACK_MODELS) {
9560
+ tuple = pricingFor(proxy);
9561
+ if (tuple) break;
9562
+ }
9563
+ }
9564
+ if (!tuple) return null;
9565
+ return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
9342
9566
  }
9343
- function agentColorName(agent) {
9344
- switch (agent) {
9345
- case "gemini":
9346
- return "blue";
9347
- case "codex":
9348
- return "magenta";
9349
- case "antigravity":
9350
- return "yellow";
9351
- case "copilot":
9352
- return "green";
9353
- case "shell":
9354
- return "yellow";
9355
- default:
9356
- return "cyan";
9567
+ function safeReaddir(dir) {
9568
+ try {
9569
+ return import_fs15.default.readdirSync(dir);
9570
+ } catch {
9571
+ return [];
9357
9572
  }
9358
9573
  }
9359
- function buildScanSummary(agents) {
9360
- const stats = {
9361
- sessions: 0,
9362
- totalToolCalls: 0,
9363
- bashCalls: 0,
9364
- totalCostUSD: 0,
9365
- firstDate: null,
9366
- lastDate: null
9367
- };
9368
- for (const a of agents) {
9369
- stats.sessions += a.scan.sessions;
9370
- stats.totalToolCalls += a.scan.totalToolCalls;
9371
- stats.bashCalls += a.scan.bashCalls;
9372
- stats.totalCostUSD += a.scan.totalCostUSD;
9373
- if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
9374
- stats.firstDate = a.scan.firstDate;
9375
- }
9376
- if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
9377
- stats.lastDate = a.scan.lastDate;
9378
- }
9379
- }
9380
- const allFindings = agents.flatMap((a) => a.scan.findings);
9381
- const allLeaks = agents.flatMap(
9382
- (a) => a.scan.dlpFindings.map((f) => ({
9383
- patternName: f.patternName,
9384
- redactedSample: f.redactedSample,
9385
- toolName: f.toolName,
9386
- timestamp: f.timestamp,
9387
- project: f.project,
9388
- sessionId: f.sessionId,
9389
- agent: f.agent
9390
- }))
9391
- );
9392
- const allLoops = agents.flatMap(
9393
- (a) => a.scan.loopFindings.map((f) => ({
9394
- toolName: f.toolName,
9395
- commandPreview: f.commandPreview,
9396
- count: f.count,
9397
- timestamp: f.timestamp,
9398
- project: f.project,
9399
- sessionId: f.sessionId,
9400
- agent: f.agent,
9401
- kind: f.kind
9402
- }))
9403
- );
9404
- const byVerdict = {
9405
- blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
9406
- supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
9407
- leaks: allLeaks.length,
9408
- loops: allLoops.length
9409
- };
9410
- const byAgent = agents.map((a) => ({
9411
- id: a.id,
9412
- label: a.label,
9413
- icon: a.icon,
9414
- sessions: a.scan.sessions,
9415
- findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
9416
- costUSD: a.scan.totalCostUSD
9417
- })).filter((s) => s.sessions > 0 || s.findings > 0);
9418
- const sections = buildSections(allFindings);
9419
- const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
9420
- const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
9421
- return {
9422
- stats,
9423
- byVerdict,
9424
- byAgent,
9425
- sections,
9426
- leaks: allLeaks,
9427
- loops: allLoops,
9428
- loopWastedUSD
9429
- };
9430
- }
9431
- function buildSections(findings) {
9432
- const sectionMap = /* @__PURE__ */ new Map();
9433
- function ensureSection(id, label, subtitle, sourceType, shieldKey) {
9434
- let s = sectionMap.get(id);
9435
- if (!s) {
9436
- s = {
9437
- id,
9438
- label,
9439
- subtitle,
9440
- sourceType,
9441
- shieldKey,
9442
- blockedCount: 0,
9443
- reviewCount: 0,
9444
- rules: []
9445
- };
9446
- sectionMap.set(id, s);
9447
- }
9448
- return s;
9449
- }
9450
- const ruleMap = /* @__PURE__ */ new Map();
9451
- for (const f of findings) {
9452
- const src = f.source;
9453
- const sourceType = src.sourceType;
9454
- const shieldName = src.shieldName;
9455
- const verdict = src.rule.verdict === "block" ? "block" : "review";
9456
- let sectionId;
9457
- let sectionLabel;
9458
- let sectionSubtitle;
9459
- let shieldKey;
9460
- if (sourceType === "default") {
9461
- sectionId = "default";
9462
- sectionLabel = "Default Rules";
9463
- sectionSubtitle = "built-in, always on";
9464
- } else if (sourceType === "shield") {
9465
- sectionId = `shield:${shieldName}`;
9466
- sectionLabel = shieldName;
9467
- sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
9468
- shieldKey = shieldName;
9469
- } else if (shieldName === "cloud") {
9470
- sectionId = "cloud";
9471
- sectionLabel = "Cloud Policy";
9472
- sectionSubtitle = "synced from node9 cloud";
9473
- } else {
9474
- sectionId = "user";
9475
- sectionLabel = "Your Rules";
9476
- sectionSubtitle = "added in node9.config.json";
9477
- }
9478
- const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
9479
- const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
9480
- const ruleKey = sectionId + "::" + ruleDisplayName;
9481
- let rule = ruleMap.get(ruleKey);
9482
- if (!rule) {
9483
- rule = {
9484
- name: ruleDisplayName,
9485
- verdict,
9486
- reason: src.rule.reason ?? "",
9487
- findings: []
9488
- };
9489
- ruleMap.set(ruleKey, rule);
9490
- section.rules.push(rule);
9491
- }
9492
- const cmdPreview = previewCommand(f.input, 120);
9493
- const fullCmd = fullCommandOf(f.input);
9494
- const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
9495
- if (!isDupe) {
9496
- rule.findings.push({
9497
- timestamp: f.timestamp ?? "",
9498
- command: cmdPreview,
9499
- fullCommand: fullCmd,
9500
- project: f.project,
9501
- sessionId: f.sessionId,
9502
- agent: f.agent,
9503
- toolName: f.toolName
9504
- });
9505
- }
9506
- if (verdict === "block") section.blockedCount++;
9507
- else section.reviewCount++;
9508
- }
9509
- const sections = [...sectionMap.values()];
9510
- sections.sort((a, b) => {
9511
- const aTotal = a.blockedCount + a.reviewCount;
9512
- const bTotal = b.blockedCount + b.reviewCount;
9513
- if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
9514
- return bTotal - aTotal;
9515
- });
9516
- for (const s of sections) {
9517
- s.rules.sort((a, b) => {
9518
- const aBlock = a.verdict === "block" ? 1 : 0;
9519
- const bBlock = b.verdict === "block" ? 1 : 0;
9520
- if (bBlock !== aBlock) return bBlock - aBlock;
9521
- return b.findings.length - a.findings.length;
9522
- });
9523
- }
9524
- return sections;
9525
- }
9526
- function previewCommand(input, max) {
9527
- const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9528
- const s = String(raw).replace(/\s+/g, " ").trim();
9529
- return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
9530
- }
9531
- function fullCommandOf(input) {
9532
- const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
9533
- return String(raw).replace(/\s+/g, " ").trim();
9534
- }
9535
- var AGENT_SHORT, AGENT_LONG;
9536
- var init_scan_summary = __esm({
9537
- "src/scan-summary.ts"() {
9538
- "use strict";
9539
- init_shields();
9540
- init_dist();
9541
- init_dist();
9542
- AGENT_SHORT = {
9543
- claude: "Claude",
9544
- gemini: "Gemini",
9545
- codex: "Codex",
9546
- antigravity: "Agy",
9547
- copilot: "Copilot",
9548
- shell: "Shell"
9549
- };
9550
- AGENT_LONG = {
9551
- claude: "Claude Code",
9552
- gemini: "Gemini CLI",
9553
- codex: "Codex",
9554
- antigravity: "Antigravity",
9555
- copilot: "GitHub Copilot",
9556
- shell: "Shell"
9557
- };
9558
- }
9559
- });
9560
-
9561
- // src/cli/commands/blast.ts
9562
- function buildSensitivePaths(home, cwd) {
9563
- return [
9564
- {
9565
- full: import_path16.default.join(home, ".ssh", "id_rsa"),
9566
- label: "~/.ssh/id_rsa",
9567
- description: "RSA private key \u2014 grants SSH access to your servers",
9568
- score: 20
9569
- },
9570
- {
9571
- full: import_path16.default.join(home, ".ssh", "id_ed25519"),
9572
- label: "~/.ssh/id_ed25519",
9573
- description: "Ed25519 private key \u2014 grants SSH access to your servers",
9574
- score: 20
9575
- },
9576
- {
9577
- full: import_path16.default.join(home, ".ssh", "id_ecdsa"),
9578
- label: "~/.ssh/id_ecdsa",
9579
- description: "ECDSA private key \u2014 grants SSH access to your servers",
9580
- score: 20
9581
- },
9582
- {
9583
- full: import_path16.default.join(home, ".aws", "credentials"),
9584
- label: "~/.aws/credentials",
9585
- description: "AWS access keys \u2014 full cloud account access",
9586
- score: 20
9587
- },
9588
- {
9589
- full: import_path16.default.join(home, ".aws", "config"),
9590
- label: "~/.aws/config",
9591
- description: "AWS configuration \u2014 account and region settings",
9592
- score: 5
9593
- },
9594
- {
9595
- full: import_path16.default.join(home, ".config", "gcloud", "credentials.db"),
9596
- label: "~/.config/gcloud/credentials.db",
9597
- description: "Google Cloud credentials",
9598
- score: 15
9599
- },
9600
- {
9601
- full: import_path16.default.join(home, ".docker", "config.json"),
9602
- label: "~/.docker/config.json",
9603
- description: "Docker registry auth tokens",
9604
- score: 10
9605
- },
9606
- {
9607
- full: import_path16.default.join(home, ".netrc"),
9608
- label: "~/.netrc",
9609
- description: "FTP/HTTP credentials in plain text",
9610
- score: 15
9611
- },
9612
- {
9613
- full: import_path16.default.join(home, ".npmrc"),
9614
- label: "~/.npmrc",
9615
- description: "npm auth token \u2014 can publish packages as you",
9616
- score: 10
9617
- },
9618
- {
9619
- full: import_path16.default.join(home, ".node9", "credentials.json"),
9620
- label: "~/.node9/credentials.json",
9621
- description: "Node9 cloud API key",
9622
- score: 10
9623
- },
9624
- {
9625
- full: import_path16.default.join(cwd, ".env"),
9626
- label: ".env (current folder)",
9627
- description: "App secrets \u2014 database passwords, API keys",
9628
- score: 20
9629
- },
9630
- {
9631
- full: import_path16.default.join(cwd, ".env.local"),
9632
- label: ".env.local (current folder)",
9633
- description: "Local overrides \u2014 often contains real credentials",
9634
- score: 15
9635
- },
9636
- {
9637
- full: import_path16.default.join(cwd, ".env.production"),
9638
- label: ".env.production (current folder)",
9639
- description: "Production secrets",
9640
- score: 20
9641
- }
9642
- ];
9643
- }
9644
- function isReadable(filePath) {
9574
+ function isDir(p) {
9645
9575
  try {
9646
- import_fs14.default.accessSync(filePath, import_fs14.default.constants.R_OK);
9647
- return true;
9576
+ return import_fs15.default.statSync(p).isDirectory();
9648
9577
  } catch {
9649
9578
  return false;
9650
9579
  }
9651
9580
  }
9652
- function scoreLabel(score) {
9653
- if (score >= 80) return import_chalk2.default.green(`${score}/100 Good`);
9654
- if (score >= 50) return import_chalk2.default.yellow(`${score}/100 Moderate risk`);
9655
- if (score >= 25) return import_chalk2.default.red(`${score}/100 High risk`);
9656
- return import_chalk2.default.red.bold(`${score}/100 Critical`);
9657
- }
9658
- function runBlast() {
9659
- const home = import_os13.default.homedir();
9660
- const cwd = process.cwd();
9661
- const paths = buildSensitivePaths(home, cwd);
9662
- let scoreDeduction = 0;
9663
- const reachable = [];
9664
- for (const p of paths) {
9665
- if (import_fs14.default.existsSync(p.full) && isReadable(p.full)) {
9666
- reachable.push(p);
9667
- scoreDeduction += p.score;
9668
- }
9669
- }
9670
- const envFindings = [];
9671
- for (const [key, value] of Object.entries(process.env)) {
9672
- if (!value) continue;
9673
- const match = scanArgs({ [key]: value });
9674
- if (match) {
9675
- envFindings.push({ key, patternName: match.patternName });
9676
- scoreDeduction += 10;
9581
+ function listGeminiSessionFiles(base) {
9582
+ const out = [];
9583
+ for (const project of safeReaddir(base)) {
9584
+ const chats = import_path17.default.join(base, project, "chats");
9585
+ if (!isDir(chats)) continue;
9586
+ for (const f of safeReaddir(chats)) {
9587
+ if (f.startsWith("session-") && f.endsWith(".jsonl")) {
9588
+ out.push({ file: import_path17.default.join(chats, f), project });
9589
+ }
9677
9590
  }
9678
9591
  }
9679
- return { reachable, envFindings, score: Math.max(0, 100 - scoreDeduction) };
9592
+ return out;
9680
9593
  }
9681
- function registerBlastCommand(program2) {
9682
- program2.command("blast").description("Map what an AI agent can currently reach on this machine").action(() => {
9683
- const home = import_os13.default.homedir();
9684
- const cwd = process.cwd();
9685
- const { reachable, envFindings, score } = runBlast();
9686
- console.log("");
9687
- console.log(
9688
- import_chalk2.default.bold(" \u{1F52D} Node9 Blast Radius") + import_chalk2.default.dim(" \xB7 what an AI agent can reach from here")
9689
- );
9690
- console.log(import_chalk2.default.dim(" Running in: ") + import_chalk2.default.white(cwd.replace(home, "~")));
9691
- console.log("");
9692
- if (reachable.length > 0) {
9693
- console.log(" " + import_chalk2.default.red.bold("Sensitive files reachable:"));
9694
- for (const p of reachable) {
9695
- console.log(
9696
- " " + import_chalk2.default.red("\u2717 ") + import_chalk2.default.yellow(p.label.padEnd(38)) + import_chalk2.default.dim(p.description)
9697
- );
9698
- }
9699
- console.log("");
9594
+ function parseGeminiSession(lines, project) {
9595
+ const seenIds = /* @__PURE__ */ new Set();
9596
+ const byKey = /* @__PURE__ */ new Map();
9597
+ let runId = "";
9598
+ for (const raw of lines) {
9599
+ if (!raw.trim()) continue;
9600
+ let obj;
9601
+ try {
9602
+ obj = JSON.parse(raw);
9603
+ } catch {
9604
+ continue;
9700
9605
  }
9701
- if (envFindings.length > 0) {
9702
- console.log(" " + import_chalk2.default.red.bold("Secrets in active environment:"));
9703
- for (const f of envFindings) {
9704
- console.log(
9705
- " " + import_chalk2.default.red("\u2717 ") + import_chalk2.default.yellow(f.key.padEnd(38)) + import_chalk2.default.dim(f.patternName)
9706
- );
9707
- }
9708
- console.log("");
9606
+ if (typeof obj.sessionId === "string" && !runId) runId = obj.sessionId;
9607
+ if (!obj.tokens || !obj.model || !obj.timestamp) continue;
9608
+ if (obj.id) {
9609
+ if (seenIds.has(obj.id)) continue;
9610
+ seenIds.add(obj.id);
9709
9611
  }
9710
- console.log(" " + import_chalk2.default.dim("\u2500".repeat(70)));
9711
- if (reachable.length === 0 && envFindings.length === 0) {
9712
- console.log(" " + import_chalk2.default.green("\u2705 No sensitive files or environment secrets found."));
9713
- console.log(" Security Score: " + scoreLabel(score));
9612
+ const price = geminiPriceFor(obj.model);
9613
+ if (!price) continue;
9614
+ const inp = obj.tokens.input ?? 0;
9615
+ const out = obj.tokens.output ?? 0;
9616
+ const cached = Math.min(obj.tokens.cached ?? 0, inp);
9617
+ const fresh = Math.max(0, inp - cached);
9618
+ const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
9619
+ const date = obj.timestamp.slice(0, 10);
9620
+ const model = normalizeModel(obj.model);
9621
+ const key = `${date}::${model}`;
9622
+ const prev = byKey.get(key);
9623
+ if (prev) {
9624
+ prev.costUSD += cost;
9625
+ prev.inputTokens += fresh;
9626
+ prev.outputTokens += out;
9627
+ prev.cacheReadTokens += cached;
9714
9628
  } else {
9715
- console.log(
9716
- " Security Score: " + scoreLabel(score) + import_chalk2.default.dim(
9717
- ` (${reachable.length} file${reachable.length !== 1 ? "s" : ""}, ${envFindings.length} env var${envFindings.length !== 1 ? "s" : ""})`
9718
- )
9719
- );
9720
- console.log("");
9721
- console.log(
9722
- import_chalk2.default.dim(
9723
- " Every AI agent you start can read the files and env vars listed above.\n Run `node9 shield enable project-jail` to restrict agent file access.\n Run `node9 mask` to redact secrets from existing session history."
9724
- )
9725
- );
9629
+ byKey.set(key, {
9630
+ date,
9631
+ model,
9632
+ workingDir: project,
9633
+ runId,
9634
+ costUSD: cost,
9635
+ inputTokens: fresh,
9636
+ outputTokens: out,
9637
+ cacheReadTokens: cached,
9638
+ cacheWriteTokens: 0
9639
+ });
9726
9640
  }
9727
- console.log("");
9728
- });
9641
+ }
9642
+ if (runId) for (const e of byKey.values()) e.runId = runId;
9643
+ return [...byKey.values()];
9729
9644
  }
9730
- var import_chalk2, import_fs14, import_path16, import_os13;
9731
- var init_blast = __esm({
9732
- "src/cli/commands/blast.ts"() {
9645
+ var import_fs15, import_os14, import_path17, GEMINI_FALLBACK_MODELS, geminiSource;
9646
+ var init_cost_gemini = __esm({
9647
+ "src/cost-gemini.ts"() {
9733
9648
  "use strict";
9734
- import_chalk2 = __toESM(require("chalk"));
9735
- import_fs14 = __toESM(require("fs"));
9736
- import_path16 = __toESM(require("path"));
9737
- import_os13 = __toESM(require("os"));
9738
- init_dlp();
9649
+ import_fs15 = __toESM(require("fs"));
9650
+ import_os14 = __toESM(require("os"));
9651
+ import_path17 = __toESM(require("path"));
9652
+ init_litellm();
9653
+ GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
9654
+ geminiSource = {
9655
+ id: "gemini",
9656
+ available() {
9657
+ try {
9658
+ return import_fs15.default.existsSync(geminiTmpDir());
9659
+ } catch {
9660
+ return false;
9661
+ }
9662
+ },
9663
+ collect(sinceMs) {
9664
+ const combined = /* @__PURE__ */ new Map();
9665
+ for (const { file, project } of listGeminiSessionFiles(geminiTmpDir())) {
9666
+ try {
9667
+ if (sinceMs !== void 0 && import_fs15.default.statSync(file).mtimeMs < sinceMs) continue;
9668
+ } catch {
9669
+ continue;
9670
+ }
9671
+ let content;
9672
+ try {
9673
+ content = import_fs15.default.readFileSync(file, "utf8");
9674
+ } catch {
9675
+ continue;
9676
+ }
9677
+ for (const e of parseGeminiSession(content.split("\n"), project)) {
9678
+ const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
9679
+ const prev = combined.get(key);
9680
+ if (prev) {
9681
+ prev.costUSD += e.costUSD;
9682
+ prev.inputTokens += e.inputTokens;
9683
+ prev.outputTokens += e.outputTokens;
9684
+ prev.cacheReadTokens += e.cacheReadTokens;
9685
+ prev.cacheWriteTokens += e.cacheWriteTokens;
9686
+ } else {
9687
+ combined.set(key, { ...e });
9688
+ }
9689
+ }
9690
+ }
9691
+ return [...combined.values()];
9692
+ }
9693
+ };
9739
9694
  }
9740
9695
  });
9741
9696
 
9742
- // src/cli/render/scan-derive.ts
9743
- function classifyScore(score) {
9744
- if (score >= 80) return { band: "good", label: "Good", color: import_chalk3.default.green };
9745
- if (score >= 50) return { band: "at-risk", label: "At Risk", color: import_chalk3.default.yellow };
9746
- return { band: "critical", label: "Critical", color: import_chalk3.default.red };
9747
- }
9748
- function topDlpPatterns(findings, n) {
9749
- const counts = /* @__PURE__ */ new Map();
9750
- for (const f of findings) {
9751
- counts.set(f.patternName, (counts.get(f.patternName) ?? 0) + 1);
9752
- }
9753
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([name, count]) => ({ name, count }));
9697
+ // src/cost-codex.ts
9698
+ function codexSessionsDir() {
9699
+ return import_path18.default.join(import_os15.default.homedir(), ".codex", "sessions");
9754
9700
  }
9755
- function topRulesByVerdict(sections, verdict, n) {
9756
- const matched = [];
9757
- for (const section of sections) {
9758
- for (const rule of section.rules) {
9759
- const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
9760
- if (matches) matched.push({ name: rule.name, count: rule.findings.length });
9761
- }
9762
- }
9763
- return matched.sort((a, b) => b.count - a.count).slice(0, n);
9701
+ function codexPriceFor(model) {
9702
+ return pricingFor(model) ?? CODEX_FALLBACK;
9764
9703
  }
9765
- function computeLoopWaste(loops, totalToolCalls) {
9766
- const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
9767
- const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
9768
- return { wastedCalls, wastePct };
9704
+ function codexSessionCost(model, tokens) {
9705
+ const nonCached = Math.max(0, tokens.input - tokens.cached);
9706
+ const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
9707
+ return nonCached * pin + tokens.cached * pcr + tokens.output * pout;
9769
9708
  }
9770
- function rollupByShield(sections, topRulesPerShield = 3) {
9709
+ function listCodexSessionFiles(base) {
9771
9710
  const out = [];
9772
- for (const section of sections) {
9773
- if (section.sourceType !== "shield") continue;
9774
- if (!section.shieldKey) continue;
9775
- const totalCatches = section.blockedCount + section.reviewCount;
9776
- const topRuleLabels = [...section.rules].sort((a, b) => b.findings.length - a.findings.length).slice(0, topRulesPerShield).map((r) => r.findings.length > 1 ? `${r.name} \xD7${r.findings.length}` : r.name);
9777
- out.push({
9778
- shieldName: section.shieldKey,
9779
- totalCatches,
9780
- blockCatches: section.blockedCount,
9781
- reviewCatches: section.reviewCount,
9782
- topRuleLabels
9783
- });
9711
+ for (const y of safeReaddir2(base)) {
9712
+ const yp = import_path18.default.join(base, y);
9713
+ if (!isDir2(yp)) continue;
9714
+ for (const m of safeReaddir2(yp)) {
9715
+ const mp = import_path18.default.join(yp, m);
9716
+ if (!isDir2(mp)) continue;
9717
+ for (const d of safeReaddir2(mp)) {
9718
+ const dp = import_path18.default.join(mp, d);
9719
+ if (!isDir2(dp)) continue;
9720
+ for (const f of safeReaddir2(dp)) {
9721
+ if (f.endsWith(".jsonl")) out.push(import_path18.default.join(dp, f));
9722
+ }
9723
+ }
9724
+ }
9784
9725
  }
9785
- return out.sort((a, b) => b.totalCatches - a.totalCatches);
9726
+ return out;
9786
9727
  }
9787
- function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
9788
- const inner = width - 4;
9789
- const out = [];
9790
- const titlePad = ` ${title} `;
9791
- const titleWidth = (0, import_string_width.default)(titlePad);
9792
- const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
9793
- const dashFill = "\u2500".repeat(Math.max(0, inner - (0, import_string_width.default)(titleSegment)));
9794
- out.push(import_chalk3.default.dim("\u256D\u2500") + import_chalk3.default.bold(titleSegment) + import_chalk3.default.dim(`${dashFill}\u2500\u256E`));
9795
- for (const line of bodyLines) {
9796
- const padding = " ".repeat(Math.max(0, inner - line.width));
9797
- out.push(import_chalk3.default.dim("\u2502 ") + line.rendered + padding + import_chalk3.default.dim(" \u2502"));
9728
+ function safeReaddir2(dir) {
9729
+ try {
9730
+ return import_fs16.default.readdirSync(dir);
9731
+ } catch {
9732
+ return [];
9798
9733
  }
9799
- out.push(import_chalk3.default.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
9800
- return out;
9801
9734
  }
9802
- function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
9803
- const t = new Date(timestamp).getTime();
9804
- if (Number.isNaN(t)) return "?";
9805
- const days = Math.floor((now.getTime() - t) / 864e5);
9806
- if (days < 1) return "today";
9807
- if (days > 90) return "90d+";
9808
- return `${days}d`;
9735
+ function isDir2(p) {
9736
+ try {
9737
+ return import_fs16.default.statSync(p).isDirectory();
9738
+ } catch {
9739
+ return false;
9740
+ }
9809
9741
  }
9810
- var import_chalk3, import_string_width, PANEL_WIDTH;
9811
- var init_scan_derive = __esm({
9812
- "src/cli/render/scan-derive.ts"() {
9813
- "use strict";
9814
- import_chalk3 = __toESM(require("chalk"));
9815
- import_string_width = __toESM(require("string-width"));
9816
- PANEL_WIDTH = 76;
9742
+ function parseCodexSession(lines) {
9743
+ let sessionStart2 = "";
9744
+ let runId = "";
9745
+ let cwd = "";
9746
+ let model = "";
9747
+ let input = 0;
9748
+ let cached = 0;
9749
+ let output = 0;
9750
+ let sawUsage = false;
9751
+ for (const raw of lines) {
9752
+ if (!raw.trim()) continue;
9753
+ let entry;
9754
+ try {
9755
+ entry = JSON.parse(raw);
9756
+ } catch {
9757
+ continue;
9758
+ }
9759
+ const p = entry.payload ?? {};
9760
+ if (entry.type === "session_meta") {
9761
+ if (!sessionStart2 && typeof p["timestamp"] === "string") sessionStart2 = p["timestamp"];
9762
+ if (!runId && typeof p["id"] === "string") runId = p["id"];
9763
+ if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
9764
+ continue;
9765
+ }
9766
+ if (entry.type === "turn_context") {
9767
+ if (typeof p["model"] === "string") model = p["model"];
9768
+ if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
9769
+ continue;
9770
+ }
9771
+ if (entry.type === "event_msg" && p["type"] === "token_count") {
9772
+ const info = p["info"] ?? {};
9773
+ const usage = info["total_token_usage"] ?? {};
9774
+ if (typeof usage["input_tokens"] === "number") input = usage["input_tokens"];
9775
+ if (typeof usage["cached_input_tokens"] === "number") cached = usage["cached_input_tokens"];
9776
+ if (typeof usage["output_tokens"] === "number") output = usage["output_tokens"];
9777
+ sawUsage = true;
9778
+ }
9817
9779
  }
9818
- });
9819
-
9820
- // src/protection.ts
9821
- var PROTECTIVE_SHIELD_DISCOUNTS;
9822
- var init_protection = __esm({
9823
- "src/protection.ts"() {
9780
+ if (!sessionStart2 || !sawUsage) return null;
9781
+ const nonCached = Math.max(0, input - cached);
9782
+ if (nonCached === 0 && output === 0 && cached === 0) return null;
9783
+ const norm = normalizeModel(model || "gpt-5");
9784
+ const costUSD = codexSessionCost(model, { input, cached, output });
9785
+ return {
9786
+ date: sessionStart2.slice(0, 10),
9787
+ model: norm,
9788
+ workingDir: cwd,
9789
+ runId,
9790
+ costUSD,
9791
+ inputTokens: nonCached,
9792
+ outputTokens: output,
9793
+ cacheReadTokens: cached,
9794
+ cacheWriteTokens: 0
9795
+ };
9796
+ }
9797
+ var import_fs16, import_os15, import_path18, CODEX_FALLBACK, codexSource;
9798
+ var init_cost_codex = __esm({
9799
+ "src/cost-codex.ts"() {
9824
9800
  "use strict";
9825
- PROTECTIVE_SHIELD_DISCOUNTS = {
9826
- "project-jail": 0.7
9801
+ import_fs16 = __toESM(require("fs"));
9802
+ import_os15 = __toESM(require("os"));
9803
+ import_path18 = __toESM(require("path"));
9804
+ init_litellm();
9805
+ CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
9806
+ codexSource = {
9807
+ id: "codex",
9808
+ available() {
9809
+ try {
9810
+ return import_fs16.default.existsSync(codexSessionsDir());
9811
+ } catch {
9812
+ return false;
9813
+ }
9814
+ },
9815
+ collect(sinceMs) {
9816
+ const base = codexSessionsDir();
9817
+ const combined = /* @__PURE__ */ new Map();
9818
+ for (const file of listCodexSessionFiles(base)) {
9819
+ try {
9820
+ if (sinceMs !== void 0 && import_fs16.default.statSync(file).mtimeMs < sinceMs) continue;
9821
+ } catch {
9822
+ continue;
9823
+ }
9824
+ let content;
9825
+ try {
9826
+ content = import_fs16.default.readFileSync(file, "utf8");
9827
+ } catch {
9828
+ continue;
9829
+ }
9830
+ const e = parseCodexSession(content.split("\n"));
9831
+ if (!e) continue;
9832
+ const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
9833
+ const prev = combined.get(key);
9834
+ if (prev) {
9835
+ prev.costUSD += e.costUSD;
9836
+ prev.inputTokens += e.inputTokens;
9837
+ prev.outputTokens += e.outputTokens;
9838
+ prev.cacheReadTokens += e.cacheReadTokens;
9839
+ prev.cacheWriteTokens += e.cacheWriteTokens;
9840
+ } else {
9841
+ combined.set(key, { ...e });
9842
+ }
9843
+ }
9844
+ return [...combined.values()];
9845
+ }
9827
9846
  };
9828
9847
  }
9829
9848
  });
9830
9849
 
9831
- // src/cli/render/scan-json.ts
9832
- function buildScanJson(input) {
9833
- const { summary, blast, isWired, generatedAt } = input;
9834
- const { band } = classifyScore(blast.score);
9835
- return {
9836
- schemaVersion: 1,
9837
- generatedAt,
9838
- isWired,
9839
- score: blast.score,
9840
- band,
9841
- totals: {
9842
- blocked: summary.byVerdict.blocked,
9843
- review: summary.byVerdict.supervised,
9844
- leaks: summary.byVerdict.leaks,
9845
- loops: summary.byVerdict.loops,
9846
- blastExposures: blast.reachable.length + blast.envFindings.length
9847
- },
9848
- summary,
9849
- blast: {
9850
- score: blast.score,
9851
- reachable: blast.reachable,
9852
- envFindings: blast.envFindings
9853
- }
9854
- };
9850
+ // src/utils/hook-payload.ts
9851
+ function extractToolName(payload, defaultValue = "") {
9852
+ return payload.tool_name ?? payload.name ?? payload.toolCall?.name ?? defaultValue;
9853
+ }
9854
+ function extractToolInput(payload) {
9855
+ return payload.tool_input ?? payload.args ?? payload.toolCall?.args ?? {};
9856
+ }
9857
+ function canonicalToolName(name) {
9858
+ switch (name) {
9859
+ // Hermes Agent
9860
+ case "terminal":
9861
+ return "Bash";
9862
+ case "write_file":
9863
+ return "Write";
9864
+ case "patch":
9865
+ return "Edit";
9866
+ case "read_file":
9867
+ return "Read";
9868
+ case "search_files":
9869
+ return "Grep";
9870
+ // Antigravity (agy) — shell tool renamed from Gemini's run_shell_command
9871
+ case "run_command":
9872
+ return "Bash";
9873
+ default:
9874
+ return name;
9875
+ }
9876
+ }
9877
+ function agentLabelFromFlag(flag) {
9878
+ if (typeof flag !== "string") return void 0;
9879
+ switch (flag.toLowerCase()) {
9880
+ case "antigravity":
9881
+ case "agy":
9882
+ return "Antigravity";
9883
+ case "copilot":
9884
+ return "GitHub Copilot";
9885
+ default:
9886
+ return void 0;
9887
+ }
9855
9888
  }
9856
- var init_scan_json = __esm({
9857
- "src/cli/render/scan-json.ts"() {
9889
+ function canonicalToolInput(rawToolName, input) {
9890
+ if (rawToolName !== "run_command") return input;
9891
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
9892
+ const args = input;
9893
+ if (typeof args.CommandLine !== "string") return input;
9894
+ const { CommandLine, Cwd, ...rest } = args;
9895
+ const canonical = { ...rest, command: CommandLine };
9896
+ if (typeof Cwd === "string" && Cwd.length > 0) canonical.cwd = Cwd;
9897
+ return canonical;
9898
+ }
9899
+ var init_hook_payload = __esm({
9900
+ "src/utils/hook-payload.ts"() {
9858
9901
  "use strict";
9859
- init_scan_derive();
9860
9902
  }
9861
9903
  });
9862
9904
 
9863
- // src/cli/render/scan-history.ts
9864
- function defaultHistoryPath() {
9865
- return import_path17.default.join(import_os14.default.homedir(), ".node9", "scan-history.json");
9905
+ // src/scan-summary.ts
9906
+ function agentDisplayName(agent) {
9907
+ return AGENT_LONG[agent] ?? "Claude Code";
9866
9908
  }
9867
- function readPreviousScan(opts = {}) {
9868
- const filePath = opts.path ?? defaultHistoryPath();
9869
- try {
9870
- if (!import_fs15.default.existsSync(filePath)) return null;
9871
- const raw = import_fs15.default.readFileSync(filePath, "utf8");
9872
- const parsed = JSON.parse(raw);
9873
- if (!Array.isArray(parsed) || parsed.length === 0) return null;
9874
- const last = parsed[parsed.length - 1];
9875
- if (!isValidRecord(last)) return null;
9876
- return last;
9877
- } catch {
9878
- return null;
9909
+ function agentBadgeText(agent, width = 10) {
9910
+ return `[${AGENT_SHORT[agent] ?? "Claude"}]`.padEnd(width);
9911
+ }
9912
+ function agentColorName(agent) {
9913
+ switch (agent) {
9914
+ case "gemini":
9915
+ return "blue";
9916
+ case "codex":
9917
+ return "magenta";
9918
+ case "antigravity":
9919
+ return "yellow";
9920
+ case "copilot":
9921
+ return "green";
9922
+ case "shell":
9923
+ return "yellow";
9924
+ default:
9925
+ return "cyan";
9879
9926
  }
9880
9927
  }
9881
- function appendScanHistory(record, opts = {}) {
9882
- const filePath = opts.path ?? defaultHistoryPath();
9883
- const cap = opts.cap ?? SCAN_HISTORY_CAP;
9884
- try {
9885
- import_fs15.default.mkdirSync(import_path17.default.dirname(filePath), { recursive: true });
9886
- let history = [];
9887
- if (import_fs15.default.existsSync(filePath)) {
9888
- try {
9889
- const parsed = JSON.parse(import_fs15.default.readFileSync(filePath, "utf8"));
9890
- if (Array.isArray(parsed)) {
9891
- history = parsed.filter(isValidRecord);
9892
- }
9893
- } catch {
9894
- }
9928
+ function buildScanSummary(agents) {
9929
+ const stats = {
9930
+ sessions: 0,
9931
+ totalToolCalls: 0,
9932
+ bashCalls: 0,
9933
+ totalCostUSD: 0,
9934
+ firstDate: null,
9935
+ lastDate: null
9936
+ };
9937
+ for (const a of agents) {
9938
+ stats.sessions += a.scan.sessions;
9939
+ stats.totalToolCalls += a.scan.totalToolCalls;
9940
+ stats.bashCalls += a.scan.bashCalls;
9941
+ stats.totalCostUSD += a.scan.totalCostUSD;
9942
+ if (a.scan.firstDate && (!stats.firstDate || a.scan.firstDate < stats.firstDate)) {
9943
+ stats.firstDate = a.scan.firstDate;
9895
9944
  }
9896
- history.push(record);
9897
- if (history.length > cap) {
9898
- history = history.slice(history.length - cap);
9945
+ if (a.scan.lastDate && (!stats.lastDate || a.scan.lastDate > stats.lastDate)) {
9946
+ stats.lastDate = a.scan.lastDate;
9899
9947
  }
9900
- import_fs15.default.writeFileSync(filePath, JSON.stringify(history, null, 2));
9901
- } catch (err2) {
9902
- process.stderr.write(
9903
- `[node9] Warning: could not write scan-history.json: ${err2.message}
9904
- `
9905
- );
9906
9948
  }
9949
+ const allFindings = agents.flatMap((a) => a.scan.findings);
9950
+ const allLeaks = agents.flatMap(
9951
+ (a) => a.scan.dlpFindings.map((f) => ({
9952
+ patternName: f.patternName,
9953
+ redactedSample: f.redactedSample,
9954
+ toolName: f.toolName,
9955
+ timestamp: f.timestamp,
9956
+ project: f.project,
9957
+ sessionId: f.sessionId,
9958
+ agent: f.agent
9959
+ }))
9960
+ );
9961
+ const allLoops = agents.flatMap(
9962
+ (a) => a.scan.loopFindings.map((f) => ({
9963
+ toolName: f.toolName,
9964
+ commandPreview: f.commandPreview,
9965
+ count: f.count,
9966
+ timestamp: f.timestamp,
9967
+ project: f.project,
9968
+ sessionId: f.sessionId,
9969
+ agent: f.agent,
9970
+ kind: f.kind
9971
+ }))
9972
+ );
9973
+ const byVerdict = {
9974
+ blocked: allFindings.filter((f) => f.source.rule.verdict === "block").length,
9975
+ supervised: allFindings.filter((f) => f.source.rule.verdict === "review").length,
9976
+ leaks: allLeaks.length,
9977
+ loops: allLoops.length
9978
+ };
9979
+ const byAgent = agents.map((a) => ({
9980
+ id: a.id,
9981
+ label: a.label,
9982
+ icon: a.icon,
9983
+ sessions: a.scan.sessions,
9984
+ findings: a.scan.findings.length + a.scan.dlpFindings.length + a.scan.loopFindings.length,
9985
+ costUSD: a.scan.totalCostUSD
9986
+ })).filter((s) => s.sessions > 0 || s.findings > 0);
9987
+ const sections = buildSections(allFindings);
9988
+ const wastedIters = allLoops.filter((l) => l.kind !== "long-iteration").reduce((sum, l) => sum + Math.max(0, l.count - LOOP_THRESHOLD_FOR_WASTE), 0);
9989
+ const loopWastedUSD = wastedIters * COST_PER_LOOP_ITER_USD;
9990
+ return {
9991
+ stats,
9992
+ byVerdict,
9993
+ byAgent,
9994
+ sections,
9995
+ leaks: allLeaks,
9996
+ loops: allLoops,
9997
+ loopWastedUSD
9998
+ };
9907
9999
  }
9908
- function computeScanDelta(current, previous, now = Date.now()) {
9909
- if (!previous) return null;
9910
- const prevMs = Date.parse(previous.timestamp);
9911
- if (Number.isNaN(prevMs)) return null;
9912
- const scoreDelta = current.score - previous.score;
9913
- const daysAgo = Math.max(0, Math.floor((now - prevMs) / 864e5));
9914
- if (scoreDelta === 0 && daysAgo === 0) return null;
9915
- return { scoreDelta, daysAgo };
9916
- }
9917
- function isValidRecord(x) {
9918
- if (typeof x !== "object" || x === null) return false;
9919
- const r = x;
9920
- return typeof r.timestamp === "string" && typeof r.score === "number" && typeof r.blocked === "number" && typeof r.review === "number" && typeof r.leaks === "number" && typeof r.loops === "number" && typeof r.totalCalls === "number";
9921
- }
9922
- var import_fs15, import_path17, import_os14, SCAN_HISTORY_CAP;
9923
- var init_scan_history = __esm({
9924
- "src/cli/render/scan-history.ts"() {
9925
- "use strict";
9926
- import_fs15 = __toESM(require("fs"));
9927
- import_path17 = __toESM(require("path"));
9928
- import_os14 = __toESM(require("os"));
9929
- SCAN_HISTORY_CAP = 30;
10000
+ function buildSections(findings) {
10001
+ const sectionMap = /* @__PURE__ */ new Map();
10002
+ function ensureSection(id, label, subtitle, sourceType, shieldKey) {
10003
+ let s = sectionMap.get(id);
10004
+ if (!s) {
10005
+ s = {
10006
+ id,
10007
+ label,
10008
+ subtitle,
10009
+ sourceType,
10010
+ shieldKey,
10011
+ blockedCount: 0,
10012
+ reviewCount: 0,
10013
+ rules: []
10014
+ };
10015
+ sectionMap.set(id, s);
10016
+ }
10017
+ return s;
10018
+ }
10019
+ const ruleMap = /* @__PURE__ */ new Map();
10020
+ for (const f of findings) {
10021
+ const src = f.source;
10022
+ const sourceType = src.sourceType;
10023
+ const shieldName = src.shieldName;
10024
+ const verdict = src.rule.verdict === "block" ? "block" : "review";
10025
+ let sectionId;
10026
+ let sectionLabel;
10027
+ let sectionSubtitle;
10028
+ let shieldKey;
10029
+ if (sourceType === "default") {
10030
+ sectionId = "default";
10031
+ sectionLabel = "Default Rules";
10032
+ sectionSubtitle = "built-in, always on";
10033
+ } else if (sourceType === "shield") {
10034
+ sectionId = `shield:${shieldName}`;
10035
+ sectionLabel = shieldName;
10036
+ sectionSubtitle = SHIELDS[shieldName]?.description ?? "";
10037
+ shieldKey = shieldName;
10038
+ } else if (shieldName === "cloud") {
10039
+ sectionId = "cloud";
10040
+ sectionLabel = "Cloud Policy";
10041
+ sectionSubtitle = "synced from node9 cloud";
10042
+ } else {
10043
+ sectionId = "user";
10044
+ sectionLabel = "Your Rules";
10045
+ sectionSubtitle = "added in node9.config.json";
10046
+ }
10047
+ const section = ensureSection(sectionId, sectionLabel, sectionSubtitle, sourceType, shieldKey);
10048
+ const ruleDisplayName = (src.rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
10049
+ const ruleKey = sectionId + "::" + ruleDisplayName;
10050
+ let rule = ruleMap.get(ruleKey);
10051
+ if (!rule) {
10052
+ rule = {
10053
+ name: ruleDisplayName,
10054
+ verdict,
10055
+ reason: src.rule.reason ?? "",
10056
+ findings: []
10057
+ };
10058
+ ruleMap.set(ruleKey, rule);
10059
+ section.rules.push(rule);
10060
+ }
10061
+ const cmdPreview = previewCommand(f.input, 120);
10062
+ const fullCmd = fullCommandOf(f.input);
10063
+ const isDupe = rule.findings.some((x) => x.project === f.project && x.command === cmdPreview);
10064
+ if (!isDupe) {
10065
+ rule.findings.push({
10066
+ timestamp: f.timestamp ?? "",
10067
+ command: cmdPreview,
10068
+ fullCommand: fullCmd,
10069
+ project: f.project,
10070
+ sessionId: f.sessionId,
10071
+ agent: f.agent,
10072
+ toolName: f.toolName
10073
+ });
10074
+ }
10075
+ if (verdict === "block") section.blockedCount++;
10076
+ else section.reviewCount++;
10077
+ }
10078
+ const sections = [...sectionMap.values()];
10079
+ sections.sort((a, b) => {
10080
+ const aTotal = a.blockedCount + a.reviewCount;
10081
+ const bTotal = b.blockedCount + b.reviewCount;
10082
+ if (b.blockedCount !== a.blockedCount) return b.blockedCount - a.blockedCount;
10083
+ return bTotal - aTotal;
10084
+ });
10085
+ for (const s of sections) {
10086
+ s.rules.sort((a, b) => {
10087
+ const aBlock = a.verdict === "block" ? 1 : 0;
10088
+ const bBlock = b.verdict === "block" ? 1 : 0;
10089
+ if (bBlock !== aBlock) return bBlock - aBlock;
10090
+ return b.findings.length - a.findings.length;
10091
+ });
9930
10092
  }
9931
- });
9932
-
9933
- // src/pricing/litellm.ts
9934
- function normalizeModel(raw) {
9935
- return raw.replace(/-\d{8}$/, "").toLowerCase();
10093
+ return sections;
9936
10094
  }
9937
- function readCache() {
9938
- try {
9939
- const raw = JSON.parse(import_fs16.default.readFileSync(CACHE_FILE(), "utf-8"));
9940
- if (typeof raw.fetchedAt !== "string" || typeof raw.prices !== "object" || raw.prices === null) {
9941
- return null;
9942
- }
9943
- const ageMs = Date.now() - new Date(raw.fetchedAt).getTime();
9944
- if (ageMs < 0 || ageMs > TTL_MS) return null;
9945
- return raw.prices;
9946
- } catch {
9947
- return null;
9948
- }
10095
+ function previewCommand(input, max) {
10096
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
10097
+ const s = String(raw).replace(/\s+/g, " ").trim();
10098
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
9949
10099
  }
9950
- function writeCache(prices) {
9951
- try {
9952
- const target = CACHE_FILE();
9953
- const dir = import_path18.default.dirname(target);
9954
- if (!import_fs16.default.existsSync(dir)) import_fs16.default.mkdirSync(dir, { recursive: true });
9955
- const tmp = target + ".tmp";
9956
- const body = {
9957
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
9958
- prices
10100
+ function fullCommandOf(input) {
10101
+ const raw = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
10102
+ return String(raw).replace(/\s+/g, " ").trim();
10103
+ }
10104
+ var AGENT_SHORT, AGENT_LONG;
10105
+ var init_scan_summary = __esm({
10106
+ "src/scan-summary.ts"() {
10107
+ "use strict";
10108
+ init_shields();
10109
+ init_dist();
10110
+ init_dist();
10111
+ AGENT_SHORT = {
10112
+ claude: "Claude",
10113
+ gemini: "Gemini",
10114
+ codex: "Codex",
10115
+ antigravity: "Agy",
10116
+ copilot: "Copilot",
10117
+ shell: "Shell"
10118
+ };
10119
+ AGENT_LONG = {
10120
+ claude: "Claude Code",
10121
+ gemini: "Gemini CLI",
10122
+ codex: "Codex",
10123
+ antigravity: "Antigravity",
10124
+ copilot: "GitHub Copilot",
10125
+ shell: "Shell"
9959
10126
  };
9960
- import_fs16.default.writeFileSync(tmp, JSON.stringify(body) + "\n", "utf-8");
9961
- import_fs16.default.renameSync(tmp, target);
9962
- } catch (err2) {
9963
- try {
9964
- import_fs16.default.appendFileSync(
9965
- HOOK_DEBUG_LOG,
9966
- `[pricing] cache write failed: ${err2.message}
9967
- `
9968
- );
9969
- } catch {
9970
- }
9971
10127
  }
9972
- }
9973
- function tupleFromLiteLLM(entry) {
9974
- if (!entry || typeof entry !== "object") return null;
9975
- const e = entry;
9976
- const num3 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
9977
- const inCost = num3(e.input_cost_per_token);
9978
- const outCost = num3(e.output_cost_per_token);
9979
- if (inCost === 0 && outCost === 0) return null;
10128
+ });
10129
+
10130
+ // src/cli/commands/blast.ts
10131
+ function buildSensitivePaths(home, cwd) {
9980
10132
  return [
9981
- inCost,
9982
- outCost,
9983
- num3(e.cache_creation_input_token_cost),
9984
- num3(e.cache_read_input_token_cost)
10133
+ {
10134
+ full: import_path19.default.join(home, ".ssh", "id_rsa"),
10135
+ label: "~/.ssh/id_rsa",
10136
+ description: "RSA private key \u2014 grants SSH access to your servers",
10137
+ score: 20
10138
+ },
10139
+ {
10140
+ full: import_path19.default.join(home, ".ssh", "id_ed25519"),
10141
+ label: "~/.ssh/id_ed25519",
10142
+ description: "Ed25519 private key \u2014 grants SSH access to your servers",
10143
+ score: 20
10144
+ },
10145
+ {
10146
+ full: import_path19.default.join(home, ".ssh", "id_ecdsa"),
10147
+ label: "~/.ssh/id_ecdsa",
10148
+ description: "ECDSA private key \u2014 grants SSH access to your servers",
10149
+ score: 20
10150
+ },
10151
+ {
10152
+ full: import_path19.default.join(home, ".aws", "credentials"),
10153
+ label: "~/.aws/credentials",
10154
+ description: "AWS access keys \u2014 full cloud account access",
10155
+ score: 20
10156
+ },
10157
+ {
10158
+ full: import_path19.default.join(home, ".aws", "config"),
10159
+ label: "~/.aws/config",
10160
+ description: "AWS configuration \u2014 account and region settings",
10161
+ score: 5
10162
+ },
10163
+ {
10164
+ full: import_path19.default.join(home, ".config", "gcloud", "credentials.db"),
10165
+ label: "~/.config/gcloud/credentials.db",
10166
+ description: "Google Cloud credentials",
10167
+ score: 15
10168
+ },
10169
+ {
10170
+ full: import_path19.default.join(home, ".docker", "config.json"),
10171
+ label: "~/.docker/config.json",
10172
+ description: "Docker registry auth tokens",
10173
+ score: 10
10174
+ },
10175
+ {
10176
+ full: import_path19.default.join(home, ".netrc"),
10177
+ label: "~/.netrc",
10178
+ description: "FTP/HTTP credentials in plain text",
10179
+ score: 15
10180
+ },
10181
+ {
10182
+ full: import_path19.default.join(home, ".npmrc"),
10183
+ label: "~/.npmrc",
10184
+ description: "npm auth token \u2014 can publish packages as you",
10185
+ score: 10
10186
+ },
10187
+ {
10188
+ full: import_path19.default.join(home, ".node9", "credentials.json"),
10189
+ label: "~/.node9/credentials.json",
10190
+ description: "Node9 cloud API key",
10191
+ score: 10
10192
+ },
10193
+ {
10194
+ full: import_path19.default.join(cwd, ".env"),
10195
+ label: ".env (current folder)",
10196
+ description: "App secrets \u2014 database passwords, API keys",
10197
+ score: 20
10198
+ },
10199
+ {
10200
+ full: import_path19.default.join(cwd, ".env.local"),
10201
+ label: ".env.local (current folder)",
10202
+ description: "Local overrides \u2014 often contains real credentials",
10203
+ score: 15
10204
+ },
10205
+ {
10206
+ full: import_path19.default.join(cwd, ".env.production"),
10207
+ label: ".env.production (current folder)",
10208
+ description: "Production secrets",
10209
+ score: 20
10210
+ }
9985
10211
  ];
9986
10212
  }
9987
- async function fetchLiteLLMPricing() {
10213
+ function isReadable(filePath) {
9988
10214
  try {
9989
- const res = await fetch(LITELLM_URL, {
9990
- signal: AbortSignal.timeout(15e3)
9991
- });
9992
- if (!res.ok) return null;
9993
- const json = await res.json();
9994
- if (!json || typeof json !== "object") return null;
9995
- const out = {};
9996
- for (const [key, value] of Object.entries(json)) {
9997
- const tuple = tupleFromLiteLLM(value);
9998
- if (tuple) out[key.toLowerCase()] = tuple;
9999
- }
10000
- if (Object.keys(out).length < 10) {
10001
- return null;
10002
- }
10003
- return out;
10215
+ import_fs17.default.accessSync(filePath, import_fs17.default.constants.R_OK);
10216
+ return true;
10004
10217
  } catch {
10005
- return null;
10218
+ return false;
10006
10219
  }
10007
10220
  }
10008
- async function ensurePricingLoaded() {
10009
- if (memCache !== null && Date.now() - memCacheAt < TTL_MS) return;
10010
- const fromDisk = readCache();
10011
- if (fromDisk && Object.keys(fromDisk).length > 0) {
10012
- memCache = fromDisk;
10013
- memCacheAt = Date.now();
10014
- lookupCache.clear();
10015
- return;
10221
+ function scoreLabel(score) {
10222
+ if (score >= 80) return import_chalk2.default.green(`${score}/100 Good`);
10223
+ if (score >= 50) return import_chalk2.default.yellow(`${score}/100 Moderate risk`);
10224
+ if (score >= 25) return import_chalk2.default.red(`${score}/100 High risk`);
10225
+ return import_chalk2.default.red.bold(`${score}/100 Critical`);
10226
+ }
10227
+ function runBlast() {
10228
+ const home = import_os16.default.homedir();
10229
+ const cwd = process.cwd();
10230
+ const paths = buildSensitivePaths(home, cwd);
10231
+ let scoreDeduction = 0;
10232
+ const reachable = [];
10233
+ for (const p of paths) {
10234
+ if (import_fs17.default.existsSync(p.full) && isReadable(p.full)) {
10235
+ reachable.push(p);
10236
+ scoreDeduction += p.score;
10237
+ }
10016
10238
  }
10017
- const fetched = await fetchLiteLLMPricing();
10018
- if (fetched && Object.keys(fetched).length > 0) {
10019
- memCache = fetched;
10020
- memCacheAt = Date.now();
10021
- writeCache(fetched);
10022
- lookupCache.clear();
10023
- return;
10239
+ const envFindings = [];
10240
+ for (const [key, value] of Object.entries(process.env)) {
10241
+ if (!value) continue;
10242
+ const match = scanArgs({ [key]: value });
10243
+ if (match) {
10244
+ envFindings.push({ key, patternName: match.patternName });
10245
+ scoreDeduction += 10;
10246
+ }
10024
10247
  }
10025
- memCache = { ...BUNDLED_PRICING };
10026
- memCacheAt = Date.now();
10027
- lookupCache.clear();
10248
+ return { reachable, envFindings, score: Math.max(0, 100 - scoreDeduction) };
10028
10249
  }
10029
- function pricingFor(model) {
10030
- const norm = normalizeModel(model);
10031
- const cached = lookupCache.get(norm);
10032
- if (cached !== void 0) return cached;
10033
- const sources = [];
10034
- if (memCache) sources.push(memCache);
10035
- sources.push(BUNDLED_PRICING);
10036
- let resolved = null;
10037
- for (const source of sources) {
10038
- const exact = source[norm];
10039
- if (exact) {
10040
- resolved = exact;
10041
- break;
10250
+ function registerBlastCommand(program2) {
10251
+ program2.command("blast").description("Map what an AI agent can currently reach on this machine").action(() => {
10252
+ const home = import_os16.default.homedir();
10253
+ const cwd = process.cwd();
10254
+ const { reachable, envFindings, score } = runBlast();
10255
+ console.log("");
10256
+ console.log(
10257
+ import_chalk2.default.bold(" \u{1F52D} Node9 Blast Radius") + import_chalk2.default.dim(" \xB7 what an AI agent can reach from here")
10258
+ );
10259
+ console.log(import_chalk2.default.dim(" Running in: ") + import_chalk2.default.white(cwd.replace(home, "~")));
10260
+ console.log("");
10261
+ if (reachable.length > 0) {
10262
+ console.log(" " + import_chalk2.default.red.bold("Sensitive files reachable:"));
10263
+ for (const p of reachable) {
10264
+ console.log(
10265
+ " " + import_chalk2.default.red("\u2717 ") + import_chalk2.default.yellow(p.label.padEnd(38)) + import_chalk2.default.dim(p.description)
10266
+ );
10267
+ }
10268
+ console.log("");
10042
10269
  }
10043
- let best = null;
10044
- for (const key of Object.keys(source)) {
10045
- if (norm.startsWith(key.toLowerCase()) && (best === null || key.length > best.length)) {
10046
- best = key;
10270
+ if (envFindings.length > 0) {
10271
+ console.log(" " + import_chalk2.default.red.bold("Secrets in active environment:"));
10272
+ for (const f of envFindings) {
10273
+ console.log(
10274
+ " " + import_chalk2.default.red("\u2717 ") + import_chalk2.default.yellow(f.key.padEnd(38)) + import_chalk2.default.dim(f.patternName)
10275
+ );
10047
10276
  }
10277
+ console.log("");
10048
10278
  }
10049
- if (best) {
10050
- resolved = source[best];
10051
- break;
10279
+ console.log(" " + import_chalk2.default.dim("\u2500".repeat(70)));
10280
+ if (reachable.length === 0 && envFindings.length === 0) {
10281
+ console.log(" " + import_chalk2.default.green("\u2705 No sensitive files or environment secrets found."));
10282
+ console.log(" Security Score: " + scoreLabel(score));
10283
+ } else {
10284
+ console.log(
10285
+ " Security Score: " + scoreLabel(score) + import_chalk2.default.dim(
10286
+ ` (${reachable.length} file${reachable.length !== 1 ? "s" : ""}, ${envFindings.length} env var${envFindings.length !== 1 ? "s" : ""})`
10287
+ )
10288
+ );
10289
+ console.log("");
10290
+ console.log(
10291
+ import_chalk2.default.dim(
10292
+ " 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."
10293
+ )
10294
+ );
10052
10295
  }
10053
- }
10054
- lookupCache.set(norm, resolved);
10055
- return resolved;
10296
+ console.log("");
10297
+ });
10056
10298
  }
10057
- var import_fs16, import_path18, import_os15, LITELLM_URL, BUNDLED_PRICING, CACHE_FILE, TTL_MS, memCache, memCacheAt, lookupCache;
10058
- var init_litellm = __esm({
10059
- "src/pricing/litellm.ts"() {
10299
+ var import_chalk2, import_fs17, import_path19, import_os16;
10300
+ var init_blast = __esm({
10301
+ "src/cli/commands/blast.ts"() {
10060
10302
  "use strict";
10061
- import_fs16 = __toESM(require("fs"));
10062
- import_path18 = __toESM(require("path"));
10063
- import_os15 = __toESM(require("os"));
10064
- init_audit();
10065
- LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
10066
- BUNDLED_PRICING = {
10067
- // Anthropic
10068
- "claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
10069
- "claude-opus-4-1": [5e-6, 25e-6, 625e-8, 5e-7],
10070
- "claude-opus-4-5": [5e-6, 25e-6, 625e-8, 5e-7],
10071
- "claude-opus-4-6": [5e-6, 25e-6, 625e-8, 5e-7],
10072
- "claude-opus-4-7": [5e-6, 25e-6, 625e-8, 5e-7],
10073
- "claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
10074
- "claude-sonnet-4-5": [3e-6, 15e-6, 375e-8, 3e-7],
10075
- "claude-sonnet-4-6": [3e-6, 15e-6, 375e-8, 3e-7],
10076
- "claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
10077
- "claude-haiku-4-5": [8e-7, 4e-6, 1e-6, 8e-8],
10078
- "claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
10079
- "claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
10080
- "claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
10081
- "claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8],
10082
- // OpenAI
10083
- "gpt-4o": [5e-6, 15e-6, 0, 25e-7],
10084
- "gpt-4o-mini": [15e-8, 6e-7, 0, 75e-9],
10085
- "gpt-5": [1e-5, 3e-5, 0, 5e-6],
10086
- // Google
10087
- "gemini-2.0-flash": [75e-9, 3e-7, 0, 0],
10088
- "gemini-1.5-pro": [125e-8, 5e-6, 0, 0]
10089
- };
10090
- CACHE_FILE = () => import_path18.default.join(import_os15.default.homedir(), ".node9", "model-pricing.json");
10091
- TTL_MS = 24 * 60 * 60 * 1e3;
10092
- memCache = null;
10093
- memCacheAt = 0;
10094
- lookupCache = /* @__PURE__ */ new Map();
10303
+ import_chalk2 = __toESM(require("chalk"));
10304
+ import_fs17 = __toESM(require("fs"));
10305
+ import_path19 = __toESM(require("path"));
10306
+ import_os16 = __toESM(require("os"));
10307
+ init_dlp();
10095
10308
  }
10096
10309
  });
10097
10310
 
10098
- // src/cost-codex.ts
10099
- function codexSessionsDir() {
10100
- return import_path19.default.join(import_os16.default.homedir(), ".codex", "sessions");
10311
+ // src/cli/render/scan-derive.ts
10312
+ function classifyScore(score) {
10313
+ if (score >= 80) return { band: "good", label: "Good", color: import_chalk3.default.green };
10314
+ if (score >= 50) return { band: "at-risk", label: "At Risk", color: import_chalk3.default.yellow };
10315
+ return { band: "critical", label: "Critical", color: import_chalk3.default.red };
10101
10316
  }
10102
- function codexPriceFor(model) {
10103
- return pricingFor(model) ?? CODEX_FALLBACK;
10317
+ function topDlpPatterns(findings, n) {
10318
+ const counts = /* @__PURE__ */ new Map();
10319
+ for (const f of findings) {
10320
+ counts.set(f.patternName, (counts.get(f.patternName) ?? 0) + 1);
10321
+ }
10322
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([name, count]) => ({ name, count }));
10104
10323
  }
10105
- function listCodexSessionFiles(base) {
10106
- const out = [];
10107
- for (const y of safeReaddir(base)) {
10108
- const yp = import_path19.default.join(base, y);
10109
- if (!isDir(yp)) continue;
10110
- for (const m of safeReaddir(yp)) {
10111
- const mp = import_path19.default.join(yp, m);
10112
- if (!isDir(mp)) continue;
10113
- for (const d of safeReaddir(mp)) {
10114
- const dp = import_path19.default.join(mp, d);
10115
- if (!isDir(dp)) continue;
10116
- for (const f of safeReaddir(dp)) {
10117
- if (f.endsWith(".jsonl")) out.push(import_path19.default.join(dp, f));
10118
- }
10119
- }
10324
+ function topRulesByVerdict(sections, verdict, n) {
10325
+ const matched = [];
10326
+ for (const section of sections) {
10327
+ for (const rule of section.rules) {
10328
+ const matches = verdict === "block" ? rule.verdict === "block" : rule.verdict !== "block";
10329
+ if (matches) matched.push({ name: rule.name, count: rule.findings.length });
10120
10330
  }
10121
10331
  }
10122
- return out;
10332
+ return matched.sort((a, b) => b.count - a.count).slice(0, n);
10123
10333
  }
10124
- function safeReaddir(dir) {
10125
- try {
10126
- return import_fs17.default.readdirSync(dir);
10127
- } catch {
10128
- return [];
10129
- }
10334
+ function computeLoopWaste(loops, totalToolCalls) {
10335
+ const wastedCalls = loops.reduce((s, l) => s + Math.max(0, l.count - 1), 0);
10336
+ const wastePct = totalToolCalls > 0 ? Math.round(wastedCalls / totalToolCalls * 100) : 0;
10337
+ return { wastedCalls, wastePct };
10130
10338
  }
10131
- function isDir(p) {
10132
- try {
10133
- return import_fs17.default.statSync(p).isDirectory();
10134
- } catch {
10135
- return false;
10339
+ function rollupByShield(sections, topRulesPerShield = 3) {
10340
+ const out = [];
10341
+ for (const section of sections) {
10342
+ if (section.sourceType !== "shield") continue;
10343
+ if (!section.shieldKey) continue;
10344
+ const totalCatches = section.blockedCount + section.reviewCount;
10345
+ 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);
10346
+ out.push({
10347
+ shieldName: section.shieldKey,
10348
+ totalCatches,
10349
+ blockCatches: section.blockedCount,
10350
+ reviewCatches: section.reviewCount,
10351
+ topRuleLabels
10352
+ });
10136
10353
  }
10354
+ return out.sort((a, b) => b.totalCatches - a.totalCatches);
10137
10355
  }
10138
- function parseCodexSession(lines) {
10139
- let sessionStart2 = "";
10140
- let runId = "";
10141
- let cwd = "";
10142
- let model = "";
10143
- let input = 0;
10144
- let cached = 0;
10145
- let output = 0;
10146
- let sawUsage = false;
10147
- for (const raw of lines) {
10148
- if (!raw.trim()) continue;
10149
- let entry;
10150
- try {
10151
- entry = JSON.parse(raw);
10152
- } catch {
10153
- continue;
10154
- }
10155
- const p = entry.payload ?? {};
10156
- if (entry.type === "session_meta") {
10157
- if (!sessionStart2 && typeof p["timestamp"] === "string") sessionStart2 = p["timestamp"];
10158
- if (!runId && typeof p["id"] === "string") runId = p["id"];
10159
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
10160
- continue;
10161
- }
10162
- if (entry.type === "turn_context") {
10163
- if (typeof p["model"] === "string") model = p["model"];
10164
- if (!cwd && typeof p["cwd"] === "string") cwd = p["cwd"];
10165
- continue;
10166
- }
10167
- if (entry.type === "event_msg" && p["type"] === "token_count") {
10168
- const info = p["info"] ?? {};
10169
- const usage = info["total_token_usage"] ?? {};
10170
- if (typeof usage["input_tokens"] === "number") input = usage["input_tokens"];
10171
- if (typeof usage["cached_input_tokens"] === "number") cached = usage["cached_input_tokens"];
10172
- if (typeof usage["output_tokens"] === "number") output = usage["output_tokens"];
10173
- sawUsage = true;
10174
- }
10356
+ function boxPanel(title, bodyLines, width = PANEL_WIDTH) {
10357
+ const inner = width - 4;
10358
+ const out = [];
10359
+ const titlePad = ` ${title} `;
10360
+ const titleWidth = (0, import_string_width.default)(titlePad);
10361
+ const titleSegment = titleWidth <= inner ? titlePad : titlePad.slice(0, inner);
10362
+ const dashFill = "\u2500".repeat(Math.max(0, inner - (0, import_string_width.default)(titleSegment)));
10363
+ out.push(import_chalk3.default.dim("\u256D\u2500") + import_chalk3.default.bold(titleSegment) + import_chalk3.default.dim(`${dashFill}\u2500\u256E`));
10364
+ for (const line of bodyLines) {
10365
+ const padding = " ".repeat(Math.max(0, inner - line.width));
10366
+ out.push(import_chalk3.default.dim("\u2502 ") + line.rendered + padding + import_chalk3.default.dim(" \u2502"));
10175
10367
  }
10176
- if (!sessionStart2 || !sawUsage) return null;
10177
- const nonCached = Math.max(0, input - cached);
10178
- if (nonCached === 0 && output === 0 && cached === 0) return null;
10179
- const norm = normalizeModel(model || "gpt-5");
10180
- const [pin, pout, , pcr] = codexPriceFor(model || "gpt-5");
10181
- const costUSD = nonCached * pin + output * pout + cached * pcr;
10182
- return {
10183
- date: sessionStart2.slice(0, 10),
10184
- model: norm,
10185
- workingDir: cwd,
10186
- runId,
10187
- costUSD,
10188
- inputTokens: nonCached,
10189
- outputTokens: output,
10190
- cacheReadTokens: cached,
10191
- cacheWriteTokens: 0
10192
- };
10368
+ out.push(import_chalk3.default.dim("\u2570" + "\u2500".repeat(inner + 2) + "\u256F"));
10369
+ return out;
10193
10370
  }
10194
- var import_fs17, import_os16, import_path19, CODEX_FALLBACK, codexSource;
10195
- var init_cost_codex = __esm({
10196
- "src/cost-codex.ts"() {
10371
+ function relativeDate(timestamp, now = /* @__PURE__ */ new Date()) {
10372
+ const t = new Date(timestamp).getTime();
10373
+ if (Number.isNaN(t)) return "?";
10374
+ const days = Math.floor((now.getTime() - t) / 864e5);
10375
+ if (days < 1) return "today";
10376
+ if (days > 90) return "90d+";
10377
+ return `${days}d`;
10378
+ }
10379
+ var import_chalk3, import_string_width, PANEL_WIDTH;
10380
+ var init_scan_derive = __esm({
10381
+ "src/cli/render/scan-derive.ts"() {
10197
10382
  "use strict";
10198
- import_fs17 = __toESM(require("fs"));
10199
- import_os16 = __toESM(require("os"));
10200
- import_path19 = __toESM(require("path"));
10201
- init_litellm();
10202
- CODEX_FALLBACK = [5e-6, 15e-6, 0, 25e-7];
10203
- codexSource = {
10204
- id: "codex",
10205
- available() {
10206
- try {
10207
- return import_fs17.default.existsSync(codexSessionsDir());
10208
- } catch {
10209
- return false;
10210
- }
10211
- },
10212
- collect(sinceMs) {
10213
- const base = codexSessionsDir();
10214
- const combined = /* @__PURE__ */ new Map();
10215
- for (const file of listCodexSessionFiles(base)) {
10216
- try {
10217
- if (sinceMs !== void 0 && import_fs17.default.statSync(file).mtimeMs < sinceMs) continue;
10218
- } catch {
10219
- continue;
10220
- }
10221
- let content;
10222
- try {
10223
- content = import_fs17.default.readFileSync(file, "utf8");
10224
- } catch {
10225
- continue;
10226
- }
10227
- const e = parseCodexSession(content.split("\n"));
10228
- if (!e) continue;
10229
- const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
10230
- const prev = combined.get(key);
10231
- if (prev) {
10232
- prev.costUSD += e.costUSD;
10233
- prev.inputTokens += e.inputTokens;
10234
- prev.outputTokens += e.outputTokens;
10235
- prev.cacheReadTokens += e.cacheReadTokens;
10236
- prev.cacheWriteTokens += e.cacheWriteTokens;
10237
- } else {
10238
- combined.set(key, { ...e });
10239
- }
10240
- }
10241
- return [...combined.values()];
10242
- }
10383
+ import_chalk3 = __toESM(require("chalk"));
10384
+ import_string_width = __toESM(require("string-width"));
10385
+ PANEL_WIDTH = 76;
10386
+ }
10387
+ });
10388
+
10389
+ // src/protection.ts
10390
+ var PROTECTIVE_SHIELD_DISCOUNTS;
10391
+ var init_protection = __esm({
10392
+ "src/protection.ts"() {
10393
+ "use strict";
10394
+ PROTECTIVE_SHIELD_DISCOUNTS = {
10395
+ "project-jail": 0.7
10243
10396
  };
10244
10397
  }
10245
10398
  });
10246
10399
 
10247
- // src/cost-gemini.ts
10248
- function geminiTmpDir() {
10249
- return import_path20.default.join(import_os17.default.homedir(), ".gemini", "tmp");
10250
- }
10251
- function geminiPriceFor(model) {
10252
- let tuple = pricingFor(model);
10253
- if (!tuple && /^gemini-/i.test(model)) {
10254
- for (const proxy of GEMINI_FALLBACK_MODELS) {
10255
- tuple = pricingFor(proxy);
10256
- if (tuple) break;
10257
- }
10258
- }
10259
- if (!tuple) return null;
10260
- return { input: tuple[0], output: tuple[1], cacheRead: tuple[3] || tuple[0] };
10400
+ // src/cli/render/scan-json.ts
10401
+ function buildScanJson(input) {
10402
+ const { summary, blast, isWired, generatedAt } = input;
10403
+ const { band } = classifyScore(blast.score);
10404
+ return {
10405
+ schemaVersion: 1,
10406
+ generatedAt,
10407
+ isWired,
10408
+ score: blast.score,
10409
+ band,
10410
+ totals: {
10411
+ blocked: summary.byVerdict.blocked,
10412
+ review: summary.byVerdict.supervised,
10413
+ leaks: summary.byVerdict.leaks,
10414
+ loops: summary.byVerdict.loops,
10415
+ blastExposures: blast.reachable.length + blast.envFindings.length
10416
+ },
10417
+ summary,
10418
+ blast: {
10419
+ score: blast.score,
10420
+ reachable: blast.reachable,
10421
+ envFindings: blast.envFindings
10422
+ }
10423
+ };
10424
+ }
10425
+ var init_scan_json = __esm({
10426
+ "src/cli/render/scan-json.ts"() {
10427
+ "use strict";
10428
+ init_scan_derive();
10429
+ }
10430
+ });
10431
+
10432
+ // src/cli/render/scan-history.ts
10433
+ function defaultHistoryPath() {
10434
+ return import_path20.default.join(import_os17.default.homedir(), ".node9", "scan-history.json");
10261
10435
  }
10262
- function safeReaddir2(dir) {
10436
+ function readPreviousScan(opts = {}) {
10437
+ const filePath = opts.path ?? defaultHistoryPath();
10263
10438
  try {
10264
- return import_fs18.default.readdirSync(dir);
10439
+ if (!import_fs18.default.existsSync(filePath)) return null;
10440
+ const raw = import_fs18.default.readFileSync(filePath, "utf8");
10441
+ const parsed = JSON.parse(raw);
10442
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
10443
+ const last = parsed[parsed.length - 1];
10444
+ if (!isValidRecord(last)) return null;
10445
+ return last;
10265
10446
  } catch {
10266
- return [];
10447
+ return null;
10267
10448
  }
10268
10449
  }
10269
- function isDir2(p) {
10450
+ function appendScanHistory(record, opts = {}) {
10451
+ const filePath = opts.path ?? defaultHistoryPath();
10452
+ const cap = opts.cap ?? SCAN_HISTORY_CAP;
10270
10453
  try {
10271
- return import_fs18.default.statSync(p).isDirectory();
10272
- } catch {
10273
- return false;
10274
- }
10275
- }
10276
- function listGeminiSessionFiles(base) {
10277
- const out = [];
10278
- for (const project of safeReaddir2(base)) {
10279
- const chats = import_path20.default.join(base, project, "chats");
10280
- if (!isDir2(chats)) continue;
10281
- for (const f of safeReaddir2(chats)) {
10282
- if (f.startsWith("session-") && f.endsWith(".jsonl")) {
10283
- out.push({ file: import_path20.default.join(chats, f), project });
10454
+ import_fs18.default.mkdirSync(import_path20.default.dirname(filePath), { recursive: true });
10455
+ let history = [];
10456
+ if (import_fs18.default.existsSync(filePath)) {
10457
+ try {
10458
+ const parsed = JSON.parse(import_fs18.default.readFileSync(filePath, "utf8"));
10459
+ if (Array.isArray(parsed)) {
10460
+ history = parsed.filter(isValidRecord);
10461
+ }
10462
+ } catch {
10284
10463
  }
10285
10464
  }
10286
- }
10287
- return out;
10288
- }
10289
- function parseGeminiSession(lines, project) {
10290
- const seenIds = /* @__PURE__ */ new Set();
10291
- const byKey = /* @__PURE__ */ new Map();
10292
- let runId = "";
10293
- for (const raw of lines) {
10294
- if (!raw.trim()) continue;
10295
- let obj;
10296
- try {
10297
- obj = JSON.parse(raw);
10298
- } catch {
10299
- continue;
10300
- }
10301
- if (typeof obj.sessionId === "string" && !runId) runId = obj.sessionId;
10302
- if (!obj.tokens || !obj.model || !obj.timestamp) continue;
10303
- if (obj.id) {
10304
- if (seenIds.has(obj.id)) continue;
10305
- seenIds.add(obj.id);
10306
- }
10307
- const price = geminiPriceFor(obj.model);
10308
- if (!price) continue;
10309
- const inp = obj.tokens.input ?? 0;
10310
- const out = obj.tokens.output ?? 0;
10311
- const cached = Math.min(obj.tokens.cached ?? 0, inp);
10312
- const fresh = Math.max(0, inp - cached);
10313
- const cost = fresh * price.input + cached * price.cacheRead + out * price.output;
10314
- const date = obj.timestamp.slice(0, 10);
10315
- const model = normalizeModel(obj.model);
10316
- const key = `${date}::${model}`;
10317
- const prev = byKey.get(key);
10318
- if (prev) {
10319
- prev.costUSD += cost;
10320
- prev.inputTokens += fresh;
10321
- prev.outputTokens += out;
10322
- prev.cacheReadTokens += cached;
10323
- } else {
10324
- byKey.set(key, {
10325
- date,
10326
- model,
10327
- workingDir: project,
10328
- runId,
10329
- costUSD: cost,
10330
- inputTokens: fresh,
10331
- outputTokens: out,
10332
- cacheReadTokens: cached,
10333
- cacheWriteTokens: 0
10334
- });
10465
+ history.push(record);
10466
+ if (history.length > cap) {
10467
+ history = history.slice(history.length - cap);
10335
10468
  }
10469
+ import_fs18.default.writeFileSync(filePath, JSON.stringify(history, null, 2));
10470
+ } catch (err2) {
10471
+ process.stderr.write(
10472
+ `[node9] Warning: could not write scan-history.json: ${err2.message}
10473
+ `
10474
+ );
10336
10475
  }
10337
- if (runId) for (const e of byKey.values()) e.runId = runId;
10338
- return [...byKey.values()];
10339
10476
  }
10340
- var import_fs18, import_os17, import_path20, GEMINI_FALLBACK_MODELS, geminiSource;
10341
- var init_cost_gemini = __esm({
10342
- "src/cost-gemini.ts"() {
10477
+ function computeScanDelta(current, previous, now = Date.now()) {
10478
+ if (!previous) return null;
10479
+ const prevMs = Date.parse(previous.timestamp);
10480
+ if (Number.isNaN(prevMs)) return null;
10481
+ const scoreDelta = current.score - previous.score;
10482
+ const daysAgo = Math.max(0, Math.floor((now - prevMs) / 864e5));
10483
+ if (scoreDelta === 0 && daysAgo === 0) return null;
10484
+ return { scoreDelta, daysAgo };
10485
+ }
10486
+ function isValidRecord(x) {
10487
+ if (typeof x !== "object" || x === null) return false;
10488
+ const r = x;
10489
+ 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";
10490
+ }
10491
+ var import_fs18, import_path20, import_os17, SCAN_HISTORY_CAP;
10492
+ var init_scan_history = __esm({
10493
+ "src/cli/render/scan-history.ts"() {
10343
10494
  "use strict";
10344
10495
  import_fs18 = __toESM(require("fs"));
10345
- import_os17 = __toESM(require("os"));
10346
10496
  import_path20 = __toESM(require("path"));
10347
- init_litellm();
10348
- GEMINI_FALLBACK_MODELS = ["gemini-2.5-flash", "gemini-2.0-flash"];
10349
- geminiSource = {
10350
- id: "gemini",
10351
- available() {
10352
- try {
10353
- return import_fs18.default.existsSync(geminiTmpDir());
10354
- } catch {
10355
- return false;
10356
- }
10357
- },
10358
- collect(sinceMs) {
10359
- const combined = /* @__PURE__ */ new Map();
10360
- for (const { file, project } of listGeminiSessionFiles(geminiTmpDir())) {
10361
- try {
10362
- if (sinceMs !== void 0 && import_fs18.default.statSync(file).mtimeMs < sinceMs) continue;
10363
- } catch {
10364
- continue;
10365
- }
10366
- let content;
10367
- try {
10368
- content = import_fs18.default.readFileSync(file, "utf8");
10369
- } catch {
10370
- continue;
10371
- }
10372
- for (const e of parseGeminiSession(content.split("\n"), project)) {
10373
- const key = `${e.date}::${e.model}::${e.workingDir ?? ""}::${e.runId ?? ""}`;
10374
- const prev = combined.get(key);
10375
- if (prev) {
10376
- prev.costUSD += e.costUSD;
10377
- prev.inputTokens += e.inputTokens;
10378
- prev.outputTokens += e.outputTokens;
10379
- prev.cacheReadTokens += e.cacheReadTokens;
10380
- prev.cacheWriteTokens += e.cacheWriteTokens;
10381
- } else {
10382
- combined.set(key, { ...e });
10383
- }
10384
- }
10385
- }
10386
- return [...combined.values()];
10387
- }
10388
- };
10497
+ import_os17 = __toESM(require("os"));
10498
+ SCAN_HISTORY_CAP = 30;
10389
10499
  }
10390
10500
  });
10391
10501
 
@@ -11424,19 +11534,15 @@ var init_scan_upload_history = __esm({
11424
11534
 
11425
11535
  // src/cli/commands/scan.ts
11426
11536
  function claudeModelPrice(model) {
11427
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
11428
- for (const [key, p] of Object.entries(CLAUDE_PRICING)) {
11429
- if (base === key || base.startsWith(key)) return p;
11430
- }
11431
- return null;
11537
+ const t = pricingFor(model);
11538
+ if (!t) return null;
11539
+ const [i, o, cw, cr] = t;
11540
+ return { i, o, cw, cr };
11432
11541
  }
11433
11542
  function geminiModelPrice(model) {
11434
- const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
11435
- for (const [key, p] of Object.entries(GEMINI_PRICING)) {
11436
- if (base === key || base.startsWith(key)) return p;
11437
- }
11438
- if (base.includes("flash")) return GEMINI_PRICING["gemini-2.0-flash"];
11439
- return null;
11543
+ const p = geminiPriceFor(model);
11544
+ if (!p) return null;
11545
+ return { i: p.input, o: p.output, cr: p.cacheRead };
11440
11546
  }
11441
11547
  function isNode9SelfOutput(text) {
11442
11548
  let hits = 0;
@@ -12082,14 +12188,17 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
12082
12188
  if (!import_fs23.default.existsSync(chatsDir)) continue;
12083
12189
  let chatFiles;
12084
12190
  try {
12085
- chatFiles = import_fs23.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
12191
+ chatFiles = import_fs23.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl")).sort((a, b) => Number(b.endsWith(".jsonl")) - Number(a.endsWith(".jsonl")));
12086
12192
  } catch {
12087
12193
  continue;
12088
12194
  }
12195
+ const seenSessions = /* @__PURE__ */ new Set();
12089
12196
  for (const chatFile of chatFiles) {
12197
+ const sessionId = chatFile.replace(/\.jsonl?$/, "");
12198
+ if (seenSessions.has(sessionId)) continue;
12199
+ seenSessions.add(sessionId);
12090
12200
  result.filesScanned++;
12091
12201
  onProgress?.(result.filesScanned);
12092
- const sessionId = chatFile.replace(/\.json$/, "");
12093
12202
  let raw;
12094
12203
  try {
12095
12204
  raw = import_fs23.default.readFileSync(import_path25.default.join(chatsDir, chatFile), "utf-8");
@@ -12099,7 +12208,18 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
12099
12208
  const sessionCalls = [];
12100
12209
  let session;
12101
12210
  try {
12102
- session = JSON.parse(raw);
12211
+ if (chatFile.endsWith(".jsonl")) {
12212
+ const messages = raw.split("\n").filter((l) => l.trim()).map((l) => {
12213
+ try {
12214
+ return JSON.parse(l);
12215
+ } catch {
12216
+ return null;
12217
+ }
12218
+ }).filter((m) => m !== null);
12219
+ session = { messages };
12220
+ } else {
12221
+ session = JSON.parse(raw);
12222
+ }
12103
12223
  } catch {
12104
12224
  continue;
12105
12225
  }
@@ -12709,6 +12829,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12709
12829
  let lastTotalInput = 0;
12710
12830
  let lastTotalCached = 0;
12711
12831
  let lastTotalOutput = 0;
12832
+ let model = "";
12712
12833
  for (const line of lines) {
12713
12834
  if (!line.trim()) continue;
12714
12835
  onLine?.();
@@ -12726,6 +12847,10 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12726
12847
  projLabel = stripTerminalEscapes(cwd.replace(import_os22.default.homedir(), "~")).slice(0, 40);
12727
12848
  continue;
12728
12849
  }
12850
+ if (entry.type === "turn_context" && typeof payload["model"] === "string") {
12851
+ model = payload["model"];
12852
+ continue;
12853
+ }
12729
12854
  if (entry.type === "event_msg" && payload["type"] === "token_count") {
12730
12855
  const info = payload["info"];
12731
12856
  const usage = info?.["total_token_usage"] ?? {};
@@ -12869,8 +12994,11 @@ function scanCodexHistory(startDate, onProgress, onLine) {
12869
12994
  }
12870
12995
  }
12871
12996
  }
12872
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
12873
- result.totalCostUSD += nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
12997
+ result.totalCostUSD += codexSessionCost(model, {
12998
+ input: lastTotalInput,
12999
+ cached: lastTotalCached,
13000
+ output: lastTotalOutput
13001
+ });
12874
13002
  result.loopFindings.push(...detectLoops(sessionCalls, projLabel, sessionId, "codex"));
12875
13003
  }
12876
13004
  return result;
@@ -13917,7 +14045,7 @@ function registerScanCommand(program2) {
13917
14045
  }
13918
14046
  );
13919
14047
  }
13920
- var import_chalk5, import_fs23, import_path25, import_os22, import_string_width2, CLAUDE_PRICING, GEMINI_PRICING, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
14048
+ var import_chalk5, import_fs23, import_path25, import_os22, import_string_width2, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE2, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS, STUCK_TOOLS_MIN_WASTE, STUCK_TOOLS_LIMIT, RECURRING_SESSION_THRESHOLD, STALE_AGE_DAYS, classifyRuleSeverity2, narrativeRuleLabel2;
13921
14049
  var init_scan = __esm({
13922
14050
  "src/cli/commands/scan.ts"() {
13923
14051
  "use strict";
@@ -13930,6 +14058,9 @@ var init_scan = __esm({
13930
14058
  init_policy();
13931
14059
  init_dist();
13932
14060
  init_dlp();
14061
+ init_litellm();
14062
+ init_cost_gemini();
14063
+ init_cost_codex();
13933
14064
  init_hook_payload();
13934
14065
  init_dist();
13935
14066
  init_scan_summary();
@@ -13940,26 +14071,6 @@ var init_scan = __esm({
13940
14071
  import_string_width2 = __toESM(require("string-width"));
13941
14072
  init_scan_json();
13942
14073
  init_scan_history();
13943
- CLAUDE_PRICING = {
13944
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13945
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13946
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
13947
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13948
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13949
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13950
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13951
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13952
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
13953
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
13954
- };
13955
- GEMINI_PRICING = {
13956
- "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
13957
- "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
13958
- "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
13959
- "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
13960
- "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
13961
- "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
13962
- };
13963
14074
  CODE_EXTENSIONS = /* @__PURE__ */ new Set([
13964
14075
  ".ts",
13965
14076
  ".tsx",
@@ -20140,6 +20251,7 @@ var import_os36 = __toESM(require("os"));
20140
20251
  var import_path41 = __toESM(require("path"));
20141
20252
  init_costSync();
20142
20253
  init_litellm();
20254
+ init_cost_codex();
20143
20255
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
20144
20256
  function buildTestTimestamps(allEntries) {
20145
20257
  const testTs = /* @__PURE__ */ new Set();
@@ -20236,24 +20348,11 @@ function isAllow(decision) {
20236
20348
  function isDlp(checkedBy) {
20237
20349
  return !!checkedBy?.includes("dlp");
20238
20350
  }
20239
- var CLAUDE_PRICING2 = {
20240
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
20241
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
20242
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
20243
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20244
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20245
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20246
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20247
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
20248
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
20249
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
20250
- };
20251
20351
  function claudeModelPrice2(model) {
20252
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
20253
- for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
20254
- if (base === key || base.startsWith(key + "-") || base.startsWith(key)) return p;
20255
- }
20256
- return null;
20352
+ const t = pricingFor(model);
20353
+ if (!t) return null;
20354
+ const [i, o, cw, cr] = t;
20355
+ return { i, o, cw, cr };
20257
20356
  }
20258
20357
  function emptyClaudeCostAccumulator() {
20259
20358
  return {
@@ -20368,6 +20467,7 @@ function processCodexCostFile(filePath, start, end, acc) {
20368
20467
  return;
20369
20468
  }
20370
20469
  let sessionStart2 = "";
20470
+ let model = "";
20371
20471
  let lastTotalInput = 0;
20372
20472
  let lastTotalCached = 0;
20373
20473
  let lastTotalOutput = 0;
@@ -20385,6 +20485,10 @@ function processCodexCostFile(filePath, start, end, acc) {
20385
20485
  sessionStart2 = String(p["timestamp"] ?? "");
20386
20486
  continue;
20387
20487
  }
20488
+ if (entry.type === "turn_context" && typeof p["model"] === "string") {
20489
+ model = p["model"];
20490
+ continue;
20491
+ }
20388
20492
  if (entry.type === "event_msg" && p["type"] === "token_count") {
20389
20493
  const info = p["info"] ?? {};
20390
20494
  const usage = info["total_token_usage"] ?? {};
@@ -20399,12 +20503,17 @@ function processCodexCostFile(filePath, start, end, acc) {
20399
20503
  if (!sessionStart2) return;
20400
20504
  const ts = new Date(sessionStart2);
20401
20505
  if (ts < start || ts > end) return;
20402
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
20403
- const cost = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
20506
+ const cost = codexSessionCost(model, {
20507
+ input: lastTotalInput,
20508
+ cached: lastTotalCached,
20509
+ output: lastTotalOutput
20510
+ });
20404
20511
  acc.total += cost;
20405
20512
  acc.toolCalls += sessionToolCalls;
20406
20513
  const dateKey = sessionStart2.slice(0, 10);
20407
20514
  acc.byDay.set(dateKey, (acc.byDay.get(dateKey) ?? 0) + cost);
20515
+ const normModel = normalizeModel(model || "gpt-5");
20516
+ acc.byModel.set(normModel, (acc.byModel.get(normModel) ?? 0) + cost);
20408
20517
  }
20409
20518
  function listCodexSessionFiles2(sessionsBase) {
20410
20519
  const jsonlFiles = [];
@@ -20442,13 +20551,25 @@ function listCodexSessionFiles2(sessionsBase) {
20442
20551
  }
20443
20552
  return jsonlFiles;
20444
20553
  }
20554
+ function mergeByModel(...maps) {
20555
+ const out = /* @__PURE__ */ new Map();
20556
+ for (const m of maps) {
20557
+ for (const [k, v] of m) out.set(k, (out.get(k) ?? 0) + v);
20558
+ }
20559
+ return out;
20560
+ }
20445
20561
  function loadCodexCost(start, end, sessionsBase) {
20446
- const acc = { total: 0, toolCalls: 0, byDay: /* @__PURE__ */ new Map() };
20562
+ const acc = {
20563
+ total: 0,
20564
+ toolCalls: 0,
20565
+ byDay: /* @__PURE__ */ new Map(),
20566
+ byModel: /* @__PURE__ */ new Map()
20567
+ };
20447
20568
  const files = listCodexSessionFiles2(sessionsBase);
20448
20569
  for (const filePath of files) {
20449
20570
  processCodexCostFile(filePath, start, end, acc);
20450
20571
  }
20451
- return { total: acc.total, byDay: acc.byDay, toolCalls: acc.toolCalls };
20572
+ return { total: acc.total, byDay: acc.byDay, byModel: acc.byModel, toolCalls: acc.toolCalls };
20452
20573
  }
20453
20574
  var GEMINI_FALLBACK_MODELS2 = ["gemini-2.5-flash", "gemini-2.0-flash"];
20454
20575
  function geminiPriceFor2(model) {
@@ -20737,7 +20858,7 @@ function aggregateReportFromAudit(period, opts = {}) {
20737
20858
  cacheWriteTokens: claudeCost.cacheWriteTokens,
20738
20859
  cacheReadTokens: claudeCost.cacheReadTokens + geminiCost.cacheReadTokens,
20739
20860
  byDay: claudeCost.byDay,
20740
- byModel: claudeCost.byModel,
20861
+ byModel: mergeByModel(claudeCost.byModel, codexCost.byModel),
20741
20862
  byProject: claudeCost.byProject
20742
20863
  },
20743
20864
  toolMap,
@@ -22010,6 +22131,8 @@ var import_chalk19 = __toESM(require("chalk"));
22010
22131
  var import_child_process10 = require("child_process");
22011
22132
  var import_execa3 = require("execa");
22012
22133
  init_orchestrator();
22134
+ init_cloud();
22135
+ init_config();
22013
22136
  init_provenance();
22014
22137
  init_mcp_pin();
22015
22138
  init_mcp_tools();
@@ -22038,6 +22161,60 @@ function normalizeClientName(name) {
22038
22161
  const sanitized = sanitize4(name).slice(0, 40);
22039
22162
  return sanitized.length > 0 ? sanitized : void 0;
22040
22163
  }
22164
+ function reportPinMismatchToCloud(serverKey, agent) {
22165
+ try {
22166
+ const creds = getCredentials();
22167
+ if (!creds) return;
22168
+ void auditLocalAllow(
22169
+ `mcp-server:${serverKey}`,
22170
+ { serverKey, reason: "tool-pin-mismatch" },
22171
+ "mcp-pin-mismatch",
22172
+ creds,
22173
+ { mcpServer: serverKey, agent },
22174
+ void 0,
22175
+ false,
22176
+ {
22177
+ ruleName: "MCP tool definitions changed (possible rug pull)",
22178
+ 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}`
22179
+ }
22180
+ );
22181
+ } catch {
22182
+ }
22183
+ }
22184
+ function reportInventoryToCloud(serverKey, toolCount, agent) {
22185
+ try {
22186
+ const creds = getCredentials();
22187
+ if (!creds) return;
22188
+ void auditLocalAllow(
22189
+ `mcp-server:${serverKey}`,
22190
+ { serverKey, toolCount },
22191
+ "mcp-discovered",
22192
+ creds,
22193
+ { mcpServer: serverKey, agent },
22194
+ void 0,
22195
+ false,
22196
+ { mcpToolCount: toolCount }
22197
+ );
22198
+ } catch {
22199
+ }
22200
+ }
22201
+ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
22202
+ try {
22203
+ const creds = getCredentials();
22204
+ if (!creds) return;
22205
+ void auditLocalAllow(
22206
+ `mcp-server:${serverKey}`,
22207
+ { serverKey, responseBytes },
22208
+ "mcp-large-response",
22209
+ creds,
22210
+ { mcpServer: serverKey, agent },
22211
+ void 0,
22212
+ false,
22213
+ { mcpResponseBytes: responseBytes }
22214
+ );
22215
+ } catch {
22216
+ }
22217
+ }
22041
22218
  function tokenize4(cmd) {
22042
22219
  const tokens = [];
22043
22220
  let current = "";
@@ -22298,6 +22475,7 @@ async function runMcpGateway(upstreamCommand) {
22298
22475
  const currentHash = hashToolDefinitions(tools);
22299
22476
  const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
22300
22477
  const token = getInternalToken();
22478
+ reportInventoryToCloud(serverKey, tools.length, clientName);
22301
22479
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
22302
22480
  const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
22303
22481
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
@@ -22362,6 +22540,7 @@ async function runMcpGateway(upstreamCommand) {
22362
22540
  console.error(import_chalk19.default.red(" Session quarantined \u2014 all tool calls blocked."));
22363
22541
  console.error(import_chalk19.default.yellow(` Run: node9 mcp pin update ${serverKey}
22364
22542
  `));
22543
+ reportPinMismatchToCloud(serverKey, clientName);
22365
22544
  const errorResponse = {
22366
22545
  jsonrpc: "2.0",
22367
22546
  id: parsed.id,
@@ -22407,6 +22586,7 @@ async function runMcpGateway(upstreamCommand) {
22407
22586
  `\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
22408
22587
  )
22409
22588
  );
22589
+ reportLargeResponseToCloud(serverKey, line.length, clientName);
22410
22590
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
22411
22591
  const token = getInternalToken();
22412
22592
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/large-response`, {
@@ -23510,40 +23690,19 @@ var import_fs45 = __toESM(require("fs"));
23510
23690
  var import_path46 = __toESM(require("path"));
23511
23691
  var import_os40 = __toESM(require("os"));
23512
23692
  init_scan_summary();
23513
- var CLAUDE_PRICING3 = {
23514
- "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
23515
- "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
23516
- "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
23517
- "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23518
- "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23519
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23520
- "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23521
- "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
23522
- "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
23523
- "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
23524
- };
23693
+ init_litellm();
23694
+ init_cost_gemini();
23695
+ init_cost_codex();
23525
23696
  function modelPrice(model) {
23526
- const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
23527
- for (const [key, p] of Object.entries(CLAUDE_PRICING3)) {
23528
- if (base === key || base.startsWith(key)) return p;
23529
- }
23530
- return null;
23697
+ const t = pricingFor(model);
23698
+ if (!t) return null;
23699
+ const [i, o, cw, cr] = t;
23700
+ return { i, o, cw, cr };
23531
23701
  }
23532
- var GEMINI_PRICING2 = {
23533
- "gemini-2.5-pro": { i: 125e-8, o: 1e-5, cr: 31e-8 },
23534
- "gemini-2.5-flash": { i: 15e-8, o: 6e-7, cr: 375e-10 },
23535
- "gemini-2.0-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 },
23536
- "gemini-1.5-pro": { i: 125e-8, o: 5e-6, cr: 3125e-10 },
23537
- "gemini-1.5-flash": { i: 75e-9, o: 3e-7, cr: 1875e-11 },
23538
- "gemini-3-flash": { i: 1e-7, o: 4e-7, cr: 25e-9 }
23539
- };
23540
23702
  function geminiModelPrice2(model) {
23541
- const base = model.replace(/-preview$/, "").replace(/-exp$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
23542
- for (const [key, p] of Object.entries(GEMINI_PRICING2)) {
23543
- if (base === key || base.startsWith(key)) return p;
23544
- }
23545
- if (base.includes("flash")) return GEMINI_PRICING2["gemini-2.0-flash"];
23546
- return null;
23703
+ const p = geminiPriceFor(model);
23704
+ if (!p) return null;
23705
+ return { i: p.input, o: p.output, cr: p.cacheRead };
23547
23706
  }
23548
23707
  function encodeProjectPath(projectPath) {
23549
23708
  return projectPath.replace(/\//g, "-");
@@ -23836,6 +23995,7 @@ function buildCodexSessions(days, allAuditEntries) {
23836
23995
  let lastTotalInput = 0;
23837
23996
  let lastTotalCached = 0;
23838
23997
  let lastTotalOutput = 0;
23998
+ let model = "";
23839
23999
  for (const line of lines) {
23840
24000
  if (!line.trim()) continue;
23841
24001
  let entry;
@@ -23851,6 +24011,10 @@ function buildCodexSessions(days, allAuditEntries) {
23851
24011
  cwd = String(p["cwd"] ?? "");
23852
24012
  continue;
23853
24013
  }
24014
+ if (entry.type === "turn_context" && typeof p["model"] === "string") {
24015
+ model = p["model"];
24016
+ continue;
24017
+ }
23854
24018
  if (entry.type === "event_msg" && p["type"] === "user_message" && !firstPrompt) {
23855
24019
  firstPrompt = String(p["message"] ?? "");
23856
24020
  continue;
@@ -23877,8 +24041,11 @@ function buildCodexSessions(days, allAuditEntries) {
23877
24041
  }
23878
24042
  if (!sessionId || !startTime) continue;
23879
24043
  if (cutoff && new Date(startTime) < cutoff) continue;
23880
- const nonCached = Math.max(0, lastTotalInput - lastTotalCached);
23881
- const costUSD = nonCached * 5e-6 + lastTotalCached * 25e-7 + lastTotalOutput * 15e-6;
24044
+ const costUSD = codexSessionCost(model, {
24045
+ input: lastTotalInput,
24046
+ cached: lastTotalCached,
24047
+ output: lastTotalOutput
24048
+ });
23882
24049
  const windowEnd = new Date(
23883
24050
  Math.max(new Date(startTime).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
23884
24051
  ).toISOString();
@@ -23902,11 +24069,10 @@ function buildCodexSessions(days, allAuditEntries) {
23902
24069
  }
23903
24070
  function buildSessions(days, historyPath) {
23904
24071
  const hPath = historyPath ?? import_path46.default.join(import_os40.default.homedir(), ".claude", "history.jsonl");
23905
- let historyRaw;
24072
+ let historyRaw = "";
23906
24073
  try {
23907
24074
  historyRaw = import_fs45.default.readFileSync(hPath, "utf-8");
23908
24075
  } catch {
23909
- return [];
23910
24076
  }
23911
24077
  const cutoff = days !== null ? (() => {
23912
24078
  const d = /* @__PURE__ */ new Date();
@@ -24197,12 +24363,6 @@ function registerSessionsCommand(program2) {
24197
24363
  console.log("");
24198
24364
  console.log(import_chalk24.default.cyan.bold("\u{1F4CB} node9 sessions") + import_chalk24.default.dim(" \u2014 what your AI agent did"));
24199
24365
  console.log("");
24200
- const historyPath = import_path46.default.join(import_os40.default.homedir(), ".claude", "history.jsonl");
24201
- if (!import_fs45.default.existsSync(historyPath)) {
24202
- console.log(import_chalk24.default.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
24203
- console.log(import_chalk24.default.gray(" Install Claude Code, run a few sessions, then try again.\n"));
24204
- return;
24205
- }
24206
24366
  const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
24207
24367
  const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
24208
24368
  console.log(import_chalk24.default.dim(" " + rangeLabel));