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,162 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { tryExecuteExternalToolAdapter, isExternalAdapterTool } from './external-tool-adapters.mjs';
|
|
7
|
+
|
|
8
|
+
function makeDir() {
|
|
9
|
+
return mkdtempSync(join(tmpdir(), 'ext-adapter-test-'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test('isExternalAdapterTool recognizes adapted names only', () => {
|
|
13
|
+
assert.equal(isExternalAdapterTool('StrReplace'), true);
|
|
14
|
+
assert.equal(isExternalAdapterTool('str_replace'), true);
|
|
15
|
+
assert.equal(isExternalAdapterTool('str_replace_editor'), true);
|
|
16
|
+
assert.equal(isExternalAdapterTool('search_replace'), true);
|
|
17
|
+
assert.equal(isExternalAdapterTool('Write'), true);
|
|
18
|
+
assert.equal(isExternalAdapterTool('create_file'), true);
|
|
19
|
+
assert.equal(isExternalAdapterTool('Bash'), true);
|
|
20
|
+
assert.equal(isExternalAdapterTool('run_terminal_cmd'), true);
|
|
21
|
+
// Destructive / different-shape names stay on the redirect path.
|
|
22
|
+
assert.equal(isExternalAdapterTool('Delete'), false);
|
|
23
|
+
assert.equal(isExternalAdapterTool('delete_file'), false);
|
|
24
|
+
assert.equal(isExternalAdapterTool('edit'), false);
|
|
25
|
+
assert.equal(isExternalAdapterTool('multiedit'), false);
|
|
26
|
+
assert.equal(isExternalAdapterTool(''), false);
|
|
27
|
+
assert.equal(isExternalAdapterTool(null), false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('StrReplace: unique match replaces content', async () => {
|
|
31
|
+
const dir = makeDir();
|
|
32
|
+
const file = join(dir, 'a.txt');
|
|
33
|
+
writeFileSync(file, 'alpha\nbeta\ngamma\n');
|
|
34
|
+
const out = await tryExecuteExternalToolAdapter('StrReplace', {
|
|
35
|
+
path: file, old_string: 'beta', new_string: 'BETA',
|
|
36
|
+
}, dir, {});
|
|
37
|
+
assert.match(out, /^Updated .*\(1 replacement\)$/);
|
|
38
|
+
assert.equal(readFileSync(file, 'utf8'), 'alpha\nBETA\ngamma\n');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('StrReplace: zero matches errors without writing', async () => {
|
|
42
|
+
const dir = makeDir();
|
|
43
|
+
const file = join(dir, 'a.txt');
|
|
44
|
+
writeFileSync(file, 'alpha\n');
|
|
45
|
+
const out = await tryExecuteExternalToolAdapter('str_replace', {
|
|
46
|
+
path: file, old_string: 'missing', new_string: 'x',
|
|
47
|
+
}, dir, {});
|
|
48
|
+
assert.match(out, /old_string not found/);
|
|
49
|
+
assert.equal(readFileSync(file, 'utf8'), 'alpha\n');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('StrReplace: ambiguous matches error with count', async () => {
|
|
53
|
+
const dir = makeDir();
|
|
54
|
+
const file = join(dir, 'a.txt');
|
|
55
|
+
writeFileSync(file, 'dup\ndup\n');
|
|
56
|
+
const out = await tryExecuteExternalToolAdapter('search_replace', {
|
|
57
|
+
path: file, old_string: 'dup', new_string: 'x',
|
|
58
|
+
}, dir, {});
|
|
59
|
+
assert.match(out, /ambiguous: 2 matches/);
|
|
60
|
+
assert.equal(readFileSync(file, 'utf8'), 'dup\ndup\n');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('StrReplace: missing old_string falls back (null)', async () => {
|
|
64
|
+
const dir = makeDir();
|
|
65
|
+
const out = await tryExecuteExternalToolAdapter('StrReplace', {
|
|
66
|
+
path: join(dir, 'a.txt'), new_string: 'x',
|
|
67
|
+
}, dir, {});
|
|
68
|
+
assert.equal(out, null);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('StrReplace: unreadable target is a concrete error', async () => {
|
|
72
|
+
const dir = makeDir();
|
|
73
|
+
const out = await tryExecuteExternalToolAdapter('StrReplace', {
|
|
74
|
+
path: join(dir, 'nope.txt'), old_string: 'a', new_string: 'b',
|
|
75
|
+
}, dir, {});
|
|
76
|
+
assert.match(out, /^Error: cannot read /);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('Write: creates a new file with contents', async () => {
|
|
80
|
+
const dir = makeDir();
|
|
81
|
+
const file = join(dir, 'new', 'nested.txt');
|
|
82
|
+
const out = await tryExecuteExternalToolAdapter('Write', {
|
|
83
|
+
path: file, contents: 'created body',
|
|
84
|
+
}, dir, {});
|
|
85
|
+
assert.match(out, /^Created .*nested\.txt \(12 bytes\)$/);
|
|
86
|
+
assert.equal(readFileSync(file, 'utf8'), 'created body');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('Write: overwrites an existing file and says Updated', async () => {
|
|
90
|
+
const dir = makeDir();
|
|
91
|
+
const file = join(dir, 'exists.txt');
|
|
92
|
+
writeFileSync(file, 'old');
|
|
93
|
+
const out = await tryExecuteExternalToolAdapter('write', {
|
|
94
|
+
file_path: file, content: 'new body',
|
|
95
|
+
}, dir, {});
|
|
96
|
+
assert.match(out, /^Updated /);
|
|
97
|
+
assert.equal(readFileSync(file, 'utf8'), 'new body');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('Write: missing contents falls back to redirect (null)', async () => {
|
|
101
|
+
const dir = makeDir();
|
|
102
|
+
const out = await tryExecuteExternalToolAdapter('Write', {
|
|
103
|
+
path: join(dir, 'x.txt'),
|
|
104
|
+
}, dir, {});
|
|
105
|
+
assert.equal(out, null);
|
|
106
|
+
assert.equal(existsSync(join(dir, 'x.txt')), false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('StrReplace: missing old_string/new_string falls back (null)', async () => {
|
|
110
|
+
const dir = makeDir();
|
|
111
|
+
const file = join(dir, 'a.txt');
|
|
112
|
+
writeFileSync(file, 'body');
|
|
113
|
+
const out = await tryExecuteExternalToolAdapter('StrReplace', {
|
|
114
|
+
path: file, old_string: 'body',
|
|
115
|
+
}, dir, {});
|
|
116
|
+
assert.equal(out, null);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('StrReplace: empty old_string is a concrete error (no hang)', async () => {
|
|
120
|
+
const dir = makeDir();
|
|
121
|
+
const file = join(dir, 'a.txt');
|
|
122
|
+
writeFileSync(file, 'body');
|
|
123
|
+
const out = await tryExecuteExternalToolAdapter('StrReplace', {
|
|
124
|
+
path: file, old_string: '', new_string: 'x',
|
|
125
|
+
}, dir, {});
|
|
126
|
+
assert.match(out, /old_string must be a non-empty string/);
|
|
127
|
+
assert.equal(readFileSync(file, 'utf8'), 'body');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('bash family: missing command falls back (null)', async () => {
|
|
131
|
+
const dir = makeDir();
|
|
132
|
+
const out = await tryExecuteExternalToolAdapter('Bash', {}, dir, {});
|
|
133
|
+
assert.equal(out, null);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('Write: target outside workDir is refused (base containment)', async () => {
|
|
137
|
+
const workDir = makeDir();
|
|
138
|
+
const outsideDir = makeDir();
|
|
139
|
+
const outside = join(outsideDir, 'escape.txt');
|
|
140
|
+
const out = await tryExecuteExternalToolAdapter('Write', {
|
|
141
|
+
path: outside, contents: 'should not land',
|
|
142
|
+
}, workDir, {});
|
|
143
|
+
assert.match(out, /cannot write outside the working directory/);
|
|
144
|
+
assert.equal(existsSync(outside), false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('non-adapted names return null without touching fs', async () => {
|
|
148
|
+
const dir = makeDir();
|
|
149
|
+
const out = await tryExecuteExternalToolAdapter('Delete', {
|
|
150
|
+
path: join(dir, 'x.txt'),
|
|
151
|
+
}, dir, {});
|
|
152
|
+
assert.equal(out, null);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('bash family: executes a command via the shell runner', async () => {
|
|
156
|
+
const dir = makeDir();
|
|
157
|
+
const out = await tryExecuteExternalToolAdapter('run_terminal_cmd', {
|
|
158
|
+
command: 'node -e "console.log(\'adapter-ok\')"',
|
|
159
|
+
}, dir, {});
|
|
160
|
+
assert.equal(typeof out, 'string');
|
|
161
|
+
assert.match(out, /adapter-ok/);
|
|
162
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// fuzzy-match.mjs — lightweight subsequence fuzzy scorer for filename / path
|
|
2
|
-
// search
|
|
2
|
+
// search. Returns a score where a
|
|
3
3
|
// higher value is a better match, or null when the query is not a subsequence
|
|
4
4
|
// of the candidate.
|
|
5
5
|
//
|
|
@@ -5,3 +5,13 @@ export function formatMtime(mtimeMs) {
|
|
|
5
5
|
if (!mtimeMs) return '-';
|
|
6
6
|
return new Date(mtimeMs).toISOString().slice(0, 19).replace('T', ' ');
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
// Human-readable size column for list/find. Directories have no meaningful
|
|
10
|
+
// byte size (render '-'); files show KB so the model can spot a large file
|
|
11
|
+
// before reading it. Sub-1KB files round up to 1KB.
|
|
12
|
+
export function formatListSize(type, bytes) {
|
|
13
|
+
if (type !== 'file') return '-';
|
|
14
|
+
const n = Number(bytes) || 0;
|
|
15
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
16
|
+
return `${Math.max(1, Math.round(n / 1024))}KB`;
|
|
17
|
+
}
|
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
NOISE_DIR_NAMES,
|
|
27
27
|
walkDir,
|
|
28
28
|
} from './glob-walk.mjs';
|
|
29
|
-
import { formatMtime } from './list-formatting.mjs';
|
|
29
|
+
import { formatMtime, formatListSize } from './list-formatting.mjs';
|
|
30
|
+
import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
30
31
|
import { runRg } from './rg-runner.mjs';
|
|
31
32
|
import { fuzzyRank } from './fuzzy-match.mjs';
|
|
32
33
|
import { assertPathReachable } from './fs-reachability.mjs';
|
|
@@ -169,7 +170,7 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
const lines = sliced.map(r =>
|
|
172
|
-
`${normalizeOutputPath(r.path)}\t${r.type}\t${r.size}\t${formatMtime(r.mtimeMs)}`);
|
|
173
|
+
`${normalizeOutputPath(r.path)}\t${r.type}\t${formatListSize(r.type, r.size)}\t${formatMtime(r.mtimeMs)}`);
|
|
173
174
|
if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${rows.length}; pass offset:${offset + sliced.length} to continue]`);
|
|
174
175
|
if (truncatedByCap) lines.push(`... walk truncated at ${LIST_ABSOLUTE_CAP} rows or ${LIST_WALK_TIMEOUT_MS}ms timeout; narrow the path or lower depth for a complete listing`);
|
|
175
176
|
let emptyMsg = '(empty directory)';
|
|
@@ -282,7 +283,7 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
282
283
|
const totalLabel = body.length >= gatherCap ? `${body.length}+` : `${body.length}`;
|
|
283
284
|
outLines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${totalLabel}; pass offset:${offset + sliced.length} to continue]`);
|
|
284
285
|
}
|
|
285
|
-
const TREE_OUTPUT_CHAR_CAP =
|
|
286
|
+
const TREE_OUTPUT_CHAR_CAP = TOOL_OUTPUT_MAX_BYTES;
|
|
286
287
|
let out = outLines.join('\n');
|
|
287
288
|
let outputCharTruncated = false;
|
|
288
289
|
if (out.length > TREE_OUTPUT_CHAR_CAP) {
|
|
@@ -603,7 +604,7 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
603
604
|
const windowed = offset > 0 ? matches.slice(offset) : matches;
|
|
604
605
|
const sliced = headLimit > 0 ? windowed.slice(0, headLimit) : windowed;
|
|
605
606
|
const lines = sliced.map(m =>
|
|
606
|
-
`${normalizeOutputPath(m.path)}\t${m.size}\t${formatMtime(m.mtimeMs)}`);
|
|
607
|
+
`${normalizeOutputPath(m.path)}\t${formatListSize('file', m.size)}\t${formatMtime(m.mtimeMs)}`);
|
|
607
608
|
if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${matches.length}; pass offset:${offset + sliced.length} to continue]`);
|
|
608
609
|
if (rgStdoutTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; results incomplete');
|
|
609
610
|
if (rgStdoutPartial) lines.push('... [warning] rg exit 2 (partial results); listing may be incomplete');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readdirSync } from 'fs';
|
|
2
|
-
import { basename, dirname, extname, isAbsolute, join, relative } from 'path';
|
|
1
|
+
import { readdirSync, statSync } from 'fs';
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join, relative, sep } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { resolvePluginData, mixdogRoot } from '../../../../shared/plugin-paths.mjs';
|
|
5
5
|
|
|
@@ -80,6 +80,46 @@ export function findFileByBasename(searchRoot, fullPath, { limit = 3, maxDirs =
|
|
|
80
80
|
} catch { return []; }
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Recover a hallucinated absolute-path PREFIX: models frequently request
|
|
84
|
+
// paths like /Users/foo/Local/Project/ink/src/tui/input-editing.mjs where the
|
|
85
|
+
// TAIL (src/tui/input-editing.mjs) is the real repo-relative path but the
|
|
86
|
+
// leading segments are an invented (or wrong-machine) prefix. findFileByBasename
|
|
87
|
+
// only matches the final basename and walks the whole tree (skipping vendor/
|
|
88
|
+
// noise dirs for performance); this instead peels leading segments off the
|
|
89
|
+
// requested path one at a time and stats the remaining tail directly against
|
|
90
|
+
// searchRoot — no directory walk, no skip-dir filtering, so it finds files
|
|
91
|
+
// under vendor/ or any other normally-skipped directory. Min tail length is 2
|
|
92
|
+
// segments so a bare basename (which would match almost anything) never
|
|
93
|
+
// counts as a hit. Capped iterations; pure best-effort, never throws.
|
|
94
|
+
export function findBySuffixStrip(searchRoot, fullPath, { maxIterations = 12 } = {}) {
|
|
95
|
+
try {
|
|
96
|
+
if (typeof searchRoot !== 'string' || !searchRoot) return null;
|
|
97
|
+
if (typeof fullPath !== 'string' || !fullPath) return null;
|
|
98
|
+
// Containment guard: drop `.`/`..` and drive/UNC-ish segments outright.
|
|
99
|
+
// Tails are joined under searchRoot; a `..` segment could stat (and
|
|
100
|
+
// hint) outside the repo, so no relative-traversal token may survive.
|
|
101
|
+
const segments = fullPath.replace(/\\/g, '/').split('/')
|
|
102
|
+
.filter((s) => s && s !== '.' && s !== '..' && !/^[A-Za-z]:$/.test(s));
|
|
103
|
+
// Peel 0..N leading segments; each peel costs one stat. maxIterations
|
|
104
|
+
// bounds the number of stats (strict <), and tails shorter than 2
|
|
105
|
+
// segments never count as a hit.
|
|
106
|
+
const iterations = Math.min(Math.max(segments.length - 1, 0), Math.max(maxIterations, 0));
|
|
107
|
+
for (let i = 0; i < iterations; i++) {
|
|
108
|
+
const tail = segments.slice(i);
|
|
109
|
+
if (tail.length < 2) break;
|
|
110
|
+
const candidate = join(searchRoot, ...tail);
|
|
111
|
+
const rel = relative(searchRoot, candidate);
|
|
112
|
+
if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue;
|
|
113
|
+
try {
|
|
114
|
+
if (statSync(candidate).isFile()) {
|
|
115
|
+
return rel.replace(/\\/g, '/');
|
|
116
|
+
}
|
|
117
|
+
} catch { /* miss this tail length, keep peeling */ }
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
} catch { return null; }
|
|
121
|
+
}
|
|
122
|
+
|
|
83
123
|
// Node's native fs errors embed the failing path wrapped in single quotes
|
|
84
124
|
// using OS-native separators ('C:\\Users\\foo\\bar.mjs' on Windows). Without
|
|
85
125
|
// this pass, read error bodies surface backslash paths that
|
|
@@ -220,5 +220,20 @@ export function coerceShapeFlex(value) {
|
|
|
220
220
|
if (typeof parsed === 'string') return parsed;
|
|
221
221
|
} catch {}
|
|
222
222
|
}
|
|
223
|
+
// A single {path,offset,limit}-shaped object JSON string (e.g. a model
|
|
224
|
+
// emitting `path: "{\"path\":\"x\",\"offset\":10}"` instead of a real
|
|
225
|
+
// object) is a lossless single-entry batch. Wrap it in an array so the
|
|
226
|
+
// downstream object-array batch path (read-tool.mjs) picks it up instead
|
|
227
|
+
// of falling through to the plain-string path guard, which would
|
|
228
|
+
// misdetect the raw JSON text as a literal (and often invalid) filename.
|
|
229
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(trimmed);
|
|
232
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
233
|
+
&& typeof (parsed.path ?? parsed.file_path) === 'string') {
|
|
234
|
+
return [parsed];
|
|
235
|
+
}
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
223
238
|
return value;
|
|
224
239
|
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { normalizeInputPath, resolveAgainstCwd } from './path-utils.mjs';
|
|
3
|
-
import { parseLineLimitArg } from './read-formatting.mjs';
|
|
4
|
-
|
|
5
|
-
const READ_LINE_CONTEXT_DEFAULT = 20;
|
|
6
|
-
const READ_LINE_CONTEXT_MAX = 200;
|
|
7
3
|
|
|
8
4
|
export function parseReadLineNumberArg(value) {
|
|
9
5
|
const n = Number(value);
|
|
10
6
|
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
|
|
11
7
|
}
|
|
12
8
|
|
|
13
|
-
export function parseReadLineContextArg(value) {
|
|
14
|
-
const n = Number(value);
|
|
15
|
-
if (!Number.isFinite(n)) return READ_LINE_CONTEXT_DEFAULT;
|
|
16
|
-
return Math.min(READ_LINE_CONTEXT_MAX, Math.max(0, Math.trunc(n)));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
export function parseReadPathLineSpec(rawPath) {
|
|
20
10
|
if (typeof rawPath !== 'string' || rawPath.length === 0) return null;
|
|
21
11
|
const text = rawPath.trim();
|
|
@@ -51,7 +41,6 @@ export function normalizePathAndStripLineCoordinate(rawPath, workDir) {
|
|
|
51
41
|
|
|
52
42
|
export function normaliseReadLineWindowArgs(inputArgs, workDir) {
|
|
53
43
|
const args = { ...inputArgs };
|
|
54
|
-
let lineNo = parseReadLineNumberArg(args.line);
|
|
55
44
|
let pathLineRange = null;
|
|
56
45
|
if (typeof args.path === 'string' && args.path) {
|
|
57
46
|
const spec = resolveExistingPathLineCoordinate(args.path, workDir);
|
|
@@ -61,45 +50,21 @@ export function normaliseReadLineWindowArgs(inputArgs, workDir) {
|
|
|
61
50
|
return args;
|
|
62
51
|
}
|
|
63
52
|
args.path = spec.path;
|
|
64
|
-
if (!lineNo) lineNo = spec.lineNo;
|
|
65
53
|
if (spec.endLine) pathLineRange = { startLine: spec.lineNo, endLine: spec.endLine };
|
|
54
|
+
else pathLineRange = { startLine: spec.lineNo, endLine: spec.lineNo };
|
|
66
55
|
}
|
|
67
56
|
}
|
|
68
57
|
const isFullMode = !args.mode || args.mode === 'full';
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
args._invertedRangeError = `Error: conflicting window args — both line (${lineNo}) and offset (${args.offset}) were given; line/context and offset/limit are alternative window forms, pass only one`;
|
|
76
|
-
return args;
|
|
77
|
-
}
|
|
78
|
-
if (isFullMode && lineNo) {
|
|
79
|
-
if (pathLineRange && args.context === undefined && (args.limit === undefined || args.limit === null)) {
|
|
80
|
-
args.offset = Math.max(0, pathLineRange.startLine - 1);
|
|
58
|
+
// Public Read args are offset/limit only. Keep a narrow private
|
|
59
|
+
// compatibility path for file#Lx/file:line strings by converting them into
|
|
60
|
+
// offset/limit; do not interpret line/context fields here.
|
|
61
|
+
if (isFullMode && pathLineRange && args.offset === undefined) {
|
|
62
|
+
args.offset = Math.max(0, pathLineRange.startLine - 1);
|
|
63
|
+
if (args.limit === undefined) {
|
|
81
64
|
args.limit = Math.max(1, pathLineRange.endLine - pathLineRange.startLine + 1);
|
|
82
|
-
} else {
|
|
83
|
-
const contextExplicit = args.context !== undefined && args.context !== null;
|
|
84
|
-
const limitExplicit = args.limit !== undefined && args.limit !== null;
|
|
85
|
-
const context = parseReadLineContextArg(args.context);
|
|
86
|
-
if (limitExplicit && !contextExplicit) {
|
|
87
|
-
// Explicit limit, no explicit context: anchor the window AT the
|
|
88
|
-
// requested line so it is always included. (Was: offset centered
|
|
89
|
-
// by the default context, which a small limit then truncated to
|
|
90
|
-
// exclude the very line the caller asked for.)
|
|
91
|
-
args.offset = Math.max(0, lineNo - 1);
|
|
92
|
-
args.limit = parseLineLimitArg(args.limit, (context * 2) + 1);
|
|
93
|
-
} else {
|
|
94
|
-
const limit = limitExplicit
|
|
95
|
-
? parseLineLimitArg(args.limit, (context * 2) + 1)
|
|
96
|
-
: (context * 2) + 1;
|
|
97
|
-
args.offset = Math.max(0, lineNo - context - 1);
|
|
98
|
-
args.limit = limit;
|
|
99
|
-
}
|
|
100
65
|
}
|
|
101
|
-
delete args.line;
|
|
102
|
-
delete args.context;
|
|
103
66
|
}
|
|
67
|
+
delete args.line;
|
|
68
|
+
delete args.context;
|
|
104
69
|
return args;
|
|
105
70
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SMART_READ_MAX_BYTES } from './read-formatting.mjs';
|
|
2
|
+
import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
2
3
|
|
|
3
4
|
// Read tool caps.
|
|
4
5
|
//
|
|
@@ -28,7 +29,7 @@ import { SMART_READ_MAX_BYTES } from './read-formatting.mjs';
|
|
|
28
29
|
// cleanly without a tokenizer.
|
|
29
30
|
export const READ_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
30
31
|
export const READ_WHOLE_FILE_MAX_BYTES = 256 * 1024;
|
|
31
|
-
export const READ_MAX_OUTPUT_BYTES =
|
|
32
|
+
export const READ_MAX_OUTPUT_BYTES = TOOL_OUTPUT_MAX_BYTES;
|
|
32
33
|
export const READ_STREAM_RANGE_MIN_BYTES = 128 * 1024;
|
|
33
34
|
export const READ_SMART_STREAM_MIN_BYTES = SMART_READ_MAX_BYTES;
|
|
34
35
|
export const READ_BATCH_RANGE_COALESCE_GAP_LINES = 256;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { mergeReadRanges } from './read-ranges.mjs';
|
|
2
|
+
import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// Smart-truncate cap: a no-window read returns the file until this cap, past
|
|
5
|
+
// which head+tail are shown and the model pages the rest with offset (footer
|
|
6
|
+
// says how). Byte budget is the shared TOOL_OUTPUT_MAX_BYTES; line/head/tail
|
|
7
|
+
// stay read-specific. Env-overridable for bench: MIXDOG_READ_MAX_LINES/_HEAD/_TAIL.
|
|
8
|
+
function _readEnvInt(name, fallback) {
|
|
9
|
+
const v = parseInt(process.env[name], 10);
|
|
10
|
+
return Number.isFinite(v) && v > 0 ? v : fallback;
|
|
11
|
+
}
|
|
12
|
+
export const SMART_READ_MAX_BYTES = TOOL_OUTPUT_MAX_BYTES;
|
|
13
|
+
export const SMART_READ_MAX_LINES = _readEnvInt('MIXDOG_READ_MAX_LINES', 2000);
|
|
14
|
+
export const SMART_READ_HEAD_LINES = _readEnvInt('MIXDOG_READ_HEAD_LINES', 1200);
|
|
15
|
+
export const SMART_READ_TAIL_LINES = _readEnvInt('MIXDOG_READ_TAIL_LINES', 400);
|
|
7
16
|
// Only the genuinely large full reads warrant the anti-re-read advisory; below
|
|
8
17
|
// this the smart-truncate path (30 KB) already caps normal reads, so a 16 KB
|
|
9
18
|
// floor mostly fired on full:true mid-size reads where the advisory was pure
|
|
@@ -11,7 +20,7 @@ export const SMART_READ_TAIL_LINES = 100;
|
|
|
11
20
|
// file actually hurts.
|
|
12
21
|
export const READ_CONTEXT_ADVISORY_BYTES = 40 * 1024;
|
|
13
22
|
export const READ_MAX_RENDERED_LINE_CHARS = 2_000;
|
|
14
|
-
//
|
|
23
|
+
// The read line-prefix separator is `→` (the `→`
|
|
15
24
|
// arrow), matching default cat -n format `<n>→<content>`. It
|
|
16
25
|
// MUST be a NON-WHITESPACE glyph: a tab/space separator collides with the
|
|
17
26
|
// content's own leading indentation, so a model hand-reconstructing an edit
|
|
@@ -3,7 +3,7 @@ import * as fsPromises from 'fs/promises';
|
|
|
3
3
|
import { readFile } from 'fs/promises';
|
|
4
4
|
import { extname } from 'path';
|
|
5
5
|
import { normalizeInputPath } from './path-utils.mjs';
|
|
6
|
-
import { findFileByBasename } from './path-diagnostics.mjs';
|
|
6
|
+
import { findFileByBasename, findBySuffixStrip } from './path-diagnostics.mjs';
|
|
7
7
|
import { getReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
8
8
|
import { snapshotCoversFullFile, statMatchesSnapshot } from './snapshot-helpers.mjs';
|
|
9
9
|
import { formatBinaryReadPreview } from './binary-file.mjs';
|
|
@@ -209,9 +209,16 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
209
209
|
// path(s) directly — the route a model would otherwise reconstruct with
|
|
210
210
|
// a grep/glob storm.
|
|
211
211
|
if (!similar) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
// Hallucinated absolute-prefix recovery first: no directory walk,
|
|
213
|
+
// stats directly (finds vendor/ paths the basename BFS skips).
|
|
214
|
+
const suffixHit = findBySuffixStrip(workDir, fullPath);
|
|
215
|
+
if (suffixHit) {
|
|
216
|
+
hint = ` Not found at this path; the same filename exists at: "${normalizeOutputPath(suffixHit)}". Read that path directly.`;
|
|
217
|
+
} else {
|
|
218
|
+
const elsewhere = findFileByBasename(workDir, fullPath);
|
|
219
|
+
if (elsewhere.length) {
|
|
220
|
+
hint = ` Not found at this path; the same filename exists at: ${elsewhere.map((p) => `"${normalizeOutputPath(p)}"`).join(', ')}. Read that path directly.`;
|
|
221
|
+
}
|
|
215
222
|
}
|
|
216
223
|
}
|
|
217
224
|
const _rawMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -49,8 +49,7 @@ function _guardedReadError(p, helpers) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Reachability preflight for EVERY read shape (scalar / array / reads[]). MUST
|
|
52
|
-
// run before any sync FS — including
|
|
53
|
-
// in readPathStringGuardError / normaliseReadLineWindowArgs) and the image
|
|
52
|
+
// run before any sync FS — including path normalization and the image
|
|
54
53
|
// stat/read. A dead mount would otherwise freeze the event loop, defeating even
|
|
55
54
|
// the 630s dispatch ceiling.
|
|
56
55
|
async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
@@ -58,10 +57,9 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
|
|
|
58
57
|
normalizeInputPath, resolveAgainstCwd,
|
|
59
58
|
} = helpers;
|
|
60
59
|
// A guarded path (UNC/SMB, Windows device, ADS, /dev/* block) must be
|
|
61
|
-
// REJECTED here, not skipped: skipping would let the later sync
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// hang. Reject up front with the same message the inline guards emit.
|
|
60
|
+
// REJECTED here, not skipped: skipping would let the later sync guard/open
|
|
61
|
+
// path touch it and trigger NTLM/raw-device access or hang. Reject up front
|
|
62
|
+
// with the same message the inline guards emit.
|
|
65
63
|
// normalizeInputPath FIRST (FS-pure) so we stat the same path the real read
|
|
66
64
|
// opens (e.g. /mnt/z/... -> Z:\...). Reachability is per-mount/dir, so the
|
|
67
65
|
// line-coordinate strip only needs to land in the right directory — exact
|
|
@@ -113,7 +111,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
113
111
|
}
|
|
114
112
|
args.path = coerceShapeFlex(args.path);
|
|
115
113
|
// Reachability preflight up front (all shapes) — before readPathStringGuardError /
|
|
116
|
-
//
|
|
114
|
+
// image stat, all of which can touch sync FS.
|
|
117
115
|
{
|
|
118
116
|
const _reErr = await _readReachPreflight(args.path, workDir, helpers);
|
|
119
117
|
if (_reErr) return _reErr;
|
|
@@ -186,8 +184,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
186
184
|
}
|
|
187
185
|
}
|
|
188
186
|
if (r?.limit !== undefined) entry.limit = r.limit;
|
|
189
|
-
if (r?.line !== undefined) entry.line = r.line;
|
|
190
|
-
if (r?.context !== undefined) entry.context = r.context;
|
|
191
187
|
if (r?.full !== undefined) entry.full = r.full;
|
|
192
188
|
entry = normaliseReadLineWindowArgs(entry, workDir);
|
|
193
189
|
return entry;
|
|
@@ -235,8 +231,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
235
231
|
if (args.n !== undefined) entry.n = args.n;
|
|
236
232
|
if (args.offset !== undefined) entry.offset = args.offset;
|
|
237
233
|
if (args.limit !== undefined) entry.limit = args.limit;
|
|
238
|
-
if (args.line !== undefined) entry.line = args.line;
|
|
239
|
-
if (args.context !== undefined) entry.context = args.context;
|
|
240
234
|
if (args.full !== undefined) entry.full = args.full;
|
|
241
235
|
entry = normaliseReadLineWindowArgs(entry, workDir);
|
|
242
236
|
return entry;
|
|
@@ -464,14 +458,13 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
464
458
|
if (span.note) args._symbolReadNote = `symbol ${sym}: ${span.note}`;
|
|
465
459
|
}
|
|
466
460
|
}
|
|
467
|
-
// A window (offset/limit
|
|
461
|
+
// A window (offset/limit or a path:line coordinate) beats a glance
|
|
468
462
|
// mode (head/tail/summary), which would otherwise read from a file end and
|
|
469
|
-
// silently drop the window. Drop the glance mode BEFORE line
|
|
470
|
-
// normalization so
|
|
471
|
-
// offset/limit
|
|
472
|
-
// set). count/hex are not text-window ops and keep their mode.
|
|
463
|
+
// silently drop the window. Drop the glance mode BEFORE path:line
|
|
464
|
+
// compatibility normalization so any path coordinate is converted to
|
|
465
|
+
// offset/limit. count/hex are not text-window ops and keep their mode.
|
|
473
466
|
{
|
|
474
|
-
const _win = args.offset != null || args.limit != null ||
|
|
467
|
+
const _win = args.offset != null || args.limit != null || hasLineCoordinate(args.path);
|
|
475
468
|
if (_win && (args.mode === 'head' || args.mode === 'tail' || args.mode === 'summary')) {
|
|
476
469
|
args = { ...args, mode: undefined };
|
|
477
470
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { statSync } from 'fs';
|
|
2
2
|
import { isAbsolute, resolve } from 'path';
|
|
3
3
|
import { trueCasePath } from './path-utils.mjs';
|
|
4
|
+
import { findBySuffixStrip } from './path-diagnostics.mjs';
|
|
4
5
|
import {
|
|
5
6
|
canonicalizeGlobSlashes,
|
|
6
7
|
coerceShapeFlex,
|
|
@@ -36,6 +37,11 @@ import {
|
|
|
36
37
|
import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
37
38
|
import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX } from './arg-guard.mjs';
|
|
38
39
|
|
|
40
|
+
// Default surrounding-lines window applied by output_mode:'content_with_context'
|
|
41
|
+
// when the caller does not pass an explicit -A/-B/-C/context. Sized to cover a
|
|
42
|
+
// typical function/block so a match arrives readable without a follow-up read.
|
|
43
|
+
const GREP_AUTO_CONTEXT_LINES = 25;
|
|
44
|
+
|
|
39
45
|
const MIXDOG_GREP_CASE_HINT_PROBE = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_GREP_CASE_HINT_PROBE || ''));
|
|
40
46
|
|
|
41
47
|
function expandLegacyEscapedAlternationPattern(rawPattern) {
|
|
@@ -353,18 +359,23 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
353
359
|
.map(canonicalizeGlobSlashes)
|
|
354
360
|
.filter((g) => !isRedundantAllFilesGlob(g)));
|
|
355
361
|
|
|
356
|
-
const ALLOWED_OUTPUT_MODES = new Set(['files_with_matches', 'content', 'count']);
|
|
362
|
+
const ALLOWED_OUTPUT_MODES = new Set(['files_with_matches', 'content', 'content_with_context', 'count']);
|
|
357
363
|
const rawOutputMode = typeof args.output_mode === 'string' ? args.output_mode.trim() : '';
|
|
358
364
|
if (rawOutputMode && !ALLOWED_OUTPUT_MODES.has(rawOutputMode)) {
|
|
359
365
|
return `Error: invalid output_mode ${JSON.stringify(args.output_mode)}; expected one of ${[...ALLOWED_OUTPUT_MODES].join(', ')}`;
|
|
360
366
|
}
|
|
367
|
+
// `content_with_context` is a convenience alias for `content` that auto-applies
|
|
368
|
+
// a generous surrounding-lines window, so the model can read a match in place
|
|
369
|
+
// without following up with `read`. It maps to content mode; the default
|
|
370
|
+
// context is applied below unless the caller passed an explicit -A/-B/-C/context.
|
|
371
|
+
const wantAutoContext = rawOutputMode === 'content_with_context';
|
|
361
372
|
// Default to `content` when output_mode is omitted. A pattern is always
|
|
362
373
|
// present here (the no-pattern case returned above), so this is a content
|
|
363
374
|
// search — it should return the matching lines WITH line numbers, not just
|
|
364
375
|
// filenames. Filename-only was forcing callers to re-grep for the actual
|
|
365
376
|
// coordinates (the explorer over-iteration root cause). `files_with_matches`
|
|
366
377
|
// is now opt-in; pure filename discovery belongs to `glob`.
|
|
367
|
-
const outputMode = rawOutputMode || 'content';
|
|
378
|
+
const outputMode = (rawOutputMode === 'content_with_context') ? 'content' : (rawOutputMode || 'content');
|
|
368
379
|
const headLimitRaw = args.head_limit;
|
|
369
380
|
const headLimitCoerced = coerceNonNegInt(headLimitRaw);
|
|
370
381
|
if (Number.isNaN(headLimitCoerced)) {
|
|
@@ -391,6 +402,12 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
391
402
|
let contextN = args['-C'] !== undefined && args['-C'] !== null && args['-C'] !== ''
|
|
392
403
|
? coerceContext(args['-C'])
|
|
393
404
|
: coerceContext(args.context);
|
|
405
|
+
// content_with_context: if no explicit context flag was supplied, apply a
|
|
406
|
+
// generous default so the match arrives with enough surrounding code to
|
|
407
|
+
// understand it (function-sized) without a separate read.
|
|
408
|
+
if (wantAutoContext && afterN === null && beforeN === null && contextN === null) {
|
|
409
|
+
contextN = GREP_AUTO_CONTEXT_LINES;
|
|
410
|
+
}
|
|
394
411
|
if (contextN !== null && contextN > 0) {
|
|
395
412
|
if (afterN === 0) afterN = null;
|
|
396
413
|
if (beforeN === 0) beforeN = null;
|
|
@@ -462,6 +479,10 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
|
|
|
462
479
|
try { grepStat = statSync(grepResolvedPath); }
|
|
463
480
|
catch (err) {
|
|
464
481
|
const msg = `Error: path does not exist: ${normalizeOutputPath(grepResolvedPath)} (${err?.code || 'ENOENT'})`;
|
|
482
|
+
const suffixHit = findBySuffixStrip(workDir, grepResolvedPath);
|
|
483
|
+
if (suffixHit) {
|
|
484
|
+
return msg + ` Not found at this path; the same filename exists at: "${normalizeOutputPath(suffixHit)}". Search that path directly.`;
|
|
485
|
+
}
|
|
465
486
|
return msg + await _suggestIndexedPaths(grepResolvedPath, executeChildBuiltinTool, workDir);
|
|
466
487
|
}
|
|
467
488
|
const filenameOmitted = grepStat.isFile();
|