charness 3.0.0 → 3.2.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.
Files changed (293) hide show
  1. package/.env.example +8 -7
  2. package/DEVELOPMENT.md +12 -6
  3. package/MIGRATION_1_TO_2.md +1 -1
  4. package/MIGRATION_RENAME_TO_CHARNESS.md +133 -0
  5. package/README.md +221 -555
  6. package/SETUP.md +539 -0
  7. package/dist/adapters/claudeCliAdapter.d.ts +16 -1
  8. package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
  9. package/dist/adapters/claudeCliAdapter.js +37 -2
  10. package/dist/adapters/claudeCliAdapter.js.map +1 -1
  11. package/dist/adapters/claudeConfigDir.d.ts +40 -0
  12. package/dist/adapters/claudeConfigDir.d.ts.map +1 -0
  13. package/dist/adapters/claudeConfigDir.js +3 -0
  14. package/dist/adapters/claudeConfigDir.js.map +1 -0
  15. package/dist/adapters/claudeJsonStreamAdapter.d.ts +32 -0
  16. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  17. package/dist/adapters/claudeJsonStreamAdapter.js +90 -8
  18. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  19. package/dist/adapters/claudePlatformFlags.d.ts +4 -4
  20. package/dist/adapters/claudePlatformFlags.js +4 -4
  21. package/dist/adapters/createAdapter.d.ts +8 -0
  22. package/dist/adapters/createAdapter.d.ts.map +1 -1
  23. package/dist/adapters/createAdapter.js +37 -0
  24. package/dist/adapters/createAdapter.js.map +1 -1
  25. package/dist/adapters/openCodeAdapter.d.ts +23 -0
  26. package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
  27. package/dist/adapters/openCodeAdapter.js +85 -2
  28. package/dist/adapters/openCodeAdapter.js.map +1 -1
  29. package/dist/agentLogin/agentLogin.d.ts +15 -2
  30. package/dist/agentLogin/agentLogin.d.ts.map +1 -1
  31. package/dist/agentLogin/agentLogin.js +53 -16
  32. package/dist/agentLogin/agentLogin.js.map +1 -1
  33. package/dist/apiErrorRetry.d.ts +8 -3
  34. package/dist/apiErrorRetry.d.ts.map +1 -1
  35. package/dist/apiErrorRetry.js +36 -5
  36. package/dist/apiErrorRetry.js.map +1 -1
  37. package/dist/apiRetryKick.d.ts +9 -0
  38. package/dist/apiRetryKick.d.ts.map +1 -1
  39. package/dist/apiRetryKick.js +16 -0
  40. package/dist/apiRetryKick.js.map +1 -1
  41. package/dist/bot.d.ts.map +1 -1
  42. package/dist/bot.js +642 -26
  43. package/dist/bot.js.map +1 -1
  44. package/dist/cli/bot.d.ts +4 -2
  45. package/dist/cli/bot.d.ts.map +1 -1
  46. package/dist/cli/bot.js +11 -3
  47. package/dist/cli/bot.js.map +1 -1
  48. package/dist/cli/botEntry.js +3 -1
  49. package/dist/cli/botEntry.js.map +1 -1
  50. package/dist/cli/claudeProfiles.d.ts +42 -0
  51. package/dist/cli/claudeProfiles.d.ts.map +1 -0
  52. package/dist/cli/claudeProfiles.js +106 -0
  53. package/dist/cli/claudeProfiles.js.map +1 -0
  54. package/dist/cli/envLoader.d.ts +6 -5
  55. package/dist/cli/envLoader.d.ts.map +1 -1
  56. package/dist/cli/envLoader.js +8 -27
  57. package/dist/cli/envLoader.js.map +1 -1
  58. package/dist/cli/hot.d.ts +2 -2
  59. package/dist/cli/hot.d.ts.map +1 -1
  60. package/dist/cli/hot.js +26 -16
  61. package/dist/cli/hot.js.map +1 -1
  62. package/dist/cli.js +19 -9
  63. package/dist/cli.js.map +1 -1
  64. package/dist/connectors/jira/adf.d.ts +23 -0
  65. package/dist/connectors/jira/adf.d.ts.map +1 -1
  66. package/dist/connectors/jira/adf.js +44 -0
  67. package/dist/connectors/jira/adf.js.map +1 -1
  68. package/dist/connectors/jira/answerSink.d.ts +27 -3
  69. package/dist/connectors/jira/answerSink.d.ts.map +1 -1
  70. package/dist/connectors/jira/answerSink.js +145 -18
  71. package/dist/connectors/jira/answerSink.js.map +1 -1
  72. package/dist/connectors/jira/attachmentTool.d.ts +7 -0
  73. package/dist/connectors/jira/attachmentTool.d.ts.map +1 -1
  74. package/dist/connectors/jira/attachmentTool.js +16 -0
  75. package/dist/connectors/jira/attachmentTool.js.map +1 -1
  76. package/dist/connectors/jira/client.d.ts +36 -0
  77. package/dist/connectors/jira/client.d.ts.map +1 -1
  78. package/dist/connectors/jira/client.js +46 -5
  79. package/dist/connectors/jira/client.js.map +1 -1
  80. package/dist/connectors/jira/config.d.ts +13 -17
  81. package/dist/connectors/jira/config.d.ts.map +1 -1
  82. package/dist/connectors/jira/config.js +15 -33
  83. package/dist/connectors/jira/config.js.map +1 -1
  84. package/dist/connectors/jira/connector.d.ts +10 -1
  85. package/dist/connectors/jira/connector.d.ts.map +1 -1
  86. package/dist/connectors/jira/connector.js +90 -16
  87. package/dist/connectors/jira/connector.js.map +1 -1
  88. package/dist/connectors/jira/contextLedger.d.ts +7 -0
  89. package/dist/connectors/jira/contextLedger.d.ts.map +1 -1
  90. package/dist/connectors/jira/contextLedger.js +18 -0
  91. package/dist/connectors/jira/contextLedger.js.map +1 -1
  92. package/dist/connectors/jira/contextPreamble.d.ts.map +1 -1
  93. package/dist/connectors/jira/contextPreamble.js +6 -3
  94. package/dist/connectors/jira/contextPreamble.js.map +1 -1
  95. package/dist/connectors/jira/inbound.d.ts +72 -10
  96. package/dist/connectors/jira/inbound.d.ts.map +1 -1
  97. package/dist/connectors/jira/inbound.js +204 -36
  98. package/dist/connectors/jira/inbound.js.map +1 -1
  99. package/dist/connectors/jira/issueBlocks.d.ts +31 -3
  100. package/dist/connectors/jira/issueBlocks.d.ts.map +1 -1
  101. package/dist/connectors/jira/issueBlocks.js +108 -29
  102. package/dist/connectors/jira/issueBlocks.js.map +1 -1
  103. package/dist/connectors/jira/issueContext.d.ts +11 -5
  104. package/dist/connectors/jira/issueContext.d.ts.map +1 -1
  105. package/dist/connectors/jira/issueContext.js +23 -6
  106. package/dist/connectors/jira/issueContext.js.map +1 -1
  107. package/dist/connectors/jira/issueDelta.d.ts +3 -0
  108. package/dist/connectors/jira/issueDelta.d.ts.map +1 -1
  109. package/dist/connectors/jira/issueDelta.js +15 -6
  110. package/dist/connectors/jira/issueDelta.js.map +1 -1
  111. package/dist/connectors/jira/mediaPlaceholders.d.ts +2 -0
  112. package/dist/connectors/jira/mediaPlaceholders.d.ts.map +1 -1
  113. package/dist/connectors/jira/mediaPlaceholders.js +16 -3
  114. package/dist/connectors/jira/mediaPlaceholders.js.map +1 -1
  115. package/dist/connectors/jira/prompt.d.ts +4 -0
  116. package/dist/connectors/jira/prompt.d.ts.map +1 -1
  117. package/dist/connectors/jira/prompt.js +20 -1
  118. package/dist/connectors/jira/prompt.js.map +1 -1
  119. package/dist/connectors/jira/sessionKeyCodec.d.ts +2 -0
  120. package/dist/connectors/jira/sessionKeyCodec.d.ts.map +1 -1
  121. package/dist/connectors/jira/sessionKeyCodec.js +5 -0
  122. package/dist/connectors/jira/sessionKeyCodec.js.map +1 -1
  123. package/dist/connectors/jira/trigger.d.ts +16 -3
  124. package/dist/connectors/jira/trigger.d.ts.map +1 -1
  125. package/dist/connectors/jira/trigger.js +17 -2
  126. package/dist/connectors/jira/trigger.js.map +1 -1
  127. package/dist/connectors/jira/triggerLog.d.ts +12 -3
  128. package/dist/connectors/jira/triggerLog.d.ts.map +1 -1
  129. package/dist/connectors/jira/triggerLog.js +18 -2
  130. package/dist/connectors/jira/triggerLog.js.map +1 -1
  131. package/dist/connectors/jira/trustedGroup.d.ts +54 -0
  132. package/dist/connectors/jira/trustedGroup.d.ts.map +1 -0
  133. package/dist/connectors/jira/trustedGroup.js +98 -0
  134. package/dist/connectors/jira/trustedGroup.js.map +1 -0
  135. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts +3 -0
  136. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts.map +1 -1
  137. package/dist/connectors/telegram/commands/autoContinueLimits.js +15 -45
  138. package/dist/connectors/telegram/commands/autoContinueLimits.js.map +1 -1
  139. package/dist/connectors/telegram/commands/claudeAccounts.d.ts +58 -0
  140. package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -0
  141. package/dist/connectors/telegram/commands/claudeAccounts.js +591 -0
  142. package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -0
  143. package/dist/connectors/telegram/commands/fallbackChain.d.ts +49 -0
  144. package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -0
  145. package/dist/connectors/telegram/commands/fallbackChain.js +624 -0
  146. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -0
  147. package/dist/connectors/telegram/commands/modelProviders.d.ts +23 -1
  148. package/dist/connectors/telegram/commands/modelProviders.d.ts.map +1 -1
  149. package/dist/connectors/telegram/commands/modelProviders.js +131 -6
  150. package/dist/connectors/telegram/commands/modelProviders.js.map +1 -1
  151. package/dist/i18n/de.d.ts.map +1 -1
  152. package/dist/i18n/de.js +137 -9
  153. package/dist/i18n/de.js.map +1 -1
  154. package/dist/i18n/en.d.ts.map +1 -1
  155. package/dist/i18n/en.js +137 -9
  156. package/dist/i18n/en.js.map +1 -1
  157. package/dist/i18n/es.d.ts.map +1 -1
  158. package/dist/i18n/es.js +137 -9
  159. package/dist/i18n/es.js.map +1 -1
  160. package/dist/i18n/fr.d.ts.map +1 -1
  161. package/dist/i18n/fr.js +137 -9
  162. package/dist/i18n/fr.js.map +1 -1
  163. package/dist/i18n/hi.d.ts.map +1 -1
  164. package/dist/i18n/hi.js +137 -9
  165. package/dist/i18n/hi.js.map +1 -1
  166. package/dist/i18n/ja.d.ts.map +1 -1
  167. package/dist/i18n/ja.js +137 -9
  168. package/dist/i18n/ja.js.map +1 -1
  169. package/dist/i18n/ka.d.ts.map +1 -1
  170. package/dist/i18n/ka.js +137 -9
  171. package/dist/i18n/ka.js.map +1 -1
  172. package/dist/i18n/pt.d.ts.map +1 -1
  173. package/dist/i18n/pt.js +137 -9
  174. package/dist/i18n/pt.js.map +1 -1
  175. package/dist/i18n/ru.d.ts.map +1 -1
  176. package/dist/i18n/ru.js +137 -9
  177. package/dist/i18n/ru.js.map +1 -1
  178. package/dist/i18n/uk.d.ts.map +1 -1
  179. package/dist/i18n/uk.js +137 -9
  180. package/dist/i18n/uk.js.map +1 -1
  181. package/dist/i18n/uz.d.ts.map +1 -1
  182. package/dist/i18n/uz.js +137 -9
  183. package/dist/i18n/uz.js.map +1 -1
  184. package/dist/i18n/zh.d.ts.map +1 -1
  185. package/dist/i18n/zh.js +137 -9
  186. package/dist/i18n/zh.js.map +1 -1
  187. package/dist/index.js +1 -1
  188. package/dist/index.js.map +1 -1
  189. package/dist/installManager.d.ts +16 -2
  190. package/dist/installManager.d.ts.map +1 -1
  191. package/dist/installManager.js +42 -12
  192. package/dist/installManager.js.map +1 -1
  193. package/dist/instanceLock.d.ts.map +1 -1
  194. package/dist/instanceLock.js +10 -8
  195. package/dist/instanceLock.js.map +1 -1
  196. package/dist/legacyMigration.d.ts +69 -0
  197. package/dist/legacyMigration.d.ts.map +1 -0
  198. package/dist/legacyMigration.js +338 -0
  199. package/dist/legacyMigration.js.map +1 -0
  200. package/dist/legacyNames.d.ts +107 -0
  201. package/dist/legacyNames.d.ts.map +1 -0
  202. package/dist/legacyNames.js +230 -0
  203. package/dist/legacyNames.js.map +1 -0
  204. package/dist/platform/answerSink.d.ts +5 -0
  205. package/dist/platform/answerSink.d.ts.map +1 -1
  206. package/dist/platform/answerSink.js.map +1 -1
  207. package/dist/requests/answerRequest.d.ts +2 -0
  208. package/dist/requests/answerRequest.d.ts.map +1 -1
  209. package/dist/requests/answerRequest.js +3 -1
  210. package/dist/requests/answerRequest.js.map +1 -1
  211. package/dist/requests/answerStyle.d.ts +11 -0
  212. package/dist/requests/answerStyle.d.ts.map +1 -0
  213. package/dist/requests/answerStyle.js +14 -0
  214. package/dist/requests/answerStyle.js.map +1 -0
  215. package/dist/requests/answerTail.d.ts.map +1 -1
  216. package/dist/requests/answerTail.js +2 -0
  217. package/dist/requests/answerTail.js.map +1 -1
  218. package/dist/requests/requestHeader.d.ts.map +1 -1
  219. package/dist/requests/requestHeader.js +3 -0
  220. package/dist/requests/requestHeader.js.map +1 -1
  221. package/dist/requests/requestLedger.js +1 -1
  222. package/dist/requests/requestLedger.js.map +1 -1
  223. package/dist/requests/types.d.ts +3 -1
  224. package/dist/requests/types.d.ts.map +1 -1
  225. package/dist/scheduler/mcpSurface.d.ts +17 -1
  226. package/dist/scheduler/mcpSurface.d.ts.map +1 -1
  227. package/dist/scheduler/mcpSurface.js +86 -13
  228. package/dist/scheduler/mcpSurface.js.map +1 -1
  229. package/dist/state.d.ts +175 -2
  230. package/dist/state.d.ts.map +1 -1
  231. package/dist/state.js +303 -2
  232. package/dist/state.js.map +1 -1
  233. package/dist/types.d.ts +7 -0
  234. package/dist/types.d.ts.map +1 -1
  235. package/dist/utils/autoContinueOnLimit.d.ts +7 -0
  236. package/dist/utils/autoContinueOnLimit.d.ts.map +1 -1
  237. package/dist/utils/autoContinueOnLimit.js +10 -0
  238. package/dist/utils/autoContinueOnLimit.js.map +1 -1
  239. package/dist/utils/claudeAccountPickerPlan.d.ts +85 -0
  240. package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -0
  241. package/dist/utils/claudeAccountPickerPlan.js +123 -0
  242. package/dist/utils/claudeAccountPickerPlan.js.map +1 -0
  243. package/dist/utils/claudeLimitSwitch.d.ts +40 -0
  244. package/dist/utils/claudeLimitSwitch.d.ts.map +1 -0
  245. package/dist/utils/claudeLimitSwitch.js +54 -0
  246. package/dist/utils/claudeLimitSwitch.js.map +1 -0
  247. package/dist/utils/claudeProfileUsageLines.d.ts +15 -0
  248. package/dist/utils/claudeProfileUsageLines.d.ts.map +1 -0
  249. package/dist/utils/claudeProfileUsageLines.js +42 -0
  250. package/dist/utils/claudeProfileUsageLines.js.map +1 -0
  251. package/dist/utils/claudeProfiles.d.ts +124 -0
  252. package/dist/utils/claudeProfiles.d.ts.map +1 -0
  253. package/dist/utils/claudeProfiles.js +304 -0
  254. package/dist/utils/claudeProfiles.js.map +1 -0
  255. package/dist/utils/claudeStreamJson.d.ts +13 -1
  256. package/dist/utils/claudeStreamJson.d.ts.map +1 -1
  257. package/dist/utils/claudeStreamJson.js +13 -0
  258. package/dist/utils/claudeStreamJson.js.map +1 -1
  259. package/dist/utils/claudeUsage.d.ts +209 -0
  260. package/dist/utils/claudeUsage.d.ts.map +1 -0
  261. package/dist/utils/claudeUsage.js +413 -0
  262. package/dist/utils/claudeUsage.js.map +1 -0
  263. package/dist/utils/claudeUsageRefresher.d.ts +44 -0
  264. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -0
  265. package/dist/utils/claudeUsageRefresher.js +95 -0
  266. package/dist/utils/claudeUsageRefresher.js.map +1 -0
  267. package/dist/utils/fallbackBridge.d.ts +32 -0
  268. package/dist/utils/fallbackBridge.d.ts.map +1 -0
  269. package/dist/utils/fallbackBridge.js +62 -0
  270. package/dist/utils/fallbackBridge.js.map +1 -0
  271. package/dist/utils/fallbackChain.d.ts +299 -0
  272. package/dist/utils/fallbackChain.d.ts.map +1 -0
  273. package/dist/utils/fallbackChain.js +542 -0
  274. package/dist/utils/fallbackChain.js.map +1 -0
  275. package/dist/utils/jsonStreamHost.d.ts +14 -3
  276. package/dist/utils/jsonStreamHost.d.ts.map +1 -1
  277. package/dist/utils/jsonStreamHost.js +26 -5
  278. package/dist/utils/jsonStreamHost.js.map +1 -1
  279. package/dist/utils/modelRecentPlan.d.ts +56 -0
  280. package/dist/utils/modelRecentPlan.d.ts.map +1 -0
  281. package/dist/utils/modelRecentPlan.js +107 -0
  282. package/dist/utils/modelRecentPlan.js.map +1 -0
  283. package/dist/utils/openCodeCompactPlugin.d.ts +11 -3
  284. package/dist/utils/openCodeCompactPlugin.d.ts.map +1 -1
  285. package/dist/utils/openCodeCompactPlugin.js +37 -5
  286. package/dist/utils/openCodeCompactPlugin.js.map +1 -1
  287. package/dist/utils/threadStatusReport.d.ts +25 -1
  288. package/dist/utils/threadStatusReport.d.ts.map +1 -1
  289. package/dist/utils/threadStatusReport.js +24 -1
  290. package/dist/utils/threadStatusReport.js.map +1 -1
  291. package/examples/.env.example +7 -7
  292. package/examples/docker-compose.yml +15 -14
  293. package/package.json +3 -1
