@molecule/api-ai-tools 1.5.20 → 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 +302 -38
- 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 +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/no-test-run.d.ts +13 -0
- package/dist/no-test-run.d.ts.map +1 -1
- package/dist/no-test-run.js +31 -22
- 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 +113 -49
- package/dist/utilities.d.ts +189 -38
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +463 -85
- 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,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';
|
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,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';
|
package/dist/no-test-run.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/no-test-run.js
CHANGED
|
@@ -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
|
-
*
|
|
31
|
-
*
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
*
|
|
86
|
+
* segment ({@link commandSegments}) — only prints.
|
|
83
87
|
*
|
|
84
|
-
* @param runner - The runner the
|
|
85
|
-
* @param after - Everything after the runner's name in that
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
}
|
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"}
|