@wrongstack/tools 0.310.1 → 0.313.1

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 (135) hide show
  1. package/dist/_shell-pick.d.ts +5 -10
  2. package/dist/audit.js +10 -4055
  3. package/dist/bash.js +9 -2332
  4. package/dist/batch-tool-use.js +2 -187
  5. package/dist/browser/index.js +25 -1339
  6. package/dist/builtin.js +69 -33985
  7. package/dist/chunk-2CTB3UK5.js +197 -0
  8. package/dist/chunk-2ME7ZWDD.js +1272 -0
  9. package/dist/chunk-2TFQDEPU.js +113 -0
  10. package/dist/chunk-2ZCJNKFE.js +315 -0
  11. package/dist/chunk-3PVYQFRQ.js +472 -0
  12. package/dist/chunk-3QMQBO5Z.js +89 -0
  13. package/dist/chunk-42AKKX7O.js +385 -0
  14. package/dist/chunk-42SKEETB.js +113 -0
  15. package/dist/chunk-4BHUSMNL.js +104 -0
  16. package/dist/chunk-4I4LWFOO.js +431 -0
  17. package/dist/chunk-553IQUMO.js +478 -0
  18. package/dist/chunk-55KF77OK.js +1944 -0
  19. package/dist/chunk-6HLZK37X.js +359 -0
  20. package/dist/chunk-6RNIFLG4.js +305 -0
  21. package/dist/chunk-A6BRWIXN.js +317 -0
  22. package/dist/chunk-AHHDWNXC.js +200 -0
  23. package/dist/chunk-B2CIOKWP.js +899 -0
  24. package/dist/chunk-BTSFTGIL.js +7 -0
  25. package/dist/chunk-D33S4RQG.js +222 -0
  26. package/dist/chunk-DIJKHSUO.js +134 -0
  27. package/dist/chunk-DPL47QGS.js +3515 -0
  28. package/dist/chunk-EMRGN4KM.js +170 -0
  29. package/dist/chunk-FGOABE4Z.js +955 -0
  30. package/dist/chunk-FW65TP4G.js +520 -0
  31. package/dist/chunk-GOIZNNOA.js +221 -0
  32. package/dist/chunk-H5MJUAA6.js +295 -0
  33. package/dist/chunk-H5OHH6F4.js +223 -0
  34. package/dist/chunk-HC5RM4WY.js +199 -0
  35. package/dist/chunk-HEQFMLZ7.js +190 -0
  36. package/dist/chunk-HFXSNKCV.js +196 -0
  37. package/dist/chunk-HXSBQ2SE.js +1723 -0
  38. package/dist/chunk-ILESNGNM.js +163 -0
  39. package/dist/chunk-INF5CGAO.js +137 -0
  40. package/dist/chunk-IWDFZ6XR.js +337 -0
  41. package/dist/chunk-IXMNTNW5.js +238 -0
  42. package/dist/chunk-J4SRJKBW.js +428 -0
  43. package/dist/chunk-J5SKD6LN.js +235 -0
  44. package/dist/chunk-JRJJX3L5.js +424 -0
  45. package/dist/chunk-KXNDHMUB.js +366 -0
  46. package/dist/chunk-LIJQMYYX.js +59 -0
  47. package/dist/chunk-MAN26PZ5.js +260 -0
  48. package/dist/chunk-MKRQMLZA.js +474 -0
  49. package/dist/chunk-NBDZMMG2.js +169 -0
  50. package/dist/chunk-NHA6BEIH.js +101 -0
  51. package/dist/chunk-OA6YJIWL.js +4412 -0
  52. package/dist/chunk-OZC255IC.js +2830 -0
  53. package/dist/chunk-PUR6WU2B.js +154 -0
  54. package/dist/chunk-Q5JPPCZC.js +12 -0
  55. package/dist/chunk-QBV725YG.js +128 -0
  56. package/dist/chunk-QHVW4AAM.js +892 -0
  57. package/dist/chunk-QNJ7AQ4B.js +1000 -0
  58. package/dist/chunk-RGISBZBD.js +231 -0
  59. package/dist/chunk-RJZQWEMS.js +84 -0
  60. package/dist/chunk-RP2ZA47V.js +15 -0
  61. package/dist/chunk-RXISBFR5.js +315 -0
  62. package/dist/chunk-S5CCLZL3.js +99 -0
  63. package/dist/chunk-TVTCSBJF.js +122 -0
  64. package/dist/chunk-U4D5S3VQ.js +21 -0
  65. package/dist/chunk-U4VAATKP.js +193 -0
  66. package/dist/chunk-UKU2ZX4C.js +203 -0
  67. package/dist/chunk-UNSKG6DC.js +159 -0
  68. package/dist/chunk-URFKEIHM.js +1091 -0
  69. package/dist/chunk-UTV7YAW7.js +178 -0
  70. package/dist/chunk-V43EQ6VR.js +68 -0
  71. package/dist/chunk-VB3MJGCV.js +610 -0
  72. package/dist/chunk-WHIIIREL.js +223 -0
  73. package/dist/chunk-XMIFYKUT.js +1371 -0
  74. package/dist/chunk-YF2RSV34.js +54 -0
  75. package/dist/chunk-Z6DAKQ3U.js +1 -0
  76. package/dist/chunk-ZIGAZFWE.js +522 -0
  77. package/dist/circuit-breaker.js +3 -191
  78. package/dist/codebase-index/index.js +84 -13688
  79. package/dist/codebase-index/parser-worker-script.js +8 -3995
  80. package/dist/codebase-index/project-server.js +60 -8548
  81. package/dist/codebase-index/worker.js +20 -8416
  82. package/dist/design.js +2 -299
  83. package/dist/diff.js +4 -256
  84. package/dist/document.js +4 -222
  85. package/dist/e2e.js +5 -476
  86. package/dist/edit.js +14 -10200
  87. package/dist/exec.js +16 -3131
  88. package/dist/fetch.js +8 -398
  89. package/dist/format.js +10 -3750
  90. package/dist/generic-parser-VUXDIAOZ.js +13 -0
  91. package/dist/git.js +4 -380
  92. package/dist/glob.js +6 -280
  93. package/dist/go-parser-6CA77WZ4.js +469 -0
  94. package/dist/grep.js +7 -604
  95. package/dist/index.js +318 -35423
  96. package/dist/install.js +10 -4091
  97. package/dist/json-parser-7YMDJSBJ.js +213 -0
  98. package/dist/json.js +4 -665
  99. package/dist/kanban.js +3 -2096
  100. package/dist/languages/index.js +23 -4664
  101. package/dist/legacy-bridge-7UKZT46G.js +18 -0
  102. package/dist/lint.js +10 -3706
  103. package/dist/logs.js +4 -287
  104. package/dist/memory.js +6 -215
  105. package/dist/mode.js +3 -99
  106. package/dist/outdated.js +4 -5170
  107. package/dist/pack.js +65 -33933
  108. package/dist/parser-dispatch-VPOFDO2U.js +14 -0
  109. package/dist/patch.js +13 -9935
  110. package/dist/plan.js +5 -3286
  111. package/dist/process-registry.js +8 -651
  112. package/dist/ps-slash.js +12 -1439
  113. package/dist/py-parser-GPM5VYRG.js +16 -0
  114. package/dist/read.js +13 -9816
  115. package/dist/replace.js +15 -10014
  116. package/dist/rs-parser-WVQ4KYCX.js +81 -0
  117. package/dist/scaffold.js +4 -253
  118. package/dist/search.js +5 -585
  119. package/dist/session-kanban.js +30 -972
  120. package/dist/skill.js +2 -215
  121. package/dist/task.js +5 -3410
  122. package/dist/test.js +10 -3773
  123. package/dist/todo.js +4 -2891
  124. package/dist/tool-help.js +3 -123
  125. package/dist/tool-search.js +3 -79
  126. package/dist/tool-tier.js +68 -34020
  127. package/dist/tool-use.js +2 -93
  128. package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
  129. package/dist/tree.js +4 -288
  130. package/dist/ts-parser-YH4ZMWMH.js +232 -0
  131. package/dist/typecheck.js +10 -3753
  132. package/dist/win32.js +6 -60
  133. package/dist/write.js +14 -9882
  134. package/dist/yaml-parser-GFAOKV53.js +157 -0
  135. package/package.json +5 -5
