@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
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ process.env.CODEBUDDY_CLI_VERSION = packageJson.version;
24
24
  import { logger } from "./utils/logger.js";
25
25
  // Import graceful shutdown for clean application termination
26
26
  import { initializeGracefulShutdown, getShutdownManager, } from "./utils/graceful-shutdown.js";
27
+ import { sweepStaleCodebuddyTemp } from "./utils/disk-guard.js";
27
28
  /**
28
29
  * CLI output helper.
29
30
  *
@@ -165,6 +166,10 @@ const _shutdownManager = initializeGracefulShutdown({
165
166
  forceExitOnTimeout: true,
166
167
  showProgress: true,
167
168
  });
169
+ // Startup janitor: remove our own stale /tmp scratch dirs left by crashed or
170
+ // SIGKILL'd runs (the durable backstop for the disk-leak class that caused the
171
+ // 2026-06-17 ENOSPC — see src/utils/disk-guard.ts). Age-gated; never throws.
172
+ sweepStaleCodebuddyTemp();
168
173
  // Note: SIGINT, SIGTERM, SIGHUP are now handled by GracefulShutdownManager
169
174
  // The manager will:
170
175
  // 1. Wait for pending operations to complete
@@ -352,11 +357,94 @@ async function getDetectedProvider() {
352
357
  return cachedProvider;
353
358
  await ensureEnvLoaded();
354
359
  cachedProvider = detectProviderFromEnv();
360
+ // xAI subscription login (`buddy login xai`). Token load + refresh is async,
361
+ // so it can't live in the sync env detector. A valid login takes precedence
362
+ // over an ambient local/env provider (mirrors ChatGPT-login precedence) — but
363
+ // ONLY when the token actually resolves, so a stale login never strands a
364
+ // working provider. Without this, `buddy login xai` stored tokens the runtime
365
+ // never consumed.
366
+ {
367
+ const xaiOverride = process.env.CODEBUDDY_PROVIDER?.toLowerCase();
368
+ const xaiWanted = xaiOverride === 'xai' ||
369
+ (!xaiOverride &&
370
+ cachedProvider?.provider !== 'chatgpt' &&
371
+ !process.env.GROK_API_KEY &&
372
+ !process.env.XAI_API_KEY);
373
+ if (xaiWanted) {
374
+ try {
375
+ const { hasXaiCredentials, getValidXaiAccessToken } = await import('./providers/xai-oauth.js');
376
+ if (hasXaiCredentials()) {
377
+ const token = await getValidXaiAccessToken();
378
+ if (token) {
379
+ cachedProvider = {
380
+ provider: 'grok',
381
+ apiKey: token,
382
+ baseURL: 'https://api.x.ai/v1',
383
+ // grok-4-latest is an alias of the current flagship grok-4.3
384
+ // (verified accessible on the SuperGrok plan; Hermes defaults here too).
385
+ defaultModel: process.env.GROK_MODEL || 'grok-4-latest',
386
+ };
387
+ }
388
+ else {
389
+ logger.warn('xAI login found but no valid access token — run `buddy login xai` again.');
390
+ }
391
+ }
392
+ }
393
+ catch (err) {
394
+ logger.debug('xAI login resolution skipped', {
395
+ error: err instanceof Error ? err.message : String(err),
396
+ });
397
+ }
398
+ }
399
+ }
400
+ // Honor an onboarded LOCAL provider (Ollama / LM Studio) persisted to
401
+ // user-settings.json by `buddy onboard`. Env detection only inspects
402
+ // OLLAMA_HOST/oauth files, so a user who completed the wizard and picked
403
+ // Ollama would otherwise dead-end on "No provider configured" on the next
404
+ // run — local providers legitimately carry no API key, so the guard that
405
+ // keys off apiKey presence misfires. Cloud/env providers are matched first
406
+ // above, so this only triggers when nothing else resolved.
407
+ if (!cachedProvider) {
408
+ cachedProvider = await detectOnboardedLocalProvider();
409
+ }
355
410
  if (cachedProvider) {
356
411
  logger.info(`Auto-detected provider: ${cachedProvider.provider} (model: ${cachedProvider.defaultModel})`);
357
412
  }
358
413
  return cachedProvider;
359
414
  }
415
+ /**
416
+ * Resolve an onboarded local provider (Ollama / LM Studio) from
417
+ * user-settings.json. Returns a detection with a placeholder API key (local
418
+ * OpenAI-compatible servers ignore it) so the downstream "no provider" guard
419
+ * passes and the persisted baseURL/model are used.
420
+ */
421
+ async function detectOnboardedLocalProvider() {
422
+ try {
423
+ const getSettingsManager = await lazyImport.settingsManager();
424
+ const settings = getSettingsManager().loadUserSettings();
425
+ const provider = (settings.provider || '').toLowerCase();
426
+ if (provider !== 'ollama' && provider !== 'lmstudio')
427
+ return null;
428
+ const fallbackBase = provider === 'ollama'
429
+ ? 'http://localhost:11434/v1'
430
+ : 'http://localhost:1234/v1';
431
+ const baseURL = settings.baseURL || fallbackBase;
432
+ // Mirror the env path so anything else reading OLLAMA_HOST stays consistent.
433
+ if (provider === 'ollama' && !process.env.OLLAMA_HOST) {
434
+ process.env.OLLAMA_HOST = baseURL.replace(/\/v1\/?$/, '');
435
+ }
436
+ return {
437
+ provider: provider,
438
+ apiKey: provider, // placeholder — ignored by local OpenAI-compat servers
439
+ baseURL,
440
+ defaultModel: settings.model || (provider === 'ollama' ? 'llama3' : 'default'),
441
+ source: 'environment',
442
+ };
443
+ }
444
+ catch {
445
+ return null;
446
+ }
447
+ }
360
448
  // Load API key from environment, secure storage, or legacy settings
