estela 0.1.38 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +17 -0
  2. package/dist/cli-badge.test.d.ts +2 -0
  3. package/dist/cli-badge.test.d.ts.map +1 -0
  4. package/dist/cli-badge.test.js +118 -0
  5. package/dist/cli-badge.test.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +130 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/export/badge.d.ts +26 -0
  10. package/dist/export/badge.d.ts.map +1 -0
  11. package/dist/export/badge.js +37 -0
  12. package/dist/export/badge.js.map +1 -0
  13. package/dist/export/badge.test.d.ts +2 -0
  14. package/dist/export/badge.test.d.ts.map +1 -0
  15. package/dist/export/badge.test.js +32 -0
  16. package/dist/export/badge.test.js.map +1 -0
  17. package/dist/export/idioma.test.js +6 -2
  18. package/dist/export/idioma.test.js.map +1 -1
  19. package/dist/export/panel.d.ts +7 -0
  20. package/dist/export/panel.d.ts.map +1 -1
  21. package/dist/export/panel.js +25 -6
  22. package/dist/export/panel.js.map +1 -1
  23. package/dist/export/panel.test.js +17 -4
  24. package/dist/export/panel.test.js.map +1 -1
  25. package/dist/export/pixelfont.d.ts +17 -0
  26. package/dist/export/pixelfont.d.ts.map +1 -0
  27. package/dist/export/pixelfont.js +157 -0
  28. package/dist/export/pixelfont.js.map +1 -0
  29. package/dist/export/png.d.ts +12 -0
  30. package/dist/export/png.d.ts.map +1 -0
  31. package/dist/export/png.js +85 -0
  32. package/dist/export/png.js.map +1 -0
  33. package/dist/export/receipt.d.ts +58 -0
  34. package/dist/export/receipt.d.ts.map +1 -0
  35. package/dist/export/receipt.js +216 -0
  36. package/dist/export/receipt.js.map +1 -0
  37. package/dist/export/receipt.test.d.ts +2 -0
  38. package/dist/export/receipt.test.d.ts.map +1 -0
  39. package/dist/export/receipt.test.js +207 -0
  40. package/dist/export/receipt.test.js.map +1 -0
  41. package/dist/i18n/en.d.ts.map +1 -1
  42. package/dist/i18n/en.js +25 -1
  43. package/dist/i18n/en.js.map +1 -1
  44. package/dist/publish.d.ts +2 -0
  45. package/dist/publish.d.ts.map +1 -1
  46. package/dist/publish.js +3 -1
  47. package/dist/publish.js.map +1 -1
  48. package/node_modules/@estela/shared/dist/money.d.ts +6 -0
  49. package/node_modules/@estela/shared/dist/money.d.ts.map +1 -1
  50. package/node_modules/@estela/shared/dist/money.js +11 -0
  51. package/node_modules/@estela/shared/dist/money.js.map +1 -1
  52. package/package.json +1 -1
package/README.md CHANGED
@@ -51,6 +51,13 @@ What it doesn't do:
51
51
  with which model.
52
52
  - **It doesn't send anything anywhere.** The free plan is entirely local.
53
53
 
