@oya-ai/browser 1.0.125 → 1.0.127

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.
package/README.md CHANGED
@@ -50,6 +50,18 @@ Node.js 20 or newer; the examples are ES modules. `OYA_BASE_URL` points at a sel
50
50
  control plane. On Node.js 24+, `await using browser = await oya.browser.start()` stops it
51
51
  when the scope exits, errors included.
52
52
 
53
+
54
+ ## For AI agents
55
+
56
+ No key yet? Get your own, and use your person's own browser with their logins:
57
+
58
+ ```js
59
+ await Oya.signup({ email: 'person@example.com' }); // saved to ~/.oya/config.json
60
+ const browser = await new Oya().desktop.connect(); // pairs the Oya desktop app
61
+ ```
62
+
63
+ Written for agents: [oyabrowser.com/llms.txt](https://oyabrowser.com/llms.txt).
64
+
53
65
  ## Portal automation: record once, replay with new inputs
54
66
 
55
67
  This example adapts the portal-automation project's workflow: reuse a persona, attach to an existing browser or start one, run a prompt the first time, then replay its saved playbook. The portal, workflow names, and request values below are fictional. Supply your own test portal and credentials through environment variables; adapt the task to its actual pages.
package/dist/index.cjs CHANGED
@@ -70,6 +70,9 @@ var Status = {
70
70
  /** A browser did not come up in time. */
71
71
  GATEWAY_TIMEOUT: 504
72
72
  };
73
+ var DESKTOP_PROVIDER = "oya-desktop";
74
+ var DESKTOP_TIMEOUT_MS = 3e5;
75
+ var DESKTOP_DOWNLOAD_URL = "https://oyabrowser.com/#download";
73
76
 
74
77
  // src/errors.ts
75
78
  var OyaError = class extends Error {
@@ -111,6 +114,18 @@ function savedConfig() {
111
114
  return {};
112
115
  }
113
116
  }
117
+ var PRIVATE_FILE = 384;
118
+ var JSON_INDENT = 2;
119
+ function saveKey(apiKey, baseUrl) {
120
+ const fs = builtin("node:fs");
121
+ const file2 = configFile();
122
+ if (!fs || !file2 || savedConfig().apiKey) return false;
123
+ fs.mkdirSync(file2.slice(0, file2.lastIndexOf("/")), { recursive: true });
124
+ fs.writeFileSync(file2, JSON.stringify({ ...savedConfig(), apiKey, baseUrl }, null, JSON_INDENT), {
125
+ mode: PRIVATE_FILE
126
+ });
127
+ return true;
128
+ }
114
129
 
115
130
  // src/client.ts
116
131
  var OPTIONS_ORIGIN = { apiKeyFrom: "the apiKey option", baseUrlFrom: "the baseUrl option", savedKey: false };
@@ -855,6 +870,80 @@ async function assertNotEnded(checks, id) {
855
870
  }
856
871
  }
857
872
 
