@phuetz/code-buddy 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -163
- package/dist/agent/autonomous/agentic-coding-contract.d.ts +12 -12
- package/dist/agent/base-agent.d.ts +4 -0
- package/dist/agent/base-agent.js +6 -0
- package/dist/agent/codebuddy-agent.d.ts +1 -1
- package/dist/agent/codebuddy-agent.js +69 -14
- 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/facades/infrastructure-facade.d.ts +9 -2
- package/dist/agent/facades/infrastructure-facade.js +15 -6
- 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/middleware/index.d.ts +1 -0
- package/dist/agent/middleware/index.js +1 -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/self-improvement/authored-artifact-gate.d.ts +18 -0
- package/dist/agent/self-improvement/authored-artifact-gate.js +42 -0
- package/dist/agent/self-improvement/authored-tool-runtime.d.ts +27 -0
- package/dist/agent/self-improvement/authored-tool-runtime.js +57 -0
- package/dist/agent/self-improvement/authored-tool-store.d.ts +24 -0
- package/dist/agent/self-improvement/authored-tool-store.js +57 -0
- package/dist/agent/self-improvement/llm-tool-proposer.d.ts +41 -0
- package/dist/agent/self-improvement/llm-tool-proposer.js +136 -0
- package/dist/agent/self-improvement/sandbox-scorer.d.ts +17 -0
- package/dist/agent/self-improvement/sandbox-scorer.js +43 -0
- package/dist/agent/self-improvement/self-knowledge.d.ts +8 -0
- package/dist/agent/self-improvement/self-knowledge.js +24 -0
- package/dist/agent/self-improvement/skill-benchmark.d.ts +9 -0
- package/dist/agent/self-improvement/skill-benchmark.js +22 -0
- package/dist/agent/self-improvement/skill-consolidator.d.ts +71 -0
- package/dist/agent/self-improvement/skill-consolidator.js +137 -0
- package/dist/agent/self-improvement/skill-engine.d.ts +42 -0
- package/dist/agent/self-improvement/skill-engine.js +87 -0
- package/dist/agent/self-improvement/skill-gate.d.ts +19 -0
- package/dist/agent/self-improvement/skill-gate.js +62 -0
- package/dist/agent/self-improvement/skill-mutator.d.ts +74 -0
- package/dist/agent/self-improvement/skill-mutator.js +223 -0
- package/dist/agent/self-improvement/skill-proposer.d.ts +40 -0
- package/dist/agent/self-improvement/skill-proposer.js +82 -0
- package/dist/agent/self-improvement/skill-types.d.ts +41 -0
- package/dist/agent/self-improvement/skill-types.js +13 -0
- package/dist/agent/self-improvement/tool-benchmark.d.ts +10 -0
- package/dist/agent/self-improvement/tool-benchmark.js +37 -0
- package/dist/agent/self-improvement/tool-engine.d.ts +54 -0
- package/dist/agent/self-improvement/tool-engine.js +101 -0
- package/dist/agent/self-improvement/tool-gate.d.ts +20 -0
- package/dist/agent/self-improvement/tool-gate.js +78 -0
- package/dist/agent/self-improvement/tool-proposer.d.ts +31 -0
- package/dist/agent/self-improvement/tool-proposer.js +34 -0
- package/dist/agent/self-improvement/tool-skill-mutator.d.ts +40 -0
- package/dist/agent/self-improvement/tool-skill-mutator.js +79 -0
- package/dist/agent/self-improvement/tool-types.d.ts +48 -0
- package/dist/agent/self-improvement/tool-types.js +9 -0
- package/dist/agent/self-improvement/types.d.ts +3 -1
- package/dist/agent/session-end-flush.d.ts +6 -1
- package/dist/agent/session-end-flush.js +20 -4
- 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 +133 -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 +7 -1
- 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 +8 -1
- package/dist/codebuddy/tools.js +67 -24
- package/dist/commands/cli/hermes-commands.js +108 -0
- package/dist/commands/cli/improve-command.js +123 -0
- package/dist/commands/cli/native-engine-commands.js +102 -3
- package/dist/commands/cli/utility-commands.js +45 -0
- package/dist/commands/enhanced-command-handler.js +2 -2
- package/dist/commands/goal-cli.d.ts +30 -0
- package/dist/commands/goal-cli.js +210 -27
- package/dist/commands/handlers/goal-handler.d.ts +9 -2
- package/dist/commands/handlers/goal-handler.js +36 -9
- package/dist/commands/handlers/infra-handlers.js +36 -0
- package/dist/commands/handlers/memory-handlers.js +96 -1
- package/dist/commands/handlers/missing-handlers.d.ts +1 -1
- package/dist/commands/handlers/missing-handlers.js +26 -3
- 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/skills-cli/index.js +123 -0
- package/dist/commands/slash/builtin-commands.js +1 -1
- 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/companion/percepts.js +11 -1
- 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 +756 -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/bootstrap-loader.js +6 -23
- package/dist/context/context-manager-v2.js +1 -0
- package/dist/context/import-directive-parser.d.ts +4 -0
- package/dist/context/import-directive-parser.js +51 -6
- package/dist/context/instruction-excludes.d.ts +30 -1
- package/dist/context/instruction-excludes.js +71 -1
- package/dist/context/jit-context.d.ts +8 -10
- package/dist/context/jit-context.js +28 -106
- package/dist/context/project-context.d.ts +90 -0
- package/dist/context/project-context.js +295 -0
- package/dist/daemon/autonomous-daemon.d.ts +1 -1
- package/dist/daemon/autonomous-daemon.js +3 -3
- package/dist/daemon/autonomous-loop.d.ts +31 -1
- package/dist/daemon/autonomous-loop.js +91 -4
- package/dist/daemon/colab-goal.js +9 -1
- 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.js +14 -2
- package/dist/fleet/peer-session-bridge.js +50 -12
- package/dist/fleet/peer-tool-bridge.js +14 -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 +2 -0
- package/dist/goals/goal-judge.js +35 -6
- package/dist/goals/goal-loop.d.ts +14 -0
- package/dist/goals/goal-loop.js +47 -2
- package/dist/goals/goal-manager.d.ts +6 -0
- package/dist/goals/goal-manager.js +67 -14
- package/dist/goals/goal-state.d.ts +13 -0
- package/dist/goals/goal-state.js +83 -13
- package/dist/goals/goal-store.d.ts +3 -0
- package/dist/goals/goal-store.js +31 -15
- package/dist/harness/contract.d.ts +28 -28
- package/dist/hooks/user-hooks.js +17 -3
- package/dist/identity/identity-manager.js +3 -2
- package/dist/index.js +481 -26
- 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-resources.js +2 -3
- 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/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/sensory/dreaming.d.ts +45 -0
- package/dist/sensory/dreaming.js +114 -0
- package/dist/sensory/heartbeat-scheduler.d.ts +38 -0
- package/dist/sensory/heartbeat-scheduler.js +72 -0
- package/dist/sensory/reactions.d.ts +24 -0
- package/dist/sensory/reactions.js +31 -0
- package/dist/sensory/screen-reaction.d.ts +23 -0
- package/dist/sensory/screen-reaction.js +59 -0
- package/dist/sensory/sensory-bridge.d.ts +23 -0
- package/dist/sensory/sensory-bridge.js +85 -0
- package/dist/sensory/sensory-memory.d.ts +20 -0
- package/dist/sensory/sensory-memory.js +39 -0
- package/dist/sensory/speech-reaction.d.ts +21 -0
- package/dist/sensory/speech-reaction.js +83 -0
- package/dist/sensory/vision-reaction.d.ts +31 -0
- package/dist/sensory/vision-reaction.js +74 -0
- package/dist/server/index.js +106 -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.js +5 -0
- package/dist/services/prompt-builder.d.ts +11 -0
- package/dist/services/prompt-builder.js +124 -17
- 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/skills/parser.js +3 -0
- package/dist/skills/skill-importer.d.ts +58 -0
- package/dist/skills/skill-importer.js +261 -0
- package/dist/skills/skill-sources.d.ts +20 -0
- package/dist/skills/skill-sources.js +102 -0
- package/dist/skills/types.d.ts +6 -0
- 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/register-tool-handler.d.ts +25 -0
- package/dist/tools/register-tool-handler.js +100 -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/registry.d.ts +6 -0
- package/dist/tools/registry.js +8 -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/init-project.d.ts +7 -0
- package/dist/utils/init-project.js +37 -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 +19 -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
|
|
@@ -81,4 +81,11 @@ export declare function initCodeBuddyProject(workingDirectory?: string, options?
|
|
|
81
81
|
* Format init result for display (ASCII markers, no emojis)
|
|
82
82
|
*/
|
|
83
83
|
export declare function formatInitResult(result: InitResult): string;
|
|
84
|
+
/**
|
|
85
|
+
* Build the prompt for the agent-driven `/init`. Instructs the model to analyze
|
|
86
|
+
* the repository and write/improve a tailored root `AGENTS.md`, grounded on the
|
|
87
|
+
* RepoProfiler's verified facts so it cannot hallucinate build/test commands.
|
|
88
|
+
* Modeled on Codex's `prompt_for_init_command.md`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function buildInitPrompt(contextPack: string): string;
|
|
84
91
|
export {};
|
|
@@ -495,6 +495,13 @@ ${archLines.join('\n')}
|
|
|
495
495
|
## More Context
|
|
496
496
|
${moreLines.join('\n')}
|
|
497
497
|
|
|
498
|
+
<sub>**How instruction files load:** Code Buddy reads from \`~/.codebuddy/\` (global), the
|
|
499
|
+
project root, and subdirectories — the file closest to what you're editing wins.
|
|
500
|
+
Accepted names compose in order: **AGENTS.md** (this file, primary), then \`CODEBUDDY.md\`,
|
|
501
|
+
\`CLAUDE.md\`, \`GEMINI.md\`, \`CONTEXT.md\`, \`INSTRUCTIONS.md\`. Drop \`@./path.md\` to import
|
|
502
|
+
another file. Override a file in the same directory with \`AGENTS.override.md\` (committed)
|
|
503
|
+
or \`AGENTS.local.md\` (gitignored).</sub>
|
|
504
|
+
|
|
498
505
|
---
|
|
499
506
|
*Auto-generated by \`buddy init\`. Re-run with \`--force\` to refresh, or edit by hand — \`buddy init\` won't overwrite without explicit force.*
|
|
500
507
|
`;
|
|
@@ -772,6 +779,8 @@ Safety checks:
|
|
|
772
779
|
.codebuddy/tool-results/
|
|
773
780
|
.codebuddy/cache/
|
|
774
781
|
.codebuddy/user-settings.json
|
|
782
|
+
# Personal, uncommitted instruction overrides (AGENTS.local.md, CODEBUDDY.local.md, …)
|
|
783
|
+
*.local.md
|
|
775
784
|
`;
|
|
776
785
|
if (fs.existsSync(gitignorePath)) {
|
|
777
786
|
const currentContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
@@ -897,6 +906,34 @@ export function formatInitResult(result) {
|
|
|
897
906
|
output += ' 5. Use /starter to browse framework starter packs for new projects\n';
|
|
898
907
|
return output;
|
|
899
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Build the prompt for the agent-driven `/init`. Instructs the model to analyze
|
|
911
|
+
* the repository and write/improve a tailored root `AGENTS.md`, grounded on the
|
|
912
|
+
* RepoProfiler's verified facts so it cannot hallucinate build/test commands.
|
|
913
|
+
* Modeled on Codex's `prompt_for_init_command.md`.
|
|
914
|
+
*/
|
|
915
|
+
export function buildInitPrompt(contextPack) {
|
|
916
|
+
const facts = contextPack && contextPack.trim()
|
|
917
|
+
? `\nVERIFIED FACTS about this repository (auto-detected — use these, do not invent commands):\n${contextPack.trim()}\n`
|
|
918
|
+
: '';
|
|
919
|
+
return `Analyze THIS repository and write a tailored \`AGENTS.md\` at the project root — a concise contributor/agent guide (the cross-CLI standard read by Claude Code, Cursor, Codex, Gemini CLI and Code Buddy).
|
|
920
|
+
${facts}
|
|
921
|
+
How to do it:
|
|
922
|
+
1. Explore the real code first — use \`list_directory\`, \`view_file\`, \`search\` (and \`code_graph\` if available) to understand the actual structure, entry points, scripts and conventions. Read \`package.json\` / \`pyproject.toml\` / \`Cargo.toml\`, the README, and a few representative source files.
|
|
923
|
+
2. Trust any auto-detected facts shown above for build/test/lint commands, languages and directories — do NOT invent commands. Confirm anything else by reading the config files.
|
|
924
|
+
3. Write a clear, concise document (aim for 200–400 words, Markdown headings), adapting or omitting sections as appropriate:
|
|
925
|
+
- **Project Structure & Module Organization** — where source, tests and assets live.
|
|
926
|
+
- **Build, Test & Dev Commands** — the real commands, each briefly explained.
|
|
927
|
+
- **Coding Style & Naming Conventions** — indentation, language style, naming, lint/format tools.
|
|
928
|
+
- **Testing Guidelines** — framework, how to run, naming.
|
|
929
|
+
- **Commit & Pull Request Guidelines** — infer conventions from the git history.
|
|
930
|
+
- *(optional)* **Architecture Overview** — for non-obvious design.
|
|
931
|
+
|
|
932
|
+
Important:
|
|
933
|
+
- Write ONLY \`AGENTS.md\` at the project root. Do NOT modify anything under \`.codebuddy/\` — that config was just scaffolded deterministically.
|
|
934
|
+
- If \`AGENTS.md\` already has meaningful content, IMPROVE it in place: preserve the user's good wording, refine and fill gaps; never blank-overwrite.
|
|
935
|
+
- Keep it specific to THIS repository and actionable. No filler.`;
|
|
936
|
+
}
|
|
900
937
|
// ============================================================================
|
|
901
938
|
// Data flow renderer — shows how key components connect
|
|
902
939
|
// ============================================================================
|
|
@@ -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;
|