@compr/opscontext-mcp 2.5.3 → 2.5.5

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.
package/dist/cli.js CHANGED
@@ -646,15 +646,13 @@ import { SERVER_COMMANDS, suggestCommands } from "./cli-commands.js";
646
646
  import { collectProjectOps, collectSystemOps } from "./collectors.js";
647
647
  import { scanCodeDir } from "./code-chunker.js";
648
648
  import { listProjects, runComplianceAudit, formatProjectList, formatPlan, scoreProject, runScoreCanary, formatScoreReport, generateScoreHTML, generateProjectScoreMD, } from "./agents.js";
649
- import { listLearnings, learningsToChunks, learningsStats, formatLearnings, saveLearning, deleteLearning, importLearningsFromFile, autoImportFromSources, LEARNING_CATEGORIES, } from "./learnings.js";
649
+ import { listLearnings, parseSince, learningsToChunks, learningsStats, formatLearnings, saveLearning, deleteLearning, importLearningsFromFile, autoImportFromSources, LEARNING_CATEGORIES, } from "./learnings.js";
650
650
  import { saveSession, loadSession, listSessions, deleteSession, formatSession, formatSessionList, } from "./sessions.js";
651
651
  import { activate, deactivate, getActivationStatus, gateCheck, } from "./activation.js";
652
652
  import { syncTierA, syncTierB, loadCommunityStore, communityRulesToChunks, mergeWithDedup, STORE_PATH as COMMUNITY_STORE_PATH, } from "./community-sync.js";
653
- import { readAuditLog, verifyChain, filterByRange, toCsv, rotateAuditLog, planRotation, listSegments, } from "./audit.js";
653
+ import { readAuditLog, verifyChain, filterByRange, toCsv, rotateAuditLog, planRotation, listSegments, acknowledgeRedaction, } from "./audit.js";
654
654
  import { loadRepoPolicy, parsePolicy, formatPolicySummary, formatValidationErrors, repoPolicyPath, } from "./policy.js";
655
- import { collectRuns, metricsFor, transcriptRoot, emptyTally, addTally, totalTokens, pricingStatus, pricingFor, } from "./transcript-collector.js";
656
- import { DEFAULT_PRICING, DEFAULT_PRICING_ASOF } from "./default-pricing.js";
657
- import { runTranscriptHeuristics, DEFAULT_COST_THRESHOLDS, } from "./detector.js";
655
+ import { buildCostReport } from "./cost-report.js";
658
656
  import { getStagedFiles, runSecretScan, runDocCoverage, runCommitMessageRequired, runRuleParity, formatSecretViolations, formatDocCoverageViolations, formatSecretViolationsJson, formatDocCoverageViolationsJson, formatCommitMessageViolations, formatCommitMessageViolationsJson, formatRuleParityViolations, formatRuleParityViolationsJson, } from "./hooks.js";
659
657
  import { safeAppend } from "./audit.js";
660
658
  import { installSkill, locateBundledSkill, buildManagedBlock, syncClaudeMd, } from "./claude-integration.js";
@@ -757,12 +755,37 @@ async function cliListProjects() {
757
755
  const text = formatProjectList(projects);
758
756
  console.log(`\n${text}`);
759
757
  }
