@polderlabs/bizar 6.2.5 → 6.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 (329) hide show
  1. package/.claude/CLAUDE.md +15 -0
  2. package/.claude/agents/_shared/AGENT_BASELINE.md +168 -0
  3. package/.claude/agents/_shared/CLAUDE_TOOLS.md +412 -0
  4. package/.claude/agents/_shared/SKILLS.md +109 -0
  5. package/.claude/agents/agent-browser.md +80 -0
  6. package/.claude/agents/baldr.md +49 -0
  7. package/.claude/agents/forseti.md +51 -0
  8. package/.claude/agents/frigg.md +42 -0
  9. package/.claude/agents/heimdall.md +33 -0
  10. package/.claude/agents/hermod.md +53 -0
  11. package/.claude/agents/mimir.md +49 -0
  12. package/.claude/agents/odin.md +287 -0
  13. package/.claude/agents/quick.md +34 -0
  14. package/.claude/agents/semble-search.md +50 -0
  15. package/.claude/agents/thor.md +53 -0
  16. package/.claude/agents/tyr.md +56 -0
  17. package/.claude/agents/vidarr.md +54 -0
  18. package/.claude/agents/vor.md +53 -0
  19. package/.claude/commands/audit.md +25 -0
  20. package/.claude/commands/bizar.md +22 -0
  21. package/.claude/commands/explain.md +17 -0
  22. package/.claude/commands/init.md +29 -0
  23. package/.claude/commands/learn.md +44 -0
  24. package/.claude/commands/plan.md +35 -0
  25. package/.claude/commands/plow-through.md +50 -0
  26. package/.claude/commands/pr-review.md +49 -0
  27. package/.claude/commands/setup-provider.md +96 -0
  28. package/.claude/commands/tailscale-serve.md +100 -0
  29. package/.claude/commands/team.md +132 -0
  30. package/.claude/commands/test.md +62 -0
  31. package/.claude/commands/validate.md +68 -0
  32. package/.claude/commands/visual-plan.md +24 -0
  33. package/.claude/hooks/README.md +92 -0
  34. package/.claude/hooks/posttooluse-editwrite.mjs +91 -0
  35. package/.claude/hooks/pretooluse-bash.mjs +81 -0
  36. package/.claude/hooks/pretooluse-editwrite.mjs +139 -0
  37. package/.claude/hooks/sessionend-recall.mjs +74 -0
  38. package/.claude/hooks/sessionstart-prime.mjs +80 -0
  39. package/.claude/hooks/userpromptsubmit-tag.mjs +80 -0
  40. package/.claude/settings.json +116 -0
  41. package/.claude/skills/9router/SKILL.md +80 -0
  42. package/.claude/skills/9router-chat/SKILL.md +73 -0
  43. package/.claude/skills/9router-embeddings/SKILL.md +69 -0
  44. package/.claude/skills/9router-image/SKILL.md +86 -0
  45. package/.claude/skills/9router-stt/SKILL.md +79 -0
  46. package/.claude/skills/9router-tts/SKILL.md +80 -0
  47. package/.claude/skills/9router-web-fetch/SKILL.md +99 -0
  48. package/.claude/skills/9router-web-search/SKILL.md +91 -0
  49. package/.claude/skills/bizar/README.md +9 -0
  50. package/.claude/skills/bizar/SKILL.md +450 -0
  51. package/.claude/skills/cpp-coding-standards/README.md +28 -0
  52. package/.claude/skills/cpp-coding-standards/SKILL.md +634 -0
  53. package/.claude/skills/cpp-coding-standards/references/concurrency.md +320 -0
  54. package/.claude/skills/cpp-coding-standards/references/error-handling.md +229 -0
  55. package/.claude/skills/cpp-coding-standards/references/memory-safety.md +216 -0
  56. package/.claude/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
  57. package/.claude/skills/cpp-coding-standards/references/review-checklist.md +96 -0
  58. package/.claude/skills/cpp-testing/README.md +28 -0
  59. package/.claude/skills/cpp-testing/SKILL.md +304 -0
  60. package/.claude/skills/cpp-testing/references/coverage.md +370 -0
  61. package/.claude/skills/cpp-testing/references/framework-compare.md +175 -0
  62. package/.claude/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
  63. package/.claude/skills/cpp-testing/references/mocking.md +364 -0
  64. package/.claude/skills/cpp-testing/references/tdd-workflow.md +308 -0
  65. package/.claude/skills/cubesandbox/SKILL.md +148 -0
  66. package/.claude/skills/embedded-esp-idf/README.md +41 -0
  67. package/.claude/skills/embedded-esp-idf/SKILL.md +439 -0
  68. package/.claude/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
  69. package/.claude/skills/embedded-esp-idf/references/host-tests.md +164 -0
  70. package/.claude/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
  71. package/.claude/skills/embedded-esp-idf/references/kconfig.md +159 -0
  72. package/.claude/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
  73. package/.claude/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
  74. package/.claude/skills/embedded-esp-idf/references/nvs.md +121 -0
  75. package/.claude/skills/embedded-esp-idf/references/packed-structs.md +192 -0
  76. package/.claude/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
  77. package/.claude/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
  78. package/.claude/skills/glyph/SKILL.md +163 -0
  79. package/.claude/skills/harness-engineering/SKILL.md +143 -0
  80. package/.claude/skills/lightrag/SKILL.md +81 -0
  81. package/.claude/skills/memory-protocol/SKILL.md +105 -0
  82. package/.claude/skills/obsidian/SKILL.md +306 -0
  83. package/.claude/skills/read-the-damn-docs/SKILL.md +113 -0
  84. package/.claude/skills/self-improvement/SKILL.md +64 -0
  85. package/README.md +87 -59
  86. package/bizar-dash/dist/assets/{EnvVarsSection-DXM8gRm_.js → EnvVarsSection-B58aiJiE.js} +1 -1
  87. package/bizar-dash/dist/assets/{EnvVarsSection-DXM8gRm_.js.map → EnvVarsSection-B58aiJiE.js.map} +1 -1
  88. package/bizar-dash/dist/assets/{MobileChat-BnKN_Ks_.js → MobileChat-BJrqwVDd.js} +1 -1
  89. package/bizar-dash/dist/assets/{MobileChat-BnKN_Ks_.js.map → MobileChat-BJrqwVDd.js.map} +1 -1
  90. package/bizar-dash/dist/assets/{MobileSettings-DjCPxC-Q.js → MobileSettings-CEQNJNLJ.js} +1 -1
  91. package/bizar-dash/dist/assets/{MobileSettings-DjCPxC-Q.js.map → MobileSettings-CEQNJNLJ.js.map} +1 -1
  92. package/bizar-dash/dist/assets/{main-DYiZqMrn.js → main-IvfQAOfy.js} +1 -1
  93. package/bizar-dash/dist/assets/{main-DYiZqMrn.js.map → main-IvfQAOfy.js.map} +1 -1
  94. package/bizar-dash/dist/assets/{markdown-C6mXtQxD.js → markdown-tOLaD6nm.js} +1 -1
  95. package/bizar-dash/dist/assets/{markdown-C6mXtQxD.js.map → markdown-tOLaD6nm.js.map} +1 -1
  96. package/bizar-dash/dist/assets/{mobile-CMHqtLV2.js → mobile-DYCHcUpq.js} +1 -1
  97. package/bizar-dash/dist/assets/{mobile-CMHqtLV2.js.map → mobile-DYCHcUpq.js.map} +1 -1
  98. package/bizar-dash/dist/assets/{mobile-layout-3jIhHX_p.js → mobile-layout-CBHjpwsb.js} +2 -2
  99. package/bizar-dash/dist/assets/{mobile-layout-3jIhHX_p.js.map → mobile-layout-CBHjpwsb.js.map} +1 -1
  100. package/bizar-dash/dist/assets/{useSlashCommands-DtITw8Xv.js → useSlashCommands-Bd7_FA6U.js} +2 -2
  101. package/bizar-dash/dist/assets/{useSlashCommands-DtITw8Xv.js.map → useSlashCommands-Bd7_FA6U.js.map} +1 -1
  102. package/bizar-dash/dist/assets/{vendor-CeHGtduv.js → vendor-C843201K.js} +12 -12
  103. package/bizar-dash/dist/assets/vendor-C843201K.js.map +1 -0
  104. package/bizar-dash/dist/index.html +6 -6
  105. package/bizar-dash/dist/mobile.html +2 -2
  106. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  107. package/bizar-dash/src/server/api.mjs +4 -4
  108. package/bizar-dash/src/server/background-store.mjs +15 -16
  109. package/bizar-dash/src/server/bg-poller.mjs +25 -35
  110. package/bizar-dash/src/server/bg-retry.mjs +66 -194
  111. package/bizar-dash/src/server/claude-bg-spawner.mjs +390 -0
  112. package/bizar-dash/src/server/claude-info.mjs +412 -0
  113. package/bizar-dash/src/server/{cline-runner.mjs → claude-runner.mjs} +53 -48
  114. package/bizar-dash/src/server/claude-sdk.mjs +191 -0
  115. package/bizar-dash/src/server/providers-store.mjs +22 -49
  116. package/bizar-dash/src/server/routes/background.mjs +24 -20
  117. package/bizar-dash/src/server/routes/chat.mjs +223 -326
  118. package/bizar-dash/src/server/routes/claude-session-detail.mjs +329 -0
  119. package/bizar-dash/src/server/routes/claude-sessions.mjs +259 -0
  120. package/bizar-dash/src/server/routes/tasks.mjs +20 -28
  121. package/bizar-dash/src/server/task-delegator.mjs +76 -77
  122. package/bizar-dash/src/web/components/chat/useChat.ts +23 -27
  123. package/bizar-dash/src/web/views/Chat.tsx +8 -8
  124. package/cli/bin.mjs +17 -18
  125. package/cli/commands/claude-cmd.mjs +348 -0
  126. package/cli/commands/install.mjs +12 -11
  127. package/cli/commands/validate.mjs +252 -325
  128. package/cli/dev-link.test.mjs +11 -11
  129. package/cli/doctor.mjs +131 -213
  130. package/cli/install.mjs +33 -37
  131. package/cli/install.test.mjs +9 -9
  132. package/cli/provision-claude.mjs +893 -0
  133. package/cli/provision.mjs +224 -227
  134. package/cli/utils.mjs +72 -58
  135. package/config/AGENTS.md +28 -28
  136. package/install.sh +66 -44
  137. package/package.json +19 -11
  138. package/packages/sdk/ARCHITECTURE.md +40 -36
  139. package/packages/sdk/package-lock.json +32 -0
  140. package/packages/sdk/package.json +15 -5
  141. package/{plugins/bizar → packages/sdk}/src/dangerous-patterns.ts +6 -28
  142. package/packages/sdk/src/fingerprint.ts +0 -0
  143. package/packages/sdk/src/index.ts +38 -73
  144. package/packages/sdk/src/mcp/bin.ts +51 -0
  145. package/packages/sdk/src/mcp/server.ts +498 -0
  146. package/{plugins/bizar/src/memory-vault.ts → packages/sdk/src/memory/index.ts} +18 -21
  147. package/packages/sdk/tests/sdk.test.mjs +148 -0
  148. package/packages/sdk/vitest.config.ts +1 -1
  149. package/scripts/bh-full-e2e.mjs +166 -363
  150. package/scripts/check-deps.mjs +28 -46
  151. package/scripts/mirror-claude-md.sh +78 -0
  152. package/scripts/test-in-container.sh +31 -9
  153. package/templates/clean-state-checklist.md +3 -3
  154. package/templates/sprint-contract.md +2 -2
  155. package/bizar-dash/.bizar/activity.log +0 -3
  156. package/bizar-dash/.omx/logs/omx-2026-07-07.jsonl +0 -1
  157. package/bizar-dash/.omx/state/session.json +0 -10
  158. package/bizar-dash/bizar-design/EXPLANATION.md +0 -307
  159. package/bizar-dash/bizar-design/INSTRUCTIONS.md +0 -149
  160. package/bizar-dash/bizar-design/canvas.html +0 -281
  161. package/bizar-dash/bizar-design/canvas.html.artifact.json +0 -19
  162. package/bizar-dash/bizar-design/components.css +0 -1665
  163. package/bizar-dash/bizar-design/components.html +0 -557
  164. package/bizar-dash/bizar-design/components.html.artifact.json +0 -19
  165. package/bizar-dash/bizar-design/desktop/agents.html +0 -74
  166. package/bizar-dash/bizar-design/desktop/agents.html.artifact.json +0 -19
  167. package/bizar-dash/bizar-design/desktop/memory.html +0 -75
  168. package/bizar-dash/bizar-design/desktop/memory.html.artifact.json +0 -19
  169. package/bizar-dash/bizar-design/desktop/metrics.html +0 -63
  170. package/bizar-dash/bizar-design/desktop/metrics.html.artifact.json +0 -19
  171. package/bizar-dash/bizar-design/desktop/overview.html +0 -95
  172. package/bizar-dash/bizar-design/desktop/overview.html.artifact.json +0 -19
  173. package/bizar-dash/bizar-design/desktop/settings.html +0 -70
  174. package/bizar-dash/bizar-design/desktop/settings.html.artifact.json +0 -19
  175. package/bizar-dash/bizar-design/icons.svg +0 -44
  176. package/bizar-dash/bizar-design/index.html +0 -192
  177. package/bizar-dash/bizar-design/index.html.artifact.json +0 -19
  178. package/bizar-dash/bizar-design/mobile/agents.html +0 -29
  179. package/bizar-dash/bizar-design/mobile/agents.html.artifact.json +0 -19
  180. package/bizar-dash/bizar-design/mobile/memory.html +0 -28
  181. package/bizar-dash/bizar-design/mobile/memory.html.artifact.json +0 -19
  182. package/bizar-dash/bizar-design/mobile/metrics.html +0 -29
  183. package/bizar-dash/bizar-design/mobile/metrics.html.artifact.json +0 -19
  184. package/bizar-dash/bizar-design/mobile/overview.html +0 -33
  185. package/bizar-dash/bizar-design/mobile/overview.html.artifact.json +0 -19
  186. package/bizar-dash/bizar-design/mobile/settings.html +0 -28
  187. package/bizar-dash/bizar-design/mobile/settings.html.artifact.json +0 -19
  188. package/bizar-dash/bizar-design/prototype.js +0 -114
  189. package/bizar-dash/bizar-design/tokens.css +0 -106
  190. package/bizar-dash/dist/assets/vendor-CeHGtduv.js.map +0 -1
  191. package/bizar-dash/node_modules/.package-lock.json +0 -6
  192. package/bizar-dash/package-lock.json +0 -6
  193. package/bizar-dash/src/server/cline-sdk.mjs +0 -132
  194. package/bizar-dash/src/server/routes/cline-session-detail.mjs +0 -559
  195. package/bizar-dash/src/server/routes/cline-sessions.mjs +0 -291
  196. package/cli/commands/cline-cmd.mjs +0 -289
  197. package/cli/commands/validate.test.mjs +0 -379
  198. package/cli/doctor.test.mjs +0 -350
  199. package/config/cline.json.template +0 -342
  200. package/packages/sdk/src/client.ts +0 -188
  201. package/packages/sdk/src/cline-events.ts +0 -134
  202. package/packages/sdk/src/cline-types.ts +0 -66
  203. package/packages/sdk/src/cline.ts +0 -339
  204. package/packages/sdk/src/errors.ts +0 -129
  205. package/packages/sdk/src/events.ts +0 -153
  206. package/packages/sdk/src/types.ts +0 -176
  207. package/packages/sdk/tests/client.test.ts +0 -217
  208. package/packages/sdk/tests/errors.test.ts +0 -108
  209. package/packages/sdk/tests/events.test.ts +0 -139
  210. package/packages/sdk/tests/fixtures/fetch-mock.ts +0 -152
  211. package/packages/sdk/tests/fixtures/sse-mock.ts +0 -30
  212. package/plugins/bizar/ARCHITECTURE.md +0 -142
  213. package/plugins/bizar/CONSTRAINTS.md +0 -67
  214. package/plugins/bizar/LICENSE +0 -21
  215. package/plugins/bizar/README.md +0 -448
  216. package/plugins/bizar/index.ts +0 -925
  217. package/plugins/bizar/package.json +0 -39
  218. package/plugins/bizar/scripts/check-forbidden-imports.sh +0 -33
  219. package/plugins/bizar/src/background-state.ts +0 -641
  220. package/plugins/bizar/src/background.ts +0 -1806
  221. package/plugins/bizar/src/cline-runner.ts +0 -203
  222. package/plugins/bizar/src/clineruntime.ts +0 -227
  223. package/plugins/bizar/src/commands-impl.ts +0 -151
  224. package/plugins/bizar/src/commands.ts +0 -1799
  225. package/plugins/bizar/src/compaction.d.mts +0 -48
  226. package/plugins/bizar/src/compaction.mjs +0 -192
  227. package/plugins/bizar/src/dashboard-client.ts +0 -233
  228. package/plugins/bizar/src/event-stream.ts +0 -606
  229. package/plugins/bizar/src/fingerprint.ts +0 -120
  230. package/plugins/bizar/src/handoff.ts +0 -79
  231. package/plugins/bizar/src/hooks/memory-flush-on-compact.ts +0 -123
  232. package/plugins/bizar/src/hooks/memory-inject.ts +0 -247
  233. package/plugins/bizar/src/hooks/memory-write-on-end.ts +0 -188
  234. package/plugins/bizar/src/hooks/skill-curator.ts +0 -180
  235. package/plugins/bizar/src/http-client.ts +0 -467
  236. package/plugins/bizar/src/key-rotation.ts +0 -218
  237. package/plugins/bizar/src/logger.ts +0 -144
  238. package/plugins/bizar/src/loop-engineering.ts +0 -241
  239. package/plugins/bizar/src/loop.ts +0 -176
  240. package/plugins/bizar/src/mistake-recovery.ts +0 -98
  241. package/plugins/bizar/src/odin.ts +0 -227
  242. package/plugins/bizar/src/options.ts +0 -470
  243. package/plugins/bizar/src/plan-fs.ts +0 -323
  244. package/plugins/bizar/src/reasoning-clean.ts +0 -454
  245. package/plugins/bizar/src/report.ts +0 -178
  246. package/plugins/bizar/src/research-prompt.ts +0 -35
  247. package/plugins/bizar/src/serve-info.ts +0 -228
  248. package/plugins/bizar/src/serve.ts +0 -496
  249. package/plugins/bizar/src/settings.ts +0 -349
  250. package/plugins/bizar/src/state.ts +0 -298
  251. package/plugins/bizar/src/tool-discipline.ts +0 -105
  252. package/plugins/bizar/src/tools/agent-browser.ts +0 -315
  253. package/plugins/bizar/src/tools/bg-collect.ts +0 -131
  254. package/plugins/bizar/src/tools/bg-get-comments.ts +0 -266
  255. package/plugins/bizar/src/tools/bg-kill.ts +0 -116
  256. package/plugins/bizar/src/tools/bg-pause.ts +0 -99
  257. package/plugins/bizar/src/tools/bg-report-progress.ts +0 -115
  258. package/plugins/bizar/src/tools/bg-resume.ts +0 -94
  259. package/plugins/bizar/src/tools/bg-send-message.ts +0 -223
  260. package/plugins/bizar/src/tools/bg-spawn.ts +0 -502
  261. package/plugins/bizar/src/tools/bg-status.ts +0 -130
  262. package/plugins/bizar/src/tools/graph-query.ts +0 -278
  263. package/plugins/bizar/src/tools/loop-engineering.ts +0 -193
  264. package/plugins/bizar/src/tools/memory-list.ts +0 -43
  265. package/plugins/bizar/src/tools/memory-read.ts +0 -69
  266. package/plugins/bizar/src/tools/memory-search.ts +0 -47
  267. package/plugins/bizar/src/tools/memory-write.ts +0 -54
  268. package/plugins/bizar/src/tools/open-kb.ts +0 -198
  269. package/plugins/bizar/src/tools/plan-action.ts +0 -785
  270. package/plugins/bizar/src/tools/read-glyph-feedback.ts +0 -191
  271. package/plugins/bizar/src/tools/sandbox.ts +0 -232
  272. package/plugins/bizar/src/tools/team-spawn.ts +0 -73
  273. package/plugins/bizar/src/tools/team-status.ts +0 -76
  274. package/plugins/bizar/src/tools/wait-for-feedback.ts +0 -415
  275. package/plugins/bizar/src/trajectory.ts +0 -104
  276. package/plugins/bizar/tests/README.md +0 -99
  277. package/plugins/bizar/tests/attach-handler-bug.test.ts +0 -169
  278. package/plugins/bizar/tests/background-state.test.ts +0 -277
  279. package/plugins/bizar/tests/background.test.ts +0 -402
  280. package/plugins/bizar/tests/block.test.ts +0 -195
  281. package/plugins/bizar/tests/canonical-key-order.test.ts +0 -75
  282. package/plugins/bizar/tests/clineruntime-config.test.ts +0 -283
  283. package/plugins/bizar/tests/commands-impl.test.ts +0 -316
  284. package/plugins/bizar/tests/commands.test.ts +0 -584
  285. package/plugins/bizar/tests/compaction.test.ts +0 -264
  286. package/plugins/bizar/tests/config.test.ts +0 -128
  287. package/plugins/bizar/tests/dashboard-client.test.ts +0 -159
  288. package/plugins/bizar/tests/dispose.test.ts +0 -336
  289. package/plugins/bizar/tests/event-stream.test.ts +0 -409
  290. package/plugins/bizar/tests/event.test.ts +0 -262
  291. package/plugins/bizar/tests/fingerprint.test.ts +0 -190
  292. package/plugins/bizar/tests/http-client.test.ts +0 -404
  293. package/plugins/bizar/tests/init-helpers.test.ts +0 -203
  294. package/plugins/bizar/tests/integration/slash-command.test.ts +0 -349
  295. package/plugins/bizar/tests/integration/tool-routing.test.ts +0 -98
  296. package/plugins/bizar/tests/key-rotation.test.ts +0 -396
  297. package/plugins/bizar/tests/loop-engineering.test.ts +0 -168
  298. package/plugins/bizar/tests/loop.test.ts +0 -397
  299. package/plugins/bizar/tests/memory-write-on-end.test.ts +0 -92
  300. package/plugins/bizar/tests/mistake-recovery.test.ts +0 -116
  301. package/plugins/bizar/tests/odin.test.ts +0 -125
  302. package/plugins/bizar/tests/options.test.ts +0 -329
  303. package/plugins/bizar/tests/reasoning-clean.test.ts +0 -422
  304. package/plugins/bizar/tests/safety.test.ts +0 -281
  305. package/plugins/bizar/tests/serve.test.ts +0 -339
  306. package/plugins/bizar/tests/settings.test.ts +0 -351
  307. package/plugins/bizar/tests/stall-think.test.ts +0 -750
  308. package/plugins/bizar/tests/state.test.ts +0 -276
  309. package/plugins/bizar/tests/tool-discipline.test.ts +0 -77
  310. package/plugins/bizar/tests/tools/agent-browser.test.ts +0 -98
  311. package/plugins/bizar/tests/tools/bg-collect.test.ts +0 -337
  312. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +0 -485
  313. package/plugins/bizar/tests/tools/bg-kill.test.ts +0 -235
  314. package/plugins/bizar/tests/tools/bg-pause.test.ts +0 -61
  315. package/plugins/bizar/tests/tools/bg-report-progress.test.ts +0 -79
  316. package/plugins/bizar/tests/tools/bg-resume.test.ts +0 -40
  317. package/plugins/bizar/tests/tools/bg-send-message.test.ts +0 -116
  318. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +0 -147
  319. package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +0 -233
  320. package/plugins/bizar/tests/tools/bg-spawn.test.ts +0 -311
  321. package/plugins/bizar/tests/tools/bg-status.test.ts +0 -217
  322. package/plugins/bizar/tests/tools/cline-runner.test.ts +0 -115
  323. package/plugins/bizar/tests/tools/plan-action.test.ts +0 -599
  324. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +0 -253
  325. package/plugins/bizar/tests/tools/sandbox.test.ts +0 -117
  326. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +0 -390
  327. package/plugins/bizar/tests/update-deadlock.test.ts +0 -151
  328. package/plugins/bizar/tsconfig.json +0 -29
  329. package/scripts/mirror-agents-md.sh +0 -69
