@phuetz/code-buddy 1.0.0 → 1.2.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 (312) hide show
  1. package/README.md +160 -164
  2. package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
  3. package/dist/agent/codebuddy-agent.d.ts +1 -1
  4. package/dist/agent/codebuddy-agent.js +101 -13
  5. package/dist/agent/execution/agent-executor.js +122 -12
  6. package/dist/agent/execution/tool-dependency-graph.js +7 -0
  7. package/dist/agent/execution/tool-hooks.d.ts +6 -0
  8. package/dist/agent/execution/tool-hooks.js +13 -5
  9. package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
  10. package/dist/agent/execution/tool-selection-strategy.js +67 -10
  11. package/dist/agent/extended-thinking.d.ts +8 -0
  12. package/dist/agent/extended-thinking.js +27 -0
  13. package/dist/agent/hermes-browser-backends.js +151 -11
  14. package/dist/agent/hermes-claw-migrate.d.ts +59 -0
  15. package/dist/agent/hermes-claw-migrate.js +427 -26
  16. package/dist/agent/hermes-parity-manifest.js +78 -71
  17. package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
  18. package/dist/agent/hermes-runtime-lifecycle.js +468 -0
  19. package/dist/agent/lesson-auto-proposer.js +10 -0
  20. package/dist/agent/middleware/index.d.ts +1 -0
  21. package/dist/agent/middleware/index.js +1 -0
  22. package/dist/agent/middleware/session-duration.d.ts +36 -0
  23. package/dist/agent/middleware/session-duration.js +78 -0
  24. package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
  25. package/dist/agent/middleware/visual-validation-middleware.js +67 -0
  26. package/dist/agent/model-benchmark.d.ts +77 -0
  27. package/dist/agent/model-benchmark.js +309 -0
  28. package/dist/agent/model-tier.d.ts +14 -0
  29. package/dist/agent/model-tier.js +71 -0
  30. package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
  31. package/dist/agent/multi-agent/multi-agent-system.js +3 -1
  32. package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
  33. package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
  34. package/dist/agent/multi-agent/session-tools.js +22 -5
  35. package/dist/agent/reasoning/mcts.js +20 -6
  36. package/dist/agent/session-end-flush.d.ts +71 -0
  37. package/dist/agent/session-end-flush.js +233 -0
  38. package/dist/agent/streaming/streaming-handler.d.ts +7 -0
  39. package/dist/agent/streaming/streaming-handler.js +8 -1
  40. package/dist/agent/tool-executor.js +1 -0
  41. package/dist/agent/tool-handler.d.ts +5 -0
  42. package/dist/agent/tool-handler.js +130 -4
  43. package/dist/browser-automation/browser-operator-executor.js +15 -0
  44. package/dist/browser-automation/browser-use-runner.d.ts +96 -0
  45. package/dist/browser-automation/browser-use-runner.js +492 -0
  46. package/dist/browser-automation/camofox-runner.d.ts +107 -0
  47. package/dist/browser-automation/camofox-runner.js +287 -0
  48. package/dist/channels/dingtalk/index.js +3 -0
  49. package/dist/channels/discord/client.d.ts +9 -0
  50. package/dist/channels/discord/client.js +12 -0
  51. package/dist/channels/feishu/index.d.ts +167 -1
  52. package/dist/channels/feishu/index.js +432 -7
  53. package/dist/channels/gateway-lifecycle.d.ts +132 -0
  54. package/dist/channels/gateway-lifecycle.js +219 -0
  55. package/dist/channels/google-chat/index.d.ts +5 -1
  56. package/dist/channels/google-chat/index.js +5 -1
  57. package/dist/channels/imessage/index.d.ts +14 -0
  58. package/dist/channels/imessage/index.js +64 -17
  59. package/dist/channels/index.d.ts +4 -0
  60. package/dist/channels/index.js +4 -0
  61. package/dist/channels/irc/index.d.ts +73 -3
  62. package/dist/channels/irc/index.js +446 -11
  63. package/dist/channels/line/index.js +3 -0
  64. package/dist/channels/mattermost/index.d.ts +51 -4
  65. package/dist/channels/mattermost/index.js +303 -20
  66. package/dist/channels/nextcloud-talk/index.d.ts +89 -8
  67. package/dist/channels/nextcloud-talk/index.js +367 -16
  68. package/dist/channels/nostr/index.d.ts +121 -1
  69. package/dist/channels/nostr/index.js +396 -8
  70. package/dist/channels/ntfy/index.js +3 -0
  71. package/dist/channels/qq/index.js +3 -0
  72. package/dist/channels/slash-parity.d.ts +117 -0
  73. package/dist/channels/slash-parity.js +185 -0
  74. package/dist/channels/synology-chat/index.js +3 -0
  75. package/dist/channels/teams/index.d.ts +3 -0
  76. package/dist/channels/teams/index.js +3 -0
  77. package/dist/channels/telegram/client.d.ts +9 -0
  78. package/dist/channels/telegram/client.js +12 -0
  79. package/dist/channels/twilio-voice/index.js +3 -0
  80. package/dist/channels/webchat/index.d.ts +7 -1
  81. package/dist/channels/webchat/index.js +7 -1
  82. package/dist/channels/wecom/index.js +3 -0
  83. package/dist/channels/weixin/index.js +3 -0
  84. package/dist/channels/zalo/index.js +3 -0
  85. package/dist/cli/config-loader.js +1 -1
  86. package/dist/codebuddy/client.d.ts +30 -1
  87. package/dist/codebuddy/client.js +167 -20
  88. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
  89. package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
  90. package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
  91. package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
  92. package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
  93. package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
  94. package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
  95. package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
  96. package/dist/codebuddy/tool-definitions/index.js +2 -1
  97. package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
  98. package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
  99. package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
  100. package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
  101. package/dist/codebuddy/tools.d.ts +1 -1
  102. package/dist/codebuddy/tools.js +27 -24
  103. package/dist/commands/cli/hermes-commands.js +108 -0
  104. package/dist/commands/cli/native-engine-commands.js +107 -3
  105. package/dist/commands/cli/utility-commands.js +45 -0
  106. package/dist/commands/enhanced-command-handler.js +5 -0
  107. package/dist/commands/goal-cli.d.ts +71 -0
  108. package/dist/commands/goal-cli.js +280 -0
  109. package/dist/commands/handlers/goal-handler.d.ts +34 -0
  110. package/dist/commands/handlers/goal-handler.js +155 -0
  111. package/dist/commands/handlers/index.d.ts +1 -0
  112. package/dist/commands/handlers/index.js +2 -0
  113. package/dist/commands/handlers/infra-handlers.js +36 -0
  114. package/dist/commands/handlers/memory-handlers.js +96 -1
  115. package/dist/commands/headless-slash.d.ts +2 -0
  116. package/dist/commands/headless-slash.js +12 -0
  117. package/dist/commands/llm-provider-resolution.d.ts +5 -3
  118. package/dist/commands/llm-provider-resolution.js +87 -33
  119. package/dist/commands/ollama.d.ts +25 -0
  120. package/dist/commands/ollama.js +100 -0
  121. package/dist/commands/provider.d.ts +5 -0
  122. package/dist/commands/provider.js +106 -55
  123. package/dist/commands/slash/builtin-commands.js +20 -0
  124. package/dist/commands/spec-next.js +2 -1
  125. package/dist/commands/spec-plan.js +5 -16
  126. package/dist/commands/tunnel.d.ts +2 -0
  127. package/dist/commands/tunnel.js +48 -0
  128. package/dist/config/config-resolver.d.ts +2 -1
  129. package/dist/config/config-resolver.js +54 -41
  130. package/dist/config/constants.d.ts +28 -0
  131. package/dist/config/constants.js +7 -0
  132. package/dist/config/env-schema.js +770 -0
  133. package/dist/config/feature-flags.js +7 -0
  134. package/dist/config/model-tools.js +4 -4
  135. package/dist/config/toml-config.d.ts +16 -0
  136. package/dist/config/toml-config.js +3 -0
  137. package/dist/context/context-manager-v2.d.ts +39 -0
  138. package/dist/context/context-manager-v2.js +91 -0
  139. package/dist/daemon/agent-task-executor.js +12 -1
  140. package/dist/daemon/autonomous-daemon.d.ts +1 -1
  141. package/dist/daemon/autonomous-daemon.js +5 -3
  142. package/dist/daemon/autonomous-loop.d.ts +21 -1
  143. package/dist/daemon/autonomous-loop.js +69 -0
  144. package/dist/daemon/colab-goal.d.ts +38 -0
  145. package/dist/daemon/colab-goal.js +81 -0
  146. package/dist/daemon/cron-agent-bridge.d.ts +12 -3
  147. package/dist/daemon/cron-agent-bridge.js +25 -9
  148. package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
  149. package/dist/desktop/codebuddy-engine-adapter.js +257 -80
  150. package/dist/desktop/engine-adapter.d.ts +14 -0
  151. package/dist/desktop-automation/automation-manager.js +16 -0
  152. package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
  153. package/dist/desktop-automation/omniparser-runner.js +115 -0
  154. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  155. package/dist/desktop-automation/smart-snapshot.js +72 -5
  156. package/dist/doctor/index.js +40 -23
  157. package/dist/events/types.d.ts +11 -0
  158. package/dist/fleet/colab-store.d.ts +21 -0
  159. package/dist/fleet/colab-store.js +28 -0
  160. package/dist/fleet/peer-session-bridge.d.ts +1 -1
  161. package/dist/fleet/peer-session-bridge.js +243 -2
  162. package/dist/fleet/peer-session-store.d.ts +3 -0
  163. package/dist/fleet/peer-tool-bridge.js +14 -0
  164. package/dist/fleet/privacy-lint.d.ts +8 -0
  165. package/dist/fleet/privacy-lint.js +22 -0
  166. package/dist/goals/goal-decomposer.d.ts +27 -0
  167. package/dist/goals/goal-decomposer.js +293 -0
  168. package/dist/goals/goal-judge-client.d.ts +15 -0
  169. package/dist/goals/goal-judge-client.js +45 -0
  170. package/dist/goals/goal-judge.d.ts +38 -0
  171. package/dist/goals/goal-judge.js +158 -0
  172. package/dist/goals/goal-loop.d.ts +37 -0
  173. package/dist/goals/goal-loop.js +101 -0
  174. package/dist/goals/goal-manager.d.ts +77 -0
  175. package/dist/goals/goal-manager.js +289 -0
  176. package/dist/goals/goal-state.d.ts +99 -0
  177. package/dist/goals/goal-state.js +315 -0
  178. package/dist/goals/goal-store.d.ts +28 -0
  179. package/dist/goals/goal-store.js +87 -0
  180. package/dist/goals/index.d.ts +5 -0
  181. package/dist/goals/index.js +6 -0
  182. package/dist/hooks/use-input-handler.js +36 -1
  183. package/dist/hooks/user-hooks.js +17 -3
  184. package/dist/index.js +506 -25
  185. package/dist/input/text-to-speech.d.ts +2 -6
  186. package/dist/input/text-to-speech.js +2 -27
  187. package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
  188. package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
  189. package/dist/integrations/tailscale.d.ts +13 -0
  190. package/dist/integrations/tailscale.js +87 -34
  191. package/dist/knowledge/workspace-indexer.js +53 -9
  192. package/dist/mcp/client.js +1 -0
  193. package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
  194. package/dist/mcp/mcp-desktop-tools.js +158 -0
  195. package/dist/mcp/mcp-server.js +2 -0
  196. package/dist/memory/enhanced-memory.d.ts +10 -0
  197. package/dist/memory/enhanced-memory.js +33 -2
  198. package/dist/memory/index.d.ts +3 -1
  199. package/dist/memory/index.js +4 -1
  200. package/dist/memory/memory-auto-proposer.d.ts +23 -0
  201. package/dist/memory/memory-auto-proposer.js +308 -0
  202. package/dist/memory/memory-candidate-queue.d.ts +92 -0
  203. package/dist/memory/memory-candidate-queue.js +261 -0
  204. package/dist/memory/persistent-memory.d.ts +45 -2
  205. package/dist/memory/persistent-memory.js +236 -40
  206. package/dist/ml/bayesian-qualifier.d.ts +1 -1
  207. package/dist/ml/bayesian-qualifier.js +35 -2
  208. package/dist/observability/run-store.d.ts +1 -1
  209. package/dist/openclaw/gateway-bridge.js +5 -0
  210. package/dist/plugins/marketplace.d.ts +1 -0
  211. package/dist/plugins/marketplace.js +7 -0
  212. package/dist/prompts/execution-discipline.d.ts +14 -0
  213. package/dist/prompts/execution-discipline.js +29 -0
  214. package/dist/prompts/variation-injector.js +9 -2
  215. package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
  216. package/dist/protocols/acp/acp-agentic-runner.js +115 -18
  217. package/dist/protocols/acp/acp-session-store.d.ts +23 -0
  218. package/dist/protocols/acp/acp-session-store.js +77 -0
  219. package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
  220. package/dist/protocols/acp/acp-stdio-server.js +71 -16
  221. package/dist/providers/active-llm-registry.d.ts +37 -0
  222. package/dist/providers/active-llm-registry.js +186 -0
  223. package/dist/providers/auxiliary-provider.d.ts +25 -0
  224. package/dist/providers/auxiliary-provider.js +192 -0
  225. package/dist/providers/codex-oauth.d.ts +1 -1
  226. package/dist/providers/codex-oauth.js +27 -4
  227. package/dist/providers/index.d.ts +3 -0
  228. package/dist/providers/index.js +4 -0
  229. package/dist/providers/provider-catalog.d.ts +70 -0
  230. package/dist/providers/provider-catalog.js +738 -0
  231. package/dist/providers/provider-fallback.d.ts +35 -0
  232. package/dist/providers/provider-fallback.js +171 -0
  233. package/dist/providers/xai-oauth.d.ts +128 -0
  234. package/dist/providers/xai-oauth.js +735 -0
  235. package/dist/scheduler/cron-scheduler.d.ts +15 -3
  236. package/dist/scheduler/cron-scheduler.js +53 -7
  237. package/dist/scheduler/script-runner.d.ts +2 -0
  238. package/dist/scheduler/script-runner.js +19 -4
  239. package/dist/scheduler/watchdog-handlers.js +7 -20
  240. package/dist/search/usearch-index.js +7 -2
  241. package/dist/security/tool-policy/tool-groups.js +2 -0
  242. package/dist/server/index.js +17 -2
  243. package/dist/server/routes/mobile.d.ts +12 -4
  244. package/dist/server/routes/mobile.js +116 -1
  245. package/dist/server/tls-config.d.ts +35 -0
  246. package/dist/server/tls-config.js +142 -0
  247. package/dist/server/tunnel-manager.d.ts +20 -0
  248. package/dist/server/tunnel-manager.js +58 -0
  249. package/dist/server/websocket/fleet-bridge.d.ts +13 -1
  250. package/dist/server/websocket/fleet-bridge.js +16 -0
  251. package/dist/services/prompt-builder.d.ts +1 -0
  252. package/dist/services/prompt-builder.js +49 -8
  253. package/dist/shared/engine-types.d.ts +15 -1
  254. package/dist/sidecar/sidecar-bridge.d.ts +1 -0
  255. package/dist/sidecar/sidecar-bridge.js +21 -10
  256. package/dist/spec/spec-store.js +8 -1
  257. package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
  258. package/dist/talk-mode/providers/audioreader-tts.js +8 -8
  259. package/dist/templates/project-scaffolding.js +3 -2
  260. package/dist/themes/theme-schema.d.ts +10 -10
  261. package/dist/tools/application-profiles.js +38 -0
  262. package/dist/tools/bash/bash-tool.d.ts +1 -0
  263. package/dist/tools/bash/bash-tool.js +16 -2
  264. package/dist/tools/bash/command-validator.js +3 -0
  265. package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
  266. package/dist/tools/bash/rtk-rewrite.js +100 -0
  267. package/dist/tools/bash/streaming-executor.js +14 -1
  268. package/dist/tools/computer-control-tool.d.ts +16 -1
  269. package/dist/tools/computer-control-tool.js +317 -5
  270. package/dist/tools/document-generator.d.ts +14 -0
  271. package/dist/tools/document-generator.js +79 -0
  272. package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
  273. package/dist/tools/execute-code-rpc-invoker.js +46 -5
  274. package/dist/tools/metadata.js +28 -0
  275. package/dist/tools/office-macro-tool.d.ts +10 -0
  276. package/dist/tools/office-macro-tool.js +93 -0
  277. package/dist/tools/registry/index.d.ts +2 -1
  278. package/dist/tools/registry/index.js +2 -1
  279. package/dist/tools/registry/memory-tools.d.ts +20 -0
  280. package/dist/tools/registry/memory-tools.js +225 -3
  281. package/dist/tools/registry/multimodal-tools.js +2 -2
  282. package/dist/tools/registry/vision-tools.d.ts +33 -0
  283. package/dist/tools/registry/vision-tools.js +308 -6
  284. package/dist/tools/registry/windows-tools.d.ts +2 -0
  285. package/dist/tools/registry/windows-tools.js +47 -0
  286. package/dist/tools/route-peer-tool.js +14 -0
  287. package/dist/tools/screenshot-tool.js +14 -2
  288. package/dist/tools/text-to-speech-tool.d.ts +1 -1
  289. package/dist/tools/text-to-speech-tool.js +2 -12
  290. package/dist/tools/tool-selector.js +3 -59
  291. package/dist/utils/config-validation/schema.d.ts +40 -7
  292. package/dist/utils/config-validation/schema.js +130 -1
  293. package/dist/utils/cost-tracker.js +21 -1
  294. package/dist/utils/disk-guard.d.ts +180 -0
  295. package/dist/utils/disk-guard.js +404 -0
  296. package/dist/utils/interactive-setup.js +35 -2
  297. package/dist/utils/model-utils.js +11 -2
  298. package/dist/utils/output-sanitizer.js +11 -0
  299. package/dist/utils/provider-detector.d.ts +8 -7
  300. package/dist/utils/provider-detector.js +19 -77
  301. package/dist/utils/settings-manager.d.ts +7 -0
  302. package/dist/utils/token-display.d.ts +3 -3
  303. package/dist/utils/token-display.js +22 -4
  304. package/dist/wizard/onboarding.d.ts +39 -0
  305. package/dist/wizard/onboarding.js +433 -21
  306. package/dist/wizard/provider-onboarding.d.ts +8 -23
  307. package/dist/wizard/provider-onboarding.js +39 -158
  308. package/package.json +4 -2
  309. package/dist/plugins/provider-onboarding.d.ts +0 -23
  310. package/dist/plugins/provider-onboarding.js +0 -116
  311. package/dist/utils/kokoro-tts.d.ts +0 -8
  312. package/dist/utils/kokoro-tts.js +0 -67
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Per-Platform Slash-Command Parity Manifest
3
+ *
4
+ * Defines the expected slash commands per messaging platform and
5
+ * compares them against the actual registered commands per adapter.
6
+ * Returns a machine-readable readiness report for each platform.
7
+ */
8
+ import { getChannelManager } from './core.js';
9
+ // ============================================================================
10
+ // Expected Commands Manifest
11
+ // ============================================================================
12
+ /**
13
+ * The canonical set of slash commands expected on each platform.
14
+ *
15
+ * These represent the Code Buddy bot commands that users should be
16
+ * able to invoke from each messaging platform. The manifest is
17
+ * intentionally conservative — platforms may support additional
18
+ * commands beyond what is listed here.
19
+ */
20
+ export const EXPECTED_SLASH_COMMANDS = {
21
+ discord: [
22
+ { name: 'ask', description: 'Ask Code Buddy a question' },
23
+ { name: 'status', description: 'Show bot and channel status' },
24
+ { name: 'clear', description: 'Clear conversation history' },
25
+ { name: 'help', description: 'Show available commands' },
26
+ { name: 'model', description: 'Switch or show current model' },
27
+ { name: 'think', description: 'Set reasoning depth', required: false },
28
+ { name: 'compact', description: 'Compact conversation context', required: false },
29
+ { name: 'repo', description: 'Show repository info', required: false },
30
+ ],
31
+ telegram: [
32
+ { name: 'ask', description: 'Ask Code Buddy a question' },
33
+ { name: 'status', description: 'Show bot and channel status' },
34
+ { name: 'clear', description: 'Clear conversation history' },
35
+ { name: 'help', description: 'Show available commands' },
36
+ { name: 'model', description: 'Switch or show current model' },
37
+ { name: 'yolo', description: 'Toggle YOLO mode', required: false },
38
+ { name: 'repo', description: 'Show repository info', required: false },
39
+ { name: 'branch', description: 'Show branch info', required: false },
40
+ ],
41
+ slack: [
42
+ { name: 'ask', description: 'Ask Code Buddy a question' },
43
+ { name: 'status', description: 'Show bot and channel status' },
44
+ { name: 'clear', description: 'Clear conversation history' },
45
+ { name: 'help', description: 'Show available commands' },
46
+ { name: 'model', description: 'Switch or show current model' },
47
+ { name: 'compact', description: 'Compact conversation context', required: false },
48
+ { name: 'think', description: 'Set reasoning depth', required: false },
49
+ ],
50
+ matrix: [
51
+ { name: 'ask', description: 'Ask Code Buddy a question' },
52
+ { name: 'status', description: 'Show bot and channel status' },
53
+ { name: 'clear', description: 'Clear conversation history' },
54
+ { name: 'help', description: 'Show available commands' },
55
+ { name: 'model', description: 'Switch or show current model', required: false },
56
+ ],
57
+ };
58
+ /**
59
+ * Check if a channel adapter implements SlashCommandProvider.
60
+ */
61
+ function isSlashCommandProvider(channel) {
62
+ return typeof channel.getRegisteredCommands === 'function';
63
+ }
64
+ /**
65
+ * Extract the actual registered commands from a channel adapter.
66
+ *
67
+ * Uses the SlashCommandProvider interface if available, otherwise
68
+ * returns an empty array (no commands can be detected).
69
+ */
70
+ export function extractActualCommands(channel) {
71
+ if (isSlashCommandProvider(channel)) {
72
+ return channel.getRegisteredCommands();
73
+ }
74
+ return [];
75
+ }
76
+ // ============================================================================
77
+ // Parity Checking
78
+ // ============================================================================
79
+ /**
80
+ * Build a parity report for a single platform.
81
+ *
82
+ * @param platform - The platform channel type
83
+ * @param expectedCommands - The expected slash commands
84
+ * @param actualCommands - The actual registered commands
85
+ * @param adapterRegistered - Whether the adapter is in the ChannelManager
86
+ */
87
+ export function buildPlatformParityReport(platform, expectedCommands, actualCommands, adapterRegistered) {
88
+ if (!adapterRegistered) {
89
+ return {
90
+ platform,
91
+ adapterRegistered: false,
92
+ expectedCount: expectedCommands.length,
93
+ presentCount: 0,
94
+ missingRequiredCount: expectedCommands.filter((c) => c.required !== false).length,
95
+ missingOptionalCount: expectedCommands.filter((c) => c.required === false).length,
96
+ commands: expectedCommands.map((cmd) => ({
97
+ name: cmd.name,
98
+ present: false,
99
+ required: cmd.required !== false,
100
+ })),
101
+ status: 'no-adapter',
102
+ };
103
+ }
104
+ const actualSet = new Set(actualCommands.map((c) => c.toLowerCase()));
105
+ const commands = expectedCommands.map((cmd) => ({
106
+ name: cmd.name,
107
+ present: actualSet.has(cmd.name.toLowerCase()),
108
+ required: cmd.required !== false,
109
+ }));
110
+ const presentCount = commands.filter((c) => c.present).length;
111
+ const missingRequiredCount = commands.filter((c) => !c.present && c.required).length;
112
+ const missingOptionalCount = commands.filter((c) => !c.present && !c.required).length;
113
+ let status;
114
+ if (presentCount === expectedCommands.length) {
115
+ status = 'full';
116
+ }
117
+ else if (presentCount > 0) {
118
+ status = 'partial';
119
+ }
120
+ else {
121
+ status = 'none';
122
+ }
123
+ return {
124
+ platform,
125
+ adapterRegistered: true,
126
+ expectedCount: expectedCommands.length,
127
+ presentCount,
128
+ missingRequiredCount,
129
+ missingOptionalCount,
130
+ commands,
131
+ status,
132
+ };
133
+ }
134
+ /**
135
+ * Build the full slash-command parity manifest across all tracked platforms.
136
+ *
137
+ * @param manager - Optional ChannelManager instance (defaults to singleton)
138
+ */
139
+ export function buildSlashParityManifest(manager) {
140
+ const mgr = manager ?? getChannelManager();
141
+ const reports = [];
142
+ for (const [platformKey, expectedCmds] of Object.entries(EXPECTED_SLASH_COMMANDS)) {
143
+ const platform = platformKey;
144
+ const channel = mgr.getChannel(platform);
145
+ const adapterRegistered = !!channel;
146
+ const actualCommands = channel ? extractActualCommands(channel) : [];
147
+ reports.push(buildPlatformParityReport(platform, expectedCmds, actualCommands, adapterRegistered));
148
+ }
149
+ const fullParityCount = reports.filter((r) => r.status === 'full').length;
150
+ const partialParityCount = reports.filter((r) => r.status === 'partial' || r.status === 'none').length;
151
+ const noAdapterCount = reports.filter((r) => r.status === 'no-adapter').length;
152
+ return {
153
+ generatedAt: new Date().toISOString(),
154
+ ok: reports.every((r) => r.missingRequiredCount === 0),
155
+ totalPlatforms: reports.length,
156
+ fullParityCount,
157
+ partialParityCount,
158
+ noAdapterCount,
159
+ platforms: reports,
160
+ };
161
+ }
162
+ /**
163
+ * Render the parity manifest as a human-readable text report.
164
+ */
165
+ export function renderSlashParityManifest(manifest) {
166
+ const lines = [
167
+ 'Slash-command parity report:',
168
+ ` Status: ${manifest.ok ? 'ok' : 'needs attention'}`,
169
+ ` Platforms: ${manifest.totalPlatforms} (${manifest.fullParityCount} full, ${manifest.partialParityCount} partial, ${manifest.noAdapterCount} no-adapter)`,
170
+ '',
171
+ ];
172
+ for (const report of manifest.platforms) {
173
+ const missing = report.commands.filter((c) => !c.present);
174
+ const present = report.commands.filter((c) => c.present);
175
+ lines.push(` ${report.platform}: ${report.status} (${report.presentCount}/${report.expectedCount})`);
176
+ if (present.length > 0) {
177
+ lines.push(` Present: ${present.map((c) => c.name).join(', ')}`);
178
+ }
179
+ if (missing.length > 0) {
180
+ lines.push(` Missing: ${missing.map((c) => `${c.name}${c.required ? ' (required)' : ''}`).join(', ')}`);
181
+ }
182
+ }
183
+ return lines.join('\n');
184
+ }
185
+ //# sourceMappingURL=slash-parity.js.map
@@ -65,6 +65,9 @@ export class SynologyChatChannel extends BaseChannel {
65
65
  constructor(config) {
66
66
  super('synology-chat', config);
67
67
  }
68
+ // REST/webhook adapter — outbound via incoming webhooks, inbound via
69
+ // outgoing-webhook callbacks; no persistent connection is held open, so
70
+ // reconnection (ReconnectionManager) is N/A.
68
71
  async connect() {
69
72
  const cfg = this.config;
70
73
  this.adapter = new SynologyChatAdapter({
@@ -129,6 +129,9 @@ export declare class TeamsChannel extends BaseChannel {
129
129
  /**
130
130
  * Connect to Teams by validating the app credentials.
131
131
  * The actual message receiving happens via webhooks.
132
+ *
133
+ * REST/webhook adapter — no persistent connection is held open here, so
134
+ * reconnection (ReconnectionManager) is N/A.
132
135
  */
133
136
  connect(): Promise<void>;
134
137
  /**
@@ -111,6 +111,9 @@ export class TeamsChannel extends BaseChannel {
111
111
  /**
112
112
  * Connect to Teams by validating the app credentials.
113
113
  * The actual message receiving happens via webhooks.
114
+ *
115
+ * REST/webhook adapter — no persistent connection is held open here, so
116
+ * reconnection (ReconnectionManager) is N/A.
114
117
  */
115
118
  async connect() {
116
119
  try {
@@ -58,6 +58,15 @@ export declare class TelegramChannel extends BaseChannel {
58
58
  * Set bot commands
59
59
  */
60
60
  private setCommands;
61
+ /**
62
+ * Return the bot command names registered with this adapter.
63
+ *
64
+ * Implements {@link SlashCommandProvider} so the slash-parity checker can
65
+ * report real parity instead of falling back to an empty list. Names come
66
+ * from the configured `commands` (the same set pushed to Telegram via
67
+ * {@link setCommands}), normalized without a leading slash.
68
+ */
69
+ getRegisteredCommands(): string[];
61
70
  /**
62
71
  * Start polling for updates
63
72
  */
@@ -294,6 +294,18 @@ export class TelegramChannel extends BaseChannel {
294
294
  }));
295
295
  await this.apiRequest('setMyCommands', { commands });
296
296
  }
297
+ /**
298
+ * Return the bot command names registered with this adapter.
299
+ *
300
+ * Implements {@link SlashCommandProvider} so the slash-parity checker can
301
+ * report real parity instead of falling back to an empty list. Names come
302
+ * from the configured `commands` (the same set pushed to Telegram via
303
+ * {@link setCommands}), normalized without a leading slash.
304
+ */
305
+ getRegisteredCommands() {
306
+ const commands = this.telegramConfig.commands ?? [];
307
+ return commands.map((cmd) => cmd.command.replace(/^\//, ''));
308
+ }
297
309
  /**
298
310
  * Start polling for updates
299
311
  */
@@ -94,6 +94,9 @@ export class TwilioVoiceChannel extends BaseChannel {
94
94
  webhookUrl: config.webhookUrl,
95
95
  });
96
96
  }
97
+ // REST/webhook adapter — calls are placed via the Twilio REST API and
98
+ // driven by TwiML webhook callbacks; no persistent connection is held open,
99
+ // so reconnection (ReconnectionManager) is N/A.
97
100
  async connect() {
98
101
  await this.adapter.start();
99
102
  this.status.connected = true;
@@ -40,7 +40,13 @@ export declare class WebChatChannel extends BaseChannel {
40
40
  constructor(config: WebChatConfig);
41
41
  private get webChatConfig();
42
42
  /**
43
- * Start the HTTP and WebSocket servers
43
+ * Start the HTTP and WebSocket servers.
44
+ *
45
+ * Server-side adapter — this hosts a WebSocket *server* that browsers
46
+ * connect *into*; it does not dial out to a remote endpoint that could
47
+ * drop, so there is nothing to reconnect to and ReconnectionManager is N/A.
48
+ * (Per-client socket drops are handled by the connections map, not by
49
+ * re-establishing an outbound link.)
44
50
  */
45
51
  connect(): Promise<void>;
46
52
  /**
@@ -46,7 +46,13 @@ export class WebChatChannel extends BaseChannel {
46
46
  // Lifecycle
47
47
  // ==========================================================================
48
48
  /**
49
- * Start the HTTP and WebSocket servers
49
+ * Start the HTTP and WebSocket servers.
50
+ *
51
+ * Server-side adapter — this hosts a WebSocket *server* that browsers
52
+ * connect *into*; it does not dial out to a remote endpoint that could
53
+ * drop, so there is nothing to reconnect to and ReconnectionManager is N/A.
54
+ * (Per-client socket drops are handled by the connections map, not by
55
+ * re-establishing an outbound link.)
50
56
  */
51
57
  async connect() {
52
58
  let WebSocketModule;
@@ -97,6 +97,9 @@ export class WeComChannel extends BaseChannel {
97
97
  mentionedMobileList: config.mentionedMobileList,
98
98
  });
99
99
  }
100
+ // REST/webhook adapter — outbound messages are one-shot HTTPS POSTs to a
101
+ // WeCom group-robot webhook; no persistent connection is held open, so
102
+ // reconnection (ReconnectionManager) is N/A.
100
103
  async connect() {
101
104
  await this.adapter.start();
102
105
  this.status.connected = true;
@@ -93,6 +93,9 @@ export class WeixinChannel extends BaseChannel {
93
93
  kfAccount: config.kfAccount,
94
94
  });
95
95
  }
96
+ // REST/webhook adapter — outbound via the WeChat (Weixin) HTTP API, inbound
97
+ // via webhook callbacks; no persistent connection is held open, so
98
+ // reconnection (ReconnectionManager) is N/A.
96
99
  async connect() {
97
100
  await this.adapter.start();
98
101
  this.status.connected = true;
@@ -70,6 +70,9 @@ export class ZaloChannel extends BaseChannel {
70
70
  mode: config.mode,
71
71
  });
72
72
  }
73
+ // REST/webhook adapter — outbound via the Zalo API, inbound via webhook
74
+ // callbacks; no persistent connection is held open, so reconnection
75
+ // (ReconnectionManager) is N/A.
73
76
  async connect() {
74
77
  await this.adapter.start();
75
78
  this.status.connected = true;
@@ -91,7 +91,7 @@ export async function saveCommandLineSettings(apiKey, baseURL) {
91
91
  export function validateConfig(config) {
92
92
  const errors = [];
93
93
  if (!config.apiKey) {
94
- errors.push('API key required. Set GROK_API_KEY environment variable, use --api-key flag, or save to ~/.codebuddy/user-settings.json');
94
+ errors.push('API key required. Sign in free with `buddy login` (ChatGPT plan), set GROK_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / GOOGLE_API_KEY, or use --api-key. Run `buddy doctor` to check your setup.');
95
95
  }
96
96
  return {
97
97
  valid: errors.length === 0,
@@ -1,5 +1,6 @@
1
1
  import type { ChatCompletionMessageParam, ChatCompletionChunk, ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming } from "openai/resources/chat";
2
2
  import type { CircuitBreakerConfig } from "../providers/circuit-breaker.js";
3
+ import { type RuntimeFallbackProvider } from "../providers/provider-fallback.js";
3
4
  export { hasToolCalls } from './message-guards.js';
4
5
  export type { CodeBuddyMessageWithToolCalls } from './message-guards.js';
5
6
  /** Sentinel apiKey for the ChatGPT OAuth path — auto-detect chain in
@@ -78,6 +79,8 @@ export type GeminiThinkingLevel = 'minimal' | 'low' | 'medium' | 'high';
78
79
  export interface ChatOptions {
79
80
  model?: string;
80
81
  temperature?: number;
82
+ /** Optional per-call output-token cap. Providers fall back to their model default when omitted. */
83
+ maxTokens?: number;
81
84
  searchOptions?: SearchOptions;
82
85
  /** Optional request timeout override (ms) for Gemini native API calls */
83
86
  timeoutMs?: number;
@@ -95,6 +98,8 @@ export interface ChatOptions {
95
98
  responseFormat?: 'text' | 'json';
96
99
  /** tool_choice override for this request */
97
100
  tool_choice?: 'auto' | 'none' | 'required';
101
+ /** Disable Hermes-style cross-provider fallback for this one request. */
102
+ disableProviderFallback?: boolean;
98
103
  /**
99
104
  * Enable Gemini's native server-side Google Search grounding for this
100
105
  * request. Only affects the GeminiNativeProvider — ignored by the
@@ -129,6 +134,16 @@ export interface ChatOptions {
129
134
  maxDelayMs?: number;
130
135
  };
131
136
  }
137
+ export interface CodeBuddyClientOptions {
138
+ /** Enable env-configured fallback providers. Default: true. */
139
+ enableFallbacks?: boolean;
140
+ /** Enable same-provider auth profile rotation before cross-provider fallback. Default: true. */
141
+ enableCredentialPool?: boolean;
142
+ /** Explicit same-provider credential pool candidates, mainly for tests and controlled embedding. */
143
+ credentialPoolProviders?: RuntimeFallbackProvider[];
144
+ /** Explicit fallback providers, mainly for tests and controlled embedding. */
145
+ fallbackProviders?: RuntimeFallbackProvider[];
146
+ }
132
147
  export interface CodeBuddyResponse {
133
148
  choices: Array<{
134
149
  message: {
@@ -168,6 +183,10 @@ export declare class CodeBuddyClient {
168
183
  private geminiCliProvider;
169
184
  /** True when routed through the Gemini CLI subprocess. */
170
185
  private isGeminiCliProvider;
186
+ /** Same-provider auth profile candidates, tried before cross-provider fallback. */
187
+ private credentialPoolProviders;
188
+ /** Hermes-style cross-provider fallback candidates, tried per failed chat turn. */
189
+ private fallbackProviders;
171
190
  /**
172
191
  * Configure the circuit breaker for this client.
173
192
  * Once configured, calls with `circuitBreaker: true` in ChatOptions
@@ -184,8 +203,14 @@ export declare class CodeBuddyClient {
184
203
  */
185
204
  setDefaultGoogleSearch(enabled: boolean): void;
186
205
  setCircuitBreakerConfig(config: Partial<CircuitBreakerConfig>): void;
206
+ /**
207
+ * Override the cross-provider fallback list after construction. Used by the
208
+ * active-LLM registry to auto-populate failover from the user's live logins.
209
+ * `chatWithProviderFallback` reads this per-call, so it takes effect immediately.
210
+ */
211
+ setRuntimeFallbackProviders(providers: RuntimeFallbackProvider[]): void;
187
212
  private static isGeminiModelName;
188
- constructor(apiKey: string, model?: string, baseURL?: string);
213
+ constructor(apiKey: string, model?: string, baseURL?: string, options?: CodeBuddyClientOptions);
189
214
  /**
190
215
  * Probe the model to check if it supports function calling
191
216
  * Makes a quick test request with a simple tool
@@ -226,6 +251,10 @@ export declare class CodeBuddyClient {
226
251
  */
227
252
  isGemini(): boolean;
228
253
  chat(messages: CodeBuddyMessage[], tools?: CodeBuddyTool[], options?: string | ChatOptions, searchOptions?: SearchOptions): Promise<CodeBuddyResponse>;
254
+ private dispatchChat;
255
+ private chatWithProviderFallback;
229
256
  chatStream(messages: CodeBuddyMessage[], tools?: CodeBuddyTool[], options?: string | ChatOptions, searchOptions?: SearchOptions): AsyncGenerator<ChatCompletionChunk, void, unknown>;
257
+ private dispatchChatStream;
258
+ private chatStreamWithProviderFallback;
230
259
  search(query: string, searchParameters?: SearchParameters): Promise<CodeBuddyResponse>;
231
260
  }
@@ -7,6 +7,7 @@ import { OpenAICompatProvider } from "./providers/provider-openai-compat.js";
7
7
  import { ChatGptResponsesProvider } from "./providers/provider-chatgpt-responses.js";
8
8
  import { GeminiCliProvider } from "./providers/provider-gemini-cli.js";
9
9
  import { withStreamRetry } from "./stream-retry.js";
10
+ import { recordRuntimeFallbackFailure, recordRuntimeFallbackSuccess, resolveRuntimeCredentialPoolProviders, resolveRuntimeFallbackProviders, } from "../providers/provider-fallback.js";
10
11
  export { hasToolCalls } from './message-guards.js';
11
12
  /** Sentinel apiKey for the ChatGPT OAuth path — auto-detect chain in
12
13
  * `src/index.ts` passes this when `~/.codebuddy/codex-auth.json` exists. */
@@ -41,6 +42,10 @@ export class CodeBuddyClient {
41
42
  geminiCliProvider = null;
42
43
  /** True when routed through the Gemini CLI subprocess. */
43
44
  isGeminiCliProvider = false;
45
+ /** Same-provider auth profile candidates, tried before cross-provider fallback. */
46
+ credentialPoolProviders = [];
47
+ /** Hermes-style cross-provider fallback candidates, tried per failed chat turn. */
48
+ fallbackProviders = [];
44
49
  /**
45
50
  * Configure the circuit breaker for this client.
46
51
  * Once configured, calls with `circuitBreaker: true` in ChatOptions
@@ -68,10 +73,18 @@ export class CodeBuddyClient {
68
73
  this.circuitBreakerConfig = config;
69
74
  // Provider reads via getter at call-time, so no propagation needed.
70
75
  }
76
+ /**
77
+ * Override the cross-provider fallback list after construction. Used by the
78
+ * active-LLM registry to auto-populate failover from the user's live logins.
79
+ * `chatWithProviderFallback` reads this per-call, so it takes effect immediately.
80
+ */
81
+ setRuntimeFallbackProviders(providers) {
82
+ this.fallbackProviders = providers;
83
+ }
71
84
  static isGeminiModelName(model) {
72
85
  return model.toLowerCase().includes('gemini');
73
86
  }
74
- constructor(apiKey, model, baseURL) {
87
+ constructor(apiKey, model, baseURL, options = {}) {
75
88
  // Validate API key
76
89
  if (!apiKey || typeof apiKey !== 'string') {
77
90
  throw new Error('API key is required and must be a non-empty string');
@@ -198,6 +211,24 @@ export class CodeBuddyClient {
198
211
  logger.warn(`Model '${model}' is not officially supported. Using default token limits.`);
199
212
  }
200
213
  }
214
+ if (options.enableFallbacks !== false) {
215
+ if (options.enableCredentialPool !== false) {
216
+ this.credentialPoolProviders = options.credentialPoolProviders ?? resolveRuntimeCredentialPoolProviders({
217
+ active: {
218
+ apiKey: this.apiKey,
219
+ baseURL: this.baseURL,
220
+ model: this.currentModel,
221
+ },
222
+ });
223
+ }
224
+ this.fallbackProviders = options.fallbackProviders ?? resolveRuntimeFallbackProviders({
225
+ active: {
226
+ apiKey: this.apiKey,
227
+ baseURL: this.baseURL,
228
+ model: this.currentModel,
229
+ },
230
+ });
231
+ }
201
232
  }
202
233
  /**
203
234
  * Probe the model to check if it supports function calling
@@ -323,6 +354,14 @@ export class CodeBuddyClient {
323
354
  ? { model: options, searchOptions }
324
355
  : options || {};
325
356
  // Dispatch to the active strategy.
357
+ try {
358
+ return await this.dispatchChat(messages, tools, opts, searchOptions);
359
+ }
360
+ catch (error) {
361
+ return await this.chatWithProviderFallback(error, messages, tools, opts, searchOptions);
362
+ }
363
+ }
364
+ async dispatchChat(messages, tools, opts, searchOptions) {
326
365
  if (this.geminiProvider) {
327
366
  return this.geminiProvider.chat(messages, tools, opts);
328
367
  }
@@ -334,36 +373,144 @@ export class CodeBuddyClient {
334
373
  }
335
374
  return this.openaiCompatProvider.chat(messages, tools, opts, searchOptions);
336
375
  }
376
+ async chatWithProviderFallback(primaryError, messages, tools, opts, searchOptions) {
377
+ const fallbackCandidates = [
378
+ ...this.credentialPoolProviders,
379
+ ...this.fallbackProviders,
380
+ ];
381
+ if (opts.disableProviderFallback || fallbackCandidates.length === 0) {
382
+ throw primaryError;
383
+ }
384
+ const fallbackOptsBase = {
385
+ ...opts,
386
+ disableProviderFallback: true,
387
+ };
388
+ for (const fallback of fallbackCandidates) {
389
+ try {
390
+ logger.warn('Primary provider failed; trying fallback provider', {
391
+ source: 'CodeBuddyClient',
392
+ primaryProvider: this.getProviderName(),
393
+ fallbackProvider: fallback.provider,
394
+ fallbackModel: fallback.model,
395
+ fallbackSource: fallback.fallbackSource,
396
+ profileId: fallback.profileId,
397
+ error: primaryError instanceof Error ? primaryError.message : String(primaryError),
398
+ });
399
+ const fallbackClient = new CodeBuddyClient(fallback.apiKey, fallback.model, fallback.baseURL, { enableFallbacks: false });
400
+ if (this.circuitBreakerConfig) {
401
+ fallbackClient.setCircuitBreakerConfig(this.circuitBreakerConfig);
402
+ }
403
+ const response = await fallbackClient.chat(messages, tools, {
404
+ ...fallbackOptsBase,
405
+ model: fallback.model,
406
+ }, searchOptions);
407
+ recordRuntimeFallbackSuccess(fallback);
408
+ return response;
409
+ }
410
+ catch (fallbackError) {
411
+ recordRuntimeFallbackFailure(fallback, fallbackError);
412
+ logger.warn('Fallback provider failed', {
413
+ source: 'CodeBuddyClient',
414
+ fallbackProvider: fallback.provider,
415
+ fallbackModel: fallback.model,
416
+ fallbackSource: fallback.fallbackSource,
417
+ profileId: fallback.profileId,
418
+ error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
419
+ });
420
+ }
421
+ }
422
+ throw primaryError;
423
+ }
337
424
  async *chatStream(messages, tools, options, searchOptions) {
338
425
  // Support both old signature (model as string) and new signature (options object)
339
426
  const opts = typeof options === "string"
340
427
  ? { model: options, searchOptions }
341
428
  : options || {};
342
- // Dispatch to the active strategy. Wrapped in a factory so we can
343
- // re-invoke it inside `withStreamRetry` when opt-in retry is enabled.
344
- const factory = () => {
345
- if (this.geminiProvider) {
346
- return this.geminiProvider.chatStream(messages, tools, opts);
347
- }
348
- if (this.chatgptProvider) {
349
- return this.chatgptProvider.chatStream(messages, tools, opts);
350
- }
351
- if (this.geminiCliProvider) {
352
- return this.geminiCliProvider.chatStream(messages, tools, opts);
353
- }
354
- return this.openaiCompatProvider.chatStream(messages, tools, opts, searchOptions);
355
- };
356
429
  // Resolve retry opt-in. Per-call `streamRetry` wins over env var when
357
430
  // explicitly set (including `false`, which forces no retry).
358
431
  const envOptIn = process.env.CODEBUDDY_STREAM_RETRY === '1';
359
432
  const callOptIn = opts.streamRetry;
360
433
  const retryEnabled = callOptIn !== undefined ? !!callOptIn : envOptIn;
361
- if (!retryEnabled) {
362
- yield* factory();
363
- return;
364
- }
365
434
  const retryOpts = typeof callOptIn === 'object' && callOptIn !== null ? callOptIn : {};
366
- yield* withStreamRetry(factory, retryOpts);
435
+ const primaryFactory = () => this.dispatchChatStream(messages, tools, opts, searchOptions);
436
+ const primaryStream = retryEnabled
437
+ ? withStreamRetry(primaryFactory, retryOpts)
438
+ : primaryFactory();
439
+ let yieldedAnyChunk = false;
440
+ try {
441
+ for await (const chunk of primaryStream) {
442
+ yieldedAnyChunk = true;
443
+ yield chunk;
444
+ }
445
+ }
446
+ catch (error) {
447
+ if (yieldedAnyChunk) {
448
+ throw error;
449
+ }
450
+ yield* this.chatStreamWithProviderFallback(error, messages, tools, opts, searchOptions);
451
+ }
452
+ }
453
+ dispatchChatStream(messages, tools, opts, searchOptions) {
454
+ if (this.geminiProvider) {
455
+ return this.geminiProvider.chatStream(messages, tools, opts);
456
+ }
457
+ if (this.chatgptProvider) {
458
+ return this.chatgptProvider.chatStream(messages, tools, opts);
459
+ }
460
+ if (this.geminiCliProvider) {
461
+ return this.geminiCliProvider.chatStream(messages, tools, opts);
462
+ }
463
+ return this.openaiCompatProvider.chatStream(messages, tools, opts, searchOptions);
464
+ }
465
+ async *chatStreamWithProviderFallback(primaryError, messages, tools, opts, searchOptions) {
466
+ const fallbackCandidates = [
467
+ ...this.credentialPoolProviders,
468
+ ...this.fallbackProviders,
469
+ ];
470
+ if (opts.disableProviderFallback || fallbackCandidates.length === 0) {
471
+ throw primaryError;
472
+ }
473
+ const fallbackOptsBase = {
474
+ ...opts,
475
+ disableProviderFallback: true,
476
+ };
477
+ for (const fallback of fallbackCandidates) {
478
+ try {
479
+ logger.warn('Primary provider stream failed before first chunk; trying fallback provider', {
480
+ source: 'CodeBuddyClient',
481
+ primaryProvider: this.getProviderName(),
482
+ fallbackProvider: fallback.provider,
483
+ fallbackModel: fallback.model,
484
+ fallbackSource: fallback.fallbackSource,
485
+ profileId: fallback.profileId,
486
+ error: primaryError instanceof Error ? primaryError.message : String(primaryError),
487
+ });
488
+ const fallbackClient = new CodeBuddyClient(fallback.apiKey, fallback.model, fallback.baseURL, { enableFallbacks: false });
489
+ if (this.circuitBreakerConfig) {
490
+ fallbackClient.setCircuitBreakerConfig(this.circuitBreakerConfig);
491
+ }
492
+ for await (const chunk of fallbackClient.chatStream(messages, tools, {
493
+ ...fallbackOptsBase,
494
+ model: fallback.model,
495
+ }, searchOptions)) {
496
+ yield chunk;
497
+ }
498
+ recordRuntimeFallbackSuccess(fallback);
499
+ return;
500
+ }
501
+ catch (fallbackError) {
502
+ recordRuntimeFallbackFailure(fallback, fallbackError);
503
+ logger.warn('Fallback provider stream failed', {
504
+ source: 'CodeBuddyClient',
505
+ fallbackProvider: fallback.provider,
506
+ fallbackModel: fallback.model,
507
+ fallbackSource: fallback.fallbackSource,
508
+ profileId: fallback.profileId,
509
+ error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
510
+ });
511
+ }
512
+ }
513
+ throw primaryError;
367
514
  }
368
515
  async search(query, searchParameters) {
369
516
  const searchMessage = {