@molecule/api-ai-tools 1.4.4 → 1.5.1

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-09-24T01:21:52.537Z
6
+ Generated: 2026-10-15T00:28:10.570Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -323,6 +323,14 @@ Re-export SkillEntry as DiscoveredSkill for backwards compatibility.
323
323
  type DiscoveredSkill = SkillEntry
324
324
  ```
325
325
 
326
+ ### Classes
327
+
328
+ #### `ToolInputError`
329
+
330
+ A tool call the handler refuses with a message meant for the model as is —
331
+ thrown from deep inside a handler (a path check), returned by the guard as the
332
+ call's `error` without the generic "check your parameters" wrapping.
333
+
326
334
  ### Functions
327
335
 
328
336
  #### `buildAgentPrompt(ctx)`
@@ -503,6 +511,24 @@ function isEnvFilePath(path: string): boolean
503
511
 
504
512
  **Returns:** `true` when the file is an env file.
505
513
 
514
+ #### `isOutsideRoot(path, projectRoot)`
515
+
516
+ Whether a tool path points outside the project root once normalized (an
517
+ absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
518
+ clamps such a path to the root; a file tool must refuse it instead of
519
+ answering about the root — a `list_files /tmp/x` used to return the
520
+ workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
521
+ 2026-10-14).
522
+
523
+ ```typescript
524
+ function isOutsideRoot(path: string, projectRoot: string): boolean
525
+ ```
526
+
527
+ - `path` — The path the tool was given.
528
+ - `projectRoot` — The project root.
529
+
530
+ **Returns:** True when the path leaves the root.
531
+
506
532
  #### `isValidGlob(pattern)`
507
533
 
508
534
  Validate that a glob/include pattern is safe (no shell metacharacters that could
@@ -4,6 +4,13 @@
4
4
  *
5
5
  * @module
6
6
  */
7
+ /**
8
+ * A tool call the handler refuses with a message meant for the model as is —
9
+ * thrown from deep inside a handler (a path check), returned by the guard as the
10
+ * call's `error` without the generic "check your parameters" wrapping.
11
+ */
12
+ export declare class ToolInputError extends Error {
13
+ }
7
14
  /** The JSON-Schema-ish shape every tool in this package declares. */
8
15
  export interface NormalizableSchema {
9
16
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"input-normalizer.d.ts","sourceRoot":"","sources":["../src/input-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QACpE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KACpB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAiDD,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,EAAE,OAAO,GAAG,eAAe,CAiD5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EAAE,GAChB,MAAM,CAaR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,EAC5E,OAAO,GAAE,YAAiB,GACzB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAsBxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,CAAC,SAAS;IACR,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAChE,EACD,IAAI,EAAE,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,CAAC,CAaxC"}
1
+ {"version":3,"file":"input-normalizer.d.ts","sourceRoot":"","sources":["../src/input-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAE5C,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QACpE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KACpB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAiDD,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,EAAE,OAAO,GAAG,eAAe,CAiD5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EAAE,GAChB,MAAM,CAaR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,EAC5E,OAAO,GAAE,YAAiB,GACzB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAuBxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,CAAC,SAAS;IACR,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAChE,EACD,IAAI,EAAE,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,CAAC,CAaxC"}
@@ -5,6 +5,13 @@
5
5
  * @module
6
6
  */
7
7
  import { redactSecrets } from './utilities.js';
8
+ /**
9
+ * A tool call the handler refuses with a message meant for the model as is —
10
+ * thrown from deep inside a handler (a path check), returned by the guard as the
11
+ * call's `error` without the generic "check your parameters" wrapping.
12
+ */
13
+ export class ToolInputError extends Error {
14
+ }
8
15
  /**
9
16
  * Other names a model sends for a declared parameter, most likely first.
10
17
  *
@@ -157,6 +164,8 @@ export function guardToolExecute(schema, impl, options = {}) {
157
164
  return await impl(input, ...rest);
158
165
  }
159
166
  catch (e) {
167
+ if (e instanceof ToolInputError)
168
+ return { error: e.message };
160
169
  if (missing.length > 0)
161
170
  return { error: missingParamError(schema, input, missing) };
162
171
  const message = redactError(e instanceof Error ? e.message : String(e));
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CA0T1C,CAAA"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CA+T1C,CAAA"}
package/dist/schemas.js CHANGED
@@ -79,7 +79,7 @@ export const TOOL_SCHEMAS = {
79
79
  },
80
80
  edit_file: {
81
81
  name: 'edit_file',
82
- description: 'Make targeted search-and-replace edits to a file. Preferred over write_file for small changes. Each old_string must match exactly once in the file. The result shows each edited region as it now reads, with line numbers, so there is no need to re-read the file to check an edit.',
82
+ description: 'Make targeted search-and-replace edits to a file. Preferred over write_file for small changes. Each old_string must match exactly once in the file, unless that replacement sets replace_all to change every occurrence (a rename, a repeated call). The result shows each edited region as it now reads, with line numbers, so there is no need to re-read the file to check an edit.',
83
83
  parameters: {
84
84
  type: 'object',
85
85
  properties: {
@@ -101,6 +101,10 @@ export const TOOL_SCHEMAS = {
101
101
  type: 'string',
102
102
  description: 'Replacement string.',
103
103
  },
104
+ replace_all: {
105
+ type: 'boolean',
106
+ description: 'Replace every occurrence of old_string instead of requiring exactly one (default false).',
107
+ },
104
108
  },
105
109
  required: ['old_string', 'new_string'],
106
110
  },
@@ -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;AAoB9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA+BnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CA2zCxF"}
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;AA2B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAgCnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAg6CxF"}
package/dist/tools.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * @module
8
8
  */
9
9
  import { addChangeSpan, changedSpan, countLines, renderEditExcerpt, renderSmallFile, } from './edit-excerpt.js';
10
- import { guardToolExecute, missingParamError } from './input-normalizer.js';
10
+ import { guardToolExecute, missingParamError, ToolInputError } from './input-normalizer.js';
11
11
  import { TOOL_SCHEMAS } from './schemas.js';
12
12
  /**
13
13
  * Total waiting, per background task, after which wait_for_task reports it
@@ -16,7 +16,14 @@ import { TOOL_SCHEMAS } from './schemas.js';
16
16
  const stuckAfterMs = () => Number(process.env.MOL_AI_BG_STUCK_AFTER_MS) || 5 * 60 * 1000;
17
17
  /** Cumulative wait_for_task time per task id, this process. */
18
18
  const waitedByTask = new Map();
19
- import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, isEnvFilePath, 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, parseCheckCommand, parseCheckProblem, pathArgError, redactSecrets, redactSecretsInCode, resolvePath, shellQuote, sliceLines, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
19
+ /**
20
+ * Each waited task's log size at the end of its last wait. A command whose log
21
+ * keeps growing is slow, not stuck: a serial 139-test suite was called stuck at
22
+ * 560 s and the executor was told to kill it while its log still grew (X1 run
23
+ * m21, 2026-10-15).
24
+ */
25
+ const logBytesByTask = new Map();
26
+ 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, parseCheckCommand, parseCheckProblem, pathArgError, redactSecrets, redactSecretsInCode, resolvePath, shellQuote, sliceLines, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
20
27
  /**
21
28
  * Build a complete set of AI agent tools bound to an execution backend.
22
29
  *
@@ -49,8 +56,30 @@ export function buildTools(backend, config) {
49
56
  const excludedDirs = (searchExcludedDirs && searchExcludedDirs.length > 0
50
57
  ? searchExcludedDirs
51
58
  : DEFAULT_SEARCH_EXCLUDED_DIRS).filter((d) => /^[A-Za-z0-9._-]+$/.test(d));
52
- const grepExcludeArgs = excludedDirs.map((d) => `--exclude-dir=${shellQuote(d)}`).join(' ');
53
- const findExcludeArgs = excludedDirs.map((d) => `-not -path ${shellQuote(`*/${d}/*`)}`).join(' ');
59
+ /**
60
+ * The excluded dirs for a search rooted at `path`. Inside an installed
61
+ * package the build output IS the code: excluding `dist` there returned
62
+ * nothing for `IconName` in `node_modules/@molecule/app-icons` (it lives in
63
+ * `dist/types.d.ts`; X1 run sol-m04).
64
+ *
65
+ * @param path - The resolved search root.
66
+ * @returns The dir names to skip below it.
67
+ */
68
+ const excludesFor = (path) => /(?:^|\/)node_modules(?:\/|$)/.test(path)
69
+ ? excludedDirs.filter((d) => d !== 'dist')
70
+ : excludedDirs;
71
+ const grepExcludeArgs = (path) => excludesFor(path)
72
+ .map((d) => `--exclude-dir=${shellQuote(d)}`)
73
+ .join(' ');
74
+ // Pruned BELOW the root only (-mindepth 1): a `-not -path '*/node_modules/*'`
75
+ // filter matched every result of a search rooted inside node_modules and
76
+ // returned none.
77
+ const findPruneArgs = (path) => {
78
+ const dirs = excludesFor(path);
79
+ if (dirs.length === 0)
80
+ return '';
81
+ return `-mindepth 1 \\( -type d \\( ${dirs.map((d) => `-name ${shellQuote(d)}`).join(' -o ')} \\) -prune \\) -o`;
82
+ };
54
83
  // ── Path helpers ───────────────────────────────────────────────
55
84
  /**
56
85
  * After a write or edit, parse the file and hand the error back IN THE SAME
@@ -81,7 +110,14 @@ export function buildTools(backend, config) {
81
110
  * @param path - Relative or absolute path requested by the tool input.
82
111
  * @returns A normalized path honoring `pathGuards` and the backend root.
83
112
  */
84
- function resolve(path) {
113
+ function resolve(path, use = 'file') {
114
+ if (pathGuards && typeof path === 'string' && isOutsideRoot(path, root)) {
115
+ throw new ToolInputError(use === 'cwd'
116
+ ? `${path} is outside the workspace (${root}), so it cannot be the working directory — ` +
117
+ `start the command with \`cd ${path} && \` instead.`
118
+ : `${path} is outside the workspace (${root}). The file tools only reach files inside it — ` +
119
+ `read or list anything else with exec_command (e.g. \`cat ${path}\` or \`ls ${path}\`).`);
120
+ }
85
121
  return pathGuards ? resolvePath(path, root) : path;
