@proagentstore/cli 0.2.4 → 0.2.6
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.
|
@@ -98,6 +98,20 @@ export class CodingRuntime {
|
|
|
98
98
|
tmuxSession: s.sessionName,
|
|
99
99
|
}));
|
|
100
100
|
}
|
|
101
|
+
/** Rich diagnostics for every tracked session — the console's transparency view. */
|
|
102
|
+
diagnostics() {
|
|
103
|
+
return [...this.sessions.entries()].map(([sessionId, s]) => ({
|
|
104
|
+
sessionId,
|
|
105
|
+
tmuxSession: s.sessionName,
|
|
106
|
+
alive: s.alive,
|
|
107
|
+
runState: s.alive ? s.runState() : "idle",
|
|
108
|
+
ready: s.alive ? s.ready : false,
|
|
109
|
+
paneLines: s.alive ? (s.snapshot().split("\n").length) : 0,
|
|
110
|
+
clientType: s.config.clientType,
|
|
111
|
+
workDir: s.config.workDir,
|
|
112
|
+
takeover: this.takeovers.has(sessionId),
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
101
115
|
// ── Human takeover (the "stuck" handoff) ────────────────────────────────
|
|
102
116
|
// A text-frame equivalent of the browser takeover: the console shows the live
|
|
103
117
|
// pane and forwards the human's keystrokes until they Resume.
|
|
@@ -161,6 +161,20 @@ async function route(runner, req, res) {
|
|
|
161
161
|
if (req.method === "GET" && path === "/coding/sessions") {
|
|
162
162
|
return json(res, 200, { sessions: runner.coding.list() });
|
|
163
163
|
}
|
|
164
|
+
if (req.method === "GET" && path === "/coding/diagnostics") {
|
|
165
|
+
const { listSessions: tmuxList } = await import("./coding/tmux.js");
|
|
166
|
+
const allTmux = tmuxList();
|
|
167
|
+
const pagsTmux = allTmux.filter((n) => n.startsWith("pags-"));
|
|
168
|
+
const tracked = runner.coding.diagnostics();
|
|
169
|
+
const trackedNames = new Set(tracked.map((s) => s.tmuxSession));
|
|
170
|
+
const orphanedTmux = pagsTmux.filter((n) => !trackedNames.has(n));
|
|
171
|
+
return json(res, 200, {
|
|
172
|
+
tracked,
|
|
173
|
+
orphanedTmux,
|
|
174
|
+
tmuxTotal: allTmux.length,
|
|
175
|
+
pagsTmuxTotal: pagsTmux.length,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
164
178
|
if (req.method === "POST" && path === "/coding/event") {
|
|
165
179
|
// Brain progress events — recorded by PAGS, ignored locally. Accept + ack.
|
|
166
180
|
return json(res, 200, { ok: true });
|
package/dist/index.js
CHANGED
|
@@ -616,7 +616,7 @@ import { randomUUID } from "crypto";
|
|
|
616
616
|
import { existsSync as existsSync5 } from "fs";
|
|
617
617
|
import { chmod, mkdir, writeFile } from "fs/promises";
|
|
618
618
|
import { gunzipSync } from "zlib";
|
|
619
|
-
import { arch, homedir as homedir2, platform } from "os";
|
|
619
|
+
import { arch, homedir as homedir2, hostname, platform } from "os";
|
|
620
620
|
import { resolve as resolve4 } from "path";
|
|
621
621
|
import { createServer as createServer2 } from "net";
|
|
622
622
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -786,7 +786,8 @@ function buildRuntimeRegistrationBody(opts, capabilities = []) {
|
|
|
786
786
|
token: clean(opts.runnerToken) || clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN),
|
|
787
787
|
placement: opts.placement === "managed" ? "managed" : "local",
|
|
788
788
|
capabilities,
|
|
789
|
-
runnerVersion: clean(opts.runnerVersion) || ""
|
|
789
|
+
runnerVersion: clean(opts.runnerVersion) || "",
|
|
790
|
+
runnerNode: hostname()
|
|
790
791
|
};
|
|
791
792
|
}
|
|
792
793
|
function clean(value) {
|
|
@@ -905,6 +906,99 @@ function responseErrorMessage(data, text, statusText) {
|
|
|
905
906
|
function collectCapability(value, previous = []) {
|
|
906
907
|
return [...previous, value];
|
|
907
908
|
}
|
|
909
|
+
async function connectViaRelay(instanceIds, localUrl, runnerToken, opts) {
|
|
910
|
+
const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
|
|
911
|
+
const pagsToken = clean(opts.pagsToken) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
|
|
912
|
+
if (!pagsToken) throw new Error("PAGS token required for WebSocket relay");
|
|
913
|
+
const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
|
|
914
|
+
const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
|
|
915
|
+
for (const id of instanceIds) {
|
|
916
|
+
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
|
|
917
|
+
endpointUrl: localUrl,
|
|
918
|
+
token: runnerToken,
|
|
919
|
+
placement: "local",
|
|
920
|
+
capabilities: caps,
|
|
921
|
+
runnerVersion: "",
|
|
922
|
+
runnerNode: hostname()
|
|
923
|
+
}).catch((e) => writeError(`register ${id.slice(0, 8)}\u2026 failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
924
|
+
}
|
|
925
|
+
for (const id of instanceIds) {
|
|
926
|
+
openRelaySocket(id, apiBase, pagsToken, localUrl, runnerToken);
|
|
927
|
+
}
|
|
928
|
+
writeLine("Runtime registered with PAGS \u2713");
|
|
929
|
+
writeLine("");
|
|
930
|
+
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");
|
|
931
|
+
writeLine(" \u2705 CONNECTED \u2014 WebSocket relay (no tunnel)");
|
|
932
|
+
writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"}`);
|
|
933
|
+
writeLine(" No cloudflared needed. Ctrl+C to disconnect.");
|
|
934
|
+
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");
|
|
935
|
+
let stopped = false;
|
|
936
|
+
const heartbeatLoop = async () => {
|
|
937
|
+
while (!stopped) {
|
|
938
|
+
await new Promise((r) => setTimeout(r, 3e4));
|
|
939
|
+
if (stopped) break;
|
|
940
|
+
for (const id of instanceIds) {
|
|
941
|
+
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {}).catch(() => void 0);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
void heartbeatLoop();
|
|
946
|
+
process.once("beforeExit", () => {
|
|
947
|
+
stopped = true;
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
function openRelaySocket(instanceId, wsBase, pagsToken, localUrl, runnerToken) {
|
|
951
|
+
let backoffMs = 1e3;
|
|
952
|
+
let reconnecting = false;
|
|
953
|
+
const connect = () => {
|
|
954
|
+
const url = `${wsBase}/v1/relay/${encodeURIComponent(instanceId)}/connect?token=${encodeURIComponent(pagsToken)}`;
|
|
955
|
+
const ws = new WebSocket(url);
|
|
956
|
+
ws.onopen = () => {
|
|
957
|
+
backoffMs = 1e3;
|
|
958
|
+
writeLine(`Relay connected: ${instanceId.slice(0, 8)}\u2026`);
|
|
959
|
+
};
|
|
960
|
+
ws.onmessage = async (event) => {
|
|
961
|
+
const text = typeof event.data === "string" ? event.data : String(event.data);
|
|
962
|
+
let cmd;
|
|
963
|
+
try {
|
|
964
|
+
cmd = JSON.parse(text);
|
|
965
|
+
} catch {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (!cmd.id || !cmd.path) return;
|
|
969
|
+
const method = (cmd.method || "POST").toUpperCase();
|
|
970
|
+
const hasBody = method !== "GET" && method !== "HEAD" && cmd.body !== void 0;
|
|
971
|
+
try {
|
|
972
|
+
const headers = {};
|
|
973
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
974
|
+
if (runnerToken) headers.Authorization = `Bearer ${runnerToken}`;
|
|
975
|
+
headers["X-PAGS-Instance-Id"] = instanceId;
|
|
976
|
+
const res = await fetch(`${localUrl}${cmd.path}`, {
|
|
977
|
+
method,
|
|
978
|
+
headers,
|
|
979
|
+
body: hasBody ? JSON.stringify(cmd.body) : void 0
|
|
980
|
+
});
|
|
981
|
+
const result = await res.json().catch(() => ({}));
|
|
982
|
+
ws.send(JSON.stringify({ id: cmd.id, status: res.status, result }));
|
|
983
|
+
} catch (err) {
|
|
984
|
+
ws.send(JSON.stringify({ id: cmd.id, status: 500, error: err instanceof Error ? err.message : String(err) }));
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
ws.onclose = () => {
|
|
988
|
+
if (reconnecting) return;
|
|
989
|
+
reconnecting = true;
|
|
990
|
+
writeLine(`Relay disconnected: ${instanceId.slice(0, 8)}\u2026 \u2014 reconnecting in ${Math.round(backoffMs / 1e3)}s`);
|
|
991
|
+
setTimeout(() => {
|
|
992
|
+
reconnecting = false;
|
|
993
|
+
connect();
|
|
994
|
+
}, backoffMs);
|
|
995
|
+
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
996
|
+
};
|
|
997
|
+
ws.onerror = () => {
|
|
998
|
+
};
|
|
999
|
+
};
|
|
1000
|
+
connect();
|
|
1001
|
+
}
|
|
908
1002
|
function createRunnerCommand() {
|
|
909
1003
|
const command = new Command6("runner").description(
|
|
910
1004
|
"Manage the local FAGS browser runtime for ProAgentStore agents"
|
|
@@ -912,7 +1006,7 @@ function createRunnerCommand() {
|
|
|
912
1006
|
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
1007
|
await startRunnerForeground(opts);
|
|
914
1008
|
});
|
|
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) => {
|
|
1009
|
+
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").option("--tunnel <mode>", "Tunnel mode: 'ws' (WebSocket relay), 'named' (production), or 'quick' (default)", "quick").action(async (instanceIds, opts) => {
|
|
916
1010
|
const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`;
|
|
917
1011
|
const host = clean(opts.host) || "127.0.0.1";
|
|
918
1012
|
const port = clean(opts.port) || String(await findFreePort2(49171));
|
|
@@ -951,6 +1045,78 @@ function createRunnerCommand() {
|
|
|
951
1045
|
try {
|
|
952
1046
|
await waitForLocalRunner({ url: localUrl, token: runnerToken, instanceId: primary });
|
|
953
1047
|
writeLine(`Local FAGS runtime healthy at ${localUrl}`);
|
|
1048
|
+
if (opts.tunnel === "ws") {
|
|
1049
|
+
await connectViaRelay(instanceIds, localUrl, runnerToken, opts);
|
|
1050
|
+
await new Promise((resolvePromise) => {
|
|
1051
|
+
runner.on("exit", () => resolvePromise());
|
|
1052
|
+
});
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (opts.tunnel === "named") {
|
|
1056
|
+
writeLine("Provisioning named tunnel\u2026");
|
|
1057
|
+
const provRes = await requestPags("POST", "/v1/tunnel/provision", opts, {});
|
|
1058
|
+
const prov = provRes;
|
|
1059
|
+
if (!prov.connectorToken) {
|
|
1060
|
+
writeError(`Named tunnel provisioning failed: ${prov.error || "no token returned"}. Falling back to quick tunnel.`);
|
|
1061
|
+
} else {
|
|
1062
|
+
const tunnelUrl2 = prov.endpointUrl || `https://${prov.hostname}`;
|
|
1063
|
+
writeLine(`Named tunnel: ${tunnelUrl2}`);
|
|
1064
|
+
const cloudflaredPath2 = await resolveCloudflaredCommand(opts.cloudflared);
|
|
1065
|
+
const tunnelProc = spawn2(cloudflaredPath2, ["tunnel", "run", "--token", prov.connectorToken], {
|
|
1066
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1067
|
+
shell: process.platform === "win32"
|
|
1068
|
+
});
|
|
1069
|
+
tunnel = tunnelProc;
|
|
1070
|
+
tunnelProc.stdout?.on("data", (d2) => {
|
|
1071
|
+
const t = d2.toString("utf-8");
|
|
1072
|
+
if (/registered/.test(t) || /CONNECTED/.test(t)) writeLine("Named tunnel connected \u2713");
|
|
1073
|
+
});
|
|
1074
|
+
tunnelProc.stderr?.on("data", (d2) => {
|
|
1075
|
+
const t = d2.toString("utf-8");
|
|
1076
|
+
if (/registered/.test(t) || /CONNECTED/.test(t)) writeLine("Named tunnel connected \u2713");
|
|
1077
|
+
});
|
|
1078
|
+
await new Promise((r) => setTimeout(r, 4e3));
|
|
1079
|
+
const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: primary });
|
|
1080
|
+
const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
|
|
1081
|
+
for (const id of instanceIds) {
|
|
1082
|
+
try {
|
|
1083
|
+
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
|
|
1084
|
+
endpointUrl: tunnelUrl2,
|
|
1085
|
+
token: runnerToken,
|
|
1086
|
+
placement: "local",
|
|
1087
|
+
capabilities: caps,
|
|
1088
|
+
runnerVersion: clean(opts.runnerVersion) || "",
|
|
1089
|
+
runnerNode: hostname()
|
|
1090
|
+
});
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
writeError(`register ${id.slice(0, 8)}\u2026 failed (${error instanceof Error ? error.message : String(error)})`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
writeLine("Runtime registered with PAGS \u2713");
|
|
1096
|
+
writeLine("");
|
|
1097
|
+
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");
|
|
1098
|
+
writeLine(" \u2705 CONNECTED \u2014 named tunnel (production)");
|
|
1099
|
+
writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"}`);
|
|
1100
|
+
writeLine(` Endpoint: ${tunnelUrl2}`);
|
|
1101
|
+
writeLine(" Stable URL \u2014 no reconnect churn. Ctrl+C to disconnect.");
|
|
1102
|
+
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");
|
|
1103
|
+
let stopped2 = false;
|
|
1104
|
+
void (async () => {
|
|
1105
|
+
while (!stopped2) {
|
|
1106
|
+
await new Promise((r) => setTimeout(r, 3e4));
|
|
1107
|
+
if (stopped2) break;
|
|
1108
|
+
for (const id of instanceIds) {
|
|
1109
|
+
await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {}).catch(() => void 0);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
})();
|
|
1113
|
+
await new Promise((resolvePromise) => {
|
|
1114
|
+
runner.on("exit", () => resolvePromise());
|
|
1115
|
+
});
|
|
1116
|
+
stopped2 = true;
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
954
1120
|
const cloudflaredPath = await resolveCloudflaredCommand(opts.cloudflared);
|
|
955
1121
|
const openTunnel = () => new Promise((resolveTunnel, reject) => {
|
|
956
1122
|
let output = "";
|
|
@@ -978,7 +1144,8 @@ function createRunnerCommand() {
|
|
|
978
1144
|
token: runnerToken,
|
|
979
1145
|
placement: "local",
|
|
980
1146
|
capabilities: caps,
|
|
981
|
-
runnerVersion: clean(opts.runnerVersion) || ""
|
|
1147
|
+
runnerVersion: clean(opts.runnerVersion) || "",
|
|
1148
|
+
runnerNode: hostname()
|
|
982
1149
|
});
|
|
983
1150
|
} catch (error) {
|
|
984
1151
|
writeError(`register ${id.slice(0, 8)}\u2026 failed (${error instanceof Error ? error.message : String(error)}); will retry`);
|
|
@@ -1000,11 +1167,15 @@ function createRunnerCommand() {
|
|
|
1000
1167
|
writeLine(" Auto-reconnect is on \u2014 keep this terminal open. Ctrl+C to disconnect.");
|
|
1001
1168
|
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");
|
|
1002
1169
|
let stopped = false;
|
|
1170
|
+
let consecutiveFailures = 0;
|
|
1171
|
+
let respawnBackoffMs = 3e4;
|
|
1172
|
+
const PROBE_FAIL_THRESHOLD = 3;
|
|
1173
|
+
const MAX_BACKOFF_MS = 5 * 6e4;
|
|
1003
1174
|
const probeTunnel = async (url) => {
|
|
1004
1175
|
try {
|
|
1005
1176
|
const res = await fetch(`${url.replace(/\/$/, "")}/health`, {
|
|
1006
1177
|
headers: { Authorization: `Bearer ${runnerToken}`, "X-PAGS-Instance-Id": primary },
|
|
1007
|
-
signal: AbortSignal.timeout(
|
|
1178
|
+
signal: AbortSignal.timeout(1e4)
|
|
1008
1179
|
});
|
|
1009
1180
|
return res.ok;
|
|
1010
1181
|
} catch {
|
|
@@ -1024,13 +1195,21 @@ function createRunnerCommand() {
|
|
|
1024
1195
|
await new Promise((r) => setTimeout(r, 3e4));
|
|
1025
1196
|
if (stopped) break;
|
|
1026
1197
|
if (await probeTunnel(tunnelUrl)) {
|
|
1198
|
+
consecutiveFailures = 0;
|
|
1199
|
+
respawnBackoffMs = 3e4;
|
|
1027
1200
|
const beat = await heartbeatAll();
|
|
1028
1201
|
if (!beat) {
|
|
1029
1202
|
await registerRuntime(tunnelUrl).then(() => writeLine("\u2705 Re-registered with PAGS")).catch(() => void 0);
|
|
1030
1203
|
}
|
|
1031
1204
|
continue;
|
|
1032
1205
|
}
|
|
1206
|
+
consecutiveFailures++;
|
|
1207
|
+
if (consecutiveFailures < PROBE_FAIL_THRESHOLD) {
|
|
1208
|
+
writeLine(`\u26A0 Tunnel probe failed (${consecutiveFailures}/${PROBE_FAIL_THRESHOLD}) \u2014 will retry`);
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1033
1211
|
writeLine("\u26A0 Tunnel unreachable \u2014 reconnecting\u2026");
|
|
1212
|
+
consecutiveFailures = 0;
|
|
1034
1213
|
try {
|
|
1035
1214
|
if (tunnel && !tunnel.killed) tunnel.kill("SIGTERM");
|
|
1036
1215
|
} catch {
|
|
@@ -1040,9 +1219,12 @@ function createRunnerCommand() {
|
|
|
1040
1219
|
tunnel = next.proc;
|
|
1041
1220
|
tunnelUrl = next.url;
|
|
1042
1221
|
await registerRuntime(tunnelUrl);
|
|
1222
|
+
respawnBackoffMs = 3e4;
|
|
1043
1223
|
writeLine(`\u2705 Reconnected: ${tunnelUrl}`);
|
|
1044
1224
|
} catch (error) {
|
|
1045
|
-
writeError(`reconnect failed (${error instanceof Error ? error.message : String(error)}); retrying in
|
|
1225
|
+
writeError(`reconnect failed (${error instanceof Error ? error.message : String(error)}); retrying in ${Math.round(respawnBackoffMs / 1e3)}s`);
|
|
1226
|
+
await new Promise((r) => setTimeout(r, respawnBackoffMs));
|
|
1227
|
+
respawnBackoffMs = Math.min(respawnBackoffMs * 2, MAX_BACKOFF_MS);
|
|
1046
1228
|
}
|
|
1047
1229
|
}
|
|
1048
1230
|
})();
|
|
@@ -1170,6 +1352,7 @@ import { Command as Command7 } from "commander";
|
|
|
1170
1352
|
|
|
1171
1353
|
// src/tui.ts
|
|
1172
1354
|
import chalk from "chalk";
|
|
1355
|
+
import { hostname as hostname2 } from "os";
|
|
1173
1356
|
import readline from "readline";
|
|
1174
1357
|
var ACCENT = "#7c3aed";
|
|
1175
1358
|
var c = chalk.hex(ACCENT);
|
|
@@ -1211,7 +1394,7 @@ function printStatus(state) {
|
|
|
1211
1394
|
clearScreen();
|
|
1212
1395
|
printLogo(state.version);
|
|
1213
1396
|
const connected = state.runner === "online" && state.tunnel === "online" && state.registration === "registered";
|
|
1214
|
-
console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance));
|
|
1397
|
+
console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(hostname2()));
|
|
1215
1398
|
console.log("");
|
|
1216
1399
|
const row = (kind, s) => {
|
|
1217
1400
|
const { icon, label, note } = describe(kind, s);
|
|
@@ -1291,7 +1474,7 @@ async function stopRunnerProcesses() {
|
|
|
1291
1474
|
}
|
|
1292
1475
|
return stopped;
|
|
1293
1476
|
}
|
|
1294
|
-
var upCommand = new Command7("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").action(async (opts) => {
|
|
1477
|
+
var upCommand = new Command7("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").option("--tunnel <mode>", "Tunnel mode: 'ws' (WebSocket relay, recommended), 'named' (production, stable), or 'quick' (default, trycloudflare.com)", "quick").action(async (opts) => {
|
|
1295
1478
|
const session = requireSession();
|
|
1296
1479
|
const state = {
|
|
1297
1480
|
user: session.user.login,
|
|
@@ -1338,6 +1521,8 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1338
1521
|
const cliPath = process.argv[1];
|
|
1339
1522
|
const args = [cliPath, "runner", "connect", ...instances.map((i) => i.id)];
|
|
1340
1523
|
if (opts.headless) args.push("--headless");
|
|
1524
|
+
if (opts.tunnel === "named") args.push("--tunnel", "named");
|
|
1525
|
+
if (opts.tunnel === "ws") args.push("--tunnel", "ws");
|
|
1341
1526
|
const child = spawn3(process.execPath, args, {
|
|
1342
1527
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1343
1528
|
env: { ...process.env, PAGS_TOKEN: session.token }
|
|
@@ -1358,6 +1543,21 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1358
1543
|
printStatus(state);
|
|
1359
1544
|
continue;
|
|
1360
1545
|
}
|
|
1546
|
+
if (trimmed.includes("Relay connected:")) {
|
|
1547
|
+
state.tunnel = "online";
|
|
1548
|
+
state.tunnelUrl = "WebSocket relay";
|
|
1549
|
+
state.lastEvent = "Relay connected";
|
|
1550
|
+
printStatus(state);
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
if (trimmed.includes("WebSocket relay (no tunnel)")) {
|
|
1554
|
+
state.tunnel = "online";
|
|
1555
|
+
state.tunnelUrl = "WebSocket relay";
|
|
1556
|
+
state.registration = "registered";
|
|
1557
|
+
state.lastEvent = "Connected via WebSocket relay";
|
|
1558
|
+
printStatus(state);
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1361
1561
|
if (trimmed.includes("FAGS browser runtime listening")) {
|
|
1362
1562
|
state.runner = "online";
|
|
1363
1563
|
state.lastEvent = "Runner started";
|