unbrowse 3.3.4 → 3.4.0

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/dist/cli.js CHANGED
@@ -31,7 +31,7 @@ var __promiseAll = (args) => Promise.all(args);
31
31
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
32
32
 
33
33
  // ../../src/build-info.generated.ts
34
- var BUILD_RELEASE_VERSION = "3.3.4", BUILD_GIT_SHA = "d398ad6f1a60", BUILD_CODE_HASH = "d6e5ef2546cd", BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy4zLjQiLCJnaXRfc2hhIjoiZDM5OGFkNmYxYTYwIiwiY29kZV9oYXNoIjoiZDZlNWVmMjU0NmNkIiwidHJhY2VfdmVyc2lvbiI6ImQ2ZTVlZjI1NDZjZEBkMzk4YWQ2ZjFhNjAiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA3VDA2OjQxOjAxLjcwNVoifQ", BUILD_RELEASE_MANIFEST_SIGNATURE = "TaaK5qrudsUghz2Lc3jQCjMDbn6-mbEul9OwGz6J6WA", BUILD_DEFAULT_BACKEND_URL = "https://beta-api.unbrowse.ai";
34
+ var BUILD_RELEASE_VERSION = "3.4.0", BUILD_GIT_SHA = "361b3d271f3d", BUILD_CODE_HASH = "656382fbb5d5", BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy40LjAiLCJnaXRfc2hhIjoiMzYxYjNkMjcxZjNkIiwiY29kZV9oYXNoIjoiNjU2MzgyZmJiNWQ1IiwidHJhY2VfdmVyc2lvbiI6IjY1NjM4MmZiYjVkNUAzNjFiM2QyNzFmM2QiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA5VDAyOjUwOjI0LjI0NVoifQ", BUILD_RELEASE_MANIFEST_SIGNATURE = "_GYe4ccws1jOZQ13TD27_rBJwKd87JDzsXDQLQR3mZU", BUILD_DEFAULT_BACKEND_URL = "https://beta-api.unbrowse.ai";
35
35
 
36
36
  // ../../src/version.ts
37
37
  import { createHash } from "crypto";
@@ -753,9 +753,16 @@ var init_reverse_engineer = __esm(() => {
753
753
  "sec-ch-ua",
754
754
  "sec-ch-ua-mobile",
755
755
  "sec-ch-ua-platform",
756
+ "sec-ch-ua-full-version-list",
757
+ "sec-ch-ua-arch",
758
+ "sec-ch-ua-bitness",
759
+ "sec-ch-ua-model",
760
+ "sec-ch-ua-wow64",
756
761
  "sec-fetch-dest",
757
762
  "sec-fetch-mode",
758
763
  "sec-fetch-site",
764
+ "sec-fetch-user",
765
+ "upgrade-insecure-requests",
759
766
  "x-requested-with"
760
767
  ]);
