@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.
Files changed (155) hide show
  1. package/lib/abort-D1_Z5Ut5.mjs +51 -0
  2. package/lib/abort-dIkZBV7Q.mjs +51 -0
  3. package/lib/amazon-bedrock-B0nxT06F.mjs +917 -0
  4. package/lib/amazon-bedrock-WQhvjHYu.mjs +917 -0
  5. package/lib/anthropic-CJCauvYz.mjs +3115 -0
  6. package/lib/anthropic-CZghe_9U.mjs +3115 -0
  7. package/lib/anthropic-sCMH7JID.mjs +262 -0
  8. package/lib/anthropic-tTntD8BD.mjs +262 -0
  9. package/lib/api-key-login-BzqNdM2y.mjs +157 -0
  10. package/lib/api-key-login-CWVwvHT1.mjs +157 -0
  11. package/lib/aws-credentials-CXryMobh.mjs +867 -0
  12. package/lib/aws-credentials-ChG79ey-.mjs +867 -0
  13. package/lib/azure-openai-responses-Cgmc-rbL.mjs +274 -0
  14. package/lib/azure-openai-responses-DuukbD0F.mjs +274 -0
  15. package/lib/bun-shim.mjs +81 -6
  16. package/lib/bundled-references-Cavaffah.mjs +55 -0
  17. package/lib/bundled-references-sLN0YdXU.mjs +55 -0
  18. package/lib/callback-server-CQ5Jo8F-.mjs +398 -0
  19. package/lib/callback-server-DDSrZ6l_.mjs +398 -0
  20. package/lib/client.js +1 -1
  21. package/lib/cursor-B3zg7EHj.mjs +273 -0
  22. package/lib/cursor-B6TUEmgP.mjs +135 -0
  23. package/lib/cursor-D8gvtTL3.mjs +135 -0
  24. package/lib/cursor-DkSX2h3H.mjs +5441 -0
  25. package/lib/cursor-aJrXeYCb.mjs +273 -0
  26. package/lib/cursor-mJQwUKXK.mjs +5441 -0
  27. package/lib/device-code-DBo3B5nh.mjs +51 -0
  28. package/lib/device-code-DIb0FU-H.mjs +51 -0
  29. package/lib/devin-BHZZyMhO.mjs +95 -0
  30. package/lib/devin-BK7DHeca.mjs +114 -0
  31. package/lib/devin-CfMp3cXS.mjs +95 -0
  32. package/lib/devin-D9zR5B_R.mjs +542 -0
  33. package/lib/devin-Q1yem3lp.mjs +114 -0
  34. package/lib/devin-vNZTNUON.mjs +542 -0
  35. package/lib/error-Dd8DIqs-.mjs +1449 -0
  36. package/lib/error-yCSG0mGC.mjs +1449 -0
  37. package/lib/event-stream-BbFZJy-z.mjs +151 -0
  38. package/lib/event-stream-loUa0s1T.mjs +151 -0
  39. package/lib/fs-promises-shim-Bx0fpxuk.mjs +57 -0
  40. package/lib/github-copilot-B1yiyVRh.mjs +104 -0
  41. package/lib/github-copilot-BWT3eAOF.mjs +208 -0
  42. package/lib/github-copilot-CYkjeTi3.mjs +208 -0
  43. package/lib/github-copilot-jscUZ8Hf.mjs +104 -0
  44. package/lib/gitlab-duo-CM7Ref1H.mjs +155 -0
  45. package/lib/gitlab-duo-gubO4FNw.mjs +155 -0
  46. package/lib/gitlab-duo-workflow-BGAF6JDw.mjs +91 -0
  47. package/lib/gitlab-duo-workflow-D78tdn8E.mjs +91 -0
  48. package/lib/google-BS4T25Pn.mjs +30 -0
  49. package/lib/google-TFe-7-of.mjs +30 -0
  50. package/lib/google-antigravity-DGmwC2KV.mjs +198 -0
  51. package/lib/google-antigravity-DNYhgTpA.mjs +198 -0
  52. package/lib/google-auth-B6gjsZvg.mjs +232 -0
  53. package/lib/google-auth-De7_s3eo.mjs +232 -0
  54. package/lib/google-gemini-cli-CWu8HvXW.mjs +211 -0
  55. package/lib/google-gemini-cli-Cb97bzQc.mjs +850 -0
  56. package/lib/google-gemini-cli-Cqds_Q5T.mjs +850 -0
  57. package/lib/google-gemini-cli-Dsztq0N5.mjs +211 -0
  58. package/lib/google-oauth-shared-CuEHaDDj.mjs +147 -0
  59. package/lib/google-oauth-shared-yqlyqPPC.mjs +147 -0
  60. package/lib/google-shared-CgKmmdO3.mjs +760 -0
  61. package/lib/google-shared-Dr-VG-ap.mjs +760 -0
  62. package/lib/google-vertex-BDN2tLaD.mjs +99 -0
  63. package/lib/google-vertex-DopVOQOG.mjs +99 -0
  64. package/lib/identity-Da1GPU9a.mjs +219 -0
  65. package/lib/identity-xRP7_eSR.mjs +219 -0
  66. package/lib/idle-iterator-Baoyn3Hp.mjs +366 -0
  67. package/lib/idle-iterator-DUjKQEGT.mjs +366 -0
  68. package/lib/index.mjs +1731 -1178
  69. package/lib/kimi--wUOWER5.mjs +236 -0
  70. package/lib/kimi-Dh0pv3WZ.mjs +236 -0
  71. package/lib/markit-BL3cH47U.mjs +1417 -0
  72. package/lib/markit-BNiNY6SE.mjs +1417 -0
  73. package/lib/minimax-code-DCbuA6Yk.mjs +53 -0
  74. package/lib/minimax-code-Uq8Qu9LW.mjs +53 -0
  75. package/lib/models-D_XnT6ay.mjs +104660 -0
  76. package/lib/models-SGASygJ3.mjs +104660 -0
  77. package/lib/ollama-DPzBmq89.mjs +589 -0
  78. package/lib/ollama-DoBz5SFm.mjs +589 -0
  79. package/lib/openai-codex-Bi-4S4PX.mjs +255 -0
  80. package/lib/openai-codex-Ch8CkT3p.mjs +255 -0
  81. package/lib/openai-codex-responses-CvxL_FMF.mjs +3144 -0
  82. package/lib/openai-codex-responses-pejZgdZo.mjs +3144 -0
  83. package/lib/openai-completions-CF8IyNtP.mjs +1360 -0
  84. package/lib/openai-completions-DIf361kF.mjs +1360 -0
  85. package/lib/openai-reasoning-fallback-Nj6yB2rY.mjs +286 -0
  86. package/lib/openai-reasoning-fallback-OdJREP1o.mjs +286 -0
  87. package/lib/openai-responses-BqXJInHX.mjs +867 -0
  88. package/lib/openai-responses-Db_a-U4-.mjs +867 -0
  89. package/lib/opencode-DhMolSoB.mjs +38 -0
  90. package/lib/opencode-Rq_Q2vAO.mjs +38 -0
  91. package/lib/perplexity-C4iLVMUj.mjs +194 -0
  92. package/lib/perplexity-Uu7Jzg5b.mjs +194 -0
  93. package/lib/proxy-DDqrmjjC.mjs +190 -0
  94. package/lib/proxy-pM_NIv-H.mjs +190 -0
  95. package/lib/request-debug-HJMoJncL.mjs +183 -0
  96. package/lib/sanitize-text-CfcXzw5X.mjs +33 -0
  97. package/lib/scrapers-BkFfcPHj.mjs +9198 -0
  98. package/lib/scrapers-CVptboMs.mjs +9198 -0
  99. package/lib/scrapers-nhQAvc8d.mjs +9198 -0
  100. package/lib/shell-snapshot-fn-env.sh +67 -43
  101. package/lib/src-BgpusnsB.mjs +5076 -0
  102. package/lib/src-lDmkSRbm.mjs +5006 -0
  103. package/lib/stream-BlldLkVq.mjs +10236 -0
  104. package/lib/stream-CRtexYLe.mjs +10236 -0
  105. package/lib/stream-markup-healing-DMsxBx1L.mjs +491 -0
  106. package/lib/stream-markup-healing-KMkiAA00.mjs +491 -0
  107. package/lib/turndown-DN7ByzEI.mjs +667 -0
  108. package/lib/types/config/settings.d.ts +9 -1
  109. package/lib/types/config/truncate.d.ts +9 -5
  110. package/lib/types/tools/bash/background.d.ts +1 -1
  111. package/lib/types/tools/bash/bash-executor.d.ts +4 -0
  112. package/lib/types/tools/bash/bash-interceptor.d.ts +10 -8
  113. package/lib/types/tools/bash/cd-workdir.d.ts +35 -0
  114. package/lib/types/tools/bash/non-interactive-env.d.ts +2 -0
  115. package/lib/types/tools/bash/render.d.ts +4 -3
  116. package/lib/types/tools/bash/sanitize-text.d.ts +5 -0
  117. package/lib/types/tools/bash/shell-snapshot.d.ts +4 -1
  118. package/lib/types/tools/bash/shell-tokenize.d.ts +60 -0
  119. package/lib/types/tools/bash/streaming-output.d.ts +12 -212
  120. package/lib/types/tools/bash/which.d.ts +20 -0
  121. package/lib/types/tools/bash/windows-shell.d.ts +31 -0
  122. package/lib/types/tools/edit/adapter/index.d.ts +25 -3
  123. package/lib/types/tools/edit/adapter/tools/auto-generated-guard.d.ts +7 -17
  124. package/lib/types/tools/glob/adapter/index.d.ts +27 -1
  125. package/lib/types/tools/grep/adapter/index.d.ts +29 -1
  126. package/lib/types/tools/omp/edit/dispatch.d.ts +43 -0
  127. package/lib/types/tools/omp/edit/hashline/noop-loop-guard.d.ts +3 -1
  128. package/lib/types/tools/omp/tools/auto-generated-guard.d.ts +20 -0
  129. package/lib/types/tools/omp/tools/write.d.ts +4 -4
  130. package/lib/types/tools/shared/auto-generated-guard.dsh.d.ts +16 -4
  131. package/lib/types/tools/shared/bun-shim.d.ts +4 -0
  132. package/lib/types/tools/shared/fs-promises-shim.d.ts +3 -1
  133. package/lib/types/tools/shared/omp-prompt.d.ts +13 -7
  134. package/lib/types/tools/shared/session-state.d.ts +9 -3
  135. package/lib/utils-Bw139LTz.mjs +8119 -0
  136. package/lib/utils-CZw4tVZQ.mjs +12315 -0
  137. package/lib/utils-CiGN7OHR.mjs +12315 -0
  138. package/lib/utils-Cp76gCyk.mjs +8119 -0
  139. package/lib/utils-D0GRDgAr.mjs +12315 -0
  140. package/lib/utils-D2YknRk4.mjs +51 -0
  141. package/lib/utils-DzXZDfYc.mjs +51 -0
  142. package/lib/vision-guard-01qSwFpf.mjs +92 -0
  143. package/lib/vision-guard-DzZGaOga.mjs +92 -0
  144. package/lib/wafer-CgtPHerf.mjs +17 -0
  145. package/lib/wafer-cBtrn6q9.mjs +17 -0
  146. package/lib/xai-oauth-Buk8XVvM.mjs +446 -0
  147. package/lib/xai-oauth-CMal8l1O.mjs +446 -0
  148. package/lib/xiaomi-CZgwvX2G.mjs +157 -0
  149. package/lib/xiaomi-D2qT5cAC.mjs +157 -0
  150. package/lib/zai-7DWMkEqM.mjs +203 -0
  151. package/lib/zai-C59YnomG.mjs +203 -0
  152. package/lib/zip-Cg9ANN_U.mjs +1316 -0
  153. package/lib/zip-D36ZKsP8.mjs +1316 -0
  154. package/package.json +1 -1
  155. 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-B-9cMxz5.mjs";
