@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.
- package/dist/_shell-pick.d.ts +5 -10
- package/dist/audit.js +10 -4055
- package/dist/bash.js +9 -2332
- package/dist/batch-tool-use.js +2 -187
- package/dist/browser/index.js +25 -1339
- package/dist/builtin.js +69 -33985
- package/dist/chunk-2CTB3UK5.js +197 -0
- package/dist/chunk-2ME7ZWDD.js +1272 -0
- package/dist/chunk-2TFQDEPU.js +113 -0
- package/dist/chunk-2ZCJNKFE.js +315 -0
- package/dist/chunk-3PVYQFRQ.js +472 -0
- package/dist/chunk-3QMQBO5Z.js +89 -0
- package/dist/chunk-42AKKX7O.js +385 -0
- package/dist/chunk-42SKEETB.js +113 -0
- package/dist/chunk-4BHUSMNL.js +104 -0
- package/dist/chunk-4I4LWFOO.js +431 -0
- package/dist/chunk-553IQUMO.js +478 -0
- package/dist/chunk-55KF77OK.js +1944 -0
- package/dist/chunk-6HLZK37X.js +359 -0
- package/dist/chunk-6RNIFLG4.js +305 -0
- package/dist/chunk-A6BRWIXN.js +317 -0
- package/dist/chunk-AHHDWNXC.js +200 -0
- package/dist/chunk-B2CIOKWP.js +899 -0
- package/dist/chunk-BTSFTGIL.js +7 -0
- package/dist/chunk-D33S4RQG.js +222 -0
- package/dist/chunk-DIJKHSUO.js +134 -0
- package/dist/chunk-DPL47QGS.js +3515 -0
- package/dist/chunk-EMRGN4KM.js +170 -0
- package/dist/chunk-FGOABE4Z.js +955 -0
- package/dist/chunk-FW65TP4G.js +520 -0
- package/dist/chunk-GOIZNNOA.js +221 -0
- package/dist/chunk-H5MJUAA6.js +295 -0
- package/dist/chunk-H5OHH6F4.js +223 -0
- package/dist/chunk-HC5RM4WY.js +199 -0
- package/dist/chunk-HEQFMLZ7.js +190 -0
- package/dist/chunk-HFXSNKCV.js +196 -0
- package/dist/chunk-HXSBQ2SE.js +1723 -0
- package/dist/chunk-ILESNGNM.js +163 -0
- package/dist/chunk-INF5CGAO.js +137 -0
- package/dist/chunk-IWDFZ6XR.js +337 -0
- package/dist/chunk-IXMNTNW5.js +238 -0
- package/dist/chunk-J4SRJKBW.js +428 -0
- package/dist/chunk-J5SKD6LN.js +235 -0
- package/dist/chunk-JRJJX3L5.js +424 -0
- package/dist/chunk-KXNDHMUB.js +366 -0
- package/dist/chunk-LIJQMYYX.js +59 -0
- package/dist/chunk-MAN26PZ5.js +260 -0
- package/dist/chunk-MKRQMLZA.js +474 -0
- package/dist/chunk-NBDZMMG2.js +169 -0
- package/dist/chunk-NHA6BEIH.js +101 -0
- package/dist/chunk-OA6YJIWL.js +4412 -0
- package/dist/chunk-OZC255IC.js +2830 -0
- package/dist/chunk-PUR6WU2B.js +154 -0
- package/dist/chunk-Q5JPPCZC.js +12 -0
- package/dist/chunk-QBV725YG.js +128 -0
- package/dist/chunk-QHVW4AAM.js +892 -0
- package/dist/chunk-QNJ7AQ4B.js +1000 -0
- package/dist/chunk-RGISBZBD.js +231 -0
- package/dist/chunk-RJZQWEMS.js +84 -0
- package/dist/chunk-RP2ZA47V.js +15 -0
- package/dist/chunk-RXISBFR5.js +315 -0
- package/dist/chunk-S5CCLZL3.js +99 -0
- package/dist/chunk-TVTCSBJF.js +122 -0
- package/dist/chunk-U4D5S3VQ.js +21 -0
- package/dist/chunk-U4VAATKP.js +193 -0
- package/dist/chunk-UKU2ZX4C.js +203 -0
- package/dist/chunk-UNSKG6DC.js +159 -0
- package/dist/chunk-URFKEIHM.js +1091 -0
- package/dist/chunk-UTV7YAW7.js +178 -0
- package/dist/chunk-V43EQ6VR.js +68 -0
- package/dist/chunk-VB3MJGCV.js +610 -0
- package/dist/chunk-WHIIIREL.js +223 -0
- package/dist/chunk-XMIFYKUT.js +1371 -0
- package/dist/chunk-YF2RSV34.js +54 -0
- package/dist/chunk-Z6DAKQ3U.js +1 -0
- package/dist/chunk-ZIGAZFWE.js +522 -0
- package/dist/circuit-breaker.js +3 -191
- package/dist/codebase-index/index.js +84 -13688
- package/dist/codebase-index/parser-worker-script.js +8 -3995
- package/dist/codebase-index/project-server.js +60 -8548
- package/dist/codebase-index/worker.js +20 -8416
- package/dist/design.js +2 -299
- package/dist/diff.js +4 -256
- package/dist/document.js +4 -222
- package/dist/e2e.js +5 -476
- package/dist/edit.js +14 -10200
- package/dist/exec.js +16 -3131
- package/dist/fetch.js +8 -398
- package/dist/format.js +10 -3750
- package/dist/generic-parser-VUXDIAOZ.js +13 -0
- package/dist/git.js +4 -380
- package/dist/glob.js +6 -280
- package/dist/go-parser-6CA77WZ4.js +469 -0
- package/dist/grep.js +7 -604
- package/dist/index.js +318 -35423
- package/dist/install.js +10 -4091
- package/dist/json-parser-7YMDJSBJ.js +213 -0
- package/dist/json.js +4 -665
- package/dist/kanban.js +3 -2096
- package/dist/languages/index.js +23 -4664
- package/dist/legacy-bridge-7UKZT46G.js +18 -0
- package/dist/lint.js +10 -3706
- package/dist/logs.js +4 -287
- package/dist/memory.js +6 -215
- package/dist/mode.js +3 -99
- package/dist/outdated.js +4 -5170
- package/dist/pack.js +65 -33933
- package/dist/parser-dispatch-VPOFDO2U.js +14 -0
- package/dist/patch.js +13 -9935
- package/dist/plan.js +5 -3286
- package/dist/process-registry.js +8 -651
- package/dist/ps-slash.js +12 -1439
- package/dist/py-parser-GPM5VYRG.js +16 -0
- package/dist/read.js +13 -9816
- package/dist/replace.js +15 -10014
- package/dist/rs-parser-WVQ4KYCX.js +81 -0
- package/dist/scaffold.js +4 -253
- package/dist/search.js +5 -585
- package/dist/session-kanban.js +30 -972
- package/dist/skill.js +2 -215
- package/dist/task.js +5 -3410
- package/dist/test.js +10 -3773
- package/dist/todo.js +4 -2891
- package/dist/tool-help.js +3 -123
- package/dist/tool-search.js +3 -79
- package/dist/tool-tier.js +68 -34020
- package/dist/tool-use.js +2 -93
- package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
- package/dist/tree.js +4 -288
- package/dist/ts-parser-YH4ZMWMH.js +232 -0
- package/dist/typecheck.js +10 -3753
- package/dist/win32.js +6 -60
- package/dist/write.js +14 -9882
- package/dist/yaml-parser-GFAOKV53.js +157 -0
- 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
|