54
+ ### Already using ccusage?
55
+
56
+ Keep it. [ccusage](https://github.com/ryoppippi/ccusage) reads the same files
57
+ and tells you what the AI cost you; Estela tells you how many hours you worked
58
+ and what to bill the client. Both only read, so they run side by side.
59
+ [Side-by-side comparison](https://getestela.dev/en/vs/ccusage).
60
+
54
61
  ## Getting started
55
62
 
56
63
  You need **Node 22.5 or later** (for `node:sqlite`).
@@ -248,6 +255,16 @@ Node 22 and **zero runtime dependencies**, on purpose: nobody installs a program
248
255
  that reads their transcripts if they can't audit it, and an empty dependency
249
256
  list can be audited in an afternoon.
250
257
 
258
+ ## Contributing
259
+
260
+ Issues and pull requests are welcome, and the
261
+ [good first issues](https://github.com/jdleonruiz/estela-cli/labels/good%20first%20issue)
262
+ are a good place to start. One thing to know so nobody's put off: this
263
+ repository mirrors the open part of a larger repo where the paid service also
264
+ lives, which is why each commit here is a release. I review PRs here and apply
265
+ them to the main repo keeping you as the author, and the next release commit
266
+ credits you as co-author. PRs welcome — I merge them into the main repo.
267
+
251
268
  ## Open source, closed service
252
269
 
253
270
  Everything that installs on your machine is open source under the MIT license:
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli-badge.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-badge.test.d.ts","sourceRoot":"","sources":["../src/cli-badge.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,118 @@
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
+ /**
48
+ * `estela badge`, ejecutando la CLI de verdad: lo que importa es la línea que
49
+ * alguien pega en su README, y que no cuente como medido lo que no lo es.
50
+ */
51
+ const BIN = (0, node_path_1.join)(__dirname, "bin.js");
52
+ function estela(db, ...args) {
53
+ const r = (0, node_child_process_1.spawnSync)(process.execPath, [BIN, ...args, "--db", db], {
54
+ encoding: "utf8",
55
+ env: { ...process.env, ESTELA_LANG: "es", NO_COLOR: "1" },
56
+ });
57
+ return { code: r.status, out: `${r.stdout}${r.stderr}` };
58
+ }
59
+ function bloque(id, source, hours, aiUsd) {
60
+ const startedAt = new Date("2026-08-10T10:00:00Z");
61
+ return {
62
+ id, projectId: "p", startedAt, endedAt: new Date(startedAt.getTime() + hours * 3600_000),
63
+ seconds: hours * 3600, description: "Trabajo", billable: true, invoiceId: null,
64
+ aiCost: { microUsd: aiUsd * 1_000_000 }, agentSeconds: source === "agent" ? hours * 3600 : 0,
65
+ commitHashes: [], agents: [], source, kind: "development", branch: "main",
66
+ };
67
+ }
68
+ function preparar(bloques, cuota) {
69
+ const path = (0, node_path_1.join)((0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "estela-badge-")), "estela.db");
70
+ const db = (0, schema_js_1.openDatabase)(path);
71
+ try {
72
+ store.upsertClient(db, { id: "c", name: "Interno", currency: "EUR" });
73
+ store.upsertProject(db, {
74
+ id: "p", clientId: "c", name: "Web", repoPaths: [],
75
+ billable: true, roundingMinutes: 0, aiCostPolicy: "absorbed", kind: "client",
76
+ });
77
+ for (const b of bloques)
78
+ store.saveTimeEntry(db, b);
79
+ if (cuota !== undefined) {
80
+ store.upsertSubscription(db, {
81
+ id: "max", name: "Claude Max", monthlyFee: { amount: cuota * 100, currency: "EUR" },
82
+ effectiveFrom: new Date("2026-01-01T00:00:00Z"), effectiveTo: null,
83
+ });
84
+ }
85
+ }
86
+ finally {
87
+ db.close();
88
+ }
89
+ return path;
90
+ }
91
+ (0, node_test_1.test)("badge: horas medidas y coste a tarifa API, sin contar lo estimado", () => {
92
+ const db = preparar([bloque("a", "agent", 42, 18.4), bloque("b", "commit", 5, 0)]);
93
+ const r = estela(db, "badge", "--project", "p");
94
+ strict_1.default.equal(r.code, 0, r.out);
95
+ strict_1.default.match(r.out, /^\[!\[Built in 42 h · \$18 of AI\]\(https:\/\/img\.shields\.io\/badge\/[^)]+\)\]\(https:\/\/getestela\.dev\/en\/\)$/m);
96
+ strict_1.default.match(r.out, /tarifa API/);
97
+ });
98
+ (0, node_test_1.test)("badge: con una cuota registrada, sale lo que se pagó de verdad", () => {
99
+ // Único proyecto con consumo ese mes: se lleva la cuota entera.
100
+ const db = preparar([bloque("a", "agent", 10, 50)], 100);
101
+ const r = estela(db, "badge", "--project", "p");
102
+ strict_1.default.equal(r.code, 0, r.out);
103
+ strict_1.default.match(r.out, /Built in 10 h · €100 of AI/);
104
+ strict_1.default.doesNotMatch(r.out, /tarifa API/);
105
+ });
106
+ (0, node_test_1.test)("badge: sin horas medidas no se inventa un badge", () => {
107
+ const db = preparar([bloque("b", "commit", 5, 0)]);
108
+ const r = estela(db, "badge", "--project", "p");
109
+ strict_1.default.notEqual(r.code, 0);
110
+ strict_1.default.match(r.out, /no tiene horas medidas/);
111
+ });
112
+ (0, node_test_1.test)("badge: un proyecto que no existe se dice así", () => {
113
+ const db = preparar([]);
114
+ const r = estela(db, "badge", "--project", "nada");
115
+ strict_1.default.notEqual(r.code, 0);
116
+ strict_1.default.match(r.out, /No existe el proyecto "nada"/);
117
+ });
118
+ //# sourceMappingURL=cli-badge.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-badge.test.js","sourceRoot":"","sources":["../src/cli-badge.test.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAyCA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAu3DpD,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;AAs/DpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
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");
@@ -141,6 +143,14 @@ estela — registro de horas para desarrollo asistido por IA
141
143
  estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
142
144
  Pone a mano los tickets de un bloque. El import ya no los cambia.
143
145
  estela ai-cost Reparto de tu cuota entre proyectos.
146
+ estela badge [--project <id>]
147
+ Badge para el README: horas medidas y coste de IA del proyecto.
148
+ Sin --project, el del repositorio en el que estás.
149
+ estela receipt [--week] [--names] [--out <fichero.png>]
150
+ Un recibo en PNG: horas medidas y estimadas, coste de IA, lo más
151
+ caro y lo que ahorró la caché. Con --week, los últimos 7 días.
152
+ Sin nombres de proyectos ni ramas salvo con --names. Se genera y
153
+ se queda en tu máquina.
144
154
  estela share --project <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
145
155
  [--author <tu nombre>] [--with-amounts] [--out <fichero.html>]
146
156
  Informe compartible en un solo fichero HTML. Horas y commits; sin
@@ -148,12 +158,15 @@ estela — registro de horas para desarrollo asistido por IA
148
158
 
149
159
  estela publish --project <id> [--author <tu nombre>] [--with-amounts]
150
160
  [--token <existente>] [--no-team] [--client <correo>[,otro]]
161
+ [--no-credit]
151
162
  Panel de solo lectura para que tu cliente USE el producto, no solo
152
163
  reciba un documento. Se aloja en getestela.dev — Free permite uno a
153
164
  la vez, Pro y Teams sin límite. Necesita "estela login" antes.
154
165
  Con --client se le avisa por correo y le aparece en su cuenta al
155
166
  entrar en getestela.dev/app. Sin --client no se toca esa lista: tus
156
167
  republicaciones no desvinculan a quien ya lo tenía.
168
+ --no-credit quita la línea "Horas rellenadas por Estela" del pie
169
+ (Pro y Teams).
157
170
 
158
171
  estela team ai-cost --project <id> --allow | --deny
159
172
  Si quien te invitó pide ver el coste de IA del proyecto, esta es tu
@@ -289,6 +302,14 @@ estela — time tracking for AI-assisted development
289
302
  estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
290
303
  Sets a block's tickets by hand. Imports won't change them anymore.
291
304
  estela ai-cost How your subscription splits across projects.
305
+ estela badge [--project <id>]
306
+ Badge for your README: the project's measured hours and AI cost.
307
+ Without --project, the one for the repository you're in.
308
+ estela receipt [--week] [--names] [--out <file.png>]
309
+ A PNG receipt: measured and estimated hours, AI cost, the most
310
+ expensive work and what caching saved. With --week, the last 7 days.
311
+ No project or branch names unless you pass --names. It's generated
312
+ and stays on your machine.
292
313
  estela share --project <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
293
314
  [--author <your name>] [--with-amounts] [--out <file.html>]
294
315
  Shareable report in a single HTML file. Hours and commits; no AI
@@ -296,12 +317,15 @@ estela — time tracking for AI-assisted development
296
317
 
297
318
  estela publish --project <id> [--author <your name>] [--with-amounts]
298
319
  [--token <existing>] [--no-team] [--client <email>[,other]]
320
+ [--no-credit]
299
321
  Read-only dashboard so your client can USE the product, not just
300
322
  receive a document. Hosted on getestela.dev — Free allows one at a
301
323
  time, Pro and Teams have no limit. Requires "estela login" first.
302
324
  With --client they get an email and it shows up in their account
303
325
  when they sign in to getestela.dev/app. Without --client that list
304
326
  isn't touched: republishing never unlinks anyone who already had it.
327
+ --no-credit removes the "Hours filled in by Estela" line from the
328
+ footer (Pro and Teams).
305
329
 
306
330
  estela team ai-cost --project <id> --allow | --deny
307
331
  If whoever invited you asks to see the project's AI cost, this is
@@ -1281,6 +1305,7 @@ async function cmdPublish(args, dbPath) {
1281
1305
  ...(clientes?.length ? { clients: clientes } : {}),
1282
1306
  withAmounts: args.flags["with-amounts"] === true,
1283
1307
  includeTeam: args.flags["no-team"] !== true,
1308
+ hideCredit: args.flags["no-credit"] === true,
1284
1309
  ...(explicitLang(args) ? { language: explicitLang(args) } : {}),
1285
1310
  });
