@proagentstore/cli 0.4.123 → 0.4.125
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/local-apply/bridge.js +41 -2
- package/dist/browser-runner/local-apply/contract.js +4 -1
- package/dist/browser-runner/local-apply/runtime.js +11 -2
- package/dist/browser-runner/local-browser/bridge.js +23 -5
- package/dist/browser-runner/local-browser/contract.js +3 -0
- package/dist/browser-runner/local-browser/replay.js +74 -0
- package/dist/browser-runner/local-browser/runtime.js +9 -1
- package/dist/browser-runner/server.js +6 -0
- package/dist/index.js +215 -9
- package/package.json +1 -1
|
@@ -123,6 +123,7 @@ function withQuote(schema, required) {
|
|
|
123
123
|
export class ApplyBridge {
|
|
124
124
|
browser;
|
|
125
125
|
host;
|
|
126
|
+
shots;
|
|
126
127
|
/** Read by the runtime for the #975 diagnostic: 0 calls is the finding, not an internal detail. */
|
|
127
128
|
actions = 0;
|
|
128
129
|
pages = 0;
|
|
@@ -142,9 +143,47 @@ export class ApplyBridge {
|
|
|
142
143
|
submitted = null;
|
|
143
144
|
/** The last thing that stopped the work and was not resolved — the block reason if the run ends here. */
|
|
144
145
|
blocked = null;
|
|
145
|
-
constructor(browser, host
|
|
146
|
+
constructor(browser, host,
|
|
147
|
+
/** Replay frames (#1063), taken by the runner with every form control blurred — never a tool the CLI can call. */
|
|
148
|
+
shots) {
|
|
146
149
|
this.browser = browser;
|
|
147
150
|
this.host = host;
|
|
151
|
+
this.shots = shots;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The frame a successful call earns (#1063). A refused call earns none, so a model retrying a refused
|
|
155
|
+
* tool cannot fill the run's frame cap.
|
|
156
|
+
*/
|
|
157
|
+
static shotPoint(name, submittedNow) {
|
|
158
|
+
if (submittedNow)
|
|
159
|
+
return "submit";
|
|
160
|
+
const points = {
|
|
161
|
+
browser_navigate: "navigated",
|
|
162
|
+
browser_navigate_back: "navigated",
|
|
163
|
+
browser_click: "clicked",
|
|
164
|
+
browser_snapshot: "snapshot",
|
|
165
|
+
browser_wait_for: "waited",
|
|
166
|
+
browser_type: "filled",
|
|
167
|
+
browser_select_option: "filled",
|
|
168
|
+
upload_artifact: "uploaded",
|
|
169
|
+
supervisor_checkpoint: "checkpoint",
|
|
170
|
+
ready_for_review: "review",
|
|
171
|
+
request_answer: "paused",
|
|
172
|
+
report_job_unavailable: "blocked",
|
|
173
|
+
};
|
|
174
|
+
return points[name] ?? null;
|
|
175
|
+
}
|
|
176
|
+
async callTool(name, args = {}) {
|
|
177
|
+
const submittedBefore = this.submitAttempted;
|
|
178
|
+
const res = await this.dispatch(name, args);
|
|
179
|
+
const point = res.isError ? null : ApplyBridge.shotPoint(name, this.submitAttempted && !submittedBefore);
|
|
180
|
+
if (point && this.shots) {
|
|
181
|
+
const seq = await this.shots.capture(this.browser, point);
|
|
182
|
+
// Metadata only: which frame, and why. The pixels — every form control blurred — stay in the runner.
|
|
183
|
+
if (seq)
|
|
184
|
+
this.host.emit({ type: "screenshot.captured", detail: { shot: seq, point } });
|
|
185
|
+
}
|
|
186
|
+
return res;
|
|
148
187
|
}
|
|
149
188
|
get done() {
|
|
150
189
|
return this.reviewReady || this.submitAttempted || this.unavailable !== null;
|
|
@@ -189,7 +228,7 @@ export class ApplyBridge {
|
|
|
189
228
|
},
|
|
190
229
|
];
|
|
191
230
|
}
|
|
192
|
-
async
|
|
231
|
+
async dispatch(name, args) {
|
|
193
232
|
if (this.done)
|
|
194
233
|
return text("The application is finished for this run. Stop now.", true);
|
|
195
234
|
if (name === "ready_for_review")
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
export const LOCAL_APPLY_TASK_TYPE = "local_browser.apply";
|
|
30
30
|
export const LOCAL_APPLY_RUN_PATH = "/local-apply/run";
|
|
31
31
|
export const LOCAL_APPLY_STATUS_PATH = "/local-apply/status";
|
|
32
|
+
/** `POST {runId, afterSeq}` — a run's replay screenshots (#1063), the shape of `/local-browser/shots`. */
|
|
33
|
+
export const LOCAL_APPLY_SHOTS_PATH = "/local-apply/shots";
|
|
32
34
|
export const LOCAL_APPLY_RESUME_PATH = "/local-apply/resume";
|
|
33
35
|
export const LOCAL_APPLY_CANCEL_PATH = "/local-apply/cancel";
|
|
34
36
|
/** Read-only job-page check used before an email lead may enter material preparation. */
|
|
@@ -158,6 +160,7 @@ export const LOCAL_APPLY_EVENT_TYPES = [
|
|
|
158
160
|
"supervisor.directive",
|
|
159
161
|
"run.paused",
|
|
160
162
|
"run.resumed",
|
|
163
|
+
"screenshot.captured",
|
|
161
164
|
"note",
|
|
162
165
|
];
|
|
163
166
|
export const LOCAL_APPLY_ENGINE_AUTHS = ["machine-login", "subscription", "api-key", "missing_login", "unknown"];
|
|
@@ -176,7 +179,7 @@ const isHttpUrl = (v) => /^https?:\/\/[^\s/]+/i.test(v);
|
|
|
176
179
|
// from `LOCAL_APPLY_CONFIRMATION_MARKERS`, how many times the page was read, and whether it moved.
|
|
177
180
|
// Each is an id, a count or a boolean — the same whitelist rule as everything else here, so no page
|
|
178
181
|
// text and no typed value can ride along on a `submit.confirmed` / `submit.unconfirmed` event.
|
|
179
|
-
const DETAIL_KEYS = new Set(["engine", "authMode", "engineAuth", "mode", "class", "tool", "decision", "reason", "rule", "role", "kind", "path", "sha256", "bytes", "gateId", "exitCode", "count", "status", "basis", "source", "checkpointId", "directive", "phase", "actions", "filled", "uploaded", "marker", "looks", "urlChanged", "titleChanged"]);
|
|
182
|
+
const DETAIL_KEYS = new Set(["engine", "authMode", "engineAuth", "mode", "class", "tool", "decision", "reason", "rule", "role", "kind", "path", "sha256", "bytes", "gateId", "exitCode", "count", "status", "basis", "source", "checkpointId", "directive", "phase", "actions", "filled", "uploaded", "marker", "looks", "urlChanged", "titleChanged", "shot", "point"]);
|
|
180
183
|
/** A validated event, or null. Detail keeps only whitelisted, primitive, bounded values. */
|
|
181
184
|
export function parseLocalApplyEvent(raw) {
|
|
182
185
|
if (!raw || typeof raw !== "object")
|
|
@@ -34,6 +34,7 @@ import { redactText, secretEnvValues } from "../local-browser/contract.js";
|
|
|
34
34
|
import { finalText, missingLogin, signInHelp } from "../local-browser/engine.js";
|
|
35
35
|
import { resolveWorkspacePath } from "../local-browser/runtime.js";
|
|
36
36
|
import { ApplyBridge } from "./bridge.js";
|
|
37
|
+
import { APPLY_SHOT_CAP, ShotRecorder } from "../local-browser/replay.js";
|
|
37
38
|
import { confirmationSignals } from "./confirmation.js";
|
|
38
39
|
import { listingUnavailableReason } from "./unavailable.js";
|
|
39
40
|
import { LOCAL_APPLY_AUTH_MODES, LOCAL_APPLY_CAPS, LOCAL_APPLY_ENGINES, LOCAL_APPLY_MODES, LOCAL_APPLY_TASK_TYPE, parseLocalApplyResult, } from "./contract.js";
|
|
@@ -276,7 +277,8 @@ export class LocalApplyRuntime {
|
|
|
276
277
|
continue;
|
|
277
278
|
const checked = parsed.result;
|
|
278
279
|
this.runs.set(envelope.runId, {
|
|
279
|
-
|
|
280
|
+
// Replay frames are memory-only by design (#1063): a run reloaded after a restart has none.
|
|
281
|
+
envelope, dir, token: "", state: "ended", events: raw.events, seq: raw.seq, shots: new ShotRecorder(0),
|
|
280
282
|
result: checked, allow: new Set(envelope.policy.allowDomains), grounding: [], secrets: [], activeSince: raw.endedAt,
|
|
281
283
|
activeMs: 0, waiters: [], output: [], engineAuth: checked.engineAuth, cancelled: false, timedOut: false,
|
|
282
284
|
checkpoints: new Map(), endedAt: raw.endedAt,
|
|
@@ -308,6 +310,7 @@ export class LocalApplyRuntime {
|
|
|
308
310
|
token: randomBytes(24).toString("hex"),
|
|
309
311
|
state: "running",
|
|
310
312
|
events: [],
|
|
313
|
+
shots: new ShotRecorder(APPLY_SHOT_CAP, () => this.now()),
|
|
311
314
|
seq: 0,
|
|
312
315
|
allow: new Set(envelope.policy.allowDomains),
|
|
313
316
|
grounding: [],
|
|
@@ -393,7 +396,7 @@ export class LocalApplyRuntime {
|
|
|
393
396
|
limits: e.limits,
|
|
394
397
|
mode: e.policy.mode,
|
|
395
398
|
gateId: e.policy.submitGate?.gateId,
|
|
396
|
-
});
|
|
399
|
+
}, run.shots);
|
|
397
400
|
if (spec.mcpConfig)
|
|
398
401
|
writeFileSync(spec.mcpConfig.path, spec.mcpConfig.json, { mode: 0o600 });
|
|
399
402
|
const child = this.spawn(spec.command, spec.args, { cwd: join(run.dir, "scratch"), env: spec.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -638,6 +641,12 @@ export class LocalApplyRuntime {
|
|
|
638
641
|
}
|
|
639
642
|
return { runId, state: run.state };
|
|
640
643
|
}
|
|
644
|
+
/** `POST /local-apply/shots {runId, afterSeq}` — the run's replay frames after `afterSeq` (#1063). */
|
|
645
|
+
shots(raw) {
|
|
646
|
+
const o = (raw && typeof raw === "object" ? raw : {});
|
|
647
|
+
const run = this.get(String(o.runId ?? ""));
|
|
648
|
+
return run.shots.page(run.envelope.runId, typeof o.afterSeq === "number" && o.afterSeq > 0 ? o.afterSeq : 0);
|
|
649
|
+
}
|
|
641
650
|
status(raw) {
|
|
642
651
|
const o = (raw && typeof raw === "object" ? raw : {});
|
|
643
652
|
const run = this.get(String(o.runId ?? ""));
|
|
@@ -130,6 +130,7 @@ export function refRole(snapshot, ref) {
|
|
|
130
130
|
export class BrowserBridge {
|
|
131
131
|
browser;
|
|
132
132
|
host;
|
|
133
|
+
shots;
|
|
133
134
|
actions = 0;
|
|
134
135
|
pages = 0;
|
|
135
136
|
lastSnapshot = "";
|
|
@@ -141,9 +142,18 @@ export class BrowserBridge {
|
|
|
141
142
|
summary = null;
|
|
142
143
|
/** Tool calls that reached the bridge. Zero at exit means the CLI never used the browser (#944). */
|
|
143
144
|
calls = 0;
|
|
144
|
-
constructor(browser, host
|
|
145
|
+
constructor(browser, host,
|
|
146
|
+
/** Replay frames (#1063), taken by the runner at the points below — never a tool the CLI can call. */
|
|
147
|
+
shots) {
|
|
145
148
|
this.browser = browser;
|
|
146
149
|
this.host = host;
|
|
150
|
+
this.shots = shots;
|
|
151
|
+
}
|
|
152
|
+
/** Take a replay frame, best-effort; the trace only records that it exists (seq + capture point). */
|
|
153
|
+
async shot(point, url) {
|
|
154
|
+
const seq = await this.shots?.capture(this.browser, point, url ? { url } : {});
|
|
155
|
+
if (seq)
|
|
156
|
+
this.host.emit({ type: "screenshot.captured", ...this.currentHost(), detail: { shot: seq, point } });
|
|
147
157
|
}
|
|
148
158
|
/** What the CLI sees: the read-only browser tools that exist, plus the research tools. */
|
|
149
159
|
async listTools() {
|
|
@@ -161,6 +171,7 @@ export class BrowserBridge {
|
|
|
161
171
|
return this.reportFailure(args);
|
|
162
172
|
if (name === "finish_research") {
|
|
163
173
|
this.summary = typeof args.summary === "string" ? args.summary.slice(0, 4000) : "";
|
|
174
|
+
await this.shot("completed");
|
|
164
175
|
return text("Recorded. The research is finished; stop now.");
|
|
165
176
|
}
|
|
166
177
|
if (WRITE_TOOLS.has(name))
|
|
@@ -179,11 +190,17 @@ export class BrowserBridge {
|
|
|
179
190
|
if (name === "browser_snapshot") {
|
|
180
191
|
this.lastSnapshot = textOf(res);
|
|
181
192
|
this.host.emit({ type: "browser.snapshot", ...this.currentHost() });
|
|
193
|
+
if (!res.isError)
|
|
194
|
+
await this.shot("snapshot");
|
|
182
195
|
}
|
|
196
|
+
if (name === "browser_wait_for" && !res.isError)
|
|
197
|
+
await this.shot("waited");
|
|
183
198
|
if (name === "browser_navigate_back") {
|
|
184
199
|
const state = await this.inspect();
|
|
185
|
-
if (state)
|
|
200
|
+
if (state) {
|
|
186
201
|
this.noteNavigation(state);
|
|
202
|
+
await this.shot("navigated", state.url);
|
|
203
|
+
}
|
|
187
204
|
}
|
|
188
205
|
return { content: [{ type: "text", text: textOf(res) }], ...(res.isError ? { isError: true } : {}) };
|
|
189
206
|
}
|
|
@@ -244,7 +261,7 @@ export class BrowserBridge {
|
|
|
244
261
|
await this.browser.callTool("browser_navigate_back", {}).catch(() => undefined);
|
|
245
262
|
return this.refuse("browser_navigate", `${host} redirected to ${landedOn}, a site this run has not been allowed on. Open ${landedOn} with browser_navigate to ask the owner, or skip it.`);
|
|
246
263
|
}
|
|
247
|
-
return this.landed(textOf(res), state);
|
|
264
|
+
return this.landed(textOf(res), state, "navigated");
|
|
248
265
|
}
|
|
249
266
|
async click(args) {
|
|
250
267
|
const ref = typeof args.target === "string" ? args.target : typeof args.ref === "string" ? args.ref : "";
|
|
@@ -265,10 +282,10 @@ export class BrowserBridge {
|
|
|
265
282
|
await this.browser.callTool("browser_navigate_back", {}).catch(() => undefined);
|
|
266
283
|
return this.refuse("browser_click", `That link leads to ${host}, a site this run has not been allowed on. Open it with browser_navigate to ask the owner.`);
|
|
267
284
|
}
|
|
268
|
-
return this.landed(textOf(res), state);
|
|
285
|
+
return this.landed(textOf(res), state, "clicked");
|
|
269
286
|
}
|
|
270
287
|
/** After a page change: count it, trace it, and stop for a person when the page needs one. */
|
|
271
|
-
async landed(toolText, known) {
|
|
288
|
+
async landed(toolText, known, point) {
|
|
272
289
|
const state = known ?? (await this.inspect());
|
|
273
290
|
if (!state)
|
|
274
291
|
return text(toolText);
|
|
@@ -278,6 +295,7 @@ export class BrowserBridge {
|
|
|
278
295
|
// Pauses, never obstacles (#947): a captcha, a sign-in, a bot check, an access-control page or a paywall is
|
|
279
296
|
// for a PERSON in the browser on this machine — the run waits, and re-checks once resumed.
|
|
280
297
|
const blocker = state.captcha ? "captcha" : state.login ? "login_required" : state.accessBlocked ? "access_blocked" : state.paywall ? "paywall" : null;
|
|
298
|
+
await this.shot(blocker || state.writeForm ? "blocked" : point, state.url);
|
|
281
299
|
if (blocker) {
|
|
282
300
|
const what = BLOCKER_WORDS[blocker];
|
|
283
301
|
this.host.emit({ type: "browser.blocked", url: state.url, domain: host, detail: { reason: blocker } });
|
|
@@ -29,6 +29,8 @@ export const LOCAL_BROWSER_RUN_PATH = "/local-browser/run";
|
|
|
29
29
|
export const LOCAL_BROWSER_CANCEL_PATH = "/local-browser/cancel";
|
|
30
30
|
export const LOCAL_BROWSER_STATUS_PATH = "/local-browser/status";
|
|
31
31
|
export const LOCAL_BROWSER_RESUME_PATH = "/local-browser/resume";
|
|
32
|
+
/** `POST {runId, afterSeq}` — a run's replay screenshots (#1063), pulled like its status. */
|
|
33
|
+
export const LOCAL_BROWSER_SHOTS_PATH = "/local-browser/shots";
|
|
32
34
|
export const LOCAL_BROWSER_ENGINES = ["claude", "codex"];
|
|
33
35
|
export const LOCAL_BROWSER_AUTH_MODES = ["subscription", "machine", "api-key"];
|
|
34
36
|
export const LOCAL_BROWSER_MODES = ["research_only"];
|
|
@@ -45,6 +47,7 @@ export const LOCAL_BROWSER_EVENT_TYPES = [
|
|
|
45
47
|
"finding.parsed",
|
|
46
48
|
"run.paused",
|
|
47
49
|
"run.resumed",
|
|
50
|
+
"screenshot.captured",
|
|
48
51
|
"note",
|
|
49
52
|
];
|
|
50
53
|
export const LOCAL_BROWSER_PLATFORM_EVENT_TYPES = ["run.requested", "runner.dispatched", "run.ended", "review.decision"];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { evaluateResult } from "./bridge.js";
|
|
2
|
+
/** Per-run caps (#1063): enough to replay a run, not enough to make storage unbounded. */
|
|
3
|
+
export const SCOUT_SHOT_CAP = 60;
|
|
4
|
+
export const APPLY_SHOT_CAP = 40;
|
|
5
|
+
/** One frame, as base64 characters (~768 KiB of JPEG) — the API refuses larger, so the runner never sends one. */
|
|
6
|
+
export const MAX_SHOT_BASE64 = Math.ceil((768 * 1024 * 4) / 3);
|
|
7
|
+
/** One pull's reply stays well under the relay's message limit. At least one frame is always returned. */
|
|
8
|
+
export const SHOTS_PAGE_BASE64 = 900 * 1024;
|
|
9
|
+
const MASK_ID = "__pags_replay_mask";
|
|
10
|
+
/** Runs in the page through the browser's own evaluate — never offered to the CLI. */
|
|
11
|
+
export const MASK_FORM_CONTROLS = `() => {
|
|
12
|
+
let s = document.getElementById(${JSON.stringify(MASK_ID)});
|
|
13
|
+
if (!s) {
|
|
14
|
+
s = document.createElement("style");
|
|
15
|
+
s.id = ${JSON.stringify(MASK_ID)};
|
|
16
|
+
s.textContent = "input, textarea, select, option, [contenteditable], [contenteditable] * { filter: blur(8px) !important; }";
|
|
17
|
+
(document.head || document.documentElement).appendChild(s);
|
|
18
|
+
}
|
|
19
|
+
return document.getElementById(${JSON.stringify(MASK_ID)}) ? "masked" : "unmasked";
|
|
20
|
+
}`;
|
|
21
|
+
export const UNMASK_FORM_CONTROLS = `() => { const s = document.getElementById(${JSON.stringify(MASK_ID)}); if (s) s.remove(); return "unmasked"; }`;
|
|
22
|
+
const textOf = (r) => (r.content ?? []).map((c) => c.text ?? "").join("\n");
|
|
23
|
+
export class ShotRecorder {
|
|
24
|
+
cap;
|
|
25
|
+
now;
|
|
26
|
+
shots = [];
|
|
27
|
+
nextSeq = 0;
|
|
28
|
+
constructor(cap, now = Date.now) {
|
|
29
|
+
this.cap = cap;
|
|
30
|
+
this.now = now;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Take one frame, best-effort. Returns its seq, or null when none was kept: the cap is reached, the
|
|
34
|
+
* form controls could not be masked (fail closed), or the browser gave no image.
|
|
35
|
+
*/
|
|
36
|
+
async capture(browser, point, meta = {}) {
|
|
37
|
+
if (this.nextSeq >= this.cap)
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const masked = await browser.callTool("browser_evaluate", { function: MASK_FORM_CONTROLS });
|
|
41
|
+
if (masked.isError || evaluateResult(textOf(masked)) !== "masked")
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
const res = await browser.callTool("browser_take_screenshot", { type: "jpeg" });
|
|
45
|
+
const image = res.isError ? undefined : (res.content ?? []).find((c) => c.type === "image" && typeof c.data === "string" && c.data.length > 0);
|
|
46
|
+
if (!image?.data || image.data.length > MAX_SHOT_BASE64)
|
|
47
|
+
return null;
|
|
48
|
+
const seq = ++this.nextSeq;
|
|
49
|
+
this.shots.push({ seq, point, ...(meta.url ? { url: meta.url } : {}), ...(meta.action ? { action: meta.action } : {}), mimeType: image.mimeType ?? "image/jpeg", data: image.data, at: new Date(this.now()).toISOString() });
|
|
50
|
+
return seq;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await browser.callTool("browser_evaluate", { function: UNMASK_FORM_CONTROLS }).catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** The frames after `afterSeq`, paged by size; `lastSeq` never runs past a frame not returned. */
|
|
61
|
+
page(runId, afterSeq) {
|
|
62
|
+
const out = [];
|
|
63
|
+
let size = 0;
|
|
64
|
+
for (const s of this.shots) {
|
|
65
|
+
if (s.seq <= afterSeq)
|
|
66
|
+
continue;
|
|
67
|
+
if (out.length && size + s.data.length > SHOTS_PAGE_BASE64)
|
|
68
|
+
break;
|
|
69
|
+
out.push(s);
|
|
70
|
+
size += s.data.length;
|
|
71
|
+
}
|
|
72
|
+
return { runId, shots: out, lastSeq: out.length ? out[out.length - 1].seq : afterSeq };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -25,6 +25,7 @@ import { join, resolve, sep } from "node:path";
|
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { RunnerInputError } from "../errors.js";
|
|
27
27
|
import { BrowserBridge, domainWithin } from "./bridge.js";
|
|
28
|
+
import { SCOUT_SHOT_CAP, ShotRecorder } from "./replay.js";
|
|
28
29
|
import { BRIDGE_ENV } from "./bridge-stdio.js";
|
|
29
30
|
import { LOCAL_BROWSER_AUTH_MODES, LOCAL_BROWSER_ENGINES, LOCAL_BROWSER_TASK_TYPE, redactDetail, redactFinding, redactSourceFailure, redactText, secretEnvValues, } from "./contract.js";
|
|
30
31
|
import { bridgeUnused, buildEngineSpec, finalText, missingLogin, observedEngineAuth, researchPrompt, signInHelp } from "./engine.js";
|
|
@@ -184,6 +185,7 @@ export class LocalBrowserRuntime {
|
|
|
184
185
|
token: randomBytes(24).toString("hex"),
|
|
185
186
|
state: "running",
|
|
186
187
|
events: [],
|
|
188
|
+
shots: new ShotRecorder(SCOUT_SHOT_CAP, () => this.now()),
|
|
187
189
|
seq: 0,
|
|
188
190
|
consented: new Set(envelope.policy.consentedDomains),
|
|
189
191
|
deny: new Set(envelope.policy.denyDomains),
|
|
@@ -228,7 +230,7 @@ export class LocalBrowserRuntime {
|
|
|
228
230
|
allowListOnly: () => e.policy.allowDomains.length > 0,
|
|
229
231
|
overTime: () => this.activeMs(run) >= e.limits.maxMinutes * 60_000,
|
|
230
232
|
limits: e.limits,
|
|
231
|
-
});
|
|
233
|
+
}, run.shots);
|
|
232
234
|
const bridgeScript = this.deps.bridgeScript ?? fileURLToPath(new URL("./bridge-stdio.js", import.meta.url));
|
|
233
235
|
const spec = buildEngineSpec({
|
|
234
236
|
engine: e.engine,
|
|
@@ -360,6 +362,12 @@ export class LocalBrowserRuntime {
|
|
|
360
362
|
...(run.result ? { result: run.result } : {}),
|
|
361
363
|
};
|
|
362
364
|
}
|
|
365
|
+
/** `POST /local-browser/shots {runId, afterSeq}` — the run's replay frames after `afterSeq` (#1063). */
|
|
366
|
+
shots(raw) {
|
|
367
|
+
const o = (raw && typeof raw === "object" ? raw : {});
|
|
368
|
+
const run = this.get(String(o.runId ?? ""));
|
|
369
|
+
return run.shots.page(run.envelope.runId, typeof o.afterSeq === "number" && o.afterSeq > 0 ? o.afterSeq : 0);
|
|
370
|
+
}
|
|
363
371
|
/** Does this token belong to this run? The bridge forwarder's only credential. */
|
|
364
372
|
authorizeBridge(runId, token) {
|
|
365
373
|
const run = this.runs.get(runId);
|
|
@@ -211,6 +211,9 @@ async function route(runner, req, res) {
|
|
|
211
211
|
if (req.method === "POST" && path === "/local-browser/status") {
|
|
212
212
|
return json(res, 200, runner.localBrowser.status(await readJson(req)));
|
|
213
213
|
}
|
|
214
|
+
if (req.method === "POST" && path === "/local-browser/shots") {
|
|
215
|
+
return json(res, 200, runner.localBrowser.shots(await readJson(req)));
|
|
216
|
+
}
|
|
214
217
|
if (req.method === "POST" && path === "/local-browser/resume") {
|
|
215
218
|
return json(res, 200, runner.localBrowser.resume(await readJson(req)));
|
|
216
219
|
}
|
|
@@ -237,6 +240,9 @@ async function route(runner, req, res) {
|
|
|
237
240
|
if (req.method === "POST" && path === "/local-apply/status") {
|
|
238
241
|
return json(res, 200, runner.localApply.status(await readJson(req)));
|
|
239
242
|
}
|
|
243
|
+
if (req.method === "POST" && path === "/local-apply/shots") {
|
|
244
|
+
return json(res, 200, runner.localApply.shots(await readJson(req)));
|
|
245
|
+
}
|
|
240
246
|
const handoff = await routeLocalApplyHandoff(runner.localApply, req.method, path, () => readJson(req));
|
|
241
247
|
if (handoff)
|
|
242
248
|
return json(res, 200, handoff);
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
// src/index.ts
|
|
13
13
|
import { createRequire as createRequire3 } from "module";
|
|
14
|
-
import { Command as
|
|
14
|
+
import { Command as Command11 } from "commander";
|
|
15
15
|
|
|
16
16
|
// src/commands/check.ts
|
|
17
17
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -1548,6 +1548,7 @@ var isTakeable = (verdict) => verdict === "dead";
|
|
|
1548
1548
|
function holderWhere(lock) {
|
|
1549
1549
|
if (lock.launch === "tmux") return `tmux session ${lock.launchDetail || "?"}`;
|
|
1550
1550
|
if (lock.launch === "service") return "a background service (launchd/systemd)";
|
|
1551
|
+
if (lock.launch === "background") return `the background (\`pags up --background\`${lock.launchDetail ? `, log ${lock.launchDetail}` : ""})`;
|
|
1551
1552
|
if (lock.launch === "headless") return "headless";
|
|
1552
1553
|
return lock.launchDetail ? `terminal ${lock.launchDetail}` : "a terminal";
|
|
1553
1554
|
}
|
|
@@ -1565,6 +1566,7 @@ function holderMessage(lock, verdict) {
|
|
|
1565
1566
|
}
|
|
1566
1567
|
lines.push("", " What you can do:");
|
|
1567
1568
|
if (lock.launch === "tmux" && lock.launchDetail) lines.push(` tmux attach -t ${lock.launchDetail} \u2014 go to the one that is running`);
|
|
1569
|
+
if (lock.launch === "background") lines.push(` pags down \u2014 stop it (it has no window to close)`);
|
|
1568
1570
|
lines.push(
|
|
1569
1571
|
` pags status \u2014 what it is doing, and anything else running here`,
|
|
1570
1572
|
lock.launch === "service" ? ` (this one is a service \u2014 stop its launchd/systemd unit rather than replacing it)` : ` pags up --replace \u2014 stop that one gracefully, then take over`
|
|
@@ -1573,6 +1575,7 @@ function holderMessage(lock, verdict) {
|
|
|
1573
1575
|
}
|
|
1574
1576
|
function detectLaunch(env = process.env, isTty = Boolean(process.stdout.isTTY)) {
|
|
1575
1577
|
if (env.PAGS_SERVICE === "1") return { launch: "service", detail: env.PAGS_SERVICE_NAME?.trim() || "" };
|
|
1578
|
+
if (env.PAGS_BACKGROUND === "1") return { launch: "background", detail: env.PAGS_BACKGROUND_LOG?.trim() || "" };
|
|
1576
1579
|
if (env.TMUX) return { launch: "tmux", detail: env.TMUX_PANE?.trim() || env.TMUX.split(",")[0]?.split("/").pop() || "" };
|
|
1577
1580
|
if (!isTty) return { launch: "headless", detail: "" };
|
|
1578
1581
|
return { launch: "tty", detail: env.TERM_SESSION_ID?.trim() || env.WINDOWID?.trim() || "" };
|
|
@@ -3094,9 +3097,148 @@ function createRunnerCommand() {
|
|
|
3094
3097
|
// src/commands/runner.ts
|
|
3095
3098
|
var runnerCommand = createRunnerCommand();
|
|
3096
3099
|
|
|
3100
|
+
// src/commands/status.ts
|
|
3101
|
+
import { readFileSync as readFileSync9, statSync as statSync2, watchFile } from "fs";
|
|
3102
|
+
import { Command as Command9 } from "commander";
|
|
3103
|
+
|
|
3104
|
+
// src/background.ts
|
|
3105
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
3106
|
+
import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync8, renameSync as renameSync3, statSync, writeFileSync as writeFileSync6 } from "fs";
|
|
3107
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
3108
|
+
var BACKGROUND_ENV = "PAGS_BACKGROUND";
|
|
3109
|
+
var BACKGROUND_LOG_ENV = "PAGS_BACKGROUND_LOG";
|
|
3110
|
+
var BACKGROUND_RESTART_ENV = "PAGS_BACKGROUND_RESTART";
|
|
3111
|
+
var LOG_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
3112
|
+
var backgroundLogPath = (account, env = process.env) => join9(configDir(env), "logs", `runner-${accountKey(account)}.log`);
|
|
3113
|
+
var backgroundStatePath = (account, env = process.env) => join9(configDir(env), `background-${accountKey(account)}.json`);
|
|
3114
|
+
function writeBackgroundState(path, state) {
|
|
3115
|
+
mkdirSync7(dirname4(path), { recursive: true });
|
|
3116
|
+
writeFileSync6(path, `${JSON.stringify(state)}
|
|
3117
|
+
`, { mode: 384 });
|
|
3118
|
+
}
|
|
3119
|
+
function readBackgroundState(path) {
|
|
3120
|
+
try {
|
|
3121
|
+
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
3122
|
+
return parsed?.v === 1 && Number.isInteger(parsed.pid) ? parsed : null;
|
|
3123
|
+
} catch {
|
|
3124
|
+
return null;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
function rotateLog(path, maxBytes = LOG_ROTATE_BYTES) {
|
|
3128
|
+
try {
|
|
3129
|
+
if (statSync(path).size <= maxBytes) return false;
|
|
3130
|
+
} catch {
|
|
3131
|
+
return false;
|
|
3132
|
+
}
|
|
3133
|
+
renameSync3(path, `${path}.1`);
|
|
3134
|
+
return true;
|
|
3135
|
+
}
|
|
3136
|
+
function shellQuote(arg, platform = process.platform) {
|
|
3137
|
+
if (platform === "win32") return `"${arg.replace(/"/g, '""')}"`;
|
|
3138
|
+
return /^[A-Za-z0-9_./:=@%+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
3139
|
+
}
|
|
3140
|
+
function backgroundRestartCommand(execPath, script, upArgs, platform = process.platform) {
|
|
3141
|
+
const argv = [execPath, script, ...upArgs, "--background"].map((a) => shellQuote(a, platform)).join(" ");
|
|
3142
|
+
return platform === "win32" ? `set ${BACKGROUND_RESTART_ENV}=1&& ${argv}` : `${BACKGROUND_RESTART_ENV}=1 ${argv}`;
|
|
3143
|
+
}
|
|
3144
|
+
function spawnDetached(command, args, env, logPath, spawn5 = nodeSpawn) {
|
|
3145
|
+
mkdirSync7(dirname4(logPath), { recursive: true });
|
|
3146
|
+
rotateLog(logPath);
|
|
3147
|
+
const fd = openSync2(logPath, "a", 384);
|
|
3148
|
+
try {
|
|
3149
|
+
const child = spawn5(command, args, { detached: true, stdio: ["ignore", fd, fd], env, windowsHide: true });
|
|
3150
|
+
child.unref();
|
|
3151
|
+
if (!child.pid) throw new Error("the runner process did not start");
|
|
3152
|
+
return child.pid;
|
|
3153
|
+
} finally {
|
|
3154
|
+
closeSync2(fd);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3158
|
+
async function waitForBackgroundHolder(childPid, read, alive, opts = {}) {
|
|
3159
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
|
|
3160
|
+
while (Date.now() < deadline) {
|
|
3161
|
+
if (!alive(childPid)) return "exited";
|
|
3162
|
+
const held = await read();
|
|
3163
|
+
if (held.lock?.pid === childPid && held.verdict === "live") return "live";
|
|
3164
|
+
await sleep(opts.intervalMs ?? 500);
|
|
3165
|
+
}
|
|
3166
|
+
return alive(childPid) ? "timeout" : "exited";
|
|
3167
|
+
}
|
|
3168
|
+
async function waitUntilTakeable(read, opts = {}) {
|
|
3169
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 15e3);
|
|
3170
|
+
for (; ; ) {
|
|
3171
|
+
const held = await read();
|
|
3172
|
+
if (!held.lock || isTakeable(held.verdict)) return true;
|
|
3173
|
+
if (Date.now() >= deadline) return false;
|
|
3174
|
+
await sleep(opts.intervalMs ?? 500);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
function tailLines(path, n) {
|
|
3178
|
+
if (!existsSync8(path)) return null;
|
|
3179
|
+
const lines = readFileSync8(path, "utf-8").split("\n");
|
|
3180
|
+
if (lines.at(-1) === "") lines.pop();
|
|
3181
|
+
return lines.slice(Math.max(0, lines.length - n));
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// src/commands/status.ts
|
|
3185
|
+
function describeStatus(lock, verdict, bg, now = Date.now()) {
|
|
3186
|
+
if (!lock || verdict === "dead") {
|
|
3187
|
+
const lines2 = ["Not running \u2014 no runner of yours is connected from this machine."];
|
|
3188
|
+
if (bg) lines2.push(`The last background runner (pid ${bg.pid}, started ${new Date(bg.startedAt).toLocaleString()}) has stopped. Its log: ${bg.logPath} (\`pags logs\`).`);
|
|
3189
|
+
lines2.push("", "Start one with `pags up` (this terminal) or `pags up --background` (no terminal).");
|
|
3190
|
+
return { running: false, lines: lines2 };
|
|
3191
|
+
}
|
|
3192
|
+
const mins = Math.max(0, Math.round((now - lock.startedAt) / 6e4));
|
|
3193
|
+
const state = verdict === "live" ? "Running" : verdict === "unresponsive" ? "Running, but NOT answering its health check (asleep, stopped or overloaded)" : "Probably running (its lock could not be fully read)";
|
|
3194
|
+
const lines = [
|
|
3195
|
+
`${state} \u2014 pid ${lock.pid}, up ${mins < 120 ? `${mins} min` : `${Math.round(mins / 60)} h`}, CLI ${lock.version || "?"}, machine ${lock.node}.`,
|
|
3196
|
+
`Launched from ${holderWhere(lock)}.`
|
|
3197
|
+
];
|
|
3198
|
+
if (lock.launch === "background" && bg && bg.pid === lock.pid) {
|
|
3199
|
+
lines.push(`Started with ${bg.instances.length} agent${bg.instances.length === 1 ? "" : "s"}${bg.watching ? " (newly eligible ones attach as they appear)" : " (scoped: only these)"}:`);
|
|
3200
|
+
for (const i of bg.instances) lines.push(` ${i.name} (${i.id.slice(0, 8)})`);
|
|
3201
|
+
lines.push("", `Logs: \`pags logs\` (${bg.logPath}). Stop: \`pags down\`.`);
|
|
3202
|
+
} else if (lock.launch === "background") {
|
|
3203
|
+
lines.push("", "Logs: `pags logs`. Stop: `pags down`.");
|
|
3204
|
+
}
|
|
3205
|
+
return { running: true, lines };
|
|
3206
|
+
}
|
|
3207
|
+
var statusCommand = new Command9("status").description("Is a runner running on this machine for your account \u2014 and how was it started").action(async () => {
|
|
3208
|
+
const session = requireSession();
|
|
3209
|
+
const held = await inspectHolder(lockPath(session.user.login));
|
|
3210
|
+
const { running, lines } = describeStatus(held.lock, held.lock ? held.verdict : null, readBackgroundState(backgroundStatePath(session.user.login)));
|
|
3211
|
+
writeLine("");
|
|
3212
|
+
for (const line of lines) writeLine(` ${line}`);
|
|
3213
|
+
writeLine("");
|
|
3214
|
+
if (!running) process.exitCode = 1;
|
|
3215
|
+
});
|
|
3216
|
+
var logsCommand = new Command9("logs").description("Show the background runner's log (`pags up --background`)").option("-n, --lines <n>", "How many recent lines to show", "50").option("-f, --follow", "Keep printing new lines as they are written").action((opts) => {
|
|
3217
|
+
const session = requireSession();
|
|
3218
|
+
const path = backgroundLogPath(session.user.login);
|
|
3219
|
+
const n = Math.max(1, Number.parseInt(opts.lines, 10) || 50);
|
|
3220
|
+
const lines = tailLines(path, n);
|
|
3221
|
+
if (!lines) {
|
|
3222
|
+
writeLine("");
|
|
3223
|
+
writeLine(" No background runner log here yet \u2014 one is written by `pags up --background`.");
|
|
3224
|
+
writeLine(" A foreground `pags up` shows its output in its own window (press `l`).");
|
|
3225
|
+
writeLine("");
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
for (const line of lines) writeLine(line);
|
|
3229
|
+
if (!opts.follow) return;
|
|
3230
|
+
let offset = statSync2(path).size;
|
|
3231
|
+
watchFile(path, { interval: 1e3 }, (cur) => {
|
|
3232
|
+
if (cur.size < offset) offset = 0;
|
|
3233
|
+
if (cur.size === offset) return;
|
|
3234
|
+
process.stdout.write(readFileSync9(path).subarray(offset, cur.size));
|
|
3235
|
+
offset = cur.size;
|
|
3236
|
+
});
|
|
3237
|
+
});
|
|
3238
|
+
|
|
3097
3239
|
// src/commands/up.ts
|
|
3098
3240
|
import { createRequire as createRequire2 } from "module";
|
|
3099
|
-
import { Command as
|
|
3241
|
+
import { Command as Command10 } from "commander";
|
|
3100
3242
|
import { hostname as hostname7 } from "os";
|
|
3101
3243
|
|
|
3102
3244
|
// src/tui.ts
|
|
@@ -3217,7 +3359,7 @@ async function waitForKey(keys, onInterrupt) {
|
|
|
3217
3359
|
}
|
|
3218
3360
|
|
|
3219
3361
|
// src/commands/up-restart.ts
|
|
3220
|
-
import { spawn as
|
|
3362
|
+
import { spawn as nodeSpawn2, spawnSync as nodeSpawnSync } from "child_process";
|
|
3221
3363
|
var UP_LAUNCHER_ENV = "PAGS_UP_LAUNCHER";
|
|
3222
3364
|
function upRestartMode(env, platform, canExecve) {
|
|
3223
3365
|
if (env[SERVICE_ENV] === "1") return "service";
|
|
@@ -3234,7 +3376,7 @@ function processRestartDeps() {
|
|
|
3234
3376
|
execArgv: process.execArgv,
|
|
3235
3377
|
script: process.argv[1],
|
|
3236
3378
|
execve: typeof execve === "function" ? execve.bind(process) : void 0,
|
|
3237
|
-
spawn:
|
|
3379
|
+
spawn: nodeSpawn2,
|
|
3238
3380
|
spawnSync: nodeSpawnSync,
|
|
3239
3381
|
exit: (code) => process.exit(code),
|
|
3240
3382
|
log: () => {
|
|
@@ -3323,7 +3465,7 @@ async function holderWork(lock, deps = {}) {
|
|
|
3323
3465
|
}
|
|
3324
3466
|
async function replaceHolder(lock, opts = {}, deps = {}) {
|
|
3325
3467
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
3326
|
-
const
|
|
3468
|
+
const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
3327
3469
|
const alive = deps.alive ?? pidAlive;
|
|
3328
3470
|
const now = deps.now ?? Date.now;
|
|
3329
3471
|
const work = await holderWork(lock, deps);
|
|
@@ -3348,7 +3490,7 @@ async function replaceHolder(lock, opts = {}, deps = {}) {
|
|
|
3348
3490
|
const until = now() + REPLACE_EXIT_TIMEOUT_MS;
|
|
3349
3491
|
while (now() < until) {
|
|
3350
3492
|
if (!alive(lock.pid)) return { ok: true, lines: [`Replaced the \`pags up\` on pid ${lock.pid}`] };
|
|
3351
|
-
await
|
|
3493
|
+
await sleep2(500);
|
|
3352
3494
|
}
|
|
3353
3495
|
return {
|
|
3354
3496
|
ok: false,
|
|
@@ -3359,10 +3501,67 @@ async function replaceHolder(lock, opts = {}, deps = {}) {
|
|
|
3359
3501
|
};
|
|
3360
3502
|
}
|
|
3361
3503
|
|
|
3504
|
+
// src/commands/up-background.ts
|
|
3505
|
+
import { spawn as nodeSpawn3 } from "child_process";
|
|
3506
|
+
var processDeps = () => ({
|
|
3507
|
+
env: process.env,
|
|
3508
|
+
execPath: process.execPath,
|
|
3509
|
+
platform: process.platform,
|
|
3510
|
+
spawn: nodeSpawn3,
|
|
3511
|
+
alive: pidAlive,
|
|
3512
|
+
inspect: inspectHolder,
|
|
3513
|
+
log: (line) => writeLine(line),
|
|
3514
|
+
step: printStep
|
|
3515
|
+
});
|
|
3516
|
+
async function startInBackground(start, deps = processDeps()) {
|
|
3517
|
+
const account = start.session.user.login;
|
|
3518
|
+
const logPath = backgroundLogPath(account, deps.env);
|
|
3519
|
+
const upArgs = restartUpArgs(start.opts);
|
|
3520
|
+
const env = {
|
|
3521
|
+
...deps.env,
|
|
3522
|
+
PAGS_TOKEN: start.session.token,
|
|
3523
|
+
PAGS_LOCK_ACCOUNT: account,
|
|
3524
|
+
[BACKGROUND_ENV]: "1",
|
|
3525
|
+
[BACKGROUND_LOG_ENV]: logPath,
|
|
3526
|
+
[RESTART_COMMAND_ENV]: backgroundRestartCommand(deps.execPath, start.runnerArgs[0], upArgs, deps.platform)
|
|
3527
|
+
};
|
|
3528
|
+
delete env.PAGS_UP_SUPERVISED;
|
|
3529
|
+
deps.step("Starting the runner in the background\u2026", "wait");
|
|
3530
|
+
const pid = spawnDetached(deps.execPath, start.runnerArgs, env, logPath, deps.spawn);
|
|
3531
|
+
writeBackgroundState(backgroundStatePath(account, deps.env), {
|
|
3532
|
+
v: 1,
|
|
3533
|
+
pid,
|
|
3534
|
+
startedAt: Date.now(),
|
|
3535
|
+
logPath,
|
|
3536
|
+
args: [...upArgs, "--background"],
|
|
3537
|
+
instances: start.instances,
|
|
3538
|
+
watching: start.watching
|
|
3539
|
+
});
|
|
3540
|
+
const outcome = await waitForBackgroundHolder(pid, () => deps.inspect(start.lockFile), deps.alive, { timeoutMs: deps.timeoutMs });
|
|
3541
|
+
if (outcome === "live") {
|
|
3542
|
+
deps.step(`Running in the background (pid ${pid}) \u2014 this terminal can be closed`, "ok");
|
|
3543
|
+
deps.log("");
|
|
3544
|
+
deps.log(` Serving ${start.instances.length} agent${start.instances.length === 1 ? "" : "s"}${start.watching ? ", and attaching newly eligible ones" : ""}.`);
|
|
3545
|
+
deps.log(" It keeps running until `pags down`, a reboot, or logging out \u2014 it does not start by itself.");
|
|
3546
|
+
deps.log("");
|
|
3547
|
+
deps.log(" pags status \u2014 is it running, and what is it serving");
|
|
3548
|
+
deps.log(` pags logs \u2014 its output (${logPath})`);
|
|
3549
|
+
deps.log(" pags down \u2014 stop it");
|
|
3550
|
+
deps.log("");
|
|
3551
|
+
return 0;
|
|
3552
|
+
}
|
|
3553
|
+
deps.step(outcome === "exited" ? "The background runner stopped before it was ready" : "The background runner did not confirm it was ready", "fail");
|
|
3554
|
+
for (const line of tailLines(logPath, 10) ?? []) deps.log(` ${line}`);
|
|
3555
|
+
deps.log("");
|
|
3556
|
+
deps.log(outcome === "exited" ? " Fix what the log says, then `pags up --background` again." : ` It may still be starting (pid ${pid}): check \`pags status\`, or stop it with \`pags down\`.`);
|
|
3557
|
+
deps.log("");
|
|
3558
|
+
return 1;
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3362
3561
|
// src/commands/up.ts
|
|
3363
3562
|
var API_BASE3 = "https://api.proagentstore.online";
|
|
3364
3563
|
var CLI_VERSION2 = createRequire2(import.meta.url)("../package.json").version;
|
|
3365
|
-
var upCommand = new
|
|
3564
|
+
var upCommand = new Command10("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("--force", "Take over from another connected machine").option("--replace", "Stop the `pags up` already running here for this account, then take over").option("--now", "With --replace: stop it even though work is running on it (that work is lost)").option("--background", "Run the runner in the background, with no terminal open (stop it with `pags down`)").action(async (opts) => {
|
|
3366
3565
|
const session = requireSession();
|
|
3367
3566
|
const state = {
|
|
3368
3567
|
user: session.user.login,
|
|
@@ -3439,6 +3638,7 @@ var upCommand = new Command9("up").description("Start the browser runner for all
|
|
|
3439
3638
|
state.activeInstance = instances.length === 1 ? instances[0].name || instances[0].slug || instances[0].id.slice(0, 8) : `${instances.length} agents`;
|
|
3440
3639
|
printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
|
|
3441
3640
|
const lockFile = lockPath(session.user.login);
|
|
3641
|
+
if (opts.background && process.env[BACKGROUND_RESTART_ENV] === "1") await waitUntilTakeable(() => inspectHolder(lockFile));
|
|
3442
3642
|
const held = await inspectHolder(lockFile);
|
|
3443
3643
|
if (held.lock && !isTakeable(held.verdict)) {
|
|
3444
3644
|
if (!opts.replace) {
|
|
@@ -3464,6 +3664,10 @@ var upCommand = new Command9("up").description("Start the browser runner for all
|
|
|
3464
3664
|
if (opts.headless) args.push("--headless");
|
|
3465
3665
|
if (opts.force) args.push("--force");
|
|
3466
3666
|
if (!opts.instance) args.push("--watch-instances");
|
|
3667
|
+
if (opts.background) {
|
|
3668
|
+
const code = await startInBackground({ session, opts, runnerArgs: args, instances: state.instances, lockFile, watching: !opts.instance });
|
|
3669
|
+
process.exit(code);
|
|
3670
|
+
}
|
|
3467
3671
|
const child = spawn5(process.execPath, args, {
|
|
3468
3672
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3469
3673
|
// The child is the lock HOLDER (#896): it owns the relay sockets, so it is what a
|
|
@@ -3600,7 +3804,7 @@ var upCommand = new Command9("up").description("Start the browser runner for all
|
|
|
3600
3804
|
}
|
|
3601
3805
|
}
|
|
3602
3806
|
});
|
|
3603
|
-
var downCommand = new
|
|
3807
|
+
var downCommand = new Command10("down").description("Stop the browser runner and disconnect").option("--now", "Stop it even though work is running on it (that work is lost)").action(async (opts) => {
|
|
3604
3808
|
clearScreen();
|
|
3605
3809
|
printLogo(CLI_VERSION2);
|
|
3606
3810
|
const session = requireSession();
|
|
@@ -3625,7 +3829,7 @@ var downCommand = new Command9("down").description("Stop the browser runner and
|
|
|
3625
3829
|
// src/index.ts
|
|
3626
3830
|
var require2 = createRequire3(import.meta.url);
|
|
3627
3831
|
var { version } = require2("../package.json");
|
|
3628
|
-
var program = new
|
|
3832
|
+
var program = new Command11();
|
|
3629
3833
|
program.name("pags").description(
|
|
3630
3834
|
"ProAgentStore CLI \u2014 create and publish server-powered AI agents"
|
|
3631
3835
|
).version(version);
|
|
@@ -3634,6 +3838,8 @@ program.addCommand(logoutCommand);
|
|
|
3634
3838
|
program.addCommand(whoamiCommand);
|
|
3635
3839
|
program.addCommand(upCommand);
|
|
3636
3840
|
program.addCommand(downCommand);
|
|
3841
|
+
program.addCommand(statusCommand);
|
|
3842
|
+
program.addCommand(logsCommand);
|
|
3637
3843
|
program.addCommand(initCommand);
|
|
3638
3844
|
program.addCommand(checkCommand);
|
|
3639
3845
|
program.addCommand(publishCommand);
|