estela 0.1.40 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/README.md +46 -2
  2. package/dist/billing/assign.d.ts +45 -0
  3. package/dist/billing/assign.js +159 -0
  4. package/dist/bin.js +23 -1
  5. package/dist/cli.js +220 -57
  6. package/dist/db/store.d.ts +2 -0
  7. package/dist/db/store.js +24 -1
  8. package/dist/demo.js +1 -1
  9. package/dist/i18n/en.js +39 -0
  10. package/dist/metrics/quota.d.ts +44 -4
  11. package/dist/metrics/quota.js +60 -14
  12. package/dist/project-of.d.ts +52 -0
  13. package/dist/project-of.js +124 -0
  14. package/dist/quota.d.ts +10 -1
  15. package/dist/quota.js +14 -20
  16. package/dist/server.js +10 -27
  17. package/dist/statusline-cache.d.ts +35 -0
  18. package/dist/statusline-cache.js +57 -0
  19. package/dist/statusline-install.d.ts +52 -0
  20. package/dist/statusline-install.js +115 -0
  21. package/dist/statusline-run.d.ts +8 -0
  22. package/dist/statusline-run.js +256 -0
  23. package/dist/statusline.d.ts +71 -0
  24. package/dist/statusline.js +152 -0
  25. package/dist/watchers/claude.d.ts +11 -1
  26. package/dist/watchers/claude.js +32 -15
  27. package/package.json +3 -1
  28. package/dist/billing/amortize.d.ts.map +0 -1
  29. package/dist/billing/amortize.js.map +0 -1
  30. package/dist/billing/amortize.test.d.ts +0 -2
  31. package/dist/billing/amortize.test.d.ts.map +0 -1
  32. package/dist/billing/amortize.test.js +0 -121
  33. package/dist/billing/amortize.test.js.map +0 -1
  34. package/dist/billing/format.test.d.ts +0 -2
  35. package/dist/billing/format.test.d.ts.map +0 -1
  36. package/dist/billing/format.test.js +0 -110
  37. package/dist/billing/format.test.js.map +0 -1
  38. package/dist/billing/invoice.d.ts.map +0 -1
  39. package/dist/billing/invoice.js.map +0 -1
  40. package/dist/billing/invoice.test.d.ts +0 -2
  41. package/dist/billing/invoice.test.d.ts.map +0 -1
  42. package/dist/billing/invoice.test.js +0 -200
  43. package/dist/billing/invoice.test.js.map +0 -1
  44. package/dist/billing/localize.d.ts.map +0 -1
  45. package/dist/billing/localize.js.map +0 -1
  46. package/dist/billing/localize.test.d.ts +0 -2
  47. package/dist/billing/localize.test.d.ts.map +0 -1
  48. package/dist/billing/localize.test.js +0 -144
  49. package/dist/billing/localize.test.js.map +0 -1
  50. package/dist/billing/sessionize.d.ts.map +0 -1
  51. package/dist/billing/sessionize.js.map +0 -1
  52. package/dist/billing/sessionize.test.d.ts +0 -2
  53. package/dist/billing/sessionize.test.d.ts.map +0 -1
  54. package/dist/billing/sessionize.test.js +0 -187
  55. package/dist/billing/sessionize.test.js.map +0 -1
  56. package/dist/billing.d.ts.map +0 -1
  57. package/dist/billing.js.map +0 -1
  58. package/dist/bin.d.ts.map +0 -1
  59. package/dist/bin.js.map +0 -1
  60. package/dist/cli-badge.test.d.ts +0 -2
  61. package/dist/cli-badge.test.d.ts.map +0 -1
  62. package/dist/cli-badge.test.js +0 -118
  63. package/dist/cli-badge.test.js.map +0 -1
  64. package/dist/cli-idioma.test.d.ts +0 -2
  65. package/dist/cli-idioma.test.d.ts.map +0 -1
  66. package/dist/cli-idioma.test.js +0 -190
  67. package/dist/cli-idioma.test.js.map +0 -1
  68. package/dist/cli-quota.test.d.ts +0 -2
  69. package/dist/cli-quota.test.d.ts.map +0 -1
  70. package/dist/cli-quota.test.js +0 -216
  71. package/dist/cli-quota.test.js.map +0 -1
  72. package/dist/cli.d.ts.map +0 -1
  73. package/dist/cli.js.map +0 -1
  74. package/dist/cloud/auth.d.ts.map +0 -1
  75. package/dist/cloud/auth.js.map +0 -1
  76. package/dist/cloud/client.d.ts.map +0 -1
  77. package/dist/cloud/client.js.map +0 -1
  78. package/dist/cloud/errors.test.d.ts +0 -2
  79. package/dist/cloud/errors.test.d.ts.map +0 -1
  80. package/dist/cloud/errors.test.js +0 -34
  81. package/dist/cloud/errors.test.js.map +0 -1
  82. package/dist/db/schema.d.ts.map +0 -1
  83. package/dist/db/schema.js.map +0 -1
  84. package/dist/db/schema.test.d.ts +0 -2
  85. package/dist/db/schema.test.d.ts.map +0 -1
  86. package/dist/db/schema.test.js +0 -825
  87. package/dist/db/schema.test.js.map +0 -1
  88. package/dist/db/store.d.ts.map +0 -1
  89. package/dist/db/store.js.map +0 -1
  90. package/dist/demo.d.ts.map +0 -1
  91. package/dist/demo.js.map +0 -1
  92. package/dist/demo.test.d.ts +0 -2
  93. package/dist/demo.test.d.ts.map +0 -1
  94. package/dist/demo.test.js +0 -148
  95. package/dist/demo.test.js.map +0 -1
  96. package/dist/doctor.d.ts.map +0 -1
  97. package/dist/doctor.js.map +0 -1
  98. package/dist/doctor.test.d.ts +0 -2
  99. package/dist/doctor.test.d.ts.map +0 -1
  100. package/dist/doctor.test.js +0 -167
  101. package/dist/doctor.test.js.map +0 -1
  102. package/dist/export/badge.d.ts.map +0 -1
  103. package/dist/export/badge.js.map +0 -1
  104. package/dist/export/badge.test.d.ts +0 -2
  105. package/dist/export/badge.test.d.ts.map +0 -1
  106. package/dist/export/badge.test.js +0 -32
  107. package/dist/export/badge.test.js.map +0 -1
  108. package/dist/export/clockify.d.ts.map +0 -1
  109. package/dist/export/clockify.js.map +0 -1
  110. package/dist/export/clockify.test.d.ts +0 -2
  111. package/dist/export/clockify.test.d.ts.map +0 -1
  112. package/dist/export/clockify.test.js +0 -51
  113. package/dist/export/clockify.test.js.map +0 -1
  114. package/dist/export/csv.d.ts.map +0 -1
  115. package/dist/export/csv.js.map +0 -1
  116. package/dist/export/dates.d.ts.map +0 -1
  117. package/dist/export/dates.js.map +0 -1
  118. package/dist/export/idioma.test.d.ts +0 -2
  119. package/dist/export/idioma.test.d.ts.map +0 -1
  120. package/dist/export/idioma.test.js +0 -255
  121. package/dist/export/idioma.test.js.map +0 -1
  122. package/dist/export/invoice-pdf.d.ts.map +0 -1
  123. package/dist/export/invoice-pdf.js.map +0 -1
  124. package/dist/export/panel.d.ts.map +0 -1
  125. package/dist/export/panel.js.map +0 -1
  126. package/dist/export/panel.test.d.ts +0 -2
  127. package/dist/export/panel.test.d.ts.map +0 -1
  128. package/dist/export/panel.test.js +0 -386
  129. package/dist/export/panel.test.js.map +0 -1
  130. package/dist/export/pdf.d.ts.map +0 -1
  131. package/dist/export/pdf.js.map +0 -1
  132. package/dist/export/pdf.test.d.ts +0 -2
  133. package/dist/export/pdf.test.d.ts.map +0 -1
  134. package/dist/export/pdf.test.js +0 -172
  135. package/dist/export/pdf.test.js.map +0 -1
  136. package/dist/export/pixelfont.d.ts.map +0 -1
  137. package/dist/export/pixelfont.js.map +0 -1
  138. package/dist/export/png.d.ts.map +0 -1
  139. package/dist/export/png.js.map +0 -1
  140. package/dist/export/receipt.d.ts.map +0 -1
  141. package/dist/export/receipt.js.map +0 -1
  142. package/dist/export/receipt.test.d.ts +0 -2
  143. package/dist/export/receipt.test.d.ts.map +0 -1
  144. package/dist/export/receipt.test.js +0 -207
  145. package/dist/export/receipt.test.js.map +0 -1
  146. package/dist/export/share.d.ts.map +0 -1
  147. package/dist/export/share.js.map +0 -1
  148. package/dist/export/share.test.d.ts +0 -2
  149. package/dist/export/share.test.d.ts.map +0 -1
  150. package/dist/export/share.test.js +0 -116
  151. package/dist/export/share.test.js.map +0 -1
  152. package/dist/i18n/en.d.ts.map +0 -1
  153. package/dist/i18n/en.js.map +0 -1
  154. package/dist/i18n/i18n.test.d.ts +0 -2
  155. package/dist/i18n/i18n.test.d.ts.map +0 -1
  156. package/dist/i18n/i18n.test.js +0 -226
  157. package/dist/i18n/i18n.test.js.map +0 -1
  158. package/dist/i18n/index.d.ts.map +0 -1
  159. package/dist/i18n/index.js.map +0 -1
  160. package/dist/i18n/labels.d.ts.map +0 -1
  161. package/dist/i18n/labels.js.map +0 -1
  162. package/dist/metrics/index.d.ts.map +0 -1
  163. package/dist/metrics/index.js.map +0 -1
  164. package/dist/metrics/metrics.test.d.ts +0 -2
  165. package/dist/metrics/metrics.test.d.ts.map +0 -1
  166. package/dist/metrics/metrics.test.js +0 -162
  167. package/dist/metrics/metrics.test.js.map +0 -1
  168. package/dist/metrics/people.d.ts.map +0 -1
  169. package/dist/metrics/people.js.map +0 -1
  170. package/dist/metrics/people.test.d.ts +0 -2
  171. package/dist/metrics/people.test.d.ts.map +0 -1
  172. package/dist/metrics/people.test.js +0 -92
  173. package/dist/metrics/people.test.js.map +0 -1
  174. package/dist/metrics/quota.d.ts.map +0 -1
  175. package/dist/metrics/quota.js.map +0 -1
  176. package/dist/metrics/quota.test.d.ts +0 -2
  177. package/dist/metrics/quota.test.d.ts.map +0 -1
  178. package/dist/metrics/quota.test.js +0 -262
  179. package/dist/metrics/quota.test.js.map +0 -1
  180. package/dist/metrics/team.d.ts.map +0 -1
  181. package/dist/metrics/team.js.map +0 -1
  182. package/dist/metrics/team.test.d.ts +0 -2
  183. package/dist/metrics/team.test.d.ts.map +0 -1
  184. package/dist/metrics/team.test.js +0 -90
  185. package/dist/metrics/team.test.js.map +0 -1
  186. package/dist/node-version.d.ts.map +0 -1
  187. package/dist/node-version.js.map +0 -1
  188. package/dist/node-version.test.d.ts +0 -2
  189. package/dist/node-version.test.d.ts.map +0 -1
  190. package/dist/node-version.test.js +0 -53
  191. package/dist/node-version.test.js.map +0 -1
  192. package/dist/paths.d.ts.map +0 -1
  193. package/dist/paths.js.map +0 -1
  194. package/dist/paths.test.d.ts +0 -2
  195. package/dist/paths.test.d.ts.map +0 -1
  196. package/dist/paths.test.js +0 -32
  197. package/dist/paths.test.js.map +0 -1
  198. package/dist/pricing/catalog.d.ts.map +0 -1
  199. package/dist/pricing/catalog.js.map +0 -1
  200. package/dist/pricing/cost.d.ts.map +0 -1
  201. package/dist/pricing/cost.js.map +0 -1
  202. package/dist/pricing/cost.test.d.ts +0 -2
  203. package/dist/pricing/cost.test.d.ts.map +0 -1
  204. package/dist/pricing/cost.test.js +0 -100
  205. package/dist/pricing/cost.test.js.map +0 -1
  206. package/dist/publish.d.ts.map +0 -1
  207. package/dist/publish.js.map +0 -1
  208. package/dist/publish.test.d.ts +0 -2
  209. package/dist/publish.test.d.ts.map +0 -1
  210. package/dist/publish.test.js +0 -113
  211. package/dist/publish.test.js.map +0 -1
  212. package/dist/quota.d.ts.map +0 -1
  213. package/dist/quota.js.map +0 -1
  214. package/dist/server.d.ts.map +0 -1
  215. package/dist/server.js.map +0 -1
  216. package/dist/server.test.d.ts +0 -2
  217. package/dist/server.test.d.ts.map +0 -1
  218. package/dist/server.test.js +0 -402
  219. package/dist/server.test.js.map +0 -1
  220. package/dist/setup.d.ts.map +0 -1
  221. package/dist/setup.js.map +0 -1
  222. package/dist/setup.test.d.ts +0 -2
  223. package/dist/setup.test.d.ts.map +0 -1
  224. package/dist/setup.test.js +0 -144
  225. package/dist/setup.test.js.map +0 -1
  226. package/dist/sync/team.d.ts.map +0 -1
  227. package/dist/sync/team.js.map +0 -1
  228. package/dist/sync/team.test.d.ts +0 -2
  229. package/dist/sync/team.test.d.ts.map +0 -1
  230. package/dist/sync/team.test.js +0 -102
  231. package/dist/sync/team.test.js.map +0 -1
  232. package/dist/sync.d.ts.map +0 -1
  233. package/dist/sync.js.map +0 -1
  234. package/dist/sync.test.d.ts +0 -2
  235. package/dist/sync.test.d.ts.map +0 -1
  236. package/dist/sync.test.js +0 -151
  237. package/dist/sync.test.js.map +0 -1
  238. package/dist/team.d.ts.map +0 -1
  239. package/dist/team.js.map +0 -1
  240. package/dist/team.test.d.ts +0 -2
  241. package/dist/team.test.d.ts.map +0 -1
  242. package/dist/team.test.js +0 -163
  243. package/dist/team.test.js.map +0 -1
  244. package/dist/updateCheck.d.ts.map +0 -1
  245. package/dist/updateCheck.js.map +0 -1
  246. package/dist/updateCheck.test.d.ts +0 -2
  247. package/dist/updateCheck.test.d.ts.map +0 -1
  248. package/dist/updateCheck.test.js +0 -27
  249. package/dist/updateCheck.test.js.map +0 -1
  250. package/dist/watchers/agents.d.ts.map +0 -1
  251. package/dist/watchers/agents.js.map +0 -1
  252. package/dist/watchers/agents.test.d.ts +0 -2
  253. package/dist/watchers/agents.test.d.ts.map +0 -1
  254. package/dist/watchers/agents.test.js +0 -96
  255. package/dist/watchers/agents.test.js.map +0 -1
  256. package/dist/watchers/claude.d.ts.map +0 -1
  257. package/dist/watchers/claude.js.map +0 -1
  258. package/dist/watchers/claude.test.d.ts +0 -2
  259. package/dist/watchers/claude.test.d.ts.map +0 -1
  260. package/dist/watchers/claude.test.js +0 -180
  261. package/dist/watchers/claude.test.js.map +0 -1
  262. package/dist/watchers/codex.d.ts.map +0 -1
  263. package/dist/watchers/codex.js.map +0 -1
  264. package/dist/watchers/codex.test.d.ts +0 -2
  265. package/dist/watchers/codex.test.d.ts.map +0 -1
  266. package/dist/watchers/codex.test.js +0 -207
  267. package/dist/watchers/codex.test.js.map +0 -1
  268. package/dist/watchers/copilot.d.ts.map +0 -1
  269. package/dist/watchers/copilot.js.map +0 -1
  270. package/dist/watchers/copilot.test.d.ts +0 -2
  271. package/dist/watchers/copilot.test.d.ts.map +0 -1
  272. package/dist/watchers/copilot.test.js +0 -98
  273. package/dist/watchers/copilot.test.js.map +0 -1
  274. package/dist/watchers/git.d.ts.map +0 -1
  275. package/dist/watchers/git.js.map +0 -1
  276. package/dist/watchers/git.test.d.ts +0 -2
  277. package/dist/watchers/git.test.d.ts.map +0 -1
  278. package/dist/watchers/git.test.js +0 -75
  279. package/dist/watchers/git.test.js.map +0 -1
  280. package/dist/watchers/identity.d.ts.map +0 -1
  281. package/dist/watchers/identity.js.map +0 -1
  282. package/dist/watchers/identity.test.d.ts +0 -2
  283. package/dist/watchers/identity.test.d.ts.map +0 -1
  284. package/dist/watchers/identity.test.js +0 -48
  285. package/dist/watchers/identity.test.js.map +0 -1
  286. package/dist/web-ui.test.d.ts +0 -2
  287. package/dist/web-ui.test.d.ts.map +0 -1
  288. package/dist/web-ui.test.js +0 -414
  289. package/dist/web-ui.test.js.map +0 -1
  290. package/dist/workitems.test.d.ts +0 -2
  291. package/dist/workitems.test.d.ts.map +0 -1
  292. package/dist/workitems.test.js +0 -59
  293. package/dist/workitems.test.js.map +0 -1
