estela 0.1.28 → 0.1.32

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 (107) hide show
  1. package/README.md +53 -7
  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/cli-idioma.test.d.ts +2 -0
  17. package/dist/cli-idioma.test.d.ts.map +1 -0
  18. package/dist/cli-idioma.test.js +190 -0
  19. package/dist/cli-idioma.test.js.map +1 -0
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +154 -51
  22. package/dist/cli.js.map +1 -1
  23. package/dist/db/schema.d.ts.map +1 -1
  24. package/dist/db/schema.js +40 -2
  25. package/dist/db/schema.js.map +1 -1
  26. package/dist/db/schema.test.js +294 -4
  27. package/dist/db/schema.test.js.map +1 -1
  28. package/dist/db/store.d.ts +50 -0
  29. package/dist/db/store.d.ts.map +1 -1
  30. package/dist/db/store.js +151 -5
  31. package/dist/db/store.js.map +1 -1
  32. package/dist/export/csv.d.ts.map +1 -1
  33. package/dist/export/csv.js +49 -18
  34. package/dist/export/csv.js.map +1 -1
  35. package/dist/export/dates.d.ts +13 -0
  36. package/dist/export/dates.d.ts.map +1 -0
  37. package/dist/export/dates.js +29 -0
  38. package/dist/export/dates.js.map +1 -0
  39. package/dist/export/idioma.test.d.ts +2 -0
  40. package/dist/export/idioma.test.d.ts.map +1 -0
  41. package/dist/export/idioma.test.js +251 -0
  42. package/dist/export/idioma.test.js.map +1 -0
  43. package/dist/export/invoice-pdf.d.ts.map +1 -1
  44. package/dist/export/invoice-pdf.js +26 -22
  45. package/dist/export/invoice-pdf.js.map +1 -1
  46. package/dist/export/panel.d.ts.map +1 -1
  47. package/dist/export/panel.js +165 -94
  48. package/dist/export/panel.js.map +1 -1
  49. package/dist/export/panel.test.js +15 -8
  50. package/dist/export/panel.test.js.map +1 -1
  51. package/dist/export/share.d.ts.map +1 -1
  52. package/dist/export/share.js +6 -28
  53. package/dist/export/share.js.map +1 -1
  54. package/dist/i18n/en.d.ts.map +1 -1
  55. package/dist/i18n/en.js +90 -10
  56. package/dist/i18n/en.js.map +1 -1
  57. package/dist/i18n/i18n.test.js +25 -0
  58. package/dist/i18n/i18n.test.js.map +1 -1
  59. package/dist/i18n/index.d.ts +15 -0
  60. package/dist/i18n/index.d.ts.map +1 -1
  61. package/dist/i18n/index.js +20 -0
  62. package/dist/i18n/index.js.map +1 -1
  63. package/dist/i18n/labels.d.ts +12 -0
  64. package/dist/i18n/labels.d.ts.map +1 -0
  65. package/dist/i18n/labels.js +25 -0
  66. package/dist/i18n/labels.js.map +1 -0
  67. package/dist/metrics/index.d.ts.map +1 -1
  68. package/dist/metrics/index.js +3 -1
  69. package/dist/metrics/index.js.map +1 -1
  70. package/dist/pricing/catalog.d.ts.map +1 -1
  71. package/dist/pricing/catalog.js +23 -0
  72. package/dist/pricing/catalog.js.map +1 -1
  73. package/dist/pricing/cost.test.js +26 -0
  74. package/dist/pricing/cost.test.js.map +1 -1
  75. package/dist/publish.d.ts +6 -0
  76. package/dist/publish.d.ts.map +1 -1
  77. package/dist/publish.js +5 -2
  78. package/dist/publish.js.map +1 -1
  79. package/dist/server.d.ts.map +1 -1
  80. package/dist/server.js +141 -31
  81. package/dist/server.js.map +1 -1
  82. package/dist/server.test.js +87 -0
  83. package/dist/server.test.js.map +1 -1
  84. package/dist/watchers/agents.d.ts +37 -0
  85. package/dist/watchers/agents.d.ts.map +1 -0
  86. package/dist/watchers/agents.js +30 -0
  87. package/dist/watchers/agents.js.map +1 -0
  88. package/dist/watchers/agents.test.d.ts +2 -0
  89. package/dist/watchers/agents.test.d.ts.map +1 -0
  90. package/dist/watchers/agents.test.js +94 -0
  91. package/dist/watchers/agents.test.js.map +1 -0
  92. package/dist/watchers/codex.d.ts +3 -0
  93. package/dist/watchers/codex.d.ts.map +1 -0
  94. package/dist/watchers/codex.js +263 -0
  95. package/dist/watchers/codex.js.map +1 -0
  96. package/dist/watchers/codex.test.d.ts +2 -0
  97. package/dist/watchers/codex.test.d.ts.map +1 -0
  98. package/dist/watchers/codex.test.js +207 -0
  99. package/dist/watchers/codex.test.js.map +1 -0
  100. package/node_modules/@estela/shared/dist/types.d.ts +8 -0
  101. package/node_modules/@estela/shared/dist/types.d.ts.map +1 -1
  102. package/node_modules/@estela/shared/dist/types.js.map +1 -1
  103. package/package.json +1 -1
  104. package/web/app.css +32 -0
  105. package/web/app.js +92 -8
  106. package/web/i18n.js +30 -6
  107. package/web/index.html +3 -3