761
768
  AD_SCHEMA_KEYS = new Set([
@@ -1383,6 +1390,7 @@ import { join as join4 } from "path";
1383
1390
  import { homedir as homedir3, hostname, release as osRelease } from "os";
1384
1391
  import { randomBytes, createHash as createHash2 } from "crypto";
1385
1392
  import { createInterface } from "readline";
1393
+ import { execSync } from "child_process";
1386
1394
  var API_URL = process.env.UNBROWSE_BACKEND_URL || DEFAULT_BACKEND_URL;
1387
1395
  var PROFILE_NAME = sanitizeProfileName(process.env.UNBROWSE_PROFILE ?? "");
1388
1396
  var recentLocalSkills = new Map;
@@ -1746,6 +1754,30 @@ async function apiRequest(method, path, body, opts) {
1746
1754
  console.warn("[unbrowse] Please restart the unbrowse service to accept the new terms.");
1747
1755
  throw new Error("ToS update required. Restart unbrowse to accept new terms.");
1748
1756
  }
1757
+ if (res.status === 426 && !opts?.skipAutoUpdate) {
1758
+ const errCode = data.error;
1759
+ if (errCode === "client_update_required" || errCode === "client_verification_failed") {
1760
+ console.warn(`
1761
+ [unbrowse] Server requires a client update (${errCode}).`);
1762
+ console.warn("[unbrowse] Attempting automatic update...");
1763
+ try {
1764
+ const updateCmd = process.env.UNBROWSE_UPDATE_COMMAND || "curl -fsSL https://unbrowse.ai/install.sh | bash";
1765
+ execSync(updateCmd, { stdio: "inherit", timeout: 120000 });
1766
+ console.warn("[unbrowse] Update installed. Restarting server...");
1767
+ try {
1768
+ execSync("pkill -9 -f 'unbrowse|kuri'", { stdio: "ignore", timeout: 5000 });
1769
+ } catch {}
1770
+ await new Promise((r) => setTimeout(r, 2000));
1771
+ console.warn("[unbrowse] Retrying request with updated client...");
1772
+ return apiRequest(method, path, body, { ...opts, skipAutoUpdate: true });
1773
+ } catch (updateErr) {
1774
+ console.warn(`[unbrowse] Auto-update failed: ${updateErr.message}`);
1775
+ const cmd = data.update_command ?? "curl -fsSL https://unbrowse.ai/install.sh | bash";
1776
+ console.warn(`[unbrowse] Please update manually: ${cmd}`);
1777
+ throw new Error(`Client update required. Run: ${cmd}`);
1778
+ }
1779
+ }
1780
+ }
1749
1781
  if (res.status === 402) {
1750
1782
  const paymentRequired = res.headers.get("PAYMENT-REQUIRED");
1751
1783
  const legacyPaymentTerms = res.headers.get("X-Payment-Required");
@@ -3629,6 +3661,53 @@ async function cmdExecute(flags) {
3629
3661
  endpoint_id: typeof flags.endpoint === "string" ? flags.endpoint : null
3630
3662
  }
3631
3663
  });
3664
+ if (flags.curl) {
3665
+ const endpointId = typeof flags.endpoint === "string" ? flags.endpoint : undefined;
3666
+ if (!endpointId)
3667
+ die("--curl requires --endpoint");
3668
+ const skill = await api2("GET", `/v1/skills/${skillId}`);
3669
+ const ep = (skill.endpoints ?? []).find((e) => e.endpoint_id === endpointId);
3670
+ if (!ep)
3671
+ die(`Endpoint ${endpointId} not found`);
3672
+ let method = ep.method ?? "GET";
3673
+ let url = ep.url_template;
3674
+ let headers = { ...ep.headers_template ?? {} };
3675
+ let body = ep.body;
3676
+ try {
3677
+ const preview = await api2("GET", `/v1/skills/${skillId}/request-preview/${endpointId}`);
3678
+ if (preview.headers)
3679
+ headers = preview.headers;
3680
+ if (preview.body)
3681
+ body = preview.body;
3682
+ if (preview.url)
3683
+ url = preview.url;
3684
+ if (preview.method)
3685
+ method = preview.method;
3686
+ } catch {}
3687
+ const parts = ["curl"];
3688
+ if (method !== "GET")
3689
+ parts.push(`-X ${method}`);
3690
+ parts.push(`'${url}'`);
3691
+ for (const [k, v] of Object.entries(headers)) {
3692
+ if (/^(newrelic|traceparent|tracestate)$/i.test(k))
3693
+ continue;
3694
+ parts.push(`-H '${k}: ${v}'`);
3695
+ }
3696
+ if (body) {
3697
+ if (!headers["content-type"] && !headers["Content-Type"])
3698
+ parts.push(`-H 'Content-Type: application/json'`);
3699
+ parts.push(`-d '${JSON.stringify(body)}'`);
3700
+ }
3701
+ output({
3702
+ curl: parts.join(" \\\n "),
3703
+ endpoint_id: endpointId,
3704
+ method,
3705
+ url,
3706
+ ...body ? { body } : {},
3707
+ headers
3708
+ }, !!flags.pretty);
3709
+ return;
3710
+ }
3632
3711
  try {
3633
3712
  const body = { params: {} };
3634
3713
  if (flags.endpoint) {
@@ -3988,6 +4067,49 @@ async function cmdSetup(flags) {
3988
4067
  output(report, true);
3989
4068
  if (report.browser_engine.action === "failed")
3990
4069
  process.exit(1);
4070
+ try {
4071
+ info("Trying your first resolve...");
4072
+ const demoUrl = "https://jsonplaceholder.typicode.com";
4073
+ const demoIntent = "list all posts";
4074
+ await recordFunnelTelemetryEvent("resolve_started", {
4075
+ source: "setup",
4076
+ hostType,
4077
+ properties: { command: "guided-first-resolve", intent: demoIntent, url: demoUrl }
4078
+ });
4079
+ const resolveResult = await api2("POST", "/v1/intent/resolve", {
4080
+ intent: demoIntent,
4081
+ params: { url: demoUrl },
4082
+ context: { url: demoUrl },
4083
+ projection: { raw: true }
4084
+ });
4085
+ if (isResolveSuccessResult(resolveResult)) {
4086
+ await recordFunnelTelemetryEvent("resolve_completed", {
4087
+ source: "setup",
4088
+ hostType,
4089
+ properties: { command: "guided-first-resolve", intent: demoIntent, url: demoUrl, source: resolveResult.source }
4090
+ });
4091
+ const endpoints = resolveResult.available_endpoints;
4092
+ if (endpoints && endpoints.length > 0) {
4093
+ info(`Found ${endpoints.length} API endpoint${endpoints.length > 1 ? "s" : ""} on ${demoUrl}:`);
4094
+ for (const ep of endpoints.slice(0, 5)) {
4095
+ const method = ep.method ?? "GET";
4096
+ const desc = ep.description ?? ep.url_template ?? ep.endpoint_id ?? "";
4097
+ info(` ${method} ${desc}`);
4098
+ }
4099
+ } else {
4100
+ info(`Resolve succeeded on ${demoUrl}`);
4101
+ }
4102
+ info("");
4103
+ info("That's unbrowse. Try your own:");
4104
+ info(' unbrowse resolve --intent "search for shoes" --url "https://amazon.com"');
4105
+ } else {
4106
+ info("Guided resolve returned no results \u2014 try manually:");
4107
+ info(' unbrowse resolve --intent "list posts" --url "https://jsonplaceholder.typicode.com"');
4108
+ }
4109
+ } catch {
4110
+ info("Setup complete. Try your first resolve:");
4111
+ info(' unbrowse resolve --intent "list posts" --url "https://jsonplaceholder.typicode.com"');
4112
+ }
3991
4113
  }
3992
4114
  var CLI_REFERENCE = {
3993
4115
  commands: [
@@ -4044,6 +4166,7 @@ var CLI_REFERENCE = {
4044
4166
  { flag: "--limit N", desc: "Cap array output to N items" },
4045
4167
  { flag: "--endpoint-id ID", desc: "Pick a specific endpoint" },
4046
4168
  { flag: "--dry-run", desc: "Preview mutations" },
4169
+ { flag: "--curl", desc: "Output the captured request as a curl command (no execution)" },
4047
4170
  { flag: "--params '{...}'", desc: "Extra params as JSON" }
4048
4171
  ],
4049
4172
  examples: [
@@ -4542,7 +4665,7 @@ async function cmdClose(flags) {
4542
4665
  output(await api2("POST", "/v1/browse/close", typeof flags.session === "string" ? { session_id: flags.session } : undefined), false);
4543
4666
  }
4544
4667
  async function cmdConnectChrome() {
4545
- const { execSync, spawn: spawnProc } = __require("child_process");
4668
+ const { execSync: execSync2, spawn: spawnProc } = __require("child_process");
4546
4669
  try {
4547
4670
  const res = await fetch("http://127.0.0.1:9222/json/version", { signal: AbortSignal.timeout(1000) });
4548
4671
  if (res.ok) {
@@ -4555,16 +4678,16 @@ async function cmdConnectChrome() {
4555
4678
  }
4556
4679
  } catch {}
4557
4680
  try {
4558
- execSync("pkill -f kuri/chrome-profile", { stdio: "ignore" });
4681
+ execSync2("pkill -f kuri/chrome-profile", { stdio: "ignore" });
4559
4682
  } catch {}
4560
4683
  console.log("Quitting Chrome to relaunch with remote debugging...");
4561
4684
  if (process.platform === "darwin") {
4562
4685
  try {
4563
- execSync('osascript -e "quit app \\"Google Chrome\\""', { stdio: "ignore", timeout: 5000 });
4686
+ execSync2('osascript -e "quit app \\"Google Chrome\\""', { stdio: "ignore", timeout: 5000 });
4564
4687
  } catch {}
4565
4688
  } else {
4566
4689
  try {
4567
- execSync("pkill -f chrome", { stdio: "ignore" });
4690
+ execSync2("pkill -f chrome", { stdio: "ignore" });
4568
4691
  } catch {}
4569
4692
  }
4570
4693
  await new Promise((r) => setTimeout(r, 2000));
package/dist/mcp.js CHANGED
@@ -225,11 +225,11 @@ import { dirname, join, parse } from "path";
225
225
  import { fileURLToPath as fileURLToPath2 } from "url";
226
226
 
227
227
  // ../../src/build-info.generated.ts
228
- var BUILD_RELEASE_VERSION = "3.3.4";
229
- var BUILD_GIT_SHA = "d398ad6f1a60";
230
- var BUILD_CODE_HASH = "d6e5ef2546cd";
231
- var BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy4zLjQiLCJnaXRfc2hhIjoiZDM5OGFkNmYxYTYwIiwiY29kZV9oYXNoIjoiZDZlNWVmMjU0NmNkIiwidHJhY2VfdmVyc2lvbiI6ImQ2ZTVlZjI1NDZjZEBkMzk4YWQ2ZjFhNjAiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA3VDA2OjQxOjAxLjcwNVoifQ";
232
- var BUILD_RELEASE_MANIFEST_SIGNATURE = "TaaK5qrudsUghz2Lc3jQCjMDbn6-mbEul9OwGz6J6WA";
228
+ var BUILD_RELEASE_VERSION = "3.4.0";
229
+ var BUILD_GIT_SHA = "361b3d271f3d";
230
+ var BUILD_CODE_HASH = "656382fbb5d5";
231
+ var BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy40LjAiLCJnaXRfc2hhIjoiMzYxYjNkMjcxZjNkIiwiY29kZV9oYXNoIjoiNjU2MzgyZmJiNWQ1IiwidHJhY2VfdmVyc2lvbiI6IjY1NjM4MmZiYjVkNUAzNjFiM2QyNzFmM2QiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA5VDAyOjUwOjI0LjI0NVoifQ";
232
+ var BUILD_RELEASE_MANIFEST_SIGNATURE = "_GYe4ccws1jOZQ13TD27_rBJwKd87JDzsXDQLQR3mZU";
233
233
  var BUILD_DEFAULT_BACKEND_URL = "https://beta-api.unbrowse.ai";
234
234
 
235
235
  // ../../src/version.ts
@@ -748,6 +748,7 @@ function readImpactSummary() {
748
748
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync3, unlinkSync as unlinkSync3 } from "fs";
749
749
  import { join as join5 } from "path";
750
750
  import { homedir as homedir4, hostname, release as osRelease } from "os";
751
+ import { execSync } from "child_process";
751
752
 
752
753
  // ../../src/payments/cascade.ts
753
754
  import bs58 from "bs58";
@@ -856,6 +857,30 @@ async function apiRequest(method, path4, body, opts) {
856
857
  console.warn("[unbrowse] Please restart the unbrowse service to accept the new terms.");
857
858
  throw new Error("ToS update required. Restart unbrowse to accept new terms.");
858
859
  }
860
+ if (res.status === 426 && !opts?.skipAutoUpdate) {
861
+ const errCode = data.error;
862
+ if (errCode === "client_update_required" || errCode === "client_verification_failed") {
863
+ console.warn(`
864
+ [unbrowse] Server requires a client update (${errCode}).`);
865
+ console.warn("[unbrowse] Attempting automatic update...");
866
+ try {
867
+ const updateCmd = process.env.UNBROWSE_UPDATE_COMMAND || "curl -fsSL https://unbrowse.ai/install.sh | bash";
868
+ execSync(updateCmd, { stdio: "inherit", timeout: 120000 });
869
+ console.warn("[unbrowse] Update installed. Restarting server...");
870
+ try {
871
+ execSync("pkill -9 -f 'unbrowse|kuri'", { stdio: "ignore", timeout: 5000 });
872
+ } catch {}
873
+ await new Promise((r) => setTimeout(r, 2000));
874
+ console.warn("[unbrowse] Retrying request with updated client...");
875
+ return apiRequest(method, path4, body, { ...opts, skipAutoUpdate: true });
876
+ } catch (updateErr) {
877
+ console.warn(`[unbrowse] Auto-update failed: ${updateErr.message}`);
878
+ const cmd = data.update_command ?? "curl -fsSL https://unbrowse.ai/install.sh | bash";
879
+ console.warn(`[unbrowse] Please update manually: ${cmd}`);
880
+ throw new Error(`Client update required. Run: ${cmd}`);
881
+ }
882
+ }
883
+ }
859
884
  if (res.status === 402) {
860
885
  const paymentRequired = res.headers.get("PAYMENT-REQUIRED");
861
886
  const legacyPaymentTerms = res.headers.get("X-Payment-Required");
package/dist/server.js CHANGED
@@ -788,7 +788,14 @@ async function findReusableIdleTab(state = defaultBrokerState) {
788
788
  try {
789
789
  const tabs = await kuriGet(state, "/tabs");
790
790
  const candidate = tabs.find((tab) => /^(about:blank|chrome:\/\/newtab\/?)$/i.test(tab?.url ?? ""));
791
- return candidate?.id ?? "";
791
+ if (!candidate?.id)
792
+ return "";
793
+ try {
794
+ await kuriGet(state, "/evaluate", { tab_id: candidate.id, expression: "1" });
795
+ return candidate.id;
796
+ } catch {
797
+ return "";
798
+ }
792
799
  } catch {
793
800
  return "";
794
801
  }
@@ -4265,7 +4272,15 @@ function extractEndpoints(requests, wsMessages, context) {
4265
4272
  }).filter(Boolean)) : new Map;
4266
4273
  for (const { req } of scored) {
4267
4274
  const normalized = normalizeUrl(req.url);
4268
- const key = `${req.method}:${normalized}`;
4275
+ let key = `${req.method}:${normalized}`;
4276
+ if (/graphql/i.test(req.url) && req.method !== "GET" && req.request_body) {
4277
+ try {
4278
+ const parsed = JSON.parse(req.request_body);
4279
+ const opName = parsed.operationName ?? parsed.query?.match(/(?:query|mutation)\s+(\w+)/)?.[1];
4280
+ if (opName)
4281
+ key += `#op=${opName}`;
4282
+ } catch {}
4283
+ }
4269
4284
  if (seen.has(key))
4270
4285
  continue;
4271
4286
  seen.add(key);
@@ -5006,9 +5021,16 @@ var init_reverse_engineer = __esm(() => {
5006
5021
  "sec-ch-ua",
5007
5022
  "sec-ch-ua-mobile",
5008
5023
  "sec-ch-ua-platform",
5024
+ "sec-ch-ua-full-version-list",
5025
+ "sec-ch-ua-arch",
5026
+ "sec-ch-ua-bitness",
5027
+ "sec-ch-ua-model",
5028
+ "sec-ch-ua-wow64",
5009
5029
  "sec-fetch-dest",
5010
5030
  "sec-fetch-mode",
5011
5031
  "sec-fetch-site",
5032
+ "sec-fetch-user",
5033
+ "upgrade-insecure-requests",
5012
5034
  "x-requested-with"
5013
5035
  ]);
5014
5036
  SENSITIVE_HEADER_PATTERN = /token|key|secret|credential|password|session/i;
@@ -5547,7 +5569,7 @@ async function collectInterceptedRequests(tabId) {
5547
5569
  }
5548
5570
  async function injectInterceptor(tabId) {
5549
5571
  const SETUP = `(function(){if(window.__unbrowse_interceptor_installed)return;window.__unbrowse_interceptor_installed=true;window.__unbrowse_intercepted=[];window.__UB_MAX=2*1024*1024;window.__UB_MAX_JS=2*1024*1024;window.__UB_MAX_N=500;})()`;
5550
- const FETCH_PATCH = `(function(){if(!window.__unbrowse_interceptor_installed)return;var M=window.__UB_MAX,MJ=window.__UB_MAX_JS,MN=window.__UB_MAX_N;var oF=window.fetch;window.fetch=function(){var a=arguments,u=typeof a[0]==='string'?a[0]:(a[0]&&a[0].url?a[0].url:''),o=a[1]||{},m=(o.method||'GET').toUpperCase(),rb=o.body?String(o.body).substring(0,M):void 0,rh={};if(o.headers){if(typeof o.headers.forEach==='function')o.headers.forEach(function(v,k){rh[k]=v});else Object.keys(o.headers).forEach(function(k){rh[k]=o.headers[k]})}return oF.apply(this,a).then(function(r){if(window.__unbrowse_intercepted.length>=MN)return r;var ct=r.headers.get('content-type')||'';var isJ=ct.indexOf('javascript')!==-1||/\\.js(\\?|$)/.test(u);var isD=ct.indexOf('json')!==-1||ct.indexOf('x-protobuf')!==-1||ct.indexOf('text/plain')!==-1||u.indexOf('/api/')!==-1||u.indexOf('graphql')!==-1||u.indexOf('voyager')!==-1;if(!isJ&&!isD)return r;if(/\\.(css|woff2?|png|jpg|svg|ico)(\\?|$)/.test(u))return r;var c=r.clone();c.text().then(function(b){var lim=isJ?MJ:M;if(b.length>lim)return;var rr={};r.headers.forEach(function(v,k){rr[k]=v});window.__unbrowse_intercepted.push({url:u,method:m,request_headers:rh,request_body:rb,response_status:r.status,response_headers:rr,response_body:b,content_type:ct,is_js:isJ,timestamp:new Date().toISOString()})}).catch(function(){});return r}).catch(function(e){throw e})}})()`;
5572
+ const FETCH_PATCH = `(function(){if(!window.__unbrowse_interceptor_installed)return;var M=window.__UB_MAX,MJ=window.__UB_MAX_JS,MN=window.__UB_MAX_N;var oF=window.fetch;window.fetch=function(){var a=arguments,isR=a[0]&&typeof a[0]==='object'&&typeof a[0].url==='string',u=typeof a[0]==='string'?a[0]:(isR?a[0].url:''),o=a[1]||{},m=(o.method||(isR?a[0].method:null)||'GET').toUpperCase(),rb=o.body?String(o.body).substring(0,M):void 0,rbP=null;if(!rb&&isR&&m!=='GET'&&m!=='HEAD'){try{var rc=a[0].clone();rbP=rc.text().then(function(t){return t?t.substring(0,M):void 0}).catch(function(){return void 0})}catch(e){}}var rh={};if(isR&&a[0].headers&&typeof a[0].headers.forEach==='function')a[0].headers.forEach(function(v,k){rh[k]=v});if(o.headers){if(typeof o.headers.forEach==='function')o.headers.forEach(function(v,k){rh[k]=v});else Object.keys(o.headers).forEach(function(k){rh[k]=o.headers[k]})}return oF.apply(this,a).then(function(r){if(window.__unbrowse_intercepted.length>=MN)return r;var ct=r.headers.get('content-type')||'';var isJ=ct.indexOf('javascript')!==-1||/\\.js(\\?|$)/.test(u);var isD=ct.indexOf('json')!==-1||ct.indexOf('x-protobuf')!==-1||ct.indexOf('text/plain')!==-1||u.indexOf('/api/')!==-1||u.indexOf('graphql')!==-1||u.indexOf('voyager')!==-1;if(!isJ&&!isD)return r;if(/\\.(css|woff2?|png|jpg|svg|ico)(\\?|$)/.test(u))return r;var c=r.clone();Promise.all([c.text(),rbP?rbP:Promise.resolve(rb)]).then(function(res){var b=res[0],rrb=res[1];var lim=isJ?MJ:M;if(b.length>lim)return;var rr={};r.headers.forEach(function(v,k){rr[k]=v});window.__unbrowse_intercepted.push({url:u,method:m,request_headers:rh,request_body:rrb,response_status:r.status,response_headers:rr,response_body:b,content_type:ct,is_js:isJ,timestamp:new Date().toISOString()})}).catch(function(){});return r}).catch(function(e){throw e})}})()`;
5551
5573
  const XHR_PATCH = `(function(){if(!window.__unbrowse_interceptor_installed)return;var M=window.__UB_MAX,MJ=window.__UB_MAX_JS,MN=window.__UB_MAX_N;var oO=XMLHttpRequest.prototype.open,oS=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open=function(m,u){this.__ub_m=m;this.__ub_u=u;this.__ub_h={};var oSH=this.setRequestHeader.bind(this);this.setRequestHeader=function(k,v){this.__ub_h[k]=v;oSH(k,v)}.bind(this);return oO.apply(this,arguments)};XMLHttpRequest.prototype.send=function(b){var x=this;x.addEventListener('load',function(){if(window.__unbrowse_intercepted.length>=MN)return;var ct=x.getResponseHeader('content-type')||'',u=x.__ub_u||'';var isJ=ct.indexOf('javascript')!==-1||/\\.js(\\?|$)/.test(u);var isD=ct.indexOf('json')!==-1||ct.indexOf('x-protobuf')!==-1||ct.indexOf('text/plain')!==-1||u.indexOf('/api/')!==-1||u.indexOf('graphql')!==-1||u.indexOf('voyager')!==-1;if(!isJ&&!isD)return;if(/\\.(css|woff2?|png|jpg|svg|ico)(\\?|$)/.test(u))return;var rb=x.responseText||'';var lim=isJ?MJ:M;if(rb.length>lim)return;window.__unbrowse_intercepted.push({url:u,method:(x.__ub_m||'GET').toUpperCase(),request_headers:x.__ub_h||{},request_body:b?String(b).substring(0,M):void 0,response_status:x.status,response_headers:{},response_body:rb,content_type:ct,is_js:isJ,timestamp:new Date().toISOString()})});return oS.apply(this,arguments)}})()`;
5552
5574
  for (const chunk of [SETUP, FETCH_PATCH, XHR_PATCH]) {
5553
5575
  await evaluate(tabId, chunk).catch(() => {});
@@ -5555,10 +5577,22 @@ async function injectInterceptor(tabId) {
5555
5577
  }
5556
5578
  function mergePassiveCaptureData(intercepted, harEntries, extensionEntries, responseBodies, performanceUrls = []) {
5557
5579
  const seen = new Map;
5580
+ function graphqlDedup(url, requestBody) {
5581
+ if (!requestBody || !/graphql/i.test(url))
5582
+ return url;
5583
+ try {
5584
+ const parsed = JSON.parse(requestBody);
5585
+ const opName = parsed.operationName ?? parsed.query?.match(/(?:query|mutation)\s+(\w+)/)?.[1];
5586
+ if (opName)
5587
+ return `${url}#op=${opName}`;
5588
+ } catch {}
5589
+ return url;
5590
+ }
5558
5591
  for (const entry of intercepted) {
5559
5592
  if (entry.is_js)
5560
5593
  continue;
5561
- seen.set(entry.url, {
5594
+ const key = graphqlDedup(entry.url, entry.request_body);
5595
+ seen.set(key, {
5562
5596
  url: entry.url,
5563
5597
  method: entry.method,
5564
5598
  request_headers: entry.request_headers,
@@ -5571,21 +5605,25 @@ function mergePassiveCaptureData(intercepted, harEntries, extensionEntries, resp
5571
5605
  }
5572
5606
  for (const entry of harEntries) {
5573
5607
  const url = entry.request?.url;
5574
- if (!url || seen.has(url))
5608
+ if (!url)
5575
5609
  continue;
5576
5610
  if (entry.request.method === "OPTIONS")
5577
5611
  continue;
5612
+ const postBody = entry.request.postData?.text;
5613
+ const key = graphqlDedup(url, postBody);
5614
+ if (seen.has(key))
5615
+ continue;
5578
5616
  const reqHeaders = {};
5579
5617
  for (const h of entry.request.headers ?? [])
5580
5618
  reqHeaders[h.name] = h.value;
5581
5619
  const respHeaders = {};
5582
5620
  for (const h of entry.response.headers ?? [])
5583
5621
  respHeaders[h.name] = h.value;
5584
- seen.set(url, {
5622
+ seen.set(key, {
5585
5623
  url,
5586
5624
  method: entry.request.method,
5587
5625
  request_headers: reqHeaders,
5588
- request_body: entry.request.postData?.text,
5626
+ request_body: postBody,
5589
5627
  response_status: entry.response.status,
5590
5628
  response_headers: respHeaders,
5591
5629
  response_body: responseBodies.get(url) ?? entry.response.content?.text,
@@ -5599,14 +5637,19 @@ function mergePassiveCaptureData(intercepted, harEntries, extensionEntries, resp
5599
5637
  const respHeaders = {};
5600
5638
  for (const h of entry.responseHeaders ?? [])
5601
5639
  respHeaders[h.name] = h.value;
5602
- const existing = seen.get(entry.url);
5603
- if (existing) {
5604
- for (const [k, v] of Object.entries(reqHeaders)) {
5605
- if (!existing.request_headers[k])
5606
- existing.request_headers[k] = v;
5640
+ let merged = false;
5641
+ for (const [key, existing] of seen) {
5642
+ if (existing.url === entry.url) {
5643
+ for (const [k, v] of Object.entries(reqHeaders)) {
5644
+ if (!existing.request_headers[k])
5645
+ existing.request_headers[k] = v;
5646
+ }
5647
+ merged = true;
5648
+ break;
5607
5649
  }
5608
- continue;
5609
5650
  }
5651
+ if (merged)
5652
+ continue;
5610
5653
  seen.set(entry.url, {
5611
5654
  url: entry.url,
5612
5655
  method: entry.method,
@@ -6763,13 +6806,23 @@ var MAX_CONCURRENT_TABS = 3, activeTabs = 0, waitQueue, activeTabRegistry, inter
6763
6806
  var origFetch = window.fetch;
6764
6807
  window.fetch = function() {
6765
6808
  var args = arguments;
6766
- var url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url ? args[0].url : '');
6809
+ var isRequestObj = args[0] && typeof args[0] === 'object' && typeof args[0].url === 'string';
6810
+ var url = typeof args[0] === 'string' ? args[0] : (isRequestObj ? args[0].url : '');
6767
6811
  var opts = args[1] || {};
6768
- var method = (opts.method || 'GET').toUpperCase();
6812
+ // Extract method: prefer explicit opts, then Request object, then default GET
6813
+ var method = (opts.method || (isRequestObj ? args[0].method : null) || 'GET').toUpperCase();
6814
+ // Extract body: prefer explicit opts.body, then clone+read Request body
6769
6815
  var reqBody = opts.body ? String(opts.body).substring(0, MAX_BODY) : undefined;
6816
+ var reqBodyPromise = null;
6817
+ if (!reqBody && isRequestObj && method !== 'GET' && method !== 'HEAD') {
6818
+ try {
6819
+ var reqClone = args[0].clone();
6820
+ reqBodyPromise = reqClone.text().then(function(t) { return t ? t.substring(0, MAX_BODY) : undefined; }).catch(function() { return undefined; });
6821
+ } catch(e) { /* clone failed */ }
6822
+ }
6770
6823
  var reqHeaders = {};
6771
6824
  // Extract headers from Request object (first arg)
6772
- if (args[0] && typeof args[0] === 'object' && args[0].headers && typeof args[0].headers.forEach === 'function') {
6825
+ if (isRequestObj && args[0].headers && typeof args[0].headers.forEach === 'function') {
6773
6826
  args[0].headers.forEach(function(v, k) { reqHeaders[k] = v; });
6774
6827
  }
6775
6828
  // Override/merge with explicit opts.headers (second arg)
@@ -6792,7 +6845,13 @@ var MAX_CONCURRENT_TABS = 3, activeTabs = 0, waitQueue, activeTabRegistry, inter
6792
6845
  if (!isJs && !isData) return response;
6793
6846
  if (/\\.(css|woff2?|png|jpg|svg|ico)(\\?|$)/.test(url)) return response;
6794
6847
  var clone = response.clone();
6795
- clone.text().then(function(body) {
6848
+ // Resolve request body (from Request object clone) and response body in parallel
6849
+ Promise.all([
6850
+ clone.text(),
6851
+ reqBodyPromise ? reqBodyPromise : Promise.resolve(reqBody)
6852
+ ]).then(function(results) {
6853
+ var body = results[0];
6854
+ var resolvedReqBody = results[1];
6796
6855
  var limit = isJs ? MAX_JS_BODY : MAX_BODY;
6797
6856
  if (body.length > limit) return;
6798
6857
  var respHeaders = {};
@@ -6801,7 +6860,7 @@ var MAX_CONCURRENT_TABS = 3, activeTabs = 0, waitQueue, activeTabRegistry, inter
6801
6860
  url: url,
6802
6861
  method: method,
6803
6862
  request_headers: reqHeaders,
6804
- request_body: reqBody,
6863
+ request_body: resolvedReqBody,
6805
6864
  response_status: response.status,
6806
6865
  response_headers: respHeaders,
6807
6866
  response_body: body,
@@ -6884,7 +6943,7 @@ var init_capture = __esm(async () => {
6884
6943
  });
6885
6944
 
6886
6945
  // ../../src/build-info.generated.ts
6887
- var BUILD_RELEASE_VERSION = "3.3.4", BUILD_GIT_SHA = "d398ad6f1a60", BUILD_CODE_HASH = "d6e5ef2546cd", BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy4zLjQiLCJnaXRfc2hhIjoiZDM5OGFkNmYxYTYwIiwiY29kZV9oYXNoIjoiZDZlNWVmMjU0NmNkIiwidHJhY2VfdmVyc2lvbiI6ImQ2ZTVlZjI1NDZjZEBkMzk4YWQ2ZjFhNjAiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA3VDA2OjQxOjAxLjcwNVoifQ", BUILD_RELEASE_MANIFEST_SIGNATURE = "TaaK5qrudsUghz2Lc3jQCjMDbn6-mbEul9OwGz6J6WA", BUILD_DEFAULT_BACKEND_URL = "https://beta-api.unbrowse.ai";
6946
+ var BUILD_RELEASE_VERSION = "3.4.0", BUILD_GIT_SHA = "361b3d271f3d", BUILD_CODE_HASH = "656382fbb5d5", BUILD_RELEASE_MANIFEST_BASE64 = "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmVsZWFzZV92ZXJzaW9uIjoiMy40LjAiLCJnaXRfc2hhIjoiMzYxYjNkMjcxZjNkIiwiY29kZV9oYXNoIjoiNjU2MzgyZmJiNWQ1IiwidHJhY2VfdmVyc2lvbiI6IjY1NjM4MmZiYjVkNUAzNjFiM2QyNzFmM2QiLCJpc3N1ZWRfYXQiOiIyMDI2LTA0LTA5VDAyOjUwOjI0LjI0NVoifQ", BUILD_RELEASE_MANIFEST_SIGNATURE = "_GYe4ccws1jOZQ13TD27_rBJwKd87JDzsXDQLQR3mZU", BUILD_DEFAULT_BACKEND_URL = "https://beta-api.unbrowse.ai";
6888
6947
 
6889
6948
  // ../../src/version.ts
6890
6949
  import { createHash } from "crypto";
@@ -7399,6 +7458,7 @@ import { join as join6 } from "path";
7399
7458
  import { homedir as homedir4, hostname, release as osRelease } from "os";
7400
7459
  import { randomBytes as randomBytes2, createHash as createHash3 } from "crypto";
7401
7460
  import { createInterface } from "readline";
7461
+ import { execSync } from "child_process";
7402
7462
  function buildReleaseAttestationHeaders(manifestBase64, signature) {
7403
7463
  const manifest = manifestBase64.trim();
7404
7464
  const sig = signature.trim();
@@ -7772,6 +7832,30 @@ async function apiRequest(method, path4, body, opts) {
7772
7832
  console.warn("[unbrowse] Please restart the unbrowse service to accept the new terms.");
7773
7833
  throw new Error("ToS update required. Restart unbrowse to accept new terms.");
7774
7834
  }
7835
+ if (res.status === 426 && !opts?.skipAutoUpdate) {
7836
+ const errCode = data.error;
7837
+ if (errCode === "client_update_required" || errCode === "client_verification_failed") {
7838
+ console.warn(`
7839
+ [unbrowse] Server requires a client update (${errCode}).`);
7840
+ console.warn("[unbrowse] Attempting automatic update...");
7841
+ try {
7842
+ const updateCmd = process.env.UNBROWSE_UPDATE_COMMAND || "curl -fsSL https://unbrowse.ai/install.sh | bash";
7843
+ execSync(updateCmd, { stdio: "inherit", timeout: 120000 });
7844
+ console.warn("[unbrowse] Update installed. Restarting server...");
7845
+ try {
7846
+ execSync("pkill -9 -f 'unbrowse|kuri'", { stdio: "ignore", timeout: 5000 });
7847
+ } catch {}
7848
+ await new Promise((r) => setTimeout(r, 2000));
7849
+ console.warn("[unbrowse] Retrying request with updated client...");
7850
+ return apiRequest(method, path4, body, { ...opts, skipAutoUpdate: true });
7851
+ } catch (updateErr) {
7852
+ console.warn(`[unbrowse] Auto-update failed: ${updateErr.message}`);
7853
+ const cmd = data.update_command ?? "curl -fsSL https://unbrowse.ai/install.sh | bash";
7854
+ console.warn(`[unbrowse] Please update manually: ${cmd}`);
7855
+ throw new Error(`Client update required. Run: ${cmd}`);
7856
+ }
7857
+ }
7858
+ }
7775
7859
  if (res.status === 402) {
7776
7860
  const paymentRequired = res.headers.get("PAYMENT-REQUIRED");
7777
7861
  const legacyPaymentTerms = res.headers.get("X-Payment-Required");
@@ -14450,6 +14534,7 @@ __export(exports_execution, {
14450
14534
  templatizeQueryParams: () => templatizeQueryParams,
14451
14535
  shouldIgnoreLearnedBrowserStrategy: () => shouldIgnoreLearnedBrowserStrategy,
14452
14536
  resolveExecutionUrlTemplate: () => resolveExecutionUrlTemplate,
14537
+ reloadExecutionAuthState: () => reloadExecutionAuthState,
14453
14538
  rankEndpoints: () => rankEndpoints,
14454
14539
  projectResultForIntent: () => projectResultForIntent,
14455
14540
  isCanonicalReplayEndpoint: () => isCanonicalReplayEndpoint,
@@ -16245,10 +16330,6 @@ async function executeEndpoint(skill, endpoint, params = {}, projection, options
16245
16330
  ...sessionHeaders,
16246
16331
  ...normalizeReplayHeaders(extraHeaders)
16247
16332
  };
16248
- delete headers["sec-ch-ua"];
16249
- delete headers["sec-ch-ua-mobile"];
16250
- delete headers["sec-ch-ua-platform"];
16251
- delete headers["upgrade-insecure-requests"];
16252
16333
  if (cookies.length > 0) {
16253
16334
  const cookieStr = cookies.map((c) => {
16254
16335
  const v = c.value.startsWith('"') && c.value.endsWith('"') ? c.value.slice(1, -1) : c.value;
@@ -22857,6 +22938,10 @@ async function resolveRequestedBrowseSession(sessions, client, requestedSessionI
22857
22938
  const session = sessions.get(requestedSessionId);
22858
22939
  if (!session)
22859
22940
  throw new BrowseSessionError("session_not_found");
22941
+ if (!await isBrowseSessionLive(session, client)) {
22942
+ removeBrowseSession(sessions, requestedSessionId);
22943
+ throw new BrowseSessionError("session_expired");
22944
+ }
22860
22945
  return session;
22861
22946
  }
22862
22947
  const live = await listLiveBrowseSessions(sessions, client);
@@ -22927,7 +23012,7 @@ async function getOrCreateNavigateBrowseSession(sessions, client, injectIntercep
22927
23012
  }
22928
23013
  return createBrowseSession(sessions, client, injectInterceptor2);
22929
23014
  }
22930
- var BrowseSessionError, RECOVERABLE_BROWSE_FAILURES, LIVE_CHECK_RETRIES = 8, LIVE_CHECK_RETRY_DELAY_MS = 250, sessionQueues;
23015
+ var BrowseSessionError, RECOVERABLE_BROWSE_FAILURES, LIVE_CHECK_RETRIES = 3, LIVE_CHECK_RETRY_DELAY_MS = 100, sessionQueues;
22931
23016
  var init_browse_session = __esm(() => {
22932
23017
  init_client();
22933
23018
  BrowseSessionError = class BrowseSessionError extends Error {
@@ -25541,6 +25626,39 @@ async function registerRoutes(app) {
25541
25626
  return reply.code(500).send({ error: err.message });
25542
25627
  }
25543
25628
  });
25629
+ app.get("/v1/skills/:skill_id/request-preview/:endpoint_id", async (req, reply) => {
25630
+ const { skill_id, endpoint_id } = req.params;
25631
+ const skill = await loadSkillForMutation(skill_id);
25632
+ if (!skill)
25633
+ return reply.status(404).send({ error: "skill_not_found" });
25634
+ const ep = skill.endpoints.find((e) => e.endpoint_id === endpoint_id);
25635
+ if (!ep)
25636
+ return reply.status(404).send({ error: "endpoint_not_found" });
25637
+ let epDomain;
25638
+ try {
25639
+ epDomain = new URL(ep.url_template).hostname;
25640
+ } catch {
25641
+ epDomain = skill.domain;
25642
+ }
25643
+ const authHeaders = {};
25644
+ const cookies = [];
25645
+ const { reloadExecutionAuthState: reloadExecutionAuthState2 } = await init_execution().then(() => exports_execution);
25646
+ await reloadExecutionAuthState2(skill, epDomain, authHeaders, cookies);
25647
+ const allHeaders = { ...ep.headers_template ?? {}, ...authHeaders };
25648
+ if (cookies.length > 0) {
25649
+ allHeaders["Cookie"] = cookies.map((c) => `${c.name}=${c.value}`).join("; ");
25650
+ }
25651
+ return reply.send({
25652
+ method: ep.method,
25653
+ url: ep.url_template,
25654
+ headers: allHeaders,
25655
+ body: ep.body ?? null,
25656
+ query: ep.query ?? null,
25657
+ path_params: ep.path_params ?? null,
25658
+ cookies: cookies.length > 0 ? cookies.map((c) => ({ name: c.name, domain: c.domain })) : null,
25659
+ trigger_url: ep.trigger_url ?? null
25660
+ });
25661
+ });
25544
25662
  app.post("/v1/auth/steal", { config: { rateLimit: { max: 30, timeWindow: "1 minute" } } }, async (req, reply) => {
25545
25663
  const {
25546
25664
  url,
@@ -25850,6 +25968,11 @@ async function registerRoutes(app) {
25850
25968
  session2.url = typeof finalUrl === "string" && finalUrl.startsWith("http") ? finalUrl : url;
25851
25969
  session2.domain = profileName(session2.url);
25852
25970
  await injectInterceptor(session2.tabId);
25971
+ try {
25972
+ await broker.evaluate(session2.tabId, "window.scrollTo(0, document.body.scrollHeight)");
25973
+ await new Promise((r) => setTimeout(r, 2500));
25974
+ await broker.evaluate(session2.tabId, "window.scrollTo(0, 0)");
25975
+ } catch {}
25853
25976
  const stillLive = await isBrowseSessionLive(session2, browseClient).catch(() => false);
25854
25977
  if (!stillLive)
25855
25978
  throw { error: "CDP command failed" };
@@ -26222,7 +26345,7 @@ await __promiseAll([
26222
26345
  init_capture(),
26223
26346
  init_stale_cleanup_runner()
26224
26347
  ]);
26225
- import { execSync as execSync2 } from "node:child_process";
26348
+ import { execSync as execSync3 } from "node:child_process";
26226
26349
  import { mkdirSync as mkdirSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync12 } from "node:fs";
26227
26350
  import path5 from "node:path";
26228
26351
  import Fastify from "fastify";
@@ -26254,7 +26377,7 @@ async function startUnbrowseServer(options = {}) {
26254
26377
  const pidFile = options.pidFile ?? process.env.UNBROWSE_PID_FILE;
26255
26378
  updatePidFile(pidFile, host, port);
26256
26379
  try {
26257
- execSync2("pkill -f chrome-headless-shell", { stdio: "ignore" });
26380
+ execSync3("pkill -f chrome-headless-shell", { stdio: "ignore" });
26258
26381
  } catch {}
26259
26382
  startBackgroundRegistration();
26260
26383
  const app = Fastify({ logger: options.logger ?? true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbrowse",
3
- "version": "3.3.4",
3
+ "version": "3.4.0",
4
4
  "description": "Reverse-engineer any website into reusable API skills. Zero-dep single binary with embedded browser engine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,18 +59,15 @@ function ensureExecutable(filePath) {
59
59
  ensureExecutable(wrapperPath);
60
60
  ensureExecutable(launcherPath);
61
61
 
62
- // Skip binary download in CI build environments — the release pipeline builds
63
- // binaries AFTER install, so the download would always 404 and fail.
64
- if (process.env.CI && (process.env.GITHUB_ACTIONS || process.env.UNBROWSE_SKIP_BINARY_DOWNLOAD)) {
65
- process.exit(0);
66
- }
67
-
68
62
  // Skip if binary already exists (re-install)
69
63
  if (existsSync(binaryPath)) {
70
64
  ensureExecutable(binaryPath);
65
+ console.log(`[unbrowse] Binary already exists, skipping.`);
71
66
  process.exit(0);
72
67
  }
73
68
 
69
+ // Local binary override — used by smoke tests to inject a pre-built binary.
70
+ // Must run BEFORE the CI skip so packaged smoke tests work in GitHub Actions.
74
71
  if (localBinaryPath) {
75
72
  if (!existsSync(localBinaryPath)) {
76
73
  console.warn(`[unbrowse] Local binary override not found: ${localBinaryPath}`);
@@ -83,6 +80,13 @@ if (localBinaryPath) {
83
80
  process.exit(0);
84
81
  }
85
82
 
83
+ // Skip binary download in CI build environments — the release pipeline builds
84
+ // binaries AFTER install, so the download would always 404 and fail.
85
+ // Placed after the local binary override so smoke tests still work.
86
+ if (process.env.CI && (process.env.GITHUB_ACTIONS || process.env.UNBROWSE_SKIP_BINARY_DOWNLOAD)) {
87
+ process.exit(0);
88
+ }
89
+
86
90
  const platform = process.platform; // darwin, linux
87
91
  const arch = process.arch; // arm64, x64
88
92
  const target = `${platform}-${arch}`;