@molecule/api-ai-tools 1.5.10 → 1.5.12

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-16T00:26:34.364Z
6
+ Generated: 2026-10-16T02:43:35.968Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -814,6 +814,19 @@ function truncateMiddle(s: string, maxLength: number): string
814
814
 
815
815
  **Returns:** The original string, or head + an elision notice + tail.
816
816
 
817
+ #### `failureBlocks(s)`
818
+
819
+ The numbered failure blocks of a Playwright report (` 1) [chromium] › file:line › title`
820
+ up to the next block or the tally).
821
+
822
+ ```typescript
823
+ function failureBlocks(s: string): string[]
824
+ ```
825
+
826
+ - `s` — Test-runner output.
827
+
828
+ **Returns:** Each block, in order (empty when there are none).
829
+
817
830
  #### `whitespaceTolerantReplace(content, oldString, newString)`
818
831
 
819
832
  Attempt a whitespace-tolerant replacement when an exact `old_string` match
@@ -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,CAyhDxF"}
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"}
package/dist/tools.js CHANGED
@@ -142,22 +142,37 @@ export function buildTools(backend, config) {
142
142
  async function checkSymlink(path) {
143
143
  if (!symlinkGuards)
144
144
  return null;
145
- try {
146
- const result = await backend.run(`readlink -f ${shellQuote(path)} 2>/dev/null || echo ${shellQuote(path)}`, { timeout: 5000 });
147
- const realPath = result.stdout.trim();
148
- if (realPath && realPath !== root && !realPath.startsWith(root + '/')) {
149
- return 'Access denied: path resolves outside the project workspace';
145
+ // One retry: the first commands on a just-attached sandbox can fail at once (X1
146
+ // flash-m44: three paths refused in 332 ms, the same check passing a beat later).
147
+ let lastError;
148
+ for (let attempt = 0; attempt < 2; attempt++) {
149
+ const started = Date.now();
150
+ try {
151
+ const result = await backend.run(`readlink -f ${shellQuote(path)} 2>/dev/null || echo ${shellQuote(path)}`, { timeout: 5000 });
152
+ const realPath = result.stdout.trim();
153
+ if (realPath && realPath !== root && !realPath.startsWith(root + '/')) {
154
+ return 'Access denied: path resolves outside the project workspace';
155
+ }
156
+ return null;
157
+ }
158
+ catch (error) {
159
+ lastError = Object.assign(error instanceof Error ? error : new Error(String(error)), {
160
+ elapsedMs: Date.now() - started,
161
+ });
162
+ if (attempt === 0)
163
+ await new Promise((resolve) => setTimeout(resolve, 300));
150
164
  }
151
- return null;
152
165
  }
153
- catch (_error) {
154
- // readlink unavailable or timed out — deny access conservatively, and say
155
- // why: "access denied" read as a forbidden path, and builders spent call
156
- // after call on paths that were fine while the sandbox was not answering.
157
- return ('Unable to verify path safety: the sandbox did not answer the check within 5 s (it may be ' +
166
+ // Deny conservatively, and say what failed: "access denied" read as a forbidden path,
167
+ // and a fixed "did not answer within 5 s" hid an error that took 332 ms.
168
+ const err = lastError;
169
+ const timedOut = err?.name === 'TimeoutError' || (err?.elapsedMs ?? 0) >= 4900;
170
+ return timedOut
171
+ ? 'Unable to verify path safety: the sandbox did not answer the check within 5 s (it may be ' +
158
172
  'restarting or out of memory), so the path was refused to stay safe. Retry in a moment; if ' +
159
- 'every file tool fails this way, the sandbox needs a restart.');
160
- }
173
+ 'every file tool fails this way, the sandbox needs a restart.'
174
+ : `Unable to verify path safety: the sandbox's command channel failed (${err?.name ?? 'Error'}: ${String(err?.message ?? err).slice(0, 160)}), ` +
175
+ 'so the path was refused to stay safe. Retry the call; if every file tool fails this way, the sandbox needs a restart.';
161
176
  }
162
177
  /**
163
178
  * Strip control characters and optionally redact secrets from tool output.
@@ -434,12 +449,15 @@ export function buildTools(backend, config) {
434
449
  // the next read uses a real name instead of another guess.
435
450
  // A package's docs are one tool call away wherever node_modules happens to live.
436
451
  const moleculePkg = /node_modules\/(@molecule\/[^/]+)\//.exec(path)?.[1];
437
- if (moleculePkg && /No such file or directory/i.test(message)) {
452
+ // Docker's `cat` says "No such file or directory"; E2B's SDK says "path '…' does not
453
+ // exist" (X1 flash-m44: the hints below never fired on E2B).
454
+ const notFound = /No such file or directory|does not exist|ENOENT/i.test(message);
455
+ if (moleculePkg && notFound) {
438
456
  return {
439
457
  error: `No such file: ${path}. Read ${moleculePkg}'s documentation with read_molecule_doc("${moleculePkg}") instead of looking for it in node_modules.`,
440
458
  };
441
459
  }
442
- if (/No such file or directory/i.test(message)) {
460
+ if (notFound) {
443
461
  const parent = path.replace(/\/[^/]*$/, '') || '/';
444
462
  let listing;
445
463
  try {
@@ -476,7 +494,7 @@ export function buildTools(backend, config) {
476
494
  const message = e.message;
477
495
  // Same ground-truth answer as read_file's miss: a guessed path gets the parent's
478
496
  // real contents back, so the next call names something that exists.
479
- if (/No such file or directory|cannot access/i.test(message)) {
497
+ if (/No such file or directory|cannot access|does not exist|ENOENT/i.test(message)) {
480
498
  const parent = path.replace(/\/[^/]*$/, '') || '/';
481
499
  try {
482
500
  const siblings = await backend.readDir(parent);
@@ -286,6 +286,14 @@ export declare function truncate(s: string, maxLength: number): string;
286
286
  * @returns The original string, or head + an elision notice + tail.
287
287
  */
288
288
  export declare function truncateMiddle(s: string, maxLength: number): string;
289
+ /**
290
+ * The numbered failure blocks of a Playwright report (` 1) [chromium] › file:line › title`
291
+ * up to the next block or the tally).
292
+ *
293
+ * @param s - Test-runner output.
294
+ * @returns Each block, in order (empty when there are none).
295
+ */
296
+ export declare function failureBlocks(s: string): string[];
289
297
  /**
290
298
  * Attempt a whitespace-tolerant replacement when an exact `old_string` match
291
299
  * failed. Finds a contiguous run of lines in `content` whose per-line
@@ -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":"AAAA;;;;;GAKG;AAIH;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAS5C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,MAAM,KAAG,MAEE,CAAA;AA6FhD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,MAAM,CAUpF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAElE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWnE;AAKD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAC3D,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB,CAkCA;AA2BD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuBlE;AAID;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAQrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAOxE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E;AAID,yCAAyC;AACzC,eAAO,MAAM,aAAa,QAAkB,CAAA;AAC5C,8CAA8C;AAC9C,eAAO,MAAM,cAAc,QAAmB,CAAA;AAC9C,kDAAkD;AAClD,eAAO,MAAM,eAAe,QAAa,CAAA;AACzC;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAE1C;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA6BhG;AAED,0BAA0B;AAC1B;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC;;;GAGG;AACH,eAAO,MAAM,oBAAoB,QAAa,CAAA;AAE9C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,QAAY,CAAA;AAE9C,0BAA0B;AAC1B,eAAO,MAAM,kBAAkB,KAAK,CAAA;AACpC,wBAAwB;AACxB,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAEnC;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,mHAW/B,CAAA;AAEV;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CA2CnE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBjD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAsCf;AAED,qEAAqE;AACrE,eAAO,MAAM,eAAe,QAA0C,CAAA;AAEtE;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB7D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,MAAM,GAAG,IAAI,CAKhB"}
package/dist/utilities.js CHANGED
@@ -204,11 +204,14 @@ export function outputIsWithheldUntilExit(command) {
204
204
  * @returns The target as written, or null (`/dev/null` and `&1`/`&2` are not files).
205
205
  */
206
206
  export function outputRedirectTarget(command) {
207
- const tee = /\|\s*tee\s+(?:-a\s+)?([^\s;&|]+)/.exec(command);
207
+ // A heredoc that writes a file (`cat > cfg.ts <<'EOF' … EOF`) is not where the run's output
208
+ // goes: X1 flash-m44 was told its config file was the suite's log.
209
+ const text = command.replace(/[^\n;&|]*<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\s*\2\b/g, '');
210
+ const tee = /\|\s*tee\s+(?:-a\s+)?([^\s;&|]+)/.exec(text);
208
211
  if (tee)
209
212
  return tee[1];
210
213
  let last = null;
211
- for (const m of command.matchAll(/(?:^|[^0-9&>])(?:&>|1?>>?)\s*([^\s;&|]+)/g)) {
214
+ for (const m of text.matchAll(/(?:^|[^0-9&>])(?:&>|1?>>?)\s*([^\s;&|]+)/g)) {
212
215
  if (m[1] !== '/dev/null' && !m[1].startsWith('&'))
213
216
  last = m[1];
214
217
  }
@@ -552,6 +555,29 @@ export function truncateMiddle(s, maxLength) {
552
555
  if (maxLength <= noticeReserve)
553
556
  return s.slice(0, maxLength);
554
557
  const budget = maxLength - noticeReserve;
558
+ // A test run's numbered failure blocks are what the reader needs, and in a long run they
559
+ // sit in the middle, between per-test progress and the tally: keep them over progress
560
+ // (X1 flash-m45: 31 failure bodies dropped, 8 KB of passing progress kept).
561
+ const failures = failureBlocks(s);
562
+ if (failures.length > 0) {
563
+ const headLen = Math.min(800, Math.floor(budget * 0.1));
564
+ const tailLen = Math.min(1500, Math.floor(budget * 0.2));
565
+ let room = budget - headLen - tailLen;
566
+ const kept = [];
567
+ for (const block of failures) {
568
+ const piece = block.length > 900 ? `${block.slice(0, 897)}...` : block;
569
+ if (piece.length + 1 > room)
570
+ break;
571
+ kept.push(piece);
572
+ room -= piece.length + 1;
573
+ }
574
+ const left = failures.length - kept.length;
575
+ return (s.slice(0, headLen) +
576
+ `\n\n... [middle truncated — progress lines left out; ${kept.length} of ${failures.length} failure blocks kept${left ? `, ${left} more not shown` : ''}] ...\n\n` +
577
+ kept.join('\n') +
578
+ '\n\n' +
579
+ s.slice(s.length - tailLen));
580
+ }
555
581
  const headLen = Math.floor(budget * 0.4);
556
582
  const tailLen = budget - headLen;
557
583
  const omitted = s.length - headLen - tailLen;
@@ -559,6 +585,36 @@ export function truncateMiddle(s, maxLength) {
559
585
  `\n\n... [middle truncated — ${omitted} chars omitted; the head AND the tail are shown, a failing command's error is usually near the end] ...\n\n` +
560
586
  s.slice(s.length - tailLen));
561
587
  }
588
+ /**
589
+ * The numbered failure blocks of a Playwright report (` 1) [chromium] › file:line › title`
590
+ * up to the next block or the tally).
591
+ *
592
+ * @param s - Test-runner output.
593
+ * @returns Each block, in order (empty when there are none).
594
+ */
595
+ export function failureBlocks(s) {
596
+ const lines = s.split('\n');
597
+ const blocks = [];
598
+ let current = null;
599
+ for (const line of lines) {
600
+ if (/^\s*\d+\) \[[^\]\n]+\] › /.test(line)) {
601
+ if (current)
602
+ blocks.push(current.join('\n').trimEnd());
603
+ current = [line];
604
+ }
605
+ else if (current) {
606
+ if (/^\s*\d+ (?:passed|failed|flaky|skipped|did not run)\b/.test(line)) {
607
+ blocks.push(current.join('\n').trimEnd());
608
+ current = null;
609
+ }
610
+ else
611
+ current.push(line);
612
+ }
613
+ }
614
+ if (current)
615
+ blocks.push(current.join('\n').trimEnd());
616
+ return blocks;
617
+ }
562
618
  /**
563
619
  * Attempt a whitespace-tolerant replacement when an exact `old_string` match
564
620
  * failed. Finds a contiguous run of lines in `content` whose per-line
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.5.10",
3
+ "version": "1.5.12",
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",