@@ -9,8 +9,10 @@ exports.weekStart = weekStart;
9
9
  exports.sameStretch = sameStretch;
10
10
  exports.compareWithHistory = compareWithHistory;
11
11
  exports.buildQuota = buildQuota;
12
+ exports.officialWindow = officialWindow;
12
13
  exports.quotaRange = quotaRange;
13
14
  exports.quotaToJson = quotaToJson;
15
+ const cost_js_1 = require("../pricing/cost.js");
14
16
  /**
15
17
  * "¿Qué proyecto se come mi cuota?"
16
18
  *
@@ -57,29 +59,44 @@ function add(u, t) {
57
59
  };
58
60
  }
59
61
  function bucketKey(b) {
60
- return b.kind === "project" ? `p:${b.projectId}` : b.kind === "folder" ? `f:${b.folder}` : "-";
62
+ return b.kind === "project" ? `p:${b.projectId}` : b.kind === "folder" ? `f:${b.path}` : "-";
63
+ }
64
+ /** Precio de lista de un turno, en micro-USD, o null si su modelo no tiene precio. */
65
+ function listCostOf(t) {
66
+ if (!t.model)
67
+ return null;
68
+ return (0, cost_js_1.costOfTurn)(t.tokens, t.model, t.at)?.microUsd ?? null;
61
69
  }
