@proagentstore/cli 0.4.46 → 0.4.47

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.
@@ -76,7 +76,14 @@ export function activeTerminalCommand(target, backend) {
76
76
  switch (t.backend) {
77
77
  case "tmux": {
78
78
  const out = tmuxExec(["display-message", "-p", "-t", t.id, "#{pane_current_command}"]).trim();
79
- return out || null;
79
+ // `#{pane_current_command}` reads the pane process's argv, and Claude Code rewrites
80
+ // its own — measured on this platform's flagship case, tmux answered `2.1.226` (the
81
+ // version) for a pane whose process `comm` was `claude`. So `aiCliDrives` was 0 for a
82
+ // week of driving Claude Code through a tmux Operator. `comm` is the executable name
83
+ // and no title rewrite touches it, so ask the process tree when the argv is not a
84
+ // name (#498). Only then: a pane reporting `zsh` genuinely has nothing in the
85
+ // foreground, and promoting a background child would be an invention.
86
+ return resolvePaneCommand(out, () => paneDescendantComms(t.id));
80
87
  }
81
88
  case "kitty":
82
89
  return kittyForegroundCommand(t.id);
@@ -90,6 +97,102 @@ export function activeTerminalCommand(target, backend) {
90
97
  return null;
91
98
  }
92
99
  }
100
+ /** Shell names that mean "the pane is at a prompt", not "this program is running". */
101
+ const SHELL_NAMES = new Set(["sh", "bash", "zsh", "fish", "dash", "ksh", "tcsh", "csh", "login", "screen", "tmux"]);
102
+ /** A program name is a bare token. A version (`2.1.226`) and a sentence are both NOT one. */
103
+ export function looksLikeCommandName(raw) {
104
+ const value = String(raw ?? "").trim();
105
+ if (!value || value.length > 60)
106
+ return false;
107
+ if (/^v?\d+(\.\d+)+$/.test(value))
108
+ return false; // a version string is not a program name
109
+ return /^[A-Za-z_][\w.@+-]*$/.test(value);
110
+ }
111
+ /**
112
+ * What the pane is running, given what tmux SAID and a way to ask the process tree (#498).
113
+ *
114
+ * Pure so the measured fixture — tmux says `2.1.226`, the pane's child `comm` is `claude` — is a
115
+ * unit test rather than a story. Keeps tmux's answer whenever it is a name: that is the fast path,
116
+ * it costs no extra exec, and the descendant walk can only ever be a second-best guess.
117
+ */
118
+ export function resolvePaneCommand(reported, listDescendantComms) {
119
+ const value = String(reported ?? "").trim();
120
+ if (looksLikeCommandName(value))
121
+ return value;
122
+ // The probe is best-effort by construction: `pgrep`/`ps` availability differs by platform, and
123
+ // a missing one must degrade to what tmux said, not lose it.
124
+ let descendants = [];
125
+ try {
126
+ descendants = listDescendantComms();
127
+ }
128
+ catch {
129
+ descendants = [];
130
+ }
131
+ for (const raw of descendants) {
132
+ const name = (String(raw ?? "").trim().split("/").pop() ?? "").trim();
133
+ if (!looksLikeCommandName(name))
134
+ continue;
135
+ if (SHELL_NAMES.has(name.toLowerCase()))
136
+ continue;
137
+ return name;
138
+ }
139
+ // Nothing better found: hand back exactly what tmux said. The cloud decides what an
140
+ // unreadable value means; this module does not invent a name it did not read.
141
+ return value || null;
142
+ }
143
+ /** Bounded, so a probe on a path a Loop drives continuously can never become the slow part. */
144
+ const PROBE_TIMEOUT_MS = 1_000;
145
+ /** `comm` for the pane's descendant processes, nearest first. `[]` on any failure. */
146
+ function paneDescendantComms(paneId) {
147
+ let pid = "";
148
+ try {
149
+ pid = tmuxExec(["display-message", "-p", "-t", paneId, "#{pane_pid}"], PROBE_TIMEOUT_MS).trim();
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ if (!/^\d+$/.test(pid))
155
+ return [];
156
+ const found = [];
157
+ let frontier = [pid];
158
+ // Two levels is enough for `zsh → claude` and for one wrapper in between, and it bounds the
159
+ // work: `pgrep`/`ps` availability differs by platform, so every step degrades to today's
160
+ // answer rather than throwing (the runner is macOS/Linux; kitty and iTerm2 are untouched).
161
+ for (let depth = 0; depth < 2 && frontier.length > 0; depth++) {
162
+ const children = childPids(frontier);
163
+ if (children.length === 0)
164
+ break;
165
+ found.push(...commsFor(children));
166
+ frontier = children;
167
+ }
168
+ return found;
169
+ }
170
+ function childPids(parents) {
171
+ try {
172
+ const out = execFileSync("pgrep", ["-P", parents.join(",")], {
173
+ encoding: "utf8",
174
+ timeout: PROBE_TIMEOUT_MS,
175
+ stdio: ["ignore", "pipe", "ignore"],
176
+ });
177
+ return out.split("\n").map((s) => s.trim()).filter((s) => /^\d+$/.test(s)).slice(0, 16);
178
+ }
179
+ catch {
180
+ return []; // pgrep exits non-zero when nothing matches — that is an answer, not an error
181
+ }
182
+ }
183
+ function commsFor(pids) {
184
+ try {
185
+ const out = execFileSync("ps", ["-o", "comm=", "-p", pids.join(",")], {
186
+ encoding: "utf8",
187
+ timeout: PROBE_TIMEOUT_MS,
188
+ stdio: ["ignore", "pipe", "ignore"],
189
+ });
190
+ return out.split("\n").map((s) => s.trim()).filter(Boolean);
191
+ }
192
+ catch {
193
+ return [];
194
+ }
195
+ }
93
196
  /** The foreground process of a kitty window, from `kitty @ ls`. Null if it can't be determined. */
94
197
  function kittyForegroundCommand(id) {
95
198
  try {
package/dist/index.js CHANGED
@@ -1214,11 +1214,44 @@ function diffMembership(attached, eligible, thisNode, blocked = /* @__PURE__ */
1214
1214
  detach: [...have].filter((id) => !wantIds.has(id))
1215
1215
  };
1216
1216
  }
1217
+ function shouldRegisterOnOpen(reconnect, alreadyRegistered) {
1218
+ return reconnect || !alreadyRegistered;
1219
+ }
1217
1220
  function instanceLabel(inst) {
1218
1221
  const short = `${inst.id.slice(0, 8)}\u2026`;
1219
1222
  return inst.name ? `${inst.name} (${short})` : short;
1220
1223
  }
1221
1224
 
1225
+ // src/commands/runner/status-line.ts
1226
+ var STATUS_PREFIX = "PAGS-STATUS";
1227
+ function formatStatusLine(status) {
1228
+ const parts = [];
1229
+ if (status.registration) parts.push(`registration=${status.registration}`);
1230
+ if (status.heartbeat) parts.push(`heartbeat=${status.heartbeat}`);
1231
+ if (status.agents) parts.push(`agents=${status.agents}`);
1232
+ if (status.reason) parts.push(`reason=${status.reason.replace(/\s+/g, " ").trim().slice(0, 200)}`);
1233
+ return `${STATUS_PREFIX} ${parts.join(" ")}`;
1234
+ }
1235
+ function parseStatusLine(line) {
1236
+ const trimmed = line.trim();
1237
+ if (!trimmed.startsWith(`${STATUS_PREFIX} `)) return null;
1238
+ const body = trimmed.slice(STATUS_PREFIX.length + 1);
1239
+ const status = {};
1240
+ const reasonAt = body.indexOf("reason=");
1241
+ const head = reasonAt >= 0 ? body.slice(0, reasonAt) : body;
1242
+ if (reasonAt >= 0) {
1243
+ const reason = body.slice(reasonAt + "reason=".length).trim();
1244
+ if (reason) status.reason = reason;
1245
+ }
1246
+ for (const token of head.split(/\s+/).filter(Boolean)) {
1247
+ const [key, value] = token.split("=", 2);
1248
+ if (key === "registration" && (value === "ok" || value === "partial" || value === "fail")) status.registration = value;
1249
+ else if (key === "heartbeat" && (value === "ok" || value === "fail")) status.heartbeat = value;
1250
+ else if (key === "agents" && value) status.agents = value;
1251
+ }
1252
+ return status;
1253
+ }
1254
+
1222
1255
  // src/commands/runner/relay.ts
1223
1256
  async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force = false, watchInstances = false) {
1224
1257
  const apiBase = pagsApiBase(opts.apiBase).replace(/^http/, "ws");
@@ -1228,7 +1261,9 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1228
1261
  const machine = loadMachineIdentity(runnerNode);
1229
1262
  const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: instanceIds[0] });
1230
1263
  const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
1231
- const registerRuntime = async (id) => {
1264
+ const registered = /* @__PURE__ */ new Set();
1265
+ let lastRegisterError = "";
1266
+ const registerRuntime = async (id, forceClaim = force) => {
1232
1267
  try {
1233
1268
  await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
1234
1269
  endpointUrl: localUrl,
@@ -1239,13 +1274,23 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1239
1274
  runnerNode,
1240
1275
  machineId: machine.id,
1241
1276
  machineNames: machine.names,
1242
- force
1277
+ force: forceClaim
1243
1278
  });
1279
+ registered.add(id);
1280
+ return true;
1244
1281
  } catch (e) {
1245
1282
  const msg = e instanceof Error ? e.message : String(e);
1283
+ registered.delete(id);
1284
+ lastRegisterError = msg;
1246
1285
  writeError(`register ${id.slice(0, 8)}\u2026 failed: ${msg}`);
1286
+ return false;
1247
1287
  }
1248
1288
  };