4
- import { A as truncateToWidth$1, C as capParseErrors, D as LRUCache, E as shortenPath, M as isSettingsInitialized, N as settings, O as padding, P as OMP_DEFAULTS, S as convertFileWithMarkit, T as formatParseErrors, _ as extractWithParallel, a as fetchBinary, b as ensureTool, g as looksLikeHtml, h as loadPage, i as convertWithMarkit, j as Settings, k as replaceTabs, o as MAX_BYTES, s as MAX_OUTPUT_CHARS, u as finalizeOutput, v as findParallelApiKey, w as formatCodeFrameLine, x as CONVERTIBLE_EXTENSIONS, y as getParallelExtractContent } from "./utils-W8WdSBpX.mjs";
5
- import { $ as untilAborted, A as formatAge, C as error, F as $envpos, I as $flag, M as formatCount, P as $env, 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, j as formatBytes$2, k as installBunShim, l as exec, p as readImageMetadata, q as getInstallId, u as render, v as hasFsCode, y as isEnoent } from "./src-DTid0g4K.mjs";
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$1, r as formatArchiveEntryLines, s as readArchiveEntries, t as archiveFormatFromPath } from "./zip-D6QXpvaJ.mjs";
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
- if (regex.test(normalizedCommand)) return {
89
- block: true,
90
- message: `Blocked: ${rule.message}\n\nOriginal command: ${command}`,
91
- suggestedTool: rule.tool
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/non-interactive-env.ts
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: "/usr/bin/false",
136
- CI: "1",
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
- ...NON_INTERACTIVE_ENV,
520
+ ...base,
181
521
  ...overrides
182
- } : NON_INTERACTIVE_ENV;
183
- const env = { ...NON_INTERACTIVE_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 snapshotDir = path.join(os.tmpdir(), "omp-shell-snapshots");
395
- fs.mkdirSync(snapshotDir, {
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
- fs.writeFileSync(snapshotPath, "", { mode: 384 });
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: SNAPSHOT_TIMEOUT_MS,
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 src/tools/bash/sanitize-text.ts
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
- * Strip ANSI escape sequences, remove control characters / lone surrogates,
455
- * and normalize line endings.
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
- * Bun-native implementation of the former native `sanitizeText` (see
458
- * `crates/pi-natives/src/text.rs::sanitize_text`). JavaScript strings are
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
- * Fast path: well-formed input with no controls or ANSI returns the original
464
- * string after the control probe.
465
- */
466
- const ESC_CHAR = "\x1B";
467
- const CONTROL_RE = /[\x00-\x08\x0B-\x1F\x7F-\x9F]/g;
468
- const ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07]*(?:\x07|\x1b\\)|[()][0-9A-B]|[^[\]()])/g;
469
- function stripAnsi(text) {
470
- return text.replace(ANSI_RE, "");
471
- }
472
- const REPLACEMENT_CHAR = "�";
473
- const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
474
- function toWellFormed(text) {
475
- return text.replace(LONE_SURROGATE_RE, REPLACEMENT_CHAR);
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/bash/streaming-output.ts
491
- /** Format byte count for human-readable display. */
492
- function formatBytes$1(bytes) {
493
- if (bytes < 1024) return `${bytes}B`;
494
- if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
495
- if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)}MB`;
496
- return `${(bytes / 1073741824).toFixed(1)}GB`;
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
- const DEFAULT_MAX_BYTES$1 = 51200;
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$1 = "\n";
989
+ const NL = "\n";
990
+ const CR = "\r";
502
991
  const ELLIPSIS = "…";
503
992
  /** Count newline characters via native substring search. */
504
- function countNewlines$1(text) {
993
+ function countNewlines(text) {
505
994
  let count = 0;
506
- let pos = text.indexOf(NL$1);
995
+ let pos = text.indexOf(NL);
507
996
  while (pos !== -1) {
508
997
  count++;
509
- pos = text.indexOf(NL$1, pos + 1);
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$1(data) {
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$1(buf, pos) {
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$1(buf, cut) {
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$1(data, maxBytesRaw, mode) {
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$1(buf, maxBytes);
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$1(buf, Math.max(0, buf.length - maxBytes));
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$1(data);
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$1(buf, maxBytes);
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$1(buf, buf.length - maxBytes);
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$1(data, maxBytes, "tail");
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$1(data, maxBytes) {
603
- return truncateBytesWindowed$1(data, maxBytes, "head");
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$1(content, totalLines, totalBytes) {
607
- if (totalLines == null) totalLines = countNewlines$1(content) + 1;
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$1(content, options = {}) {
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$1(content) + 1;
628
- if (totalLines <= maxLines && totalBytes <= maxBytes) return noTruncResult$1(content, totalLines, totalBytes);
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$1, cursor);
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$1(content) + 1;
705
- if (totalLines <= maxLines && totalBytes <= maxBytes) return noTruncResult$1(content, totalLines, totalBytes);
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$1, end - 1);
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$1(content) + 1;
803
- if (totalBytes <= maxBytes && totalLines <= maxLines) return noTruncResult$1(content, totalLines, totalBytes);
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$1(content, {
1310
+ if (tailBytes <= 0 || tailLines <= 0) return truncateHead(content, {
809
1311
  maxBytes: headBytes,
810
1312
  maxLines: headLines
811
1313
  });
812
- const head = truncateHead$1(content, {
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$1(content, totalLines, totalBytes);
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$1, headBytes = 0, maxColumns = 0, onChunk, chunkThrottleMs = 0, artifactMaxBytes = 0, artifactHeadBytes = ARTIFACT_DEFAULT_HEAD_BYTES } = options ?? {};
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
- chunk = sanitizeText(chunk);
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$1(chunk);
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$1(capped);
1531
+ this.#headLines += countNewlines(capped);
999
1532
  return;
1000
1533
  }
1001
- const headSlice = truncateHeadBytes$1(capped, room);
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$1(headSlice.text);
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$1, cursor);
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$1(segment, headRoom);
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$1);
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$1(chunk, room);
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 = fs.createWriteStream(this.#artifactPath);
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$1(text);
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
- if (this.#file) {
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$1(tailBuf) + 1 : 0;
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$1(body) + 1 : 0;
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: "bash",
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
- return shellBasename(shell).includes("zsh");
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) || hasInteractiveShellArg(args)) return args;
1632
- const commandIndex = args.findIndex((arg) => arg === "-c" || arg === "--command");
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
- ...args.slice(0, commandIndex),
2305
+ ...effectiveArgs.slice(0, commandIndex),
1635
2306
  "-i",
1636
- ...args.slice(commandIndex)
2307
+ ...effectiveArgs.slice(commandIndex)
1637
2308
  ];
1638
- const compactCommandIndex = args.findIndex((arg) => /^-[^-]*c[^-]*$/.test(arg));
1639
- if (compactCommandIndex !== -1) return args.map((arg, index) => index === compactCommandIndex ? arg.replace("c", "ic") : arg);
1640
- return [...args, "-i"];
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) timeoutTimer = setTimeout(() => {
1742
- if (!nativeOwnsTimeout) abortCurrentExecution();
1743
- timeoutDeferred.resolve("timeout");
1744
- }, deadlineTimeoutMs);
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: result.cancelled && !aborted,
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: result.output,
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/omp/config/settings-schema.ts
2047
- /** Schema default for a setting path (verbatim OMP values; unknown -> undefined). */
2048
- function getDefault(path) {
2049
- return OMP_DEFAULTS[path];
2050
- }
2051
- //#endregion
2052
- //#region node_modules/.pnpm/@oh-my-pi+pi-utils@17.3.5/node_modules/@oh-my-pi/pi-utils/src/path.ts
2053
- const WINDOWS_DRIVE_EXTENDED_PREFIX = /^\\\\[?]\\([A-Za-z]:[\\/].*)$/;
2054
- const WINDOWS_UNC_EXTENDED_PREFIX = /^\\\\[?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
2055
- const WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/([A-Za-z]:\/.*)$/;
2056
- const WINDOWS_UNC_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/UNC\/([^/]+)\/(.+)$/i;
2057
- const WINDOWS_DRIVE_NT_PREFIX = /^\\\\[?][?]\\([A-Za-z]:[\\/].*)$/;
2058
- const WINDOWS_UNC_NT_PREFIX = /^\\\\[?][?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
2059
- /** Removes Win32 extended-length prefixes before passing paths to Bun APIs. */
2060
- function stripWindowsExtendedLengthPathPrefix(filePath, platform = process.platform) {
2061
- if (platform !== "win32") return filePath;
2062
- const uncMatch = WINDOWS_UNC_EXTENDED_PREFIX.exec(filePath) ?? WINDOWS_UNC_NT_PREFIX.exec(filePath);
2063
- if (uncMatch) return `\\\\${uncMatch[1]}\\${uncMatch[2]}`;
2064
- const driveMatch = WINDOWS_DRIVE_EXTENDED_PREFIX.exec(filePath) ?? WINDOWS_DRIVE_NT_PREFIX.exec(filePath);
2065
- if (driveMatch) return driveMatch[1];
2066
- const forwardUncMatch = WINDOWS_UNC_EXTENDED_FORWARD_PREFIX.exec(filePath);
2067
- if (forwardUncMatch) return `//${forwardUncMatch[1]}/${forwardUncMatch[2]}`;
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
- * Multi-level path tree shared by grouped file listings (find / grep / ast tools)
2076
- * and compaction file-operation lists.
2757
+ * Extract a leading `cd <path> && …` prefix, expanding `~` in the target.
2077
2758
  *
2078
- * Flat path lists used to group by the *immediate* parent directory and print the
2079
- * full directory path in every header. For results spread across a deep tree — or
2080
- * rooted outside cwd, where paths stay absolute — that repeated the shared prefix
2081
- * on every line. The tree below folds single-child directory chains (so the common
2082
- * prefix collapses into one header) and nests the rest, charging the model one
2083
- * token per path segment instead of one per file.
2084
- */
2085
- const URL_LIKE_PATH_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
2086
- /** True for `scheme://…` entries that have no meaningful directory structure. */
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
- files: [],
2093
- fileNames: /* @__PURE__ */ new Set(),
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
- * Render a flat path list as a grouped, prefix-folded directory tree without
2175
- * per-file bodies (find-tool output shape, also used by compaction `<files>`
2176
- * lists). Single-child directory chains fold into one header (`# a/b/c/`),
2177
- * each level adds one `#`, and files are listed bare under the deepest
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$2(elidedBytes)}) elided`;
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$2(maxBytes)} limit)`;
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://` clause replaced by the filesystem path — DSH has no
2548
- * artifact URL space, and the read tool's `:N-M` inline selectors take their
2549
- * place.
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: false,
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
- /** OMP-parity defaults for the timing knobs (timeouts in ms; OMP uses seconds). */
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", false)),
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", false),
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
- * ported verbatim in behavior from the OMP bash-runtime
3423
- * `applyConfiguredTruncation` (index.ts). Pure functions over a policy value —
3424
- * no plugin state — so the ported runtime files stay pristine and the
3425
- * `bash-plus` settings surface drives this module.
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$1(text, {
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$1(text, {
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
- /** 渲染 {{#if KEY}}A{{else}}B{{/if}} 与 {{#if KEY}}A{{/if}}(不支持嵌套)。 */
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
- const pick = (key) => vars[key] === true;
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 引擎惰性创建 ConflictHistory 并赋到 session 对象上,必须回写才能
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
- /** Syntactic node boundaries outside a visible source range. */
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$2(content) {
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$2(read.rawContent).text);
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$2(read.rawContent);
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$2(write.text).text);
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$2(node.size),
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$1(`Invalid conflict URI '${raw}': wildcard 'conflict://*' does not accept a scope segment. Drop '/${scopePart}' or use a numeric id.`);
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$1(`Invalid conflict URI '${raw}': must be 'conflict://<N>', 'conflict://<N>/<scope>', or 'conflict://*' where N is a positive integer surfaced by a prior \`read\`.`);
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$1(`Invalid conflict URI '${raw}': id must be ≥ 1.`);
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$1(`Invalid conflict URI '${raw}': scope must be one of 'ours', 'theirs', 'base', or omitted (e.g. 'conflict://${id}/theirs').`);
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$1(`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.`);
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$1(`Conflict #${entry.id} has no base section (2-way merge). \`@base\` is only valid for diff3 conflicts.`);
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$1(`Conflict #${entry.id} has no base section (2-way merge). 'conflict://${entry.id}/base' is only valid for diff3 conflicts.`);
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$1("Line selector 0 is invalid; lines are 1-indexed. Use :1.");
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$1(`Invalid range ${rawStart}+${rhs ?? 0}: count must be >= 1.`);
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$1(`Invalid range ${rawStart}-${rhs}: end must be >= start.`);
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$1(`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.`);
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$1("Search scope entries must be non-empty paths or globs");
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$1("Search scope entries must be non-empty paths or globs");
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$1(`Cannot ${internalUrlAction} external URL: ${rawPath}. Use \`read\` to fetch web content, then search the returned text.`);
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$1(`Cannot ${internalUrlAction} a remote ssh:// path (no local file): ${rawPath}. Use \`read ${rawPath}\` to view it, or the \`search\` tool to grep remote files.`);
11900
- if (hasGlobPathChars(rawPath)) throw new ToolError$1(`Glob patterns are not supported for internal URLs: ${rawPath}`);
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$1(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
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$1(`Path not found: ${partition.missing.join(", ")}`);
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$1("`paths` must contain at least one path or glob");
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$1(`Path not found: ${scopePath}${hint}`);
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$2(bytes)} exceeds ${formatBytes$2(maxBytes)} limit.`);
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$2(value.byteLength)}>`;
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$1(`SQLite limit must be a positive integer; got '${value}'`);
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$1(`SQLite offset must be a non-negative integer; got '${value}'`);
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$1(`SQLite table '${table}' not found`);
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$1(`${label} must be an integer; got '${key}'`);
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$1(`SQLite order column '${column}' not found in table schema`);
12662
- if (direction !== "asc" && direction !== "desc") throw new ToolError$1(`SQLite order direction must be 'asc' or 'desc'; got '${direction}'`);
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$1(violation);
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$1(`SQLite column '${column}' only accepts JSON scalar values or null`);
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$1(`SQLite table '${table}' has no column named '${column}'`);
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$1("SQLite raw queries cannot be combined with table selectors or pagination");
12795
- if (!rawQuery.trim()) throw new ToolError$1("SQLite query parameter 'q' cannot be empty");
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$1("SQLite query parameters require a table selector or q=SELECT...");
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$1("SQLite selectors must include a table name");
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$1("SQLite row lookups cannot be combined with query parameters");
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$1(`Unsupported SQLite query parameter '${keyName}'`);
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$1(`Unsupported SQLite query parameter '${keyName}'`);
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$1(`SQLite schema for table '${table}' is unavailable`);
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$1(`SQLite table '${table}' has a composite primary key; use '?where=' instead`);
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$1(`SQLite table '${table}' does not expose ROWID; use '?where=' instead`);
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$1("SQLite where clause changed the expected pagination parameters; use q=SELECT ... for raw SQL");
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$1("SQLite raw queries do not support bound parameters");
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$1("SQLite updates require at least one column value");
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$1("SQLite updates require at least one column value");
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$1(`URL selector has multiple range groups; combine them with commas (e.g. \`:5-10,20-30\`).`);
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$1(`Invalid URL line selector: ${sel}`);
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$2(byteLength);
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-CL-WgdU1.mjs").then((m) => m.specialHandlers);
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$2(MAX_BYTES)} and was cut mid-stream; content is incomplete`);
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$1("Cannot search URL output because this session cannot materialize read artifacts.");
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$1("URL reads are disabled by settings.");
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$1(`Cannot record hashline snapshot for non-absolute path: ${absolutePath}`);
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$2(firstLineBytes)}, exceeds ${formatBytes$2(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
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$2(firstLineBytes)}, exceeds ${formatBytes$2(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
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$1(`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).`);
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$1(`Path '${readPath}' not found inside archive`);
15637
+ if (!node) throw new ToolError(`Path '${readPath}' not found inside archive`);
15218
15638
  if (node.isDirectory) {
15219
- if (isMultiRange(sel)) throw new ToolError$1("Multi-range line selectors are not supported for archive directory listings.");
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$2(entry.size)})]`, resolvedArchivePath.suffixResolution)).sourcePath(resolvedArchivePath.absolutePath).done();
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$1("Unsupported SQLite selector");
15777
+ throw new ToolError("Unsupported SQLite selector");
15358
15778
  } catch (error) {
15359
- if (error instanceof ToolError$1) throw error;
15360
- throw new ToolError$1(error instanceof Error ? error.message : String(error));
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$1(`Empty report. ${reportIssueDeviceUsage()}`);
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$1(`Invalid report format. ${reportIssueDeviceUsage()}`);
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$1) throw error$1;
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$1) throw error;
16367
+ if (error instanceof ToolError) throw error;
15948
16368
  const message = error instanceof Error ? error.message : String(error);
