@trazum/cli 1.35.0 → 1.36.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.35.0",
3
+ "version": "1.36.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.35.0"
40
+ "@trazum/core": "1.36.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -119,6 +119,20 @@ ${bold('OPTIONS FOR optimize')}
119
119
  --calls <n> Calls per month. Default: ${d.callsPerMonth}.
120
120
  --output-tokens <n> Average output tokens. Default: ${d.avgOutputTokens}.
121
121
  --cache-hit-rate <0-1> Estimated cache hit rate. Default: ${d.cacheHitRate}.
122
+ --all-labels With --from-log: every prompt in the config's
123
+ "labels" map, optimised and priced against its
124
+ own measured traffic, ranked by what the change
125
+ is worth — plus the mismatches in both
126
+ directions (mapped prompts with no traffic,
127
+ traffic with no mapped prompt).
128
+ --from-log <usage.jsonl> Measure the three figures above from a usage log
129
+ instead of typing them: real call count, real
130
+ output size, real cache share, and the model the
131
+ calls actually went to. Refuses the typed flags
132
+ beside it, scales to a month only past a full
133
+ week of data, and says which figures are
134
+ measured. Pair with --label, or map the prompt
135
+ under "labels" in trazum.config.json.
122
136
  --batch The work tolerates latency (Batch API, 50% off).
123
137
  --disable <id,id> Turn off specific rules (see "trazum rules").
124
138
  --suggest Ask the LLM for phrase-level rewrites and list them
@@ -444,6 +458,18 @@ ${bold('EXAMPLES')}
444
458
  mustBeNonNegative: (name, raw) =>
445
459
  `--${name} must be a non-negative number (received: "${raw}").`,
446
460
  badLevel: (received) => `--level must be "safe" or "aggressive" (received: "${received}").`,
461
+ allLabelsNeedsLog: () =>
462
+ '--all-labels ranks prompts by measured traffic, so it needs --from-log <usage.jsonl>. Without a log every saving would be multiplied by the same typed guess, which ranks prompts by length and calls it a priority.',
463
+ allLabelsNeedsMap: () =>
464
+ '--all-labels reads the "labels" map in trazum.config.json — label to prompt file — and this config has none. Map at least one workload to its prompt.',
465
+ fromLogConflict: (flag) =>
466
+ `--from-log measures the figure --${flag} types, and merging a measurement with a guess produces a number that is neither. Pass one or the other.`,
467
+ fromLogNeedsLabel: (available) =>
468
+ `--from-log needs to know which workload this prompt is: pass --label, or map the prompt file under "labels" in trazum.config.json. Labels with traffic in this log: ${available}.`,
469
+ fromLogAmbiguousLabel: (target, labels) =>
470
+ `${target} is mapped to more than one label in trazum.config.json (${labels}), so --from-log cannot pick one silently. Pass --label.`,
471
+ fromLogLabelEmpty: (label, available) =>
472
+ `No priced call in this log carries the label "${label}", so there is nothing to measure — a zero-call profile would price this change as worthless rather than as unmeasured. Labels with traffic: ${available}.`,
447
473
  unknownRuleInDisable: (id) => `Unknown rule in --disable: "${id}". Full list: trazum rules`,
448
474
  unknownCommand: (command) => `Unknown command: "${command}". Try "trazum --help".`,
449
475
  missingInputFile: () => 'Missing input file. Use "-" to read from standard input.',
@@ -510,6 +536,30 @@ ${bold('EXAMPLES')}
510
536
  costWith: (modelName) => `Cost with ${modelName}`,
511
537
  usageLine: (calls, outputTokens, batch) =>
512
538
  `${calls} calls/month · ${outputTokens} output tokens per call${batch ? ' · Batch API' : ''}`,
