@proagentstore/cli 0.4.39 → 0.4.41

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.
@@ -1,110 +1,22 @@
1
1
  /**
2
- * Per-CLI behaviour for the coding runtime.
2
+ * Per-CLI facts the coding runtime needs before it can spawn an engine.
3
3
  *
4
- * Each AI coding CLI renders its TUI differently, so "is it ready for input?",
5
- * "is it still working?", and "where does its answer start/end in the pane?" are
6
- * CLI-specific. These handlers encapsulate that, ported from the AgentCoder
7
- * `bridge/src/agents/handlers/*`. A `generic` shell handler is included so the
8
- * runtime (and tests) can drive a plain shell deterministically.
4
+ * This file used to carry pane heuristics too — `isReady` / `isProcessing` / `extractResponse` /
5
+ * `completion`, ported from AgentCoder's `bridge/src/agents/handlers/*` — which answered
6
+ * "is it ready for input?" by matching strings a vendor prints in its TUI. They died with the
7
+ * tmux backend (commit c94642f): a session is a child process now, so Claude's turn boundary is a
8
+ * `result` event and a raw engine's is process exit. Nothing had called them since, and keeping
9
+ * them left a live-looking `pane.includes("ctrl+c to interrupt")` one vendor release away from
10
+ * being silently wrong — for anyone who wired it back up believing it worked.
11
+ *
12
+ * What remains is data: which binary this engine is, and which env var carries its key.
9
13
  */
