@herbertgao/pi-extensions 2026.9.9 → 2026.9.11

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 (170) hide show
  1. package/README.md +1 -0
  2. package/THIRD_PARTY_NOTICES.md +26 -0
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
  7. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +12 -0
  8. package/node_modules/@herbertgao/pi-subagents/README.md +427 -120
  9. package/node_modules/@herbertgao/pi-subagents/docs/rpc.md +184 -0
  10. package/node_modules/@herbertgao/pi-subagents/docs/workflows.md +466 -0
  11. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +6 -6
  12. package/node_modules/@herbertgao/pi-subagents/examples/workflows/compose.js +52 -0
  13. package/node_modules/@herbertgao/pi-subagents/examples/workflows/fan-out-audit.js +56 -0
  14. package/node_modules/@herbertgao/pi-subagents/examples/workflows/gated-fix.js +60 -0
  15. package/node_modules/@herbertgao/pi-subagents/examples/workflows/lib/count-child.js +30 -0
  16. package/node_modules/@herbertgao/pi-subagents/examples/workflows/review-panel.js +68 -0
  17. package/node_modules/@herbertgao/pi-subagents/examples/workflows/structured-findings.js +81 -0
  18. package/node_modules/@herbertgao/pi-subagents/package.json +12 -10
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +52 -12
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +837 -146
  21. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +213 -39
  22. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +73 -14
  23. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +101 -47
  24. package/node_modules/@herbertgao/pi-subagents/src/index.ts +2249 -914
  25. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +13 -0
  26. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +215 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/mention.ts +147 -0
  28. package/node_modules/@herbertgao/pi-subagents/src/model-resolver.ts +9 -1
  29. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +40 -26
  30. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +18 -8
  31. package/node_modules/@herbertgao/pi-subagents/src/prompts.ts +46 -9
  32. package/node_modules/@herbertgao/pi-subagents/src/schedule.ts +21 -16
  33. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +137 -7
  34. package/node_modules/@herbertgao/pi-subagents/src/structured-output.ts +136 -0
  35. package/node_modules/@herbertgao/pi-subagents/src/types.ts +126 -8
  36. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-mention.ts +274 -0
  37. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +20 -5
  38. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +10 -4
  39. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +167 -22
  40. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-card.ts +555 -0
  41. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-dialog.ts +1304 -0
  42. package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-menu.ts +226 -0
  43. package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +122 -0
  44. package/node_modules/@herbertgao/pi-subagents/src/workflow/entry.ts +47 -0
  45. package/node_modules/@herbertgao/pi-subagents/src/workflow/host.ts +463 -0
  46. package/node_modules/@herbertgao/pi-subagents/src/workflow/journal.ts +164 -0
  47. package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +142 -0
  48. package/node_modules/@herbertgao/pi-subagents/src/workflow/meta.ts +401 -0
  49. package/node_modules/@herbertgao/pi-subagents/src/workflow/progress.ts +622 -0
  50. package/node_modules/@herbertgao/pi-subagents/src/workflow/runtime.ts +1399 -0
  51. package/node_modules/@herbertgao/pi-subagents/src/workflow/saved.ts +230 -0
  52. package/node_modules/@herbertgao/pi-subagents/src/workflow/task.ts +333 -0
  53. package/node_modules/@herbertgao/pi-subagents/src/workflow/tool-description.ts +200 -0
  54. package/node_modules/@herbertgao/pi-subagents/src/workflow/worker-source.ts +781 -0
  55. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +97 -95
  56. package/node_modules/@herbertgao/pi-subagents/src/xml.ts +13 -0
  57. package/node_modules/@herbertgao/resume-from/package.json +1 -1
  58. package/node_modules/@herbertgao/sol-pi/README.md +3 -3
  59. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
  60. package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
  61. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
  62. package/node_modules/@herbertgao/sol-pi/package.json +2 -2
  63. package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
  64. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  65. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  66. package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
  67. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
  68. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
  69. package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
  70. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
  71. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
  72. package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
  73. package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
  74. package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
  75. package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
  76. package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
  77. package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
  78. package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
  79. package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
  80. package/node_modules/pi-multi-account/LICENSE +21 -0
  81. package/node_modules/pi-multi-account/README.md +197 -0
  82. package/node_modules/pi-multi-account/SECURITY.md +27 -0
  83. package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
  84. package/node_modules/pi-multi-account/child-usability.ts +233 -0
  85. package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
  86. package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
  87. package/node_modules/pi-multi-account/context-guard.ts +420 -0
  88. package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
  89. package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
  90. package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
  91. package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
  92. package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
  93. package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
  94. package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
  95. package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
  96. package/node_modules/pi-multi-account/cursor/index.ts +572 -0
  97. package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
  98. package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
  99. package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
  100. package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
  101. package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
  102. package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
  103. package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
  104. package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
  105. package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
  106. package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
  107. package/node_modules/pi-multi-account/index.ts +11825 -0
  108. package/node_modules/pi-multi-account/model-catalog.ts +354 -0
  109. package/node_modules/pi-multi-account/package.json +101 -0
  110. package/node_modules/pi-multi-account/pi-contract.ts +281 -0
  111. package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
  112. package/node_modules/pi-multi-account/provider-priority.ts +189 -0
  113. package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
  114. package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
  115. package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
  116. package/node_modules/pi-multi-account/usage.ts +1099 -0
  117. package/node_modules/pi-typesafe/README.md +6 -2
  118. package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
  119. package/node_modules/pi-typesafe/dist/client.js +45 -10
  120. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  121. package/node_modules/pi-typesafe/dist/index.js +1 -1
  122. package/node_modules/pi-typesafe/package.json +2 -2
  123. package/node_modules/pi-web-access/CHANGELOG.md +36 -0
  124. package/node_modules/pi-web-access/README.md +75 -18
  125. package/node_modules/pi-web-access/anysearch.ts +4 -15
  126. package/node_modules/pi-web-access/bocha.ts +3 -22
  127. package/node_modules/pi-web-access/brave.ts +3 -21
  128. package/node_modules/pi-web-access/brightdata.ts +5 -32
  129. package/node_modules/pi-web-access/content-find.ts +168 -53
  130. package/node_modules/pi-web-access/curator-page.ts +4 -1
  131. package/node_modules/pi-web-access/curator-run.ts +2 -1
  132. package/node_modules/pi-web-access/curator-server.ts +1 -0
  133. package/node_modules/pi-web-access/dist/index.js +24620 -0
  134. package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
  135. package/node_modules/pi-web-access/duckduckgo.ts +3 -21
  136. package/node_modules/pi-web-access/extract.ts +3 -1
  137. package/node_modules/pi-web-access/firecrawl.ts +5 -29
  138. package/node_modules/pi-web-access/gemini-search.ts +81 -32
  139. package/node_modules/pi-web-access/index.ts +149 -148
  140. package/node_modules/pi-web-access/jina-search.ts +4 -15
  141. package/node_modules/pi-web-access/kagi.ts +4 -13
  142. package/node_modules/pi-web-access/kimi-search.ts +5 -30
  143. package/node_modules/pi-web-access/mistral-search.ts +1 -15
  144. package/node_modules/pi-web-access/ollama.ts +2 -7
  145. package/node_modules/pi-web-access/openai-search.ts +174 -36
  146. package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
  147. package/node_modules/pi-web-access/package.json +10 -4
  148. package/node_modules/pi-web-access/page-query.ts +10 -2
  149. package/node_modules/pi-web-access/parallel.ts +1 -15
  150. package/node_modules/pi-web-access/pdf-extract.ts +3 -0
  151. package/node_modules/pi-web-access/querit.ts +5 -29
  152. package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
  153. package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
  154. package/node_modules/pi-web-access/search1api.ts +5 -29
  155. package/node_modules/pi-web-access/searchinfinity.ts +5 -29
  156. package/node_modules/pi-web-access/searxng.ts +3 -21
  157. package/node_modules/pi-web-access/serpapi.ts +5 -28
  158. package/node_modules/pi-web-access/serpbase.ts +3 -22
  159. package/node_modules/pi-web-access/serpdive.ts +3 -21
  160. package/node_modules/pi-web-access/serper.ts +5 -28
  161. package/node_modules/pi-web-access/serply.ts +197 -0
  162. package/node_modules/pi-web-access/source-check.ts +11 -47
  163. package/node_modules/pi-web-access/summary-review.ts +7 -3
  164. package/node_modules/pi-web-access/tavily.ts +3 -21
  165. package/node_modules/pi-web-access/tinyfish.ts +5 -29
  166. package/node_modules/pi-web-access/utils.ts +9 -1
  167. package/node_modules/pi-web-access/valyu.ts +5 -28
  168. package/node_modules/pi-web-access/xai-search.ts +1 -15
  169. package/node_modules/pi-web-access/xcrawl.ts +5 -32
  170. package/package.json +17 -11
