@llmist/cli 16.0.1 → 16.0.2
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 +253 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.js +248 -14
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -34,18 +34,22 @@ import { createGadget } from "llmist";
|
|
|
34
34
|
import { z } from "zod";
|
|
35
35
|
|
|
36
36
|
// src/builtins/filesystem/editfile/matcher.ts
|
|
37
|
+
import DiffMatchPatch from "diff-match-patch";
|
|
38
|
+
var dmp = new DiffMatchPatch();
|
|
37
39
|
var DEFAULT_OPTIONS = {
|
|
38
40
|
fuzzyThreshold: 0.8,
|
|
39
41
|
maxSuggestions: 3,
|
|
40
42
|
contextLines: 5
|
|
41
43
|
};
|
|
42
44
|
function findMatch(content, search, options = {}) {
|
|
45
|
+
if (!search) return null;
|
|
43
46
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
44
47
|
const strategies = [
|
|
45
48
|
{ name: "exact", fn: exactMatch },
|
|
46
49
|
{ name: "whitespace", fn: whitespaceMatch },
|
|
47
50
|
{ name: "indentation", fn: indentationMatch },
|
|
48
|
-
{ name: "fuzzy", fn: (c, s) => fuzzyMatch(c, s, opts.fuzzyThreshold) }
|
|
51
|
+
{ name: "fuzzy", fn: (c, s) => fuzzyMatch(c, s, opts.fuzzyThreshold) },
|
|
52
|
+
{ name: "dmp", fn: (c, s) => dmpMatch(c, s, opts.fuzzyThreshold) }
|
|
49
53
|
];
|
|
50
54
|
for (const { name, fn } of strategies) {
|
|
51
55
|
const result = fn(content, search);
|
|
@@ -111,7 +115,8 @@ function indentationMatch(content, search) {
|
|
|
111
115
|
const stripIndent = (s) => s.split("\n").map((line) => line.trimStart()).join("\n");
|
|
112
116
|
const strippedSearch = stripIndent(search);
|
|
113
117
|
const contentLines = content.split("\n");
|
|
114
|
-
const
|
|
118
|
+
const searchLines = search.split("\n");
|
|
119
|
+
const searchLineCount = searchLines.length;
|
|
115
120
|
for (let i = 0; i <= contentLines.length - searchLineCount; i++) {
|
|
116
121
|
const windowLines = contentLines.slice(i, i + searchLineCount);
|
|
117
122
|
const strippedWindow = stripIndent(windowLines.join("\n"));
|
|
@@ -120,6 +125,7 @@ function indentationMatch(content, search) {
|
|
|
120
125
|
const matchedContent = windowLines.join("\n");
|
|
121
126
|
const endIndex = startIndex + matchedContent.length;
|
|
122
127
|
const { startLine, endLine } = getLineNumbers(content, startIndex, endIndex);
|
|
128
|
+
const indentationDelta = computeIndentationDelta(searchLines, windowLines);
|
|
123
129
|
return {
|
|
124
130
|
found: true,
|
|
125
131
|
strategy: "indentation",
|
|
@@ -128,7 +134,8 @@ function indentationMatch(content, search) {
|
|
|
128
134
|
startIndex,
|
|
129
135
|
endIndex,
|
|
130
136
|
startLine,
|
|
131
|
-
endLine
|
|
137
|
+
endLine,
|
|
138
|
+
indentationDelta
|
|
132
139
|
};
|
|
133
140
|
}
|
|
134
141
|
}
|
|
@@ -166,6 +173,94 @@ function fuzzyMatch(content, search, threshold) {
|
|
|
166
173
|
endLine
|
|
167
174
|
};
|
|
168
175
|
}
|
|
176
|
+
function dmpMatch(content, search, threshold) {
|
|
177
|
+
if (!search || !content) return null;
|
|
178
|
+
if (search.length > 1e3) return null;
|
|
179
|
+
if (search.split("\n").length > 20) return null;
|
|
180
|
+
const matchIndex = search.length <= 32 ? dmpMatchShortPattern(content, search, threshold) : dmpMatchLongPattern(content, search, threshold);
|
|
181
|
+
if (matchIndex === -1) return null;
|
|
182
|
+
const matchedContent = findBestMatchExtent(content, matchIndex, search, threshold);
|
|
183
|
+
if (!matchedContent) return null;
|
|
184
|
+
const startIndex = matchIndex;
|
|
185
|
+
const endIndex = matchIndex + matchedContent.length;
|
|
186
|
+
const { startLine, endLine } = getLineNumbers(content, startIndex, endIndex);
|
|
187
|
+
const similarity = stringSimilarity(search, matchedContent);
|
|
188
|
+
return {
|
|
189
|
+
found: true,
|
|
190
|
+
strategy: "dmp",
|
|
191
|
+
confidence: similarity,
|
|
192
|
+
matchedContent,
|
|
193
|
+
startIndex,
|
|
194
|
+
endIndex,
|
|
195
|
+
startLine,
|
|
196
|
+
endLine
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function dmpMatchShortPattern(content, search, threshold) {
|
|
200
|
+
dmp.Match_Threshold = 1 - threshold;
|
|
201
|
+
dmp.Match_Distance = 1e3;
|
|
202
|
+
const index = dmp.match_main(content, search, 0);
|
|
203
|
+
return index;
|
|
204
|
+
}
|
|
205
|
+
function dmpMatchLongPattern(content, search, threshold) {
|
|
206
|
+
const prefix = search.slice(0, 32);
|
|
207
|
+
dmp.Match_Threshold = 1 - threshold;
|
|
208
|
+
dmp.Match_Distance = 1e3;
|
|
209
|
+
const prefixIndex = dmp.match_main(content, prefix, 0);
|
|
210
|
+
if (prefixIndex === -1) return -1;
|
|
211
|
+
const windowPadding = Math.max(50, Math.floor(search.length / 2));
|
|
212
|
+
const windowStart = Math.max(0, prefixIndex - windowPadding);
|
|
213
|
+
const windowEnd = Math.min(content.length, prefixIndex + search.length + windowPadding);
|
|
214
|
+
const window = content.slice(windowStart, windowEnd);
|
|
215
|
+
let bestIndex = -1;
|
|
216
|
+
let bestSimilarity = 0;
|
|
217
|
+
for (let i = 0; i <= window.length - search.length; i++) {
|
|
218
|
+
const candidate = window.slice(i, i + search.length);
|
|
219
|
+
const similarity = stringSimilarity(search, candidate);
|
|
220
|
+
if (similarity >= threshold && similarity > bestSimilarity) {
|
|
221
|
+
bestSimilarity = similarity;
|
|
222
|
+
bestIndex = windowStart + i;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return bestIndex;
|
|
226
|
+
}
|
|
227
|
+
function findBestMatchExtent(content, matchIndex, search, threshold) {
|
|
228
|
+
const exactLength = content.slice(matchIndex, matchIndex + search.length);
|
|
229
|
+
if (stringSimilarity(search, exactLength) >= threshold) {
|
|
230
|
+
return exactLength;
|
|
231
|
+
}
|
|
232
|
+
const searchLines = search.split("\n").length;
|
|
233
|
+
const contentFromMatch = content.slice(matchIndex);
|
|
234
|
+
const contentLines = contentFromMatch.split("\n");
|
|
235
|
+
if (contentLines.length >= searchLines) {
|
|
236
|
+
const lineBasedMatch = contentLines.slice(0, searchLines).join("\n");
|
|
237
|
+
if (stringSimilarity(search, lineBasedMatch) >= threshold) {
|
|
238
|
+
return lineBasedMatch;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
function findAllMatches(content, search, options = {}) {
|
|
244
|
+
const results = [];
|
|
245
|
+
let searchStart = 0;
|
|
246
|
+
while (searchStart < content.length) {
|
|
247
|
+
const remainingContent = content.slice(searchStart);
|
|
248
|
+
const match = findMatch(remainingContent, search, options);
|
|
249
|
+
if (!match) break;
|
|
250
|
+
results.push({
|
|
251
|
+
...match,
|
|
252
|
+
startIndex: searchStart + match.startIndex,
|
|
253
|
+
endIndex: searchStart + match.endIndex,
|
|
254
|
+
// Recalculate line numbers for original content
|
|
255
|
+
...getLineNumbers(content, searchStart + match.startIndex, searchStart + match.endIndex)
|
|
256
|
+
});
|
|
257
|
+
searchStart = searchStart + match.endIndex;
|
|
258
|
+
if (match.endIndex === 0) {
|
|
259
|
+
searchStart++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return results;
|
|
263
|
+
}
|
|
169
264
|
function findSuggestions(content, search, maxSuggestions, minSimilarity) {
|
|
170
265
|
const searchLines = search.split("\n");
|
|
171
266
|
const contentLines = content.split("\n");
|
|
@@ -282,6 +377,78 @@ function getContext(content, lineNumber, contextLines) {
|
|
|
282
377
|
});
|
|
283
378
|
return contextWithNumbers.join("\n");
|
|
284
379
|
}
|
|
380
|
+
function getLeadingWhitespace(line) {
|
|
381
|
+
const match = line.match(/^[ \t]*/);
|
|
382
|
+
return match ? match[0] : "";
|
|
383
|
+
}
|
|
384
|
+
function computeIndentationDelta(searchLines, matchedLines) {
|
|
385
|
+
for (let i = 0; i < Math.min(searchLines.length, matchedLines.length); i++) {
|
|
386
|
+
const searchLine = searchLines[i];
|
|
387
|
+
const matchedLine = matchedLines[i];
|
|
388
|
+
if (searchLine.trim() === "" && matchedLine.trim() === "") continue;
|
|
389
|
+
const searchIndent = getLeadingWhitespace(searchLine);
|
|
390
|
+
const matchedIndent = getLeadingWhitespace(matchedLine);
|
|
391
|
+
if (matchedIndent.length > searchIndent.length) {
|
|
392
|
+
return matchedIndent.slice(searchIndent.length);
|
|
393
|
+
}
|
|
394
|
+
return "";
|
|
395
|
+
}
|
|
396
|
+
return "";
|
|
397
|
+
}
|
|
398
|
+
function adjustIndentation(replacement, delta) {
|
|
399
|
+
if (!delta) return replacement;
|
|
400
|
+
return replacement.split("\n").map((line, index) => {
|
|
401
|
+
if (line.trim() === "") return line;
|
|
402
|
+
return delta + line;
|
|
403
|
+
}).join("\n");
|
|
404
|
+
}
|
|
405
|
+
function formatEditContext(originalContent, match, replacement, contextLines = 5) {
|
|
406
|
+
const lines = originalContent.split("\n");
|
|
407
|
+
const startLine = match.startLine - 1;
|
|
408
|
+
const endLine = match.endLine;
|
|
409
|
+
const contextStart = Math.max(0, startLine - contextLines);
|
|
410
|
+
const contextEnd = Math.min(lines.length, endLine + contextLines);
|
|
411
|
+
const output = [];
|
|
412
|
+
output.push(`=== Edit (lines ${match.startLine}-${match.endLine}) ===`);
|
|
413
|
+
output.push("");
|
|
414
|
+
for (let i = contextStart; i < startLine; i++) {
|
|
415
|
+
output.push(` ${String(i + 1).padStart(4)} | ${lines[i]}`);
|
|
416
|
+
}
|
|
417
|
+
for (let i = startLine; i < endLine; i++) {
|
|
418
|
+
output.push(`< ${String(i + 1).padStart(4)} | ${lines[i]}`);
|
|
419
|
+
}
|
|
420
|
+
const replacementLines = replacement.split("\n");
|
|
421
|
+
for (let i = 0; i < replacementLines.length; i++) {
|
|
422
|
+
const lineNum = startLine + i + 1;
|
|
423
|
+
output.push(`> ${String(lineNum).padStart(4)} | ${replacementLines[i]}`);
|
|
424
|
+
}
|
|
425
|
+
for (let i = endLine; i < contextEnd; i++) {
|
|
426
|
+
output.push(` ${String(i + 1).padStart(4)} | ${lines[i]}`);
|
|
427
|
+
}
|
|
428
|
+
return output.join("\n");
|
|
429
|
+
}
|
|
430
|
+
function formatMultipleMatches(content, matches, maxMatches = 5) {
|
|
431
|
+
const lines = content.split("\n");
|
|
432
|
+
const output = [];
|
|
433
|
+
output.push(`Found ${matches.length} matches:`);
|
|
434
|
+
output.push("");
|
|
435
|
+
const displayMatches = matches.slice(0, maxMatches);
|
|
436
|
+
for (let i = 0; i < displayMatches.length; i++) {
|
|
437
|
+
const match = displayMatches[i];
|
|
438
|
+
output.push(`Match ${i + 1} (lines ${match.startLine}-${match.endLine}):`);
|
|
439
|
+
const contextStart = Math.max(0, match.startLine - 2);
|
|
440
|
+
const contextEnd = Math.min(lines.length, match.endLine + 1);
|
|
441
|
+
for (let j = contextStart; j < contextEnd; j++) {
|
|
442
|
+
const marker = j >= match.startLine - 1 && j < match.endLine ? ">" : " ";
|
|
443
|
+
output.push(`${marker}${String(j + 1).padStart(4)} | ${lines[j]}`);
|
|
444
|
+
}
|
|
445
|
+
output.push("");
|
|
446
|
+
}
|
|
447
|
+
if (matches.length > maxMatches) {
|
|
448
|
+
output.push(`... and ${matches.length - maxMatches} more matches`);
|
|
449
|
+
}
|
|
450
|
+
return output.join("\n");
|
|
451
|
+
}
|
|
285
452
|
|
|
286
453
|
// src/builtins/filesystem/edit-file.ts
|
|
287
454
|
function formatFailure(filePath, search, failure, fileContent) {
|
|
@@ -323,22 +490,30 @@ Uses layered matching strategies (in order):
|
|
|
323
490
|
2. Whitespace-insensitive - ignores differences in spaces/tabs
|
|
324
491
|
3. Indentation-preserving - matches structure ignoring leading whitespace
|
|
325
492
|
4. Fuzzy match - similarity-based matching (80% threshold)
|
|
493
|
+
5. DMP (diff-match-patch) - handles heavily refactored code
|
|
326
494
|
|
|
327
495
|
For multiple edits to the same file, call this gadget multiple times.
|
|
328
|
-
Each call provides immediate feedback, allowing you to adjust subsequent edits
|
|
496
|
+
Each call provides immediate feedback, allowing you to adjust subsequent edits.
|
|
497
|
+
|
|
498
|
+
Options:
|
|
499
|
+
- replaceAll: Replace all occurrences instead of just the first
|
|
500
|
+
- expectedCount: Validate exact number of matches before applying`,
|
|
329
501
|
maxConcurrent: 1,
|
|
330
502
|
// Sequential execution to prevent race conditions
|
|
331
503
|
schema: z.object({
|
|
332
504
|
filePath: z.string().describe("Path to the file to edit (relative or absolute)"),
|
|
333
505
|
search: z.string().describe("The content to search for in the file"),
|
|
334
|
-
replace: z.string().describe("The content to replace it with (empty string to delete)")
|
|
506
|
+
replace: z.string().describe("The content to replace it with (empty string to delete)"),
|
|
507
|
+
replaceAll: z.boolean().optional().default(false).describe("Replace all occurrences instead of just the first match"),
|
|
508
|
+
expectedCount: z.number().int().positive().optional().describe("Expected number of matches. Edit fails if actual count differs")
|
|
335
509
|
}),
|
|
336
510
|
examples: [
|
|
337
511
|
{
|
|
338
512
|
params: {
|
|
339
513
|
filePath: "src/config.ts",
|
|
340
514
|
search: "const DEBUG = false;",
|
|
341
|
-
replace: "const DEBUG = true;"
|
|
515
|
+
replace: "const DEBUG = true;",
|
|
516
|
+
replaceAll: false
|
|
342
517
|
},
|
|
343
518
|
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```",
|
|
344
519
|
comment: "Simple single-line edit"
|
|
@@ -351,7 +526,8 @@ Each call provides immediate feedback, allowing you to adjust subsequent edits.`
|
|
|
351
526
|
}`,
|
|
352
527
|
replace: `function newHelper() {
|
|
353
528
|
return 2;
|
|
354
|
-
}
|
|
529
|
+
}`,
|
|
530
|
+
replaceAll: false
|
|
355
531
|
},
|
|
356
532
|
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```",
|
|
357
533
|
comment: "Multi-line replacement"
|
|
@@ -360,14 +536,25 @@ Each call provides immediate feedback, allowing you to adjust subsequent edits.`
|
|
|
360
536
|
params: {
|
|
361
537
|
filePath: "src/app.ts",
|
|
362
538
|
search: "unusedImport",
|
|
363
|
-
replace: ""
|
|
539
|
+
replace: "",
|
|
540
|
+
replaceAll: false
|
|
364
541
|
},
|
|
365
542
|
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```',
|
|
366
543
|
comment: "Delete content by replacing with empty string"
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
params: {
|
|
547
|
+
filePath: "src/constants.ts",
|
|
548
|
+
search: "OLD_VALUE",
|
|
549
|
+
replace: "NEW_VALUE",
|
|
550
|
+
replaceAll: true
|
|
551
|
+
},
|
|
552
|
+
output: "path=src/constants.ts status=success matches=3 lines=[2-2, 5-5, 8-8]\n\nReplaced 3 occurrences\n\nUPDATED FILE CONTENT:\n```\n// constants.ts\nexport const A = NEW_VALUE;\nexport const B = NEW_VALUE;\nexport const C = NEW_VALUE;\n```",
|
|
553
|
+
comment: "Replace all occurrences with replaceAll=true"
|
|
367
554
|
}
|
|
368
555
|
],
|
|
369
556
|
timeoutMs: 3e4,
|
|
370
|
-
execute: ({ filePath, search, replace }) => {
|
|
557
|
+
execute: ({ filePath, search, replace, replaceAll, expectedCount }) => {
|
|
371
558
|
if (search.trim() === "") {
|
|
372
559
|
return `path=${filePath} status=error
|
|
373
560
|
|
|
@@ -397,12 +584,43 @@ Error: File not found: ${filePath}`;
|
|
|
397
584
|
|
|
398
585
|
Error reading file: ${message}`;
|
|
399
586
|
}
|
|
400
|
-
const
|
|
401
|
-
if (
|
|
587
|
+
const allMatches = findAllMatches(content, search);
|
|
588
|
+
if (allMatches.length === 0) {
|
|
402
589
|
const failure = getMatchFailure(content, search);
|
|
403
590
|
return formatFailure(filePath, search, failure, content);
|
|
404
591
|
}
|
|
405
|
-
|
|
592
|
+
if (expectedCount !== void 0 && allMatches.length !== expectedCount) {
|
|
593
|
+
return `path=${filePath} status=error
|
|
594
|
+
|
|
595
|
+
Error: Expected ${expectedCount} match(es) but found ${allMatches.length}.
|
|
596
|
+
|
|
597
|
+
${formatMultipleMatches(content, allMatches)}`;
|
|
598
|
+
}
|
|
599
|
+
if (allMatches.length > 1 && !replaceAll) {
|
|
600
|
+
const matchSummary = formatMultipleMatches(content, allMatches);
|
|
601
|
+
return `path=${filePath} status=error
|
|
602
|
+
|
|
603
|
+
Error: Found ${allMatches.length} matches. Please either:
|
|
604
|
+
1. Add more context to your search to make it unique
|
|
605
|
+
2. Use replaceAll=true to replace all occurrences
|
|
606
|
+
|
|
607
|
+
${matchSummary}`;
|
|
608
|
+
}
|
|
609
|
+
let newContent;
|
|
610
|
+
let editSummary;
|
|
611
|
+
if (replaceAll && allMatches.length > 1) {
|
|
612
|
+
newContent = executeReplaceAll(content, allMatches, replace);
|
|
613
|
+
const MAX_DISPLAYED_RANGES = 5;
|
|
614
|
+
const displayMatches = allMatches.slice(0, MAX_DISPLAYED_RANGES);
|
|
615
|
+
const lineRanges = displayMatches.map((m) => `${m.startLine}-${m.endLine}`).join(", ");
|
|
616
|
+
const suffix = allMatches.length > MAX_DISPLAYED_RANGES ? `, +${allMatches.length - MAX_DISPLAYED_RANGES} more` : "";
|
|
617
|
+
editSummary = `matches=${allMatches.length} lines=[${lineRanges}${suffix}]`;
|
|
618
|
+
} else {
|
|
619
|
+
const match = allMatches[0];
|
|
620
|
+
const finalReplace = prepareReplacement(match, replace);
|
|
621
|
+
newContent = applyReplacement(content, match, finalReplace);
|
|
622
|
+
editSummary = `strategy=${match.strategy} lines=${match.startLine}-${match.endLine}`;
|
|
623
|
+
}
|
|
406
624
|
try {
|
|
407
625
|
writeFileSync(validatedPath, newContent, "utf-8");
|
|
408
626
|
} catch (error) {
|
|
@@ -411,9 +629,10 @@ Error reading file: ${message}`;
|
|
|
411
629
|
|
|
412
630
|
Error writing file: ${message}`;
|
|
413
631
|
}
|
|
414
|
-
|
|
632
|
+
const diffContext = allMatches.length === 1 ? formatEditContext(content, allMatches[0], prepareReplacement(allMatches[0], replace)) : `Replaced ${allMatches.length} occurrences`;
|
|
633
|
+
return `path=${filePath} status=success ${editSummary}
|
|
415
634
|
|
|
416
|
-
|
|
635
|
+
${diffContext}
|
|
417
636
|
|
|
418
637
|
UPDATED FILE CONTENT:
|
|
419
638
|
\`\`\`
|
|
@@ -421,6 +640,21 @@ ${newContent}
|
|
|
421
640
|
\`\`\``;
|
|
422
641
|
}
|
|
423
642
|
});
|
|
643
|
+
function prepareReplacement(match, replace) {
|
|
644
|
+
if (match.strategy === "indentation" && match.indentationDelta) {
|
|
645
|
+
return adjustIndentation(replace, match.indentationDelta);
|
|
646
|
+
}
|
|
647
|
+
return replace;
|
|
648
|
+
}
|
|
649
|
+
function executeReplaceAll(content, matches, replace) {
|
|
650
|
+
const sortedMatches = [...matches].sort((a, b) => b.startIndex - a.startIndex);
|
|
651
|
+
let result = content;
|
|
652
|
+
for (const match of sortedMatches) {
|
|
653
|
+
const finalReplace = prepareReplacement(match, replace);
|
|
654
|
+
result = applyReplacement(result, match, finalReplace);
|
|
655
|
+
}
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
424
658
|
|
|
425
659
|
// src/builtins/filesystem/list-directory.ts
|
|
426
660
|
import fs2 from "fs";
|