@phuetz/code-buddy 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +56 -0
- package/README.md +74 -16
- package/codebuddy-runtime.json +5 -5
- package/dist/agent/codebuddy-agent.d.ts +18 -0
- package/dist/agent/codebuddy-agent.js +45 -8
- package/dist/agent/execution/agent-executor.d.ts +0 -9
- package/dist/agent/execution/agent-executor.js +176 -12
- package/dist/agent/execution/context-pipeline.d.ts +9 -0
- package/dist/agent/execution/context-pipeline.js +64 -2
- package/dist/agent/execution/tool-loop-guard.d.ts +76 -0
- package/dist/agent/execution/tool-loop-guard.js +165 -0
- package/dist/agent/execution/tool-selection-strategy.d.ts +7 -0
- package/dist/agent/execution/tool-selection-strategy.js +32 -2
- package/dist/agent/facades/session-facade.d.ts +0 -6
- package/dist/agent/facades/session-facade.js +0 -45
- package/dist/agent/hermes-claw-migrate.d.ts +0 -21
- package/dist/agent/hermes-claw-migrate.js +9 -4
- package/dist/agent/hermes-protocol-gateways.js +8 -1
- package/dist/agent/repo-profiler.js +17 -4
- package/dist/agent/self-improvement/authored-tool-runtime.js +10 -2
- package/dist/agent/self-improvement/evolution/ast-novelty.d.ts +5 -0
- package/dist/agent/self-improvement/evolution/ast-novelty.js +58 -23
- package/dist/agent/self-improvement/evolution/code-variant-store.d.ts +3 -0
- package/dist/agent/self-improvement/evolution/evolution-engine.js +2 -0
- package/dist/agent/self-improvement/evolution/protected-paths.js +6 -0
- package/dist/agent/self-improvement/evolution/variant-fitness.d.ts +13 -2
- package/dist/agent/self-improvement/evolution/variant-fitness.js +184 -58
- package/dist/agent/self-improvement/evolution/worktree-scorer.d.ts +2 -0
- package/dist/agent/self-improvement/evolution/worktree-scorer.js +15 -0
- package/dist/agent/self-improvement/evolutionary-archive.d.ts +2 -1
- package/dist/agent/self-improvement/evolutionary-archive.js +34 -5
- package/dist/agent/self-improvement/proposal-store.d.ts +1 -0
- package/dist/agent/self-improvement/proposal-store.js +79 -17
- package/dist/agent/self-improvement/skill-apply-journal.d.ts +44 -0
- package/dist/agent/self-improvement/skill-apply-journal.js +178 -0
- package/dist/agent/self-improvement/skill-behavior-benchmark.d.ts +3 -0
- package/dist/agent/self-improvement/skill-behavior-benchmark.js +19 -0
- package/dist/agent/self-improvement/skill-behavior-gate.d.ts +34 -0
- package/dist/agent/self-improvement/skill-behavior-gate.js +96 -0
- package/dist/agent/self-improvement/skill-engine.d.ts +22 -0
- package/dist/agent/self-improvement/skill-engine.js +385 -13
- package/dist/agent/self-improvement/skill-gate.d.ts +2 -0
- package/dist/agent/self-improvement/skill-gate.js +17 -2
- package/dist/agent/self-improvement/skill-mutator.d.ts +6 -1
- package/dist/agent/self-improvement/skill-mutator.js +51 -15
- package/dist/agent/self-improvement/skill-types.d.ts +2 -1
- package/dist/agent/self-improvement/types.d.ts +11 -0
- package/dist/agent/tool-handler.d.ts +1 -1
- package/dist/agent/tool-handler.js +28 -9
- package/dist/auth/profile-manager.js +1 -1
- package/dist/channels/companion-channel-turn.d.ts +53 -4
- package/dist/channels/companion-channel-turn.js +218 -16
- package/dist/channels/slash-parity.d.ts +4 -6
- package/dist/channels/slash-parity.js +9 -44
- package/dist/cli/session-commands.d.ts +9 -0
- package/dist/cli/session-commands.js +45 -5
- package/dist/cli/session-picker.d.ts +43 -0
- package/dist/cli/session-picker.js +128 -0
- package/dist/codebuddy/a2a-call-tool-defs.d.ts +26 -0
- package/dist/codebuddy/a2a-call-tool-defs.js +12 -0
- package/dist/codebuddy/fleet-tool-defs.d.ts +2 -0
- package/dist/codebuddy/fleet-tool-defs.js +55 -0
- package/dist/codebuddy/providers/chatgpt-headers.d.ts +18 -0
- package/dist/codebuddy/providers/chatgpt-headers.js +37 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +2 -1
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +10 -23
- package/dist/codebuddy/providers/provider-openai-compat.d.ts +7 -0
- package/dist/codebuddy/providers/provider-openai-compat.js +29 -1
- package/dist/codebuddy/ragchat-tool-defs.d.ts +31 -0
- package/dist/codebuddy/ragchat-tool-defs.js +19 -0
- package/dist/codebuddy/resource-catalog-tool-defs.d.ts +28 -0
- package/dist/codebuddy/resource-catalog-tool-defs.js +18 -0
- package/dist/codebuddy/tool-definitions/agent-tools.js +2 -2
- package/dist/codebuddy/tool-definitions/code-exec-tools.js +5 -1
- package/dist/codebuddy/tool-definitions/cron-tools.js +6 -0
- package/dist/codebuddy/tool-definitions/self-describe-tools.js +6 -1
- package/dist/codebuddy/tools.d.ts +0 -6
- package/dist/codebuddy/tools.js +25 -3
- package/dist/commands/cli/daemon-commands.js +3 -1
- package/dist/commands/cli/evolve-command.js +21 -5
- package/dist/commands/cli/fleet-collaboration-commands.d.ts +2 -0
- package/dist/commands/cli/fleet-collaboration-commands.js +57 -0
- package/dist/commands/cli/fleet-commands.js +29 -0
- package/dist/commands/cli/fleet-mission-commands.d.ts +3 -0
- package/dist/commands/cli/fleet-mission-commands.js +72 -0
- package/dist/commands/cli/fleet-rooms-commands.d.ts +10 -0
- package/dist/commands/cli/fleet-rooms-commands.js +289 -0
- package/dist/commands/cli/tools-commands.js +2 -1
- package/dist/commands/cli/triage-command.d.ts +8 -0
- package/dist/commands/cli/triage-command.js +72 -0
- package/dist/commands/cli/utility-commands.js +25 -1
- package/dist/commands/client-dispatcher.d.ts +7 -0
- package/dist/commands/client-dispatcher.js +73 -21
- package/dist/commands/cron-cli/index.d.ts +3 -1
- package/dist/commands/cron-cli/index.js +25 -0
- package/dist/commands/device-auth.d.ts +10 -0
- package/dist/commands/device-auth.js +91 -0
- package/dist/commands/enhanced-command-handler.d.ts +9 -0
- package/dist/commands/enhanced-command-handler.js +28 -6
- package/dist/commands/handlers/agent-handlers.d.ts +1 -1
- package/dist/commands/handlers/agent-handlers.js +4 -4
- package/dist/commands/handlers/channel-handlers.js +53 -2
- package/dist/commands/handlers/export-handlers.d.ts +3 -2
- package/dist/commands/handlers/export-handlers.js +55 -28
- package/dist/commands/handlers/extra-handlers.js +5 -1
- package/dist/commands/handlers/grill-me-handler.js +26 -18
- package/dist/commands/handlers/memory-handlers.d.ts +6 -2
- package/dist/commands/handlers/memory-handlers.js +35 -15
- package/dist/commands/handlers/missing-handlers.d.ts +2 -2
- package/dist/commands/handlers/missing-handlers.js +12 -4
- package/dist/commands/handlers/resources-handler.d.ts +12 -0
- package/dist/commands/handlers/resources-handler.js +52 -0
- package/dist/commands/handlers/starter-handlers.d.ts +1 -1
- package/dist/commands/handlers/starter-handlers.js +32 -2
- package/dist/commands/handlers/switch-handler.d.ts +1 -1
- package/dist/commands/handlers/switch-handler.js +15 -0
- package/dist/commands/handlers/test-handlers.js +4 -86
- package/dist/commands/handlers/ui-handlers.js +11 -5
- package/dist/commands/login-chatgpt.d.ts +8 -0
- package/dist/commands/login-chatgpt.js +17 -0
- package/dist/commands/mcp-import.d.ts +11 -0
- package/dist/commands/mcp-import.js +67 -0
- package/dist/commands/mcp-invoke.d.ts +2 -0
- package/dist/commands/mcp-invoke.js +68 -0
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +66 -21
- package/dist/commands/pipeline.d.ts +7 -1
- package/dist/commands/pipeline.js +104 -7
- package/dist/commands/resources.d.ts +2 -0
- package/dist/commands/resources.js +64 -0
- package/dist/commands/skills-cli/index.js +35 -12
- package/dist/commands/slash/builtin-commands.js +16 -3
- package/dist/commands/slash/index.d.ts +3 -1
- package/dist/commands/slash/index.js +2 -0
- package/dist/commands/slash/surfaces.d.ts +53 -0
- package/dist/commands/slash/surfaces.js +177 -0
- package/dist/commands/slash/types.d.ts +17 -0
- package/dist/commands/try.d.ts +1 -1
- package/dist/commands/try.js +1 -1
- package/dist/companion/companion-identity.d.ts +43 -0
- package/dist/companion/companion-identity.js +136 -0
- package/dist/companion/companion-toolset.d.ts +66 -0
- package/dist/companion/companion-toolset.js +368 -0
- package/dist/companion/companion-turn.d.ts +10 -2
- package/dist/companion/companion-turn.js +38 -1
- package/dist/config/code-exec-policy.d.ts +25 -0
- package/dist/config/code-exec-policy.js +39 -0
- package/dist/config/model-tools.d.ts +9 -0
- package/dist/config/model-tools.js +103 -0
- package/dist/context/context-manager-v2.js +2 -2
- package/dist/context/lm-resizer-compressor.js +3 -2
- package/dist/context/lm-resizer-diagnostics.d.ts +9 -0
- package/dist/context/lm-resizer-diagnostics.js +30 -0
- package/dist/context/tool-output-masking.js +3 -4
- package/dist/curator/curator.d.ts +6 -0
- package/dist/curator/curator.js +53 -3
- package/dist/daemon/cron-agent-bridge.d.ts +6 -0
- package/dist/daemon/cron-agent-bridge.js +31 -4
- package/dist/doctor/index.d.ts +11 -2
- package/dist/doctor/index.js +84 -21
- package/dist/doctor/integrations.d.ts +197 -0
- package/dist/doctor/integrations.js +241 -0
- package/dist/doctor/triage.d.ts +108 -0
- package/dist/doctor/triage.js +304 -0
- package/dist/errors/crash-handler.js +2 -0
- package/dist/fleet/collaboration.d.ts +44 -0
- package/dist/fleet/collaboration.js +170 -0
- package/dist/fleet/fleet-listener.d.ts +1 -0
- package/dist/fleet/fleet-listener.js +87 -46
- package/dist/fleet/fleet-registry.d.ts +2 -0
- package/dist/fleet/resource-catalog.d.ts +245 -0
- package/dist/fleet/resource-catalog.js +211 -0
- package/dist/fleet/rooms/room-access.d.ts +138 -0
- package/dist/fleet/rooms/room-access.js +196 -0
- package/dist/fleet/rooms/room-client.d.ts +158 -0
- package/dist/fleet/rooms/room-client.js +685 -0
- package/dist/fleet/rooms/room-event.d.ts +139 -0
- package/dist/fleet/rooms/room-event.js +330 -0
- package/dist/fleet/rooms/room-filter.d.ts +48 -0
- package/dist/fleet/rooms/room-filter.js +152 -0
- package/dist/fleet/rooms/room-hub.d.ts +154 -0
- package/dist/fleet/rooms/room-hub.js +363 -0
- package/dist/fleet/rooms/room-identity.d.ts +26 -0
- package/dist/fleet/rooms/room-identity.js +82 -0
- package/dist/fleet/rooms/room-lock.d.ts +34 -0
- package/dist/fleet/rooms/room-lock.js +180 -0
- package/dist/fleet/rooms/room-observations.d.ts +7 -0
- package/dist/fleet/rooms/room-observations.js +61 -0
- package/dist/fleet/rooms/room-server.d.ts +28 -0
- package/dist/fleet/rooms/room-server.js +75 -0
- package/dist/fleet/rooms/room-store.d.ts +119 -0
- package/dist/fleet/rooms/room-store.js +448 -0
- package/dist/fleet/rooms/room-ws-bridge.d.ts +27 -0
- package/dist/fleet/rooms/room-ws-bridge.js +137 -0
- package/dist/fleet/sensory-mission-bridge.d.ts +42 -0
- package/dist/fleet/sensory-mission-bridge.js +136 -0
- package/dist/git/worktree-sessions.js +1 -0
- package/dist/harness/contract.d.ts +4 -4
- package/dist/harness/fleet-supervisor.d.ts +16 -0
- package/dist/harness/fleet-supervisor.js +64 -0
- package/dist/harness/index.d.ts +4 -0
- package/dist/harness/index.js +4 -0
- package/dist/harness/mission-runner.d.ts +3 -0
- package/dist/harness/mission-runner.js +53 -0
- package/dist/harness/mission-store.d.ts +93 -0
- package/dist/harness/mission-store.js +297 -0
- package/dist/harness/tool-harness.d.ts +56 -0
- package/dist/harness/tool-harness.js +153 -0
- package/dist/hooks/use-enhanced-input.js +48 -4
- package/dist/hooks/use-input-handler.d.ts +1 -0
- package/dist/hooks/use-input-handler.js +82 -22
- package/dist/hooks/use-input-history.js +1 -0
- package/dist/identity/lisa-introspection.js +5 -2
- package/dist/identity/operational-self-model.d.ts +2 -0
- package/dist/identity/operational-self-model.js +4 -0
- package/dist/index.js +52 -6
- package/dist/mcp/client.d.ts +11 -0
- package/dist/mcp/client.js +68 -3
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +121 -8
- package/dist/mcp/import-normalize.d.ts +12 -0
- package/dist/mcp/import-normalize.js +114 -0
- package/dist/mcp/mcp-oauth-constants.d.ts +8 -0
- package/dist/mcp/mcp-oauth-constants.js +9 -0
- package/dist/mcp/mcp-oauth-provider.d.ts +80 -0
- package/dist/mcp/mcp-oauth-provider.js +241 -0
- package/dist/mcp/mcp-oauth.d.ts +60 -1
- package/dist/mcp/mcp-oauth.js +241 -65
- package/dist/mcp/transports.d.ts +14 -5
- package/dist/mcp/transports.js +112 -101
- package/dist/mcp/types.d.ts +5 -0
- package/dist/media/comfyui-recipe-contract.d.ts +7 -7
- package/dist/memory/facts-memory.d.ts +11 -1
- package/dist/memory/facts-memory.js +47 -8
- package/dist/memory/memory-candidate-queue.d.ts +2 -2
- package/dist/memory/memory-candidate-queue.js +6 -6
- package/dist/memory/persistent-memory.d.ts +13 -8
- package/dist/memory/persistent-memory.js +77 -30
- package/dist/nodes/device-node.js +5 -9
- package/dist/observability/mobile-supervision-gateway-contract.js +20 -7
- package/dist/observability/run-event-writer.d.ts +21 -0
- package/dist/observability/run-event-writer.js +81 -0
- package/dist/observability/run-store.d.ts +5 -0
- package/dist/observability/run-store.js +18 -13
- package/dist/observability/run-trajectory.js +1 -1
- package/dist/optimization/prompt-cache.d.ts +19 -2
- package/dist/optimization/prompt-cache.js +41 -6
- package/dist/orchestration/orchestrator.d.ts +21 -0
- package/dist/orchestration/orchestrator.js +122 -15
- package/dist/persistence/session-content.d.ts +8 -0
- package/dist/persistence/session-content.js +76 -0
- package/dist/persistence/session-handoff.d.ts +64 -0
- package/dist/persistence/session-handoff.js +80 -0
- package/dist/persistence/session-history.d.ts +4 -0
- package/dist/persistence/session-history.js +38 -0
- package/dist/persistence/session-store.d.ts +9 -0
- package/dist/persistence/session-store.js +48 -20
- package/dist/personas/persona-manager.js +17 -1
- package/dist/plugins/code-explorer/CodeExplorerManager.d.ts +10 -4
- package/dist/plugins/code-explorer/CodeExplorerManager.js +69 -16
- package/dist/plugins/plugin-manager.js +10 -1
- package/dist/prompts/prompt-manager.js +11 -4
- package/dist/prompts/system-base.js +6 -2
- package/dist/protocols/a2a/codebuddy-executor.d.ts +11 -2
- package/dist/protocols/a2a/codebuddy-executor.js +9 -8
- package/dist/protocols/a2a/index.d.ts +1 -1
- package/dist/protocols/a2a/jsonrpc-v1.d.ts +41 -0
- package/dist/protocols/a2a/jsonrpc-v1.js +145 -0
- package/dist/protocols/a2a/peer-config.d.ts +20 -0
- package/dist/protocols/a2a/peer-config.js +55 -0
- package/dist/providers/codex-oauth.js +30 -6
- package/dist/providers/model-provider-compat.js +2 -1
- package/dist/sandbox/os-sandbox.d.ts +1 -1
- package/dist/scheduler/cron-scheduler.d.ts +15 -2
- package/dist/scheduler/cron-scheduler.js +35 -4
- package/dist/scheduler/job-notepad.d.ts +69 -0
- package/dist/scheduler/job-notepad.js +339 -0
- package/dist/security/audit-logger.d.ts +1 -1
- package/dist/security/compute-confinement.d.ts +4 -0
- package/dist/security/compute-confinement.js +89 -0
- package/dist/security/session-encryption.d.ts +4 -0
- package/dist/security/session-encryption.js +31 -0
- package/dist/sensory/voice-loop.d.ts +17 -0
- package/dist/sensory/voice-loop.js +72 -15
- package/dist/server/auth/device-session-context.d.ts +9 -0
- package/dist/server/auth/device-session-context.js +13 -0
- package/dist/server/auth/device-store.d.ts +89 -0
- package/dist/server/auth/device-store.js +261 -0
- package/dist/server/auth/device-token.d.ts +3 -0
- package/dist/server/auth/device-token.js +6 -0
- package/dist/server/auth/jwt.js +4 -0
- package/dist/server/index.d.ts +0 -10
- package/dist/server/index.js +28 -0
- package/dist/server/middleware/auth.js +3 -2
- package/dist/server/routes/a2a-jsonrpc.d.ts +9 -0
- package/dist/server/routes/a2a-jsonrpc.js +66 -0
- package/dist/server/routes/a2a-protocol.d.ts +12 -0
- package/dist/server/routes/a2a-protocol.js +1 -1
- package/dist/server/routes/device-auth.d.ts +4 -0
- package/dist/server/routes/device-auth.js +40 -0
- package/dist/server/types.d.ts +4 -0
- package/dist/server/websocket/handler.d.ts +13 -0
- package/dist/server/websocket/handler.js +102 -17
- package/dist/services/prompt-builder.d.ts +2 -0
- package/dist/services/prompt-builder.js +16 -6
- package/dist/services/runtime-settings-context.d.ts +81 -0
- package/dist/services/runtime-settings-context.js +79 -0
- package/dist/skills/executor.js +2 -0
- package/dist/skills/hub.js +16 -14
- package/dist/skills/local-inventory.d.ts +21 -0
- package/dist/skills/local-inventory.js +72 -0
- package/dist/skills/skill-loader.js +2 -2
- package/dist/skills/skill-usage-store.d.ts +49 -0
- package/dist/skills/skill-usage-store.js +121 -0
- package/dist/testing/ai-integration-tests.js +8 -5
- package/dist/themes/theme-manager.d.ts +12 -0
- package/dist/themes/theme-manager.js +29 -0
- package/dist/themes/theme-schema.d.ts +24 -24
- package/dist/tools/a2a-call-tool.d.ts +34 -0
- package/dist/tools/a2a-call-tool.js +75 -0
- package/dist/tools/bash/streaming-executor.d.ts +1 -0
- package/dist/tools/bash/streaming-executor.js +78 -90
- package/dist/tools/code-exec-preflight-runner.d.ts +41 -0
- package/dist/tools/code-exec-preflight-runner.js +254 -0
- package/dist/tools/code-exec-preflight.d.ts +44 -0
- package/dist/tools/code-exec-preflight.js +501 -0
- package/dist/tools/code-exec-tool.d.ts +48 -1
- package/dist/tools/code-exec-tool.js +191 -47
- package/dist/tools/comment-watcher.js +29 -12
- package/dist/tools/core-code-inspection.d.ts +56 -0
- package/dist/tools/core-code-inspection.js +86 -0
- package/dist/tools/create-skill-tool.d.ts +2 -3
- package/dist/tools/create-skill-tool.js +16 -75
- package/dist/tools/cronjob-tool.d.ts +1 -0
- package/dist/tools/cronjob-tool.js +1 -0
- package/dist/tools/env-doctor-tool.d.ts +2 -0
- package/dist/tools/env-doctor-tool.js +4 -3
- package/dist/tools/execute-code-rpc-invoker.d.ts +1 -1
- package/dist/tools/execute-code-rpc-invoker.js +35 -19
- package/dist/tools/execute-code-runner.d.ts +2 -0
- package/dist/tools/execute-code-runner.js +52 -17
- package/dist/tools/fleet-room-tool.d.ts +104 -0
- package/dist/tools/fleet-room-tool.js +124 -0
- package/dist/tools/integration-tool-hints.d.ts +4 -0
- package/dist/tools/integration-tool-hints.js +19 -0
- package/dist/tools/list-peers-tool.d.ts +3 -0
- package/dist/tools/list-peers-tool.js +11 -4
- package/dist/tools/media-generation-tool.d.ts +16 -1
- package/dist/tools/media-generation-tool.js +341 -9
- package/dist/tools/merge-conflict-tool.js +53 -25
- package/dist/tools/metadata.d.ts +1 -5
- package/dist/tools/metadata.js +30 -28
- package/dist/tools/peer-tool-invoke-tool.d.ts +61 -0
- package/dist/tools/peer-tool-invoke-tool.js +396 -0
- package/dist/tools/ragchat-tool.d.ts +39 -0
- package/dist/tools/ragchat-tool.js +105 -0
- package/dist/tools/registry/fleet-tools.d.ts +21 -3
- package/dist/tools/registry/fleet-tools.js +137 -4
- package/dist/tools/registry/index.d.ts +1 -1
- package/dist/tools/registry/index.js +2 -2
- package/dist/tools/registry/knowledge-tools.d.ts +2 -2
- package/dist/tools/registry/knowledge-tools.js +2 -2
- package/dist/tools/registry/lessons-tools.d.ts +7 -0
- package/dist/tools/registry/lessons-tools.js +43 -3
- package/dist/tools/registry/memory-tools.d.ts +1 -1
- package/dist/tools/registry/memory-tools.js +8 -8
- package/dist/tools/registry/research-tools.d.ts +1 -0
- package/dist/tools/registry/research-tools.js +4 -13
- package/dist/tools/registry/self-describe-tools.d.ts +1 -1
- package/dist/tools/registry/self-describe-tools.js +43 -30
- package/dist/tools/resource-catalog-tool.d.ts +38 -0
- package/dist/tools/resource-catalog-tool.js +46 -0
- package/dist/tools/skills-inspection-tool.js +30 -1
- package/dist/tools/stream-tool-output.d.ts +3 -0
- package/dist/tools/stream-tool-output.js +33 -0
- package/dist/tools/tool-call-scheduler.d.ts +10 -0
- package/dist/tools/tool-call-scheduler.js +58 -0
- package/dist/tools/tool-effect.d.ts +5 -0
- package/dist/tools/tool-effect.js +26 -0
- package/dist/tools/tool-search.d.ts +12 -11
- package/dist/tools/tool-search.js +67 -26
- package/dist/tools/types.d.ts +6 -0
- package/dist/ui/components/ChatHistory.d.ts +1 -1
- package/dist/ui/components/ChatHistory.js +2 -2
- package/dist/ui/components/ChatInput.d.ts +2 -1
- package/dist/ui/components/ChatInput.js +42 -88
- package/dist/ui/components/ChatInterface.d.ts +4 -2
- package/dist/ui/components/ChatInterface.js +27 -63
- package/dist/ui/components/CommandSuggestions.d.ts +0 -1
- package/dist/ui/components/CommandSuggestions.js +4 -6
- package/dist/ui/components/FileAutocomplete.js +1 -1
- package/dist/ui/components/KeyboardHelp.js +8 -6
- package/dist/ui/components/ModelSelection.js +6 -5
- package/dist/ui/components/StatusBar.d.ts +2 -1
- package/dist/ui/components/StatusBar.js +8 -7
- package/dist/ui/context/theme-context.js +3 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/utils/markdown-renderer.js +4 -13
- package/dist/utils/atomic-write.d.ts +22 -0
- package/dist/utils/atomic-write.js +29 -0
- package/dist/utils/bounded-output.d.ts +16 -0
- package/dist/utils/bounded-output.js +63 -0
- package/dist/utils/config-validation/schema.d.ts +2 -2
- package/dist/utils/device-store-file.d.ts +4 -0
- package/dist/utils/device-store-file.js +40 -0
- package/dist/utils/export-manager.d.ts +9 -0
- package/dist/utils/export-manager.js +19 -0
- package/dist/utils/first-use-hints.d.ts +25 -0
- package/dist/utils/first-use-hints.js +58 -0
- package/dist/utils/graceful-shutdown.d.ts +8 -0
- package/dist/utils/graceful-shutdown.js +23 -7
- package/dist/utils/history-manager.js +1 -1
- package/dist/utils/update-notifier.js +16 -18
- package/dist/wizard/environment-detection.d.ts +5 -1
- package/dist/wizard/environment-detection.js +49 -7
- package/dist/wizard/onboarding.d.ts +5 -1
- package/dist/wizard/onboarding.js +90 -30
- package/dist/wizard/provider-onboarding.d.ts +2 -0
- package/dist/wizard/provider-onboarding.js +55 -17
- package/package.json +7 -3
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/** Durable, single-host mission coordination. The store directory is operator-owned. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
export class MissionBusyError extends Error {
|
|
7
|
+
constructor() { super('Mission mutation locked; retry. An abandoned lock requires operator inspection.'); }
|
|
8
|
+
}
|
|
9
|
+
function bounded(value, label, max = 4096) {
|
|
10
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max || value.includes('\0'))
|
|
11
|
+
throw new Error(`Invalid ${label}`);
|
|
12
|
+
}
|
|
13
|
+
function validate(m) {
|
|
14
|
+
if (!m || m.schema !== 1 || !Number.isSafeInteger(m.revision) || m.revision < 0 || !['ready', 'claimed', 'running', 'handoff', 'completed'].includes(m.status))
|
|
15
|
+
throw new Error('Invalid mission record');
|
|
16
|
+
bounded(m.id, 'mission id', 256);
|
|
17
|
+
const op = m.operation;
|
|
18
|
+
if (!op || !path.isAbsolute(op.workspace) || !Number.isInteger(op.timeoutMs) || op.timeoutMs < 100 || op.timeoutMs > 43200000)
|
|
19
|
+
throw new Error('Invalid mission operation');
|
|
20
|
+
bounded(op.command, 'command');
|
|
21
|
+
if (Buffer.byteLength(JSON.stringify(op)) > 262144)
|
|
22
|
+
throw new Error('Mission operation exceeds size limit');
|
|
23
|
+
if (!Array.isArray(op.args) || op.args.length > 256 || op.args.some(a => typeof a !== 'string' || a.length > 65536 || a.includes('\0')))
|
|
24
|
+
throw new Error('Invalid operation arguments');
|
|
25
|
+
if (!Number.isFinite(m.updatedAt))
|
|
26
|
+
throw new Error('Invalid update time');
|
|
27
|
+
if (m.result && (typeof m.result.success !== 'boolean' || typeof m.result.stdout !== 'string' || typeof m.result.stderr !== 'string' || !Number.isInteger(m.result.exitCode) || m.result.stdout.length > 1000000 || m.result.stderr.length > 1000000 || (m.result.truncated !== undefined && typeof m.result.truncated !== 'boolean')))
|
|
28
|
+
throw new Error('Invalid mission result');
|
|
29
|
+
if (m.review && (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(m.review.commit) || typeof m.review.author !== 'string' || (m.review.reviewer !== undefined && (typeof m.review.reviewer !== 'string' || m.review.reviewer === m.review.author))))
|
|
30
|
+
throw new Error('Invalid review');
|
|
31
|
+
if (m.acknowledgedBy !== undefined && (m.status !== 'completed' || typeof m.acknowledgedBy !== 'string'))
|
|
32
|
+
throw new Error('Invalid acknowledgement');
|
|
33
|
+
if (m.result && m.status !== 'completed')
|
|
34
|
+
throw new Error('Result on unfinished mission');
|
|
35
|
+
if (m.handoff)
|
|
36
|
+
validateHandoff(m.handoff);
|
|
37
|
+
if (m.authority) {
|
|
38
|
+
bounded(m.authority.owner, 'owner', 256);
|
|
39
|
+
bounded(m.authority.generation, 'generation', 256);
|
|
40
|
+
}
|
|
41
|
+
if (['claimed', 'running'].includes(m.status) && (!m.authority || !Number.isFinite(m.expiresAt)))
|
|
42
|
+
throw new Error('Invalid mission lease');
|
|
43
|
+
}
|
|
44
|
+
function validateHandoff(handoff) {
|
|
45
|
+
if (!handoff)
|
|
46
|
+
throw new Error('Missing handoff');
|
|
47
|
+
bounded(handoff.nextAction, 'next action');
|
|
48
|
+
for (const entries of [handoff.succeeded, handoff.failed, handoff.keyFiles, handoff.deadEnds]) {
|
|
49
|
+
if (!Array.isArray(entries) || entries.length > 32)
|
|
50
|
+
throw new Error('Invalid handoff list');
|
|
51
|
+
for (const entry of entries)
|
|
52
|
+
bounded(entry, 'handoff item');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class MissionStore {
|
|
56
|
+
directory;
|
|
57
|
+
now;
|
|
58
|
+
constructor(directory, now = Date.now) {
|
|
59
|
+
this.directory = directory;
|
|
60
|
+
this.now = now;
|
|
61
|
+
}
|
|
62
|
+
file(id) {
|
|
63
|
+
bounded(id, 'mission id', 256);
|
|
64
|
+
return path.join(this.directory, `${createHash('sha256').update(id).digest('hex')}.json`);
|
|
65
|
+
}
|
|
66
|
+
get(id) {
|
|
67
|
+
const file = this.file(id);
|
|
68
|
+
const m = this.read(file);
|
|
69
|
+
if (m.id !== id)
|
|
70
|
+
throw new Error('Mission identity mismatch');
|
|
71
|
+
return m;
|
|
72
|
+
}
|
|
73
|
+
read(file) {
|
|
74
|
+
if (!fs.lstatSync(file).isFile())
|
|
75
|
+
throw new Error('Mission must be a regular file');
|
|
76
|
+
if (fs.statSync(file).size > 3_000_000)
|
|
77
|
+
throw new Error('Mission record exceeds size limit');
|
|
78
|
+
const m = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
79
|
+
validate(m);
|
|
80
|
+
if (path.basename(this.file(m.id)) !== path.basename(file))
|
|
81
|
+
throw new Error('Mission identity mismatch');
|
|
82
|
+
return m;
|
|
83
|
+
}
|
|
84
|
+
/** Metadata only in the result. Validate at most one bounded record per page slot. */
|
|
85
|
+
list(options = {}) {
|
|
86
|
+
const limit = options.limit ?? 25;
|
|
87
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
88
|
+
throw new Error('Mission list limit must be 1..100');
|
|
89
|
+
if (options.cursor !== undefined && !/^[a-f0-9]{64}$/.test(options.cursor))
|
|
90
|
+
throw new Error('Invalid mission list cursor');
|
|
91
|
+
const page = { missions: [], errors: [] };
|
|
92
|
+
let directory;
|
|
93
|
+
try {
|
|
94
|
+
directory = fs.opendirSync(this.directory);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code === 'ENOENT')
|
|
98
|
+
return page;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
// Scan names only; retain at most limit + 1 names regardless of store size.
|
|
102
|
+
const names = [];
|
|
103
|
+
try {
|
|
104
|
+
for (let entry = directory.readSync(); entry !== null; entry = directory.readSync()) {
|
|
105
|
+
if (!/^[a-f0-9]{64}\.json$/.test(entry.name) || (options.cursor !== undefined && entry.name.slice(0, 64) <= options.cursor))
|
|
106
|
+
continue;
|
|
107
|
+
if (names.length === limit + 1 && entry.name >= names[names.length - 1])
|
|
108
|
+
continue;
|
|
109
|
+
names.push(entry.name);
|
|
110
|
+
names.sort();
|
|
111
|
+
if (names.length > limit + 1)
|
|
112
|
+
names.pop();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
directory.closeSync();
|
|
117
|
+
}
|
|
118
|
+
const now = this.now();
|
|
119
|
+
for (const name of names.slice(0, limit)) {
|
|
120
|
+
try {
|
|
121
|
+
const m = this.read(path.join(this.directory, name));
|
|
122
|
+
const active = m.status === 'claimed' || m.status === 'running';
|
|
123
|
+
page.missions.push({
|
|
124
|
+
id: m.id, revision: m.revision, status: m.status, updatedAt: m.updatedAt,
|
|
125
|
+
...(m.authority ? { owner: m.authority.owner } : {}),
|
|
126
|
+
...(active ? { expiresAt: m.expiresAt, leaseExpired: m.expiresAt <= now } : {}),
|
|
127
|
+
...(m.result ? { success: m.result.success } : {}),
|
|
128
|
+
acknowledged: m.acknowledgedBy !== undefined,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Parser/validation errors can embed record contents. Never return them.
|
|
133
|
+
page.errors.push({ file: name, error: 'Invalid or unreadable mission record' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (names.length > limit)
|
|
137
|
+
page.nextCursor = names[limit - 1].slice(0, 64);
|
|
138
|
+
return page;
|
|
139
|
+
}
|
|
140
|
+
/** Synchronous critical section, exclusive across processes; never steal a mutation lock. */
|
|
141
|
+
change(id, update) {
|
|
142
|
+
fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
143
|
+
const file = this.file(id);
|
|
144
|
+
const lock = `${file}.lock`;
|
|
145
|
+
let fd;
|
|
146
|
+
try {
|
|
147
|
+
fd = fs.openSync(lock, 'wx', 0o600);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error.code === 'EEXIST')
|
|
151
|
+
throw new MissionBusyError();
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
let temp;
|
|
155
|
+
try {
|
|
156
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, createdAt: this.now() }));
|
|
157
|
+
const current = fs.existsSync(file) ? this.get(id) : undefined;
|
|
158
|
+
const next = update(current);
|
|
159
|
+
next.revision = (current?.revision ?? -1) + 1;
|
|
160
|
+
next.updatedAt = this.now();
|
|
161
|
+
validate(next);
|
|
162
|
+
const content = JSON.stringify(next);
|
|
163
|
+
if (Buffer.byteLength(content) > 3_000_000)
|
|
164
|
+
throw new Error('Mission record exceeds size limit');
|
|
165
|
+
temp = `${file}.${randomUUID()}.tmp`;
|
|
166
|
+
const out = fs.openSync(temp, 'wx', 0o600);
|
|
167
|
+
try {
|
|
168
|
+
fs.writeFileSync(out, content);
|
|
169
|
+
fs.fsyncSync(out);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
fs.closeSync(out);
|
|
173
|
+
}
|
|
174
|
+
fs.renameSync(temp, file);
|
|
175
|
+
if (process.platform !== 'win32') {
|
|
176
|
+
const dir = fs.openSync(this.directory, 'r');
|
|
177
|
+
try {
|
|
178
|
+
fs.fsyncSync(dir);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
fs.closeSync(dir);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return next;
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
if (temp && fs.existsSync(temp))
|
|
188
|
+
fs.unlinkSync(temp);
|
|
189
|
+
fs.closeSync(fd);
|
|
190
|
+
fs.unlinkSync(lock);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
create(id, operation) {
|
|
194
|
+
return this.change(id, current => {
|
|
195
|
+
if (current)
|
|
196
|
+
throw new Error('Mission already exists');
|
|
197
|
+
return { schema: 1, id, revision: 0, status: 'ready', operation, updatedAt: this.now() };
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
claim(id, owner, leaseMs = 60000) {
|
|
201
|
+
bounded(owner, 'owner', 256);
|
|
202
|
+
this.lease(leaseMs);
|
|
203
|
+
return this.change(id, m => {
|
|
204
|
+
if (!m)
|
|
205
|
+
throw new Error('Unknown mission');
|
|
206
|
+
if (m.status === 'completed')
|
|
207
|
+
throw new Error('Mission already completed');
|
|
208
|
+
if (m.status === 'running')
|
|
209
|
+
throw new Error('Running mission requires reconciliation before retry');
|
|
210
|
+
if (m.authority && (m.expiresAt ?? 0) > this.now())
|
|
211
|
+
throw new Error('Mission already claimed');
|
|
212
|
+
delete m.review;
|
|
213
|
+
m.authority = { owner, generation: randomUUID() };
|
|
214
|
+
m.expiresAt = this.now() + leaseMs;
|
|
215
|
+
m.status = 'claimed';
|
|
216
|
+
return m;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
lease(ms) { if (!Number.isInteger(ms) || ms < 1000 || ms > 3600000)
|
|
220
|
+
throw new Error('Lease must be 1000..3600000 ms'); }
|
|
221
|
+
owned(m, a) {
|
|
222
|
+
if (!m || m.authority?.owner !== a.owner || m.authority.generation !== a.generation || (m.expiresAt ?? 0) <= this.now())
|
|
223
|
+
throw new Error('Stale or expired mission authority');
|
|
224
|
+
}
|
|
225
|
+
renew(id, a, leaseMs = 60000) {
|
|
226
|
+
this.lease(leaseMs);
|
|
227
|
+
return this.change(id, m => { this.owned(m, a); if (!['claimed', 'running'].includes(m.status))
|
|
228
|
+
throw new Error('Mission is not active'); m.expiresAt = this.now() + leaseMs; return m; });
|
|
229
|
+
}
|
|
230
|
+
start(id, a) {
|
|
231
|
+
return this.change(id, m => { this.owned(m, a); if (m.status !== 'claimed')
|
|
232
|
+
throw new Error('Mission already started'); m.status = 'running'; delete m.review; return m; });
|
|
233
|
+
}
|
|
234
|
+
complete(id, a, result) {
|
|
235
|
+
return this.change(id, m => { this.owned(m, a); if (m.status !== 'running')
|
|
236
|
+
throw new Error('Mission is not running'); m.result = { ...result, stdout: result.stdout.slice(0, 100000), stderr: result.stderr.slice(0, 100000), truncated: result.stdout.length > 100000 || result.stderr.length > 100000 }; m.status = 'completed'; delete m.expiresAt; return m; });
|
|
237
|
+
}
|
|
238
|
+
handoff(id, a, handoff) {
|
|
239
|
+
validateHandoff(handoff);
|
|
240
|
+
return this.change(id, m => { this.owned(m, a); if (m.status !== 'claimed')
|
|
241
|
+
throw new Error('Stop execution before handoff'); m.handoff = handoff; m.status = 'handoff'; delete m.review; delete m.authority; delete m.expiresAt; return m; });
|
|
242
|
+
}
|
|
243
|
+
/** Explicit operator reconciliation only; an ambiguous effect is never retried automatically. */
|
|
244
|
+
reconcile(id, generation, resolution, note) {
|
|
245
|
+
bounded(note, 'reconciliation evidence');
|
|
246
|
+
if (!['retry', 'completed'].includes(resolution))
|
|
247
|
+
throw new Error('Invalid reconciliation resolution');
|
|
248
|
+
return this.change(id, m => {
|
|
249
|
+
if (!m || m.status !== 'running' || m.authority?.generation !== generation || (m.expiresAt ?? 0) > this.now())
|
|
250
|
+
throw new Error('Only expired running missions can be reconciled');
|
|
251
|
+
m.handoff = { succeeded: [], failed: [], keyFiles: [], deadEnds: [], nextAction: note };
|
|
252
|
+
m.status = resolution === 'retry' ? 'ready' : 'completed';
|
|
253
|
+
delete m.review;
|
|
254
|
+
delete m.authority;
|
|
255
|
+
delete m.expiresAt;
|
|
256
|
+
return m;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
acknowledge(id, consumer) {
|
|
260
|
+
bounded(consumer, 'consumer', 256);
|
|
261
|
+
return this.change(id, m => { if (!m || m.status !== 'completed')
|
|
262
|
+
throw new Error('Mission is not completed'); m.acknowledgedBy = consumer; return m; });
|
|
263
|
+
}
|
|
264
|
+
submit(id, a, commit) {
|
|
265
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit))
|
|
266
|
+
throw new Error('Expected full commit SHA');
|
|
267
|
+
return this.change(id, m => {
|
|
268
|
+
if (m?.status === 'completed') {
|
|
269
|
+
if (m.authority?.owner !== a.owner || m.authority.generation !== a.generation)
|
|
270
|
+
throw new Error('Stale mission authority');
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
this.owned(m, a);
|
|
274
|
+
}
|
|
275
|
+
if (!['claimed', 'completed'].includes(m.status))
|
|
276
|
+
throw new Error('Review requires a claimed or completed mission');
|
|
277
|
+
m.review = { commit, author: a.owner };
|
|
278
|
+
return m;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
approve(id, reviewer, expectedCommit) {
|
|
282
|
+
bounded(reviewer, 'reviewer', 256);
|
|
283
|
+
return this.change(id, m => {
|
|
284
|
+
if (!m || !['claimed', 'completed'].includes(m.status) || (m.status === 'claimed' && (m.expiresAt ?? 0) <= this.now()))
|
|
285
|
+
throw new Error('Review requires active ownership or a completed mission');
|
|
286
|
+
const actualCommit = execFileSync('git', ['rev-parse', '--verify', 'HEAD'], { cwd: m.operation.workspace, encoding: 'utf8', timeout: 5000 }).trim();
|
|
287
|
+
const dirty = execFileSync('git', ['status', '--porcelain', '--untracked-files=normal'], { cwd: m.operation.workspace, encoding: 'utf8', timeout: 5000 }).trim();
|
|
288
|
+
if (dirty)
|
|
289
|
+
throw new Error('Review requires a clean worktree');
|
|
290
|
+
if (!m?.review || m.review.author === reviewer || m.review.commit !== expectedCommit || actualCommit !== expectedCommit)
|
|
291
|
+
throw new Error('Review requires an independent reviewer and unchanged commit');
|
|
292
|
+
m.review.reviewer = reviewer;
|
|
293
|
+
return m;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=mission-store.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ToolResult } from '../types/index.js';
|
|
2
|
+
import type { CodeBuddyTool } from '../codebuddy/tool-definitions/types.js';
|
|
3
|
+
export interface ToolHarnessOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
tools: readonly CodeBuddyTool[];
|
|
6
|
+
/** Must use the host's normal permission and validation pipeline. */
|
|
7
|
+
dispatch: (name: string, args: Record<string, unknown>, signal: AbortSignal) => Promise<ToolResult>;
|
|
8
|
+
/** Only tools proven safe for concurrent reads; other calls form FIFO barriers. */
|
|
9
|
+
parallelTools?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export interface HarnessWaitResult {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
status: 'running' | 'completed';
|
|
14
|
+
output: string;
|
|
15
|
+
result?: ToolResult;
|
|
16
|
+
}
|
|
17
|
+
/** Model-independent tool harness. No LLM client, process-global cwd, or global tool index. */
|
|
18
|
+
export declare class ToolHarness {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private readonly id;
|
|
21
|
+
private readonly catalog;
|
|
22
|
+
private readonly tools;
|
|
23
|
+
private readonly executions;
|
|
24
|
+
private disposed;
|
|
25
|
+
readonly cwd: string;
|
|
26
|
+
constructor(options: ToolHarnessOptions);
|
|
27
|
+
search(query: string, maxResults?: number): {
|
|
28
|
+
parameters: unknown;
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
score: number;
|
|
32
|
+
}[];
|
|
33
|
+
call(name: string, args?: Record<string, unknown>, signal?: AbortSignal): Promise<ToolResult>;
|
|
34
|
+
/** Start an isolated JavaScript cell. Effects always route through call(). */
|
|
35
|
+
start(code: string, options?: {
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
typecheck?: boolean;
|
|
38
|
+
}): string;
|
|
39
|
+
/** Consume only new explicit yield output; the final result includes the complete bounded output. */
|
|
40
|
+
wait(sessionId: string, yieldTimeMs?: number): Promise<HarnessWaitResult>;
|
|
41
|
+
cancel(sessionId: string): void;
|
|
42
|
+
exec(code: string, options?: {
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
typecheck?: boolean;
|
|
46
|
+
}): Promise<ToolResult>;
|
|
47
|
+
dispose(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
/** Bind an existing agent, preserving ToolHandler's policy, approvals and project scope. */
|
|
50
|
+
export declare function createAgentToolHarness(agent: {
|
|
51
|
+
getMemoryScope(): {
|
|
52
|
+
cwd: string;
|
|
53
|
+
botId?: string;
|
|
54
|
+
};
|
|
55
|
+
executeToolByName(name: string, args: Record<string, unknown>, extra?: Record<string, unknown>): Promise<ToolResult>;
|
|
56
|
+
}, tools?: readonly CodeBuddyTool[]): Promise<ToolHarness>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { BM25Index } from '../tools/tool-search.js';
|
|
4
|
+
import { CodeExecTool, attachCodeExecRuntime, clearCodeExecSession } from '../tools/code-exec-tool.js';
|
|
5
|
+
import { TOOL_METADATA } from '../tools/metadata.js';
|
|
6
|
+
import { BoundedOutput } from '../utils/bounded-output.js';
|
|
7
|
+
/** Model-independent tool harness. No LLM client, process-global cwd, or global tool index. */
|
|
8
|
+
export class ToolHarness {
|
|
9
|
+
options;
|
|
10
|
+
id = randomUUID();
|
|
11
|
+
catalog = new BM25Index();
|
|
12
|
+
tools;
|
|
13
|
+
executions = new Map();
|
|
14
|
+
disposed = false;
|
|
15
|
+
cwd;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.cwd = path.resolve(options.cwd);
|
|
19
|
+
this.tools = new Map(options.tools.filter(tool => !['code_exec', 'exec'].includes(tool.function.name))
|
|
20
|
+
.map(tool => [tool.function.name, tool]));
|
|
21
|
+
this.catalog.index([...this.tools.values()].map(tool => ({
|
|
22
|
+
name: tool.function.name, description: tool.function.description ?? '', parameters: tool.function.parameters,
|
|
23
|
+
})));
|
|
24
|
+
}
|
|
25
|
+
search(query, maxResults = 10) {
|
|
26
|
+
return this.catalog.search(query, maxResults).map(result => ({ ...result, parameters: this.catalog.getTool(result.name)?.parameters }));
|
|
27
|
+
}
|
|
28
|
+
async call(name, args = {}, signal = new AbortController().signal) {
|
|
29
|
+
if (this.disposed || signal.aborted)
|
|
30
|
+
return { success: false, error: 'Harness call cancelled' };
|
|
31
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
32
|
+
return { success: false, error: 'Tool arguments must be an object' };
|
|
33
|
+
if (name === 'tool_search') {
|
|
34
|
+
if (typeof args.query !== 'string' || !args.query.trim() || args.query.length > 2000)
|
|
35
|
+
return { success: false, error: 'query must be a nonempty string of at most 2000 characters' };
|
|
36
|
+
if (args.max_results !== undefined && (typeof args.max_results !== 'number' || !Number.isInteger(args.max_results) || args.max_results < 1 || args.max_results > 50))
|
|
37
|
+
return { success: false, error: 'max_results must be an integer between 1 and 50' };
|
|
38
|
+
const tools = this.search(args.query, typeof args.max_results === 'number' ? args.max_results : 10);
|
|
39
|
+
return { success: true, output: JSON.stringify(tools), data: { tools, names: tools.map(tool => tool.name) } };
|
|
40
|
+
}
|
|
41
|
+
if (!this.tools.has(name))
|
|
42
|
+
return { success: false, error: `Tool is not available: ${name}` };
|
|
43
|
+
try {
|
|
44
|
+
return await this.options.dispatch(name, args, signal);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Start an isolated JavaScript cell. Effects always route through call(). */
|
|
51
|
+
start(code, options = {}) {
|
|
52
|
+
if (this.disposed)
|
|
53
|
+
throw new Error('Harness is disposed');
|
|
54
|
+
for (const [id, execution] of this.executions) {
|
|
55
|
+
if (this.executions.size < 32)
|
|
56
|
+
break;
|
|
57
|
+
if (execution.result && !execution.waiting)
|
|
58
|
+
this.executions.delete(id);
|
|
59
|
+
}
|
|
60
|
+
if (this.executions.size >= 32)
|
|
61
|
+
throw new Error('Harness execution limit reached');
|
|
62
|
+
const sessionId = randomUUID();
|
|
63
|
+
const execution = {
|
|
64
|
+
controller: new AbortController(), output: new BoundedOutput(64 * 1024), completed: Promise.resolve(), waiting: false,
|
|
65
|
+
};
|
|
66
|
+
const context = attachCodeExecRuntime({ cwd: this.cwd, sessionId: this.id }, {
|
|
67
|
+
scopeId: `harness:${this.id}`, sessionId: this.id, cwd: this.cwd,
|
|
68
|
+
availableTools: [...new Set([...this.tools.keys(), 'tool_search'])],
|
|
69
|
+
toolCatalog: [...this.tools.values()].map(tool => ({ name: tool.function.name, description: tool.function.description, parameters: tool.function.parameters })),
|
|
70
|
+
toolMetadata: [...this.tools.values()].map(tool => ({ name: tool.function.name, description: tool.function.description ?? '' })),
|
|
71
|
+
parallelTools: this.options.parallelTools,
|
|
72
|
+
executor: (name, args, signal) => this.call(name, args, signal),
|
|
73
|
+
abortSignal: execution.controller.signal,
|
|
74
|
+
onOutput: delta => { execution.output.append(delta); execution.notify?.(); },
|
|
75
|
+
});
|
|
76
|
+
execution.completed = new CodeExecTool().execute({ code, typecheck: options.typecheck, ...(options.timeoutMs !== undefined ? { timeout_ms: options.timeoutMs } : {}) }, context)
|
|
77
|
+
.then(result => { execution.result = result; })
|
|
78
|
+
.catch(error => { execution.result = { success: false, error: error instanceof Error ? error.message : String(error) }; });
|
|
79
|
+
this.executions.set(sessionId, execution);
|
|
80
|
+
return sessionId;
|
|
81
|
+
}
|
|
82
|
+
/** Consume only new explicit yield output; the final result includes the complete bounded output. */
|
|
83
|
+
async wait(sessionId, yieldTimeMs = 1000) {
|
|
84
|
+
const execution = this.executions.get(sessionId);
|
|
85
|
+
if (!execution)
|
|
86
|
+
throw new Error('Unknown harness execution');
|
|
87
|
+
if (execution.waiting)
|
|
88
|
+
throw new Error('Only one consumer may wait on an execution');
|
|
89
|
+
if (!Number.isFinite(yieldTimeMs) || yieldTimeMs < 0 || yieldTimeMs > 60000)
|
|
90
|
+
throw new Error('yieldTimeMs must be between 0 and 60000');
|
|
91
|
+
execution.waiting = true;
|
|
92
|
+
let timer;
|
|
93
|
+
try {
|
|
94
|
+
if (!execution.result && !execution.output.retainedBytes) {
|
|
95
|
+
await Promise.race([execution.completed, new Promise(resolve => { execution.notify = resolve; timer = setTimeout(resolve, yieldTimeMs); })]);
|
|
96
|
+
}
|
|
97
|
+
return { sessionId, status: execution.result ? 'completed' : 'running', output: execution.output.drain(), ...(execution.result ? { result: execution.result } : {}) };
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
execution.waiting = false;
|
|
102
|
+
execution.notify = undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
cancel(sessionId) {
|
|
106
|
+
const execution = this.executions.get(sessionId);
|
|
107
|
+
if (!execution)
|
|
108
|
+
throw new Error('Unknown harness execution');
|
|
109
|
+
execution.controller.abort();
|
|
110
|
+
}
|
|
111
|
+
async exec(code, options = {}) {
|
|
112
|
+
if (options.signal?.aborted)
|
|
113
|
+
return { success: false, error: 'Harness call cancelled' };
|
|
114
|
+
const sessionId = this.start(code, options);
|
|
115
|
+
const execution = this.executions.get(sessionId);
|
|
116
|
+
const abort = () => execution.controller.abort();
|
|
117
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
118
|
+
if (options.signal?.aborted)
|
|
119
|
+
abort();
|
|
120
|
+
try {
|
|
121
|
+
await execution.completed;
|
|
122
|
+
return execution.result;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
options.signal?.removeEventListener('abort', abort);
|
|
126
|
+
this.executions.delete(sessionId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async dispose() {
|
|
130
|
+
this.disposed = true;
|
|
131
|
+
for (const execution of this.executions.values())
|
|
132
|
+
execution.controller.abort();
|
|
133
|
+
await Promise.all([...this.executions.values()].map(execution => execution.completed));
|
|
134
|
+
this.executions.clear();
|
|
135
|
+
clearCodeExecSession(this.id);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Bind an existing agent, preserving ToolHandler's policy, approvals and project scope. */
|
|
139
|
+
export async function createAgentToolHarness(agent, tools) {
|
|
140
|
+
const catalog = tools ?? await (await import('../codebuddy/tools.js')).getAllCodeBuddyTools();
|
|
141
|
+
const scope = agent.getMemoryScope();
|
|
142
|
+
const cwd = path.resolve(scope.cwd);
|
|
143
|
+
const botId = scope.botId;
|
|
144
|
+
return new ToolHarness({ cwd, tools: catalog, parallelTools: TOOL_METADATA.filter(tool => tool.fleetSafe === true && tool.effect === 'read').map(tool => tool.name), dispatch: (name, args, abortSignal) => {
|
|
145
|
+
const currentScope = agent.getMemoryScope();
|
|
146
|
+
if (path.resolve(currentScope.cwd) !== cwd)
|
|
147
|
+
return Promise.resolve({ success: false, error: 'Agent workspace changed; create a new harness' });
|
|
148
|
+
if (currentScope.botId !== botId)
|
|
149
|
+
return Promise.resolve({ success: false, error: 'Agent bot changed; create a new harness' });
|
|
150
|
+
return agent.executeToolByName(name, args, { abortSignal });
|
|
151
|
+
} });
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=tool-harness.js.map
|
|
@@ -3,8 +3,18 @@ import { deleteCharBefore, deleteCharAfter, deleteWordBefore, deleteWordAfter, i
|
|
|
3
3
|
import { useInputHistory } from "./use-input-history.js";
|
|
4
4
|
import { getHistoryManager } from "../utils/history-manager.js";
|
|
5
5
|
export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, disabled = false, multiline = false, } = {}) {
|
|
6
|
-
const [input,
|
|
7
|
-
const
|
|
6
|
+
const [input, updateInput] = useState('');
|
|
7
|
+
const inputRef = useRef('');
|
|
8
|
+
const setInputState = useCallback((text) => {
|
|
9
|
+
inputRef.current = text;
|
|
10
|
+
updateInput(text);
|
|
11
|
+
}, []);
|
|
12
|
+
const [cursorPosition, updateCursor] = useState(0);
|
|
13
|
+
const cursorRef = useRef(0);
|
|
14
|
+
const setCursorPositionState = useCallback((position) => {
|
|
15
|
+
cursorRef.current = position;
|
|
16
|
+
updateCursor(position);
|
|
17
|
+
}, []);
|
|
8
18
|
const [isReverseSearchActive, setIsReverseSearchActive] = useState(false);
|
|
9
19
|
const [reverseSearchPrompt, setReverseSearchPrompt] = useState("");
|
|
10
20
|
const isMultilineRef = useRef(multiline);
|
|
@@ -18,7 +28,7 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, disabled =
|
|
|
18
28
|
}
|
|
19
29
|
}, [cursorPosition, isNavigatingHistory, setOriginalInput]);
|
|
20
30
|
const setCursorPosition = useCallback((position) => {
|
|
21
|
-
setCursorPositionState(Math.max(0, Math.min(
|
|
31
|
+
setCursorPositionState(Math.max(0, Math.min(inputRef.current.length, position)));
|
|
22
32
|
}, [input.length]);
|
|
23
33
|
const clearInput = useCallback(() => {
|
|
24
34
|
setInputState("");
|
|
@@ -26,12 +36,13 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, disabled =
|
|
|
26
36
|
setOriginalInput("");
|
|
27
37
|
}, [setOriginalInput]);
|
|
28
38
|
const insertAtCursor = useCallback((text) => {
|
|
29
|
-
const result = insertText(
|
|
39
|
+
const result = insertText(inputRef.current, cursorRef.current, text);
|
|
30
40
|
setInputState(result.text);
|
|
31
41
|
setCursorPositionState(result.position);
|
|
32
42
|
setOriginalInput(result.text);
|
|
33
43
|
}, [input, cursorPosition, setOriginalInput]);
|
|
34
44
|
const handleSubmit = useCallback(() => {
|
|
45
|
+
const input = inputRef.current;
|
|
35
46
|
if (input.trim()) {
|
|
36
47
|
addToHistory(input);
|
|
37
48
|
onSubmit?.(input);
|
|
@@ -41,6 +52,25 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, disabled =
|
|
|
41
52
|
const handleInput = useCallback((inputChar, key) => {
|
|
42
53
|
if (disabled)
|
|
43
54
|
return;
|
|
55
|
+
// Read synchronously updated refs: multiple events may arrive before React
|
|
56
|
+
// commits a render (fast typing, Windows Terminal and pasted input).
|
|
57
|
+
const input = inputRef.current;
|
|
58
|
+
const cursorPosition = cursorRef.current;
|
|
59
|
+
// Ink 4 parses CRLF as Ctrl+M, and LF (Ctrl+J) as literal text.
|
|
60
|
+
// Normalize Enter without executing embedded newlines in pasted content.
|
|
61
|
+
if (inputChar === '\r\n' || (key.ctrl && inputChar === 'm')) {
|
|
62
|
+
key = { ...key, return: true };
|
|
63
|
+
}
|
|
64
|
+
if (inputChar === '\n' || (key.ctrl && inputChar === 'j')) {
|
|
65
|
+
const result = insertText(input, cursorPosition, '\n');
|
|
66
|
+
setInputState(result.text);
|
|
67
|
+
setCursorPositionState(result.position);
|
|
68
|
+
setOriginalInput(result.text);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (inputChar.length > 1 && !key.ctrl && !key.meta) {
|
|
72
|
+
inputChar = inputChar.replace(/\r\n?/g, '\n');
|
|
73
|
+
}
|
|
44
74
|
// Handle Ctrl+C - check multiple ways it could be detected
|
|
45
75
|
if ((key.ctrl && inputChar === "c") || inputChar === "\x03") {
|
|
46
76
|
setInputState("");
|
|
@@ -71,6 +101,20 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, disabled =
|
|
|
71
101
|
}
|
|
72
102
|
return;
|
|
73
103
|
}
|
|
104
|
+
// In a multiline draft, arrows move within the draft instead of replacing
|
|
105
|
+
// it with history. History remains available on a single-line prompt.
|
|
106
|
+
if (input.includes('\n') && !isNavigatingHistory() && (key.upArrow || key.downArrow) && !key.ctrl && !key.meta) {
|
|
107
|
+
const before = input.slice(0, cursorPosition).split('\n');
|
|
108
|
+
const row = before.length - 1;
|
|
109
|
+
const column = before[row]?.length ?? 0;
|
|
110
|
+
const lines = input.split('\n');
|
|
111
|
+
const target = row + (key.upArrow ? -1 : 1);
|
|
112
|
+
if (target >= 0 && target < lines.length) {
|
|
113
|
+
const offset = lines.slice(0, target).reduce((sum, line) => sum + line.length + 1, 0);
|
|
114
|
+
setCursorPositionState(offset + Math.min(column, lines[target]?.length ?? 0));
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
74
118
|
// Handle history navigation
|
|
75
119
|
if ((key.upArrow || key.name === 'up') && !key.ctrl && !key.meta) {
|
|
76
120
|
const historyInput = navigateHistory("up");
|
|
@@ -38,6 +38,7 @@ export declare function useInputHandler({ agent, chatHistory, setChatHistory, se
|
|
|
38
38
|
availableModels: ModelOption[];
|
|
39
39
|
agent: CodeBuddyAgent;
|
|
40
40
|
autoEditEnabled: boolean;
|
|
41
|
+
queuedMessageCount: number;
|
|
41
42
|
handleInputSubmit: (userInput: string) => Promise<void>;
|
|
42
43
|
};
|
|
43
44
|
export {};
|