estela 0.2.1 → 0.2.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 CHANGED
@@ -288,7 +288,9 @@ Hours measured today on this project (the same number the report will
288
288
  give), this project's share of your Claude Code usage this week, and — only
289
289
  if Claude Code sends them — its own weekly and 5-hour limit percentages, as
290
290
  they come. Estela never estimates a limit or counts down to a reset. The
291
- 5-hour figure turns amber from 80%. On a narrow terminal the line drops the
291
+ 5-hour figure turns amber from 80%. A new session shows the last figures Claude
292
+ Code sent until fresh ones arrive, if that window hasn't reset yet. On a
293
+ narrow terminal the line drops the
292
294
  5-hour figure first, then the week, then shortens the name, then this
293
295
  project, then today; `estela` always stays. It runs in about 50 ms and only
294
296
  reads what's new in the current session's transcript.
@@ -3,31 +3,60 @@
3
3
  * llamadas.
4
4
  *
5
5
  * - `files`: hasta qué byte se leyó cada transcript, para leer solo lo nuevo.
6
- * - `fiveHour`: el último `rate_limits.five_hour.resets_at` que mandó Claude
7
- * Code, y cuándo se vio. `estela quota` lo usa para saber dónde empieza de
8
- * verdad la ventana de 5 h en vez de estimarla.
6
+ * - `fiveHour` y `sevenDay`: lo último que mandó Claude Code en rate_limits
7
+ * (porcentaje y reinicio) y cuándo se vio. La status line lo enseña al
8
+ * arrancar una sesión, antes de que lleguen datos nuevos, y `estela quota`
9
+ * usa el reinicio de la 5 h para saber dónde empieza de verdad la ventana.
9
10
  *
10
11
  * Es una caché: si se pierde o se corrompe, todo sigue funcionando, solo que
11
12
  * relee más o vuelve a estimar.
12
13
  */
14
+ /** Una ventana de límite tal como la mandó Claude Code, y cuándo se vio. */
15
+ export interface SeenLimit {
16
+ resetsAt: string;
17
+ seenAt: string;
18
+ /** El porcentaje usado. Falta si Claude Code mandó el reinicio sin él. */
19
+ usedPercentage?: number;
20
+ }
13
21
  export interface StatuslineCache {
14
22
  files: Record<string, {
15
23
  offset: number;
16
24
  seenAt: string;
17
25
  }>;
18
- fiveHour?: {
19
- resetsAt: string;
20
- seenAt: string;
21
- };
26
+ fiveHour?: SeenLimit;
27
+ sevenDay?: SeenLimit;
22
28
  }
23
29
  export declare function cachePath(dbPath: string): string;
24
30
  export declare function loadCache(dbPath: string): StatuslineCache;
25
31
  export declare function saveCache(dbPath: string, cache: StatuslineCache): void;
32
+ /** Lo que llega de rate_limits para una ventana (cualquiera de los dos puede faltar). */
33
+ export interface LimitInput {
34
+ readonly usedPercentage: number | null;
35
+ readonly resetsAt: Date | null;
36
+ }
26
37
  /**
27
- * Apunta el `resets_at` de la ventana de 5 h. Solo si existe ya el directorio
28
- * de Estela: la status line no es quien empieza a guardar datos.
38
+ * Guarda lo nuevo de rate_limits y, para la ventana que no venga, devuelve el
39
+ * último porcentaje guardado si su reinicio aún no ha llegado.
40
+ *
41
+ * Es para el arranque de una sesión: Claude Code no manda rate_limits hasta la
42
+ * primera respuesta, y mientras tanto la línea perdía la semana y la 5 h que se
43
+ * sabían hace un momento. Un valor guardado cuyo reinicio ya pasó es de una
44
+ * ventana que terminó: no se enseña. En cuanto llega un dato nuevo, lo
45
+ * reemplaza.
46
+ *
47
+ * Usar lo guardado no cuenta como verlo otra vez: `seenAt` solo cambia con un
48
+ * dato nuevo, que es lo que `estela quota` mira para fiarse de la ventana.
49
+ *
50
+ * Solo escribe si existe ya el directorio de Estela: la status line no es
51
+ * quien empieza a guardar datos.
29
52
  */
