mixdog 0.9.95 → 0.9.97
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +21 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +9 -6
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +31 -33
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +10 -3
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +38 -4
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import { closeSync, openSync, readSync } from 'fs';
|
|
2
|
+
import { open } from 'fs/promises';
|
|
3
|
+
|
|
4
|
+
const HEAD_CAP = 64 * 1024;
|
|
5
|
+
const TAIL_SIZE = 4 * 1024;
|
|
6
|
+
|
|
7
|
+
function hasUtf16Bom(buf, length = buf?.length || 0) {
|
|
8
|
+
return length >= 2
|
|
9
|
+
&& ((buf[0] === 0xff && buf[1] === 0xfe)
|
|
10
|
+
|| (buf[0] === 0xfe && buf[1] === 0xff));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function containsNull(buf, length = buf?.length || 0) {
|
|
14
|
+
for (let i = 0; i < length; i++) {
|
|
15
|
+
if (buf[i] === 0) return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
2
19
|
|
|
3
20
|
// Binary detection: reading a PNG / ELF / zip / compressed blob as utf-8
|
|
4
21
|
// pollutes the context with U+FFFD characters and wastes tokens. Sample the
|
|
5
22
|
// head and tail of the file and look for a null byte — the canonical signal
|
|
6
23
|
// that the file is not plain text. Head window scales with file size:
|
|
7
24
|
// min(fileSize, 64KB) head + 4KB tail, so a 250KB file with a null byte at
|
|
8
|
-
// 9KB or 249KB is caught equally.
|
|
9
|
-
//
|
|
25
|
+
// 9KB or 249KB is caught equally. This synchronous form remains for legacy
|
|
26
|
+
// mode readers. The main read path uses inspectBinaryFile(), which performs
|
|
27
|
+
// the same probes asynchronously.
|
|
10
28
|
// Callers inside the ≤READ_MAX_SIZE_BYTES branch should pass st.size so the
|
|
11
29
|
// tail probe fires; callers above the cap pass the real size from err.size.
|
|
12
30
|
export function isBinaryFile(fullPath, fileSize = 0) {
|
|
13
|
-
const HEAD_CAP = 64 * 1024; // 64 KB max head window
|
|
14
|
-
const TAIL_SIZE = 4 * 1024; // 4 KB tail probe
|
|
15
31
|
const headBytes = fileSize > 0 ? Math.min(fileSize, HEAD_CAP) : HEAD_CAP;
|
|
16
32
|
let fd = null;
|
|
17
33
|
try {
|
|
@@ -25,20 +41,14 @@ export function isBinaryFile(fullPath, fileSize = 0) {
|
|
|
25
41
|
// (detectReadEncodingFromBuffer/decodeReadBuffer support utf16le and
|
|
26
42
|
// utf16be), so exempt it rather than reject the file as binary.
|
|
27
43
|
// FF FE = UTF-16LE, FE FF = UTF-16BE.
|
|
28
|
-
if (nHead
|
|
29
|
-
|
|
30
|
-
|| (headBuf[0] === 0xfe && headBuf[1] === 0xff))) return false;
|
|
31
|
-
for (let i = 0; i < nHead; i++) {
|
|
32
|
-
if (headBuf[i] === 0) return true;
|
|
33
|
-
}
|
|
44
|
+
if (hasUtf16Bom(headBuf, nHead)) return false;
|
|
45
|
+
if (containsNull(headBuf, nHead)) return true;
|
|
34
46
|
// Tail probe (only when file is larger than head window)
|
|
35
47
|
if (fileSize > headBytes && fileSize > TAIL_SIZE) {
|
|
36
48
|
const tailOffset = fileSize - TAIL_SIZE;
|
|
37
49
|
const tailBuf = Buffer.allocUnsafe(TAIL_SIZE);
|
|
38
50
|
const nTail = readSync(fd, tailBuf, 0, TAIL_SIZE, tailOffset);
|
|
39
|
-
|
|
40
|
-
if (tailBuf[i] === 0) return true;
|
|
41
|
-
}
|
|
51
|
+
if (containsNull(tailBuf, nTail)) return true;
|
|
42
52
|
}
|
|
43
53
|
return false;
|
|
44
54
|
} catch {
|
|
@@ -48,8 +58,71 @@ export function isBinaryFile(fullPath, fileSize = 0) {
|
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
|
|
61
|
+
/** Classify an already-loaded whole file without another open/read cycle. */
|
|
62
|
+
export function isBinaryBuffer(buf, fileSize = buf?.length || 0) {
|
|
63
|
+
if (!Buffer.isBuffer(buf) || buf.length === 0) return false;
|
|
64
|
+
const size = fileSize > 0 ? Math.min(fileSize, buf.length) : buf.length;
|
|
65
|
+
const headBytes = Math.min(size, HEAD_CAP);
|
|
66
|
+
if (hasUtf16Bom(buf, headBytes)) return false;
|
|
67
|
+
if (containsNull(buf, headBytes)) return true;
|
|
68
|
+
if (size > headBytes && size > TAIL_SIZE) {
|
|
69
|
+
const tail = buf.subarray(Math.max(0, size - TAIL_SIZE), size);
|
|
70
|
+
if (containsNull(tail)) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Async binary probe that keeps one file handle for head, tail, and preview.
|
|
77
|
+
* Returns the head bytes even when the null marker is in the tail so callers
|
|
78
|
+
* can render a hex preview without reopening the file.
|
|
79
|
+
*/
|
|
80
|
+
export async function inspectBinaryFile(fullPath, fileSize = 0, { previewBytes = 256 } = {}) {
|
|
81
|
+
const headBytes = fileSize > 0 ? Math.min(fileSize, HEAD_CAP) : HEAD_CAP;
|
|
82
|
+
let fh;
|
|
83
|
+
try {
|
|
84
|
+
fh = await open(fullPath, 'r');
|
|
85
|
+
const headBuf = Buffer.allocUnsafe(Math.max(0, headBytes));
|
|
86
|
+
const { bytesRead: nHead } = await fh.read(headBuf, 0, headBytes, 0);
|
|
87
|
+
const head = headBuf.subarray(0, nHead);
|
|
88
|
+
const preview = head.subarray(0, Math.min(previewBytes, nHead));
|
|
89
|
+
if (nHead === 0 || hasUtf16Bom(head, nHead)) return { isBinary: false, preview };
|
|
90
|
+
if (containsNull(head, nHead)) return { isBinary: true, preview };
|
|
91
|
+
if (fileSize > headBytes && fileSize > TAIL_SIZE) {
|
|
92
|
+
const tailBuf = Buffer.allocUnsafe(TAIL_SIZE);
|
|
93
|
+
const { bytesRead: nTail } = await fh.read(
|
|
94
|
+
tailBuf,
|
|
95
|
+
0,
|
|
96
|
+
TAIL_SIZE,
|
|
97
|
+
fileSize - TAIL_SIZE,
|
|
98
|
+
);
|
|
99
|
+
if (containsNull(tailBuf, nTail)) return { isBinary: true, preview };
|
|
100
|
+
}
|
|
101
|
+
return { isBinary: false, preview };
|
|
102
|
+
} catch {
|
|
103
|
+
return { isBinary: false, preview: Buffer.alloc(0) };
|
|
104
|
+
} finally {
|
|
105
|
+
if (fh) { try { await fh.close(); } catch {} }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
51
109
|
const BINARY_PREVIEW_BYTES = 256;
|
|
52
110
|
|
|
111
|
+
export function formatBinaryReadPreviewFromBuffer(
|
|
112
|
+
buffer,
|
|
113
|
+
displayPath,
|
|
114
|
+
fileSize,
|
|
115
|
+
{ previewBytes = BINARY_PREVIEW_BYTES } = {},
|
|
116
|
+
) {
|
|
117
|
+
const source = Buffer.isBuffer(buffer) ? buffer : Buffer.alloc(0);
|
|
118
|
+
const slice = source.subarray(0, Math.min(previewBytes, source.length));
|
|
119
|
+
const hex = Array.from(slice).map((b) => b.toString(16).padStart(2, '0')).join(' ');
|
|
120
|
+
const disp = displayPath || '(binary file)';
|
|
121
|
+
const note = `binary, ${fileSize} byte${fileSize === 1 ? '' : 's'}`;
|
|
122
|
+
const text = `${disp}\n${note}\n${hex || '(empty)'}`;
|
|
123
|
+
return { text, snapshotMeta: { source: 'read_hex', ranges: [] } };
|
|
124
|
+
}
|
|
125
|
+
|
|
53
126
|
/** Short hex preview for read when null bytes mark the file as binary. */
|
|
54
127
|
export function formatBinaryReadPreview(fullPath, displayPath, fileSize, { previewBytes = BINARY_PREVIEW_BYTES } = {}) {
|
|
55
128
|
const n = Math.max(0, Math.min(previewBytes, fileSize > 0 ? fileSize : previewBytes));
|
|
@@ -58,12 +131,12 @@ export function formatBinaryReadPreview(fullPath, displayPath, fileSize, { previ
|
|
|
58
131
|
fd = openSync(fullPath, 'r');
|
|
59
132
|
const buf = Buffer.alloc(n);
|
|
60
133
|
const bytesRead = readSync(fd, buf, 0, n, 0);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
134
|
+
return formatBinaryReadPreviewFromBuffer(
|
|
135
|
+
buf.subarray(0, bytesRead),
|
|
136
|
+
displayPath || fullPath,
|
|
137
|
+
fileSize,
|
|
138
|
+
{ previewBytes },
|
|
139
|
+
);
|
|
67
140
|
} catch {
|
|
68
141
|
const disp = displayPath || fullPath;
|
|
69
142
|
return {
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// only (behavior + argument shapes); usage policy lives in rules/shared/01-tool.md.
|
|
6
6
|
import {
|
|
7
7
|
TOOL_ASYNC_EXECUTION_CONTRACT,
|
|
8
|
-
TOOL_MANUAL_CONTROL_CONTRACT,
|
|
9
8
|
executionModeSchemaDescription,
|
|
10
9
|
} from '../../../../shared/background-tasks.mjs';
|
|
11
10
|
|
|
@@ -28,7 +27,7 @@ function _shellMaxTimeoutMs() {
|
|
|
28
27
|
// process lifetime, so this is evaluated once at module load.
|
|
29
28
|
const _shellSyntaxCheat =
|
|
30
29
|
process.platform === 'win32'
|
|
31
|
-
? ' PowerShell:
|
|
30
|
+
? ' PowerShell: use ; between commands, /c/→C:\\, and note that $PID is reserved.'
|
|
32
31
|
: '';
|
|
33
32
|
|
|
34
33
|
export const BUILTIN_TOOLS = [
|
|
@@ -36,7 +35,7 @@ export const BUILTIN_TOOLS = [
|
|
|
36
35
|
name: 'read',
|
|
37
36
|
title: 'Mixdog Read',
|
|
38
37
|
annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
39
|
-
description: 'Read file contents
|
|
38
|
+
description: 'Read file contents or line ranges; not directories.',
|
|
40
39
|
inputSchema: {
|
|
41
40
|
type: 'object',
|
|
42
41
|
properties: {
|
|
@@ -99,12 +98,12 @@ export const BUILTIN_TOOLS = [
|
|
|
99
98
|
name: 'task',
|
|
100
99
|
title: 'Background Task Control',
|
|
101
100
|
annotations: { title: 'Background Task Control', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
102
|
-
description:
|
|
101
|
+
description: 'Control a shell background task_id; not session or agent ids.',
|
|
103
102
|
inputSchema: {
|
|
104
103
|
type: 'object',
|
|
105
104
|
properties: {
|
|
106
105
|
task_id: { type: 'string', description: 'shell async task_id.' },
|
|
107
|
-
action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: '
|
|
106
|
+
action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Default list; with task_id, default wait.' },
|
|
108
107
|
timeout_ms: { type: 'number', description: 'Wait timeout ms.' },
|
|
109
108
|
},
|
|
110
109
|
required: [],
|
|
@@ -115,7 +114,7 @@ export const BUILTIN_TOOLS = [
|
|
|
115
114
|
name: 'grep',
|
|
116
115
|
title: 'Mixdog Grep',
|
|
117
116
|
annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
118
|
-
description: '
|
|
117
|
+
description: 'Literal/regex search over file/dir scopes; returns source blocks with context.',
|
|
119
118
|
inputSchema: {
|
|
120
119
|
type: 'object',
|
|
121
120
|
properties: {
|
|
@@ -156,7 +155,7 @@ export const BUILTIN_TOOLS = [
|
|
|
156
155
|
name: 'glob',
|
|
157
156
|
title: 'Mixdog Glob',
|
|
158
157
|
annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
159
|
-
description: 'Match
|
|
158
|
+
description: 'Match glob patterns under base directories.',
|
|
160
159
|
inputSchema: {
|
|
161
160
|
type: 'object',
|
|
162
161
|
properties: {
|
|
@@ -185,7 +184,7 @@ export const BUILTIN_TOOLS = [
|
|
|
185
184
|
name: 'find',
|
|
186
185
|
title: 'Mixdog Find Files',
|
|
187
186
|
annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
188
|
-
description: 'Fuzzy
|
|
187
|
+
description: 'Fuzzy partial path/name lookup; returns paths only.',
|
|
189
188
|
inputSchema: {
|
|
190
189
|
type: 'object',
|
|
191
190
|
properties: {
|
|
@@ -207,7 +206,7 @@ export const BUILTIN_TOOLS = [
|
|
|
207
206
|
name: 'list',
|
|
208
207
|
title: 'Mixdog List Directory',
|
|
209
208
|
annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
210
|
-
description: 'List directory entries (path + type)
|
|
209
|
+
description: 'List directory entries (path + type).',
|
|
211
210
|
inputSchema: {
|
|
212
211
|
type: 'object',
|
|
213
212
|
properties: {
|
|
@@ -238,7 +238,7 @@ export function getCachedReadOnlyStat(fullPath, loader = statSync, now = Date.no
|
|
|
238
238
|
return stat;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
export async function statPathsForMtime(paths, workDir, concurrency =
|
|
241
|
+
export async function statPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}) {
|
|
242
242
|
const items = Array.isArray(paths) ? paths : [];
|
|
243
243
|
const out = new Array(items.length);
|
|
244
244
|
const now = Date.now();
|
|
@@ -302,7 +302,7 @@ export async function statPathsForMtime(paths, workDir, concurrency = 64, opts =
|
|
|
302
302
|
// lstat variant — does NOT follow symlinks. Use for directory-listing
|
|
303
303
|
// surfaces where a symlink to a 200 GB file should report as a symlink,
|
|
304
304
|
// not as the target's size/mtime.
|
|
305
|
-
export async function lstatPathsForMtime(paths, workDir, concurrency =
|
|
305
|
+
export async function lstatPathsForMtime(paths, workDir, concurrency = Infinity, opts = {}) {
|
|
306
306
|
const items = Array.isArray(paths) ? paths : [];
|
|
307
307
|
const out = new Array(items.length);
|
|
308
308
|
const inflight = new Map();
|
|
@@ -19,8 +19,8 @@ export function isBlockedDevicePath(p) {
|
|
|
19
19
|
|
|
20
20
|
// R1: UNC / SMB share paths (\\server\share or //server/share). Reading these
|
|
21
21
|
// causes Windows to auto-authenticate to the remote host, leaking the NTLM
|
|
22
|
-
// hash of the current user to any attacker-controlled SMB target.
|
|
23
|
-
//
|
|
22
|
+
// hash of the current user to any attacker-controlled SMB target. The prefix
|
|
23
|
+
// is rejected before stat. Accepts both
|
|
24
24
|
// backslash and forward-slash leaders so a normalize that picked either form
|
|
25
25
|
// is caught.
|
|
26
26
|
export function isUncPath(p) {
|
|
@@ -47,6 +47,6 @@ export async function assertPathReachable(path, deadlineMs = FS_REACHABILITY_DEA
|
|
|
47
47
|
// the wall-clock cost is one deadline, not N.
|
|
48
48
|
export async function assertPathsReachable(paths, deadlineMs = FS_REACHABILITY_DEADLINE_MS) {
|
|
49
49
|
const list = Array.isArray(paths) ? paths.filter((p) => typeof p === 'string' && p.length) : [];
|
|
50
|
-
if (list.length === 0) return;
|
|
51
|
-
await Promise.all(list.map((p) => assertPathReachable(p, deadlineMs)));
|
|
50
|
+
if (list.length === 0) return [];
|
|
51
|
+
return await Promise.all(list.map((p) => assertPathReachable(p, deadlineMs)));
|
|
52
52
|
}
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
} from './search-input-helpers.mjs';
|
|
18
18
|
|
|
19
19
|
export const GREP_CONTEXT_CHAR_BUDGET_DEFAULT = 10_000;
|
|
20
|
-
const GREP_CONTEXT_READ_CONCURRENCY = 4;
|
|
21
20
|
const GREP_FOCUSED_CONTEXT_RADIUS = 12;
|
|
22
21
|
const GREP_FOCUSED_RAW_BLOCKS = 3;
|
|
23
22
|
// Anchors must stay usable as evidence without a follow-up read: keep the
|
|
@@ -229,21 +228,13 @@ async function readAnchorSources(anchors, radius, signal) {
|
|
|
229
228
|
groups.get(anchor.absolutePath).anchors.push(anchor);
|
|
230
229
|
}
|
|
231
230
|
const entries = [...groups.values()];
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
entry.lines = await readFileWindows(entry, radius, signal);
|
|
238
|
-
} catch (err) {
|
|
239
|
-
entry.error = err;
|
|
240
|
-
}
|
|
231
|
+
await Promise.all(entries.map(async (entry) => {
|
|
232
|
+
try {
|
|
233
|
+
entry.lines = await readFileWindows(entry, radius, signal);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
entry.error = err;
|
|
241
236
|
}
|
|
242
|
-
};
|
|
243
|
-
await Promise.all(Array.from(
|
|
244
|
-
{ length: Math.min(GREP_CONTEXT_READ_CONCURRENCY, entries.length) },
|
|
245
|
-
() => worker(),
|
|
246
|
-
));
|
|
237
|
+
}));
|
|
247
238
|
return groups;
|
|
248
239
|
}
|
|
249
240
|
|
|
@@ -18,10 +18,12 @@ import {
|
|
|
18
18
|
cacheGet,
|
|
19
19
|
cacheSet,
|
|
20
20
|
getCachedReadOnlyStat,
|
|
21
|
+
lstatPathsForMtime,
|
|
21
22
|
statCacheSet,
|
|
22
23
|
statPathsForMtime,
|
|
23
24
|
registerCacheInvalidationListener,
|
|
24
25
|
} from './cache-layers.mjs';
|
|
26
|
+
import { formatListSize, formatMtime } from './list-formatting.mjs';
|
|
25
27
|
import {
|
|
26
28
|
compileSimpleGlob,
|
|
27
29
|
NOISE_DIR_NAMES,
|
|
@@ -88,16 +90,12 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
88
90
|
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
89
91
|
if (Array.isArray(args.path)) {
|
|
90
92
|
const list = [...new Set(args.path.map((p) => (typeof p === 'string' ? p.trim() : '')).filter((p) => p.length > 0))];
|
|
91
|
-
const
|
|
92
|
-
const targets = capped ? list.slice(0, 10) : list;
|
|
93
|
+
const targets = list;
|
|
93
94
|
if (targets.length > 1) {
|
|
94
|
-
//
|
|
95
|
+
// Full parallel fan-out: each target is an independent listing
|
|
95
96
|
// (own guard/cache/reachability), so run them concurrently instead
|
|
96
97
|
// of serially. Bodies land in a fixed-index array so the emitted
|
|
97
|
-
// section order still matches the caller's `path[]` order
|
|
98
|
-
// wall-clock cost changes. Concurrency is capped so a 10-path batch
|
|
99
|
-
// cannot exhaust the child-spawn / FS-handle budget.
|
|
100
|
-
const LIST_FANOUT_CONCURRENCY = 4;
|
|
98
|
+
// section order still matches the caller's `path[]` order.
|
|
101
99
|
// Collapse semantic-duplicate targets: two spellings that resolve to
|
|
102
100
|
// the same directory (e.g. `foo` and `./foo`) are stat'd/walked ONCE.
|
|
103
101
|
// Each caller label keeps its own `# list <p>` section reusing the
|
|
@@ -116,24 +114,14 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
116
114
|
if (!repByKey.has(k)) { repByKey.set(k, targets[i]); uniqueKeys.push(k); }
|
|
117
115
|
}
|
|
118
116
|
const bodyByKey = new Map();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const k = uniqueKeys[i];
|
|
125
|
-
try {
|
|
126
|
-
bodyByKey.set(k, await executeListTool({ ...args, path: repByKey.get(k) }, workDir, options));
|
|
127
|
-
} catch (err) {
|
|
128
|
-
bodyByKey.set(k, `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`);
|
|
129
|
-
}
|
|
117
|
+
await Promise.all(uniqueKeys.map(async (k) => {
|
|
118
|
+
try {
|
|
119
|
+
bodyByKey.set(k, await executeListTool({ ...args, path: repByKey.get(k) }, workDir, options));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
bodyByKey.set(k, `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`);
|
|
130
122
|
}
|
|
131
|
-
};
|
|
132
|
-
await Promise.all(
|
|
133
|
-
Array.from({ length: Math.min(LIST_FANOUT_CONCURRENCY, uniqueKeys.length) }, runWorker),
|
|
134
|
-
);
|
|
123
|
+
}));
|
|
135
124
|
const sections = targets.map((p, i) => `# list ${p}\n${bodyByKey.get(keyByIndex[i])}`);
|
|
136
|
-
if (capped) sections.push(`... [capped at 10 of ${list.length} paths]`);
|
|
137
125
|
return sections.join('\n\n');
|
|
138
126
|
}
|
|
139
127
|
args.path = targets[0];
|
|
@@ -582,8 +570,7 @@ async function getTargetedFindEnumeration({
|
|
|
582
570
|
export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
583
571
|
if (Array.isArray(args.query)) {
|
|
584
572
|
const list = [...new Set(args.query.map((q) => (typeof q === 'string' ? q.trim() : '')).filter((q) => q.length > 0))];
|
|
585
|
-
const
|
|
586
|
-
const targets = capped ? list.slice(0, 5) : list;
|
|
573
|
+
const targets = list;
|
|
587
574
|
if (targets.length > 1) {
|
|
588
575
|
// head_limit is one call-level result budget, not a multiplier per
|
|
589
576
|
// query. Split it deterministically in caller order; if it cannot
|
|
@@ -601,7 +588,6 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
601
588
|
+ `retry query=${JSON.stringify(omittedByHeadLimit)}]`,
|
|
602
589
|
);
|
|
603
590
|
}
|
|
604
|
-
if (capped) notes.push(`... [capped at 5 of ${list.length} queries]`);
|
|
605
591
|
const sectionHeaders = activeTargets.map((query) => `# find ${query}\n`);
|
|
606
592
|
const separatorBytes = Math.max(0, activeTargets.length + notes.length - 1) * 2;
|
|
607
593
|
const fixedBytes = sectionHeaders.reduce(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile, stat } from 'fs/promises';
|
|
2
2
|
import { extname } from 'path';
|
|
3
3
|
import {
|
|
4
4
|
API_IMAGE_MAX_BASE64_SIZE,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
resizeImageBuffer,
|
|
7
7
|
} from './read-image-resize.mjs';
|
|
8
8
|
|
|
9
|
-
// Image extensions
|
|
9
|
+
// Image extensions that Read renders as image blocks.
|
|
10
10
|
const IMAGE_MIME = {
|
|
11
11
|
'.png': 'image/png',
|
|
12
12
|
'.jpg': 'image/jpeg',
|
|
@@ -36,14 +36,14 @@ export function imageMimeForPath(p) {
|
|
|
36
36
|
// sharp absent / resize failed: GRACEFUL fallback to the legacy
|
|
37
37
|
// pass-through-with-cap behaviour (refuse over-cap originals with an isError
|
|
38
38
|
// text result, else emit the raw base64 image block). No throw on missing sharp.
|
|
39
|
-
export async function readImageAsContent(fullPath, displayPath) {
|
|
39
|
+
export async function readImageAsContent(fullPath, displayPath, preflightStat = null) {
|
|
40
40
|
const mimeType = imageMimeForPath(fullPath);
|
|
41
41
|
if (!mimeType) return null;
|
|
42
42
|
let st;
|
|
43
|
-
try { st =
|
|
43
|
+
try { st = preflightStat || await stat(fullPath); } catch { return null; }
|
|
44
44
|
|
|
45
45
|
let buf;
|
|
46
|
-
try { buf =
|
|
46
|
+
try { buf = await readFile(fullPath); } catch { return null; }
|
|
47
47
|
if (buf.length === 0) {
|
|
48
48
|
return {
|
|
49
49
|
content: [{ type: 'text', text: `Error: image "${displayPath}" is empty (0 bytes).` }],
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { lstatSync, realpathSync, readdirSync, statSync } from 'fs';
|
|
2
1
|
import * as fsPromises from 'fs/promises';
|
|
3
2
|
import { readFile } from 'fs/promises';
|
|
4
3
|
import { extname } from 'path';
|
|
@@ -7,7 +6,11 @@ import { buildNotFoundHint, finalizeReadFamilyEnoentTail, tryReadFamilyEnoentRed
|
|
|
7
6
|
import { getReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
8
7
|
import { recordReadPathRedirect } from './snapshot-store.mjs';
|
|
9
8
|
import { snapshotCoversFullFile, statMatchesSnapshot } from './snapshot-helpers.mjs';
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
formatBinaryReadPreviewFromBuffer,
|
|
11
|
+
inspectBinaryFile,
|
|
12
|
+
isBinaryBuffer,
|
|
13
|
+
} from './binary-file.mjs';
|
|
11
14
|
|
|
12
15
|
function snapshotBodyWasReturnedByRead(snapshot) {
|
|
13
16
|
return String(snapshot?.source || '').startsWith('read');
|
|
@@ -27,22 +30,27 @@ function withSymbolReadNote(text, args) {
|
|
|
27
30
|
// decoding. utf8-with-BOM (EF BB BF) keeps the utf-8 decoder; its leading
|
|
28
31
|
// U+FEFF is stripped for display downstream, so bomLen is reported but not
|
|
29
32
|
// applied for utf8.
|
|
33
|
+
function detectReadEncodingFromBuffer(head) {
|
|
34
|
+
const n = head?.length || 0;
|
|
35
|
+
if (n >= 2 && head[0] === 0xff && head[1] === 0xfe) {
|
|
36
|
+
return { encoding: 'utf16le', bomLen: 2 };
|
|
37
|
+
}
|
|
38
|
+
if (n >= 2 && head[0] === 0xfe && head[1] === 0xff) {
|
|
39
|
+
return { encoding: 'utf16be', bomLen: 2 };
|
|
40
|
+
}
|
|
41
|
+
if (n >= 3 && head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) {
|
|
42
|
+
return { encoding: 'utf8', bomLen: 3 };
|
|
43
|
+
}
|
|
44
|
+
return { encoding: 'utf8', bomLen: 0 };
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
async function detectReadEncoding(fullPath) {
|
|
31
48
|
let fh;
|
|
32
49
|
try {
|
|
33
50
|
fh = await fsPromises.open(fullPath, 'r');
|
|
34
51
|
const head = Buffer.alloc(3);
|
|
35
52
|
const { bytesRead: n } = await fh.read(head, 0, 3, 0);
|
|
36
|
-
|
|
37
|
-
return { encoding: 'utf16le', bomLen: 2 };
|
|
38
|
-
}
|
|
39
|
-
if (n >= 2 && head[0] === 0xfe && head[1] === 0xff) {
|
|
40
|
-
return { encoding: 'utf16be', bomLen: 2 };
|
|
41
|
-
}
|
|
42
|
-
if (n >= 3 && head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) {
|
|
43
|
-
return { encoding: 'utf8', bomLen: 3 };
|
|
44
|
-
}
|
|
45
|
-
return { encoding: 'utf8', bomLen: 0 };
|
|
53
|
+
return detectReadEncodingFromBuffer(head.subarray(0, n));
|
|
46
54
|
} catch {
|
|
47
55
|
return { encoding: 'utf8', bomLen: 0 };
|
|
48
56
|
} finally {
|
|
@@ -112,7 +120,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
112
120
|
// R1: UNC / SMB share reject (\\server\share, //server/share). Reading
|
|
113
121
|
// these on Windows auto-authenticates to the remote host and leaks the
|
|
114
122
|
// NTLM hash of the current user to any attacker-controlled SMB target.
|
|
115
|
-
//
|
|
123
|
+
// The prefix is rejected before stat.
|
|
116
124
|
// Must run before resolveAgainstCwd so a relative path can't be coerced
|
|
117
125
|
// into a UNC share by the cwd resolution.
|
|
118
126
|
if (typeof isUncPath === 'function' && isUncPath(filePath))
|
|
@@ -142,7 +150,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
142
150
|
return `Error: cannot read Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(fullPath)}`;
|
|
143
151
|
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(fullPath))
|
|
144
152
|
return `Error: cannot read Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(fullPath)}`;
|
|
145
|
-
// Pre-read size cap
|
|
153
|
+
// Pre-read size cap:
|
|
146
154
|
// throw a small error response when the file is too big rather
|
|
147
155
|
// than truncating to 25K tokens of content. Throw is decisively
|
|
148
156
|
// more token-efficient (Anthropic #21841 reverted truncation).
|
|
@@ -164,7 +172,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
164
172
|
let st;
|
|
165
173
|
let _statErr;
|
|
166
174
|
try {
|
|
167
|
-
st =
|
|
175
|
+
st = options?._preflightStats?.get?.(fullPath) || await fsPromises.stat(fullPath);
|
|
168
176
|
} catch (err) {
|
|
169
177
|
// Fall through to the existing similar-file recovery path below.
|
|
170
178
|
st = null;
|
|
@@ -174,7 +182,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
174
182
|
if (st.isDirectory()) {
|
|
175
183
|
let entries = [];
|
|
176
184
|
try {
|
|
177
|
-
entries =
|
|
185
|
+
entries = (await fsPromises.readdir(fullPath, { withFileTypes: true }))
|
|
178
186
|
.slice(0, 20)
|
|
179
187
|
.map((entry) => `${entry.name}${entry.isDirectory() ? '/' : ''}`);
|
|
180
188
|
} catch { /* best-effort preview */ }
|
|
@@ -191,14 +199,14 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
191
199
|
return `Error: cannot read special file (FIFO / character / block device / socket): ${normalizeOutputPath(filePath)}`;
|
|
192
200
|
// R1+R2: realpath the resolved path so a symlink → /dev/zero (or any
|
|
193
201
|
// other blocked device, UNC, or Windows reserved name) is caught on
|
|
194
|
-
// the REAL target, not the symlink name.
|
|
195
|
-
// the entry IS a symlink first so
|
|
202
|
+
// the REAL target, not the symlink name. lstat detects whether
|
|
203
|
+
// the entry IS a symlink first so realpath is only called when
|
|
196
204
|
// it would actually differ — saves a syscall on the common case.
|
|
197
205
|
try {
|
|
198
|
-
const _lst =
|
|
206
|
+
const _lst = await fsPromises.lstat(fullPath);
|
|
199
207
|
if (_lst && typeof _lst.isSymbolicLink === 'function' && _lst.isSymbolicLink()) {
|
|
200
208
|
let _realTarget = null;
|
|
201
|
-
try { _realTarget =
|
|
209
|
+
try { _realTarget = await fsPromises.realpath(fullPath); } catch { _realTarget = null; }
|
|
202
210
|
if (_realTarget && _realTarget !== fullPath) {
|
|
203
211
|
if (isBlockedDevicePath(_realTarget))
|
|
204
212
|
return `Error: cannot read device file via symlink (would block or produce infinite output): ${normalizeOutputPath(filePath)} → ${normalizeOutputPath(_realTarget)}`;
|
|
@@ -211,7 +219,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
211
219
|
// the target stat could differ from the link stat in
|
|
212
220
|
// pathological cases (replaced under us).
|
|
213
221
|
try {
|
|
214
|
-
const _rst =
|
|
222
|
+
const _rst = await fsPromises.stat(_realTarget);
|
|
215
223
|
if (typeof isSpecialFileStat === 'function' && isSpecialFileStat(_rst))
|
|
216
224
|
return `Error: cannot read special file via symlink (FIFO / character / block device / socket): ${normalizeOutputPath(filePath)} → ${normalizeOutputPath(_realTarget)}`;
|
|
217
225
|
} catch { /* if the target is gone, let the normal path surface ENOENT */ }
|
|
@@ -430,11 +438,42 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
430
438
|
// large UTF-16LE+BOM file is recognized as text up front and routed to
|
|
431
439
|
// the bounded in-memory utf16le path below, never mis-decoded as utf-8
|
|
432
440
|
// or rejected by the utf-8 streaming/binary branch (Bug 1).
|
|
433
|
-
const
|
|
441
|
+
const _preferRangeStream = hasRangeArgs && !wantFull
|
|
442
|
+
&& (options?.forceReadRangeStream === true || st.size > READ_STREAM_RANGE_MIN_BYTES);
|
|
443
|
+
const _preferSmartStream = !hasRangeArgs && !wantFull
|
|
444
|
+
&& st.size >= READ_SMART_STREAM_MIN_BYTES;
|
|
445
|
+
const _preferBufferedRead = st.size <= READ_MAX_SIZE_BYTES
|
|
446
|
+
&& !_preferRangeStream
|
|
447
|
+
&& !_preferSmartStream;
|
|
448
|
+
let _prefetchedRawBuf = null;
|
|
449
|
+
let _prefetchedRawFromCache = false;
|
|
450
|
+
if (_preferBufferedRead) {
|
|
451
|
+
const _cachedRaw = _rawContentCacheGet ? _rawContentCacheGet(fullPath, st) : null;
|
|
452
|
+
if (_cachedRaw) {
|
|
453
|
+
_prefetchedRawBuf = _cachedRaw;
|
|
454
|
+
_prefetchedRawFromCache = true;
|
|
455
|
+
} else {
|
|
456
|
+
try { _prefetchedRawBuf = await readFile(fullPath); } catch { _prefetchedRawBuf = null; }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const _readEnc = _prefetchedRawBuf
|
|
460
|
+
? detectReadEncodingFromBuffer(_prefetchedRawBuf.subarray(0, Math.min(3, _prefetchedRawBuf.length)))
|
|
461
|
+
: await detectReadEncoding(fullPath);
|
|
434
462
|
// UTF-16 (LE or BE) reads share one constraint: the streaming/binary
|
|
435
463
|
// paths decode chunks as utf-8, so a BOM-flagged UTF-16 file must route
|
|
436
464
|
// to the bounded in-memory decode below regardless of byte order.
|
|
437
465
|
const _isUtf16 = _readEnc.encoding === 'utf16le' || _readEnc.encoding === 'utf16be';
|
|
466
|
+
let _binaryInspection = null;
|
|
467
|
+
const _inspectBinary = async () => {
|
|
468
|
+
if (_binaryInspection) return _binaryInspection;
|
|
469
|
+
_binaryInspection = _prefetchedRawBuf
|
|
470
|
+
? {
|
|
471
|
+
isBinary: isBinaryBuffer(_prefetchedRawBuf, st.size),
|
|
472
|
+
preview: _prefetchedRawBuf.subarray(0, Math.min(256, _prefetchedRawBuf.length)),
|
|
473
|
+
}
|
|
474
|
+
: await inspectBinaryFile(fullPath, st.size);
|
|
475
|
+
return _binaryInspection;
|
|
476
|
+
};
|
|
438
477
|
if (st.size > READ_MAX_SIZE_BYTES) {
|
|
439
478
|
// .pdf/.ipynb were already dispatched up front (MEDIA-WINS), so this
|
|
440
479
|
// >10MiB branch only handles the non-media text path from here on.
|
|
@@ -450,8 +489,13 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
450
489
|
if (!hasRangeArgs) {
|
|
451
490
|
return `Error: file size ${st.size} bytes exceeds ${READ_MAX_SIZE_BYTES}-byte cap.`;
|
|
452
491
|
}
|
|
453
|
-
|
|
454
|
-
|
|
492
|
+
const _binary = await _inspectBinary();
|
|
493
|
+
if (_binary.isBinary) {
|
|
494
|
+
const { text, snapshotMeta } = formatBinaryReadPreviewFromBuffer(
|
|
495
|
+
_binary.preview,
|
|
496
|
+
normalizeOutputPath(filePath),
|
|
497
|
+
st.size,
|
|
498
|
+
);
|
|
455
499
|
_recordReadSnapshot(fullPath, st, readStateScope, snapshotMeta);
|
|
456
500
|
_cacheSet(cacheKey, text, { paths: [fullPath], readSnapshotMeta: snapshotMeta });
|
|
457
501
|
return withSymbolReadNote(text, args);
|
|
@@ -510,11 +554,18 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
510
554
|
// unambiguous TEXT signal, so classify it as utf16le up front and skip
|
|
511
555
|
// NUL rejection. (_readEnc was detected above, before the >10MiB size
|
|
512
556
|
// branch, so the large-file path can classify utf16le.)
|
|
513
|
-
if (!_isUtf16
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
557
|
+
if (!_isUtf16) {
|
|
558
|
+
const _binary = await _inspectBinary();
|
|
559
|
+
if (_binary.isBinary) {
|
|
560
|
+
const { text, snapshotMeta } = formatBinaryReadPreviewFromBuffer(
|
|
561
|
+
_binary.preview,
|
|
562
|
+
normalizeOutputPath(filePath),
|
|
563
|
+
st.size,
|
|
564
|
+
);
|
|
565
|
+
_recordReadSnapshot(fullPath, st, readStateScope, snapshotMeta);
|
|
566
|
+
_cacheSet(cacheKey, text, { paths: [fullPath], readSnapshotMeta: snapshotMeta });
|
|
567
|
+
return withSymbolReadNote(text, args);
|
|
568
|
+
}
|
|
518
569
|
}
|
|
519
570
|
// Whole-file reads above READ_WHOLE_FILE_MAX_BYTES use stream smart-elide
|
|
520
571
|
// (then READ_MAX_OUTPUT_BYTES truncation) instead of refusing. Absolute
|
|
@@ -588,8 +639,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
588
639
|
}
|
|
589
640
|
}
|
|
590
641
|
try {
|
|
591
|
-
const
|
|
592
|
-
const rawBuf = cachedRawBuf || await readFile(fullPath);
|
|
642
|
+
const rawBuf = _prefetchedRawBuf || await readFile(fullPath);
|
|
593
643
|
// Encoding-aware decode (fresh read AND raw-content cache hit both
|
|
594
644
|
// flow through here). For a BOM-flagged UTF-16LE file, strip the
|
|
595
645
|
// 2-byte FF FE BOM and decode as utf16le so it reverses the write
|
|
@@ -614,7 +664,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
614
664
|
// the cache + snapshot record stale bytes.
|
|
615
665
|
let _stPostRead;
|
|
616
666
|
let _readStableForRawCache = true;
|
|
617
|
-
if (
|
|
667
|
+
if (_prefetchedRawFromCache) {
|
|
618
668
|
_stPostRead = st;
|
|
619
669
|
} else {
|
|
620
670
|
try { _stPostRead = await fsPromises.stat(fullPath); } catch { _stPostRead = st; }
|
|
@@ -712,7 +762,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
712
762
|
out = appendReadContextAdvisory(out, { filePath, lineCount, bytes: st.size });
|
|
713
763
|
}
|
|
714
764
|
}
|
|
715
|
-
//
|
|
765
|
+
// An empty file gets a system-reminder instead of
|
|
716
766
|
// a bare `1│` line. The reminder makes the empty-state
|
|
717
767
|
// explicit so the agent doesn't assume content was elided.
|
|
718
768
|
if (content.length === 0) {
|