@proagentstore/cli 0.4.13 → 0.4.15
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/headless.js +31 -11
- package/dist/browser-runner/coding/inspect.js +143 -0
- package/dist/browser-runner/coding/runtime.js +33 -2
- package/dist/browser-runner/coding/tmux.js +12 -4
- package/dist/browser-runner/runner.js +33 -2
- package/dist/browser-runner/server.js +29 -0
- package/dist/index.js +6 -2
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { handlerFor } from "./handlers.js";
|
|
4
5
|
export class HeadlessSession {
|
|
5
6
|
config;
|
|
6
7
|
sessionName;
|
|
@@ -28,9 +29,15 @@ export class HeadlessSession {
|
|
|
28
29
|
// Claude is the structured engine; everything else is a raw CLI.
|
|
29
30
|
this.mode = config.clientType === "claude" ? "stream-json" : "raw";
|
|
30
31
|
const { bin, args } = parseCommand(config.command);
|
|
32
|
+
// When no explicit command is configured, fall back to THIS engine's default
|
|
33
|
+
// command (codex/gemini/grok/…) — not a hard-coded "claude", which would drive a
|
|
34
|
+
// non-Claude session with the wrong CLI (and mode is already "raw" for it).
|
|
35
|
+
const fallback = parseCommand(handlerFor(config.clientType).cliCommand);
|
|
31
36
|
// A test/override bin wins for the binary only; the command's args are kept.
|
|
32
|
-
this.cmdBin = config.bin ?? (bin || "claude");
|
|
33
|
-
|
|
37
|
+
this.cmdBin = config.bin ?? (bin || fallback.bin || "claude");
|
|
38
|
+
// Use the configured command's args when a command was given (bin set), else the
|
|
39
|
+
// engine default's args.
|
|
40
|
+
this.cmdArgs = bin ? args : fallback.args;
|
|
34
41
|
this.binName = (this.cmdBin.split("/").pop() || this.cmdBin) || "cli";
|
|
35
42
|
}
|
|
36
43
|
/** True while the agent process is running. NOTE: do NOT use `proc.killed` — Node sets
|
|
@@ -81,31 +88,39 @@ export class HeadlessSession {
|
|
|
81
88
|
// (e.g. --model) without letting them clobber or orphan-value our flags. raw:
|
|
82
89
|
// run exactly what the user configured and capture stdout.
|
|
83
90
|
const args = this.mode === "stream-json" ? buildClaudeArgs(this.cmdArgs, this.claudeSessionId) : [...this.cmdArgs];
|
|
84
|
-
|
|
91
|
+
const proc = spawn(this.cmdBin, args, {
|
|
85
92
|
cwd: this.config.workDir,
|
|
86
93
|
env: { ...process.env, ...this.config.env },
|
|
87
94
|
stdio: ["pipe", "pipe", "pipe"],
|
|
88
95
|
});
|
|
96
|
+
this.proc = proc;
|
|
89
97
|
this.run = "idle";
|
|
90
98
|
this.lastOutputAt = Date.now();
|
|
91
99
|
// MUST handle 'error' — without a listener, a spawn failure (e.g. the binary
|
|
92
100
|
// not on PATH) is thrown as an uncaught exception and crashes the runner.
|
|
93
|
-
|
|
101
|
+
proc.on("error", (err) => {
|
|
102
|
+
if (this.proc !== proc)
|
|
103
|
+
return; // a newer process replaced this one — ignore the stale event
|
|
94
104
|
this.run = "idle";
|
|
95
105
|
this.push(`[cannot run \`${this.cmdBin}\`: ${err.message} — is ${this.binName} installed and on your PATH?]`);
|
|
96
106
|
this.proc = null;
|
|
97
107
|
});
|
|
98
108
|
// Swallow EPIPE when writing to a process that just exited (one-shot turn).
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
proc.stdin?.on("error", () => { });
|
|
110
|
+
proc.stdout?.on("data", (d) => this.onStdout(d.toString("utf8")));
|
|
111
|
+
proc.stderr?.on("data", (d) => {
|
|
102
112
|
this.lastOutputAt = Date.now();
|
|
103
113
|
this.sawOutputSinceInput = true;
|
|
104
114
|
const text = d.toString("utf8").trim();
|
|
105
115
|
if (text)
|
|
106
116
|
this.push(`[${this.binName}] ${stripAnsi(text)}`);
|
|
107
117
|
});
|
|
108
|
-
|
|
118
|
+
proc.on("exit", (code) => {
|
|
119
|
+
// A stop() + re-start() on the SAME session object can leave this old process's
|
|
120
|
+
// async exit to fire AFTER the fresh start — capturing `proc` and bailing when it
|
|
121
|
+
// no longer matches stops the stale handler from resetting the new turn to idle.
|
|
122
|
+
if (this.proc !== proc)
|
|
123
|
+
return;
|
|
109
124
|
this.run = "idle";
|
|
110
125
|
if (code && code !== 0)
|
|
111
126
|
this.push(`[${this.binName} exited with code ${code}]`);
|
|
@@ -184,9 +199,14 @@ export class HeadlessSession {
|
|
|
184
199
|
else
|
|
185
200
|
this.pushRaw(line); // raw engine — the line IS the terminal output
|
|
186
201
|
}
|
|
187
|
-
// A TUI/raw engine may render without newlines;
|
|
188
|
-
//
|
|
189
|
-
|
|
202
|
+
// A TUI/raw engine may render without newlines; surface the partial output and cap
|
|
203
|
+
// growth at 16KB. Claude's stream-json, though, emits ONE JSON object per line and a
|
|
204
|
+
// single event (a large tool_result / the final `result`) can legitimately exceed
|
|
205
|
+
// 16KB — truncating mid-line there corrupts the event, and losing a `result` line
|
|
206
|
+
// wedges the turn "thinking" forever (stream-json has no idle backstop). So give the
|
|
207
|
+
// structured path a far larger ceiling; only a pathologically huge line resets it.
|
|
208
|
+
const cap = this.mode === "raw" ? 16 * 1024 : 4 * 1024 * 1024;
|
|
209
|
+
if (this.buf.length > cap) {
|
|
190
210
|
if (this.mode === "raw")
|
|
191
211
|
this.pushRaw(this.buf);
|
|
192
212
|
this.buf = "";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { relative, resolve, sep } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Read-only code inspection for the coding runtime — the "eyes" the Co-pilot/Chat use
|
|
6
|
+
* to GROUND their answers in the real repo (read a file, `git diff`, list the tree)
|
|
7
|
+
* WITHOUT driving the live CLI. All access is confined to the session's workDir.
|
|
8
|
+
*
|
|
9
|
+
* Two pure, separately-tested primitives carry the safety:
|
|
10
|
+
* - resolveInside(): rejects any path escaping the repo root (../, absolute, sibling
|
|
11
|
+
* prefix, symlink escape).
|
|
12
|
+
* - gitArgv(): maps a fixed command enum to a fixed argv — no user string ever
|
|
13
|
+
* becomes a git token except a resolveInside-validated path after a literal `--`.
|
|
14
|
+
*/
|
|
15
|
+
/** Resolve `rel` under `root`, refusing anything that escapes it. Pure (no fs) EXCEPT the
|
|
16
|
+
* optional symlink check, which is what defends against a symlink inside the repo pointing
|
|
17
|
+
* at e.g. ~/.ssh. Throws on any escape. */
|
|
18
|
+
export function resolveInside(root, rel, opts = {}) {
|
|
19
|
+
const rootAbs = resolve(root);
|
|
20
|
+
const abs = resolve(rootAbs, rel);
|
|
21
|
+
// `resolve` collapses `..`, so a traversal or absolute escape lands outside rootAbs.
|
|
22
|
+
// The explicit `+ sep` blocks a sibling-prefix attack (/repo vs /repo-secrets).
|
|
23
|
+
if (abs !== rootAbs && !abs.startsWith(rootAbs + sep)) {
|
|
24
|
+
throw new InspectError(`path escapes the repo: ${rel}`);
|
|
25
|
+
}
|
|
26
|
+
if (opts.checkSymlink && existsSync(abs)) {
|
|
27
|
+
// A symlink inside the repo could still point outside it — resolve the real path and
|
|
28
|
+
// re-check the same invariant.
|
|
29
|
+
const real = realpathSync(abs);
|
|
30
|
+
const realRoot = realpathSync(rootAbs);
|
|
31
|
+
if (real !== realRoot && !real.startsWith(realRoot + sep)) {
|
|
32
|
+
throw new InspectError(`path resolves (via symlink) outside the repo: ${rel}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return abs;
|
|
36
|
+
}
|
|
37
|
+
/** Map a whitelisted command enum to a fixed git argv. `path` (already validated by the
|
|
38
|
+
* caller via resolveInside) is only ever appended after a literal `--` separator. */
|
|
39
|
+
export function gitArgv(cmd, opts = {}) {
|
|
40
|
+
const clampN = Math.max(1, Math.min(200, Math.floor(opts.n ?? 20)));
|
|
41
|
+
switch (cmd) {
|
|
42
|
+
case "status":
|
|
43
|
+
return ["status", "--short"];
|
|
44
|
+
case "diff":
|
|
45
|
+
return opts.relPath ? ["diff", "--", opts.relPath] : ["diff"];
|
|
46
|
+
case "diff-stat":
|
|
47
|
+
return ["diff", "--stat"];
|
|
48
|
+
case "log":
|
|
49
|
+
return ["log", "--oneline", "-n", String(clampN)];
|
|
50
|
+
case "ls-files":
|
|
51
|
+
return ["ls-files"];
|
|
52
|
+
default:
|
|
53
|
+
throw new InspectError(`unsupported git command: ${cmd}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export class InspectError extends Error {
|
|
57
|
+
constructor(msg) {
|
|
58
|
+
super(msg);
|
|
59
|
+
this.name = "InspectError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const DEFAULT_MAX_FILE_BYTES = 64 * 1024;
|
|
63
|
+
const HARD_MAX_FILE_BYTES = 128 * 1024;
|
|
64
|
+
/** Read a text file inside the repo. Rejects traversal, oversize, and binary files. */
|
|
65
|
+
export function readRepoFile(workDir, relPath, maxBytes) {
|
|
66
|
+
const abs = resolveInside(workDir, relPath, { checkSymlink: true });
|
|
67
|
+
const st = statSync(abs);
|
|
68
|
+
if (!st.isFile())
|
|
69
|
+
throw new InspectError(`not a regular file: ${relPath}`);
|
|
70
|
+
const cap = Math.min(maxBytes ?? DEFAULT_MAX_FILE_BYTES, HARD_MAX_FILE_BYTES);
|
|
71
|
+
const buf = readFileSync(abs);
|
|
72
|
+
// Binary sniff: a NUL byte in the first 8KB → don't feed bytes to the model.
|
|
73
|
+
const head = buf.subarray(0, 8192);
|
|
74
|
+
if (head.includes(0))
|
|
75
|
+
return { path: relPath, size: st.size, truncated: false, binary: true };
|
|
76
|
+
const truncated = buf.length > cap;
|
|
77
|
+
return { path: relPath, size: st.size, truncated, content: buf.subarray(0, cap).toString("utf-8") };
|
|
78
|
+
}
|
|
79
|
+
/** Run a whitelisted read-only git command in the repo. Never uses a shell. */
|
|
80
|
+
export function runRepoGit(workDir, cmd, opts = {}) {
|
|
81
|
+
if (!existsSync(resolve(workDir, ".git")))
|
|
82
|
+
throw new InspectError("not a git repo");
|
|
83
|
+
const relPath = opts.path ? relative(workDir, resolveInside(workDir, opts.path)) : undefined;
|
|
84
|
+
const argv = gitArgv(cmd, { relPath, n: opts.n });
|
|
85
|
+
let out = "";
|
|
86
|
+
try {
|
|
87
|
+
out = execFileSync("git", argv, { cwd: workDir, encoding: "utf-8", timeout: 10_000, maxBuffer: 4 * 1024 * 1024 });
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
// git exits non-zero for benign cases (e.g. `diff` on nothing) — surface stdout if present.
|
|
91
|
+
const err = e;
|
|
92
|
+
out = err.stdout ?? "";
|
|
93
|
+
if (!out)
|
|
94
|
+
throw new InspectError(err.message || `git ${cmd} failed`);
|
|
95
|
+
}
|
|
96
|
+
const cap = opts.maxBytes ?? 64 * 1024;
|
|
97
|
+
const truncated = out.length > cap;
|
|
98
|
+
return { cmd, output: truncated ? out.slice(0, cap) : out, truncated };
|
|
99
|
+
}
|
|
100
|
+
const IGNORE_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".turbo", "coverage", ".wrangler"]);
|
|
101
|
+
/** Bounded recursive file tree (names/type/size only — no contents). */
|
|
102
|
+
export function repoTree(workDir, relPath = ".", maxDepth = 3, maxEntries = 500) {
|
|
103
|
+
const start = resolveInside(workDir, relPath, { checkSymlink: true });
|
|
104
|
+
const depthCap = Math.max(1, Math.min(4, maxDepth));
|
|
105
|
+
const entryCap = Math.max(1, Math.min(1000, maxEntries));
|
|
106
|
+
const entries = [];
|
|
107
|
+
const queue = [{ dir: start, depth: 0 }];
|
|
108
|
+
let truncated = false;
|
|
109
|
+
while (queue.length) {
|
|
110
|
+
const { dir, depth } = queue.shift();
|
|
111
|
+
let items;
|
|
112
|
+
try {
|
|
113
|
+
items = readdirSync(dir, { withFileTypes: true });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
for (const it of items) {
|
|
119
|
+
if (it.name.startsWith(".") || IGNORE_DIRS.has(it.name))
|
|
120
|
+
continue;
|
|
121
|
+
if (entries.length >= entryCap) {
|
|
122
|
+
truncated = true;
|
|
123
|
+
return { root: relPath, entries, truncated };
|
|
124
|
+
}
|
|
125
|
+
const abs = resolve(dir, it.name);
|
|
126
|
+
const rel = relative(workDir, abs);
|
|
127
|
+
if (it.isDirectory()) {
|
|
128
|
+
entries.push({ path: rel, type: "dir" });
|
|
129
|
+
if (depth + 1 < depthCap)
|
|
130
|
+
queue.push({ dir: abs, depth: depth + 1 });
|
|
131
|
+
}
|
|
132
|
+
else if (it.isFile()) {
|
|
133
|
+
let size;
|
|
134
|
+
try {
|
|
135
|
+
size = statSync(abs).size;
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
entries.push({ path: rel, type: "file", size });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { root: relPath, entries, truncated };
|
|
143
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { defaultStatePath, HeadlessSession } from "./headless.js";
|
|
4
|
+
import { InspectError, readRepoFile, repoTree, runRepoGit } from "./inspect.js";
|
|
4
5
|
import { ensureRepo, sanitizeSessionName } from "./tmux.js";
|
|
5
6
|
/** Hard cap on a pane returned to the brain/console (matches the worker MAX_PANE_CHARS). */
|
|
6
7
|
const MAX_PANE = 64 * 1024;
|
|
@@ -18,9 +19,39 @@ export class CodingRuntime {
|
|
|
18
19
|
constructor(reposBaseDir = join(homedir(), ".config", "proagentstore", "repos")) {
|
|
19
20
|
this.reposBaseDir = reposBaseDir;
|
|
20
21
|
}
|
|
21
|
-
/** Capabilities advertised to PAGS at registration.
|
|
22
|
+
/** Capabilities advertised to PAGS at registration. `coding.inspect` signals the
|
|
23
|
+
* read-only code-inspection endpoints exist, so the cloud offers the grounding tools
|
|
24
|
+
* (older runners omit it → the cloud degrades to terminal-only). */
|
|
22
25
|
static capabilities() {
|
|
23
|
-
return ["coding.sessions", "coding.stream", "human.takeover"];
|
|
26
|
+
return ["coding.sessions", "coding.stream", "human.takeover", "coding.inspect"];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the workDir for a read-only inspection. Prefer the tracked session's real
|
|
30
|
+
* workDir (authoritative even for managed clone dirs); fall back to an explicit path
|
|
31
|
+
* (the cloud passes the D1 `repo.workdir` when the session map is empty after a runner
|
|
32
|
+
* restart). Expands a leading `~` the same way start() does.
|
|
33
|
+
*/
|
|
34
|
+
resolveWorkDir(input) {
|
|
35
|
+
if (input.sessionId) {
|
|
36
|
+
const s = this.sessions.get(input.sessionId);
|
|
37
|
+
if (s)
|
|
38
|
+
return s.config.workDir;
|
|
39
|
+
}
|
|
40
|
+
if (input.workDir)
|
|
41
|
+
return resolve(input.workDir.replace(/^~(?=$|\/)/, homedir()));
|
|
42
|
+
throw new InspectError("no session or workDir to inspect");
|
|
43
|
+
}
|
|
44
|
+
/** Read one file inside the session's repo (traversal-guarded, size-capped). */
|
|
45
|
+
readFile(input) {
|
|
46
|
+
return readRepoFile(this.resolveWorkDir(input), input.path, input.maxBytes);
|
|
47
|
+
}
|
|
48
|
+
/** Run a whitelisted read-only git command in the session's repo. */
|
|
49
|
+
git(input) {
|
|
50
|
+
return runRepoGit(this.resolveWorkDir(input), input.cmd, { path: input.path, n: input.n });
|
|
51
|
+
}
|
|
52
|
+
/** Bounded recursive file tree of the session's repo (names/type/size only). */
|
|
53
|
+
tree(input) {
|
|
54
|
+
return repoTree(this.resolveWorkDir(input), input.path, input.maxDepth, input.maxEntries);
|
|
24
55
|
}
|
|
25
56
|
static taskTypes() {
|
|
26
57
|
return ["coding.session"];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
/**
|
|
5
5
|
* Low-level tmux primitives for the coding runtime.
|
|
@@ -108,10 +108,18 @@ export function ensureRepo(dir, opts = {}) {
|
|
|
108
108
|
mkdirSync(dir, { recursive: true });
|
|
109
109
|
return dir;
|
|
110
110
|
}
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
111
|
+
// The dir exists but has no `.git`. It could be a half-cloned/empty managed dir
|
|
112
|
+
// (safe to clear) OR a real user directory the caller passed as an explicit workDir
|
|
113
|
+
// (deleting it = data loss). NEVER recursively delete a non-empty non-git dir — refuse
|
|
114
|
+
// instead, so a mis-wired workDir+cloneUrl can't nuke a user's files. An empty dir is
|
|
115
|
+
// fine to remove (git clone needs an empty/absent target).
|
|
116
|
+
if (existsSync(dir)) {
|
|
117
|
+
const entries = readdirSync(dir);
|
|
118
|
+
if (entries.length > 0) {
|
|
119
|
+
throw new Error(`Refusing to clone into non-empty directory "${dir}" (no .git found) — move it aside or point at an empty path.`);
|
|
120
|
+
}
|
|
114
121
|
rmSync(dir, { recursive: true, force: true });
|
|
122
|
+
}
|
|
115
123
|
let url = opts.cloneUrl;
|
|
116
124
|
if (opts.token && /^https:\/\//.test(url)) {
|
|
117
125
|
url = url.replace(/^https:\/\//, `https://x-access-token:${opts.token}@`);
|
|
@@ -758,6 +758,28 @@ export class LocalRunner {
|
|
|
758
758
|
return `⚠ "${name}" REJECTED: now reads "${shown}"${parsed.msg ? ` — ${parsed.msg.slice(0, 80)}` : ""}`;
|
|
759
759
|
return `"${name}" now reads "${shown}"`;
|
|
760
760
|
}
|
|
761
|
+
/** Read a checkbox/radio's checked state via the standard evaluate tool so `check`
|
|
762
|
+
* can be made idempotent (a raw click would toggle). Returns true/false, or null when
|
|
763
|
+
* the state can't be determined (then we fall back to clicking). Never throws. */
|
|
764
|
+
async isChecked(mcp, ref, label) {
|
|
765
|
+
const fn = "el => { if (el.checked !== undefined && el.checked !== null) return !!el.checked; " +
|
|
766
|
+
"const a = el.getAttribute('aria-checked'); return a === 'true' ? true : a === 'false' ? false : null; }";
|
|
767
|
+
const res = await mcp.callTool("browser_evaluate", { element: label, target: ref, function: fn }).catch(() => null);
|
|
768
|
+
if (!res || res.isError)
|
|
769
|
+
return null;
|
|
770
|
+
const txt = mcp.textOf(res);
|
|
771
|
+
const i = txt.indexOf("### Result");
|
|
772
|
+
if (i < 0)
|
|
773
|
+
return null;
|
|
774
|
+
const after = txt.slice(i + "### Result".length).trim();
|
|
775
|
+
const end = after.indexOf("\n###");
|
|
776
|
+
const block = (end >= 0 ? after.slice(0, end) : after).trim().replace(/^["']|["']$/g, "");
|
|
777
|
+
if (/^true$/i.test(block))
|
|
778
|
+
return true;
|
|
779
|
+
if (/^false$/i.test(block))
|
|
780
|
+
return false;
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
761
783
|
/** The snapshot ref the brain must target the element by (standard-tool `target`). */
|
|
762
784
|
refOf(action) {
|
|
763
785
|
const ref = (action.ref || "").trim();
|
|
@@ -773,10 +795,19 @@ export class LocalRunner {
|
|
|
773
795
|
return mcp.callTool("browser_navigate", { url: a.url });
|
|
774
796
|
case "type":
|
|
775
797
|
return mcp.callTool("browser_type", { element: label, target: this.refOf(a), text: a.text ?? "" });
|
|
776
|
-
// A checkbox/radio is just a click at the standard-tool level.
|
|
777
798
|
case "click":
|
|
778
|
-
case "check":
|
|
779
799
|
return mcp.callTool("browser_click", { element: label, target: this.refOf(a) });
|
|
800
|
+
case "check": {
|
|
801
|
+
const ref = this.refOf(a);
|
|
802
|
+
// A checkbox/radio click is a TOGGLE at the standard-tool level — clicking an
|
|
803
|
+
// ALREADY-checked control unchecks it, silently reversing a pre-ticked consent
|
|
804
|
+
// or a default-selected radio before submit. Make `check` idempotent
|
|
805
|
+
// (ensure-checked): read the current state and only click when it isn't set.
|
|
806
|
+
if ((await this.isChecked(mcp, ref, label)) === true) {
|
|
807
|
+
return { isError: false, content: [{ type: "text", text: `"${label}" already checked` }] };
|
|
808
|
+
}
|
|
809
|
+
return mcp.callTool("browser_click", { element: label, target: ref });
|
|
810
|
+
}
|
|
780
811
|
case "upload": {
|
|
781
812
|
if (!this.applyResumePath)
|
|
782
813
|
throw new RunnerInputError("no résumé file available to upload");
|
|
@@ -222,6 +222,35 @@ async function route(runner, req, res) {
|
|
|
222
222
|
}
|
|
223
223
|
return json(res, 200, { killed, sessions: targets });
|
|
224
224
|
}
|
|
225
|
+
// ── Read-only code inspection (the Co-pilot/Chat's "eyes" — no CLI driving) ──
|
|
226
|
+
// Confined to the session's workDir by inspect.ts; errors surface as 400.
|
|
227
|
+
if (req.method === "POST" && path === "/coding/read-file") {
|
|
228
|
+
const b = await readJson(req);
|
|
229
|
+
try {
|
|
230
|
+
return json(res, 200, runner.coding.readFile(b));
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return json(res, 400, { error: e instanceof Error ? e.message : String(e) });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (req.method === "POST" && path === "/coding/git") {
|
|
237
|
+
const b = await readJson(req);
|
|
238
|
+
try {
|
|
239
|
+
return json(res, 200, runner.coding.git(b));
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
return json(res, 400, { error: e instanceof Error ? e.message : String(e) });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (req.method === "POST" && path === "/coding/tree") {
|
|
246
|
+
const b = await readJson(req);
|
|
247
|
+
try {
|
|
248
|
+
return json(res, 200, runner.coding.tree(b));
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
return json(res, 400, { error: e instanceof Error ? e.message : String(e) });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
225
254
|
if (req.method === "POST" && path === "/coding/event") {
|
|
226
255
|
// Brain progress events — recorded by PAGS, ignored locally. Accept + ack.
|
|
227
256
|
return json(res, 200, { ok: true });
|
package/dist/index.js
CHANGED
|
@@ -1365,9 +1365,13 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1365
1365
|
if (key === "r") {
|
|
1366
1366
|
if (childDead) {
|
|
1367
1367
|
writeLine(" Restarting runner...");
|
|
1368
|
-
const {
|
|
1368
|
+
const { execFileSync: execFileSync2 } = await import("child_process");
|
|
1369
|
+
const restartArgs = [process.argv[1], "up"];
|
|
1370
|
+
if (opts.headless) restartArgs.push("--headless");
|
|
1371
|
+
if (opts.force) restartArgs.push("--force");
|
|
1372
|
+
if (opts.instance) restartArgs.push("--instance", opts.instance);
|
|
1369
1373
|
try {
|
|
1370
|
-
|
|
1374
|
+
execFileSync2(process.execPath, restartArgs, {
|
|
1371
1375
|
stdio: "inherit",
|
|
1372
1376
|
env: process.env
|
|
1373
1377
|
});
|