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.
- package/.dockerignore +10 -0
- package/.env.example +3 -0
- package/.github/workflows/publish-cli.yml +49 -0
- package/.mailmap +1 -0
- package/AGENTS.md +148 -0
- package/CONTRIBUTING.md +75 -0
- package/Dockerfile +59 -0
- package/Dockerfile.onboard-smoke +42 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/cli/esbuild.config.mjs +11 -0
- package/cli/package.json +24 -0
- package/cli/scripts/build-cli.mjs +5 -0
- package/cli/src/index.ts +27 -0
- package/docker-compose.quickstart.yml +18 -0
- package/docker-compose.untrusted-review.yml +33 -0
- package/docker-compose.yml +38 -0
- package/package.json +56 -0
- package/patches/embedded-postgres@18.1.0-beta.16.patch +0 -0
- package/pnpm-workspace.yaml +4 -0
- package/releases/.gitkeep +0 -0
- package/releases/v0.0.1.md +36 -0
- package/releases/v0.2.7.md +15 -0
- package/releases/v0.3.0.md +54 -0
- package/releases/v0.3.1.md +55 -0
- package/releases/v2026.318.0.md +66 -0
- package/releases/v2026.325.0.md +78 -0
- package/report/2026-03-13-08-46-token-optimization-implementation.md +48 -0
- package/scripts/backup-db.sh +17 -0
- package/scripts/build-npm.sh +80 -0
- package/scripts/check-forbidden-tokens.mjs +115 -0
- package/scripts/clean-onboard-git.sh +14 -0
- package/scripts/clean-onboard-npm.sh +13 -0
- package/scripts/clean-onboard-ref.sh +86 -0
- package/scripts/create-github-release.sh +99 -0
- package/scripts/dev-runner-paths.mjs +38 -0
- package/scripts/dev-runner.mjs +606 -0
- package/scripts/docker-onboard-smoke.sh +306 -0
- package/scripts/ensure-plugin-build-deps.mjs +47 -0
- package/scripts/generate-company-assets.ts +365 -0
- package/scripts/generate-npm-package-json.mjs +113 -0
- package/scripts/generate-org-chart-images.ts +694 -0
- package/scripts/generate-org-chart-satori-comparison.ts +225 -0
- package/scripts/generate-ui-package-json.mjs +31 -0
- package/scripts/kill-dev.sh +71 -0
- package/scripts/migrate-inline-env-secrets.ts +126 -0
- package/scripts/prepare-server-ui-dist.sh +22 -0
- package/scripts/provision-worktree.sh +333 -0
- package/scripts/release-lib.sh +306 -0
- package/scripts/release-package-map.mjs +169 -0
- package/scripts/release.sh +312 -0
- package/scripts/rollback-latest.sh +111 -0
- package/scripts/smoke/openclaw-docker-ui.sh +329 -0
- package/scripts/smoke/openclaw-gateway-e2e.sh +954 -0
- package/scripts/smoke/openclaw-join.sh +295 -0
- package/scripts/smoke/openclaw-sse-standalone.sh +146 -0
- package/scripts/workspace-compat.mjs +60 -0
- package/server/CHANGELOG.md +130 -0
- package/server/package.json +96 -0
- package/server/scripts/copy-onboarding-assets.mjs +10 -0
- package/server/scripts/dev-watch.ts +33 -0
- package/server/src/__tests__/activity-routes.test.ts +70 -0
- package/server/src/__tests__/adapter-models.test.ts +105 -0
- package/server/src/__tests__/adapter-session-codecs.test.ts +194 -0
- package/server/src/__tests__/agent-auth-jwt.test.ts +79 -0
- package/server/src/__tests__/agent-instructions-routes.test.ts +318 -0
- package/server/src/__tests__/agent-instructions-service.test.ts +361 -0
- package/server/src/__tests__/agent-permissions-routes.test.ts +275 -0
- package/server/src/__tests__/agent-shortname-collision.test.ts +69 -0
- package/server/src/__tests__/agent-skill-contract.test.ts +50 -0
- package/server/src/__tests__/agent-skills-routes.test.ts +462 -0
- package/server/src/__tests__/app-hmr-port.test.ts +19 -0
- package/server/src/__tests__/approval-routes-idempotency.test.ts +110 -0
- package/server/src/__tests__/approvals-service.test.ts +107 -0
- package/server/src/__tests__/assets.test.ts +250 -0
- package/server/src/__tests__/attachment-types.test.ts +97 -0
- package/server/src/__tests__/board-mutation-guard.test.ts +105 -0
- package/server/src/__tests__/budgets-service.test.ts +311 -0
- package/server/src/__tests__/claude-local-adapter-environment.test.ts +92 -0
- package/server/src/__tests__/claude-local-adapter.test.ts +31 -0
- package/server/src/__tests__/claude-local-skill-sync.test.ts +111 -0
- package/server/src/__tests__/cli-auth-routes.test.ts +230 -0
- package/server/src/__tests__/codex-local-adapter-environment.test.ts +143 -0
- package/server/src/__tests__/codex-local-adapter.test.ts +253 -0
- package/server/src/__tests__/codex-local-execute.test.ts +391 -0
- package/server/src/__tests__/codex-local-skill-injection.test.ts +175 -0
- package/server/src/__tests__/codex-local-skill-sync.test.ts +123 -0
- package/server/src/__tests__/companies-route-path-guard.test.ts +56 -0
- package/server/src/__tests__/company-branding-route.test.ts +196 -0
- package/server/src/__tests__/company-portability-routes.test.ts +175 -0
- package/server/src/__tests__/company-portability.test.ts +2186 -0
- package/server/src/__tests__/company-skills-routes.test.ts +113 -0
- package/server/src/__tests__/company-skills.test.ts +229 -0
- package/server/src/__tests__/costs-service.test.ts +226 -0
- package/server/src/__tests__/cursor-local-adapter-environment.test.ts +196 -0
- package/server/src/__tests__/cursor-local-adapter.test.ts +406 -0
- package/server/src/__tests__/cursor-local-execute.test.ts +263 -0
- package/server/src/__tests__/cursor-local-skill-injection.test.ts +104 -0
- package/server/src/__tests__/cursor-local-skill-sync.test.ts +145 -0
- package/server/src/__tests__/dev-runner-paths.test.ts +25 -0
- package/server/src/__tests__/dev-server-status.test.ts +66 -0
- package/server/src/__tests__/dev-watch-ignore.test.ts +42 -0
- package/server/src/__tests__/documents.test.ts +29 -0
- package/server/src/__tests__/error-handler.test.ts +53 -0
- package/server/src/__tests__/execution-workspace-policy.test.ts +170 -0
- package/server/src/__tests__/forbidden-tokens.test.ts +77 -0
- package/server/src/__tests__/gemini-local-adapter-environment.test.ts +135 -0
- package/server/src/__tests__/gemini-local-adapter.test.ts +190 -0
- package/server/src/__tests__/gemini-local-execute.test.ts +172 -0
- package/server/src/__tests__/gemini-local-skill-sync.test.ts +90 -0
- package/server/src/__tests__/health.test.ts +16 -0
- package/server/src/__tests__/heartbeat-process-recovery.test.ts +256 -0
- package/server/src/__tests__/heartbeat-run-summary.test.ts +33 -0
- package/server/src/__tests__/heartbeat-workspace-session.test.ts +334 -0
- package/server/src/__tests__/helpers/embedded-postgres.ts +7 -0
- package/server/src/__tests__/hire-hook.test.ts +181 -0
- package/server/src/__tests__/instance-settings-routes.test.ts +156 -0
- package/server/src/__tests__/invite-accept-gateway-defaults.test.ts +119 -0
- package/server/src/__tests__/invite-accept-replay.test.ts +92 -0
- package/server/src/__tests__/invite-expiry.test.ts +10 -0
- package/server/src/__tests__/invite-join-grants.test.ts +57 -0
- package/server/src/__tests__/invite-join-manager.test.ts +33 -0
- package/server/src/__tests__/invite-onboarding-text.test.ts +116 -0
- package/server/src/__tests__/issue-comment-reopen-routes.test.ts +146 -0
- package/server/src/__tests__/issue-goal-fallback.test.ts +99 -0
- package/server/src/__tests__/issues-checkout-wakeup.test.ts +48 -0
- package/server/src/__tests__/issues-goal-context-routes.test.ts +187 -0
- package/server/src/__tests__/issues-service.test.ts +317 -0
- package/server/src/__tests__/issues-user-context.test.ts +113 -0
- package/server/src/__tests__/log-redaction.test.ts +74 -0
- package/server/src/__tests__/monthly-spend-service.test.ts +90 -0
- package/server/src/__tests__/normalize-agent-mention-token.test.ts +41 -0
- package/server/src/__tests__/openclaw-gateway-adapter.test.ts +626 -0
- package/server/src/__tests__/openclaw-invite-prompt-route.test.ts +192 -0
- package/server/src/__tests__/opencode-local-adapter-environment.test.ts +97 -0
- package/server/src/__tests__/opencode-local-adapter.test.ts +226 -0
- package/server/src/__tests__/opencode-local-skill-sync.test.ts +91 -0
- package/server/src/__tests__/paperclip-env.test.ts +58 -0
- package/server/src/__tests__/paperclip-skill-utils.test.ts +63 -0
- package/server/src/__tests__/pi-local-adapter-environment.test.ts +102 -0
- package/server/src/__tests__/pi-local-skill-sync.test.ts +95 -0
- package/server/src/__tests__/plugin-dev-watcher.test.ts +68 -0
- package/server/src/__tests__/plugin-worker-manager.test.ts +43 -0
- package/server/src/__tests__/private-hostname-guard.test.ts +56 -0
- package/server/src/__tests__/project-shortname-resolution.test.ts +45 -0
- package/server/src/__tests__/quota-windows-service.test.ts +56 -0
- package/server/src/__tests__/quota-windows.test.ts +1109 -0
- package/server/src/__tests__/redaction.test.ts +66 -0
- package/server/src/__tests__/routines-e2e.test.ts +276 -0
- package/server/src/__tests__/routines-routes.test.ts +271 -0
- package/server/src/__tests__/routines-service.test.ts +424 -0
- package/server/src/__tests__/storage-local-provider.test.ts +78 -0
- package/server/src/__tests__/ui-branding.test.ts +82 -0
- package/server/src/__tests__/work-products.test.ts +95 -0
- package/server/src/__tests__/workspace-runtime.test.ts +1131 -0
- package/server/src/__tests__/worktree-config.test.ts +426 -0
- package/server/src/adapters/codex-models.ts +105 -0
- package/server/src/adapters/cursor-models.ts +171 -0
- package/server/src/adapters/http/execute.ts +42 -0
- package/server/src/adapters/http/index.ts +21 -0
- package/server/src/adapters/http/test.ts +116 -0
- package/server/src/adapters/index.ts +18 -0
- package/server/src/adapters/process/execute.ts +77 -0
- package/server/src/adapters/process/index.ts +24 -0
- package/server/src/adapters/process/test.ts +89 -0
- package/server/src/adapters/registry.ts +225 -0
- package/server/src/adapters/server-utils-compat.ts +57 -0
- package/server/src/adapters/types.ts +30 -0
- package/server/src/adapters/utils.ts +48 -0
- package/server/src/agent-auth-jwt.ts +141 -0
- package/server/src/app.ts +321 -0
- package/server/src/attachment-types.ts +74 -0
- package/server/src/auth/better-auth.ts +148 -0
- package/server/src/board-claim.ts +150 -0
- package/server/src/config-file.ts +17 -0
- package/server/src/config.ts +260 -0
- package/server/src/dev-server-status.ts +103 -0
- package/server/src/dev-watch-ignore.ts +36 -0
- package/server/src/errors.ts +34 -0
- package/server/src/home-paths.ts +95 -0
- package/server/src/index.ts +799 -0
- package/server/src/log-redaction.ts +146 -0
- package/server/src/middleware/auth.ts +178 -0
- package/server/src/middleware/board-mutation-guard.ts +66 -0
- package/server/src/middleware/error-handler.ts +71 -0
- package/server/src/middleware/index.ts +3 -0
- package/server/src/middleware/logger.ts +90 -0
- package/server/src/middleware/private-hostname-guard.ts +92 -0
- package/server/src/middleware/validate.ts +9 -0
- package/server/src/onboarding-assets/ceo/AGENTS.md +54 -0
- package/server/src/onboarding-assets/ceo/HEARTBEAT.md +72 -0
- package/server/src/onboarding-assets/ceo/SOUL.md +33 -0
- package/server/src/onboarding-assets/ceo/TOOLS.md +3 -0
- package/server/src/onboarding-assets/default/AGENTS.md +3 -0
- package/server/src/paths.ts +34 -0
- package/server/src/realtime/live-events-ws.ts +274 -0
- package/server/src/redaction.ts +59 -0
- package/server/src/routes/access.ts +2888 -0
- package/server/src/routes/activity.ts +89 -0
- package/server/src/routes/agents.ts +2313 -0
- package/server/src/routes/approvals.ts +346 -0
- package/server/src/routes/assets.ts +341 -0
- package/server/src/routes/authz.ts +52 -0
- package/server/src/routes/companies.ts +343 -0
- package/server/src/routes/company-skills.ts +300 -0
- package/server/src/routes/costs.ts +335 -0
- package/server/src/routes/dashboard.ts +19 -0
- package/server/src/routes/execution-workspaces.ts +182 -0
- package/server/src/routes/goals.ts +107 -0
- package/server/src/routes/health.ts +94 -0
- package/server/src/routes/index.ts +17 -0
- package/server/src/routes/instance-settings.ts +95 -0
- package/server/src/routes/issues-checkout-wakeup.ts +14 -0
- package/server/src/routes/issues.ts +1680 -0
- package/server/src/routes/llms.ts +86 -0
- package/server/src/routes/org-chart-svg.ts +777 -0
- package/server/src/routes/plugin-ui-static.ts +497 -0
- package/server/src/routes/plugins.ts +2220 -0
- package/server/src/routes/projects.ts +295 -0
- package/server/src/routes/routines.ts +300 -0
- package/server/src/routes/secrets.ts +166 -0
- package/server/src/routes/sidebar-badges.ts +52 -0
- package/server/src/secrets/external-stub-providers.ts +32 -0
- package/server/src/secrets/local-encrypted-provider.ts +135 -0
- package/server/src/secrets/provider-registry.ts +31 -0
- package/server/src/secrets/types.ts +23 -0
- package/server/src/services/access.ts +381 -0
- package/server/src/services/activity-log.ts +95 -0
- package/server/src/services/activity.ts +164 -0
- package/server/src/services/agent-instructions.ts +735 -0
- package/server/src/services/agent-permissions.ts +27 -0
- package/server/src/services/agents.ts +694 -0
- package/server/src/services/approvals.ts +273 -0
- package/server/src/services/assets.ts +23 -0
- package/server/src/services/board-auth.ts +355 -0
- package/server/src/services/budgets.ts +959 -0
- package/server/src/services/companies.ts +313 -0
- package/server/src/services/company-export-readme.ts +173 -0
- package/server/src/services/company-portability.ts +4263 -0
- package/server/src/services/company-skills.ts +2356 -0
- package/server/src/services/costs.ts +365 -0
- package/server/src/services/cron.ts +373 -0
- package/server/src/services/dashboard.ts +110 -0
- package/server/src/services/default-agent-instructions.ts +27 -0
- package/server/src/services/documents.ts +434 -0
- package/server/src/services/execution-workspace-policy.ts +210 -0
- package/server/src/services/execution-workspaces.ts +100 -0
- package/server/src/services/finance.ts +135 -0
- package/server/src/services/goals.ts +81 -0
- package/server/src/services/heartbeat-run-summary.ts +35 -0
- package/server/src/services/heartbeat.ts +3863 -0
- package/server/src/services/hire-hook.ts +114 -0
- package/server/src/services/index.ts +32 -0
- package/server/src/services/instance-settings.ts +138 -0
- package/server/src/services/issue-approvals.ts +175 -0
- package/server/src/services/issue-assignment-wakeup.ts +48 -0
- package/server/src/services/issue-goal-fallback.ts +56 -0
- package/server/src/services/issues.ts +1828 -0
- package/server/src/services/live-events.ts +55 -0
- package/server/src/services/plugin-capability-validator.ts +450 -0
- package/server/src/services/plugin-config-validator.ts +55 -0
- package/server/src/services/plugin-dev-watcher.ts +339 -0
- package/server/src/services/plugin-event-bus.ts +413 -0
- package/server/src/services/plugin-host-service-cleanup.ts +59 -0
- package/server/src/services/plugin-host-services.ts +1132 -0
- package/server/src/services/plugin-job-coordinator.ts +261 -0
- package/server/src/services/plugin-job-scheduler.ts +753 -0
- package/server/src/services/plugin-job-store.ts +466 -0
- package/server/src/services/plugin-lifecycle.ts +822 -0
- package/server/src/services/plugin-loader.ts +1955 -0
- package/server/src/services/plugin-log-retention.ts +87 -0
- package/server/src/services/plugin-manifest-validator.ts +164 -0
- package/server/src/services/plugin-registry.ts +683 -0
- package/server/src/services/plugin-runtime-sandbox.ts +222 -0
- package/server/src/services/plugin-secrets-handler.ts +355 -0
- package/server/src/services/plugin-state-store.ts +238 -0
- package/server/src/services/plugin-stream-bus.ts +81 -0
- package/server/src/services/plugin-tool-dispatcher.ts +449 -0
- package/server/src/services/plugin-tool-registry.ts +450 -0
- package/server/src/services/plugin-worker-manager.ts +1343 -0
- package/server/src/services/projects.ts +860 -0
- package/server/src/services/quota-windows.ts +65 -0
- package/server/src/services/routines.ts +1269 -0
- package/server/src/services/run-log-store.ts +156 -0
- package/server/src/services/secrets.ts +370 -0
- package/server/src/services/sidebar-badges.ts +56 -0
- package/server/src/services/work-products.ts +124 -0
- package/server/src/services/workspace-operation-log-store.ts +156 -0
- package/server/src/services/workspace-operations.ts +262 -0
- package/server/src/services/workspace-runtime.ts +1565 -0
- package/server/src/startup-banner.ts +176 -0
- package/server/src/storage/index.ts +35 -0
- package/server/src/storage/local-disk-provider.ts +89 -0
- package/server/src/storage/provider-registry.ts +18 -0
- package/server/src/storage/s3-provider.ts +153 -0
- package/server/src/storage/service.ts +131 -0
- package/server/src/storage/types.ts +63 -0
- package/server/src/ui-branding.ts +217 -0
- package/server/src/version.ts +10 -0
- package/server/src/worktree-config.ts +468 -0
- package/server/tsconfig.json +9 -0
- package/server/vitest.config.ts +7 -0
- package/skills/paperclip/SKILL.md +365 -0
- package/skills/paperclip/references/api-reference.md +647 -0
- package/skills/paperclip/references/company-skills.md +193 -0
- package/skills/paperclip-create-agent/SKILL.md +142 -0
- package/skills/paperclip-create-agent/references/api-reference.md +105 -0
- package/skills/paperclip-create-plugin/SKILL.md +102 -0
- package/skills/para-memory-files/SKILL.md +104 -0
- package/skills/para-memory-files/references/schemas.md +35 -0
- package/tests/e2e/onboarding.spec.ts +142 -0
- package/tests/e2e/playwright.config.ts +35 -0
- package/tests/release-smoke/docker-auth-onboarding.spec.ts +141 -0
- package/tests/release-smoke/playwright.config.ts +28 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +18 -0
- package/ui/README.md +12 -0
- package/ui/components.json +21 -0
- package/ui/index.html +47 -0
- package/ui/package.json +73 -0
- package/ui/public/android-chrome-192x192.png +0 -0
- package/ui/public/android-chrome-512x512.png +0 -0
- package/ui/public/apple-touch-icon.png +0 -0
- package/ui/public/brands/opencode-logo-dark-square.svg +18 -0
- package/ui/public/brands/opencode-logo-light-square.svg +18 -0
- package/ui/public/favicon-16x16.png +0 -0
- package/ui/public/favicon-32x32.png +0 -0
- package/ui/public/favicon.ico +0 -0
- package/ui/public/favicon.svg +9 -0
- package/ui/public/site.webmanifest +30 -0
- package/ui/public/sprites/1-D-1.png +0 -0
- package/ui/public/sprites/1-D-2.png +0 -0
- package/ui/public/sprites/1-D-3.png +0 -0
- package/ui/public/sprites/1-L-1.png +0 -0
- package/ui/public/sprites/1-R-1.png +0 -0
- package/ui/public/sprites/10-D-1.png +0 -0
- package/ui/public/sprites/10-D-2.png +0 -0
- package/ui/public/sprites/10-D-3.png +0 -0
- package/ui/public/sprites/10-L-1.png +0 -0
- package/ui/public/sprites/10-R-1.png +0 -0
- package/ui/public/sprites/11-D-1.png +0 -0
- package/ui/public/sprites/11-D-2.png +0 -0
- package/ui/public/sprites/11-D-3.png +0 -0
- package/ui/public/sprites/11-L-1.png +0 -0
- package/ui/public/sprites/11-R-1.png +0 -0
- package/ui/public/sprites/12-D-1.png +0 -0
- package/ui/public/sprites/12-D-2.png +0 -0
- package/ui/public/sprites/12-D-3.png +0 -0
- package/ui/public/sprites/12-L-1.png +0 -0
- package/ui/public/sprites/12-R-1.png +0 -0
- package/ui/public/sprites/13-D-1.png +0 -0
- package/ui/public/sprites/13-D-2.png +0 -0
- package/ui/public/sprites/13-D-3.png +0 -0
- package/ui/public/sprites/13-L-1.png +0 -0
- package/ui/public/sprites/13-R-1.png +0 -0
- package/ui/public/sprites/14-D-1.png +0 -0
- package/ui/public/sprites/14-D-2.png +0 -0
- package/ui/public/sprites/14-D-3.png +0 -0
- package/ui/public/sprites/14-L-1.png +0 -0
- package/ui/public/sprites/14-R-1.png +0 -0
- package/ui/public/sprites/2-D-1.png +0 -0
- package/ui/public/sprites/2-D-2.png +0 -0
- package/ui/public/sprites/2-D-3.png +0 -0
- package/ui/public/sprites/2-L-1.png +0 -0
- package/ui/public/sprites/2-R-1.png +0 -0
- package/ui/public/sprites/3-D-1.png +0 -0
- package/ui/public/sprites/3-D-2.png +0 -0
- package/ui/public/sprites/3-D-3.png +0 -0
- package/ui/public/sprites/3-L-1.png +0 -0
- package/ui/public/sprites/3-R-1.png +0 -0
- package/ui/public/sprites/4-D-1.png +0 -0
- package/ui/public/sprites/4-D-2.png +0 -0
- package/ui/public/sprites/4-D-3.png +0 -0
- package/ui/public/sprites/4-L-1.png +0 -0
- package/ui/public/sprites/4-R-1.png +0 -0
- package/ui/public/sprites/5-D-1.png +0 -0
- package/ui/public/sprites/5-D-2.png +0 -0
- package/ui/public/sprites/5-D-3.png +0 -0
- package/ui/public/sprites/5-L-1.png +0 -0
- package/ui/public/sprites/5-R-1.png +0 -0
- package/ui/public/sprites/6-D-1.png +0 -0
- package/ui/public/sprites/6-D-2.png +0 -0
- package/ui/public/sprites/6-D-3.png +0 -0
- package/ui/public/sprites/6-L-1.png +0 -0
- package/ui/public/sprites/6-R-1.png +0 -0
- package/ui/public/sprites/7-D-1.png +0 -0
- package/ui/public/sprites/7-D-2.png +0 -0
- package/ui/public/sprites/7-D-3.png +0 -0
- package/ui/public/sprites/7-L-1.png +0 -0
- package/ui/public/sprites/7-R-1.png +0 -0
- package/ui/public/sprites/8-D-1.png +0 -0
- package/ui/public/sprites/8-D-2.png +0 -0
- package/ui/public/sprites/8-D-3.png +0 -0
- package/ui/public/sprites/8-L-1.png +0 -0
- package/ui/public/sprites/8-R-1.png +0 -0
- package/ui/public/sprites/9-D-1.png +0 -0
- package/ui/public/sprites/9-D-2.png +0 -0
- package/ui/public/sprites/9-D-3.png +0 -0
- package/ui/public/sprites/9-L-1.png +0 -0
- package/ui/public/sprites/9-R-1.png +0 -0
- package/ui/public/sprites/ceo-lobster.png +0 -0
- package/ui/public/sw.js +42 -0
- package/ui/public/worktree-favicon-16x16.png +0 -0
- package/ui/public/worktree-favicon-32x32.png +0 -0
- package/ui/public/worktree-favicon.ico +0 -0
- package/ui/public/worktree-favicon.svg +9 -0
- package/ui/src/App.tsx +354 -0
- package/ui/src/adapters/claude-local/config-fields.tsx +138 -0
- package/ui/src/adapters/claude-local/index.ts +13 -0
- package/ui/src/adapters/codex-local/config-fields.tsx +104 -0
- package/ui/src/adapters/codex-local/index.ts +13 -0
- package/ui/src/adapters/cursor/config-fields.tsx +49 -0
- package/ui/src/adapters/cursor/index.ts +13 -0
- package/ui/src/adapters/gemini-local/config-fields.tsx +51 -0
- package/ui/src/adapters/gemini-local/index.ts +13 -0
- package/ui/src/adapters/http/build-config.ts +9 -0
- package/ui/src/adapters/http/config-fields.tsx +38 -0
- package/ui/src/adapters/http/index.ts +12 -0
- package/ui/src/adapters/http/parse-stdout.ts +5 -0
- package/ui/src/adapters/index.ts +9 -0
- package/ui/src/adapters/local-workspace-runtime-fields.tsx +5 -0
- package/ui/src/adapters/openclaw-gateway/config-fields.tsx +237 -0
- package/ui/src/adapters/openclaw-gateway/index.ts +13 -0
- package/ui/src/adapters/opencode-local/config-fields.tsx +72 -0
- package/ui/src/adapters/opencode-local/index.ts +13 -0
- package/ui/src/adapters/pi-local/config-fields.tsx +49 -0
- package/ui/src/adapters/pi-local/index.ts +13 -0
- package/ui/src/adapters/process/build-config.ts +18 -0
- package/ui/src/adapters/process/config-fields.tsx +77 -0
- package/ui/src/adapters/process/index.ts +12 -0
- package/ui/src/adapters/process/parse-stdout.ts +5 -0
- package/ui/src/adapters/registry.ts +34 -0
- package/ui/src/adapters/runtime-json-fields.tsx +122 -0
- package/ui/src/adapters/transcript.test.ts +30 -0
- package/ui/src/adapters/transcript.ts +62 -0
- package/ui/src/adapters/types.ts +34 -0
- package/ui/src/api/access.ts +160 -0
- package/ui/src/api/activity.ts +37 -0
- package/ui/src/api/agents.ts +194 -0
- package/ui/src/api/approvals.ts +25 -0
- package/ui/src/api/assets.ts +30 -0
- package/ui/src/api/auth.ts +74 -0
- package/ui/src/api/budgets.ts +21 -0
- package/ui/src/api/client.ts +50 -0
- package/ui/src/api/companies.ts +59 -0
- package/ui/src/api/companySkills.ts +55 -0
- package/ui/src/api/costs.ts +60 -0
- package/ui/src/api/dashboard.ts +7 -0
- package/ui/src/api/execution-workspaces.ts +27 -0
- package/ui/src/api/goals.ts +12 -0
- package/ui/src/api/health.ts +41 -0
- package/ui/src/api/heartbeats.ts +62 -0
- package/ui/src/api/index.ts +18 -0
- package/ui/src/api/instanceSettings.ts +19 -0
- package/ui/src/api/issues.ts +115 -0
- package/ui/src/api/plugins.ts +424 -0
- package/ui/src/api/projects.ts +34 -0
- package/ui/src/api/routines.ts +59 -0
- package/ui/src/api/secrets.ts +26 -0
- package/ui/src/api/sidebarBadges.ts +7 -0
- package/ui/src/components/AccountingModelCard.tsx +69 -0
- package/ui/src/components/ActiveAgentsPanel.tsx +157 -0
- package/ui/src/components/ActivityCharts.tsx +264 -0
- package/ui/src/components/ActivityRow.tsx +147 -0
- package/ui/src/components/AgentActionButtons.tsx +51 -0
- package/ui/src/components/AgentConfigForm.tsx +1468 -0
- package/ui/src/components/AgentIconPicker.tsx +81 -0
- package/ui/src/components/AgentProperties.tsx +107 -0
- package/ui/src/components/ApprovalCard.tsx +107 -0
- package/ui/src/components/ApprovalPayload.tsx +134 -0
- package/ui/src/components/AsciiArtAnimation.tsx +355 -0
- package/ui/src/components/BillerSpendCard.tsx +146 -0
- package/ui/src/components/BreadcrumbBar.tsx +113 -0
- package/ui/src/components/BudgetIncidentCard.tsx +101 -0
- package/ui/src/components/BudgetPolicyCard.tsx +220 -0
- package/ui/src/components/BudgetSidebarMarker.tsx +13 -0
- package/ui/src/components/ClaudeSubscriptionPanel.tsx +141 -0
- package/ui/src/components/CodexSubscriptionPanel.tsx +158 -0
- package/ui/src/components/CommandPalette.tsx +239 -0
- package/ui/src/components/CommentThread.tsx +503 -0
- package/ui/src/components/CompanyPatternIcon.tsx +212 -0
- package/ui/src/components/CompanyRail.tsx +329 -0
- package/ui/src/components/CompanySwitcher.tsx +81 -0
- package/ui/src/components/CopyText.tsx +56 -0
- package/ui/src/components/DevRestartBanner.tsx +89 -0
- package/ui/src/components/EmptyState.tsx +27 -0
- package/ui/src/components/EntityRow.tsx +69 -0
- package/ui/src/components/FilterBar.tsx +39 -0
- package/ui/src/components/FinanceBillerCard.tsx +45 -0
- package/ui/src/components/FinanceKindCard.tsx +44 -0
- package/ui/src/components/FinanceTimelineCard.tsx +72 -0
- package/ui/src/components/GoalProperties.tsx +165 -0
- package/ui/src/components/GoalTree.tsx +118 -0
- package/ui/src/components/Identity.tsx +39 -0
- package/ui/src/components/InlineEditor.tsx +248 -0
- package/ui/src/components/InlineEntitySelector.tsx +206 -0
- package/ui/src/components/InstanceSidebar.tsx +53 -0
- package/ui/src/components/IssueDocumentsSection.tsx +892 -0
- package/ui/src/components/IssueProperties.tsx +621 -0
- package/ui/src/components/IssueRow.tsx +149 -0
- package/ui/src/components/IssueWorkspaceCard.tsx +404 -0
- package/ui/src/components/IssuesList.tsx +889 -0
- package/ui/src/components/JsonSchemaForm.tsx +1048 -0
- package/ui/src/components/KanbanBoard.tsx +275 -0
- package/ui/src/components/Layout.tsx +441 -0
- package/ui/src/components/LiveRunWidget.tsx +160 -0
- package/ui/src/components/MarkdownBody.test.tsx +50 -0
- package/ui/src/components/MarkdownBody.tsx +152 -0
- package/ui/src/components/MarkdownEditor.tsx +622 -0
- package/ui/src/components/MetricCard.tsx +53 -0
- package/ui/src/components/MobileBottomNav.tsx +123 -0
- package/ui/src/components/NewAgentDialog.tsx +223 -0
- package/ui/src/components/NewGoalDialog.tsx +283 -0
- package/ui/src/components/NewIssueDialog.tsx +1473 -0
- package/ui/src/components/NewProjectDialog.tsx +451 -0
- package/ui/src/components/OnboardingWizard.tsx +1392 -0
- package/ui/src/components/OpenCodeLogoIcon.tsx +22 -0
- package/ui/src/components/PackageFileTree.tsx +318 -0
- package/ui/src/components/PageSkeleton.tsx +180 -0
- package/ui/src/components/PageTabBar.tsx +45 -0
- package/ui/src/components/PathInstructionsModal.tsx +143 -0
- package/ui/src/components/PriorityIcon.tsx +77 -0
- package/ui/src/components/ProjectProperties.tsx +1127 -0
- package/ui/src/components/PropertiesPanel.tsx +29 -0
- package/ui/src/components/ProviderQuotaCard.tsx +417 -0
- package/ui/src/components/QuotaBar.tsx +65 -0
- package/ui/src/components/ReportsToPicker.tsx +127 -0
- package/ui/src/components/ScheduleEditor.tsx +344 -0
- package/ui/src/components/ScrollToBottom.tsx +79 -0
- package/ui/src/components/Sidebar.tsx +130 -0
- package/ui/src/components/SidebarAgents.tsx +146 -0
- package/ui/src/components/SidebarNavItem.tsx +92 -0
- package/ui/src/components/SidebarProjects.tsx +234 -0
- package/ui/src/components/SidebarSection.tsx +17 -0
- package/ui/src/components/StatusBadge.tsx +15 -0
- package/ui/src/components/StatusIcon.tsx +71 -0
- package/ui/src/components/SwipeToArchive.tsx +152 -0
- package/ui/src/components/ToastViewport.tsx +99 -0
- package/ui/src/components/WorktreeBanner.tsx +25 -0
- package/ui/src/components/agent-config-defaults.ts +31 -0
- package/ui/src/components/agent-config-primitives.tsx +476 -0
- package/ui/src/components/transcript/RunTranscriptView.test.tsx +84 -0
- package/ui/src/components/transcript/RunTranscriptView.tsx +1015 -0
- package/ui/src/components/transcript/useLiveRunTranscripts.ts +297 -0
- package/ui/src/components/ui/avatar.tsx +107 -0
- package/ui/src/components/ui/badge.tsx +48 -0
- package/ui/src/components/ui/breadcrumb.tsx +109 -0
- package/ui/src/components/ui/button.tsx +64 -0
- package/ui/src/components/ui/card.tsx +92 -0
- package/ui/src/components/ui/checkbox.tsx +32 -0
- package/ui/src/components/ui/collapsible.tsx +33 -0
- package/ui/src/components/ui/command.tsx +194 -0
- package/ui/src/components/ui/dialog.tsx +156 -0
- package/ui/src/components/ui/dropdown-menu.tsx +257 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/label.tsx +22 -0
- package/ui/src/components/ui/popover.tsx +88 -0
- package/ui/src/components/ui/scroll-area.tsx +56 -0
- package/ui/src/components/ui/select.tsx +188 -0
- package/ui/src/components/ui/separator.tsx +28 -0
- package/ui/src/components/ui/sheet.tsx +143 -0
- package/ui/src/components/ui/skeleton.tsx +13 -0
- package/ui/src/components/ui/tabs.tsx +89 -0
- package/ui/src/components/ui/textarea.tsx +18 -0
- package/ui/src/components/ui/tooltip.tsx +57 -0
- package/ui/src/components/visual-office/AgentAvatar.tsx +99 -0
- package/ui/src/components/visual-office/OfficeViewExact.tsx +417 -0
- package/ui/src/components/visual-office/i18n.ts +52 -0
- package/ui/src/components/visual-office/office-view/CliUsagePanel.tsx +240 -0
- package/ui/src/components/visual-office/office-view/VirtualPadOverlay.tsx +104 -0
- package/ui/src/components/visual-office/office-view/buildScene-break-room.ts +248 -0
- package/ui/src/components/visual-office/office-view/buildScene-ceo-hallway.ts +345 -0
- package/ui/src/components/visual-office/office-view/buildScene-department-agent.ts +242 -0
- package/ui/src/components/visual-office/office-view/buildScene-departments.ts +360 -0
- package/ui/src/components/visual-office/office-view/buildScene-final-layers.ts +113 -0
- package/ui/src/components/visual-office/office-view/buildScene-types.ts +91 -0
- package/ui/src/components/visual-office/office-view/buildScene.ts +232 -0
- package/ui/src/components/visual-office/office-view/drawing-core.ts +374 -0
- package/ui/src/components/visual-office/office-view/drawing-furniture-a.ts +338 -0
- package/ui/src/components/visual-office/office-view/drawing-furniture-b.ts +241 -0
- package/ui/src/components/visual-office/office-view/model.ts +301 -0
- package/ui/src/components/visual-office/office-view/officeTicker.ts +455 -0
- package/ui/src/components/visual-office/office-view/officeTickerRoomAndDelivery.ts +133 -0
- package/ui/src/components/visual-office/office-view/themes-locale.ts +460 -0
- package/ui/src/components/visual-office/office-view/useCliUsage.ts +37 -0
- package/ui/src/components/visual-office/office-view/useOfficeDeliveryEffects.ts +465 -0
- package/ui/src/components/visual-office/office-view/useOfficePixiRuntime.ts +282 -0
- package/ui/src/components/visual-office/types.ts +123 -0
- package/ui/src/context/BreadcrumbContext.tsx +44 -0
- package/ui/src/context/CompanyContext.tsx +151 -0
- package/ui/src/context/DialogContext.tsx +135 -0
- package/ui/src/context/LiveUpdatesProvider.test.ts +119 -0
- package/ui/src/context/LiveUpdatesProvider.tsx +760 -0
- package/ui/src/context/PanelContext.tsx +73 -0
- package/ui/src/context/SidebarContext.tsx +43 -0
- package/ui/src/context/ThemeContext.tsx +83 -0
- package/ui/src/context/ToastContext.tsx +172 -0
- package/ui/src/fixtures/runTranscriptFixtures.ts +226 -0
- package/ui/src/hooks/useAgentOrder.ts +105 -0
- package/ui/src/hooks/useAutosaveIndicator.ts +72 -0
- package/ui/src/hooks/useCompanyPageMemory.test.ts +90 -0
- package/ui/src/hooks/useCompanyPageMemory.ts +79 -0
- package/ui/src/hooks/useDateRange.ts +120 -0
- package/ui/src/hooks/useInboxBadge.ts +132 -0
- package/ui/src/hooks/useKeyboardShortcuts.ts +40 -0
- package/ui/src/hooks/useProjectOrder.ts +106 -0
- package/ui/src/index.css +770 -0
- package/ui/src/lib/agent-icons.ts +99 -0
- package/ui/src/lib/agent-order.ts +107 -0
- package/ui/src/lib/agent-skills-state.test.ts +90 -0
- package/ui/src/lib/agent-skills-state.ts +41 -0
- package/ui/src/lib/assignees.test.ts +92 -0
- package/ui/src/lib/assignees.ts +82 -0
- package/ui/src/lib/color-contrast.ts +107 -0
- package/ui/src/lib/company-export-selection.test.ts +41 -0
- package/ui/src/lib/company-export-selection.ts +57 -0
- package/ui/src/lib/company-page-memory.ts +65 -0
- package/ui/src/lib/company-portability-sidebar.test.ts +101 -0
- package/ui/src/lib/company-portability-sidebar.ts +62 -0
- package/ui/src/lib/company-routes.ts +88 -0
- package/ui/src/lib/company-selection.test.ts +34 -0
- package/ui/src/lib/company-selection.ts +18 -0
- package/ui/src/lib/groupBy.ts +11 -0
- package/ui/src/lib/inbox.test.ts +404 -0
- package/ui/src/lib/inbox.ts +292 -0
- package/ui/src/lib/instance-settings.test.ts +26 -0
- package/ui/src/lib/instance-settings.ts +25 -0
- package/ui/src/lib/issueDetailBreadcrumb.ts +24 -0
- package/ui/src/lib/legacy-agent-config.test.ts +40 -0
- package/ui/src/lib/legacy-agent-config.ts +17 -0
- package/ui/src/lib/mention-aware-link-node.test.ts +50 -0
- package/ui/src/lib/mention-aware-link-node.ts +67 -0
- package/ui/src/lib/mention-chips.ts +168 -0
- package/ui/src/lib/mention-deletion.test.ts +87 -0
- package/ui/src/lib/mention-deletion.ts +143 -0
- package/ui/src/lib/model-utils.ts +16 -0
- package/ui/src/lib/onboarding-goal.test.ts +22 -0
- package/ui/src/lib/onboarding-goal.ts +18 -0
- package/ui/src/lib/onboarding-launch.test.ts +131 -0
- package/ui/src/lib/onboarding-launch.ts +54 -0
- package/ui/src/lib/onboarding-route.test.ts +80 -0
- package/ui/src/lib/onboarding-route.ts +51 -0
- package/ui/src/lib/portable-files.ts +42 -0
- package/ui/src/lib/project-order.ts +71 -0
- package/ui/src/lib/queryKeys.ts +140 -0
- package/ui/src/lib/recent-assignees.ts +36 -0
- package/ui/src/lib/router.tsx +76 -0
- package/ui/src/lib/routine-trigger-patch.test.ts +72 -0
- package/ui/src/lib/routine-trigger-patch.ts +31 -0
- package/ui/src/lib/status-colors.ts +108 -0
- package/ui/src/lib/timeAgo.ts +31 -0
- package/ui/src/lib/utils.ts +168 -0
- package/ui/src/lib/worktree-branding.ts +65 -0
- package/ui/src/lib/zip.test.ts +289 -0
- package/ui/src/lib/zip.ts +284 -0
- package/ui/src/main.tsx +67 -0
- package/ui/src/pages/Activity.tsx +141 -0
- package/ui/src/pages/AgentDetail.tsx +4053 -0
- package/ui/src/pages/Agents.tsx +415 -0
- package/ui/src/pages/ApprovalDetail.tsx +369 -0
- package/ui/src/pages/Approvals.tsx +132 -0
- package/ui/src/pages/Auth.tsx +180 -0
- package/ui/src/pages/BoardClaim.tsx +125 -0
- package/ui/src/pages/CliAuth.tsx +184 -0
- package/ui/src/pages/Companies.tsx +297 -0
- package/ui/src/pages/CompanyExport.tsx +1019 -0
- package/ui/src/pages/CompanyImport.tsx +1355 -0
- package/ui/src/pages/CompanySettings.tsx +661 -0
- package/ui/src/pages/CompanySkills.tsx +1171 -0
- package/ui/src/pages/Costs.tsx +1103 -0
- package/ui/src/pages/Dashboard.tsx +388 -0
- package/ui/src/pages/DesignGuide.tsx +1330 -0
- package/ui/src/pages/ExecutionWorkspaceDetail.tsx +82 -0
- package/ui/src/pages/GoalDetail.tsx +197 -0
- package/ui/src/pages/Goals.tsx +63 -0
- package/ui/src/pages/Inbox.tsx +1291 -0
- package/ui/src/pages/InstanceExperimentalSettings.tsx +139 -0
- package/ui/src/pages/InstanceGeneralSettings.tsx +104 -0
- package/ui/src/pages/InstanceSettings.tsx +284 -0
- package/ui/src/pages/InviteLanding.tsx +320 -0
- package/ui/src/pages/IssueDetail.tsx +1201 -0
- package/ui/src/pages/Issues.tsx +116 -0
- package/ui/src/pages/MyIssues.tsx +72 -0
- package/ui/src/pages/NewAgent.tsx +353 -0
- package/ui/src/pages/NotFound.tsx +66 -0
- package/ui/src/pages/Org.tsx +132 -0
- package/ui/src/pages/OrgChart.tsx +447 -0
- package/ui/src/pages/PluginManager.tsx +510 -0
- package/ui/src/pages/PluginPage.tsx +156 -0
- package/ui/src/pages/PluginSettings.tsx +836 -0
- package/ui/src/pages/ProjectDetail.tsx +633 -0
- package/ui/src/pages/Projects.tsx +87 -0
- package/ui/src/pages/RoutineDetail.tsx +1022 -0
- package/ui/src/pages/Routines.tsx +661 -0
- package/ui/src/pages/RunTranscriptUxLab.tsx +334 -0
- package/ui/src/pages/VisualOffice.tsx +243 -0
- package/ui/src/plugins/bridge-init.ts +69 -0
- package/ui/src/plugins/bridge.ts +476 -0
- package/ui/src/plugins/launchers.tsx +834 -0
- package/ui/src/plugins/slots.tsx +855 -0
- package/ui/tsconfig.json +21 -0
- package/ui/vite.config.ts +23 -0
- package/ui/vitest.config.ts +14 -0
- 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
|
+
|