@@ -0,0 +1,1099 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export type UsageFamily = "codex" | "anthropic" | "ollama" | "cursor" | "qwen" | "kimi-coding" | "xai" | "zai-coding-cn";
6
+
7
+ export type UsageWindow = {
8
+ usedPercent: number;
9
+ resetAt: number;
10
+ windowSeconds?: number;
11
+ };
12
+
13
+ export type UsageSnapshot = {
14
+ provider: string;
15
+ family: UsageFamily;
16
+ fetchedAt: number;
17
+ credentialHash?: string;
18
+ plan?: string;
19
+ /**
20
+ * Which real account this is — the email the provider reports, when it reports one.
21
+ *
22
+ * Slot ids (`openai-codex-account-5`) are positions in a config file, not identities. With
23
+ * several slots the position says nothing about whose quota is being spent, which is the fact
24
+ * a person actually needs when deciding where to switch.
25
+ */
26
+ account?: string;
27
+ /**
28
+ * The provider's OWN verdict on whether this account can be used right now.
29
+ *
30
+ * Everything else in this snapshot is arithmetic we do on quota windows — a forecast about
31
+ * one window, which cannot see session limits, plan limits or an early reset. This field is
32
+ * not that: it is the account answering the question directly. `undefined` means the response
33
+ * stated no verdict, and only then is the forecast the best information available.
34
+ */
35
+ serviceable?: boolean;
36
+ primary?: UsageWindow;
37
+ secondary?: UsageWindow;
38
+ /**
39
+ * A third, separately-metered bucket a provider exposes BESIDE the two rotation windows.
40
+ *
41
+ * Cursor is the reason it exists: one Pro+ subscription bills three independent pools —
42
+ * included models (`primary`), the third-party "other models" pool (`secondary`) and the
43
+ * Grok Bot product, which meters on its own weekly reset. Unlike primary/secondary it does
44
+ * not gate the account: a maxed extra bucket still leaves the other pools able to serve
45
+ * work, so it is displayed but never treated as a rotation cooldown.
46
+ */
47
+ tertiary?: UsageWindow;
48
+ credits?: {
49
+ hasCredits?: boolean;
50
+ unlimited?: boolean;
51
+ balance?: string;
52
+ };
53
+ };
54
+
55
+ export type UsageCredential = {
56
+ type?: string;
57
+ access?: string;
58
+ accountId?: string;
59
+ key?: string;
60
+ expires?: number;
61
+ };
62
+
63
+ export class UsageFetchError extends Error {
64
+ readonly status?: number;
65
+
66
+ constructor(message: string, status?: number) {
67
+ super(message);
68
+ this.name = "UsageFetchError";
69
+ this.status = status;
70
+ }
71
+ }
72
+
73
+ function record(value: unknown): Record<string, any> {
74
+ return value && typeof value === "object" ? (value as Record<string, any>) : {};
75
+ }
76
+
77
+ function finiteNumber(value: unknown): number | undefined {
78
+ if (typeof value !== "number" && (typeof value !== "string" || !value.trim())) return undefined;
79
+ const number = typeof value === "number" ? value : Number(value);
80
+ return Number.isFinite(number) ? number : undefined;
81
+ }
82
+
83
+ function percent(value: unknown): number | undefined {
84
+ const number = finiteNumber(value);
85
+ return number === undefined ? undefined : Math.min(100, Math.max(0, number));
86
+ }
87
+
88
+ function epochMs(value: unknown): number | undefined {
89
+ if (typeof value === "string" && value.trim() && !Number.isFinite(Number(value))) {
90
+ const parsed = Date.parse(value);
91
+ return Number.isFinite(parsed) ? parsed : undefined;
92
+ }
93
+ const number = finiteNumber(value);
94
+ if (number === undefined || number <= 0) return undefined;
95
+ return number < 10_000_000_000 ? number * 1000 : number;
96
+ }
97
+
98
+ function usageWindow(value: unknown, fallbackWindowSeconds?: number): UsageWindow | undefined {
99
+ const source = record(value);
100
+ const usedPercent = percent(source.used_percent ?? source.utilization);
101
+ const resetAt = epochMs(source.reset_at ?? source.resets_at);
102
+ if (usedPercent === undefined || resetAt === undefined) return undefined;
103
+ const windowSeconds = finiteNumber(source.limit_window_seconds) ?? fallbackWindowSeconds;
104
+ return {
105
+ usedPercent,
106
+ resetAt,
107
+ ...(windowSeconds !== undefined ? { windowSeconds } : {}),
108
+ };
109
+ }
110
+
111
+ export function usageFamily(provider: string): UsageFamily | undefined {
112
+ if (provider === "openai-codex" || /^openai-codex-account-\d+$/.test(provider)) return "codex";
113
+ if (provider === "anthropic" || /^anthropic-account-\d+$/.test(provider)) return "anthropic";
114
+ if (provider === "ollama" || /^ollama-account-\d+$/.test(provider)) return "ollama";
115
+ if (provider === "cursor" || /^cursor-account-\d+$/.test(provider)) return "cursor";
116
+ if (provider === "alibaba" || /^alibaba-account-\d+$/.test(provider) || /^qwen/i.test(provider)) return "qwen";
117
+ if (provider === "kimi-coding" || /^kimi-coding-account-\d+$/.test(provider)) return "kimi-coding";
118
+ if (provider === "xai" || /^xai-account-\d+$/.test(provider)) return "xai";
119
+ if (provider === "zai-coding-cn" || /^zai-coding-cn-account-\d+$/.test(provider)) return "zai-coding-cn";
120
+ return undefined;
121
+ }
122
+
123
+ export function parseCodexUsageBody(
124
+ provider: string,
125
+ body: unknown,
126
+ fetchedAt = Date.now(),
127
+ credentialHash?: string,
128
+ ): UsageSnapshot | undefined {
129
+ const source = record(body);
130
+ const rateLimit = record(source.rate_limit);
131
+ const primary = usageWindow(rateLimit.primary_window, 5 * 60 * 60);
132
+ const secondary = usageWindow(rateLimit.secondary_window, 7 * 24 * 60 * 60);
133
+ if (!primary && !secondary) return undefined;
134
+ const credits = record(source.credits);
135
+ return {
136
+ provider,
137
+ family: "codex",
138
+ fetchedAt,
139
+ credentialHash,
140
+ plan: typeof source.plan_type === "string" ? source.plan_type : undefined,
141
+ account: typeof source.email === "string" && source.email.trim() ? source.email : undefined,
142
+ // `limit_reached` is the negative statement and `allowed` the positive one; either alone
143
+ // is enough. Read both so a response that carries only one of them still answers.
144
+ serviceable:
145
+ typeof rateLimit.limit_reached === "boolean"
146
+ ? !rateLimit.limit_reached
147
+ : typeof rateLimit.allowed === "boolean"
148
+ ? rateLimit.allowed
149
+ : undefined,
150
+ primary,
151
+ secondary,
152
+ credits: {
153
+ hasCredits: typeof credits.has_credits === "boolean" ? credits.has_credits : undefined,
154
+ unlimited: typeof credits.unlimited === "boolean" ? credits.unlimited : undefined,
155
+ balance:
156
+ typeof credits.balance === "string" || typeof credits.balance === "number"
157
+ ? String(credits.balance)
158
+ : undefined,
159
+ },
160
+ };
161
+ }
162
+
163
+ export function parseAnthropicUsageBody(
164
+ provider: string,
165
+ body: unknown,
166
+ fetchedAt = Date.now(),
167
+ credentialHash?: string,
168
+ ): UsageSnapshot | undefined {
169
+ const source = record(body);
170
+ const primary = usageWindow(source.five_hour, 5 * 60 * 60);
171
+ const secondary = usageWindow(source.seven_day, 7 * 24 * 60 * 60);
172
+ if (!primary && !secondary) return undefined;
173
+ return {
174
+ provider,
175
+ family: "anthropic",
176
+ fetchedAt,
177
+ credentialHash,
178
+ primary,
179
+ secondary,
180
+ };
181
+ }
182
+
183
+ function headerValue(headers: unknown, name: string): string | undefined {
184
+ const getter = (headers as any)?.get;
185
+ if (typeof getter === "function") {
186
+ const value = getter.call(headers, name);
187
+ return typeof value === "string" ? value : undefined;
188
+ }
189
+ for (const [key, value] of Object.entries(record(headers))) {
190
+ if (key.toLowerCase() === name.toLowerCase() && value !== undefined) return String(value);
191
+ }
192
+ return undefined;
193
+ }
194
+
195
+ function headerWindow(headers: unknown, prefix: "primary" | "secondary"): UsageWindow | undefined {
196
+ const usedPercent = percent(headerValue(headers, `x-codex-${prefix}-used-percent`));
197
+ const resetAt = epochMs(headerValue(headers, `x-codex-${prefix}-reset-at`));
198
+ const windowMinutes = finiteNumber(headerValue(headers, `x-codex-${prefix}-window-minutes`));
199
+ if (usedPercent === undefined || resetAt === undefined) return undefined;
200
+ return {
201
+ usedPercent,
202
+ resetAt,
203
+ ...(windowMinutes !== undefined ? { windowSeconds: windowMinutes * 60 } : {}),
204
+ };
205
+ }
206
+
207
+ export function parseCodexUsageHeaders(
208
+ provider: string,
209
+ headers: unknown,
210
+ fetchedAt = Date.now(),
211
+ credentialHash?: string,
212
+ ): UsageSnapshot | undefined {
213
+ const primary = headerWindow(headers, "primary");
214
+ const secondary = headerWindow(headers, "secondary");
215
+ if (!primary && !secondary) return undefined;
216
+ return {
217
+ provider,
218
+ family: "codex",
219
+ fetchedAt,
220
+ credentialHash,
221
+ plan: headerValue(headers, "x-codex-plan-type"),
222
+ primary,
223
+ secondary,
224
+ credits: {
225
+ hasCredits: headerValue(headers, "x-codex-credits-has-credits")?.toLowerCase() === "true",
226
+ unlimited: headerValue(headers, "x-codex-credits-unlimited")?.toLowerCase() === "true",
227
+ balance: headerValue(headers, "x-codex-credits-balance"),
228
+ },
229
+ };
230
+ }
231
+
232
+ export function parseOllamaMeBody(
233
+ provider: string,
234
+ body: unknown,
235
+ fetchedAt = Date.now(),
236
+ credentialHash?: string,
237
+ ): UsageSnapshot {
238
+ const source = record(body);
239
+ const planName =
240
+ typeof source.Plan === "string"
241
+ ? source.Plan
242
+ : typeof source.plan === "string"
243
+ ? source.plan
244
+ : undefined;
245
+ // Ollama's /api/me carries the plan tier, billing-period end and suspended flag. Current cloud
246
+ // quota windows come from /api/usage, but retain support for windows here in case Ollama folds
247
+ // them into the documented account response later.
248
+ const nullableTime = (value: unknown): string | undefined => {
249
+ if (!value || typeof value !== "object") return undefined;
250
+ const v = value as { Time?: unknown; Valid?: unknown };
251
+ return v.Valid === true && typeof v.Time === "string" ? v.Time : undefined;
252
+ };
253
+ const planParts: string[] = [];
254
+ if (planName) planParts.push(planName);
255
+ if (nullableTime(source.SuspendedAt)) planParts.push("SUSPENDED");
256
+ const periodEnd = nullableTime(source.SubscriptionPeriodEnd);
257
+ if (periodEnd) {
258
+ const d = new Date(periodEnd);
259
+ if (!Number.isNaN(d.getTime()))
260
+ planParts.push(`renews ${d.toISOString().slice(0, 10)}`);
261
+ }
262
+ const plan = planParts.length > 0 ? planParts.join(" · ") : planName;
263
+ const sessionSource =
264
+ source.session ??
265
+ source.Session ??
266
+ source.session_usage ??
267
+ source.SessionUsage;
268
+ const weeklySource =
269
+ source.weekly ??
270
+ source.Weekly ??
271
+ source.weekly_usage ??
272
+ source.WeeklyUsage;
273
+ const primary = usageWindow(sessionSource, 5 * 60 * 60);
274
+ const secondary = usageWindow(weeklySource, 7 * 24 * 60 * 60);
275
+ return {
276
+ provider,
277
+ family: "ollama",
278
+ fetchedAt,
279
+ credentialHash,
280
+ plan,
281
+ primary,
282
+ secondary,
283
+ };
284
+ }
285
+
286
+ const OLLAMA_SESSION_SECONDS = 5 * 60 * 60;
287
+ const OLLAMA_WEEK_SECONDS = 7 * 24 * 60 * 60;
288
+ const OLLAMA_WEEK_ANCHOR_MS = 4 * 24 * 60 * 60_000; // Monday 00:00 UTC after Unix epoch.
289
+
290
+ function nextBoundary(now: number, windowMs: number, anchorMs = 0): number {
291
+ return anchorMs + (Math.floor((now - anchorMs) / windowMs) + 1) * windowMs;
292
+ }
293
+
294
+ function ollamaFractionWindow(
295
+ value: unknown,
296
+ fetchedAt: number,
297
+ windowSeconds: number,
298
+ anchorMs = 0,
299
+ ): UsageWindow | undefined {
300
+ const rawUsage = record(value).usage;
301
+ if (rawUsage === null || rawUsage === undefined || rawUsage === "") return undefined;
302
+ const usage = finiteNumber(rawUsage);
303
+ // Ollama documents this only through its live response today. Be strict about the observed
304
+ // fractional shape so a future percentage-valued response cannot silently turn 50% into 100%.
305
+ if (usage === undefined || usage < 0 || usage > 1) return undefined;
306
+ return {
307
+ usedPercent: usage * 100,
308
+ resetAt: nextBoundary(fetchedAt, windowSeconds * 1000, anchorMs),
309
+ windowSeconds,
310
+ };
311
+ }
312
+
313
+ /** Parse Ollama Cloud's best-effort /api/usage session and weekly quota fractions. */
314
+ export function parseOllamaUsageBody(
315
+ provider: string,
316
+ body: unknown,
317
+ fetchedAt = Date.now(),
318
+ credentialHash?: string,
319
+ ): UsageSnapshot | undefined {
320
+ const limits = record(record(body).limits);
321
+ const primary = ollamaFractionWindow(
322
+ limits.session,
323
+ fetchedAt,
324
+ OLLAMA_SESSION_SECONDS,
325
+ );
326
+ const secondary = ollamaFractionWindow(
327
+ limits.weekly,
328
+ fetchedAt,
329
+ OLLAMA_WEEK_SECONDS,
330
+ OLLAMA_WEEK_ANCHOR_MS,
331
+ );
332
+ if (!primary && !secondary) return undefined;
333
+ return {
334
+ provider,
335
+ family: "ollama",
336
+ fetchedAt,
337
+ credentialHash,
338
+ primary,
339
+ secondary,
340
+ };
341
+ }
342
+
343
+ async function fetchOllamaUsageSnapshot(
344
+ provider: string,
345
+ credential: UsageCredential,
346
+ options: {
347
+ fetchImpl?: typeof fetch;
348
+ timeoutMs?: number;
349
+ credentialHash?: string;
350
+ } = {},
351
+ ): Promise<UsageSnapshot> {
352
+ if (credential.type !== "api_key" || !credential.key) {
353
+ throw new UsageFetchError(`${provider} has no API key`);
354
+ }
355
+ const controller = new AbortController();
356
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
357
+ const fetchImpl = options.fetchImpl ?? fetch;
358
+ const headers = {
359
+ Authorization: `Bearer ${credential.key}`,
360
+ Accept: "application/json",
361
+ "Content-Type": "application/json",
362
+ };
363
+ try {
364
+ let response = await fetchImpl("https://ollama.com/api/me", {
365
+ method: "POST",
366
+ headers,
367
+ body: "{}",
368
+ signal: controller.signal,
369
+ });
370
+ if (!response.ok) {
371
+ response = await fetchImpl("http://127.0.0.1:11434/api/me", {
372
+ method: "POST",
373
+ headers,
374
+ body: "{}",
375
+ signal: controller.signal,
376
+ });
377
+ }
378
+ if (!response.ok) {
379
+ throw new UsageFetchError(
380
+ `${provider} Ollama account check returned HTTP ${response.status}`,
381
+ response.status,
382
+ );
383
+ }
384
+ const body = await response.json();
385
+ const account = parseOllamaMeBody(
386
+ provider,
387
+ body,
388
+ Date.now(),
389
+ options.credentialHash,
390
+ );
391
+ // /api/usage is not yet a stable documented contract. Its failure must never erase the
392
+ // useful /api/me account status or make a healthy Ollama key look invalid.
393
+ try {
394
+ const usageResponse = await fetchImpl("https://ollama.com/api/usage", {
395
+ method: "GET",
396
+ headers,
397
+ signal: controller.signal,
398
+ });
399
+ if (usageResponse.ok) {
400
+ const usage = parseOllamaUsageBody(
401
+ provider,
402
+ await usageResponse.json(),
403
+ Date.now(),
404
+ options.credentialHash,
405
+ );
406
+ if (usage) {
407
+ return {
408
+ ...account,
409
+ fetchedAt: usage.fetchedAt,
410
+ primary: usage.primary ?? account.primary,
411
+ secondary: usage.secondary ?? account.secondary,
412
+ };
413
+ }
414
+ }
415
+ } catch {
416
+ // Best-effort endpoint: preserve the plan-only account snapshot.
417
+ }
418
+ return account;
419
+ } catch (error) {
420
+ if (error instanceof UsageFetchError) throw error;
421
+ if ((error as any)?.name === "AbortError") {
422
+ throw new UsageFetchError(`${provider} Ollama usage request timed out`);
423
+ }
424
+ throw new UsageFetchError(
425
+ `${provider} Ollama usage request failed: ${error instanceof Error ? error.message : String(error)}`,
426
+ );
427
+ } finally {
428
+ clearTimeout(timer);
429
+ }
430
+ }
431
+
432
+ // Cursor's own dashboard usage lives in the aiserver.v1.DashboardService Connect-RPC service.
433
+ // The methods are undocumented; they were read out of Cursor's own clients (the editor and the
434
+ // Grok Bot app) and verified live on 2026-09-12 — the same OAuth session token the bridge already
435
+ // holds answers both, and the numbers match the dashboard exactly (97% / 35% / weekly 0%).
436
+ const CURSOR_DASHBOARD_RPC_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService";
437
+
438
+ /** The monthly pools: included models (`auto`) and third-party models (`api`). */
439
+ export function parseCursorCurrentPeriodUsage(
440
+ provider: string,
441
+ body: unknown,
442
+ fetchedAt = Date.now(),
443
+ credentialHash?: string,
444
+ ): UsageSnapshot | undefined {
445
+ const source = record(body);
446
+ const planUsage = record(source.planUsage ?? source.plan_usage);
447
+ const cycleStart = epochMs(source.billingCycleStart ?? source.billing_cycle_start);
448
+ const cycleEnd = epochMs(source.billingCycleEnd ?? source.billing_cycle_end);
449
+ const windowSeconds =
450
+ cycleStart !== undefined && cycleEnd !== undefined && cycleEnd > cycleStart
451
+ ? Math.round((cycleEnd - cycleStart) / 1000)
452
+ : undefined;
453
+ const periodWindow = (value: unknown): UsageWindow | undefined => {
454
+ const usedPercent = percent(value);
455
+ if (usedPercent === undefined || cycleEnd === undefined) return undefined;
456
+ return {
457
+ usedPercent,
458
+ resetAt: cycleEnd,
459
+ ...(windowSeconds !== undefined ? { windowSeconds } : {}),
460
+ };
461
+ };
462
+ const primary = periodWindow(planUsage.autoPercentUsed ?? planUsage.auto_percent_used);
463
+ const secondary = periodWindow(planUsage.apiPercentUsed ?? planUsage.api_percent_used);
464
+ if (!primary && !secondary) return undefined;
465
+ return {
466
+ provider,
467
+ family: "cursor",
468
+ fetchedAt,
469
+ credentialHash,
470
+ primary,
471
+ secondary,
472
+ };
473
+ }
474
+
475
+ /** The Grok Bot weekly bucket, from the same dashboard service. */
476
+ export function parseCursorSandUsage(body: unknown): UsageWindow | undefined {
477
+ const source = record(body);
478
+ const usedPercent = percent(source.usagePercent ?? source.usage_percent);
479
+ const resetAt = epochMs(source.nextResetTimestampUtc ?? source.next_reset_timestamp_utc);
480
+ const startAt = epochMs(source.currentPeriodStart ?? source.current_period_start);
481
+ if (usedPercent === undefined || resetAt === undefined) return undefined;
482
+ const windowSeconds =
483
+ startAt !== undefined && resetAt > startAt
484
+ ? Math.round((resetAt - startAt) / 1000)
485
+ : undefined;
486
+ return {
487
+ usedPercent,
488
+ resetAt,
489
+ ...(windowSeconds !== undefined ? { windowSeconds } : {}),
490
+ };
491
+ }
492
+
493
+ async function fetchCursorUsageSnapshot(
494
+ provider: string,
495
+ credential: UsageCredential,
496
+ options: {
497
+ fetchImpl?: typeof fetch;
498
+ timeoutMs?: number;
499
+ credentialHash?: string;
500
+ } = {},
501
+ ): Promise<UsageSnapshot> {
502
+ if (credential.type !== "oauth" || !credential.access) {
503
+ throw new UsageFetchError(`${provider} has no OAuth access token`);
504
+ }
505
+ const fetchedAt = Date.now();
506
+ // The plan-only snapshot is the old, always-honest answer. Quota numbers are an enrichment
507
+ // on top of it: a network hiccup must not blank the footer or report a healthy account as
508
+ // "usage unavailable", so every failure except 401 falls back to it.
509
+ const fallback = (): UsageSnapshot => ({
510
+ provider,
511
+ family: "cursor",
512
+ fetchedAt,
513
+ credentialHash: options.credentialHash,
514
+ plan: "subscription",
515
+ });
516
+ const controller = new AbortController();
517
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
518
+ const fetchImpl = options.fetchImpl ?? fetch;
519
+ const call = async (method: string): Promise<Response> =>
520
+ fetchImpl(`${CURSOR_DASHBOARD_RPC_URL}/${method}`, {
521
+ method: "POST",
522
+ headers: {
523
+ Authorization: `Bearer ${credential.access}`,
524
+ Accept: "application/json",
525
+ "Content-Type": "application/json",
526
+ "Connect-Protocol-Version": "1",
527
+ },
528
+ body: "{}",
529
+ signal: controller.signal,
530
+ });
531
+ try {
532
+ const [period, sand] = await Promise.allSettled([
533
+ call("GetCurrentPeriodUsage"),
534
+ call("GetSandUsageStatus"),
535
+ ]);
536
+ // A 401 is the one failure worth surfacing: the caller refreshes the token and retries.
537
+ for (const result of [period, sand]) {
538
+ if (result.status === "fulfilled" && result.value.status === 401) {
539
+ throw new UsageFetchError(
540
+ `${provider} Cursor usage endpoint returned HTTP 401`,
541
+ 401,
542
+ );
543
+ }
544
+ }
545
+ const bodyOf = async (result: PromiseSettledResult<Response>): Promise<unknown> => {
546
+ if (result.status !== "fulfilled" || !result.value.ok) return undefined;
547
+ try {
548
+ return await result.value.json();
549
+ } catch {
550
+ return undefined;
551
+ }
552
+ };
553
+ const periodBody = await bodyOf(period);
554
+ const sandBody = await bodyOf(sand);
555
+ const periodSnapshot =
556
+ periodBody !== undefined
557
+ ? parseCursorCurrentPeriodUsage(provider, periodBody, fetchedAt, options.credentialHash)
558
+ : undefined;
559
+ const sandWindow = sandBody !== undefined ? parseCursorSandUsage(sandBody) : undefined;
560
+ if (!periodSnapshot && !sandWindow) return fallback();
561
+ const planName = record(sandBody).cursorPlanName;
562
+ return {
563
+ provider,
564
+ family: "cursor",
565
+ fetchedAt,
566
+ credentialHash: options.credentialHash,
567
+ plan: typeof planName === "string" && planName ? planName : "subscription",
568
+ primary: periodSnapshot?.primary,
569
+ secondary: periodSnapshot?.secondary,
570
+ tertiary: sandWindow,
571
+ };
572
+ } catch (error) {
573
+ if (error instanceof UsageFetchError) throw error;
574
+ return fallback();
575
+ } finally {
576
+ clearTimeout(timer);
577
+ }
578
+ }
579
+
580
+ /** SuperGrok / X Premium OAuth billing probe. Not Cursor Grok and not XAI_API_KEY. */
581
+ export const XAI_SUBSCRIPTION_USAGE_URL =
582
+ "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
583
+
584
+ function decodeJwtPayload(token: string): Record<string, any> | undefined {
585
+ const parts = token.split(".");
586
+ if (parts.length !== 3) return undefined;
587
+ try {
588
+ let payload = parts[1].replaceAll("-", "+").replaceAll("_", "/");
589
+ payload += "=".repeat((4 - (payload.length % 4)) % 4);
590
+ const parsed = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
591
+ return parsed && typeof parsed === "object" ? (parsed as Record<string, any>) : undefined;
592
+ } catch {
593
+ return undefined;
594
+ }
595
+ }
596
+
597
+ function jwtClaimString(payload: Record<string, any>, key: string): string | undefined {
598
+ const value = payload[key];
599
+ if (typeof value !== "string") return undefined;
600
+ const trimmed = value.trim();
601
+ return trimmed ? trimmed : undefined;
602
+ }
603
+
604
+ /**
605
+ * xAI billing requires the authenticated user id in `x-userid`.
606
+ * Read it from the access-token JWT (`sub`, then `principal_id`). Never invent it
607
+ * and never treat a generic OAuth `accountId` as an xAI user id.
608
+ */
609
+ export function xaiUserIdFromAccessToken(token: string): string | undefined {
610
+ const payload = decodeJwtPayload(token);
611
+ if (!payload) return undefined;
612
+ return jwtClaimString(payload, "sub") ?? jwtClaimString(payload, "principal_id");
613
+ }
614
+
615
+ let cachedXaiClientVersion: string | undefined;
616
+
617
+ /** Truthful host id. Never impersonate an official Grok CLI version. */
618
+ export function xaiHostClientVersion(): string {
619
+ if (cachedXaiClientVersion) return cachedXaiClientVersion;
620
+ try {
621
+ const pkg = JSON.parse(
622
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), "package.json"), "utf8"),
623
+ ) as { name?: unknown; version?: unknown };
624
+ const name =
625
+ typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : "pi-multi-account";
626
+ const version =
627
+ typeof pkg.version === "string" && pkg.version.trim() ? pkg.version.trim() : undefined;
628
+ cachedXaiClientVersion = version ? `${name}/${version}` : name;
629
+ } catch {
630
+ cachedXaiClientVersion = "pi-multi-account";
631
+ }
632
+ return cachedXaiClientVersion;
633
+ }
634
+
635
+ function isGrokBuildProduct(name: unknown): boolean {
636
+ if (typeof name !== "string") return false;
637
+ const normalized = name.trim().toLowerCase().replace(/[_-]/g, "");
638
+ return normalized === "productgrokbuild" || normalized === "grokbuild";
639
+ }
640
+
641
+ function grokBuildUsagePercent(productUsage: unknown): number | undefined {
642
+ if (!Array.isArray(productUsage)) return undefined;
643
+ for (const item of productUsage) {
644
+ const product = record(item);
645
+ if (!isGrokBuildProduct(product.product)) continue;
646
+ const value = percent(product.usagePercent);
647
+ if (value !== undefined) return value;
648
+ }
649
+ return undefined;
650
+ }
651
+
652
+ type XaiUsagePeriod = Pick<UsageWindow, "resetAt" | "windowSeconds">;
653
+
654
+ function xaiUsagePeriod(value: unknown, fetchedAt: number): XaiUsagePeriod | undefined {
655
+ const source = record(value);
656
+ const start = epochMs(source.start);
657
+ const end = epochMs(source.end);
658
+ if (
659
+ start === undefined ||
660
+ end === undefined ||
661
+ end <= start ||
662
+ fetchedAt < start ||
663
+ fetchedAt >= end
664
+ ) {
665
+ return undefined;
666
+ }
667
+ return {
668
+ resetAt: end,
669
+ windowSeconds: Math.round((end - start) / 1000),
670
+ };
671
+ }
672
+
673
+ function xaiUsageSnapshot(
674
+ provider: string,
675
+ usedPercent: number,
676
+ period: XaiUsagePeriod,
677
+ fetchedAt: number,
678
+ credentialHash?: string,
679
+ ): UsageSnapshot {
680
+ return {
681
+ provider,
682
+ family: "xai",
683
+ fetchedAt,
684
+ credentialHash,
685
+ primary: { usedPercent, ...period },
686
+ };
687
+ }
688
+
689
+ export function parseXaiUsageBody(
690
+ provider: string,
691
+ body: unknown,
692
+ fetchedAt = Date.now(),
693
+ credentialHash?: string,
694
+ ): UsageSnapshot | undefined {
695
+ const source = record(body);
696
+ if (source.config === undefined || source.config === null) return undefined;
697
+ const config = record(source.config);
698
+ const modernPeriod = xaiUsagePeriod(config.currentPeriod, fetchedAt);
699
+ let modernUsedPercent = percent(config.creditUsagePercent);
700
+ if (modernUsedPercent === undefined) {
701
+ modernUsedPercent = grokBuildUsagePercent(config.productUsage);
702
+ }
703
+ if (modernUsedPercent !== undefined) {
704
+ return modernPeriod
705
+ ? xaiUsageSnapshot(
706
+ provider,
707
+ modernUsedPercent,
708
+ modernPeriod,
709
+ fetchedAt,
710
+ credentialHash,
711
+ )
712
+ : undefined;
713
+ }
714
+ // A period alone is not evidence of unused quota. Private endpoint schema
715
+ // drift must never manufacture headroom and clear a real cooldown.
716
+ if (config.currentPeriod !== undefined) return undefined;
717
+
718
+ const legacyPeriod = xaiUsagePeriod(
719
+ { start: config.billingPeriodStart, end: config.billingPeriodEnd },
720
+ fetchedAt,
721
+ );
722
+ if (!legacyPeriod) return undefined;
723
+ const monthlyLimit = finiteNumber(record(config.monthlyLimit).val);
724
+ const used = finiteNumber(record(config.used).val);
725
+ if (monthlyLimit === undefined || monthlyLimit <= 0 || used === undefined || used < 0) return undefined;
726
+ const legacyUsedPercent = percent((used / monthlyLimit) * 100);
727
+ return legacyUsedPercent === undefined
728
+ ? undefined
729
+ : xaiUsageSnapshot(
730
+ provider,
731
+ legacyUsedPercent,
732
+ legacyPeriod,
733
+ fetchedAt,
734
+ credentialHash,
735
+ );
736
+ }
737
+
738
+ async function fetchXaiUsageSnapshot(
739
+ provider: string,
740
+ credential: UsageCredential,
741
+ options: {
742
+ fetchImpl?: typeof fetch;
743
+ timeoutMs?: number;
744
+ credentialHash?: string;
745
+ } = {},
746
+ ): Promise<UsageSnapshot> {
747
+ if (credential.type !== "oauth" || !credential.access) {
748
+ // XAI_API_KEY shares the `xai` provider id. It has no SuperGrok billing session,
749
+ // so do not fall through to a doomed OAuth probe that blanks the footer.
750
+ return {
751
+ provider,
752
+ family: "xai",
753
+ fetchedAt: Date.now(),
754
+ credentialHash: options.credentialHash,
755
+ plan: "api-key · no usage endpoint",
756
+ };
757
+ }
758
+ const userId = xaiUserIdFromAccessToken(credential.access);
759
+ if (!userId) {
760
+ throw new UsageFetchError(`${provider} xAI access token has no user id`);
761
+ }
762
+ const controller = new AbortController();
763
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
764
+ try {
765
+ const response = await (options.fetchImpl ?? fetch)(XAI_SUBSCRIPTION_USAGE_URL, {
766
+ method: "GET",
767
+ headers: {
768
+ Authorization: `Bearer ${credential.access}`,
769
+ Accept: "application/json",
770
+ "X-XAI-Token-Auth": "xai-grok-cli",
771
+ "x-userid": userId,
772
+ "x-grok-client-version": xaiHostClientVersion(),
773
+ "x-grok-client-mode": "headless",
774
+ },
775
+ signal: controller.signal,
776
+ });
777
+ if (!response.ok) {
778
+ throw new UsageFetchError(
779
+ `${provider} usage endpoint returned HTTP ${response.status}`,
780
+ response.status,
781
+ );
782
+ }
783
+ const snapshot = parseXaiUsageBody(
784
+ provider,
785
+ await response.json(),
786
+ Date.now(),
787
+ options.credentialHash,
788
+ );
789
+ if (!snapshot) {
790
+ throw new UsageFetchError(`${provider} usage endpoint returned no quota window`);
791
+ }
792
+ return snapshot;
793
+ } catch (error) {
794
+ if (error instanceof UsageFetchError) throw error;
795
+ if ((error as any)?.name === "AbortError") {
796
+ throw new UsageFetchError(`${provider} usage request timed out`);
797
+ }
798
+ throw new UsageFetchError(
799
+ `${provider} usage request failed: ${error instanceof Error ? error.message : String(error)}`,
800
+ );
801
+ } finally {
802
+ clearTimeout(timer);
803
+ }
804
+ }
805
+
806
+ export const ZAI_CODING_CN_USAGE_URL = "https://open.bigmodel.cn/api/monitor/usage/quota/limit";
807
+
808
+ /** Only provider-reported model credit windows; MCP counts are not model quota. */
809
+ export function parseZaiCodingCnUsageBody(
810
+ provider: string, body: unknown, fetchedAt = Date.now(), credentialHash?: string,
811
+ ): UsageSnapshot | undefined {
812
+ const source = record(body);
813
+ if (source.success === false || (source.code !== undefined && ![0, 200, "0", "200"].includes(source.code))) return undefined;
814
+ const data = record(source.data);
815
+ if (!Array.isArray(data.limits)) return undefined;
816
+ const snapshot: UsageSnapshot = { provider, family: "zai-coding-cn", fetchedAt, credentialHash };
817
+ if (typeof data.level === "string" && data.level.trim()) snapshot.plan = data.level.trim();
818
+ for (const value of data.limits) {
819
+ const item = record(value);
820
+ if (item.type !== "CREDIT_LIMIT" && item.type !== "TOKENS_LIMIT") continue;
821
+ const unit = finiteNumber(item.unit);
822
+ const count = finiteNumber(item.number);
823
+ const seconds = unit === 3 && count === 5 ? 5 * 3600 : unit === 6 && count === 1 ? 7 * 86400 : undefined;
824
+ const usedPercent = percent(item.percentage);
825
+ const resetAt = epochMs(item.nextResetTime);
826
+ if (!seconds || usedPercent === undefined || resetAt === undefined || resetAt <= fetchedAt) continue;
827
+ const key = seconds === 5 * 3600 ? "primary" : "secondary";
828
+ // Duplicate windows cannot make a depleted account appear healthier.
829
+ if (!snapshot[key] || usedPercent > snapshot[key]!.usedPercent)
830
+ snapshot[key] = { usedPercent, resetAt, windowSeconds: seconds };
831
+ }
832
+ return snapshot.primary || snapshot.secondary ? snapshot : undefined;
833
+ }
834
+
835
+ async function fetchZaiCodingCnUsageSnapshot(
836
+ provider: string, credential: UsageCredential,
837
+ options: { fetchImpl?: typeof fetch; timeoutMs?: number; credentialHash?: string },
838
+ ): Promise<UsageSnapshot> {
839
+ if (credential.type !== "api_key" || !credential.key) throw new UsageFetchError(`${provider} has no Coding Plan API key`);
840
+ try {
841
+ const response = await (options.fetchImpl ?? fetch)(ZAI_CODING_CN_USAGE_URL, {
842
+ headers: { Authorization: credential.key, Accept: "application/json" },
843
+ redirect: "error",
844
+ signal: AbortSignal.timeout(options.timeoutMs ?? 10_000),
845
+ });
846
+ if (!response.ok) throw new UsageFetchError(`${provider} usage endpoint returned HTTP ${response.status}`, response.status);
847
+ const snapshot = parseZaiCodingCnUsageBody(provider, await response.json(), Date.now(), options.credentialHash);
848
+ if (!snapshot) throw new UsageFetchError(`${provider} usage endpoint returned no current quota window`);
849
+ return snapshot;
850
+ } catch (error) {
851
+ if (error instanceof UsageFetchError) throw error;
852
+ // Never echo provider bodies or fetch errors that could embed a credential.
853
+ throw new UsageFetchError(`${provider} usage request failed or timed out`);
854
+ }
855
+ }
856
+
857
+ export async function fetchUsageSnapshot(
858
+ provider: string,
859
+ credential: UsageCredential,
860
+ options: {
861
+ fetchImpl?: typeof fetch;
862
+ timeoutMs?: number;
863
+ credentialHash?: string;
864
+ } = {},
865
+ ): Promise<UsageSnapshot> {
866
+ const family = usageFamily(provider);
867
+ if (!family) throw new UsageFetchError(`Usage is not supported for ${provider}`);
868
+
869
+ if (family === "ollama") {
870
+ return fetchOllamaUsageSnapshot(provider, credential, options);
871
+ }
872
+ if (family === "cursor") {
873
+ return fetchCursorUsageSnapshot(provider, credential, options);
874
+ }
875
+ if (family === "kimi-coding") {
876
+ // Kimi For Coding is a subscription behind an API key, and it publishes no quota endpoint —
877
+ // /usage, /quota, /me, /subscription and the Moonshot balance path all 404 against
878
+ // api.kimi.com/coding. Falling through to the OAuth branch made every probe throw
879
+ // "has no OAuth access token" for a healthy key, blanking the footer and filling the log.
880
+ return {
881
+ provider,
882
+ family: "kimi-coding",
883
+ fetchedAt: Date.now(),
884
+ credentialHash: options.credentialHash,
885
+ plan: "subscription · no usage endpoint",
886
+ };
887
+ }
888
+ if (family === "qwen") {
889
+ // Qwen/Alibaba exposes no usage/quota endpoint over its API-key plans, so we
890
+ // report the plan honestly instead of attempting (and failing) an OAuth usage
891
+ // fetch. Keeps `limits` from throwing "not supported".
892
+ return {
893
+ provider,
894
+ family: "qwen",
895
+ fetchedAt: Date.now(),
896
+ credentialHash: options.credentialHash,
897
+ plan: "api-key · no usage endpoint",
898
+ };
899
+ }
900
+ if (family === "xai") return fetchXaiUsageSnapshot(provider, credential, options);
901
+ if (family === "zai-coding-cn") return fetchZaiCodingCnUsageSnapshot(provider, credential, options);
902
+ if (credential.type !== "oauth" || !credential.access) {
903
+ throw new UsageFetchError(`${provider} has no OAuth access token`);
904
+ }
905
+
906
+ const controller = new AbortController();
907
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
908
+ const headers: Record<string, string> = {
909
+ Authorization: `Bearer ${credential.access}`,
910
+ Accept: "application/json",
911
+ };
912
+ let url: string;
913
+ if (family === "codex") {
914
+ url = "https://chatgpt.com/backend-api/wham/usage";
915
+ if (credential.accountId) headers["ChatGPT-Account-Id"] = credential.accountId;
916
+ } else {
917
+ url = "https://api.anthropic.com/api/oauth/usage";
918
+ headers["anthropic-beta"] = "oauth-2025-04-20";
919
+ }
920
+
921
+ try {
922
+ const response = await (options.fetchImpl ?? fetch)(url, {
923
+ method: "GET",
924
+ headers,
925
+ signal: controller.signal,
926
+ });
927
+ if (!response.ok) {
928
+ throw new UsageFetchError(`${provider} usage endpoint returned HTTP ${response.status}`, response.status);
929
+ }
930
+ const body = await response.json();
931
+ const snapshot =
932
+ family === "codex"
933
+ ? parseCodexUsageBody(provider, body, Date.now(), options.credentialHash)
934
+ : parseAnthropicUsageBody(provider, body, Date.now(), options.credentialHash);
935
+ if (!snapshot) throw new UsageFetchError(`${provider} usage endpoint returned no 5h/7d windows`);
936
+ return snapshot;
937
+ } catch (error) {
938
+ if (error instanceof UsageFetchError) throw error;
939
+ if ((error as any)?.name === "AbortError") throw new UsageFetchError(`${provider} usage request timed out`);
940
+ throw new UsageFetchError(`${provider} usage request failed: ${error instanceof Error ? error.message : String(error)}`);
941
+ } finally {
942
+ clearTimeout(timer);
943
+ }
944
+ }
945
+
946
+ export function providerUsageLabel(provider: string): string {
947
+ const index = provider.match(/-account-(\d+)$/)?.[1];
948
+ if (provider.startsWith("openai-codex")) return index ? `Codex A${index}` : "Codex";
949
+ if (provider.startsWith("anthropic")) return index ? `Claude A${index}` : "Claude";
950
+ if (provider.startsWith("ollama")) return index ? `Ollama A${index}` : "Ollama";
951
+ if (provider.startsWith("cursor")) return index ? `Cursor A${index}` : "Cursor";
952
+ if (provider.startsWith("kimi-coding")) return index ? `Kimi A${index}` : "Kimi";
953
+ if (provider.startsWith("xai")) return index ? `xAI A${index}` : "xAI";
954
+ if (provider.startsWith("zai-coding-cn")) return index ? `GLM CN A${index}` : "GLM CN";
955
+ if (provider.startsWith("alibaba") || /^qwen/i.test(provider)) return index ? `Qwen A${index}` : "Qwen/Alibaba";
956
+ return provider;
957
+ }
958
+
959
+ export function remainingPercent(window: UsageWindow): number {
960
+ return Math.max(0, Math.round(100 - window.usedPercent));
961
+ }
962
+
963
+ export function formatResetDuration(resetAt: number, now = Date.now()): string {
964
+ const minutes = Math.max(0, Math.ceil((resetAt - now) / 60_000));
965
+ if (minutes < 60) return `${minutes}m`;
966
+ const hours = Math.floor(minutes / 60);
967
+ const restMinutes = minutes % 60;
968
+ if (hours < 24) return restMinutes ? `${hours}h${restMinutes}m` : `${hours}h`;
969
+ const days = Math.floor(hours / 24);
970
+ const restHours = hours % 24;
971
+ return restHours ? `${days}d${restHours}h` : `${days}d`;
972
+ }
973
+
974
+ /** Keep an email readable in a one-line footer without letting it dominate the line. */
975
+ export function shortAccount(account: string | undefined): string | undefined {
976
+ if (!account) return undefined;
977
+ const local = account.includes("@") ? account.slice(0, account.indexOf("@")) : account;
978
+ return local.length > 18 ? `${local.slice(0, 17)}…` : local;
979
+ }
980
+
981
+ /** Where a window sits in the snapshot: the two rotation windows, or an extra metered pool. */
982
+ export type UsageWindowPosition = "primary" | "secondary" | "tertiary";
983
+
984
+ /**
985
+ * Name a quota window by how long it actually is.
986
+ *
987
+ * The label used to be positional — whatever sat in the "primary" slot was called `5h` — but a
988
+ * Codex free plan meters a THIRTY-DAY window there. A number that resets next month then read as
989
+ * one resetting this afternoon, which is a materially different decision about whether to wait.
990
+ */
991
+ export function windowLabel(
992
+ window: UsageWindow,
993
+ family: UsageFamily,
994
+ position: UsageWindowPosition,
995
+ ): string {
996
+ // Cursor's pools are products, not durations: included models, the third-party pool, and the
997
+ // Grok Bot weekly bucket.
998
+ if (family === "cursor") {
999
+ if (position === "primary") return "models";
1000
+ if (position === "secondary") return "other";
1001
+ return "grok bot";
1002
+ }
1003
+ if (family === "ollama") return position === "primary" ? "session" : "weekly";
1004
+ const seconds = window.windowSeconds;
1005
+ if (!seconds) return position === "primary" ? "5h" : position === "secondary" ? "7d" : "usage";
1006
+ if (seconds >= 20 * 86_400) return "30d";
1007
+ if (seconds >= 6 * 86_400) return "7d";
1008
+ if (seconds >= 20 * 3_600) return "24h";
1009
+ return `${Math.max(1, Math.round(seconds / 3_600))}h`;
1010
+ }
1011
+
1012
+ export function formatUsageCompact(snapshot: UsageSnapshot, now = Date.now()): string {
1013
+ const who = shortAccount(snapshot.account);
1014
+ const parts = [
1015
+ who
1016
+ ? `${providerUsageLabel(snapshot.provider)} · ${who}`
1017
+ : providerUsageLabel(snapshot.provider),
1018
+ ];
1019
+ // The plan is what decides how much quota those percentages are a percentage OF — a free slot
1020
+ // at 60% left and a Plus slot at 60% left are not comparable amounts of work.
1021
+ if (snapshot.plan && (snapshot.primary || snapshot.secondary || snapshot.tertiary)) parts.push(snapshot.plan);
1022
+ // The account's own answer, when it gave one. A percentage is arithmetic on one window and can
1023
+ // disagree with reality in both directions — an account reading 0% left was answering
1024
+ // `allowed: true`, and showing only the 0% is what makes a working account look dead.
1025
+ if (snapshot.serviceable === true) parts.push("ok");
1026
+ else if (snapshot.serviceable === false) parts.push("spent");
1027
+ if (snapshot.primary) {
1028
+ parts.push(
1029
+ `${windowLabel(snapshot.primary, snapshot.family, "primary")} ${remainingPercent(snapshot.primary)}% left/${formatResetDuration(snapshot.primary.resetAt, now)}`,
1030
+ );
1031
+ }
1032
+ if (snapshot.secondary) {
1033
+ parts.push(
1034
+ `${windowLabel(snapshot.secondary, snapshot.family, "secondary")} ${remainingPercent(snapshot.secondary)}% left/${formatResetDuration(snapshot.secondary.resetAt, now)}`,
1035
+ );
1036
+ }
1037
+ if (snapshot.tertiary) {
1038
+ parts.push(
1039
+ `${windowLabel(snapshot.tertiary, snapshot.family, "tertiary")} ${remainingPercent(snapshot.tertiary)}% left/${formatResetDuration(snapshot.tertiary.resetAt, now)}`,
1040
+ );
1041
+ }
1042
+ if (!snapshot.primary && !snapshot.secondary && !snapshot.tertiary && snapshot.plan) {
1043
+ if (snapshot.family === "ollama") {
1044
+ parts.push(`${snapshot.plan} · quota unavailable`);
1045
+ } else {
1046
+ parts.push(snapshot.plan);
1047
+ }
1048
+ }
1049
+ return parts.join(" | ");
1050
+ }
1051
+
1052
+ export function formatUsageDetails(snapshot: UsageSnapshot, now = Date.now()): string {
1053
+ const lines = [
1054
+ `Limits for ${providerUsageLabel(snapshot.provider)}${snapshot.account ? ` — ${snapshot.account}` : ""}${snapshot.plan ? ` (${snapshot.plan})` : ""}`,
1055
+ ];
1056
+ if (snapshot.serviceable !== undefined)
1057
+ lines.push(
1058
+ snapshot.serviceable
1059
+ ? "The account reports it can be used right now."
1060
+ : "The account reports it is currently blocked, whatever the percentages below say.",
1061
+ );
1062
+ if (!snapshot.primary && !snapshot.secondary && !snapshot.tertiary && snapshot.plan) {
1063
+ if (snapshot.family === "ollama") {
1064
+ lines.push(
1065
+ `Plan: ${snapshot.plan}. Session/weekly quota is currently unavailable — check https://ollama.com/settings`,
1066
+ );
1067
+ } else {
1068
+ lines.push(`Status: ${snapshot.plan}`);
1069
+ }
1070
+ }
1071
+ for (const [position, window] of [
1072
+ ["primary", snapshot.primary],
1073
+ ["secondary", snapshot.secondary],
1074
+ ["tertiary", snapshot.tertiary],
1075
+ ] as const) {
1076
+ if (!window) continue;
1077
+ const label =
1078
+ snapshot.family === "ollama" && position === "primary"
1079
+ ? "session"
1080
+ : windowLabel(window, snapshot.family, position);
1081
+ lines.push(
1082
+ `${label}: ${remainingPercent(window)}% left (${Math.round(window.usedPercent)}% used), resets in ${formatResetDuration(window.resetAt, now)} at ${new Date(window.resetAt).toLocaleString()}`,
1083
+ );
1084
+ }
1085
+ if (snapshot.credits?.unlimited) lines.push("Credits: unlimited");
1086
+ else if (snapshot.credits?.balance !== undefined) lines.push(`Credits: ${snapshot.credits.balance}`);
1087
+ lines.push(`Updated ${formatResetDuration(now, snapshot.fetchedAt)} ago`);
1088
+ return lines.join("\n");
1089
+ }
1090
+
1091
+ export function usageColor(snapshot: UsageSnapshot): "success" | "warning" | "error" {
1092
+ const remaining = [snapshot.primary, snapshot.secondary]
1093
+ .filter((window): window is UsageWindow => !!window)
1094
+ .map(remainingPercent);
1095
+ const lowest = remaining.length > 0 ? Math.min(...remaining) : 100;
1096
+ if (lowest <= 10) return "error";
1097
+ if (lowest <= 30) return "warning";
1098
+ return "success";
1099
+ }