unbrowse 12.1.0 → 12.1.1

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 +68 -3
  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 {
@@ -493,7 +556,7 @@ ${url.toString()}`);
493
556
 
494
557
  // src/cli.ts
495
558
  init_src();
496
- var VERSION = "12.1.0";
559
+ var VERSION = "12.1.1";
497
560
  var DEFAULT_ORIGIN = "https://unbrowse.ai";
498
561
  var HELP = `unbrowse ${VERSION} — call websites as APIs through the Unbrowse API
499
562
 
@@ -505,6 +568,7 @@ var HELP = `unbrowse ${VERSION} — call websites as APIs through the Unbrowse A
505
568
  discover <query> Your capabilities first, then the public registry
506
569
  run <task…> Run a task and wait for a verified result
507
570
  [--capability ID] [--url URL] [--set key=value]… [--input JSON] [--unattended] [--no-wait]
571
+ [--from-here] send the site requests from this machine (your IP); Unbrowse decides and reads them
508
572
  inspect <runId> A run's status, requirements and result
509
573
  resume <runId> key=value… Answer open requirements on the same run
510
574
  cancel <runId> Stop a run; prints the effect receipt
@@ -609,14 +673,15 @@ async function main(argv, io = stdio) {
609
673
  if (!task && !capability)
610
674
  throw new UsageError("usage: unbrowse run <task…> | --capability ID");
611
675
  const input = { ...flag("input") ? JSON.parse(flag("input")) : {}, ...mapValues(args.sets) };
612
- const view = await ub.run({
676
+ const request = {
613
677
  ...task ? { task } : {},
614
678
  ...capability ? { capability } : {},
615
679
  ...flag("url") ? { targetUrl: flag("url") } : {},
616
680
  ...Object.keys(input).length ? { input } : {},
617
681
  ...args.flags.unattended ? { interactionMode: "unattended" } : {},
618
682
  idempotencyKey: flag("idempotency-key") ?? randomUUID()
619
- });
683
+ };
684
+ const view = args.flags["from-here"] ? await ub.runOnClient(request, { onRequest: (r) => void io.err(`→ ${r.method} ${r.url}`) }) : await ub.run(request);
620
685
  return settle(ub, view, args, io, print);
621
686
  }
622
687
  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.1",
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",