@trazum/cli 1.49.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 +28 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +28 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +19 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +90 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +32 -0
- package/src/i18n/es.ts +32 -0
- package/src/i18n/types.ts +20 -0
- package/src/index.ts +102 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.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.50.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -54,6 +54,7 @@ ${bold('USAGE')}
|
|
|
54
54
|
trazum blame <file> [options]
|
|
55
55
|
trazum prune <file> --cases <file> --yes
|
|
56
56
|
trazum where [file]
|
|
57
|
+
trazum conform <file|-> [--contract <name>]
|
|
57
58
|
trazum models
|
|
58
59
|
trazum rules
|
|
59
60
|
|
|
@@ -73,6 +74,20 @@ ${bold('OPTIONS FOR init')}
|
|
|
73
74
|
measured figure and leaves the limit to you — a generated config full of
|
|
74
75
|
guessed numbers is one nobody trusts.
|
|
75
76
|
|
|
77
|
+
${bold('OPTIONS FOR conform')}
|
|
78
|
+
--contract <name> Check against a named contract instead of
|
|
79
|
+
detecting one: usage-log, profile, plan,
|
|
80
|
+
verification, history, connected, cost-answer.
|
|
81
|
+
--json The report as data.
|
|
82
|
+
|
|
83
|
+
Answers two questions and keeps them apart. Does this document conform —
|
|
84
|
+
required fields, present and the right type, exits 1 when not. And what can a
|
|
85
|
+
valid document of this shape not answer, with the field that would unlock
|
|
86
|
+
each.
|
|
87
|
+
|
|
88
|
+
The second never gates. Choosing not to log sessions is a decision, not a
|
|
89
|
+
defect. See docs/format.md.
|
|
90
|
+
|
|
76
91
|
${bold('OPTIONS FOR prune')}
|
|
77
92
|
--cases <file> One input per line, or a JSON array. Required.
|
|
78
93
|
--yes Actually spend the calls. Without it the estimate is
|
|
@@ -904,6 +919,23 @@ ${bold('EXAMPLES')}
|
|
|
904
919
|
`${path} exists and could not be parsed, so nothing was written over it. Fix or move it first.`,
|
|
905
920
|
},
|
|
906
921
|
|
|
922
|
+
conform: {
|
|
923
|
+
noTarget: () =>
|
|
924
|
+
'Pass a file to check — a usage log, or any document Trazum emits. Use "-" to read from stdin.',
|
|
925
|
+
badContract: (given, known) => `"${given}" is not a contract. Known contracts: ${known}.`,
|
|
926
|
+
unrecognised: (path) => `${path} does not match any contract Trazum knows.`,
|
|
927
|
+
heading: (path, contract) => `${path} reads as a ${contract} document`,
|
|
928
|
+
headingLog: (path, contract, records) =>
|
|
929
|
+
`${path} reads as a ${contract}: ${records} ${records === 1 ? 'record' : 'records'}`,
|
|
930
|
+
conforms: () => 'It conforms. Every required field is present and the right type.',
|
|
931
|
+
problem: (at, kind, detail) => `${at}: ${detail} (${kind})`,
|
|
932
|
+
moreProblems: (count) => `…and ${count} more. Fix these first; they are often the same mistake.`,
|
|
933
|
+
unavailableHeading: () => 'What this cannot answer, and what would unlock it',
|
|
934
|
+
unavailable: (finding, because, unlockedBy) => `${finding} — ${because}. Add ${unlockedBy}.`,
|
|
935
|
+
unavailableNeverGates: () =>
|
|
936
|
+
'None of those failed anything. Choosing not to record a field is a decision, not a defect, and a gate that failed on it would be this tool telling you what to log.',
|
|
937
|
+
},
|
|
938
|
+
|
|
907
939
|
where: {
|
|
908
940
|
hostHeading: () => 'Running inside',
|
|
909
941
|
subscription: (host) =>
|
package/src/i18n/es.ts
CHANGED
|
@@ -41,6 +41,7 @@ ${bold('USO')}
|
|
|
41
41
|
trazum blame <fichero> [opciones]
|
|
42
42
|
trazum prune <fichero> --cases <fichero> --yes
|
|
43
43
|
trazum where [fichero]
|
|
44
|
+
trazum conform <fichero|-> [--contract <nombre>]
|
|
44
45
|
trazum models
|
|
45
46
|
trazum rules
|
|
46
47
|
|
|
@@ -60,6 +61,20 @@ ${bold('OPCIONES DE init')}
|
|
|
60
61
|
cifra medida y te deja el límite a ti — una configuración generada llena de
|
|
61
62
|
números adivinados no se la cree nadie.
|
|
62
63
|
|
|
64
|
+
${bold('OPCIONES DE conform')}
|
|
65
|
+
--contract <nombre> Comprueba contra un contrato concreto en vez de
|
|
66
|
+
detectarlo: usage-log, profile, plan,
|
|
67
|
+
verification, history, connected, cost-answer.
|
|
68
|
+
--json El informe como datos.
|
|
69
|
+
|
|
70
|
+
Responde dos preguntas y las mantiene separadas. ¿Cumple este documento —
|
|
71
|
+
campos obligatorios, presentes y del tipo correcto; sale con 1 si no. Y qué no
|
|
72
|
+
puede responder un documento válido de esta forma, con el campo que lo
|
|
73
|
+
desbloquearía.
|
|
74
|
+
|
|
75
|
+
La segunda nunca hace fallar nada. Decidir no registrar sesiones es una
|
|
76
|
+
decisión, no un defecto. Ver docs/format.md.
|
|
77
|
+
|
|
63
78
|
${bold('OPCIONES DE prune')}
|
|
64
79
|
--cases <fichero> Una entrada por línea, o un array JSON. Obligatorio.
|
|
65
80
|
--yes Gasta las llamadas de verdad. Sin él se imprime la
|
|
@@ -937,6 +952,23 @@ ${bold('EJEMPLOS')}
|
|
|
937
952
|
`${path} existe y no se pudo interpretar, así que no se escribió nada encima. Arréglalo o muévelo primero.`,
|
|
938
953
|
},
|
|
939
954
|
|
|
955
|
+
conform: {
|
|
956
|
+
noTarget: () =>
|
|
957
|
+
'Pasa un archivo para comprobar — un registro de consumo, o cualquier documento que emita Trazum. Usa "-" para leer de la entrada estándar.',
|
|
958
|
+
badContract: (given, known) => `"${given}" no es un contrato. Contratos conocidos: ${known}.`,
|
|
959
|
+
unrecognised: (path) => `${path} no encaja con ningún contrato que Trazum conozca.`,
|
|
960
|
+
heading: (path, contract) => `${path} se lee como un documento ${contract}`,
|
|
961
|
+
headingLog: (path, contract, records) =>
|
|
962
|
+
`${path} se lee como ${contract}: ${records} ${records === 1 ? 'registro' : 'registros'}`,
|
|
963
|
+
conforms: () => 'Cumple. Todos los campos obligatorios están y son del tipo correcto.',
|
|
964
|
+
problem: (at, kind, detail) => `${at}: ${detail} (${kind})`,
|
|
965
|
+
moreProblems: (count) => `…y ${count} más. Arregla estos primero; suelen ser el mismo error.`,
|
|
966
|
+
unavailableHeading: () => 'Lo que esto no puede responder, y qué lo desbloquearía',
|
|
967
|
+
unavailable: (finding, because, unlockedBy) => `${finding} — ${because}. Añade ${unlockedBy}.`,
|
|
968
|
+
unavailableNeverGates: () =>
|
|
969
|
+
'Nada de eso ha hecho fallar nada. Decidir no registrar un campo es una decisión, no un defecto, y una puerta que fallara por ello sería esta herramienta diciéndote qué registrar.',
|
|
970
|
+
},
|
|
971
|
+
|
|
940
972
|
where: {
|
|
941
973
|
hostHeading: () => 'Ejecutándose dentro de',
|
|
942
974
|
subscription: (host) =>
|
package/src/i18n/types.ts
CHANGED
|
@@ -246,6 +246,26 @@ export interface CliMessages {
|
|
|
246
246
|
existingUnparseable(path: string): string;
|
|
247
247
|
};
|
|
248
248
|
|
|
249
|
+
/**
|
|
250
|
+
* The conformance check.
|
|
251
|
+
*
|
|
252
|
+
* Two halves, and the copy keeps them apart everywhere: problems gate, gaps
|
|
253
|
+
* do not. Choosing not to log sessions is a decision, not a defect.
|
|
254
|
+
*/
|
|
255
|
+
conform: {
|
|
256
|
+
noTarget(): string;
|
|
257
|
+
badContract(given: string, known: string): string;
|
|
258
|
+
unrecognised(path: string): string;
|
|
259
|
+
heading(path: string, contract: string): string;
|
|
260
|
+
headingLog(path: string, contract: string, records: number): string;
|
|
261
|
+
conforms(): string;
|
|
262
|
+
problem(at: string, kind: string, detail: string): string;
|
|
263
|
+
moreProblems(count: number): string;
|
|
264
|
+
unavailableHeading(): string;
|
|
265
|
+
unavailable(finding: string, because: string, unlockedBy: string): string;
|
|
266
|
+
unavailableNeverGates(): string;
|
|
267
|
+
};
|
|
268
|
+
|
|
249
269
|
where: {
|
|
250
270
|
hostHeading(): string;
|
|
251
271
|
subscription(host: string): string;
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
countTokensAnthropic,
|
|
37
37
|
DEFAULT_USAGE,
|
|
38
38
|
budgetPositions,
|
|
39
|
+
conform,
|
|
39
40
|
detectFromSource,
|
|
40
41
|
matchLocale,
|
|
41
42
|
parsePlanDocument,
|
|
@@ -129,6 +130,7 @@ import type {
|
|
|
129
130
|
} from '@trazum/core';
|
|
130
131
|
import type {
|
|
131
132
|
BudgetReport,
|
|
133
|
+
ContractName,
|
|
132
134
|
UsageProfileReport,
|
|
133
135
|
WaiverUse,
|
|
134
136
|
InitDecline,
|
|
@@ -228,6 +230,7 @@ interface Args {
|
|
|
228
230
|
|
|
229
231
|
const VALUE_FLAGS = new Set([
|
|
230
232
|
'against',
|
|
233
|
+
'contract',
|
|
231
234
|
'from-log',
|
|
232
235
|
'min-usd',
|
|
233
236
|
'payload',
|
|
@@ -550,6 +553,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
550
553
|
models: [],
|
|
551
554
|
rank: ['level', 'model', 'calls', 'output-tokens', 'batch', 'disable', 'prompt', 'markdown-out'],
|
|
552
555
|
init: ['dry-run', 'yes', 'json', 'pricing', 'pricing-live'],
|
|
556
|
+
conform: ['contract', 'json'],
|
|
553
557
|
where: [],
|
|
554
558
|
rules: [],
|
|
555
559
|
blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
|
|
@@ -1472,6 +1476,20 @@ const INIT_LOG_CANDIDATES = [
|
|
|
1472
1476
|
* nobody has vouched for.
|
|
1473
1477
|
*/
|
|
1474
1478
|
|
|
1479
|
+
/** Problems listed before the rest are counted. A wall of them helps nobody. */
|
|
1480
|
+
const MAX_CONFORM_PROBLEMS = 20;
|
|
1481
|
+
|
|
1482
|
+
/** The contracts `--contract` accepts, so a typo is refused with the list. */
|
|
1483
|
+
const CONTRACT_NAMES = [
|
|
1484
|
+
'usage-log',
|
|
1485
|
+
'profile',
|
|
1486
|
+
'plan',
|
|
1487
|
+
'verification',
|
|
1488
|
+
'history',
|
|
1489
|
+
'connected',
|
|
1490
|
+
'cost-answer',
|
|
1491
|
+
];
|
|
1492
|
+
|
|
1475
1493
|
/** How many source files, and how large each may be. Both reported when they bite. */
|
|
1476
1494
|
const INIT_MAX_SOURCE_FILES = 400;
|
|
1477
1495
|
const INIT_MAX_SOURCE_BYTES = 256 * 1024;
|
|
@@ -1850,6 +1868,87 @@ async function commandInit(
|
|
|
1850
1868
|
console.log();
|
|
1851
1869
|
}
|
|
1852
1870
|
|
|
1871
|
+
/**
|
|
1872
|
+
* `trazum conform <file>` — does this document conform, and what will it not
|
|
1873
|
+
* be able to answer?
|
|
1874
|
+
*
|
|
1875
|
+
* The command that makes the five contracts something to build against rather
|
|
1876
|
+
* than something to read about. An emitter — a logging wrapper somebody wrote
|
|
1877
|
+
* this afternoon, a connector for a provider this repository has never heard
|
|
1878
|
+
* of, a dashboard writing profile documents of its own — points this at what
|
|
1879
|
+
* it produced and finds out before shipping.
|
|
1880
|
+
*
|
|
1881
|
+
* **The second half is the useful half.** "Valid" is a yes or no. "Here is
|
|
1882
|
+
* what a valid document of this shape cannot tell you, and the field that
|
|
1883
|
+
* would unlock each" is the answer somebody acts on: a usage log with no
|
|
1884
|
+
* `session` is perfectly conformant and simply has no conversation growth in
|
|
1885
|
+
* it, and an emitter that only ever hears "valid" ships it and never finds out
|
|
1886
|
+
* why half the report is empty.
|
|
1887
|
+
*
|
|
1888
|
+
* Exits 1 on a problem, so it gates. It never exits 1 on an *unavailable
|
|
1889
|
+
* finding*: choosing not to log sessions is a decision, not a defect, and a
|
|
1890
|
+
* gate that failed on it would be this tool telling somebody what to record.
|
|
1891
|
+
*/
|
|
1892
|
+
async function commandConform(args: Args, t: CliMessages): Promise<void> {
|
|
1893
|
+
const target = args.positional[0];
|
|
1894
|
+
if (target === undefined) throw new Error(t.conform.noTarget());
|
|
1895
|
+
|
|
1896
|
+
const named = stringFlag(args, 'contract');
|
|
1897
|
+
if (named !== undefined && !CONTRACT_NAMES.includes(named)) {
|
|
1898
|
+
throw new Error(t.conform.badContract(named, CONTRACT_NAMES.join(', ')));
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const text = target === '-' ? await readInput('-', t) : await readUsageLog(target, t);
|
|
1902
|
+
const report = conform(text, named === undefined ? {} : { contract: named as ContractName });
|
|
1903
|
+
|
|
1904
|
+
if (boolFlag(args, 'json')) {
|
|
1905
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1906
|
+
if (!report.conforms) process.exitCode = 1;
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
console.log();
|
|
1911
|
+
if (report.contract === null) {
|
|
1912
|
+
console.log(c.red(t.conform.unrecognised(target)));
|
|
1913
|
+
console.log(` ${c.dim(wrap(report.because ?? '', 74, ' '))}`);
|
|
1914
|
+
console.log();
|
|
1915
|
+
process.exitCode = 1;
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
console.log(
|
|
1920
|
+
c.bold(
|
|
1921
|
+
report.records === null
|
|
1922
|
+
? t.conform.heading(target, report.contract)
|
|
1923
|
+
: t.conform.headingLog(target, report.contract, report.records),
|
|
1924
|
+
),
|
|
1925
|
+
);
|
|
1926
|
+
|
|
1927
|
+
if (report.problems.length === 0) {
|
|
1928
|
+
console.log(` ${c.green(t.conform.conforms())}`);
|
|
1929
|
+
} else {
|
|
1930
|
+
for (const problem of report.problems.slice(0, MAX_CONFORM_PROBLEMS)) {
|
|
1931
|
+
console.log(` ${c.red(t.conform.problem(problem.at, problem.kind, problem.detail))}`);
|
|
1932
|
+
}
|
|
1933
|
+
if (report.problems.length > MAX_CONFORM_PROBLEMS) {
|
|
1934
|
+
console.log(` ${c.dim(t.conform.moreProblems(report.problems.length - MAX_CONFORM_PROBLEMS))}`);
|
|
1935
|
+
}
|
|
1936
|
+
process.exitCode = 1;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
if (report.unavailable.length > 0) {
|
|
1940
|
+
console.log();
|
|
1941
|
+
console.log(c.bold(t.conform.unavailableHeading()));
|
|
1942
|
+
for (const gap of report.unavailable) {
|
|
1943
|
+
console.log(` ${c.dim(wrap(t.conform.unavailable(gap.finding, gap.because, gap.unlockedBy), 74, ' '))}`);
|
|
1944
|
+
}
|
|
1945
|
+
// Said out loud, because the exit code says it silently and somebody
|
|
1946
|
+
// reading a red-and-yellow screen will assume both halves gated.
|
|
1947
|
+
console.log(` ${c.dim(wrap(t.conform.unavailableNeverGates(), 74, ' '))}`);
|
|
1948
|
+
}
|
|
1949
|
+
console.log();
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1853
1952
|
function commandModels(t: CliMessages, pricing: PricingCatalogue): void {
|
|
1854
1953
|
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
1855
1954
|
const col = t.models.columns;
|
|
@@ -7843,6 +7942,9 @@ async function main(): Promise<void> {
|
|
|
7843
7942
|
case 'models':
|
|
7844
7943
|
commandModels(t, pricing);
|
|
7845
7944
|
break;
|
|
7945
|
+
case 'conform':
|
|
7946
|
+
await commandConform(args, t);
|
|
7947
|
+
break;
|
|
7846
7948
|
case 'init':
|
|
7847
7949
|
await commandInit(args, config, pricing, t);
|
|
7848
7950
|
break;
|