1289
+ const reportRegistration = () => {
1290
+ const agents = `${registered.size}/${instanceIds.length}`;
1291
+ const state = registered.size === instanceIds.length ? "ok" : registered.size === 0 ? "fail" : "partial";
1292
+ writeLine(formatStatusLine({ registration: state, agents, reason: state === "ok" ? void 0 : lastRegisterError }));
1293
+ };
1249
1294
  for (const id of instanceIds) await registerRuntime(id);
1250
1295
  const attached = /* @__PURE__ */ new Map();
1251
1296
  const blocked = /* @__PURE__ */ new Set();
@@ -1254,10 +1299,31 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1254
1299
  const mintToken = () => requestPags("POST", `/v1/relay/${apiPathSegment(id)}/token`, { ...opts, pagsToken }, {}).then((r) => r.token);
1255
1300
  attached.set(
1256
1301
  id,
1257
- openRelaySocket(id, apiBase, mintToken, localUrl, runnerToken, force, (conflicted) => {
1258
- blocked.add(conflicted);
1259
- attached.delete(conflicted);
1260
- })
1302
+ openRelaySocket(
1303
+ id,
1304
+ apiBase,
1305
+ mintToken,
1306
+ localUrl,
1307
+ runnerToken,
1308
+ force,
1309
+ (conflicted) => {
1310
+ blocked.add(conflicted);
1311
+ attached.delete(conflicted);
1312
+ },
1313
+ // Registration rides the RECONNECT, which is the whole wake case (#497). The socket
1314
+ // retries with backoff; `POST …/runtime` did not, so after a sleep the machine had a
1315
+ // live relay and no runtime row — and `resumeSessionsForNode`, which lives inside that
1316
+ // route, never ran either, so its own suspended coding sessions stayed suspended. The
1317
+ // upsert is idempotent, so re-registering on every reconnect is safe. The first open
1318
+ // is skipped when the register already succeeded above (or in the discovery pass),
1319
+ // and taken when it did not — which is how a register lost to a boot-time
1320
+ // `fetch failed` finally gets a second chance.
1321
+ async (openedId, reconnect) => {
1322
+ if (!shouldRegisterOnOpen(reconnect, registered.has(openedId))) return;
1323
+ await registerRuntime(openedId, reconnect ? false : force);
1324
+ reportRegistration();
1325
+ }
1326
+ )
1261
1327
  );
