@oya-ai/cli 1.0.107 → 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 +42 -15
  2. package/dist/index.js +928 -429
  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,9 +1381,21 @@ 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;
1398
+ var PRIVATE_FILE_MODE = 384;
1273
1399
  var LsColumns = {
1274
1400
  /** Provider name. */
1275
1401
  PROVIDER: 14,
@@ -1290,6 +1416,10 @@ var PERSONA_NAME_WIDTH = 20;
1290
1416
  var Status2 = {
1291
1417
  /** The API key was rejected. */
1292
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,
1293
1423
  /** A quota or a persona concurrency cap. */
1294
1424
  TOO_MANY_REQUESTS: 429
1295
1425
  };
@@ -1310,65 +1440,65 @@ function save(values) {
1310
1440
  chmodSync2(FILE, CONFIG_FILE_MODE);
1311
1441
  }
1312
1442
  var configPath = FILE;
1313
- function resolved() {
1314
- const saved = load();
1315
- return {
1316
- apiKey: process.env.OYA_API_KEY || saved.apiKey || "",
1317
- baseUrl: (process.env.OYA_BASE_URL || saved.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "")
1318
- };
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;
1319
1447
  }
1320
-
1321
- // src/commands/login.ts
1322
- async function postJson(url, body, token2) {
1323
- const headers = { ...token2 ? { Authorization: `Bearer ${token2}` } : {}, "Content-Type": "application/json" };
1324
- const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
1325
- return { ok: res.ok, body: await res.json() };
1326
- }
1327
- async function mintKey(baseUrl) {
1328
- const email = await ask("Email:");
1329
- const password = await askSecret("Password:");
1330
- const session2 = await postJson(`${baseUrl}/api/auth/login`, { email, password });
1331
- if (!session2.ok || !session2.body.access_token) throw new Error(session2.body.error || "Sign-in failed");
1332
- const minted = await postJson(`${baseUrl}/api/auth/keys`, { label: "CLI" }, session2.body.access_token);
1333
- if (!minted.ok || !minted.body.key) throw new Error(minted.body.error || "Could not create an API key");
1334
- console.log(' Created a new API key labelled "CLI".');
1335
- return minted.body.key;
1336
- }
1337
- function chooseMethod() {
1338
- return choose("How do you want to authenticate?", [
1339
- { id: "paste", label: "Paste an API key", note: "from the dashboard, or a self-hosted key" },
1340
- { id: "password", label: "Sign in with email and password", note: "mints a new key for this machine" }
1341
- ]);
1342
- }
1343
- async function obtainKey(flags2, baseUrl) {
1344
- const given = flagStr(flags2, "key") || "";
1345
- if (given) return given;
1346
- const how = await chooseMethod();
1347
- if (how === "paste") return askSecret("API key:");
1348
- return mintKey(baseUrl);
1349
- }
1350
- async function verifyKey(baseUrl, apiKey) {
1351
- const check2 = await fetch(`${baseUrl}/api/config`, { headers: { Authorization: `Bearer ${apiKey}` } });
1352
- if (!check2.ok) throw new Error(`That key was rejected by ${baseUrl} (${check2.status})`);
1353
- }
1354
- async function loginUrl(flags2) {
1355
- const current = resolved().baseUrl;
1356
- const url = flagStr(flags2, "url") || (flagStr(flags2, "key") ? current : await ask("Control plane URL:", current));
1357
- return url.replace(/\/+$/, "");
1358
- }
1359
- async function cmdLogin(flags2) {
1360
- const baseUrl = await loginUrl(flags2);
1361
- const apiKey = await obtainKey(flags2, baseUrl);
1362
- if (!apiKey) throw new Error("No API key given");
1363
- await verifyKey(baseUrl, apiKey);
1364
- save({ apiKey, baseUrl });
1365
- console.log(`
1366
- \u2705 Signed in to ${baseUrl}. Saved to ${configPath}`);
1367
- console.log(" Scripts using @oya-ai/browser read this file too, so `new Oya()` needs no environment.");
1368
- 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 };
1369
1461
  }
1370
1462
 
1371
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
+ };
1372
1502
  var OyaError = class extends Error {
1373
1503
  /** The HTTP status, or the SDK's own status for errors it raises itself. */
1374
1504
  status;
@@ -1382,6 +1512,10 @@ var OyaError = class extends Error {
1382
1512
  this.body = body;
1383
1513
  }
1384
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
+ }
1385
1519
  var node = () => globalThis.process;
1386
1520
  function builtin(name) {
1387
1521
  const get = node()?.getBuiltinModule;
@@ -1402,54 +1536,25 @@ function savedConfig() {
1402
1536
  return {};
1403
1537
  }
1404
1538
  }
1405
- var DEFAULT_BASE_URL2 = "https://oyabrowser.com";
1406
- var DEFAULT_TIMEOUT_MS = 6e4;
1407
- var START_TIMEOUT_MS = 12e4;
1408
- var READY_TIMEOUT_MS2 = 12e4;
1409
- var READY_POLL_MS2 = 2e3;
1410
- var NAVIGATE_TIMEOUT_MS = 12e4;
1411
- var CHALLENGE_TIMEOUT_MS = 18e4;
1412
- var AGENT_TIMEOUT_MS = 6e5;
1413
- var PLAYBOOK_TIMEOUT_MS = 12e4;
1414
- var STOP_TIMEOUT_MS = 6e4;
1415
- var WAIT_FOR_DEFAULT_MS = 3e4;
1416
- var WAIT_FOR_GRACE_MS = 5e3;
1417
- var AIMED_SCROLL_AMOUNT = 500;
1418
- var RUN_POLL_MS = 2e3;
1419
- var MAX_RUN_POLL_ERRORS = 5;
1420
- var BYTES_PER_MB = 1048576;
1421
- var MAX_FILE_MB = 10;
1422
- var MS_PER_SECOND = 1e3;
1423
- var Status3 = {
1424
- /** The caller passed something unusable, such as a non-numeric element id. */
1425
- BAD_REQUEST: 400,
1426
- /** The server does not know that session. */
1427
- NOT_FOUND: 404,
1428
- /** The browser is in a state that needs attention first. */
1429
- CONFLICT: 409,
1430
- /** A browser command ran and failed. */
1431
- UNPROCESSABLE: 422,
1432
- /** A run failed without saying why. */
1433
- SERVER_ERROR: 500,
1434
- /** A browser did not come up in time. */
1435
- GATEWAY_TIMEOUT: 504
1436
- };
1539
+ var OPTIONS_ORIGIN = { apiKeyFrom: "the apiKey option", baseUrlFrom: "the baseUrl option", savedKey: false };
1437
1540
  var Http = class {
1438
- /** Stores where to call, with which key, how long to wait and which fetch to use. */
1439
- 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) {
1440
1543
  this.baseUrl = baseUrl;
1441
1544
  this.apiKey = apiKey;
1442
1545
  this.timeoutMs = timeoutMs;
1443
1546
  this.fetchImpl = fetchImpl;
1547
+ this.origin = origin3;
1444
1548
  }
