@trazum/cli 1.39.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 +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +40 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +41 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +21 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +110 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +48 -0
- package/src/i18n/es.ts +49 -0
- package/src/i18n/types.ts +22 -0
- package/src/index.ts +128 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.0",
|
|
4
4
|
"description": "Trazum CLI: find where your LLM bill goes, price every finding per month, and enforce token budgets in CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Mu\u00f1oz Rey",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "npm run build && npm test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@trazum/core": "1.
|
|
40
|
+
"@trazum/core": "1.40.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -41,6 +41,7 @@ ${bold('USAGE')}
|
|
|
41
41
|
trazum route <log.jsonl> --prompt-file <file> --cases <file> --yes
|
|
42
42
|
trazum plan <log.jsonl|dir> [options]
|
|
43
43
|
trazum verify <plan.json> --against <newer.jsonl|dir> [options]
|
|
44
|
+
trazum history <dir-of-stored-reports> [options]
|
|
44
45
|
trazum diff <before> <after> [options]
|
|
45
46
|
trazum diff --all <dir> <dir> [options]
|
|
46
47
|
trazum rank <dir> [options]
|
|
@@ -318,6 +319,20 @@ ${bold('OPTIONS FOR plan')}
|
|
|
318
319
|
a plan that hides its assumptions is advice pretending to be arithmetic.
|
|
319
320
|
Projected savings and money already spent are separate totals throughout.
|
|
320
321
|
|
|
322
|
+
${bold('OPTIONS FOR history')}
|
|
323
|
+
--markdown-out <file> Also write the series as Markdown, for a CI job
|
|
324
|
+
summary or a pull request comment.
|
|
325
|
+
--json The history as data.
|
|
326
|
+
|
|
327
|
+
Takes a directory of stored reports — the --json documents profile already
|
|
328
|
+
writes — plus any saved plans beside them, and builds the series no
|
|
329
|
+
pairwise comparison can see: a workload climbing a little every period, a
|
|
330
|
+
model share rising since a date, a cache share decaying slowly enough that
|
|
331
|
+
no single week's report called it a finding, and the same action planned
|
|
332
|
+
again and again with nothing executing it. Derived from stored reports,
|
|
333
|
+
never re-parsed logs, so a year of JSON is enough and the raw logs can be
|
|
334
|
+
thrown away. Shapes are named; nothing is forecast.
|
|
335
|
+
|
|
321
336
|
${bold('OPTIONS FOR verify')}
|
|
322
337
|
--against <log|dir> The newer usage log the plan is held to. Required.
|
|
323
338
|
--gate Exit 1 when an action did not produce what the
|
|
@@ -1432,6 +1447,39 @@ ${bold('EXAMPLES')}
|
|
|
1432
1447
|
`Plan written to ${path}, dated. Keep it: a prediction nobody wrote down is a prediction nobody can be held to.`,
|
|
1433
1448
|
},
|
|
1434
1449
|
|
|
1450
|
+
history: {
|
|
1451
|
+
noTarget: () =>
|
|
1452
|
+
'Point this at a directory of stored reports: trazum history reports/. It reads the --json documents "trazum profile" writes (and any saved plans beside them) and builds the series no pairwise comparison can see.',
|
|
1453
|
+
needsThree: (count) =>
|
|
1454
|
+
`A series needs at least three dated reports, and this directory has ${count}. Two reports is a comparison, and "trazum profile --against" already does that better.`,
|
|
1455
|
+
heading: (periods, from, to) => `The long run: ${periods} periods, ${from} → ${to}`,
|
|
1456
|
+
periodRow: (name, usd, calls, days) => `${name} ${usd} · ${calls} calls · ${days} days`,
|
|
1457
|
+
runLabel: (label, periods, sinceName, from, to) =>
|
|
1458
|
+
`${label} has climbed for ${periods} consecutive periods since ${sinceName}: ${from} → ${to}. A shape, not a forecast.`,
|
|
1459
|
+
runModel: (model, periods, sinceName, from, to) =>
|
|
1460
|
+
`${model}'s share of the bill has climbed for ${periods} consecutive periods since ${sinceName}: ${from} → ${to}. The totals can look flat while the mix moves under them.`,
|
|
1461
|
+
runCache: (periods, sinceName, from, to) =>
|
|
1462
|
+
`The cache share has decayed for ${periods} consecutive periods since ${sinceName}: ${from} → ${to} — slowly enough that no single report called it a finding, which is exactly why a series exists.`,
|
|
1463
|
+
repeated: (kind, label, model, appearances, first, last) => {
|
|
1464
|
+
const what =
|
|
1465
|
+
kind === 'route'
|
|
1466
|
+
? `Routing ${label} (${model})`
|
|
1467
|
+
: kind === 'batch'
|
|
1468
|
+
? `Batching ${label} (${model})`
|
|
1469
|
+
: kind === 'route+batch'
|
|
1470
|
+
? `Routing and batching ${label} (${model})`
|
|
1471
|
+
: kind === 'fix-truncation'
|
|
1472
|
+
? `Fixing the truncation retries on ${label} (${model})`
|
|
1473
|
+
: `Fixing the cache on ${label} (${model})`;
|
|
1474
|
+
const span = first !== null && last !== null ? ` (${first} → ${last})` : '';
|
|
1475
|
+
return `${what} has been planned ${appearances} times${span} and is still in the newest plan — a decision nobody is revisiting.`;
|
|
1476
|
+
},
|
|
1477
|
+
undated: (name) => `${name} carries no span, so it is on no timeline above — named, never silently absorbed.`,
|
|
1478
|
+
unrecognized: (name) => `${name} is neither a stored report nor a saved plan, so it is in no series above.`,
|
|
1479
|
+
footer: () =>
|
|
1480
|
+
'A series names shapes, not futures. Twenty points make a trend visible; they do not make next month knowable — where these lines go next is yours to judge.',
|
|
1481
|
+
},
|
|
1482
|
+
|
|
1435
1483
|
verify: {
|
|
1436
1484
|
noTarget: () =>
|
|
1437
1485
|
'Point this at a saved plan and a newer log: trazum verify plan.json --against usage.jsonl. It says, per action, whether the change arrived, did not arrive, or cannot be told — and never fewer than those three.',
|
package/src/i18n/es.ts
CHANGED
|
@@ -28,6 +28,7 @@ ${bold('USO')}
|
|
|
28
28
|
trazum route <log.jsonl> --prompt-file <fichero> --cases <fichero> --yes
|
|
29
29
|
trazum plan <log.jsonl|dir> [opciones]
|
|
30
30
|
trazum verify <plan.json> --against <nuevo.jsonl|dir> [opciones]
|
|
31
|
+
trazum history <dir-de-informes-guardados> [opciones]
|
|
31
32
|
trazum diff <antes> <después> [opciones]
|
|
32
33
|
trazum diff --all <dir> <dir> [opciones]
|
|
33
34
|
trazum rank <dir> [opciones]
|
|
@@ -325,6 +326,21 @@ ${bold('OPCIONES DE plan')}
|
|
|
325
326
|
consejo haciéndose pasar por aritmética. El ahorro proyectado y el dinero ya
|
|
326
327
|
gastado son totales separados en todas partes.
|
|
327
328
|
|
|
329
|
+
${bold('OPCIONES DE history')}
|
|
330
|
+
--markdown-out <fichero> Escribe además la serie como Markdown, para un
|
|
331
|
+
resumen de CI o un comentario de pull request.
|
|
332
|
+
--json La historia como datos.
|
|
333
|
+
|
|
334
|
+
Toma un directorio de informes guardados — los documentos --json que
|
|
335
|
+
profile ya escribe — más los planes guardados que haya al lado, y construye
|
|
336
|
+
la serie que ninguna comparación por pares puede ver: una carga que sube un
|
|
337
|
+
poco cada período, una cuota de modelo creciendo desde una fecha, una cuota
|
|
338
|
+
de caché decayendo tan despacio que ningún informe semanal lo llamó
|
|
339
|
+
hallazgo, y la misma acción planificada una y otra vez sin que nadie la
|
|
340
|
+
ejecute. Derivada de informes guardados, nunca de registros re-parseados:
|
|
341
|
+
un año de JSON basta y los registros crudos pueden tirarse. Las formas se
|
|
342
|
+
nombran; nada se pronostica.
|
|
343
|
+
|
|
328
344
|
${bold('OPCIONES DE verify')}
|
|
329
345
|
--against <log|dir> El registro de uso posterior al que se somete el
|
|
330
346
|
plan. Obligatorio.
|
|
@@ -1454,6 +1470,39 @@ ${bold('EJEMPLOS')}
|
|
|
1454
1470
|
`Plan escrito en ${path}, con fecha. Guárdalo: una predicción que nadie apuntó es una predicción que no se le puede exigir a nadie.`,
|
|
1455
1471
|
},
|
|
1456
1472
|
|
|
1473
|
+
history: {
|
|
1474
|
+
noTarget: () =>
|
|
1475
|
+
'Apunta esto a un directorio de informes guardados: trazum history informes/. Lee los documentos --json que escribe "trazum profile" (y los planes guardados que haya al lado) y construye la serie que ninguna comparación por pares puede ver.',
|
|
1476
|
+
needsThree: (count) =>
|
|
1477
|
+
`Una serie necesita al menos tres informes con fecha, y este directorio tiene ${count}. Dos informes son una comparación, y "trazum profile --against" ya la hace mejor.`,
|
|
1478
|
+
heading: (periods, from, to) => `La larga distancia: ${periods} períodos, ${from} → ${to}`,
|
|
1479
|
+
periodRow: (name, usd, calls, days) => `${name} ${usd} · ${calls} llamadas · ${days} días`,
|
|
1480
|
+
runLabel: (label, periods, sinceName, from, to) =>
|
|
1481
|
+
`${label} lleva ${periods} períodos consecutivos subiendo desde ${sinceName}: ${from} → ${to}. Una forma, no un pronóstico.`,
|
|
1482
|
+
runModel: (model, periods, sinceName, from, to) =>
|
|
1483
|
+
`La cuota de ${model} en la factura lleva ${periods} períodos consecutivos subiendo desde ${sinceName}: ${from} → ${to}. Los totales pueden parecer planos mientras la mezcla se mueve debajo.`,
|
|
1484
|
+
runCache: (periods, sinceName, from, to) =>
|
|
1485
|
+
`La cuota de caché lleva ${periods} períodos consecutivos decayendo desde ${sinceName}: ${from} → ${to} — tan despacio que ningún informe suelto lo llamó hallazgo, que es exactamente para lo que existe una serie.`,
|
|
1486
|
+
repeated: (kind, label, model, appearances, first, last) => {
|
|
1487
|
+
const what =
|
|
1488
|
+
kind === 'route'
|
|
1489
|
+
? `Enrutar ${label} (${model})`
|
|
1490
|
+
: kind === 'batch'
|
|
1491
|
+
? `Agrupar en batch ${label} (${model})`
|
|
1492
|
+
: kind === 'route+batch'
|
|
1493
|
+
? `Enrutar y agrupar ${label} (${model})`
|
|
1494
|
+
: kind === 'fix-truncation'
|
|
1495
|
+
? `Arreglar los reintentos por truncado de ${label} (${model})`
|
|
1496
|
+
: `Arreglar la caché de ${label} (${model})`;
|
|
1497
|
+
const span = first !== null && last !== null ? ` (${first} → ${last})` : '';
|
|
1498
|
+
return `${what} se ha planificado ${appearances} veces${span} y sigue en el plan más reciente — una decisión que nadie está revisando.`;
|
|
1499
|
+
},
|
|
1500
|
+
undated: (name) => `${name} no lleva período, así que no está en ninguna línea de tiempo de arriba — nombrado, nunca absorbido en silencio.`,
|
|
1501
|
+
unrecognized: (name) => `${name} no es ni un informe guardado ni un plan guardado, así que no está en ninguna serie de arriba.`,
|
|
1502
|
+
footer: () =>
|
|
1503
|
+
'Una serie nombra formas, no futuros. Veinte puntos hacen visible una tendencia; no hacen conocible el mes que viene — adónde van estas líneas después lo juzgas tú.',
|
|
1504
|
+
},
|
|
1505
|
+
|
|
1457
1506
|
verify: {
|
|
1458
1507
|
noTarget: () =>
|
|
1459
1508
|
'Apunta esto a un plan guardado y a un registro posterior: trazum verify plan.json --against usage.jsonl. Dice, por acción, si el cambio llegó, no llegó o no se puede saber — y nunca menos de esos tres.',
|
package/src/i18n/types.ts
CHANGED
|
@@ -1072,6 +1072,28 @@ export interface CliMessages {
|
|
|
1072
1072
|
wrote(path: string): string;
|
|
1073
1073
|
};
|
|
1074
1074
|
|
|
1075
|
+
/**
|
|
1076
|
+
* `trazum history` — many reports over many periods, as one series.
|
|
1077
|
+
*
|
|
1078
|
+
* Shapes are named as consecutive movement, never a fitted line, and no
|
|
1079
|
+
* series becomes a forecast: where the line goes next stays the reader's.
|
|
1080
|
+
*/
|
|
1081
|
+
history: {
|
|
1082
|
+
noTarget(): string;
|
|
1083
|
+
/** Under three dated reports there is no series — only the comparison --against already does. */
|
|
1084
|
+
needsThree(count: string): string;
|
|
1085
|
+
heading(periods: string, from: string, to: string): string;
|
|
1086
|
+
periodRow(name: string, usd: string, calls: string, days: string): string;
|
|
1087
|
+
runLabel(label: string, periods: string, sinceName: string, from: string, to: string): string;
|
|
1088
|
+
runModel(model: string, periods: string, sinceName: string, from: string, to: string): string;
|
|
1089
|
+
runCache(periods: string, sinceName: string, from: string, to: string): string;
|
|
1090
|
+
/** The same action in plan after plan: a decision nobody is executing. */
|
|
1091
|
+
repeated(kind: PlanActionKind, label: string, model: string, appearances: string, first: string | null, last: string | null): string;
|
|
1092
|
+
undated(name: string): string;
|
|
1093
|
+
unrecognized(name: string): string;
|
|
1094
|
+
footer(): string;
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1075
1097
|
/**
|
|
1076
1098
|
* `trazum verify` — the plan held to the log that came after it.
|
|
1077
1099
|
*
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
cacheableMinimum,
|
|
12
12
|
analyzeCachePrefix,
|
|
13
13
|
billLevers,
|
|
14
|
+
buildHistory,
|
|
14
15
|
buildPlan,
|
|
16
|
+
storedReportFrom,
|
|
15
17
|
verifyPlan,
|
|
16
18
|
cacheEconomics,
|
|
17
19
|
cacheHitRate,
|
|
@@ -79,8 +81,10 @@ import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cac
|
|
|
79
81
|
import { dayOf, formatGap, median, spanDays } from './time.js';
|
|
80
82
|
import type {
|
|
81
83
|
FleetSource,
|
|
84
|
+
HistoryRun,
|
|
82
85
|
MeasuredUsage,
|
|
83
86
|
PlanDocument,
|
|
87
|
+
StoredReport,
|
|
84
88
|
VerifiedAction,
|
|
85
89
|
BaselineBreach,
|
|
86
90
|
BaselineChange,
|
|
@@ -479,6 +483,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
479
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'],
|
|
480
484
|
plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
|
|
481
485
|
verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
|
|
486
|
+
history: ['json', 'markdown-out'],
|
|
482
487
|
route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
|
|
483
488
|
eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
|
|
484
489
|
prune: ['cases', 'concurrency', 'json', 'yes'],
|
|
@@ -2218,6 +2223,126 @@ function isoDate(): string {
|
|
|
2218
2223
|
* metered API calls somebody was actually billed for — the bill exists wherever
|
|
2219
2224
|
* Trazum happens to be running, so the host has no bearing on it.
|
|
2220
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
|
+
|
|
2221
2346
|
/**
|
|
2222
2347
|
* `trazum verify <plan.json> --against <newer.jsonl|dir>` — did it work?
|
|
2223
2348
|
*
|
|
@@ -6302,6 +6427,9 @@ async function main(): Promise<void> {
|
|
|
6302
6427
|
case 'verify':
|
|
6303
6428
|
await commandVerify(args, pricing, t);
|
|
6304
6429
|
break;
|
|
6430
|
+
case 'history':
|
|
6431
|
+
await commandHistory(args, t);
|
|
6432
|
+
break;
|
|
6305
6433
|
case 'route':
|
|
6306
6434
|
await commandRoute(args, pricing, t);
|
|
6307
6435
|
break;
|