@calliopelabs/cli 2.5.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 -661
- 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/bin.d.ts +3 -7
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +56 -79
- 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 +247 -160
- 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 +236 -304
- 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 +11 -7
- package/dist/diff.js.map +1 -1
- package/dist/errors.js +3 -3
- 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 +166 -24
- package/dist/headless.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/mcp.d.ts.map +1 -1
- package/dist/mcp.js +5 -2
- 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.js +2 -2
- package/dist/model-detection.js.map +1 -1
- package/dist/parallel-tools.js.map +1 -1
- package/dist/plugins.d.ts +32 -1
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +116 -5
- 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/bedrock.d.ts.map +1 -1
- package/dist/providers/bedrock.js +2 -5
- 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 +61 -10
- 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.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/router.d.ts +133 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/{model-router.js → router.js} +329 -3
- 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} +1 -1
- package/dist/sandbox/native.d.ts.map +1 -0
- package/dist/{sandbox-native.js → sandbox/native.js} +1 -1
- package/dist/sandbox/native.js.map +1 -0
- package/dist/scope.js +1 -1
- package/dist/scope.js.map +1 -1
- package/dist/scuttlebot/config.js.map +1 -1
- package/dist/scuttlebot/irc-client.js.map +1 -1
- 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 +39 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +90 -8
- package/dist/skills.js.map +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +6 -3
- 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 +128 -179
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +4 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +21 -40
- package/dist/types.js.map +1 -1
- package/dist/ui/agent.d.ts +3 -6
- package/dist/ui/agent.d.ts.map +1 -1
- package/dist/ui/agent.js +328 -38
- 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 +2 -1
- package/dist/ui/chat-input.d.ts.map +1 -1
- package/dist/ui/chat-input.js +31 -350
- package/dist/ui/chat-input.js.map +1 -1
- package/dist/ui/commands.d.ts +14 -46
- package/dist/ui/commands.d.ts.map +1 -1
- package/dist/ui/commands.js +416 -2899
- 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 +45 -1076
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/messages.d.ts +4 -7
- package/dist/ui/messages.d.ts.map +1 -1
- package/dist/ui/messages.js +15 -30
- 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/prompts.d.ts +37 -0
- package/dist/ui/modals/prompts.d.ts.map +1 -0
- package/dist/ui/modals/prompts.js +124 -0
- 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.map +1 -1
- package/dist/ui/status-bar.js +2 -3
- package/dist/ui/status-bar.js.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 +10 -3
- 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 -424
- 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 -45
- package/dist/api-server.d.ts.map +0 -1
- package/dist/api-server.js +0 -326
- 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 -176
- 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 -224
- 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 -330
- 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 -1042
- 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 -268
- 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/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 -85
- package/dist/idle-eviction.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 +0 -72
- package/dist/ui/modals.d.ts.map +0 -1
- package/dist/ui/modals.js +0 -266
- 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/ui/commands.js
CHANGED
|
@@ -1,101 +1,97 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UI Module - Command Handler
|
|
3
3
|
*
|
|
4
|
-
* Handles
|
|
5
|
-
*
|
|
4
|
+
* Handles the slash commands exposed by the TUI. The surface is intentionally
|
|
5
|
+
* small: see COMMAND_NAMES for the canonical list. Legacy/verbose commands have
|
|
6
|
+
* been folded into subcommands of the survivors (e.g. /models -> /model list,
|
|
7
|
+
* /add-dir -> /scope add, /set -> /config set, /breakloop -> /loop stop).
|
|
6
8
|
*/
|
|
7
9
|
import * as fs from 'fs';
|
|
8
10
|
import * as path from 'path';
|
|
9
11
|
import * as config from '../config.js';
|
|
10
12
|
import { selectProvider, getAvailableProviders } from '../providers/index.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { getAvailableModels } from '../model-detection.js';
|
|
13
|
+
import { MODE_CONFIG } from '../types.js';
|
|
14
|
+
import { getSystemPromptForProvider } from '../local-model.js';
|
|
15
|
+
import { getAvailableModels, getModelContextLimit } from '../model-detection.js';
|
|
14
16
|
import * as storage from '../storage.js';
|
|
15
17
|
import * as mcp from '../mcp.js';
|
|
16
18
|
import * as skills from '../skills.js';
|
|
17
|
-
import * as modelRouter from '../model-router.js';
|
|
18
19
|
import * as summarization from '../summarization.js';
|
|
19
20
|
import { addToScope, removeFromScope, getScopeSummary, getScopeDetails, resetScope } from '../scope.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { getCurrentSkin, getCurrentPalette, applySkin, applyPalette, listSkins, listPalettes } from '../hud/api.js';
|
|
23
|
-
import { getCurrentCompanion, applyCompanion, listCompanions, getMoodText } from '../companions.js';
|
|
24
|
-
import { scuttlebotClient } from '../scuttlebot/index.js';
|
|
25
|
-
import { createJob, runJob, cancelJob, getJob, formatJob, formatJobsList, clearFinishedJobs } from '../background-jobs.js';
|
|
26
|
-
import { listRecordings, loadRecording, formatRecording, deleteRecording } from '../terminal-recording.js';
|
|
27
|
-
import { startApiServer, stopApiServer, isApiServerRunning } from '../api-server.js';
|
|
28
|
-
import { getTerminalImageInfo, getImageModeLabel, renderSkinBanner, renderTransition } from '../terminal-image.js';
|
|
29
|
-
import { applyThemePack, listThemePacks, getCurrentPack, getCompanionMode, setCompanionMode, getThemePack } from '../hud/theme-packs/api.js';
|
|
30
|
-
import { getModelContextLimit } from '../model-detection.js';
|
|
21
|
+
import { fleetConfigured, fleetActive, fleetStatus, fleetEnable, fleetDisable, fleetPostMessage } from '../fleet.js';
|
|
22
|
+
import { getTerminalImageInfo, getImageModeLabel } from '../terminal-image.js';
|
|
31
23
|
import { resetContextWarnings } from './context.js';
|
|
32
24
|
import * as memory from '../memory.js';
|
|
25
|
+
import { getBudgetCaps, hasBudgetCaps, loadProjectSpend } from '../budget.js';
|
|
33
26
|
import { resolveIterationLimit, formatIterationLimit, isFiniteIterationLimit, } from '../iteration-limit.js';
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Command registry
|
|
29
|
+
// ============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* Canonical list of every slash-command label handled by the switch in
|
|
32
|
+
* handleCommand(). Tests assert this stays in lockstep with the switch and a
|
|
33
|
+
* superset of the completion roots. `/quit` is the sole alias (of `/exit`);
|
|
34
|
+
* `/fleet` is flag-gated in completions but always handled here.
|
|
35
|
+
*/
|
|
36
|
+
export const COMMAND_NAMES = [
|
|
37
|
+
'/help',
|
|
38
|
+
'/status',
|
|
39
|
+
'/clear',
|
|
40
|
+
'/exit',
|
|
41
|
+
'/quit',
|
|
42
|
+
'/model',
|
|
43
|
+
'/provider',
|
|
44
|
+
'/mode',
|
|
45
|
+
'/undo',
|
|
46
|
+
'/export',
|
|
47
|
+
'/resume',
|
|
48
|
+
'/compact',
|
|
49
|
+
'/scope',
|
|
50
|
+
'/memory',
|
|
51
|
+
'/mcp',
|
|
52
|
+
'/skills',
|
|
53
|
+
'/config',
|
|
54
|
+
'/setup',
|
|
55
|
+
'/trust',
|
|
56
|
+
'/cost',
|
|
57
|
+
'/loop',
|
|
58
|
+
'/restore',
|
|
59
|
+
'/debug',
|
|
60
|
+
'/fleet',
|
|
61
|
+
];
|
|
34
62
|
// Builds the full system prompt including memory context (project + global).
|
|
35
|
-
// dir should be the project directory for the active/resumed session
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
// dir should be the project directory for the active/resumed session; provider
|
|
64
|
+
// selects the compact (local) vs full (cloud) base prompt (feature 5).
|
|
65
|
+
function buildFullSystemPrompt(dir, provider) {
|
|
66
|
+
const base = getSystemPromptForProvider(provider);
|
|
38
67
|
const mem = memory.buildMemoryContext(dir);
|
|
39
68
|
return mem.trim() ? base + '\n\n--- Project Context ---\n' + mem : base;
|
|
40
69
|
}
|
|
41
70
|
function getActiveProjectDir(ctx) {
|
|
42
71
|
return ctx.sessionRef.current?.projectPath ?? process.cwd();
|
|
43
72
|
}
|
|
44
|
-
function formatLedgerDuration(durationMs) {
|
|
45
|
-
if (durationMs < 1000)
|
|
46
|
-
return `${durationMs}ms`;
|
|
47
|
-
if (durationMs < 60_000)
|
|
48
|
-
return `${(durationMs / 1000).toFixed(1)}s`;
|
|
49
|
-
return `${(durationMs / 60_000).toFixed(1)}m`;
|
|
50
|
-
}
|
|
51
73
|
function formatSessionLogLimit(limit) {
|
|
52
74
|
return limit > 0 ? String(limit) : 'unlimited';
|
|
53
75
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
parts.push('unlimited');
|
|
76
|
+
/**
|
|
77
|
+
* Apply a `budget.*` config key. `off`/`none`/empty clears the cap. Throws on an
|
|
78
|
+
* invalid number so the caller's try/catch surfaces the message.
|
|
79
|
+
*/
|
|
80
|
+
function applyBudgetKey(field, value) {
|
|
81
|
+
const budget = { ...(config.get('budget') ?? {}) };
|
|
82
|
+
if (value === 'off' || value === 'none' || value === '') {
|
|
83
|
+
delete budget[field];
|
|
84
|
+
config.set('budget', budget);
|
|
85
|
+
return `✓ ${field} cleared`;
|
|
65
86
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
line += ` (${run.errorSummary})`;
|
|
87
|
+
const num = Number(value);
|
|
88
|
+
if (isNaN(num) || num < 0) {
|
|
89
|
+
throw new Error(`${field} must be a non-negative number (or 'off' to clear)`);
|
|
70
90
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return;
|
|
76
|
-
const runId = ledger.startRun(kind, prompt);
|
|
77
|
-
void (async () => {
|
|
78
|
-
for (;;) {
|
|
79
|
-
const current = getStatus();
|
|
80
|
-
if (!current) {
|
|
81
|
-
ledger.finishRun(runId, 'failed', { errorSummary: 'Run state no longer available' });
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (current.status === 'completed') {
|
|
85
|
-
ledger.finishRun(runId, 'completed');
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (current.status === 'failed') {
|
|
89
|
-
ledger.finishRun(runId, 'failed', { errorSummary: current.error });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (current.status === 'cancelled') {
|
|
93
|
-
ledger.finishRun(runId, 'cancelled', { errorSummary: current.error || 'Cancelled' });
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
97
|
-
}
|
|
98
|
-
})();
|
|
91
|
+
budget[field] = field === 'maxTokensPerRun' ? Math.floor(num) : num;
|
|
92
|
+
config.set('budget', budget);
|
|
93
|
+
const unit = field === 'maxTokensPerRun' ? ' tokens' : ' USD';
|
|
94
|
+
return `✓ ${field} set to ${budget[field]}${unit}`;
|
|
99
95
|
}
|
|
100
96
|
// ============================================================================
|
|
101
97
|
// handleCommand
|
|
@@ -104,97 +100,54 @@ export async function handleCommand(cmd, ctx) {
|
|
|
104
100
|
const parts = cmd.split(/\s+/);
|
|
105
101
|
const command = parts[0].toLowerCase();
|
|
106
102
|
switch (command) {
|
|
107
|
-
case '/help':
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
/
|
|
111
|
-
/
|
|
112
|
-
/
|
|
113
|
-
/mode [plan|hybrid|work] - Switch modes (Shift+Tab to cycle)
|
|
114
|
-
/persona [name] - Switch personality
|
|
115
|
-
/route [on|off] - Auto model routing (/autoroute)
|
|
116
|
-
/smart [on|off|cost|test] - Cross-provider smart routing
|
|
117
|
-
/breaker [status|adjust] - Circuit breaker control (/cb)
|
|
118
|
-
|
|
119
|
-
--- Conversation ---
|
|
120
|
-
/edit - Edit and resend last message
|
|
121
|
-
/undo / /redo - Undo/redo (up to 10 steps)
|
|
122
|
-
/copy - Copy last response to clipboard
|
|
123
|
-
/export [file.md] - Export conversation to markdown
|
|
124
|
-
/branch [new|switch] - Conversation branches
|
|
125
|
-
/clear - Clear conversation (/c)
|
|
103
|
+
case '/help': {
|
|
104
|
+
let help = `Session
|
|
105
|
+
/help Show this help
|
|
106
|
+
/status Provider, model, tokens, terminal, fleet
|
|
107
|
+
/clear Clear the conversation
|
|
108
|
+
/exit Exit Calliope (alias /quit)
|
|
126
109
|
|
|
127
|
-
|
|
128
|
-
/
|
|
129
|
-
/
|
|
130
|
-
/
|
|
131
|
-
/checkpoint [list|clear] - File checkpoints (/cp)
|
|
132
|
-
/restore <path> [index] - Restore file from checkpoint
|
|
133
|
-
/bookmark [name|list|goto] - Bookmarks (/bm)
|
|
134
|
-
/queue [show|clear|flush] - Message queue (/q)
|
|
110
|
+
Model & Mode
|
|
111
|
+
/model [name|list] Switch model, or list/pick available models
|
|
112
|
+
/provider [name|list] Switch provider, or list providers
|
|
113
|
+
/mode [plan|hybrid|work] Switch mode (Shift+Tab to cycle)
|
|
135
114
|
|
|
136
|
-
|
|
137
|
-
/
|
|
138
|
-
/
|
|
139
|
-
/
|
|
140
|
-
/
|
|
141
|
-
/trust [add|remove|list] - Project trust registry
|
|
142
|
-
/find <pattern> - Fuzzy file search
|
|
143
|
-
/search <query> - Search conversation
|
|
115
|
+
Conversation
|
|
116
|
+
/undo Undo the last change (up to 10 steps)
|
|
117
|
+
/export [file.md] Export conversation to markdown
|
|
118
|
+
/resume [sessionId] Resume a saved session (restores full context)
|
|
119
|
+
/compact [status] Compress conversation context; status shows a summary
|
|
144
120
|
|
|
145
|
-
|
|
146
|
-
/
|
|
147
|
-
/
|
|
148
|
-
/
|
|
149
|
-
/
|
|
150
|
-
/approve [notes] - Approve pending plan & execute
|
|
121
|
+
Workspace
|
|
122
|
+
/scope [add|remove <dir>] File-access scope (also details|reset)
|
|
123
|
+
/memory [init|add|show] Project memory (CALLIOPE.md)
|
|
124
|
+
/trust [add|remove|list] Project trust registry
|
|
125
|
+
/restore [<path> [index]] List checkpoints, or restore a file
|
|
151
126
|
|
|
152
|
-
|
|
153
|
-
/
|
|
154
|
-
/
|
|
155
|
-
/companion [name] - Switch companion personality
|
|
156
|
-
/pack [name] - Apply theme pack (skin+palette+companion)
|
|
157
|
-
/theme [name] - Color themes
|
|
158
|
-
/layout [name] - UI layout (classic/split/etc)
|
|
159
|
-
/density [normal|compact] - Display density
|
|
160
|
-
/collapse [tools|all|off] - Tool output visibility
|
|
161
|
-
/intensity [1-5] - Immersion level
|
|
162
|
-
/emoji [on|off] - Toggle emoji
|
|
163
|
-
/banner - Show skin banner art
|
|
127
|
+
Extend
|
|
128
|
+
/mcp [add|remove|tools] MCP servers
|
|
129
|
+
/skills [add|remove] Agent skills
|
|
164
130
|
|
|
165
|
-
|
|
166
|
-
/
|
|
167
|
-
/
|
|
168
|
-
/
|
|
169
|
-
/
|
|
170
|
-
/
|
|
171
|
-
|
|
131
|
+
System
|
|
132
|
+
/config [set <key> <value>] Show or change settings (maxIterations, sessionLogLimit, diffStyle, theme)
|
|
133
|
+
/setup Reconfigure Calliope
|
|
134
|
+
/cost Cost tracking summary
|
|
135
|
+
/loop ["prompt"|stop] Iterative agent loop
|
|
136
|
+
/debug [on|off] Debug state/logging`;
|
|
137
|
+
if (fleetConfigured()) {
|
|
138
|
+
help += `
|
|
172
139
|
|
|
173
|
-
|
|
174
|
-
/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
/cancel-loop - Stop running loop (/stop, /breakloop)
|
|
178
|
-
|
|
179
|
-
--- System ---
|
|
180
|
-
/status - Show status (/s)
|
|
181
|
-
/config - Show config
|
|
182
|
-
/set <key> <value> - Set config variable
|
|
183
|
-
/cost - Cost tracking summary
|
|
184
|
-
/confirm [on|off] - Toggle risky op confirmation
|
|
185
|
-
/debug [on|off] - Debug state/logging
|
|
186
|
-
/unstick - Emergency reset
|
|
187
|
-
/upgrade - Check for updates
|
|
188
|
-
/keys or /? - Keyboard shortcuts
|
|
189
|
-
/exit - Exit (/quit)
|
|
140
|
+
Fleet
|
|
141
|
+
/fleet [message|disable] Multi-agent fleet coordination`;
|
|
142
|
+
}
|
|
143
|
+
help += `
|
|
190
144
|
|
|
191
|
-
File references: @filename, ./path, /absolute/path
|
|
192
|
-
|
|
145
|
+
File references: @filename, ./path, /absolute/path`;
|
|
146
|
+
ctx.addMessage('system', help);
|
|
193
147
|
break;
|
|
148
|
+
}
|
|
194
149
|
case '/provider':
|
|
195
|
-
|
|
196
|
-
case '/p':
|
|
197
|
-
if (parts[1]) {
|
|
150
|
+
if (parts[1] && parts[1] !== 'list') {
|
|
198
151
|
const requested = parts[1].toLowerCase();
|
|
199
152
|
const available = getAvailableProviders();
|
|
200
153
|
if (!available.includes(requested)) {
|
|
@@ -202,7 +155,14 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
|
|
|
202
155
|
break;
|
|
203
156
|
}
|
|
204
157
|
ctx.setProvider(requested);
|
|
205
|
-
|
|
158
|
+
// Explicit switches persist as the new defaults; a stale model from
|
|
159
|
+
// the previous provider must not leak into this one (#233).
|
|
160
|
+
config.set('defaultProvider', requested);
|
|
161
|
+
config.unset('defaultModel');
|
|
162
|
+
ctx.addMessage('system', `Provider: ${selectProvider(requested)} (saved as default)`);
|
|
163
|
+
}
|
|
164
|
+
else if (parts[1] === 'list') {
|
|
165
|
+
ctx.addMessage('system', `Provider: ${ctx.actualProvider} | Available: ${getAvailableProviders().join(', ')}`);
|
|
206
166
|
}
|
|
207
167
|
else if (ctx.openProviderPicker) {
|
|
208
168
|
ctx.openProviderPicker();
|
|
@@ -212,8 +172,7 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
|
|
|
212
172
|
}
|
|
213
173
|
break;
|
|
214
174
|
case '/model':
|
|
215
|
-
|
|
216
|
-
if (parts[1]) {
|
|
175
|
+
if (parts[1] && parts[1] !== 'list') {
|
|
217
176
|
const newModel = parts[1];
|
|
218
177
|
const oldModel = ctx.model || ctx.actualModel;
|
|
219
178
|
// Check context compatibility before switching (#26)
|
|
@@ -222,17 +181,19 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
|
|
|
222
181
|
const currentTokens = ctx.estimateContextTokens();
|
|
223
182
|
const newPct = Math.round((currentTokens / newLimit) * 100);
|
|
224
183
|
ctx.setModel(newModel);
|
|
184
|
+
config.set('defaultModel', newModel);
|
|
225
185
|
ctx.setContextTokens(currentTokens);
|
|
226
186
|
let switchWarning = '';
|
|
227
187
|
if (newPct > 80) {
|
|
228
|
-
switchWarning = `\n⚠️ Context at ${newPct}% of new model limit (${Math.round(currentTokens / 1000)}K/${Math.round(newLimit / 1000)}K). Consider /
|
|
188
|
+
switchWarning = `\n⚠️ Context at ${newPct}% of new model limit (${Math.round(currentTokens / 1000)}K/${Math.round(newLimit / 1000)}K). Consider /compact.`;
|
|
229
189
|
}
|
|
230
190
|
else if (newLimit < oldLimit) {
|
|
231
191
|
switchWarning = `\n📉 Context window: ${Math.round(oldLimit / 1000)}K → ${Math.round(newLimit / 1000)}K (${newPct}% used)`;
|
|
232
192
|
}
|
|
233
|
-
ctx.addMessage('system', `Model: ${oldModel} → ${newModel}${switchWarning}`);
|
|
193
|
+
ctx.addMessage('system', `Model: ${oldModel} → ${newModel} (saved as default)${switchWarning}`);
|
|
234
194
|
}
|
|
235
195
|
else {
|
|
196
|
+
// No arg or `list`: open the model picker (absorbs the old /models listing)
|
|
236
197
|
ctx.addMessage('system', `Fetching models for ${ctx.actualProvider}...`);
|
|
237
198
|
try {
|
|
238
199
|
const models = await getAvailableModels(ctx.actualProvider, { throwOnError: true });
|
|
@@ -249,22 +210,6 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
|
|
|
249
210
|
}
|
|
250
211
|
}
|
|
251
212
|
break;
|
|
252
|
-
case '/models':
|
|
253
|
-
ctx.addMessage('system', `Fetching models for ${ctx.actualProvider}...`);
|
|
254
|
-
try {
|
|
255
|
-
const models = await getAvailableModels(ctx.actualProvider, { throwOnError: true });
|
|
256
|
-
if (models.length > 0) {
|
|
257
|
-
ctx.setAvailableModels(models);
|
|
258
|
-
ctx.setModalMode('model');
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
ctx.addMessage('error', `No models found for ${ctx.actualProvider} — API key may be invalid`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
catch (e) {
|
|
265
|
-
ctx.addMessage('error', `Failed to fetch models for ${ctx.actualProvider}: ${e instanceof Error ? e.message : String(e)}. Check your API key.`);
|
|
266
|
-
}
|
|
267
|
-
break;
|
|
268
213
|
case '/mode':
|
|
269
214
|
if (parts[1] && ['plan', 'hybrid', 'work'].includes(parts[1])) {
|
|
270
215
|
const m = parts[1];
|
|
@@ -276,64 +221,13 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
|
|
|
276
221
|
ctx.addMessage('system', `Mode: ${currentConfig.icon} ${currentConfig.label}\nOptions: plan (\u{1F4CB}), hybrid (\u{1F504}), work (\u{1F527})\nUse Shift+Tab to cycle`);
|
|
277
222
|
}
|
|
278
223
|
break;
|
|
279
|
-
case '/persona':
|
|
280
|
-
if (parts[1] && ['calliope', 'muse', 'minimal'].includes(parts[1])) {
|
|
281
|
-
const p = parts[1];
|
|
282
|
-
ctx.setPersona(p);
|
|
283
|
-
ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(p, getActiveProjectDir(ctx)) }];
|
|
284
|
-
ctx.addMessage('system', `Persona: ${p}`);
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
ctx.addMessage('system', `Persona: ${ctx.persona} | Options: calliope, muse, minimal`);
|
|
288
|
-
}
|
|
289
|
-
break;
|
|
290
224
|
case '/clear':
|
|
291
|
-
case '/c':
|
|
292
225
|
ctx.setMessages([]);
|
|
293
|
-
ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx
|
|
226
|
+
ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(getActiveProjectDir(ctx), ctx.actualProvider) }];
|
|
294
227
|
ctx.ledger?.reset();
|
|
295
228
|
ctx.setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
|
|
296
229
|
resetContextWarnings(); // Reset context warning state
|
|
297
230
|
break;
|
|
298
|
-
case '/copy': {
|
|
299
|
-
// Copy last assistant response to clipboard
|
|
300
|
-
const lastAssistant = [...ctx.messages].reverse().find(m => m.type === 'assistant');
|
|
301
|
-
if (lastAssistant) {
|
|
302
|
-
try {
|
|
303
|
-
const { execSync } = await import('child_process');
|
|
304
|
-
// Try different clipboard commands based on platform
|
|
305
|
-
const content = lastAssistant.content;
|
|
306
|
-
if (process.platform === 'darwin') {
|
|
307
|
-
execSync('pbcopy', { input: content });
|
|
308
|
-
}
|
|
309
|
-
else if (process.platform === 'win32') {
|
|
310
|
-
execSync('clip', { input: content });
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
// Linux - try xclip, xsel, or wl-copy
|
|
314
|
-
try {
|
|
315
|
-
execSync('xclip -selection clipboard', { input: content });
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
try {
|
|
319
|
-
execSync('xsel --clipboard --input', { input: content });
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
execSync('wl-copy', { input: content });
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
ctx.addMessage('system', '\u2713 Copied to clipboard');
|
|
327
|
-
}
|
|
328
|
-
catch (e) {
|
|
329
|
-
ctx.addMessage('error', `Clipboard not available: ${e instanceof Error ? e.message : String(e)}`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
ctx.addMessage('system', 'No assistant message to copy');
|
|
334
|
-
}
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
231
|
case '/export': {
|
|
338
232
|
// Export conversation to markdown
|
|
339
233
|
const filename = parts[1] || `calliope-export-${Date.now()}.md`;
|
|
@@ -362,20 +256,7 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
|
|
|
362
256
|
}
|
|
363
257
|
const filepath = path.resolve(process.cwd(), filename);
|
|
364
258
|
fsModule.writeFileSync(filepath, markdown);
|
|
365
|
-
ctx.addMessage('system',
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
case '/edit': {
|
|
369
|
-
// Edit last user message
|
|
370
|
-
const lastUserIdx = [...ctx.messages].reverse().findIndex(m => m.type === 'user');
|
|
371
|
-
if (lastUserIdx >= 0) {
|
|
372
|
-
const lastUser = ctx.messages[ctx.messages.length - 1 - lastUserIdx];
|
|
373
|
-
ctx.setInput(lastUser.content);
|
|
374
|
-
ctx.addMessage('system', 'Edit the message above and press Enter to resend');
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
ctx.addMessage('system', 'No user message to edit');
|
|
378
|
-
}
|
|
259
|
+
ctx.addMessage('system', `✓ Exported to ${filename}`);
|
|
379
260
|
break;
|
|
380
261
|
}
|
|
381
262
|
case '/undo': {
|
|
@@ -394,735 +275,251 @@ Modes: Plan | Hybrid | Work | Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}${ctx.a
|
|
|
394
275
|
ctx.setMessages(prevState.messages);
|
|
395
276
|
ctx.llmMessages.current = prevState.llmMessages;
|
|
396
277
|
ctx.setContextTokens(ctx.estimateContextTokens());
|
|
397
|
-
ctx.addMessage('system',
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
400
|
-
case '/redo': {
|
|
401
|
-
if (ctx.redoStack.current.length === 0) {
|
|
402
|
-
ctx.addMessage('system', 'Nothing to redo.');
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
405
|
-
// Save current state to undo stack
|
|
406
|
-
ctx.undoStack.current.push({
|
|
407
|
-
messages: [...ctx.messages],
|
|
408
|
-
llmMessages: [...ctx.llmMessages.current],
|
|
409
|
-
timestamp: new Date(),
|
|
410
|
-
});
|
|
411
|
-
// Restore redo state
|
|
412
|
-
const redoState = ctx.redoStack.current.pop();
|
|
413
|
-
ctx.setMessages(redoState.messages);
|
|
414
|
-
ctx.llmMessages.current = redoState.llmMessages;
|
|
415
|
-
ctx.setContextTokens(ctx.estimateContextTokens());
|
|
416
|
-
ctx.addMessage('system', `\u2713 Redone (${ctx.redoStack.current.length} more available)`);
|
|
278
|
+
ctx.addMessage('system', `✓ Undone (${ctx.undoStack.current.length} more available)`);
|
|
417
279
|
break;
|
|
418
280
|
}
|
|
419
|
-
case '/status':
|
|
420
|
-
case '/s': {
|
|
281
|
+
case '/status': {
|
|
421
282
|
const imgInfo = getTerminalImageInfo();
|
|
422
283
|
let statusMsg = `${ctx.actualProvider}:${ctx.actualModel} | ${ctx.stats.messageCount} msgs | ${ctx.stats.inputTokens + ctx.stats.outputTokens} tokens | terminal: ${getImageModeLabel(imgInfo.mode)}${imgInfo.truecolor ? ' (truecolor)' : ''} ${imgInfo.width}cols`;
|
|
423
|
-
// Add
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
statusMsg += `\
|
|
284
|
+
// Add fleet status if active
|
|
285
|
+
const fleetSt = fleetStatus();
|
|
286
|
+
if (fleetSt) {
|
|
287
|
+
statusMsg += `\nFleet: active (${fleetSt.nick}) | irc:${fleetSt.config?.ircAddr} | #${fleetSt.config?.channel}`;
|
|
288
|
+
}
|
|
289
|
+
// Show budget state when any cap is configured.
|
|
290
|
+
const caps = getBudgetCaps();
|
|
291
|
+
if (hasBudgetCaps(caps)) {
|
|
292
|
+
const parts = [];
|
|
293
|
+
if (typeof caps.maxCostPerRun === 'number')
|
|
294
|
+
parts.push(`run<=$${caps.maxCostPerRun}`);
|
|
295
|
+
if (typeof caps.maxTokensPerRun === 'number')
|
|
296
|
+
parts.push(`run<=${caps.maxTokensPerRun}tok`);
|
|
297
|
+
if (typeof caps.maxCostPerProject === 'number') {
|
|
298
|
+
const spent = loadProjectSpend(getActiveProjectDir(ctx)).spentUsd;
|
|
299
|
+
parts.push(`project $${spent.toFixed(4)}/$${caps.maxCostPerProject}`);
|
|
300
|
+
}
|
|
301
|
+
statusMsg += `\nBudget: ${parts.join(' | ')}`;
|
|
427
302
|
}
|
|
428
303
|
ctx.addMessage('system', statusMsg);
|
|
429
304
|
break;
|
|
430
305
|
}
|
|
431
|
-
case '/
|
|
306
|
+
case '/fleet': {
|
|
432
307
|
const subCmd = parts[1];
|
|
433
|
-
const sbStatus = scuttlebotClient.getStatus();
|
|
434
|
-
// /scuttlebot enable - enable mid-session
|
|
435
308
|
if (subCmd === 'enable') {
|
|
436
|
-
if (
|
|
437
|
-
ctx.addMessage('system', '
|
|
309
|
+
if (fleetActive()) {
|
|
310
|
+
ctx.addMessage('system', 'Fleet mode is already active.');
|
|
438
311
|
break;
|
|
439
312
|
}
|
|
440
|
-
// Initialize scuttlebot — config is loaded from ~/.config/scuttlebot-relay.env,
|
|
441
|
-
// process.env, and .scuttlebot.yaml inside initialize()
|
|
442
313
|
const sessionId = ctx.sessionRef.current?.id || 'default';
|
|
443
314
|
const cwd = getActiveProjectDir(ctx);
|
|
444
|
-
|
|
315
|
+
fleetEnable(sessionId, cwd).then((enabled) => {
|
|
445
316
|
if (enabled) {
|
|
446
|
-
const status =
|
|
447
|
-
let msg = '✓
|
|
448
|
-
msg += ` Nick: ${status
|
|
449
|
-
msg += ` IRC: ${status
|
|
450
|
-
msg += ` Channel: #${status
|
|
451
|
-
if (status.config?.channels && status.config.channels.length > 1) {
|
|
452
|
-
msg += `\n Channels: ${status.config.channels.map((c) => '#' + c).join(', ')}`;
|
|
453
|
-
}
|
|
317
|
+
const status = fleetStatus();
|
|
318
|
+
let msg = '✓ Fleet mode enabled\n';
|
|
319
|
+
msg += ` Nick: ${status?.nick}\n`;
|
|
320
|
+
msg += ` IRC: ${status?.config?.ircAddr}\n`;
|
|
321
|
+
msg += ` Channel: #${status?.config?.channel}`;
|
|
454
322
|
ctx.addMessage('system', msg);
|
|
455
|
-
|
|
456
|
-
await scuttlebotClient.postOnline();
|
|
457
|
-
ctx.startScuttlebotPolling();
|
|
323
|
+
ctx.startFleetPolling();
|
|
458
324
|
}
|
|
459
325
|
else {
|
|
460
|
-
ctx.addMessage('system', 'Failed to enable scuttlebot');
|
|
326
|
+
ctx.addMessage('system', 'Failed to enable fleet mode — check relay config (~/.config/scuttlebot-relay.env, .scuttlebot.yaml).');
|
|
461
327
|
}
|
|
462
328
|
}).catch((err) => {
|
|
463
|
-
ctx.addMessage('system', `Failed to enable
|
|
329
|
+
ctx.addMessage('system', `Failed to enable fleet mode: ${err instanceof Error ? err.message : String(err)}`);
|
|
464
330
|
});
|
|
465
331
|
break;
|
|
466
332
|
}
|
|
467
|
-
// /scuttlebot disable - disable mid-session
|
|
468
333
|
if (subCmd === 'disable') {
|
|
469
|
-
if (!
|
|
470
|
-
ctx.addMessage('system', '
|
|
334
|
+
if (!fleetConfigured() && !fleetActive()) {
|
|
335
|
+
ctx.addMessage('system', 'Fleet mode is not enabled.');
|
|
471
336
|
break;
|
|
472
337
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}).then(() => {
|
|
476
|
-
ctx.addMessage('system', 'Scuttlebot disabled');
|
|
338
|
+
fleetDisable().then(() => {
|
|
339
|
+
ctx.addMessage('system', 'Fleet mode disabled.');
|
|
477
340
|
}).catch((err) => {
|
|
478
|
-
ctx.addMessage('system', `Error disabling
|
|
341
|
+
ctx.addMessage('system', `Error disabling fleet mode: ${err instanceof Error ? err.message : String(err)}`);
|
|
479
342
|
});
|
|
480
343
|
break;
|
|
481
344
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
ctx.addMessage('system', 'Scuttlebot not enabled.\n\nRun: /scuttlebot enable\n\nConfig is loaded automatically from ~/.config/scuttlebot-relay.env\nChannel is read from .scuttlebot.yaml');
|
|
345
|
+
if (!fleetActive()) {
|
|
346
|
+
ctx.addMessage('system', 'Fleet mode is off.\n\nRun: /fleet enable\n\nRelay config is read from ~/.config/scuttlebot-relay.env and .scuttlebot.yaml.\nDocs: docs/fleet.md');
|
|
485
347
|
break;
|
|
486
348
|
}
|
|
487
|
-
|
|
488
|
-
statusText += `Enabled: yes\n`;
|
|
489
|
-
statusText += `Nick: ${sbStatus.nick}\n`;
|
|
490
|
-
statusText += `IRC: ${sbStatus.config?.ircAddr}\n`;
|
|
491
|
-
statusText += `Channel: #${sbStatus.config?.channel}\n`;
|
|
492
|
-
statusText += `Connected: ${sbStatus.connected ? 'yes' : 'no'}`;
|
|
493
|
-
if (sbStatus.config?.channels && sbStatus.config.channels.length > 1) {
|
|
494
|
-
statusText += `\nChannels: ${sbStatus.config.channels.map((c) => '#' + c).join(', ')}`;
|
|
495
|
-
}
|
|
496
|
-
statusText += '\n\nCommands:\n /scuttlebot <message> Post a message\n /scuttlebot disable Disable integration';
|
|
497
|
-
ctx.addMessage('system', statusText);
|
|
498
|
-
// Allow manual message posting
|
|
499
|
-
if (subCmd && subCmd !== 'enable' && subCmd !== 'disable') {
|
|
349
|
+
if (subCmd) {
|
|
500
350
|
const message = parts.slice(1).join(' ');
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
ctx.addMessage('system', `Failed to post message: ${err instanceof Error ? err.message : String(err)}`);
|
|
505
|
-
});
|
|
351
|
+
fleetPostMessage(message);
|
|
352
|
+
ctx.addMessage('system', 'Message posted to the fleet channel.');
|
|
353
|
+
break;
|
|
506
354
|
}
|
|
355
|
+
const status = fleetStatus();
|
|
356
|
+
let statusText = 'Fleet Status\n────────────────────\n';
|
|
357
|
+
statusText += `Nick: ${status?.nick}\n`;
|
|
358
|
+
statusText += `IRC: ${status?.config?.ircAddr}\n`;
|
|
359
|
+
statusText += `Channel: #${status?.config?.channel}\n`;
|
|
360
|
+
statusText += `Connected: ${status?.connected ? 'yes' : 'no'}`;
|
|
361
|
+
statusText += '\n\nCommands:\n /fleet <message> Post to the fleet channel\n /fleet disable Disable fleet mode';
|
|
362
|
+
ctx.addMessage('system', statusText);
|
|
507
363
|
break;
|
|
508
364
|
}
|
|
509
|
-
case '/config':
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
else if (sub === 'teams') {
|
|
535
|
-
const teams = listTeamDefs(process.cwd());
|
|
536
|
-
if (teams.length === 0) {
|
|
537
|
-
ctx.addMessage('system', 'No team definitions loaded. Run /agents init to create examples.');
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
const lines = teams.map(t => {
|
|
541
|
-
const src = t._source || 'unknown';
|
|
542
|
-
return ` ${t.name} — ${t.mode}, ${t.members.length} members [${src}]${t.description ? `\n ${t.description}` : ''}`;
|
|
543
|
-
});
|
|
544
|
-
ctx.addMessage('system', `Team Definitions (${teams.length}):\n${lines.join('\n')}`);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
else if (sub === 'init') {
|
|
548
|
-
const result = scaffoldAgentsDir(process.cwd());
|
|
549
|
-
if (result.created.length > 0) {
|
|
550
|
-
ctx.addMessage('system', `Created .calliope/agents/ with examples:\n${result.created.map(f => ` ${f}`).join('\n')}`);
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
ctx.addMessage('system', '.calliope/agents/ already exists.');
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
else if (sub === 'show') {
|
|
557
|
-
const name = parts[2];
|
|
558
|
-
if (!name) {
|
|
559
|
-
ctx.addMessage('system', 'Usage: /agents show <name>');
|
|
365
|
+
case '/config': {
|
|
366
|
+
if (parts[1] === 'set') {
|
|
367
|
+
// /config set <key> <value> (absorbs the old /set)
|
|
368
|
+
const key = parts[2];
|
|
369
|
+
const value = parts.slice(3).join(' ');
|
|
370
|
+
if (!key || !value) {
|
|
371
|
+
ctx.addMessage('system', `Usage: /config set <key> <value>
|
|
372
|
+
Available keys:
|
|
373
|
+
maxIterations <number> - Max agent iterations (current: ${config.get('maxIterations')})
|
|
374
|
+
sessionLogLimit <number> - Cap retained session log items (current: ${formatSessionLogLimit(config.get('sessionLogLimit'))}, 0 = unlimited)
|
|
375
|
+
collapseTools <bool> - Auto-collapse tool output
|
|
376
|
+
toolDisplayLimit <number> - Tools shown expanded (0 = all)
|
|
377
|
+
diffStyle <inline|unified|side-by-side> - Diff display style
|
|
378
|
+
sandboxMode <auto|native|docker|off> - Code execution sandbox
|
|
379
|
+
routing.enabled <bool> - Smart model routing
|
|
380
|
+
routing.costSensitivity <0-1> - Cost vs quality (0 = best, 1 = cheapest)
|
|
381
|
+
theme <dark|light|no-color> - Color theme
|
|
382
|
+
budget.maxCostPerRun <usd|off> - Halt a run at this spend (0/off = no cap)
|
|
383
|
+
budget.maxTokensPerRun <n|off> - Halt a run at this token count
|
|
384
|
+
budget.maxCostPerProject <usd|off> - Halt when project spend reaches this
|
|
385
|
+
audit.enabled <bool> - Audit run log (on by default)
|
|
386
|
+
policy.command <path|off> - Pre-tool policy hook script`);
|
|
387
|
+
break;
|
|
560
388
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
`Model: ${agentDef.model || 'default'}`,
|
|
571
|
-
agentDef.role ? `Role: ${agentDef.role}` : '',
|
|
572
|
-
agentDef.weight !== undefined ? `Weight: ${agentDef.weight}` : '',
|
|
573
|
-
agentDef.instructions ? `Instructions:\n ${agentDef.instructions.split('\n').join('\n ')}` : '',
|
|
574
|
-
`Source: ${agentDef._source || 'unknown'}`,
|
|
575
|
-
agentDef._filePath ? `File: ${agentDef._filePath}` : '',
|
|
576
|
-
].filter(Boolean);
|
|
577
|
-
ctx.addMessage('system', lines.join('\n'));
|
|
389
|
+
try {
|
|
390
|
+
if (key === 'maxIterations') {
|
|
391
|
+
const num = parseInt(value, 10);
|
|
392
|
+
if (isNaN(num) || num < 0 || num > 1000000) {
|
|
393
|
+
ctx.addMessage('error', 'maxIterations must be 0-1000000 (0 = unlimited)');
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
config.set('maxIterations', num);
|
|
397
|
+
ctx.addMessage('system', `✓ maxIterations set to ${formatIterationLimit(resolveIterationLimit(num))}`);
|
|
578
398
|
}
|
|
579
|
-
else if (
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
ctx.addMessage('system',
|
|
399
|
+
else if (key === 'sessionLogLimit') {
|
|
400
|
+
const num = parseInt(value, 10);
|
|
401
|
+
if (isNaN(num) || num < 0 || num > 100000) {
|
|
402
|
+
ctx.addMessage('error', 'sessionLogLimit must be 0-100000 (0 = unlimited)');
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
config.set('sessionLogLimit', num);
|
|
406
|
+
ctx.ledger?.setRetentionLimit(num);
|
|
407
|
+
ctx.addMessage('system', `✓ sessionLogLimit set to ${num === 0 ? 'unlimited (set > 0 to cap)' : num}`);
|
|
588
408
|
}
|
|
589
|
-
else {
|
|
590
|
-
|
|
409
|
+
else if (key === 'collapseTools') {
|
|
410
|
+
const bool = value === 'true';
|
|
411
|
+
config.set('collapseTools', bool);
|
|
412
|
+
ctx.addMessage('system', `✓ collapseTools set to ${bool}`);
|
|
591
413
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
// /install-agents [name] — install missing agent CLIs and SDK backends
|
|
601
|
-
const target = parts[1];
|
|
602
|
-
if (target) {
|
|
603
|
-
ctx.addMessage('system', `Installing ${target}...`);
|
|
604
|
-
const result = installItem(target);
|
|
605
|
-
ctx.addMessage('system', result.success
|
|
606
|
-
? `✓ ${target} installed successfully.`
|
|
607
|
-
: `✗ ${result.output}`);
|
|
608
|
-
}
|
|
609
|
-
else if (parts.includes('--all')) {
|
|
610
|
-
ctx.addMessage('system', 'Installing all missing agents and SDK backends...');
|
|
611
|
-
const result = await installAllMissing();
|
|
612
|
-
const lines = [];
|
|
613
|
-
if (result.installed.length > 0) {
|
|
614
|
-
lines.push(`✓ Installed: ${result.installed.join(', ')}`);
|
|
615
|
-
}
|
|
616
|
-
if (result.failed.length > 0) {
|
|
617
|
-
lines.push(`✗ Failed:\n ${result.failed.join('\n ')}`);
|
|
618
|
-
}
|
|
619
|
-
if (result.installed.length === 0 && result.failed.length === 0) {
|
|
620
|
-
lines.push('Everything is already installed!');
|
|
621
|
-
}
|
|
622
|
-
ctx.addMessage('system', lines.join('\n'));
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
const report = await getInstallReport();
|
|
626
|
-
ctx.addMessage('system', `${report}\n\nUsage:\n /install-agents <name> Install a specific agent or SDK\n /install-agents --all Install everything missing`);
|
|
627
|
-
}
|
|
628
|
-
break;
|
|
629
|
-
}
|
|
630
|
-
case '/build-agent': {
|
|
631
|
-
// Interactive agent definition builder
|
|
632
|
-
// Parse inline args: /build-agent [name] [--engine X] [--provider X] [--model X]
|
|
633
|
-
const agentName = parts[1] && !parts[1].startsWith('--') ? parts[1] : undefined;
|
|
634
|
-
const engineArg = parts.includes('--engine') ? parts[parts.indexOf('--engine') + 1] : undefined;
|
|
635
|
-
const providerArg = parts.includes('--provider') ? parts[parts.indexOf('--provider') + 1] : undefined;
|
|
636
|
-
const modelArg = parts.includes('--model') ? parts[parts.indexOf('--model') + 1] : undefined;
|
|
637
|
-
const roleArg = parts.includes('--role') ? parts[parts.indexOf('--role') + 1] : undefined;
|
|
638
|
-
if (!agentName) {
|
|
639
|
-
// Interactive wizard — show guided step-by-step builder
|
|
640
|
-
const executors = await getAvailableExecutors();
|
|
641
|
-
const providers = config.getConfiguredProviders();
|
|
642
|
-
// Engine descriptions
|
|
643
|
-
const engineDescriptions = {
|
|
644
|
-
'cli': 'Built-in CLI agent loop (works with all providers, most flexible)',
|
|
645
|
-
'claude-sdk': 'Anthropic Claude SDK (native tool use, streaming, best for Anthropic models)',
|
|
646
|
-
'openai-sdk': 'OpenAI SDK (function calling, JSON mode, best for OpenAI models)',
|
|
647
|
-
'google-adk': 'Google ADK (grounding, search, best for Gemini models)',
|
|
648
|
-
};
|
|
649
|
-
// Provider-to-engine recommendations
|
|
650
|
-
const providerEngineMap = {
|
|
651
|
-
anthropic: 'claude-sdk',
|
|
652
|
-
openai: 'openai-sdk',
|
|
653
|
-
google: 'google-adk',
|
|
654
|
-
together: 'cli',
|
|
655
|
-
openrouter: 'cli',
|
|
656
|
-
groq: 'cli',
|
|
657
|
-
fireworks: 'cli',
|
|
658
|
-
mistral: 'cli',
|
|
659
|
-
ollama: 'cli',
|
|
660
|
-
bedrock: 'cli',
|
|
661
|
-
};
|
|
662
|
-
// Common roles
|
|
663
|
-
const roles = [
|
|
664
|
-
{ name: 'coder', desc: 'Writes and refactors code' },
|
|
665
|
-
{ name: 'reviewer', desc: 'Reviews code for bugs, style, and security' },
|
|
666
|
-
{ name: 'architect', desc: 'Designs system architecture and APIs' },
|
|
667
|
-
{ name: 'researcher', desc: 'Gathers information and summarizes findings' },
|
|
668
|
-
{ name: 'qa-engineer', desc: 'Writes tests and validates correctness' },
|
|
669
|
-
{ name: 'devops', desc: 'Infrastructure, CI/CD, and deployment' },
|
|
670
|
-
{ name: 'writer', desc: 'Documentation, READMEs, and technical writing' },
|
|
671
|
-
{ name: 'analyst', desc: 'Data analysis and insights' },
|
|
672
|
-
];
|
|
673
|
-
const lines = [];
|
|
674
|
-
lines.push('=== Build Agent Wizard ===');
|
|
675
|
-
lines.push('');
|
|
676
|
-
// Step 1: Engine
|
|
677
|
-
lines.push('STEP 1: Choose an engine');
|
|
678
|
-
lines.push('The engine determines how the agent executes (SDK backend or CLI loop).');
|
|
679
|
-
lines.push('');
|
|
680
|
-
for (const eng of ['cli', 'claude-sdk', 'openai-sdk', 'google-adk']) {
|
|
681
|
-
const available = executors.includes(eng);
|
|
682
|
-
const marker = available ? '[installed]' : '[not installed]';
|
|
683
|
-
lines.push(` ${eng} ${marker}`);
|
|
684
|
-
lines.push(` ${engineDescriptions[eng] || ''}`);
|
|
685
|
-
}
|
|
686
|
-
lines.push('');
|
|
687
|
-
if (executors.length === 1) {
|
|
688
|
-
lines.push(' Recommendation: Only "cli" is available. Install SDK packages for more options.');
|
|
689
|
-
lines.push(' Run /install-agents to see what can be installed.');
|
|
690
|
-
}
|
|
691
|
-
else {
|
|
692
|
-
lines.push(` Available on this system: ${executors.join(', ')}`);
|
|
693
|
-
}
|
|
694
|
-
lines.push('');
|
|
695
|
-
// Step 2: Provider
|
|
696
|
-
lines.push('STEP 2: Choose a provider');
|
|
697
|
-
lines.push('The provider determines which API your agent calls.');
|
|
698
|
-
lines.push('');
|
|
699
|
-
if (providers.length === 0) {
|
|
700
|
-
lines.push(' No providers configured! Run calliope --setup to add API keys.');
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
703
|
-
for (const p of providers) {
|
|
704
|
-
const recEngine = providerEngineMap[p] || 'cli';
|
|
705
|
-
const model = DEFAULT_MODELS[p] || 'auto';
|
|
706
|
-
lines.push(` ${p} (best with --engine ${recEngine}, default model: ${model})`);
|
|
414
|
+
else if (key === 'toolDisplayLimit') {
|
|
415
|
+
const num = parseInt(value, 10);
|
|
416
|
+
if (isNaN(num) || num < 0 || num > 100) {
|
|
417
|
+
ctx.addMessage('error', 'toolDisplayLimit must be 0-100 (0 = all expanded)');
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
config.set('toolDisplayLimit', num);
|
|
421
|
+
ctx.addMessage('system', `✓ toolDisplayLimit set to ${num}`);
|
|
707
422
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
for (const p of providers) {
|
|
716
|
-
lines.push(` ${p}: ${DEFAULT_MODELS[p] || 'auto'}`);
|
|
423
|
+
else if (key === 'diffStyle') {
|
|
424
|
+
if (value !== 'inline' && value !== 'unified' && value !== 'side-by-side') {
|
|
425
|
+
ctx.addMessage('error', 'diffStyle must be inline, unified, or side-by-side');
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
config.set('diffStyle', value);
|
|
429
|
+
ctx.addMessage('system', `✓ diffStyle set to ${value}`);
|
|
717
430
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
lines.push('');
|
|
726
|
-
for (const r of roles) {
|
|
727
|
-
lines.push(` ${r.name.padEnd(14)} ${r.desc}`);
|
|
728
|
-
}
|
|
729
|
-
lines.push('');
|
|
730
|
-
lines.push(' You can also use any custom role name.');
|
|
731
|
-
lines.push('');
|
|
732
|
-
// Step 5: Instructions
|
|
733
|
-
lines.push('STEP 5: Write instructions (after creation)');
|
|
734
|
-
lines.push('After creating the agent, edit its YAML file to add detailed instructions.');
|
|
735
|
-
lines.push('');
|
|
736
|
-
lines.push(' Template:');
|
|
737
|
-
lines.push(' instructions: |');
|
|
738
|
-
lines.push(' You are a specialized {role} agent.');
|
|
739
|
-
lines.push(' Your focus areas: ...');
|
|
740
|
-
lines.push(' When reviewing code: ...');
|
|
741
|
-
lines.push(' Output format: ...');
|
|
742
|
-
lines.push('');
|
|
743
|
-
// Final command
|
|
744
|
-
lines.push('--- Ready to build? ---');
|
|
745
|
-
lines.push('');
|
|
746
|
-
if (providers.includes('anthropic') && executors.includes('claude-sdk')) {
|
|
747
|
-
lines.push(' /build-agent my-agent --engine claude-sdk --provider anthropic --role coder');
|
|
748
|
-
}
|
|
749
|
-
else if (providers.includes('openai') && executors.includes('openai-sdk')) {
|
|
750
|
-
lines.push(' /build-agent my-agent --engine openai-sdk --provider openai --role coder');
|
|
751
|
-
}
|
|
752
|
-
else if (providers.length > 0) {
|
|
753
|
-
const firstProvider = providers[0];
|
|
754
|
-
lines.push(` /build-agent my-agent --engine cli --provider ${firstProvider} --role coder`);
|
|
755
|
-
}
|
|
756
|
-
else {
|
|
757
|
-
lines.push(' /build-agent my-agent --engine cli --provider anthropic --role coder');
|
|
758
|
-
}
|
|
759
|
-
lines.push('');
|
|
760
|
-
lines.push('Add --model <model> to override the default model for the chosen provider.');
|
|
761
|
-
ctx.addMessage('system', lines.join('\n'));
|
|
762
|
-
}
|
|
763
|
-
else if (!engineArg && !providerArg && !modelArg && !roleArg) {
|
|
764
|
-
// Name provided but no flags — show a concise personalized wizard
|
|
765
|
-
const executors = await getAvailableExecutors();
|
|
766
|
-
const providers = config.getConfiguredProviders();
|
|
767
|
-
const providerEngineMap = {
|
|
768
|
-
anthropic: 'claude-sdk', openai: 'openai-sdk', google: 'google-adk',
|
|
769
|
-
together: 'cli', openrouter: 'cli', groq: 'cli', fireworks: 'cli',
|
|
770
|
-
mistral: 'cli', ollama: 'cli', bedrock: 'cli',
|
|
771
|
-
};
|
|
772
|
-
const lines = [];
|
|
773
|
-
lines.push(`=== Building agent: ${agentName} ===`);
|
|
774
|
-
lines.push('');
|
|
775
|
-
lines.push('Choose your configuration:');
|
|
776
|
-
lines.push('');
|
|
777
|
-
// Show ready-to-run commands for each configured provider
|
|
778
|
-
let optionNum = 0;
|
|
779
|
-
for (const p of providers) {
|
|
780
|
-
const recEngine = providerEngineMap[p] || 'cli';
|
|
781
|
-
const engineAvailable = executors.includes(recEngine);
|
|
782
|
-
const engine = engineAvailable ? recEngine : 'cli';
|
|
783
|
-
const model = DEFAULT_MODELS[p] || 'auto';
|
|
784
|
-
optionNum++;
|
|
785
|
-
lines.push(` Option ${optionNum}: ${p}`);
|
|
786
|
-
lines.push(` /build-agent ${agentName} --engine ${engine} --provider ${p} --model ${model} --role coder`);
|
|
787
|
-
lines.push('');
|
|
788
|
-
}
|
|
789
|
-
if (optionNum === 0) {
|
|
790
|
-
lines.push(' No providers configured. Run calliope --setup to add API keys.');
|
|
791
|
-
lines.push(` Or build with defaults: /build-agent ${agentName} --engine cli --provider anthropic --role coder`);
|
|
792
|
-
}
|
|
793
|
-
lines.push('Common roles: coder, reviewer, architect, researcher, qa-engineer, devops, writer');
|
|
794
|
-
lines.push('');
|
|
795
|
-
lines.push('Pick an option above and change --role to match your needs.');
|
|
796
|
-
lines.push(`After creation, customize instructions in .calliope/agents/${agentName}.yaml`);
|
|
797
|
-
ctx.addMessage('system', lines.join('\n'));
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
// Build and save agent definition (flags provided)
|
|
801
|
-
const newDef = {
|
|
802
|
-
name: agentName,
|
|
803
|
-
engine: engineArg || 'cli',
|
|
804
|
-
provider: providerArg,
|
|
805
|
-
model: modelArg,
|
|
806
|
-
role: roleArg,
|
|
807
|
-
weight: 1.0,
|
|
808
|
-
instructions: `You are a specialized agent (${roleArg || agentName}). Complete assigned tasks thoroughly.`,
|
|
809
|
-
limits: { timeout: 600000 },
|
|
810
|
-
};
|
|
811
|
-
const filePath = saveAgentDef(process.cwd(), newDef);
|
|
812
|
-
ctx.addMessage('system', `Agent '${agentName}' created: ${filePath}\n\nEdit the file to customize instructions and settings.\nUse with: spawn_agent agentDef="${agentName}"`);
|
|
813
|
-
}
|
|
814
|
-
break;
|
|
815
|
-
}
|
|
816
|
-
case '/build-team': {
|
|
817
|
-
// Interactive team builder
|
|
818
|
-
const teamNameArg = parts[1] && !parts[1].startsWith('--') ? parts[1] : undefined;
|
|
819
|
-
const modeArg = parts.includes('--mode') ? parts[parts.indexOf('--mode') + 1] : undefined;
|
|
820
|
-
if (!teamNameArg) {
|
|
821
|
-
// Interactive wizard — guided team builder
|
|
822
|
-
const teams = listTeamDefs(process.cwd());
|
|
823
|
-
const agents = listAgentDefs(process.cwd());
|
|
824
|
-
const lines = [];
|
|
825
|
-
lines.push('=== Build Team Wizard ===');
|
|
826
|
-
lines.push('');
|
|
827
|
-
// Step 1: Available agents
|
|
828
|
-
lines.push('STEP 1: Review your available agents');
|
|
829
|
-
lines.push('Teams are composed of agent definitions. Here is what you have:');
|
|
830
|
-
lines.push('');
|
|
831
|
-
if (agents.length === 0) {
|
|
832
|
-
lines.push(' No agents defined yet!');
|
|
833
|
-
lines.push(' Create agents first: /build-agent (run without args for the wizard)');
|
|
834
|
-
lines.push('');
|
|
835
|
-
}
|
|
836
|
-
else {
|
|
837
|
-
for (const a of agents) {
|
|
838
|
-
const src = a._source === 'builtin' ? ' (built-in)' : '';
|
|
839
|
-
lines.push(` ${a.name.padEnd(20)} engine: ${a.engine.padEnd(12)} provider: ${(a.provider || 'auto').padEnd(12)} role: ${a.role || 'any'}${src}`);
|
|
431
|
+
else if (key === 'sandboxMode') {
|
|
432
|
+
if (value !== 'auto' && value !== 'native' && value !== 'docker' && value !== 'off') {
|
|
433
|
+
ctx.addMessage('error', 'sandboxMode must be auto, native, docker, or off');
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
config.set('sandboxMode', value);
|
|
437
|
+
ctx.addMessage('system', `✓ sandboxMode set to ${value}`);
|
|
840
438
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
439
|
+
else if (key === 'routing.enabled') {
|
|
440
|
+
const bool = value === 'true';
|
|
441
|
+
const routing = config.get('routing') ?? { enabled: false, costSensitivity: 0.3 };
|
|
442
|
+
config.set('routing', { ...routing, enabled: bool });
|
|
443
|
+
ctx.addMessage('system', `✓ routing.enabled set to ${bool}`);
|
|
845
444
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
lines.push(' Best for: code review, creative alternatives, getting multiple perspectives.');
|
|
856
|
-
lines.push('');
|
|
857
|
-
lines.push(' collaborative');
|
|
858
|
-
lines.push(' Agents work in a pipeline, each building on the previous output.');
|
|
859
|
-
lines.push(' First agent plans, next implements, last reviews.');
|
|
860
|
-
lines.push(' Best for: refactoring, multi-step tasks, plan-implement-review workflows.');
|
|
861
|
-
lines.push('');
|
|
862
|
-
lines.push(' consensus');
|
|
863
|
-
lines.push(' All agents must agree before a result is accepted.');
|
|
864
|
-
lines.push(' Disagreements trigger additional rounds of discussion.');
|
|
865
|
-
lines.push(' Best for: security audits, critical decisions, high-stakes analysis.');
|
|
866
|
-
lines.push('');
|
|
867
|
-
lines.push(' overseer');
|
|
868
|
-
lines.push(' A lead agent decomposes the task and delegates to workers.');
|
|
869
|
-
lines.push(' The overseer reviews and merges all results.');
|
|
870
|
-
lines.push(' Best for: complex projects, research, large feature implementation.');
|
|
871
|
-
lines.push('');
|
|
872
|
-
// Step 3: Strategy recommendations
|
|
873
|
-
lines.push('STEP 3: Pick a strategy based on your task');
|
|
874
|
-
lines.push('');
|
|
875
|
-
lines.push(' Task Type Mode Recommended Agents');
|
|
876
|
-
lines.push(' ─────────────────────────────────────────────────────────');
|
|
877
|
-
lines.push(' Code review competitive code-reviewer, qa-engineer');
|
|
878
|
-
lines.push(' Refactoring collaborative architect, coder, reviewer');
|
|
879
|
-
lines.push(' Security audit consensus security-analyst, code-reviewer, qa-engineer');
|
|
880
|
-
lines.push(' Research overseer researcher, analyst, writer');
|
|
881
|
-
lines.push(' Feature development overseer architect, coder, qa-engineer, reviewer');
|
|
882
|
-
lines.push(' Documentation collaborative researcher, writer');
|
|
883
|
-
lines.push('');
|
|
884
|
-
// Existing teams
|
|
885
|
-
if (teams.length > 0) {
|
|
886
|
-
lines.push('Existing teams:');
|
|
887
|
-
for (const t of teams) {
|
|
888
|
-
lines.push(` ${t.name.padEnd(20)} mode: ${t.mode.padEnd(14)} members: ${t.members.length}`);
|
|
445
|
+
else if (key === 'routing.costSensitivity') {
|
|
446
|
+
const num = Number(value);
|
|
447
|
+
if (isNaN(num) || num < 0 || num > 1) {
|
|
448
|
+
ctx.addMessage('error', 'routing.costSensitivity must be between 0 and 1');
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
const routing = config.get('routing') ?? { enabled: false, costSensitivity: 0.3 };
|
|
452
|
+
config.set('routing', { ...routing, costSensitivity: num });
|
|
453
|
+
ctx.addMessage('system', `✓ routing.costSensitivity set to ${num}`);
|
|
889
454
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
lines.push(` This will auto-compose a team from your available agents (${sampleAgents}${agents.length > 3 ? ', ...' : ''}).`);
|
|
900
|
-
lines.push(' After creation, edit the YAML to fine-tune members and settings.');
|
|
901
|
-
}
|
|
902
|
-
else {
|
|
903
|
-
lines.push(' First, create at least 2 agents:');
|
|
904
|
-
lines.push(' /build-agent planner --engine cli --provider anthropic --role architect');
|
|
905
|
-
lines.push(' /build-agent coder --engine cli --provider anthropic --role coder');
|
|
906
|
-
lines.push('');
|
|
907
|
-
lines.push(' Then build a team:');
|
|
908
|
-
lines.push(' /build-team my-team --mode collaborative');
|
|
909
|
-
}
|
|
910
|
-
lines.push('');
|
|
911
|
-
lines.push('After creation, use the team with:');
|
|
912
|
-
lines.push(' /swarm --team <name> <prompt>');
|
|
913
|
-
lines.push(' spawn_agent team="<name>" prompt="..."');
|
|
914
|
-
ctx.addMessage('system', lines.join('\n'));
|
|
915
|
-
}
|
|
916
|
-
else {
|
|
917
|
-
// Create a team with available agents
|
|
918
|
-
const availableAgents = listAgentDefs(process.cwd());
|
|
919
|
-
const mode = (modeArg || 'collaborative');
|
|
920
|
-
// Auto-compose a reasonable team from available agent definitions
|
|
921
|
-
const memberAgents = availableAgents
|
|
922
|
-
.filter(a => a._source !== 'builtin' || ['code-reviewer', 'architect', 'qa-engineer', 'researcher'].includes(a.name))
|
|
923
|
-
.slice(0, 4);
|
|
924
|
-
if (memberAgents.length < 2) {
|
|
925
|
-
ctx.addMessage('system', `Need at least 2 agent definitions to build a team.\nCreate agents first with /build-agent <name>, then compose them with /build-team.`);
|
|
926
|
-
}
|
|
927
|
-
else {
|
|
928
|
-
const { saveTeamDef } = await import('../agents/index.js');
|
|
929
|
-
const teamDef = {
|
|
930
|
-
name: teamNameArg,
|
|
931
|
-
description: `${teamNameArg} team`,
|
|
932
|
-
mode,
|
|
933
|
-
members: memberAgents.map(a => ({ agent: a.name, role: a.role, weight: a.weight })),
|
|
934
|
-
swarm: { strategy: 'parallel', aggregation: 'structured', maxWorkers: 3 },
|
|
935
|
-
council: { maxRounds: 2, consensusThreshold: 0.67 },
|
|
936
|
-
};
|
|
937
|
-
const filePath = saveTeamDef(process.cwd(), teamDef);
|
|
938
|
-
ctx.addMessage('system', `Team '${teamNameArg}' created: ${filePath}
|
|
939
|
-
|
|
940
|
-
Members:
|
|
941
|
-
${memberAgents.map(a => ` ${a.name} [${a.role || 'any'}]`).join('\n')}
|
|
942
|
-
|
|
943
|
-
Mode: ${mode}
|
|
944
|
-
|
|
945
|
-
Use with:
|
|
946
|
-
/swarm --team ${teamNameArg} <prompt>
|
|
947
|
-
/swarm coord --team ${teamNameArg} <prompt>
|
|
948
|
-
spawn_agent team="${teamNameArg}" prompt="..."
|
|
949
|
-
|
|
950
|
-
Edit the YAML to customize members, strategy, and coordination settings.`);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
break;
|
|
954
|
-
}
|
|
955
|
-
case '/set': {
|
|
956
|
-
// /set <key> <value>
|
|
957
|
-
const key = parts[1];
|
|
958
|
-
const value = parts.slice(2).join(' ');
|
|
959
|
-
if (!key || !value) {
|
|
960
|
-
ctx.addMessage('system', `Usage: /set <key> <value>
|
|
961
|
-
Available keys:
|
|
962
|
-
maxIterations <number> - Max agent iterations (current: ${config.get('maxIterations')})
|
|
963
|
-
sessionLogLimit <number> - Cap retained session log items (current: ${formatSessionLogLimit(config.get('sessionLogLimit'))}, 0 = unlimited)
|
|
964
|
-
persona <name> - calliope, muse, minimal
|
|
965
|
-
fancyOutput <bool> - true/false`);
|
|
966
|
-
break;
|
|
967
|
-
}
|
|
968
|
-
try {
|
|
969
|
-
if (key === 'maxIterations') {
|
|
970
|
-
const num = parseInt(value, 10);
|
|
971
|
-
if (isNaN(num) || num < 0 || num > 1000000) {
|
|
972
|
-
ctx.addMessage('error', 'maxIterations must be 0-1000000 (0 = unlimited)');
|
|
973
|
-
break;
|
|
455
|
+
else if (key === 'theme') {
|
|
456
|
+
const themes = await import('../themes.js');
|
|
457
|
+
if (themes.setCurrentTheme(value)) {
|
|
458
|
+
themes.clearThemeCache();
|
|
459
|
+
ctx.addMessage('system', `✓ theme set to ${value}`);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
ctx.addMessage('error', `Theme not found: ${value}. Options: dark, light, no-color`);
|
|
463
|
+
}
|
|
974
464
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
}
|
|
978
|
-
else if (key === 'sessionLogLimit') {
|
|
979
|
-
const num = parseInt(value, 10);
|
|
980
|
-
if (isNaN(num) || num < 0 || num > 100000) {
|
|
981
|
-
ctx.addMessage('error', 'sessionLogLimit must be 0-100000 (0 = unlimited)');
|
|
982
|
-
break;
|
|
465
|
+
else if (key === 'budget.maxCostPerRun') {
|
|
466
|
+
ctx.addMessage('system', applyBudgetKey('maxCostPerRun', value));
|
|
983
467
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
468
|
+
else if (key === 'budget.maxTokensPerRun') {
|
|
469
|
+
ctx.addMessage('system', applyBudgetKey('maxTokensPerRun', value));
|
|
470
|
+
}
|
|
471
|
+
else if (key === 'budget.maxCostPerProject') {
|
|
472
|
+
ctx.addMessage('system', applyBudgetKey('maxCostPerProject', value));
|
|
473
|
+
}
|
|
474
|
+
else if (key === 'audit.enabled') {
|
|
475
|
+
const bool = value === 'true';
|
|
476
|
+
config.set('audit', { ...(config.get('audit') ?? {}), enabled: bool });
|
|
477
|
+
ctx.addMessage('system', `✓ audit.enabled set to ${bool}`);
|
|
478
|
+
}
|
|
479
|
+
else if (key === 'policy.command') {
|
|
480
|
+
const policy = { ...(config.get('policy') ?? {}) };
|
|
481
|
+
if (value === 'off' || value === 'none') {
|
|
482
|
+
delete policy.command;
|
|
483
|
+
config.set('policy', policy);
|
|
484
|
+
ctx.addMessage('system', '✓ policy.command cleared');
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
config.set('policy', { ...policy, command: value });
|
|
488
|
+
ctx.addMessage('system', '✓ policy.command set');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
ctx.addMessage('error', `Unknown config key: ${key}`);
|
|
992
493
|
}
|
|
993
|
-
config.set('persona', value);
|
|
994
|
-
ctx.setPersona(value);
|
|
995
|
-
ctx.addMessage('system', `\u2713 persona set to ${value}`);
|
|
996
|
-
}
|
|
997
|
-
else if (key === 'fancyOutput') {
|
|
998
|
-
const bool = value === 'true';
|
|
999
|
-
config.set('fancyOutput', bool);
|
|
1000
|
-
ctx.addMessage('system', `\u2713 fancyOutput set to ${bool}`);
|
|
1001
494
|
}
|
|
1002
|
-
|
|
1003
|
-
ctx.addMessage('error', `
|
|
495
|
+
catch (err) {
|
|
496
|
+
ctx.addMessage('error', `Failed to set ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1004
497
|
}
|
|
1005
498
|
}
|
|
1006
|
-
|
|
1007
|
-
ctx.addMessage('
|
|
499
|
+
else {
|
|
500
|
+
ctx.addMessage('system', `Config: ${config.getConfigPath()}\nProviders: ${config.getConfiguredProviders().join(', ') || 'none'}\nmaxIterations: ${config.get('maxIterations')}\nsessionLogLimit: ${formatSessionLogLimit(config.get('sessionLogLimit'))} (set > 0 to cap)\n\nChange a setting with /config set <key> <value>`);
|
|
1008
501
|
}
|
|
1009
502
|
break;
|
|
1010
503
|
}
|
|
1011
504
|
case '/setup':
|
|
1012
505
|
ctx.addMessage('system', 'Run `calliope --setup` to reconfigure.');
|
|
1013
506
|
break;
|
|
1014
|
-
case '/
|
|
1015
|
-
// /
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
classic - Everything in chronological order
|
|
1022
|
-
response-top - Calliope response at top, tools below
|
|
1023
|
-
response-bottom - Tools at top, response at bottom (default)
|
|
1024
|
-
split - Side by side: tools left, response right
|
|
1025
|
-
zen - Response only, tools hidden — distraction-free
|
|
1026
|
-
focus - Latest response pinned top, compact tool log
|
|
1027
|
-
dashboard - Three-panel: stats, response, tools
|
|
1028
|
-
minimal - No decorations, raw text output
|
|
1029
|
-
|
|
1030
|
-
Usage: /layout <name>`);
|
|
1031
|
-
break;
|
|
1032
|
-
}
|
|
1033
|
-
const validLayouts = ['classic', 'response-top', 'response-bottom', 'split', 'zen', 'focus', 'dashboard', 'minimal'];
|
|
1034
|
-
if (!validLayouts.includes(layoutArg)) {
|
|
1035
|
-
ctx.addMessage('error', `Invalid layout. Choose: ${validLayouts.join(', ')}`);
|
|
1036
|
-
break;
|
|
1037
|
-
}
|
|
1038
|
-
config.set('layout', layoutArg);
|
|
1039
|
-
ctx.setLayout(layoutArg);
|
|
1040
|
-
ctx.addMessage('system', `\u2713 Layout set to: ${layoutArg}`);
|
|
1041
|
-
break;
|
|
1042
|
-
}
|
|
1043
|
-
case '/density': {
|
|
1044
|
-
// /density [normal|compact]
|
|
1045
|
-
const densityArg = parts[1];
|
|
1046
|
-
if (!densityArg) {
|
|
1047
|
-
ctx.addMessage('system', `Current density: ${ctx.density}
|
|
1048
|
-
|
|
1049
|
-
Available densities:
|
|
1050
|
-
normal - Standard spacing
|
|
1051
|
-
compact - Reduced whitespace for more info
|
|
1052
|
-
|
|
1053
|
-
Usage: /density <normal|compact>`);
|
|
1054
|
-
break;
|
|
1055
|
-
}
|
|
1056
|
-
const validDensities = ['normal', 'compact'];
|
|
1057
|
-
if (!validDensities.includes(densityArg)) {
|
|
1058
|
-
ctx.addMessage('error', `Invalid density. Choose: normal, compact`);
|
|
1059
|
-
break;
|
|
1060
|
-
}
|
|
1061
|
-
config.set('density', densityArg);
|
|
1062
|
-
ctx.setDensity(densityArg);
|
|
1063
|
-
ctx.addMessage('system', `\u2713 Density set to: ${densityArg}`);
|
|
1064
|
-
break;
|
|
1065
|
-
}
|
|
1066
|
-
case '/collapse': {
|
|
1067
|
-
// /collapse [tools|thinking|all|off] [limit N]
|
|
1068
|
-
const subCmd = parts[1];
|
|
1069
|
-
if (!subCmd) {
|
|
1070
|
-
ctx.addMessage('system', `Collapse settings:
|
|
1071
|
-
collapseTools: ${ctx.collapseSettings.collapseTools}
|
|
1072
|
-
collapseThinking: ${ctx.collapseSettings.collapseThinking}
|
|
1073
|
-
toolDisplayLimit: ${ctx.collapseSettings.toolDisplayLimit} (0 = all expanded)
|
|
1074
|
-
|
|
1075
|
-
Usage:
|
|
1076
|
-
/collapse tools - Toggle tool output collapsing
|
|
1077
|
-
/collapse thinking - Toggle thinking block collapsing
|
|
1078
|
-
/collapse all - Collapse both tools and thinking
|
|
1079
|
-
/collapse off - Expand everything
|
|
1080
|
-
/collapse limit <N> - Show last N tools expanded (0 = all)`);
|
|
1081
|
-
break;
|
|
1082
|
-
}
|
|
1083
|
-
if (subCmd === 'tools') {
|
|
1084
|
-
const newVal = !ctx.collapseSettings.collapseTools;
|
|
1085
|
-
config.set('collapseTools', newVal);
|
|
1086
|
-
ctx.setCollapseSettings(prev => ({ ...prev, collapseTools: newVal }));
|
|
1087
|
-
ctx.addMessage('system', `\u2713 collapseTools set to ${newVal}`);
|
|
1088
|
-
}
|
|
1089
|
-
else if (subCmd === 'thinking') {
|
|
1090
|
-
const newVal = !ctx.collapseSettings.collapseThinking;
|
|
1091
|
-
config.set('collapseThinking', newVal);
|
|
1092
|
-
ctx.setCollapseSettings(prev => ({ ...prev, collapseThinking: newVal }));
|
|
1093
|
-
ctx.addMessage('system', `\u2713 collapseThinking set to ${newVal}`);
|
|
1094
|
-
}
|
|
1095
|
-
else if (subCmd === 'all') {
|
|
1096
|
-
config.set('collapseTools', true);
|
|
1097
|
-
config.set('collapseThinking', true);
|
|
1098
|
-
ctx.setCollapseSettings(prev => ({ ...prev, collapseTools: true, collapseThinking: true }));
|
|
1099
|
-
ctx.addMessage('system', '\u2713 Collapsing tools and thinking');
|
|
1100
|
-
}
|
|
1101
|
-
else if (subCmd === 'off') {
|
|
1102
|
-
config.set('collapseTools', false);
|
|
1103
|
-
config.set('collapseThinking', false);
|
|
1104
|
-
ctx.setCollapseSettings(prev => ({ ...prev, collapseTools: false, collapseThinking: false }));
|
|
1105
|
-
ctx.addMessage('system', '\u2713 Expanding all output');
|
|
1106
|
-
}
|
|
1107
|
-
else if (subCmd === 'limit') {
|
|
1108
|
-
const limit = parseInt(parts[2], 10);
|
|
1109
|
-
if (isNaN(limit) || limit < 0 || limit > 100) {
|
|
1110
|
-
ctx.addMessage('error', 'Limit must be 0-100');
|
|
1111
|
-
break;
|
|
507
|
+
case '/loop': {
|
|
508
|
+
// /loop stop absorbs the old /cancel-loop, /breakloop, /stop
|
|
509
|
+
if (parts[1] === 'stop') {
|
|
510
|
+
if (ctx.loopActive) {
|
|
511
|
+
ctx.loopCancelledRef.current = true;
|
|
512
|
+
ctx.setLoopActive(false);
|
|
513
|
+
ctx.addMessage('system', '\u{1F6D1} Loop cancelled');
|
|
1112
514
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
else {
|
|
1118
|
-
ctx.addMessage('error', 'Unknown collapse option. Use: tools, thinking, all, off, or limit <N>');
|
|
515
|
+
else {
|
|
516
|
+
ctx.addMessage('system', 'No active loop to cancel');
|
|
517
|
+
}
|
|
518
|
+
break;
|
|
1119
519
|
}
|
|
1120
|
-
break;
|
|
1121
|
-
}
|
|
1122
|
-
case '/loop': {
|
|
1123
520
|
// Parse /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
|
|
1124
521
|
if (ctx.loopActive) {
|
|
1125
|
-
ctx.addMessage('system', 'Loop already running. Use /
|
|
522
|
+
ctx.addMessage('system', 'Loop already running. Use /loop stop to stop it first.');
|
|
1126
523
|
break;
|
|
1127
524
|
}
|
|
1128
525
|
const loopArgs = parts.slice(1).join(' ');
|
|
@@ -1138,7 +535,8 @@ Usage:
|
|
|
1138
535
|
prompt = quotedMatch[1];
|
|
1139
536
|
if (!prompt) {
|
|
1140
537
|
ctx.addMessage('system', `Usage: /loop "<prompt>" [--max-iterations N] [--completion-promise "text"]
|
|
1141
|
-
Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE"
|
|
538
|
+
Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE"
|
|
539
|
+
Stop a running loop with /loop stop`);
|
|
1142
540
|
break;
|
|
1143
541
|
}
|
|
1144
542
|
const defaultMaxIterations = resolveIterationLimit(config.get('maxIterations'));
|
|
@@ -1156,86 +554,11 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1156
554
|
Prompt: "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"
|
|
1157
555
|
Max iterations: ${formatIterationLimit(loopMaxIterations)}
|
|
1158
556
|
${completionMatch ? `Completion promise: "${completionMatch[1]}"` : isFiniteIterationLimit(loopMaxIterations) ? 'No completion promise (runs until max iterations)' : 'No completion promise (runs until stopped)'}
|
|
1159
|
-
Use /
|
|
557
|
+
Use /loop stop to stop`);
|
|
1160
558
|
// Start the loop execution (non-blocking)
|
|
1161
559
|
ctx.runLoop(prompt, loopMaxIterations, completionMatch?.[1]);
|
|
1162
560
|
break;
|
|
1163
561
|
}
|
|
1164
|
-
case '/cancel-loop':
|
|
1165
|
-
case '/breakloop':
|
|
1166
|
-
case '/stop':
|
|
1167
|
-
if (ctx.loopActive) {
|
|
1168
|
-
ctx.loopCancelledRef.current = true;
|
|
1169
|
-
ctx.setLoopActive(false);
|
|
1170
|
-
ctx.addMessage('system', '\u{1F6D1} Loop cancelled');
|
|
1171
|
-
}
|
|
1172
|
-
else {
|
|
1173
|
-
ctx.addMessage('system', 'No active loop to cancel');
|
|
1174
|
-
}
|
|
1175
|
-
break;
|
|
1176
|
-
case '/confirm':
|
|
1177
|
-
if (parts[1] === 'on') {
|
|
1178
|
-
ctx.setConfirmMode(true);
|
|
1179
|
-
ctx.addMessage('system', '\u2713 Confirmation mode ON - will ask before risky operations');
|
|
1180
|
-
}
|
|
1181
|
-
else if (parts[1] === 'off') {
|
|
1182
|
-
ctx.setConfirmMode(false);
|
|
1183
|
-
ctx.addMessage('system', '\u26A0\uFE0F Confirmation mode OFF - risky operations will auto-execute');
|
|
1184
|
-
}
|
|
1185
|
-
else {
|
|
1186
|
-
ctx.addMessage('system', `Confirm mode: ${ctx.confirmMode ? 'ON' : 'OFF'}\nUsage: /confirm [on|off]`);
|
|
1187
|
-
}
|
|
1188
|
-
break;
|
|
1189
|
-
case '/profile': {
|
|
1190
|
-
const subCmd = parts[1];
|
|
1191
|
-
if (subCmd === 'list' || !subCmd) {
|
|
1192
|
-
const profiles = config.listProfiles();
|
|
1193
|
-
const active = config.getActiveProfile();
|
|
1194
|
-
const list = profiles.map(p => {
|
|
1195
|
-
const marker = p.name === active ? '\u2192 ' : ' ';
|
|
1196
|
-
const tag = p.builtin ? '(built-in)' : '(custom)';
|
|
1197
|
-
return `${marker}${p.name}: ${p.profile.provider}/${p.profile.model || 'default'} ${tag}`;
|
|
1198
|
-
}).join('\n');
|
|
1199
|
-
ctx.addMessage('system', `Profiles:\n${list}\n\nUsage: /profile <name> | /profile save <name>`);
|
|
1200
|
-
}
|
|
1201
|
-
else if (subCmd === 'save' && parts[2]) {
|
|
1202
|
-
const name = parts[2];
|
|
1203
|
-
config.saveProfile(name, {
|
|
1204
|
-
provider: ctx.provider,
|
|
1205
|
-
model: ctx.model,
|
|
1206
|
-
persona: ctx.persona,
|
|
1207
|
-
confirmMode: ctx.confirmMode,
|
|
1208
|
-
});
|
|
1209
|
-
ctx.addMessage('system', `\u2713 Saved profile: ${name}`);
|
|
1210
|
-
}
|
|
1211
|
-
else if (subCmd === 'delete' && parts[2]) {
|
|
1212
|
-
const name = parts[2];
|
|
1213
|
-
if (config.deleteProfile(name)) {
|
|
1214
|
-
ctx.addMessage('system', `\u2713 Deleted profile: ${name}`);
|
|
1215
|
-
}
|
|
1216
|
-
else {
|
|
1217
|
-
ctx.addMessage('error', `Cannot delete profile: ${name} (built-in or not found)`);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
else {
|
|
1221
|
-
// Load profile
|
|
1222
|
-
const profile = config.getProfile(subCmd);
|
|
1223
|
-
if (profile) {
|
|
1224
|
-
ctx.setProvider(profile.provider);
|
|
1225
|
-
if (profile.model)
|
|
1226
|
-
ctx.setModel(profile.model);
|
|
1227
|
-
ctx.setPersona(profile.persona);
|
|
1228
|
-
if (profile.confirmMode !== undefined)
|
|
1229
|
-
ctx.setConfirmMode(profile.confirmMode);
|
|
1230
|
-
config.setActiveProfile(subCmd);
|
|
1231
|
-
ctx.addMessage('system', `\u2713 Loaded profile: ${subCmd} (${profile.provider}/${profile.model || 'default'})`);
|
|
1232
|
-
}
|
|
1233
|
-
else {
|
|
1234
|
-
ctx.addMessage('error', `Profile not found: ${subCmd}\nBuilt-in: fast, smart, cheap, local`);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
break;
|
|
1238
|
-
}
|
|
1239
562
|
case '/mcp': {
|
|
1240
563
|
const subCmd = parts[1];
|
|
1241
564
|
if (subCmd === 'list' || !subCmd) {
|
|
@@ -1245,7 +568,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1245
568
|
}
|
|
1246
569
|
else {
|
|
1247
570
|
const list = servers.map(s => {
|
|
1248
|
-
const status = s.status === 'connected' ? '\u{1F7E2}' : s.status === 'error' ? '\u{1F534}' : '
|
|
571
|
+
const status = s.status === 'connected' ? '\u{1F7E2}' : s.status === 'error' ? '\u{1F534}' : '⚪';
|
|
1249
572
|
return `${status} ${s.name} (${s.tools.length} tools)\n ${s.url}`;
|
|
1250
573
|
}).join('\n\n');
|
|
1251
574
|
ctx.addMessage('system', `MCP Servers:\n\n${list}`);
|
|
@@ -1256,7 +579,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1256
579
|
ctx.addMessage('system', `Registering MCP server: ${url}...`);
|
|
1257
580
|
try {
|
|
1258
581
|
const server = await mcp.registerServer(url);
|
|
1259
|
-
ctx.addMessage('system',
|
|
582
|
+
ctx.addMessage('system', `✓ Registered: ${server.name} (${server.tools.length} tools)`);
|
|
1260
583
|
}
|
|
1261
584
|
catch (e) {
|
|
1262
585
|
ctx.addMessage('error', `Failed to register: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -1264,7 +587,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1264
587
|
}
|
|
1265
588
|
else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
|
|
1266
589
|
if (mcp.unregisterServer(parts[2])) {
|
|
1267
|
-
ctx.addMessage('system', '
|
|
590
|
+
ctx.addMessage('system', '✓ Server removed');
|
|
1268
591
|
}
|
|
1269
592
|
else {
|
|
1270
593
|
ctx.addMessage('error', 'Server not found');
|
|
@@ -1286,7 +609,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1286
609
|
ctx.addMessage('system', 'No MCP tools available. Add servers with /mcp add <url>');
|
|
1287
610
|
}
|
|
1288
611
|
else {
|
|
1289
|
-
const list = tools.map(t =>
|
|
612
|
+
const list = tools.map(t => `• ${t.name}\n ${t.description}`).join('\n\n');
|
|
1290
613
|
ctx.addMessage('system', `MCP Tools:\n\n${list}`);
|
|
1291
614
|
}
|
|
1292
615
|
}
|
|
@@ -1298,14 +621,20 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1298
621
|
case '/skills': {
|
|
1299
622
|
const subCmd = parts[1];
|
|
1300
623
|
if (subCmd === 'list' || !subCmd) {
|
|
1301
|
-
|
|
1302
|
-
|
|
624
|
+
// Show every installed skill with its trust state (#137) — including
|
|
625
|
+
// CHANGED entries that getSkills() withholds from the model.
|
|
626
|
+
const listed = skills.listSkills();
|
|
627
|
+
if (listed.length === 0) {
|
|
1303
628
|
ctx.addMessage('system', 'No skills installed.\n\nUsage:\n /skills add <name> - Install from agentskills.io\n /skills add <github-url> - Install from GitHub\n /skills add <path> - Install from local directory');
|
|
1304
629
|
}
|
|
1305
630
|
else {
|
|
1306
|
-
const list =
|
|
631
|
+
const list = listed.map(s => {
|
|
1307
632
|
const src = s.source === 'github' ? '(GitHub)' : s.source === 'registry' ? '(agentskills.io)' : '(local)';
|
|
1308
|
-
|
|
633
|
+
const trust = s.trust === 'pinned' ? (s.fingerprint ?? 'pinned') :
|
|
634
|
+
s.trust === 'changed' ? 'CHANGED — reinstall to re-trust' :
|
|
635
|
+
'UNVERIFIED';
|
|
636
|
+
const desc = s.description ? `${s.description.substring(0, 80)}...` : '';
|
|
637
|
+
return `• ${s.name} ${src} [${trust}]\n ${desc}`;
|
|
1309
638
|
}).join('\n\n');
|
|
1310
639
|
ctx.addMessage('system', `Installed Skills:\n\n${list}`);
|
|
1311
640
|
}
|
|
@@ -1313,6 +642,10 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1313
642
|
else if (subCmd === 'add' && parts[2]) {
|
|
1314
643
|
const source = parts[2];
|
|
1315
644
|
ctx.addMessage('system', `Installing skill: ${source}...`);
|
|
645
|
+
// Snapshot pinned fingerprints so a re-install can show old→new rather
|
|
646
|
+
// than re-pinning silently (#137). The remote name is only known after
|
|
647
|
+
// the fetch, so we key the lookup by the returned skill's name.
|
|
648
|
+
const priorFingerprints = new Map(skills.listSkills().map(s => [s.name, s.fingerprint]));
|
|
1316
649
|
try {
|
|
1317
650
|
let skill;
|
|
1318
651
|
if (source.startsWith('http')) {
|
|
@@ -1325,7 +658,14 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1325
658
|
skill = await skills.installFromRegistry(source);
|
|
1326
659
|
}
|
|
1327
660
|
if (skill) {
|
|
1328
|
-
|
|
661
|
+
const prev = priorFingerprints.get(skill.metadata.name);
|
|
662
|
+
const now = skill.fingerprint ?? '(unverified)';
|
|
663
|
+
if (prev && prev !== skill.fingerprint) {
|
|
664
|
+
ctx.addMessage('system', `✓ Re-pinned ${skill.metadata.name}: ${prev} → ${now} (content updated)`);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
ctx.addMessage('system', `✓ Installed ${skill.metadata.name} — pinned ${now} (trust-on-first-use)`);
|
|
668
|
+
}
|
|
1329
669
|
}
|
|
1330
670
|
else {
|
|
1331
671
|
ctx.addMessage('error', 'Failed to install skill');
|
|
@@ -1337,7 +677,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1337
677
|
}
|
|
1338
678
|
else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
|
|
1339
679
|
if (skills.uninstallSkill(parts[2])) {
|
|
1340
|
-
ctx.addMessage('system', '
|
|
680
|
+
ctx.addMessage('system', '✓ Skill removed');
|
|
1341
681
|
}
|
|
1342
682
|
else {
|
|
1343
683
|
ctx.addMessage('error', 'Skill not found');
|
|
@@ -1354,10 +694,19 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1354
694
|
info += `License: ${skill.metadata.license}\n`;
|
|
1355
695
|
if (skill.sourceUrl)
|
|
1356
696
|
info += `Source: ${skill.sourceUrl}\n`;
|
|
697
|
+
info += skill.fingerprint ? `Fingerprint: ${skill.fingerprint} (${skill.trust})\n` : 'Fingerprint: none (UNVERIFIED)\n';
|
|
1357
698
|
ctx.addMessage('system', info);
|
|
1358
699
|
}
|
|
1359
700
|
else {
|
|
1360
|
-
|
|
701
|
+
// getSkill withholds a tampered skill; surface WHY, not a bare
|
|
702
|
+
// "not found", so the trust state stays visible (#137).
|
|
703
|
+
const changed = skills.listSkills().find(s => s.name === parts[2] && s.trust === 'changed');
|
|
704
|
+
if (changed) {
|
|
705
|
+
ctx.addMessage('error', `Skill "${parts[2]}" CHANGED since install (pinned ${changed.fingerprint}) — content no longer matches. Reinstall to re-trust.`);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
ctx.addMessage('error', 'Skill not found');
|
|
709
|
+
}
|
|
1361
710
|
}
|
|
1362
711
|
}
|
|
1363
712
|
else {
|
|
@@ -1434,894 +783,55 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
1434
783
|
}
|
|
1435
784
|
break;
|
|
1436
785
|
}
|
|
1437
|
-
case '/
|
|
1438
|
-
const fuzzy = await import('../fuzzy-search.js');
|
|
1439
|
-
const query = parts.slice(1).join(' ');
|
|
1440
|
-
if (!query) {
|
|
1441
|
-
ctx.addMessage('system', 'Usage: /find <pattern>\nFuzzy search for files');
|
|
1442
|
-
}
|
|
1443
|
-
else {
|
|
1444
|
-
const results = fuzzy.searchWithHighlight(process.cwd(), query, { maxResults: 20 });
|
|
1445
|
-
if (results.length === 0) {
|
|
1446
|
-
ctx.addMessage('system', 'No files found');
|
|
1447
|
-
}
|
|
1448
|
-
else {
|
|
1449
|
-
const list = results.map((r, i) => `${i + 1}. ${r.highlighted}`).join('\n');
|
|
1450
|
-
ctx.addMessage('system', `Found ${results.length} files:\n\n${list}`);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
break;
|
|
1454
|
-
}
|
|
1455
|
-
case '/branch': {
|
|
1456
|
-
const branching = await import('../branching.js');
|
|
786
|
+
case '/scope': {
|
|
1457
787
|
const subCmd = parts[1];
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
else if (subCmd === 'new' && parts[2]) {
|
|
1464
|
-
const branch = branching.createBranch(sessionId, parts[2], ctx.llmMessages.current, parts.slice(3).join(' '));
|
|
1465
|
-
ctx.addMessage('system', `Created branch: ${branch.name}`);
|
|
1466
|
-
}
|
|
1467
|
-
else if (subCmd === 'switch' && parts[2]) {
|
|
1468
|
-
const msgs = branching.switchBranch(sessionId, parts[2], ctx.llmMessages.current);
|
|
1469
|
-
if (msgs) {
|
|
1470
|
-
ctx.llmMessages.current = msgs;
|
|
1471
|
-
ctx.addMessage('system', `Switched to branch: ${parts[2]}`);
|
|
788
|
+
if (subCmd === 'add') {
|
|
789
|
+
// Absorbs the old /add-dir
|
|
790
|
+
const dirPath = parts.slice(2).join(' ').replace(/^["']|["']$/g, '');
|
|
791
|
+
if (!dirPath) {
|
|
792
|
+
ctx.addMessage('system', 'Usage: /scope add <path>\n\nAdd a directory to the allowed scope.\nThe agent can only access files within scope.');
|
|
1472
793
|
}
|
|
1473
794
|
else {
|
|
1474
|
-
|
|
795
|
+
const result = addToScope(dirPath);
|
|
796
|
+
if (result.success) {
|
|
797
|
+
ctx.addMessage('system', `✓ ${result.message}`);
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
ctx.addMessage('error', result.message);
|
|
801
|
+
}
|
|
1475
802
|
}
|
|
1476
803
|
}
|
|
1477
|
-
else if (subCmd === '
|
|
1478
|
-
|
|
1479
|
-
|
|
804
|
+
else if (subCmd === 'remove') {
|
|
805
|
+
// Absorbs the old /remove-dir
|
|
806
|
+
const dirPath = parts.slice(2).join(' ').replace(/^["']|["']$/g, '');
|
|
807
|
+
if (!dirPath) {
|
|
808
|
+
ctx.addMessage('system', 'Usage: /scope remove <path>\n\nRemove a directory from the allowed scope.');
|
|
1480
809
|
}
|
|
1481
810
|
else {
|
|
1482
|
-
|
|
811
|
+
const result = removeFromScope(dirPath);
|
|
812
|
+
if (result.success) {
|
|
813
|
+
ctx.addMessage('system', `✓ ${result.message}`);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
ctx.addMessage('error', result.message);
|
|
817
|
+
}
|
|
1483
818
|
}
|
|
1484
819
|
}
|
|
1485
|
-
else {
|
|
1486
|
-
ctx.addMessage('system',
|
|
1487
|
-
}
|
|
1488
|
-
break;
|
|
1489
|
-
}
|
|
1490
|
-
case '/theme': {
|
|
1491
|
-
const subCmd = parts[1];
|
|
1492
|
-
if (!subCmd) {
|
|
1493
|
-
// Open interactive theme picker
|
|
1494
|
-
ctx.setModalMode('theme-picker');
|
|
1495
|
-
}
|
|
1496
|
-
else if (subCmd === 'list') {
|
|
1497
|
-
const themes = await import('../themes.js');
|
|
1498
|
-
const list = themes.listThemes();
|
|
1499
|
-
const current = themes.getCurrentThemeName();
|
|
1500
|
-
const formatted = list.map((t) => {
|
|
1501
|
-
const marker = t.name === current ? ' *' : '';
|
|
1502
|
-
const custom = t.custom ? ' (custom)' : '';
|
|
1503
|
-
return ` ${t.name}${marker}${custom} - ${t.description || 'No description'}`;
|
|
1504
|
-
}).join('\n');
|
|
1505
|
-
ctx.addMessage('system', `Available themes:\n${formatted}`);
|
|
1506
|
-
}
|
|
1507
|
-
else {
|
|
1508
|
-
const themes = await import('../themes.js');
|
|
1509
|
-
if (themes.setCurrentTheme(subCmd)) {
|
|
1510
|
-
themes.clearThemeCache();
|
|
1511
|
-
ctx.addMessage('system', `Theme set to: ${subCmd}`);
|
|
1512
|
-
}
|
|
1513
|
-
else {
|
|
1514
|
-
ctx.addMessage('error', `Theme not found: ${subCmd}`);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
break;
|
|
1518
|
-
}
|
|
1519
|
-
case '/skin': {
|
|
1520
|
-
const subCmd = parts[1];
|
|
1521
|
-
if (subCmd === 'list' || !subCmd) {
|
|
1522
|
-
const skins = listSkins();
|
|
1523
|
-
const current = getCurrentSkin();
|
|
1524
|
-
const formatted = skins.map((s) => {
|
|
1525
|
-
const marker = s.name === current.name ? ' *' : '';
|
|
1526
|
-
const custom = s.custom ? ' (custom)' : '';
|
|
1527
|
-
return ` ${s.name}${marker}${custom} - ${s.description}`;
|
|
1528
|
-
}).join('\n');
|
|
1529
|
-
ctx.addMessage('system', `Active: ${current.name}\nAvailable skins:\n${formatted}`);
|
|
1530
|
-
}
|
|
1531
|
-
else {
|
|
1532
|
-
applySkin(subCmd);
|
|
1533
|
-
const newSkin = getCurrentSkin();
|
|
1534
|
-
if (newSkin.name === subCmd) {
|
|
1535
|
-
config.set('activeSkin', subCmd);
|
|
1536
|
-
ctx.addMessage('system', `Skin set to: ${subCmd} \u2014 ${newSkin.description}`);
|
|
1537
|
-
}
|
|
1538
|
-
else {
|
|
1539
|
-
ctx.addMessage('error', `Skin not found: ${subCmd}. Available: ${listSkins().map((s) => s.name).join(', ')}`);
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
break;
|
|
1543
|
-
}
|
|
1544
|
-
case '/palette': {
|
|
1545
|
-
const subCmd = parts[1];
|
|
1546
|
-
if (subCmd === 'list' || !subCmd) {
|
|
1547
|
-
const palettes = listPalettes();
|
|
1548
|
-
const current = getCurrentPalette();
|
|
1549
|
-
const formatted = palettes.map((p) => {
|
|
1550
|
-
const marker = p.name === current.name ? ' *' : '';
|
|
1551
|
-
const custom = p.custom ? ' (custom)' : '';
|
|
1552
|
-
return ` ${p.name}${marker}${custom} - ${p.description}`;
|
|
1553
|
-
}).join('\n');
|
|
1554
|
-
ctx.addMessage('system', `Active: ${current.name}\nAvailable palettes:\n${formatted}`);
|
|
1555
|
-
}
|
|
1556
|
-
else {
|
|
1557
|
-
applyPalette(subCmd);
|
|
1558
|
-
const newPal = getCurrentPalette();
|
|
1559
|
-
if (newPal.name === subCmd) {
|
|
1560
|
-
config.set('activePalette', subCmd);
|
|
1561
|
-
ctx.addMessage('system', `Palette set to: ${subCmd} \u2014 ${newPal.description}`);
|
|
1562
|
-
}
|
|
1563
|
-
else {
|
|
1564
|
-
ctx.addMessage('error', `Palette not found: ${subCmd}. Available: ${listPalettes().map((p) => p.name).join(', ')}`);
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
break;
|
|
1568
|
-
}
|
|
1569
|
-
case '/companion': {
|
|
1570
|
-
const subCmd = parts[1];
|
|
1571
|
-
if (subCmd === 'list' || !subCmd) {
|
|
1572
|
-
const companions = listCompanions();
|
|
1573
|
-
const current = getCurrentCompanion();
|
|
1574
|
-
const formatted = companions.map((comp) => {
|
|
1575
|
-
const marker = comp.name === current.name ? ' *' : '';
|
|
1576
|
-
return ` ${comp.name}${marker} - ${comp.description}`;
|
|
1577
|
-
}).join('\n');
|
|
1578
|
-
ctx.addMessage('system', `Active: ${current.name}\nAvailable companions:\n${formatted}`);
|
|
1579
|
-
}
|
|
1580
|
-
else {
|
|
1581
|
-
applyCompanion(subCmd);
|
|
1582
|
-
const newComp = getCurrentCompanion();
|
|
1583
|
-
if (newComp.name === subCmd) {
|
|
1584
|
-
config.set('activeCompanion', subCmd);
|
|
1585
|
-
ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
|
|
1586
|
-
ctx.addMessage('system', `Companion set to: ${subCmd} \u2014 "${newComp.greeting}"`);
|
|
1587
|
-
}
|
|
1588
|
-
else {
|
|
1589
|
-
ctx.addMessage('error', `Companion not found: ${subCmd}. Available: ${listCompanions().map((c) => c.name).join(', ')}`);
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
break;
|
|
1593
|
-
}
|
|
1594
|
-
case '/banner': {
|
|
1595
|
-
const bannerSkin = getCurrentSkin();
|
|
1596
|
-
const bannerPalette = getCurrentPalette();
|
|
1597
|
-
const bannerColor = bannerPalette.colors.primary;
|
|
1598
|
-
const imgInfo = getTerminalImageInfo();
|
|
1599
|
-
const rendered = renderSkinBanner(bannerSkin.banner.art, bannerColor, bannerSkin.banner.tagline ?? undefined, imgInfo.mode);
|
|
1600
|
-
ctx.addMessage('system', `${rendered}\n\nSkin: ${bannerSkin.name} | Terminal: ${getImageModeLabel(imgInfo.mode)}${imgInfo.truecolor ? ' (truecolor)' : ''}`);
|
|
1601
|
-
break;
|
|
1602
|
-
}
|
|
1603
|
-
case '/hud': {
|
|
1604
|
-
const hudSkin = getCurrentSkin();
|
|
1605
|
-
const hudPalette = getCurrentPalette();
|
|
1606
|
-
const hudCompanion = getCurrentCompanion();
|
|
1607
|
-
const hudPack = getCurrentPack();
|
|
1608
|
-
const hudIntensity = getCompanionMode();
|
|
1609
|
-
ctx.addMessage('system', `HUD Configuration\n` +
|
|
1610
|
-
(hudPack ? ` Pack: ${hudPack.name} — ${hudPack.description}\n` : '') +
|
|
1611
|
-
` Skin: ${hudSkin.name} — ${hudSkin.description}\n` +
|
|
1612
|
-
` Palette: ${hudPalette.name} — ${hudPalette.description}\n` +
|
|
1613
|
-
` Companion: ${hudCompanion.name} — ${hudCompanion.description}\n` +
|
|
1614
|
-
` Intensity: ${hudIntensity}\n` +
|
|
1615
|
-
` Emojis: ${config.get('useEmojis') !== false ? 'ON' : 'OFF'}\n` +
|
|
1616
|
-
` Mood: ${getMoodText()}\n\n` +
|
|
1617
|
-
` /pack <name> /intensity <pro|immersive> /emoji [on|off]\n` +
|
|
1618
|
-
` /skin <name> /palette <name> /companion <name>`);
|
|
1619
|
-
break;
|
|
1620
|
-
}
|
|
1621
|
-
case '/pack': {
|
|
1622
|
-
const subCmd = parts[1];
|
|
1623
|
-
if (!subCmd) {
|
|
1624
|
-
ctx.setModalMode('pack-picker');
|
|
1625
|
-
break;
|
|
1626
|
-
}
|
|
1627
|
-
if (subCmd === 'list') {
|
|
1628
|
-
const category = parts[2];
|
|
1629
|
-
const packs = listThemePacks(category || undefined);
|
|
1630
|
-
const currentP = getCurrentPack();
|
|
1631
|
-
// Group by category
|
|
1632
|
-
const grouped = new Map();
|
|
1633
|
-
for (const p of packs) {
|
|
1634
|
-
const group = grouped.get(p.category) || [];
|
|
1635
|
-
group.push(p);
|
|
1636
|
-
grouped.set(p.category, group);
|
|
1637
|
-
}
|
|
1638
|
-
let output = 'Theme Packs:\n';
|
|
1639
|
-
for (const [cat, catPacks] of grouped) {
|
|
1640
|
-
output += `\n [${cat}]\n`;
|
|
1641
|
-
for (const p of catPacks) {
|
|
1642
|
-
const marker = currentP && p.name === currentP.name ? ' *' : '';
|
|
1643
|
-
output += ` ${p.name}${marker} — ${p.description}\n`;
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
output += '\nUse: /pack <name>';
|
|
1647
|
-
ctx.addMessage('system', output);
|
|
1648
|
-
}
|
|
1649
|
-
else {
|
|
1650
|
-
// Run theme transition animation before applying
|
|
1651
|
-
const targetPack = getThemePack(subCmd);
|
|
1652
|
-
if (targetPack?.skin.splash?.transition) {
|
|
1653
|
-
await renderTransition(targetPack.skin.splash.transition);
|
|
1654
|
-
}
|
|
1655
|
-
const success = applyThemePack(subCmd, getCompanionMode());
|
|
1656
|
-
if (success) {
|
|
1657
|
-
const pack = getCurrentPack();
|
|
1658
|
-
config.set('activeThemePack', subCmd);
|
|
1659
|
-
config.set('activeSkin', pack.skin.name);
|
|
1660
|
-
config.set('activePalette', pack.palette.name);
|
|
1661
|
-
const companion = getCompanionMode() === 'professional'
|
|
1662
|
-
? pack.companions.professional
|
|
1663
|
-
: pack.companions.immersive;
|
|
1664
|
-
config.set('activeCompanion', companion.name);
|
|
1665
|
-
// Reset LLM system prompt to use the companion's persona
|
|
1666
|
-
ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
|
|
1667
|
-
ctx.addMessage('system', `Theme pack: ${subCmd}\n` +
|
|
1668
|
-
` Skin: ${pack.skin.name}, Palette: ${pack.palette.name}, Companion: ${companion.name}\n` +
|
|
1669
|
-
` "${companion.greeting}"`);
|
|
1670
|
-
}
|
|
1671
|
-
else {
|
|
1672
|
-
ctx.addMessage('error', `Theme pack not found: ${subCmd}. Use /pack list to see available packs.`);
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
break;
|
|
1676
|
-
}
|
|
1677
|
-
case '/intensity': {
|
|
1678
|
-
const intensity = parts[1];
|
|
1679
|
-
if (intensity === 'professional' || intensity === 'pro') {
|
|
1680
|
-
const success = setCompanionMode('professional');
|
|
1681
|
-
if (success) {
|
|
1682
|
-
const pack = getCurrentPack();
|
|
1683
|
-
config.set('companionIntensity', 'professional');
|
|
1684
|
-
config.set('activeCompanion', pack.companions.professional.name);
|
|
1685
|
-
ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
|
|
1686
|
-
ctx.addMessage('system', `Switched to professional mode — ${pack.companions.professional.description}`);
|
|
1687
|
-
}
|
|
1688
|
-
else {
|
|
1689
|
-
ctx.addMessage('error', 'No theme pack active. Use /pack <name> first.');
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
else if (intensity === 'immersive' || intensity === 'imm') {
|
|
1693
|
-
const success = setCompanionMode('immersive');
|
|
1694
|
-
if (success) {
|
|
1695
|
-
const pack = getCurrentPack();
|
|
1696
|
-
config.set('companionIntensity', 'immersive');
|
|
1697
|
-
config.set('activeCompanion', pack.companions.immersive.name);
|
|
1698
|
-
ctx.llmMessages.current = [{ role: 'system', content: buildFullSystemPrompt(ctx.persona, getActiveProjectDir(ctx)) }];
|
|
1699
|
-
ctx.addMessage('system', `Switched to immersive mode — ${pack.companions.immersive.description}`);
|
|
1700
|
-
}
|
|
1701
|
-
else {
|
|
1702
|
-
ctx.addMessage('error', 'No theme pack active. Use /pack <name> first.');
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
else {
|
|
1706
|
-
const currentIntensity = getCompanionMode();
|
|
1707
|
-
ctx.addMessage('system', `Intensity: ${currentIntensity}\nOptions: /intensity professional (pro), /intensity immersive (imm)`);
|
|
1708
|
-
}
|
|
1709
|
-
break;
|
|
1710
|
-
}
|
|
1711
|
-
case '/emoji': {
|
|
1712
|
-
const emojiArg = parts[1];
|
|
1713
|
-
const current = config.get('useEmojis') !== false;
|
|
1714
|
-
if (emojiArg === 'on') {
|
|
1715
|
-
config.set('useEmojis', true);
|
|
1716
|
-
ctx.addMessage('system', '\u2713 Emojis enabled');
|
|
1717
|
-
}
|
|
1718
|
-
else if (emojiArg === 'off') {
|
|
1719
|
-
config.set('useEmojis', false);
|
|
1720
|
-
ctx.addMessage('system', '\u2713 Emojis disabled — text fallbacks will be used');
|
|
1721
|
-
}
|
|
1722
|
-
else if (emojiArg === 'toggle') {
|
|
1723
|
-
config.set('useEmojis', !current);
|
|
1724
|
-
ctx.addMessage('system', `\u2713 Emojis ${!current ? 'enabled' : 'disabled'}`);
|
|
1725
|
-
}
|
|
1726
|
-
else {
|
|
1727
|
-
ctx.addMessage('system', `Emojis: ${current ? 'ON' : 'OFF'}\nUsage: /emoji [on|off|toggle]`);
|
|
1728
|
-
}
|
|
1729
|
-
break;
|
|
1730
|
-
}
|
|
1731
|
-
case '/hooks': {
|
|
1732
|
-
const hooksModule = await import('../hooks.js');
|
|
1733
|
-
const subCmd = parts[1];
|
|
1734
|
-
if (subCmd === 'list' || !subCmd) {
|
|
1735
|
-
ctx.addMessage('system', hooksModule.listHooksFormatted());
|
|
1736
|
-
}
|
|
1737
|
-
else if (subCmd === 'add' && parts[2]) {
|
|
1738
|
-
const event = parts[2];
|
|
1739
|
-
const hookCommand = parts.slice(3).join(' ');
|
|
1740
|
-
if (!hookCommand) {
|
|
1741
|
-
ctx.addMessage('system', 'Usage: /hooks add <event> <command>');
|
|
1742
|
-
}
|
|
1743
|
-
else {
|
|
1744
|
-
hooksModule.addHook({ event, name: `Hook for ${event}`, command: hookCommand, enabled: true, async: false });
|
|
1745
|
-
ctx.addMessage('system', 'Hook added');
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
else if (subCmd === 'init') {
|
|
1749
|
-
hooksModule.initDefaultHooks();
|
|
1750
|
-
ctx.addMessage('system', 'Default hooks initialized');
|
|
1751
|
-
}
|
|
1752
|
-
else {
|
|
1753
|
-
ctx.addMessage('system', 'Usage: /hooks [list|add <event> <command>|init]');
|
|
1754
|
-
}
|
|
1755
|
-
break;
|
|
1756
|
-
}
|
|
1757
|
-
case '/search': {
|
|
1758
|
-
const query = parts.slice(1).join(' ');
|
|
1759
|
-
if (!query) {
|
|
1760
|
-
ctx.addMessage('system', 'Usage: /search <query>\nSearch conversation history');
|
|
1761
|
-
}
|
|
1762
|
-
else {
|
|
1763
|
-
const lower = query.toLowerCase();
|
|
1764
|
-
const matches = ctx.messages.filter(m => m.content.toLowerCase().includes(lower));
|
|
1765
|
-
if (matches.length === 0) {
|
|
1766
|
-
ctx.addMessage('system', 'No matches found');
|
|
1767
|
-
}
|
|
1768
|
-
else {
|
|
1769
|
-
const results = matches.slice(-10).map(m => {
|
|
1770
|
-
const preview = m.content.slice(0, 100).replace(/\n/g, ' ');
|
|
1771
|
-
return `[${m.type}] ${preview}...`;
|
|
1772
|
-
}).join('\n\n');
|
|
1773
|
-
ctx.addMessage('system', `Found ${matches.length} matches:\n\n${results}`);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
break;
|
|
1777
|
-
}
|
|
1778
|
-
case '/project': {
|
|
1779
|
-
const projectConfig = await import('../project-config.js');
|
|
1780
|
-
const subCmd = parts[1];
|
|
1781
|
-
const cwd = process.cwd();
|
|
1782
|
-
if (subCmd === 'init') {
|
|
1783
|
-
const configPath = projectConfig.createProjectConfig(cwd);
|
|
1784
|
-
ctx.addMessage('system', `Created project config: ${configPath}\nEdit the file to customize settings.`);
|
|
1785
|
-
}
|
|
1786
|
-
else if (subCmd === 'show' || !subCmd) {
|
|
1787
|
-
const configPath = projectConfig.findProjectConfig(cwd);
|
|
1788
|
-
if (!configPath) {
|
|
1789
|
-
ctx.addMessage('system', 'No project config found.\nRun /project init to create one.');
|
|
1790
|
-
}
|
|
1791
|
-
else {
|
|
1792
|
-
const cfg = projectConfig.loadProjectConfig(configPath);
|
|
1793
|
-
if (cfg) {
|
|
1794
|
-
let info = `Config: ${configPath}\n\n`;
|
|
1795
|
-
if (cfg.project)
|
|
1796
|
-
info += `Project: ${cfg.project}\n`;
|
|
1797
|
-
if (cfg.provider)
|
|
1798
|
-
info += `Provider: ${cfg.provider}\n`;
|
|
1799
|
-
if (cfg.model)
|
|
1800
|
-
info += `Model: ${cfg.model}\n`;
|
|
1801
|
-
if (cfg.tech?.length)
|
|
1802
|
-
info += `Tech: ${cfg.tech.join(', ')}\n`;
|
|
1803
|
-
if (cfg.conventions?.length)
|
|
1804
|
-
info += `\nConventions:\n${cfg.conventions.map((c) => ` - ${c}`).join('\n')}\n`;
|
|
1805
|
-
if (cfg.commands)
|
|
1806
|
-
info += `\nCommands: ${Object.keys(cfg.commands).join(', ')}\n`;
|
|
1807
|
-
ctx.addMessage('system', info);
|
|
1808
|
-
}
|
|
1809
|
-
else {
|
|
1810
|
-
ctx.addMessage('error', 'Failed to parse config');
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
else if (subCmd === 'run' && parts[2]) {
|
|
1815
|
-
const configPath = projectConfig.findProjectConfig(cwd);
|
|
1816
|
-
const cfg = configPath ? projectConfig.loadProjectConfig(configPath) : null;
|
|
1817
|
-
const cmdName = parts[2];
|
|
1818
|
-
if (cfg?.commands?.[cmdName]) {
|
|
1819
|
-
const commandToRun = cfg.commands[cmdName];
|
|
1820
|
-
// Show the command and source for user awareness
|
|
1821
|
-
ctx.addMessage('system', `Project command "${cmdName}" from ${configPath}:\n` +
|
|
1822
|
-
` $ ${commandToRun}\n\n` +
|
|
1823
|
-
`Type "/project run-confirm ${cmdName}" to execute, or review the .calliope config first.`);
|
|
1824
|
-
}
|
|
1825
|
-
else {
|
|
1826
|
-
ctx.addMessage('error', `Command not found: ${cmdName}`);
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
else if (subCmd === 'run-confirm' && parts[2]) {
|
|
1830
|
-
const configPath = projectConfig.findProjectConfig(cwd);
|
|
1831
|
-
const cfg = configPath ? projectConfig.loadProjectConfig(configPath) : null;
|
|
1832
|
-
const cmdName = parts[2];
|
|
1833
|
-
if (cfg?.commands?.[cmdName]) {
|
|
1834
|
-
const commandToRun = cfg.commands[cmdName];
|
|
1835
|
-
ctx.addMessage('system', `Running: ${commandToRun}`);
|
|
1836
|
-
const { spawn } = await import('child_process');
|
|
1837
|
-
const proc = spawn('sh', ['-c', commandToRun], { cwd, stdio: 'pipe' });
|
|
1838
|
-
let output = '';
|
|
1839
|
-
proc.stdout?.on('data', (d) => output += d.toString());
|
|
1840
|
-
proc.stderr?.on('data', (d) => output += d.toString());
|
|
1841
|
-
proc.on('close', (code) => {
|
|
1842
|
-
ctx.addMessage('system', `Exit ${code}\n${output}`);
|
|
1843
|
-
});
|
|
1844
|
-
}
|
|
1845
|
-
else {
|
|
1846
|
-
ctx.addMessage('error', `Command not found: ${cmdName}`);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
else {
|
|
1850
|
-
ctx.addMessage('system', 'Usage: /project [init|show|run <cmd>|run-confirm <cmd>]');
|
|
1851
|
-
}
|
|
1852
|
-
break;
|
|
1853
|
-
}
|
|
1854
|
-
case '/route':
|
|
1855
|
-
case '/autoroute': {
|
|
1856
|
-
if (parts[1] === 'on') {
|
|
1857
|
-
ctx.setAutoRoute(true);
|
|
1858
|
-
ctx.addMessage('system', '\u2713 Auto-routing ON - model selected based on task complexity');
|
|
1859
|
-
}
|
|
1860
|
-
else if (parts[1] === 'off') {
|
|
1861
|
-
ctx.setAutoRoute(false);
|
|
1862
|
-
ctx.addMessage('system', '\u2713 Auto-routing OFF - using fixed model');
|
|
1863
|
-
}
|
|
1864
|
-
else if (parts[1] === 'test' && parts[2]) {
|
|
1865
|
-
const testMsg = parts.slice(2).join(' ');
|
|
1866
|
-
const decision = modelRouter.routeRequest(testMsg, ctx.actualProvider);
|
|
1867
|
-
ctx.addMessage('system', `Route test: ${decision.tier} tier (${decision.complexity})\nModel: ${decision.model.model}\nReason: ${decision.reason}\nConfidence: ${Math.round(decision.confidence * 100)}%`);
|
|
1868
|
-
}
|
|
1869
|
-
else {
|
|
1870
|
-
const tiers = modelRouter.getAllTiers(ctx.actualProvider);
|
|
1871
|
-
ctx.addMessage('system', `Auto-route: ${ctx.autoRoute ? 'ON' : 'OFF'}\n\nModel tiers for ${ctx.actualProvider}:\n fast: ${tiers.fast.model}\n balanced: ${tiers.balanced.model}\n smart: ${tiers.smart.model}\n\nUsage: /route [on|off|test <message>]`);
|
|
1872
|
-
}
|
|
1873
|
-
break;
|
|
1874
|
-
}
|
|
1875
|
-
case '/summarize': {
|
|
1876
|
-
const subCmd = parts[1];
|
|
1877
|
-
if (subCmd === 'context' || !subCmd) {
|
|
1878
|
-
const msgCount = ctx.llmMessages.current.length;
|
|
1879
|
-
if (msgCount < 5) {
|
|
1880
|
-
ctx.addMessage('system', 'Not enough messages to summarize.');
|
|
1881
|
-
}
|
|
1882
|
-
else {
|
|
1883
|
-
const summary = summarization.extractKeyInfo(ctx.llmMessages.current);
|
|
1884
|
-
let info = 'Context Summary:\n\n';
|
|
1885
|
-
if (summary.topics.length)
|
|
1886
|
-
info += `**Topics:** ${summary.topics.join(', ')}\n`;
|
|
1887
|
-
if (summary.decisions.length)
|
|
1888
|
-
info += `**Decisions:**\n${summary.decisions.map((d) => ` - ${d}`).join('\n')}\n`;
|
|
1889
|
-
if (summary.actions.length)
|
|
1890
|
-
info += `**Actions:**\n${summary.actions.map((a) => ` - ${a}`).join('\n')}\n`;
|
|
1891
|
-
if (summary.codeChanges.length)
|
|
1892
|
-
info += `**Code Changes:**\n${summary.codeChanges.slice(0, 5).map((c) => ` - ${c}`).join('\n')}\n`;
|
|
1893
|
-
ctx.addMessage('system', info || 'No key information extracted.');
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
else if (subCmd === 'compact') {
|
|
1897
|
-
// Summarize and compact the conversation
|
|
1898
|
-
const result = summarization.summarizeConversation(ctx.llmMessages.current, { maxTokens: 50000 });
|
|
1899
|
-
if (result.summarizedCount > 0) {
|
|
1900
|
-
ctx.llmMessages.current = result.messages;
|
|
1901
|
-
ctx.setContextTokens(ctx.estimateContextTokens());
|
|
1902
|
-
ctx.addMessage('system', `\u2713 Compacted ${result.summarizedCount} messages (${result.originalTokens} \u2192 ${result.reducedTokens} tokens)`);
|
|
1903
|
-
}
|
|
1904
|
-
else {
|
|
1905
|
-
ctx.addMessage('system', 'Context already within limits, no compaction needed.');
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
else {
|
|
1909
|
-
ctx.addMessage('system', 'Usage: /summarize [context|compact]');
|
|
1910
|
-
}
|
|
1911
|
-
break;
|
|
1912
|
-
}
|
|
1913
|
-
case '/upgrade':
|
|
1914
|
-
ctx.addMessage('system', 'Checking for updates...');
|
|
1915
|
-
try {
|
|
1916
|
-
const current = getVersion();
|
|
1917
|
-
const latest = await getLatestVersion();
|
|
1918
|
-
if (!latest) {
|
|
1919
|
-
ctx.addMessage('error', 'Could not check for updates');
|
|
1920
|
-
break;
|
|
1921
|
-
}
|
|
1922
|
-
const [cMaj, cMin, cPat] = current.split('.').map(Number);
|
|
1923
|
-
const [lMaj, lMin, lPat] = latest.split('.').map(Number);
|
|
1924
|
-
const hasUpdate = lMaj > cMaj || (lMaj === cMaj && lMin > cMin) || (lMaj === cMaj && lMin === cMin && lPat > cPat);
|
|
1925
|
-
if (hasUpdate) {
|
|
1926
|
-
ctx.setLatestVersion(latest);
|
|
1927
|
-
ctx.setModalMode('upgrade');
|
|
1928
|
-
}
|
|
1929
|
-
else {
|
|
1930
|
-
ctx.addMessage('system', `You're on the latest version (v${current})`);
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
catch (e) {
|
|
1934
|
-
ctx.addMessage('error', `Failed to check for updates: ${e instanceof Error ? e.message : String(e)}`);
|
|
1935
|
-
}
|
|
1936
|
-
break;
|
|
1937
|
-
case '/session':
|
|
1938
|
-
case '/sessions':
|
|
1939
|
-
if (parts[1] === 'list' || !parts[1]) {
|
|
1940
|
-
const sessions = storage.listSessions(20);
|
|
1941
|
-
if (sessions.length === 0) {
|
|
1942
|
-
ctx.addMessage('system', 'No previous sessions found.');
|
|
1943
|
-
}
|
|
1944
|
-
else {
|
|
1945
|
-
ctx.setAvailableSessions(sessions);
|
|
1946
|
-
ctx.setModalMode('sessions');
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
else if (parts[1] === 'info') {
|
|
1950
|
-
const session = ctx.sessionRef.current;
|
|
1951
|
-
if (session) {
|
|
1952
|
-
const savedMessages = storage.loadMessageHistory();
|
|
1953
|
-
const savedCount = savedMessages ? savedMessages.length : 0;
|
|
1954
|
-
const ledgerTotals = ctx.ledger?.getTotals();
|
|
1955
|
-
const latestRun = ctx.ledger?.getLatestRun();
|
|
1956
|
-
const lines = [
|
|
1957
|
-
`Session: ${session.projectName}`,
|
|
1958
|
-
`ID: ${session.id}`,
|
|
1959
|
-
`Created: ${new Date(session.createdAt).toLocaleString()}`,
|
|
1960
|
-
`Messages: ${session.messageCount}`,
|
|
1961
|
-
`Saved LLM messages: ${savedCount}`,
|
|
1962
|
-
];
|
|
1963
|
-
if (ledgerTotals) {
|
|
1964
|
-
lines.push(`Iterations logged: ${ledgerTotals.iterations}`, `Failed approaches: ${ctx.ledger?.getFailedApproachCount() ?? 0}`);
|
|
1965
|
-
}
|
|
1966
|
-
if (latestRun) {
|
|
1967
|
-
lines.push(`Latest run: ${formatLedgerRun(latestRun)}`);
|
|
1968
|
-
}
|
|
1969
|
-
ctx.addMessage('system', lines.join('\n'));
|
|
1970
|
-
}
|
|
1971
|
-
else {
|
|
1972
|
-
ctx.addMessage('system', 'No active session.');
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
else if (parts[1] === 'fork') {
|
|
1976
|
-
const session = ctx.sessionRef.current;
|
|
1977
|
-
if (!session) {
|
|
1978
|
-
ctx.addMessage('error', 'No active session to fork.');
|
|
1979
|
-
}
|
|
1980
|
-
else {
|
|
1981
|
-
// Save current messages before forking
|
|
1982
|
-
storage.saveMessageHistory(ctx.llmMessages.current);
|
|
1983
|
-
if (ctx.ledger) {
|
|
1984
|
-
storage.saveIterationLedger(ctx.ledger);
|
|
1985
|
-
}
|
|
1986
|
-
const forked = storage.forkSession(session.projectPath);
|
|
1987
|
-
if (forked) {
|
|
1988
|
-
ctx.sessionRef.current = forked;
|
|
1989
|
-
ctx.addMessage('system', `Forked session: ${forked.id}\nMessages carried over: ${ctx.llmMessages.current.length}\n\nYou are now on the forked session. The original session is preserved.`);
|
|
1990
|
-
}
|
|
1991
|
-
else {
|
|
1992
|
-
ctx.addMessage('error', 'Failed to fork session. No saved messages found.');
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
else if (parts[1] === 'save') {
|
|
1997
|
-
storage.saveMessageHistory(ctx.llmMessages.current);
|
|
1998
|
-
if (ctx.ledger) {
|
|
1999
|
-
storage.saveIterationLedger(ctx.ledger);
|
|
2000
|
-
}
|
|
2001
|
-
ctx.addMessage('system', `Saved ${ctx.llmMessages.current.length} LLM messages and current log state to session.`);
|
|
2002
|
-
}
|
|
2003
|
-
else {
|
|
2004
|
-
ctx.addMessage('system', 'Usage: /session [list|info|fork|save] or just /sessions');
|
|
2005
|
-
}
|
|
2006
|
-
break;
|
|
2007
|
-
case '/log': {
|
|
2008
|
-
if (!ctx.ledger) {
|
|
2009
|
-
ctx.addMessage('system', 'No session log available.');
|
|
2010
|
-
break;
|
|
2011
|
-
}
|
|
2012
|
-
const subCmd = parts[1] || 'summary';
|
|
2013
|
-
if (subCmd === 'summary') {
|
|
2014
|
-
const totals = ctx.ledger.getTotals();
|
|
2015
|
-
const runs = ctx.ledger.getRuns(5);
|
|
2016
|
-
const allFailures = ctx.ledger.getFailedApproaches();
|
|
2017
|
-
const failures = allFailures.slice(-5);
|
|
2018
|
-
const lines = [
|
|
2019
|
-
'Session Log',
|
|
2020
|
-
`Iterations: ${totals.iterations}`,
|
|
2021
|
-
`Failed approaches: ${ctx.ledger.getFailedApproachCount()}`,
|
|
2022
|
-
`Tokens: ${totals.totalTokens}`,
|
|
2023
|
-
`Cost: $${totals.totalCost.toFixed(4)}`,
|
|
2024
|
-
`Duration: ${formatLedgerDuration(totals.totalDurationMs)}`,
|
|
2025
|
-
];
|
|
2026
|
-
if (runs.length > 0) {
|
|
2027
|
-
lines.push('', 'Recent runs:');
|
|
2028
|
-
for (const run of runs) {
|
|
2029
|
-
lines.push(` - ${formatLedgerRun(run)}`);
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
if (failures.length > 0) {
|
|
2033
|
-
lines.push('', 'Recent failures:');
|
|
2034
|
-
for (const failure of failures) {
|
|
2035
|
-
lines.push(` - #${failure.iteration} ${failure.description} — ${failure.reason}`);
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
ctx.addMessage('system', lines.join('\n'));
|
|
2039
|
-
}
|
|
2040
|
-
else if (subCmd === 'tail') {
|
|
2041
|
-
const limit = parts[2] ? parseInt(parts[2], 10) : 10;
|
|
2042
|
-
if (isNaN(limit) || limit <= 0 || limit > 100) {
|
|
2043
|
-
ctx.addMessage('error', 'Usage: /log tail [1-100]');
|
|
2044
|
-
break;
|
|
2045
|
-
}
|
|
2046
|
-
const entries = ctx.ledger.getEntries().slice(-limit);
|
|
2047
|
-
if (entries.length === 0) {
|
|
2048
|
-
ctx.addMessage('system', 'No logged iterations yet.');
|
|
2049
|
-
break;
|
|
2050
|
-
}
|
|
2051
|
-
const lines = ['Recent iterations:'];
|
|
2052
|
-
for (const entry of entries) {
|
|
2053
|
-
const actions = entry.actions.length > 0
|
|
2054
|
-
? entry.actions.map(action => `${action.tool}(${action.args})${action.result === 'error' ? ' FAILED' : action.result === 'blocked' ? ' BLOCKED' : ''}`).join(', ')
|
|
2055
|
-
: 'no tool actions';
|
|
2056
|
-
lines.push(` #${entry.iteration} [${entry.outcome}] ${formatLedgerDuration(entry.durationMs)} — ${actions}`);
|
|
2057
|
-
}
|
|
2058
|
-
ctx.addMessage('system', lines.join('\n'));
|
|
2059
|
-
}
|
|
2060
|
-
else if (subCmd === 'failures') {
|
|
2061
|
-
const failures = ctx.ledger.getFailedApproaches();
|
|
2062
|
-
if (failures.length === 0) {
|
|
2063
|
-
ctx.addMessage('system', 'No failed approaches recorded.');
|
|
2064
|
-
break;
|
|
2065
|
-
}
|
|
2066
|
-
const lines = ['Failed approaches:'];
|
|
2067
|
-
for (const failure of failures.slice(-10)) {
|
|
2068
|
-
lines.push(` - #${failure.iteration} ${failure.description} — ${failure.reason}`);
|
|
2069
|
-
}
|
|
2070
|
-
ctx.addMessage('system', lines.join('\n'));
|
|
2071
|
-
}
|
|
2072
|
-
else if (subCmd === 'reset') {
|
|
2073
|
-
ctx.ledger.reset();
|
|
2074
|
-
ctx.addMessage('system', 'Session log reset.');
|
|
2075
|
-
}
|
|
2076
|
-
else {
|
|
2077
|
-
ctx.addMessage('system', 'Usage: /log [summary|tail [N]|failures|reset]');
|
|
2078
|
-
}
|
|
2079
|
-
break;
|
|
2080
|
-
}
|
|
2081
|
-
case '/todo': {
|
|
2082
|
-
const subCommand = parts[1];
|
|
2083
|
-
if (subCommand === 'add' && parts.length > 2) {
|
|
2084
|
-
const content = parts.slice(2).join(' ');
|
|
2085
|
-
const isGlobal = content.includes('--global');
|
|
2086
|
-
const isHigh = content.includes('--priority') && content.includes('high');
|
|
2087
|
-
const cleanContent = content.replace(/--global|--priority\s*\w+/g, '').trim();
|
|
2088
|
-
const todo = storage.addTodo(cleanContent, {
|
|
2089
|
-
global: isGlobal,
|
|
2090
|
-
priority: isHigh ? 'high' : 'normal',
|
|
2091
|
-
});
|
|
2092
|
-
ctx.addMessage('system', `\u2713 TODO added (#${todo.id.slice(-4)}${isGlobal ? ', global' : ''})`);
|
|
2093
|
-
}
|
|
2094
|
-
else if (subCommand === 'done' && parts[2]) {
|
|
2095
|
-
const id = parts[2];
|
|
2096
|
-
const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
|
|
2097
|
-
const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
|
|
2098
|
-
if (todo) {
|
|
2099
|
-
storage.updateTodo(todo.id, { status: 'completed' });
|
|
2100
|
-
ctx.addMessage('system', `\u2713 TODO #${id} marked done`);
|
|
2101
|
-
}
|
|
2102
|
-
else {
|
|
2103
|
-
ctx.addMessage('error', `TODO #${id} not found`);
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
else if (subCommand === 'list' || !subCommand) {
|
|
2107
|
-
const sessionTodos = storage.getSessionTodos();
|
|
2108
|
-
const globalTodos = storage.getGlobalTodos();
|
|
2109
|
-
const pending = [...sessionTodos, ...globalTodos].filter(t => t.status !== 'completed');
|
|
2110
|
-
const completed = [...sessionTodos, ...globalTodos].filter(t => t.status === 'completed').slice(-3);
|
|
2111
|
-
if (pending.length === 0 && completed.length === 0) {
|
|
2112
|
-
ctx.addMessage('system', 'No TODOs. Use /todo add <task> to create one.');
|
|
2113
|
-
}
|
|
2114
|
-
else {
|
|
2115
|
-
let output = '\u{1F4CB} TODOs:\n';
|
|
2116
|
-
if (pending.length > 0) {
|
|
2117
|
-
output += pending.map(t => ` ${t.priority === 'high' ? '!' : '\u25A1'} #${t.id.slice(-4)} ${t.content}`).join('\n');
|
|
2118
|
-
}
|
|
2119
|
-
if (completed.length > 0) {
|
|
2120
|
-
output += '\n\nCompleted:\n' + completed.map(t => ` \u2713 #${t.id.slice(-4)} ${t.content}`).join('\n');
|
|
2121
|
-
}
|
|
2122
|
-
ctx.addMessage('system', output);
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
else if (subCommand === 'work' && parts[2]) {
|
|
2126
|
-
const id = parts[2];
|
|
2127
|
-
const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
|
|
2128
|
-
const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
|
|
2129
|
-
if (todo) {
|
|
2130
|
-
storage.setActiveTodo(todo.id);
|
|
2131
|
-
storage.updateTodo(todo.id, { status: 'in_progress' });
|
|
2132
|
-
ctx.addMessage('system', `\u2713 Working on: ${todo.content}\n\nTip: I'll help you complete this task. Describe what you need.`);
|
|
2133
|
-
}
|
|
2134
|
-
else {
|
|
2135
|
-
ctx.addMessage('error', `TODO #${id} not found`);
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
else if (subCommand === 'clear') {
|
|
2139
|
-
storage.setActiveTodo(null);
|
|
2140
|
-
ctx.addMessage('system', '\u2713 Active TODO cleared');
|
|
2141
|
-
}
|
|
2142
|
-
else {
|
|
2143
|
-
ctx.addMessage('system', 'Usage: /todo [add <task>|done <id>|work <id>|clear|list]');
|
|
2144
|
-
}
|
|
2145
|
-
break;
|
|
2146
|
-
}
|
|
2147
|
-
case '/plans': {
|
|
2148
|
-
const subCommand = parts[1];
|
|
2149
|
-
if (subCommand === 'list' || !subCommand) {
|
|
2150
|
-
const plans = storage.getPlans();
|
|
2151
|
-
if (plans.length === 0) {
|
|
2152
|
-
ctx.addMessage('system', 'No plans yet. Plans are created in hybrid mode.');
|
|
2153
|
-
}
|
|
2154
|
-
else {
|
|
2155
|
-
const list = plans.slice(0, 5).map((p) => `${p.status === 'completed' ? '\u2713' : '\u25CB'} ${p.id.slice(-4)}: ${p.title}`).join('\n');
|
|
2156
|
-
ctx.addMessage('system', `\u{1F4CB} Plans:\n${list}`);
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
else if (subCommand === 'view' && parts[2]) {
|
|
2160
|
-
const plans = storage.getPlans();
|
|
2161
|
-
const plan = plans.find((p) => p.id.endsWith(parts[2]) || p.id === parts[2]);
|
|
2162
|
-
if (plan) {
|
|
2163
|
-
const phases = plan.phases.map((ph) => ` ${ph.status === 'completed' ? '\u2713' : '\u25CB'} ${ph.name} (${ph.risk} risk)`).join('\n');
|
|
2164
|
-
ctx.addMessage('system', `Plan: ${plan.title}\nStatus: ${plan.status}\n\nPhases:\n${phases}`);
|
|
2165
|
-
}
|
|
2166
|
-
else {
|
|
2167
|
-
ctx.addMessage('error', `Plan #${parts[2]} not found`);
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
else if (subCommand === 'rerun' && parts[2]) {
|
|
2171
|
-
const plans = storage.getPlans();
|
|
2172
|
-
const plan = plans.find((p) => p.id.endsWith(parts[2]) || p.id === parts[2]);
|
|
2173
|
-
if (plan) {
|
|
2174
|
-
// Reset plan status and activate
|
|
2175
|
-
plan.status = 'in_progress';
|
|
2176
|
-
plan.phases.forEach((ph) => ph.status = 'pending');
|
|
2177
|
-
storage.savePlan(plan);
|
|
2178
|
-
storage.setActivePlan(plan);
|
|
2179
|
-
// Generate prompt for re-execution
|
|
2180
|
-
const phaseList = plan.phases.map((ph) => `- ${ph.name}`).join('\n');
|
|
2181
|
-
const prompt = `Please help me execute this plan:\n\n**${plan.title}**\n\nPhases:\n${phaseList}\n\nStart with the first phase.`;
|
|
2182
|
-
ctx.setInput(prompt);
|
|
2183
|
-
ctx.addMessage('system', `\u2713 Plan loaded: ${plan.title}\nPress Enter to start execution.`);
|
|
2184
|
-
}
|
|
2185
|
-
else {
|
|
2186
|
-
ctx.addMessage('error', `Plan #${parts[2]} not found`);
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
else {
|
|
2190
|
-
ctx.addMessage('system', 'Usage: /plans [list|view <id>|rerun <id>]');
|
|
2191
|
-
}
|
|
2192
|
-
break;
|
|
2193
|
-
}
|
|
2194
|
-
case '/history': {
|
|
2195
|
-
const subCommand = parts[1];
|
|
2196
|
-
if (subCommand === 'search' && parts[2]) {
|
|
2197
|
-
const query = parts.slice(2).join(' ');
|
|
2198
|
-
const results = storage.searchChatHistory(query);
|
|
2199
|
-
if (results.length === 0) {
|
|
2200
|
-
ctx.addMessage('system', `No matches for "${query}"`);
|
|
2201
|
-
}
|
|
2202
|
-
else {
|
|
2203
|
-
const list = results.slice(-5).map((m) => `${new Date(m.timestamp).toLocaleTimeString()}: ${m.content.substring(0, 60)}...`).join('\n');
|
|
2204
|
-
ctx.addMessage('system', `\u{1F50D} Found ${results.length} matches:\n${list}`);
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
else if (subCommand === 'clear') {
|
|
2208
|
-
ctx.addMessage('system', 'History is preserved per session. Start a new session for fresh history.');
|
|
2209
|
-
}
|
|
2210
|
-
else {
|
|
2211
|
-
const history = storage.getChatHistory(5);
|
|
2212
|
-
if (history.length === 0) {
|
|
2213
|
-
ctx.addMessage('system', 'No chat history yet.');
|
|
2214
|
-
}
|
|
2215
|
-
else {
|
|
2216
|
-
const list = history.map((m) => `${m.role}: ${m.content.substring(0, 50)}...`).join('\n');
|
|
2217
|
-
ctx.addMessage('system', `Recent history:\n${list}\n\nUse /history search <query> to search.`);
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
break;
|
|
2221
|
-
}
|
|
2222
|
-
case '/context': {
|
|
2223
|
-
const subCommand = parts[1];
|
|
2224
|
-
if (subCommand === 'load') {
|
|
2225
|
-
const limit = parseInt(parts[2]) || 20;
|
|
2226
|
-
const history = storage.getChatHistory(limit);
|
|
2227
|
-
if (history.length > 0) {
|
|
2228
|
-
// Load history into LLM context
|
|
2229
|
-
for (const msg of history) {
|
|
2230
|
-
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
2231
|
-
ctx.llmMessages.current.push({
|
|
2232
|
-
role: msg.role,
|
|
2233
|
-
content: msg.content,
|
|
2234
|
-
});
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
ctx.addMessage('system', `\u2713 Loaded ${history.length} messages into context`);
|
|
2238
|
-
}
|
|
2239
|
-
else {
|
|
2240
|
-
ctx.addMessage('system', 'No history to load.');
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
else if (subCommand === 'summary' || !subCommand) {
|
|
2244
|
-
// Enhanced context summary with model limits
|
|
2245
|
-
const msgCount = ctx.llmMessages.current.length;
|
|
2246
|
-
const estTokens = ctx.estimateContextTokens();
|
|
2247
|
-
const modelLimit = getModelContextLimit(ctx.actualProvider, ctx.actualModel);
|
|
2248
|
-
const percentage = Math.round((estTokens / modelLimit) * 100);
|
|
2249
|
-
const formatK = (n) => n >= 1000 ? `${Math.round(n / 1000)}K` : String(n);
|
|
2250
|
-
let status = '\u{1F7E2} Healthy';
|
|
2251
|
-
if (percentage > 90)
|
|
2252
|
-
status = '\u{1F534} Critical';
|
|
2253
|
-
else if (percentage > 80)
|
|
2254
|
-
status = '\u{1F7E1} Warning';
|
|
2255
|
-
else if (percentage > 60)
|
|
2256
|
-
status = '\u{1F7E0} Caution';
|
|
2257
|
-
ctx.addMessage('system', `**Context Status: ${status}**
|
|
2258
|
-
|
|
2259
|
-
**Usage:** ${formatK(estTokens)} / ${formatK(modelLimit)} tokens (${percentage}%)
|
|
2260
|
-
**Messages:** ${msgCount}
|
|
2261
|
-
**Provider:** ${ctx.actualProvider}
|
|
2262
|
-
**Model:** ${ctx.actualModel}
|
|
2263
|
-
|
|
2264
|
-
**Commands:**
|
|
2265
|
-
/summarize compact - Auto-compress context
|
|
2266
|
-
/context load [n] - Load n messages from history
|
|
2267
|
-
/clear - Start fresh`);
|
|
2268
|
-
}
|
|
2269
|
-
else {
|
|
2270
|
-
ctx.addMessage('system', 'Usage: /context [load [n]|summary]\n\nShow context status or load history.');
|
|
2271
|
-
}
|
|
2272
|
-
break;
|
|
2273
|
-
}
|
|
2274
|
-
case '/scope':
|
|
2275
|
-
case '/dirs': {
|
|
2276
|
-
const subCmd = parts[1];
|
|
2277
|
-
if (subCmd === 'details' || subCmd === 'full') {
|
|
2278
|
-
ctx.addMessage('system', getScopeDetails());
|
|
820
|
+
else if (subCmd === 'details' || subCmd === 'full') {
|
|
821
|
+
ctx.addMessage('system', getScopeDetails());
|
|
2279
822
|
}
|
|
2280
823
|
else if (subCmd === 'reset') {
|
|
2281
824
|
resetScope(process.cwd());
|
|
2282
|
-
ctx.addMessage('system', '
|
|
825
|
+
ctx.addMessage('system', '✓ Scope reset to current directory only');
|
|
2283
826
|
}
|
|
2284
827
|
else {
|
|
2285
828
|
ctx.addMessage('system', getScopeSummary());
|
|
2286
829
|
}
|
|
2287
830
|
break;
|
|
2288
831
|
}
|
|
2289
|
-
case '/
|
|
2290
|
-
const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
|
|
2291
|
-
if (!dirPath) {
|
|
2292
|
-
ctx.addMessage('system', 'Usage: /add-dir <path>\n\nAdd a directory to the allowed scope.\nThe agent can only access files within scope.');
|
|
2293
|
-
}
|
|
2294
|
-
else {
|
|
2295
|
-
const result = addToScope(dirPath);
|
|
2296
|
-
if (result.success) {
|
|
2297
|
-
ctx.addMessage('system', `\u2713 ${result.message}`);
|
|
2298
|
-
}
|
|
2299
|
-
else {
|
|
2300
|
-
ctx.addMessage('error', result.message);
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
break;
|
|
2304
|
-
}
|
|
2305
|
-
case '/remove-dir': {
|
|
2306
|
-
const dirPath = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
|
|
2307
|
-
if (!dirPath) {
|
|
2308
|
-
ctx.addMessage('system', 'Usage: /remove-dir <path>\n\nRemove a directory from the allowed scope.');
|
|
2309
|
-
}
|
|
2310
|
-
else {
|
|
2311
|
-
const result = removeFromScope(dirPath);
|
|
2312
|
-
if (result.success) {
|
|
2313
|
-
ctx.addMessage('system', `\u2713 ${result.message}`);
|
|
2314
|
-
}
|
|
2315
|
-
else {
|
|
2316
|
-
ctx.addMessage('error', result.message);
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
break;
|
|
2320
|
-
}
|
|
2321
|
-
case '/trust':
|
|
2322
|
-
case '/untrust': {
|
|
832
|
+
case '/trust': {
|
|
2323
833
|
const { checkTrust, trustProject, untrustProject, listTrustedProjects, removeFromRegistry } = await import('../trust.js');
|
|
2324
|
-
const trustSubCmd =
|
|
834
|
+
const trustSubCmd = parts[1] || 'status';
|
|
2325
835
|
if (trustSubCmd === 'status') {
|
|
2326
836
|
const trust = checkTrust(process.cwd());
|
|
2327
837
|
ctx.addMessage('system', `Trust: ${trust.trusted ? '✓ Trusted' : '✗ Untrusted'}\n${trust.reason}${trust.changed ? '\n⚠️ CALLIOPE.md has changed since trust was granted' : ''}`);
|
|
@@ -2332,14 +842,10 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
2332
842
|
ctx.addMessage('system', `✓ Trusted: ${dir}`);
|
|
2333
843
|
}
|
|
2334
844
|
else if (trustSubCmd === 'remove' || trustSubCmd === 'no') {
|
|
845
|
+
// Absorbs the old /untrust
|
|
2335
846
|
const dir = parts[2] || process.cwd();
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
}
|
|
2339
|
-
else {
|
|
2340
|
-
untrustProject(dir);
|
|
2341
|
-
}
|
|
2342
|
-
ctx.addMessage('system', `✗ Untrusted: ${command === '/untrust' ? (parts[1] || process.cwd()) : dir}`);
|
|
847
|
+
untrustProject(dir);
|
|
848
|
+
ctx.addMessage('system', `✗ Untrusted: ${dir}`);
|
|
2343
849
|
}
|
|
2344
850
|
else if (trustSubCmd === 'list') {
|
|
2345
851
|
const projects = listTrustedProjects();
|
|
@@ -2357,215 +863,54 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
2357
863
|
ctx.addMessage('system', `Removed from trust registry: ${dir}`);
|
|
2358
864
|
}
|
|
2359
865
|
else {
|
|
2360
|
-
ctx.addMessage('system', 'Usage: /trust [status|add|remove|list|clear]\n /trust add [path]
|
|
866
|
+
ctx.addMessage('system', 'Usage: /trust [status|add|remove|list|clear]\n /trust add [path] - trust a project\n /trust remove [path] - untrust a project');
|
|
2361
867
|
}
|
|
2362
868
|
break;
|
|
2363
869
|
}
|
|
2364
|
-
case '/
|
|
2365
|
-
case '/t': {
|
|
2366
|
-
const subCmd = parts[1];
|
|
2367
|
-
if (subCmd === 'list' || !subCmd) {
|
|
2368
|
-
if (ctx.templates.length === 0) {
|
|
2369
|
-
ctx.addMessage('system', 'No templates saved.\n\nUsage:\n /template save <name> <prompt>\n /template use <name>\n /template delete <name>');
|
|
2370
|
-
}
|
|
2371
|
-
else {
|
|
2372
|
-
const list = ctx.templates.map((t, i) => ` ${i + 1}. ${t.name}: "${t.prompt.substring(0, 50)}${t.prompt.length > 50 ? '...' : ''}"`).join('\n');
|
|
2373
|
-
ctx.addMessage('system', `Templates:\n${list}`);
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
else if (subCmd === 'save' && parts[2]) {
|
|
2377
|
-
const name = parts[2];
|
|
2378
|
-
const prompt = parts.slice(3).join(' ').replace(/^["']|["']$/g, '');
|
|
2379
|
-
if (!prompt) {
|
|
2380
|
-
ctx.addMessage('error', 'Usage: /template save <name> "<prompt>"');
|
|
2381
|
-
}
|
|
2382
|
-
else {
|
|
2383
|
-
storage.saveTemplate(name, prompt);
|
|
2384
|
-
ctx.setTemplates(prev => {
|
|
2385
|
-
const filtered = prev.filter(t => t.name !== name);
|
|
2386
|
-
return [...filtered, { name, prompt, createdAt: new Date() }];
|
|
2387
|
-
});
|
|
2388
|
-
ctx.addMessage('system', `\u2713 Template saved: ${name}`);
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
else if (subCmd === 'use' && parts[2]) {
|
|
2392
|
-
const name = parts[2];
|
|
2393
|
-
const template = ctx.templates.find(t => t.name === name);
|
|
2394
|
-
if (template) {
|
|
2395
|
-
ctx.setInput(template.prompt);
|
|
2396
|
-
ctx.addMessage('system', `\u2713 Template loaded: ${name} (press Enter to send)`);
|
|
2397
|
-
}
|
|
2398
|
-
else {
|
|
2399
|
-
ctx.addMessage('error', `Template not found: ${name}`);
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
else if (subCmd === 'delete' && parts[2]) {
|
|
2403
|
-
const name = parts[2];
|
|
2404
|
-
const found = ctx.templates.find(t => t.name === name);
|
|
2405
|
-
if (found) {
|
|
2406
|
-
storage.deleteTemplate(name);
|
|
2407
|
-
ctx.setTemplates(prev => prev.filter(t => t.name !== name));
|
|
2408
|
-
ctx.addMessage('system', `\u2713 Template deleted: ${name}`);
|
|
2409
|
-
}
|
|
2410
|
-
else {
|
|
2411
|
-
ctx.addMessage('error', `Template not found: ${name}`);
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
else {
|
|
2415
|
-
ctx.addMessage('system', 'Usage: /template [list|save <name> <prompt>|use <name>|delete <name>]');
|
|
2416
|
-
}
|
|
2417
|
-
break;
|
|
2418
|
-
}
|
|
2419
|
-
case '/cost':
|
|
2420
|
-
case '/costs': {
|
|
870
|
+
case '/cost': {
|
|
2421
871
|
const subCmd = parts[1];
|
|
2422
872
|
if (subCmd === 'reset') {
|
|
2423
873
|
storage.resetCosts();
|
|
2424
|
-
ctx.addMessage('system', '
|
|
874
|
+
ctx.addMessage('system', '✓ Cost tracking reset');
|
|
2425
875
|
}
|
|
2426
876
|
else {
|
|
2427
877
|
ctx.addMessage('system', storage.getCostSummary());
|
|
2428
878
|
}
|
|
2429
879
|
break;
|
|
2430
880
|
}
|
|
2431
|
-
case '/
|
|
2432
|
-
case '/bm': {
|
|
881
|
+
case '/compact': {
|
|
2433
882
|
const subCmd = parts[1];
|
|
2434
|
-
if (
|
|
2435
|
-
|
|
2436
|
-
if (
|
|
2437
|
-
ctx.addMessage('system', '
|
|
2438
|
-
}
|
|
2439
|
-
else {
|
|
2440
|
-
const list = ctx.bookmarks.map((b, i) => ` ${i + 1}. \u{1F516} ${b.name} (message #${b.messageIndex})`).join('\n');
|
|
2441
|
-
ctx.addMessage('system', `Bookmarks:\n${list}\n\nUse /bookmark goto <number> to jump.`);
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
else if (subCmd === 'goto' && parts[2]) {
|
|
2445
|
-
const idx = parseInt(parts[2]) - 1;
|
|
2446
|
-
if (idx >= 0 && idx < ctx.bookmarks.length) {
|
|
2447
|
-
const bm = ctx.bookmarks[idx];
|
|
2448
|
-
// Save current state for undo
|
|
2449
|
-
ctx.saveUndoState();
|
|
2450
|
-
// Restore to bookmark point
|
|
2451
|
-
ctx.setMessages(ctx.messages.slice(0, bm.messageIndex + 1));
|
|
2452
|
-
ctx.llmMessages.current = ctx.llmMessages.current.slice(0, bm.llmMessageIndex + 1);
|
|
2453
|
-
ctx.setContextTokens(ctx.estimateContextTokens());
|
|
2454
|
-
ctx.addMessage('system', `\u2713 Jumped to bookmark: ${bm.name}`);
|
|
2455
|
-
}
|
|
2456
|
-
else {
|
|
2457
|
-
ctx.addMessage('error', `Invalid bookmark number. Use /bookmark list to see available.`);
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
else if (subCmd === 'delete' && parts[2]) {
|
|
2461
|
-
const idx = parseInt(parts[2]) - 1;
|
|
2462
|
-
if (idx >= 0 && idx < ctx.bookmarks.length) {
|
|
2463
|
-
const removed = ctx.bookmarks[idx];
|
|
2464
|
-
ctx.setBookmarks(prev => prev.filter((_, i) => i !== idx));
|
|
2465
|
-
ctx.addMessage('system', `\u2713 Deleted bookmark: ${removed.name}`);
|
|
2466
|
-
}
|
|
2467
|
-
else {
|
|
2468
|
-
ctx.addMessage('error', 'Invalid bookmark number.');
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
else {
|
|
2472
|
-
// Create bookmark with given name
|
|
2473
|
-
const name = parts.slice(1).join(' ').replace(/^["']|["']$/g, '');
|
|
2474
|
-
const bm = {
|
|
2475
|
-
id: `bm_${Date.now()}`,
|
|
2476
|
-
name,
|
|
2477
|
-
messageIndex: ctx.messages.length - 1,
|
|
2478
|
-
llmMessageIndex: ctx.llmMessages.current.length - 1,
|
|
2479
|
-
timestamp: new Date(),
|
|
2480
|
-
};
|
|
2481
|
-
ctx.setBookmarks(prev => [...prev, bm]);
|
|
2482
|
-
ctx.addMessage('system', `\u{1F516} Bookmark created: "${name}"`);
|
|
2483
|
-
}
|
|
2484
|
-
break;
|
|
2485
|
-
}
|
|
2486
|
-
case '/queue':
|
|
2487
|
-
case '/q': {
|
|
2488
|
-
// /q is now queue, use /exit to quit
|
|
2489
|
-
if (command === '/q' && !parts[1]) {
|
|
2490
|
-
// Just /q with no args shows queue
|
|
2491
|
-
if (ctx.queuedMessages.length === 0) {
|
|
2492
|
-
ctx.addMessage('system', 'No messages queued. Type while agent is processing to queue feedback.');
|
|
2493
|
-
}
|
|
2494
|
-
else {
|
|
2495
|
-
const list = ctx.queuedMessages.map((m, i) => ` ${i + 1}. ${m.substring(0, 60)}${m.length > 60 ? '...' : ''}`).join('\n');
|
|
2496
|
-
ctx.addMessage('system', `\u{1F4E8} Queued messages (${ctx.queuedMessages.length}):\n${list}\n\nUse /queue clear to remove all.`);
|
|
2497
|
-
}
|
|
2498
|
-
break;
|
|
2499
|
-
}
|
|
2500
|
-
const subCmd = parts[1];
|
|
2501
|
-
if (subCmd === 'clear') {
|
|
2502
|
-
const count = ctx.queuedMessages.length;
|
|
2503
|
-
ctx.setQueuedMessages([]);
|
|
2504
|
-
ctx.addMessage('system', `\u2713 Cleared ${count} queued message${count !== 1 ? 's' : ''}`);
|
|
2505
|
-
}
|
|
2506
|
-
else if (subCmd === 'show' || !subCmd) {
|
|
2507
|
-
if (ctx.queuedMessages.length === 0) {
|
|
2508
|
-
ctx.addMessage('system', 'No messages queued.');
|
|
883
|
+
if (subCmd === 'status') {
|
|
884
|
+
const msgCount = ctx.llmMessages.current.length;
|
|
885
|
+
if (msgCount < 5) {
|
|
886
|
+
ctx.addMessage('system', 'Not enough messages to summarize.');
|
|
2509
887
|
}
|
|
2510
888
|
else {
|
|
2511
|
-
const
|
|
2512
|
-
|
|
889
|
+
const summary = summarization.extractKeyInfo(ctx.llmMessages.current);
|
|
890
|
+
let info = 'Context Summary:\n\n';
|
|
891
|
+
if (summary.topics.length)
|
|
892
|
+
info += `**Topics:** ${summary.topics.join(', ')}\n`;
|
|
893
|
+
if (summary.decisions.length)
|
|
894
|
+
info += `**Decisions:**\n${summary.decisions.map((d) => ` - ${d}`).join('\n')}\n`;
|
|
895
|
+
if (summary.actions.length)
|
|
896
|
+
info += `**Actions:**\n${summary.actions.map((a) => ` - ${a}`).join('\n')}\n`;
|
|
897
|
+
if (summary.codeChanges.length)
|
|
898
|
+
info += `**Code Changes:**\n${summary.codeChanges.slice(0, 5).map((c) => ` - ${c}`).join('\n')}\n`;
|
|
899
|
+
ctx.addMessage('system', info || 'No key information extracted.');
|
|
2513
900
|
}
|
|
2514
901
|
}
|
|
2515
|
-
else
|
|
2516
|
-
//
|
|
2517
|
-
|
|
2518
|
-
|
|
902
|
+
else {
|
|
903
|
+
// Default: summarize and compact the conversation
|
|
904
|
+
const result = summarization.summarizeConversation(ctx.llmMessages.current, { maxTokens: 50000 });
|
|
905
|
+
if (result.summarizedCount > 0) {
|
|
906
|
+
ctx.llmMessages.current = result.messages;
|
|
907
|
+
ctx.setContextTokens(ctx.estimateContextTokens());
|
|
908
|
+
ctx.addMessage('system', `✓ Compacted ${result.summarizedCount} messages (${result.originalTokens} → ${result.reducedTokens} tokens)`);
|
|
2519
909
|
}
|
|
2520
910
|
else {
|
|
2521
|
-
|
|
2522
|
-
ctx.setQueuedMessages([]);
|
|
2523
|
-
ctx.setIsProcessing(false); // Force reset processing state
|
|
2524
|
-
ctx.setThinkingState(null);
|
|
2525
|
-
ctx.setStreamingResponse('');
|
|
2526
|
-
ctx.addMessage('system', `\u{1F504} Flushing ${queued.length} queued message(s)...`);
|
|
2527
|
-
const followUp = queued.length === 1
|
|
2528
|
-
? queued[0]
|
|
2529
|
-
: `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
|
|
2530
|
-
setTimeout(() => {
|
|
2531
|
-
ctx.setIsProcessing(true);
|
|
2532
|
-
ctx.runAgent(followUp).finally(() => {
|
|
2533
|
-
ctx.setIsProcessing(false);
|
|
2534
|
-
ctx.setThinkingState(null);
|
|
2535
|
-
ctx.setStreamingResponse('');
|
|
2536
|
-
});
|
|
2537
|
-
}, 50);
|
|
911
|
+
ctx.addMessage('system', 'Context already within limits, no compaction needed.');
|
|
2538
912
|
}
|
|
2539
913
|
}
|
|
2540
|
-
else {
|
|
2541
|
-
ctx.addMessage('system', 'Usage: /queue [show|clear|flush]\n\nTip: Type while agent is processing to queue follow-up messages.');
|
|
2542
|
-
}
|
|
2543
|
-
break;
|
|
2544
|
-
}
|
|
2545
|
-
case '/flush': {
|
|
2546
|
-
// Shortcut for /queue flush - force-process queued messages
|
|
2547
|
-
if (ctx.queuedMessages.length === 0) {
|
|
2548
|
-
ctx.addMessage('system', 'No messages to flush. Use /debug to see current state.');
|
|
2549
|
-
}
|
|
2550
|
-
else {
|
|
2551
|
-
const queued = [...ctx.queuedMessages];
|
|
2552
|
-
ctx.setQueuedMessages([]);
|
|
2553
|
-
ctx.setIsProcessing(false); // Force reset processing state
|
|
2554
|
-
ctx.setThinkingState(null);
|
|
2555
|
-
ctx.setStreamingResponse('');
|
|
2556
|
-
ctx.addMessage('system', `\u{1F504} Flushing ${queued.length} queued message(s)...`);
|
|
2557
|
-
const followUp = queued.length === 1
|
|
2558
|
-
? queued[0]
|
|
2559
|
-
: `[Multiple follow-up messages:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
|
|
2560
|
-
setTimeout(() => {
|
|
2561
|
-
ctx.setIsProcessing(true);
|
|
2562
|
-
ctx.runAgent(followUp).finally(() => {
|
|
2563
|
-
ctx.setIsProcessing(false);
|
|
2564
|
-
ctx.setThinkingState(null);
|
|
2565
|
-
ctx.setStreamingResponse('');
|
|
2566
|
-
});
|
|
2567
|
-
}, 50);
|
|
2568
|
-
}
|
|
2569
914
|
break;
|
|
2570
915
|
}
|
|
2571
916
|
case '/debug': {
|
|
@@ -2596,55 +941,6 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
2596
941
|
}
|
|
2597
942
|
break;
|
|
2598
943
|
}
|
|
2599
|
-
case '/unstick': {
|
|
2600
|
-
// Emergency reset of processing state
|
|
2601
|
-
ctx.setIsProcessing(false);
|
|
2602
|
-
ctx.setThinkingState(null);
|
|
2603
|
-
ctx.setStreamingResponse('');
|
|
2604
|
-
ctx.setLoopActive(false);
|
|
2605
|
-
ctx.setModalMode('none');
|
|
2606
|
-
ctx.setPendingComplexPrompt(null);
|
|
2607
|
-
// Also reset to hybrid mode if stuck in plan mode
|
|
2608
|
-
if (ctx.mode === 'plan') {
|
|
2609
|
-
ctx.setMode('hybrid');
|
|
2610
|
-
ctx.addMessage('system', '\u{1F527} Reset processing state + switched from plan to hybrid mode.');
|
|
2611
|
-
}
|
|
2612
|
-
else {
|
|
2613
|
-
ctx.addMessage('system', '\u{1F527} Reset processing state. You can now submit new messages.');
|
|
2614
|
-
}
|
|
2615
|
-
break;
|
|
2616
|
-
}
|
|
2617
|
-
case '/keys':
|
|
2618
|
-
case '/?': {
|
|
2619
|
-
// Show keybindings modal
|
|
2620
|
-
ctx.setModalMode('keys');
|
|
2621
|
-
break;
|
|
2622
|
-
}
|
|
2623
|
-
case '/work': {
|
|
2624
|
-
// Quick shortcut to enter work mode
|
|
2625
|
-
ctx.setMode('work');
|
|
2626
|
-
ctx.addMessage('system', `Mode: ${MODE_CONFIG['work'].icon} ${MODE_CONFIG['work'].label} - ${MODE_CONFIG['work'].description}`);
|
|
2627
|
-
break;
|
|
2628
|
-
}
|
|
2629
|
-
case '/plan': {
|
|
2630
|
-
// Quick shortcut to enter plan mode
|
|
2631
|
-
ctx.setMode('plan');
|
|
2632
|
-
ctx.addMessage('system', `Mode: ${MODE_CONFIG['plan'].icon} ${MODE_CONFIG['plan'].label} - ${MODE_CONFIG['plan'].description}`);
|
|
2633
|
-
break;
|
|
2634
|
-
}
|
|
2635
|
-
case '/approve': {
|
|
2636
|
-
// Approve a pending plan and start execution (#19)
|
|
2637
|
-
const approveMsg = parts.length > 1
|
|
2638
|
-
? `Plan approved with notes: ${parts.slice(1).join(' ')}. Execute it step by step, updating progress as you complete each step.`
|
|
2639
|
-
: 'Plan approved. Execute it step by step, updating progress as you complete each step.';
|
|
2640
|
-
// Switch to work mode for execution
|
|
2641
|
-
ctx.setMode('work');
|
|
2642
|
-
ctx.addMessage('system', `${MODE_CONFIG['work'].icon} Switched to work mode for plan execution`);
|
|
2643
|
-
// Send approval as user message to the agent
|
|
2644
|
-
ctx.addMessage('user', approveMsg);
|
|
2645
|
-
await ctx.runAgent(approveMsg);
|
|
2646
|
-
break;
|
|
2647
|
-
}
|
|
2648
944
|
case '/resume': {
|
|
2649
945
|
// Resume a session by loading saved LLM message history
|
|
2650
946
|
// Usage: /resume [sessionId] - resume a specific session, or current session if no ID
|
|
@@ -2684,7 +980,7 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
2684
980
|
ctx.llmMessages.current.length = 0;
|
|
2685
981
|
ctx.llmMessages.current.push({
|
|
2686
982
|
role: 'system',
|
|
2687
|
-
content: buildFullSystemPrompt(ctx
|
|
983
|
+
content: buildFullSystemPrompt(getActiveProjectDir(ctx), ctx.actualProvider),
|
|
2688
984
|
});
|
|
2689
985
|
for (const msg of history) {
|
|
2690
986
|
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
@@ -2700,644 +996,22 @@ Example: /loop "Build a REST API" --max-iterations 50 --completion-promise "DONE
|
|
|
2700
996
|
}
|
|
2701
997
|
break;
|
|
2702
998
|
}
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
if (!ctx.circuitBreaker) {
|
|
2710
|
-
ctx.addMessage('system', 'Circuit breakers not initialized. They activate automatically during agent runs.');
|
|
2711
|
-
break;
|
|
2712
|
-
}
|
|
2713
|
-
if (subCmd === 'status' || !subCmd) {
|
|
2714
|
-
const statuses = ctx.circuitBreaker.getStatus();
|
|
2715
|
-
const stats = ctx.circuitBreaker.getTrackingStats();
|
|
2716
|
-
const health = ctx.circuitBreaker.getHealth();
|
|
2717
|
-
let msg = `Circuit Breaker Health: ${health.toUpperCase()}\n\n`;
|
|
2718
|
-
for (const s of statuses) {
|
|
2719
|
-
const icon = s.state === 'open' ? '\u{1f534}' : s.state === 'half-open' ? '\u{1f7e1}' : '\u{1f7e2}';
|
|
2720
|
-
msg += `${icon} ${s.type}: ${s.state} (tripped ${s.tripCount}x)`;
|
|
2721
|
-
if (s.lastEvent)
|
|
2722
|
-
msg += ` - ${s.lastEvent.message}`;
|
|
2723
|
-
msg += '\n';
|
|
2724
|
-
}
|
|
2725
|
-
msg += `\nTracking: ${stats.consecutiveErrors} consecutive errors, ${stats.totalTokens.toLocaleString()} total tokens, $${stats.totalCost.toFixed(2)} cost, ${stats.idleCount} idle`;
|
|
2726
|
-
ctx.addMessage('system', msg);
|
|
2727
|
-
}
|
|
2728
|
-
else if (subCmd === 'resume') {
|
|
2729
|
-
const breakerType = parts[2];
|
|
2730
|
-
ctx.circuitBreaker.resume(breakerType);
|
|
2731
|
-
ctx.setBreakerHealth(ctx.circuitBreaker.getHealth());
|
|
2732
|
-
ctx.addMessage('system', `\u2713 Circuit breaker${breakerType ? ` "${breakerType}"` : 's'} resumed (half-open mode, 50% more generous thresholds)`);
|
|
2733
|
-
}
|
|
2734
|
-
else if (subCmd === 'reset') {
|
|
2735
|
-
const breakerType = parts[2];
|
|
2736
|
-
ctx.circuitBreaker.reset(breakerType);
|
|
2737
|
-
ctx.setBreakerHealth(ctx.circuitBreaker.getHealth());
|
|
2738
|
-
ctx.addMessage('system', `\u2713 Circuit breaker${breakerType ? ` "${breakerType}"` : 's'} reset to closed`);
|
|
2739
|
-
}
|
|
2740
|
-
else if (subCmd === 'off') {
|
|
2741
|
-
config.set('circuitBreakersEnabled', false);
|
|
2742
|
-
// Also disable the current circuit breaker instance if it exists
|
|
2743
|
-
if (ctx.circuitBreaker) {
|
|
2744
|
-
ctx.circuitBreaker = undefined;
|
|
2745
|
-
ctx.setBreakerHealth?.('ok');
|
|
2746
|
-
}
|
|
2747
|
-
ctx.addMessage('system', '\u2713 Circuit breakers disabled');
|
|
2748
|
-
}
|
|
2749
|
-
else if (subCmd === 'on') {
|
|
2750
|
-
config.set('circuitBreakersEnabled', true);
|
|
2751
|
-
ctx.addMessage('system', '\u2713 Circuit breakers enabled');
|
|
2752
|
-
}
|
|
2753
|
-
else if (subCmd === 'adjust') {
|
|
2754
|
-
const breakerTypeString = parts[2];
|
|
2755
|
-
const param = parts[3];
|
|
2756
|
-
const rawValue = parts[4];
|
|
2757
|
-
// Handle special 'list' command to show detailed parameter info
|
|
2758
|
-
if (breakerTypeString === 'list') {
|
|
2759
|
-
ctx.addMessage('system', `Circuit Breaker Configuration Reference:
|
|
2760
|
-
|
|
2761
|
-
📊 REPEATED-FAILURE (Consecutive Errors)
|
|
2762
|
-
• maxConsecutiveErrors: Number of consecutive errors before tripping (default: 3)
|
|
2763
|
-
Example: /breaker adjust repeated-failure maxConsecutiveErrors 5
|
|
2764
|
-
|
|
2765
|
-
💰 COST-RUNAWAY (Spending Control)
|
|
2766
|
-
• maxSessionCost: Maximum total cost per session in USD (default: $5.0)
|
|
2767
|
-
• maxCostPerMinute: Maximum spending rate per minute in USD (default: $1.0)
|
|
2768
|
-
• windowSizeMs: Sliding window size for rate calculation in milliseconds (default: 60000)
|
|
2769
|
-
Examples:
|
|
2770
|
-
/breaker adjust cost-runaway maxSessionCost 10.0
|
|
2771
|
-
/breaker adjust cost-runaway maxCostPerMinute 2.0
|
|
2772
|
-
|
|
2773
|
-
🔄 INFINITE-LOOP (Repetitive Behavior)
|
|
2774
|
-
• maxIdenticalInWindow: Max identical tool calls in window before tripping (default: 3)
|
|
2775
|
-
• windowSize: Number of recent tool calls to analyze (default: 6)
|
|
2776
|
-
• detectOscillation: Detect A-B-A-B oscillation patterns (default: true)
|
|
2777
|
-
Examples:
|
|
2778
|
-
/breaker adjust infinite-loop maxIdenticalInWindow 5
|
|
2779
|
-
/breaker adjust infinite-loop detectOscillation false
|
|
2780
|
-
|
|
2781
|
-
🔥 TOKEN-BURN (Token Usage Limits)
|
|
2782
|
-
• maxTokensPerIteration: Max tokens per single iteration (default: 200,000)
|
|
2783
|
-
• maxTotalTokens: Max total tokens per session (default: 5,000,000)
|
|
2784
|
-
Examples:
|
|
2785
|
-
/breaker adjust token-burn maxTokensPerIteration 100000
|
|
2786
|
-
/breaker adjust token-burn maxTotalTokens 1000000
|
|
2787
|
-
|
|
2788
|
-
⏸️ STALL (Progress Detection)
|
|
2789
|
-
• maxIdleIterations: Max iterations with no tool calls/content (default: 5)
|
|
2790
|
-
Example: /breaker adjust stall maxIdleIterations 3
|
|
2791
|
-
|
|
2792
|
-
⏰ WALL-CLOCK (Time Limits)
|
|
2793
|
-
• maxSessionDurationMs: Max session duration in milliseconds (0 = unlimited, default: 0)
|
|
2794
|
-
• maxIterationDurationMs: Max single iteration duration in milliseconds (default: 600000 = 10 min)
|
|
2795
|
-
Examples:
|
|
2796
|
-
/breaker adjust wall-clock maxSessionDurationMs 3600000 # 1 hour
|
|
2797
|
-
/breaker adjust wall-clock maxIterationDurationMs 300000 # 5 minutes
|
|
2798
|
-
|
|
2799
|
-
Quick Commands:
|
|
2800
|
-
/breaker adjust <type> - Show current settings for that type
|
|
2801
|
-
/breaker adjust - Show types overview
|
|
2802
|
-
/breaker status - Show current breaker states`);
|
|
2803
|
-
break;
|
|
2804
|
-
}
|
|
2805
|
-
// Show basic types overview if no type specified
|
|
2806
|
-
if (!breakerTypeString) {
|
|
2807
|
-
ctx.addMessage('system', `Circuit Breaker Types Available for Configuration:
|
|
2808
|
-
|
|
2809
|
-
repeated-failure - Consecutive errors before tripping
|
|
2810
|
-
cost-runaway - Spending rate and total cost limits
|
|
2811
|
-
infinite-loop - Identical tool calls and oscillation detection
|
|
2812
|
-
token-burn - Token usage per iteration and total limits
|
|
2813
|
-
stall - Idle iterations without progress
|
|
2814
|
-
wall-clock - Time-based session and iteration limits
|
|
2815
|
-
|
|
2816
|
-
Usage: /breaker adjust <type> [param] [value]
|
|
2817
|
-
/breaker adjust list - Show detailed parameter reference
|
|
2818
|
-
|
|
2819
|
-
Examples:
|
|
2820
|
-
/breaker adjust repeated-failure - Show current settings
|
|
2821
|
-
/breaker adjust cost-runaway maxSessionCost 10.0
|
|
2822
|
-
/breaker adjust infinite-loop detectOscillation true`);
|
|
2823
|
-
break;
|
|
2824
|
-
}
|
|
2825
|
-
// Cast to BreakerType and validate
|
|
2826
|
-
const breakerType = breakerTypeString;
|
|
2827
|
-
const validTypes = ['repeated-failure', 'cost-runaway', 'infinite-loop', 'token-burn', 'stall', 'wall-clock'];
|
|
2828
|
-
if (!validTypes.includes(breakerType)) {
|
|
2829
|
-
ctx.addMessage('error', `Invalid breaker type "${breakerType}". Valid types: ${validTypes.join(', ')}`);
|
|
2830
|
-
break;
|
|
2831
|
-
}
|
|
2832
|
-
const currentConfig = ctx.circuitBreaker.getConfig();
|
|
2833
|
-
const breakerConfig = currentConfig.breakers[breakerType];
|
|
2834
|
-
// Show current configuration if no param specified
|
|
2835
|
-
if (!param) {
|
|
2836
|
-
let configDisplay = `${breakerType} Circuit Breaker Settings:\n`;
|
|
2837
|
-
switch (breakerType) {
|
|
2838
|
-
case 'repeated-failure':
|
|
2839
|
-
configDisplay += ` maxConsecutiveErrors: ${breakerConfig.maxConsecutiveErrors} errors\n\nUsage: /breaker adjust repeated-failure <param> <value>\n /breaker adjust repeated-failure maxConsecutiveErrors <number>`;
|
|
2840
|
-
break;
|
|
2841
|
-
case 'cost-runaway':
|
|
2842
|
-
configDisplay += ` maxSessionCost: ${breakerConfig.maxSessionCost} per session\n maxCostPerMinute: ${breakerConfig.maxCostPerMinute} per minute\n windowSizeMs: ${breakerConfig.windowSizeMs}ms\n\nUsage: /breaker adjust cost-runaway <param> <value>\n /breaker adjust cost-runaway maxSessionCost <dollars>\n /breaker adjust cost-runaway maxCostPerMinute <dollars>\n /breaker adjust cost-runaway windowSizeMs <milliseconds>`;
|
|
2843
|
-
break;
|
|
2844
|
-
case 'infinite-loop':
|
|
2845
|
-
configDisplay += ` maxIdenticalInWindow: ${breakerConfig.maxIdenticalInWindow} calls\n windowSize: ${breakerConfig.windowSize} recent calls\n detectOscillation: ${breakerConfig.detectOscillation}\n\nUsage: /breaker adjust infinite-loop <param> <value>\n /breaker adjust infinite-loop maxIdenticalInWindow <number>\n /breaker adjust infinite-loop windowSize <number>\n /breaker adjust infinite-loop detectOscillation <true|false>`;
|
|
2846
|
-
break;
|
|
2847
|
-
case 'token-burn':
|
|
2848
|
-
configDisplay += ` maxTokensPerIteration: ${breakerConfig.maxTokensPerIteration.toLocaleString()} tokens\n maxTotalTokens: ${breakerConfig.maxTotalTokens.toLocaleString()} tokens\n\nUsage: /breaker adjust token-burn <param> <value>\n /breaker adjust token-burn maxTokensPerIteration <number>\n /breaker adjust token-burn maxTotalTokens <number>`;
|
|
2849
|
-
break;
|
|
2850
|
-
case 'stall':
|
|
2851
|
-
configDisplay += ` maxIdleIterations: ${breakerConfig.maxIdleIterations} iterations\n\nUsage: /breaker adjust stall <param> <value>\n /breaker adjust stall maxIdleIterations <number>`;
|
|
2852
|
-
break;
|
|
2853
|
-
case 'wall-clock':
|
|
2854
|
-
const sessionDuration = breakerConfig.maxSessionDurationMs;
|
|
2855
|
-
const iterationDuration = breakerConfig.maxIterationDurationMs;
|
|
2856
|
-
configDisplay += ` maxSessionDurationMs: ${sessionDuration === 0 ? 'unlimited' : sessionDuration + 'ms'}\n maxIterationDurationMs: ${iterationDuration}ms (${Math.round(iterationDuration / 60000)} minutes)\n\nUsage: /breaker adjust wall-clock <param> <value>\n /breaker adjust wall-clock maxSessionDurationMs <milliseconds>\n /breaker adjust wall-clock maxIterationDurationMs <milliseconds>`;
|
|
2857
|
-
break;
|
|
2858
|
-
}
|
|
2859
|
-
ctx.addMessage('system', configDisplay);
|
|
2860
|
-
break;
|
|
2861
|
-
}
|
|
2862
|
-
// Parse and validate the value
|
|
2863
|
-
let parsedValue;
|
|
2864
|
-
// Handle boolean parameters
|
|
2865
|
-
if (param === 'detectOscillation') {
|
|
2866
|
-
if (rawValue === 'true')
|
|
2867
|
-
parsedValue = true;
|
|
2868
|
-
else if (rawValue === 'false')
|
|
2869
|
-
parsedValue = false;
|
|
2870
|
-
else {
|
|
2871
|
-
ctx.addMessage('error', 'detectOscillation must be "true" or "false"');
|
|
2872
|
-
break;
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
else {
|
|
2876
|
-
// Handle numeric parameters
|
|
2877
|
-
parsedValue = parseFloat(rawValue);
|
|
2878
|
-
if (isNaN(parsedValue) || parsedValue < 0) {
|
|
2879
|
-
ctx.addMessage('error', 'Value must be a non-negative number');
|
|
2880
|
-
break;
|
|
2881
|
-
}
|
|
2882
|
-
}
|
|
2883
|
-
// Validate parameter names for each breaker type
|
|
2884
|
-
const paramValidations = {
|
|
2885
|
-
'repeated-failure': {
|
|
2886
|
-
params: ['maxConsecutiveErrors'],
|
|
2887
|
-
validate: (param, value) => value <= 0 ? 'maxConsecutiveErrors must be > 0' : null
|
|
2888
|
-
},
|
|
2889
|
-
'cost-runaway': {
|
|
2890
|
-
params: ['maxSessionCost', 'maxCostPerMinute', 'windowSizeMs'],
|
|
2891
|
-
validate: (param, value) => value <= 0 ? `${param} must be > 0` : null
|
|
2892
|
-
},
|
|
2893
|
-
'infinite-loop': {
|
|
2894
|
-
params: ['maxIdenticalInWindow', 'windowSize', 'detectOscillation'],
|
|
2895
|
-
validate: (param, value) => {
|
|
2896
|
-
if (param === 'detectOscillation')
|
|
2897
|
-
return null; // boolean is already validated above
|
|
2898
|
-
return value <= 0 ? `${param} must be > 0` : null;
|
|
2899
|
-
}
|
|
2900
|
-
},
|
|
2901
|
-
'token-burn': {
|
|
2902
|
-
params: ['maxTokensPerIteration', 'maxTotalTokens'],
|
|
2903
|
-
validate: (param, value) => value <= 0 ? `${param} must be > 0` : null
|
|
2904
|
-
},
|
|
2905
|
-
'stall': {
|
|
2906
|
-
params: ['maxIdleIterations'],
|
|
2907
|
-
validate: (param, value) => value <= 0 ? 'maxIdleIterations must be > 0' : null
|
|
2908
|
-
},
|
|
2909
|
-
'wall-clock': {
|
|
2910
|
-
params: ['maxSessionDurationMs', 'maxIterationDurationMs'],
|
|
2911
|
-
validate: (param, value) => {
|
|
2912
|
-
if (param === 'maxSessionDurationMs' && value === 0)
|
|
2913
|
-
return null; // 0 = unlimited is valid
|
|
2914
|
-
return value < 0 ? `${param} cannot be negative` : null;
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
};
|
|
2918
|
-
const validation = paramValidations[breakerType];
|
|
2919
|
-
if (!validation.params.includes(param)) {
|
|
2920
|
-
ctx.addMessage('error', `Invalid parameter "${param}" for ${breakerType}. Valid parameters: ${validation.params.join(', ')}`);
|
|
2921
|
-
break;
|
|
2922
|
-
}
|
|
2923
|
-
// Run custom validation if provided
|
|
2924
|
-
if (validation.validate) {
|
|
2925
|
-
const error = validation.validate(param, parsedValue);
|
|
2926
|
-
if (error) {
|
|
2927
|
-
ctx.addMessage('error', error);
|
|
2928
|
-
break;
|
|
2929
|
-
}
|
|
2930
|
-
}
|
|
2931
|
-
// Update the configuration
|
|
2932
|
-
const oldValue = breakerConfig[param];
|
|
2933
|
-
ctx.circuitBreaker.adjust(breakerType, { [param]: parsedValue });
|
|
2934
|
-
// Format the success message based on parameter type
|
|
2935
|
-
let formattedOld, formattedNew;
|
|
2936
|
-
if (param === 'detectOscillation') {
|
|
2937
|
-
formattedOld = String(oldValue);
|
|
2938
|
-
formattedNew = String(parsedValue);
|
|
2939
|
-
}
|
|
2940
|
-
else if (param.includes('Cost')) {
|
|
2941
|
-
formattedOld = `${oldValue}`;
|
|
2942
|
-
formattedNew = `${parsedValue}`;
|
|
2943
|
-
}
|
|
2944
|
-
else if (param.includes('Ms')) {
|
|
2945
|
-
formattedOld = oldValue === 0 ? 'unlimited' : `${oldValue}ms`;
|
|
2946
|
-
formattedNew = parsedValue === 0 ? 'unlimited' : `${parsedValue}ms`;
|
|
2947
|
-
}
|
|
2948
|
-
else if (param.includes('Tokens')) {
|
|
2949
|
-
formattedOld = oldValue.toLocaleString();
|
|
2950
|
-
formattedNew = parsedValue.toLocaleString();
|
|
2951
|
-
}
|
|
2952
|
-
else {
|
|
2953
|
-
formattedOld = String(oldValue);
|
|
2954
|
-
formattedNew = String(parsedValue);
|
|
2955
|
-
}
|
|
2956
|
-
ctx.addMessage('system', `✅ ${breakerType} ${param}: ${formattedOld} → ${formattedNew}`);
|
|
2957
|
-
}
|
|
2958
|
-
else {
|
|
2959
|
-
ctx.addMessage('system', `Usage: /breaker [status|resume|reset|adjust|on|off]
|
|
2960
|
-
/breaker resume [type] - Resume tripped breaker (half-open)
|
|
2961
|
-
/breaker reset [type] - Reset breaker to closed
|
|
2962
|
-
/breaker adjust [type] [param] [value] - Configure breaker thresholds
|
|
2963
|
-
/breaker on|off - Enable/disable circuit breakers
|
|
2964
|
-
|
|
2965
|
-
Breaker types: repeated-failure, cost-runaway, infinite-loop, token-burn, stall, wall-clock
|
|
2966
|
-
|
|
2967
|
-
Quick help:
|
|
2968
|
-
/breaker adjust - Show types overview
|
|
2969
|
-
/breaker adjust list - Show detailed parameter reference`);
|
|
2970
|
-
}
|
|
2971
|
-
break;
|
|
2972
|
-
}
|
|
2973
|
-
// ================================================================
|
|
2974
|
-
// Smart Routing
|
|
2975
|
-
// ================================================================
|
|
2976
|
-
case '/smart': {
|
|
2977
|
-
const subCmd = parts[1];
|
|
2978
|
-
if (subCmd === 'on') {
|
|
2979
|
-
ctx.setSmartRouteActive(true);
|
|
2980
|
-
config.set('smartRoutingEnabled', true);
|
|
2981
|
-
ctx.addMessage('system', '\u2713 Smart routing ON - best model selected across all providers');
|
|
2982
|
-
}
|
|
2983
|
-
else if (subCmd === 'off') {
|
|
2984
|
-
ctx.setSmartRouteActive(false);
|
|
2985
|
-
config.set('smartRoutingEnabled', false);
|
|
2986
|
-
ctx.addMessage('system', '\u2713 Smart routing OFF - using fixed provider/model');
|
|
2987
|
-
}
|
|
2988
|
-
else if (subCmd === 'cost' && parts[2]) {
|
|
2989
|
-
const sensitivity = parseFloat(parts[2]);
|
|
2990
|
-
if (isNaN(sensitivity) || sensitivity < 0 || sensitivity > 1) {
|
|
2991
|
-
ctx.addMessage('error', 'Cost sensitivity must be between 0 (best quality) and 1 (cheapest)');
|
|
2992
|
-
}
|
|
2993
|
-
else {
|
|
2994
|
-
config.set('smartRoutingCostSensitivity', sensitivity);
|
|
2995
|
-
ctx.addMessage('system', `\u2713 Smart routing cost sensitivity set to ${sensitivity} (0=quality, 1=cost)`);
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
else if (subCmd === 'test' && parts[2]) {
|
|
2999
|
-
const testMsg = parts.slice(2).join(' ');
|
|
3000
|
-
const routingConfig = ctx.smartRoutingConfig || {
|
|
3001
|
-
...getDefaultSmartRoutingConfig(),
|
|
3002
|
-
enabled: true,
|
|
3003
|
-
costSensitivity: config.get('smartRoutingCostSensitivity') ?? 0.3,
|
|
3004
|
-
};
|
|
3005
|
-
const decision = smartRoute(testMsg, routingConfig);
|
|
3006
|
-
const taskInfo = detectTaskType(testMsg);
|
|
3007
|
-
let msg = `Smart Route Test:\n`;
|
|
3008
|
-
msg += ` Task type: ${taskInfo.taskType} (${Math.round(taskInfo.confidence * 100)}% confidence)\n`;
|
|
3009
|
-
msg += ` Complexity: ${decision.complexity}\n`;
|
|
3010
|
-
msg += ` Selected: ${decision.selected.provider}/${decision.selected.model} (${decision.selected.tier} tier)\n`;
|
|
3011
|
-
msg += ` Score: ${(decision.selected.score * 100).toFixed(1)}\n`;
|
|
3012
|
-
if (decision.alternatives.length > 0) {
|
|
3013
|
-
msg += ` Alternatives:\n`;
|
|
3014
|
-
for (const alt of decision.alternatives.slice(0, 3)) {
|
|
3015
|
-
msg += ` - ${alt.provider}/${alt.model} (score: ${(alt.score * 100).toFixed(1)})\n`;
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
ctx.addMessage('system', msg);
|
|
3019
|
-
}
|
|
3020
|
-
else {
|
|
3021
|
-
ctx.addMessage('system', `Smart routing: ${ctx.smartRouteActive ? 'ON' : 'OFF'}
|
|
3022
|
-
Cost sensitivity: ${config.get('smartRoutingCostSensitivity') ?? 0.3}
|
|
3023
|
-
|
|
3024
|
-
Usage: /smart [on|off|cost <0-1>|test <message>]
|
|
3025
|
-
/smart on - Enable cross-provider routing
|
|
3026
|
-
/smart off - Disable, use fixed provider
|
|
3027
|
-
/smart cost <0-1> - Set cost sensitivity (0=quality, 1=cheapest)
|
|
3028
|
-
/smart test <msg> - Test routing decision for a message`);
|
|
3029
|
-
}
|
|
3030
|
-
break;
|
|
3031
|
-
}
|
|
3032
|
-
// ================================================================
|
|
3033
|
-
// Swarm Mode
|
|
3034
|
-
// ================================================================
|
|
3035
|
-
case '/swarm':
|
|
3036
|
-
case '/council': {
|
|
3037
|
-
const subCmd = parts[1];
|
|
3038
|
-
const swarmCwd = ctx.sessionRef.current?.projectPath ?? process.cwd();
|
|
3039
|
-
if (!ctx.agtermEnabled) {
|
|
3040
|
-
ctx.addMessage('system', 'Agents mode not enabled. Start with --agents flag to use agent swarms.');
|
|
3041
|
-
break;
|
|
3042
|
-
}
|
|
3043
|
-
// /swarm coordinate — multi-agent coordination (formerly /council)
|
|
3044
|
-
if (subCmd === 'coordinate' || subCmd === 'coord') {
|
|
3045
|
-
const coordSubCmd = parts[2];
|
|
3046
|
-
if (coordSubCmd === 'templates') {
|
|
3047
|
-
const templates = Object.values(COUNCIL_TEMPLATES);
|
|
3048
|
-
let msg = 'Swarm Coordination Templates:\n';
|
|
3049
|
-
for (const t of templates) {
|
|
3050
|
-
msg += `\n ${t.name} - ${t.description}`;
|
|
3051
|
-
msg += `\n Mode: ${t.mode}, Agents: ${t.members.map(m => m.name).join(', ')}`;
|
|
3052
|
-
}
|
|
3053
|
-
ctx.addMessage('system', msg);
|
|
3054
|
-
}
|
|
3055
|
-
else if (coordSubCmd === 'status') {
|
|
3056
|
-
const sessionId = parts[3];
|
|
3057
|
-
if (sessionId) {
|
|
3058
|
-
const session = councilManager.getAllSessions().find(s => s.id.startsWith(sessionId));
|
|
3059
|
-
if (session) {
|
|
3060
|
-
let msg = councilManager.formatSessionStatus(session);
|
|
3061
|
-
if (session.status === 'completed' && session.result) {
|
|
3062
|
-
msg += `\n\nResult:\n${session.result.slice(0, 500)}${(session.result.length > 500) ? '...' : ''}`;
|
|
3063
|
-
}
|
|
3064
|
-
ctx.addMessage('system', msg);
|
|
3065
|
-
}
|
|
3066
|
-
else {
|
|
3067
|
-
ctx.addMessage('system', `Coordination session not found: ${sessionId}`);
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
else {
|
|
3071
|
-
const sessions = councilManager.getAllSessions();
|
|
3072
|
-
if (sessions.length === 0) {
|
|
3073
|
-
ctx.addMessage('system', 'No coordination sessions.');
|
|
3074
|
-
}
|
|
3075
|
-
else {
|
|
3076
|
-
const lines = sessions.map(s => ` ${s.id.slice(0, 8)} [${s.config.mode}] ${s.status} - ${s.prompt.slice(0, 50)}`);
|
|
3077
|
-
ctx.addMessage('system', `Coordination Sessions:\n${lines.join('\n')}`);
|
|
3078
|
-
}
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3081
|
-
else if (coordSubCmd === 'list') {
|
|
3082
|
-
const sessions = councilManager.getAllSessions();
|
|
3083
|
-
const stats = councilManager.getStats();
|
|
3084
|
-
let msg = `Coordination Stats: ${stats.totalSessions} total, ${stats.activeSessions} active, ${stats.completedSessions} completed, ${stats.failedSessions} failed\n`;
|
|
3085
|
-
for (const s of sessions) {
|
|
3086
|
-
msg += `\n ${s.id.slice(0, 8)} [${s.config.mode}] ${s.status} - ${s.prompt.slice(0, 60)}`;
|
|
3087
|
-
}
|
|
3088
|
-
ctx.addMessage('system', msg);
|
|
3089
|
-
}
|
|
3090
|
-
else if (coordSubCmd === 'cancel' && parts[3]) {
|
|
3091
|
-
const session = councilManager.getAllSessions().find(s => s.id.startsWith(parts[3]));
|
|
3092
|
-
if (session) {
|
|
3093
|
-
await councilManager.cancelCouncil(session.id);
|
|
3094
|
-
ctx.addMessage('system', `\u2713 Coordination ${parts[3]} cancelled.`);
|
|
3095
|
-
}
|
|
3096
|
-
else {
|
|
3097
|
-
ctx.addMessage('system', `Coordination session not found: ${parts[3]}`);
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
else if (coordSubCmd && !['help'].includes(coordSubCmd)) {
|
|
3101
|
-
// /swarm coordinate <objective>
|
|
3102
|
-
const prompt = parts.slice(2).join(' ');
|
|
3103
|
-
let cleanPrompt = prompt;
|
|
3104
|
-
let template;
|
|
3105
|
-
let mode = 'competitive';
|
|
3106
|
-
const templateMatch = prompt.match(/--template\s+(\S+)/);
|
|
3107
|
-
if (templateMatch) {
|
|
3108
|
-
template = templateMatch[1];
|
|
3109
|
-
cleanPrompt = cleanPrompt.replace(templateMatch[0], '').trim();
|
|
3110
|
-
}
|
|
3111
|
-
const modeMatch = prompt.match(/--mode\s+(competitive|collaborative|consensus|overseer)/);
|
|
3112
|
-
if (modeMatch) {
|
|
3113
|
-
mode = modeMatch[1];
|
|
3114
|
-
cleanPrompt = cleanPrompt.replace(modeMatch[0], '').trim();
|
|
3115
|
-
}
|
|
3116
|
-
try {
|
|
3117
|
-
let session;
|
|
3118
|
-
if (template) {
|
|
3119
|
-
session = await councilManager.startFromTemplate(template, cleanPrompt, swarmCwd);
|
|
3120
|
-
}
|
|
3121
|
-
else {
|
|
3122
|
-
const { randomUUID } = await import('crypto');
|
|
3123
|
-
const members = [
|
|
3124
|
-
{ id: randomUUID(), name: 'Agent A', agent: 'claude', weight: 1.0 },
|
|
3125
|
-
{ id: randomUUID(), name: 'Agent B', agent: 'claude', weight: 1.0 },
|
|
3126
|
-
{ id: randomUUID(), name: 'Agent C', agent: 'claude', weight: 1.0 },
|
|
3127
|
-
];
|
|
3128
|
-
session = await councilManager.startCouncil(cleanPrompt, { mode, members }, swarmCwd);
|
|
3129
|
-
}
|
|
3130
|
-
watchAsyncLedgerRun(ctx.ledger, 'council', cleanPrompt, () => councilManager.getSession(session.id));
|
|
3131
|
-
ctx.addMessage('system', `\u2713 Swarm coordination started: ${session.id.slice(0, 8)}\nMode: ${session.config.mode}\nAgents: ${session.config.members.map(m => m.name).join(', ')}\n\nUse /swarm coord status ${session.id.slice(0, 8)} to check progress.`);
|
|
3132
|
-
}
|
|
3133
|
-
catch (err) {
|
|
3134
|
-
ctx.addMessage('error', `Failed to start coordination: ${err instanceof Error ? err.message : String(err)}`);
|
|
3135
|
-
}
|
|
3136
|
-
}
|
|
3137
|
-
else {
|
|
3138
|
-
ctx.addMessage('system', `Swarm Coordination: Multi-agent teams working toward shared objectives.
|
|
3139
|
-
|
|
3140
|
-
Modes:
|
|
3141
|
-
competitive — Agents work independently, best result wins
|
|
3142
|
-
collaborative — Pipeline: each agent builds on previous work
|
|
3143
|
-
consensus — Agents vote on approach, supermajority decides
|
|
3144
|
-
overseer — Lead decomposes, delegates, reviews, coordinates
|
|
3145
|
-
|
|
3146
|
-
Usage:
|
|
3147
|
-
/swarm coord <objective> Start with default competitive mode
|
|
3148
|
-
/swarm coord <objective> [options] Start with explicit options
|
|
3149
|
-
/swarm coord status [id] Show session status
|
|
3150
|
-
/swarm coord list List all sessions
|
|
3151
|
-
/swarm coord templates Show available templates
|
|
3152
|
-
/swarm coord cancel <id> Cancel a running session
|
|
3153
|
-
|
|
3154
|
-
Options:
|
|
3155
|
-
--template code-review|architecture|security-audit|brainstorm|debate
|
|
3156
|
-
--mode competitive|collaborative|consensus|overseer`);
|
|
3157
|
-
}
|
|
3158
|
-
}
|
|
3159
|
-
else if (subCmd === 'templates') {
|
|
3160
|
-
// Shortcut: /swarm templates → show coordination templates
|
|
3161
|
-
const templates = Object.values(COUNCIL_TEMPLATES);
|
|
3162
|
-
let msg = 'Swarm Coordination Templates:\n';
|
|
3163
|
-
for (const t of templates) {
|
|
3164
|
-
msg += `\n ${t.name} - ${t.description}`;
|
|
3165
|
-
msg += `\n Mode: ${t.mode}, Agents: ${t.members.map(m => m.name).join(', ')}`;
|
|
3166
|
-
}
|
|
3167
|
-
ctx.addMessage('system', msg);
|
|
3168
|
-
}
|
|
3169
|
-
else if (subCmd === 'start' || (subCmd && !['status', 'list', 'cancel', 'templates', 'coordinate', 'coord', 'help'].includes(subCmd))) {
|
|
3170
|
-
// /swarm start <prompt> or /swarm <prompt> — task decomposition
|
|
3171
|
-
const promptStart = subCmd === 'start' ? 2 : 1;
|
|
3172
|
-
const prompt = parts.slice(promptStart).join(' ');
|
|
3173
|
-
if (!prompt) {
|
|
3174
|
-
ctx.addMessage('system', 'Usage: /swarm <task description>\n /swarm start <task> [--strategy parallel|sequential|map-reduce|pipeline] [--aggregation concatenate|merge-dedupe|summarize|structured]');
|
|
3175
|
-
break;
|
|
3176
|
-
}
|
|
3177
|
-
let strategy = 'parallel';
|
|
3178
|
-
let aggregation = 'concatenate';
|
|
3179
|
-
let cleanPrompt = prompt;
|
|
3180
|
-
const strategyMatch = prompt.match(/--strategy\s+(parallel|sequential|map-reduce|pipeline)/);
|
|
3181
|
-
if (strategyMatch) {
|
|
3182
|
-
strategy = strategyMatch[1];
|
|
3183
|
-
cleanPrompt = cleanPrompt.replace(strategyMatch[0], '').trim();
|
|
3184
|
-
}
|
|
3185
|
-
const aggMatch = prompt.match(/--aggregation\s+(concatenate|merge-dedupe|summarize|structured)/);
|
|
3186
|
-
if (aggMatch) {
|
|
3187
|
-
aggregation = aggMatch[1];
|
|
3188
|
-
cleanPrompt = cleanPrompt.replace(aggMatch[0], '').trim();
|
|
3189
|
-
}
|
|
3190
|
-
try {
|
|
3191
|
-
const session = await swarmManager.startSwarm(cleanPrompt, { decomposition: strategy, aggregation }, swarmCwd);
|
|
3192
|
-
watchAsyncLedgerRun(ctx.ledger, 'swarm', cleanPrompt, () => swarmManager.getSession(session.id));
|
|
3193
|
-
ctx.addMessage('system', `\u2713 Swarm started: ${session.id.slice(0, 8)}\nStrategy: ${strategy} \u2192 ${aggregation}\nStatus: ${session.status}\n\nUse /swarm status ${session.id.slice(0, 8)} to check progress.`);
|
|
3194
|
-
}
|
|
3195
|
-
catch (err) {
|
|
3196
|
-
ctx.addMessage('error', `Failed to start swarm: ${err instanceof Error ? err.message : String(err)}`);
|
|
3197
|
-
}
|
|
3198
|
-
}
|
|
3199
|
-
else if (subCmd === 'status') {
|
|
3200
|
-
// Show both swarm and coordination sessions
|
|
3201
|
-
const sessionId = parts[2];
|
|
3202
|
-
if (sessionId) {
|
|
3203
|
-
// Check decomposition swarms first, then coordination
|
|
3204
|
-
const swarmSession = swarmManager.getAllSessions().find(s => s.id.startsWith(sessionId));
|
|
3205
|
-
const coordSession = councilManager.getAllSessions().find(s => s.id.startsWith(sessionId));
|
|
3206
|
-
if (swarmSession) {
|
|
3207
|
-
let msg = swarmManager.formatSessionStatus(swarmSession);
|
|
3208
|
-
if (swarmSession.status === 'completed' && swarmSession.result) {
|
|
3209
|
-
msg += `\n\nResult:\n${swarmSession.result.slice(0, 500)}${(swarmSession.result.length > 500) ? '...' : ''}`;
|
|
3210
|
-
}
|
|
3211
|
-
ctx.addMessage('system', msg);
|
|
3212
|
-
}
|
|
3213
|
-
else if (coordSession) {
|
|
3214
|
-
let msg = councilManager.formatSessionStatus(coordSession);
|
|
3215
|
-
if (coordSession.status === 'completed' && coordSession.result) {
|
|
3216
|
-
msg += `\n\nResult:\n${coordSession.result.slice(0, 500)}${(coordSession.result.length > 500) ? '...' : ''}`;
|
|
3217
|
-
}
|
|
3218
|
-
ctx.addMessage('system', msg);
|
|
3219
|
-
}
|
|
3220
|
-
else {
|
|
3221
|
-
ctx.addMessage('system', `Swarm session not found: ${sessionId}`);
|
|
3222
|
-
}
|
|
3223
|
-
}
|
|
3224
|
-
else {
|
|
3225
|
-
const swarmSessions = swarmManager.getAllSessions();
|
|
3226
|
-
const coordSessions = councilManager.getAllSessions();
|
|
3227
|
-
if (swarmSessions.length === 0 && coordSessions.length === 0) {
|
|
3228
|
-
ctx.addMessage('system', 'No swarm sessions.');
|
|
3229
|
-
}
|
|
3230
|
-
else {
|
|
3231
|
-
const lines = [];
|
|
3232
|
-
for (const s of swarmSessions) {
|
|
3233
|
-
const subtaskInfo = s.subtasks.length > 0
|
|
3234
|
-
? ` (${s.subtasks.filter(st => st.status === 'completed').length}/${s.subtasks.length} done)`
|
|
3235
|
-
: '';
|
|
3236
|
-
lines.push(` ${s.id.slice(0, 8)} [decompose] ${s.status}${subtaskInfo} - ${s.prompt.slice(0, 50)}`);
|
|
3237
|
-
}
|
|
3238
|
-
for (const s of coordSessions) {
|
|
3239
|
-
lines.push(` ${s.id.slice(0, 8)} [${s.config.mode}] ${s.status} - ${s.prompt.slice(0, 50)}`);
|
|
3240
|
-
}
|
|
3241
|
-
ctx.addMessage('system', `Swarm Sessions:\n${lines.join('\n')}`);
|
|
3242
|
-
}
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
else if (subCmd === 'list') {
|
|
3246
|
-
const swarmSessions = swarmManager.getAllSessions();
|
|
3247
|
-
const swarmStats = swarmManager.getStats();
|
|
3248
|
-
const coordSessions = councilManager.getAllSessions();
|
|
3249
|
-
const coordStats = councilManager.getStats();
|
|
3250
|
-
let msg = `Decomposition: ${swarmStats.totalSessions} total, ${swarmStats.activeSessions} active, ${swarmStats.completedSessions} completed\n`;
|
|
3251
|
-
msg += `Coordination: ${coordStats.totalSessions} total, ${coordStats.activeSessions} active, ${coordStats.completedSessions} completed\n`;
|
|
3252
|
-
for (const s of swarmSessions) {
|
|
3253
|
-
msg += `\n ${s.id.slice(0, 8)} [decompose] ${s.status} - ${s.prompt.slice(0, 60)}`;
|
|
3254
|
-
}
|
|
3255
|
-
for (const s of coordSessions) {
|
|
3256
|
-
msg += `\n ${s.id.slice(0, 8)} [${s.config.mode}] ${s.status} - ${s.prompt.slice(0, 60)}`;
|
|
3257
|
-
}
|
|
3258
|
-
ctx.addMessage('system', msg);
|
|
3259
|
-
}
|
|
3260
|
-
else if (subCmd === 'cancel' && parts[2]) {
|
|
3261
|
-
const swarmSession = swarmManager.getAllSessions().find(s => s.id.startsWith(parts[2]));
|
|
3262
|
-
const coordSession = councilManager.getAllSessions().find(s => s.id.startsWith(parts[2]));
|
|
3263
|
-
if (swarmSession) {
|
|
3264
|
-
await swarmManager.cancelSwarm(swarmSession.id);
|
|
3265
|
-
ctx.addMessage('system', `\u2713 Swarm ${parts[2]} cancelled.`);
|
|
3266
|
-
}
|
|
3267
|
-
else if (coordSession) {
|
|
3268
|
-
await councilManager.cancelCouncil(coordSession.id);
|
|
3269
|
-
ctx.addMessage('system', `\u2713 Coordination ${parts[2]} cancelled.`);
|
|
3270
|
-
}
|
|
3271
|
-
else {
|
|
3272
|
-
ctx.addMessage('system', `Swarm session not found: ${parts[2]}`);
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
else {
|
|
3276
|
-
ctx.addMessage('system', `Agent Swarms: Decompose tasks and coordinate multi-agent teams.
|
|
3277
|
-
|
|
3278
|
-
Decomposition (parallel workers):
|
|
3279
|
-
/swarm <task> Decompose and execute in parallel
|
|
3280
|
-
/swarm start <task> [options] Start with explicit strategy
|
|
3281
|
-
|
|
3282
|
-
Coordination (multi-agent teamwork):
|
|
3283
|
-
/swarm coord <objective> Start coordinated agents
|
|
3284
|
-
/swarm coord <objective> [options] Start with explicit mode
|
|
3285
|
-
/swarm coord templates Show coordination templates
|
|
3286
|
-
|
|
3287
|
-
Management:
|
|
3288
|
-
/swarm status [id] Show session status
|
|
3289
|
-
/swarm list List all sessions
|
|
3290
|
-
/swarm cancel <id> Cancel a session
|
|
3291
|
-
|
|
3292
|
-
Decomposition options:
|
|
3293
|
-
--strategy parallel|sequential|map-reduce|pipeline
|
|
3294
|
-
--aggregation concatenate|merge-dedupe|summarize|structured
|
|
3295
|
-
|
|
3296
|
-
Coordination options:
|
|
3297
|
-
--mode competitive|collaborative|consensus|overseer
|
|
3298
|
-
--template code-review|architecture|security-audit|brainstorm|debate
|
|
3299
|
-
|
|
3300
|
-
Requires --agents flag.`);
|
|
3301
|
-
}
|
|
3302
|
-
break;
|
|
3303
|
-
}
|
|
3304
|
-
case '/checkpoint':
|
|
3305
|
-
case '/cp': {
|
|
3306
|
-
const { listCheckpoints, clearCheckpoints } = await import('../checkpoint.js');
|
|
3307
|
-
const cpSubCmd = parts[1] || 'list';
|
|
3308
|
-
if (cpSubCmd === 'list') {
|
|
3309
|
-
const filterPath = parts[2];
|
|
3310
|
-
const checkpoints = listCheckpoints(filterPath);
|
|
999
|
+
case '/restore': {
|
|
1000
|
+
const { restoreFromCheckpoint, listCheckpoints } = await import('../checkpoint.js');
|
|
1001
|
+
const restorePath = parts[1];
|
|
1002
|
+
if (!restorePath) {
|
|
1003
|
+
// No arg: list available checkpoints (absorbs the old /checkpoint list)
|
|
1004
|
+
const checkpoints = listCheckpoints();
|
|
3311
1005
|
if (checkpoints.length === 0) {
|
|
3312
|
-
ctx.addMessage('system',
|
|
3313
|
-
? `No checkpoints found for: ${filterPath}`
|
|
3314
|
-
: 'No checkpoints found. Checkpoints are created automatically when files are overwritten.');
|
|
1006
|
+
ctx.addMessage('system', 'No checkpoints found. Checkpoints are created automatically (as git commits) before destructive tool calls, and require a git repository.');
|
|
3315
1007
|
}
|
|
3316
1008
|
else {
|
|
3317
1009
|
const list = checkpoints.slice(0, 20).map((cp, i) => {
|
|
3318
|
-
const relPath = path.relative(process.cwd(), cp.filePath);
|
|
3319
1010
|
const time = new Date(cp.timestamp).toLocaleString();
|
|
3320
|
-
|
|
3321
|
-
return ` ${i}. ${relPath} (${size}) - ${time}`;
|
|
1011
|
+
return ` ${i}. ${cp.hash} ${cp.subject} - ${time}`;
|
|
3322
1012
|
}).join('\n');
|
|
3323
|
-
ctx.addMessage('system', `Checkpoints (newest first):\n${list}${checkpoints.length > 20 ? `\n ... and ${checkpoints.length - 20} more` : ''}
|
|
1013
|
+
ctx.addMessage('system', `Checkpoints (newest first):\n${list}${checkpoints.length > 20 ? `\n ... and ${checkpoints.length - 20} more` : ''}\n\nUse /restore <path> [index] to restore a file.`);
|
|
3324
1014
|
}
|
|
3325
|
-
}
|
|
3326
|
-
else if (cpSubCmd === 'clear') {
|
|
3327
|
-
const days = parts[2] ? parseInt(parts[2]) : undefined;
|
|
3328
|
-
const removed = clearCheckpoints(days);
|
|
3329
|
-
ctx.addMessage('system', `Cleared ${removed} checkpoint${removed !== 1 ? 's' : ''}${days ? ` older than ${days} days` : ''}.`);
|
|
3330
|
-
}
|
|
3331
|
-
else {
|
|
3332
|
-
ctx.addMessage('system', 'Usage: /checkpoint [list|clear]\n /checkpoint list [path] - list checkpoints\n /checkpoint clear [days] - clear old checkpoints\n /restore <path> [index] - restore a file');
|
|
3333
|
-
}
|
|
3334
|
-
break;
|
|
3335
|
-
}
|
|
3336
|
-
case '/restore': {
|
|
3337
|
-
const { restoreCheckpoint, listCheckpoints } = await import('../checkpoint.js');
|
|
3338
|
-
const restorePath = parts[1];
|
|
3339
|
-
if (!restorePath) {
|
|
3340
|
-
ctx.addMessage('error', 'Usage: /restore <path> [index]\n Restores a file from its most recent checkpoint.\n Use /checkpoint list to see available checkpoints.');
|
|
3341
1015
|
break;
|
|
3342
1016
|
}
|
|
3343
1017
|
const idx = parts[2] ? parseInt(parts[2]) : 0;
|
|
@@ -3347,174 +1021,17 @@ Requires --agents flag.`);
|
|
|
3347
1021
|
ctx.addMessage('error', `No checkpoints found for: ${restorePath}`);
|
|
3348
1022
|
break;
|
|
3349
1023
|
}
|
|
3350
|
-
const restored =
|
|
1024
|
+
const restored = restoreFromCheckpoint(absRestorePath, idx);
|
|
3351
1025
|
if (restored !== undefined) {
|
|
3352
1026
|
const relPath = path.relative(process.cwd(), absRestorePath);
|
|
3353
1027
|
const cp = checkpoints[idx];
|
|
3354
|
-
ctx.addMessage('system', `✓ Restored ${relPath} from checkpoint (${new Date(cp.timestamp).toLocaleString()})`);
|
|
1028
|
+
ctx.addMessage('system', `✓ Restored ${relPath} from checkpoint ${cp.hash} (${new Date(cp.timestamp).toLocaleString()})`);
|
|
3355
1029
|
}
|
|
3356
1030
|
else {
|
|
3357
1031
|
ctx.addMessage('error', `Failed to restore: checkpoint index ${idx} not found for ${restorePath}`);
|
|
3358
1032
|
}
|
|
3359
1033
|
break;
|
|
3360
1034
|
}
|
|
3361
|
-
// ================================================================
|
|
3362
|
-
// Background Jobs
|
|
3363
|
-
// ================================================================
|
|
3364
|
-
case '/bg': {
|
|
3365
|
-
const bgPrompt = parts.slice(1).join(' ');
|
|
3366
|
-
if (!bgPrompt) {
|
|
3367
|
-
ctx.addMessage('error', 'Usage: /bg <prompt> — run a task in the background');
|
|
3368
|
-
break;
|
|
3369
|
-
}
|
|
3370
|
-
const bgJob = createJob(bgPrompt, { provider: ctx.actualProvider, model: ctx.actualModel });
|
|
3371
|
-
ctx.addMessage('system', `Background job ${bgJob.id} created: "${bgPrompt.length > 60 ? bgPrompt.slice(0, 57) + '...' : bgPrompt}"`);
|
|
3372
|
-
// Run the job using the agent
|
|
3373
|
-
runJob(bgJob.id, async (prompt, signal) => {
|
|
3374
|
-
const { chat } = await import('../providers/index.js');
|
|
3375
|
-
const { TOOLS } = await import('../tools.js');
|
|
3376
|
-
const { executeTool: execTool } = await import('../tools.js');
|
|
3377
|
-
const cwd = ctx.sessionRef.current?.projectPath || process.cwd();
|
|
3378
|
-
const maxIterations = resolveIterationLimit(config.get('maxIterations'));
|
|
3379
|
-
const bgMessages = [
|
|
3380
|
-
{ role: 'system', content: buildFullSystemPrompt(ctx.persona, cwd) },
|
|
3381
|
-
{ role: 'user', content: prompt },
|
|
3382
|
-
];
|
|
3383
|
-
let iterations = 0;
|
|
3384
|
-
let lastContent = '';
|
|
3385
|
-
while (!signal.aborted && iterations < maxIterations) {
|
|
3386
|
-
iterations++;
|
|
3387
|
-
const response = await chat(ctx.provider, bgMessages, TOOLS, ctx.model);
|
|
3388
|
-
if (!response.toolCalls?.length) {
|
|
3389
|
-
lastContent = response.content;
|
|
3390
|
-
break;
|
|
3391
|
-
}
|
|
3392
|
-
bgMessages.push({ role: 'assistant', content: response.content, toolCalls: response.toolCalls });
|
|
3393
|
-
for (const tc of response.toolCalls) {
|
|
3394
|
-
const result = await execTool(tc, cwd);
|
|
3395
|
-
bgMessages.push({ role: 'tool', content: result.result, toolCallId: tc.id });
|
|
3396
|
-
if (signal.aborted)
|
|
3397
|
-
break;
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
if (signal.aborted) {
|
|
3401
|
-
const error = new Error('Background job cancelled');
|
|
3402
|
-
error.name = 'AbortError';
|
|
3403
|
-
throw error;
|
|
3404
|
-
}
|
|
3405
|
-
return { result: lastContent, iterations };
|
|
3406
|
-
}).then(completed => {
|
|
3407
|
-
ctx.addMessage('system', `Background job ${completed.id} ${completed.status}: ${completed.result?.slice(0, 200) || completed.error || 'done'}`);
|
|
3408
|
-
}).catch(() => { });
|
|
3409
|
-
break;
|
|
3410
|
-
}
|
|
3411
|
-
case '/jobs': {
|
|
3412
|
-
ctx.addMessage('system', formatJobsList());
|
|
3413
|
-
break;
|
|
3414
|
-
}
|
|
3415
|
-
case '/job': {
|
|
3416
|
-
const jobId = parts[1];
|
|
3417
|
-
if (!jobId) {
|
|
3418
|
-
ctx.addMessage('error', 'Usage: /job <id> — show job details');
|
|
3419
|
-
break;
|
|
3420
|
-
}
|
|
3421
|
-
const jobInfo = getJob(jobId);
|
|
3422
|
-
if (!jobInfo) {
|
|
3423
|
-
ctx.addMessage('error', `Job "${jobId}" not found.`);
|
|
3424
|
-
}
|
|
3425
|
-
else {
|
|
3426
|
-
ctx.addMessage('system', formatJob(jobInfo) + (jobInfo.result ? `\n\nResult:\n${jobInfo.result.slice(0, 2000)}` : ''));
|
|
3427
|
-
}
|
|
3428
|
-
break;
|
|
3429
|
-
}
|
|
3430
|
-
case '/cancel': {
|
|
3431
|
-
const cancelId = parts[1];
|
|
3432
|
-
if (!cancelId) {
|
|
3433
|
-
ctx.addMessage('error', 'Usage: /cancel <job-id>');
|
|
3434
|
-
break;
|
|
3435
|
-
}
|
|
3436
|
-
const cancelled = cancelJob(cancelId);
|
|
3437
|
-
ctx.addMessage('system', cancelled ? `Cancelled job ${cancelId}` : `Could not cancel ${cancelId} (not running or not found)`);
|
|
3438
|
-
break;
|
|
3439
|
-
}
|
|
3440
|
-
case '/clear-jobs': {
|
|
3441
|
-
const cleared = clearFinishedJobs();
|
|
3442
|
-
ctx.addMessage('system', cleared > 0 ? `Cleared ${cleared} finished job(s).` : 'No finished jobs to clear.');
|
|
3443
|
-
break;
|
|
3444
|
-
}
|
|
3445
|
-
// ================================================================
|
|
3446
|
-
// Recordings
|
|
3447
|
-
// ================================================================
|
|
3448
|
-
case '/recordings': {
|
|
3449
|
-
const recs = listRecordings();
|
|
3450
|
-
if (recs.length === 0) {
|
|
3451
|
-
ctx.addMessage('system', 'No recordings found.');
|
|
3452
|
-
}
|
|
3453
|
-
else {
|
|
3454
|
-
const lines = recs.slice(0, 20).map(r => {
|
|
3455
|
-
const dur = r.duration > 0 ? ` (${Math.floor(r.duration / 60000)}m${Math.floor((r.duration % 60000) / 1000)}s)` : '';
|
|
3456
|
-
return ` ${r.id} ${r.startTime} ${r.eventCount} events${dur}`;
|
|
3457
|
-
});
|
|
3458
|
-
ctx.addMessage('system', `Recordings (${recs.length} total):\n${lines.join('\n')}`);
|
|
3459
|
-
}
|
|
3460
|
-
break;
|
|
3461
|
-
}
|
|
3462
|
-
case '/recording': {
|
|
3463
|
-
const recId = parts[1];
|
|
3464
|
-
if (!recId) {
|
|
3465
|
-
ctx.addMessage('error', 'Usage: /recording <id> — show recording details');
|
|
3466
|
-
break;
|
|
3467
|
-
}
|
|
3468
|
-
const rec = loadRecording(recId);
|
|
3469
|
-
if (!rec) {
|
|
3470
|
-
ctx.addMessage('error', `Recording "${recId}" not found.`);
|
|
3471
|
-
}
|
|
3472
|
-
else {
|
|
3473
|
-
ctx.addMessage('system', formatRecording(rec));
|
|
3474
|
-
}
|
|
3475
|
-
break;
|
|
3476
|
-
}
|
|
3477
|
-
case '/delete-recording': {
|
|
3478
|
-
const delRecId = parts[1];
|
|
3479
|
-
if (!delRecId) {
|
|
3480
|
-
ctx.addMessage('error', 'Usage: /delete-recording <id>');
|
|
3481
|
-
break;
|
|
3482
|
-
}
|
|
3483
|
-
const delResult = deleteRecording(delRecId);
|
|
3484
|
-
ctx.addMessage('system', delResult ? `Deleted recording ${delRecId}` : `Recording "${delRecId}" not found.`);
|
|
3485
|
-
break;
|
|
3486
|
-
}
|
|
3487
|
-
// ================================================================
|
|
3488
|
-
// API Server
|
|
3489
|
-
// ================================================================
|
|
3490
|
-
case '/serve': {
|
|
3491
|
-
if (isApiServerRunning()) {
|
|
3492
|
-
ctx.addMessage('system', 'API server is already running.');
|
|
3493
|
-
break;
|
|
3494
|
-
}
|
|
3495
|
-
const servePort = parts[1] ? parseInt(parts[1], 10) : 3100;
|
|
3496
|
-
if (isNaN(servePort) || servePort < 1 || servePort > 65535) {
|
|
3497
|
-
ctx.addMessage('error', 'Invalid port. Usage: /serve [port]');
|
|
3498
|
-
break;
|
|
3499
|
-
}
|
|
3500
|
-
try {
|
|
3501
|
-
const info = await startApiServer({ port: servePort, host: '127.0.0.1' });
|
|
3502
|
-
ctx.addMessage('system', `API server started on http://${info.host}:${info.port}`);
|
|
3503
|
-
}
|
|
3504
|
-
catch (err) {
|
|
3505
|
-
ctx.addMessage('error', `Failed to start API server: ${err instanceof Error ? err.message : String(err)}`);
|
|
3506
|
-
}
|
|
3507
|
-
break;
|
|
3508
|
-
}
|
|
3509
|
-
case '/serve-stop': {
|
|
3510
|
-
if (!isApiServerRunning()) {
|
|
3511
|
-
ctx.addMessage('system', 'API server is not running.');
|
|
3512
|
-
break;
|
|
3513
|
-
}
|
|
3514
|
-
await stopApiServer();
|
|
3515
|
-
ctx.addMessage('system', 'API server stopped.');
|
|
3516
|
-
break;
|
|
3517
|
-
}
|
|
3518
1035
|
case '/exit':
|
|
3519
1036
|
case '/quit':
|
|
3520
1037
|
process.exit(0);
|