@oar-sh/oar 0.9.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/.github/extensions/web-relay/README.md +190 -0
  2. package/.github/extensions/web-relay/extension.mjs +972 -0
  3. package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
  4. package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
  5. package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
  6. package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
  7. package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
  8. package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
  9. package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
  10. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
  11. package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
  12. package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
  13. package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
  14. package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
  15. package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
  16. package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
  17. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
  18. package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
  19. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
  20. package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
  21. package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
  22. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
  23. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
  24. package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
  25. package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
  26. package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
  27. package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
  28. package/CHANGELOG.md +25 -0
  29. package/LICENSE +21 -0
  30. package/README.md +717 -0
  31. package/bin/bat/oar.bat +46 -0
  32. package/bin/oar.js +665 -0
  33. package/package.json +68 -0
  34. package/server/README.md +1379 -0
  35. package/server/claude-worker/claude-attachments.mjs +134 -0
  36. package/server/claude-worker/claude-preview-tool.mjs +57 -0
  37. package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
  38. package/server/claude-worker/claude-session-process.mjs +2391 -0
  39. package/server/claude-worker/claude-session-test-harness.mjs +236 -0
  40. package/server/claude-worker/claude-session-worker.mjs +186 -0
  41. package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
  42. package/server/claude-worker/claude-turn-publisher.mjs +291 -0
  43. package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
  44. package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
  45. package/server/claude-worker/workflow-progress-digest.mjs +269 -0
  46. package/server/copilot-sdk-runtime.mjs +126 -0
  47. package/server/copilot-worker/copilot-attachments.mjs +97 -0
  48. package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
  49. package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
  50. package/server/copilot-worker/copilot-plan-board.mjs +110 -0
  51. package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
  52. package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
  53. package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
  54. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
  55. package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
  56. package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
  57. package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
  58. package/server/copilot-worker/fixtures/abort-turn.json +588 -0
  59. package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
  60. package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
  61. package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
  62. package/server/copilot-worker/fixtures/happy-turn.json +566 -0
  63. package/server/copilot-worker/fixtures/quota-turn.json +647 -0
  64. package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
  65. package/server/copilot-worker/fixtures/resume-turn.json +700 -0
  66. package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
  67. package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
  68. package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
  69. package/server/cursor-worker/cursor-attachments.mjs +88 -0
  70. package/server/cursor-worker/cursor-context-usage.mjs +85 -0
  71. package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
  72. package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
  73. package/server/cursor-worker/cursor-session-worker.mjs +145 -0
  74. package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
  75. package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
  76. package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
  77. package/server/db-schema.mjs +932 -0
  78. package/server/features.mjs +106 -0
  79. package/server/grok-worker/acp-client.mjs +434 -0
  80. package/server/grok-worker/acp-host-services.mjs +277 -0
  81. package/server/grok-worker/grok-context-usage.mjs +77 -0
  82. package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
  83. package/server/grok-worker/grok-session-worker.mjs +145 -0
  84. package/server/grok-worker/grok-turn-runner.mjs +523 -0
  85. package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
  86. package/server/migrations/0001-add-structured-answer.mjs +80 -0
  87. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
  88. package/server/migrations/0003-push-subscriptions.mjs +87 -0
  89. package/server/public/app/action-confirmations.js +267 -0
  90. package/server/public/app/activity-replay-state.mjs +149 -0
  91. package/server/public/app/api-client.js +1082 -0
  92. package/server/public/app/ask-user-view.js +545 -0
  93. package/server/public/app/attachments-view.js +2112 -0
  94. package/server/public/app/auto-compact-window-options.mjs +59 -0
  95. package/server/public/app/background-tasks-view.mjs +494 -0
  96. package/server/public/app/bootstrap.js +4347 -0
  97. package/server/public/app/claude-auth-ui.js +394 -0
  98. package/server/public/app/claude-thinking-options.mjs +38 -0
  99. package/server/public/app/cli-install-ui.js +455 -0
  100. package/server/public/app/code-copy.mjs +85 -0
  101. package/server/public/app/composer-attachment-cache.mjs +151 -0
  102. package/server/public/app/composer-control-state.mjs +107 -0
  103. package/server/public/app/composer-paste.mjs +132 -0
  104. package/server/public/app/context-tier-options.mjs +68 -0
  105. package/server/public/app/context-usage-view.mjs +338 -0
  106. package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
  107. package/server/public/app/conversation-preferences.mjs +91 -0
  108. package/server/public/app/conversation-provider-indicator.mjs +88 -0
  109. package/server/public/app/conversation-view.js +2898 -0
  110. package/server/public/app/cwd-menu-placement.mjs +85 -0
  111. package/server/public/app/cwd-picker.js +605 -0
  112. package/server/public/app/emoji-view.js +101 -0
  113. package/server/public/app/external-link-policy.mjs +80 -0
  114. package/server/public/app/file-preview-navigation.mjs +51 -0
  115. package/server/public/app/font-scaling.js +262 -0
  116. package/server/public/app/git-changes-view.js +339 -0
  117. package/server/public/app/git-diff-model.mjs +109 -0
  118. package/server/public/app/grok-auth-ui.js +387 -0
  119. package/server/public/app/image-downscale.mjs +174 -0
  120. package/server/public/app/in-flight-snapshot.mjs +40 -0
  121. package/server/public/app/infinite-loader.js +164 -0
  122. package/server/public/app/journal-view.js +1141 -0
  123. package/server/public/app/known-cwd-options.mjs +41 -0
  124. package/server/public/app/launch-cwd-path.mjs +23 -0
  125. package/server/public/app/live-message-dedupe.mjs +57 -0
  126. package/server/public/app/message-search-view.js +242 -0
  127. package/server/public/app/model-selector-options.mjs +31 -0
  128. package/server/public/app/model-vision-support.mjs +56 -0
  129. package/server/public/app/new-conversation-model-choice.mjs +67 -0
  130. package/server/public/app/plan-usage-view.mjs +307 -0
  131. package/server/public/app/preview-cards.mjs +273 -0
  132. package/server/public/app/preview-command.mjs +103 -0
  133. package/server/public/app/push-settings.js +370 -0
  134. package/server/public/app/pwa-install.js +456 -0
  135. package/server/public/app/question-schema-view.mjs +71 -0
  136. package/server/public/app/reasoning-effort-labels.mjs +42 -0
  137. package/server/public/app/relay-board-view.js +117 -0
  138. package/server/public/app/relay-dot-state.mjs +69 -0
  139. package/server/public/app/relay-error-ctas.mjs +52 -0
  140. package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
  141. package/server/public/app/relay-thoughts.mjs +98 -0
  142. package/server/public/app/relay-watchdog-policy.mjs +29 -0
  143. package/server/public/app/repo-browser-preferences.mjs +79 -0
  144. package/server/public/app/repo-browser-tree-state.mjs +97 -0
  145. package/server/public/app/router.js +657 -0
  146. package/server/public/app/selection-guard.mjs +93 -0
  147. package/server/public/app/settings-modal.js +1484 -0
  148. package/server/public/app/settings-tabs.js +129 -0
  149. package/server/public/app/slash-autocomplete.mjs +188 -0
  150. package/server/public/app/slash-commands.mjs +183 -0
  151. package/server/public/app/socket-handlers.js +703 -0
  152. package/server/public/app/status-store.mjs +242 -0
  153. package/server/public/app/status-view.mjs +315 -0
  154. package/server/public/app/store.js +1454 -0
  155. package/server/public/app/stream-state.mjs +73 -0
  156. package/server/public/app/streaming-dom-patch.mjs +23 -0
  157. package/server/public/app/sync-outbox.mjs +208 -0
  158. package/server/public/app/tap-actions.js +62 -0
  159. package/server/public/app/thread-order.mjs +54 -0
  160. package/server/public/app/tmux-inspector-view.js +422 -0
  161. package/server/public/app/transcript-separators.mjs +282 -0
  162. package/server/public/app-icon-192.png +0 -0
  163. package/server/public/app-icon-512.png +0 -0
  164. package/server/public/app-icon.svg +4 -0
  165. package/server/public/favicon.ico +0 -0
  166. package/server/public/index.html +5743 -0
  167. package/server/public/manifest.webmanifest +32 -0
  168. package/server/public/sw.js +307 -0
  169. package/server/relay-exit-codes.mjs +4 -0
  170. package/server/relay-self-restart.mjs +170 -0
  171. package/server/relay-tools.md +27 -0
  172. package/server/relay.mjs +1026 -0
  173. package/server/repositories/image-conversation-repository.mjs +317 -0
  174. package/server/repositories/message-repository.mjs +434 -0
  175. package/server/repositories/question-repository.mjs +161 -0
  176. package/server/repositories/session-repository.mjs +320 -0
  177. package/server/repositories/share-visibility-statements.mjs +24 -0
  178. package/server/routes/ask-user-routes.mjs +292 -0
  179. package/server/routes/cache-routes.mjs +44 -0
  180. package/server/routes/git-routes.mjs +92 -0
  181. package/server/routes/messages-routes-test-harness.mjs +111 -0
  182. package/server/routes/messages-routes.mjs +7107 -0
  183. package/server/routes/preview-routes.mjs +108 -0
  184. package/server/routes/push-routes.mjs +59 -0
  185. package/server/routes/relay-board-routes.mjs +268 -0
  186. package/server/routes/sessions-routes.mjs +6150 -0
  187. package/server/server-runtime.mjs +7463 -0
  188. package/server/server.js +24 -0
  189. package/server/services/ask-user-routing-service.mjs +117 -0
  190. package/server/services/cache-rebuild-service.mjs +236 -0
  191. package/server/services/claude-auth-service.mjs +578 -0
  192. package/server/services/claude-context-usage.mjs +240 -0
  193. package/server/services/claude-session-root-service.mjs +228 -0
  194. package/server/services/cli-install-service.mjs +1152 -0
  195. package/server/services/cli-process-runner.mjs +181 -0
  196. package/server/services/cloudflared-tunnel-service.mjs +358 -0
  197. package/server/services/context-snapshot-service.mjs +499 -0
  198. package/server/services/context-usage-view.mjs +160 -0
  199. package/server/services/conversation-preferences-service.mjs +208 -0
  200. package/server/services/cursor-dashboard-usage.mjs +217 -0
  201. package/server/services/cursor-session-root-service.mjs +62 -0
  202. package/server/services/delete-archive-service.mjs +154 -0
  203. package/server/services/drives-path-helpers.mjs +66 -0
  204. package/server/services/fixtures/claude-auth-stub.sh +62 -0
  205. package/server/services/fixtures/cli-install-stub.sh +75 -0
  206. package/server/services/fixtures/grok-stub.sh +86 -0
  207. package/server/services/generated-image-cleanup-service.mjs +114 -0
  208. package/server/services/git-changes-service.mjs +195 -0
  209. package/server/services/github-billing-usage.mjs +133 -0
  210. package/server/services/grok-auth-service.mjs +593 -0
  211. package/server/services/grok-billing-usage.mjs +120 -0
  212. package/server/services/image-context-service.mjs +106 -0
  213. package/server/services/image-operation-service.mjs +135 -0
  214. package/server/services/keyed-mutex-service.mjs +82 -0
  215. package/server/services/mime-sniffer.mjs +118 -0
  216. package/server/services/oar-cli-helpers.mjs +109 -0
  217. package/server/services/oar-state-migration-service.mjs +243 -0
  218. package/server/services/plan-usage-claude.mjs +387 -0
  219. package/server/services/plan-usage-contract.mjs +271 -0
  220. package/server/services/plan-usage-copilot.mjs +421 -0
  221. package/server/services/plan-usage-cursor.mjs +566 -0
  222. package/server/services/plan-usage-grok.mjs +436 -0
  223. package/server/services/plan-usage-service.mjs +487 -0
  224. package/server/services/preview-health-probe.mjs +82 -0
  225. package/server/services/preview-proxy-server.mjs +498 -0
  226. package/server/services/preview-registry-service.mjs +457 -0
  227. package/server/services/preview-static-handler.mjs +237 -0
  228. package/server/services/provider-model-selection.mjs +91 -0
  229. package/server/services/provider-reasoning-effort.mjs +94 -0
  230. package/server/services/push-active-device-service.mjs +57 -0
  231. package/server/services/push-dispatch-service.mjs +459 -0
  232. package/server/services/rate-limit-service.mjs +63 -0
  233. package/server/services/relay-bridge-owner-service.mjs +84 -0
  234. package/server/services/relay-cli-launcher-service.mjs +209 -0
  235. package/server/services/relay-prompt-sanitizer.mjs +137 -0
  236. package/server/services/relay-queue-gate-service.mjs +38 -0
  237. package/server/services/relay-restart-orchestrator-service.mjs +935 -0
  238. package/server/services/relay-singleton-guard.mjs +124 -0
  239. package/server/services/safe-served-content.mjs +57 -0
  240. package/server/services/sdk-session-import-service.mjs +271 -0
  241. package/server/services/sdk-session-sync-service.mjs +207 -0
  242. package/server/services/session-history-refresh-service.mjs +497 -0
  243. package/server/services/session-transcript-service.mjs +419 -0
  244. package/server/services/session-worker-launch-service.mjs +786 -0
  245. package/server/services/session-worker-process-service.mjs +319 -0
  246. package/server/services/session-worker-registry-service.mjs +194 -0
  247. package/server/services/session-worker-stop-service.mjs +141 -0
  248. package/server/services/session-worker-supervisor-service.mjs +898 -0
  249. package/server/services/session-worker-websocket-service.mjs +483 -0
  250. package/server/services/ssh-tunnel-manager-service.mjs +423 -0
  251. package/server/services/status-event-service.mjs +158 -0
  252. package/server/services/tmux-inspector-access-policy.mjs +77 -0
  253. package/server/services/tmux-inspector-socket-service.mjs +191 -0
  254. package/server/services/tmux-inspector-stream-service.mjs +552 -0
  255. package/server/services/tunnel-worker-path-guard.mjs +107 -0
  256. package/server/services/upload-sweep.mjs +60 -0
  257. package/server/services/usage-snapshot-helpers.mjs +209 -0
  258. package/server/services/windows-autostart-service.mjs +115 -0
  259. package/server/services/workspace-root-defaults-service.mjs +34 -0
  260. package/server/services/workspace-root-path-policy.mjs +196 -0
  261. package/server/services/workspace-root-relaunch-service.mjs +132 -0
  262. package/server/services/workspace-symlink-guard.mjs +54 -0
  263. package/server/tools/capture-proxy.mjs +173 -0
  264. package/server/tty-console-bootstrap.mjs +75 -0
  265. package/server/uploads/README.md +36 -0
  266. package/server/uploads/image_input.md +540 -0
  267. package/server/windows-terminal-launcher.mjs +29 -0
  268. package/server/workspace-root.mjs +87 -0
  269. package/shared/ask-user-bridge.mjs +131 -0
  270. package/shared/auto-compact-window.mjs +116 -0
  271. package/shared/background-task-timeout.mjs +52 -0
  272. package/shared/claude-thinking.mjs +101 -0
  273. package/shared/context-window-fallbacks.mjs +148 -0
  274. package/shared/control-poller.mjs +115 -0
  275. package/shared/empty-turn-completion.mjs +20 -0
  276. package/shared/model-catalog-freshness.mjs +11 -0
  277. package/shared/model-descriptors.mjs +118 -0
  278. package/shared/model-id.mjs +147 -0
  279. package/shared/model-refresh.mjs +24 -0
  280. package/shared/openai-reasoning.mjs +13 -0
  281. package/shared/plan-lines.mjs +13 -0
  282. package/shared/preview-instructions.mjs +77 -0
  283. package/shared/preview-tool-core.mjs +191 -0
  284. package/shared/provider-routing.mjs +29 -0
  285. package/shared/question-schema.mjs +315 -0
  286. package/shared/question-timeout.mjs +18 -0
  287. package/shared/stream-emit-gating.mjs +28 -0
  288. package/shared/subagent-run-id.mjs +17 -0
  289. package/shared/thought-cap.mjs +13 -0
  290. package/shared/tool-activity.mjs +98 -0
  291. package/shared/turn-ceiling.mjs +53 -0
  292. package/shared/worker-bootstrap.mjs +44 -0
  293. package/shared/worker-crash-guard.mjs +60 -0
