@molecule/api-ai-tools 1.0.9 → 1.1.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.
@@ -64,6 +64,31 @@ export declare function redactSecretsInCode(s: string): string;
64
64
  * @returns `true` when the file is an env file.
65
65
  */
66
66
  export declare function isEnvFilePath(path: string): boolean;
67
+ /**
68
+ * The time budget a COMMAND declares for itself — a `timeout <seconds>` prefix
69
+ * anywhere in it, or the tool call's own `timeout` (ms) parameter, whichever is
70
+ * larger. Zero when it declares none.
71
+ *
72
+ * @param command - The shell command.
73
+ * @param timeoutParam - The tool call's `timeout` input, in milliseconds.
74
+ * @returns The declared budget in seconds, or 0.
75
+ */
76
+ export declare function declaredBudgetSeconds(command: string, timeoutParam: unknown): number;
77
+ /**
78
+ * Whether a command's output reaches the tool only when the whole pipeline
79
+ * ends — a pipe into `tail`, `grep`, `sort`, `wc` and friends, which
80
+ * block-buffer when stdout is not a terminal.
81
+ *
82
+ * It matters only in combination with an overrun: a command stopped at the
83
+ * tool's ceiling normally hands back everything it printed, but behind one of
84
+ * these it printed nothing, so the whole budget is spent for no information.
85
+ * Measured across six real agent runs: ten such commands, every one of them
86
+ * piped, 49 minutes returning a few hundred bytes each.
87
+ *
88
+ * @param command - The shell command.
89
+ * @returns True when a kill would return no useful output.
90
+ */
91
+ export declare function outputIsWithheldUntilExit(command: string): boolean;
67
92
  /**
68
93
  * Check if a command is blocked for security reasons. Returns error message or null if allowed.
69
94
  *
@@ -127,6 +152,60 @@ export declare const MAX_READ_SIZE: number;
127
152
  export declare const MAX_WRITE_SIZE: number;
128
153
  /** Max command output size (100KB per stream). */
129
154
  export declare const MAX_OUTPUT_SIZE: number;
155
+ /**
156
+ * Most lines of surrounding context `search_files` will return per match.
157
+ * Bounded so a broad pattern with a generous context cannot pull a whole tree
158
+ * into one reply.
159
+ */
160
+ export declare const MAX_SEARCH_CONTEXT_LINES = 40;
161
+ /**
162
+ * Return a line window of `content`, or null when no window was asked for.
163
+ *
164
+ * `read_file` returned whole files only, so an executor that wanted part of one
165
+ * shelled out: across six real agent runs, 260 of 377 shell-inspection commands
166
+ * (69%) were `head`/`tail`/`sed -n` windowing a file the tool could have
167
+ * windowed itself. `offset` is 1-based, matching the line numbers every other
168
+ * tool reports and what `grep -n` prints.
169
+ *
170
+ * @param content - The file's full text.
171
+ * @param window - The requested `offset` (1-based) and `limit` (line count).
172
+ * @returns The window plus its position, or null when neither was given.
173
+ */
174
+ export declare function sliceLines(content: string, window?: {
175
+ offset?: unknown;
176
+ limit?: unknown;
177
+ }): {
178
+ text: string;
179
+ offset: number;
180
+ lines: number;
181
+ totalLines: number;
182
+ truncated: boolean;
183
+ } | null;
184
+ /** Max search results. */
185
+ /**
186
+ * Most files one batched `read_file` call returns. The cap exists so a survey
187
+ * of a large tree cannot swallow the turn's context in one result; the tool
188
+ * reports where it stopped so the model continues from there.
189
+ */
190
+ export declare const MAX_BATCH_READ_FILES = 25;
191
+ /**
192
+ * Total content bytes one batched `read_file` call returns, across all files.
193
+ * The per-file {@link MAX_READ_SIZE} still applies to each one.
194
+ */
195
+ export declare const MAX_BATCH_READ_BYTES: number;
196
+ /**
197
+ * The most one read_file call returns for ONE file when no window was asked
198
+ * for, in chars. Larger files come back as their first window with a note
199
+ * saying how to read the rest.
200
+ *
201
+ * MAX_READ_SIZE (5 MB) is a bound on what the tool will open, not on what it
202
+ * should hand a model: a single batched read that returned a 1.2 MB README
203
+ * put the next three provider calls at 366k tokens (~330k of them fresh)
204
+ * against a 120k-token context cap (X0 run x5, call 26). ~80 KB is ~20–25k
205
+ * tokens — a fifth of that cap, and more than any one file read usually
206
+ * needs.
207
+ */
208
+ export declare const MAX_READ_RETURN_CHARS: number;
130
209
  /** Max search results. */