873
+ // src/api/agent.ts
874
+ var NO_KEY = "signup";
875
+ var solves = (crypto, challenge, nonce, zeros) => crypto.createHash("sha256").update(challenge + nonce).digest("hex").startsWith(zeros);
876
+ function solve({ challenge, difficulty }) {
877
+ const crypto = builtin("node:crypto");
878
+ if (!crypto) throw new Error("Oya.signup() needs Node 22.3 or newer.");
879
+ const zeros = "0".repeat(difficulty);
880
+ let nonce = 0;
881
+ while (!solves(crypto, challenge, nonce, zeros)) nonce++;
882
+ return String(nonce);
883
+ }
884
+ async function signup(options) {
885
+ const http = createHttp({ baseUrl: options.baseUrl, fetch: options.fetch, apiKey: NO_KEY });
886
+ const puzzle = await http.request("GET", "/api/auth/agent/challenge");
887
+ const body = { email: options.email, challenge: puzzle.challenge, nonce: solve(puzzle) };
888
+ const answer = await http.request("POST", "/api/auth/agent/signup", body);
889
+ const saved = options.save !== false && saveKey(answer.api_key, http.baseUrl);
890
+ return { apiKey: answer.api_key, claimUrl: answer.claim_url, cloudBrowsers: answer.cloud_browsers, saved };
891
+ }
892
+ var desktopOf = (all) => all.find((b) => b.provider === DESKTOP_PROVIDER && b.health !== "dead");
893
+ async function pairingLink(http, persona) {
894
+ const { code } = await http.request("POST", "/api/pairing", {
895
+ persona
896
+ });
897
+ const server = `${http.baseUrl.replace(/^http/, "ws")}/ws`;
898
+ return `oya://connect?code=${encodeURIComponent(code)}&server=${encodeURIComponent(server)}`;
899
+ }
900
+ var OPENERS = {
901
+ darwin: ["open"],
902
+ linux: ["xdg-open"],
903
+ win32: ["rundll32", "url.dll,FileProtocolHandler"]
904
+ };
905
+ function openLink(link) {
906
+ const platform = globalThis.process?.platform ?? "";
907
+ const cp = builtin("node:child_process");
908
+ if (!cp || !Object.hasOwn(OPENERS, platform)) return;
909
+ const [cmd, ...args] = OPENERS[platform];
910
+ const child = cp.spawn(cmd, [...args, link], { detached: true, stdio: "ignore" });
911
+ child.on("error", () => {
912
+ });
913
+ child.unref();
914
+ }
915
+ async function waitForDesktop(list, link, timeoutMs) {
916
+ const deadline = Date.now() + timeoutMs;
917
+ while (Date.now() < deadline) {
918
+ const found = desktopOf(await list());
919
+ if (found) return found;
920
+ await new Promise((r) => setTimeout(r, READY_POLL_MS));
921
+ }
922
+ const how = `Install it from ${DESKTOP_DOWNLOAD_URL} if needed, then open ${link} and click Connect.`;
923
+ throw new OyaError(`The Oya desktop app did not connect. ${how}`, Status.GATEWAY_TIMEOUT, { link });
924
+ }
925
+ async function handleOn(http, id) {
926
+ const found = await http.request("GET", `/api/browsers/${segment(id)}`);
927
+ const started = { id, provider: DESKTOP_PROVIDER, persona: found.persona || "default", cdpUrl: found.cdpUrl };
928
+ return new Browser(http, { ...started, status: "ready", reused: true }, false);
929
+ }
930
+ var desktopApi = (http, list) => ({
931
+ /**
932
+ * The person's own desktop browser, signed in to their sites. When it is not
933
+ * connected yet this pairs it: it opens a link, the person clicks Connect in
934
+ * the app (with "Import my logins" ticked, their Chrome logins come along),
935
+ * and this waits until it is up.
936
+ */
937
+ connect: async (options = {}) => {
938
+ const running = desktopOf(await list());
939
+ if (running) return handleOn(http(), running.id);
940
+ const link = await pairingLink(http(), options.persona);
941
+ if (options.open !== false) openLink(link);
942
+ const found = await waitForDesktop(list, link, options.timeoutMs ?? DESKTOP_TIMEOUT_MS);
943
+ return handleOn(http(), found.id);
944
+ }
945
+ });
946
+
858
947
  // src/file.ts
859
948
  var MAX_FILE_BYTES = MAX_FILE_MB * BYTES_PER_MB;
860
949
  var NODE_FS = "node:fs/promises";
@@ -934,6 +1023,18 @@ var Oya = class {
934
1023
  () => this.http,
935
1024
  (id, timeoutMs) => this.waitUntilConnected(id, timeoutMs)
936
1025
  );
1026
+ /** The person's own desktop browser, with their logins: `await oya.desktop.connect()` pairs it when it is not up yet. */
1027
+ desktop = desktopApi(
1028
+ () => this.http,
1029
+ () => this.browser.list()
1030
+ );
1031
+ /**
1032
+ * An agent's own key, with no person or dashboard: `await Oya.signup({ email })`.
1033
+ * Saved to ~/.oya/config.json, so a later `new Oya()` finds it.
1034
+ */
1035
+ static signup(options) {
1036
+ return signup(options);
1037
+ }
937
1038
  /** Durable operational controls, including disconnected and cleanup-pending sessions. */
938
1039
  control = controlApi(() => this.http);
939
1040
  /** Playbooks saved with `browser.toPlaybook()`. */
package/dist/index.d.cts CHANGED
@@ -272,7 +272,9 @@ type LlmProvider = 'openai' | 'anthropic'
272
272
  | 'gemini'