15949
- throw new ToolError$1(`Apply failed: ${message}`);
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$1(`No plan is awaiting approval — ${PROPOSE_DEVICE_PATH} only accepts a plan title while plan mode is active.`);
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$1(`No pending action to apply — ${RESOLVE_DEVICE_PATH} is only valid while a staged preview is pending.${proposeHint}`);
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$1(`Image file too large: ${fileSize} bytes exceeds ${maxBytes} bytes; downscale the image and read the smaller copy`);
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$1(`Read image file [${mimeType}] failed: unsupported image format.`);
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$1(error.message);
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$1("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.");
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$1("URL reads are disabled by settings.");
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$1(`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).`);
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$1(`Path '${localReadPath}' not found`);
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$1("Multi-range line selectors are not supported for directory listings.");
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$2(fileSize)}); not valid UTF-8 text. Use ':raw' to read bytes verbatim.]`, suffixResolution)).sourcePath(absolutePath).done();
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$2(firstLineBytes)}, exceeds ${formatBytes$2(maxBytesForRead)} limit. Hashline output requires full lines; cannot emit an editable numbered preview for a truncated line.]`;
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$2(firstLineBytes)}, exceeds ${formatBytes$2(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
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$2(fileSize)} total; not scanned to EOF). Use :${nextOffset} to continue]`;
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$1(`Conflict #${id} not found. Conflict ids are registered when \`read\` surfaces a marker block; re-read the file to get a current id.`);
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$2(artifact.size)}). Use ${artifactUrl}:N-M to page, ${artifactUrl}:raw:N-M for verbatim chunks, and the artifact file path for search/copy workflows.`;
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$2(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}`;
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$2(firstLineBytes)}, exceeds ${formatBytes$2(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
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$2(artifact.size)} total; not scanned to EOF). Use ${artifactUrl}:${nextOffset} to continue]`;
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$1(e instanceof Error ? e.message : String(e));
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$1("Cannot combine query extraction with line selectors");
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$1(name ? `Unknown xd:// device '${name}'.` : "No xd:// tools are mounted in this session.");
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$1(`Cannot read directory: ${message}`);
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-B-9cMxz5.mjs").then((n) => n.n);
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/shared/tool-errors.ts
17959
+ //#region src/tools/omp/tools/auto-generated-guard.ts
17540
17960
  /**
17541
- * Standardized error types for tool execution.
17961
+ * Auto-generated file detection guard.
17542
17962
  *
17543
- * Tools should throw these instead of returning error text.
17544
- * The agent loop catches and renders them appropriately.
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
- * Base error for tool execution failures.
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
- var ToolError = class extends Error {
17551
- context;
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
- * DSH-adapted auto-generated guard (minimal, Node-only).
17566
- * Port of OMP auto-generated-guard.ts without Bun/pi-utils.
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 CHECK_BYTE_COUNT$1 = 1024;
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$1 = /* @__PURE__ */ new Map([
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$1 = /* @__PURE__ */ new Map([
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$1(content) {
18054
+ function stripBom(content) {
17637
18055
  if (content.charCodeAt(0) === 65279) return content.slice(1);
17638
18056
  return content;
17639
18057
  }
17640
- function getCommentStylesForPath$1(filePath) {
18058
+ function getCommentStylesForPath(filePath) {
17641
18059
  const normalizedPath = filePath.toLowerCase();
17642
18060
  const fileName = path.basename(normalizedPath);
17643
- const stylesByName = COMMENT_STYLES_BY_BASENAME$1.get(fileName);
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$1.get(ext) ?? [];
18064
+ return COMMENT_STYLES_BY_EXTENSION.get(ext) ?? [];
17647
18065
  }
17648
- function extractLeadingHeaderCommentText$1(content, commentStyles) {
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$1(content).split(/\r?\n/);
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$1; lineIndex++) {
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((p) => p.test(fileName));
18132
+ return AUTO_GENERATED_FILENAME_PATTERNS.some((pattern) => pattern.test(fileName));
17711
18133
  }
17712
- function detectAutoGeneratedMarker$1(content, filePath) {
17713
- const headerCommentText = extractLeadingHeaderCommentText$1(content, getCommentStylesForPath$1(filePath));
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$1) {
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
- function buildAutoGeneratedError$1(displayPath, detected) {
17721
- 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.`);
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
- var SimpleLRU = class {
17724
- max;
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, size;
18156
+ let mtimeMs;
18157
+ let size;
17750
18158
  try {
17751
- const stat = await fs_promises_shim_exports.stat(filePath);
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?.code === "ENOENT") return void 0;
18163
+ if (isEnoent(err)) return;
17756
18164
  throw err;
17757
18165
  }
17758
- const cached = autoGeneratedMap$1.get(filePath);
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
- const handle = await fs_promises_shim_exports.open(filePath, "r");
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?.code === "ENOENT") return void 0;
18172
+ if (isEnoent(err)) return;
17772
18173
  throw err;
17773
18174
  }
17774
- autoGeneratedMap$1.set(filePath, {
18175
+ autoGeneratedMap.set(filePath, {
17775
18176
  mtimeMs,
17776
18177
  size,
17777
18178
  marker
17778
18179
  });
17779
18180
  return marker;
17780
18181
  }
17781
- let blockAutoGenerated = true;
17782
- /** Update whether auto-generated files are blocked (`edit.blockAutoGenerated`). */
17783
- function setBlockAutoGenerated(enabled) {
17784
- blockAutoGenerated = enabled;
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
- async function assertEditableFile(absolutePath, displayPath, _activeSettings) {
17790
- if (!shouldBlockAutoGeneratedFiles$1()) return;
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$1(pathForDisplay, marker);
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$1("Plan mode: renaming files is not allowed.");
17916
- if (options?.op === "delete") throw new ToolError$1("Plan mode: deleting files is not allowed.");
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$1("Plan mode: the working tree is read-only. Write your plan to a local://<slug>-plan.md file instead.");
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$1(`Unknown URI-like write target '${trimmed}'. Did you mean 'xd://${missingDelimiter[1]}'? Prefix the path with './' to write it as a filesystem path.`);
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$1(`Unknown URI-like write target '${trimmed}'.${suggestion} Prefix the path with './' to write it as a filesystem path.`);
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$1(`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).`);
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$1(`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>" })).`);
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$1(`Bulk directive lists conflict #${id} twice — each id may appear once.`);
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$1(`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).");
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$1("Archive write path must target a file inside the archive");
18189
- if (normalized.endsWith("/")) throw new ToolError$1("Archive write path must target a file, not a directory");
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$1("Archive path cannot contain '..'");
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$1("Archive write path must target a file inside the archive");
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$1("SQLite write paths do not support query parameters");
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$1("SQLite write path must target a table");
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$1("SQLite write path must target a table");
18208
- if (key !== void 0 && key.length === 0) throw new ToolError$1("SQLite row writes require a non-empty row key");
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$1(error instanceof Error ? error.message : String(error));
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$1(error instanceof Error ? error.message : String(error));
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$1(`SQLite database '${displayPath}' not found`);
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$1("SQLite deletes require a row key in the path");
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$1(`SQLite write content must be valid JSON5: ${error instanceof Error ? error.message : String(error)}`);
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$1("SQLite write content must be a JSON object");
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$1(`SQLite database '${displayPath}' not found`);
18394
- if (error instanceof ToolError$1) throw error;
18395
- throw new ToolError$1(error instanceof Error ? error.message : String(error));
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$1(`Conflict #${entry.id} target '${entry.displayPath}' no longer exists.`);
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$1(`Conflict #${id} not found. Conflict ids are registered when \`read\` surfaces a marker block; re-read the file to get a current id.`);
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$1("`conflict://*` has nothing to resolve — no conflicts are currently registered. Re-read the file(s) with conflicts first.");
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$1(`Bulk directive references unknown conflict id(s) ${unknown.map((id) => `#${id}`).join(", ")}. Currently registered: ${allEntries.map((e) => `#${e.id}`).join(", ")}.`);
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$1(resultText);
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$1(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.");
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$1(`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\`.`);
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$2(rawContent);
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$2(originalContent);
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$1(`edit appeared successful but file content did not change on disk: ${path}`, { path: resolvedPath });
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$2(result.change.oldContent).text), normalizeToLF(stripBom$2(result.change.newContent).text), void 0, { path: result.change.newPath ?? result.change.path });
21310
- else if (result.change.type === "create" && result.change.newContent !== void 0) diffResult = generateUnifiedDiffString("", normalizeToLF(stripBom$2(result.change.newContent).text), void 0, { path: result.change.path });
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
- * Build the error message for an auto-generated file.
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 buildAutoGeneratedError(displayPath, detected) {
21545
- return new ToolError$1(`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`);
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
- * Check if file content is auto-generated.
21556
- * Uses only the first CHECK_BYTE_COUNT characters of the content.
21557
- * Throws ToolError if the content appears to be auto-generated.
21558
- *
21559
- * @param content - File content to check (can be full content or prefix)
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
- function assertEditableFileContent(content, displayPath, activeSettings) {
21564
- if (!shouldBlockAutoGeneratedFiles(activeSettings)) return;
21565
- const marker = detectAutoGeneratedMarker(content.slice(0, CHECK_BYTE_COUNT), displayPath);
21566
- if (marker) throw buildAutoGeneratedError(displayPath, marker);
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 xxHash64 — fast,
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$1(noChangeLoopDiagnostic(sectionResult.path, count));
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$1(noChangeLoopDiagnostic(entry.section.path, count)) : new ToolError$1(noChangeDiagnostic(entry.section.path));
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$1(noChangeLoopDiagnostic(sectionResult.path, count)) : new ToolError$1(noChangeDiagnostic(sectionResult.path));
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("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"), {}),
22086
- patch: renderOmpPrompt(sanitizePatchPrompt("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"), {}),
22087
- apply_patch: renderOmpPrompt(sanitizeApplyPatchPrompt("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"), {}),
22088
- hashline: renderOmpPrompt(sanitizeHashlinePrompt("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"), {})
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
- return {
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
- const session = createToolSession$4(exec, getConfig());
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$1(`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`);
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$1(`path entry "${entry}" — only line-range selectors like ":50-100" are supported (no ":raw"/":conflicts")`);
22523
- if (hasGlobPathChars(split.path)) throw new ToolError$1(`Line-range selector requires a single file, not a glob: ${entry}`);
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$1(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
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$1(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
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$1(`Glob patterns are not supported for internal URLs: ${rawPath}`);
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$1(`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`);
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$1("Pattern must not be empty");
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$1("Skip must be a non-negative number");
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$1(`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.`);
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$1(`Path not found for line-range selector: ${spec.original}`);
23108
- if (!stats.isFile()) throw new ToolError$1(`Line-range selector requires a single file: ${spec.original} is a directory`);
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$1(`Path not found: ${missingPaths.join(", ")}; list each target in the semicolon-delimited \`path\`${archiveHint}`);
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$1(err.message.replace(/^regex(?: parse)? error:?\s*/i, "Invalid regex: "));
23221
- if (err instanceof Error && err.message.includes("Aborted: Timeout")) throw new ToolError$1(`Grep timed out after ${SEARCH_GREP_TIMEOUT_MS / 1e3}s; narrow paths or pattern, or scope with \`glob\` first`);
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$1(err.message.replace(/^regex(?: parse)? error:?\s*/i, "Invalid regex: "));
23229
- if (err instanceof SyntaxError) throw new ToolError$1(`Invalid regex: ${err.message}`);
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
- const tool = new GrepTool(createToolSession$3(exec, getConfig()));
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$1("Searching from root directory '/' is not allowed");
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$1(`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.`);
23645
- if (hasGlobPathChars(rawPattern)) throw new ToolError$1(`Glob patterns are not supported for internal URLs: ${rawPattern}`);
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$1(`Cannot find internal URL without a backing file: ${rawPattern}`);
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$1("`path` must contain non-empty globs or paths");
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$1(`Path not found: ${partition.missing.join(", ")}`);
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$1("Searching from root directory '/' is not allowed");
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$1("Limit must be a positive number");
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$1(`Path not found: ${scopePath}`);
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$1(`Path not found: ${scopePath}`);
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$1(`Path is not a directory: ${target.searchPath}`);
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
- const result = await new GlobTool(createToolSession$2(exec, getConfig())).execute("glob", {
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$1(`\`ops[${index}].pat\` must be a non-empty pattern`);
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$1("`ops` must include at least one op entry");
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$1(`Duplicate rewrite pattern: ${pat}`);
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$1(`Cannot rewrite external URL: ${rawPath}. Use \`read\` or \`search\` to inspect fetched web content; ast_edit only applies to local files.`);
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$1("`pat` must be a non-empty pattern");
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$1("skip must be a non-negative number");
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.cancelled,
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 match = CD_PREFIX_PATTERN.exec(command);
25091
- if (match !== null && !/[$`(]/.test(match[1])) {
25092
- workdir = match[1].trim().replace(/^['"]|['"]$/g, "");
25093
- command = command.slice(match[0].length);
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.isAbsolute(workdir) ? workdir : path.resolve(state.cwd, workdir);
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 ? void 0 : Math.min(Math.max(1e3, rawTimeoutMs), cfg.maxTimeoutMs);
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 === void 0 ? cfg.autoBackgroundMs : Math.max(0, Math.min(cfg.autoBackgroundMs, timeoutMs - 1e3));
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 {