@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.
- package/README.md +160 -164
- package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
- package/dist/agent/codebuddy-agent.d.ts +1 -1
- package/dist/agent/codebuddy-agent.js +101 -13
- package/dist/agent/execution/agent-executor.js +122 -12
- package/dist/agent/execution/tool-dependency-graph.js +7 -0
- package/dist/agent/execution/tool-hooks.d.ts +6 -0
- package/dist/agent/execution/tool-hooks.js +13 -5
- package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
- package/dist/agent/execution/tool-selection-strategy.js +67 -10
- package/dist/agent/extended-thinking.d.ts +8 -0
- package/dist/agent/extended-thinking.js +27 -0
- package/dist/agent/hermes-browser-backends.js +151 -11
- package/dist/agent/hermes-claw-migrate.d.ts +59 -0
- package/dist/agent/hermes-claw-migrate.js +427 -26
- package/dist/agent/hermes-parity-manifest.js +78 -71
- package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
- package/dist/agent/hermes-runtime-lifecycle.js +468 -0
- package/dist/agent/lesson-auto-proposer.js +10 -0
- package/dist/agent/middleware/index.d.ts +1 -0
- package/dist/agent/middleware/index.js +1 -0
- package/dist/agent/middleware/session-duration.d.ts +36 -0
- package/dist/agent/middleware/session-duration.js +78 -0
- package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
- package/dist/agent/middleware/visual-validation-middleware.js +67 -0
- package/dist/agent/model-benchmark.d.ts +77 -0
- package/dist/agent/model-benchmark.js +309 -0
- package/dist/agent/model-tier.d.ts +14 -0
- package/dist/agent/model-tier.js +71 -0
- package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
- package/dist/agent/multi-agent/multi-agent-system.js +3 -1
- package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
- package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
- package/dist/agent/multi-agent/session-tools.js +22 -5
- package/dist/agent/reasoning/mcts.js +20 -6
- package/dist/agent/session-end-flush.d.ts +71 -0
- package/dist/agent/session-end-flush.js +233 -0
- package/dist/agent/streaming/streaming-handler.d.ts +7 -0
- package/dist/agent/streaming/streaming-handler.js +8 -1
- package/dist/agent/tool-executor.js +1 -0
- package/dist/agent/tool-handler.d.ts +5 -0
- package/dist/agent/tool-handler.js +130 -4
- package/dist/browser-automation/browser-operator-executor.js +15 -0
- package/dist/browser-automation/browser-use-runner.d.ts +96 -0
- package/dist/browser-automation/browser-use-runner.js +492 -0
- package/dist/browser-automation/camofox-runner.d.ts +107 -0
- package/dist/browser-automation/camofox-runner.js +287 -0
- package/dist/channels/dingtalk/index.js +3 -0
- package/dist/channels/discord/client.d.ts +9 -0
- package/dist/channels/discord/client.js +12 -0
- package/dist/channels/feishu/index.d.ts +167 -1
- package/dist/channels/feishu/index.js +432 -7
- package/dist/channels/gateway-lifecycle.d.ts +132 -0
- package/dist/channels/gateway-lifecycle.js +219 -0
- package/dist/channels/google-chat/index.d.ts +5 -1
- package/dist/channels/google-chat/index.js +5 -1
- package/dist/channels/imessage/index.d.ts +14 -0
- package/dist/channels/imessage/index.js +64 -17
- package/dist/channels/index.d.ts +4 -0
- package/dist/channels/index.js +4 -0
- package/dist/channels/irc/index.d.ts +73 -3
- package/dist/channels/irc/index.js +446 -11
- package/dist/channels/line/index.js +3 -0
- package/dist/channels/mattermost/index.d.ts +51 -4
- package/dist/channels/mattermost/index.js +303 -20
- package/dist/channels/nextcloud-talk/index.d.ts +89 -8
- package/dist/channels/nextcloud-talk/index.js +367 -16
- package/dist/channels/nostr/index.d.ts +121 -1
- package/dist/channels/nostr/index.js +396 -8
- package/dist/channels/ntfy/index.js +3 -0
- package/dist/channels/qq/index.js +3 -0
- package/dist/channels/slash-parity.d.ts +117 -0
- package/dist/channels/slash-parity.js +185 -0
- package/dist/channels/synology-chat/index.js +3 -0
- package/dist/channels/teams/index.d.ts +3 -0
- package/dist/channels/teams/index.js +3 -0
- package/dist/channels/telegram/client.d.ts +9 -0
- package/dist/channels/telegram/client.js +12 -0
- package/dist/channels/twilio-voice/index.js +3 -0
- package/dist/channels/webchat/index.d.ts +7 -1
- package/dist/channels/webchat/index.js +7 -1
- package/dist/channels/wecom/index.js +3 -0
- package/dist/channels/weixin/index.js +3 -0
- package/dist/channels/zalo/index.js +3 -0
- package/dist/cli/config-loader.js +1 -1
- package/dist/codebuddy/client.d.ts +30 -1
- package/dist/codebuddy/client.js +167 -20
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
- package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
- package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
- package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
- package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
- package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
- package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
- package/dist/codebuddy/tool-definitions/index.js +2 -1
- package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
- package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
- package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
- package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
- package/dist/codebuddy/tools.d.ts +1 -1
- package/dist/codebuddy/tools.js +27 -24
- package/dist/commands/cli/hermes-commands.js +108 -0
- package/dist/commands/cli/native-engine-commands.js +107 -3
- package/dist/commands/cli/utility-commands.js +45 -0
- package/dist/commands/enhanced-command-handler.js +5 -0
- package/dist/commands/goal-cli.d.ts +71 -0
- package/dist/commands/goal-cli.js +280 -0
- package/dist/commands/handlers/goal-handler.d.ts +34 -0
- package/dist/commands/handlers/goal-handler.js +155 -0
- package/dist/commands/handlers/index.d.ts +1 -0
- package/dist/commands/handlers/index.js +2 -0
- package/dist/commands/handlers/infra-handlers.js +36 -0
- package/dist/commands/handlers/memory-handlers.js +96 -1
- package/dist/commands/headless-slash.d.ts +2 -0
- package/dist/commands/headless-slash.js +12 -0
- package/dist/commands/llm-provider-resolution.d.ts +5 -3
- package/dist/commands/llm-provider-resolution.js +87 -33
- package/dist/commands/ollama.d.ts +25 -0
- package/dist/commands/ollama.js +100 -0
- package/dist/commands/provider.d.ts +5 -0
- package/dist/commands/provider.js +106 -55
- package/dist/commands/slash/builtin-commands.js +20 -0
- package/dist/commands/spec-next.js +2 -1
- package/dist/commands/spec-plan.js +5 -16
- package/dist/commands/tunnel.d.ts +2 -0
- package/dist/commands/tunnel.js +48 -0
- package/dist/config/config-resolver.d.ts +2 -1
- package/dist/config/config-resolver.js +54 -41
- package/dist/config/constants.d.ts +28 -0
- package/dist/config/constants.js +7 -0
- package/dist/config/env-schema.js +770 -0
- package/dist/config/feature-flags.js +7 -0
- package/dist/config/model-tools.js +4 -4
- package/dist/config/toml-config.d.ts +16 -0
- package/dist/config/toml-config.js +3 -0
- package/dist/context/context-manager-v2.d.ts +39 -0
- package/dist/context/context-manager-v2.js +91 -0
- package/dist/daemon/agent-task-executor.js +12 -1
- package/dist/daemon/autonomous-daemon.d.ts +1 -1
- package/dist/daemon/autonomous-daemon.js +5 -3
- package/dist/daemon/autonomous-loop.d.ts +21 -1
- package/dist/daemon/autonomous-loop.js +69 -0
- package/dist/daemon/colab-goal.d.ts +38 -0
- package/dist/daemon/colab-goal.js +81 -0
- package/dist/daemon/cron-agent-bridge.d.ts +12 -3
- package/dist/daemon/cron-agent-bridge.js +25 -9
- package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
- package/dist/desktop/codebuddy-engine-adapter.js +257 -80
- package/dist/desktop/engine-adapter.d.ts +14 -0
- package/dist/desktop-automation/automation-manager.js +16 -0
- package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
- package/dist/desktop-automation/omniparser-runner.js +115 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
- package/dist/desktop-automation/smart-snapshot.js +72 -5
- package/dist/doctor/index.js +40 -23
- package/dist/events/types.d.ts +11 -0
- package/dist/fleet/colab-store.d.ts +21 -0
- package/dist/fleet/colab-store.js +28 -0
- package/dist/fleet/peer-session-bridge.d.ts +1 -1
- package/dist/fleet/peer-session-bridge.js +243 -2
- package/dist/fleet/peer-session-store.d.ts +3 -0
- package/dist/fleet/peer-tool-bridge.js +14 -0
- package/dist/fleet/privacy-lint.d.ts +8 -0
- package/dist/fleet/privacy-lint.js +22 -0
- package/dist/goals/goal-decomposer.d.ts +27 -0
- package/dist/goals/goal-decomposer.js +293 -0
- package/dist/goals/goal-judge-client.d.ts +15 -0
- package/dist/goals/goal-judge-client.js +45 -0
- package/dist/goals/goal-judge.d.ts +38 -0
- package/dist/goals/goal-judge.js +158 -0
- package/dist/goals/goal-loop.d.ts +37 -0
- package/dist/goals/goal-loop.js +101 -0
- package/dist/goals/goal-manager.d.ts +77 -0
- package/dist/goals/goal-manager.js +289 -0
- package/dist/goals/goal-state.d.ts +99 -0
- package/dist/goals/goal-state.js +315 -0
- package/dist/goals/goal-store.d.ts +28 -0
- package/dist/goals/goal-store.js +87 -0
- package/dist/goals/index.d.ts +5 -0
- package/dist/goals/index.js +6 -0
- package/dist/hooks/use-input-handler.js +36 -1
- package/dist/hooks/user-hooks.js +17 -3
- package/dist/index.js +506 -25
- package/dist/input/text-to-speech.d.ts +2 -6
- package/dist/input/text-to-speech.js +2 -27
- package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
- package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
- package/dist/integrations/tailscale.d.ts +13 -0
- package/dist/integrations/tailscale.js +87 -34
- package/dist/knowledge/workspace-indexer.js +53 -9
- package/dist/mcp/client.js +1 -0
- package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
- package/dist/mcp/mcp-desktop-tools.js +158 -0
- package/dist/mcp/mcp-server.js +2 -0
- package/dist/memory/enhanced-memory.d.ts +10 -0
- package/dist/memory/enhanced-memory.js +33 -2
- package/dist/memory/index.d.ts +3 -1
- package/dist/memory/index.js +4 -1
- package/dist/memory/memory-auto-proposer.d.ts +23 -0
- package/dist/memory/memory-auto-proposer.js +308 -0
- package/dist/memory/memory-candidate-queue.d.ts +92 -0
- package/dist/memory/memory-candidate-queue.js +261 -0
- package/dist/memory/persistent-memory.d.ts +45 -2
- package/dist/memory/persistent-memory.js +236 -40
- package/dist/ml/bayesian-qualifier.d.ts +1 -1
- package/dist/ml/bayesian-qualifier.js +35 -2
- package/dist/observability/run-store.d.ts +1 -1
- package/dist/openclaw/gateway-bridge.js +5 -0
- package/dist/plugins/marketplace.d.ts +1 -0
- package/dist/plugins/marketplace.js +7 -0
- package/dist/prompts/execution-discipline.d.ts +14 -0
- package/dist/prompts/execution-discipline.js +29 -0
- package/dist/prompts/variation-injector.js +9 -2
- package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
- package/dist/protocols/acp/acp-agentic-runner.js +115 -18
- package/dist/protocols/acp/acp-session-store.d.ts +23 -0
- package/dist/protocols/acp/acp-session-store.js +77 -0
- package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
- package/dist/protocols/acp/acp-stdio-server.js +71 -16
- package/dist/providers/active-llm-registry.d.ts +37 -0
- package/dist/providers/active-llm-registry.js +186 -0
- package/dist/providers/auxiliary-provider.d.ts +25 -0
- package/dist/providers/auxiliary-provider.js +192 -0
- package/dist/providers/codex-oauth.d.ts +1 -1
- package/dist/providers/codex-oauth.js +27 -4
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/provider-catalog.d.ts +70 -0
- package/dist/providers/provider-catalog.js +738 -0
- package/dist/providers/provider-fallback.d.ts +35 -0
- package/dist/providers/provider-fallback.js +171 -0
- package/dist/providers/xai-oauth.d.ts +128 -0
- package/dist/providers/xai-oauth.js +735 -0
- package/dist/scheduler/cron-scheduler.d.ts +15 -3
- package/dist/scheduler/cron-scheduler.js +53 -7
- package/dist/scheduler/script-runner.d.ts +2 -0
- package/dist/scheduler/script-runner.js +19 -4
- package/dist/scheduler/watchdog-handlers.js +7 -20
- package/dist/search/usearch-index.js +7 -2
- package/dist/security/tool-policy/tool-groups.js +2 -0
- package/dist/server/index.js +17 -2
- package/dist/server/routes/mobile.d.ts +12 -4
- package/dist/server/routes/mobile.js +116 -1
- package/dist/server/tls-config.d.ts +35 -0
- package/dist/server/tls-config.js +142 -0
- package/dist/server/tunnel-manager.d.ts +20 -0
- package/dist/server/tunnel-manager.js +58 -0
- package/dist/server/websocket/fleet-bridge.d.ts +13 -1
- package/dist/server/websocket/fleet-bridge.js +16 -0
- package/dist/services/prompt-builder.d.ts +1 -0
- package/dist/services/prompt-builder.js +49 -8
- package/dist/shared/engine-types.d.ts +15 -1
- package/dist/sidecar/sidecar-bridge.d.ts +1 -0
- package/dist/sidecar/sidecar-bridge.js +21 -10
- package/dist/spec/spec-store.js +8 -1
- package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
- package/dist/talk-mode/providers/audioreader-tts.js +8 -8
- package/dist/templates/project-scaffolding.js +3 -2
- package/dist/themes/theme-schema.d.ts +10 -10
- package/dist/tools/application-profiles.js +38 -0
- package/dist/tools/bash/bash-tool.d.ts +1 -0
- package/dist/tools/bash/bash-tool.js +16 -2
- package/dist/tools/bash/command-validator.js +3 -0
- package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
- package/dist/tools/bash/rtk-rewrite.js +100 -0
- package/dist/tools/bash/streaming-executor.js +14 -1
- package/dist/tools/computer-control-tool.d.ts +16 -1
- package/dist/tools/computer-control-tool.js +317 -5
- package/dist/tools/document-generator.d.ts +14 -0
- package/dist/tools/document-generator.js +79 -0
- package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
- package/dist/tools/execute-code-rpc-invoker.js +46 -5
- package/dist/tools/metadata.js +28 -0
- package/dist/tools/office-macro-tool.d.ts +10 -0
- package/dist/tools/office-macro-tool.js +93 -0
- package/dist/tools/registry/index.d.ts +2 -1
- package/dist/tools/registry/index.js +2 -1
- package/dist/tools/registry/memory-tools.d.ts +20 -0
- package/dist/tools/registry/memory-tools.js +225 -3
- package/dist/tools/registry/multimodal-tools.js +2 -2
- package/dist/tools/registry/vision-tools.d.ts +33 -0
- package/dist/tools/registry/vision-tools.js +308 -6
- package/dist/tools/registry/windows-tools.d.ts +2 -0
- package/dist/tools/registry/windows-tools.js +47 -0
- package/dist/tools/route-peer-tool.js +14 -0
- package/dist/tools/screenshot-tool.js +14 -2
- package/dist/tools/text-to-speech-tool.d.ts +1 -1
- package/dist/tools/text-to-speech-tool.js +2 -12
- package/dist/tools/tool-selector.js +3 -59
- package/dist/utils/config-validation/schema.d.ts +40 -7
- package/dist/utils/config-validation/schema.js +130 -1
- package/dist/utils/cost-tracker.js +21 -1
- package/dist/utils/disk-guard.d.ts +180 -0
- package/dist/utils/disk-guard.js +404 -0
- package/dist/utils/interactive-setup.js +35 -2
- package/dist/utils/model-utils.js +11 -2
- package/dist/utils/output-sanitizer.js +11 -0
- package/dist/utils/provider-detector.d.ts +8 -7
- package/dist/utils/provider-detector.js +19 -77
- package/dist/utils/settings-manager.d.ts +7 -0
- package/dist/utils/token-display.d.ts +3 -3
- package/dist/utils/token-display.js +22 -4
- package/dist/wizard/onboarding.d.ts +39 -0
- package/dist/wizard/onboarding.js +433 -21
- package/dist/wizard/provider-onboarding.d.ts +8 -23
- package/dist/wizard/provider-onboarding.js +39 -158
- package/package.json +4 -2
- package/dist/plugins/provider-onboarding.d.ts +0 -23
- package/dist/plugins/provider-onboarding.js +0 -116
- package/dist/utils/kokoro-tts.d.ts +0 -8
- 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
|
-
|
|
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
|
-
|
|
24
|
+
const supportedModel = resolveSupportedModelName(model);
|
|
25
|
+
if (supportedModel) {
|
|
17
26
|
return {
|
|
18
|
-
...SUPPORTED_MODELS[
|
|
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.
|
|
13
|
-
* 3.
|
|
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:
|
|
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.
|
|
13
|
-
* 3.
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|