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.
@@ -0,0 +1,110 @@
1
+ import { type ClaudePlan, type ClaudePlanId } from "../plan-prices.js";
2
+ import type { RateSample } from "../rate-history.js";
3
+ /**
4
+ * "¿El plan de Claude que pago es el que me conviene?"
5
+ *
6
+ * Funciones puras sobre el historial de la status line (rate-limits.jsonl):
7
+ * entran muestras, salen cifras, y cada cifra tiene su test.
8
+ *
9
+ * La regla de siempre, más estricta aquí que en ningún sitio: Estela no
10
+ * inventa ni estima el límite de ningún plan. Usa solo
11
+ *
12
+ * - los porcentajes que manda Claude Code, tal cual, y
13
+ * - el multiplicador por sesión de 5 h que publica Anthropic (plan-prices.ts).
14
+ *
15
+ * Por eso la comparación entre planes es solo de la ventana de 5 h: del
16
+ * límite semanal Anthropic no publica la relación entre planes, y no se
17
+ * calcula. Y nada de "te quedan X" ni de cuándo te vas a quedar sin cuota.
18
+ */
19
+ /** Pico desde el que una ventana cuenta como tope. */
20
+ export declare const CAP_AT = 95;
21
+ export declare const MIN_DAYS = 7;
22
+ export declare const FULL_DAYS = 14;
23
+ export interface FiveHourWindow {
24
+ /** `resets_at` de la ventana (epoch en s). */
25
+ readonly reset: number;
26
+ readonly peak: number;
27
+ readonly samples: number;
28
+ readonly capped: boolean;
29
+ /** Desde la primera muestra ≥ 99 % hasta el reinicio (o hasta ahora). Cota superior, estimada. */
30
+ readonly stoppedMs: number;
31
+ }
32
+ export interface WeekWindow {
33
+ readonly reset: number;
34
+ readonly peak: number;
35
+ }
36
+ export interface Alternative {
37
+ readonly plan: ClaudePlan;
38
+ readonly direction: "lower" | "higher";
39
+ readonly p90Equivalent: number;
40
+ readonly maxEquivalent: number;
41
+ /** Ventanas cuyo pico equivalente pasa de 100: te habrían bloqueado. */
42
+ readonly blockedWindows: number;
43
+ /** Subiendo de plan: topes de ahora cuyo pico equivalente se queda por debajo del tope. */
44
+ readonly capsAvoided: number;
45
+ }
46
+ export type Recommendation = {
47
+ readonly kind: "tight";
48
+ readonly higher: ClaudePlan | null;
49
+ readonly capsAvoided: number | null;
50
+ } | {
51
+ readonly kind: "enough";
52
+ readonly plan: ClaudePlan;
53
+ readonly weeks: number;
54
+ } | {
55
+ readonly kind: "almost";
56
+ readonly plan: ClaudePlan;
57
+ readonly blocked: number;
58
+ } | {
59
+ readonly kind: "fits";
60
+ };
61
+ export type DataState = "none" | "insufficient" | "provisional" | "ok";
62
+ export interface PlanAnalysis {
63
+ readonly plan: ClaudePlan;
64
+ readonly since: Date;
65
+ readonly from: Date;
66
+ readonly to: Date;
67
+ readonly days: number;
68
+ readonly daysWithData: number;
69
+ readonly samples: number;
70
+ readonly dataState: DataState;
71
+ readonly fiveHour: {
72
+ readonly windows: readonly FiveHourWindow[];
73
+ readonly medianPeak: number | null;
74
+ readonly p90Peak: number | null;
75
+ readonly maxPeak: number | null;
76
+ readonly caps: number;
77
+ readonly stoppedMs: number;
78
+ };
79
+ readonly week: {
80
+ readonly weeks: readonly WeekWindow[];
81
+ readonly medianPeak: number | null;
82
+ readonly maxPeak: number | null;
83
+ readonly weeksAtOrAbove90: number;
84
+ };
85
+ readonly alternatives: readonly Alternative[];
86
+ /** null con menos de 7 días de datos. */
87
+ readonly recommendation: Recommendation | null;
88
+ }
89
+ export interface PlanAnalysisInput {
90
+ readonly samples: readonly RateSample[];
91
+ readonly plan: ClaudePlanId;
92
+ readonly since: Date;
93
+ readonly now: Date;
94
+ /** Período, en días hacia atrás desde `now`. */
95
+ readonly days: number;
96
+ /** Para contar los días con datos en tu zona horaria. Por defecto, la de esta máquina. */
97
+ readonly localDay?: (ms: number) => string;
98
+ }
99
+ export declare function analyzePlan(input: PlanAnalysisInput): PlanAnalysis;
100
+ /**
101
+ * Una ventana por `resets_at`, redondeado al minuto: dos sesiones pueden
102
+ * mandar el mismo reinicio con segundos distintos. Una muestra sin reinicio
103
+ * no se puede asignar a ninguna ventana y no cuenta.
104
+ */
105
+ export declare function fiveHourWindows(samples: readonly RateSample[], now: Date): FiveHourWindow[];
106
+ export declare function weekWindows(samples: readonly RateSample[]): WeekWindow[];
107
+ export declare function median(values: readonly number[]): number | null;
108
+ /** Percentil 90 por rango más cercano: siempre es un valor observado. */
109
+ export declare function p90(values: readonly number[]): number | null;
110
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FULL_DAYS = exports.MIN_DAYS = exports.CAP_AT = void 0;
4
+ exports.analyzePlan = analyzePlan;
5
+ exports.fiveHourWindows = fiveHourWindows;
6
+ exports.weekWindows = weekWindows;
7
+ exports.median = median;
8
+ exports.p90 = p90;
9
+ const plan_prices_js_1 = require("../plan-prices.js");
10
+ /**
11
+ * "¿El plan de Claude que pago es el que me conviene?"
12
+ *
13
+ * Funciones puras sobre el historial de la status line (rate-limits.jsonl):
14
+ * entran muestras, salen cifras, y cada cifra tiene su test.
15
+ *
16
+ * La regla de siempre, más estricta aquí que en ningún sitio: Estela no
17
+ * inventa ni estima el límite de ningún plan. Usa solo
18
+ *
19
+ * - los porcentajes que manda Claude Code, tal cual, y
20
+ * - el multiplicador por sesión de 5 h que publica Anthropic (plan-prices.ts).
21
+ *
22
+ * Por eso la comparación entre planes es solo de la ventana de 5 h: del
23
+ * límite semanal Anthropic no publica la relación entre planes, y no se
24
+ * calcula. Y nada de "te quedan X" ni de cuándo te vas a quedar sin cuota.
25
+ */
26
+ /** Pico desde el que una ventana cuenta como tope. */
27
+ exports.CAP_AT = 95;
28
+ /** Desde aquí se cuenta el tiempo parado tras un tope. */
29
+ const STOPPED_FROM = 99;
30
+ /** Una semana con este pico o más es "ir justo". */
31
+ const TIGHT_WEEK_AT = 90;
32
+ const TIGHT_CAPS_PER_WEEK = 2;
33
+ /** Un plan inferior "basta" si no habría bloqueado nunca y su p90 equivalente no pasa de aquí. */
34
+ const ENOUGH_P90 = 80;
35
+ const ALMOST_MAX_BLOCKS = 3;
36
+ exports.MIN_DAYS = 7;
37
+ exports.FULL_DAYS = 14;
38
+ function analyzePlan(input) {
39
+ const plan = (0, plan_prices_js_1.planById)(input.plan);
40
+ if (!plan)
41
+ throw new Error(`plan desconocido: ${input.plan}`);
42
+ const to = input.now;
43
+ const from = new Date(Math.max(input.since.getTime(), to.getTime() - input.days * 86_400_000));
44
+ const localDay = input.localDay ?? defaultLocalDay;
45
+ const samples = dedupe(input.samples)
46
+ .filter((s) => s.ts >= from.getTime() && s.ts <= to.getTime())
47
+ .sort((a, b) => a.ts - b.ts);
48
+ const daysWithData = new Set(samples.map((s) => localDay(s.ts))).size;
49
+ const windows = fiveHourWindows(samples, to);
50
+ const peaks = windows.map((w) => w.peak);
51
+ const weeks = weekWindows(samples);
52
+ const weekPeaks = weeks.map((w) => w.peak);
53
+ const caps = windows.filter((w) => w.capped).length;
54
+ const alternatives = plan_prices_js_1.CLAUDE_PLANS.filter((p) => p.id !== plan.id).map((target) => {
55
+ const factor = plan.sessionMultiplier / target.sessionMultiplier;
56
+ const eq = windows.map((w) => w.peak * factor);
57
+ return {
58
+ plan: target,
59
+ direction: target.sessionMultiplier < plan.sessionMultiplier ? "lower" : "higher",
60
+ p90Equivalent: p90(eq) ?? 0,
61
+ maxEquivalent: eq.length ? Math.max(...eq) : 0,
62
+ blockedWindows: eq.filter((e) => e > 100).length,
63
+ capsAvoided: factor < 1 ? windows.filter((w) => w.capped && w.peak * factor < exports.CAP_AT).length : 0,
64
+ };
65
+ });
66
+ const dataState = samples.length === 0 ? "none"
67
+ : daysWithData < exports.MIN_DAYS ? "insufficient"
68
+ : daysWithData < exports.FULL_DAYS ? "provisional" : "ok";
69
+ const analysis = {
70
+ plan, since: input.since, from, to, days: input.days, daysWithData, samples: samples.length, dataState,
71
+ fiveHour: {
72
+ windows, medianPeak: median(peaks), p90Peak: p90(peaks), maxPeak: peaks.length ? Math.max(...peaks) : null,
73
+ caps, stoppedMs: windows.reduce((s, w) => s + w.stoppedMs, 0),
74
+ },
75
+ week: {
76
+ weeks, medianPeak: median(weekPeaks), maxPeak: weekPeaks.length ? Math.max(...weekPeaks) : null,
77
+ weeksAtOrAbove90: weekPeaks.filter((p) => p >= TIGHT_WEEK_AT).length,
78
+ },
79
+ alternatives,
80
+ };
81
+ return {
82
+ ...analysis,
83
+ recommendation: dataState === "provisional" || dataState === "ok" ? recommend(analysis) : null,
84
+ };
85
+ }
86
+ /** Las cuatro reglas, en orden. Conservadoras: ante la duda, el plan actual. */
87
+ function recommend(a) {
88
+ const weeksOfData = a.daysWithData / 7;
89
+ const capsPerWeek = weeksOfData > 0 ? a.fiveHour.caps / weeksOfData : 0;
90
+ if (capsPerWeek >= TIGHT_CAPS_PER_WEEK || a.week.weeksAtOrAbove90 > 0) {
91
+ const higher = a.alternatives
92
+ .filter((x) => x.direction === "higher")
93
+ .sort((x, y) => x.plan.sessionMultiplier - y.plan.sessionMultiplier)[0];
94
+ return { kind: "tight", higher: higher?.plan ?? null, capsAvoided: higher ? higher.capsAvoided : null };
95
+ }
96
+ const lower = a.alternatives
97
+ .filter((x) => x.direction === "lower")
98
+ .sort((x, y) => x.plan.monthlyUsd - y.plan.monthlyUsd);
99
+ const enough = lower.find((x) => x.blockedWindows === 0 && x.p90Equivalent <= ENOUGH_P90);
100
+ if (enough)
101
+ return { kind: "enough", plan: enough.plan, weeks: Math.max(1, Math.round(weeksOfData)) };
102
+ const almost = lower.find((x) => x.blockedWindows >= 1 && x.blockedWindows <= ALMOST_MAX_BLOCKS);
103
+ if (almost)
104
+ return { kind: "almost", plan: almost.plan, blocked: almost.blockedWindows };
105
+ return { kind: "fits" };
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ /**
109
+ * Una ventana por `resets_at`, redondeado al minuto: dos sesiones pueden
110
+ * mandar el mismo reinicio con segundos distintos. Una muestra sin reinicio
111
+ * no se puede asignar a ninguna ventana y no cuenta.
112
+ */
113
+ function fiveHourWindows(samples, now) {
114
+ const groups = new Map();
115
+ for (const s of samples) {
116
+ if (s.w5 === undefined || s.w5_reset === undefined)
117
+ continue;
118
+ const key = Math.round(s.w5_reset / 60);
119
+ const g = groups.get(key);
120
+ if (g)
121
+ g.push(s);
122
+ else
123
+ groups.set(key, [s]);
124
+ }
125
+ return [...groups.entries()].sort((a, b) => a[0] - b[0]).map(([key, g]) => {
126
+ const peak = Math.max(...g.map((s) => s.w5));
127
+ const capped = peak >= exports.CAP_AT;
128
+ const first99 = g.filter((s) => s.w5 >= STOPPED_FROM).map((s) => s.ts).sort((a, b) => a - b)[0];
129
+ const end = Math.min(key * 60_000, now.getTime());
130
+ return {
131
+ reset: key * 60, peak, samples: g.length, capped,
132
+ stoppedMs: capped && first99 !== undefined ? Math.max(0, end - first99) : 0,
133
+ };
134
+ });
135
+ }
136
+ function weekWindows(samples) {
137
+ const peaks = new Map();
138
+ for (const s of samples) {
139
+ if (s.w7 === undefined || s.w7_reset === undefined)
140
+ continue;
141
+ const key = Math.round(s.w7_reset / 60);
142
+ peaks.set(key, Math.max(peaks.get(key) ?? -Infinity, s.w7));
143
+ }
144
+ return [...peaks.entries()].sort((a, b) => a[0] - b[0]).map(([key, peak]) => ({ reset: key * 60, peak }));
145
+ }
146
+ /** Dos sesiones a la vez pueden escribir la misma muestra. */
147
+ function dedupe(samples) {
148
+ const seen = new Set();
149
+ return samples.filter((s) => {
150
+ const k = `${s.ts}|${s.w5}|${s.w5_reset}|${s.w7}|${s.w7_reset}`;
151
+ if (seen.has(k))
152
+ return false;
153
+ seen.add(k);
154
+ return true;
155
+ });
156
+ }
157
+ function median(values) {
158
+ if (values.length === 0)
159
+ return null;
160
+ const v = [...values].sort((a, b) => a - b);
161
+ const mid = Math.floor(v.length / 2);
162
+ return v.length % 2 ? v[mid] : (v[mid - 1] + v[mid]) / 2;
163
+ }
164
+ /** Percentil 90 por rango más cercano: siempre es un valor observado. */
165
+ function p90(values) {
166
+ if (values.length === 0)
167
+ return null;
168
+ const v = [...values].sort((a, b) => a - b);
169
+ return v[Math.ceil(0.9 * v.length) - 1];
170
+ }
171
+ function defaultLocalDay(ms) {
172
+ const d = new Date(ms);
173
+ return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
174
+ }
175
+ //# sourceMappingURL=plan.js.map
@@ -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