estela 0.2.4 → 0.3.2
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 +55 -0
- package/dist/billing/assign.d.ts +11 -1
- package/dist/billing/assign.js +14 -0
- package/dist/billing/sessionize.d.ts +5 -0
- package/dist/billing/sessionize.js +11 -1
- package/dist/cli.js +127 -4
- package/dist/db/schema.js +10 -0
- package/dist/db/store.d.ts +9 -0
- package/dist/db/store.js +24 -0
- package/dist/export/invoice-pdf.d.ts +2 -0
- package/dist/export/invoice-pdf.js +26 -16
- package/dist/i18n/en.js +45 -0
- package/dist/metrics/plan.d.ts +110 -0
- package/dist/metrics/plan.js +175 -0
- package/dist/plan-blocks.d.ts +26 -0
- package/dist/plan-blocks.js +110 -0
- package/dist/plan-prices.d.ts +27 -0
- package/dist/plan-prices.js +29 -0
- package/dist/plan-report.d.ts +96 -0
- package/dist/plan-report.js +190 -0
- package/dist/server.js +13 -7
- package/package.json +1 -1
- package/web/app.css +1 -0
- package/web/app.js +10 -5
- package/web/i18n.js +6 -4
package/README.md
CHANGED
|
@@ -273,6 +273,61 @@ anything you use on claude.ai or another machine counts too but isn't on this
|
|
|
273
273
|
one. Copilot is left out because it doesn't record reliable tokens. Everything
|
|
274
274
|
is calculated on your machine.
|
|
275
275
|
|
|
276
|
+
## Is your Claude plan the right one?
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
estela plan set max5 # pro | max5 | max20; add --since YYYY-MM-DD if you had it before today
|
|
280
|
+
estela plan # last 28 days; --days N, --json, --no-color
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Answers "is the Claude plan I pay for too big or too small?" with your own data:
|
|
284
|
+
the 5-hour and weekly percentages Claude Code sends to the status line (stored
|
|
285
|
+
since 0.2.4, only in the terminal), and nothing else. Example (made-up data):
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
estela plan · Max 5x (since 2026-10-01) · 26 days with data
|
|
289
|
+
|
|
290
|
+
5-hour windows 41 windows · median peak 22% · p90 38% · max 61%
|
|
291
|
+
Caps (≥95%) 0
|
|
292
|
+
Week median peak 34% · max 47% · 0 weeks ≥ 90%
|
|
293
|
+
Blocks (429) 0 5-hour · 0 weekly, from Claude Code's transcripts
|
|
294
|
+
|
|
295
|
+
If you were on…
|
|
296
|
+
Pro ($20) equivalent p90 190% · would have blocked you in 29 of 41 windows
|
|
297
|
+
Max 20x ($200) no caps, same as now
|
|
298
|
+
|
|
299
|
+
Recommendation Your current plan fits your usage.
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
- **It never invents or estimates a plan's limit.** Other plans are compared only
|
|
303
|
+
on the 5-hour window, using the per-session multiplier Anthropic publishes
|
|
304
|
+
(Max 5x = 5 × Pro, Max 20x = 20 × Pro;
|
|
305
|
+
[source](https://support.claude.com/en/articles/11049741-what-is-the-max-plan)):
|
|
306
|
+
an equivalent peak is your peak × your multiplier ÷ the other plan's. Anthropic
|
|
307
|
+
doesn't publish the weekly limit between plans, so that comparison isn't made.
|
|
308
|
+
It never says how much you have left.
|
|
309
|
+
- A window with a peak of 95% or more is a cap. The time stopped after a cap
|
|
310
|
+
(from the first sample at 99% to the reset) is an upper bound: we don't know if
|
|
311
|
+
you kept working.
|
|
312
|
+
- "Blocks (429)" are the rate-limit responses Claude Code wrote in its
|
|
313
|
+
transcripts. They're official and go back before 0.2.4, but carry no
|
|
314
|
+
percentage, so they're shown apart and don't enter the calculation.
|
|
315
|
+
- With fewer than 7 days of data there's no recommendation; from 7 to 13 it's
|
|
316
|
+
provisional. After a plan change only data since then counts.
|
|
317
|
+
- Prices are Anthropic's website prices on the date in the output; check them.
|
|
318
|
+
Anthropic may change limits: run it again every month.
|
|
319
|
+
|
|
320
|
+
`--json` returns the same figures with a stable structure (`version: 1`): `plan`,
|
|
321
|
+
`period` (`from`, `to`, `days`, `daysWithData`, `samples`), `dataState`
|
|
322
|
+
(`none` | `insufficient` | `provisional` | `ok`), `fiveHour` (`windows`,
|
|
323
|
+
`medianPeak`, `p90Peak`, `maxPeak`, `caps`, `stoppedMinutesUpperBound`), `week`
|
|
324
|
+
(`weeks`, `medianPeak`, `maxPeak`, `weeksAtOrAbove90`), `blocks429`,
|
|
325
|
+
`alternatives[]` (`plan`, `direction`, `monthlyUsd`, `monthlyDiffUsd`,
|
|
326
|
+
`p90Equivalent`, `maxEquivalent`, `blockedWindows`, `capsAvoided`),
|
|
327
|
+
`recommendation` (`kind`: `tight` | `enough` | `almost` | `fits`, `plan`,
|
|
328
|
+
`provisional`, `text`, or `null`), `weeklyBetweenPlans: "not_published"`,
|
|
329
|
+
`sources` and `plans`. New fields may be added; none change meaning.
|
|
330
|
+
|
|
276
331
|
## A line under every Claude Code session
|
|
277
332
|
|
|
278
333
|
```sh
|
package/dist/billing/assign.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import type { TimeEntry } from "@estela/shared";
|
|
2
|
+
import type { CommitRecord, TimeEntry } from "@estela/shared";
|
|
3
3
|
import { type WorkBlock } from "./sessionize.js";
|
|
4
4
|
/**
|
|
5
5
|
* De bloques de trabajo a imputaciones: a qué proyecto va cada uno y con qué
|
|
@@ -39,6 +39,16 @@ export interface AssignmentPlan {
|
|
|
39
39
|
readonly blocked: readonly RecoveryBlocked[];
|
|
40
40
|
}
|
|
41
41
|
export declare function entryId(projectId: string, day: string, branch: string | null): string;
|
|
42
|
+
/**
|
|
43
|
+
* Los bloques deducidos solo de commits: los que no se solapan con ninguna
|
|
44
|
+
* sesión de agente, tampoco con las que ya no están en el disco.
|
|
45
|
+
*
|
|
46
|
+
* Claude Code limpia sus transcripts antiguos, así que lo escaneado no es todo
|
|
47
|
+
* lo que hubo. Comparar solo con eso convertía los commits de esas sesiones en
|
|
48
|
+
* trabajo sin agente, y sumaba horas estimadas a un trabajo ya medido. La base
|
|
49
|
+
* guarda todos los turnos: `saveTurns` no borra nada.
|
|
50
|
+
*/
|
|
51
|
+
export declare function commitOnlyBlocks(db: DatabaseSync, commits: readonly CommitRecord[], agentBlocks: readonly WorkBlock[]): WorkBlock[];
|
|
42
52
|
export declare function planEntries(db: DatabaseSync, raw: readonly WorkBlock[], options: {
|
|
43
53
|
readonly recover: boolean;
|
|
44
54
|
}): AssignmentPlan;
|
package/dist/billing/assign.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.entryId = entryId;
|
|
37
|
+
exports.commitOnlyBlocks = commitOnlyBlocks;
|
|
37
38
|
exports.planEntries = planEntries;
|
|
38
39
|
const shared_1 = require("@estela/shared");
|
|
39
40
|
const store = __importStar(require("../db/store.js"));
|
|
@@ -45,6 +46,19 @@ function entryId(projectId, day, branch) {
|
|
|
45
46
|
const slug = (branch ?? "sin-rama").replace(/[^a-zA-Z0-9]+/g, "-");
|
|
46
47
|
return `te_${projectId}_${day}_${slug}`;
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Los bloques deducidos solo de commits: los que no se solapan con ninguna
|
|
51
|
+
* sesión de agente, tampoco con las que ya no están en el disco.
|
|
52
|
+
*
|
|
53
|
+
* Claude Code limpia sus transcripts antiguos, así que lo escaneado no es todo
|
|
54
|
+
* lo que hubo. Comparar solo con eso convertía los commits de esas sesiones en
|
|
55
|
+
* trabajo sin agente, y sumaba horas estimadas a un trabajo ya medido. La base
|
|
56
|
+
* guarda todos los turnos: `saveTurns` no borra nada.
|
|
57
|
+
*/
|
|
58
|
+
function commitOnlyBlocks(db, commits, agentBlocks) {
|
|
59
|
+
const saved = (0, sessionize_js_1.sessionize)(store.turnsBetween(db, new Date(0), new Date("9999-12-31")));
|
|
60
|
+
return (0, sessionize_js_1.withoutOverlap)((0, sessionize_js_1.sessionizeCommits)(commits), [...agentBlocks, ...saved]);
|
|
61
|
+
}
|
|
48
62
|
function planEntries(db, raw, options) {
|
|
49
63
|
const ownerOf = (0, project_of_js_1.ownerResolver)(db);
|
|
50
64
|
const direct = (repo) => (repo ? store.projectForRepo(db, repo) : null);
|
|
@@ -113,6 +113,11 @@ export declare function sessionizeCommits(commits: readonly CommitRecord[], opti
|
|
|
113
113
|
*
|
|
114
114
|
* Sin esto se contaría dos veces el mismo rato: el agente ya cubrió esas horas
|
|
115
115
|
* y el commit que las cerró generaría otro bloque encima.
|
|
116
|
+
*
|
|
117
|
+
* Mismo repositorio quiere decir lo mismo que en `attachCommits`: la carpeta
|
|
118
|
+
* del commit (la raíz) o una subcarpeta suya. Antes se exigía la ruta exacta,
|
|
119
|
+
* y una sesión abierta en `repo/src/UI` no tapaba el commit hecho en `repo`:
|
|
120
|
+
* se sumaban 25 minutos estimados encima de una sesión medida.
|
|
116
121
|
*/
|
|
117
122
|
export declare function withoutOverlap(commitBlocks: readonly WorkBlock[], agentBlocks: readonly WorkBlock[]): WorkBlock[];
|
|
118
123
|
//# sourceMappingURL=sessionize.d.ts.map
|
|
@@ -290,10 +290,20 @@ function sessionizeCommits(commits, options = {}) {
|
|
|
290
290
|
*
|
|
291
291
|
* Sin esto se contaría dos veces el mismo rato: el agente ya cubrió esas horas
|
|
292
292
|
* y el commit que las cerró generaría otro bloque encima.
|
|
293
|
+
*
|
|
294
|
+
* Mismo repositorio quiere decir lo mismo que en `attachCommits`: la carpeta
|
|
295
|
+
* del commit (la raíz) o una subcarpeta suya. Antes se exigía la ruta exacta,
|
|
296
|
+
* y una sesión abierta en `repo/src/UI` no tapaba el commit hecho en `repo`:
|
|
297
|
+
* se sumaban 25 minutos estimados encima de una sesión medida.
|
|
293
298
|
*/
|
|
294
299
|
function withoutOverlap(commitBlocks, agentBlocks) {
|
|
295
|
-
return commitBlocks.filter((c) => !agentBlocks.some((a) => a.repoPath
|
|
300
|
+
return commitBlocks.filter((c) => !agentBlocks.some((a) => sameRepo(a.repoPath, c.repoPath) &&
|
|
296
301
|
a.startedAt.getTime() < c.endedAt.getTime() &&
|
|
297
302
|
c.startedAt.getTime() < a.endedAt.getTime()));
|
|
298
303
|
}
|
|
304
|
+
function sameRepo(a, b) {
|
|
305
|
+
if (a === b)
|
|
306
|
+
return true;
|
|
307
|
+
return !!a && !!b && ((0, paths_js_1.isSameOrInside)(a, b) || (0, paths_js_1.isSameOrInside)(b, a));
|
|
308
|
+
}
|
|
299
309
|
//# sourceMappingURL=sessionize.js.map
|
package/dist/cli.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
exports.breakdownOfTurn = void 0;
|
|
38
|
+
const node_os_1 = require("node:os");
|
|
38
39
|
const node_path_1 = require("node:path");
|
|
39
40
|
const node_readline_1 = require("node:readline");
|
|
40
41
|
// `node:sqlite` avisa de que es experimental en cada arranque. Es ruido para
|
|
@@ -63,6 +64,12 @@ const invoice_pdf_js_1 = require("./export/invoice-pdf.js");
|
|
|
63
64
|
const share_js_1 = require("./export/share.js");
|
|
64
65
|
const server_js_1 = require("./server.js");
|
|
65
66
|
const updateCheck_js_1 = require("./updateCheck.js");
|
|
67
|
+
const plan_js_1 = require("./metrics/plan.js");
|
|
68
|
+
const plan_blocks_js_1 = require("./plan-blocks.js");
|
|
69
|
+
const plan_prices_js_1 = require("./plan-prices.js");
|
|
70
|
+
const plan_report_js_1 = require("./plan-report.js");
|
|
71
|
+
const rate_history_js_1 = require("./rate-history.js");
|
|
72
|
+
const statusline_cache_js_1 = require("./statusline-cache.js");
|
|
66
73
|
const publish_js_1 = require("./publish.js");
|
|
67
74
|
const sync_js_1 = require("./sync.js");
|
|
68
75
|
const team_js_1 = require("./sync/team.js");
|
|
@@ -169,6 +176,15 @@ estela — registro de horas para desarrollo asistido por IA
|
|
|
169
176
|
Copilot queda fuera: no guarda tokens fiables. Antes de calcular
|
|
170
177
|
lee los transcripts desde el lunes (--no-refresh lo evita).
|
|
171
178
|
A diferencia de ai-cost, que reparte dinero, esto reparte tokens.
|
|
179
|
+
|
|
180
|
+
estela plan [--days <n>] [--json] [--no-color]
|
|
181
|
+
estela plan set pro|max5|max20 [--since AAAA-MM-DD]
|
|
182
|
+
¿El plan de Claude que pagas te sobra o te falta? Analiza los
|
|
183
|
+
porcentajes de 5 h y semana que Claude Code manda a la status line
|
|
184
|
+
(solo en la terminal) y los compara con los otros planes usando el
|
|
185
|
+
multiplicador por sesión que publica Anthropic. El límite semanal
|
|
186
|
+
entre planes no se compara: no se publica. Primero declara tu plan
|
|
187
|
+
con "plan set". Por defecto, los últimos 28 días.
|
|
172
188
|
estela statusline install [--force | --append]
|
|
173
189
|
Una línea bajo cada sesión de Claude Code: proyecto, horas medidas
|
|
174
190
|
hoy, la parte de tu consumo de la semana que es de este proyecto y,
|
|
@@ -262,9 +278,11 @@ estela — registro de horas para desarrollo asistido por IA
|
|
|
262
278
|
[--pdf <fichero.pdf>] [--csv <fichero.csv>] [--fx <tipo>]
|
|
263
279
|
[--number <nº>]
|
|
264
280
|
[--from-name <tu nombre>] [--from-email <email>]
|
|
265
|
-
[--from-tax-id <tu NIF/CIF>]
|
|
281
|
+
[--from-tax-id <tu NIF/CIF>] [--no-amounts]
|
|
266
282
|
Informe de horas y commits para respaldar tu trabajo. No es una
|
|
267
283
|
factura: Estela no emite documentos fiscales.
|
|
284
|
+
--no-amounts saca el PDF sin tarifa ni importes. En el panel, lo ya
|
|
285
|
+
emitido se vuelve a descargar con o sin ellos.
|
|
268
286
|
--number fija el número del informe; sin él se numera solo y
|
|
269
287
|
correlativo por año (INF-${new Date().getFullYear()}-001, -002...).
|
|
270
288
|
Los --from-* son tus datos como emisor, en la cabecera del PDF.
|
|
@@ -369,6 +387,15 @@ estela — time tracking for AI-assisted development
|
|
|
369
387
|
left out: it doesn't record reliable tokens. Before calculating it reads transcripts since Monday
|
|
370
388
|
(--no-refresh skips that). Unlike ai-cost, which splits money, this
|
|
371
389
|
splits tokens.
|
|
390
|
+
|
|
391
|
+
estela plan [--days <n>] [--json] [--no-color]
|
|
392
|
+
estela plan set pro|max5|max20 [--since YYYY-MM-DD]
|
|
393
|
+
Is your Claude plan too big or too small? It analyses the 5-hour
|
|
394
|
+
and weekly percentages Claude Code sends to the status line (only
|
|
395
|
+
in the terminal) and compares them with the other plans using the
|
|
396
|
+
per-session multiplier Anthropic publishes. The weekly limit isn't
|
|
397
|
+
compared across plans: it isn't published. Declare your plan first
|
|
398
|
+
with "plan set". Defaults to the last 28 days.
|
|
372
399
|
estela statusline install [--force | --append]
|
|
373
400
|
A line under every Claude Code session: project, hours measured
|
|
374
401
|
today, this project's share of your usage this week and, if Claude
|
|
@@ -464,9 +491,11 @@ estela — time tracking for AI-assisted development
|
|
|
464
491
|
[--pdf <file.pdf>] [--csv <file.csv>] [--fx <rate>]
|
|
465
492
|
[--number <no.>]
|
|
466
493
|
[--from-name <your name>] [--from-email <email>]
|
|
467
|
-
[--from-tax-id <your tax ID>]
|
|
494
|
+
[--from-tax-id <your tax ID>] [--no-amounts]
|
|
468
495
|
Report of hours and commits to back up your work. It isn't an
|
|
469
496
|
invoice: Estela doesn't issue tax documents.
|
|
497
|
+
--no-amounts leaves the rate and amounts out of the PDF. In the panel,
|
|
498
|
+
issued reports can be downloaded again with or without them.
|
|
470
499
|
--number sets the report number; without it, reports are numbered
|
|
471
500
|
automatically and sequentially per year (INF-${new Date().getFullYear()}-001, -002...).
|
|
472
501
|
The --from-* flags are your details as the issuer, in the PDF header.
|
|
@@ -877,7 +906,7 @@ async function cmdImport(args, dbPath) {
|
|
|
877
906
|
// compañero se convertirían en tiempo facturable a tu nombre.
|
|
878
907
|
const commitsAll = (0, identity_js_1.onlyMine)(await allCommits(db), mine);
|
|
879
908
|
const fromAgents = (0, sessionize_js_1.attachCommits)((0, sessionize_js_1.sessionize)(turns), commitsAll);
|
|
880
|
-
const fromCommits = (0,
|
|
909
|
+
const fromCommits = (0, assign_js_1.commitOnlyBlocks)(db, commitsAll, fromAgents);
|
|
881
910
|
const raw = [...fromAgents, ...fromCommits];
|
|
882
911
|
if (fromCommits.length) {
|
|
883
912
|
console.log((0, index_js_1.tr) ` ${fromCommits.length} bloques deducidos solo de commits (sin agente)`);
|
|
@@ -1544,6 +1573,90 @@ async function cmdUnpublish(args, dbPath) {
|
|
|
1544
1573
|
db.close();
|
|
1545
1574
|
}
|
|
1546
1575
|
}
|
|
1576
|
+
/**
|
|
1577
|
+
* `estela plan`: ¿el plan de Claude que pagas es el que te conviene?
|
|
1578
|
+
*
|
|
1579
|
+
* Solo con lo que manda Claude Code a la status line (rate-limits.jsonl) y el
|
|
1580
|
+
* multiplicador por sesión que publica Anthropic. Ver metrics/plan.ts.
|
|
1581
|
+
*/
|
|
1582
|
+
function cmdPlan(args, dbPath) {
|
|
1583
|
+
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
1584
|
+
try {
|
|
1585
|
+
const declared = store.getClaudePlan(db);
|
|
1586
|
+
const json = args.flags["json"] === true;
|
|
1587
|
+
if (!declared || !(0, plan_prices_js_1.planById)(declared.plan)) {
|
|
1588
|
+
if (json)
|
|
1589
|
+
console.log(JSON.stringify({ version: 1, plan: null }, null, 2));
|
|
1590
|
+
else
|
|
1591
|
+
console.log((0, plan_report_js_1.renderNoPlan)());
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const days = str(args, "days") === undefined ? 28 : Number(str(args, "days"));
|
|
1595
|
+
if (!Number.isInteger(days) || days < 1 || days > rate_history_js_1.KEEP_DAYS) {
|
|
1596
|
+
throw new UserError((0, index_js_1.tr) `--days tiene que ser un número entero de 1 a ${rate_history_js_1.KEEP_DAYS}.`);
|
|
1597
|
+
}
|
|
1598
|
+
const now = new Date();
|
|
1599
|
+
const file = (0, rate_history_js_1.historyPath)(dbPath);
|
|
1600
|
+
try {
|
|
1601
|
+
(0, rate_history_js_1.pruneSamples)(file, now);
|
|
1602
|
+
}
|
|
1603
|
+
catch { /* sin rotar se analiza igual */ }
|
|
1604
|
+
const all = (0, rate_history_js_1.readSamples)(file);
|
|
1605
|
+
const analysis = (0, plan_js_1.analyzePlan)({ samples: all, plan: declared.plan, since: declared.since, now, days });
|
|
1606
|
+
// Los 429 que Claude Code dejó en sus transcripts, en el mismo período.
|
|
1607
|
+
let blocks = null;
|
|
1608
|
+
try {
|
|
1609
|
+
const found = (0, plan_blocks_js_1.scanBlocks)((0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "projects"), (0, plan_blocks_js_1.blocksCachePath)(dbPath), now)
|
|
1610
|
+
.filter((b) => b.at >= analysis.from.getTime() && b.at <= now.getTime());
|
|
1611
|
+
blocks = { fiveHour: found.filter((b) => b.type === "five_hour").length,
|
|
1612
|
+
sevenDay: found.filter((b) => b.type === "seven_day").length };
|
|
1613
|
+
}
|
|
1614
|
+
catch { /* sin transcripts legibles, la línea no sale */ }
|
|
1615
|
+
if (json) {
|
|
1616
|
+
console.log(JSON.stringify((0, plan_report_js_1.planToJson)(analysis, { blocks, now }), null, 2));
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const color = args.flags["no-color"] !== true && !process.env["NO_COLOR"] && process.stdout.isTTY === true;
|
|
1620
|
+
if (analysis.dataState === "none") {
|
|
1621
|
+
// Sin ninguna muestra nunca, y la status line vio entradas sin rate_limits: API key.
|
|
1622
|
+
const apiKey = all.length === 0 && (0, statusline_cache_js_1.loadCache)(dbPath).noLimitsAt !== undefined;
|
|
1623
|
+
console.log((0, plan_report_js_1.renderNoData)(analysis, apiKey, color));
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
console.log((0, plan_report_js_1.renderPlanReport)(analysis, { color, now, blocks }));
|
|
1627
|
+
}
|
|
1628
|
+
finally {
|
|
1629
|
+
db.close();
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
/** `estela plan set pro|max5|max20 [--since AAAA-MM-DD]`. */
|
|
1633
|
+
function cmdPlanSet(args, dbPath) {
|
|
1634
|
+
const id = args._[2] ?? "";
|
|
1635
|
+
const plan = (0, plan_prices_js_1.planById)(id);
|
|
1636
|
+
if (!plan)
|
|
1637
|
+
throw new UserError((0, index_js_1.tr) `Plan desconocido: "${id}". Usa pro, max5 o max20.`);
|
|
1638
|
+
const sinceText = str(args, "since");
|
|
1639
|
+
let since = new Date();
|
|
1640
|
+
if (sinceText !== undefined) {
|
|
1641
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(sinceText) || Number.isNaN(new Date(`${sinceText}T00:00:00`).getTime())) {
|
|
1642
|
+
throw new UserError((0, index_js_1.tr) `--since tiene que ser una fecha AAAA-MM-DD.`);
|
|
1643
|
+
}
|
|
1644
|
+
since = new Date(`${sinceText}T00:00:00`);
|
|
1645
|
+
}
|
|
1646
|
+
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
1647
|
+
try {
|
|
1648
|
+
store.setClaudePlan(db, plan.id, since);
|
|
1649
|
+
const d = since;
|
|
1650
|
+
const fecha = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1651
|
+
console.log((0, index_js_1.tr) `Plan declarado: ${plan.name}, desde ${fecha}. El análisis solo usa lo guardado desde esa fecha.`);
|
|
1652
|
+
if (sinceText === undefined) {
|
|
1653
|
+
console.log((0, index_js_1.tr) `Si ya lo tenías antes, indica desde cuándo: estela plan set ${plan.id} --since AAAA-MM-DD`);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
finally {
|
|
1657
|
+
db.close();
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1547
1660
|
/**
|
|
1548
1661
|
* `estela sync enable --project <id>` — opt-in explícito, proyecto por
|
|
1549
1662
|
* proyecto. Sin esta fila, ese proyecto no sale de esta máquina nunca, sea
|
|
@@ -2418,6 +2531,7 @@ function cmdInvoice(args, dbPath) {
|
|
|
2418
2531
|
},
|
|
2419
2532
|
} : {}),
|
|
2420
2533
|
...(invoice.aiAmortized ? { amortizedAiCost: invoice.aiAmortized } : {}),
|
|
2534
|
+
withAmounts: args.flags["no-amounts"] !== true,
|
|
2421
2535
|
}));
|
|
2422
2536
|
(0, node_fs_1.writeFileSync)(pdfPath, pdf);
|
|
2423
2537
|
console.log((0, index_js_1.tr) `\nPDF: ${pdfPath} (${(pdf.length / 1024).toFixed(1)} KB, ${invoice.lines.length} conceptos)`);
|
|
@@ -2466,7 +2580,10 @@ async function main() {
|
|
|
2466
2580
|
(0, client_js_1.setClientVersion)(VERSION);
|
|
2467
2581
|
const dbPath = str(args, "db") ?? schema_js_1.DEFAULT_DB_PATH;
|
|
2468
2582
|
// `estela unpublish <panel>` lleva el panel suelto, sin flag: no es un subcomando.
|
|
2469
|
-
const command = args._[0] === "unpublish" ? "unpublish"
|
|
2583
|
+
const command = args._[0] === "unpublish" ? "unpublish"
|
|
2584
|
+
// `estela plan set max5`: el plan va suelto, como el panel de unpublish.
|
|
2585
|
+
: args._[0] === "plan" && args._[1] === "set" ? "plan set"
|
|
2586
|
+
: args._.join(" ");
|
|
2470
2587
|
// Se llama aquí, no al final: un comando que falla a medias (por ejemplo
|
|
2471
2588
|
// "estela import" sin repositorios) debe avisar igual de que hay una
|
|
2472
2589
|
// versión nueva, no solo los que terminan bien.
|
|
@@ -2553,6 +2670,12 @@ async function main() {
|
|
|
2553
2670
|
case "unpublish":
|
|
2554
2671
|
await cmdUnpublish(args, dbPath);
|
|
2555
2672
|
break;
|
|
2673
|
+
case "plan":
|
|
2674
|
+
cmdPlan(args, dbPath);
|
|
2675
|
+
break;
|
|
2676
|
+
case "plan set":
|
|
2677
|
+
cmdPlanSet(args, dbPath);
|
|
2678
|
+
break;
|
|
2556
2679
|
case "export":
|
|
2557
2680
|
await cmdExport(args, dbPath);
|
|
2558
2681
|
break;
|
package/dist/db/schema.js
CHANGED
|
@@ -269,6 +269,16 @@ CREATE TABLE IF NOT EXISTS personal_sync_state (
|
|
|
269
269
|
entity_type TEXT PRIMARY KEY,
|
|
270
270
|
synced_up_to TEXT NOT NULL DEFAULT ''
|
|
271
271
|
);
|
|
272
|
+
|
|
273
|
+
-- El plan de Claude que declara quien usa Estela (estela plan set): el JSON
|
|
274
|
+
-- de la status line no lo dice. Una fila por cambio, para conservar el
|
|
275
|
+
-- historial; el vigente es el de 'since' más reciente.
|
|
276
|
+
CREATE TABLE IF NOT EXISTS claude_plans (
|
|
277
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
278
|
+
plan TEXT NOT NULL,
|
|
279
|
+
since TEXT NOT NULL,
|
|
280
|
+
declared_at TEXT NOT NULL
|
|
281
|
+
);
|
|
272
282
|
`;
|
|
273
283
|
/**
|
|
274
284
|
* Migraciones.
|
package/dist/db/store.d.ts
CHANGED
|
@@ -304,4 +304,13 @@ export declare function entryDetailForTeam(db: DatabaseSync, projectId: string):
|
|
|
304
304
|
}[];
|
|
305
305
|
export declare function getAiConsent(db: DatabaseSync, projectId: string): AiConsentRecord | null;
|
|
306
306
|
export declare function setAiConsent(db: DatabaseSync, projectId: string, requested: boolean, decision: AiConsentDecision): void;
|
|
307
|
+
export interface DeclaredPlan {
|
|
308
|
+
readonly plan: string;
|
|
309
|
+
readonly since: Date;
|
|
310
|
+
}
|
|
311
|
+
/** El plan vigente: el de `since` más reciente. null si nunca se declaró. */
|
|
312
|
+
export declare function getClaudePlan(db: DatabaseSync): DeclaredPlan | null;
|
|
313
|
+
/** Añade un cambio de plan; los anteriores se conservan. */
|
|
314
|
+
export declare function setClaudePlan(db: DatabaseSync, plan: string, since: Date, at?: Date): void;
|
|
315
|
+
export declare function listClaudePlans(db: DatabaseSync): DeclaredPlan[];
|
|
307
316
|
//# sourceMappingURL=store.d.ts.map
|
package/dist/db/store.js
CHANGED
|
@@ -61,6 +61,9 @@ exports.entryAiByDay = entryAiByDay;
|
|
|
61
61
|
exports.entryDetailForTeam = entryDetailForTeam;
|
|
62
62
|
exports.getAiConsent = getAiConsent;
|
|
63
63
|
exports.setAiConsent = setAiConsent;
|
|
64
|
+
exports.getClaudePlan = getClaudePlan;
|
|
65
|
+
exports.setClaudePlan = setClaudePlan;
|
|
66
|
+
exports.listClaudePlans = listClaudePlans;
|
|
64
67
|
const node_crypto_1 = require("node:crypto");
|
|
65
68
|
const shared_1 = require("@estela/shared");
|
|
66
69
|
const cost_js_1 = require("../pricing/cost.js");
|
|
@@ -557,8 +560,14 @@ function saveTimeEntry(db, entry, at = new Date()) {
|
|
|
557
560
|
-- Y lo ya facturado es historia. Sin esta condición, un reimport
|
|
558
561
|
-- recalculaba filas que respaldan una factura emitida y dejaba el panel
|
|
559
562
|
-- diciendo una cosa y el PDF del cliente otra.
|
|
563
|
+
--
|
|
564
|
+
-- Lo aprobado, igual: aprobar un día es dar por buena esa cifra, y a
|
|
565
|
+
-- menudo ya se ha compartido (un panel publicado, un avance al cliente).
|
|
566
|
+
-- Un cambio en cómo se calculan las horas se aplica a lo que viene, no
|
|
567
|
+
-- reescribe lo que ya se dio por bueno. Para recalcularlo, se desaprueba.
|
|
560
568
|
WHERE time_entries.source IN ('agent', 'commit')
|
|
561
569
|
AND time_entries.invoice_id IS NULL
|
|
570
|
+
AND time_entries.approved = 0
|
|
562
571
|
`).run(id, entry.projectId, iso(entry.startedAt), (0, shared_1.localDate)(entry.startedAt), iso(entry.endedAt), entry.seconds, entry.description, entry.billable ? 1 : 0, entry.invoiceId, entry.aiCost.microUsd, entry.agentSeconds, entry.commitHashes.join(","), entry.agents.join(","), entry.source, entry.kind, entry.branch, (entry.workItems ?? []).join(","), iso(at));
|
|
563
572
|
return id;
|
|
564
573
|
}
|
|
@@ -982,4 +991,19 @@ function setAiConsent(db, projectId, requested, decision) {
|
|
|
982
991
|
requested = excluded.requested, decision = excluded.decision, decided_at = excluded.decided_at
|
|
983
992
|
`).run(projectId, requested ? 1 : 0, decision, decision === "pending" ? null : iso(new Date()));
|
|
984
993
|
}
|
|
994
|
+
/** El plan vigente: el de `since` más reciente. null si nunca se declaró. */
|
|
995
|
+
function getClaudePlan(db) {
|
|
996
|
+
const row = db.prepare("SELECT plan, since FROM claude_plans ORDER BY since DESC, id DESC LIMIT 1")
|
|
997
|
+
.get();
|
|
998
|
+
return row ? { plan: row.plan, since: new Date(row.since) } : null;
|
|
999
|
+
}
|
|
1000
|
+
/** Añade un cambio de plan; los anteriores se conservan. */
|
|
1001
|
+
function setClaudePlan(db, plan, since, at = new Date()) {
|
|
1002
|
+
db.prepare("INSERT INTO claude_plans (plan, since, declared_at) VALUES (?, ?, ?)")
|
|
1003
|
+
.run(plan, iso(since), iso(at));
|
|
1004
|
+
}
|
|
1005
|
+
function listClaudePlans(db) {
|
|
1006
|
+
return db.prepare("SELECT plan, since FROM claude_plans ORDER BY since, id").all()
|
|
1007
|
+
.map((r) => ({ plan: r.plan, since: new Date(r.since) }));
|
|
1008
|
+
}
|
|
985
1009
|
//# sourceMappingURL=store.js.map
|
|
@@ -15,6 +15,8 @@ export interface InvoicePdfOptions {
|
|
|
15
15
|
};
|
|
16
16
|
/** Coste real de IA imputado al periodo (cuota amortizada). Solo informativo. */
|
|
17
17
|
readonly amortizedAiCost?: Money;
|
|
18
|
+
/** Sin tarifa ni importes: solo las horas. Por defecto, con importes. */
|
|
19
|
+
readonly withAmounts?: boolean;
|
|
18
20
|
readonly footer?: string;
|
|
19
21
|
}
|
|
20
22
|
export declare function invoiceToPdf(invoice: Invoice, client: Client, project: Project, options?: InvoicePdfOptions): Buffer;
|
|
@@ -15,6 +15,9 @@ function invoiceToPdf(invoice, client, project, options = {}) {
|
|
|
15
15
|
// Los importes van con el locale del documento, no con el de la terminal: el
|
|
16
16
|
// cliente lee "€1.254,79" o "€1,254.79" según SU idioma.
|
|
17
17
|
const money = (m) => (0, shared_1.formatMoney)(m, (0, index_js_1.moneyLocale)((0, index_js_1.getLang)()));
|
|
18
|
+
const withAmounts = options.withAmounts !== false;
|
|
19
|
+
// Sin tarifa ni importe, el tiempo pasa a ser la última columna.
|
|
20
|
+
const colTime = withAmounts ? COL_TIME : COL_AMOUNT;
|
|
18
21
|
let y = M;
|
|
19
22
|
// --- Cabecera -------------------------------------------------------------
|
|
20
23
|
doc.text(M, y + 4, (0, index_js_1.tr) `INFORME DE HORAS`, "Helvetica-Bold", 20);
|
|
@@ -57,20 +60,22 @@ function invoiceToPdf(invoice, client, project, options = {}) {
|
|
|
57
60
|
// --- Cabecera de tabla ----------------------------------------------------
|
|
58
61
|
const header = () => {
|
|
59
62
|
doc.text(M, y, (0, index_js_1.tr) `CONCEPTO`, "Helvetica-Bold", 8, 0.45);
|
|
60
|
-
doc.textRight(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
doc.textRight(colTime, y, (0, index_js_1.tr) `TIEMPO`, "Helvetica-Bold", 8, 0.45);
|
|
64
|
+
if (withAmounts) {
|
|
65
|
+
doc.textRight(COL_RATE, y, (0, index_js_1.tr) `TARIFA`, "Helvetica-Bold", 8, 0.45);
|
|
66
|
+
doc.textRight(COL_AMOUNT, y, (0, index_js_1.tr) `IMPORTE`, "Helvetica-Bold", 8, 0.45);
|
|
67
|
+
}
|
|
63
68
|
y += 7;
|
|
64
69
|
doc.line(M, y, COL_AMOUNT, y, 0.8, 0.3);
|
|
65
70
|
y += 15;
|
|
66
71
|
};
|
|
67
72
|
header();
|
|
68
73
|
// --- Conceptos ------------------------------------------------------------
|
|
69
|
-
// La columna TIEMPO se alinea a la derecha en
|
|
74
|
+
// La columna TIEMPO se alinea a la derecha en colTime, así que su texto crece
|
|
70
75
|
// hacia la izquierda. Hay que restar el más ancho de esos textos, no un hueco
|
|
71
76
|
// fijo, o la descripción se le mete encima.
|
|
72
77
|
const widestTime = invoice.lines.reduce((max, l) => Math.max(max, pdf_js_1.PdfDocument.measure((0, shared_1.formatDuration)(l.seconds), "Helvetica", 10)), 0);
|
|
73
|
-
const maxDescWidth =
|
|
78
|
+
const maxDescWidth = colTime - M - widestTime - 14;
|
|
74
79
|
for (const line of invoice.lines) {
|
|
75
80
|
if (y > BOTTOM - 60) {
|
|
76
81
|
doc.newPage();
|
|
@@ -79,9 +84,11 @@ function invoiceToPdf(invoice, client, project, options = {}) {
|
|
|
79
84
|
}
|
|
80
85
|
const text = pdf_js_1.PdfDocument.truncate(line.description, "Helvetica", 10, maxDescWidth);
|
|
81
86
|
doc.text(M, y, text, "Helvetica", 10);
|
|
82
|
-
doc.textRight(
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
doc.textRight(colTime, y, (0, shared_1.formatDuration)(line.seconds), "Helvetica", 10, 0.2);
|
|
88
|
+
if (withAmounts) {
|
|
89
|
+
doc.textRight(COL_RATE, y, `${money(line.hourlyRate)}/h`, "Helvetica", 10, 0.4);
|
|
90
|
+
doc.textRight(COL_AMOUNT, y, money(line.amount), "Helvetica", 10);
|
|
91
|
+
}
|
|
85
92
|
y += 9;
|
|
86
93
|
doc.line(M, y, COL_AMOUNT, y, 0.4, 0.9);
|
|
87
94
|
y += 12;
|
|
@@ -95,19 +102,22 @@ function invoiceToPdf(invoice, client, project, options = {}) {
|
|
|
95
102
|
doc.line(M, y, COL_AMOUNT, y, 0.8, 0.3);
|
|
96
103
|
y += 18;
|
|
97
104
|
doc.text(M, y, invoice.lines.length === 1 ? (0, index_js_1.tr) `1 concepto` : (0, index_js_1.tr) `${invoice.lines.length} conceptos`, "Helvetica", 9, 0.4);
|
|
98
|
-
doc.textRight(
|
|
99
|
-
|
|
105
|
+
doc.textRight(colTime, y, (0, shared_1.formatDuration)(invoice.totalSeconds), "Helvetica-Bold", 10);
|
|
106
|
+
if (withAmounts)
|
|
107
|
+
doc.textRight(COL_AMOUNT, y, money(invoice.subtotal), "Helvetica", 10);
|
|
100
108
|
y += 18;
|
|
101
|
-
if (invoice.aiCostBilled) {
|
|
109
|
+
if (withAmounts && invoice.aiCostBilled) {
|
|
102
110
|
doc.text(M, y, (0, index_js_1.tr) `Coste de IA repercutido (1 USD = ${invoice.usdFxRate} ${invoice.currency})`, "Helvetica", 9, 0.4);
|
|
103
111
|
doc.textRight(COL_AMOUNT, y, money(invoice.aiCostBilled), "Helvetica", 10);
|
|
104
112
|
y += 18;
|
|
105
113
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
if (withAmounts) {
|
|
115
|
+
y += 4;
|
|
116
|
+
doc.rect(rightCol - 20, y - 14, COL_AMOUNT - rightCol + 20, 32, 0.94);
|
|
117
|
+
doc.text(rightCol - 8, y + 2, (0, index_js_1.tr) `VALOR`, "Helvetica-Bold", 11);
|
|
118
|
+
doc.textRight(COL_AMOUNT - 8, y + 3, money(invoice.total), "Helvetica-Bold", 14);
|
|
119
|
+
y += 46;
|
|
120
|
+
}
|
|
111
121
|
// Aquí había una "NOTA INTERNA (no se comparte)" con el consumo de IA del
|
|
112
122
|
// periodo. Se quitó: esa frase era un deseo, no un mecanismo. Este PDF es
|
|
113
123
|
// justamente el que se adjunta a la factura del cliente, así que en cuanto
|
package/dist/i18n/en.js
CHANGED
|
@@ -579,5 +579,50 @@ exports.EN = {
|
|
|
579
579
|
"ESTELA_OFFLINE=1: Estela no se conecta a la red. Quita la variable para usar tu cuenta.": "ESTELA_OFFLINE=1: Estela doesn't connect to the network. Unset it to use your account.",
|
|
580
580
|
// ── statusline install: VS Code ──
|
|
581
581
|
"Solo en Claude Code en la terminal: la extensión de VS Code no enseña la status line.": "Only in Claude Code in the terminal: the VS Code extension doesn't show the status line.",
|
|
582
|
+
// ── estela plan ──
|
|
583
|
+
"--days tiene que ser un número entero de 1 a {0}.": "--days has to be a whole number from 1 to {0}.",
|
|
584
|
+
"Plan desconocido: \"{0}\". Usa pro, max5 o max20.": "Unknown plan: \"{0}\". Use pro, max5 or max20.",
|
|
585
|
+
"--since tiene que ser una fecha AAAA-MM-DD.": "--since has to be a YYYY-MM-DD date.",
|
|
586
|
+
"Plan declarado: {0}, desde {1}. El análisis solo usa lo guardado desde esa fecha.": "Plan declared: {0}, since {1}. The analysis only uses what was stored from that date.",
|
|
587
|
+
"Si ya lo tenías antes, indica desde cuándo: estela plan set {0} --since AAAA-MM-DD": "If you had it before, say since when: estela plan set {0} --since YYYY-MM-DD",
|
|
588
|
+
"Ventanas de 5 h": "5-hour windows",
|
|
589
|
+
"ninguna completa todavía": "none yet",
|
|
590
|
+
"{0} ventanas · pico mediano {1} % · p90 {2} % · máximo {3} %": "{0} windows · median peak {1}% · p90 {2}% · max {3}%",
|
|
591
|
+
"Topes (≥95 %)": "Caps (≥95%)",
|
|
592
|
+
"Parado tras topes": "Stopped after caps",
|
|
593
|
+
"hasta {0} (estimado: no sabemos si seguías trabajando)": "up to {0} (estimated: we don't know if you kept working)",
|
|
594
|
+
"Semana": "Week",
|
|
595
|
+
"sin datos de la semana": "no weekly data",
|
|
596
|
+
"pico mediano {0} % · máximo {1} % · {2} semanas ≥ 90 %": "median peak {0}% · max {1}% · {2} weeks ≥ 90%",
|
|
597
|
+
"Bloqueos (429)": "Blocks (429)",
|
|
598
|
+
"{0} de 5 h · {1} semanales, según los transcripts de Claude Code": "{0} 5-hour · {1} weekly, from Claude Code's transcripts",
|
|
599
|
+
"Si estuvieras en…": "If you were on…",
|
|
600
|
+
"Datos de {0} días. La recomendación necesita al menos 14.": "{0} days of data. The recommendation needs at least 14.",
|
|
601
|
+
"Recomendación (provisional, {0} días)": "Recommendation (provisional, {0} days)",
|
|
602
|
+
"Recomendación": "Recommendation",
|
|
603
|
+
"Límite semanal entre planes: Anthropic no publica la relación. No lo estimamos.": "Weekly limit between plans: Anthropic doesn't publish the ratio. We don't estimate it.",
|
|
604
|
+
"Basado en el multiplicador por sesión publicado por Anthropic:": "Based on the per-session multiplier published by Anthropic:",
|
|
605
|
+
"Anthropic puede cambiar los límites; repite el análisis cada mes.": "Anthropic may change the limits; run this again every month.",
|
|
606
|
+
"Precios según la web de Anthropic a {0}; compruébalo: {1}": "Prices from Anthropic's website as of {0}; check them: {1}",
|
|
607
|
+
"Esa fecha tiene más de {0} días: los precios pueden estar desactualizados.": "That date is more than {0} days old: prices may be out of date.",
|
|
608
|
+
"(desde {0})": "(since {0})",
|
|
609
|
+
"{0} días con datos": "{0} days with data",
|
|
610
|
+
"sin topes, igual que ahora": "no caps, same as now",
|
|
611
|
+
"p90 equivalente {0} % · habrías evitado {1} de {2} topes (con el uso que llegó a registrarse)": "equivalent p90 {0}% · you'd have avoided {1} of {2} caps (with the usage that got recorded)",
|
|
612
|
+
"p90 equivalente {0} % · no te habría bloqueado en ninguna de las {1} ventanas": "equivalent p90 {0}% · wouldn't have blocked you in any of the {1} windows",
|
|
613
|
+
"p90 equivalente {0} % · te habría bloqueado en {1} de {2} ventanas": "equivalent p90 {0}% · would have blocked you in {1} of {2} windows",
|
|
614
|
+
"Vas justo con tu plan. Con {0} habrías evitado {1} de {2} topes.": "Your plan is tight for you. With {0} you'd have avoided {1} of {2} caps.",
|
|
615
|
+
"Vas justo con tu plan.": "Your plan is tight for you.",
|
|
616
|
+
"Con los datos de estas {0} semanas, el plan {1} te habría bastado en todas las ventanas de 5 h.": "Based on these {0} weeks of data, the {1} plan would have been enough in every 5-hour window.",
|
|
617
|
+
"El plan {0} te habría bastado casi siempre: te habría bloqueado {1} veces en estas semanas.": "The {0} plan would have been enough almost always: it would have blocked you {1} times in these weeks.",
|
|
618
|
+
"Tu plan actual encaja con tu uso.": "Your current plan fits your usage.",
|
|
619
|
+
"{0} menos al mes, según la web de Anthropic a {1}; compruébalo.": "{0} less per month, per Anthropic's website as of {1}; check it.",
|
|
620
|
+
"{0} más al mes, según la web de Anthropic a {1}; compruébalo.": "{0} more per month, per Anthropic's website as of {1}; check it.",
|
|
621
|
+
"estela plan compara tu uso real de Claude con los otros planes. Primero dime cuál pagas:": "estela plan compares your real Claude usage with the other plans. First tell it which one you pay for:",
|
|
622
|
+
"Si ya lo tenías antes de hoy, añade desde cuándo: --since AAAA-MM-DD": "If you had it before today, add since when: --since YYYY-MM-DD",
|
|
623
|
+
"Este análisis solo funciona con planes Pro o Max.": "This analysis only works with Pro or Max plans.",
|
|
624
|
+
"Aún no hay datos. Instala la status line (estela statusline install) y vuelve en unos días.": "No data yet. Install the status line (estela statusline install) and come back in a few days.",
|
|
625
|
+
"Solo se recogen en Claude Code en la terminal: la extensión de VS Code no ejecuta la status line.": "They're only collected in Claude Code in the terminal: the VS Code extension doesn't run the status line.",
|
|
626
|
+
"{0} $": "${0}",
|
|
582
627
|
};
|
|
583
628
|
//# sourceMappingURL=en.js.map
|