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,791 @@
1
+ /**
2
+ * Usage Optimizer
3
+ *
4
+ * Tracks API quota utilization every 10 minutes and dynamically adjusts
5
+ * automation spawn rates to target 90% usage at reset time.
6
+ *
7
+ * Called as the first step in hourly-automation.js on every 10-minute invocation.
8
+ *
9
+ * Process:
10
+ * 1. Snapshot: Fetch usage from Anthropic API for all keys in api-key-rotation.json
11
+ * 2. Trajectory: Calculate linear usage rate from recent snapshots (needs 3+)
12
+ * 3. Adjustment: Compare projected-at-reset to 90% target, compute factor
13
+ *
14
+ * @version 1.0.0
15
+ */
16
+
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import * as os from 'os';
20
+ import { execFileSync } from 'child_process';
21
+ import { getConfigPath, getDefaults } from './config-reader.js';
22
+
23
+ const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
24
+ const ROTATION_STATE_PATH = path.join(os.homedir(), '.claude', 'api-key-rotation.json');
25
+ const OLD_PROJECT_ROTATION_PATH = path.join(PROJECT_DIR, '.claude', 'api-key-rotation.json');
26
+ const SNAPSHOTS_PATH = path.join(PROJECT_DIR, '.claude', 'state', 'usage-snapshots.json');
27
+ const CREDENTIALS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
28
+ const ANTHROPIC_API_URL = 'https://api.anthropic.com/api/oauth/usage';
29
+ const ANTHROPIC_BETA_HEADER = 'oauth-2025-04-20';
30
+
31
+ const TARGET_UTILIZATION = 0.90;
32
+ const MAX_FACTOR = 20.0; // up to 20x speedup (MIN_EFFECTIVE_MINUTES is the real ceiling)
33
+ const MIN_FACTOR = 0.05; // up to 20x slowdown
34
+ const MAX_CHANGE_PER_CYCLE = 0.10; // ±10% per cycle
35
+ const SNAPSHOT_RETENTION_DAYS = 7;
36
+ const MIN_SNAPSHOTS_FOR_TRAJECTORY = 3;
37
+ const MIN_EFFECTIVE_MINUTES = 5; // Floor: no cooldown can go below 5 minutes
38
+ const MAX_COOLDOWN_MINUTES = {
39
+ production_health_monitor: 120, // 2h max (default 60) — never throttle production health beyond 2h
40
+ staging_health_monitor: 360, // 6h max (default 180) — cap staging health throttling
41
+ triage_check: 15, // 15min max (default 5) — keep triage responsive
42
+ };
43
+ const SINGLE_KEY_WARNING_THRESHOLD = 0.80; // Warn when any key exceeds 80%
44
+ const PER_KEY_RESET_DROP_THRESHOLD = 0.50; // Detect reset when ANY key's 5h drops >50pp
45
+ const MIN_SNAPSHOT_INTERVAL_MS = 5 * 60 * 1000; // 5 min — throttle between snapshots
46
+ const EMA_WINDOW_MS = 2 * 60 * 60 * 1000; // 2 hours — time window for EMA
47
+ const EMA_MIN_INTERVAL_MS = 5 * 60 * 1000; // 5 min — dedup interval for EMA input
48
+ const MIN_HOURS_DELTA = 0.05; // 3 min — floor for EMA interval pairs
49
+
50
+ /**
51
+ * Revert overdrive state, restoring previous effective values and factor.
52
+ */
53
+ function revertOverdrive(config, log) {
54
+ const prev = config.overdrive.previous_state;
55
+ if (prev?.effective) {
56
+ config.effective = prev.effective;
57
+ }
58
+ if (prev?.factor !== undefined) {
59
+ const restoredFactor = Math.max(MIN_FACTOR, Math.min(MAX_FACTOR, prev.factor));
60
+ config.adjustment = config.adjustment || {};
61
+ config.adjustment.factor = restoredFactor;
62
+ config.adjustment.last_updated = new Date().toISOString();
63
+ config.adjustment.direction = 'overdrive-reverted';
64
+ }
65
+ config.overdrive.active = false;
66
+
67
+ const configPath = getConfigPath();
68
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
69
+ log(`Usage optimizer: Overdrive expired, reverted to previous state (factor: ${prev?.factor ?? 1.0})`);
70
+ }
71
+
72
+ /**
73
+ * Get the timestamp of the most recent snapshot from the snapshots file.
74
+ * Returns null if no snapshots exist or file is unreadable.
75
+ */
76
+ function getLastSnapshotTimestamp() {
77
+ try {
78
+ if (!fs.existsSync(SNAPSHOTS_PATH)) return null;
79
+ const data = JSON.parse(fs.readFileSync(SNAPSHOTS_PATH, 'utf8'));
80
+ if (!data || !Array.isArray(data.snapshots) || data.snapshots.length === 0) return null;
81
+ return data.snapshots[data.snapshots.length - 1]?.ts || null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Select time-based snapshots from an array, deduplicating by minimum interval.
89
+ * Walks backward from the most recent snapshot, only including entries at least
90
+ * minIntervalMs apart, stopping when windowMs is exceeded.
91
+ *
92
+ * @param {Array} snapshots - All snapshots (must have .ts)
93
+ * @param {number} windowMs - Maximum time window from most recent snapshot
94
+ * @param {number} minIntervalMs - Minimum interval between selected snapshots
95
+ * @returns {Array} Selected snapshots in chronological order
96
+ */
97
+ function selectTimeBasedSnapshots(snapshots, windowMs, minIntervalMs) {
98
+ if (!snapshots || snapshots.length === 0) return [];
99
+
100
+ const latest = snapshots[snapshots.length - 1];
101
+ const windowStart = latest.ts - windowMs;
102
+ const selected = [latest];
103
+ let lastSelectedTs = latest.ts;
104
+
105
+ for (let i = snapshots.length - 2; i >= 0; i--) {
106
+ const s = snapshots[i];
107
+ if (s.ts < windowStart) break;
108
+ if (lastSelectedTs - s.ts >= minIntervalMs) {
109
+ selected.push(s);
110
+ lastSelectedTs = s.ts;
111
+ }
112
+ }
113
+
114
+ // Fall back to array tail if window yields fewer than 3 snapshots (cold start)
115
+ if (selected.length < 3) {
116
+ return snapshots.slice(-30);
117
+ }
118
+
119
+ return selected.reverse(); // chronological order
120
+ }
121
+
122
+ /**
123
+ * Main entry point - run the usage optimizer.
124
+ * Designed to be cheap and fast (one API call + math).
125
+ *
126
+ * @param {function} [logFn] - Optional log function (default: console.log)
127
+ * @returns {Promise<{success: boolean, snapshotTaken: boolean, adjustmentMade: boolean, error?: string}>}
128
+ */
129
+ export async function runUsageOptimizer(logFn) {
130
+ const log = logFn || console.log;
131
+
132
+ try {
133
+ // Snapshot throttle: skip collection if last snapshot is less than 5 minutes old
134
+ const lastTs = getLastSnapshotTimestamp();
135
+ if (lastTs && (Date.now() - lastTs) < MIN_SNAPSHOT_INTERVAL_MS) {
136
+ return { success: true, snapshotTaken: false, adjustmentMade: false };
137
+ }
138
+
139
+ // Overdrive check: skip adjustment if overdrive is active
140
+ try {
141
+ const configPath = getConfigPath();
142
+ if (fs.existsSync(configPath)) {
143
+ const overdriveConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
144
+ if (overdriveConfig.overdrive?.active) {
145
+ if (new Date() > new Date(overdriveConfig.overdrive.expires_at)) {
146
+ // Expired - revert and continue with normal adjustment
147
+ revertOverdrive(overdriveConfig, log);
148
+ } else {
149
+ // Active - take snapshot but skip adjustment
150
+ log(`Usage optimizer: Overdrive active until ${overdriveConfig.overdrive.expires_at}, skipping cooldown adjustment.`);
151
+ const snapshot = await collectSnapshot(log);
152
+ if (snapshot) {
153
+ storeSnapshot(snapshot, log);
154
+ }
155
+ return { success: true, snapshotTaken: !!snapshot, adjustmentMade: false };
156
+ }
157
+ }
158
+ }
159
+ } catch (err) {
160
+ log(`Usage optimizer: Overdrive check failed (non-fatal): ${err.message}`);
161
+ }
162
+
163
+ // Step 1: Collect usage snapshot
164
+ const snapshot = await collectSnapshot(log);
165
+ if (!snapshot) {
166
+ return { success: true, snapshotTaken: false, adjustmentMade: false };
167
+ }
168
+
169
+ // Step 2: Store snapshot
170
+ storeSnapshot(snapshot, log);
171
+
172
+ // Step 3: Calculate trajectory and adjust (if enough data)
173
+ const adjusted = calculateAndAdjust(log);
174
+
175
+ return { success: true, snapshotTaken: true, adjustmentMade: adjusted };
176
+ } catch (err) {
177
+ log(`Usage optimizer error: ${err.message}`);
178
+ return { success: false, snapshotTaken: false, adjustmentMade: false, error: err.message };
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Fetch usage data from all tracked API keys.
184
+ * Returns null if no keys available or API unreachable.
185
+ */
186
+ async function collectSnapshot(log) {
187
+ const keys = getApiKeys();
188
+ if (keys.length === 0) {
189
+ log('Usage optimizer: No API keys found, skipping snapshot.');
190
+ return null;
191
+ }
192
+
193
+ const ts = Date.now();
194
+ const rawKeyData = {};
195
+ const keyLookup = new Map();
196
+
197
+ for (const key of keys) {
198
+ keyLookup.set(key.id, key);
199
+ try {
200
+ const usage = await fetchUsage(key.accessToken);
201
+ if (usage) {
202
+ rawKeyData[key.id] = {
203
+ '5h': (usage.fiveHour.utilization ?? 0) / 100,
204
+ '5h_reset': usage.fiveHour.resetsAt,
205
+ '7d': (usage.sevenDay.utilization ?? 0) / 100,
206
+ '7d_reset': usage.sevenDay.resetsAt,
207
+ };
208
+ }
209
+ } catch (err) {
210
+ log(`Usage optimizer: Failed to fetch usage for key ${key.id}: ${err.message}`);
211
+ }
212
+ }
213
+
214
+ if (Object.keys(rawKeyData).length === 0) {
215
+ log('Usage optimizer: No usage data retrieved, skipping snapshot.');
216
+ return null;
217
+ }
218
+
219
+ // Deduplicate: group by account, keep first key per account.
220
+ // Uses account_uuid as primary dedup key, falls back to a fingerprint
221
+ // of usage values (same principle as cto-notification-hook.js).
222
+ const accountMap = new Map();
223
+ for (const [keyId, usage] of Object.entries(rawKeyData)) {
224
+ const key = keyLookup.get(keyId);
225
+ const dedupeKey = key?.accountId
226
+ || `fp:${usage['5h']}:${usage['7d']}`;
227
+ if (!accountMap.has(dedupeKey)) {
228
+ accountMap.set(dedupeKey, { id: keyId, usage });
229
+ }
230
+ }
231
+
232
+ // Build snapshot from deduplicated accounts
233
+ const keyData = {};
234
+ for (const [, entry] of accountMap) {
235
+ keyData[entry.id] = entry.usage;
236
+ }
237
+
238
+ return { ts, keys: keyData };
239
+ }
240
+
241
+ /**
242
+ * Get API keys from rotation state or credentials file.
243
+ * Returns array of { id, accessToken }.
244
+ */
245
+ function getApiKeys() {
246
+ const keys = [];
247
+ const now = Date.now();
248
+
249
+ // Source 1: Environment variable override (highest priority)
250
+ const envToken = process.env['CLAUDE_CODE_OAUTH_TOKEN'];
251
+ if (envToken) {
252
+ return [{ id: 'env', accessToken: envToken, accountId: null }];
253
+ }
254
+
255
+ // Source 2: Rotation state (multiple keys) - check user-level, fallback to project-level
256
+ const rotationPath = fs.existsSync(ROTATION_STATE_PATH) ? ROTATION_STATE_PATH
257
+ : fs.existsSync(OLD_PROJECT_ROTATION_PATH) ? OLD_PROJECT_ROTATION_PATH
258
+ : null;
259
+ if (rotationPath) {
260
+ try {
261
+ const state = JSON.parse(fs.readFileSync(rotationPath, 'utf8'));
262
+ if (state && state.keys && typeof state.keys === 'object') {
263
+ for (const [id, data] of Object.entries(state.keys)) {
264
+ if (!data.accessToken) continue;
265
+ // Skip expired or invalid keys
266
+ if (data.status === 'expired' || data.status === 'invalid') continue;
267
+ if (data.expiresAt && data.expiresAt < now) continue;
268
+ keys.push({ id: id.substring(0, 8), accessToken: data.accessToken, accountId: data.account_uuid || null });
269
+ }
270
+ }
271
+ } catch (err) {
272
+ console.error(`[usage-optimizer] Failed to read rotation state: ${err.message}`);
273
+ }
274
+ }
275
+
276
+ // Source 3: macOS Keychain (matches data-reader.ts:getCredentialToken)
277
+ if (keys.length === 0 && process.platform === 'darwin') {
278
+ try {
279
+ const { username } = os.userInfo();
280
+ const raw = execFileSync('security', [
281
+ 'find-generic-password', '-s', 'Claude Code-credentials', '-a', username, '-w',
282
+ ], { encoding: 'utf8', timeout: 3000 }).trim();
283
+ const creds = JSON.parse(raw);
284
+ if (creds?.claudeAiOauth?.accessToken) {
285
+ const expiresAt = creds.claudeAiOauth.expiresAt;
286
+ if (!expiresAt || expiresAt > now) {
287
+ keys.push({ id: 'keychain', accessToken: creds.claudeAiOauth.accessToken, accountId: null });
288
+ }
289
+ }
290
+ } catch {
291
+ // Keychain not available (locked, no entry, or non-macOS)
292
+ }
293
+ }
294
+
295
+ // Source 4: Credentials file (~/.claude/.credentials.json)
296
+ if (keys.length === 0 && fs.existsSync(CREDENTIALS_PATH)) {
297
+ try {
298
+ const creds = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
299
+ if (creds?.claudeAiOauth?.accessToken) {
300
+ keys.push({ id: 'default', accessToken: creds.claudeAiOauth.accessToken, accountId: null });
301
+ }
302
+ } catch (err) {
303
+ console.error(`[usage-optimizer] Failed to read credentials: ${err.message}`);
304
+ }
305
+ }
306
+
307
+ return keys;
308
+ }
309
+
310
+ /**
311
+ * Fetch usage from Anthropic API for a single key.
312
+ * Returns { fiveHour: { utilization, resetsAt }, sevenDay: { utilization, resetsAt } } or null.
313
+ */
314
+ async function fetchUsage(accessToken) {
315
+ const response = await fetch(ANTHROPIC_API_URL, {
316
+ method: 'GET',
317
+ headers: {
318
+ 'Authorization': `Bearer ${accessToken}`,
319
+ 'Content-Type': 'application/json',
320
+ 'User-Agent': 'claude-code/2.1.14',
321
+ 'anthropic-beta': ANTHROPIC_BETA_HEADER,
322
+ },
323
+ });
324
+
325
+ if (!response.ok) {
326
+ return null;
327
+ }
328
+
329
+ const data = await response.json();
330
+
331
+ return {
332
+ fiveHour: {
333
+ utilization: data.five_hour?.utilization ?? 0,
334
+ resetsAt: data.five_hour?.resets_at ?? null,
335
+ },
336
+ sevenDay: {
337
+ utilization: data.seven_day?.utilization ?? 0,
338
+ resetsAt: data.seven_day?.resets_at ?? null,
339
+ },
340
+ };
341
+ }
342
+
343
+ /**
344
+ * Store a snapshot and prune old entries.
345
+ */
346
+ function storeSnapshot(snapshot, log) {
347
+ let data = { snapshots: [] };
348
+
349
+ if (fs.existsSync(SNAPSHOTS_PATH)) {
350
+ try {
351
+ data = JSON.parse(fs.readFileSync(SNAPSHOTS_PATH, 'utf8'));
352
+ if (!data || !Array.isArray(data.snapshots)) {
353
+ data = { snapshots: [] };
354
+ }
355
+ } catch {
356
+ data = { snapshots: [] };
357
+ }
358
+ }
359
+
360
+ // Migrate old-format snapshots (0-100 scale → 0-1 fraction)
361
+ for (const s of data.snapshots) {
362
+ if (!s.keys) continue;
363
+ for (const k of Object.values(s.keys)) {
364
+ if ((k['5h'] ?? 0) > 1.0) k['5h'] = k['5h'] / 100;
365
+ if ((k['7d'] ?? 0) > 1.0) k['7d'] = k['7d'] / 100;
366
+ }
367
+ }
368
+
369
+ data.snapshots.push(snapshot);
370
+
371
+ // Prune entries older than retention period
372
+ const cutoff = Date.now() - (SNAPSHOT_RETENTION_DAYS * 24 * 60 * 60 * 1000);
373
+ data.snapshots = data.snapshots.filter(s => s.ts > cutoff);
374
+
375
+ try {
376
+ fs.writeFileSync(SNAPSHOTS_PATH, JSON.stringify(data, null, 2));
377
+ } catch (err) {
378
+ log(`Usage optimizer: Failed to write snapshots: ${err.message}`);
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Calculate trajectory and adjust cooldowns.
384
+ * Returns true if an adjustment was made.
385
+ */
386
+ function calculateAndAdjust(log) {
387
+ let data;
388
+ try {
389
+ data = JSON.parse(fs.readFileSync(SNAPSHOTS_PATH, 'utf8'));
390
+ } catch {
391
+ return false;
392
+ }
393
+
394
+ if (!data || !data.snapshots || data.snapshots.length < MIN_SNAPSHOTS_FOR_TRAJECTORY) {
395
+ log(`Usage optimizer: Only ${data?.snapshots?.length ?? 0} snapshots, need ${MIN_SNAPSHOTS_FOR_TRAJECTORY}. Skipping adjustment.`);
396
+ return false;
397
+ }
398
+
399
+ // Reset-boundary detection: if ANY single key's 5h utilization dropped >50pp
400
+ // between consecutive snapshots, a window reset just happened. Skip this cycle
401
+ // to avoid the stale rate causing the factor to ramp up blindly.
402
+ // Per-key detection avoids dilution when some keys are already at 0%.
403
+ if (data.snapshots.length >= 2) {
404
+ const prev = data.snapshots[data.snapshots.length - 2];
405
+ const curr = data.snapshots[data.snapshots.length - 1];
406
+ const commonKeys = Object.keys(curr.keys).filter(k => k in prev.keys);
407
+ for (const k of commonKeys) {
408
+ const keyDrop = (prev.keys[k]['5h'] ?? 0) - (curr.keys[k]['5h'] ?? 0);
409
+ if (keyDrop >= PER_KEY_RESET_DROP_THRESHOLD) {
410
+ log(`Usage optimizer: Reset boundary detected (key ${k} 5h dropped ${Math.round(keyDrop * 100)}pp). Skipping adjustment cycle.`);
411
+ return false;
412
+ }
413
+ }
414
+ }
415
+
416
+ // Key-count discontinuity guard: when key count changes between consecutive
417
+ // snapshots (key discovery/removal), skip one cycle to let EMA stabilize.
418
+ if (data.snapshots.length >= 2) {
419
+ const prevSnap = data.snapshots[data.snapshots.length - 2];
420
+ const currSnap = data.snapshots[data.snapshots.length - 1];
421
+ const prevKeyCount = Object.keys(prevSnap.keys).length;
422
+ const currKeyCount = Object.keys(currSnap.keys).length;
423
+ if (prevKeyCount !== currKeyCount) {
424
+ log(`Usage optimizer: Key count changed (${prevKeyCount} → ${currKeyCount}). Skipping adjustment cycle to stabilize EMA.`);
425
+ return false;
426
+ }
427
+ }
428
+
429
+ // Get the most relevant metrics (aggregate across keys)
430
+ const latest = data.snapshots[data.snapshots.length - 1];
431
+ const timeBasedWindow = selectTimeBasedSnapshots(data.snapshots, EMA_WINDOW_MS, EMA_MIN_INTERVAL_MS);
432
+ const earliest = timeBasedWindow[0]; // First snapshot in time-based window
433
+
434
+ const hoursBetween = (latest.ts - earliest.ts) / (1000 * 60 * 60);
435
+ if (hoursBetween < 0.15) { // Less than ~10 minutes of data
436
+ log('Usage optimizer: Not enough time span for trajectory. Skipping.');
437
+ return false;
438
+ }
439
+
440
+ // Calculate aggregate metrics across all keys (with EMA from all snapshots)
441
+ const aggregate = calculateAggregate(latest, earliest, hoursBetween, data.snapshots);
442
+ if (!aggregate) {
443
+ log('Usage optimizer: Could not calculate aggregate metrics.');
444
+ return false;
445
+ }
446
+
447
+ // Determine constraining metric
448
+ // Cap projections to prevent runaway extrapolation — linear rate projection
449
+ // over long horizons (e.g. 155h for 7d) produces nonsensical values that
450
+ // pin the factor at MIN_FACTOR permanently.
451
+ const MAX_PROJECTION = 1.5;
452
+ const projected5h = Math.min(MAX_PROJECTION, aggregate.current5h + (aggregate.rate5h * aggregate.hoursUntil5hReset));
453
+ const projected7d = Math.min(MAX_PROJECTION, aggregate.current7d + (aggregate.rate7d * aggregate.hoursUntil7dReset));
454
+ const constraining = projected5h > projected7d ? '5h' : '7d';
455
+ const projectedAtReset = Math.max(projected5h, projected7d);
456
+
457
+ // Read current config
458
+ const configPath = getConfigPath();
459
+ let config;
460
+ try {
461
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
462
+ } catch {
463
+ log('Usage optimizer: Config file unreadable, skipping adjustment.');
464
+ return false;
465
+ }
466
+
467
+ const currentFactor = config.adjustment?.factor ?? 1.0;
468
+ const currentUsage = constraining === '5h' ? aggregate.current5h : aggregate.current7d;
469
+ const currentRate = constraining === '5h' ? aggregate.rate5h : aggregate.rate7d;
470
+ const hoursUntilReset = constraining === '5h' ? aggregate.hoursUntil5hReset : aggregate.hoursUntil7dReset;
471
+
472
+ // Tier 1 recovery: if factor is stuck at minimum but current usage is well below
473
+ // target, the projection model was unreliable. Reset factor to baseline so
474
+ // automations aren't permanently throttled.
475
+ // 0.15 threshold = 85%+ slower than default; catches factors that drifted very low
476
+ if (currentFactor <= 0.15 && currentUsage < TARGET_UTILIZATION * 0.7) {
477
+ applyFactor(config, 1.0, constraining, projectedAtReset, log, hoursUntilReset);
478
+ log(`Usage optimizer: Factor recovery (tier 1) — usage at ${Math.round(currentUsage * 100)}% (well below ${Math.round(TARGET_UTILIZATION * 100)}% target) but factor stuck at minimum. Reset to 1.0.`);
479
+ return true;
480
+ }
481
+
482
+ // Tier 2 recovery: gradual recovery for factors in the 0.15-0.5 dead zone.
483
+ // The 5% per-cycle conservative speedup is too slow to recover from this range.
484
+ if (currentFactor < 0.5 && currentFactor > 0.15 && currentUsage < TARGET_UTILIZATION * 0.8) {
485
+ const recoveredFactor = Math.min(1.0, currentFactor * 1.5);
486
+ applyFactor(config, recoveredFactor, constraining, projectedAtReset, log, hoursUntilReset);
487
+ log(`Usage optimizer: Factor recovery (tier 2) — usage at ${Math.round(currentUsage * 100)}% with factor at ${currentFactor.toFixed(3)}. Boosted to ${recoveredFactor.toFixed(3)}.`);
488
+ return true;
489
+ }
490
+
491
+ // Per-key warnings: flag any key exceeding the warning threshold
492
+ if (aggregate.perKeyUtilization) {
493
+ for (const [keyId, util] of Object.entries(aggregate.perKeyUtilization)) {
494
+ if (util['5h'] >= SINGLE_KEY_WARNING_THRESHOLD) {
495
+ log(`Usage optimizer WARNING: Account ${keyId} at ${Math.round(util['5h'] * 100)}% 5h utilization`);
496
+ }
497
+ if (util['7d'] >= SINGLE_KEY_WARNING_THRESHOLD) {
498
+ log(`Usage optimizer WARNING: Account ${keyId} at ${Math.round(util['7d'] * 100)}% 7d utilization`);
499
+ }
500
+ }
501
+ }
502
+
503
+ // Bias currentUsage upward if any single key is near exhaustion
504
+ let effectiveUsage = currentUsage;
505
+ const maxKeyUsage = constraining === '5h' ? aggregate.maxKey5h : aggregate.maxKey7d;
506
+ if (maxKeyUsage >= SINGLE_KEY_WARNING_THRESHOLD) {
507
+ effectiveUsage = Math.max(effectiveUsage, maxKeyUsage * 0.8);
508
+ }
509
+
510
+ // Edge case: already at or above target
511
+ if (effectiveUsage >= TARGET_UTILIZATION) {
512
+ // Never speed up if already near cap - clamp factor to <= 1.0
513
+ const newFactor = Math.min(currentFactor, 1.0);
514
+ if (newFactor !== currentFactor) {
515
+ applyFactor(config, newFactor, constraining, projectedAtReset, log, hoursUntilReset);
516
+ return true;
517
+ }
518
+ log(`Usage optimizer: Already at ${Math.round(effectiveUsage * 100)}% usage. Holding steady. Reset in ${hoursUntilReset.toFixed(1)}h.`);
519
+ return false;
520
+ }
521
+
522
+ // Edge case: rate is zero or negative (usage flat/decreasing)
523
+ if (currentRate <= 0) {
524
+ // Conservatively speed up (5% per cycle, capped at MAX_FACTOR)
525
+ const newFactor = Math.min(currentFactor * 1.05, MAX_FACTOR);
526
+ if (Math.abs(newFactor - currentFactor) > 0.001) {
527
+ applyFactor(config, newFactor, constraining, projectedAtReset, log, hoursUntilReset);
528
+ return true;
529
+ }
530
+ return false;
531
+ }
532
+
533
+ // Normal case: calculate desired rate to hit target at reset
534
+ const desiredRate = (TARGET_UTILIZATION - effectiveUsage) / hoursUntilReset;
535
+ const rawRatio = desiredRate / currentRate;
536
+
537
+ // Conservative bounds: max ±10% per cycle
538
+ const clamped = Math.max(1.0 - MAX_CHANGE_PER_CYCLE, Math.min(1.0 + MAX_CHANGE_PER_CYCLE, rawRatio));
539
+ let newFactor = currentFactor * clamped;
540
+
541
+ // Overall bounds
542
+ newFactor = Math.max(MIN_FACTOR, Math.min(MAX_FACTOR, newFactor));
543
+
544
+ // Only apply if meaningful change
545
+ if (Math.abs(newFactor - currentFactor) < 0.01) {
546
+ log(`Usage optimizer: Factor unchanged (${currentFactor.toFixed(2)}). On track for ${Math.round(projectedAtReset * 100)}% at reset. Reset in ${hoursUntilReset.toFixed(1)}h.`);
547
+ return false;
548
+ }
549
+
550
+ applyFactor(config, newFactor, constraining, projectedAtReset, log, hoursUntilReset);
551
+ return true;
552
+ }
553
+
554
+ /**
555
+ * Calculate EMA-smoothed rate from an array of snapshots.
556
+ * Uses exponential moving average of per-interval deltas for smoother estimation.
557
+ *
558
+ * @param {Array} snapshots - Array of raw snapshots (must have .ts and .keys)
559
+ * @param {'5h'|'7d'} metricKey - Which metric to compute rate for
560
+ * @param {number} [alpha=0.3] - EMA smoothing factor (higher = more weight on recent)
561
+ * @returns {number} Smoothed rate per hour
562
+ */
563
+ function calculateEmaRate(snapshots, metricKey, alpha = 0.3, excludeKeys = null) {
564
+ if (snapshots.length < 2) return 0;
565
+
566
+ let emaRate = null;
567
+
568
+ for (let i = 1; i < snapshots.length; i++) {
569
+ const prev = snapshots[i - 1];
570
+ const curr = snapshots[i];
571
+ const hoursDelta = (curr.ts - prev.ts) / (1000 * 60 * 60);
572
+ if (hoursDelta < MIN_HOURS_DELTA) continue; // Skip rapid-fire intervals (<3 min)
573
+
574
+ // Average across common keys for this pair, excluding exhausted keys
575
+ let commonKeys = Object.keys(curr.keys).filter(k => k in prev.keys);
576
+ if (excludeKeys) commonKeys = commonKeys.filter(k => !excludeKeys.has(k));
577
+ if (commonKeys.length === 0) continue;
578
+
579
+ let sumCurr = 0, sumPrev = 0;
580
+ for (const k of commonKeys) {
581
+ sumCurr += curr.keys[k][metricKey] ?? 0;
582
+ sumPrev += prev.keys[k][metricKey] ?? 0;
583
+ }
584
+ const avgCurr = sumCurr / commonKeys.length;
585
+ const avgPrev = sumPrev / commonKeys.length;
586
+ const intervalRate = (avgCurr - avgPrev) / hoursDelta;
587
+
588
+ if (emaRate === null) {
589
+ emaRate = intervalRate;
590
+ } else {
591
+ emaRate = alpha * intervalRate + (1 - alpha) * emaRate;
592
+ }
593
+ }
594
+
595
+ return emaRate ?? 0;
596
+ }
597
+
598
+ /**
599
+ * Calculate aggregate metrics across all keys in a snapshot pair.
600
+ * Uses EMA-smoothed rates from recent snapshots for stability.
601
+ * Also tracks per-key utilization and max values across keys.
602
+ */
603
+ function calculateAggregate(latest, earliest, hoursBetween, allSnapshots) {
604
+ const latestEntries = Object.entries(latest.keys);
605
+ if (latestEntries.length === 0) return null;
606
+
607
+ // Classify keys as exhausted (7d >= 0.995) vs active
608
+ const EXHAUSTED_THRESHOLD = 0.995;
609
+ const exhaustedKeyIds = new Set();
610
+ let resetAt5h = null, resetAt7d = null;
611
+ const perKeyUtilization = {};
612
+
613
+ // First pass: classify keys, collect per-key data and reset times
614
+ for (const [id, k] of latestEntries) {
615
+ const val5h = k['5h'] ?? 0;
616
+ const val7d = k['7d'] ?? 0;
617
+ perKeyUtilization[id] = { '5h': val5h, '7d': val7d };
618
+ if (k['5h_reset'] && (!resetAt5h || k['5h_reset'] < resetAt5h)) resetAt5h = k['5h_reset'];
619
+ if (k['7d_reset'] && (!resetAt7d || k['7d_reset'] < resetAt7d)) resetAt7d = k['7d_reset'];
620
+ if (val7d >= EXHAUSTED_THRESHOLD) exhaustedKeyIds.add(id);
621
+ }
622
+
623
+ // Compute aggregate from active keys only; fall back to all-key average if ALL exhausted
624
+ const activeEntries = latestEntries.filter(([id]) => !exhaustedKeyIds.has(id));
625
+ const entriesToAverage = activeEntries.length > 0 ? activeEntries : latestEntries;
626
+
627
+ // Compute maxKey from active keys only so exhausted keys don't bias effectiveUsage
628
+ let maxKey5h = 0, maxKey7d = 0;
629
+ for (const [, k] of entriesToAverage) {
630
+ maxKey5h = Math.max(maxKey5h, k['5h'] ?? 0);
631
+ maxKey7d = Math.max(maxKey7d, k['7d'] ?? 0);
632
+ }
633
+
634
+ let sum5h = 0, sum7d = 0;
635
+ for (const [, k] of entriesToAverage) {
636
+ sum5h += k['5h'] ?? 0;
637
+ sum7d += k['7d'] ?? 0;
638
+ }
639
+
640
+ const numKeys = entriesToAverage.length;
641
+ const current5h = sum5h / numKeys;
642
+ const current7d = sum7d / numKeys;
643
+
644
+ // Calculate rates: use EMA from recent snapshots if available, fall back to two-point slope
645
+ // Exclude exhausted keys from rate calculation so rates reflect only growing accounts
646
+ let rate5h = 0, rate7d = 0;
647
+ const excludeKeys = exhaustedKeyIds.size > 0 && activeEntries.length > 0 ? exhaustedKeyIds : null;
648
+ if (allSnapshots && allSnapshots.length >= 3) {
649
+ const recentSnapshots = selectTimeBasedSnapshots(allSnapshots, EMA_WINDOW_MS, EMA_MIN_INTERVAL_MS);
650
+ rate5h = calculateEmaRate(recentSnapshots, '5h', 0.3, excludeKeys);
651
+ rate7d = calculateEmaRate(recentSnapshots, '7d', 0.3, excludeKeys);
652
+ } else {
653
+ // Fallback: two-point slope from common keys (excluding exhausted)
654
+ let commonKeyIds = latestEntries
655
+ .map(([id]) => id)
656
+ .filter(id => id in earliest.keys);
657
+ if (excludeKeys) commonKeyIds = commonKeyIds.filter(id => !excludeKeys.has(id));
658
+
659
+ if (commonKeyIds.length > 0 && hoursBetween > 0) {
660
+ let latestCommon5h = 0, latestCommon7d = 0;
661
+ let earliestCommon5h = 0, earliestCommon7d = 0;
662
+
663
+ for (const id of commonKeyIds) {
664
+ latestCommon5h += latest.keys[id]['5h'] ?? 0;
665
+ latestCommon7d += latest.keys[id]['7d'] ?? 0;
666
+ earliestCommon5h += earliest.keys[id]['5h'] ?? 0;
667
+ earliestCommon7d += earliest.keys[id]['7d'] ?? 0;
668
+ }
669
+
670
+ const avg5hNow = latestCommon5h / commonKeyIds.length;
671
+ const avg7dNow = latestCommon7d / commonKeyIds.length;
672
+ const avg5hPrev = earliestCommon5h / commonKeyIds.length;
673
+ const avg7dPrev = earliestCommon7d / commonKeyIds.length;
674
+
675
+ rate5h = (avg5hNow - avg5hPrev) / hoursBetween;
676
+ rate7d = (avg7dNow - avg7dPrev) / hoursBetween;
677
+ }
678
+ }
679
+
680
+ // Calculate hours until reset
681
+ const now = Date.now();
682
+ let hoursUntil5hReset = 5; // default fallback
683
+ let hoursUntil7dReset = 168; // default fallback (7 days)
684
+
685
+ if (resetAt5h) {
686
+ const resetTime = new Date(resetAt5h).getTime();
687
+ hoursUntil5hReset = Math.max(0.1, (resetTime - now) / (1000 * 60 * 60));
688
+ }
689
+
690
+ if (resetAt7d) {
691
+ const resetTime = new Date(resetAt7d).getTime();
692
+ hoursUntil7dReset = Math.max(0.1, (resetTime - now) / (1000 * 60 * 60));
693
+ }
694
+
695
+ return {
696
+ current5h, current7d, rate5h, rate7d,
697
+ hoursUntil5hReset, hoursUntil7dReset,
698
+ maxKey5h, maxKey7d, perKeyUtilization,
699
+ activeKeyCount: activeEntries.length,
700
+ totalKeyCount: latestEntries.length,
701
+ };
702
+ }
703
+
704
+ /**
705
+ * Apply a new factor to the config, recalculating all effective cooldowns.
706
+ */
707
+ function applyFactor(config, newFactor, constraining, projectedAtReset, log, hoursUntilReset) {
708
+ const previousFactor = config.adjustment?.factor ?? 1.0;
709
+ const defaults = getDefaults();
710
+
711
+ // Calculate effective cooldowns: higher factor = shorter cooldowns = more activity
712
+ const effective = {};
713
+ for (const [key, defaultVal] of Object.entries(defaults)) {
714
+ // Skip static-mode automations - they keep their fixed interval
715
+ if (config.modes?.[key]?.mode === 'static') {
716
+ effective[key] = config.modes[key].static_minutes ?? defaultVal;
717
+ continue;
718
+ }
719
+ let computed = Math.max(MIN_EFFECTIVE_MINUTES, Math.round(defaultVal / newFactor));
720
+ if (MAX_COOLDOWN_MINUTES[key] !== undefined) {
721
+ computed = Math.min(computed, MAX_COOLDOWN_MINUTES[key]);
722
+ }
723
+ effective[key] = computed;
724
+ }
725
+
726
+ const direction = newFactor > previousFactor + 0.005 ? 'ramping up' : newFactor < previousFactor - 0.005 ? 'ramping down' : 'holding';
727
+
728
+ config.effective = effective;
729
+ config.adjustment = {
730
+ factor: Math.round(newFactor * 1000) / 1000, // 3 decimal places
731
+ last_updated: new Date().toISOString(),
732
+ constraining_metric: constraining,
733
+ projected_at_reset: Math.round(projectedAtReset * 1000) / 1000,
734
+ direction,
735
+ hours_until_reset: hoursUntilReset != null ? Math.round(hoursUntilReset * 10) / 10 : null,
736
+ };
737
+
738
+ const configPath = getConfigPath();
739
+ try {
740
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
741
+ const resetStr = hoursUntilReset != null ? ` Reset in ${hoursUntilReset.toFixed(1)}h.` : '';
742
+ log(`Usage optimizer: Factor ${newFactor.toFixed(3)} (was ${previousFactor.toFixed(3)}), ${direction}. ` +
743
+ `Constraining: ${constraining}. Projected at reset: ${Math.round(projectedAtReset * 100)}%.${resetStr}`);
744
+ } catch (err) {
745
+ log(`Usage optimizer: Failed to write config: ${err.message}`);
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Reset the optimizer by clearing all snapshots and restoring factor to 1.0.
751
+ * Used after deploying fixes to flush polluted historical data.
752
+ *
753
+ * @param {function} [logFn] - Optional log function (default: console.log)
754
+ */
755
+ export function resetOptimizer(logFn) {
756
+ const log = logFn || console.log;
757
+
758
+ // Clear snapshots
759
+ try {
760
+ if (fs.existsSync(SNAPSHOTS_PATH)) {
761
+ fs.writeFileSync(SNAPSHOTS_PATH, JSON.stringify({ snapshots: [] }, null, 2));
762
+ log('Usage optimizer reset: Cleared usage snapshots.');
763
+ }
764
+ } catch (err) {
765
+ log(`Usage optimizer reset: Failed to clear snapshots: ${err.message}`);
766
+ }
767
+
768
+ // Reset factor to 1.0 with default cooldowns
769
+ try {
770
+ const configPath = getConfigPath();
771
+ if (fs.existsSync(configPath)) {
772
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
773
+ const defaults = getDefaults();
774
+
775
+ config.effective = { ...defaults };
776
+ config.adjustment = {
777
+ factor: 1.0,
778
+ last_updated: new Date().toISOString(),
779
+ constraining_metric: null,
780
+ projected_at_reset: null,
781
+ direction: 'reset',
782
+ hours_until_reset: null,
783
+ };
784
+
785
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
786
+ log('Usage optimizer reset: Factor restored to 1.0 with default cooldowns.');
787
+ }
788
+ } catch (err) {
789
+ log(`Usage optimizer reset: Failed to reset config: ${err.message}`);
790
+ }
791
+ }