gentyr 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/antipattern-hunter.md +176 -0
- package/.claude/agents/code-reviewer.md +205 -0
- package/.claude/agents/code-writer.md +154 -0
- package/.claude/agents/deputy-cto.md +309 -0
- package/.claude/agents/feedback-agent.md +101 -0
- package/.claude/agents/investigator.md +136 -0
- package/.claude/agents/product-manager.md +97 -0
- package/.claude/agents/project-manager.md +116 -0
- package/.claude/agents/repo-hygiene-expert.md +626 -0
- package/.claude/agents/secret-manager.md +324 -0
- package/.claude/agents/test-writer.md +354 -0
- package/.claude/commands/configure-personas.md +144 -0
- package/.claude/commands/cto-report.md +36 -0
- package/.claude/commands/demo.md +89 -0
- package/.claude/commands/deputy-cto.md +345 -0
- package/.claude/commands/hotfix.md +31 -0
- package/.claude/commands/overdrive-gentyr.md +167 -0
- package/.claude/commands/product-manager.md +32 -0
- package/.claude/commands/push-migrations.md +86 -0
- package/.claude/commands/push-secrets.md +97 -0
- package/.claude/commands/services.json.example +30 -0
- package/.claude/commands/setup-gentyr.md +396 -0
- package/.claude/commands/show.md +42 -0
- package/.claude/commands/spawn-tasks.md +79 -0
- package/.claude/commands/toggle-automation-gentyr.md +75 -0
- package/.claude/commands/toggle-product-manager.md +19 -0
- package/.claude/commands/triage.md +69 -0
- package/.claude/hooks/README.md +686 -0
- package/.claude/hooks/__tests__/README.md +129 -0
- package/.claude/hooks/agent-tracker.js +434 -0
- package/.claude/hooks/antipattern-hunter-hook.js +401 -0
- package/.claude/hooks/api-key-watcher.js +289 -0
- package/.claude/hooks/block-no-verify.js +301 -0
- package/.claude/hooks/bypass-approval-hook.js +313 -0
- package/.claude/hooks/compliance-checker.js +1309 -0
- package/.claude/hooks/config-reader.js +143 -0
- package/.claude/hooks/credential-file-guard.js +1139 -0
- package/.claude/hooks/credential-health-check.js +168 -0
- package/.claude/hooks/credential-sync-hook.js +79 -0
- package/.claude/hooks/cto-notification-hook.js +656 -0
- package/.claude/hooks/feedback-launcher.js +424 -0
- package/.claude/hooks/feedback-orchestrator.js +367 -0
- package/.claude/hooks/gentyr-splash.js +47 -0
- package/.claude/hooks/gentyr-sync.js +389 -0
- package/.claude/hooks/hourly-automation.js +3340 -0
- package/.claude/hooks/key-sync.js +899 -0
- package/.claude/hooks/lib/approval-utils.js +731 -0
- package/.claude/hooks/lib/feature-branch-helper.js +102 -0
- package/.claude/hooks/lib/worktree-manager.js +330 -0
- package/.claude/hooks/mapping-validator.js +285 -0
- package/.claude/hooks/plan-executor.js +398 -0
- package/.claude/hooks/playwright-cli-guard.js +104 -0
- package/.claude/hooks/playwright-health-check.js +71 -0
- package/.claude/hooks/pre-commit-review.js +725 -0
- package/.claude/hooks/prompts/local-spec-enforcement.md +310 -0
- package/.claude/hooks/prompts/mapping-fix.md +92 -0
- package/.claude/hooks/prompts/mapping-review.md +140 -0
- package/.claude/hooks/prompts/schema-mapper.md +185 -0
- package/.claude/hooks/prompts/spec-enforcement.md +233 -0
- package/.claude/hooks/protected-action-approval-hook.js +336 -0
- package/.claude/hooks/protected-action-gate.js +562 -0
- package/.claude/hooks/protected-actions.json +208 -0
- package/.claude/hooks/protected-actions.json.template +122 -0
- package/.claude/hooks/quota-monitor.js +490 -0
- package/.claude/hooks/reporters/jest-failure-reporter.js +401 -0
- package/.claude/hooks/reporters/playwright-failure-reporter.js +446 -0
- package/.claude/hooks/reporters/vitest-failure-reporter.js +443 -0
- package/.claude/hooks/schema-mapper-hook.js +544 -0
- package/.claude/hooks/secret-leak-detector.js +216 -0
- package/.claude/hooks/session-reviver.js +514 -0
- package/.claude/hooks/slash-command-prefetch.js +1145 -0
- package/.claude/hooks/stale-work-detector.js +205 -0
- package/.claude/hooks/stop-continue-hook.js +414 -0
- package/.claude/hooks/todo-maintenance.js +522 -0
- package/.claude/hooks/todo-processing-prompt.md +75 -0
- package/.claude/hooks/usage-optimizer.js +791 -0
- package/.claude/mcp/README.md +246 -0
- package/.claude/settings.json.template +168 -0
- package/.mcp.json.template +207 -0
- package/CLAUDE.md +340 -0
- package/CLAUDE.md.gentyr-section +89 -0
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/cli/commands/init.js +471 -0
- package/cli/commands/migrate.js +132 -0
- package/cli/commands/protect.js +271 -0
- package/cli/commands/scaffold.js +48 -0
- package/cli/commands/status.js +133 -0
- package/cli/commands/sync.js +101 -0
- package/cli/commands/uninstall.js +207 -0
- package/cli/index.js +111 -0
- package/cli/lib/config-gen.js +214 -0
- package/cli/lib/resolve-framework.js +97 -0
- package/cli/lib/state.js +140 -0
- package/cli/lib/symlinks.js +260 -0
- package/docs/AUTOMATION-SYSTEMS.md +484 -0
- package/docs/BINARY-PATCHING.md +212 -0
- package/docs/CHANGELOG.md +2830 -0
- package/docs/CREDENTIAL-DETECTION.md +151 -0
- package/docs/CTO-DASHBOARD.md +476 -0
- package/docs/DEPLOYMENT-FLOW.md +477 -0
- package/docs/DEVELOPER.md +116 -0
- package/docs/Executive.md +372 -0
- package/docs/SECRET-PATHS.md +77 -0
- package/docs/SETUP-GUIDE.md +419 -0
- package/docs/STACK.md +109 -0
- package/docs/TESTING.md +440 -0
- package/docs/assets/claude-logo.svg +3 -0
- package/docs/sessions/2026-01-24-spec-suite-implementation.md +190 -0
- package/docs/sessions/2026-02-15-feedback-e2e-audit.md +484 -0
- package/docs/sessions/2026-02-20-credential-rotation-experiments.md +340 -0
- package/docs/sessions/TEST-COVERAGE-REPORT-2026-02-20.md +168 -0
- package/docs/shared/EPHEMERAL-STATE-FILES.md +115 -0
- package/docs/shared/PROTECTION-SYSTEM.md +341 -0
- package/husky/post-commit +10 -0
- package/husky/pre-commit +40 -0
- package/husky/pre-push +94 -0
- package/package.json +43 -0
- package/packages/cto-dashboard/package-lock.json +3510 -0
- package/packages/cto-dashboard/package.json +41 -0
- package/packages/cto-dashboard/pnpm-lock.yaml +2168 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts +220 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.js +376 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.js.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/index.d.ts +121 -0
- package/packages/mcp-servers/dist/__testUtils__/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/index.js +180 -0
- package/packages/mcp-servers/dist/__testUtils__/index.js.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts +84 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.js +309 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.js.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/index.d.ts +7 -0
- package/packages/mcp-servers/dist/agent-reports/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/index.js +8 -0
- package/packages/mcp-servers/dist/agent-reports/index.js.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/server.d.ts +22 -0
- package/packages/mcp-servers/dist/agent-reports/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/server.js +535 -0
- package/packages/mcp-servers/dist/agent-reports/server.js.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/types.d.ts +258 -0
- package/packages/mcp-servers/dist/agent-reports/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/types.js +81 -0
- package/packages/mcp-servers/dist/agent-reports/types.js.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/index.d.ts +5 -0
- package/packages/mcp-servers/dist/agent-tracker/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/index.js +5 -0
- package/packages/mcp-servers/dist/agent-tracker/index.js.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/server.d.ts +12 -0
- package/packages/mcp-servers/dist/agent-tracker/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/server.js +919 -0
- package/packages/mcp-servers/dist/agent-tracker/server.js.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/types.d.ts +328 -0
- package/packages/mcp-servers/dist/agent-tracker/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/types.js +128 -0
- package/packages/mcp-servers/dist/agent-tracker/types.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts +27 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js +167 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.d.ts +6 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.js +6 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.d.ts +13 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.js +959 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.d.ts +41 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.js +8 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.js.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/index.d.ts +8 -0
- package/packages/mcp-servers/dist/cloudflare/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/index.js +8 -0
- package/packages/mcp-servers/dist/cloudflare/index.js.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/server.d.ts +16 -0
- package/packages/mcp-servers/dist/cloudflare/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/server.js +253 -0
- package/packages/mcp-servers/dist/cloudflare/server.js.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/types.d.ts +141 -0
- package/packages/mcp-servers/dist/cloudflare/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/types.js +53 -0
- package/packages/mcp-servers/dist/cloudflare/types.js.map +1 -0
- package/packages/mcp-servers/dist/codecov/index.d.ts +7 -0
- package/packages/mcp-servers/dist/codecov/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/codecov/index.js +7 -0
- package/packages/mcp-servers/dist/codecov/index.js.map +1 -0
- package/packages/mcp-servers/dist/codecov/server.d.ts +21 -0
- package/packages/mcp-servers/dist/codecov/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/codecov/server.js +376 -0
- package/packages/mcp-servers/dist/codecov/server.js.map +1 -0
- package/packages/mcp-servers/dist/codecov/types.d.ts +269 -0
- package/packages/mcp-servers/dist/codecov/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/codecov/types.js +128 -0
- package/packages/mcp-servers/dist/codecov/types.js.map +1 -0
- package/packages/mcp-servers/dist/cto-report/index.d.ts +9 -0
- package/packages/mcp-servers/dist/cto-report/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-report/index.js +9 -0
- package/packages/mcp-servers/dist/cto-report/index.js.map +1 -0
- package/packages/mcp-servers/dist/cto-report/server.d.ts +14 -0
- package/packages/mcp-servers/dist/cto-report/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-report/server.js +859 -0
- package/packages/mcp-servers/dist/cto-report/server.js.map +1 -0
- package/packages/mcp-servers/dist/cto-report/types.d.ts +213 -0
- package/packages/mcp-servers/dist/cto-report/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-report/types.js +29 -0
- package/packages/mcp-servers/dist/cto-report/types.js.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/index.d.ts +7 -0
- package/packages/mcp-servers/dist/cto-reports/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/index.js +8 -0
- package/packages/mcp-servers/dist/cto-reports/index.js.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/server.d.ts +20 -0
- package/packages/mcp-servers/dist/cto-reports/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/server.js +538 -0
- package/packages/mcp-servers/dist/cto-reports/server.js.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/types.d.ts +236 -0
- package/packages/mcp-servers/dist/cto-reports/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/types.js +77 -0
- package/packages/mcp-servers/dist/cto-reports/types.js.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/index.d.ts +7 -0
- package/packages/mcp-servers/dist/deputy-cto/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/index.js +8 -0
- package/packages/mcp-servers/dist/deputy-cto/index.js.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/server.d.ts +23 -0
- package/packages/mcp-servers/dist/deputy-cto/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/server.js +1700 -0
- package/packages/mcp-servers/dist/deputy-cto/server.js.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/types.d.ts +439 -0
- package/packages/mcp-servers/dist/deputy-cto/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/types.js +102 -0
- package/packages/mcp-servers/dist/deputy-cto/types.js.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/index.d.ts +5 -0
- package/packages/mcp-servers/dist/elastic-logs/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/index.js +5 -0
- package/packages/mcp-servers/dist/elastic-logs/index.js.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/server.d.ts +18 -0
- package/packages/mcp-servers/dist/elastic-logs/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/server.js +259 -0
- package/packages/mcp-servers/dist/elastic-logs/server.js.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/types.d.ts +107 -0
- package/packages/mcp-servers/dist/elastic-logs/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/types.js +31 -0
- package/packages/mcp-servers/dist/elastic-logs/types.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.d.ts +2 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.js +2 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.d.ts +21 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.js +580 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.d.ts +331 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.js +40 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.d.ts +9 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.js +9 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.d.ts +36 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.js +392 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.d.ts +152 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.js +67 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.js.map +1 -0
- package/packages/mcp-servers/dist/github/index.d.ts +7 -0
- package/packages/mcp-servers/dist/github/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/github/index.js +7 -0
- package/packages/mcp-servers/dist/github/index.js.map +1 -0
- package/packages/mcp-servers/dist/github/server.d.ts +15 -0
- package/packages/mcp-servers/dist/github/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/github/server.js +686 -0
- package/packages/mcp-servers/dist/github/server.js.map +1 -0
- package/packages/mcp-servers/dist/github/types.d.ts +660 -0
- package/packages/mcp-servers/dist/github/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/github/types.js +209 -0
- package/packages/mcp-servers/dist/github/types.js.map +1 -0
- package/packages/mcp-servers/dist/index.d.ts +30 -0
- package/packages/mcp-servers/dist/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/index.js +32 -0
- package/packages/mcp-servers/dist/index.js.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.d.ts +5 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.js +5 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.js.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.d.ts +15 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.js +252 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.js.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.d.ts +74 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.js +20 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.js.map +1 -0
- package/packages/mcp-servers/dist/onepassword/index.d.ts +2 -0
- package/packages/mcp-servers/dist/onepassword/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/onepassword/index.js +2 -0
- package/packages/mcp-servers/dist/onepassword/index.js.map +1 -0
- package/packages/mcp-servers/dist/onepassword/server.d.ts +2 -0
- package/packages/mcp-servers/dist/onepassword/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/onepassword/server.js +159 -0
- package/packages/mcp-servers/dist/onepassword/server.js.map +1 -0
- package/packages/mcp-servers/dist/onepassword/types.d.ts +55 -0
- package/packages/mcp-servers/dist/onepassword/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/onepassword/types.js +22 -0
- package/packages/mcp-servers/dist/onepassword/types.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/helpers.d.ts +20 -0
- package/packages/mcp-servers/dist/playwright/helpers.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/helpers.js +31 -0
- package/packages/mcp-servers/dist/playwright/helpers.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/index.d.ts +5 -0
- package/packages/mcp-servers/dist/playwright/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/index.js +5 -0
- package/packages/mcp-servers/dist/playwright/index.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/server.d.ts +13 -0
- package/packages/mcp-servers/dist/playwright/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/server.js +1201 -0
- package/packages/mcp-servers/dist/playwright/server.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/types.d.ts +216 -0
- package/packages/mcp-servers/dist/playwright/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/types.js +172 -0
- package/packages/mcp-servers/dist/playwright/types.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts +39 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js +71 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.d.ts +5 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.js +5 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.d.ts +34 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.js +538 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.d.ts +305 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.js +123 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.js.map +1 -0
- package/packages/mcp-servers/dist/product-manager/server.d.ts +17 -0
- package/packages/mcp-servers/dist/product-manager/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/product-manager/server.js +690 -0
- package/packages/mcp-servers/dist/product-manager/server.js.map +1 -0
- package/packages/mcp-servers/dist/product-manager/types.d.ts +286 -0
- package/packages/mcp-servers/dist/product-manager/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/product-manager/types.js +99 -0
- package/packages/mcp-servers/dist/product-manager/types.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts +7 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.js +7 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts +19 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js +174 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts +35 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.js +465 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts +127 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.js +80 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.js.map +1 -0
- package/packages/mcp-servers/dist/render/index.d.ts +8 -0
- package/packages/mcp-servers/dist/render/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/render/index.js +8 -0
- package/packages/mcp-servers/dist/render/index.js.map +1 -0
- package/packages/mcp-servers/dist/render/server.d.ts +15 -0
- package/packages/mcp-servers/dist/render/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/render/server.js +428 -0
- package/packages/mcp-servers/dist/render/server.js.map +1 -0
- package/packages/mcp-servers/dist/render/types.d.ts +273 -0
- package/packages/mcp-servers/dist/render/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/render/types.js +102 -0
- package/packages/mcp-servers/dist/render/types.js.map +1 -0
- package/packages/mcp-servers/dist/resend/index.d.ts +7 -0
- package/packages/mcp-servers/dist/resend/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/resend/index.js +7 -0
- package/packages/mcp-servers/dist/resend/index.js.map +1 -0
- package/packages/mcp-servers/dist/resend/server.d.ts +15 -0
- package/packages/mcp-servers/dist/resend/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/resend/server.js +298 -0
- package/packages/mcp-servers/dist/resend/server.js.map +1 -0
- package/packages/mcp-servers/dist/resend/types.d.ts +222 -0
- package/packages/mcp-servers/dist/resend/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/resend/types.js +58 -0
- package/packages/mcp-servers/dist/resend/types.js.map +1 -0
- package/packages/mcp-servers/dist/review-queue/index.d.ts +6 -0
- package/packages/mcp-servers/dist/review-queue/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/review-queue/index.js +6 -0
- package/packages/mcp-servers/dist/review-queue/index.js.map +1 -0
- package/packages/mcp-servers/dist/review-queue/server.d.ts +17 -0
- package/packages/mcp-servers/dist/review-queue/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/review-queue/server.js +348 -0
- package/packages/mcp-servers/dist/review-queue/server.js.map +1 -0
- package/packages/mcp-servers/dist/review-queue/types.d.ts +162 -0
- package/packages/mcp-servers/dist/review-queue/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/review-queue/types.js +56 -0
- package/packages/mcp-servers/dist/review-queue/types.js.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/server.d.ts +19 -0
- package/packages/mcp-servers/dist/secret-sync/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/server.js +1139 -0
- package/packages/mcp-servers/dist/secret-sync/server.js.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/types.d.ts +442 -0
- package/packages/mcp-servers/dist/secret-sync/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/types.js +113 -0
- package/packages/mcp-servers/dist/secret-sync/types.js.map +1 -0
- package/packages/mcp-servers/dist/session-events/index.d.ts +5 -0
- package/packages/mcp-servers/dist/session-events/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-events/index.js +5 -0
- package/packages/mcp-servers/dist/session-events/index.js.map +1 -0
- package/packages/mcp-servers/dist/session-events/server.d.ts +11 -0
- package/packages/mcp-servers/dist/session-events/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-events/server.js +290 -0
- package/packages/mcp-servers/dist/session-events/server.js.map +1 -0
- package/packages/mcp-servers/dist/session-events/types.d.ts +213 -0
- package/packages/mcp-servers/dist/session-events/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-events/types.js +69 -0
- package/packages/mcp-servers/dist/session-events/types.js.map +1 -0
- package/packages/mcp-servers/dist/session-restart/index.d.ts +9 -0
- package/packages/mcp-servers/dist/session-restart/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-restart/index.js +9 -0
- package/packages/mcp-servers/dist/session-restart/index.js.map +1 -0
- package/packages/mcp-servers/dist/session-restart/server.d.ts +20 -0
- package/packages/mcp-servers/dist/session-restart/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-restart/server.js +411 -0
- package/packages/mcp-servers/dist/session-restart/server.js.map +1 -0
- package/packages/mcp-servers/dist/session-restart/types.d.ts +26 -0
- package/packages/mcp-servers/dist/session-restart/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-restart/types.js +16 -0
- package/packages/mcp-servers/dist/session-restart/types.js.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/index.d.ts +5 -0
- package/packages/mcp-servers/dist/setup-helper/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/index.js +5 -0
- package/packages/mcp-servers/dist/setup-helper/index.js.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/server.d.ts +14 -0
- package/packages/mcp-servers/dist/setup-helper/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/server.js +454 -0
- package/packages/mcp-servers/dist/setup-helper/server.js.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/types.d.ts +81 -0
- package/packages/mcp-servers/dist/setup-helper/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/types.js +41 -0
- package/packages/mcp-servers/dist/setup-helper/types.js.map +1 -0
- package/packages/mcp-servers/dist/shared/audited-server.d.ts +31 -0
- package/packages/mcp-servers/dist/shared/audited-server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/audited-server.js +126 -0
- package/packages/mcp-servers/dist/shared/audited-server.js.map +1 -0
- package/packages/mcp-servers/dist/shared/constants.d.ts +26 -0
- package/packages/mcp-servers/dist/shared/constants.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/constants.js +41 -0
- package/packages/mcp-servers/dist/shared/constants.js.map +1 -0
- package/packages/mcp-servers/dist/shared/index.d.ts +6 -0
- package/packages/mcp-servers/dist/shared/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/index.js +6 -0
- package/packages/mcp-servers/dist/shared/index.js.map +1 -0
- package/packages/mcp-servers/dist/shared/readonly-db.d.ts +11 -0
- package/packages/mcp-servers/dist/shared/readonly-db.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/readonly-db.js +47 -0
- package/packages/mcp-servers/dist/shared/readonly-db.js.map +1 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.d.ts +20 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.js +65 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.js.map +1 -0
- package/packages/mcp-servers/dist/shared/server.d.ts +86 -0
- package/packages/mcp-servers/dist/shared/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/server.js +291 -0
- package/packages/mcp-servers/dist/shared/server.js.map +1 -0
- package/packages/mcp-servers/dist/shared/types.d.ts +113 -0
- package/packages/mcp-servers/dist/shared/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/types.js +36 -0
- package/packages/mcp-servers/dist/shared/types.js.map +1 -0
- package/packages/mcp-servers/dist/show/server.d.ts +12 -0
- package/packages/mcp-servers/dist/show/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/show/server.js +97 -0
- package/packages/mcp-servers/dist/show/server.js.map +1 -0
- package/packages/mcp-servers/dist/show/types.d.ts +19 -0
- package/packages/mcp-servers/dist/show/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/show/types.js +32 -0
- package/packages/mcp-servers/dist/show/types.js.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/index.d.ts +5 -0
- package/packages/mcp-servers/dist/specs-browser/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/index.js +5 -0
- package/packages/mcp-servers/dist/specs-browser/index.js.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/server.d.ts +13 -0
- package/packages/mcp-servers/dist/specs-browser/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/server.js +692 -0
- package/packages/mcp-servers/dist/specs-browser/server.js.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/types.d.ts +337 -0
- package/packages/mcp-servers/dist/specs-browser/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/types.js +134 -0
- package/packages/mcp-servers/dist/specs-browser/types.js.map +1 -0
- package/packages/mcp-servers/dist/supabase/index.d.ts +10 -0
- package/packages/mcp-servers/dist/supabase/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/supabase/index.js +10 -0
- package/packages/mcp-servers/dist/supabase/index.js.map +1 -0
- package/packages/mcp-servers/dist/supabase/server.d.ts +20 -0
- package/packages/mcp-servers/dist/supabase/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/supabase/server.js +451 -0
- package/packages/mcp-servers/dist/supabase/server.js.map +1 -0
- package/packages/mcp-servers/dist/supabase/types.d.ts +196 -0
- package/packages/mcp-servers/dist/supabase/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/supabase/types.js +76 -0
- package/packages/mcp-servers/dist/supabase/types.js.map +1 -0
- package/packages/mcp-servers/dist/todo-db/index.d.ts +5 -0
- package/packages/mcp-servers/dist/todo-db/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/todo-db/index.js +5 -0
- package/packages/mcp-servers/dist/todo-db/index.js.map +1 -0
- package/packages/mcp-servers/dist/todo-db/server.d.ts +13 -0
- package/packages/mcp-servers/dist/todo-db/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/todo-db/server.js +649 -0
- package/packages/mcp-servers/dist/todo-db/server.js.map +1 -0
- package/packages/mcp-servers/dist/todo-db/types.d.ts +225 -0
- package/packages/mcp-servers/dist/todo-db/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/todo-db/types.js +69 -0
- package/packages/mcp-servers/dist/todo-db/types.js.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/index.d.ts +7 -0
- package/packages/mcp-servers/dist/user-feedback/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/index.js +8 -0
- package/packages/mcp-servers/dist/user-feedback/index.js.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/server.d.ts +25 -0
- package/packages/mcp-servers/dist/user-feedback/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/server.js +914 -0
- package/packages/mcp-servers/dist/user-feedback/server.js.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/types.d.ts +415 -0
- package/packages/mcp-servers/dist/user-feedback/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/types.js +132 -0
- package/packages/mcp-servers/dist/user-feedback/types.js.map +1 -0
- package/packages/mcp-servers/dist/vercel/index.d.ts +9 -0
- package/packages/mcp-servers/dist/vercel/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/vercel/index.js +9 -0
- package/packages/mcp-servers/dist/vercel/index.js.map +1 -0
- package/packages/mcp-servers/dist/vercel/server.d.ts +17 -0
- package/packages/mcp-servers/dist/vercel/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/vercel/server.js +265 -0
- package/packages/mcp-servers/dist/vercel/server.js.map +1 -0
- package/packages/mcp-servers/dist/vercel/types.d.ts +189 -0
- package/packages/mcp-servers/dist/vercel/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/vercel/types.js +65 -0
- package/packages/mcp-servers/dist/vercel/types.js.map +1 -0
- package/packages/mcp-servers/package-lock.json +3765 -0
- package/packages/mcp-servers/package.json +64 -0
- package/packages/mcp-servers/test/reporters/test-failure-reporter.ts +372 -0
- package/packages/mcp-servers/vitest.config.ts +27 -0
- package/scripts/__tests__/README.md +163 -0
- package/scripts/apply-credential-hardening.sh +271 -0
- package/scripts/credential-providers/manual.js +56 -0
- package/scripts/credential-providers/onepassword.js +85 -0
- package/scripts/credential-providers/provider-interface.js +104 -0
- package/scripts/encrypt-credential.js +337 -0
- package/scripts/feedback-launcher.js +338 -0
- package/scripts/feedback-orchestrator.js +373 -0
- package/scripts/fix-mcp-launcher-issues.sh +97 -0
- package/scripts/force-spawn-tasks.js +651 -0
- package/scripts/force-triage-reports.js +560 -0
- package/scripts/generate-protected-actions-spec.js +142 -0
- package/scripts/generate-proxy-certs.sh +158 -0
- package/scripts/grant-chrome-ext-permissions.sh +242 -0
- package/scripts/mcp-launcher.js +125 -0
- package/scripts/merge-settings.cjs +167 -0
- package/scripts/patch-clawd.py +844 -0
- package/scripts/patch-credential-cache.py +313 -0
- package/scripts/patches/credential-file-guard-patched.mjs +573 -0
- package/scripts/patches/credential-file-guard.js.patched +573 -0
- package/scripts/patches/verify-tokenizer.mjs +132 -0
- package/scripts/protect-framework.sh +478 -0
- package/scripts/readme-chrome.template +12 -0
- package/scripts/reap-completed-agents.js +439 -0
- package/scripts/reinstall.sh +86 -0
- package/scripts/resign-node.sh +185 -0
- package/scripts/rotation-proxy.js +656 -0
- package/scripts/rotation-stress-monitor.mjs +862 -0
- package/scripts/setup-automation-service.sh +648 -0
- package/scripts/setup-check.js +251 -0
- package/scripts/watch-claude-version.js +142 -0
- package/specs/framework/CORE-INVARIANTS.md +161 -0
- package/specs/patterns/AGENT-PATTERNS.md +223 -0
- package/specs/patterns/HOOK-PATTERNS.md +242 -0
- package/specs/patterns/MCP-SERVER-PATTERNS.md +144 -0
- package/templates/config/gitignore.template +14 -0
- package/templates/config/merge-chain-check.yml.template +51 -0
- package/templates/config/package.json.template +18 -0
- package/templates/config/pnpm-workspace.yaml +5 -0
- package/templates/config/services.json.template +18 -0
- package/templates/config/tsconfig.base.json +17 -0
- package/templates/scaffold/integrations/_template/.gitkeep +0 -0
- package/templates/scaffold/packages/logger/package.json +17 -0
- package/templates/scaffold/packages/logger/src/logger.ts +44 -0
- package/templates/scaffold/packages/shared/package.json +17 -0
- package/templates/scaffold/packages/shared/src/errors.ts +43 -0
- package/templates/scaffold/products/_product/apps/backend/package.json +21 -0
- package/templates/scaffold/products/_product/apps/backend/src/index.ts +17 -0
- package/templates/scaffold/products/_product/apps/extension/.gitkeep +0 -0
- package/templates/scaffold/products/_product/apps/web/.gitkeep +0 -0
- package/templates/scaffold/specs/global/.gitkeep +0 -0
- package/templates/scaffold/specs/local/.gitkeep +0 -0
- package/templates/scaffold/specs/reference/.gitkeep +0 -0
- package/version.json +15 -0
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key Sync - Shared module for multi-source credential detection and rotation state management
|
|
3
|
+
*
|
|
4
|
+
* Discovers Claude API keys from multiple sources (env var, macOS Keychain,
|
|
5
|
+
* credentials file) and maintains a user-level rotation state registry at
|
|
6
|
+
* ~/.claude/api-key-rotation.json shared across all projects.
|
|
7
|
+
*
|
|
8
|
+
* Used by:
|
|
9
|
+
* - api-key-watcher.js (SessionStart hook) - key sync + health checks + rotation
|
|
10
|
+
* - hourly-automation.js (10-min timer / WatchPaths) - key sync only
|
|
11
|
+
* - credential-sync-hook.js (PreToolUse, throttled) - key sync only
|
|
12
|
+
*
|
|
13
|
+
* @version 1.0.0
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import os from 'os';
|
|
19
|
+
import crypto from 'crypto';
|
|
20
|
+
import { execFileSync } from 'child_process';
|
|
21
|
+
|
|
22
|
+
// Paths
|
|
23
|
+
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
24
|
+
|
|
25
|
+
// Rotation thresholds (API returns utilization as 0-100 percentages)
|
|
26
|
+
export const HIGH_USAGE_THRESHOLD = 90; // 90%
|
|
27
|
+
export const EXHAUSTED_THRESHOLD = 100; // 100%
|
|
28
|
+
export const EXPIRY_BUFFER_MS = 600_000; // 10 min — pre-expiry window for proactive refresh and restartless swap
|
|
29
|
+
export const HEALTH_DATA_MAX_AGE_MS = 15 * 60 * 1000; // 15 min — usage data older than this is treated as unknown
|
|
30
|
+
const CREDENTIALS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
31
|
+
const ROTATION_STATE_PATH = path.join(os.homedir(), '.claude', 'api-key-rotation.json');
|
|
32
|
+
const ROTATION_LOG_PATH = path.join(PROJECT_DIR, '.claude', 'api-key-rotation.log');
|
|
33
|
+
const OLD_PROJECT_STATE_PATH = path.join(PROJECT_DIR, '.claude', 'api-key-rotation.json');
|
|
34
|
+
|
|
35
|
+
// Constants
|
|
36
|
+
const MAX_LOG_ENTRIES = 100;
|
|
37
|
+
const OAUTH_TOKEN_ENDPOINT = 'https://platform.claude.com/v1/oauth/token';
|
|
38
|
+
const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
39
|
+
const OAUTH_SCOPES = 'user:profile user:inference user:sessions:claude_code user:mcp_servers';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate a stable key ID from an access token.
|
|
43
|
+
* Uses SHA256 hash (first 16 chars) for privacy.
|
|
44
|
+
* @param {string} accessToken
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function generateKeyId(accessToken) {
|
|
48
|
+
const cleanToken = accessToken
|
|
49
|
+
.replace(/^sk-ant-oat01-/, '')
|
|
50
|
+
.replace(/^sk-ant-/, '');
|
|
51
|
+
|
|
52
|
+
const hash = crypto.createHash('sha256').update(cleanToken).digest('hex');
|
|
53
|
+
return hash.substring(0, 16);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read credentials from all available sources.
|
|
58
|
+
* Returns an array of credential objects, one per source that has data.
|
|
59
|
+
*
|
|
60
|
+
* Priority order (all returned, not short-circuited):
|
|
61
|
+
* 1. Environment variable CLAUDE_CODE_OAUTH_TOKEN
|
|
62
|
+
* 2. macOS Keychain
|
|
63
|
+
* 3. ~/.claude/.credentials.json
|
|
64
|
+
*
|
|
65
|
+
* @returns {Array<{accessToken: string, refreshToken?: string, expiresAt?: number, subscriptionType?: string, rateLimitTier?: string, source: string}>}
|
|
66
|
+
*/
|
|
67
|
+
export function readCredentialSources() {
|
|
68
|
+
const sources = [];
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
|
|
71
|
+
// Source 1: Environment variable override
|
|
72
|
+
const envToken = process.env['CLAUDE_CODE_OAUTH_TOKEN'];
|
|
73
|
+
if (envToken) {
|
|
74
|
+
sources.push({
|
|
75
|
+
accessToken: envToken,
|
|
76
|
+
source: 'env',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Source 2: macOS Keychain
|
|
81
|
+
if (process.platform === 'darwin') {
|
|
82
|
+
try {
|
|
83
|
+
const { username } = os.userInfo();
|
|
84
|
+
const raw = execFileSync('security', [
|
|
85
|
+
'find-generic-password', '-s', 'Claude Code-credentials', '-a', username, '-w',
|
|
86
|
+
], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
87
|
+
const creds = JSON.parse(raw);
|
|
88
|
+
if (creds?.claudeAiOauth?.accessToken) {
|
|
89
|
+
const oauth = creds.claudeAiOauth;
|
|
90
|
+
if (!oauth.expiresAt || oauth.expiresAt > now) {
|
|
91
|
+
sources.push({
|
|
92
|
+
accessToken: oauth.accessToken,
|
|
93
|
+
refreshToken: oauth.refreshToken,
|
|
94
|
+
expiresAt: oauth.expiresAt,
|
|
95
|
+
subscriptionType: oauth.subscriptionType,
|
|
96
|
+
rateLimitTier: oauth.rateLimitTier,
|
|
97
|
+
source: 'keychain',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Keychain not available (locked, no entry, or non-macOS)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Source 3: Credentials file
|
|
107
|
+
if (fs.existsSync(CREDENTIALS_PATH)) {
|
|
108
|
+
try {
|
|
109
|
+
const creds = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
110
|
+
if (creds?.claudeAiOauth?.accessToken) {
|
|
111
|
+
const oauth = creds.claudeAiOauth;
|
|
112
|
+
sources.push({
|
|
113
|
+
accessToken: oauth.accessToken,
|
|
114
|
+
refreshToken: oauth.refreshToken,
|
|
115
|
+
expiresAt: oauth.expiresAt,
|
|
116
|
+
subscriptionType: oauth.subscriptionType,
|
|
117
|
+
rateLimitTier: oauth.rateLimitTier,
|
|
118
|
+
source: 'file',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// File unreadable
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return sources;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Read the rotation state file (user-level).
|
|
131
|
+
* On first run, migrates from project-level if available.
|
|
132
|
+
* @returns {{version: 1, active_key_id: string|null, keys: Object, rotation_log: Array}}
|
|
133
|
+
*/
|
|
134
|
+
export function readRotationState() {
|
|
135
|
+
const defaultState = {
|
|
136
|
+
version: 1,
|
|
137
|
+
active_key_id: null,
|
|
138
|
+
keys: {},
|
|
139
|
+
rotation_log: []
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Try user-level state first
|
|
143
|
+
if (fs.existsSync(ROTATION_STATE_PATH)) {
|
|
144
|
+
try {
|
|
145
|
+
const content = fs.readFileSync(ROTATION_STATE_PATH, 'utf8');
|
|
146
|
+
const parsed = JSON.parse(content);
|
|
147
|
+
if (parsed && parsed.version === 1 && typeof parsed.keys === 'object') {
|
|
148
|
+
// Migrate: merge any project-level keys not in user-level
|
|
149
|
+
if (fs.existsSync(OLD_PROJECT_STATE_PATH)) {
|
|
150
|
+
try {
|
|
151
|
+
const oldState = JSON.parse(fs.readFileSync(OLD_PROJECT_STATE_PATH, 'utf8'));
|
|
152
|
+
if (oldState?.keys) {
|
|
153
|
+
let merged = false;
|
|
154
|
+
for (const [id, keyData] of Object.entries(oldState.keys)) {
|
|
155
|
+
if (!parsed.keys[id]) {
|
|
156
|
+
parsed.keys[id] = keyData;
|
|
157
|
+
merged = true;
|
|
158
|
+
} else {
|
|
159
|
+
const existing = parsed.keys[id];
|
|
160
|
+
if (keyData.last_health_check && (!existing.last_health_check || keyData.last_health_check > existing.last_health_check)) {
|
|
161
|
+
parsed.keys[id] = { ...existing, ...keyData };
|
|
162
|
+
merged = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (merged) {
|
|
167
|
+
writeRotationState(parsed);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Ignore old state errors
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return parsed;
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// Fall through to default
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Migration: copy project-level state to user-level
|
|
182
|
+
if (fs.existsSync(OLD_PROJECT_STATE_PATH)) {
|
|
183
|
+
try {
|
|
184
|
+
const content = fs.readFileSync(OLD_PROJECT_STATE_PATH, 'utf8');
|
|
185
|
+
const parsed = JSON.parse(content);
|
|
186
|
+
if (parsed && parsed.version === 1 && typeof parsed.keys === 'object') {
|
|
187
|
+
writeRotationState(parsed);
|
|
188
|
+
return parsed;
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// Ignore migration errors
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return defaultState;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Write the rotation state file (user-level).
|
|
200
|
+
* @param {{version: 1, active_key_id: string|null, keys: Object, rotation_log: Array}} state
|
|
201
|
+
*/
|
|
202
|
+
export function writeRotationState(state) {
|
|
203
|
+
try {
|
|
204
|
+
const dir = path.dirname(ROTATION_STATE_PATH);
|
|
205
|
+
if (!fs.existsSync(dir)) {
|
|
206
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
207
|
+
}
|
|
208
|
+
fs.writeFileSync(ROTATION_STATE_PATH, JSON.stringify(state, null, 2), 'utf8');
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error(`[key-sync] Failed to write rotation state: ${err.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Log a rotation event to both state and human-readable log file.
|
|
216
|
+
* @param {{version: 1, active_key_id: string|null, keys: Object, rotation_log: Array}} state
|
|
217
|
+
* @param {{timestamp: number, event: string, key_id: string, reason?: string, usage_snapshot?: Object}} entry
|
|
218
|
+
*/
|
|
219
|
+
export function logRotationEvent(state, entry) {
|
|
220
|
+
state.rotation_log.unshift(entry);
|
|
221
|
+
if (state.rotation_log.length > MAX_LOG_ENTRIES) {
|
|
222
|
+
state.rotation_log = state.rotation_log.slice(0, MAX_LOG_ENTRIES);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const timestamp = new Date(entry.timestamp).toISOString();
|
|
227
|
+
let line = `[${timestamp}] ${entry.event}: key=${entry.key_id.slice(0, 8)}...`;
|
|
228
|
+
|
|
229
|
+
if (entry.reason) {
|
|
230
|
+
line += ` reason=${entry.reason}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (entry.usage_snapshot) {
|
|
234
|
+
const u = entry.usage_snapshot;
|
|
235
|
+
line += ` usage=(5h:${Math.round(u.five_hour)}%, 7d:${Math.round(u.seven_day)}%, sonnet:${Math.round(u.seven_day_sonnet)}%)`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fs.appendFileSync(ROTATION_LOG_PATH, line + '\n', 'utf8');
|
|
239
|
+
} catch {
|
|
240
|
+
// Ignore log file errors
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Update the active credentials in the appropriate store.
|
|
246
|
+
* Writes to both file and keychain (if on macOS) for consistency.
|
|
247
|
+
* @param {object} keyData - Key data with accessToken, refreshToken, etc.
|
|
248
|
+
*/
|
|
249
|
+
export function updateActiveCredentials(keyData) {
|
|
250
|
+
// Update credentials file
|
|
251
|
+
try {
|
|
252
|
+
let creds = {};
|
|
253
|
+
if (fs.existsSync(CREDENTIALS_PATH)) {
|
|
254
|
+
creds = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
creds.claudeAiOauth = {
|
|
258
|
+
...creds.claudeAiOauth,
|
|
259
|
+
accessToken: keyData.accessToken,
|
|
260
|
+
refreshToken: keyData.refreshToken,
|
|
261
|
+
expiresAt: keyData.expiresAt,
|
|
262
|
+
subscriptionType: keyData.subscriptionType,
|
|
263
|
+
rateLimitTier: keyData.rateLimitTier,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), 'utf8');
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.error(`[key-sync] Failed to write credentials file: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Update macOS Keychain
|
|
272
|
+
if (process.platform === 'darwin') {
|
|
273
|
+
try {
|
|
274
|
+
const { username } = os.userInfo();
|
|
275
|
+
let keychainCreds = {};
|
|
276
|
+
|
|
277
|
+
// Read existing keychain data to preserve other fields
|
|
278
|
+
try {
|
|
279
|
+
const raw = execFileSync('security', [
|
|
280
|
+
'find-generic-password', '-s', 'Claude Code-credentials', '-a', username, '-w',
|
|
281
|
+
], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
282
|
+
keychainCreds = JSON.parse(raw);
|
|
283
|
+
} catch {
|
|
284
|
+
// No existing keychain entry
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
keychainCreds.claudeAiOauth = {
|
|
288
|
+
...keychainCreds.claudeAiOauth,
|
|
289
|
+
accessToken: keyData.accessToken,
|
|
290
|
+
refreshToken: keyData.refreshToken,
|
|
291
|
+
expiresAt: keyData.expiresAt,
|
|
292
|
+
subscriptionType: keyData.subscriptionType,
|
|
293
|
+
rateLimitTier: keyData.rateLimitTier,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
execFileSync('security', [
|
|
297
|
+
'add-generic-password', '-U',
|
|
298
|
+
'-s', 'Claude Code-credentials',
|
|
299
|
+
'-a', username,
|
|
300
|
+
'-w', JSON.stringify(keychainCreds),
|
|
301
|
+
], { encoding: 'utf8', timeout: 3000 });
|
|
302
|
+
} catch {
|
|
303
|
+
// Keychain update failed - non-fatal, file was already updated
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Attempt to refresh an expired OAuth token.
|
|
310
|
+
* @param {object} keyData - Key data with refreshToken
|
|
311
|
+
* @returns {Promise<{accessToken: string, refreshToken: string, expiresAt: number}|'invalid_grant'|null>}
|
|
312
|
+
*/
|
|
313
|
+
export async function refreshExpiredToken(keyData) {
|
|
314
|
+
if (!keyData.refreshToken || keyData.status === 'invalid') return null;
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const response = await fetch(OAUTH_TOKEN_ENDPOINT, {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: { 'Content-Type': 'application/json' },
|
|
320
|
+
body: JSON.stringify({
|
|
321
|
+
grant_type: 'refresh_token',
|
|
322
|
+
refresh_token: keyData.refreshToken,
|
|
323
|
+
client_id: OAUTH_CLIENT_ID,
|
|
324
|
+
scope: OAUTH_SCOPES,
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (!response.ok) {
|
|
329
|
+
if (response.status === 400) {
|
|
330
|
+
try {
|
|
331
|
+
const errBody = await response.json();
|
|
332
|
+
if (errBody.error === 'invalid_grant') return 'invalid_grant';
|
|
333
|
+
} catch { /* treat as transient */ }
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const data = await response.json();
|
|
338
|
+
|
|
339
|
+
if (!data.access_token) return null;
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
accessToken: data.access_token,
|
|
343
|
+
refreshToken: data.refresh_token || keyData.refreshToken,
|
|
344
|
+
expiresAt: data.expires_in ? (Date.now() + data.expires_in * 1000) : (Date.now() + 3600 * 1000),
|
|
345
|
+
};
|
|
346
|
+
} catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Sync keys from all credential sources into the rotation state.
|
|
353
|
+
* Discovers new keys and updates existing tokens.
|
|
354
|
+
*
|
|
355
|
+
* @param {function} [log] - Optional log function
|
|
356
|
+
* @returns {Promise<{keysAdded: number, keysUpdated: number, tokensRefreshed: number}>}
|
|
357
|
+
*/
|
|
358
|
+
export async function syncKeys(log) {
|
|
359
|
+
const logFn = log || (() => {});
|
|
360
|
+
const result = { keysAdded: 0, keysUpdated: 0, tokensRefreshed: 0 };
|
|
361
|
+
|
|
362
|
+
const sources = readCredentialSources();
|
|
363
|
+
if (sources.length === 0) {
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const state = readRotationState();
|
|
368
|
+
const now = Date.now();
|
|
369
|
+
|
|
370
|
+
// Process each credential source
|
|
371
|
+
for (const cred of sources) {
|
|
372
|
+
const keyId = generateKeyId(cred.accessToken);
|
|
373
|
+
const isNewKey = !state.keys[keyId];
|
|
374
|
+
|
|
375
|
+
if (isNewKey) {
|
|
376
|
+
state.keys[keyId] = {
|
|
377
|
+
accessToken: cred.accessToken,
|
|
378
|
+
refreshToken: cred.refreshToken || null,
|
|
379
|
+
expiresAt: cred.expiresAt || null,
|
|
380
|
+
subscriptionType: cred.subscriptionType || 'unknown',
|
|
381
|
+
rateLimitTier: cred.rateLimitTier || 'unknown',
|
|
382
|
+
added_at: now,
|
|
383
|
+
last_used_at: null,
|
|
384
|
+
last_health_check: null,
|
|
385
|
+
last_usage: null,
|
|
386
|
+
account_uuid: null,
|
|
387
|
+
account_email: null,
|
|
388
|
+
status: 'active',
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
logRotationEvent(state, {
|
|
392
|
+
timestamp: now,
|
|
393
|
+
event: 'key_added',
|
|
394
|
+
key_id: keyId,
|
|
395
|
+
reason: `new_key_from_${cred.source}_${cred.subscriptionType || 'unknown'}`,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
result.keysAdded++;
|
|
399
|
+
logFn(`[key-sync] New key discovered from ${cred.source}: ${keyId.slice(0, 8)}...`);
|
|
400
|
+
} else {
|
|
401
|
+
// Update existing key data (tokens may have been refreshed)
|
|
402
|
+
const existingKey = state.keys[keyId];
|
|
403
|
+
existingKey.accessToken = cred.accessToken;
|
|
404
|
+
if (cred.refreshToken) existingKey.refreshToken = cred.refreshToken;
|
|
405
|
+
if (cred.expiresAt) existingKey.expiresAt = cred.expiresAt;
|
|
406
|
+
if (cred.subscriptionType) existingKey.subscriptionType = cred.subscriptionType;
|
|
407
|
+
if (cred.rateLimitTier) existingKey.rateLimitTier = cred.rateLimitTier;
|
|
408
|
+
|
|
409
|
+
// If the key was marked expired but we got fresh data, reactivate
|
|
410
|
+
if (existingKey.status === 'expired' && cred.expiresAt && cred.expiresAt > now) {
|
|
411
|
+
existingKey.status = 'active';
|
|
412
|
+
logFn(`[key-sync] Reactivated key from ${cred.source}: ${keyId.slice(0, 8)}...`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
result.keysUpdated++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Attempt token refresh for expired keys AND non-active keys approaching expiry.
|
|
420
|
+
// Proactive refresh keeps standby tokens fresh so SRA()/r6T() always has a valid replacement.
|
|
421
|
+
// Safe: refreshing one account's token does NOT revoke another account's in-memory token.
|
|
422
|
+
for (const [keyId, keyData] of Object.entries(state.keys)) {
|
|
423
|
+
const isExpired = keyData.expiresAt && keyData.expiresAt < now;
|
|
424
|
+
const isApproachingExpiry = keyData.expiresAt && keyData.expiresAt > now && keyData.expiresAt < now + EXPIRY_BUFFER_MS && keyId !== state.active_key_id;
|
|
425
|
+
if ((isExpired || isApproachingExpiry) && keyData.status !== 'invalid') {
|
|
426
|
+
const refreshed = await refreshExpiredToken(keyData);
|
|
427
|
+
if (refreshed === 'invalid_grant') {
|
|
428
|
+
keyData.status = 'invalid';
|
|
429
|
+
logRotationEvent(state, {
|
|
430
|
+
timestamp: now,
|
|
431
|
+
event: 'key_removed',
|
|
432
|
+
key_id: keyId,
|
|
433
|
+
reason: 'refresh_token_invalid_grant',
|
|
434
|
+
});
|
|
435
|
+
logFn(`[key-sync] Refresh token revoked for key ${keyId.slice(0, 8)}... — marked invalid`);
|
|
436
|
+
} else if (refreshed) {
|
|
437
|
+
keyData.accessToken = refreshed.accessToken;
|
|
438
|
+
keyData.refreshToken = refreshed.refreshToken;
|
|
439
|
+
keyData.expiresAt = refreshed.expiresAt;
|
|
440
|
+
keyData.status = 'active';
|
|
441
|
+
result.tokensRefreshed++;
|
|
442
|
+
logFn(`[key-sync] Refreshed expired token for key ${keyId.slice(0, 8)}...`);
|
|
443
|
+
|
|
444
|
+
logRotationEvent(state, {
|
|
445
|
+
timestamp: now,
|
|
446
|
+
event: 'key_added',
|
|
447
|
+
key_id: keyId,
|
|
448
|
+
reason: 'token_refreshed',
|
|
449
|
+
});
|
|
450
|
+
} else {
|
|
451
|
+
keyData.status = 'expired';
|
|
452
|
+
logRotationEvent(state, {
|
|
453
|
+
timestamp: now,
|
|
454
|
+
event: 'key_removed',
|
|
455
|
+
key_id: keyId,
|
|
456
|
+
reason: 'token_expired_refresh_failed',
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Resolve account profiles for keys missing account_uuid.
|
|
463
|
+
// This ensures keys added by hourly automation, token refresh, or credential sync
|
|
464
|
+
// get their profile resolved without waiting for an interactive SessionStart.
|
|
465
|
+
for (const [keyId, keyData] of Object.entries(state.keys)) {
|
|
466
|
+
if (keyData.account_uuid) continue;
|
|
467
|
+
if (keyData.status !== 'active' && keyData.status !== 'exhausted') continue;
|
|
468
|
+
try {
|
|
469
|
+
const profile = await fetchAccountProfile(keyData.accessToken);
|
|
470
|
+
if (profile) {
|
|
471
|
+
keyData.account_uuid = profile.account_uuid;
|
|
472
|
+
keyData.account_email = profile.email;
|
|
473
|
+
logFn(`[key-sync] Resolved profile for key ${keyId.slice(0, 8)}...: ${profile.email}`);
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
// Non-fatal — profile will be retried on next sync
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Set initial active key if none set
|
|
481
|
+
if (!state.active_key_id) {
|
|
482
|
+
const firstActive = Object.entries(state.keys).find(([_, k]) => k.status === 'active');
|
|
483
|
+
if (firstActive) {
|
|
484
|
+
state.active_key_id = firstActive[0];
|
|
485
|
+
state.keys[firstActive[0]].last_used_at = now;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Deduplicate keys sharing the same account before swap logic runs.
|
|
490
|
+
// Must run before pre-expiry swap to prevent swapping to a key that gets merged away.
|
|
491
|
+
const dedup = deduplicateKeys(state);
|
|
492
|
+
if (dedup.merged > 0) {
|
|
493
|
+
logFn(`[key-sync] Deduplicated ${dedup.merged} key(s) by account_uuid`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Pre-expiry restartless swap: if the active key is near expiry, write a valid standby
|
|
497
|
+
// to Keychain so Claude Code's SRA()/r6T() picks it up without requiring a restart.
|
|
498
|
+
// This is critical for idle sessions — hourly-automation calls syncKeys() every 10 min
|
|
499
|
+
// even when no Claude Code process is making API calls.
|
|
500
|
+
const activeKey = state.active_key_id && state.keys[state.active_key_id];
|
|
501
|
+
if (activeKey && activeKey.expiresAt && activeKey.expiresAt < now + EXPIRY_BUFFER_MS) {
|
|
502
|
+
const standby = Object.entries(state.keys).find(([id, k]) =>
|
|
503
|
+
id !== state.active_key_id &&
|
|
504
|
+
k.status === 'active' &&
|
|
505
|
+
k.expiresAt && k.expiresAt > now + EXPIRY_BUFFER_MS
|
|
506
|
+
);
|
|
507
|
+
if (standby) {
|
|
508
|
+
const [newKeyId, newKeyData] = standby;
|
|
509
|
+
const previousKeyId = state.active_key_id;
|
|
510
|
+
state.active_key_id = newKeyId;
|
|
511
|
+
newKeyData.last_used_at = now;
|
|
512
|
+
updateActiveCredentials(newKeyData);
|
|
513
|
+
logRotationEvent(state, {
|
|
514
|
+
timestamp: now,
|
|
515
|
+
event: 'key_switched',
|
|
516
|
+
key_id: newKeyId,
|
|
517
|
+
reason: 'pre_expiry_restartless_swap',
|
|
518
|
+
previous_key: previousKeyId,
|
|
519
|
+
});
|
|
520
|
+
logFn(`[key-sync] Pre-expiry restartless swap: ${previousKeyId.slice(0, 8)} → ${newKeyId.slice(0, 8)}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Garbage-collect dead keys
|
|
525
|
+
pruneDeadKeys(state, logFn);
|
|
526
|
+
|
|
527
|
+
writeRotationState(state);
|
|
528
|
+
return result;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Garbage-collect dead keys from the rotation state.
|
|
533
|
+
* Removes all keys with status === 'invalid' immediately (invalid keys have
|
|
534
|
+
* permanently revoked refresh tokens and cannot recover). Never prunes the
|
|
535
|
+
* active key. Also removes orphaned rotation_log entries referencing pruned keys.
|
|
536
|
+
*
|
|
537
|
+
* @param {{version: 1, active_key_id: string|null, keys: Object, rotation_log: Array}} state
|
|
538
|
+
* @param {function} [log] - Optional log function
|
|
539
|
+
*/
|
|
540
|
+
export function pruneDeadKeys(state, log) {
|
|
541
|
+
const logFn = log || (() => {});
|
|
542
|
+
const prunedKeyIds = [];
|
|
543
|
+
|
|
544
|
+
for (const [keyId, keyData] of Object.entries(state.keys)) {
|
|
545
|
+
if (keyData.status !== 'invalid') continue;
|
|
546
|
+
if (keyId === state.active_key_id) continue;
|
|
547
|
+
|
|
548
|
+
prunedKeyIds.push(keyId);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (prunedKeyIds.length === 0) return;
|
|
552
|
+
|
|
553
|
+
for (const keyId of prunedKeyIds) {
|
|
554
|
+
delete state.keys[keyId];
|
|
555
|
+
logFn(`[key-sync] Pruned dead key ${keyId.slice(0, 8)}... (invalid, gc'd)`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Remove orphaned rotation_log entries
|
|
559
|
+
const prunedSet = new Set(prunedKeyIds);
|
|
560
|
+
state.rotation_log = state.rotation_log.filter(
|
|
561
|
+
entry => !entry.key_id || !prunedSet.has(entry.key_id)
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ============================================================================
|
|
566
|
+
// Health Check & Key Selection (shared with api-key-watcher, quota-monitor, stop-hook)
|
|
567
|
+
// ============================================================================
|
|
568
|
+
|
|
569
|
+
const ANTHROPIC_API_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
570
|
+
const ANTHROPIC_BETA_HEADER = 'oauth-2025-04-20';
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Check the health/usage of a key via Anthropic API.
|
|
574
|
+
* @param {string} accessToken
|
|
575
|
+
* @returns {Promise<{valid: boolean, usage: {five_hour: number, seven_day: number, seven_day_sonnet: number} | null, raw?: object, error?: string}>}
|
|
576
|
+
*/
|
|
577
|
+
export async function checkKeyHealth(accessToken) {
|
|
578
|
+
try {
|
|
579
|
+
const response = await fetch(ANTHROPIC_API_URL, {
|
|
580
|
+
method: 'GET',
|
|
581
|
+
headers: {
|
|
582
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
583
|
+
'Content-Type': 'application/json',
|
|
584
|
+
'User-Agent': 'claude-code/2.1.14',
|
|
585
|
+
'anthropic-beta': ANTHROPIC_BETA_HEADER,
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
if (response.status === 401) {
|
|
590
|
+
return { valid: false, usage: null, error: 'unauthorized' };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (!response.ok) {
|
|
594
|
+
return { valid: false, usage: null, error: `http_${response.status}` };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const data = await response.json();
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
valid: true,
|
|
601
|
+
usage: {
|
|
602
|
+
five_hour: data.five_hour?.utilization ?? 0,
|
|
603
|
+
seven_day: data.seven_day?.utilization ?? 0,
|
|
604
|
+
seven_day_sonnet: data.seven_day_sonnet?.utilization ?? 0,
|
|
605
|
+
},
|
|
606
|
+
raw: data,
|
|
607
|
+
};
|
|
608
|
+
} catch (err) {
|
|
609
|
+
return { valid: false, usage: null, error: err.message };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Select the best key to use based on current usage levels.
|
|
615
|
+
* Groups keys by account_uuid first: for each unique account, picks the key
|
|
616
|
+
* with the freshest token (highest expiresAt). Keys without account_uuid are
|
|
617
|
+
* treated as unique (each is its own "account"). Then applies existing
|
|
618
|
+
* threshold logic between account-representative keys.
|
|
619
|
+
*
|
|
620
|
+
* Returns the key ID of the best key, or null if all keys are exhausted.
|
|
621
|
+
* @param {{version: 1, active_key_id: string|null, keys: Object, rotation_log: Array}} state
|
|
622
|
+
* @returns {string|null}
|
|
623
|
+
*/
|
|
624
|
+
export function selectActiveKey(state) {
|
|
625
|
+
const now = Date.now();
|
|
626
|
+
const validKeys = Object.entries(state.keys)
|
|
627
|
+
.filter(([_, key]) => key.status === 'active' || key.status === 'exhausted')
|
|
628
|
+
.map(([id, key]) => ({ id, key, usage: key.last_usage }));
|
|
629
|
+
|
|
630
|
+
if (validKeys.length === 0) return null;
|
|
631
|
+
|
|
632
|
+
// Freshness gate: null out usage for keys with stale health data (>15 min old).
|
|
633
|
+
// Effect: stale keys pass "usable" filter (not proven exhausted), block "allAbove90"
|
|
634
|
+
// early-return, and are excluded from comparison logic. Net: system stays put with
|
|
635
|
+
// stale data rather than making uninformed switches.
|
|
636
|
+
for (const entry of validKeys) {
|
|
637
|
+
const lastCheck = entry.key.last_health_check;
|
|
638
|
+
if (lastCheck && (now - lastCheck) > HEALTH_DATA_MAX_AGE_MS) {
|
|
639
|
+
entry.usage = null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Group by account_uuid. Keys without account_uuid are each treated as unique.
|
|
644
|
+
const accountGroups = new Map();
|
|
645
|
+
for (const entry of validKeys) {
|
|
646
|
+
const uuid = entry.key.account_uuid;
|
|
647
|
+
if (!uuid) {
|
|
648
|
+
// No account_uuid — treat as its own unique group
|
|
649
|
+
accountGroups.set(`__no_uuid__${entry.id}`, [entry]);
|
|
650
|
+
} else {
|
|
651
|
+
if (!accountGroups.has(uuid)) {
|
|
652
|
+
accountGroups.set(uuid, []);
|
|
653
|
+
}
|
|
654
|
+
accountGroups.get(uuid).push(entry);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// For each account group, pick the representative: key with freshest token (highest expiresAt).
|
|
659
|
+
// If expiresAt is missing/null, treat as 0 (least fresh).
|
|
660
|
+
const representatives = [];
|
|
661
|
+
for (const entries of accountGroups.values()) {
|
|
662
|
+
if (entries.length === 1) {
|
|
663
|
+
representatives.push(entries[0]);
|
|
664
|
+
} else {
|
|
665
|
+
entries.sort((a, b) => (b.key.expiresAt || 0) - (a.key.expiresAt || 0));
|
|
666
|
+
representatives.push(entries[0]);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Filter out keys at 100% in ANY category (unusable)
|
|
671
|
+
const usableKeys = representatives.filter(({ usage }) => {
|
|
672
|
+
if (!usage) return true; // No data yet, assume usable
|
|
673
|
+
return usage.five_hour < EXHAUSTED_THRESHOLD &&
|
|
674
|
+
usage.seven_day < EXHAUSTED_THRESHOLD &&
|
|
675
|
+
usage.seven_day_sonnet < EXHAUSTED_THRESHOLD;
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
if (usableKeys.length === 0) return null; // All keys exhausted
|
|
679
|
+
|
|
680
|
+
// Check if ALL usable keys are above 90% in at least one category
|
|
681
|
+
const allAbove90 = usableKeys.every(({ usage }) => {
|
|
682
|
+
if (!usage) return false;
|
|
683
|
+
return usage.five_hour >= HIGH_USAGE_THRESHOLD ||
|
|
684
|
+
usage.seven_day >= HIGH_USAGE_THRESHOLD ||
|
|
685
|
+
usage.seven_day_sonnet >= HIGH_USAGE_THRESHOLD;
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// Current key info
|
|
689
|
+
const currentKey = state.active_key_id
|
|
690
|
+
? usableKeys.find(k => k.id === state.active_key_id)
|
|
691
|
+
: null;
|
|
692
|
+
|
|
693
|
+
if (allAbove90) {
|
|
694
|
+
// All keys high usage: only switch when current is completely exhausted
|
|
695
|
+
if (currentKey) {
|
|
696
|
+
const currentUsage = currentKey.usage;
|
|
697
|
+
if (currentUsage && (
|
|
698
|
+
currentUsage.five_hour >= EXHAUSTED_THRESHOLD ||
|
|
699
|
+
currentUsage.seven_day >= EXHAUSTED_THRESHOLD ||
|
|
700
|
+
currentUsage.seven_day_sonnet >= EXHAUSTED_THRESHOLD
|
|
701
|
+
)) {
|
|
702
|
+
// Current key hit 100% somewhere, switch to another
|
|
703
|
+
const otherKey = usableKeys.find(k => k.id !== state.active_key_id);
|
|
704
|
+
return otherKey?.id ?? null;
|
|
705
|
+
}
|
|
706
|
+
return currentKey.id; // Stick with current
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
// Some keys below 90%: switch when current reaches >=90% in any category
|
|
710
|
+
if (currentKey?.usage) {
|
|
711
|
+
const maxUsage = Math.max(
|
|
712
|
+
currentKey.usage.five_hour,
|
|
713
|
+
currentKey.usage.seven_day,
|
|
714
|
+
currentKey.usage.seven_day_sonnet
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
if (maxUsage >= HIGH_USAGE_THRESHOLD) {
|
|
718
|
+
// Find key with lowest max usage
|
|
719
|
+
const sortedByUsage = usableKeys
|
|
720
|
+
.filter(k => k.id !== state.active_key_id && k.usage)
|
|
721
|
+
.sort((a, b) => {
|
|
722
|
+
const aMax = Math.max(a.usage.five_hour, a.usage.seven_day, a.usage.seven_day_sonnet);
|
|
723
|
+
const bMax = Math.max(b.usage.five_hour, b.usage.seven_day, b.usage.seven_day_sonnet);
|
|
724
|
+
return aMax - bMax;
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
if (sortedByUsage.length > 0 && sortedByUsage[0].usage) {
|
|
728
|
+
const altMax = Math.max(
|
|
729
|
+
sortedByUsage[0].usage.five_hour,
|
|
730
|
+
sortedByUsage[0].usage.seven_day,
|
|
731
|
+
sortedByUsage[0].usage.seven_day_sonnet
|
|
732
|
+
);
|
|
733
|
+
if (altMax < HIGH_USAGE_THRESHOLD) {
|
|
734
|
+
return sortedByUsage[0].id;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Default: use current key or pick first usable
|
|
742
|
+
return currentKey?.id ?? usableKeys[0]?.id ?? null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Deduplicate keys sharing the same account_uuid.
|
|
747
|
+
* For each group of keys with the same account_uuid, keeps the entry with the
|
|
748
|
+
* freshest token (highest expiresAt). Copies last_usage and last_health_check
|
|
749
|
+
* from the most recently health-checked entry. Keys without account_uuid are
|
|
750
|
+
* left untouched. Should only be called after health checks have populated
|
|
751
|
+
* account_uuid fields.
|
|
752
|
+
*
|
|
753
|
+
* If the active_key_id points to a key that gets merged away, updates
|
|
754
|
+
* active_key_id to the surviving key.
|
|
755
|
+
*
|
|
756
|
+
* @param {{version: 1, active_key_id: string|null, keys: Object, rotation_log: Array}} state
|
|
757
|
+
* @returns {{merged: number}} Number of keys removed by deduplication
|
|
758
|
+
*/
|
|
759
|
+
export function deduplicateKeys(state) {
|
|
760
|
+
const result = { merged: 0 };
|
|
761
|
+
|
|
762
|
+
// Group keys by account_uuid. Skip keys without one.
|
|
763
|
+
const accountGroups = new Map();
|
|
764
|
+
for (const [keyId, keyData] of Object.entries(state.keys)) {
|
|
765
|
+
const uuid = keyData.account_uuid;
|
|
766
|
+
if (!uuid) continue;
|
|
767
|
+
if (!accountGroups.has(uuid)) {
|
|
768
|
+
accountGroups.set(uuid, []);
|
|
769
|
+
}
|
|
770
|
+
accountGroups.get(uuid).push({ id: keyId, data: keyData });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
for (const [, entries] of accountGroups) {
|
|
774
|
+
if (entries.length <= 1) continue;
|
|
775
|
+
|
|
776
|
+
// Pick the entry with the freshest token (highest expiresAt)
|
|
777
|
+
entries.sort((a, b) => (b.data.expiresAt || 0) - (a.data.expiresAt || 0));
|
|
778
|
+
const survivor = entries[0];
|
|
779
|
+
|
|
780
|
+
// Find the most recently health-checked entry for usage data
|
|
781
|
+
const mostRecentlyChecked = entries
|
|
782
|
+
.filter(e => e.data.last_health_check != null)
|
|
783
|
+
.sort((a, b) => b.data.last_health_check - a.data.last_health_check)[0];
|
|
784
|
+
|
|
785
|
+
if (mostRecentlyChecked && mostRecentlyChecked.id !== survivor.id) {
|
|
786
|
+
if (mostRecentlyChecked.data.last_usage != null) {
|
|
787
|
+
survivor.data.last_usage = mostRecentlyChecked.data.last_usage;
|
|
788
|
+
}
|
|
789
|
+
if (mostRecentlyChecked.data.last_health_check != null) {
|
|
790
|
+
survivor.data.last_health_check = mostRecentlyChecked.data.last_health_check;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Remove all non-survivor entries
|
|
795
|
+
for (let i = 1; i < entries.length; i++) {
|
|
796
|
+
const removedId = entries[i].id;
|
|
797
|
+
|
|
798
|
+
// If active_key_id pointed to a removed key, redirect to survivor
|
|
799
|
+
if (state.active_key_id === removedId) {
|
|
800
|
+
state.active_key_id = survivor.id;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
delete state.keys[removedId];
|
|
804
|
+
result.merged++;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Read credentials directly from macOS Keychain.
|
|
813
|
+
* Returns the parsed credential object (with claudeAiOauth field) or null.
|
|
814
|
+
* @returns {object|null}
|
|
815
|
+
*/
|
|
816
|
+
export function readKeychainCredentials() {
|
|
817
|
+
if (process.platform !== 'darwin') return null;
|
|
818
|
+
try {
|
|
819
|
+
const { username } = os.userInfo();
|
|
820
|
+
const raw = execFileSync('security', [
|
|
821
|
+
'find-generic-password', '-s', 'Claude Code-credentials', '-a', username, '-w',
|
|
822
|
+
], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
823
|
+
return JSON.parse(raw);
|
|
824
|
+
} catch {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ============================================================================
|
|
830
|
+
// Account Profile Resolution
|
|
831
|
+
// ============================================================================
|
|
832
|
+
|
|
833
|
+
const PROFILE_API_URL = 'https://api.anthropic.com/api/oauth/profile';
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Fetch account profile to get account UUID and email for deduplication.
|
|
837
|
+
* Uses the same OAuth Bearer auth as the usage endpoint.
|
|
838
|
+
* @param {string} accessToken
|
|
839
|
+
* @returns {Promise<{account_uuid: string, email: string}|null>}
|
|
840
|
+
*/
|
|
841
|
+
export async function fetchAccountProfile(accessToken) {
|
|
842
|
+
try {
|
|
843
|
+
const response = await fetch(PROFILE_API_URL, {
|
|
844
|
+
method: 'GET',
|
|
845
|
+
headers: {
|
|
846
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
847
|
+
'Content-Type': 'application/json',
|
|
848
|
+
'User-Agent': 'claude-code/2.1.14',
|
|
849
|
+
'anthropic-beta': ANTHROPIC_BETA_HEADER,
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
if (!response.ok) return null;
|
|
854
|
+
|
|
855
|
+
const data = await response.json();
|
|
856
|
+
if (data.account?.uuid && data.account?.email) {
|
|
857
|
+
return {
|
|
858
|
+
account_uuid: data.account.uuid,
|
|
859
|
+
email: data.account.email,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return null;
|
|
863
|
+
} catch {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Rotation audit log — structured event log for post-rotation health verification
|
|
869
|
+
const ROTATION_AUDIT_LOG_PATH = path.join(
|
|
870
|
+
process.env.CLAUDE_PROJECT_DIR || process.cwd(),
|
|
871
|
+
'.claude', 'state', 'rotation-audit.log'
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Append a structured audit event to the rotation audit log.
|
|
876
|
+
* Format: [ISO_TIMESTAMP] EVENT key1=val1 key2=val2
|
|
877
|
+
*
|
|
878
|
+
* @param {string} event - Event name (e.g. 'rotation_completed', 'adoption_verified')
|
|
879
|
+
* @param {Object} details - Key-value pairs to include in the log line
|
|
880
|
+
*/
|
|
881
|
+
export function appendRotationAudit(event, details = {}) {
|
|
882
|
+
const ts = new Date().toISOString();
|
|
883
|
+
const sanitizedEvent = String(event).replace(/[\r\n]/g, ' ');
|
|
884
|
+
const parts = [`[${ts}] ${sanitizedEvent}`];
|
|
885
|
+
for (const [k, v] of Object.entries(details)) {
|
|
886
|
+
parts.push(`${k}=${String(v).replace(/[\r\n]/g, ' ')}`);
|
|
887
|
+
}
|
|
888
|
+
const line = parts.join(' ');
|
|
889
|
+
try {
|
|
890
|
+
const dir = path.dirname(ROTATION_AUDIT_LOG_PATH);
|
|
891
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
892
|
+
fs.appendFileSync(ROTATION_AUDIT_LOG_PATH, line + '\n', 'utf8');
|
|
893
|
+
} catch (err) {
|
|
894
|
+
console.error(`[key-sync] Failed to write rotation audit log: ${err.message}`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Export paths for consumers
|
|
899
|
+
export { ROTATION_STATE_PATH, ROTATION_LOG_PATH, CREDENTIALS_PATH, OLD_PROJECT_STATE_PATH, ROTATION_AUDIT_LOG_PATH };
|