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.
Files changed (599) hide show
  1. package/.claude/agents/antipattern-hunter.md +176 -0
  2. package/.claude/agents/code-reviewer.md +205 -0
  3. package/.claude/agents/code-writer.md +154 -0
  4. package/.claude/agents/deputy-cto.md +309 -0
  5. package/.claude/agents/feedback-agent.md +101 -0
  6. package/.claude/agents/investigator.md +136 -0
  7. package/.claude/agents/product-manager.md +97 -0
  8. package/.claude/agents/project-manager.md +116 -0
  9. package/.claude/agents/repo-hygiene-expert.md +626 -0
  10. package/.claude/agents/secret-manager.md +324 -0
  11. package/.claude/agents/test-writer.md +354 -0
  12. package/.claude/commands/configure-personas.md +144 -0
  13. package/.claude/commands/cto-report.md +36 -0
  14. package/.claude/commands/demo.md +89 -0
  15. package/.claude/commands/deputy-cto.md +345 -0
  16. package/.claude/commands/hotfix.md +31 -0
  17. package/.claude/commands/overdrive-gentyr.md +167 -0
  18. package/.claude/commands/product-manager.md +32 -0
  19. package/.claude/commands/push-migrations.md +86 -0
  20. package/.claude/commands/push-secrets.md +97 -0
  21. package/.claude/commands/services.json.example +30 -0
  22. package/.claude/commands/setup-gentyr.md +396 -0
  23. package/.claude/commands/show.md +42 -0
  24. package/.claude/commands/spawn-tasks.md +79 -0
  25. package/.claude/commands/toggle-automation-gentyr.md +75 -0
  26. package/.claude/commands/toggle-product-manager.md +19 -0
  27. package/.claude/commands/triage.md +69 -0
  28. package/.claude/hooks/README.md +686 -0
  29. package/.claude/hooks/__tests__/README.md +129 -0
  30. package/.claude/hooks/agent-tracker.js +434 -0
  31. package/.claude/hooks/antipattern-hunter-hook.js +401 -0
  32. package/.claude/hooks/api-key-watcher.js +289 -0
  33. package/.claude/hooks/block-no-verify.js +301 -0
  34. package/.claude/hooks/bypass-approval-hook.js +313 -0
  35. package/.claude/hooks/compliance-checker.js +1309 -0
  36. package/.claude/hooks/config-reader.js +143 -0
  37. package/.claude/hooks/credential-file-guard.js +1139 -0
  38. package/.claude/hooks/credential-health-check.js +168 -0
  39. package/.claude/hooks/credential-sync-hook.js +79 -0
  40. package/.claude/hooks/cto-notification-hook.js +656 -0
  41. package/.claude/hooks/feedback-launcher.js +424 -0
  42. package/.claude/hooks/feedback-orchestrator.js +367 -0
  43. package/.claude/hooks/gentyr-splash.js +47 -0
  44. package/.claude/hooks/gentyr-sync.js +389 -0
  45. package/.claude/hooks/hourly-automation.js +3340 -0
  46. package/.claude/hooks/key-sync.js +899 -0
  47. package/.claude/hooks/lib/approval-utils.js +731 -0
  48. package/.claude/hooks/lib/feature-branch-helper.js +102 -0
  49. package/.claude/hooks/lib/worktree-manager.js +330 -0
  50. package/.claude/hooks/mapping-validator.js +285 -0
  51. package/.claude/hooks/plan-executor.js +398 -0
  52. package/.claude/hooks/playwright-cli-guard.js +104 -0
  53. package/.claude/hooks/playwright-health-check.js +71 -0
  54. package/.claude/hooks/pre-commit-review.js +725 -0
  55. package/.claude/hooks/prompts/local-spec-enforcement.md +310 -0
  56. package/.claude/hooks/prompts/mapping-fix.md +92 -0
  57. package/.claude/hooks/prompts/mapping-review.md +140 -0
  58. package/.claude/hooks/prompts/schema-mapper.md +185 -0
  59. package/.claude/hooks/prompts/spec-enforcement.md +233 -0
  60. package/.claude/hooks/protected-action-approval-hook.js +336 -0
  61. package/.claude/hooks/protected-action-gate.js +562 -0
  62. package/.claude/hooks/protected-actions.json +208 -0
  63. package/.claude/hooks/protected-actions.json.template +122 -0
  64. package/.claude/hooks/quota-monitor.js +490 -0
  65. package/.claude/hooks/reporters/jest-failure-reporter.js +401 -0
  66. package/.claude/hooks/reporters/playwright-failure-reporter.js +446 -0
  67. package/.claude/hooks/reporters/vitest-failure-reporter.js +443 -0
  68. package/.claude/hooks/schema-mapper-hook.js +544 -0
  69. package/.claude/hooks/secret-leak-detector.js +216 -0
  70. package/.claude/hooks/session-reviver.js +514 -0
  71. package/.claude/hooks/slash-command-prefetch.js +1145 -0
  72. package/.claude/hooks/stale-work-detector.js +205 -0
  73. package/.claude/hooks/stop-continue-hook.js +414 -0
  74. package/.claude/hooks/todo-maintenance.js +522 -0
  75. package/.claude/hooks/todo-processing-prompt.md +75 -0
  76. package/.claude/hooks/usage-optimizer.js +791 -0
  77. package/.claude/mcp/README.md +246 -0
  78. package/.claude/settings.json.template +168 -0
  79. package/.mcp.json.template +207 -0
  80. package/CLAUDE.md +340 -0
  81. package/CLAUDE.md.gentyr-section +89 -0
  82. package/LICENSE +21 -0
  83. package/README.md +297 -0
  84. package/cli/commands/init.js +471 -0
  85. package/cli/commands/migrate.js +132 -0
  86. package/cli/commands/protect.js +271 -0
  87. package/cli/commands/scaffold.js +48 -0
  88. package/cli/commands/status.js +133 -0
  89. package/cli/commands/sync.js +101 -0
  90. package/cli/commands/uninstall.js +207 -0
  91. package/cli/index.js +111 -0
  92. package/cli/lib/config-gen.js +214 -0
  93. package/cli/lib/resolve-framework.js +97 -0
  94. package/cli/lib/state.js +140 -0
  95. package/cli/lib/symlinks.js +260 -0
  96. package/docs/AUTOMATION-SYSTEMS.md +484 -0
  97. package/docs/BINARY-PATCHING.md +212 -0
  98. package/docs/CHANGELOG.md +2830 -0
  99. package/docs/CREDENTIAL-DETECTION.md +151 -0
  100. package/docs/CTO-DASHBOARD.md +476 -0
  101. package/docs/DEPLOYMENT-FLOW.md +477 -0
  102. package/docs/DEVELOPER.md +116 -0
  103. package/docs/Executive.md +372 -0
  104. package/docs/SECRET-PATHS.md +77 -0
  105. package/docs/SETUP-GUIDE.md +419 -0
  106. package/docs/STACK.md +109 -0
  107. package/docs/TESTING.md +440 -0
  108. package/docs/assets/claude-logo.svg +3 -0
  109. package/docs/sessions/2026-01-24-spec-suite-implementation.md +190 -0
  110. package/docs/sessions/2026-02-15-feedback-e2e-audit.md +484 -0
  111. package/docs/sessions/2026-02-20-credential-rotation-experiments.md +340 -0
  112. package/docs/sessions/TEST-COVERAGE-REPORT-2026-02-20.md +168 -0
  113. package/docs/shared/EPHEMERAL-STATE-FILES.md +115 -0
  114. package/docs/shared/PROTECTION-SYSTEM.md +341 -0
  115. package/husky/post-commit +10 -0
  116. package/husky/pre-commit +40 -0
  117. package/husky/pre-push +94 -0
  118. package/package.json +43 -0
  119. package/packages/cto-dashboard/package-lock.json +3510 -0
  120. package/packages/cto-dashboard/package.json +41 -0
  121. package/packages/cto-dashboard/pnpm-lock.yaml +2168 -0
  122. package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts +220 -0
  123. package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts.map +1 -0
  124. package/packages/mcp-servers/dist/__testUtils__/fixtures.js +376 -0
  125. package/packages/mcp-servers/dist/__testUtils__/fixtures.js.map +1 -0
  126. package/packages/mcp-servers/dist/__testUtils__/index.d.ts +121 -0
  127. package/packages/mcp-servers/dist/__testUtils__/index.d.ts.map +1 -0
  128. package/packages/mcp-servers/dist/__testUtils__/index.js +180 -0
  129. package/packages/mcp-servers/dist/__testUtils__/index.js.map +1 -0
  130. package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts +84 -0
  131. package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts.map +1 -0
  132. package/packages/mcp-servers/dist/__testUtils__/schemas.js +309 -0
  133. package/packages/mcp-servers/dist/__testUtils__/schemas.js.map +1 -0
  134. package/packages/mcp-servers/dist/agent-reports/index.d.ts +7 -0
  135. package/packages/mcp-servers/dist/agent-reports/index.d.ts.map +1 -0
  136. package/packages/mcp-servers/dist/agent-reports/index.js +8 -0
  137. package/packages/mcp-servers/dist/agent-reports/index.js.map +1 -0
  138. package/packages/mcp-servers/dist/agent-reports/server.d.ts +22 -0
  139. package/packages/mcp-servers/dist/agent-reports/server.d.ts.map +1 -0
  140. package/packages/mcp-servers/dist/agent-reports/server.js +535 -0
  141. package/packages/mcp-servers/dist/agent-reports/server.js.map +1 -0
  142. package/packages/mcp-servers/dist/agent-reports/types.d.ts +258 -0
  143. package/packages/mcp-servers/dist/agent-reports/types.d.ts.map +1 -0
  144. package/packages/mcp-servers/dist/agent-reports/types.js +81 -0
  145. package/packages/mcp-servers/dist/agent-reports/types.js.map +1 -0
  146. package/packages/mcp-servers/dist/agent-tracker/index.d.ts +5 -0
  147. package/packages/mcp-servers/dist/agent-tracker/index.d.ts.map +1 -0
  148. package/packages/mcp-servers/dist/agent-tracker/index.js +5 -0
  149. package/packages/mcp-servers/dist/agent-tracker/index.js.map +1 -0
  150. package/packages/mcp-servers/dist/agent-tracker/server.d.ts +12 -0
  151. package/packages/mcp-servers/dist/agent-tracker/server.d.ts.map +1 -0
  152. package/packages/mcp-servers/dist/agent-tracker/server.js +919 -0
  153. package/packages/mcp-servers/dist/agent-tracker/server.js.map +1 -0
  154. package/packages/mcp-servers/dist/agent-tracker/types.d.ts +328 -0
  155. package/packages/mcp-servers/dist/agent-tracker/types.d.ts.map +1 -0
  156. package/packages/mcp-servers/dist/agent-tracker/types.js +128 -0
  157. package/packages/mcp-servers/dist/agent-tracker/types.js.map +1 -0
  158. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts +27 -0
  159. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts.map +1 -0
  160. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js +167 -0
  161. package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js.map +1 -0
  162. package/packages/mcp-servers/dist/chrome-bridge/index.d.ts +6 -0
  163. package/packages/mcp-servers/dist/chrome-bridge/index.d.ts.map +1 -0
  164. package/packages/mcp-servers/dist/chrome-bridge/index.js +6 -0
  165. package/packages/mcp-servers/dist/chrome-bridge/index.js.map +1 -0
  166. package/packages/mcp-servers/dist/chrome-bridge/server.d.ts +13 -0
  167. package/packages/mcp-servers/dist/chrome-bridge/server.d.ts.map +1 -0
  168. package/packages/mcp-servers/dist/chrome-bridge/server.js +959 -0
  169. package/packages/mcp-servers/dist/chrome-bridge/server.js.map +1 -0
  170. package/packages/mcp-servers/dist/chrome-bridge/types.d.ts +41 -0
  171. package/packages/mcp-servers/dist/chrome-bridge/types.d.ts.map +1 -0
  172. package/packages/mcp-servers/dist/chrome-bridge/types.js +8 -0
  173. package/packages/mcp-servers/dist/chrome-bridge/types.js.map +1 -0
  174. package/packages/mcp-servers/dist/cloudflare/index.d.ts +8 -0
  175. package/packages/mcp-servers/dist/cloudflare/index.d.ts.map +1 -0
  176. package/packages/mcp-servers/dist/cloudflare/index.js +8 -0
  177. package/packages/mcp-servers/dist/cloudflare/index.js.map +1 -0
  178. package/packages/mcp-servers/dist/cloudflare/server.d.ts +16 -0
  179. package/packages/mcp-servers/dist/cloudflare/server.d.ts.map +1 -0
  180. package/packages/mcp-servers/dist/cloudflare/server.js +253 -0
  181. package/packages/mcp-servers/dist/cloudflare/server.js.map +1 -0
  182. package/packages/mcp-servers/dist/cloudflare/types.d.ts +141 -0
  183. package/packages/mcp-servers/dist/cloudflare/types.d.ts.map +1 -0
  184. package/packages/mcp-servers/dist/cloudflare/types.js +53 -0
  185. package/packages/mcp-servers/dist/cloudflare/types.js.map +1 -0
  186. package/packages/mcp-servers/dist/codecov/index.d.ts +7 -0
  187. package/packages/mcp-servers/dist/codecov/index.d.ts.map +1 -0
  188. package/packages/mcp-servers/dist/codecov/index.js +7 -0
  189. package/packages/mcp-servers/dist/codecov/index.js.map +1 -0
  190. package/packages/mcp-servers/dist/codecov/server.d.ts +21 -0
  191. package/packages/mcp-servers/dist/codecov/server.d.ts.map +1 -0
  192. package/packages/mcp-servers/dist/codecov/server.js +376 -0
  193. package/packages/mcp-servers/dist/codecov/server.js.map +1 -0
  194. package/packages/mcp-servers/dist/codecov/types.d.ts +269 -0
  195. package/packages/mcp-servers/dist/codecov/types.d.ts.map +1 -0
  196. package/packages/mcp-servers/dist/codecov/types.js +128 -0
  197. package/packages/mcp-servers/dist/codecov/types.js.map +1 -0
  198. package/packages/mcp-servers/dist/cto-report/index.d.ts +9 -0
  199. package/packages/mcp-servers/dist/cto-report/index.d.ts.map +1 -0
  200. package/packages/mcp-servers/dist/cto-report/index.js +9 -0
  201. package/packages/mcp-servers/dist/cto-report/index.js.map +1 -0
  202. package/packages/mcp-servers/dist/cto-report/server.d.ts +14 -0
  203. package/packages/mcp-servers/dist/cto-report/server.d.ts.map +1 -0
  204. package/packages/mcp-servers/dist/cto-report/server.js +859 -0
  205. package/packages/mcp-servers/dist/cto-report/server.js.map +1 -0
  206. package/packages/mcp-servers/dist/cto-report/types.d.ts +213 -0
  207. package/packages/mcp-servers/dist/cto-report/types.d.ts.map +1 -0
  208. package/packages/mcp-servers/dist/cto-report/types.js +29 -0
  209. package/packages/mcp-servers/dist/cto-report/types.js.map +1 -0
  210. package/packages/mcp-servers/dist/cto-reports/index.d.ts +7 -0
  211. package/packages/mcp-servers/dist/cto-reports/index.d.ts.map +1 -0
  212. package/packages/mcp-servers/dist/cto-reports/index.js +8 -0
  213. package/packages/mcp-servers/dist/cto-reports/index.js.map +1 -0
  214. package/packages/mcp-servers/dist/cto-reports/server.d.ts +20 -0
  215. package/packages/mcp-servers/dist/cto-reports/server.d.ts.map +1 -0
  216. package/packages/mcp-servers/dist/cto-reports/server.js +538 -0
  217. package/packages/mcp-servers/dist/cto-reports/server.js.map +1 -0
  218. package/packages/mcp-servers/dist/cto-reports/types.d.ts +236 -0
  219. package/packages/mcp-servers/dist/cto-reports/types.d.ts.map +1 -0
  220. package/packages/mcp-servers/dist/cto-reports/types.js +77 -0
  221. package/packages/mcp-servers/dist/cto-reports/types.js.map +1 -0
  222. package/packages/mcp-servers/dist/deputy-cto/index.d.ts +7 -0
  223. package/packages/mcp-servers/dist/deputy-cto/index.d.ts.map +1 -0
  224. package/packages/mcp-servers/dist/deputy-cto/index.js +8 -0
  225. package/packages/mcp-servers/dist/deputy-cto/index.js.map +1 -0
  226. package/packages/mcp-servers/dist/deputy-cto/server.d.ts +23 -0
  227. package/packages/mcp-servers/dist/deputy-cto/server.d.ts.map +1 -0
  228. package/packages/mcp-servers/dist/deputy-cto/server.js +1700 -0
  229. package/packages/mcp-servers/dist/deputy-cto/server.js.map +1 -0
  230. package/packages/mcp-servers/dist/deputy-cto/types.d.ts +439 -0
  231. package/packages/mcp-servers/dist/deputy-cto/types.d.ts.map +1 -0
  232. package/packages/mcp-servers/dist/deputy-cto/types.js +102 -0
  233. package/packages/mcp-servers/dist/deputy-cto/types.js.map +1 -0
  234. package/packages/mcp-servers/dist/elastic-logs/index.d.ts +5 -0
  235. package/packages/mcp-servers/dist/elastic-logs/index.d.ts.map +1 -0
  236. package/packages/mcp-servers/dist/elastic-logs/index.js +5 -0
  237. package/packages/mcp-servers/dist/elastic-logs/index.js.map +1 -0
  238. package/packages/mcp-servers/dist/elastic-logs/server.d.ts +18 -0
  239. package/packages/mcp-servers/dist/elastic-logs/server.d.ts.map +1 -0
  240. package/packages/mcp-servers/dist/elastic-logs/server.js +259 -0
  241. package/packages/mcp-servers/dist/elastic-logs/server.js.map +1 -0
  242. package/packages/mcp-servers/dist/elastic-logs/types.d.ts +107 -0
  243. package/packages/mcp-servers/dist/elastic-logs/types.d.ts.map +1 -0
  244. package/packages/mcp-servers/dist/elastic-logs/types.js +31 -0
  245. package/packages/mcp-servers/dist/elastic-logs/types.js.map +1 -0
  246. package/packages/mcp-servers/dist/feedback-explorer/index.d.ts +2 -0
  247. package/packages/mcp-servers/dist/feedback-explorer/index.d.ts.map +1 -0
  248. package/packages/mcp-servers/dist/feedback-explorer/index.js +2 -0
  249. package/packages/mcp-servers/dist/feedback-explorer/index.js.map +1 -0
  250. package/packages/mcp-servers/dist/feedback-explorer/server.d.ts +21 -0
  251. package/packages/mcp-servers/dist/feedback-explorer/server.d.ts.map +1 -0
  252. package/packages/mcp-servers/dist/feedback-explorer/server.js +580 -0
  253. package/packages/mcp-servers/dist/feedback-explorer/server.js.map +1 -0
  254. package/packages/mcp-servers/dist/feedback-explorer/types.d.ts +331 -0
  255. package/packages/mcp-servers/dist/feedback-explorer/types.d.ts.map +1 -0
  256. package/packages/mcp-servers/dist/feedback-explorer/types.js +40 -0
  257. package/packages/mcp-servers/dist/feedback-explorer/types.js.map +1 -0
  258. package/packages/mcp-servers/dist/feedback-reporter/index.d.ts +9 -0
  259. package/packages/mcp-servers/dist/feedback-reporter/index.d.ts.map +1 -0
  260. package/packages/mcp-servers/dist/feedback-reporter/index.js +9 -0
  261. package/packages/mcp-servers/dist/feedback-reporter/index.js.map +1 -0
  262. package/packages/mcp-servers/dist/feedback-reporter/server.d.ts +36 -0
  263. package/packages/mcp-servers/dist/feedback-reporter/server.d.ts.map +1 -0
  264. package/packages/mcp-servers/dist/feedback-reporter/server.js +392 -0
  265. package/packages/mcp-servers/dist/feedback-reporter/server.js.map +1 -0
  266. package/packages/mcp-servers/dist/feedback-reporter/types.d.ts +152 -0
  267. package/packages/mcp-servers/dist/feedback-reporter/types.d.ts.map +1 -0
  268. package/packages/mcp-servers/dist/feedback-reporter/types.js +67 -0
  269. package/packages/mcp-servers/dist/feedback-reporter/types.js.map +1 -0
  270. package/packages/mcp-servers/dist/github/index.d.ts +7 -0
  271. package/packages/mcp-servers/dist/github/index.d.ts.map +1 -0
  272. package/packages/mcp-servers/dist/github/index.js +7 -0
  273. package/packages/mcp-servers/dist/github/index.js.map +1 -0
  274. package/packages/mcp-servers/dist/github/server.d.ts +15 -0
  275. package/packages/mcp-servers/dist/github/server.d.ts.map +1 -0
  276. package/packages/mcp-servers/dist/github/server.js +686 -0
  277. package/packages/mcp-servers/dist/github/server.js.map +1 -0
  278. package/packages/mcp-servers/dist/github/types.d.ts +660 -0
  279. package/packages/mcp-servers/dist/github/types.d.ts.map +1 -0
  280. package/packages/mcp-servers/dist/github/types.js +209 -0
  281. package/packages/mcp-servers/dist/github/types.js.map +1 -0
  282. package/packages/mcp-servers/dist/index.d.ts +30 -0
  283. package/packages/mcp-servers/dist/index.d.ts.map +1 -0
  284. package/packages/mcp-servers/dist/index.js +32 -0
  285. package/packages/mcp-servers/dist/index.js.map +1 -0
  286. package/packages/mcp-servers/dist/makerkit-docs/index.d.ts +5 -0
  287. package/packages/mcp-servers/dist/makerkit-docs/index.d.ts.map +1 -0
  288. package/packages/mcp-servers/dist/makerkit-docs/index.js +5 -0
  289. package/packages/mcp-servers/dist/makerkit-docs/index.js.map +1 -0
  290. package/packages/mcp-servers/dist/makerkit-docs/server.d.ts +15 -0
  291. package/packages/mcp-servers/dist/makerkit-docs/server.d.ts.map +1 -0
  292. package/packages/mcp-servers/dist/makerkit-docs/server.js +252 -0
  293. package/packages/mcp-servers/dist/makerkit-docs/server.js.map +1 -0
  294. package/packages/mcp-servers/dist/makerkit-docs/types.d.ts +74 -0
  295. package/packages/mcp-servers/dist/makerkit-docs/types.d.ts.map +1 -0
  296. package/packages/mcp-servers/dist/makerkit-docs/types.js +20 -0
  297. package/packages/mcp-servers/dist/makerkit-docs/types.js.map +1 -0
  298. package/packages/mcp-servers/dist/onepassword/index.d.ts +2 -0
  299. package/packages/mcp-servers/dist/onepassword/index.d.ts.map +1 -0
  300. package/packages/mcp-servers/dist/onepassword/index.js +2 -0
  301. package/packages/mcp-servers/dist/onepassword/index.js.map +1 -0
  302. package/packages/mcp-servers/dist/onepassword/server.d.ts +2 -0
  303. package/packages/mcp-servers/dist/onepassword/server.d.ts.map +1 -0
  304. package/packages/mcp-servers/dist/onepassword/server.js +159 -0
  305. package/packages/mcp-servers/dist/onepassword/server.js.map +1 -0
  306. package/packages/mcp-servers/dist/onepassword/types.d.ts +55 -0
  307. package/packages/mcp-servers/dist/onepassword/types.d.ts.map +1 -0
  308. package/packages/mcp-servers/dist/onepassword/types.js +22 -0
  309. package/packages/mcp-servers/dist/onepassword/types.js.map +1 -0
  310. package/packages/mcp-servers/dist/playwright/helpers.d.ts +20 -0
  311. package/packages/mcp-servers/dist/playwright/helpers.d.ts.map +1 -0
  312. package/packages/mcp-servers/dist/playwright/helpers.js +31 -0
  313. package/packages/mcp-servers/dist/playwright/helpers.js.map +1 -0
  314. package/packages/mcp-servers/dist/playwright/index.d.ts +5 -0
  315. package/packages/mcp-servers/dist/playwright/index.d.ts.map +1 -0
  316. package/packages/mcp-servers/dist/playwright/index.js +5 -0
  317. package/packages/mcp-servers/dist/playwright/index.js.map +1 -0
  318. package/packages/mcp-servers/dist/playwright/server.d.ts +13 -0
  319. package/packages/mcp-servers/dist/playwright/server.d.ts.map +1 -0
  320. package/packages/mcp-servers/dist/playwright/server.js +1201 -0
  321. package/packages/mcp-servers/dist/playwright/server.js.map +1 -0
  322. package/packages/mcp-servers/dist/playwright/types.d.ts +216 -0
  323. package/packages/mcp-servers/dist/playwright/types.d.ts.map +1 -0
  324. package/packages/mcp-servers/dist/playwright/types.js +172 -0
  325. package/packages/mcp-servers/dist/playwright/types.js.map +1 -0
  326. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts +39 -0
  327. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts.map +1 -0
  328. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js +71 -0
  329. package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js.map +1 -0
  330. package/packages/mcp-servers/dist/playwright-feedback/index.d.ts +5 -0
  331. package/packages/mcp-servers/dist/playwright-feedback/index.d.ts.map +1 -0
  332. package/packages/mcp-servers/dist/playwright-feedback/index.js +5 -0
  333. package/packages/mcp-servers/dist/playwright-feedback/index.js.map +1 -0
  334. package/packages/mcp-servers/dist/playwright-feedback/server.d.ts +34 -0
  335. package/packages/mcp-servers/dist/playwright-feedback/server.d.ts.map +1 -0
  336. package/packages/mcp-servers/dist/playwright-feedback/server.js +538 -0
  337. package/packages/mcp-servers/dist/playwright-feedback/server.js.map +1 -0
  338. package/packages/mcp-servers/dist/playwright-feedback/types.d.ts +305 -0
  339. package/packages/mcp-servers/dist/playwright-feedback/types.d.ts.map +1 -0
  340. package/packages/mcp-servers/dist/playwright-feedback/types.js +123 -0
  341. package/packages/mcp-servers/dist/playwright-feedback/types.js.map +1 -0
  342. package/packages/mcp-servers/dist/product-manager/server.d.ts +17 -0
  343. package/packages/mcp-servers/dist/product-manager/server.d.ts.map +1 -0
  344. package/packages/mcp-servers/dist/product-manager/server.js +690 -0
  345. package/packages/mcp-servers/dist/product-manager/server.js.map +1 -0
  346. package/packages/mcp-servers/dist/product-manager/types.d.ts +286 -0
  347. package/packages/mcp-servers/dist/product-manager/types.d.ts.map +1 -0
  348. package/packages/mcp-servers/dist/product-manager/types.js +99 -0
  349. package/packages/mcp-servers/dist/product-manager/types.js.map +1 -0
  350. package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts +7 -0
  351. package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts.map +1 -0
  352. package/packages/mcp-servers/dist/programmatic-feedback/index.js +7 -0
  353. package/packages/mcp-servers/dist/programmatic-feedback/index.js.map +1 -0
  354. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts +19 -0
  355. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts.map +1 -0
  356. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js +174 -0
  357. package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js.map +1 -0
  358. package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts +35 -0
  359. package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts.map +1 -0
  360. package/packages/mcp-servers/dist/programmatic-feedback/server.js +465 -0
  361. package/packages/mcp-servers/dist/programmatic-feedback/server.js.map +1 -0
  362. package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts +127 -0
  363. package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts.map +1 -0
  364. package/packages/mcp-servers/dist/programmatic-feedback/types.js +80 -0
  365. package/packages/mcp-servers/dist/programmatic-feedback/types.js.map +1 -0
  366. package/packages/mcp-servers/dist/render/index.d.ts +8 -0
  367. package/packages/mcp-servers/dist/render/index.d.ts.map +1 -0
  368. package/packages/mcp-servers/dist/render/index.js +8 -0
  369. package/packages/mcp-servers/dist/render/index.js.map +1 -0
  370. package/packages/mcp-servers/dist/render/server.d.ts +15 -0
  371. package/packages/mcp-servers/dist/render/server.d.ts.map +1 -0
  372. package/packages/mcp-servers/dist/render/server.js +428 -0
  373. package/packages/mcp-servers/dist/render/server.js.map +1 -0
  374. package/packages/mcp-servers/dist/render/types.d.ts +273 -0
  375. package/packages/mcp-servers/dist/render/types.d.ts.map +1 -0
  376. package/packages/mcp-servers/dist/render/types.js +102 -0
  377. package/packages/mcp-servers/dist/render/types.js.map +1 -0
  378. package/packages/mcp-servers/dist/resend/index.d.ts +7 -0
  379. package/packages/mcp-servers/dist/resend/index.d.ts.map +1 -0
  380. package/packages/mcp-servers/dist/resend/index.js +7 -0
  381. package/packages/mcp-servers/dist/resend/index.js.map +1 -0
  382. package/packages/mcp-servers/dist/resend/server.d.ts +15 -0
  383. package/packages/mcp-servers/dist/resend/server.d.ts.map +1 -0
  384. package/packages/mcp-servers/dist/resend/server.js +298 -0
  385. package/packages/mcp-servers/dist/resend/server.js.map +1 -0
  386. package/packages/mcp-servers/dist/resend/types.d.ts +222 -0
  387. package/packages/mcp-servers/dist/resend/types.d.ts.map +1 -0
  388. package/packages/mcp-servers/dist/resend/types.js +58 -0
  389. package/packages/mcp-servers/dist/resend/types.js.map +1 -0
  390. package/packages/mcp-servers/dist/review-queue/index.d.ts +6 -0
  391. package/packages/mcp-servers/dist/review-queue/index.d.ts.map +1 -0
  392. package/packages/mcp-servers/dist/review-queue/index.js +6 -0
  393. package/packages/mcp-servers/dist/review-queue/index.js.map +1 -0
  394. package/packages/mcp-servers/dist/review-queue/server.d.ts +17 -0
  395. package/packages/mcp-servers/dist/review-queue/server.d.ts.map +1 -0
  396. package/packages/mcp-servers/dist/review-queue/server.js +348 -0
  397. package/packages/mcp-servers/dist/review-queue/server.js.map +1 -0
  398. package/packages/mcp-servers/dist/review-queue/types.d.ts +162 -0
  399. package/packages/mcp-servers/dist/review-queue/types.d.ts.map +1 -0
  400. package/packages/mcp-servers/dist/review-queue/types.js +56 -0
  401. package/packages/mcp-servers/dist/review-queue/types.js.map +1 -0
  402. package/packages/mcp-servers/dist/secret-sync/server.d.ts +19 -0
  403. package/packages/mcp-servers/dist/secret-sync/server.d.ts.map +1 -0
  404. package/packages/mcp-servers/dist/secret-sync/server.js +1139 -0
  405. package/packages/mcp-servers/dist/secret-sync/server.js.map +1 -0
  406. package/packages/mcp-servers/dist/secret-sync/types.d.ts +442 -0
  407. package/packages/mcp-servers/dist/secret-sync/types.d.ts.map +1 -0
  408. package/packages/mcp-servers/dist/secret-sync/types.js +113 -0
  409. package/packages/mcp-servers/dist/secret-sync/types.js.map +1 -0
  410. package/packages/mcp-servers/dist/session-events/index.d.ts +5 -0
  411. package/packages/mcp-servers/dist/session-events/index.d.ts.map +1 -0
  412. package/packages/mcp-servers/dist/session-events/index.js +5 -0
  413. package/packages/mcp-servers/dist/session-events/index.js.map +1 -0
  414. package/packages/mcp-servers/dist/session-events/server.d.ts +11 -0
  415. package/packages/mcp-servers/dist/session-events/server.d.ts.map +1 -0
  416. package/packages/mcp-servers/dist/session-events/server.js +290 -0
  417. package/packages/mcp-servers/dist/session-events/server.js.map +1 -0
  418. package/packages/mcp-servers/dist/session-events/types.d.ts +213 -0
  419. package/packages/mcp-servers/dist/session-events/types.d.ts.map +1 -0
  420. package/packages/mcp-servers/dist/session-events/types.js +69 -0
  421. package/packages/mcp-servers/dist/session-events/types.js.map +1 -0
  422. package/packages/mcp-servers/dist/session-restart/index.d.ts +9 -0
  423. package/packages/mcp-servers/dist/session-restart/index.d.ts.map +1 -0
  424. package/packages/mcp-servers/dist/session-restart/index.js +9 -0
  425. package/packages/mcp-servers/dist/session-restart/index.js.map +1 -0
  426. package/packages/mcp-servers/dist/session-restart/server.d.ts +20 -0
  427. package/packages/mcp-servers/dist/session-restart/server.d.ts.map +1 -0
  428. package/packages/mcp-servers/dist/session-restart/server.js +411 -0
  429. package/packages/mcp-servers/dist/session-restart/server.js.map +1 -0
  430. package/packages/mcp-servers/dist/session-restart/types.d.ts +26 -0
  431. package/packages/mcp-servers/dist/session-restart/types.d.ts.map +1 -0
  432. package/packages/mcp-servers/dist/session-restart/types.js +16 -0
  433. package/packages/mcp-servers/dist/session-restart/types.js.map +1 -0
  434. package/packages/mcp-servers/dist/setup-helper/index.d.ts +5 -0
  435. package/packages/mcp-servers/dist/setup-helper/index.d.ts.map +1 -0
  436. package/packages/mcp-servers/dist/setup-helper/index.js +5 -0
  437. package/packages/mcp-servers/dist/setup-helper/index.js.map +1 -0
  438. package/packages/mcp-servers/dist/setup-helper/server.d.ts +14 -0
  439. package/packages/mcp-servers/dist/setup-helper/server.d.ts.map +1 -0
  440. package/packages/mcp-servers/dist/setup-helper/server.js +454 -0
  441. package/packages/mcp-servers/dist/setup-helper/server.js.map +1 -0
  442. package/packages/mcp-servers/dist/setup-helper/types.d.ts +81 -0
  443. package/packages/mcp-servers/dist/setup-helper/types.d.ts.map +1 -0
  444. package/packages/mcp-servers/dist/setup-helper/types.js +41 -0
  445. package/packages/mcp-servers/dist/setup-helper/types.js.map +1 -0
  446. package/packages/mcp-servers/dist/shared/audited-server.d.ts +31 -0
  447. package/packages/mcp-servers/dist/shared/audited-server.d.ts.map +1 -0
  448. package/packages/mcp-servers/dist/shared/audited-server.js +126 -0
  449. package/packages/mcp-servers/dist/shared/audited-server.js.map +1 -0
  450. package/packages/mcp-servers/dist/shared/constants.d.ts +26 -0
  451. package/packages/mcp-servers/dist/shared/constants.d.ts.map +1 -0
  452. package/packages/mcp-servers/dist/shared/constants.js +41 -0
  453. package/packages/mcp-servers/dist/shared/constants.js.map +1 -0
  454. package/packages/mcp-servers/dist/shared/index.d.ts +6 -0
  455. package/packages/mcp-servers/dist/shared/index.d.ts.map +1 -0
  456. package/packages/mcp-servers/dist/shared/index.js +6 -0
  457. package/packages/mcp-servers/dist/shared/index.js.map +1 -0
  458. package/packages/mcp-servers/dist/shared/readonly-db.d.ts +11 -0
  459. package/packages/mcp-servers/dist/shared/readonly-db.d.ts.map +1 -0
  460. package/packages/mcp-servers/dist/shared/readonly-db.js +47 -0
  461. package/packages/mcp-servers/dist/shared/readonly-db.js.map +1 -0
  462. package/packages/mcp-servers/dist/shared/resolve-framework.d.ts +20 -0
  463. package/packages/mcp-servers/dist/shared/resolve-framework.d.ts.map +1 -0
  464. package/packages/mcp-servers/dist/shared/resolve-framework.js +65 -0
  465. package/packages/mcp-servers/dist/shared/resolve-framework.js.map +1 -0
  466. package/packages/mcp-servers/dist/shared/server.d.ts +86 -0
  467. package/packages/mcp-servers/dist/shared/server.d.ts.map +1 -0
  468. package/packages/mcp-servers/dist/shared/server.js +291 -0
  469. package/packages/mcp-servers/dist/shared/server.js.map +1 -0
  470. package/packages/mcp-servers/dist/shared/types.d.ts +113 -0
  471. package/packages/mcp-servers/dist/shared/types.d.ts.map +1 -0
  472. package/packages/mcp-servers/dist/shared/types.js +36 -0
  473. package/packages/mcp-servers/dist/shared/types.js.map +1 -0
  474. package/packages/mcp-servers/dist/show/server.d.ts +12 -0
  475. package/packages/mcp-servers/dist/show/server.d.ts.map +1 -0
  476. package/packages/mcp-servers/dist/show/server.js +97 -0
  477. package/packages/mcp-servers/dist/show/server.js.map +1 -0
  478. package/packages/mcp-servers/dist/show/types.d.ts +19 -0
  479. package/packages/mcp-servers/dist/show/types.d.ts.map +1 -0
  480. package/packages/mcp-servers/dist/show/types.js +32 -0
  481. package/packages/mcp-servers/dist/show/types.js.map +1 -0
  482. package/packages/mcp-servers/dist/specs-browser/index.d.ts +5 -0
  483. package/packages/mcp-servers/dist/specs-browser/index.d.ts.map +1 -0
  484. package/packages/mcp-servers/dist/specs-browser/index.js +5 -0
  485. package/packages/mcp-servers/dist/specs-browser/index.js.map +1 -0
  486. package/packages/mcp-servers/dist/specs-browser/server.d.ts +13 -0
  487. package/packages/mcp-servers/dist/specs-browser/server.d.ts.map +1 -0
  488. package/packages/mcp-servers/dist/specs-browser/server.js +692 -0
  489. package/packages/mcp-servers/dist/specs-browser/server.js.map +1 -0
  490. package/packages/mcp-servers/dist/specs-browser/types.d.ts +337 -0
  491. package/packages/mcp-servers/dist/specs-browser/types.d.ts.map +1 -0
  492. package/packages/mcp-servers/dist/specs-browser/types.js +134 -0
  493. package/packages/mcp-servers/dist/specs-browser/types.js.map +1 -0
  494. package/packages/mcp-servers/dist/supabase/index.d.ts +10 -0
  495. package/packages/mcp-servers/dist/supabase/index.d.ts.map +1 -0
  496. package/packages/mcp-servers/dist/supabase/index.js +10 -0
  497. package/packages/mcp-servers/dist/supabase/index.js.map +1 -0
  498. package/packages/mcp-servers/dist/supabase/server.d.ts +20 -0
  499. package/packages/mcp-servers/dist/supabase/server.d.ts.map +1 -0
  500. package/packages/mcp-servers/dist/supabase/server.js +451 -0
  501. package/packages/mcp-servers/dist/supabase/server.js.map +1 -0
  502. package/packages/mcp-servers/dist/supabase/types.d.ts +196 -0
  503. package/packages/mcp-servers/dist/supabase/types.d.ts.map +1 -0
  504. package/packages/mcp-servers/dist/supabase/types.js +76 -0
  505. package/packages/mcp-servers/dist/supabase/types.js.map +1 -0
  506. package/packages/mcp-servers/dist/todo-db/index.d.ts +5 -0
  507. package/packages/mcp-servers/dist/todo-db/index.d.ts.map +1 -0
  508. package/packages/mcp-servers/dist/todo-db/index.js +5 -0
  509. package/packages/mcp-servers/dist/todo-db/index.js.map +1 -0
  510. package/packages/mcp-servers/dist/todo-db/server.d.ts +13 -0
  511. package/packages/mcp-servers/dist/todo-db/server.d.ts.map +1 -0
  512. package/packages/mcp-servers/dist/todo-db/server.js +649 -0
  513. package/packages/mcp-servers/dist/todo-db/server.js.map +1 -0
  514. package/packages/mcp-servers/dist/todo-db/types.d.ts +225 -0
  515. package/packages/mcp-servers/dist/todo-db/types.d.ts.map +1 -0
  516. package/packages/mcp-servers/dist/todo-db/types.js +69 -0
  517. package/packages/mcp-servers/dist/todo-db/types.js.map +1 -0
  518. package/packages/mcp-servers/dist/user-feedback/index.d.ts +7 -0
  519. package/packages/mcp-servers/dist/user-feedback/index.d.ts.map +1 -0
  520. package/packages/mcp-servers/dist/user-feedback/index.js +8 -0
  521. package/packages/mcp-servers/dist/user-feedback/index.js.map +1 -0
  522. package/packages/mcp-servers/dist/user-feedback/server.d.ts +25 -0
  523. package/packages/mcp-servers/dist/user-feedback/server.d.ts.map +1 -0
  524. package/packages/mcp-servers/dist/user-feedback/server.js +914 -0
  525. package/packages/mcp-servers/dist/user-feedback/server.js.map +1 -0
  526. package/packages/mcp-servers/dist/user-feedback/types.d.ts +415 -0
  527. package/packages/mcp-servers/dist/user-feedback/types.d.ts.map +1 -0
  528. package/packages/mcp-servers/dist/user-feedback/types.js +132 -0
  529. package/packages/mcp-servers/dist/user-feedback/types.js.map +1 -0
  530. package/packages/mcp-servers/dist/vercel/index.d.ts +9 -0
  531. package/packages/mcp-servers/dist/vercel/index.d.ts.map +1 -0
  532. package/packages/mcp-servers/dist/vercel/index.js +9 -0
  533. package/packages/mcp-servers/dist/vercel/index.js.map +1 -0
  534. package/packages/mcp-servers/dist/vercel/server.d.ts +17 -0
  535. package/packages/mcp-servers/dist/vercel/server.d.ts.map +1 -0
  536. package/packages/mcp-servers/dist/vercel/server.js +265 -0
  537. package/packages/mcp-servers/dist/vercel/server.js.map +1 -0
  538. package/packages/mcp-servers/dist/vercel/types.d.ts +189 -0
  539. package/packages/mcp-servers/dist/vercel/types.d.ts.map +1 -0
  540. package/packages/mcp-servers/dist/vercel/types.js +65 -0
  541. package/packages/mcp-servers/dist/vercel/types.js.map +1 -0
  542. package/packages/mcp-servers/package-lock.json +3765 -0
  543. package/packages/mcp-servers/package.json +64 -0
  544. package/packages/mcp-servers/test/reporters/test-failure-reporter.ts +372 -0
  545. package/packages/mcp-servers/vitest.config.ts +27 -0
  546. package/scripts/__tests__/README.md +163 -0
  547. package/scripts/apply-credential-hardening.sh +271 -0
  548. package/scripts/credential-providers/manual.js +56 -0
  549. package/scripts/credential-providers/onepassword.js +85 -0
  550. package/scripts/credential-providers/provider-interface.js +104 -0
  551. package/scripts/encrypt-credential.js +337 -0
  552. package/scripts/feedback-launcher.js +338 -0
  553. package/scripts/feedback-orchestrator.js +373 -0
  554. package/scripts/fix-mcp-launcher-issues.sh +97 -0
  555. package/scripts/force-spawn-tasks.js +651 -0
  556. package/scripts/force-triage-reports.js +560 -0
  557. package/scripts/generate-protected-actions-spec.js +142 -0
  558. package/scripts/generate-proxy-certs.sh +158 -0
  559. package/scripts/grant-chrome-ext-permissions.sh +242 -0
  560. package/scripts/mcp-launcher.js +125 -0
  561. package/scripts/merge-settings.cjs +167 -0
  562. package/scripts/patch-clawd.py +844 -0
  563. package/scripts/patch-credential-cache.py +313 -0
  564. package/scripts/patches/credential-file-guard-patched.mjs +573 -0
  565. package/scripts/patches/credential-file-guard.js.patched +573 -0
  566. package/scripts/patches/verify-tokenizer.mjs +132 -0
  567. package/scripts/protect-framework.sh +478 -0
  568. package/scripts/readme-chrome.template +12 -0
  569. package/scripts/reap-completed-agents.js +439 -0
  570. package/scripts/reinstall.sh +86 -0
  571. package/scripts/resign-node.sh +185 -0
  572. package/scripts/rotation-proxy.js +656 -0
  573. package/scripts/rotation-stress-monitor.mjs +862 -0
  574. package/scripts/setup-automation-service.sh +648 -0
  575. package/scripts/setup-check.js +251 -0
  576. package/scripts/watch-claude-version.js +142 -0
  577. package/specs/framework/CORE-INVARIANTS.md +161 -0
  578. package/specs/patterns/AGENT-PATTERNS.md +223 -0
  579. package/specs/patterns/HOOK-PATTERNS.md +242 -0
  580. package/specs/patterns/MCP-SERVER-PATTERNS.md +144 -0
  581. package/templates/config/gitignore.template +14 -0
  582. package/templates/config/merge-chain-check.yml.template +51 -0
  583. package/templates/config/package.json.template +18 -0
  584. package/templates/config/pnpm-workspace.yaml +5 -0
  585. package/templates/config/services.json.template +18 -0
  586. package/templates/config/tsconfig.base.json +17 -0
  587. package/templates/scaffold/integrations/_template/.gitkeep +0 -0
  588. package/templates/scaffold/packages/logger/package.json +17 -0
  589. package/templates/scaffold/packages/logger/src/logger.ts +44 -0
  590. package/templates/scaffold/packages/shared/package.json +17 -0
  591. package/templates/scaffold/packages/shared/src/errors.ts +43 -0
  592. package/templates/scaffold/products/_product/apps/backend/package.json +21 -0
  593. package/templates/scaffold/products/_product/apps/backend/src/index.ts +17 -0
  594. package/templates/scaffold/products/_product/apps/extension/.gitkeep +0 -0
  595. package/templates/scaffold/products/_product/apps/web/.gitkeep +0 -0
  596. package/templates/scaffold/specs/global/.gitkeep +0 -0
  597. package/templates/scaffold/specs/local/.gitkeep +0 -0
  598. package/templates/scaffold/specs/reference/.gitkeep +0 -0
  599. 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 };