estela 0.1.39 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +96 -0
  2. package/dist/billing/assign.d.ts +45 -0
  3. package/dist/billing/assign.d.ts.map +1 -0
  4. package/dist/billing/assign.js +159 -0
  5. package/dist/billing/assign.js.map +1 -0
  6. package/dist/billing/assign.test.d.ts +2 -0
  7. package/dist/billing/assign.test.d.ts.map +1 -0
  8. package/dist/billing/assign.test.js +153 -0
  9. package/dist/billing/assign.test.js.map +1 -0
  10. package/dist/bin.js +23 -1
  11. package/dist/bin.js.map +1 -1
  12. package/dist/cli-import-recover.test.d.ts +2 -0
  13. package/dist/cli-import-recover.test.d.ts.map +1 -0
  14. package/dist/cli-import-recover.test.js +191 -0
  15. package/dist/cli-import-recover.test.js.map +1 -0
  16. package/dist/cli-quota.test.d.ts +2 -0
  17. package/dist/cli-quota.test.d.ts.map +1 -0
  18. package/dist/cli-quota.test.js +281 -0
  19. package/dist/cli-quota.test.js.map +1 -0
  20. package/dist/cli-statusline.test.d.ts +2 -0
  21. package/dist/cli-statusline.test.d.ts.map +1 -0
  22. package/dist/cli-statusline.test.js +333 -0
  23. package/dist/cli-statusline.test.js.map +1 -0
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +350 -49
  26. package/dist/cli.js.map +1 -1
  27. package/dist/db/store.d.ts +12 -1
  28. package/dist/db/store.d.ts.map +1 -1
  29. package/dist/db/store.js +52 -0
  30. package/dist/db/store.js.map +1 -1
  31. package/dist/demo.d.ts.map +1 -1
  32. package/dist/demo.js +53 -0
  33. package/dist/demo.js.map +1 -1
  34. package/dist/i18n/en.d.ts.map +1 -1
  35. package/dist/i18n/en.js +68 -0
  36. package/dist/i18n/en.js.map +1 -1
  37. package/dist/metrics/quota.d.ts +226 -0
  38. package/dist/metrics/quota.d.ts.map +1 -0
  39. package/dist/metrics/quota.js +291 -0
  40. package/dist/metrics/quota.js.map +1 -0
  41. package/dist/metrics/quota.test.d.ts +2 -0
  42. package/dist/metrics/quota.test.d.ts.map +1 -0
  43. package/dist/metrics/quota.test.js +325 -0
  44. package/dist/metrics/quota.test.js.map +1 -0
  45. package/dist/project-of.d.ts +52 -0
  46. package/dist/project-of.d.ts.map +1 -0
  47. package/dist/project-of.js +124 -0
  48. package/dist/project-of.js.map +1 -0
  49. package/dist/project-of.test.d.ts +2 -0
  50. package/dist/project-of.test.d.ts.map +1 -0
  51. package/dist/project-of.test.js +203 -0
  52. package/dist/project-of.test.js.map +1 -0
  53. package/dist/quota.d.ts +27 -0
  54. package/dist/quota.d.ts.map +1 -0
  55. package/dist/quota.js +79 -0
  56. package/dist/quota.js.map +1 -0
  57. package/dist/server.d.ts.map +1 -1
  58. package/dist/server.js +17 -26
  59. package/dist/server.js.map +1 -1
  60. package/dist/server.test.js +37 -0
  61. package/dist/server.test.js.map +1 -1
  62. package/dist/statusline-cache.d.ts +35 -0
  63. package/dist/statusline-cache.d.ts.map +1 -0
  64. package/dist/statusline-cache.js +57 -0
  65. package/dist/statusline-cache.js.map +1 -0
  66. package/dist/statusline-install.d.ts +52 -0
  67. package/dist/statusline-install.d.ts.map +1 -0
  68. package/dist/statusline-install.js +115 -0
  69. package/dist/statusline-install.js.map +1 -0
  70. package/dist/statusline-run.d.ts +8 -0
  71. package/dist/statusline-run.d.ts.map +1 -0
  72. package/dist/statusline-run.js +256 -0
  73. package/dist/statusline-run.js.map +1 -0
  74. package/dist/statusline.d.ts +71 -0
  75. package/dist/statusline.d.ts.map +1 -0
  76. package/dist/statusline.js +139 -0
  77. package/dist/statusline.js.map +1 -0
  78. package/dist/statusline.test.d.ts +2 -0
  79. package/dist/statusline.test.d.ts.map +1 -0
  80. package/dist/statusline.test.js +197 -0
  81. package/dist/statusline.test.js.map +1 -0
  82. package/dist/watchers/claude.d.ts +11 -1
  83. package/dist/watchers/claude.d.ts.map +1 -1
  84. package/dist/watchers/claude.js +32 -15
  85. package/dist/watchers/claude.js.map +1 -1
  86. package/package.json +1 -1
  87. package/web/app.css +36 -0
  88. package/web/app.js +61 -1
  89. package/web/i18n.js +28 -0
  90. package/web/index.html +4 -1
