estela 0.1.38 → 0.1.40

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 (84) hide show
  1. package/README.md +70 -0
  2. package/dist/cli-badge.test.d.ts +2 -0
  3. package/dist/cli-badge.test.d.ts.map +1 -0
  4. package/dist/cli-badge.test.js +118 -0
  5. package/dist/cli-badge.test.js.map +1 -0
  6. package/dist/cli-quota.test.d.ts +2 -0
  7. package/dist/cli-quota.test.d.ts.map +1 -0
  8. package/dist/cli-quota.test.js +216 -0
  9. package/dist/cli-quota.test.js.map +1 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +268 -0
  12. package/dist/cli.js.map +1 -1
  13. package/dist/db/store.d.ts +10 -1
  14. package/dist/db/store.d.ts.map +1 -1
  15. package/dist/db/store.js +29 -0
  16. package/dist/db/store.js.map +1 -1
  17. package/dist/demo.d.ts.map +1 -1
  18. package/dist/demo.js +53 -0
  19. package/dist/demo.js.map +1 -1
  20. package/dist/export/badge.d.ts +26 -0
  21. package/dist/export/badge.d.ts.map +1 -0
  22. package/dist/export/badge.js +37 -0
  23. package/dist/export/badge.js.map +1 -0
  24. package/dist/export/badge.test.d.ts +2 -0
  25. package/dist/export/badge.test.d.ts.map +1 -0
  26. package/dist/export/badge.test.js +32 -0
  27. package/dist/export/badge.test.js.map +1 -0
  28. package/dist/export/idioma.test.js +6 -2
  29. package/dist/export/idioma.test.js.map +1 -1
  30. package/dist/export/panel.d.ts +7 -0
  31. package/dist/export/panel.d.ts.map +1 -1
  32. package/dist/export/panel.js +25 -6
  33. package/dist/export/panel.js.map +1 -1
  34. package/dist/export/panel.test.js +17 -4
  35. package/dist/export/panel.test.js.map +1 -1
  36. package/dist/export/pixelfont.d.ts +17 -0
  37. package/dist/export/pixelfont.d.ts.map +1 -0
  38. package/dist/export/pixelfont.js +157 -0
  39. package/dist/export/pixelfont.js.map +1 -0
  40. package/dist/export/png.d.ts +12 -0
  41. package/dist/export/png.d.ts.map +1 -0
  42. package/dist/export/png.js +85 -0
  43. package/dist/export/png.js.map +1 -0
  44. package/dist/export/receipt.d.ts +58 -0
  45. package/dist/export/receipt.d.ts.map +1 -0
  46. package/dist/export/receipt.js +216 -0
  47. package/dist/export/receipt.js.map +1 -0
  48. package/dist/export/receipt.test.d.ts +2 -0
  49. package/dist/export/receipt.test.d.ts.map +1 -0
  50. package/dist/export/receipt.test.js +207 -0
  51. package/dist/export/receipt.test.js.map +1 -0
  52. package/dist/i18n/en.d.ts.map +1 -1
  53. package/dist/i18n/en.js +54 -1
  54. package/dist/i18n/en.js.map +1 -1
  55. package/dist/metrics/quota.d.ts +186 -0
  56. package/dist/metrics/quota.d.ts.map +1 -0
  57. package/dist/metrics/quota.js +245 -0
  58. package/dist/metrics/quota.js.map +1 -0
  59. package/dist/metrics/quota.test.d.ts +2 -0
  60. package/dist/metrics/quota.test.d.ts.map +1 -0
  61. package/dist/metrics/quota.test.js +262 -0
  62. package/dist/metrics/quota.test.js.map +1 -0
  63. package/dist/publish.d.ts +2 -0
  64. package/dist/publish.d.ts.map +1 -1
  65. package/dist/publish.js +3 -1
  66. package/dist/publish.js.map +1 -1
  67. package/dist/quota.d.ts +18 -0
  68. package/dist/quota.d.ts.map +1 -0
  69. package/dist/quota.js +85 -0
  70. package/dist/quota.js.map +1 -0
  71. package/dist/server.d.ts.map +1 -1
  72. package/dist/server.js +8 -0
  73. package/dist/server.js.map +1 -1
  74. package/dist/server.test.js +37 -0
  75. package/dist/server.test.js.map +1 -1
  76. package/node_modules/@estela/shared/dist/money.d.ts +6 -0
  77. package/node_modules/@estela/shared/dist/money.d.ts.map +1 -1
  78. package/node_modules/@estela/shared/dist/money.js +11 -0
  79. package/node_modules/@estela/shared/dist/money.js.map +1 -1
  80. package/package.json +1 -1
  81. package/web/app.css +36 -0
  82. package/web/app.js +61 -1
  83. package/web/i18n.js +28 -0
  84. package/web/index.html +4 -1