10
- /** Shared "find the user's input line, return everything after it up to the next prompt". */
11
- function sliceAfterInput(captured, userInput, isPromptLine) {
12
- const lines = captured.split("\n");
13
- const needle = userInput.slice(0, 25);
14
- let start = -1;
15
- if (needle) {
16
- for (let i = lines.length - 1; i >= 0; i--) {
17
- if (lines[i].includes(needle)) {
18
- start = i;
19
- break;
20
- }
21
- }
22
- }
23
- if (start === -1)
24
- return lines.slice(-200).join("\n").trim();
25
- let end = lines.length;
26
- for (let i = lines.length - 1; i > start; i--) {
27
- if (isPromptLine(lines[i].trim())) {
28
- end = i;
29
- break;
30
- }
31
- }
32
- return lines.slice(start + 1, end).join("\n").trim();
33
- }
34
- export class ClaudeHandler {
35
- clientType = "claude";
36
- cliCommand = "claude --dangerously-skip-permissions";
37
- envVar = "ANTHROPIC_API_KEY";
38
- isProcessing(pane) {
39
- return pane.includes("ctrl+c to interrupt") || /Working|Thinking|Reading|Searching|Running|Editing|Writing/.test(pane);
40
- }
41
- isReady(pane) {
42
- if (this.isProcessing(pane))
43
- return false;
44
- const tail = pane.split("\n").slice(-15).join("\n");
45
- return (tail.includes("bypass permissions") ||
46
- tail.includes("? for shortcuts") ||
47
- /❯\s*$/.test(tail) ||
48
- /❯ .*↵ send/.test(tail));
49
- }
50
- extractResponse(captured, userInput) {
51
- return sliceAfterInput(captured, userInput, (l) => l === "❯" || l.startsWith("❯ "));
52
- }
53
- completion() {
54
- return { minWait: 1000, stableThreshold: 0, forceCompleteAfter: 5 * 60 * 1000, pollInterval: 500 };
55
- }
56
- }
57
- export class GeminiHandler {
58
- clientType = "gemini";
59
- cliCommand = "gemini";
60
- envVar = "GEMINI_API_KEY";
61
- isProcessing(pane) {
62
- return /thinking|processing|generating|working|loading/i.test(pane);
63
- }
64
- isReady(pane) {
65
- const last = pane.split("\n").slice(-1)[0]?.trim() ?? "";
66
- const hasPrompt = /[>$]\s*$/.test(last) || /^>/.test(last);
67
- return hasPrompt && !this.isProcessing(pane);
68
- }
69
- extractResponse(captured, userInput) {
70
- return sliceAfterInput(captured, userInput, (l) => /^[>$]\s*$/.test(l) || /^[>$]\s+\S/.test(l));
71
- }
72
- completion() {
73
- return { stableThreshold: 1500, forceCompleteAfter: 5 * 60 * 1000, pollInterval: 500 };
74
- }
75
- }
76
- /** Codex / Grok render close enough to a generic prompt; reuse the shell heuristics. */
77
- export class GenericHandler {
78
- clientType;
79
- cliCommand;
80
- envVar;
81
- constructor(clientType = "generic", cliCommand = "bash", envVar = "") {
82
- this.clientType = clientType;
83
- this.cliCommand = cliCommand;
84
- this.envVar = envVar;
85
- }
86
- isProcessing(pane) {
87
- const last = pane.split("\n").slice(-1)[0]?.trim() ?? "";
88
- return /\.\.\.$/.test(last);
89
- }
90
- isReady(pane) {
91
- const last = pane.split("\n").slice(-1)[0]?.trim() ?? "";
92
- // A shell prompt ends in $, #, >, or % (optionally followed by a cursor space).
93
- return /[$#>%]\s*$/.test(last) && !this.isProcessing(pane);
94
- }
95
- extractResponse(captured, userInput) {
96
- return sliceAfterInput(captured, userInput, (l) => /[$#>%]\s*$/.test(l));
97
- }
98
- completion() {
99
- return { stableThreshold: 800, forceCompleteAfter: 60 * 1000, pollInterval: 300 };
100
- }
101
- }
102
14
  const HANDLERS = {
103
- claude: new ClaudeHandler(),
104
- gemini: new GeminiHandler(),
105
- codex: new GenericHandler("codex", "codex", "OPENAI_API_KEY"),
106
- grok: new GenericHandler("grok", "grok", "XAI_API_KEY"),
107
- generic: new GenericHandler(),
15
+ claude: { clientType: "claude", cliCommand: "claude --dangerously-skip-permissions", envVar: "ANTHROPIC_API_KEY" },
16
+ gemini: { clientType: "gemini", cliCommand: "gemini --approval-mode yolo --skip-trust --prompt", envVar: "GEMINI_API_KEY" },
17
+ codex: { clientType: "codex", cliCommand: "codex exec --sandbox danger-full-access", envVar: "OPENAI_API_KEY" },
18
+ grok: { clientType: "grok", cliCommand: "grok --permission-mode bypassPermissions -p", envVar: "XAI_API_KEY" },
19
+ generic: { clientType: "generic", cliCommand: "bash", envVar: "" },
108
20
  };
109
21
  export function handlerFor(clientType) {
110
22
  return HANDLERS[clientType] ?? HANDLERS.generic;
@@ -44,6 +44,19 @@ const MAX_PENDING_USAGE = 200;
44
44
  * matter most, whereas the merge that started an incident is the one you cannot afford to lose.
45
45
  */
46
46
  const MAX_PENDING_ACTS = 100;
47
+ /**
48
+ * Absolute ceiling on ONE one-shot turn (#391).
49
+ *
50
+ * The old idle heuristic carried a 15-minute backstop so a wedged engine could not sit "thinking"
51
+ * forever. Now that exit is the only thing that ends a one-shot turn, that ceiling has to be
52
+ * ENFORCED rather than inferred: a rule that merely relabels a still-running process as idle is
53
+ * the very defect #391 is about — it hands the next turn a repo another engine is still editing.
54
+ *
55
+ * So the timer ends the turn (SIGTERM) and says so in the transcript. Removing the ceiling
56
+ * outright would trade an early kill for a permanent hang, which is worse: nothing else on this
57
+ * path can unstick a process that never exits, and the console's Restart is a human noticing.
58
+ */
59
+ const MAX_ONE_SHOT_TURN_MS = 15 * 60 * 1000;
47
60
  export class HeadlessSession {
48
61
  config;
49
62
  /**
@@ -67,11 +80,11 @@ export class HeadlessSession {
67
80
  cmdBin;
68
81
  cmdArgs;
69
82
  binName;
70
- /** Wall-clock of the last stdout/stderr byte — drives the raw-mode idle heuristic. */
83
+ /** Wall-clock of the last stdout/stderr byte — drives the persistent-raw idle heuristic. */
71
84
  lastOutputAt = 0;
72
- /** Raw-mode: has any output arrived since the current turn's input? */
85
+ /** Persistent-raw: has any output arrived since the current turn's input? */
73
86
  sawOutputSinceInput = false;
74
- /** Raw-mode: when the current turn started (absolute idle backstop). */
87
+ /** Persistent-raw: when the current turn started (absolute idle backstop). */
75
88
  turnStartedAt = 0;
76
89
  /** Set by stop() — the only thing that ends a one-shot session (see `alive`). */
77
90
  stopped = false;
@@ -198,7 +211,27 @@ export class HeadlessSession {
198
211
  runState() {
199
212
  if (!this.alive)
200
213
  return "idle";
201
- // Raw engines have no "turn over" event, so we INFER idle. Three rules, in order:
214
+ // A ONE-SHOT engine's turn IS a process, so its exit is an exact end-of-turn signal and no
215
+ // timer may pre-empt it (#391). Idle is set by the `close` handler; here the only question
216
+ // is whether that process is still running.
217
+ //
218
+ // This used to fall through to the timer rules below, which were written for a PERSISTENT
219
+ // interactive CLI — one with no other signal to read. Against a one-shot engine the 1.5s
220
+ // quiet rule could only ever fire EARLY: any pause inside a turn (a test suite, an install,
221
+ // a slow network fetch) read as "finished", the Pilot sent turn 2, and `runOneShot` killed
222
+ // turn 1 to keep two engines off one repo. The heuristic that existed to prevent a
223
+ // premature finish was causing one, and destroying the work in flight to do it.
224
+ //
225
+ // The ceiling that stops a wedged process is armed in `runOneShot` — it ENDS the turn
226
+ // rather than relabelling a live one as idle, which is the same mistake in slower form.
227
+ if (this.oneShot)
228
+ return this.procAlive ? "thinking" : "idle";
229
+ // Below: a PERSISTENT non-Claude engine — alive between turns, so exit says nothing about
230
+ // a turn and idle must be inferred. None ships today; every raw engine is one-shot. The
231
+ // gate is `!oneShot` rather than `mode === "raw"` because the latter now means the
232
+ // OPPOSITE of the condition these rules were written for.
233
+ //
234
+ // Three rules, in order:
202
235
  // 1. produced output, then went quiet for 1.5s → settled (the common case).
203
236
  // 2. NEVER produced output but 8s elapsed → a silent turn (just a prompt) is done.
204
237
  // 3. absolute 15-min backstop → never wedge "thinking" forever (e.g. a heartbeat
@@ -316,7 +349,7 @@ export class HeadlessSession {
316
349
  const now = Date.now();
317
350
  this.lastOutputAt = now;
318
351
  this.turnStartedAt = now;
319
- this.sawOutputSinceInput = false; // arm the raw idle heuristic for THIS turn
352
+ this.sawOutputSinceInput = false; // arm the persistent-raw idle heuristic for THIS turn
320
353
  try {
321
354
  if (this.mode === "stream-json") {
322
355
  const msg = JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text }] } });
@@ -353,11 +386,17 @@ export class HeadlessSession {
353
386
  env: mergeEnv(process.env, this.config.env),
354
387
  stdio: ["ignore", "pipe", "pipe"],
355
388
  });
356
- // A turn already running is aborted before its replacement starts. `input()` never killed
357
- // the previous process, and the raw idle heuristic declares idle after a >1.5s output
358
- // pause — so a long build could be judged idle, the brain sends turn 2, and TWO engine
359
- // processes edit the same repo at once.
389
+ // A turn already running is aborted before its replacement starts: `input()` accepts a turn
390
+ // at any time — a human typing in the console, a takeover, a Loop that stopped waiting —
391
+ // so without this TWO engine processes edit the same repo at once. Still needed after #391
392
+ // made exit authoritative: that stops the Pilot from being TOLD a running turn is over, it
393
+ // does not stop anyone from sending anyway.
394
+ //
395
+ // It SAYS what it destroyed, for the same reason the non-zero exit code goes into the
396
+ // transcript below: a turn's work vanishing with no line in the record is how the Pilot
397
+ // ends up reasoning about a turn that never finished, with nothing to explain the gap.
360
398
  if (this.procAlive) {
399
+ this.push(`[${this.config.clientType} turn aborted — a new instruction arrived while the previous one was still running]`);
361
400
  try {
362
401
  this.proc?.kill();
363
402
  }
@@ -380,7 +419,23 @@ export class HeadlessSession {
380
419
  });
381
420
  proc.stdout?.on("data", (d) => this.onStdout(d.toString()));
382
421
  proc.stderr?.on("data", (d) => this.onStdout(d.toString()));
422
+ // The enforced ceiling (#391). `unref` so a pending timer can never keep the runner's
423
+ // event loop alive past its own shutdown.
424
+ const maxTurnMs = this.config.maxTurnMs ?? MAX_ONE_SHOT_TURN_MS;
425
+ const ceiling = setTimeout(() => {
426
+ if (this.proc !== proc)
427
+ return; // a newer turn owns the session; this one is already gone
428
+ this.push(`[${this.config.clientType} turn ended after ${Math.round(maxTurnMs / 60000)}m — the engine never exited, so the session was unwedged]`);
429
+ try {
430
+ proc.kill();
431
+ }
432
+ catch {
433
+ /* already gone */
434
+ }
435
+ }, maxTurnMs);
436
+ ceiling.unref();
383
437
  proc.on("close", (code) => {
438
+ clearTimeout(ceiling); // cleared before the staleness guard: the timer belongs to THIS process
384
439
  // A non-zero exit is the engine's own failure (bad flags, not signed in) and the
385
440
  // operator needs to see it — silently going idle is how "stdin is not a terminal"
386
441
  // looked like an idle session for a whole afternoon.
@@ -11,8 +11,8 @@ export class CodingRuntime {
11
11
  /**
12
12
  * Active human handoffs keyed by session id. `resolved` flips when the human
13
13
  * finishes (console "Resume" / submits a value); the brain workflow polls
14
- * {@link takeoverStatus} and continues once it does — the tmux analogue of the
15
- * browser runtime's handoff-status machinery.
14
+ * {@link takeoverStatus} and continues once it does — the coding-session analogue of
15
+ * the browser runtime's handoff-status machinery.
16
16
  */
17
17
  takeovers = new Map();
18
18
  /** Base directory under which repos are cloned (one subdir per repo). */
package/dist/index.js CHANGED
@@ -617,11 +617,57 @@ var publishCommand = new Command5("publish").description("Publish an agent to Pr
617
617
 
618
618
  // src/commands/runner/command.ts
619
619
  import { spawn as spawn3 } from "child_process";
620
- import { randomUUID } from "crypto";
620
+ import { randomUUID as randomUUID2 } from "crypto";
621
621
  import { Command as Command6 } from "commander";
622
622
 
623
623
  // src/commands/runner/http.ts
624
- import { hostname } from "os";
624
+ 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
625
671
  function clean(value) {
626
672
  const trimmed = value?.trim();
627
673
  return trimmed || void 0;
@@ -647,13 +693,17 @@ function apiPathSegment(value) {
647
693
  return encodeURIComponent(value);
648
694
  }
649
695
  function buildRuntimeRegistrationBody(opts, capabilities = []) {
696
+ const node = hostname2();
697
+ const machine = loadMachineIdentity(node);
650
698
  return {
651
699
  endpointUrl: clean(opts.endpointUrl) || opts.endpointUrl,
652
700
  token: clean(opts.runnerToken) || clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN),
653
701
  placement: opts.placement === "managed" ? "managed" : "local",
654
702
  capabilities,
655
703
  runnerVersion: clean(opts.runnerVersion) || "",
656
- runnerNode: hostname()
704
+ runnerNode: node,
705
+ machineId: machine.id,
706
+ machineNames: machine.names
657
707
  };
658
708
  }
659
709
  async function requestRunner(method, path, opts, body) {
@@ -708,7 +758,7 @@ function responseErrorMessage(data, text, statusText) {
708
758
 
709
759
  // src/commands/runner/process.ts
710
760
  import { spawn as spawn2 } from "child_process";
711
- import { existsSync as existsSync5 } from "fs";
761
+ import { existsSync as existsSync6 } from "fs";
712
762
  import { resolve as resolve4 } from "path";
713
763
  import { createServer as createServer2 } from "net";
714
764
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -745,7 +795,7 @@ function buildRunnerArgs(opts) {
745
795
  function findWorkspaceRoot() {
746
796
  let dir = process.cwd();
747
797
  for (let i = 0; i < 8; i++) {
748
- if (existsSync5(resolve4(dir, "pnpm-workspace.yaml"))) return dir;
798
+ if (existsSync6(resolve4(dir, "pnpm-workspace.yaml"))) return dir;
749
799
  const parent = resolve4(dir, "..");
750
800
  if (parent === dir) break;
751
801
  dir = parent;
@@ -763,10 +813,10 @@ function runnerSpawnSpec(opts) {
763
813
  let cwd = root;
764
814
  let command = "pags-browser-runner";
765
815
  let args = runnerArgs;
766
- if (existsSync5(localPackage)) {
816
+ if (existsSync6(localPackage)) {
767
817
  command = "pnpm";
768
818
  args = ["--filter", "@proagentstore/browser-runner", "dev", "--", ...runnerArgs];
769
- } else if (existsSync5(bundledPackage)) {
819
+ } else if (existsSync6(bundledPackage)) {
770
820
  cwd = process.cwd();
771
821
  command = process.execPath;
772
822
  args = [bundledPackage, ...runnerArgs];
@@ -804,19 +854,19 @@ async function waitForLocalRunner(opts, timeoutMs = 15e3) {
804
854
  }
805
855
 
806
856
  // src/commands/runner/relay.ts
807
- import { hostname as hostname2 } from "os";
857
+ import { hostname as hostname3 } from "os";
808
858
 
809
859
  // src/commands/runner/membership.ts
810
- function isEligible(inst, thisNode) {
860
+ function isEligible(inst, thisNode, alsoKnownAs = []) {
811
861
  if (inst.status !== "active") return false;
812
862
  if (inst.capabilities?.runtime == null) return false;
813
863
  const pin = inst.config?.runnerNode;
814
- if (pin && pin !== thisNode) return false;
864
+ if (pin && pin !== thisNode && !alsoKnownAs.includes(pin)) return false;
815
865
  return true;
816
866
  }
817
- function diffMembership(attached, eligible, thisNode, blocked = /* @__PURE__ */ new Set()) {
867
+ function diffMembership(attached, eligible, thisNode, blocked = /* @__PURE__ */ new Set(), alsoKnownAs = []) {
818
868
  const have = new Set(attached);
819
- const want = eligible.filter((i) => isEligible(i, thisNode));
869
+ const want = eligible.filter((i) => isEligible(i, thisNode, alsoKnownAs));
820
870
  const wantIds = new Set(want.map((i) => i.id));
821
871
  return {
822
872
  attach: want.filter((i) => !have.has(i.id) && !blocked.has(i.id)),
@@ -836,7 +886,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
836
886
  const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
837
887
  const pagsToken = clean(opts.pagsToken) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
838
888
  if (!pagsToken) throw new Error("PAGS token required for WebSocket relay");
839
- const runnerNode = hostname2();
889
+ const runnerNode = hostname3();
890
+ const machine = loadMachineIdentity(runnerNode);
840
891
  const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
841
892
  const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
842
893
  const registerRuntime = async (id) => {
@@ -848,6 +899,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
848
899
  capabilities: caps,
849
900
  runnerVersion: CLI_VERSION,
850
901
  runnerNode,
902
+ machineId: machine.id,
903
+ machineNames: machine.names,
851
904
  force
852
905
  });
853
906
  } catch (e) {
@@ -881,7 +934,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
881
934
  writeLine("Runtime registered with PAGS \u2713");
882
935
  writeLine("");
883
936
  writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
884
- writeLine(` \u2705 CONNECTED \u2014 WebSocket relay \xB7 ${hostname2()}`);
937
+ writeLine(` \u2705 CONNECTED \u2014 WebSocket relay \xB7 ${hostname3()}`);
885
938
  writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"}`);
886
939
  writeLine(" No cloudflared needed. Ctrl+C to disconnect.");
887
940
  writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
@@ -922,7 +975,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
922
975
  attached.keys(),
923
976
  res.instances ?? [],
924
977
  runnerNode,
925
- blocked
978
+ blocked,
979
+ // The names this machine has also worn. Without them a pin made under a
980
+ // previous hostname reads as "pinned to another machine", and this poll
981
+ // detaches the agent twenty seconds after startup attached it (#379).
982
+ machine.names
926
983
  );
927
984
  for (const inst of toAttach) {
928
985
  await registerRuntime(inst.id);
@@ -963,7 +1020,7 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
963
1020
  backoffMs = Math.min(backoffMs * 2, 3e4);
964
1021
  return;
965
1022
  }
966
- const params = new URLSearchParams({ token: relayToken, node: hostname2() });
1023
+ const params = new URLSearchParams({ token: relayToken, node: hostname3() });
967
1024
  if (force) params.set("force", "1");
968
1025
  const url = `${wsBase}/v1/relay/${encodeURIComponent(instanceId)}/connect?${params.toString()}`;
969
1026
  const ws = new WebSocket(url);
@@ -1069,7 +1126,7 @@ function createRunnerCommand() {
1069
1126
  await startRunnerForeground(opts);
1070
1127
  });
1071
1128
  command.command("connect <instanceIds...>").description("Start ONE local runtime, connect via WebSocket relay, and register it for every given PAGS instance").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "49171").option("--data-dir <path>", "Runner data directory").option("--token <token>", "Runner bearer token. Defaults to PAGS_RUNNER_TOKEN or a generated token").option("--headless", "Run Playwright headless").option("--api-base <url>", "PAGS API base URL").option("--pags-token <token>", "PAGS session token. Defaults to PAGS_TOKEN").option("--runner-version <version>", "Runner version").option("--force", "Take over from another connected machine").option("--watch-instances", "Attach newly eligible agents while running, without a restart").action(async (instanceIds, opts) => {
1072
- const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`;
1129
+ const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID2()}`;
1073
1130
  const host = clean(opts.host) || "127.0.0.1";
1074
1131
  const port = clean(opts.port) || String(await findFreePort2(49171));
1075
1132
  const localUrl = `http://${host}:${port}`;
@@ -1231,7 +1288,7 @@ import { Command as Command7 } from "commander";
1231
1288
 
1232
1289
  // src/tui.ts
1233
1290
  import chalk from "chalk";
1234
- import { hostname as hostname3 } from "os";
1291
+ import { hostname as hostname4 } from "os";
1235
1292
  import readline from "readline";
1236
1293
  var ACCENT = "#7c3aed";
1237
1294
  var c = chalk.hex(ACCENT);
@@ -1273,7 +1330,7 @@ function printStatus(state) {
1273
1330
  clearScreen();
1274
1331
  printLogo(state.version);
1275
1332
  const connected = state.runner === "online" && state.tunnel === "online" && state.registration === "registered";
1276
- console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(hostname3()));
1333
+ console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(hostname4()));
1277
1334
  console.log("");
1278
1335
  const row = (kind, s) => {
1279
1336
  const { icon, label, note } = describe(kind, s);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.39",
3
+ "version": "0.4.41",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",