273
273
  /** Gemini Enterprise, ex-Vertex AI. Express mode by default; set `openai_base_url` to a
274
274
  * project-scoped `.../endpoints/openapi` endpoint to use an enterprise project. */
275
- | 'vertex';
275
+ | 'vertex'
276
+ /** OpenRouter: one key for many vendors' models, named vendor/model (e.g. 'anthropic/claude-sonnet-5'). */
277
+ | 'openrouter';
276
278
  /**
277
279
  * Which runtime this key's `'oya-cloud'` browsers run on. `'docker'` and `'k8s'`
278
280
  * run on the deployment's own daemon or cluster, exactly as its operator set them up;
@@ -422,6 +424,30 @@ interface Config extends Omit<ConfigUpdate, 'llm_provider' | 'browser_provider'
422
424
  /** Whether those fields are set. */
423
425
  configured: boolean;
424
426
  }>;
427
+ /** Every LLM provider and the models it offers; the console, desktop app and CLI build their pickers from this. */
428
+ llm_catalog: LlmCatalogEntry[];
429
+ }
430
+ /** One LLM provider in `Config.llm_catalog`. */
431
+ interface LlmCatalogEntry {
432
+ /** The `llm_provider` value. */
433
+ id: LlmProvider;
434
+ /** Its name. */
435
+ label: string;
436
+ /** How its key looks. */
437
+ hint: string;
438
+ /** Where to get a key. */
439
+ keysUrl: string;
440
+ /** Its endpoint. */
441
+ base: string;
442
+ /** The model a key runs on when it sets no `chat_model`. */
443
+ model: string;
444
+ /** The models to offer (OpenRouter's is its live list); any other id works as `chat_model` too. */
445
+ models: Array<{
446
+ /** The id the provider's API expects. */
447
+ id: string;
448
+ /** A readable name. */
449
+ label: string;
450
+ }>;
425
451
  }
426
452
 
427
453
  /** A failed API call or browser command. */
@@ -918,6 +944,37 @@ interface OyaOptions {
918
944
  /** A fetch to use instead of the global one, for tests, proxies or old runtimes. */
919
945
  fetch?: typeof globalThis.fetch;
920
946
  }
947
+ /** What `Oya.signup()` takes. */
948
+ interface SignupOptions {
949
+ /** The email of the person the agent works for. They get the claim link that unlocks cloud browsers. */
950
+ email: string;
951
+ /** Defaults to OYA_BASE_URL, then https://oyabrowser.com. */
952
+ baseUrl?: string;
953
+ /** Save the key to ~/.oya/config.json, where `new Oya()` finds it later. On by default; a key already saved there is never replaced. */
954
+ save?: boolean;
955
+ /** A fetch to use instead of the global one. */
956
+ fetch?: typeof globalThis.fetch;
957
+ }
958
+ /** A new key from `Oya.signup()`. */
959
+ interface Signup {
960
+ /** The key. Shown once; keep it secret. */
961
+ apiKey: string;
962
+ /** Send this to the person: opening it while signed in to Oya claims the key and unlocks cloud browsers. */
963
+ claimUrl: string;
964
+ /** Whether cloud browsers work yet (false until the key is claimed). */
965
+ cloudBrowsers: boolean;
966
+ /** Whether the key was saved to ~/.oya/config.json. */
967
+ saved: boolean;
968
+ }
969
+ /** What `oya.desktop.connect()` takes. */
970
+ interface DesktopOptions {
971
+ /** The persona the desktop app signs in as. Defaults to the key's own. */
972
+ persona?: string;
973
+ /** Open the pairing link on this machine (the default). Off, the link is only in the error when the app does not connect. */
974
+ open?: boolean;
975
+ /** How long to wait for the person to click Connect in the app. Five minutes by default. */
976
+ timeoutMs?: number;
977
+ }
921
978
 
922
979
  /** Where the key and the address came from, so a message names the one to fix. */