539
+ allLabelsHeading: (count) => `Every mapped prompt against its own measured traffic — ${count} ranked by what the change is worth`,
540
+ allLabelsRow: (saving) => `${saving}/month if optimised`,
541
+ allLabelsRowPeriod: (saving) => `${saving} over the measured period if optimised`,
542
+ allLabelsFooter: () =>
543
+ 'Ranked by measured traffic, not by prompt length: a big prompt on a dead workload is worth less than a small one on a busy one. Each figure is this prompt\'s token delta at its own label\'s measured rate.',
544
+ allLabelsUnmapped: (label, usd) =>
545
+ `${label} carries ${usd} of measured spend and no prompt file is mapped to it — the workload nobody can optimise because nobody said where it lives. Map it under "labels" in trazum.config.json.`,
546
+ allLabelsDead: (label, path) =>
547
+ `${label} is mapped to ${path} and has no traffic in this log — a retired workload, a renamed label, or a typo that has been silently doing nothing.`,
548
+ allLabelsUnreadable: (label, path) =>
549
+ `${label} is mapped to ${path}, which could not be read. The mapping exists; the file does not.`,
550
+ usageLineMeasured: (calls, days, scaled, outputTokens, batch) =>
551
+ `${calls} calls measured over ${days} days — ${scaled}/month at that rate · ${outputTokens} output tokens per call, measured${batch ? ' · Batch API' : ''}`,
552
+ usageLineMeasuredPeriod: (calls, days, outputTokens, batch) =>
553
+ `${calls} calls measured${days === null ? ' (the log carries no clock)' : ` over ${days} days`} · ${outputTokens} output tokens per call, measured${batch ? ' · Batch API' : ''}`,
554
+ measuredModelShare: (model, share, count) =>
555
+ `This label ran on ${count} models; the figures use ${model}, which carried ${share} of its spend.`,
556
+ measuredNoOutput: () =>
557
+ 'No call in this slice recorded output tokens, so the output half of every figure below is $0 measured — not $0 assumed.',
558
+ perPeriodSaving: (saving, pct) => `saving ${saving} over the measured period (${pct}%)`,
559
+ periodNotScaled: (days) =>
560
+ days === null
561
+ ? 'Not scaled to a month: the log carries no clock, so there is no rate to scale. These figures cover exactly the calls measured.'
562
+ : `Not scaled to a month: ${days} days is under the week a scaling needs — shorter than one weekly cycle multiplies whichever part of the cycle it caught. These figures cover exactly the period measured.`,
513
563
  perMonthSaving: (saving, pct) => `saving ${saving}/month (${pct}%)`,
514
564
  beyondShortening: () => 'Beyond shortening the prompt',
515
565
  biggestLever: () => 'Start here:',
package/src/i18n/es.ts CHANGED
@@ -109,6 +109,20 @@ ${bold('OPCIONES DE optimize')}
109
109
  --calls <n> Llamadas al mes. Por defecto: ${d.callsPerMonth}.
110
110
  --output-tokens <n> Tokens de salida medios. Por defecto: ${d.avgOutputTokens}.
111
111
  --cache-hit-rate <0-1> Tasa de acierto de caché estimada. Por defecto: ${d.cacheHitRate}.
112
+ --all-labels Con --from-log: cada prompt del mapa "labels"
113
+ de la config, optimizado y valorado contra su
114
+ propio tráfico medido, ordenado por lo que vale
115
+ el cambio — más los desajustes en ambos sentidos
116
+ (prompts mapeados sin tráfico, tráfico sin
117
+ prompt mapeado).
118
+ --from-log <usage.jsonl> Mide las tres cifras de arriba desde un registro
119
+ de uso en vez de teclearlas: llamadas reales,
120
+ tamaño de salida real, cuota de caché real y el
121
+ modelo al que fueron las llamadas. Rechaza los
122
+ flags tecleados a su lado, escala a un mes solo
123
+ con una semana completa de datos, y dice qué
124
+ cifras están medidas. Combínalo con --label, o
125
+ mapea el prompt en "labels" de trazum.config.json.
112
126
  --batch El trabajo tolera latencia (Batch API, 50% menos).
113
127
  --disable <id,id> Desactiva reglas concretas (ver "trazum rules").
114
128
  --suggest Pide al LLM reescrituras a nivel de frase y las lista
@@ -451,6 +465,18 @@ ${bold('EJEMPLOS')}
451
465
  mustBeNonNegative: (name, raw) =>
452
466
  `--${name} debe ser un número no negativo (recibido: "${raw}").`,
453
467
  badLevel: (received) => `--level debe ser "safe" o "aggressive" (recibido: "${received}").`,
