@trazum/cli 1.48.0 → 1.49.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.48.0",
3
+ "version": "1.49.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.48.0"
40
+ "@trazum/core": "1.49.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -1666,7 +1666,9 @@ ${bold('EXAMPLES')}
1666
1666
  nothingMeasured: (dir) =>
1667
1667
  `Nothing is measured yet (the store at ${dir} is empty), so the budget half of every answer will say so. The cost half still answers from the catalogue: offline is a mode, not a failure.`,
1668
1668
  noBudget: () =>
1669
- 'No spend.maxUsd is configured, so "is there budget left" has no subject and every answer says so rather than guessing one.',
1669
+ 'No spend.monthlyUsd is configured, so "is there budget left" has no subject and every answer says so rather than guessing one. spend.maxUsd is deliberately not read here: it gates whatever period a log covers, and reading it as a monthly limit is how two surfaces of this tool come to disagree.',
1670
+ partialCoverage: (measuredDays, elapsedDays, period) =>
1671
+ `Only ${measuredDays} of the ${elapsedDays} elapsed days of ${period} carry any measurement, so the consumed figure is a floor on the period rather than the period. Pull the missing days with trazum connect before treating what is left as headroom.`,
1670
1672
  badPort: (value) => `"${value}" is not a port. Give a whole number from 0 to 65535, or use --socket.`,
1671
1673
  },
1672
1674
 
@@ -1742,6 +1744,27 @@ ${bold('EXAMPLES')}
1742
1744
  span === null
1743
1745
  ? `Nothing was older than ${days} days. ${kept} measurements kept, and the append log compacted.`
1744
1746
  : `Deleted ${count} measurements older than ${days} days, covering ${span} and ${usd} of measured spend. ${kept} kept, and the append log compacted to what the store already resolved to.`,
1747
+ budgetHeading: (period) => `Budget for ${period}`,
1748
+ budgetStanding: (consumed, limit, share, measuredDays, periodDays) =>
1749
+ `${consumed} of ${limit} (${share}), measured over ${measuredDays} of the month's ${periodDays} days.`,
1750
+ budgetShape: (shape, elapsedPct, coverage) =>
1751
+ shape === 'ahead'
1752
+ ? `The money is going faster than the calendar: ${elapsedPct}% of the month has elapsed.`
1753
+ : shape === 'behind'
1754
+ ? `The money is going slower than the calendar: ${elapsedPct}% of the month has elapsed.`
1755
+ : shape === 'on-pace'
1756
+ ? `Tracking the calendar: ${elapsedPct}% of the month has elapsed.`
1757
+ : coverage === 'partial'
1758
+ ? 'Whether that is fast or slow for the month cannot be told from a floor: the unmeasured days spent something, and only an overrun would be unarguable.'
1759
+ : 'There is nothing to compare the spend against yet.',
1760
+ budgetNeverForecast: () =>
1761
+ 'That is a shape, not a forecast. Where this goes next depends on what you do next, and no arithmetic here knows that.',
1762
+ budgetNothingMeasured: (elapsedDays) =>
1763
+ `Nothing has been measured this month, across ${elapsedDays} elapsed ${elapsedDays === 1 ? 'day' : 'days'}. That is not a budget under control — it is a budget nobody is watching. Run trazum connect to pull what the provider has.`,
1764
+ budgetPartial: (measuredDays, elapsedDays, days) =>
1765
+ `Only ${measuredDays} of ${elapsedDays} elapsed days carry any measurement, so the figure below is a floor on the month rather than the month. Missing: ${days}.`,
1766
+ budgetScopesUnmeasured: (count) =>
1767
+ `${count} budgeted ${count === 1 ? 'scope' : 'scopes'} (per label or per service) cannot be answered from the store: a store record carries a provider and a model, not a workload label. Gate those with trazum profile against a per-call log.`,
1745
1768
  },
1746
1769
 
