@llmist/cli 16.0.0 → 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 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.0",
113
+ version: "16.0.2",
114
114
  description: "CLI for llmist - run LLM agents from the command line",
115
115
  type: "module",
116
116
  main: "dist/cli.js",
@@ -136,7 +136,8 @@ var package_default = {
136
136
  build: "tsup",
137
137
  typecheck: "tsc --noEmit",
138
138
  clean: "rimraf dist",
139
- postinstall: "node scripts/postinstall.js"
139
+ postinstall: "node scripts/postinstall.js",
140
+ dev: "npx --silent tsx src/cli.ts"
140
141
  },
141
142
  homepage: "https://llmist.dev",
142
143
  repository: {
@@ -166,8 +167,9 @@ var package_default = {
166
167
  node: ">=22.0.0"
167
168
  },
168
169
  dependencies: {
169
- llmist: "^16.0.0",
170
+ llmist: "^16.0.2",
170
171
  "@unblessed/node": "^1.0.0-alpha.23",
172
+ "diff-match-patch": "^1.0.5",
171
173
  chalk: "^5.6.2",
172
174
  commander: "^12.1.0",
173
175
  diff: "^8.0.2",
@@ -180,8 +182,9 @@ var package_default = {
180
182
  zod: "^4.1.12"
181
183
  },
182
184
  devDependencies: {
183
- "@llmist/testing": "^16.0.0",
185
+ "@llmist/testing": "^16.0.2",
184
186
  "@types/diff": "^8.0.0",
187
+ "@types/diff-match-patch": "^1.0.36",
185
188
  "@types/js-yaml": "^4.0.9",
186
189
  "@types/marked-terminal": "^6.1.1",
187
190
  "@types/node": "^20.12.7",
@@ -1718,18 +1721,22 @@ import { createGadget as createGadget3 } from "llmist";
1718
1721
  import { z as z3 } from "zod";
1719
1722
 
1720
1723
  // src/builtins/filesystem/editfile/matcher.ts
1724
+ import DiffMatchPatch from "diff-match-patch";
1725
+ var dmp = new DiffMatchPatch();
1721
1726
  var DEFAULT_OPTIONS = {
1722
1727
  fuzzyThreshold: 0.8,
1723
1728
  maxSuggestions: 3,
1724
1729
  contextLines: 5
1725
1730
  };
1726
1731
  function findMatch(content, search, options = {}) {
1732
+ if (!search) return null;
1727
1733
  const opts = { ...DEFAULT_OPTIONS, ...options };
1728
1734
  const strategies = [
1729
1735
  { name: "exact", fn: exactMatch },
1730
1736
  { name: "whitespace", fn: whitespaceMatch },
1731
1737
  { name: "indentation", fn: indentationMatch },
1732
- { 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) }
1733
1740
  ];
1734
1741
  for (const { name, fn } of strategies) {
1735
1742
  const result = fn(content, search);
@@ -1795,7 +1802,8 @@ function indentationMatch(content, search) {
1795
1802
  const stripIndent = (s) => s.split("\n").map((line) => line.trimStart()).join("\n");
1796
1803
  const strippedSearch = stripIndent(search);
1797
1804
  const contentLines = content.split("\n");
1798
- const searchLineCount = search.split("\n").length;
1805
+ const searchLines = search.split("\n");
1806
+ const searchLineCount = searchLines.length;
1799
1807
  for (let i = 0; i <= contentLines.length - searchLineCount; i++) {
1800
1808
  const windowLines = contentLines.slice(i, i + searchLineCount);
1801
1809
  const strippedWindow = stripIndent(windowLines.join("\n"));
@@ -1804,6 +1812,7 @@ function indentationMatch(content, search) {
1804
1812
  const matchedContent = windowLines.join("\n");
1805
1813
  const endIndex = startIndex + matchedContent.length;
1806
1814
  const { startLine, endLine } = getLineNumbers(content, startIndex, endIndex);
1815
+ const indentationDelta = computeIndentationDelta(searchLines, windowLines);
1807
1816
  return {
1808
1817
  found: true,
1809
1818
  strategy: "indentation",
@@ -1812,7 +1821,8 @@ function indentationMatch(content, search) {
1812
1821
  startIndex,
1813
1822
  endIndex,
1814
1823
  startLine,
1815
- endLine
1824
+ endLine,
1825
+ indentationDelta
1816
1826
  };
1817
1827
  }
1818
1828
  }
@@ -1850,6 +1860,94 @@ function fuzzyMatch(content, search, threshold) {
1850
1860
  endLine
1851
1861
  };
1852
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
+ }
1853
1951
  function findSuggestions(content, search, maxSuggestions, minSimilarity) {
1854
1952
  const searchLines = search.split("\n");
1855
1953
  const contentLines = content.split("\n");
@@ -1966,6 +2064,78 @@ function getContext(content, lineNumber, contextLines) {
1966
2064
  });
1967
2065
  return contextWithNumbers.join("\n");
1968
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
+ }
1969
2139
 
1970
2140
  // src/builtins/filesystem/utils.ts
1971
2141
  import fs from "fs";
@@ -2037,22 +2207,30 @@ Uses layered matching strategies (in order):
2037
2207
  2. Whitespace-insensitive - ignores differences in spaces/tabs
2038
2208
  3. Indentation-preserving - matches structure ignoring leading whitespace
2039
2209
  4. Fuzzy match - similarity-based matching (80% threshold)
2210
+ 5. DMP (diff-match-patch) - handles heavily refactored code
2040
2211
 
2041
2212
  For multiple edits to the same file, call this gadget multiple times.
2042
- 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`,
2043
2218
  maxConcurrent: 1,
2044
2219
  // Sequential execution to prevent race conditions
2045
2220
  schema: z3.object({
2046
2221
  filePath: z3.string().describe("Path to the file to edit (relative or absolute)"),
2047
2222
  search: z3.string().describe("The content to search for in the file"),
2048
- 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")
2049
2226
  }),
2050
2227
  examples: [
2051
2228
  {
2052
2229
  params: {
2053
2230
  filePath: "src/config.ts",
2054
2231
  search: "const DEBUG = false;",
2055
- replace: "const DEBUG = true;"
2232
+ replace: "const DEBUG = true;",
2233
+ replaceAll: false
2056
2234
  },
2057
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```",
2058
2236
  comment: "Simple single-line edit"
@@ -2065,7 +2243,8 @@ Each call provides immediate feedback, allowing you to adjust subsequent edits.`
2065
2243
  }`,
2066
2244
  replace: `function newHelper() {
2067
2245
  return 2;
2068
- }`
2246
+ }`,
2247
+ replaceAll: false
2069
2248
  },
2070
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```",
2071
2250
  comment: "Multi-line replacement"
@@ -2074,14 +2253,25 @@ Each call provides immediate feedback, allowing you to adjust subsequent edits.`
2074
2253
  params: {
2075
2254
  filePath: "src/app.ts",
2076
2255
  search: "unusedImport",
2077
- replace: ""
2256
+ replace: "",
2257
+ replaceAll: false
2078
2258
  },
2079
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```',
2080
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"
2081
2271
  }
2082
2272
  ],
2083
2273
  timeoutMs: 3e4,
2084
- execute: ({ filePath, search, replace }) => {
2274
+ execute: ({ filePath, search, replace, replaceAll, expectedCount }) => {
2085
2275
  if (search.trim() === "") {
2086
2276
  return `path=${filePath} status=error
2087
2277
 
@@ -2111,12 +2301,43 @@ Error: File not found: ${filePath}`;
2111
2301
 
2112
2302
  Error reading file: ${message}`;
2113
2303
  }
2114
- const match = findMatch(content, search);
2115
- if (!match) {
2304
+ const allMatches = findAllMatches(content, search);
2305
+ if (allMatches.length === 0) {
2116
2306
  const failure = getMatchFailure(content, search);
2117
2307
  return formatFailure(filePath, search, failure, content);
2118
2308
  }
2119
- 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
+ }
2120
2341
  try {
2121
2342
  writeFileSync(validatedPath, newContent, "utf-8");
2122
2343
  } catch (error) {
@@ -2125,9 +2346,10 @@ Error reading file: ${message}`;
2125
2346
 
2126
2347
  Error writing file: ${message}`;
2127
2348
  }
2128
- 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}
2129
2351
 
2130
- Replaced content successfully.
2352
+ ${diffContext}
2131
2353
 
2132
2354
  UPDATED FILE CONTENT:
2133
2355
  \`\`\`
@@ -2135,6 +2357,21 @@ ${newContent}
2135
2357
  \`\`\``;
2136
2358
  }
2137
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
+ }
2138
2375
 
2139
2376
  // src/builtins/filesystem/list-directory.ts
2140
2377
  import fs2 from "fs";