@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,487 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Persistence + aggregation for plan usage.
5
+ *
6
+ * Storage is deliberately "latest snapshot only" for the provider payloads:
7
+ * plan limits are account-wide and only the newest reading is meaningful, so
8
+ * there is no per-turn history table here (per-turn Copilot deltas already
9
+ * live in `message_usage_snapshots` and serve a different purpose).
10
+ *
11
+ * Cursor is the exception, and only as far as correctness demands: its spend
12
+ * has to be *derived*, so two small bookkeeping tables are kept — a cumulative
13
+ * per-agent checkpoint (to diff against) and per-cycle pool totals (the
14
+ * accumulated result). Neither grows with conversation history.
15
+ */
16
+
17
+ import {
18
+ PLAN_USAGE_VERSION,
19
+ toTrimmedString,
20
+ } from './plan-usage-contract.mjs';
21
+ import { buildCopilotPlanCard } from './plan-usage-copilot.mjs';
22
+ import { buildClaudePlanCard } from './plan-usage-claude.mjs';
23
+ import {
24
+ CURSOR_POOLS,
25
+ applyCursorUsageDelta,
26
+ buildCursorPlanCard,
27
+ emptyCursorPoolTotals,
28
+ normalizeCursorAllowanceSettings,
29
+ resolveCursorBillingCycle,
30
+ } from './plan-usage-cursor.mjs';
31
+ import {
32
+ buildGrokPlanCard,
33
+ normalizeGrokAllowanceSettings,
34
+ normalizeGrokTurnUsage,
35
+ resolveGrokBillingCycle,
36
+ } from './plan-usage-grok.mjs';
37
+
38
+ export const PROVIDER_USAGE_SNAPSHOT_TABLE = 'provider_usage_snapshots';
39
+ export const CURSOR_USAGE_CHECKPOINT_TABLE = 'cursor_usage_checkpoints';
40
+ export const CURSOR_USAGE_CYCLE_TABLE = 'cursor_usage_cycle_totals';
41
+ export const GROK_USAGE_CYCLE_TABLE = 'grok_usage_cycle_totals';
42
+
43
+ export const PLAN_USAGE_SCHEMA_SQL = `
44
+ CREATE TABLE IF NOT EXISTS ${PROVIDER_USAGE_SNAPSHOT_TABLE} (
45
+ provider TEXT PRIMARY KEY,
46
+ payload_json TEXT,
47
+ source TEXT,
48
+ captured_at TEXT NOT NULL,
49
+ error TEXT
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS ${CURSOR_USAGE_CHECKPOINT_TABLE} (
53
+ agent_id TEXT PRIMARY KEY,
54
+ raw_cost_cents REAL,
55
+ charged_cents REAL,
56
+ input_tokens REAL,
57
+ output_tokens REAL,
58
+ cache_read_tokens REAL,
59
+ cache_write_tokens REAL,
60
+ total_tokens REAL,
61
+ run_count REAL,
62
+ updated_at TEXT NOT NULL
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS ${CURSOR_USAGE_CYCLE_TABLE} (
66
+ cycle_key TEXT NOT NULL,
67
+ pool TEXT NOT NULL,
68
+ raw_cost_cents REAL NOT NULL DEFAULT 0,
69
+ charged_cents REAL NOT NULL DEFAULT 0,
70
+ input_tokens REAL NOT NULL DEFAULT 0,
71
+ output_tokens REAL NOT NULL DEFAULT 0,
72
+ cache_read_tokens REAL NOT NULL DEFAULT 0,
73
+ cache_write_tokens REAL NOT NULL DEFAULT 0,
74
+ total_tokens REAL NOT NULL DEFAULT 0,
75
+ run_count REAL NOT NULL DEFAULT 0,
76
+ updated_at TEXT NOT NULL,
77
+ PRIMARY KEY (cycle_key, pool)
78
+ );
79
+
80
+ CREATE TABLE IF NOT EXISTS ${GROK_USAGE_CYCLE_TABLE} (
81
+ cycle_key TEXT PRIMARY KEY,
82
+ cost_usd REAL NOT NULL DEFAULT 0,
83
+ input_tokens REAL NOT NULL DEFAULT 0,
84
+ output_tokens REAL NOT NULL DEFAULT 0,
85
+ total_tokens REAL NOT NULL DEFAULT 0,
86
+ turn_count REAL NOT NULL DEFAULT 0,
87
+ updated_at TEXT NOT NULL
88
+ );
89
+ `;
90
+
91
+ const CHECKPOINT_COLUMNS = [
92
+ ['rawCostCents', 'raw_cost_cents'],
93
+ ['chargedCents', 'charged_cents'],
94
+ ['inputTokens', 'input_tokens'],
95
+ ['outputTokens', 'output_tokens'],
96
+ ['cacheReadTokens', 'cache_read_tokens'],
97
+ ['cacheWriteTokens', 'cache_write_tokens'],
98
+ ['totalTokens', 'total_tokens'],
99
+ ['runCount', 'run_count'],
100
+ ];
101
+
102
+ export function ensurePlanUsageSchema(db) {
103
+ if (typeof db?.exec !== 'function') return false;
104
+ db.exec(PLAN_USAGE_SCHEMA_SQL);
105
+ return true;
106
+ }
107
+
108
+ function rowToCheckpoint(row) {
109
+ if (!row) return null;
110
+ const checkpoint = { agentId: row.agent_id, capturedAt: row.updated_at };
111
+ for (const [field, column] of CHECKPOINT_COLUMNS) {
112
+ const value = Number(row[column]);
113
+ checkpoint[field] = Number.isFinite(value) ? value : null;
114
+ }
115
+ return checkpoint;
116
+ }
117
+
118
+ function rowsToPoolTotals(rows) {
119
+ const totals = emptyCursorPoolTotals();
120
+ for (const row of (Array.isArray(rows) ? rows : [])) {
121
+ const pool = toTrimmedString(row?.pool);
122
+ if (!pool || !totals[pool]) continue;
123
+ for (const [field, column] of CHECKPOINT_COLUMNS) {
124
+ const value = Number(row[column]);
125
+ totals[pool][field] = Number.isFinite(value) ? value : 0;
126
+ }
127
+ }
128
+ return totals;
129
+ }
130
+
131
+ /**
132
+ * @param {object} deps
133
+ * @param {object} deps.db better-sqlite3 handle
134
+ * @param {() => Date} [deps.now] injectable clock for deterministic tests
135
+ */
136
+ export function createPlanUsageService({ db, now = () => new Date(), dbg = () => {} } = {}) {
137
+ if (!db || typeof db.prepare !== 'function') {
138
+ throw new Error('createPlanUsageService requires a database handle');
139
+ }
140
+ ensurePlanUsageSchema(db);
141
+
142
+ const stmts = {
143
+ upsertSnapshot: db.prepare(`
144
+ INSERT INTO ${PROVIDER_USAGE_SNAPSHOT_TABLE} (provider, payload_json, source, captured_at, error)
145
+ VALUES (?, ?, ?, ?, ?)
146
+ ON CONFLICT(provider) DO UPDATE SET
147
+ payload_json = excluded.payload_json,
148
+ source = excluded.source,
149
+ captured_at = excluded.captured_at,
150
+ error = excluded.error
151
+ `),
152
+ getSnapshot: db.prepare(`SELECT * FROM ${PROVIDER_USAGE_SNAPSHOT_TABLE} WHERE provider = ?`),
153
+ getCheckpoint: db.prepare(`SELECT * FROM ${CURSOR_USAGE_CHECKPOINT_TABLE} WHERE agent_id = ?`),
154
+ upsertCheckpoint: db.prepare(`
155
+ INSERT INTO ${CURSOR_USAGE_CHECKPOINT_TABLE} (
156
+ agent_id, raw_cost_cents, charged_cents, input_tokens, output_tokens,
157
+ cache_read_tokens, cache_write_tokens, total_tokens, run_count, updated_at
158
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
159
+ ON CONFLICT(agent_id) DO UPDATE SET
160
+ raw_cost_cents = excluded.raw_cost_cents,
161
+ charged_cents = excluded.charged_cents,
162
+ input_tokens = excluded.input_tokens,
163
+ output_tokens = excluded.output_tokens,
164
+ cache_read_tokens = excluded.cache_read_tokens,
165
+ cache_write_tokens = excluded.cache_write_tokens,
166
+ total_tokens = excluded.total_tokens,
167
+ run_count = excluded.run_count,
168
+ updated_at = excluded.updated_at
169
+ `),
170
+ listCycleTotals: db.prepare(`SELECT * FROM ${CURSOR_USAGE_CYCLE_TABLE} WHERE cycle_key = ?`),
171
+ addCycleTotals: db.prepare(`
172
+ INSERT INTO ${CURSOR_USAGE_CYCLE_TABLE} (
173
+ cycle_key, pool, raw_cost_cents, charged_cents, input_tokens, output_tokens,
174
+ cache_read_tokens, cache_write_tokens, total_tokens, run_count, updated_at
175
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
176
+ ON CONFLICT(cycle_key, pool) DO UPDATE SET
177
+ raw_cost_cents = ${CURSOR_USAGE_CYCLE_TABLE}.raw_cost_cents + excluded.raw_cost_cents,
178
+ charged_cents = ${CURSOR_USAGE_CYCLE_TABLE}.charged_cents + excluded.charged_cents,
179
+ input_tokens = ${CURSOR_USAGE_CYCLE_TABLE}.input_tokens + excluded.input_tokens,
180
+ output_tokens = ${CURSOR_USAGE_CYCLE_TABLE}.output_tokens + excluded.output_tokens,
181
+ cache_read_tokens = ${CURSOR_USAGE_CYCLE_TABLE}.cache_read_tokens + excluded.cache_read_tokens,
182
+ cache_write_tokens = ${CURSOR_USAGE_CYCLE_TABLE}.cache_write_tokens + excluded.cache_write_tokens,
183
+ total_tokens = ${CURSOR_USAGE_CYCLE_TABLE}.total_tokens + excluded.total_tokens,
184
+ run_count = ${CURSOR_USAGE_CYCLE_TABLE}.run_count + excluded.run_count,
185
+ updated_at = excluded.updated_at
186
+ `),
187
+ clearCheckpoints: db.prepare(`DELETE FROM ${CURSOR_USAGE_CHECKPOINT_TABLE}`),
188
+ clearCycle: db.prepare(`DELETE FROM ${CURSOR_USAGE_CYCLE_TABLE} WHERE cycle_key = ?`),
189
+ getGrokCycle: db.prepare(`SELECT * FROM ${GROK_USAGE_CYCLE_TABLE} WHERE cycle_key = ?`),
190
+ addGrokCycle: db.prepare(`
191
+ INSERT INTO ${GROK_USAGE_CYCLE_TABLE} (
192
+ cycle_key, cost_usd, input_tokens, output_tokens, total_tokens, turn_count, updated_at
193
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
194
+ ON CONFLICT(cycle_key) DO UPDATE SET
195
+ cost_usd = ${GROK_USAGE_CYCLE_TABLE}.cost_usd + excluded.cost_usd,
196
+ input_tokens = ${GROK_USAGE_CYCLE_TABLE}.input_tokens + excluded.input_tokens,
197
+ output_tokens = ${GROK_USAGE_CYCLE_TABLE}.output_tokens + excluded.output_tokens,
198
+ total_tokens = ${GROK_USAGE_CYCLE_TABLE}.total_tokens + excluded.total_tokens,
199
+ turn_count = ${GROK_USAGE_CYCLE_TABLE}.turn_count + excluded.turn_count,
200
+ updated_at = excluded.updated_at
201
+ `),
202
+ clearGrokCycle: db.prepare(`DELETE FROM ${GROK_USAGE_CYCLE_TABLE} WHERE cycle_key = ?`),
203
+ };
204
+
205
+ function nowIso() {
206
+ const value = now();
207
+ return value instanceof Date && !Number.isNaN(value.getTime())
208
+ ? value.toISOString()
209
+ : new Date().toISOString();
210
+ }
211
+
212
+ function saveSnapshot(provider, payload, { source = 'worker', error = null, capturedAt = null } = {}) {
213
+ const key = toTrimmedString(provider);
214
+ if (!key) return false;
215
+ stmts.upsertSnapshot.run(
216
+ key,
217
+ payload ? JSON.stringify(payload) : null,
218
+ toTrimmedString(source),
219
+ toTrimmedString(capturedAt) || nowIso(),
220
+ toTrimmedString(error),
221
+ );
222
+ return true;
223
+ }
224
+
225
+ function readSnapshot(provider) {
226
+ const row = stmts.getSnapshot.get(toTrimmedString(provider) || '');
227
+ if (!row) return null;
228
+ let payload = null;
229
+ if (row.payload_json) {
230
+ try {
231
+ payload = JSON.parse(row.payload_json);
232
+ } catch (parseError) {
233
+ dbg('plan usage snapshot parse failed', provider, parseError?.message || String(parseError));
234
+ payload = null;
235
+ }
236
+ }
237
+ return {
238
+ provider: row.provider,
239
+ payload,
240
+ source: row.source || null,
241
+ capturedAt: row.captured_at || null,
242
+ error: row.error || null,
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Fold one cumulative Cursor agent report into the current cycle. Returns the
248
+ * applied delta so callers can log/trace it; a report that adds nothing (a
249
+ * repeated snapshot, or a backend restatement) still refreshes the checkpoint.
250
+ */
251
+ function recordCursorUsageReport(report, { resetDay } = {}) {
252
+ const previousRow = stmts.getCheckpoint.get(toTrimmedString(report?.agentId) || '');
253
+ const applied = applyCursorUsageDelta({ checkpoint: rowToCheckpoint(previousRow), report });
254
+ if (!applied) return null;
255
+
256
+ const cycle = resolveCursorBillingCycle({ resetDay, now: now() });
257
+ const timestamp = applied.capturedAt || nowIso();
258
+
259
+ const persist = db.transaction(() => {
260
+ stmts.upsertCheckpoint.run(
261
+ applied.agentId,
262
+ applied.checkpoint.rawCostCents,
263
+ applied.checkpoint.chargedCents,
264
+ applied.checkpoint.inputTokens,
265
+ applied.checkpoint.outputTokens,
266
+ applied.checkpoint.cacheReadTokens,
267
+ applied.checkpoint.cacheWriteTokens,
268
+ applied.checkpoint.totalTokens,
269
+ applied.checkpoint.runCount,
270
+ timestamp,
271
+ );
272
+ if (applied.changed) {
273
+ stmts.addCycleTotals.run(
274
+ cycle.key,
275
+ applied.pool,
276
+ applied.delta.rawCostCents,
277
+ applied.delta.chargedCents,
278
+ applied.delta.inputTokens,
279
+ applied.delta.outputTokens,
280
+ applied.delta.cacheReadTokens,
281
+ applied.delta.cacheWriteTokens,
282
+ applied.delta.totalTokens,
283
+ applied.delta.runCount,
284
+ timestamp,
285
+ );
286
+ }
287
+ });
288
+ persist();
289
+
290
+ return { ...applied, cycle };
291
+ }
292
+
293
+ function readCursorCycleTotals({ resetDay } = {}) {
294
+ const cycle = resolveCursorBillingCycle({ resetDay, now: now() });
295
+ const rows = stmts.listCycleTotals.all(cycle.key);
296
+ const totals = rowsToPoolTotals(rows);
297
+ const capturedAt = (rows || [])
298
+ .map((row) => toTrimmedString(row?.updated_at))
299
+ .filter(Boolean)
300
+ .sort()
301
+ .pop() || null;
302
+ return { cycle, totals, capturedAt };
303
+ }
304
+
305
+ /**
306
+ * Drop the derived Cursor bookkeeping. Used when the user changes plan or
307
+ * account and the accumulated cycle no longer reflects reality — the next
308
+ * report re-baselines from the agent's current cumulative totals, so no
309
+ * historical spend is retroactively counted.
310
+ */
311
+ function resetCursorAccounting({ resetDay } = {}) {
312
+ const cycle = resolveCursorBillingCycle({ resetDay, now: now() });
313
+ const reset = db.transaction(() => {
314
+ stmts.clearCheckpoints.run();
315
+ stmts.clearCycle.run(cycle.key);
316
+ });
317
+ reset();
318
+ return { cycle };
319
+ }
320
+
321
+ /**
322
+ * Book one completed Grok turn into the current billing cycle and refresh the
323
+ * last-turn snapshot. Grok reports per-prompt totals (not cumulative agent
324
+ * counters), so each report is added whole — no checkpoint/diff.
325
+ */
326
+ function recordGrokUsageReport(rawReport, { resetDay } = {}) {
327
+ const usage = normalizeGrokTurnUsage(rawReport);
328
+ if (!usage) return null;
329
+ const cycle = resolveGrokBillingCycle({ resetDay, now: now() });
330
+ // Server clock for the ledger/snapshot timestamp: the cycle is chosen by
331
+ // server time too, and a client-supplied capturedAt would otherwise drive
332
+ // the card's "Updated …" freshness label. The worker's capturedAt stays
333
+ // inside the snapshot payload itself.
334
+ const timestamp = nowIso();
335
+ const persist = db.transaction(() => {
336
+ stmts.addGrokCycle.run(
337
+ cycle.key,
338
+ usage.costUsd || 0,
339
+ usage.inputTokens || 0,
340
+ usage.outputTokens || 0,
341
+ usage.totalTokens || 0,
342
+ 1,
343
+ timestamp,
344
+ );
345
+ stmts.upsertSnapshot.run(
346
+ 'grok',
347
+ JSON.stringify(usage),
348
+ 'worker',
349
+ timestamp,
350
+ null,
351
+ );
352
+ });
353
+ persist();
354
+ return { usage, cycle };
355
+ }
356
+
357
+ function readGrokCycleTotals({ resetDay } = {}) {
358
+ const cycle = resolveGrokBillingCycle({ resetDay, now: now() });
359
+ const row = stmts.getGrokCycle.get(cycle.key);
360
+ if (!row) {
361
+ return {
362
+ cycle,
363
+ totals: { costUsd: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, turnCount: 0 },
364
+ capturedAt: null,
365
+ };
366
+ }
367
+ return {
368
+ cycle,
369
+ totals: {
370
+ costUsd: Number(row.cost_usd) || 0,
371
+ inputTokens: Number(row.input_tokens) || 0,
372
+ outputTokens: Number(row.output_tokens) || 0,
373
+ totalTokens: Number(row.total_tokens) || 0,
374
+ turnCount: Number(row.turn_count) || 0,
375
+ },
376
+ capturedAt: row.updated_at || null,
377
+ };
378
+ }
379
+
380
+ function resetGrokAccounting({ resetDay } = {}) {
381
+ const cycle = resolveGrokBillingCycle({ resetDay, now: now() });
382
+ stmts.clearGrokCycle.run(cycle.key);
383
+ return { cycle };
384
+ }
385
+
386
+ /**
387
+ * Assemble the full multi-provider report. Every provider is independent:
388
+ * one failing source produces an unavailable card instead of failing the
389
+ * whole response.
390
+ */
391
+ function buildReport({
392
+ copilotSummary = null,
393
+ copilotError = null,
394
+ copilotStale = false,
395
+ copilotBilling = null,
396
+ claudeConfigured = true,
397
+ cursorConfigured = true,
398
+ cursorAllowances = null,
399
+ cursorBilling = null,
400
+ cursorDashboardAuth = null,
401
+ grokConfigured = false,
402
+ grokAllowances = null,
403
+ grokBilling = null,
404
+ } = {}) {
405
+ const generatedAt = nowIso();
406
+ const claudeSnapshot = readSnapshot('claude');
407
+ const allowances = normalizeCursorAllowanceSettings(cursorAllowances || {});
408
+ const cursorCycle = readCursorCycleTotals({ resetDay: allowances.resetDay });
409
+ const grokSettings = normalizeGrokAllowanceSettings(grokAllowances || {});
410
+ const grokCycle = readGrokCycleTotals({ resetDay: grokSettings.resetDay });
411
+ const grokSnapshot = readSnapshot('grok');
412
+ // The SDK engine's per-turn detail. Stored under its own key rather than
413
+ // 'github' so it stays clearly additive: the Copilot card's meters come
414
+ // from the account-level quota API and are correct for both engines, and
415
+ // an install still on the extension engine simply has no row here.
416
+ const copilotWorkerSnapshot = readSnapshot('copilot-sdk');
417
+
418
+ const providers = [
419
+ buildCopilotPlanCard({
420
+ summary: copilotSummary,
421
+ billing: copilotBilling,
422
+ workerUsage: copilotWorkerSnapshot?.payload || null,
423
+ error: copilotError,
424
+ capturedAt: generatedAt,
425
+ stale: copilotStale === true,
426
+ }),
427
+ buildClaudePlanCard({
428
+ usage: claudeSnapshot?.payload || null,
429
+ capturedAt: claudeSnapshot?.capturedAt || null,
430
+ configured: claudeConfigured !== false,
431
+ message: claudeSnapshot?.error || null,
432
+ // A stored snapshot is by definition from an earlier turn; the SDK's
433
+ // usage control call only works while a session transport is open, and
434
+ // the relay never opens a billable turn just to refresh this.
435
+ stale: !!claudeSnapshot?.payload,
436
+ }),
437
+ buildCursorPlanCard({
438
+ totals: cursorCycle.totals,
439
+ allowances,
440
+ cycle: cursorCycle.cycle,
441
+ capturedAt: cursorCycle.capturedAt,
442
+ configured: cursorConfigured !== false,
443
+ dashboard: cursorBilling,
444
+ dashboardAuth: cursorDashboardAuth,
445
+ }),
446
+ ];
447
+
448
+ // Grok is opt-in: hide the card entirely when the provider is disabled
449
+ // (unlike Claude/Cursor which show a not-configured placeholder).
450
+ if (grokConfigured === true) {
451
+ providers.push(buildGrokPlanCard({
452
+ usage: grokSnapshot?.payload || null,
453
+ cycleTotals: grokCycle.totals,
454
+ allowances: grokSettings,
455
+ cycle: grokCycle.cycle,
456
+ billing: grokBilling,
457
+ capturedAt: grokSnapshot?.capturedAt || grokCycle.capturedAt || null,
458
+ configured: true,
459
+ message: grokSnapshot?.error || null,
460
+ // Same semantics as the Claude card: a stored snapshot is by
461
+ // definition from an earlier turn (the relay never opens a billable
462
+ // turn just to refresh it), so the two identical data shapes carry
463
+ // the same staleness flag.
464
+ stale: !!grokSnapshot?.payload,
465
+ }));
466
+ }
467
+
468
+ return {
469
+ version: PLAN_USAGE_VERSION,
470
+ generatedAt,
471
+ providers: providers.filter(Boolean),
472
+ };
473
+ }
474
+
475
+ return {
476
+ saveSnapshot,
477
+ readSnapshot,
478
+ recordCursorUsageReport,
479
+ readCursorCycleTotals,
480
+ resetCursorAccounting,
481
+ recordGrokUsageReport,
482
+ readGrokCycleTotals,
483
+ resetGrokAccounting,
484
+ buildReport,
485
+ pools: CURSOR_POOLS,
486
+ };
487
+ }
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ import net from 'net';
4
+
5
+ // Liveness for preview cards. Previews never auto-close, so the card has to say
6
+ // whether the dev server behind it is still up — a TCP connect is enough and,
7
+ // unlike an HTTP request, leaves nothing in the app's request log.
8
+
9
+ const DEFAULT_INTERVAL_MS = 15_000;
10
+ const DEFAULT_CONNECT_TIMEOUT_MS = 250;
11
+
12
+ export function probeTcpPort({ host, port, timeoutMs = DEFAULT_CONNECT_TIMEOUT_MS, connect = net.connect } = {}) {
13
+ return new Promise((resolve) => {
14
+ let settled = false;
15
+ const finish = (online) => {
16
+ if (settled) return;
17
+ settled = true;
18
+ try { socket.destroy(); } catch {}
19
+ resolve(online);
20
+ };
21
+ const socket = connect({ host, port });
22
+ socket.setTimeout?.(timeoutMs);
23
+ socket.once('connect', () => finish(true));
24
+ socket.once('timeout', () => finish(false));
25
+ socket.once('error', () => finish(false));
26
+ });
27
+ }
28
+
29
+ export function createPreviewHealthProbe({
30
+ registry,
31
+ intervalMs = DEFAULT_INTERVAL_MS,
32
+ connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS,
33
+ probe = probeTcpPort,
34
+ setIntervalImpl = setInterval,
35
+ clearIntervalImpl = clearInterval,
36
+ } = {}) {
37
+ let timer = null;
38
+ let sweeping = false;
39
+
40
+ async function probeNow(token) {
41
+ const entry = registry.get(token);
42
+ if (!entry) return null;
43
+ // Static previews have no upstream to die; they are online by definition.
44
+ if (entry.mode === 'static') return true;
45
+ const online = await probe({
46
+ host: entry.targetHost,
47
+ port: entry.targetPort,
48
+ timeoutMs: connectTimeoutMs,
49
+ });
50
+ registry.markHealth(entry.token, online);
51
+ return online;
52
+ }
53
+
54
+ async function sweep() {
55
+ // A slow sweep must not overlap itself; with the registry capped at 8
56
+ // previews and a 250ms connect budget, one pass is bounded at ~2s anyway.
57
+ if (sweeping) return;
58
+ const previews = registry.list().filter((entry) => entry.mode !== 'static');
59
+ if (previews.length === 0) return;
60
+ sweeping = true;
61
+ try {
62
+ await Promise.all(previews.map((entry) => probeNow(entry.token)));
63
+ } finally {
64
+ sweeping = false;
65
+ }
66
+ }
67
+
68
+ function start() {
69
+ if (timer) return;
70
+ timer = setIntervalImpl(() => { sweep().catch(() => {}); }, intervalMs);
71
+ // An idle relay should not be held awake by a probe that has nothing to do.
72
+ if (typeof timer?.unref === 'function') timer.unref();
73
+ }
74
+
75
+ function stop() {
76
+ if (!timer) return;
77
+ clearIntervalImpl(timer);
78
+ timer = null;
79
+ }
80
+
81
+ return { start, stop, sweep, probeNow, get running() { return Boolean(timer); } };
82
+ }