@trazum/cli 1.48.0 → 1.50.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.
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +45 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +45 -1
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +34 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +161 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +56 -1
- package/src/i18n/es.ts +56 -1
- package/src/i18n/types.ts +35 -0
- package/src/index.ts +199 -26
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve as resolvePath } from 'node:path';
|
|
4
4
|
import { gunzipSync } from 'node:zlib';
|
|
5
|
-
import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, detectFromSource, matchLocale, parsePlanDocument, waiverDay, waiverHistory, proposeInit, MIN_RATE_DAYS, parseConfig, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
|
|
5
|
+
import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, budgetPositions, conform, detectFromSource, matchLocale, parsePlanDocument, waiverDay, waiverHistory, proposeInit, MIN_RATE_DAYS, parseConfig, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
|
|
6
6
|
import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
|
|
7
7
|
import { dayOf, formatGap, median, spanDays } from './time.js';
|
|
8
8
|
// Everything that reads the filesystem, on its own entry point so the web
|
|
@@ -30,6 +30,7 @@ const c = {
|
|
|
30
30
|
};
|
|
31
31
|
const VALUE_FLAGS = new Set([
|
|
32
32
|
'against',
|
|
33
|
+
'contract',
|
|
33
34
|
'from-log',
|
|
34
35
|
'min-usd',
|
|
35
36
|
'payload',
|
|
@@ -311,6 +312,7 @@ const COMMAND_FLAGS = {
|
|
|
311
312
|
models: [],
|
|
312
313
|
rank: ['level', 'model', 'calls', 'output-tokens', 'batch', 'disable', 'prompt', 'markdown-out'],
|
|
313
314
|
init: ['dry-run', 'yes', 'json', 'pricing', 'pricing-live'],
|
|
315
|
+
conform: ['contract', 'json'],
|
|
314
316
|
where: [],
|
|
315
317
|
rules: [],
|
|
316
318
|
blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
|
|
@@ -1037,6 +1039,18 @@ const INIT_LOG_CANDIDATES = [
|
|
|
1037
1039
|
* answers for a file somebody named while this answers for a repository
|
|
1038
1040
|
* nobody has vouched for.
|
|
1039
1041
|
*/
|
|
1042
|
+
/** Problems listed before the rest are counted. A wall of them helps nobody. */
|
|
1043
|
+
const MAX_CONFORM_PROBLEMS = 20;
|
|
1044
|
+
/** The contracts `--contract` accepts, so a typo is refused with the list. */
|
|
1045
|
+
const CONTRACT_NAMES = [
|
|
1046
|
+
'usage-log',
|
|
1047
|
+
'profile',
|
|
1048
|
+
'plan',
|
|
1049
|
+
'verification',
|
|
1050
|
+
'history',
|
|
1051
|
+
'connected',
|
|
1052
|
+
'cost-answer',
|
|
1053
|
+
];
|
|
1040
1054
|
/** How many source files, and how large each may be. Both reported when they bite. */
|
|
1041
1055
|
const INIT_MAX_SOURCE_FILES = 400;
|
|
1042
1056
|
const INIT_MAX_SOURCE_BYTES = 256 * 1024;
|
|
@@ -1381,6 +1395,78 @@ async function commandInit(args, config, pricing, t) {
|
|
|
1381
1395
|
console.log(c.green(t.init.wrote(configPath)));
|
|
1382
1396
|
console.log();
|
|
1383
1397
|
}
|
|
1398
|
+
/**
|
|
1399
|
+
* `trazum conform <file>` — does this document conform, and what will it not
|
|
1400
|
+
* be able to answer?
|
|
1401
|
+
*
|
|
1402
|
+
* The command that makes the five contracts something to build against rather
|
|
1403
|
+
* than something to read about. An emitter — a logging wrapper somebody wrote
|
|
1404
|
+
* this afternoon, a connector for a provider this repository has never heard
|
|
1405
|
+
* of, a dashboard writing profile documents of its own — points this at what
|
|
1406
|
+
* it produced and finds out before shipping.
|
|
1407
|
+
*
|
|
1408
|
+
* **The second half is the useful half.** "Valid" is a yes or no. "Here is
|
|
1409
|
+
* what a valid document of this shape cannot tell you, and the field that
|
|
1410
|
+
* would unlock each" is the answer somebody acts on: a usage log with no
|
|
1411
|
+
* `session` is perfectly conformant and simply has no conversation growth in
|
|
1412
|
+
* it, and an emitter that only ever hears "valid" ships it and never finds out
|
|
1413
|
+
* why half the report is empty.
|
|
1414
|
+
*
|
|
1415
|
+
* Exits 1 on a problem, so it gates. It never exits 1 on an *unavailable
|
|
1416
|
+
* finding*: choosing not to log sessions is a decision, not a defect, and a
|
|
1417
|
+
* gate that failed on it would be this tool telling somebody what to record.
|
|
1418
|
+
*/
|
|
1419
|
+
async function commandConform(args, t) {
|
|
1420
|
+
const target = args.positional[0];
|
|
1421
|
+
if (target === undefined)
|
|
1422
|
+
throw new Error(t.conform.noTarget());
|
|
1423
|
+
const named = stringFlag(args, 'contract');
|
|
1424
|
+
if (named !== undefined && !CONTRACT_NAMES.includes(named)) {
|
|
1425
|
+
throw new Error(t.conform.badContract(named, CONTRACT_NAMES.join(', ')));
|
|
1426
|
+
}
|
|
1427
|
+
const text = target === '-' ? await readInput('-', t) : await readUsageLog(target, t);
|
|
1428
|
+
const report = conform(text, named === undefined ? {} : { contract: named });
|
|
1429
|
+
if (boolFlag(args, 'json')) {
|
|
1430
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1431
|
+
if (!report.conforms)
|
|
1432
|
+
process.exitCode = 1;
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
console.log();
|
|
1436
|
+
if (report.contract === null) {
|
|
1437
|
+
console.log(c.red(t.conform.unrecognised(target)));
|
|
1438
|
+
console.log(` ${c.dim(wrap(report.because ?? '', 74, ' '))}`);
|
|
1439
|
+
console.log();
|
|
1440
|
+
process.exitCode = 1;
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
console.log(c.bold(report.records === null
|
|
1444
|
+
? t.conform.heading(target, report.contract)
|
|
1445
|
+
: t.conform.headingLog(target, report.contract, report.records)));
|
|
1446
|
+
if (report.problems.length === 0) {
|
|
1447
|
+
console.log(` ${c.green(t.conform.conforms())}`);
|
|
1448
|
+
}
|
|
1449
|
+
else {
|
|
1450
|
+
for (const problem of report.problems.slice(0, MAX_CONFORM_PROBLEMS)) {
|
|
1451
|
+
console.log(` ${c.red(t.conform.problem(problem.at, problem.kind, problem.detail))}`);
|
|
1452
|
+
}
|
|
1453
|
+
if (report.problems.length > MAX_CONFORM_PROBLEMS) {
|
|
1454
|
+
console.log(` ${c.dim(t.conform.moreProblems(report.problems.length - MAX_CONFORM_PROBLEMS))}`);
|
|
1455
|
+
}
|
|
1456
|
+
process.exitCode = 1;
|
|
1457
|
+
}
|
|
1458
|
+
if (report.unavailable.length > 0) {
|
|
1459
|
+
console.log();
|
|
1460
|
+
console.log(c.bold(t.conform.unavailableHeading()));
|
|
1461
|
+
for (const gap of report.unavailable) {
|
|
1462
|
+
console.log(` ${c.dim(wrap(t.conform.unavailable(gap.finding, gap.because, gap.unlockedBy), 74, ' '))}`);
|
|
1463
|
+
}
|
|
1464
|
+
// Said out loud, because the exit code says it silently and somebody
|
|
1465
|
+
// reading a red-and-yellow screen will assume both halves gated.
|
|
1466
|
+
console.log(` ${c.dim(wrap(t.conform.unavailableNeverGates(), 74, ' '))}`);
|
|
1467
|
+
}
|
|
1468
|
+
console.log();
|
|
1469
|
+
}
|
|
1384
1470
|
function commandModels(t, pricing) {
|
|
1385
1471
|
const n = (value) => value.toLocaleString(t.numberLocale);
|
|
1386
1472
|
const col = t.models.columns;
|
|
@@ -2082,36 +2168,36 @@ function parseWhen(args, flag, endOfDay, t, now) {
|
|
|
2082
2168
|
*/
|
|
2083
2169
|
async function commandServe(args, config, pricing, t) {
|
|
2084
2170
|
const root = process.cwd();
|
|
2085
|
-
const limitUsd = config.spend?.maxUsd;
|
|
2086
2171
|
const { resolved } = await readStore(root);
|
|
2087
|
-
const measured = resolved.records.length > 0;
|
|
2088
2172
|
/**
|
|
2089
|
-
* The
|
|
2173
|
+
* The live budget, from `budgetPositions` — the same number `store` prints
|
|
2174
|
+
* and the same one the MCP guard consults.
|
|
2090
2175
|
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2176
|
+
* **This used to read `spend.maxUsd` against the whole store**, which is a
|
|
2177
|
+
* per-log gate compared against however much history the store happened to
|
|
2178
|
+
* hold. A year of records against a monthly limit reported as a budget
|
|
2179
|
+
* position, with a straight face and no way for a caller to tell. Same
|
|
2180
|
+
* units, different denominators, and the two surfaces disagreed by exactly
|
|
2181
|
+
* as much history as the machine had. `spend.monthlyUsd` is the key for a
|
|
2182
|
+
* calendar month and nothing infers one key from the other: a repository
|
|
2183
|
+
* with a per-log gate and no monthly budget has no monthly position, and
|
|
2184
|
+
* this says so rather than picking a number that is the right shape.
|
|
2094
2185
|
*/
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
}
|
|
2100
|
-
: null;
|
|
2101
|
-
const report = bucketedProfile({
|
|
2102
|
-
provider: 'store',
|
|
2103
|
-
granularity: 'bucketed',
|
|
2104
|
-
buckets: bucketsFromRecords(resolved.records),
|
|
2105
|
-
window,
|
|
2106
|
-
gaps: [],
|
|
2107
|
-
unavailable: [],
|
|
2108
|
-
}, { catalogue: pricing });
|
|
2186
|
+
const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
|
|
2187
|
+
const standing = budget.positions[0] ?? null;
|
|
2188
|
+
const limitUsd = config.spend?.monthlyUsd;
|
|
2189
|
+
const measured = standing !== null && standing.coverage !== 'none';
|
|
2109
2190
|
const server = buildServer({
|
|
2110
2191
|
catalogue: pricing,
|
|
2111
2192
|
position: () => ({
|
|
2112
|
-
|
|
2193
|
+
// Nothing measured inside the period is `undefined`, never zero: the
|
|
2194
|
+
// endpoint's `cannot-tell` exists for exactly this, and a $0 consumed
|
|
2195
|
+
// would be the healthiest-looking budget a dead store can produce.
|
|
2196
|
+
consumedUsd: measured ? standing.consumedUsd : undefined,
|
|
2113
2197
|
limitUsd,
|
|
2114
|
-
|
|
2198
|
+
// The period, not the store's span. A caller judging staleness needs to
|
|
2199
|
+
// know which month the figure is about.
|
|
2200
|
+
window: standing === null ? null : { fromMs: standing.period.fromMs, toMs: standing.period.toMs },
|
|
2115
2201
|
}),
|
|
2116
2202
|
});
|
|
2117
2203
|
const socket = stringFlag(args, 'socket');
|
|
@@ -2123,7 +2209,10 @@ async function commandServe(args, config, pricing, t) {
|
|
|
2123
2209
|
const where = await listen(server, socket !== undefined ? { socket } : { port });
|
|
2124
2210
|
console.log(c.bold(t.serve.listening(where)));
|
|
2125
2211
|
console.log(` ${c.dim(wrap(t.serve.loopbackOnly(), 74, ' '))}`);
|
|
2126
|
-
console.log(` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(
|
|
2212
|
+
console.log(` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(standing.consumedUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`);
|
|
2213
|
+
if (standing !== null && standing.coverage === 'partial') {
|
|
2214
|
+
console.log(` ${c.yellow(wrap(t.serve.partialCoverage(standing.measuredDays, standing.elapsedDays, standing.period.id), 74, ' '))}`);
|
|
2215
|
+
}
|
|
2127
2216
|
if (limitUsd === undefined) {
|
|
2128
2217
|
console.log(` ${c.dim(wrap(t.serve.noBudget(), 74, ' '))}`);
|
|
2129
2218
|
}
|
|
@@ -2376,6 +2465,51 @@ async function commandStore(args, config, pricing, t) {
|
|
|
2376
2465
|
}
|
|
2377
2466
|
const keepDays = config.store?.keepDays;
|
|
2378
2467
|
console.log(` ${c.dim(wrap(keepDays === undefined ? t.store.noRetention() : t.store.retention(String(keepDays)), 74, ' '))}`);
|
|
2468
|
+
/**
|
|
2469
|
+
* The live budget, printed here because this is where the measurement lives.
|
|
2470
|
+
*
|
|
2471
|
+
* The same call `serve` makes and the same call the MCP guard makes, so the
|
|
2472
|
+
* three cannot disagree about how much of the month is gone — which is the
|
|
2473
|
+
* whole point of the number existing in one place.
|
|
2474
|
+
*/
|
|
2475
|
+
renderBudget(budgetPositions(resolved.records, config.spend, { catalogue: pricing }), t, n);
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* One budget standing, rendered.
|
|
2479
|
+
*
|
|
2480
|
+
* Coverage before the money, deliberately. A reader who sees "$61 of $100"
|
|
2481
|
+
* first has already formed a view by the time they reach "over three of
|
|
2482
|
+
* nineteen elapsed days", and the second sentence has to undo the first.
|
|
2483
|
+
*/
|
|
2484
|
+
function renderBudget(report, t, n) {
|
|
2485
|
+
const standing = report.positions[0];
|
|
2486
|
+
if (standing === undefined) {
|
|
2487
|
+
if (report.unmeasuredScopes.length > 0) {
|
|
2488
|
+
console.log();
|
|
2489
|
+
console.log(` ${c.dim(wrap(t.store.budgetScopesUnmeasured(report.unmeasuredScopes.length), 74, ' '))}`);
|
|
2490
|
+
}
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
console.log();
|
|
2494
|
+
console.log(c.bold(t.store.budgetHeading(standing.period.id)));
|
|
2495
|
+
if (standing.coverage === 'none') {
|
|
2496
|
+
// Nothing measured is never rendered as nothing spent. A dead store and a
|
|
2497
|
+
// quiet month produce the same zero, and only one of them is good news.
|
|
2498
|
+
console.log(` ${c.red(wrap(t.store.budgetNothingMeasured(standing.elapsedDays), 74, ' '))}`);
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
if (standing.coverage === 'partial') {
|
|
2502
|
+
console.log(` ${c.yellow(wrap(t.store.budgetPartial(standing.measuredDays, standing.elapsedDays, standing.unmeasuredDays.join(', ')), 74, ' '))}`);
|
|
2503
|
+
}
|
|
2504
|
+
const share = standing.burn.consumedShare;
|
|
2505
|
+
console.log(` ${t.store.budgetStanding(formatUsd(standing.consumedUsd), formatUsd(standing.limitUsd), share === null ? '—' : `${Math.round(share * 100)}%`, n(standing.measuredDays), n(standing.period.days))}`);
|
|
2506
|
+
const line = t.store.budgetShape(standing.burn.shape, Math.round(standing.burn.elapsedShare * 100), standing.coverage);
|
|
2507
|
+
console.log(` ${standing.verdict === 'over' ? c.red(line) : c.dim(wrap(line, 74, ' '))}`);
|
|
2508
|
+
// Only where there is a shape to disclaim. "That is a shape, not a forecast"
|
|
2509
|
+
// under "nothing to compare against" is a disclaimer about nothing.
|
|
2510
|
+
if (standing.burn.shape !== 'cannot-tell') {
|
|
2511
|
+
console.log(` ${c.dim(wrap(t.store.budgetNeverForecast(), 74, ' '))}`);
|
|
2512
|
+
}
|
|
2379
2513
|
}
|
|
2380
2514
|
/**
|
|
2381
2515
|
* `trazum connect <provider>` — the bill, read from the provider.
|
|
@@ -6231,6 +6365,9 @@ async function main() {
|
|
|
6231
6365
|
case 'models':
|
|
6232
6366
|
commandModels(t, pricing);
|
|
6233
6367
|
break;
|
|
6368
|
+
case 'conform':
|
|
6369
|
+
await commandConform(args, t);
|
|
6370
|
+
break;
|
|
6234
6371
|
case 'init':
|
|
6235
6372
|
await commandInit(args, config, pricing, t);
|
|
6236
6373
|
break;
|