@trazum/cli 1.50.0 → 1.50.2

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.50.0",
3
+ "version": "1.50.2",
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.50.0"
40
+ "@trazum/core": "1.50.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -57,6 +57,8 @@ ${bold('USAGE')}
57
57
  trazum conform <file|-> [--contract <name>]
58
58
  trazum models
59
59
  trazum rules
60
+ trazum feedback
61
+ trazum --version
60
62
 
61
63
  ${bold('OPTIONS FOR init')}
62
64
  --dry-run Print the config it would write and write nothing.
@@ -88,6 +90,17 @@ ${bold('OPTIONS FOR conform')}
88
90
  The second never gates. Choosing not to log sessions is a decision, not a
89
91
  defect. See docs/format.md.
90
92
 
93
+ ${bold('OPTIONS FOR feedback')}
94
+ (none)
95
+
96
+ Prints where to report a wrong optimisation, a bug, a question or a security
97
+ problem — and a blank issue with the version, runtime and platform already
98
+ filled in, printed in full first so nothing travels you have not read.
99
+
100
+ It sends nothing. Trazum has no telemetry: no ping, no install hook, no
101
+ anonymous counter, and a test fails the build if this command ever reaches
102
+ the network.
103
+
91
104
  ${bold('OPTIONS FOR prune')}
92
105
  --cases <file> One input per line, or a JSON array. Required.
93
106
  --yes Actually spend the calls. Without it the estimate is
@@ -907,7 +920,8 @@ ${bold('EXAMPLES')}
907
920
  findingRoute: (model) => `The same work fits ${model}, which is cheaper per token.`,
908
921
  findingBatch: () => 'The Batch API halves both halves of the bill, for work that can wait.',
909
922
  findingTotal: (usd, days) => `Together: $${usd} over the same ${days} days.`,
910
- findingNext: () => 'trazum plan <your log> ranks every action, not just this one.',
923
+ findingNext: () =>
924
+ 'trazum plan <your log> ranks every action, not just this one. If a figure here looks wrong, trazum feedback says where to tell us — it sends nothing on its own.',
911
925
 
912
926
  wouldOverwrite: (keys) => `This replaces keys you already set: ${keys}. Pass --yes to write anyway.`,
913
927
  nothingToWrite: () => 'No config written: nothing above could be justified from what is here.',
@@ -919,6 +933,22 @@ ${bold('EXAMPLES')}
919
933
  `${path} exists and could not be parsed, so nothing was written over it. Fix or move it first.`,
920
934
  },
921
935
 
936
+ feedback: {
937
+ heading: () => 'Telling us something',
938
+ sendsNothing: () =>
939
+ 'This command sends nothing, and neither does anything else here. Trazum has no telemetry: no ping, no install hook, no anonymous counter. A tool whose whole argument is that it reads your bill without uploading it cannot also be quietly reporting on you, and a test fails the build if this command ever reaches the network.',
940
+ whereHeading: () => 'Where',
941
+ wrongOptimisation: () =>
942
+ 'A rule changed what a prompt asks for — the report that matters most, and the failure this product exists to avoid:',
943
+ bug: () => 'Anything else that is wrong:',
944
+ question: () => 'A question, or an idea you are not sure about:',
945
+ security: () => 'A security problem — privately, never a public issue:',
946
+ environmentHeading: () => 'What a maintainer will ask for',
947
+ environmentOnly: () =>
948
+ 'That is the whole of it. Nothing about your work is here — not the config, not a prompt, not a label, not a figure. Those are what a good report needs and what only you can decide to share.',
949
+ linkHeading: () => 'A blank issue with the above already filled in',
950
+ },
951
+
922
952
  conform: {
923
953
  noTarget: () =>
924
954
  'Pass a file to check — a usage log, or any document Trazum emits. Use "-" to read from stdin.',
package/src/i18n/es.ts CHANGED
@@ -44,6 +44,8 @@ ${bold('USO')}
44
44
  trazum conform <fichero|-> [--contract <nombre>]
45
45
  trazum models
46
46
  trazum rules
47
+ trazum feedback
48
+ trazum --version
47
49
 
48
50
  ${bold('OPCIONES DE init')}
49
51
  --dry-run Imprime la configuración que escribiría y no escribe nada.
@@ -75,6 +77,18 @@ ${bold('OPCIONES DE conform')}
75
77
  La segunda nunca hace fallar nada. Decidir no registrar sesiones es una
76
78
  decisión, no un defecto. Ver docs/format.md.
77
79
 
80
+ ${bold('OPCIONES DE feedback')}
81
+ (ninguna)
82
+
83
+ Imprime dónde reportar una optimización incorrecta, un fallo, una pregunta o
84
+ un problema de seguridad — y una incidencia en blanco con la versión, el
85
+ runtime y la plataforma ya puestos, mostrados enteros antes para que no viaje
86
+ nada que no hayas leído.
87
+
88
+ No envía nada. Trazum no tiene telemetría: ni ping, ni hook de instalación, ni
89
+ contador anónimo, y una prueba hace fallar la compilación si este comando
90
+ llega a tocar la red.
91
+
78
92
  ${bold('OPCIONES DE prune')}
79
93
  --cases <fichero> Una entrada por línea, o un array JSON. Obligatorio.
80
94
  --yes Gasta las llamadas de verdad. Sin él se imprime la
@@ -940,7 +954,8 @@ ${bold('EJEMPLOS')}
940
954
  findingRoute: (model) => `El mismo trabajo cabe en ${model}, más barato por token.`,
941
955
  findingBatch: () => 'La API de lotes reduce a la mitad ambas mitades de la factura, para trabajo que puede esperar.',
942
956
  findingTotal: (usd, days) => `Juntas: $${usd} en esos mismos ${days} días.`,
943
- findingNext: () => 'trazum plan <tu registro> ordena todas las acciones, no solo esta.',
957
+ findingNext: () =>
958
+ 'trazum plan <tu registro> ordena todas las acciones, no solo esta. Si alguna cifra de aquí parece mal, trazum feedback dice dónde contarlo — no envía nada por su cuenta.',
944
959
 
945
960
  wouldOverwrite: (keys) => `Esto reemplaza claves que ya tenías: ${keys}. Pasa --yes para escribir igualmente.`,
946
961
  nothingToWrite: () => 'No se escribió configuración: nada de lo anterior se pudo justificar con lo que hay aquí.',
@@ -952,6 +967,22 @@ ${bold('EJEMPLOS')}
952
967
  `${path} existe y no se pudo interpretar, así que no se escribió nada encima. Arréglalo o muévelo primero.`,
953
968
  },
