@molecule/api-ai-tools 1.5.19 → 1.6.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 +343 -37
- package/dist/command-position.d.ts +62 -0
- package/dist/command-position.d.ts.map +1 -0
- package/dist/command-position.js +352 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/no-test-run.d.ts +58 -0
- package/dist/no-test-run.d.ts.map +1 -0
- package/dist/no-test-run.js +134 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +10 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +118 -51
- package/dist/utilities.d.ts +208 -39
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +495 -91
- package/package.json +1 -1
|
@@ -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`
|
|
30
|
-
*
|
|
31
|
-
* path
|
|
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,7 +120,9 @@
|
|
|
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';
|
|
125
|
+
export * from './no-test-run.js';
|
|
120
126
|
export * from './schemas.js';
|
|
121
127
|
export * from './system-prompt.js';
|
|
122
128
|
export * from './tools.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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`
|
|
30
|
-
*
|
|
31
|
-
* path
|
|
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,7 +120,9 @@
|
|
|
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';
|
|
125
|
+
export * from './no-test-run.js';
|
|
120
126
|
export * from './schemas.js';
|
|
121
127
|
export * from './system-prompt.js';
|
|
122
128
|
export * from './tools.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner invocations that print information and run no test — the one classifier every
|
|
3
|
+
* site asks before it treats a command as a test run.
|
|
4
|
+
*
|
|
5
|
+
* A Playwright CLI has subcommands and flags that never start a test: `playwright test --list`
|
|
6
|
+
* prints the tests it would run and exits, `show-report`/`show-trace` open a saved report,
|
|
7
|
+
* `codegen` records one, `install` fetches browsers, `merge-reports` joins blobs, and
|
|
8
|
+
* `--help`/`--version` print and exit. Vitest lists with `vitest list` / `--list` and hurl
|
|
9
|
+
* prints with `--help`/`--version`, the same way.
|
|
10
|
+
*
|
|
11
|
+
* Every check in the platform read such a command as a whole-suite run, because each one
|
|
12
|
+
* matched the runner's NAME. X1 xr-molecule-flash-m79's first Playwright command was a config
|
|
13
|
+
* check — a config written to `/tmp`, `cp`'d beside the suite and used with `--list` — and it
|
|
14
|
+
* came back HELD, `status: not_run`, with a ~4 KB `suiteNote` telling it to send the same
|
|
15
|
+
* command with `--max-failures=15` ("uncapped it walks every test … 8–13 minutes"), beside an
|
|
16
|
+
* environment block saying the run "ran on their defaults" and an import note about a path the
|
|
17
|
+
* `cp` had already fixed. Nothing ran. The model gave up on the approach, wrote a second
|
|
18
|
+
* config, and its next command was held again (flash-m74's "same command three times" shape):
|
|
19
|
+
* two holds, zero tests, and the config check it asked for never answered.
|
|
20
|
+
*
|
|
21
|
+
* So the classifiers live here, in one place, and callers ask them instead of their own
|
|
22
|
+
* regexes: {@link commandRunsNoTests} answers for a whole command, and the runner-specific
|
|
23
|
+
* sites (molecule-dev's suite checks, `streamingBackgroundCommand`, `isSuiteRunCommand`) route
|
|
24
|
+
* through it. A command that runs no test is never held, never noted, and never rewritten
|
|
25
|
+
* (`--retries`, `--reporter`) as if it were a run.
|
|
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
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Whether a shell command invokes a test runner and EVERY such invocation only prints — so the
|
|
38
|
+
* command runs no test at all.
|
|
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
|
+
*
|
|
46
|
+
* A command that invokes no runner is NOT one of these (false): "the command runs no test" is
|
|
47
|
+
* then true for `ls`, which is not the question any caller is asking. The question is whether
|
|
48
|
+
* the runner invocation each caller read as a run is one.
|
|
49
|
+
*
|
|
50
|
+
* A command that also runs tests is false however it is spelled: `npx playwright install &&
|
|
51
|
+
* npx playwright test` installs and runs, so a caller that sees `playwright test` in it is
|
|
52
|
+
* reading a real run.
|
|
53
|
+
*
|
|
54
|
+
* @param command - The shell command as the executor sent it.
|
|
55
|
+
* @returns True when the command invokes a runner only to print.
|
|
56
|
+
*/
|
|
57
|
+
export declare function commandRunsNoTests(command: string): boolean;
|
|
58
|
+
//# sourceMappingURL=no-test-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner invocations that print information and run no test — the one classifier every
|
|
3
|
+
* site asks before it treats a command as a test run.
|
|
4
|
+
*
|
|
5
|
+
* A Playwright CLI has subcommands and flags that never start a test: `playwright test --list`
|
|
6
|
+
* prints the tests it would run and exits, `show-report`/`show-trace` open a saved report,
|
|
7
|
+
* `codegen` records one, `install` fetches browsers, `merge-reports` joins blobs, and
|
|
8
|
+
* `--help`/`--version` print and exit. Vitest lists with `vitest list` / `--list` and hurl
|
|
9
|
+
* prints with `--help`/`--version`, the same way.
|
|
10
|
+
*
|
|
11
|
+
* Every check in the platform read such a command as a whole-suite run, because each one
|
|
12
|
+
* matched the runner's NAME. X1 xr-molecule-flash-m79's first Playwright command was a config
|
|
13
|
+
* check — a config written to `/tmp`, `cp`'d beside the suite and used with `--list` — and it
|
|
14
|
+
* came back HELD, `status: not_run`, with a ~4 KB `suiteNote` telling it to send the same
|
|
15
|
+
* command with `--max-failures=15` ("uncapped it walks every test … 8–13 minutes"), beside an
|
|
16
|
+
* environment block saying the run "ran on their defaults" and an import note about a path the
|
|
17
|
+
* `cp` had already fixed. Nothing ran. The model gave up on the approach, wrote a second
|
|
18
|
+
* config, and its next command was held again (flash-m74's "same command three times" shape):
|
|
19
|
+
* two holds, zero tests, and the config check it asked for never answered.
|
|
20
|
+
*
|
|
21
|
+
* So the classifiers live here, in one place, and callers ask them instead of their own
|
|
22
|
+
* regexes: {@link commandRunsNoTests} answers for a whole command, and the runner-specific
|
|
23
|
+
* sites (molecule-dev's suite checks, `streamingBackgroundCommand`, `isSuiteRunCommand`) route
|
|
24
|
+
* through it. A command that runs no test is never held, never noted, and never rewritten
|
|
25
|
+
* (`--retries`, `--reporter`) as if it were a run.
|
|
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
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
import { commandSegments } from './command-position.js';
|
|
37
|
+
/**
|
|
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.
|
|
40
|
+
*/
|
|
41
|
+
const RUNNERS = {
|
|
42
|
+
playwright: 'playwright',
|
|
43
|
+
vitest: 'vitest',
|
|
44
|
+
hurl: 'hurl',
|
|
45
|
+
};
|
|
46
|
+
/** Playwright's subcommands that start no test. */
|
|
47
|
+
const PLAYWRIGHT_INFO_SUBCOMMANDS = [
|
|
48
|
+
'show-report',
|
|
49
|
+
'show-trace',
|
|
50
|
+
'codegen',
|
|
51
|
+
'install',
|
|
52
|
+
'merge-reports',
|
|
53
|
+
];
|
|
54
|
+
/** Vitest's subcommand that prints what a run would collect. */
|
|
55
|
+
const VITEST_LIST_SUBCOMMAND = 'list';
|
|
56
|
+
/**
|
|
57
|
+
* The flags that put a runner in an information mode: it prints and exits. `-h` is `--help`
|
|
58
|
+
* for all three (hurl's manual documents `-h, --help`; Playwright's and Vitest's CLIs follow
|
|
59
|
+
* the same convention), and the value-taking flags beside them are why each is matched as a
|
|
60
|
+
* whole token — hurl's `-H` is a header, not help.
|
|
61
|
+
*/
|
|
62
|
+
const NO_RUN_FLAGS = {
|
|
63
|
+
playwright: [
|
|
64
|
+
/(?:^|\s)--list(?=\s|$)/,
|
|
65
|
+
/(?:^|\s)--dry-run(?=\s|$)/,
|
|
66
|
+
/(?:^|\s)--help(?=\s|$)/,
|
|
67
|
+
/(?:^|\s)-h(?=\s|$)/,
|
|
68
|
+
/(?:^|\s)--version(?=\s|$)/,
|
|
69
|
+
],
|
|
70
|
+
vitest: [
|
|
71
|
+
/(?:^|\s)--list(?=\s|$)/,
|
|
72
|
+
/(?:^|\s)--help(?=\s|$)/,
|
|
73
|
+
/(?:^|\s)-h(?=\s|$)/,
|
|
74
|
+
/(?:^|\s)--version(?=\s|$)/,
|
|
75
|
+
],
|
|
76
|
+
hurl: [/(?:^|\s)--help(?=\s|$)/, /(?:^|\s)-h(?=\s|$)/, /(?:^|\s)--version(?=\s|$)/],
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* A heredoc body is data a command writes, not what it runs: a `playwright test --list` inside
|
|
80
|
+
* the config a command writes is not a run of this command either way. The pattern consumes the
|
|
81
|
+
* writer's own line prefix, the same shape `outputRedirectTarget` reads.
|
|
82
|
+
*/
|
|
83
|
+
const HEREDOC = /[^\n;&|]*<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\s*\2\b/g;
|
|
84
|
+
/**
|
|
85
|
+
* Whether this runner's invocation — the text from just after its name to the end of its
|
|
86
|
+
* segment ({@link commandSegments}) — only prints.
|
|
87
|
+
*
|
|
88
|
+
* @param runner - The runner the segment invokes.
|
|
89
|
+
* @param after - Everything after the runner's name in that segment.
|
|
90
|
+
* @returns True when the invocation starts no test.
|
|
91
|
+
*/
|
|
92
|
+
function informational(runner, after) {
|
|
93
|
+
const first = after.trimStart().split(/\s+/)[0] ?? '';
|
|
94
|
+
if (runner === 'playwright' && PLAYWRIGHT_INFO_SUBCOMMANDS.includes(first))
|
|
95
|
+
return true;
|
|
96
|
+
if (runner === 'vitest' && first === VITEST_LIST_SUBCOMMAND)
|
|
97
|
+
return true;
|
|
98
|
+
return NO_RUN_FLAGS[runner].some((re) => re.test(after));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Whether a shell command invokes a test runner and EVERY such invocation only prints — so the
|
|
102
|
+
* command runs no test at all.
|
|
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
|
+
*
|
|
110
|
+
* A command that invokes no runner is NOT one of these (false): "the command runs no test" is
|
|
111
|
+
* then true for `ls`, which is not the question any caller is asking. The question is whether
|
|
112
|
+
* the runner invocation each caller read as a run is one.
|
|
113
|
+
*
|
|
114
|
+
* A command that also runs tests is false however it is spelled: `npx playwright install &&
|
|
115
|
+
* npx playwright test` installs and runs, so a caller that sees `playwright test` in it is
|
|
116
|
+
* reading a real run.
|
|
117
|
+
*
|
|
118
|
+
* @param command - The shell command as the executor sent it.
|
|
119
|
+
* @returns True when the command invokes a runner only to print.
|
|
120
|
+
*/
|
|
121
|
+
export function commandRunsNoTests(command) {
|
|
122
|
+
if (typeof command !== 'string' || !command.trim())
|
|
123
|
+
return false;
|
|
124
|
+
let invocations = 0;
|
|
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;
|
|
132
|
+
}
|
|
133
|
+
return invocations > 0;
|
|
134
|
+
}
|
package/dist/schemas.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
252
|
-
'
|
|
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: {
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -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;
|
|
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;AA0DnE;;;;;;;;;;;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,CAk4DxF"}
|