86
122
  }
87
123
  /**
@@ -547,7 +583,13 @@ export function buildTools(backend, config) {
547
583
  if (!replacements?.length &&
548
584
  typeof input.old_string === 'string' &&
549
585
  typeof input.new_string === 'string') {
550
- replacements = [{ old_string: input.old_string, new_string: input.new_string }];
586
+ replacements = [
587
+ {
588
+ old_string: input.old_string,
589
+ new_string: input.new_string,
590
+ replace_all: input.replace_all,
591
+ },
592
+ ];
551
593
  }
552
594
  if (!replacements?.length)
553
595
  return {
@@ -572,7 +614,10 @@ export function buildTools(backend, config) {
572
614
  // Where each applied replacement landed, in the coordinates of the
573
615
  // latest content — rendered into the result so the model sees the edit.
574
616
  let spans = [];
575
- for (const { old_string: oldString, new_string: newString } of replacements) {
617
+ for (const { old_string: oldString, new_string: newString, replace_all: replaceAll,
618
+ // Models sometimes send the camelCase spelling inside an item, where
619
+ // the input normalizer (top-level keys only) does not reach.
620
+ replaceAll: replaceAllCamel, } of replacements) {
576
621
  // Validate each replacement is well-formed BEFORE touching content. A
577
622
  // weak model sometimes emits a batch element missing a field or with a
578
623
  // non-string value; left unchecked that either crashes (`undefined`
@@ -694,13 +739,22 @@ export function buildTools(backend, config) {
694
739
  `indentation — or include more surrounding context to make it unique.`,
695
740
  };
696
741
  }
742
+ if (count > 1 && (replaceAll === true || replaceAllCamel === true)) {
743
+ // Every occurrence, by split/join — never String.replace, whose
744
+ // replacement string treats ` if (count > 1) {
745
+ // Say WHERE,` and friends as patterns (see below).
746
+ const replaced = content.split(oldString).join(newString);
747
+ spans = addChangeSpan(spans, content, replaced);
748
+ content = replaced;
749
+ continue;
750
+ }
697
751
  if (count > 1) {
698
752
  // Say WHERE, so the retry can widen old_string around the one it means.
699
753
  const lines = [];
700
754
  for (let at = content.indexOf(oldString); at >= 0 && lines.length < 10; at = content.indexOf(oldString, at + 1))
701
755
  lines.push(content.slice(0, at).split('\n').length);
702
756
  return {
703
- error: `old_string found ${count} times in ${path} (starting at lines ${lines.join(', ')}${count > lines.length ? ', …' : ''}) — must be unique. Include more surrounding context from the one you mean, or make one edit per occurrence.`,
757
+ error: `old_string found ${count} times in ${path} (starting at lines ${lines.join(', ')}${count > lines.length ? ', …' : ''}) — must be unique. Include more surrounding context from the one you mean, or set replace_all: true on this replacement to change every occurrence.`,
704
758
  };
705
759
  }
706
760
  // Splice by index rather than `content.replace(oldString, newString)`:
@@ -780,7 +834,7 @@ export function buildTools(backend, config) {
780
834
  // `file:line:content` for matches, plus `--` group separators. Both
781
835
  // shapes are parsed below; without context the output is unchanged.
782
836
  const contextArg = contextLines > 0 ? `-C ${contextLines}` : '';
783
- const result = await backend.run(`grep -rn ${globArg} ${contextArg} ${grepExcludeArgs} --max-count=${MAX_SEARCH_RESULTS} -- ${shellQuote(pattern)} ${shellQuote(path)} 2>/dev/null || true`, { timeout: 10000 });
837
+ const result = await backend.run(`grep -rn ${globArg} ${contextArg} ${grepExcludeArgs(path)} --max-count=${MAX_SEARCH_RESULTS} -- ${shellQuote(pattern)} ${shellQuote(path)} 2>/dev/null || true`, { timeout: 10000 });
784
838
  // grep emits `<file>:<line>:<content>`, so redaction runs PER MATCH on the
785
839
  // content alone: the file prefix would otherwise hide an env assignment from
786
840
  // the line-anchored env pattern, and knowing each match's own path is what
@@ -839,7 +893,7 @@ export function buildTools(backend, config) {
839
893
  if (symlinkErr)
840
894
  return { error: symlinkErr };
841
895
  try {
842
- const result = await backend.run(`find ${shellQuote(path)} -name ${shellQuote(pattern)} ${findExcludeArgs} 2>/dev/null | head -${MAX_FIND_RESULTS}`, { timeout: 10000 });
896
+ const result = await backend.run(`find ${shellQuote(path)} ${findPruneArgs(path)} -name ${shellQuote(pattern)} -print 2>/dev/null | head -${MAX_FIND_RESULTS}`, { timeout: 10000 });
843
897
  const files = result.stdout.trim().split('\n').filter(Boolean);
844
898
  return { pattern, path, files };
845
899
  }
@@ -903,7 +957,7 @@ export function buildTools(backend, config) {
903
957
  },
904
958
  async exec_command(input) {
905
959
  const command = input.command;
906
- const cwd = input.cwd ? resolve(input.cwd) : root;
960
+ const cwd = input.cwd ? resolve(input.cwd, 'cwd') : root;
907
961
  // A model that sent the command under another name (`cmd`, `script`, …)
908
962
  // has already been normalized by guardToolExecute; reaching here with
909
963
  // nothing means there is genuinely no command. Say so in the terms that
@@ -1105,7 +1159,33 @@ export function buildTools(backend, config) {
1105
1159
  // exiting is not going to: on one run the executor waited five times,
1106
1160
  // 120 s each, on an e2e suite that could never start (no browser page
1107
1161
  // attached). Say so once, name the pid, and refuse to wait on it again.
1108
- const waitedBefore = waitedByTask.get(taskId) ?? 0;
1162
+ let waitedBefore = waitedByTask.get(taskId) ?? 0;
1163
+ // Output since the last wait means the command is working: the stuck
1164
+ // clock starts again from here.
1165
+ const logBytes = async () => {
1166
+ if (!/^mol-bg-[a-z0-9]+-[a-z0-9]+$/.test(taskId))
1167
+ return null;
1168
+ let r;
1169
+ try {
1170
+ r = await backend.run(`wc -c < /tmp/${taskId}.log 2>/dev/null`, { timeout: 5_000 });
1171
+ }
1172
+ catch (_error) {
1173
+ // No answer about the log: treat the size as unknown, which keeps the
1174
+ // stuck verdict exactly as before.
1175
+ r = null;
1176
+ }
1177
+ const n = Number.parseInt(r?.stdout?.trim() ?? '', 10);
1178
+ return Number.isFinite(n) ? n : null;
1179
+ };
1180
+ if (taskId && waitedBefore >= stuckAfterMs()) {
1181
+ const now = await logBytes();
1182
+ const before = logBytesByTask.get(taskId);
1183
+ if (now !== null && before !== undefined && now > before) {
1184
+ logBytesByTask.set(taskId, now);
1185
+ waitedByTask.set(taskId, 0);
1186
+ waitedBefore = 0;
1187
+ }
1188
+ }
1109
1189
  if (taskId && waitedBefore >= stuckAfterMs()) {
1110
1190
  return {
1111
1191
  taskId,
@@ -1151,6 +1231,22 @@ export function buildTools(backend, config) {
1151
1231
  const waitedMs = Date.now() - startedAt;
1152
1232
  waitedByTask.set(taskId, waitedBefore + waitedMs);
1153
1233
  if (exitCode === null) {
1234
+ const grownTo = await logBytes();
1235
+ const lastSize = logBytesByTask.get(taskId);
1236
+ if (grownTo !== null)
1237
+ logBytesByTask.set(taskId, grownTo);
1238
+ const stillWriting = grownTo !== null && (lastSize === undefined ? grownTo > 0 : grownTo > lastSize);
1239
+ if (stillWriting && waitedBefore + waitedMs >= stuckAfterMs()) {
1240
+ waitedByTask.set(taskId, 0);
1241
+ return {
1242
+ taskId,
1243
+ status: 'running',
1244
+ waitedMs,
1245
+ note: `Still running after ${Math.round((waitedBefore + waitedMs) / 1000)}s of waiting, and its ` +
1246
+ `output is still growing (${grownTo} bytes) — slow, not stuck. Call wait_for_task again to ` +
1247
+ `keep waiting; peek with exec_command: tail ${log}.`,
1248
+ };
1249
+ }
1154
1250
  if (waitedBefore + waitedMs >= stuckAfterMs()) {
1155
1251
  return {
1156
1252
  taskId,
@@ -106,6 +106,19 @@ export declare function checkBlockedCommand(command: string): string | null;
106
106
  * @returns A normalized absolute path confined to `projectRoot`.
107
107
  */
