@molecule/api-ai-tools 1.5.7 → 1.5.9

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.
@@ -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;AA2B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAuCnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAs+CxF"}
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,CA8/CxF"}
package/dist/tools.js CHANGED
@@ -23,7 +23,9 @@ const waitedByTask = new Map();
23
23
  * m21, 2026-10-15).
24
24
  */
25
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, streamingBackgroundCommand, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
26
+ /** The file a background command writes its own output to, when it redirects it. */
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';
27
29
  /**
28
30
  * How long exec_command waits for a command it backgrounded only because the
29
31
  * command asked for more than the tool's ceiling, before handing back a task id.
@@ -1032,9 +1034,16 @@ export function buildTools(backend, config) {
1032
1034
  // executor gets a handle to wait on.
1033
1035
  const ceilingSeconds = commandBudgetMs ? Math.max(1, Math.round(commandBudgetMs / 1000)) : 0;
1034
1036
  const declaredSeconds = commandBudgetMs ? declaredBudgetSeconds(command, input.timeout) : 0;
1037
+ // A Playwright run with its output held back by a pipe is backgrounded without
1038
+ // declaring a budget too: a suite routinely outlasts the ceiling, and killed there
1039
+ // behind `| tail` it returns nothing (X1 flash-m29 lost a 290 s run that way).
1040
+ // A short one still comes back as a foreground result, through the grace below.
1035
1041
  const overCeiling = commandBudgetMs !== undefined &&
1036
- declaredSeconds > ceilingSeconds &&
1037
- outputIsWithheldUntilExit(command);
1042
+ ((outputIsWithheldUntilExit(command) &&
1043
+ (declaredSeconds > ceilingSeconds || /\bplaywright\s+test\b/.test(command))) ||
1044
+ // A redirected Playwright run shows nothing either (`> out.txt`), and was killed at
1045
+ // the ceiling with 38 tests unrun (X1 flash-m36).
1046
+ (/\bplaywright\s+test\b/.test(command) && outputRedirectTarget(command) !== null));
1038
1047
  if (input.run_in_background === true || overCeiling) {
1039
1048
  const id = `mol-bg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1040
1049
  const log = `/tmp/${id}.log`;
@@ -1048,6 +1057,14 @@ export function buildTools(backend, config) {
1048
1057
  // Keep the log growing while it runs (see streamingBackgroundCommand):
1049
1058
  // an empty log is what wait_for_task reads as stuck.
1050
1059
  const streaming = streamingBackgroundCommand(command);
1060
+ const ownTarget = outputRedirectTarget(streaming.command);
1061
+ const ownFile = ownTarget
1062
+ ? ownTarget.startsWith('/')
1063
+ ? ownTarget
1064
+ : `${cwd}/${ownTarget}`
1065
+ : null;
1066
+ if (ownFile)
1067
+ outputFileByTask.set(id, ownFile);
1051
1068
  await backend.writeFile(script, `${streaming.command}\n`);
1052
1069
  await backend.run(`nohup sh -c ${shellQuote(`sh ${script} > ${log} 2>&1; echo $? > ${exitFile}`)} ` +
1053
1070
  `> /dev/null 2>&1 & echo $! > /tmp/${id}.pid`, { cwd, timeout: 15_000 });
@@ -1094,15 +1111,21 @@ export function buildTools(backend, config) {
1094
1111
  log,
1095
1112
  exitFile,
1096
1113
  note: (overCeiling
1097
- ? `This command asks for ${declaredSeconds}s, more than this tool's ${ceilingSeconds}s, and its ` +
1098
- 'output only appears when it ends — so it was started in the background instead. '
1114
+ ? declaredSeconds > ceilingSeconds
1115
+ ? `This command asks for ${declaredSeconds}s, more than this tool's ${ceilingSeconds}s, and its ` +
1116
+ 'output only appears when it ends — so it was started in the background instead. '
1117
+ : `A Playwright run can take longer than this tool's ${ceilingSeconds}s, and behind a pipe its ` +
1118
+ 'output only appears when it ends — so it was started in the background instead. '
1099
1119
  : '') +
1100
1120
  `Started in the background. Its output is written to ${log} and comes back as the ` +
1101
1121
  `stdout of wait_for_task when the command finishes (read_file cannot reach /tmp — ` +
1102
1122
  `to peek mid-run, exec_command: tail ${log}). If you have other work, do it and ` +
1103
1123
  `check back. If you need this result before you can continue, call wait_for_task ` +
1104
1124
  `with this taskId — never sleep and read the log by hand.` +
1105
- (streaming.changes.length ? ` ${streaming.changes.join(' ')}` : ''),
1125
+ (streaming.changes.length ? ` ${streaming.changes.join(' ')}` : '') +
1126
+ (ownFile
1127
+ ? ` This command writes its own output to ${ownFile}, so the task log stays empty: peek with exec_command: tail ${ownFile}.`
1128
+ : ''),
1106
1129
  };
1107
1130
  }
1108
1131
  catch (e) {
@@ -1239,7 +1262,8 @@ export function buildTools(backend, config) {
1239
1262
  return null;
1240
1263
  let r;
1241
1264
  try {
1242
- r = await backend.run(`wc -c < /tmp/${taskId}.log 2>/dev/null`, { timeout: 5_000 });
1265
+ const own = outputFileByTask.get(taskId);
1266
+ r = await backend.run(`cat /tmp/${taskId}.log${own ? ` ${shellQuote(own)}` : ''} 2>/dev/null | wc -c`, { timeout: 5_000 });
1243
1267
  }
1244
1268
  catch (_error) {
1245
1269
  // No answer about the log: treat the size as unknown, which keeps the
@@ -89,6 +89,16 @@ export declare function declaredBudgetSeconds(command: string, timeoutParam: unk
89
89
  * @returns True when a kill would return no useful output.
90
90
  */