1747
1770
  connect: {
package/src/i18n/es.ts CHANGED
@@ -1696,7 +1696,9 @@ ${bold('EJEMPLOS')}
1696
1696
  nothingMeasured: (dir) =>
1697
1697
  `Todavía no hay nada medido (el almacén de ${dir} está vacío), así que la mitad de presupuesto de cada respuesta lo dirá. La mitad del coste sigue respondiendo desde el catálogo: sin conexión es un modo, no un fallo.`,
1698
1698
  noBudget: () =>
1699
- 'No hay spend.maxUsd configurado, así que "queda presupuesto" no tiene sujeto y cada respuesta lo dice en vez de inventarse uno.',
1699
+ 'No hay spend.monthlyUsd configurado, así que "queda presupuesto" no tiene sujeto y cada respuesta lo dice en vez de inventarse uno. spend.maxUsd no se lee aquí a propósito: ese controla el periodo que cubra un registro, y leerlo como límite mensual es justo como dos superficies de esta herramienta acaban en desacuerdo.',
1700
+ partialCoverage: (measuredDays, elapsedDays, period) =>
1701
+ `Solo ${measuredDays} de los ${elapsedDays} días transcurridos de ${period} tienen alguna medición, así que la cifra consumida es un suelo del periodo y no el periodo. Descarga los días que faltan con trazum connect antes de tratar lo que queda como margen.`,
1700
1702
  badPort: (value) => `"${value}" no es un puerto. Da un número entero de 0 a 65535, o usa --socket.`,
1701
1703
  },
1702
1704
 
@@ -1772,6 +1774,27 @@ ${bold('EJEMPLOS')}
1772
1774
  span === null
1773
1775
  ? `Nada era más antiguo que ${days} días. ${kept} mediciones conservadas, y el log compactado.`
1774
1776
  : `Borradas ${count} mediciones de más de ${days} días, que cubren ${span} y ${usd} de gasto medido. ${kept} conservadas, y el log compactado a lo que el almacén ya resolvía.`,
1777
+ budgetHeading: (period) => `Presupuesto de ${period}`,
1778
+ budgetStanding: (consumed, limit, share, measuredDays, periodDays) =>
1779
+ `${consumed} de ${limit} (${share}), medido sobre ${measuredDays} de los ${periodDays} días del mes.`,
1780
+ budgetShape: (shape, elapsedPct, coverage) =>
1781
+ shape === 'ahead'
1782
+ ? `El dinero va más rápido que el calendario: ha transcurrido el ${elapsedPct}% del mes.`
1783
+ : shape === 'behind'
1784
+ ? `El dinero va más lento que el calendario: ha transcurrido el ${elapsedPct}% del mes.`
1785
+ : shape === 'on-pace'
1786
+ ? `Al ritmo del calendario: ha transcurrido el ${elapsedPct}% del mes.`
1787
+ : coverage === 'partial'
1788
+ ? 'Si eso es rápido o lento para el mes no se puede saber desde un suelo: los días sin medir gastaron algo, y solo un exceso sería incontestable.'
1789
+ : 'Todavía no hay nada con lo que comparar el gasto.',
1790
+ budgetNeverForecast: () =>
1791
+ 'Eso es una forma, no un pronóstico. A dónde va esto después depende de lo que hagas después, y ninguna aritmética de aquí lo sabe.',
1792
+ budgetNothingMeasured: (elapsedDays) =>
1793
+ `No se ha medido nada este mes, en ${elapsedDays} ${elapsedDays === 1 ? 'día transcurrido' : 'días transcurridos'}. Eso no es un presupuesto bajo control — es un presupuesto que nadie está mirando. Ejecuta trazum connect para descargar lo que tenga el proveedor.`,
1794
+ budgetPartial: (measuredDays, elapsedDays, days) =>
1795
+ `Solo ${measuredDays} de ${elapsedDays} días transcurridos tienen alguna medición, así que la cifra de abajo es un suelo del mes y no el mes. Faltan: ${days}.`,
1796
+ budgetScopesUnmeasured: (count) =>
1797
+ `${count} ${count === 1 ? 'ámbito presupuestado' : 'ámbitos presupuestados'} (por etiqueta o por servicio) no se pueden responder desde el almacén: un registro del almacén lleva un proveedor y un modelo, no una etiqueta de flujo. Contrólalos con trazum profile contra un registro por llamada.`,
1775
1798
  },
1776
1799
 
