@calliopelabs/cli 2.3.0 → 3.0.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 +45 -644
- package/dist/acp.d.ts +78 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +572 -0
- package/dist/acp.js.map +1 -0
- package/dist/auto-compressor.d.ts.map +1 -1
- package/dist/auto-compressor.js +9 -5
- package/dist/auto-compressor.js.map +1 -1
- package/dist/bin.d.ts +9 -5
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +108 -76
- package/dist/bin.js.map +1 -1
- package/dist/budget.d.ts +64 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +157 -0
- package/dist/budget.js.map +1 -0
- package/dist/checkpoint.d.ts +63 -30
- package/dist/checkpoint.d.ts.map +1 -1
- package/dist/checkpoint.js +249 -150
- package/dist/checkpoint.js.map +1 -1
- package/dist/circuit-breaker/breaker.js.map +1 -1
- package/dist/config.d.ts +86 -81
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +237 -305
- package/dist/config.js.map +1 -1
- package/dist/cost.d.ts +114 -0
- package/dist/cost.d.ts.map +1 -0
- package/dist/cost.js +499 -0
- package/dist/cost.js.map +1 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +53 -11
- package/dist/diff.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +33 -6
- package/dist/errors.js.map +1 -1
- package/dist/files.js.map +1 -1
- package/dist/fleet.d.ts +30 -0
- package/dist/fleet.d.ts.map +1 -0
- package/dist/fleet.js +86 -0
- package/dist/fleet.js.map +1 -0
- package/dist/fuzzy-search.d.ts.map +1 -1
- package/dist/fuzzy-search.js +2 -1
- package/dist/fuzzy-search.js.map +1 -1
- package/dist/git-status.js.map +1 -1
- package/dist/headless.d.ts +1 -2
- package/dist/headless.d.ts.map +1 -1
- package/dist/headless.js +222 -26
- package/dist/headless.js.map +1 -1
- package/dist/hooks.d.ts +8 -2
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +97 -11
- package/dist/hooks.js.map +1 -1
- package/dist/hud/api.d.ts +3 -18
- package/dist/hud/api.d.ts.map +1 -1
- package/dist/hud/api.js +9 -352
- package/dist/hud/api.js.map +1 -1
- package/dist/hud/palettes.d.ts.map +1 -1
- package/dist/hud/palettes.js +0 -180
- package/dist/hud/palettes.js.map +1 -1
- package/dist/hud/skins.d.ts.map +1 -1
- package/dist/hud/skins.js +0 -263
- package/dist/hud/skins.js.map +1 -1
- package/dist/hud/types.d.ts +2 -5
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/iteration-ledger.d.ts.map +1 -1
- package/dist/iteration-ledger.js +3 -2
- package/dist/iteration-ledger.js.map +1 -1
- package/dist/local-model.d.ts +150 -0
- package/dist/local-model.d.ts.map +1 -0
- package/dist/local-model.js +408 -0
- package/dist/local-model.js.map +1 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +32 -10
- package/dist/markdown.js.map +1 -1
- package/dist/mcp.d.ts +35 -5
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +190 -13
- package/dist/mcp.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +6 -4
- package/dist/memory.js.map +1 -1
- package/dist/model-detection.d.ts +14 -1
- package/dist/model-detection.d.ts.map +1 -1
- package/dist/model-detection.js +308 -115
- package/dist/model-detection.js.map +1 -1
- package/dist/parallel-tools.d.ts +9 -1
- package/dist/parallel-tools.d.ts.map +1 -1
- package/dist/parallel-tools.js +6 -5
- package/dist/parallel-tools.js.map +1 -1
- package/dist/plugins.d.ts +68 -0
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +201 -3
- package/dist/plugins.js.map +1 -1
- package/dist/policy.d.ts +43 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +139 -0
- package/dist/policy.js.map +1 -0
- package/dist/project-config.d.ts +1 -2
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +0 -3
- package/dist/project-config.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +36 -2
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/bedrock.d.ts.map +1 -1
- package/dist/providers/bedrock.js +83 -22
- package/dist/providers/bedrock.js.map +1 -1
- package/dist/providers/compat.d.ts +21 -0
- package/dist/providers/compat.d.ts.map +1 -1
- package/dist/providers/compat.js +176 -6
- package/dist/providers/compat.js.map +1 -1
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +19 -4
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +62 -9
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.d.ts +2 -2
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +26 -6
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +14 -10
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +8 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +19 -10
- package/dist/providers/types.js.map +1 -1
- package/dist/replay.d.ts +37 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +168 -0
- package/dist/replay.js.map +1 -0
- package/dist/risk.d.ts.map +1 -1
- package/dist/risk.js +15 -5
- package/dist/risk.js.map +1 -1
- package/dist/router.d.ts +133 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/{model-router.js → router.js} +336 -10
- package/dist/router.js.map +1 -0
- package/dist/runlog.d.ts +185 -0
- package/dist/runlog.d.ts.map +1 -0
- package/dist/runlog.js +398 -0
- package/dist/runlog.js.map +1 -0
- package/dist/{sandbox.d.ts → sandbox/docker.d.ts} +1 -1
- package/dist/sandbox/docker.d.ts.map +1 -0
- package/dist/{sandbox.js → sandbox/docker.js} +1 -1
- package/dist/sandbox/docker.js.map +1 -0
- package/dist/sandbox/index.d.ts +36 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +62 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/{sandbox-native.d.ts → sandbox/native.d.ts} +2 -1
- package/dist/sandbox/native.d.ts.map +1 -0
- package/dist/{sandbox-native.js → sandbox/native.js} +38 -6
- package/dist/sandbox/native.js.map +1 -0
- package/dist/scope.d.ts +10 -0
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +76 -16
- package/dist/scope.js.map +1 -1
- package/dist/scuttlebot/client.d.ts +83 -0
- package/dist/scuttlebot/client.d.ts.map +1 -0
- package/dist/scuttlebot/client.js +350 -0
- package/dist/scuttlebot/client.js.map +1 -0
- package/dist/scuttlebot/config.d.ts +28 -0
- package/dist/scuttlebot/config.d.ts.map +1 -0
- package/dist/scuttlebot/config.js +91 -0
- package/dist/scuttlebot/config.js.map +1 -0
- package/dist/scuttlebot/http-client.d.ts +63 -0
- package/dist/scuttlebot/http-client.d.ts.map +1 -0
- package/dist/scuttlebot/http-client.js +124 -0
- package/dist/scuttlebot/http-client.js.map +1 -0
- package/dist/scuttlebot/index.d.ts +13 -0
- package/dist/scuttlebot/index.d.ts.map +1 -0
- package/dist/scuttlebot/index.js +10 -0
- package/dist/scuttlebot/index.js.map +1 -0
- package/dist/scuttlebot/irc-client.d.ts +124 -0
- package/dist/scuttlebot/irc-client.d.ts.map +1 -0
- package/dist/scuttlebot/irc-client.js +599 -0
- package/dist/scuttlebot/irc-client.js.map +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +22 -54
- package/dist/setup.js.map +1 -1
- package/dist/skills.d.ts +58 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +186 -16
- package/dist/skills.js.map +1 -1
- package/dist/storage.d.ts +0 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +87 -8
- package/dist/storage.js.map +1 -1
- package/dist/streaming.d.ts +25 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +59 -0
- package/dist/streaming.js.map +1 -1
- package/dist/styles.d.ts +6 -0
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +11 -0
- package/dist/styles.js.map +1 -1
- package/dist/summarization.js.map +1 -1
- package/dist/terminal-image.js +1 -1
- package/dist/terminal-image.js.map +1 -1
- package/dist/themes.d.ts +5 -4
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +35 -90
- package/dist/themes.js.map +1 -1
- package/dist/tools.d.ts +27 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +352 -209
- package/dist/tools.js.map +1 -1
- package/dist/trust.d.ts +16 -3
- package/dist/trust.d.ts.map +1 -1
- package/dist/trust.js +23 -4
- package/dist/trust.js.map +1 -1
- package/dist/types.d.ts +4 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +34 -44
- package/dist/types.js.map +1 -1
- package/dist/ui/agent.d.ts +4 -7
- package/dist/ui/agent.d.ts.map +1 -1
- package/dist/ui/agent.js +354 -73
- package/dist/ui/agent.js.map +1 -1
- package/dist/ui/animations.d.ts +37 -0
- package/dist/ui/animations.d.ts.map +1 -0
- package/dist/ui/animations.js +227 -0
- package/dist/ui/animations.js.map +1 -0
- package/dist/ui/chat-input-keys.d.ts +42 -0
- package/dist/ui/chat-input-keys.d.ts.map +1 -0
- package/dist/ui/chat-input-keys.js +360 -0
- package/dist/ui/chat-input-keys.js.map +1 -0
- package/dist/ui/chat-input.d.ts +5 -2
- package/dist/ui/chat-input.d.ts.map +1 -1
- package/dist/ui/chat-input.js +54 -308
- package/dist/ui/chat-input.js.map +1 -1
- package/dist/ui/commands.d.ts +15 -45
- package/dist/ui/commands.d.ts.map +1 -1
- package/dist/ui/commands.js +461 -2636
- package/dist/ui/commands.js.map +1 -1
- package/dist/ui/completions.d.ts +1 -0
- package/dist/ui/completions.d.ts.map +1 -1
- package/dist/ui/completions.js +36 -107
- package/dist/ui/completions.js.map +1 -1
- package/dist/ui/components.d.ts +14 -6
- package/dist/ui/components.d.ts.map +1 -1
- package/dist/ui/components.js +17 -226
- package/dist/ui/components.js.map +1 -1
- package/dist/ui/context.js +6 -6
- package/dist/ui/context.js.map +1 -1
- package/dist/ui/debug-log.d.ts +11 -0
- package/dist/ui/debug-log.d.ts.map +1 -0
- package/dist/ui/debug-log.js +21 -0
- package/dist/ui/debug-log.js.map +1 -0
- package/dist/ui/error-boundary.d.ts.map +1 -1
- package/dist/ui/error-boundary.js.map +1 -1
- package/dist/ui/frame.d.ts.map +1 -1
- package/dist/ui/frame.js +1 -3
- package/dist/ui/frame.js.map +1 -1
- package/dist/ui/index.d.ts +11 -3
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +46 -887
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/input-utils.d.ts +20 -0
- package/dist/ui/input-utils.d.ts.map +1 -0
- package/dist/ui/input-utils.js +35 -0
- package/dist/ui/input-utils.js.map +1 -0
- package/dist/ui/messages.d.ts +8 -7
- package/dist/ui/messages.d.ts.map +1 -1
- package/dist/ui/messages.js +51 -35
- package/dist/ui/messages.js.map +1 -1
- package/dist/ui/modals/index.d.ts +11 -0
- package/dist/ui/modals/index.d.ts.map +1 -0
- package/dist/ui/modals/index.js +10 -0
- package/dist/ui/modals/index.js.map +1 -0
- package/dist/ui/{modals.d.ts → modals/prompts.d.ts} +5 -20
- package/dist/ui/modals/prompts.d.ts.map +1 -0
- package/dist/ui/{modals.js → modals/prompts.js} +14 -94
- package/dist/ui/modals/prompts.js.map +1 -0
- package/dist/ui/modals/selectors.d.ts +33 -0
- package/dist/ui/modals/selectors.d.ts.map +1 -0
- package/dist/ui/modals/selectors.js +111 -0
- package/dist/ui/modals/selectors.js.map +1 -0
- package/dist/ui/modals/setup.d.ts +17 -0
- package/dist/ui/modals/setup.d.ts.map +1 -0
- package/dist/ui/modals/setup.js +51 -0
- package/dist/ui/modals/setup.js.map +1 -0
- package/dist/ui/regions/input-region.d.ts +40 -0
- package/dist/ui/regions/input-region.d.ts.map +1 -0
- package/dist/ui/regions/input-region.js +116 -0
- package/dist/ui/regions/input-region.js.map +1 -0
- package/dist/ui/regions/modal-host.d.ts +41 -0
- package/dist/ui/regions/modal-host.d.ts.map +1 -0
- package/dist/ui/regions/modal-host.js +51 -0
- package/dist/ui/regions/modal-host.js.map +1 -0
- package/dist/ui/regions/render-probe.d.ts +22 -0
- package/dist/ui/regions/render-probe.d.ts.map +1 -0
- package/dist/ui/regions/render-probe.js +45 -0
- package/dist/ui/regions/render-probe.js.map +1 -0
- package/dist/ui/regions/static-scrollback.d.ts +39 -0
- package/dist/ui/regions/static-scrollback.d.ts.map +1 -0
- package/dist/ui/regions/static-scrollback.js +68 -0
- package/dist/ui/regions/static-scrollback.js.map +1 -0
- package/dist/ui/regions/status-region.d.ts +24 -0
- package/dist/ui/regions/status-region.d.ts.map +1 -0
- package/dist/ui/regions/status-region.js +17 -0
- package/dist/ui/regions/status-region.js.map +1 -0
- package/dist/ui/regions/transcript-region.d.ts +41 -0
- package/dist/ui/regions/transcript-region.d.ts.map +1 -0
- package/dist/ui/regions/transcript-region.js +52 -0
- package/dist/ui/regions/transcript-region.js.map +1 -0
- package/dist/ui/self-restart.d.ts +9 -0
- package/dist/ui/self-restart.d.ts.map +1 -0
- package/dist/ui/self-restart.js +24 -0
- package/dist/ui/self-restart.js.map +1 -0
- package/dist/ui/state/use-chat-controller.d.ts +23 -0
- package/dist/ui/state/use-chat-controller.d.ts.map +1 -0
- package/dist/ui/state/use-chat-controller.js +643 -0
- package/dist/ui/state/use-chat-controller.js.map +1 -0
- package/dist/ui/state/use-loop-state.d.ts +23 -0
- package/dist/ui/state/use-loop-state.d.ts.map +1 -0
- package/dist/ui/state/use-loop-state.js +39 -0
- package/dist/ui/state/use-loop-state.js.map +1 -0
- package/dist/ui/state/use-modal-state.d.ts +52 -0
- package/dist/ui/state/use-modal-state.d.ts.map +1 -0
- package/dist/ui/state/use-modal-state.js +44 -0
- package/dist/ui/state/use-modal-state.js.map +1 -0
- package/dist/ui/state/use-model-state.d.ts +29 -0
- package/dist/ui/state/use-model-state.d.ts.map +1 -0
- package/dist/ui/state/use-model-state.js +47 -0
- package/dist/ui/state/use-model-state.js.map +1 -0
- package/dist/ui/state/use-processing-state.d.ts +24 -0
- package/dist/ui/state/use-processing-state.d.ts.map +1 -0
- package/dist/ui/state/use-processing-state.js +46 -0
- package/dist/ui/state/use-processing-state.js.map +1 -0
- package/dist/ui/state/use-queue-state.d.ts +19 -0
- package/dist/ui/state/use-queue-state.d.ts.map +1 -0
- package/dist/ui/state/use-queue-state.js +30 -0
- package/dist/ui/state/use-queue-state.js.map +1 -0
- package/dist/ui/state/use-session-init.d.ts +22 -0
- package/dist/ui/state/use-session-init.d.ts.map +1 -0
- package/dist/ui/state/use-session-init.js +77 -0
- package/dist/ui/state/use-session-init.js.map +1 -0
- package/dist/ui/state/use-session-stats.d.ts +16 -0
- package/dist/ui/state/use-session-stats.d.ts.map +1 -0
- package/dist/ui/state/use-session-stats.js +18 -0
- package/dist/ui/state/use-session-stats.js.map +1 -0
- package/dist/ui/state/use-terminal-width.d.ts +8 -0
- package/dist/ui/state/use-terminal-width.d.ts.map +1 -0
- package/dist/ui/state/use-terminal-width.js +22 -0
- package/dist/ui/state/use-terminal-width.js.map +1 -0
- package/dist/ui/state/use-transcript-state.d.ts +21 -0
- package/dist/ui/state/use-transcript-state.d.ts.map +1 -0
- package/dist/ui/state/use-transcript-state.js +49 -0
- package/dist/ui/state/use-transcript-state.js.map +1 -0
- package/dist/ui/status-bar.d.ts +4 -1
- package/dist/ui/status-bar.d.ts.map +1 -1
- package/dist/ui/status-bar.js +13 -3
- package/dist/ui/status-bar.js.map +1 -1
- package/dist/ui/types.d.ts +3 -0
- package/dist/ui/types.d.ts.map +1 -1
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +10 -0
- package/dist/version-check.js.map +1 -1
- package/package.json +12 -8
- package/dist/agents/agent-config-loader.d.ts +0 -60
- package/dist/agents/agent-config-loader.d.ts.map +0 -1
- package/dist/agents/agent-config-loader.js +0 -402
- package/dist/agents/agent-config-loader.js.map +0 -1
- package/dist/agents/agent-config-presets.d.ts +0 -10
- package/dist/agents/agent-config-presets.d.ts.map +0 -1
- package/dist/agents/agent-config-presets.js +0 -940
- package/dist/agents/agent-config-presets.js.map +0 -1
- package/dist/agents/agent-config-types.d.ts +0 -145
- package/dist/agents/agent-config-types.d.ts.map +0 -1
- package/dist/agents/agent-config-types.js +0 -12
- package/dist/agents/agent-config-types.js.map +0 -1
- package/dist/agents/agent-detection.d.ts +0 -35
- package/dist/agents/agent-detection.d.ts.map +0 -1
- package/dist/agents/agent-detection.js +0 -110
- package/dist/agents/agent-detection.js.map +0 -1
- package/dist/agents/aggregator.d.ts +0 -19
- package/dist/agents/aggregator.d.ts.map +0 -1
- package/dist/agents/aggregator.js +0 -141
- package/dist/agents/aggregator.js.map +0 -1
- package/dist/agents/cli-backend.d.ts +0 -32
- package/dist/agents/cli-backend.d.ts.map +0 -1
- package/dist/agents/cli-backend.js +0 -271
- package/dist/agents/cli-backend.js.map +0 -1
- package/dist/agents/council-types.d.ts +0 -115
- package/dist/agents/council-types.d.ts.map +0 -1
- package/dist/agents/council-types.js +0 -81
- package/dist/agents/council-types.js.map +0 -1
- package/dist/agents/council.d.ts +0 -112
- package/dist/agents/council.d.ts.map +0 -1
- package/dist/agents/council.js +0 -722
- package/dist/agents/council.js.map +0 -1
- package/dist/agents/decomposer.d.ts +0 -33
- package/dist/agents/decomposer.d.ts.map +0 -1
- package/dist/agents/decomposer.js +0 -138
- package/dist/agents/decomposer.js.map +0 -1
- package/dist/agents/dynamic-tools.d.ts +0 -52
- package/dist/agents/dynamic-tools.d.ts.map +0 -1
- package/dist/agents/dynamic-tools.js +0 -395
- package/dist/agents/dynamic-tools.js.map +0 -1
- package/dist/agents/index.d.ts +0 -29
- package/dist/agents/index.d.ts.map +0 -1
- package/dist/agents/index.js +0 -29
- package/dist/agents/index.js.map +0 -1
- package/dist/agents/installer.d.ts +0 -39
- package/dist/agents/installer.d.ts.map +0 -1
- package/dist/agents/installer.js +0 -205
- package/dist/agents/installer.js.map +0 -1
- package/dist/agents/orchestrator.d.ts +0 -106
- package/dist/agents/orchestrator.d.ts.map +0 -1
- package/dist/agents/orchestrator.js +0 -363
- package/dist/agents/orchestrator.js.map +0 -1
- package/dist/agents/sdk-backend.d.ts +0 -63
- package/dist/agents/sdk-backend.d.ts.map +0 -1
- package/dist/agents/sdk-backend.js +0 -489
- package/dist/agents/sdk-backend.js.map +0 -1
- package/dist/agents/swarm-types.d.ts +0 -84
- package/dist/agents/swarm-types.d.ts.map +0 -1
- package/dist/agents/swarm-types.js +0 -20
- package/dist/agents/swarm-types.js.map +0 -1
- package/dist/agents/swarm.d.ts +0 -79
- package/dist/agents/swarm.d.ts.map +0 -1
- package/dist/agents/swarm.js +0 -375
- package/dist/agents/swarm.js.map +0 -1
- package/dist/agents/tools.d.ts +0 -26
- package/dist/agents/tools.d.ts.map +0 -1
- package/dist/agents/tools.js +0 -776
- package/dist/agents/tools.js.map +0 -1
- package/dist/agents/types.d.ts +0 -108
- package/dist/agents/types.d.ts.map +0 -1
- package/dist/agents/types.js +0 -43
- package/dist/agents/types.js.map +0 -1
- package/dist/api-server.d.ts +0 -36
- package/dist/api-server.d.ts.map +0 -1
- package/dist/api-server.js +0 -255
- package/dist/api-server.js.map +0 -1
- package/dist/auto-checkpoint.d.ts +0 -35
- package/dist/auto-checkpoint.d.ts.map +0 -1
- package/dist/auto-checkpoint.js +0 -143
- package/dist/auto-checkpoint.js.map +0 -1
- package/dist/background-jobs.d.ts +0 -45
- package/dist/background-jobs.d.ts.map +0 -1
- package/dist/background-jobs.js +0 -127
- package/dist/background-jobs.js.map +0 -1
- package/dist/branching.d.ts +0 -56
- package/dist/branching.d.ts.map +0 -1
- package/dist/branching.js +0 -211
- package/dist/branching.js.map +0 -1
- package/dist/cli/agent.d.ts +0 -9
- package/dist/cli/agent.d.ts.map +0 -1
- package/dist/cli/agent.js +0 -314
- package/dist/cli/agent.js.map +0 -1
- package/dist/cli/commands.d.ts +0 -12
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -943
- package/dist/cli/commands.js.map +0 -1
- package/dist/cli/index.d.ts +0 -8
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -237
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/types.d.ts +0 -33
- package/dist/cli/types.d.ts.map +0 -1
- package/dist/cli/types.js +0 -21
- package/dist/cli/types.js.map +0 -1
- package/dist/companions.d.ts +0 -54
- package/dist/companions.d.ts.map +0 -1
- package/dist/companions.js +0 -440
- package/dist/companions.js.map +0 -1
- package/dist/completion.d.ts +0 -75
- package/dist/completion.d.ts.map +0 -1
- package/dist/completion.js +0 -234
- package/dist/completion.js.map +0 -1
- package/dist/file-watcher.d.ts +0 -91
- package/dist/file-watcher.d.ts.map +0 -1
- package/dist/file-watcher.js +0 -269
- package/dist/file-watcher.js.map +0 -1
- package/dist/hud/theme-packs/api.d.ts +0 -51
- package/dist/hud/theme-packs/api.d.ts.map +0 -1
- package/dist/hud/theme-packs/api.js +0 -145
- package/dist/hud/theme-packs/api.js.map +0 -1
- package/dist/hud/theme-packs/index.d.ts +0 -18
- package/dist/hud/theme-packs/index.d.ts.map +0 -1
- package/dist/hud/theme-packs/index.js +0 -38
- package/dist/hud/theme-packs/index.js.map +0 -1
- package/dist/hud/theme-packs/types.d.ts +0 -29
- package/dist/hud/theme-packs/types.d.ts.map +0 -1
- package/dist/hud/theme-packs/types.js +0 -9
- package/dist/hud/theme-packs/types.js.map +0 -1
- package/dist/idle-eviction.d.ts +0 -34
- package/dist/idle-eviction.d.ts.map +0 -1
- package/dist/idle-eviction.js +0 -78
- package/dist/idle-eviction.js.map +0 -1
- package/dist/keyboard.d.ts +0 -57
- package/dist/keyboard.d.ts.map +0 -1
- package/dist/keyboard.js +0 -265
- package/dist/keyboard.js.map +0 -1
- package/dist/model-router.d.ts +0 -67
- package/dist/model-router.d.ts.map +0 -1
- package/dist/model-router.js.map +0 -1
- package/dist/providers/openai-compat-shims.d.ts +0 -31
- package/dist/providers/openai-compat-shims.d.ts.map +0 -1
- package/dist/providers/openai-compat-shims.js +0 -179
- package/dist/providers/openai-compat-shims.js.map +0 -1
- package/dist/sandbox-native.d.ts.map +0 -1
- package/dist/sandbox-native.js.map +0 -1
- package/dist/sandbox.d.ts.map +0 -1
- package/dist/sandbox.js.map +0 -1
- package/dist/session-timeout.d.ts +0 -31
- package/dist/session-timeout.d.ts.map +0 -1
- package/dist/session-timeout.js +0 -100
- package/dist/session-timeout.js.map +0 -1
- package/dist/smart-router.d.ts +0 -73
- package/dist/smart-router.d.ts.map +0 -1
- package/dist/smart-router.js +0 -332
- package/dist/smart-router.js.map +0 -1
- package/dist/terminal-recording.d.ts +0 -55
- package/dist/terminal-recording.d.ts.map +0 -1
- package/dist/terminal-recording.js +0 -182
- package/dist/terminal-recording.js.map +0 -1
- package/dist/tmux.d.ts +0 -35
- package/dist/tmux.d.ts.map +0 -1
- package/dist/tmux.js +0 -106
- package/dist/tmux.js.map +0 -1
- package/dist/ui/modals.d.ts.map +0 -1
- package/dist/ui/modals.js.map +0 -1
- package/dist/ui/pack-picker.d.ts +0 -12
- package/dist/ui/pack-picker.d.ts.map +0 -1
- package/dist/ui/pack-picker.js +0 -101
- package/dist/ui/pack-picker.js.map +0 -1
- package/dist/ui/theme-picker.d.ts +0 -24
- package/dist/ui/theme-picker.d.ts.map +0 -1
- package/dist/ui/theme-picker.js +0 -190
- package/dist/ui/theme-picker.js.map +0 -1
package/dist/tools.js
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
import { spawn } from 'child_process';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import * as
|
|
10
|
-
import
|
|
11
|
-
import
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import { StringDecoder } from 'string_decoder';
|
|
11
|
+
import * as sandbox from './sandbox/index.js';
|
|
12
12
|
import { validatePath as scopeValidatePath, isInScope } from './scope.js';
|
|
13
13
|
import { getPluginTools, isPluginTool, executePluginTool } from './plugins.js';
|
|
14
14
|
import config from './config.js';
|
|
15
|
-
import { applySkin, applyPalette, listSkins, listPalettes } from './hud/api.js';
|
|
16
|
-
import { listCompanions } from './companions.js';
|
|
17
15
|
import { generateDiff as generateFileDiff } from './diff.js';
|
|
16
|
+
import { fleetActive, fleetMirrorToolCall } from './fleet.js';
|
|
17
|
+
import { computeAnchorHash, anchorHashFooter } from './local-model.js';
|
|
18
18
|
/**
|
|
19
19
|
* Available tools for the agent
|
|
20
20
|
*/
|
|
@@ -270,36 +270,21 @@ export const TOOLS = [
|
|
|
270
270
|
},
|
|
271
271
|
{
|
|
272
272
|
name: 'configure',
|
|
273
|
-
description: `Read, set, or list Calliope configuration options. Use this when the user asks to change settings, switch themes, providers, models,
|
|
273
|
+
description: `Read, set, or list Calliope configuration options. Use this when the user asks to change settings, switch themes, providers, models, or any preference through natural conversation. Always use action "list" first if you need to show available options.
|
|
274
274
|
|
|
275
275
|
CONFIGURABLE SETTINGS:
|
|
276
276
|
- defaultProvider: AI provider (anthropic, google, openai, together, openrouter, groq, fireworks, mistral, ollama, ai21, huggingface, litellm, bedrock, auto)
|
|
277
|
-
- defaultModel: Model name string (provider-specific, e.g. "claude-sonnet-4-
|
|
278
|
-
- persona: Agent persona style (calliope, muse, minimal)
|
|
277
|
+
- defaultModel: Model name string (provider-specific, e.g. "claude-sonnet-4-6", "gemini-2.0-flash", "gpt-4o")
|
|
279
278
|
- maxIterations: Max agent loop iterations (0 = unlimited)
|
|
280
279
|
- maxIterationTime: Max seconds per iteration (0 = no limit, default: 600)
|
|
281
|
-
- fancyOutput: Enable rich formatting (true/false)
|
|
282
280
|
- autoSaveHistory: Auto-save session history (true/false)
|
|
283
281
|
- autoUpgrade: Check for updates on startup (true/false)
|
|
284
282
|
- collapseTools: Auto-collapse tool output (true/false)
|
|
285
|
-
- collapseThinking: Auto-collapse think blocks (true/false)
|
|
286
283
|
- toolDisplayLimit: Show last N tools expanded (0 = all)
|
|
287
|
-
- layout: UI layout (classic, response-top, response-bottom, split, zen, focus, dashboard, minimal)
|
|
288
|
-
- density: Display density (normal, compact)
|
|
289
|
-
- activeSkin: Terminal skin/theme name (use action "list" category "skins" to see options)
|
|
290
|
-
- activePalette: Color palette name (use action "list" category "palettes" to see options)
|
|
291
|
-
- activeCompanion: AI companion personality (use action "list" category "companions" to see options)
|
|
292
|
-
- companionIntensity: Companion personality level (professional, immersive)
|
|
293
|
-
- useEmojis: Show emojis in UI (true/false)
|
|
294
284
|
- diffStyle: Diff display format (inline, unified, side-by-side)
|
|
295
|
-
- borderStyle: UI border style (rounded, sharp, double, ascii, none)
|
|
296
|
-
- bannerStyle: Startup banner (full, compact, none)
|
|
297
285
|
- circuitBreakersEnabled: Safety circuit breakers (true/false)
|
|
298
286
|
- sandboxMode: Code execution sandbox (auto, native, docker, off)
|
|
299
|
-
-
|
|
300
|
-
- smartRoutingCostSensitivity: Cost vs quality (0-1, 0=best quality, 1=cheapest)
|
|
301
|
-
- recordSessions: Record session audit logs (true/false)
|
|
302
|
-
- recordingRetentionDays: Auto-delete old recordings after N days (0 = keep forever)`,
|
|
287
|
+
- sessionLogLimit: Cap retained session log items (0 = unlimited)`,
|
|
303
288
|
parameters: {
|
|
304
289
|
type: 'object',
|
|
305
290
|
properties: {
|
|
@@ -318,8 +303,8 @@ CONFIGURABLE SETTINGS:
|
|
|
318
303
|
},
|
|
319
304
|
category: {
|
|
320
305
|
type: 'string',
|
|
321
|
-
description: 'Category to list options for (for list action):
|
|
322
|
-
enum: ['
|
|
306
|
+
description: 'Category to list options for (for list action): providers, layouts, all',
|
|
307
|
+
enum: ['providers', 'layouts', 'all'],
|
|
323
308
|
},
|
|
324
309
|
},
|
|
325
310
|
required: ['action'],
|
|
@@ -351,13 +336,9 @@ CONFIGURABLE SETTINGS:
|
|
|
351
336
|
];
|
|
352
337
|
/**
|
|
353
338
|
* Get all available tools
|
|
354
|
-
* Includes agent tools when agentEnabled is true
|
|
355
339
|
*/
|
|
356
|
-
export function getTools(
|
|
340
|
+
export function getTools() {
|
|
357
341
|
const pluginTools = getPluginTools();
|
|
358
|
-
if (agentEnabled) {
|
|
359
|
-
return [...TOOLS, ...getAgtermTools(), ...pluginTools];
|
|
360
|
-
}
|
|
361
342
|
return [...TOOLS, ...pluginTools];
|
|
362
343
|
}
|
|
363
344
|
/**
|
|
@@ -368,14 +349,23 @@ function validatePath(filePath, cwd) {
|
|
|
368
349
|
if (filePath.includes('\0')) {
|
|
369
350
|
throw new Error(`Invalid path: contains null bytes`);
|
|
370
351
|
}
|
|
371
|
-
// Check raw input for path traversal attempts before resolution
|
|
352
|
+
// Check raw input for path traversal attempts before resolution.
|
|
353
|
+
// The scope manager is the single source of truth — no /tmp escape hatch (#139).
|
|
372
354
|
if (filePath.includes('..')) {
|
|
373
355
|
const resolved = path.resolve(cwd, filePath);
|
|
374
356
|
const normalizedCwd = path.resolve(cwd);
|
|
375
|
-
if (!resolved.startsWith(normalizedCwd + path.sep) && resolved !== normalizedCwd
|
|
357
|
+
if (!resolved.startsWith(normalizedCwd + path.sep) && resolved !== normalizedCwd) {
|
|
376
358
|
throw new Error(`Path traversal detected: ${filePath} resolves outside allowed scope`);
|
|
377
359
|
}
|
|
378
360
|
}
|
|
361
|
+
// Block tool access to the CLI's own state directory (~/.calliope-cli):
|
|
362
|
+
// hooks, plugins, skills, trust, and MCP server configs there are a
|
|
363
|
+
// code-execution / trust foothold if the agent can plant or read them (#141).
|
|
364
|
+
const cliStateDir = path.join(os.homedir(), '.calliope-cli');
|
|
365
|
+
const absResolved = path.resolve(cwd, filePath);
|
|
366
|
+
if (absResolved === cliStateDir || absResolved.startsWith(cliStateDir + path.sep)) {
|
|
367
|
+
throw new Error(`Refusing tool access inside the Calliope state directory (${cliStateDir}); hooks/plugins/skills/trust there are protected`);
|
|
368
|
+
}
|
|
379
369
|
// Primary validation via scope manager
|
|
380
370
|
const validated = scopeValidatePath(filePath, cwd);
|
|
381
371
|
return validated;
|
|
@@ -383,11 +373,11 @@ function validatePath(filePath, cwd) {
|
|
|
383
373
|
/**
|
|
384
374
|
* Execute a tool call
|
|
385
375
|
*/
|
|
386
|
-
export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
376
|
+
export async function executeTool(toolCall, cwd, timeout = 60000, onOutput, options) {
|
|
387
377
|
const { id, name, arguments: args } = toolCall;
|
|
388
|
-
//
|
|
389
|
-
if (
|
|
390
|
-
|
|
378
|
+
// Mirror tool call to the fleet channel
|
|
379
|
+
if (fleetActive()) {
|
|
380
|
+
await fleetMirrorToolCall(name, args);
|
|
391
381
|
}
|
|
392
382
|
// Handle plugin tools
|
|
393
383
|
if (isPluginTool(name)) {
|
|
@@ -407,7 +397,7 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
|
407
397
|
if (typeof args.path !== 'string') {
|
|
408
398
|
return { toolCallId: id, result: 'Error: path must be a string', isError: true };
|
|
409
399
|
}
|
|
410
|
-
result = await readFile(args.path, cwd);
|
|
400
|
+
result = await readFile(args.path, cwd, options?.appendAnchorHash === true, options?.fs);
|
|
411
401
|
break;
|
|
412
402
|
}
|
|
413
403
|
case 'write_file': {
|
|
@@ -417,7 +407,7 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
|
417
407
|
if (typeof args.content !== 'string') {
|
|
418
408
|
return { toolCallId: id, result: 'Error: content must be a string', isError: true };
|
|
419
409
|
}
|
|
420
|
-
result = await writeFile(args.path, args.content, cwd);
|
|
410
|
+
result = await writeFile(args.path, args.content, cwd, options?.fs);
|
|
421
411
|
break;
|
|
422
412
|
}
|
|
423
413
|
case 'list_files': {
|
|
@@ -502,42 +492,21 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
|
502
492
|
if (action === 'list') {
|
|
503
493
|
const category = args.category || 'all';
|
|
504
494
|
const sections = [];
|
|
505
|
-
if (category === 'skins' || category === 'all') {
|
|
506
|
-
const skins = listSkins();
|
|
507
|
-
const current = config.get('activeSkin');
|
|
508
|
-
sections.push('SKINS (activeSkin):\n' + skins.map(s => ` ${s.name === current ? '→ ' : ' '}${s.name} - ${s.description}`).join('\n'));
|
|
509
|
-
}
|
|
510
|
-
if (category === 'palettes' || category === 'all') {
|
|
511
|
-
const palettes = listPalettes();
|
|
512
|
-
const current = config.get('activePalette');
|
|
513
|
-
sections.push('PALETTES (activePalette):\n' + palettes.map(p => ` ${p.name === current ? '→ ' : ' '}${p.name} - ${p.description}`).join('\n'));
|
|
514
|
-
}
|
|
515
|
-
if (category === 'companions' || category === 'all') {
|
|
516
|
-
const companions = listCompanions();
|
|
517
|
-
const current = config.get('activeCompanion');
|
|
518
|
-
sections.push('COMPANIONS (activeCompanion):\n' + companions.map(c => ` ${c.name === current ? '→ ' : ' '}${c.name} - ${c.description}`).join('\n'));
|
|
519
|
-
}
|
|
520
495
|
if (category === 'providers' || category === 'all') {
|
|
521
496
|
const providers = ['anthropic', 'google', 'openai', 'together', 'openrouter', 'groq', 'fireworks', 'mistral', 'ollama', 'ai21', 'huggingface', 'litellm', 'bedrock', 'auto'];
|
|
522
497
|
const current = config.get('defaultProvider');
|
|
523
498
|
sections.push('PROVIDERS (defaultProvider):\n' + providers.map(p => ` ${p === current ? '→ ' : ' '}${p}`).join('\n'));
|
|
524
499
|
}
|
|
525
|
-
if (category === 'layouts' || category === 'all') {
|
|
526
|
-
const layouts = ['classic', 'response-top', 'response-bottom', 'split', 'zen', 'focus', 'dashboard', 'minimal'];
|
|
527
|
-
const current = config.get('layout');
|
|
528
|
-
sections.push('LAYOUTS (layout):\n' + layouts.map(l => ` ${l === current ? '→ ' : ' '}${l}`).join('\n'));
|
|
529
|
-
}
|
|
530
500
|
if (category === 'all') {
|
|
531
501
|
// Also show current key settings
|
|
532
502
|
const currentSettings = [
|
|
533
|
-
`density: ${config.get('density')}`,
|
|
534
|
-
`companionIntensity: ${config.get('companionIntensity')}`,
|
|
535
|
-
`useEmojis: ${config.get('useEmojis')}`,
|
|
536
503
|
`diffStyle: ${config.get('diffStyle')}`,
|
|
537
|
-
`borderStyle: ${config.get('borderStyle')}`,
|
|
538
|
-
`bannerStyle: ${config.get('bannerStyle')}`,
|
|
539
504
|
`sandboxMode: ${config.get('sandboxMode')}`,
|
|
540
|
-
`
|
|
505
|
+
`collapseTools: ${config.get('collapseTools')}`,
|
|
506
|
+
`toolDisplayLimit: ${config.get('toolDisplayLimit')}`,
|
|
507
|
+
`maxIterations: ${config.get('maxIterations')}`,
|
|
508
|
+
`sessionLogLimit: ${config.get('sessionLogLimit')}`,
|
|
509
|
+
`routing.enabled: ${config.get('routing')?.enabled ?? false}`,
|
|
541
510
|
`defaultModel: ${config.get('defaultModel') || '(auto)'}`,
|
|
542
511
|
];
|
|
543
512
|
sections.push('CURRENT SETTINGS:\n' + currentSettings.map(s => ` ${s}`).join('\n'));
|
|
@@ -565,16 +534,11 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
|
565
534
|
}
|
|
566
535
|
// Only allow setting safe keys through conversation (allowlist)
|
|
567
536
|
const SAFE_CONFIG_KEYS = new Set([
|
|
568
|
-
'defaultProvider', 'defaultModel', '
|
|
569
|
-
'
|
|
570
|
-
'collapseTools', '
|
|
571
|
-
'layout', 'density',
|
|
572
|
-
'activeSkin', 'activePalette', 'activeCompanion', 'activeThemePack',
|
|
573
|
-
'companionIntensity', 'useEmojis', 'diffStyle', 'borderStyle', 'bannerStyle',
|
|
537
|
+
'defaultProvider', 'defaultModel', 'maxIterations', 'maxIterationTime',
|
|
538
|
+
'autoSaveHistory', 'autoUpgrade',
|
|
539
|
+
'collapseTools', 'toolDisplayLimit', 'diffStyle',
|
|
574
540
|
'circuitBreakersEnabled', 'sandboxMode',
|
|
575
|
-
'
|
|
576
|
-
'recordSessions', 'recordingRetentionDays', 'sessionLogLimit',
|
|
577
|
-
'awsRegion', 'awsProfile',
|
|
541
|
+
'sessionLogLimit',
|
|
578
542
|
]);
|
|
579
543
|
if (!SAFE_CONFIG_KEYS.has(key)) {
|
|
580
544
|
return { toolCallId: id, result: `Error: "${key}" cannot be set through conversation. Use /keys command, environment variables, or edit the config file directly.`, isError: true };
|
|
@@ -588,31 +552,6 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
|
588
552
|
else if (/^\d+(\.\d+)?$/.test(rawValue))
|
|
589
553
|
parsedValue = Number(rawValue);
|
|
590
554
|
try {
|
|
591
|
-
// Special handling for HUD settings that need apply functions
|
|
592
|
-
if (key === 'activeSkin') {
|
|
593
|
-
const success = applySkin(rawValue);
|
|
594
|
-
if (!success) {
|
|
595
|
-
return { toolCallId: id, result: `Error: skin "${rawValue}" not found. Use action "list" category "skins" to see available skins.`, isError: true };
|
|
596
|
-
}
|
|
597
|
-
result = `Skin changed to "${rawValue}"`;
|
|
598
|
-
break;
|
|
599
|
-
}
|
|
600
|
-
if (key === 'activePalette') {
|
|
601
|
-
const success = applyPalette(rawValue);
|
|
602
|
-
if (!success) {
|
|
603
|
-
return { toolCallId: id, result: `Error: palette "${rawValue}" not found. Use action "list" category "palettes" to see available palettes.`, isError: true };
|
|
604
|
-
}
|
|
605
|
-
result = `Palette changed to "${rawValue}"`;
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
if (key === 'activeCompanion') {
|
|
609
|
-
if (!listCompanions().some(c => c.name === rawValue)) {
|
|
610
|
-
return { toolCallId: id, result: `Error: companion "${rawValue}" not found. Use action "list" category "companions" to see available companions.`, isError: true };
|
|
611
|
-
}
|
|
612
|
-
config.set('activeCompanion', rawValue);
|
|
613
|
-
result = `Companion changed to "${rawValue}"`;
|
|
614
|
-
break;
|
|
615
|
-
}
|
|
616
555
|
// Generic config set
|
|
617
556
|
config.set(key, parsedValue);
|
|
618
557
|
result = `Set ${key} = ${JSON.stringify(parsedValue)}`;
|
|
@@ -642,7 +581,10 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
|
642
581
|
return { toolCallId: id, result: 'Error: new_string must be a string', isError: true };
|
|
643
582
|
}
|
|
644
583
|
const replaceAll = args.replace_all === true;
|
|
645
|
-
|
|
584
|
+
// anchor_hash is optional and accepted from any caller; it is only
|
|
585
|
+
// advertised in the simplified local schema (feature 4).
|
|
586
|
+
const anchorHash = typeof args.anchor_hash === 'string' ? args.anchor_hash : undefined;
|
|
587
|
+
result = await editFile(args.path, args.old_string, args.new_string, replaceAll, cwd, anchorHash, options?.fs);
|
|
646
588
|
break;
|
|
647
589
|
}
|
|
648
590
|
case 'glob': {
|
|
@@ -686,6 +628,12 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
|
686
628
|
*
|
|
687
629
|
* Patterns are tested against the normalized command (see normalizeCommand())
|
|
688
630
|
* to defeat common bypass techniques like quoting, env prefixes, and subshells.
|
|
631
|
+
*
|
|
632
|
+
* SECURITY NOTE (#132): This denylist is ADVISORY / defense-in-depth only. It is
|
|
633
|
+
* fundamentally unwinnable to perfectly screen an arbitrary `bash -c` string, so
|
|
634
|
+
* treat this as a best-effort tripwire, NOT a security boundary. The real control
|
|
635
|
+
* is the native sandbox (see src/sandbox/native.ts); when no sandbox is available
|
|
636
|
+
* the shell tool fails closed (see shouldUseNativeSandbox / executeShell).
|
|
689
637
|
*/
|
|
690
638
|
const BLOCKED_COMMANDS = [
|
|
691
639
|
/^sudo\s/,
|
|
@@ -738,12 +686,29 @@ function normalizeCommand(command) {
|
|
|
738
686
|
return cmd.trim();
|
|
739
687
|
}
|
|
740
688
|
/**
|
|
741
|
-
*
|
|
742
|
-
*
|
|
689
|
+
* Split a shell command into sub-commands on every separator so anchored deny
|
|
690
|
+
* patterns (^sudo, ^su, ^rm -rf /, ...) are tested at the start of each fragment.
|
|
691
|
+
*
|
|
692
|
+
* Process two-char operators (&&, ||) before single & / | so the singles do not
|
|
693
|
+
* shred the two-char operators they are part of. Newline is also a separator.
|
|
694
|
+
* normalizeCommand() strips quotes for matching, so quoted separators are
|
|
695
|
+
* intentionally not special-cased here (advisory blocklist, #132).
|
|
696
|
+
*/
|
|
697
|
+
function splitSubCommands(command) {
|
|
698
|
+
return command
|
|
699
|
+
// two-char logical operators first
|
|
700
|
+
.split(/\s*(?:&&|\|\|)\s*/)
|
|
701
|
+
// then single separators: ; & | and newline (also a trailing & background)
|
|
702
|
+
.flatMap((part) => part.split(/\s*[;&|\n]\s*/));
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Check a command (and all sub-commands separated by ; && || & | or newline)
|
|
706
|
+
* against the blocklist. Returns the matching pattern source string, or null if
|
|
707
|
+
* allowed.
|
|
743
708
|
*/
|
|
744
709
|
function matchesBlocklist(command) {
|
|
745
710
|
// Split on command separators to check each sub-command
|
|
746
|
-
const subCommands = command
|
|
711
|
+
const subCommands = splitSubCommands(command);
|
|
747
712
|
for (const sub of subCommands) {
|
|
748
713
|
const normalized = normalizeCommand(sub);
|
|
749
714
|
for (const pattern of BLOCKED_COMMANDS) {
|
|
@@ -811,29 +776,11 @@ function validateShellPaths(command, cwd) {
|
|
|
811
776
|
for (const p of extractedPaths) {
|
|
812
777
|
const allowed = isInScope(p, cwd);
|
|
813
778
|
if (!allowed) {
|
|
814
|
-
return 'Shell command blocked: "' + p + '" is outside allowed scope. Use /add
|
|
779
|
+
return 'Shell command blocked: "' + p + '" is outside allowed scope. Use /scope add to expand scope.';
|
|
815
780
|
}
|
|
816
781
|
}
|
|
817
782
|
return null;
|
|
818
783
|
}
|
|
819
|
-
/**
|
|
820
|
-
* Determine whether to use native sandboxing for shell commands based on config.
|
|
821
|
-
*
|
|
822
|
-
* sandboxMode values:
|
|
823
|
-
* - 'auto': use native sandbox when available, otherwise run unsandboxed
|
|
824
|
-
* - 'native': require native sandbox (fail if unavailable)
|
|
825
|
-
* - 'docker': defer to Docker sandbox (handled elsewhere)
|
|
826
|
-
* - 'off': no sandboxing
|
|
827
|
-
*/
|
|
828
|
-
function shouldUseNativeSandbox() {
|
|
829
|
-
const mode = config.get('sandboxMode') || 'auto';
|
|
830
|
-
if (mode === 'off' || mode === 'docker')
|
|
831
|
-
return 'skip';
|
|
832
|
-
if (mode === 'native')
|
|
833
|
-
return 'require';
|
|
834
|
-
// 'auto': use if available
|
|
835
|
-
return nativeSandbox.isNativeSandboxAvailable() ? 'use' : 'skip';
|
|
836
|
-
}
|
|
837
784
|
/**
|
|
838
785
|
* Execute a shell command
|
|
839
786
|
*/
|
|
@@ -849,14 +796,22 @@ async function executeShell(command, cwd, timeout, onOutput) {
|
|
|
849
796
|
return `Error: ${scopeError}`;
|
|
850
797
|
}
|
|
851
798
|
// Check if native sandbox should be used
|
|
852
|
-
const sandboxDecision = shouldUseNativeSandbox();
|
|
853
|
-
if (sandboxDecision === 'require' && !
|
|
799
|
+
const sandboxDecision = sandbox.shouldUseNativeSandbox();
|
|
800
|
+
if (sandboxDecision === 'require' && !sandbox.isNativeSandboxAvailable()) {
|
|
854
801
|
return 'Error: Native sandbox required (sandboxMode=native) but not available on this platform.';
|
|
855
802
|
}
|
|
803
|
+
// 'auto' is best-effort: when no native backend exists (e.g. Linux/Windows
|
|
804
|
+
// without Seatbelt/Landlock) the command runs unsandboxed rather than failing,
|
|
805
|
+
// so shell execution keeps working on every platform. Users who require
|
|
806
|
+
// enforcement set sandboxMode=native (fail-closed above) or sandboxMode=docker.
|
|
807
|
+
// The sandbox hardening (#133 — network off by default, restricted reads)
|
|
808
|
+
// still applies whenever a sandbox IS active ('use'/'require'/docker).
|
|
856
809
|
if (sandboxDecision === 'use' || sandboxDecision === 'require') {
|
|
857
|
-
|
|
810
|
+
// Network is OFF by default; opt in via CALLIOPE_SHELL_NETWORK=1 (#133).
|
|
811
|
+
const networkEnabled = process.env.CALLIOPE_SHELL_NETWORK === '1';
|
|
812
|
+
const result = await sandbox.executeInNativeSandbox(command, cwd, {
|
|
858
813
|
timeout,
|
|
859
|
-
networkEnabled
|
|
814
|
+
networkEnabled,
|
|
860
815
|
});
|
|
861
816
|
// Shell tool output is transparent — same format as unsandboxed execution
|
|
862
817
|
let output = result.stdout + (result.stderr ? `\nstderr: ${result.stderr}` : '');
|
|
@@ -925,30 +880,41 @@ async function executeShell(command, cwd, timeout, onOutput) {
|
|
|
925
880
|
}
|
|
926
881
|
/**
|
|
927
882
|
* Read a file
|
|
883
|
+
*
|
|
884
|
+
* When `appendAnchorHash` is set (local backends only), the current content hash
|
|
885
|
+
* is appended so the model can echo it back on a subsequent anchored edit_file
|
|
886
|
+
* (stale-view protection, feature 4).
|
|
928
887
|
*/
|
|
929
|
-
async function readFile(filePath, cwd) {
|
|
888
|
+
async function readFile(filePath, cwd, appendAnchorHash = false, fsDelegate) {
|
|
930
889
|
const absPath = validatePath(filePath, cwd);
|
|
931
|
-
|
|
932
|
-
|
|
890
|
+
let content;
|
|
891
|
+
if (fsDelegate?.readTextFile) {
|
|
892
|
+
// Client-side fs (e.g. an ACP editor buffer that may hold unsaved changes).
|
|
893
|
+
// The client owns existence/size; a missing file surfaces as a thrown error
|
|
894
|
+
// that executeTool turns into a normal tool error.
|
|
895
|
+
content = await fsDelegate.readTextFile(absPath);
|
|
933
896
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
897
|
+
else {
|
|
898
|
+
if (!fs.existsSync(absPath)) {
|
|
899
|
+
throw new Error(`File not found: ${absPath}`);
|
|
900
|
+
}
|
|
901
|
+
const stats = fs.statSync(absPath);
|
|
902
|
+
if (stats.isDirectory()) {
|
|
903
|
+
throw new Error(`Path is a directory: ${absPath}`);
|
|
904
|
+
}
|
|
905
|
+
// Check file size (limit to 1MB)
|
|
906
|
+
if (stats.size > 1024 * 1024) {
|
|
907
|
+
throw new Error(`File too large (${Math.round(stats.size / 1024)}KB). Max 1MB.`);
|
|
908
|
+
}
|
|
909
|
+
content = fs.readFileSync(absPath, 'utf-8');
|
|
910
|
+
}
|
|
911
|
+
// Inline file preview header (#119). The header carries the line count;
|
|
912
|
+
// the full content follows once — no preview/footer, those duplicated the
|
|
913
|
+
// first 20 lines on every read and cost ~20% extra tokens on short files.
|
|
914
|
+
const totalLines = content.split('\n').length;
|
|
948
915
|
const header = `[file: ${filePath} \u2014 ${totalLines} line${totalLines !== 1 ? 's' : ''}]\n${'─'.repeat(40)}`;
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
return `${header}\n${previewBody}${footer}\n\n${content}`;
|
|
916
|
+
const footer = appendAnchorHash ? anchorHashFooter(computeAnchorHash(content)) : '';
|
|
917
|
+
return `${header}\n${content}${footer}`;
|
|
952
918
|
}
|
|
953
919
|
/**
|
|
954
920
|
* Generate a simple line-diff between old and new content
|
|
@@ -963,9 +929,7 @@ function generateDiff(oldContent, newContent, maxLines = 20) {
|
|
|
963
929
|
let deletions = 0;
|
|
964
930
|
let changesShown = 0;
|
|
965
931
|
let contextLines = 0;
|
|
966
|
-
|
|
967
|
-
const density = config.get('density') || 'normal';
|
|
968
|
-
const contextWindow = density === 'compact' ? 1 : 3;
|
|
932
|
+
const contextWindow = 3;
|
|
969
933
|
// Compute line number width
|
|
970
934
|
const lineNumWidth = Math.max(4, maxIdx.toString().length);
|
|
971
935
|
const padNum = (n) => String(n).padStart(lineNumWidth, ' ');
|
|
@@ -1013,39 +977,46 @@ function generateDiff(oldContent, newContent, maxLines = 20) {
|
|
|
1013
977
|
/**
|
|
1014
978
|
* Write a file
|
|
1015
979
|
*/
|
|
1016
|
-
async function writeFile(filePath, content, cwd) {
|
|
980
|
+
async function writeFile(filePath, content, cwd, fsDelegate) {
|
|
1017
981
|
const absPath = validatePath(filePath, cwd);
|
|
1018
|
-
// Create directory if needed
|
|
1019
|
-
const dir = path.dirname(absPath);
|
|
1020
|
-
if (!fs.existsSync(dir)) {
|
|
1021
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1022
|
-
}
|
|
1023
982
|
// Read existing content for diff
|
|
1024
983
|
let oldContent = '';
|
|
1025
984
|
let isNewFile = true;
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
985
|
+
if (fsDelegate?.writeTextFile) {
|
|
986
|
+
// Client-side fs: read the current buffer for the diff (absent = new file),
|
|
987
|
+
// then write through the client instead of touching local disk.
|
|
988
|
+
if (fsDelegate.readTextFile) {
|
|
989
|
+
try {
|
|
990
|
+
oldContent = await fsDelegate.readTextFile(absPath);
|
|
1031
991
|
isNewFile = false;
|
|
1032
992
|
}
|
|
993
|
+
catch {
|
|
994
|
+
oldContent = '';
|
|
995
|
+
isNewFile = true;
|
|
996
|
+
}
|
|
1033
997
|
}
|
|
1034
|
-
|
|
1035
|
-
// Ignore errors reading old content
|
|
1036
|
-
}
|
|
998
|
+
await fsDelegate.writeTextFile(absPath, content);
|
|
1037
999
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1000
|
+
else {
|
|
1001
|
+
// Create directory if needed
|
|
1002
|
+
const dir = path.dirname(absPath);
|
|
1003
|
+
if (!fs.existsSync(dir)) {
|
|
1004
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1043
1005
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1006
|
+
if (fs.existsSync(absPath)) {
|
|
1007
|
+
try {
|
|
1008
|
+
const stats = fs.statSync(absPath);
|
|
1009
|
+
if (!stats.isDirectory() && stats.size < 100 * 1024) {
|
|
1010
|
+
oldContent = fs.readFileSync(absPath, 'utf-8');
|
|
1011
|
+
isNewFile = false;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
// Ignore errors reading old content
|
|
1016
|
+
}
|
|
1046
1017
|
}
|
|
1018
|
+
fs.writeFileSync(absPath, content);
|
|
1047
1019
|
}
|
|
1048
|
-
fs.writeFileSync(absPath, content);
|
|
1049
1020
|
// Generate diff output (#119)
|
|
1050
1021
|
const DIFF_CAP = 50;
|
|
1051
1022
|
const header = `[wrote: ${filePath}]\n${'─'.repeat(Math.min(filePath.length + 9, 60))}`;
|
|
@@ -1121,6 +1092,8 @@ function listFilesRecursive(dir, prefix, depth) {
|
|
|
1121
1092
|
for (const entry of entries.slice(0, 50)) {
|
|
1122
1093
|
if (entry.name.startsWith('.'))
|
|
1123
1094
|
continue; // Skip hidden files
|
|
1095
|
+
if (entry.isDirectory() && WALK_IGNORED_DIRS.has(entry.name))
|
|
1096
|
+
continue;
|
|
1124
1097
|
const entryPath = path.join(dir, entry.name);
|
|
1125
1098
|
if (entry.isDirectory()) {
|
|
1126
1099
|
lines.push(`${prefix}📁 ${entry.name}/`);
|
|
@@ -1145,12 +1118,10 @@ function listFilesRecursive(dir, prefix, depth) {
|
|
|
1145
1118
|
* - 'off': no sandboxing
|
|
1146
1119
|
*/
|
|
1147
1120
|
async function executeCode(language, code, cwd, timeout) {
|
|
1148
|
-
const mode = config.get('sandboxMode') || 'auto';
|
|
1149
1121
|
const sandboxLang = language === 'node' ? 'node' : language;
|
|
1150
|
-
// Determine execution strategy
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
if (useDocker) {
|
|
1122
|
+
// Determine execution strategy from sandboxMode config
|
|
1123
|
+
const strategy = sandbox.selectCodeSandbox();
|
|
1124
|
+
if (strategy === 'docker') {
|
|
1154
1125
|
// Docker sandbox path (existing behaviour)
|
|
1155
1126
|
const result = await sandbox.execute(sandboxLang, code, {
|
|
1156
1127
|
timeout,
|
|
@@ -1169,7 +1140,7 @@ async function executeCode(language, code, cwd, timeout) {
|
|
|
1169
1140
|
output += `Duration: ${result.duration}ms`;
|
|
1170
1141
|
return output;
|
|
1171
1142
|
}
|
|
1172
|
-
if (
|
|
1143
|
+
if (strategy === 'native') {
|
|
1173
1144
|
// Native OS sandbox path — write code to temp file and execute
|
|
1174
1145
|
const fs = await import('fs');
|
|
1175
1146
|
const os = await import('os');
|
|
@@ -1182,7 +1153,7 @@ async function executeCode(language, code, cwd, timeout) {
|
|
|
1182
1153
|
: language === 'node' ? `node "${codePath}"`
|
|
1183
1154
|
: `bash "${codePath}"`;
|
|
1184
1155
|
const startTime = Date.now();
|
|
1185
|
-
const result = await
|
|
1156
|
+
const result = await sandbox.executeInNativeSandbox(cmd, cwd, {
|
|
1186
1157
|
timeout,
|
|
1187
1158
|
readOnlyPaths: [tempDir],
|
|
1188
1159
|
});
|
|
@@ -1277,44 +1248,74 @@ async function webSearch(query, numResults) {
|
|
|
1277
1248
|
/**
|
|
1278
1249
|
* Execute git commands safely
|
|
1279
1250
|
*/
|
|
1251
|
+
// Tokenize git args supporting simple shell-style quoting so commit messages
|
|
1252
|
+
// like -m "fix: thing" parse into two tokens.
|
|
1253
|
+
function parseGitArgs(args) {
|
|
1254
|
+
const tokens = [];
|
|
1255
|
+
const re = /"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|(\S+)/g;
|
|
1256
|
+
let m;
|
|
1257
|
+
while ((m = re.exec(args)) !== null) {
|
|
1258
|
+
tokens.push(m[1] ?? m[2] ?? m[3]);
|
|
1259
|
+
}
|
|
1260
|
+
return tokens;
|
|
1261
|
+
}
|
|
1262
|
+
// Single-quote each token so the final string is safe to pass through bash -c
|
|
1263
|
+
// (what executeShell does). A single quote inside a single-quoted string is
|
|
1264
|
+
// escaped as '\'' .
|
|
1265
|
+
function shellEscape(s) {
|
|
1266
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1267
|
+
}
|
|
1268
|
+
// Git-specific flags that cause arbitrary command execution even when passed
|
|
1269
|
+
// without a shell (e.g. over SSH via upload-pack/receive-pack). Blocklisted
|
|
1270
|
+
// because the shell-metachar strip used before did not catch them.
|
|
1271
|
+
const DANGEROUS_GIT_FLAG_RE = /^(--upload-pack|--receive-pack|--exec|--config-env)(=|$)/;
|
|
1280
1272
|
async function executeGit(operation, args, cwd) {
|
|
1281
1273
|
const allowedOps = ['status', 'diff', 'log', 'branch', 'add', 'commit', 'push', 'pull', 'stash'];
|
|
1282
1274
|
if (!allowedOps.includes(operation)) {
|
|
1283
1275
|
return `Error: Unknown git operation: ${operation}. Allowed: ${allowedOps.join(', ')}`;
|
|
1284
1276
|
}
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1277
|
+
const tokens = parseGitArgs(args);
|
|
1278
|
+
for (const tok of tokens) {
|
|
1279
|
+
if (DANGEROUS_GIT_FLAG_RE.test(tok)) {
|
|
1280
|
+
return `Error: git flag "${tok.split('=')[0]}" is not allowed (RCE risk)`;
|
|
1281
|
+
}
|
|
1282
|
+
// ext:: remote protocol runs an arbitrary command on the client side.
|
|
1283
|
+
if (tok.startsWith('ext::')) {
|
|
1284
|
+
return 'Error: git "ext::" remote protocol is not allowed (RCE risk)';
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const quoted = tokens.map(shellEscape).join(' ');
|
|
1287
1288
|
let command;
|
|
1288
1289
|
switch (operation) {
|
|
1289
1290
|
case 'status':
|
|
1290
1291
|
command = 'git status --short';
|
|
1291
1292
|
break;
|
|
1292
1293
|
case 'diff':
|
|
1293
|
-
command = `git diff ${
|
|
1294
|
+
command = `git diff ${quoted}`.trim();
|
|
1294
1295
|
break;
|
|
1295
1296
|
case 'log':
|
|
1296
|
-
command = `git log --oneline -20 ${
|
|
1297
|
+
command = `git log --oneline -20 ${quoted}`.trim();
|
|
1297
1298
|
break;
|
|
1298
1299
|
case 'branch':
|
|
1299
|
-
command = `git branch ${
|
|
1300
|
+
command = `git branch ${quoted}`.trim();
|
|
1300
1301
|
break;
|
|
1301
1302
|
case 'add':
|
|
1302
|
-
command = `git add ${
|
|
1303
|
+
command = tokens.length ? `git add ${quoted}` : 'git add .';
|
|
1303
1304
|
break;
|
|
1304
1305
|
case 'commit':
|
|
1305
|
-
if (!
|
|
1306
|
+
if (!tokens.includes('-m')) {
|
|
1306
1307
|
return 'Error: commit requires -m "message"';
|
|
1307
1308
|
}
|
|
1308
|
-
command = `git commit ${
|
|
1309
|
+
command = `git commit ${quoted}`.trim();
|
|
1309
1310
|
break;
|
|
1310
1311
|
case 'push':
|
|
1311
|
-
command = `git push ${
|
|
1312
|
+
command = `git push ${quoted}`.trim();
|
|
1312
1313
|
break;
|
|
1313
1314
|
case 'pull':
|
|
1314
|
-
command = `git pull ${
|
|
1315
|
+
command = `git pull ${quoted}`.trim();
|
|
1315
1316
|
break;
|
|
1316
1317
|
case 'stash':
|
|
1317
|
-
command = `git stash ${
|
|
1318
|
+
command = `git stash ${quoted}`.trim();
|
|
1318
1319
|
break;
|
|
1319
1320
|
default:
|
|
1320
1321
|
return `Unknown operation: ${operation}`;
|
|
@@ -1347,16 +1348,42 @@ ${content}
|
|
|
1347
1348
|
* Edit a file by replacing an exact string (in-place edit).
|
|
1348
1349
|
* Supports single-occurrence enforcement or replace_all mode.
|
|
1349
1350
|
*/
|
|
1350
|
-
async function editFile(filePath, oldString, newString, replaceAll, cwd) {
|
|
1351
|
+
async function editFile(filePath, oldString, newString, replaceAll, cwd, anchorHash, fsDelegate) {
|
|
1351
1352
|
const absPath = validatePath(filePath, cwd);
|
|
1352
|
-
|
|
1353
|
-
|
|
1353
|
+
let content;
|
|
1354
|
+
if (fsDelegate?.readTextFile) {
|
|
1355
|
+
// Client-side fs: edit against the editor's current buffer, not disk.
|
|
1356
|
+
content = await fsDelegate.readTextFile(absPath);
|
|
1354
1357
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
+
else {
|
|
1359
|
+
if (!fs.existsSync(absPath)) {
|
|
1360
|
+
throw new Error(`File not found: ${absPath}`);
|
|
1361
|
+
}
|
|
1362
|
+
const stats = fs.statSync(absPath);
|
|
1363
|
+
if (stats.isDirectory()) {
|
|
1364
|
+
throw new Error(`Path is a directory: ${absPath}`);
|
|
1365
|
+
}
|
|
1366
|
+
content = fs.readFileSync(absPath, 'utf-8');
|
|
1367
|
+
}
|
|
1368
|
+
// Persist the edited content through the delegate when available, else disk.
|
|
1369
|
+
const writeUpdated = async (updated) => {
|
|
1370
|
+
if (fsDelegate?.writeTextFile) {
|
|
1371
|
+
await fsDelegate.writeTextFile(absPath, updated);
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1374
|
+
fs.writeFileSync(absPath, updated);
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
// Stale-view protection (feature 4): if the caller supplied an anchor_hash, it
|
|
1378
|
+
// must match the file's current content. A mismatch means the model is acting
|
|
1379
|
+
// on an out-of-date view — fail loudly and tell it to re-read. Absent hash =
|
|
1380
|
+
// no check (inert for cloud providers, which never see the param).
|
|
1381
|
+
if (anchorHash !== undefined) {
|
|
1382
|
+
const currentHash = computeAnchorHash(content);
|
|
1383
|
+
if (anchorHash !== currentHash) {
|
|
1384
|
+
throw new Error(`anchor_hash mismatch: file has changed since you read it (you sent "${anchorHash}", current is "${currentHash}"). Re-read ${filePath} before editing.`);
|
|
1385
|
+
}
|
|
1358
1386
|
}
|
|
1359
|
-
const content = fs.readFileSync(absPath, 'utf-8');
|
|
1360
1387
|
// Helper to build a compact edit diff from old_string/new_string (#119)
|
|
1361
1388
|
const buildEditDiff = (oldStr, newStr, fPath, count) => {
|
|
1362
1389
|
const DIFF_CAP = 50;
|
|
@@ -1398,7 +1425,7 @@ async function editFile(filePath, oldString, newString, replaceAll, cwd) {
|
|
|
1398
1425
|
if (count === 0) {
|
|
1399
1426
|
throw new Error(`old_string not found in file: ${absPath}`);
|
|
1400
1427
|
}
|
|
1401
|
-
|
|
1428
|
+
await writeUpdated(updated);
|
|
1402
1429
|
return buildEditDiff(oldString, newString, filePath, count);
|
|
1403
1430
|
}
|
|
1404
1431
|
// Count occurrences
|
|
@@ -1410,7 +1437,7 @@ async function editFile(filePath, oldString, newString, replaceAll, cwd) {
|
|
|
1410
1437
|
throw new Error(`old_string matches ${occurrences} occurrences — use replace_all: true or make it more specific`);
|
|
1411
1438
|
}
|
|
1412
1439
|
const updated = content.replace(oldString, newString);
|
|
1413
|
-
|
|
1440
|
+
await writeUpdated(updated);
|
|
1414
1441
|
return buildEditDiff(oldString, newString, filePath, 1);
|
|
1415
1442
|
}
|
|
1416
1443
|
/**
|
|
@@ -1468,7 +1495,56 @@ function globToRegex(pattern) {
|
|
|
1468
1495
|
/**
|
|
1469
1496
|
* Recursively walk a directory and collect all file paths relative to the base.
|
|
1470
1497
|
*/
|
|
1471
|
-
|
|
1498
|
+
// Directories that are almost always noise when searching a project tree.
|
|
1499
|
+
// Keeping this list small and hardcoded (rather than parsing .gitignore) avoids
|
|
1500
|
+
// both performance cliffs on large monorepos and accidental scans of vendored
|
|
1501
|
+
// secrets in node_modules. Users who need to search these can use the shell tool.
|
|
1502
|
+
const WALK_IGNORED_DIRS = new Set([
|
|
1503
|
+
'.git',
|
|
1504
|
+
'node_modules',
|
|
1505
|
+
'dist',
|
|
1506
|
+
'build',
|
|
1507
|
+
'.next',
|
|
1508
|
+
'.nuxt',
|
|
1509
|
+
'.turbo',
|
|
1510
|
+
'.cache',
|
|
1511
|
+
'coverage',
|
|
1512
|
+
'.venv',
|
|
1513
|
+
'venv',
|
|
1514
|
+
'__pycache__',
|
|
1515
|
+
'target', // rust
|
|
1516
|
+
'.gradle',
|
|
1517
|
+
'.idea',
|
|
1518
|
+
'.vscode',
|
|
1519
|
+
]);
|
|
1520
|
+
/** Maximum directory recursion depth before walkDir stops descending (#154). */
|
|
1521
|
+
const WALK_MAX_DEPTH = 40;
|
|
1522
|
+
/** Maximum number of entries walkDir will collect before it stops (#154). */
|
|
1523
|
+
const WALK_MAX_ENTRIES = 100000;
|
|
1524
|
+
/**
|
|
1525
|
+
* Recursively collect file paths under `dir` (relative to `base`).
|
|
1526
|
+
*
|
|
1527
|
+
* Hardened (#154): bounded recursion depth, a per-walk Set of visited real
|
|
1528
|
+
* directory paths to guard against directory cycles (bind mounts / hardlinks /
|
|
1529
|
+
* any future symlink-follow), and a cap on the total number of collected
|
|
1530
|
+
* entries. `depth` and `visited` are internal accumulators.
|
|
1531
|
+
*/
|
|
1532
|
+
function walkDir(dir, base, results, depth = 0, visited = new Set()) {
|
|
1533
|
+
if (depth > WALK_MAX_DEPTH)
|
|
1534
|
+
return;
|
|
1535
|
+
if (results.length >= WALK_MAX_ENTRIES)
|
|
1536
|
+
return;
|
|
1537
|
+
// Guard against directory cycles by tracking canonical (real) paths.
|
|
1538
|
+
let realDir;
|
|
1539
|
+
try {
|
|
1540
|
+
realDir = fs.realpathSync(dir);
|
|
1541
|
+
}
|
|
1542
|
+
catch {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
if (visited.has(realDir))
|
|
1546
|
+
return;
|
|
1547
|
+
visited.add(realDir);
|
|
1472
1548
|
let entries;
|
|
1473
1549
|
try {
|
|
1474
1550
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -1477,10 +1553,14 @@ function walkDir(dir, base, results) {
|
|
|
1477
1553
|
return;
|
|
1478
1554
|
}
|
|
1479
1555
|
for (const entry of entries) {
|
|
1556
|
+
if (results.length >= WALK_MAX_ENTRIES)
|
|
1557
|
+
return;
|
|
1558
|
+
if (entry.isDirectory() && WALK_IGNORED_DIRS.has(entry.name))
|
|
1559
|
+
continue;
|
|
1480
1560
|
const fullPath = path.join(dir, entry.name);
|
|
1481
1561
|
const relPath = path.relative(base, fullPath);
|
|
1482
1562
|
if (entry.isDirectory()) {
|
|
1483
|
-
walkDir(fullPath, base, results);
|
|
1563
|
+
walkDir(fullPath, base, results, depth + 1, visited);
|
|
1484
1564
|
}
|
|
1485
1565
|
else {
|
|
1486
1566
|
results.push(relPath);
|
|
@@ -1516,6 +1596,73 @@ async function globFiles(pattern, searchCwd) {
|
|
|
1516
1596
|
}
|
|
1517
1597
|
return matched.join('\n');
|
|
1518
1598
|
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Scan a single file line-by-line and push matching lines into `results`,
|
|
1601
|
+
* without holding the entire file in memory (#154).
|
|
1602
|
+
*
|
|
1603
|
+
* Reads the file in fixed-size chunks, emits complete lines as they are found,
|
|
1604
|
+
* and stops as soon as `results` reaches `maxResults`.
|
|
1605
|
+
*/
|
|
1606
|
+
function grepFileStreaming(filePath, regex, relPath, results, maxResults) {
|
|
1607
|
+
const CHUNK_SIZE = 64 * 1024;
|
|
1608
|
+
let fd;
|
|
1609
|
+
try {
|
|
1610
|
+
fd = fs.openSync(filePath, 'r');
|
|
1611
|
+
}
|
|
1612
|
+
catch {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
// StringDecoder buffers partial multi-byte UTF-8 sequences that straddle a
|
|
1617
|
+
// chunk boundary, so lines decode correctly across reads.
|
|
1618
|
+
const decoder = new StringDecoder('utf-8');
|
|
1619
|
+
const buffer = Buffer.allocUnsafe(CHUNK_SIZE);
|
|
1620
|
+
let leftover = '';
|
|
1621
|
+
let lineNo = 0;
|
|
1622
|
+
let bytesRead;
|
|
1623
|
+
try {
|
|
1624
|
+
do {
|
|
1625
|
+
bytesRead = fs.readSync(fd, buffer, 0, CHUNK_SIZE, null);
|
|
1626
|
+
if (bytesRead <= 0)
|
|
1627
|
+
break;
|
|
1628
|
+
const text = leftover + decoder.write(buffer.subarray(0, bytesRead));
|
|
1629
|
+
const parts = text.split('\n');
|
|
1630
|
+
// The last element may be a partial line; carry it to the next chunk.
|
|
1631
|
+
leftover = parts.pop() ?? '';
|
|
1632
|
+
for (const line of parts) {
|
|
1633
|
+
lineNo++;
|
|
1634
|
+
if (regex.test(line)) {
|
|
1635
|
+
results.push(`${relPath}:${lineNo}: ${line}`);
|
|
1636
|
+
if (results.length >= maxResults)
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
} while (bytesRead > 0);
|
|
1641
|
+
}
|
|
1642
|
+
catch {
|
|
1643
|
+
// Unreadable file (e.g. EISDIR, binary read error) — skip it, mirroring
|
|
1644
|
+
// the previous readFileSync try/catch behavior.
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
// Flush any bytes the decoder was still holding.
|
|
1648
|
+
leftover += decoder.end();
|
|
1649
|
+
// Final partial line (file not ending in newline).
|
|
1650
|
+
if (leftover.length > 0) {
|
|
1651
|
+
lineNo++;
|
|
1652
|
+
if (regex.test(leftover) && results.length < maxResults) {
|
|
1653
|
+
results.push(`${relPath}:${lineNo}: ${leftover}`);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
finally {
|
|
1658
|
+
try {
|
|
1659
|
+
fs.closeSync(fd);
|
|
1660
|
+
}
|
|
1661
|
+
catch {
|
|
1662
|
+
/* ignore */
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1519
1666
|
/**
|
|
1520
1667
|
* Search file contents using a regex pattern (or literal string fallback).
|
|
1521
1668
|
*/
|
|
@@ -1568,25 +1715,21 @@ async function grepFiles(pattern, searchPath, cwd, globPattern, caseInsensitive)
|
|
|
1568
1715
|
for (const filePath of filesToSearch) {
|
|
1569
1716
|
if (results.length >= MAX_RESULTS)
|
|
1570
1717
|
break;
|
|
1571
|
-
let content;
|
|
1572
1718
|
try {
|
|
1573
1719
|
const fileStat = fs.statSync(filePath);
|
|
1720
|
+
if (!fileStat.isFile())
|
|
1721
|
+
continue; // skip dirs / symlinked dirs / sockets
|
|
1574
1722
|
if (fileStat.size > 5 * 1024 * 1024)
|
|
1575
1723
|
continue; // skip files > 5MB
|
|
1576
|
-
content = fs.readFileSync(filePath, 'utf-8');
|
|
1577
1724
|
}
|
|
1578
1725
|
catch {
|
|
1579
1726
|
continue;
|
|
1580
1727
|
}
|
|
1581
|
-
const lines = content.split('\n');
|
|
1582
1728
|
const relPath = path.relative(cwd, filePath);
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
results.push(`${relPath}:${lineIdx + 1}: ${lines[lineIdx]}`);
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1729
|
+
// Scan line-by-line without holding the whole file in memory (#154):
|
|
1730
|
+
// stream the file and only test each line as it is produced, breaking early
|
|
1731
|
+
// once MAX_RESULTS is reached.
|
|
1732
|
+
grepFileStreaming(filePath, regex, relPath, results, MAX_RESULTS);
|
|
1590
1733
|
}
|
|
1591
1734
|
if (results.length === 0) {
|
|
1592
1735
|
return 'No matches found';
|