@proagentstore/cli 0.1.27 → 0.1.29
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.
|
@@ -122,10 +122,15 @@ export class CodingRuntime {
|
|
|
122
122
|
}
|
|
123
123
|
return { ok: true };
|
|
124
124
|
}
|
|
125
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Polled by the brain workflow: has the human resolved the handoff? Defaults to
|
|
127
|
+
* NOT resolved when there's no live entry — if the runner restarted mid-handoff
|
|
128
|
+
* we must NOT auto-resume (the brain would proceed without the value it was told
|
|
129
|
+
* to wait for); the workflow's poll loop times out safely instead.
|
|
130
|
+
*/
|
|
126
131
|
takeoverStatus(sessionId) {
|
|
127
132
|
const t = this.takeovers.get(sessionId);
|
|
128
|
-
return { resolved: t?.resolved ??
|
|
133
|
+
return { resolved: t?.resolved ?? false, value: t?.value };
|
|
129
134
|
}
|
|
130
135
|
endTakeover(sessionId) {
|
|
131
136
|
this.takeovers.delete(sessionId);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
/**
|
|
4
5
|
* Low-level tmux primitives for the coding runtime.
|
|
5
6
|
*
|
|
@@ -98,13 +99,19 @@ export function sanitizeSessionName(label) {
|
|
|
98
99
|
* can surface it (a session can't start without its repo).
|
|
99
100
|
*/
|
|
100
101
|
export function ensureRepo(dir, opts = {}) {
|
|
101
|
-
|
|
102
|
+
// A real checkout (has .git) is reused as-is.
|
|
103
|
+
if (existsSync(join(dir, ".git")))
|
|
102
104
|
return dir;
|
|
103
105
|
if (!opts.cloneUrl) {
|
|
104
106
|
// No source to clone from — make the directory so tmux can cd into it.
|
|
105
|
-
|
|
107
|
+
if (!existsSync(dir))
|
|
108
|
+
mkdirSync(dir, { recursive: true });
|
|
106
109
|
return dir;
|
|
107
110
|
}
|
|
111
|
+
// A stale/empty/half-cloned dir (exists but no .git) would make `git clone`
|
|
112
|
+
// fail on a non-empty target — clear it first so the clone is clean.
|
|
113
|
+
if (existsSync(dir))
|
|
114
|
+
rmSync(dir, { recursive: true, force: true });
|
|
108
115
|
let url = opts.cloneUrl;
|
|
109
116
|
if (opts.token && /^https:\/\//.test(url)) {
|
|
110
117
|
url = url.replace(/^https:\/\//, `https://x-access-token:${opts.token}@`);
|
|
@@ -765,7 +765,10 @@ export class LocalRunner {
|
|
|
765
765
|
const task = this.store.getTask(taskId);
|
|
766
766
|
if (task) {
|
|
767
767
|
const success = outcome === "submitted" || outcome === "ready" || outcome === "expired";
|
|
768
|
-
|
|
768
|
+
// 'blocked' = the agent stopped and needs the USER (email verification, can't
|
|
769
|
+
// proceed truthfully) — it's needs-attention, not a failure, so keep it a
|
|
770
|
+
// distinct status (the console shows it as an active ticket, not buried).
|
|
771
|
+
task.status = outcome === "blocked" ? "blocked" : success ? "completed" : "failed";
|
|
769
772
|
task.output = { outcome, detail };
|
|
770
773
|
if (!success)
|
|
771
774
|
task.error = detail || outcome;
|
package/dist/index.js
CHANGED
|
@@ -912,17 +912,17 @@ function createRunnerCommand() {
|
|
|
912
912
|
command.command("start").description("Start the local FAGS browser runtime in the foreground").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "49171").option("--data-dir <path>", "Runner data directory").option("--token <token>", "Require this bearer token").option("--instance-id <id>", "Bind runner requests to a PAGS instance id").option("--headless", "Run Playwright headless").action(async (opts) => {
|
|
913
913
|
await startRunnerForeground(opts);
|
|
914
914
|
});
|
|
915
|
-
command.command("connect <
|
|
915
|
+
command.command("connect <instanceIds...>").description("Start ONE local runtime, open a Cloudflare quick tunnel, and register it for every given PAGS instance").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "49171").option("--data-dir <path>", "Runner data directory").option("--token <token>", "Runner bearer token. Defaults to PAGS_RUNNER_TOKEN or a generated token").option("--headless", "Run Playwright headless").option("--api-base <url>", "PAGS API base URL").option("--pags-token <token>", "PAGS session token. Defaults to PAGS_TOKEN").option("--cloudflared <path>", "cloudflared executable").option("--runner-version <version>", "Runner version").option("--skip-probe", "Skip PAGS FAGS-runtime probe after registration").action(async (instanceIds, opts) => {
|
|
916
916
|
const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`;
|
|
917
917
|
const host = clean(opts.host) || "127.0.0.1";
|
|
918
918
|
const port = clean(opts.port) || String(await findFreePort2(49171));
|
|
919
919
|
const localUrl = `http://${host}:${port}`;
|
|
920
|
+
const primary = instanceIds[0];
|
|
920
921
|
const runnerOpts = {
|
|
921
922
|
...opts,
|
|
922
923
|
host,
|
|
923
924
|
port,
|
|
924
|
-
token: runnerToken
|
|
925
|
-
instanceId
|
|
925
|
+
token: runnerToken
|
|
926
926
|
};
|
|
927
927
|
const spec = runnerSpawnSpec(runnerOpts);
|
|
928
928
|
const runner = spawn2(spec.command, spec.args, {
|
|
@@ -949,7 +949,7 @@ function createRunnerCommand() {
|
|
|
949
949
|
process.exit(0);
|
|
950
950
|
});
|
|
951
951
|
try {
|
|
952
|
-
await waitForLocalRunner({ url: localUrl, token: runnerToken, instanceId });
|
|
952
|
+
await waitForLocalRunner({ url: localUrl, token: runnerToken, instanceId: primary });
|
|
953
953
|
writeLine(`Local FAGS runtime healthy at ${localUrl}`);
|
|
954
954
|
const cloudflaredPath = await resolveCloudflaredCommand(opts.cloudflared);
|
|
955
955
|
const openTunnel = () => new Promise((resolveTunnel, reject) => {
|
|
@@ -969,14 +969,17 @@ function createRunnerCommand() {
|
|
|
969
969
|
setTimeout(() => reject(new Error("timed out waiting for cloudflared tunnel URL")), 45e3).unref();
|
|
970
970
|
});
|
|
971
971
|
const registerRuntime = async (url) => {
|
|
972
|
-
const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId });
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
972
|
+
const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: primary });
|
|
973
|
+
const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
|
|
974
|
+
for (const id of instanceIds) {
|
|
975
|
+
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
|
|
976
|
+
endpointUrl: url,
|
|
977
|
+
token: runnerToken,
|
|
978
|
+
placement: "local",
|
|
979
|
+
capabilities: caps,
|
|
980
|
+
runnerVersion: clean(opts.runnerVersion) || ""
|
|
981
|
+
});
|
|
982
|
+
}
|
|
980
983
|
};
|
|
981
984
|
const first = await openTunnel();
|
|
982
985
|
tunnel = first.proc;
|
|
@@ -987,17 +990,16 @@ function createRunnerCommand() {
|
|
|
987
990
|
writeLine("Runtime registered with PAGS \u2713");
|
|
988
991
|
writeLine("");
|
|
989
992
|
writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
990
|
-
writeLine(" \u2705 CONNECTED \u2014
|
|
991
|
-
writeLine(`
|
|
993
|
+
writeLine(" \u2705 CONNECTED \u2014 runner is live");
|
|
994
|
+
writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"} served by this runner`);
|
|
992
995
|
writeLine(` Tunnel: ${tunnelUrl}`);
|
|
993
996
|
writeLine(" Auto-reconnect is on \u2014 keep this terminal open. Ctrl+C to disconnect.");
|
|
994
997
|
writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
995
998
|
let stopped = false;
|
|
996
|
-
const heartbeatPath = `/v1/instances/${apiPathSegment(instanceId)}/runtime/heartbeat`;
|
|
997
999
|
const probeTunnel = async (url) => {
|
|
998
1000
|
try {
|
|
999
1001
|
const res = await fetch(`${url.replace(/\/$/, "")}/health`, {
|
|
1000
|
-
headers: { Authorization: `Bearer ${runnerToken}`, "X-PAGS-Instance-Id":
|
|
1002
|
+
headers: { Authorization: `Bearer ${runnerToken}`, "X-PAGS-Instance-Id": primary },
|
|
1001
1003
|
signal: AbortSignal.timeout(8e3)
|
|
1002
1004
|
});
|
|
1003
1005
|
return res.ok;
|
|
@@ -1005,12 +1007,20 @@ function createRunnerCommand() {
|
|
|
1005
1007
|
return false;
|
|
1006
1008
|
}
|
|
1007
1009
|
};
|
|
1010
|
+
const heartbeatAll = async () => {
|
|
1011
|
+
let allOk = true;
|
|
1012
|
+
for (const id of instanceIds) {
|
|
1013
|
+
const ok = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {}).then(() => true).catch(() => false);
|
|
1014
|
+
if (!ok) allOk = false;
|
|
1015
|
+
}
|
|
1016
|
+
return allOk;
|
|
1017
|
+
};
|
|
1008
1018
|
void (async () => {
|
|
1009
1019
|
while (!stopped) {
|
|
1010
1020
|
await new Promise((r) => setTimeout(r, 3e4));
|
|
1011
1021
|
if (stopped) break;
|
|
1012
1022
|
if (await probeTunnel(tunnelUrl)) {
|
|
1013
|
-
const beat = await
|
|
1023
|
+
const beat = await heartbeatAll();
|
|
1014
1024
|
if (!beat) {
|
|
1015
1025
|
await registerRuntime(tunnelUrl).then(() => writeLine("\u2705 Re-registered with PAGS")).catch(() => void 0);
|
|
1016
1026
|
}
|
|
@@ -1317,13 +1327,12 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1317
1327
|
for (const inst of state.instances) {
|
|
1318
1328
|
writeLine(` ${inst.name} (${inst.id.slice(0, 8)}...)`);
|
|
1319
1329
|
}
|
|
1320
|
-
|
|
1321
|
-
state.activeInstance
|
|
1322
|
-
printStep(`Connecting: ${state.activeInstance}`, "wait");
|
|
1330
|
+
state.activeInstance = instances.length === 1 ? instances[0].name || instances[0].slug || instances[0].id.slice(0, 8) : `${instances.length} agents`;
|
|
1331
|
+
printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
|
|
1323
1332
|
await stopRunnerProcesses();
|
|
1324
1333
|
const { spawn: spawn3 } = await import("child_process");
|
|
1325
1334
|
const cliPath = process.argv[1];
|
|
1326
|
-
const args = [cliPath, "runner", "connect",
|
|
1335
|
+
const args = [cliPath, "runner", "connect", ...instances.map((i) => i.id)];
|
|
1327
1336
|
if (opts.headless) args.push("--headless");
|
|
1328
1337
|
const child = spawn3(process.execPath, args, {
|
|
1329
1338
|
stdio: ["ignore", "pipe", "pipe"],
|