package/dist/cli.js CHANGED
@@ -36,6 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.breakdownOfTurn = void 0;
38
38
  const node_path_1 = require("node:path");
39
+ const node_readline_1 = require("node:readline");
39
40
  // `node:sqlite` avisa de que es experimental en cada arranque. Es ruido para
40
41
  // quien solo quiere su factura, y el aviso no le dice nada accionable.
41
42
  process.removeAllListeners("warning");
@@ -75,6 +76,11 @@ const agents_js_1 = require("./watchers/agents.js");
75
76
  const git_js_1 = require("./watchers/git.js");
76
77
  const identity_js_1 = require("./watchers/identity.js");
77
78
  const index_js_1 = require("./i18n/index.js");
79
+ const dates_js_1 = require("./export/dates.js");
80
+ const quota_js_1 = require("./metrics/quota.js");
81
+ const quota_js_2 = require("./quota.js");
82
+ const assign_js_1 = require("./billing/assign.js");
83
+ const statusline_install_js_1 = require("./statusline-install.js");
78
84
  const labels_js_1 = require("./i18n/labels.js");
79
85
  const HELP_ES = `
80
86
  estela — registro de horas para desarrollo asistido por IA
@@ -83,8 +89,12 @@ estela — registro de horas para desarrollo asistido por IA
83
89
  Primera ejecución: detecta tus agentes y repositorios, y reconstruye
84
90
  tu historial. Dos minutos, sin preguntas y sin cuenta.
85
91
 
86
- estela import [--since YYYY-MM-DD] [--repo <ruta>]
92
+ estela import [--since YYYY-MM-DD] [--repo <ruta>] [--yes]
87
93
  Reconstruye tu historial desde los transcripts de los agentes y git.
94
+ Una sesión en una subcarpeta o en un worktree cuenta para el
95
+ proyecto de su repo. La primera vez que eso suma horas a lo ya
96
+ importado, te enseña cuántas y pregunta (--yes no pregunta). Nunca
97
+ mueve horas de un proyecto a otro ni toca un día aprobado.
88
98
 
89
99
  estela client add --id <id> --name <nombre> --currency <EUR|USD|...>
90
100
  [--tax-id <NIF/CIF>] [--email <a@b.com>]
@@ -143,6 +153,32 @@ estela — registro de horas para desarrollo asistido por IA
143
153
  estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
144
154
  Pone a mano los tickets de un bloque. El import ya no los cambia.
145
155
  estela ai-cost Reparto de tu cuota entre proyectos.
156
+ estela quota [--json] [--no-refresh] [--weighted]
157
+ ¿Qué proyecto se come tu cuota de Claude Code o Codex? Tokens por
158
+ proyecto en la ventana de 5 h actual y en la semana (de lunes a
159
+ hoy), y esta semana frente a la media de tus 4 anteriores en el
160
+ mismo tramo. Solo consumo real: no dice cuánto te queda, porque
161
+ Anthropic y OpenAI no publican los límites. La ventana de 5 h es
162
+ una estimación: no se sabe cómo redondean su inicio, y lo que uses
163
+ en claude.ai u otro equipo también cuenta y Estela no lo ve.
164
+ Si la status line de Estela está instalada, la ventana de Claude
165
+ Code es la que da Claude Code, no una estimación. Una sesión en un
166
+ worktree o en una subcarpeta cuenta para el proyecto de su repo.
167
+ --weighted reparte el % por precio de lista del modelo en vez de
168
+ por tokens (no es cómo el proveedor cuenta tu límite).
169
+ Copilot queda fuera: no guarda tokens fiables. Antes de calcular
170
+ lee los transcripts desde el lunes (--no-refresh lo evita).
171
+ A diferencia de ai-cost, que reparte dinero, esto reparte tokens.
172
+ estela statusline install [--force | --append]
173
+ Una línea bajo cada sesión de Claude Code: proyecto, horas medidas
174
+ hoy, la parte de tu consumo de la semana que es de este proyecto y,
175
+ si Claude Code los manda, sus porcentajes de límite de la semana y
176
+ de 5 h (tal cual, sin estimar nada). Se calcula en tu máquina en
177
+ unos 50 ms. Añade el bloque a ~/.claude/settings.json sin tocar lo
178
+ demás, y antes deja una copia. Si ya tienes otra status line no la
179
+ pisa: --force la sustituye y --append la encadena detrás.
180
+ estela statusline uninstall
181
+ La quita. Si la instalaste con --append, deja la que tenías.
146
182
  estela badge [--project <id>]
147
183
  Badge para el README: horas medidas y coste de IA del proyecto.
148
184
  Sin --project, el del repositorio en el que estás.
@@ -242,8 +278,12 @@ estela — time tracking for AI-assisted development
242
278
  First run: detects your agents and repositories, and rebuilds your
243
279
  history. Two minutes, no questions, no account.
244
280
 
245
- estela import [--since YYYY-MM-DD] [--repo <path>]
246
- Rebuilds your history from agent transcripts and git.
281
+ estela import [--since YYYY-MM-DD] [--repo <path>] [--yes]
282
+ Rebuilds your history from agent transcripts and git. A session in a
283
+ subfolder or a worktree counts for its repo's project. The first
284
+ time that adds hours to what was already imported, it shows how many
285
+ and asks (--yes skips the question). It never moves hours from one
286
+ project to another or touches an approved day.
247
287
 
248
288
  estela client add --id <id> --name <name> --currency <EUR|USD|...>
249
289
  [--tax-id <tax ID>] [--email <a@b.com>]
@@ -302,6 +342,34 @@ estela — time tracking for AI-assisted development
302
342
  estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
303
343
  Sets a block's tickets by hand. Imports won't change them anymore.
304
344
  estela ai-cost How your subscription splits across projects.
345
+ estela quota [--json] [--no-refresh] [--weighted]
346
+ Which project is eating your Claude Code or Codex quota? Tokens per
347
+ project in the current 5-hour window and this week (Monday to
348
+ today), and this week against the average of your previous 4 over
349
+ the same stretch. Real usage only: it never says how much you have
350
+ left, because Anthropic and OpenAI don't publish the limits. The
351
+ 5-hour window is an estimate: how its start is rounded isn't public,
352
+ and anything you use on claude.ai or another machine also counts and
353
+ Estela can't see it. With Estela's status line installed, the
354
+ Claude Code window is the one Claude Code reports, not an estimate.
355
+ A session in a worktree or a subfolder counts for its repo's
356
+ project. --weighted splits the % by the model's list price instead
357
+ of by tokens (not how the provider counts your limit). Copilot is
358
+ left out: it doesn't record reliable tokens. Before calculating it reads transcripts since Monday
359
+ (--no-refresh skips that). Unlike ai-cost, which splits money, this
360
+ splits tokens.
361
+ estela statusline install [--force | --append]
362
+ A line under every Claude Code session: project, hours measured
363
+ today, this project's share of your usage this week and, if Claude
364
+ Code sends them, its weekly and 5-hour limit percentages (as they
365
+ come, nothing estimated). Calculated on your machine in about
366
+ 50 ms. It adds the block to ~/.claude/settings.json without touching
367
+ anything else, after making a backup. If you already have another
368
+ status line it won't overwrite it: --force replaces it and --append
369
+ chains it after Estela's.
370
+ estela statusline uninstall
371
+ Removes it. If you installed it with --append, your previous one
372
+ is put back.
305
373
  estela badge [--project <id>]
306
374
  Badge for your README: the project's measured hours and AI cost.
307
375
  Without --project, the one for the repository you're in.
@@ -661,6 +729,41 @@ async function cmdSetup(args, dbPath) {
661
729
  db.close();
662
730
  }
663
731
  }
732
+ /** La vista previa de la primera recuperación: horas por proyecto. */
733
+ function printRecoveryPreview(db, plan) {
734
+ console.log((0, index_js_1.tr) `\nEstela ahora reconoce las sesiones en subcarpetas y worktrees de un repositorio.`);
735
+ console.log((0, index_js_1.tr) `Estas horas no estaban imputadas y se sumarían a su proyecto:\n`);
736
+ for (const [projectId, seconds] of [...plan.recovered].sort((a, b) => b[1] - a[1])) {
737
+ const name = store.getProject(db, projectId)?.name ?? projectId;
738
+ console.log(` ${name.padEnd(30)} +${(0, shared_1.formatDuration)(seconds)}`);
739
+ }
740
+ console.log((0, index_js_1.tr) `\n Nada de lo ya imputado cambia de proyecto, y los días aprobados no se tocan.`);
741
+ printRecoveryBlocked(db, plan.blocked);
742
+ console.log("");
743
+ }
744
+ /** Lo que se recuperaría en días aprobados o facturados: se avisa, no se aplica. */
745
+ function printRecoveryBlocked(db, blocked) {
746
+ if (blocked.length === 0)
747
+ return;
748
+ console.log((0, index_js_1.tr) `\n Sin tocar, porque ese día ya está aprobado o facturado:`);
749
+ for (const b of [...blocked].sort((x, y) => x.day.localeCompare(y.day))) {
750
+ const name = store.getProject(db, b.projectId)?.name ?? b.projectId;
751
+ console.log((0, index_js_1.tr) ` ${b.day} · ${name}: ${(0, shared_1.formatDuration)(b.seconds)} recuperables. Revísalo con: estela entries --project ${b.projectId}`);
752
+ }
753
+ }
754
+ /** Una pregunta de sí o no. Sin terminal (un script, CI) la respuesta es no. */
755
+ async function confirm(question) {
756
+ if (!process.stdin.isTTY)
757
+ return false;
758
+ const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
759
+ try {
760
+ const answer = (await new Promise((res) => rl.question(question, res))).trim().toLowerCase();
761
+ return ["s", "si", "sí", "y", "yes"].includes(answer);
762
+ }
763
+ finally {
764
+ rl.close();
765
+ }
766
+ }
664
767
  /**
665
768
  * La estrella se pide una vez, al terminar bien el primer setup: es cuando se
666
769
  * acaba de ver funcionar. Casi todo el que instala lo hace por npm y nunca
@@ -754,57 +857,54 @@ async function cmdImport(args, dbPath) {
754
857
  const fromAgents = (0, sessionize_js_1.attachCommits)((0, sessionize_js_1.sessionize)(turns), commitsAll);
755
858
  const fromCommits = (0, sessionize_js_1.withoutOverlap)((0, sessionize_js_1.sessionizeCommits)(commitsAll), fromAgents);
756
859
  const raw = [...fromAgents, ...fromCommits];
757
- // Se agrupa por PROYECTO, no por ruta: el agente abierto en `repo/` y en
758
- // `repo/src/UI` son dos rutas del mismo proyecto, y agrupar por ruta
759
- // producía dos bloques que luego se pisaban al guardarse con el mismo id.
760
- const porProyecto = (b) => (b.repoPath ? store.projectForRepo(db, b.repoPath) ?? b.repoPath : "");
761
- const blocks = (0, sessionize_js_1.groupByBranchAndDay)(raw, porProyecto);
762
860
  if (fromCommits.length) {
763
861
  console.log((0, index_js_1.tr) ` ${fromCommits.length} bloques deducidos solo de commits (sin agente)`);
764
862
  }
765
- let entries = 0;
766
- let unassigned = 0;
767
- for (const block of blocks) {
768
- const projectId = block.repoPath ? store.projectForRepo(db, block.repoPath) : null;
769
- if (!projectId) {
770
- unassigned++;
771
- continue;
863
+ // Horas de sesiones en subcarpetas y worktrees, que antes se quedaban sin
864
+ // proyecto (ver billing/assign.ts). La primera vez que cambian horas ya
865
+ // importadas se enseñan antes y se pide confirmación; en una base nueva no
866
+ // hay nada que cambiar, así que se aplica sin preguntar.
867
+ const marca = (0, node_path_1.join)((0, node_path_1.dirname)(dbPath), ".recover-git-root");
868
+ const habiaHoras = db.prepare("SELECT COUNT(*) AS n FROM time_entries").get().n > 0;
869
+ let plan = (0, assign_js_1.planEntries)(db, raw, { recover: true });
870
+ const conRaiz = plan;
871
+ const recuperable = [...plan.recovered.values()].reduce((a, b) => a + b, 0);
872
+ if (!(0, node_fs_1.existsSync)(marca) && habiaHoras && recuperable > 0) {
873
+ printRecoveryPreview(db, plan);
874
+ const ok = args.flags["yes"] === true || await confirm((0, index_js_1.tr) `¿Imputar estas horas? [s/N] `);
875
+ if (!ok) {
876
+ plan = (0, assign_js_1.planEntries)(db, raw, { recover: false });
877
+ console.log((0, index_js_1.tr) ` No se ha tocado nada. Cuando quieras: estela import --yes\n`);
772
878
  }
773
- const project = store.getProject(db, projectId);
774
- const entry = {
775
- projectId,
776
- startedAt: block.startedAt,
777
- endedAt: block.endedAt,
778
- seconds: block.seconds,
779
- description: (0, sessionize_js_1.describeBlock)(block),
780
- billable: project.billable,
781
- invoiceId: null,
782
- aiCost: block.aiCost,
783
- // Un bloque deducido solo de commits no tuvo agente: ni segundos de
784
- // agente ni agente al que atribuirlos.
785
- agentSeconds: block.turnCount > 0 ? block.seconds : 0,
786
- commitHashes: block.commits.map((c) => c.hash),
787
- agents: block.turnCount > 0 ? [...(block.agents ?? ["claude-code"])] : [],
788
- source: block.turnCount > 0 ? "agent" : "commit",
789
- kind: "development",
790
- branch: block.branch,
791
- // El ticket se saca aquí y no más tarde: en la base solo quedan los
792
- // hashes de los commits, y es en sus mensajes donde va el `AB#1234`.
793
- workItems: (0, shared_1.extractWorkItems)(block.branch, block.commits.map((c) => c.subject), project.tracker),
794
- };
795
- // El id es determinista (proyecto + día + rama) para que reimportar
796
- // actualice la imputación en vez de duplicarla. La fecha tiene que ser la
797
- // LOCAL y la misma que usa el servidor: con toISOString() se generaba un
798
- // id distinto para el mismo bloque y el día se contaba dos veces.
799
- const day = (0, shared_1.localDate)(block.startedAt);
800
- const slug = (block.branch ?? "sin-rama").replace(/[^a-zA-Z0-9]+/g, "-");
801
- store.saveTimeEntry(db, { ...entry, id: `te_${projectId}_${day}_${slug}` });
802
- entries++;
803
- }
804
- console.log((0, index_js_1.tr) `Bloques: ${raw.length} detectados → ${blocks.length} tras agrupar por rama y día · ` +
879
+ else {
880
+ (0, node_fs_1.writeFileSync)(marca, new Date().toISOString());
881
+ }
882
+ }
883
+ else {
884
+ try {
885
+ (0, node_fs_1.writeFileSync)(marca, new Date().toISOString());
886
+ }
887
+ catch { /* sin marca, se volverá a preguntar */ }
888
+ }
889
+ for (const { entry } of plan.entries)
890
+ store.saveTimeEntry(db, entry);
891
+ const entries = plan.entries.length;
892
+ const unassigned = plan.unassigned;
893
+ console.log((0, index_js_1.tr) `Bloques: ${raw.length} detectados → ${plan.grouped} tras agrupar por rama y día · ` +
805
894
  (0, index_js_1.tr) `${entries} imputados · ${unassigned} sin proyecto`);
