@calliopelabs/cli 0.8.20 → 2.0.2
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/dist/agents/agent-config-loader.d.ts +60 -0
- package/dist/agents/agent-config-loader.d.ts.map +1 -0
- package/dist/agents/agent-config-loader.js +402 -0
- package/dist/agents/agent-config-loader.js.map +1 -0
- package/dist/agents/agent-config-presets.d.ts +10 -0
- package/dist/agents/agent-config-presets.d.ts.map +1 -0
- package/dist/agents/agent-config-presets.js +940 -0
- package/dist/agents/agent-config-presets.js.map +1 -0
- package/dist/agents/agent-config-types.d.ts +145 -0
- package/dist/agents/agent-config-types.d.ts.map +1 -0
- package/dist/agents/agent-config-types.js +12 -0
- package/dist/agents/agent-config-types.js.map +1 -0
- package/dist/{agterm → agents}/agent-detection.d.ts +1 -1
- package/dist/{agterm → agents}/agent-detection.d.ts.map +1 -1
- package/dist/{agterm → agents}/agent-detection.js +21 -5
- package/dist/agents/agent-detection.js.map +1 -0
- package/dist/agents/aggregator.d.ts +19 -0
- package/dist/agents/aggregator.d.ts.map +1 -0
- package/dist/agents/aggregator.js +141 -0
- package/dist/agents/aggregator.js.map +1 -0
- package/dist/{agterm → agents}/cli-backend.d.ts +1 -1
- package/dist/{agterm → agents}/cli-backend.d.ts.map +1 -1
- package/dist/{agterm → agents}/cli-backend.js +90 -12
- package/dist/agents/cli-backend.js.map +1 -0
- package/dist/agents/council-types.d.ts +113 -0
- package/dist/agents/council-types.d.ts.map +1 -0
- package/dist/agents/council-types.js +81 -0
- package/dist/agents/council-types.js.map +1 -0
- package/dist/agents/council.d.ts +107 -0
- package/dist/agents/council.d.ts.map +1 -0
- package/dist/agents/council.js +586 -0
- package/dist/agents/council.js.map +1 -0
- package/dist/agents/decomposer.d.ts +33 -0
- package/dist/agents/decomposer.d.ts.map +1 -0
- package/dist/agents/decomposer.js +138 -0
- package/dist/agents/decomposer.js.map +1 -0
- package/dist/agents/dynamic-tools.d.ts +52 -0
- package/dist/agents/dynamic-tools.d.ts.map +1 -0
- package/dist/agents/dynamic-tools.js +395 -0
- package/dist/agents/dynamic-tools.js.map +1 -0
- package/dist/agents/index.d.ts +29 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +29 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/installer.d.ts +39 -0
- package/dist/agents/installer.d.ts.map +1 -0
- package/dist/agents/installer.js +205 -0
- package/dist/agents/installer.js.map +1 -0
- package/dist/{agterm → agents}/orchestrator.d.ts +7 -2
- package/dist/agents/orchestrator.d.ts.map +1 -0
- package/dist/{agterm → agents}/orchestrator.js +22 -2
- package/dist/agents/orchestrator.js.map +1 -0
- package/dist/agents/sdk-backend.d.ts +63 -0
- package/dist/agents/sdk-backend.d.ts.map +1 -0
- package/dist/agents/sdk-backend.js +489 -0
- package/dist/agents/sdk-backend.js.map +1 -0
- package/dist/agents/swarm-types.d.ts +83 -0
- package/dist/agents/swarm-types.d.ts.map +1 -0
- package/dist/agents/swarm-types.js +20 -0
- package/dist/agents/swarm-types.js.map +1 -0
- package/dist/agents/swarm.d.ts +74 -0
- package/dist/agents/swarm.d.ts.map +1 -0
- package/dist/agents/swarm.js +307 -0
- package/dist/agents/swarm.js.map +1 -0
- package/dist/{agterm → agents}/tools.d.ts +7 -5
- package/dist/agents/tools.d.ts.map +1 -0
- package/dist/agents/tools.js +776 -0
- package/dist/agents/tools.js.map +1 -0
- package/dist/{agterm → agents}/types.d.ts +14 -2
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/{agterm → agents}/types.js +2 -2
- package/dist/agents/types.js.map +1 -0
- package/dist/api-server.d.ts +26 -0
- package/dist/api-server.d.ts.map +1 -0
- package/dist/api-server.js +230 -0
- package/dist/api-server.js.map +1 -0
- package/dist/auto-checkpoint.d.ts +35 -0
- package/dist/auto-checkpoint.d.ts.map +1 -0
- package/dist/auto-checkpoint.js +143 -0
- package/dist/auto-checkpoint.js.map +1 -0
- package/dist/auto-compressor.d.ts +44 -0
- package/dist/auto-compressor.d.ts.map +1 -0
- package/dist/auto-compressor.js +145 -0
- package/dist/auto-compressor.js.map +1 -0
- package/dist/background-jobs.d.ts +45 -0
- package/dist/background-jobs.d.ts.map +1 -0
- package/dist/background-jobs.js +122 -0
- package/dist/background-jobs.js.map +1 -0
- package/dist/bin.d.ts +6 -2
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +127 -24
- package/dist/bin.js.map +1 -1
- package/dist/checkpoint.d.ts +49 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +219 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/circuit-breaker/breaker.d.ts +80 -0
- package/dist/circuit-breaker/breaker.d.ts.map +1 -0
- package/dist/circuit-breaker/breaker.js +408 -0
- package/dist/circuit-breaker/breaker.js.map +1 -0
- package/dist/circuit-breaker/defaults.d.ts +8 -0
- package/dist/circuit-breaker/defaults.d.ts.map +1 -0
- package/dist/circuit-breaker/defaults.js +35 -0
- package/dist/circuit-breaker/defaults.js.map +1 -0
- package/dist/circuit-breaker/index.d.ts +9 -0
- package/dist/circuit-breaker/index.d.ts.map +1 -0
- package/dist/circuit-breaker/index.js +8 -0
- package/dist/circuit-breaker/index.js.map +1 -0
- package/dist/circuit-breaker/types.d.ts +77 -0
- package/dist/circuit-breaker/types.d.ts.map +1 -0
- package/dist/circuit-breaker/types.js +8 -0
- package/dist/circuit-breaker/types.js.map +1 -0
- package/dist/circuit-breaker.d.ts +8 -0
- package/dist/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker.js +7 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/cli/agent.d.ts +9 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/agent.js +262 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/cli/commands.d.ts +12 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/{cli.js → cli/commands.js} +285 -422
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +222 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/types.d.ts +30 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +20 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/companions.d.ts +54 -0
- package/dist/companions.d.ts.map +1 -0
- package/dist/companions.js +440 -0
- package/dist/companions.js.map +1 -0
- package/dist/config.d.ts +23 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +95 -22
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +27 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +415 -10
- package/dist/diff.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -11
- package/dist/errors.js.map +1 -1
- package/dist/git-status.d.ts +23 -0
- package/dist/git-status.d.ts.map +1 -0
- package/dist/git-status.js +92 -0
- package/dist/git-status.js.map +1 -0
- package/dist/headless.d.ts +25 -0
- package/dist/headless.d.ts.map +1 -0
- package/dist/headless.js +182 -0
- package/dist/headless.js.map +1 -0
- package/dist/hud/api.d.ts +35 -0
- package/dist/hud/api.d.ts.map +1 -0
- package/dist/hud/api.js +448 -0
- package/dist/hud/api.js.map +1 -0
- package/dist/hud/palettes.d.ts +9 -0
- package/dist/hud/palettes.d.ts.map +1 -0
- package/dist/hud/palettes.js +280 -0
- package/dist/hud/palettes.js.map +1 -0
- package/dist/hud/skins.d.ts +12 -0
- package/dist/hud/skins.d.ts.map +1 -0
- package/dist/hud/skins.js +365 -0
- package/dist/hud/skins.js.map +1 -0
- package/dist/hud/theme-packs/api.d.ts +51 -0
- package/dist/hud/theme-packs/api.d.ts.map +1 -0
- package/dist/hud/theme-packs/api.js +145 -0
- package/dist/hud/theme-packs/api.js.map +1 -0
- package/dist/hud/theme-packs/index.d.ts +18 -0
- package/dist/hud/theme-packs/index.d.ts.map +1 -0
- package/dist/hud/theme-packs/index.js +38 -0
- package/dist/hud/theme-packs/index.js.map +1 -0
- package/dist/hud/theme-packs/types.d.ts +29 -0
- package/dist/hud/theme-packs/types.d.ts.map +1 -0
- package/dist/hud/theme-packs/types.js +9 -0
- package/dist/hud/theme-packs/types.js.map +1 -0
- package/dist/hud/types.d.ts +182 -0
- package/dist/hud/types.d.ts.map +1 -0
- package/dist/hud/types.js +7 -0
- package/dist/hud/types.js.map +1 -0
- package/dist/idle-eviction.d.ts +34 -0
- package/dist/idle-eviction.d.ts.map +1 -0
- package/dist/idle-eviction.js +78 -0
- package/dist/idle-eviction.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/iteration-ledger.d.ts +105 -0
- package/dist/iteration-ledger.d.ts.map +1 -0
- package/dist/iteration-ledger.js +237 -0
- package/dist/iteration-ledger.js.map +1 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +1 -27
- package/dist/markdown.js.map +1 -1
- package/dist/mcp.d.ts +35 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +291 -7
- package/dist/mcp.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +12 -2
- package/dist/memory.js.map +1 -1
- package/dist/model-detection.d.ts +5 -0
- package/dist/model-detection.d.ts.map +1 -1
- package/dist/model-detection.js +278 -10
- package/dist/model-detection.js.map +1 -1
- package/dist/model-router.d.ts.map +1 -1
- package/dist/model-router.js +33 -11
- package/dist/model-router.js.map +1 -1
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +97 -6
- package/dist/plugins.js.map +1 -1
- package/dist/providers/anthropic.d.ts +10 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +221 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/bedrock.d.ts +17 -0
- package/dist/providers/bedrock.d.ts.map +1 -0
- package/dist/providers/bedrock.js +574 -0
- package/dist/providers/bedrock.js.map +1 -0
- package/dist/providers/compat.d.ts +13 -0
- package/dist/providers/compat.d.ts.map +1 -0
- package/dist/providers/compat.js +202 -0
- package/dist/providers/compat.js.map +1 -0
- package/dist/providers/google.d.ts +10 -0
- package/dist/providers/google.d.ts.map +1 -0
- package/dist/providers/google.js +203 -0
- package/dist/providers/google.js.map +1 -0
- package/dist/providers/index.d.ts +23 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +145 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ollama.d.ts +17 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +289 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +121 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +485 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/types.d.ts +63 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +164 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/sandbox-native.d.ts +59 -0
- package/dist/sandbox-native.d.ts.map +1 -0
- package/dist/sandbox-native.js +292 -0
- package/dist/sandbox-native.js.map +1 -0
- package/dist/sandbox.d.ts +2 -2
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +59 -13
- package/dist/sandbox.js.map +1 -1
- package/dist/scope.d.ts +3 -1
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +13 -1
- package/dist/scope.js.map +1 -1
- package/dist/session-timeout.d.ts +31 -0
- package/dist/session-timeout.d.ts.map +1 -0
- package/dist/session-timeout.js +100 -0
- package/dist/session-timeout.js.map +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +29 -17
- package/dist/setup.js.map +1 -1
- package/dist/smart-router.d.ts +73 -0
- package/dist/smart-router.d.ts.map +1 -0
- package/dist/smart-router.js +332 -0
- package/dist/smart-router.js.map +1 -0
- package/dist/storage.d.ts +19 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +164 -1
- package/dist/storage.js.map +1 -1
- package/dist/streaming.d.ts +4 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +12 -0
- package/dist/streaming.js.map +1 -1
- package/dist/styles.d.ts +32 -0
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +91 -0
- package/dist/styles.js.map +1 -1
- package/dist/summarization.d.ts +1 -1
- package/dist/summarization.js +4 -4
- package/dist/summarization.js.map +1 -1
- package/dist/terminal-image.d.ts +115 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +766 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal-recording.d.ts +55 -0
- package/dist/terminal-recording.d.ts.map +1 -0
- package/dist/terminal-recording.js +182 -0
- package/dist/terminal-recording.js.map +1 -0
- package/dist/themes.d.ts +19 -35
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +101 -210
- package/dist/themes.js.map +1 -1
- package/dist/tmux.d.ts +35 -0
- package/dist/tmux.d.ts.map +1 -0
- package/dist/tmux.js +106 -0
- package/dist/tmux.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +587 -45
- package/dist/tools.js.map +1 -1
- package/dist/trust.d.ts +53 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +154 -0
- package/dist/trust.js.map +1 -0
- package/dist/types.d.ts +7 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +70 -32
- package/dist/types.js.map +1 -1
- package/dist/ui/agent.d.ts +61 -0
- package/dist/ui/agent.d.ts.map +1 -0
- package/dist/ui/agent.js +768 -0
- package/dist/ui/agent.js.map +1 -0
- package/dist/ui/chat-input.d.ts +32 -0
- package/dist/ui/chat-input.d.ts.map +1 -0
- package/dist/ui/chat-input.js +355 -0
- package/dist/ui/chat-input.js.map +1 -0
- package/dist/ui/commands.d.ts +92 -0
- package/dist/ui/commands.d.ts.map +1 -0
- package/dist/ui/commands.js +3006 -0
- package/dist/ui/commands.js.map +1 -0
- package/dist/ui/completions.d.ts +22 -0
- package/dist/ui/completions.d.ts.map +1 -0
- package/dist/ui/completions.js +215 -0
- package/dist/ui/completions.js.map +1 -0
- package/dist/ui/components.d.ts +38 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/components.js +422 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/context.d.ts +12 -0
- package/dist/ui/context.d.ts.map +1 -0
- package/dist/ui/context.js +102 -0
- package/dist/ui/context.js.map +1 -0
- package/dist/ui/error-boundary.d.ts +33 -0
- package/dist/ui/error-boundary.d.ts.map +1 -0
- package/dist/ui/error-boundary.js +94 -0
- package/dist/ui/error-boundary.js.map +1 -0
- package/dist/ui/frame.d.ts +13 -0
- package/dist/ui/frame.d.ts.map +1 -0
- package/dist/ui/frame.js +89 -0
- package/dist/ui/frame.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +928 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/messages.d.ts +19 -0
- package/dist/ui/messages.d.ts.map +1 -0
- package/dist/ui/messages.js +181 -0
- package/dist/ui/messages.js.map +1 -0
- package/dist/ui/modals.d.ts +52 -0
- package/dist/ui/modals.d.ts.map +1 -0
- package/dist/ui/modals.js +204 -0
- package/dist/ui/modals.js.map +1 -0
- package/dist/ui/pack-picker.d.ts +12 -0
- package/dist/ui/pack-picker.d.ts.map +1 -0
- package/dist/ui/pack-picker.js +101 -0
- package/dist/ui/pack-picker.js.map +1 -0
- package/dist/ui/status-bar.d.ts +20 -0
- package/dist/ui/status-bar.d.ts.map +1 -0
- package/dist/ui/status-bar.js +41 -0
- package/dist/ui/status-bar.js.map +1 -0
- package/dist/ui/theme-picker.d.ts +24 -0
- package/dist/ui/theme-picker.d.ts.map +1 -0
- package/dist/ui/theme-picker.js +190 -0
- package/dist/ui/theme-picker.js.map +1 -0
- package/dist/ui/types.d.ts +62 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +7 -0
- package/dist/ui/types.js.map +1 -0
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +1 -9
- package/dist/version-check.js.map +1 -1
- package/package.json +8 -3
- package/dist/agterm/agent-detection.js.map +0 -1
- package/dist/agterm/cli-backend.js.map +0 -1
- package/dist/agterm/index.d.ts +0 -12
- package/dist/agterm/index.d.ts.map +0 -1
- package/dist/agterm/index.js +0 -15
- package/dist/agterm/index.js.map +0 -1
- package/dist/agterm/orchestrator.d.ts.map +0 -1
- package/dist/agterm/orchestrator.js.map +0 -1
- package/dist/agterm/tools.d.ts.map +0 -1
- package/dist/agterm/tools.js +0 -278
- package/dist/agterm/tools.js.map +0 -1
- package/dist/agterm/types.d.ts.map +0 -1
- package/dist/agterm/types.js.map +0 -1
- package/dist/cli.d.ts +0 -14
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/providers.d.ts +0 -51
- package/dist/providers.d.ts.map +0 -1
- package/dist/providers.js +0 -1146
- package/dist/providers.js.map +0 -1
- package/dist/ui-cli.d.ts +0 -17
- package/dist/ui-cli.d.ts.map +0 -1
- package/dist/ui-cli.js +0 -3730
- package/dist/ui-cli.js.map +0 -1
package/dist/tools.js
CHANGED
|
@@ -7,10 +7,13 @@ import { spawn } from 'child_process';
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import * as sandbox from './sandbox.js';
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
10
|
+
import * as nativeSandbox from './sandbox-native.js';
|
|
11
|
+
import { getAgtermTools, isAgtermTool, executeAgtermTool } from './agents/index.js';
|
|
12
|
+
import { validatePath as scopeValidatePath, isInScope } from './scope.js';
|
|
12
13
|
import { getPluginTools, isPluginTool, executePluginTool } from './plugins.js';
|
|
13
14
|
import config from './config.js';
|
|
15
|
+
import { applySkin, applyPalette, listSkins, listPalettes } from './hud/api.js';
|
|
16
|
+
import { listCompanions } from './companions.js';
|
|
14
17
|
/**
|
|
15
18
|
* Available tools for the agent
|
|
16
19
|
*/
|
|
@@ -92,6 +95,52 @@ export const TOOLS = [
|
|
|
92
95
|
required: ['thought'],
|
|
93
96
|
},
|
|
94
97
|
},
|
|
98
|
+
{
|
|
99
|
+
name: 'ask_question',
|
|
100
|
+
description: 'Ask the user a clarifying question. Use in plan mode to gather requirements before finalizing a plan. Can present multiple choice options or ask freeform questions.',
|
|
101
|
+
parameters: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
question: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: 'The question to ask the user',
|
|
107
|
+
},
|
|
108
|
+
options: {
|
|
109
|
+
type: 'array',
|
|
110
|
+
items: { type: 'string' },
|
|
111
|
+
description: 'Optional list of choices. If provided, displayed as numbered options. Omit for freeform questions.',
|
|
112
|
+
},
|
|
113
|
+
context: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
description: 'Optional context explaining why this question matters for the plan',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ['question'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'create_plan',
|
|
123
|
+
description: 'Create a structured execution plan for a complex task. Use this when a task requires multiple steps. The plan will be shown to the user for approval before execution begins. Available in all modes including plan mode.',
|
|
124
|
+
parameters: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
title: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'Brief title for the plan',
|
|
130
|
+
},
|
|
131
|
+
steps: {
|
|
132
|
+
type: 'array',
|
|
133
|
+
items: { type: 'string' },
|
|
134
|
+
description: 'Ordered list of steps to execute',
|
|
135
|
+
},
|
|
136
|
+
reasoning: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'Brief explanation of the approach',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ['title', 'steps'],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
95
144
|
{
|
|
96
145
|
name: 'execute_code',
|
|
97
146
|
description: 'Execute code in a sandboxed environment. Supports Python, Node.js, and shell scripts.',
|
|
@@ -148,6 +197,63 @@ export const TOOLS = [
|
|
|
148
197
|
required: ['operation'],
|
|
149
198
|
},
|
|
150
199
|
},
|
|
200
|
+
{
|
|
201
|
+
name: 'configure',
|
|
202
|
+
description: `Read, set, or list Calliope configuration options. Use this when the user asks to change settings, switch themes, providers, models, companions, or any preference through natural conversation. Always use action "list" first if you need to show available options.
|
|
203
|
+
|
|
204
|
+
CONFIGURABLE SETTINGS:
|
|
205
|
+
- defaultProvider: AI provider (anthropic, google, openai, together, openrouter, groq, fireworks, mistral, ollama, ai21, huggingface, litellm, bedrock, auto)
|
|
206
|
+
- defaultModel: Model name string (provider-specific, e.g. "claude-sonnet-4-20250514", "gemini-2.0-flash", "gpt-4o")
|
|
207
|
+
- persona: Agent persona style (calliope, muse, minimal)
|
|
208
|
+
- maxIterations: Max agent loop iterations (0 = unlimited)
|
|
209
|
+
- maxIterationTime: Max seconds per iteration (0 = no limit, default: 600)
|
|
210
|
+
- fancyOutput: Enable rich formatting (true/false)
|
|
211
|
+
- autoSaveHistory: Auto-save session history (true/false)
|
|
212
|
+
- autoUpgrade: Check for updates on startup (true/false)
|
|
213
|
+
- collapseTools: Auto-collapse tool output (true/false)
|
|
214
|
+
- collapseThinking: Auto-collapse think blocks (true/false)
|
|
215
|
+
- toolDisplayLimit: Show last N tools expanded (0 = all)
|
|
216
|
+
- layout: UI layout (classic, response-top, response-bottom, split, zen, focus, dashboard, minimal)
|
|
217
|
+
- density: Display density (normal, compact)
|
|
218
|
+
- activeSkin: Terminal skin/theme name (use action "list" category "skins" to see options)
|
|
219
|
+
- activePalette: Color palette name (use action "list" category "palettes" to see options)
|
|
220
|
+
- activeCompanion: AI companion personality (use action "list" category "companions" to see options)
|
|
221
|
+
- companionIntensity: Companion personality level (professional, immersive)
|
|
222
|
+
- useEmojis: Show emojis in UI (true/false)
|
|
223
|
+
- diffStyle: Diff display format (inline, unified, side-by-side)
|
|
224
|
+
- borderStyle: UI border style (rounded, sharp, double, ascii, none)
|
|
225
|
+
- bannerStyle: Startup banner (full, compact, none)
|
|
226
|
+
- circuitBreakersEnabled: Safety circuit breakers (true/false)
|
|
227
|
+
- sandboxMode: Code execution sandbox (auto, native, docker, off)
|
|
228
|
+
- smartRoutingEnabled: Dynamic model routing (true/false)
|
|
229
|
+
- smartRoutingCostSensitivity: Cost vs quality (0-1, 0=best quality, 1=cheapest)
|
|
230
|
+
- recordSessions: Record session audit logs (true/false)
|
|
231
|
+
- recordingRetentionDays: Auto-delete old recordings after N days (0 = keep forever)`,
|
|
232
|
+
parameters: {
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: {
|
|
235
|
+
action: {
|
|
236
|
+
type: 'string',
|
|
237
|
+
description: 'Action to perform: "get" reads a setting, "set" changes a setting, "list" shows available options for a category',
|
|
238
|
+
enum: ['get', 'set', 'list'],
|
|
239
|
+
},
|
|
240
|
+
key: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
description: 'Config key name (for get/set actions)',
|
|
243
|
+
},
|
|
244
|
+
value: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
description: 'New value to set (for set action). Use "true"/"false" for booleans, numbers as strings.',
|
|
247
|
+
},
|
|
248
|
+
category: {
|
|
249
|
+
type: 'string',
|
|
250
|
+
description: 'Category to list options for (for list action): skins, palettes, companions, providers, layouts, all',
|
|
251
|
+
enum: ['skins', 'palettes', 'companions', 'providers', 'layouts', 'all'],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
required: ['action'],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
151
257
|
{
|
|
152
258
|
name: 'mermaid',
|
|
153
259
|
description: 'Generate a Mermaid diagram. The output can be rendered as a visual graph.',
|
|
@@ -174,11 +280,11 @@ export const TOOLS = [
|
|
|
174
280
|
];
|
|
175
281
|
/**
|
|
176
282
|
* Get all available tools
|
|
177
|
-
* Includes
|
|
283
|
+
* Includes agent tools when agentEnabled is true
|
|
178
284
|
*/
|
|
179
|
-
export function getTools(
|
|
285
|
+
export function getTools(agentEnabled = false) {
|
|
180
286
|
const pluginTools = getPluginTools();
|
|
181
|
-
if (
|
|
287
|
+
if (agentEnabled) {
|
|
182
288
|
return [...TOOLS, ...getAgtermTools(), ...pluginTools];
|
|
183
289
|
}
|
|
184
290
|
return [...TOOLS, ...pluginTools];
|
|
@@ -187,30 +293,28 @@ export function getTools(agtermEnabled = false) {
|
|
|
187
293
|
* Validate path is within allowed directory (prevent path traversal)
|
|
188
294
|
*/
|
|
189
295
|
function validatePath(filePath, cwd) {
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!resolved.startsWith(normalizedCwd) && !resolved.startsWith('/tmp')) {
|
|
296
|
+
// Check raw input for null bytes before any resolution (path injection attack)
|
|
297
|
+
if (filePath.includes('\0')) {
|
|
298
|
+
throw new Error(`Invalid path: contains null bytes`);
|
|
299
|
+
}
|
|
300
|
+
// Check raw input for path traversal attempts before resolution
|
|
301
|
+
if (filePath.includes('..')) {
|
|
302
|
+
const resolved = path.resolve(cwd, filePath);
|
|
303
|
+
const normalizedCwd = path.resolve(cwd);
|
|
304
|
+
if (!resolved.startsWith(normalizedCwd + path.sep) && resolved !== normalizedCwd && !resolved.startsWith('/tmp/') && resolved !== '/tmp') {
|
|
199
305
|
throw new Error(`Path traversal detected: ${filePath} resolves outside allowed scope`);
|
|
200
306
|
}
|
|
201
307
|
}
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
throw new Error(`Invalid path: contains null bytes`);
|
|
205
|
-
}
|
|
308
|
+
// Primary validation via scope manager
|
|
309
|
+
const validated = scopeValidatePath(filePath, cwd);
|
|
206
310
|
return validated;
|
|
207
311
|
}
|
|
208
312
|
/**
|
|
209
313
|
* Execute a tool call
|
|
210
314
|
*/
|
|
211
|
-
export async function executeTool(toolCall, cwd, timeout = 60000) {
|
|
315
|
+
export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
212
316
|
const { id, name, arguments: args } = toolCall;
|
|
213
|
-
// Handle
|
|
317
|
+
// Handle agent tools
|
|
214
318
|
if (isAgtermTool(name)) {
|
|
215
319
|
return executeAgtermTool(toolCall, cwd);
|
|
216
320
|
}
|
|
@@ -225,7 +329,7 @@ export async function executeTool(toolCall, cwd, timeout = 60000) {
|
|
|
225
329
|
if (typeof args.command !== 'string') {
|
|
226
330
|
return { toolCallId: id, result: 'Error: command must be a string', isError: true };
|
|
227
331
|
}
|
|
228
|
-
result = await executeShell(args.command, cwd, timeout);
|
|
332
|
+
result = await executeShell(args.command, cwd, timeout, onOutput);
|
|
229
333
|
break;
|
|
230
334
|
}
|
|
231
335
|
case 'read_file': {
|
|
@@ -258,6 +362,39 @@ export async function executeTool(toolCall, cwd, timeout = 60000) {
|
|
|
258
362
|
result = 'Thought recorded.';
|
|
259
363
|
break;
|
|
260
364
|
}
|
|
365
|
+
case 'ask_question': {
|
|
366
|
+
if (typeof args.question !== 'string') {
|
|
367
|
+
return { toolCallId: id, result: 'Error: question must be a string', isError: true };
|
|
368
|
+
}
|
|
369
|
+
// The actual question display is handled by the UI layer (agent.ts)
|
|
370
|
+
// This just returns a placeholder that gets replaced with the user's answer
|
|
371
|
+
const options = Array.isArray(args.options) ? args.options : undefined;
|
|
372
|
+
const contextNote = typeof args.context === 'string' ? args.context : undefined;
|
|
373
|
+
let questionDisplay = args.question;
|
|
374
|
+
if (contextNote)
|
|
375
|
+
questionDisplay += `\n Context: ${contextNote}`;
|
|
376
|
+
if (options)
|
|
377
|
+
questionDisplay += '\n' + options.map((o, i) => ` ${i + 1}. ${o}`).join('\n');
|
|
378
|
+
result = `QUESTION:${questionDisplay}`;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case 'create_plan': {
|
|
382
|
+
if (typeof args.title !== 'string') {
|
|
383
|
+
return { toolCallId: id, result: 'Error: title must be a string', isError: true };
|
|
384
|
+
}
|
|
385
|
+
if (!Array.isArray(args.steps) || args.steps.length === 0) {
|
|
386
|
+
return { toolCallId: id, result: 'Error: steps must be a non-empty array of strings', isError: true };
|
|
387
|
+
}
|
|
388
|
+
const planTitle = args.title;
|
|
389
|
+
const planSteps = args.steps;
|
|
390
|
+
const planReasoning = typeof args.reasoning === 'string' ? args.reasoning : undefined;
|
|
391
|
+
let planDisplay = `PLAN:${planTitle}\n`;
|
|
392
|
+
if (planReasoning)
|
|
393
|
+
planDisplay += `Approach: ${planReasoning}\n`;
|
|
394
|
+
planDisplay += '\n' + planSteps.map((s, i) => ` ${i + 1}. [ ] ${s}`).join('\n');
|
|
395
|
+
result = planDisplay;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
261
398
|
case 'execute_code': {
|
|
262
399
|
if (typeof args.language !== 'string' || !['python', 'node', 'bash'].includes(args.language)) {
|
|
263
400
|
return { toolCallId: id, result: 'Error: language must be python, node, or bash', isError: true };
|
|
@@ -286,6 +423,134 @@ export async function executeTool(toolCall, cwd, timeout = 60000) {
|
|
|
286
423
|
result = await executeGit(args.operation, gitArgs, cwd);
|
|
287
424
|
break;
|
|
288
425
|
}
|
|
426
|
+
case 'configure': {
|
|
427
|
+
const action = args.action;
|
|
428
|
+
if (!action || !['get', 'set', 'list'].includes(action)) {
|
|
429
|
+
return { toolCallId: id, result: 'Error: action must be "get", "set", or "list"', isError: true };
|
|
430
|
+
}
|
|
431
|
+
if (action === 'list') {
|
|
432
|
+
const category = args.category || 'all';
|
|
433
|
+
const sections = [];
|
|
434
|
+
if (category === 'skins' || category === 'all') {
|
|
435
|
+
const skins = listSkins();
|
|
436
|
+
const current = config.get('activeSkin');
|
|
437
|
+
sections.push('SKINS (activeSkin):\n' + skins.map(s => ` ${s.name === current ? '→ ' : ' '}${s.name} - ${s.description}`).join('\n'));
|
|
438
|
+
}
|
|
439
|
+
if (category === 'palettes' || category === 'all') {
|
|
440
|
+
const palettes = listPalettes();
|
|
441
|
+
const current = config.get('activePalette');
|
|
442
|
+
sections.push('PALETTES (activePalette):\n' + palettes.map(p => ` ${p.name === current ? '→ ' : ' '}${p.name} - ${p.description}`).join('\n'));
|
|
443
|
+
}
|
|
444
|
+
if (category === 'companions' || category === 'all') {
|
|
445
|
+
const companions = listCompanions();
|
|
446
|
+
const current = config.get('activeCompanion');
|
|
447
|
+
sections.push('COMPANIONS (activeCompanion):\n' + companions.map(c => ` ${c.name === current ? '→ ' : ' '}${c.name} - ${c.description}`).join('\n'));
|
|
448
|
+
}
|
|
449
|
+
if (category === 'providers' || category === 'all') {
|
|
450
|
+
const providers = ['anthropic', 'google', 'openai', 'together', 'openrouter', 'groq', 'fireworks', 'mistral', 'ollama', 'ai21', 'huggingface', 'litellm', 'bedrock', 'auto'];
|
|
451
|
+
const current = config.get('defaultProvider');
|
|
452
|
+
sections.push('PROVIDERS (defaultProvider):\n' + providers.map(p => ` ${p === current ? '→ ' : ' '}${p}`).join('\n'));
|
|
453
|
+
}
|
|
454
|
+
if (category === 'layouts' || category === 'all') {
|
|
455
|
+
const layouts = ['classic', 'response-top', 'response-bottom', 'split', 'zen', 'focus', 'dashboard', 'minimal'];
|
|
456
|
+
const current = config.get('layout');
|
|
457
|
+
sections.push('LAYOUTS (layout):\n' + layouts.map(l => ` ${l === current ? '→ ' : ' '}${l}`).join('\n'));
|
|
458
|
+
}
|
|
459
|
+
if (category === 'all') {
|
|
460
|
+
// Also show current key settings
|
|
461
|
+
const currentSettings = [
|
|
462
|
+
`density: ${config.get('density')}`,
|
|
463
|
+
`companionIntensity: ${config.get('companionIntensity')}`,
|
|
464
|
+
`useEmojis: ${config.get('useEmojis')}`,
|
|
465
|
+
`diffStyle: ${config.get('diffStyle')}`,
|
|
466
|
+
`borderStyle: ${config.get('borderStyle')}`,
|
|
467
|
+
`bannerStyle: ${config.get('bannerStyle')}`,
|
|
468
|
+
`sandboxMode: ${config.get('sandboxMode')}`,
|
|
469
|
+
`smartRoutingEnabled: ${config.get('smartRoutingEnabled')}`,
|
|
470
|
+
`defaultModel: ${config.get('defaultModel') || '(auto)'}`,
|
|
471
|
+
];
|
|
472
|
+
sections.push('CURRENT SETTINGS:\n' + currentSettings.map(s => ` ${s}`).join('\n'));
|
|
473
|
+
}
|
|
474
|
+
result = sections.join('\n\n');
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
if (action === 'get') {
|
|
478
|
+
const key = args.key;
|
|
479
|
+
if (!key) {
|
|
480
|
+
return { toolCallId: id, result: 'Error: key is required for get action', isError: true };
|
|
481
|
+
}
|
|
482
|
+
const val = config.get(key);
|
|
483
|
+
result = `${key} = ${JSON.stringify(val)}`;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
// action === 'set'
|
|
487
|
+
const key = args.key;
|
|
488
|
+
const rawValue = args.value;
|
|
489
|
+
if (!key) {
|
|
490
|
+
return { toolCallId: id, result: 'Error: key is required for set action', isError: true };
|
|
491
|
+
}
|
|
492
|
+
if (rawValue === undefined || rawValue === null) {
|
|
493
|
+
return { toolCallId: id, result: 'Error: value is required for set action', isError: true };
|
|
494
|
+
}
|
|
495
|
+
// Only allow setting safe keys through conversation (allowlist)
|
|
496
|
+
const SAFE_CONFIG_KEYS = new Set([
|
|
497
|
+
'defaultProvider', 'defaultModel', 'persona', 'maxIterations', 'maxIterationTime',
|
|
498
|
+
'fancyOutput', 'autoSaveHistory', 'autoUpgrade',
|
|
499
|
+
'collapseTools', 'collapseThinking', 'toolDisplayLimit',
|
|
500
|
+
'layout', 'density',
|
|
501
|
+
'activeSkin', 'activePalette', 'activeCompanion', 'activeThemePack',
|
|
502
|
+
'companionIntensity', 'useEmojis', 'diffStyle', 'borderStyle', 'bannerStyle',
|
|
503
|
+
'circuitBreakersEnabled', 'sandboxMode',
|
|
504
|
+
'smartRoutingEnabled', 'smartRoutingCostSensitivity',
|
|
505
|
+
'recordSessions', 'recordingRetentionDays',
|
|
506
|
+
'awsRegion', 'awsProfile',
|
|
507
|
+
]);
|
|
508
|
+
if (!SAFE_CONFIG_KEYS.has(key)) {
|
|
509
|
+
return { toolCallId: id, result: `Error: "${key}" cannot be set through conversation. Use /keys command, environment variables, or edit the config file directly.`, isError: true };
|
|
510
|
+
}
|
|
511
|
+
// Parse the value to the correct type
|
|
512
|
+
let parsedValue = rawValue;
|
|
513
|
+
if (rawValue === 'true')
|
|
514
|
+
parsedValue = true;
|
|
515
|
+
else if (rawValue === 'false')
|
|
516
|
+
parsedValue = false;
|
|
517
|
+
else if (/^\d+(\.\d+)?$/.test(rawValue))
|
|
518
|
+
parsedValue = Number(rawValue);
|
|
519
|
+
try {
|
|
520
|
+
// Special handling for HUD settings that need apply functions
|
|
521
|
+
if (key === 'activeSkin') {
|
|
522
|
+
const success = applySkin(rawValue);
|
|
523
|
+
if (!success) {
|
|
524
|
+
return { toolCallId: id, result: `Error: skin "${rawValue}" not found. Use action "list" category "skins" to see available skins.`, isError: true };
|
|
525
|
+
}
|
|
526
|
+
result = `Skin changed to "${rawValue}"`;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
if (key === 'activePalette') {
|
|
530
|
+
const success = applyPalette(rawValue);
|
|
531
|
+
if (!success) {
|
|
532
|
+
return { toolCallId: id, result: `Error: palette "${rawValue}" not found. Use action "list" category "palettes" to see available palettes.`, isError: true };
|
|
533
|
+
}
|
|
534
|
+
result = `Palette changed to "${rawValue}"`;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
if (key === 'activeCompanion') {
|
|
538
|
+
if (!listCompanions().some(c => c.name === rawValue)) {
|
|
539
|
+
return { toolCallId: id, result: `Error: companion "${rawValue}" not found. Use action "list" category "companions" to see available companions.`, isError: true };
|
|
540
|
+
}
|
|
541
|
+
config.set('activeCompanion', rawValue);
|
|
542
|
+
result = `Companion changed to "${rawValue}"`;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
// Generic config set
|
|
546
|
+
config.set(key, parsedValue);
|
|
547
|
+
result = `Set ${key} = ${JSON.stringify(parsedValue)}`;
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
return { toolCallId: id, result: `Error setting ${key}: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
289
554
|
case 'mermaid': {
|
|
290
555
|
const diagramType = typeof args.type === 'string' ? args.type : 'flowchart';
|
|
291
556
|
if (typeof args.content !== 'string') {
|
|
@@ -298,17 +563,207 @@ export async function executeTool(toolCall, cwd, timeout = 60000) {
|
|
|
298
563
|
default:
|
|
299
564
|
return { toolCallId: id, result: `Unknown tool: ${name}`, isError: true };
|
|
300
565
|
}
|
|
301
|
-
|
|
566
|
+
// Generate human-friendly display summary for large results (#25)
|
|
567
|
+
const lines = result.split('\n');
|
|
568
|
+
let displayResult;
|
|
569
|
+
if (lines.length > 10) {
|
|
570
|
+
const preview = lines.slice(0, 5).join('\n');
|
|
571
|
+
displayResult = `${preview}\n... (${lines.length - 5} more lines)`;
|
|
572
|
+
}
|
|
573
|
+
return { toolCallId: id, result, displayResult };
|
|
302
574
|
}
|
|
303
575
|
catch (error) {
|
|
304
576
|
const msg = error instanceof Error ? error.message : String(error);
|
|
305
577
|
return { toolCallId: id, result: `Error: ${msg}`, isError: true };
|
|
306
578
|
}
|
|
307
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Commands that are blocked outright (not just flagged as risky).
|
|
582
|
+
* These are destructive system-level commands that should never be run by an agent.
|
|
583
|
+
*
|
|
584
|
+
* Patterns are tested against the normalized command (see normalizeCommand())
|
|
585
|
+
* to defeat common bypass techniques like quoting, env prefixes, and subshells.
|
|
586
|
+
*/
|
|
587
|
+
const BLOCKED_COMMANDS = [
|
|
588
|
+
/^sudo\s/,
|
|
589
|
+
/^su\s/,
|
|
590
|
+
/^rm\s+-rf\s+\//, // rm -rf /
|
|
591
|
+
/^rm\s+-fr\s+\//,
|
|
592
|
+
/^rm\s+-rf\s+~/, // rm -rf ~
|
|
593
|
+
/^rm\s+-fr\s+~/,
|
|
594
|
+
/^dd\s+.*of=\/dev\//, // dd to block devices
|
|
595
|
+
/^mkfs/,
|
|
596
|
+
/^fdisk/,
|
|
597
|
+
/^parted/,
|
|
598
|
+
/^format/,
|
|
599
|
+
/>\s*\/dev\//, // redirect to devices
|
|
600
|
+
/^chmod\s+-R\s+777/,
|
|
601
|
+
/^curl.*\|\s*(sh|bash)/, // pipe to shell
|
|
602
|
+
/^wget.*\|\s*(sh|bash)/,
|
|
603
|
+
/\|\s*sh(\s|;|$)/, // pipe to sh (anywhere, not just end)
|
|
604
|
+
/\|\s*bash(\s|;|$)/, // pipe to bash (anywhere, not just end)
|
|
605
|
+
/\|\s*zsh(\s|;|$)/, // pipe to zsh
|
|
606
|
+
/bash\s+<\(/, // process substitution: bash <(...)
|
|
607
|
+
/sh\s+<\(/, // process substitution: sh <(...)
|
|
608
|
+
/zsh\s+<\(/, // process substitution: zsh <(...)
|
|
609
|
+
];
|
|
610
|
+
/**
|
|
611
|
+
* Normalize a shell command to defeat common blocklist bypass techniques (#60).
|
|
612
|
+
*
|
|
613
|
+
* Handles:
|
|
614
|
+
* - Leading env-var assignments: \`VAR=1 sudo ...\` -> \`sudo ...\`
|
|
615
|
+
* - Subshell wrapping: \`(sudo rm ...)\` -> \`sudo rm ...\`
|
|
616
|
+
* - Quote insertion: \`'su'do\` or \`"su"do\` -> \`sudo\`
|
|
617
|
+
* - Backslash escaping: \`su\do\` -> \`sudo\`
|
|
618
|
+
*
|
|
619
|
+
* The result is used only for blocklist matching; the original command is still
|
|
620
|
+
* passed to the shell for execution.
|
|
621
|
+
*/
|
|
622
|
+
function normalizeCommand(command) {
|
|
623
|
+
let cmd = command.trim();
|
|
624
|
+
// Strip leading subshell / group wrappers: ( ... ), { ... }
|
|
625
|
+
while ((cmd.startsWith('(') && cmd.endsWith(')')) ||
|
|
626
|
+
(cmd.startsWith('{') && cmd.endsWith('}'))) {
|
|
627
|
+
cmd = cmd.slice(1, -1).trim();
|
|
628
|
+
}
|
|
629
|
+
// Strip leading env-var assignments: FOO=bar BAZ="qux" command ...
|
|
630
|
+
cmd = cmd.replace(/^(\s*[A-Za-z_][A-Za-z0-9_]*=\S*\s+)+/, '');
|
|
631
|
+
// Remove inserted quotes that break up words: 'su'do -> sudo, "su"do -> sudo
|
|
632
|
+
cmd = cmd.replace(/['"]/g, '');
|
|
633
|
+
// Remove backslash escapes: su\do -> sudo
|
|
634
|
+
cmd = cmd.replace(/\\(.)/g, '$1');
|
|
635
|
+
return cmd.trim();
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Check a command (and all sub-commands separated by ; or &&/||) against
|
|
639
|
+
* the blocklist. Returns the matching pattern source string, or null if allowed.
|
|
640
|
+
*/
|
|
641
|
+
function matchesBlocklist(command) {
|
|
642
|
+
// Split on command separators to check each sub-command
|
|
643
|
+
const subCommands = command.split(/\s*(?:;|&&|\|\|)\s*/);
|
|
644
|
+
for (const sub of subCommands) {
|
|
645
|
+
const normalized = normalizeCommand(sub);
|
|
646
|
+
for (const pattern of BLOCKED_COMMANDS) {
|
|
647
|
+
if (pattern.test(normalized)) {
|
|
648
|
+
return pattern.source;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Also test the full normalized command (for patterns that span separators, like pipes)
|
|
653
|
+
const fullNormalized = normalizeCommand(command);
|
|
654
|
+
for (const pattern of BLOCKED_COMMANDS) {
|
|
655
|
+
if (pattern.test(fullNormalized)) {
|
|
656
|
+
return pattern.source;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Extract file paths from a shell command for scope validation (#63).
|
|
663
|
+
*
|
|
664
|
+
* Looks for common file-access commands (cat, cp, mv, head, tail, etc.) and
|
|
665
|
+
* extracts the path arguments. Only absolute paths and paths starting with ~/
|
|
666
|
+
* are extracted, since relative paths are within the cwd which is already in scope.
|
|
667
|
+
*
|
|
668
|
+
* Returns an array of extracted paths (may be empty).
|
|
669
|
+
*/
|
|
670
|
+
function extractFilePathsFromCommand(command) {
|
|
671
|
+
const paths = [];
|
|
672
|
+
// Commands that read or write files, followed by path arguments
|
|
673
|
+
const fileCommands = [
|
|
674
|
+
'cat', 'head', 'tail', 'less', 'more', 'cp', 'mv', 'rm',
|
|
675
|
+
'tee', 'touch', 'chmod', 'chown', 'ln', 'readlink',
|
|
676
|
+
'source', '\\.',
|
|
677
|
+
];
|
|
678
|
+
const cmdPattern = new RegExp('(?:^|[;&|]\\s*)(?:' + fileCommands.join('|') + ')\\s+' +
|
|
679
|
+
'(?:-[^\\s]*\\s+)*' +
|
|
680
|
+
'((?:\\/|~\\/)[^\\s;|&>]+)', 'g');
|
|
681
|
+
let match;
|
|
682
|
+
while ((match = cmdPattern.exec(command)) !== null) {
|
|
683
|
+
let p = match[1];
|
|
684
|
+
if (p.startsWith('~/')) {
|
|
685
|
+
p = path.join(process.env.HOME || '/tmp', p.slice(2));
|
|
686
|
+
}
|
|
687
|
+
p = p.replace(/['"]+$/, '');
|
|
688
|
+
paths.push(p);
|
|
689
|
+
}
|
|
690
|
+
// Also catch redirection targets: > /path, >> /path
|
|
691
|
+
const redirectPattern = />{1,2}\s*((?:\/|~\/)[^\s;|&]+)/g;
|
|
692
|
+
while ((match = redirectPattern.exec(command)) !== null) {
|
|
693
|
+
let p = match[1];
|
|
694
|
+
if (p.startsWith('~/')) {
|
|
695
|
+
p = path.join(process.env.HOME || '/tmp', p.slice(2));
|
|
696
|
+
}
|
|
697
|
+
p = p.replace(/['"]+$/, '');
|
|
698
|
+
paths.push(p);
|
|
699
|
+
}
|
|
700
|
+
return paths;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Validate that a shell command does not access files outside scope (#63).
|
|
704
|
+
* Returns an error message if a path violation is found, or null if ok.
|
|
705
|
+
*/
|
|
706
|
+
function validateShellPaths(command, cwd) {
|
|
707
|
+
const extractedPaths = extractFilePathsFromCommand(command);
|
|
708
|
+
for (const p of extractedPaths) {
|
|
709
|
+
const allowed = isInScope(p, cwd);
|
|
710
|
+
if (!allowed) {
|
|
711
|
+
return 'Shell command blocked: "' + p + '" is outside allowed scope. Use /add-dir to expand scope.';
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Determine whether to use native sandboxing for shell commands based on config.
|
|
718
|
+
*
|
|
719
|
+
* sandboxMode values:
|
|
720
|
+
* - 'auto': use native sandbox when available, otherwise run unsandboxed
|
|
721
|
+
* - 'native': require native sandbox (fail if unavailable)
|
|
722
|
+
* - 'docker': defer to Docker sandbox (handled elsewhere)
|
|
723
|
+
* - 'off': no sandboxing
|
|
724
|
+
*/
|
|
725
|
+
function shouldUseNativeSandbox() {
|
|
726
|
+
const mode = config.get('sandboxMode') || 'auto';
|
|
727
|
+
if (mode === 'off' || mode === 'docker')
|
|
728
|
+
return 'skip';
|
|
729
|
+
if (mode === 'native')
|
|
730
|
+
return 'require';
|
|
731
|
+
// 'auto': use if available
|
|
732
|
+
return nativeSandbox.isNativeSandboxAvailable() ? 'use' : 'skip';
|
|
733
|
+
}
|
|
308
734
|
/**
|
|
309
735
|
* Execute a shell command
|
|
310
736
|
*/
|
|
311
|
-
async function executeShell(command, cwd, timeout) {
|
|
737
|
+
async function executeShell(command, cwd, timeout, onOutput) {
|
|
738
|
+
// Check against blocked command patterns using normalized matching (#60)
|
|
739
|
+
const blocked = matchesBlocklist(command);
|
|
740
|
+
if (blocked) {
|
|
741
|
+
return `Error: Command blocked for safety. Pattern "${blocked}" is not allowed.`;
|
|
742
|
+
}
|
|
743
|
+
// Check file paths in shell commands against scope (#63)
|
|
744
|
+
const scopeError = validateShellPaths(command, cwd);
|
|
745
|
+
if (scopeError) {
|
|
746
|
+
return `Error: ${scopeError}`;
|
|
747
|
+
}
|
|
748
|
+
// Check if native sandbox should be used
|
|
749
|
+
const sandboxDecision = shouldUseNativeSandbox();
|
|
750
|
+
if (sandboxDecision === 'require' && !nativeSandbox.isNativeSandboxAvailable()) {
|
|
751
|
+
return 'Error: Native sandbox required (sandboxMode=native) but not available on this platform.';
|
|
752
|
+
}
|
|
753
|
+
if (sandboxDecision === 'use' || sandboxDecision === 'require') {
|
|
754
|
+
const result = await nativeSandbox.executeInNativeSandbox(command, cwd, {
|
|
755
|
+
timeout,
|
|
756
|
+
networkEnabled: true,
|
|
757
|
+
});
|
|
758
|
+
// Shell tool output is transparent — same format as unsandboxed execution
|
|
759
|
+
let output = result.stdout + (result.stderr ? `\nstderr: ${result.stderr}` : '');
|
|
760
|
+
if (result.exitCode !== 0) {
|
|
761
|
+
return `Exit code ${result.exitCode}\n${output}`;
|
|
762
|
+
}
|
|
763
|
+
return output || '(no output)';
|
|
764
|
+
}
|
|
765
|
+
// Fallback: unsandboxed execution
|
|
766
|
+
const MAX_OUTPUT_SIZE = 50000; // 50K chars max output
|
|
312
767
|
return new Promise((resolve, reject) => {
|
|
313
768
|
const proc = spawn('bash', ['-c', command], {
|
|
314
769
|
cwd,
|
|
@@ -317,11 +772,30 @@ async function executeShell(command, cwd, timeout) {
|
|
|
317
772
|
});
|
|
318
773
|
let stdout = '';
|
|
319
774
|
let stderr = '';
|
|
775
|
+
let truncated = false;
|
|
320
776
|
proc.stdout.on('data', (data) => {
|
|
321
|
-
|
|
777
|
+
const chunk = data.toString();
|
|
778
|
+
if (onOutput)
|
|
779
|
+
onOutput(chunk);
|
|
780
|
+
if (!truncated) {
|
|
781
|
+
stdout += chunk;
|
|
782
|
+
if (stdout.length > MAX_OUTPUT_SIZE) {
|
|
783
|
+
stdout = stdout.slice(0, MAX_OUTPUT_SIZE);
|
|
784
|
+
truncated = true;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
322
787
|
});
|
|
323
788
|
proc.stderr.on('data', (data) => {
|
|
324
|
-
|
|
789
|
+
const chunk = data.toString();
|
|
790
|
+
if (onOutput)
|
|
791
|
+
onOutput(chunk);
|
|
792
|
+
if (!truncated) {
|
|
793
|
+
stderr += chunk;
|
|
794
|
+
if (stderr.length > MAX_OUTPUT_SIZE) {
|
|
795
|
+
stderr = stderr.slice(0, MAX_OUTPUT_SIZE);
|
|
796
|
+
truncated = true;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
325
799
|
});
|
|
326
800
|
const timer = setTimeout(() => {
|
|
327
801
|
proc.kill('SIGTERM');
|
|
@@ -329,7 +803,10 @@ async function executeShell(command, cwd, timeout) {
|
|
|
329
803
|
}, timeout);
|
|
330
804
|
proc.on('close', (code) => {
|
|
331
805
|
clearTimeout(timer);
|
|
332
|
-
|
|
806
|
+
let output = stdout + (stderr ? `\nstderr: ${stderr}` : '');
|
|
807
|
+
if (truncated) {
|
|
808
|
+
output += '\n\n[Output truncated at 50K chars. Use head/tail/grep to filter.]';
|
|
809
|
+
}
|
|
333
810
|
if (code !== 0) {
|
|
334
811
|
resolve(`Exit code ${code}\n${output}`);
|
|
335
812
|
}
|
|
@@ -446,6 +923,16 @@ async function writeFile(filePath, content, cwd) {
|
|
|
446
923
|
// Ignore errors reading old content
|
|
447
924
|
}
|
|
448
925
|
}
|
|
926
|
+
// Auto-checkpoint before overwriting existing files (#20)
|
|
927
|
+
if (!isNewFile && oldContent) {
|
|
928
|
+
try {
|
|
929
|
+
const { createCheckpoint } = require('./checkpoint.js');
|
|
930
|
+
createCheckpoint(absPath, oldContent);
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
// Checkpoint module not available or failed - continue with write
|
|
934
|
+
}
|
|
935
|
+
}
|
|
449
936
|
fs.writeFileSync(absPath, content);
|
|
450
937
|
// Generate diff output
|
|
451
938
|
if (isNewFile) {
|
|
@@ -519,29 +1006,84 @@ function listFilesRecursive(dir, prefix, depth) {
|
|
|
519
1006
|
return lines.join('\n');
|
|
520
1007
|
}
|
|
521
1008
|
/**
|
|
522
|
-
* Execute code in a sandboxed environment
|
|
1009
|
+
* Execute code in a sandboxed environment.
|
|
1010
|
+
*
|
|
1011
|
+
* Sandbox selection order based on sandboxMode config:
|
|
1012
|
+
* - 'docker': Docker only (existing behaviour)
|
|
1013
|
+
* - 'native': native OS sandbox only
|
|
1014
|
+
* - 'auto': Docker first, then native sandbox, then unsandboxed
|
|
1015
|
+
* - 'off': no sandboxing
|
|
523
1016
|
*/
|
|
524
1017
|
async function executeCode(language, code, cwd, timeout) {
|
|
525
|
-
|
|
1018
|
+
const mode = config.get('sandboxMode') || 'auto';
|
|
526
1019
|
const sandboxLang = language === 'node' ? 'node' : language;
|
|
527
|
-
//
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1020
|
+
// Determine execution strategy
|
|
1021
|
+
const useDocker = mode === 'docker' || (mode === 'auto' && sandbox.isDockerAvailable());
|
|
1022
|
+
const useNative = mode === 'native' || (mode === 'auto' && !sandbox.isDockerAvailable() && nativeSandbox.isNativeSandboxAvailable());
|
|
1023
|
+
if (useDocker) {
|
|
1024
|
+
// Docker sandbox path (existing behaviour)
|
|
1025
|
+
const result = await sandbox.execute(sandboxLang, code, {
|
|
1026
|
+
timeout,
|
|
1027
|
+
mountWorkdir: true,
|
|
1028
|
+
readOnly: true,
|
|
1029
|
+
}, cwd);
|
|
1030
|
+
const sandboxIndicator = result.sandboxed ? '[sandboxed:docker]' : '[unsandboxed]';
|
|
1031
|
+
const statusIndicator = result.success ? 'ok' : 'err';
|
|
1032
|
+
let output = `${sandboxIndicator} ${statusIndicator} [${language}]\n`;
|
|
1033
|
+
if (result.stdout)
|
|
1034
|
+
output += `Output:\n${result.stdout}\n`;
|
|
1035
|
+
if (result.stderr)
|
|
1036
|
+
output += `Errors:\n${result.stderr}\n`;
|
|
1037
|
+
if (!result.success && !result.stdout && !result.stderr)
|
|
1038
|
+
output += `Exit code: ${result.exitCode}\n`;
|
|
1039
|
+
output += `Duration: ${result.duration}ms`;
|
|
1040
|
+
return output;
|
|
538
1041
|
}
|
|
539
|
-
if (
|
|
540
|
-
|
|
1042
|
+
if (useNative) {
|
|
1043
|
+
// Native OS sandbox path — write code to temp file and execute
|
|
1044
|
+
const fs = await import('fs');
|
|
1045
|
+
const os = await import('os');
|
|
1046
|
+
const pathMod = await import('path');
|
|
1047
|
+
const tempDir = fs.mkdtempSync(pathMod.join(os.tmpdir(), 'calliope-native-'));
|
|
1048
|
+
const ext = language === 'python' ? 'py' : language === 'node' ? 'js' : 'sh';
|
|
1049
|
+
const codePath = pathMod.join(tempDir, `code.${ext}`);
|
|
1050
|
+
fs.writeFileSync(codePath, code);
|
|
1051
|
+
const cmd = language === 'python' ? `python3 "${codePath}"`
|
|
1052
|
+
: language === 'node' ? `node "${codePath}"`
|
|
1053
|
+
: `bash "${codePath}"`;
|
|
1054
|
+
const startTime = Date.now();
|
|
1055
|
+
const result = await nativeSandbox.executeInNativeSandbox(cmd, cwd, {
|
|
1056
|
+
timeout,
|
|
1057
|
+
readOnlyPaths: [tempDir],
|
|
1058
|
+
});
|
|
1059
|
+
// Cleanup temp
|
|
1060
|
+
try {
|
|
1061
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
1062
|
+
}
|
|
1063
|
+
catch { /* ignore */ }
|
|
1064
|
+
const duration = Date.now() - startTime;
|
|
1065
|
+
const sandboxIndicator = result.sandboxed ? `[sandboxed:${result.backend}]` : '[unsandboxed]';
|
|
1066
|
+
const statusIndicator = result.exitCode === 0 ? 'ok' : 'err';
|
|
1067
|
+
let output = `${sandboxIndicator} ${statusIndicator} [${language}]\n`;
|
|
1068
|
+
if (result.stdout)
|
|
1069
|
+
output += `Output:\n${result.stdout}\n`;
|
|
1070
|
+
if (result.stderr)
|
|
1071
|
+
output += `Errors:\n${result.stderr}\n`;
|
|
1072
|
+
if (result.exitCode !== 0 && !result.stdout && !result.stderr)
|
|
1073
|
+
output += `Exit code: ${result.exitCode}\n`;
|
|
1074
|
+
output += `Duration: ${duration}ms`;
|
|
1075
|
+
return output;
|
|
541
1076
|
}
|
|
542
|
-
|
|
1077
|
+
// Unsandboxed fallback (mode === 'off' or nothing available)
|
|
1078
|
+
const result = await sandbox.executeUnsafe(sandboxLang, code, timeout);
|
|
1079
|
+
const statusIndicator = result.success ? 'ok' : 'err';
|
|
1080
|
+
let output = `[unsandboxed] ${statusIndicator} [${language}]\n`;
|
|
1081
|
+
if (result.stdout)
|
|
1082
|
+
output += `Output:\n${result.stdout}\n`;
|
|
1083
|
+
if (result.stderr)
|
|
1084
|
+
output += `Errors:\n${result.stderr}\n`;
|
|
1085
|
+
if (!result.success && !result.stdout && !result.stderr)
|
|
543
1086
|
output += `Exit code: ${result.exitCode}\n`;
|
|
544
|
-
}
|
|
545
1087
|
output += `Duration: ${result.duration}ms`;
|
|
546
1088
|
return output;
|
|
547
1089
|
}
|
|
@@ -610,8 +1152,8 @@ async function executeGit(operation, args, cwd) {
|
|
|
610
1152
|
if (!allowedOps.includes(operation)) {
|
|
611
1153
|
return `Error: Unknown git operation: ${operation}. Allowed: ${allowedOps.join(', ')}`;
|
|
612
1154
|
}
|
|
613
|
-
// Sanitize args to prevent command injection
|
|
614
|
-
const safeArgs = args.replace(/[;&|`$]/g, '');
|
|
1155
|
+
// Sanitize args to prevent command injection via shell metacharacters
|
|
1156
|
+
const safeArgs = args.replace(/[;&|`$(){}!#\n\r]/g, '');
|
|
615
1157
|
let command;
|
|
616
1158
|
switch (operation) {
|
|
617
1159
|
case 'status':
|