923
980
  interface Origin {
@@ -1412,6 +1469,15 @@ declare class Oya {
1412
1469
  stop: (ids: string[] | "all") => Promise<StopManyResult>;
1413
1470
  stopAll: () => Promise<number>;
1414
1471
  };
1472
+ /** The person's own desktop browser, with their logins: `await oya.desktop.connect()` pairs it when it is not up yet. */
1473
+ readonly desktop: {
1474
+ connect: (options?: DesktopOptions) => Promise<Browser>;
1475
+ };
1476
+ /**
1477
+ * An agent's own key, with no person or dashboard: `await Oya.signup({ email })`.
1478
+ * Saved to ~/.oya/config.json, so a later `new Oya()` finds it.
1479
+ */
1480
+ static signup(options: SignupOptions): Promise<Signup>;
1415
1481
  /** Durable operational controls, including disconnected and cleanup-pending sessions. */
1416
1482
  readonly control: {
1417
1483
  createWebhook: (url: string, types?: string[]) => Promise<Webhook>;
@@ -1513,4 +1579,4 @@ declare class Oya {
1513
1579
  private waitUntilConnected;
1514
1580
  }
1515
1581
 
1516
- export { type Activity, type Analysis, type AnalyzeOptions, type AttentionRequest, type Block, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type CaptchaSolver, type Config, type ConfigUpdate, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Cookie, type CookieFormat, type EcsAuth, type EcsConfig, type Element, type FileValue, type Fingerprint, type Health, type HumanInputAction, type LlmProvider, MAX_FILE_BYTES, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PageFormat, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, type RecoverOptions, Run, type RunData, type RunInfo, type RunResult, type SandboxRuntime, type SiteCredentials, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default, file };
1582
+ export { type Activity, type Analysis, type AnalyzeOptions, type AttentionRequest, type Block, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type CaptchaSolver, type Config, type ConfigUpdate, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Cookie, type CookieFormat, type DesktopOptions, type EcsAuth, type EcsConfig, type Element, type FileValue, type Fingerprint, type Health, type HumanInputAction, type LlmCatalogEntry, type LlmProvider, MAX_FILE_BYTES, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PageFormat, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, type RecoverOptions, Run, type RunData, type RunInfo, type RunResult, type SandboxRuntime, type Signup, type SignupOptions, type SiteCredentials, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default, file };
package/dist/index.d.ts CHANGED
@@ -272,7 +272,9 @@ type LlmProvider = 'openai' | 'anthropic'
272
272
  | 'gemini'
273
273
  /** Gemini Enterprise, ex-Vertex AI. Express mode by default; set `openai_base_url` to a
274
274
  * project-scoped `.../endpoints/openapi` endpoint to use an enterprise project. */
275
- | 'vertex';
275
+ | 'vertex'
276
+ /** OpenRouter: one key for many vendors' models, named vendor/model (e.g. 'anthropic/claude-sonnet-5'). */
277
+ | 'openrouter';
276
278
  /**
277
279
  * Which runtime this key's `'oya-cloud'` browsers run on. `'docker'` and `'k8s'`
278
280
  * run on the deployment's own daemon or cluster, exactly as its operator set them up;
@@ -422,6 +424,30 @@ interface Config extends Omit<ConfigUpdate, 'llm_provider' | 'browser_provider'
422
424
  /** Whether those fields are set. */
423
425
  configured: boolean;
424
426
  }>;
427
+ /** Every LLM provider and the models it offers; the console, desktop app and CLI build their pickers from this. */
428
+ llm_catalog: LlmCatalogEntry[];
429
+ }
430
+ /** One LLM provider in `Config.llm_catalog`. */
431
+ interface LlmCatalogEntry {
432
+ /** The `llm_provider` value. */
433
+ id: LlmProvider;
434
+ /** Its name. */
435
+ label: string;
436
+ /** How its key looks. */
437
+ hint: string;
438
+ /** Where to get a key. */
439
+ keysUrl: string;
440
+ /** Its endpoint. */
441
+ base: string;
442
+ /** The model a key runs on when it sets no `chat_model`. */
443
+ model: string;
444
+ /** The models to offer (OpenRouter's is its live list); any other id works as `chat_model` too. */
445
+ models: Array<{
446
+ /** The id the provider's API expects. */
447
+ id: string;
448
+ /** A readable name. */
449
+ label: string;
450
+ }>;
425
451
  }
426
452
 
427
453
  /** A failed API call or browser command. */