468
+ allLabelsNeedsLog: () =>
469
+ '--all-labels ordena los prompts por tráfico medido, así que necesita --from-log <usage.jsonl>. Sin registro cada ahorro se multiplicaría por la misma suposición tecleada, lo que ordena los prompts por longitud y lo llama prioridad.',
470
+ allLabelsNeedsMap: () =>
471
+ '--all-labels lee el mapa "labels" de trazum.config.json — etiqueta a fichero de prompt — y esta config no tiene ninguno. Mapea al menos una carga a su prompt.',
472
+ fromLogConflict: (flag) =>
473
+ `--from-log mide la cifra que --${flag} teclea, y mezclar una medición con una suposición produce un número que no es ninguna de las dos. Pasa una u otra.`,
474
+ fromLogNeedsLabel: (available) =>
475
+ `--from-log necesita saber qué carga es este prompt: pasa --label, o mapea el fichero bajo "labels" en trazum.config.json. Etiquetas con tráfico en este registro: ${available}.`,
476
+ fromLogAmbiguousLabel: (target, labels) =>
477
+ `${target} está mapeado a más de una etiqueta en trazum.config.json (${labels}), así que --from-log no puede elegir una en silencio. Pasa --label.`,
478
+ fromLogLabelEmpty: (label, available) =>
479
+ `Ninguna llamada valorada de este registro lleva la etiqueta "${label}", así que no hay nada que medir — un perfil de cero llamadas valoraría este cambio como inútil en vez de como no medido. Etiquetas con tráfico: ${available}.`,
454
480
  unknownRuleInDisable: (id) =>
455
481
  `Regla desconocida en --disable: "${id}". Lista completa: trazum rules`,
456
482
  unknownCommand: (command) => `Comando desconocido: "${command}". Prueba con "trazum --help".`,
@@ -519,6 +545,30 @@ ${bold('EJEMPLOS')}
519
545
  `${calls} llamadas/mes · ${outputTokens} tokens de salida por llamada${
520
546
  batch ? ' · Batch API' : ''
521
547
  }`,
548
+ allLabelsHeading: (count) => `Cada prompt mapeado contra su propio tráfico medido — ${count} ordenados por lo que vale el cambio`,
549
+ allLabelsRow: (saving) => `${saving}/mes si se optimiza`,
550
+ allLabelsRowPeriod: (saving) => `${saving} en el periodo medido si se optimiza`,
551
+ allLabelsFooter: () =>
552
+ 'Ordenado por tráfico medido, no por longitud del prompt: un prompt grande en una carga muerta vale menos que uno pequeño en una ocupada. Cada cifra es el delta de tokens de este prompt al ritmo medido de su propia etiqueta.',
553
+ allLabelsUnmapped: (label, usd) =>
554
+ `${label} lleva ${usd} de gasto medido y ningún fichero de prompt está mapeado a ella — la carga que nadie puede optimizar porque nadie dijo dónde vive. Mapéala en "labels" de trazum.config.json.`,
555
+ allLabelsDead: (label, path) =>
556
+ `${label} está mapeada a ${path} y no tiene tráfico en este registro — una carga retirada, una etiqueta renombrada, o una errata que lleva sin hacer nada en silencio.`,
557
+ allLabelsUnreadable: (label, path) =>
558
+ `${label} está mapeada a ${path}, que no se pudo leer. El mapeo existe; el fichero no.`,
559
+ usageLineMeasured: (calls, days, scaled, outputTokens, batch) =>
560
+ `${calls} llamadas medidas en ${days} días — ${scaled}/mes a ese ritmo · ${outputTokens} tokens de salida por llamada, medidos${batch ? ' · Batch API' : ''}`,
561
+ usageLineMeasuredPeriod: (calls, days, outputTokens, batch) =>
562
+ `${calls} llamadas medidas${days === null ? ' (el registro no tiene reloj)' : ` en ${days} días`} · ${outputTokens} tokens de salida por llamada, medidos${batch ? ' · Batch API' : ''}`,
563
+ measuredModelShare: (model, share, count) =>
564
+ `Esta etiqueta corrió en ${count} modelos; las cifras usan ${model}, que llevó el ${share} de su gasto.`,
565
+ measuredNoOutput: () =>
566
+ 'Ninguna llamada de este corte registró tokens de salida, así que la mitad de salida de cada cifra de abajo es $0 medido — no $0 supuesto.',
567
+ perPeriodSaving: (saving, pct) => `ahorro de ${saving} en el periodo medido (${pct}%)`,
568
+ periodNotScaled: (days) =>
569
+ days === null
570
+ ? 'Sin escalar a un mes: el registro no tiene reloj, así que no hay ritmo que escalar. Estas cifras cubren exactamente las llamadas medidas.'
571
+ : `Sin escalar a un mes: ${days} días queda por debajo de la semana que un escalado necesita — menos de un ciclo semanal multiplica la parte del ciclo que pilló. Estas cifras cubren exactamente el periodo medido.`,
522
572
  perMonthSaving: (saving, pct) => `ahorro ${saving}/mes (${pct}%)`,
