@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/README.md +1 -1
- package/dist/activation.d.ts +0 -28
- package/dist/activation.js +27 -148
- package/dist/audit.d.ts +59 -1
- package/dist/audit.js +194 -4
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +78 -209
- package/dist/cost-report.d.ts +22 -0
- package/dist/cost-report.js +220 -0
- package/dist/index.js +59 -4
- package/dist/learnings.d.ts +14 -1
- package/dist/learnings.js +67 -4
- package/dist/tools-manifest.d.ts +2 -2
- package/dist/tools-manifest.js +1 -0
- package/package.json +1 -1
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 {
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
2446
|
+
top: topRaw ? parseInt(topRaw, 10) || 10 : 10,
|
|
2447
|
+
days: daysRaw ? parseInt(daysRaw, 10) || undefined : undefined,
|
|
2441
2448
|
});
|
|
2442
|
-
if (
|
|
2443
|
-
console.log(
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
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
|