@@ -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;AAmrDpD,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
  /**
@@ -382,9 +411,24 @@ function kindLabel(kind) {
382
411
  * Quien instala la CLI no tiene forma de arrancar el servidor: hasta ahora
383
412
  * vivía en un script del repositorio, que solo existe si te lo has clonado.
384
413
  */
414
+ /**
415
+ * Arranca el panel convirtiendo el fallo en algo legible.
416
+ *
417
+ * `startServer` ya rechaza con una frase en vez de un errno, pero si llega
418
+ * como Error normal, el catch del final imprime el objeto entero con su
419
+ * traza. Como UserError, sale la frase sola.
420
+ */
421
+ async function arrancarPanel(opciones) {
422
+ try {
423
+ return await (0, server_js_1.startServer)(opciones);
424
+ }
425
+ catch (error) {
426
+ throw new UserError(error instanceof Error ? error.message : String(error));
427
+ }
428
+ }
385
429
  async function cmdWeb(args, dbPath) {
386
430
  const port = Number(str(args, "port") ?? 4319);
387
- const url = await (0, server_js_1.startServer)({ port, dbPath });
431
+ const url = await arrancarPanel({ port, dbPath });
388
432
  console.log((0, index_js_1.tr) `\n Estela ${url}`);
389
433
  console.log((0, index_js_1.tr) ` Datos: ${dbPath}`);
390
434
  console.log((0, index_js_1.tr) `\n Ctrl+C para parar.\n`);
@@ -410,7 +454,7 @@ async function cmdDemo(args, dbPath) {
410
454
  db.close();
411
455
  }
412
456
  const port = Number(str(args, "port") ?? 4320);
413
- const url = await (0, server_js_1.startServer)({ port, dbPath: rutaDemo, autoImportMinutes: 0, demo: true });
457
+ const url = await arrancarPanel({ port, dbPath: rutaDemo, autoImportMinutes: 0, demo: true });
414
458
  console.log((0, index_js_1.tr) `\n Estela — demo con datos inventados ${url}`);
415
459
  console.log((0, index_js_1.tr) ` No es tu trabajo: no se ha tocado tu base ni se ha leído nada tuyo.`);
416
460
  console.log((0, index_js_1.tr) ` Para el tuyo de verdad: estela setup`);
@@ -447,13 +491,45 @@ async function cmdLogout(dbPath) {
447
491
  * Dos minutos, sin preguntas y sin cuenta. Si al terminar esa persona no ve
448
492
  * algo cierto sobre su propio trabajo, no habrá una segunda ejecución.
449
493
  */
494
+ /** Nombre del agente tal y como lo llama quien lo usa, no como lo llamamos aquí. */
495
+ function agentName(agent) {
496
+ switch (agent) {
497
+ case "claude-code": return "Claude Code";
498
+ case "codex": return "Codex";
499
+ case "cursor": return "Cursor";
500
+ case "gemini-cli": return "Gemini CLI";
501
+ }
502
+ }
503
+ /**
504
+ * El parte de un escaneo.
505
+ *
506
+ * Se imprime por agente y no sumado: si Codex cambia de formato y empieza a
507
+ * dar registros malformados, un total conjunto lo diluye con la salud de
508
+ * Claude Code y el canario deja de avisar de nada.
509
+ */
510
+ function printScanReport(scan) {
511
+ const r = scan.report;
512
+ console.log(` ${agentName(scan.agent)}`);
513
+ console.log((0, index_js_1.tr) ` ${r.filesRead} archivos · ${r.recordsSeen} registros`);
514
+ console.log((0, index_js_1.tr) ` ${r.turnsAccepted} turnos aceptados`);
515
+ console.log((0, index_js_1.tr) ` ${r.duplicatesDropped} duplicados descartados` +
516
+ (r.turnsAccepted > 0
517
+ ? (0, index_js_1.tr) ` (${(r.duplicatesDropped / (r.turnsAccepted + r.duplicatesDropped) * 100).toFixed(0)}% de las filas)`
518
+ : ""));
519
+ console.log((0, index_js_1.tr) ` ${r.unknownRecords} registros internos ignorados · ${r.malformedRecords} malformados`);
520
+ if (r.producerVersions.length) {
521
+ console.log((0, index_js_1.tr) ` versiones: ${r.producerVersions.join(", ")}`);
522
+ }
523
+ for (const warning of r.warnings)
524
+ console.warn(` ⚠ ${agentName(scan.agent)}: ${warning}`);
525
+ }
450
526
  async function cmdSetup(args, dbPath) {
451
527
  const db = (0, schema_js_1.openDatabase)(dbPath);
452
528
  try {
453
529
  console.log((0, index_js_1.tr) `\nEstela\n`);
454
530
  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);
531
+ const scans = await (0, agents_js_1.scanAgents)({});
532
+ const turns = (0, agents_js_1.allTurns)(scans);
457
533
  // Los repos no pueden salir solo de los transcripts: quien programa sin
458
534
  // IA, o con un agente que no deja rastro en ~/.claude (Cursor, Copilot),
459
535
  // tenía cero repos que mirar y `setup` se rendía ahí mismo — aunque la
@@ -469,19 +545,22 @@ async function cmdSetup(args, dbPath) {
469
545
  if (cwdRoot)
470
546
  repos.add(cwdRoot);
471
547
  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`);
548
+ console.log((0, index_js_1.tr) `\n No se han encontrado sesiones de agentes en ~/.claude ni ~/.codex,`);
549
+ console.log((0, index_js_1.tr) ` ni un repositorio de Git en esta carpeta. Corre esto de nuevo desde`);
474
550
  console.log((0, index_js_1.tr) ` dentro de tu proyecto, o trabaja un rato con tu agente y vuelve.\n`);
475
551
  return;
476
552
  }
477
553
  if (turns.length > 0) {
478
554
  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`);
555
+ for (const scan of scans)
556
+ store.logScan(db, scan.agent, scan.report);
557
+ // Nombrar los agentes encontrados, en vez de contar versiones: es lo
558
+ // que quien acaba de instalar quiere confirmar —que ha visto lo suyo—.
559
+ const vistos = scans.filter((s) => s.report.turnsAccepted > 0).map((s) => agentName(s.agent));
560
+ console.log((0, index_js_1.tr) ` ${turns.length} turnos · ${vistos.join(", ")}`);
482
561
  }
483
562
  else {
484
- console.log((0, index_js_1.tr) ` No se han encontrado sesiones de Claude Code en ~/.claude —`);
563
+ console.log((0, index_js_1.tr) ` No se han encontrado sesiones de agentes en ~/.claude ni ~/.codex —`);
485
564
  console.log((0, index_js_1.tr) ` sin problema, se reconstruye igual desde tus commits de Git.`);
486
565
  }
487
566
  console.log((0, index_js_1.tr) `\nBuscando repositorios… (${repos.size})`);
@@ -536,29 +615,25 @@ async function cmdImport(args, dbPath) {
536
615
  const repoFilter = str(args, "repo");
537
616
  const repoPaths = repoFilter ? [(0, node_path_1.resolve)(repoFilter)] : undefined;
538
617
  console.log((0, index_js_1.tr) `Leyendo transcripts de agentes…`);
539
- const scan = await (0, claude_js_1.scanClaudeCode)({
618
+ const scans = await (0, agents_js_1.scanAgents)({
540
619
  ...(since ? { since } : {}),
541
620
  ...(repoPaths ? { repoPaths } : {}),
542
621
  });
543
- const report = scan.report;
544
622
  // Devolver al repositorio el trabajo hecho en scratchpads, o esas horas
545
623
  // 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;
624
+ const turns = (0, agents_js_1.allTurns)(scans);
625
+ const rescued = scans.reduce((n, s) => n + s.scratchpadsResolved, 0);
548
626
  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}`);
627
+ for (const scan of scans)
628
+ store.logScan(db, scan.agent, scan.report);
629
+ // Un bloque por agente, y solo de los que tienen algo. Quien no use Codex
630
+ // no debe leer cuatro líneas de ceros para enterarse de eso.
631
+ console.log((0, index_js_1.tr) ` ${savedTurns} turnos nuevos guardados`);
632
+ for (const scan of scans) {
633
+ if (scan.report.filesRead === 0)
634
+ continue;
635
+ printScanReport(scan);
636
+ }
562
637
  // Commits de cada repositorio que aparece en los transcripts, MÁS todo
563
638
  // repo ya vinculado a un proyecto aunque ningún transcript lo mencione
564
639
  // — si no, un proyecto que `setup` acaba de registrar desde el
@@ -680,14 +755,28 @@ function cmdClientAdd(args, dbPath) {
680
755
  const db = (0, schema_js_1.openDatabase)(dbPath);
681
756
  try {
682
757
  const currency = required(args, "currency").toUpperCase();
758
+ const id = required(args, "id");
759
+ // El idioma es la única parte de la ficha que NO se borra al repetir el
760
+ // comando sin ella: quien vuelve a ejecutar `client add` solo para cambiar un
761
+ // correo no espera que los PDFs de ese cliente cambien de idioma sin avisar.
762
+ // "auto" es la forma de quitarlo a propósito.
763
+ const asked = str(args, "language");
764
+ if (asked !== undefined && asked !== "es" && asked !== "en" && asked !== "auto") {
765
+ throw new UserError((0, index_js_1.tr) `--language admite es, en o auto, y recibí "${asked}".`);
766
+ }
767
+ const language = asked === "auto" ? undefined : (asked ?? store.getClient(db, id)?.language);
683
768
  store.upsertClient(db, {
684
- id: required(args, "id"),
769
+ id,
685
770
  name: required(args, "name"),
686
771
  currency,
687
772
  ...(str(args, "tax-id") ? { taxId: str(args, "tax-id") } : {}),
688
773
  ...(str(args, "email") ? { email: str(args, "email") } : {}),
774
+ ...(language ? { language } : {}),
689
775
  });
690
776
  console.log((0, index_js_1.tr) `Cliente "${required(args, "name")}" guardado. Factura en ${currency}.`);
777
+ if (language) {
778
+ 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`}.`);
779
+ }
691
780
  }
692
781
  finally {
693
782
  db.close();
@@ -928,7 +1017,8 @@ function cmdShare(args, dbPath) {
928
1017
  const to = str(args, "to") ?? (0, shared_1.localDate)(new Date());
929
1018
  const from = str(args, "from") ?? to.slice(0, 8) + "01";
930
1019
  const rates = store.getRates(db, projectId);
931
- const html = (0, share_js_1.buildShareReport)({
1020
+ const lang = docLangOf(args, client);
1021
+ const html = (0, index_js_1.withLang)(lang, () => (0, share_js_1.buildShareReport)({
932
1022
  project, client,
933
1023
  entries: store.getTimeEntries(db, projectId),
934
1024
  from, to,
@@ -941,7 +1031,7 @@ function cmdShare(args, dbPath) {
941
1031
  commitsOf: (hashes) => hashes.length
942
1032
  ? db.prepare(`SELECT hash, subject FROM commits WHERE hash IN (${hashes.map(() => "?").join(",")})`).all(...hashes)
943
1033
  : [],
944
- });
1034
+ }));
945
1035
  const out = str(args, "out") ?? `informe-${projectId}-${to}.html`;
946
1036
  (0, node_fs_1.writeFileSync)(out, html, "utf8");
947
1037
  console.log((0, index_js_1.tr) `\nInforme: ${out}`);
@@ -951,6 +1041,7 @@ function cmdShare(args, dbPath) {
951
1041
  console.log((0, index_js_1.tr) `\n Incluye horas y commits. Sin importes (añade --with-amounts) y sin`);
952
1042
  console.log((0, index_js_1.tr) ` consumo de IA, que es tuyo mientras la pagues tú.`);
953
1043
  }
1044
+ hintDocLang(args, client, lang);
954
1045
  }
955
1046
  finally {
956
1047
  db.close();
@@ -991,7 +1082,7 @@ function cmdLog(args, dbPath) {
991
1082
  throw new UserError((0, index_js_1.tr) `Fecha inválida: ${day}`);
992
1083
  // A media mañana: no sabemos la hora real y fingir precisión sería peor.
993
1084
  const startedAt = new Date(`${day}T10:00:00`);
994
- const what = str(args, "what") ?? kindLabel(kind);
1085
+ const what = str(args, "what") ?? (0, labels_js_1.kindLabel)(kind);
995
1086
  const id = store.saveTimeEntry(db, {
996
1087
  projectId,
997
1088
  startedAt,
@@ -1012,7 +1103,7 @@ function cmdLog(args, dbPath) {
1012
1103
  const amount = rate
1013
1104
  ? ` · ${(0, shared_1.formatMoney)({ amount: Math.round((rate.amount * seconds) / 3600), currency: rate.currency })}`
1014
1105
  : "";
1015
- console.log(`\n${kindLabel(kind)} · ${(0, shared_1.formatDuration)(seconds)}${amount}`);
1106
+ console.log(`\n${(0, labels_js_1.kindLabel)(kind)} · ${(0, shared_1.formatDuration)(seconds)}${amount}`);
1016
1107
  console.log(` ${what}`);
1017
1108
  console.log(` ${project.name} · ${day}`);
1018
1109
  console.log((0, index_js_1.tr) `\n Registro manual: un import no lo va a tocar. (${id})`);
@@ -1057,6 +1148,7 @@ async function cmdPublish(args, dbPath) {
1057
1148
  ...(clientes?.length ? { clients: clientes } : {}),
1058
1149
  withAmounts: args.flags["with-amounts"] === true,
1059
1150
  includeTeam: args.flags["no-team"] !== true,
1151
+ ...(explicitLang(args) ? { language: explicitLang(args) } : {}),
1060
1152
  });
1061
1153
  }
1062
1154
  catch (error) {
@@ -1076,6 +1168,8 @@ async function cmdPublish(args, dbPath) {
1076
1168
  console.log((0, index_js_1.tr) ` El token es lo único que protege la página: quien tenga el enlace, entra.`);
1077
1169
  console.log((0, index_js_1.tr) ` Al volver a publicar desde esta máquina, este enlace se reusa solo.`);
1078
1170
  console.log((0, index_js_1.tr) ` Desde otra máquina, pasa --token ${result.token} o el cliente perderá su enlace.`);
1171
+ const owner = store.getClient(db, store.getProject(db, projectId).clientId);
1172
+ hintDocLang(args, owner, docLangOf(args, owner));
1079
1173
  }
1080
1174
  finally {
1081
1175
  db.close();
@@ -1441,11 +1535,15 @@ function cmdExport(args, dbPath) {
1441
1535
  throw new UserError((0, index_js_1.tr) `No existe el proyecto "${projectId}".`);
1442
1536
  const client = store.getClient(db, project.clientId);
1443
1537
  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));
1538
+ const lang = docLangOf(args, client);
1539
+ 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
1540
  const out = str(args, "out");
1446
1541
  if (out) {
1447
1542
  (0, node_fs_1.writeFileSync)(out, csv, "utf8");
1448
1543
  console.log((0, index_js_1.tr) `CSV: ${out}`);
1544
+ // Solo si va a un fichero: por la salida estándar el aviso se colaría
1545
+ // dentro del propio CSV.
1546
+ hintDocLang(args, client, lang);
1449
1547
  }
1450
1548
  else {
1451
1549
  process.stdout.write(csv);
@@ -1540,13 +1638,16 @@ function cmdInvoice(args, dbPath) {
1540
1638
  const amortized = subs.length
1541
1639
  ? (0, amortize_js_1.shareForProject)((0, amortize_js_1.amortize)(store.consumptionByProjectMonth(db), subs, store.totalConsumptionByMonth(db)), projectId, [...months])
1542
1640
  : null;
1543
- const invoice = (0, invoice_js_1.issueInvoice)({
1641
+ // Las líneas de un informe se congelan al emitirlo, así que se emiten ya en
1642
+ // el idioma del cliente: es el que verá en el PDF y en el CSV.
1643
+ const lang = docLangOf(args, client);
1644
+ const invoice = (0, index_js_1.withLang)(lang, () => (0, invoice_js_1.issueInvoice)({
1544
1645
  client, project,
1545
1646
  rates: store.getRates(db, projectId),
1546
1647
  entries, cutoffAt, number,
1547
1648
  ...(fx ? { usdFxRate: Number(fx) } : {}),
1548
1649
  ...(amortized ? { aiAmortized: amortized } : {}),
1549
- });
1650
+ }));
1550
1651
  console.log((0, invoice_js_1.renderInvoice)(invoice, client, project));
1551
1652
  // Coste real de IA del periodo: la cuota de tus suscripciones repartida
1552
1653
  // por consumo. Es lo que de verdad te costó, frente a la tarifa API.
@@ -1567,7 +1668,7 @@ function cmdInvoice(args, dbPath) {
1567
1668
  const pdfPath = str(args, "pdf");
1568
1669
  if (pdfPath) {
1569
1670
  const issuerName = str(args, "from-name");
1570
- const pdf = (0, invoice_pdf_js_1.invoiceToPdf)(invoice, client, project, {
1671
+ const pdf = (0, index_js_1.withLang)(lang, () => (0, invoice_pdf_js_1.invoiceToPdf)(invoice, client, project, {
1571
1672
  ...(issuerName ? {
1572
1673
  issuer: {
1573
1674
  name: issuerName,
@@ -1576,15 +1677,17 @@ function cmdInvoice(args, dbPath) {
1576
1677
  },
1577
1678
  } : {}),
1578
1679
  ...(invoice.aiAmortized ? { amortizedAiCost: invoice.aiAmortized } : {}),
1579
- });
1680
+ }));
1580
1681
  (0, node_fs_1.writeFileSync)(pdfPath, pdf);
1581
1682
  console.log((0, index_js_1.tr) `\nPDF: ${pdfPath} (${(pdf.length / 1024).toFixed(1)} KB, ${invoice.lines.length} conceptos)`);
1582
1683
  }
1583
1684
  const csvPath = str(args, "csv");
1584
1685
  if (csvPath) {
1585
- (0, node_fs_1.writeFileSync)(csvPath, (0, csv_js_1.invoiceToCsv)(invoice, client, project), "utf8");
1686
+ (0, node_fs_1.writeFileSync)(csvPath, (0, index_js_1.withLang)(lang, () => (0, csv_js_1.invoiceToCsv)(invoice, client, project)), "utf8");
1586
1687
  console.log((0, index_js_1.tr) `CSV: ${csvPath}`);
1587
1688
  }
1689
+ if (pdfPath || csvPath)
1690
+ hintDocLang(args, client, lang);
1588
1691
  if (dryRun) {
1589
1692
  console.log((0, index_js_1.tr) `\n[--dry-run] No se guardó nada. Repite sin --dry-run para emitirlo.`);
1590
1693
  return;