estela 0.1.26 → 0.1.31

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.
Files changed (110) hide show
  1. package/README.md +57 -4
  2. package/dist/billing/invoice.d.ts.map +1 -1
  3. package/dist/billing/invoice.js +2 -1
  4. package/dist/billing/invoice.js.map +1 -1
  5. package/dist/billing/localize.d.ts +37 -0
  6. package/dist/billing/localize.d.ts.map +1 -0
  7. package/dist/billing/localize.js +72 -0
  8. package/dist/billing/localize.js.map +1 -0
  9. package/dist/billing/localize.test.d.ts +2 -0
  10. package/dist/billing/localize.test.d.ts.map +1 -0
  11. package/dist/billing/localize.test.js +144 -0
  12. package/dist/billing/localize.test.js.map +1 -0
  13. package/dist/billing/sessionize.d.ts.map +1 -1
  14. package/dist/billing/sessionize.js +4 -7
  15. package/dist/billing/sessionize.js.map +1 -1
  16. package/dist/bin.js +1 -1
  17. package/dist/bin.js.map +1 -1
  18. package/dist/cli-idioma.test.d.ts +2 -0
  19. package/dist/cli-idioma.test.d.ts.map +1 -0
  20. package/dist/cli-idioma.test.js +190 -0
  21. package/dist/cli-idioma.test.js.map +1 -0
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +137 -49
  24. package/dist/cli.js.map +1 -1
  25. package/dist/db/schema.d.ts.map +1 -1
  26. package/dist/db/schema.js +19 -2
  27. package/dist/db/schema.js.map +1 -1
  28. package/dist/db/schema.test.js +68 -4
  29. package/dist/db/schema.test.js.map +1 -1
  30. package/dist/db/store.d.ts.map +1 -1
  31. package/dist/db/store.js +7 -4
  32. package/dist/db/store.js.map +1 -1
  33. package/dist/export/csv.d.ts.map +1 -1
  34. package/dist/export/csv.js +49 -18
  35. package/dist/export/csv.js.map +1 -1
  36. package/dist/export/dates.d.ts +13 -0
  37. package/dist/export/dates.d.ts.map +1 -0
  38. package/dist/export/dates.js +29 -0
  39. package/dist/export/dates.js.map +1 -0
  40. package/dist/export/idioma.test.d.ts +2 -0
  41. package/dist/export/idioma.test.d.ts.map +1 -0
  42. package/dist/export/idioma.test.js +251 -0
  43. package/dist/export/idioma.test.js.map +1 -0
  44. package/dist/export/invoice-pdf.d.ts.map +1 -1
  45. package/dist/export/invoice-pdf.js +26 -22
  46. package/dist/export/invoice-pdf.js.map +1 -1
  47. package/dist/export/panel.d.ts.map +1 -1
  48. package/dist/export/panel.js +165 -94
  49. package/dist/export/panel.js.map +1 -1
  50. package/dist/export/panel.test.js +15 -8
  51. package/dist/export/panel.test.js.map +1 -1
  52. package/dist/export/share.d.ts.map +1 -1
  53. package/dist/export/share.js +6 -28
  54. package/dist/export/share.js.map +1 -1
  55. package/dist/i18n/en.d.ts.map +1 -1
  56. package/dist/i18n/en.js +87 -11
  57. package/dist/i18n/en.js.map +1 -1
  58. package/dist/i18n/i18n.test.js +25 -0
  59. package/dist/i18n/i18n.test.js.map +1 -1
  60. package/dist/i18n/index.d.ts +15 -0
  61. package/dist/i18n/index.d.ts.map +1 -1
  62. package/dist/i18n/index.js +20 -0
  63. package/dist/i18n/index.js.map +1 -1
  64. package/dist/i18n/labels.d.ts +12 -0
  65. package/dist/i18n/labels.d.ts.map +1 -0
  66. package/dist/i18n/labels.js +25 -0
  67. package/dist/i18n/labels.js.map +1 -0
  68. package/dist/metrics/index.d.ts.map +1 -1
  69. package/dist/metrics/index.js +3 -1
  70. package/dist/metrics/index.js.map +1 -1
  71. package/dist/node-version.d.ts +12 -0
  72. package/dist/node-version.d.ts.map +1 -1
  73. package/dist/node-version.js +17 -0
  74. package/dist/node-version.js.map +1 -1
  75. package/dist/node-version.test.js +18 -0
  76. package/dist/node-version.test.js.map +1 -1
  77. package/dist/pricing/catalog.d.ts.map +1 -1
  78. package/dist/pricing/catalog.js +23 -0
  79. package/dist/pricing/catalog.js.map +1 -1
  80. package/dist/pricing/cost.test.js +26 -0
  81. package/dist/pricing/cost.test.js.map +1 -1
  82. package/dist/publish.d.ts +6 -0
  83. package/dist/publish.d.ts.map +1 -1
  84. package/dist/publish.js +5 -2
  85. package/dist/publish.js.map +1 -1
  86. package/dist/server.d.ts.map +1 -1
  87. package/dist/server.js +35 -16
  88. package/dist/server.js.map +1 -1
  89. package/dist/server.test.js +57 -0
  90. package/dist/server.test.js.map +1 -1
  91. package/dist/watchers/agents.d.ts +37 -0
  92. package/dist/watchers/agents.d.ts.map +1 -0
  93. package/dist/watchers/agents.js +30 -0
  94. package/dist/watchers/agents.js.map +1 -0
  95. package/dist/watchers/agents.test.d.ts +2 -0
  96. package/dist/watchers/agents.test.d.ts.map +1 -0
  97. package/dist/watchers/agents.test.js +94 -0
  98. package/dist/watchers/agents.test.js.map +1 -0
  99. package/dist/watchers/codex.d.ts +3 -0
  100. package/dist/watchers/codex.d.ts.map +1 -0
  101. package/dist/watchers/codex.js +263 -0
  102. package/dist/watchers/codex.js.map +1 -0
  103. package/dist/watchers/codex.test.d.ts +2 -0
  104. package/dist/watchers/codex.test.d.ts.map +1 -0
  105. package/dist/watchers/codex.test.js +207 -0
  106. package/dist/watchers/codex.test.js.map +1 -0
  107. package/node_modules/@estela/shared/dist/types.d.ts +8 -0
  108. package/node_modules/@estela/shared/dist/types.d.ts.map +1 -1
  109. package/node_modules/@estela/shared/dist/types.js.map +1 -1
  110. package/package.json +1 -1
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const strict_1 = __importDefault(require("node:assert/strict"));
40
+ const node_child_process_1 = require("node:child_process");
41
+ const node_fs_1 = require("node:fs");
42
+ const node_os_1 = require("node:os");
43
+ const node_path_1 = require("node:path");
44
+ const node_test_1 = require("node:test");
45
+ const schema_js_1 = require("./db/schema.js");
46
+ const store = __importStar(require("./db/store.js"));
47
+ /**
48
+ * El idioma de los documentos, ejecutando la CLI de verdad.
49
+ *
50
+ * Lo que se prueba aquí es lo que ve una persona: qué idioma sale en el fichero
51
+ * que le manda a su cliente, cuándo se le avisa de dónde fijarlo, y que volver a
52
+ * ejecutar `client add` para cambiar un correo no le cambie los PDFs de idioma
53
+ * sin decirle nada. Ver `documentLang` en i18n/index.ts.
54
+ */
55
+ const BIN = (0, node_path_1.join)(__dirname, "bin.js");
56
+ function estela(db, lang, ...args) {
57
+ const r = (0, node_child_process_1.spawnSync)(process.execPath, [BIN, ...args, "--db", db], {
58
+ encoding: "utf8",
59
+ env: { ...process.env, ESTELA_LANG: lang, NO_COLOR: "1" },
60
+ });
61
+ return { code: r.status, out: `${r.stdout}${r.stderr}` };
62
+ }
63
+ /** Una base con un cliente, un proyecto con tarifa y un bloque facturable. */
64
+ function preparar(language) {
65
+ const dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "estela-idioma-"));
66
+ const path = (0, node_path_1.join)(dir, "estela.db");
67
+ const db = (0, schema_js_1.openDatabase)(path);
68
+ try {
69
+ store.upsertClient(db, {
70
+ id: "acme", name: "ACME", currency: "EUR", taxId: "B-1", email: "ap@acme.test",
71
+ ...(language ? { language } : {}),
72
+ });
73
+ store.upsertProject(db, {
74
+ id: "p", clientId: "acme", name: "Web", repoPaths: [],
75
+ billable: true, roundingMinutes: 0, aiCostPolicy: "absorbed", kind: "client",
76
+ });
77
+ store.addRatePeriod(db, {
78
+ projectId: "p", hourlyRate: { amount: 5000, currency: "EUR" },
79
+ effectiveFrom: new Date("2026-01-01T00:00:00Z"), effectiveTo: null,
80
+ });
81
+ const startedAt = new Date("2026-08-10T10:00:00Z");
82
+ store.saveTimeEntry(db, {
83
+ id: "e1", projectId: "p", startedAt,
84
+ endedAt: new Date(startedAt.getTime() + 3600_000), seconds: 3600,
85
+ description: "Trabajo", billable: true, invoiceId: null,
86
+ aiCost: { microUsd: 0 }, agentSeconds: 3600, commitHashes: [], agents: [],
87
+ source: "agent", kind: "development", branch: "main",
88
+ });
89
+ }
90
+ finally {
91
+ db.close();
92
+ }
93
+ return { db: path, dir };
94
+ }
95
+ const htmlLang = (file) => /<html lang="(\w+)"/.exec((0, node_fs_1.readFileSync)(file, "utf8"))[1];
96
+ // ── share: el documento sale en el idioma del cliente ───────────────────
97
+ (0, node_test_1.test)("sin idioma fijado, el informe sale en el de la terminal y avisa dónde fijarlo", () => {
98
+ const { db, dir } = preparar();
99
+ const out = (0, node_path_1.join)(dir, "r.html");
100
+ const es = estela(db, "es", "share", "--project", "p", "--out", out);
101
+ strict_1.default.equal(es.code, 0, es.out);
102
+ strict_1.default.equal(htmlLang(out), "es");
103
+ strict_1.default.match(es.out, /Sale en español, el idioma de tu terminal/);
104
+ // El comando viene ya escrito, con los datos del cliente: `client add` reescribe
105
+ // la ficha entera y teclearlo de memoria es perder el NIF sin querer.
106
+ strict_1.default.match(es.out, /estela client add --id acme --name "ACME" --currency EUR --tax-id "B-1" --email "ap@acme.test" --language <es\|en>/);
107
+ const en = estela(db, "en", "share", "--project", "p", "--out", out);
108
+ strict_1.default.equal(htmlLang(out), "en");
109
+ strict_1.default.match(en.out, /It comes out in English, your terminal's language/);
110
+ });
111
+ (0, node_test_1.test)("el idioma del cliente manda sobre el de la terminal, y no hay aviso", () => {
112
+ // Es el caso para el que existe todo esto: terminal en inglés, cliente en español.
113
+ const { db, dir } = preparar("es");
114
+ const out = (0, node_path_1.join)(dir, "r.html");
115
+ const r = estela(db, "en", "share", "--project", "p", "--out", out);
116
+ strict_1.default.equal(r.code, 0, r.out);
117
+ strict_1.default.equal(htmlLang(out), "es", "el documento sale en el idioma del cliente");
118
+ strict_1.default.doesNotMatch(r.out, /It comes out in|To set it/, "ya está decidido: no se avisa");
119
+ strict_1.default.match(r.out, /Report:/, "lo que imprime la terminal sigue en el idioma de la terminal");
120
+ });
121
+ (0, node_test_1.test)("--lang en el comando manda sobre todo", () => {
122
+ const { db, dir } = preparar("es");
123
+ const out = (0, node_path_1.join)(dir, "r.html");
124
+ const r = estela(db, "es", "share", "--project", "p", "--out", out, "--lang", "en");
125
+ strict_1.default.equal(r.code, 0, r.out);
126
+ strict_1.default.equal(htmlLang(out), "en");
127
+ strict_1.default.doesNotMatch(r.out, /To set it|Para fijarlo/);
128
+ });
129
+ // ── export y report ─────────────────────────────────────────────────────
130
+ (0, node_test_1.test)("el CSV sale en el idioma del cliente; por la salida estándar no lleva ningún aviso dentro", () => {
131
+ const { db, dir } = preparar("en");
132
+ const aFichero = estela(db, "es", "export", "--project", "p", "--out", (0, node_path_1.join)(dir, "h.csv"));
133
+ strict_1.default.equal(aFichero.code, 0, aFichero.out);
134
+ strict_1.default.ok((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, "h.csv"), "utf8").includes("date,start,end,hours"));
135
+ // Sin idioma fijado y por stdout: el aviso se colaría dentro del CSV.
136
+ const sin = preparar();
137
+ const porStdout = estela(sin.db, "es", "export", "--project", "p");
138
+ strict_1.default.equal(porStdout.code, 0, porStdout.out);
139
+ strict_1.default.doesNotMatch(porStdout.out, /Para fijarlo|Sale en/);
140
+ strict_1.default.ok(porStdout.out.replace("", "").startsWith("fecha,inicio"));
141
+ });
142
+ (0, node_test_1.test)("el PDF del informe de horas sale en el idioma del cliente", () => {
143
+ const en = preparar("en");
144
+ const pdf = (0, node_path_1.join)(en.dir, "f.pdf");
145
+ const r = estela(en.db, "es", "report", "--project", "p", "--cutoff", "2026-08-31", "--dry-run", "--pdf", pdf);
146
+ strict_1.default.equal(r.code, 0, r.out);
147
+ const texto = (0, node_fs_1.readFileSync)(pdf).toString("latin1");
148
+ strict_1.default.ok(texto.includes("(HOURS REPORT)"));
149
+ strict_1.default.ok(!texto.includes("INFORME DE HORAS"));
150
+ const es = preparar("es");
151
+ const pdfEs = (0, node_path_1.join)(es.dir, "f.pdf");
152
+ estela(es.db, "en", "report", "--project", "p", "--cutoff", "2026-08-31", "--dry-run", "--pdf", pdfEs);
153
+ strict_1.default.ok((0, node_fs_1.readFileSync)(pdfEs).toString("latin1").includes("(INFORME DE HORAS)"));
154
+ });
155
+ // ── client add ──────────────────────────────────────────────────────────
156
+ const idiomaEnBase = (db) => {
157
+ const conn = (0, schema_js_1.openDatabase)(db);
158
+ try {
159
+ return store.getClient(conn, "acme")?.language;
160
+ }
161
+ finally {
162
+ conn.close();
163
+ }
164
+ };
165
+ (0, node_test_1.test)("client add --language lo fija, volver a ejecutarlo sin él lo conserva, y auto lo quita", () => {
166
+ const { db } = preparar();
167
+ const fija = estela(db, "es", "client", "add", "--id", "acme", "--name", "ACME", "--currency", "EUR", "--language", "en");
168
+ strict_1.default.equal(fija.code, 0, fija.out);
169
+ strict_1.default.match(fija.out, /Sus documentos salen en inglés/);
170
+ strict_1.default.equal(idiomaEnBase(db), "en");
171
+ // Cambiar un correo no puede cambiar el idioma de los PDFs sin avisar.
172
+ estela(db, "es", "client", "add", "--id", "acme", "--name", "ACME", "--currency", "EUR", "--email", "nuevo@acme.test");
173
+ strict_1.default.equal(idiomaEnBase(db), "en", "sin --language se conserva el que había");
174
+ estela(db, "es", "client", "add", "--id", "acme", "--name", "ACME", "--currency", "EUR", "--language", "auto");
175
+ strict_1.default.equal(idiomaEnBase(db), undefined, "auto vuelve a seguir la terminal");
176
+ });
177
+ (0, node_test_1.test)("client add rechaza un idioma que no existe, sin guardar nada", () => {
178
+ const { db } = preparar();
179
+ const r = estela(db, "es", "client", "add", "--id", "acme", "--name", "Otro", "--currency", "EUR", "--language", "fr");
180
+ strict_1.default.notEqual(r.code, 0);
181
+ strict_1.default.match(r.out, /--language admite es, en o auto, y recibí "fr"/);
182
+ const conn = (0, schema_js_1.openDatabase)(db);
183
+ try {
184
+ strict_1.default.equal(store.getClient(conn, "acme").name, "ACME", "no se tocó la ficha");
185
+ }
186
+ finally {
187
+ conn.close();
188
+ }
189
+ });
190
+ //# sourceMappingURL=cli-idioma.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-idioma.test.js","sourceRoot":"","sources":["../src/cli-idioma.test.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAuCA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AA0kDpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAuCA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAoqDpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -67,10 +67,11 @@ const auth_js_1 = require("./cloud/auth.js");
67
67
  const client_js_1 = require("./cloud/client.js");
