@proagentstore/cli 0.4.44 → 0.4.46
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 +7 -8
- package/dist/browser-runner/coding/runtime.js +11 -3
- package/dist/browser-runner/coding/tmux.js +41 -0
- package/dist/browser-runner/runner.js +19 -1
- package/dist/browser-runner/server.js +54 -11
- package/dist/index.js +489 -145
- package/package.json +1 -1
|
@@ -470,16 +470,15 @@ export class HeadlessSession {
|
|
|
470
470
|
});
|
|
471
471
|
}
|
|
472
472
|
/**
|
|
473
|
-
* No TTY in headless mode; control is via messages.
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
* success: `act` returned an ordinary snapshot with an unchanged pane, so the caller could not
|
|
478
|
-
* tell "sent, nothing happened" from "never sent". The transcript is what the brain and the
|
|
479
|
-
* console both read, so the truth belongs there.
|
|
473
|
+
* No TTY in headless mode; control is via messages. RECORDED **and** REPORTED: recording came
|
|
474
|
+
* first (#391) because a pure no-op read as success and the transcript is what the brain and
|
|
475
|
+
* the console see — but a line in the pane is no answer to the caller, `runtime.act` had
|
|
476
|
+
* nothing to raise, so the route answered 200 (#448). A real PTY backend flips `delivered`.
|
|
480
477
|
*/
|
|
481
478
|
key(keys) {
|
|
482
|
-
|
|
479
|
+
const reason = "this session has no terminal attached";
|
|
480
|
+
this.push(`[ignored keypress ${keys.slice(0, 40)} — ${reason}]`);
|
|
481
|
+
return { delivered: false, reason };
|
|
483
482
|
}
|
|
484
483
|
/** Abort the current turn (SIGINT, like Ctrl-C). The process stays usable. */
|
|
485
484
|
interrupt() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { RunnerInputError } from "../errors.js";
|
|
3
4
|
import { defaultStatePath, HeadlessSession } from "./headless.js";
|
|
4
5
|
import { InspectError, readGitRemoteOrigin, readRepoFile, repoTree, runRepoGit } from "./inspect.js";
|
|
5
6
|
import { checkWorkdir, ensureRepo, sanitizeSessionName } from "./repo.js";
|
|
@@ -135,9 +136,16 @@ export class CodingRuntime {
|
|
|
135
136
|
case "message":
|
|
136
137
|
session.input(action.text);
|
|
137
138
|
break;
|
|
138
|
-
case "keys":
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
case "keys": {
|
|
140
|
+
// A snapshot is no longer the whole answer (#448). `key()` records the attempt and
|
|
141
|
+
// reports that it was not delivered; answering 200 with a pane that simply did not
|
|
142
|
+
// change is the defect this replaces — a caller cannot tell it apart from success.
|
|
143
|
+
// `RunnerInputError` (400) is the honest class: with no PTY, asking this runner for
|
|
144
|
+
// a keystroke is a bad request, not a runner fault. The cloud refuses it a step
|
|
145
|
+
// earlier with a 409, so in practice this only catches a direct runner caller.
|
|
146
|
+
const { reason } = session.key(action.keys);
|
|
147
|
+
throw new RunnerInputError(`Keystrokes are not deliverable: ${reason} — send an instruction instead, or take the session over.`);
|
|
148
|
+
}
|
|
141
149
|
case "interrupt":
|
|
142
150
|
session.interrupt();
|
|
143
151
|
break;
|
|
@@ -111,6 +111,47 @@ export function runCommand(target, command) {
|
|
|
111
111
|
sendText(target, command);
|
|
112
112
|
sendKey(target, "Enter");
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Settle heuristic constants — mirror the Coder headless.ts values (1.5s quiet = idle;
|
|
116
|
+
* 8s absolute backstop for a slow-booting CLI). The short backstop covers send/run where
|
|
117
|
+
* the pane is already live; the long one is for new-session launches where the CLI may
|
|
118
|
+
* take several seconds to paint its first prompt.
|
|
119
|
+
*/
|
|
120
|
+
export const SETTLE_QUIET_MS = 750;
|
|
121
|
+
export const SETTLE_POLL_MS = 120;
|
|
122
|
+
export const SETTLE_TIMEOUT_MS = 8_000;
|
|
123
|
+
/**
|
|
124
|
+
* Poll-capture a pane until its content is unchanged for `quietMs` ms, or until
|
|
125
|
+
* `timeoutMs` elapses (backstop so a continuously-animated pane can't hang the tool).
|
|
126
|
+
*
|
|
127
|
+
* Returns the final pane content. This is the write-side analogue of the read-side labels
|
|
128
|
+
* in `terminal-label.ts`: before returning "Sent", we verify the pane has reacted.
|
|
129
|
+
*
|
|
130
|
+
* Pure behaviour — no side effects beyond calling `capturePane`; tested in unit tests
|
|
131
|
+
* without a real tmux by passing a custom `captureFn`.
|
|
132
|
+
*/
|
|
133
|
+
export async function waitForPaneSettle(target, opts = {}) {
|
|
134
|
+
const quietMs = opts.quietMs ?? SETTLE_QUIET_MS;
|
|
135
|
+
const timeoutMs = opts.timeoutMs ?? SETTLE_TIMEOUT_MS;
|
|
136
|
+
const pollMs = opts.pollMs ?? SETTLE_POLL_MS;
|
|
137
|
+
const capture = opts.captureFn ?? ((t) => capturePane(t));
|
|
138
|
+
const deadline = Date.now() + timeoutMs;
|
|
139
|
+
let last = capture(target);
|
|
140
|
+
let lastChangedAt = Date.now();
|
|
141
|
+
while (true) {
|
|
142
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
const current = capture(target);
|
|
145
|
+
if (current !== last) {
|
|
146
|
+
last = current;
|
|
147
|
+
lastChangedAt = now;
|
|
148
|
+
}
|
|
149
|
+
const quietFor = now - lastChangedAt;
|
|
150
|
+
if (quietFor >= quietMs || now >= deadline) {
|
|
151
|
+
return last;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
114
155
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: matching ANSI escape codes from tmux output.
|
|
115
156
|
const ANSI = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
116
157
|
/** Strip ANSI escape codes. */
|
|
@@ -590,7 +590,25 @@ export class LocalRunner {
|
|
|
590
590
|
// --remote-debugging-port=0 → Chrome picks a free CDP port and writes it to
|
|
591
591
|
// DevToolsActivePort in the profile dir; the standard @playwright/mcp server
|
|
592
592
|
// attaches to that endpoint so it drives THIS same real-profile browser.
|
|
593
|
-
|
|
593
|
+
// The media-permission pair is POLICY, not a workaround (#425). Nothing in this package
|
|
594
|
+
// uses audio — `grep -niE "microphone|grantPermissions" src` finds nothing — and the
|
|
595
|
+
// runner navigates only to job/ATS pages, so any mic prompt from here is a third-party
|
|
596
|
+
// site asking for something no part of this product needs. Both flags, and in this
|
|
597
|
+
// order of reasoning:
|
|
598
|
+
// --use-fake-ui-for-media-stream auto-answers the prompt, so the UI never appears…
|
|
599
|
+
// --use-fake-device-for-media-stream …and hands over a SYNTHETIC device, because the
|
|
600
|
+
// first flag ALONE auto-GRANTS the real microphone to whatever page asked. That is
|
|
601
|
+
// strictly worse than the prompt it removes, which is why it must never ship alone.
|
|
602
|
+
// The console, where voice actually runs, is a different browser and is untouched: a
|
|
603
|
+
// real mic grant stays the user's decision.
|
|
604
|
+
args: [
|
|
605
|
+
"--disable-blink-features=AutomationControlled",
|
|
606
|
+
"--start-maximized",
|
|
607
|
+
"--window-size=1512,982",
|
|
608
|
+
"--remote-debugging-port=0",
|
|
609
|
+
"--use-fake-ui-for-media-stream",
|
|
610
|
+
"--use-fake-device-for-media-stream",
|
|
611
|
+
],
|
|
594
612
|
};
|
|
595
613
|
// Prefer the real Chrome build (better TLS/fingerprint → fewer CAPTCHAs);
|
|
596
614
|
// fall back to bundled Chromium if Chrome isn't installed. Disable with
|
|
@@ -305,24 +305,29 @@ async function route(runner, req, res) {
|
|
|
305
305
|
return json(res, 200, { session, pane: capturePane(session, lines) });
|
|
306
306
|
}
|
|
307
307
|
if (req.method === "POST" && path === "/tmux/send") {
|
|
308
|
-
const { sendText, sendKey, capturePane, sessionExists } = await import("./coding/tmux.js");
|
|
308
|
+
const { sendText, sendKey, capturePane, sessionExists, waitForPaneSettle } = await import("./coding/tmux.js");
|
|
309
309
|
const b = await readJson(req);
|
|
310
310
|
const session = String(b.session || "").trim();
|
|
311
311
|
if (!session)
|
|
312
312
|
return json(res, 400, { error: "A `session` name is required." });
|
|
313
313
|
if (!sessionExists(session))
|
|
314
314
|
return json(res, 404, { error: `No tmux session "${session}".` });
|
|
315
|
+
// Capture BEFORE the send so the caller can verify what changed (#481).
|
|
316
|
+
const paneBefore = capturePane(session, 200);
|
|
315
317
|
if (b.text != null)
|
|
316
318
|
sendText(session, String(b.text));
|
|
317
319
|
for (const k of b.keys ?? [])
|
|
318
320
|
sendKey(session, String(k));
|
|
321
|
+
// Wait for the pane to quiesce (750ms quiet / 3s backstop) instead of returning
|
|
322
|
+
// the pre-reaction snapshot. Mirrors the settle heuristic in headless.ts.
|
|
323
|
+
const pane = await waitForPaneSettle(session, { quietMs: 750, timeoutMs: 3_000 });
|
|
319
324
|
// `activeCommand` rides along on every WRITE so the cloud can record what it just drove
|
|
320
325
|
// (#348). It is a process name, never a cost — see activeTerminalCommand's comment.
|
|
321
326
|
const { activeTerminalCommand } = await import("./coding/terminal.js");
|
|
322
|
-
return json(res, 200, { session, pane
|
|
327
|
+
return json(res, 200, { session, pane, paneBefore, changed: pane !== paneBefore, activeCommand: activeTerminalCommand(session, "tmux") });
|
|
323
328
|
}
|
|
324
329
|
if (req.method === "POST" && path === "/tmux/run") {
|
|
325
|
-
const { runCommand, capturePane, sessionExists } = await import("./coding/tmux.js");
|
|
330
|
+
const { runCommand, capturePane, sessionExists, waitForPaneSettle } = await import("./coding/tmux.js");
|
|
326
331
|
const b = await readJson(req);
|
|
327
332
|
const session = String(b.session || "").trim();
|
|
328
333
|
const command = String(b.command ?? "");
|
|
@@ -332,12 +337,14 @@ async function route(runner, req, res) {
|
|
|
332
337
|
return json(res, 400, { error: "A `command` is required." });
|
|
333
338
|
if (!sessionExists(session))
|
|
334
339
|
return json(res, 404, { error: `No tmux session "${session}".` });
|
|
340
|
+
const paneBefore = capturePane(session, 200);
|
|
335
341
|
runCommand(session, command);
|
|
342
|
+
const pane = await waitForPaneSettle(session, { quietMs: 750, timeoutMs: 3_000 });
|
|
336
343
|
const { activeTerminalCommand } = await import("./coding/terminal.js");
|
|
337
|
-
return json(res, 200, { session, command, pane
|
|
344
|
+
return json(res, 200, { session, command, pane, paneBefore, changed: pane !== paneBefore, activeCommand: activeTerminalCommand(session, "tmux") });
|
|
338
345
|
}
|
|
339
346
|
if (req.method === "POST" && path === "/tmux/session") {
|
|
340
|
-
const { createSession, killSession, sessionExists } = await import("./coding/tmux.js");
|
|
347
|
+
const { createSession, killSession, sessionExists, waitForPaneSettle } = await import("./coding/tmux.js");
|
|
341
348
|
const { homedir } = await import("node:os");
|
|
342
349
|
const b = await readJson(req);
|
|
343
350
|
const session = String(b.session || "").trim();
|
|
@@ -352,6 +359,12 @@ async function route(runner, req, res) {
|
|
|
352
359
|
const { resolve } = await import("node:path");
|
|
353
360
|
const workDir = resolve(String(b.workDir || "~").replace(/^~(?=$|\/)/, homedir()));
|
|
354
361
|
createSession(session, workDir, b.command ? String(b.command) : undefined);
|
|
362
|
+
// When the session starts a command (e.g. "claude"), wait until the pane quiesces
|
|
363
|
+
// (the CLI has painted its initial prompt) before returning "ready" (#481). Without a
|
|
364
|
+
// startup command the pane is already at a shell prompt — no settle needed.
|
|
365
|
+
if (b.command) {
|
|
366
|
+
await waitForPaneSettle(session, { quietMs: 750, timeoutMs: 8_000 });
|
|
367
|
+
}
|
|
355
368
|
return json(res, 200, { session, created: true, workDir });
|
|
356
369
|
}
|
|
357
370
|
// ── generic terminal connector ──────────────────────────────────────────
|
|
@@ -373,7 +386,8 @@ async function route(runner, req, res) {
|
|
|
373
386
|
return json(res, 200, { target, pane: captureTerminalTarget(target, { backend, lines: b.lines }) });
|
|
374
387
|
}
|
|
375
388
|
if (req.method === "POST" && path === "/terminal/run") {
|
|
376
|
-
const { runTerminalCommand, activeTerminalCommand } = await import("./coding/terminal.js");
|
|
389
|
+
const { runTerminalCommand, captureTerminalTarget, activeTerminalCommand } = await import("./coding/terminal.js");
|
|
390
|
+
const { waitForPaneSettle, SETTLE_QUIET_MS } = await import("./coding/tmux.js");
|
|
377
391
|
const b = await readJson(req);
|
|
378
392
|
const target = String(b.target || "").trim();
|
|
379
393
|
const command = String(b.command ?? "");
|
|
@@ -382,23 +396,48 @@ async function route(runner, req, res) {
|
|
|
382
396
|
if (!command.trim())
|
|
383
397
|
return json(res, 400, { error: "A `command` is required." });
|
|
384
398
|
const backend = b.backend === "tmux" || b.backend === "kitty" || b.backend === "iterm2" ? b.backend : undefined;
|
|
385
|
-
|
|
399
|
+
// Capture BEFORE to detect whether the input landed (#481).
|
|
400
|
+
const paneBefore = captureTerminalTarget(target, { backend });
|
|
401
|
+
runTerminalCommand(target, command, backend);
|
|
402
|
+
// Settle: for tmux targets, use the tmux poll. For other backends, fall back to the
|
|
403
|
+
// just-run snapshot (they don't support a reliable settle poll).
|
|
404
|
+
let pane;
|
|
405
|
+
if (!backend && target.startsWith("tmux:") || backend === "tmux") {
|
|
406
|
+
const { splitTerminalTarget } = await import("./coding/terminal.js");
|
|
407
|
+
const t = splitTerminalTarget(target, backend);
|
|
408
|
+
pane = await waitForPaneSettle(t.id, { quietMs: SETTLE_QUIET_MS, timeoutMs: 3_000 });
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
pane = captureTerminalTarget(target, { backend });
|
|
412
|
+
}
|
|
386
413
|
// Read AFTER the command lands, so `claude "fix x"` reports `claude` rather than the shell
|
|
387
414
|
// that was sitting there a moment earlier (#348).
|
|
388
|
-
return json(res, 200, { target, command, pane, activeCommand: activeTerminalCommand(target, backend) });
|
|
415
|
+
return json(res, 200, { target, command, pane, paneBefore, changed: pane !== paneBefore, activeCommand: activeTerminalCommand(target, backend) });
|
|
389
416
|
}
|
|
390
417
|
if (req.method === "POST" && path === "/terminal/send") {
|
|
391
|
-
const { sendTerminalKeys, activeTerminalCommand } = await import("./coding/terminal.js");
|
|
418
|
+
const { sendTerminalKeys, captureTerminalTarget, activeTerminalCommand } = await import("./coding/terminal.js");
|
|
419
|
+
const { waitForPaneSettle, SETTLE_QUIET_MS } = await import("./coding/tmux.js");
|
|
392
420
|
const b = await readJson(req);
|
|
393
421
|
const target = String(b.target || "").trim();
|
|
394
422
|
if (!target)
|
|
395
423
|
return json(res, 400, { error: "A `target` is required." });
|
|
396
424
|
const backend = b.backend === "tmux" || b.backend === "kitty" || b.backend === "iterm2" ? b.backend : undefined;
|
|
397
|
-
const
|
|
398
|
-
|
|
425
|
+
const paneBefore = captureTerminalTarget(target, { backend });
|
|
426
|
+
sendTerminalKeys(target, { backend, text: b.text == null ? undefined : String(b.text), keys: b.keys ?? [] });
|
|
427
|
+
let pane;
|
|
428
|
+
if (!backend && target.startsWith("tmux:") || backend === "tmux") {
|
|
429
|
+
const { splitTerminalTarget } = await import("./coding/terminal.js");
|
|
430
|
+
const t = splitTerminalTarget(target, backend);
|
|
431
|
+
pane = await waitForPaneSettle(t.id, { quietMs: SETTLE_QUIET_MS, timeoutMs: 3_000 });
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
pane = captureTerminalTarget(target, { backend });
|
|
435
|
+
}
|
|
436
|
+
return json(res, 200, { target, pane, paneBefore, changed: pane !== paneBefore, activeCommand: activeTerminalCommand(target, backend) });
|
|
399
437
|
}
|
|
400
438
|
if (req.method === "POST" && path === "/terminal/session") {
|
|
401
439
|
const { createTerminalTarget, killTerminalTarget } = await import("./coding/terminal.js");
|
|
440
|
+
const { waitForPaneSettle } = await import("./coding/tmux.js");
|
|
402
441
|
const b = await readJson(req);
|
|
403
442
|
const backend = b.backend === "tmux" || b.backend === "kitty" || b.backend === "iterm2" ? b.backend : undefined;
|
|
404
443
|
if (b.action === "kill") {
|
|
@@ -410,6 +449,10 @@ async function route(runner, req, res) {
|
|
|
410
449
|
if (!backend)
|
|
411
450
|
return json(res, 400, { error: "`backend` must be tmux, kitty, or iterm2." });
|
|
412
451
|
const target = createTerminalTarget({ backend, name: b.name, workDir: b.workDir, command: b.command });
|
|
452
|
+
// When the new target starts a command, wait until the pane quiesces (#481).
|
|
453
|
+
if (b.command && backend === "tmux" && !target.existed) {
|
|
454
|
+
await waitForPaneSettle(target.id, { quietMs: 750, timeoutMs: 8_000 });
|
|
455
|
+
}
|
|
413
456
|
return json(res, 200, { target });
|
|
414
457
|
}
|
|
415
458
|
return json(res, 404, { error: "Not found" });
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createRequire as createRequire3 } from "module";
|
|
5
|
-
import { Command as
|
|
5
|
+
import { Command as Command9 } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/check.ts
|
|
8
8
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -497,140 +497,21 @@ async function findFreePort() {
|
|
|
497
497
|
});
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
-
// src/commands/
|
|
501
|
-
import { spawn } from "child_process";
|
|
500
|
+
// src/commands/machines.ts
|
|
502
501
|
import { Command as Command4 } from "commander";
|
|
503
|
-
var DEFAULT_MCP_URL = "https://mcp.proagentstore.online/mcp";
|
|
504
|
-
function buildMcpRemoteArgs(opts, extraArgs = []) {
|
|
505
|
-
return ["-y", "mcp-remote", opts.url || DEFAULT_MCP_URL, ...extraArgs];
|
|
506
|
-
}
|
|
507
|
-
async function runMcpProxy(opts, extraArgs = []) {
|
|
508
|
-
const child = spawn("npx", buildMcpRemoteArgs(opts, extraArgs), {
|
|
509
|
-
stdio: "inherit",
|
|
510
|
-
env: process.env
|
|
511
|
-
});
|
|
512
|
-
await new Promise((resolve5, reject) => {
|
|
513
|
-
child.on("error", reject);
|
|
514
|
-
child.on("close", (code) => {
|
|
515
|
-
if (code && code !== 0) reject(new Error(`mcp proxy exited with code ${code}`));
|
|
516
|
-
else resolve5();
|
|
517
|
-
});
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
var mcpCommand = new Command4("mcp").description("Run a local stdio proxy for the official ProAgentStore MCP server").option("--url <url>", "Remote MCP endpoint", DEFAULT_MCP_URL).argument("[args...]", "Extra arguments passed to mcp-remote").action(async (args, opts) => {
|
|
521
|
-
await runMcpProxy(opts, args);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
// src/commands/publish.ts
|
|
525
|
-
import { execFileSync } from "child_process";
|
|
526
|
-
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
527
|
-
import { join as join4, resolve as resolve3 } from "path";
|
|
528
|
-
import { Command as Command5 } from "commander";
|
|
529
|
-
var publishCommand = new Command5("publish").description("Publish an agent to ProAgentStore").option("-d, --dir <path>", "Agent directory", ".").action(async (opts) => {
|
|
530
|
-
const dir = resolve3(opts.dir);
|
|
531
|
-
const manifestPath = join4(dir, "agent.json");
|
|
532
|
-
if (!existsSync4(manifestPath)) {
|
|
533
|
-
writeError("No agent.json found. Run `pags init` first.");
|
|
534
|
-
process.exit(1);
|
|
535
|
-
}
|
|
536
|
-
let manifest;
|
|
537
|
-
try {
|
|
538
|
-
manifest = JSON.parse(readFileSync3(manifestPath, "utf-8"));
|
|
539
|
-
} catch (e) {
|
|
540
|
-
writeError(`agent.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
541
|
-
process.exit(1);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
const slug = manifest.id;
|
|
545
|
-
if (!slug) {
|
|
546
|
-
writeError("agent.json missing id");
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
549
|
-
writeLine(`
|
|
550
|
-
Publishing ${manifest.name} (${slug})...
|
|
551
|
-
`);
|
|
552
|
-
writeLine(" Running compliance checks...");
|
|
553
|
-
try {
|
|
554
|
-
execFileSync("pags", ["check"], { cwd: dir, stdio: "inherit" });
|
|
555
|
-
} catch {
|
|
556
|
-
writeError("\n Compliance checks failed. Fix issues and retry.\n");
|
|
557
|
-
process.exit(1);
|
|
558
|
-
}
|
|
559
|
-
const org = "ProAgentStore";
|
|
560
|
-
const repoName = slug;
|
|
561
|
-
writeLine(`
|
|
562
|
-
Checking GitHub repo: ${org}/${repoName}`);
|
|
563
|
-
let repoExists = false;
|
|
564
|
-
try {
|
|
565
|
-
execFileSync("gh", ["api", `repos/${org}/${repoName}`, "--jq", ".name"], {
|
|
566
|
-
stdio: "pipe"
|
|
567
|
-
});
|
|
568
|
-
repoExists = true;
|
|
569
|
-
writeLine(" Repo exists, pushing...");
|
|
570
|
-
} catch {
|
|
571
|
-
writeLine(" Creating repo...");
|
|
572
|
-
try {
|
|
573
|
-
execFileSync(
|
|
574
|
-
"gh",
|
|
575
|
-
["repo", "create", `${org}/${repoName}`, "--public", `--source=${dir}`, "--push"],
|
|
576
|
-
{ stdio: "inherit" }
|
|
577
|
-
);
|
|
578
|
-
repoExists = true;
|
|
579
|
-
} catch (e) {
|
|
580
|
-
writeError(` Failed to create repo: ${e}`);
|
|
581
|
-
process.exit(1);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
if (repoExists) {
|
|
585
|
-
try {
|
|
586
|
-
execFileSync("git", ["remote", "get-url", "origin"], {
|
|
587
|
-
cwd: dir,
|
|
588
|
-
stdio: "pipe"
|
|
589
|
-
});
|
|
590
|
-
} catch {
|
|
591
|
-
execFileSync(
|
|
592
|
-
"git",
|
|
593
|
-
["remote", "add", "origin", `https://github.com/${org}/${repoName}.git`],
|
|
594
|
-
{ cwd: dir }
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
try {
|
|
598
|
-
execFileSync("git", ["push", "-u", "origin", "main"], {
|
|
599
|
-
cwd: dir,
|
|
600
|
-
stdio: "inherit"
|
|
601
|
-
});
|
|
602
|
-
} catch (e) {
|
|
603
|
-
writeError(`
|
|
604
|
-
Push failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
605
|
-
writeError(" Nothing was published \u2014 fix the push (pull/rebase, or check your GitHub auth) and retry.\n");
|
|
606
|
-
process.exit(1);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
writeLine("\n Registering agent in store...");
|
|
611
|
-
writeLine(`
|
|
612
|
-
Published! ${slug}.proagentstore.online`);
|
|
613
|
-
writeLine(` Store: https://proagentstore.online/agents/${slug}/`);
|
|
614
|
-
writeLine(` Repo: https://github.com/${org}/${repoName}`);
|
|
615
|
-
writeLine();
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
// src/commands/runner/command.ts
|
|
619
|
-
import { spawn as spawn3 } from "child_process";
|
|
620
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
621
|
-
import { Command as Command6 } from "commander";
|
|
622
|
-
|
|
623
|
-
// src/commands/runner/http.ts
|
|
624
|
-
import { hostname as hostname2 } from "os";
|
|
625
502
|
|
|
626
503
|
// src/machine.ts
|
|
627
504
|
import { randomUUID } from "crypto";
|
|
628
|
-
import { existsSync as
|
|
505
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
629
506
|
import { homedir as homedir2, hostname } from "os";
|
|
630
|
-
import { join as
|
|
631
|
-
var CONFIG_DIR2 =
|
|
632
|
-
var MACHINE_FILE =
|
|
507
|
+
import { join as join4 } from "path";
|
|
508
|
+
var CONFIG_DIR2 = join4(homedir2(), ".config", "proagentstore");
|
|
509
|
+
var MACHINE_FILE = join4(CONFIG_DIR2, "machine.json");
|
|
633
510
|
var MAX_NAMES = 10;
|
|
511
|
+
var MAX_DECLINED = 40;
|
|
512
|
+
function machineFilePath() {
|
|
513
|
+
return MACHINE_FILE;
|
|
514
|
+
}
|
|
634
515
|
function isValidMachineId(value) {
|
|
635
516
|
return typeof value === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(value);
|
|
636
517
|
}
|
|
@@ -638,36 +519,274 @@ function parseMachineFile(text) {
|
|
|
638
519
|
try {
|
|
639
520
|
const data = JSON.parse(text);
|
|
640
521
|
if (!isValidMachineId(data.id)) return null;
|
|
641
|
-
|
|
642
|
-
|
|
522
|
+
return {
|
|
523
|
+
id: data.id,
|
|
524
|
+
names: stringList(data.names).slice(0, MAX_NAMES),
|
|
525
|
+
declined: stringList(data.declined).slice(0, MAX_DECLINED)
|
|
526
|
+
};
|
|
643
527
|
} catch {
|
|
644
528
|
return null;
|
|
645
529
|
}
|
|
646
530
|
}
|
|
531
|
+
function stringList(value) {
|
|
532
|
+
if (!Array.isArray(value)) return [];
|
|
533
|
+
return value.filter((n) => typeof n === "string" && n.trim().length > 0).map((n) => n.trim());
|
|
534
|
+
}
|
|
647
535
|
function withName(identity, name) {
|
|
648
536
|
const current = name.trim();
|
|
649
537
|
const prev = identity?.names ?? [];
|
|
650
538
|
const names = current ? [current, ...prev.filter((n) => n !== current)] : [...prev];
|
|
651
|
-
return { id: identity?.id ?? "", names: names.slice(0, MAX_NAMES) };
|
|
539
|
+
return { id: identity?.id ?? "", names: names.slice(0, MAX_NAMES), declined: identity?.declined ?? [] };
|
|
540
|
+
}
|
|
541
|
+
function withClaimedNames(identity, claimed) {
|
|
542
|
+
const add = claimed.map((n) => n.trim()).filter(Boolean);
|
|
543
|
+
const ordered = [identity.names[0], ...add, ...identity.names.slice(1)].filter((n) => !!n);
|
|
544
|
+
const names = [];
|
|
545
|
+
for (const n of ordered) if (!names.includes(n)) names.push(n);
|
|
546
|
+
return {
|
|
547
|
+
id: identity.id,
|
|
548
|
+
names: names.slice(0, MAX_NAMES),
|
|
549
|
+
declined: (identity.declined ?? []).filter((n) => !add.includes(n))
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function withUnclaimedName(identity, name) {
|
|
553
|
+
const unclaim = name.trim();
|
|
554
|
+
if (!unclaim) return identity;
|
|
555
|
+
const names = identity.names.filter((n) => n !== unclaim);
|
|
556
|
+
const declined = [...identity.declined ?? []];
|
|
557
|
+
if (!declined.includes(unclaim)) declined.push(unclaim);
|
|
558
|
+
return { id: identity.id, names, declined: declined.slice(-MAX_DECLINED) };
|
|
559
|
+
}
|
|
560
|
+
function withDeclinedNames(identity, declined) {
|
|
561
|
+
const out = [...identity.declined ?? []];
|
|
562
|
+
for (const raw of declined) {
|
|
563
|
+
const name = raw.trim();
|
|
564
|
+
if (!name || out.includes(name) || identity.names.includes(name)) continue;
|
|
565
|
+
out.push(name);
|
|
566
|
+
}
|
|
567
|
+
return { id: identity.id, names: identity.names, declined: out.slice(-MAX_DECLINED) };
|
|
568
|
+
}
|
|
569
|
+
function saveMachineIdentity(identity) {
|
|
570
|
+
try {
|
|
571
|
+
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
572
|
+
writeFileSync3(MACHINE_FILE, JSON.stringify(identity, null, 2));
|
|
573
|
+
return true;
|
|
574
|
+
} catch {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function sameIdentity(a, b) {
|
|
579
|
+
return a.id === b.id && a.names.join("\0") === b.names.join("\0") && (a.declined ?? []).join("\0") === (b.declined ?? []).join("\0");
|
|
652
580
|
}
|
|
653
581
|
function loadMachineIdentity(now = hostname()) {
|
|
654
582
|
let stored = null;
|
|
655
583
|
try {
|
|
656
|
-
if (
|
|
584
|
+
if (existsSync4(MACHINE_FILE)) stored = parseMachineFile(readFileSync3(MACHINE_FILE, "utf-8"));
|
|
657
585
|
} catch {
|
|
658
586
|
}
|
|
659
587
|
const next = withName(stored ?? { id: randomUUID(), names: [] }, now);
|
|
660
|
-
if (stored && stored
|
|
588
|
+
if (stored && sameIdentity(stored, next)) return next;
|
|
589
|
+
if (saveMachineIdentity(next)) return next;
|
|
590
|
+
return stored ?? { id: "", names: [] };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/machine-claim.ts
|
|
594
|
+
function claimPromptSkipReason(gate) {
|
|
595
|
+
if (gate.headless) return "headless";
|
|
596
|
+
if (gate.suppressed) return "suppressed";
|
|
597
|
+
if (gate.ci) return "ci";
|
|
598
|
+
if (!gate.isTTY) return "no-tty";
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
function stampMs(value) {
|
|
602
|
+
if (!value) return 0;
|
|
603
|
+
const t = Date.parse(value.includes("T") ? value : `${value.replace(" ", "T")}Z`);
|
|
604
|
+
return Number.isFinite(t) ? t : 0;
|
|
605
|
+
}
|
|
606
|
+
function parseNodesResponse(data) {
|
|
607
|
+
const nodes = data?.nodes;
|
|
608
|
+
if (!Array.isArray(nodes)) return [];
|
|
609
|
+
const out = [];
|
|
610
|
+
for (const raw of nodes) {
|
|
611
|
+
const n = raw;
|
|
612
|
+
const node = typeof n?.node === "string" ? n.node.trim() : "";
|
|
613
|
+
if (!node) continue;
|
|
614
|
+
out.push({
|
|
615
|
+
node,
|
|
616
|
+
machineId: typeof n.machineId === "string" && n.machineId.trim() ? n.machineId.trim() : null,
|
|
617
|
+
lastSeenAt: typeof n.lastSeenAt === "string" ? n.lastSeenAt : null,
|
|
618
|
+
connected: n.connected === true,
|
|
619
|
+
agentCount: Array.isArray(n.instances) ? n.instances.length : 0
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
function claimCandidates(nodes, identity) {
|
|
625
|
+
const mine = new Set(identity.names);
|
|
626
|
+
const declined = new Set(identity.declined ?? []);
|
|
627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
628
|
+
const out = [];
|
|
629
|
+
for (const n of nodes) {
|
|
630
|
+
const name = n.node.trim();
|
|
631
|
+
if (!name || seen.has(name)) continue;
|
|
632
|
+
if (n.machineId) continue;
|
|
633
|
+
if (mine.has(name)) continue;
|
|
634
|
+
if (declined.has(name)) continue;
|
|
635
|
+
if (n.connected) continue;
|
|
636
|
+
seen.add(name);
|
|
637
|
+
out.push(n);
|
|
638
|
+
}
|
|
639
|
+
return out.sort((a, b) => stampMs(b.lastSeenAt) - stampMs(a.lastSeenAt));
|
|
640
|
+
}
|
|
641
|
+
function resolveClaimByName(requested, nodes, identity) {
|
|
642
|
+
const byName = new Map(nodes.map((n) => [n.node, n]));
|
|
643
|
+
const claim = [];
|
|
644
|
+
const problems = [];
|
|
645
|
+
for (const raw of requested) {
|
|
646
|
+
const name = raw.trim();
|
|
647
|
+
if (!name || claim.includes(name)) continue;
|
|
648
|
+
if (identity.names.includes(name)) {
|
|
649
|
+
problems.push(`${name}: this machine already claims that name.`);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const node = byName.get(name);
|
|
653
|
+
if (!node) {
|
|
654
|
+
problems.push(`${name}: no machine on this account is registered under that name.`);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (node.machineId) {
|
|
658
|
+
problems.push(`${name}: already claimed by another machine (${node.machineId}).`);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (node.connected) {
|
|
662
|
+
problems.push(`${name}: a runner is connected there right now, so it is a different machine. Stop it first if it is not.`);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
claim.push(name);
|
|
666
|
+
}
|
|
667
|
+
return { claim, problems };
|
|
668
|
+
}
|
|
669
|
+
function relativeAge(lastSeenAt, nowMs) {
|
|
670
|
+
const at = stampMs(lastSeenAt);
|
|
671
|
+
if (!at) return "last seen unknown";
|
|
672
|
+
const mins = Math.max(0, Math.round((nowMs - at) / 6e4));
|
|
673
|
+
if (mins < 60) return `last seen ${mins} min ago`;
|
|
674
|
+
const hours = Math.round(mins / 60);
|
|
675
|
+
if (hours < 48) return `last seen ${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
676
|
+
const days = Math.round(hours / 24);
|
|
677
|
+
if (days < 45) return `last seen ${days} days ago`;
|
|
678
|
+
return `last seen ${Math.round(days / 30)} months ago`;
|
|
679
|
+
}
|
|
680
|
+
function describeCandidate(candidate, nowMs) {
|
|
681
|
+
const agents = `${candidate.agentCount} agent${candidate.agentCount === 1 ? "" : "s"}`;
|
|
682
|
+
return `${relativeAge(candidate.lastSeenAt, nowMs)} \xB7 ${agents}`;
|
|
683
|
+
}
|
|
684
|
+
function parseSelection(input, count) {
|
|
685
|
+
const raw = input.trim().toLowerCase();
|
|
686
|
+
if (!raw || raw === "n" || raw === "no" || raw === "s" || raw === "skip" || raw === "none") return { kind: "skip" };
|
|
687
|
+
if (raw === "a" || raw === "all") return { kind: "pick", indices: Array.from({ length: count }, (_, i) => i) };
|
|
688
|
+
const parts = raw.split(/[\s,]+/).filter(Boolean);
|
|
689
|
+
const indices = [];
|
|
690
|
+
for (const part of parts) {
|
|
691
|
+
if (!/^\d+$/.test(part)) return { kind: "invalid", message: `"${part}" is not a number.` };
|
|
692
|
+
const n = Number.parseInt(part, 10);
|
|
693
|
+
if (n < 1 || n > count) return { kind: "invalid", message: `${n} is not on the list (1\u2013${count}).` };
|
|
694
|
+
if (!indices.includes(n - 1)) indices.push(n - 1);
|
|
695
|
+
}
|
|
696
|
+
return indices.length ? { kind: "pick", indices } : { kind: "skip" };
|
|
697
|
+
}
|
|
698
|
+
function renderCandidates(candidates, nowMs) {
|
|
699
|
+
const width = candidates.reduce((w2, c2) => Math.max(w2, c2.node.length), 0);
|
|
700
|
+
const lines = [
|
|
701
|
+
`PAGS knows ${candidates.length} machine name${candidates.length === 1 ? "" : "s"} on this account that no machine has claimed.`,
|
|
702
|
+
"Is this machine also known as:",
|
|
703
|
+
""
|
|
704
|
+
];
|
|
705
|
+
candidates.forEach((c2, i) => {
|
|
706
|
+
lines.push(` ${i + 1}) ${c2.node.padEnd(width)} ${describeCandidate(c2, nowMs)}`);
|
|
707
|
+
});
|
|
708
|
+
lines.push("");
|
|
709
|
+
lines.push("Selecting a name merges its agents, pins and sessions onto this machine.");
|
|
710
|
+
lines.push("Pick only names THIS machine has used \u2014 use `pags machines unclaim <name>` to undo a wrong claim.");
|
|
711
|
+
return lines;
|
|
712
|
+
}
|
|
713
|
+
async function fetchNodeSummaries(opts) {
|
|
714
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
661
715
|
try {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
716
|
+
const res = await doFetch(`${opts.apiBase.replace(/\/$/, "")}/v1/terminals/nodes`, {
|
|
717
|
+
headers: { Authorization: `Bearer ${opts.token}` },
|
|
718
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5e3)
|
|
719
|
+
});
|
|
720
|
+
if (!res.ok) return null;
|
|
721
|
+
return parseNodesResponse(await res.json());
|
|
665
722
|
} catch {
|
|
666
|
-
return
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function maybeClaimMachineNames(opts) {
|
|
727
|
+
const env = opts.env ?? process.env;
|
|
728
|
+
const skip = claimPromptSkipReason({
|
|
729
|
+
headless: opts.headless,
|
|
730
|
+
isTTY: opts.isTTY ?? process.stdin.isTTY,
|
|
731
|
+
ci: !!env.CI,
|
|
732
|
+
suppressed: !!env.PAGS_NO_PROMPT
|
|
733
|
+
});
|
|
734
|
+
if (skip) return { prompted: false, reason: skip };
|
|
735
|
+
const identity = loadMachineIdentity();
|
|
736
|
+
if (!identity.id) return { prompted: false, reason: "no-identity" };
|
|
737
|
+
const nodes = await fetchNodeSummaries(opts);
|
|
738
|
+
if (!nodes) return { prompted: false, reason: "unavailable" };
|
|
739
|
+
const candidates = claimCandidates(nodes, identity);
|
|
740
|
+
if (!candidates.length) return { prompted: false, reason: "nothing-unclaimed" };
|
|
741
|
+
const nowMs = opts.now ?? Date.now();
|
|
742
|
+
writeLine("");
|
|
743
|
+
for (const line of renderCandidates(candidates, nowMs)) writeLine(` ${line}`);
|
|
744
|
+
writeLine("");
|
|
745
|
+
const ask = opts.ask ?? defaultAsk;
|
|
746
|
+
let selection = { kind: "skip" };
|
|
747
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
748
|
+
let answer = "";
|
|
749
|
+
try {
|
|
750
|
+
answer = await ask(" Numbers to claim (e.g. 1,2 or all), or Enter to skip: ");
|
|
751
|
+
} catch {
|
|
752
|
+
return { prompted: false, reason: "no-tty" };
|
|
753
|
+
}
|
|
754
|
+
selection = parseSelection(answer, candidates.length);
|
|
755
|
+
if (selection.kind !== "invalid") break;
|
|
756
|
+
writeLine(` ${selection.message} Enter to skip.`);
|
|
757
|
+
}
|
|
758
|
+
if (selection.kind === "invalid") {
|
|
759
|
+
writeLine(" Nothing claimed \u2014 run `pags machines claim <name>` when you know which.");
|
|
760
|
+
writeLine("");
|
|
761
|
+
return { prompted: false, reason: "unanswered" };
|
|
762
|
+
}
|
|
763
|
+
const names = candidates.map((c2) => c2.node);
|
|
764
|
+
if (selection.kind !== "pick") {
|
|
765
|
+
saveMachineIdentity(withDeclinedNames(identity, names));
|
|
766
|
+
writeLine(` Skipped \u2014 these names will not be offered again (${machineFilePath()}).`);
|
|
767
|
+
writeLine("");
|
|
768
|
+
return { prompted: true, reason: "declined", claimed: [], declined: names };
|
|
769
|
+
}
|
|
770
|
+
const claimed = selection.indices.map((i) => names[i]);
|
|
771
|
+
const rest = names.filter((n) => !claimed.includes(n));
|
|
772
|
+
saveMachineIdentity(withDeclinedNames(withClaimedNames(identity, claimed), rest));
|
|
773
|
+
writeLine(` Claiming ${claimed.join(", ")} \u2014 merged on the next register.`);
|
|
774
|
+
writeLine("");
|
|
775
|
+
return { prompted: true, reason: "claimed", claimed, declined: rest };
|
|
776
|
+
}
|
|
777
|
+
async function defaultAsk(question) {
|
|
778
|
+
const { createInterface } = await import("readline/promises");
|
|
779
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
780
|
+
try {
|
|
781
|
+
const closed = new Promise((resolve5) => rl.once("close", () => resolve5("")));
|
|
782
|
+
return await Promise.race([rl.question(question), closed]);
|
|
783
|
+
} finally {
|
|
784
|
+
rl.close();
|
|
667
785
|
}
|
|
668
786
|
}
|
|
669
787
|
|
|
670
788
|
// src/commands/runner/http.ts
|
|
789
|
+
import { hostname as hostname2 } from "os";
|
|
671
790
|
function clean(value) {
|
|
672
791
|
const trimmed = value?.trim();
|
|
673
792
|
return trimmed || void 0;
|
|
@@ -756,6 +875,225 @@ function responseErrorMessage(data, text, statusText) {
|
|
|
756
875
|
return typeof data.error === "string" ? data.error : text || statusText;
|
|
757
876
|
}
|
|
758
877
|
|
|
878
|
+
// src/commands/machines.ts
|
|
879
|
+
var API_BASE2 = "https://api.proagentstore.online";
|
|
880
|
+
async function loadNodes(token) {
|
|
881
|
+
const nodes = await fetchNodeSummaries({ token, apiBase: API_BASE2 });
|
|
882
|
+
if (!nodes) {
|
|
883
|
+
writeError("Could not reach ProAgentStore to list your machines.");
|
|
884
|
+
process.exit(1);
|
|
885
|
+
}
|
|
886
|
+
return nodes;
|
|
887
|
+
}
|
|
888
|
+
var listCommand = new Command4("list").description("List the machines ProAgentStore has seen on this account").action(async () => {
|
|
889
|
+
const session = requireSession();
|
|
890
|
+
const nodes = await loadNodes(session.token);
|
|
891
|
+
const identity = loadMachineIdentity();
|
|
892
|
+
writeLine("");
|
|
893
|
+
writeLine(` This machine: ${identity.names[0] ?? "unknown"} ${identity.id ? `(id ${identity.id})` : "(no id \u2014 check that ~/.config/proagentstore/ is writable)"}`);
|
|
894
|
+
if (identity.names.length > 1) writeLine(` Also claims: ${identity.names.slice(1).join(", ")}`);
|
|
895
|
+
writeLine("");
|
|
896
|
+
if (!nodes.length) writeLine(" No machines registered yet \u2014 run `pags up`.");
|
|
897
|
+
const now = Date.now();
|
|
898
|
+
for (const n of nodes) {
|
|
899
|
+
const owner = n.machineId ? n.machineId === identity.id ? "this machine" : "claimed" : "unclaimed";
|
|
900
|
+
writeLine(` ${n.node}`);
|
|
901
|
+
writeLine(` ${describeCandidate(n, now)} \xB7 ${n.connected ? "connected" : "offline"} \xB7 ${owner}`);
|
|
902
|
+
}
|
|
903
|
+
writeLine("");
|
|
904
|
+
writeLine(" Claim a name this machine has used before: pags machines claim <name>");
|
|
905
|
+
writeLine(" Remove a wrong claim: pags machines unclaim <name>");
|
|
906
|
+
writeLine("");
|
|
907
|
+
});
|
|
908
|
+
var claimCommand = new Command4("claim").description("Record that a machine name on this account is THIS machine").argument("<name...>", "Node name(s) to claim, as shown by `pags machines list`").action(async (names) => {
|
|
909
|
+
const session = requireSession();
|
|
910
|
+
const identity = loadMachineIdentity();
|
|
911
|
+
if (!identity.id) {
|
|
912
|
+
writeError("This machine has no id \u2014 `~/.config/proagentstore/` is not writable, so a claim could not be sent.");
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
const nodes = await loadNodes(session.token);
|
|
916
|
+
const { claim, problems } = resolveClaimByName(names, nodes, identity);
|
|
917
|
+
for (const p of problems) writeError(` \u2717 ${p}`);
|
|
918
|
+
if (!claim.length) {
|
|
919
|
+
writeError(" Nothing claimed.");
|
|
920
|
+
process.exit(problems.length ? 1 : 0);
|
|
921
|
+
}
|
|
922
|
+
if (!saveMachineIdentity(withClaimedNames(identity, claim))) {
|
|
923
|
+
writeError(` \u2717 Could not write ${machineFilePath()} \u2014 nothing claimed.`);
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
writeLine(` \u2713 Claimed ${claim.join(", ")}. Restart \`pags up\` to merge them onto this machine.`);
|
|
927
|
+
});
|
|
928
|
+
var unclaimCommand = new Command4("unclaim").description("Remove a mistaken machine name claim from this machine (#467)").argument("<name...>", "Node name(s) to un-claim, as shown by `pags machines list`").action(async (names) => {
|
|
929
|
+
const session = requireSession();
|
|
930
|
+
const identity = loadMachineIdentity();
|
|
931
|
+
if (!identity.id) {
|
|
932
|
+
writeError("This machine has no id \u2014 `~/.config/proagentstore/` is not writable, so the claim record cannot be found.");
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
let anyFailed = false;
|
|
936
|
+
for (const raw of names) {
|
|
937
|
+
const name = raw.trim();
|
|
938
|
+
if (!name) continue;
|
|
939
|
+
if (name === identity.names[0]) {
|
|
940
|
+
writeError(` \u2717 ${name}: this is the machine's CURRENT hostname. You cannot un-claim the name it is actively registering under \u2014 stop \`pags up\` and rename the machine first.`);
|
|
941
|
+
anyFailed = true;
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
if (!identity.names.includes(name)) {
|
|
945
|
+
writeError(` \u2717 ${name}: this machine does not claim that name.`);
|
|
946
|
+
anyFailed = true;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
await requestPags(
|
|
951
|
+
"DELETE",
|
|
952
|
+
`/v1/terminals/nodes/${apiPathSegment(name)}/claim`,
|
|
953
|
+
{ pagsToken: session.token, apiBase: pagsApiBase() },
|
|
954
|
+
{ machineId: identity.id }
|
|
955
|
+
);
|
|
956
|
+
} catch (e) {
|
|
957
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
958
|
+
writeError(` \u2717 ${name}: ${msg}`);
|
|
959
|
+
anyFailed = true;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const updated = withUnclaimedName(identity, name);
|
|
963
|
+
if (!saveMachineIdentity(updated)) {
|
|
964
|
+
writeError(` \u2717 Server un-claimed ${name} but could not update ${machineFilePath()} \u2014 the next \`pags up\` may re-stamp it. Edit that file by hand and remove "${name}" from the names array.`);
|
|
965
|
+
anyFailed = true;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
writeLine(` \u2713 ${name} \u2014 un-claimed on server and removed from ${machineFilePath()}.`);
|
|
969
|
+
}
|
|
970
|
+
if (anyFailed) process.exit(1);
|
|
971
|
+
});
|
|
972
|
+
var machinesCommand = new Command4("machines").description("Show and claim the machine names ProAgentStore has for this account").addCommand(listCommand, { isDefault: true }).addCommand(claimCommand).addCommand(unclaimCommand);
|
|
973
|
+
|
|
974
|
+
// src/commands/mcp.ts
|
|
975
|
+
import { spawn } from "child_process";
|
|
976
|
+
import { Command as Command5 } from "commander";
|
|
977
|
+
var DEFAULT_MCP_URL = "https://mcp.proagentstore.online/mcp";
|
|
978
|
+
function buildMcpRemoteArgs(opts, extraArgs = []) {
|
|
979
|
+
return ["-y", "mcp-remote", opts.url || DEFAULT_MCP_URL, ...extraArgs];
|
|
980
|
+
}
|
|
981
|
+
async function runMcpProxy(opts, extraArgs = []) {
|
|
982
|
+
const child = spawn("npx", buildMcpRemoteArgs(opts, extraArgs), {
|
|
983
|
+
stdio: "inherit",
|
|
984
|
+
env: process.env
|
|
985
|
+
});
|
|
986
|
+
await new Promise((resolve5, reject) => {
|
|
987
|
+
child.on("error", reject);
|
|
988
|
+
child.on("close", (code) => {
|
|
989
|
+
if (code && code !== 0) reject(new Error(`mcp proxy exited with code ${code}`));
|
|
990
|
+
else resolve5();
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
var mcpCommand = new Command5("mcp").description("Run a local stdio proxy for the official ProAgentStore MCP server").option("--url <url>", "Remote MCP endpoint", DEFAULT_MCP_URL).argument("[args...]", "Extra arguments passed to mcp-remote").action(async (args, opts) => {
|
|
995
|
+
await runMcpProxy(opts, args);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
// src/commands/publish.ts
|
|
999
|
+
import { execFileSync } from "child_process";
|
|
1000
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1001
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
1002
|
+
import { Command as Command6 } from "commander";
|
|
1003
|
+
var publishCommand = new Command6("publish").description("Publish an agent to ProAgentStore").option("-d, --dir <path>", "Agent directory", ".").action(async (opts) => {
|
|
1004
|
+
const dir = resolve3(opts.dir);
|
|
1005
|
+
const manifestPath = join5(dir, "agent.json");
|
|
1006
|
+
if (!existsSync5(manifestPath)) {
|
|
1007
|
+
writeError("No agent.json found. Run `pags init` first.");
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
let manifest;
|
|
1011
|
+
try {
|
|
1012
|
+
manifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
|
|
1013
|
+
} catch (e) {
|
|
1014
|
+
writeError(`agent.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const slug = manifest.id;
|
|
1019
|
+
if (!slug) {
|
|
1020
|
+
writeError("agent.json missing id");
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
writeLine(`
|
|
1024
|
+
Publishing ${manifest.name} (${slug})...
|
|
1025
|
+
`);
|
|
1026
|
+
writeLine(" Running compliance checks...");
|
|
1027
|
+
try {
|
|
1028
|
+
execFileSync("pags", ["check"], { cwd: dir, stdio: "inherit" });
|
|
1029
|
+
} catch {
|
|
1030
|
+
writeError("\n Compliance checks failed. Fix issues and retry.\n");
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
const org = "ProAgentStore";
|
|
1034
|
+
const repoName = slug;
|
|
1035
|
+
writeLine(`
|
|
1036
|
+
Checking GitHub repo: ${org}/${repoName}`);
|
|
1037
|
+
let repoExists = false;
|
|
1038
|
+
try {
|
|
1039
|
+
execFileSync("gh", ["api", `repos/${org}/${repoName}`, "--jq", ".name"], {
|
|
1040
|
+
stdio: "pipe"
|
|
1041
|
+
});
|
|
1042
|
+
repoExists = true;
|
|
1043
|
+
writeLine(" Repo exists, pushing...");
|
|
1044
|
+
} catch {
|
|
1045
|
+
writeLine(" Creating repo...");
|
|
1046
|
+
try {
|
|
1047
|
+
execFileSync(
|
|
1048
|
+
"gh",
|
|
1049
|
+
["repo", "create", `${org}/${repoName}`, "--public", `--source=${dir}`, "--push"],
|
|
1050
|
+
{ stdio: "inherit" }
|
|
1051
|
+
);
|
|
1052
|
+
repoExists = true;
|
|
1053
|
+
} catch (e) {
|
|
1054
|
+
writeError(` Failed to create repo: ${e}`);
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (repoExists) {
|
|
1059
|
+
try {
|
|
1060
|
+
execFileSync("git", ["remote", "get-url", "origin"], {
|
|
1061
|
+
cwd: dir,
|
|
1062
|
+
stdio: "pipe"
|
|
1063
|
+
});
|
|
1064
|
+
} catch {
|
|
1065
|
+
execFileSync(
|
|
1066
|
+
"git",
|
|
1067
|
+
["remote", "add", "origin", `https://github.com/${org}/${repoName}.git`],
|
|
1068
|
+
{ cwd: dir }
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
execFileSync("git", ["push", "-u", "origin", "main"], {
|
|
1073
|
+
cwd: dir,
|
|
1074
|
+
stdio: "inherit"
|
|
1075
|
+
});
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
writeError(`
|
|
1078
|
+
Push failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1079
|
+
writeError(" Nothing was published \u2014 fix the push (pull/rebase, or check your GitHub auth) and retry.\n");
|
|
1080
|
+
process.exit(1);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
writeLine("\n Registering agent in store...");
|
|
1085
|
+
writeLine(`
|
|
1086
|
+
Published! ${slug}.proagentstore.online`);
|
|
1087
|
+
writeLine(` Store: https://proagentstore.online/agents/${slug}/`);
|
|
1088
|
+
writeLine(` Repo: https://github.com/${org}/${repoName}`);
|
|
1089
|
+
writeLine();
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
// src/commands/runner/command.ts
|
|
1093
|
+
import { spawn as spawn3 } from "child_process";
|
|
1094
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1095
|
+
import { Command as Command7 } from "commander";
|
|
1096
|
+
|
|
759
1097
|
// src/commands/runner/process.ts
|
|
760
1098
|
import { spawn as spawn2 } from "child_process";
|
|
761
1099
|
import { existsSync as existsSync6 } from "fs";
|
|
@@ -1119,7 +1457,7 @@ function collectCapability(value, previous = []) {
|
|
|
1119
1457
|
return [...previous, value];
|
|
1120
1458
|
}
|
|
1121
1459
|
function createRunnerCommand() {
|
|
1122
|
-
const command = new
|
|
1460
|
+
const command = new Command7("runner").description(
|
|
1123
1461
|
"Manage the local ProAgentStore browser runtime for ProAgentStore agents"
|
|
1124
1462
|
);
|
|
1125
1463
|
command.command("start").description("Start the local ProAgentStore browser runtime in the foreground").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind (default: first free port from 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) => {
|
|
@@ -1284,7 +1622,7 @@ var runnerCommand = createRunnerCommand();
|
|
|
1284
1622
|
|
|
1285
1623
|
// src/commands/up.ts
|
|
1286
1624
|
import { createRequire as createRequire2 } from "module";
|
|
1287
|
-
import { Command as
|
|
1625
|
+
import { Command as Command8 } from "commander";
|
|
1288
1626
|
|
|
1289
1627
|
// src/tui.ts
|
|
1290
1628
|
import chalk from "chalk";
|
|
@@ -1393,7 +1731,7 @@ async function waitForKey(keys, onInterrupt) {
|
|
|
1393
1731
|
}
|
|
1394
1732
|
|
|
1395
1733
|
// src/commands/up.ts
|
|
1396
|
-
var
|
|
1734
|
+
var API_BASE3 = "https://api.proagentstore.online";
|
|
1397
1735
|
var CLI_VERSION2 = createRequire2(import.meta.url)("../package.json").version;
|
|
1398
1736
|
async function stopRunnerProcesses() {
|
|
1399
1737
|
if (process.platform === "win32") return false;
|
|
@@ -1413,7 +1751,7 @@ async function stopRunnerProcesses() {
|
|
|
1413
1751
|
}
|
|
1414
1752
|
return stopped;
|
|
1415
1753
|
}
|
|
1416
|
-
var upCommand = new
|
|
1754
|
+
var upCommand = new Command8("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").action(async (opts) => {
|
|
1417
1755
|
const session = requireSession();
|
|
1418
1756
|
const state = {
|
|
1419
1757
|
user: session.user.login,
|
|
@@ -1431,7 +1769,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1431
1769
|
printLogo(CLI_VERSION2);
|
|
1432
1770
|
printStep("Signed in as " + session.user.login, "ok");
|
|
1433
1771
|
printStep("Fetching instances...", "wait");
|
|
1434
|
-
const res = await fetch(`${
|
|
1772
|
+
const res = await fetch(`${API_BASE3}/v1/instances/my/instances`, {
|
|
1435
1773
|
headers: { Authorization: `Bearer ${session.token}` }
|
|
1436
1774
|
});
|
|
1437
1775
|
if (!res.ok) {
|
|
@@ -1462,6 +1800,11 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1462
1800
|
for (const inst of state.instances) {
|
|
1463
1801
|
writeLine(` ${inst.name} (${inst.id.slice(0, 8)}...)`);
|
|
1464
1802
|
}
|
|
1803
|
+
await maybeClaimMachineNames({
|
|
1804
|
+
token: session.token,
|
|
1805
|
+
apiBase: API_BASE3,
|
|
1806
|
+
headless: opts.headless
|
|
1807
|
+
}).catch(() => void 0);
|
|
1465
1808
|
state.activeInstance = instances.length === 1 ? instances[0].name || instances[0].slug || instances[0].id.slice(0, 8) : `${instances.length} agents`;
|
|
1466
1809
|
printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
|
|
1467
1810
|
await stopRunnerProcesses();
|
|
@@ -1591,7 +1934,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1591
1934
|
}
|
|
1592
1935
|
}
|
|
1593
1936
|
});
|
|
1594
|
-
var downCommand = new
|
|
1937
|
+
var downCommand = new Command8("down").description("Stop the browser runner and disconnect").action(async () => {
|
|
1595
1938
|
clearScreen();
|
|
1596
1939
|
printLogo(CLI_VERSION2);
|
|
1597
1940
|
if (process.platform === "win32") {
|
|
@@ -1613,7 +1956,7 @@ var downCommand = new Command7("down").description("Stop the browser runner and
|
|
|
1613
1956
|
// src/index.ts
|
|
1614
1957
|
var require2 = createRequire3(import.meta.url);
|
|
1615
1958
|
var { version } = require2("../package.json");
|
|
1616
|
-
var program = new
|
|
1959
|
+
var program = new Command9();
|
|
1617
1960
|
program.name("pags").description(
|
|
1618
1961
|
"ProAgentStore CLI \u2014 create and publish server-powered AI agents"
|
|
1619
1962
|
).version(version);
|
|
@@ -1626,6 +1969,7 @@ program.addCommand(initCommand);
|
|
|
1626
1969
|
program.addCommand(checkCommand);
|
|
1627
1970
|
program.addCommand(publishCommand);
|
|
1628
1971
|
program.addCommand(runnerCommand);
|
|
1972
|
+
program.addCommand(machinesCommand);
|
|
1629
1973
|
program.addCommand(mcpCommand);
|
|
1630
1974
|
try {
|
|
1631
1975
|
await program.parseAsync();
|