@@ -918,6 +944,37 @@ interface OyaOptions {
918
944
  /** A fetch to use instead of the global one, for tests, proxies or old runtimes. */
919
945
  fetch?: typeof globalThis.fetch;
920
946
  }
947
+ /** What `Oya.signup()` takes. */
948
+ interface SignupOptions {
949
+ /** The email of the person the agent works for. They get the claim link that unlocks cloud browsers. */
950
+ email: string;
951
+ /** Defaults to OYA_BASE_URL, then https://oyabrowser.com. */
952
+ baseUrl?: string;
953
+ /** Save the key to ~/.oya/config.json, where `new Oya()` finds it later. On by default; a key already saved there is never replaced. */
954
+ save?: boolean;
955
+ /** A fetch to use instead of the global one. */
956
+ fetch?: typeof globalThis.fetch;
957
+ }
958
+ /** A new key from `Oya.signup()`. */
959
+ interface Signup {
960
+ /** The key. Shown once; keep it secret. */
961
+ apiKey: string;
962
+ /** Send this to the person: opening it while signed in to Oya claims the key and unlocks cloud browsers. */
963
+ claimUrl: string;
964
+ /** Whether cloud browsers work yet (false until the key is claimed). */
965
+ cloudBrowsers: boolean;
966
+ /** Whether the key was saved to ~/.oya/config.json. */
967
+ saved: boolean;
968
+ }
969
+ /** What `oya.desktop.connect()` takes. */
970
+ interface DesktopOptions {
971
+ /** The persona the desktop app signs in as. Defaults to the key's own. */
972
+ persona?: string;
973
+ /** Open the pairing link on this machine (the default). Off, the link is only in the error when the app does not connect. */
974
+ open?: boolean;
975
+ /** How long to wait for the person to click Connect in the app. Five minutes by default. */
976
+ timeoutMs?: number;
977
+ }
921
978
 
922
979
  /** Where the key and the address came from, so a message names the one to fix. */
