@proagentstore/cli 0.4.43 → 0.4.45
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.
|
@@ -154,10 +154,26 @@ export class HeadlessSession {
|
|
|
154
154
|
get authResolved() {
|
|
155
155
|
return resolveEngineAuth(this.config.clientType, mergeEnv(process.env, this.config.env));
|
|
156
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Did this engine launch with a conversation to continue (#408)?
|
|
159
|
+
*
|
|
160
|
+
* Reported back to the cloud by `/coding/start` so the sentence the agent says to the user is
|
|
161
|
+
* something this side CONFIRMED rather than something the cloud asked for. The distinction is
|
|
162
|
+
* not academic: a runner published before #408 ignores `resumeFrom` entirely and always starts
|
|
163
|
+
* clean, so a cloud that announced "resumed where we left off" on its own intent would be
|
|
164
|
+
* telling most of the fleet's users the opposite of what happened.
|
|
165
|
+
*
|
|
166
|
+
* False for a raw (non-Claude) engine under every circumstance — `--resume` is a Claude Code
|
|
167
|
+
* flag and {@link buildClaudeArgs} is only reached in stream-json mode.
|
|
168
|
+
*/
|
|
169
|
+
get resumedConversation() {
|
|
170
|
+
return this.mode === "stream-json" && this.claudeSessionId !== null;
|
|
171
|
+
}
|
|
157
172
|
constructor(config) {
|
|
158
173
|
this.config = config;
|
|
159
174
|
this.engineLabel = `${config.clientType}:${config.id}`;
|
|
160
|
-
|
|
175
|
+
// Our own key first, the cloud's nominated predecessor second. See `resumeFrom`.
|
|
176
|
+
this.claudeSessionId = readState(config.statePath, config.id) ?? (config.resumeFrom ? readState(config.statePath, config.resumeFrom) : null);
|
|
161
177
|
// Claude is the structured engine; everything else is a raw CLI.
|
|
162
178
|
this.mode = config.clientType === "claude" ? "stream-json" : "raw";
|
|
163
179
|
const { bin, args } = parseCommand(config.command);
|
|
@@ -454,16 +470,15 @@ export class HeadlessSession {
|
|
|
454
470
|
});
|
|
455
471
|
}
|
|
456
472
|
/**
|
|
457
|
-
* No TTY in headless mode; control is via messages.
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
* success: `act` returned an ordinary snapshot with an unchanged pane, so the caller could not
|
|
462
|
-
* tell "sent, nothing happened" from "never sent". The transcript is what the brain and the
|
|
463
|
-
* 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`.
|
|
464
477
|
*/
|
|
465
478
|
key(keys) {
|
|
466
|
-
|
|
479
|
+
const reason = "this session has no terminal attached";
|
|
480
|
+
this.push(`[ignored keypress ${keys.slice(0, 40)} — ${reason}]`);
|
|
481
|
+
return { delivered: false, reason };
|
|
467
482
|
}
|
|
468
483
|
/** Abort the current turn (SIGINT, like Ctrl-C). The process stays usable. */
|
|
469
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";
|
|
@@ -89,12 +90,17 @@ export class CodingRuntime {
|
|
|
89
90
|
command: input.command,
|
|
90
91
|
env: input.env,
|
|
91
92
|
statePath: defaultStatePath(this.reposBaseDir),
|
|
93
|
+
resumeFrom: input.resumeFrom,
|
|
92
94
|
bin: input.bin,
|
|
93
95
|
});
|
|
94
96
|
this.sessions.set(input.sessionId, session);
|
|
95
97
|
}
|
|
98
|
+
// Read BEFORE `start()`: a bad `--resume` can kill the process on spawn, and the engine
|
|
99
|
+
// clears its own key on that exit. Reporting after would say "started clean" about a launch
|
|
100
|
+
// that did carry a conversation, and the transcript (which shows the crash) would disagree.
|
|
101
|
+
const resumed = session.resumedConversation;
|
|
96
102
|
session.start();
|
|
97
|
-
return this.snapshot(input.sessionId);
|
|
103
|
+
return { ...this.snapshot(input.sessionId), resumed };
|
|
98
104
|
}
|
|
99
105
|
/**
|
|
100
106
|
* The pane the brain reasons over + the inferred run state.
|
|
@@ -130,9 +136,16 @@ export class CodingRuntime {
|
|
|
130
136
|
case "message":
|
|
131
137
|
session.input(action.text);
|
|
132
138
|
break;
|
|
133
|
-
case "keys":
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
|
136
149
|
case "interrupt":
|
|
137
150
|
session.interrupt();
|
|
138
151
|
break;
|
|
@@ -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
|
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,9 +497,340 @@ async function findFreePort() {
|
|
|
497
497
|
});
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
+
// src/commands/machines.ts
|
|
501
|
+
import { Command as Command4 } from "commander";
|
|
502
|
+
|
|
503
|
+
// src/machine.ts
|
|
504
|
+
import { randomUUID } from "crypto";
|
|
505
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
506
|
+
import { homedir as homedir2, hostname } from "os";
|
|
507
|
+
import { join as join4 } from "path";
|
|
508
|
+
var CONFIG_DIR2 = join4(homedir2(), ".config", "proagentstore");
|
|
509
|
+
var MACHINE_FILE = join4(CONFIG_DIR2, "machine.json");
|
|
510
|
+
var MAX_NAMES = 10;
|
|
511
|
+
var MAX_DECLINED = 40;
|
|
512
|
+
function machineFilePath() {
|
|
513
|
+
return MACHINE_FILE;
|
|
514
|
+
}
|
|
515
|
+
function isValidMachineId(value) {
|
|
516
|
+
return typeof value === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(value);
|
|
517
|
+
}
|
|
518
|
+
function parseMachineFile(text) {
|
|
519
|
+
try {
|
|
520
|
+
const data = JSON.parse(text);
|
|
521
|
+
if (!isValidMachineId(data.id)) return null;
|
|
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
|
+
};
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
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
|
+
}
|
|
535
|
+
function withName(identity, name) {
|
|
536
|
+
const current = name.trim();
|
|
537
|
+
const prev = identity?.names ?? [];
|
|
538
|
+
const names = current ? [current, ...prev.filter((n) => n !== current)] : [...prev];
|
|
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 withDeclinedNames(identity, declined) {
|
|
553
|
+
const out = [...identity.declined ?? []];
|
|
554
|
+
for (const raw of declined) {
|
|
555
|
+
const name = raw.trim();
|
|
556
|
+
if (!name || out.includes(name) || identity.names.includes(name)) continue;
|
|
557
|
+
out.push(name);
|
|
558
|
+
}
|
|
559
|
+
return { id: identity.id, names: identity.names, declined: out.slice(-MAX_DECLINED) };
|
|
560
|
+
}
|
|
561
|
+
function saveMachineIdentity(identity) {
|
|
562
|
+
try {
|
|
563
|
+
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
564
|
+
writeFileSync3(MACHINE_FILE, JSON.stringify(identity, null, 2));
|
|
565
|
+
return true;
|
|
566
|
+
} catch {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function sameIdentity(a, b) {
|
|
571
|
+
return a.id === b.id && a.names.join("\0") === b.names.join("\0") && (a.declined ?? []).join("\0") === (b.declined ?? []).join("\0");
|
|
572
|
+
}
|
|
573
|
+
function loadMachineIdentity(now = hostname()) {
|
|
574
|
+
let stored = null;
|
|
575
|
+
try {
|
|
576
|
+
if (existsSync4(MACHINE_FILE)) stored = parseMachineFile(readFileSync3(MACHINE_FILE, "utf-8"));
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
const next = withName(stored ?? { id: randomUUID(), names: [] }, now);
|
|
580
|
+
if (stored && sameIdentity(stored, next)) return next;
|
|
581
|
+
if (saveMachineIdentity(next)) return next;
|
|
582
|
+
return stored ?? { id: "", names: [] };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/machine-claim.ts
|
|
586
|
+
function claimPromptSkipReason(gate) {
|
|
587
|
+
if (gate.headless) return "headless";
|
|
588
|
+
if (gate.suppressed) return "suppressed";
|
|
589
|
+
if (gate.ci) return "ci";
|
|
590
|
+
if (!gate.isTTY) return "no-tty";
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
function stampMs(value) {
|
|
594
|
+
if (!value) return 0;
|
|
595
|
+
const t = Date.parse(value.includes("T") ? value : `${value.replace(" ", "T")}Z`);
|
|
596
|
+
return Number.isFinite(t) ? t : 0;
|
|
597
|
+
}
|
|
598
|
+
function parseNodesResponse(data) {
|
|
599
|
+
const nodes = data?.nodes;
|
|
600
|
+
if (!Array.isArray(nodes)) return [];
|
|
601
|
+
const out = [];
|
|
602
|
+
for (const raw of nodes) {
|
|
603
|
+
const n = raw;
|
|
604
|
+
const node = typeof n?.node === "string" ? n.node.trim() : "";
|
|
605
|
+
if (!node) continue;
|
|
606
|
+
out.push({
|
|
607
|
+
node,
|
|
608
|
+
machineId: typeof n.machineId === "string" && n.machineId.trim() ? n.machineId.trim() : null,
|
|
609
|
+
lastSeenAt: typeof n.lastSeenAt === "string" ? n.lastSeenAt : null,
|
|
610
|
+
connected: n.connected === true,
|
|
611
|
+
agentCount: Array.isArray(n.instances) ? n.instances.length : 0
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
function claimCandidates(nodes, identity) {
|
|
617
|
+
const mine = new Set(identity.names);
|
|
618
|
+
const declined = new Set(identity.declined ?? []);
|
|
619
|
+
const seen = /* @__PURE__ */ new Set();
|
|
620
|
+
const out = [];
|
|
621
|
+
for (const n of nodes) {
|
|
622
|
+
const name = n.node.trim();
|
|
623
|
+
if (!name || seen.has(name)) continue;
|
|
624
|
+
if (n.machineId) continue;
|
|
625
|
+
if (mine.has(name)) continue;
|
|
626
|
+
if (declined.has(name)) continue;
|
|
627
|
+
if (n.connected) continue;
|
|
628
|
+
seen.add(name);
|
|
629
|
+
out.push(n);
|
|
630
|
+
}
|
|
631
|
+
return out.sort((a, b) => stampMs(b.lastSeenAt) - stampMs(a.lastSeenAt));
|
|
632
|
+
}
|
|
633
|
+
function resolveClaimByName(requested, nodes, identity) {
|
|
634
|
+
const byName = new Map(nodes.map((n) => [n.node, n]));
|
|
635
|
+
const claim = [];
|
|
636
|
+
const problems = [];
|
|
637
|
+
for (const raw of requested) {
|
|
638
|
+
const name = raw.trim();
|
|
639
|
+
if (!name || claim.includes(name)) continue;
|
|
640
|
+
if (identity.names.includes(name)) {
|
|
641
|
+
problems.push(`${name}: this machine already claims that name.`);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const node = byName.get(name);
|
|
645
|
+
if (!node) {
|
|
646
|
+
problems.push(`${name}: no machine on this account is registered under that name.`);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (node.machineId) {
|
|
650
|
+
problems.push(`${name}: already claimed by another machine (${node.machineId}).`);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (node.connected) {
|
|
654
|
+
problems.push(`${name}: a runner is connected there right now, so it is a different machine. Stop it first if it is not.`);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
claim.push(name);
|
|
658
|
+
}
|
|
659
|
+
return { claim, problems };
|
|
660
|
+
}
|
|
661
|
+
function relativeAge(lastSeenAt, nowMs) {
|
|
662
|
+
const at = stampMs(lastSeenAt);
|
|
663
|
+
if (!at) return "last seen unknown";
|
|
664
|
+
const mins = Math.max(0, Math.round((nowMs - at) / 6e4));
|
|
665
|
+
if (mins < 60) return `last seen ${mins} min ago`;
|
|
666
|
+
const hours = Math.round(mins / 60);
|
|
667
|
+
if (hours < 48) return `last seen ${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
668
|
+
const days = Math.round(hours / 24);
|
|
669
|
+
if (days < 45) return `last seen ${days} days ago`;
|
|
670
|
+
return `last seen ${Math.round(days / 30)} months ago`;
|
|
671
|
+
}
|
|
672
|
+
function describeCandidate(candidate, nowMs) {
|
|
673
|
+
const agents = `${candidate.agentCount} agent${candidate.agentCount === 1 ? "" : "s"}`;
|
|
674
|
+
return `${relativeAge(candidate.lastSeenAt, nowMs)} \xB7 ${agents}`;
|
|
675
|
+
}
|
|
676
|
+
function parseSelection(input, count) {
|
|
677
|
+
const raw = input.trim().toLowerCase();
|
|
678
|
+
if (!raw || raw === "n" || raw === "no" || raw === "s" || raw === "skip" || raw === "none") return { kind: "skip" };
|
|
679
|
+
if (raw === "a" || raw === "all") return { kind: "pick", indices: Array.from({ length: count }, (_, i) => i) };
|
|
680
|
+
const parts = raw.split(/[\s,]+/).filter(Boolean);
|
|
681
|
+
const indices = [];
|
|
682
|
+
for (const part of parts) {
|
|
683
|
+
if (!/^\d+$/.test(part)) return { kind: "invalid", message: `"${part}" is not a number.` };
|
|
684
|
+
const n = Number.parseInt(part, 10);
|
|
685
|
+
if (n < 1 || n > count) return { kind: "invalid", message: `${n} is not on the list (1\u2013${count}).` };
|
|
686
|
+
if (!indices.includes(n - 1)) indices.push(n - 1);
|
|
687
|
+
}
|
|
688
|
+
return indices.length ? { kind: "pick", indices } : { kind: "skip" };
|
|
689
|
+
}
|
|
690
|
+
function renderCandidates(candidates, nowMs) {
|
|
691
|
+
const width = candidates.reduce((w2, c2) => Math.max(w2, c2.node.length), 0);
|
|
692
|
+
const lines = [
|
|
693
|
+
`PAGS knows ${candidates.length} machine name${candidates.length === 1 ? "" : "s"} on this account that no machine has claimed.`,
|
|
694
|
+
"Is this machine also known as:",
|
|
695
|
+
""
|
|
696
|
+
];
|
|
697
|
+
candidates.forEach((c2, i) => {
|
|
698
|
+
lines.push(` ${i + 1}) ${c2.node.padEnd(width)} ${describeCandidate(c2, nowMs)}`);
|
|
699
|
+
});
|
|
700
|
+
lines.push("");
|
|
701
|
+
lines.push("Selecting a name merges its agents, pins and sessions onto this machine.");
|
|
702
|
+
lines.push("Pick only names THIS machine has used \u2014 a claim cannot be undone from the CLI.");
|
|
703
|
+
return lines;
|
|
704
|
+
}
|
|
705
|
+
async function fetchNodeSummaries(opts) {
|
|
706
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
707
|
+
try {
|
|
708
|
+
const res = await doFetch(`${opts.apiBase.replace(/\/$/, "")}/v1/terminals/nodes`, {
|
|
709
|
+
headers: { Authorization: `Bearer ${opts.token}` },
|
|
710
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5e3)
|
|
711
|
+
});
|
|
712
|
+
if (!res.ok) return null;
|
|
713
|
+
return parseNodesResponse(await res.json());
|
|
714
|
+
} catch {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async function maybeClaimMachineNames(opts) {
|
|
719
|
+
const env = opts.env ?? process.env;
|
|
720
|
+
const skip = claimPromptSkipReason({
|
|
721
|
+
headless: opts.headless,
|
|
722
|
+
isTTY: opts.isTTY ?? process.stdin.isTTY,
|
|
723
|
+
ci: !!env.CI,
|
|
724
|
+
suppressed: !!env.PAGS_NO_PROMPT
|
|
725
|
+
});
|
|
726
|
+
if (skip) return { prompted: false, reason: skip };
|
|
727
|
+
const identity = loadMachineIdentity();
|
|
728
|
+
if (!identity.id) return { prompted: false, reason: "no-identity" };
|
|
729
|
+
const nodes = await fetchNodeSummaries(opts);
|
|
730
|
+
if (!nodes) return { prompted: false, reason: "unavailable" };
|
|
731
|
+
const candidates = claimCandidates(nodes, identity);
|
|
732
|
+
if (!candidates.length) return { prompted: false, reason: "nothing-unclaimed" };
|
|
733
|
+
const nowMs = opts.now ?? Date.now();
|
|
734
|
+
writeLine("");
|
|
735
|
+
for (const line of renderCandidates(candidates, nowMs)) writeLine(` ${line}`);
|
|
736
|
+
writeLine("");
|
|
737
|
+
const ask = opts.ask ?? defaultAsk;
|
|
738
|
+
let selection = { kind: "skip" };
|
|
739
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
740
|
+
let answer = "";
|
|
741
|
+
try {
|
|
742
|
+
answer = await ask(" Numbers to claim (e.g. 1,2 or all), or Enter to skip: ");
|
|
743
|
+
} catch {
|
|
744
|
+
return { prompted: false, reason: "no-tty" };
|
|
745
|
+
}
|
|
746
|
+
selection = parseSelection(answer, candidates.length);
|
|
747
|
+
if (selection.kind !== "invalid") break;
|
|
748
|
+
writeLine(` ${selection.message} Enter to skip.`);
|
|
749
|
+
}
|
|
750
|
+
if (selection.kind === "invalid") {
|
|
751
|
+
writeLine(" Nothing claimed \u2014 run `pags machines claim <name>` when you know which.");
|
|
752
|
+
writeLine("");
|
|
753
|
+
return { prompted: false, reason: "unanswered" };
|
|
754
|
+
}
|
|
755
|
+
const names = candidates.map((c2) => c2.node);
|
|
756
|
+
if (selection.kind !== "pick") {
|
|
757
|
+
saveMachineIdentity(withDeclinedNames(identity, names));
|
|
758
|
+
writeLine(` Skipped \u2014 these names will not be offered again (${machineFilePath()}).`);
|
|
759
|
+
writeLine("");
|
|
760
|
+
return { prompted: true, reason: "declined", claimed: [], declined: names };
|
|
761
|
+
}
|
|
762
|
+
const claimed = selection.indices.map((i) => names[i]);
|
|
763
|
+
const rest = names.filter((n) => !claimed.includes(n));
|
|
764
|
+
saveMachineIdentity(withDeclinedNames(withClaimedNames(identity, claimed), rest));
|
|
765
|
+
writeLine(` Claiming ${claimed.join(", ")} \u2014 merged on the next register.`);
|
|
766
|
+
writeLine("");
|
|
767
|
+
return { prompted: true, reason: "claimed", claimed, declined: rest };
|
|
768
|
+
}
|
|
769
|
+
async function defaultAsk(question) {
|
|
770
|
+
const { createInterface } = await import("readline/promises");
|
|
771
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
772
|
+
try {
|
|
773
|
+
const closed = new Promise((resolve5) => rl.once("close", () => resolve5("")));
|
|
774
|
+
return await Promise.race([rl.question(question), closed]);
|
|
775
|
+
} finally {
|
|
776
|
+
rl.close();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/commands/machines.ts
|
|
781
|
+
var API_BASE2 = "https://api.proagentstore.online";
|
|
782
|
+
async function loadNodes(token) {
|
|
783
|
+
const nodes = await fetchNodeSummaries({ token, apiBase: API_BASE2 });
|
|
784
|
+
if (!nodes) {
|
|
785
|
+
writeError("Could not reach ProAgentStore to list your machines.");
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
return nodes;
|
|
789
|
+
}
|
|
790
|
+
var listCommand = new Command4("list").description("List the machines ProAgentStore has seen on this account").action(async () => {
|
|
791
|
+
const session = requireSession();
|
|
792
|
+
const nodes = await loadNodes(session.token);
|
|
793
|
+
const identity = loadMachineIdentity();
|
|
794
|
+
writeLine("");
|
|
795
|
+
writeLine(` This machine: ${identity.names[0] ?? "unknown"} ${identity.id ? `(id ${identity.id})` : "(no id \u2014 check that ~/.config/proagentstore/ is writable)"}`);
|
|
796
|
+
if (identity.names.length > 1) writeLine(` Also claims: ${identity.names.slice(1).join(", ")}`);
|
|
797
|
+
writeLine("");
|
|
798
|
+
if (!nodes.length) writeLine(" No machines registered yet \u2014 run `pags up`.");
|
|
799
|
+
const now = Date.now();
|
|
800
|
+
for (const n of nodes) {
|
|
801
|
+
const owner = n.machineId ? n.machineId === identity.id ? "this machine" : "claimed" : "unclaimed";
|
|
802
|
+
writeLine(` ${n.node}`);
|
|
803
|
+
writeLine(` ${describeCandidate(n, now)} \xB7 ${n.connected ? "connected" : "offline"} \xB7 ${owner}`);
|
|
804
|
+
}
|
|
805
|
+
writeLine("");
|
|
806
|
+
writeLine(" Claim a name this machine has used before: pags machines claim <name>");
|
|
807
|
+
writeLine("");
|
|
808
|
+
});
|
|
809
|
+
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) => {
|
|
810
|
+
const session = requireSession();
|
|
811
|
+
const identity = loadMachineIdentity();
|
|
812
|
+
if (!identity.id) {
|
|
813
|
+
writeError("This machine has no id \u2014 `~/.config/proagentstore/` is not writable, so a claim could not be sent.");
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
const nodes = await loadNodes(session.token);
|
|
817
|
+
const { claim, problems } = resolveClaimByName(names, nodes, identity);
|
|
818
|
+
for (const p of problems) writeError(` \u2717 ${p}`);
|
|
819
|
+
if (!claim.length) {
|
|
820
|
+
writeError(" Nothing claimed.");
|
|
821
|
+
process.exit(problems.length ? 1 : 0);
|
|
822
|
+
}
|
|
823
|
+
if (!saveMachineIdentity(withClaimedNames(identity, claim))) {
|
|
824
|
+
writeError(` \u2717 Could not write ${machineFilePath()} \u2014 nothing claimed.`);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
writeLine(` \u2713 Claimed ${claim.join(", ")}. Restart \`pags up\` to merge them onto this machine.`);
|
|
828
|
+
});
|
|
829
|
+
var machinesCommand = new Command4("machines").description("Show and claim the machine names ProAgentStore has for this account").addCommand(listCommand, { isDefault: true }).addCommand(claimCommand);
|
|
830
|
+
|
|
500
831
|
// src/commands/mcp.ts
|
|
501
832
|
import { spawn } from "child_process";
|
|
502
|
-
import { Command as
|
|
833
|
+
import { Command as Command5 } from "commander";
|
|
503
834
|
var DEFAULT_MCP_URL = "https://mcp.proagentstore.online/mcp";
|
|
504
835
|
function buildMcpRemoteArgs(opts, extraArgs = []) {
|
|
505
836
|
return ["-y", "mcp-remote", opts.url || DEFAULT_MCP_URL, ...extraArgs];
|
|
@@ -517,25 +848,25 @@ async function runMcpProxy(opts, extraArgs = []) {
|
|
|
517
848
|
});
|
|
518
849
|
});
|
|
519
850
|
}
|
|
520
|
-
var mcpCommand = new
|
|
851
|
+
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) => {
|
|
521
852
|
await runMcpProxy(opts, args);
|
|
522
853
|
});
|
|
523
854
|
|
|
524
855
|
// src/commands/publish.ts
|
|
525
856
|
import { execFileSync } from "child_process";
|
|
526
|
-
import { existsSync as
|
|
527
|
-
import { join as
|
|
528
|
-
import { Command as
|
|
529
|
-
var publishCommand = new
|
|
857
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
858
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
859
|
+
import { Command as Command6 } from "commander";
|
|
860
|
+
var publishCommand = new Command6("publish").description("Publish an agent to ProAgentStore").option("-d, --dir <path>", "Agent directory", ".").action(async (opts) => {
|
|
530
861
|
const dir = resolve3(opts.dir);
|
|
531
|
-
const manifestPath =
|
|
532
|
-
if (!
|
|
862
|
+
const manifestPath = join5(dir, "agent.json");
|
|
863
|
+
if (!existsSync5(manifestPath)) {
|
|
533
864
|
writeError("No agent.json found. Run `pags init` first.");
|
|
534
865
|
process.exit(1);
|
|
535
866
|
}
|
|
536
867
|
let manifest;
|
|
537
868
|
try {
|
|
538
|
-
manifest = JSON.parse(
|
|
869
|
+
manifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
|
|
539
870
|
} catch (e) {
|
|
540
871
|
writeError(`agent.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
541
872
|
process.exit(1);
|
|
@@ -618,56 +949,10 @@ var publishCommand = new Command5("publish").description("Publish an agent to Pr
|
|
|
618
949
|
// src/commands/runner/command.ts
|
|
619
950
|
import { spawn as spawn3 } from "child_process";
|
|
620
951
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
621
|
-
import { Command as
|
|
952
|
+
import { Command as Command7 } from "commander";
|
|
622
953
|
|
|
623
954
|
// src/commands/runner/http.ts
|
|
624
955
|
import { hostname as hostname2 } from "os";
|
|
625
|
-
|
|
626
|
-
// src/machine.ts
|
|
627
|
-
import { randomUUID } from "crypto";
|
|
628
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
629
|
-
import { homedir as homedir2, hostname } from "os";
|
|
630
|
-
import { join as join5 } from "path";
|
|
631
|
-
var CONFIG_DIR2 = join5(homedir2(), ".config", "proagentstore");
|
|
632
|
-
var MACHINE_FILE = join5(CONFIG_DIR2, "machine.json");
|
|
633
|
-
var MAX_NAMES = 10;
|
|
634
|
-
function isValidMachineId(value) {
|
|
635
|
-
return typeof value === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(value);
|
|
636
|
-
}
|
|
637
|
-
function parseMachineFile(text) {
|
|
638
|
-
try {
|
|
639
|
-
const data = JSON.parse(text);
|
|
640
|
-
if (!isValidMachineId(data.id)) return null;
|
|
641
|
-
const names = Array.isArray(data.names) ? data.names.filter((n) => typeof n === "string" && n.trim().length > 0).map((n) => n.trim()) : [];
|
|
642
|
-
return { id: data.id, names: names.slice(0, MAX_NAMES) };
|
|
643
|
-
} catch {
|
|
644
|
-
return null;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
function withName(identity, name) {
|
|
648
|
-
const current = name.trim();
|
|
649
|
-
const prev = identity?.names ?? [];
|
|
650
|
-
const names = current ? [current, ...prev.filter((n) => n !== current)] : [...prev];
|
|
651
|
-
return { id: identity?.id ?? "", names: names.slice(0, MAX_NAMES) };
|
|
652
|
-
}
|
|
653
|
-
function loadMachineIdentity(now = hostname()) {
|
|
654
|
-
let stored = null;
|
|
655
|
-
try {
|
|
656
|
-
if (existsSync5(MACHINE_FILE)) stored = parseMachineFile(readFileSync4(MACHINE_FILE, "utf-8"));
|
|
657
|
-
} catch {
|
|
658
|
-
}
|
|
659
|
-
const next = withName(stored ?? { id: randomUUID(), names: [] }, now);
|
|
660
|
-
if (stored && stored.id === next.id && stored.names.join("\0") === next.names.join("\0")) return next;
|
|
661
|
-
try {
|
|
662
|
-
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
663
|
-
writeFileSync3(MACHINE_FILE, JSON.stringify(next, null, 2));
|
|
664
|
-
return next;
|
|
665
|
-
} catch {
|
|
666
|
-
return stored ?? { id: "", names: [] };
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// src/commands/runner/http.ts
|
|
671
956
|
function clean(value) {
|
|
672
957
|
const trimmed = value?.trim();
|
|
673
958
|
return trimmed || void 0;
|
|
@@ -1119,7 +1404,7 @@ function collectCapability(value, previous = []) {
|
|
|
1119
1404
|
return [...previous, value];
|
|
1120
1405
|
}
|
|
1121
1406
|
function createRunnerCommand() {
|
|
1122
|
-
const command = new
|
|
1407
|
+
const command = new Command7("runner").description(
|
|
1123
1408
|
"Manage the local ProAgentStore browser runtime for ProAgentStore agents"
|
|
1124
1409
|
);
|
|
1125
1410
|
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 +1569,7 @@ var runnerCommand = createRunnerCommand();
|
|
|
1284
1569
|
|
|
1285
1570
|
// src/commands/up.ts
|
|
1286
1571
|
import { createRequire as createRequire2 } from "module";
|
|
1287
|
-
import { Command as
|
|
1572
|
+
import { Command as Command8 } from "commander";
|
|
1288
1573
|
|
|
1289
1574
|
// src/tui.ts
|
|
1290
1575
|
import chalk from "chalk";
|
|
@@ -1393,7 +1678,7 @@ async function waitForKey(keys, onInterrupt) {
|
|
|
1393
1678
|
}
|
|
1394
1679
|
|
|
1395
1680
|
// src/commands/up.ts
|
|
1396
|
-
var
|
|
1681
|
+
var API_BASE3 = "https://api.proagentstore.online";
|
|
1397
1682
|
var CLI_VERSION2 = createRequire2(import.meta.url)("../package.json").version;
|
|
1398
1683
|
async function stopRunnerProcesses() {
|
|
1399
1684
|
if (process.platform === "win32") return false;
|
|
@@ -1413,7 +1698,7 @@ async function stopRunnerProcesses() {
|
|
|
1413
1698
|
}
|
|
1414
1699
|
return stopped;
|
|
1415
1700
|
}
|
|
1416
|
-
var upCommand = new
|
|
1701
|
+
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
1702
|
const session = requireSession();
|
|
1418
1703
|
const state = {
|
|
1419
1704
|
user: session.user.login,
|
|
@@ -1431,7 +1716,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1431
1716
|
printLogo(CLI_VERSION2);
|
|
1432
1717
|
printStep("Signed in as " + session.user.login, "ok");
|
|
1433
1718
|
printStep("Fetching instances...", "wait");
|
|
1434
|
-
const res = await fetch(`${
|
|
1719
|
+
const res = await fetch(`${API_BASE3}/v1/instances/my/instances`, {
|
|
1435
1720
|
headers: { Authorization: `Bearer ${session.token}` }
|
|
1436
1721
|
});
|
|
1437
1722
|
if (!res.ok) {
|
|
@@ -1462,6 +1747,11 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1462
1747
|
for (const inst of state.instances) {
|
|
1463
1748
|
writeLine(` ${inst.name} (${inst.id.slice(0, 8)}...)`);
|
|
1464
1749
|
}
|
|
1750
|
+
await maybeClaimMachineNames({
|
|
1751
|
+
token: session.token,
|
|
1752
|
+
apiBase: API_BASE3,
|
|
1753
|
+
headless: opts.headless
|
|
1754
|
+
}).catch(() => void 0);
|
|
1465
1755
|
state.activeInstance = instances.length === 1 ? instances[0].name || instances[0].slug || instances[0].id.slice(0, 8) : `${instances.length} agents`;
|
|
1466
1756
|
printStep(`Connecting ${state.activeInstance}\u2026`, "wait");
|
|
1467
1757
|
await stopRunnerProcesses();
|
|
@@ -1591,7 +1881,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
|
|
|
1591
1881
|
}
|
|
1592
1882
|
}
|
|
1593
1883
|
});
|
|
1594
|
-
var downCommand = new
|
|
1884
|
+
var downCommand = new Command8("down").description("Stop the browser runner and disconnect").action(async () => {
|
|
1595
1885
|
clearScreen();
|
|
1596
1886
|
printLogo(CLI_VERSION2);
|
|
1597
1887
|
if (process.platform === "win32") {
|
|
@@ -1613,7 +1903,7 @@ var downCommand = new Command7("down").description("Stop the browser runner and
|
|
|
1613
1903
|
// src/index.ts
|
|
1614
1904
|
var require2 = createRequire3(import.meta.url);
|
|
1615
1905
|
var { version } = require2("../package.json");
|
|
1616
|
-
var program = new
|
|
1906
|
+
var program = new Command9();
|
|
1617
1907
|
program.name("pags").description(
|
|
1618
1908
|
"ProAgentStore CLI \u2014 create and publish server-powered AI agents"
|
|
1619
1909
|
).version(version);
|
|
@@ -1626,6 +1916,7 @@ program.addCommand(initCommand);
|
|
|
1626
1916
|
program.addCommand(checkCommand);
|
|
1627
1917
|
program.addCommand(publishCommand);
|
|
1628
1918
|
program.addCommand(runnerCommand);
|
|
1919
|
+
program.addCommand(machinesCommand);
|
|
1629
1920
|
program.addCommand(mcpCommand);
|
|
1630
1921
|
try {
|
|
1631
1922
|
await program.parseAsync();
|