@@ -0,0 +1,209 @@
1
+ /**
2
+ * @description Per-profile Claude USAGE — how much of each subscription window an account profile
3
+ * (`utils/claudeProfiles.ts`) has used, and which profile a new process should run on.
4
+ *
5
+ * Two sources feed one snapshot per profile (`ClaudeProfileRecord.usage`):
6
+ * - the OAuth usage endpoint (`GET https://api.anthropic.com/api/oauth/usage`, the profile's own access
7
+ * token off its `.credentials.json`), polled by the bot (`claudeUsageRefresher.ts`) and on demand — its
8
+ * `limits[]` carry the three windows: `session` (five hours), `weekly_all` (seven days, every model) and
9
+ * `weekly_scoped` (seven days, ONE model — Fable; `scope.model.display_name`), measured on 2026-10-12;
10
+ * - the `rate_limit_event` frames of a json-stream session, one per API request, whose `unifiedWindows`
11
+ * carry the same windows under their CLI names (`five_hour`, `seven_day`, `seven_day_overage_included`
12
+ * — the last one IS the Fable window: same utilization and reset instant as the endpoint's scoped limit).
13
+ *
14
+ * Token refresh is the CLI's job: a token past its `expiresAt` is never used (nor refreshed here) — the
15
+ * snapshot is marked stale and the next CLI run renews it. The token itself is never logged or returned.
16
+ *
17
+ * Pure except {@link readClaudeOAuthToken} (one file read) and {@link fetchClaudeUsage} (one GET through an
18
+ * injectable `fetch`), so the rest is unit-tested without the network.
19
+ */
20
+ import type { ClaudeProfileRecord } from '../state';
21
+ import { type ClaudeAccountKind } from './claudeProfiles';
22
+ export declare const claudeUsageEndpoint = "https://api.anthropic.com/api/oauth/usage";
23
+ export declare const claudeUsageFetchTimeoutMs = 15000;
24
+ /** The one usage window kind set, named by what it bounds rather than by the API's spelling. */
25
+ export type ClaudeUsageBucketKind = 'session' | 'weekly_all' | 'weekly_scoped';
26
+ export declare const claudeUsageBucketKinds: readonly ClaudeUsageBucketKind[];
27
+ export interface ClaudeUsageBucket {
28
+ kind: ClaudeUsageBucketKind;
29
+ /** 0..100, the endpoint's `percent` (the CLI's `utilization` × 100), rounded. */
30
+ percent: number;
31
+ /** ISO instant the window resets; `null` when the source carried none. */
32
+ resetsAt: string | null;
33
+ /** The model a `weekly_scoped` bucket is for (`"Fable"`); `null` for the shared windows. */
34
+ scopeModel: string | null;
35
+ }
36
+ /** The cached usage of one profile — what `ClaudeProfileRecord.usage` holds. */
37
+ export interface ClaudeUsageSnapshot {
38
+ /** ISO instant of the last write (either source). */
39
+ fetchedAt: string;
40
+ source: 'api' | 'stream';
41
+ buckets: ClaudeUsageBucket[];
42
+ /**
43
+ * The profile's token had expired when the poll ran (the CLI renews it at its next run) or the fetch
44
+ * failed: the buckets are the LAST known ones, not fresh.
45
+ */
46
+ isStale?: boolean;
47
+ /** Why the last poll did not refresh (short, never the token). */
48
+ error?: string;
49
+ }
50
+ export interface ClaudeOAuthToken {
51
+ accessToken: string;
52
+ /** Epoch ms; `null` when the file carries none. */
53
+ expiresAtMs: number | null;
54
+ }
55
+ /**
56
+ * @description The subscription access token of a profile, off `<configDir>/.credentials.json`
57
+ * (`~/.claude/.credentials.json` for the default profile). `null` when the file or the OAuth entry is absent —
58
+ * a logged-out directory. The caller never logs the result.
59
+ */
60
+ export declare function readClaudeOAuthToken(configDir: string | null, homeClaudeDir?: string): ClaudeOAuthToken | null;
61
+ /** Whether the token is past its expiry at `nowMs` (a token without expiry counts as live). */
62
+ export declare function checkIsClaudeTokenExpired(token: ClaudeOAuthToken, nowMs: number): boolean;
63
+ export type ClaudeUsageFetchResult = {
64
+ ok: true;
65
+ buckets: ClaudeUsageBucket[];
66
+ } | {
67
+ ok: false;
68
+ error: string;
69
+ isAuthError: boolean;
70
+ };
71
+ type FetchLike = (url: string, init: {
72
+ headers: Record<string, string>;
73
+ signal: AbortSignal;
74
+ }) => Promise<{
75
+ status: number;
76
+ json(): Promise<unknown>;
77
+ }>;
78
+ /**
79
+ * @description One GET of the usage endpoint with the profile's token. A 401/403 is an auth error (the
80
+ * token is no longer accepted — the CLI's next run renews it); anything else is a plain failure. Never
81
+ * throws; never includes the token in the result.
82
+ */
83
+ export declare function fetchClaudeUsage(token: string, options?: {
84
+ fetchImpl?: FetchLike;
85
+ timeoutMs?: number;
86
+ }): Promise<ClaudeUsageFetchResult>;
87
+ /**
88
+ * @description The buckets of a usage response. Reads `limits[]` (`kind` session / weekly_all /
89
+ * weekly_scoped, `percent`, `resets_at`, `scope.model.display_name`); a response without it (an older
90
+ * shape) falls back to the top-level `five_hour` / `seven_day` objects (`utilization` already 0..100).
91
+ * Unknown kinds are dropped, so a window the API adds later never breaks a reader.
92
+ */
93
+ export declare function parseClaudeUsageResponse(body: unknown): ClaudeUsageBucket[];
94
+ /** One `unifiedWindows` entry of a `rate_limit_event` frame, as `claudeStreamJson.ts` lifts it. */
95
+ export interface ClaudeRateLimitWindow {
96
+ /** `five_hour` / `seven_day` / `seven_day_overage_included` (the CLI's names). */
97
+ name: string;
98
+ /** 0..1 */
99
+ utilization: number;
100
+ /** Epoch ms; `null` when absent. */
101
+ resetsAtMs: number | null;
102
+ }
103
+ /** The buckets a frame's windows carry (unknown window names are dropped). */
104
+ export declare function buildBucketsFromRateLimitWindows(windows: readonly ClaudeRateLimitWindow[]): ClaudeUsageBucket[];
105
+ /**
106
+ * @description Fold fresh buckets into a profile's snapshot. The endpoint answers with EVERY window, so an
107
+ * `api` read replaces the set; a frame carries only the windows the request touched, so a `stream` read
108
+ * upserts those and keeps the rest. Either way the snapshot is fresh (not stale) afterwards.
109
+ */
110
+ export declare function mergeClaudeUsageSnapshot(previous: ClaudeUsageSnapshot | undefined, incoming: readonly ClaudeUsageBucket[], source: ClaudeUsageSnapshot['source'], nowMs: number): ClaudeUsageSnapshot;
111
+ /** The snapshot kept as it was, marked stale with the reason (a failed or impossible poll). */
112
+ export declare function markClaudeUsageSnapshotStale(previous: ClaudeUsageSnapshot | undefined, error: string, nowMs: number): ClaudeUsageSnapshot;
113
+ /** Whether `next` says anything `previous` did not — a stream frame per API request must not rewrite state.json each time. */
114
+ export declare function checkClaudeUsageBucketsChanged(previous: ClaudeUsageSnapshot | undefined, next: readonly ClaudeUsageBucket[]): boolean;
115
+ export declare function getClaudeUsageBucket(snapshot: ClaudeUsageSnapshot | undefined, kind: ClaudeUsageBucketKind): ClaudeUsageBucket | null;
116
+ /** Whether a model name (`fable`, `claude-fable-5-1`, …) runs on the scoped weekly window. */
117
+ export declare function checkIsScopedWeeklyModel(model: string | null): boolean;
118
+ /** The windows that bound a launch of `model`: the session window plus the weekly one the model drains. */
119
+ export declare function getRelevantClaudeUsageKinds(model: string | null): ClaudeUsageBucketKind[];
120
+ /** What the ranking needs to know about one profile (the bot reads it off state, the CLI off state.json). */
121
+ export interface ClaudeProfileUsageView {
122
+ name: string;
123
+ configDir: string | null;
124
+ loggedIn: boolean | undefined;
125
+ email: string | null;
126
+ subscriptionType: string | null;
127
+ usage: ClaudeUsageSnapshot | undefined;
128
+ limitedUntil: string | null;
129
+ }
130
+ /** One profile in the report the MCP tool and the CLI hand to an agent. */
131
+ export interface ClaudeProfileReportEntry {
132
+ name: string;
133
+ /** The `CLAUDE_CONFIG_DIR` to launch under; `null` for the default profile (set nothing). */
134
+ configDir: string | null;
135
+ email: string | null;
136
+ kind: ClaudeAccountKind;
137
+ subscriptionType: string | null;
138
+ /** `null` when never probed. */
139
+ loggedIn: boolean | null;
140
+ buckets: ClaudeUsageBucket[];
141
+ usageFetchedAt: string | null;
142
+ isUsageStale: boolean;
143
+ limitedUntil: string | null;
144
+ /** Signed in, not limited, and not at 100 % on a window the model drains. */
145
+ isEligible: boolean;
146
+ /** `100 − max(relevant percent)`; `null` when the usage is unknown. */
147
+ headroomPercent: number | null;
148
+ isRecommended: boolean;
149
+ }
150
+ export interface ClaudeProfilesReport {
151
+ model: string | null;
152
+ /** The windows the ranking looked at for `model`. */
153
+ relevantBuckets: ClaudeUsageBucketKind[];
154
+ /** The profile a new process of `model` should run on; `null` when none is eligible. */
155
+ recommended: string | null;
156
+ profiles: ClaudeProfileReportEntry[];
157
+ }
158
+ /**
159
+ * @description Rank the profiles for a launch of `model`: a profile that is signed out, marked limited
160
+ * (`limitedUntil` in the future) or at 100 % on a relevant window is out; the rest are ordered by headroom on
161
+ * the relevant windows (most first; unknown usage ranks after any known one; a tie goes to the lower total of
162
+ * the relevant percentages). The first in that order is `recommended`. The entries keep the input order.
163
+ */
164
+ export declare function buildClaudeProfilesReport(profiles: readonly ClaudeProfileUsageView[], model: string | null, nowMs: number): ClaudeProfilesReport;
165
+ /**
166
+ * @description The ranking's view of every profile off the registry: `default` (its hidden record, when the
167
+ * bot has written one) first, then the added profiles in creation order. Shared by the bot (live state) and
168
+ * the CLI (`state.json` read directly).
169
+ */
170
+ export declare function buildClaudeProfileUsageViews(input: {
171
+ dataDir: string;
172
+ /** Every record, the hidden `default` one included (`StateStore.getClaudeProfileInfo` / the raw file). */
173
+ records: Readonly<Record<string, ClaudeProfileRecord>>;
174
+ }): ClaudeProfileUsageView[];
175
+ /** The profile registry as `state.json` holds it — the CLI's read, no `StateStore` (which would migrate and lock). */
176
+ export declare function readClaudeProfileRecordsFromStateFile(dataDir: string): Readonly<Record<string, ClaudeProfileRecord>>;
177
+ /**
178
+ * @description A reset instant for a human, in the instance zone: `16:00` when it is today there, `Sat 00:00`
179
+ * within the week, `17.10 00:00` beyond. The weekday is English on purpose (a three-letter token every locale
180
+ * reads; the surrounding line is localised).
181
+ */
182
+ export declare function formatClaudeUsageResetAt(resetsAt: string | null, timezone: string, nowMs: number): string;
183
+ /** The words a relative reset is spelled with: `pattern` carries `{time}`, the units are appended to their numbers. */
184
+ export interface ClaudeRelativeResetWords {
185
+ pattern: string;
186
+ day: string;
187
+ hour: string;
188
+ minute: string;
189
+ }
190
+ /** English words, the CLI's (the bot passes its localised ones). */
191
+ export declare const englishRelativeResetWords: ClaudeRelativeResetWords;
192
+ /**
193
+ * @description A reset instant as a time from now: `🔄 in 2d 1h`, `🔄 in 2h`, `🔄 in 30m`. The two largest
194
+ * units at most, minutes only under an hour: from a day up it is days + hours (hours dropped when zero), from
195
+ * an hour up whole hours (rounded), below that minutes (at least one). An instant already past reads as
196
+ * `🔄 in 1m` (the window is about to roll over). `?` for a missing or unparseable instant.
197
+ */
198
+ export declare function formatClaudeUsageRelativeReset(resetsAt: string | null, nowMs: number, words?: ClaudeRelativeResetWords): string;
199
+ /**
200
+ * @description `81% 🔄 in 2h` for a bucket, `—` for a missing one. A bucket whose reset is unknown shows
201
+ * `fallbackResetsAt` instead (the session window's reset is often missing while the weekly one is known);
202
+ * with neither it is the bare percent — a «?» after a number would read as a doubt about the number.
203
+ */
204
+ export declare function formatClaudeUsageBucket(bucket: ClaudeUsageBucket | null, nowMs: number, options?: {
205
+ fallbackResetsAt?: string | null;
206
+ words?: ClaudeRelativeResetWords;
207
+ }): string;
208
+ export {};
209
+ //# sourceMappingURL=claudeUsage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claudeUsage.d.ts","sourceRoot":"","sources":["../../src/utils/claudeUsage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,413 @@
1
+ "use strict";
2
+ /**
3
+ * @description Per-profile Claude USAGE — how much of each subscription window an account profile
4
+ * (`utils/claudeProfiles.ts`) has used, and which profile a new process should run on.
5
+ *
6
+ * Two sources feed one snapshot per profile (`ClaudeProfileRecord.usage`):
7
+ * - the OAuth usage endpoint (`GET https://api.anthropic.com/api/oauth/usage`, the profile's own access
8
+ * token off its `.credentials.json`), polled by the bot (`claudeUsageRefresher.ts`) and on demand — its
9
+ * `limits[]` carry the three windows: `session` (five hours), `weekly_all` (seven days, every model) and
10
+ * `weekly_scoped` (seven days, ONE model — Fable; `scope.model.display_name`), measured on 2026-10-12;
11
+ * - the `rate_limit_event` frames of a json-stream session, one per API request, whose `unifiedWindows`
12
+ * carry the same windows under their CLI names (`five_hour`, `seven_day`, `seven_day_overage_included`
13
+ * — the last one IS the Fable window: same utilization and reset instant as the endpoint's scoped limit).
14
+ *
15
+ * Token refresh is the CLI's job: a token past its `expiresAt` is never used (nor refreshed here) — the
16
+ * snapshot is marked stale and the next CLI run renews it. The token itself is never logged or returned.
17
+ *
18
+ * Pure except {@link readClaudeOAuthToken} (one file read) and {@link fetchClaudeUsage} (one GET through an
19
+ * injectable `fetch`), so the rest is unit-tested without the network.
20
+ */
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ var desc = Object.getOwnPropertyDescriptor(m, k);
24
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
25
+ desc = { enumerable: true, get: function() { return m[k]; } };
26
+ }
27
+ Object.defineProperty(o, k2, desc);
28
+ }) : (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ o[k2] = m[k];
31
+ }));
32
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
33
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34
+ }) : function(o, v) {
35
+ o["default"] = v;
36
+ });
37
+ var __importStar = (this && this.__importStar) || (function () {
38
+ var ownKeys = function(o) {
39
+ ownKeys = Object.getOwnPropertyNames || function (o) {
40
+ var ar = [];
41
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
42
+ return ar;
43
+ };
44
+ return ownKeys(o);
45
+ };
46
+ return function (mod) {
47
+ if (mod && mod.__esModule) return mod;
48
+ var result = {};
49
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
54
+ Object.defineProperty(exports, "__esModule", { value: true });
55
+ exports.englishRelativeResetWords = exports.claudeUsageBucketKinds = exports.claudeUsageFetchTimeoutMs = exports.claudeUsageEndpoint = void 0;
56
+ exports.readClaudeOAuthToken = readClaudeOAuthToken;
57
+ exports.checkIsClaudeTokenExpired = checkIsClaudeTokenExpired;
58
+ exports.fetchClaudeUsage = fetchClaudeUsage;
59
+ exports.parseClaudeUsageResponse = parseClaudeUsageResponse;
60
+ exports.buildBucketsFromRateLimitWindows = buildBucketsFromRateLimitWindows;
61
+ exports.mergeClaudeUsageSnapshot = mergeClaudeUsageSnapshot;
62
+ exports.markClaudeUsageSnapshotStale = markClaudeUsageSnapshotStale;
63
+ exports.checkClaudeUsageBucketsChanged = checkClaudeUsageBucketsChanged;
64
+ exports.getClaudeUsageBucket = getClaudeUsageBucket;
65
+ exports.checkIsScopedWeeklyModel = checkIsScopedWeeklyModel;
66
+ exports.getRelevantClaudeUsageKinds = getRelevantClaudeUsageKinds;
67
+ exports.buildClaudeProfilesReport = buildClaudeProfilesReport;
68
+ exports.buildClaudeProfileUsageViews = buildClaudeProfileUsageViews;
69
+ exports.readClaudeProfileRecordsFromStateFile = readClaudeProfileRecordsFromStateFile;
70
+ exports.formatClaudeUsageResetAt = formatClaudeUsageResetAt;
71
+ exports.formatClaudeUsageRelativeReset = formatClaudeUsageRelativeReset;
72
+ exports.formatClaudeUsageBucket = formatClaudeUsageBucket;
73
+ const fs = __importStar(require("fs"));
74
+ const path = __importStar(require("path"));
75
+ const claudeProfiles_1 = require("./claudeProfiles");
76
+ exports.claudeUsageEndpoint = 'https://api.anthropic.com/api/oauth/usage';
77
+ /** The OAuth beta header the CLI sends with its subscription token. */
78
+ const claudeOAuthBetaHeader = 'oauth-2025-04-20';
79
+ exports.claudeUsageFetchTimeoutMs = 15_000;
80
+ exports.claudeUsageBucketKinds = ['session', 'weekly_all', 'weekly_scoped'];
81
+ /**
82
+ * @description The subscription access token of a profile, off `<configDir>/.credentials.json`
83
+ * (`~/.claude/.credentials.json` for the default profile). `null` when the file or the OAuth entry is absent —
84
+ * a logged-out directory. The caller never logs the result.
85
+ */
86
+ function readClaudeOAuthToken(configDir, homeClaudeDir = (0, claudeProfiles_1.resolveHomeClaudeDir)()) {
87
+ const file = path.join(configDir ?? homeClaudeDir, '.credentials.json');
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ const oauth = checkIsRecord(parsed) && checkIsRecord(parsed.claudeAiOauth) ? parsed.claudeAiOauth : null;
96
+ if (!oauth || typeof oauth.accessToken !== 'string' || oauth.accessToken === '')
97
+ return null;
98
+ return { accessToken: oauth.accessToken, expiresAtMs: typeof oauth.expiresAt === 'number' ? oauth.expiresAt : null };
99
+ }
100
+ /** Whether the token is past its expiry at `nowMs` (a token without expiry counts as live). */
101
+ function checkIsClaudeTokenExpired(token, nowMs) {
102
+ return token.expiresAtMs !== null && token.expiresAtMs <= nowMs;
103
+ }
104
+ /**
105
+ * @description One GET of the usage endpoint with the profile's token. A 401/403 is an auth error (the
106
+ * token is no longer accepted — the CLI's next run renews it); anything else is a plain failure. Never
107
+ * throws; never includes the token in the result.
108
+ */
109
+ async function fetchClaudeUsage(token, options = {}) {
110
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
111
+ const controller = new AbortController();
112
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? exports.claudeUsageFetchTimeoutMs);
113
+ try {
114
+ const response = await fetchImpl(exports.claudeUsageEndpoint, {
115
+ headers: { Authorization: `Bearer ${token}`, 'anthropic-beta': claudeOAuthBetaHeader, Accept: 'application/json' },
116
+ signal: controller.signal,
117
+ });
118
+ if (response.status === 401 || response.status === 403)
119
+ return { ok: false, error: `http ${response.status}`, isAuthError: true };
120
+ if (response.status !== 200)
121
+ return { ok: false, error: `http ${response.status}`, isAuthError: false };
122
+ const buckets = parseClaudeUsageResponse(await response.json());
123
+ if (buckets.length === 0)
124
+ return { ok: false, error: 'no limits in the response', isAuthError: false };
125
+ return { ok: true, buckets };
126
+ }
127
+ catch (e) {
128
+ const reason = e instanceof Error ? (e.name === 'AbortError' ? 'timeout' : e.message) : String(e);
129
+ return { ok: false, error: reason.slice(0, 120), isAuthError: false };
130
+ }
131
+ finally {
132
+ clearTimeout(timer);
133
+ }
134
+ }
135
+ /**
136
+ * @description The buckets of a usage response. Reads `limits[]` (`kind` session / weekly_all /
137
+ * weekly_scoped, `percent`, `resets_at`, `scope.model.display_name`); a response without it (an older
138
+ * shape) falls back to the top-level `five_hour` / `seven_day` objects (`utilization` already 0..100).
139
+ * Unknown kinds are dropped, so a window the API adds later never breaks a reader.
140
+ */
141
+ function parseClaudeUsageResponse(body) {
142
+ if (!checkIsRecord(body))
143
+ return [];
144
+ if (Array.isArray(body.limits)) {
145
+ const buckets = [];
146
+ for (const limit of body.limits) {
147
+ if (!checkIsRecord(limit))
148
+ continue;
149
+ const kind = readBucketKind(limit.kind);
150
+ if (!kind || typeof limit.percent !== 'number')
151
+ continue;
152
+ const scope = checkIsRecord(limit.scope) && checkIsRecord(limit.scope.model) ? limit.scope.model : null;
153
+ buckets.push({
154
+ kind,
155
+ percent: clampPercent(limit.percent),
156
+ resetsAt: readIso(limit.resets_at),
157
+ scopeModel: scope && typeof scope.display_name === 'string' ? scope.display_name : null,
158
+ });
159
+ }
160
+ if (buckets.length > 0)
161
+ return buckets;
162
+ }
163
+ const buckets = [];
164
+ for (const [field, kind] of [['five_hour', 'session'], ['seven_day', 'weekly_all']]) {
165
+ const window = body[field];
166
+ if (!checkIsRecord(window) || typeof window.utilization !== 'number')
167
+ continue;
168
+ buckets.push({ kind, percent: clampPercent(window.utilization), resetsAt: readIso(window.resets_at), scopeModel: null });
169
+ }
170
+ return buckets;
171
+ }
172
+ function readBucketKind(value) {
173
+ return value === 'session' || value === 'weekly_all' || value === 'weekly_scoped' ? value : null;
174
+ }
175
+ /** The CLI's window names → bucket kinds; `seven_day_overage_included` is the Fable window (see the module doc). */
176
+ const rateLimitWindowKinds = {
177
+ five_hour: { kind: 'session', scopeModel: null },
178
+ seven_day: { kind: 'weekly_all', scopeModel: null },
179
+ seven_day_overage_included: { kind: 'weekly_scoped', scopeModel: 'Fable' },
180
+ };
181
+ /** The buckets a frame's windows carry (unknown window names are dropped). */
182
+ function buildBucketsFromRateLimitWindows(windows) {
183
+ const buckets = [];
184
+ for (const window of windows) {
185
+ const mapping = rateLimitWindowKinds[window.name];
186
+ if (!mapping)
187
+ continue;
188
+ buckets.push({
189
+ kind: mapping.kind,
190
+ percent: clampPercent(window.utilization * 100),
191
+ resetsAt: window.resetsAtMs === null ? null : new Date(window.resetsAtMs).toISOString(),
192
+ scopeModel: mapping.scopeModel,
193
+ });
194
+ }
195
+ return buckets;
196
+ }
197
+ // ── the snapshot ──
198
+ /**
199
+ * @description Fold fresh buckets into a profile's snapshot. The endpoint answers with EVERY window, so an
200
+ * `api` read replaces the set; a frame carries only the windows the request touched, so a `stream` read
201
+ * upserts those and keeps the rest. Either way the snapshot is fresh (not stale) afterwards.
202
+ */
203
+ function mergeClaudeUsageSnapshot(previous, incoming, source, nowMs) {
204
+ let buckets;
205
+ if (source === 'api' || !previous) {
206
+ buckets = [...incoming];
207
+ }
208
+ else {
209
+ buckets = previous.buckets.filter((bucket) => !incoming.some((fresh) => fresh.kind === bucket.kind));
210
+ buckets.push(...incoming);
211
+ }
212
+ buckets.sort((a, b) => exports.claudeUsageBucketKinds.indexOf(a.kind) - exports.claudeUsageBucketKinds.indexOf(b.kind));
213
+ return { fetchedAt: new Date(nowMs).toISOString(), source, buckets };
214
+ }
215
+ /** The snapshot kept as it was, marked stale with the reason (a failed or impossible poll). */
216
+ function markClaudeUsageSnapshotStale(previous, error, nowMs) {
217
+ return { fetchedAt: previous?.fetchedAt ?? new Date(nowMs).toISOString(), source: previous?.source ?? 'api', buckets: previous?.buckets ?? [], isStale: true, error };
218
+ }
219
+ /** Whether `next` says anything `previous` did not — a stream frame per API request must not rewrite state.json each time. */
220
+ function checkClaudeUsageBucketsChanged(previous, next) {
221
+ if (!previous || previous.isStale)
222
+ return true;
223
+ return next.some((bucket) => {
224
+ const old = previous.buckets.find((candidate) => candidate.kind === bucket.kind);
225
+ return !old || old.percent !== bucket.percent || old.resetsAt !== bucket.resetsAt;
226
+ });
227
+ }
228
+ function getClaudeUsageBucket(snapshot, kind) {
229
+ return snapshot?.buckets.find((bucket) => bucket.kind === kind) ?? null;
230
+ }
231
+ // ── which profile for a model ──
232
+ /** Whether a model name (`fable`, `claude-fable-5-1`, …) runs on the scoped weekly window. */
233
+ function checkIsScopedWeeklyModel(model) {
234
+ return model !== null && /fable/i.test(model);
235
+ }
236
+ /** The windows that bound a launch of `model`: the session window plus the weekly one the model drains. */
237
+ function getRelevantClaudeUsageKinds(model) {
238
+ return ['session', checkIsScopedWeeklyModel(model) ? 'weekly_scoped' : 'weekly_all'];
239
+ }
240
+ /**
241
+ * @description Rank the profiles for a launch of `model`: a profile that is signed out, marked limited
242
+ * (`limitedUntil` in the future) or at 100 % on a relevant window is out; the rest are ordered by headroom on
243
+ * the relevant windows (most first; unknown usage ranks after any known one; a tie goes to the lower total of
244
+ * the relevant percentages). The first in that order is `recommended`. The entries keep the input order.
245
+ */
246
+ function buildClaudeProfilesReport(profiles, model, nowMs) {
247
+ const relevantBuckets = getRelevantClaudeUsageKinds(model);
248
+ const entries = profiles.map((profile) => {
249
+ const relevant = relevantBuckets
250
+ .map((kind) => getClaudeUsageBucket(profile.usage, kind))
251
+ .filter((bucket) => bucket !== null);
252
+ const maxPercent = relevant.length === 0 ? null : Math.max(...relevant.map((bucket) => bucket.percent));
253
+ const isLimited = profile.limitedUntil !== null && Date.parse(profile.limitedUntil) > nowMs;
254
+ const isEligible = profile.loggedIn !== false && !isLimited && (maxPercent === null || maxPercent < 100);
255
+ return {
256
+ name: profile.name,
257
+ configDir: profile.configDir,
258
+ email: profile.email,
259
+ kind: (0, claudeProfiles_1.getClaudeAccountKind)(profile.subscriptionType),
260
+ subscriptionType: profile.subscriptionType,
261
+ loggedIn: profile.loggedIn ?? null,
262
+ buckets: profile.usage?.buckets ?? [],
263
+ usageFetchedAt: profile.usage?.fetchedAt ?? null,
264
+ isUsageStale: profile.usage?.isStale === true,
265
+ limitedUntil: profile.limitedUntil,
266
+ isEligible,
267
+ headroomPercent: maxPercent === null ? null : 100 - maxPercent,
268
+ isRecommended: false,
269
+ };
270
+ });
271
+ const ranked = entries
272
+ .filter((entry) => entry.isEligible)
273
+ .sort((a, b) => {
274
+ if (a.headroomPercent === null || b.headroomPercent === null) {
275
+ return (a.headroomPercent === null ? 1 : 0) - (b.headroomPercent === null ? 1 : 0);
276
+ }
277
+ if (a.headroomPercent !== b.headroomPercent)
278
+ return b.headroomPercent - a.headroomPercent;
279
+ return sumRelevantPercent(a, relevantBuckets) - sumRelevantPercent(b, relevantBuckets);
280
+ });
281
+ const recommended = ranked[0] ?? null;
282
+ if (recommended)
283
+ recommended.isRecommended = true;
284
+ return { model, relevantBuckets, recommended: recommended?.name ?? null, profiles: entries };
285
+ }
286
+ function sumRelevantPercent(entry, kinds) {
287
+ return entry.buckets.filter((bucket) => kinds.includes(bucket.kind)).reduce((sum, bucket) => sum + bucket.percent, 0);
288
+ }
289
+ /**
290
+ * @description The ranking's view of every profile off the registry: `default` (its hidden record, when the
291
+ * bot has written one) first, then the added profiles in creation order. Shared by the bot (live state) and
292
+ * the CLI (`state.json` read directly).
293
+ */
294
+ function buildClaudeProfileUsageViews(input) {
295
+ // A `default` the operator signed out through /accounts stays out of every ranking until it is signed in again.
296
+ const isDefaultRemoved = input.records.default?.isRemoved === true;
297
+ const toView = (name, record) => ({
298
+ name,
299
+ configDir: (0, claudeProfiles_1.resolveClaudeProfileDir)(input.dataDir, name),
300
+ loggedIn: record?.loggedIn,
301
+ email: record?.email ?? null,
302
+ subscriptionType: record?.subscriptionType ?? null,
303
+ usage: record?.usage,
304
+ limitedUntil: record?.limitedUntil ?? null,
305
+ });
306
+ const added = Object.entries(input.records)
307
+ .filter(([name]) => name !== 'default')
308
+ .sort(([, a], [, b]) => a.createdAt.localeCompare(b.createdAt));
309
+ return [...(isDefaultRemoved ? [] : [toView('default', input.records.default)]), ...added.map(([name, record]) => toView(name, record))];
310
+ }
311
+ /** The profile registry as `state.json` holds it — the CLI's read, no `StateStore` (which would migrate and lock). */
312
+ function readClaudeProfileRecordsFromStateFile(dataDir) {
313
+ let parsed;
314
+ try {
315
+ parsed = JSON.parse(fs.readFileSync(path.join(dataDir, 'state.json'), 'utf8'));
316
+ }
317
+ catch {
318
+ return {};
319
+ }
320
+ const records = checkIsRecord(parsed) && checkIsRecord(parsed.claudeProfiles) ? parsed.claudeProfiles : {};
321
+ const result = {};
322
+ for (const [name, record] of Object.entries(records)) {
323
+ if (checkIsRecord(record) && typeof record.createdAt === 'string')
324
+ result[name] = record;
325
+ }
326
+ return result;
327
+ }
328
+ // ── rendering ──
329
+ /**
330
+ * @description A reset instant for a human, in the instance zone: `16:00` when it is today there, `Sat 00:00`
331
+ * within the week, `17.10 00:00` beyond. The weekday is English on purpose (a three-letter token every locale
332
+ * reads; the surrounding line is localised).
333
+ */
334
+ function formatClaudeUsageResetAt(resetsAt, timezone, nowMs) {
335
+ if (resetsAt === null)
336
+ return '?';
337
+ const exact = Date.parse(resetsAt);
338
+ if (Number.isNaN(exact))
339
+ return '?';
340
+ // Windows reset on a minute boundary; the endpoint's instant carries jitter below a second (`13:00:00.40`,
341
+ // once `12:59:59.9`), which must not read as a minute earlier.
342
+ const at = Math.round(exact / 60_000) * 60_000;
343
+ const read = (options, instant) => new Intl.DateTimeFormat('en-GB', { timeZone: timezone, hourCycle: 'h23', ...options }).format(instant);
344
+ const time = read({ hour: '2-digit', minute: '2-digit' }, at);
345
+ const dayOf = (instant) => read({ year: 'numeric', month: '2-digit', day: '2-digit' }, instant);
346
+ if (dayOf(at) === dayOf(nowMs))
347
+ return time;
348
+ if (at - nowMs < 6 * 24 * 3_600_000)
349
+ return `${read({ weekday: 'short' }, at)} ${time}`;
350
+ return `${read({ day: '2-digit' }, at)}.${read({ month: '2-digit' }, at)} ${time}`;
351
+ }
352
+ /** English words, the CLI's (the bot passes its localised ones). */
353
+ exports.englishRelativeResetWords = { pattern: '🔄 in {time}', day: 'd', hour: 'h', minute: 'm' };
354
+ /**
355
+ * @description A reset instant as a time from now: `🔄 in 2d 1h`, `🔄 in 2h`, `🔄 in 30m`. The two largest
356
+ * units at most, minutes only under an hour: from a day up it is days + hours (hours dropped when zero), from
357
+ * an hour up whole hours (rounded), below that minutes (at least one). An instant already past reads as
358
+ * `🔄 in 1m` (the window is about to roll over). `?` for a missing or unparseable instant.
359
+ */
360
+ function formatClaudeUsageRelativeReset(resetsAt, nowMs, words = exports.englishRelativeResetWords) {
361
+ if (resetsAt === null)
362
+ return '?';
363
+ const at = Date.parse(resetsAt);
364
+ if (Number.isNaN(at))
365
+ return '?';
366
+ const totalMinutes = Math.max(1, Math.round((at - nowMs) / 60_000));
367
+ let time;
368
+ if (totalMinutes < 60) {
369
+ time = `${totalMinutes}${words.minute}`;
370
+ }
371
+ else if (totalMinutes < 24 * 60) {
372
+ // 23h40m rounds to 24h: that is a day.
373
+ time = totalMinutes / 60 >= 23.5 ? `1${words.day}` : `${Math.round(totalMinutes / 60)}${words.hour}`;
374
+ }
375
+ else {
376
+ let days = Math.floor(totalMinutes / 1440);
377
+ let hours = Math.round((totalMinutes % 1440) / 60);
378
+ if (hours === 24) {
379
+ days += 1;
380
+ hours = 0;
381
+ }
382
+ time = hours === 0 ? `${days}${words.day}` : `${days}${words.day} ${hours}${words.hour}`;
383
+ }
384
+ return words.pattern.replace('{time}', time);
385
+ }
386
+ /**
387
+ * @description `81% 🔄 in 2h` for a bucket, `—` for a missing one. A bucket whose reset is unknown shows
388
+ * `fallbackResetsAt` instead (the session window's reset is often missing while the weekly one is known);
389
+ * with neither it is the bare percent — a «?» after a number would read as a doubt about the number.
390
+ */
391
+ function formatClaudeUsageBucket(bucket, nowMs, options = {}) {
392
+ if (bucket === null)
393
+ return '—';
394
+ const known = bucket.resetsAt !== null && !Number.isNaN(Date.parse(bucket.resetsAt));
395
+ const resetsAt = known ? bucket.resetsAt : options.fallbackResetsAt ?? null;
396
+ if (resetsAt === null || Number.isNaN(Date.parse(resetsAt)))
397
+ return `${bucket.percent}%`;
398
+ return `${bucket.percent}% ${formatClaudeUsageRelativeReset(resetsAt, nowMs, options.words)}`;
399
+ }
400
+ // ── small readers ──
401
+ function checkIsRecord(value) {
402
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
403
+ }
404
+ function readIso(value) {
405
+ if (typeof value !== 'string')
406
+ return null;
407
+ const ms = Date.parse(value);
408
+ return Number.isNaN(ms) ? null : new Date(ms).toISOString();
409
+ }
410
+ function clampPercent(value) {
411
+ return Math.max(0, Math.min(100, Math.round(value)));
412
+ }
413
+ //# sourceMappingURL=claudeUsage.js.map