@proagentstore/cli 0.4.28 → 0.4.29

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.
@@ -454,21 +454,28 @@ export function parseCommand(command) {
454
454
  i++;
455
455
  if (i >= src.length)
456
456
  break;
457
- const start = i;
457
+ const tokenStart = i;
458
458
  let out = "";
459
- let quote = null;
460
459
  while (i < src.length) {
461
460
  const ch = src[i];
462
- if (quote) {
463
- if (ch === quote)
464
- quote = null;
465
- else
466
- out += ch;
467
- i++;
468
- continue;
469
- }
470
- if (ch === '"' || ch === "'") {
471
- quote = ch;
461
+ // A DOUBLE quote always opens a span — that is what `--flag "two words"` means and
462
+ // what anyone typing this expects.
463
+ //
464
+ // A SINGLE quote opens one only at a token boundary (token start, or right after `=`),
465
+ // because in ordinary English it is an apostrophe. This field is a preset text box, not
466
+ // a shell: a real shell would pair the two apostrophes in `don't guess and don't stop`
467
+ // and hand the engine `dont guess and dont` plus a stray `stop`, which is exactly the
468
+ // mangling seen here. `--agent='my agent'` and `'my agent'` still work.
469
+ const opens = ch === '"' || (ch === "'" && (i === tokenStart || src[i - 1] === "="));
470
+ if (opens) {
471
+ const close = src.indexOf(ch, i + 1);
472
+ if (close !== -1) {
473
+ out += src.slice(i + 1, close);
474
+ i = close + 1;
475
+ continue;
476
+ }
477
+ // Unterminated — the character is literal, not the start of a span.
478
+ out += ch;
472
479
  i++;
473
480
  continue;
474
481
  }
@@ -477,21 +484,7 @@ export function parseCommand(command) {
477
484
  out += ch;
478
485
  i++;
479
486
  }
480
- if (quote) {
481
- // UNTERMINATED quote → it was a literal character, not a quote. `don't` is ordinary
482
- // English in a user-edited preset; treating the apostrophe as an opening quote made
483
- // `--append-system-prompt don't guess` reach the engine as three broken arguments.
484
- i = start;
485
- let raw = "";
486
- while (i < src.length && !isSpace(src[i])) {
487
- raw += src[i];
488
- i++;
489
- }
490
- tokens.push(raw);
491
- }
492
- else {
493
- tokens.push(out);
494
- }
487
+ tokens.push(out);
495
488
  }
496
489
  return { bin: tokens[0] ?? "", args: tokens.slice(1) };
497
490
  }
@@ -9,6 +9,7 @@ import { McpRuntime } from "./mcp-runtime.js";
9
9
  import { HumanHandoffError, RunnerInputError } from "./errors.js";
10
10
  import { RunnerStore } from "./store.js";
11
11
  import { CodingRuntime } from "./coding/runtime.js";
12
+ import { WORKFLOW_DRIVEN_TASKS } from "./task-types.js";
12
13
  /** True for a plain object. */
13
14
  function isRecord(value) {
14
15
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -39,7 +40,6 @@ const APPROVAL_REQUIRED_TASKS = new Set(["browser.open"]);
39
40
  * workflow then polled a dead session for 15 minutes before failing a run it had already
40
41
  * completed.
41
42
  */
42
- const WORKFLOW_DRIVEN_TASKS = new Set(["job.apply_agent", "browser.task"]);
43
43
  const require = createRequire(import.meta.url);
44
44
  export class LocalRunner {
45
45
  config;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { WORKFLOW_DRIVEN_TASKS } from "./task-types.js";
3
4
  function emptyStore() {
4
5
  return {
5
6
  sessions: [],
@@ -50,7 +51,7 @@ export class RunnerStore {
50
51
  // to "failed", which re-mirrors to the board, resurrects the Retry button, and
51
52
  // slips past the API single-flight guard → a second concurrent apply on the one
52
53
  // browser page. Mirrors the API carve-out in expireOrphanedRuntimeTasks.
53
- if (task.type === "job.apply_agent")
54
+ if (WORKFLOW_DRIVEN_TASKS.has(task.type))
54
55
  continue;
55
56
  if (task.status === "needs_human" || task.status === "running") {
56
57
  task.status = "failed";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Task types steered by a remote durable Workflow rather than executed by the runner.
3
+ *
4
+ * ONE list, in its own module because THREE places need it and they drifted: task creation had
5
+ * `job.apply_agent || browser.task`, `resumeTakeover` had only `job.apply_agent` (so resolving a
6
+ * stuck browse handoff destroyed the session), and `RunnerStore.expireInFlightTasks` still
7
+ * hardcodes its own copy — which meant restarting `pags up` failed a live browse run locally while
8
+ * the cloud side preserved it, leaving the two views of one durable run disagreeing.
9
+ *
10
+ * `browser.handoff` is the synthetic task `browserHandoff` creates for a caller that has none (the
11
+ * engine sign-in relay mints its own id for a coding session with no runner task). It belongs here
12
+ * for the same reason: the console's Resume button is agent-agnostic, so without it pressing
13
+ * Resume would complete and END the takeover mid-sign-in — reintroducing the exact bug the browse
14
+ * case was fixed for.
15
+ */
16
+ export const WORKFLOW_DRIVEN_TASKS = new Set([
17
+ "job.apply_agent",
18
+ "browser.task",
19
+ "browser.handoff",
20
+ ]);
package/dist/index.js CHANGED
@@ -1,10 +1,4 @@
1
1
  #!/usr/bin/env node
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
6
- throw Error('Dynamic require of "' + x + '" is not supported');
7
- });
8
2
 
9
3
  // src/index.ts
10
4
  import { createRequire as createRequire3 } from "module";
@@ -371,7 +365,7 @@ jobs:
371
365
  });
372
366
 
373
367
  // src/commands/login.ts
374
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
368
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
375
369
  import { createServer } from "http";
376
370
  import { homedir } from "os";
377
371
  import { join as join3 } from "path";
@@ -472,14 +466,16 @@ var loginCommand = new Command3("login").description("Sign in with Google or Git
472
466
  }
473
467
  });
474
468
  var logoutCommand = new Command3("logout").description("Sign out and remove stored session").action(() => {
469
+ if (!existsSync3(TOKEN_FILE)) {
470
+ writeLine("Already signed out.");
471
+ return;
472
+ }
475
473
  try {
476
- if (existsSync3(TOKEN_FILE)) {
477
- const { unlinkSync } = __require("fs");
478
- unlinkSync(TOKEN_FILE);
479
- }
474
+ unlinkSync(TOKEN_FILE);
480
475
  writeLine("Signed out.");
481
- } catch {
482
- writeLine("Already signed out.");
476
+ } catch (err) {
477
+ writeError(`Could not remove ${TOKEN_FILE}: ${err instanceof Error ? err.message : String(err)}`);
478
+ process.exit(1);
483
479
  }
484
480
  });
485
481
  var whoamiCommand = new Command3("whoami").description("Show current signed-in user").action(() => {
@@ -921,7 +917,9 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
921
917
  ws.onclose = (ev) => {
922
918
  if (reconnecting) return;
923
919
  reconnecting = true;
924
- const reason = ev.code === 4401 || ev.code === 1008 ? " (token expired \u2014 run `pags login` then `pags up`)" : "";
920
+ const said = (ev.reason || "").trim();
921
+ const hint = ev.code === 4401 ? " \u2014 run `pags login`, then `pags up`" : ev.code === 4409 ? " \u2014 run `pags up --force` to take over" : "";
922
+ const reason = said ? ` (${said}${hint})` : ev.code === 1008 ? " (token expired \u2014 run `pags login` then `pags up`)" : "";
925
923
  writeLine(`Relay disconnected: ${instanceId.slice(0, 8)}\u2026${reason} \u2014 reconnecting in ${Math.round(backoffMs / 1e3)}s`);
926
924
  setTimeout(() => {
927
925
  reconnecting = false;
@@ -943,7 +941,7 @@ function createRunnerCommand() {
943
941
  const command = new Command6("runner").description(
944
942
  "Manage the local ProAgentStore browser runtime for ProAgentStore agents"
945
943
  );
946
- 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", "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) => {
944
+ 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) => {
947
945
  await startRunnerForeground(opts);
948
946
  });
949
947
  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").action(async (instanceIds, opts) => {
@@ -1184,7 +1182,7 @@ function printStep(label, status) {
1184
1182
  const icon = status === "ok" ? chalk.green("\u2713") : status === "fail" ? chalk.red("\u2717") : chalk.yellow("\u2026");
1185
1183
  console.log(pad + icon + " " + label);
1186
1184
  }
1187
- async function waitForKey(keys) {
1185
+ async function waitForKey(keys, onInterrupt) {
1188
1186
  return new Promise((resolve5) => {
1189
1187
  readline.emitKeypressEvents(process.stdin);
1190
1188
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
@@ -1192,6 +1190,10 @@ async function waitForKey(keys) {
1192
1190
  const onKeypress = (str, key) => {
1193
1191
  if (key?.ctrl && key.name === "c") {
1194
1192
  cleanup();
1193
+ if (onInterrupt) {
1194
+ onInterrupt();
1195
+ return;
1196
+ }
1195
1197
  process.exit(0);
1196
1198
  }
1197
1199
  const val = (str || "").trim().toLowerCase();
@@ -1366,7 +1368,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
1366
1368
  process.on("SIGINT", shutdown);
1367
1369
  process.on("SIGTERM", shutdown);
1368
1370
  while (true) {
1369
- const key = await waitForKey(["r", "l", "q"]);
1371
+ const key = await waitForKey(["r", "l", "q"], shutdown);
1370
1372
  if (key === "q") {
1371
1373
  shutdown();
1372
1374
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.28",
3
+ "version": "0.4.29",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",