361
449
  async function loadApiKey() {
362
450
  await ensureEnvLoaded();
@@ -426,28 +514,77 @@ async function loadModel() {
426
514
  // 1. Explicit env var takes highest priority
427
515
  if (process.env.GROK_MODEL)
428
516
  return process.env.GROK_MODEL;
429
- // 2. Project/user settings override auto-detection
517
+ const detected = await getDetectedProvider();
518
+ // 2. Project/user settings override auto-detection — UNLESS the saved model is
519
+ // incompatible with the detected provider. A `gpt-5.5` left in settings.json
520
+ // would 404 against xAI after `buddy login xai`; prefer the Grok default then.
430
521
  try {
431
522
  const getSettingsManager = await lazyImport.settingsManager();
432
- const manager = getSettingsManager();
433
- const settingsModel = manager.getCurrentModel();
434
- if (settingsModel)
435
- return settingsModel;
523
+ const settingsModel = getSettingsManager().getCurrentModel();
524
+ if (settingsModel) {
525
+ const grokProvider = detected?.provider === 'grok';
526
+ if (!grokProvider || /grok/i.test(settingsModel)) {
527
+ return settingsModel;
528
+ }
529
+ }
436
530
  }
437
531
  catch (_err) {
438
532
  logger.debug('Failed to load model from settings manager', { error: _err });
439
533
  }
440
534
  // 3. Fallback to auto-detected provider's default model
441
- const detected = await getDetectedProvider();
442
535
  if (detected)
443
536
  return detected.defaultModel;
444
537
  return undefined;
445
538
  }
539
+ /**
540
+ * Active-LLM auto-failover. When `[llm] enabled` (or CODEBUDDY_LLM_FAILOVER=1),
541
+ * build the registry of the user's live logins and inject it into the agent's
542
+ * client fallback list — so a failing primary transparently fails over to the
543
+ * next active LLM (resilience order by default: capable/subscription first,
544
+ * local last). OFF by default → single-provider behavior is unchanged.
545
+ */
546
+ async function applyActiveLlmFailover(agent) {
547
+ try {
548
+ const { getConfigManager } = await import('./config/toml-config.js');
549
+ const llmCfg = getConfigManager().getConfig().llm;
550
+ const enabled = Boolean(llmCfg?.enabled) || process.env.CODEBUDDY_LLM_FAILOVER === '1';
551
+ if (!enabled)
552
+ return;
553
+ const primary = await getDetectedProvider();
554
+ const { buildActiveLlmRegistry } = await import('./providers/active-llm-registry.js');
555
+ const orderEnv = process.env.CODEBUDDY_LLM_ORDER;
556
+ const registry = await buildActiveLlmRegistry({
557
+ primary: primary
558
+ ? {
559
+ provider: primary.provider,
560
+ apiKey: primary.apiKey,
561
+ baseURL: primary.baseURL,
562
+ model: primary.defaultModel,
563
+ }
564
+ : undefined,
565
+ policy: orderEnv || llmCfg?.order || 'resilience',
566
+ manualOrder: llmCfg?.manualOrder,
567
+ localOnly: llmCfg?.local_only,
568
+ });
569
+ if (registry.fallbacks.length > 0) {
570
+ agent.getClient().setRuntimeFallbackProviders(registry.fallbacks);
571
+ logger.info(`Active-LLM failover: ${primary?.provider ?? '?'} → ${registry.fallbacks
572
+ .map((f) => f.provider)
573
+ .join(' → ')}`);
574
+ }
575
+ }
576
+ catch (err) {
577
+ logger.debug('Active-LLM failover setup skipped', {
578
+ error: err instanceof Error ? err.message : String(err),
579
+ });
580
+ }
581
+ }
446
582
  // Handle commit-and-push command in headless mode
447
583
  async function handleCommitAndPushHeadless(apiKey, baseURL, model, maxToolRounds) {
448
584
  try {
449
585
  const CodeBuddyAgent = await lazyImport.CodeBuddyAgent();
450
586
  const agent = new CodeBuddyAgent(apiKey, baseURL, model, maxToolRounds);
587
+ await applyActiveLlmFailover(agent);
451
588
  // Configure confirmation service for headless mode (auto-approve all operations)
452
589
  const { ConfirmationService } = await import("./utils/confirmation-service.js");
453
590
  const confirmationService = ConfirmationService.getInstance();
@@ -695,6 +832,7 @@ async function processPromptHeadless(prompt, apiKey, baseURL, model, maxToolRoun
695
832
  const modelToUse = customAgentConfig?.model ?? model;
696
833
  const CodeBuddyAgent = await lazyImport.CodeBuddyAgent();
697
834
  const agent = new CodeBuddyAgent(apiKey, baseURL, modelToUse, maxToolRounds);
835
+ await applyActiveLlmFailover(agent);
698
836
  await agent.systemPromptReady;
699
837
  if (customAgentConfig?.systemPrompt) {
700
838
  agent.setSystemPrompt(customAgentConfig.systemPrompt);
@@ -728,6 +866,20 @@ async function processPromptHeadless(prompt, apiKey, baseURL, model, maxToolRoun
728
866
  }
729
867
  // Process the user message
730
868
  const chatEntries = await agent.processUserMessage(prompt);
869
+ // WS3-T1 — session-end flush (handoff + lesson candidates). Awaited with
870
+ // a hard cap so headless runs keep their continuity write without ever
871
+ // hanging the exit; trivial sessions no-op inside the module.
872
+ try {
873
+ const { runSessionEndFlush } = await import('./agent/session-end-flush.js');
874
+ const flushTimeoutMs = parseInt(process.env.CODEBUDDY_SESSION_END_FLUSH_TIMEOUT_MS || '15000', 10);
875
+ await Promise.race([
876
+ runSessionEndFlush({ chatHistory: chatEntries }),
877
+ new Promise((resolve) => setTimeout(resolve, flushTimeoutMs).unref()),
878
+ ]);
879
+ }
880
+ catch (e) {
881
+ logger.debug('Headless session-end flush skipped', { error: String(e) });
882
+ }
731
883
  // Log entries to interaction logger
732
884
  if (interactionLogger) {
733
885
  for (const entry of chatEntries) {
@@ -928,7 +1080,7 @@ program
928
1080
  .option("--quiet", "suppress informational logs (only show errors and responses)")
929
1081
  .option("--verbose", "enable verbose/debug output")
930
1082
  .option("--speak", "enable automatic speech synthesis of agent responses using Text-to-Speech")
931
- .option("--tts-provider <provider>", "TTS provider (edge-tts, espeak, say, piper, audioreader, kokoro)")
1083
+ .option("--tts-provider <provider>", "TTS provider (edge-tts, espeak, say, piper, audioreader)")
932
1084
  .action(async (message, options) => {
933
1085
  // Apply --quiet / --verbose flags
934
1086
  if (options.quiet) {
@@ -949,7 +1101,7 @@ program
949
1101
  ttsManager.setAutoSpeak(true);
950
1102
  }
951
1103
  if (options.ttsProvider) {
952
- const validProviders = ['edge-tts', 'espeak', 'say', 'piper', 'audioreader', 'kokoro'];
1104
+ const validProviders = ['edge-tts', 'espeak', 'say', 'piper', 'audioreader'];
953
1105
  if (validProviders.includes(options.ttsProvider)) {
954
1106
  ttsManager.updateConfig({ provider: options.ttsProvider });
955
1107
  }
@@ -1129,13 +1281,56 @@ program
1129
1281
  }
1130
1282
  try {
1131
1283
  // Get API key from options, environment, or user settings
1132
- const apiKey = options.apiKey || await loadApiKey();
1133
- const baseURL = options.baseUrl || await loadBaseURL();
1134
- let model = options.model || await loadModel(); // let: can be overridden by --agent
1284
+ const explicitProvider = options.model && !options.apiKey && !options.baseUrl
1285
+ ? (await import('./commands/llm-provider-resolution.js')).resolveCommandProvider({
1286
+ explicitModel: options.model,
1287
+ })
1288
+ : null;
1289
+ let apiKey = options.apiKey || explicitProvider?.apiKey || await loadApiKey();
1290
+ let baseURL = options.baseUrl || explicitProvider?.baseURL || await loadBaseURL();
1291
+ let model = options.model || explicitProvider?.model || await loadModel(); // let: can be overridden by --agent
1135
1292
  const maxToolRounds = parseInt(options.maxToolRounds) || 400;
1136
1293
  if (!apiKey) {
1137
- logger.error("❌ Error: API key required. Set GOOGLE_API_KEY, GROK_API_KEY, or OPENAI_API_KEY environment variable, use --api-key flag, or save to ~/.codebuddy/user-settings.json");
1138
- process.exit(1);
1294
+ // In an interactive terminal, offer the guided setup wizard right here
1295
+ // (like Hermes) instead of dead-ending on an error — then continue the
1296
+ // session with the credentials it captured.
1297
+ const interactive = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) &&
1298
+ !options.prompt && !options.print &&
1299
+ process.env.CI !== 'true' && process.env.GITHUB_ACTIONS !== 'true';
1300
+ let recovered = false;
1301
+ if (interactive) {
1302
+ const readline = await import('readline');
1303
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1304
+ const answer = await new Promise((resolve) => rl.question('\n❔ No AI provider configured. Run guided setup now? [Y/n] ', resolve));
1305
+ rl.close();
1306
+ const yes = answer.trim() === '' || /^y(es)?$/i.test(answer.trim());
1307
+ if (yes) {
1308
+ try {
1309
+ const { runOnboarding } = await import('./wizard/onboarding.js');
1310
+ const result = await runOnboarding();
1311
+ // Bust the cached provider detection so the just-saved creds resolve.
1312
+ cachedProvider = undefined;
1313
+ apiKey = options.apiKey || await loadApiKey();
1314
+ baseURL = options.baseUrl || await loadBaseURL();
1315
+ model = options.model || result.model || await loadModel();
1316
+ recovered = Boolean(apiKey);
1317
+ }
1318
+ catch (err) {
1319
+ logger.error('Guided setup did not complete', err instanceof Error ? err : { error: String(err) });
1320
+ }
1321
+ }
1322
+ }
1323
+ if (!recovered) {
1324
+ logger.error([
1325
+ "❌ No AI provider configured. Pick one to get started:",
1326
+ " • Guided setup (recommended) — interactive wizard: buddy onboard",
1327
+ " • Free, no API key — sign in with your ChatGPT plan: buddy login",
1328
+ " • Local & free — run a model with Ollama, then: export OLLAMA_HOST=http://localhost:11434",
1329
+ " • API key — set GROK_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / GOOGLE_API_KEY (or pass --api-key)",
1330
+ " Run buddy doctor to check your setup.",
1331
+ ].join("\n"));
1332
+ process.exit(1);
1333
+ }
1139
1334
  }
1140
1335
  // Save API key and base URL to user settings if provided via command line
1141
1336
  if (options.apiKey || options.baseUrl) {
@@ -1319,6 +1514,7 @@ program
1319
1514
  recordStartupPhase('agent-create-start');
1320
1515
  const agent = new CodeBuddyAgent(apiKey, baseURL, model, maxToolRounds, true, systemPromptId);
1321
1516
  recordStartupPhase('agent-create-done');
1517
+ await applyActiveLlmFailover(agent);
1322
1518
  // Apply custom agent system prompt if configured
1323
1519
  if (customAgentConfig?.systemPrompt) {
1324
1520
  await agent.systemPromptReady;
@@ -1520,6 +1716,9 @@ program
1520
1716
  tags: ['interactive'],
1521
1717
  });
1522
1718
  agent.__interactionLogger = interactionLogger;
1719
+ // WS3-T1 — pre-load the flush module so the sync `exit` handler
1720
+ // can call it (ESM has no require; dynamic import is async).
1721
+ const sessionEndFlush = await import('./agent/session-end-flush.js');
1523
1722
  const cleanup = () => {
1524
1723
  try {
1525
1724
  interactionLogger.endSession();
@@ -1527,10 +1726,30 @@ program
1527
1726
  catch (e) {
1528
1727
  logger.debug('Failed to end interaction logger session', { error: String(e) });
1529
1728
  }
1729
+ // `exit` handlers are sync-only: write at least the handoff
1730
+ // (no LLM) so an interrupted session still leaves a resume
1731
+ // point. The full async flush runs on SIGINT/SIGTERM below.
1732
+ try {
1733
+ sessionEndFlush.writeHandoffSync(agent.getChatHistory());
1734
+ }
1735
+ catch (_err) { /* handoff is best-effort on hard exit */ }
1736
+ };
1737
+ const flushThenExit = () => {
1738
+ cleanup();
1739
+ void (async () => {
1740
+ try {
1741
+ await Promise.race([
1742
+ sessionEndFlush.runSessionEndFlush({ chatHistory: agent.getChatHistory() }),
1743
+ new Promise((resolve) => setTimeout(resolve, 8000).unref()),
1744
+ ]);
1745
+ }
1746
+ catch (_err) { /* never block exit on the flush */ }
1747
+ process.exit(0);
1748
+ })();
1530
1749
  };
1531
1750
  process.on('exit', cleanup);
1532
- process.on('SIGINT', () => { cleanup(); process.exit(0); });
1533
- process.on('SIGTERM', () => { cleanup(); process.exit(0); });
1751
+ process.on('SIGINT', flushThenExit);
1752
+ process.on('SIGTERM', flushThenExit);
1534
1753
  }
1535
1754
  catch (err) {
1536
1755
  logger.warn('Failed to initialize interaction logger', { error: String(err) });
@@ -1624,7 +1843,14 @@ gitCommand
1624
1843
  const model = options.model || await loadModel();
1625
1844
  const maxToolRounds = parseInt(options.maxToolRounds) || 400;
1626
1845
  if (!apiKey) {
1627
- logger.error("❌ Error: API key required. Set GOOGLE_API_KEY, GROK_API_KEY, or OPENAI_API_KEY environment variable, use --api-key flag, or save to ~/.codebuddy/user-settings.json");
1846
+ logger.error([
1847
+ "❌ No AI provider configured. Pick one to get started:",
1848
+ " • Guided setup (recommended) — interactive wizard: buddy onboard",
1849
+ " • Free, no API key — sign in with your ChatGPT plan: buddy login",
1850
+ " • Local & free — run a model with Ollama, then: export OLLAMA_HOST=http://localhost:11434",
1851
+ " • API key — set GROK_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / GOOGLE_API_KEY (or pass --api-key)",
1852
+ " Run buddy doctor to check your setup.",
1853
+ ].join("\n"));
1628
1854
  process.exit(1);
1629
1855
  }
1630
1856
  // Save API key and base URL to user settings if provided via command line
@@ -1664,7 +1890,7 @@ function removeCommands(parent, names) {
1664
1890
  * subcommands, we register a thin wrapper that delegates to the real
1665
1891
  * command tree on first use.
1666
1892
  */
1667
- function addLazyCommand(parent, name, description, loader) {
1893
+ function addLazyCommand(parent, name, description, loader, prevalidateArgv) {
1668
1894
  // Create a pass-through command that accepts arbitrary args
1669
1895
  const stub = parent
1670
1896
  .command(name)
@@ -1674,6 +1900,14 @@ function addLazyCommand(parent, name, description, loader) {
1674
1900
  .helpOption(false);
1675
1901
  // Override the parse to delegate to the real command
1676
1902
  stub.action(async () => {
1903
+ try {
1904
+ await prevalidateArgv?.(process.argv);
1905
+ }
1906
+ catch (error) {
1907
+ const message = error instanceof Error ? error.message : String(error);
1908
+ process.stderr.write(`error: ${message}\n`);
1909
+ process.exit(1);
1910
+ }
1677
1911
  const realCommand = await loader();
1678
1912
  // Replace the stub with the real command and re-parse
1679
1913
  removeCommands(parent, name);
@@ -1727,6 +1961,19 @@ program
1727
1961
  process.exit(1);
1728
1962
  }
1729
1963
  });
1964
+ // Cowork (the Electron desktop GUI) needs Node >= 22; the terminal CLI runs on
1965
+ // Node >= 18. Without this guard a Node 18/20 user gets a cryptic Electron/Vite
1966
+ // crash mid-launch instead of a clear message — a classic first-run star-killer.
1967
+ const COWORK_MIN_NODE_MAJOR = 22;
1968
+ function assertNodeForCowork() {
1969
+ const major = Number(process.versions.node.split(".")[0]);
1970
+ if (Number.isFinite(major) && major < COWORK_MIN_NODE_MAJOR) {
1971
+ cli.stdout(`❌ Cowork (the desktop GUI) requires Node.js >= ${COWORK_MIN_NODE_MAJOR} — you're on v${process.versions.node}.`);
1972
+ cli.stdout(` The terminal CLI works on Node >= 18; only the Electron app needs >= ${COWORK_MIN_NODE_MAJOR}.`);
1973
+ cli.stdout(` Upgrade Node (e.g. \`nvm install 22 && nvm use 22\`) and retry. Linux build notes: cowork/DEV-LINUX.md`);
1974
+ process.exit(1);
1975
+ }
1976
+ }
1730
1977
  // Desktop GUI commands
1731
1978
  program
1732
1979
  .command("gui")
@@ -1734,6 +1981,7 @@ program
1734
1981
  .option("--dev", "start with Vite dev server (hot reload)")
1735
1982
  .option("--detach", "run in background")
1736
1983
  .action(async (options) => {
1984
+ assertNodeForCowork();
1737
1985
  const { launchDesktop } = await import("./desktop/launcher.js");
1738
1986
  const code = await launchDesktop({
1739
1987
  dev: options.dev,
@@ -1747,6 +1995,7 @@ program
1747
1995
  .option("--dev", "start with Vite dev server")
1748
1996
  .option("--detach", "run in background")
1749
1997
  .action(async (options) => {
1998
+ assertNodeForCowork();
1750
1999
  const { launchDesktop } = await import("./desktop/launcher.js");
1751
2000
  const code = await launchDesktop({
1752
2001
  dev: options.dev,
@@ -1758,21 +2007,146 @@ program
1758
2007
  .command("install-gui")
1759
2008
  .description("Install Electron and build the desktop GUI")
1760
2009
  .action(async () => {
2010
+ assertNodeForCowork();
1761
2011
  const { installGUI } = await import("./desktop/installer.js");
1762
2012
  await installGUI();
1763
2013
  });
2014
+ /**
2015
+ * After a successful xAI login, probe api.x.ai with one tiny real call so the
2016
+ * user learns immediately whether their subscription includes API inference
2017
+ * (a SuperGrok plan does not always — the OAuth `api:access` scope is
2018
+ * requested, not guaranteed granted).
2019
+ */
2020
+ async function probeXaiInference() {
2021
+ const { getValidXaiAccessToken, XAI_OAUTH_BASE_URL } = await import("./providers/xai-oauth.js");
2022
+ cli.stdout("\nVerifying inference access (api.x.ai) ...");
2023
+ const token = await getValidXaiAccessToken();
2024
+ if (!token) {
2025
+ cli.error("❌ Could not load a valid token after login.");
2026
+ return;
2027
+ }
2028
+ const probeModel = process.env.GROK_MODEL || "grok-3";
2029
+ try {
2030
+ const res = await fetch(`${XAI_OAUTH_BASE_URL}/chat/completions`, {
2031
+ method: "POST",
2032
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
2033
+ body: JSON.stringify({
2034
+ model: probeModel,
2035
+ max_tokens: 1,
2036
+ messages: [{ role: "user", content: "hi" }],
2037
+ }),
2038
+ signal: AbortSignal.timeout(30_000),
2039
+ });
2040
+ if (res.ok) {
2041
+ cli.stdout("✅ Inference access confirmed — your subscription works in Code Buddy.");
2042
+ cli.stdout(` Try: buddy --model ${probeModel} -p "hello" (or just \`buddy\`)`);
2043
+ }
2044
+ else if (res.status === 401 || res.status === 403) {
2045
+ const body = await res.text().catch(() => "");
2046
+ cli.error(`⚠️ Login succeeded, but api.x.ai returned ${res.status} — your subscription may not include API inference.`);
2047
+ if (body)
2048
+ cli.error(` Detail: ${body.slice(0, 200)}`);
2049
+ cli.error(" You can still use Grok with a metered API key: set XAI_API_KEY.");
2050
+ }
2051
+ else {
2052
+ const body = await res.text().catch(() => "");
2053
+ cli.error(`⚠️ Probe returned ${res.status} (model "${probeModel}"?): ${body.slice(0, 200)}`);
2054
+ }
2055
+ }
2056
+ catch (err) {
2057
+ cli.error(`⚠️ Probe call failed (network?): ${err instanceof Error ? err.message : String(err)}`);
2058
+ cli.stdout(' Tokens are stored; try `buddy -p "hello"`.');
2059
+ }
2060
+ }
2061
+ /**
2062
+ * xAI / Grok subscription login (`buddy login xai`). Two-step so it works in
2063
+ * backgrounded / non-interactive / remote shells where stdin can't be pasted
2064
+ * into: step 1 opens the browser + persists the PKCE material, step 2
2065
+ * (`--code <code>`) exchanges the code xAI shows in-page. In an interactive
2066
+ * TTY, step 1 also accepts the code inline for a one-shot experience.
2067
+ */
2068
+ async function loginXaiCli(codeArg) {
2069
+ const xai = await import("./providers/xai-oauth.js");
2070
+ // Step 2 — complete a pending login with the copied code.
2071
+ if (codeArg) {
2072
+ try {
2073
+ const auth = await xai.completeLogin(codeArg);
2074
+ cli.stdout("✅ Authenticated with xAI");
2075
+ if (auth.email)
2076
+ cli.stdout(` Account: ${auth.email}`);
2077
+ if (typeof auth.expires_in_seconds === "number") {
2078
+ cli.stdout(` Token TTL: ~${Math.round(auth.expires_in_seconds / 60)} min`);
2079
+ }
2080
+ cli.stdout(` Tokens stored at: ${xai.getXaiAuthFilePath()}`);
2081
+ await probeXaiInference();
2082
+ }
2083
+ catch (err) {
2084
+ cli.error(`❌ Login failed: ${err instanceof Error ? err.message : String(err)}`);
2085
+ process.exit(1);
2086
+ }
2087
+ return;
2088
+ }
2089
+ // Step 1 — open the browser and persist the pending PKCE material.
2090
+ cli.stdout("🔐 xAI / Grok login");
2091
+ let authorizeUrl;
2092
+ try {
2093
+ ({ authorizeUrl } = await xai.beginLogin());
2094
+ }
2095
+ catch (err) {
2096
+ cli.error(`❌ Could not start login: ${err instanceof Error ? err.message : String(err)}`);
2097
+ process.exit(1);
2098
+ return;
2099
+ }
2100
+ try {
2101
+ const openModule = await import("open");
2102
+ await openModule.default(authorizeUrl);
2103
+ cli.stdout("Opening your browser to authorize Code Buddy with your xAI / SuperGrok plan ...");
2104
+ }
2105
+ catch (_err) {
2106
+ /* browser open is best-effort — fall back to the printed URL below */
2107
+ }
2108
+ cli.stdout(`\nIf your browser didn't open, visit this URL:\n${authorizeUrl}\n`);
2109
+ cli.stdout("xAI shows an authorization CODE in the page. Copy it, then run:");
2110
+ cli.stdout(" buddy login xai --code <PASTE_THE_CODE>\n");
2111
+ // Interactive convenience: if we have a real TTY, accept the code inline.
2112
+ if (process.stdin.isTTY) {
2113
+ const readline = await import("readline");
2114
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2115
+ const answer = await new Promise((resolve) => rl.question("Paste the code here (or Ctrl-C to finish later): ", resolve));
2116
+ rl.close();
2117
+ if (answer.trim()) {
2118
+ try {
2119
+ const auth = await xai.completeLogin(answer);
2120
+ cli.stdout("✅ Authenticated with xAI");
2121
+ if (auth.email)
2122
+ cli.stdout(` Account: ${auth.email}`);
2123
+ cli.stdout(` Tokens stored at: ${xai.getXaiAuthFilePath()}`);
2124
+ await probeXaiInference();
2125
+ }
2126
+ catch (err) {
2127
+ cli.error(`❌ Login failed: ${err instanceof Error ? err.message : String(err)}`);
2128
+ process.exit(1);
2129
+ }
2130
+ }
2131
+ }
2132
+ }
1764
2133
  // Phase d.23+ — Authentication commands.
1765
2134
  // Run the OAuth flow directly from the CLI so users don't have to enter
1766
2135
  // the interactive TUI just to log in. Mirrors the `/login` slash command
1767
2136
  // that's available inside a session.
1768
2137
  program
1769
2138
  .command("login [provider]")
1770
- .description("Authenticate with a provider (default: chatgpt — uses your ChatGPT subscription)")
1771
- .action(async (provider) => {
2139
+ .description("Authenticate with a provider (chatgpt | xai — uses your subscription, no API key)")
2140
+ .option("--code <code>", "Complete an xAI login with the code shown in the browser")
2141
+ .action(async (provider, options) => {
1772
2142
  const target = (provider ?? "chatgpt").toLowerCase();
2143
+ if (target === "xai" || target === "grok" || target === "xai-oauth") {
2144
+ await loginXaiCli(options.code);
2145
+ return;
2146
+ }
1773
2147
  if (target !== "chatgpt" && target !== "codex" && target !== "openai") {
1774
- cli.stdout(`Unknown provider: "${provider}". Only \`chatgpt\` is supported.`);
1775
- cli.stdout("Other providers (Gemini, Anthropic, Grok) authenticate via API key env vars.");
2148
+ cli.stdout(`Unknown provider: "${provider}". Supported: \`chatgpt\`, \`xai\`.`);
2149
+ cli.stdout("Other providers (Gemini, Anthropic) authenticate via API key env vars.");
1776
2150
  process.exit(1);
1777
2151
  }
1778
2152
  const { loginInteractive, getCodexAuthFilePath } = await import("./providers/codex-oauth.js");
@@ -1800,11 +2174,21 @@ program
1800
2174
  });
1801
2175
  program
1802
2176
  .command("logout [provider]")
1803
- .description("Clear stored credentials for a provider (default: chatgpt)")
2177
+ .description("Clear stored credentials for a provider (chatgpt | xai)")
1804
2178
  .action(async (provider) => {
1805
2179
  const target = (provider ?? "chatgpt").toLowerCase();
2180
+ if (target === "xai" || target === "grok" || target === "xai-oauth") {
2181
+ const { hasXaiCredentials, clearXaiCredentials, getXaiAuthFilePath } = await import("./providers/xai-oauth.js");
2182
+ if (!hasXaiCredentials()) {
2183
+ cli.stdout("No xAI credentials on disk — already logged out.");
2184
+ return;
2185
+ }
2186
+ clearXaiCredentials();
2187
+ cli.stdout(`✅ xAI credentials cleared (${getXaiAuthFilePath()})`);
2188
+ return;
2189
+ }
1806
2190
  if (target !== "chatgpt" && target !== "codex" && target !== "openai") {
1807
- cli.stdout(`Unknown provider: "${provider}". Only \`chatgpt\` is supported.`);
2191
+ cli.stdout(`Unknown provider: "${provider}". Supported: \`chatgpt\`, \`xai\`.`);
1808
2192
  process.exit(1);
1809
2193
  }
1810
2194
  const { hasCodexCredentials, clearCodexCredentials, getCodexAuthFilePath } = await import("./providers/codex-oauth.js");
@@ -1845,6 +2229,89 @@ program
1845
2229
  process.exit(1);
1846
2230
  }
1847
2231
  });
2232
+ // Active-LLM registry — list the LLMs you're logged into + the failover order.
2233
+ program
2234
+ .command("llm [action] [prompt...]")
2235
+ .description("List active LLMs, or run several together: llm ensemble|consensus|race <prompt>")
2236
+ .option("--order <policy>", "Ordering: resilience | free-first | manual")
2237
+ .action(async (action, promptParts = [], opts) => {
2238
+ const primary = await getDetectedProvider();
2239
+ const { buildActiveLlmRegistry } = await import("./providers/active-llm-registry.js");
2240
+ const registry = await buildActiveLlmRegistry({
2241
+ primary: primary
2242
+ ? {
2243
+ provider: primary.provider,
2244
+ apiKey: primary.apiKey,
2245
+ baseURL: primary.baseURL,
2246
+ model: primary.defaultModel,
2247
+ }
2248
+ : undefined,
2249
+ policy: opts.order,
2250
+ });
2251
+ if (registry.all.length === 0) {
2252
+ cli.stdout("No active LLMs detected. Run `buddy login`, set an API key, or start Ollama.");
2253
+ return;
2254
+ }
2255
+ // "Together" — run several active LLMs at once and aggregate the result.
2256
+ const strategyByAction = {
2257
+ ensemble: "ensemble",
2258
+ consensus: "consensus",
2259
+ race: "fastest",
2260
+ together: "all",
2261
+ };
2262
+ const strategy = action ? strategyByAction[action.toLowerCase()] : undefined;
2263
+ if (strategy) {
2264
+ const promptText = promptParts.join(" ").trim();
2265
+ if (!promptText) {
2266
+ cli.error(`Usage: buddy llm ${action} "<prompt>"`);
2267
+ process.exit(1);
2268
+ }
2269
+ const models = registry.all.map((p) => ({
2270
+ id: p.provider,
2271
+ name: p.provider,
2272
+ provider: "codebuddy",
2273
+ model: p.model,
2274
+ apiKey: p.apiKey,
2275
+ baseURL: p.baseURL,
2276
+ enabled: true,
2277
+ costPerToken: p.costInputUsdPerMtok / 1_000_000,
2278
+ }));
2279
+ cli.stdout(`Running ${action} across ${models.map((m) => m.id).join(", ")} …`);
2280
+ const { createParallelExecutor } = await import("./agent/parallel/parallel-executor.js");
2281
+ const executor = createParallelExecutor({ models, strategy });
2282
+ const result = await executor.execute(promptText);
2283
+ // Show each LLM's own answer — the point of running them together.
2284
+ for (const r of result.responses) {
2285
+ if (r.error) {
2286
+ cli.stdout(`\n── ${r.modelName} ❌ ${r.error.slice(0, 140)}`);
2287
+ }
2288
+ else {
2289
+ cli.stdout(`\n── ${r.modelName} (${r.latency}ms, ${r.tokensUsed} tok)\n${r.content.trim()}`);
2290
+ }
2291
+ }
2292
+ cli.stdout(`\n${executor.formatResult(result)}`);
2293
+ return;
2294
+ }
2295
+ cli.stdout("Active LLMs (logged in + reachable):");
2296
+ for (const p of registry.all) {
2297
+ const isPrimary = registry.primaryProvider === p.provider ? " ← primary" : "";
2298
+ const tag = p.isLocal ? "local" : "cloud";
2299
+ const cost = p.costInputUsdPerMtok === 0 ? "$0" : `$${p.costInputUsdPerMtok}/Mtok`;
2300
+ cli.stdout(` • ${p.provider.padEnd(10)} [${tag}] ${p.model} (${cost})${isPrimary}`);
2301
+ }
2302
+ if (registry.fallbacks.length > 0) {
2303
+ cli.stdout(`\nFailover order: ${primary?.provider ?? "?"} → ${registry.fallbacks
2304
+ .map((f) => f.provider)
2305
+ .join(" → ")}`);
2306
+ }
2307
+ else {
2308
+ cli.stdout("\nNo additional active LLMs available for failover.");
2309
+ }
2310
+ const { getConfigManager } = await import("./config/toml-config.js");
2311
+ const on = Boolean(getConfigManager().getConfig().llm?.enabled) ||
2312
+ process.env.CODEBUDDY_LLM_FAILOVER === "1";
2313
+ cli.stdout(`Auto-failover: ${on ? "ON" : "OFF (enable with [llm].enabled = true, or CODEBUDDY_LLM_FAILOVER=1)"}`);
2314
+ });
1848
2315
  // MCP Server command - run Code Buddy as an MCP tool provider over stdio
1849
2316
  program
1850
2317
  .command("mcp-server")
@@ -1909,7 +2376,7 @@ addLazyCommandGroup(program, 'speak', 'Synthesize speech using AudioReader TTS',
1909
2376
  // Utility commands (doctor, security-audit, onboard, webhook) are all registered
1910
2377
  // by a single registerUtilityCommands() call, so we must remove all stubs before
1911
2378
  // re-registering to avoid Commander duplicate command errors.
1912
- const utilityCommandNames = ['doctor', 'security-audit', 'onboard', 'webhook'];
2379
+ const utilityCommandNames = ['doctor', 'security-audit', 'onboard', 'webhook', 'ollama'];
1913
2380
  const loadUtilityCommands = async () => {
1914
2381
  // Remove all utility stubs at once
1915
2382
  removeCommands(program, utilityCommandNames);
@@ -1921,7 +2388,8 @@ for (const cmdName of utilityCommandNames) {
1921
2388
  const desc = cmdName === 'doctor' ? 'Diagnose Code Buddy environment, dependencies, and configuration'
1922
2389
  : cmdName === 'security-audit' ? 'Run a security audit of your Code Buddy environment'
1923
2390
  : cmdName === 'onboard' ? 'Interactive setup wizard for Code Buddy'
1924
- : 'Manage webhook triggers';
2391
+ : cmdName === 'ollama' ? 'Inspect or update the local Ollama runtime'
2392
+ : 'Manage webhook triggers';
1925
2393
  const stub = program
1926
2394
  .command(cmdName)
1927
2395
  .description(desc)
@@ -2045,6 +2513,14 @@ addLazyCommand(program, 'flow', 'Execute a multi-agent planning flow (OpenManus-
2045
2513
  const { createFlowCommand } = await import('./commands/flow.js');
2046
2514
  return createFlowCommand();
2047
2515
  });
2516
+ // Goal Ralph loop — headless judge-gated auto-continue (Hermes Agent parity)
2517
+ addLazyCommand(program, 'goal', 'Run the agent toward a standing goal until a judge model confirms it is done (Ralph loop)', async () => {
2518
+ const { createGoalCommand } = await import('./commands/goal-cli.js');
2519
+ return createGoalCommand();
2520
+ }, async (argv) => {
2521
+ const { validateGoalCommandNumericOptions } = await import('./commands/goal-cli.js');
2522
+ validateGoalCommandNumericOptions(argv);
2523
+ });
2048
2524
  // Todo attention bias — Manus AI-inspired persistent task list
2049
2525
  addLazyCommand(program, 'todo', 'Manage persistent task list (todo.md) — injected at end of every agent turn for focus', async () => {
2050
2526
  const { createTodosCommand } = await import('./commands/todos.js');
@@ -2075,6 +2551,11 @@ addLazyCommand(program, 'update', 'Update Code Buddy (switch channels: stable, b
2075
2551
  const { createUpdateCommand } = await import('./commands/update.js');
2076
2552
  return createUpdateCommand();
2077
2553
  });
2554
+ // Tunnel — manage ngrok tunnels
2555
+ addLazyCommand(program, 'tunnel', 'Manage ngrok tunnels for the Code Buddy remote gateway', async () => {
2556
+ const { createTunnelCommand } = await import('./commands/tunnel.js');
2557
+ return createTunnelCommand();
2558
+ });
2078
2559
  // Nodes — companion app node management (macOS, iOS, Android)
2079
2560
  addLazyCommandGroup(program, 'nodes', 'Manage companion app nodes (macOS, iOS, Android)', async () => {
2080
2561
  const { registerNodeCommands } = await import('./commands/cli/node-commands.js');