923
980
  interface Origin {
@@ -1412,6 +1469,15 @@ declare class Oya {
1412
1469
  stop: (ids: string[] | "all") => Promise<StopManyResult>;
1413
1470
  stopAll: () => Promise<number>;
1414
1471
  };
1472
+ /** The person's own desktop browser, with their logins: `await oya.desktop.connect()` pairs it when it is not up yet. */
1473
+ readonly desktop: {
1474
+ connect: (options?: DesktopOptions) => Promise<Browser>;
1475
+ };
1476
+ /**
1477
+ * An agent's own key, with no person or dashboard: `await Oya.signup({ email })`.
1478
+ * Saved to ~/.oya/config.json, so a later `new Oya()` finds it.
1479
+ */
1480
+ static signup(options: SignupOptions): Promise<Signup>;
1415
1481
  /** Durable operational controls, including disconnected and cleanup-pending sessions. */
1416
1482
  readonly control: {
1417
1483
  createWebhook: (url: string, types?: string[]) => Promise<Webhook>;
@@ -1513,4 +1579,4 @@ declare class Oya {
1513
1579
  private waitUntilConnected;
1514
1580
  }
1515
1581
 
1516
- export { type Activity, type Analysis, type AnalyzeOptions, type AttentionRequest, type Block, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type CaptchaSolver, type Config, type ConfigUpdate, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Cookie, type CookieFormat, type EcsAuth, type EcsConfig, type Element, type FileValue, type Fingerprint, type Health, type HumanInputAction, type LlmProvider, MAX_FILE_BYTES, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PageFormat, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, type RecoverOptions, Run, type RunData, type RunInfo, type RunResult, type SandboxRuntime, type SiteCredentials, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default, file };
1582
+ export { type Activity, type Analysis, type AnalyzeOptions, type AttentionRequest, type Block, Browser, type BrowserDetail, type BrowserInfo, type CaptchaResult, type CaptchaSolver, type Config, type ConfigUpdate, type ControlCredential, type ControlEvent, type ControlOverview, type ControlRole, type ControlSession, type Cookie, type CookieFormat, type DesktopOptions, type EcsAuth, type EcsConfig, type Element, type FileValue, type Fingerprint, type Health, type HumanInputAction, type LlmCatalogEntry, type LlmProvider, MAX_FILE_BYTES, type MfaConfig, type MfaResult, Oya, OyaError, type OyaOptions, type PageFormat, type PersonaInfo, type PersonaPrefs, type PlayResult, type Playbook, type PlaybookSummary, type ProjectSettings, type Provider, type ProxyCreate, type ProxyInfo, type RecoverOptions, Run, type RunData, type RunInfo, type RunResult, type SandboxRuntime, type Signup, type SignupOptions, type SiteCredentials, type StartOptions, type StartResult, type StopResult, type SubmitOptions, Oya as default, file };
package/dist/index.js CHANGED
@@ -38,6 +38,9 @@ var Status = {
38
38
  /** A browser did not come up in time. */
39
39
  GATEWAY_TIMEOUT: 504
40
40
  };
41
+ var DESKTOP_PROVIDER = "oya-desktop";
42
+ var DESKTOP_TIMEOUT_MS = 3e5;
43
+ var DESKTOP_DOWNLOAD_URL = "https://oyabrowser.com/#download";
41
44
 
42
45
  // src/errors.ts
43
46
  var OyaError = class extends Error {
@@ -79,6 +82,18 @@ function savedConfig() {
79
82
  return {};
80
83
  }
81
84
  }
85
+ var PRIVATE_FILE = 384;
86
+ var JSON_INDENT = 2;
87
+ function saveKey(apiKey, baseUrl) {
88
+ const fs = builtin("node:fs");
89
+ const file2 = configFile();
90
+ if (!fs || !file2 || savedConfig().apiKey) return false;
91
+ fs.mkdirSync(file2.slice(0, file2.lastIndexOf("/")), { recursive: true });
92
+ fs.writeFileSync(file2, JSON.stringify({ ...savedConfig(), apiKey, baseUrl }, null, JSON_INDENT), {
93
+ mode: PRIVATE_FILE
94
+ });
95
+ return true;
96
+ }
82
97
 
83
98
  // src/client.ts
84
99
  var OPTIONS_ORIGIN = { apiKeyFrom: "the apiKey option", baseUrlFrom: "the baseUrl option", savedKey: false };
@@ -823,6 +838,80 @@ async function assertNotEnded(checks, id) {
823
838
  }
824
839
  }
825
840
 
841
+ // src/api/agent.ts
842
+ var NO_KEY = "signup";
843
+ var solves = (crypto, challenge, nonce, zeros) => crypto.createHash("sha256").update(challenge + nonce).digest("hex").startsWith(zeros);
844
+ function solve({ challenge, difficulty }) {
845
+ const crypto = builtin("node:crypto");
846
+ if (!crypto) throw new Error("Oya.signup() needs Node 22.3 or newer.");
847
+ const zeros = "0".repeat(difficulty);
848
+ let nonce = 0;
849
+ while (!solves(crypto, challenge, nonce, zeros)) nonce++;
850
+ return String(nonce);
851
+ }
852
+ async function signup(options) {
853
+ const http = createHttp({ baseUrl: options.baseUrl, fetch: options.fetch, apiKey: NO_KEY });
854
+ const puzzle = await http.request("GET", "/api/auth/agent/challenge");
855
+ const body = { email: options.email, challenge: puzzle.challenge, nonce: solve(puzzle) };
856
+ const answer = await http.request("POST", "/api/auth/agent/signup", body);
857
+ const saved = options.save !== false && saveKey(answer.api_key, http.baseUrl);
858
+ return { apiKey: answer.api_key, claimUrl: answer.claim_url, cloudBrowsers: answer.cloud_browsers, saved };
859
+ }
860
+ var desktopOf = (all) => all.find((b) => b.provider === DESKTOP_PROVIDER && b.health !== "dead");
861
+ async function pairingLink(http, persona) {
862
+ const { code } = await http.request("POST", "/api/pairing", {
863
+ persona
864
+ });
865
+ const server = `${http.baseUrl.replace(/^http/, "ws")}/ws`;
866
+ return `oya://connect?code=${encodeURIComponent(code)}&server=${encodeURIComponent(server)}`;
867
+ }
868
+ var OPENERS = {
869
+ darwin: ["open"],
870
+ linux: ["xdg-open"],
871
+ win32: ["rundll32", "url.dll,FileProtocolHandler"]
872
+ };
873
+ function openLink(link) {
874
+ const platform = globalThis.process?.platform ?? "";
875
+ const cp = builtin("node:child_process");
876
+ if (!cp || !Object.hasOwn(OPENERS, platform)) return;
877
+ const [cmd, ...args] = OPENERS[platform];
878
+ const child = cp.spawn(cmd, [...args, link], { detached: true, stdio: "ignore" });
879
+ child.on("error", () => {
880
+ });
881
+ child.unref();
882
+ }
883
+ async function waitForDesktop(list, link, timeoutMs) {
884
+ const deadline = Date.now() + timeoutMs;
885
+ while (Date.now() < deadline) {
886
+ const found = desktopOf(await list());
887
+ if (found) return found;
888
+ await new Promise((r) => setTimeout(r, READY_POLL_MS));
889
+ }
890
+ const how = `Install it from ${DESKTOP_DOWNLOAD_URL} if needed, then open ${link} and click Connect.`;
891
+ throw new OyaError(`The Oya desktop app did not connect. ${how}`, Status.GATEWAY_TIMEOUT, { link });
892
+ }
893
+ async function handleOn(http, id) {
894
+ const found = await http.request("GET", `/api/browsers/${segment(id)}`);
895
+ const started = { id, provider: DESKTOP_PROVIDER, persona: found.persona || "default", cdpUrl: found.cdpUrl };
896
+ return new Browser(http, { ...started, status: "ready", reused: true }, false);
897
+ }
898
+ var desktopApi = (http, list) => ({
899
+ /**
900
+ * The person's own desktop browser, signed in to their sites. When it is not
901
+ * connected yet this pairs it: it opens a link, the person clicks Connect in
902
+ * the app (with "Import my logins" ticked, their Chrome logins come along),
903
+ * and this waits until it is up.
904
+ */
905
+ connect: async (options = {}) => {
906
+ const running = desktopOf(await list());
907
+ if (running) return handleOn(http(), running.id);
908
+ const link = await pairingLink(http(), options.persona);
909
+ if (options.open !== false) openLink(link);
910
+ const found = await waitForDesktop(list, link, options.timeoutMs ?? DESKTOP_TIMEOUT_MS);
911
+ return handleOn(http(), found.id);
912
+ }
913
+ });
914
+
826
915
  // src/file.ts
827
916
  var MAX_FILE_BYTES = MAX_FILE_MB * BYTES_PER_MB;
828
917
  var NODE_FS = "node:fs/promises";
@@ -902,6 +991,18 @@ var Oya = class {
902
991
  () => this.http,
903
992
  (id, timeoutMs) => this.waitUntilConnected(id, timeoutMs)
904
993
  );
994
+ /** The person's own desktop browser, with their logins: `await oya.desktop.connect()` pairs it when it is not up yet. */
995
+ desktop = desktopApi(
996
+ () => this.http,
997
+ () => this.browser.list()
998
+ );
999
+ /**
1000
+ * An agent's own key, with no person or dashboard: `await Oya.signup({ email })`.
1001
+ * Saved to ~/.oya/config.json, so a later `new Oya()` finds it.
1002
+ */
1003
+ static signup(options) {
1004
+ return signup(options);
1005
+ }
905
1006
  /** Durable operational controls, including disconnected and cleanup-pending sessions. */
906
1007
  control = controlApi(() => this.http);
907
1008
  /** Playbooks saved with `browser.toPlaybook()`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oya-ai/browser",
3
- "version": "1.0.125",
3
+ "version": "1.0.127",
4
4
  "description": "Rotate thousands of browsers behind one API, personas, proxies, stealth, CAPTCHA and MFA.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://oyabrowser.com",
@@ -22,7 +22,18 @@
22
22
  "browser-use",
23
23
  "captcha",
24
24
  "fingerprint",
25
- "mcp"
25
+ "mcp",
26
+ "ai-agent",
27
+ "agent-browser",
28
+ "browser-agent",
29
+ "agent-skills",
30
+ "claude-code",
31
+ "codex",
32
+ "openclaw",
33
+ "hermes-agent",
34
+ "computer-use",
35
+ "mcp-server",
36
+ "llms-txt"
26
37
  ],
27
38
  "type": "module",
28
39
  "main": "./dist/index.cjs",