estela 0.1.39 → 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.
package/README.md CHANGED
@@ -189,6 +189,35 @@ different machine, pass `--token` with the existing one, or your client ends up
189
189
  with a dead link. The Free plan allows one published dashboard at a time; Pro
190
190
  and Teams have no limit.
191
191
 
192
+ The dashboard's footer says "Hours filled in by Estela", linking to
193
+ getestela.dev. On Pro and Teams, `--no-credit` removes that line.
194
+
195
+ ## Showing what you built
196
+
197
+ A badge for your project's README, with its measured hours and what the AI
198
+ cost:
199
+
200
+ ```sh
201
+ estela badge # the project for the repo you're in
202
+ estela badge --project acme-web
203
+ ```
204
+
205
+ It prints a Markdown line with a static [shields.io](https://shields.io) image,
206
+ something like `Built in 42 h · $18 of AI`. Nothing is hosted and nothing is
207
+ looked up: the numbers are baked into the URL, so run it again to update them.
208
+ It only counts **measured** hours, never the ones estimated from commits.
209
+
210
+ And a receipt of your week (or of your whole history) as a PNG:
211
+
212
+ ```sh
213
+ estela receipt --week
214
+ ```
215
+
216
+ Measured versus estimated hours, the AI cost, what caching saved you, and the
217
+ most expensive project and branch. It's drawn on your machine and stays there.
218
+ Project and branch names are left out by default, because they're usually a
219
+ client's; add `--names` to include them.
220
+
192
221
  ## What the AI actually costs
193
222
 
194
223
  On a flat subscription, your real spend isn't the sum of the tokens: it's the
@@ -202,6 +231,30 @@ estela ai-cost
202
231
  Cache is counted separately because it's usually most of the bill — ignoring it
203
232
  underestimated spend fivefold.
204
233
 
234
+ ## Which project is eating your quota
235
+
236
+ On Claude Max/Pro or Codex with a subscription, the quota runs out before you
237
+ expect it and nothing tells you where it went. `estela quota` splits it by
238
+ project:
239
+
240
+ ```sh
241
+ estela quota # current 5-hour window and this week, per project
242
+ estela quota --json # the same, for scripts
243
+ ```
244
+
245
+ Tokens (input, output, cache read and cache write) per project, for the current
246
+ 5-hour window and for this week (Monday to today, in your time zone), plus this
247
+ week against the average of your previous 4 over the same stretch. Claude Code
248
+ and Codex are shown separately: they draw on different quotas. It's also a card
249
+ in the Summary tab of `estela web`, and it's free.
250
+
251
+ What it never does is tell you how much you have left: Anthropic and OpenAI
252
+ don't publish their plans' limits, so any "X tokens left" would be made up. The
253
+ 5-hour window is an estimate — how its start is rounded isn't public, and
254
+ anything you use on claude.ai or another machine counts too but isn't on this
255
+ one. Copilot is left out because it doesn't record reliable tokens. Everything
256
+ is calculated on your machine.
257
+
205
258
  ## Hours no import will infer
206
259
 
207
260
  Meetings, travel, research, and development without an agent that didn't leave
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli-quota.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-quota.test.d.ts","sourceRoot":"","sources":["../src/cli-quota.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,216 @@
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
+ const quota_js_1 = require("./metrics/quota.js");
48
+ /**
49
+ * `estela quota`, ejecutando la CLI de verdad.
50
+ *
51
+ * Cada prueba corre con un HOME temporal: el refresco lee los transcripts de
52
+ * ~/.claude, y un test no puede ni leer los de quien lo ejecuta ni depender de
53
+ * que existan.
54
+ */
55
+ const BIN = (0, node_path_1.join)(__dirname, "bin.js");
56
+ function estela(home, lang, ...args) {
57
+ const r = (0, node_child_process_1.spawnSync)(process.execPath, [BIN, ...args], {
58
+ encoding: "utf8",
59
+ env: { ...process.env, HOME: home, USERPROFILE: home, ESTELA_LANG: lang, NO_COLOR: "1" },
60
+ });
61
+ return { code: r.status, out: r.stdout, err: r.stderr };
62
+ }
63
+ /**
64
+ * Datos fijos para la cuota, válidos a cualquier hora de cualquier día.
65
+ *
66
+ * No sirve la demo: pone las sesiones de Codex por la tarde, y un lunes por
67
+ * la mañana la semana aún no tiene ninguna — estos tests fallaban según el
68
+ * momento en que se ejecutaran. Aquí todo cae a mitad de camino entre el
69
+ * lunes a las 00:00 y ahora, que siempre es esta semana.
70
+ */
71
+ function sembrarCuota(dbPath) {
72
+ const ahora = new Date();
73
+ const lunes = (0, quota_js_1.weekStart)(ahora);
74
+ const enEstaSemana = new Date((lunes.getTime() + ahora.getTime()) / 2);
75
+ const haceSemanas = (n) => new Date(lunes.getTime() - n * 7 * 86_400_000 + 3600_000);
76
+ let n = 0;
77
+ const t = (agent, at, repo, cacheRead) => ({
78
+ agent, turnId: `q${n++}`, sessionId: "s", at, model: "m", repoPath: repo, branch: "main",
79
+ tokens: { input: 1, output: 10, cacheRead, cacheWrite5m: 0, cacheWrite1h: 0 }, producerVersion: null,
80
+ });
81
+ const db = (0, schema_js_1.openDatabase)(dbPath);
82
+ try {
83
+ store.upsertClient(db, { id: "c", name: "C", currency: "EUR" });
84
+ for (const id of ["web", "api"]) {
85
+ store.upsertProject(db, { id, clientId: "c", name: id, repoPaths: [`/repos/${id}`], billable: true,
86
+ roundingMinutes: 0, aiCostPolicy: "absorbed", kind: "client" });
87
+ }
88
+ store.saveTurns(db, [
89
+ // Claude Code: seis semanas de historial, así que compara.
90
+ t("claude-code", haceSemanas(6), "/repos/web", 100),
91
+ t("claude-code", enEstaSemana, "/repos/web", 3000),
92
+ t("claude-code", enEstaSemana, "/repos/api", 1000),
93
+ // Codex: empezó esta semana, así que no hay con qué comparar.
94
+ t("codex", enEstaSemana, "/repos/api", 500),
95
+ ]);
96
+ }
97
+ finally {
98
+ db.close();
99
+ }
100
+ }
101
+ function entorno() {
102
+ const home = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "estela-quota-"));
103
+ return { home, db: (0, node_path_1.join)(home, "estela.db") };
104
+ }
105
+ /** Un transcript de Claude Code con un turno de hace media hora, en `cwd`. */
106
+ function transcript(home, cwd, id, cacheRead) {
107
+ const dir = (0, node_path_1.join)(home, ".claude", "projects", "-proyecto");
108
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
109
+ const at = new Date(Date.now() - 30 * 60_000).toISOString();
110
+ const linea = JSON.stringify({
111
+ type: "assistant", timestamp: at, cwd, gitBranch: "main", version: "2.1.238", sessionId: "s1",
112
+ message: { id, model: "claude-sonnet-4-6",
113
+ usage: { input_tokens: 10, output_tokens: 100, cache_read_input_tokens: cacheRead,
114
+ cache_creation_input_tokens: 0 } },
115
+ });
116
+ // Dos veces la misma fila: Claude Code repite el turno en el transcript.
117
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, "sesion.jsonl"), `${linea}\n${linea}\n`);
118
+ }
119
+ (0, node_test_1.test)("sin nada guardado, lo dice y sugiere setup en vez de enseñar una tabla vacía", () => {
120
+ const { home, db } = entorno();
121
+ const r = estela(home, "es", "quota", "--db", db, "--no-refresh");
122
+ strict_1.default.equal(r.code, 0, r.err);
123
+ strict_1.default.match(r.out, /Sin turnos de agente guardados\. Empieza con: estela setup/);
124
+ strict_1.default.match(r.out, /no es el límite de tu plan/);
125
+ });
126
+ (0, node_test_1.test)("refresca los transcripts de esta semana antes de calcular, y un turno repetido cuenta una vez", () => {
127
+ const { home, db } = entorno();
128
+ transcript(home, "/repos/web", "msg_cuota_1", 5000);
129
+ const r = estela(home, "es", "quota", "--db", db, "--json");
130
+ strict_1.default.equal(r.code, 0, r.err);
131
+ const j = JSON.parse(r.out);
132
+ strict_1.default.equal(j.agents[0].agent, "claude-code");
133
+ // 10 + 100 + 5000, una sola vez aunque el transcript traiga la fila dos veces.
134
+ strict_1.default.equal(j.agents[0].week.total.total, 5110);
135
+ // Repetirlo no duplica: el turno ya está guardado.
136
+ const otra = JSON.parse(estela(home, "es", "quota", "--db", db, "--json").out);
137
+ strict_1.default.equal(otra.agents[0].week.total.total, 5110);
138
+ });
139
+ (0, node_test_1.test)("--no-refresh no lee transcripts: usa solo lo ya guardado", () => {
140
+ const { home, db } = entorno();
141
+ transcript(home, "/repos/web", "msg_cuota_2", 5000);
142
+ const r = estela(home, "es", "quota", "--db", db, "--no-refresh", "--json");
143
+ strict_1.default.equal(r.code, 0, r.err);
144
+ strict_1.default.deepEqual(JSON.parse(r.out).agents, []);
145
+ });
146
+ (0, node_test_1.test)("sobre la base de la demo nunca lee tus transcripts, aunque no se pase --no-refresh", () => {
147
+ // Mezclaría tu trabajo real con los datos inventados, justo lo que la demo
148
+ // promete no hacer.
149
+ const { home } = entorno();
150
+ const demo = (0, node_path_1.join)(home, "estela-demo.db");
151
+ (0, schema_js_1.openDatabase)(demo).close();
152
+ transcript(home, "/repos/web", "msg_real", 5000);
153
+ const r = estela(home, "es", "quota", "--db", demo, "--json");
154
+ strict_1.default.equal(r.code, 0, r.err);
155
+ const conn = (0, schema_js_1.openDatabase)(demo);
156
+ try {
157
+ strict_1.default.equal(store.firstTurnByAgent(conn).size, 0, "no se guardó ningún turno real en la demo");
158
+ }
159
+ finally {
160
+ conn.close();
161
+ }
162
+ });
163
+ (0, node_test_1.test)("--json es JSON y nada más: ni avisos ni cabeceras en la salida", () => {
164
+ const { home, db } = entorno();
165
+ sembrarCuota(db);
166
+ const r = estela(home, "es", "quota", "--db", db, "--no-refresh", "--json");
167
+ strict_1.default.equal(r.code, 0, r.err);
168
+ const j = JSON.parse(r.out);
169
+ strict_1.default.equal(j.windowIsEstimate, true);
170
+ strict_1.default.equal(j.windowHours, 5);
171
+ strict_1.default.deepEqual(j.agents.map((a) => a.agent), ["claude-code", "codex"]);
172
+ // Seis semanas de Claude Code y Codex recién empezado: uno compara, el otro no.
173
+ strict_1.default.equal(j.agents[0].comparison.status, "ok");
174
+ strict_1.default.equal(j.agents[1].comparison.status, "insufficient");
175
+ const shares = j.agents[0].week.projects.map((p) => p.share);
176
+ strict_1.default.ok(Math.abs(shares.reduce((s, x) => s + x, 0) - 1) < 0.001, "los porcentajes suman el 100 %");
177
+ });
178
+ (0, node_test_1.test)("la salida en texto, en los dos idiomas, sin promesas sobre lo que queda", () => {
179
+ const { home, db } = entorno();
180
+ sembrarCuota(db);
181
+ const es = estela(home, "es", "quota", "--db", db, "--no-refresh").out;
182
+ strict_1.default.match(es, /Claude Code — esta semana \(lun \d+ \w+ → hoy\)/);
183
+ strict_1.default.match(es, /Proyecto\s+Entrada\s+Salida\s+Lect\. caché\s+Escr\. caché\s+Total\s+%/);
184
+ strict_1.default.match(es, /Sin historial suficiente para comparar: hacen falta 4 semanas completas y hay \d\./);
185
+ strict_1.default.match(es, /La ventana de 5 h es una estimación/);
186
+ const en = estela(home, "en", "quota", "--db", db, "--no-refresh").out;
187
+ strict_1.default.match(en, /Claude Code — this week \(Mon \w+ \d+ → today\)/);
188
+ strict_1.default.match(en, /Project\s+Input\s+Output\s+Cache read\s+Cache write\s+Total\s+%/);
189
+ strict_1.default.match(en, /The 5-hour window is an estimate/);
190
+ // La regla que no se rompe: nada de "te quedan" ni de cuándo se acaba.
191
+ for (const out of [es, en]) {
192
+ strict_1.default.doesNotMatch(out, /te quedan|quedan \d|se te acaba|you have \d|left until|runs out/i);
193
+ }
194
+ });
195
+ (0, node_test_1.test)("Copilot queda fuera, y se dice en una línea", () => {
196
+ const { home, db } = entorno();
197
+ const conn = (0, schema_js_1.openDatabase)(db);
198
+ try {
199
+ store.saveTurns(conn, [{
200
+ agent: "copilot", turnId: "copilot:1:end", sessionId: "s", at: new Date(Date.now() - 60_000),
201
+ model: "gpt-4o", repoPath: "/repos/web", branch: null,
202
+ tokens: { input: 900, output: 100, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0 },
203
+ producerVersion: null,
204
+ }]);
205
+ }
206
+ finally {
207
+ conn.close();
208
+ }
209
+ const r = estela(home, "es", "quota", "--db", db, "--no-refresh");
210
+ strict_1.default.match(r.out, /Copilot no guarda tokens fiables y queda fuera de este cálculo\./);
211
+ strict_1.default.match(r.out, /Sin consumo de Claude Code ni Codex esta semana\./);
212
+ const j = JSON.parse(estela(home, "es", "quota", "--db", db, "--no-refresh", "--json").out);
213
+ strict_1.default.deepEqual(j.agents, []);
214
+ strict_1.default.deepEqual(j.excluded.map((e) => e.agent), ["copilot"]);
215
+ });
216
+ //# sourceMappingURL=cli-quota.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-quota.test.js","sourceRoot":"","sources":["../src/cli-quota.test.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,gEAAwC;AACxC,2DAA+C;AAC/C,qCAAgE;AAChE,qCAAiC;AACjC,yCAAiC;AACjC,yCAAiC;AAIjC,8CAA8C;AAC9C,qDAAuC;AACvC,iDAA+C;AAE/C;;;;;;GAMG;AAEH,MAAM,GAAG,GAAG,IAAA,gBAAI,EAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAEtC,SAAS,MAAM,CAAC,IAAY,EAAE,IAAiB,EAAE,GAAG,IAAc;IAChE,MAAM,CAAC,GAAG,IAAA,8BAAS,EAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,EAAE;QACpD,QAAQ,EAAE,MAAM;QAChB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE;KACzF,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;AAC1D,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,YAAY,CAAC,MAAc;IAClC,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,IAAA,oBAAS,EAAC,KAAK,CAAC,CAAC;IAC/B,MAAM,YAAY,GAAG,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvE,MAAM,WAAW,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,UAAU,GAAG,QAAQ,CAAC,CAAC;IAC7F,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,MAAM,CAAC,GAAG,CAAC,KAAgB,EAAE,EAAQ,EAAE,IAAY,EAAE,SAAiB,EAAa,EAAE,CAAC,CAAC;QACrF,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM;QACxF,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,eAAe,EAAE,IAAI;KACrG,CAAC,CAAC;IACH,MAAM,EAAE,GAAG,IAAA,wBAAY,EAAC,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC;QACH,KAAK,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,KAAK,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;YAChC,KAAK,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI;gBAChG,eAAe,EAAE,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACpE,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE;YAClB,2DAA2D;YAC3D,CAAC,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,CAAC;YACnD,CAAC,CAAC,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,IAAI,CAAC;YAClD,CAAC,CAAC,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,IAAI,CAAC;YAClD,8DAA8D;YAC9D,CAAC,CAAC,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,CAAC;SAC5C,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAAC,CAAC;AAC3B,CAAC;AAED,SAAS,OAAO;IACd,MAAM,IAAI,GAAG,IAAA,qBAAW,EAAC,IAAA,gBAAI,EAAC,IAAA,gBAAM,GAAE,EAAE,eAAe,CAAC,CAAC,CAAC;IAC1D,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAA,gBAAI,EAAC,IAAI,EAAE,WAAW,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,8EAA8E;AAC9E,SAAS,UAAU,CAAC,IAAY,EAAE,GAAW,EAAE,EAAU,EAAE,SAAiB;IAC1E,MAAM,GAAG,GAAG,IAAA,gBAAI,EAAC,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;IAC3D,IAAA,mBAAS,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5D,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3B,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI;QAC7F,OAAO,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,mBAAmB;YACvC,KAAK,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,uBAAuB,EAAE,SAAS;gBACxE,2BAA2B,EAAE,CAAC,EAAE,EAAE;KAC9C,CAAC,CAAC;IACH,yEAAyE;IACzE,IAAA,uBAAa,EAAC,IAAA,gBAAI,EAAC,GAAG,EAAE,cAAc,CAAC,EAAE,GAAG,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC;AACnE,CAAC;AAED,IAAA,gBAAI,EAAC,8EAA8E,EAAE,GAAG,EAAE;IACxF,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;IAC/B,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,CAAC,CAAC;IAClE,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;IAC/B,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,4DAA4D,CAAC,CAAC;IAClF,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,4BAA4B,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,IAAA,gBAAI,EAAC,+FAA+F,EAAE,GAAG,EAAE;IACzG,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;IAC/B,UAAU,CAAC,IAAI,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC;IAEpD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;IAC5D,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAwE,CAAC;IACnG,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;IAChD,+EAA+E;IAC/E,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAElD,mDAAmD;IACnD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAa,CAAC;IAC3F,gBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACvD,CAAC,CAAC,CAAC;AAEH,IAAA,gBAAI,EAAC,0DAA0D,EAAE,GAAG,EAAE;IACpE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;IAC/B,UAAU,CAAC,IAAI,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC;IACpD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;IAC5E,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;IAC/B,gBAAM,CAAC,SAAS,CAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAA2B,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC5E,CAAC,CAAC,CAAC;AAEH,IAAA,gBAAI,EAAC,oFAAoF,EAAE,GAAG,EAAE;IAC9F,2EAA2E;IAC3E,oBAAoB;IACpB,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,IAAA,gBAAI,EAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAC1C,IAAA,wBAAY,EAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;IAC3B,UAAU,CAAC,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;IAEjD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC9D,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAA,wBAAY,EAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC;QACH,gBAAM,CAAC,KAAK,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,2CAA2C,CAAC,CAAC;IAClG,CAAC;YAAS,CAAC;QAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEH,IAAA,gBAAI,EAAC,gEAAgE,EAAE,GAAG,EAAE;IAC1E,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;IAC/B,YAAY,CAAC,EAAE,CAAC,CAAC;IAEjB,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;IAC5E,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAIzB,CAAC;IACF,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC;IACvC,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC/B,gBAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC;IACzE,gFAAgF;IAChF,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACnD,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,UAAU,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC9D,gBAAM,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,EAAE,gCAAgC,CAAC,CAAC;AACvG,CAAC,CAAC,CAAC;AAEH,IAAA,gBAAI,EAAC,yEAAyE,EAAE,GAAG,EAAE;IACnF,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;IAC/B,YAAY,CAAC,EAAE,CAAC,CAAC;IAEjB,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,CAAC,CAAC,GAAG,CAAC;IACvE,gBAAM,CAAC,KAAK,CAAC,EAAE,EAAE,iDAAiD,CAAC,CAAC;IACpE,gBAAM,CAAC,KAAK,CAAC,EAAE,EAAE,uEAAuE,CAAC,CAAC;IAC1F,gBAAM,CAAC,KAAK,CAAC,EAAE,EAAE,oFAAoF,CAAC,CAAC;IACvG,gBAAM,CAAC,KAAK,CAAC,EAAE,EAAE,qCAAqC,CAAC,CAAC;IAExD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,CAAC,CAAC,GAAG,CAAC;IACvE,gBAAM,CAAC,KAAK,CAAC,EAAE,EAAE,iDAAiD,CAAC,CAAC;IACpE,gBAAM,CAAC,KAAK,CAAC,EAAE,EAAE,iEAAiE,CAAC,CAAC;IACpF,gBAAM,CAAC,KAAK,CAAC,EAAE,EAAE,kCAAkC,CAAC,CAAC;IAErD,uEAAuE;IACvE,KAAK,MAAM,GAAG,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;QAC3B,gBAAM,CAAC,YAAY,CAAC,GAAG,EAAE,kEAAkE,CAAC,CAAC;IAC/F,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAA,gBAAI,EAAC,6CAA6C,EAAE,GAAG,EAAE;IACvD,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAA,wBAAY,EAAC,EAAE,CAAC,CAAC;IAC9B,IAAI,CAAC;QACH,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC;gBAC5F,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI;gBACrD,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE;gBACnF,eAAe,EAAE,IAAI;aACtB,CAAC,CAAC,CAAC;IACN,CAAC;YAAS,CAAC;QAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IAAC,CAAC;IAE3B,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,CAAC,CAAC;IAClE,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,kEAAkE,CAAC,CAAC;IACxF,gBAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,mDAAmD,CAAC,CAAC;IACzE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC,GAAG,CAEzF,CAAC;IACF,gBAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC/B,gBAAM,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;AAChE,CAAC,CAAC,CAAC"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA6CA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAs/DpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA6CA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAsoEpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -75,6 +75,9 @@ const agents_js_1 = require("./watchers/agents.js");
75
75
  const git_js_1 = require("./watchers/git.js");