954
969
 
970
+ feedback: {
971
+ heading: () => 'Contarnos algo',
972
+ sendsNothing: () =>
973
+ 'Este comando no envía nada, y nada más aquí lo hace tampoco. Trazum no tiene telemetría: ni ping, ni hook de instalación, ni contador anónimo. Una herramienta cuyo argumento entero es que lee tu factura sin subirla no puede a la vez estar informando sobre ti en silencio, y una prueba hace fallar la compilación si este comando llega a tocar la red.',
974
+ whereHeading: () => 'Dónde',
975
+ wrongOptimisation: () =>
976
+ 'Una regla cambió lo que pedía un prompt — el informe que más importa, y el fallo que este producto existe para evitar:',
977
+ bug: () => 'Cualquier otra cosa que esté mal:',
978
+ question: () => 'Una pregunta, o una idea de la que no estás seguro:',
979
+ security: () => 'Un problema de seguridad — en privado, nunca en una incidencia pública:',
980
+ environmentHeading: () => 'Lo que te va a preguntar quien mantiene esto',
981
+ environmentOnly: () =>
982
+ 'Y eso es todo. Aquí no hay nada de tu trabajo — ni la configuración, ni un prompt, ni una etiqueta, ni una cifra. Eso es lo que necesita un buen informe y lo que solo tú puedes decidir compartir.',
983
+ linkHeading: () => 'Una incidencia en blanco con lo de arriba ya puesto',
984
+ },
985
+
955
986
  conform: {
956
987
  noTarget: () =>
957
988
  'Pasa un archivo para comprobar — un registro de consumo, o cualquier documento que emita Trazum. Usa "-" para leer de la entrada estándar.',
package/src/i18n/types.ts CHANGED
@@ -252,6 +252,25 @@ export interface CliMessages {
252
252
  * Two halves, and the copy keeps them apart everywhere: problems gate, gaps
253
253
  * do not. Choosing not to log sessions is a decision, not a defect.
254
254
  */
255
+ /**
256
+ * Where to say something, and the standing fact that this command sends
257
+ * nothing. The second half is not decoration: a tool that offers to help you
258
+ * file a report is exactly the shape of a tool that phones home, and the
259
+ * only way to be believed is to say so where somebody is looking.
260
+ */
261
+ feedback: {
262
+ heading(): string;
263
+ sendsNothing(): string;
264
+ whereHeading(): string;
265
+ wrongOptimisation(): string;
266
+ bug(): string;
267
+ question(): string;
268
+ security(): string;
269
+ environmentHeading(): string;
270
+ environmentOnly(): string;
271
+ linkHeading(): string;
272
+ };
273
+
255
274
  conform: {
256
275
  noTarget(): string;
257
276
  badContract(given: string, known: string): string;
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
2
3
  import { open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
- import { join, resolve as resolvePath } from 'node:path';
4
+ import { dirname, join, resolve as resolvePath } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
4
6
  import { gunzipSync } from 'node:zlib';
5
7
 
6
8
  import {
@@ -528,7 +530,7 @@ function disabledRules(args: Args, config: TrazumConfig): RuleId[] | undefined {
528
530
  * a threshold is set — `--max-growh 5` would have been ignored and the build
529
531
  * gone green. Silence is the wrong answer for a typo.
530
532
  */
531
- const GLOBAL_FLAGS = ['help', 'h', 'locale', 'json', 'config', 'pricing', 'pricing-live'];
533
+ const GLOBAL_FLAGS = ['help', 'h', 'version', 'v', 'locale', 'json', 'config', 'pricing', 'pricing-live'];
532
534
  const COMMAND_FLAGS: Record<string, string[]> = {
533
535
  optimize: [
534
536
  'level', 'model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch',
@@ -554,6 +556,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
554
556
  rank: ['level', 'model', 'calls', 'output-tokens', 'batch', 'disable', 'prompt', 'markdown-out'],
555
557
  init: ['dry-run', 'yes', 'json', 'pricing', 'pricing-live'],
556
558
  conform: ['contract', 'json'],
559
+ feedback: [],
557
560
  where: [],
558
561
  rules: [],
559
562
  blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
@@ -1476,6 +1479,44 @@ const INIT_LOG_CANDIDATES = [
1476
1479
  * nobody has vouched for.
1477
1480
  */
1478
1481
 
1482
+ /**
1483
+ * Where feedback goes. Compiled in, never configurable.
1484
+ *
1485
+ * A flag or a config key naming this host would let a fork — or anything that
1486
+ * had rewritten a config on disk — point somebody's bug report, and the
1487
+ * prefilled body with it, at a machine they did not choose. It is one string
1488
+ * and it stays one string.
1489
+ */
1490
+ /**
1491
+ * Which Trazum this is.
1492
+ *
1493
+ * Read from the manifest beside the built entry point rather than baked in by
1494
+ * a generator, so it cannot drift from what npm installed — the one number a
1495
+ * bug report is useless without is the one that must not be a copy.
1496
+ *
1497
+ * `readFileSync` at module load, deliberately: every other read in this file
1498
+ * is async and inside a command, but a version has to be available to
1499
+ * `--version` before any command is chosen, and one small synchronous read at
1500
+ * startup is cheaper than making the whole entry point await.
1501
+ *
1502
+ * A failure falls back to `unknown` rather than throwing. A tool that will not
1503
+ * start because it cannot find its own manifest is worse than one that admits
1504
+ * it does not know — and `unknown` in a bug report is itself a useful fact
1505
+ * about how somebody installed it.
1506
+ */
1507
+ const VERSION: string = (() => {
1508
+ try {
1509
+ const here = dirname(fileURLToPath(import.meta.url));
1510
+ const manifest: unknown = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
1511
+ const found = (manifest as { version?: unknown }).version;
1512
+ return typeof found === 'string' ? found : 'unknown';
1513
+ } catch {
1514
+ return 'unknown';
1515
+ }
1516
+ })();
1517
+
1518
+ const FEEDBACK_REPO = 'https://github.com/Davmunrey/Trazum';
1519
+
1479
1520
  /** Problems listed before the rest are counted. A wall of them helps nobody. */
1480
1521
  const MAX_CONFORM_PROBLEMS = 20;
1481
1522
 
@@ -1949,6 +1990,80 @@ async function commandConform(args: Args, t: CliMessages): Promise<void> {
1949
1990
  console.log();
1950
1991
  }
1951
1992
 
1993
+ /**
1994
+ * `trazum feedback` — where to say it, and what to say.
1995
+ *
1996
+ * **This command sends nothing.** Trazum has no telemetry: the CLI makes no
1997
+ * network call it was not explicitly asked to make, and there is no ping, no
1998
+ * install hook and no anonymous counter anywhere in it. That is not an
1999
+ * omission somebody has been meaning to fix — a tool whose entire argument is
2000
+ * that it reads your bill without uploading it cannot also be quietly
2001
+ * reporting on you, and the security suite fails the build if this command
2002
+ * ever reaches the network.
2003
+ *
2004
+ * So the loop is closed the only honest way: the person decides to send
2005
+ * something, and this makes that as cheap as possible. It prints the four
2006
+ * places worth writing to, and a **prefilled link** carrying the facts a
2007
+ * maintainer always has to ask for — version, runtime, platform — printed in
2008
+ * full first, so nothing travels that the sender has not read.
2009
+ *
2010
+ * Nothing about *their work* is in it. Not the config, not a prompt, not a
2011
+ * label, not a figure. Those are the things a bug report needs and the things
2012
+ * only the reporter can decide to share, and a command that helpfully attached
2013
+ * them would be the leak this product exists not to be.
2014
+ */
2015
+ function commandFeedback(t: CliMessages): void {
2016
+ const version = VERSION;
2017
+ /**
2018
+ * Facts about the machine, and nothing about the person.
2019
+ *
2020
+ * `process.platform` and the Node version are what every "cannot reproduce"
2021
+ * thread eventually asks for. The locale is here because Trazum ships two
2022
+ * languages and a report reading wrong in one of them is a real bug class.
2023
+ */
2024
+ const environment = [
2025
+ `Trazum ${version}`,
2026
+ `Node ${process.version}`,
2027
+ `${process.platform} ${process.arch}`,
2028
+ `locale ${t.locale}`,
2029
+ ];
2030
+
2031
+ const body = [
2032
+ '<!-- What happened, and what you expected instead. -->',
2033
+ '',
2034
+ '',
2035
+ '---',
2036
+ ...environment.map((line) => `- ${line}`),
2037
+ ].join('\n');
2038
+ const url =
2039
+ `${FEEDBACK_REPO}/issues/new?body=${encodeURIComponent(body)}`;
2040
+
2041
+ console.log();
2042
+ console.log(c.bold(t.feedback.heading()));
2043
+ console.log(` ${c.dim(wrap(t.feedback.sendsNothing(), 74, ' '))}`);
2044
+ console.log();
2045
+
2046
+ console.log(c.bold(t.feedback.whereHeading()));
2047
+ console.log(` ${t.feedback.wrongOptimisation()}`);
2048
+ console.log(` ${c.dim(`${FEEDBACK_REPO}/issues/new?template=wrong_optimisation.yml`)}`);
2049
+ console.log(` ${t.feedback.bug()}`);
2050
+ console.log(` ${c.dim(`${FEEDBACK_REPO}/issues/new?template=bug_report.yml`)}`);
2051
+ console.log(` ${t.feedback.question()}`);
2052
+ console.log(` ${c.dim(`${FEEDBACK_REPO}/discussions`)}`);
2053
+ console.log(` ${t.feedback.security()}`);
2054
+ console.log(` ${c.dim(`${FEEDBACK_REPO}/security/advisories/new`)}`);
2055
+ console.log();
2056
+
2057
+ console.log(c.bold(t.feedback.environmentHeading()));
2058
+ for (const line of environment) console.log(` ${line}`);
2059
+ console.log(` ${c.dim(wrap(t.feedback.environmentOnly(), 74, ' '))}`);
2060
+ console.log();
2061
+
2062
+ console.log(c.bold(t.feedback.linkHeading()));
2063
+ console.log(` ${url}`);
2064
+ console.log();
2065
+ }
2066
+
1952
2067
  function commandModels(t: CliMessages, pricing: PricingCatalogue): void {
1953
2068
  const n = (value: number): string => value.toLocaleString(t.numberLocale);
1954
2069
  const col = t.models.columns;
@@ -7835,6 +7950,20 @@ async function main(): Promise<void> {
7835
7950
  return;
7836
7951
  }
7837
7952
 
7953
+ /**
7954
+ * Before the help branch, and before the config loads.
7955
+ *
7956
+ * `trazum --version` on its own is how somebody answers "which one is
7957
+ * installed", and it has to work when the config is broken — that is
7958
+ * precisely the moment they are being asked. Placed above `!args.command`
7959
+ * for the same reason `--clear-suggestion-cache` is: with nothing else on
7960
+ * the line, the help branch would have swallowed it.
7961
+ */
7962
+ if (boolFlag(args, 'version') || boolFlag(args, 'v')) {
7963
+ console.log(VERSION);
7964
+ return;
7965
+ }
7966
+
7838
7967
  if (boolFlag(args, 'help') || boolFlag(args, 'h') || !args.command) {
7839
7968
  console.log(
7840
7969
  t.help(
@@ -7942,6 +8071,9 @@ async function main(): Promise<void> {
7942
8071
  case 'models':
7943
8072
  commandModels(t, pricing);
7944
8073
  break;
8074
+ case 'feedback':
8075
+ commandFeedback(t);
8076
+ break;
7945
8077
  case 'conform':
7946
8078
  await commandConform(args, t);
7947
8079
  break;