1445
1549
  baseUrl;
1446
1550
  apiKey;
1447
1551
  timeoutMs;
1448
1552
  fetchImpl;
1553
+ origin;
1449
1554
  /** Sends one request and returns the parsed answer, or throws an OyaError when it failed. */
1450
1555
  async request(method, path, body, timeoutMs = this.timeoutMs, headers = {}) {
1451
1556
  const res = await this.send(`${this.baseUrl}${path}`, this.init(method, body, timeoutMs, headers), timeoutMs);
1452
- return readAnswer2(res, `${method} ${path}`, this.baseUrl);
1557
+ return readAnswer2(res, `${method} ${path}`, this);
1453
1558
  }
1454
1559
  /** The fetch options for one call, with this client's key on them. */
1455
1560
  init(method, body, timeoutMs, headers) {
@@ -1464,23 +1569,34 @@ var Http = class {
1464
1569
  try {
1465
1570
  return await this.fetchImpl(url, init);
1466
1571
  } catch (err) {
1467
- throw new OyaError(unreachable(this.baseUrl, timeoutMs, err), 0, { error: String(err?.message) });
1572
+ throw noAnswer(this, timeoutMs, err);
1468
1573
  }
1469
1574
  }
1470
1575
  };
1471
- function unreachable(baseUrl, timeoutMs, err) {
1472
- const cause = err?.name === "TimeoutError" || /abort/i.test(String(err?.message));
1473
- if (cause) return `No answer from ${baseUrl} within ${timeoutMs}ms. Is it reachable, and is the call this slow?`;
1474
- 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?`;
1475
1591
  }
1476
1592
  function requestInit(method, body, timeoutMs, headers) {
1477
1593
  const json = body === void 0 ? {} : { "Content-Type": "application/json" };
1478
1594
  const payload = body === void 0 ? void 0 : JSON.stringify(body);
1479
1595
  return { method, headers: { ...headers, ...json }, body: payload, signal: AbortSignal.timeout(timeoutMs) };
1480
1596
  }
1481
- async function readAnswer2(res, call2, baseUrl) {
1597
+ async function readAnswer2(res, call2, http) {
1482
1598
  const payload = parseBody(await res.text());
1483
- if (!res.ok) throw failure(call2, res.status, payload, baseUrl);
1599
+ if (!res.ok) throw failure(call2, res.status, payload, http);
1484
1600
  return payload;
1485
1601
  }
1486
1602
  function parseBody(text) {
@@ -1490,22 +1606,74 @@ function parseBody(text) {
1490
1606
  return text;
1491
1607
  }
1492
1608
  }
1493
- function failure(call2, status, payload, baseUrl) {
1494
- let message = payload?.error || `${call2} failed (${status})`;
1495
- if (message === "Invalid API key") {
1496
- message += ` for ${baseUrl}. Check OYA_API_KEY: a value exported in your shell beats .env.`;
1497
- }
1498
- 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}.`;
1499
1617
  }
1500
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
+ }
1501
1633
  function createHttp(options) {
1502
1634
  const saved = savedConfig();
1503
- const apiKey = options.apiKey || env("OYA_API_KEY") || saved.apiKey;
1504
- if (!apiKey) throw new Error("No API key. Pass { apiKey }, set OYA_API_KEY, or run `npx @oya-ai/cli login`.");
1505
- 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);
1506
1637
  const fetchImpl = options.fetch || globalThis.fetch;
1507
1638
  if (!fetchImpl) throw new Error("No fetch available, pass { fetch } or use Node 18+.");
1508
- 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);
1509
1677
  }
