mixdog 0.9.1 → 0.9.3
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/package.json +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { validateBuiltinArgs } from './arg-guard.mjs';
|
|
4
|
+
|
|
5
|
+
test('read: numeric-string offset/limit coerce losslessly', () => {
|
|
6
|
+
const a = { path: 'x.js', offset: '850', limit: '850.0' };
|
|
7
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
8
|
+
assert.equal(a.offset, 850);
|
|
9
|
+
assert.equal(a.limit, 850);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('read: non-integer numeric string still rejected', () => {
|
|
13
|
+
const a = { path: 'x.js', offset: '3.5' };
|
|
14
|
+
assert.match(validateBuiltinArgs('read', a), /must be a finite integer/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('read: non-numeric string still rejected', () => {
|
|
18
|
+
const a = { path: 'x.js', offset: 'soon' };
|
|
19
|
+
assert.match(validateBuiltinArgs('read', a), /must be a finite integer/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('read: top-level line/context convert to offset/limit', () => {
|
|
23
|
+
const a = { path: 'x.js', line: 100, context: 10 };
|
|
24
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
25
|
+
assert.equal(a.offset, 89);
|
|
26
|
+
assert.equal(a.limit, 21);
|
|
27
|
+
assert.equal('line' in a, false);
|
|
28
|
+
assert.equal('context' in a, false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('read: line without context defaults to a single-line window', () => {
|
|
32
|
+
const a = { path: 'x.js', line: 1 };
|
|
33
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
34
|
+
assert.equal(a.offset, 0);
|
|
35
|
+
assert.equal(a.limit, 1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('read: explicit offset/limit wins over line/context (dropped)', () => {
|
|
39
|
+
const a = { path: 'x.js', line: 100, context: 10, offset: 5, limit: 20 };
|
|
40
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
41
|
+
assert.equal(a.offset, 5);
|
|
42
|
+
assert.equal(a.limit, 20);
|
|
43
|
+
assert.equal('line' in a, false);
|
|
44
|
+
assert.equal('context' in a, false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('read: path[] entry object line/context converts the same way', () => {
|
|
48
|
+
const a = { path: [{ path: 'x.js', line: 50, context: 5 }] };
|
|
49
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
50
|
+
assert.equal(a.path[0].offset, 44);
|
|
51
|
+
assert.equal(a.path[0].limit, 11);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('read: path[] entry negative numeric-string offset is rejected after coercion', () => {
|
|
55
|
+
const a = { path: [{ path: 'x.js', offset: '-5' }] };
|
|
56
|
+
const err = validateBuiltinArgs('read', a);
|
|
57
|
+
assert.match(err, /path\[0\]\.offset/);
|
|
58
|
+
assert.match(err, />= 0/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('read: path[] entry negative numeric-string limit is rejected after coercion', () => {
|
|
62
|
+
const a = { path: [{ path: 'x.js', limit: '-1' }] };
|
|
63
|
+
const err = validateBuiltinArgs('read', a);
|
|
64
|
+
assert.match(err, /path\[0\]\.limit/);
|
|
65
|
+
assert.match(err, />= 0/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('read: path[] entry valid numeric-string offset/limit still coerces and passes', () => {
|
|
69
|
+
const a = { path: [{ path: 'x.js', offset: '10', limit: '5' }] };
|
|
70
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
71
|
+
assert.equal(a.path[0].offset, 10);
|
|
72
|
+
assert.equal(a.path[0].limit, 5);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('read: JSON object string path is accepted as a single-entry batch', () => {
|
|
76
|
+
const a = { path: '{"path":"x.js","offset":10,"limit":5}' };
|
|
77
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
78
|
+
assert.equal(Array.isArray(a.path), true);
|
|
79
|
+
assert.equal(a.path[0].path, 'x.js');
|
|
80
|
+
assert.equal(a.path[0].offset, 10);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('shell: win32 drive path with no shell set defaults to powershell', (t) => {
|
|
84
|
+
if (process.platform !== 'win32') { t.skip('win32 only'); return; }
|
|
85
|
+
const a = { command: 'dir C:\\Project\\mixdog' };
|
|
86
|
+
assert.equal(validateBuiltinArgs('shell', a), null);
|
|
87
|
+
assert.equal(a.shell, 'powershell');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('glob: missing pattern with a path defaults pattern to "*"', () => {
|
|
91
|
+
const a = { path: 'src' };
|
|
92
|
+
assert.equal(validateBuiltinArgs('glob', a), null);
|
|
93
|
+
assert.equal(a.pattern, '*');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('glob: path carrying glob magic is left for the path-magic fallback, not overridden', () => {
|
|
97
|
+
const a = { path: 'src/**/*.mjs' };
|
|
98
|
+
assert.equal(validateBuiltinArgs('glob', a), null);
|
|
99
|
+
assert.equal('pattern' in a, false);
|
|
100
|
+
assert.equal(a.path, 'src/**/*.mjs');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('glob: missing pattern and path is still an error surface (guard passes through)', () => {
|
|
104
|
+
const a = {};
|
|
105
|
+
assert.equal(validateBuiltinArgs('glob', a), null);
|
|
106
|
+
assert.equal('pattern' in a, false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('grep: output_mode with newline-concatenated debris truncates to valid enum', () => {
|
|
110
|
+
const a = { pattern: 'foo', output_mode: 'content_with_context\ntrue' };
|
|
111
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
112
|
+
assert.equal(a.output_mode, 'content_with_context');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('grep: trailing literal \\n artifact stripped from pattern', () => {
|
|
116
|
+
const a = { pattern: 'foo bar\\n' };
|
|
117
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
118
|
+
assert.equal(a.pattern, 'foo bar');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('grep: trailing concatenation artifact ">\\n" stripped from pattern', () => {
|
|
122
|
+
const a = { pattern: 'foo bar">\n' };
|
|
123
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
124
|
+
assert.equal(a.pattern, 'foo bar');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('grep: mid-pattern \\n is left untouched', () => {
|
|
128
|
+
const a = { pattern: 'foo\\nbar' };
|
|
129
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
130
|
+
assert.equal(a.pattern, 'foo\\nbar');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('grep: bare trailing ">" with no newline artifact survives untouched', () => {
|
|
134
|
+
const a = { pattern: 'class="active">' };
|
|
135
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
136
|
+
assert.equal(a.pattern, 'class="active">');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('grep: numeric-string -C context coerces losslessly', () => {
|
|
140
|
+
const a = { pattern: 'foo', '-C': '5' };
|
|
141
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
142
|
+
assert.equal(a['-C'], 5);
|
|
143
|
+
});
|
|
@@ -17,7 +17,7 @@ export const BUILTIN_TOOLS = [
|
|
|
17
17
|
name: 'read',
|
|
18
18
|
title: 'Mixdog Read',
|
|
19
19
|
annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
20
|
-
description: 'Read known file path(s).
|
|
20
|
+
description: 'Read known file path(s). Prefer grep content_with_context or code_graph anchors first. Window with numeric offset+limit only. Batch paths/regions as real arrays; adjacent spans in one file = one window, not repeated calls. Dirs use list.',
|
|
21
21
|
inputSchema: {
|
|
22
22
|
type: 'object',
|
|
23
23
|
properties: {
|
|
@@ -26,14 +26,26 @@ export const BUILTIN_TOOLS = [
|
|
|
26
26
|
{ type: 'string' },
|
|
27
27
|
{
|
|
28
28
|
type: 'array',
|
|
29
|
-
items: {
|
|
29
|
+
items: {
|
|
30
|
+
anyOf: [
|
|
31
|
+
{ type: 'string' },
|
|
32
|
+
{
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
path: { type: 'string' },
|
|
36
|
+
offset: { type: 'number', minimum: 0 },
|
|
37
|
+
limit: { type: 'number', minimum: 1 },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
30
42
|
minItems: 1,
|
|
31
43
|
},
|
|
32
44
|
],
|
|
33
|
-
description: 'File path or
|
|
45
|
+
description: 'File path, path[], or {path,offset,limit}[] region objects. Pass arrays directly; JSON strings are legacy recovery only.',
|
|
34
46
|
},
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
offset: { type: 'number', minimum: 0, description: 'Numeric lines to skip before reading; 0 starts at line 1. Continue with offset:N.' },
|
|
48
|
+
limit: { type: 'number', minimum: 1, description: 'Numeric max lines to return after offset.' },
|
|
37
49
|
},
|
|
38
50
|
},
|
|
39
51
|
},
|
|
@@ -53,7 +65,10 @@ export const BUILTIN_TOOLS = [
|
|
|
53
65
|
{
|
|
54
66
|
name: 'open_config',
|
|
55
67
|
title: 'Open Config UI',
|
|
56
|
-
|
|
68
|
+
// agentHidden: a worker/reviewer session has no business popping the
|
|
69
|
+
// settings UI on the user's machine; it also wastes schema bytes on
|
|
70
|
+
// every agent request. Lead keeps it.
|
|
71
|
+
annotations: { title: 'Open Config UI', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, agentHidden: true },
|
|
57
72
|
description: 'Open settings UI; returns URL. No params.',
|
|
58
73
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
59
74
|
},
|
|
@@ -61,7 +76,7 @@ export const BUILTIN_TOOLS = [
|
|
|
61
76
|
name: 'shell',
|
|
62
77
|
title: 'Mixdog Shell',
|
|
63
78
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
64
|
-
description: `Run shell
|
|
79
|
+
description: `Run shell to CHANGE state or RUN programs (git/build/test/run). Never to inspect the filesystem — reading, listing, searching, or checking existence go through the dedicated tools, never a shell command. Set shell: powershell or bash. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
|
|
65
80
|
inputSchema: {
|
|
66
81
|
type: 'object',
|
|
67
82
|
properties: {
|
|
@@ -101,7 +116,7 @@ export const BUILTIN_TOOLS = [
|
|
|
101
116
|
name: 'grep',
|
|
102
117
|
title: 'Mixdog Grep',
|
|
103
118
|
annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
104
|
-
description: 'Search text/
|
|
119
|
+
description: 'Search file contents by text/regex in a known scope. Use files_with_matches/count for broad anchors, content_with_context for narrow code answers. One concept → one grep.',
|
|
105
120
|
inputSchema: {
|
|
106
121
|
type: 'object',
|
|
107
122
|
properties: {
|
|
@@ -110,19 +125,20 @@ export const BUILTIN_TOOLS = [
|
|
|
110
125
|
{ type: 'string' },
|
|
111
126
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
112
127
|
],
|
|
113
|
-
description: 'Text/regex.
|
|
128
|
+
description: 'Text/regex. Put synonyms in pattern[] as OR in ONE grep; no serial rewording or equivalent repeats.',
|
|
114
129
|
},
|
|
115
|
-
path: { type: 'string', description: '
|
|
130
|
+
path: { type: 'string', description: 'Known narrowest file/dir; broad scopes return paths first, then refine from returned paths.' },
|
|
116
131
|
glob: {
|
|
117
132
|
anyOf: [
|
|
118
133
|
{ type: 'string' },
|
|
119
134
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
120
135
|
],
|
|
121
|
-
description: '
|
|
136
|
+
description: 'Narrow in same grep; no follow-up grep for equivalent scope changes.',
|
|
122
137
|
},
|
|
123
|
-
output_mode: { type: 'string', enum: ['
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
output_mode: { type: 'string', enum: ['content_with_context', 'content', 'files_with_matches', 'count'], description: 'Broad scope: files_with_matches/count. Narrow scope: content_with_context; answer from it, skip read unless span is not shown.' },
|
|
139
|
+
context: { type: 'number', minimum: 0, description: 'Lines before/after each match; keep bounded.' },
|
|
140
|
+
head_limit: { type: 'number', minimum: 0, description: 'Max output lines; keep small.' },
|
|
141
|
+
offset: { type: 'number', minimum: 0, description: 'Skip output lines for paging.' },
|
|
126
142
|
},
|
|
127
143
|
required: [],
|
|
128
144
|
},
|
|
@@ -131,7 +147,7 @@ export const BUILTIN_TOOLS = [
|
|
|
131
147
|
name: 'glob',
|
|
132
148
|
title: 'Mixdog Glob',
|
|
133
149
|
annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
134
|
-
description: 'Find files by glob.',
|
|
150
|
+
description: 'Find files by exact glob. Unknown path/name uses find.',
|
|
135
151
|
inputSchema: {
|
|
136
152
|
type: 'object',
|
|
137
153
|
properties: {
|
|
@@ -140,7 +156,7 @@ export const BUILTIN_TOOLS = [
|
|
|
140
156
|
{ type: 'string' },
|
|
141
157
|
{ type: 'array', items: { type: 'string' }, minItems: 1 },
|
|
142
158
|
],
|
|
143
|
-
description: '
|
|
159
|
+
description: 'Exact glob pattern(s).',
|
|
144
160
|
},
|
|
145
161
|
path: {
|
|
146
162
|
anyOf: [
|
|
@@ -159,11 +175,11 @@ export const BUILTIN_TOOLS = [
|
|
|
159
175
|
name: 'find',
|
|
160
176
|
title: 'Mixdog Find Files',
|
|
161
177
|
annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
162
|
-
description: '
|
|
178
|
+
description: 'Find files by partial path/name. Exact structure uses glob. Returns verified paths.',
|
|
163
179
|
inputSchema: {
|
|
164
180
|
type: 'object',
|
|
165
181
|
properties: {
|
|
166
|
-
query: { type: 'string', description: 'Partial path/name words.' },
|
|
182
|
+
query: { type: 'string', description: 'Partial path/name words; not file contents.' },
|
|
167
183
|
path: { type: 'string', description: 'Base directory.' },
|
|
168
184
|
head_limit: { type: 'number', description: 'Max paths.' },
|
|
169
185
|
},
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Native adapters for well-known tool names from OTHER agent CLIs whose
|
|
2
|
+
// argument shapes differ from any mixdog builtin (StrReplace/Write/bash
|
|
3
|
+
// variants). Executed directly against the filesystem via atomicWrite / the
|
|
4
|
+
// existing shell runner — NOT by synthesizing an apply_patch V4A string
|
|
5
|
+
// (that approach was tried and abandoned: building a correct V4A envelope
|
|
6
|
+
// from arbitrary old_string/new_string/contents blew up in edge-case
|
|
7
|
+
// complexity for no benefit over a direct fs edit).
|
|
8
|
+
//
|
|
9
|
+
// Contract: tryExecuteExternalToolAdapter(name, args, workDir, options)
|
|
10
|
+
// returns a result STRING when the call was handled (success or a concrete
|
|
11
|
+
// tool-level error), or `null` when the args shape didn't match what the
|
|
12
|
+
// adapter expects — the caller (builtin.mjs default: case) falls back to
|
|
13
|
+
// the existing EXTERNAL_TOOL_REDIRECTS guidance message in that case.
|
|
14
|
+
import { readFileSync, mkdirSync, existsSync, lstatSync, realpathSync, statSync } from 'node:fs';
|
|
15
|
+
import { dirname, sep } from 'node:path';
|
|
16
|
+
import { atomicWrite } from './atomic-write.mjs';
|
|
17
|
+
import { assertPathsReachable } from './fs-reachability.mjs';
|
|
18
|
+
import { normalizeInputPath, resolveAgainstCwd, normalizeOutputPath } from './path-utils.mjs';
|
|
19
|
+
import { isUncPath, isWindowsDevicePath, hasUnsafeWin32Component, isBlockedDevicePath, isSpecialFileStat } from './device-paths.mjs';
|
|
20
|
+
import { executeBashTool } from './bash-tool.mjs';
|
|
21
|
+
import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
22
|
+
import { markCodeGraphDirtyPaths } from '../code-graph-state.mjs';
|
|
23
|
+
|
|
24
|
+
const STR_REPLACE_NAMES = new Set(['strreplace', 'str_replace', 'str_replace_editor', 'search_replace']);
|
|
25
|
+
const WRITE_NAMES = new Set(['write', 'create_file', 'createfile']);
|
|
26
|
+
// 'bash'/'Bash' explicitly request the posix/git-bash shell kind; the
|
|
27
|
+
// run/runcommand/terminal/run_terminal_cmd family leaves `shell` unset so
|
|
28
|
+
// executeBashTool's own default-shell resolution applies (mirrors how the
|
|
29
|
+
// native `shell` tool behaves when the caller omits `shell`).
|
|
30
|
+
const BASH_SHELL_KIND_NAMES = new Set(['bash']);
|
|
31
|
+
const BASH_DEFAULT_NAMES = new Set(['run', 'runcommand', 'terminal', 'run_terminal_cmd']);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True when `name` is a foreign-CLI tool this module can adapt natively.
|
|
35
|
+
* Used by the session loop's dispatch so these names route INTO
|
|
36
|
+
* executeBuiltinTool (whose default: case invokes the adapter) instead of
|
|
37
|
+
* short-circuiting to the unknown-tool redirect message.
|
|
38
|
+
*/
|
|
39
|
+
export function isExternalAdapterTool(name) {
|
|
40
|
+
if (typeof name !== 'string' || !name) return false;
|
|
41
|
+
const key = name.toLowerCase();
|
|
42
|
+
return STR_REPLACE_NAMES.has(key) || WRITE_NAMES.has(key)
|
|
43
|
+
|| BASH_SHELL_KIND_NAMES.has(key) || BASH_DEFAULT_NAMES.has(key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Same write-target guards the read/list surfaces enforce (UNC → NTLM hash
|
|
47
|
+
// leak, device namespace → raw device access / hang, trailing-dot / ADS →
|
|
48
|
+
// device-guard bypass). Writes are strictly more dangerous than reads, so
|
|
49
|
+
// every adapter write path must run these on both the normalized input and
|
|
50
|
+
// the fully resolved path. Returns an Error string or null.
|
|
51
|
+
function guardWritePath(p) {
|
|
52
|
+
if (isUncPath(p))
|
|
53
|
+
return `Error: cannot write UNC / SMB path (network credential leak risk): ${normalizeOutputPath(p)}`;
|
|
54
|
+
if (isWindowsDevicePath(p))
|
|
55
|
+
return `Error: cannot write Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(p)}`;
|
|
56
|
+
if (hasUnsafeWin32Component(p))
|
|
57
|
+
return `Error: cannot write Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(p)}`;
|
|
58
|
+
if (isBlockedDevicePath(p))
|
|
59
|
+
return `Error: cannot write device file (would block or corrupt a device): ${normalizeOutputPath(p)}`;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Realpath to the nearest existing ancestor (create-mode leaves don't exist
|
|
64
|
+
// yet). Returns { probe, real } or null when nothing on the path exists /
|
|
65
|
+
// realpath itself fails.
|
|
66
|
+
function realpathNearestExisting(fullPath) {
|
|
67
|
+
let probe = fullPath;
|
|
68
|
+
while (probe && !existsSync(probe)) {
|
|
69
|
+
const parent = dirname(probe);
|
|
70
|
+
if (!parent || parent === probe) return null;
|
|
71
|
+
probe = parent;
|
|
72
|
+
}
|
|
73
|
+
if (!probe || !existsSync(probe)) return null;
|
|
74
|
+
try { return { probe, real: realpathSync(probe) }; } catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Realpath-based guard: a symlink/junction in the target (or its nearest
|
|
78
|
+
// existing ancestor, for create-mode paths) can point at a UNC share or a
|
|
79
|
+
// device namespace that the lexical checks above never see. Mirrors the
|
|
80
|
+
// realpath verification apply_patch runs on every header. Returns an Error
|
|
81
|
+
// string or null; never throws.
|
|
82
|
+
function guardRealTarget(fullPath) {
|
|
83
|
+
const nearest = realpathNearestExisting(fullPath);
|
|
84
|
+
if (nearest && nearest.real !== nearest.probe) {
|
|
85
|
+
const guardErr = guardWritePath(nearest.real);
|
|
86
|
+
if (guardErr) return `${guardErr} (symlink target of ${normalizeOutputPath(nearest.probe)})`;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const lst = lstatSync(fullPath);
|
|
90
|
+
if (lst.isSymbolicLink()) {
|
|
91
|
+
const realTarget = realpathSync(fullPath);
|
|
92
|
+
const linkGuardErr = guardWritePath(realTarget);
|
|
93
|
+
if (linkGuardErr) return `${linkGuardErr} (symlink target of ${normalizeOutputPath(fullPath)})`;
|
|
94
|
+
}
|
|
95
|
+
// statSync FOLLOWS a leaf symlink, so a link pointing at a custom
|
|
96
|
+
// FIFO/socket/char/block inode trips here too (lstat alone only saw
|
|
97
|
+
// the link inode itself).
|
|
98
|
+
const st = statSync(fullPath);
|
|
99
|
+
if (isSpecialFileStat(st))
|
|
100
|
+
return `Error: cannot write special file (FIFO / character / block device / socket): ${normalizeOutputPath(fullPath)}`;
|
|
101
|
+
} catch { /* ENOENT (create mode) — nothing to check */ }
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Write containment — mirrors apply_patch's realBase check: the REAL resolved
|
|
106
|
+
// target (symlinks flattened, create-mode suffix re-attached lexically) must
|
|
107
|
+
// stay inside the REAL workDir. A lexically-inside path whose ancestor
|
|
108
|
+
// symlink/junction lands outside the project must not be writable through
|
|
109
|
+
// this adapter surface (apply_patch refuses the same shape).
|
|
110
|
+
function guardBaseContainment(fullPath, workDir) {
|
|
111
|
+
let realBase;
|
|
112
|
+
try { realBase = realpathSync(workDir); } catch { return null; }
|
|
113
|
+
const nearest = realpathNearestExisting(fullPath);
|
|
114
|
+
if (!nearest) return null;
|
|
115
|
+
const realResolved = nearest.real + fullPath.slice(nearest.probe.length);
|
|
116
|
+
const fold = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
|
|
117
|
+
const baseWithSep = realBase.endsWith(sep) ? realBase : realBase + sep;
|
|
118
|
+
if (fold(realResolved) !== fold(realBase) && !fold(realResolved).startsWith(fold(baseWithSep))) {
|
|
119
|
+
return `Error: cannot write outside the working directory: ${normalizeOutputPath(realResolved)} escapes ${normalizeOutputPath(realBase)}`;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function resolveTargetPath(args, workDir) {
|
|
125
|
+
const raw = args?.path ?? args?.file_path;
|
|
126
|
+
if (typeof raw !== 'string' || raw.length === 0) return null;
|
|
127
|
+
const norm = normalizeInputPath(raw);
|
|
128
|
+
const guardErr = guardWritePath(norm);
|
|
129
|
+
if (guardErr) return { error: guardErr };
|
|
130
|
+
const full = resolveAgainstCwd(norm, workDir);
|
|
131
|
+
const fullGuardErr = guardWritePath(full);
|
|
132
|
+
if (fullGuardErr) return { error: fullGuardErr };
|
|
133
|
+
// Reachability preflight BEFORE any sync fs (existsSync/realpathSync/
|
|
134
|
+
// lstatSync in the guards below): a dead mount would wedge the event loop
|
|
135
|
+
// on the first sync stat, defeating every downstream timeout. Same
|
|
136
|
+
// deadline-raced probe the read path runs (_readReachPreflight).
|
|
137
|
+
try { await assertPathsReachable([full]); }
|
|
138
|
+
catch (e) { return { error: `Error: ${e?.message || e}` }; }
|
|
139
|
+
const realGuardErr = guardRealTarget(full);
|
|
140
|
+
if (realGuardErr) return { error: realGuardErr };
|
|
141
|
+
const containErr = guardBaseContainment(full, workDir);
|
|
142
|
+
if (containErr) return { error: containErr };
|
|
143
|
+
return { full };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function invalidateAfterWrite(fullPath) {
|
|
147
|
+
try { invalidateBuiltinResultCache([fullPath]); } catch { /* best-effort */ }
|
|
148
|
+
try { markCodeGraphDirtyPaths([fullPath]); } catch { /* best-effort */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function countOccurrences(haystack, needle) {
|
|
152
|
+
let count = 0;
|
|
153
|
+
let idx = -1;
|
|
154
|
+
while ((idx = haystack.indexOf(needle, idx + 1)) !== -1) count += 1;
|
|
155
|
+
return count;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function adaptStrReplace(args, workDir, options) {
|
|
159
|
+
const oldStr = args?.old_string;
|
|
160
|
+
const newStr = args?.new_string;
|
|
161
|
+
if (typeof oldStr !== 'string' || typeof newStr !== 'string') return null;
|
|
162
|
+
// Empty old_string would loop forever in countOccurrences (indexOf('', i)
|
|
163
|
+
// never returns -1) and has no meaningful replace semantics anyway.
|
|
164
|
+
if (oldStr.length === 0) return 'Error: old_string must be a non-empty string';
|
|
165
|
+
const target = await resolveTargetPath(args, workDir);
|
|
166
|
+
if (!target) return null;
|
|
167
|
+
if (target.error) return target.error;
|
|
168
|
+
const fullPath = target.full;
|
|
169
|
+
let content;
|
|
170
|
+
let statBefore = null;
|
|
171
|
+
try {
|
|
172
|
+
try { statBefore = statSync(fullPath); } catch { /* readFileSync below surfaces the real error */ }
|
|
173
|
+
content = readFileSync(fullPath, 'utf8');
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return `Error: cannot read ${fullPath} (${err?.message || err})`;
|
|
176
|
+
}
|
|
177
|
+
const matchCount = countOccurrences(content, oldStr);
|
|
178
|
+
if (matchCount === 0) return `Error: old_string not found in ${fullPath}`;
|
|
179
|
+
if (matchCount > 1) return `Error: old_string is ambiguous: ${matchCount} matches in ${fullPath}`;
|
|
180
|
+
const first = content.indexOf(oldStr);
|
|
181
|
+
const updated = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
|
|
182
|
+
// Lost-update guard: hand the pre-read stat to atomicWrite as an expected
|
|
183
|
+
// target snapshot — it re-stats immediately before EACH rename attempt and
|
|
184
|
+
// aborts with ESTALE_TARGET when another writer (parallel tool call,
|
|
185
|
+
// interleaved apply_patch) changed the file after our read. This closes
|
|
186
|
+
// the check-then-write race a pre-write mtime comparison here would leave.
|
|
187
|
+
try {
|
|
188
|
+
await atomicWrite(fullPath, updated, {
|
|
189
|
+
sessionId: options?.readStateScope || options?.sessionId,
|
|
190
|
+
expectedTargetSnapshot: statBefore ? {
|
|
191
|
+
exists: true,
|
|
192
|
+
size: statBefore.size,
|
|
193
|
+
mtimeMs: statBefore.mtimeMs,
|
|
194
|
+
ctimeMs: statBefore.ctimeMs,
|
|
195
|
+
ino: statBefore.ino,
|
|
196
|
+
} : undefined,
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (err?.code === 'ESTALE_TARGET') {
|
|
200
|
+
return `Error: ${fullPath} changed on disk during the replace; re-read and retry`;
|
|
201
|
+
}
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
invalidateAfterWrite(fullPath);
|
|
205
|
+
return `Updated ${fullPath} (1 replacement)`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function adaptWrite(args, workDir, options) {
|
|
209
|
+
const contents = args?.contents ?? args?.content ?? args?.file_text;
|
|
210
|
+
if (typeof contents !== 'string') return null;
|
|
211
|
+
const target = await resolveTargetPath(args, workDir);
|
|
212
|
+
if (!target) return null;
|
|
213
|
+
if (target.error) return target.error;
|
|
214
|
+
const fullPath = target.full;
|
|
215
|
+
const existed = existsSync(fullPath);
|
|
216
|
+
try { mkdirSync(dirname(fullPath), { recursive: true }); } catch { /* best-effort: parent may already exist */ }
|
|
217
|
+
await atomicWrite(fullPath, contents, { sessionId: options?.readStateScope || options?.sessionId });
|
|
218
|
+
invalidateAfterWrite(fullPath);
|
|
219
|
+
return `${existed ? 'Updated' : 'Created'} ${fullPath} (${Buffer.byteLength(contents, 'utf8')} bytes)`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function adaptBash(key, args, workDir, options) {
|
|
223
|
+
if (typeof args?.command !== 'string' || args.command.length === 0) return null;
|
|
224
|
+
const shellArgs = { ...args };
|
|
225
|
+
if (BASH_SHELL_KIND_NAMES.has(key) && shellArgs.shell === undefined) shellArgs.shell = 'bash';
|
|
226
|
+
return executeBashTool(shellArgs, workDir, options);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function tryExecuteExternalToolAdapter(name, args, workDir, options) {
|
|
230
|
+
if (typeof name !== 'string' || !name) return null;
|
|
231
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) return null;
|
|
232
|
+
const key = name.toLowerCase();
|
|
233
|
+
try {
|
|
234
|
+
if (STR_REPLACE_NAMES.has(key)) return await adaptStrReplace(args, workDir, options);
|
|
235
|
+
if (WRITE_NAMES.has(key)) return await adaptWrite(args, workDir, options);
|
|
236
|
+
if (BASH_SHELL_KIND_NAMES.has(key) || BASH_DEFAULT_NAMES.has(key)) return await adaptBash(key, args, workDir, options);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
return `Error: ${name} failed (${err?.message || String(err)})`;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|