@molecule/api-ai-tools 1.5.13 → 1.5.15

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/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-16T02:43:35.968Z
6
+ Generated: 2026-10-16T05:16:44.196Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -887,6 +887,22 @@ function parseCheckProblem(result: {
887
887
 
888
888
  **Returns:** The message for the tool result, or null.
889
889
 
890
+ #### `fusedLine(content, newString)`
891
+
892
+ The line a replacement fused when it dropped the newline its `old_string`
893
+ ended with: `new_string`'s text is followed on the same line by what used to
894
+ be the next line. Read off the file as it now is, so the line shown is the
895
+ line the parser is actually complaining about.
896
+
897
+ ```typescript
898
+ function fusedLine(content: string, newString: string): { line: number; text: string } | null
899
+ ```
900
+
901
+ - `content` — The file's content after the edit.
902
+ - `newString` — A replacement's `new_string`, whose `old_string` ended with a newline.
903
+
904
+ **Returns:** The fused line's 1-based number and its text, or null when the replacement's text is not followed by more of the same line (it ends the file, the following newline survived, or another replacement rewrote it).
905
+
890
906
  ### Constants
891
907
 
892
908
  #### `TOOL_SCHEMAS`
@@ -967,6 +983,14 @@ needs.
967
983
  const MAX_READ_RETURN_CHARS: number
968
984
  ```
969
985
 
986
+ #### `SMALL_FILE_WHOLE_CHARS`
987
+
988
+ A file this small is returned whole even when a window is asked for (paging it costs a call each).
989
+
990
+ ```typescript
991
+ const SMALL_FILE_WHOLE_CHARS: number
992
+ ```
993
+
970
994
  #### `MAX_SEARCH_RESULTS`
971
995
 
972
996
  Max search results.
@@ -1053,6 +1077,15 @@ Peer dependencies:
1053
1077
  Line numbers count the way `read_file` does, so they are valid offsets. The
1054
1078
  excerpt is redacted like a read. The existing fields (`ok`,
1055
1079
  `replacementsApplied`, `alreadyApplied`, `syntaxError`) are unchanged.
1080
+ - **A dropped line break and the parse error it causes are reported as ONE
1081
+ thing.** When a replacement's `old_string` ended with a newline and its
1082
+ `new_string` does not (so the next line is pulled up onto it) and the file
1083
+ then fails its parse check, `edit_file` returns a single `syntaxError` that
1084
+ names the replacement, shows the fused line with its number, and says to
1085
+ re-send that replacement with the newline restored — instead of the
1086
+ `newlineNote` hint beside an unrelated-looking parse error, which an executor
1087
+ repairs by rewriting the fused line's text and fuses the next one. The
1088
+ `newlineNote` alone is still returned when the file parses.
1056
1089
  - **A parameter under another name is not a broken tool.** Every tool's input
1057
1090
  is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
1058
1091
  snake_case), and a handler that throws returns an error naming the missing
package/dist/index.d.ts CHANGED
@@ -38,6 +38,15 @@
38
38
  * Line numbers count the way `read_file` does, so they are valid offsets. The
39
39
  * excerpt is redacted like a read. The existing fields (`ok`,
40
40
  * `replacementsApplied`, `alreadyApplied`, `syntaxError`) are unchanged.
41
+ * - **A dropped line break and the parse error it causes are reported as ONE
42
+ * thing.** When a replacement's `old_string` ended with a newline and its
43
+ * `new_string` does not (so the next line is pulled up onto it) and the file
44
+ * then fails its parse check, `edit_file` returns a single `syntaxError` that
45
+ * names the replacement, shows the fused line with its number, and says to
46
+ * re-send that replacement with the newline restored — instead of the
47
+ * `newlineNote` hint beside an unrelated-looking parse error, which an executor
48
+ * repairs by rewriting the fused line's text and fuses the next one. The
49
+ * `newlineNote` alone is still returned when the file parses.
41
50
  * - **A parameter under another name is not a broken tool.** Every tool's input
42
51
  * is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
43
52
  * snake_case), and a handler that throws returns an error naming the missing
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyGG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkHG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -38,6 +38,15 @@
38
38
  * Line numbers count the way `read_file` does, so they are valid offsets. The
39
39
  * excerpt is redacted like a read. The existing fields (`ok`,
40
40
  * `replacementsApplied`, `alreadyApplied`, `syntaxError`) are unchanged.
41
+ * - **A dropped line break and the parse error it causes are reported as ONE
42
+ * thing.** When a replacement's `old_string` ended with a newline and its
43
+ * `new_string` does not (so the next line is pulled up onto it) and the file
44
+ * then fails its parse check, `edit_file` returns a single `syntaxError` that
45
+ * names the replacement, shows the fused line with its number, and says to
46
+ * re-send that replacement with the newline restored — instead of the
47
+ * `newlineNote` hint beside an unrelated-looking parse error, which an executor
48
+ * repairs by rewriting the fused line's text and fuses the next one. The
49
+ * `newlineNote` alone is still returned when the file parses.
41
50
  * - **A parameter under another name is not a broken tool.** Every tool's input
42
51
  * is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
43
52
  * snake_case), and a handler that throws returns an error naming the missing
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA8B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAwCnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAwiDxF"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA8B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA0CnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAooDxF"}
package/dist/tools.js CHANGED
@@ -25,7 +25,7 @@ const waitedByTask = new Map();
25
25
  const logBytesByTask = new Map();
26
26
  /** The file a background command writes its own output to, when it redirects it. */
27
27
  const outputFileByTask = new Map();
28
- import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, redactSecrets, redactSecretsInCode, resolvePath, shellQuote, sliceLines, streamingBackgroundCommand, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
28
+ import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, fusedLine, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, redactSecrets, redactSecretsInCode, resolvePath, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
29
29
  /**
30
30
  * How long exec_command waits for a command it backgrounded only because the
31
31
  * command asked for more than the tool's ceiling, before handing back a task id.
@@ -133,6 +133,8 @@ export function buildTools(backend, config) {
133
133
  }
134
134
  return pathGuards ? resolvePath(path, root) : path;
135
135
  }
136
+ /** Paths read with a window in this tool set, to tell paging from one targeted read. */
137
+ const windowedReads = new Set();
136
138
  /**
137
139
  * Verify that `path` does not symlink outside the project root when guards are enabled.
138
140
  *
@@ -391,6 +393,20 @@ export function buildTools(backend, config) {
391
393
  const effectiveWindow = !sliceLines(content, window) && content.length > MAX_READ_RETURN_CHARS
392
394
  ? { offset: 1, limit: linesWithin(content, MAX_READ_RETURN_CHARS) }
393
395
  : window;
396
+ // A SECOND window of the same small file: hand back all of it (one window is a targeted read). X1 flash-m50 paged files that fit in one
397
+ // call 13 times in a row, each window answered with "read it again with no offset/limit".
398
+ const small = sliceLines(content, window);
399
+ const pagedBefore = small?.truncated ? windowedReads.has(path) : false;
400
+ if (small?.truncated)
401
+ windowedReads.add(path);
402
+ if (small?.truncated && pagedBefore && content.length <= SMALL_FILE_WHOLE_CHARS) {
403
+ return {
404
+ path,
405
+ content: sanitizeFileContent(content, path),
406
+ totalLines: small.totalLines,
407
+ note: `The whole file is returned (${Math.round(content.length / 1024)} KB, ${small.totalLines} lines): a file this small fits in one call, so it is not paged.`,
408
+ };
409
+ }
394
410
  const windowed = sliceLines(content, effectiveWindow);
395
411
  if (windowed) {
396
412
  return {
@@ -826,7 +842,14 @@ export function buildTools(backend, config) {
826
842
  ? `${alreadyApplied} of these edits ${alreadyApplied === 1 ? 'was' : 'were'} already in the file (its new_string is there and its old_string is not) — nothing to do for ${alreadyApplied === 1 ? 'it' : 'them'}.`
827
843
  : null;
828
844
  if (content === oldContent) {
829
- return { path, ok: true, replacementsApplied: 0, alreadyApplied, note: alreadyNote };
845
+ // A replacement whose new_string IS its old_string changes nothing; `ok` alone read
846
+ // as a fix applied (X1 flash-m51).
847
+ const identical = replacements.filter((r) => typeof r.old_string === 'string' && r.old_string === r.new_string).length;
848
+ const note = alreadyNote ??
849
+ (identical > 0
850
+ ? `Nothing changed: ${identical === 1 ? 'the replacement’s new_string is' : `${identical} replacements’ new_string are`} identical to old_string, so the file is exactly as it was. Write the text you want in new_string.`
851
+ : 'Nothing changed: the file is exactly as it was.');
852
+ return { path, ok: true, replacementsApplied: 0, alreadyApplied, note };
830
853
  }
831
854
  if (onFileDiff)
832
855
  onFileDiff({ path, oldContent, newContent: content });
@@ -853,24 +876,61 @@ export function buildTools(backend, config) {
853
876
  // A replacement whose old text ended in a newline and whose new text does not
854
877
  // joins the line after it onto its last line. Applied as written, but said:
855
878
  // flash-m28 saw two lines merged only one round trip later.
856
- const joined = replacements.filter((r) => typeof r.old_string === 'string' &&
879
+ const dropped = replacements
880
+ .map((r, i) => ({ ...r, position: i + 1 }))
881
+ .filter((r) => typeof r.old_string === 'string' &&
857
882
  typeof r.new_string === 'string' &&
858
883
  r.old_string.endsWith('\n') &&
859
884
  r.new_string !== '' &&
860
- !r.new_string.endsWith('\n')).length;
885
+ !r.new_string.endsWith('\n'));
886
+ const syntax = await syntaxErrorAfterWrite(path);
887
+ // Those two facts are ONE accident, and reported apart the model repairs the
888
+ // wrong one: flash-m55 dropped the boundary on two consecutive edit_file calls,
889
+ // both left `handlers/conduit.ts` unparseable, and it fixed the fused LINE's
890
+ // text — which re-fused the next line in the same batch. When the parse error
891
+ // follows a dropped newline, name the fused line and the single edit that
892
+ // undoes it, instead of a style note beside an unrelated-looking parse error.
893
+ const which = dropped.length === 1
894
+ ? `replacement ${dropped[0].position}`
895
+ : `replacements ${dropped.map((r) => r.position).join(', ')}`;
896
+ const located = dropped.flatMap((r) => {
897
+ const line = fusedLine(finalContent, r.new_string);
898
+ return line ? [line] : [];
899
+ });
900
+ const fused = located.slice(0, 3);
901
+ const fusedOmitted = located.length - fused.length;
902
+ const shownFused = fused
903
+ .map((l) => ` ${l.line}: ${l.text.length > 200 ? `${l.text.slice(0, 200)}…` : l.text}`)
904
+ .join('\n');
905
+ const fusedNote = syntax.syntaxError && dropped.length > 0
906
+ ? 'The file no longer parses after this change, and ' +
907
+ `${which} removed the line break that followed the replaced text — so the next line ` +
908
+ 'now continues on the line before it, and that fused line is what the parser rejects:\n' +
909
+ (shownFused || ` (not located as the file now reads — read_file ${path})`) +
910
+ (fusedOmitted > 0
911
+ ? `\n (${fusedOmitted} more fused line(s) — read_file ${path})`
912
+ : '') +
913
+ `\nRe-send ${which} with the trailing newline restored on new_string (change nothing ` +
914
+ "else about it). Editing the FUSED LINE's text instead fuses the next line the same " +
915
+ `way (flash-m55: three edits in a row).\n${syntax.syntaxError}`
916
+ : null;
861
917
  return {
862
918
  path,
863
919
  ok: true,
864
920
  replacementsApplied: replacements.length - alreadyApplied,
865
- ...(joined > 0
866
- ? {
867
- newlineNote: `${joined === 1 ? 'A replacement' : `${joined} replacements`} removed the line break after the replaced text (old_string ended with a newline, new_string does not), so the next line now continues on the same line. Check the excerpt; add the newline back if that was not intended.`,
868
- }
869
- : {}),
921
+ ...(fusedNote
922
+ ? { syntaxError: fusedNote }
923
+ : {
924
+ ...(dropped.length > 0
925
+ ? {
926
+ newlineNote: `${dropped.length === 1 ? 'A replacement' : `${dropped.length} replacements`} removed the line break after the replaced text (old_string ended with a newline, new_string does not), so the next line now continues on the same line. Check the excerpt; add the newline back if that was not intended.`,
927
+ }
928
+ : {}),
929
+ ...syntax,
930
+ }),
870
931
  ...(alreadyApplied > 0 ? { alreadyApplied, note: alreadyNote } : {}),
871
932
  totalLines: countLines(finalContent),
872
933
  ...(excerpt ? { excerpt: sanitizeFileContent(excerpt, path) } : {}),
873
- ...(await syntaxErrorAfterWrite(path)),
874
934
  };
875
935
  }
876
936
  catch (e) {
@@ -1395,13 +1455,26 @@ export function buildTools(backend, config) {
1395
1455
  `kill $(cat /tmp/${taskId}.pid) 2>/dev/null; pkill -f ${taskId}.sh) and move on; do not wait on it again.`,
1396
1456
  };
1397
1457
  }
1458
+ // The output so far rides the result: a wait that shows nothing was waited blind
1459
+ // (X1 flash-m50: 4m42s across two waits on an 11-minute run).
1460
+ const own = outputFileByTask.get(taskId);
1461
+ let soFar;
1462
+ try {
1463
+ const r = await backend.run(`cat ${log}${own ? ` ${shellQuote(own)}` : ''} 2>/dev/null | tail -c 3000`, { timeout: 5_000 });
1464
+ soFar = r.stdout ?? '';
1465
+ }
1466
+ catch (_error) {
1467
+ // No tail: the note still says how to peek.
1468
+ soFar = '';
1469
+ }
1398
1470
  return {
1399
1471
  taskId,
1400
1472
  status: 'running',
1401
1473
  waitedMs,
1474
+ ...(soFar.trim() ? { outputSoFar: sanitizeOutput(soFar) } : {}),
1402
1475
  note: `Still running after ${Math.round(waitedMs / 1000)}s. Call wait_for_task again to ` +
1403
- `keep waiting — its result carries the log output. To peek at the output so far, ` +
1404
- `exec_command: tail ${log} (read_file cannot reach /tmp).`,
1476
+ `keep waiting — its result carries the log output${soFar.trim() ? '; outputSoFar is the end of the log as it stands, so there is no need to tail it' : ''}. ` +
1477
+ `Do other work in the meantime (another suite, a fix you have already found) rather than waiting.`,
1405
1478
  };
1406
1479
  }
1407
1480
  let output;
@@ -1413,6 +1486,20 @@ export function buildTools(backend, config) {
1413
1486
  // an empty output with a real exit code is still a result.
1414
1487
  output = '';
1415
1488
  }
1489
+ // A command that wrote its output to its own file (`> /tmp/e2e.log`) left the task log
1490
+ // holding a word: read that file too, so the result (and every note built on it) has the
1491
+ // run's output (X1 flash-m48: 60 passed / 14 failed, and the notes saw "started").
1492
+ const ownFile = outputFileByTask.get(taskId);
1493
+ if (ownFile && output.trim().length < 2000) {
1494
+ try {
1495
+ const ownText = await backend.readFile(ownFile);
1496
+ if (ownText.trim())
1497
+ output = `${output.trim() ? `${output.trimEnd()}\n` : ''}${ownText}`;
1498
+ }
1499
+ catch (_error) {
1500
+ // The file is gone or unreadable: the task log is the whole result, as before.
1501
+ }
1502
+ }
1416
1503
  // Same shaping as a foreground exec result, so the executor reads both
1417
1504
  // the same way.
1418
1505
  return {
@@ -249,6 +249,8 @@ export declare const MAX_BATCH_READ_BYTES: number;
249
249
  * needs.
250
250
  */
251
251
  export declare const MAX_READ_RETURN_CHARS: number;
252
+ /** A file this small is returned whole even when a window is asked for (paging it costs a call each). */
253
+ export declare const SMALL_FILE_WHOLE_CHARS: number;
252
254
  /** Max search results. */
253
255
  export declare const MAX_SEARCH_RESULTS = 50;
254
256
  /** Max find results. */
@@ -337,4 +339,20 @@ export declare function parseCheckProblem(result: {
337
339
  stderr: string;
338
340
  exitCode: number;
339
341
  }): string | null;
342
+ /**
343
+ * The line a replacement fused when it dropped the newline its `old_string`
344
+ * ended with: `new_string`'s text is followed on the same line by what used to
345
+ * be the next line. Read off the file as it now is, so the line shown is the
346
+ * line the parser is actually complaining about.
347
+ *
348
+ * @param content - The file's content after the edit.
349
+ * @param newString - A replacement's `new_string`, whose `old_string` ended with a newline.
350
+ * @returns The fused line's 1-based number and its text, or null when the
351
+ * replacement's text is not followed by more of the same line (it ends the
352
+ * file, the following newline survived, or another replacement rewrote it).
353
+ */
354
+ export declare function fusedLine(content: string, newString: string): {
355
+ line: number;
356
+ text: string;
357
+ } | null;
340
358
  //# sourceMappingURL=utilities.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
package/dist/utilities.js CHANGED
@@ -101,7 +101,13 @@ const NON_SECRET_VALUES = new Set([
101
101
  const jsonValueReplacer = (quote) => (match, prefix) => {
102
102
  const value = match.slice(prefix.length);
103
103
  const inner = value.slice(1, -1);
104
- if (NON_SECRET_VALUES.has(inner.toLowerCase()))
104
+ // An empty value hides nothing, and a suite fixture's `"password": ""` is the fact a
105
+ // failing assertion turns on (X1 flash-m52).
106
+ // A version (`"jsonwebtoken": "9.0.2"`, a package name that holds a keyword) is not a
107
+ // credential, and a masked one hid which version is installed (X1 flash-m57).
108
+ if (/^[\^~]?\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(inner))
109
+ return match;
110
+ if (inner === '' || NON_SECRET_VALUES.has(inner.toLowerCase()))
105
111
  return match;
106
112
  return `${prefix}${quote}[REDACTED]${quote}`;
107
113
  };
@@ -492,6 +498,8 @@ export const MAX_BATCH_READ_BYTES = 200 * 1024;
492
498
  * needs.
493
499
  */
494
500
  export const MAX_READ_RETURN_CHARS = 80 * 1024;
501
+ /** A file this small is returned whole even when a window is asked for (paging it costs a call each). */
502
+ export const SMALL_FILE_WHOLE_CHARS = 24 * 1024;
495
503
  /** Max search results. */
496
504
  export const MAX_SEARCH_RESULTS = 50;
497
505
  /** Max find results. */
@@ -716,3 +724,33 @@ export function parseCheckProblem(result) {
716
724
  return null;
717
725
  return text.slice(0, 1200);
718
726
  }
727
+ /**
728
+ * The line a replacement fused when it dropped the newline its `old_string`
729
+ * ended with: `new_string`'s text is followed on the same line by what used to
730
+ * be the next line. Read off the file as it now is, so the line shown is the
731
+ * line the parser is actually complaining about.
732
+ *
733
+ * @param content - The file's content after the edit.
734
+ * @param newString - A replacement's `new_string`, whose `old_string` ended with a newline.
735
+ * @returns The fused line's 1-based number and its text, or null when the
736
+ * replacement's text is not followed by more of the same line (it ends the
737
+ * file, the following newline survived, or another replacement rewrote it).
738
+ */
739
+ export function fusedLine(content, newString) {
740
+ for (let at = content.indexOf(newString); at >= 0; at = content.indexOf(newString, at + 1)) {
741
+ const endsAt = at + newString.length;
742
+ const next = content[endsAt];
743
+ // End of file (nothing was joined) or a newline (the boundary survived) are
744
+ // both "not fused". A later replacement in the same batch may have rewritten
745
+ // this text, so keep looking rather than giving up on the first occurrence.
746
+ if (next === undefined || next === '\n')
747
+ continue;
748
+ const lineStart = content.lastIndexOf('\n', at) + 1;
749
+ const lineEnd = content.indexOf('\n', endsAt);
750
+ return {
751
+ line: content.slice(0, lineStart).split('\n').length,
752
+ text: content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd),
753
+ };
754
+ }
755
+ return null;
756
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.5.13",
3
+ "version": "1.5.15",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",