30
- export declare function recordFiveHourReset(dbPath: string, resetsAt: Date, now: Date): void;
53
+ export declare function rememberLimits(dbPath: string, input: {
54
+ readonly fiveHour: LimitInput;
55
+ readonly sevenDay: LimitInput;
56
+ }, now: Date): {
57
+ fiveHour: number | null;
58
+ sevenDay: number | null;
59
+ };
31
60
  export declare function readFiveHourReset(dbPath: string): {
32
61
  resetsAt: Date;
33
62
  seenAt: Date;
@@ -3,13 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cachePath = cachePath;
4
4
  exports.loadCache = loadCache;
5
5
  exports.saveCache = saveCache;
6
- exports.recordFiveHourReset = recordFiveHourReset;
6
+ exports.rememberLimits = rememberLimits;
7
7
  exports.readFiveHourReset = readFiveHourReset;
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = require("node:path");
10
10
  const MAX_FILES = 30;
11
- /** Con el mismo reinicio, no se reescribe más a menudo que esto. */
12
- const FIVE_HOUR_REFRESH_MS = 10 * 60_000;
11
+ /** Con el mismo reinicio y el mismo porcentaje, no se reescribe más a menudo que esto. */
12
+ const REFRESH_MS = 10 * 60_000;
13
13
  function cachePath(dbPath) {
14
14
  return (0, node_path_1.join)((0, node_path_1.dirname)(dbPath), "statusline.json");
15
15
  }
@@ -33,19 +33,52 @@ function saveCache(dbPath, cache) {
33
33
  catch { /* sin caché, la próxima vez se relee: más lento, no incorrecto */ }
34
34
  }
35
35
  /**
36
- * Apunta el `resets_at` de la ventana de 5 h. Solo si existe ya el directorio
37
- * de Estela: la status line no es quien empieza a guardar datos.
36
+ * Guarda lo nuevo de rate_limits y, para la ventana que no venga, devuelve el
37
+ * último porcentaje guardado si su reinicio aún no ha llegado.
38
+ *
39
+ * Es para el arranque de una sesión: Claude Code no manda rate_limits hasta la
40
+ * primera respuesta, y mientras tanto la línea perdía la semana y la 5 h que se
41
+ * sabían hace un momento. Un valor guardado cuyo reinicio ya pasó es de una
42
+ * ventana que terminó: no se enseña. En cuanto llega un dato nuevo, lo
43
+ * reemplaza.
44
+ *
45
+ * Usar lo guardado no cuenta como verlo otra vez: `seenAt` solo cambia con un
46
+ * dato nuevo, que es lo que `estela quota` mira para fiarse de la ventana.
47
+ *
48
+ * Solo escribe si existe ya el directorio de Estela: la status line no es
49
+ * quien empieza a guardar datos.
38
50
  */
39
- function recordFiveHourReset(dbPath, resetsAt, now) {
40
- if (!(0, node_fs_1.existsSync)((0, node_path_1.dirname)(dbPath)))
41
- return;
51
+ function rememberLimits(dbPath, input, now) {
42
52
  const cache = loadCache(dbPath);
43
- const prev = cache.fiveHour;
44
- const same = prev && new Date(prev.resetsAt).getTime() === resetsAt.getTime();
45
- if (same && now.getTime() - new Date(prev.seenAt).getTime() < FIVE_HOUR_REFRESH_MS)
46
- return;
47
- cache.fiveHour = { resetsAt: resetsAt.toISOString(), seenAt: now.toISOString() };
48
- saveCache(dbPath, cache);
53
+ let changed = false;
54
+ const pick = (key, fresh) => {
55
+ const prev = cache[key];
56
+ if (fresh.resetsAt) {
57
+ const sameReset = !!prev && new Date(prev.resetsAt).getTime() === fresh.resetsAt.getTime();
58
+ // Un reinicio sin porcentaje de la misma ventana no borra el que había.
59
+ const pct = fresh.usedPercentage ?? (sameReset ? prev.usedPercentage ?? null : null);
60
+ const samePct = (prev?.usedPercentage ?? null) === pct;
61
+ const recent = !!prev && now.getTime() - new Date(prev.seenAt).getTime() < REFRESH_MS;
62
+ if (!(sameReset && samePct && recent)) {
63
+ cache[key] = {
64
+ resetsAt: fresh.resetsAt.toISOString(), seenAt: now.toISOString(),
65
+ ...(pct !== null ? { usedPercentage: pct } : {}),
66
+ };
67
+ changed = true;
68
+ }
69
+ }
70
+ if (fresh.usedPercentage !== null)
71
+ return fresh.usedPercentage;
72
+ // Sin dato nuevo: lo último que se vio, si su ventana sigue abierta.
73
+ const last = cache[key];
74
+ if (!last || last.usedPercentage === undefined)
75
+ return null;
76
+ return new Date(last.resetsAt) > now ? last.usedPercentage : null;
77
+ };
78
+ const out = { fiveHour: pick("fiveHour", input.fiveHour), sevenDay: pick("sevenDay", input.sevenDay) };
79
+ if (changed && (0, node_fs_1.existsSync)((0, node_path_1.dirname)(dbPath)))
80
+ saveCache(dbPath, cache);
81
+ return out;
49
82
  }
50
83
  function readFiveHourReset(dbPath) {
51
84
  const f = loadCache(dbPath).fiveHour;
@@ -55,13 +55,17 @@ async function runStatusline(argv) {
55
55
  const reserved = extra ? (0, statusline_js_1.stripAnsi)(extra).length + 3 : 0;
56
56
  const columns = Number(process.env["COLUMNS"]) || null;
57
57
  const now = new Date();
58
- if (input.fiveHourResetsAt) {
59
- try {
60
- (0, statusline_cache_js_1.recordFiveHourReset)(flags.db, input.fiveHourResetsAt, now);
61
- }
62
- catch { /* es una caché */ }
58
+ // Lo nuevo de rate_limits se guarda; lo que no venga (al arrancar una
59
+ // sesión) se toma de lo último visto, si su ventana sigue abierta.
60
+ let limits = { fiveHour: input.fiveHour, sevenDay: input.sevenDay };
61
+ try {
62
+ limits = (0, statusline_cache_js_1.rememberLimits)(flags.db, {
63
+ fiveHour: { usedPercentage: input.fiveHour, resetsAt: input.fiveHourResetsAt ?? null },
64
+ sevenDay: { usedPercentage: input.sevenDay, resetsAt: input.sevenDayResetsAt ?? null },
65
+ }, now);
63
66
  }
64
- const data = computeStatus(input, flags.db, now);
67
+ catch { /* es una caché: sin ella, la línea sale con lo que llegó */ }
68
+ const data = computeStatus({ ...input, ...limits }, flags.db, now);
65
69
  const ours = (0, statusline_js_1.renderStatus)(data, { columns, color: flags.color, reserved });
66
70
  process.stdout.write((extra ? `${ours}${flags.color ? "\x1b[90m | \x1b[0m" : " | "}${extra}` : ours) + "\n");
67
71
  }
@@ -83,7 +87,7 @@ function parseFlags(argv) {
83
87
  }
84
88
  /** Ejecutado a mano en una terminal, sin nada por stdin: el directorio actual. */
85
89
  function manualInput() {
86
- return { dir: process.cwd(), transcriptPath: null, fiveHour: null, sevenDay: null, fiveHourResetsAt: null };
90
+ return { dir: process.cwd(), transcriptPath: null, fiveHour: null, sevenDay: null, fiveHourResetsAt: null, sevenDayResetsAt: null };
87
91
  }
88
92
  function readStdin() {
89
93
  if (process.stdin.isTTY)
@@ -21,6 +21,8 @@ export interface StatusInput {
21
21
  * atrás): se guarda para que `estela quota` sepa dónde empieza la ventana.
22
22
  */
23
23
  readonly fiveHourResetsAt?: Date | null;
24
+ /** `rate_limits.seven_day.resets_at`: tampoco se enseña nunca. */
25
+ readonly sevenDayResetsAt?: Date | null;
24
26
  }
25
27
  /**
26
28
  * Lee la entrada. Lanza si no es un objeto JSON: quien llama imprime
@@ -23,6 +23,7 @@ function parseStatusInput(text) {
23
23
  fiveHour: pct(obj(limits["five_hour"])["used_percentage"]),
24
24
  sevenDay: pct(obj(limits["seven_day"])["used_percentage"]),
25
25
  fiveHourResetsAt: epoch(obj(limits["five_hour"])["resets_at"]),
26
+ sevenDayResetsAt: epoch(obj(limits["seven_day"])["resets_at"]),
26
27
  };
27
28
  }
28
29
  /** Segundos desde 1970 (lo que manda Claude Code); por si acaso, también milisegundos. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "estela",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Time tracking you never fill in: rebuilds your billable hours from Claude Code, Codex and GitHub Copilot sessions and your Git history, and splits what the AI cost per project.",
5
5
  "keywords": [
6
6
  "time-tracking",