760
- async function cliListLearnings(category) {
758
+ async function cliListLearnings(args) {
759
+ // list-learnings [category] [--since today|yesterday|ISO]. [LOCK] [LEARNINGS-LIST-SHOWS-CREATED]
760
+ let category;
761
+ let sinceSpec;
762
+ for (let i = 0; i < args.length; i++) {
763
+ if (args[i] === "--since") {
764
+ sinceSpec = args[i + 1];
765
+ if (!sinceSpec) {
766
+ console.error("--since needs a value: today, yesterday, or an ISO date");
767
+ process.exit(1);
768
+ }
769
+ i++;
770
+ }
771
+ else if (!category) {
772
+ category = args[i];
773
+ }
774
+ }
775
+ let since;
776
+ if (sinceSpec) {
777
+ const parsed = parseSince(sinceSpec);
778
+ if (!parsed) {
779
+ console.error(`--since "${sinceSpec}" is not today, yesterday, or an ISO date. Refusing to answer with a zero.`);
780
+ process.exit(1);
781
+ }
782
+ since = parsed;
783
+ }
761
784
  // Project-scoped: only show learnings for workspace projects + universal
762
785
  const projectDirs = loadProjectDirs();
763
786
  const projectNames = projectDirs.map((d) => d.name);
764
787
  const learnings = listLearnings(category, projectNames);
765
- const text = formatLearnings(learnings);
788
+ const text = formatLearnings(learnings, { since, sinceSpec });
766
789
  console.log(`\n${text}`);
767
790
  }
768
791
  async function cliSaveLearning(args) {
@@ -1966,12 +1989,39 @@ function cliAuditRotate(args) {
1966
1989
  process.exit(2);
1967
1990
  }
1968
1991
  }
1992
+ /** Acknowledge deliberately redacted audit records on the chain. [LOCK] [REDACTION-IS-A-CHAINED-RECORD] */
1993
+ function cliAuditRedactAck(args) {
1994
+ const idxAt = args.indexOf("--index");
1995
+ const reasonAt = args.indexOf("--reason");
1996
+ const raw = idxAt >= 0 ? args[idxAt + 1] ?? "" : "";
1997
+ const reason = reasonAt >= 0 ? args[reasonAt + 1] ?? "" : "";
1998
+ const indices = raw.split(",").map((x) => Number(x.trim())).filter((n) => Number.isInteger(n) && n >= 0);
1999
+ if (indices.length === 0 || !reason.trim()) {
2000
+ console.error(`usage: contextengine audit-redact-ack --index <i,j,k> --reason "<what was removed and why>"`);
2001
+ console.error(` Indices are the ones 'audit-verify' lists as altered. Only altered records can be acknowledged.`);
2002
+ process.exit(1);
2003
+ }
2004
+ const r = acknowledgeRedaction(indices, reason, "cli");
2005
+ for (const x of r.rejected)
2006
+ console.error(` ✗ ${x.index}: ${x.why}`);
2007
+ if (!r.record) {
2008
+ console.error(`\nNothing acknowledged.`);
2009
+ process.exit(1);
2010
+ }
2011
+ console.log(`\n✅ Acknowledged ${r.acknowledged.length} redacted record(s): ${r.acknowledged.join(", ")}`);
2012
+ console.log(` Chained as audit.redact, hash ${r.record.hash.slice(0, 16)}…`);
2013
+ const after = verifyChain();
2014
+ console.log(` audit-verify now: ${after.ok ? "OK" : "FAILED"}, ${(after.redactedIndices ?? []).length} redacted, ${(after.tamperedIndices ?? []).length} altered.`);
2015
+ }
1969
2016
  async function cliAuditVerify() {
1970
2017
  const report = verifyChain();
1971
2018
  const forks = report.forkIndices ?? [];
2019
+ const redacted = report.redactedIndices ?? [];
1972
2020
  if (report.ok) {
1973
2021
  console.log(`✅ Audit chain verified — ${report.total} record(s).`);
1974
- console.log(` No record was altered, and no history is missing.`);
2022
+ console.log(redacted.length === 0
2023
+ ? ` No record was altered, and no history is missing.`
2024
+ : ` No history is missing. ${redacted.length} record(s) redacted and acknowledged on the chain (indices ${redacted.slice(0, 8).join(", ")}${redacted.length > 8 ? ", …" : ""}), 0 altered.`);
1975
2025
  if (forks.length > 0) {
1976
2026
  // [VERIFY-FORK-IS-NOT-TAMPER] — surface this, but do not call it tampering.
1977
2027
  console.log(`\n⚠️ ${forks.length} concurrent-append fork(s) detected (not tampering).`);
@@ -1990,6 +2040,11 @@ async function cliAuditVerify() {
1990
2040
  console.error(`\n Altered records (content does not match its own hash):`);
1991
2041
  console.error(` ${t.slice(0, 10).join(", ")}${t.length > 10 ? `, … (+${t.length - 10} more)` : ""}`);
1992
2042
  console.error(` This is tampering: the record's bytes were changed after it was written.`);
2043
+ console.error(` If this was a deliberate redaction of a secret, acknowledge it on the chain:`);
2044
+ console.error(` contextengine audit-redact-ack --index ${t.slice(0, 3).join(",")} --reason "<what was removed and why>"`);
2045
+ }
2046
+ if (redacted.length > 0) {
2047
+ console.error(`\n Also ${redacted.length} redacted record(s), acknowledged on the chain, not counted above.`);
1993
2048
  }
1994
2049
  if ((report.orphanIndices ?? []).length > 0) {
1995
2050
  const o = report.orphanIndices;
@@ -2374,218 +2429,28 @@ function cliStats() {
2374
2429
  }
2375
2430
  }
2376
2431
  // ---------------------------------------------------------------------------
2377
- // cost — multi-agent spend, read from Claude Code's own transcripts
2432
+ // cost — multi-agent spend, read from Claude Code's own transcripts.
2433
+ // Rendered by src/cost-report.ts, shared with the MCP tool. [LOCK] [COST-REPORT-ONE-RENDERER]
2378
2434
  // ---------------------------------------------------------------------------
2379
- function fmtTok(n) {
2380
- if (n >= 1e6)
2381
- return `${(n / 1e6).toFixed(1)}M`;
2382
- if (n >= 1e3)
2383
- return `${(n / 1e3).toFixed(0)}k`;
2384
- return String(n);
2385
- }
2386
- function fmtDur(ms) {
2387
- if (ms === null || !Number.isFinite(ms))
2388
- return "—";
2389
- const s = Math.round(ms / 1000);
2390
- if (s < 60)
2391
- return `${s}s`;
2392
- const m = Math.floor(s / 60);
2393
- if (m < 60)
2394
- return `${m}m${String(s % 60).padStart(2, "0")}s`;
2395
- return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
2396
- }
2397
- /** Resolve cost thresholds + pricing from policy, falling back to defaults. */
2398
- function loadCostThresholds(cwd) {
2399
- const res = loadRepoPolicy(cwd);
2400
- if (res && res.ok && res.policy.agent_cost) {
2401
- const a = res.policy.agent_cost;
2402
- // [DEFAULT-RATES-SHIP-WITH-THE-PACKAGE] — an agent_cost block that omits
2403
- // `pricing` must not silently price nothing.
2404
- const hasOwnRates = a.pricing.length > 0;
2405
- return {
2406
- t: {
2407
- billing_mode: a.billing_mode,
2408
- pricing: hasOwnRates ? a.pricing : DEFAULT_PRICING,
2409
- min_cache_efficiency: a.min_cache_efficiency,
2410
- max_tool_calls_per_agent: a.max_tool_calls_per_agent,
2411
- max_cost_per_agent_usd: a.max_cost_per_agent_usd,
2412
- min_fanout_for_canary: a.min_fanout_for_canary,
2413
- max_failed_share: a.max_failed_share,
2414
- },
2415
- source: ".contextengine/policy.json" +
2416
- (hasOwnRates ? "" : ` (rates: built-in, as of ${DEFAULT_PRICING_ASOF})`),
2417
- };
2418
- }
2419
- return {
2420
- t: DEFAULT_COST_THRESHOLDS,
2421
- source: `built-in defaults, rates as of ${DEFAULT_PRICING_ASOF} (no agent_cost in policy.json)`,
2422
- };
2423
- }
2424
2435
  async function cliCost(argv) {
2425
2436
  const flag = (name) => {
2426
2437
  const i = argv.indexOf(`--${name}`);
2427
2438
  return i >= 0 ? argv[i + 1] : undefined;
2428
2439
  };
2429
- const json = argv.includes("--json");
2430
2440
  const topRaw = flag("top");
2431
- const top = topRaw ? Math.max(1, parseInt(topRaw, 10) || 10) : 10;
2432
2441
  const daysRaw = flag("days");
2433
- const since = daysRaw ? Date.now() - parseInt(daysRaw, 10) * 86_400_000 : undefined;
2434
- const cwd = process.cwd();
2435
- const { t, source } = loadCostThresholds(cwd);
2436
- const runs = collectRuns({
2442
+ const report = buildCostReport({
2437
2443
  session: flag("session"),
2438
2444
  project: flag("project"),
2439
2445
  run: flag("run"),
2440
- since,
2446
+ top: topRaw ? parseInt(topRaw, 10) || 10 : 10,
2447
+ days: daysRaw ? parseInt(daysRaw, 10) || undefined : undefined,
2441
2448
  });
2442
- if (!runs.length) {
2443
- console.log("No multi-agent runs found in " + transcriptRoot());
2444
- console.log("(fan-outs only: parent sessions are not counted — this measures delegation)");
2449
+ if (argv.includes("--json") && report.json) {
2450
+ console.log(JSON.stringify(report.json, null, 2));
2445
2451
  return;
2446
2452
  }
2447
- const scored = runs
2448
- .map((r) => ({ run: r, m: metricsFor(r, t.pricing) }))
2449
- .sort((a, b) => b.m.cost.total - a.m.cost.total);
2450
- const signals = runTranscriptHeuristics(runs, t);
2451
- if (json) {
2452
- console.log(JSON.stringify({
2453
- billing_mode: t.billing_mode,
2454
- cost_is_notional: t.billing_mode === "subscription",
2455
- thresholds_source: source,
2456
- runs: scored.map(({ run, m }) => ({
2457
- runId: run.runId, kind: run.kind, project: run.project, sessionId: run.sessionId,
2458
- volume: run.totals, intensity: {
2459
- agents: m.agents, reported: m.reported, failed: m.failed,
2460
- capacityExhausted: m.capacityExhausted, toolCalls: m.toolCalls,
2461
- medianToolCalls: m.medianToolCalls, durationMs: run.durationMs,
2462
- launchedBeforeFirstReport: m.launchedBeforeFirstReport,
2463
- },
2464
- cost: m.cost, cacheEfficiency: Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency : null,
2465
- outputShare: m.outputShare,
2466
- })),
2467
- signals,
2468
- }, null, 2));
2469
- return;
2470
- }
2471
- // Aggregate across everything in scope.
2472
- let vol = emptyTally();
2473
- let agents = 0, toolCalls = 0, failed = 0, capacity = 0, reported = 0;
2474
- let cost = 0, withoutCache = 0, unpriced = 0;
2475
- // Which models carried tokens but matched no rate — named in the output so
2476
- // the fix is actionable instead of "something was unpriced".
2477
- const unpricedModels = new Set();
2478
- for (const { run, m } of scored) {
2479
- for (const a of run.agents) {
2480
- for (const [model, tally] of a.tokensByModel) {
2481
- if (totalTokens(tally) > 0 && !pricingFor(model, t.pricing)) {
2482
- unpricedModels.add(model ?? "(no model recorded)");
2483
- }
2484
- }
2485
- }
2486
- vol = addTally(vol, run.totals);
2487
- agents += m.agents;
2488
- toolCalls += m.toolCalls;
2489
- failed += m.failed;
2490
- capacity += m.capacityExhausted;
2491
- reported += m.reported;
2492
- cost += m.cost.total;
2493
- withoutCache += m.cost.withoutCache;
2494
- unpriced += m.cost.unpricedTokens;
2495
- }
2496
- const allTok = totalTokens(vol);
2497
- const cw = vol.cacheWrite5m + vol.cacheWrite1h;
2498
- console.log("");
2499
- console.log(`MULTI-AGENT COST — ${scored.length} run(s), ${agents} subagents`);
2500
- console.log(`thresholds: ${source}`);
2501
- console.log("");
2502
- // ── 1. VOLUME ───────────────────────────────────────────────────────────
2503
- console.log("VOLUME (tokens moved)");
2504
- const volRow = (label, n) => console.log(` ${label.padEnd(16)} ${fmtTok(n).padStart(8)} ${allTok ? ((100 * n) / allTok).toFixed(1).padStart(5) : " 0.0"}%`);
2505
- volRow("cache read", vol.cacheRead);
2506
- volRow("cache write", cw);
2507
- volRow("input (fresh)", vol.input);
2508
- volRow("output", vol.output);
2509
- console.log(` ${"total".padEnd(16)} ${fmtTok(allTok).padStart(8)}`);
2510
- console.log("");
2511
- // ── 2. VALUED COST ──────────────────────────────────────────────────────
2512
- const notional = t.billing_mode === "subscription";
2513
- let ci = 0, ccw = 0, ccr = 0, co = 0;
2514
- for (const { m } of scored) {
2515
- ci += m.cost.input;
2516
- ccw += m.cost.cacheWrite;
2517
- ccr += m.cost.cacheRead;
2518
- co += m.cost.output;
2519
- }
2520
- const agg = {
2521
- input: ci, cacheWrite: ccw, cacheRead: ccr, output: co,
2522
- total: cost, withoutCache, unpricedTokens: unpriced,
2523
- };
2524
- const status = pricingStatus(agg);
2525
- console.log(`VALUED COST (API list prices)${notional && status !== "unpriced" ? " — NOTIONAL, NOT BILLED" : ""}`);
2526
- // [NEVER-RENDER-AN-UNKNOWN-AS-A-NUMBER] — with nothing priced there is no
2527
- // cost to show. Printing a $0.00 table here reads as "this run was free"
2528
- // and "caching saved 0%", both false.
2529
- if (status === "unpriced") {
2530
- console.log(` UNPRICED — no rate matched any model in this data, so no cost can be`);
2531
- console.log(` stated. ${fmtTok(unpriced)} tokens were moved. This is an unknown, not $0.`);
2532
- console.log("");
2533
- console.log(` Models seen without a rate: ${[...unpricedModels].sort().join(", ") || "(unknown)"}`);
2534
- console.log(` Add them to .contextengine/policy.json → agent_cost.pricing.`);
2535
- console.log("");
2536
- }
2537
- else {
2538
- if (notional) {
2539
- console.log(" This machine runs Claude Code on a subscription: no dollar below is");
2540
- console.log(" debited. Use these figures to compare approaches, not as spend.");
2541
- }
2542
- const costRow = (label, n) => console.log(` ${label.padEnd(16)} ${("$" + n.toFixed(2)).padStart(8)} ${cost ? ((100 * n) / cost).toFixed(1).padStart(5) : " 0.0"}%`);
2543
- costRow("cache read", ccr);
2544
- costRow("cache write", ccw);
2545
- costRow("input (fresh)", ci);
2546
- costRow("output", co);
2547
- console.log(` ${"total".padEnd(16)} ${("$" + cost.toFixed(2)).padStart(8)}`);
2548
- console.log(` ${"without cache".padEnd(16)} ${("$" + withoutCache.toFixed(2)).padStart(8)} ` +
2549
- `caching saved $${(withoutCache - cost).toFixed(2)} (${withoutCache ? (100 * (1 - cost / withoutCache)).toFixed(0) : "0"}%)`);
2550
- if (status === "partial") {
2551
- console.log(` ⚠ ${fmtTok(unpriced)} tokens UNPRICED and NOT in the figures above` +
2552
- ` (${[...unpricedModels].sort().join(", ") || "unknown model"}) — the total is a floor, not the cost`);
2553
- }
2554
- console.log("");
2555
- }
2556
- // ── 3. INTENSITY (the capacity proxy) ───────────────────────────────────
2557
- console.log(`INTENSITY (capacity proxy${notional ? " — the scarce resource here" : ""})`);
2558
- console.log(` subagents ${String(agents).padStart(8)}`);
2559
- console.log(` reported ${String(reported).padStart(8)}`);
2560
- console.log(` returned nothing ${String(failed).padStart(8)}${failed ? ` (${((100 * failed) / agents).toFixed(0)}% of the fleet)` : ""}`);
2561
- console.log(` died at window ${String(capacity).padStart(8)}${capacity ? " ← capacity spent for no result" : ""}`);
2562
- console.log(` tool calls ${String(toolCalls).padStart(8)} (${(toolCalls / Math.max(1, agents)).toFixed(1)}/agent)`);
2563
- console.log(` cache reuse ${(cw ? (vol.cacheRead / cw).toFixed(1) + "x" : "—").padStart(8)} ${cw && vol.cacheRead / cw < t.min_cache_efficiency ? "← below floor, prefix is being rebuilt" : "(higher is better)"}`);
2564
- console.log("");
2565
- // ── Top runs ────────────────────────────────────────────────────────────
2566
- console.log(`TOP RUNS BY VALUED COST (${Math.min(top, scored.length)} of ${scored.length})`);
2567
- console.log(` ${"cost".padStart(8)} ${"agents".padStart(6)} ${"dead".padStart(4)} ${"tools".padStart(5)} ${"reuse".padStart(6)} ${"dur".padStart(7)} run`);
2568
- for (const { run, m } of scored.slice(0, top)) {
2569
- const reuse = Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency.toFixed(1) + "x" : "—";
2570
- console.log(` ${("$" + m.cost.total.toFixed(2)).padStart(8)} ${String(m.agents).padStart(6)} ` +
2571
- `${String(m.failed).padStart(4)} ${String(m.medianToolCalls).padStart(5)} ${reuse.padStart(6)} ` +
2572
- `${fmtDur(run.durationMs).padStart(7)} ${run.runId} ${run.project.replace(/^-Users-yan-/, "")}`);
2573
- }
2574
- console.log("");
2575
- // ── Signals ─────────────────────────────────────────────────────────────
2576
- if (!signals.length) {
2577
- console.log("✅ No context_burn or fanout_without_canary signals.");
2578
- }
2579
- else {
2580
- const crit = signals.filter((s) => s.severity === "critical");
2581
- console.log(`SIGNALS — ${signals.length} (${crit.length} critical)`);
2582
- for (const s of signals.slice(0, 20)) {
2583
- console.log(` ${s.severity === "critical" ? "🔴" : "⚠️ "} [${s.kind}] ${s.reason}`);
2584
- }
2585
- if (signals.length > 20)
2586
- console.log(` … ${signals.length - 20} more (use --json)`);
2587
- }
2588
- console.log("");
2453
+ console.log(report.text);
2589
2454
  }
2590
2455
  /** Package version, read from the installed package.json rather than hardcoded. */
2591
2456
  function readPackageVersion() {
@@ -2618,7 +2483,8 @@ Usage:
2618
2483
  contextengine search <query> [-n N] Search indexed knowledge (default: top 5)
2619
2484
  contextengine list-sources Show all indexed sources with chunk counts
2620
2485
  contextengine list-projects Discover and analyze all projects (Pro)
2621
- contextengine list-learnings [cat] List all learnings (optional: filter by category)
2486
+ contextengine list-learnings [cat] [--since today|yesterday|ISO]
2487
+ List learnings with their created instant (UTC + Europe/Zurich)
2622
2488
  contextengine save-learning <text> -c <category> Save a learning
2623
2489
  contextengine delete-learning <id> Delete a learning by ID
2624
2490
  contextengine import-learnings <file> [-c cat] [-p project] Bulk-import learnings
@@ -2635,6 +2501,7 @@ Usage:
2635
2501
  Export hash-chained audit log (evidence aligned with
2636
2502
  SOC 2 CC7.2 + ISO 27001 A.12.4.1 — not a certification)
2637
2503
  contextengine audit-verify Verify audit log chain integrity (tamper detection)
2504
+ contextengine audit-redact-ack Acknowledge deliberately redacted records on the chain (--index i,j --reason "...")
2638
2505
  contextengine audit-rotate [--keep-days N] [--max-records N] [--dry-run]
2639
2506
  Move old history into an archive segment. Archives
2640
2507
  whatever is older than N days (default 30) OR beyond
@@ -2738,8 +2605,7 @@ else if (command === "list-projects") {
2738
2605
  });
2739
2606
  }
2740
2607
  else if (command === "list-learnings") {
2741
- const category = process.argv[3];
2742
- cliListLearnings(category).catch((err) => {
2608
+ cliListLearnings(process.argv.slice(3)).catch((err) => {
2743
2609
  console.error("Error:", err);
2744
2610
  process.exit(1);
2745
2611
  });
@@ -2840,6 +2706,9 @@ else if (command === "sync-claude-md") {
2840
2706
  process.exit(1);
2841
2707
  });
2842
2708
  }
2709
+ else if (command === "audit-redact-ack") {
2710
+ cliAuditRedactAck(process.argv.slice(3));
2711
+ }
2843
2712
  else if (command === "audit-rotate") {
2844
2713
  cliAuditRotate(process.argv.slice(3));
2845
2714
  }
@@ -0,0 +1,22 @@
1
+ import { type CostThresholds } from "./detector.js";
2
+ export interface CostReportOptions {
3
+ session?: string;
4
+ project?: string;
5
+ run?: string;
6
+ days?: number;
7
+ top?: number;
8
+ }
9
+ export interface CostReport {
10
+ /** Human-readable report, what the CLI prints. */
11
+ text: string;
12
+ /** Structured report, what `--json` prints. null when no runs were found. */
13
+ json: Record<string, unknown> | null;
14
+ runs: number;
15
+ }
16
+ /** Resolve cost thresholds + pricing from policy, falling back to defaults. */
17
+ export declare function loadCostThresholds(cwd: string): {
18
+ t: CostThresholds;
19
+ source: string;
20
+ };
21
+ export declare function buildCostReport(opts?: CostReportOptions, cwd?: string): CostReport;
22
+ //# sourceMappingURL=cost-report.d.ts.map
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Multi-agent cost report, shared by the CLI (`contextengine cost`) and the MCP tool
3
+ * (`agent_cost`). One renderer, two surfaces.
4
+ *
5
+ * [LOCKED] [COST-REPORT-ONE-RENDERER] — 2026-08-21
6
+ * [NEVER] render the cost report in cli.ts or index.ts directly.
7
+ * WHY: the CLI shipped on 2026-08-20 as 170 lines of console.log; an MCP tool written the same
8
+ * way would have been a second copy of every threshold, label and guard (NOTIONAL, UNPRICED,
9
+ * floor-not-cost) that drifts the first time one of them is edited.
10
+ * FIX: buildCostReport() returns { text, json }; cli.ts prints, index.ts responds. Both surfaces
11
+ * read the same thresholds from .contextengine/policy.json via loadCostThresholds().
12
+ */
13
+ import { collectRuns, metricsFor, transcriptRoot, emptyTally, addTally, totalTokens, pricingStatus, pricingFor, } from "./transcript-collector.js";
14
+ import { DEFAULT_PRICING, DEFAULT_PRICING_ASOF } from "./default-pricing.js";
15
+ import { runTranscriptHeuristics, DEFAULT_COST_THRESHOLDS, } from "./detector.js";
16
+ import { loadRepoPolicy } from "./policy.js";
17
+ function fmtTok(n) {
18
+ if (n >= 1e6)
19
+ return `${(n / 1e6).toFixed(1)}M`;
20
+ if (n >= 1e3)
21
+ return `${(n / 1e3).toFixed(0)}k`;
22
+ return String(n);
23
+ }
24
+ function fmtDur(ms) {
25
+ if (ms === null || !Number.isFinite(ms))
26
+ return "—";
27
+ const s = Math.round(ms / 1000);
28
+ if (s < 60)
29
+ return `${s}s`;
30
+ const m = Math.floor(s / 60);
31
+ if (m < 60)
32
+ return `${m}m${String(s % 60).padStart(2, "0")}s`;
33
+ return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
34
+ }
35
+ /** Resolve cost thresholds + pricing from policy, falling back to defaults. */
36
+ export function loadCostThresholds(cwd) {
37
+ const res = loadRepoPolicy(cwd);
38
+ if (res && res.ok && res.policy.agent_cost) {
39
+ const a = res.policy.agent_cost;
40
+ // [DEFAULT-RATES-SHIP-WITH-THE-PACKAGE] — an agent_cost block that omits
41
+ // `pricing` must not silently price nothing.
42
+ const hasOwnRates = a.pricing.length > 0;
43
+ return {
44
+ t: {
45
+ billing_mode: a.billing_mode,
46
+ pricing: hasOwnRates ? a.pricing : DEFAULT_PRICING,
47
+ min_cache_efficiency: a.min_cache_efficiency,
48
+ max_tool_calls_per_agent: a.max_tool_calls_per_agent,
49
+ max_cost_per_agent_usd: a.max_cost_per_agent_usd,
50
+ min_fanout_for_canary: a.min_fanout_for_canary,
51
+ max_failed_share: a.max_failed_share,
52
+ },
53
+ source: ".contextengine/policy.json" +
54
+ (hasOwnRates ? "" : ` (rates: built-in, as of ${DEFAULT_PRICING_ASOF})`),
55
+ };
56
+ }
57
+ return {
58
+ t: DEFAULT_COST_THRESHOLDS,
59
+ source: `built-in defaults, rates as of ${DEFAULT_PRICING_ASOF} (no agent_cost in policy.json)`,
60
+ };
61
+ }
62
+ export function buildCostReport(opts = {}, cwd = process.cwd()) {
63
+ const out = [];
64
+ const line = (s = "") => { out.push(s); };
65
+ const top = Math.max(1, opts.top ?? 10);
66
+ const since = opts.days ? Date.now() - opts.days * 86_400_000 : undefined;
67
+ const { t, source } = loadCostThresholds(cwd);
68
+ const runs = collectRuns({
69
+ session: opts.session,
70
+ project: opts.project,
71
+ run: opts.run,
72
+ since,
73
+ });
74
+ if (!runs.length) {
75
+ line("No multi-agent runs found in " + transcriptRoot());
76
+ line("(fan-outs only: parent sessions are not counted — this measures delegation)");
77
+ return { text: out.join("\n"), json: null, runs: 0 };
78
+ }
79
+ const scored = runs
80
+ .map((r) => ({ run: r, m: metricsFor(r, t.pricing) }))
81
+ .sort((a, b) => b.m.cost.total - a.m.cost.total);
82
+ const signals = runTranscriptHeuristics(runs, t);
83
+ const json = {
84
+ billing_mode: t.billing_mode,
85
+ cost_is_notional: t.billing_mode === "subscription",
86
+ thresholds_source: source,
87
+ runs: scored.map(({ run, m }) => ({
88
+ runId: run.runId, kind: run.kind, project: run.project, sessionId: run.sessionId,
89
+ volume: run.totals, intensity: {
90
+ agents: m.agents, reported: m.reported, failed: m.failed,
91
+ capacityExhausted: m.capacityExhausted, toolCalls: m.toolCalls,
92
+ medianToolCalls: m.medianToolCalls, durationMs: run.durationMs,
93
+ launchedBeforeFirstReport: m.launchedBeforeFirstReport,
94
+ },
95
+ cost: m.cost, cacheEfficiency: Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency : null,
96
+ outputShare: m.outputShare,
97
+ })),
98
+ signals,
99
+ };
100
+ // Aggregate across everything in scope.
101
+ let vol = emptyTally();
102
+ let agents = 0, toolCalls = 0, failed = 0, capacity = 0, reported = 0;
103
+ let cost = 0, withoutCache = 0, unpriced = 0;
104
+ // Which models carried tokens but matched no rate — named in the output so
105
+ // the fix is actionable instead of "something was unpriced".
106
+ const unpricedModels = new Set();
107
+ for (const { run, m } of scored) {
108
+ for (const a of run.agents) {
109
+ for (const [model, tally] of a.tokensByModel) {
110
+ if (totalTokens(tally) > 0 && !pricingFor(model, t.pricing)) {
111
+ unpricedModels.add(model ?? "(no model recorded)");
112
+ }
113
+ }
114
+ }
115
+ vol = addTally(vol, run.totals);
116
+ agents += m.agents;
117
+ toolCalls += m.toolCalls;
118
+ failed += m.failed;
119
+ capacity += m.capacityExhausted;
120
+ reported += m.reported;
121
+ cost += m.cost.total;
122
+ withoutCache += m.cost.withoutCache;
123
+ unpriced += m.cost.unpricedTokens;
124
+ }
125
+ const allTok = totalTokens(vol);
126
+ const cw = vol.cacheWrite5m + vol.cacheWrite1h;
127
+ line();
128
+ line(`MULTI-AGENT COST — ${scored.length} run(s), ${agents} subagents`);
129
+ line(`thresholds: ${source}`);
130
+ line();
131
+ // ── 1. VOLUME ───────────────────────────────────────────────────────────
132
+ line("VOLUME (tokens moved)");
133
+ const volRow = (label, n) => line(` ${label.padEnd(16)} ${fmtTok(n).padStart(8)} ${allTok ? ((100 * n) / allTok).toFixed(1).padStart(5) : " 0.0"}%`);
134
+ volRow("cache read", vol.cacheRead);
135
+ volRow("cache write", cw);
136
+ volRow("input (fresh)", vol.input);
137
+ volRow("output", vol.output);
138
+ line(` ${"total".padEnd(16)} ${fmtTok(allTok).padStart(8)}`);
139
+ line();
140
+ // ── 2. VALUED COST ──────────────────────────────────────────────────────
141
+ const notional = t.billing_mode === "subscription";
142
+ let ci = 0, ccw = 0, ccr = 0, co = 0;
143
+ for (const { m } of scored) {
144
+ ci += m.cost.input;
145
+ ccw += m.cost.cacheWrite;
146
+ ccr += m.cost.cacheRead;
147
+ co += m.cost.output;
148
+ }
149
+ const agg = {
150
+ input: ci, cacheWrite: ccw, cacheRead: ccr, output: co,
151
+ total: cost, withoutCache, unpricedTokens: unpriced,
152
+ };
153
+ const status = pricingStatus(agg);
154
+ line(`VALUED COST (API list prices)${notional && status !== "unpriced" ? " — NOTIONAL, NOT BILLED" : ""}`);
155
+ // [NEVER-RENDER-AN-UNKNOWN-AS-A-NUMBER] — with nothing priced there is no
156
+ // cost to show. Printing a $0.00 table here reads as "this run was free"
157
+ // and "caching saved 0%", both false.
158
+ if (status === "unpriced") {
159
+ line(` UNPRICED — no rate matched any model in this data, so no cost can be`);
160
+ line(` stated. ${fmtTok(unpriced)} tokens were moved. This is an unknown, not $0.`);
161
+ line();
162
+ line(` Models seen without a rate: ${[...unpricedModels].sort().join(", ") || "(unknown)"}`);
163
+ line(` Add them to .contextengine/policy.json → agent_cost.pricing.`);
164
+ line();
165
+ }
166
+ else {
167
+ if (notional) {
168
+ line(" This machine runs Claude Code on a subscription: no dollar below is");
169
+ line(" debited. Use these figures to compare approaches, not as spend.");
170
+ }
171
+ const costRow = (label, n) => line(` ${label.padEnd(16)} ${("$" + n.toFixed(2)).padStart(8)} ${cost ? ((100 * n) / cost).toFixed(1).padStart(5) : " 0.0"}%`);
172
+ costRow("cache read", ccr);
173
+ costRow("cache write", ccw);
174
+ costRow("input (fresh)", ci);
175
+ costRow("output", co);
176
+ line(` ${"total".padEnd(16)} ${("$" + cost.toFixed(2)).padStart(8)}`);
177
+ line(` ${"without cache".padEnd(16)} ${("$" + withoutCache.toFixed(2)).padStart(8)} ` +
178
+ `caching saved $${(withoutCache - cost).toFixed(2)} (${withoutCache ? (100 * (1 - cost / withoutCache)).toFixed(0) : "0"}%)`);
179
+ if (status === "partial") {
180
+ line(` ⚠ ${fmtTok(unpriced)} tokens UNPRICED and NOT in the figures above` +
181
+ ` (${[...unpricedModels].sort().join(", ") || "unknown model"}) — the total is a floor, not the cost`);
182
+ }
183
+ line();
184
+ }
185
+ // ── 3. INTENSITY (the capacity proxy) ───────────────────────────────────
186
+ line(`INTENSITY (capacity proxy${notional ? " — the scarce resource here" : ""})`);
187
+ line(` subagents ${String(agents).padStart(8)}`);
188
+ line(` reported ${String(reported).padStart(8)}`);
189
+ line(` returned nothing ${String(failed).padStart(8)}${failed ? ` (${((100 * failed) / agents).toFixed(0)}% of the fleet)` : ""}`);
190
+ line(` died at window ${String(capacity).padStart(8)}${capacity ? " ← capacity spent for no result" : ""}`);
191
+ line(` tool calls ${String(toolCalls).padStart(8)} (${(toolCalls / Math.max(1, agents)).toFixed(1)}/agent)`);
192
+ line(` cache reuse ${(cw ? (vol.cacheRead / cw).toFixed(1) + "x" : "—").padStart(8)} ${cw && vol.cacheRead / cw < t.min_cache_efficiency ? "← below floor, prefix is being rebuilt" : "(higher is better)"}`);
193
+ line();
194
+ // ── Top runs ────────────────────────────────────────────────────────────
195
+ line(`TOP RUNS BY VALUED COST (${Math.min(top, scored.length)} of ${scored.length})`);
196
+ line(` ${"cost".padStart(8)} ${"agents".padStart(6)} ${"dead".padStart(4)} ${"tools".padStart(5)} ${"reuse".padStart(6)} ${"dur".padStart(7)} run`);
197
+ for (const { run, m } of scored.slice(0, top)) {
198
+ const reuse = Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency.toFixed(1) + "x" : "—";
199
+ line(` ${("$" + m.cost.total.toFixed(2)).padStart(8)} ${String(m.agents).padStart(6)} ` +
200
+ `${String(m.failed).padStart(4)} ${String(m.medianToolCalls).padStart(5)} ${reuse.padStart(6)} ` +
201
+ `${fmtDur(run.durationMs).padStart(7)} ${run.runId} ${run.project.replace(/^-Users-yan-/, "")}`);
202
+ }
203
+ line();
204
+ // ── Signals ─────────────────────────────────────────────────────────────
205
+ if (!signals.length) {
206
+ line("✅ No context_burn or fanout_without_canary signals.");
207
+ }
208
+ else {
209
+ const crit = signals.filter((s) => s.severity === "critical");
210
+ line(`SIGNALS — ${signals.length} (${crit.length} critical)`);
211
+ for (const s of signals.slice(0, 20)) {
212
+ line(` ${s.severity === "critical" ? "🔴" : "⚠️ "} [${s.kind}] ${s.reason}`);
213
+ }
214
+ if (signals.length > 20)
215
+ line(` … ${signals.length - 20} more (use --json)`);
216
+ }
217
+ line();
218
+ return { text: out.join("\n"), json, runs: scored.length };
219
+ }
220
+ //# sourceMappingURL=cost-report.js.map