1510
1678
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1511
1679
  async function call(fn) {
@@ -1738,7 +1906,7 @@ var Browser = class {
1738
1906
  * Play `'<name>:draft'` to try a draft before promoting it.
1739
1907
  */
1740
1908
  async play(name, data = {}, { autoHeal = true } = {}) {
1741
- const path = `/api/browsers/${this.id}/playbooks/${encodeURIComponent(name)}/play`;
1909
+ const path = `/api/browsers/${this.id}/playbooks/${segment(name, "name")}/play`;
1742
1910
  return agentAnswer(
1743
1911
  await this.http.request("POST", path, { variables: data, autoHeal }, AGENT_TIMEOUT_MS)
1744
1912
  );
@@ -1789,7 +1957,7 @@ var Browser = class {
1789
1957
  }
1790
1958
  /** Revoke a link from `shareUrl()` before it expires, by the id it returned. */
1791
1959
  async revokeShare(id) {
1792
- await this.http.request("DELETE", `/api/control/credentials/${encodeURIComponent(id)}`);
1960
+ await this.http.request("DELETE", `/api/control/credentials/${segment(id)}`);
1793
1961
  }
1794
1962
  /** Counters, health and the last 50 things this browser did. */
1795
1963
  status() {
@@ -1843,8 +2011,9 @@ function startBody(options) {
1843
2011
  const { profile, persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy } = options;
1844
2012
  return { profile: profile || persona, provider, wsUrl, name, queueMs, priority, budgetUsd, governed, policy };
1845
2013
  }
2014
+ var idempotencyKeyOf = ({ idempotencyKey }) => idempotencyKey ? headerSafe(idempotencyKey, "idempotencyKey", 'Use letters, digits, "-" and "_".') : globalThis.crypto.randomUUID();
1846
2015
  async function start(http, wait, options) {
1847
- const headers = { "Idempotency-Key": options.idempotencyKey || globalThis.crypto.randomUUID() };
2016
+ const headers = { "Idempotency-Key": idempotencyKeyOf(options) };
1848
2017
  const path = "/api/browsers/start";
1849
2018
  const started = await http().request("POST", path, startBody(options), START_TIMEOUT_MS, headers);
1850
2019
  if (started.status === "starting") {
@@ -1853,7 +2022,7 @@ async function start(http, wait, options) {
1853
2022
  }
1854
2023
  return new Browser(http(), started, options.captcha === "auto");
1855
2024
  }
1856
- var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${encodeURIComponent(id)}`);
2025
+ var fetchBrowser = (http, id) => http().request("GET", `/api/browsers/${segment(id)}`);
1857
2026
  async function reattach(http, id) {
1858
2027
  const found = await fetchBrowser(http, id);
1859
2028
  return new Browser(http(), asStarted(found), false);
@@ -1865,67 +2034,77 @@ function asStarted(found) {
1865
2034
  var stopBrowsers = (http, ids) => http().request("POST", "/api/browsers/stop", ids === "all" ? { all: true } : { ids }, START_TIMEOUT_MS);
1866
2035
  var browserApi = (http, wait) => ({
1867
2036
  /** Start a browser and wait until it can take commands. */
1868
- start: (options = {}) => start(http, wait, options),
2037
+ start: async (options = {}) => start(http, wait, options),
1869
2038
  /** Reattach to a browser that is already running. */
1870
- get: (id) => reattach(http, id),
2039
+ get: async (id) => reattach(http, id),
1871
2040
  /** Every browser on this key. */
1872
- list: () => http().request("GET", "/api/browsers"),
2041
+ list: async () => http().request("GET", "/api/browsers"),
1873
2042
  /** Stop some (`ids`) or every browser on this key. Each reports separately. */
1874
- stop: (ids) => stopBrowsers(http, ids),
2043
+ stop: async (ids) => stopBrowsers(http, ids),
1875
2044
  /** Stop every browser on this key; returns how many stopped. */
1876
2045
  stopAll: async () => (await stopBrowsers(http, "all")).stopped
1877
2046
  });
1878
- var session = (id, action = "") => `/api/control/sessions/${encodeURIComponent(id)}${action}`;
1879
- 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)}`;
1880
2049
  var sessionCalls = (http) => ({
1881
2050
  /** Settings, sessions and recent events at a glance. */
1882
- overview: () => http().request("GET", "/api/control"),
2051
+ overview: async () => http().request("GET", "/api/control"),
1883
2052
  /** Every session, including disconnected and cleanup-pending ones. */
1884
- sessions: () => http().request("GET", "/api/control/sessions"),
2053
+ sessions: async () => http().request("GET", "/api/control/sessions"),
1885
2054
  /** One session. */
1886
- session: (id) => http().request("GET", session(id)),
2055
+ session: async (id) => http().request("GET", session(id)),
1887
2056
  /** Update limits, rate cards and retention. */
1888
- settings: (changes) => http().request("PATCH", "/api/control/project", changes)
2057
+ settings: async (changes) => http().request("PATCH", "/api/control/project", changes)
1889
2058
  });
1890
2059
  var lifecycleCalls = (http) => ({
1891
- /** Cancel queued or provisioning work. */
1892
- 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"), {}),
1893
2062
  /** Stop a session; `force` stops despite a profile-save error, or reconciles. */
1894
- stop: (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
2063
+ stop: async (id, force = false) => http().request("POST", session(id, "/stop"), { force }),
1895
2064
  /** Acquire or release human control, or acknowledge the agent's resume. */
1896
- takeover: (id, action) => http().request("POST", session(id, "/control"), { action }),
2065
+ takeover: async (id, action) => http().request("POST", session(id, "/control"), { action }),
1897
2066
  /** Send one input as the human holding the control lease. */
1898
- 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 })
1899
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
+ }
1900
2075
  var recoveryCalls = (http) => ({
1901
- /** Explicitly recover a session, or replace it with a fresh one. */
1902
- 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)),
1903
2082
  /** A single-use ticket for the live stream. */
1904
- ticket: (id) => http().request("POST", session(id, "/ticket"), {}),
2083
+ ticket: async (id) => http().request("POST", session(id, "/ticket"), {}),
1905
2084
  /** Durable lifecycle events after a cursor. */
1906
- events: (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
2085
+ events: async (after = 0) => http().request("GET", `/api/control/events?after=${after}`)
1907
2086
  });
1908
2087
  var credentialCalls = (http) => ({
1909
2088
  /** Mint a service credential. Its token is returned this once. */
1910
- createCredential: (options) => http().request("POST", "/api/control/credentials", options),
2089
+ createCredential: async (options) => http().request("POST", "/api/control/credentials", options),
1911
2090
  /** Revoke a service credential. */
1912
- revokeCredential: (id) => http().request("DELETE", item("credentials", id))
2091
+ revokeCredential: async (id) => http().request("DELETE", item("credentials", id))
1913
2092
  });
1914
2093
  var memberCalls = (http) => ({
1915
2094
  /** The owner and every member. */
1916
- members: () => http().request("GET", "/api/control/members"),
2095
+ members: async () => http().request("GET", "/api/control/members"),
1917
2096
  /** An invitation code for a new member. */
1918
- inviteMember: (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
2097
+ inviteMember: async (role = "operator") => http().request("POST", "/api/control/members/invite", { role }),
1919
2098
  /** Remove a member. */
1920
- removeMember: (userId) => http().request("DELETE", item("members", userId))
2099
+ removeMember: async (userId) => http().request("DELETE", item("members", userId))
1921
2100
  });
1922
2101
  var webhookCalls = (http) => ({
1923
2102
  /** Register a webhook; `types` limits which events it receives. */
1924
- createWebhook: (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
2103
+ createWebhook: async (url, types = []) => http().request("POST", "/api/control/webhooks", { url, types }),
1925
2104
  /** Disable a webhook. */
1926
- removeWebhook: (id) => http().request("DELETE", item("webhooks", id)),
2105
+ removeWebhook: async (id) => http().request("DELETE", item("webhooks", id)),
1927
2106
  /** Send a delivery again. */
1928
- replayDelivery: (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
2107
+ replayDelivery: async (id) => http().request("POST", `${item("deliveries", id)}/replay`, {})
1929
2108
  });
1930
2109
  var controlApi = (http) => ({
1931
2110
  ...sessionCalls(http),
@@ -1935,7 +2114,7 @@ var controlApi = (http) => ({
1935
2114
  ...memberCalls(http),
1936
2115
  ...webhookCalls(http)
1937
2116
  });
1938
- var playbook = (name) => `/api/playbooks/${encodeURIComponent(name)}`;
2117
+ var playbook = (name) => `/api/playbooks/${segment(name, "name")}`;
1939
2118
  var playbookApi = (http) => ({
1940
2119
  /** Every saved playbook, with any draft waiting on it. */
1941
2120
  list: async () => (await http().request("GET", "/api/playbooks")).playbooks,
@@ -1944,16 +2123,16 @@ var playbookApi = (http) => ({
1944
2123
  await http().request("DELETE", playbook(name));
1945
2124
  },
1946
2125
  /** Replace a playbook with the draft a healed replay saved. Try it first with `browser.play('<name>:draft')`. */
1947
- promote: (name) => http().request("POST", `${playbook(name)}/promote`, {})
2126
+ promote: async (name) => http().request("POST", `${playbook(name)}/promote`, {})
1948
2127
  });
1949
2128
  var proxyApi = (http) => ({
1950
2129
  /** Every proxy this key can use, shared ones included. */
1951
2130
  list: async () => (await http().request("GET", "/api/proxies")).proxies,
1952
2131
  /** Add a proxy. Its credentials are never read back. */
1953
- create: (proxy) => http().request("POST", "/api/proxies", proxy),
2132
+ create: async (proxy) => http().request("POST", "/api/proxies", proxy),
1954
2133
  /** Remove one of this key's proxies. */
1955
2134
  remove: async (id) => {
1956
- await http().request("DELETE", `/api/proxies/${encodeURIComponent(id)}`);
2135
+ await http().request("DELETE", `/api/proxies/${segment(id)}`);
1957
2136
  },
1958
2137
  /** Dial each proxy and learn its real exit IP. Failing ones cool down and are skipped. */
1959
2138
  check: async () => (await http().request("POST", "/api/proxies/check", {})).results
@@ -1962,35 +2141,38 @@ var identityCalls = (http) => ({
1962
2141
  /** Every persona on this key. */
1963
2142
  list: async () => (await http().request("GET", "/api/personas")).personas,
1964
2143
  /** One persona. */
1965
- get: (id) => http().request("GET", `/api/personas/${id}`),
2144
+ get: async (id) => http().request("GET", `/api/personas/${segment(id)}`),
1966
2145
  /**
1967
2146
  * Create an identity. The device, platform, timezone, locale, is chosen
1968
2147
  * here and fixed for its life; `preview()` shows what a choice produces.
1969
2148
  */
1970
- create: (options = {}) => http().request("POST", "/api/personas", options),
2149
+ create: async (options = {}) => http().request("POST", "/api/personas", options),
1971
2150
  /** Name, concurrency cap and proxy hint. Never the device, clone for that. */
1972
- update: (id, changes) => http().request("PUT", `/api/personas/${id}`, changes),
2151
+ update: async (id, changes) => http().request("PUT", `/api/personas/${segment(id)}`, changes),
1973
2152
  /** A new persona of the same kind of device: same choices, fresh identity, empty jar. */
1974
- 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)
1975
2154
  });
1976
2155
  var deviceCalls = (http) => ({
1977
2156
  /** The fingerprint these choices would produce. Persists nothing. */
1978
2157
  preview: async (prefs = {}) => (await http().request("POST", "/api/personas/preview", { prefs })).fingerprint,
1979
2158
  /** Platforms, and the timezones and locales each may coherently claim. */
1980
- options: () => http().request("GET", "/api/personas/options"),
2159
+ options: async () => http().request("GET", "/api/personas/options"),
1981
2160
  /** Pin the persona to one of your proxies, or `null` to let assignment happen at connect. */
1982
- 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 }),
1983
2162
  /** Delete a persona. */
1984
2163
  remove: async (id) => {
1985
- await http().request("DELETE", `/api/personas/${id}`);
2164
+ await http().request("DELETE", `/api/personas/${segment(id)}`);
1986
2165
  }
1987
2166
  });
1988
2167
  var mfaCalls = (http) => ({
1989
2168
  /** Store the second factor for this identity. Sealed at rest, never read back. */
1990
- 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),
1991
2170
  /** Remove the persona-wide factor, or the one filed against `domain`. */
1992
2171
  clearMfa: async (id, domain) => {
1993
- 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
+ );
1994
2176
  }
1995
2177
  });
1996
2178
  var loginCalls = (http) => ({
@@ -2001,19 +2183,36 @@ var loginCalls = (http) => ({
2001
2183
  * better path. This is for portals that expire a session server-side
2002
2184
  * between runs, where an unattended run has nothing else to recover with.
2003
2185
  */
2004
- 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),
2005
2187
  /** Which sites this identity can sign in to. Usernames only. */
2006
- credentials: (id) => http().request("GET", `/api/personas/${id}/credentials`),
2188
+ credentials: async (id) => http().request("GET", `/api/personas/${segment(id)}/credentials`),
2007
2189
  /** Remove the login stored for one site. */
2008
2190
  clearCredentials: async (id, domain) => {
2009
- await http().request("DELETE", `/api/personas/${id}/credentials?domain=${encodeURIComponent(domain)}`);
2191
+ await http().request("DELETE", `/api/personas/${segment(id)}/credentials?domain=${encodeURIComponent(domain)}`);
2192
+ }
2193
+ });
2194
+ var jarPath = (id) => `/api/pool/cookies?persona=${segment(id)}`;
2195
+ var jarCalls = (http) => ({
2196
+ /** Every cookie in the jar; `format: 'playwright'` is ready for `context.addCookies()`. */
2197
+ cookies: async (id, format = "json") => (await http().request("GET", `${jarPath(id)}&format=${encodeURIComponent(format)}`)).cookies,
2198
+ /** Merge cookies into the jar. The persona's browsers pick them up on their next visit to each site. */
2199
+ importCookies: async (id, list2) => http().request("PUT", jarPath(id), { cookies: list2 })
2200
+ });
2201
+ var cookieCalls = (http) => ({
2202
+ ...jarCalls(http),
2203
+ /** Copy every login of persona `from` into persona `to`, which keeps its own device. */
2204
+ copyCookies: async (from, to) => jarCalls(http).importCookies(to, await jarCalls(http).cookies(from)),
2205
+ /** Forget every cookie in the jar: signs the persona out everywhere. */
2206
+ clearCookies: async (id) => {
2207
+ await http().request("DELETE", jarPath(id));
2010
2208
  }
2011
2209
  });
2012
2210
  var personaApi = (http) => ({
2013
2211
  ...identityCalls(http),
2014
2212
  ...deviceCalls(http),
2015
2213
  ...mfaCalls(http),
2016
- ...loginCalls(http)
2214
+ ...loginCalls(http),
2215
+ ...cookieCalls(http)
2017
2216
  });
2018
2217
  var configApi = (http) => ({
2019
2218
  /** This key's settings. Secrets read back masked. */
@@ -2097,32 +2296,101 @@ var Oya = class {
2097
2296
  };
2098
2297
 
2099
2298
  // src/context.ts
2100
- function client(flags2) {
2101
- const saved = resolved();
2102
- const apiKey = flagStr(flags2, "key") || saved.apiKey;
2103
- const baseUrl = flagStr(flags2, "url") || saved.baseUrl;
2104
- if (!apiKey) {
2105
- console.error("No API key. Run `oya login`, or pass --key / set OYA_API_KEY.");
2106
- process.exit(1);
2107
- }
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." });
2108
2304
  return new Oya({ apiKey, baseUrl });
2109
2305
  }
2110
2306
  var printJson = (value) => console.log(JSON.stringify(value, null, JSON_INDENT2));
2111
- var out = (flags2, value, human) => {
2112
- if (flags2.json) printJson(value);
2307
+ var out = (flags, value, human) => {
2308
+ if (flags.json) printJson(value);
2113
2309
  else human();
2114
2310
  };
2115
- async function targetBrowser(oya, flags2) {
2116
- const id = flagStr(flags2, "id");
2311
+ async function targetBrowser(oya, flags) {
2312
+ const id = flagStr(flags, "id");
2117
2313
  if (id) return oya.browser.get(id);
2118
2314
  const all = await oya.browser.list();
2119
- if (!all.length) {
2120
- console.error("No browsers running. Start one with `oya start`.");
2121
- process.exit(1);
2122
- }
2315
+ if (!all.length) throw new CliError("No browsers running.", "no_browser", { hint: "Start one with oya start." });
2123
2316
  return oya.browser.get(all[all.length - 1].id);
2124
2317
  }
2125
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
+
2126
2394
  // src/commands/init.ts
2127
2395
  var PRESETS = {
2128
2396
  anthropic: { name: "Anthropic", model: "claude-sonnet-5" },
@@ -2149,20 +2417,29 @@ async function askModelDetails(llm, updates) {
2149
2417
  const model = await ask("Default model:", PRESETS[llm].model);
2150
2418
  if (model) updates.chat_model = model;
2151
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
+ }
2152
2426
  function providerOptions(current) {
2153
- 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;
2154
2428
  return current.providers.map((p) => ({ id: p.id, label: p.label, note: note2(p) }));
2155
2429
  }
2156
2430
  async function askProvider(current, updates) {
2157
2431
  console.log("\n\u2500\u2500 2. Where your browsers run \u2500\u2500");
2158
2432
  const provider = await choose("Browser provider:", providerOptions(current));
2159
2433
  updates.browser_provider = provider;
2160
- for (const field of current.providers.find((p) => p.id === provider)?.needs || []) {
2161
- const value = await askSecret(`${field.replace(/_/g, " ")}:`);
2162
- if (value) updates[field] = value;
2163
- }
2434
+ const info = current.providers.find((p) => p.id === provider);
2435
+ for (const field of info?.needs || []) await askField(info, field, updates);
2164
2436
  return provider;
2165
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
+ }
2166
2443
  var CAPTCHA_OPTIONS2 = [
2167
2444
  { id: "", label: "No", note: "providers that solve natively still will" },
2168
2445
  { id: "capsolver", label: "Yes, via CapSolver" }
@@ -2182,18 +2459,23 @@ function signInHint(provider) {
2182
2459
  console.log(" arrive already logged in, as the same identity, from the same fingerprint.");
2183
2460
  console.log(` Download the desktop browser: ${resolved().baseUrl}/downloads`);
2184
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
+ }
2185
2468
  async function interview2(oya) {
2186
2469
  const current = await oya.config.get();
2187
2470
  const updates = {};
2188
2471
  await askModel(current, updates);
2189
2472
  const provider = await askProvider(current, updates);
2190
2473
  await askCaptcha(updates);
2191
- updates.onboarded = "true";
2192
- await oya.config.set(updates);
2474
+ await saveAnswers(oya, updates);
2193
2475
  return provider;
2194
2476
  }
2195
- async function cmdInit(flags2) {
2196
- const provider = await interview2(client(flags2));
2477
+ async function cmdInit(flags) {
2478
+ const provider = await interview2(client(flags));
2197
2479
  console.log("\n\u2705 Saved against your API key.");
2198
2480
  signInHint(provider);
2199
2481
  console.log("\n Then: oya start && oya goto https://example.com");
@@ -2201,47 +2483,48 @@ async function cmdInit(flags2) {
2201
2483
 
2202
2484
  // src/commands/browsers.ts
2203
2485
  import { spawn as spawn3 } from "child_process";
2204
- function identityOptions(flags2) {
2486
+ function identityOptions(flags) {
2205
2487
  return {
2206
- persona: flagStr(flags2, "persona") || "default",
2207
- captcha: flags2.captcha === false ? "off" : "auto",
2208
- provider: flagStr(flags2, "provider"),
2209
- wsUrl: flagStr(flags2, "ws-url"),
2210
- name: flagStr(flags2, "name"),
2211
- 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")
2212
2493
  };
2213
2494
  }
2214
- function governanceOptions(flags2) {
2495
+ function governanceOptions(flags) {
2215
2496
  return {
2216
- queueMs: flagNum(flags2, "queue-ms"),
2217
- budgetUsd: flagNum(flags2, "budget-usd"),
2218
- governed: flags2.governed === true,
2219
- priority: flagStr(flags2, "priority"),
2220
- 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
2221
2502
  };
2222
2503
  }
2223
- var startOptions = (flags2) => ({ ...identityOptions(flags2), ...governanceOptions(flags2) });
2224
- async function cmdStart(flags2) {
2225
- 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));
2226
2508
  const { id, provider, persona, cdpUrl } = browser;
2227
- out(flags2, { id, provider, persona, cdpUrl }, () => {
2509
+ out(flags, { id, provider, persona, cdpUrl }, () => {
2228
2510
  console.log(`\u2705 ${id}`);
2229
2511
  console.log(` provider: ${provider} persona: ${persona}`);
2230
2512
  if (cdpUrl) console.log(` cdp: ${cdpUrl}`);
2231
2513
  });
2232
2514
  }
2233
- async function cmdGoto(args2, flags2) {
2234
- const url = args2[0];
2235
- if (!url) throw new Error("Usage: oya goto <url>");
2236
- 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);
2237
2519
  await browser.goto(url);
2238
- console.log(`\u2705 ${browser.id} \u2192 ${url}`);
2520
+ out(flags, { id: browser.id, url }, () => console.log(`\u2705 ${browser.id} \u2192 ${url}`));
2239
2521
  }
2240
- async function cmdAsk(args2, flags2) {
2241
- const prompt = args2.join(" ");
2242
- if (!prompt) throw new Error('Usage: oya ask "find the pricing page"');
2243
- const browser = await targetBrowser(client(flags2), flags2);
2244
- 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));
2245
2528
  }
2246
2529
  var DOT = { ok: "\u25CF", stale: "\u25D0", errors: "\u2717", dead: "\u25CB" };
2247
2530
  function lsRow(b) {
@@ -2250,9 +2533,9 @@ function lsRow(b) {
2250
2533
  const url = b.currentUrl.replace(/^https?:\/\//, "").slice(0, LsColumns.URL);
2251
2534
  return `${DOT[b.health] || "\xB7"} ${b.id} ${provider} ${persona} ${b.name.padEnd(LsColumns.NAME)} ${b.commands}\xB7${b.errors} ${url}`;
2252
2535
  }
2253
- async function cmdLs(flags2) {
2254
- const all = await client(flags2).browser.list();
2255
- out(flags2, all, () => {
2536
+ async function cmdLs(flags) {
2537
+ const all = await client(flags).browser.list();
2538
+ out(flags, all, () => {
2256
2539
  if (!all.length) return console.log("No browsers running.");
2257
2540
  for (const b of all) console.log(lsRow(b));
2258
2541
  console.log(`
@@ -2264,13 +2547,19 @@ function rmLine(x) {
2264
2547
  const note2 = x.sandboxRemoved === false ? ", sandbox NOT removed, check Oya Cloud" : removed;
2265
2548
  return `${x.ok ? "\u2705" : "\u2717"} ${x.id}${note2}${x.error ? ` ${x.error}` : ""}`;
2266
2549
  }
2267
- async function cmdRm(args2, flags2) {
2268
- const oya = client(flags2);
2269
- if (!flags2.all && !args2.length) throw new Error("Usage: oya rm <id>\u2026 | oya rm --all");
2270
- const r = await oya.browser.stop(flags2.all ? "all" : args2);
2271
- for (const x of r.results) console.log(rmLine(x));
2272
- 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);
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}`);
2273
2561
  }
2562
+ var stopsFailed = (stopped, of, json) => new CliError(`stopped ${stopped} of ${of}`, "partial_failure", { shown: !json });
2274
2563
  function activityLine(a) {
2275
2564
  const error = a.error ? `, ${a.error}` : "";
2276
2565
  return ` ${a.ok ? "\xB7" : "\u2717"} ${a.action.padEnd(StatusColumns.ACTION)} ${a.summary}${error} ${a.ms}ms`;
@@ -2284,21 +2573,21 @@ function printStatus(s) {
2284
2573
  console.log(" recent:");
2285
2574
  for (const a of s.activity.slice(0, StatusColumns.RECENT)) console.log(activityLine(a));
2286
2575
  }
2287
- async function cmdStatus(flags2) {
2288
- const browser = await targetBrowser(client(flags2), flags2);
2576
+ async function cmdStatus(flags) {
2577
+ const browser = await targetBrowser(client(flags), flags);
2289
2578
  const s = await browser.status();
2290
- out(flags2, s, () => printStatus(s));
2579
+ out(flags, s, () => printStatus(s));
2291
2580
  }
2292
2581
  function opener(url) {
2293
2582
  if (process.platform === "darwin") return ["open", [url]];
2294
2583
  if (process.platform === "win32") return ["cmd", ["/c", "start", "", url]];
2295
2584
  return ["xdg-open", [url]];
2296
2585
  }
2297
- async function cmdOpen(flags2) {
2298
- const browser = await targetBrowser(client(flags2), flags2);
2299
- const [command2, args2] = opener(browser.liveViewUrl());
2300
- spawn3(command2, args2, { detached: true, stdio: "ignore" }).unref();
2301
- 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}`));
2302
2591
  }
2303
2592
 
2304
2593
  // src/commands/personas.ts
@@ -2312,61 +2601,74 @@ var PLATFORM_ALIAS = {
2312
2601
  linux: "Linux x86_64",
2313
2602
  "linux x86_64": "Linux x86_64"
2314
2603
  };
2315
- function prefsFrom(flags2) {
2316
- const platformFlag = flagStr(flags2, "platform");
2604
+ function prefsFrom(flags) {
2605
+ const platformFlag = flagStr(flags, "platform");
2317
2606
  return {
2318
2607
  ...platformFlag ? { platform: PLATFORM_ALIAS[platformFlag.toLowerCase()] || platformFlag } : {},
2319
- ...flagStr(flags2, "tz") ? { timezone: flagStr(flags2, "tz") } : {},
2320
- ...flagStr(flags2, "locale") ? { locale: flagStr(flags2, "locale") } : {}
2608
+ ...flagStr(flags, "tz") ? { timezone: flagStr(flags, "tz") } : {},
2609
+ ...flagStr(flags, "locale") ? { locale: flagStr(flags, "locale") } : {}
2321
2610
  };
2322
2611
  }
2323
- function createOptions(flags2, rest, prefs) {
2612
+ function createOptions(flags, rest, prefs) {
2324
2613
  return {
2325
- name: flagStr(flags2, "name") || rest[0],
2614
+ name: flagStr(flags, "name") || rest[0],
2326
2615
  prefs,
2327
- proxy: flagStr(flags2, "geo") ? { geo: flagStr(flags2, "geo") } : void 0,
2328
- 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
2329
2618
  };
2330
2619
  }
2331
- var create = async (oya, rest, flags2) => {
2332
- const prefs = prefsFrom(flags2);
2333
- if (flags2.preview) return preview(oya, prefs, flags2);
2334
- 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));
2335
2624
  const fp = created.fingerprint;
2336
- 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}`));
2337
2626
  };
2338
- async function preview(oya, prefs, flags2) {
2627
+ async function preview(oya, prefs, flags) {
2339
2628
  const fp = await oya.personas.preview(prefs);
2340
- 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}`));
2341
2630
  }
2342
- function editChanges(flags2) {
2343
- const max = flagStr(flags2, "max");
2631
+ function editChanges(flags) {
2632
+ const max = flagCount(flags, "max", { allowNone: true });
2344
2633
  return {
2345
- ...flagStr(flags2, "name") ? { name: flagStr(flags2, "name") } : {},
2346
- ...max ? { maxConcurrent: max === "none" ? null : Number(max) } : {},
2347
- ...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") } } : {}
2348
2637
  };
2349
2638
  }
2350
- var edit = async (oya, rest, flags2) => {
2351
- if (!rest[0]) throw new Error("Usage: oya personas edit <id> --name <n> --max <n> --geo <cc>");
2352
- const updated = await oya.personas.update(rest[0], editChanges(flags2));
2353
- 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"}`));
2354
2643
  };
2355
- var clone = async (oya, rest, flags2) => {
2356
- if (!rest[0]) throw new Error("Usage: oya personas clone <id> [--name <n>]");
2357
- 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") });
2358
2647
  const fp = c.fingerprint;
2359
2648
  out(
2360
- flags2,
2649
+ flags,
2361
2650
  c,
2362
2651
  () => console.log(`\u2705 ${c.id} ${c.name} ${fp.platform} \xB7 ${fp.timezone} (new identity, same kind of device)`)
2363
2652
  );
2364
2653
  };
2365
- var remove = async (oya, rest) => {
2366
- if (!rest[0]) throw new Error("Usage: oya personas rm <id>");
2367
- await oya.personas.remove(rest[0]);
2368
- 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);
2369
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 });
2370
2672
  function personaLine(p) {
2371
2673
  const cap = p.maxConcurrent === null ? "\u221E" : String(p.maxConcurrent);
2372
2674
  const where = p.exit ? ` via ${p.exit.label}` : p.proxy?.geo ? ` geo ${p.proxy.geo}` : "";
@@ -2374,58 +2676,117 @@ function personaLine(p) {
2374
2676
  const device = `${p.fingerprint.platform} \xB7 ${p.fingerprint.timezone}`;
2375
2677
  return `${p.id} ${p.name.padEnd(PERSONA_NAME_WIDTH)} ${p.activeBrowsers}/${cap} running ${device}${extras}`;
2376
2678
  }
2377
- var list = async (oya, _rest, flags2) => {
2679
+ var list = async (oya, _rest, flags) => {
2378
2680
  const all = await oya.personas.list();
2379
- out(flags2, all, () => {
2681
+ out(flags, all, () => {
2380
2682
  if (!all.length) return console.log("No personas yet, `oya personas new`.");
2381
2683
  for (const p of all) console.log(personaLine(p));
2382
2684
  });
2383
2685
  };
2384
2686
  var SUBCOMMANDS = { new: create, create, edit, clone, rm: remove, delete: remove };
2385
- async function cmdPersonas(args2, flags2) {
2386
- const oya = client(flags2);
2387
- const [sub, ...rest] = args2;
2388
- const run2 = sub !== void 0 && Object.hasOwn(SUBCOMMANDS, sub) ? SUBCOMMANDS[sub] : list;
2389
- 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);
2692
+ }
2693
+
2694
+ // src/commands/cookies.ts
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) {
2703
+ writeFileSync5(file, JSON.stringify(cookies, null, JSON_INDENT2) + "\n", { mode: PRIVATE_FILE_MODE });
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));
2715
+ };
2716
+ function cookiesIn(file) {
2717
+ const parsed = readJsonFile(file);
2718
+ const list2 = Array.isArray(parsed) ? parsed : parsed?.cookies;
2719
+ if (!Array.isArray(list2)) throw usage(`${file} holds no list of cookies.`);
2720
+ return list2;
2721
+ }
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>.");
2725
+ const done = await oya.personas.importCookies(rest[0], cookiesIn(rest[1]));
2726
+ out(
2727
+ flags,
2728
+ done,
2729
+ () => console.log(`\u2705 ${done.imported} imported, ${done.skipped} skipped, ${done.total} in the jar`)
2730
+ );
2731
+ };
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>.");
2734
+ const done = await oya.personas.copyCookies(rest[0], rest[1]);
2735
+ out(flags, done, () => console.log(`\u2705 ${done.imported} copied to ${rest[1]}, ${done.total} in its jar`));
2736
+ };
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]}`));
2741
+ };
2742
+ var SUBCOMMANDS2 = { export: exportJar, import: importJar, copy, clear };
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);
2390
2748
  }
2391
2749
 
2392
2750
  // src/commands/settings.ts
2393
- function parsePairs(args2) {
2751
+ function parsePairs(args) {
2394
2752
  const updates = {};
2395
- for (const pair of args2) {
2753
+ for (const pair of args) {
2396
2754
  const index = pair.indexOf("=");
2397
- 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}".`);
2398
2756
  updates[pair.slice(0, index)] = pair.slice(index + 1);
2399
2757
  }
2400
2758
  return updates;
2401
2759
  }
2402
- async function cmdConfig(args2, flags2) {
2403
- const oya = client(flags2);
2404
- if (!args2.length) {
2760
+ async function cmdConfig(args, flags) {
2761
+ const oya = client(flags);
2762
+ if (!args.length) {
2405
2763
  const current = await oya.config.get();
2406
- return out(flags2, current, () => printJson(current));
2764
+ return out(flags, current, () => printJson(current));
2407
2765
  }
2408
- const updates = parsePairs(args2);
2409
- await oya.config.set(updates);
2410
- 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(", ")}`));
2411
2769
  }
2412
- async function cmdUsage(flags2) {
2413
- printJson(await client(flags2).usage());
2770
+ async function cmdUsage(flags) {
2771
+ printJson(await client(flags).usage());
2414
2772
  }
2415
- async function cmdWhoami() {
2416
- 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 });
2417
2778
  }
2418
2779
 
2419
2780
  // src/commands/stealth.ts
2420
2781
  import { spawn as spawn4 } from "child_process";
2421
2782
  var HARNESS = "tests/integration/stealth.test.js";
2422
- var spawnHarness = (flags2) => spawn4("node", [HARNESS, ...flags2.live ? ["--live"] : []], {
2783
+ var spawnHarness = (flags) => spawn4("node", [HARNESS, ...flags.live ? ["--live"] : []], {
2423
2784
  cwd: process.env.OYA_SERVER_DIR || "server",
2424
2785
  stdio: "inherit"
2425
2786
  });
2426
- function cmdStealthTest(flags2) {
2787
+ function cmdStealthTest(flags) {
2427
2788
  return new Promise((resolve, reject) => {
2428
- const child = spawnHarness(flags2);
2789
+ const child = spawnHarness(flags);
2429
2790
  const missing = "Could not find the stealth harness. Run it from a checkout, or set OYA_SERVER_DIR.";
2430
2791
  child.on("error", () => reject(new Error(missing)));
2431
2792
  child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`stealth test exited ${code}`)));
@@ -2434,163 +2795,301 @@ function cmdStealthTest(flags2) {
2434
2795
 
2435
2796
  // src/commands/control.ts
2436
2797
  var ROLES = ["viewer", "operator", "administrator"];
2437
- function required(args2, i = 0) {
2438
- if (!args2[i]) throw new Error("Missing argument; run oya help");
2439
- 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];
2440
2801
  }
2441
- function roleFlag(flags2) {
2442
- const role = flagStr(flags2, "role") || "operator";
2443
- 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}".`);
2444
2805
  return role;
2445
2806
  }
2446
- function subcommand(table, sub, call2, usage) {
2447
- 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))}.`);
2448
2810
  return table[sub](call2);
2449
2811
  }
2812
+ var orList = (names) => names.join(", ").replace(/, ([^,]*)$/, " or $1");
2450
2813
  var members = (call2) => {
2451
2814
  if (!call2.args[0]) return call2.c.members();
2452
2815
  const table = {
2453
- remove: ({ c, args: args2 }) => c.removeMember(required(args2, 1)),
2454
- 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))
2455
2818
  };
2456
- return subcommand(table, call2.args[0], call2, "Use members, members invite, or members remove");
2819
+ return subcommand(table, call2.args[0], call2, "members");
2457
2820
  };
2458
2821
  var credential = (call2) => {
2459
2822
  const table = {
2460
- revoke: ({ c, args: args2 }) => c.revokeCredential(required(args2, 1)),
2461
- 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") })
2462
2825
  };
2463
- 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");
2464
2827
  };
2465
2828
  var webhook = (call2) => {
2466
2829
  const table = {
2467
- new: ({ c, args: args2 }) => c.createWebhook(required(args2, 1)),
2468
- remove: ({ c, args: args2 }) => c.removeWebhook(required(args2, 1)),
2469
- 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"))
2470
2833
  };
2471
- 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");
2472
2835
  };
2473
- var takeover = ({ c, command: command2, args: args2 }) => c.takeover(required(args2), command2 === "takeover" ? "acquire" : command2);
2474
- 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 = {
2475
2853
  control: ({ c }) => c.overview(),
2476
- sessions: ({ c, args: args2 }) => args2[0] ? c.session(args2[0]) : c.sessions(),
2477
- cancel: ({ c, args: args2 }) => c.cancel(required(args2)),
2478
- 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)),
2479
2857
  members,
2480
- 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),
2481
2859
  takeover,
2482
2860
  release: takeover,
2483
2861
  resume: takeover,
2484
- events: ({ c, flags: flags2 }) => c.events(Number(flagStr(flags2, "after") || 0)),
2485
- 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"))),
2486
2864
  credential,
2487
2865
  webhook
2488
2866
  };
2489
- function isControlCommand(command2, flags2) {
2490
- if (command2 === "stop") return flags2.force === true;
2491
- 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);
2492
2870
  }
2493
- async function runControl(command2, args2, flags2) {
2494
- const c = client(flags2).control;
2495
- 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 }));
2496
2874
  }
2497
2875
 
2498
2876
  // src/help.ts
2499
2877
  var HELP = `oya, thousands of browsers, one API
2500
2878
 
2501
- oya install Stand up a self-hosted control plane
2502
- [--dry-run] [--config oya-install.json]
2503
- oya login Save an API key for this machine
2504
- oya init Set your model, browser provider and sign-ins
2505
- oya start [--persona auto] Start a browser and print its id
2506
- oya goto <url> [--id <id>] Navigate (defaults to the newest browser)
2507
- oya ask "<prompt>" [--id <id>] Drive it in plain language
2508
- oya ls List running browsers
2509
- oya rm <id> | --all Stop browsers
2510
- oya personas Identities: fingerprint + cookies + proxy
2511
- oya personas new [name] --platform Win32|MacIntel|Linux --tz <zone> --locale <l> --max <n>
2512
- oya personas edit <id> --name <n> --max <n> --geo <cc>
2513
- oya personas clone|rm <id> A new device of the same kind \xB7 delete
2514
- oya status [--id <id>] Health, counters and what it has been doing
2515
- oya open [--id <id>] Open the live view in your browser
2516
- oya config [key=value ...] Show or change this key's settings
2517
- oya control Durable project overview
2518
- oya sessions [id] All sessions, including pending cleanup
2519
- oya stop <id> --force Stop despite a profile-save error, or reconcile
2520
- oya takeover <id> Acquire human control
2521
- oya release <id> Release human control, leaving the agent paused
2522
- oya resume <id> Acknowledge agent resume
2523
- oya events [--after <cursor>] Read durable lifecycle events
2524
- oya project <settings-json> Update limits, rate cards and retention
2525
- oya start --governed --provider oya-selfhosted [--queue-ms 30000]
2526
- [--budget-usd 5] [--policy JSON] [--idempotency-key ID]
2527
- oya cancel <id> Cancel queued or provisioning work
2528
- oya recover <id> [--replace] Explicitly recover or replace a session
2529
- oya members [invite|remove] List members, invite, or remove a user
2530
- oya credential new [--role viewer|operator|administrator]
2531
- oya credential revoke <id> Revoke a service credential
2532
- oya webhook new <https-url> Register a signed event webhook
2533
- oya webhook remove <id> Disable a webhook
2534
- oya webhook replay <id> Replay a delivery
2535
- oya usage What this key has spent
2536
- 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
2537
2886
 
2538
- Options: --url <control plane> --key <api key> --json
2539
- 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}
2540
2947
  `;
2541
2948
 
2542
2949
  // src/commands/index.ts
2950
+ import { createRequire } from "module";
2543
2951
  var help = async () => console.log(HELP);
2544
- var COMMANDS2 = {
2545
- install: (_args, flags2) => cmdInstall(flags2),
2546
- login: (_args, flags2) => cmdLogin(flags2),
2547
- init: (_args, flags2) => cmdInit(flags2),
2548
- 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),
2549
2973
  goto: cmdGoto,
2550
2974
  ask: cmdAsk,
2551
- ls: (_args, flags2) => cmdLs(flags2),
2552
- list: (_args, flags2) => cmdLs(flags2),
2553
- status: (_args, flags2) => cmdStatus(flags2),
2975
+ ls: (_args, flags) => cmdLs(flags),
2976
+ list: (_args, flags) => cmdLs(flags),
2977
+ status: (_args, flags) => cmdStatus(flags),
2554
2978
  rm: cmdRm,
2555
2979
  stop: cmdRm,
2556
2980
  personas: cmdPersonas,
2557
- open: (_args, flags2) => cmdOpen(flags2),
2981
+ cookies: cmdCookies,
2982
+ open: (_args, flags) => cmdOpen(flags),
2558
2983
  config: cmdConfig,
2559
- usage: (_args, flags2) => cmdUsage(flags2),
2560
- "stealth-test": (_args, flags2) => cmdStealthTest(flags2),
2561
- 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,
2562
2988
  help,
2563
2989
  "--help": help,
2564
2990
  "-h": help
2565
2991
  };
2566
- function unknown(command2) {
2567
- console.error(`Unknown command: ${command2}
2568
- `);
2569
- console.log(HELP);
2570
- 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;
2571
3071
  }
2572
- async function dispatch(command2, args2, flags2) {
2573
- if (isControlCommand(command2, flags2)) return runControl(command2, args2, flags2);
2574
- if (!Object.hasOwn(COMMANDS2, command2)) unknown(command2);
2575
- 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 }));
2576
3084
  }
2577
3085
 
2578
3086
  // src/index.ts
2579
- function fail2(err) {
2580
- const error = err;
2581
- console.error(`
2582
- \u2717 ${error.message}`);
2583
- if (error.status === Status2.UNAUTHORIZED) console.error(" The API key was rejected. Run `oya login`.");
2584
- if (error.status === Status2.TOO_MANY_REQUESTS) {
2585
- console.error(" A quota or a persona concurrency cap. `oya personas` shows what is running.");
2586
- }
2587
- process.exit(1);
2588
- }
2589
- var { command, args, flags } = parse(process.argv.slice(ARGV_SKIP));
3087
+ var argv = process.argv.slice(ARGV_SKIP);
2590
3088
  try {
3089
+ const { command, args, flags } = parse(argv);
2591
3090
  await dispatch(command, args, flags);
2592
3091
  } catch (err) {
2593
- fail2(err);
3092
+ fail2(err, argv);
2594
3093
  } finally {
2595
3094
  closePrompts();
2596
3095
  }