@molecule/api-ai-tools 1.5.20 → 1.6.2

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.
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Command position: which program a shell segment actually RUNS, and which words are only
3
+ * arguments to something else.
4
+ *
5
+ * Every classifier that read a command as "this runs the suite" matched the runner's NAME
6
+ * anywhere in the text, so a runner named in an argument was read as an invocation: X1
7
+ * xr-molecule-flash-m81 (+20:03) sent
8
+ * `cd /workspace/fixtures/realworld/specs/api && ls && head -30 run-api-tests-hurl.sh` — two
9
+ * reads of a file that was already there — and the suite-overlap hold answered "a Playwright
10
+ * suite is already running against this app and database … wait for it", costing a minute of
11
+ * forced idling on a command that writes nothing and runs nothing. `cat`, `grep`, `sed -n`,
12
+ * `wc`, `ls` and `git show` of a runner script are the same shape.
13
+ *
14
+ * So this parser answers it in one place, for every caller: split the command at its pipeline
15
+ * and statement separators, take each segment's word in command position — after the wrapper
16
+ * prefixes a shell puts in front of it (`HOST=x sudo nice -n 10 npx`, `bash`, `timeout 280`) —
17
+ * and let the caller match its runner against THAT segment's text, never against the raw
18
+ * command. `{@link commandSegments}` returns the segments, and
19
+ * {@link runsInCommandPosition} asks one anchored pattern of them all.
20
+ *
21
+ * Not command position, and therefore not an invocation: a runner named as an argument
22
+ * (`head -30 run-api-tests-hurl.sh`, `grep -n playwright e2e/README.md`,
23
+ * `git show HEAD:run-api-tests.sh`), and a runner named in a heredoc body — a file a command
24
+ * WRITES is the caller's business, so a command with a heredoc goes through the caller's own
25
+ * stripper first (`withoutHeredocs` in molecule-dev's suite checks).
26
+ *
27
+ * @module
28
+ */
29
+ /** A command's words, quotes kept whole — the same reading a printed-source check uses. */
30
+ const WORD = /"(?:\\.|[^"\\])*"|'[^']*'|\S+/g;
31
+ /** Shell words that stand alone: grouping, never an argument. */
32
+ const GROUPING = new Set(['(', ')', '{', '}']);
33
+ /** How deep a `sh -c '<command>'` is opened before the parser leaves it alone. */
34
+ const MAX_NESTING = 3;
35
+ /** How many wrapper words one segment may carry before the parser gives up on it. */
36
+ const MAX_WRAPPERS = 32;
37
+ /**
38
+ * Words that run in front of the real program: a scheduler, a privilege wrapper, a process
39
+ * launcher, or a name that simply runs the next word (`command ls`, `exec ls`). All of them
40
+ * leave what runs unchanged, so all of them are stripped.
41
+ */
42
+ const LAUNCHERS = new Set([
43
+ 'command',
44
+ 'doas',
45
+ 'env',
46
+ 'exec',
47
+ 'ionice',
48
+ 'nice',
49
+ 'nohup',
50
+ 'setsid',
51
+ 'sudo',
52
+ 'time',
53
+ 'timeout',
54
+ ]);
55
+ /** The shells whose `-c '<command>'` argument is a command of its own. */
56
+ const SHELLS = new Set(['ash', 'bash', 'dash', 'ksh', 'sh', 'zsh']);
57
+ /** Package runners that run the program named after them (`npx playwright test`). */
58
+ const PACKAGE_RUNNERS = new Set(['bunx', 'npx', 'pnpx']);
59
+ /** Package managers whose sub-command runs a package's binary (`bun x`, `pnpm dlx`, `npm exec`). */
60
+ const PACKAGE_RUNNER_HEADS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
61
+ /**
62
+ * The sub-commands that run a package's binary, after a {@link PACKAGE_RUNNER_HEADS} word. Its
63
+ * other sub-commands are the program itself, not a wrapper: `yarn test` and `bun run build` run
64
+ * a script of the project's, and nothing is stripped from them.
65
+ */
66
+ const PACKAGE_RUNNER_SUBCOMMANDS = new Set(['dlx', 'exec', 'x']);
67
+ /** Wrapper flags that are followed by their own value (`sudo -u root`, `timeout -k 5`). */
68
+ const WRAPPER_VALUE_FLAGS = {
69
+ env: ['-S', '--split-string', '-u', '--unset', '-C', '--chdir'],
70
+ ionice: ['-c', '--class', '-n', '--classdata', '-p', '--pid', '-P', '--pgid', '-u', '--uid'],
71
+ nice: ['-n', '--adjustment'],
72
+ sudo: [
73
+ '-C',
74
+ '--chdir',
75
+ '-g',
76
+ '--group',
77
+ '-p',
78
+ '--prompt',
79
+ '-r',
80
+ '--role',
81
+ '-t',
82
+ '--type',
83
+ '-u',
84
+ '--user',
85
+ '-U',
86
+ '--other-user',
87
+ ],
88
+ time: ['-f', '--format', '-o', '--output'],
89
+ timeout: ['-k', '--kill-after', '-s', '--signal'],
90
+ };
91
+ /** An environment assignment (`HOST=…`, `BASE_URL="…"`): it runs nothing of its own. */
92
+ const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
93
+ /** A wrapper's own duration — `timeout 280 …`, `timeout 1m …` — which is not a flag. */
94
+ const DURATION = /^\d+(?:\.\d+)?[smhd]?$/;
95
+ /**
96
+ * A word without its shell quotes.
97
+ *
98
+ * @param word - One word of a segment.
99
+ * @returns The word with a leading and a trailing quote character removed.
100
+ */
101
+ function unquoted(word) {
102
+ return word.replace(/^['"]|['"]$/g, '');
103
+ }
104
+ /**
105
+ * A word's program name: quotes removed, directory removed (`/usr/bin/hurl` → `hurl`). A scoped
106
+ * package keeps its scope — the `/` in `@usebruno/cli` is part of the name, not a directory.
107
+ *
108
+ * @param word - One word of a segment.
109
+ * @returns The bare name.
110
+ */
111
+ function programName(word) {
112
+ const bare = unquoted(word);
113
+ if (bare.startsWith('@'))
114
+ return bare;
115
+ const parts = bare.split('/');
116
+ return parts[parts.length - 1] ?? '';
117
+ }
118
+ /**
119
+ * How many of a wrapper's own words follow it — its flags, and each value flag's value.
120
+ *
121
+ * @param words - The segment's words.
122
+ * @param from - The index just after the wrapper.
123
+ * @param wrapper - The wrapper's program name.
124
+ * @returns The count of words the wrapper owns.
125
+ */
126
+ function flagWords(words, from, wrapper) {
127
+ const withValue = WRAPPER_VALUE_FLAGS[wrapper] ?? [];
128
+ let count = 0;
129
+ while (from + count < words.length && (words[from + count] ?? '').startsWith('-')) {
130
+ const flag = words[from + count] ?? '';
131
+ // `--flag=value` carries its value inside the same word.
132
+ count += !flag.includes('=') && withValue.includes(flag) ? 2 : 1;
133
+ }
134
+ return count;
135
+ }
136
+ /**
137
+ * The `-c '<command>'` string a shell was handed, or null when the shell was given a FILE
138
+ * (`bash run.sh`) — the wrapper walk then steps past its flags instead.
139
+ *
140
+ * @param words - The segment's words.
141
+ * @param from - The index just after the shell's name.
142
+ * @returns The command string unquoted, or null.
143
+ */
144
+ function nestedCommand(words, from) {
145
+ for (let i = from; i < words.length; i++) {
146
+ const word = words[i] ?? '';
147
+ if (!word.startsWith('-'))
148
+ return null;
149
+ // A flag cluster holding `c` is `-c` (`bash -lc '…'`, `sh -ec '…'`).
150
+ if (/^-[A-Za-z]*c[A-Za-z]*$/.test(word) || word === '--command') {
151
+ const next = words[i + 1];
152
+ return next === undefined ? null : unquoted(next);
153
+ }
154
+ }
155
+ return null;
156
+ }
157
+ /**
158
+ * Where the program starts in one segment's words.
159
+ *
160
+ * @param words - The segment's words, quotes kept.
161
+ * @returns The index of the word in command position (one past the end when the segment runs
162
+ * nothing), and the command string of a `sh -c '<command>'` this segment turns out to be.
163
+ */
164
+ function commandStart(words) {
165
+ let at = 0;
166
+ for (let guard = 0; guard < MAX_WRAPPERS && at < words.length; guard++) {
167
+ const word = words[at] ?? '';
168
+ if (ENV_ASSIGNMENT.test(word)) {
169
+ at += 1;
170
+ continue;
171
+ }
172
+ const name = programName(word);
173
+ // `! cmd` inverts an exit status: what runs is still the word after it.
174
+ if (name === '!') {
175
+ at += 1;
176
+ continue;
177
+ }
178
+ if (SHELLS.has(name)) {
179
+ const nested = nestedCommand(words, at + 1);
180
+ if (nested !== null)
181
+ return { at, nested };
182
+ at += 1 + flagWords(words, at + 1, name);
183
+ continue;
184
+ }
185
+ if (PACKAGE_RUNNERS.has(name)) {
186
+ at += 1 + flagWords(words, at + 1, name);
187
+ continue;
188
+ }
189
+ if (PACKAGE_RUNNER_HEADS.has(name) &&
190
+ PACKAGE_RUNNER_SUBCOMMANDS.has(unquoted(words[at + 1] ?? ''))) {
191
+ at += 2 + flagWords(words, at + 2, name);
192
+ continue;
193
+ }
194
+ if (!LAUNCHERS.has(name))
195
+ return { at, nested: null };
196
+ at += 1 + flagWords(words, at + 1, name);
197
+ if (name === 'timeout' && DURATION.test(words[at] ?? ''))
198
+ at += 1;
199
+ }
200
+ return { at, nested: null };
201
+ }
202
+ /**
203
+ * A command's statements and pipeline stages, split where the shell splits them — inside quotes
204
+ * and inside a command substitution, a `|`, `;` or `&&` belongs to that command, not to this one.
205
+ *
206
+ * A single `&` separates only when it backgrounds a command: the one in a redirect (`2>&1`,
207
+ * `&>log`) is part of the word it sits in.
208
+ *
209
+ * @param command - The shell command.
210
+ * @returns Each segment's text, in order, with empty ones dropped.
211
+ */
212
+ function splitSegments(command) {
213
+ const parts = [];
214
+ let buffer = '';
215
+ let quote = null;
216
+ let substitutions = 0;
217
+ for (let i = 0; i < command.length; i++) {
218
+ const char = command[i] ?? '';
219
+ if (quote !== null) {
220
+ buffer += char;
221
+ if (char === '\\' && quote === '"' && i + 1 < command.length) {
222
+ buffer += command[i + 1] ?? '';
223
+ i += 1;
224
+ }
225
+ else if (char === quote)
226
+ quote = null;
227
+ continue;
228
+ }
229
+ if (char === '"' || char === "'") {
230
+ quote = char;
231
+ buffer += char;
232
+ continue;
233
+ }
234
+ if (char === '\\' && i + 1 < command.length) {
235
+ buffer += char + (command[i + 1] ?? '');
236
+ i += 1;
237
+ continue;
238
+ }
239
+ if (char === '$' && command[i + 1] === '(') {
240
+ substitutions += 1;
241
+ buffer += char;
242
+ continue;
243
+ }
244
+ if (substitutions > 0) {
245
+ if (char === '(')
246
+ substitutions += 1;
247
+ else if (char === ')')
248
+ substitutions -= 1;
249
+ buffer += char;
250
+ continue;
251
+ }
252
+ const next = command[i + 1];
253
+ if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
254
+ parts.push(buffer);
255
+ buffer = '';
256
+ i += 1;
257
+ continue;
258
+ }
259
+ if (char === ';' || char === '|' || char === '\n') {
260
+ parts.push(buffer);
261
+ buffer = '';
262
+ continue;
263
+ }
264
+ if (char === '&' && command[i - 1] !== '>' && next !== '>') {
265
+ parts.push(buffer);
266
+ buffer = '';
267
+ continue;
268
+ }
269
+ buffer += char;
270
+ }
271
+ parts.push(buffer);
272
+ return parts.map((part) => part.trim()).filter(Boolean);
273
+ }
274
+ /**
275
+ * One segment's words: quotes kept whole, grouping parentheses dropped, and a `(` written
276
+ * against the first word taken off it (`(npx playwright test)`).
277
+ *
278
+ * @param text - One segment's text.
279
+ * @returns The words.
280
+ */
281
+ function segmentWords(text) {
282
+ const words = text.match(WORD) ?? [];
283
+ const cleaned = words.filter((word) => !GROUPING.has(word));
284
+ const first = cleaned[0];
285
+ if (first !== undefined) {
286
+ const bare = first.replace(/^[({]+/, '');
287
+ if (bare === '')
288
+ cleaned.shift();
289
+ else
290
+ cleaned[0] = bare;
291
+ }
292
+ return cleaned;
293
+ }
294
+ /**
295
+ * The segments of a command, each with its program and arguments — the answer every classifier
296
+ * that reads a command as "this runs the suite" is asking for.
297
+ *
298
+ * @param command - The shell command.
299
+ * @returns One entry per statement or pipeline stage that runs something, in order.
300
+ */
301
+ export function commandSegments(command) {
302
+ return segmentsOf(command, 0);
303
+ }
304
+ /**
305
+ * {@link commandSegments}'s recursion, so a `sh -c '<command>'` is opened as far as
306
+ * {@link MAX_NESTING} allows.
307
+ *
308
+ * @param command - The shell command.
309
+ * @param depth - How many `sh -c` strings this command already sits inside.
310
+ * @returns The segments.
311
+ */
312
+ function segmentsOf(command, depth) {
313
+ if (typeof command !== 'string' || depth > MAX_NESTING)
314
+ return [];
315
+ const out = [];
316
+ for (const text of splitSegments(command)) {
317
+ const words = segmentWords(text);
318
+ if (words.length === 0)
319
+ continue;
320
+ const start = commandStart(words);
321
+ if (start.nested !== null) {
322
+ out.push(...segmentsOf(start.nested, depth + 1));
323
+ continue;
324
+ }
325
+ const word = words[start.at];
326
+ // Only wrappers and assignments: nothing this segment runs.
327
+ if (word === undefined)
328
+ continue;
329
+ const program = programName(word);
330
+ const args = words.slice(start.at + 1);
331
+ out.push({ program, args, text: [program, ...args].join(' ') });
332
+ }
333
+ return out;
334
+ }
335
+ /**
336
+ * Whether any segment of the command runs a program whose command-position text matches
337
+ * `pattern` — the one question a runner-kind table asks.
338
+ *
339
+ * The pattern is matched against `program` and its arguments as one line, so it must be
340
+ * anchored at `^`; it must not carry the `g` flag (a pattern shared between calls would then
341
+ * carry its own match position from one segment to the next).
342
+ *
343
+ * @param command - The shell command.
344
+ * @param pattern - An anchored pattern for the runner's invocation.
345
+ * @returns True when a segment in command position matches.
346
+ */
347
+ export function runsInCommandPosition(command, pattern) {
348
+ return commandSegments(command).some((segment) => {
349
+ pattern.lastIndex = 0;
350
+ return pattern.test(segment.text);
351
+ });
352
+ }
package/dist/index.d.ts CHANGED
@@ -26,9 +26,13 @@
26
26
  * - **`read_file` takes `paths` as well as `path`.** An agent that issues one
27
27
  * tool call per model round-trip pays a full round-trip per file, so a project
28
28
  * survey costs minutes; batched, the same reads cost one call each 25 files
29
- * (`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
30
- * stopped). Each entry carries its own content or its own error, so one bad
31
- * path never fails the batch.
29
+ * (`MAX_BATCH_READ_FILES`). One result is capped at `MAX_READ_CALL_CHARS`, the
30
+ * ceiling the request side puts on a single tool result: whole files are
31
+ * returned up to it and every path past that is named in `note` and listed as
32
+ * `not returned`, so a file is never handed back in part — the shape that lost
33
+ * the middle of a ten-file batch to the request-side cut (X0 flash-m81). Each
34
+ * entry carries its own content or its own error, so one bad path never fails
35
+ * the batch.
32
36
  * - **`edit_file` shows the result, so the model need not re-read.** A
33
37
  * successful edit returns `excerpt`: each edited region as it now reads, with
34
38
  * 1-based line numbers (`N: text`) and a few lines of context, capped at
@@ -116,6 +120,7 @@
116
120
  export * from './backends/local.js';
117
121
  export * from './backends/sandbox.js';
118
122
  export * from './browser-guard.js';
123
+ export * from './command-position.js';
119
124
  export * from './input-normalizer.js';
120
125
  export * from './no-test-run.js';
121
126
  export * from './schemas.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkHG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsHG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -26,9 +26,13 @@
26
26
  * - **`read_file` takes `paths` as well as `path`.** An agent that issues one
27
27
  * tool call per model round-trip pays a full round-trip per file, so a project
28
28
  * survey costs minutes; batched, the same reads cost one call each 25 files
29
- * (`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
30
- * stopped). Each entry carries its own content or its own error, so one bad
31
- * path never fails the batch.
29
+ * (`MAX_BATCH_READ_FILES`). One result is capped at `MAX_READ_CALL_CHARS`, the
30
+ * ceiling the request side puts on a single tool result: whole files are
31
+ * returned up to it and every path past that is named in `note` and listed as
32
+ * `not returned`, so a file is never handed back in part — the shape that lost
33
+ * the middle of a ten-file batch to the request-side cut (X0 flash-m81). Each
34
+ * entry carries its own content or its own error, so one bad path never fails
35
+ * the batch.
32
36
  * - **`edit_file` shows the result, so the model need not re-read.** A
33
37
  * successful edit returns `excerpt`: each edited region as it now reads, with
34
38
  * 1-based line numbers (`N: text`) and a few lines of context, capped at
@@ -116,6 +120,7 @@
116
120
  export * from './backends/local.js';
117
121
  export * from './backends/sandbox.js';
118
122
  export * from './browser-guard.js';
123
+ export * from './command-position.js';
119
124
  export * from './input-normalizer.js';
120
125
  export * from './no-test-run.js';
121
126
  export * from './schemas.js';
@@ -24,12 +24,25 @@
24
24
  * through it. A command that runs no test is never held, never noted, and never rewritten
25
25
  * (`--retries`, `--reporter`) as if it were a run.
26
26
  *
27
+ * What counts as an invocation is COMMAND POSITION, read by `@molecule/api-ai-tools`'
28
+ * `commandSegments`: `npx playwright test --list`, `hurl --help` and `bash run.sh` invoke the
29
+ * runner, while a runner named as an argument (`grep -n playwright e2e/README.md`,
30
+ * `head -30 run-api-tests-hurl.sh`) invokes nothing at all. X1 xr-molecule-flash-m81 (+20:03)
31
+ * read a runner script with `head -30` and the suite-overlap hold answered "a Playwright suite is
32
+ * already running … wait for it", for a command that runs nothing.
33
+ *
27
34
  * @module
28
35
  */
29
36
  /**
30
37
  * Whether a shell command invokes a test runner and EVERY such invocation only prints — so the
31
38
  * command runs no test at all.
32
39
  *
40
+ * A runner counts only where a shell would RUN it ({@link commandSegments}): the word in command
41
+ * position of a segment, after its wrapper prefixes. A runner named in an argument is not an
42
+ * invocation — `head -30 run-api-tests-hurl.sh` and `grep -n playwright e2e/README.md` name a
43
+ * file to read, and a `--list` written inside a file the command writes is not a flag anyone runs
44
+ * with (flash-m81).
45
+ *
33
46
  * A command that invokes no runner is NOT one of these (false): "the command runs no test" is
34
47
  * then true for `ls`, which is not the question any caller is asking. The question is whether
35
48
  * the runner invocation each caller read as a run is one.
@@ -1 +1 @@
1
- {"version":3,"file":"no-test-run.d.ts","sourceRoot":"","sources":["../src/no-test-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA6EH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAY3D"}
1
+ {"version":3,"file":"no-test-run.d.ts","sourceRoot":"","sources":["../src/no-test-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AA0EH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAU3D"}
@@ -24,19 +24,25 @@
24
24
  * through it. A command that runs no test is never held, never noted, and never rewritten
25
25
  * (`--retries`, `--reporter`) as if it were a run.
26
26
  *
27
+ * What counts as an invocation is COMMAND POSITION, read by `@molecule/api-ai-tools`'
28
+ * `commandSegments`: `npx playwright test --list`, `hurl --help` and `bash run.sh` invoke the
29
+ * runner, while a runner named as an argument (`grep -n playwright e2e/README.md`,
30
+ * `head -30 run-api-tests-hurl.sh`) invokes nothing at all. X1 xr-molecule-flash-m81 (+20:03)
31
+ * read a runner script with `head -30` and the suite-overlap hold answered "a Playwright suite is
32
+ * already running … wait for it", for a command that runs nothing.
33
+ *
27
34
  * @module
28
35
  */
36
+ import { commandSegments } from './command-position.js';
29
37
  /**
30
- * A runner's name as a statement invokes it, from the start of its own arguments. The trailing
31
- * guard is what separates the runner from a FILE named after it — `-c playwright.local.config.ts`
32
- * names a config, not a second invocation, and reading it as one would give that "invocation" no
33
- * `--list` of its own and un-do the answer for the real one.
38
+ * The runners by the word in command position. A runner spelled as a FILE's name is not one:
39
+ * `-c playwright.local.config.ts` names a config, and `node playwright.js` runs node.
34
40
  */
35
- const INVOCATIONS = [
36
- ['playwright', /(?:^|[\s;&|(])(?:npx\s+(?:--no-install\s+)?)?(?:\S*\/)?playwright(?![\w.-])/g],
37
- ['vitest', /(?:^|[\s;&|(])(?:npx\s+(?:--no-install\s+)?)?(?:\S*\/)?vitest(?![\w.-])/g],
38
- ['hurl', /(?:^|[\s;&|(])(?:\S*\/)?hurl(?![\w.-])/g],
39
- ];
41
+ const RUNNERS = {
42
+ playwright: 'playwright',
43
+ vitest: 'vitest',
44
+ hurl: 'hurl',
45
+ };
40
46
  /** Playwright's subcommands that start no test. */
41
47
  const PLAYWRIGHT_INFO_SUBCOMMANDS = [
42
48
  'show-report',
@@ -69,8 +75,6 @@ const NO_RUN_FLAGS = {
69
75
  ],
70
76
  hurl: [/(?:^|\s)--help(?=\s|$)/, /(?:^|\s)-h(?=\s|$)/, /(?:^|\s)--version(?=\s|$)/],
71
77
  };
72
- /** Statements of a shell command, the way the suite checks split them. */
73
- const STATEMENT_SEP = /\s*(?:&&|\|\||;|\||\n)\s*/;
74
78
  /**
75
79
  * A heredoc body is data a command writes, not what it runs: a `playwright test --list` inside
76
80
  * the config a command writes is not a run of this command either way. The pattern consumes the
@@ -79,10 +83,10 @@ const STATEMENT_SEP = /\s*(?:&&|\|\||;|\||\n)\s*/;
79
83
  const HEREDOC = /[^\n;&|]*<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\s*\2\b/g;
80
84
  /**
81
85
  * Whether this runner's invocation — the text from just after its name to the end of its
82
- * statement — only prints.
86
+ * segment ({@link commandSegments}) — only prints.
83
87
  *
84
- * @param runner - The runner the statement invokes.
85
- * @param after - Everything after the runner's name in that statement.
88
+ * @param runner - The runner the segment invokes.
89
+ * @param after - Everything after the runner's name in that segment.
86
90
  * @returns True when the invocation starts no test.
87
91
  */
88
92
  function informational(runner, after) {
@@ -97,6 +101,12 @@ function informational(runner, after) {
97
101
  * Whether a shell command invokes a test runner and EVERY such invocation only prints — so the
98
102
  * command runs no test at all.
99
103
  *
104
+ * A runner counts only where a shell would RUN it ({@link commandSegments}): the word in command
105
+ * position of a segment, after its wrapper prefixes. A runner named in an argument is not an
106
+ * invocation — `head -30 run-api-tests-hurl.sh` and `grep -n playwright e2e/README.md` name a
107
+ * file to read, and a `--list` written inside a file the command writes is not a flag anyone runs
108
+ * with (flash-m81).
109
+ *
100
110
  * A command that invokes no runner is NOT one of these (false): "the command runs no test" is
101
111
  * then true for `ls`, which is not the question any caller is asking. The question is whether
102
112
  * the runner invocation each caller read as a run is one.
@@ -112,14 +122,13 @@ export function commandRunsNoTests(command) {
112
122
  if (typeof command !== 'string' || !command.trim())
113
123
  return false;
114
124
  let invocations = 0;
115
- for (const statement of command.replace(HEREDOC, ' ').split(STATEMENT_SEP)) {
116
- for (const [runner, re] of INVOCATIONS) {
117
- for (const m of statement.matchAll(re)) {
118
- invocations += 1;
119
- if (!informational(runner, statement.slice((m.index ?? 0) + m[0].length)))
120
- return false;
121
- }
122
- }
125
+ for (const segment of commandSegments(command.replace(HEREDOC, ' '))) {
126
+ const runner = RUNNERS[segment.program];
127
+ if (!runner)
128
+ continue;
129
+ invocations += 1;
130
+ if (!informational(runner, segment.args.join(' ')))
131
+ return false;
123
132
  }
124
133
  return invocations > 0;
125
134
  }
@@ -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,CAmU1C,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,CAyU1C,CAAA"}
package/dist/schemas.js CHANGED
@@ -29,7 +29,8 @@ export const TOOL_SCHEMAS = {
29
29
  'gathering the files behind one feature — pass them all as `paths` in ONE call rather ' +
30
30
  'than calling this tool once per file: same content, a fraction of the time. ' +
31
31
  'For a @molecule package\u2019s docs call read_molecule_doc instead — one call, the whole ' +
32
- 'document. A file under 80 KB comes back whole; do not page through it with offset/limit.',
32
+ 'document. A file that fits one read result comes back whole; do not page through it with ' +
33
+ 'offset/limit.',
33
34
  parameters: {
34
35
  type: 'object',
35
36
  properties: {
@@ -42,7 +43,9 @@ export const TOOL_SCHEMAS = {
42
43
  items: { type: 'string' },
43
44
  description: 'Paths to SEVERAL files to read in one call — preferred whenever you want more ' +
44
45
  'than one file. Returns `files`, one entry per path in the order given, each ' +
45
- "with that file's content or its own error.",
46
+ "with that file's content or its own error. Whole files are returned up to what one " +
47
+ 'read result carries; every path past that comes back in the same list marked ' +
48
+ '`not returned` — read those next, in their own calls. No file is ever returned in part.',
46
49
  },
47
50
  offset: {
48
51
  type: 'number',
@@ -248,8 +251,11 @@ export const TOOL_SCHEMAS = {
248
251
  'no polling. Use it the moment you need that result to continue; never `sleep` and ' +
249
252
  'read the log by hand. If it is still running when the budget ends, the result says so ' +
250
253
  'and you can call it again. A suite whose failures keep repeating one cause comes back ' +
251
- 'EARLY with that cause named, the tests it hit and the command that stops the run — act ' +
252
- 'on it instead of waiting for the rest.',
254
+ 'EARLY with that cause named and the tests it hit — start fixing it, and still LET THE RUN ' +
255
+ 'FINISH, because the run is what names its other causes; the note says so, and names the ' +
256
+ 'command that stops the run only when that one cause is failing every test. A finished ' +
257
+ 'run with failures comes back with a note grouping them by cause, so one round of fixes ' +
258
+ 'covers every cause it found.',
253
259
  parameters: {
254
260
  type: 'object',
255
261
  properties: {
@@ -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;AAkL9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAqDnE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CA0zDxF"}
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;AAkL9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAoEnE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAm5DxF"}