@proagentstore/cli 0.4.143 → 0.4.145
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/dist/browser-runner/coding/inspect.js +7 -3
- package/dist/browser-runner/coding/path-glob.js +82 -0
- package/dist/browser-runner/coding/repo.js +4 -2
- package/dist/browser-runner/coding/runtime.js +3 -2
- package/dist/browser-runner/coding/terminal.js +9 -8
- package/dist/browser-runner/runner.js +4 -4
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { globMatches, isGlob } from "./path-glob.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
4
|
import { relative, resolve, sep } from "node:path";
|
|
@@ -331,9 +332,12 @@ export function repoSearch(workDir, opts) {
|
|
|
331
332
|
const lines = out.split("\n").filter((l) => l !== "");
|
|
332
333
|
if (mode === "path") {
|
|
333
334
|
// The pattern never reaches git here — the filter is a plain case-insensitive substring
|
|
334
|
-
// over the path, which is what "find the file called X" actually means.
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
// over the path, which is what "find the file called X" actually means. A `glob` (#1070) is
|
|
336
|
+
// matched instead, over the WHOLE list, so `total` counts real matches, not the literal's.
|
|
337
|
+
const glob = (opts.glob ?? "").trim();
|
|
338
|
+
const useGlob = glob !== "" && isGlob(glob);
|
|
339
|
+
const all = lines.filter((p) => (useGlob ? globMatches(glob, p) : p.toLowerCase().includes(pattern.toLowerCase()))).map((p) => ({ path: p }));
|
|
340
|
+
return { mode, pattern, matches: all.slice(0, limit), shown: Math.min(all.length, limit), total: all.length, truncated: all.length > limit, ...(useGlob ? { glob: true } : {}) };
|
|
337
341
|
}
|
|
338
342
|
const all = lines.map(parseGrepLine).filter((m) => m !== null);
|
|
339
343
|
const perFile = new Map();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob patterns over repo-relative paths, for `repo_find` (#1070).
|
|
3
|
+
*
|
|
4
|
+
* `repo_find` matched its pattern as a plain substring, so `*.test.js` returned NOTHING — no path
|
|
5
|
+
* contains an asterisk — and an agent concluded a repository with ~90 test files had none. Silent false
|
|
6
|
+
* negatives are the failure; this is the matcher that ends them.
|
|
7
|
+
*
|
|
8
|
+
* Semantics (gitignore/ripgrep-like, case-insensitive like the substring search it extends):
|
|
9
|
+
* `*` any run of characters within ONE path segment (never a `/`)
|
|
10
|
+
* `**` any run across segments; `**` followed by `/` also matches zero folders
|
|
11
|
+
* `?` one character, not `/`
|
|
12
|
+
* `[ab]` `[a-z]` `[!ab]` a character class (`!` negates), never `/`
|
|
13
|
+
* A pattern WITHOUT a `/` matches any single segment — a file name or a folder name at any depth, a
|
|
14
|
+
* folder matching taking everything beneath it — so `*.test.js` and `*e2e*` mean what they mean in a
|
|
15
|
+
* shell or `rg -g`. A pattern WITH a `/` is anchored at the repository root (`src/*.ts`,
|
|
16
|
+
* `**\/handlers/*.js`); a leading `/` or `./` is accepted and means the same.
|
|
17
|
+
*
|
|
18
|
+
* DUPLICATED in `workers/api/src/lib/path-glob.ts`: the cloud filters with it when the runner is too old
|
|
19
|
+
* to (an old runner ignores `glob`). `workers/api/src/lib/path-glob.parity.test.ts` holds the two equal.
|
|
20
|
+
*/
|
|
21
|
+
/** Does this pattern use glob syntax? A pattern without any is the plain substring search, unchanged. */
|
|
22
|
+
export function isGlob(pattern) {
|
|
23
|
+
return /[*?[]/.test(pattern);
|
|
24
|
+
}
|
|
25
|
+
function toRegExp(glob) {
|
|
26
|
+
let re = "";
|
|
27
|
+
for (let i = 0; i < glob.length; i++) {
|
|
28
|
+
const c = glob[i];
|
|
29
|
+
if (c === "*") {
|
|
30
|
+
if (glob[i + 1] === "*") {
|
|
31
|
+
i++;
|
|
32
|
+
if (glob[i + 1] === "/") {
|
|
33
|
+
i++;
|
|
34
|
+
re += "(?:.*/)?";
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
re += ".*";
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
re += "[^/]*";
|
|
41
|
+
}
|
|
42
|
+
else if (c === "?")
|
|
43
|
+
re += "[^/]";
|
|
44
|
+
else if (c === "[") {
|
|
45
|
+
const end = glob.indexOf("]", i + 2);
|
|
46
|
+
if (end === -1) {
|
|
47
|
+
re += "\\[";
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let body = glob.slice(i + 1, end).replace(/[\\^\]]/g, "\\$&");
|
|
51
|
+
const negate = body.startsWith("!");
|
|
52
|
+
if (negate)
|
|
53
|
+
body = body.slice(1);
|
|
54
|
+
re += `(?!/)[${negate ? "^" : ""}${body}]`;
|
|
55
|
+
i = end;
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
re += c.replace(/[.+^${}()|\\\]/]/g, "\\$&");
|
|
59
|
+
}
|
|
60
|
+
return new RegExp(`^${re}$`, "i");
|
|
61
|
+
}
|
|
62
|
+
/** Does `path` (repo-relative, `/`-separated) match `glob`? */
|
|
63
|
+
export function globMatches(glob, path) {
|
|
64
|
+
const g = glob.trim().replace(/^\.?\/+/, "").replace(/\/+$/, "");
|
|
65
|
+
if (!g)
|
|
66
|
+
return false;
|
|
67
|
+
const re = toRegExp(g);
|
|
68
|
+
if (g.includes("/"))
|
|
69
|
+
return re.test(path);
|
|
70
|
+
return path.split("/").some((segment) => re.test(segment));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The longest run of literal text a match MUST contain — what an old runner's substring search is given,
|
|
74
|
+
* so the glob filter then runs over a superset that cannot have dropped a match. `""` when there is none
|
|
75
|
+
* (`*`, `**`, `???`): nothing literal to search by.
|
|
76
|
+
*/
|
|
77
|
+
export function globLiteral(glob) {
|
|
78
|
+
// Normalised exactly as `globMatches` normalises, or `./src/*.ts` would ask for "./src", which no path contains.
|
|
79
|
+
const pieces = glob.trim().replace(/^\.?\/+/, "").split(/\[[^\]]*\]|[*?]+/);
|
|
80
|
+
const longest = pieces.reduce((a, b) => (b.length > a.length ? b : a), "");
|
|
81
|
+
return longest.replace(/^\/+|\/+$/g, "");
|
|
82
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { RunnerInputError } from "../errors.js";
|
|
4
5
|
/**
|
|
5
6
|
* Inspect a workdir: does it exist, does it hold anything, is it a checkout. Never throws —
|
|
6
7
|
* every failure is a `false`, because this function exists to REPORT a broken path and one that
|
|
@@ -175,10 +176,11 @@ export function ensureRepo(dir, opts = {}) {
|
|
|
175
176
|
// The owner's own folder (a monorepo subfolder, a plain project) — run in it untouched.
|
|
176
177
|
if (opts.ownFolder)
|
|
177
178
|
return dir;
|
|
178
|
-
|
|
179
|
+
// A refusal about the owner's folder, answered as one (#1116) — never a runner crash.
|
|
180
|
+
throw new RunnerInputError(`Refusing to clone into non-empty directory "${dir}" (no .git found) — move it aside or point at an empty path.`, 409);
|
|
179
181
|
}
|
|
180
182
|
if (opts.ownFolder && checkWorkdir(dir).insideWorkTree) {
|
|
181
|
-
throw new
|
|
183
|
+
throw new RunnerInputError(`"${dir}" is an empty folder inside another git checkout — not cloning a second repository into it. Point this repo at the checkout itself, or remove the folder.`, 409);
|
|
182
184
|
}
|
|
183
185
|
rmSync(dir, { recursive: true, force: true });
|
|
184
186
|
}
|
|
@@ -314,7 +314,7 @@ export class CodingRuntime {
|
|
|
314
314
|
session.interrupt();
|
|
315
315
|
break;
|
|
316
316
|
default:
|
|
317
|
-
throw new
|
|
317
|
+
throw new RunnerInputError(`Unknown coding action: ${action.kind}`);
|
|
318
318
|
}
|
|
319
319
|
return this.snapshot(sessionId);
|
|
320
320
|
}
|
|
@@ -472,8 +472,9 @@ export class CodingRuntime {
|
|
|
472
472
|
}
|
|
473
473
|
require(sessionId) {
|
|
474
474
|
const session = this.sessions.get(sessionId);
|
|
475
|
+
// A caller naming a session this runner does not hold is a lookup, not a crash (#1116): 404, with the same sentence.
|
|
475
476
|
if (!session)
|
|
476
|
-
throw new
|
|
477
|
+
throw new RunnerInputError(`No coding session: ${sessionId}`, 404);
|
|
477
478
|
return session;
|
|
478
479
|
}
|
|
479
480
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { RunnerInputError } from "../errors.js";
|
|
4
5
|
import { capturePane, createSession, killSession, listSessionsDetailed, runCommand as tmuxRunCommand, sendKey as tmuxSendKey, sendText as tmuxSendText, sessionExists, tmuxExec } from "./tmux.js";
|
|
5
6
|
function shellPath() {
|
|
6
7
|
return process.env.SHELL || "/bin/zsh";
|
|
@@ -14,7 +15,7 @@ export function splitTerminalTarget(raw, fallback) {
|
|
|
14
15
|
if (m)
|
|
15
16
|
return { backend: m[1].toLowerCase(), id: m[2].trim() };
|
|
16
17
|
if (!fallback)
|
|
17
|
-
throw new
|
|
18
|
+
throw new RunnerInputError("A target must include a backend prefix like `tmux:main`, or pass `backend` separately.");
|
|
18
19
|
return { backend: fallback, id: value };
|
|
19
20
|
}
|
|
20
21
|
export function listTerminalTargets(backend = "all") {
|
|
@@ -43,7 +44,7 @@ export function captureTerminalTarget(target, opts = {}) {
|
|
|
43
44
|
switch (t.backend) {
|
|
44
45
|
case "tmux":
|
|
45
46
|
if (!sessionExists(t.id))
|
|
46
|
-
throw new
|
|
47
|
+
throw new RunnerInputError(`No tmux session "${t.id}".`, 404);
|
|
47
48
|
return capturePane(t.id, Math.min(Math.max(Number(opts.lines) || 200, 1), 2000));
|
|
48
49
|
case "kitty":
|
|
49
50
|
return kittyExec(["@", "get-text", "--match", `id:${t.id}`, "--extent", "all"], 5000);
|
|
@@ -227,11 +228,11 @@ function kittyForegroundCommand(id) {
|
|
|
227
228
|
export function runTerminalCommand(target, command, backend) {
|
|
228
229
|
const t = splitTerminalTarget(target, backend);
|
|
229
230
|
if (!command.trim())
|
|
230
|
-
throw new
|
|
231
|
+
throw new RunnerInputError("A `command` is required.");
|
|
231
232
|
switch (t.backend) {
|
|
232
233
|
case "tmux":
|
|
233
234
|
if (!sessionExists(t.id))
|
|
234
|
-
throw new
|
|
235
|
+
throw new RunnerInputError(`No tmux session "${t.id}".`, 404);
|
|
235
236
|
tmuxRunCommand(t.id, command);
|
|
236
237
|
return capturePane(t.id, 200);
|
|
237
238
|
case "kitty":
|
|
@@ -246,11 +247,11 @@ export function runTerminalCommand(target, command, backend) {
|
|
|
246
247
|
export function sendTerminalKeys(target, opts) {
|
|
247
248
|
const t = splitTerminalTarget(target, opts.backend);
|
|
248
249
|
if (opts.text == null && (!opts.keys || opts.keys.length === 0))
|
|
249
|
-
throw new
|
|
250
|
+
throw new RunnerInputError("Provide `text` and/or `keys` to send.");
|
|
250
251
|
switch (t.backend) {
|
|
251
252
|
case "tmux":
|
|
252
253
|
if (!sessionExists(t.id))
|
|
253
|
-
throw new
|
|
254
|
+
throw new RunnerInputError(`No tmux session "${t.id}".`, 404);
|
|
254
255
|
if (opts.text != null)
|
|
255
256
|
tmuxSendText(t.id, opts.text);
|
|
256
257
|
for (const key of opts.keys ?? [])
|
|
@@ -264,7 +265,7 @@ export function sendTerminalKeys(target, opts) {
|
|
|
264
265
|
return captureTerminalTarget(t.id, { backend: "kitty" });
|
|
265
266
|
case "iterm2":
|
|
266
267
|
if ((opts.keys ?? []).length > 0)
|
|
267
|
-
throw new
|
|
268
|
+
throw new RunnerInputError("iTerm2 generic key events are not supported yet; send text or run a command.");
|
|
268
269
|
itermSessionScript(t.id, `tell theSession to write text ${appleString(opts.text ?? "")}`);
|
|
269
270
|
return captureTerminalTarget(t.id, { backend: "iterm2" });
|
|
270
271
|
}
|
|
@@ -275,7 +276,7 @@ export function createTerminalTarget(opts) {
|
|
|
275
276
|
case "tmux": {
|
|
276
277
|
const name = String(opts.name || "").trim();
|
|
277
278
|
if (!name)
|
|
278
|
-
throw new
|
|
279
|
+
throw new RunnerInputError("A `name` is required for tmux.");
|
|
279
280
|
if (sessionExists(name))
|
|
280
281
|
return { backend: "tmux", id: name, name, existed: true, workDir };
|
|
281
282
|
createSession(name, workDir, opts.command);
|
|
@@ -242,7 +242,7 @@ export class LocalRunner {
|
|
|
242
242
|
async approveTask(id) {
|
|
243
243
|
const task = this.requireTask(id);
|
|
244
244
|
if (task.status !== "needs_approval") {
|
|
245
|
-
throw new
|
|
245
|
+
throw new RunnerInputError(`Task is not waiting for approval: ${task.status}`, 409);
|
|
246
246
|
}
|
|
247
247
|
task.status = "queued";
|
|
248
248
|
task.updatedAt = new Date().toISOString();
|
|
@@ -662,7 +662,7 @@ export class LocalRunner {
|
|
|
662
662
|
if (task.type === "browser.open") {
|
|
663
663
|
const url = String(task.input.url || "");
|
|
664
664
|
if (!url || !/^https?:\/\//.test(url)) {
|
|
665
|
-
throw new
|
|
665
|
+
throw new RunnerInputError("browser.open requires an http(s) url");
|
|
666
666
|
}
|
|
667
667
|
const context = await this.getBrowserContext();
|
|
668
668
|
const page = context.pages()[0] || (await context.newPage());
|
|
@@ -701,12 +701,12 @@ export class LocalRunner {
|
|
|
701
701
|
this.addTaskEvent(task, "browser.goto.completed", "Browser page loaded", output);
|
|
702
702
|
return output;
|
|
703
703
|
}
|
|
704
|
-
throw new
|
|
704
|
+
throw new RunnerInputError(`Unknown task type: ${task.type}`);
|
|
705
705
|
}
|
|
706
706
|
requireTask(id) {
|
|
707
707
|
const task = this.store.getTask(id);
|
|
708
708
|
if (!task)
|
|
709
|
-
throw new
|
|
709
|
+
throw new RunnerInputError(`Task not found: ${id}`, 404);
|
|
710
710
|
return task;
|
|
711
711
|
}
|
|
712
712
|
async getBrowserContext() {
|