corporateai 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (704) hide show
  1. package/.dockerignore +10 -0
  2. package/.env.example +3 -0
  3. package/.github/workflows/publish-cli.yml +49 -0
  4. package/.mailmap +1 -0
  5. package/AGENTS.md +148 -0
  6. package/CONTRIBUTING.md +75 -0
  7. package/Dockerfile +59 -0
  8. package/Dockerfile.onboard-smoke +42 -0
  9. package/LICENSE +21 -0
  10. package/README.md +93 -0
  11. package/cli/esbuild.config.mjs +11 -0
  12. package/cli/package.json +24 -0
  13. package/cli/scripts/build-cli.mjs +5 -0
  14. package/cli/src/index.ts +27 -0
  15. package/docker-compose.quickstart.yml +18 -0
  16. package/docker-compose.untrusted-review.yml +33 -0
  17. package/docker-compose.yml +38 -0
  18. package/package.json +56 -0
  19. package/patches/embedded-postgres@18.1.0-beta.16.patch +0 -0
  20. package/pnpm-workspace.yaml +4 -0
  21. package/releases/.gitkeep +0 -0
  22. package/releases/v0.0.1.md +36 -0
  23. package/releases/v0.2.7.md +15 -0
  24. package/releases/v0.3.0.md +54 -0
  25. package/releases/v0.3.1.md +55 -0
  26. package/releases/v2026.318.0.md +66 -0
  27. package/releases/v2026.325.0.md +78 -0
  28. package/report/2026-03-13-08-46-token-optimization-implementation.md +48 -0
  29. package/scripts/backup-db.sh +17 -0
  30. package/scripts/build-npm.sh +80 -0
  31. package/scripts/check-forbidden-tokens.mjs +115 -0
  32. package/scripts/clean-onboard-git.sh +14 -0
  33. package/scripts/clean-onboard-npm.sh +13 -0
  34. package/scripts/clean-onboard-ref.sh +86 -0
  35. package/scripts/create-github-release.sh +99 -0
  36. package/scripts/dev-runner-paths.mjs +38 -0
  37. package/scripts/dev-runner.mjs +606 -0
  38. package/scripts/docker-onboard-smoke.sh +306 -0
  39. package/scripts/ensure-plugin-build-deps.mjs +47 -0
  40. package/scripts/generate-company-assets.ts +365 -0
  41. package/scripts/generate-npm-package-json.mjs +113 -0
  42. package/scripts/generate-org-chart-images.ts +694 -0
  43. package/scripts/generate-org-chart-satori-comparison.ts +225 -0
  44. package/scripts/generate-ui-package-json.mjs +31 -0
  45. package/scripts/kill-dev.sh +71 -0
  46. package/scripts/migrate-inline-env-secrets.ts +126 -0
  47. package/scripts/prepare-server-ui-dist.sh +22 -0
  48. package/scripts/provision-worktree.sh +333 -0
  49. package/scripts/release-lib.sh +306 -0
  50. package/scripts/release-package-map.mjs +169 -0
  51. package/scripts/release.sh +312 -0
  52. package/scripts/rollback-latest.sh +111 -0
  53. package/scripts/smoke/openclaw-docker-ui.sh +329 -0
  54. package/scripts/smoke/openclaw-gateway-e2e.sh +954 -0
  55. package/scripts/smoke/openclaw-join.sh +295 -0
  56. package/scripts/smoke/openclaw-sse-standalone.sh +146 -0
  57. package/scripts/workspace-compat.mjs +60 -0
  58. package/server/CHANGELOG.md +130 -0
  59. package/server/package.json +96 -0
  60. package/server/scripts/copy-onboarding-assets.mjs +10 -0
  61. package/server/scripts/dev-watch.ts +33 -0
  62. package/server/src/__tests__/activity-routes.test.ts +70 -0
  63. package/server/src/__tests__/adapter-models.test.ts +105 -0
  64. package/server/src/__tests__/adapter-session-codecs.test.ts +194 -0
  65. package/server/src/__tests__/agent-auth-jwt.test.ts +79 -0
  66. package/server/src/__tests__/agent-instructions-routes.test.ts +318 -0
  67. package/server/src/__tests__/agent-instructions-service.test.ts +361 -0
  68. package/server/src/__tests__/agent-permissions-routes.test.ts +275 -0
  69. package/server/src/__tests__/agent-shortname-collision.test.ts +69 -0
  70. package/server/src/__tests__/agent-skill-contract.test.ts +50 -0
  71. package/server/src/__tests__/agent-skills-routes.test.ts +462 -0
  72. package/server/src/__tests__/app-hmr-port.test.ts +19 -0
  73. package/server/src/__tests__/approval-routes-idempotency.test.ts +110 -0
  74. package/server/src/__tests__/approvals-service.test.ts +107 -0
  75. package/server/src/__tests__/assets.test.ts +250 -0
  76. package/server/src/__tests__/attachment-types.test.ts +97 -0
  77. package/server/src/__tests__/board-mutation-guard.test.ts +105 -0
  78. package/server/src/__tests__/budgets-service.test.ts +311 -0
  79. package/server/src/__tests__/claude-local-adapter-environment.test.ts +92 -0
  80. package/server/src/__tests__/claude-local-adapter.test.ts +31 -0
  81. package/server/src/__tests__/claude-local-skill-sync.test.ts +111 -0
  82. package/server/src/__tests__/cli-auth-routes.test.ts +230 -0
  83. package/server/src/__tests__/codex-local-adapter-environment.test.ts +143 -0
  84. package/server/src/__tests__/codex-local-adapter.test.ts +253 -0
  85. package/server/src/__tests__/codex-local-execute.test.ts +391 -0
  86. package/server/src/__tests__/codex-local-skill-injection.test.ts +175 -0
  87. package/server/src/__tests__/codex-local-skill-sync.test.ts +123 -0
  88. package/server/src/__tests__/companies-route-path-guard.test.ts +56 -0
  89. package/server/src/__tests__/company-branding-route.test.ts +196 -0
  90. package/server/src/__tests__/company-portability-routes.test.ts +175 -0
  91. package/server/src/__tests__/company-portability.test.ts +2186 -0
  92. package/server/src/__tests__/company-skills-routes.test.ts +113 -0
  93. package/server/src/__tests__/company-skills.test.ts +229 -0
  94. package/server/src/__tests__/costs-service.test.ts +226 -0
  95. package/server/src/__tests__/cursor-local-adapter-environment.test.ts +196 -0
  96. package/server/src/__tests__/cursor-local-adapter.test.ts +406 -0
  97. package/server/src/__tests__/cursor-local-execute.test.ts +263 -0
  98. package/server/src/__tests__/cursor-local-skill-injection.test.ts +104 -0
  99. package/server/src/__tests__/cursor-local-skill-sync.test.ts +145 -0
  100. package/server/src/__tests__/dev-runner-paths.test.ts +25 -0
  101. package/server/src/__tests__/dev-server-status.test.ts +66 -0
  102. package/server/src/__tests__/dev-watch-ignore.test.ts +42 -0
  103. package/server/src/__tests__/documents.test.ts +29 -0
  104. package/server/src/__tests__/error-handler.test.ts +53 -0
  105. package/server/src/__tests__/execution-workspace-policy.test.ts +170 -0
  106. package/server/src/__tests__/forbidden-tokens.test.ts +77 -0
  107. package/server/src/__tests__/gemini-local-adapter-environment.test.ts +135 -0
  108. package/server/src/__tests__/gemini-local-adapter.test.ts +190 -0
  109. package/server/src/__tests__/gemini-local-execute.test.ts +172 -0
  110. package/server/src/__tests__/gemini-local-skill-sync.test.ts +90 -0
  111. package/server/src/__tests__/health.test.ts +16 -0
  112. package/server/src/__tests__/heartbeat-process-recovery.test.ts +256 -0
  113. package/server/src/__tests__/heartbeat-run-summary.test.ts +33 -0
  114. package/server/src/__tests__/heartbeat-workspace-session.test.ts +334 -0
  115. package/server/src/__tests__/helpers/embedded-postgres.ts +7 -0
  116. package/server/src/__tests__/hire-hook.test.ts +181 -0
  117. package/server/src/__tests__/instance-settings-routes.test.ts +156 -0
  118. package/server/src/__tests__/invite-accept-gateway-defaults.test.ts +119 -0
  119. package/server/src/__tests__/invite-accept-replay.test.ts +92 -0
  120. package/server/src/__tests__/invite-expiry.test.ts +10 -0
  121. package/server/src/__tests__/invite-join-grants.test.ts +57 -0
  122. package/server/src/__tests__/invite-join-manager.test.ts +33 -0
  123. package/server/src/__tests__/invite-onboarding-text.test.ts +116 -0
  124. package/server/src/__tests__/issue-comment-reopen-routes.test.ts +146 -0
  125. package/server/src/__tests__/issue-goal-fallback.test.ts +99 -0
  126. package/server/src/__tests__/issues-checkout-wakeup.test.ts +48 -0
  127. package/server/src/__tests__/issues-goal-context-routes.test.ts +187 -0
  128. package/server/src/__tests__/issues-service.test.ts +317 -0
  129. package/server/src/__tests__/issues-user-context.test.ts +113 -0
  130. package/server/src/__tests__/log-redaction.test.ts +74 -0
  131. package/server/src/__tests__/monthly-spend-service.test.ts +90 -0
  132. package/server/src/__tests__/normalize-agent-mention-token.test.ts +41 -0
  133. package/server/src/__tests__/openclaw-gateway-adapter.test.ts +626 -0
  134. package/server/src/__tests__/openclaw-invite-prompt-route.test.ts +192 -0
  135. package/server/src/__tests__/opencode-local-adapter-environment.test.ts +97 -0
  136. package/server/src/__tests__/opencode-local-adapter.test.ts +226 -0
  137. package/server/src/__tests__/opencode-local-skill-sync.test.ts +91 -0
  138. package/server/src/__tests__/paperclip-env.test.ts +58 -0
  139. package/server/src/__tests__/paperclip-skill-utils.test.ts +63 -0
  140. package/server/src/__tests__/pi-local-adapter-environment.test.ts +102 -0
  141. package/server/src/__tests__/pi-local-skill-sync.test.ts +95 -0
  142. package/server/src/__tests__/plugin-dev-watcher.test.ts +68 -0
  143. package/server/src/__tests__/plugin-worker-manager.test.ts +43 -0
  144. package/server/src/__tests__/private-hostname-guard.test.ts +56 -0
  145. package/server/src/__tests__/project-shortname-resolution.test.ts +45 -0
  146. package/server/src/__tests__/quota-windows-service.test.ts +56 -0
  147. package/server/src/__tests__/quota-windows.test.ts +1109 -0
  148. package/server/src/__tests__/redaction.test.ts +66 -0
  149. package/server/src/__tests__/routines-e2e.test.ts +276 -0
  150. package/server/src/__tests__/routines-routes.test.ts +271 -0
  151. package/server/src/__tests__/routines-service.test.ts +424 -0
  152. package/server/src/__tests__/storage-local-provider.test.ts +78 -0
  153. package/server/src/__tests__/ui-branding.test.ts +82 -0
  154. package/server/src/__tests__/work-products.test.ts +95 -0
  155. package/server/src/__tests__/workspace-runtime.test.ts +1131 -0
  156. package/server/src/__tests__/worktree-config.test.ts +426 -0
  157. package/server/src/adapters/codex-models.ts +105 -0
  158. package/server/src/adapters/cursor-models.ts +171 -0
  159. package/server/src/adapters/http/execute.ts +42 -0
  160. package/server/src/adapters/http/index.ts +21 -0
  161. package/server/src/adapters/http/test.ts +116 -0
  162. package/server/src/adapters/index.ts +18 -0
  163. package/server/src/adapters/process/execute.ts +77 -0
  164. package/server/src/adapters/process/index.ts +24 -0
  165. package/server/src/adapters/process/test.ts +89 -0
  166. package/server/src/adapters/registry.ts +225 -0
  167. package/server/src/adapters/server-utils-compat.ts +57 -0
  168. package/server/src/adapters/types.ts +30 -0
  169. package/server/src/adapters/utils.ts +48 -0
  170. package/server/src/agent-auth-jwt.ts +141 -0
  171. package/server/src/app.ts +321 -0
  172. package/server/src/attachment-types.ts +74 -0
  173. package/server/src/auth/better-auth.ts +148 -0
  174. package/server/src/board-claim.ts +150 -0
  175. package/server/src/config-file.ts +17 -0
  176. package/server/src/config.ts +260 -0
  177. package/server/src/dev-server-status.ts +103 -0
  178. package/server/src/dev-watch-ignore.ts +36 -0
  179. package/server/src/errors.ts +34 -0
  180. package/server/src/home-paths.ts +95 -0
  181. package/server/src/index.ts +799 -0
  182. package/server/src/log-redaction.ts +146 -0
  183. package/server/src/middleware/auth.ts +178 -0
  184. package/server/src/middleware/board-mutation-guard.ts +66 -0
  185. package/server/src/middleware/error-handler.ts +71 -0
  186. package/server/src/middleware/index.ts +3 -0
  187. package/server/src/middleware/logger.ts +90 -0
  188. package/server/src/middleware/private-hostname-guard.ts +92 -0
  189. package/server/src/middleware/validate.ts +9 -0
  190. package/server/src/onboarding-assets/ceo/AGENTS.md +54 -0
  191. package/server/src/onboarding-assets/ceo/HEARTBEAT.md +72 -0
  192. package/server/src/onboarding-assets/ceo/SOUL.md +33 -0
  193. package/server/src/onboarding-assets/ceo/TOOLS.md +3 -0
  194. package/server/src/onboarding-assets/default/AGENTS.md +3 -0
  195. package/server/src/paths.ts +34 -0
  196. package/server/src/realtime/live-events-ws.ts +274 -0
  197. package/server/src/redaction.ts +59 -0
  198. package/server/src/routes/access.ts +2888 -0
  199. package/server/src/routes/activity.ts +89 -0
  200. package/server/src/routes/agents.ts +2313 -0
  201. package/server/src/routes/approvals.ts +346 -0
  202. package/server/src/routes/assets.ts +341 -0
  203. package/server/src/routes/authz.ts +52 -0
  204. package/server/src/routes/companies.ts +343 -0
  205. package/server/src/routes/company-skills.ts +300 -0
  206. package/server/src/routes/costs.ts +335 -0
  207. package/server/src/routes/dashboard.ts +19 -0
  208. package/server/src/routes/execution-workspaces.ts +182 -0
  209. package/server/src/routes/goals.ts +107 -0
  210. package/server/src/routes/health.ts +94 -0
  211. package/server/src/routes/index.ts +17 -0
  212. package/server/src/routes/instance-settings.ts +95 -0
  213. package/server/src/routes/issues-checkout-wakeup.ts +14 -0
  214. package/server/src/routes/issues.ts +1680 -0
  215. package/server/src/routes/llms.ts +86 -0
  216. package/server/src/routes/org-chart-svg.ts +777 -0
  217. package/server/src/routes/plugin-ui-static.ts +497 -0
  218. package/server/src/routes/plugins.ts +2220 -0
  219. package/server/src/routes/projects.ts +295 -0
  220. package/server/src/routes/routines.ts +300 -0
  221. package/server/src/routes/secrets.ts +166 -0
  222. package/server/src/routes/sidebar-badges.ts +52 -0
  223. package/server/src/secrets/external-stub-providers.ts +32 -0
  224. package/server/src/secrets/local-encrypted-provider.ts +135 -0
  225. package/server/src/secrets/provider-registry.ts +31 -0
  226. package/server/src/secrets/types.ts +23 -0
  227. package/server/src/services/access.ts +381 -0
  228. package/server/src/services/activity-log.ts +95 -0
  229. package/server/src/services/activity.ts +164 -0
  230. package/server/src/services/agent-instructions.ts +735 -0
  231. package/server/src/services/agent-permissions.ts +27 -0
  232. package/server/src/services/agents.ts +694 -0
  233. package/server/src/services/approvals.ts +273 -0
  234. package/server/src/services/assets.ts +23 -0
  235. package/server/src/services/board-auth.ts +355 -0
  236. package/server/src/services/budgets.ts +959 -0
  237. package/server/src/services/companies.ts +313 -0
  238. package/server/src/services/company-export-readme.ts +173 -0
  239. package/server/src/services/company-portability.ts +4263 -0
  240. package/server/src/services/company-skills.ts +2356 -0
  241. package/server/src/services/costs.ts +365 -0
  242. package/server/src/services/cron.ts +373 -0
  243. package/server/src/services/dashboard.ts +110 -0
  244. package/server/src/services/default-agent-instructions.ts +27 -0
  245. package/server/src/services/documents.ts +434 -0
  246. package/server/src/services/execution-workspace-policy.ts +210 -0
  247. package/server/src/services/execution-workspaces.ts +100 -0
  248. package/server/src/services/finance.ts +135 -0
  249. package/server/src/services/goals.ts +81 -0
  250. package/server/src/services/heartbeat-run-summary.ts +35 -0
  251. package/server/src/services/heartbeat.ts +3863 -0
  252. package/server/src/services/hire-hook.ts +114 -0
  253. package/server/src/services/index.ts +32 -0
  254. package/server/src/services/instance-settings.ts +138 -0
  255. package/server/src/services/issue-approvals.ts +175 -0
  256. package/server/src/services/issue-assignment-wakeup.ts +48 -0
  257. package/server/src/services/issue-goal-fallback.ts +56 -0
  258. package/server/src/services/issues.ts +1828 -0
  259. package/server/src/services/live-events.ts +55 -0
  260. package/server/src/services/plugin-capability-validator.ts +450 -0
  261. package/server/src/services/plugin-config-validator.ts +55 -0
  262. package/server/src/services/plugin-dev-watcher.ts +339 -0
  263. package/server/src/services/plugin-event-bus.ts +413 -0
  264. package/server/src/services/plugin-host-service-cleanup.ts +59 -0
  265. package/server/src/services/plugin-host-services.ts +1132 -0
  266. package/server/src/services/plugin-job-coordinator.ts +261 -0
  267. package/server/src/services/plugin-job-scheduler.ts +753 -0
  268. package/server/src/services/plugin-job-store.ts +466 -0
  269. package/server/src/services/plugin-lifecycle.ts +822 -0
  270. package/server/src/services/plugin-loader.ts +1955 -0
  271. package/server/src/services/plugin-log-retention.ts +87 -0
  272. package/server/src/services/plugin-manifest-validator.ts +164 -0
  273. package/server/src/services/plugin-registry.ts +683 -0
  274. package/server/src/services/plugin-runtime-sandbox.ts +222 -0
  275. package/server/src/services/plugin-secrets-handler.ts +355 -0
  276. package/server/src/services/plugin-state-store.ts +238 -0
  277. package/server/src/services/plugin-stream-bus.ts +81 -0
  278. package/server/src/services/plugin-tool-dispatcher.ts +449 -0
  279. package/server/src/services/plugin-tool-registry.ts +450 -0
  280. package/server/src/services/plugin-worker-manager.ts +1343 -0
  281. package/server/src/services/projects.ts +860 -0
  282. package/server/src/services/quota-windows.ts +65 -0
  283. package/server/src/services/routines.ts +1269 -0
  284. package/server/src/services/run-log-store.ts +156 -0
  285. package/server/src/services/secrets.ts +370 -0
  286. package/server/src/services/sidebar-badges.ts +56 -0
  287. package/server/src/services/work-products.ts +124 -0
  288. package/server/src/services/workspace-operation-log-store.ts +156 -0
  289. package/server/src/services/workspace-operations.ts +262 -0
  290. package/server/src/services/workspace-runtime.ts +1565 -0
  291. package/server/src/startup-banner.ts +176 -0
  292. package/server/src/storage/index.ts +35 -0
  293. package/server/src/storage/local-disk-provider.ts +89 -0
  294. package/server/src/storage/provider-registry.ts +18 -0
  295. package/server/src/storage/s3-provider.ts +153 -0
  296. package/server/src/storage/service.ts +131 -0
  297. package/server/src/storage/types.ts +63 -0
  298. package/server/src/ui-branding.ts +217 -0
  299. package/server/src/version.ts +10 -0
  300. package/server/src/worktree-config.ts +468 -0
  301. package/server/tsconfig.json +9 -0
  302. package/server/vitest.config.ts +7 -0
  303. package/skills/paperclip/SKILL.md +365 -0
  304. package/skills/paperclip/references/api-reference.md +647 -0
  305. package/skills/paperclip/references/company-skills.md +193 -0
  306. package/skills/paperclip-create-agent/SKILL.md +142 -0
  307. package/skills/paperclip-create-agent/references/api-reference.md +105 -0
  308. package/skills/paperclip-create-plugin/SKILL.md +102 -0
  309. package/skills/para-memory-files/SKILL.md +104 -0
  310. package/skills/para-memory-files/references/schemas.md +35 -0
  311. package/tests/e2e/onboarding.spec.ts +142 -0
  312. package/tests/e2e/playwright.config.ts +35 -0
  313. package/tests/release-smoke/docker-auth-onboarding.spec.ts +141 -0
  314. package/tests/release-smoke/playwright.config.ts +28 -0
  315. package/tsconfig.base.json +18 -0
  316. package/tsconfig.json +18 -0
  317. package/ui/README.md +12 -0
  318. package/ui/components.json +21 -0
  319. package/ui/index.html +47 -0
  320. package/ui/package.json +73 -0
  321. package/ui/public/android-chrome-192x192.png +0 -0
  322. package/ui/public/android-chrome-512x512.png +0 -0
  323. package/ui/public/apple-touch-icon.png +0 -0
  324. package/ui/public/brands/opencode-logo-dark-square.svg +18 -0
  325. package/ui/public/brands/opencode-logo-light-square.svg +18 -0
  326. package/ui/public/favicon-16x16.png +0 -0
  327. package/ui/public/favicon-32x32.png +0 -0
  328. package/ui/public/favicon.ico +0 -0
  329. package/ui/public/favicon.svg +9 -0
  330. package/ui/public/site.webmanifest +30 -0
  331. package/ui/public/sprites/1-D-1.png +0 -0
  332. package/ui/public/sprites/1-D-2.png +0 -0
  333. package/ui/public/sprites/1-D-3.png +0 -0
  334. package/ui/public/sprites/1-L-1.png +0 -0
  335. package/ui/public/sprites/1-R-1.png +0 -0
  336. package/ui/public/sprites/10-D-1.png +0 -0
  337. package/ui/public/sprites/10-D-2.png +0 -0
  338. package/ui/public/sprites/10-D-3.png +0 -0
  339. package/ui/public/sprites/10-L-1.png +0 -0
  340. package/ui/public/sprites/10-R-1.png +0 -0
  341. package/ui/public/sprites/11-D-1.png +0 -0
  342. package/ui/public/sprites/11-D-2.png +0 -0
  343. package/ui/public/sprites/11-D-3.png +0 -0
  344. package/ui/public/sprites/11-L-1.png +0 -0
  345. package/ui/public/sprites/11-R-1.png +0 -0
  346. package/ui/public/sprites/12-D-1.png +0 -0
  347. package/ui/public/sprites/12-D-2.png +0 -0
  348. package/ui/public/sprites/12-D-3.png +0 -0
  349. package/ui/public/sprites/12-L-1.png +0 -0
  350. package/ui/public/sprites/12-R-1.png +0 -0
  351. package/ui/public/sprites/13-D-1.png +0 -0
  352. package/ui/public/sprites/13-D-2.png +0 -0
  353. package/ui/public/sprites/13-D-3.png +0 -0
  354. package/ui/public/sprites/13-L-1.png +0 -0
  355. package/ui/public/sprites/13-R-1.png +0 -0
  356. package/ui/public/sprites/14-D-1.png +0 -0
  357. package/ui/public/sprites/14-D-2.png +0 -0
  358. package/ui/public/sprites/14-D-3.png +0 -0
  359. package/ui/public/sprites/14-L-1.png +0 -0
  360. package/ui/public/sprites/14-R-1.png +0 -0
  361. package/ui/public/sprites/2-D-1.png +0 -0
  362. package/ui/public/sprites/2-D-2.png +0 -0
  363. package/ui/public/sprites/2-D-3.png +0 -0
  364. package/ui/public/sprites/2-L-1.png +0 -0
  365. package/ui/public/sprites/2-R-1.png +0 -0
  366. package/ui/public/sprites/3-D-1.png +0 -0
  367. package/ui/public/sprites/3-D-2.png +0 -0
  368. package/ui/public/sprites/3-D-3.png +0 -0
  369. package/ui/public/sprites/3-L-1.png +0 -0
  370. package/ui/public/sprites/3-R-1.png +0 -0
  371. package/ui/public/sprites/4-D-1.png +0 -0
  372. package/ui/public/sprites/4-D-2.png +0 -0
  373. package/ui/public/sprites/4-D-3.png +0 -0
  374. package/ui/public/sprites/4-L-1.png +0 -0
  375. package/ui/public/sprites/4-R-1.png +0 -0
  376. package/ui/public/sprites/5-D-1.png +0 -0
  377. package/ui/public/sprites/5-D-2.png +0 -0
  378. package/ui/public/sprites/5-D-3.png +0 -0
  379. package/ui/public/sprites/5-L-1.png +0 -0
  380. package/ui/public/sprites/5-R-1.png +0 -0
  381. package/ui/public/sprites/6-D-1.png +0 -0
  382. package/ui/public/sprites/6-D-2.png +0 -0
  383. package/ui/public/sprites/6-D-3.png +0 -0
  384. package/ui/public/sprites/6-L-1.png +0 -0
  385. package/ui/public/sprites/6-R-1.png +0 -0
  386. package/ui/public/sprites/7-D-1.png +0 -0
  387. package/ui/public/sprites/7-D-2.png +0 -0
  388. package/ui/public/sprites/7-D-3.png +0 -0
  389. package/ui/public/sprites/7-L-1.png +0 -0
  390. package/ui/public/sprites/7-R-1.png +0 -0
  391. package/ui/public/sprites/8-D-1.png +0 -0
  392. package/ui/public/sprites/8-D-2.png +0 -0
  393. package/ui/public/sprites/8-D-3.png +0 -0
  394. package/ui/public/sprites/8-L-1.png +0 -0
  395. package/ui/public/sprites/8-R-1.png +0 -0
  396. package/ui/public/sprites/9-D-1.png +0 -0
  397. package/ui/public/sprites/9-D-2.png +0 -0
  398. package/ui/public/sprites/9-D-3.png +0 -0
  399. package/ui/public/sprites/9-L-1.png +0 -0
  400. package/ui/public/sprites/9-R-1.png +0 -0
  401. package/ui/public/sprites/ceo-lobster.png +0 -0
  402. package/ui/public/sw.js +42 -0
  403. package/ui/public/worktree-favicon-16x16.png +0 -0
  404. package/ui/public/worktree-favicon-32x32.png +0 -0
  405. package/ui/public/worktree-favicon.ico +0 -0
  406. package/ui/public/worktree-favicon.svg +9 -0
  407. package/ui/src/App.tsx +354 -0
  408. package/ui/src/adapters/claude-local/config-fields.tsx +138 -0
  409. package/ui/src/adapters/claude-local/index.ts +13 -0
  410. package/ui/src/adapters/codex-local/config-fields.tsx +104 -0
  411. package/ui/src/adapters/codex-local/index.ts +13 -0
  412. package/ui/src/adapters/cursor/config-fields.tsx +49 -0
  413. package/ui/src/adapters/cursor/index.ts +13 -0
  414. package/ui/src/adapters/gemini-local/config-fields.tsx +51 -0
  415. package/ui/src/adapters/gemini-local/index.ts +13 -0
  416. package/ui/src/adapters/http/build-config.ts +9 -0
  417. package/ui/src/adapters/http/config-fields.tsx +38 -0
  418. package/ui/src/adapters/http/index.ts +12 -0
  419. package/ui/src/adapters/http/parse-stdout.ts +5 -0
  420. package/ui/src/adapters/index.ts +9 -0
  421. package/ui/src/adapters/local-workspace-runtime-fields.tsx +5 -0
  422. package/ui/src/adapters/openclaw-gateway/config-fields.tsx +237 -0
  423. package/ui/src/adapters/openclaw-gateway/index.ts +13 -0
  424. package/ui/src/adapters/opencode-local/config-fields.tsx +72 -0
  425. package/ui/src/adapters/opencode-local/index.ts +13 -0
  426. package/ui/src/adapters/pi-local/config-fields.tsx +49 -0
  427. package/ui/src/adapters/pi-local/index.ts +13 -0
  428. package/ui/src/adapters/process/build-config.ts +18 -0
  429. package/ui/src/adapters/process/config-fields.tsx +77 -0
  430. package/ui/src/adapters/process/index.ts +12 -0
  431. package/ui/src/adapters/process/parse-stdout.ts +5 -0
  432. package/ui/src/adapters/registry.ts +34 -0
  433. package/ui/src/adapters/runtime-json-fields.tsx +122 -0
  434. package/ui/src/adapters/transcript.test.ts +30 -0
  435. package/ui/src/adapters/transcript.ts +62 -0
  436. package/ui/src/adapters/types.ts +34 -0
  437. package/ui/src/api/access.ts +160 -0
  438. package/ui/src/api/activity.ts +37 -0
  439. package/ui/src/api/agents.ts +194 -0
  440. package/ui/src/api/approvals.ts +25 -0
  441. package/ui/src/api/assets.ts +30 -0
  442. package/ui/src/api/auth.ts +74 -0
  443. package/ui/src/api/budgets.ts +21 -0
  444. package/ui/src/api/client.ts +50 -0
  445. package/ui/src/api/companies.ts +59 -0
  446. package/ui/src/api/companySkills.ts +55 -0
  447. package/ui/src/api/costs.ts +60 -0
  448. package/ui/src/api/dashboard.ts +7 -0
  449. package/ui/src/api/execution-workspaces.ts +27 -0
  450. package/ui/src/api/goals.ts +12 -0
  451. package/ui/src/api/health.ts +41 -0
  452. package/ui/src/api/heartbeats.ts +62 -0
  453. package/ui/src/api/index.ts +18 -0
  454. package/ui/src/api/instanceSettings.ts +19 -0
  455. package/ui/src/api/issues.ts +115 -0
  456. package/ui/src/api/plugins.ts +424 -0
  457. package/ui/src/api/projects.ts +34 -0
  458. package/ui/src/api/routines.ts +59 -0
  459. package/ui/src/api/secrets.ts +26 -0
  460. package/ui/src/api/sidebarBadges.ts +7 -0
  461. package/ui/src/components/AccountingModelCard.tsx +69 -0
  462. package/ui/src/components/ActiveAgentsPanel.tsx +157 -0
  463. package/ui/src/components/ActivityCharts.tsx +264 -0
  464. package/ui/src/components/ActivityRow.tsx +147 -0
  465. package/ui/src/components/AgentActionButtons.tsx +51 -0
  466. package/ui/src/components/AgentConfigForm.tsx +1468 -0
  467. package/ui/src/components/AgentIconPicker.tsx +81 -0
  468. package/ui/src/components/AgentProperties.tsx +107 -0
  469. package/ui/src/components/ApprovalCard.tsx +107 -0
  470. package/ui/src/components/ApprovalPayload.tsx +134 -0
  471. package/ui/src/components/AsciiArtAnimation.tsx +355 -0
  472. package/ui/src/components/BillerSpendCard.tsx +146 -0
  473. package/ui/src/components/BreadcrumbBar.tsx +113 -0
  474. package/ui/src/components/BudgetIncidentCard.tsx +101 -0
  475. package/ui/src/components/BudgetPolicyCard.tsx +220 -0
  476. package/ui/src/components/BudgetSidebarMarker.tsx +13 -0
  477. package/ui/src/components/ClaudeSubscriptionPanel.tsx +141 -0
  478. package/ui/src/components/CodexSubscriptionPanel.tsx +158 -0
  479. package/ui/src/components/CommandPalette.tsx +239 -0
  480. package/ui/src/components/CommentThread.tsx +503 -0
  481. package/ui/src/components/CompanyPatternIcon.tsx +212 -0
  482. package/ui/src/components/CompanyRail.tsx +329 -0
  483. package/ui/src/components/CompanySwitcher.tsx +81 -0
  484. package/ui/src/components/CopyText.tsx +56 -0
  485. package/ui/src/components/DevRestartBanner.tsx +89 -0
  486. package/ui/src/components/EmptyState.tsx +27 -0
  487. package/ui/src/components/EntityRow.tsx +69 -0
  488. package/ui/src/components/FilterBar.tsx +39 -0
  489. package/ui/src/components/FinanceBillerCard.tsx +45 -0
  490. package/ui/src/components/FinanceKindCard.tsx +44 -0
  491. package/ui/src/components/FinanceTimelineCard.tsx +72 -0
  492. package/ui/src/components/GoalProperties.tsx +165 -0
  493. package/ui/src/components/GoalTree.tsx +118 -0
  494. package/ui/src/components/Identity.tsx +39 -0
  495. package/ui/src/components/InlineEditor.tsx +248 -0
  496. package/ui/src/components/InlineEntitySelector.tsx +206 -0
  497. package/ui/src/components/InstanceSidebar.tsx +53 -0
  498. package/ui/src/components/IssueDocumentsSection.tsx +892 -0
  499. package/ui/src/components/IssueProperties.tsx +621 -0
  500. package/ui/src/components/IssueRow.tsx +149 -0
  501. package/ui/src/components/IssueWorkspaceCard.tsx +404 -0
  502. package/ui/src/components/IssuesList.tsx +889 -0
  503. package/ui/src/components/JsonSchemaForm.tsx +1048 -0
  504. package/ui/src/components/KanbanBoard.tsx +275 -0
  505. package/ui/src/components/Layout.tsx +441 -0
  506. package/ui/src/components/LiveRunWidget.tsx +160 -0
  507. package/ui/src/components/MarkdownBody.test.tsx +50 -0
  508. package/ui/src/components/MarkdownBody.tsx +152 -0
  509. package/ui/src/components/MarkdownEditor.tsx +622 -0
  510. package/ui/src/components/MetricCard.tsx +53 -0
  511. package/ui/src/components/MobileBottomNav.tsx +123 -0
  512. package/ui/src/components/NewAgentDialog.tsx +223 -0
  513. package/ui/src/components/NewGoalDialog.tsx +283 -0
  514. package/ui/src/components/NewIssueDialog.tsx +1473 -0
  515. package/ui/src/components/NewProjectDialog.tsx +451 -0
  516. package/ui/src/components/OnboardingWizard.tsx +1392 -0
  517. package/ui/src/components/OpenCodeLogoIcon.tsx +22 -0
  518. package/ui/src/components/PackageFileTree.tsx +318 -0
  519. package/ui/src/components/PageSkeleton.tsx +180 -0
  520. package/ui/src/components/PageTabBar.tsx +45 -0
  521. package/ui/src/components/PathInstructionsModal.tsx +143 -0
  522. package/ui/src/components/PriorityIcon.tsx +77 -0
  523. package/ui/src/components/ProjectProperties.tsx +1127 -0
  524. package/ui/src/components/PropertiesPanel.tsx +29 -0
  525. package/ui/src/components/ProviderQuotaCard.tsx +417 -0
  526. package/ui/src/components/QuotaBar.tsx +65 -0
  527. package/ui/src/components/ReportsToPicker.tsx +127 -0
  528. package/ui/src/components/ScheduleEditor.tsx +344 -0
  529. package/ui/src/components/ScrollToBottom.tsx +79 -0
  530. package/ui/src/components/Sidebar.tsx +130 -0
  531. package/ui/src/components/SidebarAgents.tsx +146 -0
  532. package/ui/src/components/SidebarNavItem.tsx +92 -0
  533. package/ui/src/components/SidebarProjects.tsx +234 -0
  534. package/ui/src/components/SidebarSection.tsx +17 -0
  535. package/ui/src/components/StatusBadge.tsx +15 -0
  536. package/ui/src/components/StatusIcon.tsx +71 -0
  537. package/ui/src/components/SwipeToArchive.tsx +152 -0
  538. package/ui/src/components/ToastViewport.tsx +99 -0
  539. package/ui/src/components/WorktreeBanner.tsx +25 -0
  540. package/ui/src/components/agent-config-defaults.ts +31 -0
  541. package/ui/src/components/agent-config-primitives.tsx +476 -0
  542. package/ui/src/components/transcript/RunTranscriptView.test.tsx +84 -0
  543. package/ui/src/components/transcript/RunTranscriptView.tsx +1015 -0
  544. package/ui/src/components/transcript/useLiveRunTranscripts.ts +297 -0
  545. package/ui/src/components/ui/avatar.tsx +107 -0
  546. package/ui/src/components/ui/badge.tsx +48 -0
  547. package/ui/src/components/ui/breadcrumb.tsx +109 -0
  548. package/ui/src/components/ui/button.tsx +64 -0
  549. package/ui/src/components/ui/card.tsx +92 -0
  550. package/ui/src/components/ui/checkbox.tsx +32 -0
  551. package/ui/src/components/ui/collapsible.tsx +33 -0
  552. package/ui/src/components/ui/command.tsx +194 -0
  553. package/ui/src/components/ui/dialog.tsx +156 -0
  554. package/ui/src/components/ui/dropdown-menu.tsx +257 -0
  555. package/ui/src/components/ui/input.tsx +21 -0
  556. package/ui/src/components/ui/label.tsx +22 -0
  557. package/ui/src/components/ui/popover.tsx +88 -0
  558. package/ui/src/components/ui/scroll-area.tsx +56 -0
  559. package/ui/src/components/ui/select.tsx +188 -0
  560. package/ui/src/components/ui/separator.tsx +28 -0
  561. package/ui/src/components/ui/sheet.tsx +143 -0
  562. package/ui/src/components/ui/skeleton.tsx +13 -0
  563. package/ui/src/components/ui/tabs.tsx +89 -0
  564. package/ui/src/components/ui/textarea.tsx +18 -0
  565. package/ui/src/components/ui/tooltip.tsx +57 -0
  566. package/ui/src/components/visual-office/AgentAvatar.tsx +99 -0
  567. package/ui/src/components/visual-office/OfficeViewExact.tsx +417 -0
  568. package/ui/src/components/visual-office/i18n.ts +52 -0
  569. package/ui/src/components/visual-office/office-view/CliUsagePanel.tsx +240 -0
  570. package/ui/src/components/visual-office/office-view/VirtualPadOverlay.tsx +104 -0
  571. package/ui/src/components/visual-office/office-view/buildScene-break-room.ts +248 -0
  572. package/ui/src/components/visual-office/office-view/buildScene-ceo-hallway.ts +345 -0
  573. package/ui/src/components/visual-office/office-view/buildScene-department-agent.ts +242 -0
  574. package/ui/src/components/visual-office/office-view/buildScene-departments.ts +360 -0
  575. package/ui/src/components/visual-office/office-view/buildScene-final-layers.ts +113 -0
  576. package/ui/src/components/visual-office/office-view/buildScene-types.ts +91 -0
  577. package/ui/src/components/visual-office/office-view/buildScene.ts +232 -0
  578. package/ui/src/components/visual-office/office-view/drawing-core.ts +374 -0
  579. package/ui/src/components/visual-office/office-view/drawing-furniture-a.ts +338 -0
  580. package/ui/src/components/visual-office/office-view/drawing-furniture-b.ts +241 -0
  581. package/ui/src/components/visual-office/office-view/model.ts +301 -0
  582. package/ui/src/components/visual-office/office-view/officeTicker.ts +455 -0
  583. package/ui/src/components/visual-office/office-view/officeTickerRoomAndDelivery.ts +133 -0
  584. package/ui/src/components/visual-office/office-view/themes-locale.ts +460 -0
  585. package/ui/src/components/visual-office/office-view/useCliUsage.ts +37 -0
  586. package/ui/src/components/visual-office/office-view/useOfficeDeliveryEffects.ts +465 -0
  587. package/ui/src/components/visual-office/office-view/useOfficePixiRuntime.ts +282 -0
  588. package/ui/src/components/visual-office/types.ts +123 -0
  589. package/ui/src/context/BreadcrumbContext.tsx +44 -0
  590. package/ui/src/context/CompanyContext.tsx +151 -0
  591. package/ui/src/context/DialogContext.tsx +135 -0
  592. package/ui/src/context/LiveUpdatesProvider.test.ts +119 -0
  593. package/ui/src/context/LiveUpdatesProvider.tsx +760 -0
  594. package/ui/src/context/PanelContext.tsx +73 -0
  595. package/ui/src/context/SidebarContext.tsx +43 -0
  596. package/ui/src/context/ThemeContext.tsx +83 -0
  597. package/ui/src/context/ToastContext.tsx +172 -0
  598. package/ui/src/fixtures/runTranscriptFixtures.ts +226 -0
  599. package/ui/src/hooks/useAgentOrder.ts +105 -0
  600. package/ui/src/hooks/useAutosaveIndicator.ts +72 -0
  601. package/ui/src/hooks/useCompanyPageMemory.test.ts +90 -0
  602. package/ui/src/hooks/useCompanyPageMemory.ts +79 -0
  603. package/ui/src/hooks/useDateRange.ts +120 -0
  604. package/ui/src/hooks/useInboxBadge.ts +132 -0
  605. package/ui/src/hooks/useKeyboardShortcuts.ts +40 -0
  606. package/ui/src/hooks/useProjectOrder.ts +106 -0
  607. package/ui/src/index.css +770 -0
  608. package/ui/src/lib/agent-icons.ts +99 -0
  609. package/ui/src/lib/agent-order.ts +107 -0
  610. package/ui/src/lib/agent-skills-state.test.ts +90 -0
  611. package/ui/src/lib/agent-skills-state.ts +41 -0
  612. package/ui/src/lib/assignees.test.ts +92 -0
  613. package/ui/src/lib/assignees.ts +82 -0
  614. package/ui/src/lib/color-contrast.ts +107 -0
  615. package/ui/src/lib/company-export-selection.test.ts +41 -0
  616. package/ui/src/lib/company-export-selection.ts +57 -0
  617. package/ui/src/lib/company-page-memory.ts +65 -0
  618. package/ui/src/lib/company-portability-sidebar.test.ts +101 -0
  619. package/ui/src/lib/company-portability-sidebar.ts +62 -0
  620. package/ui/src/lib/company-routes.ts +88 -0
  621. package/ui/src/lib/company-selection.test.ts +34 -0
  622. package/ui/src/lib/company-selection.ts +18 -0
  623. package/ui/src/lib/groupBy.ts +11 -0
  624. package/ui/src/lib/inbox.test.ts +404 -0
  625. package/ui/src/lib/inbox.ts +292 -0
  626. package/ui/src/lib/instance-settings.test.ts +26 -0
  627. package/ui/src/lib/instance-settings.ts +25 -0
  628. package/ui/src/lib/issueDetailBreadcrumb.ts +24 -0
  629. package/ui/src/lib/legacy-agent-config.test.ts +40 -0
  630. package/ui/src/lib/legacy-agent-config.ts +17 -0
  631. package/ui/src/lib/mention-aware-link-node.test.ts +50 -0
  632. package/ui/src/lib/mention-aware-link-node.ts +67 -0
  633. package/ui/src/lib/mention-chips.ts +168 -0
  634. package/ui/src/lib/mention-deletion.test.ts +87 -0
  635. package/ui/src/lib/mention-deletion.ts +143 -0
  636. package/ui/src/lib/model-utils.ts +16 -0
  637. package/ui/src/lib/onboarding-goal.test.ts +22 -0
  638. package/ui/src/lib/onboarding-goal.ts +18 -0
  639. package/ui/src/lib/onboarding-launch.test.ts +131 -0
  640. package/ui/src/lib/onboarding-launch.ts +54 -0
  641. package/ui/src/lib/onboarding-route.test.ts +80 -0
  642. package/ui/src/lib/onboarding-route.ts +51 -0
  643. package/ui/src/lib/portable-files.ts +42 -0
  644. package/ui/src/lib/project-order.ts +71 -0
  645. package/ui/src/lib/queryKeys.ts +140 -0
  646. package/ui/src/lib/recent-assignees.ts +36 -0
  647. package/ui/src/lib/router.tsx +76 -0
  648. package/ui/src/lib/routine-trigger-patch.test.ts +72 -0
  649. package/ui/src/lib/routine-trigger-patch.ts +31 -0
  650. package/ui/src/lib/status-colors.ts +108 -0
  651. package/ui/src/lib/timeAgo.ts +31 -0
  652. package/ui/src/lib/utils.ts +168 -0
  653. package/ui/src/lib/worktree-branding.ts +65 -0
  654. package/ui/src/lib/zip.test.ts +289 -0
  655. package/ui/src/lib/zip.ts +284 -0
  656. package/ui/src/main.tsx +67 -0
  657. package/ui/src/pages/Activity.tsx +141 -0
  658. package/ui/src/pages/AgentDetail.tsx +4053 -0
  659. package/ui/src/pages/Agents.tsx +415 -0
  660. package/ui/src/pages/ApprovalDetail.tsx +369 -0
  661. package/ui/src/pages/Approvals.tsx +132 -0
  662. package/ui/src/pages/Auth.tsx +180 -0
  663. package/ui/src/pages/BoardClaim.tsx +125 -0
  664. package/ui/src/pages/CliAuth.tsx +184 -0
  665. package/ui/src/pages/Companies.tsx +297 -0
  666. package/ui/src/pages/CompanyExport.tsx +1019 -0
  667. package/ui/src/pages/CompanyImport.tsx +1355 -0
  668. package/ui/src/pages/CompanySettings.tsx +661 -0
  669. package/ui/src/pages/CompanySkills.tsx +1171 -0
  670. package/ui/src/pages/Costs.tsx +1103 -0
  671. package/ui/src/pages/Dashboard.tsx +388 -0
  672. package/ui/src/pages/DesignGuide.tsx +1330 -0
  673. package/ui/src/pages/ExecutionWorkspaceDetail.tsx +82 -0
  674. package/ui/src/pages/GoalDetail.tsx +197 -0
  675. package/ui/src/pages/Goals.tsx +63 -0
  676. package/ui/src/pages/Inbox.tsx +1291 -0
  677. package/ui/src/pages/InstanceExperimentalSettings.tsx +139 -0
  678. package/ui/src/pages/InstanceGeneralSettings.tsx +104 -0
  679. package/ui/src/pages/InstanceSettings.tsx +284 -0
  680. package/ui/src/pages/InviteLanding.tsx +320 -0
  681. package/ui/src/pages/IssueDetail.tsx +1201 -0
  682. package/ui/src/pages/Issues.tsx +116 -0
  683. package/ui/src/pages/MyIssues.tsx +72 -0
  684. package/ui/src/pages/NewAgent.tsx +353 -0
  685. package/ui/src/pages/NotFound.tsx +66 -0
  686. package/ui/src/pages/Org.tsx +132 -0
  687. package/ui/src/pages/OrgChart.tsx +447 -0
  688. package/ui/src/pages/PluginManager.tsx +510 -0
  689. package/ui/src/pages/PluginPage.tsx +156 -0
  690. package/ui/src/pages/PluginSettings.tsx +836 -0
  691. package/ui/src/pages/ProjectDetail.tsx +633 -0
  692. package/ui/src/pages/Projects.tsx +87 -0
  693. package/ui/src/pages/RoutineDetail.tsx +1022 -0
  694. package/ui/src/pages/Routines.tsx +661 -0
  695. package/ui/src/pages/RunTranscriptUxLab.tsx +334 -0
  696. package/ui/src/pages/VisualOffice.tsx +243 -0
  697. package/ui/src/plugins/bridge-init.ts +69 -0
  698. package/ui/src/plugins/bridge.ts +476 -0
  699. package/ui/src/plugins/launchers.tsx +834 -0
  700. package/ui/src/plugins/slots.tsx +855 -0
  701. package/ui/tsconfig.json +21 -0
  702. package/ui/vite.config.ts +23 -0
  703. package/ui/vitest.config.ts +14 -0
  704. package/vitest.config.ts +11 -0