package/dist/cli.js CHANGED
@@ -46,6 +46,8 @@ process.on("warning", (w) => {
46
46
  const shared_1 = require("@estela/shared");
47
47
  const node_fs_1 = require("node:fs");
48
48
  const amortize_js_1 = require("./billing/amortize.js");
49
+ const badge_js_1 = require("./export/badge.js");
50
+ const receipt_js_1 = require("./export/receipt.js");
49
51
  const invoice_js_1 = require("./billing/invoice.js");
50
52
  const sessionize_js_1 = require("./billing/sessionize.js");
51
53
  const schema_js_1 = require("./db/schema.js");
@@ -73,6 +75,9 @@ const agents_js_1 = require("./watchers/agents.js");
73
75
  const git_js_1 = require("./watchers/git.js");
74
76
  const identity_js_1 = require("./watchers/identity.js");
75
77
  const index_js_1 = require("./i18n/index.js");
78
+ const dates_js_1 = require("./export/dates.js");
79
+ const quota_js_1 = require("./metrics/quota.js");
80
+ const quota_js_2 = require("./quota.js");
76
81
  const labels_js_1 = require("./i18n/labels.js");
77
82
  const HELP_ES = `
78
83
  estela — registro de horas para desarrollo asistido por IA
@@ -141,6 +146,25 @@ estela — registro de horas para desarrollo asistido por IA
141
146
  estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
142
147
  Pone a mano los tickets de un bloque. El import ya no los cambia.
143
148
  estela ai-cost Reparto de tu cuota entre proyectos.
149
+ estela quota [--json] [--no-refresh]
150
+ ¿Qué proyecto se come tu cuota de Claude Code o Codex? Tokens por
151
+ proyecto en la ventana de 5 h actual y en la semana (de lunes a
152
+ hoy), y esta semana frente a la media de tus 4 anteriores en el
153
+ mismo tramo. Solo consumo real: no dice cuánto te queda, porque
154
+ Anthropic y OpenAI no publican los límites. La ventana de 5 h es
155
+ una estimación: no se sabe cómo redondean su inicio, y lo que uses
156
+ en claude.ai u otro equipo también cuenta y Estela no lo ve.
157
+ Copilot queda fuera: no guarda tokens fiables. Antes de calcular
158
+ lee los transcripts desde el lunes (--no-refresh lo evita).
159
+ A diferencia de ai-cost, que reparte dinero, esto reparte tokens.
160
+ estela badge [--project <id>]
161
+ Badge para el README: horas medidas y coste de IA del proyecto.
162
+ Sin --project, el del repositorio en el que estás.
163
+ estela receipt [--week] [--names] [--out <fichero.png>]
164
+ Un recibo en PNG: horas medidas y estimadas, coste de IA, lo más
165
+ caro y lo que ahorró la caché. Con --week, los últimos 7 días.
166
+ Sin nombres de proyectos ni ramas salvo con --names. Se genera y
167
+ se queda en tu máquina.
144
168
  estela share --project <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
145
169
  [--author <tu nombre>] [--with-amounts] [--out <fichero.html>]
146
170
  Informe compartible en un solo fichero HTML. Horas y commits; sin
@@ -148,12 +172,15 @@ estela — registro de horas para desarrollo asistido por IA
148
172
 
149
173
  estela publish --project <id> [--author <tu nombre>] [--with-amounts]
150
174
  [--token <existente>] [--no-team] [--client <correo>[,otro]]
175
+ [--no-credit]
151
176
  Panel de solo lectura para que tu cliente USE el producto, no solo
152
177
  reciba un documento. Se aloja en getestela.dev — Free permite uno a
153
178
  la vez, Pro y Teams sin límite. Necesita "estela login" antes.
154
179
  Con --client se le avisa por correo y le aparece en su cuenta al
155
180
  entrar en getestela.dev/app. Sin --client no se toca esa lista: tus
156
181
  republicaciones no desvinculan a quien ya lo tenía.
182
+ --no-credit quita la línea "Horas rellenadas por Estela" del pie
183
+ (Pro y Teams).
157
184
 
158
185
  estela team ai-cost --project <id> --allow | --deny
159
186
  Si quien te invitó pide ver el coste de IA del proyecto, esta es tu
@@ -289,6 +316,26 @@ estela — time tracking for AI-assisted development
289
316
  estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
290
317
  Sets a block's tickets by hand. Imports won't change them anymore.
291
318
  estela ai-cost How your subscription splits across projects.
319
+ estela quota [--json] [--no-refresh]
320
+ Which project is eating your Claude Code or Codex quota? Tokens per
321
+ project in the current 5-hour window and this week (Monday to
322
+ today), and this week against the average of your previous 4 over
323
+ the same stretch. Real usage only: it never says how much you have
324
+ left, because Anthropic and OpenAI don't publish the limits. The
325
+ 5-hour window is an estimate: how its start is rounded isn't public,
326
+ and anything you use on claude.ai or another machine also counts and
327
+ Estela can't see it. Copilot is left out: it doesn't record reliable
328
+ tokens. Before calculating it reads transcripts since Monday
329
+ (--no-refresh skips that). Unlike ai-cost, which splits money, this
330
+ splits tokens.
331
+ estela badge [--project <id>]
332
+ Badge for your README: the project's measured hours and AI cost.
333
+ Without --project, the one for the repository you're in.
334
+ estela receipt [--week] [--names] [--out <file.png>]
335
+ A PNG receipt: measured and estimated hours, AI cost, the most
336
+ expensive work and what caching saved. With --week, the last 7 days.
337
+ No project or branch names unless you pass --names. It's generated
338
+ and stays on your machine.
292
339
  estela share --project <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
293
340
  [--author <your name>] [--with-amounts] [--out <file.html>]
294
341
  Shareable report in a single HTML file. Hours and commits; no AI
@@ -296,12 +343,15 @@ estela — time tracking for AI-assisted development
296
343
 
297
344
  estela publish --project <id> [--author <your name>] [--with-amounts]
298
345
  [--token <existing>] [--no-team] [--client <email>[,other]]
346
+ [--no-credit]
299
347
  Read-only dashboard so your client can USE the product, not just
300
348
  receive a document. Hosted on getestela.dev — Free allows one at a
301
349
  time, Pro and Teams have no limit. Requires "estela login" first.
302
350
  With --client they get an email and it shows up in their account
303
351
  when they sign in to getestela.dev/app. Without --client that list
304
352
  isn't touched: republishing never unlinks anyone who already had it.
353
+ --no-credit removes the "Hours filled in by Estela" line from the
354
+ footer (Pro and Teams).
305
355
 
306
356
  estela team ai-cost --project <id> --allow | --deny
307
357
  If whoever invited you asks to see the project's AI cost, this is
@@ -1281,6 +1331,7 @@ async function cmdPublish(args, dbPath) {
1281
1331
  ...(clientes?.length ? { clients: clientes } : {}),
1282
1332
  withAmounts: args.flags["with-amounts"] === true,
1283
1333
  includeTeam: args.flags["no-team"] !== true,
1334
+ hideCredit: args.flags["no-credit"] === true,
1284
1335
  ...(explicitLang(args) ? { language: explicitLang(args) } : {}),
1285
1336
  });
1286
1337
  }
@@ -1820,6 +1871,214 @@ function cmdAiCost(dbPath) {
1820
1871
  db.close();
1821
1872
  }
1822
1873
  }
1874
+ /**
1875
+ * El proyecto de un comando: el de --project, o el del repositorio en el que
1876
+ * se está. Es lo natural para lo que se pega en el README de ese repositorio.
1877
+ */
1878
+ async function projectFromArgsOrCwd(args, db) {
1879
+ const explicit = str(args, "project");
1880
+ if (explicit) {
1881
+ if (!store.getProject(db, explicit))
1882
+ throw new UserError((0, index_js_1.tr) `No existe el proyecto "${explicit}".`);
1883
+ return explicit;
1884
+ }
1885
+ const root = await (0, git_js_1.repoRoot)(process.cwd());
1886
+ const id = root ? store.projectForRepo(db, root) : null;
1887
+ if (!id)
1888
+ throw new UserError((0, index_js_1.tr) `Este directorio no es de ningún proyecto. Indica cuál con --project <id>.`);
1889
+ return id;
1890
+ }
1891
+ /**
1892
+ * Lo que costó la IA de un proyecto, en dinero de verdad si se puede.
1893
+ *
1894
+ * Con una cuota registrada, es su reparto (lo que se pagó); sin ella, la
1895
+ * tarifa API equivalente, que es lo que valdría ese uso y así se marca. Los
1896
+ * meses sin cuota vigente no suman: ahí no hay reparto que hacer. `months`
1897
+ * limita a esos meses; sin él, cuenta toda la historia del proyecto.
1898
+ */
1899
+ function projectAiCost(db, projectId, entries, months) {
1900
+ const subs = store.listSubscriptions(db);
1901
+ if (subs.length) {
1902
+ const shares = (0, amortize_js_1.amortize)(store.consumptionByProjectMonth(db), subs, store.totalConsumptionByMonth(db))
1903
+ .filter((sh) => sh.projectId === projectId && (0, amortize_js_1.feeForMonth)(subs, sh.month) !== null &&
1904
+ (!months || months.includes(sh.month)));
1905
+ if (shares.length) {
1906
+ const currency = shares[0].amount.currency;
1907
+ return { money: { amount: shares.reduce((n, sh) => n + sh.amount.amount, 0), currency }, api: false };
1908
+ }
1909
+ }
1910
+ const micro = entries.reduce((n, e) => n + e.aiCost.microUsd, 0);
1911
+ if (micro === 0)
1912
+ return null;
1913
+ return { money: { amount: Math.round(micro / 10_000), currency: "USD" }, api: true };
1914
+ }
1915
+ async function cmdBadge(args, dbPath) {
1916
+ const db = (0, schema_js_1.openDatabase)(dbPath);
1917
+ try {
1918
+ const projectId = await projectFromArgsOrCwd(args, db);
1919
+ const entries = store.getTimeEntries(db, projectId);
1920
+ const measuredSeconds = entries.filter((e) => e.source === "agent").reduce((n, e) => n + e.seconds, 0);
1921
+ if (measuredSeconds === 0) {
1922
+ throw new UserError((0, index_js_1.tr) `"${projectId}" no tiene horas medidas todavía: el badge solo cuenta las de sesiones de agente.`);
1923
+ }
1924
+ const cost = projectAiCost(db, projectId, entries);
1925
+ const text = (0, badge_js_1.badgeText)({ measuredSeconds, aiCost: cost ? (0, shared_1.formatMoneyWhole)(cost.money) : null });
1926
+ console.log((0, badge_js_1.badgeMarkdown)(text));
1927
+ console.log("");
1928
+ console.log((0, index_js_1.tr) `Vista previa: ${(0, badge_js_1.badgeUrl)(text)}`);
1929
+ if (cost?.api) {
1930
+ console.log((0, index_js_1.tr) `El coste es a tarifa API. Si pagas cuota fija, regístrala para que salga lo que pagas de verdad:`);
1931
+ console.log((0, index_js_1.tr) ` estela subscription add --id claude-max --name "Claude Max" --fee 200`);
1932
+ }
1933
+ console.log((0, index_js_1.tr) `Es una imagen estática: para actualizar las cifras, vuelve a generarlo.`);
1934
+ }
1935
+ finally {
1936
+ db.close();
1937
+ }
1938
+ }
1939
+ /**
1940
+ * `estela quota` — qué proyecto se come la cuota. El cálculo vive en
1941
+ * `metrics/quota.ts`; aquí solo se presenta.
1942
+ */
1943
+ async function cmdQuota(args, dbPath) {
1944
+ const db = (0, schema_js_1.openDatabase)(dbPath);
1945
+ try {
1946
+ const now = new Date();
1947
+ // Sobre la base de la demo no se lee nada tuyo: mezclaría tus turnos
1948
+ // reales con los inventados, justo lo que la demo promete no hacer.
1949
+ const esDemo = (0, node_path_1.basename)(dbPath) === "estela-demo.db";
1950
+ if (args.flags["no-refresh"] !== true && !esDemo) {
1951
+ try {
1952
+ await (0, quota_js_2.refreshQuotaTurns)(db, (0, quota_js_1.weekStart)(now));
1953
+ }
1954
+ catch {
1955
+ // Sin refrescar se sigue: lo guardado sigue valiendo, solo más viejo.
1956
+ if (args.flags["json"] !== true)
1957
+ console.log((0, index_js_1.tr) `(No se pudieron leer los transcripts de esta semana; se usa lo ya guardado.)`);
1958
+ }
1959
+ }
1960
+ const report = (0, quota_js_2.loadQuotaReport)(db, now);
1961
+ if (args.flags["json"] === true) {
1962
+ console.log(JSON.stringify((0, quota_js_1.quotaToJson)(report), null, 2));
1963
+ return;
1964
+ }
1965
+ printQuota(report, store.firstTurnByAgent(db).size > 0);
1966
+ }
1967
+ finally {
1968
+ db.close();
1969
+ }
1970
+ }
1971
+ function printQuota(report, hayTurnos) {
1972
+ console.log((0, index_js_1.tr) `\nConsumo de cuota · lo calcula Estela en tu máquina; no es el límite de tu plan\n`);
1973
+ if (report.agents.length === 0) {
1974
+ console.log(hayTurnos
1975
+ ? (0, index_js_1.tr) `Sin consumo de Claude Code ni Codex esta semana.`
1976
+ : (0, index_js_1.tr) `Sin turnos de agente guardados. Empieza con: estela setup`);
1977
+ }
1978
+ for (const a of report.agents) {
1979
+ const nombre = agentName(a.agent);
1980
+ if (a.window) {
1981
+ console.log((0, index_js_1.tr) `${nombre} — ventana de 5 h actual (estimada): desde las ${hhmm(a.window.start)}, termina a las ${hhmm(a.window.end)}`);
1982
+ printQuotaTable(a.window);
1983
+ }
1984
+ else if (a.lastWindowEnded) {
1985
+ console.log((0, index_js_1.tr) `${nombre} — sin ventana de 5 h abierta ahora; la última terminó ${cuando(a.lastWindowEnded, report.generatedAt)}.\n`);
1986
+ }
1987
+ console.log((0, index_js_1.tr) `${nombre} — esta semana (${diaCorto(report.weekStart)} → hoy)`);
1988
+ printQuotaTable(a.week);
1989
+ console.log(` ${comparacion(a, report.generatedAt)}\n`);
1990
+ }
1991
+ if (report.copilotExcluded)
1992
+ console.log((0, index_js_1.tr) `Copilot no guarda tokens fiables y queda fuera de este cálculo.`);
1993
+ console.log((0, index_js_1.tr) `La ventana de 5 h es una estimación: no se publica cómo se redondea su inicio, y lo que`);
1994
+ console.log((0, index_js_1.tr) `uses en claude.ai u otro equipo también consume cuota y Estela no lo ve.`);
1995
+ }
1996
+ function printQuotaTable(b) {
1997
+ const filas = b.rows.map((r) => [nombreDeFila(r.bucket), r.usage, pct(r.share)]);
1998
+ const ancho = Math.min(28, Math.max((0, index_js_1.tr) `Proyecto`.length, (0, index_js_1.tr) `Total`.length, ...filas.map((f) => f[0].length)));
1999
+ const cabeza = [(0, index_js_1.tr) `Entrada`, (0, index_js_1.tr) `Salida`, (0, index_js_1.tr) `Lect. caché`, (0, index_js_1.tr) `Escr. caché`, (0, index_js_1.tr) `Total`];
2000
+ const col = Math.max(10, ...cabeza.map((c) => c.length + 2));
2001
+ const celdas = (u) => [u.input, u.output, u.cacheRead, u.cacheWrite, u.total].map((n) => fmtTokens(n).padStart(col)).join("");
2002
+ const nombre = (t) => (t.length > ancho ? t.slice(0, ancho - 1) + "…" : t).padEnd(ancho);
2003
+ console.log(` ${nombre((0, index_js_1.tr) `Proyecto`)}${cabeza.map((c) => c.padStart(col)).join("")}${"%".padStart(7)}`);
2004
+ for (const [n, u, p] of filas)
2005
+ console.log(` ${nombre(n)}${celdas(u)}${p.padStart(7)}`);
2006
+ console.log(` ${nombre((0, index_js_1.tr) `Total`)}${celdas(b.total)}\n`);
2007
+ }
2008
+ function nombreDeFila(b) {
2009
+ if (b.kind === "project")
2010
+ return b.projectName;
2011
+ if (b.kind === "folder")
2012
+ return (0, index_js_1.tr) `sin proyecto · ${b.folder}`;
2013
+ return (0, index_js_1.tr) `sin carpeta`;
2014
+ }
2015
+ function comparacion(a, now) {
2016
+ const c = a.comparison;
2017
+ if (c.status === "insufficient") {
2018
+ return (0, index_js_1.tr) `Sin historial suficiente para comparar: hacen falta ${quota_js_1.HISTORY_WEEKS} semanas completas y hay ${c.completeWeeks}.`;
2019
+ }
2020
+ const tramo = (0, index_js_1.tr) `lunes → ${dates_js_1.DAYS[(0, index_js_1.getLang)()][now.getDay()]} ${hhmm(now)}`;
2021
+ if (c.ratio === null) {
2022
+ return (0, index_js_1.tr) `Frente a tu historial: ${fmtTokens(c.current)} esta semana; en el mismo tramo (${tramo}) de las ${c.weeks} semanas anteriores no hubo consumo.`;
2023
+ }
2024
+ const diff = Math.round((c.ratio - 1) * 100);
2025
+ const cuanto = diff > 0 ? (0, index_js_1.tr) `un ${diff} % más` : diff < 0 ? (0, index_js_1.tr) `un ${-diff} % menos` : (0, index_js_1.tr) `lo mismo`;
2026
+ return (0, index_js_1.tr) `Frente a tu historial: ${fmtTokens(c.current)} esta semana, y una media de ${fmtTokens(c.average)} en el mismo tramo (${tramo}) de las ${c.weeks} semanas anteriores: ${cuanto}.`;
2027
+ }
2028
+ /** Tokens con el formato corto del idioma: "22,7 M" o "22.7M". */
2029
+ function fmtTokens(n) {
2030
+ return new Intl.NumberFormat((0, index_js_1.getLang)() === "en" ? "en-US" : "es-ES", { notation: "compact", maximumFractionDigits: 1 }).format(n);
2031
+ }
2032
+ function pct(share) {
2033
+ return (0, index_js_1.tr) `${Math.round(share * 100)} %`;
2034
+ }
2035
+ function hhmm(d) {
2036
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
2037
+ }
2038
+ /** "lun 5 oct" o "Mon Oct 5". */
2039
+ function diaCorto(d) {
2040
+ const dia = dates_js_1.DAYS[(0, index_js_1.getLang)()][d.getDay()].slice(0, 3);
2041
+ const mes = dates_js_1.MONTHS[(0, index_js_1.getLang)()][d.getMonth()].slice(0, 3);
2042
+ return (0, index_js_1.getLang)() === "en" ? `${dia} ${mes} ${d.getDate()}` : `${dia} ${d.getDate()} ${mes}`;
2043
+ }
2044
+ /** "a las 14:00" si fue hoy; si no, con su día. */
2045
+ function cuando(d, now) {
2046
+ return (0, shared_1.localDate)(d) === (0, shared_1.localDate)(now) ? (0, index_js_1.tr) `a las ${hhmm(d)}` : (0, index_js_1.tr) `el ${diaCorto(d)} a las ${hhmm(d)}`;
2047
+ }
2048
+ function cmdReceipt(args, dbPath) {
2049
+ const db = (0, schema_js_1.openDatabase)(dbPath);
2050
+ try {
2051
+ const week = args.flags["week"] === true;
2052
+ // Hasta el final de hoy, en la hora de quien lo pide; con --week, desde
2053
+ // el principio del día de hace seis: siete días naturales contando hoy.
2054
+ const hoy = new Date();
2055
+ const to = new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate() + 1);
2056
+ const from = week ? new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate() - 6) : null;
2057
+ const projects = store.listProjects(db);
2058
+ const entries = projects.flatMap((p) => store.getTimeEntries(db, p.id));
2059
+ if (entries.length === 0)
2060
+ throw new UserError((0, index_js_1.tr) `Sin bloques imputados. Ejecuta: estela import`);
2061
+ const turns = db.prepare(`SELECT at, model, tok_cache_read, tok_cache_w5m, tok_cache_w1h FROM agent_turns
2062
+ WHERE at >= ? AND at < ?`).all((from ?? new Date(0)).toISOString(), to.toISOString()).map((t) => ({
2063
+ at: new Date(t.at), model: t.model,
2064
+ cacheRead: t.tok_cache_read, cacheWrite5m: t.tok_cache_w5m, cacheWrite1h: t.tok_cache_w1h,
2065
+ }));
2066
+ const data = (0, receipt_js_1.receiptData)({
2067
+ entries, turns, from, to, projectNames: new Map(projects.map((p) => [p.id, p.name])),
2068
+ });
2069
+ const out = (0, node_path_1.resolve)(str(args, "out") ??
2070
+ (week ? `estela-receipt-${(0, shared_1.localDate)(hoy)}.png` : "estela-receipt.png"));
2071
+ const names = args.flags["names"] === true;
2072
+ (0, node_fs_1.writeFileSync)(out, (0, receipt_js_1.renderReceipt)(data, { week, names }));
2073
+ console.log((0, index_js_1.tr) `Recibo guardado en ${out}`);
2074
+ if (!names)
2075
+ console.log((0, index_js_1.tr) `Sin nombres de proyectos ni ramas, para que puedas compartirlo. Con --names, se incluyen.`);
2076
+ console.log((0, index_js_1.tr) `Se ha generado en tu máquina y no se ha enviado a ningún sitio.`);
2077
+ }
2078
+ finally {
2079
+ db.close();
2080
+ }
2081
+ }
1823
2082
  function cmdInvoice(args, dbPath) {
1824
2083
  const db = (0, schema_js_1.openDatabase)(dbPath);
1825
2084
  try {
@@ -1982,6 +2241,15 @@ async function main() {
1982
2241
  case "ai-cost":
1983
2242
  cmdAiCost(dbPath);
1984
2243
  break;
2244
+ case "quota":
2245
+ await cmdQuota(args, dbPath);
2246
+ break;
2247
+ case "badge":
2248
+ await cmdBadge(args, dbPath);
2249
+ break;
2250
+ case "receipt":
2251
+ cmdReceipt(args, dbPath);
2252
+ break;
1985
2253
  case "status":
1986
2254
  cmdStatus(dbPath);
1987
2255
  break;