523
573
  beyondShortening: () => 'Además de acortar el prompt',
524
574
  biggestLever: () => 'Empieza por aquí:',
package/src/i18n/types.ts CHANGED
@@ -37,6 +37,13 @@ export interface CliMessages {
37
37
  optionNeedsValue(name: string): string;
38
38
  mustBeNonNegative(name: string, raw: string): string;
39
39
  badLevel(received: string): string;
40
+ /** `--from-log`'s refusals: contradiction, missing label, ambiguity, emptiness. */
41
+ allLabelsNeedsLog(): string;
42
+ allLabelsNeedsMap(): string;
43
+ fromLogConflict(flag: string): string;
44
+ fromLogNeedsLabel(available: string): string;
45
+ fromLogAmbiguousLabel(target: string, labels: string): string;
46
+ fromLogLabelEmpty(label: string, available: string): string;
40
47
  unknownRuleInDisable(id: string): string;
41
48
  unknownCommand(command: string): string;
42
49
  unknownFlag(name: string, allowed: string): string;
@@ -82,6 +89,28 @@ export interface CliMessages {
82
89
  llmRejected(reason: string): string;
83
90
  costWith(modelName: string): string;
84
91
  usageLine(calls: string, outputTokens: number, batch: boolean): string;
92
+ /**
93
+ * `--from-log`: the usage line names its provenance. Measured and typed
94
+ * are different claims about the same multiplication, and under the week
95
+ * floor nothing says "month".
96
+ */
97
+ /**
98
+ * `--all-labels`: every mapped prompt against its own measured traffic,
99
+ * ranked by what the change is worth, with both coverage mismatches named.
100
+ */
101
+ allLabelsHeading(count: string): string;
102
+ allLabelsRow(saving: string): string;
103
+ allLabelsRowPeriod(saving: string): string;
104
+ allLabelsFooter(): string;
105
+ allLabelsUnmapped(label: string, usd: string): string;
106
+ allLabelsDead(label: string, path: string): string;
107
+ allLabelsUnreadable(label: string, path: string): string;
108
+ usageLineMeasured(calls: string, days: string, scaled: string, outputTokens: number, batch: boolean): string;
109
+ usageLineMeasuredPeriod(calls: string, days: string | null, outputTokens: number, batch: boolean): string;
110
+ measuredModelShare(model: string, share: string, count: string): string;
111
+ measuredNoOutput(): string;
112
+ perPeriodSaving(saving: string, pct: string): string;
113
+ periodNotScaled(days: string | null): string;
85
114
  perMonthSaving(saving: string, pct: string): string;
86
115
  beyondShortening(): string;
87
116
  biggestLever(): string;
package/src/index.ts CHANGED
@@ -23,6 +23,8 @@ import {
23
23
  coverageDrift,
24
24
  driversBetween,
25
25
  explainGateFailure,
26
+ labelCoverage,
27
+ measuredUsage,
26
28
  gateMargin,
27
29
  GATE_MARGIN_TIGHT,
28
30
  estimateTokens,
@@ -72,6 +74,7 @@ import {
72
74
  import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
73
75
  import { dayOf, formatGap, median, spanDays } from './time.js';
74
76
  import type {
77
+ MeasuredUsage,
75
78
  BaselineBreach,
76
79
  BaselineChange,
77
80
  BaselineComparison,
@@ -165,6 +168,7 @@ interface Args {
165
168
 
166
169
  const VALUE_FLAGS = new Set([
167
170
  'against',
171
+ 'from-log',
168
172
  // `route` takes a path here, and the flag is deliberately not `--prompt`:
169
173
  // everywhere else in this tool `--prompt` names a marked prompt *inside* a
170
174
  // source file, and reusing it for a path would be a trap laid for the reader.
@@ -305,6 +309,23 @@ function levelFlag(args: Args, config: TrazumConfig, t: CliMessages): RuleLevel
305
309
  * model id. It beats the default because reading the code is better than
306
310
  * assuming, and loses to config because being told is better than reading.
307
311
  */
312
+ /**
313
+ * One usage log, gzip included, shared by every command that reads one.
314
+ *
315
+ * A `.gz` that will not decompress is an error naming the file — skipping it
316
+ * would be a figure quietly missing a day, the failure this repository
317
+ * refuses everywhere it can occur.
318
+ */
319
+ async function readUsageLog(file: string, t: CliMessages): Promise<string> {
320
+ if (!file.endsWith('.gz')) return readFile(file, 'utf8');
321
+ const compressed = await readFile(file);
322
+ try {
323
+ return gunzipSync(compressed).toString('utf8');
324
+ } catch (error) {
325
+ throw new Error(t.profile.badGzip(file, error instanceof Error ? error.message : String(error)));
326
+ }
327
+ }
328
+
308
329
  function usageFrom(
309
330
  args: Args,
310
331
  config: TrazumConfig,
@@ -436,7 +457,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
436
457
  'level', 'model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch',
437
458
  'disable', 'llm', 'exact-tokens', 'diff', 'reorder', 'out', 'o',
438
459
  'tokens-only', 'cost', 'prompt', 'suggest', 'apply-suggestions',
439
- 'cache-suggestions',
460
+ 'cache-suggestions', 'from-log', 'label', 'all-labels',
440
461
  ],
441
462
  check: ['max-tokens', 'level', 'exact-tokens', 'markdown-out', 'baseline'],
442
463
  baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
@@ -629,6 +650,8 @@ function printReport(
629
650
  suggestions: { result: SuggestResult; applied: boolean; locale: Locale } | null = null,
630
651
  /** They named a scenario, and the host is suppressing the money anyway. */
631
652
  namedScenario = false,
653
+ /** Present when the usage came from a log rather than from typing. */
654
+ measured: MeasuredUsage | null = null,
632
655
  ): void {
633
656
  const n = (value: number): string => value.toLocaleString(t.numberLocale);
634
657
  const sourceNote =
@@ -776,7 +799,7 @@ function printReport(
776
799
  if (tokensOnly) {
777
800
  printTokensOnly(result, host, t, n, namedScenario);
778
801
  } else {
779
- printMoney(result, t, n);
802
+ printMoney(result, t, n, measured);
780
803
  }
781
804
 
782
805
  // On a subscription, an advisory whose entire pitch is money is not weaker
@@ -852,17 +875,61 @@ function printReport(
852
875
  }
853
876
 
854
877
  /** The cost section, for anyone billed by the token. */
855
- function printMoney(result: OptimizationResult, t: CliMessages, n: (v: number) => string): void {
878
+ function printMoney(
879
+ result: OptimizationResult,
880
+ t: CliMessages,
881
+ n: (v: number) => string,
882
+ /** Present when the usage came from a log rather than from typing. */
883
+ measured: MeasuredUsage | null = null,
884
+ ): void {
856
885
  const { savings } = result;
857
886
  console.log();
858
887
  console.log(c.bold(t.report.costWith(savings.modelDisplayName)));
859
- console.log(
860
- ` ${t.report.usageLine(
861
- n(result.usage.callsPerMonth),
862
- result.usage.avgOutputTokens,
863
- result.usage.batchEligible,
864
- )}`,
865
- );
888
+ /**
889
+ * The usage line names its provenance. "1,000 calls/month" typed and
890
+ * "1,043 calls measured over 12 days, scaled" are different claims about
891
+ * the same multiplication, and the reader budgeting on the result must
892
+ * know which one they are holding. Under the week floor nothing is scaled
893
+ * and nothing says "month": the figures cover exactly the period measured.
894
+ */
895
+ if (measured !== null) {
896
+ if (measured.scaled !== null) {
897
+ console.log(
898
+ ` ${t.report.usageLineMeasured(
899
+ n(measured.calls),
900
+ measured.scaled.fromDays.toFixed(1),
901
+ n(result.usage.callsPerMonth),
902
+ result.usage.avgOutputTokens,
903
+ result.usage.batchEligible,
904
+ )}`,
905
+ );
906
+ } else {
907
+ console.log(
908
+ ` ${t.report.usageLineMeasuredPeriod(
909
+ n(measured.calls),
910
+ measured.spanDays === null ? null : measured.spanDays.toFixed(1),
911
+ result.usage.avgOutputTokens,
912
+ result.usage.batchEligible,
913
+ )}`,
914
+ );
915
+ }
916
+ if (measured.models.count > 1) {
917
+ console.log(
918
+ ` ${c.dim(wrap(t.report.measuredModelShare(measured.models.chosen, `${(measured.models.chosenShareOfSpend * 100).toFixed(0)}%`, n(measured.models.count)), 74, ' '))}`,
919
+ );
920
+ }
921
+ if (measured.outputUnmeasured) {
922
+ console.log(` ${c.dim(wrap(t.report.measuredNoOutput(), 74, ' '))}`);
923
+ }
924
+ } else {
925
+ console.log(
926
+ ` ${t.report.usageLine(
927
+ n(result.usage.callsPerMonth),
928
+ result.usage.avgOutputTokens,
929
+ result.usage.batchEligible,
930
+ )}`,
931
+ );
932
+ }
866
933
  // Said, not assumed. Once prices can be overlaid locally, a figure from the
867
934
  // bundled catalogue and a figure from somebody's JSON file look identical, and
868
935
  // the reader has to be able to tell which one they are about to budget against.
@@ -875,17 +942,27 @@ function printMoney(result: OptimizationResult, t: CliMessages, n: (v: number) =
875
942
  ` ${c.yellow(t.report.pricingOverlaid(touched.join(', '), result.pricingSource.lastReviewed))}`,
876
943
  );
877
944
  }
945
+ const periodOnly = measured !== null && measured.scaled === null;
878
946
  console.log(
879
947
  ` ${formatUsd(savings.perMonth.before.totalUsd)} → ` +
880
948
  `${c.green(formatUsd(savings.perMonth.after.totalUsd))} ` +
881
949
  c.bold(
882
- t.report.perMonthSaving(
883
- formatUsd(savings.monthlySavingsUsd),
884
- savings.monthlySavingsPct.toFixed(1),
885
- ),
950
+ periodOnly
951
+ ? t.report.perPeriodSaving(
952
+ formatUsd(savings.monthlySavingsUsd),
953
+ savings.monthlySavingsPct.toFixed(1),
954
+ )
955
+ : t.report.perMonthSaving(
956
+ formatUsd(savings.monthlySavingsUsd),
957
+ savings.monthlySavingsPct.toFixed(1),
958
+ ),
886
959
  ),
887
960
  );
888
-
961
+ if (periodOnly) {
962
+ console.log(
963
+ ` ${c.dim(wrap(t.report.periodNotScaled(measured!.spanDays === null ? null : measured!.spanDays.toFixed(1)), 74, ' '))}`,
964
+ );
965
+ }
889
966
  }
890
967
 
891
968
  /**
@@ -1372,6 +1449,97 @@ async function commandOptimize(
1372
1449
  t: CliMessages,
1373
1450
  locale: Locale,
1374
1451
  ): Promise<void> {
1452
+ /**
1453
+ * `--all-labels`: every mapped prompt against its own measured traffic,
1454
+ * ranked by what the change is worth — the list a person actually wants,
1455
+ * which is "which prompt do I edit first".
1456
+ *
1457
+ * Requires `--from-log`, because ranking estimated savings that were all
1458
+ * multiplied by the same typed guess ranks the prompts by length, and calls
1459
+ * that a priority. And it renders both coverage mismatches at the end: a
1460
+ * prompt mapped to a label with no traffic is dead weight or a rename, and
1461
+ * a label carrying real money with no prompt mapped is the workload nobody
1462
+ * can optimise because nobody said where it lives.
1463
+ */
1464
+ if (boolFlag(args, 'all-labels')) {
1465
+ const fromLogPath = stringFlag(args, 'from-log');
1466
+ if (fromLogPath === undefined) throw new Error(t.errors.allLabelsNeedsLog());
1467
+ const labelsMap = config.labels ?? {};
1468
+ if (Object.keys(labelsMap).length === 0) throw new Error(t.errors.allLabelsNeedsMap());
1469
+ const report = profileUsage(await readUsageLog(fromLogPath, t), { catalogue: pricing });
1470
+ const coverage = labelCoverage(report, labelsMap);
1471
+ const level = levelFlag(args, config, t);
1472
+
1473
+ interface Row {
1474
+ label: string;
1475
+ path: string;
1476
+ tokensBefore: number;
1477
+ tokensAfter: number;
1478
+ savingUsd: number;
1479
+ periodOnly: boolean;
1480
+ spentUsd: number;
1481
+ }
1482
+ const rows: Row[] = [];
1483
+ const unreadable: { label: string; path: string }[] = [];
1484
+ for (const { label, promptPath } of coverage.joined) {
1485
+ const m = measuredUsage(report, label, { batchEligible: config.usage?.batchEligible ?? false });
1486
+ if (m === null) continue;
1487
+ let text: string;
1488
+ try {
1489
+ text = await readFile(promptPath, 'utf8');
1490
+ } catch {
1491
+ unreadable.push({ label, path: promptPath });
1492
+ continue;
1493
+ }
1494
+ const r = optimize(text, { level, usage: m.profile, locale, pricing });
1495
+ rows.push({
1496
+ label,
1497
+ path: promptPath,
1498
+ tokensBefore: r.tokensBefore,
1499
+ tokensAfter: r.tokensAfter,
1500
+ savingUsd: r.savings.monthlySavingsUsd,
1501
+ periodOnly: m.scaled === null,
1502
+ spentUsd: m.spentUsd,
1503
+ });
1504
+ }
1505
+ rows.sort((a, b) => b.savingUsd - a.savingUsd);
1506
+
1507
+ const n = (value: number): string => value.toLocaleString(t.numberLocale);
1508
+ console.log(c.bold(t.report.allLabelsHeading(n(rows.length))));
1509
+ for (const row of rows) {
1510
+ const saving = row.periodOnly
1511
+ ? t.report.allLabelsRowPeriod(formatUsd(row.savingUsd))
1512
+ : t.report.allLabelsRow(formatUsd(row.savingUsd));
1513
+ console.log(
1514
+ ` ${row.savingUsd > 0 ? c.green('→') : c.dim('·')} ${c.bold(row.label)} ${saving} ${c.dim(`${row.path} · ${n(row.tokensBefore)} → ${n(row.tokensAfter)} tokens · ${formatUsd(row.spentUsd)} measured`)}`,
1515
+ );
1516
+ }
1517
+ if (rows.length > 0) {
1518
+ console.log(` ${c.dim(wrap(t.report.allLabelsFooter(), 74, ' '))}`);
1519
+ }
1520
+
1521
+ /**
1522
+ * The mismatches, both directions, never silently. These are the two
1523
+ * failures neither side can see alone.
1524
+ */
1525
+ for (const gap of coverage.trafficWithoutPrompt.slice(0, 5)) {
1526
+ console.log(
1527
+ ` ${c.yellow('!')} ${wrap(t.report.allLabelsUnmapped(gap.label, formatUsd(gap.spentUsd)), 74, ' ')}`,
1528
+ );
1529
+ }
1530
+ for (const dead of coverage.mappedWithoutTraffic) {
1531
+ console.log(
1532
+ ` ${c.dim(wrap(t.report.allLabelsDead(dead.label, dead.promptPath), 74, ' '))}`,
1533
+ );
1534
+ }
1535
+ for (const miss of unreadable) {
1536
+ console.log(
1537
+ ` ${c.yellow('!')} ${wrap(t.report.allLabelsUnreadable(miss.label, miss.path), 74, ' ')}`,
1538
+ );
1539
+ }
1540
+ return;
1541
+ }
1542
+
1375
1543
  const target = args.positional[0];
1376
1544
  const raw = await readInput(target, t);
1377
1545
  const level = levelFlag(args, config, t);
@@ -1393,7 +1561,61 @@ async function commandOptimize(
1393
1561
  // Detection sits between config and defaults, as everywhere: a flag beats
1394
1562
  // config, config beats what the code says, and what the code says beats a
1395
1563
  // built-in default that has no idea which provider you use.
1396
- const usage = usageFrom(args, config, t, source?.model);
1564
+ let usage = usageFrom(args, config, t, source?.model);
1565
+
1566
+ /**
1567
+ * `--from-log`: the multiplication stops guessing.
1568
+ *
1569
+ * The saving printed below is `token delta × usage`, and until now every
1570
+ * part of `usage` was typed by a human. A usage log knows the real call
1571
+ * count, the real output size, the real cache share and the model the
1572
+ * calls actually went to — so `--from-log` measures them, and the typed
1573
+ * flags are refused beside it rather than merged: measuring and typing the
1574
+ * same figure is a contradiction, not a preference order.
1575
+ */
1576
+ const fromLog = stringFlag(args, 'from-log');
1577
+ let measured: MeasuredUsage | null = null;
1578
+ if (fromLog !== undefined) {
1579
+ for (const flag of ['calls', 'output-tokens', 'cache-hit-rate', 'model']) {
1580
+ if (args.flags.get(flag) !== undefined) {
1581
+ throw new Error(t.errors.fromLogConflict(flag));
1582
+ }
1583
+ }
1584
+ const report = profileUsage(await readUsageLog(fromLog, t), { catalogue: pricing });
1585
+
1586
+ /**
1587
+ * Which label this prompt is. `--label` says it outright; otherwise the
1588
+ * config's `labels` map is read in reverse — it maps labels to prompt
1589
+ * files, and the file on the command line is looked up among its values.
1590
+ * Ambiguity (two labels mapped to one file) is an error naming both,
1591
+ * never a silent first match.
1592
+ */
1593
+ let label = stringFlag(args, 'label');
1594
+ if (label === undefined && target !== undefined && config.labels !== undefined) {
1595
+ const hits = Object.entries(config.labels)
1596
+ .filter(([, path]) => resolvePath(path) === resolvePath(target))
1597
+ .map(([name]) => name);
1598
+ if (hits.length > 1) throw new Error(t.errors.fromLogAmbiguousLabel(target, hits.join(', ')));
1599
+ label = hits[0];
1600
+ }
1601
+ if (label === undefined) {
1602
+ const available = report.byLabel
1603
+ .map((row) => (row.label === UNLABELLED ? t.profile.unlabelled() : row.label))
1604
+ .join(', ');
1605
+ throw new Error(t.errors.fromLogNeedsLabel(available || '—'));
1606
+ }
1607
+
1608
+ measured = measuredUsage(report, label, {
1609
+ batchEligible: boolFlag(args, 'batch', config.usage?.batchEligible ?? false),
1610
+ });
1611
+ if (measured === null) {
1612
+ const available = report.byLabel
1613
+ .map((row) => (row.label === UNLABELLED ? t.profile.unlabelled() : row.label))
1614
+ .join(', ');
1615
+ throw new Error(t.errors.fromLogLabelEmpty(label, available || '—'));
1616
+ }
1617
+ usage = measured.profile;
1618
+ }
1397
1619
 
1398
1620
  const disableRules = disabledRules(args, config) ?? [];
1399
1621
  for (const id of disableRules) {
@@ -1586,7 +1808,15 @@ async function commandOptimize(
1586
1808
  // Cursor wants the dollars, and they should not have to leave the editor to
1587
1809
  // see them.
1588
1810
  const host = detectHost();
1589
- const tokensOnly = boolFlag(args, 'cost')
1811
+ /**
1812
+ * `--from-log` implies `--cost`, and the reasoning is different from the
1813
+ * `--calls` case documented below: `--calls` is a typed scenario parameter,
1814
+ * but a usage log with billed token counts is *evidence* — proof this
1815
+ * prompt's traffic goes to a metered API, whatever the terminal running
1816
+ * the command bills like. Withholding the money there would suppress
1817
+ * exactly the figures the person measured in order to see.
1818
+ */
1819
+ const tokensOnly = boolFlag(args, 'cost') || measured !== null
1590
1820
  ? false
1591
1821
  : boolFlag(args, 'tokens-only') || host.billing === 'subscription';
1592
1822
  /**
@@ -1609,6 +1839,7 @@ async function commandOptimize(
1609
1839
  ? { result: suggestions, applied: boolFlag(args, 'apply-suggestions'), locale }
1610
1840
  : null,
1611
1841
  namedScenario,
1842
+ measured,
1612
1843
  );
1613
1844
  if (outPath) {
1614
1845
  console.log(c.dim(t.report.wroteTo(outPath)));
@@ -2031,16 +2262,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2031
2262
  * alternative — skipping it — is a total quietly missing a day, which is
2032
2263
  * the failure this repository refuses in every other place it can occur.
2033
2264
  */
2034
- const readLog = async (file: string): Promise<string> => {
2035
- if (!file.endsWith('.gz')) return readFile(file, 'utf8');
2036
- const compressed = await readFile(file);
2037
- try {
2038
- return gunzipSync(compressed).toString('utf8');
2039
- } catch (error) {
2040
- throw new Error(t.profile.badGzip(file, error instanceof Error ? error.message : String(error)));
2041
- }
2042
- };
2043
- const logTexts = await Promise.all(logFiles.map((file) => readLog(file)));
2265
+ const logTexts = await Promise.all(logFiles.map((file) => readUsageLog(file, t)));
2044
2266
  // A file that does not end in a newline would otherwise glue its last record
2045
2267
  // to the next file's first one, and both would be reported as unreadable.
2046
2268
  const raw = logTexts.map((text) => (text.endsWith('\n') ? text : `${text}\n`)).join('');
@@ -2206,7 +2428,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2206
2428
  // The same reader as the log itself, so `--against last-month.jsonl.gz`
2207
2429
  // works: a comparison that could only read one of the two formats would
2208
2430
  // be a flag that fails on exactly the rotated file it exists to read.
2209
- ? profileUsage(await readLog(againstPath), {
2431
+ ? profileUsage(await readUsageLog(againstPath, t), {
2210
2432
  catalogue: pricing,
2211
2433
  label: onlyLabel,
2212
2434
  // The same window on both sides, for the same reason as the label: