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.
- package/README.md +96 -0
- package/dist/billing/assign.d.ts +45 -0
- package/dist/billing/assign.d.ts.map +1 -0
- package/dist/billing/assign.js +159 -0
- package/dist/billing/assign.js.map +1 -0
- package/dist/billing/assign.test.d.ts +2 -0
- package/dist/billing/assign.test.d.ts.map +1 -0
- package/dist/billing/assign.test.js +153 -0
- package/dist/billing/assign.test.js.map +1 -0
- package/dist/bin.js +23 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli-import-recover.test.d.ts +2 -0
- package/dist/cli-import-recover.test.d.ts.map +1 -0
- package/dist/cli-import-recover.test.js +191 -0
- package/dist/cli-import-recover.test.js.map +1 -0
- package/dist/cli-quota.test.d.ts +2 -0
- package/dist/cli-quota.test.d.ts.map +1 -0
- package/dist/cli-quota.test.js +281 -0
- package/dist/cli-quota.test.js.map +1 -0
- package/dist/cli-statusline.test.d.ts +2 -0
- package/dist/cli-statusline.test.d.ts.map +1 -0
- package/dist/cli-statusline.test.js +333 -0
- package/dist/cli-statusline.test.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +350 -49
- package/dist/cli.js.map +1 -1
- package/dist/db/store.d.ts +12 -1
- package/dist/db/store.d.ts.map +1 -1
- package/dist/db/store.js +52 -0
- package/dist/db/store.js.map +1 -1
- package/dist/demo.d.ts.map +1 -1
- package/dist/demo.js +53 -0
- package/dist/demo.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +68 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/metrics/quota.d.ts +226 -0
- package/dist/metrics/quota.d.ts.map +1 -0
- package/dist/metrics/quota.js +291 -0
- package/dist/metrics/quota.js.map +1 -0
- package/dist/metrics/quota.test.d.ts +2 -0
- package/dist/metrics/quota.test.d.ts.map +1 -0
- package/dist/metrics/quota.test.js +325 -0
- package/dist/metrics/quota.test.js.map +1 -0
- package/dist/project-of.d.ts +52 -0
- package/dist/project-of.d.ts.map +1 -0
- package/dist/project-of.js +124 -0
- package/dist/project-of.js.map +1 -0
- package/dist/project-of.test.d.ts +2 -0
- package/dist/project-of.test.d.ts.map +1 -0
- package/dist/project-of.test.js +203 -0
- package/dist/project-of.test.js.map +1 -0
- package/dist/quota.d.ts +27 -0
- package/dist/quota.d.ts.map +1 -0
- package/dist/quota.js +79 -0
- package/dist/quota.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +17 -26
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +37 -0
- package/dist/server.test.js.map +1 -1
- package/dist/statusline-cache.d.ts +35 -0
- package/dist/statusline-cache.d.ts.map +1 -0
- package/dist/statusline-cache.js +57 -0
- package/dist/statusline-cache.js.map +1 -0
- package/dist/statusline-install.d.ts +52 -0
- package/dist/statusline-install.d.ts.map +1 -0
- package/dist/statusline-install.js +115 -0
- package/dist/statusline-install.js.map +1 -0
- package/dist/statusline-run.d.ts +8 -0
- package/dist/statusline-run.d.ts.map +1 -0
- package/dist/statusline-run.js +256 -0
- package/dist/statusline-run.js.map +1 -0
- package/dist/statusline.d.ts +71 -0
- package/dist/statusline.d.ts.map +1 -0
- package/dist/statusline.js +139 -0
- package/dist/statusline.js.map +1 -0
- package/dist/statusline.test.d.ts +2 -0
- package/dist/statusline.test.d.ts.map +1 -0
- package/dist/statusline.test.js +197 -0
- package/dist/statusline.test.js.map +1 -0
- package/dist/watchers/claude.d.ts +11 -1
- package/dist/watchers/claude.d.ts.map +1 -1
- package/dist/watchers/claude.js +32 -15
- package/dist/watchers/claude.js.map +1 -1
- package/package.json +1 -1
- package/web/app.css +36 -0
- package/web/app.js +61 -1
- package/web/i18n.js +28 -0
- 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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
807
|
-
|
|
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;
|