@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.
- package/.claude/CLAUDE.md +15 -0
- package/.claude/agents/_shared/AGENT_BASELINE.md +168 -0
- package/.claude/agents/_shared/CLAUDE_TOOLS.md +412 -0
- package/.claude/agents/_shared/SKILLS.md +109 -0
- package/.claude/agents/agent-browser.md +80 -0
- package/.claude/agents/baldr.md +49 -0
- package/.claude/agents/forseti.md +51 -0
- package/.claude/agents/frigg.md +42 -0
- package/.claude/agents/heimdall.md +33 -0
- package/.claude/agents/hermod.md +53 -0
- package/.claude/agents/mimir.md +49 -0
- package/.claude/agents/odin.md +287 -0
- package/.claude/agents/quick.md +34 -0
- package/.claude/agents/semble-search.md +50 -0
- package/.claude/agents/thor.md +53 -0
- package/.claude/agents/tyr.md +56 -0
- package/.claude/agents/vidarr.md +54 -0
- package/.claude/agents/vor.md +53 -0
- package/.claude/commands/audit.md +25 -0
- package/.claude/commands/bizar.md +22 -0
- package/.claude/commands/explain.md +17 -0
- package/.claude/commands/init.md +29 -0
- package/.claude/commands/learn.md +44 -0
- package/.claude/commands/plan.md +35 -0
- package/.claude/commands/plow-through.md +50 -0
- package/.claude/commands/pr-review.md +49 -0
- package/.claude/commands/setup-provider.md +96 -0
- package/.claude/commands/tailscale-serve.md +100 -0
- package/.claude/commands/team.md +132 -0
- package/.claude/commands/test.md +62 -0
- package/.claude/commands/validate.md +68 -0
- package/.claude/commands/visual-plan.md +24 -0
- package/.claude/hooks/README.md +92 -0
- package/.claude/hooks/posttooluse-editwrite.mjs +91 -0
- package/.claude/hooks/pretooluse-bash.mjs +81 -0
- package/.claude/hooks/pretooluse-editwrite.mjs +139 -0
- package/.claude/hooks/sessionend-recall.mjs +74 -0
- package/.claude/hooks/sessionstart-prime.mjs +80 -0
- package/.claude/hooks/userpromptsubmit-tag.mjs +80 -0
- package/.claude/settings.json +116 -0
- package/.claude/skills/9router/SKILL.md +80 -0
- package/.claude/skills/9router-chat/SKILL.md +73 -0
- package/.claude/skills/9router-embeddings/SKILL.md +69 -0
- package/.claude/skills/9router-image/SKILL.md +86 -0
- package/.claude/skills/9router-stt/SKILL.md +79 -0
- package/.claude/skills/9router-tts/SKILL.md +80 -0
- package/.claude/skills/9router-web-fetch/SKILL.md +99 -0
- package/.claude/skills/9router-web-search/SKILL.md +91 -0
- package/.claude/skills/bizar/README.md +9 -0
- package/.claude/skills/bizar/SKILL.md +450 -0
- package/.claude/skills/cpp-coding-standards/README.md +28 -0
- package/.claude/skills/cpp-coding-standards/SKILL.md +634 -0
- package/.claude/skills/cpp-coding-standards/references/concurrency.md +320 -0
- package/.claude/skills/cpp-coding-standards/references/error-handling.md +229 -0
- package/.claude/skills/cpp-coding-standards/references/memory-safety.md +216 -0
- package/.claude/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
- package/.claude/skills/cpp-coding-standards/references/review-checklist.md +96 -0
- package/.claude/skills/cpp-testing/README.md +28 -0
- package/.claude/skills/cpp-testing/SKILL.md +304 -0
- package/.claude/skills/cpp-testing/references/coverage.md +370 -0
- package/.claude/skills/cpp-testing/references/framework-compare.md +175 -0
- package/.claude/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
- package/.claude/skills/cpp-testing/references/mocking.md +364 -0
- package/.claude/skills/cpp-testing/references/tdd-workflow.md +308 -0
- package/.claude/skills/cubesandbox/SKILL.md +148 -0
- package/.claude/skills/embedded-esp-idf/README.md +41 -0
- package/.claude/skills/embedded-esp-idf/SKILL.md +439 -0
- package/.claude/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
- package/.claude/skills/embedded-esp-idf/references/host-tests.md +164 -0
- package/.claude/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
- package/.claude/skills/embedded-esp-idf/references/kconfig.md +159 -0
- package/.claude/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
- package/.claude/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
- package/.claude/skills/embedded-esp-idf/references/nvs.md +121 -0
- package/.claude/skills/embedded-esp-idf/references/packed-structs.md +192 -0
- package/.claude/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
- package/.claude/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
- package/.claude/skills/glyph/SKILL.md +163 -0
- package/.claude/skills/harness-engineering/SKILL.md +143 -0
- package/.claude/skills/lightrag/SKILL.md +81 -0
- package/.claude/skills/memory-protocol/SKILL.md +105 -0
- package/.claude/skills/obsidian/SKILL.md +306 -0
- package/.claude/skills/read-the-damn-docs/SKILL.md +113 -0
- package/.claude/skills/self-improvement/SKILL.md +64 -0
- package/README.md +87 -59
- package/bizar-dash/dist/assets/{EnvVarsSection-DXM8gRm_.js → EnvVarsSection-B58aiJiE.js} +1 -1
- package/bizar-dash/dist/assets/{EnvVarsSection-DXM8gRm_.js.map → EnvVarsSection-B58aiJiE.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-BnKN_Ks_.js → MobileChat-BJrqwVDd.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-BnKN_Ks_.js.map → MobileChat-BJrqwVDd.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileSettings-DjCPxC-Q.js → MobileSettings-CEQNJNLJ.js} +1 -1
- package/bizar-dash/dist/assets/{MobileSettings-DjCPxC-Q.js.map → MobileSettings-CEQNJNLJ.js.map} +1 -1
- package/bizar-dash/dist/assets/{main-DYiZqMrn.js → main-IvfQAOfy.js} +1 -1
- package/bizar-dash/dist/assets/{main-DYiZqMrn.js.map → main-IvfQAOfy.js.map} +1 -1
- package/bizar-dash/dist/assets/{markdown-C6mXtQxD.js → markdown-tOLaD6nm.js} +1 -1
- package/bizar-dash/dist/assets/{markdown-C6mXtQxD.js.map → markdown-tOLaD6nm.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-CMHqtLV2.js → mobile-DYCHcUpq.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-CMHqtLV2.js.map → mobile-DYCHcUpq.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-3jIhHX_p.js → mobile-layout-CBHjpwsb.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-layout-3jIhHX_p.js.map → mobile-layout-CBHjpwsb.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-DtITw8Xv.js → useSlashCommands-Bd7_FA6U.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-DtITw8Xv.js.map → useSlashCommands-Bd7_FA6U.js.map} +1 -1
- package/bizar-dash/dist/assets/{vendor-CeHGtduv.js → vendor-C843201K.js} +12 -12
- package/bizar-dash/dist/assets/vendor-C843201K.js.map +1 -0
- package/bizar-dash/dist/index.html +6 -6
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +4 -4
- package/bizar-dash/src/server/background-store.mjs +15 -16
- package/bizar-dash/src/server/bg-poller.mjs +25 -35
- package/bizar-dash/src/server/bg-retry.mjs +66 -194
- package/bizar-dash/src/server/claude-bg-spawner.mjs +390 -0
- package/bizar-dash/src/server/claude-info.mjs +412 -0
- package/bizar-dash/src/server/{cline-runner.mjs → claude-runner.mjs} +53 -48
- package/bizar-dash/src/server/claude-sdk.mjs +191 -0
- package/bizar-dash/src/server/providers-store.mjs +22 -49
- package/bizar-dash/src/server/routes/background.mjs +24 -20
- package/bizar-dash/src/server/routes/chat.mjs +223 -326
- package/bizar-dash/src/server/routes/claude-session-detail.mjs +329 -0
- package/bizar-dash/src/server/routes/claude-sessions.mjs +259 -0
- package/bizar-dash/src/server/routes/tasks.mjs +20 -28
- package/bizar-dash/src/server/task-delegator.mjs +76 -77
- package/bizar-dash/src/web/components/chat/useChat.ts +23 -27
- package/bizar-dash/src/web/views/Chat.tsx +8 -8
- package/cli/bin.mjs +31 -18
- package/cli/commands/claude-cmd.mjs +348 -0
- package/cli/commands/install.mjs +12 -11
- package/cli/commands/sandbox.mjs +220 -0
- package/cli/commands/validate.mjs +252 -288
- package/cli/dev-link.test.mjs +11 -11
- package/cli/doctor.mjs +131 -213
- package/cli/install.mjs +33 -37
- package/cli/install.test.mjs +9 -9
- package/cli/provision-claude.mjs +893 -0
- package/cli/provision.mjs +330 -226
- package/cli/provision.test.mjs +102 -0
- package/cli/utils.mjs +72 -58
- package/config/AGENTS.md +28 -28
- package/config/agents/_shared/AGENT_BASELINE.md +104 -684
- package/config/skills/bizar/SKILL.md +197 -0
- package/config/skills/cubesandbox/SKILL.md +148 -0
- package/config/skills/harness-engineering/SKILL.md +142 -0
- package/install.sh +66 -44
- package/package.json +19 -11
- package/packages/sdk/ARCHITECTURE.md +40 -36
- package/packages/sdk/package-lock.json +32 -0
- package/packages/sdk/package.json +15 -5
- package/{plugins/bizar → packages/sdk}/src/dangerous-patterns.ts +6 -28
- package/packages/sdk/src/fingerprint.ts +0 -0
- package/packages/sdk/src/index.ts +38 -73
- package/packages/sdk/src/mcp/bin.ts +51 -0
- package/packages/sdk/src/mcp/server.ts +498 -0
- package/{plugins/bizar/src/memory-vault.ts → packages/sdk/src/memory/index.ts} +18 -21
- package/packages/sdk/tests/sdk.test.mjs +148 -0
- package/packages/sdk/vitest.config.ts +1 -1
- package/scripts/bh-full-e2e.mjs +166 -363
- package/scripts/check-deps.mjs +28 -46
- package/scripts/mirror-claude-md.sh +78 -0
- package/scripts/test-in-container.sh +157 -0
- package/templates/clean-state-checklist.md +3 -3
- package/templates/sprint-contract.md +2 -2
- package/bizar-dash/.bizar/activity.log +0 -3
- package/bizar-dash/.omx/logs/omx-2026-07-07.jsonl +0 -1
- package/bizar-dash/.omx/state/session.json +0 -10
- package/bizar-dash/bizar-design/EXPLANATION.md +0 -307
- package/bizar-dash/bizar-design/INSTRUCTIONS.md +0 -149
- package/bizar-dash/bizar-design/canvas.html +0 -281
- package/bizar-dash/bizar-design/canvas.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/components.css +0 -1665
- package/bizar-dash/bizar-design/components.html +0 -557
- package/bizar-dash/bizar-design/components.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/agents.html +0 -74
- package/bizar-dash/bizar-design/desktop/agents.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/memory.html +0 -75
- package/bizar-dash/bizar-design/desktop/memory.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/metrics.html +0 -63
- package/bizar-dash/bizar-design/desktop/metrics.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/overview.html +0 -95
- package/bizar-dash/bizar-design/desktop/overview.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/desktop/settings.html +0 -70
- package/bizar-dash/bizar-design/desktop/settings.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/icons.svg +0 -44
- package/bizar-dash/bizar-design/index.html +0 -192
- package/bizar-dash/bizar-design/index.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/agents.html +0 -29
- package/bizar-dash/bizar-design/mobile/agents.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/memory.html +0 -28
- package/bizar-dash/bizar-design/mobile/memory.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/metrics.html +0 -29
- package/bizar-dash/bizar-design/mobile/metrics.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/overview.html +0 -33
- package/bizar-dash/bizar-design/mobile/overview.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/mobile/settings.html +0 -28
- package/bizar-dash/bizar-design/mobile/settings.html.artifact.json +0 -19
- package/bizar-dash/bizar-design/prototype.js +0 -114
- package/bizar-dash/bizar-design/tokens.css +0 -106
- package/bizar-dash/dist/assets/vendor-CeHGtduv.js.map +0 -1
- package/bizar-dash/node_modules/.package-lock.json +0 -6
- package/bizar-dash/package-lock.json +0 -6
- package/bizar-dash/src/server/cline-sdk.mjs +0 -132
- package/bizar-dash/src/server/routes/cline-session-detail.mjs +0 -559
- package/bizar-dash/src/server/routes/cline-sessions.mjs +0 -291
- package/cli/commands/cline-cmd.mjs +0 -289
- package/cli/commands/validate.test.mjs +0 -344
- package/cli/doctor.test.mjs +0 -350
- package/config/cline.json.template +0 -342
- package/packages/sdk/src/client.ts +0 -188
- package/packages/sdk/src/cline-events.ts +0 -134
- package/packages/sdk/src/cline-types.ts +0 -66
- package/packages/sdk/src/cline.ts +0 -339
- package/packages/sdk/src/errors.ts +0 -129
- package/packages/sdk/src/events.ts +0 -153
- package/packages/sdk/src/types.ts +0 -176
- package/packages/sdk/tests/client.test.ts +0 -217
- package/packages/sdk/tests/errors.test.ts +0 -108
- package/packages/sdk/tests/events.test.ts +0 -139
- package/packages/sdk/tests/fixtures/fetch-mock.ts +0 -152
- package/packages/sdk/tests/fixtures/sse-mock.ts +0 -30
- package/plugins/bizar/ARCHITECTURE.md +0 -142
- package/plugins/bizar/CONSTRAINTS.md +0 -67
- package/plugins/bizar/LICENSE +0 -21
- package/plugins/bizar/README.md +0 -448
- package/plugins/bizar/index.ts +0 -872
- package/plugins/bizar/package.json +0 -39
- package/plugins/bizar/scripts/check-forbidden-imports.sh +0 -33
- package/plugins/bizar/src/background-state.ts +0 -641
- package/plugins/bizar/src/background.ts +0 -1806
- package/plugins/bizar/src/cline-runner.ts +0 -203
- package/plugins/bizar/src/clineruntime.ts +0 -227
- package/plugins/bizar/src/commands-impl.ts +0 -151
- package/plugins/bizar/src/commands.ts +0 -1799
- package/plugins/bizar/src/compaction.d.mts +0 -48
- package/plugins/bizar/src/compaction.mjs +0 -192
- package/plugins/bizar/src/dashboard-client.ts +0 -233
- package/plugins/bizar/src/event-stream.ts +0 -606
- package/plugins/bizar/src/fingerprint.ts +0 -120
- package/plugins/bizar/src/handoff.ts +0 -79
- package/plugins/bizar/src/hooks/memory-flush-on-compact.ts +0 -123
- package/plugins/bizar/src/hooks/memory-inject.ts +0 -247
- package/plugins/bizar/src/hooks/memory-write-on-end.ts +0 -188
- package/plugins/bizar/src/hooks/skill-curator.ts +0 -180
- package/plugins/bizar/src/http-client.ts +0 -467
- package/plugins/bizar/src/key-rotation.ts +0 -218
- package/plugins/bizar/src/logger.ts +0 -144
- package/plugins/bizar/src/loop-engineering.ts +0 -241
- package/plugins/bizar/src/loop.ts +0 -176
- package/plugins/bizar/src/mistake-recovery.ts +0 -98
- package/plugins/bizar/src/odin.ts +0 -227
- package/plugins/bizar/src/options.ts +0 -470
- package/plugins/bizar/src/plan-fs.ts +0 -323
- package/plugins/bizar/src/reasoning-clean.ts +0 -454
- package/plugins/bizar/src/report.ts +0 -178
- package/plugins/bizar/src/research-prompt.ts +0 -35
- package/plugins/bizar/src/serve-info.ts +0 -228
- package/plugins/bizar/src/serve.ts +0 -496
- package/plugins/bizar/src/settings.ts +0 -349
- package/plugins/bizar/src/state.ts +0 -298
- package/plugins/bizar/src/tool-discipline.ts +0 -105
- package/plugins/bizar/src/tools/agent-browser.ts +0 -315
- package/plugins/bizar/src/tools/bg-collect.ts +0 -131
- package/plugins/bizar/src/tools/bg-get-comments.ts +0 -266
- package/plugins/bizar/src/tools/bg-kill.ts +0 -116
- package/plugins/bizar/src/tools/bg-pause.ts +0 -99
- package/plugins/bizar/src/tools/bg-report-progress.ts +0 -115
- package/plugins/bizar/src/tools/bg-resume.ts +0 -94
- package/plugins/bizar/src/tools/bg-send-message.ts +0 -223
- package/plugins/bizar/src/tools/bg-spawn.ts +0 -502
- package/plugins/bizar/src/tools/bg-status.ts +0 -130
- package/plugins/bizar/src/tools/graph-query.ts +0 -278
- package/plugins/bizar/src/tools/loop-engineering.ts +0 -193
- package/plugins/bizar/src/tools/memory-list.ts +0 -43
- package/plugins/bizar/src/tools/memory-read.ts +0 -69
- package/plugins/bizar/src/tools/memory-search.ts +0 -47
- package/plugins/bizar/src/tools/memory-write.ts +0 -54
- package/plugins/bizar/src/tools/open-kb.ts +0 -198
- package/plugins/bizar/src/tools/plan-action.ts +0 -785
- package/plugins/bizar/src/tools/read-glyph-feedback.ts +0 -191
- package/plugins/bizar/src/tools/team-spawn.ts +0 -73
- package/plugins/bizar/src/tools/team-status.ts +0 -76
- package/plugins/bizar/src/tools/wait-for-feedback.ts +0 -415
- package/plugins/bizar/src/trajectory.ts +0 -104
- package/plugins/bizar/tests/README.md +0 -99
- package/plugins/bizar/tests/attach-handler-bug.test.ts +0 -169
- package/plugins/bizar/tests/background-state.test.ts +0 -277
- package/plugins/bizar/tests/background.test.ts +0 -402
- package/plugins/bizar/tests/block.test.ts +0 -195
- package/plugins/bizar/tests/canonical-key-order.test.ts +0 -75
- package/plugins/bizar/tests/clineruntime-config.test.ts +0 -283
- package/plugins/bizar/tests/commands-impl.test.ts +0 -316
- package/plugins/bizar/tests/commands.test.ts +0 -584
- package/plugins/bizar/tests/compaction.test.ts +0 -264
- package/plugins/bizar/tests/config.test.ts +0 -128
- package/plugins/bizar/tests/dashboard-client.test.ts +0 -159
- package/plugins/bizar/tests/dispose.test.ts +0 -336
- package/plugins/bizar/tests/event-stream.test.ts +0 -409
- package/plugins/bizar/tests/event.test.ts +0 -262
- package/plugins/bizar/tests/fingerprint.test.ts +0 -162
- package/plugins/bizar/tests/http-client.test.ts +0 -404
- package/plugins/bizar/tests/init-helpers.test.ts +0 -203
- package/plugins/bizar/tests/integration/slash-command.test.ts +0 -349
- package/plugins/bizar/tests/integration/tool-routing.test.ts +0 -98
- package/plugins/bizar/tests/key-rotation.test.ts +0 -396
- package/plugins/bizar/tests/loop-engineering.test.ts +0 -168
- package/plugins/bizar/tests/loop.test.ts +0 -397
- package/plugins/bizar/tests/memory-write-on-end.test.ts +0 -92
- package/plugins/bizar/tests/mistake-recovery.test.ts +0 -116
- package/plugins/bizar/tests/odin.test.ts +0 -125
- package/plugins/bizar/tests/options.test.ts +0 -329
- package/plugins/bizar/tests/reasoning-clean.test.ts +0 -422
- package/plugins/bizar/tests/safety.test.ts +0 -256
- package/plugins/bizar/tests/serve.test.ts +0 -339
- package/plugins/bizar/tests/settings.test.ts +0 -351
- package/plugins/bizar/tests/stall-think.test.ts +0 -750
- package/plugins/bizar/tests/state.test.ts +0 -276
- package/plugins/bizar/tests/tool-discipline.test.ts +0 -77
- package/plugins/bizar/tests/tools/agent-browser.test.ts +0 -98
- package/plugins/bizar/tests/tools/bg-collect.test.ts +0 -337
- package/plugins/bizar/tests/tools/bg-get-comments.test.ts +0 -485
- package/plugins/bizar/tests/tools/bg-kill.test.ts +0 -235
- package/plugins/bizar/tests/tools/bg-pause.test.ts +0 -61
- package/plugins/bizar/tests/tools/bg-report-progress.test.ts +0 -79
- package/plugins/bizar/tests/tools/bg-resume.test.ts +0 -40
- package/plugins/bizar/tests/tools/bg-send-message.test.ts +0 -116
- package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +0 -147
- package/plugins/bizar/tests/tools/bg-spawn-http.test.ts +0 -233
- package/plugins/bizar/tests/tools/bg-spawn.test.ts +0 -311
- package/plugins/bizar/tests/tools/bg-status.test.ts +0 -217
- package/plugins/bizar/tests/tools/cline-runner.test.ts +0 -115
- package/plugins/bizar/tests/tools/plan-action.test.ts +0 -599
- package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +0 -253
- package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +0 -390
- package/plugins/bizar/tests/update-deadlock.test.ts +0 -151
- 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
|
-
}
|