@@ -1,467 +0,0 @@
1
- /**
2
- * http-client.ts
3
- *
4
- * Typed fetch wrapper for plugin → cline serve calls (v0.4.2 spec §1, §2.3).
5
- *
6
- * Responsibilities:
7
- * - Auth header on every call: `Authorization: Basic base64("cline:<password>")`
8
- * (spec §6.1; this is the spec's best understanding of cline serve's
9
- * auth scheme and is verified by integration test in BizarHarness-dev).
10
- * - `directory` query param on every per-instance call (spec §1.7).
11
- * - 30s default timeout via `AbortController` (spec §2.3 / §6.1 env
12
- * `BIZAR_HTTP_TIMEOUT_MS`, option `httpTimeoutMs`).
13
- * - Never throws on transport errors; returns a discriminated result so
14
- * callers can log + surface a clear error to the agent without an
15
- * unhandled rejection (spec §2.3 last paragraph).
16
- *
17
- * v0.4.3 — v2 API migration (see `.bizar/cline-sse-investigation.md`):
18
- * - The v1 session routes (`/session`, `/session/{id}/prompt_async`,
19
- * `/session/{id}/abort`, `/session/{id}/message`) hang indefinitely
20
- * against cline serve 1.17.7. We migrated to the v2 API:
21
- *
22
- * | Method | Old (v1, hangs) | New (v2) |
23
- * |------------|----------------------------|------------------------------------|
24
- * | create | POST /session | POST /api/session |
25
- * | sendPrompt | POST /session/{id}/prompt_async | POST /api/session/{id}/prompt |
26
- * | abort | POST /session/{id}/abort | POST /api/session/{id}/abort |
27
- * | listMsgs | GET /session/{id}/message | GET /api/session/{id}/message |
28
- *
29
- * - v2 wraps responses in `{data: ...}`. This client unwraps internally
30
- * so the public interface (e.g. `{id: string}` from createSession,
31
- * `ListMessagesResult[]` from listMessages) is unchanged.
32
- * - v2 prompt body shape is `{id, prompt: {text}, agent, model?}` — we
33
- * extract the text from the first `parts` text entry.
34
- * - v2 abort: the OpenAPI investigation found no documented v2 abort
35
- * route. We try `/api/session/{id}/abort` as a best-effort; if it
36
- * fails, the in-memory instance state is still marked `killed` for
37
- * immediate caller feedback, and the next SSE event will finalize it.
38
- * - The SSE endpoint `GET /event?directory=...` (v1) still works for
39
- * the event subscription (v1 SSE connects fine; only the session
40
- * routes hang). We keep using it; the v2 `/api/event` endpoint with
41
- * `location[directory]=...` query syntax is a known alternative.
42
- *
43
- * Boundary policy: the only `node:` import allowed in this file is
44
- * implicit (none). We use the global `fetch` / `AbortController` /
45
- * `ReadableStream` provided by Bun's runtime. If the test runtime is pure
46
- * Node, those are available as globals in Node 20+; in Bun they are
47
- * always available.
48
- *
49
- * Note on auth scheme: cline serve's exact auth scheme is cline-
50
- * dependent. The current best understanding, based on v0.4 research, is
51
- * `Authorization: Basic` with username `cline`. This must be verified
52
- * by reading cline's serve-side code or by integration test. If
53
- * cline uses a different scheme (e.g. a custom `x-cline-password`
54
- * header), this file is the single place to change.
55
- */
56
-
57
- // --- Logger interface -----------------------------------------------------
58
-
59
- /**
60
- * Minimal Logger interface — matches the shape in `state.ts` / `logger.ts`.
61
- */
62
- export interface Logger {
63
- log(opts: { level: "debug" | "info" | "warn" | "error"; message: string }): void;
64
- debug(message: string): void;
65
- info(message: string): void;
66
- warn(message: string): void;
67
- error(message: string): void;
68
- }
69
-
70
- // --- Public surface -------------------------------------------------------
71
-
72
- /**
73
- * Per-call request options for the message-listing endpoint.
74
- * Used by `bizar_collect` to reconstruct the result text.
75
- */
76
- export interface ListMessagesResult {
77
- id: string;
78
- role: string;
79
- parts: Array<{
80
- type: string;
81
- text?: string;
82
- error?: string;
83
- }>;
84
- }
85
-
86
- /**
87
- * A model override for createSession / sendPrompt. Both fields required.
88
- */
89
- export interface ModelOverride {
90
- providerID: string;
91
- modelID: string;
92
- }
93
-
94
- export interface CreateSessionOptions {
95
- parentID?: string;
96
- title: string;
97
- agent: string;
98
- model?: ModelOverride;
99
- }
100
-
101
- export interface SendPromptOptions {
102
- sessionId: string;
103
- messageID: string;
104
- agent: string;
105
- model?: ModelOverride;
106
- parts: Array<{ type: "text"; text: string }>;
107
- }
108
-
109
- /**
110
- * Discriminated result of an HTTP call. We never throw across the
111
- * client boundary — callers pattern-match on `ok`.
112
- */
113
- export type HttpResult<T> =
114
- | { ok: true; value: T; status: number }
115
- | { ok: false; error: string; status?: number };
116
-
117
- /**
118
- * Typed HTTP client for the cline serve child. All methods take a
119
- * `directory` argument (spec §1.7) so the same client works for the
120
- * plugin's own worktree and any future multi-worktree setups.
121
- */
122
- export class HttpClient {
123
- private baseUrl: string;
124
- private authHeader: string;
125
- private logger: Logger;
126
- private timeoutMs: number;
127
-
128
- constructor(opts: {
129
- baseUrl: string;
130
- password: string;
131
- logger: Logger;
132
- timeoutMs?: number;
133
- }) {
134
- this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
135
- this.logger = opts.logger;
136
- this.timeoutMs = Math.max(1000, Math.floor(opts.timeoutMs ?? 30_000));
137
- // Basic auth: "cline:<password>" base64-encoded.
138
- // (spec §6.1; see module-level note re: scheme verification.)
139
- const credentials = `cline:${opts.password}`;
140
- this.authHeader = `Basic ${btoa(credentials)}`;
141
- }
142
-
143
- // --- Public API ---------------------------------------------------------
144
-
145
- /**
146
- * POST /api/session — create a new background session (v2 route).
147
- *
148
- * v0.4.3 migration: v1 `POST /session` hangs against cline 1.17.7.
149
- * The v2 endpoint returns `{data: {id, ...}}`; we unwrap `.data` so
150
- * the public interface stays `{id: string}`.
151
- *
152
- * Body (verified via OpenAPI spec):
153
- * { parentID?, title, agent, model? }
154
- *
155
- * The `agent` field is REQUIRED — without it cline spawns the
156
- * default agent instead of the requested one.
157
- */
158
- async createSession(
159
- opts: CreateSessionOptions,
160
- directory: string,
161
- ): Promise<HttpResult<{ id: string }>> {
162
- const body: Record<string, unknown> = {
163
- title: opts.title,
164
- agent: opts.agent,
165
- };
166
- if (opts.parentID !== undefined) body.parentID = opts.parentID;
167
- if (opts.model !== undefined) body.model = opts.model;
168
-
169
- const res = await this.request<{ data?: { id?: string } }>(
170
- "POST",
171
- `/api/session?directory=${encodeURIComponent(directory)}`,
172
- body,
173
- );
174
- if (!res.ok) return res;
175
- // v2 wraps the session in `{data: {...}}`. Defensive: if the server
176
- // ever returns the session at top level, fall back to that shape.
177
- const id = res.value.data?.id ?? (res.value as unknown as { id?: string }).id;
178
- if (typeof id !== "string" || id.length === 0) {
179
- return {
180
- ok: false,
181
- error: "POST /api/session: response missing `data.id`",
182
- status: res.status,
183
- };
184
- }
185
- return { ok: true, value: { id }, status: res.status };
186
- }
187
-
188
- /**
189
- * POST /api/session/{id}/prompt — fire the prompt (v2 route).
190
- *
191
- * v0.4.3 migration: v1 `POST /session/{id}/prompt_async` hangs. The v2
192
- * endpoint is synchronous and uses a different body shape:
193
- *
194
- * OLD: { messageID, parts: [{type:"text", text}], agent, model? }
195
- * NEW: { id, prompt: {text: "..."}, agent, model? }
196
- *
197
- * The text is extracted from the first text-type part. `id` is the
198
- * plugin-generated `messageID` (renamed from `messageID`).
199
- *
200
- * Response shape is not fully documented in the OpenAPI spec; we
201
- * accept any JSON (or empty) body and return the raw parsed value.
202
- * Callers that need the response data should check `value`.
203
- */
204
- async sendPrompt(
205
- opts: SendPromptOptions,
206
- directory: string,
207
- ): Promise<HttpResult<unknown>> {
208
- // v2 takes the prompt text in `prompt.text`, not in `parts[]`.
209
- // Concatenate all text parts in order; fall back to empty string.
210
- const text = opts.parts
211
- .filter((p) => p.type === "text" && typeof p.text === "string")
212
- .map((p) => p.text)
213
- .join("");
214
- const body: Record<string, unknown> = {
215
- id: opts.messageID,
216
- prompt: { text },
217
- agent: opts.agent,
218
- };
219
- if (opts.model) body.model = opts.model;
220
-
221
- return this.request<unknown>(
222
- "POST",
223
- `/api/session/${encodeURIComponent(opts.sessionId)}/prompt?directory=${encodeURIComponent(directory)}`,
224
- body,
225
- );
226
- }
227
-
228
- /**
229
- * POST /api/session/{id}/abort — kill a running session (v2 route).
230
- *
231
- * v0.4.3 migration: v1 `POST /session/{id}/abort` likely hangs (the
232
- * v1 session routes all hang on cline 1.17.7). The OpenAPI
233
- * investigation did not surface a documented v2 abort endpoint, so
234
- * this is a best-effort call against the v2-mirrored path. If the
235
- * server returns a 404, we log a warning via the result `error`
236
- * field and the in-memory state is still marked `killed` for
237
- * immediate caller feedback. The next SSE `session.idle` or
238
- * `session.error` for the session will finalize the state.
239
- *
240
- * This is what `bizar_kill` and the shutdown path call.
241
- * NOT `DELETE /session/{id}`.
242
- */
243
- async abortSession(
244
- sessionId: string,
245
- directory: string,
246
- ): Promise<HttpResult<boolean>> {
247
- const res = await this.request<unknown>(
248
- "POST",
249
- `/api/session/${encodeURIComponent(sessionId)}/abort?directory=${encodeURIComponent(directory)}`,
250
- null,
251
- );
252
- if (!res.ok) return res;
253
- // The server may return `true`, a `{data: true}` wrapper, or nothing.
254
- // We treat any 2xx with a body or no body as "ok".
255
- const value: unknown = res.value;
256
- if (value === undefined || value === null) {
257
- return { ok: true, value: true, status: res.status };
258
- }
259
- if (typeof value === "object") {
260
- const v = value as { data?: unknown; result?: unknown };
261
- if (v.data === true) return { ok: true, value: true, status: res.status };
262
- if (v.result === true) return { ok: true, value: true, status: res.status };
263
- }
264
- if (value === true) return { ok: true, value: true, status: res.status };
265
- // Any other truthy/falsey body: treat as best-effort success.
266
- return { ok: true, value: true, status: res.status };
267
- }
268
-
269
- /**
270
- * GET /api/session/{id}/message — list the messages of a session (v2 route).
271
- *
272
- * v0.4.3 migration: v1 `GET /session/{id}/message` likely hangs. The
273
- * v2 endpoint returns `{data: Array<{info, parts}>}`; we unwrap
274
- * `.data` so the public interface stays `ListMessagesResult[]`.
275
- *
276
- * Each message is normalized to the flattened
277
- * {@link ListMessagesResult} shape for the tool layer.
278
- */
279
- async listMessages(
280
- sessionId: string,
281
- directory: string,
282
- ): Promise<HttpResult<ListMessagesResult[]>> {
283
- type RawMessage = {
284
- info?: { id?: string; role?: string };
285
- parts?: Array<{ type?: string; text?: string; error?: string }>;
286
- };
287
- const res = await this.request<{ data?: RawMessage[] } | RawMessage[]>(
288
- "GET",
289
- `/api/session/${encodeURIComponent(sessionId)}/message?directory=${encodeURIComponent(directory)}`,
290
- );
291
- if (!res.ok) return res;
292
- // v2 wraps the array in `{data: [...]}`. Defensive: fall back to
293
- // top-level array if the server returns the bare array.
294
- const arr: RawMessage[] = Array.isArray(res.value)
295
- ? res.value
296
- : (res.value.data ?? []);
297
- const normalized: ListMessagesResult[] = arr.map((m) => ({
298
- id: m.info?.id ?? "",
299
- role: m.info?.role ?? "",
300
- parts: (m.parts ?? []).map((p) => {
301
- const part: ListMessagesResult["parts"][number] = {
302
- type: p.type ?? "unknown",
303
- };
304
- if (p.text !== undefined) part.text = p.text;
305
- if (p.error !== undefined) part.error = p.error;
306
- return part;
307
- }),
308
- }));
309
- return { ok: true, value: normalized, status: res.status };
310
- }
311
-
312
- /**
313
- * GET /event?directory=... — open the SSE event stream.
314
- *
315
- * We return the raw `ReadableStream` so the caller (EventStream) can
316
- * parse the SSE wire format. The HTTP response is the underlying
317
- * `Response`; only the body stream is consumed here.
318
- *
319
- * Note: the SSE response uses `Content-Type: text/event-stream` and
320
- * may stay open indefinitely. The AbortController we attach kills
321
- * the connection on `disconnect()`.
322
- */
323
- async fetchEventStream(directory: string, signal?: AbortSignal): Promise<HttpResult<ReadableStream>> {
324
- const url = `${this.baseUrl}/api/event?location[directory]=${encodeURIComponent(directory)}`;
325
- const ac = new AbortController();
326
- const timer = setTimeout(() => ac.abort(), this.timeoutMs);
327
-
328
- // Bridge caller-provided signal (disconnect) into our internal ac.
329
- const onCallerAbort = () => ac.abort();
330
- if (signal) {
331
- if (signal.aborted) {
332
- ac.abort();
333
- } else {
334
- signal.addEventListener("abort", onCallerAbort, { once: true });
335
- }
336
- }
337
-
338
- try {
339
- const response = await fetch(url, {
340
- method: "GET",
341
- headers: {
342
- Authorization: this.authHeader,
343
- Accept: "text/event-stream",
344
- },
345
- signal: ac.signal,
346
- });
347
- if (!response.ok) {
348
- // Drain the body so the connection is released.
349
- try {
350
- await response.arrayBuffer();
351
- } catch {
352
- // ignore
353
- }
354
- return {
355
- ok: false,
356
- error: `GET /event failed: ${response.status} ${response.statusText}`,
357
- status: response.status,
358
- };
359
- }
360
- if (!response.body) {
361
- return { ok: false, error: "GET /event: no response body" };
362
- }
363
- return { ok: true, value: response.body, status: response.status };
364
- } catch (err: unknown) {
365
- const msg = err instanceof Error ? err.message : String(err);
366
- const isAbort = err instanceof Error && err.name === "AbortError";
367
- const finalMsg = isAbort
368
- ? `GET /event aborted after ${this.timeoutMs}ms`
369
- : `GET /event network error: ${msg}`;
370
- this.logger.log({ level: "warn", message: `bizar: ${finalMsg}` });
371
- return { ok: false, error: finalMsg };
372
- } finally {
373
- clearTimeout(timer);
374
- if (signal) signal.removeEventListener("abort", onCallerAbort);
375
- }
376
- }
377
-
378
- /**
379
- * GET /health — used by ServeLifecycle to confirm the server is up.
380
- */
381
- async healthCheck(): Promise<boolean> {
382
- const ac = new AbortController();
383
- const timer = setTimeout(() => ac.abort(), this.timeoutMs);
384
- try {
385
- const response = await fetch(`${this.baseUrl}/health`, {
386
- method: "GET",
387
- headers: { Authorization: this.authHeader },
388
- signal: ac.signal,
389
- });
390
- return response.ok;
391
- } catch {
392
- return false;
393
- } finally {
394
- clearTimeout(timer);
395
- }
396
- }
397
-
398
- // --- Internal request helper -------------------------------------------
399
-
400
- private async request<T>(
401
- method: "GET" | "POST" | "DELETE",
402
- pathAndQuery: string,
403
- body?: unknown,
404
- opts: { expectNoBody?: boolean } = {},
405
- ): Promise<HttpResult<T>> {
406
- const url = `${this.baseUrl}${pathAndQuery}`;
407
- const ac = new AbortController();
408
- const timer = setTimeout(() => ac.abort(), this.timeoutMs);
409
- try {
410
- const init: RequestInit = {
411
- method,
412
- headers: {
413
- Authorization: this.authHeader,
414
- "Content-Type": "application/json",
415
- Accept: "application/json",
416
- },
417
- signal: ac.signal,
418
- };
419
- if (body !== null && body !== undefined) {
420
- init.body = JSON.stringify(body);
421
- }
422
- const response = await fetch(url, init);
423
-
424
- if (!response.ok) {
425
- // Capture the error body for the caller but do not throw.
426
- let detail = "";
427
- try {
428
- detail = await response.text();
429
- } catch {
430
- // ignore
431
- }
432
- if (detail.length > 500) detail = detail.slice(0, 500) + "…";
433
- return {
434
- ok: false,
435
- error: `${method} ${pathAndQuery} failed: ${response.status} ${response.statusText}${detail ? ` — ${detail}` : ""}`,
436
- status: response.status,
437
- };
438
- }
439
-
440
- if (opts.expectNoBody || response.status === 204) {
441
- return { ok: true, value: undefined as T, status: response.status };
442
- }
443
-
444
- // 200 with body — try to parse as JSON.
445
- try {
446
- const parsed = (await response.json()) as T;
447
- return { ok: true, value: parsed, status: response.status };
448
- } catch (err: unknown) {
449
- return {
450
- ok: false,
451
- error: `${method} ${pathAndQuery} returned ${response.status} with non-JSON body: ${err instanceof Error ? err.message : String(err)}`,
452
- status: response.status,
453
- };
454
- }
455
- } catch (err: unknown) {
456
- const msg = err instanceof Error ? err.message : String(err);
457
- const isAbort = err instanceof Error && err.name === "AbortError";
458
- const finalMsg = isAbort
459
- ? `Request to ${url} timed out after ${this.timeoutMs}ms`
460
- : `Request to ${url} failed: ${msg}`;
461
- this.logger.log({ level: "warn", message: `bizar: ${finalMsg}` });
462
- return { ok: false, error: finalMsg };
463
- } finally {
464
- clearTimeout(timer);
465
- }
466
- }
467
- }
@@ -1,218 +0,0 @@
1
- /**
2
- * plugins/bizar/src/key-rotation.ts
3
- *
4
- * Wraps a provider's fetch to automatically rotate through a list of API
5
- * keys when the upstream returns a retryable status (429 / 402 / 5xx).
6
- *
7
- * Why this exists
8
- * ───────────────
9
- * MiniMax (like most LLM providers) applies per-key rate limits and
10
- * quotas. When a key hits its limit, the request fails with 429 or 402
11
- * and the agent session dies — there is no way to swap keys mid-session
12
- * because the provider reads its key from auth.json once at init.
13
- *
14
- * cline has no built-in multi-key rotation. This wrapper fills the gap:
15
- *
16
- * • Read N keys from env vars (MINIMAX_API_KEY, MINIMAX_API_KEY_2,
17
- * ..., or MINIMAX_API_KEYS=key1,key2,...)
18
- * • On the first attempt, use key #0
19
- * • On 429/402/5xx response, retry with the next key (capped at N)
20
- * • On 401 (unauthorized) and other 4xx, do NOT rotate — those are
21
- * client errors that no key will fix
22
- *
23
- * Behaviour
24
- * ─────────
25
- * • Only POST requests targeting `/chat/completions` on a known provider
26
- * URL are intercepted. Other requests pass through untouched.
27
- * • The `Authorization` header on the outgoing request is replaced with
28
- * the currently-selected key on every attempt. All other headers
29
- * (Content-Type, Accept, etc.) are preserved from the SDK.
30
- * • Responses are NEVER consumed by this wrapper. The caller receives
31
- * the final response (success or last failure) intact, including its
32
- * unconsumed body stream — safe for SSE.
33
- * • On a successful response, the "current key" index advances by one so
34
- * the next incoming request starts on a different key, spreading load.
35
- * • On all-keys-exhausted, the last error response is returned so the
36
- * cline SDK surfaces it normally — no special handling required
37
- * downstream.
38
- *
39
- * Concurrency
40
- * ───────────
41
- * Multiple requests can be in flight. The current-key index is shared
42
- * state but JS is single-threaded, so a simple counter is safe. Each
43
- * request reads the counter once at entry and writes it on success.
44
- */
45
-
46
- import type { FetchLike } from "./reasoning-clean.js";
47
-
48
- export interface KeyRotationOptions {
49
- /** Provider id whose requests should be wrapped (e.g. "minimax"). */
50
- providerId: string;
51
- /**
52
- * List of API keys to rotate through. Must have at least 2 entries
53
- * for rotation to kick in. A single-entry list (or empty) makes the
54
- * wrapper a pass-through no-op.
55
- */
56
- apiKeys: string[];
57
- /** Optional debug logger. */
58
- debug?: (msg: string) => void;
59
- }
60
-
61
- /**
62
- * HTTP status codes that indicate "this key is exhausted, try the next".
63
- * 401 (unauthorized) is intentionally excluded — a bad key won't get
64
- * better by retrying, and surfacing the auth error to the user is more
65
- * useful than silently rotating past it.
66
- */
67
- const RETRYABLE_STATUSES = new Set([
68
- 429, // Too Many Requests / rate limit
69
- 402, // Payment Required (quota exhausted)
70
- 500, 502, 503, 504, // Server errors that may be account-specific
71
- ]);
72
-
73
- /** Decide whether `url` targets the provider we should rotate for.
74
- * Matches by substring on the provider id, mirroring the matcher in
75
- * `reasoning-clean.ts` so the two wrappers stay consistent. */
76
- function isProviderUrl(url: string, providerId: string): boolean {
77
- const lower = url.toLowerCase();
78
- const lp = providerId.toLowerCase();
79
- return (
80
- lower.includes(`/${lp}/`) ||
81
- lower.includes(`/${lp}?`) ||
82
- lower.includes(`${lp}.`) ||
83
- lower.includes(`-${lp}.`) ||
84
- lower.includes(`.${lp}/`)
85
- );
86
- }
87
-
88
- /** Return a fresh `RequestInit` whose `Authorization` header is `Bearer <key>`.
89
- * Other headers from `init` are preserved. */
90
- function injectKey(init: RequestInit | undefined, key: string): RequestInit {
91
- const headers = new Headers(init?.headers ?? {});
92
- headers.set("Authorization", `Bearer ${key}`);
93
- return { ...init, headers };
94
- }
95
-
96
- /**
97
- * Wrap a fetch implementation so that requests to the target provider
98
- * rotate through `apiKeys` on retryable status codes. Returns a function
99
- * with the same signature as the original fetch.
100
- */
101
- export function wrapFetchForKeyRotation(
102
- originalFetch: FetchLike,
103
- options: KeyRotationOptions,
104
- ): FetchLike {
105
- const { providerId, apiKeys, debug } = options;
106
-
107
- if (apiKeys.length < 2) {
108
- debug?.(
109
- `key-rotation: apiKeys has ${apiKeys.length} entries; rotation disabled (need >= 2)`,
110
- );
111
- return originalFetch;
112
- }
113
-
114
- let currentIndex = 0;
115
- let successCount = 0;
116
- let rotationCount = 0;
117
-
118
- return async (input, init) => {
119
- const url =
120
- typeof input === "string"
121
- ? input
122
- : input instanceof URL
123
- ? input.toString()
124
- : (input as Request).url;
125
-
126
- // Only intercept POST chat-completions on the target provider.
127
- // Everything else passes through untouched so the reasoning-clean
128
- // wrapper (and any other wrapper) still sees the request.
129
- if (!/\/chat\/completions(?:\?|$)/.test(url)) return originalFetch(input, init);
130
- if (!isProviderUrl(url, providerId)) return originalFetch(input, init);
131
- if ((init?.method ?? "POST").toUpperCase() !== "POST") return originalFetch(input, init);
132
-
133
- const startIndex = currentIndex;
134
- let lastResponse: Response | null = null;
135
-
136
- for (let attempt = 0; attempt < apiKeys.length; attempt++) {
137
- const keyIndex = (startIndex + attempt) % apiKeys.length;
138
- const key = apiKeys[keyIndex]!;
139
- const modifiedInit = injectKey(init, key);
140
-
141
- let response: Response;
142
- try {
143
- response = await originalFetch(input, modifiedInit);
144
- } catch (err) {
145
- debug?.(
146
- `key-rotation: fetch threw for key #${keyIndex}: ${(err as Error).message}`,
147
- );
148
- // Network errors also trigger retry with the next key.
149
- if (attempt < apiKeys.length - 1) continue;
150
- throw err;
151
- }
152
-
153
- lastResponse = response;
154
-
155
- if (!RETRYABLE_STATUSES.has(response.status)) {
156
- if (response.ok) {
157
- successCount++;
158
- // Advance the starting index so the next request begins on
159
- // a different key — spreads load and avoids one hot key.
160
- currentIndex = (keyIndex + 1) % apiKeys.length;
161
- }
162
- return response;
163
- }
164
-
165
- // Retryable status. Discard the response body and try the next key.
166
- rotationCount++;
167
- debug?.(
168
- `key-rotation: key #${keyIndex} returned ${response.status}; ` +
169
- `rotating to key #${(keyIndex + 1) % apiKeys.length}`,
170
- );
171
- // Best-effort body drain so the underlying connection can be reused.
172
- try {
173
- await response.body?.cancel();
174
- } catch {
175
- /* ignore */
176
- }
177
- }
178
-
179
- debug?.(
180
- `key-rotation: all ${apiKeys.length} keys exhausted; ` +
181
- `returning last ${lastResponse?.status} response`,
182
- );
183
- return lastResponse!;
184
- };
185
- }
186
-
187
- /**
188
- * Resolve a list of MiniMax API keys from environment variables.
189
- *
190
- * Resolution order (first non-empty wins):
191
- * 1. `MINIMAX_API_KEYS` — single env var holding a comma-separated list
192
- * 2. `MINIMAX_API_KEY` + `MINIMAX_API_KEY_2`, `_3`, ..., `_16`
193
- * 3. Empty array (rotation disabled)
194
- *
195
- * Whitespace around entries is trimmed; empty strings are filtered out.
196
- * At most 16 keys are returned as a defensive cap so a misconfigured
197
- * env var (e.g. an accidental comma-spam) cannot drive runaway rotation.
198
- */
199
- export function discoverMiniMaxKeys(
200
- env: Record<string, string | undefined> = process.env,
201
- ): string[] {
202
- const explicit = env.MINIMAX_API_KEYS;
203
- if (explicit) {
204
- return explicit
205
- .split(",")
206
- .map((s) => s.trim())
207
- .filter((s) => s.length > 0)
208
- .slice(0, 16);
209
- }
210
- const keys: string[] = [];
211
- const primary = env.MINIMAX_API_KEY;
212
- if (primary && primary.trim().length > 0) keys.push(primary.trim());
213
- for (let i = 2; i <= 16; i++) {
214
- const k = env[`MINIMAX_API_KEY_${i}`];
215
- if (k && k.trim().length > 0) keys.push(k.trim());
216
- }
217
- return keys;
218
- }