91
91
  export declare function outputIsWithheldUntilExit(command: string): boolean;
92
+ /**
93
+ * The file a command sends its own output to, when it redirects it (`> out.txt`,
94
+ * `>> log`, `&> run.log`, `| tee [-a] file`), or null. A background command that does
95
+ * this leaves its task log empty, so the log is the wrong place to look and an empty
96
+ * log proves nothing (X1 flash-m29).
97
+ *
98
+ * @param command - The shell command.
99
+ * @returns The target as written, or null (`/dev/null` and `&1`/`&2` are not files).
100
+ */
101
+ export declare function outputRedirectTarget(command: string): string | null;
92
102
  /**
93
103
  * Rewrite a command about to run in the background so its log grows while it
94
104
  * runs. Two shapes left the log empty until exit, and an empty log is what
@@ -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
@@ -67,8 +67,13 @@ const SECRET_ENV_ASSIGNMENT = new RegExp(
67
67
  // un-masked exactly the plainest env vars.
68
68
  `^((?:export[ \\t]+)?[A-Za-z0-9_]*(?:${SECRET_KEYWORDS})[A-Za-z0-9_]*)=(?![{<])(.+)$`, 'gim');
69
69
  /** Catch JSON-formatted env dumps like { KEY: 'value' } from node/python. */
70
- const SECRET_JSON_DQ = new RegExp(`(['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)"(?:[^"\\\\]|\\\\.)*"`, 'gi');
71
- const SECRET_JSON_SQ = new RegExp(`(['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)'(?:[^'\\\\]|\\\\.)*'`, 'gi');
70
+ const SECRET_JSON_DQ = new RegExp(
71
+ // A value ends at its closing quote OR the end of its line: cut off mid-value (`head -c`),
72
+ // the old pattern ran on to a quote on a LATER line and swallowed the lines between (flash-m32).
73
+ // The key starts at a real boundary: a package name ending in a keyword ("@molecule/app-auth": "1.2.0")
74
+ // is not a secret, and matching from its tail masked the version (flash-m35).
75
+ `((?<![\\w@/.-])['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)"(?:[^"\\\\\\n]|\\\\.)*(?:"|(?=\\n)|$)`, 'gi');
76
+ const SECRET_JSON_SQ = new RegExp(`((?<![\\w@/.-])['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)'(?:[^'\\\\\\n]|\\\\.)*(?:'|(?=\\n)|$)`, 'gi');
72
77
  /**
73
78
  * Exact VALUES that are UI vocabulary, never credentials — kept verbatim even when they
74
79
  * sit next to a keyword-looking name. Frontend auth code is full of these: a JSX ternary
@@ -189,6 +194,26 @@ export function declaredBudgetSeconds(command, timeoutParam) {
189
194
  export function outputIsWithheldUntilExit(command) {
190
195
  return /\|\s*(?:tail|head|grep|egrep|fgrep|sort|uniq|wc|jq)\b/.test(command);
191
196
  }
197
+ /**
198
+ * The file a command sends its own output to, when it redirects it (`> out.txt`,
199
+ * `>> log`, `&> run.log`, `| tee [-a] file`), or null. A background command that does
200
+ * this leaves its task log empty, so the log is the wrong place to look and an empty
201
+ * log proves nothing (X1 flash-m29).
202
+ *
203
+ * @param command - The shell command.
204
+ * @returns The target as written, or null (`/dev/null` and `&1`/`&2` are not files).
205
+ */
206
+ export function outputRedirectTarget(command) {
207
+ const tee = /\|\s*tee\s+(?:-a\s+)?([^\s;&|]+)/.exec(command);
208
+ if (tee)
209
+ return tee[1];
210
+ let last = null;
211
+ for (const m of command.matchAll(/(?:^|[^0-9&>])(?:&>|1?>>?)\s*([^\s;&|]+)/g)) {
212
+ if (m[1] !== '/dev/null' && !m[1].startsWith('&'))
213
+ last = m[1];
214
+ }
215
+ return last;
216
+ }
192
217
  /** A trailing `| tail -N` / `| tail -n N` (stderr redirects before it stay). */
193
218
  const TRAILING_TAIL = /\s*\|\s*tail(?:\s+-n)?\s+[-+]?\d+\s*$/;
194
219
  /**
@@ -216,6 +241,13 @@ export function streamingBackgroundCommand(command) {
216
241
  changes.push(`Dropped the trailing \`${tail[0].trim()}\`: it holds all output back until the command ends, ` +
217
242
  'and wait_for_task returns the end of the log anyway.');
218
243
  }
244
+ // A Playwright run behind `| head -N` loses its tally, which prints last (X1
245
+ // flash-m32 counted ✓/✘ marks by hand); the full log comes back anyway.
246
+ const head = /\s*\|\s*head(?:\s+-n)?\s+-?\d+\s*$/.exec(out);
247
+ if (head && /\bplaywright\s+test\b/.test(out)) {
248
+ out = out.slice(0, head.index);
249
+ changes.push(`Dropped the trailing \`${head[0].trim()}\`: Playwright prints its pass/fail count last, and it would be cut.`);
250
+ }
219
251
  const lastStep = out.split(/&&|\|\||;/).at(-1) ?? '';
220
252
  if (/\bplaywright\s+test\b/.test(lastStep) &&
221
253
  !/--reporter\b/.test(lastStep) &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.5.7",
3
+ "version": "1.5.9",
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",