131
210
  export declare const MAX_SEARCH_RESULTS = 50;
132
211
  /** Max find results. */
@@ -1 +1 @@
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;AAyFhD;;;;;;;;;;;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;AA2BD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBlE;AAID;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAQrE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEpD;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,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,CAmBnE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAyBf"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
package/dist/utilities.js CHANGED
@@ -153,6 +153,42 @@ export function isEnvFilePath(path) {
153
153
  const base = path.split('/').pop() ?? '';
154
154
  return base === '.env' || base.startsWith('.env.') || base.endsWith('.env');
155
155
  }
156
+ /**
157
+ * The time budget a COMMAND declares for itself — a `timeout <seconds>` prefix
158
+ * anywhere in it, or the tool call's own `timeout` (ms) parameter, whichever is
159
+ * larger. Zero when it declares none.
160
+ *
161
+ * @param command - The shell command.
162
+ * @param timeoutParam - The tool call's `timeout` input, in milliseconds.
163
+ * @returns The declared budget in seconds, or 0.
164
+ */
165
+ export function declaredBudgetSeconds(command, timeoutParam) {
166
+ let declared = 0;
167
+ for (const m of command.matchAll(/(?:^|[;&|]\s*|\s)timeout\s+(?:-[a-zA-Z-]+\s+)*(\d+)(?![\d.])/g)) {
168
+ declared = Math.max(declared, Number(m[1]));
169
+ }
170
+ const asMs = Number(timeoutParam);
171
+ if (Number.isFinite(asMs) && asMs > 0)
172
+ declared = Math.max(declared, Math.round(asMs / 1000));
173
+ return declared;
174
+ }
175
+ /**
176
+ * Whether a command's output reaches the tool only when the whole pipeline
177
+ * ends — a pipe into `tail`, `grep`, `sort`, `wc` and friends, which
178
+ * block-buffer when stdout is not a terminal.
179
+ *
180
+ * It matters only in combination with an overrun: a command stopped at the
181
+ * tool's ceiling normally hands back everything it printed, but behind one of
182
+ * these it printed nothing, so the whole budget is spent for no information.
183
+ * Measured across six real agent runs: ten such commands, every one of them
184
+ * piped, 49 minutes returning a few hundred bytes each.
185
+ *
186
+ * @param command - The shell command.
187
+ * @returns True when a kill would return no useful output.
188
+ */
189
+ export function outputIsWithheldUntilExit(command) {
190
+ return /\|\s*(?:tail|head|grep|egrep|fgrep|sort|uniq|wc|jq)\b/.test(command);
191
+ }
156
192
  // ── Command blocking ──────────────────────────────────────────────────────────
157
193
  /**
158
194
  * Commands that dump environment variables — blocked to prevent secret leakage.
@@ -236,7 +272,13 @@ export function resolvePath(path, projectRoot) {
236
272
  * @returns `true` when the pattern contains only allowed characters.
237
273
  */
238
274
  export function isValidGlob(pattern) {
239
- return /^[A-Za-z0-9*?._/()[\]-]+$/.test(pattern);
275
+ // `@` is in the set because every package in this ecosystem is scoped:
276
+ // measured on a live build, the executor tried
277
+ // `node_modules/@molecule/app-ui/*` twice and was refused both times, so in
278
+ // a molecule project it could not glob its own dependencies. The character
279
+ // is inert here — patterns are shell-quoted before they reach find/grep, and
280
+ // `@` has no meaning to either.
281
+ return /^[A-Za-z0-9@*?._/()[\]-]+$/.test(pattern);
240
282
  }
241
283
  /**
242
284
  * Validate a file tool's `path` argument is a non-empty string. A weak model
@@ -276,6 +318,80 @@ export const MAX_READ_SIZE = 5 * 1024 * 1024;
276
318
  export const MAX_WRITE_SIZE = 10 * 1024 * 1024;
277
319
  /** Max command output size (100KB per stream). */
278
320
  export const MAX_OUTPUT_SIZE = 100 * 1024;