108
108
  export declare function resolvePath(path: string, projectRoot: string): string;
109
+ /**
110
+ * Whether a tool path points outside the project root once normalized (an
111
+ * absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
112
+ * clamps such a path to the root; a file tool must refuse it instead of
113
+ * answering about the root — a `list_files /tmp/x` used to return the
114
+ * workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
115
+ * 2026-10-14).
116
+ *
117
+ * @param path - The path the tool was given.
118
+ * @param projectRoot - The project root.
119
+ * @returns True when the path leaves the root.
120
+ */
121
+ export declare function isOutsideRoot(path: string, projectRoot: string): boolean;
109
122
  /**
110
123
  * Validate that a glob/include pattern is safe (no shell metacharacters that could
111
124
  * inject). Allows alphanumeric, `* ? . _ - /` and the bracket/paren glob chars `[] ()`.
@@ -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
@@ -256,6 +256,27 @@ export function resolvePath(path, projectRoot) {
256
256
  return projectRoot;
257
257
  return resolved;
258
258
  }
259
+ /**
260
+ * Whether a tool path points outside the project root once normalized (an
261
+ * absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
262
+ * clamps such a path to the root; a file tool must refuse it instead of
263
+ * answering about the root — a `list_files /tmp/x` used to return the
264
+ * workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
265
+ * 2026-10-14).
266
+ *
267
+ * @param path - The path the tool was given.
268
+ * @param projectRoot - The project root.
269
+ * @returns True when the path leaves the root.
270
+ */
271
+ export function isOutsideRoot(path, projectRoot) {
272
+ if (path === '' || path === '/')
273
+ return false;
274
+ const clean = path.replace(/\0/g, '');
275
+ const resolved = clean.startsWith('/')
276
+ ? posix.normalize(clean)
277
+ : posix.normalize(`${projectRoot}/${clean}`);
278
+ return resolved !== projectRoot && !resolved.startsWith(projectRoot + '/');
279
+ }
259
280
  /**
260
281
  * Validate that a glob/include pattern is safe (no shell metacharacters that could
261
282
  * inject). Allows alphanumeric, `* ? . _ - /` and the bracket/paren glob chars `[] ()`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.4.4",
3
+ "version": "1.5.1",
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",