1262
1328
  if (label) writeLine(`Attached agent: ${label}`);
1263
1329
  };
@@ -1269,7 +1335,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1269
1335
  writeLine(`Detached agent: ${label}`);
1270
1336
  };
1271
1337
  for (const id of instanceIds) attach(id, "");
1272
- writeLine("Runtime registered with PAGS \u2713");
1338
+ reportRegistration();
1339
+ writeLine(registered.size === instanceIds.length ? `Runtime registered with PAGS \u2713 (${registered.size}/${instanceIds.length} agents)` : `Runtime registration incomplete: ${registered.size}/${instanceIds.length} agents \u2014 retried on each relay (re)connect.`);
1273
1340
  writeLine("");
1274
1341
  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");
1275
1342
  writeLine(` \u2705 CONNECTED \u2014 WebSocket relay \xB7 ${hostname3()}`);
@@ -1289,9 +1356,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1289
1356
  }
1290
1357
  if (failure && !heartbeatFailing) {
1291
1358
  heartbeatFailing = true;
1359
+ writeLine(formatStatusLine({ heartbeat: "fail", reason: failure }));
1292
1360
  writeError(`Heartbeat failed: ${failure} \u2014 the console will show this machine as OFFLINE until it recovers. The relay itself is still connected; don't run \`pags up --force\` elsewhere.`);
1293
1361
  } else if (!failure && heartbeatFailing) {
1294
1362
  heartbeatFailing = false;
1363
+ writeLine(formatStatusLine({ heartbeat: "ok" }));
1295
1364
  writeLine("Heartbeat recovered \u2014 this machine reads as online again.");
1296
1365
  }