321
+ /**
322
+ * Most lines of surrounding context `search_files` will return per match.
323
+ * Bounded so a broad pattern with a generous context cannot pull a whole tree
324
+ * into one reply.
325
+ */
326
+ export const MAX_SEARCH_CONTEXT_LINES = 40;
327
+ /**
328
+ * Return a line window of `content`, or null when no window was asked for.
329
+ *
330
+ * `read_file` returned whole files only, so an executor that wanted part of one
331
+ * shelled out: across six real agent runs, 260 of 377 shell-inspection commands
332
+ * (69%) were `head`/`tail`/`sed -n` windowing a file the tool could have
333
+ * windowed itself. `offset` is 1-based, matching the line numbers every other
334
+ * tool reports and what `grep -n` prints.
335
+ *
336
+ * @param content - The file's full text.
337
+ * @param window - The requested `offset` (1-based) and `limit` (line count).
338
+ * @returns The window plus its position, or null when neither was given.
339
+ */
340
+ export function sliceLines(content, window) {
341
+ const rawOffset = Number(window?.offset);
342
+ const rawLimit = Number(window?.limit);
343
+ const hasOffset = Number.isFinite(rawOffset) && rawOffset !== 0;
344
+ const hasLimit = Number.isFinite(rawLimit) && rawLimit > 0;
345
+ if (!hasOffset && !hasLimit)
346
+ return null;
347
+ const all = content.split('\n');
348
+ const totalLines = all.length;
349
+ // A NEGATIVE offset counts from the end, the `tail -N` convention. Without it
350
+ // "show me the last 30 lines" has no expression here, and the executor either
351
+ // reads the whole file or goes back to the shell — measured: asked for a
352
+ // file's tail with these tools available, it read the entire file 3 times out
353
+ // of 3 until this existed.
354
+ const fromEnd = hasOffset && rawOffset < 0;
355
+ const offset = fromEnd
356
+ ? Math.max(1, totalLines + Math.trunc(rawOffset) + 1)
357
+ : hasOffset
358
+ ? Math.min(Math.trunc(rawOffset), totalLines)
359
+ : 1;
360
+ const limit = hasLimit ? Math.trunc(rawLimit) : totalLines - offset + 1;
361
+ const picked = all.slice(offset - 1, offset - 1 + limit);
362
+ return {
363
+ text: picked.join('\n'),
364
+ offset,
365
+ lines: picked.length,
366
+ totalLines,
367
+ truncated: offset > 1 || offset - 1 + picked.length < totalLines,
368
+ };
369
+ }
370
+ /** Max search results. */
371
+ /**
372
+ * Most files one batched `read_file` call returns. The cap exists so a survey
373
+ * of a large tree cannot swallow the turn's context in one result; the tool
374
+ * reports where it stopped so the model continues from there.
375
+ */
376
+ export const MAX_BATCH_READ_FILES = 25;
377
+ /**
378
+ * Total content bytes one batched `read_file` call returns, across all files.
379
+ * The per-file {@link MAX_READ_SIZE} still applies to each one.
380
+ */
381
+ export const MAX_BATCH_READ_BYTES = 200 * 1024;
382
+ /**
383
+ * The most one read_file call returns for ONE file when no window was asked
384
+ * for, in chars. Larger files come back as their first window with a note
385
+ * saying how to read the rest.
386
+ *
387
+ * MAX_READ_SIZE (5 MB) is a bound on what the tool will open, not on what it
388
+ * should hand a model: a single batched read that returned a 1.2 MB README
389
+ * put the next three provider calls at 366k tokens (~330k of them fresh)
390
+ * against a 120k-token context cap (X0 run x5, call 26). ~80 KB is ~20–25k
391
+ * tokens — a fifth of that cap, and more than any one file read usually
392
+ * needs.
393
+ */
394
+ export const MAX_READ_RETURN_CHARS = 80 * 1024;
279
395
  /** Max search results. */
280
396
  export const MAX_SEARCH_RESULTS = 50;
281
397
  /** Max find results. */
@@ -367,7 +483,19 @@ export function truncateMiddle(s, maxLength) {
367
483
  * @returns The new content if a unique fuzzy run matched, else null.
368
484
  */
369
485
  export function whitespaceTolerantReplace(content, oldString, newString) {
370
- const norm = (s) => s.replace(/\s+/g, ' ').trim();
486
+ // Per-line normalization: runs of whitespace become one space, and spaces
487
+ // next to punctuation are dropped entirely. The second rule is for the edit
488
+ // a weak executor makes from memory with one space missing —
489
+ // `readdirSync,readFileSync` against a file that reads
490
+ // `readdirSync, readFileSync` (X0 run x6: 15 of 48 edits failed, most of
491
+ // them this shape, after the file's exact text had left the context). A
492
+ // missing space is zero whitespace, which the runs rule cannot see. This
493
+ // only decides WHERE the block is; the replacement text is applied
494
+ // verbatim, and uniqueness is still required.
495
+ const norm = (s) => s
496
+ .replace(/\s*([,;:{}()[\]=<>.])\s*/g, '$1')
497
+ .replace(/\s+/g, ' ')
498
+ .trim();
371
499
  const fileLines = content.split('\n');
372
500
  const normOld = oldString.split('\n').map(norm);
373
501
  // Refuse a degenerate all-blank search block (would match any blank run).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.0.9",
3
+ "version": "1.1.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",