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