@@ -0,0 +1,474 @@
1
+ import {
2
+ checkSyntax
3
+ } from "./chunk-3QMQBO5Z.js";
4
+ import {
5
+ enqueueReindex
6
+ } from "./chunk-2ME7ZWDD.js";
7
+ import {
8
+ safeResolveReal,
9
+ sha256hex,
10
+ truncateDiffPayload
11
+ } from "./chunk-D33S4RQG.js";
12
+
13
+ // src/edit.ts
14
+ import * as fs from "node:fs/promises";
15
+ import {
16
+ atomicWrite,
17
+ detectNewlineStyle,
18
+ normalizeToLf,
19
+ toStyle,
20
+ unifiedDiff
21
+ } from "@wrongstack/core/utils";
22
+ import { ToolValidationError } from "@wrongstack/core/types";
23
+
24
+ // src/_edit-match.ts
25
+ var TIER_LABEL = {
26
+ exact: "exact match",
27
+ "trailing-whitespace": "whitespace-normalized match (trailing whitespace ignored)",
28
+ "whitespace-normalized": "whitespace-normalized match (indentation ignored, replacement re-indented)",
29
+ fuzzy: "fuzzy match (block anchors exact, interior \u226590% similar)"
30
+ };
31
+ var TIER_CONFIDENCE = {
32
+ exact: "exact",
33
+ "trailing-whitespace": "high",
34
+ "whitespace-normalized": "medium",
35
+ fuzzy: "low"
36
+ };
37
+ var MIN_NORMALIZED_NEEDLE_CHARS = 16;
38
+ var FUZZY_MIN_SIMILARITY = 0.9;
39
+ var FUZZY_AMBIGUITY_MARGIN = 0.05;
40
+ var FUZZY_MAX_INTERIOR_CHARS = 2e3;
41
+ function findLadderMatches(fileLf, oldLf) {
42
+ const exact = [];
43
+ let idx = fileLf.indexOf(oldLf);
44
+ while (idx !== -1) {
45
+ exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
46
+ idx = fileLf.indexOf(oldLf, idx + 1);
47
+ }
48
+ if (exact.length > 0) return { tier: "exact", matches: exact };
49
+ const fileLines = fileLf.split("\n");
50
+ const needleLines = oldLf.split("\n");
51
+ if (needleLines.length > fileLines.length) return void 0;
52
+ const offsets = lineOffsets(fileLines);
53
+ const fileTrimEnd = fileLines.map((l) => l.trimEnd());
54
+ const needleTrimEnd = needleLines.map((l) => l.trimEnd());
55
+ const trailing = windowScan(fileTrimEnd, needleTrimEnd, offsets, fileLines, (a, b) => a === b);
56
+ if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
57
+ const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);
58
+ if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
59
+ const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());
60
+ const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());
61
+ const normalized = windowScan(fileTrimmed, needleTrimmed, offsets, fileLines, (a, b) => a === b);
62
+ if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
63
+ return fuzzyScan(fileLines, needleLines, offsets);
64
+ }
65
+ function lineAt(text, pos) {
66
+ if (pos < 512) {
67
+ let line2 = 1;
68
+ for (let i = 0; i < pos; i++) {
69
+ if (text.charCodeAt(i) === 10) line2++;
70
+ }
71
+ return line2;
72
+ }
73
+ let line = 1;
74
+ let search = 0;
75
+ while (true) {
76
+ const idx = text.indexOf("\n", search);
77
+ if (idx === -1 || idx >= pos) return line;
78
+ search = idx + 1;
79
+ line++;
80
+ }
81
+ }
82
+ function lineOffsets(lines) {
83
+ const out = new Array(lines.length);
84
+ let pos = 0;
85
+ for (let i = 0; i < lines.length; i++) {
86
+ out[i] = pos;
87
+ pos += lines[i].length + 1;
88
+ }
89
+ return out;
90
+ }
91
+ function windowToMatch(fileLines, offsets, start, windowLen) {
92
+ const lastLine = start + windowLen - 1;
93
+ return {
94
+ start: offsets[start],
95
+ end: offsets[lastLine] + fileLines[lastLine].length,
96
+ startLine: start + 1
97
+ };
98
+ }
99
+ function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
100
+ const n = needleLines.length;
101
+ const out = [];
102
+ for (let i = 0; i + n <= comparisonLines.length; i++) {
103
+ let all = true;
104
+ for (let j = 0; j < n; j++) {
105
+ if (!eq(comparisonLines[i + j], needleLines[j])) {
106
+ all = false;
107
+ break;
108
+ }
109
+ }
110
+ if (all) {
111
+ out.push(windowToMatch(originalLines, offsets, i, n));
112
+ i += n - 1;
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+ function fuzzyScan(fileLines, needleLines, offsets) {
118
+ const n = needleLines.length;
119
+ if (n < 3) return void 0;
120
+ const firstNeedle = needleLines[0].trim();
121
+ const lastNeedle = needleLines[n - 1].trim();
122
+ const needleInterior = needleLines.slice(1, -1).map((l) => l.trim()).join("\n");
123
+ if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return void 0;
124
+ const candidates = [];
125
+ for (let i = 0; i + n <= fileLines.length; i++) {
126
+ if (fileLines[i].trim() !== firstNeedle) continue;
127
+ if (fileLines[i + n - 1].trim() !== lastNeedle) continue;
128
+ const windowInterior = fileLines.slice(i + 1, i + n - 1).map((l) => l.trim()).join("\n");
129
+ if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;
130
+ const score = similarity(needleInterior, windowInterior);
131
+ if (score >= FUZZY_MIN_SIMILARITY) {
132
+ candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });
133
+ }
134
+ }
135
+ if (candidates.length === 0) return void 0;
136
+ candidates.sort((a, b) => b.score - a.score);
137
+ const best = candidates[0];
138
+ const runnerUp = candidates[1];
139
+ if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {
140
+ return {
141
+ tier: "fuzzy",
142
+ matches: candidates.map((c) => c.match),
143
+ score: best.score,
144
+ ambiguous: true
145
+ };
146
+ }
147
+ return { tier: "fuzzy", matches: [best.match], score: best.score };
148
+ }
149
+ function similarity(a, b) {
150
+ if (a === b) return 1;
151
+ const max = Math.max(a.length, b.length);
152
+ if (max === 0) return 1;
153
+ if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {
154
+ return 1 - Math.abs(a.length - b.length) / max;
155
+ }
156
+ return 1 - levenshtein(a, b) / max;
157
+ }
158
+ function levenshtein(a, b) {
159
+ if (a.length === 0) return b.length;
160
+ if (b.length === 0) return a.length;
161
+ let prev = new Array(b.length + 1);
162
+ let cur = new Array(b.length + 1);
163
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
164
+ for (let i = 1; i <= a.length; i++) {
165
+ cur[0] = i;
166
+ const ca = a.charCodeAt(i - 1);
167
+ for (let j = 1; j <= b.length; j++) {
168
+ const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
169
+ cur[j] = Math.min(
170
+ prev[j] + 1,
171
+ cur[j - 1] + 1,
172
+ prev[j - 1] + cost
173
+ );
174
+ }
175
+ [prev, cur] = [cur, prev];
176
+ }
177
+ return prev[b.length];
178
+ }
179
+ function firstLineIndent(text) {
180
+ for (const line of text.split("\n")) {
181
+ if (line.trim() === "") continue;
182
+ return /^[ \t]*/.exec(line)[0];
183
+ }
184
+ return "";
185
+ }
186
+ function adjustIndent(newLf, fromIndent, toIndent) {
187
+ if (fromIndent === toIndent) return { text: newLf, adjusted: false };
188
+ const lines = newLf.split("\n");
189
+ if (toIndent.startsWith(fromIndent)) {
190
+ const extra = toIndent.slice(fromIndent.length);
191
+ return {
192
+ text: lines.map((l) => l.trim() === "" ? l : extra + l).join("\n"),
193
+ adjusted: true
194
+ };
195
+ }
196
+ if (fromIndent.startsWith(toIndent)) {
197
+ const remove = fromIndent.slice(toIndent.length);
198
+ return {
199
+ text: lines.map((l) => l.startsWith(remove) ? l.slice(remove.length) : l).join("\n"),
200
+ adjusted: true
201
+ };
202
+ }
203
+ return { text: newLf, adjusted: false };
204
+ }
205
+ function nearestMatchHint(fileLf, oldLf) {
206
+ const fileLines = fileLf.split("\n");
207
+ const needleLines = oldLf.split("\n");
208
+ if (needleLines.length > fileLines.length) return void 0;
209
+ const needleTrimmed = needleLines.map((l) => l.trim());
210
+ let bestScore = 0;
211
+ let bestStart = -1;
212
+ for (let i = 0; i + needleLines.length <= fileLines.length; i++) {
213
+ let sum = 0;
214
+ for (let j = 0; j < needleLines.length; j++) {
215
+ sum += prefixSimilarity(needleTrimmed[j], fileLines[i + j].trim());
216
+ }
217
+ const score = sum / needleLines.length;
218
+ if (score > bestScore) {
219
+ bestScore = score;
220
+ bestStart = i;
221
+ }
222
+ }
223
+ if (bestStart === -1 || bestScore < 0.5) return void 0;
224
+ const snippet = fileLines.slice(bestStart, bestStart + Math.min(3, needleLines.length)).map((l) => l.length > 120 ? `${l.slice(0, 120)}\u2026` : l).join("\n");
225
+ return { line: bestStart + 1, snippet };
226
+ }
227
+ function prefixSimilarity(a, b) {
228
+ if (a === b) return 1;
229
+ const max = Math.max(a.length, b.length);
230
+ if (max === 0) return 1;
231
+ let p = 0;
232
+ const lim = Math.min(a.length, b.length);
233
+ while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;
234
+ return p / max;
235
+ }
236
+
237
+ // src/edit.ts
238
+ var MAX_DIFF_BYTES = 262144;
239
+ var editTool = {
240
+ name: "edit",
241
+ category: "Filesystem",
242
+ description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
243
+ usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.\nIf `old_string` differs from the file only in whitespace (trailing spaces, indentation), a lower-confidence fallback match is applied and reported in `matched_by`/`note` \u2014 always verify the diff when this happens.\nAfter the edit, TS/JS/JSON files are syntax-checked; if `syntax_errors` is present in the output, fix those errors immediately with a follow-up edit.",
244
+ selection: {
245
+ doNotUseWhen: "creating a new file, replacing the whole file, or applying an existing unified diff.",
246
+ useInstead: ["write", "patch"]
247
+ },
248
+ permission: "confirm",
249
+ // WS-046: gives permission decisions something to key on — the file being
250
+ // edited, so trust rules can scope by path.
251
+ subjectKey: "path",
252
+ mutating: true,
253
+ capabilities: ["fs.write"],
254
+ icon: "edit",
255
+ timeoutMs: 5e3,
256
+ maxOutputBytes: 262144,
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: {
260
+ path: {
261
+ type: "string",
262
+ description: "Path to the file to edit \u2014 relative to the project root, or absolute inside it."
263
+ },
264
+ old_string: {
265
+ type: "string",
266
+ description: "The exact text to replace, including whitespace and indentation. Must be unique in the file unless `replace_all` is set \u2014 add surrounding lines to disambiguate."
267
+ },
268
+ new_string: {
269
+ type: "string",
270
+ description: "The exact replacement text (may be empty to delete `old_string`)."
271
+ },
272
+ replace_all: {
273
+ type: "boolean",
274
+ description: "Replace every occurrence instead of requiring a unique match. Only allowed when `old_string` matches exactly (or up to trailing whitespace) \u2014 fuzzy matches stay single-target."
275
+ }
276
+ },
277
+ required: ["path", "old_string", "new_string"]
278
+ },
279
+ async execute(input, ctx, opts) {
280
+ if (!input?.path) {
281
+ throw new ToolValidationError({ message: "edit: path is required", field: "path" });
282
+ }
283
+ if (input.old_string === void 0) {
284
+ throw new ToolValidationError({
285
+ message: "edit: old_string is required",
286
+ field: "old_string"
287
+ });
288
+ }
289
+ if (input.new_string === void 0) {
290
+ throw new ToolValidationError({
291
+ message: "edit: new_string is required",
292
+ field: "new_string"
293
+ });
294
+ }
295
+ if (input.old_string === "") {
296
+ throw new ToolValidationError({
297
+ message: "edit: old_string cannot be empty",
298
+ field: "old_string"
299
+ });
300
+ }
301
+ const absPath = await safeResolveReal(input.path, ctx);
302
+ const stat2 = await fs.stat(absPath).catch((err) => {
303
+ if (err.code === "ENOENT") {
304
+ throw new ToolValidationError({
305
+ message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
306
+ field: "path",
307
+ context: { exists: false }
308
+ });
309
+ }
310
+ throw err;
311
+ });
312
+ if (!stat2.isFile()) {
313
+ throw new ToolValidationError({
314
+ message: `edit: "${input.path}" is not a regular file`,
315
+ field: "path"
316
+ });
317
+ }
318
+ const autoRead = !ctx.hasRead(absPath);
319
+ const original = await fs.readFile(absPath, "utf8");
320
+ const updated = await fs.stat(absPath);
321
+ const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
322
+ const originalHash = sha256hex(original);
323
+ const lastReadHash = ctx.lastReadHash?.(absPath);
324
+ if (lastReadHash !== void 0) {
325
+ if (lastReadHash !== originalHash) {
326
+ throw new ToolValidationError({
327
+ message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
328
+ field: "path",
329
+ context: { reason: "external_modification" }
330
+ });
331
+ }
332
+ } else {
333
+ const lastReadMtime = ctx.lastReadMtime(absPath);
334
+ if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
335
+ throw new ToolValidationError({
336
+ message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
337
+ field: "path",
338
+ context: { reason: "external_modification" }
339
+ });
340
+ }
341
+ }
342
+ if (autoRead && updated.mtimeMs > stat2.mtimeMs + mtimeTolerance) {
343
+ throw new ToolValidationError({
344
+ message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
345
+ field: "path",
346
+ context: { reason: "auto_read_race" }
347
+ });
348
+ }
349
+ const autoReadNote = autoRead ? `No prior read was recorded for "${input.path}"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.` : void 0;
350
+ const style = detectNewlineStyle(original);
351
+ const fileLf = normalizeToLf(original);
352
+ const oldLf = normalizeToLf(input.old_string);
353
+ const newLf = normalizeToLf(input.new_string);
354
+ if (oldLf === newLf) {
355
+ if (!fileLf.includes(oldLf)) {
356
+ throw noMatchError(input.path, fileLf, oldLf);
357
+ }
358
+ if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
359
+ return {
360
+ path: absPath,
361
+ replacements: 0,
362
+ diff: "(no-op: old and new are identical)",
363
+ matched_by: "exact",
364
+ note: autoReadNote
365
+ };
366
+ }
367
+ opts?.signal?.throwIfAborted();
368
+ const ladder = findLadderMatches(fileLf, oldLf);
369
+ if (!ladder) {
370
+ opts?.signal?.throwIfAborted();
371
+ throw noMatchError(input.path, fileLf, oldLf);
372
+ }
373
+ const { tier, matches } = ladder;
374
+ const count = matches.length;
375
+ if (ladder.ambiguous) {
376
+ const lines = matches.map((m) => m.startLine);
377
+ throw new ToolValidationError({
378
+ message: `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close to distinguish (lines: ${lines.join(", ")}) in "${input.path}". Re-read the file and use the exact text of the intended block.`,
379
+ field: "old_string",
380
+ context: { occurrences: count, matchTier: tier }
381
+ });
382
+ }
383
+ if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
384
+ throw new ToolValidationError({
385
+ message: `edit: old_string only matched via ${TIER_LABEL[tier]} in "${input.path}", but replace_all requires an exact (or trailing-whitespace) match. Re-read the file and use its exact text.`,
386
+ field: "old_string",
387
+ context: { matchTier: tier }
388
+ });
389
+ }
390
+ if (count > 1 && !input.replace_all) {
391
+ const lines = matches.map((m) => m.startLine);
392
+ throw new ToolValidationError({
393
+ message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")})${tier === "exact" ? "" : ` via ${TIER_LABEL[tier]}`}. Add more context to make it unique, or set replace_all: true.`,
394
+ field: "old_string",
395
+ context: { occurrences: count, matchTier: tier }
396
+ });
397
+ }
398
+ let newFileLf;
399
+ let tierNote;
400
+ if (tier === "exact") {
401
+ newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
402
+ } else {
403
+ let replacement = newLf;
404
+ let indentSuffix = "";
405
+ if (tier === "whitespace-normalized" || tier === "fuzzy") {
406
+ const first = matches[0];
407
+ const matchedText = fileLf.slice(first.start, first.end);
408
+ const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));
409
+ replacement = adjusted.text;
410
+ if (adjusted.adjusted) indentSuffix = "; replacement re-indented to match the file";
411
+ }
412
+ newFileLf = fileLf;
413
+ const applied = input.replace_all ? matches : matches.slice(0, 1);
414
+ for (let i = applied.length - 1; i >= 0; i--) {
415
+ const m = applied[i];
416
+ newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);
417
+ }
418
+ const scoreSuffix = ladder.score !== void 0 ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : "";
419
+ tierNote = `old_string did not match exactly; applied via ${TIER_LABEL[tier]} at line ${matches[0].startLine} (confidence: ${TIER_CONFIDENCE[tier]}${scoreSuffix}${indentSuffix}). Review the diff to confirm the intended target was edited.`;
420
+ }
421
+ const newFile = toStyle(newFileLf, style);
422
+ opts?.signal?.throwIfAborted();
423
+ await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
424
+ try {
425
+ enqueueReindex({ projectRoot: ctx.projectRoot, files: [absPath] });
426
+ } catch {
427
+ }
428
+ const written = await fs.stat(absPath);
429
+ ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
430
+ ctx.session?.recordFileChange?.({
431
+ path: absPath,
432
+ action: "modified",
433
+ before: original,
434
+ after: newFile
435
+ });
436
+ opts?.signal?.throwIfAborted();
437
+ const { text: diff, truncated: diffTruncated } = truncateDiffPayload(
438
+ unifiedDiff(original, newFile, {
439
+ fromFile: input.path,
440
+ toFile: input.path
441
+ }),
442
+ MAX_DIFF_BYTES
443
+ );
444
+ const diffNote = diffTruncated ? "Diff truncated to the 256 KiB output budget \u2014 the full edit is on disk." : void 0;
445
+ const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
446
+ let syntaxNote;
447
+ if (syntax && syntax.errors.length > 0) {
448
+ syntaxNote = syntax.preExisting ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.` : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;
449
+ }
450
+ const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
451
+ return {
452
+ path: absPath,
453
+ replacements: input.replace_all ? count : 1,
454
+ diff,
455
+ matched_by: tier,
456
+ syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : void 0,
457
+ note: notes.length > 0 ? notes.join("\n") : void 0
458
+ };
459
+ }
460
+ };
461
+ function noMatchError(inputPath, fileLf, oldLf) {
462
+ const hint = nearestMatchHint(fileLf, oldLf);
463
+ return new ToolValidationError({
464
+ message: `edit: no match for old_string in "${inputPath}".${hint ? ` Nearest match near line ${hint.line}:
465
+ ${hint.snippet}
466
+ Compare this against your old_string and retry with the file's actual text.` : ""}`,
467
+ field: "old_string"
468
+ });
469
+ }
470
+
471
+ export {
472
+ editTool
473
+ };
474
+ //# sourceMappingURL=chunk-MKRQMLZA.js.map
@@ -0,0 +1,169 @@
1
+ // src/_fetch-guard.ts
2
+ import * as dns from "node:dns/promises";
3
+ import * as net from "node:net";
4
+ import { isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
5
+ import { FetchError, ToolValidationError } from "@wrongstack/core/types";
6
+ import { Agent, fetch as undiciFetch } from "undici";
7
+ var nativeGlobalFetch = globalThis.fetch;
8
+ var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
9
+ if (ALLOW_PRIVATE && !process.env["CI"]) {
10
+ console.warn(
11
+ "[WrongStack] WARNING: WRONGSTACK_FETCH_ALLOW_PRIVATE=1 is active \u2014\n fetch tool can now access private IPs (10.x, 192.168.x, 169.254.x),\n cloud metadata endpoints, and plaintext HTTP. Use only on isolated networks."
12
+ );
13
+ }
14
+ function guardedLookup(hostname, options, callback) {
15
+ dns.lookup(hostname, { all: true }).then((records) => {
16
+ const family = options?.family;
17
+ const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
18
+ const list = byFamily.length > 0 ? byFamily : records;
19
+ if (!ALLOW_PRIVATE) {
20
+ for (const r of list) {
21
+ const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
22
+ if (bad) {
23
+ callback(
24
+ Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
25
+ code: "EAI_FAIL"
26
+ })
27
+ );
28
+ return;
29
+ }
30
+ }
31
+ }
32
+ if (options?.all) {
33
+ callback(
34
+ null,
35
+ list.map((r) => ({ address: r.address, family: r.family }))
36
+ );
37
+ return;
38
+ }
39
+ const first = list.at(0);
40
+ if (!first) {
41
+ callback(
42
+ Object.assign(new Error(`fetch: no address for ${hostname}`), { code: "ENOTFOUND" })
43
+ );
44
+ return;
45
+ }
46
+ callback(null, first.address, first.family);
47
+ }).catch((err) => callback(err));
48
+ }
49
+ var pinnedAgent;
50
+ function getPinnedDispatcher() {
51
+ if (!pinnedAgent) {
52
+ pinnedAgent = new Agent({ allowH2: false, connect: { lookup: guardedLookup } });
53
+ }
54
+ return pinnedAgent;
55
+ }
56
+ function dispatcherFetch() {
57
+ return globalThis.fetch === nativeGlobalFetch ? undiciFetch : globalThis.fetch;
58
+ }
59
+ var _beforeExitRegistered = false;
60
+ if (!_beforeExitRegistered) {
61
+ _beforeExitRegistered = true;
62
+ process.on("beforeExit", () => {
63
+ pinnedAgent?.destroy();
64
+ pinnedAgent = void 0;
65
+ });
66
+ }
67
+ async function guardedFetch(url, maxRedirects, signal, headers = {
68
+ "user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
69
+ accept: "text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1"
70
+ }) {
71
+ let redirectCount = 0;
72
+ let currentUrl = url;
73
+ for (; ; ) {
74
+ const parsed = new URL(currentUrl);
75
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
76
+ throw new ToolValidationError({
77
+ message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
78
+ field: "url"
79
+ });
80
+ }
81
+ if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
82
+ throw new ToolValidationError({
83
+ message: "fetch: redirect to http:// blocked (HTTPS required by default)",
84
+ field: "url"
85
+ });
86
+ }
87
+ await assertNotPrivate(parsed.hostname);
88
+ const init = {
89
+ redirect: "manual",
90
+ signal,
91
+ headers,
92
+ dispatcher: getPinnedDispatcher()
93
+ };
94
+ const res = await dispatcherFetch()(currentUrl, init);
95
+ if (res.status < 300 || res.status > 399) {
96
+ return res;
97
+ }
98
+ try {
99
+ await res.body?.cancel();
100
+ } catch {
101
+ }
102
+ redirectCount++;
103
+ if (redirectCount > maxRedirects) {
104
+ throw new FetchError({
105
+ message: `fetch: exceeded ${maxRedirects} redirects`,
106
+ status: res.status,
107
+ context: { url: currentUrl, maxRedirects, redirectCount }
108
+ });
109
+ }
110
+ const location = res.headers.get("location");
111
+ if (!location) {
112
+ throw new FetchError({
113
+ message: "fetch: redirect status with no location header",
114
+ status: res.status,
115
+ context: { url: currentUrl, redirectCount }
116
+ });
117
+ }
118
+ currentUrl = new URL(location, currentUrl).toString();
119
+ }
120
+ }
121
+ async function assertNotPrivate(hostname) {
122
+ if (ALLOW_PRIVATE) return;
123
+ const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
124
+ if (host === "localhost" || host.endsWith(".localhost")) {
125
+ throw new ToolValidationError({
126
+ message: "fetch: blocked localhost target",
127
+ field: "url"
128
+ });
129
+ }
130
+ const ipVersion = net.isIP(host);
131
+ if (ipVersion === 4) {
132
+ if (isPrivateIPv4(host)) {
133
+ throw new ToolValidationError({
134
+ message: `fetch: blocked private/loopback address "${host}"`,
135
+ field: "url"
136
+ });
137
+ }
138
+ } else if (ipVersion === 6) {
139
+ if (isPrivateIPv6(host)) {
140
+ throw new ToolValidationError({
141
+ message: `fetch: blocked private/loopback address "${host}"`,
142
+ field: "url"
143
+ });
144
+ }
145
+ } else {
146
+ try {
147
+ const records = await dns.lookup(host, { all: true });
148
+ for (const r of records) {
149
+ const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
150
+ if (bad) {
151
+ throw new ToolValidationError({
152
+ message: `fetch: resolved to private address ${r.address}`,
153
+ field: "url"
154
+ });
155
+ }
156
+ }
157
+ } catch (err) {
158
+ if (err instanceof ToolValidationError) throw err;
159
+ }
160
+ }
161
+ }
162
+
163
+ export {
164
+ ALLOW_PRIVATE,
165
+ guardedLookup,
166
+ guardedFetch,
167
+ assertNotPrivate
168
+ };
169
+ //# sourceMappingURL=chunk-NBDZMMG2.js.map