unbrowse 12.1.0 → 12.1.2

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 (2) hide show
  1. package/dist/cli.js +82 -4
  2. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -98,6 +98,43 @@ class Unbrowse {
98
98
  cancel(runId) {
99
99
  return this.req(`/runs/${runId}/cancel`, { method: "POST" });
100
100
  }
101
+ async runOnClient(request, opts = {}) {
102
+ const send = opts.fetch ?? ((...a) => globalThis.fetch(...a));
103
+ const deadline = Date.now() + (opts.timeoutMs ?? 600000);
104
+ let step = await this.run({ ...request, egress: "client" });
105
+ while (isEgressStep(step)) {
106
+ if (Date.now() > deadline) {
107
+ await this.closeEgress(step.egressId).catch(() => {
108
+ return;
109
+ });
110
+ throw new UnbrowseError("client-egress run did not finish in time", 408, "timeout");
111
+ }
112
+ const { egressId } = step;
113
+ for (const req of step.requests) {
114
+ let answer;
115
+ try {
116
+ if (await opts.onRequest?.(req) === false)
117
+ throw new Error("refused by onRequest");
118
+ answer = { response: await sendEgress(send, req) };
119
+ } catch (err) {
120
+ answer = { error: err instanceof Error ? err.message : String(err) };
121
+ }
122
+ step = await this.answerEgress(egressId, req.id, answer);
123
+ if (!isEgressStep(step))
124
+ break;
125
+ }
126
+ }
127
+ return step;
128
+ }
129
+ egress(egressId) {
130
+ return this.req(`/egress/${egressId}`);
131
+ }
132
+ answerEgress(egressId, requestId, answer) {
133
+ return this.post(`/egress/${egressId}`, { requestId, ...answer });
134
+ }
135
+ closeEgress(egressId) {
136
+ return this.req(`/egress/${egressId}`, { method: "DELETE" });
137
+ }
101
138
  discover(query) {
102
139
  return this.post("/capabilities/search", { query });
103
140
  }
@@ -159,6 +196,32 @@ function normalizeHost(site) {
159
196
  host = new URL(host).hostname;
160
197
  return host.replace(/\/.*$/, "").replace(/^www\./, "");
161
198
  }
199
+ function isEgressStep(v) {
200
+ return !!v && typeof v === "object" && v.status === "egress_required";
201
+ }
202
+ async function sendEgress(send, req) {
203
+ const body = req.body === undefined ? undefined : req.bodyEncoding === "base64" ? base64ToBytes(req.body) : req.body;
204
+ const res = await send(req.url, { method: req.method, headers: req.headers, redirect: req.redirect, ...body !== undefined ? { body } : {} });
205
+ const headers = [];
206
+ res.headers.forEach((value, name) => headers.push([name, value]));
207
+ const cookies = res.headers.getSetCookie?.();
208
+ const pairs = cookies?.length ? [...headers.filter(([n]) => n.toLowerCase() !== "set-cookie"), ...cookies.map((c) => ["set-cookie", c])] : headers;
209
+ const bytes = new Uint8Array(await res.arrayBuffer());
210
+ return { status: res.status, headers: pairs, body: bytesToBase64(bytes), bodyEncoding: "base64", ...res.url ? { url: res.url } : {} };
211
+ }
212
+ function base64ToBytes(b64) {
213
+ const bin = atob(b64);
214
+ const out = new Uint8Array(bin.length);
215
+ for (let i = 0;i < bin.length; i++)
216
+ out[i] = bin.charCodeAt(i);
217
+ return out;
218
+ }
219
+ function bytesToBase64(bytes) {
220
+ let bin = "";
221
+ for (let i = 0;i < bytes.length; i += 32768)
222
+ bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
223
+ return btoa(bin);
224
+ }
162
225
  var DEFAULT_BASE_URL = "https://unbrowse.ai/api/v1", UnbrowseError, env = (name) => (typeof process !== "undefined" ? process.env?.[name] : undefined) || undefined;
163
226
  var init_client = __esm(() => {
164
227
  UnbrowseError = class UnbrowseError extends Error {
@@ -377,6 +440,16 @@ function save(stored) {
377
440
  function clear() {
378
441
  rmSync(configPath(), { force: true });
379
442
  }
443
+ function v1Key(baseUrl) {
444
+ if (!/^https:\/\/(www\.|api\.|v3\.)?unbrowse\.ai\/?$/.test(baseUrl))
445
+ return;
446
+ try {
447
+ const key = JSON.parse(readFileSync(join(process.env.HOME || homedir(), ".unbrowse", "config.json"), "utf8")).api_key;
448
+ return typeof key === "string" && /^ubr_[0-9a-f]{48}$/.test(key) ? key : undefined;
449
+ } catch {
450
+ return;
451
+ }
452
+ }
380
453
  async function currentToken(baseUrl, fetchImpl = fetch) {
381
454
  if (process.env.UNBROWSE_API_KEY)
382
455
  return process.env.UNBROWSE_API_KEY;
@@ -387,7 +460,7 @@ async function currentToken(baseUrl, fetchImpl = fetch) {
387
460
  return stored.apiKey;
388
461
  const o = stored.oauth;
389
462
  if (!o)
390
- return;
463
+ return v1Key(baseUrl);
391
464
  if (o.expiresAt && o.refreshToken && Date.now() > o.expiresAt - 60000) {
392
465
  try {
393
466
  const fresh = await tokenRequest(baseUrl, { grant_type: "refresh_token", refresh_token: o.refreshToken, client_id: o.clientId, resource: apiResource(baseUrl) }, fetchImpl);
@@ -409,6 +482,9 @@ function describe(baseUrl) {
409
482
  return `API key ${mask(stored.apiKey)} in ${configPath()}`;
410
483
  if (stored.oauth)
411
484
  return `OAuth sign-in in ${configPath()}`;
485
+ const v1 = !stored.baseUrl ? v1Key(baseUrl) : undefined;
486
+ if (v1)
487
+ return `v1 API key ${mask(v1)} from ~/.unbrowse/config.json (still works; \`unbrowse login\` replaces it)`;
412
488
  return "not signed in";
413
489
  }
414
490
  function mask(key) {
@@ -493,7 +569,7 @@ ${url.toString()}`);
493
569
 
494
570
  // src/cli.ts
495
571
  init_src();
496
- var VERSION = "12.1.0";
572
+ var VERSION = "12.1.2";
497
573
  var DEFAULT_ORIGIN = "https://unbrowse.ai";
498
574
  var HELP = `unbrowse ${VERSION} — call websites as APIs through the Unbrowse API
499
575
 
@@ -505,6 +581,7 @@ var HELP = `unbrowse ${VERSION} — call websites as APIs through the Unbrowse A
505
581
  discover <query> Your capabilities first, then the public registry
506
582
  run <task…> Run a task and wait for a verified result
507
583
  [--capability ID] [--url URL] [--set key=value]… [--input JSON] [--unattended] [--no-wait]
584
+ [--from-here] send the site requests from this machine (your IP); Unbrowse decides and reads them
508
585
  inspect <runId> A run's status, requirements and result
509
586
  resume <runId> key=value… Answer open requirements on the same run
510
587
  cancel <runId> Stop a run; prints the effect receipt
@@ -609,14 +686,15 @@ async function main(argv, io = stdio) {
609
686
  if (!task && !capability)
610
687
  throw new UsageError("usage: unbrowse run <task…> | --capability ID");
611
688
  const input = { ...flag("input") ? JSON.parse(flag("input")) : {}, ...mapValues(args.sets) };
612
- const view = await ub.run({
689
+ const request = {
613
690
  ...task ? { task } : {},
614
691
  ...capability ? { capability } : {},
615
692
  ...flag("url") ? { targetUrl: flag("url") } : {},
616
693
  ...Object.keys(input).length ? { input } : {},
617
694
  ...args.flags.unattended ? { interactionMode: "unattended" } : {},
618
695
  idempotencyKey: flag("idempotency-key") ?? randomUUID()
619
- });
696
+ };
697
+ const view = args.flags["from-here"] ? await ub.runOnClient(request, { onRequest: (r) => void io.err(`→ ${r.method} ${r.url}`) }) : await ub.run(request);
620
698
  return settle(ub, view, args, io, print);
621
699
  }
622
700
  case "inspect":
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "unbrowse",
3
- "version": "12.1.0",
3
+ "version": "12.1.2",
4
+ "mcpName": "io.github.unbrowse-ai/unbrowse",
4
5
  "description": "Call websites as APIs: a thin CLI over the Unbrowse API.",
5
6
  "license": "MIT",
6
7
  "type": "module",