@molecule/api-ai-tools 1.4.4 → 1.5.0

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;AAoB9C,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,CAu3CxF"}
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,7 @@ 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
+ 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
20
  /**
21
21
  * Build a complete set of AI agent tools bound to an execution backend.
22
22
  *
@@ -49,8 +49,30 @@ export function buildTools(backend, config) {
49
49
  const excludedDirs = (searchExcludedDirs && searchExcludedDirs.length > 0
50
50
  ? searchExcludedDirs
51
51
  : 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(' ');
52
+ /**
53
+ * The excluded dirs for a search rooted at `path`. Inside an installed
54
+ * package the build output IS the code: excluding `dist` there returned
55
+ * nothing for `IconName` in `node_modules/@molecule/app-icons` (it lives in
56
+ * `dist/types.d.ts`; X1 run sol-m04).
57
+ *
58
+ * @param path - The resolved search root.
59
+ * @returns The dir names to skip below it.
60
+ */
61
+ const excludesFor = (path) => /(?:^|\/)node_modules(?:\/|$)/.test(path)
62
+ ? excludedDirs.filter((d) => d !== 'dist')
63
+ : excludedDirs;
64
+ const grepExcludeArgs = (path) => excludesFor(path)
65
+ .map((d) => `--exclude-dir=${shellQuote(d)}`)
66
+ .join(' ');
67
+ // Pruned BELOW the root only (-mindepth 1): a `-not -path '*/node_modules/*'`
68
+ // filter matched every result of a search rooted inside node_modules and
69
+ // returned none.
70
+ const findPruneArgs = (path) => {
71
+ const dirs = excludesFor(path);
72
+ if (dirs.length === 0)
73
+ return '';
74
+ return `-mindepth 1 \\( -type d \\( ${dirs.map((d) => `-name ${shellQuote(d)}`).join(' -o ')} \\) -prune \\) -o`;
75
+ };
54
76
  // ── Path helpers ───────────────────────────────────────────────
55
77
  /**
56
78
  * After a write or edit, parse the file and hand the error back IN THE SAME
@@ -81,7 +103,14 @@ export function buildTools(backend, config) {
81
103
  * @param path - Relative or absolute path requested by the tool input.
82
104
  * @returns A normalized path honoring `pathGuards` and the backend root.
83
105
  */
84
- function resolve(path) {
106
+ function resolve(path, use = 'file') {
107
+ if (pathGuards && typeof path === 'string' && isOutsideRoot(path, root)) {
108
+ throw new ToolInputError(use === 'cwd'
109
+ ? `${path} is outside the workspace (${root}), so it cannot be the working directory — ` +
110
+ `start the command with \`cd ${path} && \` instead.`
111
+ : `${path} is outside the workspace (${root}). The file tools only reach files inside it — ` +
112
+ `read or list anything else with exec_command (e.g. \`cat ${path}\` or \`ls ${path}\`).`);
113
+ }
85
114
  return pathGuards ? resolvePath(path, root) : path;
86
115
  }
87
116
  /**
@@ -547,7 +576,13 @@ export function buildTools(backend, config) {
547
576
  if (!replacements?.length &&
548
577
  typeof input.old_string === 'string' &&
549
578
  typeof input.new_string === 'string') {
550
- replacements = [{ old_string: input.old_string, new_string: input.new_string }];
579
+ replacements = [
580
+ {
581
+ old_string: input.old_string,
582
+ new_string: input.new_string,
583
+ replace_all: input.replace_all,
584
+ },
585
+ ];
551
586
  }
552
587
  if (!replacements?.length)
553
588
  return {
@@ -572,7 +607,10 @@ export function buildTools(backend, config) {
572
607
  // Where each applied replacement landed, in the coordinates of the
573
608
  // latest content — rendered into the result so the model sees the edit.
574
609
  let spans = [];
575
- for (const { old_string: oldString, new_string: newString } of replacements) {
610
+ for (const { old_string: oldString, new_string: newString, replace_all: replaceAll,
611
+ // Models sometimes send the camelCase spelling inside an item, where
612
+ // the input normalizer (top-level keys only) does not reach.
613
+ replaceAll: replaceAllCamel, } of replacements) {
576
614
  // Validate each replacement is well-formed BEFORE touching content. A
577
615
  // weak model sometimes emits a batch element missing a field or with a
578
616
  // non-string value; left unchecked that either crashes (`undefined`
@@ -694,13 +732,22 @@ export function buildTools(backend, config) {
694
732
  `indentation — or include more surrounding context to make it unique.`,
695
733
  };
696
734
  }
735
+ if (count > 1 && (replaceAll === true || replaceAllCamel === true)) {
736
+ // Every occurrence, by split/join — never String.replace, whose
737
+ // replacement string treats ` if (count > 1) {
738
+ // Say WHERE,` and friends as patterns (see below).
739
+ const replaced = content.split(oldString).join(newString);
740
+ spans = addChangeSpan(spans, content, replaced);
741
+ content = replaced;
742
+ continue;
743
+ }
697
744
  if (count > 1) {
698
745
  // Say WHERE, so the retry can widen old_string around the one it means.
699
746
  const lines = [];
700
747
  for (let at = content.indexOf(oldString); at >= 0 && lines.length < 10; at = content.indexOf(oldString, at + 1))
701
748
  lines.push(content.slice(0, at).split('\n').length);
702
749
  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.`,
750
+ 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
751
  };
705
752
  }
706
753
  // Splice by index rather than `content.replace(oldString, newString)`:
@@ -780,7 +827,7 @@ export function buildTools(backend, config) {
780
827
  // `file:line:content` for matches, plus `--` group separators. Both
781
828
  // shapes are parsed below; without context the output is unchanged.
782
829
  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 });
830
+ 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
831
  // grep emits `<file>:<line>:<content>`, so redaction runs PER MATCH on the
785
832
  // content alone: the file prefix would otherwise hide an env assignment from
786
833
  // the line-anchored env pattern, and knowing each match's own path is what
@@ -839,7 +886,7 @@ export function buildTools(backend, config) {
839
886
  if (symlinkErr)
840
887
  return { error: symlinkErr };
841
888
  try {
842
- const result = await backend.run(`find ${shellQuote(path)} -name ${shellQuote(pattern)} ${findExcludeArgs} 2>/dev/null | head -${MAX_FIND_RESULTS}`, { timeout: 10000 });
889
+ const result = await backend.run(`find ${shellQuote(path)} ${findPruneArgs(path)} -name ${shellQuote(pattern)} -print 2>/dev/null | head -${MAX_FIND_RESULTS}`, { timeout: 10000 });
843
890
  const files = result.stdout.trim().split('\n').filter(Boolean);
844
891
  return { pattern, path, files };
845
892
  }
@@ -903,7 +950,7 @@ export function buildTools(backend, config) {
903
950
  },
904
951
  async exec_command(input) {
905
952
  const command = input.command;
906
- const cwd = input.cwd ? resolve(input.cwd) : root;
953
+ const cwd = input.cwd ? resolve(input.cwd, 'cwd') : root;
907
954
  // A model that sent the command under another name (`cmd`, `script`, …)
908
955
  // has already been normalized by guardToolExecute; reaching here with
909
956
  // nothing means there is genuinely no command. Say so in the terms that
@@ -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.0",
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",