@xiaoso/dsh-tool-plus 0.1.0-beta.3 → 0.1.0-beta.4
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/lib/abort-D1_Z5Ut5.mjs +51 -0
- package/lib/abort-dIkZBV7Q.mjs +51 -0
- package/lib/amazon-bedrock-B0nxT06F.mjs +917 -0
- package/lib/amazon-bedrock-WQhvjHYu.mjs +917 -0
- package/lib/anthropic-CJCauvYz.mjs +3115 -0
- package/lib/anthropic-CZghe_9U.mjs +3115 -0
- package/lib/anthropic-sCMH7JID.mjs +262 -0
- package/lib/anthropic-tTntD8BD.mjs +262 -0
- package/lib/api-key-login-BzqNdM2y.mjs +157 -0
- package/lib/api-key-login-CWVwvHT1.mjs +157 -0
- package/lib/aws-credentials-CXryMobh.mjs +867 -0
- package/lib/aws-credentials-ChG79ey-.mjs +867 -0
- package/lib/azure-openai-responses-Cgmc-rbL.mjs +274 -0
- package/lib/azure-openai-responses-DuukbD0F.mjs +274 -0
- package/lib/bun-shim.mjs +81 -6
- package/lib/bundled-references-Cavaffah.mjs +55 -0
- package/lib/bundled-references-sLN0YdXU.mjs +55 -0
- package/lib/callback-server-CQ5Jo8F-.mjs +398 -0
- package/lib/callback-server-DDSrZ6l_.mjs +398 -0
- package/lib/client.js +1 -1
- package/lib/cursor-B3zg7EHj.mjs +273 -0
- package/lib/cursor-B6TUEmgP.mjs +135 -0
- package/lib/cursor-D8gvtTL3.mjs +135 -0
- package/lib/cursor-DkSX2h3H.mjs +5441 -0
- package/lib/cursor-aJrXeYCb.mjs +273 -0
- package/lib/cursor-mJQwUKXK.mjs +5441 -0
- package/lib/device-code-DBo3B5nh.mjs +51 -0
- package/lib/device-code-DIb0FU-H.mjs +51 -0
- package/lib/devin-BHZZyMhO.mjs +95 -0
- package/lib/devin-BK7DHeca.mjs +114 -0
- package/lib/devin-CfMp3cXS.mjs +95 -0
- package/lib/devin-D9zR5B_R.mjs +542 -0
- package/lib/devin-Q1yem3lp.mjs +114 -0
- package/lib/devin-vNZTNUON.mjs +542 -0
- package/lib/error-Dd8DIqs-.mjs +1449 -0
- package/lib/error-yCSG0mGC.mjs +1449 -0
- package/lib/event-stream-BbFZJy-z.mjs +151 -0
- package/lib/event-stream-loUa0s1T.mjs +151 -0
- package/lib/fs-promises-shim-Bx0fpxuk.mjs +57 -0
- package/lib/github-copilot-B1yiyVRh.mjs +104 -0
- package/lib/github-copilot-BWT3eAOF.mjs +208 -0
- package/lib/github-copilot-CYkjeTi3.mjs +208 -0
- package/lib/github-copilot-jscUZ8Hf.mjs +104 -0
- package/lib/gitlab-duo-CM7Ref1H.mjs +155 -0
- package/lib/gitlab-duo-gubO4FNw.mjs +155 -0
- package/lib/gitlab-duo-workflow-BGAF6JDw.mjs +91 -0
- package/lib/gitlab-duo-workflow-D78tdn8E.mjs +91 -0
- package/lib/google-BS4T25Pn.mjs +30 -0
- package/lib/google-TFe-7-of.mjs +30 -0
- package/lib/google-antigravity-DGmwC2KV.mjs +198 -0
- package/lib/google-antigravity-DNYhgTpA.mjs +198 -0
- package/lib/google-auth-B6gjsZvg.mjs +232 -0
- package/lib/google-auth-De7_s3eo.mjs +232 -0
- package/lib/google-gemini-cli-CWu8HvXW.mjs +211 -0
- package/lib/google-gemini-cli-Cb97bzQc.mjs +850 -0
- package/lib/google-gemini-cli-Cqds_Q5T.mjs +850 -0
- package/lib/google-gemini-cli-Dsztq0N5.mjs +211 -0
- package/lib/google-oauth-shared-CuEHaDDj.mjs +147 -0
- package/lib/google-oauth-shared-yqlyqPPC.mjs +147 -0
- package/lib/google-shared-CgKmmdO3.mjs +760 -0
- package/lib/google-shared-Dr-VG-ap.mjs +760 -0
- package/lib/google-vertex-BDN2tLaD.mjs +99 -0
- package/lib/google-vertex-DopVOQOG.mjs +99 -0
- package/lib/identity-Da1GPU9a.mjs +219 -0
- package/lib/identity-xRP7_eSR.mjs +219 -0
- package/lib/idle-iterator-Baoyn3Hp.mjs +366 -0
- package/lib/idle-iterator-DUjKQEGT.mjs +366 -0
- package/lib/index.mjs +1731 -1178
- package/lib/kimi--wUOWER5.mjs +236 -0
- package/lib/kimi-Dh0pv3WZ.mjs +236 -0
- package/lib/markit-BL3cH47U.mjs +1417 -0
- package/lib/markit-BNiNY6SE.mjs +1417 -0
- package/lib/minimax-code-DCbuA6Yk.mjs +53 -0
- package/lib/minimax-code-Uq8Qu9LW.mjs +53 -0
- package/lib/models-D_XnT6ay.mjs +104660 -0
- package/lib/models-SGASygJ3.mjs +104660 -0
- package/lib/ollama-DPzBmq89.mjs +589 -0
- package/lib/ollama-DoBz5SFm.mjs +589 -0
- package/lib/openai-codex-Bi-4S4PX.mjs +255 -0
- package/lib/openai-codex-Ch8CkT3p.mjs +255 -0
- package/lib/openai-codex-responses-CvxL_FMF.mjs +3144 -0
- package/lib/openai-codex-responses-pejZgdZo.mjs +3144 -0
- package/lib/openai-completions-CF8IyNtP.mjs +1360 -0
- package/lib/openai-completions-DIf361kF.mjs +1360 -0
- package/lib/openai-reasoning-fallback-Nj6yB2rY.mjs +286 -0
- package/lib/openai-reasoning-fallback-OdJREP1o.mjs +286 -0
- package/lib/openai-responses-BqXJInHX.mjs +867 -0
- package/lib/openai-responses-Db_a-U4-.mjs +867 -0
- package/lib/opencode-DhMolSoB.mjs +38 -0
- package/lib/opencode-Rq_Q2vAO.mjs +38 -0
- package/lib/perplexity-C4iLVMUj.mjs +194 -0
- package/lib/perplexity-Uu7Jzg5b.mjs +194 -0
- package/lib/proxy-DDqrmjjC.mjs +190 -0
- package/lib/proxy-pM_NIv-H.mjs +190 -0
- package/lib/request-debug-HJMoJncL.mjs +183 -0
- package/lib/sanitize-text-CfcXzw5X.mjs +33 -0
- package/lib/scrapers-BkFfcPHj.mjs +9198 -0
- package/lib/scrapers-CVptboMs.mjs +9198 -0
- package/lib/scrapers-nhQAvc8d.mjs +9198 -0
- package/lib/shell-snapshot-fn-env.sh +67 -43
- package/lib/src-BgpusnsB.mjs +5076 -0
- package/lib/src-lDmkSRbm.mjs +5006 -0
- package/lib/stream-BlldLkVq.mjs +10236 -0
- package/lib/stream-CRtexYLe.mjs +10236 -0
- package/lib/stream-markup-healing-DMsxBx1L.mjs +491 -0
- package/lib/stream-markup-healing-KMkiAA00.mjs +491 -0
- package/lib/turndown-DN7ByzEI.mjs +667 -0
- package/lib/types/config/settings.d.ts +9 -1
- package/lib/types/config/truncate.d.ts +9 -5
- package/lib/types/tools/bash/background.d.ts +1 -1
- package/lib/types/tools/bash/bash-executor.d.ts +4 -0
- package/lib/types/tools/bash/bash-interceptor.d.ts +10 -8
- package/lib/types/tools/bash/cd-workdir.d.ts +35 -0
- package/lib/types/tools/bash/non-interactive-env.d.ts +2 -0
- package/lib/types/tools/bash/render.d.ts +4 -3
- package/lib/types/tools/bash/sanitize-text.d.ts +5 -0
- package/lib/types/tools/bash/shell-snapshot.d.ts +4 -1
- package/lib/types/tools/bash/shell-tokenize.d.ts +60 -0
- package/lib/types/tools/bash/streaming-output.d.ts +12 -212
- package/lib/types/tools/bash/which.d.ts +20 -0
- package/lib/types/tools/bash/windows-shell.d.ts +31 -0
- package/lib/types/tools/edit/adapter/index.d.ts +25 -3
- package/lib/types/tools/edit/adapter/tools/auto-generated-guard.d.ts +7 -17
- package/lib/types/tools/glob/adapter/index.d.ts +27 -1
- package/lib/types/tools/grep/adapter/index.d.ts +29 -1
- package/lib/types/tools/omp/edit/dispatch.d.ts +43 -0
- package/lib/types/tools/omp/edit/hashline/noop-loop-guard.d.ts +3 -1
- package/lib/types/tools/omp/tools/auto-generated-guard.d.ts +20 -0
- package/lib/types/tools/omp/tools/write.d.ts +4 -4
- package/lib/types/tools/shared/auto-generated-guard.dsh.d.ts +16 -4
- package/lib/types/tools/shared/bun-shim.d.ts +4 -0
- package/lib/types/tools/shared/fs-promises-shim.d.ts +3 -1
- package/lib/types/tools/shared/omp-prompt.d.ts +13 -7
- package/lib/types/tools/shared/session-state.d.ts +9 -3
- package/lib/utils-Bw139LTz.mjs +8119 -0
- package/lib/utils-CZw4tVZQ.mjs +12315 -0
- package/lib/utils-CiGN7OHR.mjs +12315 -0
- package/lib/utils-Cp76gCyk.mjs +8119 -0
- package/lib/utils-D0GRDgAr.mjs +12315 -0
- package/lib/utils-D2YknRk4.mjs +51 -0
- package/lib/utils-DzXZDfYc.mjs +51 -0
- package/lib/vision-guard-01qSwFpf.mjs +92 -0
- package/lib/vision-guard-DzZGaOga.mjs +92 -0
- package/lib/wafer-CgtPHerf.mjs +17 -0
- package/lib/wafer-cBtrn6q9.mjs +17 -0
- package/lib/xai-oauth-Buk8XVvM.mjs +446 -0
- package/lib/xai-oauth-CMal8l1O.mjs +446 -0
- package/lib/xiaomi-CZgwvX2G.mjs +157 -0
- package/lib/xiaomi-D2qT5cAC.mjs +157 -0
- package/lib/zai-7DWMkEqM.mjs +203 -0
- package/lib/zai-C59YnomG.mjs +203 -0
- package/lib/zip-Cg9ANN_U.mjs +1316 -0
- package/lib/zip-D36ZKsP8.mjs +1316 -0
- package/package.json +1 -1
- package/presets/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { installBunShim as __installBunShim } from "./bun-shim.mjs";
|
|
2
2
|
__installBunShim();
|
|
3
|
-
import { n as fs_promises_shim_exports, t as exists } from "./fs-promises-shim-
|
|
4
|
-
import { A as
|
|
5
|
-
import {
|
|
3
|
+
import { n as fs_promises_shim_exports, t as exists } from "./fs-promises-shim-Bx0fpxuk.mjs";
|
|
4
|
+
import { A as formatAge, C as error, F as $envpos, I as $flag, M as formatCount, P as $env, Q as peekFile, S as debug, T as warn, U as getAutoQaDbPath, V as VERSION, Y as getRemoteDir, Z as isProbablyBinary, b as isEnotdir, c as combineSignals, d as $which, et as untilAborted, j as formatBytes$1, k as installBunShim, l as exec, p as readImageMetadata, q as getInstallId, u as render, v as hasFsCode, y as isEnoent } from "./src-BgpusnsB.mjs";
|
|
5
|
+
import { A as shortenPath, C as Settings, D as capParseErrors, E as OMP_DEFAULTS, M as padding, N as replaceTabs, O as formatCodeFrameLine, P as truncateToWidth$1, S as convertFileWithMarkit, T as settings, _ as extractWithParallel, a as fetchBinary, b as ensureTool, g as looksLikeHtml, h as loadPage, i as convertWithMarkit, j as LRUCache, k as formatParseErrors, o as MAX_BYTES, s as MAX_OUTPUT_CHARS, u as finalizeOutput, v as findParallelApiKey, w as isSettingsInitialized, x as CONVERTIBLE_EXTENSIONS, y as getParallelExtractContent } from "./utils-CZw4tVZQ.mjs";
|
|
6
|
+
import { t as sanitizeText } from "./sanitize-text-CfcXzw5X.mjs";
|
|
6
7
|
import { n as isRecord$2 } from "./type-guards-B9VVjRDK.mjs";
|
|
7
|
-
import { a as openArchive, c as sniffArchiveFormat, d as writeArchive, f as ToolAbortError, i as listArchiveRoot, m as throwIfAborted, o as parseArchivePathCandidates, p as ToolError
|
|
8
|
+
import { a as openArchive, c as sniffArchiveFormat, d as writeArchive, f as ToolAbortError, i as listArchiveRoot, m as throwIfAborted, o as parseArchivePathCandidates, p as ToolError, r as formatArchiveEntryLines, s as readArchiveEntries, t as archiveFormatFromPath } from "./zip-Cg9ANN_U.mjs";
|
|
8
9
|
import * as path from "node:path";
|
|
9
10
|
import { TOOL_ABORTED, defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
11
|
import { HarnessError } from "@deepseek-ai/dsh-llm";
|
|
@@ -24,7 +25,207 @@ import z from "@deepseek-ai/schemastery";
|
|
|
24
25
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
25
26
|
import { type } from "@oh-my-pi/omptype";
|
|
26
27
|
import { DatabaseSync } from "node:sqlite";
|
|
28
|
+
//#region src/tools/bash/shell-tokenize.ts
|
|
29
|
+
/**
|
|
30
|
+
* Returns the flat shell command segments with the original text of each. Unlike
|
|
31
|
+
* `tokenizeShellSegments`, this preserves quoting and escaping so the results
|
|
32
|
+
* are safe to match against user-configured regular expressions, and flags
|
|
33
|
+
* segments that receive piped stdin.
|
|
34
|
+
*
|
|
35
|
+
* The extractor deliberately declines to split syntax whose execution context
|
|
36
|
+
* cannot be determined with this small scanner (heredocs, command substitution,
|
|
37
|
+
* backticks, grouping, and malformed quoting). Callers must still check the
|
|
38
|
+
* complete input in that case.
|
|
39
|
+
*/
|
|
40
|
+
function extractFlatShellCommandSegments(command) {
|
|
41
|
+
const segments = [];
|
|
42
|
+
let segmentStart = 0;
|
|
43
|
+
let inSingle = false;
|
|
44
|
+
let inDouble = false;
|
|
45
|
+
let atWordStart = true;
|
|
46
|
+
let currentPiped = false;
|
|
47
|
+
const pushSegment = (end) => {
|
|
48
|
+
const segment = command.slice(segmentStart, end).trim();
|
|
49
|
+
if (segment.length === 0) return false;
|
|
50
|
+
segments.push({
|
|
51
|
+
text: segment,
|
|
52
|
+
pipedStdin: currentPiped
|
|
53
|
+
});
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
for (let i = 0; i < command.length; i++) {
|
|
57
|
+
const ch = command[i];
|
|
58
|
+
if (inSingle) {
|
|
59
|
+
if (ch === "'") inSingle = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (inDouble) {
|
|
63
|
+
if (ch === "\\") {
|
|
64
|
+
if (i + 1 >= command.length) return [];
|
|
65
|
+
i++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (ch === "\"") {
|
|
69
|
+
inDouble = false;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (ch === "`" || ch === "$" && command[i + 1] === "(") return [];
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === "'") {
|
|
76
|
+
inSingle = true;
|
|
77
|
+
atWordStart = false;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ch === "\"") {
|
|
81
|
+
inDouble = true;
|
|
82
|
+
atWordStart = false;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === "\\") {
|
|
86
|
+
if (i + 1 >= command.length) return [];
|
|
87
|
+
i++;
|
|
88
|
+
atWordStart = false;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ch === "`" || ch === "(" || ch === ")" || ch === "$" && command[i + 1] === "(" || ch === "$" && command[i + 1] === "{" || ch === "<" && command[i + 1] === "<" || (ch === "{" || ch === "}") && atWordStart && (command[i + 1] === void 0 || /[ \t\n;]/.test(command[i + 1]))) return [];
|
|
92
|
+
if (ch === "#" && atWordStart) {
|
|
93
|
+
const pushed = pushSegment(i);
|
|
94
|
+
const newline = command.indexOf("\n", i + 1);
|
|
95
|
+
if (newline === -1) return segments;
|
|
96
|
+
i = newline;
|
|
97
|
+
segmentStart = newline + 1;
|
|
98
|
+
atWordStart = true;
|
|
99
|
+
if (pushed) currentPiped = false;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const isRedirectionOperatorCharacter = ch === "|" ? command[i - 1] === ">" : ch === "&" ? command[i - 1] === ">" || command[i - 1] === "<" || command[i + 1] === ">" : false;
|
|
103
|
+
if ((ch === "\n" || ch === ";" || ch === "|" || ch === "&") && !isRedirectionOperatorCharacter) {
|
|
104
|
+
const pushed = pushSegment(i);
|
|
105
|
+
const doubled = (ch === "|" || ch === "&") && command[i + 1] === ch;
|
|
106
|
+
const pipeStderr = ch === "|" && command[i + 1] === "&";
|
|
107
|
+
if (doubled || pipeStderr) i++;
|
|
108
|
+
if (pushed || ch !== "\n") currentPiped = ch === "|" && !doubled;
|
|
109
|
+
segmentStart = i + 1;
|
|
110
|
+
atWordStart = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
atWordStart = ch === " " || ch === " ";
|
|
114
|
+
}
|
|
115
|
+
if (inSingle || inDouble) return [];
|
|
116
|
+
pushSegment(command.length);
|
|
117
|
+
return segments;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Shell metacharacters that end an unquoted `cd` target token. A redirect,
|
|
121
|
+
* extra argument, or any operator in this set means the leading construct is
|
|
122
|
+
* more than a bare `cd <path>`, so extraction must bail.
|
|
123
|
+
*/
|
|
124
|
+
const CD_TARGET_TERMINATORS = {
|
|
125
|
+
" ": true,
|
|
126
|
+
" ": true,
|
|
127
|
+
"\n": true,
|
|
128
|
+
"\r": true,
|
|
129
|
+
"&": true,
|
|
130
|
+
"|": true,
|
|
131
|
+
";": true,
|
|
132
|
+
"<": true,
|
|
133
|
+
">": true,
|
|
134
|
+
"(": true,
|
|
135
|
+
")": true
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Parses a leading `cd <path> && ...` prefix so the bash tool can route the
|
|
139
|
+
* target through its structured `cwd` parameter when the model omits it.
|
|
140
|
+
*
|
|
141
|
+
* Returns the single path token (quotes and backslash escapes resolved to their
|
|
142
|
+
* literal value) and the command remainder after the top-level `&&`, or `null`
|
|
143
|
+
* when the command does not begin with exactly `cd`, one path token, and a
|
|
144
|
+
* top-level `&&`. The scanner deliberately bails on anything else in the prefix
|
|
145
|
+
* — redirects (`cd /tmp 2>/dev/null && ...`), extra arguments, or paths needing
|
|
146
|
+
* shell expansion (`$`, backticks, `(`) — leaving the whole command for the
|
|
147
|
+
* shell instead of absorbing shell syntax into `cwd`.
|
|
148
|
+
*/
|
|
149
|
+
function extractLeadingCdTarget(command) {
|
|
150
|
+
const prefix = /^cd[ \t]+/.exec(command);
|
|
151
|
+
if (!prefix) return null;
|
|
152
|
+
let i = prefix[0].length;
|
|
153
|
+
let path = "";
|
|
154
|
+
let inSingle = false;
|
|
155
|
+
let inDouble = false;
|
|
156
|
+
for (; i < command.length; i++) {
|
|
157
|
+
const ch = command[i];
|
|
158
|
+
if (inSingle) {
|
|
159
|
+
if (ch === "'") {
|
|
160
|
+
inSingle = false;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
path += ch;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (inDouble) {
|
|
167
|
+
if (ch === "\\" && i + 1 < command.length) {
|
|
168
|
+
const next = command[i + 1];
|
|
169
|
+
if (next === "\n" || next === "\r") return null;
|
|
170
|
+
if (next === "\"" || next === "\\" || next === "$" || next === "`") {
|
|
171
|
+
path += next;
|
|
172
|
+
i++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (ch === "\"") {
|
|
177
|
+
inDouble = false;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
path += ch;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (ch === "'") {
|
|
184
|
+
inSingle = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === "\"") {
|
|
188
|
+
inDouble = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (ch === "\\" && i + 1 < command.length) {
|
|
192
|
+
if (command[i + 1] === "\n" || command[i + 1] === "\r") return null;
|
|
193
|
+
path += command[i + 1];
|
|
194
|
+
i++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (CD_TARGET_TERMINATORS[ch]) break;
|
|
198
|
+
path += ch;
|
|
199
|
+
}
|
|
200
|
+
if (inSingle || inDouble || path.length === 0) return null;
|
|
201
|
+
if (/[$`(]/.test(path)) return null;
|
|
202
|
+
while (command[i] === " " || command[i] === " ") i++;
|
|
203
|
+
if (command[i] !== "&" || command[i + 1] !== "&") return null;
|
|
204
|
+
i += 2;
|
|
205
|
+
while (command[i] === " " || command[i] === " ") i++;
|
|
206
|
+
return {
|
|
207
|
+
path,
|
|
208
|
+
rest: command.slice(i)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
27
212
|
//#region src/tools/bash/bash-interceptor.ts
|
|
213
|
+
/**
|
|
214
|
+
* Bash intent interceptor - redirects common shell patterns to proper tools.
|
|
215
|
+
*
|
|
216
|
+
* When an LLM calls bash with patterns like `grep`, `cat`, `find`, etc.,
|
|
217
|
+
* this interceptor provides helpful error messages directing them to use
|
|
218
|
+
* the specialized tools instead.
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* 规则表对齐上游 refs/oh-my-pi/packages/coding-agent/src/config/settings-schema.ts:325-387(10 条)。
|
|
222
|
+
* 既有 6 条(read/grep/glob/edit×3)语义与消息文案保持不变;新增 echo/printf 重定向 → write(上游 :356-367)。
|
|
223
|
+
* hub 类 3 条(nohup/后台语法、dev/start 服务、--watch,上游 :368-386)按映射决策加入:
|
|
224
|
+
* pattern 逐字照抄;tool hub → bash(DSH 等价受管机制 = bash 工具 run_in_background:true,
|
|
225
|
+
* startBashJob → jobs 注册表 → job_output 读输出 / job_kill 停止);文案改写为 run_in_background 引导。
|
|
226
|
+
* DSH 无 hub 的 stdin-send/restart/跨实例观察能力,属有意不移植。
|
|
227
|
+
* `bashInterceptor.patterns` 配置键不在本次范围,未引入。
|
|
228
|
+
*/
|
|
28
229
|
const DEFAULT_BASH_INTERCEPTOR_RULES = [
|
|
29
230
|
{
|
|
30
231
|
pattern: "^\\s*(cat|head|tail|less|more)\\s+",
|
|
@@ -55,6 +256,26 @@ const DEFAULT_BASH_INTERCEPTOR_RULES = [
|
|
|
55
256
|
pattern: "^\\s*awk\\s+.*-i\\s+inplace",
|
|
56
257
|
tool: "edit",
|
|
57
258
|
message: "Use the `edit` tool instead of awk -i inplace. It provides diff preview and fuzzy matching."
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+(?:(?:[^\"'>]|\"[^\"]*\"|'[^']*')|(?<!\\|)>{1,2}\\|?\\s*(?:\"/dev/(?:null|tty|stdout|stderr)\"|'/dev/(?:null|tty|stdout|stderr)'|/dev/(?:null|tty|stdout|stderr))(?:[\\s;&|]|$))*(?<!\\|)>{1,2}\\|?\\s*(?!(?:\"/dev/(?:null|tty|stdout|stderr)\"|'/dev/(?:null|tty|stdout|stderr)'|/dev/(?:null|tty|stdout|stderr))(?:[\\s;&|]|$))[$\\w./~\"'-]",
|
|
262
|
+
tool: "write",
|
|
263
|
+
message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation."
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
pattern: "^\\s*nohup\\s+|(?<!&)\\&\\s*$",
|
|
267
|
+
tool: "bash",
|
|
268
|
+
message: "Use the bash tool with `run_in_background: true` instead of nohup/background shell syntax, so the process runs as a managed background job (job id returned immediately; result delivered on completion; stop with job_kill) instead of an untracked detached process."
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
pattern: "^\\s*(?:(?:bun|npm|pnpm|yarn)\\s+(?:run\\s+)?(?:dev|start)(?:\\s|$)|(?:vite|next\\s+dev|nuxt\\s+dev|nodemon|lldb|gdb|tail\\s+-f)(?:\\s|$)|docker\\s+compose\\s+up(?!.*(?:\\s-d(?:\\s|$)|--detach))(?:\\s|$))",
|
|
272
|
+
tool: "bash",
|
|
273
|
+
message: "Run services, watchers, and debuggers via the bash tool with `run_in_background: true` so their output, lifecycle, and exit stay observable as a background job (job_output / job_kill) instead of hanging the foreground call."
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
pattern: "^\\s*(?:(?:bun|npm|pnpm|yarn)\\s+(?:run\\s+)?\\S+|cargo\\s+watch|watchexec|pytest|vitest|jest|tsc)(?:.|\\n)*(?:--watch|-w)(?:\\s|$)",
|
|
277
|
+
tool: "bash",
|
|
278
|
+
message: "Run watch mode via the bash tool with `run_in_background: true` so its output, input, and lifecycle stay managed as a background job (job_output / job_kill)."
|
|
58
279
|
}
|
|
59
280
|
];
|
|
60
281
|
/**
|
|
@@ -73,6 +294,73 @@ function compileRules(rules) {
|
|
|
73
294
|
}
|
|
74
295
|
return compiled;
|
|
75
296
|
}
|
|
297
|
+
/** Finds the end of a shell word, respecting quotes and escapes; returns null for incomplete syntax. */
|
|
298
|
+
function skipShellWord(command, start) {
|
|
299
|
+
let inSingle = false;
|
|
300
|
+
let inDouble = false;
|
|
301
|
+
for (let i = start; i < command.length; i++) {
|
|
302
|
+
const ch = command[i];
|
|
303
|
+
if (inSingle) {
|
|
304
|
+
if (ch === "'") inSingle = false;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (inDouble) {
|
|
308
|
+
if (ch === "\\") {
|
|
309
|
+
if (i + 1 >= command.length) return null;
|
|
310
|
+
i++;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (ch === "\"") inDouble = false;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (ch === "'") {
|
|
317
|
+
inSingle = true;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (ch === "\"") {
|
|
321
|
+
inDouble = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (ch === "\\") {
|
|
325
|
+
if (i + 1 >= command.length) return null;
|
|
326
|
+
i++;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (ch === " " || ch === " ") return i;
|
|
330
|
+
}
|
|
331
|
+
return inSingle || inDouble ? null : command.length;
|
|
332
|
+
}
|
|
333
|
+
/** Removes leading `NAME=value` assignments without interpreting shell syntax. */
|
|
334
|
+
function withoutLeadingEnvironmentAssignments(command) {
|
|
335
|
+
let index = 0;
|
|
336
|
+
let foundAssignment = false;
|
|
337
|
+
while (index < command.length) {
|
|
338
|
+
while (command[index] === " " || command[index] === " ") index++;
|
|
339
|
+
const assignmentStart = index;
|
|
340
|
+
if (!/[A-Za-z_]/.test(command[index] ?? "")) break;
|
|
341
|
+
let nameEnd = index + 1;
|
|
342
|
+
while (/[A-Za-z0-9_]/.test(command[nameEnd] ?? "")) nameEnd++;
|
|
343
|
+
if (command[nameEnd] !== "=") return foundAssignment ? command.slice(assignmentStart).trimStart() : null;
|
|
344
|
+
const wordEnd = skipShellWord(command, nameEnd + 1);
|
|
345
|
+
if (wordEnd === null) return null;
|
|
346
|
+
foundAssignment = true;
|
|
347
|
+
index = wordEnd;
|
|
348
|
+
if (index === command.length) return null;
|
|
349
|
+
}
|
|
350
|
+
if (!foundAssignment) return null;
|
|
351
|
+
const commandWithoutAssignments = command.slice(index).trimStart();
|
|
352
|
+
return commandWithoutAssignments.length > 0 ? commandWithoutAssignments : null;
|
|
353
|
+
}
|
|
354
|
+
function interceptionCandidates(command) {
|
|
355
|
+
const candidates = [command.trim()];
|
|
356
|
+
for (const segment of extractFlatShellCommandSegments(command)) {
|
|
357
|
+
if (segment.pipedStdin) continue;
|
|
358
|
+
candidates.push(segment.text);
|
|
359
|
+
const withoutAssignments = withoutLeadingEnvironmentAssignments(segment.text);
|
|
360
|
+
if (withoutAssignments) candidates.push(withoutAssignments);
|
|
361
|
+
}
|
|
362
|
+
return candidates;
|
|
363
|
+
}
|
|
76
364
|
/**
|
|
77
365
|
* Check if a bash command should be intercepted.
|
|
78
366
|
*
|
|
@@ -80,16 +368,19 @@ function compileRules(rules) {
|
|
|
80
368
|
* @param availableTools Set of tool names that are available
|
|
81
369
|
* @returns InterceptionResult indicating if the command should be blocked
|
|
82
370
|
*/
|
|
83
|
-
function checkBashInterception(command, availableTools, rules = DEFAULT_BASH_INTERCEPTOR_RULES) {
|
|
84
|
-
const normalizedCommand = command.trim();
|
|
371
|
+
function checkBashInterception(command, availableTools, rules = DEFAULT_BASH_INTERCEPTOR_RULES, originalCommand = command) {
|
|
85
372
|
const compiled = compileRules(rules);
|
|
373
|
+
const candidates = interceptionCandidates(command);
|
|
86
374
|
for (const { rule, regex } of compiled) {
|
|
87
375
|
if (!availableTools.includes(rule.tool)) continue;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
376
|
+
for (const candidate of candidates) {
|
|
377
|
+
regex.lastIndex = 0;
|
|
378
|
+
if (regex.test(candidate)) return {
|
|
379
|
+
block: true,
|
|
380
|
+
message: `Blocked: ${rule.message}\n\nOriginal command: ${originalCommand}`,
|
|
381
|
+
suggestedTool: rule.tool
|
|
382
|
+
};
|
|
383
|
+
}
|
|
93
384
|
}
|
|
94
385
|
return { block: false };
|
|
95
386
|
}
|
|
@@ -110,7 +401,49 @@ function runtimeLogger() {
|
|
|
110
401
|
return current;
|
|
111
402
|
}
|
|
112
403
|
//#endregion
|
|
113
|
-
//#region src/tools/bash/
|
|
404
|
+
//#region src/tools/bash/which.ts
|
|
405
|
+
/**
|
|
406
|
+
* Minimal PATH (+ PATHEXT on Windows) executable lookup, standing in for
|
|
407
|
+
* `Bun.which` / pi-utils `$which` on the Node runtime.
|
|
408
|
+
*
|
|
409
|
+
* Why this exists: pi-utils' `$which` calls `Bun.which` at module top level
|
|
410
|
+
* (node_modules @oh-my-pi/pi-utils src/which.ts:196), which the bun-shim does
|
|
411
|
+
* not provide — so DSH code that must resolve executables the way OMP does
|
|
412
|
+
* (procmgr `resolveWindowsShell` for shell discovery, `REJECT_PROMPT_COMMAND`
|
|
413
|
+
* in non-interactive-env) uses this scanner instead. Single implementation —
|
|
414
|
+
* any new caller resolves executables through here, never hand-rolls a PATH
|
|
415
|
+
* walk.
|
|
416
|
+
* @module @xiaoso/dsh-tool-plus/bash/which
|
|
417
|
+
*/
|
|
418
|
+
/** Windows default extension order when PATHEXT is unset. */
|
|
419
|
+
const WIN32_DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
420
|
+
/** True when `command` carries its own directory component. */
|
|
421
|
+
function hasPathSeparator(command) {
|
|
422
|
+
return command.includes("/") || command.includes("\\");
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Resolve `command` against PATH the way `Bun.which` would: absolute/relative
|
|
426
|
+
* candidates are returned as-is when they name an existing file; bare names
|
|
427
|
+
* are looked up in each PATH dir (appending PATHEXT extensions on win32, in
|
|
428
|
+
* order). Returns the first existing regular file, or `undefined`.
|
|
429
|
+
*/
|
|
430
|
+
function findOnPath(command, env = process.env) {
|
|
431
|
+
if (command.length === 0) return void 0;
|
|
432
|
+
if (hasPathSeparator(command)) try {
|
|
433
|
+
return fs.statSync(command).isFile() ? path.resolve(command) : void 0;
|
|
434
|
+
} catch {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const dirs = (env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
438
|
+
const extensions = process.platform === "win32" ? (env.PATHEXT && env.PATHEXT.length > 0 ? env.PATHEXT : WIN32_DEFAULT_PATHEXT).split(";").filter(Boolean) : [];
|
|
439
|
+
const candidates = process.platform === "win32" ? [command, ...extensions.map((ext) => command + ext)] : [command];
|
|
440
|
+
for (const dir of dirs) for (const candidate of candidates) {
|
|
441
|
+
const full = path.join(dir, candidate);
|
|
442
|
+
try {
|
|
443
|
+
if (fs.statSync(full).isFile()) return full;
|
|
444
|
+
} catch {}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
114
447
|
const NON_INTERACTIVE_ENV = {
|
|
115
448
|
PAGER: "cat",
|
|
116
449
|
GIT_PAGER: "cat",
|
|
@@ -132,8 +465,9 @@ const NON_INTERACTIVE_ENV = {
|
|
|
132
465
|
VISUAL: "true",
|
|
133
466
|
EDITOR: "true",
|
|
134
467
|
GIT_TERMINAL_PROMPT: "0",
|
|
135
|
-
SSH_ASKPASS: "
|
|
136
|
-
CI: "
|
|
468
|
+
SSH_ASKPASS: findOnPath("false") ?? "false",
|
|
469
|
+
CI: "true",
|
|
470
|
+
AGENT: "1",
|
|
137
471
|
npm_config_yes: "true",
|
|
138
472
|
npm_config_update_notifier: "false",
|
|
139
473
|
npm_config_fund: "false",
|
|
@@ -174,13 +508,19 @@ function hasEnvGroupValue(env, group, platform) {
|
|
|
174
508
|
for (const [key] of group) if (hasEnvValue(env, key, platform)) return true;
|
|
175
509
|
return false;
|
|
176
510
|
}
|
|
511
|
+
/** Copy of the base env with `CI` removed, for the `PI_BASH_NO_CI` opt-out. */
|
|
512
|
+
function withoutCI(env) {
|
|
513
|
+
const { CI: _ci, ...rest } = env;
|
|
514
|
+
return rest;
|
|
515
|
+
}
|
|
177
516
|
/** Builds the per-command environment for non-interactive child processes. */
|
|
178
517
|
function buildNonInteractiveEnv(overrides, baseEnv = process.env, platform = process.platform) {
|
|
518
|
+
const base = baseEnv.PI_BASH_NO_CI || baseEnv.CLAUDE_BASH_NO_CI ? withoutCI(NON_INTERACTIVE_ENV) : NON_INTERACTIVE_ENV;
|
|
179
519
|
if (platform !== "win32") return overrides ? {
|
|
180
|
-
...
|
|
520
|
+
...base,
|
|
181
521
|
...overrides
|
|
182
|
-
} :
|
|
183
|
-
const env = { ...
|
|
522
|
+
} : base;
|
|
523
|
+
const env = { ...base };
|
|
184
524
|
for (const group of WINDOWS_UTF8_ENV_DEFAULT_GROUPS) {
|
|
185
525
|
if (hasEnvGroupValue(baseEnv, group, platform) || hasEnvGroupValue(overrides, group, platform)) continue;
|
|
186
526
|
for (const [key, value] of group) env[key] = value;
|
|
@@ -383,25 +723,37 @@ fi
|
|
|
383
723
|
/**
|
|
384
724
|
* Create a shell snapshot, caching the result.
|
|
385
725
|
* Returns the path to the snapshot file, or null if creation failed.
|
|
726
|
+
*
|
|
727
|
+
* `timeoutMs` is configurable so callers exercising failure handling do not
|
|
728
|
+
* have to wait out the production startup budget.
|
|
386
729
|
*/
|
|
387
|
-
async function getOrCreateSnapshot(shell, env) {
|
|
730
|
+
async function getOrCreateSnapshot(shell, env, timeoutMs = SNAPSHOT_TIMEOUT_MS) {
|
|
388
731
|
const cacheKey = shell;
|
|
389
732
|
const cached = cachedSnapshotPaths.get(cacheKey);
|
|
390
733
|
if (cached && fs.existsSync(cached)) return cached;
|
|
391
734
|
if (cached) cachedSnapshotPaths.delete(cacheKey);
|
|
392
735
|
if (process.platform === "win32") return null;
|
|
393
736
|
const rcFile = getShellConfigFile(shell, env);
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
recursive: true,
|
|
397
|
-
mode: 448
|
|
398
|
-
});
|
|
399
|
-
try {
|
|
400
|
-
fs.chmodSync(snapshotDir, 448);
|
|
401
|
-
} catch {}
|
|
737
|
+
const uid = process.getuid?.();
|
|
738
|
+
const snapshotDir = path.join(os.tmpdir(), uid === void 0 ? "omp-shell-snapshots" : `omp-shell-snapshots-${uid}`);
|
|
402
739
|
const shellName = shell.includes("zsh") ? "zsh" : shell.includes("bash") ? "bash" : "sh";
|
|
403
740
|
const snapshotPath = path.join(snapshotDir, `snapshot-${shellName}-${crypto.randomUUID()}.sh`);
|
|
404
|
-
|
|
741
|
+
try {
|
|
742
|
+
fs.mkdirSync(snapshotDir, {
|
|
743
|
+
recursive: true,
|
|
744
|
+
mode: 448
|
|
745
|
+
});
|
|
746
|
+
try {
|
|
747
|
+
fs.chmodSync(snapshotDir, 448);
|
|
748
|
+
} catch {}
|
|
749
|
+
fs.writeFileSync(snapshotPath, "", { mode: 384 });
|
|
750
|
+
} catch (err) {
|
|
751
|
+
runtimeLogger().debug("shell-snapshot: snapshot dir unusable", {
|
|
752
|
+
dir: snapshotDir,
|
|
753
|
+
err: String(err)
|
|
754
|
+
});
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
405
757
|
const script = generateSnapshotScript(shell, snapshotPath, rcFile);
|
|
406
758
|
let succeeded = false;
|
|
407
759
|
try {
|
|
@@ -410,7 +762,7 @@ async function getOrCreateSnapshot(shell, env) {
|
|
|
410
762
|
for (const [key, value] of Object.entries(snapshotEnv)) if (value !== void 0) spawnEnv[key] = value;
|
|
411
763
|
const child = spawn(shell, ["-c", script], {
|
|
412
764
|
env: spawnEnv,
|
|
413
|
-
timeout:
|
|
765
|
+
timeout: timeoutMs,
|
|
414
766
|
stdio: "ignore",
|
|
415
767
|
killSignal: "SIGKILL"
|
|
416
768
|
});
|
|
@@ -449,85 +801,222 @@ function cleanupSnapshots() {
|
|
|
449
801
|
cachedSnapshotPaths.clear();
|
|
450
802
|
}
|
|
451
803
|
//#endregion
|
|
452
|
-
//#region
|
|
804
|
+
//#region node_modules/.pnpm/@oh-my-pi+pi-utils@17.3.5/node_modules/@oh-my-pi/pi-utils/src/path.ts
|
|
805
|
+
const WINDOWS_DRIVE_EXTENDED_PREFIX = /^\\\\[?]\\([A-Za-z]:[\\/].*)$/;
|
|
806
|
+
const WINDOWS_UNC_EXTENDED_PREFIX = /^\\\\[?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
|
|
807
|
+
const WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/([A-Za-z]:\/.*)$/;
|
|
808
|
+
const WINDOWS_UNC_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/UNC\/([^/]+)\/(.+)$/i;
|
|
809
|
+
const WINDOWS_DRIVE_NT_PREFIX = /^\\\\[?][?]\\([A-Za-z]:[\\/].*)$/;
|
|
810
|
+
const WINDOWS_UNC_NT_PREFIX = /^\\\\[?][?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
|
|
811
|
+
/** Removes Win32 extended-length prefixes before passing paths to Bun APIs. */
|
|
812
|
+
function stripWindowsExtendedLengthPathPrefix(filePath, platform = process.platform) {
|
|
813
|
+
if (platform !== "win32") return filePath;
|
|
814
|
+
const uncMatch = WINDOWS_UNC_EXTENDED_PREFIX.exec(filePath) ?? WINDOWS_UNC_NT_PREFIX.exec(filePath);
|
|
815
|
+
if (uncMatch) return `\\\\${uncMatch[1]}\\${uncMatch[2]}`;
|
|
816
|
+
const driveMatch = WINDOWS_DRIVE_EXTENDED_PREFIX.exec(filePath) ?? WINDOWS_DRIVE_NT_PREFIX.exec(filePath);
|
|
817
|
+
if (driveMatch) return driveMatch[1];
|
|
818
|
+
const forwardUncMatch = WINDOWS_UNC_EXTENDED_FORWARD_PREFIX.exec(filePath);
|
|
819
|
+
if (forwardUncMatch) return `//${forwardUncMatch[1]}/${forwardUncMatch[2]}`;
|
|
820
|
+
const forwardDriveMatch = WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX.exec(filePath);
|
|
821
|
+
if (forwardDriveMatch) return forwardDriveMatch[1];
|
|
822
|
+
return filePath;
|
|
823
|
+
}
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region node_modules/.pnpm/@oh-my-pi+pi-utils@17.3.5/node_modules/@oh-my-pi/pi-utils/src/path-tree.ts
|
|
826
|
+
/**
|
|
827
|
+
* Multi-level path tree shared by grouped file listings (find / grep / ast tools)
|
|
828
|
+
* and compaction file-operation lists.
|
|
829
|
+
*
|
|
830
|
+
* Flat path lists used to group by the *immediate* parent directory and print the
|
|
831
|
+
* full directory path in every header. For results spread across a deep tree — or
|
|
832
|
+
* rooted outside cwd, where paths stay absolute — that repeated the shared prefix
|
|
833
|
+
* on every line. The tree below folds single-child directory chains (so the common
|
|
834
|
+
* prefix collapses into one header) and nests the rest, charging the model one
|
|
835
|
+
* token per path segment instead of one per file.
|
|
836
|
+
*/
|
|
837
|
+
const URL_LIKE_PATH_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
838
|
+
/** True for `scheme://…` entries that have no meaningful directory structure. */
|
|
839
|
+
function isUrlLikePath(filePath) {
|
|
840
|
+
return URL_LIKE_PATH_RE.test(filePath);
|
|
841
|
+
}
|
|
842
|
+
function createNode() {
|
|
843
|
+
return {
|
|
844
|
+
files: [],
|
|
845
|
+
fileNames: /* @__PURE__ */ new Set(),
|
|
846
|
+
subdirs: [],
|
|
847
|
+
dirIndex: /* @__PURE__ */ new Map()
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function addFile(node, name, key) {
|
|
851
|
+
if (node.fileNames.has(name)) return;
|
|
852
|
+
node.fileNames.add(name);
|
|
853
|
+
node.files.push({
|
|
854
|
+
name,
|
|
855
|
+
key
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Build a directory tree from a flat list of paths. URL-like entries are kept
|
|
860
|
+
* whole as root-level file leaves (they have no meaningful directory structure).
|
|
861
|
+
* Absolute paths carry a leading empty segment so they share a common `/` root
|
|
862
|
+
* and fold like any other prefix.
|
|
863
|
+
*/
|
|
864
|
+
function buildPathTree(entries) {
|
|
865
|
+
const root = createNode();
|
|
866
|
+
for (const { path: rawPath, isDir, key } of entries) {
|
|
867
|
+
const normalized = rawPath.replace(/\\/g, "/");
|
|
868
|
+
const fileKey = key ?? rawPath;
|
|
869
|
+
if (isUrlLikePath(normalized)) {
|
|
870
|
+
addFile(root, normalized, fileKey);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const trimmed = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
874
|
+
if (trimmed.length === 0) continue;
|
|
875
|
+
const segments = trimmed.split("/");
|
|
876
|
+
const dirCount = isDir ? segments.length : segments.length - 1;
|
|
877
|
+
let node = root;
|
|
878
|
+
for (let i = 0; i < dirCount; i++) {
|
|
879
|
+
const segment = segments[i];
|
|
880
|
+
let child = node.dirIndex.get(segment);
|
|
881
|
+
if (!child) {
|
|
882
|
+
child = createNode();
|
|
883
|
+
node.dirIndex.set(segment, child);
|
|
884
|
+
node.subdirs.push({
|
|
885
|
+
name: segment,
|
|
886
|
+
node: child
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
node = child;
|
|
890
|
+
}
|
|
891
|
+
if (!isDir) addFile(node, segments[segments.length - 1], fileKey);
|
|
892
|
+
}
|
|
893
|
+
return root;
|
|
894
|
+
}
|
|
453
895
|
/**
|
|
454
|
-
*
|
|
455
|
-
*
|
|
896
|
+
* Depth-first walk yielding directory and file events. Directories collapse their
|
|
897
|
+
* single-child chains (`a` → `a/b` → `a/b/c`) so a shared prefix becomes one
|
|
898
|
+
* header. Each node's direct files are emitted before its subdirectories, keeping
|
|
899
|
+
* a file unambiguously attached to the header above it.
|
|
900
|
+
*/
|
|
901
|
+
function* walkPathTree(node, depth = 0) {
|
|
902
|
+
for (const file of node.files) yield {
|
|
903
|
+
kind: "file",
|
|
904
|
+
depth,
|
|
905
|
+
name: file.name,
|
|
906
|
+
key: file.key
|
|
907
|
+
};
|
|
908
|
+
for (const subdir of node.subdirs) {
|
|
909
|
+
let dirNode = subdir.node;
|
|
910
|
+
const parts = [subdir.name];
|
|
911
|
+
while (dirNode.files.length === 0 && dirNode.subdirs.length === 1) {
|
|
912
|
+
const only = dirNode.subdirs[0];
|
|
913
|
+
parts.push(only.name);
|
|
914
|
+
dirNode = only.node;
|
|
915
|
+
}
|
|
916
|
+
yield {
|
|
917
|
+
kind: "dir",
|
|
918
|
+
depth,
|
|
919
|
+
name: parts.join("/"),
|
|
920
|
+
key: ""
|
|
921
|
+
};
|
|
922
|
+
yield* walkPathTree(dirNode, depth + 1);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Render a flat path list as a grouped, prefix-folded directory tree without
|
|
927
|
+
* per-file bodies (find-tool output shape, also used by compaction `<files>`
|
|
928
|
+
* lists). Single-child directory chains fold into one header (`# a/b/c/`),
|
|
929
|
+
* each level adds one `#`, and files are listed bare under the deepest
|
|
930
|
+
* directory header that owns them. Trailing-slash entries are directory
|
|
931
|
+
* leaves and keep their slash in the header.
|
|
456
932
|
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
* already UTF-16 code-unit arrays. `toWellFormed()` handles the uncommon
|
|
460
|
-
* malformed path; when it changes the input, replacement characters are
|
|
461
|
-
* dropped and the normalized result goes through the well-formed sanitizer.
|
|
933
|
+
* `annotate` receives each file's full original path and its return value is
|
|
934
|
+
* appended verbatim to the file line (e.g. ` (RW)`).
|
|
462
935
|
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*/
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
return
|
|
476
|
-
}
|
|
477
|
-
function sanitizeText(text) {
|
|
478
|
-
const wellFormed = toWellFormed(text);
|
|
479
|
-
if (wellFormed !== text) return sanitizeWellFormedText(wellFormed.replaceAll(REPLACEMENT_CHAR, ""));
|
|
480
|
-
return sanitizeWellFormedText(text);
|
|
481
|
-
}
|
|
482
|
-
function sanitizeWellFormedText(text) {
|
|
483
|
-
CONTROL_RE.lastIndex = 0;
|
|
484
|
-
if (CONTROL_RE.exec(text) === null) return text;
|
|
485
|
-
const stripped = text.indexOf(ESC_CHAR) === -1 ? text : stripAnsi(text);
|
|
486
|
-
CONTROL_RE.lastIndex = 0;
|
|
487
|
-
return stripped.replace(CONTROL_RE, "");
|
|
936
|
+
* Order follows the input: a directory appears when its first member is
|
|
937
|
+
* emitted, and a node's own files precede its subdirectories.
|
|
938
|
+
*/
|
|
939
|
+
function formatGroupedPaths(paths, annotate) {
|
|
940
|
+
if (paths.length === 0) return "";
|
|
941
|
+
const tree = buildPathTree(paths.map((entry) => ({
|
|
942
|
+
path: entry,
|
|
943
|
+
isDir: entry.endsWith("/")
|
|
944
|
+
})));
|
|
945
|
+
const lines = [];
|
|
946
|
+
for (const event of walkPathTree(tree)) if (event.kind === "dir") lines.push(`${"#".repeat(event.depth + 1)} ${event.name}/`);
|
|
947
|
+
else lines.push(annotate ? `${event.name}${annotate(event.key)}` : event.name);
|
|
948
|
+
return lines.join("\n");
|
|
488
949
|
}
|
|
489
950
|
//#endregion
|
|
490
|
-
//#region src/tools/
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
951
|
+
//#region src/tools/omp/utils/sixel.ts
|
|
952
|
+
const SIXEL_START_REGEX = /\x1bP(?:[0-9;]*)q/u;
|
|
953
|
+
const SIXEL_SEQUENCE_REGEX = /\x1bP(?:[0-9;]*)q[\s\S]*?(?:\x1b\\|\x07)/gu;
|
|
954
|
+
const SIXEL_PLACEHOLDER_PREFIX = "__OMP_SIXEL_SEQUENCE_";
|
|
955
|
+
/**
|
|
956
|
+
* Returns whether SIXEL passthrough is explicitly enabled.
|
|
957
|
+
*
|
|
958
|
+
* Both gates must be enabled to preserve SIXEL control sequences:
|
|
959
|
+
* - PI_FORCE_IMAGE_PROTOCOL=sixel
|
|
960
|
+
* - PI_ALLOW_SIXEL_PASSTHROUGH=1
|
|
961
|
+
*/
|
|
962
|
+
function isSixelPassthroughEnabled() {
|
|
963
|
+
return $env.PI_FORCE_IMAGE_PROTOCOL?.trim().toLowerCase() === "sixel" && $flag("PI_ALLOW_SIXEL_PASSTHROUGH");
|
|
497
964
|
}
|
|
498
|
-
|
|
965
|
+
/** Returns true when the text contains a SIXEL start sequence. */
|
|
966
|
+
function containsSixelSequence(text) {
|
|
967
|
+
return SIXEL_START_REGEX.test(text);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Sanitizes text while preserving embedded SIXEL sequences when passthrough is enabled.
|
|
971
|
+
*/
|
|
972
|
+
function sanitizeWithOptionalSixelPassthrough(text, sanitize) {
|
|
973
|
+
if (!isSixelPassthroughEnabled() || !containsSixelSequence(text)) return sanitize(text);
|
|
974
|
+
const preservedSequences = [];
|
|
975
|
+
return sanitize(text.replace(SIXEL_SEQUENCE_REGEX, (match) => {
|
|
976
|
+
const token = `${SIXEL_PLACEHOLDER_PREFIX}${preservedSequences.length}__`;
|
|
977
|
+
preservedSequences.push(match);
|
|
978
|
+
return token;
|
|
979
|
+
})).replace(/__OMP_SIXEL_SEQUENCE_(\d+)__/gu, (_, indexText) => {
|
|
980
|
+
return preservedSequences[Number.parseInt(indexText, 10)] ?? "";
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
//#endregion
|
|
984
|
+
//#region src/tools/omp/session/streaming-output.ts
|
|
985
|
+
const DEFAULT_MAX_LINES = 3e3;
|
|
986
|
+
const DEFAULT_MAX_BYTES = 51200;
|
|
499
987
|
/** Default head budget; the remainder becomes the rolling tail window. */
|
|
500
988
|
const ARTIFACT_DEFAULT_HEAD_BYTES = 3145728;
|
|
501
|
-
const NL
|
|
989
|
+
const NL = "\n";
|
|
990
|
+
const CR = "\r";
|
|
502
991
|
const ELLIPSIS = "…";
|
|
503
992
|
/** Count newline characters via native substring search. */
|
|
504
|
-
function countNewlines
|
|
993
|
+
function countNewlines(text) {
|
|
505
994
|
let count = 0;
|
|
506
|
-
let pos = text.indexOf(NL
|
|
995
|
+
let pos = text.indexOf(NL);
|
|
507
996
|
while (pos !== -1) {
|
|
508
997
|
count++;
|
|
509
|
-
pos = text.indexOf(NL
|
|
998
|
+
pos = text.indexOf(NL, pos + 1);
|
|
510
999
|
}
|
|
511
1000
|
return count;
|
|
512
1001
|
}
|
|
513
1002
|
/** Zero-copy view of a Uint8Array as a Buffer (copies only if already a Buffer). */
|
|
514
|
-
function asBuffer
|
|
1003
|
+
function asBuffer(data) {
|
|
515
1004
|
return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
516
1005
|
}
|
|
517
1006
|
/** Advance past UTF-8 continuation bytes (10xxxxxx) to a leading byte. */
|
|
518
|
-
function findUtf8BoundaryForward
|
|
1007
|
+
function findUtf8BoundaryForward(buf, pos) {
|
|
519
1008
|
let i = Math.max(0, pos);
|
|
520
1009
|
while (i < buf.length && (buf[i] & 192) === 128) i++;
|
|
521
1010
|
return i;
|
|
522
1011
|
}
|
|
523
1012
|
/** Retreat past UTF-8 continuation bytes to land on a leading byte. */
|
|
524
|
-
function findUtf8BoundaryBackward
|
|
1013
|
+
function findUtf8BoundaryBackward(buf, cut) {
|
|
525
1014
|
let i = Math.min(buf.length, Math.max(0, cut));
|
|
526
1015
|
if (i >= buf.length) return buf.length;
|
|
527
1016
|
while (i > 0 && (buf[i] & 192) === 128) i--;
|
|
528
1017
|
return i;
|
|
529
1018
|
}
|
|
530
|
-
function truncateBytesWindowed
|
|
1019
|
+
function truncateBytesWindowed(data, maxBytesRaw, mode) {
|
|
531
1020
|
const maxBytes = maxBytesRaw;
|
|
532
1021
|
if (maxBytes === 0) return {
|
|
533
1022
|
text: "",
|
|
@@ -544,7 +1033,7 @@ function truncateBytesWindowed$1(data, maxBytesRaw, mode) {
|
|
|
544
1033
|
const window = mode === "head" ? data.substring(0, Math.min(data.length, maxBytes)) : data.substring(Math.max(0, data.length - maxBytes));
|
|
545
1034
|
const buf = Buffer.from(window, "utf-8");
|
|
546
1035
|
if (mode === "head") {
|
|
547
|
-
const end = findUtf8BoundaryBackward
|
|
1036
|
+
const end = findUtf8BoundaryBackward(buf, maxBytes);
|
|
548
1037
|
if (end <= 0) return {
|
|
549
1038
|
text: "",
|
|
550
1039
|
bytes: 0
|
|
@@ -555,7 +1044,7 @@ function truncateBytesWindowed$1(data, maxBytesRaw, mode) {
|
|
|
555
1044
|
bytes: slice.length
|
|
556
1045
|
};
|
|
557
1046
|
} else {
|
|
558
|
-
const start = findUtf8BoundaryForward
|
|
1047
|
+
const start = findUtf8BoundaryForward(buf, Math.max(0, buf.length - maxBytes));
|
|
559
1048
|
const slice = buf.subarray(start);
|
|
560
1049
|
return {
|
|
561
1050
|
text: slice.toString("utf-8"),
|
|
@@ -563,13 +1052,13 @@ function truncateBytesWindowed$1(data, maxBytesRaw, mode) {
|
|
|
563
1052
|
};
|
|
564
1053
|
}
|
|
565
1054
|
}
|
|
566
|
-
const buf = asBuffer
|
|
1055
|
+
const buf = asBuffer(data);
|
|
567
1056
|
if (buf.length <= maxBytes) return {
|
|
568
1057
|
text: buf.toString("utf-8"),
|
|
569
1058
|
bytes: buf.length
|
|
570
1059
|
};
|
|
571
1060
|
if (mode === "head") {
|
|
572
|
-
const end = findUtf8BoundaryBackward
|
|
1061
|
+
const end = findUtf8BoundaryBackward(buf, maxBytes);
|
|
573
1062
|
if (end <= 0) return {
|
|
574
1063
|
text: "",
|
|
575
1064
|
bytes: 0
|
|
@@ -580,7 +1069,7 @@ function truncateBytesWindowed$1(data, maxBytesRaw, mode) {
|
|
|
580
1069
|
bytes: slice.length
|
|
581
1070
|
};
|
|
582
1071
|
} else {
|
|
583
|
-
const start = findUtf8BoundaryForward
|
|
1072
|
+
const start = findUtf8BoundaryForward(buf, buf.length - maxBytes);
|
|
584
1073
|
const slice = buf.subarray(start);
|
|
585
1074
|
return {
|
|
586
1075
|
text: slice.toString("utf-8"),
|
|
@@ -593,18 +1082,31 @@ function truncateBytesWindowed$1(data, maxBytesRaw, mode) {
|
|
|
593
1082
|
* Handles multi-byte UTF-8 boundaries correctly.
|
|
594
1083
|
*/
|
|
595
1084
|
function truncateTailBytes(data, maxBytes) {
|
|
596
|
-
return truncateBytesWindowed
|
|
1085
|
+
return truncateBytesWindowed(data, maxBytes, "tail");
|
|
597
1086
|
}
|
|
598
1087
|
/**
|
|
599
1088
|
* Truncate a string/buffer to fit within a byte limit, keeping the head.
|
|
600
1089
|
* Handles multi-byte UTF-8 boundaries correctly.
|
|
601
1090
|
*/
|
|
602
|
-
function truncateHeadBytes
|
|
603
|
-
return truncateBytesWindowed
|
|
1091
|
+
function truncateHeadBytes(data, maxBytes) {
|
|
1092
|
+
return truncateBytesWindowed(data, maxBytes, "head");
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Truncate a single line to max characters, appending '…' if truncated.
|
|
1096
|
+
*/
|
|
1097
|
+
function truncateLine(line, maxChars = 512) {
|
|
1098
|
+
if (line.length <= maxChars) return {
|
|
1099
|
+
text: line,
|
|
1100
|
+
wasTruncated: false
|
|
1101
|
+
};
|
|
1102
|
+
return {
|
|
1103
|
+
text: `${line.slice(0, maxChars)}…`,
|
|
1104
|
+
wasTruncated: true
|
|
1105
|
+
};
|
|
604
1106
|
}
|
|
605
1107
|
/** Shared helper to build a no-truncation result. */
|
|
606
|
-
function noTruncResult
|
|
607
|
-
if (totalLines == null) totalLines = countNewlines
|
|
1108
|
+
function noTruncResult(content, totalLines, totalBytes) {
|
|
1109
|
+
if (totalLines == null) totalLines = countNewlines(content) + 1;
|
|
608
1110
|
if (totalBytes == null) totalBytes = Buffer.byteLength(content, "utf-8");
|
|
609
1111
|
return {
|
|
610
1112
|
content,
|
|
@@ -620,19 +1122,19 @@ function noTruncResult$1(content, totalLines, totalBytes) {
|
|
|
620
1122
|
* This implementation avoids Buffer.from(content) for the whole input.
|
|
621
1123
|
* It only computes UTF-8 byteLength for candidate lines that can still fit.
|
|
622
1124
|
*/
|
|
623
|
-
function truncateHead
|
|
1125
|
+
function truncateHead(content, options = {}) {
|
|
624
1126
|
const maxLines = options.maxLines ?? 3e3;
|
|
625
1127
|
const maxBytes = options.maxBytes ?? 51200;
|
|
626
1128
|
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
627
|
-
const totalLines = countNewlines
|
|
628
|
-
if (totalLines <= maxLines && totalBytes <= maxBytes) return noTruncResult
|
|
1129
|
+
const totalLines = countNewlines(content) + 1;
|
|
1130
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) return noTruncResult(content, totalLines, totalBytes);
|
|
629
1131
|
let includedLines = 0;
|
|
630
1132
|
let bytesUsed = 0;
|
|
631
1133
|
let cutIndex = 0;
|
|
632
1134
|
let cursor = 0;
|
|
633
1135
|
let truncatedBy = "lines";
|
|
634
1136
|
while (includedLines < maxLines) {
|
|
635
|
-
const nl = content.indexOf(NL
|
|
1137
|
+
const nl = content.indexOf(NL, cursor);
|
|
636
1138
|
const lineEnd = nl === -1 ? content.length : nl;
|
|
637
1139
|
const sepBytes = includedLines > 0 ? 1 : 0;
|
|
638
1140
|
const remaining = maxBytes - bytesUsed - sepBytes;
|
|
@@ -701,15 +1203,15 @@ function truncateTail(content, options = {}) {
|
|
|
701
1203
|
const maxLines = options.maxLines ?? 3e3;
|
|
702
1204
|
const maxBytes = options.maxBytes ?? 51200;
|
|
703
1205
|
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
704
|
-
const totalLines = countNewlines
|
|
705
|
-
if (totalLines <= maxLines && totalBytes <= maxBytes) return noTruncResult
|
|
1206
|
+
const totalLines = countNewlines(content) + 1;
|
|
1207
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) return noTruncResult(content, totalLines, totalBytes);
|
|
706
1208
|
let includedLines = 0;
|
|
707
1209
|
let bytesUsed = 0;
|
|
708
1210
|
let startIndex = content.length;
|
|
709
1211
|
let end = content.length;
|
|
710
1212
|
let truncatedBy = "lines";
|
|
711
1213
|
while (includedLines < maxLines) {
|
|
712
|
-
const nl = content.lastIndexOf(NL
|
|
1214
|
+
const nl = content.lastIndexOf(NL, end - 1);
|
|
713
1215
|
const lineStart = nl === -1 ? 0 : nl + 1;
|
|
714
1216
|
const sepBytes = includedLines > 0 ? 1 : 0;
|
|
715
1217
|
const remaining = maxBytes - bytesUsed - sepBytes;
|
|
@@ -799,17 +1301,17 @@ function truncateMiddle(content, options = {}) {
|
|
|
799
1301
|
const headLines = options.maxHeadLines ?? Math.max(1, Math.floor(maxLines / 2));
|
|
800
1302
|
const tailLines = Math.max(0, maxLines - headLines);
|
|
801
1303
|
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
802
|
-
const totalLines = countNewlines
|
|
803
|
-
if (totalBytes <= maxBytes && totalLines <= maxLines) return noTruncResult
|
|
1304
|
+
const totalLines = countNewlines(content) + 1;
|
|
1305
|
+
if (totalBytes <= maxBytes && totalLines <= maxLines) return noTruncResult(content, totalLines, totalBytes);
|
|
804
1306
|
if (headBytes <= 0 || headLines <= 0) return truncateTail(content, {
|
|
805
1307
|
maxBytes: tailBytes || maxBytes,
|
|
806
1308
|
maxLines: tailLines || maxLines
|
|
807
1309
|
});
|
|
808
|
-
if (tailBytes <= 0 || tailLines <= 0) return truncateHead
|
|
1310
|
+
if (tailBytes <= 0 || tailLines <= 0) return truncateHead(content, {
|
|
809
1311
|
maxBytes: headBytes,
|
|
810
1312
|
maxLines: headLines
|
|
811
1313
|
});
|
|
812
|
-
const head = truncateHead
|
|
1314
|
+
const head = truncateHead(content, {
|
|
813
1315
|
maxBytes: headBytes,
|
|
814
1316
|
maxLines: headLines
|
|
815
1317
|
});
|
|
@@ -823,7 +1325,7 @@ function truncateMiddle(content, options = {}) {
|
|
|
823
1325
|
const tailBytesKept = tail.outputBytes ?? Buffer.byteLength(tail.content, "utf-8");
|
|
824
1326
|
if (headLinesKept === 0 || head.firstLineExceedsLimit) return tail;
|
|
825
1327
|
if (tailLinesKept === 0) return head;
|
|
826
|
-
if (headLinesKept + tailLinesKept >= totalLines) return noTruncResult
|
|
1328
|
+
if (headLinesKept + tailLinesKept >= totalLines) return noTruncResult(content, totalLines, totalBytes);
|
|
827
1329
|
const elidedLines = totalLines - headLinesKept - tailLinesKept;
|
|
828
1330
|
const elidedBytes = Math.max(0, totalBytes - headBytesKept - tailBytesKept);
|
|
829
1331
|
const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
|
|
@@ -844,10 +1346,10 @@ function truncateMiddle(content, options = {}) {
|
|
|
844
1346
|
};
|
|
845
1347
|
}
|
|
846
1348
|
const MAX_PENDING = 10;
|
|
847
|
-
var TailBuffer = class {
|
|
1349
|
+
var TailBuffer$1 = class {
|
|
1350
|
+
maxBytes;
|
|
848
1351
|
#pending = [];
|
|
849
1352
|
#pos = 0;
|
|
850
|
-
maxBytes;
|
|
851
1353
|
constructor(maxBytes) {
|
|
852
1354
|
this.maxBytes = maxBytes;
|
|
853
1355
|
}
|
|
@@ -882,11 +1384,6 @@ var TailBuffer = class {
|
|
|
882
1384
|
this.#trimTo(max);
|
|
883
1385
|
return this.#flush();
|
|
884
1386
|
}
|
|
885
|
-
/** Drop all held output; the next {@link append} starts a fresh window. */
|
|
886
|
-
reset() {
|
|
887
|
-
this.#pending.length = 0;
|
|
888
|
-
this.#pos = 0;
|
|
889
|
-
}
|
|
890
1387
|
bytes() {
|
|
891
1388
|
const max = this.maxBytes;
|
|
892
1389
|
this.#trimTo(max);
|
|
@@ -914,7 +1411,7 @@ var TailBuffer = class {
|
|
|
914
1411
|
this.#pending.length = 1;
|
|
915
1412
|
}
|
|
916
1413
|
};
|
|
917
|
-
var OutputSink = class {
|
|
1414
|
+
var OutputSink$1 = class {
|
|
918
1415
|
#buffer = "";
|
|
919
1416
|
#bufferBytes = 0;
|
|
920
1417
|
#head = "";
|
|
@@ -927,6 +1424,7 @@ var OutputSink = class {
|
|
|
927
1424
|
#truncated = false;
|
|
928
1425
|
#lastChunkTime = 0;
|
|
929
1426
|
#pendingChunk = "";
|
|
1427
|
+
#pendingCarriageReturn = false;
|
|
930
1428
|
#pendingChunkTimer;
|
|
931
1429
|
#currentLineBytes = 0;
|
|
932
1430
|
#columnEllipsisAdded = false;
|
|
@@ -935,6 +1433,10 @@ var OutputSink = class {
|
|
|
935
1433
|
#file;
|
|
936
1434
|
#pendingFileWrites;
|
|
937
1435
|
#fileReady = false;
|
|
1436
|
+
/** In-flight sink creation, awaited by finalize/dispose so a fd opened by a late chunk is still released. */
|
|
1437
|
+
#fileCreation;
|
|
1438
|
+
/** Set once the spill file has been closed; guards double-close and post-finalize resurrection. */
|
|
1439
|
+
#finalized = false;
|
|
938
1440
|
#artifactPath;
|
|
939
1441
|
#artifactId;
|
|
940
1442
|
#spillThreshold;
|
|
@@ -951,11 +1453,11 @@ var OutputSink = class {
|
|
|
951
1453
|
#artifactTailRingBytes = 0;
|
|
952
1454
|
#artifactTailIncomingBytes = 0;
|
|
953
1455
|
constructor(options) {
|
|
954
|
-
const { artifactPath, artifactId, spillThreshold = DEFAULT_MAX_BYTES
|
|
1456
|
+
const { artifactPath, artifactId, spillThreshold = DEFAULT_MAX_BYTES, headBytes = 0, maxColumns = 0, onChunk, chunkThrottleMs = 0, artifactMaxBytes = 0, artifactHeadBytes = ARTIFACT_DEFAULT_HEAD_BYTES } = options ?? {};
|
|
955
1457
|
this.#artifactPath = artifactPath;
|
|
956
1458
|
this.#artifactId = artifactId;
|
|
957
1459
|
this.#spillThreshold = spillThreshold;
|
|
958
|
-
this.#headLimit = Math.max(0, headBytes);
|
|
1460
|
+
this.#headLimit = Math.max(0, Math.min(headBytes, Math.floor(spillThreshold / 2)));
|
|
959
1461
|
this.#maxColumns = Math.max(0, maxColumns);
|
|
960
1462
|
this.#onChunk = onChunk;
|
|
961
1463
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
@@ -964,11 +1466,42 @@ var OutputSink = class {
|
|
|
964
1466
|
this.#artifactTailBudget = Math.max(0, this.#artifactMaxBytes - this.#artifactHeadBudget);
|
|
965
1467
|
}
|
|
966
1468
|
/**
|
|
1469
|
+
* Converts carriage-return progress updates into line boundaries while
|
|
1470
|
+
* collapsing CRLF to one newline. A trailing CR is held until the next
|
|
1471
|
+
* chunk so split CRLF sequences do not create blank lines.
|
|
1472
|
+
*/
|
|
1473
|
+
#normalizeCarriageReturns(text) {
|
|
1474
|
+
if (text.length === 0 || !this.#pendingCarriageReturn && !text.includes(CR)) return text;
|
|
1475
|
+
let cursor = 0;
|
|
1476
|
+
let normalized = "";
|
|
1477
|
+
if (this.#pendingCarriageReturn) {
|
|
1478
|
+
this.#pendingCarriageReturn = false;
|
|
1479
|
+
normalized = NL;
|
|
1480
|
+
if (text.startsWith(NL)) cursor = 1;
|
|
1481
|
+
}
|
|
1482
|
+
while (cursor < text.length) {
|
|
1483
|
+
const carriageReturn = text.indexOf(CR, cursor);
|
|
1484
|
+
if (carriageReturn === -1) {
|
|
1485
|
+
normalized += text.substring(cursor);
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
normalized += text.substring(cursor, carriageReturn);
|
|
1489
|
+
if (carriageReturn === text.length - 1) {
|
|
1490
|
+
this.#pendingCarriageReturn = true;
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1493
|
+
normalized += NL;
|
|
1494
|
+
cursor = text.startsWith(NL, carriageReturn + 1) ? carriageReturn + 2 : carriageReturn + 1;
|
|
1495
|
+
}
|
|
1496
|
+
return normalized;
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
967
1499
|
* Push a chunk of output. The buffer management and onChunk callback run
|
|
968
1500
|
* synchronously. File sink writes are deferred and serialized internally.
|
|
969
1501
|
*/
|
|
970
1502
|
push(chunk) {
|
|
971
|
-
|
|
1503
|
+
if (this.#finalized) return;
|
|
1504
|
+
chunk = sanitizeWithOptionalSixelPassthrough(chunk, (text) => sanitizeText(this.#normalizeCarriageReturns(text)));
|
|
972
1505
|
if (this.#onChunk) {
|
|
973
1506
|
const now = Date.now();
|
|
974
1507
|
if (now - this.#lastChunkTime >= this.#chunkThrottleMs) this.#emitPendingChunkWith(chunk, now);
|
|
@@ -981,7 +1514,7 @@ var OutputSink = class {
|
|
|
981
1514
|
this.#totalBytes += rawBytes;
|
|
982
1515
|
if (chunk.length > 0) {
|
|
983
1516
|
this.#sawData = true;
|
|
984
|
-
this.#totalLines += countNewlines
|
|
1517
|
+
this.#totalLines += countNewlines(chunk);
|
|
985
1518
|
}
|
|
986
1519
|
const capped = this.#maxColumns > 0 ? this.#applyColumnCap(chunk) : chunk;
|
|
987
1520
|
const cappedBytes = capped === chunk ? rawBytes : Buffer.byteLength(capped, "utf-8");
|
|
@@ -995,14 +1528,14 @@ var OutputSink = class {
|
|
|
995
1528
|
if (cappedBytes <= room) {
|
|
996
1529
|
this.#head += capped;
|
|
997
1530
|
this.#headBytes += cappedBytes;
|
|
998
|
-
this.#headLines += countNewlines
|
|
1531
|
+
this.#headLines += countNewlines(capped);
|
|
999
1532
|
return;
|
|
1000
1533
|
}
|
|
1001
|
-
const headSlice = truncateHeadBytes
|
|
1534
|
+
const headSlice = truncateHeadBytes(capped, room);
|
|
1002
1535
|
if (headSlice.bytes > 0) {
|
|
1003
1536
|
this.#head += headSlice.text;
|
|
1004
1537
|
this.#headBytes += headSlice.bytes;
|
|
1005
|
-
this.#headLines += countNewlines
|
|
1538
|
+
this.#headLines += countNewlines(headSlice.text);
|
|
1006
1539
|
tailChunk = capped.substring(headSlice.text.length);
|
|
1007
1540
|
tailBytes = cappedBytes - headSlice.bytes;
|
|
1008
1541
|
}
|
|
@@ -1021,7 +1554,7 @@ var OutputSink = class {
|
|
|
1021
1554
|
const parts = [];
|
|
1022
1555
|
let cursor = 0;
|
|
1023
1556
|
while (cursor < chunk.length) {
|
|
1024
|
-
const nlIdx = chunk.indexOf(NL
|
|
1557
|
+
const nlIdx = chunk.indexOf(NL, cursor);
|
|
1025
1558
|
const segEnd = nlIdx === -1 ? chunk.length : nlIdx;
|
|
1026
1559
|
if (segEnd > cursor) {
|
|
1027
1560
|
const segment = chunk.substring(cursor, segEnd);
|
|
@@ -1038,7 +1571,7 @@ var OutputSink = class {
|
|
|
1038
1571
|
let kept = "";
|
|
1039
1572
|
let keptBytes = 0;
|
|
1040
1573
|
if (headRoom > 0) {
|
|
1041
|
-
const sliced = truncateHeadBytes
|
|
1574
|
+
const sliced = truncateHeadBytes(segment, headRoom);
|
|
1042
1575
|
kept = sliced.text;
|
|
1043
1576
|
keptBytes = sliced.bytes;
|
|
1044
1577
|
parts.push(kept);
|
|
@@ -1052,7 +1585,7 @@ var OutputSink = class {
|
|
|
1052
1585
|
}
|
|
1053
1586
|
}
|
|
1054
1587
|
if (nlIdx === -1) break;
|
|
1055
|
-
parts.push(NL
|
|
1588
|
+
parts.push(NL);
|
|
1056
1589
|
this.#currentLineBytes = 0;
|
|
1057
1590
|
this.#columnEllipsisAdded = false;
|
|
1058
1591
|
cursor = nlIdx + 1;
|
|
@@ -1060,11 +1593,11 @@ var OutputSink = class {
|
|
|
1060
1593
|
return parts.join("");
|
|
1061
1594
|
}
|
|
1062
1595
|
#willOverflow(dataBytes) {
|
|
1063
|
-
return this.#bufferBytes + dataBytes > this.#spillThreshold;
|
|
1596
|
+
return this.#bufferBytes + dataBytes > this.#spillThreshold - this.#headBytes;
|
|
1064
1597
|
}
|
|
1065
1598
|
#pushTail(chunk, dataBytes) {
|
|
1066
1599
|
if (dataBytes === 0) return;
|
|
1067
|
-
const threshold = this.#spillThreshold;
|
|
1600
|
+
const threshold = Math.max(0, this.#spillThreshold - this.#headBytes);
|
|
1068
1601
|
if (!(this.#bufferBytes + dataBytes > threshold)) {
|
|
1069
1602
|
this.#buffer += chunk;
|
|
1070
1603
|
this.#bufferBytes += dataBytes;
|
|
@@ -1097,7 +1630,7 @@ var OutputSink = class {
|
|
|
1097
1630
|
}
|
|
1098
1631
|
if (!this.#pendingFileWrites) {
|
|
1099
1632
|
this.#pendingFileWrites = [chunk];
|
|
1100
|
-
this.#createFileSink();
|
|
1633
|
+
this.#fileCreation = this.#createFileSink();
|
|
1101
1634
|
} else this.#pendingFileWrites.push(chunk);
|
|
1102
1635
|
}
|
|
1103
1636
|
/**
|
|
@@ -1126,7 +1659,7 @@ var OutputSink = class {
|
|
|
1126
1659
|
}
|
|
1127
1660
|
let overflow = chunk;
|
|
1128
1661
|
if (room > 0) {
|
|
1129
|
-
const headSlice = truncateHeadBytes
|
|
1662
|
+
const headSlice = truncateHeadBytes(chunk, room);
|
|
1130
1663
|
if (headSlice.bytes > 0) {
|
|
1131
1664
|
this.#file.sink.write(headSlice.text);
|
|
1132
1665
|
this.#artifactHeadBytesWritten += headSlice.bytes;
|
|
@@ -1161,8 +1694,7 @@ var OutputSink = class {
|
|
|
1161
1694
|
async #createFileSink() {
|
|
1162
1695
|
if (!this.#artifactPath || this.#fileReady) return;
|
|
1163
1696
|
try {
|
|
1164
|
-
const sink =
|
|
1165
|
-
sink.on("error", () => void 0);
|
|
1697
|
+
const sink = Bun.file(this.#artifactPath).writer();
|
|
1166
1698
|
this.#file = {
|
|
1167
1699
|
path: this.#artifactPath,
|
|
1168
1700
|
artifactId: this.#artifactId,
|
|
@@ -1218,7 +1750,7 @@ var OutputSink = class {
|
|
|
1218
1750
|
this.#headLines = 0;
|
|
1219
1751
|
this.#headRetentionDisabled = true;
|
|
1220
1752
|
this.#totalBytes = this.#bufferBytes;
|
|
1221
|
-
this.#totalLines = countNewlines
|
|
1753
|
+
this.#totalLines = countNewlines(text);
|
|
1222
1754
|
this.#sawData = text.length > 0;
|
|
1223
1755
|
this.#truncated = false;
|
|
1224
1756
|
this.#currentLineBytes = 0;
|
|
@@ -1226,6 +1758,7 @@ var OutputSink = class {
|
|
|
1226
1758
|
this.#columnDroppedBytes = 0;
|
|
1227
1759
|
this.#columnTruncatedLines = 0;
|
|
1228
1760
|
this.#pendingChunk = "";
|
|
1761
|
+
this.#pendingCarriageReturn = false;
|
|
1229
1762
|
}
|
|
1230
1763
|
#clearPendingChunkTimer() {
|
|
1231
1764
|
if (!this.#pendingChunkTimer) return;
|
|
@@ -1286,22 +1819,19 @@ var OutputSink = class {
|
|
|
1286
1819
|
if (tailBytes > 0) this.#file.sink.write(this.#artifactTailRing);
|
|
1287
1820
|
}
|
|
1288
1821
|
async dump(notice) {
|
|
1822
|
+
if (this.#pendingCarriageReturn) {
|
|
1823
|
+
this.#pendingCarriageReturn = false;
|
|
1824
|
+
this.push(NL);
|
|
1825
|
+
}
|
|
1289
1826
|
const noticeLine = notice ? `[${notice}]\n` : "";
|
|
1290
1827
|
this.#flushPendingChunk();
|
|
1291
1828
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
1292
|
-
|
|
1293
|
-
this.#flushArtifactTailIfCapped();
|
|
1294
|
-
const fileSink = this.#file.sink;
|
|
1295
|
-
await new Promise((resolve) => {
|
|
1296
|
-
fileSink.once("error", () => resolve());
|
|
1297
|
-
fileSink.end(() => resolve());
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1829
|
+
await this.#finalizeFile();
|
|
1300
1830
|
const headBytes = this.#headBytes;
|
|
1301
1831
|
const tailBuf = this.#buffer;
|
|
1302
1832
|
const tailBytes = this.#bufferBytes;
|
|
1303
1833
|
const headLines = this.#headLines + (headBytes > 0 && !this.#head.endsWith("\n") ? 1 : 0);
|
|
1304
|
-
const tailLines = tailBuf.length > 0 ? countNewlines
|
|
1834
|
+
const tailLines = tailBuf.length > 0 ? countNewlines(tailBuf) + 1 : 0;
|
|
1305
1835
|
const effectiveTotalBytes = Math.max(0, this.#totalBytes - this.#columnDroppedBytes);
|
|
1306
1836
|
let body;
|
|
1307
1837
|
let outputBytes;
|
|
@@ -1322,7 +1852,7 @@ var OutputSink = class {
|
|
|
1322
1852
|
} else if (headBytes > 0) {
|
|
1323
1853
|
body = `${this.#head}${tailBuf}`;
|
|
1324
1854
|
outputBytes = headBytes + tailBytes;
|
|
1325
|
-
outputLines = body.length > 0 ? countNewlines
|
|
1855
|
+
outputLines = body.length > 0 ? countNewlines(body) + 1 : 0;
|
|
1326
1856
|
} else {
|
|
1327
1857
|
body = tailBuf;
|
|
1328
1858
|
outputBytes = tailBytes;
|
|
@@ -1340,12 +1870,158 @@ var OutputSink = class {
|
|
|
1340
1870
|
columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : void 0,
|
|
1341
1871
|
columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : void 0,
|
|
1342
1872
|
columnMax: this.#columnTruncatedLines > 0 ? this.#maxColumns : void 0,
|
|
1343
|
-
artifactId: this.#file?.artifactId
|
|
1344
|
-
artifactPath: this.#file?.path
|
|
1873
|
+
artifactId: this.#file?.artifactId
|
|
1345
1874
|
};
|
|
1346
1875
|
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Flush any capped artifact tail and close the spill file descriptor,
|
|
1878
|
+
* awaiting an in-flight sink creation so a descriptor opened by a late
|
|
1879
|
+
* chunk is still released. Idempotent via {@link #finalized}: the artifact
|
|
1880
|
+
* is finalized exactly once whether the caller reached {@link dump} or
|
|
1881
|
+
* bailed through {@link dispose}. `#file` is left set so {@link dump} can
|
|
1882
|
+
* still read `artifactId` for its summary.
|
|
1883
|
+
*/
|
|
1884
|
+
async #finalizeFile() {
|
|
1885
|
+
if (this.#finalized) return;
|
|
1886
|
+
this.#finalized = true;
|
|
1887
|
+
if (this.#fileCreation) await this.#fileCreation.catch(() => void 0);
|
|
1888
|
+
const file = this.#file;
|
|
1889
|
+
if (!file) return;
|
|
1890
|
+
try {
|
|
1891
|
+
this.#flushArtifactTailIfCapped();
|
|
1892
|
+
} catch {} finally {
|
|
1893
|
+
try {
|
|
1894
|
+
await file.sink.end();
|
|
1895
|
+
} catch {}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Release the artifact spill descriptor on an exit path that skips
|
|
1900
|
+
* {@link dump} — a thrown error or abort. Idempotent and safe in a
|
|
1901
|
+
* `finally`: if {@link dump} already ran this is a no-op, otherwise it
|
|
1902
|
+
* flushes the capped tail and closes the sink so the descriptor is not
|
|
1903
|
+
* leaked until a later unrelated read hits `EMFILE` (issue #6463).
|
|
1904
|
+
*/
|
|
1905
|
+
async dispose() {
|
|
1906
|
+
this.#clearPendingChunkTimer();
|
|
1907
|
+
await this.#finalizeFile();
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
//#endregion
|
|
1911
|
+
//#region src/tools/bash/streaming-output.ts
|
|
1912
|
+
/**
|
|
1913
|
+
* Marker passed as the verbatim sink's `artifactId` when the caller configured
|
|
1914
|
+
* only `artifactPath`: the verbatim sink reports `artifactId` from its internal
|
|
1915
|
+
* file record, which is set exactly when the spill file was created — so the
|
|
1916
|
+
* marker doubles as a "mirror exists" signal for surfacing `artifactPath` on
|
|
1917
|
+
* {@link OutputSink.dump}. Stripped again before the summary leaves this file.
|
|
1918
|
+
*/
|
|
1919
|
+
const SPILL_PATH_ONLY = "__dsh-spill-path-only__";
|
|
1920
|
+
var OutputSink = class extends OutputSink$1 {
|
|
1921
|
+
#artifactPath;
|
|
1922
|
+
constructor(options) {
|
|
1923
|
+
super({
|
|
1924
|
+
...options,
|
|
1925
|
+
artifactId: options?.artifactId ?? (options?.artifactPath !== void 0 ? SPILL_PATH_ONLY : void 0)
|
|
1926
|
+
});
|
|
1927
|
+
this.#artifactPath = options?.artifactPath;
|
|
1928
|
+
}
|
|
1929
|
+
async dump(notice) {
|
|
1930
|
+
const { artifactId, ...rest } = await super.dump(notice);
|
|
1931
|
+
if (artifactId === SPILL_PATH_ONLY) return {
|
|
1932
|
+
...rest,
|
|
1933
|
+
artifactPath: this.#artifactPath
|
|
1934
|
+
};
|
|
1935
|
+
if (artifactId !== void 0) return {
|
|
1936
|
+
...rest,
|
|
1937
|
+
artifactId,
|
|
1938
|
+
artifactPath: this.#artifactPath
|
|
1939
|
+
};
|
|
1940
|
+
return rest;
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
/**
|
|
1944
|
+
* Composition wrapper over the verbatim {@link TailBuffer}, adding the DSH
|
|
1945
|
+
* `reset()` increment. The verbatim class keeps its state in private fields,
|
|
1946
|
+
* so reset is modeled as a fresh window with the same budget.
|
|
1947
|
+
*/
|
|
1948
|
+
var TailBuffer = class {
|
|
1949
|
+
#inner;
|
|
1950
|
+
maxBytes;
|
|
1951
|
+
constructor(maxBytes) {
|
|
1952
|
+
this.maxBytes = maxBytes;
|
|
1953
|
+
this.#inner = new TailBuffer$1(maxBytes);
|
|
1954
|
+
}
|
|
1955
|
+
append(text) {
|
|
1956
|
+
this.#inner.append(text);
|
|
1957
|
+
}
|
|
1958
|
+
text() {
|
|
1959
|
+
return this.#inner.text();
|
|
1960
|
+
}
|
|
1961
|
+
bytes() {
|
|
1962
|
+
return this.#inner.bytes();
|
|
1963
|
+
}
|
|
1964
|
+
/** Drop all held output; the next {@link append} starts a fresh window. */
|
|
1965
|
+
reset() {
|
|
1966
|
+
this.#inner = new TailBuffer$1(this.maxBytes);
|
|
1967
|
+
}
|
|
1347
1968
|
};
|
|
1348
1969
|
//#endregion
|
|
1970
|
+
//#region src/tools/bash/windows-shell.ts
|
|
1971
|
+
/**
|
|
1972
|
+
* Windows shell discovery, ported from oh-my-pi procmgr's `resolveWindowsShell`
|
|
1973
|
+
* (refs/oh-my-pi/packages/utils/src/procmgr.ts:148-173) — adapted verbatim:
|
|
1974
|
+
* `Bun.env` → injectable Node env (default `process.env`), and pi-utils
|
|
1975
|
+
* `$which` → `findOnPath` (src/tools/bash/which.ts).
|
|
1976
|
+
*
|
|
1977
|
+
* Deliberately NOT ported from procmgr: buildSpawnEnv, shell prefix handling,
|
|
1978
|
+
* direnv integration, and PI_SHELL_PREFIX — DSH has no shell-prefix seam;
|
|
1979
|
+
* commands run inside the embedded brush shell or the resolved user shell.
|
|
1980
|
+
* @module @xiaoso/dsh-tool-plus/bash/windows-shell
|
|
1981
|
+
*/
|
|
1982
|
+
/**
|
|
1983
|
+
* Resolve the external shell to advertise on Windows.
|
|
1984
|
+
*
|
|
1985
|
+
* A host bash is OPTIONAL: bash tool commands always execute in the embedded
|
|
1986
|
+
* brush-core shell. The resolved binary only serves the spawn-a-shell paths,
|
|
1987
|
+
* so this prefers a real Git Bash when one exists and otherwise falls back to
|
|
1988
|
+
* cmd.exe — it never fails.
|
|
1989
|
+
*
|
|
1990
|
+
* Search order:
|
|
1991
|
+
* 1. Git for Windows install roots (machine + per-user installers)
|
|
1992
|
+
* 2. scoop installs — scoop's git manifest sets GIT_INSTALL_ROOT and shims
|
|
1993
|
+
* sh.exe/git.exe but never bash.exe, so PATH lookup alone misses it
|
|
1994
|
+
* 3. bash.exe on PATH (Cygwin, MSYS2, ...)
|
|
1995
|
+
* 4. sh.exe on PATH (Git for Windows' sh.exe is bash; prefer a sibling
|
|
1996
|
+
* bash.exe when present)
|
|
1997
|
+
* 5. cmd.exe from ComSpec
|
|
1998
|
+
*
|
|
1999
|
+
* Exported for tests; `env` overrides process.env-based discovery.
|
|
2000
|
+
*/
|
|
2001
|
+
function resolveWindowsShell(env = process.env) {
|
|
2002
|
+
const gitRoots = [
|
|
2003
|
+
env.ProgramFiles && path.join(env.ProgramFiles, "Git"),
|
|
2004
|
+
env["ProgramFiles(x86)"] && path.join(env["ProgramFiles(x86)"], "Git"),
|
|
2005
|
+
env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, "Programs", "Git"),
|
|
2006
|
+
env.GIT_INSTALL_ROOT,
|
|
2007
|
+
env.SCOOP && path.join(env.SCOOP, "apps", "git", "current"),
|
|
2008
|
+
env.USERPROFILE && path.join(env.USERPROFILE, "scoop", "apps", "git", "current")
|
|
2009
|
+
];
|
|
2010
|
+
for (const root of gitRoots) {
|
|
2011
|
+
if (!root) continue;
|
|
2012
|
+
const candidate = path.join(root, "bin", "bash.exe");
|
|
2013
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
2014
|
+
}
|
|
2015
|
+
const bashOnPath = findOnPath("bash.exe", env);
|
|
2016
|
+
if (bashOnPath) return bashOnPath;
|
|
2017
|
+
const shOnPath = findOnPath("sh.exe", env);
|
|
2018
|
+
if (shOnPath) {
|
|
2019
|
+
const siblingBash = path.join(path.dirname(shOnPath), "bash.exe");
|
|
2020
|
+
return fs.existsSync(siblingBash) ? siblingBash : shOnPath;
|
|
2021
|
+
}
|
|
2022
|
+
return env.ComSpec || env.COMSPEC || "C:\\Windows\\System32\\cmd.exe";
|
|
2023
|
+
}
|
|
2024
|
+
//#endregion
|
|
1349
2025
|
//#region src/tools/bash/yield.ts
|
|
1350
2026
|
/**
|
|
1351
2027
|
* Cooperative yield utility for preventing Bun event-loop busy-wait.
|
|
@@ -1468,6 +2144,11 @@ var ExponentialYield = class {
|
|
|
1468
2144
|
* Bash command execution with streaming support and cancellation.
|
|
1469
2145
|
*
|
|
1470
2146
|
* Uses brush-core via native bindings for shell execution.
|
|
2147
|
+
|
|
2148
|
+
* Known deviations from upstream exec/bash-executor.ts:
|
|
2149
|
+
* - POSIX getShellConfig does not add `-l` to zsh args and does not honor
|
|
2150
|
+
* PI_BASH_NO_LOGIN — DSH has no PI_BASH_NO_LOGIN semantics and the shell
|
|
2151
|
+
* `args` currently have no consumer beyond the user-shell spawn path.
|
|
1471
2152
|
*/
|
|
1472
2153
|
function isExecutable(path) {
|
|
1473
2154
|
try {
|
|
@@ -1481,22 +2162,8 @@ let cachedShellConfig = null;
|
|
|
1481
2162
|
function getShellConfig() {
|
|
1482
2163
|
if (cachedShellConfig) return cachedShellConfig;
|
|
1483
2164
|
if (process.platform === "win32") {
|
|
1484
|
-
const paths = [];
|
|
1485
|
-
const programFiles = process.env.ProgramFiles;
|
|
1486
|
-
if (programFiles) paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
|
|
1487
|
-
const programFilesX86 = process.env["ProgramFiles(x86)"];
|
|
1488
|
-
if (programFilesX86) paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
|
|
1489
|
-
for (const p of paths) if (fs.existsSync(p)) {
|
|
1490
|
-
cachedShellConfig = {
|
|
1491
|
-
shell: p,
|
|
1492
|
-
args: ["--login", "-c"],
|
|
1493
|
-
env: {},
|
|
1494
|
-
prefix: void 0
|
|
1495
|
-
};
|
|
1496
|
-
return cachedShellConfig;
|
|
1497
|
-
}
|
|
1498
2165
|
cachedShellConfig = {
|
|
1499
|
-
shell:
|
|
2166
|
+
shell: resolveWindowsShell(),
|
|
1500
2167
|
args: ["--login", "-c"],
|
|
1501
2168
|
env: {},
|
|
1502
2169
|
prefix: void 0
|
|
@@ -1563,6 +2230,7 @@ function closeSessionShells(sessionId) {
|
|
|
1563
2230
|
*/
|
|
1564
2231
|
const retainedShells = /* @__PURE__ */ new Set();
|
|
1565
2232
|
const RETAIN_REAP_INTERVAL_MS = 5e3;
|
|
2233
|
+
const NATIVE_TIMEOUT_FALLBACK_GRACE_MS = 5e3;
|
|
1566
2234
|
async function retainShellWithLiveBackgroundJobs(shell) {
|
|
1567
2235
|
let live;
|
|
1568
2236
|
try {
|
|
@@ -1618,7 +2286,8 @@ function isBashShell(shell) {
|
|
|
1618
2286
|
return shellBasename(shell).includes("bash");
|
|
1619
2287
|
}
|
|
1620
2288
|
function needsInteractiveShellArg(shell) {
|
|
1621
|
-
|
|
2289
|
+
const basename = shellBasename(shell);
|
|
2290
|
+
return basename.includes("zsh") || basename.includes("fish");
|
|
1622
2291
|
}
|
|
1623
2292
|
function supportsAutoUserShell(shell) {
|
|
1624
2293
|
const basename = shellBasename(shell);
|
|
@@ -1628,16 +2297,18 @@ function hasInteractiveShellArg(args) {
|
|
|
1628
2297
|
return args.some((arg) => arg === "--interactive" || /^-[^-]*i/.test(arg));
|
|
1629
2298
|
}
|
|
1630
2299
|
function ensureInteractiveShellArgs(shell, args) {
|
|
1631
|
-
if (!needsInteractiveShellArg(shell)
|
|
1632
|
-
const
|
|
2300
|
+
if (!needsInteractiveShellArg(shell)) return args;
|
|
2301
|
+
const effectiveArgs = shellBasename(shell).includes("fish") ? args.filter((arg) => arg !== "-l" && arg !== "--login") : args;
|
|
2302
|
+
if (hasInteractiveShellArg(effectiveArgs)) return effectiveArgs;
|
|
2303
|
+
const commandIndex = effectiveArgs.findIndex((arg) => arg === "-c" || arg === "--command");
|
|
1633
2304
|
if (commandIndex !== -1) return [
|
|
1634
|
-
...
|
|
2305
|
+
...effectiveArgs.slice(0, commandIndex),
|
|
1635
2306
|
"-i",
|
|
1636
|
-
...
|
|
2307
|
+
...effectiveArgs.slice(commandIndex)
|
|
1637
2308
|
];
|
|
1638
|
-
const compactCommandIndex =
|
|
1639
|
-
if (compactCommandIndex !== -1) return
|
|
1640
|
-
return [...
|
|
2309
|
+
const compactCommandIndex = effectiveArgs.findIndex((arg) => /^-[^-]*c[^-]*$/.test(arg));
|
|
2310
|
+
if (compactCommandIndex !== -1) return effectiveArgs.map((arg, index) => index === compactCommandIndex ? arg.replace("c", "ic") : arg);
|
|
2311
|
+
return [...effectiveArgs, "-i"];
|
|
1641
2312
|
}
|
|
1642
2313
|
function quoteShellArg(value) {
|
|
1643
2314
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
@@ -1738,10 +2409,13 @@ async function executeBash(command, options) {
|
|
|
1738
2409
|
const deadlineTimeoutMs = requestedTimeoutMs === 0 ? void 0 : Math.max(1e3, requestedTimeoutMs ?? 3e5);
|
|
1739
2410
|
const nativeTimeoutMs = requestedTimeoutMs !== void 0 && requestedTimeoutMs > 0 ? requestedTimeoutMs : void 0;
|
|
1740
2411
|
const nativeOwnsTimeout = nativeTimeoutMs !== void 0;
|
|
1741
|
-
if (deadlineTimeoutMs !== void 0)
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2412
|
+
if (deadlineTimeoutMs !== void 0) {
|
|
2413
|
+
const fallbackTimeoutMs = nativeOwnsTimeout ? deadlineTimeoutMs + NATIVE_TIMEOUT_FALLBACK_GRACE_MS : deadlineTimeoutMs;
|
|
2414
|
+
timeoutTimer = setTimeout(() => {
|
|
2415
|
+
if (!nativeOwnsTimeout) abortCurrentExecution();
|
|
2416
|
+
timeoutDeferred.resolve("timeout");
|
|
2417
|
+
}, fallbackTimeoutMs);
|
|
2418
|
+
}
|
|
1745
2419
|
let resetSession = false;
|
|
1746
2420
|
try {
|
|
1747
2421
|
const runPromise = executionShell.run({
|
|
@@ -1771,6 +2445,7 @@ async function executeBash(command, options) {
|
|
|
1771
2445
|
return {
|
|
1772
2446
|
exitCode: void 0,
|
|
1773
2447
|
cancelled: true,
|
|
2448
|
+
...winner.kind === "timeout" ? { timedOut: true } : {},
|
|
1774
2449
|
...await settle(winner.kind === "timeout" && deadlineTimeoutMs !== void 0 ? `Command timed out after ${Math.round(deadlineTimeoutMs / 1e3)} seconds` : "Command cancelled")
|
|
1775
2450
|
};
|
|
1776
2451
|
}
|
|
@@ -1785,6 +2460,7 @@ async function executeBash(command, options) {
|
|
|
1785
2460
|
return {
|
|
1786
2461
|
exitCode: void 0,
|
|
1787
2462
|
cancelled: true,
|
|
2463
|
+
timedOut: true,
|
|
1788
2464
|
...await settle(annotation)
|
|
1789
2465
|
};
|
|
1790
2466
|
}
|
|
@@ -1992,16 +2668,22 @@ function startBashJob(options) {
|
|
|
1992
2668
|
settledText = preview.text();
|
|
1993
2669
|
const wallTimeMs = performance.now() - startedAt;
|
|
1994
2670
|
const aborted = result.cancelled && abortController.signal.aborted;
|
|
2671
|
+
const timedOut = result.timedOut ?? (result.cancelled && !aborted);
|
|
2672
|
+
let text = result.output;
|
|
2673
|
+
if (timedOut) {
|
|
2674
|
+
const seconds = Math.max(1, Math.round((timeoutMs ?? 0) / 1e3));
|
|
2675
|
+
if (!text.includes("[Command timed out after")) text = `[Command timed out after ${seconds} seconds]\n\n${text}`;
|
|
2676
|
+
}
|
|
1995
2677
|
return {
|
|
1996
2678
|
kind: "foreground",
|
|
1997
2679
|
exitCode: result.exitCode ?? null,
|
|
1998
|
-
timedOut
|
|
2680
|
+
timedOut,
|
|
1999
2681
|
aborted,
|
|
2000
2682
|
timeoutMs: timeoutMs ?? null,
|
|
2001
2683
|
wallTimeMs,
|
|
2002
2684
|
...result.workingDir !== void 0 ? { workingDir: result.workingDir } : {},
|
|
2003
2685
|
output: {
|
|
2004
|
-
text
|
|
2686
|
+
text,
|
|
2005
2687
|
truncated: result.truncated,
|
|
2006
2688
|
...result.spillPath !== void 0 ? { spillPath: result.spillPath } : {},
|
|
2007
2689
|
...result.originalOutputPath !== void 0 ? { originalSpillPath: result.originalOutputPath } : {},
|
|
@@ -2024,6 +2706,10 @@ function startBashJob(options) {
|
|
|
2024
2706
|
status: "killed",
|
|
2025
2707
|
detail: "cancelled"
|
|
2026
2708
|
};
|
|
2709
|
+
if (value.timedOut) return {
|
|
2710
|
+
status: "failed",
|
|
2711
|
+
detail: `Command timed out after ${Math.max(1, Math.round((value.timeoutMs ?? 0) / 1e3))} seconds`
|
|
2712
|
+
};
|
|
2027
2713
|
return {
|
|
2028
2714
|
status: "completed",
|
|
2029
2715
|
detail: value.exitCode === null ? void 0 : `exit code: ${value.exitCode}`
|
|
@@ -2043,157 +2729,52 @@ function startBashJob(options) {
|
|
|
2043
2729
|
};
|
|
2044
2730
|
}
|
|
2045
2731
|
//#endregion
|
|
2046
|
-
//#region src/tools/
|
|
2047
|
-
/**
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
/**
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
if (
|
|
2066
|
-
|
|
2067
|
-
if (
|
|
2068
|
-
const forwardDriveMatch = WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX.exec(filePath);
|
|
2069
|
-
if (forwardDriveMatch) return forwardDriveMatch[1];
|
|
2732
|
+
//#region src/tools/bash/cd-workdir.ts
|
|
2733
|
+
/**
|
|
2734
|
+
* Leading `cd <path> && …` workdir extraction and `~` expansion for the bash
|
|
2735
|
+
* tool.
|
|
2736
|
+
*
|
|
2737
|
+
* Extraction delegates to `extractLeadingCdTarget` (shell-tokenize.ts, verbatim
|
|
2738
|
+
* port of upstream tools/shell-tokenize.ts), so redirect prefixes, extra
|
|
2739
|
+
* arguments, command substitution, and unterminated quotes all bail and leave
|
|
2740
|
+
* the whole command with the shell. `~`/`~/…` prefixes expand against
|
|
2741
|
+
* `os.homedir()` with upstream `expandTilde` semantics
|
|
2742
|
+
* (refs packages/coding-agent/src/tools/path-utils.ts).
|
|
2743
|
+
* @module @xiaoso/dsh-tool-plus/bash/cd-workdir
|
|
2744
|
+
*/
|
|
2745
|
+
/**
|
|
2746
|
+
* Expand a leading `~`/`~/…`/`~\…` prefix to the home directory, matching
|
|
2747
|
+
* upstream `expandTilde` (refs tools/path-utils.ts). A bare `~name` is joined
|
|
2748
|
+
* under the home directory like upstream (user-home approximation).
|
|
2749
|
+
*/
|
|
2750
|
+
function expandTilde$1(filePath, home = os.homedir()) {
|
|
2751
|
+
if (filePath === "~") return home;
|
|
2752
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) return home + filePath.slice(1);
|
|
2753
|
+
if (filePath.startsWith("~")) return path.join(home, filePath.slice(1));
|
|
2070
2754
|
return filePath;
|
|
2071
2755
|
}
|
|
2072
|
-
//#endregion
|
|
2073
|
-
//#region node_modules/.pnpm/@oh-my-pi+pi-utils@17.3.5/node_modules/@oh-my-pi/pi-utils/src/path-tree.ts
|
|
2074
2756
|
/**
|
|
2075
|
-
*
|
|
2076
|
-
* and compaction file-operation lists.
|
|
2757
|
+
* Extract a leading `cd <path> && …` prefix, expanding `~` in the target.
|
|
2077
2758
|
*
|
|
2078
|
-
*
|
|
2079
|
-
*
|
|
2080
|
-
*
|
|
2081
|
-
*
|
|
2082
|
-
*
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
function isUrlLikePath(filePath) {
|
|
2088
|
-
return URL_LIKE_PATH_RE.test(filePath);
|
|
2089
|
-
}
|
|
2090
|
-
function createNode() {
|
|
2759
|
+
* Returns `null` when the command does not start with exactly `cd`, one path
|
|
2760
|
+
* token, and a top-level `&&` — redirects (`cd /tmp 2>/dev/null && …`), extra
|
|
2761
|
+
* arguments, command substitution (`$`, backticks, `(`), and unterminated
|
|
2762
|
+
* quotes all leave the whole command for the shell instead of absorbing shell
|
|
2763
|
+
* syntax into the structured workdir seam.
|
|
2764
|
+
*/
|
|
2765
|
+
function extractCdWorkdir(command, home) {
|
|
2766
|
+
const extracted = extractLeadingCdTarget(command);
|
|
2767
|
+
if (extracted === null) return null;
|
|
2091
2768
|
return {
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
subdirs: [],
|
|
2095
|
-
dirIndex: /* @__PURE__ */ new Map()
|
|
2096
|
-
};
|
|
2097
|
-
}
|
|
2098
|
-
function addFile(node, name, key) {
|
|
2099
|
-
if (node.fileNames.has(name)) return;
|
|
2100
|
-
node.fileNames.add(name);
|
|
2101
|
-
node.files.push({
|
|
2102
|
-
name,
|
|
2103
|
-
key
|
|
2104
|
-
});
|
|
2105
|
-
}
|
|
2106
|
-
/**
|
|
2107
|
-
* Build a directory tree from a flat list of paths. URL-like entries are kept
|
|
2108
|
-
* whole as root-level file leaves (they have no meaningful directory structure).
|
|
2109
|
-
* Absolute paths carry a leading empty segment so they share a common `/` root
|
|
2110
|
-
* and fold like any other prefix.
|
|
2111
|
-
*/
|
|
2112
|
-
function buildPathTree(entries) {
|
|
2113
|
-
const root = createNode();
|
|
2114
|
-
for (const { path: rawPath, isDir, key } of entries) {
|
|
2115
|
-
const normalized = rawPath.replace(/\\/g, "/");
|
|
2116
|
-
const fileKey = key ?? rawPath;
|
|
2117
|
-
if (isUrlLikePath(normalized)) {
|
|
2118
|
-
addFile(root, normalized, fileKey);
|
|
2119
|
-
continue;
|
|
2120
|
-
}
|
|
2121
|
-
const trimmed = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
2122
|
-
if (trimmed.length === 0) continue;
|
|
2123
|
-
const segments = trimmed.split("/");
|
|
2124
|
-
const dirCount = isDir ? segments.length : segments.length - 1;
|
|
2125
|
-
let node = root;
|
|
2126
|
-
for (let i = 0; i < dirCount; i++) {
|
|
2127
|
-
const segment = segments[i];
|
|
2128
|
-
let child = node.dirIndex.get(segment);
|
|
2129
|
-
if (!child) {
|
|
2130
|
-
child = createNode();
|
|
2131
|
-
node.dirIndex.set(segment, child);
|
|
2132
|
-
node.subdirs.push({
|
|
2133
|
-
name: segment,
|
|
2134
|
-
node: child
|
|
2135
|
-
});
|
|
2136
|
-
}
|
|
2137
|
-
node = child;
|
|
2138
|
-
}
|
|
2139
|
-
if (!isDir) addFile(node, segments[segments.length - 1], fileKey);
|
|
2140
|
-
}
|
|
2141
|
-
return root;
|
|
2142
|
-
}
|
|
2143
|
-
/**
|
|
2144
|
-
* Depth-first walk yielding directory and file events. Directories collapse their
|
|
2145
|
-
* single-child chains (`a` → `a/b` → `a/b/c`) so a shared prefix becomes one
|
|
2146
|
-
* header. Each node's direct files are emitted before its subdirectories, keeping
|
|
2147
|
-
* a file unambiguously attached to the header above it.
|
|
2148
|
-
*/
|
|
2149
|
-
function* walkPathTree(node, depth = 0) {
|
|
2150
|
-
for (const file of node.files) yield {
|
|
2151
|
-
kind: "file",
|
|
2152
|
-
depth,
|
|
2153
|
-
name: file.name,
|
|
2154
|
-
key: file.key
|
|
2769
|
+
workdir: expandTilde$1(extracted.path, home),
|
|
2770
|
+
command: extracted.rest
|
|
2155
2771
|
};
|
|
2156
|
-
for (const subdir of node.subdirs) {
|
|
2157
|
-
let dirNode = subdir.node;
|
|
2158
|
-
const parts = [subdir.name];
|
|
2159
|
-
while (dirNode.files.length === 0 && dirNode.subdirs.length === 1) {
|
|
2160
|
-
const only = dirNode.subdirs[0];
|
|
2161
|
-
parts.push(only.name);
|
|
2162
|
-
dirNode = only.node;
|
|
2163
|
-
}
|
|
2164
|
-
yield {
|
|
2165
|
-
kind: "dir",
|
|
2166
|
-
depth,
|
|
2167
|
-
name: parts.join("/"),
|
|
2168
|
-
key: ""
|
|
2169
|
-
};
|
|
2170
|
-
yield* walkPathTree(dirNode, depth + 1);
|
|
2171
|
-
}
|
|
2172
2772
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
* directory header that owns them. Trailing-slash entries are directory
|
|
2179
|
-
* leaves and keep their slash in the header.
|
|
2180
|
-
*
|
|
2181
|
-
* `annotate` receives each file's full original path and its return value is
|
|
2182
|
-
* appended verbatim to the file line (e.g. ` (RW)`).
|
|
2183
|
-
*
|
|
2184
|
-
* Order follows the input: a directory appears when its first member is
|
|
2185
|
-
* emitted, and a node's own files precede its subdirectories.
|
|
2186
|
-
*/
|
|
2187
|
-
function formatGroupedPaths(paths, annotate) {
|
|
2188
|
-
if (paths.length === 0) return "";
|
|
2189
|
-
const tree = buildPathTree(paths.map((entry) => ({
|
|
2190
|
-
path: entry,
|
|
2191
|
-
isDir: entry.endsWith("/")
|
|
2192
|
-
})));
|
|
2193
|
-
const lines = [];
|
|
2194
|
-
for (const event of walkPathTree(tree)) if (event.kind === "dir") lines.push(`${"#".repeat(event.depth + 1)} ${event.name}/`);
|
|
2195
|
-
else lines.push(annotate ? `${event.name}${annotate(event.key)}` : event.name);
|
|
2196
|
-
return lines.join("\n");
|
|
2773
|
+
//#endregion
|
|
2774
|
+
//#region src/tools/omp/config/settings-schema.ts
|
|
2775
|
+
/** Schema default for a setting path (verbatim OMP values; unknown -> undefined). */
|
|
2776
|
+
function getDefault(path) {
|
|
2777
|
+
return OMP_DEFAULTS[path];
|
|
2197
2778
|
}
|
|
2198
2779
|
//#endregion
|
|
2199
2780
|
//#region src/tools/omp/tools/output-meta.ts
|
|
@@ -2469,7 +3050,7 @@ function formatTruncationMetaNotice(truncation) {
|
|
|
2469
3050
|
const elidedLines = truncation.elidedLines ?? Math.max(0, totalLines - truncation.outputLines);
|
|
2470
3051
|
const headPart = head ? `lines ${head.start}-${head.end}` : "";
|
|
2471
3052
|
const tailPart = tail ? `${tail.start}-${tail.end}` : "";
|
|
2472
|
-
if (headPart && tailPart) notice = `Showing ${headPart} and ${tailPart} of ${totalLines}; ${elidedLines.toLocaleString()} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes$
|
|
3053
|
+
if (headPart && tailPart) notice = `Showing ${headPart} and ${tailPart} of ${totalLines}; ${elidedLines.toLocaleString()} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes$1(elidedBytes)}) elided`;
|
|
2473
3054
|
else notice = `Showing ${truncation.outputLines} of ${totalLines} lines; middle elided`;
|
|
2474
3055
|
if (truncation.nextOffset != null) notice += `. Use :${truncation.nextOffset} to continue`;
|
|
2475
3056
|
if (truncation.artifactId != null) notice += `. ${formatFullOutputReference(truncation.artifactId)}`;
|
|
@@ -2480,7 +3061,7 @@ function formatTruncationMetaNotice(truncation) {
|
|
|
2480
3061
|
else notice = `Showing ${truncation.outputLines} of ${truncation.totalLines} lines`;
|
|
2481
3062
|
if (truncation.truncatedBy === "bytes") {
|
|
2482
3063
|
const maxBytes = truncation.maxBytes ?? truncation.outputBytes;
|
|
2483
|
-
notice += ` (${formatBytes$
|
|
3064
|
+
notice += ` (${formatBytes$1(maxBytes)} limit)`;
|
|
2484
3065
|
}
|
|
2485
3066
|
if (truncation.nextOffset != null) notice += `. Use :${truncation.nextOffset} to continue`;
|
|
2486
3067
|
if (truncation.artifactId != null) notice += `. ${formatFullOutputReference(truncation.artifactId)}`;
|
|
@@ -2544,9 +3125,10 @@ function formatWallTimeNotice(wallTimeMs) {
|
|
|
2544
3125
|
* middle elision, shown range for one-sided truncation) plus elided stats,
|
|
2545
3126
|
* then the spill-file pointer and a read-back hint. Range reconstruction rides
|
|
2546
3127
|
* the shared OMP algorithm (`outputMeta().truncationFromSummary`), with the
|
|
2547
|
-
* upstream `artifact
|
|
2548
|
-
*
|
|
2549
|
-
*
|
|
3128
|
+
* upstream `artifact://N` clause replaced by the spill-file path — bash spill
|
|
3129
|
+
* files are plain files (deliberately not registered in the read tool's
|
|
3130
|
+
* artifact:// space, see adapter/spill.ts) and are recovered with `:N-M`
|
|
3131
|
+
* inline selectors instead.
|
|
2550
3132
|
* @returns the notice lines, or `undefined` when output was not truncated.
|
|
2551
3133
|
*/
|
|
2552
3134
|
function formatTruncationNotice(value) {
|
|
@@ -2636,7 +3218,7 @@ const TOOL_PLUS_FIELDS = [
|
|
|
2636
3218
|
{
|
|
2637
3219
|
name: "interceptorEnabled",
|
|
2638
3220
|
kind: "boolean",
|
|
2639
|
-
default:
|
|
3221
|
+
default: true,
|
|
2640
3222
|
labelKey: "interceptorEnabled",
|
|
2641
3223
|
hintKey: "interceptorEnabledHint",
|
|
2642
3224
|
group: "behavior",
|
|
@@ -3235,7 +3817,15 @@ const DEFAULT_OUTPUT_TRUNCATE = {
|
|
|
3235
3817
|
tailLines: 100
|
|
3236
3818
|
}
|
|
3237
3819
|
};
|
|
3238
|
-
/**
|
|
3820
|
+
/**
|
|
3821
|
+
* Timing knobs (ms). DEFAULT_MAX_TIMEOUT_MS is OMP-parity (= upstream bash
|
|
3822
|
+
* per-tool max 3600s); DEFAULT_TIMEOUT_MS was deliberately relaxed from
|
|
3823
|
+
* 300_000 to 3_600_000 with the bash-plus settings card (763023e,
|
|
3824
|
+
* 2026-08-19) — NOT OMP-parity (OMP bash default is 300s), compensated by
|
|
3825
|
+
* auto-backgrounding after DEFAULT_AUTO_BACKGROUND_MS. By design there is no
|
|
3826
|
+
* upstream `tools.maxTimeout` global-cap equivalent: maxTimeoutMs always
|
|
3827
|
+
* applies. (second-impl-audit.md S-11 终局, 2026-08-29.)
|
|
3828
|
+
*/
|
|
3239
3829
|
const DEFAULT_TIMEOUT_MS = 36e5;
|
|
3240
3830
|
const DEFAULT_MAX_TIMEOUT_MS = 36e5;
|
|
3241
3831
|
const DEFAULT_AUTO_BACKGROUND_MS = 6e4;
|
|
@@ -3257,7 +3847,7 @@ const Config = z.object({
|
|
|
3257
3847
|
outputSinkTailBytes: z.number().default(fieldDefault("outputSinkTailBytes", 51200)),
|
|
3258
3848
|
outputSinkHeadBytes: z.number().default(fieldDefault("outputSinkHeadBytes", 20480)),
|
|
3259
3849
|
minimizerEnabled: z.boolean().default(fieldDefault("minimizerEnabled", true)),
|
|
3260
|
-
interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled",
|
|
3850
|
+
interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled", true)),
|
|
3261
3851
|
nonInteractiveEnv: z.boolean().default(fieldDefault("nonInteractiveEnv", true)),
|
|
3262
3852
|
snapshotEnabled: z.boolean().default(fieldDefault("snapshotEnabled", true)),
|
|
3263
3853
|
useShellCommandWrapper: z.boolean().default(fieldDefault("useShellCommandWrapper", false)),
|
|
@@ -3339,7 +3929,7 @@ function resolveConfig(config) {
|
|
|
3339
3929
|
outputSinkTailBytes: config.outputSinkTailBytes ?? fieldDefault("outputSinkTailBytes", 51200),
|
|
3340
3930
|
outputSinkHeadBytes: config.outputSinkHeadBytes ?? fieldDefault("outputSinkHeadBytes", 20480),
|
|
3341
3931
|
minimizer: resolveMinimizer(),
|
|
3342
|
-
interceptorEnabled: config.interceptorEnabled ?? fieldDefault("interceptorEnabled",
|
|
3932
|
+
interceptorEnabled: config.interceptorEnabled ?? fieldDefault("interceptorEnabled", true),
|
|
3343
3933
|
nonInteractiveEnv: config.nonInteractiveEnv ?? fieldDefault("nonInteractiveEnv", true),
|
|
3344
3934
|
snapshotEnabled: config.snapshotEnabled ?? fieldDefault("snapshotEnabled", true),
|
|
3345
3935
|
useShellCommandWrapper: config.useShellCommandWrapper ?? fieldDefault("useShellCommandWrapper", false),
|
|
@@ -3418,11 +4008,15 @@ function installBashPlusSettings(ctx, entry, onSource) {
|
|
|
3418
4008
|
//#endregion
|
|
3419
4009
|
//#region src/config/truncate.ts
|
|
3420
4010
|
/**
|
|
3421
|
-
* Config-driven output truncation for background→foreground completion text
|
|
3422
|
-
*
|
|
3423
|
-
*
|
|
3424
|
-
*
|
|
3425
|
-
*
|
|
4011
|
+
* Config-driven output truncation for background→foreground completion text.
|
|
4012
|
+
* Provenance (second-impl-audit.md S-1, 2026-08-29): the truncation
|
|
4013
|
+
* primitives (truncateHead/Middle/Tail, TruncationResult) are OMP 17.3.5
|
|
4014
|
+
* verbatim, imported directly from `../omp/session/streaming-output.ts`;
|
|
4015
|
+
* the composition layer below (trigger threshold + retention mode + notice
|
|
4016
|
+
* format) is a plugin-specific design shipped with the bash-plus settings
|
|
4017
|
+
* card (763023e, 2026-08-19) — `applyConfiguredTruncation` has no
|
|
4018
|
+
* counterpart in refs/oh-my-pi 17.3.5, so the old "ported verbatim from the
|
|
4019
|
+
* OMP bash-runtime" claim was unverifiable and has been dropped.
|
|
3426
4020
|
* @module @xiaoso/dsh-tool-plus/truncate
|
|
3427
4021
|
*/
|
|
3428
4022
|
/** Upper bound standing in for "no limit", matching the OMP helper. */
|
|
@@ -3451,7 +4045,7 @@ function applyConfiguredTruncation(text, fullOutputPath, truncate) {
|
|
|
3451
4045
|
const mode = isBytes ? truncate.bytes.mode : truncate.lines.mode;
|
|
3452
4046
|
let result;
|
|
3453
4047
|
if (isBytes) {
|
|
3454
|
-
if (mode === "head") result = truncateHead
|
|
4048
|
+
if (mode === "head") result = truncateHead(text, {
|
|
3455
4049
|
maxBytes: truncate.bytes.headBytes,
|
|
3456
4050
|
maxLines: TRUNC_NO_LIMIT
|
|
3457
4051
|
});
|
|
@@ -3465,7 +4059,7 @@ function applyConfiguredTruncation(text, fullOutputPath, truncate) {
|
|
|
3465
4059
|
maxBytes: truncate.bytes.tailBytes,
|
|
3466
4060
|
maxLines: TRUNC_NO_LIMIT
|
|
3467
4061
|
});
|
|
3468
|
-
} else if (mode === "head") result = truncateHead
|
|
4062
|
+
} else if (mode === "head") result = truncateHead(text, {
|
|
3469
4063
|
maxLines: truncate.lines.headLines,
|
|
3470
4064
|
maxBytes: TRUNC_NO_LIMIT
|
|
3471
4065
|
});
|
|
@@ -3489,10 +4083,28 @@ function applyConfiguredTruncation(text, fullOutputPath, truncate) {
|
|
|
3489
4083
|
}
|
|
3490
4084
|
//#endregion
|
|
3491
4085
|
//#region src/tools/shared/omp-prompt.ts
|
|
3492
|
-
/**
|
|
4086
|
+
/**
|
|
4087
|
+
* OMP prompt.md → DSH tool description 适配层(verbatim md 不动,适配在此)。
|
|
4088
|
+
*
|
|
4089
|
+
* OMP 的 prompts/tools/*.md 用 Handlebars 条件渲染,DSH 的 defineTool
|
|
4090
|
+
* description 是静态字符串,这里提供:
|
|
4091
|
+
* - `renderOmpPrompt`:完全委托 OMP 自家引擎(@oh-my-pi/pi-utils 的
|
|
4092
|
+
* prompt.render = 结构化 Handlebars 解析 + post-render format 后处理,
|
|
4093
|
+
* 与上游 read.ts 渲染工具描述同一条管线)。按「移植绝不允许第二套实现」
|
|
4094
|
+
* (notebook T10-1 同类教训):自写 regex 渲染器已删除——其全文懒匹配
|
|
4095
|
+
* 会跨条件边界吞并并在真值分支残留孤立 `{{/if}}`(2026-08-28 tools:sdk
|
|
4096
|
+
* "malformed prompt variable" 事故根源)。strict:false → 未传变量按 falsy
|
|
4097
|
+
* 取 else 分支;畸形模板 compile 期抛错(注册期 fail-loud)。
|
|
4098
|
+
* - `sanitizeOmpPrompt`:按工具剔除 DSH 不适用的 OMP 提法(内部协议
|
|
4099
|
+
* artifact:// ssh:// memory:// xd://、browser、scout/Task 子代理、inspect_image),
|
|
4100
|
+
* 全部剔除点记录在 step.md「提示词接入」节;md 源文件保持 verbatim 不变。
|
|
4101
|
+
*/
|
|
4102
|
+
/** 渲染 {{#if KEY}}A{{else}}B{{/if}} 与 {{#if KEY}}A{{/if}}——委托 OMP 引擎
|
|
4103
|
+
* (@oh-my-pi/pi-utils prompt.render,strict:false:未传变量按 falsy 取 else
|
|
4104
|
+
* 分支;畸形模板 compile 期抛错,注册期 fail-loud)。保留首尾 trim 对齐
|
|
4105
|
+
* 既有调用点行为。 */
|
|
3493
4106
|
function renderOmpPrompt(text, vars) {
|
|
3494
|
-
|
|
3495
|
-
return text.replace(/\{\{#if\s+([A-Za-z_]+)\}\}([\s\S]*?)\{\{else\}\}([\s\S]*?)\{\{\/if\}\}/g, (_m, key, a, b) => pick(key) ? a : b).replace(/\{\{#if\s+([A-Za-z_]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (m, key, body) => pick(key) ? body : "").trim();
|
|
4107
|
+
return render(text, { ...vars }).trim();
|
|
3496
4108
|
}
|
|
3497
4109
|
/** grep.md:internal URLs 提法 + scout 句剔除。 */
|
|
3498
4110
|
function sanitizeGrepPrompt(text) {
|
|
@@ -3550,16 +4162,19 @@ function attachOmpSessionState(session, sessionKey) {
|
|
|
3550
4162
|
if (!sessionKey) return;
|
|
3551
4163
|
const state = getOmpSessionState(sessionKey);
|
|
3552
4164
|
if (state.conflictHistory) session.conflictHistory = state.conflictHistory;
|
|
4165
|
+
if (state.fileSnapshotStore) session.fileSnapshotStore = state.fileSnapshotStore;
|
|
3553
4166
|
}
|
|
3554
4167
|
/**
|
|
3555
4168
|
* 把 ToolSession 上引擎新建/更新的状态写回共享存储(execute 结束后调用)。
|
|
3556
|
-
* OMP
|
|
3557
|
-
*
|
|
4169
|
+
* OMP 引擎会惰性新建 ConflictHistory / fileSnapshotStore 并赋到 session
|
|
4170
|
+
* 对象上(如 read.ts 经 getFileSnapshotStore 挂快照库),必须回写才能被
|
|
4171
|
+
* 下一次调用恢复。
|
|
3558
4172
|
*/
|
|
3559
4173
|
function persistOmpSessionState(sessionKey, session) {
|
|
3560
4174
|
if (!sessionKey) return;
|
|
3561
4175
|
const state = getOmpSessionState(sessionKey);
|
|
3562
4176
|
if (session.conflictHistory) state.conflictHistory = session.conflictHistory;
|
|
4177
|
+
if (session.fileSnapshotStore) state.fileSnapshotStore = session.fileSnapshotStore;
|
|
3563
4178
|
}
|
|
3564
4179
|
function regexEscape(str) {
|
|
3565
4180
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4739,8 +5354,15 @@ function validateClipboardSequence(edits, clipboard) {
|
|
|
4739
5354
|
/** Parse-result cache keyed by content hash + path; FIFO-bounded. */
|
|
4740
5355
|
const parseCache = /* @__PURE__ */ new Map();
|
|
4741
5356
|
const PARSE_CACHE_MAX = 256;
|
|
5357
|
+
/** Syntactic node boundaries outside a visible source range; FIFO-bounded. */
|
|
4742
5358
|
const boundaryCache = /* @__PURE__ */ new Map();
|
|
4743
|
-
/**
|
|
5359
|
+
/**
|
|
5360
|
+
* FNV-1a over UTF-16 code units → base36 parse-cache key. In-process only
|
|
5361
|
+
* (boundary/parse caches, FIFO ≤256): equal to the bun-shim `Bun.hash`
|
|
5362
|
+
* shim's UTF-8 FNV-1a on ASCII keys, differs on non-ASCII; no cross-runtime
|
|
5363
|
+
* or persistence contract (second-impl-audit.md S-3). Bun's native
|
|
5364
|
+
* `Bun.hash` is wyhash64 — a different function by design.
|
|
5365
|
+
*/
|
|
4744
5366
|
function hashStr(s) {
|
|
4745
5367
|
let h = 2166136261;
|
|
4746
5368
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -7283,7 +7905,7 @@ function restoreLineEndings(text, ending) {
|
|
|
7283
7905
|
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
7284
7906
|
}
|
|
7285
7907
|
/** Strip a UTF-8 BOM if present and return both the BOM and the trailing text. */
|
|
7286
|
-
function stripBom$
|
|
7908
|
+
function stripBom$1(content) {
|
|
7287
7909
|
return content.startsWith("") ? {
|
|
7288
7910
|
bom: "",
|
|
7289
7911
|
text: content.slice(1)
|
|
@@ -7798,7 +8420,7 @@ var Patcher = class {
|
|
|
7798
8420
|
try {
|
|
7799
8421
|
const read = await this.#tryRead(section.path);
|
|
7800
8422
|
if (read.exists) {
|
|
7801
|
-
const normalized = normalizeToLF(stripBom$
|
|
8423
|
+
const normalized = normalizeToLF(stripBom$1(read.rawContent).text);
|
|
7802
8424
|
span = this.blockResolver({
|
|
7803
8425
|
path: section.path,
|
|
7804
8426
|
text: normalized,
|
|
@@ -7841,7 +8463,7 @@ var Patcher = class {
|
|
|
7841
8463
|
await this.fs.preflightWrite(target.path, { fileOp });
|
|
7842
8464
|
if (!read.exists) throw new Error(`File not found: ${target.path}. Use the write tool to create new files.`);
|
|
7843
8465
|
if (fileOp?.kind === "move" && this.fs.canonicalPath(fileOp.dest) === canonicalPath) throw new Error(`MV destination is the same as ${target.path}.`);
|
|
7844
|
-
const { bom: bomFromText, text } = stripBom$
|
|
8466
|
+
const { bom: bomFromText, text } = stripBom$1(read.rawContent);
|
|
7845
8467
|
const bom = bomFromText || await this.#readBinaryBom(target.path);
|
|
7846
8468
|
const lineEnding = detectLineEnding(text);
|
|
7847
8469
|
const normalized = normalizeToLF(text);
|
|
@@ -7954,7 +8576,7 @@ var Patcher = class {
|
|
|
7954
8576
|
}
|
|
7955
8577
|
const write = await this.fs.writeText(section.path, persisted);
|
|
7956
8578
|
const op = exists ? "update" : "create";
|
|
7957
|
-
const recorded = normalizeToLF(stripBom$
|
|
8579
|
+
const recorded = normalizeToLF(stripBom$1(write.text).text);
|
|
7958
8580
|
const driftedOnWrite = recorded !== after;
|
|
7959
8581
|
const fileHash = this.#recordFullSnapshot(canonicalPath, recorded);
|
|
7960
8582
|
const allWarnings = driftedOnWrite ? [...warnings, writeDriftWarning(section.path)] : warnings;
|
|
@@ -8967,208 +9589,6 @@ function parseInternalUrl(input) {
|
|
|
8967
9589
|
//#region src/tools/omp/prompts/tools/read.md
|
|
8968
9590
|
var read_default = "Read files, directories, archives, SQLite, images, documents, internal resources, and web URLs via `path`.\n\n<instruction>\n- SHOULD parallelize independent reads.\n- SHOULD use `read` (not browser) for web content; browser only when `read` can't deliver.\n</instruction>\n\n## Selectors — append `:<sel>` to `path` (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`)\n- `:50` / `:50-` — from line 50 | `:50-200` — inclusive | `:50+150` — 150 lines from 50 | `:5-16,960-973` — multiple ranges\n- `:raw` — verbatim, no anchors/prefixes | `:2-4:raw` / `:raw:2-4` — range + verbatim\n- `:conflicts` — one line per unresolved git merge conflict block\n\n## Source kinds\n- Parseable code, no selector → structural summary (declarations only, body elided). Footer names recovery selector — re-issue ONLY those ranges.\n- {{#if IS_HL_MODE}}File + selector → `[foo.ts#1A2B]` snapshot header + numbered lines. Copy `[FILENAME#TAG]` for anchored edits; NEVER fabricate the tag.{{/if}}\n- Directory → depth-limited dirent listing.\n- SQLite (`.sqlite`, `.sqlite3`, `.db`, `.db3`): `file.db` (tables), `file.db:table` (schema+rows), `file.db:table:key` (by PK), `?limit=`/`?where=`/`?q=SELECT`.\n- Archives (`.tar`, `.tar.gz`, `.tgz`, `.zip`, plus ZIP-based `.jar`/`.war`/`.ear`/`.apk`): `archive.ext:path/inside/archive` reads a member.\n- Documents → extracted text. Notebooks → editable cells. Images → {{#if INSPECT_IMAGE_ENABLED}}metadata; call `inspect_image`{{else}}decoded inline{{/if}}. `:raw` bypasses converters.\n- URLs → reader-mode clean text/markdown; `:raw` → untouched HTML. Bare `host:port` needs trailing slash.\n- Internal URIs — all schemes take selectors. `artifact://<id>` recovers spilled output; page with `:N-M`/`:raw:N-M`.\n- `ssh://host/<path>` reads remote file/dir (UTF-8, ≤1 MiB); bare `ssh://` lists hosts; also `write`/`search`-able.\n Literal `:`, `?`, `#` → percent-encode (`%3A`/`%3F`/`%23`). Requires POSIX shell (else `ssh` tool).\n\n<critical>\nSummary footer names elided ranges? Re-issue ONLY those ranges. NEVER guess `..`/`…` content.\n</critical>\n";
|
|
8969
9591
|
//#endregion
|
|
8970
|
-
//#region src/tools/omp/session/streaming-output.ts
|
|
8971
|
-
const DEFAULT_MAX_LINES = 3e3;
|
|
8972
|
-
const DEFAULT_MAX_BYTES = 51200;
|
|
8973
|
-
const NL = "\n";
|
|
8974
|
-
/** Count newline characters via native substring search. */
|
|
8975
|
-
function countNewlines(text) {
|
|
8976
|
-
let count = 0;
|
|
8977
|
-
let pos = text.indexOf(NL);
|
|
8978
|
-
while (pos !== -1) {
|
|
8979
|
-
count++;
|
|
8980
|
-
pos = text.indexOf(NL, pos + 1);
|
|
8981
|
-
}
|
|
8982
|
-
return count;
|
|
8983
|
-
}
|
|
8984
|
-
/** Zero-copy view of a Uint8Array as a Buffer (copies only if already a Buffer). */
|
|
8985
|
-
function asBuffer(data) {
|
|
8986
|
-
return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
8987
|
-
}
|
|
8988
|
-
/** Advance past UTF-8 continuation bytes (10xxxxxx) to a leading byte. */
|
|
8989
|
-
function findUtf8BoundaryForward(buf, pos) {
|
|
8990
|
-
let i = Math.max(0, pos);
|
|
8991
|
-
while (i < buf.length && (buf[i] & 192) === 128) i++;
|
|
8992
|
-
return i;
|
|
8993
|
-
}
|
|
8994
|
-
/** Retreat past UTF-8 continuation bytes to land on a leading byte. */
|
|
8995
|
-
function findUtf8BoundaryBackward(buf, cut) {
|
|
8996
|
-
let i = Math.min(buf.length, Math.max(0, cut));
|
|
8997
|
-
if (i >= buf.length) return buf.length;
|
|
8998
|
-
while (i > 0 && (buf[i] & 192) === 128) i--;
|
|
8999
|
-
return i;
|
|
9000
|
-
}
|
|
9001
|
-
function truncateBytesWindowed(data, maxBytesRaw, mode) {
|
|
9002
|
-
const maxBytes = maxBytesRaw;
|
|
9003
|
-
if (maxBytes === 0) return {
|
|
9004
|
-
text: "",
|
|
9005
|
-
bytes: 0
|
|
9006
|
-
};
|
|
9007
|
-
if (typeof data === "string") {
|
|
9008
|
-
if (data.length <= maxBytes) {
|
|
9009
|
-
const len = Buffer.byteLength(data, "utf-8");
|
|
9010
|
-
if (len <= maxBytes) return {
|
|
9011
|
-
text: data,
|
|
9012
|
-
bytes: len
|
|
9013
|
-
};
|
|
9014
|
-
}
|
|
9015
|
-
const window = mode === "head" ? data.substring(0, Math.min(data.length, maxBytes)) : data.substring(Math.max(0, data.length - maxBytes));
|
|
9016
|
-
const buf = Buffer.from(window, "utf-8");
|
|
9017
|
-
if (mode === "head") {
|
|
9018
|
-
const end = findUtf8BoundaryBackward(buf, maxBytes);
|
|
9019
|
-
if (end <= 0) return {
|
|
9020
|
-
text: "",
|
|
9021
|
-
bytes: 0
|
|
9022
|
-
};
|
|
9023
|
-
const slice = buf.subarray(0, end);
|
|
9024
|
-
return {
|
|
9025
|
-
text: slice.toString("utf-8"),
|
|
9026
|
-
bytes: slice.length
|
|
9027
|
-
};
|
|
9028
|
-
} else {
|
|
9029
|
-
const start = findUtf8BoundaryForward(buf, Math.max(0, buf.length - maxBytes));
|
|
9030
|
-
const slice = buf.subarray(start);
|
|
9031
|
-
return {
|
|
9032
|
-
text: slice.toString("utf-8"),
|
|
9033
|
-
bytes: slice.length
|
|
9034
|
-
};
|
|
9035
|
-
}
|
|
9036
|
-
}
|
|
9037
|
-
const buf = asBuffer(data);
|
|
9038
|
-
if (buf.length <= maxBytes) return {
|
|
9039
|
-
text: buf.toString("utf-8"),
|
|
9040
|
-
bytes: buf.length
|
|
9041
|
-
};
|
|
9042
|
-
if (mode === "head") {
|
|
9043
|
-
const end = findUtf8BoundaryBackward(buf, maxBytes);
|
|
9044
|
-
if (end <= 0) return {
|
|
9045
|
-
text: "",
|
|
9046
|
-
bytes: 0
|
|
9047
|
-
};
|
|
9048
|
-
const slice = buf.subarray(0, end);
|
|
9049
|
-
return {
|
|
9050
|
-
text: slice.toString("utf-8"),
|
|
9051
|
-
bytes: slice.length
|
|
9052
|
-
};
|
|
9053
|
-
} else {
|
|
9054
|
-
const start = findUtf8BoundaryForward(buf, buf.length - maxBytes);
|
|
9055
|
-
const slice = buf.subarray(start);
|
|
9056
|
-
return {
|
|
9057
|
-
text: slice.toString("utf-8"),
|
|
9058
|
-
bytes: slice.length
|
|
9059
|
-
};
|
|
9060
|
-
}
|
|
9061
|
-
}
|
|
9062
|
-
/**
|
|
9063
|
-
* Truncate a string/buffer to fit within a byte limit, keeping the head.
|
|
9064
|
-
* Handles multi-byte UTF-8 boundaries correctly.
|
|
9065
|
-
*/
|
|
9066
|
-
function truncateHeadBytes(data, maxBytes) {
|
|
9067
|
-
return truncateBytesWindowed(data, maxBytes, "head");
|
|
9068
|
-
}
|
|
9069
|
-
/**
|
|
9070
|
-
* Truncate a single line to max characters, appending '…' if truncated.
|
|
9071
|
-
*/
|
|
9072
|
-
function truncateLine(line, maxChars = 512) {
|
|
9073
|
-
if (line.length <= maxChars) return {
|
|
9074
|
-
text: line,
|
|
9075
|
-
wasTruncated: false
|
|
9076
|
-
};
|
|
9077
|
-
return {
|
|
9078
|
-
text: `${line.slice(0, maxChars)}…`,
|
|
9079
|
-
wasTruncated: true
|
|
9080
|
-
};
|
|
9081
|
-
}
|
|
9082
|
-
/** Shared helper to build a no-truncation result. */
|
|
9083
|
-
function noTruncResult(content, totalLines, totalBytes) {
|
|
9084
|
-
if (totalLines == null) totalLines = countNewlines(content) + 1;
|
|
9085
|
-
if (totalBytes == null) totalBytes = Buffer.byteLength(content, "utf-8");
|
|
9086
|
-
return {
|
|
9087
|
-
content,
|
|
9088
|
-
totalLines,
|
|
9089
|
-
totalBytes
|
|
9090
|
-
};
|
|
9091
|
-
}
|
|
9092
|
-
/**
|
|
9093
|
-
* Truncate content from the head (keep first N lines/bytes).
|
|
9094
|
-
* Never returns partial lines. If the first line exceeds the byte limit,
|
|
9095
|
-
* returns empty content with firstLineExceedsLimit=true.
|
|
9096
|
-
*
|
|
9097
|
-
* This implementation avoids Buffer.from(content) for the whole input.
|
|
9098
|
-
* It only computes UTF-8 byteLength for candidate lines that can still fit.
|
|
9099
|
-
*/
|
|
9100
|
-
function truncateHead(content, options = {}) {
|
|
9101
|
-
const maxLines = options.maxLines ?? 3e3;
|
|
9102
|
-
const maxBytes = options.maxBytes ?? 51200;
|
|
9103
|
-
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
9104
|
-
const totalLines = countNewlines(content) + 1;
|
|
9105
|
-
if (totalLines <= maxLines && totalBytes <= maxBytes) return noTruncResult(content, totalLines, totalBytes);
|
|
9106
|
-
let includedLines = 0;
|
|
9107
|
-
let bytesUsed = 0;
|
|
9108
|
-
let cutIndex = 0;
|
|
9109
|
-
let cursor = 0;
|
|
9110
|
-
let truncatedBy = "lines";
|
|
9111
|
-
while (includedLines < maxLines) {
|
|
9112
|
-
const nl = content.indexOf(NL, cursor);
|
|
9113
|
-
const lineEnd = nl === -1 ? content.length : nl;
|
|
9114
|
-
const sepBytes = includedLines > 0 ? 1 : 0;
|
|
9115
|
-
const remaining = maxBytes - bytesUsed - sepBytes;
|
|
9116
|
-
if (remaining < 0) {
|
|
9117
|
-
truncatedBy = "bytes";
|
|
9118
|
-
break;
|
|
9119
|
-
}
|
|
9120
|
-
if (lineEnd - cursor > remaining) {
|
|
9121
|
-
truncatedBy = "bytes";
|
|
9122
|
-
if (includedLines === 0) return {
|
|
9123
|
-
content: "",
|
|
9124
|
-
truncated: true,
|
|
9125
|
-
truncatedBy: "bytes",
|
|
9126
|
-
totalLines,
|
|
9127
|
-
totalBytes,
|
|
9128
|
-
outputLines: 0,
|
|
9129
|
-
outputBytes: 0,
|
|
9130
|
-
lastLinePartial: false,
|
|
9131
|
-
firstLineExceedsLimit: true
|
|
9132
|
-
};
|
|
9133
|
-
break;
|
|
9134
|
-
}
|
|
9135
|
-
const lineText = content.slice(cursor, lineEnd);
|
|
9136
|
-
const lineBytes = Buffer.byteLength(lineText, "utf-8");
|
|
9137
|
-
if (lineBytes > remaining) {
|
|
9138
|
-
truncatedBy = "bytes";
|
|
9139
|
-
if (includedLines === 0) return {
|
|
9140
|
-
content: "",
|
|
9141
|
-
truncated: true,
|
|
9142
|
-
truncatedBy: "bytes",
|
|
9143
|
-
totalLines,
|
|
9144
|
-
totalBytes,
|
|
9145
|
-
outputLines: 0,
|
|
9146
|
-
outputBytes: 0,
|
|
9147
|
-
lastLinePartial: false,
|
|
9148
|
-
firstLineExceedsLimit: true
|
|
9149
|
-
};
|
|
9150
|
-
break;
|
|
9151
|
-
}
|
|
9152
|
-
bytesUsed += sepBytes + lineBytes;
|
|
9153
|
-
includedLines++;
|
|
9154
|
-
cutIndex = nl === -1 ? content.length : nl;
|
|
9155
|
-
if (nl === -1) break;
|
|
9156
|
-
cursor = nl + 1;
|
|
9157
|
-
}
|
|
9158
|
-
if (includedLines >= maxLines && bytesUsed <= maxBytes) truncatedBy = "lines";
|
|
9159
|
-
return {
|
|
9160
|
-
content: content.slice(0, cutIndex),
|
|
9161
|
-
truncated: true,
|
|
9162
|
-
truncatedBy,
|
|
9163
|
-
totalLines,
|
|
9164
|
-
totalBytes,
|
|
9165
|
-
outputLines: includedLines,
|
|
9166
|
-
outputBytes: bytesUsed,
|
|
9167
|
-
lastLinePartial: false,
|
|
9168
|
-
firstLineExceedsLimit: false
|
|
9169
|
-
};
|
|
9170
|
-
}
|
|
9171
|
-
//#endregion
|
|
9172
9592
|
//#region src/tools/omp/utils/block-context.ts
|
|
9173
9593
|
const OPEN_TO_CLOSE = {
|
|
9174
9594
|
"(": ")",
|
|
@@ -10256,7 +10676,7 @@ function renderNode(node, formatNodeAge, out) {
|
|
|
10256
10676
|
label: `${indent}- ${node.name}${suffix}`,
|
|
10257
10677
|
depth: node.depth,
|
|
10258
10678
|
isRoot: false,
|
|
10259
|
-
size: node.isDir ? void 0 : formatBytes$
|
|
10679
|
+
size: node.isDir ? void 0 : formatBytes$1(node.size),
|
|
10260
10680
|
age: formatNodeAge(node.mtimeMs)
|
|
10261
10681
|
});
|
|
10262
10682
|
}
|
|
@@ -10511,18 +10931,18 @@ function parseConflictUri(raw) {
|
|
|
10511
10931
|
const idPart = slashIdx === -1 ? tail : tail.slice(0, slashIdx);
|
|
10512
10932
|
const scopePart = slashIdx === -1 ? void 0 : tail.slice(slashIdx + 1);
|
|
10513
10933
|
if (idPart === "*") {
|
|
10514
|
-
if (scopePart !== void 0) throw new ToolError
|
|
10934
|
+
if (scopePart !== void 0) throw new ToolError(`Invalid conflict URI '${raw}': wildcard 'conflict://*' does not accept a scope segment. Drop '/${scopePart}' or use a numeric id.`);
|
|
10515
10935
|
return recoveredPrefix !== void 0 ? {
|
|
10516
10936
|
id: "*",
|
|
10517
10937
|
recoveredPrefix
|
|
10518
10938
|
} : { id: "*" };
|
|
10519
10939
|
}
|
|
10520
|
-
if (!/^\d+$/.test(idPart)) throw new ToolError
|
|
10940
|
+
if (!/^\d+$/.test(idPart)) throw new ToolError(`Invalid conflict URI '${raw}': must be 'conflict://<N>', 'conflict://<N>/<scope>', or 'conflict://*' where N is a positive integer surfaced by a prior \`read\`.`);
|
|
10521
10941
|
const id = Number.parseInt(idPart, 10);
|
|
10522
|
-
if (!Number.isFinite(id) || id < 1) throw new ToolError
|
|
10942
|
+
if (!Number.isFinite(id) || id < 1) throw new ToolError(`Invalid conflict URI '${raw}': id must be ≥ 1.`);
|
|
10523
10943
|
let scope;
|
|
10524
10944
|
if (scopePart !== void 0) {
|
|
10525
|
-
if (!CONFLICT_SCOPES.has(scopePart)) throw new ToolError
|
|
10945
|
+
if (!CONFLICT_SCOPES.has(scopePart)) throw new ToolError(`Invalid conflict URI '${raw}': scope must be one of 'ours', 'theirs', 'base', or omitted (e.g. 'conflict://${id}/theirs').`);
|
|
10526
10946
|
scope = scopePart;
|
|
10527
10947
|
}
|
|
10528
10948
|
return recoveredPrefix !== void 0 ? {
|
|
@@ -10555,7 +10975,7 @@ function parseConflictUri(raw) {
|
|
|
10555
10975
|
function spliceConflict(originalText, entry, replacement) {
|
|
10556
10976
|
const lines = originalText.split("\n");
|
|
10557
10977
|
const match = locateRegion(lines, buildRecordedRegion(entry), entry.startLine - 1);
|
|
10558
|
-
if (!match) throw new ToolError
|
|
10978
|
+
if (!match) throw new ToolError(`Conflict #${entry.id} no longer present in '${entry.displayPath}': the recorded marker block can't be located. The file changed since the conflict was registered — re-read it to re-register conflicts.`);
|
|
10559
10979
|
let replacementLines = normalizeTrailingNewline(replacement).split("\n").map(stripTrailingCr);
|
|
10560
10980
|
const echo = trimBoundaryEcho(replacementLines, lines, match, entry);
|
|
10561
10981
|
replacementLines = echo.lines;
|
|
@@ -10732,7 +11152,7 @@ function expandContentTokens(content, entry) {
|
|
|
10732
11152
|
out.push(...entry.theirsLines);
|
|
10733
11153
|
break;
|
|
10734
11154
|
case "@base":
|
|
10735
|
-
if (!entry.baseLines) throw new ToolError
|
|
11155
|
+
if (!entry.baseLines) throw new ToolError(`Conflict #${entry.id} has no base section (2-way merge). \`@base\` is only valid for diff3 conflicts.`);
|
|
10736
11156
|
out.push(...entry.baseLines);
|
|
10737
11157
|
break;
|
|
10738
11158
|
case "@both":
|
|
@@ -10770,7 +11190,7 @@ function renderConflictRegion(entry, scope) {
|
|
|
10770
11190
|
startLine: entry.separatorLine + 1
|
|
10771
11191
|
};
|
|
10772
11192
|
if (scope === "base") {
|
|
10773
|
-
if (entry.baseLines === void 0 || entry.baseLine === void 0) throw new ToolError
|
|
11193
|
+
if (entry.baseLines === void 0 || entry.baseLine === void 0) throw new ToolError(`Conflict #${entry.id} has no base section (2-way merge). 'conflict://${entry.id}/base' is only valid for diff3 conflicts.`);
|
|
10774
11194
|
return {
|
|
10775
11195
|
lines: [...entry.baseLines],
|
|
10776
11196
|
startLine: entry.baseLine + 1
|
|
@@ -11205,16 +11625,16 @@ function parseLineRangeChunk(sel) {
|
|
|
11205
11625
|
const lineMatch = LINE_RANGE_CHUNK_RE.exec(sel);
|
|
11206
11626
|
if (!lineMatch) return null;
|
|
11207
11627
|
const rawStart = Number.parseInt(lineMatch[1], 10);
|
|
11208
|
-
if (rawStart < 1) throw new ToolError
|
|
11628
|
+
if (rawStart < 1) throw new ToolError("Line selector 0 is invalid; lines are 1-indexed. Use :1.");
|
|
11209
11629
|
const sep = lineMatch[2] === ".." ? "-" : lineMatch[2];
|
|
11210
11630
|
const rhs = lineMatch[3] ? Number.parseInt(lineMatch[3], 10) : void 0;
|
|
11211
11631
|
let rawEnd;
|
|
11212
11632
|
if (sep === "+") {
|
|
11213
|
-
if (rhs === void 0 || rhs < 1) throw new ToolError
|
|
11633
|
+
if (rhs === void 0 || rhs < 1) throw new ToolError(`Invalid range ${rawStart}+${rhs ?? 0}: count must be >= 1.`);
|
|
11214
11634
|
rawEnd = rawStart + rhs - 1;
|
|
11215
11635
|
} else if (sep === "-") {
|
|
11216
11636
|
if (rhs !== void 0) {
|
|
11217
|
-
if (rhs < rawStart) throw new ToolError
|
|
11637
|
+
if (rhs < rawStart) throw new ToolError(`Invalid range ${rawStart}-${rhs}: end must be >= start.`);
|
|
11218
11638
|
rawEnd = rhs;
|
|
11219
11639
|
}
|
|
11220
11640
|
}
|
|
@@ -11387,7 +11807,7 @@ function peelWriteUrlSelector(rawPath) {
|
|
|
11387
11807
|
const { path, sel } = splitInternalUrlSel(rawPath);
|
|
11388
11808
|
if (sel === void 0) return rawPath;
|
|
11389
11809
|
if (/^(?:raw|conflicts)$/i.test(sel)) return path;
|
|
11390
|
-
throw new ToolError
|
|
11810
|
+
throw new ToolError(`write does not accept the trailing selector ":${sel}" — it writes a whole file. Remove ":${sel}", or if the filename truly ends with it, percent-encode the ":" as %3A.`);
|
|
11391
11811
|
}
|
|
11392
11812
|
function assertNotInternalUrl(expanded, original) {
|
|
11393
11813
|
for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) if (expanded.startsWith(prefix)) throw new Error(`Path "${original}" uses internal scheme "${prefix}" and must be resolved through the proper protocol handler, not as a filesystem path.`);
|
|
@@ -11869,9 +12289,9 @@ async function findUniqueWorkspaceSuffix(rawPath, cwd, signal) {
|
|
|
11869
12289
|
async function resolveToolSearchScope(opts) {
|
|
11870
12290
|
const { rawPaths: inputs, cwd, internalUrlAction } = opts;
|
|
11871
12291
|
const normalizedRawPaths = inputs.map(normalizePathLikeInput);
|
|
11872
|
-
if (normalizedRawPaths.some((rawPath) => rawPath.length === 0)) throw new ToolError
|
|
12292
|
+
if (normalizedRawPaths.some((rawPath) => rawPath.length === 0)) throw new ToolError("Search scope entries must be non-empty paths or globs");
|
|
11873
12293
|
const rawPaths = await expandDelimitedPathEntries(normalizedRawPaths, cwd);
|
|
11874
|
-
if (rawPaths.some((rawPath) => rawPath.length === 0)) throw new ToolError
|
|
12294
|
+
if (rawPaths.some((rawPath) => rawPath.length === 0)) throw new ToolError("Search scope entries must be non-empty paths or globs");
|
|
11875
12295
|
const strictExternalUrlRe = /^(?:https?|ftp|ws|wss):\/\//i;
|
|
11876
12296
|
const internalRouter = InternalUrlRouter.instance();
|
|
11877
12297
|
const resolvedPathInputs = [];
|
|
@@ -11890,14 +12310,14 @@ async function resolveToolSearchScope(opts) {
|
|
|
11890
12310
|
if (opts.trackImmutableSources && resolved.immutable) immutableSourcePaths.add(path.resolve(resolved.sourcePath));
|
|
11891
12311
|
continue;
|
|
11892
12312
|
}
|
|
11893
|
-
throw new ToolError
|
|
12313
|
+
throw new ToolError(`Cannot ${internalUrlAction} external URL: ${rawPath}. Use \`read\` to fetch web content, then search the returned text.`);
|
|
11894
12314
|
}
|
|
11895
12315
|
if (!internalRouter.canHandle(rawPath)) {
|
|
11896
12316
|
resolvedPathInputs.push(rawPath);
|
|
11897
12317
|
continue;
|
|
11898
12318
|
}
|
|
11899
|
-
if (isSshUrl(rawPath)) throw new ToolError
|
|
11900
|
-
if (hasGlobPathChars(rawPath)) throw new ToolError
|
|
12319
|
+
if (isSshUrl(rawPath)) throw new ToolError(`Cannot ${internalUrlAction} a remote ssh:// path (no local file): ${rawPath}. Use \`read ${rawPath}\` to view it, or the \`search\` tool to grep remote files.`);
|
|
12320
|
+
if (hasGlobPathChars(rawPath)) throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
11901
12321
|
const resource = await internalRouter.resolve(rawPath, {
|
|
11902
12322
|
cwd,
|
|
11903
12323
|
settings: opts.settings,
|
|
@@ -11906,7 +12326,7 @@ async function resolveToolSearchScope(opts) {
|
|
|
11906
12326
|
skills: opts.skills,
|
|
11907
12327
|
pathOnly: true
|
|
11908
12328
|
});
|
|
11909
|
-
if (!resource.sourcePath) throw new ToolError
|
|
12329
|
+
if (!resource.sourcePath) throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
|
|
11910
12330
|
if (opts.trackImmutableSources && resource.immutable) immutableSourcePaths.add(path.resolve(resource.sourcePath));
|
|
11911
12331
|
resolvedPathInputs.push(resource.sourcePath);
|
|
11912
12332
|
}
|
|
@@ -11914,7 +12334,7 @@ async function resolveToolSearchScope(opts) {
|
|
|
11914
12334
|
let effectivePaths = resolvedPathInputs;
|
|
11915
12335
|
if (resolvedPathInputs.length > 1) {
|
|
11916
12336
|
const partition = await partitionExistingPaths(resolvedPathInputs, cwd, parseSearchPath);
|
|
11917
|
-
if (partition.valid.length === 0) throw new ToolError
|
|
12337
|
+
if (partition.valid.length === 0) throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
11918
12338
|
effectivePaths = partition.valid;
|
|
11919
12339
|
missingPaths = partition.missing;
|
|
11920
12340
|
}
|
|
@@ -11930,7 +12350,7 @@ async function resolveToolSearchScope(opts) {
|
|
|
11930
12350
|
scopePath = formatPathRelativeToCwd(searchPath, cwd);
|
|
11931
12351
|
} else {
|
|
11932
12352
|
const multiSearchPath = await resolveExplicitSearchPaths(effectivePaths, cwd, void 0, opts.fanOutFileTargets === true);
|
|
11933
|
-
if (!multiSearchPath) throw new ToolError
|
|
12353
|
+
if (!multiSearchPath) throw new ToolError("`paths` must contain at least one path or glob");
|
|
11934
12354
|
searchPath = multiSearchPath.basePath;
|
|
11935
12355
|
multiTargets = multiSearchPath.targets;
|
|
11936
12356
|
if (opts.surfaceExactFilePaths) {
|
|
@@ -11944,7 +12364,7 @@ async function resolveToolSearchScope(opts) {
|
|
|
11944
12364
|
isDirectory = (await Bun.file(searchPath).stat()).isDirectory();
|
|
11945
12365
|
} catch {
|
|
11946
12366
|
const hint = opts.multipathStatHint && rawPaths.length > 1 ? opts.multipathStatHint : "";
|
|
11947
|
-
throw new ToolError
|
|
12367
|
+
throw new ToolError(`Path not found: ${scopePath}${hint}`);
|
|
11948
12368
|
}
|
|
11949
12369
|
return {
|
|
11950
12370
|
searchPath,
|
|
@@ -12270,7 +12690,7 @@ var ImageInputTooLargeError = class extends Error {
|
|
|
12270
12690
|
bytes;
|
|
12271
12691
|
maxBytes;
|
|
12272
12692
|
constructor(bytes, maxBytes) {
|
|
12273
|
-
super(`Image file too large: ${formatBytes$
|
|
12693
|
+
super(`Image file too large: ${formatBytes$1(bytes)} exceeds ${formatBytes$1(maxBytes)} limit.`);
|
|
12274
12694
|
this.name = "ImageInputTooLargeError";
|
|
12275
12695
|
this.bytes = bytes;
|
|
12276
12696
|
this.maxBytes = maxBytes;
|
|
@@ -12531,7 +12951,7 @@ function stringifySqliteValue(value) {
|
|
|
12531
12951
|
if (value === void 0) return "";
|
|
12532
12952
|
if (typeof value === "string") return value;
|
|
12533
12953
|
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
12534
|
-
if (value instanceof Uint8Array) return `<BLOB ${formatBytes$
|
|
12954
|
+
if (value instanceof Uint8Array) return `<BLOB ${formatBytes$1(value.byteLength)}>`;
|
|
12535
12955
|
try {
|
|
12536
12956
|
return JSON.stringify(value) ?? String(value);
|
|
12537
12957
|
} catch {
|
|
@@ -12611,18 +13031,18 @@ function buildAsciiTable(columns, rows) {
|
|
|
12611
13031
|
function parseLimit(value, fallback) {
|
|
12612
13032
|
if (value === null || value.trim().length === 0) return fallback;
|
|
12613
13033
|
const parsed = Number.parseInt(value, 10);
|
|
12614
|
-
if (!Number.isFinite(parsed) || parsed < 1) throw new ToolError
|
|
13034
|
+
if (!Number.isFinite(parsed) || parsed < 1) throw new ToolError(`SQLite limit must be a positive integer; got '${value}'`);
|
|
12615
13035
|
return Math.min(parsed, MAX_QUERY_LIMIT);
|
|
12616
13036
|
}
|
|
12617
13037
|
function parseOffset(value) {
|
|
12618
13038
|
if (value === null || value.trim().length === 0) return 0;
|
|
12619
13039
|
const parsed = Number.parseInt(value, 10);
|
|
12620
|
-
if (!Number.isFinite(parsed) || parsed < 0) throw new ToolError
|
|
13040
|
+
if (!Number.isFinite(parsed) || parsed < 0) throw new ToolError(`SQLite offset must be a non-negative integer; got '${value}'`);
|
|
12621
13041
|
return parsed;
|
|
12622
13042
|
}
|
|
12623
13043
|
function getTableMasterRow(db, table) {
|
|
12624
13044
|
const row = db.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name = ?").get(table) ?? null;
|
|
12625
|
-
if (!row) throw new ToolError
|
|
13045
|
+
if (!row) throw new ToolError(`SQLite table '${table}' not found`);
|
|
12626
13046
|
return row;
|
|
12627
13047
|
}
|
|
12628
13048
|
function getTableInfoRows(db, table) {
|
|
@@ -12637,7 +13057,7 @@ function getPrimaryKeyColumns(db, table) {
|
|
|
12637
13057
|
}
|
|
12638
13058
|
function coerceIntegerKey(key, label) {
|
|
12639
13059
|
const trimmed = key.trim();
|
|
12640
|
-
if (!/^-?\d+$/.test(trimmed)) throw new ToolError
|
|
13060
|
+
if (!/^-?\d+$/.test(trimmed)) throw new ToolError(`${label} must be an integer; got '${key}'`);
|
|
12641
13061
|
const asNumber = Number.parseInt(trimmed, 10);
|
|
12642
13062
|
if (Number.isSafeInteger(asNumber)) return asNumber;
|
|
12643
13063
|
return BigInt(trimmed);
|
|
@@ -12658,8 +13078,8 @@ function resolveOrderClause(order, columns) {
|
|
|
12658
13078
|
const separatorIndex = trimmed.lastIndexOf(":");
|
|
12659
13079
|
const column = separatorIndex === -1 ? trimmed : trimmed.slice(0, separatorIndex);
|
|
12660
13080
|
const direction = separatorIndex === -1 ? "asc" : trimmed.slice(separatorIndex + 1).trim().toLowerCase();
|
|
12661
|
-
if (!columns.includes(column)) throw new ToolError
|
|
12662
|
-
if (direction !== "asc" && direction !== "desc") throw new ToolError
|
|
13081
|
+
if (!columns.includes(column)) throw new ToolError(`SQLite order column '${column}' not found in table schema`);
|
|
13082
|
+
if (direction !== "asc" && direction !== "desc") throw new ToolError(`SQLite order direction must be 'asc' or 'desc'; got '${direction}'`);
|
|
12663
13083
|
return ` ORDER BY ${quoteSqliteIdentifier(column)} ${direction.toUpperCase()}`;
|
|
12664
13084
|
}
|
|
12665
13085
|
const FORBIDDEN_WHERE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
@@ -12741,18 +13161,18 @@ function validateWhereClause(where) {
|
|
|
12741
13161
|
const trimmed = where.trim();
|
|
12742
13162
|
if (!trimmed) return void 0;
|
|
12743
13163
|
const violation = findWhereClauseViolation(trimmed);
|
|
12744
|
-
if (violation) throw new ToolError
|
|
13164
|
+
if (violation) throw new ToolError(violation);
|
|
12745
13165
|
return trimmed;
|
|
12746
13166
|
}
|
|
12747
13167
|
function normalizeWriteValue(value, column) {
|
|
12748
13168
|
if (value === null) return null;
|
|
12749
13169
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value;
|
|
12750
|
-
throw new ToolError
|
|
13170
|
+
throw new ToolError(`SQLite column '${column}' only accepts JSON scalar values or null`);
|
|
12751
13171
|
}
|
|
12752
13172
|
function validateWriteColumns(db, table, data) {
|
|
12753
13173
|
const columns = new Set(getTableColumns(db, table));
|
|
12754
13174
|
return Object.entries(data).map(([column, value]) => {
|
|
12755
|
-
if (!columns.has(column)) throw new ToolError
|
|
13175
|
+
if (!columns.has(column)) throw new ToolError(`SQLite table '${table}' has no column named '${column}'`);
|
|
12756
13176
|
return [column, normalizeWriteValue(value, column)];
|
|
12757
13177
|
});
|
|
12758
13178
|
}
|
|
@@ -12791,23 +13211,23 @@ function parseSqliteSelector(subPath, queryString) {
|
|
|
12791
13211
|
const rawQuery = params.get("q");
|
|
12792
13212
|
if (rawQuery !== null) {
|
|
12793
13213
|
const otherKeys = [...params.keys()].filter((key) => key !== "q");
|
|
12794
|
-
if (normalizedSubPath || otherKeys.length > 0) throw new ToolError
|
|
12795
|
-
if (!rawQuery.trim()) throw new ToolError
|
|
13214
|
+
if (normalizedSubPath || otherKeys.length > 0) throw new ToolError("SQLite raw queries cannot be combined with table selectors or pagination");
|
|
13215
|
+
if (!rawQuery.trim()) throw new ToolError("SQLite query parameter 'q' cannot be empty");
|
|
12796
13216
|
return {
|
|
12797
13217
|
kind: "raw",
|
|
12798
13218
|
sql: rawQuery
|
|
12799
13219
|
};
|
|
12800
13220
|
}
|
|
12801
13221
|
if (!normalizedSubPath) {
|
|
12802
|
-
if (params.size > 0) throw new ToolError
|
|
13222
|
+
if (params.size > 0) throw new ToolError("SQLite query parameters require a table selector or q=SELECT...");
|
|
12803
13223
|
return { kind: "list" };
|
|
12804
13224
|
}
|
|
12805
13225
|
const separatorIndex = normalizedSubPath.indexOf(":");
|
|
12806
13226
|
const table = separatorIndex === -1 ? normalizedSubPath : normalizedSubPath.slice(0, separatorIndex);
|
|
12807
13227
|
const key = separatorIndex === -1 ? void 0 : normalizedSubPath.slice(separatorIndex + 1);
|
|
12808
|
-
if (!table) throw new ToolError
|
|
13228
|
+
if (!table) throw new ToolError("SQLite selectors must include a table name");
|
|
12809
13229
|
if (key !== void 0 && key.length > 0) {
|
|
12810
|
-
if (params.size > 0) throw new ToolError
|
|
13230
|
+
if (params.size > 0) throw new ToolError("SQLite row lookups cannot be combined with query parameters");
|
|
12811
13231
|
return {
|
|
12812
13232
|
kind: "row",
|
|
12813
13233
|
table,
|
|
@@ -12823,7 +13243,7 @@ function parseSqliteSelector(subPath, queryString) {
|
|
|
12823
13243
|
"order",
|
|
12824
13244
|
"where"
|
|
12825
13245
|
]);
|
|
12826
|
-
for (const keyName of params.keys()) if (!knownKeys.has(keyName)) throw new ToolError
|
|
13246
|
+
for (const keyName of params.keys()) if (!knownKeys.has(keyName)) throw new ToolError(`Unsupported SQLite query parameter '${keyName}'`);
|
|
12827
13247
|
return {
|
|
12828
13248
|
kind: "query",
|
|
12829
13249
|
table,
|
|
@@ -12833,7 +13253,7 @@ function parseSqliteSelector(subPath, queryString) {
|
|
|
12833
13253
|
where
|
|
12834
13254
|
};
|
|
12835
13255
|
}
|
|
12836
|
-
if (params.size > 0) for (const keyName of params.keys()) throw new ToolError
|
|
13256
|
+
if (params.size > 0) for (const keyName of params.keys()) throw new ToolError(`Unsupported SQLite query parameter '${keyName}'`);
|
|
12837
13257
|
return {
|
|
12838
13258
|
kind: "schema",
|
|
12839
13259
|
table,
|
|
@@ -12893,7 +13313,7 @@ function listTables(db, options = {}) {
|
|
|
12893
13313
|
}
|
|
12894
13314
|
function getTableSchema(db, table) {
|
|
12895
13315
|
const row = getTableMasterRow(db, table);
|
|
12896
|
-
if (!row.sql) throw new ToolError
|
|
13316
|
+
if (!row.sql) throw new ToolError(`SQLite schema for table '${table}' is unavailable`);
|
|
12897
13317
|
return row.sql;
|
|
12898
13318
|
}
|
|
12899
13319
|
function resolveTableRowLookup(db, table) {
|
|
@@ -12906,9 +13326,9 @@ function resolveTableRowLookup(db, table) {
|
|
|
12906
13326
|
type: column.type
|
|
12907
13327
|
};
|
|
12908
13328
|
}
|
|
12909
|
-
if (primaryKeyColumns.length > 1) throw new ToolError
|
|
13329
|
+
if (primaryKeyColumns.length > 1) throw new ToolError(`SQLite table '${table}' has a composite primary key; use '?where=' instead`);
|
|
12910
13330
|
const schema = getTableSchema(db, table);
|
|
12911
|
-
if (/\bWITHOUT\s+ROWID\b/i.test(schema)) throw new ToolError
|
|
13331
|
+
if (/\bWITHOUT\s+ROWID\b/i.test(schema)) throw new ToolError(`SQLite table '${table}' does not expose ROWID; use '?where=' instead`);
|
|
12912
13332
|
return { kind: "rowid" };
|
|
12913
13333
|
}
|
|
12914
13334
|
function queryRows(db, table, opts) {
|
|
@@ -12920,7 +13340,7 @@ function queryRows(db, table, opts) {
|
|
|
12920
13340
|
const selectSql = `SELECT * FROM ${quoteSqliteIdentifier(table)}${whereClause}${orderClause} LIMIT ? OFFSET ?`;
|
|
12921
13341
|
const totalCount = db.prepare(countSql).get()?.count ?? 0;
|
|
12922
13342
|
const statement = db.prepare(selectSql);
|
|
12923
|
-
if (statement.paramsCount !== 2) throw new ToolError
|
|
13343
|
+
if (statement.paramsCount !== 2) throw new ToolError("SQLite where clause changed the expected pagination parameters; use q=SELECT ... for raw SQL");
|
|
12924
13344
|
return {
|
|
12925
13345
|
columns,
|
|
12926
13346
|
rows: statement.all(opts.limit, opts.offset),
|
|
@@ -12940,7 +13360,7 @@ function getRowByRowId(db, table, key) {
|
|
|
12940
13360
|
}
|
|
12941
13361
|
function executeReadQuery(db, sql) {
|
|
12942
13362
|
const statement = db.prepare(sql);
|
|
12943
|
-
if (statement.paramsCount > 0) throw new ToolError
|
|
13363
|
+
if (statement.paramsCount > 0) throw new ToolError("SQLite raw queries do not support bound parameters");
|
|
12944
13364
|
const columns = [...statement.columnNames];
|
|
12945
13365
|
const rows = [];
|
|
12946
13366
|
let truncated = false;
|
|
@@ -12972,7 +13392,7 @@ function insertRow(db, table, data) {
|
|
|
12972
13392
|
function updateRowByKey(db, table, pk, key, data) {
|
|
12973
13393
|
getTableMasterRow(db, table);
|
|
12974
13394
|
const entries = validateWriteColumns(db, table, data);
|
|
12975
|
-
if (entries.length === 0) throw new ToolError
|
|
13395
|
+
if (entries.length === 0) throw new ToolError("SQLite updates require at least one column value");
|
|
12976
13396
|
const assignments = entries.map(([column]) => `${quoteSqliteIdentifier(column)} = ?`).join(", ");
|
|
12977
13397
|
const bindings = entries.map(([, value]) => value);
|
|
12978
13398
|
bindings.push(coerceLookupValue(key, pk.type ?? ""));
|
|
@@ -12981,7 +13401,7 @@ function updateRowByKey(db, table, pk, key, data) {
|
|
|
12981
13401
|
function updateRowByRowId(db, table, key, data) {
|
|
12982
13402
|
getTableMasterRow(db, table);
|
|
12983
13403
|
const entries = validateWriteColumns(db, table, data);
|
|
12984
|
-
if (entries.length === 0) throw new ToolError
|
|
13404
|
+
if (entries.length === 0) throw new ToolError("SQLite updates require at least one column value");
|
|
12985
13405
|
const assignments = entries.map(([column]) => `${quoteSqliteIdentifier(column)} = ?`).join(", ");
|
|
12986
13406
|
const bindings = entries.map(([, value]) => value);
|
|
12987
13407
|
bindings.push(coerceIntegerKey(key, "SQLite ROWID"));
|
|
@@ -13283,9 +13703,9 @@ function parseReadUrlTarget(readPath) {
|
|
|
13283
13703
|
raw = true;
|
|
13284
13704
|
continue;
|
|
13285
13705
|
}
|
|
13286
|
-
if (ranges !== void 0) throw new ToolError
|
|
13706
|
+
if (ranges !== void 0) throw new ToolError(`URL selector has multiple range groups; combine them with commas (e.g. \`:5-10,20-30\`).`);
|
|
13287
13707
|
const parsed = parseLineRanges(sel);
|
|
13288
|
-
if (parsed === null) throw new ToolError
|
|
13708
|
+
if (parsed === null) throw new ToolError(`Invalid URL line selector: ${sel}`);
|
|
13289
13709
|
ranges = parsed;
|
|
13290
13710
|
}
|
|
13291
13711
|
if (!ranges || ranges.length === 0) return {
|
|
@@ -13765,7 +14185,7 @@ function binaryContentType(mime) {
|
|
|
13765
14185
|
return mime || "application/octet-stream";
|
|
13766
14186
|
}
|
|
13767
14187
|
function buildBinaryNotice(finalUrl, mime, byteLength) {
|
|
13768
|
-
const size = byteLength === void 0 ? "unknown size" : formatBytes$
|
|
14188
|
+
const size = byteLength === void 0 ? "unknown size" : formatBytes$1(byteLength);
|
|
13769
14189
|
return `[Binary content: ${binaryContentType(mime)}, ${size}] ${finalUrl}`;
|
|
13770
14190
|
}
|
|
13771
14191
|
function buildBinaryPayloadResult(url, finalUrl, mime, method, content, fetchedAt, notes) {
|
|
@@ -13856,7 +14276,7 @@ let specialHandlersPromise;
|
|
|
13856
14276
|
* requires a special handler, so we keep them out of the cold-startup graph.
|
|
13857
14277
|
*/
|
|
13858
14278
|
function loadSpecialHandlers() {
|
|
13859
|
-
specialHandlersPromise ??= import("./scrapers-
|
|
14279
|
+
specialHandlersPromise ??= import("./scrapers-nhQAvc8d.mjs").then((m) => m.specialHandlers);
|
|
13860
14280
|
return specialHandlersPromise;
|
|
13861
14281
|
}
|
|
13862
14282
|
/**
|
|
@@ -13910,7 +14330,7 @@ async function renderUrl(url, timeout, raw, settings, signal, storage, fetchOver
|
|
|
13910
14330
|
notes: [response.status ? `Failed to fetch URL (HTTP ${response.status})` : "Failed to fetch URL", ...response.error ? [`Cause: ${response.error}`] : []]
|
|
13911
14331
|
};
|
|
13912
14332
|
const { finalUrl, content: rawContent } = response;
|
|
13913
|
-
if (response.truncated) notes.push(`Response body exceeded ${formatBytes$
|
|
14333
|
+
if (response.truncated) notes.push(`Response body exceeded ${formatBytes$1(MAX_BYTES)} and was cut mid-stream; content is incomplete`);
|
|
13914
14334
|
const mime = normalizeMime(response.contentType);
|
|
13915
14335
|
const extHint = getExtensionHint(finalUrl);
|
|
13916
14336
|
const imageMimeType = resolveImageMimeType(mime, extHint);
|
|
@@ -14357,7 +14777,7 @@ function readUrlContentExtension(finalUrl) {
|
|
|
14357
14777
|
}
|
|
14358
14778
|
async function materializeReadUrlContent(session, entry, raw) {
|
|
14359
14779
|
const root = session.getArtifactsDir?.();
|
|
14360
|
-
if (!root) throw new ToolError
|
|
14780
|
+
if (!root) throw new ToolError("Cannot search URL output because this session cannot materialize read artifacts.");
|
|
14361
14781
|
const dir = path.join(root, "url-search");
|
|
14362
14782
|
await fs_promises_shim_exports.mkdir(dir, { recursive: true });
|
|
14363
14783
|
const hash = Bun.hash(`${raw ? "raw" : "rendered"}:${entry.details.finalUrl}`).toString(36);
|
|
@@ -14394,7 +14814,7 @@ async function fetchReadUrl(session, params, signal, options) {
|
|
|
14394
14814
|
}
|
|
14395
14815
|
/** Materialize rendered URL body text to a local file for tools that require filesystem paths. */
|
|
14396
14816
|
async function materializeReadUrlToFile(session, params, signal) {
|
|
14397
|
-
if (!session.settings.get("fetch.enabled")) throw new ToolError
|
|
14817
|
+
if (!session.settings.get("fetch.enabled")) throw new ToolError("URL reads are disabled by settings.");
|
|
14398
14818
|
const entry = await fetchReadUrl(session, params, signal);
|
|
14399
14819
|
return {
|
|
14400
14820
|
path: await materializeReadUrlContent(session, entry, params.raw ?? false),
|
|
@@ -14722,7 +15142,7 @@ function recordFullHashlineContext(session, absolutePath, displayPath, fullText)
|
|
|
14722
15142
|
async function readHashlineHeaderContext(session, absolutePath, cwd) {
|
|
14723
15143
|
const fullText = await Bun.file(absolutePath).text();
|
|
14724
15144
|
const context = recordFullHashlineContext(session, absolutePath, formatPathRelativeToCwd(absolutePath, cwd), fullText);
|
|
14725
|
-
if (!context) throw new ToolError
|
|
15145
|
+
if (!context) throw new ToolError(`Cannot record hashline snapshot for non-absolute path: ${absolutePath}`);
|
|
14726
15146
|
return context;
|
|
14727
15147
|
}
|
|
14728
15148
|
function hashlineHeaderContext(displayPath, tag) {
|
|
@@ -14913,9 +15333,9 @@ function buildInMemoryTextResult(session, text, offset, limit, options) {
|
|
|
14913
15333
|
const firstLine = allLines[startLine] ?? "";
|
|
14914
15334
|
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
14915
15335
|
const snippet = truncateHeadBytes(firstLine, DEFAULT_MAX_BYTES);
|
|
14916
|
-
if (shouldAddHashLines) outputText = `[Line ${startLineDisplay} is ${formatBytes$
|
|
15336
|
+
if (shouldAddHashLines) outputText = `[Line ${startLineDisplay} is ${formatBytes$1(firstLineBytes)}, exceeds ${formatBytes$1(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
|
|
14917
15337
|
else outputText = formatText(snippet.text, startLineDisplay);
|
|
14918
|
-
if (snippet.text.length === 0) outputText = `[Line ${startLineDisplay} is ${formatBytes$
|
|
15338
|
+
if (snippet.text.length === 0) outputText = `[Line ${startLineDisplay} is ${formatBytes$1(firstLineBytes)}, exceeds ${formatBytes$1(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
14919
15339
|
details.truncation = truncation;
|
|
14920
15340
|
truncationInfo = {
|
|
14921
15341
|
result: truncation,
|
|
@@ -15089,7 +15509,7 @@ function selectorChunkLooksReadLike(chunk) {
|
|
|
15089
15509
|
return lower === "raw" || lower === "conflicts" || /^-\d+(?:[-+]\d+)?$/.test(chunk) || parseLineRanges(chunk) !== null;
|
|
15090
15510
|
}
|
|
15091
15511
|
function invalidSelector(sel) {
|
|
15092
|
-
return new ToolError
|
|
15512
|
+
return new ToolError(`Invalid selector ':${sel}'. Use :N, :N-M, :N+K, :N- (open-ended), a comma-separated list of ranges, :raw, or a range combined with raw (e.g. :raw:50-100).`);
|
|
15093
15513
|
}
|
|
15094
15514
|
function parseSel(sel) {
|
|
15095
15515
|
if (!sel || sel.length === 0) return { kind: "none" };
|
|
@@ -15214,15 +15634,15 @@ async function readArchive(session, readPath, parsedSel, resolvedArchivePath, si
|
|
|
15214
15634
|
sel = wholeSel;
|
|
15215
15635
|
}
|
|
15216
15636
|
}
|
|
15217
|
-
if (!node) throw new ToolError
|
|
15637
|
+
if (!node) throw new ToolError(`Path '${readPath}' not found inside archive`);
|
|
15218
15638
|
if (node.isDirectory) {
|
|
15219
|
-
if (isMultiRange(sel)) throw new ToolError
|
|
15639
|
+
if (isMultiRange(sel)) throw new ToolError("Multi-range line selectors are not supported for archive directory listings.");
|
|
15220
15640
|
const { offset, limit } = selToOffsetLimit(sel);
|
|
15221
15641
|
return readArchiveDirectory(archive, resolvedArchivePath.absolutePath, archiveSubPath, offset, limit, details, signal);
|
|
15222
15642
|
}
|
|
15223
15643
|
const entry = await archive.readFile(archiveSubPath);
|
|
15224
15644
|
const text = decodeUtf8Text(entry.bytes);
|
|
15225
|
-
if (text === null) return toolResult(details).text(prependSuffixResolutionNotice(`[Cannot read binary archive entry '${entry.path}' (${formatBytes$
|
|
15645
|
+
if (text === null) return toolResult(details).text(prependSuffixResolutionNotice(`[Cannot read binary archive entry '${entry.path}' (${formatBytes$1(entry.size)})]`, resolvedArchivePath.suffixResolution)).sourcePath(resolvedArchivePath.absolutePath).done();
|
|
15226
15646
|
const raw = isRawSelector(sel);
|
|
15227
15647
|
const result = isMultiRange(sel) && sel.kind === "lines" ? buildInMemoryMultiRangeResult(session, text, sel.ranges, {
|
|
15228
15648
|
details,
|
|
@@ -15354,10 +15774,10 @@ async function readSqlite(resolvedSqlitePath, signal) {
|
|
|
15354
15774
|
return toolResult(details).text(prependSuffixResolutionNotice(output, resolvedSqlitePath.suffixResolution)).sourcePath(resolvedSqlitePath.absolutePath).done();
|
|
15355
15775
|
}
|
|
15356
15776
|
}
|
|
15357
|
-
throw new ToolError
|
|
15777
|
+
throw new ToolError("Unsupported SQLite selector");
|
|
15358
15778
|
} catch (error) {
|
|
15359
|
-
if (error instanceof ToolError
|
|
15360
|
-
throw new ToolError
|
|
15779
|
+
if (error instanceof ToolError) throw error;
|
|
15780
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
15361
15781
|
} finally {
|
|
15362
15782
|
db?.close();
|
|
15363
15783
|
}
|
|
@@ -15537,7 +15957,7 @@ function reportIssueDeviceUsage() {
|
|
|
15537
15957
|
}
|
|
15538
15958
|
function parseReportIssueBody(text) {
|
|
15539
15959
|
const body = text.trim();
|
|
15540
|
-
if (!body) throw new ToolError
|
|
15960
|
+
if (!body) throw new ToolError(`Empty report. ${reportIssueDeviceUsage()}`);
|
|
15541
15961
|
const firstNewline = body.indexOf("\n");
|
|
15542
15962
|
if (firstNewline >= 0) {
|
|
15543
15963
|
const tool = body.slice(0, firstNewline).trim();
|
|
@@ -15556,7 +15976,7 @@ function parseReportIssueBody(text) {
|
|
|
15556
15976
|
report
|
|
15557
15977
|
};
|
|
15558
15978
|
}
|
|
15559
|
-
throw new ToolError
|
|
15979
|
+
throw new ToolError(`Invalid report format. ${reportIssueDeviceUsage()}`);
|
|
15560
15980
|
}
|
|
15561
15981
|
/**
|
|
15562
15982
|
* Whether Auto-QA is active for this session.
|
|
@@ -15828,7 +16248,7 @@ async function dispatchReportIssueDevice(session, text) {
|
|
|
15828
16248
|
recordToolIssue(session, tool, report);
|
|
15829
16249
|
}
|
|
15830
16250
|
} catch (error$1) {
|
|
15831
|
-
if (error$1 instanceof ToolError
|
|
16251
|
+
if (error$1 instanceof ToolError) throw error$1;
|
|
15832
16252
|
error("Failed to record tool issue", { error: error$1 });
|
|
15833
16253
|
}
|
|
15834
16254
|
return {
|
|
@@ -15944,9 +16364,9 @@ async function runResolveInvocation(params, options) {
|
|
|
15944
16364
|
try {
|
|
15945
16365
|
options.onApplyError?.(error);
|
|
15946
16366
|
} catch {}
|
|
15947
|
-
if (error instanceof ToolError
|
|
16367
|
+
if (error instanceof ToolError) throw error;
|
|
15948
16368
|
const message = error instanceof Error ? error.message : String(error);
|
|
15949
|
-
throw new ToolError
|
|
16369
|
+
throw new ToolError(`Apply failed: ${message}`);
|
|
15950
16370
|
}
|
|
15951
16371
|
return {
|
|
15952
16372
|
...result,
|
|
@@ -15985,7 +16405,7 @@ async function dispatchResolutionDevice(session, device, text) {
|
|
|
15985
16405
|
const body = text.trim();
|
|
15986
16406
|
if (device === "propose") {
|
|
15987
16407
|
const handler = session.peekPlanProposalHandler?.();
|
|
15988
|
-
if (!handler) throw new ToolError
|
|
16408
|
+
if (!handler) throw new ToolError(`No plan is awaiting approval — ${PROPOSE_DEVICE_PATH} only accepts a plan title while plan mode is active.`);
|
|
15989
16409
|
const result = await handler(body);
|
|
15990
16410
|
return {
|
|
15991
16411
|
result,
|
|
@@ -16026,7 +16446,7 @@ async function dispatchResolutionDevice(session, device, text) {
|
|
|
16026
16446
|
}
|
|
16027
16447
|
};
|
|
16028
16448
|
}
|
|
16029
|
-
throw new ToolError
|
|
16449
|
+
throw new ToolError(`No pending action to apply — ${RESOLVE_DEVICE_PATH} is only valid while a staged preview is pending.${proposeHint}`);
|
|
16030
16450
|
}
|
|
16031
16451
|
const result = await invoker({
|
|
16032
16452
|
action,
|
|
@@ -16309,7 +16729,7 @@ var ReadTool = class {
|
|
|
16309
16729
|
}, "the current model does not accept image input")
|
|
16310
16730
|
}]);
|
|
16311
16731
|
const maxBytes = Math.min(this.#imageInputMaxBytes, imageByteCap(bridge, this.#imageInputMaxBytes));
|
|
16312
|
-
if (fileSize > maxBytes) throw new ToolError
|
|
16732
|
+
if (fileSize > maxBytes) throw new ToolError(`Image file too large: ${fileSize} bytes exceeds ${maxBytes} bytes; downscale the image and read the smaller copy`);
|
|
16313
16733
|
let loaded;
|
|
16314
16734
|
try {
|
|
16315
16735
|
const maybeLoaded = await loadImageInput({
|
|
@@ -16322,10 +16742,10 @@ var ReadTool = class {
|
|
|
16322
16742
|
excludeWebP: this.#forceExcludeWebp ? true : webpExclusionForModel(this.session.getActiveModel?.()),
|
|
16323
16743
|
resize: this.#imageResizeOptions
|
|
16324
16744
|
});
|
|
16325
|
-
if (!maybeLoaded) throw new ToolError
|
|
16745
|
+
if (!maybeLoaded) throw new ToolError(`Read image file [${mimeType}] failed: unsupported image format.`);
|
|
16326
16746
|
loaded = maybeLoaded;
|
|
16327
16747
|
} catch (error) {
|
|
16328
|
-
if (error instanceof ImageInputTooLargeError) throw new ToolError
|
|
16748
|
+
if (error instanceof ImageInputTooLargeError) throw new ToolError(error.message);
|
|
16329
16749
|
throw error;
|
|
16330
16750
|
}
|
|
16331
16751
|
const commitMediaType = ((/* @__PURE__ */ new Set([
|
|
@@ -16527,13 +16947,13 @@ var ReadTool = class {
|
|
|
16527
16947
|
if (readPath.startsWith("file://")) readPath = expandPath(readPath);
|
|
16528
16948
|
const conflictUri = parseConflictUri(readPath);
|
|
16529
16949
|
if (conflictUri) {
|
|
16530
|
-
if (conflictUri.id === "*") throw new ToolError
|
|
16950
|
+
if (conflictUri.id === "*") throw new ToolError("Reading `conflict://*` is not supported — wildcards are write-only. Use the `<path>:conflicts` read selector for the full list of conflicts in a file, or read `conflict://<N>` to inspect a single block.");
|
|
16531
16951
|
return this.#readConflictRegion(conflictUri.id, conflictUri.scope);
|
|
16532
16952
|
}
|
|
16533
16953
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
16534
16954
|
const parsedUrlTarget = parseReadUrlTarget(readPath);
|
|
16535
16955
|
if (parsedUrlTarget) {
|
|
16536
|
-
if (!this.session.settings.get("fetch.enabled")) throw new ToolError
|
|
16956
|
+
if (!this.session.settings.get("fetch.enabled")) throw new ToolError("URL reads are disabled by settings.");
|
|
16537
16957
|
const urlRaw = parsedUrlTarget.raw;
|
|
16538
16958
|
const urlRanges = parsedUrlTarget.ranges;
|
|
16539
16959
|
if (urlRanges !== void 0 && urlRanges.length > 1) {
|
|
@@ -16576,7 +16996,7 @@ var ReadTool = class {
|
|
|
16576
16996
|
if (internalRouter.canResolve(readPath)) {
|
|
16577
16997
|
const internalTarget = splitInternalUrlSel(readPath);
|
|
16578
16998
|
const parsed = parseSel(internalTarget.sel);
|
|
16579
|
-
if (internalTarget.sel !== void 0 && parsed.kind === "none") throw new ToolError
|
|
16999
|
+
if (internalTarget.sel !== void 0 && parsed.kind === "none") throw new ToolError(`Invalid selector ':${internalTarget.sel}' on '${internalTarget.path}'. Use :N, :N-M, :N+K, :N- (open-ended), a comma-separated list of ranges, :raw, or a range combined with raw (e.g. :raw:50-100).`);
|
|
16580
17000
|
const urlMeta = parseInternalUrl(internalTarget.path);
|
|
16581
17001
|
if (urlMeta.protocol.replace(/:$/, "").toLowerCase() === "local") {
|
|
16582
17002
|
const localFile = await resolveLocalUrlToFile(urlMeta, {
|
|
@@ -16653,12 +17073,12 @@ var ReadTool = class {
|
|
|
16653
17073
|
if (!recoveredApprovedPlan && !suffixResolution) {
|
|
16654
17074
|
const delimitedResult = await this.#tryReadDelimitedPaths(readPath, signal);
|
|
16655
17075
|
if (delimitedResult) return delimitedResult;
|
|
16656
|
-
throw new ToolError
|
|
17076
|
+
throw new ToolError(`Path '${localReadPath}' not found`);
|
|
16657
17077
|
}
|
|
16658
17078
|
} else throw error;
|
|
16659
17079
|
}
|
|
16660
17080
|
if (isDirectory) {
|
|
16661
|
-
if (isMultiRange(parsed)) throw new ToolError
|
|
17081
|
+
if (isMultiRange(parsed)) throw new ToolError("Multi-range line selectors are not supported for directory listings.");
|
|
16662
17082
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
16663
17083
|
const dirResult = await this.#readDirectory(absolutePath, offset, limit, void 0);
|
|
16664
17084
|
if (suffixResolution) {
|
|
@@ -16751,7 +17171,7 @@ var ReadTool = class {
|
|
|
16751
17171
|
if (!isRawSelector(parsed) && await isProbablyBinary(absolutePath)) return toolResult({
|
|
16752
17172
|
resolvedPath: absolutePath,
|
|
16753
17173
|
suffixResolution
|
|
16754
|
-
}).text(prependSuffixResolutionNotice(`[Cannot read binary file '${resolvedDisplayPath}' (${formatBytes$
|
|
17174
|
+
}).text(prependSuffixResolutionNotice(`[Cannot read binary file '${resolvedDisplayPath}' (${formatBytes$1(fileSize)}); not valid UTF-8 text. Use ':raw' to read bytes verbatim.]`, suffixResolution)).sourcePath(absolutePath).done();
|
|
16755
17175
|
if (parsed.kind === "none" && this.session.settings.get("read.summarize.enabled") && (this.session.settings.get("read.summarize.prose") || !isProseSummaryPath(absolutePath))) {
|
|
16756
17176
|
const summary = await trySummarize(this.session, absolutePath, fileSize, signal);
|
|
16757
17177
|
if (summary?.parsed && summary.elided) {
|
|
@@ -16901,9 +17321,9 @@ var ReadTool = class {
|
|
|
16901
17321
|
text: "",
|
|
16902
17322
|
bytes: 0
|
|
16903
17323
|
};
|
|
16904
|
-
if (shouldAddHashLines) outputText = `[Line ${startLineDisplay} is ${formatBytes$
|
|
17324
|
+
if (shouldAddHashLines) outputText = `[Line ${startLineDisplay} is ${formatBytes$1(firstLineBytes)}, exceeds ${formatBytes$1(maxBytesForRead)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
|
|
16905
17325
|
else outputText = formatText(snippet.text, startLineDisplay);
|
|
16906
|
-
if (snippet.text.length === 0) outputText = `[Line ${startLineDisplay} is ${formatBytes$
|
|
17326
|
+
if (snippet.text.length === 0) outputText = `[Line ${startLineDisplay} is ${formatBytes$1(firstLineBytes)}, exceeds ${formatBytes$1(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
16907
17327
|
details = { truncation };
|
|
16908
17328
|
sourcePath = absolutePath;
|
|
16909
17329
|
truncationInfo = {
|
|
@@ -16929,7 +17349,7 @@ var ReadTool = class {
|
|
|
16929
17349
|
} else if (startLine + userLimitedLines < totalFileLines || !reachedEof) {
|
|
16930
17350
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
16931
17351
|
outputText = formatBracketAwareText() ?? formatText(truncation.content, startLineDisplay);
|
|
16932
|
-
outputText += reachedEof ? `\n\n[${totalFileLines - (startLine + userLimitedLines)} more lines in file. Use :${nextOffset} to continue]` : `\n\n[More lines in file (${formatBytes$
|
|
17352
|
+
outputText += reachedEof ? `\n\n[${totalFileLines - (startLine + userLimitedLines)} more lines in file. Use :${nextOffset} to continue]` : `\n\n[More lines in file (${formatBytes$1(fileSize)} total; not scanned to EOF). Use :${nextOffset} to continue]`;
|
|
16933
17353
|
details = {};
|
|
16934
17354
|
sourcePath = absolutePath;
|
|
16935
17355
|
} else {
|
|
@@ -16999,7 +17419,7 @@ var ReadTool = class {
|
|
|
16999
17419
|
*/
|
|
17000
17420
|
async #readConflictRegion(id, scope) {
|
|
17001
17421
|
const entry = getConflictHistory(this.session).get(id);
|
|
17002
|
-
if (!entry) throw new ToolError
|
|
17422
|
+
if (!entry) throw new ToolError(`Conflict #${id} not found. Conflict ids are registered when \`read\` surfaces a marker block; re-read the file to get a current id.`);
|
|
17003
17423
|
const region = renderConflictRegion(entry, scope);
|
|
17004
17424
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
17005
17425
|
const shouldAddHashLines = displayMode.hashLines;
|
|
@@ -17043,11 +17463,11 @@ var ReadTool = class {
|
|
|
17043
17463
|
}).text(summary).sourcePath(absolutePath).done();
|
|
17044
17464
|
}
|
|
17045
17465
|
#formatArtifactWorkflowNotice(artifact, artifactUrl) {
|
|
17046
|
-
return `Artifact storage: ${shortenPath(artifact.path)} (${formatBytes$
|
|
17466
|
+
return `Artifact storage: ${shortenPath(artifact.path)} (${formatBytes$1(artifact.size)}). Use ${artifactUrl}:N-M to page, ${artifactUrl}:raw:N-M for verbatim chunks, and the artifact file path for search/copy workflows.`;
|
|
17047
17467
|
}
|
|
17048
17468
|
#formatRawArtifactBlockedNotice(artifact, artifactUrl) {
|
|
17049
17469
|
const displayPath = shortenPath(artifact.path);
|
|
17050
|
-
return `Unbounded raw read blocked for ${artifactUrl} (${formatBytes$
|
|
17470
|
+
return `Unbounded raw read blocked for ${artifactUrl} (${formatBytes$1(artifact.size)}). Reading the whole artifact verbatim can exhaust memory. Use ${artifactUrl}:raw:1-3000 for bounded verbatim chunks, ${artifactUrl}:1-3000 for numbered exploration, and the artifact file path for search/copy workflows: ${displayPath}`;
|
|
17051
17471
|
}
|
|
17052
17472
|
async #readArtifactFile(url, parsedSel, signal) {
|
|
17053
17473
|
const artifact = await resolveArtifactFile(url, {
|
|
@@ -17131,7 +17551,7 @@ var ReadTool = class {
|
|
|
17131
17551
|
text: "",
|
|
17132
17552
|
bytes: 0
|
|
17133
17553
|
};
|
|
17134
|
-
outputText = snippet.text.length > 0 ? formatText(snippet.text, startLineDisplay) : `[Line ${startLineDisplay} is ${formatBytes$
|
|
17554
|
+
outputText = snippet.text.length > 0 ? formatText(snippet.text, startLineDisplay) : `[Line ${startLineDisplay} is ${formatBytes$1(firstLineBytes)}, exceeds ${formatBytes$1(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
17135
17555
|
truncationInfo = {
|
|
17136
17556
|
result: truncation,
|
|
17137
17557
|
options: {
|
|
@@ -17152,7 +17572,7 @@ var ReadTool = class {
|
|
|
17152
17572
|
};
|
|
17153
17573
|
else if (startLine + collectedLines.length < totalFileLines || !reachedEof) {
|
|
17154
17574
|
const nextOffset = startLine + collectedLines.length + 1;
|
|
17155
|
-
outputText += reachedEof ? `\n\n[${totalFileLines - (startLine + collectedLines.length)} more lines in artifact. Use ${artifactUrl}:${nextOffset} to continue]` : `\n\n[More lines in artifact (${formatBytes$
|
|
17575
|
+
outputText += reachedEof ? `\n\n[${totalFileLines - (startLine + collectedLines.length)} more lines in artifact. Use ${artifactUrl}:${nextOffset} to continue]` : `\n\n[More lines in artifact (${formatBytes$1(artifact.size)} total; not scanned to EOF). Use ${artifactUrl}:${nextOffset} to continue]`;
|
|
17156
17576
|
}
|
|
17157
17577
|
}
|
|
17158
17578
|
if (!rawSelector && artifact.size > MAX_ARTIFACT_RAW_INLINE_BYTES) outputText += `\n\n[${this.#formatArtifactWorkflowNotice(artifact, artifactUrl)}]`;
|
|
@@ -17173,7 +17593,7 @@ var ReadTool = class {
|
|
|
17173
17593
|
try {
|
|
17174
17594
|
urlMeta = parseInternalUrl(url);
|
|
17175
17595
|
} catch (e) {
|
|
17176
|
-
throw new ToolError
|
|
17596
|
+
throw new ToolError(e instanceof Error ? e.message : String(e));
|
|
17177
17597
|
}
|
|
17178
17598
|
const scheme = urlMeta.protocol.replace(/:$/, "").toLowerCase();
|
|
17179
17599
|
let hasExtraction = false;
|
|
@@ -17187,7 +17607,7 @@ var ReadTool = class {
|
|
|
17187
17607
|
const localImage = await this.#loadLocalImageUrl(urlMeta, signal);
|
|
17188
17608
|
if (localImage) return localImage;
|
|
17189
17609
|
}
|
|
17190
|
-
if (hasExtraction && parsedSel.kind !== "none" && parsedSel.kind !== "raw") throw new ToolError
|
|
17610
|
+
if (hasExtraction && parsedSel.kind !== "none" && parsedSel.kind !== "raw") throw new ToolError("Cannot combine query extraction with line selectors");
|
|
17191
17611
|
const resource = await internalRouter.resolve(url, {
|
|
17192
17612
|
cwd: this.session.cwd,
|
|
17193
17613
|
settings: this.session.settings,
|
|
@@ -17197,7 +17617,7 @@ var ReadTool = class {
|
|
|
17197
17617
|
xd: { read: async (name) => {
|
|
17198
17618
|
if (name === "report_issue") return reportIssueDeviceUsage();
|
|
17199
17619
|
if (name && isResolutionDeviceName(name)) return resolutionDeviceUsage(name);
|
|
17200
|
-
throw new ToolError
|
|
17620
|
+
throw new ToolError(name ? `Unknown xd:// device '${name}'.` : "No xd:// tools are mounted in this session.");
|
|
17201
17621
|
} }
|
|
17202
17622
|
});
|
|
17203
17623
|
const details = {
|
|
@@ -17272,7 +17692,7 @@ var ReadTool = class {
|
|
|
17272
17692
|
});
|
|
17273
17693
|
} catch (error) {
|
|
17274
17694
|
const message = error instanceof Error ? error.message : String(error);
|
|
17275
|
-
throw new ToolError
|
|
17695
|
+
throw new ToolError(`Cannot read directory: ${message}`);
|
|
17276
17696
|
}
|
|
17277
17697
|
throwIfAborted(signal);
|
|
17278
17698
|
const output = tree.totalLines <= 1 ? "(empty directory)" : tree.rendered;
|
|
@@ -17511,7 +17931,7 @@ async function writethroughNoop(dst, content, _signal, file) {
|
|
|
17511
17931
|
}
|
|
17512
17932
|
/** Async UTF-8 write (node:fs) used when no file handle is supplied. */
|
|
17513
17933
|
async function writeFileUtf8(dst, content) {
|
|
17514
|
-
const { writeFile, mkdir } = await import("./fs-promises-shim-
|
|
17934
|
+
const { writeFile, mkdir } = await import("./fs-promises-shim-Bx0fpxuk.mjs").then((n) => n.n);
|
|
17515
17935
|
const { dirname } = await import("node:path");
|
|
17516
17936
|
try {
|
|
17517
17937
|
await writeFile(dst, content, "utf8");
|
|
@@ -17536,44 +17956,38 @@ function truncateForPrompt(value, maxChars = DEFAULT_PROMPT_TRUNCATE_CHARS) {
|
|
|
17536
17956
|
return `${value.slice(0, maxChars)}[…${omitted}ch elided…]`;
|
|
17537
17957
|
}
|
|
17538
17958
|
//#endregion
|
|
17539
|
-
//#region src/tools/
|
|
17959
|
+
//#region src/tools/omp/tools/auto-generated-guard.ts
|
|
17540
17960
|
/**
|
|
17541
|
-
*
|
|
17961
|
+
* Auto-generated file detection guard.
|
|
17542
17962
|
*
|
|
17543
|
-
*
|
|
17544
|
-
*
|
|
17963
|
+
* Prevents editing of files that appear to be automatically generated
|
|
17964
|
+
* by code generation tools (protoc, sqlc, buf, swagger, etc.).
|
|
17965
|
+
*
|
|
17966
|
+
* 唯一实现(A-3,second-impl-audit.md):本文件是 OMP refs
|
|
17967
|
+
* packages/coding-agent/src/tools/auto-generated-guard.ts 的逐字移植,此前
|
|
17968
|
+
* 曾与 `src/tools/shared/auto-generated-guard.dsh.ts`(简化重写)并存,现已
|
|
17969
|
+
* 收敛到此处;`src/tools/edit/adapter/tools/auto-generated-guard.ts` 与
|
|
17970
|
+
* `src/tools/shared/auto-generated-guard.dsh.ts` 仅保留纯转发。
|
|
17971
|
+
* 唯一偏离(T17-1 修复,随合并带入):basename 提取兼容 Windows 反斜杠路径。
|
|
17545
17972
|
*/
|
|
17546
17973
|
/**
|
|
17547
|
-
*
|
|
17548
|
-
* Override render() for custom LLM-facing formatting.
|
|
17974
|
+
* Number of bytes to read from the start of a file for auto-generated detection.
|
|
17549
17975
|
*/
|
|
17550
|
-
|
|
17551
|
-
|
|
17552
|
-
constructor(message, context) {
|
|
17553
|
-
super(message);
|
|
17554
|
-
this.context = context;
|
|
17555
|
-
this.name = "ToolError";
|
|
17556
|
-
}
|
|
17557
|
-
/** Render error for LLM consumption. Override for custom formatting. */
|
|
17558
|
-
render() {
|
|
17559
|
-
return this.message;
|
|
17560
|
-
}
|
|
17561
|
-
};
|
|
17562
|
-
//#endregion
|
|
17563
|
-
//#region src/tools/shared/auto-generated-guard.dsh.ts
|
|
17976
|
+
const CHECK_BYTE_COUNT = 1024;
|
|
17977
|
+
const HEADER_LINE_LIMIT = 40;
|
|
17564
17978
|
/**
|
|
17565
|
-
*
|
|
17566
|
-
*
|
|
17979
|
+
* Strong marker patterns for generated-file headers.
|
|
17980
|
+
*
|
|
17981
|
+
* Keep these strict: broad patterns like /auto-generated/ cause false positives
|
|
17982
|
+
* in normal hand-written files (including this guard implementation itself).
|
|
17567
17983
|
*/
|
|
17568
|
-
const
|
|
17569
|
-
const HEADER_LINE_LIMIT$1 = 40;
|
|
17570
|
-
const AUTO_GENERATED_HEADER_MARKERS$1 = [
|
|
17984
|
+
const AUTO_GENERATED_HEADER_MARKERS = [
|
|
17571
17985
|
/@generated\b/i,
|
|
17572
17986
|
/\bcode\s+generated\s+by\s+[a-z0-9_.-]+/i,
|
|
17573
17987
|
/\bthis\s+file\s+was\s+automatically\s+generated\b/i,
|
|
17574
17988
|
new RegExp(`\\bgenerated\\s+by\\s+(?:protoc(?:-gen-[\\w-]+)?|sqlc|buf|swagger(?:-codegen)?|openapi(?:-generator)?|grpc-gateway|mockery|stringer|easyjson|deepcopy-gen|defaulter-gen|conversion-gen|client-gen|lister-gen|informer-gen|kysely-codegen|napi-rs)\\b`, "i")
|
|
17575
17989
|
];
|
|
17576
|
-
const COMMENT_STYLES_BY_EXTENSION
|
|
17990
|
+
const COMMENT_STYLES_BY_EXTENSION = /* @__PURE__ */ new Map([
|
|
17577
17991
|
[".c", ["slash"]],
|
|
17578
17992
|
[".cc", ["slash"]],
|
|
17579
17993
|
[".cpp", ["slash"]],
|
|
@@ -17616,11 +18030,15 @@ const COMMENT_STYLES_BY_EXTENSION$1 = /* @__PURE__ */ new Map([
|
|
|
17616
18030
|
[".svg", ["html"]],
|
|
17617
18031
|
[".xhtml", ["html"]]
|
|
17618
18032
|
]);
|
|
17619
|
-
const COMMENT_STYLES_BY_BASENAME
|
|
18033
|
+
const COMMENT_STYLES_BY_BASENAME = /* @__PURE__ */ new Map([
|
|
17620
18034
|
["dockerfile", ["hash"]],
|
|
17621
18035
|
["makefile", ["hash"]],
|
|
17622
18036
|
["justfile", ["hash"]]
|
|
17623
18037
|
]);
|
|
18038
|
+
/**
|
|
18039
|
+
* File name patterns that strongly indicate auto-generated files.
|
|
18040
|
+
* These are checked against the file name (not content).
|
|
18041
|
+
*/
|
|
17624
18042
|
const AUTO_GENERATED_FILENAME_PATTERNS = [
|
|
17625
18043
|
/^zz_generated\./,
|
|
17626
18044
|
/\.pb\.(go|cc|h|c|js|ts)$/,
|
|
@@ -17633,30 +18051,30 @@ const AUTO_GENERATED_FILENAME_PATTERNS = [
|
|
|
17633
18051
|
/\.mock\.(go|ts)$/,
|
|
17634
18052
|
/\.mocks?\.(go|ts|js)$/
|
|
17635
18053
|
];
|
|
17636
|
-
function stripBom
|
|
18054
|
+
function stripBom(content) {
|
|
17637
18055
|
if (content.charCodeAt(0) === 65279) return content.slice(1);
|
|
17638
18056
|
return content;
|
|
17639
18057
|
}
|
|
17640
|
-
function getCommentStylesForPath
|
|
18058
|
+
function getCommentStylesForPath(filePath) {
|
|
17641
18059
|
const normalizedPath = filePath.toLowerCase();
|
|
17642
18060
|
const fileName = path.basename(normalizedPath);
|
|
17643
|
-
const stylesByName = COMMENT_STYLES_BY_BASENAME
|
|
18061
|
+
const stylesByName = COMMENT_STYLES_BY_BASENAME.get(fileName);
|
|
17644
18062
|
if (stylesByName) return stylesByName;
|
|
17645
18063
|
const ext = path.extname(fileName);
|
|
17646
|
-
return COMMENT_STYLES_BY_EXTENSION
|
|
18064
|
+
return COMMENT_STYLES_BY_EXTENSION.get(ext) ?? [];
|
|
17647
18065
|
}
|
|
17648
|
-
function extractLeadingHeaderCommentText
|
|
18066
|
+
function extractLeadingHeaderCommentText(content, commentStyles) {
|
|
17649
18067
|
if (commentStyles.length === 0) return "";
|
|
17650
18068
|
const includeSlash = commentStyles.includes("slash");
|
|
17651
18069
|
const includeHash = commentStyles.includes("hash");
|
|
17652
18070
|
const includeSql = commentStyles.includes("sql");
|
|
17653
18071
|
const includeHtml = commentStyles.includes("html");
|
|
17654
|
-
const lines = stripBom
|
|
18072
|
+
const lines = stripBom(content).split(/\r?\n/);
|
|
17655
18073
|
const headerLines = [];
|
|
17656
18074
|
let started = false;
|
|
17657
18075
|
let inSlashBlock = false;
|
|
17658
18076
|
let inHtmlBlock = false;
|
|
17659
|
-
for (let lineIndex = 0; lineIndex < lines.length && lineIndex < HEADER_LINE_LIMIT
|
|
18077
|
+
for (let lineIndex = 0; lineIndex < lines.length && lineIndex < HEADER_LINE_LIMIT; lineIndex += 1) {
|
|
17660
18078
|
const trimmed = lines[lineIndex]?.trim() ?? "";
|
|
17661
18079
|
if (lineIndex === 0 && trimmed.startsWith("#!")) continue;
|
|
17662
18080
|
if (inSlashBlock) {
|
|
@@ -17705,92 +18123,94 @@ function extractLeadingHeaderCommentText$1(content, commentStyles) {
|
|
|
17705
18123
|
}
|
|
17706
18124
|
return headerLines.join("\n");
|
|
17707
18125
|
}
|
|
18126
|
+
/**
|
|
18127
|
+
* Check if a file name indicates it might be auto-generated.
|
|
18128
|
+
* This is a quick pre-check before reading file content.
|
|
18129
|
+
*/
|
|
17708
18130
|
function isAutoGeneratedFileName(filePath) {
|
|
17709
18131
|
const fileName = filePath.split(/[\\/]/).pop() ?? "";
|
|
17710
|
-
return AUTO_GENERATED_FILENAME_PATTERNS.some((
|
|
18132
|
+
return AUTO_GENERATED_FILENAME_PATTERNS.some((pattern) => pattern.test(fileName));
|
|
17711
18133
|
}
|
|
17712
|
-
|
|
17713
|
-
|
|
18134
|
+
/**
|
|
18135
|
+
* Check if leading header comments contain auto-generated markers.
|
|
18136
|
+
* Returns the matched marker text if found, undefined otherwise.
|
|
18137
|
+
*/
|
|
18138
|
+
function detectAutoGeneratedMarker(content, filePath) {
|
|
18139
|
+
const headerCommentText = extractLeadingHeaderCommentText(content, getCommentStylesForPath(filePath));
|
|
17714
18140
|
if (!headerCommentText) return void 0;
|
|
17715
|
-
for (const markerPattern of AUTO_GENERATED_HEADER_MARKERS
|
|
18141
|
+
for (const markerPattern of AUTO_GENERATED_HEADER_MARKERS) {
|
|
17716
18142
|
const match = markerPattern.exec(headerCommentText);
|
|
17717
18143
|
if (match?.[0]) return match[0];
|
|
17718
18144
|
}
|
|
17719
18145
|
}
|
|
17720
|
-
|
|
17721
|
-
|
|
18146
|
+
/**
|
|
18147
|
+
* Build the error message for an auto-generated file.
|
|
18148
|
+
*/
|
|
18149
|
+
function buildAutoGeneratedError(displayPath, detected) {
|
|
18150
|
+
return new ToolError(`Cannot modify auto-generated file: ${displayPath}\n\nThis file appears to be automatically generated (detected marker: "${detected}").\nAuto-generated files should not be edited directly. Instead:\n1. Find the source file or generator configuration\n2. Make changes to the source\n3. Regenerate the file`);
|
|
17722
18151
|
}
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
map = /* @__PURE__ */ new Map();
|
|
17726
|
-
constructor(max) {
|
|
17727
|
-
this.max = max;
|
|
17728
|
-
}
|
|
17729
|
-
get(key) {
|
|
17730
|
-
const v = this.map.get(key);
|
|
17731
|
-
if (v !== void 0) {
|
|
17732
|
-
this.map.delete(key);
|
|
17733
|
-
this.map.set(key, v);
|
|
17734
|
-
}
|
|
17735
|
-
return v;
|
|
17736
|
-
}
|
|
17737
|
-
set(key, value) {
|
|
17738
|
-
if (this.map.has(key)) this.map.delete(key);
|
|
17739
|
-
this.map.set(key, value);
|
|
17740
|
-
if (this.map.size > this.max) {
|
|
17741
|
-
const first = this.map.keys().next().value;
|
|
17742
|
-
this.map.delete(first);
|
|
17743
|
-
}
|
|
17744
|
-
}
|
|
17745
|
-
};
|
|
17746
|
-
const autoGeneratedMap$1 = new SimpleLRU(10);
|
|
18152
|
+
const decoder = new TextDecoder("utf-8");
|
|
18153
|
+
const autoGeneratedMap = new LRUCache({ max: 10 });
|
|
17747
18154
|
async function getAutoGeneratedMarker(filePath) {
|
|
17748
18155
|
if (isAutoGeneratedFileName(filePath)) return filePath.split(/[\\/]/).pop() ?? "";
|
|
17749
|
-
let mtimeMs
|
|
18156
|
+
let mtimeMs;
|
|
18157
|
+
let size;
|
|
17750
18158
|
try {
|
|
17751
|
-
const stat = await
|
|
18159
|
+
const stat = await Bun.file(filePath).stat();
|
|
17752
18160
|
mtimeMs = stat.mtimeMs;
|
|
17753
18161
|
size = stat.size;
|
|
17754
18162
|
} catch (err) {
|
|
17755
|
-
if (err
|
|
18163
|
+
if (isEnoent(err)) return;
|
|
17756
18164
|
throw err;
|
|
17757
18165
|
}
|
|
17758
|
-
const cached = autoGeneratedMap
|
|
18166
|
+
const cached = autoGeneratedMap.get(filePath);
|
|
17759
18167
|
if (cached && cached.mtimeMs === mtimeMs && cached.size === size) return cached.marker;
|
|
17760
18168
|
let marker;
|
|
17761
18169
|
try {
|
|
17762
|
-
|
|
17763
|
-
try {
|
|
17764
|
-
const buf = Buffer.alloc(CHECK_BYTE_COUNT$1);
|
|
17765
|
-
const { bytesRead } = await handle.read(buf, 0, CHECK_BYTE_COUNT$1, 0);
|
|
17766
|
-
marker = detectAutoGeneratedMarker$1(buf.subarray(0, bytesRead).toString("utf-8"), filePath);
|
|
17767
|
-
} finally {
|
|
17768
|
-
await handle.close();
|
|
17769
|
-
}
|
|
18170
|
+
marker = detectAutoGeneratedMarker(await peekFile(filePath, CHECK_BYTE_COUNT, (header) => decoder.decode(header)), filePath);
|
|
17770
18171
|
} catch (err) {
|
|
17771
|
-
if (err
|
|
18172
|
+
if (isEnoent(err)) return;
|
|
17772
18173
|
throw err;
|
|
17773
18174
|
}
|
|
17774
|
-
autoGeneratedMap
|
|
18175
|
+
autoGeneratedMap.set(filePath, {
|
|
17775
18176
|
mtimeMs,
|
|
17776
18177
|
size,
|
|
17777
18178
|
marker
|
|
17778
18179
|
});
|
|
17779
18180
|
return marker;
|
|
17780
18181
|
}
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
17784
|
-
blockAutoGenerated
|
|
17785
|
-
}
|
|
17786
|
-
function shouldBlockAutoGeneratedFiles$1() {
|
|
17787
|
-
return blockAutoGenerated;
|
|
18182
|
+
function shouldBlockAutoGeneratedFiles(activeSettings) {
|
|
18183
|
+
if (activeSettings) return activeSettings.get("edit.blockAutoGenerated");
|
|
18184
|
+
if (isSettingsInitialized()) return settings.get("edit.blockAutoGenerated");
|
|
18185
|
+
return getDefault("edit.blockAutoGenerated");
|
|
17788
18186
|
}
|
|
17789
|
-
|
|
17790
|
-
|
|
18187
|
+
/**
|
|
18188
|
+
* Check if a file is auto-generated by examining its content.
|
|
18189
|
+
* Throws ToolError if the file appears to be auto-generated.
|
|
18190
|
+
*
|
|
18191
|
+
* @param absolutePath - Absolute path to the file
|
|
18192
|
+
* @param displayPath - Path to show in error messages (relative or as provided)
|
|
18193
|
+
* @param activeSettings - Session settings; falls back to global settings or the schema default
|
|
18194
|
+
*/
|
|
18195
|
+
async function assertEditableFile(absolutePath, displayPath, activeSettings) {
|
|
18196
|
+
if (!shouldBlockAutoGeneratedFiles(activeSettings)) return;
|
|
17791
18197
|
const pathForDisplay = displayPath ?? absolutePath;
|
|
17792
18198
|
const marker = await getAutoGeneratedMarker(absolutePath);
|
|
17793
|
-
if (marker) throw buildAutoGeneratedError
|
|
18199
|
+
if (marker) throw buildAutoGeneratedError(pathForDisplay, marker);
|
|
18200
|
+
}
|
|
18201
|
+
/**
|
|
18202
|
+
* Check if file content is auto-generated.
|
|
18203
|
+
* Uses only the first CHECK_BYTE_COUNT characters of the content.
|
|
18204
|
+
* Throws ToolError if the content appears to be auto-generated.
|
|
18205
|
+
*
|
|
18206
|
+
* @param content - File content to check (can be full content or prefix)
|
|
18207
|
+
* @param displayPath - Path to show in error messages
|
|
18208
|
+
* @param activeSettings - Session settings; falls back to global settings or the schema default
|
|
18209
|
+
*/
|
|
18210
|
+
function assertEditableFileContent(content, displayPath, activeSettings) {
|
|
18211
|
+
if (!shouldBlockAutoGeneratedFiles(activeSettings)) return;
|
|
18212
|
+
const marker = detectAutoGeneratedMarker(content.slice(0, CHECK_BYTE_COUNT), displayPath);
|
|
18213
|
+
if (marker) throw buildAutoGeneratedError(displayPath, marker);
|
|
17794
18214
|
}
|
|
17795
18215
|
//#endregion
|
|
17796
18216
|
//#region src/tools/omp/tools/fs-cache-invalidation.ts
|
|
@@ -17912,10 +18332,10 @@ function resolvePlanPath$1(session, targetPath) {
|
|
|
17912
18332
|
*/
|
|
17913
18333
|
function enforcePlanModeWrite$1(session, targetPath, options) {
|
|
17914
18334
|
if (!(session.getPlanModeState?.())?.enabled) return;
|
|
17915
|
-
if (options?.move) throw new ToolError
|
|
17916
|
-
if (options?.op === "delete") throw new ToolError
|
|
18335
|
+
if (options?.move) throw new ToolError("Plan mode: renaming files is not allowed.");
|
|
18336
|
+
if (options?.op === "delete") throw new ToolError("Plan mode: deleting files is not allowed.");
|
|
17917
18337
|
if (targetsLocalSandbox$1(session, targetPath)) return;
|
|
17918
|
-
throw new ToolError
|
|
18338
|
+
throw new ToolError("Plan mode: the working tree is read-only. Write your plan to a local://<slug>-plan.md file instead.");
|
|
17919
18339
|
}
|
|
17920
18340
|
//#endregion
|
|
17921
18341
|
//#region src/tools/omp/tools/write.ts
|
|
@@ -17932,14 +18352,14 @@ function assertWriteTargetAddressable(target, router) {
|
|
|
17932
18352
|
const trimmed = target.trim();
|
|
17933
18353
|
if (path.win32.isAbsolute(trimmed) || router.canHandle(trimmed)) return;
|
|
17934
18354
|
const missingDelimiter = trimmed.match(XD_MISSING_DELIMITER_RE);
|
|
17935
|
-
if (missingDelimiter) throw new ToolError
|
|
18355
|
+
if (missingDelimiter) throw new ToolError(`Unknown URI-like write target '${trimmed}'. Did you mean 'xd://${missingDelimiter[1]}'? Prefix the path with './' to write it as a filesystem path.`);
|
|
17936
18356
|
const uriLike = trimmed.match(URI_LIKE_WRITE_PATH_RE);
|
|
17937
18357
|
if (!uriLike) return;
|
|
17938
18358
|
const scheme = uriLike[1].toLowerCase();
|
|
17939
18359
|
if (scheme === "conflict") return;
|
|
17940
18360
|
const canonicalScheme = router.getHandler(scheme) ? scheme : XD_SCHEME_NEAR_MISSES[scheme] ? "xd" : void 0;
|
|
17941
18361
|
const suggestion = canonicalScheme ? ` Did you mean '${canonicalScheme}://${uriLike[2]}'?` : " Available 'xd://' devices include 'xd://resolve' and 'xd://report_issue'.";
|
|
17942
|
-
throw new ToolError
|
|
18362
|
+
throw new ToolError(`Unknown URI-like write target '${trimmed}'.${suggestion} Prefix the path with './' to write it as a filesystem path.`);
|
|
17943
18363
|
}
|
|
17944
18364
|
/**
|
|
17945
18365
|
* Fail closed when a local write target looks like a mis-dispatched read.
|
|
@@ -17963,7 +18383,7 @@ function readSelectorForEmptyWrite(target, content) {
|
|
|
17963
18383
|
return splitPathAndSel(target).sel;
|
|
17964
18384
|
}
|
|
17965
18385
|
function throwReadSelectorMisfire(target, sel) {
|
|
17966
|
-
throw new ToolError
|
|
18386
|
+
throw new ToolError(`write target '${target}' ends with a read-tool selector ':${sel}' and no such file exists — refusing to create a literal file by that name. If you meant to read it, use read({ path: "${target}" }). If you truly intend to create this file, pass its contents in \`content\` (a non-empty write is never blocked).`);
|
|
17967
18387
|
}
|
|
17968
18388
|
/**
|
|
17969
18389
|
* Recognize a semicolon-joined list of read-tool selectors mis-dispatched as a
|
|
@@ -17989,7 +18409,7 @@ function readSelectorListMisfire(target) {
|
|
|
17989
18409
|
return segments.length;
|
|
17990
18410
|
}
|
|
17991
18411
|
function throwReadSelectorListMisfire(target, count) {
|
|
17992
|
-
throw new ToolError
|
|
18412
|
+
throw new ToolError(`write target '${target}' is a semicolon-joined list of ${count} read-tool selectors, not a filesystem path — refusing to create it. write creates a single file; issue one read() per path to read these ranges (e.g. read({ path: "<one path>:<range>" })).`);
|
|
17993
18413
|
}
|
|
17994
18414
|
async function assertNotReadSelectorMisfire(target, content, cwd) {
|
|
17995
18415
|
const listCount = readSelectorListMisfire(target);
|
|
@@ -18037,14 +18457,14 @@ function parseBulkDirectives(content) {
|
|
|
18037
18457
|
}
|
|
18038
18458
|
sawDirective = true;
|
|
18039
18459
|
const id = Number.parseInt(match[1], 10);
|
|
18040
|
-
if (map.has(id)) throw new ToolError
|
|
18460
|
+
if (map.has(id)) throw new ToolError(`Bulk directive lists conflict #${id} twice — each id may appear once.`);
|
|
18041
18461
|
map.set(id, match[2]);
|
|
18042
18462
|
}
|
|
18043
18463
|
if (!sawDirective) return null;
|
|
18044
18464
|
if (stray.length > 0) {
|
|
18045
18465
|
const sample = stray[0];
|
|
18046
18466
|
const tokenHint = BULK_DIRECTIVE_HEAD_RE.test(sample) ? `Per-id bulk only accepts the tokens @ours/@theirs/@base/@both — one side per id, single line. ` : "";
|
|
18047
|
-
throw new ToolError
|
|
18467
|
+
throw new ToolError(`Malformed \`conflict://*\` per-id block: ${stray.length} line(s) are not \`<id>: @side\` directives (first: \`${truncateDirectiveLine(sample)}\`). ` + tokenHint + "Literal or multi-line replacement content isn't supported in a per-id block — resolve those blocks with individual `write({ path: \"conflict://<N>\", content })` calls (you can issue several at once). For a pure pick-a-side pass, make every non-empty line `<id>: @ours` (or @theirs/@base/@both).");
|
|
18048
18468
|
}
|
|
18049
18469
|
return map;
|
|
18050
18470
|
}
|
|
@@ -18185,27 +18605,27 @@ function isArchivePathNotFound(error) {
|
|
|
18185
18605
|
}
|
|
18186
18606
|
function normalizeArchiveWriteSubPath(rawPath) {
|
|
18187
18607
|
const normalized = rawPath.replace(/\\/g, "/");
|
|
18188
|
-
if (normalized.length === 0) throw new ToolError
|
|
18189
|
-
if (normalized.endsWith("/")) throw new ToolError
|
|
18608
|
+
if (normalized.length === 0) throw new ToolError("Archive write path must target a file inside the archive");
|
|
18609
|
+
if (normalized.endsWith("/")) throw new ToolError("Archive write path must target a file, not a directory");
|
|
18190
18610
|
const parts = normalized.split("/");
|
|
18191
18611
|
const normalizedParts = [];
|
|
18192
18612
|
for (const part of parts) {
|
|
18193
18613
|
if (!part || part === ".") continue;
|
|
18194
|
-
if (part === "..") throw new ToolError
|
|
18614
|
+
if (part === "..") throw new ToolError("Archive path cannot contain '..'");
|
|
18195
18615
|
normalizedParts.push(part);
|
|
18196
18616
|
}
|
|
18197
|
-
if (normalizedParts.length === 0) throw new ToolError
|
|
18617
|
+
if (normalizedParts.length === 0) throw new ToolError("Archive write path must target a file inside the archive");
|
|
18198
18618
|
return normalizedParts.join("/");
|
|
18199
18619
|
}
|
|
18200
18620
|
function parseSqliteWriteTarget(subPath, queryString) {
|
|
18201
|
-
if (queryString.trim().length > 0) throw new ToolError
|
|
18621
|
+
if (queryString.trim().length > 0) throw new ToolError("SQLite write paths do not support query parameters");
|
|
18202
18622
|
const normalized = subPath.replace(/^:+/, "").trim();
|
|
18203
|
-
if (!normalized) throw new ToolError
|
|
18623
|
+
if (!normalized) throw new ToolError("SQLite write path must target a table");
|
|
18204
18624
|
const separatorIndex = normalized.indexOf(":");
|
|
18205
18625
|
const table = separatorIndex === -1 ? normalized : normalized.slice(0, separatorIndex);
|
|
18206
18626
|
const key = separatorIndex === -1 ? void 0 : normalized.slice(separatorIndex + 1);
|
|
18207
|
-
if (!table) throw new ToolError
|
|
18208
|
-
if (key !== void 0 && key.length === 0) throw new ToolError
|
|
18627
|
+
if (!table) throw new ToolError("SQLite write path must target a table");
|
|
18628
|
+
if (key !== void 0 && key.length === 0) throw new ToolError("SQLite row writes require a non-empty row key");
|
|
18209
18629
|
return {
|
|
18210
18630
|
table,
|
|
18211
18631
|
key
|
|
@@ -18297,7 +18717,7 @@ var WriteTool = class {
|
|
|
18297
18717
|
});
|
|
18298
18718
|
for (const [entryPath, data] of existing) entries.set(entryPath, data);
|
|
18299
18719
|
} catch (error) {
|
|
18300
|
-
throw new ToolError
|
|
18720
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
18301
18721
|
}
|
|
18302
18722
|
const writeTarget = `${resolvedArchivePath.archivePath}:${resolvedArchivePath.archiveSubPath}`;
|
|
18303
18723
|
const sel = readSelectorForEmptyWrite(writeTarget, content);
|
|
@@ -18308,7 +18728,7 @@ var WriteTool = class {
|
|
|
18308
18728
|
await fs_promises_shim_exports.rename(tmpPath, finalPath);
|
|
18309
18729
|
} catch (error) {
|
|
18310
18730
|
await fs_promises_shim_exports.rm(tmpPath, { force: true }).catch(() => {});
|
|
18311
|
-
throw new ToolError
|
|
18731
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
18312
18732
|
}
|
|
18313
18733
|
invalidateFsScanAfterWrite(resolvedArchivePath.absolutePath);
|
|
18314
18734
|
const outputPath = `${formatPathRelativeToCwd(resolvedArchivePath.absolutePath, this.session.cwd)}:${resolvedArchivePath.archiveSubPath}`;
|
|
@@ -18359,7 +18779,7 @@ var WriteTool = class {
|
|
|
18359
18779
|
async #writeSqliteRow(displayPath, content, resolvedSqlitePath) {
|
|
18360
18780
|
let db = null;
|
|
18361
18781
|
try {
|
|
18362
|
-
if (!resolvedSqlitePath.exists) throw new ToolError
|
|
18782
|
+
if (!resolvedSqlitePath.exists) throw new ToolError(`SQLite database '${displayPath}' not found`);
|
|
18363
18783
|
db = new Database(resolvedSqlitePath.absolutePath, {
|
|
18364
18784
|
create: false,
|
|
18365
18785
|
strict: true
|
|
@@ -18368,7 +18788,7 @@ var WriteTool = class {
|
|
|
18368
18788
|
const trimmedContent = content.trim();
|
|
18369
18789
|
let resultText;
|
|
18370
18790
|
if (trimmedContent.length === 0) {
|
|
18371
|
-
if (!resolvedSqlitePath.key) throw new ToolError
|
|
18791
|
+
if (!resolvedSqlitePath.key) throw new ToolError("SQLite deletes require a row key in the path");
|
|
18372
18792
|
const lookup = resolveTableRowLookup(db, resolvedSqlitePath.table);
|
|
18373
18793
|
resultText = (lookup.kind === "pk" ? deleteRowByKey(db, resolvedSqlitePath.table, lookup, resolvedSqlitePath.key) : deleteRowByRowId(db, resolvedSqlitePath.table, resolvedSqlitePath.key)) > 0 ? `Deleted row '${resolvedSqlitePath.key}' from ${resolvedSqlitePath.table}` : `No row deleted from ${resolvedSqlitePath.table} for key '${resolvedSqlitePath.key}'`;
|
|
18374
18794
|
} else {
|
|
@@ -18376,9 +18796,9 @@ var WriteTool = class {
|
|
|
18376
18796
|
try {
|
|
18377
18797
|
parsedContent = Bun.JSON5.parse(content);
|
|
18378
18798
|
} catch (error) {
|
|
18379
|
-
throw new ToolError
|
|
18799
|
+
throw new ToolError(`SQLite write content must be valid JSON5: ${error instanceof Error ? error.message : String(error)}`);
|
|
18380
18800
|
}
|
|
18381
|
-
if (!isRecord$2(parsedContent)) throw new ToolError
|
|
18801
|
+
if (!isRecord$2(parsedContent)) throw new ToolError("SQLite write content must be a JSON object");
|
|
18382
18802
|
if (resolvedSqlitePath.key) {
|
|
18383
18803
|
const lookup = resolveTableRowLookup(db, resolvedSqlitePath.table);
|
|
18384
18804
|
resultText = (lookup.kind === "pk" ? updateRowByKey(db, resolvedSqlitePath.table, lookup, resolvedSqlitePath.key, parsedContent) : updateRowByRowId(db, resolvedSqlitePath.table, resolvedSqlitePath.key, parsedContent)) > 0 ? `Updated row '${resolvedSqlitePath.key}' in ${resolvedSqlitePath.table}` : `No row updated in ${resolvedSqlitePath.table} for key '${resolvedSqlitePath.key}'`;
|
|
@@ -18390,9 +18810,9 @@ var WriteTool = class {
|
|
|
18390
18810
|
invalidateFsScanAfterWrite(resolvedSqlitePath.absolutePath);
|
|
18391
18811
|
return toolResult({ resolvedPath: resolvedSqlitePath.absolutePath }).text(resultText).sourcePath(resolvedSqlitePath.absolutePath).done();
|
|
18392
18812
|
} catch (error) {
|
|
18393
|
-
if (isEnoent(error)) throw new ToolError
|
|
18394
|
-
if (error instanceof ToolError
|
|
18395
|
-
throw new ToolError
|
|
18813
|
+
if (isEnoent(error)) throw new ToolError(`SQLite database '${displayPath}' not found`);
|
|
18814
|
+
if (error instanceof ToolError) throw error;
|
|
18815
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
18396
18816
|
} finally {
|
|
18397
18817
|
db?.close();
|
|
18398
18818
|
}
|
|
@@ -18411,7 +18831,7 @@ var WriteTool = class {
|
|
|
18411
18831
|
*/
|
|
18412
18832
|
async #resolveConflict(entry, replacementContent, stripped, signal) {
|
|
18413
18833
|
const absolutePath = entry.absolutePath;
|
|
18414
|
-
if (!await exists(absolutePath)) throw new ToolError
|
|
18834
|
+
if (!await exists(absolutePath)) throw new ToolError(`Conflict #${entry.id} target '${entry.displayPath}' no longer exists.`);
|
|
18415
18835
|
const expanded = expandContentTokens(replacementContent, entry);
|
|
18416
18836
|
const splice = spliceConflict(await Bun.file(absolutePath).text(), entry, expanded);
|
|
18417
18837
|
const newContent = splice.text;
|
|
@@ -18444,7 +18864,7 @@ var WriteTool = class {
|
|
|
18444
18864
|
*/
|
|
18445
18865
|
async #resolveSingleConflictById(id, replacementContent, stripped, signal) {
|
|
18446
18866
|
const entry = getConflictHistory(this.session).get(id);
|
|
18447
|
-
if (!entry) throw new ToolError
|
|
18867
|
+
if (!entry) throw new ToolError(`Conflict #${id} not found. Conflict ids are registered when \`read\` surfaces a marker block; re-read the file to get a current id.`);
|
|
18448
18868
|
return this.#resolveConflict(entry, replacementContent, stripped, signal);
|
|
18449
18869
|
}
|
|
18450
18870
|
/**
|
|
@@ -18465,12 +18885,12 @@ var WriteTool = class {
|
|
|
18465
18885
|
async #resolveAllConflicts(replacementContent, stripped, signal, rawContent = replacementContent) {
|
|
18466
18886
|
const history = getConflictHistory(this.session);
|
|
18467
18887
|
const allEntries = history.entries();
|
|
18468
|
-
if (allEntries.length === 0) throw new ToolError
|
|
18888
|
+
if (allEntries.length === 0) throw new ToolError("`conflict://*` has nothing to resolve — no conflicts are currently registered. Re-read the file(s) with conflicts first.");
|
|
18469
18889
|
const directives = resolveBulkDirectives(rawContent, replacementContent);
|
|
18470
18890
|
if (directives) {
|
|
18471
18891
|
const known = new Set(allEntries.map((entry) => entry.id));
|
|
18472
18892
|
const unknown = [...directives.keys()].filter((id) => !known.has(id));
|
|
18473
|
-
if (unknown.length > 0) throw new ToolError
|
|
18893
|
+
if (unknown.length > 0) throw new ToolError(`Bulk directive references unknown conflict id(s) ${unknown.map((id) => `#${id}`).join(", ")}. Currently registered: ${allEntries.map((e) => `#${e.id}`).join(", ")}.`);
|
|
18474
18894
|
}
|
|
18475
18895
|
const selectedEntries = directives ? allEntries.filter((entry) => directives.has(entry.id)) : allEntries;
|
|
18476
18896
|
const contentFor = (entry) => directives ? directives.get(entry.id) : replacementContent;
|
|
@@ -18567,7 +18987,7 @@ var WriteTool = class {
|
|
|
18567
18987
|
}
|
|
18568
18988
|
if (stripped && !directives) summaryLines.push("Note: auto-stripped hashline display prefixes from content before writing.");
|
|
18569
18989
|
const resultText = summaryLines.join("\n");
|
|
18570
|
-
if (failedFiles.length > 0 && succeededFiles.length === 0) throw new ToolError
|
|
18990
|
+
if (failedFiles.length > 0 && succeededFiles.length === 0) throw new ToolError(resultText);
|
|
18571
18991
|
return {
|
|
18572
18992
|
content: [{
|
|
18573
18993
|
type: "text",
|
|
@@ -18615,7 +19035,7 @@ var WriteTool = class {
|
|
|
18615
19035
|
};
|
|
18616
19036
|
return;
|
|
18617
19037
|
}
|
|
18618
|
-
throw new ToolError
|
|
19038
|
+
throw new ToolError(name ? `Unknown xd:// device '${name}'. DSH mounts no tool devices.` : "Cannot write to xd:// itself — available devices: xd://resolve, xd://reject, xd://propose, xd://report_issue.");
|
|
18619
19039
|
} }
|
|
18620
19040
|
});
|
|
18621
19041
|
if (xdResult) return xdResult;
|
|
@@ -18633,7 +19053,7 @@ var WriteTool = class {
|
|
|
18633
19053
|
}
|
|
18634
19054
|
const conflictUri = parseConflictUri(path);
|
|
18635
19055
|
if (conflictUri) {
|
|
18636
|
-
if (conflictUri.scope) throw new ToolError
|
|
19056
|
+
if (conflictUri.scope) throw new ToolError(`Conflict URI scope '/${conflictUri.scope}' is read-only — read \`conflict://${conflictUri.id}/${conflictUri.scope}\` to inspect that side. To write, drop the scope (\`conflict://${conflictUri.id}\`) and put the chosen content (or shorthand like \`@${conflictUri.scope}\`) in \`content\`.`);
|
|
18637
19057
|
emitWriteProgress(onUpdate, cleanContent, path);
|
|
18638
19058
|
const result = conflictUri.id === "*" ? await this.#resolveAllConflicts(cleanContent, stripped, signal, content) : await this.#resolveSingleConflictById(conflictUri.id, cleanContent, stripped, signal);
|
|
18639
19059
|
if (conflictUri.recoveredPrefix !== void 0) appendNoteToResult(result, `Note: stripped erroneous '${conflictUri.recoveredPrefix}:' prefix from path; conflict URIs are global (use \`conflict://${conflictUri.id}\`, not \`<file>:conflict://${conflictUri.id}\`).`);
|
|
@@ -20231,7 +20651,7 @@ async function executeReplace(options) {
|
|
|
20231
20651
|
if (old_string.length === 0) throw new Error("old_string must not be empty.");
|
|
20232
20652
|
const absolutePath = resolvePlanPath(session, path);
|
|
20233
20653
|
const rawContent = await readEditFileText(absolutePath, path);
|
|
20234
|
-
const { bom, text: content } = stripBom$
|
|
20654
|
+
const { bom, text: content } = stripBom$1(rawContent);
|
|
20235
20655
|
const originalEnding = detectLineEnding(content);
|
|
20236
20656
|
const normalizedContent = normalizeToLF(content);
|
|
20237
20657
|
const normalizedOldText = normalizeToLF(old_string);
|
|
@@ -21181,7 +21601,7 @@ async function applyNormalizedPatch(input, options) {
|
|
|
21181
21601
|
}
|
|
21182
21602
|
if (!input.diff) throw new ApplyPatchError("Update operation requires diff (hunks)");
|
|
21183
21603
|
const originalContent = await readExistingPatchFile(fs, absolutePath, input.path);
|
|
21184
|
-
const { bom: bomFromText, text: strippedContent } = stripBom$
|
|
21604
|
+
const { bom: bomFromText, text: strippedContent } = stripBom$1(originalContent);
|
|
21185
21605
|
let bom = bomFromText;
|
|
21186
21606
|
if (!bom && fs.readBinary) {
|
|
21187
21607
|
const bytes = await fs.readBinary(absolutePath);
|
|
@@ -21296,275 +21716,91 @@ async function executePatchSingle(options) {
|
|
|
21296
21716
|
} catch (err) {
|
|
21297
21717
|
if (!isEnoent(err)) throw err;
|
|
21298
21718
|
}
|
|
21299
|
-
if (postEditContent !== void 0 && postEditContent.length === preEditContent.length && postEditContent.every((b, i) => b === preEditContent[i])) throw new ToolError
|
|
21719
|
+
if (postEditContent !== void 0 && postEditContent.length === preEditContent.length && postEditContent.every((b, i) => b === preEditContent[i])) throw new ToolError(`edit appeared successful but file content did not change on disk: ${path}`, { path: resolvedPath });
|
|
21300
21720
|
}
|
|
21301
21721
|
if (resolvedRename) invalidateFsScanAfterRename(resolvedPath, resolvedRename);
|
|
21302
|
-
else if (result.change.type === "delete") invalidateFsScanAfterDelete(resolvedPath);
|
|
21303
|
-
else invalidateFsScanAfterWrite(resolvedPath);
|
|
21304
|
-
const effectiveRename = result.change.newPath ? rename : void 0;
|
|
21305
|
-
let diffResult = {
|
|
21306
|
-
diff: "",
|
|
21307
|
-
firstChangedLine: void 0
|
|
21308
|
-
};
|
|
21309
|
-
if (result.change.type === "update" && result.change.oldContent !== void 0 && result.change.newContent !== void 0) diffResult = generateUnifiedDiffString(normalizeToLF(stripBom$
|
|
21310
|
-
else if (result.change.type === "create" && result.change.newContent !== void 0) diffResult = generateUnifiedDiffString("", normalizeToLF(stripBom$
|
|
21311
|
-
let resultText;
|
|
21312
|
-
switch (result.change.type) {
|
|
21313
|
-
case "create":
|
|
21314
|
-
resultText = `Created ${path}`;
|
|
21315
|
-
break;
|
|
21316
|
-
case "delete":
|
|
21317
|
-
resultText = `Deleted ${path}`;
|
|
21318
|
-
break;
|
|
21319
|
-
case "update": resultText = effectiveRename ? `Updated and moved ${path} to ${effectiveRename}` : `Updated ${path}`;
|
|
21320
|
-
}
|
|
21321
|
-
const meta = outputMeta().get();
|
|
21322
|
-
const oldText = result.change.type !== "create" ? result.change.oldContent : void 0;
|
|
21323
|
-
const newText = result.change.type !== "delete" ? result.change.newContent : void 0;
|
|
21324
|
-
return {
|
|
21325
|
-
content: [{
|
|
21326
|
-
type: "text",
|
|
21327
|
-
text: resultText
|
|
21328
|
-
}],
|
|
21329
|
-
details: pruneOversizedEditSnapshots({
|
|
21330
|
-
diff: diffResult.diff,
|
|
21331
|
-
path: result.change.newPath ?? resolvedPath,
|
|
21332
|
-
firstChangedLine: diffResult.firstChangedLine,
|
|
21333
|
-
op,
|
|
21334
|
-
move: effectiveRename,
|
|
21335
|
-
sourcePath: result.change.newPath ? resolvedPath : void 0,
|
|
21336
|
-
meta,
|
|
21337
|
-
oldText,
|
|
21338
|
-
newText
|
|
21339
|
-
})
|
|
21340
|
-
};
|
|
21341
|
-
}
|
|
21342
|
-
//#endregion
|
|
21343
|
-
//#region src/tools/omp/edit/edit-clipboard.ts
|
|
21344
|
-
/**
|
|
21345
|
-
* Look up (or lazily create) the clipboard register attached to a session.
|
|
21346
|
-
* Storage lives on `session.editClipboard` so it ages out exactly with the
|
|
21347
|
-
* session itself.
|
|
21348
|
-
*/
|
|
21349
|
-
function getEditClipboard(session) {
|
|
21350
|
-
session.editClipboard ??= {};
|
|
21351
|
-
return session.editClipboard;
|
|
21352
|
-
}
|
|
21353
|
-
//#endregion
|
|
21354
|
-
//#region src/tools/omp/edit/hashline/block-resolver.ts
|
|
21355
|
-
/**
|
|
21356
|
-
* `blockRangeAt` runs a full synchronous tree-sitter parse of `text` per
|
|
21357
|
-
* call, and streaming previews re-resolve the same (text, line) every
|
|
21358
|
-
* streamed chunk. Memoize by content: identical text + line always yields the
|
|
21359
|
-
* same span. FIFO-bounded; hashing the text is orders of magnitude cheaper
|
|
21360
|
-
* than re-parsing it.
|
|
21361
|
-
*/
|
|
21362
|
-
const resolutionCache = /* @__PURE__ */ new Map();
|
|
21363
|
-
const RESOLUTION_CACHE_MAX = 512;
|
|
21364
|
-
const nativeBlockResolver = ({ path, text, line }) => {
|
|
21365
|
-
const key = `${Bun.hash(text).toString(36)}:${text.length}:${line}:${path}`;
|
|
21366
|
-
const cached = resolutionCache.get(key);
|
|
21367
|
-
if (cached !== void 0) return cached;
|
|
21368
|
-
const range = blockRangeAt({
|
|
21369
|
-
code: text,
|
|
21370
|
-
path,
|
|
21371
|
-
line
|
|
21372
|
-
});
|
|
21373
|
-
const result = range ? {
|
|
21374
|
-
start: range.startLine,
|
|
21375
|
-
end: range.endLine
|
|
21376
|
-
} : null;
|
|
21377
|
-
if (resolutionCache.size >= RESOLUTION_CACHE_MAX) {
|
|
21378
|
-
const oldest = resolutionCache.keys().next().value;
|
|
21379
|
-
if (oldest !== void 0) resolutionCache.delete(oldest);
|
|
21380
|
-
}
|
|
21381
|
-
resolutionCache.set(key, result);
|
|
21382
|
-
return result;
|
|
21383
|
-
};
|
|
21384
|
-
//#endregion
|
|
21385
|
-
//#region src/tools/edit/adapter/tools/auto-generated-guard.ts
|
|
21386
|
-
/**
|
|
21387
|
-
* Auto-generated file detection guard.
|
|
21388
|
-
*
|
|
21389
|
-
* Prevents editing of files that appear to be automatically generated
|
|
21390
|
-
* by code generation tools (protoc, sqlc, buf, swagger, etc.).
|
|
21391
|
-
*/
|
|
21392
|
-
/**
|
|
21393
|
-
* Number of bytes to read from the start of a file for auto-generated detection.
|
|
21394
|
-
*/
|
|
21395
|
-
const CHECK_BYTE_COUNT = 1024;
|
|
21396
|
-
const HEADER_LINE_LIMIT = 40;
|
|
21397
|
-
/**
|
|
21398
|
-
* Strong marker patterns for generated-file headers.
|
|
21399
|
-
*
|
|
21400
|
-
* Keep these strict: broad patterns like /auto-generated/ cause false positives
|
|
21401
|
-
* in normal hand-written files (including this guard implementation itself).
|
|
21402
|
-
*/
|
|
21403
|
-
const AUTO_GENERATED_HEADER_MARKERS = [
|
|
21404
|
-
/@generated\b/i,
|
|
21405
|
-
/\bcode\s+generated\s+by\s+[a-z0-9_.-]+/i,
|
|
21406
|
-
/\bthis\s+file\s+was\s+automatically\s+generated\b/i,
|
|
21407
|
-
new RegExp(`\\bgenerated\\s+by\\s+(?:protoc(?:-gen-[\\w-]+)?|sqlc|buf|swagger(?:-codegen)?|openapi(?:-generator)?|grpc-gateway|mockery|stringer|easyjson|deepcopy-gen|defaulter-gen|conversion-gen|client-gen|lister-gen|informer-gen|kysely-codegen|napi-rs)\\b`, "i")
|
|
21408
|
-
];
|
|
21409
|
-
const COMMENT_STYLES_BY_EXTENSION = /* @__PURE__ */ new Map([
|
|
21410
|
-
[".c", ["slash"]],
|
|
21411
|
-
[".cc", ["slash"]],
|
|
21412
|
-
[".cpp", ["slash"]],
|
|
21413
|
-
[".cs", ["slash"]],
|
|
21414
|
-
[".dart", ["slash"]],
|
|
21415
|
-
[".go", ["slash"]],
|
|
21416
|
-
[".h", ["slash"]],
|
|
21417
|
-
[".hpp", ["slash"]],
|
|
21418
|
-
[".java", ["slash"]],
|
|
21419
|
-
[".js", ["slash"]],
|
|
21420
|
-
[".jsx", ["slash"]],
|
|
21421
|
-
[".kt", ["slash"]],
|
|
21422
|
-
[".kts", ["slash"]],
|
|
21423
|
-
[".mjs", ["slash"]],
|
|
21424
|
-
[".cjs", ["slash"]],
|
|
21425
|
-
[".php", ["slash"]],
|
|
21426
|
-
[".rs", ["slash"]],
|
|
21427
|
-
[".scala", ["slash"]],
|
|
21428
|
-
[".swift", ["slash"]],
|
|
21429
|
-
[".ts", ["slash"]],
|
|
21430
|
-
[".tsx", ["slash"]],
|
|
21431
|
-
[".py", ["hash"]],
|
|
21432
|
-
[".rb", ["hash"]],
|
|
21433
|
-
[".sh", ["hash"]],
|
|
21434
|
-
[".bash", ["hash"]],
|
|
21435
|
-
[".zsh", ["hash"]],
|
|
21436
|
-
[".yml", ["hash"]],
|
|
21437
|
-
[".yaml", ["hash"]],
|
|
21438
|
-
[".toml", ["hash"]],
|
|
21439
|
-
[".ini", ["hash"]],
|
|
21440
|
-
[".cfg", ["hash"]],
|
|
21441
|
-
[".conf", ["hash"]],
|
|
21442
|
-
[".env", ["hash"]],
|
|
21443
|
-
[".pl", ["hash"]],
|
|
21444
|
-
[".r", ["hash"]],
|
|
21445
|
-
[".sql", ["sql"]],
|
|
21446
|
-
[".html", ["html"]],
|
|
21447
|
-
[".htm", ["html"]],
|
|
21448
|
-
[".xml", ["html"]],
|
|
21449
|
-
[".svg", ["html"]],
|
|
21450
|
-
[".xhtml", ["html"]]
|
|
21451
|
-
]);
|
|
21452
|
-
const COMMENT_STYLES_BY_BASENAME = /* @__PURE__ */ new Map([
|
|
21453
|
-
["dockerfile", ["hash"]],
|
|
21454
|
-
["makefile", ["hash"]],
|
|
21455
|
-
["justfile", ["hash"]]
|
|
21456
|
-
]);
|
|
21457
|
-
function stripBom(content) {
|
|
21458
|
-
if (content.charCodeAt(0) === 65279) return content.slice(1);
|
|
21459
|
-
return content;
|
|
21460
|
-
}
|
|
21461
|
-
function getCommentStylesForPath(filePath) {
|
|
21462
|
-
const normalizedPath = filePath.toLowerCase();
|
|
21463
|
-
const fileName = path.basename(normalizedPath);
|
|
21464
|
-
const stylesByName = COMMENT_STYLES_BY_BASENAME.get(fileName);
|
|
21465
|
-
if (stylesByName) return stylesByName;
|
|
21466
|
-
const ext = path.extname(fileName);
|
|
21467
|
-
return COMMENT_STYLES_BY_EXTENSION.get(ext) ?? [];
|
|
21468
|
-
}
|
|
21469
|
-
function extractLeadingHeaderCommentText(content, commentStyles) {
|
|
21470
|
-
if (commentStyles.length === 0) return "";
|
|
21471
|
-
const includeSlash = commentStyles.includes("slash");
|
|
21472
|
-
const includeHash = commentStyles.includes("hash");
|
|
21473
|
-
const includeSql = commentStyles.includes("sql");
|
|
21474
|
-
const includeHtml = commentStyles.includes("html");
|
|
21475
|
-
const lines = stripBom(content).split(/\r?\n/);
|
|
21476
|
-
const headerLines = [];
|
|
21477
|
-
let started = false;
|
|
21478
|
-
let inSlashBlock = false;
|
|
21479
|
-
let inHtmlBlock = false;
|
|
21480
|
-
for (let lineIndex = 0; lineIndex < lines.length && lineIndex < HEADER_LINE_LIMIT; lineIndex += 1) {
|
|
21481
|
-
const trimmed = lines[lineIndex]?.trim() ?? "";
|
|
21482
|
-
if (lineIndex === 0 && trimmed.startsWith("#!")) continue;
|
|
21483
|
-
if (inSlashBlock) {
|
|
21484
|
-
headerLines.push(trimmed);
|
|
21485
|
-
if (trimmed.includes("*/")) inSlashBlock = false;
|
|
21486
|
-
continue;
|
|
21487
|
-
}
|
|
21488
|
-
if (inHtmlBlock) {
|
|
21489
|
-
headerLines.push(trimmed);
|
|
21490
|
-
if (trimmed.includes("-->")) inHtmlBlock = false;
|
|
21491
|
-
continue;
|
|
21492
|
-
}
|
|
21493
|
-
if (trimmed.length === 0) {
|
|
21494
|
-
if (started) headerLines.push("");
|
|
21495
|
-
continue;
|
|
21496
|
-
}
|
|
21497
|
-
if (includeSlash && trimmed.startsWith("//")) {
|
|
21498
|
-
started = true;
|
|
21499
|
-
headerLines.push(trimmed);
|
|
21500
|
-
continue;
|
|
21501
|
-
}
|
|
21502
|
-
if (includeSlash && trimmed.startsWith("/*")) {
|
|
21503
|
-
started = true;
|
|
21504
|
-
headerLines.push(trimmed);
|
|
21505
|
-
if (!trimmed.includes("*/")) inSlashBlock = true;
|
|
21506
|
-
continue;
|
|
21507
|
-
}
|
|
21508
|
-
if (includeHash && trimmed.startsWith("#")) {
|
|
21509
|
-
started = true;
|
|
21510
|
-
headerLines.push(trimmed);
|
|
21511
|
-
continue;
|
|
21512
|
-
}
|
|
21513
|
-
if (includeSql && trimmed.startsWith("--")) {
|
|
21514
|
-
started = true;
|
|
21515
|
-
headerLines.push(trimmed);
|
|
21516
|
-
continue;
|
|
21517
|
-
}
|
|
21518
|
-
if (includeHtml && trimmed.startsWith("<!--")) {
|
|
21519
|
-
started = true;
|
|
21520
|
-
headerLines.push(trimmed);
|
|
21521
|
-
if (!trimmed.includes("-->")) inHtmlBlock = true;
|
|
21522
|
-
continue;
|
|
21523
|
-
}
|
|
21524
|
-
if (started) break;
|
|
21525
|
-
break;
|
|
21526
|
-
}
|
|
21527
|
-
return headerLines.join("\n");
|
|
21528
|
-
}
|
|
21529
|
-
/**
|
|
21530
|
-
* Check if leading header comments contain auto-generated markers.
|
|
21531
|
-
* Returns the matched marker text if found, undefined otherwise.
|
|
21532
|
-
*/
|
|
21533
|
-
function detectAutoGeneratedMarker(content, filePath) {
|
|
21534
|
-
const headerCommentText = extractLeadingHeaderCommentText(content, getCommentStylesForPath(filePath));
|
|
21535
|
-
if (!headerCommentText) return void 0;
|
|
21536
|
-
for (const markerPattern of AUTO_GENERATED_HEADER_MARKERS) {
|
|
21537
|
-
const match = markerPattern.exec(headerCommentText);
|
|
21538
|
-
if (match?.[0]) return match[0];
|
|
21722
|
+
else if (result.change.type === "delete") invalidateFsScanAfterDelete(resolvedPath);
|
|
21723
|
+
else invalidateFsScanAfterWrite(resolvedPath);
|
|
21724
|
+
const effectiveRename = result.change.newPath ? rename : void 0;
|
|
21725
|
+
let diffResult = {
|
|
21726
|
+
diff: "",
|
|
21727
|
+
firstChangedLine: void 0
|
|
21728
|
+
};
|
|
21729
|
+
if (result.change.type === "update" && result.change.oldContent !== void 0 && result.change.newContent !== void 0) diffResult = generateUnifiedDiffString(normalizeToLF(stripBom$1(result.change.oldContent).text), normalizeToLF(stripBom$1(result.change.newContent).text), void 0, { path: result.change.newPath ?? result.change.path });
|
|
21730
|
+
else if (result.change.type === "create" && result.change.newContent !== void 0) diffResult = generateUnifiedDiffString("", normalizeToLF(stripBom$1(result.change.newContent).text), void 0, { path: result.change.path });
|
|
21731
|
+
let resultText;
|
|
21732
|
+
switch (result.change.type) {
|
|
21733
|
+
case "create":
|
|
21734
|
+
resultText = `Created ${path}`;
|
|
21735
|
+
break;
|
|
21736
|
+
case "delete":
|
|
21737
|
+
resultText = `Deleted ${path}`;
|
|
21738
|
+
break;
|
|
21739
|
+
case "update": resultText = effectiveRename ? `Updated and moved ${path} to ${effectiveRename}` : `Updated ${path}`;
|
|
21539
21740
|
}
|
|
21741
|
+
const meta = outputMeta().get();
|
|
21742
|
+
const oldText = result.change.type !== "create" ? result.change.oldContent : void 0;
|
|
21743
|
+
const newText = result.change.type !== "delete" ? result.change.newContent : void 0;
|
|
21744
|
+
return {
|
|
21745
|
+
content: [{
|
|
21746
|
+
type: "text",
|
|
21747
|
+
text: resultText
|
|
21748
|
+
}],
|
|
21749
|
+
details: pruneOversizedEditSnapshots({
|
|
21750
|
+
diff: diffResult.diff,
|
|
21751
|
+
path: result.change.newPath ?? resolvedPath,
|
|
21752
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
21753
|
+
op,
|
|
21754
|
+
move: effectiveRename,
|
|
21755
|
+
sourcePath: result.change.newPath ? resolvedPath : void 0,
|
|
21756
|
+
meta,
|
|
21757
|
+
oldText,
|
|
21758
|
+
newText
|
|
21759
|
+
})
|
|
21760
|
+
};
|
|
21540
21761
|
}
|
|
21762
|
+
//#endregion
|
|
21763
|
+
//#region src/tools/omp/edit/edit-clipboard.ts
|
|
21541
21764
|
/**
|
|
21542
|
-
*
|
|
21765
|
+
* Look up (or lazily create) the clipboard register attached to a session.
|
|
21766
|
+
* Storage lives on `session.editClipboard` so it ages out exactly with the
|
|
21767
|
+
* session itself.
|
|
21543
21768
|
*/
|
|
21544
|
-
function
|
|
21545
|
-
|
|
21546
|
-
|
|
21547
|
-
new TextDecoder("utf-8");
|
|
21548
|
-
new LRUCache({ max: 10 });
|
|
21549
|
-
function shouldBlockAutoGeneratedFiles(activeSettings) {
|
|
21550
|
-
if (activeSettings) return activeSettings.get("edit.blockAutoGenerated");
|
|
21551
|
-
if (isSettingsInitialized()) return settings.get("edit.blockAutoGenerated");
|
|
21552
|
-
return getDefault("edit.blockAutoGenerated");
|
|
21769
|
+
function getEditClipboard(session) {
|
|
21770
|
+
session.editClipboard ??= {};
|
|
21771
|
+
return session.editClipboard;
|
|
21553
21772
|
}
|
|
21773
|
+
//#endregion
|
|
21774
|
+
//#region src/tools/omp/edit/hashline/block-resolver.ts
|
|
21554
21775
|
/**
|
|
21555
|
-
*
|
|
21556
|
-
*
|
|
21557
|
-
*
|
|
21558
|
-
*
|
|
21559
|
-
*
|
|
21560
|
-
* @param displayPath - Path to show in error messages
|
|
21561
|
-
* @param activeSettings - Session settings; falls back to global settings or the schema default
|
|
21776
|
+
* `blockRangeAt` runs a full synchronous tree-sitter parse of `text` per
|
|
21777
|
+
* call, and streaming previews re-resolve the same (text, line) every
|
|
21778
|
+
* streamed chunk. Memoize by content: identical text + line always yields the
|
|
21779
|
+
* same span. FIFO-bounded; hashing the text is orders of magnitude cheaper
|
|
21780
|
+
* than re-parsing it.
|
|
21562
21781
|
*/
|
|
21563
|
-
|
|
21564
|
-
|
|
21565
|
-
|
|
21566
|
-
|
|
21567
|
-
|
|
21782
|
+
const resolutionCache = /* @__PURE__ */ new Map();
|
|
21783
|
+
const RESOLUTION_CACHE_MAX = 512;
|
|
21784
|
+
const nativeBlockResolver = ({ path, text, line }) => {
|
|
21785
|
+
const key = `${Bun.hash(text).toString(36)}:${text.length}:${line}:${path}`;
|
|
21786
|
+
const cached = resolutionCache.get(key);
|
|
21787
|
+
if (cached !== void 0) return cached;
|
|
21788
|
+
const range = blockRangeAt({
|
|
21789
|
+
code: text,
|
|
21790
|
+
path,
|
|
21791
|
+
line
|
|
21792
|
+
});
|
|
21793
|
+
const result = range ? {
|
|
21794
|
+
start: range.startLine,
|
|
21795
|
+
end: range.endLine
|
|
21796
|
+
} : null;
|
|
21797
|
+
if (resolutionCache.size >= RESOLUTION_CACHE_MAX) {
|
|
21798
|
+
const oldest = resolutionCache.keys().next().value;
|
|
21799
|
+
if (oldest !== void 0) resolutionCache.delete(oldest);
|
|
21800
|
+
}
|
|
21801
|
+
resolutionCache.set(key, result);
|
|
21802
|
+
return result;
|
|
21803
|
+
};
|
|
21568
21804
|
//#endregion
|
|
21569
21805
|
//#region src/tools/omp/edit/hashline/filesystem.ts
|
|
21570
21806
|
/**
|
|
@@ -21714,8 +21950,10 @@ function resetNoopEdit(session, canonicalPath) {
|
|
|
21714
21950
|
guard.entries.delete(canonicalPath);
|
|
21715
21951
|
}
|
|
21716
21952
|
/**
|
|
21717
|
-
* Stable hash of the raw patch input. Bun's `Bun.hash` is
|
|
21953
|
+
* Stable hash of the raw patch input. Bun's `Bun.hash` is wyhash64 — fast,
|
|
21718
21954
|
* non-cryptographic, more than adequate for "is this the same payload?".
|
|
21955
|
+
* (xxHash64 is the separate `Bun.hash.xxHash64` namespace; the shim serves
|
|
21956
|
+
* both — second-impl-audit.md S-3.)
|
|
21719
21957
|
*/
|
|
21720
21958
|
function hashPatchInput(input) {
|
|
21721
21959
|
return Bun.hash(input).toString(16);
|
|
@@ -21870,7 +22108,7 @@ async function executeHashlineSingle(options) {
|
|
|
21870
22108
|
commitClipboard(clipboard, sessionClipboard);
|
|
21871
22109
|
if (sectionResult.op === "noop") {
|
|
21872
22110
|
const { count, escalate } = recordNoopEdit(options.session, sectionResult.canonicalPath, inputHash);
|
|
21873
|
-
if (escalate) throw new ToolError
|
|
22111
|
+
if (escalate) throw new ToolError(noChangeLoopDiagnostic(sectionResult.path, count));
|
|
21874
22112
|
return renderSection(sectionResult, prepared.section.path).toolResult;
|
|
21875
22113
|
}
|
|
21876
22114
|
resetNoopEdit(options.session, sectionResult.canonicalPath);
|
|
@@ -21885,7 +22123,7 @@ async function executeHashlineSingle(options) {
|
|
|
21885
22123
|
assertUniqueCanonicalPaths(prepared);
|
|
21886
22124
|
for (const entry of prepared) if (entry.isNoop) {
|
|
21887
22125
|
const { count, escalate } = recordNoopEdit(options.session, entry.canonicalPath, inputHash);
|
|
21888
|
-
throw escalate ? new ToolError
|
|
22126
|
+
throw escalate ? new ToolError(noChangeLoopDiagnostic(entry.section.path, count)) : new ToolError(noChangeDiagnostic(entry.section.path));
|
|
21889
22127
|
}
|
|
21890
22128
|
const rendered = [];
|
|
21891
22129
|
for (let i = 0; i < prepared.length; i++) {
|
|
@@ -21893,7 +22131,7 @@ async function executeHashlineSingle(options) {
|
|
|
21893
22131
|
commitClipboard(sectionStates[i], sessionClipboard);
|
|
21894
22132
|
if (sectionResult.op === "noop") {
|
|
21895
22133
|
const { count, escalate } = recordNoopEdit(options.session, sectionResult.canonicalPath, inputHash);
|
|
21896
|
-
throw escalate ? new ToolError
|
|
22134
|
+
throw escalate ? new ToolError(noChangeLoopDiagnostic(sectionResult.path, count)) : new ToolError(noChangeDiagnostic(sectionResult.path));
|
|
21897
22135
|
}
|
|
21898
22136
|
resetNoopEdit(options.session, sectionResult.canonicalPath);
|
|
21899
22137
|
rendered.push(renderSection(sectionResult, prepared[i].section.path));
|
|
@@ -22064,6 +22302,197 @@ function expandApplyPatchToEntries(params) {
|
|
|
22064
22302
|
}));
|
|
22065
22303
|
}
|
|
22066
22304
|
//#endregion
|
|
22305
|
+
//#region src/tools/omp/edit/dispatch.ts
|
|
22306
|
+
/**
|
|
22307
|
+
* Edit dispatch layer — path resolution + multi-entry/multi-file aggregation.
|
|
22308
|
+
*
|
|
22309
|
+
* Ported verbatim from upstream `packages/coding-agent/src/edit/index.ts`
|
|
22310
|
+
* (refs/oh-my-pi, 17.3.5): `resolveEditPath` (:77-93), `executeApplyPatchPerFile`
|
|
22311
|
+
* (:139-256) and `executeSinglePathEntries` (:258-365). Algorithm and user-facing
|
|
22312
|
+
* copy are byte-for-byte upstream; only the import surface was redirected to the
|
|
22313
|
+
* DSH-resident modules and the LSP-only surface was cut, per the established
|
|
22314
|
+
* omp/ adaptation conventions:
|
|
22315
|
+
* - `renderer` → `./details` (TUI renderer cut, plan.md 拍板#17): the
|
|
22316
|
+
* `EditToolDetails` / `EditToolPerFileResult` shapes live in `./details.ts`;
|
|
22317
|
+
* the upstream per-file `diagnostics` field went with the LSP layer.
|
|
22318
|
+
* - `resolvePlanPath` → DSH adapter shim (`edit/adapter/tools/plan-mode-guard`,
|
|
22319
|
+
* verbatim resolution minus local:// schemes) — same redirection the local
|
|
22320
|
+
* engines (modes/replace.ts, modes/patch.ts) already use.
|
|
22321
|
+
* - `LspBatchRequest` / `flushLspWritethroughBatch` → local type + no-op below.
|
|
22322
|
+
* DSH never constructs an outer batch request, so every `batchRequest` inside
|
|
22323
|
+
* the aggregators resolves to `undefined` and the flush branches stay
|
|
22324
|
+
* unreachable; the flow is kept verbatim so this file stays diffable
|
|
22325
|
+
* against refs.
|
|
22326
|
+
*/
|
|
22327
|
+
/**
|
|
22328
|
+
* Upstream flushes the LSP writethrough batch when aggregation stops early
|
|
22329
|
+
* (refs `lsp/writethrough.ts`). DSH has no LSP batch to flush — the engines
|
|
22330
|
+
* write through directly — so this is a documented no-op that keeps the
|
|
22331
|
+
* verbatim call sites compilable.
|
|
22332
|
+
*/
|
|
22333
|
+
async function flushLspWritethroughBatch(_id, _cwd, _signal) {}
|
|
22334
|
+
async function resolveEditPath(session, authoredPath, options) {
|
|
22335
|
+
if (!options.mustExist || isInternalUrlPath(authoredPath)) return authoredPath;
|
|
22336
|
+
try {
|
|
22337
|
+
await Bun.file(resolvePlanPath(session, authoredPath)).stat();
|
|
22338
|
+
return authoredPath;
|
|
22339
|
+
} catch (error) {
|
|
22340
|
+
if (!isEnoent(error) && !isEnotdir(error)) throw error;
|
|
22341
|
+
}
|
|
22342
|
+
return (await findUniqueWorkspaceSuffix(authoredPath, session.cwd, options.signal))?.displayPath ?? authoredPath;
|
|
22343
|
+
}
|
|
22344
|
+
/** Run apply_patch file operations and aggregate their multi-file result. */
|
|
22345
|
+
async function executeApplyPatchPerFile(fileEntries, outerBatchRequest, cwd, signal, onUpdate) {
|
|
22346
|
+
if (fileEntries.length === 1) return fileEntries[0].run(outerBatchRequest);
|
|
22347
|
+
const perFileResults = [];
|
|
22348
|
+
const contentTexts = [];
|
|
22349
|
+
let hasError = false;
|
|
22350
|
+
for (let i = 0; i < fileEntries.length; i++) {
|
|
22351
|
+
const { path, run } = fileEntries[i];
|
|
22352
|
+
const isLast = i === fileEntries.length - 1;
|
|
22353
|
+
const batchRequest = outerBatchRequest ? {
|
|
22354
|
+
id: outerBatchRequest.id,
|
|
22355
|
+
flush: isLast && outerBatchRequest.flush
|
|
22356
|
+
} : void 0;
|
|
22357
|
+
try {
|
|
22358
|
+
const result = await run(batchRequest);
|
|
22359
|
+
const details = result.details;
|
|
22360
|
+
perFileResults.push({
|
|
22361
|
+
path: details?.path ?? path,
|
|
22362
|
+
diff: details?.diff ?? "",
|
|
22363
|
+
firstChangedLine: details?.firstChangedLine,
|
|
22364
|
+
op: details?.op,
|
|
22365
|
+
move: details?.move,
|
|
22366
|
+
sourcePath: details?.sourcePath,
|
|
22367
|
+
meta: details?.meta,
|
|
22368
|
+
oldText: details?.oldText,
|
|
22369
|
+
newText: details?.newText,
|
|
22370
|
+
snapshotsPruned: details?.snapshotsPruned
|
|
22371
|
+
});
|
|
22372
|
+
const text = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
22373
|
+
if (text) contentTexts.push(text);
|
|
22374
|
+
} catch (err) {
|
|
22375
|
+
const errorText = err instanceof Error ? err.message : String(err);
|
|
22376
|
+
const displayErrorText = err instanceof MismatchError ? err.displayMessage : void 0;
|
|
22377
|
+
perFileResults.push({
|
|
22378
|
+
path,
|
|
22379
|
+
diff: "",
|
|
22380
|
+
isError: true,
|
|
22381
|
+
errorText,
|
|
22382
|
+
displayErrorText
|
|
22383
|
+
});
|
|
22384
|
+
contentTexts.push(`Error editing ${path}: ${errorText}`);
|
|
22385
|
+
hasError = true;
|
|
22386
|
+
if (i > 0) {
|
|
22387
|
+
const appliedPaths = fileEntries.slice(0, i).map((e) => e.path).join(", ");
|
|
22388
|
+
contentTexts.push(`Files already applied: ${appliedPaths}.`);
|
|
22389
|
+
}
|
|
22390
|
+
if (i + 1 < fileEntries.length) {
|
|
22391
|
+
const skippedPaths = fileEntries.slice(i + 1).map((e) => e.path).join(", ");
|
|
22392
|
+
contentTexts.push(`Files NOT applied: ${skippedPaths}; re-read the affected files and re-issue only the failed and unapplied files.`);
|
|
22393
|
+
}
|
|
22394
|
+
if (outerBatchRequest?.flush) await flushLspWritethroughBatch(outerBatchRequest.id, cwd, signal);
|
|
22395
|
+
break;
|
|
22396
|
+
}
|
|
22397
|
+
if (!isLast && onUpdate) onUpdate({
|
|
22398
|
+
content: [{
|
|
22399
|
+
type: "text",
|
|
22400
|
+
text: contentTexts.join("\n")
|
|
22401
|
+
}],
|
|
22402
|
+
details: {
|
|
22403
|
+
diff: perFileResults.map((r) => r.diff).filter(Boolean).join("\n"),
|
|
22404
|
+
firstChangedLine: perFileResults.find((r) => r.firstChangedLine)?.firstChangedLine,
|
|
22405
|
+
perFileResults: [...perFileResults]
|
|
22406
|
+
}
|
|
22407
|
+
});
|
|
22408
|
+
}
|
|
22409
|
+
return {
|
|
22410
|
+
content: [{
|
|
22411
|
+
type: "text",
|
|
22412
|
+
text: contentTexts.join("\n")
|
|
22413
|
+
}],
|
|
22414
|
+
details: pruneOversizedEditSnapshots({
|
|
22415
|
+
diff: perFileResults.map((r) => r.diff).filter(Boolean).join("\n"),
|
|
22416
|
+
firstChangedLine: perFileResults.find((r) => r.firstChangedLine)?.firstChangedLine,
|
|
22417
|
+
perFileResults
|
|
22418
|
+
}),
|
|
22419
|
+
...hasError ? { isError: true } : {}
|
|
22420
|
+
};
|
|
22421
|
+
}
|
|
22422
|
+
async function executeSinglePathEntries(path, runs, outerBatchRequest, onUpdate, cwd, signal) {
|
|
22423
|
+
if (runs.length === 1) return runs[0](outerBatchRequest);
|
|
22424
|
+
const contentTexts = [];
|
|
22425
|
+
const diffTexts = [];
|
|
22426
|
+
let firstChangedLine;
|
|
22427
|
+
let hasError = false;
|
|
22428
|
+
let metadataPath;
|
|
22429
|
+
let hasFirstOldText = false;
|
|
22430
|
+
let firstOldText;
|
|
22431
|
+
let hasLastNewText = false;
|
|
22432
|
+
let lastNewText;
|
|
22433
|
+
let snapshotsPruned = false;
|
|
22434
|
+
for (let i = 0; i < runs.length; i++) {
|
|
22435
|
+
const isLast = i === runs.length - 1;
|
|
22436
|
+
const batchRequest = outerBatchRequest ? {
|
|
22437
|
+
id: outerBatchRequest.id,
|
|
22438
|
+
flush: isLast && outerBatchRequest.flush
|
|
22439
|
+
} : void 0;
|
|
22440
|
+
try {
|
|
22441
|
+
const result = await runs[i](batchRequest);
|
|
22442
|
+
const details = result.details;
|
|
22443
|
+
if (details?.diff) diffTexts.push(details.diff);
|
|
22444
|
+
firstChangedLine ??= details?.firstChangedLine;
|
|
22445
|
+
if (details?.path) metadataPath ??= details.path;
|
|
22446
|
+
if (details && "oldText" in details && !hasFirstOldText) {
|
|
22447
|
+
firstOldText = details.oldText;
|
|
22448
|
+
hasFirstOldText = true;
|
|
22449
|
+
}
|
|
22450
|
+
if (details && "newText" in details) {
|
|
22451
|
+
lastNewText = details.newText;
|
|
22452
|
+
hasLastNewText = true;
|
|
22453
|
+
}
|
|
22454
|
+
if (details?.snapshotsPruned) snapshotsPruned = true;
|
|
22455
|
+
const text = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
22456
|
+
if (text) contentTexts.push(text);
|
|
22457
|
+
} catch (err) {
|
|
22458
|
+
const errorText = err instanceof Error ? err.message : String(err);
|
|
22459
|
+
contentTexts.push(`Error editing ${path} (entry ${i + 1} of ${runs.length}): ${errorText}`);
|
|
22460
|
+
if (i > 0) contentTexts.push(i === 1 ? `Entry 1 was already applied.` : `Entries 1-${i} were already applied.`);
|
|
22461
|
+
if (i + 1 < runs.length) contentTexts.push((i + 2 === runs.length ? `Entry ${runs.length} was NOT applied` : `Entries ${i + 2}-${runs.length} were NOT applied`) + `; re-read the file and re-issue only the failed and unapplied entries.`);
|
|
22462
|
+
hasError = true;
|
|
22463
|
+
if (outerBatchRequest?.flush) await flushLspWritethroughBatch(outerBatchRequest.id, cwd, signal);
|
|
22464
|
+
break;
|
|
22465
|
+
}
|
|
22466
|
+
if (!isLast && onUpdate) onUpdate({
|
|
22467
|
+
content: [{
|
|
22468
|
+
type: "text",
|
|
22469
|
+
text: contentTexts.join("\n")
|
|
22470
|
+
}],
|
|
22471
|
+
details: {
|
|
22472
|
+
diff: diffTexts.join("\n"),
|
|
22473
|
+
firstChangedLine
|
|
22474
|
+
},
|
|
22475
|
+
...hasError ? { isError: true } : {}
|
|
22476
|
+
});
|
|
22477
|
+
}
|
|
22478
|
+
return {
|
|
22479
|
+
content: [{
|
|
22480
|
+
type: "text",
|
|
22481
|
+
text: contentTexts.join("\n")
|
|
22482
|
+
}],
|
|
22483
|
+
details: pruneOversizedEditSnapshots({
|
|
22484
|
+
diff: diffTexts.join("\n"),
|
|
22485
|
+
firstChangedLine,
|
|
22486
|
+
path: metadataPath ?? path,
|
|
22487
|
+
...snapshotsPruned ? { snapshotsPruned: true } : {
|
|
22488
|
+
...hasFirstOldText ? { oldText: firstOldText } : {},
|
|
22489
|
+
...hasLastNewText ? { newText: lastNewText } : {}
|
|
22490
|
+
}
|
|
22491
|
+
}),
|
|
22492
|
+
...hasError ? { isError: true } : {}
|
|
22493
|
+
};
|
|
22494
|
+
}
|
|
22495
|
+
//#endregion
|
|
22067
22496
|
//#region src/tools/shared/sandbox-policy.ts
|
|
22068
22497
|
/**
|
|
22069
22498
|
* Resolve the per-call mutation policy for the given exec context, or
|
|
@@ -22079,21 +22508,54 @@ function resolveSandboxPolicy(ctx, exec) {
|
|
|
22079
22508
|
return policy.resolve(request) ?? void 0;
|
|
22080
22509
|
}
|
|
22081
22510
|
//#endregion
|
|
22511
|
+
//#region src/tools/edit/adapter/prompts/tools/patch.md
|
|
22512
|
+
var patch_default = "Patches files given diff hunks. Primary tool for existing-file edits.\n\n<instruction>\n**Hunk Headers:**\n- `@@` — bare header when context lines unique\n- `@@ $ANCHOR` — anchor copied verbatim from file (full line or unique substring)\n**Anchor Selection:**\n1. Prefer bare `@@` when context lines alone are unique; otherwise choose highly specific anchor copied from file:\n - full function signature\n - class declaration\n - unique string literal/error message\n - config key with uncommon name\n2. On \"Found multiple matches\": add context lines, use multiple hunks with separate anchors, or use longer anchor substring\n**Context Lines:**\nUse enough ` `-prefixed lines to make match unique (usually 2–8)\nWhen editing structured blocks (nested braces, tags, indented regions), include opening and closing lines so edit stays inside block\n</instruction>\n\n<parameters>\n```ts\n// Input is { path: string, edits: Entry[] }. `path` is required and applies to every entry.\ntype Entry =\n // Diff is one or more hunks for the top-level path.\n // - Each hunk begins with \"@@\" (anchor optional).\n // - Each hunk body only has lines starting with ' ' | '+' | '-'.\n // - Each hunk includes at least one change (+ or -).\n | { op: \"update\", diff: string }\n // Diff is full file content, no prefixes.\n | { op: \"create\", diff: string }\n // No diff for delete.\n | { op: \"delete\" }\n // New path for update+move from the top-level path.\n | { op: \"update\", rename: string, diff: string }\n```\n</parameters>\n\n<output>\nReturns success/failure; on failure, error message indicates:\n- \"Found multiple matches\" — anchor/context not unique enough\n- \"No match found\" — context lines don't exist in file (wrong content or stale read)\n- Syntax errors in diff format\n</output>\n\n<critical>\n- You MUST read the target file before editing\n- You MUST copy anchors and context lines verbatim (including whitespace)\n- You NEVER use anchors as comments (no line numbers, location labels, placeholders like `@@ @@`)\n- You NEVER place new lines outside the intended block\n- If edit fails or breaks structure, you MUST re-read the file and produce a new patch from current content — you NEVER retry the same diff\n- NEVER use edit to fix indentation, whitespace, or reformat code. Formatting is a single command run once at the end (`bun fmt`, `cargo fmt`, `prettier --write`, etc.) — not N individual edits. If you see inconsistent indentation after an edit, leave it; the formatter will fix all of it in one pass.\n</critical>\n\n<avoid>\n- Generic anchors: `import`, `export`, `describe`, `function`, `const`\n- Repeating same addition in multiple hunks (duplicate blocks)\n- Full-file overwrites for minor changes (acceptable for major restructures or short files)\n</avoid>\n";
|
|
22513
|
+
//#endregion
|
|
22514
|
+
//#region src/tools/edit/adapter/prompts/tools/apply-patch.md
|
|
22515
|
+
var apply_patch_default = "Edit files: `apply_patch` shell command.\n\n`apply_patch`: stripped-down, file-oriented diff; easy to parse, safe to apply.\n\nEnvelope:\n```\n*** Begin Patch\n[ one or more file sections ]\n*** End Patch\n```\nContains file operations. Each MUST have an action header:\n\n`*** Add File: <path>`: create file; every following line `+` (initial contents).\n\n`*** Delete File: <path>`: remove existing file; nothing follows.\n\n`*** Update File: <path>`: patch existing file in place; optional immediate `*** Move to: <new path>` renames it; then one or more `@@` hunks (optional hunk header). Hunk lines start with space, `-`, or `+`.\n\nContext: default 3 code lines immediately before and after each change. Changes within 3 lines: do NOT duplicate first change's context-after lines as second change's context-before lines. If 3 lines do not uniquely identify code in the file, use `@@` with its class/function; if one `@@` plus 3 context lines still cannot uniquely identify repeated code in a class/function, use multiple `@@` lines to reach it:\n```\n@@ class BaseClass\n[3 lines of pre-context]\n- [old_code]\n+ [new_code]\n[3 lines of post-context]\n```\n```\n@@ class BaseClass\n@@ def method():\n[3 lines of pre-context]\n- [old_code]\n+ [new_code]\n[3 lines of post-context]\n```\n\nGrammar:\n```\nPatch := Begin { FileOp } End\nBegin := \"*** Begin Patch\" NEWLINE\nEnd := \"*** End Patch\" NEWLINE\nFileOp := AddFile | DeleteFile | UpdateFile\nAddFile := \"*** Add File: \" path NEWLINE { \"+\" line NEWLINE }\nDeleteFile := \"*** Delete File: \" path NEWLINE\nUpdateFile := \"*** Update File: \" path NEWLINE [ MoveTo ] { Hunk }\nMoveTo := \"*** Move to: \" newPath NEWLINE\nHunk := \"@@\" [ header ] NEWLINE { HunkLine } [ \"*** End of File\" NEWLINE ]\nHunkLine := (\" \" | \"-\" | \"+\") text NEWLINE\n```\n\nFull patches may combine operations:\n```\n*** Begin Patch\n*** Add File: hello.txt\n+Hello world\n*** Update File: src/app.py\n*** Move to: src/main.py\n@@ def greet():\n-print(\"Hi\")\n+print(\"Hello, world!\")\n*** Delete File: obsolete.txt\n*** End Patch\n```\n\nMUST use Add/Delete/Update header; new-file lines MUST start `+`; file references relative, NEVER absolute.\n";
|
|
22516
|
+
//#endregion
|
|
22517
|
+
//#region src/tools/edit/adapter/prompts/tools/replace.md
|
|
22518
|
+
var replace_default = "Single file string replacement; fuzzy whitespace matching.\n\n<instruction>\n- MUST use smallest `old_string` uniquely identifying change.\n- Nonunique `old_string` → MUST add context or use `replace_all: true` for all occurrences.\n- Rename a string across file → use `replace_all: true`.\n- SHOULD edit existing files, not create new.\n</instruction>\n\n<output>\nSuccess/failure status.\nSuccess: file modified in place; replacement applied.\nFailure — e.g., `old_string` absent or multiple matches without `replace_all: true`: error describes issue.\n</output>\n\n<critical>\n- MUST read file at least once in conversation before editing. Tool errors on edit before read.\n</critical>\n\n<bash-alternatives>\nReplace content-addressed — identify change by text.\n\nPattern-addressed bulk changes: bash more efficient:\n\n|Operation|Command|\n|---|---|\n|Regex replace|`sd 'pattern' 'replacement' file`|\n|Bulk replace across files|`sd 'pattern' 'replacement' **/*.ts`|\n\nUse Replace when content identifies location; `ast_edit` for structure-aware codemods.\nFor in-place edits prefer Replace or `write` — diff preview and fuzzy matching.\n</bash-alternatives>\n";
|
|
22519
|
+
//#endregion
|
|
22520
|
+
//#region src/tools/hashline/engine/prompt.md
|
|
22521
|
+
var prompt_default = "Line-anchored patch language: name original lines/gaps to replace, insert, cut, or paste; then give new content. `:` headers take `+` body rows; colonless paste `PUT`, `CUT`, `REM`, `MV` take none.\r\n\r\n<headers>\r\nSection: `[PATH#TAG]`; `TAG`: 4-hex snapshot from latest `read`/`search`, REQUIRED each section. New files: `write`; hashline edits existing files only.\r\n</headers>\r\n\r\n<ops>\r\n`PUT N.=M:`: replace original inclusive lines N–M with body.\r\n`PUT N*:`: replace syntactic block beginning N; closing line resolved.\r\n`PUT <N:` insert body rows before line N (`PUT <1:` = file head).\r\n`PUT >N:` insert body rows after line N (`PUT >$:` = file tail).\r\n`PUT >N*:`: insert after block N's end, at sibling depth. Append inside block: `PUT >M:`.\r\n`PUT <N @name` / `PUT >N @name` paste register `@name` at the gap before/after line N; omit `@name` for the anonymous register.\r\n`PUT N.=M @name` / `PUT N* @name` paste `@name` over the range / resolved block; `@name` required here.\r\n`CUT N.=M` / `CUT N*`: delete and capture inclusive lines N–M / block N; anonymous or given `@name`.\r\n`REM`: delete section file. `MV DEST`: move/rename (quote paths with spaces); prior edits apply to source, final content to `DEST`.\r\nSingle line: `PUT N.=N:` / `CUT N.=N`. Ranges name original inclusive touched lines; body length irrelevant.\r\n</ops>\r\n\r\n<body-rows>\r\nOnly below `:` headers. Row: verbatim `+TEXT` (leading whitespace preserved); `+`: blank. NEVER `-old`, bare, or context rows: range deletes; body is final content. Keep line: exclude it from every range. Literal initial `-`/`+`: `- item` → `+- item`; `+ item` → `++ item`.\r\n</body-rows>\r\n\r\n<rules>\r\n- Numbers and `#TAG`: latest `read`/`search` `LINE:TEXT`; numbers are original, never shifted by hunks.\r\n- Each edit renumbers and changes `#TAG` → next numbers from edit response or fresh `read`.\r\n- Touch displayed lines only; undisplayed hunks REJECTED. Far from read window: re-`read`; confirm construct.\r\n- Elisions UNSEEN: `…`, `..`, collapsed `N-M:` rows. NEVER hunk in/across one; `read` first.\r\n- NEVER start/end range mid-expression or mid-block.\r\n- Ranges: changed lines only; NEVER widen over keepers. Non-adjacent changes: separate hunks.\r\n- Whole construct: `PUT N*:`; internal lines: `PUT N.=M:`.\r\n- `PUT N*:` resolves exactly node N. Leading decorators/attributes/doc-comments are separate nodes: point N at first decorator to include both. Standalone line-comments never swept: use `PUT N.=M:`.\r\n- Block ops: opening line of multi-line construct, NEVER closer, last line, bare inner statement. One statement: plain `PUT N.=N:` / `CUT N.=N` / `PUT >N:`. At closer: `PUT >M:`.\r\n- Markdown headings are block openers. Block op on `##`/`###`: whole section through deeper headings to next same/higher heading. After section `PUT >N*:`: end body with blank line to separate next heading.\r\n- Pure addition: `PUT <N:` / `PUT >N:`, NEVER widened `PUT N.=M:`.\r\n- Move: `CUT`+`PUT`; `CUT 5.=9 @fn` → `@fn`, `PUT >40 @fn` pastes. Single call-local move: unlabeled `CUT` + `PUT >40`. Named registers persist across edit calls.\r\n- NEVER format/restyle with this tool; run project formatter.\r\n</rules>\r\n\r\n<example>\r\n`read` output shape:\r\n```\r\n[greet.py#A1B2]\r\n1:def greet(name):\r\n2: msg = \"Hello, \" + name\r\n3: print(msg)\r\n4:greet(\"world\")\r\n```\r\n\r\nEdit, then move:\r\n```\r\n[greet.py#A1B2]\r\nPUT 1.=3:\r\n+def greet(name):\r\n+ print(f\"Hi, {name}\")\r\nMV lib/greet.py\r\n```\r\n\r\nMarkdown bullets — file receives `- task`:\r\n```\r\n[PLAN.md#A1B2]\r\nPUT >2:\r\n+- task\r\n+ - nested task\r\n```\r\n\r\nMove `greet` to sibling file via named register; flows across sections:\r\n```\r\n[greet.py#A1B2]\r\nCUT 1* @fn\r\n[other.py#3C4D]\r\nPUT <1 @fn\r\n```\r\n\r\n`PUT 1*:` resolves lines 1–3 (`def` through `print(msg)`); line 4 separate, remains:\r\n```\r\n[greet.py#A1B2]\r\nPUT 1*:\r\n+def greet(name):\r\n+ print(f\"Hello, {name}\")\r\n```\r\n\r\nDecorator/doc-comment separate block: point N at decorator to include both; anchoring `def` line 2 orphans `@cache`:\r\n```\r\n[svc.py#C3D4]\r\nPUT 1*:\r\n+@cache\r\n+def load(key):\r\n+ return store[key]\r\n```\r\n</example>\r\n\r\n<anti-patterns>\r\n# WRONG — empty `PUT` to delete. RIGHT: `CUT 4.=4`\r\nPUT 4.=4:\r\n\r\n# WRONG — range sized to the post-edit content. RIGHT: `PUT 1.=1:` (body length irrelevant)\r\nPUT 1.=2:\r\n+def greet(name):\r\n\r\n# WRONG — `-` rows / bare context lines do not exist; the range deletes, the body is only new content.\r\nPUT 3.=3:\r\n msg = \"Hello, \" + name\r\n- print(msg)\r\n+ return msg\r\n# RIGHT\r\nPUT 3.=3:\r\n+ return msg\r\n\r\n# WRONG — pure insertion as a widened `PUT`: retyped keepers get dropped (here line 4).\r\nPUT 2.=4:\r\n+ msg = \"Hello, \" + name\r\n+ extra = compute(name)\r\n+ print(msg)\r\n# RIGHT — touch nothing you keep.\r\nPUT >2:\r\n+ extra = compute(name)\r\n\r\n# WRONG — `PUT >N*:` anchored on the closing delimiter / last visible line. RIGHT: plain `PUT >M:`\r\nPUT >3*:\r\n+after()\r\n# RIGHT\r\nPUT >3:\r\n+after()\r\n\r\n# WRONG — body rows under register PUT; register pastes take no body. RIGHT: bodyless `PUT >20 @fn`.\r\nPUT >20 @fn:\r\n+function f() {}\r\n</anti-patterns>\r\n\r\n<critical>\r\n1. RE-GROUND AFTER EVERY EDIT: edits renumber and change `#TAG`; take next numbers from edit response or fresh `read`. Stale tag/surprise: STOP; re-`read`.\r\n2. RANGES TIGHT: changed lines only. Whole construct: `PUT N*:`.\r\n3. BODY FINAL CONTENT: every row starts `+`; Markdown bullet: `+- item`, not `- item`.\r\n</critical>\r\n";
|
|
22522
|
+
//#endregion
|
|
22082
22523
|
//#region src/tools/edit/adapter/index.ts
|
|
22524
|
+
/**
|
|
22525
|
+
* S-14c:patch.md 是 OMP JSON patch 入口的 verbatim 提示词,其 <parameters>
|
|
22526
|
+
* 块教模型发 `{ path, edits: Entry[] }` 形状;而 DSH schema(registerEdit)只收
|
|
22527
|
+
* `patch: string`(unified-diff 文本 + `file_path` 定位文件),create/delete/
|
|
22528
|
+
* rename 走 `input` 的 `*** Begin Patch` 信封(apply_patch 模式)。此处把该块
|
|
22529
|
+
* 重写为与 schema 一致的参数说明,其余段落(hunk/anchor 语法、critical、avoid)
|
|
22530
|
+
* 保持 verbatim。sanitize 本体在 shared/omp-prompt.ts(不动)。
|
|
22531
|
+
*/
|
|
22532
|
+
function rewritePatchParametersForSchema(description) {
|
|
22533
|
+
const block = /<parameters>[\s\S]*?<\/parameters>/.exec(description);
|
|
22534
|
+
if (!block) return description;
|
|
22535
|
+
return description.replace(block[0], [
|
|
22536
|
+
"<parameters>",
|
|
22537
|
+
"- `file_path` (string, required): the file to patch.",
|
|
22538
|
+
"- `patch` (string, required): unified diff applied to that file — one or more hunks, each beginning with `@@` (anchor optional); hunk body lines start with a space (context), `+` (add), or `-` (remove); every hunk must contain at least one change.",
|
|
22539
|
+
"- Creating, deleting or renaming files is not available in patch mode: pass an `input` string with a Codex `*** Begin Patch` envelope instead (`*** Add File:`, `*** Delete File:`, `*** Update File:` with `*** Move to:`).",
|
|
22540
|
+
"</parameters>"
|
|
22541
|
+
].join("\n"));
|
|
22542
|
+
}
|
|
22083
22543
|
/** OMP 原版按模式渲染的 edit 描述(prompts/tools/*.md verbatim + hashline 引擎 prompt.md)。 */
|
|
22084
22544
|
const EDIT_MODE_DESCRIPTIONS = {
|
|
22085
|
-
replace: renderOmpPrompt(sanitizeReplacePrompt(
|
|
22086
|
-
patch: renderOmpPrompt(sanitizePatchPrompt(
|
|
22087
|
-
apply_patch: renderOmpPrompt(sanitizeApplyPatchPrompt(
|
|
22088
|
-
hashline: renderOmpPrompt(sanitizeHashlinePrompt(
|
|
22545
|
+
replace: renderOmpPrompt(sanitizeReplacePrompt(replace_default), {}),
|
|
22546
|
+
patch: rewritePatchParametersForSchema(renderOmpPrompt(sanitizePatchPrompt(patch_default), {})),
|
|
22547
|
+
apply_patch: renderOmpPrompt(sanitizeApplyPatchPrompt(apply_patch_default), {}),
|
|
22548
|
+
hashline: renderOmpPrompt(sanitizeHashlinePrompt(prompt_default), {})
|
|
22089
22549
|
};
|
|
22090
22550
|
/** Build the OMP ToolSession facade over a DSH exec context. */
|
|
22091
22551
|
function createToolSession$4(exec, cfg) {
|
|
22092
|
-
|
|
22552
|
+
const session = {
|
|
22093
22553
|
cwd: exec?.agent?.session?.header?.cwd ?? process.cwd(),
|
|
22094
22554
|
settings: new Settings(cfg, getDefault),
|
|
22095
22555
|
hasEditTool: true
|
|
22096
22556
|
};
|
|
22557
|
+
attachOmpSessionState(session, exec?.agent?.session);
|
|
22558
|
+
return session;
|
|
22097
22559
|
}
|
|
22098
22560
|
/** File-write channel for DSH (方案A, 2026-08-27): every edit mode funnels
|
|
22099
22561
|
* through `ctx.fs` with the per-call sandbox policy resolved from the session
|
|
@@ -22134,6 +22596,140 @@ function runPatchEntry(options) {
|
|
|
22134
22596
|
});
|
|
22135
22597
|
}
|
|
22136
22598
|
/**
|
|
22599
|
+
* 把 dispatch 聚合结果转成 DSH 输出。成功:保留引擎 per-entry 摘要原文;
|
|
22600
|
+
* 失败:抛出完整聚合文案,并在“失败即末条目”的空档补上游同款未应用指引。
|
|
22601
|
+
*
|
|
22602
|
+
* 上游 executeSinglePathEntries / executeApplyPatchPerFile 的 NOT-applied 指引
|
|
22603
|
+
* 只覆盖失败条目**之后**的未应用条目;失败发生在末条目时(无后续条目)不再
|
|
22604
|
+
* 输出,而失败条目本身同样未被应用——此处按上游同款句式补点名(A-7 契约:
|
|
22605
|
+
* 错误信息三要素 already applied / NOT applied / re-issue only 齐全)。
|
|
22606
|
+
*/
|
|
22607
|
+
function toEditToolResult(result, entryCount, failedEntry, failedPath) {
|
|
22608
|
+
const text = result.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
22609
|
+
if (result.isError) {
|
|
22610
|
+
const supplement = entryCount > 1 && failedEntry !== void 0 && failedEntry === entryCount ? failedPath !== void 0 ? `\nFiles NOT applied: ${failedPath}; re-read the affected files and re-issue only the failed and unapplied files.` : `\nEntry ${failedEntry} was NOT applied; re-read the file and re-issue only the failed and unapplied entries.` : "";
|
|
22611
|
+
throw new Error(`${text || "edit failed"}${supplement}`);
|
|
22612
|
+
}
|
|
22613
|
+
return { text };
|
|
22614
|
+
}
|
|
22615
|
+
/**
|
|
22616
|
+
* S-13:edit.fuzzyThreshold 解析——上游 settings 面原样放行显式 0/负值
|
|
22617
|
+
* (0 = 接受任意置信度的最佳匹配,refs edit/index.ts:112 同款语义),仅
|
|
22618
|
+
* undefined/非数值回退引擎默认 0.95。env 层(上游 PI_EDIT_FUZZY* 环境变量
|
|
22619
|
+
* 串解析)有意不移植:DSH 无该环境变量面,面板字段是唯一入口。
|
|
22620
|
+
*/
|
|
22621
|
+
function resolveFuzzyThreshold(raw) {
|
|
22622
|
+
return typeof raw === "number" ? raw : DEFAULT_FUZZY_THRESHOLD;
|
|
22623
|
+
}
|
|
22624
|
+
/** 完整执行链路(defineTool.execute 与单测共用,镜像 read 的 executeReadTool)。 */
|
|
22625
|
+
async function executeEditTool(exec, cfg, args, ctx) {
|
|
22626
|
+
const session = createToolSession$4(exec, cfg);
|
|
22627
|
+
const writethrough = ctx ? createWritethrough(ctx, exec) : writethroughNoop;
|
|
22628
|
+
const cwd = session.cwd;
|
|
22629
|
+
const signal = exec?.signal;
|
|
22630
|
+
const allowFuzzy = session.settings.get("edit.fuzzyMatch") !== false;
|
|
22631
|
+
const fuzzyThreshold = resolveFuzzyThreshold(session.settings.get("edit.fuzzyThreshold"));
|
|
22632
|
+
try {
|
|
22633
|
+
if (typeof args.input === "string" && args.input.trim().length > 0) {
|
|
22634
|
+
if (/^\s*\*\*\* Begin Patch/.test(args.input)) {
|
|
22635
|
+
const entries = expandApplyPatchToEntries({ input: args.input });
|
|
22636
|
+
const resolvedTargets = /* @__PURE__ */ new Map();
|
|
22637
|
+
const resolveOnce = (authoredPath, mustExist) => {
|
|
22638
|
+
let pending = resolvedTargets.get(authoredPath);
|
|
22639
|
+
if (!pending) {
|
|
22640
|
+
pending = resolveEditPath(session, authoredPath, {
|
|
22641
|
+
mustExist,
|
|
22642
|
+
signal
|
|
22643
|
+
});
|
|
22644
|
+
resolvedTargets.set(authoredPath, pending);
|
|
22645
|
+
}
|
|
22646
|
+
return pending;
|
|
22647
|
+
};
|
|
22648
|
+
let failedFile;
|
|
22649
|
+
return toEditToolResult(await executeApplyPatchPerFile(entries.map((entry, idx) => {
|
|
22650
|
+
const { path: entryPath, ...patchParams } = entry;
|
|
22651
|
+
return {
|
|
22652
|
+
path: entryPath,
|
|
22653
|
+
run: async () => {
|
|
22654
|
+
try {
|
|
22655
|
+
const targetPath = await resolveOnce(entryPath, patchParams.op !== "create");
|
|
22656
|
+
return await runPatchEntry({
|
|
22657
|
+
session,
|
|
22658
|
+
path: targetPath,
|
|
22659
|
+
entry: patchParams,
|
|
22660
|
+
signal,
|
|
22661
|
+
writethrough,
|
|
22662
|
+
allowFuzzy,
|
|
22663
|
+
fuzzyThreshold
|
|
22664
|
+
});
|
|
22665
|
+
} catch (err) {
|
|
22666
|
+
failedFile = idx + 1;
|
|
22667
|
+
throw err;
|
|
22668
|
+
}
|
|
22669
|
+
}
|
|
22670
|
+
};
|
|
22671
|
+
}), void 0, cwd, signal), entries.length, failedFile, failedFile !== void 0 ? entries[failedFile - 1]?.path : void 0);
|
|
22672
|
+
}
|
|
22673
|
+
return { text: toText$4(await executeHashlineSingle({
|
|
22674
|
+
session,
|
|
22675
|
+
input: args.input,
|
|
22676
|
+
signal,
|
|
22677
|
+
writethrough
|
|
22678
|
+
})) };
|
|
22679
|
+
}
|
|
22680
|
+
const filePath = args.file_path ?? args.path ?? "";
|
|
22681
|
+
if (!filePath || typeof filePath !== "string" || filePath.trim().length === 0) throw new Error("file_path must be a non-empty string");
|
|
22682
|
+
if (typeof args.patch === "string" && args.patch.trim().length > 0) return { text: toText$4(await runPatchEntry({
|
|
22683
|
+
session,
|
|
22684
|
+
path: await resolveEditPath(session, filePath, {
|
|
22685
|
+
mustExist: true,
|
|
22686
|
+
signal
|
|
22687
|
+
}),
|
|
22688
|
+
entry: {
|
|
22689
|
+
op: "update",
|
|
22690
|
+
diff: args.patch
|
|
22691
|
+
},
|
|
22692
|
+
signal,
|
|
22693
|
+
writethrough,
|
|
22694
|
+
allowFuzzy,
|
|
22695
|
+
fuzzyThreshold,
|
|
22696
|
+
allowCreateOverwrite: true
|
|
22697
|
+
})) };
|
|
22698
|
+
const editsInput = Array.isArray(args.edits) && args.edits.length > 0 ? args.edits : typeof args.old_string === "string" ? [{
|
|
22699
|
+
oldText: args.old_string,
|
|
22700
|
+
newText: String(args.new_string ?? "")
|
|
22701
|
+
}] : [];
|
|
22702
|
+
if (editsInput.length === 0) throw new Error("edit: provide old_string/new_string, edits[], patch, or input");
|
|
22703
|
+
const targetPath = await resolveEditPath(session, filePath, {
|
|
22704
|
+
mustExist: true,
|
|
22705
|
+
signal
|
|
22706
|
+
});
|
|
22707
|
+
let failedEntry;
|
|
22708
|
+
return toEditToolResult(await executeSinglePathEntries(targetPath, editsInput.map((edit, idx) => async () => {
|
|
22709
|
+
try {
|
|
22710
|
+
return await executeReplace({
|
|
22711
|
+
session,
|
|
22712
|
+
path: targetPath,
|
|
22713
|
+
params: {
|
|
22714
|
+
old_string: edit.oldText,
|
|
22715
|
+
new_string: edit.newText,
|
|
22716
|
+
replace_all: args.replace_all ?? false
|
|
22717
|
+
},
|
|
22718
|
+
signal,
|
|
22719
|
+
allowFuzzy,
|
|
22720
|
+
fuzzyThreshold,
|
|
22721
|
+
writethrough
|
|
22722
|
+
});
|
|
22723
|
+
} catch (err) {
|
|
22724
|
+
failedEntry = idx + 1;
|
|
22725
|
+
throw err;
|
|
22726
|
+
}
|
|
22727
|
+
}), void 0, void 0, cwd, signal), editsInput.length, failedEntry);
|
|
22728
|
+
} finally {
|
|
22729
|
+
persistOmpSessionState(exec?.agent?.session, session);
|
|
22730
|
+
}
|
|
22731
|
+
}
|
|
22732
|
+
/**
|
|
22137
22733
|
* Register the edit tool. Mode is resolved per call from the argument shape
|
|
22138
22734
|
* (matching OMP's mode dispatch):
|
|
22139
22735
|
* - `input` present → hashline, or apply_patch when it starts with `***`
|
|
@@ -22203,86 +22799,7 @@ function registerEdit(ctx, getConfig) {
|
|
|
22203
22799
|
}]
|
|
22204
22800
|
},
|
|
22205
22801
|
async execute(args, exec) {
|
|
22206
|
-
|
|
22207
|
-
const writethrough = createWritethrough(ctx, exec);
|
|
22208
|
-
const signal = exec?.signal;
|
|
22209
|
-
const allowFuzzy = session.settings.get("edit.fuzzyMatch") !== false;
|
|
22210
|
-
const fuzzyThresholdRaw = session.settings.get("edit.fuzzyThreshold");
|
|
22211
|
-
const fuzzyThreshold = typeof fuzzyThresholdRaw === "number" && fuzzyThresholdRaw > 0 ? fuzzyThresholdRaw : .95;
|
|
22212
|
-
if (typeof args.input === "string" && args.input.trim().length > 0) {
|
|
22213
|
-
if (/^\s*\*\*\* Begin Patch/.test(args.input)) {
|
|
22214
|
-
const entries = expandApplyPatchToEntries({ input: args.input });
|
|
22215
|
-
const texts = [];
|
|
22216
|
-
for (const entry of entries) {
|
|
22217
|
-
const result = await runPatchEntry({
|
|
22218
|
-
session,
|
|
22219
|
-
path: entry.path,
|
|
22220
|
-
entry: {
|
|
22221
|
-
op: entry.op,
|
|
22222
|
-
rename: entry.rename,
|
|
22223
|
-
diff: entry.diff
|
|
22224
|
-
},
|
|
22225
|
-
signal,
|
|
22226
|
-
writethrough,
|
|
22227
|
-
allowFuzzy,
|
|
22228
|
-
fuzzyThreshold
|
|
22229
|
-
});
|
|
22230
|
-
texts.push(result.content.filter((b) => b.type === "text").map((b) => b.text).join("\n"));
|
|
22231
|
-
if (result.isError) throw new Error(texts.join("\n") || "apply_patch failed");
|
|
22232
|
-
}
|
|
22233
|
-
return { text: texts.join("\n") };
|
|
22234
|
-
}
|
|
22235
|
-
return { text: toText$4(await executeHashlineSingle({
|
|
22236
|
-
session,
|
|
22237
|
-
input: args.input,
|
|
22238
|
-
signal,
|
|
22239
|
-
writethrough
|
|
22240
|
-
})) };
|
|
22241
|
-
}
|
|
22242
|
-
const filePath = args.file_path ?? args.path ?? "";
|
|
22243
|
-
if (!filePath || typeof filePath !== "string" || filePath.trim().length === 0) throw new Error("file_path must be a non-empty string");
|
|
22244
|
-
if (typeof args.patch === "string" && args.patch.trim().length > 0) return { text: toText$4(await runPatchEntry({
|
|
22245
|
-
session,
|
|
22246
|
-
path: filePath,
|
|
22247
|
-
entry: {
|
|
22248
|
-
op: "update",
|
|
22249
|
-
diff: args.patch
|
|
22250
|
-
},
|
|
22251
|
-
signal,
|
|
22252
|
-
writethrough,
|
|
22253
|
-
allowFuzzy,
|
|
22254
|
-
fuzzyThreshold,
|
|
22255
|
-
allowCreateOverwrite: true
|
|
22256
|
-
})) };
|
|
22257
|
-
const editsInput = Array.isArray(args.edits) && args.edits.length > 0 ? args.edits : typeof args.old_string === "string" ? [{
|
|
22258
|
-
oldText: args.old_string,
|
|
22259
|
-
newText: String(args.new_string ?? "")
|
|
22260
|
-
}] : [];
|
|
22261
|
-
if (editsInput.length === 0) throw new Error("edit: provide old_string/new_string, edits[], patch, or input");
|
|
22262
|
-
let totalReplacements = 0;
|
|
22263
|
-
for (const edit of editsInput) {
|
|
22264
|
-
const result = await executeReplace({
|
|
22265
|
-
session,
|
|
22266
|
-
path: filePath,
|
|
22267
|
-
params: {
|
|
22268
|
-
old_string: edit.oldText,
|
|
22269
|
-
new_string: edit.newText,
|
|
22270
|
-
replace_all: args.replace_all ?? false
|
|
22271
|
-
},
|
|
22272
|
-
signal,
|
|
22273
|
-
allowFuzzy,
|
|
22274
|
-
fuzzyThreshold,
|
|
22275
|
-
writethrough
|
|
22276
|
-
});
|
|
22277
|
-
const text = toText$4(result);
|
|
22278
|
-
const occ = /replaced (\d+) occurrences/.exec(text);
|
|
22279
|
-
totalReplacements += occ ? Number(occ[1]) : text.startsWith("Successfully replaced") ? 1 : 0;
|
|
22280
|
-
if (result.isError) {
|
|
22281
|
-
const first = result.content.find((b) => b.type === "text");
|
|
22282
|
-
throw new Error(first?.text ?? "edit failed");
|
|
22283
|
-
}
|
|
22284
|
-
}
|
|
22285
|
-
return { text: `Edited ${filePath}: ${totalReplacements} replacement(s)` };
|
|
22802
|
+
return executeEditTool(exec, getConfig(), args, ctx);
|
|
22286
22803
|
}
|
|
22287
22804
|
}));
|
|
22288
22805
|
}
|
|
@@ -22504,7 +23021,7 @@ async function parsePathSpecs(rawEntries, cwd) {
|
|
|
22504
23021
|
for (const entry of rawEntries) {
|
|
22505
23022
|
const internalSplit = splitInternalUrlSel(entry);
|
|
22506
23023
|
if (internalSplit.sel !== void 0) {
|
|
22507
|
-
if (!isReadSelectorGrammar(internalSplit.sel)) throw new ToolError
|
|
23024
|
+
if (!isReadSelectorGrammar(internalSplit.sel)) throw new ToolError(`path entry "${entry}" has an invalid selector ":${internalSplit.sel}" — use ":N-M" line ranges, ":raw"/":conflicts", a range plus ":raw", or percent-encode a literal ":" as %3A`);
|
|
22508
23025
|
specs.push({
|
|
22509
23026
|
original: entry,
|
|
22510
23027
|
clean: internalSplit.path,
|
|
@@ -22519,8 +23036,8 @@ async function parsePathSpecs(rawEntries, cwd) {
|
|
|
22519
23036
|
let ranges;
|
|
22520
23037
|
if (!literalFilesystemMatch && split.sel) {
|
|
22521
23038
|
const parsed = parseLineRanges(split.sel);
|
|
22522
|
-
if (!parsed) throw new ToolError
|
|
22523
|
-
if (hasGlobPathChars(split.path)) throw new ToolError
|
|
23039
|
+
if (!parsed) throw new ToolError(`path entry "${entry}" — only line-range selectors like ":50-100" are supported (no ":raw"/":conflicts")`);
|
|
23040
|
+
if (hasGlobPathChars(split.path)) throw new ToolError(`Line-range selector requires a single file, not a glob: ${entry}`);
|
|
22524
23041
|
clean = split.path;
|
|
22525
23042
|
ranges = parsed;
|
|
22526
23043
|
}
|
|
@@ -22691,7 +23208,7 @@ function jsMatchedLineIndexes(content, lines, pattern, ignoreCase, multiline) {
|
|
|
22691
23208
|
regex = new RegExp(pattern, flags);
|
|
22692
23209
|
} catch (err) {
|
|
22693
23210
|
const message = err instanceof Error ? err.message : String(err);
|
|
22694
|
-
throw new ToolError
|
|
23211
|
+
throw new ToolError(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
|
|
22695
23212
|
}
|
|
22696
23213
|
if (!multiline) {
|
|
22697
23214
|
const out = [];
|
|
@@ -22765,7 +23282,7 @@ async function nativeChunkedLineIndexes(dir, resourceIdx, content, pattern, igno
|
|
|
22765
23282
|
lineRegex = new RegExp(pattern, ignoreCase ? "i" : "");
|
|
22766
23283
|
} catch (err) {
|
|
22767
23284
|
const message = err instanceof Error ? err.message : String(err);
|
|
22768
|
-
throw new ToolError
|
|
23285
|
+
throw new ToolError(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
|
|
22769
23286
|
}
|
|
22770
23287
|
lineRegex.lastIndex = 0;
|
|
22771
23288
|
if (lineRegex.test(line)) indexes.push(i);
|
|
@@ -22953,9 +23470,9 @@ async function resolveInternalSearchInputs(opts) {
|
|
|
22953
23470
|
for (let idx = 0; idx < paths.length; idx++) {
|
|
22954
23471
|
const rawPath = paths[idx];
|
|
22955
23472
|
if (!rawPath || opts.archiveDisplayMap.has(rawPath) || !internalRouter.canHandle(rawPath)) continue;
|
|
22956
|
-
if (hasGlobPathChars(/^ssh:\/\//i.test(rawPath) ? rawPath.replace(/^ssh:\/\/[^/]*/i, "") : rawPath)) throw new ToolError
|
|
23473
|
+
if (hasGlobPathChars(/^ssh:\/\//i.test(rawPath) ? rawPath.replace(/^ssh:\/\/[^/]*/i, "") : rawPath)) throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
22957
23474
|
let resource = await internalRouter.resolve(rawPath, context);
|
|
22958
|
-
if (resource.isDirectory && !resource.sourcePath) throw new ToolError
|
|
23475
|
+
if (resource.isDirectory && !resource.sourcePath) throw new ToolError(`search cannot recurse the directory listing at ${rawPath}; search a specific file under it (e.g. ${rawPath.replace(/\/+$/, "")}/<file>) or read ${rawPath} to list its entries`);
|
|
22959
23476
|
if (resource.sourcePath) {
|
|
22960
23477
|
paths[idx] = resource.sourcePath;
|
|
22961
23478
|
if (resource.immutable) immutableSourcePaths.add(path.resolve(resource.sourcePath));
|
|
@@ -23019,10 +23536,10 @@ var GrepTool = class {
|
|
|
23019
23536
|
async execute(_toolCallId, params, signal, _onUpdate, _toolContext) {
|
|
23020
23537
|
const { pattern, path: rawPath, case: caseSensitive, gitignore, skip } = params;
|
|
23021
23538
|
return untilAborted(signal, async () => {
|
|
23022
|
-
if (!pattern.trim()) throw new ToolError
|
|
23539
|
+
if (!pattern.trim()) throw new ToolError("Pattern must not be empty");
|
|
23023
23540
|
const normalizedPattern = pattern;
|
|
23024
23541
|
const normalizedSkip = skip === void 0 || skip === null ? 0 : Number.isFinite(skip) ? Math.floor(skip) : NaN;
|
|
23025
|
-
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) throw new ToolError
|
|
23542
|
+
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) throw new ToolError("Skip must be a non-negative number");
|
|
23026
23543
|
const scopedPaths = toPathList(rawPath);
|
|
23027
23544
|
const pathSpecs = await parsePathSpecs(await expandDelimitedPathEntries(scopedPaths.length > 0 ? scopedPaths : ["."], this.session.cwd), this.session.cwd);
|
|
23028
23545
|
const materializedExternalPaths = /* @__PURE__ */ new Map();
|
|
@@ -23054,7 +23571,7 @@ var GrepTool = class {
|
|
|
23054
23571
|
const searchablePaths = internalResolution.paths;
|
|
23055
23572
|
const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
|
|
23056
23573
|
const rangesByAbsPath = /* @__PURE__ */ new Map();
|
|
23057
|
-
if (archiveUnreadable.length > 0 && searchablePaths.length === archiveUnreadable.length && virtualResources.length === 0) throw new ToolError
|
|
23574
|
+
if (archiveUnreadable.length > 0 && searchablePaths.length === archiveUnreadable.length && virtualResources.length === 0) throw new ToolError(`Cannot search archive member(s): ${archiveUnreadable.join(", ")}. Read the member with \`read <archive>:<member>\` and inspect the returned text, or pass a UTF-8 text member.`);
|
|
23058
23575
|
const normalizedContextBefore = this.#contextOverride ?? this.session.settings.get("grep.contextBefore");
|
|
23059
23576
|
const normalizedContextAfter = this.#contextOverride ?? this.session.settings.get("grep.contextAfter");
|
|
23060
23577
|
const ignoreCase = !(caseSensitive ?? true);
|
|
@@ -23104,8 +23621,8 @@ var GrepTool = class {
|
|
|
23104
23621
|
if (resolved === spec.clean && !archiveDisplayMap.has(resolved)) {
|
|
23105
23622
|
const absKey = path.resolve(resolveReadPath(resolved, this.session.cwd));
|
|
23106
23623
|
const stats = await (0, fs_promises_shim_exports.stat)(absKey).catch(() => null);
|
|
23107
|
-
if (!stats) throw new ToolError
|
|
23108
|
-
if (!stats.isFile()) throw new ToolError
|
|
23624
|
+
if (!stats) throw new ToolError(`Path not found for line-range selector: ${spec.original}`);
|
|
23625
|
+
if (!stats.isFile()) throw new ToolError(`Line-range selector requires a single file: ${spec.original} is a directory`);
|
|
23109
23626
|
mergeRangesInto(rangesByAbsPath, absKey, spec.ranges);
|
|
23110
23627
|
} else mergeRangesInto(rangesByAbsPath, path.resolve(resolved), spec.ranges);
|
|
23111
23628
|
}
|
|
@@ -23122,7 +23639,7 @@ var GrepTool = class {
|
|
|
23122
23639
|
}
|
|
23123
23640
|
if (missingPaths.length > 0 && missingPaths.length === searchablePaths.length && virtualResources.length === 0) {
|
|
23124
23641
|
const archiveHint = archiveUnreadable.length > 0 ? ` (archive members were not searchable: ${archiveUnreadable.join(", ")})` : "";
|
|
23125
|
-
throw new ToolError
|
|
23642
|
+
throw new ToolError(`Path not found: ${missingPaths.join(", ")}; list each target in the semicolon-delimited \`path\`${archiveHint}`);
|
|
23126
23643
|
}
|
|
23127
23644
|
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
23128
23645
|
const effectiveOutputMode = GrepOutputMode.Content;
|
|
@@ -23217,16 +23734,16 @@ var GrepTool = class {
|
|
|
23217
23734
|
}
|
|
23218
23735
|
}
|
|
23219
23736
|
} catch (err) {
|
|
23220
|
-
if (err instanceof Error && /^regex(?: parse)? error/i.test(err.message)) throw new ToolError
|
|
23221
|
-
if (err instanceof Error && err.message.includes("Aborted: Timeout")) throw new ToolError
|
|
23737
|
+
if (err instanceof Error && /^regex(?: parse)? error/i.test(err.message)) throw new ToolError(err.message.replace(/^regex(?: parse)? error:?\s*/i, "Invalid regex: "));
|
|
23738
|
+
if (err instanceof Error && err.message.includes("Aborted: Timeout")) throw new ToolError(`Grep timed out after ${SEARCH_GREP_TIMEOUT_MS / 1e3}s; narrow paths or pattern, or scope with \`glob\` first`);
|
|
23222
23739
|
throw err;
|
|
23223
23740
|
}
|
|
23224
23741
|
let virtualResult;
|
|
23225
23742
|
try {
|
|
23226
23743
|
virtualResult = await searchVirtualResources(virtualResources, normalizedPattern, ignoreCase, effectiveMultiline, normalizedContextBefore, normalizedContextAfter, INTERNAL_TOTAL_CAP, signal);
|
|
23227
23744
|
} catch (err) {
|
|
23228
|
-
if (err instanceof Error && /^regex(?: parse)? error/i.test(err.message)) throw new ToolError
|
|
23229
|
-
if (err instanceof SyntaxError) throw new ToolError
|
|
23745
|
+
if (err instanceof Error && /^regex(?: parse)? error/i.test(err.message)) throw new ToolError(err.message.replace(/^regex(?: parse)? error:?\s*/i, "Invalid regex: "));
|
|
23746
|
+
if (err instanceof SyntaxError) throw new ToolError(`Invalid regex: ${err.message}`);
|
|
23230
23747
|
throw err;
|
|
23231
23748
|
}
|
|
23232
23749
|
result = mergeGrepResults(result, virtualResult, nativeMaxCount);
|
|
@@ -23483,6 +24000,41 @@ function toText$3(result) {
|
|
|
23483
24000
|
return text;
|
|
23484
24001
|
}
|
|
23485
24002
|
/**
|
|
24003
|
+
* 把 OMP AgentToolResult 转成 DSH grep 工具输出。
|
|
24004
|
+
*
|
|
24005
|
+
* 上游 session 层会把 `formatOutputNotice(details.meta)` 的限量/截断提示追加
|
|
24006
|
+
* 进模型可见文本(wrapToolWithMetaNotice → appendOutputNotice,refs
|
|
24007
|
+
* tools/output-meta.ts:586);DSH 无 session 层,适配层在此补上(A-6),
|
|
24008
|
+
* 否则 grep 的列宽截断/限量对模型静默。
|
|
24009
|
+
*/
|
|
24010
|
+
function toGrepToolResult(result) {
|
|
24011
|
+
const text = toText$3(result);
|
|
24012
|
+
const details = result.details ?? {};
|
|
24013
|
+
return {
|
|
24014
|
+
text: text + formatOutputNotice(details.meta),
|
|
24015
|
+
...typeof details.matchCount === "number" ? { matchCount: details.matchCount } : {},
|
|
24016
|
+
...typeof details.fileCount === "number" ? { fileCount: details.fileCount } : {},
|
|
24017
|
+
...Array.isArray(details.files) ? { files: details.files } : {},
|
|
24018
|
+
...typeof details.scopePath === "string" ? { scopePath: details.scopePath } : {},
|
|
24019
|
+
...details.truncated === true ? { truncated: true } : {}
|
|
24020
|
+
};
|
|
24021
|
+
}
|
|
24022
|
+
/**
|
|
24023
|
+
* 完整执行链路(defineTool.execute 与单测共用)。
|
|
24024
|
+
*
|
|
24025
|
+
* `ctx` 参数与 read 的 executeReadTool 对齐(grep 引擎不需要 image bridge /
|
|
24026
|
+
* session state,仅占位保持签名一致)。
|
|
24027
|
+
*/
|
|
24028
|
+
async function executeGrepTool(exec, cfg, args, ctx) {
|
|
24029
|
+
const tool = new GrepTool(createToolSession$3(exec, cfg));
|
|
24030
|
+
const params = { pattern: String(args.pattern ?? "") };
|
|
24031
|
+
if (args.path !== void 0 && args.path !== null && args.path !== "") params.path = String(args.path);
|
|
24032
|
+
if (typeof args.case === "boolean") params.case = args.case;
|
|
24033
|
+
if (typeof args.gitignore === "boolean") params.gitignore = args.gitignore;
|
|
24034
|
+
if (typeof args.skip === "number") params.skip = args.skip;
|
|
24035
|
+
return toGrepToolResult(await tool.execute("grep", params, exec.signal));
|
|
24036
|
+
}
|
|
24037
|
+
/**
|
|
23486
24038
|
* Register the grep tool (OMP-native `pattern` + `path` parameters; the `path`
|
|
23487
24039
|
* accepts an inline `:N-M` selector and may be a semicolon-delimited list).
|
|
23488
24040
|
*/
|
|
@@ -23543,23 +24095,7 @@ function registerGrep(ctx, getConfig) {
|
|
|
23543
24095
|
})
|
|
23544
24096
|
},
|
|
23545
24097
|
async execute(args, exec) {
|
|
23546
|
-
|
|
23547
|
-
const params = { pattern: String(args.pattern ?? "") };
|
|
23548
|
-
if (args.path !== void 0 && args.path !== null && args.path !== "") params.path = String(args.path);
|
|
23549
|
-
if (typeof args.case === "boolean") params.case = args.case;
|
|
23550
|
-
if (typeof args.gitignore === "boolean") params.gitignore = args.gitignore;
|
|
23551
|
-
if (typeof args.skip === "number") params.skip = args.skip;
|
|
23552
|
-
const result = await tool.execute("grep", params, exec.signal);
|
|
23553
|
-
const text = toText$3(result);
|
|
23554
|
-
const details = result.details ?? {};
|
|
23555
|
-
return {
|
|
23556
|
-
text,
|
|
23557
|
-
...typeof details.matchCount === "number" ? { matchCount: details.matchCount } : {},
|
|
23558
|
-
...typeof details.fileCount === "number" ? { fileCount: details.fileCount } : {},
|
|
23559
|
-
...Array.isArray(details.files) ? { files: details.files } : {},
|
|
23560
|
-
...typeof details.scopePath === "string" ? { scopePath: details.scopePath } : {},
|
|
23561
|
-
...details.truncated === true ? { truncated: true } : {}
|
|
23562
|
-
};
|
|
24098
|
+
return executeGrepTool(exec, getConfig(), args, ctx);
|
|
23563
24099
|
},
|
|
23564
24100
|
presentCall: (args) => ({
|
|
23565
24101
|
card: "generic",
|
|
@@ -23633,7 +24169,7 @@ var GlobTool = class {
|
|
|
23633
24169
|
const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
23634
24170
|
const rawPatterns = (this.#customOps ? effectivePaths : await expandDelimitedPathEntries(effectivePaths, this.session.cwd, { splitter: parseFindPattern })).map((input) => normalizePathLikeInput(input).replace(/\\/g, "/"));
|
|
23635
24171
|
const aliasResolvedPatterns = this.#rootPathAlias ? rawPatterns.map((pattern) => /^\/+$/.test(pattern) ? "." : pattern) : rawPatterns;
|
|
23636
|
-
if (aliasResolvedPatterns.some((pattern) => /^\/+$/.test(pattern))) throw new ToolError
|
|
24172
|
+
if (aliasResolvedPatterns.some((pattern) => /^\/+$/.test(pattern))) throw new ToolError("Searching from root directory '/' is not allowed");
|
|
23637
24173
|
const internalRouter = InternalUrlRouter.instance();
|
|
23638
24174
|
const normalizedPatterns = [];
|
|
23639
24175
|
for (const rawPattern of aliasResolvedPatterns) {
|
|
@@ -23641,8 +24177,8 @@ var GlobTool = class {
|
|
|
23641
24177
|
normalizedPatterns.push(rawPattern);
|
|
23642
24178
|
continue;
|
|
23643
24179
|
}
|
|
23644
|
-
if (isSshUrl(rawPattern)) throw new ToolError
|
|
23645
|
-
if (hasGlobPathChars(rawPattern)) throw new ToolError
|
|
24180
|
+
if (isSshUrl(rawPattern)) throw new ToolError(`find cannot operate on a remote ssh:// path: ${rawPattern}. ssh:// has no local file to glob; use \`read ${rawPattern}\` to list or inspect the remote path.`);
|
|
24181
|
+
if (hasGlobPathChars(rawPattern)) throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPattern}`);
|
|
23646
24182
|
const resource = await internalRouter.resolve(rawPattern, {
|
|
23647
24183
|
cwd: this.session.cwd,
|
|
23648
24184
|
settings: this.session.settings,
|
|
@@ -23651,15 +24187,15 @@ var GlobTool = class {
|
|
|
23651
24187
|
skills: this.session.skills,
|
|
23652
24188
|
pathOnly: true
|
|
23653
24189
|
});
|
|
23654
|
-
if (!resource.sourcePath) throw new ToolError
|
|
24190
|
+
if (!resource.sourcePath) throw new ToolError(`Cannot find internal URL without a backing file: ${rawPattern}`);
|
|
23655
24191
|
normalizedPatterns.push(resource.sourcePath);
|
|
23656
24192
|
}
|
|
23657
|
-
if (normalizedPatterns.some((pattern) => pattern.length === 0)) throw new ToolError
|
|
24193
|
+
if (normalizedPatterns.some((pattern) => pattern.length === 0)) throw new ToolError("`path` must contain non-empty globs or paths");
|
|
23658
24194
|
let missingPaths = [];
|
|
23659
24195
|
let effectivePatterns = normalizedPatterns;
|
|
23660
24196
|
if (normalizedPatterns.length > 1 && !this.#customOps) {
|
|
23661
24197
|
const partition = await partitionExistingPaths(normalizedPatterns, this.session.cwd, parseFindPattern);
|
|
23662
|
-
if (partition.valid.length === 0) throw new ToolError
|
|
24198
|
+
if (partition.valid.length === 0) throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
23663
24199
|
effectivePatterns = partition.valid;
|
|
23664
24200
|
missingPaths = partition.missing;
|
|
23665
24201
|
}
|
|
@@ -23678,9 +24214,9 @@ var GlobTool = class {
|
|
|
23678
24214
|
};
|
|
23679
24215
|
})()];
|
|
23680
24216
|
const scopePath = multiPattern?.scopePath ?? formatScopePath(targets[0].searchPath);
|
|
23681
|
-
for (const target of targets) if (target.searchPath === "/") throw new ToolError
|
|
24217
|
+
for (const target of targets) if (target.searchPath === "/") throw new ToolError("Searching from root directory '/' is not allowed");
|
|
23682
24218
|
const requestedLimit = limit ?? DEFAULT_LIMIT;
|
|
23683
|
-
if (!Number.isFinite(requestedLimit) || requestedLimit <= 0) throw new ToolError
|
|
24219
|
+
if (!Number.isFinite(requestedLimit) || requestedLimit <= 0) throw new ToolError("Limit must be a positive number");
|
|
23684
24220
|
const effectiveLimit = Math.min(MAX_LIMIT, Math.max(1, Math.floor(requestedLimit)));
|
|
23685
24221
|
const includeHidden = hidden ?? true;
|
|
23686
24222
|
const useGitignore = gitignore ?? true;
|
|
@@ -23734,7 +24270,7 @@ var GlobTool = class {
|
|
|
23734
24270
|
const customOps = this.#customOps;
|
|
23735
24271
|
const perTarget = await Promise.all(targets.map(async (target) => {
|
|
23736
24272
|
if (!await customOps.exists(target.searchPath)) {
|
|
23737
|
-
if (isSingle) throw new ToolError
|
|
24273
|
+
if (isSingle) throw new ToolError(`Path not found: ${scopePath}`);
|
|
23738
24274
|
return [];
|
|
23739
24275
|
}
|
|
23740
24276
|
if (!target.hasGlob && customOps.stat) {
|
|
@@ -23795,7 +24331,7 @@ var GlobTool = class {
|
|
|
23795
24331
|
stat = await fs.promises.stat(target.searchPath);
|
|
23796
24332
|
} catch (err) {
|
|
23797
24333
|
if (isEnoent(err) || hasFsCode(err, "ENAMETOOLONG")) {
|
|
23798
|
-
if (isSingle) throw new ToolError
|
|
24334
|
+
if (isSingle) throw new ToolError(`Path not found: ${scopePath}`);
|
|
23799
24335
|
return [];
|
|
23800
24336
|
}
|
|
23801
24337
|
throw err;
|
|
@@ -23805,7 +24341,7 @@ var GlobTool = class {
|
|
|
23805
24341
|
mtime: stat.mtimeMs
|
|
23806
24342
|
}];
|
|
23807
24343
|
if (!stat.isDirectory()) {
|
|
23808
|
-
if (isSingle) throw new ToolError
|
|
24344
|
+
if (isSingle) throw new ToolError(`Path is not a directory: ${target.searchPath}`);
|
|
23809
24345
|
return [];
|
|
23810
24346
|
}
|
|
23811
24347
|
try {
|
|
@@ -23884,6 +24420,39 @@ function toText$2(result) {
|
|
|
23884
24420
|
return text;
|
|
23885
24421
|
}
|
|
23886
24422
|
/**
|
|
24423
|
+
* 把 OMP AgentToolResult 转成 DSH glob 工具输出。
|
|
24424
|
+
*
|
|
24425
|
+
* 上游 session 层会把 `formatOutputNotice(details.meta)` 的限量/截断提示追加
|
|
24426
|
+
* 进模型可见文本(wrapToolWithMetaNotice → appendOutputNotice,refs
|
|
24427
|
+
* tools/output-meta.ts:586);DSH 无 session 层,适配层在此补上(A-6),
|
|
24428
|
+
* 否则 glob 超限截断对模型静默。
|
|
24429
|
+
*/
|
|
24430
|
+
function toGlobToolResult(result, args) {
|
|
24431
|
+
const text = toText$2(result);
|
|
24432
|
+
const details = result.details ?? {};
|
|
24433
|
+
const notice = formatOutputNotice(details.meta);
|
|
24434
|
+
return {
|
|
24435
|
+
path: String(details.scopePath ?? args.path ?? ""),
|
|
24436
|
+
text: text + notice,
|
|
24437
|
+
...typeof details.fileCount === "number" ? { fileCount: details.fileCount } : {},
|
|
24438
|
+
...typeof details.truncated === "boolean" ? { truncated: details.truncated } : {}
|
|
24439
|
+
};
|
|
24440
|
+
}
|
|
24441
|
+
/**
|
|
24442
|
+
* 完整执行链路(defineTool.execute 与单测共用)。
|
|
24443
|
+
*
|
|
24444
|
+
* `ctx` 参数与 read 的 executeReadTool 对齐(glob 引擎不需要 image bridge /
|
|
24445
|
+
* session state,仅占位保持签名一致)。
|
|
24446
|
+
*/
|
|
24447
|
+
async function executeGlobTool(exec, cfg, args, ctx) {
|
|
24448
|
+
return toGlobToolResult(await new GlobTool(createToolSession$2(exec, cfg)).execute("glob", {
|
|
24449
|
+
path: args.path !== void 0 ? String(args.path) : void 0,
|
|
24450
|
+
hidden: args.hidden,
|
|
24451
|
+
gitignore: args.gitignore,
|
|
24452
|
+
limit: args.limit
|
|
24453
|
+
}, exec.signal), args);
|
|
24454
|
+
}
|
|
24455
|
+
/**
|
|
23887
24456
|
* Register the glob tool (OMP native `path` + optional `hidden`/`gitignore`/
|
|
23888
24457
|
* `limit` parameters).
|
|
23889
24458
|
*/
|
|
@@ -23936,20 +24505,7 @@ function registerGlob(ctx, getConfig) {
|
|
|
23936
24505
|
})
|
|
23937
24506
|
},
|
|
23938
24507
|
async execute(args, exec) {
|
|
23939
|
-
|
|
23940
|
-
path: args.path !== void 0 ? String(args.path) : void 0,
|
|
23941
|
-
hidden: args.hidden,
|
|
23942
|
-
gitignore: args.gitignore,
|
|
23943
|
-
limit: args.limit
|
|
23944
|
-
}, exec.signal);
|
|
23945
|
-
const text = toText$2(result);
|
|
23946
|
-
const details = result.details ?? {};
|
|
23947
|
-
return {
|
|
23948
|
-
path: String(details.scopePath ?? args.path ?? ""),
|
|
23949
|
-
text,
|
|
23950
|
-
...typeof details.fileCount === "number" ? { fileCount: details.fileCount } : {},
|
|
23951
|
-
...typeof details.truncated === "boolean" ? { truncated: details.truncated } : {}
|
|
23952
|
-
};
|
|
24508
|
+
return executeGlobTool(exec, getConfig(), args, ctx);
|
|
23953
24509
|
},
|
|
23954
24510
|
presentCall: (args) => ({
|
|
23955
24511
|
card: "generic",
|
|
@@ -24135,13 +24691,13 @@ var AstEditTool = class {
|
|
|
24135
24691
|
async execute(_toolCallId, params, signal, _onUpdate, _context) {
|
|
24136
24692
|
return untilAborted(signal, async () => {
|
|
24137
24693
|
const ops = params.ops.map((entry, index) => {
|
|
24138
|
-
if (entry.pat.length === 0) throw new ToolError
|
|
24694
|
+
if (entry.pat.length === 0) throw new ToolError(`\`ops[${index}].pat\` must be a non-empty pattern`);
|
|
24139
24695
|
return [entry.pat, entry.out];
|
|
24140
24696
|
});
|
|
24141
|
-
if (ops.length === 0) throw new ToolError
|
|
24697
|
+
if (ops.length === 0) throw new ToolError("`ops` must include at least one op entry");
|
|
24142
24698
|
const seenPatterns = /* @__PURE__ */ new Set();
|
|
24143
24699
|
for (const [pat] of ops) {
|
|
24144
|
-
if (seenPatterns.has(pat)) throw new ToolError
|
|
24700
|
+
if (seenPatterns.has(pat)) throw new ToolError(`Duplicate rewrite pattern: ${pat}`);
|
|
24145
24701
|
seenPatterns.add(pat);
|
|
24146
24702
|
}
|
|
24147
24703
|
const normalizedRewrites = Object.fromEntries(ops);
|
|
@@ -24156,7 +24712,7 @@ var AstEditTool = class {
|
|
|
24156
24712
|
skills: this.session.skills,
|
|
24157
24713
|
resolveExternalUrl: async (rawPath) => {
|
|
24158
24714
|
if (!parseReadUrlTarget(rawPath)) return void 0;
|
|
24159
|
-
throw new ToolError
|
|
24715
|
+
throw new ToolError(`Cannot rewrite external URL: ${rawPath}. Use \`read\` or \`search\` to inspect fetched web content; ast_edit only applies to local files.`);
|
|
24160
24716
|
}
|
|
24161
24717
|
});
|
|
24162
24718
|
const result = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
|
|
@@ -24588,10 +25144,10 @@ var AstGrepTool = class {
|
|
|
24588
25144
|
async execute(_toolCallId, params, signal, _onUpdate, _context) {
|
|
24589
25145
|
return untilAborted(signal, async () => {
|
|
24590
25146
|
const pattern = params.pat.trim();
|
|
24591
|
-
if (pattern.length === 0) throw new ToolError
|
|
25147
|
+
if (pattern.length === 0) throw new ToolError("`pat` must be a non-empty pattern");
|
|
24592
25148
|
const patterns = [pattern];
|
|
24593
25149
|
const skip = params.skip === void 0 ? 0 : Math.floor(params.skip);
|
|
24594
|
-
if (!Number.isFinite(skip) || skip < 0) throw new ToolError
|
|
25150
|
+
if (!Number.isFinite(skip) || skip < 0) throw new ToolError("skip must be a non-negative number");
|
|
24595
25151
|
const scopedPaths = toPathList(params.path);
|
|
24596
25152
|
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = await resolveToolSearchScope({
|
|
24597
25153
|
rawPaths: scopedPaths.length > 0 ? scopedPaths : ["."],
|
|
@@ -24834,7 +25390,6 @@ const inject = [
|
|
|
24834
25390
|
"fs"
|
|
24835
25391
|
];
|
|
24836
25392
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
24837
|
-
const CD_PREFIX_PATTERN = /^cd[ \t]+((?:[^&\\\n\r]|\\.)+?)[ \t]*&&[ \t]*/;
|
|
24838
25393
|
const ANONYMOUS_SESSION = "anonymous";
|
|
24839
25394
|
function abortError() {
|
|
24840
25395
|
const error = new HarnessError("tool call aborted", TOOL_ABORTED);
|
|
@@ -24864,7 +25419,7 @@ function buildForeground(result, wallTimeMs, timeoutMs) {
|
|
|
24864
25419
|
return {
|
|
24865
25420
|
kind: "foreground",
|
|
24866
25421
|
exitCode: result.exitCode ?? null,
|
|
24867
|
-
timedOut: result.
|
|
25422
|
+
timedOut: result.timedOut ?? false,
|
|
24868
25423
|
aborted: false,
|
|
24869
25424
|
timeoutMs: timeoutMs ?? null,
|
|
24870
25425
|
wallTimeMs,
|
|
@@ -24925,7 +25480,6 @@ function apply(ctx, config = {}) {
|
|
|
24925
25480
|
let cfg = resolveConfig(config);
|
|
24926
25481
|
setRuntimeLogger(ctx.logger);
|
|
24927
25482
|
sweepStaleSpillFiles();
|
|
24928
|
-
setBlockAutoGenerated(cfg.editBlockAutoGenerated);
|
|
24929
25483
|
let disposeModeSensitive = [];
|
|
24930
25484
|
const registerModeSensitive = () => {
|
|
24931
25485
|
for (const dispose of disposeModeSensitive) dispose();
|
|
@@ -24937,7 +25491,6 @@ function apply(ctx, config = {}) {
|
|
|
24937
25491
|
registerModeSensitive();
|
|
24938
25492
|
installBashPlusSettings(ctx, config, (current) => {
|
|
24939
25493
|
cfg = current();
|
|
24940
|
-
setBlockAutoGenerated(cfg.editBlockAutoGenerated);
|
|
24941
25494
|
registerModeSensitive();
|
|
24942
25495
|
});
|
|
24943
25496
|
ctx.systemPrompt.section({
|
|
@@ -25087,20 +25640,20 @@ function apply(ctx, config = {}) {
|
|
|
25087
25640
|
const commandEnv = normalizeBashEnv(args.env);
|
|
25088
25641
|
let workdir = args.workdir;
|
|
25089
25642
|
if (workdir === void 0) {
|
|
25090
|
-
const
|
|
25091
|
-
if (
|
|
25092
|
-
workdir =
|
|
25093
|
-
command = command
|
|
25643
|
+
const cd = extractCdWorkdir(command);
|
|
25644
|
+
if (cd !== null) {
|
|
25645
|
+
workdir = cd.workdir;
|
|
25646
|
+
command = cd.command;
|
|
25094
25647
|
}
|
|
25095
25648
|
}
|
|
25096
|
-
const commandCwd = workdir === void 0 ? state.cwd : path.
|
|
25649
|
+
const commandCwd = workdir === void 0 ? state.cwd : path.resolve(state.cwd, expandTilde$1(workdir));
|
|
25097
25650
|
if (cfg.interceptorEnabled) {
|
|
25098
25651
|
const availableTools = [...new Set(DEFAULT_BASH_INTERCEPTOR_RULES.map((rule) => rule.tool))].filter((tool) => ctx.tools.get(tool, exec.agent) !== void 0);
|
|
25099
25652
|
const interception = checkBashInterception(command, availableTools);
|
|
25100
25653
|
if (interception.block) throw new Error(interception.message);
|
|
25101
25654
|
}
|
|
25102
25655
|
const rawTimeoutMs = args.timeoutMs ?? cfg.defaultTimeoutMs;
|
|
25103
|
-
const timeoutMs = rawTimeoutMs === 0 ?
|
|
25656
|
+
const timeoutMs = rawTimeoutMs === 0 ? 0 : Math.max(1e3, Math.min(rawTimeoutMs, cfg.maxTimeoutMs));
|
|
25104
25657
|
const env = {
|
|
25105
25658
|
...ctx.shellEnv.collect(exec),
|
|
25106
25659
|
...commandEnv
|
|
@@ -25115,7 +25668,7 @@ function apply(ctx, config = {}) {
|
|
|
25115
25668
|
sessionId,
|
|
25116
25669
|
command,
|
|
25117
25670
|
cwd: commandCwd,
|
|
25118
|
-
timeoutMs,
|
|
25671
|
+
timeoutMs: args.timeoutMs ?? 0,
|
|
25119
25672
|
env,
|
|
25120
25673
|
config: cfg
|
|
25121
25674
|
});
|
|
@@ -25159,7 +25712,7 @@ function apply(ctx, config = {}) {
|
|
|
25159
25712
|
};
|
|
25160
25713
|
}
|
|
25161
25714
|
if (cfg.autoBackgroundMs > 0 && jobs !== void 0 && backgroundSlotsAvailable() && !exec.signal.aborted) {
|
|
25162
|
-
const autoBgWaitMs = timeoutMs ===
|
|
25715
|
+
const autoBgWaitMs = timeoutMs === 0 ? cfg.autoBackgroundMs : Math.max(0, Math.min(cfg.autoBackgroundMs, timeoutMs - 1e3));
|
|
25163
25716
|
let managed;
|
|
25164
25717
|
let id;
|
|
25165
25718
|
try {
|