@llmist/cli 16.0.1 → 16.0.3

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 CHANGED
@@ -110,7 +110,7 @@ import { Command, InvalidArgumentError as InvalidArgumentError2 } from "commande
110
110
  // package.json
111
111
  var package_default = {
112
112
  name: "@llmist/cli",
113
- version: "16.0.1",
113
+ version: "16.0.3",
114
114
  description: "CLI for llmist - run LLM agents from the command line",
115
115
  type: "module",
116
116
  main: "dist/cli.js",
@@ -167,8 +167,9 @@ var package_default = {
167
167
  node: ">=22.0.0"
168
168
  },
169
169
  dependencies: {
170
- llmist: "^16.0.1",
170
+ llmist: "^16.0.3",
171
171
  "@unblessed/node": "^1.0.0-alpha.23",
172
+ "diff-match-patch": "^1.0.5",
172
173
  chalk: "^5.6.2",
173
174
  commander: "^12.1.0",
174
175
  diff: "^8.0.2",
@@ -181,8 +182,9 @@ var package_default = {
181
182
  zod: "^4.1.12"
182
183
  },
183
184
  devDependencies: {
184
- "@llmist/testing": "^16.0.1",
185
+ "@llmist/testing": "^16.0.3",
185
186
  "@types/diff": "^8.0.0",
187
+ "@types/diff-match-patch": "^1.0.36",
186
188
  "@types/js-yaml": "^4.0.9",
187
189
  "@types/marked-terminal": "^6.1.1",
188
190
  "@types/node": "^20.12.7",
@@ -1719,18 +1721,22 @@ import { createGadget as createGadget3 } from "llmist";
1719
1721
  import { z as z3 } from "zod";
1720
1722
 
1721
1723
  // src/builtins/filesystem/editfile/matcher.ts
1724
+ import DiffMatchPatch from "diff-match-patch";
1725
+ var dmp = new DiffMatchPatch();
1722
1726
  var DEFAULT_OPTIONS = {
1723
1727
  fuzzyThreshold: 0.8,
1724
1728
  maxSuggestions: 3,
1725
1729
  contextLines: 5
1726
1730
  };
1727
1731
  function findMatch(content, search, options = {}) {
1732
+ if (!search) return null;
1728
1733
  const opts = { ...DEFAULT_OPTIONS, ...options };
1729
1734
  const strategies = [
1730
1735
  { name: "exact", fn: exactMatch },
1731
1736
  { name: "whitespace", fn: whitespaceMatch },
1732
1737
  { name: "indentation", fn: indentationMatch },
1733
- { name: "fuzzy", fn: (c, s) => fuzzyMatch(c, s, opts.fuzzyThreshold) }
1738
+ { name: "fuzzy", fn: (c, s) => fuzzyMatch(c, s, opts.fuzzyThreshold) },
1739
+ { name: "dmp", fn: (c, s) => dmpMatch(c, s, opts.fuzzyThreshold) }
1734
1740
  ];
1735
1741
  for (const { name, fn } of strategies) {
1736
1742
  const result = fn(content, search);
@@ -1796,7 +1802,8 @@ function indentationMatch(content, search) {
1796
1802
  const stripIndent = (s) => s.split("\n").map((line) => line.trimStart()).join("\n");
1797
1803
  const strippedSearch = stripIndent(search);
1798
1804
  const contentLines = content.split("\n");
1799
- const searchLineCount = search.split("\n").length;
1805
+ const searchLines = search.split("\n");
1806
+ const searchLineCount = searchLines.length;
1800
1807
  for (let i = 0; i <= contentLines.length - searchLineCount; i++) {
1801
1808
  const windowLines = contentLines.slice(i, i + searchLineCount);
1802
1809
  const strippedWindow = stripIndent(windowLines.join("\n"));
@@ -1805,6 +1812,7 @@ function indentationMatch(content, search) {
1805
1812
  const matchedContent = windowLines.join("\n");
1806
1813
  const endIndex = startIndex + matchedContent.length;
1807
1814
  const { startLine, endLine } = getLineNumbers(content, startIndex, endIndex);
1815
+ const indentationDelta = computeIndentationDelta(searchLines, windowLines);
1808
1816
  return {
1809
1817
  found: true,
1810
1818
  strategy: "indentation",
@@ -1813,7 +1821,8 @@ function indentationMatch(content, search) {
1813
1821
  startIndex,
1814
1822
  endIndex,
1815
1823
  startLine,
1816
- endLine
1824
+ endLine,
1825
+ indentationDelta
1817
1826
  };
1818
1827
  }
1819
1828
  }
@@ -1851,6 +1860,94 @@ function fuzzyMatch(content, search, threshold) {
1851
1860
  endLine
1852
1861
  };
1853
1862
  }
1863
+ function dmpMatch(content, search, threshold) {
1864
+ if (!search || !content) return null;
1865
+ if (search.length > 1e3) return null;
1866
+ if (search.split("\n").length > 20) return null;
1867
+ const matchIndex = search.length <= 32 ? dmpMatchShortPattern(content, search, threshold) : dmpMatchLongPattern(content, search, threshold);
1868
+ if (matchIndex === -1) return null;
1869
+ const matchedContent = findBestMatchExtent(content, matchIndex, search, threshold);
1870
+ if (!matchedContent) return null;
1871
+ const startIndex = matchIndex;
1872
+ const endIndex = matchIndex + matchedContent.length;
1873
+ const { startLine, endLine } = getLineNumbers(content, startIndex, endIndex);
1874
+ const similarity = stringSimilarity(search, matchedContent);
1875
+ return {
1876
+ found: true,
1877
+ strategy: "dmp",
1878
+ confidence: similarity,
1879
+ matchedContent,
1880
+ startIndex,
1881
+ endIndex,
1882
+ startLine,
1883
+ endLine
1884
+ };
1885
+ }
1886
+ function dmpMatchShortPattern(content, search, threshold) {
1887
+ dmp.Match_Threshold = 1 - threshold;
1888
+ dmp.Match_Distance = 1e3;
1889
+ const index = dmp.match_main(content, search, 0);
1890
+ return index;
1891
+ }
1892
+ function dmpMatchLongPattern(content, search, threshold) {
1893
+ const prefix = search.slice(0, 32);
1894
+ dmp.Match_Threshold = 1 - threshold;
1895
+ dmp.Match_Distance = 1e3;
1896
+ const prefixIndex = dmp.match_main(content, prefix, 0);
1897
+ if (prefixIndex === -1) return -1;
1898
+ const windowPadding = Math.max(50, Math.floor(search.length / 2));
1899
+ const windowStart = Math.max(0, prefixIndex - windowPadding);
1900
+ const windowEnd = Math.min(content.length, prefixIndex + search.length + windowPadding);
1901
+ const window = content.slice(windowStart, windowEnd);
1902
+ let bestIndex = -1;
1903
+ let bestSimilarity = 0;
1904
+ for (let i = 0; i <= window.length - search.length; i++) {
1905
+ const candidate = window.slice(i, i + search.length);
1906
+ const similarity = stringSimilarity(search, candidate);
1907
+ if (similarity >= threshold && similarity > bestSimilarity) {
1908
+ bestSimilarity = similarity;
1909
+ bestIndex = windowStart + i;
1910
+ }
1911
+ }
1912
+ return bestIndex;
1913
+ }
1914
+ function findBestMatchExtent(content, matchIndex, search, threshold) {
1915
+ const exactLength = content.slice(matchIndex, matchIndex + search.length);
1916
+ if (stringSimilarity(search, exactLength) >= threshold) {
1917
+ return exactLength;
1918
+ }
1919
+ const searchLines = search.split("\n").length;
1920
+ const contentFromMatch = content.slice(matchIndex);
1921
+ const contentLines = contentFromMatch.split("\n");
1922
+ if (contentLines.length >= searchLines) {
1923
+ const lineBasedMatch = contentLines.slice(0, searchLines).join("\n");
1924
+ if (stringSimilarity(search, lineBasedMatch) >= threshold) {
1925
+ return lineBasedMatch;
1926
+ }
1927
+ }
1928
+ return null;
1929
+ }
1930
+ function findAllMatches(content, search, options = {}) {
1931
+ const results = [];
1932
+ let searchStart = 0;
1933
+ while (searchStart < content.length) {
1934
+ const remainingContent = content.slice(searchStart);
1935
+ const match = findMatch(remainingContent, search, options);
1936
+ if (!match) break;
1937
+ results.push({
1938
+ ...match,
1939
+ startIndex: searchStart + match.startIndex,
1940
+ endIndex: searchStart + match.endIndex,
1941
+ // Recalculate line numbers for original content
1942
+ ...getLineNumbers(content, searchStart + match.startIndex, searchStart + match.endIndex)
1943
+ });
1944
+ searchStart = searchStart + match.endIndex;
1945
+ if (match.endIndex === 0) {
1946
+ searchStart++;
1947
+ }
1948
+ }
1949
+ return results;
1950
+ }
1854
1951
  function findSuggestions(content, search, maxSuggestions, minSimilarity) {
1855
1952
  const searchLines = search.split("\n");
1856
1953
  const contentLines = content.split("\n");
@@ -1967,6 +2064,78 @@ function getContext(content, lineNumber, contextLines) {
1967
2064
  });
1968
2065
  return contextWithNumbers.join("\n");
1969
2066
  }
2067
+ function getLeadingWhitespace(line) {
2068
+ const match = line.match(/^[ \t]*/);
2069
+ return match ? match[0] : "";
2070
+ }
2071
+ function computeIndentationDelta(searchLines, matchedLines) {
2072
+ for (let i = 0; i < Math.min(searchLines.length, matchedLines.length); i++) {
2073
+ const searchLine = searchLines[i];
2074
+ const matchedLine = matchedLines[i];
2075
+ if (searchLine.trim() === "" && matchedLine.trim() === "") continue;
2076
+ const searchIndent = getLeadingWhitespace(searchLine);
2077
+ const matchedIndent = getLeadingWhitespace(matchedLine);
2078
+ if (matchedIndent.length > searchIndent.length) {
2079
+ return matchedIndent.slice(searchIndent.length);
2080
+ }
2081
+ return "";
2082
+ }
2083
+ return "";
2084
+ }
2085
+ function adjustIndentation(replacement, delta) {
2086
+ if (!delta) return replacement;
2087
+ return replacement.split("\n").map((line, index) => {
2088
+ if (line.trim() === "") return line;
2089
+ return delta + line;
2090
+ }).join("\n");
2091
+ }
2092
+ function formatEditContext(originalContent, match, replacement, contextLines = 5) {
2093
+ const lines = originalContent.split("\n");
2094
+ const startLine = match.startLine - 1;
2095
+ const endLine = match.endLine;
2096
+ const contextStart = Math.max(0, startLine - contextLines);
2097
+ const contextEnd = Math.min(lines.length, endLine + contextLines);
2098
+ const output = [];
2099
+ output.push(`=== Edit (lines ${match.startLine}-${match.endLine}) ===`);
2100
+ output.push("");
2101
+ for (let i = contextStart; i < startLine; i++) {
2102
+ output.push(` ${String(i + 1).padStart(4)} | ${lines[i]}`);
2103
+ }
2104
+ for (let i = startLine; i < endLine; i++) {
2105
+ output.push(`< ${String(i + 1).padStart(4)} | ${lines[i]}`);
2106
+ }
2107
+ const replacementLines = replacement.split("\n");
2108
+ for (let i = 0; i < replacementLines.length; i++) {
2109
+ const lineNum = startLine + i + 1;
2110
+ output.push(`> ${String(lineNum).padStart(4)} | ${replacementLines[i]}`);
2111
+ }
2112
+ for (let i = endLine; i < contextEnd; i++) {
2113
+ output.push(` ${String(i + 1).padStart(4)} | ${lines[i]}`);
2114
+ }
2115
+ return output.join("\n");
2116
+ }
2117
+ function formatMultipleMatches(content, matches, maxMatches = 5) {
2118
+ const lines = content.split("\n");
2119
+ const output = [];
2120
+ output.push(`Found ${matches.length} matches:`);
2121
+ output.push("");
2122
+ const displayMatches = matches.slice(0, maxMatches);
2123
+ for (let i = 0; i < displayMatches.length; i++) {
2124
+ const match = displayMatches[i];
2125
+ output.push(`Match ${i + 1} (lines ${match.startLine}-${match.endLine}):`);
2126
+ const contextStart = Math.max(0, match.startLine - 2);
2127
+ const contextEnd = Math.min(lines.length, match.endLine + 1);
2128
+ for (let j = contextStart; j < contextEnd; j++) {
2129
+ const marker = j >= match.startLine - 1 && j < match.endLine ? ">" : " ";
2130
+ output.push(`${marker}${String(j + 1).padStart(4)} | ${lines[j]}`);
2131
+ }
2132
+ output.push("");
2133
+ }
2134
+ if (matches.length > maxMatches) {
2135
+ output.push(`... and ${matches.length - maxMatches} more matches`);
2136
+ }
2137
+ return output.join("\n");
2138
+ }
1970
2139
 
1971
2140
  // src/builtins/filesystem/utils.ts
1972
2141
  import fs from "fs";
@@ -2038,22 +2207,30 @@ Uses layered matching strategies (in order):
2038
2207
  2. Whitespace-insensitive - ignores differences in spaces/tabs
2039
2208
  3. Indentation-preserving - matches structure ignoring leading whitespace
2040
2209
  4. Fuzzy match - similarity-based matching (80% threshold)
2210
+ 5. DMP (diff-match-patch) - handles heavily refactored code
2041
2211
 
2042
2212
  For multiple edits to the same file, call this gadget multiple times.
2043
- Each call provides immediate feedback, allowing you to adjust subsequent edits.`,
2213
+ Each call provides immediate feedback, allowing you to adjust subsequent edits.
2214
+
2215
+ Options:
2216
+ - replaceAll: Replace all occurrences instead of just the first
2217
+ - expectedCount: Validate exact number of matches before applying`,
2044
2218
  maxConcurrent: 1,
2045
2219
  // Sequential execution to prevent race conditions
2046
2220
  schema: z3.object({
2047
2221
  filePath: z3.string().describe("Path to the file to edit (relative or absolute)"),
2048
2222
  search: z3.string().describe("The content to search for in the file"),
2049
- replace: z3.string().describe("The content to replace it with (empty string to delete)")
2223
+ replace: z3.string().describe("The content to replace it with (empty string to delete)"),
2224
+ replaceAll: z3.boolean().optional().default(false).describe("Replace all occurrences instead of just the first match"),
2225
+ expectedCount: z3.number().int().positive().optional().describe("Expected number of matches. Edit fails if actual count differs")
2050
2226
  }),
2051
2227
  examples: [
2052
2228
  {
2053
2229
  params: {
2054
2230
  filePath: "src/config.ts",
2055
2231
  search: "const DEBUG = false;",
2056
- replace: "const DEBUG = true;"
2232
+ replace: "const DEBUG = true;",
2233
+ replaceAll: false
2057
2234
  },
2058
2235
  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```",
2059
2236
  comment: "Simple single-line edit"
@@ -2066,7 +2243,8 @@ Each call provides immediate feedback, allowing you to adjust subsequent edits.`
2066
2243
  }`,
2067
2244
  replace: `function newHelper() {
2068
2245
  return 2;
2069
- }`
2246
+ }`,
2247
+ replaceAll: false
2070
2248
  },
2071
2249
  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```",
2072
2250
  comment: "Multi-line replacement"
@@ -2075,14 +2253,25 @@ Each call provides immediate feedback, allowing you to adjust subsequent edits.`
2075
2253
  params: {
2076
2254
  filePath: "src/app.ts",
2077
2255
  search: "unusedImport",
2078
- replace: ""
2256
+ replace: "",
2257
+ replaceAll: false
2079
2258
  },
2080
2259
  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```',
2081
2260
  comment: "Delete content by replacing with empty string"
2261
+ },
2262
+ {
2263
+ params: {
2264
+ filePath: "src/constants.ts",
2265
+ search: "OLD_VALUE",
2266
+ replace: "NEW_VALUE",
2267
+ replaceAll: true
2268
+ },
2269
+ 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```",
2270
+ comment: "Replace all occurrences with replaceAll=true"
2082
2271
  }
2083
2272
  ],
2084
2273
  timeoutMs: 3e4,
2085
- execute: ({ filePath, search, replace }) => {
2274
+ execute: ({ filePath, search, replace, replaceAll, expectedCount }) => {
2086
2275
  if (search.trim() === "") {
2087
2276
  return `path=${filePath} status=error
2088
2277
 
@@ -2112,12 +2301,43 @@ Error: File not found: ${filePath}`;
2112
2301
 
2113
2302
  Error reading file: ${message}`;
2114
2303
  }
2115
- const match = findMatch(content, search);
2116
- if (!match) {
2304
+ const allMatches = findAllMatches(content, search);
2305
+ if (allMatches.length === 0) {
2117
2306
  const failure = getMatchFailure(content, search);
2118
2307
  return formatFailure(filePath, search, failure, content);
2119
2308
  }
2120
- const newContent = applyReplacement(content, match, replace);
2309
+ if (expectedCount !== void 0 && allMatches.length !== expectedCount) {
2310
+ return `path=${filePath} status=error
2311
+
2312
+ Error: Expected ${expectedCount} match(es) but found ${allMatches.length}.
2313
+
2314
+ ${formatMultipleMatches(content, allMatches)}`;
2315
+ }
2316
+ if (allMatches.length > 1 && !replaceAll) {
2317
+ const matchSummary = formatMultipleMatches(content, allMatches);
2318
+ return `path=${filePath} status=error
2319
+
2320
+ Error: Found ${allMatches.length} matches. Please either:
2321
+ 1. Add more context to your search to make it unique
2322
+ 2. Use replaceAll=true to replace all occurrences
2323
+
2324
+ ${matchSummary}`;
2325
+ }
2326
+ let newContent;
2327
+ let editSummary;
2328
+ if (replaceAll && allMatches.length > 1) {
2329
+ newContent = executeReplaceAll(content, allMatches, replace);
2330
+ const MAX_DISPLAYED_RANGES = 5;
2331
+ const displayMatches = allMatches.slice(0, MAX_DISPLAYED_RANGES);
2332
+ const lineRanges = displayMatches.map((m) => `${m.startLine}-${m.endLine}`).join(", ");
2333
+ const suffix = allMatches.length > MAX_DISPLAYED_RANGES ? `, +${allMatches.length - MAX_DISPLAYED_RANGES} more` : "";
2334
+ editSummary = `matches=${allMatches.length} lines=[${lineRanges}${suffix}]`;
2335
+ } else {
2336
+ const match = allMatches[0];
2337
+ const finalReplace = prepareReplacement(match, replace);
2338
+ newContent = applyReplacement(content, match, finalReplace);
2339
+ editSummary = `strategy=${match.strategy} lines=${match.startLine}-${match.endLine}`;
2340
+ }
2121
2341
  try {
2122
2342
  writeFileSync(validatedPath, newContent, "utf-8");
2123
2343
  } catch (error) {
@@ -2126,9 +2346,10 @@ Error reading file: ${message}`;
2126
2346
 
2127
2347
  Error writing file: ${message}`;
2128
2348
  }
2129
- return `path=${filePath} status=success strategy=${match.strategy} lines=${match.startLine}-${match.endLine}
2349
+ const diffContext = allMatches.length === 1 ? formatEditContext(content, allMatches[0], prepareReplacement(allMatches[0], replace)) : `Replaced ${allMatches.length} occurrences`;
2350
+ return `path=${filePath} status=success ${editSummary}
2130
2351
 
2131
- Replaced content successfully.
2352
+ ${diffContext}
2132
2353
 
2133
2354
  UPDATED FILE CONTENT:
2134
2355
  \`\`\`
@@ -2136,6 +2357,21 @@ ${newContent}
2136
2357
  \`\`\``;
2137
2358
  }
2138
2359
  });
2360
+ function prepareReplacement(match, replace) {
2361
+ if (match.strategy === "indentation" && match.indentationDelta) {
2362
+ return adjustIndentation(replace, match.indentationDelta);
2363
+ }
2364
+ return replace;
2365
+ }
2366
+ function executeReplaceAll(content, matches, replace) {
2367
+ const sortedMatches = [...matches].sort((a, b) => b.startIndex - a.startIndex);
2368
+ let result = content;
2369
+ for (const match of sortedMatches) {
2370
+ const finalReplace = prepareReplacement(match, replace);
2371
+ result = applyReplacement(result, match, finalReplace);
2372
+ }
2373
+ return result;
2374
+ }
2139
2375
 
2140
2376
  // src/builtins/filesystem/list-directory.ts
2141
2377
  import fs2 from "fs";