@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,404 @@
1
+ /**
2
+ * disk-guard — the single place that bounds disk access.
3
+ *
4
+ * Motivation: on 2026-06-17 a tight build/Electron restart loop leaked temp
5
+ * dirs until the disk filled mid-run, corrupting command-output capture
6
+ * (ENOSPC). The fix at the time guarded ONE call site (the Cowork pilot). This
7
+ * module generalizes that lesson into reusable primitives so the failure class
8
+ * — "unbounded resource accumulation in a tight loop without monitoring or
9
+ * cleanup" — is handled in one place:
10
+ *
11
+ * - free-space preflight → {@link getFreeSpaceInfo}, {@link ensureFreeSpace}
12
+ * - managed temp lifecycle → {@link createManagedTempDir} (auto-cleaned on exit)
13
+ * - orphan sweep backstop → {@link sweepOrphans} (catches SIGKILL'd leaks)
14
+ * - byte budget → {@link DiskBudget}
15
+ * - bounded writes / logs → {@link boundedWriteFile}, {@link boundedAppendFile},
16
+ * {@link rotateIfLarge}
17
+ *
18
+ * Scope is deliberately focused. It is adopted at the highest-risk sites
19
+ * (disk-space checks, leaky /tmp dirs, unbounded append logs) rather than
20
+ * routing every fs write through it. It deliberately does NOT bound:
21
+ * - general / user-intentional workspace writes (the user asked for them),
22
+ * - third-party writers (Electron / Docker / Playwright) — covered instead by
23
+ * the startup {@link sweepOrphans} backstop,
24
+ * - low-traffic logs (add a one-line {@link rotateIfLarge} as needed).
25
+ *
26
+ * Config (read from the environment at call time, validated, with fallbacks —
27
+ * see {@link DISK_GUARD_DEFAULTS}; registered in `src/config/env-schema.ts`):
28
+ * CODEBUDDY_MIN_FREE_MB, CODEBUDDY_DISK_QUOTA_MB, CODEBUDDY_TEMP_MAX_AGE_MS,
29
+ * CODEBUDDY_LOG_MAX_MB, CODEBUDDY_LOG_MAX_FILES.
30
+ */
31
+ import { statfsSync, readdirSync, statSync, existsSync, renameSync, unlinkSync, rmSync, } from 'node:fs';
32
+ import { mkdtemp, rm, writeFile, appendFile } from 'node:fs/promises';
33
+ import { tmpdir } from 'node:os';
34
+ import { join, dirname, basename, extname } from 'node:path';
35
+ import { logger } from './logger.js';
36
+ import { CodeBuddyError } from '../errors/base-error.js';
37
+ import { registerDisposable, unregisterDisposable } from './disposable.js';
38
+ import { GracefulShutdownManager } from './graceful-shutdown.js';
39
+ const MB = 1024 * 1024;
40
+ /** Built-in fallbacks, used when the matching env var is unset/invalid. */
41
+ export const DISK_GUARD_DEFAULTS = {
42
+ /** Min free space (MB) before a guarded write/launch is refused. Matches the pilot's 500MB. */
43
+ minFreeMb: 500,
44
+ /** Per-session byte budget (MB); 0 = unlimited. */
45
+ quotaMb: 0,
46
+ /** Orphan-sweep age cutoff (ms); 0 = presence-based (remove any match). */
47
+ tempMaxAgeMs: 0,
48
+ /** Append-log rotation size (MB). Matches logger.ts. */
49
+ logMaxMb: 10,
50
+ /** Append-log retention count. Matches logger.ts. */
51
+ logMaxFiles: 5,
52
+ };
53
+ /** Parse a non-negative integer env var with range validation + fallback (mirrors AutoCompactConfig.fromEnv). */
54
+ function readIntEnv(name, fallback, min, max) {
55
+ const raw = process.env[name];
56
+ if (raw !== undefined && raw !== '') {
57
+ const parsed = parseInt(raw, 10);
58
+ if (!Number.isNaN(parsed) && parsed >= min && parsed <= max) {
59
+ return parsed;
60
+ }
61
+ }
62
+ return fallback;
63
+ }
64
+ function defaultMinFreeBytes() {
65
+ return readIntEnv('CODEBUDDY_MIN_FREE_MB', DISK_GUARD_DEFAULTS.minFreeMb, 0, 1_000_000) * MB;
66
+ }
67
+ function defaultQuotaBytes() {
68
+ return readIntEnv('CODEBUDDY_DISK_QUOTA_MB', DISK_GUARD_DEFAULTS.quotaMb, 0, 100_000_000) * MB;
69
+ }
70
+ function defaultTempMaxAgeMs() {
71
+ return readIntEnv('CODEBUDDY_TEMP_MAX_AGE_MS', DISK_GUARD_DEFAULTS.tempMaxAgeMs, 0, Number.MAX_SAFE_INTEGER);
72
+ }
73
+ function defaultLogMaxBytes() {
74
+ return readIntEnv('CODEBUDDY_LOG_MAX_MB', DISK_GUARD_DEFAULTS.logMaxMb, 1, 100_000) * MB;
75
+ }
76
+ function defaultLogMaxFiles() {
77
+ return readIntEnv('CODEBUDDY_LOG_MAX_FILES', DISK_GUARD_DEFAULTS.logMaxFiles, 1, 1000);
78
+ }
79
+ function formatMb(bytes) {
80
+ return `${Math.round(bytes / MB)}MB`;
81
+ }
82
+ function byteLength(data) {
83
+ return typeof data === 'string' ? Buffer.byteLength(data) : data.byteLength;
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ // Errors
87
+ // ---------------------------------------------------------------------------
88
+ /** Thrown by {@link ensureFreeSpace} when free disk space is below the threshold. */
89
+ export class DiskSpaceError extends CodeBuddyError {
90
+ constructor(message, context) {
91
+ super('DISK_SPACE_LOW', message, { context });
92
+ this.name = 'DiskSpaceError';
93
+ }
94
+ }
95
+ /** Thrown by {@link boundedWriteFile} when a {@link DiskBudget} reservation is refused. */
96
+ export class DiskBudgetError extends CodeBuddyError {
97
+ constructor(message, context) {
98
+ super('DISK_BUDGET_EXCEEDED', message, { context });
99
+ this.name = 'DiskBudgetError';
100
+ }
101
+ }
102
+ /**
103
+ * Free-space info for the filesystem containing `targetPath`, or `null` when
104
+ * `fs.statfsSync` is unavailable / the path can't be stat'd (never throws).
105
+ * Uses `bavail` (blocks available to non-root) — the honest free figure a
106
+ * normal process can actually use — not `bfree`.
107
+ */
108
+ export function getFreeSpaceInfo(targetPath) {
109
+ try {
110
+ const st = statfsSync(targetPath);
111
+ const bsize = Number(st.bsize);
112
+ const totalBytes = Number(st.blocks) * bsize;
113
+ const freeBytes = Number(st.bavail) * bsize;
114
+ const freePercent = totalBytes > 0 ? (freeBytes / totalBytes) * 100 : 0;
115
+ return { freeBytes, totalBytes, freePercent };
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ /** Free bytes for the filesystem containing `targetPath`, or `null` if unavailable. */
122
+ export function getFreeBytes(targetPath) {
123
+ return getFreeSpaceInfo(targetPath)?.freeBytes ?? null;
124
+ }
125
+ /**
126
+ * Throw {@link DiskSpaceError} if free space on `targetPath`'s filesystem is
127
+ * below `minBytes` (default {@link DISK_GUARD_DEFAULTS.minFreeMb}). No-op when
128
+ * statfs is unavailable — we can't guard, so we don't block (matches the
129
+ * platform-resilient behavior of the original pilot guard).
130
+ */
131
+ export function ensureFreeSpace(targetPath, minBytes, opts = {}) {
132
+ const min = minBytes ?? defaultMinFreeBytes();
133
+ const info = getFreeSpaceInfo(targetPath);
134
+ if (info === null)
135
+ return;
136
+ if (info.freeBytes < min) {
137
+ const label = opts.label ? `${opts.label}: ` : '';
138
+ throw new DiskSpaceError(`${label}only ${formatMb(info.freeBytes)} free on ${targetPath} — refusing to proceed ` +
139
+ `(needs ${formatMb(min)} headroom; free disk space first).`, { path: targetPath, freeBytes: info.freeBytes, minBytes: min });
140
+ }
141
+ }
142
+ /**
143
+ * Remove orphaned entries named `<prefix>*` directly under `root`. This is the
144
+ * durable backstop for the leak class: managed temp dirs are auto-disposed on a
145
+ * graceful exit, but a SIGKILL'd / crashed run skips cleanup and leaks its dir
146
+ * until the next startup sweep.
147
+ *
148
+ * - `maxAgeMs: 0` (default) → presence-based: remove every match (the original
149
+ * pilot semantics — safe when only one instance runs at a time).
150
+ * - `maxAgeMs > 0` → age-safe: only remove matches whose mtime is older than
151
+ * `maxAgeMs`, so a concurrently-running peer's fresh dir is never nuked.
152
+ */
153
+ export function sweepOrphans(root, prefix, opts = {}) {
154
+ const maxAgeMs = opts.maxAgeMs ?? defaultTempMaxAgeMs();
155
+ const dryRun = opts.dryRun ?? false;
156
+ const removed = [];
157
+ const skipped = [];
158
+ let names;
159
+ try {
160
+ names = readdirSync(root);
161
+ }
162
+ catch {
163
+ return { removed, skipped };
164
+ }
165
+ const now = Date.now();
166
+ for (const name of names) {
167
+ if (!name.startsWith(prefix))
168
+ continue;
169
+ const full = join(root, name);
170
+ if (maxAgeMs > 0) {
171
+ try {
172
+ const st = statSync(full);
173
+ if (now - st.mtimeMs < maxAgeMs) {
174
+ skipped.push(full);
175
+ continue;
176
+ }
177
+ }
178
+ catch {
179
+ skipped.push(full);
180
+ continue;
181
+ }
182
+ }
183
+ if (dryRun) {
184
+ removed.push(full);
185
+ continue;
186
+ }
187
+ try {
188
+ rmSync(full, { recursive: true, force: true });
189
+ removed.push(full);
190
+ }
191
+ catch {
192
+ skipped.push(full);
193
+ }
194
+ }
195
+ return { removed, skipped };
196
+ }
197
+ /**
198
+ * One-shot startup janitor: remove our own stale scratch dirs/files left in the
199
+ * system temp dir by crashed or SIGKILL'd runs (the durable backstop for the
200
+ * leak class). Age-gated (default 24h) so a concurrently-running session's fresh
201
+ * scratch dirs are never touched. Matches both `codebuddy-*` and `codebuddy_*`.
202
+ * Never throws.
203
+ */
204
+ export function sweepStaleCodebuddyTemp(maxAgeMs = 24 * 60 * 60 * 1000) {
205
+ return sweepOrphans(tmpdir(), 'codebuddy', { maxAgeMs });
206
+ }
207
+ const managedTempDirs = new Set();
208
+ let shutdownHandlerRegistered = false;
209
+ class ManagedTempDirImpl {
210
+ path;
211
+ disposed = false;
212
+ constructor(path) {
213
+ this.path = path;
214
+ }
215
+ async dispose() {
216
+ if (this.disposed)
217
+ return;
218
+ this.disposed = true;
219
+ managedTempDirs.delete(this);
220
+ unregisterDisposable(this);
221
+ try {
222
+ await rm(this.path, { recursive: true, force: true });
223
+ }
224
+ catch (err) {
225
+ logger.warn(`disk-guard: failed to remove managed temp dir ${this.path}`, {
226
+ error: err instanceof Error ? err.message : String(err),
227
+ });
228
+ }
229
+ }
230
+ }
231
+ /** Lazily wire a single shutdown handler the first time a managed temp dir is created. */
232
+ function ensureShutdownHandler() {
233
+ if (shutdownHandlerRegistered)
234
+ return;
235
+ shutdownHandlerRegistered = true;
236
+ try {
237
+ GracefulShutdownManager.getInstance().registerHandler({
238
+ name: 'disk-guard:temp-cleanup',
239
+ priority: 100,
240
+ handler: async () => {
241
+ await disposeAllManagedTempDirs();
242
+ },
243
+ });
244
+ }
245
+ catch (err) {
246
+ logger.debug('disk-guard: could not register shutdown handler', {
247
+ error: err instanceof Error ? err.message : String(err),
248
+ });
249
+ }
250
+ }
251
+ /** Dispose every still-tracked managed temp dir (invoked on graceful shutdown). */
252
+ export async function disposeAllManagedTempDirs() {
253
+ await Promise.allSettled([...managedTempDirs].map((d) => d.dispose()));
254
+ }
255
+ /**
256
+ * Create a temp directory that is tracked and auto-removed on graceful exit
257
+ * (and via the {@link Disposable} registry). Preflights {@link ensureFreeSpace}
258
+ * on the parent so we fail fast instead of mkdtemp'ing into a full disk.
259
+ *
260
+ * Pair this with a startup {@link sweepOrphans} on the same `parentDir`/`prefix`
261
+ * as the durable backstop for crashed runs that never reach `dispose()`.
262
+ */
263
+ export async function createManagedTempDir(prefix, opts = {}) {
264
+ const parent = opts.parentDir ?? tmpdir();
265
+ ensureFreeSpace(parent, opts.minFreeBytes, { label: `createManagedTempDir(${prefix})` });
266
+ const dir = await mkdtemp(join(parent, prefix));
267
+ const handle = new ManagedTempDirImpl(dir);
268
+ managedTempDirs.add(handle);
269
+ registerDisposable(handle);
270
+ ensureShutdownHandler();
271
+ return handle;
272
+ }
273
+ /**
274
+ * Tracks cumulative bytes written against a quota
275
+ * (default {@link DISK_GUARD_DEFAULTS.quotaMb} → `CODEBUDDY_DISK_QUOTA_MB`).
276
+ * A quota of 0 means unlimited — every reservation is allowed.
277
+ */
278
+ export class DiskBudget {
279
+ quotaBytes;
280
+ usedBytes = 0;
281
+ constructor(quotaBytes) {
282
+ this.quotaBytes = quotaBytes ?? defaultQuotaBytes();
283
+ }
284
+ /** Check (without recording) whether `bytes` more would fit. */
285
+ reserve(bytes) {
286
+ if (this.quotaBytes <= 0)
287
+ return { allowed: true };
288
+ if (this.usedBytes + bytes > this.quotaBytes) {
289
+ return {
290
+ allowed: false,
291
+ reason: `disk budget exceeded: ${formatMb(this.usedBytes + bytes)} would exceed ` +
292
+ `quota ${formatMb(this.quotaBytes)}`,
293
+ };
294
+ }
295
+ return { allowed: true };
296
+ }
297
+ record(bytes) {
298
+ this.usedBytes += bytes;
299
+ }
300
+ release(bytes) {
301
+ this.usedBytes = Math.max(0, this.usedBytes - bytes);
302
+ }
303
+ used() {
304
+ return this.usedBytes;
305
+ }
306
+ remaining() {
307
+ return this.quotaBytes <= 0 ? Infinity : Math.max(0, this.quotaBytes - this.usedBytes);
308
+ }
309
+ }
310
+ // ---------------------------------------------------------------------------
311
+ // Bounded writes + log rotation
312
+ // ---------------------------------------------------------------------------
313
+ function rotatedPath(filePath, index) {
314
+ const ext = extname(filePath);
315
+ const base = filePath.slice(0, filePath.length - ext.length);
316
+ return `${base}.${index}${ext}`;
317
+ }
318
+ /**
319
+ * Rotate `filePath` when it reaches `maxBytes`, keeping at most `maxFiles`
320
+ * generations (`name.1.ext` … `name.N.ext`, oldest dropped). Ports the
321
+ * algorithm in logger.ts so unbounded append logs can't fill the disk.
322
+ * Returns true if a rotation happened. Never throws.
323
+ */
324
+ export function rotateIfLarge(filePath, maxBytes, maxFiles) {
325
+ const max = maxBytes ?? defaultLogMaxBytes();
326
+ const keep = maxFiles ?? defaultLogMaxFiles();
327
+ try {
328
+ const st = statSync(filePath);
329
+ if (st.size < max)
330
+ return false;
331
+ }
332
+ catch {
333
+ return false; // file doesn't exist yet → nothing to rotate
334
+ }
335
+ // Shift existing rotated files (N -> N+1), dropping anything beyond `keep`.
336
+ for (let i = keep - 1; i >= 1; i--) {
337
+ const src = rotatedPath(filePath, i);
338
+ const dst = rotatedPath(filePath, i + 1);
339
+ try {
340
+ if (existsSync(src)) {
341
+ if (i + 1 > keep)
342
+ unlinkSync(src);
343
+ else
344
+ renameSync(src, dst);
345
+ }
346
+ }
347
+ catch {
348
+ /* ignore individual file rotation errors */
349
+ }
350
+ }
351
+ try {
352
+ const overflow = rotatedPath(filePath, keep + 1);
353
+ if (existsSync(overflow))
354
+ unlinkSync(overflow);
355
+ }
356
+ catch {
357
+ /* ignore */
358
+ }
359
+ try {
360
+ renameSync(filePath, rotatedPath(filePath, 1));
361
+ }
362
+ catch {
363
+ /* if rename fails, continue appending to the existing file */
364
+ }
365
+ return true;
366
+ }
367
+ /**
368
+ * Write a file after preflighting free space (and, if a {@link DiskBudget} is
369
+ * supplied, reserving + recording its size). Throws {@link DiskSpaceError} or
370
+ * {@link DiskBudgetError} before touching disk when bounds would be breached.
371
+ */
372
+ export async function boundedWriteFile(filePath, data, opts = {}) {
373
+ ensureFreeSpace(dirname(filePath), opts.minFreeBytes, { label: `write ${basename(filePath)}` });
374
+ const size = byteLength(data);
375
+ if (opts.budget) {
376
+ const res = opts.budget.reserve(size);
377
+ if (!res.allowed) {
378
+ throw new DiskBudgetError(res.reason ?? 'disk budget exceeded', {
379
+ usedBytes: opts.budget.used(),
380
+ quotaBytes: opts.budget.used() + opts.budget.remaining(),
381
+ requestedBytes: size,
382
+ });
383
+ }
384
+ }
385
+ await writeFile(filePath, data);
386
+ opts.budget?.record(size);
387
+ }
388
+ /**
389
+ * Append to a file after optionally rotating it ({@link rotateIfLarge}) and
390
+ * preflighting free space. The append-log counterpart to {@link boundedWriteFile}.
391
+ */
392
+ export async function boundedAppendFile(filePath, data, opts = {}) {
393
+ if (opts.rotate) {
394
+ rotateIfLarge(filePath, opts.rotate.maxBytes, opts.rotate.maxFiles);
395
+ }
396
+ ensureFreeSpace(dirname(filePath), opts.minFreeBytes, { label: `append ${basename(filePath)}` });
397
+ await appendFile(filePath, data);
398
+ }
399
+ /** Test helper: clear tracked temp dirs + reset the lazy shutdown-handler flag. */
400
+ export function resetDiskGuardForTests() {
401
+ managedTempDirs.clear();
402
+ shutdownHandlerRegistered = false;
403
+ }
404
+ //# sourceMappingURL=disk-guard.js.map
@@ -12,6 +12,7 @@ import { logger } from "./logger.js";
12
12
  import path from 'path';
13
13
  import readline from 'readline';
14
14
  import { getCodeBuddyHome, ensureCodeBuddyHome } from './codebuddy-home.js';
15
+ import { getApiKey as getStoredApiKey, setApiKey as setStoredApiKey, } from '../security/credential-manager.js';
15
16
  // ============================================================================
16
17
  // Readline Utilities
17
18
  // ============================================================================
@@ -208,6 +209,15 @@ export async function runSetup() {
208
209
  * Load existing API key from settings
209
210
  */
210
211
  function loadExistingApiKey() {
212
+ // Encrypted credential store first; fall back to a legacy plaintext key.
213
+ try {
214
+ const stored = getStoredApiKey();
215
+ if (stored)
216
+ return stored;
217
+ }
218
+ catch {
219
+ // Ignore — fall through to the legacy settings file.
220
+ }
211
221
  try {
212
222
  const settingsPath = path.join(getCodeBuddyHome(), 'user-settings.json');
213
223
  if (fs.existsSync(settingsPath)) {
@@ -232,9 +242,25 @@ async function saveConfig(config) {
232
242
  if (fs.existsSync(settingsPath)) {
233
243
  settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
234
244
  }
235
- // Update settings
245
+ // Update settings.
246
+ // API keys are secrets: persist them to the encrypted credential store,
247
+ // NEVER plaintext in user-settings.json. Mirrors the onboarding wizard
248
+ // (src/wizard/onboarding.ts → persistProviderSelection).
236
249
  if (config.apiKey) {
237
- settings.apiKey = config.apiKey;
250
+ try {
251
+ setStoredApiKey(config.apiKey);
252
+ }
253
+ catch (err) {
254
+ logger.warn('Could not persist API key to the credential store', {
255
+ error: err instanceof Error ? err.message : String(err),
256
+ });
257
+ }
258
+ if (!process.env.GROK_API_KEY)
259
+ process.env.GROK_API_KEY = config.apiKey;
260
+ }
261
+ // Migrate any legacy plaintext key out of the settings file.
262
+ if (typeof settings.apiKey === 'string') {
263
+ delete settings.apiKey;
238
264
  }
239
265
  if (config.baseURL) {
240
266
  settings.baseURL = config.baseURL;
@@ -263,6 +289,13 @@ export function needsSetup() {
263
289
  if (process.env.GROK_API_KEY) {
264
290
  return false;
265
291
  }
292
+ try {
293
+ if (getStoredApiKey())
294
+ return false;
295
+ }
296
+ catch {
297
+ // Ignore — fall through to the legacy settings file.
298
+ }
266
299
  try {
267
300
  const settingsPath = path.join(getCodeBuddyHome(), 'user-settings.json');
268
301
  if (fs.existsSync(settingsPath)) {
@@ -9,13 +9,22 @@ import { ValidationError } from './errors.js';
9
9
  export function isSupportedModel(model) {
10
10
  return model in SUPPORTED_MODELS;
11
11
  }
12
+ function resolveSupportedModelName(model) {
13
+ if (isSupportedModel(model))
14
+ return model;
15
+ const withoutLatest = model.endsWith(':latest') ? model.slice(0, -':latest'.length) : '';
16
+ if (withoutLatest && isSupportedModel(withoutLatest))
17
+ return withoutLatest;
18
+ return null;
19
+ }
12
20
  /**
13
21
  * Get information about a model
14
22
  */
15
23
  export function getModelInfo(model) {
16
- if (isSupportedModel(model)) {
24
+ const supportedModel = resolveSupportedModelName(model);
25
+ if (supportedModel) {
17
26
  return {
18
- ...SUPPORTED_MODELS[model],
27
+ ...SUPPORTED_MODELS[supportedModel],
19
28
  isSupported: true,
20
29
  };
21
30
  }
@@ -30,6 +30,14 @@ const CHATML_TOKEN_PATTERN = /<\|(?:im_start|im_end|endoftext|assistant|user|sys
30
30
  * Kept as a separate pass to avoid over-matching legitimate content.
31
31
  */
32
32
  const GENERIC_CONTROL_TOKEN_PATTERN = /<\|[^|>]+\|>/g;
33
+ /**
34
+ * Some local OpenAI-compatible runtimes leak malformed channel tokens that
35
+ * miss one side of the ChatML pipe pair, e.g. `thought\n<channel|>42` or
36
+ * `<|tool_call>...`. Require at least one pipe so legitimate XML/HTML tags
37
+ * such as `<message>` are preserved.
38
+ */
39
+ const LOCAL_CHANNEL_HEADER_PATTERN = /^(?:thought|analysis|commentary)\s*\n\s*(?=<(?:\|(?:channel|message|tool_call|constrain)\|?|(?:channel|message|tool_call|constrain)\|)>)/i;
40
+ const MALFORMED_PIPE_CONTROL_TOKEN_PATTERN = /<(?:\|(?:channel|message|tool_call|constrain)\|?|(?:channel|message|tool_call|constrain)\|)>/gi;
33
41
  /**
34
42
  * JSON-escaped control tokens: \u003c|token|\u003e
35
43
  */
@@ -67,6 +75,9 @@ const SANITIZATION_RULES = [
67
75
  { pattern: CHATML_TOKEN_PATTERN, replacement: '' },
68
76
  // Generic <|...|> control tokens
69
77
  { pattern: GENERIC_CONTROL_TOKEN_PATTERN, replacement: '' },
78
+ // Local malformed channel/control markers
79
+ { pattern: LOCAL_CHANNEL_HEADER_PATTERN, replacement: '' },
80
+ { pattern: MALFORMED_PIPE_CONTROL_TOKEN_PATTERN, replacement: '' },
70
81
  // JSON-escaped control tokens
71
82
  { pattern: JSON_ESCAPED_CONTROL_TOKEN_PATTERN, replacement: '' },
72
83
  // LLaMA system prompt blocks
@@ -5,21 +5,22 @@
5
5
  * a Code Buddy session. Pure function (no side effects beyond stat/read
6
6
  * on the OAuth file) so it's unit-testable.
7
7
  *
8
- * Priority order:
8
+ * Priority order is defined in `src/providers/provider-catalog.ts`:
9
9
  * 0. CODEBUDDY_PROVIDER override (always wins when set + valid)
10
10
  * 1. ChatGPT OAuth credentials present (~/.codebuddy/codex-auth.json)
11
11
  * → explicit "I logged in" act beats ambient env vars
12
- * 2. OLLAMA_HOST → ollama (local, free, unlimited)
13
- * 3. GROK_API_KEY → grok / OpenAI-compat (incl. xAI)
14
- * 4. GEMINI/GOOGLE → gemini
15
- * 5. OPENAI → openai
16
- * 6. ANTHROPIC → anthropic
12
+ * 2. Local providers (Ollama / LM Studio / vLLM)
13
+ * 3. Cloud providers in catalog priority order
17
14
  * else null (no provider available)
18
15
  */
16
+ import { type RuntimeProviderId, type ProviderApiMode, type ProviderAuthMode } from '../providers/provider-catalog.js';
19
17
  export interface DetectedProvider {
20
- provider: 'gemini' | 'grok' | 'openai' | 'anthropic' | 'ollama' | 'chatgpt' | 'unknown';
18
+ provider: RuntimeProviderId | 'unknown';
21
19
  apiKey: string;
22
20
  baseURL: string;
23
21
  defaultModel: string;
22
+ apiMode?: ProviderApiMode;
23
+ authMode?: ProviderAuthMode;
24
+ source?: 'oauth' | 'environment' | 'override';
24
25
  }
25
26
  export declare function detectProviderFromEnv(): DetectedProvider | null;
@@ -5,93 +5,35 @@
5
5
  * a Code Buddy session. Pure function (no side effects beyond stat/read
6
6
  * on the OAuth file) so it's unit-testable.
7
7
  *
8
- * Priority order:
8
+ * Priority order is defined in `src/providers/provider-catalog.ts`:
9
9
  * 0. CODEBUDDY_PROVIDER override (always wins when set + valid)
10
10
  * 1. ChatGPT OAuth credentials present (~/.codebuddy/codex-auth.json)
11
11
  * → explicit "I logged in" act beats ambient env vars
12
- * 2. OLLAMA_HOST → ollama (local, free, unlimited)
13
- * 3. GROK_API_KEY → grok / OpenAI-compat (incl. xAI)
14
- * 4. GEMINI/GOOGLE → gemini
15
- * 5. OPENAI → openai
16
- * 6. ANTHROPIC → anthropic
12
+ * 2. Local providers (Ollama / LM Studio / vLLM)
13
+ * 3. Cloud providers in catalog priority order
17
14
  * else null (no provider available)
18
15
  */
19
16
  import * as fs from 'fs';
20
17
  import * as path from 'path';
21
18
  import * as os from 'os';
19
+ import { resolveProviderFromCatalog, } from '../providers/provider-catalog.js';
22
20
  export function detectProviderFromEnv() {
23
- const override = process.env.CODEBUDDY_PROVIDER?.toLowerCase();
24
- // ChatGPT subscription auth — explicit login wins over ambient
25
- // env-detected providers. User who ran `buddy login chatgpt` recently
26
- // expects subsequent calls to route through their ChatGPT plan, not
27
- // get hijacked by an OLLAMA_HOST set in their shell rc weeks ago.
28
- if (override === 'chatgpt' || !override) {
29
- try {
30
- const authPath = path.join(os.homedir(), '.codebuddy', 'codex-auth.json');
31
- if (fs.existsSync(authPath)) {
32
- const raw = fs.readFileSync(authPath, 'utf-8').trim();
33
- const parsed = raw ? JSON.parse(raw) : null;
34
- if (parsed?.tokens?.access_token) {
35
- return {
36
- provider: 'chatgpt',
37
- apiKey: 'oauth-chatgpt',
38
- baseURL: 'https://chatgpt.com/backend-api/codex',
39
- defaultModel: process.env.CHATGPT_MODEL || 'gpt-5.5',
40
- };
41
- }
42
- }
43
- }
44
- catch {
45
- // Malformed auth file or unexpected — fall through.
46
- }
47
- }
48
- if (override === 'ollama' || (!override && process.env.OLLAMA_HOST)) {
49
- let host = process.env.OLLAMA_HOST || 'http://localhost:11434';
50
- if (!/^https?:\/\//i.test(host))
51
- host = `http://${host}`;
52
- if (!host.endsWith('/v1'))
53
- host = host.replace(/\/+$/, '') + '/v1';
54
- return {
55
- provider: 'ollama',
56
- apiKey: 'ollama',
57
- baseURL: host,
58
- defaultModel: process.env.GROK_MODEL || process.env.OLLAMA_MODEL || 'qwen2.5-coder:7b',
59
- };
60
- }
61
- if ((override === 'grok' || override === 'xai') ||
62
- (!override && (process.env.GROK_API_KEY || process.env.XAI_API_KEY))) {
63
- return {
64
- provider: 'grok',
65
- apiKey: process.env.GROK_API_KEY || process.env.XAI_API_KEY || '',
66
- baseURL: process.env.GROK_BASE_URL || 'https://api.x.ai/v1',
67
- defaultModel: process.env.GROK_MODEL || 'grok-3-fast',
68
- };
69
- }
70
- if ((override === 'gemini' || override === 'google') ||
71
- (!override && (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY))) {
72
- return {
73
- provider: 'gemini',
74
- apiKey: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || '',
75
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
76
- defaultModel: process.env.GEMINI_MODEL || 'gemini-2.5-flash',
77
- };
78
- }
79
- if (override === 'openai' || (!override && process.env.OPENAI_API_KEY)) {
80
- return {
81
- provider: 'openai',
82
- apiKey: process.env.OPENAI_API_KEY || '',
83
- baseURL: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
84
- defaultModel: process.env.OPENAI_MODEL || 'gpt-4o',
85
- };
21
+ return resolveProviderFromCatalog({
22
+ hasChatGptOAuth: hasChatGptOAuthCredentials(),
23
+ });
24
+ }
25
+ function hasChatGptOAuthCredentials() {
26
+ try {
27
+ const authPath = path.join(os.homedir(), '.codebuddy', 'codex-auth.json');
28
+ if (!fs.existsSync(authPath))
29
+ return false;
30
+ const raw = fs.readFileSync(authPath, 'utf-8').trim();
31
+ const parsed = raw ? JSON.parse(raw) : null;
32
+ return Boolean(parsed?.tokens?.access_token);
86
33
  }
87
- if (override === 'anthropic' || (!override && process.env.ANTHROPIC_API_KEY)) {
88
- return {
89
- provider: 'anthropic',
90
- apiKey: process.env.ANTHROPIC_API_KEY || '',
91
- baseURL: 'https://api.anthropic.com/v1',
92
- defaultModel: process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514',
93
- };
34
+ catch {
35
+ // Malformed auth file or unexpected — fall through to env providers.
36
+ return false;
94
37
  }
95
- return null;
96
38
  }
97
39
  //# sourceMappingURL=provider-detector.js.map
@@ -20,6 +20,13 @@ export interface ProjectSettings {
20
20
  model?: string;
21
21
  mcpServers?: Record<string, unknown>;
22
22
  thinkingLevel?: 'off' | 'minimal' | 'low' | 'medium' | 'high';
23
+ goals?: {
24
+ maxTurns?: number;
25
+ judgeModel?: string;
26
+ plannerModel?: string;
27
+ judgeMaxTokens?: number;
28
+ judgeTimeoutMs?: number;
29
+ };
23
30
  }
24
31
  /**
25
32
  * Unified settings manager that handles both user-level and project-level settings