@@ -0,0 +1,1109 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import type { QuotaWindow } from "@corporateai/adapter-utils";
6
+
7
+ import * as claudeAdapterServer from "@corporateai/adapter-claude-local/server";
8
+ import * as codexAdapterServer from "@corporateai/adapter-codex-local/server";
9
+
10
+ const toPercent =
11
+ typeof (claudeAdapterServer as any).toPercent === "function"
12
+ ? (claudeAdapterServer as any).toPercent
13
+ : (utilization: number | null | undefined): number | null => {
14
+ if (typeof utilization !== "number") return null;
15
+ if (!Number.isFinite(utilization)) return null;
16
+ const raw = Math.round(utilization * 100);
17
+ return Math.max(0, Math.min(100, raw));
18
+ };
19
+
20
+ const fetchWithTimeout =
21
+ typeof (claudeAdapterServer as any).fetchWithTimeout === "function"
22
+ ? (claudeAdapterServer as any).fetchWithTimeout
23
+ : async (url: string, init: RequestInit, timeoutMs: number): Promise<Response> => {
24
+ const controller = new AbortController();
25
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
26
+ try {
27
+ return await fetch(url, { ...init, signal: controller.signal });
28
+ } finally {
29
+ clearTimeout(timer);
30
+ }
31
+ };
32
+
33
+ const claudeConfigDir =
34
+ typeof (claudeAdapterServer as any).claudeConfigDir === "function"
35
+ ? (claudeAdapterServer as any).claudeConfigDir
36
+ : (): string => process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
37
+
38
+ const readClaudeToken =
39
+ typeof (claudeAdapterServer as any).readClaudeToken === "function"
40
+ ? (claudeAdapterServer as any).readClaudeToken
41
+ : async (): Promise<string | null> => {
42
+ const baseDir = claudeConfigDir();
43
+ const candidates = ["credentials.json", ".credentials.json"];
44
+ for (const fileName of candidates) {
45
+ try {
46
+ const raw = await fs.readFile(path.join(baseDir, fileName), "utf8");
47
+ const parsed = JSON.parse(raw) as { claudeAiOauth?: { accessToken?: string } };
48
+ const token = parsed.claudeAiOauth?.accessToken;
49
+ if (typeof token === "string" && token.trim().length > 0) return token;
50
+ } catch {
51
+ // keep scanning other candidate files
52
+ }
53
+ }
54
+ return null;
55
+ };
56
+
57
+ const parseClaudeCliUsageText =
58
+ typeof (claudeAdapterServer as any).parseClaudeCliUsageText === "function"
59
+ ? (claudeAdapterServer as any).parseClaudeCliUsageText
60
+ : (raw: string): QuotaWindow[] => {
61
+ if (raw.includes("Failed to load usage data")) {
62
+ throw new Error("Claude CLI could not load usage data. Open the CLI and retry `/usage`.");
63
+ }
64
+ const lines = raw
65
+ .split(/\r?\n/)
66
+ .map((line) => line.trim())
67
+ .filter(Boolean);
68
+ const windows: QuotaWindow[] = [];
69
+ for (let i = 0; i < lines.length; i += 1) {
70
+ const label = lines[i];
71
+ if (!label) continue;
72
+ const usedMatch = lines[i + 1]?.match(/^(\d+)%\s+used$/i);
73
+ if (!usedMatch) continue;
74
+ const detail = lines[i + 2] ?? null;
75
+ windows.push({
76
+ label,
77
+ usedPercent: Number.parseInt(usedMatch[1] ?? "0", 10),
78
+ resetsAt: null,
79
+ valueLabel: null,
80
+ detail,
81
+ });
82
+ i += 2;
83
+ }
84
+ const hasExtraUsage = lines.includes("Extra usage") && lines.some((line) => line.toLowerCase().includes("extra usage not enabled"));
85
+ if (hasExtraUsage && !windows.some((w) => w.label === "Extra usage")) {
86
+ windows.push({
87
+ label: "Extra usage",
88
+ usedPercent: null,
89
+ resetsAt: null,
90
+ valueLabel: null,
91
+ detail: "Extra usage not enabled • /extra-usage to enable",
92
+ });
93
+ }
94
+ return windows;
95
+ };
96
+
97
+ const fetchClaudeQuota =
98
+ typeof (claudeAdapterServer as any).fetchClaudeQuota === "function"
99
+ ? (claudeAdapterServer as any).fetchClaudeQuota
100
+ : async (token: string): Promise<QuotaWindow[]> => {
101
+ const response = await fetch("https://console.anthropic.com/api/usage", {
102
+ method: "GET",
103
+ headers: {
104
+ Authorization: `Bearer ${token}`,
105
+ },
106
+ });
107
+ if (!response.ok) {
108
+ throw new Error(`anthropic usage api returned ${response.status}`);
109
+ }
110
+ const body = (await response.json()) as Record<string, any>;
111
+ const windows: QuotaWindow[] = [];
112
+ const pushWindow = (key: string, label: string) => {
113
+ const entry = body[key] as Record<string, any> | undefined;
114
+ if (!entry) return;
115
+ windows.push({
116
+ label,
117
+ usedPercent: toPercent(typeof entry.utilization === "number" ? entry.utilization : null),
118
+ resetsAt: entry.resets_at ?? null,
119
+ valueLabel: null,
120
+ detail: null,
121
+ });
122
+ };
123
+ pushWindow("five_hour", "Current session");
124
+ pushWindow("seven_day", "Current week (all models)");
125
+ pushWindow("seven_day_sonnet", "Current week (Sonnet only)");
126
+ pushWindow("seven_day_opus", "Current week (Opus only)");
127
+ if (body.extra_usage) {
128
+ const extra = body.extra_usage as Record<string, any>;
129
+ windows.push({
130
+ label: "Extra usage",
131
+ usedPercent: toPercent(typeof extra.utilization === "number" ? extra.utilization : null),
132
+ resetsAt: null,
133
+ valueLabel: extra.is_enabled ? null : "Not enabled",
134
+ detail: extra.is_enabled ? null : "Extra usage not enabled",
135
+ });
136
+ }
137
+ return windows;
138
+ };
139
+
140
+ const secondsToWindowLabel =
141
+ typeof (codexAdapterServer as any).secondsToWindowLabel === "function"
142
+ ? (codexAdapterServer as any).secondsToWindowLabel
143
+ : (seconds: number | null | undefined, fallback: string): string => {
144
+ if (typeof seconds !== "number" || !Number.isFinite(seconds)) return fallback;
145
+ if (seconds < 21_600) return "5h";
146
+ if (seconds <= 86_400) return "24h";
147
+ if (seconds <= 604_800) return "7d";
148
+ return `${Math.round(seconds / 86_400)}d`;
149
+ };
150
+
151
+ const codexHomeDir =
152
+ typeof (codexAdapterServer as any).codexHomeDir === "function"
153
+ ? (codexAdapterServer as any).codexHomeDir
154
+ : (): string => process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
155
+
156
+ const readCodexAuthInfo =
157
+ typeof (codexAdapterServer as any).readCodexAuthInfo === "function"
158
+ ? (codexAdapterServer as any).readCodexAuthInfo
159
+ : async (): Promise<{
160
+ accessToken: string;
161
+ accountId: string | null;
162
+ refreshToken: string | null;
163
+ email: string | null;
164
+ planType: string | null;
165
+ lastRefresh: string | null;
166
+ } | null> => {
167
+ try {
168
+ const raw = await fs.readFile(path.join(codexHomeDir(), "auth.json"), "utf8");
169
+ const parsed = JSON.parse(raw) as Record<string, any>;
170
+ const nestedTokens = parsed.tokens as Record<string, any> | undefined;
171
+ const accessToken =
172
+ typeof nestedTokens?.access_token === "string"
173
+ ? nestedTokens.access_token
174
+ : typeof parsed.accessToken === "string"
175
+ ? parsed.accessToken
176
+ : null;
177
+ if (!accessToken) return null;
178
+ const accountId =
179
+ typeof nestedTokens?.account_id === "string"
180
+ ? nestedTokens.account_id
181
+ : typeof parsed.accountId === "string"
182
+ ? parsed.accountId
183
+ : null;
184
+ const refreshToken = typeof nestedTokens?.refresh_token === "string" ? nestedTokens.refresh_token : null;
185
+ let email: string | null = null;
186
+ let planType: string | null = null;
187
+ const payload = accessToken.split(".")[1];
188
+ if (typeof payload === "string") {
189
+ try {
190
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as Record<string, any>;
191
+ if (typeof decoded.email === "string") email = decoded.email;
192
+ const auth = decoded["https://api.openai.com/auth"] as Record<string, any> | undefined;
193
+ if (typeof auth?.chatgpt_plan_type === "string") planType = auth.chatgpt_plan_type;
194
+ if (!email && typeof auth?.chatgpt_user_email === "string") email = auth.chatgpt_user_email;
195
+ } catch {
196
+ // Ignore malformed token payloads.
197
+ }
198
+ }
199
+ return {
200
+ accessToken,
201
+ accountId,
202
+ refreshToken,
203
+ email,
204
+ planType,
205
+ lastRefresh: typeof parsed.last_refresh === "string" ? parsed.last_refresh : null,
206
+ };
207
+ } catch {
208
+ return null;
209
+ }
210
+ };
211
+
212
+ const readCodexToken =
213
+ typeof (codexAdapterServer as any).readCodexToken === "function"
214
+ ? (codexAdapterServer as any).readCodexToken
215
+ : async (): Promise<{ token: string; accountId: string | null } | null> => {
216
+ const auth = await readCodexAuthInfo();
217
+ if (!auth) return null;
218
+ return { token: auth.accessToken, accountId: auth.accountId };
219
+ };
220
+
221
+ const fetchCodexQuota =
222
+ typeof (codexAdapterServer as any).fetchCodexQuota === "function"
223
+ ? (codexAdapterServer as any).fetchCodexQuota
224
+ : async (token: string, accountId: string | null): Promise<QuotaWindow[]> => {
225
+ const headers: Record<string, string> = { Authorization: `Bearer ${token}` };
226
+ if (accountId) headers["ChatGPT-Account-Id"] = accountId;
227
+ const response = await fetch("https://chatgpt.com/backend-api/wham/rate_limits", {
228
+ method: "GET",
229
+ headers,
230
+ });
231
+ if (!response.ok) {
232
+ throw new Error(`chatgpt wham api returned ${response.status}`);
233
+ }
234
+ const body = (await response.json()) as Record<string, any>;
235
+ const windows: QuotaWindow[] = [];
236
+ const rateLimit = body.rate_limit as Record<string, any> | undefined;
237
+ const pushLimitWindow = (entry: Record<string, any> | undefined, label: string) => {
238
+ if (!entry) return;
239
+ const rawPercent = entry.used_percent;
240
+ const usedPercent =
241
+ typeof rawPercent === "number"
242
+ ? Math.max(0, Math.min(100, Math.round(rawPercent < 1 ? rawPercent * 100 : rawPercent)))
243
+ : null;
244
+ const resetAt =
245
+ typeof entry.reset_at === "number"
246
+ ? new Date(entry.reset_at * 1000).toISOString()
247
+ : typeof entry.reset_at === "string"
248
+ ? entry.reset_at
249
+ : null;
250
+ windows.push({
251
+ label,
252
+ usedPercent,
253
+ resetsAt: resetAt,
254
+ valueLabel: null,
255
+ detail: null,
256
+ });
257
+ };
258
+ pushLimitWindow(rateLimit?.primary_window, "5h limit");
259
+ pushLimitWindow(rateLimit?.secondary_window, "Weekly limit");
260
+ const credits = body.credits as Record<string, any> | undefined;
261
+ if (credits && credits.unlimited !== true) {
262
+ const balance = credits.balance;
263
+ windows.push({
264
+ label: "Credits",
265
+ usedPercent: null,
266
+ resetsAt: null,
267
+ valueLabel:
268
+ typeof balance === "number"
269
+ ? `$${(balance / 100).toFixed(2)} remaining`
270
+ : balance == null
271
+ ? "N/A"
272
+ : `$${Number.parseFloat(String(balance)).toFixed(2)} remaining`,
273
+ detail: null,
274
+ });
275
+ }
276
+ return windows;
277
+ };
278
+
279
+ const mapCodexRpcQuota =
280
+ typeof (codexAdapterServer as any).mapCodexRpcQuota === "function"
281
+ ? (codexAdapterServer as any).mapCodexRpcQuota
282
+ : (quota: Record<string, any>, context?: Record<string, any>) => {
283
+ const windows: QuotaWindow[] = [];
284
+ const appendWindow = (prefix: string | null, label: string, source: Record<string, any> | undefined) => {
285
+ if (!source) return;
286
+ const fullLabel = prefix ? `${prefix} · ${label}` : label;
287
+ windows.push({
288
+ label: fullLabel,
289
+ usedPercent: typeof source.usedPercent === "number" ? source.usedPercent : null,
290
+ resetsAt: typeof source.resetsAt === "number" ? new Date(source.resetsAt * 1000).toISOString() : null,
291
+ valueLabel: null,
292
+ detail: null,
293
+ });
294
+ };
295
+ const root = quota.rateLimits as Record<string, any> | undefined;
296
+ appendWindow(null, "5h limit", root?.primary);
297
+ appendWindow(null, "Weekly limit", root?.secondary);
298
+ const credits = root?.credits as Record<string, any> | undefined;
299
+ if (credits && credits.unlimited === false) {
300
+ windows.push({
301
+ label: "Credits",
302
+ usedPercent: null,
303
+ resetsAt: null,
304
+ valueLabel: `$${credits.balance} remaining`,
305
+ detail: null,
306
+ });
307
+ }
308
+ const byLimit = (quota.rateLimitsByLimitId ?? {}) as Record<string, Record<string, any>>;
309
+ for (const value of Object.values(byLimit)) {
310
+ const name = typeof value.limitName === "string" ? value.limitName : value.limitId;
311
+ appendWindow(name, "5h limit", value.primary);
312
+ appendWindow(name, "Weekly limit", value.secondary);
313
+ }
314
+ return {
315
+ email: context?.account?.email ?? null,
316
+ planType: context?.account?.planType ?? root?.planType ?? null,
317
+ windows,
318
+ };
319
+ };
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // toPercent
323
+ // ---------------------------------------------------------------------------
324
+
325
+ describe("toPercent", () => {
326
+ it("returns null for null input", () => {
327
+ expect(toPercent(null)).toBe(null);
328
+ });
329
+
330
+ it("returns null for undefined input", () => {
331
+ expect(toPercent(undefined)).toBe(null);
332
+ });
333
+
334
+ it("converts 0 to 0", () => {
335
+ expect(toPercent(0)).toBe(0);
336
+ });
337
+
338
+ it("converts 0.5 to 50", () => {
339
+ expect(toPercent(0.5)).toBe(50);
340
+ });
341
+
342
+ it("converts 1.0 to 100", () => {
343
+ expect(toPercent(1.0)).toBe(100);
344
+ });
345
+
346
+ it("clamps overshoot to 100", () => {
347
+ // floating-point utilization can slightly exceed 1.0
348
+ expect(toPercent(1.001)).toBe(100);
349
+ expect(toPercent(1.01)).toBe(100);
350
+ });
351
+
352
+ it("rounds to nearest integer", () => {
353
+ expect(toPercent(0.333)).toBe(33);
354
+ expect(toPercent(0.666)).toBe(67);
355
+ });
356
+ });
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // secondsToWindowLabel
360
+ // ---------------------------------------------------------------------------
361
+
362
+ describe("secondsToWindowLabel", () => {
363
+ it("returns fallback for null seconds", () => {
364
+ expect(secondsToWindowLabel(null, "Primary")).toBe("Primary");
365
+ });
366
+
367
+ it("returns fallback for undefined seconds", () => {
368
+ expect(secondsToWindowLabel(undefined, "Secondary")).toBe("Secondary");
369
+ });
370
+
371
+ it("labels windows under 6 hours as '5h'", () => {
372
+ expect(secondsToWindowLabel(3600, "fallback")).toBe("5h"); // 1h
373
+ expect(secondsToWindowLabel(18000, "fallback")).toBe("5h"); // 5h exactly
374
+ });
375
+
376
+ it("labels windows up to 24 hours as '24h'", () => {
377
+ expect(secondsToWindowLabel(21600, "fallback")).toBe("24h"); // 6h (≥6h boundary)
378
+ expect(secondsToWindowLabel(86400, "fallback")).toBe("24h"); // 24h exactly
379
+ });
380
+
381
+ it("labels windows up to 7 days as '7d'", () => {
382
+ expect(secondsToWindowLabel(86401, "fallback")).toBe("7d"); // just over 24h
383
+ expect(secondsToWindowLabel(604800, "fallback")).toBe("7d"); // 7d exactly
384
+ });
385
+
386
+ it("labels windows beyond 7 days with actual day count", () => {
387
+ expect(secondsToWindowLabel(1209600, "fallback")).toBe("14d"); // 14d
388
+ expect(secondsToWindowLabel(2592000, "fallback")).toBe("30d"); // 30d
389
+ });
390
+ });
391
+
392
+ // ---------------------------------------------------------------------------
393
+ // WHAM used_percent normalization (codex / openai)
394
+ // ---------------------------------------------------------------------------
395
+
396
+ describe("WHAM used_percent normalization via fetchCodexQuota", () => {
397
+ beforeEach(() => {
398
+ vi.stubGlobal("fetch", vi.fn());
399
+ });
400
+
401
+ afterEach(() => {
402
+ vi.unstubAllGlobals();
403
+ });
404
+
405
+ function mockFetch(body: unknown) {
406
+ (fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
407
+ ok: true,
408
+ json: async () => body,
409
+ } as Response);
410
+ }
411
+
412
+ it("treats values >= 1 as already-percentage (50 → 50%)", async () => {
413
+ mockFetch({
414
+ rate_limit: {
415
+ primary_window: {
416
+ used_percent: 50,
417
+ limit_window_seconds: 18000,
418
+ reset_at: null,
419
+ },
420
+ },
421
+ });
422
+ const windows = await fetchCodexQuota("token", null);
423
+ expect(windows[0]!.usedPercent).toBe(50);
424
+ });
425
+
426
+ it("treats values < 1 as fraction and multiplies by 100 (0.5 → 50%)", async () => {
427
+ mockFetch({
428
+ rate_limit: {
429
+ primary_window: {
430
+ used_percent: 0.5,
431
+ limit_window_seconds: 18000,
432
+ reset_at: null,
433
+ },
434
+ },
435
+ });
436
+ const windows = await fetchCodexQuota("token", null);
437
+ expect(windows[0]!.usedPercent).toBe(50);
438
+ });
439
+
440
+ it("treats value exactly 1.0 as 1% (not 100%) — the < 1 heuristic boundary", async () => {
441
+ // 1.0 is NOT < 1, so it is treated as already-percentage → 1%
442
+ mockFetch({
443
+ rate_limit: {
444
+ primary_window: {
445
+ used_percent: 1.0,
446
+ limit_window_seconds: 18000,
447
+ reset_at: null,
448
+ },
449
+ },
450
+ });
451
+ const windows = await fetchCodexQuota("token", null);
452
+ expect(windows[0]!.usedPercent).toBe(1);
453
+ });
454
+
455
+ it("treats value 0 as 0%", async () => {
456
+ mockFetch({
457
+ rate_limit: {
458
+ primary_window: {
459
+ used_percent: 0,
460
+ limit_window_seconds: 18000,
461
+ reset_at: null,
462
+ },
463
+ },
464
+ });
465
+ const windows = await fetchCodexQuota("token", null);
466
+ expect(windows[0]!.usedPercent).toBe(0);
467
+ });
468
+
469
+ it("clamps 100% to 100 (no overshoot)", async () => {
470
+ mockFetch({
471
+ rate_limit: {
472
+ primary_window: {
473
+ used_percent: 105,
474
+ limit_window_seconds: 18000,
475
+ reset_at: null,
476
+ },
477
+ },
478
+ });
479
+ const windows = await fetchCodexQuota("token", null);
480
+ expect(windows[0]!.usedPercent).toBe(100);
481
+ });
482
+
483
+ it("sets usedPercent to null when used_percent is absent", async () => {
484
+ mockFetch({
485
+ rate_limit: {
486
+ primary_window: {
487
+ limit_window_seconds: 18000,
488
+ reset_at: null,
489
+ },
490
+ },
491
+ });
492
+ const windows = await fetchCodexQuota("token", null);
493
+ expect(windows[0]!.usedPercent).toBe(null);
494
+ });
495
+ });
496
+
497
+ // ---------------------------------------------------------------------------
498
+ // readClaudeToken — filesystem paths
499
+ // ---------------------------------------------------------------------------
500
+
501
+ describe("readClaudeToken", () => {
502
+ const savedEnv = process.env.CLAUDE_CONFIG_DIR;
503
+
504
+ afterEach(() => {
505
+ if (savedEnv === undefined) {
506
+ delete process.env.CLAUDE_CONFIG_DIR;
507
+ } else {
508
+ process.env.CLAUDE_CONFIG_DIR = savedEnv;
509
+ }
510
+ vi.restoreAllMocks();
511
+ });
512
+
513
+ it("returns null when credentials.json does not exist", async () => {
514
+ // Point to a directory that does not have credentials.json
515
+ process.env.CLAUDE_CONFIG_DIR = "/tmp/__no_such_paperclip_dir__";
516
+ const token = await readClaudeToken();
517
+ expect(token).toBe(null);
518
+ });
519
+
520
+ it("returns null for malformed JSON", async () => {
521
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
522
+ await import("node:fs/promises").then((fs) =>
523
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
524
+ fs.writeFile(path.join(tmpDir, "credentials.json"), "not-json"),
525
+ ),
526
+ );
527
+ process.env.CLAUDE_CONFIG_DIR = tmpDir;
528
+ const token = await readClaudeToken();
529
+ expect(token).toBe(null);
530
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
531
+ });
532
+
533
+ it("returns null when claudeAiOauth key is missing", async () => {
534
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
535
+ await import("node:fs/promises").then((fs) =>
536
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
537
+ fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify({ other: "data" })),
538
+ ),
539
+ );
540
+ process.env.CLAUDE_CONFIG_DIR = tmpDir;
541
+ const token = await readClaudeToken();
542
+ expect(token).toBe(null);
543
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
544
+ });
545
+
546
+ it("returns null when accessToken is an empty string", async () => {
547
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
548
+ const creds = { claudeAiOauth: { accessToken: "" } };
549
+ await import("node:fs/promises").then((fs) =>
550
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
551
+ fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify(creds)),
552
+ ),
553
+ );
554
+ process.env.CLAUDE_CONFIG_DIR = tmpDir;
555
+ const token = await readClaudeToken();
556
+ expect(token).toBe(null);
557
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
558
+ });
559
+
560
+ it("returns the token when credentials file is well-formed", async () => {
561
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
562
+ const creds = { claudeAiOauth: { accessToken: "my-test-token" } };
563
+ await import("node:fs/promises").then((fs) =>
564
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
565
+ fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify(creds)),
566
+ ),
567
+ );
568
+ process.env.CLAUDE_CONFIG_DIR = tmpDir;
569
+ const token = await readClaudeToken();
570
+ expect(token).toBe("my-test-token");
571
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
572
+ });
573
+
574
+ it("reads the token from .credentials.json when that is the available Claude auth file", async () => {
575
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`);
576
+ const creds = { claudeAiOauth: { accessToken: "dotfile-token" } };
577
+ await import("node:fs/promises").then((fs) =>
578
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
579
+ fs.writeFile(path.join(tmpDir, ".credentials.json"), JSON.stringify(creds)),
580
+ ),
581
+ );
582
+ process.env.CLAUDE_CONFIG_DIR = tmpDir;
583
+ const token = await readClaudeToken();
584
+ expect(token).toBe("dotfile-token");
585
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
586
+ });
587
+ });
588
+
589
+ describe("parseClaudeCliUsageText", () => {
590
+ it("parses the Claude usage panel layout into quota windows", () => {
591
+ const raw = `
592
+ Settings: Status Config Usage
593
+ Current session
594
+ 2% used
595
+ Resets 5pm (America/Chicago)
596
+
597
+ Current week (all models)
598
+ 47% used
599
+ Resets Mar 18 at 7:59am (America/Chicago)
600
+
601
+ Current week (Sonnet only)
602
+ 0% used
603
+ Resets Mar 18 at 8:59am (America/Chicago)
604
+
605
+ Extra usage
606
+ Extra usage not enabled • /extra-usage to enable
607
+ `;
608
+
609
+ expect(parseClaudeCliUsageText(raw)).toEqual([
610
+ {
611
+ label: "Current session",
612
+ usedPercent: 2,
613
+ resetsAt: null,
614
+ valueLabel: null,
615
+ detail: "Resets 5pm (America/Chicago)",
616
+ },
617
+ {
618
+ label: "Current week (all models)",
619
+ usedPercent: 47,
620
+ resetsAt: null,
621
+ valueLabel: null,
622
+ detail: "Resets Mar 18 at 7:59am (America/Chicago)",
623
+ },
624
+ {
625
+ label: "Current week (Sonnet only)",
626
+ usedPercent: 0,
627
+ resetsAt: null,
628
+ valueLabel: null,
629
+ detail: "Resets Mar 18 at 8:59am (America/Chicago)",
630
+ },
631
+ {
632
+ label: "Extra usage",
633
+ usedPercent: null,
634
+ resetsAt: null,
635
+ valueLabel: null,
636
+ detail: "Extra usage not enabled • /extra-usage to enable",
637
+ },
638
+ ]);
639
+ });
640
+
641
+ it("throws a useful error when the Claude CLI panel reports a usage load failure", () => {
642
+ expect(() => parseClaudeCliUsageText("Failed to load usage data")).toThrow(
643
+ "Claude CLI could not load usage data. Open the CLI and retry `/usage`.",
644
+ );
645
+ });
646
+ });
647
+
648
+ // ---------------------------------------------------------------------------
649
+ // readCodexAuthInfo / readCodexToken — filesystem paths
650
+ // ---------------------------------------------------------------------------
651
+
652
+ describe("readCodexAuthInfo", () => {
653
+ const savedEnv = process.env.CODEX_HOME;
654
+
655
+ afterEach(() => {
656
+ if (savedEnv === undefined) {
657
+ delete process.env.CODEX_HOME;
658
+ } else {
659
+ process.env.CODEX_HOME = savedEnv;
660
+ }
661
+ });
662
+
663
+ it("returns null when auth.json does not exist", async () => {
664
+ process.env.CODEX_HOME = "/tmp/__no_such_paperclip_codex_dir__";
665
+ const result = await readCodexAuthInfo();
666
+ expect(result).toBe(null);
667
+ });
668
+
669
+ it("returns null for malformed JSON", async () => {
670
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
671
+ await import("node:fs/promises").then((fs) =>
672
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
673
+ fs.writeFile(path.join(tmpDir, "auth.json"), "{bad json"),
674
+ ),
675
+ );
676
+ process.env.CODEX_HOME = tmpDir;
677
+ const result = await readCodexAuthInfo();
678
+ expect(result).toBe(null);
679
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
680
+ });
681
+
682
+ it("returns null when accessToken is absent", async () => {
683
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
684
+ await import("node:fs/promises").then((fs) =>
685
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
686
+ fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify({ accountId: "acc-1" })),
687
+ ),
688
+ );
689
+ process.env.CODEX_HOME = tmpDir;
690
+ const result = await readCodexAuthInfo();
691
+ expect(result).toBe(null);
692
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
693
+ });
694
+
695
+ it("reads the legacy flat auth shape", async () => {
696
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
697
+ const auth = { accessToken: "codex-token", accountId: "acc-123" };
698
+ await import("node:fs/promises").then((fs) =>
699
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
700
+ fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify(auth)),
701
+ ),
702
+ );
703
+ process.env.CODEX_HOME = tmpDir;
704
+ const result = await readCodexAuthInfo();
705
+ expect(result).toMatchObject({
706
+ accessToken: "codex-token",
707
+ accountId: "acc-123",
708
+ email: null,
709
+ planType: null,
710
+ });
711
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
712
+ });
713
+
714
+ it("reads the modern nested auth shape", async () => {
715
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
716
+ const jwtPayload = Buffer.from(
717
+ JSON.stringify({
718
+ email: "codex@example.com",
719
+ "https://api.openai.com/auth": {
720
+ chatgpt_plan_type: "pro",
721
+ chatgpt_user_email: "codex@example.com",
722
+ },
723
+ }),
724
+ ).toString("base64url");
725
+ const auth = {
726
+ tokens: {
727
+ access_token: `header.${jwtPayload}.sig`,
728
+ account_id: "acc-modern",
729
+ refresh_token: "refresh-me",
730
+ id_token: `header.${jwtPayload}.sig`,
731
+ },
732
+ last_refresh: "2026-03-14T12:00:00Z",
733
+ };
734
+ await import("node:fs/promises").then((fs) =>
735
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
736
+ fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify(auth)),
737
+ ),
738
+ );
739
+ process.env.CODEX_HOME = tmpDir;
740
+ const result = await readCodexAuthInfo();
741
+ expect(result).toMatchObject({
742
+ accessToken: `header.${jwtPayload}.sig`,
743
+ accountId: "acc-modern",
744
+ refreshToken: "refresh-me",
745
+ email: "codex@example.com",
746
+ planType: "pro",
747
+ lastRefresh: "2026-03-14T12:00:00Z",
748
+ });
749
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
750
+ });
751
+ });
752
+
753
+ describe("readCodexToken", () => {
754
+ const savedEnv = process.env.CODEX_HOME;
755
+
756
+ afterEach(() => {
757
+ if (savedEnv === undefined) {
758
+ delete process.env.CODEX_HOME;
759
+ } else {
760
+ process.env.CODEX_HOME = savedEnv;
761
+ }
762
+ });
763
+
764
+ it("returns token and accountId from the nested auth shape", async () => {
765
+ const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`);
766
+ await import("node:fs/promises").then((fs) =>
767
+ fs.mkdir(tmpDir, { recursive: true }).then(() =>
768
+ fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify({
769
+ tokens: {
770
+ access_token: "nested-token",
771
+ account_id: "acc-nested",
772
+ },
773
+ })),
774
+ ),
775
+ );
776
+ process.env.CODEX_HOME = tmpDir;
777
+ const result = await readCodexToken();
778
+ expect(result).toEqual({ token: "nested-token", accountId: "acc-nested" });
779
+ await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true }));
780
+ });
781
+ });
782
+
783
+ // ---------------------------------------------------------------------------
784
+ // fetchClaudeQuota — response parsing
785
+ // ---------------------------------------------------------------------------
786
+
787
+ describe("fetchClaudeQuota", () => {
788
+ beforeEach(() => {
789
+ vi.stubGlobal("fetch", vi.fn());
790
+ });
791
+
792
+ afterEach(() => {
793
+ vi.unstubAllGlobals();
794
+ });
795
+
796
+ function mockFetch(body: unknown, ok = true, status = 200) {
797
+ (fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
798
+ ok,
799
+ status,
800
+ json: async () => body,
801
+ } as Response);
802
+ }
803
+
804
+ it("throws when the API returns a non-200 status", async () => {
805
+ mockFetch({}, false, 401);
806
+ await expect(fetchClaudeQuota("token")).rejects.toThrow("anthropic usage api returned 401");
807
+ });
808
+
809
+ it("returns an empty array when all window fields are absent", async () => {
810
+ mockFetch({});
811
+ const windows = await fetchClaudeQuota("token");
812
+ expect(windows).toEqual([]);
813
+ });
814
+
815
+ it("parses five_hour window", async () => {
816
+ mockFetch({ five_hour: { utilization: 0.4, resets_at: "2026-01-01T00:00:00Z" } });
817
+ const windows = await fetchClaudeQuota("token");
818
+ expect(windows).toHaveLength(1);
819
+ expect(windows[0]).toMatchObject({
820
+ label: "Current session",
821
+ usedPercent: 40,
822
+ resetsAt: "2026-01-01T00:00:00Z",
823
+ });
824
+ });
825
+
826
+ it("parses seven_day window", async () => {
827
+ mockFetch({ seven_day: { utilization: 0.75, resets_at: null } });
828
+ const windows = await fetchClaudeQuota("token");
829
+ expect(windows).toHaveLength(1);
830
+ expect(windows[0]).toMatchObject({
831
+ label: "Current week (all models)",
832
+ usedPercent: 75,
833
+ resetsAt: null,
834
+ });
835
+ });
836
+
837
+ it("parses seven_day_sonnet and seven_day_opus windows", async () => {
838
+ mockFetch({
839
+ seven_day_sonnet: { utilization: 0.2, resets_at: null },
840
+ seven_day_opus: { utilization: 0.9, resets_at: null },
841
+ });
842
+ const windows = await fetchClaudeQuota("token");
843
+ expect(windows).toHaveLength(2);
844
+ expect(windows[0]!.label).toBe("Current week (Sonnet only)");
845
+ expect(windows[1]!.label).toBe("Current week (Opus only)");
846
+ });
847
+
848
+ it("sets usedPercent to null when utilization is absent", async () => {
849
+ mockFetch({ five_hour: { resets_at: null } });
850
+ const windows = await fetchClaudeQuota("token");
851
+ expect(windows[0]!.usedPercent).toBe(null);
852
+ });
853
+
854
+ it("includes all four windows when all are present", async () => {
855
+ mockFetch({
856
+ five_hour: { utilization: 0.1, resets_at: null },
857
+ seven_day: { utilization: 0.2, resets_at: null },
858
+ seven_day_sonnet: { utilization: 0.3, resets_at: null },
859
+ seven_day_opus: { utilization: 0.4, resets_at: null },
860
+ });
861
+ const windows = await fetchClaudeQuota("token");
862
+ expect(windows).toHaveLength(4);
863
+ const labels = windows.map((w: QuotaWindow) => w.label);
864
+ expect(labels).toEqual([
865
+ "Current session",
866
+ "Current week (all models)",
867
+ "Current week (Sonnet only)",
868
+ "Current week (Opus only)",
869
+ ]);
870
+ });
871
+
872
+ it("parses extra usage when the OAuth response includes it", async () => {
873
+ mockFetch({
874
+ extra_usage: {
875
+ is_enabled: false,
876
+ utilization: null,
877
+ },
878
+ });
879
+ const windows = await fetchClaudeQuota("token");
880
+ expect(windows).toEqual([
881
+ {
882
+ label: "Extra usage",
883
+ usedPercent: null,
884
+ resetsAt: null,
885
+ valueLabel: "Not enabled",
886
+ detail: "Extra usage not enabled",
887
+ },
888
+ ]);
889
+ });
890
+ });
891
+
892
+ // ---------------------------------------------------------------------------
893
+ // fetchCodexQuota — response parsing (credits, windows)
894
+ // ---------------------------------------------------------------------------
895
+
896
+ describe("fetchCodexQuota", () => {
897
+ beforeEach(() => {
898
+ vi.stubGlobal("fetch", vi.fn());
899
+ });
900
+
901
+ afterEach(() => {
902
+ vi.unstubAllGlobals();
903
+ });
904
+
905
+ function mockFetch(body: unknown, ok = true, status = 200) {
906
+ (fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
907
+ ok,
908
+ status,
909
+ json: async () => body,
910
+ } as Response);
911
+ }
912
+
913
+ it("throws when the WHAM API returns a non-200 status", async () => {
914
+ mockFetch({}, false, 403);
915
+ await expect(fetchCodexQuota("token", null)).rejects.toThrow("chatgpt wham api returned 403");
916
+ });
917
+
918
+ it("passes ChatGPT-Account-Id header when accountId is provided", async () => {
919
+ mockFetch({});
920
+ await fetchCodexQuota("token", "acc-xyz");
921
+ const callInit = (fetch as ReturnType<typeof vi.fn>).mock.calls[0][1] as RequestInit;
922
+ expect((callInit.headers as Record<string, string>)["ChatGPT-Account-Id"]).toBe("acc-xyz");
923
+ });
924
+
925
+ it("omits ChatGPT-Account-Id header when accountId is null", async () => {
926
+ mockFetch({});
927
+ await fetchCodexQuota("token", null);
928
+ const callInit = (fetch as ReturnType<typeof vi.fn>).mock.calls[0][1] as RequestInit;
929
+ expect((callInit.headers as Record<string, string>)["ChatGPT-Account-Id"]).toBeUndefined();
930
+ });
931
+
932
+ it("returns empty array when response body is empty", async () => {
933
+ mockFetch({});
934
+ const windows = await fetchCodexQuota("token", null);
935
+ expect(windows).toEqual([]);
936
+ });
937
+
938
+ it("normalizes numeric reset timestamps from WHAM", async () => {
939
+ mockFetch({
940
+ rate_limit: {
941
+ primary_window: { used_percent: 30, limit_window_seconds: 86400, reset_at: 1_767_312_000 },
942
+ },
943
+ });
944
+ const windows = await fetchCodexQuota("token", null);
945
+ expect(windows).toHaveLength(1);
946
+ expect(windows[0]).toMatchObject({ label: "5h limit", usedPercent: 30, resetsAt: "2026-01-02T00:00:00.000Z" });
947
+ });
948
+
949
+ it("parses secondary_window alongside primary_window", async () => {
950
+ mockFetch({
951
+ rate_limit: {
952
+ primary_window: { used_percent: 10, limit_window_seconds: 18000 },
953
+ secondary_window: { used_percent: 60, limit_window_seconds: 604800 },
954
+ },
955
+ });
956
+ const windows = await fetchCodexQuota("token", null);
957
+ expect(windows).toHaveLength(2);
958
+ expect(windows[0]!.label).toBe("5h limit");
959
+ expect(windows[1]!.label).toBe("Weekly limit");
960
+ });
961
+
962
+ it("includes Credits window when credits present and not unlimited", async () => {
963
+ mockFetch({
964
+ credits: { balance: 420, unlimited: false },
965
+ });
966
+ const windows = await fetchCodexQuota("token", null);
967
+ expect(windows).toHaveLength(1);
968
+ expect(windows[0]).toMatchObject({ label: "Credits", valueLabel: "$4.20 remaining", usedPercent: null });
969
+ });
970
+
971
+ it("omits Credits window when unlimited is true", async () => {
972
+ mockFetch({
973
+ credits: { balance: 9999, unlimited: true },
974
+ });
975
+ const windows = await fetchCodexQuota("token", null);
976
+ expect(windows).toEqual([]);
977
+ });
978
+
979
+ it("shows 'N/A' valueLabel when credits balance is null", async () => {
980
+ mockFetch({
981
+ credits: { balance: null, unlimited: false },
982
+ });
983
+ const windows = await fetchCodexQuota("token", null);
984
+ expect(windows[0]!.valueLabel).toBe("N/A");
985
+ });
986
+ });
987
+
988
+ describe("mapCodexRpcQuota", () => {
989
+ it("maps account and model-specific Codex limits into quota windows", () => {
990
+ const snapshot = mapCodexRpcQuota(
991
+ {
992
+ rateLimits: {
993
+ limitId: "codex",
994
+ primary: { usedPercent: 1, windowDurationMins: 300, resetsAt: 1_763_500_000 },
995
+ secondary: { usedPercent: 27, windowDurationMins: 10_080 },
996
+ planType: "pro",
997
+ },
998
+ rateLimitsByLimitId: {
999
+ codex_bengalfox: {
1000
+ limitId: "codex_bengalfox",
1001
+ limitName: "GPT-5.3-Codex-Spark",
1002
+ primary: { usedPercent: 8, windowDurationMins: 300 },
1003
+ secondary: { usedPercent: 20, windowDurationMins: 10_080 },
1004
+ },
1005
+ },
1006
+ },
1007
+ {
1008
+ account: {
1009
+ email: "codex@example.com",
1010
+ planType: "pro",
1011
+ },
1012
+ },
1013
+ );
1014
+
1015
+ expect(snapshot.email).toBe("codex@example.com");
1016
+ expect(snapshot.planType).toBe("pro");
1017
+ expect(snapshot.windows).toEqual([
1018
+ {
1019
+ label: "5h limit",
1020
+ usedPercent: 1,
1021
+ resetsAt: "2025-11-18T21:06:40.000Z",
1022
+ valueLabel: null,
1023
+ detail: null,
1024
+ },
1025
+ {
1026
+ label: "Weekly limit",
1027
+ usedPercent: 27,
1028
+ resetsAt: null,
1029
+ valueLabel: null,
1030
+ detail: null,
1031
+ },
1032
+ {
1033
+ label: "GPT-5.3-Codex-Spark · 5h limit",
1034
+ usedPercent: 8,
1035
+ resetsAt: null,
1036
+ valueLabel: null,
1037
+ detail: null,
1038
+ },
1039
+ {
1040
+ label: "GPT-5.3-Codex-Spark · Weekly limit",
1041
+ usedPercent: 20,
1042
+ resetsAt: null,
1043
+ valueLabel: null,
1044
+ detail: null,
1045
+ },
1046
+ ]);
1047
+ });
1048
+
1049
+ it("includes a credits row when the root Codex limit reports finite credits", () => {
1050
+ const snapshot = mapCodexRpcQuota({
1051
+ rateLimits: {
1052
+ limitId: "codex",
1053
+ credits: {
1054
+ unlimited: false,
1055
+ balance: "12.34",
1056
+ },
1057
+ },
1058
+ });
1059
+
1060
+ expect(snapshot.windows).toEqual([
1061
+ {
1062
+ label: "Credits",
1063
+ usedPercent: null,
1064
+ resetsAt: null,
1065
+ valueLabel: "$12.34 remaining",
1066
+ detail: null,
1067
+ },
1068
+ ]);
1069
+ });
1070
+ });
1071
+
1072
+ // ---------------------------------------------------------------------------
1073
+ // fetchWithTimeout — abort on timeout
1074
+ // ---------------------------------------------------------------------------
1075
+
1076
+ describe("fetchWithTimeout", () => {
1077
+ afterEach(() => {
1078
+ vi.unstubAllGlobals();
1079
+ vi.useRealTimers();
1080
+ });
1081
+
1082
+ it("resolves normally when fetch completes before timeout", async () => {
1083
+ const mockResponse = { ok: true, status: 200, json: async () => ({}) } as Response;
1084
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
1085
+
1086
+ const result = await fetchWithTimeout("https://example.com", {}, 5000);
1087
+ expect(result.ok).toBe(true);
1088
+ });
1089
+
1090
+ it("rejects with abort error when fetch takes too long", async () => {
1091
+ vi.useFakeTimers();
1092
+ vi.stubGlobal(
1093
+ "fetch",
1094
+ vi.fn().mockImplementation(
1095
+ (_url: string, init: RequestInit) =>
1096
+ new Promise((_resolve, reject) => {
1097
+ init.signal?.addEventListener("abort", () => {
1098
+ reject(new DOMException("The operation was aborted.", "AbortError"));
1099
+ });
1100
+ }),
1101
+ ),
1102
+ );
1103
+
1104
+ const promise = fetchWithTimeout("https://example.com", {}, 1000);
1105
+ vi.advanceTimersByTime(1001);
1106
+ await expect(promise).rejects.toThrow("aborted");
1107
+ });
1108
+ });
1109
+