estela 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/dist/cli.js +123 -1
- package/dist/db/schema.js +10 -0
- package/dist/db/store.d.ts +9 -0
- package/dist/db/store.js +18 -0
- package/dist/i18n/en.js +47 -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/rate-history.d.ts +63 -0
- package/dist/rate-history.js +193 -0
- package/dist/statusline-cache.d.ts +10 -0
- package/dist/statusline-run.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Los bloqueos que Claude Code dejó escritos en sus transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Los transcripts no traen los porcentajes de límite, pero cuando Claude Code
|
|
5
|
+
* recibe un 429 guarda la respuesta con un bloque `quotaLimits`: qué ventana
|
|
6
|
+
* fue (`five_hour` o `seven_day`) y cuándo se reinicia. Es un dato oficial y
|
|
7
|
+
* llega a antes de la 0.2.4, así que `estela plan` lo enseña como una línea
|
|
8
|
+
* aparte. No entra en el cálculo de equivalencias, que es solo de porcentajes.
|
|
9
|
+
*
|
|
10
|
+
* Los transcripts de un mes pueden pesar cientos de MB: se leen una vez y
|
|
11
|
+
* después solo lo nuevo de cada fichero, con lo visto guardado al lado de la
|
|
12
|
+
* base (`plan-blocks.json`).
|
|
13
|
+
*/
|
|
14
|
+
export interface RateLimitBlock {
|
|
15
|
+
readonly type: "five_hour" | "seven_day";
|
|
16
|
+
/** `resetsAt` (epoch en s). */
|
|
17
|
+
readonly reset: number;
|
|
18
|
+
/** El primer 429 de esa ventana (epoch en ms). */
|
|
19
|
+
readonly at: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function blocksCachePath(dbPath: string): string;
|
|
22
|
+
/** Lee lo nuevo de los transcripts y devuelve todos los bloqueos conocidos. */
|
|
23
|
+
export declare function scanBlocks(root: string, cacheFile: string, now: Date): RateLimitBlock[];
|
|
24
|
+
/** Los bloqueos de un texto JSONL. Solo se parsean las líneas que traen `quotaLimits`. */
|
|
25
|
+
export declare function blocksIn(text: string): RateLimitBlock[];
|
|
26
|
+
//# sourceMappingURL=plan-blocks.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.blocksCachePath = blocksCachePath;
|
|
4
|
+
exports.scanBlocks = scanBlocks;
|
|
5
|
+
exports.blocksIn = blocksIn;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const MAX_AGE_MS = 120 * 86_400_000;
|
|
9
|
+
function blocksCachePath(dbPath) {
|
|
10
|
+
return (0, node_path_1.join)((0, node_path_1.dirname)(dbPath), "plan-blocks.json");
|
|
11
|
+
}
|
|
12
|
+
/** Lee lo nuevo de los transcripts y devuelve todos los bloqueos conocidos. */
|
|
13
|
+
function scanBlocks(root, cacheFile, now) {
|
|
14
|
+
let cache = { files: {}, blocks: [] };
|
|
15
|
+
try {
|
|
16
|
+
cache = JSON.parse((0, node_fs_1.readFileSync)(cacheFile, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
catch { /* primera vez */ }
|
|
19
|
+
const byKey = new Map(cache.blocks.map((b) => [`${b.type}|${b.reset}`, b]));
|
|
20
|
+
for (const file of jsonlFiles(root, now.getTime() - MAX_AGE_MS)) {
|
|
21
|
+
let size;
|
|
22
|
+
try {
|
|
23
|
+
size = (0, node_fs_1.statSync)(file).size;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const offset = cache.files[file] ?? 0;
|
|
29
|
+
const start = offset <= size ? offset : 0;
|
|
30
|
+
if (start >= size)
|
|
31
|
+
continue;
|
|
32
|
+
const { text, end } = readFrom(file, start, size);
|
|
33
|
+
for (const b of blocksIn(text)) {
|
|
34
|
+
const key = `${b.type}|${b.reset}`;
|
|
35
|
+
const prev = byKey.get(key);
|
|
36
|
+
if (!prev || b.at < prev.at)
|
|
37
|
+
byKey.set(key, b);
|
|
38
|
+
}
|
|
39
|
+
cache.files[file] = end;
|
|
40
|
+
}
|
|
41
|
+
const blocks = [...byKey.values()].filter((b) => b.at >= now.getTime() - MAX_AGE_MS).sort((a, b) => a.at - b.at);
|
|
42
|
+
try {
|
|
43
|
+
(0, node_fs_1.writeFileSync)(cacheFile, JSON.stringify({ files: cache.files, blocks }));
|
|
44
|
+
}
|
|
45
|
+
catch { /* sin caché se relee */ }
|
|
46
|
+
return blocks;
|
|
47
|
+
}
|
|
48
|
+
/** Los bloqueos de un texto JSONL. Solo se parsean las líneas que traen `quotaLimits`. */
|
|
49
|
+
function blocksIn(text) {
|
|
50
|
+
const out = [];
|
|
51
|
+
let i = text.indexOf('"quotaLimits"');
|
|
52
|
+
while (i >= 0) {
|
|
53
|
+
const startLine = text.lastIndexOf("\n", i) + 1;
|
|
54
|
+
const endLine = text.indexOf("\n", i);
|
|
55
|
+
const line = text.slice(startLine, endLine < 0 ? text.length : endLine);
|
|
56
|
+
try {
|
|
57
|
+
const d = JSON.parse(line);
|
|
58
|
+
const q = d["quotaLimits"];
|
|
59
|
+
const type = q?.["rateLimitType"];
|
|
60
|
+
const reset = q?.["resetsAt"];
|
|
61
|
+
const at = Date.parse(String(d["timestamp"]));
|
|
62
|
+
if ((type === "five_hour" || type === "seven_day") && typeof reset === "number" && !Number.isNaN(at) &&
|
|
63
|
+
(d["apiErrorStatus"] === 429 || q?.["status"] === "rejected")) {
|
|
64
|
+
out.push({ type, reset, at });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch { /* línea rota o a medias */ }
|
|
68
|
+
i = endLine < 0 ? -1 : text.indexOf('"quotaLimits"', endLine);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** Hasta el último salto de línea: una línea a medias se lee entera la próxima vez. */
|
|
73
|
+
function readFrom(file, start, size) {
|
|
74
|
+
const fd = (0, node_fs_1.openSync)(file, "r");
|
|
75
|
+
try {
|
|
76
|
+
const buf = Buffer.alloc(size - start);
|
|
77
|
+
(0, node_fs_1.readSync)(fd, buf, 0, buf.length, start);
|
|
78
|
+
const nl = buf.lastIndexOf(0x0a);
|
|
79
|
+
if (nl < 0)
|
|
80
|
+
return { text: "", end: start };
|
|
81
|
+
return { text: buf.subarray(0, nl + 1).toString("utf8"), end: start + nl + 1 };
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
(0, node_fs_1.closeSync)(fd);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function jsonlFiles(dir, modifiedAfter) {
|
|
88
|
+
const out = [];
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
for (const e of entries) {
|
|
97
|
+
const p = (0, node_path_1.join)(dir, e.name);
|
|
98
|
+
if (e.isDirectory())
|
|
99
|
+
out.push(...jsonlFiles(p, modifiedAfter));
|
|
100
|
+
else if (e.name.endsWith(".jsonl")) {
|
|
101
|
+
try {
|
|
102
|
+
if ((0, node_fs_1.statSync)(p).mtimeMs >= modifiedAfter)
|
|
103
|
+
out.push(p);
|
|
104
|
+
}
|
|
105
|
+
catch { /* se borró mientras tanto */ }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=plan-blocks.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Los planes de Claude que analiza `estela plan`: lo único que Estela sabe de
|
|
3
|
+
* ellos, en un solo sitio y con su fuente.
|
|
4
|
+
*
|
|
5
|
+
* - El multiplicador por sesión de 5 h es el que publica Anthropic: Max 5x da
|
|
6
|
+
* 5 veces la cuota por sesión de Pro, y Max 20x, 20 veces.
|
|
7
|
+
* - Del límite semanal Anthropic no publica la relación entre planes, así que
|
|
8
|
+
* aquí no hay ningún número para él, y no se compara.
|
|
9
|
+
* - Los precios son los de la suscripción web, en USD al mes, y caducan:
|
|
10
|
+
* `estela plan` avisa si la fecha de comprobación tiene más de 90 días.
|
|
11
|
+
*/
|
|
12
|
+
export type ClaudePlanId = "pro" | "max5" | "max20";
|
|
13
|
+
export interface ClaudePlan {
|
|
14
|
+
readonly id: ClaudePlanId;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
/** Veces la cuota por sesión de 5 h de Pro. */
|
|
17
|
+
readonly sessionMultiplier: number;
|
|
18
|
+
readonly monthlyUsd: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const CLAUDE_PLANS: readonly ClaudePlan[];
|
|
21
|
+
export declare const MULTIPLIER_SOURCE = "https://support.claude.com/en/articles/11049741-what-is-the-max-plan";
|
|
22
|
+
export declare const PRICES_SOURCE = "https://claude.com/pricing";
|
|
23
|
+
/** Cuándo se comprobaron los precios en `PRICES_SOURCE`. */
|
|
24
|
+
export declare const PRICES_CHECKED_AT = "2026-10-19";
|
|
25
|
+
export declare const PRICES_STALE_DAYS = 90;
|
|
26
|
+
export declare function planById(id: string): ClaudePlan | null;
|
|
27
|
+
//# sourceMappingURL=plan-prices.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Los planes de Claude que analiza `estela plan`: lo único que Estela sabe de
|
|
4
|
+
* ellos, en un solo sitio y con su fuente.
|
|
5
|
+
*
|
|
6
|
+
* - El multiplicador por sesión de 5 h es el que publica Anthropic: Max 5x da
|
|
7
|
+
* 5 veces la cuota por sesión de Pro, y Max 20x, 20 veces.
|
|
8
|
+
* - Del límite semanal Anthropic no publica la relación entre planes, así que
|
|
9
|
+
* aquí no hay ningún número para él, y no se compara.
|
|
10
|
+
* - Los precios son los de la suscripción web, en USD al mes, y caducan:
|
|
11
|
+
* `estela plan` avisa si la fecha de comprobación tiene más de 90 días.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.PRICES_STALE_DAYS = exports.PRICES_CHECKED_AT = exports.PRICES_SOURCE = exports.MULTIPLIER_SOURCE = exports.CLAUDE_PLANS = void 0;
|
|
15
|
+
exports.planById = planById;
|
|
16
|
+
exports.CLAUDE_PLANS = [
|
|
17
|
+
{ id: "pro", name: "Pro", sessionMultiplier: 1, monthlyUsd: 20 },
|
|
18
|
+
{ id: "max5", name: "Max 5x", sessionMultiplier: 5, monthlyUsd: 100 },
|
|
19
|
+
{ id: "max20", name: "Max 20x", sessionMultiplier: 20, monthlyUsd: 200 },
|
|
20
|
+
];
|
|
21
|
+
exports.MULTIPLIER_SOURCE = "https://support.claude.com/en/articles/11049741-what-is-the-max-plan";
|
|
22
|
+
exports.PRICES_SOURCE = "https://claude.com/pricing";
|
|
23
|
+
/** Cuándo se comprobaron los precios en `PRICES_SOURCE`. */
|
|
24
|
+
exports.PRICES_CHECKED_AT = "2026-10-19";
|
|
25
|
+
exports.PRICES_STALE_DAYS = 90;
|
|
26
|
+
function planById(id) {
|
|
27
|
+
return exports.CLAUDE_PLANS.find((p) => p.id === id) ?? null;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=plan-prices.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { PlanAnalysis } from "./metrics/plan.js";
|
|
2
|
+
/**
|
|
3
|
+
* Lo que enseña `estela plan`: el texto (es/en) y el JSON.
|
|
4
|
+
*
|
|
5
|
+
* Aparte de cli.ts para poder comprobar cada frase: ninguna puede decir
|
|
6
|
+
* cuánto te queda ni hablar de tokens. Lo que hay son los porcentajes que
|
|
7
|
+
* mandó Claude Code y las equivalencias por el multiplicador publicado.
|
|
8
|
+
*/
|
|
9
|
+
export interface BlockCounts {
|
|
10
|
+
readonly fiveHour: number;
|
|
11
|
+
readonly sevenDay: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ReportOptions {
|
|
14
|
+
readonly color: boolean;
|
|
15
|
+
readonly now: Date;
|
|
16
|
+
/** Bloqueos 429 de los transcripts en el período, si se pudieron leer. */
|
|
17
|
+
readonly blocks: BlockCounts | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function renderPlanReport(a: PlanAnalysis, o: ReportOptions): string;
|
|
20
|
+
export declare function recommendationText(a: PlanAnalysis): string;
|
|
21
|
+
export declare function pricesStale(now: Date): boolean;
|
|
22
|
+
/** Sin plan declarado: cómo declararlo. */
|
|
23
|
+
export declare function renderNoPlan(): string;
|
|
24
|
+
export declare function renderNoData(a: PlanAnalysis, apiKey: boolean, color: boolean): string;
|
|
25
|
+
/**
|
|
26
|
+
* `estela plan --json`. Estructura estable (documentada en el README):
|
|
27
|
+
* campos nuevos se pueden añadir, ninguno cambia de nombre ni de significado.
|
|
28
|
+
*/
|
|
29
|
+
export declare function planToJson(a: PlanAnalysis, o: {
|
|
30
|
+
readonly blocks: BlockCounts | null;
|
|
31
|
+
readonly now: Date;
|
|
32
|
+
}): {
|
|
33
|
+
version: number;
|
|
34
|
+
plan: {
|
|
35
|
+
id: import("./plan-prices.js").ClaudePlanId;
|
|
36
|
+
name: string;
|
|
37
|
+
since: string;
|
|
38
|
+
sessionMultiplier: number;
|
|
39
|
+
};
|
|
40
|
+
period: {
|
|
41
|
+
from: string;
|
|
42
|
+
to: string;
|
|
43
|
+
days: number;
|
|
44
|
+
daysWithData: number;
|
|
45
|
+
samples: number;
|
|
46
|
+
};
|
|
47
|
+
dataState: import("./metrics/plan.js").DataState;
|
|
48
|
+
fiveHour: {
|
|
49
|
+
windows: number;
|
|
50
|
+
medianPeak: number | null;
|
|
51
|
+
p90Peak: number | null;
|
|
52
|
+
maxPeak: number | null;
|
|
53
|
+
caps: number;
|
|
54
|
+
stoppedMinutesUpperBound: number;
|
|
55
|
+
};
|
|
56
|
+
week: {
|
|
57
|
+
weeks: number;
|
|
58
|
+
medianPeak: number | null;
|
|
59
|
+
maxPeak: number | null;
|
|
60
|
+
weeksAtOrAbove90: number;
|
|
61
|
+
};
|
|
62
|
+
blocks429: BlockCounts | null;
|
|
63
|
+
alternatives: {
|
|
64
|
+
plan: import("./plan-prices.js").ClaudePlanId;
|
|
65
|
+
name: string;
|
|
66
|
+
direction: "lower" | "higher";
|
|
67
|
+
monthlyUsd: number;
|
|
68
|
+
monthlyDiffUsd: number;
|
|
69
|
+
p90Equivalent: number | null;
|
|
70
|
+
maxEquivalent: number | null;
|
|
71
|
+
blockedWindows: number;
|
|
72
|
+
capsAvoided: number;
|
|
73
|
+
}[];
|
|
74
|
+
recommendation: {
|
|
75
|
+
kind: "tight" | "enough" | "almost" | "fits";
|
|
76
|
+
provisional: boolean;
|
|
77
|
+
plan: import("./plan-prices.js").ClaudePlanId | null;
|
|
78
|
+
blockedWindows: number | null;
|
|
79
|
+
capsAvoided: number | null;
|
|
80
|
+
text: string;
|
|
81
|
+
} | null;
|
|
82
|
+
weeklyBetweenPlans: string;
|
|
83
|
+
sources: {
|
|
84
|
+
sessionMultiplier: string;
|
|
85
|
+
prices: string;
|
|
86
|
+
pricesCheckedAt: string;
|
|
87
|
+
pricesStale: boolean;
|
|
88
|
+
};
|
|
89
|
+
plans: {
|
|
90
|
+
id: import("./plan-prices.js").ClaudePlanId;
|
|
91
|
+
name: string;
|
|
92
|
+
sessionMultiplier: number;
|
|
93
|
+
monthlyUsd: number;
|
|
94
|
+
}[];
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=plan-report.d.ts.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderPlanReport = renderPlanReport;
|
|
4
|
+
exports.recommendationText = recommendationText;
|
|
5
|
+
exports.pricesStale = pricesStale;
|
|
6
|
+
exports.renderNoPlan = renderNoPlan;
|
|
7
|
+
exports.renderNoData = renderNoData;
|
|
8
|
+
exports.planToJson = planToJson;
|
|
9
|
+
const index_js_1 = require("./i18n/index.js");
|
|
10
|
+
const plan_prices_js_1 = require("./plan-prices.js");
|
|
11
|
+
const statusline_js_1 = require("./statusline.js");
|
|
12
|
+
const ESC = "\x1b[";
|
|
13
|
+
const JADE = `${ESC}38;2;70;192;167m`; // #46C0A7, como en la status line
|
|
14
|
+
const AMBER = `${ESC}38;2;221;168;90m`;
|
|
15
|
+
const DIM = `${ESC}90m`;
|
|
16
|
+
const RESET = `${ESC}0m`;
|
|
17
|
+
const LABEL = 21;
|
|
18
|
+
function renderPlanReport(a, o) {
|
|
19
|
+
const paint = (code, text) => (o.color ? `${code}${text}${RESET}` : text);
|
|
20
|
+
const row = (label, value) => `${label.padEnd(LABEL)}${value}`;
|
|
21
|
+
const out = [];
|
|
22
|
+
out.push(header(a, paint));
|
|
23
|
+
out.push("");
|
|
24
|
+
const f = a.fiveHour;
|
|
25
|
+
out.push(row((0, index_js_1.tr) `Ventanas de 5 h`, f.windows.length === 0
|
|
26
|
+
? (0, index_js_1.tr) `ninguna completa todavía`
|
|
27
|
+
: (0, index_js_1.tr) `${f.windows.length} ventanas · pico mediano ${pct(f.medianPeak)} % · p90 ${pct(f.p90Peak)} % · máximo ${pct(f.maxPeak)} %`));
|
|
28
|
+
out.push(row((0, index_js_1.tr) `Topes (≥95 %)`, String(f.caps)));
|
|
29
|
+
if (f.stoppedMs > 0) {
|
|
30
|
+
out.push(row((0, index_js_1.tr) `Parado tras topes`, (0, index_js_1.tr) `hasta ${(0, statusline_js_1.compactDuration)(f.stoppedMs / 1000)} (estimado: no sabemos si seguías trabajando)`));
|
|
31
|
+
}
|
|
32
|
+
const w = a.week;
|
|
33
|
+
out.push(row((0, index_js_1.tr) `Semana`, w.weeks.length === 0
|
|
34
|
+
? (0, index_js_1.tr) `sin datos de la semana`
|
|
35
|
+
: (0, index_js_1.tr) `pico mediano ${pct(w.medianPeak)} % · máximo ${pct(w.maxPeak)} % · ${w.weeksAtOrAbove90} semanas ≥ 90 %`));
|
|
36
|
+
if (o.blocks) {
|
|
37
|
+
out.push(row((0, index_js_1.tr) `Bloqueos (429)`, (0, index_js_1.tr) `${o.blocks.fiveHour} de 5 h · ${o.blocks.sevenDay} semanales, según los transcripts de Claude Code`));
|
|
38
|
+
}
|
|
39
|
+
if (f.windows.length > 0) {
|
|
40
|
+
out.push("");
|
|
41
|
+
out.push((0, index_js_1.tr) `Si estuvieras en…`);
|
|
42
|
+
for (const alt of [...a.alternatives].sort((x, y) => x.plan.monthlyUsd - y.plan.monthlyUsd)) {
|
|
43
|
+
out.push(` ${`${alt.plan.name} (${usd(alt.plan.monthlyUsd)})`.padEnd(LABEL - 2)}${alternativeText(alt, a)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
out.push("");
|
|
47
|
+
if (a.dataState === "insufficient") {
|
|
48
|
+
out.push(paint(AMBER, (0, index_js_1.tr) `Datos de ${a.daysWithData} días. La recomendación necesita al menos 14.`));
|
|
49
|
+
}
|
|
50
|
+
else if (a.recommendation) {
|
|
51
|
+
const label = a.dataState === "provisional"
|
|
52
|
+
? (0, index_js_1.tr) `Recomendación (provisional, ${a.daysWithData} días)`
|
|
53
|
+
: (0, index_js_1.tr) `Recomendación`;
|
|
54
|
+
const text = recommendationText(a);
|
|
55
|
+
out.push(row(label, paint(a.recommendation.kind === "tight" ? AMBER : JADE, text)));
|
|
56
|
+
const diff = priceDiffText(a);
|
|
57
|
+
if (diff)
|
|
58
|
+
out.push(row("", paint(DIM, diff)));
|
|
59
|
+
}
|
|
60
|
+
out.push("");
|
|
61
|
+
out.push((0, index_js_1.tr) `Límite semanal entre planes: Anthropic no publica la relación. No lo estimamos.`);
|
|
62
|
+
out.push((0, index_js_1.tr) `Basado en el multiplicador por sesión publicado por Anthropic:`);
|
|
63
|
+
out.push(plan_prices_js_1.MULTIPLIER_SOURCE);
|
|
64
|
+
out.push((0, index_js_1.tr) `Anthropic puede cambiar los límites; repite el análisis cada mes.`);
|
|
65
|
+
out.push((0, index_js_1.tr) `Precios según la web de Anthropic a ${plan_prices_js_1.PRICES_CHECKED_AT}; compruébalo: ${plan_prices_js_1.PRICES_SOURCE}`);
|
|
66
|
+
if (pricesStale(o.now))
|
|
67
|
+
out.push(paint(AMBER, (0, index_js_1.tr) `Esa fecha tiene más de ${plan_prices_js_1.PRICES_STALE_DAYS} días: los precios pueden estar desactualizados.`));
|
|
68
|
+
return out.join("\n");
|
|
69
|
+
}
|
|
70
|
+
function header(a, paint) {
|
|
71
|
+
return `${paint(JADE, "estela")} plan · ${a.plan.name} ${(0, index_js_1.tr) `(desde ${day(a.since)})`} · ${(0, index_js_1.tr) `${a.daysWithData} días con datos`}`;
|
|
72
|
+
}
|
|
73
|
+
function alternativeText(alt, a) {
|
|
74
|
+
const n = a.fiveHour.windows.length;
|
|
75
|
+
if (alt.direction === "higher") {
|
|
76
|
+
if (a.fiveHour.caps === 0)
|
|
77
|
+
return (0, index_js_1.tr) `sin topes, igual que ahora`;
|
|
78
|
+
return (0, index_js_1.tr) `p90 equivalente ${pct(alt.p90Equivalent)} % · habrías evitado ${alt.capsAvoided} de ${a.fiveHour.caps} topes (con el uso que llegó a registrarse)`;
|
|
79
|
+
}
|
|
80
|
+
return alt.blockedWindows === 0
|
|
81
|
+
? (0, index_js_1.tr) `p90 equivalente ${pct(alt.p90Equivalent)} % · no te habría bloqueado en ninguna de las ${n} ventanas`
|
|
82
|
+
: (0, index_js_1.tr) `p90 equivalente ${pct(alt.p90Equivalent)} % · te habría bloqueado en ${alt.blockedWindows} de ${n} ventanas`;
|
|
83
|
+
}
|
|
84
|
+
function recommendationText(a) {
|
|
85
|
+
const r = a.recommendation;
|
|
86
|
+
if (!r)
|
|
87
|
+
return "";
|
|
88
|
+
switch (r.kind) {
|
|
89
|
+
case "tight":
|
|
90
|
+
return r.higher && r.capsAvoided !== null
|
|
91
|
+
? (0, index_js_1.tr) `Vas justo con tu plan. Con ${r.higher.name} habrías evitado ${r.capsAvoided} de ${a.fiveHour.caps} topes.`
|
|
92
|
+
: (0, index_js_1.tr) `Vas justo con tu plan.`;
|
|
93
|
+
case "enough":
|
|
94
|
+
return (0, index_js_1.tr) `Con los datos de estas ${r.weeks} semanas, el plan ${r.plan.name} te habría bastado en todas las ventanas de 5 h.`;
|
|
95
|
+
case "almost":
|
|
96
|
+
return (0, index_js_1.tr) `El plan ${r.plan.name} te habría bastado casi siempre: te habría bloqueado ${r.blocked} veces en estas semanas.`;
|
|
97
|
+
case "fits":
|
|
98
|
+
return (0, index_js_1.tr) `Tu plan actual encaja con tu uso.`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** La diferencia de precio de lo que se recomienda, si es cambiar de plan. */
|
|
102
|
+
function priceDiffText(a) {
|
|
103
|
+
const r = a.recommendation;
|
|
104
|
+
const other = r?.kind === "enough" || r?.kind === "almost" ? r.plan
|
|
105
|
+
: r?.kind === "tight" ? r.higher : null;
|
|
106
|
+
if (!other)
|
|
107
|
+
return null;
|
|
108
|
+
const diff = other.monthlyUsd - a.plan.monthlyUsd;
|
|
109
|
+
return diff < 0
|
|
110
|
+
? (0, index_js_1.tr) `${usd(-diff)} menos al mes, según la web de Anthropic a ${plan_prices_js_1.PRICES_CHECKED_AT}; compruébalo.`
|
|
111
|
+
: (0, index_js_1.tr) `${usd(diff)} más al mes, según la web de Anthropic a ${plan_prices_js_1.PRICES_CHECKED_AT}; compruébalo.`;
|
|
112
|
+
}
|
|
113
|
+
function pricesStale(now) {
|
|
114
|
+
return now.getTime() - Date.parse(`${plan_prices_js_1.PRICES_CHECKED_AT}T00:00:00Z`) > plan_prices_js_1.PRICES_STALE_DAYS * 86_400_000;
|
|
115
|
+
}
|
|
116
|
+
/** Sin plan declarado: cómo declararlo. */
|
|
117
|
+
function renderNoPlan() {
|
|
118
|
+
return [
|
|
119
|
+
(0, index_js_1.tr) `estela plan compara tu uso real de Claude con los otros planes. Primero dime cuál pagas:`,
|
|
120
|
+
"",
|
|
121
|
+
" estela plan set pro",
|
|
122
|
+
" estela plan set max5",
|
|
123
|
+
" estela plan set max20",
|
|
124
|
+
"",
|
|
125
|
+
(0, index_js_1.tr) `Si ya lo tenías antes de hoy, añade desde cuándo: --since AAAA-MM-DD`,
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
function renderNoData(a, apiKey, color) {
|
|
129
|
+
const paint = (code, text) => (color ? `${code}${text}${RESET}` : text);
|
|
130
|
+
return [
|
|
131
|
+
header(a, paint), "",
|
|
132
|
+
apiKey
|
|
133
|
+
? (0, index_js_1.tr) `Este análisis solo funciona con planes Pro o Max.`
|
|
134
|
+
: (0, index_js_1.tr) `Aún no hay datos. Instala la status line (estela statusline install) y vuelve en unos días.`,
|
|
135
|
+
...(apiKey ? [] : [(0, index_js_1.tr) `Solo se recogen en Claude Code en la terminal: la extensión de VS Code no ejecuta la status line.`]),
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
/**
|
|
140
|
+
* `estela plan --json`. Estructura estable (documentada en el README):
|
|
141
|
+
* campos nuevos se pueden añadir, ninguno cambia de nombre ni de significado.
|
|
142
|
+
*/
|
|
143
|
+
function planToJson(a, o) {
|
|
144
|
+
const r1 = (n) => (n === null ? null : Math.round(n * 10) / 10);
|
|
145
|
+
const rec = a.recommendation;
|
|
146
|
+
return {
|
|
147
|
+
version: 1,
|
|
148
|
+
plan: { id: a.plan.id, name: a.plan.name, since: a.since.toISOString(), sessionMultiplier: a.plan.sessionMultiplier },
|
|
149
|
+
period: { from: a.from.toISOString(), to: a.to.toISOString(), days: a.days, daysWithData: a.daysWithData, samples: a.samples },
|
|
150
|
+
dataState: a.dataState,
|
|
151
|
+
fiveHour: {
|
|
152
|
+
windows: a.fiveHour.windows.length,
|
|
153
|
+
medianPeak: r1(a.fiveHour.medianPeak), p90Peak: r1(a.fiveHour.p90Peak), maxPeak: r1(a.fiveHour.maxPeak),
|
|
154
|
+
caps: a.fiveHour.caps,
|
|
155
|
+
stoppedMinutesUpperBound: Math.round(a.fiveHour.stoppedMs / 60_000),
|
|
156
|
+
},
|
|
157
|
+
week: {
|
|
158
|
+
weeks: a.week.weeks.length,
|
|
159
|
+
medianPeak: r1(a.week.medianPeak), maxPeak: r1(a.week.maxPeak),
|
|
160
|
+
weeksAtOrAbove90: a.week.weeksAtOrAbove90,
|
|
161
|
+
},
|
|
162
|
+
blocks429: o.blocks,
|
|
163
|
+
alternatives: [...a.alternatives].sort((x, y) => x.plan.monthlyUsd - y.plan.monthlyUsd).map((x) => ({
|
|
164
|
+
plan: x.plan.id, name: x.plan.name, direction: x.direction,
|
|
165
|
+
monthlyUsd: x.plan.monthlyUsd, monthlyDiffUsd: x.plan.monthlyUsd - a.plan.monthlyUsd,
|
|
166
|
+
p90Equivalent: r1(x.p90Equivalent), maxEquivalent: r1(x.maxEquivalent),
|
|
167
|
+
blockedWindows: x.blockedWindows, capsAvoided: x.capsAvoided,
|
|
168
|
+
})),
|
|
169
|
+
recommendation: rec === null ? null : {
|
|
170
|
+
kind: rec.kind,
|
|
171
|
+
provisional: a.dataState === "provisional",
|
|
172
|
+
plan: rec.kind === "enough" || rec.kind === "almost" ? rec.plan.id : rec.kind === "tight" ? rec.higher?.id ?? null : null,
|
|
173
|
+
blockedWindows: rec.kind === "almost" ? rec.blocked : null,
|
|
174
|
+
capsAvoided: rec.kind === "tight" ? rec.capsAvoided : null,
|
|
175
|
+
text: recommendationText(a),
|
|
176
|
+
},
|
|
177
|
+
weeklyBetweenPlans: "not_published",
|
|
178
|
+
sources: {
|
|
179
|
+
sessionMultiplier: plan_prices_js_1.MULTIPLIER_SOURCE,
|
|
180
|
+
prices: plan_prices_js_1.PRICES_SOURCE, pricesCheckedAt: plan_prices_js_1.PRICES_CHECKED_AT, pricesStale: pricesStale(o.now),
|
|
181
|
+
},
|
|
182
|
+
plans: plan_prices_js_1.CLAUDE_PLANS.map((p) => ({ id: p.id, name: p.name, sessionMultiplier: p.sessionMultiplier, monthlyUsd: p.monthlyUsd })),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function pct(n) { return Math.round(n); }
|
|
186
|
+
function usd(n) { return (0, index_js_1.tr) `${n} $`; }
|
|
187
|
+
function day(d) {
|
|
188
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=plan-report.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `~/.estela/rate-limits.jsonl`: el historial de los porcentajes de límite que
|
|
3
|
+
* Claude Code manda a la status line. Es lo que analiza `estela plan`.
|
|
4
|
+
*
|
|
5
|
+
* Solo se guarda lo que manda Claude Code, tal cual: el porcentaje usado y el
|
|
6
|
+
* reinicio de cada ventana, y su versión. Ni sesión, ni rutas, ni proyectos.
|
|
7
|
+
* Estela no estima ningún límite: sin `rate_limits` en la entrada (API key, o
|
|
8
|
+
* la primera respuesta de la sesión aún no llegó), no se guarda nada.
|
|
9
|
+
*
|
|
10
|
+
* Los transcripts no traen estos porcentajes, así que no hay forma de
|
|
11
|
+
* recuperar el pasado: el historial empieza el día que se instala la 0.2.4. Y
|
|
12
|
+
* solo se recoge con Claude Code en la terminal; la extensión de VS Code no
|
|
13
|
+
* ejecuta la status line.
|
|
14
|
+
*
|
|
15
|
+
* Va por el camino rápido de la status line (100 ms): sin `node:sqlite`, una
|
|
16
|
+
* línea por `appendFileSync`, y cualquier error se ignora.
|
|
17
|
+
*/
|
|
18
|
+
export interface RateSample {
|
|
19
|
+
/** Epoch en ms de la muestra. */
|
|
20
|
+
readonly ts: number;
|
|
21
|
+
readonly src: "claude-code";
|
|
22
|
+
readonly w5?: number;
|
|
23
|
+
/** `resets_at` tal cual (epoch en s). */
|
|
24
|
+
readonly w5_reset?: number;
|
|
25
|
+
readonly w7?: number;
|
|
26
|
+
readonly w7_reset?: number;
|
|
27
|
+
/** Versión de Claude Code, si viene. */
|
|
28
|
+
readonly cc?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare const KEEP_DAYS = 120;
|
|
31
|
+
export declare function historyPath(dbPath: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* La muestra que sale del JSON de la status line, o null si no trae ningún
|
|
34
|
+
* porcentaje. Cada ventana que falte se omite: nunca se escribe un 0.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sampleOf(raw: unknown, now: Date): RateSample | null;
|
|
37
|
+
/**
|
|
38
|
+
* Si vale la pena guardar la muestra: han pasado 5 minutos desde la última,
|
|
39
|
+
* algún porcentaje cambió al menos un punto, o empezó una ventana nueva.
|
|
40
|
+
*/
|
|
41
|
+
export declare function shouldWrite(last: RateSample | undefined, s: RateSample): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Lo que hace la status line en cada evento. Nunca lanza: es un historial, y
|
|
44
|
+
* la línea tiene que salir igual aunque no se pueda escribir.
|
|
45
|
+
*
|
|
46
|
+
* Solo escribe si ya existe el directorio de Estela: la status line no es
|
|
47
|
+
* quien empieza a guardar datos.
|
|
48
|
+
*/
|
|
49
|
+
export declare function recordSample(dbPath: string, raw: unknown, now: Date): void;
|
|
50
|
+
/** Lee el historial. Las líneas rotas se saltan. */
|
|
51
|
+
export declare function readSamples(file: string): RateSample[];
|
|
52
|
+
/**
|
|
53
|
+
* Quita las muestras de hace más de 120 días. Reescribe en un fichero aparte y
|
|
54
|
+
* lo renombra encima: un corte a mitad no deja el historial a medias.
|
|
55
|
+
* Devuelve cuántas quitó.
|
|
56
|
+
*
|
|
57
|
+
* Corre desde la status line una vez al día, así que lo normal (nada que
|
|
58
|
+
* quitar) tiene que costar casi nada: el fichero se escribe en orden, y si la
|
|
59
|
+
* primera muestra no ha caducado, ninguna lo ha hecho. Solo entonces se lee
|
|
60
|
+
* entero, y el `ts` se saca del principio de cada línea sin parsear el JSON.
|
|
61
|
+
*/
|
|
62
|
+
export declare function pruneSamples(file: string, now: Date, keepDays?: number): number;
|
|
63
|
+
//# sourceMappingURL=rate-history.d.ts.map
|