@trazum/cli 1.38.0 → 1.40.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/README.md +2 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +101 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +53 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +227 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +123 -0
- package/src/i18n/es.ts +126 -0
- package/src/i18n/types.ts +55 -1
- package/src/index.ts +269 -0
package/src/index.ts
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
cacheableMinimum,
|
|
12
12
|
analyzeCachePrefix,
|
|
13
13
|
billLevers,
|
|
14
|
+
buildHistory,
|
|
14
15
|
buildPlan,
|
|
16
|
+
storedReportFrom,
|
|
17
|
+
verifyPlan,
|
|
15
18
|
cacheEconomics,
|
|
16
19
|
cacheHitRate,
|
|
17
20
|
contextPressure,
|
|
@@ -78,7 +81,11 @@ import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cac
|
|
|
78
81
|
import { dayOf, formatGap, median, spanDays } from './time.js';
|
|
79
82
|
import type {
|
|
80
83
|
FleetSource,
|
|
84
|
+
HistoryRun,
|
|
81
85
|
MeasuredUsage,
|
|
86
|
+
PlanDocument,
|
|
87
|
+
StoredReport,
|
|
88
|
+
VerifiedAction,
|
|
82
89
|
BaselineBreach,
|
|
83
90
|
BaselineChange,
|
|
84
91
|
BaselineComparison,
|
|
@@ -475,6 +482,8 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
475
482
|
baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
|
|
476
483
|
profile: ['json', 'pricing', 'pricing-live', 'against', 'what-if', 'markdown-out', 'csv-out', 'csv-shape', 'max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'max-session-usd', 'label', 'since', 'until', 'dry-run', 'markdown-summary', 'by-source'],
|
|
477
484
|
plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
|
|
485
|
+
verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
|
|
486
|
+
history: ['json', 'markdown-out'],
|
|
478
487
|
route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
|
|
479
488
|
eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
|
|
480
489
|
prune: ['cases', 'concurrency', 'json', 'yes'],
|
|
@@ -2214,6 +2223,260 @@ function isoDate(): string {
|
|
|
2214
2223
|
* metered API calls somebody was actually billed for — the bill exists wherever
|
|
2215
2224
|
* Trazum happens to be running, so the host has no bearing on it.
|
|
2216
2225
|
*/
|
|
2226
|
+
/**
|
|
2227
|
+
* `trazum history <dir>` — many reports over many periods, as one series.
|
|
2228
|
+
*
|
|
2229
|
+
* Derived from *stored* `--json` documents, never re-parsed logs: a team can
|
|
2230
|
+
* keep a year of reports and throw the raw logs away, which is what the
|
|
2231
|
+
* privacy story requires anyway. Shapes are named — a climb, a decay, the
|
|
2232
|
+
* same action planned twice — and no series, however long, becomes a
|
|
2233
|
+
* forecast.
|
|
2234
|
+
*/
|
|
2235
|
+
async function commandHistory(args: Args, t: CliMessages): Promise<void> {
|
|
2236
|
+
const path = args.positional[0];
|
|
2237
|
+
if (path === undefined) throw new Error(t.history.noTarget());
|
|
2238
|
+
const target = await stat(path).catch(() => null);
|
|
2239
|
+
if (!target?.isDirectory()) throw new Error(t.history.noTarget());
|
|
2240
|
+
|
|
2241
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
2242
|
+
const files = entries
|
|
2243
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
2244
|
+
.map((entry) => join(path, entry.name))
|
|
2245
|
+
.sort((a, b) => a.localeCompare(b));
|
|
2246
|
+
|
|
2247
|
+
const reports: StoredReport[] = [];
|
|
2248
|
+
const plans: (PlanDocument & { createdAt?: string })[] = [];
|
|
2249
|
+
const unrecognized: string[] = [];
|
|
2250
|
+
for (const file of files) {
|
|
2251
|
+
let parsed: unknown;
|
|
2252
|
+
try {
|
|
2253
|
+
parsed = JSON.parse(await readFile(file, 'utf8'));
|
|
2254
|
+
} catch {
|
|
2255
|
+
unrecognized.push(file);
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
const report = storedReportFrom(file, parsed);
|
|
2259
|
+
if (report !== null) {
|
|
2260
|
+
reports.push(report);
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
const maybePlan = parsed as PlanDocument & { createdAt?: string };
|
|
2264
|
+
if (maybePlan?.schemaVersion === 1 && Array.isArray(maybePlan.actions)) {
|
|
2265
|
+
plans.push(maybePlan);
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
unrecognized.push(file);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
const history = buildHistory(reports, plans);
|
|
2272
|
+
if (history.periods.length < 3) {
|
|
2273
|
+
throw new Error(t.history.needsThree(String(history.periods.length)));
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
const stamped = { ...history, unrecognizedFiles: unrecognized };
|
|
2277
|
+
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2278
|
+
const day = (ms: number): string => new Date(ms).toISOString().slice(0, 10);
|
|
2279
|
+
const pct = (value: number): string => `${(value * 100).toFixed(1)}%`;
|
|
2280
|
+
|
|
2281
|
+
const runLine = (run: HistoryRun): string => {
|
|
2282
|
+
if (run.kind === 'label-spend-climbing') {
|
|
2283
|
+
const name = run.subject === UNLABELLED ? t.profile.unlabelled() : run.subject;
|
|
2284
|
+
return t.history.runLabel(name, n(run.periods), run.sinceName, formatUsd(run.from), formatUsd(run.to));
|
|
2285
|
+
}
|
|
2286
|
+
if (run.kind === 'model-share-climbing') {
|
|
2287
|
+
return t.history.runModel(run.subject, n(run.periods), run.sinceName, pct(run.from), pct(run.to));
|
|
2288
|
+
}
|
|
2289
|
+
return t.history.runCache(n(run.periods), run.sinceName, pct(run.from), pct(run.to));
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2292
|
+
const lines = (md: boolean): string[] => {
|
|
2293
|
+
const out: string[] = [];
|
|
2294
|
+
const first = history.periods[0]!;
|
|
2295
|
+
const last = history.periods[history.periods.length - 1]!;
|
|
2296
|
+
const heading = t.history.heading(n(history.periods.length), day(first.fromMs), day(last.toMs));
|
|
2297
|
+
out.push(md ? `## ${heading}` : heading);
|
|
2298
|
+
for (const period of history.periods) {
|
|
2299
|
+
const row = t.history.periodRow(
|
|
2300
|
+
period.name,
|
|
2301
|
+
formatUsd(period.totalUsd),
|
|
2302
|
+
n(period.calls),
|
|
2303
|
+
((period.toMs - period.fromMs) / 86_400_000).toFixed(1),
|
|
2304
|
+
);
|
|
2305
|
+
out.push(md ? `- ${row}` : ` ${row}`);
|
|
2306
|
+
}
|
|
2307
|
+
if (history.runs.length > 0) out.push('');
|
|
2308
|
+
for (const run of history.runs) {
|
|
2309
|
+
out.push(md ? `- ${runLine(run)}` : ` ! ${runLine(run)}`);
|
|
2310
|
+
}
|
|
2311
|
+
if (history.repeatedPlanActions.length > 0) out.push('');
|
|
2312
|
+
for (const repeat of history.repeatedPlanActions) {
|
|
2313
|
+
const name = repeat.label === UNLABELLED ? t.profile.unlabelled() : repeat.label;
|
|
2314
|
+
const row = t.history.repeated(
|
|
2315
|
+
repeat.kind,
|
|
2316
|
+
name,
|
|
2317
|
+
repeat.model,
|
|
2318
|
+
n(repeat.appearances),
|
|
2319
|
+
repeat.firstPlanned?.slice(0, 10) ?? null,
|
|
2320
|
+
repeat.lastPlanned?.slice(0, 10) ?? null,
|
|
2321
|
+
);
|
|
2322
|
+
out.push(md ? `- ${row}` : ` ! ${row}`);
|
|
2323
|
+
}
|
|
2324
|
+
for (const name of history.undatedReports) {
|
|
2325
|
+
out.push(md ? `- ${t.history.undated(name)}` : ` ${t.history.undated(name)}`);
|
|
2326
|
+
}
|
|
2327
|
+
for (const name of unrecognized) {
|
|
2328
|
+
out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
|
|
2329
|
+
}
|
|
2330
|
+
out.push('');
|
|
2331
|
+
out.push(md ? `_${t.history.footer()}_` : ` ${t.history.footer()}`);
|
|
2332
|
+
return out;
|
|
2333
|
+
};
|
|
2334
|
+
|
|
2335
|
+
await writeMarkdown(args, () => lines(true).join('\n'));
|
|
2336
|
+
|
|
2337
|
+
if (boolFlag(args, 'json')) {
|
|
2338
|
+
console.log(JSON.stringify(stamped, null, 2));
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
const [head, ...rest] = lines(false);
|
|
2342
|
+
console.log(c.bold(head!));
|
|
2343
|
+
for (const row of rest) console.log(row === '' ? '' : wrap(row, 76, ' '));
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
/**
|
|
2347
|
+
* `trazum verify <plan.json> --against <newer.jsonl|dir>` — did it work?
|
|
2348
|
+
*
|
|
2349
|
+
* The plan predicted; this holds the prediction to the log that came after
|
|
2350
|
+
* it. Three outcomes and never two — arrived, did not arrive, cannot be told
|
|
2351
|
+
* — because "cannot be told" rendered as "arrived" is how every other tool
|
|
2352
|
+
* congratulates a team for a workload that merely vanished. With `--gate`,
|
|
2353
|
+
* a broken promise is a failing exit code: a different and more useful gate
|
|
2354
|
+
* than "spend went up".
|
|
2355
|
+
*/
|
|
2356
|
+
async function commandVerify(
|
|
2357
|
+
args: Args,
|
|
2358
|
+
pricing: PricingCatalogue,
|
|
2359
|
+
t: CliMessages,
|
|
2360
|
+
): Promise<void> {
|
|
2361
|
+
const planPath = args.positional[0];
|
|
2362
|
+
if (planPath === undefined) throw new Error(t.verify.noTarget());
|
|
2363
|
+
const againstPath = stringFlag(args, 'against');
|
|
2364
|
+
if (againstPath === undefined) throw new Error(t.verify.needsAgainst());
|
|
2365
|
+
|
|
2366
|
+
let plan: PlanDocument & { createdAt?: string };
|
|
2367
|
+
try {
|
|
2368
|
+
const parsed = JSON.parse(await readFile(planPath, 'utf8'));
|
|
2369
|
+
if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.actions)) {
|
|
2370
|
+
throw new Error(t.verify.badPlan(planPath));
|
|
2371
|
+
}
|
|
2372
|
+
plan = parsed;
|
|
2373
|
+
} catch (error) {
|
|
2374
|
+
if (error instanceof SyntaxError) throw new Error(t.verify.badPlan(planPath));
|
|
2375
|
+
throw error;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
|
|
2379
|
+
const READABLE = [...LOG_EXTENSIONS, ...GZ];
|
|
2380
|
+
const target = await stat(againstPath).catch(() => null);
|
|
2381
|
+
let files: string[] = [againstPath];
|
|
2382
|
+
if (target?.isDirectory()) {
|
|
2383
|
+
const entries = await readdir(againstPath, { withFileTypes: true });
|
|
2384
|
+
files = entries
|
|
2385
|
+
.filter((entry) => entry.isFile() && READABLE.some((ext) => entry.name.endsWith(ext)))
|
|
2386
|
+
.map((entry) => join(againstPath, entry.name))
|
|
2387
|
+
.sort((a, b) => a.localeCompare(b));
|
|
2388
|
+
if (files.length === 0) throw new Error(t.profile.noLogsInDirectory(againstPath, READABLE.join(', ')));
|
|
2389
|
+
}
|
|
2390
|
+
const texts = await Promise.all(files.map((file) => readUsageLog(file, t)));
|
|
2391
|
+
const raw = texts.map((text) => (text.endsWith('\n') ? text : `${text}\n`)).join('');
|
|
2392
|
+
const report = profileUsage(raw, { catalogue: pricing });
|
|
2393
|
+
|
|
2394
|
+
const verification = verifyPlan(plan, report, { currentPricingLastReviewed: pricing.lastReviewed });
|
|
2395
|
+
const gate = boolFlag(args, 'gate');
|
|
2396
|
+
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2397
|
+
|
|
2398
|
+
const lines = (md: boolean): string[] => {
|
|
2399
|
+
const out: string[] = [];
|
|
2400
|
+
const actionLine = (v: VerifiedAction): string[] => {
|
|
2401
|
+
const name = v.action.label === UNLABELLED ? t.profile.unlabelled() : v.action.label;
|
|
2402
|
+
const rows: string[] = [];
|
|
2403
|
+
rows.push(t.verify.action(v.action.kind, name, v.action.model, v.outcome));
|
|
2404
|
+
if (v.outcome === 'cannot-tell' && v.reason !== null) rows.push(t.verify.reason(v.reason));
|
|
2405
|
+
if (v.action.kind === 'route' || v.action.kind === 'route+batch') {
|
|
2406
|
+
if (v.outcome !== 'cannot-tell') {
|
|
2407
|
+
rows.push(
|
|
2408
|
+
t.verify.routeObserved(
|
|
2409
|
+
String(v.observed.dearestModel ?? ''),
|
|
2410
|
+
formatUsd(Number(v.observed.onTargetUsd ?? 0)),
|
|
2411
|
+
formatUsd(Number(v.observed.onOldModelUsd ?? 0)),
|
|
2412
|
+
),
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
if (v.action.kind === 'route+batch' && v.outcome !== 'cannot-tell') rows.push(t.verify.batchUnobservable());
|
|
2416
|
+
}
|
|
2417
|
+
if (v.action.kind === 'fix-truncation' && v.outcome === 'not-arrived') {
|
|
2418
|
+
rows.push(t.verify.truncationObserved(formatUsd(Number(v.observed.retryBillUsd ?? 0))));
|
|
2419
|
+
}
|
|
2420
|
+
if (v.action.kind === 'fix-caching' && v.outcome !== 'cannot-tell') {
|
|
2421
|
+
rows.push(t.verify.cacheObserved(formatUsd(Number(v.observed.deltaUsd ?? 0)), v.outcome));
|
|
2422
|
+
}
|
|
2423
|
+
if (v.attribution?.calls !== undefined) {
|
|
2424
|
+
rows.push(
|
|
2425
|
+
t.verify.attribution(
|
|
2426
|
+
n(Math.round(v.attribution.calls.before)),
|
|
2427
|
+
n(Math.round(v.attribution.calls.after)),
|
|
2428
|
+
n(Math.round(v.attribution.outputPerCallTokens?.before ?? 0)),
|
|
2429
|
+
n(Math.round(v.attribution.outputPerCallTokens?.after ?? 0)),
|
|
2430
|
+
),
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
return rows;
|
|
2434
|
+
};
|
|
2435
|
+
|
|
2436
|
+
const heading = t.verify.heading(
|
|
2437
|
+
n(verification.actions.length),
|
|
2438
|
+
verification.planCreatedAt === null ? null : verification.planCreatedAt.slice(0, 10),
|
|
2439
|
+
);
|
|
2440
|
+
out.push(md ? `## ${heading}` : heading);
|
|
2441
|
+
out.push(
|
|
2442
|
+
t.verify.counts(n(verification.arrived), n(verification.notArrived), n(verification.cannotTell)),
|
|
2443
|
+
);
|
|
2444
|
+
if (verification.pricesChanged) {
|
|
2445
|
+
out.push(t.verify.pricesChanged(verification.planPricing, verification.currentPricing));
|
|
2446
|
+
}
|
|
2447
|
+
for (const v of verification.actions) {
|
|
2448
|
+
out.push('');
|
|
2449
|
+
const [head, ...rest] = actionLine(v);
|
|
2450
|
+
out.push(md ? `### ${head}` : `→ ${head}`);
|
|
2451
|
+
for (const row of rest) out.push(md ? `- ${row}` : ` · ${row}`);
|
|
2452
|
+
}
|
|
2453
|
+
out.push('');
|
|
2454
|
+
out.push(t.verify.footer());
|
|
2455
|
+
return out;
|
|
2456
|
+
};
|
|
2457
|
+
|
|
2458
|
+
await writeMarkdown(args, () => lines(true).join('\n'));
|
|
2459
|
+
|
|
2460
|
+
if (boolFlag(args, 'json')) {
|
|
2461
|
+
console.log(JSON.stringify(verification, null, 2));
|
|
2462
|
+
} else {
|
|
2463
|
+
const [head, ...rest] = lines(false);
|
|
2464
|
+
console.log(c.bold(head!));
|
|
2465
|
+
for (const row of rest) {
|
|
2466
|
+
console.log(row === '' ? '' : ` ${wrap(row, 74, ' ')}`);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
if (gate) {
|
|
2471
|
+
if (verification.gateFailures > 0) {
|
|
2472
|
+
console.error(c.red(t.verify.gateFailed(n(verification.gateFailures), n(verification.actions.length))));
|
|
2473
|
+
process.exitCode = 1;
|
|
2474
|
+
} else {
|
|
2475
|
+
console.log(c.green(t.verify.gateOk()));
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2217
2480
|
/**
|
|
2218
2481
|
* `trazum plan <log>` — not a list of findings, a ranked plan of what to do.
|
|
2219
2482
|
*
|
|
@@ -6161,6 +6424,12 @@ async function main(): Promise<void> {
|
|
|
6161
6424
|
case 'plan':
|
|
6162
6425
|
await commandPlan(args, pricing, t);
|
|
6163
6426
|
break;
|
|
6427
|
+
case 'verify':
|
|
6428
|
+
await commandVerify(args, pricing, t);
|
|
6429
|
+
break;
|
|
6430
|
+
case 'history':
|
|
6431
|
+
await commandHistory(args, t);
|
|
6432
|
+
break;
|
|
6164
6433
|
case 'route':
|
|
6165
6434
|
await commandRoute(args, pricing, t);
|
|
6166
6435
|
break;
|