76
76
  const identity_js_1 = require("./watchers/identity.js");
77
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");
78
81
  const labels_js_1 = require("./i18n/labels.js");
79
82
  const HELP_ES = `
80
83
  estela — registro de horas para desarrollo asistido por IA
@@ -143,6 +146,17 @@ estela — registro de horas para desarrollo asistido por IA
143
146
  estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
144
147
  Pone a mano los tickets de un bloque. El import ya no los cambia.
145
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.
146
160
  estela badge [--project <id>]
147
161
  Badge para el README: horas medidas y coste de IA del proyecto.
148
162
  Sin --project, el del repositorio en el que estás.
@@ -302,6 +316,18 @@ estela — time tracking for AI-assisted development
302
316
  estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
303
317
  Sets a block's tickets by hand. Imports won't change them anymore.
304
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.
305
331
  estela badge [--project <id>]
306
332
  Badge for your README: the project's measured hours and AI cost.
307
333
  Without --project, the one for the repository you're in.
@@ -1910,6 +1936,115 @@ async function cmdBadge(args, dbPath) {
1910
1936
  db.close();
1911
1937
  }
1912
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
+ }
1913
2048
  function cmdReceipt(args, dbPath) {
1914
2049
  const db = (0, schema_js_1.openDatabase)(dbPath);
1915
2050
  try {
@@ -2106,6 +2241,9 @@ async function main() {
2106
2241
  case "ai-cost":
2107
2242
  cmdAiCost(dbPath);
2108
2243
  break;
2244
+ case "quota":
2245
+ await cmdQuota(args, dbPath);
2246
+ break;
2109
2247
  case "badge":
2110
2248
  await cmdBadge(args, dbPath);
2111
2249
  break;