806
- if (unassigned > 0) {
807
- console.log((0, index_js_1.tr) `\nHay ${unassigned} bloques sin proyecto asignado. Regístralos con:`);
895
+ const aplicadas = [...plan.recovered.values()].reduce((a, b) => a + b, 0);
896
+ if (aplicadas > 0 && (0, node_fs_1.existsSync)(marca)) {
897
+ console.log((0, index_js_1.tr) ` ${(0, shared_1.formatDuration)(aplicadas)} recuperadas de sesiones en subcarpetas y worktrees`);
898
+ }
899
+ printRecoveryBlocked(db, plan.blocked);
900
+ // Si no se confirmó, parte de "sin proyecto" son sesiones de subcarpetas y
901
+ // worktrees: a esas no les falta registrarse, les falta el --yes.
902
+ const porRaiz = unassigned - conRaiz.unassigned;
903
+ if (porRaiz > 0) {
904
+ console.log((0, index_js_1.tr) `\n${porRaiz} de esos bloques son de subcarpetas o worktrees de un proyecto: estela import --yes los imputa.`);
905
+ }
906
+ if (unassigned - porRaiz > 0) {
907
+ console.log((0, index_js_1.tr) `\nHay ${unassigned - porRaiz} bloques sin proyecto asignado. Regístralos con:`);
808
908
  console.log((0, index_js_1.tr) ` estela project add --id <id> --client <id> --name <nombre> --repo <ruta>`);
809
909
  }
810
910
  }