1286
1311
  }
@@ -1820,6 +1845,105 @@ function cmdAiCost(dbPath) {
1820
1845
  db.close();
1821
1846
  }
1822
1847
  }
1848
+ /**
1849
+ * El proyecto de un comando: el de --project, o el del repositorio en el que
1850
+ * se está. Es lo natural para lo que se pega en el README de ese repositorio.
1851
+ */
1852
+ async function projectFromArgsOrCwd(args, db) {
1853
+ const explicit = str(args, "project");
1854
+ if (explicit) {
1855
+ if (!store.getProject(db, explicit))
1856
+ throw new UserError((0, index_js_1.tr) `No existe el proyecto "${explicit}".`);
1857
+ return explicit;
1858
+ }
1859
+ const root = await (0, git_js_1.repoRoot)(process.cwd());
1860
+ const id = root ? store.projectForRepo(db, root) : null;
1861
+ if (!id)
1862
+ throw new UserError((0, index_js_1.tr) `Este directorio no es de ningún proyecto. Indica cuál con --project <id>.`);
1863
+ return id;
1864
+ }
1865
+ /**
1866
+ * Lo que costó la IA de un proyecto, en dinero de verdad si se puede.
1867
+ *
1868
+ * Con una cuota registrada, es su reparto (lo que se pagó); sin ella, la
1869
+ * tarifa API equivalente, que es lo que valdría ese uso y así se marca. Los
1870
+ * meses sin cuota vigente no suman: ahí no hay reparto que hacer. `months`
1871
+ * limita a esos meses; sin él, cuenta toda la historia del proyecto.
1872
+ */
1873
+ function projectAiCost(db, projectId, entries, months) {
1874
+ const subs = store.listSubscriptions(db);
1875
+ if (subs.length) {
1876
+ const shares = (0, amortize_js_1.amortize)(store.consumptionByProjectMonth(db), subs, store.totalConsumptionByMonth(db))
1877
+ .filter((sh) => sh.projectId === projectId && (0, amortize_js_1.feeForMonth)(subs, sh.month) !== null &&
1878
+ (!months || months.includes(sh.month)));
1879
+ if (shares.length) {
1880
+ const currency = shares[0].amount.currency;
1881
+ return { money: { amount: shares.reduce((n, sh) => n + sh.amount.amount, 0), currency }, api: false };
1882
+ }
1883
+ }
1884
+ const micro = entries.reduce((n, e) => n + e.aiCost.microUsd, 0);
1885
+ if (micro === 0)
1886
+ return null;
1887
+ return { money: { amount: Math.round(micro / 10_000), currency: "USD" }, api: true };
1888
+ }
1889
+ async function cmdBadge(args, dbPath) {
1890
+ const db = (0, schema_js_1.openDatabase)(dbPath);
1891
+ try {
1892
+ const projectId = await projectFromArgsOrCwd(args, db);
1893
+ const entries = store.getTimeEntries(db, projectId);
1894
+ const measuredSeconds = entries.filter((e) => e.source === "agent").reduce((n, e) => n + e.seconds, 0);
1895
+ if (measuredSeconds === 0) {
1896
+ throw new UserError((0, index_js_1.tr) `"${projectId}" no tiene horas medidas todavía: el badge solo cuenta las de sesiones de agente.`);
1897
+ }
1898
+ const cost = projectAiCost(db, projectId, entries);
1899
+ const text = (0, badge_js_1.badgeText)({ measuredSeconds, aiCost: cost ? (0, shared_1.formatMoneyWhole)(cost.money) : null });
1900
+ console.log((0, badge_js_1.badgeMarkdown)(text));
1901
+ console.log("");
1902
+ console.log((0, index_js_1.tr) `Vista previa: ${(0, badge_js_1.badgeUrl)(text)}`);
1903
+ if (cost?.api) {
1904
+ 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:`);
1905
+ console.log((0, index_js_1.tr) ` estela subscription add --id claude-max --name "Claude Max" --fee 200`);
1906
+ }
1907
+ console.log((0, index_js_1.tr) `Es una imagen estática: para actualizar las cifras, vuelve a generarlo.`);
1908
+ }
1909
+ finally {
1910
+ db.close();
1911
+ }
1912
+ }
1913
+ function cmdReceipt(args, dbPath) {
1914
+ const db = (0, schema_js_1.openDatabase)(dbPath);
1915
+ try {
1916
+ const week = args.flags["week"] === true;
1917
+ // Hasta el final de hoy, en la hora de quien lo pide; con --week, desde
1918
+ // el principio del día de hace seis: siete días naturales contando hoy.
1919
+ const hoy = new Date();
1920
+ const to = new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate() + 1);
1921
+ const from = week ? new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate() - 6) : null;
1922
+ const projects = store.listProjects(db);
1923
+ const entries = projects.flatMap((p) => store.getTimeEntries(db, p.id));
1924
+ if (entries.length === 0)
1925
+ throw new UserError((0, index_js_1.tr) `Sin bloques imputados. Ejecuta: estela import`);
1926
+ const turns = db.prepare(`SELECT at, model, tok_cache_read, tok_cache_w5m, tok_cache_w1h FROM agent_turns
1927
+ WHERE at >= ? AND at < ?`).all((from ?? new Date(0)).toISOString(), to.toISOString()).map((t) => ({
1928
+ at: new Date(t.at), model: t.model,
1929
+ cacheRead: t.tok_cache_read, cacheWrite5m: t.tok_cache_w5m, cacheWrite1h: t.tok_cache_w1h,
1930
+ }));
1931
+ const data = (0, receipt_js_1.receiptData)({
1932
+ entries, turns, from, to, projectNames: new Map(projects.map((p) => [p.id, p.name])),
1933
+ });
1934
+ const out = (0, node_path_1.resolve)(str(args, "out") ??
1935
+ (week ? `estela-receipt-${(0, shared_1.localDate)(hoy)}.png` : "estela-receipt.png"));
1936
+ const names = args.flags["names"] === true;
1937
+ (0, node_fs_1.writeFileSync)(out, (0, receipt_js_1.renderReceipt)(data, { week, names }));
1938
+ console.log((0, index_js_1.tr) `Recibo guardado en ${out}`);
1939
+ if (!names)
1940
+ console.log((0, index_js_1.tr) `Sin nombres de proyectos ni ramas, para que puedas compartirlo. Con --names, se incluyen.`);
1941
+ console.log((0, index_js_1.tr) `Se ha generado en tu máquina y no se ha enviado a ningún sitio.`);
1942
+ }
1943
+ finally {
1944
+ db.close();
1945
+ }
1946
+ }
1823
1947
  function cmdInvoice(args, dbPath) {
1824
1948
  const db = (0, schema_js_1.openDatabase)(dbPath);
1825
1949
  try {
@@ -1982,6 +2106,12 @@ async function main() {
1982
2106
  case "ai-cost":
1983
2107
  cmdAiCost(dbPath);
1984
2108
  break;
2109
+ case "badge":
2110
+ await cmdBadge(args, dbPath);
2111
+ break;
2112
+ case "receipt":
2113
+ cmdReceipt(args, dbPath);
2114
+ break;
1985
2115
  case "status":
1986
2116
  cmdStatus(dbPath);
1987
2117
  break;