@trazum/cli 1.45.0 → 1.47.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trazum/cli",
3
- "version": "1.45.0",
3
+ "version": "1.47.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.45.0"
40
+ "@trazum/core": "1.47.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -33,6 +33,7 @@ export const en: CliMessages = {
33
33
  help: (d, bold) => `${bold('trazum')} — cut the cost of your prompts without losing what they ask for.
34
34
 
35
35
  ${bold('USAGE')}
36
+ trazum init [dir] [--dry-run | --yes]
36
37
  trazum optimize <file|-> [options]
37
38
  trazum check <file|dir|-> --max-tokens <n> [options]
38
39
  trazum baseline [dir] [options]
@@ -56,6 +57,22 @@ ${bold('USAGE')}
56
57
  trazum models
57
58
  trazum rules
58
59
 
60
+ ${bold('OPTIONS FOR init')}
61
+ --dry-run Print the config it would write and write nothing.
62
+ --yes Replace a trazum.config.json that is already there.
63
+ Without it an existing config is left alone.
64
+ --json The proposal as data, including every key it
65
+ declined and why. Writes nothing.
66
+
67
+ The first five minutes: finds your prompts, reads your code for which provider
68
+ it calls, finds a usage log or a credential for one, and writes a config out of
69
+ what it can actually justify. Then it prints the single most valuable thing it
70
+ found, arithmetic first.
71
+
72
+ It never invents a threshold. A budget is a policy, so init hands you the
73
+ measured figure and leaves the limit to you — a generated config full of
74
+ guessed numbers is one nobody trusts.
75
+
59
76
  ${bold('OPTIONS FOR prune')}
60
77
  --cases <file> One input per line, or a JSON array. Required.
61
78
  --yes Actually spend the calls. Without it the estimate is
@@ -809,6 +826,84 @@ ${bold('EXAMPLES')}
809
826
  wroteTo: (path) => `Optimised prompt written to ${path}`,
810
827
  },
811
828
 
829
+ init: {
830
+ heading: () => 'What is here',
831
+ host: (name) => `Running inside ${name}.`,
832
+ prompts: (count) =>
833
+ `${count} prompt ${count === 1 ? 'file' : 'files'} found.`,
834
+ noPrompts: () =>
835
+ 'No prompt files found. Directory mode reads .txt, .md, .prompt and .tmpl by default — set "extensions" if yours are named otherwise.',
836
+ sourcesTruncated: (cap) =>
837
+ `Stopped after ${cap} source files, so a provider named further down was not seen.`,
838
+ usageFound: (kind, where) =>
839
+ kind === 'connector-credential'
840
+ ? `Usage can be pulled: a credential is in ${where}. Run trazum connect.`
841
+ : kind === 'store'
842
+ ? `A local store of past measurements is in ${where}.`
843
+ : `Usage log found: ${where}.`,
844
+ noUsage: () =>
845
+ 'No usage found. Point trazum at a log (trazum profile <log.jsonl>) or pull one (trazum connect anthropic) — every money figure in this tool comes from one.',
846
+ usageUnreadable: (where, because) =>
847
+ `${where} is there and could not be read: ${because}. That is a different problem from having no usage, and it is the one to fix first.`,
848
+
849
+ configHeading: () => 'What the config would say',
850
+ nothingJustified: () =>
851
+ 'Nothing. Every key below needs evidence this run did not find, and a guessed config is worse than none.',
852
+ whyLocale: (locale) => `your environment asks for ${locale}`,
853
+ whyExtensions: (extensions, files) => `${files} prompt files use ${extensions}`,
854
+ whyModelMeasured: (model, sharePct) => `${sharePct}% of the measured bill went to ${model}`,
855
+ whyModelSource: (model, file, line) => `${file}:${line} names ${model}`,
856
+ whyCalls: (perMonth, calls, days) =>
857
+ `${calls} calls over ${days} days, stated as ${perMonth} a month`,
858
+ whyOutput: (average, outputTokens, calls) =>
859
+ `${outputTokens} output tokens over ${calls} calls averages ${average}`,
860
+ whyCache: (rate, cacheReadTokens, inputTokens) =>
861
+ `${cacheReadTokens} cached against ${inputTokens} fresh input is a hit rate of ${rate}`,
862
+
863
+ noModelEvidence: () => 'nothing measured or written in the source names one model',
864
+ modelConflict: (files) => `these files name more than one provider: ${files}`,
865
+ modelProviderOnly: (provider, file) =>
866
+ `${file} names ${provider} and no model — a provider default would read as your decision six weeks from now`,
867
+ nothingMeasured: () => 'nothing has been measured yet',
868
+ windowTooShort: (days, minimum) =>
869
+ `${days} days measured; ${minimum} are needed before that is a monthly rate rather than a forecast`,
870
+ undatedCalls: (undated, calls) =>
871
+ `${undated} of ${calls} calls carry no timestamp, so they cannot be placed inside the window a rate would divide by`,
872
+ cacheNotRecorded: () =>
873
+ 'this log has no cache columns at all, which is not the same as a hit rate of zero',
874
+ batchOnlyYouKnow: () =>
875
+ 'whether the work can wait for a batch window is a product decision, and no log records it',
876
+ labelsUnprovable: (labels) =>
877
+ `${labels} ${labels === 1 ? 'label' : 'labels'} in the log, and nothing here proves which prompt file sends which`,
878
+ budgetIsPolicy: () => 'a budget is a policy, and there is no measured figure to write one against yet',
879
+ budgetIsPolicyMeasured: (usd, days) =>
880
+ `a budget is a policy, so it is yours to set — the measured figure is $${usd} over ${days} days`,
881
+
882
+ findingHeading: () => 'The most valuable thing found',
883
+ noFinding: (why) =>
884
+ why === 'nothing-measured'
885
+ ? 'Nothing, because nothing has been measured. Every figure this tool prints as money comes from a usage log.'
886
+ : why === 'nothing-could-be-priced'
887
+ ? 'Nothing: the log was read, and no model in it is in the price catalogue. Run trazum models to see what is priced.'
888
+ : 'Nothing worth a line: no single slice of this bill can be moved by more than one per cent of it.',
889
+ findingCalls: (calls, label, model, days) =>
890
+ `${calls} calls labelled "${label}" went to ${model} over ${days} days.`,
891
+ findingSpent: (usd) => `They cost $${usd}.`,
892
+ findingRoute: (model) => `The same work fits ${model}, which is cheaper per token.`,
893
+ findingBatch: () => 'The Batch API halves both halves of the bill, for work that can wait.',
894
+ findingTotal: (usd, days) => `Together: $${usd} over the same ${days} days.`,
895
+ findingNext: () => 'trazum plan <your log> ranks every action, not just this one.',
896
+
897
+ wouldOverwrite: (keys) => `This replaces keys you already set: ${keys}. Pass --yes to write anyway.`,
898
+ nothingToWrite: () => 'No config written: nothing above could be justified from what is here.',
899
+ wouldWrite: (path) => `Would write ${path}:`,
900
+ wrote: (path) => `Written to ${path}.`,
901
+ existingRefused: (path) =>
902
+ `${path} already exists and was left alone. Pass --dry-run to see what would go in it, or --yes to replace it.`,
903
+ existingUnparseable: (path) =>
904
+ `${path} exists and could not be parsed, so nothing was written over it. Fix or move it first.`,
905
+ },
906
+
812
907
  where: {
813
908
  hostHeading: () => 'Running inside',
814
909
  subscription: (host) =>
@@ -1719,8 +1814,18 @@ ${bold('EXAMPLES')}
1719
1814
  '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.',
1720
1815
  needsAgainst: () =>
1721
1816
  '--against <newer.jsonl|dir> is required. A plan can only be verified against a log that came after it; without one there is nothing to hold the prediction to.',
1722
- badPlan: (path) =>
1723
- `${path} is not a plan document this tool can verify expected the JSON that "trazum plan -o" writes (schemaVersion 1, with an actions array).`,
1817
+ badPlan: (path, why) =>
1818
+ `${path} is not a plan document this tool can verify: ${why}. Expected the JSON that "trazum plan -o" writes.`,
1819
+ planRefusal: (why) =>
1820
+ why.kind === 'not-json'
1821
+ ? 'it is not valid JSON'
1822
+ : why.kind === 'not-an-object'
1823
+ ? 'the top level is not a JSON object'
1824
+ : why.kind === 'wrong-schema-version'
1825
+ ? `schemaVersion is ${JSON.stringify(why.found)} rather than 1`
1826
+ : why.kind === 'actions-not-a-list'
1827
+ ? 'there is no actions array'
1828
+ : `action ${why.index + 1} is malformed (${why.because})`,
1724
1829
  heading: (actions, planDate) =>
1725
1830
  planDate === null
1726
1831
  ? `Did it work? ${actions} actions from an undated plan, against this log`
package/src/i18n/es.ts CHANGED
@@ -20,6 +20,7 @@ export const es: CliMessages = {
20
20
  help: (d, bold) => `${bold('trazum')} — reduce el coste de tus prompts sin perder lo que piden.