@@ -0,0 +1,436 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Grok / xAI plan usage.
5
+ *
6
+ * There is no ACP plan-quota RPC (probed 2026-08-08: session/usage, x.ai/usage,
7
+ * … all Method not found). Authoritative remaining credits live only in the
8
+ * product `/usage` TUI. What the agent *does* return is rich per-prompt usage
9
+ * on the session/prompt result `_meta.usage` (tokens + costUsdTicks).
10
+ *
11
+ * So the Grok card is reconstructed like Cursor's estimated path:
12
+ * - always show the latest turn's token/cost detail when a snapshot exists;
13
+ * - optionally accumulate turn cost into a monthly cycle when the user enters
14
+ * a monthly USD allowance + reset day;
15
+ * - badge meters as estimated and link to the console for real billing.
16
+ */
17
+
18
+ import {
19
+ SOURCE_CACHE,
20
+ SOURCE_LIVE,
21
+ SOURCE_MANUAL,
22
+ SOURCE_WORKER,
23
+ STATUS_NOT_CONFIGURED,
24
+ STATUS_OK,
25
+ STATUS_PARTIAL,
26
+ STATUS_UNAVAILABLE,
27
+ buildDetailSection,
28
+ buildMeter,
29
+ buildProviderCard,
30
+ buildUnavailableCard,
31
+ pickNonNegative,
32
+ pickNumber,
33
+ roundCurrency,
34
+ toFiniteNumber,
35
+ toIsoTimestamp,
36
+ toTrimmedString,
37
+ } from './plan-usage-contract.mjs';
38
+ // Reuse Cursor's cycle calendar (same monthly reset-day semantics).
39
+ import {
40
+ DEFAULT_CURSOR_RESET_DAY,
41
+ normalizeCursorResetDay,
42
+ resolveCursorBillingCycle,
43
+ } from './plan-usage-cursor.mjs';
44
+
45
+ export const GROK_PROVIDER_ID = 'grok';
46
+ export const GROK_LABEL = 'Grok';
47
+
48
+ /** Console landing for SuperGrok / API billing (card footer link). */
49
+ export const GROK_BILLING_URL = 'https://console.x.ai';
50
+
51
+ /**
52
+ * ACP reports cost as integer ticks. Working assumption: 1 USD = 1e9 ticks
53
+ * (a live grok-4.5 turn reported costUsdTicks=148864000 for ~14k tokens,
54
+ * which reads as $0.149 at 1e9 ticks/USD).
55
+ *
56
+ * Calibration attempt 2026-08-08: exact USD verification is impossible for
57
+ * OIDC subscription logins — the account has no dollar billing at all, only
58
+ * a weekly SuperGrok percentage quota on grok.com (console.x.ai shows
59
+ * nothing). Magnitude cross-check: ~$2.45 of tick-USD booked locally lined
60
+ * up with 25% of a weekly plan quota, implying a weekly allowance in the
61
+ * $10-40-equivalent range — plausible at 1e9, implausible (<$1/week) at
62
+ * 1e10. So 1e9 is very likely right but unprovable to the cent; everything
63
+ * derived from it stays labeled "estimated".
64
+ */
65
+ export const COST_USD_TICKS_PER_USD = 1_000_000_000;
66
+
67
+ export const DEFAULT_GROK_RESET_DAY = DEFAULT_CURSOR_RESET_DAY;
68
+
69
+ export function costUsdTicksToUsd(ticks) {
70
+ const numeric = toFiniteNumber(ticks);
71
+ if (numeric === null || numeric < 0) return null;
72
+ // Full precision here: rounding per turn before the cycle ledger sums the
73
+ // values would book every sub-cent turn as $0.00 forever. Display formatting
74
+ // (formatUsd / roundCurrency) rounds at render time.
75
+ return numeric / COST_USD_TICKS_PER_USD;
76
+ }
77
+
78
+ export function normalizeGrokAllowanceSettings(raw = {}) {
79
+ const monthlyUsd = (() => {
80
+ const value = toFiniteNumber(raw?.monthlyUsd);
81
+ // A $0 allowance cannot meter anything ("$X of $0.00 used") — treat it
82
+ // as unset, same as negative or missing.
83
+ if (value === null || value <= 0) return null;
84
+ return Math.round(value * 100) / 100;
85
+ })();
86
+ return {
87
+ monthlyUsd,
88
+ resetDay: normalizeCursorResetDay(
89
+ raw?.resetDay === null || raw?.resetDay === undefined || raw?.resetDay === ''
90
+ ? DEFAULT_GROK_RESET_DAY
91
+ : raw.resetDay,
92
+ ),
93
+ };
94
+ }
95
+
96
+ export function resolveGrokBillingCycle(options = {}) {
97
+ return resolveCursorBillingCycle({
98
+ resetDay: options.resetDay ?? DEFAULT_GROK_RESET_DAY,
99
+ now: options.now,
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Normalize a single turn's usage from ACP prompt-result `_meta` (or a
105
+ * usage_update payload). Returns null when nothing usable is present.
106
+ */
107
+ export function normalizeGrokTurnUsage(raw = {}) {
108
+ if (!raw || typeof raw !== 'object') return null;
109
+
110
+ // Accept both the nested `_meta.usage` shape and a flattened worker body.
111
+ const usage = raw.usage && typeof raw.usage === 'object' ? raw.usage : raw;
112
+ const meta = raw._meta && typeof raw._meta === 'object' ? raw._meta : raw;
113
+
114
+ const inputTokens = pickNonNegative(
115
+ usage.inputTokens,
116
+ usage.input_tokens,
117
+ meta.inputTokens,
118
+ meta.input_tokens,
119
+ );
120
+ const outputTokens = pickNonNegative(
121
+ usage.outputTokens,
122
+ usage.output_tokens,
123
+ meta.outputTokens,
124
+ meta.output_tokens,
125
+ );
126
+ const totalTokens = pickNonNegative(
127
+ usage.totalTokens,
128
+ usage.total_tokens,
129
+ meta.totalTokens,
130
+ meta.total_tokens,
131
+ );
132
+ const cachedReadTokens = pickNonNegative(
133
+ usage.cachedReadTokens,
134
+ usage.cacheReadTokens,
135
+ usage.cache_read_tokens,
136
+ meta.cachedReadTokens,
137
+ );
138
+ const cacheCreationTokens = pickNonNegative(
139
+ usage.cacheCreationTokens,
140
+ usage.cacheWriteTokens,
141
+ usage.cache_creation_tokens,
142
+ meta.cacheCreationTokens,
143
+ );
144
+ const reasoningTokens = pickNonNegative(
145
+ usage.reasoningTokens,
146
+ usage.reasoning_tokens,
147
+ meta.reasoningTokens,
148
+ );
149
+ const costUsd = pickNonNegative(
150
+ usage.costUsd,
151
+ usage.cost_usd,
152
+ meta.costUsd,
153
+ ) ?? costUsdTicksToUsd(
154
+ pickNumber(usage.costUsdTicks, usage.cost_usd_ticks, meta.costUsdTicks),
155
+ );
156
+ const apiDurationMs = pickNonNegative(usage.apiDurationMs, usage.api_duration_ms, meta.apiDurationMs);
157
+ const modelCalls = pickNonNegative(usage.modelCalls, usage.model_calls);
158
+ const numTurns = pickNonNegative(usage.numTurns, usage.num_turns, meta.numTurns);
159
+ const modelId = toTrimmedString(
160
+ usage.modelId
161
+ || usage.model
162
+ || meta.modelId
163
+ || meta.model
164
+ || raw.modelId
165
+ || raw.model,
166
+ );
167
+
168
+ const modelUsageRaw = usage.modelUsage || usage.model_usage || meta.modelUsage || null;
169
+ const modelUsage = [];
170
+ if (modelUsageRaw && typeof modelUsageRaw === 'object') {
171
+ for (const [model, entry] of Object.entries(modelUsageRaw)) {
172
+ if (!entry || typeof entry !== 'object') continue;
173
+ // ACP `_meta` ships a { modelName: entry } map, but a worker re-posting
174
+ // an already-normalized blob ships an array — whose Object.entries keys
175
+ // are indices ("0"), so the entry's own model field must win there.
176
+ const modelName = toTrimmedString(Array.isArray(modelUsageRaw) ? entry.model : model);
177
+ if (!modelName) continue;
178
+ modelUsage.push({
179
+ model: modelName,
180
+ inputTokens: pickNonNegative(entry.inputTokens, entry.input_tokens),
181
+ outputTokens: pickNonNegative(entry.outputTokens, entry.output_tokens),
182
+ totalTokens: pickNonNegative(entry.totalTokens, entry.total_tokens),
183
+ cachedReadTokens: pickNonNegative(entry.cachedReadTokens, entry.cacheReadTokens),
184
+ cacheCreationTokens: pickNonNegative(entry.cacheCreationTokens, entry.cacheWriteTokens),
185
+ reasoningTokens: pickNonNegative(entry.reasoningTokens),
186
+ costUsd: pickNonNegative(entry.costUsd)
187
+ ?? costUsdTicksToUsd(pickNumber(entry.costUsdTicks, entry.cost_usd_ticks)),
188
+ modelCalls: pickNonNegative(entry.modelCalls),
189
+ apiDurationMs: pickNonNegative(entry.apiDurationMs),
190
+ });
191
+ }
192
+ }
193
+
194
+ // apiDurationMs is deliberately not a signal: a duration-only payload has
195
+ // no tokens or cost and would only inflate the cycle's turn count.
196
+ const hasSignal = [
197
+ inputTokens, outputTokens, totalTokens, cachedReadTokens, cacheCreationTokens,
198
+ reasoningTokens, costUsd,
199
+ ].some((value) => value !== null) || modelUsage.length > 0;
200
+
201
+ if (!hasSignal) return null;
202
+
203
+ return {
204
+ modelId,
205
+ inputTokens,
206
+ outputTokens,
207
+ totalTokens,
208
+ cachedReadTokens,
209
+ cacheCreationTokens,
210
+ reasoningTokens,
211
+ costUsd,
212
+ apiDurationMs,
213
+ modelCalls,
214
+ numTurns,
215
+ modelUsage,
216
+ capturedAt: toIsoTimestamp(raw.capturedAt) || new Date().toISOString(),
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Extract usage from a live ACP session/prompt result object.
222
+ */
223
+ export function extractGrokUsageFromPromptResult(promptResult = null) {
224
+ if (!promptResult || typeof promptResult !== 'object') return null;
225
+ const meta = promptResult._meta && typeof promptResult._meta === 'object'
226
+ ? promptResult._meta
227
+ : null;
228
+ if (!meta) return null;
229
+ return normalizeGrokTurnUsage({
230
+ ...meta,
231
+ usage: meta.usage,
232
+ modelId: meta.modelId,
233
+ inputTokens: meta.inputTokens,
234
+ outputTokens: meta.outputTokens,
235
+ totalTokens: meta.totalTokens,
236
+ cachedReadTokens: meta.cachedReadTokens,
237
+ reasoningTokens: meta.reasoningTokens,
238
+ costUsdTicks: meta.usage?.costUsdTicks ?? meta.costUsdTicks,
239
+ });
240
+ }
241
+
242
+ function formatUsd(value) {
243
+ const numeric = roundCurrency(value);
244
+ return numeric === null ? null : `$${numeric.toFixed(2)}`;
245
+ }
246
+
247
+ function formatTokens(value) {
248
+ const numeric = toFiniteNumber(value);
249
+ if (numeric === null || numeric < 0) return null;
250
+ if (numeric < 1000) return String(Math.round(numeric));
251
+ if (numeric < 1_000_000) return `${(numeric / 1000).toFixed(1)}k`;
252
+ return `${(numeric / 1_000_000).toFixed(1)}M`;
253
+ }
254
+
255
+ function formatDuration(ms) {
256
+ const value = toFiniteNumber(ms);
257
+ if (value === null || value < 0) return null;
258
+ const totalSeconds = Math.round(value / 1000);
259
+ if (totalSeconds < 60) return `${totalSeconds}s`;
260
+ const minutes = Math.floor(totalSeconds / 60);
261
+ const seconds = totalSeconds % 60;
262
+ return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
263
+ }
264
+
265
+ /**
266
+ * @param {object} options
267
+ * @param {object|null} options.usage last-turn snapshot (normalizeGrokTurnUsage)
268
+ * @param {object|null} options.cycleTotals { costUsd, inputTokens, outputTokens, totalTokens, turnCount }
269
+ * @param {object|null} options.allowances normalizeGrokAllowanceSettings()
270
+ * @param {object|null} options.cycle resolveGrokBillingCycle()
271
+ * @param {object|null} options.billing normalizeGrokBillingCredits() — live plan quota
272
+ * @param {string|null} options.capturedAt
273
+ * @param {boolean} options.configured Grok provider enabled
274
+ * @param {string|null} options.message
275
+ */
276
+ export function buildGrokPlanCard({
277
+ usage = null,
278
+ cycleTotals = null,
279
+ allowances = null,
280
+ cycle = null,
281
+ billing = null,
282
+ capturedAt = null,
283
+ configured = true,
284
+ message = null,
285
+ stale = false,
286
+ } = {}) {
287
+ if (!configured) {
288
+ // Caller usually filters Grok out when disabled; this is a defensive path.
289
+ return null;
290
+ }
291
+
292
+ const settings = normalizeGrokAllowanceSettings(allowances || {});
293
+ const resolvedCycle = cycle || resolveGrokBillingCycle({ resetDay: settings.resetDay });
294
+ const totals = cycleTotals && typeof cycleTotals === 'object'
295
+ ? {
296
+ costUsd: toFiniteNumber(cycleTotals.costUsd) || 0,
297
+ inputTokens: toFiniteNumber(cycleTotals.inputTokens) || 0,
298
+ outputTokens: toFiniteNumber(cycleTotals.outputTokens) || 0,
299
+ totalTokens: toFiniteNumber(cycleTotals.totalTokens) || 0,
300
+ turnCount: toFiniteNumber(cycleTotals.turnCount) || 0,
301
+ }
302
+ : { costUsd: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, turnCount: 0 };
303
+
304
+ const liveQuotaPercent = toFiniteNumber(billing?.usagePercent);
305
+ const hasLiveQuota = liveQuotaPercent !== null;
306
+
307
+ if (!hasLiveQuota && !usage && totals.turnCount <= 0 && totals.costUsd <= 0) {
308
+ return buildUnavailableCard({
309
+ provider: GROK_PROVIDER_ID,
310
+ label: GROK_LABEL,
311
+ status: STATUS_UNAVAILABLE,
312
+ message: toTrimmedString(message)
313
+ || 'No Grok usage captured yet. Run a Grok turn — per-prompt tokens and cost come from the agent result (there is no live plan-quota API).',
314
+ links: [{ label: 'xAI console / billing', url: GROK_BILLING_URL }],
315
+ });
316
+ }
317
+
318
+ const meters = [];
319
+ if (hasLiveQuota) {
320
+ const periodLabel = billing.periodType === 'weekly'
321
+ ? 'Weekly plan usage'
322
+ : `Plan usage${billing.periodType ? ` (${billing.periodType})` : ''}`;
323
+ const productBits = (billing.products || [])
324
+ .map((entry) => `${entry.product} ${entry.usagePercent}%`)
325
+ .join(' · ');
326
+ meters.push(buildMeter({
327
+ id: 'grok-plan-quota',
328
+ label: periodLabel,
329
+ unit: 'percent',
330
+ utilization: liveQuotaPercent,
331
+ resetAt: billing.periodEnd || null,
332
+ estimated: false,
333
+ emphasis: 'primary',
334
+ note: productBits
335
+ ? `Live from the Grok subscription (${productBits}).`
336
+ : 'Live from the Grok subscription.',
337
+ }));
338
+ }
339
+ if (settings.monthlyUsd !== null) {
340
+ meters.push(buildMeter({
341
+ id: 'grok-monthly-spend',
342
+ label: 'Estimated monthly spend',
343
+ unit: 'usd',
344
+ used: totals.costUsd,
345
+ allowance: settings.monthlyUsd,
346
+ resetAt: resolvedCycle.endsAt,
347
+ estimated: true,
348
+ emphasis: hasLiveQuota ? 'secondary' : 'primary',
349
+ note: 'Local estimate from relay turns. xAI does not expose remaining plan credits over ACP.',
350
+ }));
351
+ }
352
+
353
+ const details = [];
354
+ if (usage) {
355
+ const rows = [];
356
+ const cost = formatUsd(usage.costUsd);
357
+ if (cost) rows.push({ label: 'Last turn cost (estimate)', value: cost });
358
+ if (usage.modelId) rows.push({ label: 'Model', value: usage.modelId });
359
+ const tokenBits = [
360
+ formatTokens(usage.inputTokens) ? `in ${formatTokens(usage.inputTokens)}` : null,
361
+ formatTokens(usage.outputTokens) ? `out ${formatTokens(usage.outputTokens)}` : null,
362
+ formatTokens(usage.cachedReadTokens) ? `cache r ${formatTokens(usage.cachedReadTokens)}` : null,
363
+ formatTokens(usage.reasoningTokens) ? `reason ${formatTokens(usage.reasoningTokens)}` : null,
364
+ formatTokens(usage.totalTokens) ? `total ${formatTokens(usage.totalTokens)}` : null,
365
+ ].filter(Boolean);
366
+ if (tokenBits.length) rows.push({ label: 'Tokens', value: tokenBits.join(' · ') });
367
+ const apiDuration = formatDuration(usage.apiDurationMs);
368
+ if (apiDuration) rows.push({ label: 'API time', value: apiDuration });
369
+ if (usage.numTurns !== null) rows.push({ label: 'Agent turns', value: String(usage.numTurns) });
370
+ const lastSection = buildDetailSection({
371
+ id: 'grok-last-turn',
372
+ label: 'Latest turn',
373
+ rows,
374
+ note: 'From the last Grok agent prompt result on this relay host.',
375
+ });
376
+ if (lastSection) details.push(lastSection);
377
+
378
+ const modelRows = (usage.modelUsage || []).map((entry) => ({
379
+ label: entry.model,
380
+ value: formatUsd(entry.costUsd) ?? '—',
381
+ hint: [
382
+ formatTokens(entry.inputTokens) ? `in ${formatTokens(entry.inputTokens)}` : null,
383
+ formatTokens(entry.outputTokens) ? `out ${formatTokens(entry.outputTokens)}` : null,
384
+ formatTokens(entry.cachedReadTokens) ? `cache r ${formatTokens(entry.cachedReadTokens)}` : null,
385
+ ].filter(Boolean).join(' · ') || null,
386
+ }));
387
+ const modelSection = buildDetailSection({
388
+ id: 'grok-models',
389
+ label: 'By model (last turn)',
390
+ rows: modelRows,
391
+ });
392
+ if (modelSection) details.push(modelSection);
393
+ }
394
+
395
+ if (totals.turnCount > 0 || totals.costUsd > 0) {
396
+ const cycleRows = [];
397
+ const cycleCost = formatUsd(totals.costUsd);
398
+ if (cycleCost) cycleRows.push({ label: 'Cycle spend (estimate)', value: cycleCost });
399
+ if (totals.turnCount > 0) cycleRows.push({ label: 'Turns tracked', value: String(Math.round(totals.turnCount)) });
400
+ const cycleTokens = formatTokens(totals.totalTokens);
401
+ if (cycleTokens) cycleRows.push({ label: 'Tokens (sum)', value: cycleTokens });
402
+ if (resolvedCycle.startsAt && resolvedCycle.endsAt) {
403
+ cycleRows.push({
404
+ label: 'Billing cycle',
405
+ value: `${resolvedCycle.startsAt.slice(0, 10)} → ${resolvedCycle.endsAt.slice(0, 10)}`,
406
+ });
407
+ }
408
+ const cycleSection = buildDetailSection({
409
+ id: 'grok-cycle',
410
+ label: 'Current cycle (local)',
411
+ rows: cycleRows,
412
+ note: settings.monthlyUsd === null
413
+ ? 'Set a monthly allowance in Settings to show a remaining-budget meter.'
414
+ : 'Accumulated from Grok turns on this relay. Not an xAI statement.',
415
+ });
416
+ if (cycleSection) details.push(cycleSection);
417
+ }
418
+
419
+ const hasMeter = meters.filter(Boolean).length > 0;
420
+ return buildProviderCard({
421
+ provider: GROK_PROVIDER_ID,
422
+ label: GROK_LABEL,
423
+ status: hasMeter ? STATUS_OK : STATUS_PARTIAL,
424
+ message: hasMeter
425
+ ? null
426
+ : 'No live plan-quota data for Grok right now. Showing last-turn cost/tokens; set a monthly allowance in Settings for an estimated budget meter.',
427
+ source: hasLiveQuota
428
+ ? SOURCE_LIVE
429
+ : (settings.monthlyUsd !== null ? SOURCE_MANUAL : (stale ? SOURCE_CACHE : SOURCE_WORKER)),
430
+ stale: stale === true,
431
+ capturedAt: capturedAt || usage?.capturedAt || null,
432
+ meters: meters.filter(Boolean),
433
+ details,
434
+ links: [{ label: 'xAI console / billing', url: GROK_BILLING_URL }],
435
+ });
436
+ }