@llmist/cli 13.0.0 → 15.0.0
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/cli.js +417 -135
- package/dist/cli.js.map +1 -1
- package/dist/index.js +408 -126
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,64 +1,257 @@
|
|
|
1
1
|
// src/builtins/filesystem/edit-file.ts
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { createGadget } from "llmist";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
|
|
5
|
-
// src/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
6
|
+
// src/builtins/filesystem/editfile/matcher.ts
|
|
7
|
+
var DEFAULT_OPTIONS = {
|
|
8
|
+
fuzzyThreshold: 0.8,
|
|
9
|
+
maxSuggestions: 3,
|
|
10
|
+
contextLines: 5
|
|
11
|
+
};
|
|
12
|
+
function findMatch(content, search, options = {}) {
|
|
13
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
14
|
+
const strategies = [
|
|
15
|
+
{ name: "exact", fn: exactMatch },
|
|
16
|
+
{ name: "whitespace", fn: whitespaceMatch },
|
|
17
|
+
{ name: "indentation", fn: indentationMatch },
|
|
18
|
+
{ name: "fuzzy", fn: (c, s) => fuzzyMatch(c, s, opts.fuzzyThreshold) }
|
|
19
|
+
];
|
|
20
|
+
for (const { name, fn } of strategies) {
|
|
21
|
+
const result = fn(content, search);
|
|
22
|
+
if (result) {
|
|
23
|
+
return { ...result, strategy: name };
|
|
23
24
|
}
|
|
24
|
-
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
25
27
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
options.stderr === "pipe" ? "pipe" : options.stderr ?? "ignore"
|
|
34
|
-
]
|
|
35
|
-
});
|
|
36
|
-
const exited = new Promise((resolve, reject) => {
|
|
37
|
-
proc.on("exit", (code) => {
|
|
38
|
-
resolve(code ?? 1);
|
|
39
|
-
});
|
|
40
|
-
proc.on("error", (err) => {
|
|
41
|
-
reject(err);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
const stdin = proc.stdin ? {
|
|
45
|
-
write(data) {
|
|
46
|
-
proc.stdin?.write(data);
|
|
47
|
-
},
|
|
48
|
-
end() {
|
|
49
|
-
proc.stdin?.end();
|
|
50
|
-
}
|
|
51
|
-
} : null;
|
|
28
|
+
function applyReplacement(content, match, replacement) {
|
|
29
|
+
return content.slice(0, match.startIndex) + replacement + content.slice(match.endIndex);
|
|
30
|
+
}
|
|
31
|
+
function getMatchFailure(content, search, options = {}) {
|
|
32
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
33
|
+
const suggestions = findSuggestions(content, search, opts.maxSuggestions, opts.fuzzyThreshold);
|
|
34
|
+
const nearbyContext = suggestions.length > 0 ? getContext(content, suggestions[0].lineNumber, opts.contextLines) : "";
|
|
52
35
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
36
|
+
reason: "Search content not found in file",
|
|
37
|
+
suggestions,
|
|
38
|
+
nearbyContext
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function exactMatch(content, search) {
|
|
42
|
+
const index = content.indexOf(search);
|
|
43
|
+
if (index === -1) return null;
|
|
44
|
+
const { startLine, endLine } = getLineNumbers(content, index, index + search.length);
|
|
45
|
+
return {
|
|
46
|
+
found: true,
|
|
47
|
+
strategy: "exact",
|
|
48
|
+
confidence: 1,
|
|
49
|
+
matchedContent: search,
|
|
50
|
+
startIndex: index,
|
|
51
|
+
endIndex: index + search.length,
|
|
52
|
+
startLine,
|
|
53
|
+
endLine
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function whitespaceMatch(content, search) {
|
|
57
|
+
const normalizeWs = (s) => s.replace(/[ \t]+/g, " ");
|
|
58
|
+
const normalizedContent = normalizeWs(content);
|
|
59
|
+
const normalizedSearch = normalizeWs(search);
|
|
60
|
+
const normalizedIndex = normalizedContent.indexOf(normalizedSearch);
|
|
61
|
+
if (normalizedIndex === -1) return null;
|
|
62
|
+
const { originalStart, originalEnd } = mapNormalizedToOriginal(
|
|
63
|
+
content,
|
|
64
|
+
normalizedIndex,
|
|
65
|
+
normalizedSearch.length
|
|
66
|
+
);
|
|
67
|
+
const matchedContent = content.slice(originalStart, originalEnd);
|
|
68
|
+
const { startLine, endLine } = getLineNumbers(content, originalStart, originalEnd);
|
|
69
|
+
return {
|
|
70
|
+
found: true,
|
|
71
|
+
strategy: "whitespace",
|
|
72
|
+
confidence: 0.95,
|
|
73
|
+
matchedContent,
|
|
74
|
+
startIndex: originalStart,
|
|
75
|
+
endIndex: originalEnd,
|
|
76
|
+
startLine,
|
|
77
|
+
endLine
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function indentationMatch(content, search) {
|
|
81
|
+
const stripIndent = (s) => s.split("\n").map((line) => line.trimStart()).join("\n");
|
|
82
|
+
const strippedSearch = stripIndent(search);
|
|
83
|
+
const contentLines = content.split("\n");
|
|
84
|
+
const searchLineCount = search.split("\n").length;
|
|
85
|
+
for (let i = 0; i <= contentLines.length - searchLineCount; i++) {
|
|
86
|
+
const windowLines = contentLines.slice(i, i + searchLineCount);
|
|
87
|
+
const strippedWindow = stripIndent(windowLines.join("\n"));
|
|
88
|
+
if (strippedWindow === strippedSearch) {
|
|
89
|
+
const startIndex = contentLines.slice(0, i).join("\n").length + (i > 0 ? 1 : 0);
|
|
90
|
+
const matchedContent = windowLines.join("\n");
|
|
91
|
+
const endIndex = startIndex + matchedContent.length;
|
|
92
|
+
const { startLine, endLine } = getLineNumbers(content, startIndex, endIndex);
|
|
93
|
+
return {
|
|
94
|
+
found: true,
|
|
95
|
+
strategy: "indentation",
|
|
96
|
+
confidence: 0.9,
|
|
97
|
+
matchedContent,
|
|
98
|
+
startIndex,
|
|
99
|
+
endIndex,
|
|
100
|
+
startLine,
|
|
101
|
+
endLine
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function fuzzyMatch(content, search, threshold) {
|
|
108
|
+
const searchLines = search.split("\n");
|
|
109
|
+
const contentLines = content.split("\n");
|
|
110
|
+
if (searchLines.length > contentLines.length) return null;
|
|
111
|
+
let bestMatch = null;
|
|
112
|
+
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
113
|
+
const windowLines = contentLines.slice(i, i + searchLines.length);
|
|
114
|
+
const similarity = calculateLineSimilarity(searchLines, windowLines);
|
|
115
|
+
if (similarity >= threshold && (!bestMatch || similarity > bestMatch.similarity)) {
|
|
116
|
+
bestMatch = {
|
|
117
|
+
startLineIndex: i,
|
|
118
|
+
endLineIndex: i + searchLines.length,
|
|
119
|
+
similarity
|
|
120
|
+
};
|
|
59
121
|
}
|
|
122
|
+
}
|
|
123
|
+
if (!bestMatch) return null;
|
|
124
|
+
const startIndex = contentLines.slice(0, bestMatch.startLineIndex).join("\n").length + (bestMatch.startLineIndex > 0 ? 1 : 0);
|
|
125
|
+
const matchedContent = contentLines.slice(bestMatch.startLineIndex, bestMatch.endLineIndex).join("\n");
|
|
126
|
+
const endIndex = startIndex + matchedContent.length;
|
|
127
|
+
const { startLine, endLine } = getLineNumbers(content, startIndex, endIndex);
|
|
128
|
+
return {
|
|
129
|
+
found: true,
|
|
130
|
+
strategy: "fuzzy",
|
|
131
|
+
confidence: bestMatch.similarity,
|
|
132
|
+
matchedContent,
|
|
133
|
+
startIndex,
|
|
134
|
+
endIndex,
|
|
135
|
+
startLine,
|
|
136
|
+
endLine
|
|
60
137
|
};
|
|
61
138
|
}
|
|
139
|
+
function findSuggestions(content, search, maxSuggestions, minSimilarity) {
|
|
140
|
+
const searchLines = search.split("\n");
|
|
141
|
+
const contentLines = content.split("\n");
|
|
142
|
+
const suggestions = [];
|
|
143
|
+
const suggestionThreshold = Math.max(0.5, minSimilarity - 0.2);
|
|
144
|
+
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
145
|
+
const windowLines = contentLines.slice(i, i + searchLines.length);
|
|
146
|
+
const similarity = calculateLineSimilarity(searchLines, windowLines);
|
|
147
|
+
if (similarity >= suggestionThreshold) {
|
|
148
|
+
suggestions.push({
|
|
149
|
+
lineIndex: i,
|
|
150
|
+
similarity,
|
|
151
|
+
content: windowLines.join("\n")
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
suggestions.sort((a, b) => b.similarity - a.similarity);
|
|
156
|
+
return suggestions.slice(0, maxSuggestions).map((s) => ({
|
|
157
|
+
content: s.content,
|
|
158
|
+
lineNumber: s.lineIndex + 1,
|
|
159
|
+
// 1-based
|
|
160
|
+
similarity: s.similarity
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
function calculateLineSimilarity(a, b) {
|
|
164
|
+
if (a.length !== b.length) return 0;
|
|
165
|
+
if (a.length === 0) return 1;
|
|
166
|
+
let totalSimilarity = 0;
|
|
167
|
+
let totalWeight = 0;
|
|
168
|
+
for (let i = 0; i < a.length; i++) {
|
|
169
|
+
const lineA = a[i];
|
|
170
|
+
const lineB = b[i];
|
|
171
|
+
const weight = Math.max(lineA.length, lineB.length, 1);
|
|
172
|
+
const similarity = stringSimilarity(lineA, lineB);
|
|
173
|
+
totalSimilarity += similarity * weight;
|
|
174
|
+
totalWeight += weight;
|
|
175
|
+
}
|
|
176
|
+
return totalWeight > 0 ? totalSimilarity / totalWeight : 0;
|
|
177
|
+
}
|
|
178
|
+
function stringSimilarity(a, b) {
|
|
179
|
+
if (a === b) return 1;
|
|
180
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
181
|
+
const distance = levenshteinDistance(a, b);
|
|
182
|
+
const maxLen = Math.max(a.length, b.length);
|
|
183
|
+
return 1 - distance / maxLen;
|
|
184
|
+
}
|
|
185
|
+
function levenshteinDistance(a, b) {
|
|
186
|
+
const matrix = [];
|
|
187
|
+
for (let i = 0; i <= b.length; i++) {
|
|
188
|
+
matrix[i] = [i];
|
|
189
|
+
}
|
|
190
|
+
for (let j = 0; j <= a.length; j++) {
|
|
191
|
+
matrix[0][j] = j;
|
|
192
|
+
}
|
|
193
|
+
for (let i = 1; i <= b.length; i++) {
|
|
194
|
+
for (let j = 1; j <= a.length; j++) {
|
|
195
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
196
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
197
|
+
} else {
|
|
198
|
+
matrix[i][j] = Math.min(
|
|
199
|
+
matrix[i - 1][j - 1] + 1,
|
|
200
|
+
// substitution
|
|
201
|
+
matrix[i][j - 1] + 1,
|
|
202
|
+
// insertion
|
|
203
|
+
matrix[i - 1][j] + 1
|
|
204
|
+
// deletion
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return matrix[b.length][a.length];
|
|
210
|
+
}
|
|
211
|
+
function getLineNumbers(content, startIndex, endIndex) {
|
|
212
|
+
const beforeStart = content.slice(0, startIndex);
|
|
213
|
+
const beforeEnd = content.slice(0, endIndex);
|
|
214
|
+
const startLine = (beforeStart.match(/\n/g) || []).length + 1;
|
|
215
|
+
const endLine = (beforeEnd.match(/\n/g) || []).length + 1;
|
|
216
|
+
return { startLine, endLine };
|
|
217
|
+
}
|
|
218
|
+
function isHorizontalWhitespace(char) {
|
|
219
|
+
return char === " " || char === " ";
|
|
220
|
+
}
|
|
221
|
+
function mapNormalizedToOriginal(original, normalizedStart, normalizedLength) {
|
|
222
|
+
const originalStart = findOriginalIndex(original, normalizedStart);
|
|
223
|
+
const originalEnd = findOriginalIndex(original, normalizedStart + normalizedLength);
|
|
224
|
+
return { originalStart, originalEnd: originalEnd === -1 ? original.length : originalEnd };
|
|
225
|
+
}
|
|
226
|
+
function findOriginalIndex(original, targetNormalizedPos) {
|
|
227
|
+
let normalizedPos = 0;
|
|
228
|
+
let inWhitespace = false;
|
|
229
|
+
for (let i = 0; i < original.length; i++) {
|
|
230
|
+
if (normalizedPos === targetNormalizedPos) {
|
|
231
|
+
return i;
|
|
232
|
+
}
|
|
233
|
+
const isWs = isHorizontalWhitespace(original[i]);
|
|
234
|
+
if (isWs && !inWhitespace) {
|
|
235
|
+
normalizedPos++;
|
|
236
|
+
inWhitespace = true;
|
|
237
|
+
} else if (!isWs) {
|
|
238
|
+
normalizedPos++;
|
|
239
|
+
inWhitespace = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return normalizedPos === targetNormalizedPos ? original.length : -1;
|
|
243
|
+
}
|
|
244
|
+
function getContext(content, lineNumber, contextLines) {
|
|
245
|
+
const lines = content.split("\n");
|
|
246
|
+
const start = Math.max(0, lineNumber - 1 - contextLines);
|
|
247
|
+
const end = Math.min(lines.length, lineNumber + contextLines);
|
|
248
|
+
const contextWithNumbers = lines.slice(start, end).map((line, i) => {
|
|
249
|
+
const num = start + i + 1;
|
|
250
|
+
const marker = num === lineNumber ? ">" : " ";
|
|
251
|
+
return `${marker}${String(num).padStart(4)} | ${line}`;
|
|
252
|
+
});
|
|
253
|
+
return contextWithNumbers.join("\n");
|
|
254
|
+
}
|
|
62
255
|
|
|
63
256
|
// src/builtins/filesystem/utils.ts
|
|
64
257
|
import fs from "fs";
|
|
@@ -91,110 +284,139 @@ function validatePathIsWithinCwd(inputPath) {
|
|
|
91
284
|
}
|
|
92
285
|
|
|
93
286
|
// src/builtins/filesystem/edit-file.ts
|
|
94
|
-
function
|
|
95
|
-
|
|
287
|
+
function formatFailure(filePath, search, failure, fileContent) {
|
|
288
|
+
const lines = [
|
|
289
|
+
`path=${filePath} status=failed`,
|
|
290
|
+
"",
|
|
291
|
+
`Error: ${failure.reason}`,
|
|
292
|
+
"",
|
|
293
|
+
"SEARCH CONTENT:",
|
|
294
|
+
"```",
|
|
295
|
+
search,
|
|
296
|
+
"```"
|
|
297
|
+
];
|
|
298
|
+
if (failure.suggestions.length > 0) {
|
|
299
|
+
lines.push("", "SUGGESTIONS (similar content found):");
|
|
300
|
+
for (const suggestion of failure.suggestions) {
|
|
301
|
+
const percent = Math.round(suggestion.similarity * 100);
|
|
302
|
+
lines.push(
|
|
303
|
+
"",
|
|
304
|
+
`Line ${suggestion.lineNumber} (${percent}% similar):`,
|
|
305
|
+
"```",
|
|
306
|
+
suggestion.content,
|
|
307
|
+
"```"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (failure.nearbyContext) {
|
|
311
|
+
lines.push("", "CONTEXT:", failure.nearbyContext);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
lines.push("", "CURRENT FILE CONTENT:", "```", fileContent, "```");
|
|
315
|
+
return lines.join("\n");
|
|
96
316
|
}
|
|
97
317
|
var editFile = createGadget({
|
|
98
318
|
name: "EditFile",
|
|
99
|
-
description:
|
|
319
|
+
description: `Edit a file by searching for content and replacing it.
|
|
320
|
+
|
|
321
|
+
Uses layered matching strategies (in order):
|
|
322
|
+
1. Exact match - byte-for-byte comparison
|
|
323
|
+
2. Whitespace-insensitive - ignores differences in spaces/tabs
|
|
324
|
+
3. Indentation-preserving - matches structure ignoring leading whitespace
|
|
325
|
+
4. Fuzzy match - similarity-based matching (80% threshold)
|
|
326
|
+
|
|
327
|
+
For multiple edits to the same file, call this gadget multiple times.
|
|
328
|
+
Each call provides immediate feedback, allowing you to adjust subsequent edits.`,
|
|
100
329
|
schema: z.object({
|
|
101
330
|
filePath: z.string().describe("Path to the file to edit (relative or absolute)"),
|
|
102
|
-
|
|
331
|
+
search: z.string().describe("The content to search for in the file"),
|
|
332
|
+
replace: z.string().describe("The content to replace it with (empty string to delete)")
|
|
103
333
|
}),
|
|
104
334
|
examples: [
|
|
105
335
|
{
|
|
106
336
|
params: {
|
|
107
|
-
filePath: "config.
|
|
108
|
-
|
|
109
|
-
|
|
337
|
+
filePath: "src/config.ts",
|
|
338
|
+
search: "const DEBUG = false;",
|
|
339
|
+
replace: "const DEBUG = true;"
|
|
110
340
|
},
|
|
111
|
-
output: "path=config.
|
|
112
|
-
comment: "
|
|
341
|
+
output: "path=src/config.ts status=success strategy=exact lines=5-5\n\nReplaced content successfully.\n\nUPDATED FILE CONTENT:\n```\n// config.ts\nconst DEBUG = true;\nexport default { DEBUG };\n```",
|
|
342
|
+
comment: "Simple single-line edit"
|
|
113
343
|
},
|
|
114
344
|
{
|
|
115
345
|
params: {
|
|
116
|
-
filePath: "
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
346
|
+
filePath: "src/utils.ts",
|
|
347
|
+
search: `function oldHelper() {
|
|
348
|
+
return 1;
|
|
349
|
+
}`,
|
|
350
|
+
replace: `function newHelper() {
|
|
351
|
+
return 2;
|
|
352
|
+
}`
|
|
120
353
|
},
|
|
121
|
-
output: "path=
|
|
122
|
-
comment: "
|
|
354
|
+
output: "path=src/utils.ts status=success strategy=exact lines=10-12\n\nReplaced content successfully.\n\nUPDATED FILE CONTENT:\n```\n// utils.ts\nfunction newHelper() {\n return 2;\n}\n```",
|
|
355
|
+
comment: "Multi-line replacement"
|
|
123
356
|
},
|
|
124
357
|
{
|
|
125
358
|
params: {
|
|
126
|
-
filePath: "
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
q`
|
|
359
|
+
filePath: "src/app.ts",
|
|
360
|
+
search: "unusedImport",
|
|
361
|
+
replace: ""
|
|
130
362
|
},
|
|
131
|
-
output:
|
|
132
|
-
comment: "Delete
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
params: {
|
|
136
|
-
filePath: "readme.txt",
|
|
137
|
-
commands: `$a
|
|
138
|
-
New last line
|
|
139
|
-
.
|
|
140
|
-
w
|
|
141
|
-
q`
|
|
142
|
-
},
|
|
143
|
-
output: "path=readme.txt\n\n40\n56",
|
|
144
|
-
comment: "Append text after last line ($ = last line, . = end input mode)"
|
|
363
|
+
output: 'path=src/app.ts status=success strategy=exact lines=3-3\n\nReplaced content successfully.\n\nUPDATED FILE CONTENT:\n```\n// app.ts\nimport { usedImport } from "./lib";\n```',
|
|
364
|
+
comment: "Delete content by replacing with empty string"
|
|
145
365
|
}
|
|
146
366
|
],
|
|
147
367
|
timeoutMs: 3e4,
|
|
148
|
-
execute:
|
|
149
|
-
|
|
150
|
-
|
|
368
|
+
execute: ({ filePath, search, replace }) => {
|
|
369
|
+
if (search.trim() === "") {
|
|
370
|
+
return `path=${filePath} status=error
|
|
371
|
+
|
|
372
|
+
Error: Search content cannot be empty.`;
|
|
373
|
+
}
|
|
374
|
+
let validatedPath;
|
|
151
375
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
157
|
-
if (!proc.stdin) {
|
|
158
|
-
return `path=${filePath}
|
|
376
|
+
validatedPath = validatePathIsWithinCwd(filePath);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
379
|
+
return `path=${filePath} status=error
|
|
159
380
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
reject(new Error("ed command timed out after 30000ms"));
|
|
170
|
-
}, 3e4);
|
|
171
|
-
});
|
|
172
|
-
const [exitCode, stdout, stderr] = await Promise.race([
|
|
173
|
-
Promise.all([
|
|
174
|
-
proc.exited,
|
|
175
|
-
new Response(proc.stdout).text(),
|
|
176
|
-
new Response(proc.stderr).text()
|
|
177
|
-
]),
|
|
178
|
-
timeoutPromise
|
|
179
|
-
]);
|
|
180
|
-
if (timeoutId) {
|
|
181
|
-
clearTimeout(timeoutId);
|
|
182
|
-
}
|
|
183
|
-
const output = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
184
|
-
if (exitCode !== 0) {
|
|
185
|
-
return `path=${filePath}
|
|
381
|
+
Error: ${message}`;
|
|
382
|
+
}
|
|
383
|
+
let content;
|
|
384
|
+
try {
|
|
385
|
+
content = readFileSync(validatedPath, "utf-8");
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const nodeError = error;
|
|
388
|
+
if (nodeError.code === "ENOENT") {
|
|
389
|
+
return `path=${filePath} status=error
|
|
186
390
|
|
|
187
|
-
|
|
391
|
+
Error: File not found: ${filePath}`;
|
|
188
392
|
}
|
|
189
|
-
|
|
393
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
394
|
+
return `path=${filePath} status=error
|
|
190
395
|
|
|
191
|
-
|
|
396
|
+
Error reading file: ${message}`;
|
|
397
|
+
}
|
|
398
|
+
const match = findMatch(content, search);
|
|
399
|
+
if (!match) {
|
|
400
|
+
const failure = getMatchFailure(content, search);
|
|
401
|
+
return formatFailure(filePath, search, failure, content);
|
|
402
|
+
}
|
|
403
|
+
const newContent = applyReplacement(content, match, replace);
|
|
404
|
+
try {
|
|
405
|
+
writeFileSync(validatedPath, newContent, "utf-8");
|
|
192
406
|
} catch (error) {
|
|
193
407
|
const message = error instanceof Error ? error.message : String(error);
|
|
194
|
-
return `path=${filePath}
|
|
408
|
+
return `path=${filePath} status=error
|
|
195
409
|
|
|
196
|
-
|
|
410
|
+
Error writing file: ${message}`;
|
|
197
411
|
}
|
|
412
|
+
return `path=${filePath} status=success strategy=${match.strategy} lines=${match.startLine}-${match.endLine}
|
|
413
|
+
|
|
414
|
+
Replaced content successfully.
|
|
415
|
+
|
|
416
|
+
UPDATED FILE CONTENT:
|
|
417
|
+
\`\`\`
|
|
418
|
+
${newContent}
|
|
419
|
+
\`\`\``;
|
|
198
420
|
}
|
|
199
421
|
});
|
|
200
422
|
|
|
@@ -409,6 +631,66 @@ Wrote ${bytesWritten} bytes${dirNote}`;
|
|
|
409
631
|
// src/builtins/run-command.ts
|
|
410
632
|
import { z as z5 } from "zod";
|
|
411
633
|
import { createGadget as createGadget5 } from "llmist";
|
|
634
|
+
|
|
635
|
+
// src/spawn.ts
|
|
636
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
637
|
+
function nodeStreamToReadableStream(nodeStream) {
|
|
638
|
+
if (!nodeStream) return null;
|
|
639
|
+
return new ReadableStream({
|
|
640
|
+
start(controller) {
|
|
641
|
+
nodeStream.on("data", (chunk) => {
|
|
642
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
643
|
+
});
|
|
644
|
+
nodeStream.on("end", () => {
|
|
645
|
+
controller.close();
|
|
646
|
+
});
|
|
647
|
+
nodeStream.on("error", (err) => {
|
|
648
|
+
controller.error(err);
|
|
649
|
+
});
|
|
650
|
+
},
|
|
651
|
+
cancel() {
|
|
652
|
+
nodeStream.destroy();
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
function spawn(argv, options = {}) {
|
|
657
|
+
const [command, ...args] = argv;
|
|
658
|
+
const proc = nodeSpawn(command, args, {
|
|
659
|
+
cwd: options.cwd,
|
|
660
|
+
stdio: [
|
|
661
|
+
options.stdin === "pipe" ? "pipe" : options.stdin ?? "ignore",
|
|
662
|
+
options.stdout === "pipe" ? "pipe" : options.stdout ?? "ignore",
|
|
663
|
+
options.stderr === "pipe" ? "pipe" : options.stderr ?? "ignore"
|
|
664
|
+
]
|
|
665
|
+
});
|
|
666
|
+
const exited = new Promise((resolve, reject) => {
|
|
667
|
+
proc.on("exit", (code) => {
|
|
668
|
+
resolve(code ?? 1);
|
|
669
|
+
});
|
|
670
|
+
proc.on("error", (err) => {
|
|
671
|
+
reject(err);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
const stdin = proc.stdin ? {
|
|
675
|
+
write(data) {
|
|
676
|
+
proc.stdin?.write(data);
|
|
677
|
+
},
|
|
678
|
+
end() {
|
|
679
|
+
proc.stdin?.end();
|
|
680
|
+
}
|
|
681
|
+
} : null;
|
|
682
|
+
return {
|
|
683
|
+
exited,
|
|
684
|
+
stdout: nodeStreamToReadableStream(proc.stdout),
|
|
685
|
+
stderr: nodeStreamToReadableStream(proc.stderr),
|
|
686
|
+
stdin,
|
|
687
|
+
kill() {
|
|
688
|
+
proc.kill();
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/builtins/run-command.ts
|
|
412
694
|
var runCommand = createGadget5({
|
|
413
695
|
name: "RunCommand",
|
|
414
696
|
description: "Execute a command with arguments and return its output. Uses argv array to bypass shell - arguments are passed directly without interpretation. Returns stdout/stderr combined with exit status.",
|