@oya-ai/cli 1.0.110 → 1.0.111

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.
Files changed (3) hide show
  1. package/README.md +24 -15
  2. package/dist/index.js +892 -461
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,26 +1,127 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/args.ts
4
- function parse(argv) {
5
- const [command2 = "help", ...rest] = argv;
6
- const args2 = [];
7
- const flags2 = {};
4
+ import { readFileSync } from "fs";
5
+
6
+ // src/errors.ts
7
+ var CliError = class extends Error {
8
+ /** usage, invalid_json, unreachable, no_api_key, no_browser, partial_failure, and the like. */
9
+ code;
10
+ /** A second line with the next step, when there is one. */
11
+ hint;
12
+ /** Already printed by the command (a partial failure's own lines), so not printed again. */
13
+ shown;
14
+ /** Records the message, code, hint and whether it was shown. */
15
+ constructor(message, code, { hint: hint2, shown: shown2 = false } = {}) {
16
+ super(message);
17
+ this.name = "CliError";
18
+ this.code = code;
19
+ this.hint = hint2;
20
+ this.shown = shown2;
21
+ }
22
+ };
23
+ var usage = (message, hint2) => new CliError(message, "usage", { hint: hint2 });
24
+ var invalidJson = (message, hint2) => new CliError(message, "invalid_json", { hint: hint2 });
25
+
26
+ // src/args.ts
27
+ var SWITCHES = /* @__PURE__ */ new Set([
28
+ "all",
29
+ "debug",
30
+ "dry-run",
31
+ "force",
32
+ "governed",
33
+ "help",
34
+ "json",
35
+ "live",
36
+ "preview",
37
+ "replace",
38
+ "version"
39
+ ]);
40
+ var VALUE_FLAGS = {
41
+ after: "--after 120",
42
+ "budget-usd": "--budget-usd 5",
43
+ config: "--config oya-install.json",
44
+ format: "--format playwright",
45
+ geo: "--geo us",
46
+ id: "--id oya-k3f9",
47
+ "idempotency-key": "--idempotency-key checkout-42",
48
+ key: "--key oya_...",
49
+ label: "--label ci",
50
+ locale: "--locale en-US",
51
+ max: "--max 3",
52
+ name: "--name shopper",
53
+ out: "--out cookies.json",
54
+ persona: "--persona auto",
55
+ platform: "--platform MacIntel",
56
+ policy: `--policy '{"allowedHosts":["example.com"]}'`,
57
+ priority: "--priority normal",
58
+ provider: "--provider cdp",
59
+ "queue-ms": "--queue-ms 30000",
60
+ role: "--role operator",
61
+ tz: "--tz Europe/Berlin",
62
+ url: "--url http://127.0.0.1:3100",
63
+ "ws-url": "--ws-url ws://127.0.0.1:9222"
64
+ };
65
+ function parse(argv2) {
66
+ const [command = "help", ...rest] = argv2;
67
+ const args = [];
68
+ const flags = {};
8
69
  for (let i = 0; i < rest.length; i++) {
9
- if (!rest[i].startsWith("--")) args2.push(rest[i]);
10
- else if (takeFlag(rest[i].slice("--".length), rest[i + 1], flags2)) i++;
70
+ if (!rest[i].startsWith("--")) args.push(rest[i]);
71
+ else if (takeFlag(rest[i].slice("--".length), rest[i + 1], flags)) i++;
72
+ }
73
+ return { command, args, flags };
74
+ }
75
+ function takeFlag(token2, next, flags) {
76
+ const [name, inline] = splitOnce(token2);
77
+ if (SWITCHES.has(name) && inline !== void 0) throw usage(`--${name} takes no value.`);
78
+ flags[name] = SWITCHES.has(name) || valueOf(name, inline, next);
79
+ return !SWITCHES.has(name) && inline === void 0;
80
+ }
81
+ function valueOf(name, inline, next) {
82
+ if (!Object.hasOwn(VALUE_FLAGS, name)) throw usage(`Unknown flag --${name}. Run oya help for the flags.`);
83
+ if (inline !== void 0) return inline;
84
+ if (next === void 0 || next.startsWith("--"))
85
+ throw usage(`--${name} needs a value, such as ${VALUE_FLAGS[name]}.`);
86
+ return next;
87
+ }
88
+ function splitOnce(token2) {
89
+ const at = token2.indexOf("=");
90
+ return at < 0 ? [token2, void 0] : [token2.slice(0, at), token2.slice(at + 1)];
91
+ }
92
+ var flagStr = (flags, name) => typeof flags[name] === "string" ? flags[name] : void 0;
93
+ function flagNum(flags, name) {
94
+ const raw = flagStr(flags, name);
95
+ if (raw === void 0) return void 0;
96
+ if (raw.trim() === "" || !Number.isFinite(Number(raw))) throw usage(`--${name} must be a number, not "${raw}".`);
97
+ return Number(raw);
98
+ }
99
+ function flagCount(flags, name, { allowNone = false } = {}) {
100
+ const raw = flagStr(flags, name);
101
+ if (raw === void 0) return void 0;
102
+ if (allowNone && raw === "none") return null;
103
+ if (/^[1-9]\d*$/.test(raw)) return Number(raw);
104
+ throw usage(`--${name} must be a whole number of 1 or more${allowNone ? ", or none" : ""}, not "${raw}".`);
105
+ }
106
+ function parseJson(text, subject, hint2, verb = "is") {
107
+ try {
108
+ return JSON.parse(text);
109
+ } catch (err) {
110
+ throw invalidJson(`${subject} ${verb} not valid JSON: ${err.message}.`, hint2);
11
111
  }
12
- return { command: command2, args: args2, flags: flags2 };
13
112
  }
14
- function takeFlag(name, next, flags2) {
15
- const bare = next === void 0 || next.startsWith("--");
16
- flags2[name] = bare ? true : next;
17
- return !bare;
113
+ function readJsonFile(file) {
114
+ let text;
115
+ try {
116
+ text = readFileSync(file, "utf8");
117
+ } catch (err) {
118
+ throw usage(`${file} could not be read: ${err.code ?? err.message}.`);
119
+ }
120
+ return parseJson(text, file);
18
121
  }
19
- var flagStr = (flags2, name) => typeof flags2[name] === "string" ? flags2[name] : void 0;
20
- var flagNum = (flags2, name) => flagStr(flags2, name) === void 0 ? void 0 : Number(flagStr(flags2, name));
21
122
 
22
123
  // src/install.ts
23
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
124
+ import { writeFileSync as writeFileSync3 } from "fs";
24
125
  import { join as join5 } from "path";
25
126
 
26
127
  // src/prompt/style.ts
@@ -78,9 +179,9 @@ function steps(total) {
78
179
  }
79
180
  function step(title, hint2) {
80
181
  counter.number += 1;
81
- const shown = counter.total ? style.grey(`${counter.number}/${counter.total}`) : "";
182
+ const shown2 = counter.total ? style.grey(`${counter.number}/${counter.total}`) : "";
82
183
  stdout2.write(`
83
- ${style.cyan("\u25C6")} ${style.bold(title)} ${shown}
184
+ ${style.cyan("\u25C6")} ${style.bold(title)} ${shown2}
84
185
  `);
85
186
  if (hint2) stdout2.write(`${style.grey(" " + hint2)}
86
187
  `);
@@ -132,6 +233,11 @@ var masked = false;
132
233
  var pending = [];
133
234
  var waiter = null;
134
235
  var ended = false;
236
+ var linesRead = 0;
237
+ function readLine(line) {
238
+ linesRead++;
239
+ deliver(line);
240
+ }
135
241
  function deliver(line) {
136
242
  if (waiter) {
137
243
  const w = waiter;
@@ -153,7 +259,7 @@ function maskEcho(line) {
153
259
  function iface() {
154
260
  if (rl) return rl;
155
261
  rl = createInterface({ input: stdin, output: stdout3, terminal: !!stdin.isTTY });
156
- rl.on("line", deliver);
262
+ rl.on("line", readLine);
157
263
  rl.on("close", close);
158
264
  maskEcho(rl);
159
265
  return rl;
@@ -170,6 +276,9 @@ function nextLine() {
170
276
  function setMasked(on) {
171
277
  masked = on;
172
278
  }
279
+ function answered() {
280
+ return !!stdin.isTTY || linesRead > 0;
281
+ }
173
282
  function inputExhausted() {
174
283
  return ended && !pending.length;
175
284
  }
@@ -401,27 +510,27 @@ var Status = {
401
510
  };
402
511
 
403
512
  // src/install/shell.ts
404
- function settle(cmd, args2, resolve, reject) {
405
- return (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args2.join(" ")} exited ${code}`));
513
+ function settle(cmd, args, resolve, reject) {
514
+ return (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`));
406
515
  }
407
- function run(cmd, args2, opts = { cwd: process.cwd() }) {
516
+ function run(cmd, args, opts = { cwd: process.cwd() }) {
408
517
  return new Promise((resolve, reject) => {
409
- const child = spawn(cmd, args2, { cwd: opts.cwd, stdio: opts.quiet ? "ignore" : "inherit" });
518
+ const child = spawn(cmd, args, { cwd: opts.cwd, stdio: opts.quiet ? "ignore" : "inherit" });
410
519
  child.on("error", reject);
411
- child.on("close", settle(cmd, args2, resolve, reject));
520
+ child.on("close", settle(cmd, args, resolve, reject));
412
521
  });
413
522
  }
414
- function pipeTo(cmd, args2, stdin4, cwd) {
523
+ function pipeTo(cmd, args, stdin4, cwd) {
415
524
  return new Promise((resolve, reject) => {
416
- const child = spawn(cmd, args2, { cwd, stdio: ["pipe", "inherit", "inherit"] });
525
+ const child = spawn(cmd, args, { cwd, stdio: ["pipe", "inherit", "inherit"] });
417
526
  child.on("error", reject);
418
- child.on("close", settle(cmd, args2, resolve, reject));
527
+ child.on("close", settle(cmd, args, resolve, reject));
419
528
  child.stdin.end(stdin4);
420
529
  });
421
530
  }
422
- async function capture(cmd, args2, cwd = process.cwd()) {
531
+ async function capture(cmd, args, cwd = process.cwd()) {
423
532
  return new Promise((resolve) => {
424
- const child = spawn(cmd, args2, { cwd, stdio: ["ignore", "pipe", "ignore"] });
533
+ const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "ignore"] });
425
534
  const out2 = [];
426
535
  child.stdout.on("data", (d) => out2.push(String(d)));
427
536
  child.on("error", () => resolve(null));
@@ -429,8 +538,8 @@ async function capture(cmd, args2, cwd = process.cwd()) {
429
538
  });
430
539
  }
431
540
  var has = async (cmd) => await capture(cmd, ["--version"]) !== null;
432
- function atLeast(version, major, minor) {
433
- const m = /(\d+)\.(\d+)/.exec(version || "");
541
+ function atLeast(version2, major, minor) {
542
+ const m = /(\d+)\.(\d+)/.exec(version2 || "");
434
543
  if (!m) return false;
435
544
  return Number(m[1]) > major || Number(m[1]) === major && Number(m[2]) >= minor;
436
545
  }
@@ -459,14 +568,19 @@ function findRepoRoot(from = process.cwd()) {
459
568
  }
460
569
  return null;
461
570
  }
462
- async function locateRepo() {
571
+ var REPO_URL = "https://github.com/OyadotAI/oya-browser.git";
572
+ async function locateRepo(dryRun = false) {
463
573
  const found = findRepoRoot();
464
574
  if (found) return found;
465
575
  console.log("\nThis does not look like an oya-browser checkout.");
466
576
  const where = await ask("Clone it to:", join(process.cwd(), "oya-browser"));
577
+ if (dryRun) return wouldClone(where);
467
578
  if (existsSync(where)) throw new Error(`${where} already exists`);
468
- const repo = "https://github.com/OyadotAI/oya-browser.git";
469
- await run("git", ["clone", "--depth", "1", repo, where], { cwd: process.cwd() });
579
+ await run("git", ["clone", "--depth", "1", REPO_URL, where], { cwd: process.cwd() });
580
+ return where;
581
+ }
582
+ function wouldClone(where) {
583
+ console.log(`--dry-run: would clone ${REPO_URL} to ${where}`);
470
584
  return where;
471
585
  }
472
586
 
@@ -759,11 +873,11 @@ function composeCheck(compose) {
759
873
  return { ok, label: "docker compose \u2265 2.24", detail: compose || "not found", fatal: true };
760
874
  }
761
875
  async function dockerChecks() {
762
- const version = await capture("docker", ["--version"]);
876
+ const version2 = await capture("docker", ["--version"]);
763
877
  const daemon = await capture("docker", ["info", "--format", "{{.ID}}"]);
764
878
  const compose = await capture("docker", ["compose", "version", "--short"]);
765
879
  return [
766
- { ok: !!version, label: "docker", detail: version || "not found", fatal: true },
880
+ { ok: !!version2, label: "docker", detail: version2 || "not found", fatal: true },
767
881
  { ok: !!daemon, label: "docker daemon reachable", detail: daemon ? "ok" : "not running", fatal: true },
768
882
  composeCheck(compose)
769
883
  ];
@@ -773,10 +887,10 @@ function nodeCheck() {
773
887
  return { ok, label: "node \u2265 22.13 (node:sqlite)", detail: process.versions.node, fatal: true };
774
888
  }
775
889
  async function kubeChecks() {
776
- const version = await capture("kubectl", ["version", "--client=true", "-o", "json"]);
890
+ const version2 = await capture("kubectl", ["version", "--client=true", "-o", "json"]);
777
891
  const cluster = await capture("kubectl", ["get", "namespace", "kube-system", "-o", "name"]);
778
892
  return [
779
- { ok: !!version, label: "kubectl", detail: version ? "ok" : "not found", fatal: true },
893
+ { ok: !!version2, label: "kubectl", detail: version2 ? "ok" : "not found", fatal: true },
780
894
  { ok: !!cluster, label: "cluster reachable", detail: cluster || "kubectl cannot reach a cluster", fatal: true }
781
895
  ];
782
896
  }
@@ -811,7 +925,7 @@ async function preflight(root, a) {
811
925
  }
812
926
 
813
927
  // src/install/env-file.ts
814
- import { existsSync as existsSync3, readFileSync, writeFileSync, chmodSync } from "fs";
928
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, chmodSync } from "fs";
815
929
  var SECRET_KEYS = /* @__PURE__ */ new Set([
816
930
  "OYA_PROFILE_SECRET",
817
931
  "OYA_OPERATOR_TOKEN",
@@ -864,7 +978,7 @@ var SECTIONS = [
864
978
  function readEnv(path) {
865
979
  if (!existsSync3(path)) return {};
866
980
  const out2 = {};
867
- for (const line of readFileSync(path, "utf8").split("\n")) {
981
+ for (const line of readFileSync2(path, "utf8").split("\n")) {
868
982
  const m = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
869
983
  if (m) out2[m[1]] = m[2];
870
984
  }
@@ -1191,9 +1305,9 @@ function printDone(publicUrl, apiKey) {
1191
1305
 
1192
1306
  // src/install.ts
1193
1307
  var ANSWERS_FILE = "oya-install.json";
1194
- async function gather(configPath2) {
1195
- const root = await locateRepo();
1196
- const preset = configPath2 ? JSON.parse(readFileSync2(configPath2, "utf8")) : {};
1308
+ async function gather(configPath2, dryRun) {
1309
+ const root = await locateRepo(dryRun);
1310
+ const preset = configPath2 ? readJsonFile(configPath2) : {};
1197
1311
  banner("Oya Browser, self-host install", root);
1198
1312
  if (configPath2) note(`replaying ${configPath2}`);
1199
1313
  steps(configPath2 ? 0 : INTERVIEW_STEPS);
@@ -1219,8 +1333,8 @@ async function guardKek(existing, answers, root, dryRun) {
1219
1333
  const reused = await resolveKekConflict(volume, root);
1220
1334
  if (reused) existing.OYA_PROFILE_SECRET = reused;
1221
1335
  }
1222
- async function plan(flags2, dryRun) {
1223
- const interviewed = await gather(typeof flags2.config === "string" ? flags2.config : null);
1336
+ async function plan(flags, dryRun) {
1337
+ const interviewed = await gather(typeof flags.config === "string" ? flags.config : null, dryRun);
1224
1338
  const { root, answers, secrets } = interviewed;
1225
1339
  await checkMachine(root, answers, dryRun);
1226
1340
  warnLocalDatabase(answers, secrets.DATABASE_URL);
@@ -1252,9 +1366,9 @@ async function apply2(p) {
1252
1366
  await waitReady(p.answers.publicUrl);
1253
1367
  printDone(p.answers.publicUrl, p.env.apiKey);
1254
1368
  }
1255
- async function cmdInstall(flags2) {
1256
- const dryRun = flags2["dry-run"] === true;
1257
- const p = await plan(flags2, dryRun);
1369
+ async function cmdInstall(flags) {
1370
+ const dryRun = flags["dry-run"] === true;
1371
+ const p = await plan(flags, dryRun);
1258
1372
  printPlan(p.answers, p.envPath);
1259
1373
  if (dryRun) return printDryRun(p.env.values);
1260
1374
  await apply2(p);
@@ -1267,6 +1381,17 @@ import { join as join6, dirname as dirname2 } from "path";
1267
1381
 
1268
1382
  // src/constants.ts
1269
1383
  var ARGV_SKIP = 2;
1384
+ var KEY_TAIL = 4;
1385
+ var MIN_KEY_FOR_TAIL = 12;
1386
+ var LOGIN_TIMEOUT_MS = 6e4;
1387
+ var ExitCode = {
1388
+ /** Done. */
1389
+ DONE: 0,
1390
+ /** The request failed: the server or the browser refused it, or no answer came. */
1391
+ FAILED: 1,
1392
+ /** The command could not be run as typed; nothing was sent. */
1393
+ USAGE: 2
1394
+ };
1270
1395
  var JSON_INDENT2 = 2;
1271
1396
  var DEFAULT_BASE_URL = "https://oyabrowser.com";
1272
1397
  var CONFIG_FILE_MODE = 384;
@@ -1291,6 +1416,10 @@ var PERSONA_NAME_WIDTH = 20;
1291
1416
  var Status2 = {
1292
1417
  /** The API key was rejected. */
1293
1418
  UNAUTHORIZED: 401,
1419
+ /** A role refusal, or the auth layer's answer for an unknown key. */
1420
+ FORBIDDEN: 403,
1421
+ /** An endpoint another browser already holds. */
1422
+ CONFLICT: 409,
1294
1423
  /** A quota or a persona concurrency cap. */
1295
1424
  TOO_MANY_REQUESTS: 429
1296
1425
  };
@@ -1311,65 +1440,65 @@ function save(values) {
1311
1440
  chmodSync2(FILE, CONFIG_FILE_MODE);
1312
1441
  }
1313
1442
  var configPath = FILE;
1314
- function resolved() {
1315
- const saved = load();
1316
- return {
1317
- apiKey: process.env.OYA_API_KEY || saved.apiKey || "",
1318
- baseUrl: (process.env.OYA_BASE_URL || saved.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "")
1319
- };
1443
+ function first(flag, env2, saved, names) {
1444
+ if (flag) return { value: flag, from: names[0] };
1445
+ if (env2) return { value: env2, from: names[1] };
1446
+ return saved ? { value: saved, from: names[2] } : null;
1320
1447
  }
1321
-
1322
- // src/commands/login.ts
1323
- async function postJson(url, body, token2) {
1324
- const headers = { ...token2 ? { Authorization: `Bearer ${token2}` } : {}, "Content-Type": "application/json" };
1325
- const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
1326
- return { ok: res.ok, body: await res.json() };
1327
- }
1328
- async function mintKey(baseUrl) {
1329
- const email = await ask("Email:");
1330
- const password = await askSecret("Password:");
1331
- const session2 = await postJson(`${baseUrl}/api/auth/login`, { email, password });
1332
- if (!session2.ok || !session2.body.access_token) throw new Error(session2.body.error || "Sign-in failed");
1333
- const minted = await postJson(`${baseUrl}/api/auth/keys`, { label: "CLI" }, session2.body.access_token);
1334
- if (!minted.ok || !minted.body.key) throw new Error(minted.body.error || "Could not create an API key");
1335
- console.log(' Created a new API key labelled "CLI".');
1336
- return minted.body.key;
1337
- }
1338
- function chooseMethod() {
1339
- return choose("How do you want to authenticate?", [
1340
- { id: "paste", label: "Paste an API key", note: "from the dashboard, or a self-hosted key" },
1341
- { id: "password", label: "Sign in with email and password", note: "mints a new key for this machine" }
1342
- ]);
1343
- }
1344
- async function obtainKey(flags2, baseUrl) {
1345
- const given = flagStr(flags2, "key") || "";
1346
- if (given) return given;
1347
- const how = await chooseMethod();
1348
- if (how === "paste") return askSecret("API key:");
1349
- return mintKey(baseUrl);
1350
- }
1351
- async function verifyKey(baseUrl, apiKey) {
1352
- const check2 = await fetch(`${baseUrl}/api/config`, { headers: { Authorization: `Bearer ${apiKey}` } });
1353
- if (!check2.ok) throw new Error(`That key was rejected by ${baseUrl} (${check2.status})`);
1354
- }
1355
- async function loginUrl(flags2) {
1356
- const current = resolved().baseUrl;
1357
- const url = flagStr(flags2, "url") || (flagStr(flags2, "key") ? current : await ask("Control plane URL:", current));
1358
- return url.replace(/\/+$/, "");
1359
- }
1360
- async function cmdLogin(flags2) {
1361
- const baseUrl = await loginUrl(flags2);
1362
- const apiKey = await obtainKey(flags2, baseUrl);
1363
- if (!apiKey) throw new Error("No API key given");
1364
- await verifyKey(baseUrl, apiKey);
1365
- save({ apiKey, baseUrl });
1366
- console.log(`
1367
- \u2705 Signed in to ${baseUrl}. Saved to ${configPath}`);
1368
- console.log(" Scripts using @oya-ai/browser read this file too, so `new Oya()` needs no environment.");
1369
- console.log(" Next: `oya init` to pick your model and browser provider.");
1448
+ function origin({ apiKey, baseUrl } = {}) {
1449
+ const saved = load();
1450
+ const key = first(apiKey, process.env.OYA_API_KEY, saved.apiKey, KEY_SOURCES);
1451
+ const url = first(baseUrl, process.env.OYA_BASE_URL, saved.baseUrl, URL_SOURCES);
1452
+ const address = (url?.value ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
1453
+ const found = { apiKey: key?.value ?? "", apiKeyFrom: key?.from ?? "none", baseUrl: address };
1454
+ return { ...found, baseUrlFrom: url?.from ?? "default", savedKey: !!saved.apiKey };
1455
+ }
1456
+ var KEY_SOURCES = ["--key", "OYA_API_KEY", "saved"];
1457
+ var URL_SOURCES = ["--url", "OYA_BASE_URL", "saved"];
1458
+ function resolved() {
1459
+ const { apiKey, baseUrl } = origin();
1460
+ return { apiKey, baseUrl };
1370
1461
  }
1371
1462
 
1372
1463
  // ../sdk/dist/index.js
1464
+ var DEFAULT_BASE_URL2 = "https://oyabrowser.com";
1465
+ var DEFAULT_TIMEOUT_MS = 6e4;
1466
+ var START_TIMEOUT_MS = 12e4;
1467
+ var READY_TIMEOUT_MS2 = 12e4;
1468
+ var READY_POLL_MS2 = 2e3;
1469
+ var NAVIGATE_TIMEOUT_MS = 12e4;
1470
+ var CHALLENGE_TIMEOUT_MS = 18e4;
1471
+ var AGENT_TIMEOUT_MS = 6e5;
1472
+ var PLAYBOOK_TIMEOUT_MS = 12e4;
1473
+ var STOP_TIMEOUT_MS = 6e4;
1474
+ var WAIT_FOR_DEFAULT_MS = 3e4;
1475
+ var WAIT_FOR_GRACE_MS = 5e3;
1476
+ var AIMED_SCROLL_AMOUNT = 500;
1477
+ var RUN_POLL_MS = 2e3;
1478
+ var MAX_RUN_POLL_ERRORS = 5;
1479
+ var BYTES_PER_MB = 1048576;
1480
+ var MAX_FILE_MB = 10;
1481
+ var MS_PER_SECOND = 1e3;
1482
+ var LATIN1_MAX = 255;
1483
+ var SPACE = 32;
1484
+ var TAB = 9;
1485
+ var DEL = 127;
1486
+ var HEX = 16;
1487
+ var CODE_POINT_DIGITS = 4;
1488
+ var Status3 = {
1489
+ /** The caller passed something unusable, such as a non-numeric element id. */
1490
+ BAD_REQUEST: 400,
1491
+ /** The server does not know that session. */
1492
+ NOT_FOUND: 404,
1493
+ /** The browser is in a state that needs attention first. */
1494
+ CONFLICT: 409,
1495
+ /** A browser command ran and failed. */
1496
+ UNPROCESSABLE: 422,
1497
+ /** A run failed without saying why. */
1498
+ SERVER_ERROR: 500,
1499
+ /** A browser did not come up in time. */
1500
+ GATEWAY_TIMEOUT: 504
1501
+ };
1373
1502
  var OyaError = class extends Error {
1374
1503
  /** The HTTP status, or the SDK's own status for errors it raises itself. */
1375
1504
  status;
@@ -1383,6 +1512,10 @@ var OyaError = class extends Error {
1383
1512
  this.body = body;
1384
1513
  }
1385
1514
  };
1515
+ function refusal(message, field, suggestion) {
1516
+ const body = { error: message, code: "invalid_request", field, ...suggestion ? { suggestion } : {} };
1517
+ return new OyaError(message, Status3.BAD_REQUEST, body);
1518
+ }
1386
1519
  var node = () => globalThis.process;
1387
1520
  function builtin(name) {
1388
1521
  const get = node()?.getBuiltinModule;
@@ -1403,54 +1536,25 @@ function savedConfig() {
1403
1536
  return {};
1404
1537
  }
1405
1538
  }
1406
- var DEFAULT_BASE_URL2 = "https://oyabrowser.com";
1407
- var DEFAULT_TIMEOUT_MS = 6e4;
1408
- var START_TIMEOUT_MS = 12e4;
1409
- var READY_TIMEOUT_MS2 = 12e4;
1410
- var READY_POLL_MS2 = 2e3;
1411
- var NAVIGATE_TIMEOUT_MS = 12e4;
1412
- var CHALLENGE_TIMEOUT_MS = 18e4;
1413
- var AGENT_TIMEOUT_MS = 6e5;
1414
- var PLAYBOOK_TIMEOUT_MS = 12e4;
1415
- var STOP_TIMEOUT_MS = 6e4;
1416
- var WAIT_FOR_DEFAULT_MS = 3e4;
1417
- var WAIT_FOR_GRACE_MS = 5e3;
1418
- var AIMED_SCROLL_AMOUNT = 500;
1419
- var RUN_POLL_MS = 2e3;
1420
- var MAX_RUN_POLL_ERRORS = 5;
1421
- var BYTES_PER_MB = 1048576;
1422
- var MAX_FILE_MB = 10;
1423
- var MS_PER_SECOND = 1e3;
1424
- var Status3 = {
1425
- /** The caller passed something unusable, such as a non-numeric element id. */
1426
- BAD_REQUEST: 400,
1427
- /** The server does not know that session. */
1428
- NOT_FOUND: 404,
1429
- /** The browser is in a state that needs attention first. */
1430
- CONFLICT: 409,
1431
- /** A browser command ran and failed. */
1432
- UNPROCESSABLE: 422,
1433
- /** A run failed without saying why. */
1434
- SERVER_ERROR: 500,
1435
- /** A browser did not come up in time. */
1436
- GATEWAY_TIMEOUT: 504
1437
- };
1539
+ var OPTIONS_ORIGIN = { apiKeyFrom: "the apiKey option", baseUrlFrom: "the baseUrl option", savedKey: false };
1438
1540
  var Http = class {
1439
- /** Stores where to call, with which key, how long to wait and which fetch to use. */
1440
- constructor(baseUrl, apiKey, timeoutMs, fetchImpl) {
1541
+ /** Stores where to call, with which key, how long to wait, which fetch to use, and where the key and address came from. */
1542
+ constructor(baseUrl, apiKey, timeoutMs, fetchImpl, origin3 = OPTIONS_ORIGIN) {
1441
1543
  this.baseUrl = baseUrl;
1442
1544
  this.apiKey = apiKey;
1443
1545
  this.timeoutMs = timeoutMs;
1444
1546
  this.fetchImpl = fetchImpl;
1547
+ this.origin = origin3;
1445
1548
  }
1446
1549
  baseUrl;
1447
1550
  apiKey;
1448
1551
  timeoutMs;
1449
1552
  fetchImpl;
1553
+ origin;
1450
1554
  /** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
1451
1555
  async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
1452
1556
  const res = await this.send(`${this.baseUrl}${path}`, this.init(method, body, timeoutMs, headers), timeoutMs);
1453
- return readAnswer2(res, `${method} ${path}`, this.baseUrl);
1557
+ return readAnswer2(res, `${method} ${path}`, this);
1454
1558
  }
1455
1559
  /** The fetch options for one call, with this client's key on them. */
1456
1560
  init(method, body, timeoutMs, headers) {
@@ -1465,23 +1569,34 @@ var Http = class {
1465
1569
  try {
1466
1570
  return await this.fetchImpl(url, init);
1467
1571
  } catch (err) {
1468
- throw new OyaError(unreachable(this.baseUrl, timeoutMs, err), 0, { error: String(err?.message) });
1572
+ throw noAnswer(this, timeoutMs, err);
1469
1573
  }
1470
1574
  }
1471
1575
  };
1472
- function unreachable(baseUrl, timeoutMs, err) {
1473
- const cause = err?.name === "TimeoutError" || /abort/i.test(String(err?.message));
1474
- if (cause) return `No answer from ${baseUrl} within ${timeoutMs}ms. Is it reachable, and is the call this slow?`;
1475
- return `Could not reach ${baseUrl}. Is the server running, and is OYA_BASE_URL right?`;
1576
+ var timedOut = (err) => err?.name === "TimeoutError" || /abort/i.test(String(err?.message));
1577
+ function causeOf(err) {
1578
+ const cause = err?.cause;
1579
+ return cause?.code ?? cause?.errors?.[0]?.code;
1580
+ }
1581
+ function noAnswer(http, timeoutMs, err) {
1582
+ const code = timedOut(err) ? "timeout" : "unreachable";
1583
+ const body = { error: String(err?.message), code, ...causeOf(err) ? { cause: causeOf(err) } : {} };
1584
+ return new OyaError(unreachable(http, timeoutMs, code), 0, body);
1585
+ }
1586
+ function unreachable({ baseUrl, origin: origin3 }, timeoutMs, code) {
1587
+ if (code === "timeout")
1588
+ return `No answer from ${baseUrl} within ${timeoutMs}ms. Is it reachable, and is the call this slow?`;
1589
+ if (origin3.baseUrlFrom === "default") return `Could not reach ${baseUrl}. Check your connection.`;
1590
+ return `Could not reach ${baseUrl}. Is the server running, and is ${origin3.baseUrlFrom} right?`;
1476
1591
  }
1477
1592
  function requestInit(method, body, timeoutMs, headers) {
1478
1593
  const json = body === void 0 ? {} : { "Content-Type": "application/json" };
1479
1594
  const payload = body === void 0 ? void 0 : JSON.stringify(body);
1480
1595
  return { method, headers: { ...headers, ...json }, body: payload, signal: AbortSignal.timeout(timeoutMs) };
1481
1596
  }
1482
- async function readAnswer2(res, call2, baseUrl) {
1597
+ async function readAnswer2(res, call2, http) {
1483
1598
  const payload = parseBody(await res.text());
1484
- if (!res.ok) throw failure(call2, res.status, payload, baseUrl);
1599
+ if (!res.ok) throw failure(call2, res.status, payload, http);
1485
1600
  return payload;
1486
1601
  }
1487
1602
  function parseBody(text) {
@@ -1491,22 +1606,74 @@ function parseBody(text) {
1491
1606
  return text;
1492
1607
  }
1493
1608
  }
1494
- function failure(call2, status, payload, baseUrl) {
1495
- let message = payload?.error || `${call2} failed (${status})`;
1496
- if (message === "Invalid API key") {
1497
- message += ` for ${baseUrl}. Check OYA_API_KEY: a value exported in your shell beats .env.`;
1498
- }
1499
- return new OyaError(message, status, payload);
1609
+ function failure(call2, status, payload, http) {
1610
+ const message = payload?.error || `${call2} failed (${status})`;
1611
+ return new OyaError(message === "Invalid API key" ? invalidKey(http) : message, status, payload);
1612
+ }
1613
+ function invalidKey({ baseUrl, origin: origin3 }) {
1614
+ const overrides = origin3.apiKeyFrom === "OYA_API_KEY" && origin3.savedKey;
1615
+ const rest = overrides ? ", which overrides the one saved in ~/.oya/config.json" : "";
1616
+ return `Invalid API key for ${baseUrl}. The key came from ${origin3.apiKeyFrom}${rest}.`;
1500
1617
  }
1501
1618
  var env = (name) => globalThis.process?.env?.[name];
1619
+ function firstOf(option, name, saved, optionName) {
1620
+ if (option) return { value: option, from: `the ${optionName} option` };
1621
+ if (env(name)) return { value: env(name), from: name };
1622
+ return { value: saved, from: "~/.oya/config.json" };
1623
+ }
1624
+ function addressOf(options, saved) {
1625
+ const found = firstOf(options.baseUrl, "OYA_BASE_URL", saved, "baseUrl");
1626
+ return found.value ? found : { value: DEFAULT_BASE_URL2, from: "default" };
1627
+ }
1628
+ function keyOf(options, saved) {
1629
+ const found = firstOf(options.apiKey, "OYA_API_KEY", saved, "apiKey");
1630
+ if (!found.value) throw new Error("No API key. Pass { apiKey }, set OYA_API_KEY, or run `npx @oya-ai/cli login`.");
1631
+ return { ...found, value: headerSafe(found.value, "apiKey", "Copy the key again.") };
1632
+ }
1502
1633
  function createHttp(options) {
1503
1634
  const saved = savedConfig();
1504
- const apiKey = options.apiKey || env("OYA_API_KEY") || saved.apiKey;
1505
- if (!apiKey) throw new Error("No API key. Pass { apiKey }, set OYA_API_KEY, or run `npx @oya-ai/cli login`.");
1506
- const baseUrl = (options.baseUrl || env("OYA_BASE_URL") || saved.baseUrl || DEFAULT_BASE_URL2).replace(/\/+$/, "");
1635
+ const key = keyOf(options, saved.apiKey);
1636
+ const address = addressOf(options, saved.baseUrl);
1507
1637
  const fetchImpl = options.fetch || globalThis.fetch;
1508
1638
  if (!fetchImpl) throw new Error("No fetch available, pass { fetch } or use Node 18+.");
1509
- return new Http(baseUrl, apiKey, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, fetchImpl.bind(globalThis));
1639
+ const origin3 = { apiKeyFrom: key.from, baseUrlFrom: address.from, savedKey: !!saved.apiKey };
1640
+ const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1641
+ return new Http(webAddress(address), key.value, timeout, fetchImpl.bind(globalThis), origin3);
1642
+ }
1643
+ var LOCAL_HOST = /^(localhost|127\.)/i;
1644
+ function webAddress({ value, from }) {
1645
+ const url = value.trim().replace(/\/+$/, "");
1646
+ if (/^https?:\/\//i.test(url)) return url;
1647
+ const name = from.startsWith("the ") ? "baseUrl" : from === "OYA_BASE_URL" ? from : `baseUrl in ${from}`;
1648
+ const suggestion = suggestionFor(url);
1649
+ const tryIt = suggestion ? ` Try ${suggestion}.` : "";
1650
+ throw refusal(`${name} must start with http:// or https://, not "${url}".${tryIt}`, "baseUrl", suggestion);
1651
+ }
1652
+ function suggestionFor(url) {
1653
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !/^[^:/]+:\d+(\/|$)/.test(url)) return void 0;
1654
+ return `${LOCAL_HOST.test(url) ? "http" : "https"}://${url}`;
1655
+ }
1656
+ function notInAHeader(ch) {
1657
+ const code = ch.codePointAt(0);
1658
+ return code < SPACE && code !== TAB || code === DEL || code > LATIN1_MAX;
1659
+ }
1660
+ function headerSafe(value, field, ending) {
1661
+ const trimmed = value.trim();
1662
+ const chars = Array.from(trimmed);
1663
+ const at = chars.findIndex(notInAHeader);
1664
+ if (at < 0) return trimmed;
1665
+ const leading = Array.from(value).length - Array.from(value.trimStart()).length;
1666
+ const where = `position ${leading + at + 1} (${codePoint(chars[at])})`;
1667
+ throw refusal(`${field} has a character HTTP headers cannot carry at ${where}. ${ending}`, field);
1668
+ }
1669
+ var codePoint = (ch) => `U+${ch.codePointAt(0).toString(HEX).toUpperCase().padStart(CODE_POINT_DIGITS, "0")}`;
1670
+ var shown = (value) => typeof value === "string" ? JSON.stringify(value) : String(value);
1671
+ function segment(value, field = "id") {
1672
+ if (typeof value !== "string" || !value)
1673
+ throw refusal(`${field} must be a non-empty string, not ${shown(value)}`, field);
1674
+ if (value.includes("/") || value === "." || value === "..")
1675
+ throw refusal(`${field} must be a single path segment (no "/" and not "." or ".."), not ${shown(value)}`, field);
1676
+ return encodeURIComponent(value);
1510
1677
  }
1511
1678
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1512
1679
  async function call(fn) {
@@ -1739,7 +1906,7 @@ var Browser = class {
1739
1906
  * Play `'<name>:draft'` to try a draft before promoting it.
1740
1907
  */
1741
1908
  async play(name, data = {}, { autoHeal = true } = {}) {
1742
- const path = `/api/browsers/${this.id}/playbooks/${encodeURIComponent(name)}/play`;
1909
+ const path = `/api/browsers/${this.id}/playbooks/${segment(name, "name")}/play`;
1743
1910
  return agentAnswer(
1744
1911
  await this.http.request("POST", path, { variables: data, autoHeal }, AGENT_TIMEOUT_MS)
1745
1912
  );
@@ -1790,7 +1957,7 @@ var Browser = class {
1790
1957
  }
1791
1958
  /** Revoke a link from `shareUrl()` before it expires, by the id it returned. */
1792
1959
  async revokeShare(id) {
1793
- await this.http.request("DELETE", `/api/control/credentials/${encodeURIComponent(id)}`);
1960
+ await this.http.request("DELETE", `/api/control/credentials/${segment(id)}`);
1794
1961
  }
1795
1962
  /** Counters, health and the last 50 things this browser did. */
1796
1963
  status() {
@@ -1844,8 +2011,9 @@ function startBody(options) {
1844
2011
  const { profile, persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy } = options;
1845
2012
  return { profile: profile || persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy };
1846
2013
  }
2014
+ var idempotencyKeyOf = ({ idempotencyKey }) => idempotencyKey ? headerSafe(idempotencyKey, "idempotencyKey", 'Use letters, digits, "-" and "_".') : globalThis.crypto.randomUUID();
1847
2015
  async function start(http, wait, options) {
1848
- const headers = { "Idempotency-Key": options.idempotencyKey || globalThis.crypto.randomUUID() };
2016
+ const headers = { "Idempotency-Key": idempotencyKeyOf(options) };
1849
2017
  const path = "/api/browsers/start";
1850
2018
  const started = await http().request("POST", path, startBody(options), START_TIMEOUT_MS, headers);
1851
2019
  if (started.status === "starting") {
@@ -1854,7 +2022,7 @@ async function start(http, wait, options) {
1854
2022
  }
1855
2023
  return new Browser(http(), started, options.captcha === "auto");
1856
2024
  }
1857
- var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${encodeURIComponent(id)}`);
2025
+ var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${segment(id)}`);
1858
2026
  async function reattach(http, id) {
1859
2027
  const found = await fetchBrowser(http, id);
1860
2028
  return new Browser(http(), asStarted(found), false);
@@ -1866,67 +2034,77 @@ function asStarted(found) {
1866
2034
  var stopBrowsers = (http, ids) => http().request("POST", "/api/browsers/stop", ids === "all" ? { all: true } : { ids }, START_TIMEOUT_MS);
1867
2035
  var browserApi = (http, wait) => ({
1868
2036
  /** Start a browser and wait until it can take commands. */
1869
- start: (options = {}) => start(http, wait, options),
2037
+ start: async (options = {}) => start(http, wait, options),
1870
2038
  /** Reattach to a browser that is already running. */
1871
- get: (id) => reattach(http, id),
2039
+ get: async (id) => reattach(http, id),
1872
2040
  /** Every browser on this key. */
1873
- list: () => http().request("GET", "/api/browsers"),
2041
+ list: async () => http().request("GET", "/api/browsers"),
1874
2042
  /** Stop some (`ids`) or every browser on this key. Each reports separately. */
1875
- stop: (ids) => stopBrowsers(http, ids),
2043
+ stop: async (ids) => stopBrowsers(http, ids),
1876
2044
  /** Stop every browser on this key; returns how many stopped. */
1877
2045
  stopAll: async () => (await stopBrowsers(http, "all")).stopped
1878
2046
  });
1879
- var session = (id, action = "") => `/api/control/sessions/${encodeURIComponent(id)}${action}`;
1880
- var item = (kind, id) => `/api/control/${kind}/${encodeURIComponent(id)}`;
2047
+ var session = (id, action = "") => `/api/control/sessions/${segment(id)}${action}`;
2048
+ var item = (kind, id) => `/api/control/${kind}/${segment(id)}`;
1881
2049
  var sessionCalls = (http) => ({
1882
2050
  /** Settings, sessions and recent events at a glance. */
1883
- overview: () => http().request("GET", "/api/control"),
2051
+ overview: async () => http().request("GET", "/api/control"),
1884
2052
  /** Every session, including disconnected and cleanup-pending ones. */
1885
- sessions: () => http().request("GET", "/api/control/sessions"),
2053
+ sessions: async () => http().request("GET", "/api/control/sessions"),
1886
2054
  /** One session. */
1887
- session: (id) => http().request("GET", session(id)),
2055
+ session: async (id) => http().request("GET", session(id)),
1888
2056
  /** Update limits, rate cards and retention. */
1889
- settings: (changes) => http().request("PATCH", "/api/control/project", changes)
2057
+ settings: async (changes) => http().request("PATCH", "/api/control/project", changes)
1890
2058
  });
1891
2059
  var lifecycleCalls = (http) => ({
1892
- /** Cancel queued or provisioning work. */
1893
- cancel: (id) => http().request("POST", session(id, "/cancel"), {}),
2060
+ /** Stop a session in any state; answers its final state. */
2061
+ cancel: async (id) => http().request("POST", session(id, "/cancel"), {}),
1894
2062
  /** Stop a session; `force` stops despite a profile-save error, or reconciles. */
1895
- stop: (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
2063
+ stop: async (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
1896
2064
  /** Acquire or release human control, or acknowledge the agent's resume. */
1897
- takeover: (id, action) => http().request("POST", session(id, "/control"), { action }),
2065
+ takeover: async (id, action) => http().request("POST", session(id, "/control"), { action }),
1898
2066
  /** Send one input as the human holding the control lease. */
1899
- input: (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
2067
+ input: async (id, action, params) => http().request("POST", session(id, "/input"), { action, params })
1900
2068
  });
2069
+ function recoverBody(options) {
2070
+ const { replace = false, wsUrl } = typeof options === "boolean" ? { replace: options } : options ?? {};
2071
+ if (wsUrl && !replace)
2072
+ throw refusal("wsUrl is only used with replace: true; recovering in place keeps the endpoint it had.", "wsUrl");
2073
+ return wsUrl ? { replace, wsUrl } : { replace };
2074
+ }
1901
2075
  var recoveryCalls = (http) => ({
1902
- /** Explicitly recover a session, or replace it with a fresh one. */
1903
- recover: (id, replace = false) => http().request("POST", session(id, "/recover"), { replace }),
2076
+ /**
2077
+ * Explicitly recover a session, or replace it with a fresh one. A cdp
2078
+ * session's replacement needs the Chrome it runs on: `{ replace: true, wsUrl }`.
2079
+ * `recover(id, true)` still means replace.
2080
+ */
2081
+ recover: async (id, options = {}) => http().request("POST", session(id, "/recover"), recoverBody(options)),
1904
2082
  /** A single-use ticket for the live stream. */
1905
- ticket: (id) => http().request("POST", session(id, "/ticket"), {}),
2083
+ ticket: async (id) => http().request("POST", session(id, "/ticket"), {}),
1906
2084
  /** Durable lifecycle events after a cursor. */
1907
- events: (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
2085
+ events: async (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
1908
2086
  });
1909
2087
  var credentialCalls = (http) => ({
1910
2088
  /** Mint a service credential. Its token is returned this once. */
1911
- createCredential: (options) => http().request("POST", "/api/control/credentials", options),
2089
+ createCredential: async (options) => http().request("POST", "/api/control/credentials", options),
1912
2090
  /** Revoke a service credential. */
1913
- revokeCredential: (id) => http().request("DELETE", item("credentials", id))
2091
+ revokeCredential: async (id) => http().request("DELETE", item("credentials", id))
1914
2092
  });
1915
2093
  var memberCalls = (http) => ({
1916
2094
  /** The owner and every member. */
1917
- members: () => http().request("GET", "/api/control/members"),
2095
+ members: async () => http().request("GET", "/api/control/members"),
1918
2096
  /** An invitation code for a new member. */
1919
- inviteMember: (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
2097
+ inviteMember: async (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
1920
2098
  /** Remove a member. */
1921
- removeMember: (userId) => http().request("DELETE", item("members", userId))
2099
+ removeMember: async (userId) => http().request("DELETE", item("members", userId))
1922
2100
  });
1923
2101
  var webhookCalls = (http) => ({
1924
2102
  /** Register a webhook; `types` limits which events it receives. */
1925
- createWebhook: (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
2103
+ createWebhook: async (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
1926
2104
  /** Disable a webhook. */
1927
- removeWebhook: (id) => http().request("DELETE", item("webhooks", id)),
2105
+ removeWebhook: async (id) => http().request("DELETE", item("webhooks", id)),
1928
2106
  /** Send a delivery again. */
1929
- replayDelivery: (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
2107
+ replayDelivery: async (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
1930
2108
  });
1931
2109
  var controlApi = (http) => ({
1932
2110
  ...sessionCalls(http),
@@ -1936,7 +2114,7 @@ var controlApi = (http) => ({
1936
2114
  ...memberCalls(http),
1937
2115
  ...webhookCalls(http)
1938
2116
  });
1939
- var playbook = (name) => `/api/playbooks/${encodeURIComponent(name)}`;
2117
+ var playbook = (name) => `/api/playbooks/${segment(name, "name")}`;
1940
2118
  var playbookApi = (http) => ({
1941
2119
  /** Every saved playbook, with any draft waiting on it. */
1942
2120
  list: async () => (await http().request("GET", "/api/playbooks")).playbooks,
@@ -1945,16 +2123,16 @@ var playbookApi = (http) => ({
1945
2123
  await http().request("DELETE", playbook(name));
1946
2124
  },
1947
2125
  /** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
1948
- promote: (name) => http().request("POST", `${playbook(name)}/promote`, {})
2126
+ promote: async (name) => http().request("POST", `${playbook(name)}/promote`, {})
1949
2127
  });
1950
2128
  var proxyApi = (http) => ({
1951
2129
  /** Every proxy this key can use, shared ones included. */
1952
2130
  list: async () => (await http().request("GET", "/api/proxies")).proxies,
1953
2131
  /** Add a proxy. Its credentials are never read back. */
1954
- create: (proxy) => http().request("POST", "/api/proxies", proxy),
2132
+ create: async (proxy) => http().request("POST", "/api/proxies", proxy),
1955
2133
  /** Remove one of this key's proxies. */
1956
2134
  remove: async (id) => {
1957
- await http().request("DELETE", `/api/proxies/${encodeURIComponent(id)}`);
2135
+ await http().request("DELETE", `/api/proxies/${segment(id)}`);
1958
2136
  },
1959
2137
  /** Dial each proxy and learn its real exit IP. Failing ones cool down and are skipped. */
1960
2138
  check: async () => (await http().request("POST", "/api/proxies/check", {})).results
@@ -1963,35 +2141,38 @@ var identityCalls = (http) => ({
1963
2141
  /** Every persona on this key. */
1964
2142
  list: async () => (await http().request("GET", "/api/personas")).personas,
1965
2143
  /** One persona. */
1966
- get: (id) => http().request("GET", `/api/personas/${id}`),
2144
+ get: async (id) => http().request("GET", `/api/personas/${segment(id)}`),
1967
2145
  /**
1968
2146
  * Create an identity. The device, platform, timezone, locale, is chosen
1969
2147
  * here and fixed for its life; `preview()` shows what a choice produces.
1970
2148
  */
1971
- create: (options = {}) => http().request("POST", "/api/personas", options),
2149
+ create: async (options = {}) => http().request("POST", "/api/personas", options),
1972
2150
  /** Name, concurrency cap and proxy hint. Never the device, clone for that. */
1973
- update: (id, changes) => http().request("PUT", `/api/personas/${id}`, changes),
2151
+ update: async (id, changes) => http().request("PUT", `/api/personas/${segment(id)}`, changes),
1974
2152
  /** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
1975
- clone: (id, options = {}) => http().request("POST", `/api/personas/${id}/clone`, options)
2153
+ clone: async (id, options = {}) => http().request("POST", `/api/personas/${segment(id)}/clone`, options)
1976
2154
  });
1977
2155
  var deviceCalls = (http) => ({
1978
2156
  /** The fingerprint these choices would produce. Persists nothing. */
1979
2157
  preview: async (prefs = {}) => (await http().request("POST", "/api/personas/preview", { prefs })).fingerprint,
1980
2158
  /** Platforms, and the timezones and locales each may coherently claim. */
1981
- options: () => http().request("GET", "/api/personas/options"),
2159
+ options: async () => http().request("GET", "/api/personas/options"),
1982
2160
  /** Pin the persona to one of your proxies, or `null` to let assignment happen at connect. */
1983
- pinProxy: (id, proxyId) => http().request("PUT", `/api/personas/${id}/proxy`, { proxyId }),
2161
+ pinProxy: async (id, proxyId) => http().request("PUT", `/api/personas/${segment(id)}/proxy`, { proxyId }),
1984
2162
  /** Delete a persona. */
1985
2163
  remove: async (id) => {
1986
- await http().request("DELETE", `/api/personas/${id}`);
2164
+ await http().request("DELETE", `/api/personas/${segment(id)}`);
1987
2165
  }
1988
2166
  });
1989
2167
  var mfaCalls = (http) => ({
1990
2168
  /** Store the second factor for this identity. Sealed at rest, never read back. */
1991
- setMfa: (id, config) => http().request("PUT", `/api/personas/${id}/mfa`, config),
2169
+ setMfa: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/mfa`, config),
1992
2170
  /** Remove the persona-wide factor, or the one filed against `domain`. */
1993
2171
  clearMfa: async (id, domain) => {
1994
- await http().request("DELETE", `/api/personas/${id}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`);
2172
+ await http().request(
2173
+ "DELETE",
2174
+ `/api/personas/${segment(id)}/mfa${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`
2175
+ );
1995
2176
  }
1996
2177
  });
1997
2178
  var loginCalls = (http) => ({
@@ -2002,20 +2183,20 @@ var loginCalls = (http) => ({
2002
2183
  * better path. This is for portals that expire a session server-side
2003
2184
  * between runs, where an unattended run has nothing else to recover with.
2004
2185
  */
2005
- setCredentials: (id, config) => http().request("PUT", `/api/personas/${id}/credentials`, config),
2186
+ setCredentials: async (id, config) => http().request("PUT", `/api/personas/${segment(id)}/credentials`, config),
2006
2187
  /** Which sites this identity can sign in to. Usernames only. */
2007
- credentials: (id) => http().request("GET", `/api/personas/${id}/credentials`),
2188
+ credentials: async (id) => http().request("GET", `/api/personas/${segment(id)}/credentials`),
2008
2189
  /** Remove the login stored for one site. */
2009
2190
  clearCredentials: async (id, domain) => {
2010
- await http().request("DELETE", `/api/personas/${id}/credentials?domain=${encodeURIComponent(domain)}`);
2191
+ await http().request("DELETE", `/api/personas/${segment(id)}/credentials?domain=${encodeURIComponent(domain)}`);
2011
2192
  }
2012
2193
  });
2013
- var jarPath = (id) => `/api/pool/cookies?persona=${encodeURIComponent(id)}`;
2194
+ var jarPath = (id) => `/api/pool/cookies?persona=${segment(id)}`;
2014
2195
  var jarCalls = (http) => ({
2015
2196
  /** Every cookie in the jar; `format: 'playwright'` is ready for `context.addCookies()`. */
2016
- cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${format}`)).cookies,
2197
+ cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${encodeURIComponent(format)}`)).cookies,
2017
2198
  /** Merge cookies into the jar. The persona's browsers pick them up on their next visit to each site. */
2018
- importCookies: (id, list2) => http().request("PUT", jarPath(id), { cookies: list2 })
2199
+ importCookies: async (id, list2) => http().request("PUT", jarPath(id), { cookies: list2 })
2019
2200
  });
2020
2201
  var cookieCalls = (http) => ({
2021
2202
  ...jarCalls(http),
@@ -2115,32 +2296,101 @@ var Oya = class {
2115
2296
  };
2116
2297
 
2117
2298
  // src/context.ts
2118
- function client(flags2) {
2119
- const saved = resolved();
2120
- const apiKey = flagStr(flags2, "key") || saved.apiKey;
2121
- const baseUrl = flagStr(flags2, "url") || saved.baseUrl;
2122
- if (!apiKey) {
2123
- console.error("No API key. Run `oya login`, or pass --key / set OYA_API_KEY.");
2124
- process.exit(1);
2125
- }
2299
+ var origin2 = (flags) => origin({ apiKey: flagStr(flags, "key"), baseUrl: flagStr(flags, "url") });
2300
+ function client(flags) {
2301
+ const { apiKey, baseUrl } = origin2(flags);
2302
+ if (!apiKey)
2303
+ throw new CliError("No API key.", "no_api_key", { hint: "Run oya login, or pass --key, or set OYA_API_KEY." });
2126
2304
  return new Oya({ apiKey, baseUrl });
2127
2305
  }
2128
2306
  var printJson = (value) => console.log(JSON.stringify(value, null, JSON_INDENT2));
2129
- var out = (flags2, value, human) => {
2130
- if (flags2.json) printJson(value);
2307
+ var out = (flags, value, human) => {
2308
+ if (flags.json) printJson(value);
2131
2309
  else human();
2132
2310
  };
2133
- async function targetBrowser(oya, flags2) {
2134
- const id = flagStr(flags2, "id");
2311
+ async function targetBrowser(oya, flags) {
2312
+ const id = flagStr(flags, "id");
2135
2313
  if (id) return oya.browser.get(id);
2136
2314
  const all = await oya.browser.list();
2137
- if (!all.length) {
2138
- console.error("No browsers running. Start one with `oya start`.");
2139
- process.exit(1);
2140
- }
2315
+ if (!all.length) throw new CliError("No browsers running.", "no_browser", { hint: "Start one with oya start." });
2141
2316
  return oya.browser.get(all[all.length - 1].id);
2142
2317
  }
2143
2318
 
2319
+ // src/commands/login.ts
2320
+ async function postJson(url, body, token2) {
2321
+ const headers = { ...token2 ? { Authorization: `Bearer ${token2}` } : {}, "Content-Type": "application/json" };
2322
+ const signal = AbortSignal.timeout(LOGIN_TIMEOUT_MS);
2323
+ const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body), signal }).catch(() => {
2324
+ throw new CliError(`Could not reach ${new URL(url).origin}.`, "unreachable", {
2325
+ hint: "Check the address, then run oya login again."
2326
+ });
2327
+ });
2328
+ return { ok: res.ok, body: jsonOr(await res.text(), `${res.status} from ${url}`) };
2329
+ }
2330
+ function jsonOr(text, answer) {
2331
+ try {
2332
+ return JSON.parse(text);
2333
+ } catch {
2334
+ return { error: `Not a JSON answer: ${answer}` };
2335
+ }
2336
+ }
2337
+ async function mintKey(baseUrl) {
2338
+ const email = await ask("Email:");
2339
+ const password = await askSecret("Password:");
2340
+ const session2 = await postJson(`${baseUrl}/api/auth/login`, { email, password });
2341
+ if (!session2.ok || !session2.body.access_token) throw new Error(session2.body.error || "Sign-in failed");
2342
+ const minted = await postJson(`${baseUrl}/api/auth/keys`, { label: "CLI" }, session2.body.access_token);
2343
+ if (!minted.ok || !minted.body.key) throw new Error(minted.body.error || "Could not create an API key");
2344
+ console.log(' Created a new API key labelled "CLI".');
2345
+ return minted.body.key;
2346
+ }
2347
+ function chooseMethod() {
2348
+ return choose("How do you want to authenticate?", [
2349
+ { id: "paste", label: "Paste an API key", note: "from the dashboard, or a self-hosted key" },
2350
+ { id: "password", label: "Sign in with email and password", note: "mints a new key for this machine" }
2351
+ ]);
2352
+ }
2353
+ async function obtainKey(flags, baseUrl) {
2354
+ const given = flagStr(flags, "key") || "";
2355
+ if (given) return given;
2356
+ const how = await chooseMethod();
2357
+ if (how === "paste") return askSecret("API key:");
2358
+ return mintKey(baseUrl);
2359
+ }
2360
+ async function verifyKey(baseUrl, apiKey) {
2361
+ await new Oya({ apiKey, baseUrl }).config.get().catch((err) => {
2362
+ throw namedFailure(err, baseUrl);
2363
+ });
2364
+ }
2365
+ function namedFailure(err, baseUrl) {
2366
+ const status = err.status;
2367
+ const again = "then run oya login again.";
2368
+ if (status === 0)
2369
+ return new CliError(`Could not reach ${baseUrl}.`, "unreachable", { hint: `Check the address, ${again}` });
2370
+ if (status !== Status2.UNAUTHORIZED && status !== Status2.FORBIDDEN) return err;
2371
+ return new CliError(`${baseUrl} rejected that key.`, "invalid_key", { hint: `Check the key, ${again}` });
2372
+ }
2373
+ async function loginUrl(flags) {
2374
+ const current = resolved().baseUrl;
2375
+ const url = flagStr(flags, "url") || (flagStr(flags, "key") ? current : await ask("Control plane URL:", current));
2376
+ return url.replace(/\/+$/, "");
2377
+ }
2378
+ async function cmdLogin(flags) {
2379
+ if (flags.json && !flagStr(flags, "key")) throw usage("oya login --json never prompts, so it needs --key <key>.");
2380
+ const baseUrl = await loginUrl(flags);
2381
+ const apiKey = await obtainKey(flags, baseUrl);
2382
+ if (!apiKey) throw usage("No API key given.");
2383
+ await verifyKey(baseUrl, apiKey);
2384
+ save({ apiKey, baseUrl });
2385
+ out(flags, { ok: true, baseUrl, configPath }, () => signedIn(baseUrl));
2386
+ }
2387
+ function signedIn(baseUrl) {
2388
+ console.log(`
2389
+ \u2705 Signed in to ${baseUrl}. Saved to ${configPath}`);
2390
+ console.log(" Scripts using @oya-ai/browser read this file too, so `new Oya()` needs no environment.");
2391
+ console.log(" Next: `oya init` to pick your model and browser provider.");
2392
+ }
2393
+
2144
2394
  // src/commands/init.ts
2145
2395
  var PRESETS = {
2146
2396
  anthropic: { name: "Anthropic", model: "claude-sonnet-5" },
@@ -2167,20 +2417,29 @@ async function askModelDetails(llm, updates) {
2167
2417
  const model = await ask("Default model:", PRESETS[llm].model);
2168
2418
  if (model) updates.chat_model = model;
2169
2419
  }
2420
+ var needsNote = (p) => p.needs.some((f) => f.endsWith("_url")) ? "needs its ws:// address" : "needs an API key";
2421
+ function fieldLabel(p, field) {
2422
+ if (field.endsWith("_url")) return "Chrome DevTools address, such as ws://127.0.0.1:9222:";
2423
+ if (field.endsWith("_api_key")) return `${p.label} API key:`;
2424
+ return `${p.label} ${field.replace(/^[a-z]+_/, "").replace(/_/g, " ")}:`;
2425
+ }
2170
2426
  function providerOptions(current) {
2171
- const note2 = (p) => p.needs.length ? p.configured ? "configured" : "needs an API key" : void 0;
2427
+ const note2 = (p) => p.needs.length ? p.configured ? "configured" : needsNote(p) : void 0;
2172
2428
  return current.providers.map((p) => ({ id: p.id, label: p.label, note: note2(p) }));
2173
2429
  }
2174
2430
  async function askProvider(current, updates) {
2175
2431
  console.log("\n\u2500\u2500 2. Where your browsers run \u2500\u2500");
2176
2432
  const provider = await choose("Browser provider:", providerOptions(current));
2177
2433
  updates.browser_provider = provider;
2178
- for (const field of current.providers.find((p) => p.id === provider)?.needs || []) {
2179
- const value = await askSecret(`${field.replace(/_/g, " ")}:`);
2180
- if (value) updates[field] = value;
2181
- }
2434
+ const info = current.providers.find((p) => p.id === provider);
2435
+ for (const field of info?.needs || []) await askField(info, field, updates);
2182
2436
  return provider;
2183
2437
  }
2438
+ async function askField(info, field, updates) {
2439
+ const label = fieldLabel(info, field);
2440
+ const value = field.endsWith("_url") ? await ask(label) : await askSecret(label);
2441
+ if (value) updates[field] = value;
2442
+ }
2184
2443
  var CAPTCHA_OPTIONS2 = [
2185
2444
  { id: "", label: "No", note: "providers that solve natively still will" },
2186
2445
  { id: "capsolver", label: "Yes, via CapSolver" }
@@ -2200,18 +2459,23 @@ function signInHint(provider) {
2200
2459
  console.log(" arrive already logged in, as the same identity, from the same fingerprint.");
2201
2460
  console.log(` Download the desktop browser: ${resolved().baseUrl}/downloads`);
2202
2461
  }
2462
+ var NO_ANSWERS = "oya init had no input to read, so its defaults were never chosen. Nothing was saved. Run it in a terminal, or pipe the answers in.";
2463
+ async function saveAnswers(oya, updates) {
2464
+ if (!answered()) throw new InputError(NO_ANSWERS);
2465
+ updates.onboarded = "true";
2466
+ await oya.config.set(updates);
2467
+ }
2203
2468
  async function interview2(oya) {
2204
2469
  const current = await oya.config.get();
2205
2470
  const updates = {};
2206
2471
  await askModel(current, updates);
2207
2472
  const provider = await askProvider(current, updates);
2208
2473
  await askCaptcha(updates);
2209
- updates.onboarded = "true";
2210
- await oya.config.set(updates);
2474
+ await saveAnswers(oya, updates);
2211
2475
  return provider;
2212
2476
  }
2213
- async function cmdInit(flags2) {
2214
- const provider = await interview2(client(flags2));
2477
+ async function cmdInit(flags) {
2478
+ const provider = await interview2(client(flags));
2215
2479
  console.log("\n\u2705 Saved against your API key.");
2216
2480
  signInHint(provider);
2217
2481
  console.log("\n Then: oya start && oya goto https://example.com");
@@ -2219,47 +2483,48 @@ async function cmdInit(flags2) {
2219
2483
 
2220
2484
  // src/commands/browsers.ts
2221
2485
  import { spawn as spawn3 } from "child_process";
2222
- function identityOptions(flags2) {
2486
+ function identityOptions(flags) {
2223
2487
  return {
2224
- persona: flagStr(flags2, "persona") || "default",
2225
- captcha: flags2.captcha === false ? "off" : "auto",
2226
- provider: flagStr(flags2, "provider"),
2227
- wsUrl: flagStr(flags2, "ws-url"),
2228
- name: flagStr(flags2, "name"),
2229
- idempotencyKey: flagStr(flags2, "idempotency-key")
2488
+ persona: flagStr(flags, "persona") || "default",
2489
+ provider: flagStr(flags, "provider"),
2490
+ wsUrl: flagStr(flags, "ws-url"),
2491
+ name: flagStr(flags, "name"),
2492
+ idempotencyKey: flagStr(flags, "idempotency-key")
2230
2493
  };
2231
2494
  }
2232
- function governanceOptions(flags2) {
2495
+ function governanceOptions(flags) {
2233
2496
  return {
2234
- queueMs: flagNum(flags2, "queue-ms"),
2235
- budgetUsd: flagNum(flags2, "budget-usd"),
2236
- governed: flags2.governed === true,
2237
- priority: flagStr(flags2, "priority"),
2238
- policy: flagStr(flags2, "policy") ? JSON.parse(flagStr(flags2, "policy")) : void 0
2497
+ queueMs: flagNum(flags, "queue-ms"),
2498
+ budgetUsd: flagNum(flags, "budget-usd"),
2499
+ governed: flags.governed === true,
2500
+ priority: flagStr(flags, "priority"),
2501
+ policy: flagStr(flags, "policy") ? policyOf(flagStr(flags, "policy")) : void 0
2239
2502
  };
2240
2503
  }
2241
- var startOptions = (flags2) => ({ ...identityOptions(flags2), ...governanceOptions(flags2) });
2242
- async function cmdStart(flags2) {
2243
- const browser = await client(flags2).browser.start(startOptions(flags2));
2504
+ var policyOf = (text) => parseJson(text, "--policy", `Quote it: --policy '{"allowedHosts":["example.com"]}'.`);
2505
+ var startOptions = (flags) => ({ ...identityOptions(flags), ...governanceOptions(flags) });
2506
+ async function cmdStart(flags) {
2507
+ const browser = await client(flags).browser.start(startOptions(flags));
2244
2508
  const { id, provider, persona, cdpUrl } = browser;
2245
- out(flags2, { id, provider, persona, cdpUrl }, () => {
2509
+ out(flags, { id, provider, persona, cdpUrl }, () => {
2246
2510
  console.log(`\u2705 ${id}`);
2247
2511
  console.log(` provider: ${provider} persona: ${persona}`);
2248
2512
  if (cdpUrl) console.log(` cdp: ${cdpUrl}`);
2249
2513
  });
2250
2514
  }
2251
- async function cmdGoto(args2, flags2) {
2252
- const url = args2[0];
2253
- if (!url) throw new Error("Usage: oya goto <url>");
2254
- const browser = await targetBrowser(client(flags2), flags2);
2515
+ async function cmdGoto(args, flags) {
2516
+ const url = args[0];
2517
+ if (!url) throw usage("oya goto needs an address: oya goto <url>.");
2518
+ const browser = await targetBrowser(client(flags), flags);
2255
2519
  await browser.goto(url);
2256
- console.log(`\u2705 ${browser.id} \u2192 ${url}`);
2520
+ out(flags, { id: browser.id, url }, () => console.log(`\u2705 ${browser.id} \u2192 ${url}`));
2257
2521
  }
2258
- async function cmdAsk(args2, flags2) {
2259
- const prompt = args2.join(" ");
2260
- if (!prompt) throw new Error('Usage: oya ask "find the pricing page"');
2261
- const browser = await targetBrowser(client(flags2), flags2);
2262
- console.log(await browser.ask(prompt));
2522
+ async function cmdAsk(args, flags) {
2523
+ const prompt = args.join(" ");
2524
+ if (!prompt) throw usage('oya ask needs a prompt: oya ask "find the pricing page".');
2525
+ const browser = await targetBrowser(client(flags), flags);
2526
+ const answer = await browser.ask(prompt);
2527
+ out(flags, { id: browser.id, answer }, () => console.log(answer));
2263
2528
  }
2264
2529
  var DOT = { ok: "\u25CF", stale: "\u25D0", errors: "\u2717", dead: "\u25CB" };
2265
2530
  function lsRow(b) {
@@ -2268,9 +2533,9 @@ function lsRow(b) {
2268
2533
  const url = b.currentUrl.replace(/^https?:\/\//, "").slice(0, LsColumns.URL);
2269
2534
  return `${DOT[b.health] || "\xB7"} ${b.id} ${provider} ${persona} ${b.name.padEnd(LsColumns.NAME)} ${b.commands}\xB7${b.errors} ${url}`;
2270
2535
  }
2271
- async function cmdLs(flags2) {
2272
- const all = await client(flags2).browser.list();
2273
- out(flags2, all, () => {
2536
+ async function cmdLs(flags) {
2537
+ const all = await client(flags).browser.list();
2538
+ out(flags, all, () => {
2274
2539
  if (!all.length) return console.log("No browsers running.");
2275
2540
  for (const b of all) console.log(lsRow(b));
2276
2541
  console.log(`
@@ -2282,13 +2547,19 @@ function rmLine(x) {
2282
2547
  const note2 = x.sandboxRemoved === false ? ", sandbox NOT removed, check Oya Cloud" : removed;
2283
2548
  return `${x.ok ? "\u2705" : "\u2717"} ${x.id}${note2}${x.error ? ` ${x.error}` : ""}`;
2284
2549
  }
2285
- async function cmdRm(args2, flags2) {
2286
- const oya = client(flags2);
2287
- if (!flags2.all && !args2.length) throw new Error("Usage: oya rm <id>\u2026 | oya rm --all");
2288
- const r = await oya.browser.stop(flags2.all ? "all" : args2);
2289
- for (const x of r.results) console.log(rmLine(x));
2290
- console.log(`stopped ${r.stopped}`);
2550
+ async function cmdRm(args, flags) {
2551
+ const oya = client(flags);
2552
+ if (!flags.all && !args.length) throw usage("oya rm needs a browser id, or --all: oya rm <id>... | oya rm --all.");
2553
+ const r = await oya.browser.stop(flags.all ? "all" : args);
2554
+ out(flags, r, () => printStops(r.results));
2555
+ const failed = r.results.filter((x) => !x.ok).length;
2556
+ if (failed) throw stopsFailed(r.results.length - failed, r.results.length, !!flags.json);
2291
2557
  }
2558
+ function printStops(results) {
2559
+ for (const x of results) (x.ok ? console.log : console.error)(rmLine(x));
2560
+ console.log(`stopped ${results.filter((x) => x.ok).length} of ${results.length}`);
2561
+ }
2562
+ var stopsFailed = (stopped, of, json) => new CliError(`stopped ${stopped} of ${of}`, "partial_failure", { shown: !json });
2292
2563
  function activityLine(a) {
2293
2564
  const error = a.error ? `, ${a.error}` : "";
2294
2565
  return ` ${a.ok ? "\xB7" : "\u2717"} ${a.action.padEnd(StatusColumns.ACTION)} ${a.summary}${error} ${a.ms}ms`;
@@ -2302,21 +2573,21 @@ function printStatus(s) {
2302
2573
  console.log(" recent:");
2303
2574
  for (const a of s.activity.slice(0, StatusColumns.RECENT)) console.log(activityLine(a));
2304
2575
  }
2305
- async function cmdStatus(flags2) {
2306
- const browser = await targetBrowser(client(flags2), flags2);
2576
+ async function cmdStatus(flags) {
2577
+ const browser = await targetBrowser(client(flags), flags);
2307
2578
  const s = await browser.status();
2308
- out(flags2, s, () => printStatus(s));
2579
+ out(flags, s, () => printStatus(s));
2309
2580
  }
2310
2581
  function opener(url) {
2311
2582
  if (process.platform === "darwin") return ["open", [url]];
2312
2583
  if (process.platform === "win32") return ["cmd", ["/c", "start", "", url]];
2313
2584
  return ["xdg-open", [url]];
2314
2585
  }
2315
- async function cmdOpen(flags2) {
2316
- const browser = await targetBrowser(client(flags2), flags2);
2317
- const [command2, args2] = opener(browser.liveViewUrl());
2318
- spawn3(command2, args2, { detached: true, stdio: "ignore" }).unref();
2319
- console.log(`Opening ${browser.id}`);
2586
+ async function cmdOpen(flags) {
2587
+ const browser = await targetBrowser(client(flags), flags);
2588
+ const [command, args] = opener(browser.liveViewUrl());
2589
+ spawn3(command, args, { detached: true, stdio: "ignore" }).unref();
2590
+ out(flags, { ok: true, id: browser.id }, () => console.log(`Opening the live view of ${browser.id}`));
2320
2591
  }
2321
2592
 
2322
2593
  // src/commands/personas.ts
@@ -2330,61 +2601,74 @@ var PLATFORM_ALIAS = {
2330
2601
  linux: "Linux x86_64",
2331
2602
  "linux x86_64": "Linux x86_64"
2332
2603
  };
2333
- function prefsFrom(flags2) {
2334
- const platformFlag = flagStr(flags2, "platform");
2604
+ function prefsFrom(flags) {
2605
+ const platformFlag = flagStr(flags, "platform");
2335
2606
  return {
2336
2607
  ...platformFlag ? { platform: PLATFORM_ALIAS[platformFlag.toLowerCase()] || platformFlag } : {},
2337
- ...flagStr(flags2, "tz") ? { timezone: flagStr(flags2, "tz") } : {},
2338
- ...flagStr(flags2, "locale") ? { locale: flagStr(flags2, "locale") } : {}
2608
+ ...flagStr(flags, "tz") ? { timezone: flagStr(flags, "tz") } : {},
2609
+ ...flagStr(flags, "locale") ? { locale: flagStr(flags, "locale") } : {}
2339
2610
  };
2340
2611
  }
2341
- function createOptions(flags2, rest, prefs) {
2612
+ function createOptions(flags, rest, prefs) {
2342
2613
  return {
2343
- name: flagStr(flags2, "name") || rest[0],
2614
+ name: flagStr(flags, "name") || rest[0],
2344
2615
  prefs,
2345
- proxy: flagStr(flags2, "geo") ? { geo: flagStr(flags2, "geo") } : void 0,
2346
- maxConcurrent: flagStr(flags2, "max") ? Number(flagStr(flags2, "max")) : void 0
2616
+ proxy: flagStr(flags, "geo") ? { geo: flagStr(flags, "geo") } : void 0,
2617
+ maxConcurrent: flagCount(flags, "max") ?? void 0
2347
2618
  };
2348
2619
  }
2349
- var create = async (oya, rest, flags2) => {
2350
- const prefs = prefsFrom(flags2);
2351
- if (flags2.preview) return preview(oya, prefs, flags2);
2352
- const created = await oya.personas.create(createOptions(flags2, rest, prefs));
2620
+ var create = async (oya, rest, flags) => {
2621
+ const prefs = prefsFrom(flags);
2622
+ if (flags.preview) return preview(oya, prefs, flags);
2623
+ const created = await oya.personas.create(createOptions(flags, rest, prefs));
2353
2624
  const fp = created.fingerprint;
2354
- out(flags2, created, () => console.log(`\u2705 ${created.id} ${created.name} ${fp.platform} \xB7 ${fp.timezone}`));
2625
+ out(flags, created, () => console.log(`\u2705 ${created.id} ${created.name} ${fp.platform} \xB7 ${fp.timezone}`));
2355
2626
  };
2356
- async function preview(oya, prefs, flags2) {
2627
+ async function preview(oya, prefs, flags) {
2357
2628
  const fp = await oya.personas.preview(prefs);
2358
- out(flags2, fp, () => console.log(`${fp.platform} \xB7 ${fp.timezone} \xB7 ${fp.locale} \xB7 ${fp.screen} \xB7 ${fp.webgl}`));
2629
+ out(flags, fp, () => console.log(`${fp.platform} \xB7 ${fp.timezone} \xB7 ${fp.locale} \xB7 ${fp.screen} \xB7 ${fp.webgl}`));
2359
2630
  }
2360
- function editChanges(flags2) {
2361
- const max = flagStr(flags2, "max");
2631
+ function editChanges(flags) {
2632
+ const max = flagCount(flags, "max", { allowNone: true });
2362
2633
  return {
2363
- ...flagStr(flags2, "name") ? { name: flagStr(flags2, "name") } : {},
2364
- ...max ? { maxConcurrent: max === "none" ? null : Number(max) } : {},
2365
- ...flagStr(flags2, "geo") ? { proxy: { geo: flagStr(flags2, "geo") } } : {}
2634
+ ...flagStr(flags, "name") ? { name: flagStr(flags, "name") } : {},
2635
+ ...max !== void 0 ? { maxConcurrent: max } : {},
2636
+ ...flagStr(flags, "geo") ? { proxy: { geo: flagStr(flags, "geo") } } : {}
2366
2637
  };
2367
2638
  }
2368
- var edit = async (oya, rest, flags2) => {
2369
- if (!rest[0]) throw new Error("Usage: oya personas edit <id> --name <n> --max <n> --geo <cc>");
2370
- const updated = await oya.personas.update(rest[0], editChanges(flags2));
2371
- out(flags2, updated, () => console.log(`\u2705 ${updated.id} ${updated.name} cap ${updated.maxConcurrent ?? "\u221E"}`));
2639
+ var edit = async (oya, rest, flags) => {
2640
+ if (!rest[0]) throw usage("oya personas edit needs a persona id: oya personas edit <id> --name <n>.");
2641
+ const updated = await oya.personas.update(rest[0], editChanges(flags));
2642
+ out(flags, updated, () => console.log(`\u2705 ${updated.id} ${updated.name} cap ${updated.maxConcurrent ?? "\u221E"}`));
2372
2643
  };
2373
- var clone = async (oya, rest, flags2) => {
2374
- if (!rest[0]) throw new Error("Usage: oya personas clone <id> [--name <n>]");
2375
- const c = await oya.personas.clone(rest[0], { name: flagStr(flags2, "name") });
2644
+ var clone = async (oya, rest, flags) => {
2645
+ if (!rest[0]) throw usage("oya personas clone needs a persona id: oya personas clone <id>.");
2646
+ const c = await oya.personas.clone(rest[0], { name: flagStr(flags, "name") });
2376
2647
  const fp = c.fingerprint;
2377
2648
  out(
2378
- flags2,
2649
+ flags,
2379
2650
  c,
2380
2651
  () => console.log(`\u2705 ${c.id} ${c.name} ${fp.platform} \xB7 ${fp.timezone} (new identity, same kind of device)`)
2381
2652
  );
2382
2653
  };
2383
- var remove = async (oya, rest) => {
2384
- if (!rest[0]) throw new Error("Usage: oya personas rm <id>");
2385
- await oya.personas.remove(rest[0]);
2386
- console.log(`\u2705 removed ${rest[0]}`);
2654
+ var remove = async (oya, rest, flags) => {
2655
+ if (!rest.length) throw usage("oya personas rm needs a persona id: oya personas rm <id>...");
2656
+ const removed = [];
2657
+ for (const id of rest) await removeOne(oya, id, flags, removed);
2658
+ out(flags, { ok: removed.length === rest.length, id: removed }, () => {
2659
+ });
2660
+ if (removed.length < rest.length) throw removalsFailed(removed.length, rest.length, !!flags.json);
2387
2661
  };
2662
+ async function removeOne(oya, id, flags, removed) {
2663
+ try {
2664
+ await oya.personas.remove(id);
2665
+ removed.push(id);
2666
+ if (!flags.json) console.log(`\u2705 removed ${id}`);
2667
+ } catch (err) {
2668
+ if (!flags.json) console.error(`\u2717 ${id} ${err.message}`);
2669
+ }
2670
+ }
2671
+ var removalsFailed = (removed, of, json) => new CliError(`removed ${removed} of ${of}`, "partial_failure", { shown: !json });
2388
2672
  function personaLine(p) {
2389
2673
  const cap = p.maxConcurrent === null ? "\u221E" : String(p.maxConcurrent);
2390
2674
  const where = p.exit ? ` via ${p.exit.label}` : p.proxy?.geo ? ` geo ${p.proxy.geo}` : "";
@@ -2392,104 +2676,117 @@ function personaLine(p) {
2392
2676
  const device = `${p.fingerprint.platform} \xB7 ${p.fingerprint.timezone}`;
2393
2677
  return `${p.id} ${p.name.padEnd(PERSONA_NAME_WIDTH)} ${p.activeBrowsers}/${cap} running ${device}${extras}`;
2394
2678
  }
2395
- var list = async (oya, _rest, flags2) => {
2679
+ var list = async (oya, _rest, flags) => {
2396
2680
  const all = await oya.personas.list();
2397
- out(flags2, all, () => {
2681
+ out(flags, all, () => {
2398
2682
  if (!all.length) return console.log("No personas yet, `oya personas new`.");
2399
2683
  for (const p of all) console.log(personaLine(p));
2400
2684
  });
2401
2685
  };
2402
2686
  var SUBCOMMANDS = { new: create, create, edit, clone, rm: remove, delete: remove };
2403
- async function cmdPersonas(args2, flags2) {
2404
- const oya = client(flags2);
2405
- const [sub, ...rest] = args2;
2406
- const run2 = sub !== void 0 && Object.hasOwn(SUBCOMMANDS, sub) ? SUBCOMMANDS[sub] : list;
2407
- await run2(oya, rest, flags2);
2687
+ async function cmdPersonas(args, flags) {
2688
+ const [sub, ...rest] = args;
2689
+ if (sub !== void 0 && !Object.hasOwn(SUBCOMMANDS, sub))
2690
+ throw usage(`Unknown personas subcommand "${sub}". Use new, edit, clone or rm.`);
2691
+ await (sub === void 0 ? list : SUBCOMMANDS[sub])(client(flags), rest, flags);
2408
2692
  }
2409
2693
 
2410
2694
  // src/commands/cookies.ts
2411
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2412
- var USAGE = `Usage: oya cookies export <persona> [--format json|playwright] [--out <file>]
2413
- oya cookies import <persona> <file.json>
2414
- oya cookies copy <from-persona> <to-persona>
2415
- oya cookies clear <persona>`;
2416
- var exportJar = async (oya, rest, flags2) => {
2417
- if (!rest[0]) throw new Error("Usage: oya cookies export <persona> [--format json|playwright] [--out <file>]");
2418
- const cookies = await oya.personas.cookies(rest[0], flagStr(flags2, "format") || "json");
2419
- const file = flagStr(flags2, "out");
2420
- if (!file) return console.log(JSON.stringify(cookies, null, JSON_INDENT2));
2695
+ import { chmodSync as chmodSync3, writeFileSync as writeFileSync5 } from "fs";
2696
+ var FORMATS = ["json", "playwright"];
2697
+ function formatOf(flags) {
2698
+ const format = flagStr(flags, "format") || "json";
2699
+ if (FORMATS.includes(format)) return format;
2700
+ throw usage(`Unknown format "${format}". Use --format json or --format playwright.`);
2701
+ }
2702
+ function writeJar(file, cookies) {
2421
2703
  writeFileSync5(file, JSON.stringify(cookies, null, JSON_INDENT2) + "\n", { mode: PRIVATE_FILE_MODE });
2422
- console.log(`\u2705 ${cookies.length} cookie${cookies.length === 1 ? "" : "s"} written to ${file}`);
2704
+ chmodSync3(file, PRIVATE_FILE_MODE);
2705
+ }
2706
+ var exportJar = async (oya, rest, flags) => {
2707
+ if (!rest[0]) throw usage("oya cookies export needs a persona id: oya cookies export <persona> [--out <file>].");
2708
+ const cookies = await oya.personas.cookies(rest[0], formatOf(flags));
2709
+ if (!Array.isArray(cookies)) throw new Error("The server sent no cookie list, so nothing was written.");
2710
+ const file = flagStr(flags, "out");
2711
+ if (!file) return console.log(JSON.stringify(cookies, null, JSON_INDENT2));
2712
+ writeJar(file, cookies);
2713
+ const written = `\u2705 ${cookies.length} cookie${cookies.length === 1 ? "" : "s"} written to ${file}`;
2714
+ out(flags, { ok: true, id: rest[0], file, count: cookies.length }, () => console.log(written));
2423
2715
  };
2424
2716
  function cookiesIn(file) {
2425
- const parsed = JSON.parse(readFileSync4(file, "utf8"));
2717
+ const parsed = readJsonFile(file);
2426
2718
  const list2 = Array.isArray(parsed) ? parsed : parsed?.cookies;
2427
- if (!Array.isArray(list2)) throw new Error(`${file} holds no list of cookies`);
2719
+ if (!Array.isArray(list2)) throw usage(`${file} holds no list of cookies.`);
2428
2720
  return list2;
2429
2721
  }
2430
- var importJar = async (oya, rest, flags2) => {
2431
- if (!rest[0] || !rest[1]) throw new Error("Usage: oya cookies import <persona> <file.json>");
2722
+ var importJar = async (oya, rest, flags) => {
2723
+ if (!rest[0] || !rest[1])
2724
+ throw usage("oya cookies import needs a persona and a file: oya cookies import <persona> <file>.");
2432
2725
  const done = await oya.personas.importCookies(rest[0], cookiesIn(rest[1]));
2433
2726
  out(
2434
- flags2,
2727
+ flags,
2435
2728
  done,
2436
2729
  () => console.log(`\u2705 ${done.imported} imported, ${done.skipped} skipped, ${done.total} in the jar`)
2437
2730
  );
2438
2731
  };
2439
- var copy = async (oya, rest, flags2) => {
2440
- if (!rest[0] || !rest[1]) throw new Error("Usage: oya cookies copy <from-persona> <to-persona>");
2732
+ var copy = async (oya, rest, flags) => {
2733
+ if (!rest[0] || !rest[1]) throw usage("oya cookies copy needs two persona ids: oya cookies copy <from> <to>.");
2441
2734
  const done = await oya.personas.copyCookies(rest[0], rest[1]);
2442
- out(flags2, done, () => console.log(`\u2705 ${done.imported} copied to ${rest[1]}, ${done.total} in its jar`));
2735
+ out(flags, done, () => console.log(`\u2705 ${done.imported} copied to ${rest[1]}, ${done.total} in its jar`));
2443
2736
  };
2444
- var clear = async (oya, rest) => {
2445
- if (!rest[0]) throw new Error("Usage: oya cookies clear <persona>");
2446
- await oya.personas.clearCookies(rest[0]);
2447
- console.log(`\u2705 cleared ${rest[0]}`);
2737
+ var clear = async (oya, rest, flags) => {
2738
+ if (!rest[0]) throw usage("oya cookies clear needs a persona id: oya cookies clear <persona>.");
2739
+ const done = await oya.personas.clearCookies(rest[0]);
2740
+ out(flags, done ?? { ok: true, id: rest[0] }, () => console.log(`\u2705 cleared ${rest[0]}`));
2448
2741
  };
2449
2742
  var SUBCOMMANDS2 = { export: exportJar, import: importJar, copy, clear };
2450
- async function cmdCookies(args2, flags2) {
2451
- const [sub, ...rest] = args2;
2452
- if (sub === void 0 || !Object.hasOwn(SUBCOMMANDS2, sub)) throw new Error(USAGE);
2453
- await SUBCOMMANDS2[sub](client(flags2), rest, flags2);
2743
+ async function cmdCookies(args, flags) {
2744
+ const [sub, ...rest] = args;
2745
+ if (sub === void 0 || !Object.hasOwn(SUBCOMMANDS2, sub))
2746
+ throw usage(`Unknown cookies subcommand "${sub ?? ""}". Use export, import, copy or clear.`);
2747
+ await SUBCOMMANDS2[sub](client(flags), rest, flags);
2454
2748
  }
2455
2749
 
2456
2750
  // src/commands/settings.ts
2457
- function parsePairs(args2) {
2751
+ function parsePairs(args) {
2458
2752
  const updates = {};
2459
- for (const pair of args2) {
2753
+ for (const pair of args) {
2460
2754
  const index = pair.indexOf("=");
2461
- if (index < 1) throw new Error(`Expected key=value, got "${pair}"`);
2755
+ if (index < 1) throw usage(`oya config takes settings as key=value, not "${pair}".`);
2462
2756
  updates[pair.slice(0, index)] = pair.slice(index + 1);
2463
2757
  }
2464
2758
  return updates;
2465
2759
  }
2466
- async function cmdConfig(args2, flags2) {
2467
- const oya = client(flags2);
2468
- if (!args2.length) {
2760
+ async function cmdConfig(args, flags) {
2761
+ const oya = client(flags);
2762
+ if (!args.length) {
2469
2763
  const current = await oya.config.get();
2470
- return out(flags2, current, () => printJson(current));
2764
+ return out(flags, current, () => printJson(current));
2471
2765
  }
2472
- const updates = parsePairs(args2);
2473
- await oya.config.set(updates);
2474
- console.log(`\u2705 updated ${Object.keys(updates).join(", ")}`);
2766
+ const updates = parsePairs(args);
2767
+ const saved = await oya.config.set(updates);
2768
+ out(flags, saved, () => console.log(`\u2705 updated ${Object.keys(updates).join(", ")}`));
2475
2769
  }
2476
- async function cmdUsage(flags2) {
2477
- printJson(await client(flags2).usage());
2770
+ async function cmdUsage(flags) {
2771
+ printJson(await client(flags).usage());
2478
2772
  }
2479
- async function cmdWhoami() {
2480
- printJson({ ...resolved(), apiKey: load().apiKey ? "saved" : "none" });
2773
+ async function cmdWhoami(flags) {
2774
+ const { apiKey, apiKeyFrom, baseUrl, baseUrlFrom, savedKey } = origin2(flags);
2775
+ const tail = apiKey.length >= MIN_KEY_FOR_TAIL ? { apiKeyEnds: apiKey.slice(-KEY_TAIL) } : {};
2776
+ const ignored = savedKey && apiKeyFrom !== "saved" ? { savedKeyIgnored: true } : {};
2777
+ printJson({ baseUrl, baseUrlFrom, apiKeyFrom, ...tail, ...ignored, configPath });
2481
2778
  }
2482
2779
 
2483
2780
  // src/commands/stealth.ts
2484
2781
  import { spawn as spawn4 } from "child_process";
2485
2782
  var HARNESS = "tests/integration/stealth.test.js";
2486
- var spawnHarness = (flags2) => spawn4("node", [HARNESS, ...flags2.live ? ["--live"] : []], {
2783
+ var spawnHarness = (flags) => spawn4("node", [HARNESS, ...flags.live ? ["--live"] : []], {
2487
2784
  cwd: process.env.OYA_SERVER_DIR || "server",
2488
2785
  stdio: "inherit"
2489
2786
  });
2490
- function cmdStealthTest(flags2) {
2787
+ function cmdStealthTest(flags) {
2491
2788
  return new Promise((resolve, reject) => {
2492
- const child = spawnHarness(flags2);
2789
+ const child = spawnHarness(flags);
2493
2790
  const missing = "Could not find the stealth harness. Run it from a checkout, or set OYA_SERVER_DIR.";
2494
2791
  child.on("error", () => reject(new Error(missing)));
2495
2792
  child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`stealth test exited ${code}`)));
@@ -2498,167 +2795,301 @@ function cmdStealthTest(flags2) {
2498
2795
 
2499
2796
  // src/commands/control.ts
2500
2797
  var ROLES = ["viewer", "operator", "administrator"];
2501
- function required(args2, i = 0) {
2502
- if (!args2[i]) throw new Error("Missing argument; run oya help");
2503
- return args2[i];
2798
+ function required(args, i, command, what) {
2799
+ if (!args[i]) throw usage(`oya ${command} needs ${what}: oya ${command} <${what.split(" ").pop()}>.`);
2800
+ return args[i];
2504
2801
  }
2505
- function roleFlag(flags2) {
2506
- const role = flagStr(flags2, "role") || "operator";
2507
- if (!ROLES.includes(role)) throw new Error("Invalid role");
2802
+ function roleFlag(flags) {
2803
+ const role = flagStr(flags, "role") || "operator";
2804
+ if (!ROLES.includes(role)) throw usage(`--role must be viewer, operator or administrator, not "${role}".`);
2508
2805
  return role;
2509
2806
  }
2510
- function subcommand(table, sub, call2, usage) {
2511
- if (!Object.hasOwn(table, sub)) throw new Error(usage);
2807
+ function subcommand(table, sub, call2, command) {
2808
+ if (!Object.hasOwn(table, sub))
2809
+ throw usage(`Unknown ${command} subcommand "${sub}". Use ${orList(Object.keys(table))}.`);
2512
2810
  return table[sub](call2);
2513
2811
  }
2812
+ var orList = (names) => names.join(", ").replace(/, ([^,]*)$/, " or $1");
2514
2813
  var members = (call2) => {
2515
2814
  if (!call2.args[0]) return call2.c.members();
2516
2815
  const table = {
2517
- remove: ({ c, args: args2 }) => c.removeMember(required(args2, 1)),
2518
- invite: ({ c, flags: flags2 }) => c.inviteMember(roleFlag(flags2))
2816
+ remove: ({ c, args }) => c.removeMember(required(args, 1, "members remove", "a user id")),
2817
+ invite: ({ c, flags }) => c.inviteMember(roleFlag(flags))
2519
2818
  };
2520
- return subcommand(table, call2.args[0], call2, "Use members, members invite, or members remove");
2819
+ return subcommand(table, call2.args[0], call2, "members");
2521
2820
  };
2522
2821
  var credential = (call2) => {
2523
2822
  const table = {
2524
- revoke: ({ c, args: args2 }) => c.revokeCredential(required(args2, 1)),
2525
- new: ({ c, flags: flags2 }) => c.createCredential({ role: roleFlag(flags2), label: flagStr(flags2, "label") })
2823
+ revoke: ({ c, args }) => c.revokeCredential(required(args, 1, "credential revoke", "a credential id")),
2824
+ new: ({ c, flags }) => c.createCredential({ role: roleFlag(flags), label: flagStr(flags, "label") })
2526
2825
  };
2527
- return subcommand(table, required(call2.args), call2, "Use credential new or credential revoke");
2826
+ return subcommand(table, required(call2.args, 0, "credential", "new or revoke"), call2, "credential");
2528
2827
  };
2529
2828
  var webhook = (call2) => {
2530
2829
  const table = {
2531
- new: ({ c, args: args2 }) => c.createWebhook(required(args2, 1)),
2532
- remove: ({ c, args: args2 }) => c.removeWebhook(required(args2, 1)),
2533
- replay: ({ c, args: args2 }) => c.replayDelivery(required(args2, 1))
2830
+ new: ({ c, args }) => c.createWebhook(required(args, 1, "webhook new", "an https url")),
2831
+ remove: ({ c, args }) => c.removeWebhook(required(args, 1, "webhook remove", "a webhook id")),
2832
+ replay: ({ c, args }) => c.replayDelivery(required(args, 1, "webhook replay", "a delivery id"))
2534
2833
  };
2535
- return subcommand(table, required(call2.args), call2, "Use webhook new, remove, or replay");
2834
+ return subcommand(table, required(call2.args, 0, "webhook", "new, remove or replay"), call2, "webhook");
2536
2835
  };
2537
- var takeover = ({ c, command: command2, args: args2 }) => c.takeover(required(args2), command2 === "takeover" ? "acquire" : command2);
2538
- var COMMANDS = {
2836
+ function recoverOptions(flags) {
2837
+ const wsUrl = flagStr(flags, "ws-url");
2838
+ if (wsUrl && flags.replace !== true)
2839
+ throw usage("--ws-url is only used with --replace; recovering in place keeps the endpoint it had.");
2840
+ return { replace: flags.replace === true, ...wsUrl ? { wsUrl } : {} };
2841
+ }
2842
+ var projectSettings = (text) => parseJson(
2843
+ text,
2844
+ "The settings for oya project",
2845
+ `Quote them: oya project '{"retentionDays":7}'.`,
2846
+ "are"
2847
+ );
2848
+ var takeover = ({ c, command, args }) => c.takeover(
2849
+ required(args, 0, command, "a session id"),
2850
+ command === "takeover" ? "acquire" : command
2851
+ );
2852
+ var CONTROL_COMMANDS = {
2539
2853
  control: ({ c }) => c.overview(),
2540
- sessions: ({ c, args: args2 }) => args2[0] ? c.session(args2[0]) : c.sessions(),
2541
- cancel: ({ c, args: args2 }) => c.cancel(required(args2)),
2542
- recover: ({ c, args: args2, flags: flags2 }) => c.recover(required(args2), flags2.replace === true),
2854
+ sessions: ({ c, args }) => args[0] ? c.session(args[0]) : c.sessions(),
2855
+ cancel: ({ c, args }) => c.cancel(required(args, 0, "cancel", "a session id")),
2856
+ recover: ({ c, args, flags }) => c.recover(required(args, 0, "recover", "a session id"), recoverOptions(flags)),
2543
2857
  members,
2544
- stop: ({ c, args: args2, flags: flags2 }) => c.stop(required(args2), flags2.force === true),
2858
+ stop: ({ c, args, flags }) => c.stop(required(args, 0, "stop", "a session id"), flags.force === true),
2545
2859
  takeover,
2546
2860
  release: takeover,
2547
2861
  resume: takeover,
2548
- events: ({ c, flags: flags2 }) => c.events(Number(flagStr(flags2, "after") || 0)),
2549
- project: ({ c, args: args2 }) => c.settings(JSON.parse(required(args2))),
2862
+ events: ({ c, flags }) => c.events(flagNum(flags, "after") ?? 0),
2863
+ project: ({ c, args }) => c.settings(projectSettings(required(args, 0, "project", "settings as JSON"))),
2550
2864
  credential,
2551
2865
  webhook
2552
2866
  };
2553
- function isControlCommand(command2, flags2) {
2554
- if (command2 === "stop") return flags2.force === true;
2555
- return Object.hasOwn(COMMANDS, command2);
2867
+ function isControlCommand(command, flags) {
2868
+ if (command === "stop") return flags.force === true;
2869
+ return Object.hasOwn(CONTROL_COMMANDS, command);
2556
2870
  }
2557
- async function runControl(command2, args2, flags2) {
2558
- const c = client(flags2).control;
2559
- printJson(await COMMANDS[command2]({ c, command: command2, args: args2, flags: flags2 }));
2871
+ async function runControl(command, args, flags) {
2872
+ const c = client(flags).control;
2873
+ printJson(await CONTROL_COMMANDS[command]({ c, command, args, flags }));
2560
2874
  }
2561
2875
 
2562
2876
  // src/help.ts
2563
2877
  var HELP = `oya, thousands of browsers, one API
2564
2878
 
2565
- oya install Stand up a self-hosted control plane
2566
- [--dry-run] [--config oya-install.json]
2567
- oya login Save an API key for this machine
2568
- oya init Set your model, browser provider and sign-ins
2569
- oya start [--persona auto] Start a browser and print its id
2570
- oya goto <url> [--id <id>] Navigate (defaults to the newest browser)
2571
- oya ask "<prompt>" [--id <id>] Drive it in plain language
2572
- oya ls List running browsers
2573
- oya rm <id> | --all Stop browsers
2574
- oya personas Identities: fingerprint + cookies + proxy
2575
- oya personas new [name] --platform Win32|MacIntel|Linux --tz <zone> --locale <l> --max <n>
2576
- oya personas edit <id> --name <n> --max <n> --geo <cc>
2577
- oya personas clone|rm <id> A new device of the same kind \xB7 delete
2578
- oya cookies export <persona> Logins to stdout or --out <file>; --format playwright for addCookies()
2579
- oya cookies import <persona> <file> Logins from a JSON file
2580
- oya cookies copy <from> <to> One persona's logins into another
2581
- oya status [--id <id>] Health, counters and what it has been doing
2582
- oya open [--id <id>] Open the live view in your browser
2583
- oya config [key=value ...] Show or change this key's settings
2584
- oya control Durable project overview
2585
- oya sessions [id] All sessions, including pending cleanup
2586
- oya stop <id> --force Stop despite a profile-save error, or reconcile
2587
- oya takeover <id> Acquire human control
2588
- oya release <id> Release human control, leaving the agent paused
2589
- oya resume <id> Acknowledge agent resume
2590
- oya events [--after <cursor>] Read durable lifecycle events
2591
- oya project <settings-json> Update limits, rate cards and retention
2592
- oya start --governed --provider oya-selfhosted [--queue-ms 30000]
2593
- [--budget-usd 5] [--policy JSON] [--idempotency-key ID]
2594
- oya cancel <id> Cancel queued or provisioning work
2595
- oya recover <id> [--replace] Explicitly recover or replace a session
2596
- oya members [invite|remove] List members, invite, or remove a user
2597
- oya credential new [--role viewer|operator|administrator]
2598
- oya credential revoke <id> Revoke a service credential
2599
- oya webhook new <https-url> Register a signed event webhook
2600
- oya webhook remove <id> Disable a webhook
2601
- oya webhook replay <id> Replay a delivery
2602
- oya usage What this key has spent
2603
- oya stealth-test [--live] Score this deployment against bot detectors
2879
+ Usage: oya <command> [arguments] [options]
2880
+
2881
+ Set up
2882
+ oya install [--dry-run] [--config <file>] Stand up a self-hosted control plane
2883
+ oya login [--key <key>] [--url <url>] Save an API key for this machine
2884
+ oya init Set your model, browser provider and sign-ins
2885
+ oya whoami Which control plane and key are in use, and from where
2604
2886
 
2605
- Options: --url <control plane> --key <api key> --json
2606
- Config: ${configPath}
2887
+ Browsers
2888
+ oya start Start a browser and print its id
2889
+ [--persona <id|auto>] [--name <name>] [--idempotency-key <key>]
2890
+ [--provider cdp --ws-url ws://127.0.0.1:9222] on a Chrome you run
2891
+ [--governed] [--queue-ms 30000] [--budget-usd 5] [--priority low|normal|high] [--policy '<json>']
2892
+ oya goto <url> [--id <id>] Navigate (defaults to the newest browser)
2893
+ oya ask "<prompt>" [--id <id>] Drive it in plain language
2894
+ oya ls | list List running browsers
2895
+ oya status [--id <id>] Health, counters and what it has been doing
2896
+ oya open [--id <id>] Open the live view in your browser
2897
+ oya rm | stop <id>... | --all Stop browsers
2898
+
2899
+ Personas and cookies
2900
+ oya personas List identities: fingerprint + cookies + proxy
2901
+ oya personas new | create [name] [--name <n>] [--platform Win32|MacIntel|Linux] [--tz <zone>]
2902
+ [--locale <l>] [--max <n>] [--geo <cc>] [--preview]
2903
+ oya personas edit <id> [--name <n>] [--max <n|none>] [--geo <cc>]
2904
+ oya personas clone <id> [--name <n>] A new device of the same kind
2905
+ oya personas rm | delete <id>... Delete personas
2906
+ oya cookies export <persona> Logins to stdout; [--out <file>] [--format json|playwright]
2907
+ oya cookies import <persona> <file> Logins from a JSON file
2908
+ oya cookies copy <from> <to> One persona's logins into another
2909
+ oya cookies clear <persona> Delete a persona's logins
2910
+
2911
+ Account
2912
+ oya config [key=value ...] Show or change this key's settings
2913
+ oya usage What this key has spent
2914
+ oya stealth-test [--live] Score this deployment against bot detectors
2915
+
2916
+ Control plane (always prints JSON)
2917
+ oya control Durable project overview
2918
+ oya sessions [<id>] All sessions, including pending cleanup
2919
+ oya cancel <id> Stop a session in any state; prints its final state
2920
+ oya stop <id> --force Stop despite a profile-save error, or reconcile
2921
+ oya recover <id> [--replace [--ws-url ws://host:9222]]
2922
+ Recover in place, or replace it (a cdp session needs --ws-url)
2923
+ oya takeover <id> Take human control
2924
+ oya release <id> Hand control back, leaving the agent paused
2925
+ oya resume <id> Let the agent carry on
2926
+ oya events [--after <cursor>] Read durable lifecycle events
2927
+ oya project '<settings-json>' Update limits, rate cards and retention
2928
+ oya members List the owner and members
2929
+ oya members invite [--role <role>] An invitation code; role viewer|operator|administrator
2930
+ oya members remove <user> Remove a member
2931
+ oya credential new [--role <role>] [--label <text>] Mint a service credential (shown once)
2932
+ oya credential revoke <id> Revoke a service credential
2933
+ oya webhook new <https-url> Register a signed event webhook
2934
+ oya webhook remove <id> Disable a webhook
2935
+ oya webhook replay <id> Send a delivery again
2936
+
2937
+ Options
2938
+ --json One JSON document on stdout; errors as JSON on stderr
2939
+ --url <url> Control plane for this call (else OYA_BASE_URL, else the one oya login saved)
2940
+ --key <key> API key for this call (else OYA_API_KEY, else the one oya login saved)
2941
+ --debug On failure, also print the status, the response body and the stack
2942
+ --version Print the version
2943
+ --help, -h This text; the command is not run
2944
+
2945
+ Exit codes: 0 done \xB7 1 failed, or no answer \xB7 2 usage error, nothing was sent
2946
+ Config: ${configPath}
2607
2947
  `;
2608
2948
 
2609
2949
  // src/commands/index.ts
2950
+ import { createRequire } from "module";
2610
2951
  var help = async () => console.log(HELP);
2611
- var COMMANDS2 = {
2612
- install: (_args, flags2) => cmdInstall(flags2),
2613
- login: (_args, flags2) => cmdLogin(flags2),
2614
- init: (_args, flags2) => cmdInit(flags2),
2615
- start: (_args, flags2) => cmdStart(flags2),
2952
+ function packageVersion() {
2953
+ const require2 = createRequire(import.meta.url);
2954
+ for (const path of ["../package.json", "../../package.json"]) {
2955
+ try {
2956
+ return require2(path).version;
2957
+ } catch {
2958
+ }
2959
+ }
2960
+ return "unknown";
2961
+ }
2962
+ var VERSION = packageVersion();
2963
+ var version = async (_args, flags) => out(flags, { version: VERSION }, () => console.log(`oya ${VERSION}`));
2964
+ var noJson = (command, run2, instead) => async (args, flags) => {
2965
+ if (flags.json) throw usage(`oya ${command} has no --json output. ${instead}`);
2966
+ await run2(args, flags);
2967
+ };
2968
+ var COMMANDS = {
2969
+ install: noJson("install", (_args, flags) => cmdInstall(flags), "For an unattended install, use --config <file>."),
2970
+ login: (_args, flags) => cmdLogin(flags),
2971
+ init: noJson("init", (_args, flags) => cmdInit(flags), "Use oya config key=value --json."),
2972
+ start: (_args, flags) => cmdStart(flags),
2616
2973
  goto: cmdGoto,
2617
2974
  ask: cmdAsk,
2618
- ls: (_args, flags2) => cmdLs(flags2),
2619
- list: (_args, flags2) => cmdLs(flags2),
2620
- status: (_args, flags2) => cmdStatus(flags2),
2975
+ ls: (_args, flags) => cmdLs(flags),
2976
+ list: (_args, flags) => cmdLs(flags),
2977
+ status: (_args, flags) => cmdStatus(flags),
2621
2978
  rm: cmdRm,
2622
2979
  stop: cmdRm,
2623
2980
  personas: cmdPersonas,
2624
2981
  cookies: cmdCookies,
2625
- open: (_args, flags2) => cmdOpen(flags2),
2982
+ open: (_args, flags) => cmdOpen(flags),
2626
2983
  config: cmdConfig,
2627
- usage: (_args, flags2) => cmdUsage(flags2),
2628
- "stealth-test": (_args, flags2) => cmdStealthTest(flags2),
2629
- whoami: () => cmdWhoami(),
2984
+ usage: (_args, flags) => cmdUsage(flags),
2985
+ "stealth-test": noJson("stealth-test", (_args, flags) => cmdStealthTest(flags), "Read its report from the terminal."),
2986
+ whoami: (_args, flags) => cmdWhoami(flags),
2987
+ version,
2630
2988
  help,
2631
2989
  "--help": help,
2632
2990
  "-h": help
2633
2991
  };
2634
- function unknown(command2) {
2635
- console.error(`Unknown command: ${command2}
2636
- `);
2637
- console.log(HELP);
2638
- process.exit(1);
2992
+ var unknown = (command) => usage(`Unknown command "${command}". Run oya help for the list.`);
2993
+ var wantsHelp = (args, flags) => Object.hasOwn(flags, "help") || args.includes("-h");
2994
+ async function dispatch(command, args, flags) {
2995
+ if (wantsHelp(args, flags)) return help(args, flags);
2996
+ if (flags.version || command === "--version") return version(args, flags);
2997
+ if (isControlCommand(command, flags)) return runControl(command, args, flags);
2998
+ if (!Object.hasOwn(COMMANDS, command)) throw unknown(command);
2999
+ await COMMANDS[command](args, flags);
3000
+ }
3001
+
3002
+ // src/failure.ts
3003
+ var USAGE_CODES = /* @__PURE__ */ new Set(["usage", "invalid_json"]);
3004
+ function fail2(err, argv2) {
3005
+ const failure2 = describe(err, origin(sourceFlags(argv2)));
3006
+ process.exitCode = USAGE_CODES.has(failure2.code) ? ExitCode.USAGE : ExitCode.FAILED;
3007
+ if (argv2.includes("--json")) printJson2(failure2, err, debugging(argv2));
3008
+ else printPlain(failure2, err, debugging(argv2));
3009
+ }
3010
+ var debugging = (argv2) => argv2.includes("--debug") || process.env.OYA_DEBUG === "1";
3011
+ function sourceFlags(argv2) {
3012
+ const valueOf2 = (name) => {
3013
+ const at = argv2.findIndex((a) => a === `--${name}` || a.startsWith(`--${name}=`));
3014
+ if (at < 0) return void 0;
3015
+ return argv2[at].includes("=") ? argv2[at].split("=").slice(1).join("=") : argv2[at + 1];
3016
+ };
3017
+ return { apiKey: valueOf2("key"), baseUrl: valueOf2("url") };
3018
+ }
3019
+ function describe(err, origin3) {
3020
+ if (err instanceof CliError)
3021
+ return { message: err.message, code: err.code, status: 0, hint: err.hint, shown: err.shown };
3022
+ const e = err;
3023
+ if (typeof e?.status !== "number")
3024
+ return { message: String(err?.message ?? err), code: "internal", status: 0 };
3025
+ return fromServer(e, origin3);
3026
+ }
3027
+ function fromServer(e, origin3) {
3028
+ const body = e.body ?? {};
3029
+ const code = typeof body.code === "string" ? body.code : e.status === 0 ? "unreachable" : "http_error";
3030
+ const base = { code, status: e.status, browserId: typeof body.browserId === "string" ? body.browserId : void 0 };
3031
+ if (e.status === 0 && code === "unreachable") return { ...base, ...unreachable2(origin3, body.cause) };
3032
+ if (isInvalidKey(e, body)) return { ...base, ...invalidKey2(origin3) };
3033
+ return { ...base, message: renamed(e.message, body, origin3), hint: hintFor(e.status, code, base.browserId) };
3034
+ }
3035
+ function renamed(message, body, origin3) {
3036
+ if (body.field !== "baseUrl" || !message.startsWith("baseUrl ")) return message;
3037
+ const source = origin3.baseUrlFrom === "saved" ? "The URL saved by oya login" : origin3.baseUrlFrom;
3038
+ return `${source}${message.slice("baseUrl".length)}`;
3039
+ }
3040
+ var isInvalidKey = (e, body) => e.status === Status2.UNAUTHORIZED || e.status === Status2.FORBIDDEN && body.error === "Invalid API key";
3041
+ var CAUSES = { ECONNREFUSED: "nothing is listening there", ENOTFOUND: "no such host" };
3042
+ var URL_HINTS = {
3043
+ "--url": "The URL came from --url. Start the server there, or pass the right --url.",
3044
+ OYA_BASE_URL: "The URL came from OYA_BASE_URL. Start the server there, or export the right URL.",
3045
+ saved: "The URL was saved by oya login. Start the server there, or run oya login --url <url>.",
3046
+ default: "That is the hosted default. Check your connection, or pass --url for a self-hosted server."
3047
+ };
3048
+ function unreachable2(origin3, cause) {
3049
+ const why = typeof cause === "string" ? Object.hasOwn(CAUSES, cause) ? CAUSES[cause] : cause : "no answer";
3050
+ return { message: `Could not reach ${origin3.baseUrl}: ${why}.`, hint: URL_HINTS[origin3.baseUrlFrom] };
3051
+ }
3052
+ var KEY_NAMES = {
3053
+ "--key": "--key",
3054
+ OYA_API_KEY: "OYA_API_KEY",
3055
+ saved: "oya login",
3056
+ none: "nowhere"
3057
+ };
3058
+ function invalidKey2(origin3) {
3059
+ const message = `Invalid API key for ${origin3.baseUrl} (the key came from ${KEY_NAMES[origin3.apiKeyFrom]}).`;
3060
+ const overrides = origin3.apiKeyFrom === "OYA_API_KEY" && origin3.savedKey;
3061
+ const hint2 = overrides ? "OYA_API_KEY overrides the key saved by oya login. Unset it, or export the right key." : "Run oya login to save a new key.";
3062
+ return { message, hint: hint2 };
3063
+ }
3064
+ function hintFor(status, code, holder) {
3065
+ if (status === Status2.TOO_MANY_REQUESTS)
3066
+ return "A quota or a persona concurrency cap. oya personas shows what is running.";
3067
+ if (holder) return `oya stop ${holder} frees it; oya goto <url> --id ${holder} drives it.`;
3068
+ if (code === "replacement_endpoint_required")
3069
+ return "Add --ws-url ws://host:9222, the Chrome the replacement runs on.";
3070
+ return void 0;
2639
3071
  }
2640
- async function dispatch(command2, args2, flags2) {
2641
- if (isControlCommand(command2, flags2)) return runControl(command2, args2, flags2);
2642
- if (!Object.hasOwn(COMMANDS2, command2)) unknown(command2);
2643
- await COMMANDS2[command2](args2, flags2);
3072
+ function printPlain(failure2, err, debug) {
3073
+ if (!failure2.shown) console.error(`\u2717 ${failure2.message}`);
3074
+ if (failure2.hint && !failure2.shown) console.error(` ${failure2.hint}`);
3075
+ if (!debug) return;
3076
+ console.error(` status ${failure2.status} \xB7 code ${failure2.code}`);
3077
+ if (err?.body !== void 0) console.error(JSON.stringify(err.body));
3078
+ if (err?.stack) console.error(err.stack);
3079
+ }
3080
+ function printJson2(failure2, err, debug) {
3081
+ const { message, code, status, browserId } = failure2;
3082
+ const stack = debug ? { stack: err?.stack } : {};
3083
+ console.error(JSON.stringify({ error: message, code, status, ...browserId ? { browserId } : {}, ...stack }));
2644
3084
  }
2645
3085
 
2646
3086
  // src/index.ts
2647
- function fail2(err) {
2648
- const error = err;
2649
- console.error(`
2650
- \u2717 ${error.message}`);
2651
- if (error.status === Status2.UNAUTHORIZED) console.error(" The API key was rejected. Run `oya login`.");
2652
- if (error.status === Status2.TOO_MANY_REQUESTS) {
2653
- console.error(" A quota or a persona concurrency cap. `oya personas` shows what is running.");
2654
- }
2655
- process.exit(1);
2656
- }
2657
- var { command, args, flags } = parse(process.argv.slice(ARGV_SKIP));
3087
+ var argv = process.argv.slice(ARGV_SKIP);
2658
3088
  try {
3089
+ const { command, args, flags } = parse(argv);
2659
3090
  await dispatch(command, args, flags);
2660
3091
  } catch (err) {
2661
- fail2(err);
3092
+ fail2(err, argv);
2662
3093
  } finally {
2663
3094
  closePrompts();
2664
3095
  }