62
70
  /**
63
71
  * Reparto por proyecto de los turnos con `from <= at < to`, de mayor a menor.
64
72
  * Un proyecto sin actividad en el periodo no sale: una fila a cero en una
65
73
  * tabla de "quién se come la cuota" es ruido.
66
74
  */
67
- function breakdown(turns, from, to, bucketOf) {
75
+ function breakdown(turns, from, to, bucketOf, options = {}) {
68
76
  const acc = new Map();
69
77
  let total = ZERO;
78
+ let listCost = 0;
79
+ let unpriced = 0;
70
80
  for (const t of turns) {
71
81
  if (t.at < from || t.at >= to)
72
82
  continue;
73
83
  const bucket = bucketOf(t.repoPath);
74
84
  const key = bucketKey(bucket);
75
- const prev = acc.get(key) ?? { bucket, usage: ZERO };
76
- acc.set(key, { bucket, usage: add(prev.usage, t.tokens) });
85
+ const cost = listCostOf(t);
86
+ if (cost === null)
87
+ unpriced++;
88
+ const prev = acc.get(key) ?? { bucket, usage: ZERO, listCost: 0 };
89
+ acc.set(key, { bucket, usage: add(prev.usage, t.tokens), listCost: prev.listCost + (cost ?? 0) });
77
90
  total = add(total, t.tokens);
91
+ listCost += cost ?? 0;
78
92
  }
93
+ const weighted = options.weighted === true;
94
+ const base = (r) => (weighted ? r.listCost : r.usage.total);
95
+ const whole = weighted ? listCost : total.total;
79
96
  const rows = [...acc.values()]
80
- .map(({ bucket, usage }) => ({ bucket, usage, share: total.total > 0 ? usage.total / total.total : 0 }))
81
- .sort((a, b) => b.usage.total - a.usage.total);
82
- return { rows, total };
97
+ .map((r) => ({ ...r, share: whole > 0 ? base(r) / whole : 0 }))
98
+ .sort((a, b) => base(b) - base(a));
99
+ return { rows, total, listCost, unpriced, weighted };
83
100
  }
84
101
  // ---------------------------------------------------------------------------
85
102
  // La ventana de 5 horas
@@ -176,20 +193,26 @@ function compareWithHistory(times, firstTurnAt, now) {
176
193
  */
177
194
  function buildQuota(input) {
178
195
  const { now, bucketOf } = input;
196
+ const opts = { weighted: input.weighted === true };
179
197
  const monday = weekStart(now);
180
198
  const until = new Date(now.getTime() + 1);
181
199
  const turns = input.turns.filter((t) => t.at <= now);
182
200
  const agents = [];
183
201
  for (const agent of exports.QUOTA_AGENTS) {
184
202
  const own = turns.filter((t) => t.agent === agent);
185
- const { current, lastEnded } = windowAt(own.map((t) => t.at), now);
186
- const week = breakdown(own, monday, until, bucketOf);
203
+ const official = agent === "claude-code" && input.claudeWindow &&
204
+ input.claudeWindow.start <= now && now < input.claudeWindow.end ? input.claudeWindow : null;
205
+ const estimate = windowAt(own.map((t) => t.at), now);
206
+ const current = official ?? estimate.current;
207
+ const week = breakdown(own, monday, until, bucketOf, opts);
187
208
  if (!current && week.rows.length === 0)
188
209
  continue;
189
210
  agents.push({
190
211
  agent,
191
- window: current ? { ...current, ...breakdown(own, current.start, until, bucketOf) } : null,
192
- lastWindowEnded: lastEnded,
212
+ window: current
213
+ ? { ...current, ...breakdown(own, current.start, until, bucketOf, opts), estimated: !official }
214
+ : null,
215
+ lastWindowEnded: current ? null : estimate.lastEnded,
193
216
  week,
194
217
  comparison: compareWithHistory(own.map((t) => ({ at: t.at, total: add(ZERO, t.tokens).total })), input.firstTurnAt.get(agent) ?? null, now),
195
218
  });
@@ -201,6 +224,25 @@ function buildQuota(input) {
201
224
  copilotExcluded: turns.some((t) => t.agent === "copilot" && t.at >= monday),
202
225
  };
203
226
  }
227
+ /**
228
+ * La ventana de 5 h que dio Claude Code, a partir del último `resets_at` que
229
+ * vio la status line: empieza 5 h antes de su reinicio.
230
+ *
231
+ * Solo vale si el dato tiene menos de 5 h (uno más viejo puede ser de otra
232
+ * ventana) y si el reinicio aún no ha llegado (pasado ese momento, la ventana
233
+ * en curso es otra que nadie ha visto). Si no, null: se estima como siempre.
234
+ */
235
+ function officialWindow(seen, now) {
236
+ if (!seen)
237
+ return null;
238
+ const age = now.getTime() - seen.seenAt.getTime();
239
+ if (age < 0 || age >= WINDOW_MS)
240
+ return null;
241
+ if (seen.resetsAt <= now)
242
+ return null;
243
+ const start = new Date(seen.resetsAt.getTime() - WINDOW_MS);
244
+ return start <= now ? { start, end: seen.resetsAt } : null;
245
+ }
204
246
  /** Desde cuándo hay que leer turnos para montar el informe de `now`. */
205
247
  function quotaRange(now) {
206
248
  return { start: sameStretch(now, exports.HISTORY_WEEKS).start, end: new Date(now.getTime() + 1) };
@@ -219,19 +261,23 @@ function quotaToJson(report) {
219
261
  projectName: r.bucket.kind === "project" ? r.bucket.projectName : null,
220
262
  folder: r.bucket.kind === "folder" ? r.bucket.folder : null,
221
263
  ...r.usage,
264
+ listCostMicroUsd: r.listCost,
222
265
  share: Math.round(r.share * 10_000) / 10_000,
223
266
  }));
267
+ const weighted = report.agents.some((a) => a.week.weighted);
224
268
  return {
225
269
  generatedAt: report.generatedAt.toISOString(),
226
270
  weekStart: report.weekStart.toISOString(),
227
- // Para que ningún script lo trate como un dato exacto sin haberlo leído.
228
- windowIsEstimate: true,
271
+ // Para que ningún script lo trate como un dato exacto sin haberlo leído:
272
+ // false solo si todas las ventanas que salen las dio Claude Code.
273
+ windowIsEstimate: report.agents.some((a) => !a.window || a.window.estimated),
229
274
  windowHours: exports.WINDOW_HOURS,
275
+ shareBasis: weighted ? "list-price" : "tokens",
230
276
  agents: report.agents.map((a) => ({
231
277
  agent: a.agent,
232
278
  window: a.window
233
279
  ? { start: a.window.start.toISOString(), end: a.window.end.toISOString(),
234
- projects: rows(a.window), total: a.window.total }
280
+ estimated: a.window.estimated, projects: rows(a.window), total: a.window.total }
235
281
  : null,
236
282
  lastWindowEnded: a.lastWindowEnded?.toISOString() ?? null,
237
283
  week: { projects: rows(a.week), total: a.week.total },
@@ -0,0 +1,52 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ /**
3
+ * A qué proyecto pertenece una carpeta de trabajo: lo comparten `estela quota`
4
+ * y `estela statusline`.
5
+ *
6
+ * El `cwd` de una sesión no siempre está dentro de la carpeta registrada:
7
+ * un worktree vive al lado del repositorio (`../web-feature`), y una sesión
8
+ * abierta en `repo/src` de un repo sin registrar se llamaba "src". Por eso,
9
+ * en este orden:
10
+ *
11
+ * 1. La carpeta tal cual contra los proyectos registrados (el prefijo más
12
+ * largo gana). Cubre `repo/src` y también un subproyecto registrado
13
+ * dentro de un monorepo (`repo/packages/x`), que resolviendo antes a la
14
+ * raíz git se perdería.
15
+ * 2. La raíz git — y si es un worktree, la del repositorio principal —
16
+ * contra los proyectos registrados.
17
+ * 3. Si nada coincide, "sin proyecto" con el nombre de la raíz git, nunca
18
+ * el de una subcarpeta. Fuera de git, la propia carpeta.
19
+ */
20
+ export type Owner = {
21
+ readonly kind: "project";
22
+ readonly projectId: string;
23
+ }
24
+ /**
25
+ * `path` agrupa (dos repos pueden llamarse igual); `name` es lo que se
26
+ * enseña; `inGit` dice si es la raíz de un repo o una carpeta suelta.
27
+ */
28
+ | {
29
+ readonly kind: "folder";
30
+ readonly path: string;
31
+ readonly name: string;
32
+ readonly inGit: boolean;
33
+ } | {
34
+ readonly kind: "none";
35
+ };
36
+ /** Un resolvedor con caché: una semana tiene miles de turnos y pocas carpetas. */
37
+ export declare function ownerResolver(db: DatabaseSync): (dir: string | null) => Owner;
38
+ /** El nombre de una carpeta para enseñarlo: la última parte de la ruta. */
39
+ export declare function folderName(path: string): string;
40
+ /**
41
+ * La raíz del repositorio git que contiene `dir`, o null si no está en git.
42
+ *
43
+ * Se sube hasta encontrar `.git`. Si es una carpeta, esa es la raíz. Si es un
44
+ * fichero (`gitdir: …`), es un worktree o un submódulo: un worktree tiene un
45
+ * `commondir` que apunta al `.git` del repositorio principal, y la raíz es la
46
+ * de ese; un submódulo no lo tiene, y es su propio repositorio.
47
+ *
48
+ * Solo lee el disco, sin ejecutar git: se llama en cada evento de la status
49
+ * line. Si la carpeta ya no existe (un worktree borrado), sube igualmente.
50
+ */
51
+ export declare function gitRoot(dir: string): string | null;
52
+ //# sourceMappingURL=project-of.d.ts.map
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ownerResolver = ownerResolver;
37
+ exports.folderName = folderName;
38
+ exports.gitRoot = gitRoot;
39
+ const node_fs_1 = require("node:fs");
40
+ const node_path_1 = require("node:path");
41
+ const store = __importStar(require("./db/store.js"));
42
+ const paths_js_1 = require("./paths.js");
43
+ /** Un resolvedor con caché: una semana tiene miles de turnos y pocas carpetas. */
44
+ function ownerResolver(db) {
45
+ const cache = new Map();
46
+ return (dir) => {
47
+ if (!dir)
48
+ return { kind: "none" };
49
+ let owner = cache.get(dir);
50
+ if (!owner) {
51
+ owner = resolveOwner(db, dir);
52
+ cache.set(dir, owner);
53
+ }
54
+ return owner;
55
+ };
56
+ }
57
+ function resolveOwner(db, dir) {
58
+ const direct = store.projectForRepo(db, dir);
59
+ if (direct)
60
+ return { kind: "project", projectId: direct };
61
+ const root = gitRoot(dir);
62
+ if (root && root !== dir) {
63
+ const viaRoot = store.projectForRepo(db, root);
64
+ if (viaRoot)
65
+ return { kind: "project", projectId: viaRoot };
66
+ }
67
+ const path = root ?? (0, paths_js_1.normalizePath)(dir);
68
+ return { kind: "folder", path, name: folderName(path), inGit: root !== null };
69
+ }
70
+ /** El nombre de una carpeta para enseñarlo: la última parte de la ruta. */
71
+ function folderName(path) {
72
+ return (0, node_path_1.basename)(path.replace(/[\\/]+$/, "")) || path;
73
+ }
74
+ /**
75
+ * La raíz del repositorio git que contiene `dir`, o null si no está en git.
76
+ *
77
+ * Se sube hasta encontrar `.git`. Si es una carpeta, esa es la raíz. Si es un
78
+ * fichero (`gitdir: …`), es un worktree o un submódulo: un worktree tiene un
79
+ * `commondir` que apunta al `.git` del repositorio principal, y la raíz es la
80
+ * de ese; un submódulo no lo tiene, y es su propio repositorio.
81
+ *
82
+ * Solo lee el disco, sin ejecutar git: se llama en cada evento de la status
83
+ * line. Si la carpeta ya no existe (un worktree borrado), sube igualmente.
84
+ */
85
+ function gitRoot(dir) {
86
+ // Una ruta relativa (o "~/…", como en la demo) se resolvería contra la
87
+ // carpeta desde la que se ejecuta Estela, y acabaría en el repo de quien la
88
+ // ejecuta. Los cwd de verdad son absolutos.
89
+ if (!(0, node_path_1.isAbsolute)(dir))
90
+ return null;
91
+ let d = (0, node_path_1.resolve)(dir);
92
+ for (;;) {
93
+ const dotGit = (0, node_path_1.join)(d, ".git");
94
+ try {
95
+ const st = (0, node_fs_1.statSync)(dotGit);
96
+ if (st.isDirectory())
97
+ return (0, paths_js_1.normalizePath)(d);
98
+ if (st.isFile())
99
+ return (0, paths_js_1.normalizePath)(worktreeMain(dotGit) ?? d);
100
+ }
101
+ catch { /* no hay .git aquí: se sube */ }
102
+ const parent = (0, node_path_1.dirname)(d);
103
+ if (parent === d)
104
+ return null;
105
+ d = parent;
106
+ }
107
+ }
108
+ /** La raíz del repositorio principal de un worktree, o null si no lo es. */
109
+ function worktreeMain(dotGitFile) {
110
+ try {
111
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec((0, node_fs_1.readFileSync)(dotGitFile, "utf8"));
112
+ if (!m)
113
+ return null;
114
+ const gitdir = (0, node_path_1.isAbsolute)(m[1]) ? m[1] : (0, node_path_1.resolve)((0, node_path_1.dirname)(dotGitFile), m[1]);
115
+ const common = (0, node_fs_1.readFileSync)((0, node_path_1.join)(gitdir, "commondir"), "utf8").trim();
116
+ const commonDir = (0, node_path_1.isAbsolute)(common) ? common : (0, node_path_1.resolve)(gitdir, common);
117
+ // El .git del principal: su carpeta padre es la raíz del repositorio.
118
+ return (0, node_path_1.basename)(commonDir) === ".git" ? (0, node_path_1.dirname)(commonDir) : null;
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ //# sourceMappingURL=project-of.js.map
package/dist/quota.d.ts CHANGED
@@ -4,7 +4,16 @@ import { type QuotaReport } from "./metrics/quota.js";
4
4
  * La cuota sobre la base: lo que comparten `estela quota` y el panel web.
5
5
  * El cálculo vive en `metrics/quota.ts`; aquí solo se leen los datos.
6
6
  */
7
- export declare function loadQuotaReport(db: DatabaseSync, now?: Date): QuotaReport;
7
+ export interface QuotaLoadOptions {
8
+ /** % por precio de lista en vez de por tokens (`--weighted`). */
9
+ readonly weighted?: boolean;
10
+ /**
11
+ * La base de la que se lee, para encontrar al lado lo que guardó la status
12
+ * line (el `resets_at` de Claude Code). Sin ella, la ventana se estima.
13
+ */
14
+ readonly dbPath?: string;
15
+ }
16
+ export declare function loadQuotaReport(db: DatabaseSync, now?: Date, options?: QuotaLoadOptions): QuotaReport;
8
17
  /**
9
18
  * Trae los turnos de esta semana antes de calcular.
10
19
  *
package/dist/quota.js CHANGED
@@ -35,38 +35,32 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadQuotaReport = loadQuotaReport;
37
37
  exports.refreshQuotaTurns = refreshQuotaTurns;
38
- const node_path_1 = require("node:path");
39
38
  const store = __importStar(require("./db/store.js"));
40
39
  const quota_js_1 = require("./metrics/quota.js");
40
+ const project_of_js_1 = require("./project-of.js");
41
+ const statusline_cache_js_1 = require("./statusline-cache.js");
41
42
  const agents_js_1 = require("./watchers/agents.js");
42
- /**
43
- * La cuota sobre la base: lo que comparten `estela quota` y el panel web.
44
- * El cálculo vive en `metrics/quota.ts`; aquí solo se leen los datos.
45
- */
46
- function loadQuotaReport(db, now = new Date()) {
43
+ function loadQuotaReport(db, now = new Date(), options = {}) {
47
44
  const range = (0, quota_js_1.quotaRange)(now);
48
45
  const names = new Map(store.listProjects(db).map((p) => [p.id, p.name]));
49
- // Un proyecto por carpeta, una sola vez: `projectForRepo` lee todos los
50
- // repos vinculados en cada llamada, y una semana tiene miles de turnos.
51
- const cache = new Map();
46
+ // La carpeta, y si no, su raíz git: ver project-of.ts. Una carpeta sin
47
+ // proyecto sale con el nombre de su raíz git, no el de una subcarpeta.
48
+ const ownerOf = (0, project_of_js_1.ownerResolver)(db);
52
49
  const bucketOf = (repoPath) => {
53
- if (!repoPath)
54
- return { kind: "none" };
55
- let bucket = cache.get(repoPath);
56
- if (!bucket) {
57
- const projectId = store.projectForRepo(db, repoPath);
58
- bucket = projectId
59
- ? { kind: "project", projectId, projectName: names.get(projectId) ?? projectId }
60
- : { kind: "folder", folder: (0, node_path_1.basename)(repoPath.replace(/[\\/]+$/, "")) || repoPath };
61
- cache.set(repoPath, bucket);
62
- }
63
- return bucket;
50
+ const o = ownerOf(repoPath);
51
+ if (o.kind === "project")
52
+ return { kind: "project", projectId: o.projectId, projectName: names.get(o.projectId) ?? o.projectId };
53
+ if (o.kind === "folder")
54
+ return { kind: "folder", folder: o.name, path: o.path };
55
+ return { kind: "none" };
64
56
  };
65
57
  return (0, quota_js_1.buildQuota)({
66
58
  turns: store.quotaTurns(db, range.start, range.end),
67
59
  firstTurnAt: store.firstTurnByAgent(db),
68
60
  now,
69
61
  bucketOf,
62
+ weighted: options.weighted === true,
63
+ claudeWindow: options.dbPath ? (0, quota_js_1.officialWindow)((0, statusline_cache_js_1.readFiveHourReset)(options.dbPath), now) : null,
70
64
  });
71
65
  }
72
66
  /**
package/dist/server.js CHANGED
@@ -60,6 +60,7 @@ const identity_js_1 = require("./watchers/identity.js");
60
60
  const index_js_2 = require("./i18n/index.js");
61
61
  const paths_js_1 = require("./paths.js");
62
62
  const localize_js_1 = require("./billing/localize.js");
63
+ const assign_js_1 = require("./billing/assign.js");
63
64
  const quota_js_1 = require("./metrics/quota.js");
64
65
  const quota_js_2 = require("./quota.js");
65
66
  const labels_js_1 = require("./i18n/labels.js");
@@ -141,32 +142,14 @@ async function importOnce(dbPath) {
141
142
  const commits = (0, identity_js_1.onlyMine)(allRepoCommits, mine);
142
143
  const fromAgents = (0, sessionize_js_1.attachCommits)((0, sessionize_js_1.sessionize)(turns), commits);
143
144
  const fromCommits = (0, sessionize_js_1.withoutOverlap)((0, sessionize_js_1.sessionizeCommits)(commits), fromAgents);
144
- // Se agrupa por PROYECTO, no por ruta: el agente abierto en `repo/` y en
145
- // `repo/src/UI` son dos rutas del mismo proyecto, y agrupar por ruta
146
- // producía dos bloques que luego se pisaban al guardarse con el mismo id.
147
- const porProyecto = (b) => (b.repoPath ? store.projectForRepo(db, b.repoPath) ?? b.repoPath : "");
148
- for (const block of (0, sessionize_js_1.groupByBranchAndDay)([...fromAgents, ...fromCommits], porProyecto)) {
149
- const projectId = block.repoPath ? store.projectForRepo(db, block.repoPath) : null;
150
- if (!projectId)
151
- continue;
152
- const project = store.getProject(db, projectId);
153
- const day = (0, shared_1.localDate)(block.startedAt);
154
- const slug = (block.branch ?? "sin-rama").replace(/[^a-zA-Z0-9]+/g, "-");
155
- store.saveTimeEntry(db, {
156
- id: `te_${projectId}_${day}_${slug}`,
157
- projectId,
158
- startedAt: block.startedAt, endedAt: block.endedAt, seconds: block.seconds,
159
- description: (0, sessionize_js_1.describeBlock)(block),
160
- billable: project.billable, invoiceId: null,
161
- aiCost: block.aiCost,
162
- // Sin agente no hay segundos de agente que atribuir.
163
- agentSeconds: block.turnCount > 0 ? block.seconds : 0,
164
- commitHashes: block.commits.map((c) => c.hash),
165
- agents: block.turnCount > 0 ? [...(block.agents ?? ["claude-code"])] : [],
166
- source: block.turnCount > 0 ? "agent" : "commit", kind: "development",
167
- branch: block.branch,
168
- workItems: (0, shared_1.extractWorkItems)(block.branch, block.commits.map((c) => c.subject), project.tracker),
169
- });
145
+ // Las mismas reglas que `estela import` (billing/assign.ts). Las horas de
146
+ // subcarpetas y worktrees solo se recuperan si ya se confirmó una vez
147
+ // desde la terminal: el panel no cambia horas importadas en segundo plano
148
+ // sin que nadie haya visto antes qué iba a cambiar.
149
+ const recover = (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.dirname)(dbPath), ".recover-git-root"));
150
+ const plan = (0, assign_js_1.planEntries)(db, [...fromAgents, ...fromCommits], { recover });
151
+ for (const { entry } of plan.entries) {
152
+ store.saveTimeEntry(db, entry);
170
153
  imputed++;
171
154
  }
172
155
  return imputed;
@@ -325,7 +308,7 @@ async function api(req, res, url, dbPath) {
325
308
  // el panel ya importa solo cada pocos minutos, y una petición web no debe
326
309
  // ponerse a leer el disco entero.
327
310
  if (path === "/api/quota" && req.method === "GET") {
328
- return json(res, 200, (0, quota_js_1.quotaToJson)((0, quota_js_2.loadQuotaReport)(db)));
311
+ return json(res, 200, (0, quota_js_1.quotaToJson)((0, quota_js_2.loadQuotaReport)(db, new Date(), { dbPath })));
329
312
  }
330
313
  if (path === "/api/day" && req.method === "GET") {
331
314
  const date = url.searchParams.get("date") ?? todayIso();
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `statusline.json`, junto a la base: lo que la status line recuerda entre
3
+ * llamadas.
4
+ *
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.
9
+ *
10
+ * Es una caché: si se pierde o se corrompe, todo sigue funcionando, solo que
11
+ * relee más o vuelve a estimar.
12
+ */
13
+ export interface StatuslineCache {
14
+ files: Record<string, {
15
+ offset: number;
16
+ seenAt: string;
17
+ }>;
18
+ fiveHour?: {
19
+ resetsAt: string;
20
+ seenAt: string;
21
+ };
22
+ }
23
+ export declare function cachePath(dbPath: string): string;
24
+ export declare function loadCache(dbPath: string): StatuslineCache;
25
+ export declare function saveCache(dbPath: string, cache: StatuslineCache): void;
26
+ /**
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.
29
+ */
30
+ export declare function recordFiveHourReset(dbPath: string, resetsAt: Date, now: Date): void;
31
+ export declare function readFiveHourReset(dbPath: string): {
32
+ resetsAt: Date;
33
+ seenAt: Date;
34
+ } | null;
35
+ //# sourceMappingURL=statusline-cache.d.ts.map
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cachePath = cachePath;
4
+ exports.loadCache = loadCache;
5
+ exports.saveCache = saveCache;
6
+ exports.recordFiveHourReset = recordFiveHourReset;
7
+ exports.readFiveHourReset = readFiveHourReset;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
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;
13
+ function cachePath(dbPath) {
14
+ return (0, node_path_1.join)((0, node_path_1.dirname)(dbPath), "statusline.json");
15
+ }
16
+ function loadCache(dbPath) {
17
+ try {
18
+ const c = JSON.parse((0, node_fs_1.readFileSync)(cachePath(dbPath), "utf8"));
19
+ return c && typeof c.files === "object" && c.files ? c : { files: {} };
20
+ }
21
+ catch {
22
+ return { files: {} };
23
+ }
24
+ }
25
+ function saveCache(dbPath, cache) {
26
+ // Solo los transcripts vistos más recientemente: uno por sesión, y las
27
+ // sesiones viejas ya no escriben.
28
+ const files = Object.fromEntries(Object.entries(cache.files)
29
+ .sort((a, b) => b[1].seenAt.localeCompare(a[1].seenAt)).slice(0, MAX_FILES));
30
+ try {
31
+ (0, node_fs_1.writeFileSync)(cachePath(dbPath), JSON.stringify({ ...cache, files }));
32
+ }
33
+ catch { /* sin caché, la próxima vez se relee: más lento, no incorrecto */ }
34
+ }
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.
38
+ */
39
+ function recordFiveHourReset(dbPath, resetsAt, now) {
40
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.dirname)(dbPath)))
41
+ return;
42
+ 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);
49
+ }
50
+ function readFiveHourReset(dbPath) {
51
+ const f = loadCache(dbPath).fiveHour;
52
+ if (!f)
53
+ return null;
54
+ const resetsAt = new Date(f.resetsAt), seenAt = new Date(f.seenAt);
55
+ return Number.isNaN(resetsAt.getTime()) || Number.isNaN(seenAt.getTime()) ? null : { resetsAt, seenAt };
56
+ }
57
+ //# sourceMappingURL=statusline-cache.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `estela statusline install | uninstall`: el bloque `statusLine` de
3
+ * `~/.claude/settings.json`.
4
+ *
5
+ * Ese fichero es de quien usa Claude Code, con sus permisos, sus hooks y sus
6
+ * plugins. Se toca lo justo: se lee, se cambia una clave y se reescribe el
7
+ * resto tal cual; antes, una copia. Si ya hay otra status line, no se pisa.
8
+ *
9
+ * Funciones puras sobre el texto del fichero: quien llama lee y escribe.
10
+ */
11
+ export declare const OUR_COMMAND = "estela statusline";
12
+ /** Dónde vive la configuración de Claude Code (`CLAUDE_CONFIG_DIR` la mueve). */
13
+ export declare function claudeSettingsPath(env?: NodeJS.ProcessEnv): string;
14
+ export type InstallResult = {
15
+ readonly status: "installed" | "replaced" | "chained";
16
+ readonly text: string;
17
+ readonly previous: string | null;
18
+ } | {
19
+ readonly status: "already";
20
+ } | {
21
+ readonly status: "conflict";
22
+ readonly existing: string;
23
+ } | {
24
+ readonly status: "invalid";
25
+ };
26
+ export type UninstallResult = {
27
+ readonly status: "removed" | "restored";
28
+ readonly text: string;
29
+ readonly restored: string | null;
30
+ } | {
31
+ readonly status: "absent";
32
+ } | {
33
+ readonly status: "foreign";
34
+ readonly existing: string;
35
+ } | {
36
+ readonly status: "invalid";
37
+ };
38
+ export declare function isOurs(command: string | null): boolean;
39
+ /** Comillas simples de shell: lo que va dentro se pasa tal cual. */
40
+ export declare function shellQuote(text: string): string;
41
+ /** El comando encadenado dentro de `--append '…'`, si lo hay. */
42
+ export declare function appendedCommand(command: string): string | null;
43
+ export declare function planInstall(text: string | null, options?: {
44
+ readonly force?: boolean;
45
+ readonly append?: boolean;
46
+ }): InstallResult;
47
+ export declare function planUninstall(text: string | null): UninstallResult;
48
+ /** `estela` en el PATH. Con `npx` cada evento tardaría cientos de milisegundos. */
49
+ export declare function estelaOnPath(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
50
+ /** "settings.json.bak-20261014-161230": una copia por instalación, sin pisar las anteriores. */
51
+ export declare function backupName(path: string, now?: Date): string;
52
+ //# sourceMappingURL=statusline-install.d.ts.map