@@ -1910,6 +2010,198 @@ async function cmdBadge(args, dbPath) {
1910
2010
  db.close();
1911
2011
  }
1912
2012
  }
2013
+ /**
2014
+ * `estela statusline install` — la línea de Estela en Claude Code. La lógica
2015
+ * del fichero está en `statusline-install.ts`; aquí, leer, escribir y contar.
2016
+ */
2017
+ function cmdStatuslineInstall(args) {
2018
+ const path = (0, statusline_install_js_1.claudeSettingsPath)();
2019
+ const before = (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, "utf8") : null;
2020
+ const plan = (0, statusline_install_js_1.planInstall)(before, { force: args.flags["force"] === true, append: args.flags["append"] === true });
2021
+ if (plan.status === "invalid") {
2022
+ throw new UserError((0, index_js_1.tr) `${path} no es un JSON válido. No lo toco: arréglalo y vuelve a intentarlo.`);
2023
+ }
2024
+ if (plan.status === "already") {
2025
+ console.log((0, index_js_1.tr) `La status line de Estela ya está instalada en ${path}.`);
2026
+ return;
2027
+ }
2028
+ if (plan.status === "conflict") {
2029
+ console.log((0, index_js_1.tr) `Ya tienes otra status line en ${path}:\n`);
2030
+ console.log(` ${plan.existing}\n`);
2031
+ console.log((0, index_js_1.tr) `No la toco. Elige:`);
2032
+ console.log((0, index_js_1.tr) ` estela statusline install --append enseña las dos, la de Estela primero`);
2033
+ console.log((0, index_js_1.tr) ` estela statusline install --force sustituye la tuya por la de Estela`);
2034
+ process.exitCode = 1;
2035
+ return;
2036
+ }
2037
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
2038
+ if (before !== null) {
2039
+ const copia = (0, statusline_install_js_1.backupName)(path);
2040
+ (0, node_fs_1.copyFileSync)(path, copia);
2041
+ console.log((0, index_js_1.tr) `Copia de seguridad: ${copia}`);
2042
+ }
2043
+ (0, node_fs_1.writeFileSync)(path, plan.text);
2044
+ console.log(plan.status === "chained"
2045
+ ? (0, index_js_1.tr) `Status line de Estela instalada, con la tuya detrás: ${plan.previous ?? ""}`
2046
+ : plan.status === "replaced"
2047
+ ? (0, index_js_1.tr) `Status line de Estela instalada en lugar de: ${plan.previous ?? ""}`
2048
+ : (0, index_js_1.tr) `Status line de Estela instalada en ${path}.`);
2049
+ console.log((0, index_js_1.tr) `Se verá en la próxima sesión de Claude Code (o al reiniciar la actual).`);
2050
+ if (!(0, statusline_install_js_1.estelaOnPath)()) {
2051
+ console.log((0, index_js_1.tr) `\nAviso: "estela" no está en tu PATH, así que Claude Code no podrá ejecutarla.`);
2052
+ console.log((0, index_js_1.tr) `Instálala de forma global: npm install -g estela`);
2053
+ }
2054
+ }
2055
+ function cmdStatuslineUninstall() {
2056
+ const path = (0, statusline_install_js_1.claudeSettingsPath)();
2057
+ if (!(0, node_fs_1.existsSync)(path)) {
2058
+ console.log((0, index_js_1.tr) `No hay status line de Estela que quitar.`);
2059
+ return;
2060
+ }
2061
+ const plan = (0, statusline_install_js_1.planUninstall)((0, node_fs_1.readFileSync)(path, "utf8"));
2062
+ if (plan.status === "invalid") {
2063
+ throw new UserError((0, index_js_1.tr) `${path} no es un JSON válido. No lo toco: arréglalo y vuelve a intentarlo.`);
2064
+ }
2065
+ if (plan.status === "absent") {
2066
+ console.log((0, index_js_1.tr) `No hay status line de Estela que quitar.`);
2067
+ return;
2068
+ }
2069
+ if (plan.status === "foreign") {
2070
+ console.log((0, index_js_1.tr) `La status line de ${path} no es la de Estela, así que no la toco:\n\n ${plan.existing}`);
2071
+ return;
2072
+ }
2073
+ const copia = (0, statusline_install_js_1.backupName)(path);
2074
+ (0, node_fs_1.copyFileSync)(path, copia);
2075
+ (0, node_fs_1.writeFileSync)(path, plan.text);
2076
+ console.log((0, index_js_1.tr) `Copia de seguridad: ${copia}`);
2077
+ console.log(plan.status === "restored"
2078
+ ? (0, index_js_1.tr) `Status line de Estela quitada; vuelve a estar la que tenías: ${plan.restored ?? ""}`
2079
+ : (0, index_js_1.tr) `Status line de Estela quitada.`);
2080
+ }
2081
+ /**
2082
+ * `estela quota` — qué proyecto se come la cuota. El cálculo vive en
2083
+ * `metrics/quota.ts`; aquí solo se presenta.
2084
+ */
2085
+ async function cmdQuota(args, dbPath) {
2086
+ const db = (0, schema_js_1.openDatabase)(dbPath);
2087
+ try {
2088
+ const now = new Date();
2089
+ // Sobre la base de la demo no se lee nada tuyo: mezclaría tus turnos
2090
+ // reales con los inventados, justo lo que la demo promete no hacer.
2091
+ const esDemo = (0, node_path_1.basename)(dbPath) === "estela-demo.db";
2092
+ if (args.flags["no-refresh"] !== true && !esDemo) {
2093
+ try {
2094
+ await (0, quota_js_2.refreshQuotaTurns)(db, (0, quota_js_1.weekStart)(now));
2095
+ }
2096
+ catch {
2097
+ // Sin refrescar se sigue: lo guardado sigue valiendo, solo más viejo.
2098
+ if (args.flags["json"] !== true)
2099
+ console.log((0, index_js_1.tr) `(No se pudieron leer los transcripts de esta semana; se usa lo ya guardado.)`);
2100
+ }
2101
+ }
2102
+ const report = (0, quota_js_2.loadQuotaReport)(db, now, { dbPath, weighted: args.flags["weighted"] === true });
2103
+ if (args.flags["json"] === true) {
2104
+ console.log(JSON.stringify((0, quota_js_1.quotaToJson)(report), null, 2));
2105
+ return;
2106
+ }
2107
+ printQuota(report, store.firstTurnByAgent(db).size > 0);
2108
+ }
2109
+ finally {
2110
+ db.close();
2111
+ }
2112
+ }
2113
+ function printQuota(report, hayTurnos) {
2114
+ 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`);
2115
+ if (report.agents.length === 0) {
2116
+ console.log(hayTurnos
2117
+ ? (0, index_js_1.tr) `Sin consumo de Claude Code ni Codex esta semana.`
2118
+ : (0, index_js_1.tr) `Sin turnos de agente guardados. Empieza con: estela setup`);
2119
+ }
2120
+ for (const a of report.agents) {
2121
+ const nombre = agentName(a.agent);
2122
+ if (a.window) {
2123
+ console.log(a.window.estimated
2124
+ ? (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)}`
2125
+ : (0, index_js_1.tr) `${nombre} — ventana de 5 h actual: desde las ${hhmm(a.window.start)}, termina a las ${hhmm(a.window.end)}`);
2126
+ printQuotaTable(a.window);
2127
+ }
2128
+ else if (a.lastWindowEnded) {
2129
+ console.log((0, index_js_1.tr) `${nombre} — sin ventana de 5 h abierta ahora; la última terminó ${cuando(a.lastWindowEnded, report.generatedAt)}.\n`);
2130
+ }
2131
+ console.log((0, index_js_1.tr) `${nombre} — esta semana (${diaCorto(report.weekStart)} → hoy)`);
2132
+ printQuotaTable(a.week);
2133
+ console.log(` ${comparacion(a, report.generatedAt)}\n`);
2134
+ }
2135
+ if (report.copilotExcluded)
2136
+ console.log((0, index_js_1.tr) `Copilot no guarda tokens fiables y queda fuera de este cálculo.`);
2137
+ if (report.agents.some((a) => a.week.weighted)) {
2138
+ console.log((0, index_js_1.tr) `Ponderado por precio de lista; no es cómo el proveedor cuenta tu límite.`);
2139
+ const sinPrecio = report.agents.reduce((n, a) => n + a.week.unpriced, 0);
2140
+ if (sinPrecio > 0)
2141
+ console.log((0, index_js_1.tr) `${sinPrecio} turnos de modelos sin precio de lista cuentan como 0 en ese %.`);
2142
+ }
2143
+ // La ventana estimada lo dice; la que dio Claude Code no lo necesita, pero
2144
+ // el reparto sigue siendo solo de esta máquina.
2145
+ if (report.agents.some((a) => a.window?.estimated)) {
2146
+ 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`);
2147
+ console.log((0, index_js_1.tr) `uses en claude.ai u otro equipo también consume cuota y Estela no lo ve.`);
2148
+ }
2149
+ else if (report.agents.some((a) => a.window)) {
2150
+ console.log((0, index_js_1.tr) `La ventana de 5 h es la que da Claude Code; el reparto solo incluye lo usado en esta máquina.`);
2151
+ }
2152
+ }
2153
+ function printQuotaTable(b) {
2154
+ const filas = b.rows.map((r) => [nombreDeFila(r.bucket), r.usage, pct(r.share)]);
2155
+ 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)));
2156
+ 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`];
2157
+ const col = Math.max(10, ...cabeza.map((c) => c.length + 2));
2158
+ const celdas = (u) => [u.input, u.output, u.cacheRead, u.cacheWrite, u.total].map((n) => fmtTokens(n).padStart(col)).join("");
2159
+ const nombre = (t) => (t.length > ancho ? t.slice(0, ancho - 1) + "…" : t).padEnd(ancho);
2160
+ console.log(` ${nombre((0, index_js_1.tr) `Proyecto`)}${cabeza.map((c) => c.padStart(col)).join("")}${"%".padStart(7)}`);
2161
+ for (const [n, u, p] of filas)
2162
+ console.log(` ${nombre(n)}${celdas(u)}${p.padStart(7)}`);
2163
+ console.log(` ${nombre((0, index_js_1.tr) `Total`)}${celdas(b.total)}\n`);
2164
+ }
2165
+ function nombreDeFila(b) {
2166
+ if (b.kind === "project")
2167
+ return b.projectName;
2168
+ if (b.kind === "folder")
2169
+ return (0, index_js_1.tr) `sin proyecto · ${b.folder}`;
2170
+ return (0, index_js_1.tr) `sin carpeta`;
2171
+ }
2172
+ function comparacion(a, now) {
2173
+ const c = a.comparison;
2174
+ if (c.status === "insufficient") {
2175
+ return (0, index_js_1.tr) `Sin historial suficiente para comparar: hacen falta ${quota_js_1.HISTORY_WEEKS} semanas completas y hay ${c.completeWeeks}.`;
2176
+ }
2177
+ const tramo = (0, index_js_1.tr) `lunes → ${dates_js_1.DAYS[(0, index_js_1.getLang)()][now.getDay()]} ${hhmm(now)}`;
2178
+ if (c.ratio === null) {
2179
+ 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.`;
2180
+ }
2181
+ const diff = Math.round((c.ratio - 1) * 100);
2182
+ 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`;
2183
+ 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}.`;
2184
+ }
2185
+ /** Tokens con el formato corto del idioma: "22,7 M" o "22.7M". */
2186
+ function fmtTokens(n) {
2187
+ return new Intl.NumberFormat((0, index_js_1.getLang)() === "en" ? "en-US" : "es-ES", { notation: "compact", maximumFractionDigits: 1 }).format(n);
2188
+ }
2189
+ function pct(share) {
2190
+ return (0, index_js_1.tr) `${Math.round(share * 100)} %`;
2191
+ }
2192
+ function hhmm(d) {
2193
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
2194
+ }
2195
+ /** "lun 5 oct" o "Mon Oct 5". */
2196
+ function diaCorto(d) {
2197
+ const dia = dates_js_1.DAYS[(0, index_js_1.getLang)()][d.getDay()].slice(0, 3);
2198
+ const mes = dates_js_1.MONTHS[(0, index_js_1.getLang)()][d.getMonth()].slice(0, 3);
2199
+ return (0, index_js_1.getLang)() === "en" ? `${dia} ${mes} ${d.getDate()}` : `${dia} ${d.getDate()} ${mes}`;
2200
+ }
2201
+ /** "a las 14:00" si fue hoy; si no, con su día. */
2202
+ function cuando(d, now) {
2203
+ 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)}`;
2204
+ }
1913
2205
  function cmdReceipt(args, dbPath) {
1914
2206
  const db = (0, schema_js_1.openDatabase)(dbPath);
1915
2207
  try {
@@ -2106,6 +2398,15 @@ async function main() {
2106
2398
  case "ai-cost":
2107
2399
  cmdAiCost(dbPath);
2108
2400
  break;
2401
+ case "quota":
2402
+ await cmdQuota(args, dbPath);
2403
+ break;
2404
+ case "statusline install":
2405
+ cmdStatuslineInstall(args);
2406
+ break;
2407
+ case "statusline uninstall":
2408
+ cmdStatuslineUninstall();
2409
+ break;
2109
2410
  case "badge":
2110
2411
  await cmdBadge(args, dbPath);
2111
2412
  break;