21
21
 
22
22
  ${bold('USO')}
23
+ trazum init [dir] [--dry-run | --yes]
23
24
  trazum optimize <fichero|-> [opciones]
24
25
  trazum check <fichero|dir|-> --max-tokens <n> [opciones]
25
26
  trazum baseline [dir] [opciones]
@@ -43,6 +44,22 @@ ${bold('USO')}
43
44
  trazum models
44
45
  trazum rules
45
46
 
47
+ ${bold('OPCIONES DE init')}
48
+ --dry-run Imprime la configuración que escribiría y no escribe nada.
49
+ --yes Reemplaza un trazum.config.json que ya exista. Sin
50
+ esto, una configuración existente se deja intacta.
51
+ --json La propuesta como datos, incluida cada clave que
52
+ descartó y por qué. No escribe nada.
53
+
54
+ Los primeros cinco minutos: encuentra tus prompts, lee tu código para saber a
55
+ qué proveedor llama, busca un registro de consumo o una credencial para uno, y
56
+ escribe una configuración con lo que puede justificar de verdad. Después imprime
57
+ lo más valioso que encontró, con la aritmética delante.
58
+
59
+ Nunca se inventa un umbral. Un presupuesto es una política, así que init te da la
60
+ cifra medida y te deja el límite a ti — una configuración generada llena de
61
+ números adivinados no se la cree nadie.
62
+
46
63
  ${bold('OPCIONES DE prune')}
