mixdog 0.7.1
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/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Async one-shot shell runner.
|
|
3
|
+
//
|
|
4
|
+
// Replaces the legacy spawnSync path in builtin.mjs case 'bash'. The
|
|
5
|
+
// improvements over spawnSync are:
|
|
6
|
+
// - tree-kill on timeout / abort (Windows taskkill /T /F, POSIX process
|
|
7
|
+
// group SIGTERM->SIGKILL escalation) so forked children come down with
|
|
8
|
+
// the parent shell instead of being orphaned holding pipes.
|
|
9
|
+
// - automatic spill to $PLUGIN_DATA/shell-output/<taskId>.* once the
|
|
10
|
+
// in-memory buffers exceed SHELL_OUTPUT_INLINE_CAP*4 bytes. The caller
|
|
11
|
+
// receives an outputFilePath marker the model can FileRead later
|
|
12
|
+
// instead of losing the tail past the inline cap.
|
|
13
|
+
// - external AbortSignal hookup so a session-scoped abort (ESC, new
|
|
14
|
+
// prompt) cancels in-flight bash work without orphaning the child.
|
|
15
|
+
//
|
|
16
|
+
// Persistent shells in bash-session.mjs keep their separate stdin-marker
|
|
17
|
+
// protocol — that runner is stateful and uses a different model entirely.
|
|
18
|
+
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import {
|
|
21
|
+
mkdirSync,
|
|
22
|
+
openSync,
|
|
23
|
+
closeSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
readSync,
|
|
26
|
+
writeSync,
|
|
27
|
+
fsyncSync,
|
|
28
|
+
unlinkSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from 'node:fs';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
import { randomUUID } from 'node:crypto';
|
|
33
|
+
import * as nodeUtil from 'node:util';
|
|
34
|
+
import { getPluginData } from '../config.mjs';
|
|
35
|
+
// Runtime-only import (used inside execShellCommand's auto-background
|
|
36
|
+
// transition). shell-jobs.mjs imports stripAnsi from this module, so this is
|
|
37
|
+
// a static cycle — safe because neither binding is touched at module-eval
|
|
38
|
+
// time, only when the respective functions actually run.
|
|
39
|
+
import { adoptForegroundShellJob } from './builtin/shell-jobs.mjs';
|
|
40
|
+
|
|
41
|
+
// Inline cap. Output above this size is spilled to disk and the caller
|
|
42
|
+
// renders a path marker instead of pasting the tail. Matches the
|
|
43
|
+
// SHELL_OUTPUT_MAX_CHARS used by the smart-truncate renderer in
|
|
44
|
+
// builtin.mjs so spilled output and inline output share the same boundary.
|
|
45
|
+
const SHELL_OUTPUT_INLINE_CAP = 30_000;
|
|
46
|
+
|
|
47
|
+
// Hard ceiling on disk-backed output. Past this the SIZE_WATCHDOG (G2)
|
|
48
|
+
// SIGKILLs the child to avoid filling the filesystem. 100 MB is generous
|
|
49
|
+
// for any legitimate command output and tight enough to catch a runaway
|
|
50
|
+
// loop within ~seconds on a typical SSD.
|
|
51
|
+
const SHELL_OUTPUT_DISK_CAP = 100 * 1024 * 1024;
|
|
52
|
+
|
|
53
|
+
// Background-task disk watchdog cadence. The size guard polls the spilled
|
|
54
|
+
// stdout/stderr files every interval and SIGKILLs the child once the
|
|
55
|
+
// combined size exceeds SHELL_OUTPUT_DISK_CAP. 5 s matches Claude Code's
|
|
56
|
+
// upstream cadence — short enough that a runaway loop is caught within a
|
|
57
|
+
// few seconds, long enough that the stat overhead is negligible.
|
|
58
|
+
const SIZE_WATCHDOG_INTERVAL_MS = 1_000;
|
|
59
|
+
|
|
60
|
+
// ANSI / VT control sequence stripper. Falls back to a regex sweep when
|
|
61
|
+
// node:util's stripVTControlCharacters isn't available (older Node).
|
|
62
|
+
const _ANSI_REGEX =
|
|
63
|
+
/(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:|\\|))/g;
|
|
64
|
+
const _stripAnsiImpl =
|
|
65
|
+
typeof nodeUtil.stripVTControlCharacters === 'function'
|
|
66
|
+
? (s) => nodeUtil.stripVTControlCharacters(s)
|
|
67
|
+
: (s) => String(s).replace(_ANSI_REGEX, () => '');
|
|
68
|
+
|
|
69
|
+
export function stripAnsi(s) {
|
|
70
|
+
if (typeof s !== 'string' || s.length === 0) return s;
|
|
71
|
+
return _stripAnsiImpl(s);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Tree-kill helper. spawn alone only signals the direct child, so a
|
|
75
|
+
// `sleep 1000 &` or a forked node server inside the shell stays alive
|
|
76
|
+
// holding the pipes open. POSIX path signals the process group (we spawn
|
|
77
|
+
// with detached:true to give the child its own pgid). Windows uses
|
|
78
|
+
// taskkill /T /F to walk the tree. Safe to call repeatedly; all errors
|
|
79
|
+
// swallowed.
|
|
80
|
+
function treeKill(child) {
|
|
81
|
+
if (!child) return;
|
|
82
|
+
// Track close/exit via the standard child fields (set by Node when
|
|
83
|
+
// the corresponding events fire) instead of `child.killed`, which is
|
|
84
|
+
// true the moment any signal is delivered — even before the child has
|
|
85
|
+
// actually terminated. Using exitCode/signalCode means the SIGKILL
|
|
86
|
+
// escalation only suppresses itself when the process is genuinely
|
|
87
|
+
// gone.
|
|
88
|
+
if (child.exitCode != null || child.signalCode != null) return;
|
|
89
|
+
const pid = child.pid;
|
|
90
|
+
if (!pid) return;
|
|
91
|
+
try {
|
|
92
|
+
if (process.platform === 'win32') {
|
|
93
|
+
spawn('taskkill', ['/pid', String(pid), '/t', '/f'], {
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
stdio: 'ignore',
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
try {
|
|
99
|
+
process.kill(-pid, 'SIGTERM');
|
|
100
|
+
} catch {
|
|
101
|
+
try {
|
|
102
|
+
child.kill('SIGTERM');
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
// Escalate to SIGKILL after 3s so a child that ignores SIGTERM
|
|
106
|
+
// still comes down. Windows taskkill /F is already forceful so
|
|
107
|
+
// skip the escalation timer there.
|
|
108
|
+
const esc = setTimeout(() => {
|
|
109
|
+
if (child.exitCode != null || child.signalCode != null) return;
|
|
110
|
+
try {
|
|
111
|
+
process.kill(-pid, 'SIGKILL');
|
|
112
|
+
} catch {
|
|
113
|
+
try {
|
|
114
|
+
child.kill('SIGKILL');
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
}, 3000);
|
|
118
|
+
if (esc.unref) esc.unref();
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
/* swallow */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Head+tail read helper: avoid pulling a large spill back into memory, but
|
|
126
|
+
// preserve BOTH the start of the output (where build / compiler / test errors
|
|
127
|
+
// are usually printed first) and the most recent output. Past INLINE_CAP*4
|
|
128
|
+
// bytes we return the first half-budget and the trailing half-budget with an
|
|
129
|
+
// elision marker between; below that the full body is returned as-is. A tail-
|
|
130
|
+
// only slice silently dropped early diagnostics. UTF-8 sequences are at most
|
|
131
|
+
// 4 B, so a small padding window lets us cut/advance on codepoint boundaries
|
|
132
|
+
// instead of emitting a U+FFFD glyph at the seam.
|
|
133
|
+
function _readHeadTail(filePath, fileSize) {
|
|
134
|
+
if (fileSize <= SHELL_OUTPUT_INLINE_CAP * 4) {
|
|
135
|
+
return readFileSync(filePath, 'utf-8');
|
|
136
|
+
}
|
|
137
|
+
const padding = 4;
|
|
138
|
+
const headBudget = Math.floor(SHELL_OUTPUT_INLINE_CAP / 2);
|
|
139
|
+
const tailBudget = SHELL_OUTPUT_INLINE_CAP - headBudget;
|
|
140
|
+
const fd = openSync(filePath, 'r');
|
|
141
|
+
try {
|
|
142
|
+
// Head: first headBudget bytes, dropping a split trailing codepoint.
|
|
143
|
+
const headBuf = Buffer.allocUnsafe(headBudget + padding);
|
|
144
|
+
const hn = readSync(fd, headBuf, 0, headBudget + padding, 0);
|
|
145
|
+
let hEnd = Math.min(headBudget, hn);
|
|
146
|
+
while (hEnd > 0 && hEnd < hn && (headBuf[hEnd] & 0xC0) === 0x80) hEnd--;
|
|
147
|
+
const head = headBuf.slice(0, hEnd).toString('utf-8');
|
|
148
|
+
// Tail: last tailBudget bytes, advancing past a leading split codepoint.
|
|
149
|
+
const tailReadSize = tailBudget + padding;
|
|
150
|
+
const tailStart = Math.max(hEnd, fileSize - tailReadSize);
|
|
151
|
+
const tailBuf = Buffer.allocUnsafe(tailReadSize);
|
|
152
|
+
const tn = readSync(fd, tailBuf, 0, tailReadSize, tailStart);
|
|
153
|
+
let tOff = 0;
|
|
154
|
+
if (tailStart > 0) {
|
|
155
|
+
while (tOff < tn && tOff < padding && (tailBuf[tOff] & 0xC0) === 0x80) tOff++;
|
|
156
|
+
}
|
|
157
|
+
const tail = tailBuf.slice(tOff, tn).toString('utf-8');
|
|
158
|
+
const elided = Math.max(0, (tailStart + tOff) - hEnd);
|
|
159
|
+
return `${head}\n... [${elided} bytes elided of ${fileSize} total — head+tail shown; full output spilled to disk] ...\n${tail}`;
|
|
160
|
+
} finally {
|
|
161
|
+
try { closeSync(fd); } catch {}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Owns the captured stdout/stderr buffers for a single command run. Starts
|
|
166
|
+
// fully in memory; once the combined byte total exceeds the spill threshold
|
|
167
|
+
// (SHELL_OUTPUT_INLINE_CAP*4), opens append-only files in
|
|
168
|
+
// $PLUGIN_DATA/shell-output/ and from then on writes go straight to disk.
|
|
169
|
+
// On settle, the caller (execShellCommand) decides whether to keep the
|
|
170
|
+
// spilled files based on the final size.
|
|
171
|
+
class TaskOutput {
|
|
172
|
+
constructor(taskId) {
|
|
173
|
+
this.taskId = taskId;
|
|
174
|
+
this.stdoutBuf = '';
|
|
175
|
+
this.stderrBuf = '';
|
|
176
|
+
this._inlineBytes = 0;
|
|
177
|
+
this.stdoutFd = null;
|
|
178
|
+
this.stderrFd = null;
|
|
179
|
+
this.stdoutPath = null;
|
|
180
|
+
this.stderrPath = null;
|
|
181
|
+
this.spilled = false;
|
|
182
|
+
this.stdoutFileSize = 0;
|
|
183
|
+
this.stderrFileSize = 0;
|
|
184
|
+
this.writeError = null;
|
|
185
|
+
// fsync throttle: job_wait + tail-read polling can call getStdout/
|
|
186
|
+
// getStderr many times per second. Every call used to fsyncSync(fd),
|
|
187
|
+
// a noticeable I/O tax on Windows. Skip if a recent fsync landed
|
|
188
|
+
// within 200ms — the next read still picks up writes via the kernel's
|
|
189
|
+
// normal write-back. Final settle (closeFds) flushes via close anyway.
|
|
190
|
+
this._lastStdoutFsyncMs = 0;
|
|
191
|
+
this._lastStderrFsyncMs = 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_ensureFileBacking() {
|
|
195
|
+
if (this.spilled) return;
|
|
196
|
+
const dir = join(getPluginData(), 'shell-output');
|
|
197
|
+
try {
|
|
198
|
+
mkdirSync(dir, { recursive: true });
|
|
199
|
+
} catch {}
|
|
200
|
+
this.stdoutPath = join(dir, `${this.taskId}.stdout`);
|
|
201
|
+
this.stderrPath = join(dir, `${this.taskId}.stderr`);
|
|
202
|
+
// openSync failure (EMFILE, EACCES, ENOSPC, ENOTDIR after a race) used
|
|
203
|
+
// to throw straight up into the stream `data` handler, which left the
|
|
204
|
+
// child running with no further writes captured. Catch + record so the
|
|
205
|
+
// run settles cleanly under inline-only mode; the partial buffer in
|
|
206
|
+
// stdoutBuf/stderrBuf survives.
|
|
207
|
+
try {
|
|
208
|
+
this.stdoutFd = openSync(this.stdoutPath, 'a');
|
|
209
|
+
this.stderrFd = openSync(this.stderrPath, 'a');
|
|
210
|
+
} catch (err) {
|
|
211
|
+
this._recordWriteError('spill-open', err);
|
|
212
|
+
if (this.stdoutFd != null) {
|
|
213
|
+
try { closeSync(this.stdoutFd); } catch {}
|
|
214
|
+
this.stdoutFd = null;
|
|
215
|
+
}
|
|
216
|
+
this.stderrFd = null;
|
|
217
|
+
this.stdoutPath = null;
|
|
218
|
+
this.stderrPath = null;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (this.stdoutBuf) {
|
|
222
|
+
try {
|
|
223
|
+
writeSync(this.stdoutFd, this.stdoutBuf);
|
|
224
|
+
this.stdoutFileSize += Buffer.byteLength(this.stdoutBuf, 'utf-8');
|
|
225
|
+
} catch (err) {
|
|
226
|
+
this._recordWriteError('stdout-spill-flush', err);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (this.stderrBuf) {
|
|
230
|
+
try {
|
|
231
|
+
writeSync(this.stderrFd, this.stderrBuf);
|
|
232
|
+
this.stderrFileSize += Buffer.byteLength(this.stderrBuf, 'utf-8');
|
|
233
|
+
} catch (err) {
|
|
234
|
+
this._recordWriteError('stderr-spill-flush', err);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
this.spilled = true;
|
|
238
|
+
// The flushed bytes now live in the spill files and getStdout/getStderr
|
|
239
|
+
// read from disk once spilled — drop the inline copies.
|
|
240
|
+
this.stdoutBuf = '';
|
|
241
|
+
this.stderrBuf = '';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_maybeSpill() {
|
|
245
|
+
if (this.spilled) return;
|
|
246
|
+
// Threshold is in BYTES — string .length counts UTF-16 units, which
|
|
247
|
+
// understates CJK output by up to 3x against the byte-sized cap.
|
|
248
|
+
if (this._inlineBytes > SHELL_OUTPUT_INLINE_CAP * 4) {
|
|
249
|
+
this._ensureFileBacking();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Force the in-memory buffers onto disk-backed files regardless of the
|
|
254
|
+
// SHELL_OUTPUT_INLINE_CAP*4 threshold. Used by the auto-background
|
|
255
|
+
// transition: once a foreground command is detached into a tracked job,
|
|
256
|
+
// every subsequent stdout/stderr chunk must land in the spill files so
|
|
257
|
+
// job_wait/peek can read it (the caller has already settled and will no
|
|
258
|
+
// longer drain the in-memory buffers). No-op once already spilled.
|
|
259
|
+
forceSpill() {
|
|
260
|
+
if (this.spilled) return;
|
|
261
|
+
this._ensureFileBacking();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_recordWriteError(stage, err) {
|
|
265
|
+
if (this.writeError) return;
|
|
266
|
+
const msg = (err && err.message) ? err.message : String(err);
|
|
267
|
+
this.writeError = `[output-capture-error: ${stage}] ${msg}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
writeStdout(s) {
|
|
271
|
+
if (!s) return;
|
|
272
|
+
if (this.spilled) {
|
|
273
|
+
try {
|
|
274
|
+
writeSync(this.stdoutFd, s);
|
|
275
|
+
this.stdoutFileSize += Buffer.byteLength(s, 'utf-8');
|
|
276
|
+
} catch (err) {
|
|
277
|
+
this._recordWriteError('stdout-write', err);
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this.stdoutBuf += s;
|
|
282
|
+
this._inlineBytes += Buffer.byteLength(s, 'utf-8');
|
|
283
|
+
this._maybeSpill();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
writeStderr(s) {
|
|
287
|
+
if (!s) return;
|
|
288
|
+
if (this.spilled) {
|
|
289
|
+
try {
|
|
290
|
+
writeSync(this.stderrFd, s);
|
|
291
|
+
this.stderrFileSize += Buffer.byteLength(s, 'utf-8');
|
|
292
|
+
} catch (err) {
|
|
293
|
+
this._recordWriteError('stderr-write', err);
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
this.stderrBuf += s;
|
|
298
|
+
this._inlineBytes += Buffer.byteLength(s, 'utf-8');
|
|
299
|
+
this._maybeSpill();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
totalDiskBytes() {
|
|
303
|
+
return this.stdoutFileSize + this.stderrFileSize;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async getStdout() {
|
|
307
|
+
if (this.spilled) {
|
|
308
|
+
const now = Date.now();
|
|
309
|
+
if (now - this._lastStdoutFsyncMs >= 200) {
|
|
310
|
+
try { fsyncSync(this.stdoutFd); } catch {}
|
|
311
|
+
this._lastStdoutFsyncMs = now;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
return _readHeadTail(this.stdoutPath, this.stdoutFileSize);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
throw new Error(`[shell-command] spilled stdout read failed (${this.stdoutPath}): ${err.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return this.stdoutBuf;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async getStderr() {
|
|
323
|
+
if (this.spilled) {
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
if (now - this._lastStderrFsyncMs >= 200) {
|
|
326
|
+
try { fsyncSync(this.stderrFd); } catch {}
|
|
327
|
+
this._lastStderrFsyncMs = now;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
return _readHeadTail(this.stderrPath, this.stderrFileSize);
|
|
331
|
+
} catch {
|
|
332
|
+
return '';
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return this.stderrBuf;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
closeFds() {
|
|
339
|
+
if (this.stdoutFd != null) {
|
|
340
|
+
try {
|
|
341
|
+
closeSync(this.stdoutFd);
|
|
342
|
+
} catch {}
|
|
343
|
+
this.stdoutFd = null;
|
|
344
|
+
}
|
|
345
|
+
if (this.stderrFd != null) {
|
|
346
|
+
try {
|
|
347
|
+
closeSync(this.stderrFd);
|
|
348
|
+
} catch {}
|
|
349
|
+
this.stderrFd = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Drop the spilled files when the inline body already covers the full
|
|
354
|
+
// output. Called when total spilled bytes <= SHELL_OUTPUT_INLINE_CAP, so
|
|
355
|
+
// outputFilePath would only point at a duplicate of what the caller is
|
|
356
|
+
// already pasting into the result.
|
|
357
|
+
deleteFiles() {
|
|
358
|
+
this.closeFds();
|
|
359
|
+
if (this.stdoutPath) {
|
|
360
|
+
try {
|
|
361
|
+
unlinkSync(this.stdoutPath);
|
|
362
|
+
} catch {}
|
|
363
|
+
this.stdoutPath = null;
|
|
364
|
+
}
|
|
365
|
+
if (this.stderrPath) {
|
|
366
|
+
try {
|
|
367
|
+
unlinkSync(this.stderrPath);
|
|
368
|
+
} catch {}
|
|
369
|
+
this.stderrPath = null;
|
|
370
|
+
}
|
|
371
|
+
this.spilled = false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export { TaskOutput };
|
|
376
|
+
|
|
377
|
+
// Result envelope. Status markers ([exit code: N], [signal: SIGTERM]) are
|
|
378
|
+
// the caller's responsibility — case 'bash' in builtin.mjs owns that
|
|
379
|
+
// rendering convention.
|
|
380
|
+
export class ExecResult {
|
|
381
|
+
constructor(opts) {
|
|
382
|
+
this.stdout = opts.stdout;
|
|
383
|
+
this.stderr = opts.stderr;
|
|
384
|
+
this.exitCode = opts.exitCode;
|
|
385
|
+
this.signal = opts.signal || null;
|
|
386
|
+
this.timedOut = opts.timedOut === true;
|
|
387
|
+
this.killed = opts.killed === true;
|
|
388
|
+
this.stdoutPath = opts.stdoutPath || null;
|
|
389
|
+
this.stdoutFileSize = opts.stdoutFileSize || 0;
|
|
390
|
+
this.stderrPath = opts.stderrPath || null;
|
|
391
|
+
this.stderrFileSize = opts.stderrFileSize || 0;
|
|
392
|
+
this.taskId = opts.taskId;
|
|
393
|
+
this.partialOutput = opts.partialOutput === true;
|
|
394
|
+
this.outputCaptureError = opts.outputCaptureError || null;
|
|
395
|
+
// Auto-background transition (CC startBackgrounding analogue). When a
|
|
396
|
+
// foreground command outlives autoBackgroundMs the call settles with
|
|
397
|
+
// backgrounded:true + the jobId the model can poll via job_wait. The
|
|
398
|
+
// child keeps running detached; stdout/stderr keep flowing to the spill
|
|
399
|
+
// files now adopted by the shell-jobs registry.
|
|
400
|
+
this.backgrounded = opts.backgrounded === true;
|
|
401
|
+
this.jobId = opts.jobId || null;
|
|
402
|
+
this.backgroundMessage = opts.backgroundMessage || null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// On Windows, nested `powershell -Command "<inline>"` invocations can be
|
|
407
|
+
// mangled by an outer shell quoting layer before powershell.exe sees
|
|
408
|
+
// automatic variables (`$_`, `$args`, `$($_.Line)`, etc.). Rewrite
|
|
409
|
+
// `powershell -Command "<inline>"` / `pwsh -Command "<inline>"` to
|
|
410
|
+
// `-EncodedCommand <utf16le-base64>` so the payload stays opaque to the
|
|
411
|
+
// outer shell. Other shells / non-Windows are no-op pass-through.
|
|
412
|
+
// Match -Command "<body>" where the body may contain escaped quotes
|
|
413
|
+
// (`\"` or `""`). Stops at the first unescaped closing quote so nested
|
|
414
|
+
// patterns like `"... \"inner\" ..."` survive intact. Common PowerShell
|
|
415
|
+
// flags (NoProfile, NonInteractive, WindowStyle, ExecutionPolicy, Sta,
|
|
416
|
+
// Mta, NoLogo, NoExit) are recognised so they don't break the match.
|
|
417
|
+
// Single-quoted -Command '<body>' is also covered.
|
|
418
|
+
const _POWERSHELL_FLAGS_RE = /\s+-(?:NoProfile|NonInteractive|WindowStyle\s+\S+|ExecutionPolicy\s+\S+|Sta|Mta|NoLogo|NoExit)/.source;
|
|
419
|
+
const _POWERSHELL_DOUBLE_RE = new RegExp(
|
|
420
|
+
'\\b(powershell(?:\\.exe)?|pwsh(?:\\.exe)?)((?:' + _POWERSHELL_FLAGS_RE + ')*)\\s+(?:-Command|-c)\\s+"((?:[^"\\\\]|\\\\.|"")+?)"(?=\\s|$|;|&&|\\|\\|)',
|
|
421
|
+
'gi',
|
|
422
|
+
);
|
|
423
|
+
const _POWERSHELL_SINGLE_RE = new RegExp(
|
|
424
|
+
"\\b(powershell(?:\\.exe)?|pwsh(?:\\.exe)?)((?:" + _POWERSHELL_FLAGS_RE + ")*)\\s+(?:-Command|-c)\\s+'((?:[^'\\\\]|\\\\.|'')+?)'(?=\\s|$|;|&&|\\|\\|)",
|
|
425
|
+
'gi',
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
export function _maybeEncodePowerShellCommand(command) {
|
|
429
|
+
if (process.platform !== 'win32') return command;
|
|
430
|
+
if (typeof command !== 'string' || command.length === 0) return command;
|
|
431
|
+
const replaceFn = (match, exe, flags, body) => {
|
|
432
|
+
try {
|
|
433
|
+
// Unescape doubled-up quotes the caller used to embed " / ' inside
|
|
434
|
+
// the -Command literal. We're handing the body to powershell as
|
|
435
|
+
// base64 so the outer-shell escaping is no longer needed.
|
|
436
|
+
// Unescape both PowerShell-style doubled quotes (`""` / `''`) AND
|
|
437
|
+
// bash-style backslash-escaped quotes (`\"` / `\'`) since POSIX
|
|
438
|
+
// outer-shell wrappers commonly use backslash form. Without
|
|
439
|
+
// backslash unescape, `pwsh -Command "Get-Process \"foo\""` would
|
|
440
|
+
// base64-encode the literal backslash, breaking inside PowerShell.
|
|
441
|
+
const unescaped = body
|
|
442
|
+
.replace(/""/g, '"')
|
|
443
|
+
.replace(/''/g, "'")
|
|
444
|
+
.replace(/\\"/g, '"')
|
|
445
|
+
.replace(/\\'/g, "'");
|
|
446
|
+
const encoded = Buffer.from(unescaped, 'utf16le').toString('base64');
|
|
447
|
+
const trimmedFlags = (flags || '').replace(/\s+/g, ' ').trim();
|
|
448
|
+
return `${exe}${trimmedFlags ? ' ' + trimmedFlags : ''} -EncodedCommand ${encoded}`;
|
|
449
|
+
} catch {
|
|
450
|
+
return match;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
return command.replace(_POWERSHELL_DOUBLE_RE, replaceFn).replace(_POWERSHELL_SINGLE_RE, replaceFn);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function _unescapePowerShellCommandBody(body) {
|
|
457
|
+
return String(body || '')
|
|
458
|
+
.replace(/""/g, '"')
|
|
459
|
+
.replace(/''/g, "'")
|
|
460
|
+
.replace(/\\"/g, '"')
|
|
461
|
+
.replace(/\\'/g, "'");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Extract inline `powershell -Command "…"` bodies for policy scan parity
|
|
465
|
+
// with hard-block normalization (encoded payloads use decodePowerShellEncodedCommand).
|
|
466
|
+
export function extractPowerShellCommandInner(command) {
|
|
467
|
+
if (typeof command !== 'string' || command.length === 0) return [];
|
|
468
|
+
const out = [];
|
|
469
|
+
const push = (body) => {
|
|
470
|
+
const unescaped = _unescapePowerShellCommandBody(body);
|
|
471
|
+
if (unescaped.trim()) out.push(unescaped);
|
|
472
|
+
};
|
|
473
|
+
for (const m of command.matchAll(_POWERSHELL_DOUBLE_RE)) push(m[3]);
|
|
474
|
+
for (const m of command.matchAll(_POWERSHELL_SINGLE_RE)) push(m[3]);
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// One-shot async shell runner. abortSignal optional (session-scoped abort
|
|
479
|
+
// from getAbortSignalForSession in builtin.mjs). Timeout implemented via
|
|
480
|
+
// treeKill so forked grandchildren also come down. Output streams capture
|
|
481
|
+
// to TaskOutput which transparently spills to disk past the inline cap.
|
|
482
|
+
async function _execPolicyBlockMessage(command) {
|
|
483
|
+
const { checkExecPolicyMessage } = await import('./bash-policy-scan.mjs');
|
|
484
|
+
return checkExecPolicyMessage(command);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function execShellCommand({
|
|
488
|
+
shell,
|
|
489
|
+
shellArg,
|
|
490
|
+
shellArgs,
|
|
491
|
+
command,
|
|
492
|
+
env,
|
|
493
|
+
cwd,
|
|
494
|
+
timeoutMs,
|
|
495
|
+
abortSignal,
|
|
496
|
+
autoBackgroundMs,
|
|
497
|
+
onProgress,
|
|
498
|
+
clientHostPid,
|
|
499
|
+
}) {
|
|
500
|
+
return new Promise(async (resolve) => {
|
|
501
|
+
const taskId = `bash_${randomUUID().slice(0, 8)}`;
|
|
502
|
+
const taskOutput = new TaskOutput(taskId);
|
|
503
|
+
let timedOut = false;
|
|
504
|
+
let killed = false;
|
|
505
|
+
let settled = false;
|
|
506
|
+
let timer = null;
|
|
507
|
+
let abortHandler = null;
|
|
508
|
+
let partialOutput = false;
|
|
509
|
+
// MCP live-progress: throttled "running Ns, M lines" emits while the
|
|
510
|
+
// foreground command runs. Inert (never armed) when onProgress is null.
|
|
511
|
+
const _hasProgress = typeof onProgress === 'function';
|
|
512
|
+
const _startMs = Date.now();
|
|
513
|
+
let progressTimer = null;
|
|
514
|
+
const _clearProgressTimer = () => {
|
|
515
|
+
if (progressTimer) { clearInterval(progressTimer); progressTimer = null; }
|
|
516
|
+
};
|
|
517
|
+
// Auto-background transition flag. Set the moment the autoBackgroundMs
|
|
518
|
+
// timer fires and successfully detaches the still-running child. Once
|
|
519
|
+
// true the normal settle()/close/exit/treeKill paths are inert for this
|
|
520
|
+
// run — the call has already resolved with a 'backgrounded' result and
|
|
521
|
+
// the child's lifecycle is owned by the shell-jobs registry. Mutually
|
|
522
|
+
// exclusive with `settled`: whichever transition wins first wins for good.
|
|
523
|
+
let autoBackgrounded = false;
|
|
524
|
+
let autoBgTimer = null;
|
|
525
|
+
// Treekill + force-settle deadline. treeKill alone leaves settle()
|
|
526
|
+
// pending on 'close'/'exit'; on Windows a taskkill miss or a grandchild
|
|
527
|
+
// holding stdio fds keeps the dispatch stalled until the upstream
|
|
528
|
+
// ceiling. Covers every kill path (timeout / pre-aborted / abort /
|
|
529
|
+
// capture-error / size-watchdog) so the hang risk does not live on
|
|
530
|
+
// outside the timeout branch. Function declaration so callers placed
|
|
531
|
+
// above settle()'s const definition still resolve via hoisting; the
|
|
532
|
+
// 5 s deadline always fires after settle is constructed.
|
|
533
|
+
function _treeKillForceSettle() {
|
|
534
|
+
treeKill(child);
|
|
535
|
+
const _killDeadline = setTimeout(() => {
|
|
536
|
+
if (settled) return;
|
|
537
|
+
partialOutput = true;
|
|
538
|
+
settle(1, 'SIGKILL');
|
|
539
|
+
}, 5000);
|
|
540
|
+
if (_killDeadline.unref) _killDeadline.unref();
|
|
541
|
+
}
|
|
542
|
+
// Background commands (trailing `&`) intentionally detach stdio
|
|
543
|
+
// from the parent shell, so 'close' may never fire while the
|
|
544
|
+
// backgrounded grandchild is still alive. For those we settle
|
|
545
|
+
// immediately on direct-child exit instead of waiting for close.
|
|
546
|
+
const _trimmed = String(command || '').replace(/\s+$/, '');
|
|
547
|
+
const _isBackground = /(^|[^&|])&$/.test(_trimmed);
|
|
548
|
+
|
|
549
|
+
let child;
|
|
550
|
+
try {
|
|
551
|
+
const _policyErr = await _execPolicyBlockMessage(command);
|
|
552
|
+
if (_policyErr) {
|
|
553
|
+
resolve(
|
|
554
|
+
new ExecResult({
|
|
555
|
+
stdout: '',
|
|
556
|
+
stderr: _policyErr,
|
|
557
|
+
exitCode: 1,
|
|
558
|
+
signal: null,
|
|
559
|
+
timedOut: false,
|
|
560
|
+
killed: false,
|
|
561
|
+
taskId,
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const _spawnCommand = _maybeEncodePowerShellCommand(command);
|
|
567
|
+
const argv = Array.isArray(shellArgs) && shellArgs.length > 0
|
|
568
|
+
? [...shellArgs, _spawnCommand]
|
|
569
|
+
: [shellArg, _spawnCommand];
|
|
570
|
+
child = spawn(shell, argv, {
|
|
571
|
+
env,
|
|
572
|
+
cwd,
|
|
573
|
+
windowsHide: true,
|
|
574
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
575
|
+
// POSIX: detached gives the child its own process group so
|
|
576
|
+
// treeKill can signal the whole group. Windows detached has
|
|
577
|
+
// different semantics (no console attached, used for daemonization)
|
|
578
|
+
// so it stays off there.
|
|
579
|
+
detached: process.platform !== 'win32',
|
|
580
|
+
});
|
|
581
|
+
} catch (err) {
|
|
582
|
+
resolve(
|
|
583
|
+
new ExecResult({
|
|
584
|
+
stdout: '',
|
|
585
|
+
stderr: String((err && err.message) || err),
|
|
586
|
+
exitCode: 1,
|
|
587
|
+
signal: null,
|
|
588
|
+
timedOut: false,
|
|
589
|
+
killed: false,
|
|
590
|
+
taskId,
|
|
591
|
+
}),
|
|
592
|
+
);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Pre-aborted signal: kill immediately if the abort already fired
|
|
597
|
+
// before spawn returned (synchronous reentry from a parent abort), so
|
|
598
|
+
// the child doesn't run for the full timeoutMs window.
|
|
599
|
+
if (abortSignal && abortSignal.aborted) {
|
|
600
|
+
killed = true;
|
|
601
|
+
_treeKillForceSettle();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
child.stdout.setEncoding('utf-8');
|
|
605
|
+
child.stderr.setEncoding('utf-8');
|
|
606
|
+
child.stdout.on('data', (chunk) => {
|
|
607
|
+
taskOutput.writeStdout(chunk);
|
|
608
|
+
});
|
|
609
|
+
child.stderr.on('data', (chunk) => taskOutput.writeStderr(chunk));
|
|
610
|
+
|
|
611
|
+
// If the spill writer hits an I/O failure (full disk, EBADF after
|
|
612
|
+
// an unlink race) bring the child down so the agent isn't deceived
|
|
613
|
+
// by a successful exit code on a truncated capture.
|
|
614
|
+
const _abortOnCaptureError = () => {
|
|
615
|
+
if (taskOutput.writeError && !killed && !settled && !autoBackgrounded) {
|
|
616
|
+
killed = true;
|
|
617
|
+
_treeKillForceSettle();
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
let sizeWatchdog = null;
|
|
622
|
+
const settle = async (exitCode, signal) => {
|
|
623
|
+
if (settled || autoBackgrounded) return;
|
|
624
|
+
settled = true;
|
|
625
|
+
if (timer) {
|
|
626
|
+
clearTimeout(timer);
|
|
627
|
+
timer = null;
|
|
628
|
+
}
|
|
629
|
+
_clearProgressTimer();
|
|
630
|
+
if (sizeWatchdog) {
|
|
631
|
+
clearInterval(sizeWatchdog);
|
|
632
|
+
sizeWatchdog = null;
|
|
633
|
+
}
|
|
634
|
+
if (autoBgTimer) {
|
|
635
|
+
clearTimeout(autoBgTimer);
|
|
636
|
+
autoBgTimer = null;
|
|
637
|
+
}
|
|
638
|
+
if (abortSignal && abortHandler) {
|
|
639
|
+
try {
|
|
640
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
641
|
+
} catch {}
|
|
642
|
+
}
|
|
643
|
+
// getStdout/getStderr can throw on a spilled-file read failure (EBADF
|
|
644
|
+
// after unlink race, EACCES). Without this catch the rejection bubbles
|
|
645
|
+
// up and leaves the outer settle promise unresolved, hanging the call.
|
|
646
|
+
// Capture as writeError so the caller sees outputCaptureError and the
|
|
647
|
+
// partial inline buffer (if any) is still surfaced via partialOutput.
|
|
648
|
+
let stdout = '';
|
|
649
|
+
let stderr = '';
|
|
650
|
+
try { stdout = await taskOutput.getStdout(); }
|
|
651
|
+
catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
|
|
652
|
+
try { stderr = await taskOutput.getStderr(); }
|
|
653
|
+
catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
|
|
654
|
+
// Inline-only path: nothing spilled. Nothing to clean up.
|
|
655
|
+
// Spilled but tiny: drop the files — outputFilePath would duplicate
|
|
656
|
+
// the inline body. Spilled and large: keep the files, caller renders
|
|
657
|
+
// the path marker.
|
|
658
|
+
if (
|
|
659
|
+
taskOutput.spilled &&
|
|
660
|
+
stdout.length + stderr.length <= SHELL_OUTPUT_INLINE_CAP
|
|
661
|
+
) {
|
|
662
|
+
taskOutput.deleteFiles();
|
|
663
|
+
} else {
|
|
664
|
+
taskOutput.closeFds();
|
|
665
|
+
}
|
|
666
|
+
resolve(
|
|
667
|
+
new ExecResult({
|
|
668
|
+
stdout,
|
|
669
|
+
stderr,
|
|
670
|
+
exitCode,
|
|
671
|
+
signal,
|
|
672
|
+
timedOut,
|
|
673
|
+
killed,
|
|
674
|
+
stdoutPath: taskOutput.spilled ? taskOutput.stdoutPath : null,
|
|
675
|
+
stdoutFileSize: taskOutput.stdoutFileSize,
|
|
676
|
+
stderrPath: taskOutput.spilled ? taskOutput.stderrPath : null,
|
|
677
|
+
stderrFileSize: taskOutput.stderrFileSize,
|
|
678
|
+
taskId,
|
|
679
|
+
partialOutput,
|
|
680
|
+
outputCaptureError: taskOutput.writeError,
|
|
681
|
+
}),
|
|
682
|
+
);
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// P1 fix: settle on 'close', not 'exit'. 'exit' fires when the child
|
|
686
|
+
// terminates but stdout/stderr streams may still be flushing buffered
|
|
687
|
+
// bytes; settling there can lose the tail of the output. 'close' fires
|
|
688
|
+
// after stdio is fully drained, so getStdout()/getStderr() see the
|
|
689
|
+
// complete capture.
|
|
690
|
+
child.once('close', (code, signal) => settle(code, signal));
|
|
691
|
+
child.once('error', () => settle(1, null));
|
|
692
|
+
// 'close' only fires after stdio drains; a forked grandchild that
|
|
693
|
+
// inherited stdout/stderr fds can hold them open past direct-child
|
|
694
|
+
// exit and stall settle() until timeoutMs. 'exit' fires on direct
|
|
695
|
+
// child termination regardless — give 'close' a 2 s grace then
|
|
696
|
+
// settle anyway.
|
|
697
|
+
child.once('exit', (code, signal) => {
|
|
698
|
+
if (_isBackground) {
|
|
699
|
+
setImmediate(() => settle(code == null ? 1 : code, signal));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const grace = setTimeout(() => {
|
|
703
|
+
if (settled || autoBackgrounded) return;
|
|
704
|
+
partialOutput = true;
|
|
705
|
+
settle(code == null ? 1 : code, signal);
|
|
706
|
+
}, 2000);
|
|
707
|
+
if (grace.unref) grace.unref();
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Auto-background transition (CC ASSISTANT_BLOCKING_BUDGET_MS +
|
|
711
|
+
// startBackgrounding analogue). Fires once, autoBackgroundMs after spawn,
|
|
712
|
+
// IFF the child is still running and the run has not already settled /
|
|
713
|
+
// been killed / timed out. Detaches the child (keeps it running, stops
|
|
714
|
+
// blocking host exit), hands its live spill files to the shell-jobs
|
|
715
|
+
// registry, and resolves the call immediately with a 'backgrounded'
|
|
716
|
+
// result so the tool stops hanging. The 600 s timeoutMs upper bound is
|
|
717
|
+
// carried into the adopted job detail so refreshShellJob still enforces
|
|
718
|
+
// it. Mutually exclusive with settle() via the autoBackgrounded flag set
|
|
719
|
+
// synchronously at the top before any await.
|
|
720
|
+
const _autoBackground = async () => {
|
|
721
|
+
// Win the race: bail if a terminal transition already happened, and
|
|
722
|
+
// claim the transition synchronously so a concurrently-queued settle()
|
|
723
|
+
// (which checks autoBackgrounded) becomes inert.
|
|
724
|
+
if (settled || autoBackgrounded || killed || timedOut) return;
|
|
725
|
+
if (child.exitCode != null || child.signalCode != null) return;
|
|
726
|
+
autoBackgrounded = true;
|
|
727
|
+
// The foreground capture is over; stop the local watchdogs/timers so
|
|
728
|
+
// they cannot treeKill the now-detached child. The 600 s bound lives
|
|
729
|
+
// on in the adopted job detail (timeoutMs) for refreshShellJob.
|
|
730
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
731
|
+
_clearProgressTimer();
|
|
732
|
+
if (sizeWatchdog) { clearInterval(sizeWatchdog); sizeWatchdog = null; }
|
|
733
|
+
if (autoBgTimer) { clearTimeout(autoBgTimer); autoBgTimer = null; }
|
|
734
|
+
if (abortSignal && abortHandler) {
|
|
735
|
+
try { abortSignal.removeEventListener('abort', abortHandler); } catch {}
|
|
736
|
+
abortHandler = null;
|
|
737
|
+
}
|
|
738
|
+
// Keep running without holding the host event loop open.
|
|
739
|
+
try { child.unref(); } catch {}
|
|
740
|
+
// Every subsequent stdout/stderr chunk must hit disk — the call is
|
|
741
|
+
// about to resolve and nobody will drain the in-memory buffers again.
|
|
742
|
+
try { taskOutput.forceSpill(); } catch {}
|
|
743
|
+
const stdoutPath = taskOutput.spilled ? taskOutput.stdoutPath : null;
|
|
744
|
+
const stderrPath = taskOutput.spilled ? taskOutput.stderrPath : null;
|
|
745
|
+
let job = null;
|
|
746
|
+
try {
|
|
747
|
+
job = adoptForegroundShellJob({
|
|
748
|
+
command,
|
|
749
|
+
cwd,
|
|
750
|
+
pid: child.pid,
|
|
751
|
+
timeoutMs,
|
|
752
|
+
mergeStderr: false,
|
|
753
|
+
stdoutPath,
|
|
754
|
+
stderrPath,
|
|
755
|
+
// Stamp the adopted job with the dispatching terminal's claude.exe
|
|
756
|
+
// pid so the statusline scopes it to the owning session.
|
|
757
|
+
clientHostPid,
|
|
758
|
+
});
|
|
759
|
+
} catch {
|
|
760
|
+
job = null;
|
|
761
|
+
}
|
|
762
|
+
// Wire the lifecycle: on close, write the exit-code file FIRST then
|
|
763
|
+
// touch donePath STRICTLY AFTER — the exact ordering refreshShellJob()
|
|
764
|
+
// gates completion on (donePath visible ⇒ exit file fully flushed).
|
|
765
|
+
if (job && job.exitPath && job.donePath) {
|
|
766
|
+
child.once('close', (code, signal) => {
|
|
767
|
+
const rc = code == null ? (signal ? 1 : 0) : code;
|
|
768
|
+
try { writeFileSync(job.exitPath, String(rc)); } catch {}
|
|
769
|
+
try { writeFileSync(job.donePath, ''); } catch {}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
// Snapshot the partial output captured so far for the immediate result.
|
|
773
|
+
let stdout = '';
|
|
774
|
+
let stderr = '';
|
|
775
|
+
try { stdout = await taskOutput.getStdout(); }
|
|
776
|
+
catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
|
|
777
|
+
try { stderr = await taskOutput.getStderr(); }
|
|
778
|
+
catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
|
|
779
|
+
const jobId = job ? job.jobId : null;
|
|
780
|
+
const secs = Math.round(autoBackgroundMs / 1000);
|
|
781
|
+
resolve(
|
|
782
|
+
new ExecResult({
|
|
783
|
+
stdout,
|
|
784
|
+
stderr,
|
|
785
|
+
exitCode: null,
|
|
786
|
+
signal: null,
|
|
787
|
+
timedOut: false,
|
|
788
|
+
killed: false,
|
|
789
|
+
stdoutPath,
|
|
790
|
+
stdoutFileSize: taskOutput.stdoutFileSize,
|
|
791
|
+
stderrPath: taskOutput.spilled ? taskOutput.stderrPath : null,
|
|
792
|
+
stderrFileSize: taskOutput.stderrFileSize,
|
|
793
|
+
taskId,
|
|
794
|
+
partialOutput: true,
|
|
795
|
+
outputCaptureError: taskOutput.writeError,
|
|
796
|
+
backgrounded: true,
|
|
797
|
+
jobId,
|
|
798
|
+
backgroundMessage: jobId
|
|
799
|
+
? `auto-backgrounded after ${secs}s; still running — use job_wait with job_id:${jobId}`
|
|
800
|
+
: `auto-backgrounded after ${secs}s; still running`,
|
|
801
|
+
}),
|
|
802
|
+
);
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
if (timeoutMs > 0) {
|
|
806
|
+
timer = setTimeout(() => {
|
|
807
|
+
timedOut = true;
|
|
808
|
+
_treeKillForceSettle();
|
|
809
|
+
}, timeoutMs);
|
|
810
|
+
if (timer.unref) timer.unref();
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Live-progress heartbeat: every 2 s while the foreground command runs,
|
|
814
|
+
// emit "running Ns" so the MCP client renders live progress
|
|
815
|
+
// instead of an opaque hang. Only armed for a genuine foreground run with
|
|
816
|
+
// a subscribed client; trailing-`&` background commands settle on exit and
|
|
817
|
+
// never need it. Cleared on settle / auto-background (see above).
|
|
818
|
+
if (_hasProgress && !_isBackground) {
|
|
819
|
+
progressTimer = setInterval(() => {
|
|
820
|
+
if (settled || autoBackgrounded) return;
|
|
821
|
+
const secs = Math.round((Date.now() - _startMs) / 1000);
|
|
822
|
+
try { onProgress(`running ${secs}s`); } catch {}
|
|
823
|
+
}, 2000);
|
|
824
|
+
if (progressTimer.unref) progressTimer.unref();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Arm the auto-background timer only for the genuine foreground one-shot
|
|
828
|
+
// path: a positive threshold strictly below the hard timeout, and not a
|
|
829
|
+
// trailing-`&` background command (those already detach + settle on exit).
|
|
830
|
+
if (
|
|
831
|
+
typeof autoBackgroundMs === 'number' &&
|
|
832
|
+
autoBackgroundMs > 0 &&
|
|
833
|
+
!_isBackground &&
|
|
834
|
+
(timeoutMs <= 0 || autoBackgroundMs < timeoutMs)
|
|
835
|
+
) {
|
|
836
|
+
autoBgTimer = setTimeout(() => { _autoBackground(); }, autoBackgroundMs);
|
|
837
|
+
if (autoBgTimer.unref) autoBgTimer.unref();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Size watchdog — a stuck command pumping GBs of stdout into the spill
|
|
841
|
+
// file would fill the user's disk before the timeout fires. Poll the
|
|
842
|
+
// running disk total every 5 s and SIGKILL once we cross the cap. The
|
|
843
|
+
// settle() path clears this interval directly (see top of this Promise
|
|
844
|
+
// body) so no extra exit / error listeners are needed here.
|
|
845
|
+
sizeWatchdog = setInterval(() => {
|
|
846
|
+
if (settled || autoBackgrounded) return;
|
|
847
|
+
_abortOnCaptureError();
|
|
848
|
+
if (taskOutput.totalDiskBytes() > SHELL_OUTPUT_DISK_CAP) {
|
|
849
|
+
killed = true;
|
|
850
|
+
_treeKillForceSettle();
|
|
851
|
+
}
|
|
852
|
+
}, SIZE_WATCHDOG_INTERVAL_MS);
|
|
853
|
+
if (sizeWatchdog.unref) sizeWatchdog.unref();
|
|
854
|
+
|
|
855
|
+
if (abortSignal) {
|
|
856
|
+
abortHandler = () => {
|
|
857
|
+
killed = true;
|
|
858
|
+
_treeKillForceSettle();
|
|
859
|
+
};
|
|
860
|
+
try {
|
|
861
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
862
|
+
} catch {}
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|