1777
1800
  connect: {
package/src/i18n/types.ts CHANGED
@@ -1151,6 +1151,12 @@ export interface CliMessages {
1151
1151
  measuredFrom(usd: string): string;
1152
1152
  nothingMeasured(dir: string): string;
1153
1153
  noBudget(): string;
1154
+ /**
1155
+ * The period is only partly measured — said out loud, because a position
1156
+ * standing on three days out of thirty must not read as a comfortable
1157
+ * ninety per cent remaining.
1158
+ */
1159
+ partialCoverage(measuredDays: number, elapsedDays: number, period: string): string;
1154
1160
  badPort(value: string): string;
1155
1161
  };
1156
1162
 
@@ -1205,6 +1211,15 @@ export interface CliMessages {
1205
1211
  pruneNeedsPolicy(): string;
1206
1212
  pruneDryRun(count: string, days: string, span: string | null, usd: string): string;
1207
1213
  pruned(count: string, days: string, span: string | null, usd: string, kept: string): string;
1214
+ /** The live budget — the one number `serve` and the MCP guard also read. */
1215
+ budgetHeading(period: string): string;
1216
+ budgetStanding(consumed: string, limit: string, share: string, measuredDays: string, periodDays: string): string;
1217
+ /** The shape of the burn, named. Never a date — see `budgetNeverForecast`. */
1218
+ budgetShape(shape: string, elapsedPct: number, coverage: string): string;
1219
+ budgetNeverForecast(): string;
1220
+ budgetNothingMeasured(elapsedDays: number): string;
1221
+ budgetPartial(measuredDays: number, elapsedDays: number, days: string): string;
1222
+ budgetScopesUnmeasured(count: number): string;
1208
1223
  };
1209
1224
 
1210
1225
  /**
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  computeSavings,
36
36
  countTokensAnthropic,
37
37
  DEFAULT_USAGE,
38
+ budgetPositions,
38
39
  detectFromSource,
39
40
  matchLocale,
40
41
  parsePlanDocument,
@@ -127,6 +128,7 @@ import type {
127
128
  UsageProfile,
128
129
  } from '@trazum/core';
129
130
  import type {
131
+ BudgetReport,
130
132
  UsageProfileReport,
131
133
  WaiverUse,
132
134
  InitDecline,
@@ -2747,41 +2749,39 @@ async function commandServe(
2747
2749
  t: CliMessages,
2748
2750
  ): Promise<void> {
2749
2751
  const root = process.cwd();
2750
- const limitUsd = config.spend?.maxUsd;
2751
2752
 
2752
2753
  const { resolved } = await readStore(root);
2753
- const measured = resolved.records.length > 0;
2754
+
2754
2755
  /**
2755
- * The window the measurement covers, carried into every answer.
2756
+ * The live budget, from `budgetPositions` the same number `store` prints
2757
+ * and the same one the MCP guard consults.
2756
2758
  *
2757
- * The position is read once at start, so a caller has to be able to see how
2758
- * old it is. A null window here would let a figure from last month read as
2759
- * current, which is the staleness this endpoint is otherwise honest about.
2759
+ * **This used to read `spend.maxUsd` against the whole store**, which is a
2760
+ * per-log gate compared against however much history the store happened to
2761
+ * hold. A year of records against a monthly limit reported as a budget
2762
+ * position, with a straight face and no way for a caller to tell. Same
2763
+ * units, different denominators, and the two surfaces disagreed by exactly
2764
+ * as much history as the machine had. `spend.monthlyUsd` is the key for a
2765
+ * calendar month and nothing infers one key from the other: a repository
2766
+ * with a per-log gate and no monthly budget has no monthly position, and
2767
+ * this says so rather than picking a number that is the right shape.
2760
2768
  */
2761
- const window = measured
2762
- ? {
2763
- fromMs: Math.min(...resolved.records.map((record) => record.fromMs)),
2764
- toMs: Math.max(...resolved.records.map((record) => record.toMs)),
2765
- }
2766
- : null;
2767
- const report = bucketedProfile(
2768
- {
2769
- provider: 'store',
2770
- granularity: 'bucketed',
2771
- buckets: bucketsFromRecords(resolved.records),
2772
- window,
2773
- gaps: [],
2774
- unavailable: [],
2775
- },
2776
- { catalogue: pricing },
2777
- );
2769
+ const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
2770
+ const standing = budget.positions[0] ?? null;
2771
+ const limitUsd = config.spend?.monthlyUsd;
2772
+ const measured = standing !== null && standing.coverage !== 'none';
2778
2773
 
2779
2774
  const server = buildServer({
2780
2775
  catalogue: pricing,
2781
2776
  position: () => ({
2782
- consumedUsd: measured ? report.total.totalUsd : undefined,
2777
+ // Nothing measured inside the period is `undefined`, never zero: the
2778
+ // endpoint's `cannot-tell` exists for exactly this, and a $0 consumed
2779
+ // would be the healthiest-looking budget a dead store can produce.
2780
+ consumedUsd: measured ? standing.consumedUsd : undefined,
2783
2781
  limitUsd,
2784
- window: report.span,
2782
+ // The period, not the store's span. A caller judging staleness needs to
2783
+ // know which month the figure is about.
2784
+ window: standing === null ? null : { fromMs: standing.period.fromMs, toMs: standing.period.toMs },
2785
2785
  }),
2786
2786
  });
2787
2787
 
@@ -2796,8 +2796,13 @@ async function commandServe(
2796
2796
  console.log(c.bold(t.serve.listening(where)));
2797
2797
  console.log(` ${c.dim(wrap(t.serve.loopbackOnly(), 74, ' '))}`);
2798
2798
  console.log(
2799
- ` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(report.total.totalUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`,
2799
+ ` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(standing.consumedUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`,
2800
2800
  );
2801
+ if (standing !== null && standing.coverage === 'partial') {
2802
+ console.log(
2803
+ ` ${c.yellow(wrap(t.serve.partialCoverage(standing.measuredDays, standing.elapsedDays, standing.period.id), 74, ' '))}`,
2804
+ );
2805
+ }
2801
2806
  if (limitUsd === undefined) {
2802
2807
  console.log(` ${c.dim(wrap(t.serve.noBudget(), 74, ' '))}`);
2803
2808
  }
@@ -3164,6 +3169,72 @@ async function commandStore(
3164
3169
  console.log(
3165
3170
  ` ${c.dim(wrap(keepDays === undefined ? t.store.noRetention() : t.store.retention(String(keepDays)), 74, ' '))}`,
3166
3171
  );
3172
+
3173
+ /**
3174
+ * The live budget, printed here because this is where the measurement lives.
3175
+ *
3176
+ * The same call `serve` makes and the same call the MCP guard makes, so the
3177
+ * three cannot disagree about how much of the month is gone — which is the
3178
+ * whole point of the number existing in one place.
3179
+ */
3180
+ renderBudget(budgetPositions(resolved.records, config.spend, { catalogue: pricing }), t, n);
3181
+ }
3182
+
3183
+ /**
3184
+ * One budget standing, rendered.
3185
+ *
3186
+ * Coverage before the money, deliberately. A reader who sees "$61 of $100"
3187
+ * first has already formed a view by the time they reach "over three of
3188
+ * nineteen elapsed days", and the second sentence has to undo the first.
3189
+ */
3190
+ function renderBudget(report: BudgetReport, t: CliMessages, n: (value: number) => string): void {
3191
+ const standing = report.positions[0];
3192
+ if (standing === undefined) {
3193
+ if (report.unmeasuredScopes.length > 0) {
3194
+ console.log();
3195
+ console.log(
3196
+ ` ${c.dim(wrap(t.store.budgetScopesUnmeasured(report.unmeasuredScopes.length), 74, ' '))}`,
3197
+ );
3198
+ }
3199
+ return;
3200
+ }
3201
+
3202
+ console.log();
3203
+ console.log(c.bold(t.store.budgetHeading(standing.period.id)));
3204
+
3205
+ if (standing.coverage === 'none') {
3206
+ // Nothing measured is never rendered as nothing spent. A dead store and a
3207
+ // quiet month produce the same zero, and only one of them is good news.
3208
+ console.log(` ${c.red(wrap(t.store.budgetNothingMeasured(standing.elapsedDays), 74, ' '))}`);
3209
+ return;
3210
+ }
3211
+ if (standing.coverage === 'partial') {
3212
+ console.log(
3213
+ ` ${c.yellow(wrap(t.store.budgetPartial(standing.measuredDays, standing.elapsedDays, standing.unmeasuredDays.join(', ')), 74, ' '))}`,
3214
+ );
3215
+ }
3216
+
3217
+ const share = standing.burn.consumedShare;
3218
+ console.log(
3219
+ ` ${t.store.budgetStanding(
3220
+ formatUsd(standing.consumedUsd),
3221
+ formatUsd(standing.limitUsd),
3222
+ share === null ? '—' : `${Math.round(share * 100)}%`,
3223
+ n(standing.measuredDays),
3224
+ n(standing.period.days),
3225
+ )}`,
3226
+ );
3227
+ const line = t.store.budgetShape(
3228
+ standing.burn.shape,
3229
+ Math.round(standing.burn.elapsedShare * 100),
3230
+ standing.coverage,
3231
+ );
3232
+ console.log(` ${standing.verdict === 'over' ? c.red(line) : c.dim(wrap(line, 74, ' '))}`);
3233
+ // Only where there is a shape to disclaim. "That is a shape, not a forecast"
3234
+ // under "nothing to compare against" is a disclaimer about nothing.
3235
+ if (standing.burn.shape !== 'cannot-tell') {
3236
+ console.log(` ${c.dim(wrap(t.store.budgetNeverForecast(), 74, ' '))}`);
3237
+ }
3167
3238
  }
3168
3239
 
3169
3240
  /**