68
68
  const cost_js_1 = require("./pricing/cost.js");
69
69
  Object.defineProperty(exports, "breakdownOfTurn", { enumerable: true, get: function () { return cost_js_1.breakdownOfTurn; } });
70
- const claude_js_1 = require("./watchers/claude.js");
70
+ const agents_js_1 = require("./watchers/agents.js");
71
71
  const git_js_1 = require("./watchers/git.js");
72
72
  const identity_js_1 = require("./watchers/identity.js");
73
73
  const index_js_1 = require("./i18n/index.js");
74
+ const labels_js_1 = require("./i18n/labels.js");
74
75
  const HELP_ES = `
75
76
  estela — registro de horas para desarrollo asistido por IA
76
77
 
@@ -83,10 +84,17 @@ estela — registro de horas para desarrollo asistido por IA
83
84
 
84
85
  estela client add --id <id> --name <nombre> --currency <EUR|USD|...>
85
86
  [--tax-id <NIF/CIF>] [--email <a@b.com>]
87
+ [--language es|en|auto]
86
88
  --tax-id y --email son los datos del cliente que saldrán impresos en
87
89
  el informe de "estela report". Repetir el comando con el mismo --id
88
90
  reescribe la ficha entera: vuelve a pasar todo lo que quieras
89
91
  conservar, lo que omitas se queda vacío.
92
+ --language es el idioma de lo que recibe ESE cliente: el PDF, el CSV,
93
+ el informe y el panel compartido. Es del cliente y no de tu terminal:
94
+ puedes usar Estela en español y facturarle a una empresa en inglés.
95
+ Sin --language se usa el de tu terminal (y se te avisa). Es lo único
96
+ que NO se borra al repetir el comando; "auto" lo quita a propósito.
97
+ Un "--lang en" suelto en cualquier comando manda sobre todo.
90
98
  estela project add --id <id> --client <id> --name <nombre> --repo <ruta>
91
99
  [--rounding <min>] [--ai-cost absorbed|passthrough]
92
100
  estela project close --project <id>
@@ -210,10 +218,17 @@ estela — time tracking for AI-assisted development
210
218
 
211
219
  estela client add --id <id> --name <name> --currency <EUR|USD|...>
212
220
  [--tax-id <tax ID>] [--email <a@b.com>]
221
+ [--language es|en|auto]
213
222
  --tax-id and --email are the client details printed on the report
214
223
  from "estela report". Running the command again with the same --id
215
224
  rewrites the whole record: pass everything you want to keep again,
216
225
  anything you leave out is cleared.
226
+ --language is the language of what THAT client receives: the PDF, the
227
+ CSV, the report and the shared panel. It belongs to the client, not to
228
+ your terminal: you can use Estela in Spanish and bill a company in
229
+ English. Without --language your terminal's is used (and you're told).
230
+ It's the one thing that is NOT cleared when you run the command again;
231
+ "auto" removes it on purpose. A bare "--lang en" on any command wins.
217
232
  estela project add --id <id> --client <id> --name <name> --repo <path>
218
233
  [--rounding <min>] [--ai-cost absorbed|passthrough]
219
234
  estela project close --project <id>
@@ -360,20 +375,34 @@ function required(args, key) {
360
375
  }
361
376
  class UserError extends Error {
362
377
  }
378
+ /** El `--lang` escrito en el comando, si es uno que existe. Distinto del de la terminal. */
379
+ function explicitLang(args) {
380
+ const v = str(args, "lang");
381
+ return v === "es" || v === "en" ? v : undefined;
382
+ }
363
383
  /**
364
- * Nombre de un tipo de trabajo en el idioma de la terminal. WORK_KIND_LABELS
365
- * vive en `shared` y está en español; aquí se traduce sin tocar ese paquete.
384
+ * En qué idioma sale el documento que recibe este cliente: `--lang`, luego el
385
+ * idioma que se le fijó, y solo si nada de eso, el de la terminal.
366
386
  */
367
- function kindLabel(kind) {
368
- switch (kind) {
369
- case "development": return (0, index_js_1.tr) `Desarrollo`;
370
- case "meeting": return (0, index_js_1.tr) `Reunión`;
371
- case "research": return (0, index_js_1.tr) `Investigación`;
372
- case "review": return (0, index_js_1.tr) `Revisión`;
373
- case "travel": return (0, index_js_1.tr) `Desplazamiento`;
374
- case "support": return (0, index_js_1.tr) `Soporte`;
375
- case "other": return (0, index_js_1.tr) `Otro`;
376
- }
387
+ function docLangOf(args, client) {
388
+ return (0, index_js_1.documentLang)({ explicit: explicitLang(args), clientLanguage: client.language });
389
+ }
390
+ /**
391
+ * Si el idioma salió de la terminal, y no de una decisión, se dice — con el
392
+ * comando ya escrito. `client add` reescribe la ficha entera, así que pedirle a
393
+ * alguien que lo teclee de memoria es pedirle que pierda el NIF sin querer.
394
+ */
395
+ function hintDocLang(args, client, lang) {
396
+ if (client.language || explicitLang(args))
397
+ return;
398
+ const dice = lang === "es" ? (0, index_js_1.tr) `español` : (0, index_js_1.tr) `inglés`;
399
+ const cmd = `estela client add --id ${client.id} --name ${JSON.stringify(client.name)} ` +
400
+ `--currency ${client.currency}` +
401
+ (client.taxId ? ` --tax-id ${JSON.stringify(client.taxId)}` : "") +
402
+ (client.email ? ` --email ${JSON.stringify(client.email)}` : "") +
403
+ ` --language <es|en>`;
404
+ console.log((0, index_js_1.tr) `\n Sale en ${dice}, el idioma de tu terminal: a este cliente no se le fijó ninguno.`);
405
+ console.log((0, index_js_1.tr) ` Para fijarlo: ${cmd}`);
377
406
  }
378
407
  // ---------------------------------------------------------------------------
379
408
  /**
@@ -447,13 +476,45 @@ async function cmdLogout(dbPath) {
447
476
  * Dos minutos, sin preguntas y sin cuenta. Si al terminar esa persona no ve
448
477
  * algo cierto sobre su propio trabajo, no habrá una segunda ejecución.
449
478
  */
479
+ /** Nombre del agente tal y como lo llama quien lo usa, no como lo llamamos aquí. */
480
+ function agentName(agent) {
481
+ switch (agent) {
482
+ case "claude-code": return "Claude Code";
483
+ case "codex": return "Codex";
484
+ case "cursor": return "Cursor";
485
+ case "gemini-cli": return "Gemini CLI";
486
+ }
487
+ }
488
+ /**
489
+ * El parte de un escaneo.
490
+ *
491
+ * Se imprime por agente y no sumado: si Codex cambia de formato y empieza a
492
+ * dar registros malformados, un total conjunto lo diluye con la salud de
493
+ * Claude Code y el canario deja de avisar de nada.
494
+ */
495
+ function printScanReport(scan) {
496
+ const r = scan.report;
497
+ console.log(` ${agentName(scan.agent)}`);
498
+ console.log((0, index_js_1.tr) ` ${r.filesRead} archivos · ${r.recordsSeen} registros`);
499
+ console.log((0, index_js_1.tr) ` ${r.turnsAccepted} turnos aceptados`);
500
+ console.log((0, index_js_1.tr) ` ${r.duplicatesDropped} duplicados descartados` +
501
+ (r.turnsAccepted > 0
502
+ ? (0, index_js_1.tr) ` (${(r.duplicatesDropped / (r.turnsAccepted + r.duplicatesDropped) * 100).toFixed(0)}% de las filas)`
503
+ : ""));
504
+ console.log((0, index_js_1.tr) ` ${r.unknownRecords} registros internos ignorados · ${r.malformedRecords} malformados`);
505
+ if (r.producerVersions.length) {
506
+ console.log((0, index_js_1.tr) ` versiones: ${r.producerVersions.join(", ")}`);
507
+ }
508
+ for (const warning of r.warnings)
509
+ console.warn(` ⚠ ${agentName(scan.agent)}: ${warning}`);
510
+ }
450
511
  async function cmdSetup(args, dbPath) {
451
512
  const db = (0, schema_js_1.openDatabase)(dbPath);
452
513
  try {
453
514
  console.log((0, index_js_1.tr) `\nEstela\n`);
454
515
  console.log((0, index_js_1.tr) `Leyendo lo que tus agentes ya guardaron en disco…`);
455
- const scan = await (0, claude_js_1.scanClaudeCode)({});
456
- const turns = (0, claude_js_1.resolveScratchpads)(scan.turns);
516
+ const scans = await (0, agents_js_1.scanAgents)({});
517
+ const turns = (0, agents_js_1.allTurns)(scans);
457
518
  // Los repos no pueden salir solo de los transcripts: quien programa sin
458
519
  // IA, o con un agente que no deja rastro en ~/.claude (Cursor, Copilot),
459
520
  // tenía cero repos que mirar y `setup` se rendía ahí mismo — aunque la
@@ -469,19 +530,22 @@ async function cmdSetup(args, dbPath) {
469
530
  if (cwdRoot)
470
531
  repos.add(cwdRoot);
471
532
  if (turns.length === 0 && repos.size === 0) {
472
- console.log((0, index_js_1.tr) `\n No se han encontrado sesiones de Claude Code en ~/.claude, ni`);
473
- console.log((0, index_js_1.tr) ` un repositorio de Git en esta carpeta. Corre esto de nuevo desde`);
533
+ console.log((0, index_js_1.tr) `\n No se han encontrado sesiones de agentes en ~/.claude ni ~/.codex,`);
534
+ console.log((0, index_js_1.tr) ` ni un repositorio de Git en esta carpeta. Corre esto de nuevo desde`);
474
535
  console.log((0, index_js_1.tr) ` dentro de tu proyecto, o trabaja un rato con tu agente y vuelve.\n`);
475
536
  return;
476
537
  }
477
538
  if (turns.length > 0) {
478
539
  store.saveTurns(db, turns);
479
- store.logScan(db, "claude-code", scan.report);
480
- console.log((0, index_js_1.tr) ` ${scan.report.turnsAccepted} turnos · ` +
481
- (0, index_js_1.tr) `${scan.report.producerVersions.length} versiones de Claude Code`);
540
+ for (const scan of scans)
541
+ store.logScan(db, scan.agent, scan.report);
542
+ // Nombrar los agentes encontrados, en vez de contar versiones: es lo
543
+ // que quien acaba de instalar quiere confirmar —que ha visto lo suyo—.
544
+ const vistos = scans.filter((s) => s.report.turnsAccepted > 0).map((s) => agentName(s.agent));
545
+ console.log((0, index_js_1.tr) ` ${turns.length} turnos · ${vistos.join(", ")}`);
482
546
  }
483
547
  else {
484
- console.log((0, index_js_1.tr) ` No se han encontrado sesiones de Claude Code en ~/.claude —`);
548
+ console.log((0, index_js_1.tr) ` No se han encontrado sesiones de agentes en ~/.claude ni ~/.codex —`);
485
549
  console.log((0, index_js_1.tr) ` sin problema, se reconstruye igual desde tus commits de Git.`);
486
550
  }
487
551
  console.log((0, index_js_1.tr) `\nBuscando repositorios… (${repos.size})`);
@@ -536,29 +600,25 @@ async function cmdImport(args, dbPath) {
536
600
  const repoFilter = str(args, "repo");
537
601
  const repoPaths = repoFilter ? [(0, node_path_1.resolve)(repoFilter)] : undefined;
538
602
  console.log((0, index_js_1.tr) `Leyendo transcripts de agentes…`);
539
- const scan = await (0, claude_js_1.scanClaudeCode)({
603
+ const scans = await (0, agents_js_1.scanAgents)({
540
604
  ...(since ? { since } : {}),
541
605
  ...(repoPaths ? { repoPaths } : {}),
542
606
  });
543
- const report = scan.report;
544
607
  // Devolver al repositorio el trabajo hecho en scratchpads, o esas horas
545
608
  // quedan sin proyecto y desaparecen del parte.
546
- const turns = (0, claude_js_1.resolveScratchpads)(scan.turns);
547
- const rescued = turns.filter((t, i) => t.repoPath !== scan.turns[i].repoPath).length;
609
+ const turns = (0, agents_js_1.allTurns)(scans);
610
+ const rescued = scans.reduce((n, s) => n + s.scratchpadsResolved, 0);
548
611
  const savedTurns = store.saveTurns(db, turns);
549
- store.logScan(db, "claude-code", report);
550
- console.log((0, index_js_1.tr) ` ${report.filesRead} archivos · ${report.recordsSeen} registros`);
551
- console.log((0, index_js_1.tr) ` ${report.turnsAccepted} turnos aceptados · ${savedTurns} nuevos`);
552
- console.log((0, index_js_1.tr) ` ${report.duplicatesDropped} duplicados descartados` +
553
- (report.turnsAccepted > 0
554
- ? (0, index_js_1.tr) ` (${(report.duplicatesDropped / (report.turnsAccepted + report.duplicatesDropped) * 100).toFixed(0)}% de las filas)`
555
- : ""));
556
- console.log((0, index_js_1.tr) ` ${report.unknownRecords} registros internos ignorados · ${report.malformedRecords} malformados`);
557
- if (report.producerVersions.length) {
558
- console.log((0, index_js_1.tr) ` versiones de Claude Code: ${report.producerVersions.join(", ")}`);
559
- }
560
- for (const warning of report.warnings)
561
- console.warn(` ⚠ ${warning}`);
612
+ for (const scan of scans)
613
+ store.logScan(db, scan.agent, scan.report);
614
+ // Un bloque por agente, y solo de los que tienen algo. Quien no use Codex
615
+ // no debe leer cuatro líneas de ceros para enterarse de eso.
616
+ console.log((0, index_js_1.tr) ` ${savedTurns} turnos nuevos guardados`);
617
+ for (const scan of scans) {
618
+ if (scan.report.filesRead === 0)
619
+ continue;
620
+ printScanReport(scan);
621
+ }
562
622
  // Commits de cada repositorio que aparece en los transcripts, MÁS todo
563
623
  // repo ya vinculado a un proyecto aunque ningún transcript lo mencione
564
624
  // — si no, un proyecto que `setup` acaba de registrar desde el
@@ -680,14 +740,28 @@ function cmdClientAdd(args, dbPath) {
680
740
  const db = (0, schema_js_1.openDatabase)(dbPath);
681
741
  try {
682
742
  const currency = required(args, "currency").toUpperCase();
743
+ const id = required(args, "id");
744
+ // El idioma es la única parte de la ficha que NO se borra al repetir el
745
+ // comando sin ella: quien vuelve a ejecutar `client add` solo para cambiar un
746
+ // correo no espera que los PDFs de ese cliente cambien de idioma sin avisar.
747
+ // "auto" es la forma de quitarlo a propósito.
748
+ const asked = str(args, "language");
749
+ if (asked !== undefined && asked !== "es" && asked !== "en" && asked !== "auto") {
750
+ throw new UserError((0, index_js_1.tr) `--language admite es, en o auto, y recibí "${asked}".`);
751
+ }
752
+ const language = asked === "auto" ? undefined : (asked ?? store.getClient(db, id)?.language);
683
753
  store.upsertClient(db, {
684
- id: required(args, "id"),
754
+ id,
685
755
  name: required(args, "name"),
686
756
  currency,
687
757
  ...(str(args, "tax-id") ? { taxId: str(args, "tax-id") } : {}),
688
758
  ...(str(args, "email") ? { email: str(args, "email") } : {}),
759
+ ...(language ? { language } : {}),
689
760
  });
690
761
  console.log((0, index_js_1.tr) `Cliente "${required(args, "name")}" guardado. Factura en ${currency}.`);
762
+ if (language) {
763
+ console.log((0, index_js_1.tr) ` Sus documentos salen en ${language === "es" ? (0, index_js_1.tr) `español` : (0, index_js_1.tr) `inglés`}.`);
764
+ }
691
765
  }
692
766
  finally {
693
767
  db.close();
@@ -928,7 +1002,8 @@ function cmdShare(args, dbPath) {
928
1002
  const to = str(args, "to") ?? (0, shared_1.localDate)(new Date());
929
1003
  const from = str(args, "from") ?? to.slice(0, 8) + "01";
930
1004
  const rates = store.getRates(db, projectId);
931
- const html = (0, share_js_1.buildShareReport)({
1005
+ const lang = docLangOf(args, client);
1006
+ const html = (0, index_js_1.withLang)(lang, () => (0, share_js_1.buildShareReport)({
932
1007
  project, client,
933
1008
  entries: store.getTimeEntries(db, projectId),
934
1009
  from, to,
@@ -941,7 +1016,7 @@ function cmdShare(args, dbPath) {
941
1016
  commitsOf: (hashes) => hashes.length
942
1017
  ? db.prepare(`SELECT hash, subject FROM commits WHERE hash IN (${hashes.map(() => "?").join(",")})`).all(...hashes)
943
1018
  : [],
944
- });
1019
+ }));
945
1020
  const out = str(args, "out") ?? `informe-${projectId}-${to}.html`;
946
1021
  (0, node_fs_1.writeFileSync)(out, html, "utf8");
947
1022
  console.log((0, index_js_1.tr) `\nInforme: ${out}`);
@@ -951,6 +1026,7 @@ function cmdShare(args, dbPath) {
951
1026
  console.log((0, index_js_1.tr) `\n Incluye horas y commits. Sin importes (añade --with-amounts) y sin`);
952
1027
  console.log((0, index_js_1.tr) ` consumo de IA, que es tuyo mientras la pagues tú.`);
953
1028
  }
1029
+ hintDocLang(args, client, lang);
954
1030
  }
955
1031
  finally {
956
1032
  db.close();
@@ -991,7 +1067,7 @@ function cmdLog(args, dbPath) {
991
1067
  throw new UserError((0, index_js_1.tr) `Fecha inválida: ${day}`);
992
1068
  // A media mañana: no sabemos la hora real y fingir precisión sería peor.
993
1069
  const startedAt = new Date(`${day}T10:00:00`);
994
- const what = str(args, "what") ?? kindLabel(kind);
1070
+ const what = str(args, "what") ?? (0, labels_js_1.kindLabel)(kind);
995
1071
  const id = store.saveTimeEntry(db, {
996
1072
  projectId,
997
1073
  startedAt,
@@ -1012,7 +1088,7 @@ function cmdLog(args, dbPath) {
1012
1088
  const amount = rate
1013
1089
  ? ` · ${(0, shared_1.formatMoney)({ amount: Math.round((rate.amount * seconds) / 3600), currency: rate.currency })}`
1014
1090
  : "";
1015
- console.log(`\n${kindLabel(kind)} · ${(0, shared_1.formatDuration)(seconds)}${amount}`);
1091
+ console.log(`\n${(0, labels_js_1.kindLabel)(kind)} · ${(0, shared_1.formatDuration)(seconds)}${amount}`);
1016
1092
  console.log(` ${what}`);
1017
1093
  console.log(` ${project.name} · ${day}`);
1018
1094
  console.log((0, index_js_1.tr) `\n Registro manual: un import no lo va a tocar. (${id})`);
@@ -1057,6 +1133,7 @@ async function cmdPublish(args, dbPath) {
1057
1133
  ...(clientes?.length ? { clients: clientes } : {}),
1058
1134
  withAmounts: args.flags["with-amounts"] === true,
1059
1135
  includeTeam: args.flags["no-team"] !== true,
1136
+ ...(explicitLang(args) ? { language: explicitLang(args) } : {}),
1060
1137
  });
1061
1138
  }
1062
1139
  catch (error) {
@@ -1076,6 +1153,8 @@ async function cmdPublish(args, dbPath) {
1076
1153
  console.log((0, index_js_1.tr) ` El token es lo único que protege la página: quien tenga el enlace, entra.`);
1077
1154
  console.log((0, index_js_1.tr) ` Al volver a publicar desde esta máquina, este enlace se reusa solo.`);
1078
1155
  console.log((0, index_js_1.tr) ` Desde otra máquina, pasa --token ${result.token} o el cliente perderá su enlace.`);
1156
+ const owner = store.getClient(db, store.getProject(db, projectId).clientId);
1157
+ hintDocLang(args, owner, docLangOf(args, owner));
1079
1158
  }
1080
1159
  finally {
1081
1160
  db.close();
@@ -1441,11 +1520,15 @@ function cmdExport(args, dbPath) {
1441
1520
  throw new UserError((0, index_js_1.tr) `No existe el proyecto "${projectId}".`);
1442
1521
  const client = store.getClient(db, project.clientId);
1443
1522
  const rates = store.getRates(db, projectId);
1444
- const csv = (0, csv_js_1.timeEntriesToCsv)(store.getTimeEntries(db, projectId), project, client, (at) => (0, invoice_js_1.rateAt)(rates, projectId, at));
1523
+ const lang = docLangOf(args, client);
1524
+ const csv = (0, index_js_1.withLang)(lang, () => (0, csv_js_1.timeEntriesToCsv)(store.getTimeEntries(db, projectId), project, client, (at) => (0, invoice_js_1.rateAt)(rates, projectId, at)));
1445
1525
  const out = str(args, "out");
1446
1526
  if (out) {
1447
1527
  (0, node_fs_1.writeFileSync)(out, csv, "utf8");
1448
1528
  console.log((0, index_js_1.tr) `CSV: ${out}`);
1529
+ // Solo si va a un fichero: por la salida estándar el aviso se colaría
1530
+ // dentro del propio CSV.
1531
+ hintDocLang(args, client, lang);
1449
1532
  }
1450
1533
  else {
1451
1534
  process.stdout.write(csv);
@@ -1540,13 +1623,16 @@ function cmdInvoice(args, dbPath) {
1540
1623
  const amortized = subs.length
1541
1624
  ? (0, amortize_js_1.shareForProject)((0, amortize_js_1.amortize)(store.consumptionByProjectMonth(db), subs, store.totalConsumptionByMonth(db)), projectId, [...months])
1542
1625
  : null;
1543
- const invoice = (0, invoice_js_1.issueInvoice)({
1626
+ // Las líneas de un informe se congelan al emitirlo, así que se emiten ya en
1627
+ // el idioma del cliente: es el que verá en el PDF y en el CSV.
1628
+ const lang = docLangOf(args, client);
1629
+ const invoice = (0, index_js_1.withLang)(lang, () => (0, invoice_js_1.issueInvoice)({
1544
1630
  client, project,
1545
1631
  rates: store.getRates(db, projectId),
1546
1632
  entries, cutoffAt, number,
1547
1633
  ...(fx ? { usdFxRate: Number(fx) } : {}),
1548
1634
  ...(amortized ? { aiAmortized: amortized } : {}),
1549
- });
1635
+ }));
1550
1636
  console.log((0, invoice_js_1.renderInvoice)(invoice, client, project));
1551
1637
  // Coste real de IA del periodo: la cuota de tus suscripciones repartida
1552
1638
  // por consumo. Es lo que de verdad te costó, frente a la tarifa API.
@@ -1567,7 +1653,7 @@ function cmdInvoice(args, dbPath) {
1567
1653
  const pdfPath = str(args, "pdf");
1568
1654
  if (pdfPath) {
1569
1655
  const issuerName = str(args, "from-name");
1570
- const pdf = (0, invoice_pdf_js_1.invoiceToPdf)(invoice, client, project, {
1656
+ const pdf = (0, index_js_1.withLang)(lang, () => (0, invoice_pdf_js_1.invoiceToPdf)(invoice, client, project, {
1571
1657
  ...(issuerName ? {
1572
1658
  issuer: {
1573
1659
  name: issuerName,
@@ -1576,15 +1662,17 @@ function cmdInvoice(args, dbPath) {
1576
1662
  },
1577
1663
  } : {}),
1578
1664
  ...(invoice.aiAmortized ? { amortizedAiCost: invoice.aiAmortized } : {}),
1579
- });
1665
+ }));
1580
1666
  (0, node_fs_1.writeFileSync)(pdfPath, pdf);
1581
1667
  console.log((0, index_js_1.tr) `\nPDF: ${pdfPath} (${(pdf.length / 1024).toFixed(1)} KB, ${invoice.lines.length} conceptos)`);
1582
1668
  }
1583
1669
  const csvPath = str(args, "csv");
1584
1670
  if (csvPath) {
1585
- (0, node_fs_1.writeFileSync)(csvPath, (0, csv_js_1.invoiceToCsv)(invoice, client, project), "utf8");
1671
+ (0, node_fs_1.writeFileSync)(csvPath, (0, index_js_1.withLang)(lang, () => (0, csv_js_1.invoiceToCsv)(invoice, client, project)), "utf8");
1586
1672
  console.log((0, index_js_1.tr) `CSV: ${csvPath}`);
1587
1673
  }
1674
+ if (pdfPath || csvPath)
1675
+ hintDocLang(args, client, lang);
1588
1676
  if (dryRun) {
1589
1677
  console.log((0, index_js_1.tr) `\n[--dry-run] No se guardó nada. Repite sin --dry-run para emitirlo.`);
1590
1678
  return;