@polderlabs/bizar 6.2.4 → 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 (332) 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 +31 -18
  125. package/cli/commands/claude-cmd.mjs +348 -0
  126. package/cli/commands/install.mjs +12 -11
  127. package/cli/commands/sandbox.mjs +220 -0
  128. package/cli/commands/validate.mjs +252 -288
  129. package/cli/dev-link.test.mjs +11 -11
  130. package/cli/doctor.mjs +131 -213
  131. package/cli/install.mjs +33 -37
  132. package/cli/install.test.mjs +9 -9
  133. package/cli/provision-claude.mjs +893 -0
  134. package/cli/provision.mjs +330 -226
  135. package/cli/provision.test.mjs +102 -0
  136. package/cli/utils.mjs +72 -58
  137. package/config/AGENTS.md +28 -28
  138. package/config/agents/_shared/AGENT_BASELINE.md +104 -684
  139. package/config/skills/bizar/SKILL.md +197 -0
  140. package/config/skills/cubesandbox/SKILL.md +148 -0
  141. package/config/skills/harness-engineering/SKILL.md +142 -0
  142. package/install.sh +66 -44
  143. package/package.json +19 -11
  144. package/packages/sdk/ARCHITECTURE.md +40 -36
  145. package/packages/sdk/package-lock.json +32 -0
  146. package/packages/sdk/package.json +15 -5
  147. package/{plugins/bizar → packages/sdk}/src/dangerous-patterns.ts +6 -28
  148. package/packages/sdk/src/fingerprint.ts +0 -0
  149. package/packages/sdk/src/index.ts +38 -73
  150. package/packages/sdk/src/mcp/bin.ts +51 -0
  151. package/packages/sdk/src/mcp/server.ts +498 -0
  152. package/{plugins/bizar/src/memory-vault.ts → packages/sdk/src/memory/index.ts} +18 -21
  153. package/packages/sdk/tests/sdk.test.mjs +148 -0
  154. package/packages/sdk/vitest.config.ts +1 -1
  155. package/scripts/bh-full-e2e.mjs +166 -363
  156. package/scripts/check-deps.mjs +28 -46
  157. package/scripts/mirror-claude-md.sh +78 -0
  158. package/scripts/test-in-container.sh +157 -0
  159. package/templates/clean-state-checklist.md +3 -3
  160. package/templates/sprint-contract.md +2 -2
  161. package/bizar-dash/.bizar/activity.log +0 -3
  162. package/bizar-dash/.omx/logs/omx-2026-07-07.jsonl +0 -1
  163. package/bizar-dash/.omx/state/session.json +0 -10
  164. package/bizar-dash/bizar-design/EXPLANATION.md +0 -307
  165. package/bizar-dash/bizar-design/INSTRUCTIONS.md +0 -149
  166. package/bizar-dash/bizar-design/canvas.html +0 -281
  167. package/bizar-dash/bizar-design/canvas.html.artifact.json +0 -19
  168. package/bizar-dash/bizar-design/components.css +0 -1665
  169. package/bizar-dash/bizar-design/components.html +0 -557
  170. package/bizar-dash/bizar-design/components.html.artifact.json +0 -19
  171. package/bizar-dash/bizar-design/desktop/agents.html +0 -74
  172. package/bizar-dash/bizar-design/desktop/agents.html.artifact.json +0 -19
  173. package/bizar-dash/bizar-design/desktop/memory.html +0 -75
  174. package/bizar-dash/bizar-design/desktop/memory.html.artifact.json +0 -19
  175. package/bizar-dash/bizar-design/desktop/metrics.html +0 -63
  176. package/bizar-dash/bizar-design/desktop/metrics.html.artifact.json +0 -19
  177. package/bizar-dash/bizar-design/desktop/overview.html +0 -95
  178. package/bizar-dash/bizar-design/desktop/overview.html.artifact.json +0 -19
  179. package/bizar-dash/bizar-design/desktop/settings.html +0 -70
  180. package/bizar-dash/bizar-design/desktop/settings.html.artifact.json +0 -19
  181. package/bizar-dash/bizar-design/icons.svg +0 -44
  182. package/bizar-dash/bizar-design/index.html +0 -192
  183. package/bizar-dash/bizar-design/index.html.artifact.json +0 -19
  184. package/bizar-dash/bizar-design/mobile/agents.html +0 -29
  185. package/bizar-dash/bizar-design/mobile/agents.html.artifact.json +0 -19
  186. package/bizar-dash/bizar-design/mobile/memory.html +0 -28
  187. package/bizar-dash/bizar-design/mobile/memory.html.artifact.json +0 -19
  188. package/bizar-dash/bizar-design/mobile/metrics.html +0 -29
  189. package/bizar-dash/bizar-design/mobile/metrics.html.artifact.json +0 -19
  190. package/bizar-dash/bizar-design/mobile/overview.html +0 -33
  191. package/bizar-dash/bizar-design/mobile/overview.html.artifact.json +0 -19
  192. package/bizar-dash/bizar-design/mobile/settings.html +0 -28
  193. package/bizar-dash/bizar-design/mobile/settings.html.artifact.json +0 -19
  194. package/bizar-dash/bizar-design/prototype.js +0 -114
  195. package/bizar-dash/bizar-design/tokens.css +0 -106
  196. package/bizar-dash/dist/assets/vendor-CeHGtduv.js.map +0 -1
  197. package/bizar-dash/node_modules/.package-lock.json +0 -6
  198. package/bizar-dash/package-lock.json +0 -6
  199. package/bizar-dash/src/server/cline-sdk.mjs +0 -132
  200. package/bizar-dash/src/server/routes/cline-session-detail.mjs +0 -559
  201. package/bizar-dash/src/server/routes/cline-sessions.mjs +0 -291
  202. package/cli/commands/cline-cmd.mjs +0 -289
  203. package/cli/commands/validate.test.mjs +0 -344
  204. package/cli/doctor.test.mjs +0 -350
  205. package/config/cline.json.template +0 -342
  206. package/packages/sdk/src/client.ts +0 -188
  207. package/packages/sdk/src/cline-events.ts +0 -134
  208. package/packages/sdk/src/cline-types.ts +0 -66
  209. package/packages/sdk/src/cline.ts +0 -339
  210. package/packages/sdk/src/errors.ts +0 -129
  211. package/packages/sdk/src/events.ts +0 -153
  212. package/packages/sdk/src/types.ts +0 -176
  213. package/packages/sdk/tests/client.test.ts +0 -217
  214. package/packages/sdk/tests/errors.test.ts +0 -108
  215. package/packages/sdk/tests/events.test.ts +0 -139
  216. package/packages/sdk/tests/fixtures/fetch-mock.ts +0 -152
  217. package/packages/sdk/tests/fixtures/sse-mock.ts +0 -30
  218. package/plugins/bizar/ARCHITECTURE.md +0 -142
  219. package/plugins/bizar/CONSTRAINTS.md +0 -67
  220. package/plugins/bizar/LICENSE +0 -21
  221. package/plugins/bizar/README.md +0 -448
  222. package/plugins/bizar/index.ts +0 -872
  223. package/plugins/bizar/package.json +0 -39
  224. package/plugins/bizar/scripts/check-forbidden-imports.sh +0 -33
  225. package/plugins/bizar/src/background-state.ts +0 -641
  226. package/plugins/bizar/src/background.ts +0 -1806
  227. package/plugins/bizar/src/cline-runner.ts +0 -203
  228. package/plugins/bizar/src/clineruntime.ts +0 -227
  229. package/plugins/bizar/src/commands-impl.ts +0 -151
  230. package/plugins/bizar/src/commands.ts +0 -1799
  231. package/plugins/bizar/src/compaction.d.mts +0 -48
  232. package/plugins/bizar/src/compaction.mjs +0 -192
  233. package/plugins/bizar/src/dashboard-client.ts +0 -233
  234. package/plugins/bizar/src/event-stream.ts +0 -606
  235. package/plugins/bizar/src/fingerprint.ts +0 -120
  236. package/plugins/bizar/src/handoff.ts +0 -79
  237. package/plugins/bizar/src/hooks/memory-flush-on-compact.ts +0 -123
  238. package/plugins/bizar/src/hooks/memory-inject.ts +0 -247
  239. package/plugins/bizar/src/hooks/memory-write-on-end.ts +0 -188
  240. package/plugins/bizar/src/hooks/skill-curator.ts +0 -180
  241. package/plugins/bizar/src/http-client.ts +0 -467
  242. package/plugins/bizar/src/key-rotation.ts +0 -218
  243. package/plugins/bizar/src/logger.ts +0 -144
  244. package/plugins/bizar/src/loop-engineering.ts +0 -241
  245. package/plugins/bizar/src/loop.ts +0 -176
  246. package/plugins/bizar/src/mistake-recovery.ts +0 -98
  247. package/plugins/bizar/src/odin.ts +0 -227
  248. package/plugins/bizar/src/options.ts +0 -470
  249. package/plugins/bizar/src/plan-fs.ts +0 -323
  250. package/plugins/bizar/src/reasoning-clean.ts +0 -454
  251. package/plugins/bizar/src/report.ts +0 -178
  252. package/plugins/bizar/src/research-prompt.ts +0 -35
  253. package/plugins/bizar/src/serve-info.ts +0 -228
  254. package/plugins/bizar/src/serve.ts +0 -496
  255. package/plugins/bizar/src/settings.ts +0 -349
  256. package/plugins/bizar/src/state.ts +0 -298
  257. package/plugins/bizar/src/tool-discipline.ts +0 -105
  258. package/plugins/bizar/src/tools/agent-browser.ts +0 -315
  259. package/plugins/bizar/src/tools/bg-collect.ts +0 -131
  260. package/plugins/bizar/src/tools/bg-get-comments.ts +0 -266
  261. package/plugins/bizar/src/tools/bg-kill.ts +0 -116
  262. package/plugins/bizar/src/tools/bg-pause.ts +0 -99
  263. package/plugins/bizar/src/tools/bg-report-progress.ts +0 -115
  264. package/plugins/bizar/src/tools/bg-resume.ts +0 -94
  265. package/plugins/bizar/src/tools/bg-send-message.ts +0 -223
  266. package/plugins/bizar/src/tools/bg-spawn.ts +0 -502
  267. package/plugins/bizar/src/tools/bg-status.ts +0 -130
  268. package/plugins/bizar/src/tools/graph-query.ts +0 -278
  269. package/plugins/bizar/src/tools/loop-engineering.ts +0 -193
  270. package/plugins/bizar/src/tools/memory-list.ts +0 -43
  271. package/plugins/bizar/src/tools/memory-read.ts +0 -69
  272. package/plugins/bizar/src/tools/memory-search.ts +0 -47
  273. package/plugins/bizar/src/tools/memory-write.ts +0 -54
  274. package/plugins/bizar/src/tools/open-kb.ts +0 -198
  275. package/plugins/bizar/src/tools/plan-action.ts +0 -785
  276. package/plugins/bizar/src/tools/read-glyph-feedback.ts +0 -191
  277. package/plugins/bizar/src/tools/team-spawn.ts +0 -73
  278. package/plugins/bizar/src/tools/team-status.ts +0 -76
  279. package/plugins/bizar/src/tools/wait-for-feedback.ts +0 -415
  280. package/plugins/bizar/src/trajectory.ts +0 -104
  281. package/plugins/bizar/tests/README.md +0 -99
  282. package/plugins/bizar/tests/attach-handler-bug.test.ts +0 -169
  283. package/plugins/bizar/tests/background-state.test.ts +0 -277
  284. package/plugins/bizar/tests/background.test.ts +0 -402
  285. package/plugins/bizar/tests/block.test.ts +0 -195
  286. package/plugins/bizar/tests/canonical-key-order.test.ts +0 -75
  287. package/plugins/bizar/tests/clineruntime-config.test.ts +0 -283
  288. package/plugins/bizar/tests/commands-impl.test.ts +0 -316
  289. package/plugins/bizar/tests/commands.test.ts +0 -584
  290. package/plugins/bizar/tests/compaction.test.ts +0 -264
  291. package/plugins/bizar/tests/config.test.ts +0 -128
  292. package/plugins/bizar/tests/dashboard-client.test.ts +0 -159
  293. package/plugins/bizar/tests/dispose.test.ts +0 -336
  294. package/plugins/bizar/tests/event-stream.test.ts +0 -409
  295. package/plugins/bizar/tests/event.test.ts +0 -262
  296. package/plugins/bizar/tests/fingerprint.test.ts +0 -162
  297. package/plugins/bizar/tests/http-client.test.ts +0 -404
  298. package/plugins/bizar/tests/init-helpers.test.ts +0 -203
  299. package/plugins/bizar/tests/integration/slash-command.test.ts +0 -349
  300. package/plugins/bizar/tests/integration/tool-routing.test.ts +0 -98
  301. package/plugins/bizar/tests/key-rotation.test.ts +0 -396
  302. package/plugins/bizar/tests/loop-engineering.test.ts +0 -168
  303. package/plugins/bizar/tests/loop.test.ts +0 -397
  304. package/plugins/bizar/tests/memory-write-on-end.test.ts +0 -92
  305. package/plugins/bizar/tests/mistake-recovery.test.ts +0 -116
  306. package/plugins/bizar/tests/odin.test.ts +0 -125
  307. package/plugins/bizar/tests/options.test.ts +0 -329
  308. package/plugins/bizar/tests/reasoning-clean.test.ts +0 -422
  309. package/plugins/bizar/tests/safety.test.ts +0 -256
  310. package/plugins/bizar/tests/serve.test.ts +0 -339
  311. package/plugins/bizar/tests/settings.test.ts +0 -351
  312. package/plugins/bizar/tests/stall-think.test.ts +0 -750
  313. package/plugins/bizar/tests/state.test.ts +0 -276
  314. package/plugins/bizar/tests/tool-discipline.test.ts +0 -77
  315. package/plugins/bizar/tests/tools/agent-browser.test.ts +0 -98
  316. package/plugins/bizar/tests/tools/bg-collect.test.ts +0 -337
  317. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +0 -485
  318. package/plugins/bizar/tests/tools/bg-kill.test.ts +0 -235
  319. package/plugins/bizar/tests/tools/bg-pause.test.ts +0 -61
  320. package/plugins/bizar/tests/tools/bg-report-progress.test.ts +0 -79
  321. package/plugins/bizar/tests/tools/bg-resume.test.ts +0 -40
  322. package/plugins/bizar/tests/tools/bg-send-message.test.ts +0 -116
  323. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +0 -147
  324. package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +0 -233
  325. package/plugins/bizar/tests/tools/bg-spawn.test.ts +0 -311
  326. package/plugins/bizar/tests/tools/bg-status.test.ts +0 -217
  327. package/plugins/bizar/tests/tools/cline-runner.test.ts +0 -115
  328. package/plugins/bizar/tests/tools/plan-action.test.ts +0 -599
  329. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +0 -253
  330. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +0 -390
  331. package/plugins/bizar/tests/update-deadlock.test.ts +0 -151
  332. package/plugins/bizar/tsconfig.json +0 -29
@@ -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
- }