47
64
  --cases <fichero> Una entrada por línea, o un array JSON. Obligatorio.
48
65
  --yes Gasta las llamadas de verdad. Sin él se imprime la
@@ -842,6 +859,84 @@ ${bold('EJEMPLOS')}
842
859
  wroteTo: (path) => `Prompt optimizado escrito en ${path}`,
843
860
  },
844
861
 
862
+ init: {
863
+ heading: () => 'Lo que hay aquí',
864
+ host: (name) => `Ejecutándose dentro de ${name}.`,
865
+ prompts: (count) =>
866
+ `${count} ${count === 1 ? 'archivo de prompt encontrado' : 'archivos de prompt encontrados'}.`,
867
+ noPrompts: () =>
868
+ 'No se encontraron prompts. El modo directorio lee .txt, .md, .prompt y .tmpl por defecto — define "extensions" si los tuyos se llaman de otra forma.',
869
+ sourcesTruncated: (cap) =>
870
+ `Se paró tras ${cap} archivos de código, así que un proveedor nombrado más abajo no se vio.`,
871
+ usageFound: (kind, where) =>
872
+ kind === 'connector-credential'
873
+ ? `El consumo se puede descargar: hay una credencial en ${where}. Ejecuta trazum connect.`
874
+ : kind === 'store'
875
+ ? `Hay un almacén local de mediciones pasadas en ${where}.`
876
+ : `Registro de consumo encontrado: ${where}.`,
877
+ noUsage: () =>
878
+ 'No se encontró consumo. Apunta trazum a un registro (trazum profile <log.jsonl>) o descárgalo (trazum connect anthropic) — toda cifra en dinero de esta herramienta sale de uno.',
879
+ usageUnreadable: (where, because) =>
880
+ `${where} está ahí y no se pudo leer: ${because}. Ese es un problema distinto de no tener consumo, y es el primero que hay que arreglar.`,
881
+
882
+ configHeading: () => 'Lo que diría la configuración',
883
+ nothingJustified: () =>
884
+ 'Nada. Cada clave de abajo necesita pruebas que esta ejecución no encontró, y una configuración adivinada es peor que ninguna.',
885
+ whyLocale: (locale) => `tu entorno pide ${locale}`,
886
+ whyExtensions: (extensions, files) => `${files} prompts usan ${extensions}`,
887
+ whyModelMeasured: (model, sharePct) => `el ${sharePct}% de la factura medida fue a ${model}`,
888
+ whyModelSource: (model, file, line) => `${file}:${line} nombra ${model}`,
889
+ whyCalls: (perMonth, calls, days) =>
890
+ `${calls} llamadas en ${days} días, expresadas como ${perMonth} al mes`,
891
+ whyOutput: (average, outputTokens, calls) =>
892
+ `${outputTokens} tokens de salida en ${calls} llamadas dan una media de ${average}`,
893
+ whyCache: (rate, cacheReadTokens, inputTokens) =>
894
+ `${cacheReadTokens} en caché frente a ${inputTokens} de entrada nueva dan una tasa de ${rate}`,
895
+
896
+ noModelEvidence: () => 'nada medido ni escrito en el código nombra un único modelo',
897
+ modelConflict: (files) => `estos archivos nombran más de un proveedor: ${files}`,
898
+ modelProviderOnly: (provider, file) =>
899
+ `${file} nombra ${provider} y ningún modelo — el modelo por defecto del proveedor se leería como decisión tuya dentro de seis semanas`,
900
+ nothingMeasured: () => 'todavía no se ha medido nada',
901
+ windowTooShort: (days, minimum) =>
902
+ `${days} días medidos; hacen falta ${minimum} antes de que eso sea un ritmo mensual y no un pronóstico`,
903
+ undatedCalls: (undated, calls) =>
904
+ `${undated} de ${calls} llamadas no llevan fecha, así que no se pueden situar dentro de la ventana por la que dividiría un ritmo`,
905
+ cacheNotRecorded: () =>
906
+ 'este registro no tiene columnas de caché en absoluto, que no es lo mismo que una tasa de cero',
907
+ batchOnlyYouKnow: () =>
908
+ 'si el trabajo puede esperar a una ventana de lote es una decisión de producto, y ningún registro la anota',
909
+ labelsUnprovable: (labels) =>
910
+ `${labels} ${labels === 1 ? 'etiqueta' : 'etiquetas'} en el registro, y nada aquí demuestra qué prompt envía cuál`,
911
+ budgetIsPolicy: () => 'un presupuesto es una política, y todavía no hay cifra medida contra la que escribir una',
912
+ budgetIsPolicyMeasured: (usd, days) =>
913
+ `un presupuesto es una política, así que es tuyo — la cifra medida es $${usd} en ${days} días`,
914
+
915
+ findingHeading: () => 'Lo más valioso encontrado',
916
+ noFinding: (why) =>
917
+ why === 'nothing-measured'
918
+ ? 'Nada, porque no se ha medido nada. Toda cifra que esta herramienta imprime como dinero sale de un registro de consumo.'
919
+ : why === 'nothing-could-be-priced'
920
+ ? 'Nada: el registro se leyó y ningún modelo suyo está en el catálogo de precios. Ejecuta trazum models para ver cuáles lo están.'
921
+ : 'Nada que merezca una línea: ninguna porción de esta factura se puede mover más de un uno por ciento del total.',
922
+ findingCalls: (calls, label, model, days) =>
923
+ `${calls} llamadas con la etiqueta "${label}" fueron a ${model} en ${days} días.`,
924
+ findingSpent: (usd) => `Costaron $${usd}.`,
925
+ findingRoute: (model) => `El mismo trabajo cabe en ${model}, más barato por token.`,
926
+ findingBatch: () => 'La API de lotes reduce a la mitad ambas mitades de la factura, para trabajo que puede esperar.',
927
+ findingTotal: (usd, days) => `Juntas: $${usd} en esos mismos ${days} días.`,
928
+ findingNext: () => 'trazum plan <tu registro> ordena todas las acciones, no solo esta.',
929
+
930
+ wouldOverwrite: (keys) => `Esto reemplaza claves que ya tenías: ${keys}. Pasa --yes para escribir igualmente.`,
931
+ nothingToWrite: () => 'No se escribió configuración: nada de lo anterior se pudo justificar con lo que hay aquí.',
932
+ wouldWrite: (path) => `Escribiría ${path}:`,
933
+ wrote: (path) => `Escrito en ${path}.`,
934
+ existingRefused: (path) =>
935
+ `${path} ya existe y se dejó intacto. Pasa --dry-run para ver qué iría dentro, o --yes para reemplazarlo.`,
936
+ existingUnparseable: (path) =>
937
+ `${path} existe y no se pudo interpretar, así que no se escribió nada encima. Arréglalo o muévelo primero.`,
938
+ },
939
+
845
940
  where: {
846
941
  hostHeading: () => 'Ejecutándose dentro de',
847
942
  subscription: (host) =>
@@ -1749,8 +1844,18 @@ ${bold('EJEMPLOS')}
1749
1844
  '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.',
1750
1845
  needsAgainst: () =>
1751
1846
  '--against <nuevo.jsonl|dir> es obligatorio. Un plan solo puede verificarse contra un registro posterior; sin uno no hay nada a lo que someter la predicción.',
1752
- badPlan: (path) =>
1753
- `${path} no es un documento de plan que esta herramienta pueda verificar se esperaba el JSON que escribe "trazum plan -o" (schemaVersion 1, con un array actions).`,
1847
+ badPlan: (path, why) =>
1848
+ `${path} no es un documento de plan que esta herramienta pueda verificar: ${why}. Se esperaba el JSON que escribe "trazum plan -o".`,
1849
+ planRefusal: (why) =>
1850
+ why.kind === 'not-json'
1851
+ ? 'no es JSON válido'
1852
+ : why.kind === 'not-an-object'
1853
+ ? 'el nivel superior no es un objeto JSON'
1854
+ : why.kind === 'wrong-schema-version'
1855
+ ? `schemaVersion es ${JSON.stringify(why.found)} en vez de 1`
1856
+ : why.kind === 'actions-not-a-list'
1857
+ ? 'no hay un array actions'
1858
+ : `la acción ${why.index + 1} está mal formada (${why.because})`,
1754
1859
  heading: (actions, planDate) =>
1755
1860
  planDate === null
1756
1861
  ? `¿Funcionó? ${actions} acciones de un plan sin fecha, contra este registro`
package/src/i18n/types.ts CHANGED
@@ -189,6 +189,63 @@ export interface CliMessages {
189
189
  wroteTo(path: string): string;
190
190
  };
191
191
 
192
+ /**
193
+ * The first run.
194
+ *
195
+ * Every string here is either something that was *found* or something that
196
+ * was *declined with what would settle it*. There is deliberately no
197
+ * congratulation copy and no next-steps list: a first run that celebrates
198
+ * itself before showing a number is the shape people have learned to skip.
199
+ */
200
+ init: {
201
+ heading(): string;
202
+ host(name: string): string;
203
+ prompts(count: number): string;
204
+ noPrompts(): string;
205
+ sourcesTruncated(cap: number): string;
206
+ usageFound(kind: string, where: string): string;
207
+ noUsage(): string;
208
+ usageUnreadable(where: string, because: string): string;
209
+
210
+ configHeading(): string;
211
+ nothingJustified(): string;
212
+ whyLocale(locale: string): string;
213
+ whyExtensions(extensions: string, files: number): string;
214
+ whyModelMeasured(model: string, sharePct: number): string;
215
+ whyModelSource(model: string, file: string, line: number): string;
216
+ whyCalls(perMonth: number, calls: number, days: number): string;
217
+ whyOutput(average: number, outputTokens: number, calls: number): string;
218
+ whyCache(rate: number, cacheReadTokens: number, inputTokens: number): string;
219
+
220
+ noModelEvidence(): string;
221
+ modelConflict(files: string): string;
222
+ modelProviderOnly(provider: string, file: string): string;
223
+ nothingMeasured(): string;
224
+ windowTooShort(days: number, minimum: number): string;
225
+ undatedCalls(undated: number, calls: number): string;
226
+ cacheNotRecorded(): string;
227
+ batchOnlyYouKnow(): string;
228
+ labelsUnprovable(labels: number): string;
229
+ budgetIsPolicy(): string;
230
+ budgetIsPolicyMeasured(usd: string, days: number): string;
231
+
232
+ findingHeading(): string;
233
+ noFinding(why: string): string;
234
+ findingCalls(calls: string, label: string, model: string, days: number): string;
235
+ findingSpent(usd: string): string;
236
+ findingRoute(model: string): string;
237
+ findingBatch(): string;
238
+ findingTotal(usd: string, days: number): string;
239
+ findingNext(): string;
240
+
241
+ wouldOverwrite(keys: string): string;
242
+ nothingToWrite(): string;
243
+ wouldWrite(path: string): string;
244
+ wrote(path: string): string;
245
+ existingRefused(path: string): string;
246
+ existingUnparseable(path: string): string;
247
+ };
248
+
192
249
  where: {
193
250
  hostHeading(): string;
194
251
  subscription(host: string): string;
@@ -1212,7 +1269,22 @@ export interface CliMessages {
1212
1269
  noTarget(): string;
1213
1270
  needsAgainst(): string;
1214
1271
  /** Not a plan document: wrong shape, wrong version, or not JSON at all. */
1215
- badPlan(path: string): string;
1272
+ /**
1273
+ * `why` is the typed reason from `parsePlanDocument`, rendered so the
1274
+ * refusal names what is wrong rather than only that something is. A file
1275
+ * that is valid JSON and not a plan and a plan with one bad action are
1276
+ * different problems with different fixes.
1277
+ */
1278
+ badPlan(path: string, why: string): string;
1279
+ /** The typed refusal from `parsePlanDocument`, in one clause. */
1280
+ planRefusal(
1281
+ why:
1282
+ | { kind: 'not-json' }
1283
+ | { kind: 'not-an-object' }
1284
+ | { kind: 'wrong-schema-version'; found: unknown }
1285
+ | { kind: 'actions-not-a-list' }
1286
+ | { kind: 'action-malformed'; index: number; because: string },
1287
+ ): string;
1216
1288
  heading(actions: string, planDate: string | null): string;
1217
1289
  counts(arrived: string, notArrived: string, cannotTell: string): string;
1218
1290
  /** Two price lists are two measurements; every dollar line inherits this. */