1297
1366
  heartbeat();
@@ -1300,10 +1369,23 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1300
1369
  };
1301
1370
  heartbeat();
1302
1371
  if (watchInstances) startDiscovery();
1372
+ async function clearFinishedConflicts() {
1373
+ for (const id of [...blocked]) {
1374
+ const free = await requestPags(
1375
+ "GET",
1376
+ `/v1/relay/${apiPathSegment(id)}/status`,
1377
+ { ...opts, pagsToken }
1378
+ ).then((r) => r.connected === false).catch(() => false);
1379
+ if (!free) continue;
1380
+ blocked.delete(id);
1381
+ writeLine(`Relay conflict cleared: ${id.slice(0, 8)}\u2026 \u2014 the other runner is gone; reattaching.`);
1382
+ }
1383
+ }
1303
1384
  function startDiscovery() {
1304
1385
  const tick = () => {
1305
1386
  const timer = setTimeout(async () => {
1306
1387
  try {
1388
+ await clearFinishedConflicts();
1307
1389
  const res = await requestPags(
1308
1390
  "GET",
1309
1391
  "/v1/instances/my/instances",
@@ -1333,10 +1415,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
1333
1415
  tick();
1334
1416
  }
1335
1417
  }
1336
- function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict) {
1418
+ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen) {
1337
1419
  let backoffMs = 1e3;
1338
1420
  let reconnecting = false;
1339
1421
  let closed = false;
1422
+ let opened = false;
1340
1423
  let socket = null;
1341
1424
  let retryTimer = null;
1342
1425
  const connect = async () => {
@@ -1365,7 +1448,10 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
1365
1448
  socket = ws;
1366
1449
  ws.onopen = () => {
1367
1450
  backoffMs = 1e3;
1451
+ const reconnect = opened;
1452
+ opened = true;
1368
1453
  writeLine(`Relay connected: ${instanceId.slice(0, 8)}\u2026`);
1454
+ void Promise.resolve(onOpen?.(instanceId, reconnect)).catch(() => void 0);
1369
1455
  };
1370
1456
  ws.onmessage = async (event) => {
1371
1457
  const text = typeof event.data === "string" ? event.data : String(event.data);
@@ -1652,31 +1738,42 @@ function printLogo(version2) {
1652
1738
  console.log(pad + d(" Browser runner") + (version2 ? d(` \xB7 v${version2}`) : ""));
1653
1739
  console.log("");
1654
1740
  }
1741
+ var STATUS_ROWS = {
1742
+ runner: { label: "Browser", ok: "running on your computer", busy: "starting up\u2026", bad: "stopped \u2014 press r to retry" },
1743
+ tunnel: { label: "Secure link", ok: "connected to ProAgentStore", busy: "opening\u2026", bad: "offline" },
1744
+ pags: { label: "ProAgentStore", ok: "connected \u2014 ready for jobs", busy: "registering\u2026", bad: "not registered (retries automatically)" }
1745
+ };
1746
+ var LABEL_WIDTH = Math.max(...Object.values(STATUS_ROWS).map((r) => r.label.length)) + 2;
1655
1747
  function describe(kind, s) {
1656
1748
  const ok = s === "online" || s === "registered";
1657
1749
  const busy = s === "starting" || s === "pending";
1658
1750
  const icon = ok ? chalk.green("\u2713") : busy ? chalk.yellow("\u2026") : chalk.red("\u2717");
1659
- const map = {
1660
- runner: { label: "Browser", ok: "running on your computer", busy: "starting up\u2026", bad: "stopped \u2014 press r to retry" },
1661
- tunnel: { label: "Secure link", ok: "connected to ProAgentStore", busy: "opening\u2026", bad: "offline" },
1662
- pags: { label: "ProAgentStore", ok: "connected \u2014 ready for jobs", busy: "registering\u2026", bad: "not registered (retries automatically)" }
1663
- };
1664
- const m = map[kind];
1751
+ const m = STATUS_ROWS[kind];
1665
1752
  return { icon, label: m.label, note: ok ? m.ok : busy ? m.busy : m.bad };
1666
1753
  }
1754
+ function connectingNote(elapsedMs) {
1755
+ if (elapsedMs < 3e4) return "Setting things up\u2026 this takes a few seconds. Keep this window open.";
1756
+ const mins = Math.floor(elapsedMs / 6e4);
1757
+ const elapsed = mins >= 1 ? `${mins}m` : `${Math.floor(elapsedMs / 1e3)}s`;
1758
+ return `Still connecting \u2014 ${elapsed} elapsed. Press l for logs.`;
1759
+ }
1760
+ function pagsNote(registration, heartbeat) {
1761
+ if (registration !== "registered" || heartbeat !== "failing") return void 0;
1762
+ return "registered \u2014 but the heartbeat is failing, so the website reads this machine as offline";
1763
+ }
1667
1764
  function printStatus(state) {
1668
1765
  clearScreen();
1669
1766
  printLogo(state.version);
1670
1767
  const connected = state.runner === "online" && state.tunnel === "online" && state.registration === "registered";
1671
1768
  console.log(pad + d("Signed in as ") + w(state.user) + d(" \xB7 agent: ") + w(state.activeInstance) + d(" \xB7 node: ") + w(hostname4()));
1672
1769
  console.log("");
1673
- const row = (kind, s) => {
1770
+ const row = (kind, s, override) => {
1674
1771
  const { icon, label, note } = describe(kind, s);
1675
- console.log(pad + icon + " " + w(label.padEnd(13)) + d(note));
1772
+ console.log(pad + icon + " " + w(label.padEnd(LABEL_WIDTH)) + d(override ?? note));
1676
1773
  };
1677
1774
  row("runner", state.runner);
1678
1775
  row("tunnel", state.tunnel);
1679
- row("pags", state.registration);
1776
+ row("pags", state.registration, pagsNote(state.registration, state.heartbeat));
1680
1777
  console.log("");
1681
1778
  if (connected) {
1682
1779
  console.log(pad + chalk.green("\u2713 You're all set!") + d(" Your agent can now act on the web."));
@@ -1688,7 +1785,7 @@ function printStatus(state) {
1688
1785
  console.log("");
1689
1786
  console.log(pad + d("The website can take a few seconds to show \u201Conline\u201D \u2014 that's normal."));
1690
1787
  } else {
1691
- console.log(pad + d("Setting things up\u2026 this takes a few seconds. Keep this window open."));
1788
+ console.log(pad + d(connectingNote(state.startedAt ? Date.now() - state.startedAt : 0)));
1692
1789
  if (state.lastEvent) console.log(pad + d("Status: ") + d(state.lastEvent));
1693
1790
  }
1694
1791
  console.log("");
@@ -1761,9 +1858,13 @@ var upCommand = new Command8("up").description("Start the browser runner for all
1761
1858
  tunnel: "offline",
1762
1859
  tunnelUrl: "",
1763
1860
  registration: "pending",
1861
+ heartbeat: "ok",
1764
1862
  lastEvent: "Fetching instances...",
1765
1863
  taskCount: 0,
1766
- version: CLI_VERSION2
1864
+ version: CLI_VERSION2,
1865
+ // What "a few seconds" is measured against: a state that never resolves was described
1866
+ // as taking a few seconds, indefinitely, because nothing counted (#497).
1867
+ startedAt: Date.now()
1767
1868
  };
1768
1869
  clearScreen();
1769
1870
  printLogo(CLI_VERSION2);
@@ -1826,6 +1927,19 @@ var upCommand = new Command8("up").description("Start the browser runner for all
1826
1927
  if (!trimmed) continue;
1827
1928
  logs.push(trimmed);
1828
1929
  if (logs.length > 200) logs.shift();
1930
+ const status = parseStatusLine(trimmed);
1931
+ if (status) {
1932
+ if (status.registration) {
1933
+ state.registration = status.registration === "ok" ? "registered" : "failed";
1934
+ state.lastEvent = status.registration === "ok" ? `Registered with PAGS \u2014 ${status.agents ?? "all"} agents ready` : `PAGS registration ${status.registration}${status.agents ? ` (${status.agents} agents)` : ""}${status.reason ? `: ${status.reason}` : ""}`;
1935
+ }
1936
+ if (status.heartbeat) {
1937
+ state.heartbeat = status.heartbeat === "ok" ? "ok" : "failing";
1938
+ state.lastEvent = status.heartbeat === "ok" ? "Heartbeat recovered \u2014 this machine reads as online again" : `Heartbeat failing${status.reason ? `: ${status.reason}` : ""} \u2014 the console will show this machine offline`;
1939
+ }
1940
+ printStatus(state);
1941
+ continue;
1942
+ }
1829
1943
  if (trimmed.includes("Relay connected:")) {
1830
1944
  state.tunnel = "online";
1831
1945
  state.tunnelUrl = "WebSocket relay";
@@ -1836,7 +1950,6 @@ var upCommand = new Command8("up").description("Start the browser runner for all
1836
1950
  if (trimmed.includes("WebSocket relay")) {
1837
1951
  state.tunnel = "online";
1838
1952
  state.tunnelUrl = "WebSocket relay";
1839
- state.registration = "registered";
1840
1953
  state.lastEvent = "Connected via WebSocket relay";
1841
1954
  printStatus(state);
1842
1955
  continue;
@@ -1847,21 +1960,13 @@ var upCommand = new Command8("up").description("Start the browser runner for all
1847
1960
  printStatus(state);
1848
1961
  continue;
1849
1962
  }
1850
- if (trimmed.includes("Runtime registered") || trimmed.includes("CONNECTED")) {
1851
- state.registration = "registered";
1852
- state.lastEvent = "Registered with PAGS \u2014 ready for tasks";
1853
- printStatus(state);
1854
- continue;
1855
- }
1856
- if (trimmed.includes("Another machine")) {
1857
- state.registration = "failed";
1858
- state.lastEvent = trimmed.slice(0, 80);
1963
+ if (trimmed.includes("Relay conflict:")) {
1964
+ state.lastEvent = "Another runner holds this agent \u2014 run `pags up --force` here to take it over";
1859
1965
  printStatus(state);
1860
1966
  continue;
1861
1967
  }
1862
- if (trimmed.includes("fetch failed")) {
1863
- state.registration = "failed";
1864
- state.lastEvent = "PAGS registration failed";
1968
+ if (trimmed.includes("Relay conflict cleared:")) {
1969
+ state.lastEvent = "Relay conflict cleared \u2014 reattaching";
1865
1970
  printStatus(state);
1866
1971
  continue;
1867
1972
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.46",
3
+ "version": "0.4.47",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",