unbrowse 2.10.2 → 2.12.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
@@ -116,6 +116,19 @@ function attributeLifecycle(events) {
116
116
  return totals;
117
117
  }
118
118
 
119
+ // ../../src/runtime/browser-host.ts
120
+ function detectHostEnvironment() {
121
+ if (process.env.OPENCLAW_RUNTIME)
122
+ return "openclaw";
123
+ if (process.env.OPENAI_TOOL_RUNTIME)
124
+ return "openai";
125
+ if (process.env.MCP_SERVER_MODE)
126
+ return "mcp";
127
+ if (process.env.UNBROWSE_NATIVE)
128
+ return "native";
129
+ return "unknown";
130
+ }
131
+
119
132
  // ../../src/runtime/paths.ts
120
133
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
121
134
  import os from "node:os";
@@ -241,8 +254,85 @@ var init_logger = __esm(() => {
241
254
  });
242
255
 
243
256
  // ../../src/kuri/client.ts
257
+ var exports_client = {};
258
+ __export(exports_client, {
259
+ waitForSelector: () => waitForSelector,
260
+ waitForLoad: () => waitForLoad,
261
+ waitForCloudflare: () => waitForCloudflare,
262
+ stop: () => stop,
263
+ start: () => start,
264
+ snapshot: () => snapshot,
265
+ setViewport: () => setViewport,
266
+ setUserAgent: () => setUserAgent,
267
+ setHeaders: () => setHeaders,
268
+ setCredentials: () => setCredentials,
269
+ setCookies: () => setCookies,
270
+ setCookie: () => setCookie,
271
+ sessionSave: () => sessionSave,
272
+ sessionLoad: () => sessionLoad,
273
+ sessionList: () => sessionList,
274
+ select: () => select,
275
+ scrollIntoView: () => scrollIntoView,
276
+ scroll: () => scroll,
277
+ scriptInject: () => scriptInject,
278
+ screenshot: () => screenshot,
279
+ resolveKuriPort: () => resolveKuriPort,
280
+ reload: () => reload,
281
+ press: () => press,
282
+ newTab: () => newTab,
283
+ networkEnable: () => networkEnable,
284
+ navigate: () => navigate,
285
+ keyboardType: () => keyboardType,
286
+ keyboardInsertText: () => keyboardInsertText,
287
+ keyUp: () => keyUp,
288
+ keyDown: () => keyDown,
289
+ isReady: () => isReady,
290
+ interceptStart: () => interceptStart,
291
+ health: () => health,
292
+ hasCloudflareChallenge: () => hasCloudflareChallenge,
293
+ harStop: () => harStop,
294
+ harStart: () => harStart,
295
+ goForward: () => goForward,
296
+ goBack: () => goBack,
297
+ getText: () => getText,
298
+ getPort: () => getPort,
299
+ getPerfLcp: () => getPerfLcp,
300
+ getPageHtml: () => getPageHtml,
301
+ getNetworkEvents: () => getNetworkEvents,
302
+ getMarkdown: () => getMarkdown,
303
+ getLinks: () => getLinks,
304
+ getKuriSourceCandidates: () => getKuriSourceCandidates,
305
+ getKuriErrorMessage: () => getKuriErrorMessage,
306
+ getKuriBinaryCandidates: () => getKuriBinaryCandidates,
307
+ getErrors: () => getErrors,
308
+ getDefaultTab: () => getDefaultTab,
309
+ getCurrentUrl: () => getCurrentUrl,
310
+ getCookies: () => getCookies,
311
+ getConsole: () => getConsole,
312
+ findText: () => findText,
313
+ findKuriBinary: () => findKuriBinary,
314
+ fill: () => fill,
315
+ extractLoadPluginsFromHtml: () => extractLoadPluginsFromHtml,
316
+ extractLoadPlugins: () => extractLoadPlugins,
317
+ executeInPageFetch: () => executeInPageFetch,
318
+ evaluate: () => evaluate,
319
+ drag: () => drag,
320
+ domQuery: () => domQuery,
321
+ domHtml: () => domHtml,
322
+ domAttributes: () => domAttributes,
323
+ discoverTabs: () => discoverTabs,
324
+ closeTab: () => closeTab,
325
+ click: () => click,
326
+ bestEffortRehydratePlugins: () => bestEffortRehydratePlugins,
327
+ authProfileSave: () => authProfileSave,
328
+ authProfileLoad: () => authProfileLoad,
329
+ authProfileList: () => authProfileList,
330
+ authProfileDelete: () => authProfileDelete,
331
+ action: () => action
332
+ });
244
333
  import { execFileSync, spawn } from "node:child_process";
245
334
  import { existsSync as existsSync3 } from "node:fs";
335
+ import net from "node:net";
246
336
  import path3 from "node:path";
247
337
  function kuriBinaryName() {
248
338
  return process.platform === "win32" ? "kuri.exe" : "kuri";
@@ -316,6 +406,46 @@ async function discoverCdpPort() {
316
406
  }
317
407
  log("kuri", "could not discover CDP port — tab discovery may fail");
318
408
  }
409
+ async function isKuriHealthyOnPort(port) {
410
+ try {
411
+ const health = await fetch(`http://127.0.0.1:${port}/health`, {
412
+ signal: AbortSignal.timeout(1000)
413
+ });
414
+ return health.ok;
415
+ } catch {
416
+ return false;
417
+ }
418
+ }
419
+ async function isTcpPortOpen(port, timeoutMs = 400) {
420
+ return await new Promise((resolve) => {
421
+ const socket = net.createConnection({ host: "127.0.0.1", port });
422
+ const finish = (open) => {
423
+ socket.removeAllListeners();
424
+ socket.destroy();
425
+ resolve(open);
426
+ };
427
+ socket.setTimeout(timeoutMs);
428
+ socket.once("connect", () => finish(true));
429
+ socket.once("timeout", () => finish(false));
430
+ socket.once("error", () => finish(false));
431
+ });
432
+ }
433
+ async function resolveKuriPort(preferredPort, deps = {}) {
434
+ const isHealthyPort = deps.isHealthyPort ?? isKuriHealthyOnPort;
435
+ const isPortOpen = deps.isPortOpen ?? isTcpPortOpen;
436
+ const searchLimit = deps.searchLimit ?? KURI_PORT_SEARCH_LIMIT;
437
+ if (await isHealthyPort(preferredPort))
438
+ return preferredPort;
439
+ if (!await isPortOpen(preferredPort))
440
+ return preferredPort;
441
+ for (let candidate = preferredPort + 1;candidate <= preferredPort + searchLimit; candidate++) {
442
+ if (await isHealthyPort(candidate))
443
+ return candidate;
444
+ if (!await isPortOpen(candidate))
445
+ return candidate;
446
+ }
447
+ return preferredPort;
448
+ }
319
449
  function kuriUrl(path4, params) {
320
450
  const base = `http://127.0.0.1:${kuriPort}${path4}`;
321
451
  if (!params || Object.keys(params).length === 0)
@@ -382,105 +512,118 @@ async function waitForChildExit(child, timeoutMs = 2000) {
382
512
  async function start(port) {
383
513
  if (kuriReady)
384
514
  return;
385
- kuriPort = port ?? KURI_DEFAULT_PORT;
386
- try {
387
- const health = await fetch(`http://127.0.0.1:${kuriPort}/health`, {
388
- signal: AbortSignal.timeout(1000)
389
- });
390
- if (health.ok) {
515
+ if (kuriStartPromise)
516
+ return kuriStartPromise;
517
+ const startPromise = (async () => {
518
+ const requestedPort = port ?? Number(process.env.KURI_PORT || KURI_DEFAULT_PORT);
519
+ kuriPort = await resolveKuriPort(requestedPort);
520
+ if (kuriPort !== requestedPort) {
521
+ log("kuri", `preferred port ${requestedPort} is occupied but unhealthy; falling back to ${kuriPort}`);
522
+ }
523
+ if (await isKuriHealthyOnPort(kuriPort)) {
391
524
  log("kuri", `already running on port ${kuriPort}`);
392
525
  kuriReady = true;
393
526
  await discoverCdpPort();
394
527
  await ensureTabsDiscovered();
395
528
  return;
396
529
  }
397
- } catch {}
398
- const binary = findKuriBinary();
399
- log("kuri", `starting: ${binary} on port ${kuriPort}`);
400
- if (!existsSync3(binary)) {
401
- throw new Error(`Kuri binary not found at ${binary}`);
402
- }
403
- await discoverCdpPort();
404
- const env = {
405
- ...process.env,
406
- PORT: String(kuriPort),
407
- HOST: "127.0.0.1",
408
- HEADLESS: "false"
409
- };
410
- if (kuriCdpPort) {
411
- env.CDP_URL = `ws://127.0.0.1:${kuriCdpPort}`;
412
- log("kuri", `connecting to existing Chrome on port ${kuriCdpPort}`);
413
- } else {
414
- log("kuri", "no existing Chrome found — Kuri will launch managed Chrome");
415
- }
416
- const maxAttempts = KURI_SPAWN_RETRIES + 1;
417
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
418
- if (attempt > 1) {
419
- log("kuri", `spawn retry ${attempt}/${maxAttempts} after ${KURI_SPAWN_RETRY_DELAY_MS}ms`);
420
- await new Promise((r) => setTimeout(r, KURI_SPAWN_RETRY_DELAY_MS));
421
- }
422
- let exitedBeforeReady = false;
423
- kuriProcess = spawn(binary, [], {
424
- env,
425
- stdio: ["ignore", "pipe", "pipe"]
426
- });
427
- kuriProcess.stderr?.on("data", (chunk) => {
428
- const line = chunk.toString().trim();
429
- if (line)
430
- log("kuri", `[stderr] ${line}`);
431
- const cdpMatch = line.match(/CDP port:\s*(\d+)/);
432
- if (cdpMatch) {
433
- kuriCdpPort = parseInt(cdpMatch[1], 10);
434
- log("kuri", `discovered CDP port: ${kuriCdpPort}`);
530
+ const binary = findKuriBinary();
531
+ log("kuri", `starting: ${binary} on port ${kuriPort}`);
532
+ if (!existsSync3(binary)) {
533
+ throw new Error(`Kuri binary not found at ${binary}`);
534
+ }
535
+ await discoverCdpPort();
536
+ const env = {
537
+ ...process.env,
538
+ PORT: String(kuriPort),
539
+ HOST: "127.0.0.1",
540
+ HEADLESS: "false"
541
+ };
542
+ if (kuriCdpPort) {
543
+ env.CDP_URL = `ws://127.0.0.1:${kuriCdpPort}`;
544
+ log("kuri", `connecting to existing Chrome on port ${kuriCdpPort}`);
545
+ } else {
546
+ log("kuri", "no existing Chrome found — Kuri will launch managed Chrome");
547
+ }
548
+ const maxAttempts = KURI_SPAWN_RETRIES + 1;
549
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
550
+ if (attempt > 1) {
551
+ log("kuri", `spawn retry ${attempt}/${maxAttempts} after ${KURI_SPAWN_RETRY_DELAY_MS}ms`);
552
+ await new Promise((r) => setTimeout(r, KURI_SPAWN_RETRY_DELAY_MS));
435
553
  }
436
- });
437
- kuriProcess.on("exit", (code) => {
438
- if (!kuriReady)
439
- exitedBeforeReady = true;
440
- log("kuri", `process exited with code ${code}`);
441
- kuriReady = false;
442
- kuriProcess = null;
443
- });
444
- const deadline = Date.now() + KURI_STARTUP_TIMEOUT_MS;
445
- while (Date.now() < deadline) {
446
- if (exitedBeforeReady)
447
- break;
448
- try {
449
- const res = await fetch(`http://127.0.0.1:${kuriPort}/health`, {
450
- signal: AbortSignal.timeout(500)
451
- });
452
- if (res.ok) {
453
- kuriReady = true;
454
- log("kuri", `ready on port ${kuriPort}`);
455
- await new Promise((r) => setTimeout(r, 300));
456
- if (!kuriCdpPort)
457
- await discoverCdpPort();
458
- await ensureTabsDiscovered();
459
- return;
554
+ let exitedBeforeReady = false;
555
+ kuriProcess = spawn(binary, [], {
556
+ env,
557
+ stdio: ["ignore", "pipe", "pipe"]
558
+ });
559
+ kuriProcess.stderr?.on("data", (chunk) => {
560
+ const line = chunk.toString().trim();
561
+ if (line)
562
+ log("kuri", `[stderr] ${line}`);
563
+ const cdpMatch = line.match(/CDP port:\s*(\d+)/);
564
+ if (cdpMatch) {
565
+ kuriCdpPort = parseInt(cdpMatch[1], 10);
566
+ log("kuri", `discovered CDP port: ${kuriCdpPort}`);
460
567
  }
568
+ });
569
+ kuriProcess.on("exit", (code) => {
570
+ if (!kuriReady)
571
+ exitedBeforeReady = true;
572
+ log("kuri", `process exited with code ${code}`);
573
+ kuriReady = false;
574
+ kuriProcess = null;
575
+ });
576
+ const deadline = Date.now() + KURI_STARTUP_TIMEOUT_MS;
577
+ while (Date.now() < deadline) {
578
+ if (exitedBeforeReady)
579
+ break;
580
+ try {
581
+ const res = await fetch(`http://127.0.0.1:${kuriPort}/health`, {
582
+ signal: AbortSignal.timeout(500)
583
+ });
584
+ if (res.ok) {
585
+ kuriReady = true;
586
+ log("kuri", `ready on port ${kuriPort}`);
587
+ await new Promise((r) => setTimeout(r, 300));
588
+ if (!kuriCdpPort)
589
+ await discoverCdpPort();
590
+ await ensureTabsDiscovered();
591
+ return;
592
+ }
593
+ } catch {}
594
+ await new Promise((r) => setTimeout(r, 200));
595
+ }
596
+ if (kuriReady)
597
+ return;
598
+ if (kuriProcess) {
599
+ kuriProcess.kill();
600
+ await waitForChildExit(kuriProcess);
601
+ }
602
+ try {
603
+ execFileSync("pkill", ["-f", `remote-debugging-port=${kuriCdpPort ?? 9222}`], { stdio: "ignore" });
604
+ await new Promise((r) => setTimeout(r, 1000));
461
605
  } catch {}
462
- await new Promise((r) => setTimeout(r, 200));
463
606
  }
464
- if (kuriReady)
465
- return;
466
- if (kuriProcess) {
467
- kuriProcess.kill();
468
- await waitForChildExit(kuriProcess);
607
+ throw new Error(`Kuri failed to start after ${maxAttempts} attempts`);
608
+ })();
609
+ kuriStartPromise = startPromise.finally(() => {
610
+ if (kuriStartPromise === startPromise) {
611
+ kuriStartPromise = null;
469
612
  }
470
- try {
471
- execFileSync("pkill", ["-f", `remote-debugging-port=${kuriCdpPort ?? 9222}`], { stdio: "ignore" });
472
- await new Promise((r) => setTimeout(r, 1000));
473
- } catch {}
474
- }
475
- throw new Error(`Kuri failed to start after ${maxAttempts} attempts`);
613
+ });
614
+ return kuriStartPromise;
476
615
  }
477
616
  async function stop() {
617
+ if (kuriStartPromise) {
618
+ await kuriStartPromise.catch(() => {});
619
+ }
478
620
  if (kuriProcess) {
479
621
  kuriProcess.kill("SIGTERM");
480
622
  kuriProcess = null;
481
623
  }
482
624
  kuriReady = false;
483
625
  kuriCdpPort = null;
626
+ kuriStartPromise = null;
484
627
  }
485
628
  async function discoverTabs() {
486
629
  await ensureTabsDiscovered();
@@ -574,6 +717,25 @@ async function evaluate(tabId, expression) {
574
717
  return inner.value;
575
718
  return inner.description ?? raw;
576
719
  }
720
+ function getKuriErrorMessage(value) {
721
+ if (typeof value === "string")
722
+ return null;
723
+ if (!value || typeof value !== "object")
724
+ return null;
725
+ const record = value;
726
+ if (typeof record.error === "string")
727
+ return record.error;
728
+ if (typeof record.message === "string")
729
+ return record.message;
730
+ if (record.result && typeof record.result === "object") {
731
+ const nested = record.result;
732
+ if (typeof nested.error === "string")
733
+ return nested.error;
734
+ if (typeof nested.message === "string")
735
+ return nested.message;
736
+ }
737
+ return null;
738
+ }
577
739
  async function getCookies(tabId) {
578
740
  const raw = await kuriGet("/cookies", { tab_id: tabId });
579
741
  return raw?.result?.cookies ?? [];
@@ -672,6 +834,9 @@ async function harStop(tabId) {
672
834
  async function networkEnable(tabId) {
673
835
  await kuriGet("/network", { tab_id: tabId, mode: "enable" });
674
836
  }
837
+ async function interceptStart(tabId) {
838
+ await kuriGet("/intercept/start", { tab_id: tabId });
839
+ }
675
840
  async function getText(tabId) {
676
841
  const result = await kuriGet("/text", { tab_id: tabId });
677
842
  return result?.text ?? "";
@@ -713,12 +878,106 @@ async function newTab(url) {
713
878
  }
714
879
  async function getCurrentUrl(tabId) {
715
880
  const result = await evaluate(tabId, "window.location.href");
716
- return String(result ?? "");
881
+ return typeof result === "string" ? result : "";
717
882
  }
718
883
  async function getPageHtml(tabId) {
719
884
  const result = await evaluate(tabId, "document.documentElement.outerHTML");
720
885
  return String(result ?? "");
721
886
  }
887
+ function extractLoadPlugins(value) {
888
+ if (typeof value !== "string")
889
+ return [];
890
+ return Array.from(new Set(value.split(/[\s,;]+/).map((part) => part.trim()).filter(Boolean)));
891
+ }
892
+ function extractLoadPluginsFromHtml(html) {
893
+ const modules = [];
894
+ const pattern = /data-load-plugins=(["'])(.*?)\1/gi;
895
+ for (const match of html.matchAll(pattern)) {
896
+ modules.push(...extractLoadPlugins(match[2]));
897
+ }
898
+ return Array.from(new Set(modules));
899
+ }
900
+ async function bestEffortRehydratePlugins(tabId) {
901
+ const result = await evaluate(tabId, `(async function() {
902
+ function splitPlugins(value) {
903
+ return String(value || "")
904
+ .split(/[\\s,;]+/)
905
+ .map(function(part) { return part.trim(); })
906
+ .filter(Boolean);
907
+ }
908
+ function pluginPath(name) {
909
+ if (/^https?:\\/\\//i.test(name) || name.startsWith("/")) return name;
910
+ return "/etc/designs/wrs/footLibs/js/plugins/" + (name.endsWith(".js") ? name : name + ".js");
911
+ }
912
+ var modules = Array.from(new Set(
913
+ Array.from(document.querySelectorAll("[data-load-plugins]"))
914
+ .flatMap(function(node) { return splitPlugins(node.getAttribute("data-load-plugins")); })
915
+ ));
916
+ if (modules.length === 0) {
917
+ return JSON.stringify({ attempted: false, loaded: false, nooped: true, reason: "no_plugins", modules: [] });
918
+ }
919
+ if (!window.WRS || typeof window.WRS.require !== "function") {
920
+ return JSON.stringify({ attempted: false, loaded: false, nooped: true, reason: "missing_wrs_require", modules: modules });
921
+ }
922
+ var requireWrs = window.WRS.require.bind(window.WRS);
923
+ async function loadModules(paths) {
924
+ return await new Promise(function(resolve) {
925
+ var done = false;
926
+ var timer = setTimeout(function() {
927
+ if (done) return;
928
+ done = true;
929
+ resolve({ ok: false, reason: "timeout" });
930
+ }, 1500);
931
+ try {
932
+ requireWrs(paths, function() {
933
+ if (done) return;
934
+ done = true;
935
+ clearTimeout(timer);
936
+ resolve({ ok: true });
937
+ });
938
+ } catch (error) {
939
+ if (done) return;
940
+ done = true;
941
+ clearTimeout(timer);
942
+ resolve({ ok: false, reason: error && error.message ? error.message : String(error) });
943
+ }
944
+ });
945
+ }
946
+ var configResult = await loadModules(["/etc/designs/wrs/footLibs/js/config.js"]);
947
+ var pluginResult = await loadModules(modules.map(pluginPath));
948
+ for (var i = 0; i < 6; i++) {
949
+ await new Promise(function(resolve) { return setTimeout(resolve, 100); });
950
+ }
951
+ return JSON.stringify({
952
+ attempted: true,
953
+ loaded: !!pluginResult.ok,
954
+ nooped: false,
955
+ reason: pluginResult.ok ? undefined : pluginResult.reason,
956
+ config_loaded: !!configResult.ok,
957
+ modules: modules,
958
+ });
959
+ })()`);
960
+ if (typeof result !== "string") {
961
+ return {
962
+ attempted: false,
963
+ loaded: false,
964
+ nooped: true,
965
+ reason: "invalid_rehydrate_result",
966
+ modules: []
967
+ };
968
+ }
969
+ try {
970
+ return JSON.parse(result);
971
+ } catch {
972
+ return {
973
+ attempted: false,
974
+ loaded: false,
975
+ nooped: true,
976
+ reason: "invalid_rehydrate_result",
977
+ modules: []
978
+ };
979
+ }
980
+ }
722
981
  async function hasCloudflareChallenge(tabId) {
723
982
  const result = await evaluate(tabId, `(function() {
724
983
  var html = document.documentElement.innerHTML;
@@ -765,6 +1024,20 @@ async function executeInPageFetch(tabId, url, method, headers, body) {
765
1024
  return { status: 0, data: result };
766
1025
  }
767
1026
  }
1027
+ async function health() {
1028
+ try {
1029
+ const result = await kuriGet("/health");
1030
+ return { ok: result?.ok === true || result?.status === "ok", tabs: result?.tabs };
1031
+ } catch {
1032
+ return { ok: false };
1033
+ }
1034
+ }
1035
+ function getPort() {
1036
+ return kuriPort;
1037
+ }
1038
+ function isReady() {
1039
+ return kuriReady;
1040
+ }
768
1041
  async function action(tabId, actionType, ref, value) {
769
1042
  const params = { tab_id: tabId, action: actionType, ref };
770
1043
  if (value !== undefined)
@@ -836,25 +1109,99 @@ async function waitForLoad(tabId, timeoutMs) {
836
1109
  async function keyboardType(tabId, text) {
837
1110
  return kuriGet("/keyboard/type", { tab_id: tabId, text });
838
1111
  }
1112
+ async function keyboardInsertText(tabId, text) {
1113
+ return kuriGet("/keyboard/inserttext", { tab_id: tabId, text });
1114
+ }
1115
+ async function keyDown(tabId, key) {
1116
+ return kuriGet("/keydown", { tab_id: tabId, key });
1117
+ }
1118
+ async function keyUp(tabId, key) {
1119
+ return kuriGet("/keyup", { tab_id: tabId, key });
1120
+ }
839
1121
  async function scrollIntoView(tabId, ref) {
840
1122
  return kuriGet("/scrollintoview", { tab_id: tabId, ref });
841
1123
  }
1124
+ async function drag(tabId, sourceRef, targetRef) {
1125
+ return kuriGet("/drag", { tab_id: tabId, source: sourceRef, target: targetRef });
1126
+ }
1127
+ async function domQuery(tabId, selector, all = false) {
1128
+ const params = { tab_id: tabId, selector };
1129
+ if (all)
1130
+ params.all = "true";
1131
+ return await kuriGet("/dom/query", params);
1132
+ }
1133
+ async function domHtml(tabId, nodeId) {
1134
+ return kuriGet("/dom/html", { tab_id: tabId, node_id: String(nodeId) });
1135
+ }
1136
+ async function domAttributes(tabId, opts) {
1137
+ const params = { tab_id: tabId };
1138
+ if (opts.ref)
1139
+ params.ref = opts.ref;
1140
+ if (opts.selector)
1141
+ params.selector = opts.selector;
1142
+ return kuriGet("/dom/attributes", params);
1143
+ }
842
1144
  async function scriptInject(tabId, source) {
843
1145
  return kuriPost("/script/inject", { tab_id: tabId }, { source });
844
1146
  }
1147
+ async function setCredentials(tabId, username, password) {
1148
+ return kuriGet("/set/credentials", { tab_id: tabId, username, password });
1149
+ }
1150
+ async function setViewport(tabId, width, height) {
1151
+ return kuriGet("/set/viewport", { tab_id: tabId, width: String(width), height: String(height) });
1152
+ }
1153
+ async function setUserAgent(tabId, ua) {
1154
+ return kuriGet("/set/useragent", { tab_id: tabId, ua });
1155
+ }
1156
+ async function sessionSave() {
1157
+ return kuriGet("/session/save");
1158
+ }
1159
+ async function sessionLoad(state) {
1160
+ return kuriPost("/session/load", {}, state);
1161
+ }
1162
+ async function sessionList() {
1163
+ return kuriGet("/session/list");
1164
+ }
845
1165
  async function goBack(tabId) {
846
1166
  return kuriGet("/back", { tab_id: tabId });
847
1167
  }
848
1168
  async function goForward(tabId) {
849
1169
  return kuriGet("/forward", { tab_id: tabId });
850
1170
  }
1171
+ async function reload(tabId) {
1172
+ return kuriGet("/reload", { tab_id: tabId });
1173
+ }
1174
+ async function getNetworkEvents(tabId) {
1175
+ return kuriGet("/network", { tab_id: tabId });
1176
+ }
1177
+ async function getPerfLcp(tabId) {
1178
+ return kuriGet("/perf/lcp", { tab_id: tabId });
1179
+ }
1180
+ async function findText(tabId, query) {
1181
+ return kuriGet("/find", { tab_id: tabId, query });
1182
+ }
1183
+ async function getLinks(tabId) {
1184
+ return kuriGet("/links", { tab_id: tabId });
1185
+ }
1186
+ async function getConsole(tabId) {
1187
+ return kuriGet("/console", { tab_id: tabId });
1188
+ }
1189
+ async function getErrors(tabId) {
1190
+ return kuriGet("/errors", { tab_id: tabId });
1191
+ }
851
1192
  async function authProfileSave(tabId, name) {
852
1193
  return kuriGet("/auth/profile/save", { tab_id: tabId, name });
853
1194
  }
854
1195
  async function authProfileLoad(tabId, name) {
855
1196
  return kuriGet("/auth/profile/load", { tab_id: tabId, name });
856
1197
  }
857
- var KURI_DEFAULT_PORT = 7700, KURI_STARTUP_TIMEOUT_MS = 1e4, KURI_REQUEST_TIMEOUT_MS = 30000, KURI_SPAWN_RETRIES = 3, KURI_SPAWN_RETRY_DELAY_MS = 1000, kuriProcess = null, kuriPort, kuriCdpPort = null, kuriReady = false;
1198
+ async function authProfileList(tabId) {
1199
+ return kuriGet("/auth/profile/list", { tab_id: tabId });
1200
+ }
1201
+ async function authProfileDelete(name) {
1202
+ return kuriGet("/auth/profile/delete", { name });
1203
+ }
1204
+ var KURI_DEFAULT_PORT = 7700, KURI_STARTUP_TIMEOUT_MS = 1e4, KURI_REQUEST_TIMEOUT_MS = 30000, KURI_SPAWN_RETRIES = 3, KURI_SPAWN_RETRY_DELAY_MS = 1000, KURI_PORT_SEARCH_LIMIT = 10, kuriProcess = null, kuriPort, kuriCdpPort = null, kuriReady = false, kuriStartPromise = null;
858
1205
  var init_client = __esm(() => {
859
1206
  init_logger();
860
1207
  init_paths();
@@ -1277,20 +1624,6 @@ function mergeContextTemplateParams(params, urlTemplate, contextUrl) {
1277
1624
  }
1278
1625
 
1279
1626
  // ../../src/graph/index.ts
1280
- var exports_graph = {};
1281
- __export(exports_graph, {
1282
- toAgentSkillChunkView: () => toAgentSkillChunkView,
1283
- resolveEndpointSemantic: () => resolveEndpointSemantic,
1284
- operationSoftPenalty: () => operationSoftPenalty,
1285
- knownBindingsFromInputs: () => knownBindingsFromInputs,
1286
- isRunnable: () => isRunnable,
1287
- isOperationHardExcluded: () => isOperationHardExcluded,
1288
- inferEndpointSemantic: () => inferEndpointSemantic,
1289
- getSkillChunk: () => getSkillChunk,
1290
- ensureSkillOperationGraph: () => ensureSkillOperationGraph,
1291
- computeReachableEndpoints: () => computeReachableEndpoints,
1292
- buildSkillOperationGraph: () => buildSkillOperationGraph
1293
- });
1294
1627
  function normalizeTokenText(text) {
1295
1628
  return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-zA-Z])(\d)/g, "$1 $2").replace(/(\d)([a-zA-Z])/g, "$1 $2");
1296
1629
  }
@@ -2654,8 +2987,8 @@ function templatizeBodyObject(value, context, path4 = "", bodyParams = {}) {
2654
2987
  function inferCsrfPlan(req, parsedBody) {
2655
2988
  const headers = Object.fromEntries(Object.entries(req.request_headers).map(([key, value]) => [key.toLowerCase(), value]));
2656
2989
  const cookies = parseCookieHeader(headers["cookie"]);
2657
- const csrfCookieNames = Object.keys(cookies).filter((name) => /^(ct0|csrf_token|_csrf|csrftoken|xsrf-token|_xsrf)$/i.test(name));
2658
- const headerName = ["x-csrf-token", "x-xsrf-token", "x-csrftoken"].find((name) => typeof headers[name] === "string" && headers[name].length > 0);
2990
+ const csrfCookieNames = Object.keys(cookies).filter((name) => /^(ct0|csrf_token|_csrf|csrftoken|xsrf-token|_xsrf|jsessionid)$/i.test(name));
2991
+ const headerName = ["x-csrf-token", "x-xsrf-token", "x-csrftoken", "csrf-token"].find((name) => typeof headers[name] === "string" && headers[name].length > 0);
2659
2992
  if (headerName && csrfCookieNames.length > 0) {
2660
2993
  return {
2661
2994
  source: "cookie",
@@ -3165,6 +3498,15 @@ function isSensitiveHeader(name) {
3165
3498
  return true;
3166
3499
  return false;
3167
3500
  }
3501
+ function isReplayCriticalHeader(name, value) {
3502
+ const lower = name.toLowerCase();
3503
+ if (REPLAY_HEADER_EXACT.has(lower)) {
3504
+ if (lower !== "accept")
3505
+ return true;
3506
+ return /application\/vnd\./i.test(value);
3507
+ }
3508
+ return REPLAY_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix));
3509
+ }
3168
3510
  function sanitizeHeaders(headers) {
3169
3511
  return Object.fromEntries(Object.entries(headers ?? {}).filter(([k]) => {
3170
3512
  const lower = k.toLowerCase();
@@ -3180,7 +3522,7 @@ function extractAuthHeaders(requests) {
3180
3522
  const lower = k.toLowerCase();
3181
3523
  if (lower === "cookie" || lower === "content-length" || lower === "host")
3182
3524
  continue;
3183
- if (isSensitiveHeader(k) && !authHeaders[lower]) {
3525
+ if ((isSensitiveHeader(k) || isReplayCriticalHeader(k, v)) && !authHeaders[lower]) {
3184
3526
  authHeaders[lower] = v;
3185
3527
  }
3186
3528
  }
@@ -3283,7 +3625,12 @@ function templatizePathSegments(templateUrl, originalUrl, context) {
3283
3625
  try {
3284
3626
  const contextSegments = new URL(context.pageUrl).pathname.split("/");
3285
3627
  const contextSeg = contextSegments[i];
3286
- if (contextSeg && contextSeg !== tSeg && !contextSeg.includes(".") && contextSeg.length >= 2 && contextSeg.length <= 40 && !/^(api|v\d+|www|en|es|fr|de|latest|search|i)$/i.test(contextSeg)) {
3628
+ const prevSeg = tSegments[i - 1] ?? "";
3629
+ const prevContextSeg = contextSegments[i - 1] ?? "";
3630
+ const nextSeg = tSegments[i + 1] ?? "";
3631
+ const nextContextSeg = contextSegments[i + 1] ?? "";
3632
+ const hasStructuralNeighborMatch = !!prevSeg && !!prevContextSeg && prevSeg === prevContextSeg || !!nextSeg && !!nextContextSeg && nextSeg === nextContextSeg;
3633
+ if (contextSeg && contextSeg !== tSeg && hasStructuralNeighborMatch && !contextSeg.includes(".") && contextSeg.length >= 2 && contextSeg.length <= 40 && !/^(api|v\d+|www|en|es|fr|de|latest|search|i)$/i.test(contextSeg)) {
3287
3634
  const paramName = inferParamName(tSegments, i, "slug", usedNames);
3288
3635
  tSegments[i] = `{${paramName}}`;
3289
3636
  pathParams[paramName] = contextSeg;
@@ -3481,7 +3828,7 @@ function isCloudflareChallenge(responseBody) {
3481
3828
  const bodyLower = responseBody.toLowerCase();
3482
3829
  return CF_MARKERS.some((marker) => bodyLower.includes(marker.toLowerCase()));
3483
3830
  }
3484
- var SKIP_EXTENSIONS, SKIP_JS_BUNDLES, SKIP_PATHS, SKIP_HOSTS, SKIP_TELEMETRY_HOSTS, SKIP_TELEMETRY_PATHS, RPC_HINTS, ALLOWED_METHODS, STRIP_HEADERS, STRIP_HEADER_PREFIXES, SAFE_HEADERS, SENSITIVE_HEADER_PATTERN, SENSITIVE_QUERY_PARAMS, FRAMEWORK_QUERY_PARAMS, AD_HOSTS, AD_SCHEMA_KEYS, AD_SCHEMA_THRESHOLD = 3, ON_DOMAIN_NOISE;
3831
+ var SKIP_EXTENSIONS, SKIP_JS_BUNDLES, SKIP_PATHS, SKIP_HOSTS, SKIP_TELEMETRY_HOSTS, SKIP_TELEMETRY_PATHS, RPC_HINTS, ALLOWED_METHODS, STRIP_HEADERS, STRIP_HEADER_PREFIXES, REPLAY_HEADER_PREFIXES, REPLAY_HEADER_EXACT, SAFE_HEADERS, SENSITIVE_HEADER_PATTERN, SENSITIVE_QUERY_PARAMS, FRAMEWORK_QUERY_PARAMS, AD_HOSTS, AD_SCHEMA_KEYS, AD_SCHEMA_THRESHOLD = 3, ON_DOMAIN_NOISE;
3485
3832
  var init_reverse_engineer = __esm(() => {
3486
3833
  init_transform();
3487
3834
  init_domain();
@@ -3520,6 +3867,16 @@ var init_reverse_engineer = __esm(() => {
3520
3867
  "x-stripe-",
3521
3868
  "x-firebase-"
3522
3869
  ];
3870
+ REPLAY_HEADER_PREFIXES = [
3871
+ "x-li-"
3872
+ ];
3873
+ REPLAY_HEADER_EXACT = new Set([
3874
+ "accept",
3875
+ "csrf-token",
3876
+ "origin",
3877
+ "x-requested-with",
3878
+ "x-restli-protocol-version"
3879
+ ]);
3523
3880
  SAFE_HEADERS = new Set([
3524
3881
  "accept",
3525
3882
  "accept-encoding",
@@ -4732,6 +5089,8 @@ __export(exports_client2, {
4732
5089
  registerAgent: () => registerAgent,
4733
5090
  recordTransaction: () => recordTransaction,
4734
5091
  recordOrchestrationPerf: () => recordOrchestrationPerf,
5092
+ recordInstallTelemetryEvent: () => recordInstallTelemetryEvent2,
5093
+ recordFunnelTelemetryEvent: () => recordFunnelTelemetryEvent2,
4735
5094
  recordFeedback: () => recordFeedback,
4736
5095
  recordExecution: () => recordExecution,
4737
5096
  recordDiagnostics: () => recordDiagnostics,
@@ -4749,14 +5108,18 @@ __export(exports_client2, {
4749
5108
  getSkill: () => getSkill,
4750
5109
  getRecentLocalSkill: () => getRecentLocalSkill,
4751
5110
  getMyProfile: () => getMyProfile2,
5111
+ getLocalWalletContext: () => getLocalWalletContext2,
5112
+ getInstallId: () => getInstallId2,
4752
5113
  getEndpointSchema: () => getEndpointSchema,
4753
5114
  getCreatorEarnings: () => getCreatorEarnings,
4754
5115
  getApiKey: () => getApiKey2,
4755
5116
  getAgentId: () => getAgentId,
4756
5117
  getAgent: () => getAgent,
4757
- getActiveProfile: () => getActiveProfile,
5118
+ getActiveProfile: () => getActiveProfile2,
4758
5119
  findExistingSkillForDomain: () => findExistingSkillForDomain,
4759
5120
  ensureRegistered: () => ensureRegistered2,
5121
+ ensureCliInstallTracked: () => ensureCliInstallTracked2,
5122
+ detectTelemetryHostType: () => detectTelemetryHostType2,
4760
5123
  deprecateSkill: () => deprecateSkill,
4761
5124
  cachePublishedSkill: () => cachePublishedSkill,
4762
5125
  buildExecutionPayload: () => buildExecutionPayload,
@@ -4800,10 +5163,13 @@ function getConfigDir2() {
4800
5163
  function getConfigPath2() {
4801
5164
  return join3(getConfigDir2(), "config.json");
4802
5165
  }
5166
+ function getInstallTelemetryPath2() {
5167
+ return join3(getConfigDir2(), "install-state.json");
5168
+ }
4803
5169
  function sanitizeProfileName2(value) {
4804
5170
  return value.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4805
5171
  }
4806
- function getActiveProfile() {
5172
+ function getActiveProfile2() {
4807
5173
  return PROFILE_NAME2 || "default";
4808
5174
  }
4809
5175
  function isLocalOnlyMode() {
@@ -4825,6 +5191,120 @@ function saveConfig2(config) {
4825
5191
  mkdirSync4(configDir, { recursive: true });
4826
5192
  writeFileSync3(configPath, JSON.stringify(config, null, 2), { mode: 384 });
4827
5193
  }
5194
+ function loadInstallTelemetryState2() {
5195
+ try {
5196
+ const statePath = getInstallTelemetryPath2();
5197
+ if (existsSync4(statePath)) {
5198
+ return JSON.parse(readFileSync2(statePath, "utf-8"));
5199
+ }
5200
+ } catch {}
5201
+ return null;
5202
+ }
5203
+ function saveInstallTelemetryState2(state) {
5204
+ const configDir = getConfigDir2();
5205
+ const statePath = getInstallTelemetryPath2();
5206
+ if (!existsSync4(configDir))
5207
+ mkdirSync4(configDir, { recursive: true });
5208
+ writeFileSync3(statePath, JSON.stringify(state, null, 2), { mode: 384 });
5209
+ }
5210
+ function createInstallTelemetryState2() {
5211
+ return {
5212
+ install_id: `install_${randomBytes2(8).toString("hex")}`,
5213
+ first_seen_at: new Date().toISOString()
5214
+ };
5215
+ }
5216
+ function getOrCreateInstallTelemetryState2() {
5217
+ const existing = loadInstallTelemetryState2();
5218
+ if (existing?.install_id)
5219
+ return existing;
5220
+ const created = createInstallTelemetryState2();
5221
+ saveInstallTelemetryState2(created);
5222
+ return created;
5223
+ }
5224
+ function getInstallId2() {
5225
+ return getOrCreateInstallTelemetryState2().install_id;
5226
+ }
5227
+ function detectTelemetryHostType2() {
5228
+ switch (detectHostEnvironment()) {
5229
+ case "openai":
5230
+ return "codex";
5231
+ case "openclaw":
5232
+ return "openclaw";
5233
+ case "mcp":
5234
+ return "mcp";
5235
+ case "native":
5236
+ return "native";
5237
+ case "unknown":
5238
+ default:
5239
+ return "cli";
5240
+ }
5241
+ }
5242
+ async function postTelemetry2(path4, body) {
5243
+ if (LOCAL_ONLY2)
5244
+ return false;
5245
+ try {
5246
+ const key = getApiKey2();
5247
+ const res = await fetch(`${API_URL2}${path4}`, {
5248
+ method: "POST",
5249
+ headers: {
5250
+ "Content-Type": "application/json",
5251
+ "Accept-Encoding": "gzip, deflate",
5252
+ ...key ? { Authorization: `Bearer ${key}` } : {}
5253
+ },
5254
+ body: JSON.stringify(body)
5255
+ });
5256
+ return res.ok;
5257
+ } catch {
5258
+ return false;
5259
+ }
5260
+ }
5261
+ async function ensureCliInstallTracked2(hostType = detectTelemetryHostType2()) {
5262
+ const state = getOrCreateInstallTelemetryState2();
5263
+ if (state.cli_first_seen_reported_at)
5264
+ return;
5265
+ const createdAt = new Date().toISOString();
5266
+ const ok = await postTelemetry2("/v1/telemetry/install", {
5267
+ install_id: state.install_id,
5268
+ source: "cli-first-seen",
5269
+ host_type: hostType,
5270
+ skill: "unbrowse",
5271
+ status: "installed",
5272
+ created_at: createdAt,
5273
+ properties: {
5274
+ profile: getActiveProfile2(),
5275
+ first_seen_at: state.first_seen_at
5276
+ }
5277
+ });
5278
+ if (!ok)
5279
+ return;
5280
+ state.cli_first_seen_reported_at = createdAt;
5281
+ saveInstallTelemetryState2(state);
5282
+ }
5283
+ async function recordInstallTelemetryEvent2(source, options) {
5284
+ const createdAt = options?.createdAt ?? new Date().toISOString();
5285
+ await postTelemetry2("/v1/telemetry/install", {
5286
+ install_id: getInstallId2(),
5287
+ source,
5288
+ host_type: options?.hostType ?? detectTelemetryHostType2(),
5289
+ skill: options?.skill ?? "unbrowse",
5290
+ skill_version: options?.skillVersion,
5291
+ status: options?.status ?? "installed",
5292
+ created_at: createdAt,
5293
+ properties: options?.properties
5294
+ });
5295
+ }
5296
+ async function recordFunnelTelemetryEvent2(name, options) {
5297
+ const createdAt = options?.createdAt ?? new Date().toISOString();
5298
+ await postTelemetry2("/v1/telemetry/events", {
5299
+ install_id: getInstallId2(),
5300
+ session_id: options?.sessionId,
5301
+ name,
5302
+ source: options?.source ?? "cli",
5303
+ host_type: options?.hostType ?? detectTelemetryHostType2(),
5304
+ created_at: createdAt,
5305
+ properties: options?.properties
5306
+ });
5307
+ }
4828
5308
  function normalizeAgentEmail2(value) {
4829
5309
  return value.trim().toLowerCase();
4830
5310
  }
@@ -5115,6 +5595,12 @@ async function ensureRegistered2(options) {
5115
5595
  tos_accepted_at: new Date().toISOString(),
5116
5596
  ...wallet
5117
5597
  });
5598
+ await recordFunnelTelemetryEvent2("registration_succeeded", {
5599
+ source: "cli",
5600
+ properties: {
5601
+ prompt_for_email: options?.promptForEmail === true
5602
+ }
5603
+ });
5118
5604
  console.log(`Registered as ${name}. API key saved to ~/.unbrowse/config.json`);
5119
5605
  } catch (err) {
5120
5606
  console.warn(`Registration failed: ${err.message}`);
@@ -5376,6 +5862,11 @@ async function recordExecution(skillId, endpointId, trace, skill) {
5376
5862
  const payload = buildExecutionPayload(skillId, endpointId, trace, skill);
5377
5863
  await api2("POST", "/v1/stats/execution", payload);
5378
5864
  }
5865
+ async function recordAnalyticsSession(payload) {
5866
+ if (LOCAL_ONLY2)
5867
+ return;
5868
+ await api2("POST", "/v1/analytics/sessions", payload);
5869
+ }
5379
5870
  async function recordTransaction(params) {
5380
5871
  if (LOCAL_ONLY2)
5381
5872
  return;
@@ -5418,11 +5909,6 @@ async function recordOrchestrationPerf(timing) {
5418
5909
  const phaseTotals = Object.fromEntries(attributeLifecycle(events));
5419
5910
  await api2("POST", "/v1/stats/perf", { ...timing, phase_totals_ms: phaseTotals });
5420
5911
  }
5421
- async function recordAnalyticsSession(session) {
5422
- if (LOCAL_ONLY2)
5423
- return;
5424
- await api2("POST", "/v1/analytics/sessions", session);
5425
- }
5426
5912
  async function validateManifest(manifest) {
5427
5913
  if (LOCAL_ONLY2)
5428
5914
  return { valid: true, hardErrors: [], softWarnings: [] };
@@ -6338,6 +6824,32 @@ import fs2 from "node:fs";
6338
6824
  function getProfilePath(domain) {
6339
6825
  return path4.join(os3.homedir(), ".unbrowse", "profiles", getRegistrableDomain(domain));
6340
6826
  }
6827
+ function assessInteractiveLoginState(input) {
6828
+ let parsed;
6829
+ try {
6830
+ parsed = new URL(input.currentUrl);
6831
+ } catch {
6832
+ return { status: "pending", reason: "invalid_url" };
6833
+ }
6834
+ const currentDomain = parsed.hostname.toLowerCase();
6835
+ const targetNorm = input.targetDomain.toLowerCase();
6836
+ const isOnTarget = currentDomain === targetNorm || currentDomain.endsWith(`.${targetNorm}`);
6837
+ if (!isOnTarget)
6838
+ return { status: "pending", reason: "off_target_domain" };
6839
+ if (input.hasCloudflareChallenge)
6840
+ return { status: "blocked", reason: "cloudflare_challenge" };
6841
+ if (input.pageText && CLOUDFLARE_TEXT.test(input.pageText))
6842
+ return { status: "blocked", reason: "cloudflare_text" };
6843
+ if (LOGIN_PATHS.test(parsed.pathname))
6844
+ return { status: "pending", reason: "still_on_login_path" };
6845
+ if (input.currentCookieCount > input.initialCookieCount) {
6846
+ return { status: "authenticated", reason: "new_cookies_on_target" };
6847
+ }
6848
+ if (input.currentCookieCount > 0) {
6849
+ return { status: "authenticated", reason: "cookies_present_on_target" };
6850
+ }
6851
+ return { status: "pending", reason: "no_session_cookies" };
6852
+ }
6341
6853
  async function interactiveLogin(url, domain) {
6342
6854
  const targetDomain = domain ?? new URL(url).hostname;
6343
6855
  const profileDir = getProfilePath(targetDomain);
@@ -6360,6 +6872,7 @@ async function interactiveLogin(url, domain) {
6360
6872
  const initialCookieCount = initialCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
6361
6873
  log("auth", `initial cookies for ${targetDomain}: ${initialCookieCount}`);
6362
6874
  let loggedIn = false;
6875
+ let blockedReason = null;
6363
6876
  let lastLoggedUrl = "";
6364
6877
  const effectiveTimeout = loginConfig.interactive ? LOGIN_TIMEOUT_MS : loginConfig.timeout_ms;
6365
6878
  while (Date.now() - startTime < effectiveTimeout) {
@@ -6367,37 +6880,40 @@ async function interactiveLogin(url, domain) {
6367
6880
  const elapsed = Date.now() - startTime;
6368
6881
  try {
6369
6882
  const currentUrl = await getCurrentUrl(tabId);
6370
- const currentDomain = new URL(currentUrl).hostname.toLowerCase();
6371
- const targetNorm = targetDomain.toLowerCase();
6372
6883
  if (currentUrl !== lastLoggedUrl) {
6373
6884
  log("auth", `navigated to: ${currentUrl}`);
6374
6885
  lastLoggedUrl = currentUrl;
6375
6886
  }
6376
6887
  if (elapsed < MIN_WAIT_MS)
6377
6888
  continue;
6378
- const isOnTarget = currentDomain === targetNorm || currentDomain.endsWith("." + targetNorm);
6379
- if (isOnTarget) {
6380
- const isStillLogin = /\/(login|signin|sign-in|sso|auth|oauth|uas\/login|checkpoint)/.test(new URL(currentUrl).pathname);
6381
- const currentCookies = await getCookies(tabId);
6382
- const currentCookieCount = currentCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
6383
- const gotNewCookies = currentCookieCount > initialCookieCount;
6384
- if (!isStillLogin && gotNewCookies) {
6385
- loggedIn = true;
6386
- log("auth", `login complete — ${currentUrl} (cookies: ${initialCookieCount} → ${currentCookieCount})`);
6387
- break;
6388
- }
6389
- if (!isStillLogin && currentCookieCount > 0) {
6390
- loggedIn = true;
6391
- log("auth", `already logged in — ${currentUrl} (${currentCookieCount} cookies present)`);
6392
- break;
6393
- }
6889
+ const currentCookies = await getCookies(tabId);
6890
+ const currentCookieCount = currentCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
6891
+ const hasCloudflareChallenge2 = await hasCloudflareChallenge(tabId).catch(() => false);
6892
+ const pageText = hasCloudflareChallenge2 ? await getText(tabId).catch(() => "") : "";
6893
+ const assessment = assessInteractiveLoginState({
6894
+ currentUrl,
6895
+ targetDomain,
6896
+ initialCookieCount,
6897
+ currentCookieCount,
6898
+ hasCloudflareChallenge: hasCloudflareChallenge2,
6899
+ pageText
6900
+ });
6901
+ if (assessment.status === "authenticated") {
6902
+ loggedIn = true;
6903
+ log("auth", `login complete — ${currentUrl} (cookies: ${initialCookieCount} → ${currentCookieCount}; ${assessment.reason})`);
6904
+ break;
6905
+ }
6906
+ if (assessment.status === "blocked") {
6907
+ blockedReason = assessment.reason;
6908
+ log("auth", `login blocked — ${currentUrl} (${assessment.reason})`);
6394
6909
  }
6395
6910
  } catch {}
6396
6911
  }
6397
6912
  if (!loggedIn) {
6398
6913
  log("auth", `login wait ended after ${Math.round((Date.now() - startTime) / 1000)}s — fallback: ${loginConfig.fallback_strategy}`);
6399
6914
  if (loginConfig.fallback_strategy === "fail") {
6400
- return { success: false, domain: targetDomain, cookies_stored: 0, error: "Login timed out (fallback: fail)" };
6915
+ const error = blockedReason ? `Login blocked (${blockedReason})` : "Login timed out (fallback: fail)";
6916
+ return { success: false, domain: targetDomain, cookies_stored: 0, error };
6401
6917
  }
6402
6918
  if (loginConfig.fallback_strategy === "skip") {
6403
6919
  log("auth", `skipping cookie capture per fallback_strategy`);
@@ -6533,13 +7049,15 @@ async function refreshAuthFromBrowser(domain) {
6533
7049
  }
6534
7050
  return false;
6535
7051
  }
6536
- var LOGIN_TIMEOUT_MS = 300000, POLL_INTERVAL_MS = 2000, MIN_WAIT_MS = 15000;
7052
+ var LOGIN_TIMEOUT_MS = 300000, POLL_INTERVAL_MS = 2000, MIN_WAIT_MS = 15000, LOGIN_PATHS, CLOUDFLARE_TEXT;
6537
7053
  var init_auth = __esm(async () => {
6538
7054
  init_client();
6539
7055
  init_domain();
6540
7056
  init_logger();
6541
7057
  init_supervisor();
6542
7058
  await init_vault();
7059
+ LOGIN_PATHS = /\/(login|signin|sign-in|sso|auth|oauth|uas\/login|checkpoint)/i;
7060
+ CLOUDFLARE_TEXT = /just a moment|attention required|verify you are human|cloudflare/i;
6543
7061
  });
6544
7062
 
6545
7063
  // ../../src/auth/runtime.ts
@@ -9708,6 +10226,116 @@ var init_payments = __esm(() => {
9708
10226
  PRICING_API_URL = process.env.UNBROWSE_BACKEND_URL ?? "https://beta-api.unbrowse.ai";
9709
10227
  });
9710
10228
 
10229
+ // ../../src/execution/robots.ts
10230
+ function parseRobotsTxt(text) {
10231
+ const groups = [];
10232
+ let current = null;
10233
+ for (const rawLine of text.split(/\r?\n/)) {
10234
+ const line = rawLine.replace(/#.*$/, "").trim();
10235
+ if (!line) {
10236
+ current = null;
10237
+ continue;
10238
+ }
10239
+ const colon = line.indexOf(":");
10240
+ if (colon === -1)
10241
+ continue;
10242
+ const field = line.slice(0, colon).trim().toLowerCase();
10243
+ const value = line.slice(colon + 1).trim();
10244
+ if (field === "user-agent") {
10245
+ if (!current) {
10246
+ current = { agents: [], disallow: [], allow: [] };
10247
+ groups.push(current);
10248
+ }
10249
+ current.agents.push(value.toLowerCase());
10250
+ } else if (field === "disallow") {
10251
+ if (current && value)
10252
+ current.disallow.push(value);
10253
+ } else if (field === "allow") {
10254
+ if (current && value)
10255
+ current.allow.push(value);
10256
+ } else if (field === "crawl-delay") {
10257
+ if (current)
10258
+ current.crawlDelay = parseFloat(value);
10259
+ } else {
10260
+ current = null;
10261
+ }
10262
+ }
10263
+ return groups;
10264
+ }
10265
+ function selectRules(groups, agent) {
10266
+ const lower = agent.toLowerCase();
10267
+ const exact = groups.find((g) => g.agents.some((a) => a === lower));
10268
+ if (exact)
10269
+ return exact;
10270
+ return groups.find((g) => g.agents.includes("*")) ?? null;
10271
+ }
10272
+ function pathMatches(path5, prefix) {
10273
+ if (prefix.endsWith("$")) {
10274
+ return path5 === prefix.slice(0, -1);
10275
+ }
10276
+ return path5.startsWith(prefix);
10277
+ }
10278
+ function longestMatch(path5, patterns) {
10279
+ let best = -1;
10280
+ for (const p of patterns) {
10281
+ const base = p.endsWith("$") ? p.slice(0, -1) : p;
10282
+ if (pathMatches(path5, p) && base.length > best)
10283
+ best = base.length;
10284
+ }
10285
+ return best;
10286
+ }
10287
+ async function fetchRules(origin) {
10288
+ const now = Date.now();
10289
+ const cached = cache.get(origin);
10290
+ if (cached && now - cached.fetchedAt < TTL_MS)
10291
+ return cached.rules;
10292
+ try {
10293
+ const res = await fetch(`${origin}/robots.txt`, {
10294
+ headers: { "user-agent": USER_AGENT },
10295
+ redirect: "follow",
10296
+ signal: AbortSignal.timeout(5000)
10297
+ });
10298
+ if (!res.ok) {
10299
+ cache.set(origin, { rules: [], fetchedAt: now });
10300
+ return [];
10301
+ }
10302
+ const text = await res.text();
10303
+ const rules = parseRobotsTxt(text);
10304
+ cache.set(origin, { rules, fetchedAt: now });
10305
+ return rules;
10306
+ } catch {
10307
+ cache.set(origin, { rules: [], fetchedAt: now });
10308
+ return [];
10309
+ }
10310
+ }
10311
+ async function isAllowedByRobots(url) {
10312
+ let origin;
10313
+ let pathname;
10314
+ try {
10315
+ const parsed = new URL(url);
10316
+ origin = parsed.origin;
10317
+ pathname = parsed.pathname || "/";
10318
+ } catch {
10319
+ return true;
10320
+ }
10321
+ const groups = await fetchRules(origin);
10322
+ const rules = selectRules(groups, USER_AGENT);
10323
+ if (!rules)
10324
+ return true;
10325
+ const allowLen = longestMatch(pathname, rules.allow);
10326
+ const disallowLen = longestMatch(pathname, rules.disallow);
10327
+ if (disallowLen < 0)
10328
+ return true;
10329
+ if (allowLen >= disallowLen)
10330
+ return true;
10331
+ return false;
10332
+ }
10333
+ var USER_AGENT = "unbrowse", TTL_MS, cache;
10334
+ var init_robots = __esm(() => {
10335
+ TTL_MS = 24 * 60 * 60 * 1000;
10336
+ cache = new Map;
10337
+ });
10338
+
9711
10339
  // ../../src/execution/index.ts
9712
10340
  var exports_execution = {};
9713
10341
  __export(exports_execution, {
@@ -10144,6 +10772,20 @@ function buildStructuredReplayHeaders(originalUrl, replayUrl, baseHeaders) {
10144
10772
  }
10145
10773
  return headers;
10146
10774
  }
10775
+ function normalizeReplayHeaders(...bags) {
10776
+ const normalized = {};
10777
+ for (const bag of bags) {
10778
+ for (const [key, value] of Object.entries(bag ?? {})) {
10779
+ if (typeof value !== "string")
10780
+ continue;
10781
+ const trimmed = value.trim();
10782
+ if (!trimmed)
10783
+ continue;
10784
+ normalized[key.toLowerCase()] = trimmed;
10785
+ }
10786
+ }
10787
+ return normalized;
10788
+ }
10147
10789
  function shouldFallbackToBrowserReplay(data, endpoint, intent, contextUrl) {
10148
10790
  const replayUrl = resolveExecutionUrlTemplate(endpoint, contextUrl);
10149
10791
  if (!isDocumentLikeUrl(replayUrl))
@@ -10540,11 +11182,11 @@ async function executeBrowserCapture(skill, params, options) {
10540
11182
  }
10541
11183
  })();
10542
11184
  const AUTH_PROVIDERS = /accounts\.google\.com|login\.microsoftonline\.com|auth0\.com|cognito-idp\.|appleid\.apple\.com|github\.com|facebook\.com/i;
10543
- const LOGIN_PATHS = /\/(login|signin|sign-in|sso|auth|uas\/login|checkpoint|oauth)/i;
11185
+ const LOGIN_PATHS2 = /\/(login|signin|sign-in|sso|auth|uas\/login|checkpoint|oauth)/i;
10544
11186
  const redirectedToAuth = finalDomain !== targetDomain && AUTH_PROVIDERS.test(finalDomain);
10545
11187
  const redirectedToLogin = captured.final_url !== url && (() => {
10546
11188
  try {
10547
- return LOGIN_PATHS.test(new URL(String(captured.final_url)).pathname);
11189
+ return LOGIN_PATHS2.test(new URL(String(captured.final_url)).pathname);
10548
11190
  } catch {
10549
11191
  return false;
10550
11192
  }
@@ -10651,7 +11293,7 @@ async function executeBrowserCapture(skill, params, options) {
10651
11293
  const cleanEndpoints = endpoints.filter((ep) => {
10652
11294
  try {
10653
11295
  const host = new URL(ep.url_template).hostname;
10654
- return !AUTH_PROVIDERS.test(host) && !LOGIN_PATHS.test(new URL(ep.url_template).pathname);
11296
+ return !AUTH_PROVIDERS.test(host) && !LOGIN_PATHS2.test(new URL(ep.url_template).pathname);
10655
11297
  } catch {
10656
11298
  return true;
10657
11299
  }
@@ -11067,9 +11709,9 @@ async function executeEndpoint(skill, endpoint, params = {}, projection, options
11067
11709
  }
11068
11710
  }
11069
11711
  if (!skill.skill_id.startsWith("local:") && skill.execution_type === "http" && skill.owner_type !== "agent") {
11070
- const walletAddr = process.env.LOBSTER_WALLET_ADDRESS;
11712
+ const wallet = getLocalWalletContext2();
11071
11713
  const gate = await checkPaymentRequirement(skill.skill_id, endpoint.endpoint_id, {
11072
- wallet_configured: !!walletAddr
11714
+ wallet_configured: !!wallet.wallet_address
11073
11715
  });
11074
11716
  if (gate.status === "payment_required" || gate.status === "wallet_not_configured" || gate.status === "insufficient_balance") {
11075
11717
  const trace2 = stampTrace({
@@ -11089,7 +11731,8 @@ async function executeEndpoint(skill, endpoint, params = {}, projection, options
11089
11731
  price_usd: gate.requirement?.amount,
11090
11732
  payment_status: gate.status,
11091
11733
  message: gate.message,
11092
- wallet_provider: "lobster.cash",
11734
+ wallet_provider: wallet.wallet_provider ?? "lobster.cash",
11735
+ wallet_address: wallet.wallet_address,
11093
11736
  indexing_fallback_available: true
11094
11737
  }
11095
11738
  };
@@ -11258,14 +11901,39 @@ async function executeEndpoint(skill, endpoint, params = {}, projection, options
11258
11901
  } catch {}
11259
11902
  }
11260
11903
  }
11904
+ const hasAuthContext = cookies.length > 0 || Object.keys(authHeaders).length > 0 || !!skill.auth_profile_ref || endpoint.semantic?.auth_required === true;
11905
+ if (!options?.skip_robots_check && !hasAuthContext) {
11906
+ const allowed = await isAllowedByRobots(url);
11907
+ if (!allowed) {
11908
+ const traceId = nanoid5();
11909
+ log("exec", `robots.txt blocked ${url}`);
11910
+ return {
11911
+ trace: stampTrace({
11912
+ trace_id: traceId,
11913
+ skill_id: skill.skill_id,
11914
+ endpoint_id: endpoint.endpoint_id,
11915
+ started_at: startedAt,
11916
+ completed_at: new Date().toISOString(),
11917
+ success: false,
11918
+ error: "robots_txt_disallowed"
11919
+ }),
11920
+ result: {
11921
+ error: "robots_txt_disallowed",
11922
+ message: `robots.txt disallows access to ${url} for the Unbrowse user-agent.`
11923
+ }
11924
+ };
11925
+ }
11926
+ }
11261
11927
  const structuredReplayUrl = isSafe ? deriveStructuredDataReplayUrl(url) : url;
11262
11928
  const hasStructuredReplay = structuredReplayUrl !== url;
11263
11929
  const serverFetch = async () => {
11264
- const defaultAccept = !endpoint.dom_extraction && !endpoint.headers_template?.["accept"] ? { accept: "application/json" } : {};
11930
+ const endpointHeaders = normalizeReplayHeaders(endpoint.headers_template);
11931
+ const sessionHeaders = normalizeReplayHeaders(authHeaders);
11932
+ const defaultAccept = !endpoint.dom_extraction && !endpointHeaders["accept"] && !sessionHeaders["accept"] ? { accept: "application/json" } : {};
11265
11933
  const headers = {
11266
11934
  ...defaultAccept,
11267
- ...endpoint.headers_template,
11268
- ...authHeaders
11935
+ ...endpointHeaders,
11936
+ ...sessionHeaders
11269
11937
  };
11270
11938
  delete headers["sec-ch-ua"];
11271
11939
  delete headers["sec-ch-ua-mobile"];
@@ -11279,7 +11947,7 @@ async function executeEndpoint(skill, endpoint, params = {}, projection, options
11279
11947
  headers["cookie"] = cookieStr;
11280
11948
  const csrfCookie = cookies.find((c) => /^(ct0|csrf_token|_csrf|csrftoken|XSRF-TOKEN|_xsrf)$/i.test(c.name));
11281
11949
  if (csrfCookie) {
11282
- const v = csrfCookie.value.startsWith(") && csrfCookie.value.endsWith(") ? csrfCookie.value.slice(1, -1) : csrfCookie.value;
11950
+ const v = csrfCookie.value.startsWith('"') && csrfCookie.value.endsWith('"') ? csrfCookie.value.slice(1, -1) : csrfCookie.value;
11283
11951
  headers["x-csrf-token"] = v;
11284
11952
  headers["x-xsrf-token"] = v;
11285
11953
  }
@@ -11289,7 +11957,7 @@ async function executeEndpoint(skill, endpoint, params = {}, projection, options
11289
11957
  if (csrfCookie) {
11290
11958
  const v = csrfCookie.value.startsWith('"') && csrfCookie.value.endsWith('"') ? csrfCookie.value.slice(1, -1) : csrfCookie.value;
11291
11959
  if (endpoint.csrf_plan.source === "cookie" || endpoint.csrf_plan.source === "header") {
11292
- headers[endpoint.csrf_plan.param_name.toLowerCase()] ??= v;
11960
+ headers[endpoint.csrf_plan.param_name.toLowerCase()] = v;
11293
11961
  } else if (endpoint.csrf_plan.source === "form" && body && typeof body === "object" && !Array.isArray(body)) {
11294
11962
  body[endpoint.csrf_plan.param_name] ??= v;
11295
11963
  }
@@ -11588,6 +12256,7 @@ async function executeEndpoint(skill, endpoint, params = {}, projection, options
11588
12256
  }
11589
12257
  })();
11590
12258
  if (consumerConfig.agent_id) {
12259
+ const wallet = getLocalWalletContext2();
11591
12260
  recordTransaction({
11592
12261
  transaction_id: trace.trace_id,
11593
12262
  consumer_id: consumerConfig.agent_id,
@@ -11595,7 +12264,7 @@ async function executeEndpoint(skill, endpoint, params = {}, projection, options
11595
12264
  skill_id: skill.skill_id,
11596
12265
  endpoint_id: endpoint.endpoint_id,
11597
12266
  price_usd: skill.base_price_usd,
11598
- payment_proof: process.env.LOBSTER_WALLET_ADDRESS ? `wallet:${process.env.LOBSTER_WALLET_ADDRESS}` : undefined
12267
+ payment_proof: wallet.wallet_address ? `wallet:${wallet.wallet_address}` : undefined
11599
12268
  }).catch(() => {});
11600
12269
  }
11601
12270
  }
@@ -12195,6 +12864,7 @@ var init_execution = __esm(async () => {
12195
12864
  init_version();
12196
12865
  init_search_forms();
12197
12866
  init_payments();
12867
+ init_robots();
12198
12868
  await __promiseAll([
12199
12869
  init_vault(),
12200
12870
  init_auth(),
@@ -13145,8 +13815,56 @@ var init_first_pass_action = __esm(() => {
13145
13815
  init_client();
13146
13816
  });
13147
13817
 
13148
- // ../../src/payments/wallet.ts
13149
- function checkWalletConfigured() {
13818
+ // ../../src/orchestrator/timing-economics.ts
13819
+ function computeTimingEconomics({
13820
+ source,
13821
+ totalMs,
13822
+ result,
13823
+ skill,
13824
+ paidSearchUc = 0,
13825
+ paidExecutionUc = 0
13826
+ }) {
13827
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result ?? "");
13828
+ const responseBytes = resultStr.length;
13829
+ const responseTokens = Math.ceil(responseBytes / CHARS_PER_TOKEN);
13830
+ const actualCostUc = responseTokens * TOKEN_COST_UC + paidSearchUc + paidExecutionUc;
13831
+ const economics = {
13832
+ response_bytes: responseBytes,
13833
+ response_tokens: responseTokens,
13834
+ tokens_saved: 0,
13835
+ tokens_saved_pct: 0,
13836
+ time_saved_pct: 0,
13837
+ actual_cost_uc: actualCostUc
13838
+ };
13839
+ if (!SAVINGS_SOURCES.has(source))
13840
+ return economics;
13841
+ const baselineTokens = skill?.discovery_cost?.capture_tokens ?? DEFAULT_CAPTURE_TOKENS;
13842
+ const baselineMs = skill?.discovery_cost?.capture_ms ?? DEFAULT_CAPTURE_MS;
13843
+ const baselineCostUc = baselineTokens * TOKEN_COST_UC;
13844
+ economics.tokens_saved = Math.max(0, baselineTokens - responseTokens);
13845
+ economics.tokens_saved_pct = baselineTokens > 0 ? Math.round(economics.tokens_saved / baselineTokens * 100) : 0;
13846
+ economics.time_saved_pct = baselineMs > 0 ? Math.round(Math.max(0, baselineMs - totalMs) / baselineMs * 100) : 0;
13847
+ economics.baseline_total_ms = baselineMs;
13848
+ economics.time_saved_ms = Math.max(0, baselineMs - totalMs);
13849
+ economics.baseline_cost_uc = baselineCostUc;
13850
+ economics.cost_saved_uc = Math.max(0, baselineCostUc - actualCostUc);
13851
+ economics.baseline_source = skill?.discovery_cost ? "real" : "estimated";
13852
+ return economics;
13853
+ }
13854
+ var DEFAULT_CAPTURE_MS = 22000, DEFAULT_CAPTURE_TOKENS = 30000, CHARS_PER_TOKEN = 4, TOKEN_COST_PER_MILLION_USD = 3, TOKEN_COST_UC, SAVINGS_SOURCES;
13855
+ var init_timing_economics = __esm(() => {
13856
+ TOKEN_COST_UC = Math.round(TOKEN_COST_PER_MILLION_USD);
13857
+ SAVINGS_SOURCES = new Set([
13858
+ "marketplace",
13859
+ "route-cache",
13860
+ "first-pass",
13861
+ "direct-fetch",
13862
+ "browser-action"
13863
+ ]);
13864
+ });
13865
+
13866
+ // ../../src/payments/wallet.ts
13867
+ function checkWalletConfigured() {
13150
13868
  if (process.env.LOBSTER_WALLET_ADDRESS) {
13151
13869
  return { configured: true, provider: "lobster.cash" };
13152
13870
  }
@@ -13581,6 +14299,31 @@ function isSearchLikeIntent(intent, contextUrl) {
13581
14299
  return false;
13582
14300
  }
13583
14301
  }
14302
+ function buildLocalCanonicalReplaySkill(intent, contextUrl) {
14303
+ const endpoint = buildCanonicalDocumentEndpoint(contextUrl, intent, false);
14304
+ if (!endpoint)
14305
+ return;
14306
+ const domain = new URL(contextUrl).hostname.replace(/^www\./, "");
14307
+ const now = new Date().toISOString();
14308
+ const skill = {
14309
+ skill_id: `canonical-${createHash7("sha1").update(contextUrl).digest("hex").slice(0, 12)}`,
14310
+ version: "1.0.0",
14311
+ schema_version: "1",
14312
+ name: `Canonical replay for ${domain}`,
14313
+ intent_signature: intent,
14314
+ intents: [intent],
14315
+ domain,
14316
+ description: `Deterministic structured replay for ${contextUrl}`,
14317
+ owner_type: "agent",
14318
+ execution_type: "http",
14319
+ endpoints: [endpoint],
14320
+ lifecycle: "active",
14321
+ created_at: now,
14322
+ updated_at: now
14323
+ };
14324
+ cachePublishedSkill(skill);
14325
+ return skill;
14326
+ }
13584
14327
  function isCachedSkillRelevantForIntent(skill, intent, contextUrl) {
13585
14328
  if (!hasUsableEndpoints(skill))
13586
14329
  return false;
@@ -14392,9 +15135,6 @@ async function resolveAndExecute(intent, params = {}, context, projection, optio
14392
15135
  const queryIntent = selectSearchTermsForExecution(intent) ?? extractSearchTermsFromIntent(intent) ?? intent;
14393
15136
  if (queryIntent !== intent)
14394
15137
  decisionTrace.query_intent = queryIntent;
14395
- const DEFAULT_CAPTURE_MS = 22000;
14396
- const DEFAULT_CAPTURE_TOKENS = 30000;
14397
- const CHARS_PER_TOKEN = 4;
14398
15138
  const agentChoseEndpoint = !!params.endpoint_id;
14399
15139
  const forceCapture = !!options?.force_capture;
14400
15140
  const clientScope = options?.client_scope ?? "global";
@@ -14418,32 +15158,33 @@ async function resolveAndExecute(intent, params = {}, context, projection, optio
14418
15158
  timing.actual_total_ms = timing.total_ms;
14419
15159
  timing.source = source;
14420
15160
  timing.skill_id = skillId;
14421
- const resultStr = typeof result2 === "string" ? result2 : JSON.stringify(result2 ?? "");
14422
- timing.response_bytes = resultStr.length;
14423
- const responseTokens = Math.ceil(resultStr.length / CHARS_PER_TOKEN);
14424
- const cost = skill?.discovery_cost;
14425
- const baselineTokens = cost?.capture_tokens ?? DEFAULT_CAPTURE_TOKENS;
14426
- const baselineMs = cost?.capture_ms ?? DEFAULT_CAPTURE_MS;
14427
- const paidSearchUc = timing.paid_search_uc ?? 0;
14428
- const paidExecutionUc = timing.paid_execution_uc ?? 0;
14429
- const totalActualCostUc = paidSearchUc + paidExecutionUc;
14430
- if (totalActualCostUc > 0)
14431
- timing.actual_cost_uc = totalActualCostUc;
14432
- if (source === "marketplace" || source === "route-cache" || source === "first-pass") {
14433
- timing.tokens_saved = Math.max(0, baselineTokens - responseTokens);
14434
- timing.tokens_saved_pct = baselineTokens > 0 ? Math.round(timing.tokens_saved / baselineTokens * 100) : 0;
14435
- timing.time_saved_pct = baselineMs > 0 ? Math.round(Math.max(0, baselineMs - timing.total_ms) / baselineMs * 100) : 0;
14436
- }
14437
- if (cost?.capture_ms != null) {
14438
- timing.baseline_total_ms = cost.capture_ms;
14439
- timing.time_saved_ms = Math.max(0, cost.capture_ms - timing.total_ms);
14440
- }
15161
+ const economics = computeTimingEconomics({
15162
+ source,
15163
+ totalMs: timing.total_ms,
15164
+ result: result2,
15165
+ skill,
15166
+ paidSearchUc: timing.paid_search_uc ?? 0,
15167
+ paidExecutionUc: timing.paid_execution_uc ?? 0
15168
+ });
15169
+ timing.response_bytes = economics.response_bytes;
15170
+ timing.tokens_saved = economics.tokens_saved;
15171
+ timing.tokens_saved_pct = economics.tokens_saved_pct;
15172
+ timing.time_saved_pct = economics.time_saved_pct;
15173
+ timing.actual_cost_uc = economics.actual_cost_uc;
15174
+ if (economics.baseline_total_ms != null)
15175
+ timing.baseline_total_ms = economics.baseline_total_ms;
15176
+ if (economics.time_saved_ms != null)
15177
+ timing.time_saved_ms = economics.time_saved_ms;
15178
+ if (economics.baseline_cost_uc != null)
15179
+ timing.baseline_cost_uc = economics.baseline_cost_uc;
15180
+ if (economics.cost_saved_uc != null)
15181
+ timing.cost_saved_uc = economics.cost_saved_uc;
14441
15182
  if (trace2) {
14442
- trace2.tokens_used = responseTokens;
15183
+ trace2.tokens_used = economics.response_tokens;
14443
15184
  trace2.tokens_saved = timing.tokens_saved;
14444
15185
  trace2.tokens_saved_pct = timing.tokens_saved_pct;
14445
15186
  }
14446
- console.log(`[perf] ${source}: ${timing.total_ms}ms (time_saved=${timing.time_saved_pct}% tokens_saved=${timing.tokens_saved_pct}%${cost ? " [real baseline]" : " [estimated]"})`);
15187
+ console.log(`[perf] ${source}: ${timing.total_ms}ms (time_saved=${timing.time_saved_pct}% tokens_saved=${timing.tokens_saved_pct}%${economics.baseline_source === "real" ? " [real baseline]" : economics.baseline_source === "estimated" ? " [estimated]" : ""})`);
14447
15188
  const lifecycleSource = source === "marketplace" ? "marketplace" : source === "route-cache" ? "cache" : "live-capture";
14448
15189
  const skillIdForLifecycle = skillId ?? "unknown";
14449
15190
  const now = new Date().toISOString();
@@ -14553,6 +15294,7 @@ async function resolveAndExecute(intent, params = {}, context, projection, optio
14553
15294
  result: {
14554
15295
  message: `Found ${epRanked.length} endpoint(s). Pick one and call POST /v1/skills/${resolvedSkill.skill_id}/execute with params.endpoint_id.`,
14555
15296
  skill_id: resolvedSkill.skill_id,
15297
+ suggested_next_operation_id: chunk.available_operation_ids[0],
14556
15298
  available_operations: chunk.operations.map((operation) => ({
14557
15299
  operation_id: operation.operation_id,
14558
15300
  endpoint_id: operation.endpoint_id,
@@ -14953,6 +15695,7 @@ async function resolveAndExecute(intent, params = {}, context, projection, optio
14953
15695
  if (source === "marketplace" && skill.base_price_usd && skill.base_price_usd > 0) {
14954
15696
  try {
14955
15697
  const walletCheck = checkWalletConfigured();
15698
+ const wallet = getLocalWalletContext2();
14956
15699
  const paymentResult = await checkPaymentRequirement(skill.skill_id, candidate.endpoint.endpoint_id, {
14957
15700
  price_usd: String(skill.base_price_usd),
14958
15701
  wallet_configured: walletCheck.configured
@@ -14970,7 +15713,8 @@ async function resolveAndExecute(intent, params = {}, context, projection, optio
14970
15713
  payment_status: paymentResult.status,
14971
15714
  message: paymentResult.message,
14972
15715
  next_step: paymentResult.next_step,
14973
- wallet_provider: "lobster.cash",
15716
+ wallet_provider: wallet.wallet_provider ?? "lobster.cash",
15717
+ wallet_address: wallet.wallet_address,
14974
15718
  indexing_fallback_available: true
14975
15719
  },
14976
15720
  trace: execOut.trace,
@@ -15308,6 +16052,14 @@ async function resolveAndExecute(intent, params = {}, context, projection, optio
15308
16052
  ]);
15309
16053
  } catch (err) {
15310
16054
  if (isX402Error(err)) {
16055
+ const localCanonicalSkill = context?.url && !isSearchLikeIntent(queryIntent, context.url) ? buildLocalCanonicalReplaySkill(queryIntent, context.url) : undefined;
16056
+ if (localCanonicalSkill) {
16057
+ const deferred2 = await buildDeferralWithAutoExec(localCanonicalSkill, "marketplace", {
16058
+ local_canonical_replay: true,
16059
+ payment_bypass: "canonical-detail-page"
16060
+ });
16061
+ return deferred2.orchestratorResult;
16062
+ }
15311
16063
  const trace2 = {
15312
16064
  trace_id: nanoid7(),
15313
16065
  skill_id: "marketplace-search",
@@ -15322,7 +16074,8 @@ async function resolveAndExecute(intent, params = {}, context, projection, optio
15322
16074
  result: {
15323
16075
  error: "payment_required",
15324
16076
  payment_status: "payment_required",
15325
- wallet_provider: "lobster.cash",
16077
+ wallet_provider: getLocalWalletContext2().wallet_provider ?? "lobster.cash",
16078
+ wallet_address: getLocalWalletContext2().wallet_address,
15326
16079
  message: "Marketplace search requires payment before shared graph results are returned.",
15327
16080
  next_step: "Pay the Tier 3 search fee, or re-run with force capture for free local discovery.",
15328
16081
  indexing_fallback_available: true,
@@ -15964,6 +16717,7 @@ var init_orchestrator = __esm(async () => {
15964
16717
  init_trace_store();
15965
16718
  init_passive_publish();
15966
16719
  init_first_pass_action();
16720
+ init_timing_economics();
15967
16721
  init_payments();
15968
16722
  init_version();
15969
16723
  init_runtime();
@@ -16557,189 +17311,1024 @@ var init_session_logs = __esm(() => {
16557
17311
  init_domain();
16558
17312
  });
16559
17313
 
16560
- // ../../src/verification/matrix.ts
16561
- function computeVerificationCoverage(matrix) {
16562
- if (matrix.length === 0)
16563
- return 0;
16564
- const tested = matrix.filter((c) => c.status !== "untested").length;
16565
- return tested / matrix.length;
17314
+ // ../../src/agent-outcome.ts
17315
+ function edgePriority(kind) {
17316
+ switch (kind) {
17317
+ case "parent_child":
17318
+ return 4;
17319
+ case "pagination":
17320
+ return 3;
17321
+ case "dependency":
17322
+ return 2;
17323
+ case "hint":
17324
+ return 1;
17325
+ case "auth":
17326
+ return 0;
17327
+ default:
17328
+ return -1;
17329
+ }
16566
17330
  }
16567
- var INITIAL_MATRIX;
16568
- var init_matrix = __esm(() => {
16569
- INITIAL_MATRIX = [
16570
- { host: "openclaw", capability: "capture", status: "pass", last_verified: "2026-03-31" },
16571
- { host: "openclaw", capability: "execute", status: "pass", last_verified: "2026-03-31" },
16572
- { host: "openclaw", capability: "search", status: "pass", last_verified: "2026-03-31" },
16573
- { host: "mcp", capability: "execute", status: "pass", last_verified: "2026-03-31" },
16574
- { host: "mcp", capability: "search", status: "pass", last_verified: "2026-03-31" },
16575
- { host: "cli", capability: "capture", status: "pass", last_verified: "2026-03-31" },
16576
- { host: "cli", capability: "execute", status: "pass", last_verified: "2026-03-31" },
16577
- { host: "cli", capability: "search", status: "pass", last_verified: "2026-03-31" },
16578
- { host: "cli", capability: "publish", status: "pass", last_verified: "2026-03-31" },
16579
- { host: "hermes", capability: "execute", status: "untested" },
16580
- { host: "elizaos", capability: "execute", status: "untested" },
16581
- { host: "langchain", capability: "execute", status: "untested" },
16582
- { host: "langchain", capability: "search", status: "untested" }
16583
- ];
17331
+ function nextActionWhy(kind, bindingKey, title) {
17332
+ switch (kind) {
17333
+ case "parent_child":
17334
+ return `Likely next detail step after this result. Exposes ${title}.`;
17335
+ case "pagination":
17336
+ return `Likely next page or continuation step. Carries ${bindingKey || "cursor"} forward.`;
17337
+ case "dependency":
17338
+ return `Unlocks the next dependent call using ${bindingKey || "known bindings"}.`;
17339
+ case "auth":
17340
+ return "Useful once authentication is in place.";
17341
+ case "hint":
17342
+ return "Common follow-up action from the current result.";
17343
+ default:
17344
+ return "Likely follow-up action.";
17345
+ }
17346
+ }
17347
+ function operationTitle(operation) {
17348
+ const semantic = [operation.action_kind, operation.resource_kind].filter(Boolean).join(" ").replace(/_/g, " ").trim();
17349
+ return operation.description_out || semantic || operation.endpoint_id;
17350
+ }
17351
+ function buildAgentImpact(timing) {
17352
+ if (!timing?.source)
17353
+ return;
17354
+ return {
17355
+ source: timing.source,
17356
+ cache_hit: timing.cache_hit === true,
17357
+ browser_avoided: !BROWSER_SOURCES.has(timing.source),
17358
+ baseline_total_ms: timing.baseline_total_ms,
17359
+ actual_total_ms: timing.actual_total_ms,
17360
+ time_saved_ms: timing.time_saved_ms,
17361
+ time_saved_pct: timing.time_saved_pct ?? 0,
17362
+ tokens_saved: timing.tokens_saved ?? 0,
17363
+ tokens_saved_pct: timing.tokens_saved_pct ?? 0,
17364
+ baseline_cost_uc: timing.baseline_cost_uc,
17365
+ actual_cost_uc: timing.actual_cost_uc,
17366
+ cost_saved_uc: timing.cost_saved_uc
17367
+ };
17368
+ }
17369
+ function buildNextActions(skill, endpointId, maxActions = 3) {
17370
+ if (!skill?.operation_graph || !endpointId)
17371
+ return [];
17372
+ const graph = skill.operation_graph;
17373
+ const current = graph.operations.find((operation) => operation.endpoint_id === endpointId);
17374
+ if (!current)
17375
+ return [];
17376
+ const byOperationId = new Map(graph.operations.map((operation) => [operation.operation_id, operation]));
17377
+ const scored = new Map;
17378
+ for (const edge of graph.edges) {
17379
+ if (edge.from_operation_id !== current.operation_id)
17380
+ continue;
17381
+ const target = byOperationId.get(edge.to_operation_id);
17382
+ if (!target)
17383
+ continue;
17384
+ const candidate = {
17385
+ operation_id: target.operation_id,
17386
+ endpoint_id: target.endpoint_id,
17387
+ title: operationTitle(target),
17388
+ why: nextActionWhy(edge.kind, edge.binding_key, operationTitle(target)),
17389
+ score: edgePriority(edge.kind) * 10 + Math.round(edge.confidence * 10)
17390
+ };
17391
+ const existing = scored.get(target.operation_id);
17392
+ if (!existing || candidate.score > existing.score) {
17393
+ scored.set(target.operation_id, candidate);
17394
+ }
17395
+ }
17396
+ return [...scored.values()].sort((a, b) => b.score - a.score || a.title.localeCompare(b.title)).slice(0, maxActions).map((candidate) => ({
17397
+ endpoint_id: candidate.endpoint_id,
17398
+ operation_id: candidate.operation_id,
17399
+ title: candidate.title,
17400
+ why: candidate.why,
17401
+ command: `unbrowse execute --skill ${skill.skill_id} --endpoint ${candidate.endpoint_id}`
17402
+ }));
17403
+ }
17404
+ function attachAgentOutcomeHints(payload, opts) {
17405
+ const target = payload;
17406
+ const impact = buildAgentImpact(opts?.timing);
17407
+ if (impact) {
17408
+ target.impact = impact;
17409
+ }
17410
+ const nextActions = buildNextActions(opts?.skill, opts?.endpointId);
17411
+ if (nextActions.length > 0) {
17412
+ target.next_actions = nextActions;
17413
+ }
17414
+ return target;
17415
+ }
17416
+ var BROWSER_SOURCES;
17417
+ var init_agent_outcome = __esm(() => {
17418
+ BROWSER_SOURCES = new Set(["live-capture", "first-pass", "browser-action"]);
16584
17419
  });
16585
17420
 
16586
- // ../../src/verification/index.ts
16587
- var exports_verification = {};
16588
- __export(exports_verification, {
16589
- verifySkillWithCoverage: () => verifySkillWithCoverage,
16590
- verifySkill: () => verifySkill,
16591
- verifyEndpoint: () => verifyEndpoint,
16592
- schedulePeriodicVerification: () => schedulePeriodicVerification
16593
- });
16594
- async function verifyEndpoint(skill, endpoint) {
16595
- if (endpoint.method !== "GET")
16596
- return endpoint.verification_status;
17421
+ // ../../src/api/browse-session.ts
17422
+ function extractBrowseFailureMessage(value) {
17423
+ return typeof value === "string" ? value : getKuriErrorMessage(value);
17424
+ }
17425
+ function isRecoverableBrowseFailure(value) {
17426
+ const message = extractBrowseFailureMessage(value);
17427
+ if (!message)
17428
+ return false;
17429
+ const normalized = message.toLowerCase();
17430
+ return RECOVERABLE_BROWSE_FAILURES.some((needle) => normalized.includes(needle));
17431
+ }
17432
+ async function createBrowseSession(sessions, client, injectInterceptor2) {
17433
+ await client.start().catch(() => {});
17434
+ const tabId = await client.newTab();
17435
+ await client.harStart(tabId).catch(() => {});
17436
+ await injectInterceptor2(tabId);
17437
+ const session = { tabId, url: "about:blank", harActive: true, domain: "" };
17438
+ sessions.set("default", session);
17439
+ return session;
17440
+ }
17441
+ function extractDomain2(url) {
17442
+ if (!url)
17443
+ return "";
16597
17444
  try {
16598
- const { status, data } = await executeInBrowser(endpoint.url_template, endpoint.method, endpoint.headers_template ?? {}, undefined, undefined, undefined);
16599
- if (status < 200 || status >= 300) {
16600
- await updateEndpointScore2(skill.skill_id, endpoint.endpoint_id, endpoint.reliability_score, "failed");
16601
- return "failed";
16602
- }
16603
- let hasCriticalDrift = false;
16604
- if (endpoint.response_schema && data != null) {
16605
- const drift = detectSchemaDrift(endpoint.response_schema, data);
16606
- if (drift.drifted && (drift.removed_fields.length > 0 || drift.type_changes.length > 0)) {
16607
- hasCriticalDrift = true;
16608
- }
16609
- }
16610
- const newStatus = hasCriticalDrift ? "pending" : "verified";
16611
- const newScore = endpoint.verification_status === "disabled" && newStatus === "verified" ? 0.5 : endpoint.reliability_score;
16612
- await updateEndpointScore2(skill.skill_id, endpoint.endpoint_id, newScore, newStatus);
16613
- const fullSkill = await getSkill2(skill.skill_id);
16614
- if (fullSkill) {
16615
- const ep = fullSkill.endpoints.find((e) => e.endpoint_id === endpoint.endpoint_id);
16616
- if (ep)
16617
- ep.last_verified_at = new Date().toISOString();
16618
- }
16619
- return newStatus;
17445
+ return new URL(url).hostname.replace(/^www\./, "");
16620
17446
  } catch {
16621
- await updateEndpointScore2(skill.skill_id, endpoint.endpoint_id, endpoint.reliability_score, "failed");
16622
- return "failed";
17447
+ return "";
16623
17448
  }
16624
17449
  }
16625
- async function verifySkill(skill) {
16626
- const results = {};
16627
- for (const endpoint of skill.endpoints) {
16628
- results[endpoint.endpoint_id] = await verifyEndpoint(skill, endpoint);
17450
+ async function adoptExistingBrowseTab(sessions, client, injectInterceptor2, preferredDomain) {
17451
+ try {
17452
+ const tabs = await client.discoverTabs();
17453
+ const normalizedPreferred = preferredDomain?.replace(/^www\./, "") ?? "";
17454
+ const candidate = tabs.find((tab) => {
17455
+ const domain = extractDomain2(tab.url);
17456
+ return !!domain && !!normalizedPreferred && domain === normalizedPreferred;
17457
+ }) ?? tabs.find((tab) => /^https?:\/\//.test(tab.url ?? ""));
17458
+ if (!candidate?.id)
17459
+ return null;
17460
+ await client.harStart(candidate.id).catch(() => {});
17461
+ await injectInterceptor2(candidate.id);
17462
+ const session = {
17463
+ tabId: candidate.id,
17464
+ url: candidate.url ?? "about:blank",
17465
+ harActive: true,
17466
+ domain: extractDomain2(candidate.url)
17467
+ };
17468
+ sessions.set("default", session);
17469
+ return session;
17470
+ } catch {
17471
+ return null;
16629
17472
  }
16630
- return results;
16631
17473
  }
16632
- async function verifySkillWithCoverage(skill, matrix = INITIAL_MATRIX) {
16633
- const results = await verifySkill(skill);
16634
- const coverage = computeVerificationCoverage(matrix);
16635
- return { results, coverage };
17474
+ async function dropBrowseSession(sessions, client, session) {
17475
+ if (!session)
17476
+ return;
17477
+ await client.closeTab(session.tabId).catch(() => {});
17478
+ if (sessions.get("default")?.tabId === session.tabId) {
17479
+ sessions.delete("default");
17480
+ }
16636
17481
  }
16637
- function schedulePeriodicVerification() {
16638
- return setInterval(async () => {
16639
- const skills = await listSkills2();
16640
- const now = Date.now();
16641
- for (const skill of skills) {
16642
- if (skill.lifecycle !== "active")
16643
- continue;
16644
- for (const endpoint of skill.endpoints) {
16645
- if (endpoint.method !== "GET")
16646
- continue;
16647
- const isDisabled = endpoint.verification_status === "disabled";
16648
- const lastVerified = endpoint.last_verified_at ? new Date(endpoint.last_verified_at).getTime() : 0;
16649
- if (isDisabled || now - lastVerified > STALE_THRESHOLD_MS) {
16650
- await verifyEndpoint(skill, endpoint).catch(() => {});
16651
- }
16652
- }
17482
+ async function isBrowseSessionLive(session, client) {
17483
+ if (!session.tabId)
17484
+ return false;
17485
+ try {
17486
+ const tabs = await client.discoverTabs();
17487
+ if (!tabs.some((tab) => tab.id === session.tabId))
17488
+ return false;
17489
+ const currentUrl = await client.getCurrentUrl(session.tabId);
17490
+ return typeof currentUrl === "string" && currentUrl.length > 0;
17491
+ } catch {
17492
+ return false;
17493
+ }
17494
+ }
17495
+ async function resetBrowseSession(sessions, client, injectInterceptor2) {
17496
+ const existing = sessions.get("default");
17497
+ const preferredDomain = existing?.domain || extractDomain2(existing?.url);
17498
+ await dropBrowseSession(sessions, client, existing);
17499
+ const adopted = await adoptExistingBrowseTab(sessions, client, injectInterceptor2, preferredDomain);
17500
+ if (adopted)
17501
+ return adopted;
17502
+ return createBrowseSession(sessions, client, injectInterceptor2);
17503
+ }
17504
+ async function getOrCreateBrowseSession(sessions, client, injectInterceptor2) {
17505
+ const existing = sessions.get("default");
17506
+ if (existing && await isBrowseSessionLive(existing, client))
17507
+ return existing;
17508
+ const preferredDomain = existing?.domain || extractDomain2(existing?.url);
17509
+ if (existing)
17510
+ await dropBrowseSession(sessions, client, existing);
17511
+ const adopted = await adoptExistingBrowseTab(sessions, client, injectInterceptor2, preferredDomain);
17512
+ if (adopted)
17513
+ return adopted;
17514
+ return createBrowseSession(sessions, client, injectInterceptor2);
17515
+ }
17516
+ async function withRecoveredBrowseSession(sessions, client, injectInterceptor2, run, shouldReset) {
17517
+ let session = await getOrCreateBrowseSession(sessions, client, injectInterceptor2);
17518
+ try {
17519
+ const result2 = await run(session);
17520
+ if (!shouldReset || !shouldReset(result2)) {
17521
+ return { session, result: result2, recovered: false };
16653
17522
  }
16654
- }, VERIFICATION_INTERVAL_MS);
17523
+ } catch (error) {
17524
+ if (!isRecoverableBrowseFailure(error))
17525
+ throw error;
17526
+ }
17527
+ session = await resetBrowseSession(sessions, client, injectInterceptor2);
17528
+ const result = await run(session);
17529
+ return { session, result, recovered: true };
16655
17530
  }
16656
- var VERIFICATION_INTERVAL_MS, STALE_THRESHOLD_MS;
16657
- var init_verification = __esm(() => {
16658
- init_capture();
16659
- init_marketplace();
16660
- init_marketplace();
16661
- init_drift();
16662
- init_matrix();
16663
- VERIFICATION_INTERVAL_MS = 6 * 60 * 60 * 1000;
16664
- STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
17531
+ var RECOVERABLE_BROWSE_FAILURES;
17532
+ var init_browse_session = __esm(() => {
17533
+ init_client();
17534
+ RECOVERABLE_BROWSE_FAILURES = [
17535
+ "cdp command failed",
17536
+ "transport closed",
17537
+ "target closed",
17538
+ "tab not found",
17539
+ "session closed",
17540
+ "execution context was destroyed",
17541
+ "cannot find context with specified id",
17542
+ "no such target"
17543
+ ];
16665
17544
  });
16666
17545
 
16667
- // ../../src/api/routes.ts
16668
- var exports_routes = {};
16669
- __export(exports_routes, {
16670
- registerRoutes: () => registerRoutes,
16671
- registerBrowseSession: () => registerBrowseSession,
16672
- buildAnalyticsSessionPayload: () => buildAnalyticsSessionPayload
16673
- });
17546
+ // ../../src/api/browse-index.ts
16674
17547
  import { nanoid as nanoid8 } from "nanoid";
16675
- import { writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
16676
- import { join as join12 } from "path";
16677
- function buildAnalyticsSessionPayload(result, opts) {
16678
- const source = result.timing?.source ?? result.source;
16679
- const apiCalls = result.trace.endpoint_id ? 1 : 0;
16680
- const cachedSkillCalls = opts.cached_skill_calls ?? (apiCalls > 0 && source !== "live-capture" && source !== "first-pass" ? 1 : 0);
16681
- const freshIndexCalls = opts.fresh_index_calls ?? (apiCalls > 0 && (source === "live-capture" || source === "first-pass") ? 1 : 0);
16682
- return {
16683
- session_id: result.trace.trace_id,
16684
- started_at: result.trace.started_at,
16685
- completed_at: result.trace.completed_at,
16686
- trace_version: result.trace.trace_version ?? TRACE_VERSION,
16687
- api_calls: apiCalls,
16688
- discovery_queries: opts.discovery_queries,
16689
- cached_skill_calls: cachedSkillCalls,
16690
- fresh_index_calls: freshIndexCalls,
16691
- browser_mode: opts.browser_mode ?? "unknown"
16692
- };
17548
+ import { readFileSync as readFileSync9 } from "node:fs";
17549
+ function normalizeBrowseUrl(url, baseUrl) {
17550
+ if (!url)
17551
+ return url;
17552
+ try {
17553
+ return new URL(url).toString();
17554
+ } catch {
17555
+ if (!baseUrl)
17556
+ return url;
17557
+ try {
17558
+ return new URL(url, baseUrl).toString();
17559
+ } catch {
17560
+ return url;
17561
+ }
17562
+ }
16693
17563
  }
16694
- function harEntriesToRawRequests(entries) {
16695
- return entries.filter((e) => e.request && e.response).map((e) => ({
16696
- url: e.request.url,
16697
- method: e.request.method,
16698
- request_headers: Object.fromEntries((e.request.headers ?? []).map((h) => [h.name.toLowerCase(), h.value])),
16699
- request_body: e.request.postData?.text,
16700
- response_status: e.response.status,
16701
- response_headers: Object.fromEntries((e.response.headers ?? []).map((h) => [h.name.toLowerCase(), h.value])),
16702
- response_body: e.response.content?.text,
16703
- timestamp: e.startedDateTime ?? new Date().toISOString()
17564
+ function harEntriesToRawRequests(entries, baseUrl) {
17565
+ return entries.filter((entry) => entry.request && entry.response).map((entry) => ({
17566
+ url: normalizeBrowseUrl(entry.request.url, baseUrl),
17567
+ method: entry.request.method,
17568
+ request_headers: Object.fromEntries((entry.request.headers ?? []).map((header) => [header.name.toLowerCase(), header.value])),
17569
+ request_body: entry.request.postData?.text,
17570
+ response_status: entry.response.status,
17571
+ response_headers: Object.fromEntries((entry.response.headers ?? []).map((header) => [header.name.toLowerCase(), header.value])),
17572
+ response_body: entry.response.content?.text,
17573
+ timestamp: entry.startedDateTime ?? new Date().toISOString()
16704
17574
  }));
16705
17575
  }
16706
- function passiveIndexFromRequests(requests, pageUrl) {
16707
- if (requests.length === 0)
16708
- return;
17576
+ function buildBrowseRequestKey(request) {
17577
+ return [
17578
+ request.method,
17579
+ request.url,
17580
+ typeof request.request_body === "string" ? request.request_body : JSON.stringify(request.request_body ?? null)
17581
+ ].join(":");
17582
+ }
17583
+ function mergeBrowseRequests(intercepted, harEntries, baseUrl) {
17584
+ const normalizedIntercepted = intercepted.map((request) => ({
17585
+ ...request,
17586
+ url: normalizeBrowseUrl(request.url, baseUrl)
17587
+ }));
17588
+ const harRequests = harEntriesToRawRequests(harEntries, baseUrl);
17589
+ const seen = new Set;
17590
+ const allRequests = [];
17591
+ for (const request of normalizedIntercepted) {
17592
+ const key = buildBrowseRequestKey(request);
17593
+ if (!seen.has(key)) {
17594
+ seen.add(key);
17595
+ allRequests.push(request);
17596
+ }
17597
+ }
17598
+ for (const request of harRequests) {
17599
+ const key = buildBrowseRequestKey(request);
17600
+ if (!seen.has(key)) {
17601
+ seen.add(key);
17602
+ allRequests.push(request);
17603
+ }
17604
+ }
17605
+ return allRequests;
17606
+ }
17607
+ async function cacheBrowseRequests(params) {
17608
+ const { sessionUrl, sessionDomain, requests, getPageHtml: getPageHtml2 } = params;
16709
17609
  let domain;
16710
17610
  try {
16711
- domain = new URL(pageUrl).hostname;
17611
+ domain = new URL(sessionUrl).hostname;
16712
17612
  } catch {
16713
- return;
16714
- }
16715
- const intent = `browse ${domain}`;
16716
- (async () => {
16717
- try {
16718
- const rawEndpoints = extractEndpoints(requests, undefined, { pageUrl, finalUrl: pageUrl });
16719
- if (rawEndpoints.length === 0) {
16720
- console.log(`[passive-index] ${domain}: 0 endpoints from ${requests.length} requests`);
16721
- return;
16722
- }
16723
- const capturedAuthHeaders = extractAuthHeaders(requests);
16724
- if (Object.keys(capturedAuthHeaders).length > 0) {
16725
- const authKey = `${domain}-session`;
16726
- await storeCredential(authKey, JSON.stringify({ headers: capturedAuthHeaders }));
16727
- }
16728
- const existingSkill = findExistingSkillForDomain(domain, intent);
16729
- const mergedEndpoints = existingSkill ? mergeEndpoints(existingSkill.endpoints, rawEndpoints) : rawEndpoints;
16730
- if (existingSkill && mergedEndpoints.length < existingSkill.endpoints.length) {
16731
- console.log(`[passive-index] ${domain}: skipping — would reduce ${existingSkill.endpoints.length} → ${mergedEndpoints.length} endpoints`);
16732
- return;
17613
+ domain = sessionDomain;
17614
+ }
17615
+ const rawEndpoints = extractEndpoints(requests, undefined, { pageUrl: sessionUrl, finalUrl: sessionUrl });
17616
+ if (rawEndpoints.length > 0) {
17617
+ const existingSkill = findExistingSkillForDomain(domain);
17618
+ let allExisting = existingSkill?.endpoints ?? [];
17619
+ const domainKey = getDomainReuseKey(sessionUrl ?? domain);
17620
+ if (domainKey) {
17621
+ const cached = domainSkillCache.get(domainKey);
17622
+ if (cached?.localSkillPath) {
17623
+ try {
17624
+ const snapshot2 = JSON.parse(readFileSync9(cached.localSkillPath, "utf-8"));
17625
+ if (snapshot2?.endpoints?.length > 0) {
17626
+ allExisting = mergeEndpoints(allExisting, snapshot2.endpoints);
17627
+ }
17628
+ } catch {}
16733
17629
  }
16734
- for (const ep of mergedEndpoints) {
16735
- if (!ep.description) {
16736
- ep.description = generateLocalDescription(ep);
16737
- }
17630
+ }
17631
+ const mergedEndpoints = allExisting.length > 0 ? mergeEndpoints(allExisting, rawEndpoints) : rawEndpoints;
17632
+ if (!existingSkill || mergedEndpoints.length >= existingSkill.endpoints.length) {
17633
+ for (const endpoint of mergedEndpoints) {
17634
+ if (!endpoint.description)
17635
+ endpoint.description = generateLocalDescription(endpoint);
17636
+ }
17637
+ const quickSkill = {
17638
+ skill_id: existingSkill?.skill_id ?? nanoid8(),
17639
+ version: "1.0.0",
17640
+ schema_version: "1",
17641
+ lifecycle: "active",
17642
+ execution_type: "http",
17643
+ created_at: existingSkill?.created_at ?? new Date().toISOString(),
17644
+ updated_at: new Date().toISOString(),
17645
+ name: domain,
17646
+ intent_signature: `browse ${domain}`,
17647
+ domain,
17648
+ description: `API skill for ${domain}`,
17649
+ owner_type: "agent",
17650
+ endpoints: mergedEndpoints,
17651
+ operation_graph: buildSkillOperationGraph(mergedEndpoints),
17652
+ intents: Array.from(new Set([...existingSkill?.intents ?? [], `browse ${domain}`]))
17653
+ };
17654
+ const cacheKey = buildResolveCacheKey(domain, `browse ${domain}`, sessionUrl);
17655
+ const scopedKey = scopedCacheKey("global", cacheKey);
17656
+ writeSkillSnapshot(scopedKey, quickSkill);
17657
+ if (domainKey) {
17658
+ domainSkillCache.set(domainKey, {
17659
+ skillId: quickSkill.skill_id,
17660
+ localSkillPath: snapshotPathForCacheKey(scopedKey),
17661
+ ts: Date.now()
17662
+ });
17663
+ persistDomainCache();
17664
+ }
17665
+ try {
17666
+ cachePublishedSkill(quickSkill);
17667
+ } catch {}
17668
+ upsertDagEdgesFromOperationGraph(quickSkill);
17669
+ invalidateRouteCacheForDomain(domain);
17670
+ return { domain, indexed: true, mode: "http", skill: quickSkill };
17671
+ }
17672
+ return { domain, indexed: false, mode: "http", skill: existingSkill ?? null };
17673
+ }
17674
+ if (!getPageHtml2)
17675
+ return { domain, indexed: false, mode: "none", skill: null };
17676
+ try {
17677
+ const html = await getPageHtml2();
17678
+ if (!html || !html.startsWith("<"))
17679
+ return { domain, indexed: false, mode: "none", skill: null };
17680
+ const { extractFromDOM: extractFromDOM2 } = await Promise.resolve().then(() => (init_extraction(), exports_extraction));
17681
+ const { detectSearchForms: detectSearchForms2, isStructuredSearchForm: isStructuredSearchForm2 } = await Promise.resolve().then(() => (init_search_forms(), exports_search_forms));
17682
+ const { inferSchema: inferSchema2 } = await Promise.resolve().then(() => (init_transform(), exports_transform));
17683
+ const { templatizeQueryParams: templatizeQueryParams2 } = await init_execution().then(() => exports_execution);
17684
+ const extracted = extractFromDOM2(html, `browse ${domain}`);
17685
+ const searchForms = detectSearchForms2(html);
17686
+ const validForm = searchForms.find((form) => isStructuredSearchForm2(form));
17687
+ if (!extracted.data && !validForm)
17688
+ return { domain, indexed: false, mode: "none", skill: null };
17689
+ const urlTemplate = templatizeQueryParams2(sessionUrl);
17690
+ const endpoint = {
17691
+ endpoint_id: nanoid8(),
17692
+ method: "GET",
17693
+ url_template: urlTemplate,
17694
+ idempotency: "safe",
17695
+ verification_status: "verified",
17696
+ reliability_score: extracted.confidence ?? 0.7,
17697
+ description: validForm ? `Search form for ${domain}` : `Page content from ${domain}`,
17698
+ response_schema: extracted.data ? inferSchema2([extracted.data]) : undefined,
17699
+ dom_extraction: {
17700
+ extraction_method: extracted.extraction_method ?? "repeated-elements",
17701
+ confidence: extracted.confidence ?? 0.7,
17702
+ ...extracted.selector ? { selector: extracted.selector } : {}
17703
+ },
17704
+ trigger_url: sessionUrl,
17705
+ ...validForm ? { search_form: validForm } : {}
17706
+ };
17707
+ endpoint.semantic = inferEndpointSemantic(endpoint, {
17708
+ sampleResponse: extracted.data,
17709
+ observedAt: new Date().toISOString(),
17710
+ sampleRequestUrl: sessionUrl
17711
+ });
17712
+ const existing = findExistingSkillForDomain(domain);
17713
+ const allEndpoints = existing ? mergeEndpoints(existing.endpoints, [endpoint]) : [endpoint];
17714
+ for (const candidate of allEndpoints) {
17715
+ if (!candidate.description)
17716
+ candidate.description = generateLocalDescription(candidate);
17717
+ }
17718
+ const skill = {
17719
+ skill_id: existing?.skill_id ?? nanoid8(),
17720
+ version: "1.0.0",
17721
+ schema_version: "1",
17722
+ lifecycle: "active",
17723
+ execution_type: "http",
17724
+ created_at: existing?.created_at ?? new Date().toISOString(),
17725
+ updated_at: new Date().toISOString(),
17726
+ name: domain,
17727
+ intent_signature: `browse ${domain}`,
17728
+ domain,
17729
+ description: `DOM skill for ${domain}`,
17730
+ owner_type: "agent",
17731
+ endpoints: allEndpoints,
17732
+ operation_graph: buildSkillOperationGraph(allEndpoints),
17733
+ intents: [...new Set([...existing?.intents ?? [], `browse ${domain}`])]
17734
+ };
17735
+ const cacheKey = buildResolveCacheKey(domain, `browse ${domain}`, sessionUrl);
17736
+ const scopedKey = scopedCacheKey("global", cacheKey);
17737
+ writeSkillSnapshot(scopedKey, skill);
17738
+ const domainReuseKey = getDomainReuseKey(sessionUrl ?? domain);
17739
+ if (domainReuseKey) {
17740
+ domainSkillCache.set(domainReuseKey, {
17741
+ skillId: skill.skill_id,
17742
+ localSkillPath: snapshotPathForCacheKey(scopedKey),
17743
+ ts: Date.now()
17744
+ });
17745
+ persistDomainCache();
17746
+ }
17747
+ try {
17748
+ cachePublishedSkill(skill);
17749
+ } catch {}
17750
+ upsertDagEdgesFromOperationGraph(skill);
17751
+ invalidateRouteCacheForDomain(domain);
17752
+ return { domain, indexed: true, mode: "dom", skill };
17753
+ } catch {
17754
+ return { domain, indexed: false, mode: "none", skill: null };
17755
+ }
17756
+ }
17757
+ var init_browse_index = __esm(async () => {
17758
+ init_reverse_engineer();
17759
+ init_graph();
17760
+ init_client2();
17761
+ init_marketplace();
17762
+ init_dag_feedback();
17763
+ await init_orchestrator();
17764
+ });
17765
+
17766
+ // ../../src/api/browse-submit.ts
17767
+ function sleep(ms) {
17768
+ return new Promise((resolve) => setTimeout(resolve, ms));
17769
+ }
17770
+ function asRecord(value) {
17771
+ return value && typeof value === "object" ? value : null;
17772
+ }
17773
+ function isUrlWaitHint(value) {
17774
+ if (!value)
17775
+ return false;
17776
+ return /^https?:\/\//i.test(value) || value.startsWith("/");
17777
+ }
17778
+ function hasMeaningfulPageChange(beforeHtml, afterHtml) {
17779
+ const before = beforeHtml.trim();
17780
+ const after = afterHtml.trim();
17781
+ if (!after)
17782
+ return false;
17783
+ if (!before)
17784
+ return after.length > 64;
17785
+ if (before === after)
17786
+ return false;
17787
+ if (Math.abs(before.length - after.length) > 48)
17788
+ return true;
17789
+ const beforeBody = before.match(/<body[\s\S]*?>([\s\S]*?)<\/body>/i)?.[1] ?? before;
17790
+ const afterBody = after.match(/<body[\s\S]*?>([\s\S]*?)<\/body>/i)?.[1] ?? after;
17791
+ return beforeBody.trim() !== afterBody.trim();
17792
+ }
17793
+ function buildDomSubmitExpression(options) {
17794
+ return `(function() {
17795
+ function findForm(selector) {
17796
+ if (selector) return document.querySelector(selector);
17797
+ var active = document.activeElement;
17798
+ if (active && active.closest) {
17799
+ var fromActive = active.closest("form");
17800
+ if (fromActive) return fromActive;
17801
+ }
17802
+ return document.querySelector("form");
17803
+ }
17804
+ function findSubmitter(form, selector) {
17805
+ if (!form) return null;
17806
+ if (selector) return document.querySelector(selector);
17807
+ var active = document.activeElement;
17808
+ if (active && form.contains(active) && /^(submit|image)$/i.test(active.getAttribute("type") || "")) return active;
17809
+ return form.querySelector('button[type="submit"], input[type="submit"], input[type="image"], button:not([type])');
17810
+ }
17811
+
17812
+ var form = findForm(${JSON.stringify(options.formSelector ?? "")});
17813
+ if (!form) {
17814
+ return JSON.stringify({ ok: false, reason: "form_not_found" });
17815
+ }
17816
+
17817
+ var submitter = findSubmitter(form, ${JSON.stringify(options.submitSelector ?? "")});
17818
+ var meta = {
17819
+ ok: true,
17820
+ form_action: form.getAttribute("action") || "",
17821
+ form_method: (form.getAttribute("method") || "GET").toUpperCase(),
17822
+ submitter: submitter ? (submitter.getAttribute("name") || submitter.id || submitter.textContent || submitter.tagName || "").trim() : null,
17823
+ submit_selector_used: ${JSON.stringify(options.submitSelector ?? null)},
17824
+ form_selector_used: ${JSON.stringify(options.formSelector ?? null)},
17825
+ };
17826
+
17827
+ if (submitter && typeof submitter.click === "function") {
17828
+ submitter.click();
17829
+ return JSON.stringify({ ...meta, submit_kind: "click" });
17830
+ }
17831
+ if (typeof form.requestSubmit === "function") {
17832
+ form.requestSubmit();
17833
+ return JSON.stringify({ ...meta, submit_kind: "requestSubmit" });
17834
+ }
17835
+ if (typeof form.submit === "function") {
17836
+ form.submit();
17837
+ return JSON.stringify({ ...meta, submit_kind: "submit" });
17838
+ }
17839
+ return JSON.stringify({ ok: false, reason: "submit_unavailable" });
17840
+ })()`;
17841
+ }
17842
+ function buildSameOriginFetchExpression(options) {
17843
+ return `(async function() {
17844
+ function splitPlugins(value) {
17845
+ return String(value || "")
17846
+ .split(/[\\s,;]+/)
17847
+ .map(function(part) { return part.trim(); })
17848
+ .filter(Boolean);
17849
+ }
17850
+ function pluginPath(name) {
17851
+ if (/^https?:\\/\\//i.test(name) || name.startsWith("/")) return name;
17852
+ return "/etc/designs/wrs/footLibs/js/plugins/" + (name.endsWith(".js") ? name : name + ".js");
17853
+ }
17854
+ async function bestEffortRehydrate() {
17855
+ var modules = Array.from(new Set(
17856
+ Array.from(document.querySelectorAll("[data-load-plugins]"))
17857
+ .flatMap(function(node) { return splitPlugins(node.getAttribute("data-load-plugins")); })
17858
+ ));
17859
+ if (modules.length === 0) {
17860
+ return { attempted: false, loaded: false, nooped: true, reason: "no_plugins", modules: [] };
17861
+ }
17862
+ if (!window.WRS || typeof window.WRS.require !== "function") {
17863
+ return { attempted: false, loaded: false, nooped: true, reason: "missing_wrs_require", modules: modules };
17864
+ }
17865
+ var requireWrs = window.WRS.require.bind(window.WRS);
17866
+ async function loadModules(paths) {
17867
+ return await new Promise(function(resolve) {
17868
+ var done = false;
17869
+ var timer = setTimeout(function() {
17870
+ if (done) return;
17871
+ done = true;
17872
+ resolve({ ok: false, reason: "timeout" });
17873
+ }, 1500);
17874
+ try {
17875
+ requireWrs(paths, function() {
17876
+ if (done) return;
17877
+ done = true;
17878
+ clearTimeout(timer);
17879
+ resolve({ ok: true });
17880
+ });
17881
+ } catch (error) {
17882
+ if (done) return;
17883
+ done = true;
17884
+ clearTimeout(timer);
17885
+ resolve({ ok: false, reason: error && error.message ? error.message : String(error) });
17886
+ }
17887
+ });
17888
+ }
17889
+
17890
+ var configResult = await loadModules(["/etc/designs/wrs/footLibs/js/config.js"]);
17891
+ var pluginResult = await loadModules(modules.map(pluginPath));
17892
+ for (var i = 0; i < 6; i++) {
17893
+ await new Promise(function(resolve) { return setTimeout(resolve, 100); });
17894
+ }
17895
+ return {
17896
+ attempted: true,
17897
+ loaded: !!pluginResult.ok,
17898
+ nooped: false,
17899
+ reason: pluginResult.ok ? undefined : pluginResult.reason,
17900
+ config_loaded: !!configResult.ok,
17901
+ modules: modules,
17902
+ };
17903
+ }
17904
+ function findForm(selector) {
17905
+ if (selector) return document.querySelector(selector);
17906
+ var active = document.activeElement;
17907
+ if (active && active.closest) {
17908
+ var fromActive = active.closest("form");
17909
+ if (fromActive) return fromActive;
17910
+ }
17911
+ return document.querySelector("form");
17912
+ }
17913
+ function findSubmitter(form, selector) {
17914
+ if (!form) return null;
17915
+ if (selector) return document.querySelector(selector);
17916
+ var active = document.activeElement;
17917
+ if (active && form.contains(active) && /^(submit|image)$/i.test(active.getAttribute("type") || "")) return active;
17918
+ return form.querySelector('button[type="submit"], input[type="submit"], input[type="image"], button:not([type])');
17919
+ }
17920
+
17921
+ var form = findForm(${JSON.stringify(options.formSelector ?? "")});
17922
+ if (!form) return JSON.stringify({ ok: false, reason: "form_not_found" });
17923
+
17924
+ var submitter = findSubmitter(form, ${JSON.stringify(options.submitSelector ?? "")});
17925
+ var method = (form.getAttribute("method") || "GET").toUpperCase();
17926
+ var action = form.getAttribute("action") || window.location.href;
17927
+ var targetUrl = new URL(action, window.location.href);
17928
+ if (targetUrl.origin !== window.location.origin) {
17929
+ return JSON.stringify({ ok: false, reason: "cross_origin", url: targetUrl.href });
17930
+ }
17931
+
17932
+ var formData = new FormData(form);
17933
+ if (submitter && submitter.name) {
17934
+ var submitValue = submitter.value != null ? submitter.value : "";
17935
+ if (!formData.has(submitter.name)) formData.append(submitter.name, submitValue);
17936
+ }
17937
+
17938
+ var headers = {};
17939
+ var requestUrl = targetUrl.href;
17940
+ var body;
17941
+ if (method === "GET") {
17942
+ var params = new URLSearchParams();
17943
+ Array.from(formData.entries()).forEach(function(entry) {
17944
+ var value = entry[1];
17945
+ if (typeof value === "string") params.append(entry[0], value);
17946
+ });
17947
+ var query = params.toString();
17948
+ if (query) requestUrl += (requestUrl.includes("?") ? "&" : "?") + query;
17949
+ } else if ((form.enctype || "").includes("application/x-www-form-urlencoded")) {
17950
+ var encoded = new URLSearchParams();
17951
+ Array.from(formData.entries()).forEach(function(entry) {
17952
+ var value = entry[1];
17953
+ if (typeof value === "string") encoded.append(entry[0], value);
17954
+ });
17955
+ body = encoded.toString();
17956
+ headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
17957
+ } else {
17958
+ body = formData;
17959
+ }
17960
+
17961
+ try {
17962
+ var response = await fetch(requestUrl, {
17963
+ method: method,
17964
+ body: method === "GET" ? undefined : body,
17965
+ headers: headers,
17966
+ credentials: "include",
17967
+ redirect: "follow",
17968
+ });
17969
+ var contentType = response.headers.get("content-type") || "";
17970
+ var text = await response.text();
17971
+ var finalUrl = response.url || requestUrl;
17972
+ if (!/text\\/html|application\\/xhtml\\+xml/i.test(contentType)) {
17973
+ return JSON.stringify({
17974
+ ok: false,
17975
+ reason: "non_html_response",
17976
+ status: response.status,
17977
+ url: finalUrl,
17978
+ content_type: contentType,
17979
+ });
17980
+ }
17981
+
17982
+ document.open();
17983
+ document.write(text);
17984
+ document.close();
17985
+ if (finalUrl && finalUrl !== window.location.href) {
17986
+ history.replaceState({}, "", finalUrl);
17987
+ }
17988
+ await new Promise(function(resolve) { return setTimeout(resolve, 50); });
17989
+ var rehydrate = await bestEffortRehydrate();
17990
+ return JSON.stringify({
17991
+ ok: true,
17992
+ status: response.status,
17993
+ url: finalUrl,
17994
+ same_origin_html_rehydrated: true,
17995
+ rehydrate: rehydrate,
17996
+ });
17997
+ } catch (error) {
17998
+ return JSON.stringify({
17999
+ ok: false,
18000
+ reason: error && error.message ? error.message : String(error),
18001
+ });
18002
+ }
18003
+ })()`;
18004
+ }
18005
+ function parseJsonString(value) {
18006
+ if (typeof value !== "string")
18007
+ return asRecord(value);
18008
+ try {
18009
+ return asRecord(JSON.parse(value));
18010
+ } catch {
18011
+ return null;
18012
+ }
18013
+ }
18014
+ async function waitForSubmitOutcome(client, tabId, beforeUrl, beforeHtml, options) {
18015
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SUBMIT_TIMEOUT_MS;
18016
+ const deadline = Date.now() + timeoutMs;
18017
+ const waitFor = options.waitFor?.trim();
18018
+ if (waitFor && !isUrlWaitHint(waitFor)) {
18019
+ try {
18020
+ const waitResult = await client.waitForSelector(tabId, waitFor, timeoutMs);
18021
+ if (waitResult?.status === "found" || waitResult?.status === "ready") {
18022
+ const url = await client.getCurrentUrl(tabId).catch(() => beforeUrl);
18023
+ const html = await client.getPageHtml(tabId).catch(() => beforeHtml);
18024
+ return { ok: true, url, html };
18025
+ }
18026
+ } catch {}
18027
+ }
18028
+ while (Date.now() < deadline) {
18029
+ const url = await client.getCurrentUrl(tabId).catch(() => "");
18030
+ const html = await client.getPageHtml(tabId).catch(() => "");
18031
+ if (waitFor && isUrlWaitHint(waitFor) && url.includes(waitFor)) {
18032
+ return { ok: true, url, html };
18033
+ }
18034
+ if (url && url !== beforeUrl && !url.startsWith("about:blank")) {
18035
+ return { ok: true, url, html };
18036
+ }
18037
+ if (hasMeaningfulPageChange(beforeHtml, html)) {
18038
+ return { ok: true, url: url || beforeUrl, html };
18039
+ }
18040
+ await sleep(SUBMIT_POLL_INTERVAL_MS);
18041
+ }
18042
+ return { ok: false, url: beforeUrl, html: beforeHtml };
18043
+ }
18044
+ async function submitBrowseForm(deps, options = {}) {
18045
+ const { client, session, flushCapture, restartCapture, rehydratePlugins } = deps;
18046
+ const sameOriginFetchFallback = options.sameOriginFetchFallback !== false;
18047
+ const beforeUrl = await client.getCurrentUrl(session.tabId).catch(() => session.url);
18048
+ const beforeHtml = await client.getPageHtml(session.tabId).catch(() => "");
18049
+ let submitMeta = null;
18050
+ let submitError = null;
18051
+ try {
18052
+ submitMeta = parseJsonString(await client.evaluate(session.tabId, buildDomSubmitExpression(options)));
18053
+ } catch (error) {
18054
+ submitError = error;
18055
+ }
18056
+ if (!submitMeta?.ok && submitMeta?.reason === "form_not_found") {
18057
+ return {
18058
+ ok: false,
18059
+ url: beforeUrl || session.url,
18060
+ mode: "noop",
18061
+ fallback_used: false,
18062
+ same_origin_html_rehydrated: false,
18063
+ recoverable: false,
18064
+ reason: "form_not_found",
18065
+ submit_meta: submitMeta
18066
+ };
18067
+ }
18068
+ const domOutcome = await waitForSubmitOutcome(client, session.tabId, beforeUrl, beforeHtml, options);
18069
+ if (domOutcome.ok) {
18070
+ session.url = domOutcome.url || beforeUrl || session.url;
18071
+ let captureSync2 = null;
18072
+ if (flushCapture) {
18073
+ try {
18074
+ captureSync2 = await flushCapture(session);
18075
+ } catch {
18076
+ captureSync2 = null;
18077
+ }
18078
+ }
18079
+ await restartCapture(session);
18080
+ return {
18081
+ ok: true,
18082
+ url: session.url,
18083
+ mode: "dom",
18084
+ fallback_used: false,
18085
+ same_origin_html_rehydrated: false,
18086
+ wait_for: options.waitFor,
18087
+ submit_meta: submitMeta,
18088
+ capture_sync: captureSync2
18089
+ };
18090
+ }
18091
+ if (submitError && !isRecoverableBrowseFailure(submitError) && !sameOriginFetchFallback) {
18092
+ throw submitError;
18093
+ }
18094
+ if (!sameOriginFetchFallback) {
18095
+ return {
18096
+ ok: false,
18097
+ url: beforeUrl || session.url,
18098
+ mode: "noop",
18099
+ fallback_used: false,
18100
+ same_origin_html_rehydrated: false,
18101
+ recoverable: !!submitError && isRecoverableBrowseFailure(submitError),
18102
+ reason: submitError instanceof Error ? submitError.message : "submit_failed",
18103
+ submit_meta: submitMeta
18104
+ };
18105
+ }
18106
+ const fallbackPayload = parseJsonString(await client.evaluate(session.tabId, buildSameOriginFetchExpression(options)));
18107
+ if (!fallbackPayload?.ok) {
18108
+ return {
18109
+ ok: false,
18110
+ url: String(fallbackPayload?.url ?? beforeUrl ?? session.url),
18111
+ mode: "same_origin_fetch",
18112
+ fallback_used: true,
18113
+ same_origin_html_rehydrated: false,
18114
+ recoverable: !!submitError && isRecoverableBrowseFailure(submitError),
18115
+ reason: String(fallbackPayload?.reason ?? "same_origin_fetch_failed"),
18116
+ status: typeof fallbackPayload?.status === "number" ? fallbackPayload.status : undefined,
18117
+ submit_meta: submitMeta
18118
+ };
18119
+ }
18120
+ const finalUrl = String(fallbackPayload.url ?? await client.getCurrentUrl(session.tabId).catch(() => beforeUrl));
18121
+ session.url = finalUrl || beforeUrl || session.url;
18122
+ let rehydrate = fallbackPayload.rehydrate;
18123
+ if (!rehydrate) {
18124
+ rehydrate = await rehydratePlugins(session.tabId).catch(() => null);
18125
+ }
18126
+ let captureSync = null;
18127
+ if (flushCapture) {
18128
+ try {
18129
+ captureSync = await flushCapture(session);
18130
+ } catch {
18131
+ captureSync = null;
18132
+ }
18133
+ }
18134
+ await restartCapture(session);
18135
+ return {
18136
+ ok: true,
18137
+ url: session.url,
18138
+ mode: "same_origin_fetch",
18139
+ fallback_used: true,
18140
+ same_origin_html_rehydrated: fallbackPayload.same_origin_html_rehydrated === true,
18141
+ status: typeof fallbackPayload.status === "number" ? fallbackPayload.status : undefined,
18142
+ wait_for: options.waitFor,
18143
+ submit_meta: submitMeta,
18144
+ capture_sync: captureSync,
18145
+ rehydrate
18146
+ };
18147
+ }
18148
+ var DEFAULT_SUBMIT_TIMEOUT_MS = 8000, SUBMIT_POLL_INTERVAL_MS = 250;
18149
+ var init_browse_submit = __esm(() => {
18150
+ init_browse_session();
18151
+ });
18152
+
18153
+ // ../../src/verification/matrix.ts
18154
+ function computeVerificationCoverage(matrix) {
18155
+ if (matrix.length === 0)
18156
+ return 0;
18157
+ const tested = matrix.filter((c) => c.status !== "untested").length;
18158
+ return tested / matrix.length;
18159
+ }
18160
+ var INITIAL_MATRIX;
18161
+ var init_matrix = __esm(() => {
18162
+ INITIAL_MATRIX = [
18163
+ { host: "openclaw", capability: "capture", status: "pass", last_verified: "2026-03-31" },
18164
+ { host: "openclaw", capability: "execute", status: "pass", last_verified: "2026-03-31" },
18165
+ { host: "openclaw", capability: "search", status: "pass", last_verified: "2026-03-31" },
18166
+ { host: "mcp", capability: "execute", status: "pass", last_verified: "2026-03-31" },
18167
+ { host: "mcp", capability: "search", status: "pass", last_verified: "2026-03-31" },
18168
+ { host: "cli", capability: "capture", status: "pass", last_verified: "2026-03-31" },
18169
+ { host: "cli", capability: "execute", status: "pass", last_verified: "2026-03-31" },
18170
+ { host: "cli", capability: "search", status: "pass", last_verified: "2026-03-31" },
18171
+ { host: "cli", capability: "publish", status: "pass", last_verified: "2026-03-31" },
18172
+ { host: "hermes", capability: "execute", status: "untested" },
18173
+ { host: "elizaos", capability: "execute", status: "untested" },
18174
+ { host: "langchain", capability: "execute", status: "untested" },
18175
+ { host: "langchain", capability: "search", status: "untested" }
18176
+ ];
18177
+ });
18178
+
18179
+ // ../../src/verification/index.ts
18180
+ var exports_verification = {};
18181
+ __export(exports_verification, {
18182
+ verifySkillWithCoverage: () => verifySkillWithCoverage,
18183
+ verifySkill: () => verifySkill,
18184
+ verifyEndpoint: () => verifyEndpoint,
18185
+ schedulePeriodicVerification: () => schedulePeriodicVerification
18186
+ });
18187
+ async function verifyEndpoint(skill, endpoint) {
18188
+ if (endpoint.method !== "GET")
18189
+ return endpoint.verification_status;
18190
+ try {
18191
+ const { status, data } = await executeInBrowser(endpoint.url_template, endpoint.method, endpoint.headers_template ?? {}, undefined, undefined, undefined);
18192
+ if (status < 200 || status >= 300) {
18193
+ await updateEndpointScore2(skill.skill_id, endpoint.endpoint_id, endpoint.reliability_score, "failed");
18194
+ return "failed";
18195
+ }
18196
+ let hasCriticalDrift = false;
18197
+ if (endpoint.response_schema && data != null) {
18198
+ const drift = detectSchemaDrift(endpoint.response_schema, data);
18199
+ if (drift.drifted && (drift.removed_fields.length > 0 || drift.type_changes.length > 0)) {
18200
+ hasCriticalDrift = true;
18201
+ }
18202
+ }
18203
+ const newStatus = hasCriticalDrift ? "pending" : "verified";
18204
+ const newScore = endpoint.verification_status === "disabled" && newStatus === "verified" ? 0.5 : endpoint.reliability_score;
18205
+ await updateEndpointScore2(skill.skill_id, endpoint.endpoint_id, newScore, newStatus);
18206
+ const fullSkill = await getSkill2(skill.skill_id);
18207
+ if (fullSkill) {
18208
+ const ep = fullSkill.endpoints.find((e) => e.endpoint_id === endpoint.endpoint_id);
18209
+ if (ep)
18210
+ ep.last_verified_at = new Date().toISOString();
18211
+ }
18212
+ return newStatus;
18213
+ } catch {
18214
+ await updateEndpointScore2(skill.skill_id, endpoint.endpoint_id, endpoint.reliability_score, "failed");
18215
+ return "failed";
18216
+ }
18217
+ }
18218
+ async function verifySkill(skill) {
18219
+ const results = {};
18220
+ for (const endpoint of skill.endpoints) {
18221
+ results[endpoint.endpoint_id] = await verifyEndpoint(skill, endpoint);
18222
+ }
18223
+ return results;
18224
+ }
18225
+ async function verifySkillWithCoverage(skill, matrix = INITIAL_MATRIX) {
18226
+ const results = await verifySkill(skill);
18227
+ const coverage = computeVerificationCoverage(matrix);
18228
+ return { results, coverage };
18229
+ }
18230
+ function schedulePeriodicVerification() {
18231
+ return setInterval(async () => {
18232
+ const skills = await listSkills2();
18233
+ const now = Date.now();
18234
+ for (const skill of skills) {
18235
+ if (skill.lifecycle !== "active")
18236
+ continue;
18237
+ for (const endpoint of skill.endpoints) {
18238
+ if (endpoint.method !== "GET")
18239
+ continue;
18240
+ const isDisabled = endpoint.verification_status === "disabled";
18241
+ const lastVerified = endpoint.last_verified_at ? new Date(endpoint.last_verified_at).getTime() : 0;
18242
+ if (isDisabled || now - lastVerified > STALE_THRESHOLD_MS) {
18243
+ await verifyEndpoint(skill, endpoint).catch(() => {});
18244
+ }
18245
+ }
18246
+ }
18247
+ }, VERIFICATION_INTERVAL_MS);
18248
+ }
18249
+ var VERIFICATION_INTERVAL_MS, STALE_THRESHOLD_MS;
18250
+ var init_verification = __esm(() => {
18251
+ init_capture();
18252
+ init_marketplace();
18253
+ init_marketplace();
18254
+ init_drift();
18255
+ init_matrix();
18256
+ VERIFICATION_INTERVAL_MS = 6 * 60 * 60 * 1000;
18257
+ STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
18258
+ });
18259
+
18260
+ // ../../src/api/routes.ts
18261
+ var exports_routes = {};
18262
+ __export(exports_routes, {
18263
+ registerRoutes: () => registerRoutes,
18264
+ registerBrowseSession: () => registerBrowseSession,
18265
+ buildAnalyticsSessionPayload: () => buildAnalyticsSessionPayload
18266
+ });
18267
+ import { nanoid as nanoid9 } from "nanoid";
18268
+ import { writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
18269
+ import { join as join12 } from "path";
18270
+ function buildAnalyticsSessionPayload(result, opts) {
18271
+ const source = result.timing?.source ?? result.source;
18272
+ const apiCalls = result.trace.endpoint_id ? 1 : 0;
18273
+ const browserMode = opts.browser_mode ?? (source === "live-capture" || source === "first-pass" || source === "browser-action" ? "default" : "replaced");
18274
+ const cachedSkillCalls = opts.cached_skill_calls ?? (apiCalls > 0 && source !== "live-capture" && source !== "first-pass" ? 1 : 0);
18275
+ const freshIndexCalls = opts.fresh_index_calls ?? (apiCalls > 0 && (source === "live-capture" || source === "first-pass") ? 1 : 0);
18276
+ return {
18277
+ session_id: result.trace.trace_id,
18278
+ started_at: result.trace.started_at,
18279
+ completed_at: result.trace.completed_at,
18280
+ trace_version: result.trace.trace_version ?? TRACE_VERSION,
18281
+ api_calls: apiCalls,
18282
+ discovery_queries: opts.discovery_queries,
18283
+ cached_skill_calls: cachedSkillCalls,
18284
+ fresh_index_calls: freshIndexCalls,
18285
+ browser_mode: browserMode,
18286
+ success: result.trace.success ?? true,
18287
+ source,
18288
+ time_saved_ms: result.timing?.time_saved_ms,
18289
+ time_saved_pct: result.timing?.time_saved_pct,
18290
+ tokens_saved: result.trace.tokens_saved ?? result.timing?.tokens_saved,
18291
+ tokens_saved_pct: result.trace.tokens_saved_pct ?? result.timing?.tokens_saved_pct,
18292
+ cost_saved_uc: result.timing?.cost_saved_uc
18293
+ };
18294
+ }
18295
+ function passiveIndexFromRequests(requests, pageUrl) {
18296
+ if (requests.length === 0)
18297
+ return;
18298
+ let domain;
18299
+ try {
18300
+ domain = new URL(pageUrl).hostname;
18301
+ } catch {
18302
+ return;
18303
+ }
18304
+ const intent = `browse ${domain}`;
18305
+ (async () => {
18306
+ try {
18307
+ const rawEndpoints = extractEndpoints(requests, undefined, { pageUrl, finalUrl: pageUrl });
18308
+ if (rawEndpoints.length === 0) {
18309
+ console.log(`[passive-index] ${domain}: 0 endpoints from ${requests.length} requests`);
18310
+ return;
18311
+ }
18312
+ const capturedAuthHeaders = extractAuthHeaders(requests);
18313
+ if (Object.keys(capturedAuthHeaders).length > 0) {
18314
+ const authKey = `${domain}-session`;
18315
+ await storeCredential(authKey, JSON.stringify({ headers: capturedAuthHeaders }));
18316
+ }
18317
+ const existingSkill = findExistingSkillForDomain(domain, intent);
18318
+ const mergedEndpoints = existingSkill ? mergeEndpoints(existingSkill.endpoints, rawEndpoints) : rawEndpoints;
18319
+ if (existingSkill && mergedEndpoints.length < existingSkill.endpoints.length) {
18320
+ console.log(`[passive-index] ${domain}: skipping — would reduce ${existingSkill.endpoints.length} → ${mergedEndpoints.length} endpoints`);
18321
+ return;
18322
+ }
18323
+ for (const ep of mergedEndpoints) {
18324
+ if (!ep.description) {
18325
+ ep.description = generateLocalDescription(ep);
18326
+ }
16738
18327
  }
16739
18328
  const enrichedEndpoints = mergedEndpoints;
16740
18329
  const operationGraph = buildSkillOperationGraph(enrichedEndpoints);
16741
18330
  const skill = {
16742
- skill_id: existingSkill?.skill_id ?? nanoid8(),
18331
+ skill_id: existingSkill?.skill_id ?? nanoid9(),
16743
18332
  version: "1.0.0",
16744
18333
  schema_version: "1",
16745
18334
  lifecycle: "active",
@@ -16897,7 +18486,11 @@ async function registerRoutes(app) {
16897
18486
  return reply.code(400).send({ error: "intent required" });
16898
18487
  try {
16899
18488
  const result = await resolveAndExecute(intent, params ?? {}, context, projection, { confirm_unsafe, dry_run, force_capture, client_scope: clientScope });
16900
- const res = result;
18489
+ const res = attachAgentOutcomeHints({ ...result }, {
18490
+ skill: result.skill,
18491
+ endpointId: result.trace.endpoint_id,
18492
+ timing: result.timing
18493
+ });
16901
18494
  if (result.timing) {
16902
18495
  res.timing = result.timing;
16903
18496
  }
@@ -16906,10 +18499,9 @@ async function registerRoutes(app) {
16906
18499
  res.available_endpoints = innerResult.available_endpoints;
16907
18500
  }
16908
18501
  await recordAnalyticsSession(buildAnalyticsSessionPayload(result, {
16909
- browser_mode: "replaced",
16910
18502
  discovery_queries: 1
16911
18503
  })).catch(() => {});
16912
- return reply.send(result);
18504
+ return reply.send(res);
16913
18505
  } catch (err) {
16914
18506
  return reply.code(500).send({ error: err.message });
16915
18507
  }
@@ -17119,26 +18711,33 @@ async function registerRoutes(app) {
17119
18711
  recordExecution(freshResult.trace.skill_id, freshResult.trace.endpoint_id, freshResult.trace, skill).catch(() => {});
17120
18712
  }
17121
18713
  await recordAnalyticsSession(buildAnalyticsSessionPayload(freshResult, {
17122
- browser_mode: "manual",
17123
18714
  discovery_queries: 1
17124
18715
  })).catch(() => {});
17125
- return reply.send({
18716
+ const recovered = attachAgentOutcomeHints({
17126
18717
  ...freshResult,
17127
18718
  _recovery: {
17128
18719
  reason: "stale_endpoint_404",
17129
18720
  original_skill_id: skill_id,
17130
18721
  message: "Original endpoint returned 404. Auto-recovered with fresh capture."
17131
18722
  }
18723
+ }, {
18724
+ skill: freshResult.skill ?? skill,
18725
+ endpointId: freshResult.trace.endpoint_id,
18726
+ timing: freshResult.timing
18727
+ });
18728
+ return reply.send({
18729
+ ...recovered
17132
18730
  });
17133
18731
  } catch {}
17134
18732
  }
17135
18733
  await recordAnalyticsSession(buildAnalyticsSessionPayload(execResult, {
17136
- browser_mode: "manual",
17137
- discovery_queries: 0,
17138
- cached_skill_calls: execResult.trace.endpoint_id ? 1 : 0,
17139
- fresh_index_calls: 0
18734
+ discovery_queries: 0
17140
18735
  })).catch(() => {});
17141
- return reply.send(execResult);
18736
+ const response = attachAgentOutcomeHints({ ...execResult }, {
18737
+ skill,
18738
+ endpointId: execResult.trace.endpoint_id
18739
+ });
18740
+ return reply.send(response);
17142
18741
  } catch (err) {
17143
18742
  return reply.code(500).send({ error: err.message });
17144
18743
  }
@@ -17280,344 +18879,293 @@ async function registerRoutes(app) {
17280
18879
  return "unknown";
17281
18880
  }
17282
18881
  }
17283
- async function getOrCreateBrowseSession() {
17284
- const existing = browseSessions.get("default");
17285
- if (existing)
17286
- return existing;
17287
- await start().catch(() => {});
17288
- const tabId = await newTab();
17289
- await harStart(tabId).catch(() => {});
17290
- await injectInterceptor(tabId);
17291
- const session = { tabId, url: "about:blank", harActive: true, domain: "" };
17292
- browseSessions.set("default", session);
17293
- return session;
18882
+ async function restartBrowseCapture(session) {
18883
+ await networkEnable(session.tabId).catch(() => {});
18884
+ await harStart(session.tabId).catch(() => {});
18885
+ await scriptInject(session.tabId, INTERCEPTOR_SCRIPT).catch(() => {});
18886
+ session.harActive = true;
18887
+ await injectInterceptor(session.tabId).catch(() => {});
17294
18888
  }
17295
- app.post("/v1/browse/go", async (req, reply) => {
17296
- const { url } = req.body;
17297
- if (!url)
17298
- return reply.code(400).send({ error: "url required" });
17299
- const session = await getOrCreateBrowseSession();
17300
- const newDomain = profileName(url);
17301
- if (session.harActive && session.url !== "about:blank") {
18889
+ async function flushBrowseCapture(session, options = {}) {
18890
+ let intercepted = [];
18891
+ try {
18892
+ const raw = await collectInterceptedRequests(session.tabId);
18893
+ intercepted = raw.map((request) => ({
18894
+ url: request.url,
18895
+ method: request.method,
18896
+ request_headers: request.request_headers ?? {},
18897
+ request_body: request.request_body,
18898
+ response_status: request.response_status,
18899
+ response_headers: request.response_headers ?? {},
18900
+ response_body: request.response_body,
18901
+ timestamp: request.timestamp
18902
+ }));
18903
+ } catch {}
18904
+ let harEntries = [];
18905
+ if (session.harActive) {
17302
18906
  try {
17303
18907
  const { entries } = await harStop(session.tabId);
17304
- passiveIndexHar(entries, session.url);
18908
+ harEntries = entries;
17305
18909
  } catch {}
17306
- session.harActive = false;
17307
18910
  }
17308
- if (session.domain && session.domain !== newDomain) {
17309
- await authProfileSave(session.tabId, session.domain).catch(() => {});
18911
+ session.harActive = false;
18912
+ const allRequests = mergeBrowseRequests(intercepted, harEntries, session.url);
18913
+ const syncResult = await cacheBrowseRequests({
18914
+ sessionUrl: session.url,
18915
+ sessionDomain: session.domain,
18916
+ requests: allRequests,
18917
+ getPageHtml: () => getPageHtml(session.tabId)
18918
+ });
18919
+ let backgroundPublishQueued = false;
18920
+ if (options.queueBackgroundPublish) {
18921
+ if (allRequests.length > 0) {
18922
+ passiveIndexFromRequests(allRequests, session.url);
18923
+ backgroundPublishQueued = true;
18924
+ } else if (syncResult.skill) {
18925
+ queueBackgroundIndex({
18926
+ skill: { ...syncResult.skill },
18927
+ domain: syncResult.domain,
18928
+ intent: syncResult.skill.intent_signature || `browse ${syncResult.domain}`,
18929
+ contextUrl: session.url,
18930
+ cacheKey: `browse-submit:${syncResult.domain}:${Date.now()}`
18931
+ });
18932
+ backgroundPublishQueued = true;
18933
+ }
17310
18934
  }
17311
- let cookiesInjected = 0;
17312
- if (newDomain && newDomain !== session.domain) {
17313
- await authProfileLoad(session.tabId, newDomain).catch(() => {});
17314
- try {
17315
- const { cookies: browserCookies } = extractBrowserCookies(newDomain);
17316
- if (browserCookies.length > 0) {
17317
- for (const c of browserCookies) {
17318
- await setCookie(session.tabId, c).catch(() => {});
18935
+ return {
18936
+ indexed: syncResult.indexed,
18937
+ mode: syncResult.mode,
18938
+ domain: syncResult.domain,
18939
+ skill_id: syncResult.skill?.skill_id ?? null,
18940
+ endpoint_count: syncResult.skill?.endpoints.length ?? 0,
18941
+ endpoints: (syncResult.skill?.endpoints ?? []).map((endpoint) => ({
18942
+ endpoint_id: endpoint.endpoint_id,
18943
+ method: endpoint.method,
18944
+ url_template: endpoint.url_template,
18945
+ description: endpoint.description,
18946
+ trigger_url: endpoint.trigger_url,
18947
+ action_kind: endpoint.semantic?.action_kind,
18948
+ resource_kind: endpoint.semantic?.resource_kind
18949
+ })),
18950
+ request_count: allRequests.length,
18951
+ background_publish_queued: backgroundPublishQueued
18952
+ };
18953
+ }
18954
+ app.post("/v1/browse/go", async (req, reply) => {
18955
+ const { url } = req.body;
18956
+ if (!url)
18957
+ return reply.code(400).send({ error: "url required" });
18958
+ const { session, result } = await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session2) => {
18959
+ const newDomain = profileName(url);
18960
+ if (session2.harActive && session2.url !== "about:blank") {
18961
+ try {
18962
+ const { entries } = await harStop(session2.tabId);
18963
+ passiveIndexHar(entries, session2.url);
18964
+ } catch {}
18965
+ session2.harActive = false;
18966
+ }
18967
+ if (session2.domain && session2.domain !== newDomain) {
18968
+ await authProfileSave(session2.tabId, session2.domain).catch(() => {});
18969
+ }
18970
+ let cookiesInjected = 0;
18971
+ if (newDomain && newDomain !== session2.domain) {
18972
+ await authProfileLoad(session2.tabId, newDomain).catch(() => {});
18973
+ try {
18974
+ const { cookies: browserCookies } = extractBrowserCookies(newDomain);
18975
+ if (browserCookies.length > 0) {
18976
+ for (const c of browserCookies) {
18977
+ await setCookie(session2.tabId, c).catch(() => {});
18978
+ }
18979
+ cookiesInjected = browserCookies.length;
17319
18980
  }
17320
- cookiesInjected = browserCookies.length;
17321
- }
17322
- } catch {}
17323
- }
17324
- await networkEnable(session.tabId).catch(() => {});
17325
- await harStart(session.tabId).catch(() => {});
17326
- await scriptInject(session.tabId, INTERCEPTOR_SCRIPT).catch(() => {});
17327
- session.harActive = true;
17328
- await navigate(session.tabId, url);
17329
- const finalUrl = await getCurrentUrl(session.tabId).catch(() => url);
17330
- session.url = typeof finalUrl === "string" && finalUrl.startsWith("http") ? finalUrl : url;
18981
+ } catch {}
18982
+ }
18983
+ await restartBrowseCapture(session2);
18984
+ await navigate(session2.tabId, url);
18985
+ const finalUrl = await getCurrentUrl(session2.tabId).catch(() => url);
18986
+ session2.url = typeof finalUrl === "string" && finalUrl.startsWith("http") ? finalUrl : url;
18987
+ session2.domain = profileName(session2.url);
18988
+ await injectInterceptor(session2.tabId);
18989
+ return { cookiesInjected };
18990
+ }, (result2) => isRecoverableBrowseFailure(result2));
18991
+ return reply.send({
18992
+ ok: true,
18993
+ url: session.url,
18994
+ tab_id: session.tabId,
18995
+ auth_profile: session.domain,
18996
+ ...result.cookiesInjected > 0 ? { cookies_injected: result.cookiesInjected } : {}
18997
+ });
18998
+ });
18999
+ app.post("/v1/browse/submit", async (req, reply) => {
19000
+ const {
19001
+ form_selector: formSelector,
19002
+ submit_selector: submitSelector,
19003
+ wait_for: waitFor,
19004
+ same_origin_fetch_fallback: sameOriginFetchFallback,
19005
+ timeout_ms: timeoutMs
19006
+ } = req.body ?? {};
19007
+ const { session, result, recovered } = await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session2) => submitBrowseForm({
19008
+ client: exports_client,
19009
+ session: session2,
19010
+ flushCapture: async (session3) => await flushBrowseCapture(session3, { queueBackgroundPublish: true }),
19011
+ restartCapture: restartBrowseCapture,
19012
+ rehydratePlugins: bestEffortRehydratePlugins
19013
+ }, {
19014
+ formSelector,
19015
+ submitSelector,
19016
+ waitFor,
19017
+ sameOriginFetchFallback,
19018
+ timeoutMs
19019
+ }), (result2) => !result2.ok && result2.recoverable === true);
19020
+ session.url = result.url || await getCurrentUrl(session.tabId).catch(() => session.url);
17331
19021
  session.domain = profileName(session.url);
17332
- await injectInterceptor(session.tabId);
17333
- return reply.send({ ok: true, url: session.url, tab_id: session.tabId, auth_profile: session.domain, ...cookiesInjected > 0 ? { cookies_injected: cookiesInjected } : {} });
19022
+ const statusCode = result.ok ? 200 : result.recoverable ? 502 : 400;
19023
+ const nextStep = result.ok ? result.capture_sync?.background_publish_queued ? "Background publish queued for this step. Continue the flow, then run `unbrowse close` when you're done to save auth and finalize any remaining capture." : "If more UI steps remain, continue the flow. Run `unbrowse close` when you're done to save auth and finalize capture." : "Inspect the page state with `unbrowse snap --filter interactive`, then retry submit with selectors or a wait hint if needed.";
19024
+ return reply.code(statusCode).send({
19025
+ ...result,
19026
+ next_step: nextStep,
19027
+ recovered,
19028
+ tab_id: session.tabId,
19029
+ url: session.url
19030
+ });
17334
19031
  });
17335
19032
  app.post("/v1/browse/snap", async (req, reply) => {
17336
19033
  const { filter } = req.body ?? {};
17337
- const session = await getOrCreateBrowseSession();
17338
- const snapshot2 = await snapshot(session.tabId, filter);
19034
+ const { session, result: snapshot2 } = await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session2) => snapshot(session2.tabId, filter), (snapshot3) => typeof snapshot3 !== "string" || snapshot3.trim().length === 0);
17339
19035
  return reply.send({ snapshot: snapshot2, tab_id: session.tabId });
17340
19036
  });
17341
19037
  app.post("/v1/browse/click", async (req, reply) => {
17342
19038
  const { ref } = req.body;
17343
19039
  if (!ref)
17344
19040
  return reply.code(400).send({ error: "ref required" });
17345
- const session = await getOrCreateBrowseSession();
17346
- await click(session.tabId, ref);
19041
+ await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => {
19042
+ await click(session.tabId, ref);
19043
+ return true;
19044
+ });
17347
19045
  return reply.send({ ok: true });
17348
19046
  });
17349
19047
  app.post("/v1/browse/fill", async (req, reply) => {
17350
19048
  const { ref, value } = req.body;
17351
19049
  if (!ref || value === undefined)
17352
19050
  return reply.code(400).send({ error: "ref and value required" });
17353
- const session = await getOrCreateBrowseSession();
17354
- await fill(session.tabId, ref, value);
19051
+ await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => {
19052
+ await fill(session.tabId, ref, value);
19053
+ return true;
19054
+ });
17355
19055
  return reply.send({ ok: true });
17356
19056
  });
17357
19057
  app.post("/v1/browse/type", async (req, reply) => {
17358
19058
  const { text } = req.body;
17359
19059
  if (!text)
17360
19060
  return reply.code(400).send({ error: "text required" });
17361
- const session = await getOrCreateBrowseSession();
17362
- await keyboardType(session.tabId, text);
19061
+ await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => {
19062
+ await keyboardType(session.tabId, text);
19063
+ return true;
19064
+ });
17363
19065
  return reply.send({ ok: true });
17364
19066
  });
17365
19067
  app.post("/v1/browse/press", async (req, reply) => {
17366
19068
  const { key } = req.body;
17367
19069
  if (!key)
17368
19070
  return reply.code(400).send({ error: "key required" });
17369
- const session = await getOrCreateBrowseSession();
17370
- await press(session.tabId, key);
19071
+ await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => {
19072
+ await press(session.tabId, key);
19073
+ return true;
19074
+ });
17371
19075
  return reply.send({ ok: true });
17372
19076
  });
17373
19077
  app.post("/v1/browse/select", async (req, reply) => {
17374
19078
  const { ref, value } = req.body;
17375
19079
  if (!ref || value === undefined)
17376
19080
  return reply.code(400).send({ error: "ref and value required" });
17377
- const session = await getOrCreateBrowseSession();
17378
- await select(session.tabId, ref, value);
19081
+ await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => {
19082
+ await select(session.tabId, ref, value);
19083
+ return true;
19084
+ });
17379
19085
  return reply.send({ ok: true });
17380
19086
  });
17381
19087
  app.post("/v1/browse/scroll", async (req, reply) => {
17382
19088
  const { direction, amount } = req.body ?? {};
17383
- const session = await getOrCreateBrowseSession();
17384
- await scroll(session.tabId, direction ?? "down", amount);
19089
+ await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => {
19090
+ await scroll(session.tabId, direction ?? "down", amount);
19091
+ return true;
19092
+ });
17385
19093
  return reply.send({ ok: true });
17386
19094
  });
17387
19095
  app.get("/v1/browse/screenshot", async (_req, reply) => {
17388
- const session = await getOrCreateBrowseSession();
17389
- const data = await screenshot(session.tabId);
19096
+ const { session, result: data } = await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session2) => screenshot(session2.tabId), (data2) => typeof data2 !== "string" || data2.trim().length === 0);
17390
19097
  return reply.send({ screenshot: data, tab_id: session.tabId });
17391
19098
  });
17392
19099
  app.get("/v1/browse/text", async (_req, reply) => {
17393
- const session = await getOrCreateBrowseSession();
17394
- const text = await getText(session.tabId);
19100
+ const { result: text } = await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => getText(session.tabId), (text2) => typeof text2 !== "string");
17395
19101
  return reply.send({ text });
17396
19102
  });
17397
19103
  app.get("/v1/browse/markdown", async (_req, reply) => {
17398
- const session = await getOrCreateBrowseSession();
17399
- const markdown = await getMarkdown(session.tabId);
19104
+ const { result: markdown } = await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => getMarkdown(session.tabId), (markdown2) => typeof markdown2 !== "string");
17400
19105
  return reply.send({ markdown });
17401
19106
  });
17402
19107
  app.get("/v1/browse/cookies", async (_req, reply) => {
17403
- const session = await getOrCreateBrowseSession();
17404
- const cookies = await getCookies(session.tabId);
19108
+ const { result: cookies } = await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => getCookies(session.tabId));
17405
19109
  return reply.send({ cookies });
17406
19110
  });
17407
19111
  app.post("/v1/browse/eval", async (req, reply) => {
17408
- const { expression } = req.body;
17409
- if (!expression)
17410
- return reply.code(400).send({ error: "expression required" });
17411
- const session = await getOrCreateBrowseSession();
17412
- const result = await evaluate(session.tabId, expression);
17413
- return reply.send({ result });
17414
- });
17415
- app.post("/v1/browse/back", async (_req, reply) => {
17416
- const session = await getOrCreateBrowseSession();
17417
- await goBack(session.tabId);
17418
- return reply.send({ ok: true });
17419
- });
17420
- app.post("/v1/browse/forward", async (_req, reply) => {
17421
- const session = await getOrCreateBrowseSession();
17422
- await goForward(session.tabId);
17423
- return reply.send({ ok: true });
17424
- });
17425
- app.post("/v1/browse/close", async (_req, reply) => {
17426
- const session = browseSessions.get("default");
17427
- if (!session)
17428
- return reply.send({ ok: true, message: "no active session" });
17429
- if (session.domain) {
17430
- await authProfileSave(session.tabId, session.domain).catch(() => {});
17431
- }
17432
- let intercepted = [];
17433
- try {
17434
- const raw = await collectInterceptedRequests(session.tabId);
17435
- intercepted = raw.map((r) => ({
17436
- url: r.url,
17437
- method: r.method,
17438
- request_headers: r.request_headers ?? {},
17439
- request_body: r.request_body,
17440
- response_status: r.response_status,
17441
- response_headers: r.response_headers ?? {},
17442
- response_body: r.response_body,
17443
- timestamp: r.timestamp
17444
- }));
17445
- } catch {}
17446
- let harEntries = [];
17447
- if (session.harActive) {
17448
- try {
17449
- const { entries } = await harStop(session.tabId);
17450
- harEntries = entries;
17451
- } catch {}
17452
- }
17453
- const harRequests = harEntriesToRawRequests(harEntries);
17454
- const seen = new Set;
17455
- const allRequests = [];
17456
- for (const r of intercepted) {
17457
- const key = `${r.method}:${r.url}`;
17458
- if (!seen.has(key)) {
17459
- seen.add(key);
17460
- allRequests.push(r);
17461
- }
17462
- }
17463
- for (const r of harRequests) {
17464
- const key = `${r.method}:${r.url}`;
17465
- if (!seen.has(key)) {
17466
- seen.add(key);
17467
- allRequests.push(r);
17468
- }
17469
- }
17470
- {
17471
- let domain;
17472
- try {
17473
- domain = new URL(session.url).hostname;
17474
- } catch {
17475
- domain = session.domain;
17476
- }
17477
- const rawEndpoints = extractEndpoints(allRequests, undefined, { pageUrl: session.url, finalUrl: session.url });
17478
- if (rawEndpoints.length > 0) {
17479
- const existingSkill = findExistingSkillForDomain(domain);
17480
- let allExisting = existingSkill?.endpoints ?? [];
17481
- const domainKey = getDomainReuseKey(session.url ?? domain);
17482
- if (domainKey) {
17483
- const cached = domainSkillCache.get(domainKey);
17484
- if (cached?.localSkillPath) {
17485
- try {
17486
- const snapshot2 = JSON.parse(__require("fs").readFileSync(cached.localSkillPath, "utf-8"));
17487
- if (snapshot2?.endpoints?.length > 0) {
17488
- allExisting = mergeEndpoints(allExisting, snapshot2.endpoints);
17489
- }
17490
- } catch {}
17491
- }
17492
- }
17493
- const mergedEps = allExisting.length > 0 ? mergeEndpoints(allExisting, rawEndpoints) : rawEndpoints;
17494
- if (!existingSkill || mergedEps.length >= existingSkill.endpoints.length) {
17495
- for (const ep of mergedEps) {
17496
- if (!ep.description)
17497
- ep.description = generateLocalDescription(ep);
17498
- }
17499
- const quickSkill = {
17500
- skill_id: existingSkill?.skill_id ?? nanoid8(),
17501
- version: "1.0.0",
17502
- schema_version: "1",
17503
- lifecycle: "active",
17504
- execution_type: "http",
17505
- created_at: existingSkill?.created_at ?? new Date().toISOString(),
17506
- updated_at: new Date().toISOString(),
17507
- name: domain,
17508
- intent_signature: `browse ${domain}`,
17509
- domain,
17510
- description: `API skill for ${domain}`,
17511
- owner_type: "agent",
17512
- endpoints: mergedEps,
17513
- intents: Array.from(new Set([...existingSkill?.intents ?? [], `browse ${domain}`]))
17514
- };
17515
- const cacheKey = buildResolveCacheKey(domain, `browse ${domain}`, session.url);
17516
- const scopedKey = scopedCacheKey("global", cacheKey);
17517
- writeSkillSnapshot(scopedKey, quickSkill);
17518
- const domainKey2 = getDomainReuseKey(session.url ?? domain);
17519
- if (domainKey2) {
17520
- domainSkillCache.set(domainKey2, {
17521
- skillId: quickSkill.skill_id,
17522
- localSkillPath: snapshotPathForCacheKey(scopedKey),
17523
- ts: Date.now()
17524
- });
17525
- persistDomainCache();
17526
- }
17527
- try {
17528
- cachePublishedSkill(quickSkill);
17529
- } catch {}
17530
- invalidateRouteCacheForDomain(domain);
17531
- console.log(`[passive-index] ${domain}: ${mergedEps.length} endpoints cached synchronously`);
17532
- }
17533
- } else {
17534
- let domain2;
17535
- try {
17536
- domain2 = new URL(session.url).hostname;
17537
- } catch {
17538
- domain2 = session.domain;
17539
- }
17540
- try {
17541
- const html = await getPageHtml(session.tabId);
17542
- if (html && typeof html === "string" && html.startsWith("<")) {
17543
- const { extractFromDOM: extractFromDOM2 } = await Promise.resolve().then(() => (init_extraction(), exports_extraction));
17544
- const { detectSearchForms: detectSearchForms2, isStructuredSearchForm: isStructuredSearchForm2 } = await Promise.resolve().then(() => (init_search_forms(), exports_search_forms));
17545
- const { inferSchema: inferSchema2 } = await Promise.resolve().then(() => (init_transform(), exports_transform));
17546
- const { inferEndpointSemantic: inferEndpointSemantic2 } = await Promise.resolve().then(() => (init_graph(), exports_graph));
17547
- const { templatizeQueryParams: templatizeQueryParams2 } = await init_execution().then(() => exports_execution);
17548
- const extracted = extractFromDOM2(html, `browse ${domain2}`);
17549
- const searchForms = detectSearchForms2(html);
17550
- const validForm = searchForms.find((s) => isStructuredSearchForm2(s));
17551
- if (extracted.data || validForm) {
17552
- const urlTemplate = templatizeQueryParams2(session.url);
17553
- const ep = {
17554
- endpoint_id: nanoid8(),
17555
- method: "GET",
17556
- url_template: urlTemplate,
17557
- idempotency: "safe",
17558
- verification_status: "verified",
17559
- reliability_score: extracted.confidence ?? 0.7,
17560
- description: validForm ? `Search form for ${domain2}` : `Page content from ${domain2}`,
17561
- response_schema: extracted.data ? inferSchema2([extracted.data]) : undefined,
17562
- dom_extraction: {
17563
- extraction_method: extracted.extraction_method ?? "repeated-elements",
17564
- confidence: extracted.confidence ?? 0.7,
17565
- ...extracted.selector ? { selector: extracted.selector } : {},
17566
- ...validForm ? { search_form: validForm } : {}
17567
- },
17568
- trigger_url: session.url
17569
- };
17570
- ep.semantic = inferEndpointSemantic2(ep, {
17571
- sampleResponse: extracted.data,
17572
- observedAt: new Date().toISOString(),
17573
- sampleRequestUrl: session.url
17574
- });
17575
- const existing = findExistingSkillForDomain(domain2);
17576
- const allEps = existing ? mergeEndpoints(existing.endpoints, [ep]) : [ep];
17577
- for (const e of allEps) {
17578
- if (!e.description)
17579
- e.description = generateLocalDescription(e);
17580
- }
17581
- const skill = {
17582
- skill_id: existing?.skill_id ?? nanoid8(),
17583
- version: "1.0.0",
17584
- schema_version: "1",
17585
- lifecycle: "active",
17586
- execution_type: "http",
17587
- created_at: existing?.created_at ?? new Date().toISOString(),
17588
- updated_at: new Date().toISOString(),
17589
- name: domain2,
17590
- intent_signature: `browse ${domain2}`,
17591
- domain: domain2,
17592
- description: `DOM skill for ${domain2}`,
17593
- owner_type: "agent",
17594
- endpoints: allEps,
17595
- intents: [...new Set([...existing?.intents ?? [], `browse ${domain2}`])]
17596
- };
17597
- const ck = buildResolveCacheKey(domain2, `browse ${domain2}`, session.url);
17598
- const sk = scopedCacheKey("global", ck);
17599
- writeSkillSnapshot(sk, skill);
17600
- const dk = getDomainReuseKey(session.url ?? domain2);
17601
- if (dk) {
17602
- domainSkillCache.set(dk, { skillId: skill.skill_id, localSkillPath: snapshotPathForCacheKey(sk), ts: Date.now() });
17603
- persistDomainCache();
17604
- }
17605
- try {
17606
- cachePublishedSkill(skill);
17607
- } catch {}
17608
- invalidateRouteCacheForDomain(domain2);
17609
- console.log(`[close] ${domain2}: DOM endpoint created (form=${!!validForm})`);
17610
- }
17611
- }
17612
- } catch (err) {
17613
- console.log(`[close] DOM fallback failed: ${err instanceof Error ? err.message : err}`);
17614
- }
17615
- }
19112
+ const { expression } = req.body;
19113
+ if (!expression)
19114
+ return reply.code(400).send({ error: "expression required" });
19115
+ const { result } = await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => evaluate(session.tabId, expression), (result2) => isRecoverableBrowseFailure(result2));
19116
+ return reply.send({ result });
19117
+ });
19118
+ app.post("/v1/browse/back", async (_req, reply) => {
19119
+ await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => {
19120
+ await goBack(session.tabId);
19121
+ return true;
19122
+ });
19123
+ return reply.send({ ok: true });
19124
+ });
19125
+ app.post("/v1/browse/forward", async (_req, reply) => {
19126
+ await withRecoveredBrowseSession(browseSessions, exports_client, injectInterceptor, async (session) => {
19127
+ await goForward(session.tabId);
19128
+ return true;
19129
+ });
19130
+ return reply.send({ ok: true });
19131
+ });
19132
+ app.post("/v1/browse/sync", async (_req, reply) => {
19133
+ const session = browseSessions.get("default");
19134
+ if (!session)
19135
+ return reply.send({ ok: false, error: "no active session" });
19136
+ const syncResult = await flushBrowseCapture(session);
19137
+ await restartBrowseCapture(session);
19138
+ return reply.send({
19139
+ ok: true,
19140
+ tab_id: session.tabId,
19141
+ indexed: syncResult.indexed,
19142
+ mode: syncResult.mode,
19143
+ domain: syncResult.domain,
19144
+ skill_id: syncResult.skill_id,
19145
+ endpoint_count: syncResult.endpoint_count,
19146
+ endpoints: syncResult.endpoints,
19147
+ request_count: syncResult.request_count
19148
+ });
19149
+ });
19150
+ app.post("/v1/browse/close", async (_req, reply) => {
19151
+ const session = browseSessions.get("default");
19152
+ if (!session)
19153
+ return reply.send({ ok: true, message: "no active session" });
19154
+ if (session.domain) {
19155
+ await authProfileSave(session.tabId, session.domain).catch(() => {});
17616
19156
  }
17617
- passiveIndexFromRequests(allRequests, session.url);
19157
+ const syncResult = await flushBrowseCapture(session, { queueBackgroundPublish: true });
17618
19158
  await closeTab(session.tabId).catch(() => {});
17619
19159
  browseSessions.delete("default");
17620
- return reply.send({ ok: true, indexed: true, auth_saved: session.domain || null });
19160
+ return reply.send({
19161
+ ok: true,
19162
+ indexed: syncResult.indexed,
19163
+ mode: syncResult.mode,
19164
+ endpoint_count: syncResult.endpoint_count,
19165
+ request_count: syncResult.request_count,
19166
+ background_publish_queued: syncResult.background_publish_queued,
19167
+ auth_saved: session.domain || null
19168
+ });
17621
19169
  });
17622
19170
  }
17623
19171
  function saveTrace(trace) {
@@ -17642,6 +19190,9 @@ var init_routes = __esm(async () => {
17642
19190
  init_ratelimit();
17643
19191
  init_graph();
17644
19192
  init_session_logs();
19193
+ init_agent_outcome();
19194
+ init_browse_session();
19195
+ init_browse_submit();
17645
19196
  await __promiseAll([
17646
19197
  init_indexer(),
17647
19198
  init_vault(),
@@ -17649,7 +19200,8 @@ var init_routes = __esm(async () => {
17649
19200
  init_orchestrator(),
17650
19201
  init_execution(),
17651
19202
  init_auth(),
17652
- init_indexer()
19203
+ init_indexer(),
19204
+ init_browse_index()
17653
19205
  ]);
17654
19206
  BETA_API_URL = process.env.UNBROWSE_BACKEND_URL || "https://beta-api.unbrowse.ai";
17655
19207
  TRACES_DIR = process.env.TRACES_DIR ?? join12(process.cwd(), "traces");
@@ -17730,6 +19282,7 @@ var init_server = __esm(async () => {
17730
19282
 
17731
19283
  // ../../src/cli.ts
17732
19284
  import { config as loadEnv } from "dotenv";
19285
+ import { spawn as spawn3 } from "child_process";
17733
19286
 
17734
19287
  // ../../src/client/index.ts
17735
19288
  init_cascade();
@@ -17765,9 +19318,15 @@ function getConfigDir() {
17765
19318
  function getConfigPath() {
17766
19319
  return join(getConfigDir(), "config.json");
17767
19320
  }
19321
+ function getInstallTelemetryPath() {
19322
+ return join(getConfigDir(), "install-state.json");
19323
+ }
17768
19324
  function sanitizeProfileName(value) {
17769
19325
  return value.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
17770
19326
  }
19327
+ function getActiveProfile() {
19328
+ return PROFILE_NAME || "default";
19329
+ }
17771
19330
  function loadConfig() {
17772
19331
  try {
17773
19332
  const configPath = getConfigPath();
@@ -17784,6 +19343,120 @@ function saveConfig(config) {
17784
19343
  mkdirSync(configDir, { recursive: true });
17785
19344
  writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
17786
19345
  }
19346
+ function loadInstallTelemetryState() {
19347
+ try {
19348
+ const statePath = getInstallTelemetryPath();
19349
+ if (existsSync(statePath)) {
19350
+ return JSON.parse(readFileSync(statePath, "utf-8"));
19351
+ }
19352
+ } catch {}
19353
+ return null;
19354
+ }
19355
+ function saveInstallTelemetryState(state) {
19356
+ const configDir = getConfigDir();
19357
+ const statePath = getInstallTelemetryPath();
19358
+ if (!existsSync(configDir))
19359
+ mkdirSync(configDir, { recursive: true });
19360
+ writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
19361
+ }
19362
+ function createInstallTelemetryState() {
19363
+ return {
19364
+ install_id: `install_${randomBytes(8).toString("hex")}`,
19365
+ first_seen_at: new Date().toISOString()
19366
+ };
19367
+ }
19368
+ function getOrCreateInstallTelemetryState() {
19369
+ const existing = loadInstallTelemetryState();
19370
+ if (existing?.install_id)
19371
+ return existing;
19372
+ const created = createInstallTelemetryState();
19373
+ saveInstallTelemetryState(created);
19374
+ return created;
19375
+ }
19376
+ function getInstallId() {
19377
+ return getOrCreateInstallTelemetryState().install_id;
19378
+ }
19379
+ function detectTelemetryHostType() {
19380
+ switch (detectHostEnvironment()) {
19381
+ case "openai":
19382
+ return "codex";
19383
+ case "openclaw":
19384
+ return "openclaw";
19385
+ case "mcp":
19386
+ return "mcp";
19387
+ case "native":
19388
+ return "native";
19389
+ case "unknown":
19390
+ default:
19391
+ return "cli";
19392
+ }
19393
+ }
19394
+ async function postTelemetry(path, body) {
19395
+ if (LOCAL_ONLY)
19396
+ return false;
19397
+ try {
19398
+ const key = getApiKey();
19399
+ const res = await fetch(`${API_URL}${path}`, {
19400
+ method: "POST",
19401
+ headers: {
19402
+ "Content-Type": "application/json",
19403
+ "Accept-Encoding": "gzip, deflate",
19404
+ ...key ? { Authorization: `Bearer ${key}` } : {}
19405
+ },
19406
+ body: JSON.stringify(body)
19407
+ });
19408
+ return res.ok;
19409
+ } catch {
19410
+ return false;
19411
+ }
19412
+ }
19413
+ async function ensureCliInstallTracked(hostType = detectTelemetryHostType()) {
19414
+ const state = getOrCreateInstallTelemetryState();
19415
+ if (state.cli_first_seen_reported_at)
19416
+ return;
19417
+ const createdAt = new Date().toISOString();
19418
+ const ok = await postTelemetry("/v1/telemetry/install", {
19419
+ install_id: state.install_id,
19420
+ source: "cli-first-seen",
19421
+ host_type: hostType,
19422
+ skill: "unbrowse",
19423
+ status: "installed",
19424
+ created_at: createdAt,
19425
+ properties: {
19426
+ profile: getActiveProfile(),
19427
+ first_seen_at: state.first_seen_at
19428
+ }
19429
+ });
19430
+ if (!ok)
19431
+ return;
19432
+ state.cli_first_seen_reported_at = createdAt;
19433
+ saveInstallTelemetryState(state);
19434
+ }
19435
+ async function recordInstallTelemetryEvent(source, options) {
19436
+ const createdAt = options?.createdAt ?? new Date().toISOString();
19437
+ await postTelemetry("/v1/telemetry/install", {
19438
+ install_id: getInstallId(),
19439
+ source,
19440
+ host_type: options?.hostType ?? detectTelemetryHostType(),
19441
+ skill: options?.skill ?? "unbrowse",
19442
+ skill_version: options?.skillVersion,
19443
+ status: options?.status ?? "installed",
19444
+ created_at: createdAt,
19445
+ properties: options?.properties
19446
+ });
19447
+ }
19448
+ async function recordFunnelTelemetryEvent(name, options) {
19449
+ const createdAt = options?.createdAt ?? new Date().toISOString();
19450
+ await postTelemetry("/v1/telemetry/events", {
19451
+ install_id: getInstallId(),
19452
+ session_id: options?.sessionId,
19453
+ name,
19454
+ source: options?.source ?? "cli",
19455
+ host_type: options?.hostType ?? detectTelemetryHostType(),
19456
+ created_at: createdAt,
19457
+ properties: options?.properties
19458
+ });
19459
+ }
17787
19460
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/i;
17788
19461
  function normalizeAgentEmail(value) {
17789
19462
  return value.trim().toLowerCase();
@@ -18068,6 +19741,12 @@ async function ensureRegistered(options) {
18068
19741
  tos_accepted_at: new Date().toISOString(),
18069
19742
  ...wallet
18070
19743
  });
19744
+ await recordFunnelTelemetryEvent("registration_succeeded", {
19745
+ source: "cli",
19746
+ properties: {
19747
+ prompt_for_email: options?.promptForEmail === true
19748
+ }
19749
+ });
18071
19750
  console.log(`Registered as ${name}. API key saved to ~/.unbrowse/config.json`);
18072
19751
  } catch (err) {
18073
19752
  console.warn(`Registration failed: ${err.message}`);
@@ -18286,7 +19965,7 @@ function buildDepsMetadata(pack, taskName) {
18286
19965
  // ../../src/runtime/local-server.ts
18287
19966
  init_paths();
18288
19967
  init_supervisor();
18289
- import { openSync, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
19968
+ import { openSync, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
18290
19969
  import path6 from "node:path";
18291
19970
  import { spawn as spawn2 } from "node:child_process";
18292
19971
  async function isServerHealthy(baseUrl, timeoutMs = 2000) {
@@ -18316,7 +19995,7 @@ function isPidAlive(pid) {
18316
19995
  }
18317
19996
  function readPidState(pidFile) {
18318
19997
  try {
18319
- return JSON.parse(readFileSync9(pidFile, "utf-8"));
19998
+ return JSON.parse(readFileSync10(pidFile, "utf-8"));
18320
19999
  } catch {
18321
20000
  return null;
18322
20001
  }
@@ -18335,7 +20014,7 @@ function deriveListenEnv(baseUrl) {
18335
20014
  function getVersion(metaUrl) {
18336
20015
  try {
18337
20016
  const root = getPackageRoot(metaUrl);
18338
- const pkg = JSON.parse(readFileSync9(path6.join(root, "package.json"), "utf-8"));
20017
+ const pkg = JSON.parse(readFileSync10(path6.join(root, "package.json"), "utf-8"));
18339
20018
  return pkg.version ?? "unknown";
18340
20019
  } catch {
18341
20020
  return "unknown";
@@ -18470,7 +20149,26 @@ async function restartServer(baseUrl, metaUrl) {
18470
20149
  // ../../src/runtime/paths.ts
18471
20150
  import { existsSync as existsSync13, mkdirSync as mkdirSync11, realpathSync as realpathSync2 } from "node:fs";
18472
20151
  import path7 from "node:path";
20152
+ import { createRequire as createRequire3 } from "node:module";
18473
20153
  import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "node:url";
20154
+ function resolveSiblingEntrypoint2(metaUrl, basename) {
20155
+ const file = fileURLToPath3(metaUrl);
20156
+ return path7.join(path7.dirname(file), `${basename}${path7.extname(file) || ".js"}`);
20157
+ }
20158
+ function runtimeArgsForEntrypoint2(metaUrl, entrypoint) {
20159
+ if (path7.extname(entrypoint) !== ".ts")
20160
+ return [entrypoint];
20161
+ if (process.versions.bun)
20162
+ return [entrypoint];
20163
+ try {
20164
+ const req = createRequire3(metaUrl);
20165
+ const tsxPkg = req.resolve("tsx/package.json");
20166
+ const tsxLoader = path7.join(path7.dirname(tsxPkg), "dist", "loader.mjs");
20167
+ if (existsSync13(tsxLoader))
20168
+ return ["--import", pathToFileURL2(tsxLoader).href, entrypoint];
20169
+ } catch {}
20170
+ return ["--import", "tsx", entrypoint];
20171
+ }
18474
20172
  function isMainModule(metaUrl) {
18475
20173
  const entry = process.argv[1];
18476
20174
  if (!entry)
@@ -18519,26 +20217,11 @@ async function drainPendingPassivePublishes() {
18519
20217
  // ../../src/runtime/setup.ts
18520
20218
  init_paths();
18521
20219
  init_client();
20220
+ init_logger();
18522
20221
  import { execFileSync as execFileSync3 } from "node:child_process";
18523
20222
  import { existsSync as existsSync14, mkdirSync as mkdirSync12, writeFileSync as writeFileSync11 } from "node:fs";
18524
20223
  import os4 from "node:os";
18525
20224
  import path8 from "node:path";
18526
-
18527
- // ../../src/runtime/browser-host.ts
18528
- function detectHostEnvironment() {
18529
- if (process.env.OPENCLAW_RUNTIME)
18530
- return "openclaw";
18531
- if (process.env.OPENAI_TOOL_RUNTIME)
18532
- return "openai";
18533
- if (process.env.MCP_SERVER_MODE)
18534
- return "mcp";
18535
- if (process.env.UNBROWSE_NATIVE)
18536
- return "native";
18537
- return "unknown";
18538
- }
18539
-
18540
- // ../../src/runtime/setup.ts
18541
- init_logger();
18542
20225
  function hasBinary(name) {
18543
20226
  const checker = process.platform === "win32" ? "where" : "which";
18544
20227
  try {
@@ -18664,7 +20347,7 @@ async function runSetup(options) {
18664
20347
  const skipWalletSetup = process.env.UNBROWSE_SKIP_WALLET_SETUP === "1";
18665
20348
  const lobsterInstalled = hasBinary("lobstercash") || existsSync14(path8.join(os4.homedir(), ".agents", "skills", "lobstercash", "SKILL.md"));
18666
20349
  if (!skipWalletSetup && !walletCheck.configured && lobsterInstalled) {
18667
- console.log("[unbrowse] lobster.cash skill detected but wallet not configured — running wallet setup...");
20350
+ console.log("[unbrowse] Crossmint lobster.cash detected but wallet not configured — running wallet setup...");
18668
20351
  try {
18669
20352
  execFileSync3("npx", ["@crossmint/lobster-cli", "setup"], {
18670
20353
  stdio: "inherit",
@@ -18675,15 +20358,15 @@ async function runSetup(options) {
18675
20358
  console.log(`[unbrowse] wallet configured (${recheck.provider})`);
18676
20359
  }
18677
20360
  } catch {
18678
- console.warn("[unbrowse] lobster.cash wallet setup failed or was skipped — continuing without wallet");
20361
+ console.warn("[unbrowse] Crossmint lobster.cash setup failed or was skipped — continuing without wallet");
18679
20362
  }
18680
20363
  }
18681
20364
  const finalWalletCheck = checkWalletConfigured();
18682
20365
  const wallet = {
18683
20366
  ...finalWalletCheck,
18684
20367
  lobster_installed: lobsterInstalled,
18685
- message: finalWalletCheck.configured ? `Wallet configured (${finalWalletCheck.provider})` : lobsterInstalled ? "lobster.cash installed but wallet not paired. Run: lobstercash setup" : "No wallet configured. Install lobster.cash for paid marketplace skills, or use indexing mode for free.",
18686
- install_hint: finalWalletCheck.configured ? undefined : lobsterInstalled ? "lobstercash setup" : "npx skills add https://github.com/Crossmint/lobstercash-cli-skills --global --yes"
20368
+ message: finalWalletCheck.configured ? `Wallet configured (${finalWalletCheck.provider}). This address is the contributor truth: it is synced onto your agent profile, used for contributor payouts when your routes earn, and used for paid-route spending.` : lobsterInstalled ? "Crossmint lobster.cash is installed but not paired. Pair it now so this wallet address becomes your contributor payout target and your paid-route spending wallet. Run: npx @crossmint/lobster-cli setup" : "No wallet configured. Recommended for new installs: set up Crossmint lobster.cash so contributor payouts have a destination address and paid-route spending can clear automatically. Without it you stay in free indexing mode only.",
20369
+ install_hint: finalWalletCheck.configured ? undefined : lobsterInstalled ? "npx @crossmint/lobster-cli setup" : "npx @crossmint/lobster-cli setup"
18687
20370
  };
18688
20371
  return {
18689
20372
  os: {
@@ -18756,6 +20439,23 @@ function info(msg) {
18756
20439
  process.stderr.write(`[unbrowse] ${msg}
18757
20440
  `);
18758
20441
  }
20442
+ function resolveResultError(result) {
20443
+ return result.result?.error ?? result.error;
20444
+ }
20445
+ function resolveLoginUrl(result, fallbackUrl) {
20446
+ return result.result?.login_url ?? fallbackUrl ?? "";
20447
+ }
20448
+ function hasIndexingFallback(result) {
20449
+ return result.result?.indexing_fallback_available === true;
20450
+ }
20451
+ function isResolveSuccessResult(result) {
20452
+ const resultObj = result.result;
20453
+ if (resolveResultError(result))
20454
+ return false;
20455
+ if (resultObj?.status === "browse_session_open")
20456
+ return false;
20457
+ return !!result.result || Array.isArray(result.available_endpoints);
20458
+ }
18759
20459
  async function withPendingNotice(promise, message, delayMs = 3000) {
18760
20460
  let done = false;
18761
20461
  const timer = setTimeout(() => {
@@ -18794,101 +20494,252 @@ function slimTrace(obj) {
18794
20494
  out.result = obj.result;
18795
20495
  if (obj.available_endpoints)
18796
20496
  out.available_endpoints = obj.available_endpoints;
20497
+ if (obj.impact)
20498
+ out.impact = obj.impact;
20499
+ if (obj.next_actions)
20500
+ out.next_actions = obj.next_actions;
20501
+ if (obj.next_step)
20502
+ out.next_step = obj.next_step;
18797
20503
  if (obj.source)
18798
20504
  out.source = obj.source;
18799
20505
  if (obj.skill)
18800
20506
  out.skill = obj.skill;
18801
20507
  return out;
18802
20508
  }
20509
+ function formatSavedDuration(ms) {
20510
+ if (ms >= 60000)
20511
+ return `${(ms / 60000).toFixed(1)}m`;
20512
+ if (ms >= 1e4)
20513
+ return `${Math.round(ms / 1000)}s`;
20514
+ if (ms >= 1000)
20515
+ return `${(ms / 1000).toFixed(1)}s`;
20516
+ return `${ms}ms`;
20517
+ }
20518
+ function emitImpactSummary(result) {
20519
+ const impact = result.impact;
20520
+ if (!impact)
20521
+ return;
20522
+ const timeSavedMs = typeof impact.time_saved_ms === "number" ? impact.time_saved_ms : 0;
20523
+ const tokensSaved = typeof impact.tokens_saved === "number" ? impact.tokens_saved : 0;
20524
+ const timeSavedPct = typeof impact.time_saved_pct === "number" ? impact.time_saved_pct : 0;
20525
+ const tokensSavedPct = typeof impact.tokens_saved_pct === "number" ? impact.tokens_saved_pct : 0;
20526
+ const browserAvoided = impact.browser_avoided === true;
20527
+ if (timeSavedMs <= 0 && tokensSaved <= 0 && !browserAvoided)
20528
+ return;
20529
+ const parts = [];
20530
+ if (timeSavedMs > 0)
20531
+ parts.push(`${formatSavedDuration(timeSavedMs)} saved (${timeSavedPct}% faster)`);
20532
+ if (tokensSaved > 0)
20533
+ parts.push(`${tokensSaved.toLocaleString("en-US")} tokens saved (${tokensSavedPct}% less context)`);
20534
+ if (browserAvoided)
20535
+ parts.push("browser avoided");
20536
+ info(parts.join(" \u2022 "));
20537
+ }
20538
+ function emitNextActionSummary(result) {
20539
+ const nextActions = Array.isArray(result.next_actions) ? result.next_actions : [];
20540
+ if (nextActions.length === 0)
20541
+ return;
20542
+ info("Likely next actions:");
20543
+ for (const action2 of nextActions.slice(0, 3)) {
20544
+ const command = typeof action2.command === "string" ? action2.command : "";
20545
+ const title = typeof action2.title === "string" ? action2.title : action2.endpoint_id ?? "next step";
20546
+ const why = typeof action2.why === "string" ? action2.why : "";
20547
+ info(` ${command || title}${why ? ` # ${why}` : ""}`);
20548
+ }
20549
+ }
18803
20550
  async function cmdHealth(flags) {
18804
20551
  output(await api3("GET", "/health"), !!flags.pretty);
18805
20552
  }
20553
+ function telemetryDomainFromInput(domain, url) {
20554
+ if (domain?.trim())
20555
+ return domain.trim().replace(/^www\./, "");
20556
+ if (!url?.trim())
20557
+ return null;
20558
+ try {
20559
+ return new URL(url).hostname.replace(/^www\./, "");
20560
+ } catch {
20561
+ return null;
20562
+ }
20563
+ }
18806
20564
  async function cmdResolve(flags) {
18807
20565
  const intent = flags.intent;
18808
20566
  if (!intent)
18809
20567
  die("--intent is required");
18810
- const body = { intent };
18811
- const url = flags.url;
18812
- const domain = flags.domain;
18813
- const explicitEndpointId = flags["endpoint-id"];
18814
- const autoExecute = !!flags.execute;
18815
- const extraParams = flags.params ? JSON.parse(flags.params) : {};
18816
- if (url) {
18817
- body.params = { url };
18818
- body.context = { url };
18819
- }
18820
- if (domain) {
18821
- body.context = { ...body.context ?? {}, domain };
18822
- }
18823
- if (explicitEndpointId) {
18824
- body.params = { ...body.params ?? {}, endpoint_id: explicitEndpointId };
18825
- }
18826
- if (flags.params) {
18827
- body.params = { ...body.params ?? {}, ...extraParams };
18828
- }
18829
- if (flags["dry-run"])
18830
- body.dry_run = true;
18831
- if (flags["force-capture"])
18832
- body.force_capture = true;
18833
- body.projection = { raw: true };
18834
- function execBody(endpointId) {
18835
- return { params: { endpoint_id: endpointId, ...extraParams }, intent, projection: { raw: true } };
18836
- }
18837
- function resolveSkillId() {
18838
- return result.skill?.skill_id ?? result.skill_id;
18839
- }
18840
- const startedAt = Date.now();
18841
- let result = await withPendingNotice(api3("POST", "/v1/intent/resolve", body), "Still working. First-time capture/indexing for a site can take 20-80s. Waiting is usually better than falling back.");
18842
- const resultError = result.result?.error ?? result.error;
18843
- if (resultError === "auth_required") {
18844
- const loginUrl = result.result?.login_url ?? url ?? "";
18845
- if (loginUrl) {
18846
- info("Site requires authentication. Opening browser for login...");
18847
- try {
18848
- await api3("POST", "/v1/auth/login", { url: loginUrl });
18849
- info("Login complete. Retrying...");
18850
- result = await withPendingNotice(api3("POST", "/v1/intent/resolve", body), "Retrying after login...");
18851
- } catch (err) {
18852
- die(`Login failed: ${err.message}. Run: unbrowse login --url "${loginUrl}"`);
20568
+ const hostType = detectTelemetryHostType();
20569
+ await ensureCliInstallTracked(hostType);
20570
+ await recordFunnelTelemetryEvent("cli_invoked", {
20571
+ source: "cli",
20572
+ hostType,
20573
+ properties: { command: "resolve" }
20574
+ });
20575
+ await recordFunnelTelemetryEvent("resolve_started", {
20576
+ source: "cli",
20577
+ hostType,
20578
+ properties: {
20579
+ command: "resolve",
20580
+ intent,
20581
+ domain: telemetryDomainFromInput(flags.domain, flags.url),
20582
+ url: typeof flags.url === "string" ? flags.url : null,
20583
+ has_url: typeof flags.url === "string",
20584
+ has_domain: typeof flags.domain === "string",
20585
+ auto_execute: !!flags.execute
20586
+ }
20587
+ });
20588
+ try {
20589
+ let execBody = function(endpointId) {
20590
+ return { params: { endpoint_id: endpointId, ...extraParams }, intent, projection: { raw: true } };
20591
+ }, resolveSkillId = function() {
20592
+ return result.skill?.skill_id ?? result.skill_id;
20593
+ };
20594
+ const body = { intent };
20595
+ const url = flags.url;
20596
+ const domain = flags.domain;
20597
+ const explicitEndpointId = flags["endpoint-id"];
20598
+ const autoExecute = !!flags.execute;
20599
+ const extraParams = flags.params ? JSON.parse(flags.params) : {};
20600
+ if (url) {
20601
+ body.params = { url };
20602
+ body.context = { url };
20603
+ }
20604
+ if (domain) {
20605
+ body.context = { ...body.context ?? {}, domain };
20606
+ }
20607
+ if (explicitEndpointId) {
20608
+ body.params = { ...body.params ?? {}, endpoint_id: explicitEndpointId };
20609
+ }
20610
+ if (flags.params) {
20611
+ body.params = { ...body.params ?? {}, ...extraParams };
20612
+ }
20613
+ if (flags["dry-run"])
20614
+ body.dry_run = true;
20615
+ if (flags["force-capture"])
20616
+ body.force_capture = true;
20617
+ body.projection = { raw: true };
20618
+ const startedAt = Date.now();
20619
+ async function resolveOnce(message = "Still working. First-time capture/indexing for a site can take 20-80s. Waiting is usually better than falling back.") {
20620
+ return withPendingNotice(api3("POST", "/v1/intent/resolve", body), message);
20621
+ }
20622
+ let result = await resolveOnce();
20623
+ let attemptedForceCapture = !!body.force_capture;
20624
+ let attemptedCookieImport = false;
20625
+ let attemptedInteractiveLogin = false;
20626
+ while (true) {
20627
+ const resultError = resolveResultError(result);
20628
+ if (resultError === "payment_required" && hasIndexingFallback(result) && url && !attemptedForceCapture) {
20629
+ attemptedForceCapture = true;
20630
+ body.force_capture = true;
20631
+ info("Marketplace search is paid here. Falling back to free live capture on the exact URL...");
20632
+ result = await resolveOnce("Running free live capture...");
20633
+ continue;
20634
+ }
20635
+ if (resultError === "auth_required") {
20636
+ const loginUrl = resolveLoginUrl(result, url);
20637
+ if (!loginUrl)
20638
+ break;
20639
+ if (!attemptedCookieImport) {
20640
+ attemptedCookieImport = true;
20641
+ info("Site requires authentication. Trying browser cookie import first...");
20642
+ const stealResult = await api3("POST", "/v1/auth/steal", { url: loginUrl });
20643
+ const cookiesStored = typeof stealResult.cookies_stored === "number" ? stealResult.cookies_stored : Number(stealResult.cookies_stored ?? 0);
20644
+ if (stealResult.success === true && cookiesStored > 0) {
20645
+ info(`Imported ${cookiesStored} browser cookies. Retrying...`);
20646
+ result = await resolveOnce("Retrying after browser cookie import...");
20647
+ continue;
20648
+ }
20649
+ }
20650
+ if (!attemptedInteractiveLogin) {
20651
+ attemptedInteractiveLogin = true;
20652
+ info("Site requires authentication. Opening browser for login...");
20653
+ const loginResult = await api3("POST", "/v1/auth/login", { url: loginUrl });
20654
+ if (loginResult.error || loginResult.success === false) {
20655
+ const message = typeof loginResult.error === "string" ? loginResult.error : "interactive login did not produce a reusable session";
20656
+ throw new Error(`Login failed: ${message}. Run: unbrowse login --url "${loginUrl}"`);
20657
+ }
20658
+ info("Login complete. Retrying...");
20659
+ result = await resolveOnce("Retrying after login...");
20660
+ continue;
20661
+ }
18853
20662
  }
20663
+ break;
18854
20664
  }
18855
- }
18856
- if (explicitEndpointId && result.available_endpoints) {
18857
- const skillId = resolveSkillId();
18858
- if (skillId) {
18859
- result = await withPendingNotice(api3("POST", `/v1/skills/${skillId}/execute`, execBody(explicitEndpointId)), "Executing selected endpoint...");
20665
+ if (explicitEndpointId && result.available_endpoints) {
20666
+ const skillId = resolveSkillId();
20667
+ if (skillId) {
20668
+ result = await withPendingNotice(api3("POST", `/v1/skills/${skillId}/execute`, execBody(explicitEndpointId)), "Executing selected endpoint...");
20669
+ }
18860
20670
  }
18861
- }
18862
- if (autoExecute && result.available_endpoints && !result.result) {
18863
- const endpoints = result.available_endpoints;
18864
- const skillId = resolveSkillId();
18865
- if (skillId && endpoints.length > 0) {
18866
- const bestEndpoint = endpoints[0];
18867
- info(`Auto-executing endpoint: ${bestEndpoint.description ?? bestEndpoint.endpoint_id}`);
18868
- result = await withPendingNotice(api3("POST", `/v1/skills/${skillId}/execute`, execBody(bestEndpoint.endpoint_id)), "Executing best endpoint...");
20671
+ if (autoExecute && result.available_endpoints && !result.result) {
20672
+ const endpoints = result.available_endpoints;
20673
+ const skillId = resolveSkillId();
20674
+ if (skillId && endpoints.length > 0) {
20675
+ const bestEndpoint = endpoints[0];
20676
+ info(`Auto-executing endpoint: ${bestEndpoint.description ?? bestEndpoint.endpoint_id}`);
20677
+ result = await withPendingNotice(api3("POST", `/v1/skills/${skillId}/execute`, execBody(bestEndpoint.endpoint_id)), "Executing best endpoint...");
20678
+ }
18869
20679
  }
20680
+ const resultObj = result.result;
20681
+ if (resultObj?.status === "browse_session_open") {
20682
+ info(`No cached API. Browser session open on ${resultObj.domain ?? resultObj.url}.`);
20683
+ info(`Preferred flow: snap -> click/fill/eval -> submit -> sync -> close.`);
20684
+ info(`Use these commands to get your data:`);
20685
+ const commands = resultObj.commands ?? [
20686
+ "unbrowse snap --filter interactive",
20687
+ "unbrowse click <ref>",
20688
+ "unbrowse fill <ref> <value>",
20689
+ 'unbrowse submit --wait-for "/next-step"',
20690
+ "unbrowse sync",
20691
+ "unbrowse close"
20692
+ ];
20693
+ for (const cmd of commands)
20694
+ info(` ${cmd}`);
20695
+ info(`For JS-heavy forms: prefer real date/time clicks first, inspect hidden inputs with eval when needed, then submit.`);
20696
+ info(`All traffic is being passively captured. Run "unbrowse close" when done.`);
20697
+ output(slimTrace(result), !!flags.pretty);
20698
+ return;
20699
+ }
20700
+ if (Date.now() - startedAt > 3000 && result.source === "live-capture") {
20701
+ info("Live capture finished. Future runs against this site should be much faster.");
20702
+ }
20703
+ if (isResolveSuccessResult(result)) {
20704
+ await recordFunnelTelemetryEvent("resolve_completed", {
20705
+ source: "cli",
20706
+ hostType,
20707
+ properties: {
20708
+ command: "resolve",
20709
+ intent,
20710
+ domain: telemetryDomainFromInput(domain, url),
20711
+ url: url ?? null,
20712
+ source: result.source,
20713
+ auto_execute: autoExecute,
20714
+ explicit_endpoint: explicitEndpointId ?? null
20715
+ }
20716
+ });
20717
+ }
20718
+ result = slimTrace(result);
20719
+ emitImpactSummary(result);
20720
+ emitNextActionSummary(result);
20721
+ const skill = result.skill;
20722
+ const trace = result.trace;
20723
+ if (skill?.skill_id && trace) {
20724
+ result._feedback = `unbrowse feedback --skill ${skill.skill_id} --endpoint ${trace.endpoint_id || "?"} --rating <1-5>`;
20725
+ }
20726
+ output(result, !!flags.pretty);
20727
+ } catch (error) {
20728
+ const message = error instanceof Error ? error.message : String(error);
20729
+ await recordFunnelTelemetryEvent("resolve_failed", {
20730
+ source: "cli",
20731
+ hostType,
20732
+ properties: {
20733
+ command: "resolve",
20734
+ intent,
20735
+ domain: telemetryDomainFromInput(flags.domain, flags.url),
20736
+ url: typeof flags.url === "string" ? flags.url : null,
20737
+ failure_stage: "resolve",
20738
+ failure_reason: message
20739
+ }
20740
+ });
20741
+ throw error;
18870
20742
  }
18871
- const resultObj = result.result;
18872
- if (resultObj?.status === "browse_session_open") {
18873
- info(`No cached API. Browser session open on ${resultObj.domain ?? resultObj.url}.`);
18874
- info(`Use these commands to get your data:`);
18875
- const commands = resultObj.commands ?? ["unbrowse snap --filter interactive", "unbrowse click <ref>", "unbrowse close"];
18876
- for (const cmd of commands)
18877
- info(` ${cmd}`);
18878
- info(`All traffic is being passively captured. Run "unbrowse close" when done.`);
18879
- output(slimTrace(result), !!flags.pretty);
18880
- return;
18881
- }
18882
- if (Date.now() - startedAt > 3000 && result.source === "live-capture") {
18883
- info("Live capture finished. Future runs against this site should be much faster.");
18884
- }
18885
- result = slimTrace(result);
18886
- const skill = result.skill;
18887
- const trace = result.trace;
18888
- if (skill?.skill_id && trace) {
18889
- result._feedback = `unbrowse feedback --skill ${skill.skill_id} --endpoint ${trace.endpoint_id || "?"} --rating <1-5>`;
18890
- }
18891
- output(result, !!flags.pretty);
18892
20743
  }
18893
20744
  function drillPath(data, path9) {
18894
20745
  const segments = path9.split(/\./).flatMap((s) => {
@@ -18969,65 +20820,131 @@ async function cmdExecute(flags) {
18969
20820
  const skillId = flags.skill;
18970
20821
  if (!skillId)
18971
20822
  die("--skill is required");
18972
- const body = { params: {} };
18973
- if (flags.endpoint) {
18974
- body.params.endpoint_id = flags.endpoint;
18975
- }
18976
- if (flags.params) {
18977
- body.params = { ...body.params, ...JSON.parse(flags.params) };
18978
- }
18979
- if (flags.url) {
18980
- body.context_url = flags.url;
18981
- body.params.url = flags.url;
18982
- }
18983
- if (flags.intent)
18984
- body.intent = flags.intent;
18985
- if (flags["dry-run"])
18986
- body.dry_run = true;
18987
- if (flags["confirm-unsafe"])
18988
- body.confirm_unsafe = true;
18989
- body.projection = { raw: true };
18990
- let result = await withPendingNotice(api3("POST", `/v1/skills/${skillId}/execute`, body), "Still working. This endpoint may require browser replay or first-time auth/capture setup.");
18991
- result = slimTrace(result);
18992
- const pathFlag = flags.path;
18993
- const extractFlag = flags.extract;
18994
- const limitFlag = flags.limit ? Number(flags.limit) : undefined;
18995
- const schemaFlag = !!flags.schema;
18996
- const rawFlag = !!flags.raw;
18997
- if (schemaFlag && !rawFlag) {
18998
- const data = result.result;
18999
- output({ trace: result.trace, schema: schemaOf(data) }, !!flags.pretty);
19000
- return;
19001
- }
19002
- if (!rawFlag && (pathFlag || extractFlag || limitFlag)) {
19003
- let data = pathFlag ? drillPath(result.result, pathFlag) : result.result;
19004
- const items = Array.isArray(data) ? data : data != null ? [data] : [];
19005
- const extracted = extractFlag ? applyExtract(items, extractFlag) : items;
19006
- const limited = limitFlag ? extracted.slice(0, limitFlag) : extracted;
19007
- const trace = result.trace;
19008
- const out = { trace: result.trace, data: limited, count: limited.length };
19009
- if (trace?.skill_id && trace?.endpoint_id && limited.length > 0) {
19010
- out._review_hint = `After presenting results, improve this endpoint's description: unbrowse review --skill ${trace.skill_id} --endpoints '[{"endpoint_id":"${trace.endpoint_id}","description":"DESCRIBE WHAT THIS RETURNS","action_kind":"ACTION","resource_kind":"RESOURCE"}]'`;
20823
+ const hostType = detectTelemetryHostType();
20824
+ await ensureCliInstallTracked(hostType);
20825
+ await recordFunnelTelemetryEvent("cli_invoked", {
20826
+ source: "cli",
20827
+ hostType,
20828
+ properties: { command: "execute" }
20829
+ });
20830
+ await recordFunnelTelemetryEvent("resolve_started", {
20831
+ source: "cli",
20832
+ hostType,
20833
+ properties: {
20834
+ command: "execute",
20835
+ intent: typeof flags.intent === "string" ? flags.intent : null,
20836
+ domain: telemetryDomainFromInput(undefined, flags.url),
20837
+ url: typeof flags.url === "string" ? flags.url : null,
20838
+ skill_id: skillId,
20839
+ endpoint_id: typeof flags.endpoint === "string" ? flags.endpoint : null
19011
20840
  }
19012
- output(out, !!flags.pretty);
19013
- return;
19014
- }
19015
- if (!rawFlag && !pathFlag && !extractFlag && !schemaFlag) {
19016
- const raw = JSON.stringify(result.result);
19017
- if (raw && raw.length > 2048) {
19018
- const schema = schemaOf(result.result);
20841
+ });
20842
+ try {
20843
+ const body = { params: {} };
20844
+ if (flags.endpoint) {
20845
+ body.params.endpoint_id = flags.endpoint;
20846
+ }
20847
+ if (flags.params) {
20848
+ body.params = { ...body.params, ...JSON.parse(flags.params) };
20849
+ }
20850
+ if (flags.url) {
20851
+ body.context_url = flags.url;
20852
+ body.params.url = flags.url;
20853
+ }
20854
+ if (flags.intent)
20855
+ body.intent = flags.intent;
20856
+ if (flags["dry-run"])
20857
+ body.dry_run = true;
20858
+ if (flags["confirm-unsafe"])
20859
+ body.confirm_unsafe = true;
20860
+ body.projection = { raw: true };
20861
+ let result = await withPendingNotice(api3("POST", `/v1/skills/${skillId}/execute`, body), "Still working. This endpoint may require browser replay or first-time auth/capture setup.");
20862
+ if (isResolveSuccessResult(result)) {
20863
+ await recordFunnelTelemetryEvent("resolve_completed", {
20864
+ source: "cli",
20865
+ hostType,
20866
+ properties: {
20867
+ command: "execute",
20868
+ intent: typeof flags.intent === "string" ? flags.intent : null,
20869
+ domain: telemetryDomainFromInput(undefined, flags.url),
20870
+ url: typeof flags.url === "string" ? flags.url : null,
20871
+ skill_id: skillId,
20872
+ endpoint_id: typeof flags.endpoint === "string" ? flags.endpoint : null
20873
+ }
20874
+ });
20875
+ }
20876
+ result = slimTrace(result);
20877
+ emitImpactSummary(result);
20878
+ emitNextActionSummary(result);
20879
+ const pathFlag = flags.path;
20880
+ const extractFlag = flags.extract;
20881
+ const limitFlag = flags.limit ? Number(flags.limit) : undefined;
20882
+ const schemaFlag = !!flags.schema;
20883
+ const rawFlag = !!flags.raw;
20884
+ if (schemaFlag && !rawFlag) {
20885
+ const data = result.result;
19019
20886
  output({
19020
20887
  trace: result.trace,
19021
- extraction_hints: {
19022
- message: "Response is large. Use --path/--extract/--limit to filter, or --schema to see structure, or --raw for full response.",
19023
- schema_tree: schema,
19024
- response_bytes: raw.length
19025
- }
20888
+ schema: schemaOf(data),
20889
+ ...result.impact ? { impact: result.impact } : {},
20890
+ ...result.next_actions ? { next_actions: result.next_actions } : {}
19026
20891
  }, !!flags.pretty);
19027
20892
  return;
19028
20893
  }
20894
+ if (!rawFlag && (pathFlag || extractFlag || limitFlag)) {
20895
+ const data = pathFlag ? drillPath(result.result, pathFlag) : result.result;
20896
+ const items = Array.isArray(data) ? data : data != null ? [data] : [];
20897
+ const extracted = extractFlag ? applyExtract(items, extractFlag) : items;
20898
+ const limited = limitFlag ? extracted.slice(0, limitFlag) : extracted;
20899
+ const trace = result.trace;
20900
+ const out = {
20901
+ trace: result.trace,
20902
+ data: limited,
20903
+ count: limited.length,
20904
+ ...result.impact ? { impact: result.impact } : {},
20905
+ ...result.next_actions ? { next_actions: result.next_actions } : {}
20906
+ };
20907
+ if (trace?.skill_id && trace?.endpoint_id && limited.length > 0) {
20908
+ out._review_hint = `After presenting results, improve this endpoint's description: unbrowse review --skill ${trace.skill_id} --endpoints '[{"endpoint_id":"${trace.endpoint_id}","description":"DESCRIBE WHAT THIS RETURNS","action_kind":"ACTION","resource_kind":"RESOURCE"}]'`;
20909
+ }
20910
+ output(out, !!flags.pretty);
20911
+ return;
20912
+ }
20913
+ if (!rawFlag && !pathFlag && !extractFlag && !schemaFlag) {
20914
+ const raw = JSON.stringify(result.result);
20915
+ if (raw && raw.length > 2048) {
20916
+ const schema = schemaOf(result.result);
20917
+ output({
20918
+ trace: result.trace,
20919
+ ...result.impact ? { impact: result.impact } : {},
20920
+ ...result.next_actions ? { next_actions: result.next_actions } : {},
20921
+ extraction_hints: {
20922
+ message: "Response is large. Use --path/--extract/--limit to filter, or --schema to see structure, or --raw for full response.",
20923
+ schema_tree: schema,
20924
+ response_bytes: raw.length
20925
+ }
20926
+ }, !!flags.pretty);
20927
+ return;
20928
+ }
20929
+ }
20930
+ output(result, !!flags.pretty);
20931
+ } catch (error) {
20932
+ const message = error instanceof Error ? error.message : String(error);
20933
+ await recordFunnelTelemetryEvent("resolve_failed", {
20934
+ source: "cli",
20935
+ hostType,
20936
+ properties: {
20937
+ command: "execute",
20938
+ intent: typeof flags.intent === "string" ? flags.intent : null,
20939
+ domain: telemetryDomainFromInput(undefined, flags.url),
20940
+ url: typeof flags.url === "string" ? flags.url : null,
20941
+ skill_id: skillId,
20942
+ failure_stage: "execute",
20943
+ failure_reason: message
20944
+ }
20945
+ });
20946
+ throw error;
19029
20947
  }
19030
- output(result, !!flags.pretty);
19031
20948
  }
19032
20949
  async function cmdFeedback(flags) {
19033
20950
  const skillId = flags.skill;
@@ -19096,7 +21013,53 @@ async function cmdSearch(flags) {
19096
21013
  const body = { intent, k: Number(flags.k) || 5 };
19097
21014
  if (domain)
19098
21015
  body.domain = domain;
19099
- output(await api3("POST", path9, body), !!flags.pretty);
21016
+ const hostType = detectTelemetryHostType();
21017
+ await ensureCliInstallTracked(hostType);
21018
+ await recordFunnelTelemetryEvent("cli_invoked", {
21019
+ source: "cli",
21020
+ hostType,
21021
+ properties: { command: "search" }
21022
+ });
21023
+ await recordFunnelTelemetryEvent("search_started", {
21024
+ source: "cli",
21025
+ hostType,
21026
+ properties: {
21027
+ command: "search",
21028
+ intent,
21029
+ domain: domain ?? null,
21030
+ k: body.k
21031
+ }
21032
+ });
21033
+ try {
21034
+ const result = await api3("POST", path9, body);
21035
+ const results = Array.isArray(result.results) ? result.results : [];
21036
+ await recordFunnelTelemetryEvent("search_completed", {
21037
+ source: "cli",
21038
+ hostType,
21039
+ properties: {
21040
+ command: "search",
21041
+ intent,
21042
+ domain: domain ?? null,
21043
+ k: body.k,
21044
+ result_count: results.length
21045
+ }
21046
+ });
21047
+ output(result, !!flags.pretty);
21048
+ } catch (error) {
21049
+ const message = error instanceof Error ? error.message : String(error);
21050
+ await recordFunnelTelemetryEvent("search_failed", {
21051
+ source: "cli",
21052
+ hostType,
21053
+ properties: {
21054
+ command: "search",
21055
+ intent,
21056
+ domain: domain ?? null,
21057
+ failure_stage: "search",
21058
+ failure_reason: message
21059
+ }
21060
+ });
21061
+ throw error;
21062
+ }
19100
21063
  }
19101
21064
  async function cmdSessions(flags) {
19102
21065
  const domain = flags.domain;
@@ -19106,6 +21069,13 @@ async function cmdSessions(flags) {
19106
21069
  output(await api3("GET", `/v1/sessions/${domain}?limit=${limit}`), !!flags.pretty);
19107
21070
  }
19108
21071
  async function cmdSetup(flags) {
21072
+ const hostType = detectTelemetryHostType();
21073
+ await ensureCliInstallTracked(hostType);
21074
+ await recordFunnelTelemetryEvent("cli_invoked", {
21075
+ source: "setup",
21076
+ hostType,
21077
+ properties: { command: "setup" }
21078
+ });
19109
21079
  info("Running setup checks");
19110
21080
  const report = await runSetup({
19111
21081
  cwd: process.cwd(),
@@ -19120,6 +21090,25 @@ async function cmdSetup(flags) {
19120
21090
  if (report.opencode.action === "installed" || report.opencode.action === "updated") {
19121
21091
  info(`Open Code command installed at ${report.opencode.command_file}`);
19122
21092
  }
21093
+ await recordInstallTelemetryEvent("setup", {
21094
+ hostType,
21095
+ status: report.browser_engine.action === "failed" ? "failed" : "installed",
21096
+ properties: {
21097
+ browser_engine_action: report.browser_engine.action,
21098
+ opencode_action: report.opencode.action,
21099
+ no_start: !!flags["no-start"],
21100
+ skip_browser: !!flags["skip-browser"]
21101
+ }
21102
+ });
21103
+ await recordFunnelTelemetryEvent("setup_completed", {
21104
+ source: "setup",
21105
+ hostType,
21106
+ properties: {
21107
+ browser_engine_action: report.browser_engine.action,
21108
+ opencode_action: report.opencode.action,
21109
+ no_start: !!flags["no-start"]
21110
+ }
21111
+ });
19123
21112
  if (flags["no-start"]) {
19124
21113
  report.server = { started: false, skipped: true, base_url: BASE_URL };
19125
21114
  output(report, true);
@@ -19133,8 +21122,23 @@ async function cmdSetup(flags) {
19133
21122
  try {
19134
21123
  await ensureLocalServer(BASE_URL, false, import.meta.url);
19135
21124
  report.server = { started: true, base_url: BASE_URL };
21125
+ await recordFunnelTelemetryEvent("server_autostart_succeeded", {
21126
+ source: "setup",
21127
+ hostType,
21128
+ properties: {
21129
+ base_url: BASE_URL
21130
+ }
21131
+ });
19136
21132
  } catch (error) {
19137
21133
  const message = error instanceof Error ? error.message : String(error);
21134
+ await recordFunnelTelemetryEvent("server_autostart_failed", {
21135
+ source: "setup",
21136
+ hostType,
21137
+ properties: {
21138
+ failure_stage: "server_autostart",
21139
+ failure_reason: message
21140
+ }
21141
+ });
19138
21142
  report.server = { started: false, error: message, base_url: BASE_URL };
19139
21143
  output(report, true);
19140
21144
  process.exit(1);
@@ -19146,6 +21150,7 @@ async function cmdSetup(flags) {
19146
21150
  var CLI_REFERENCE = {
19147
21151
  commands: [
19148
21152
  { name: "health", usage: "", desc: "Server health check" },
21153
+ { name: "mcp", usage: "[--no-auto-start]", desc: "Run the stdio MCP server" },
19149
21154
  { name: "setup", usage: "[--opencode auto|global|project|off] [--no-start]", desc: "Bootstrap browser deps + Open Code command" },
19150
21155
  { name: "resolve", usage: '--intent "..." --url "..." [opts]', desc: "Resolve intent \u2192 search/capture/execute" },
19151
21156
  { name: "execute", usage: "--skill ID --endpoint ID [opts]", desc: "Execute a specific endpoint" },
@@ -19157,7 +21162,8 @@ var CLI_REFERENCE = {
19157
21162
  { name: "skill", usage: "<id>", desc: "Get skill details" },
19158
21163
  { name: "search", usage: '--intent "..." [--domain "..."]', desc: "Search marketplace" },
19159
21164
  { name: "sessions", usage: '--domain "..." [--limit N]', desc: "Debug session logs" },
19160
- { name: "go", usage: "<url>", desc: "Navigate browser to URL (passive indexing)" },
21165
+ { name: "go", usage: "<url>", desc: "Open a live Kuri browser tab for capture-first workflows" },
21166
+ { name: "submit", usage: "[--form-selector sel] [--submit-selector sel] [--wait-for hint]", desc: "Submit current form, auto-flush current capture, and fall back to same-origin rehydrate for JS-heavy flows" },
19161
21167
  { name: "snap", usage: "[--filter interactive]", desc: "A11y snapshot with @eN refs" },
19162
21168
  { name: "click", usage: "<ref>", desc: "Click element by ref (e.g. e5)" },
19163
21169
  { name: "fill", usage: "<ref> <value>", desc: "Fill input by ref" },
@@ -19172,6 +21178,7 @@ var CLI_REFERENCE = {
19172
21178
  { name: "eval", usage: "<expression>", desc: "Evaluate JavaScript" },
19173
21179
  { name: "back", usage: "", desc: "Navigate back" },
19174
21180
  { name: "forward", usage: "", desc: "Navigate forward" },
21181
+ { name: "sync", usage: "", desc: "Flush the current step's captured traffic into route cache without closing tab" },
19175
21182
  { name: "close", usage: "", desc: "Close browse session, flush + index traffic" }
19176
21183
  ],
19177
21184
  globalFlags: [
@@ -19193,8 +21200,13 @@ var CLI_REFERENCE = {
19193
21200
  ],
19194
21201
  examples: [
19195
21202
  "unbrowse setup",
21203
+ "unbrowse mcp",
19196
21204
  'unbrowse resolve --intent "top stories" --url "https://news.ycombinator.com" --execute',
19197
21205
  'unbrowse resolve --intent "get timeline" --url "https://x.com"',
21206
+ 'unbrowse go "https://www.mandai.com/en/ticketing/admission-and-rides/parks-selection.html"',
21207
+ "unbrowse snap --filter interactive",
21208
+ 'unbrowse submit --wait-for "/time-selection.html"',
21209
+ "unbrowse sync",
19198
21210
  "unbrowse execute --skill abc --endpoint def --pretty",
19199
21211
  "unbrowse execute --skill abc --endpoint def --schema --pretty",
19200
21212
  'unbrowse execute --skill abc --endpoint def --path "data.items[]" --extract "name,url" --limit 10 --pretty',
@@ -19227,6 +21239,8 @@ function printHelp() {
19227
21239
  for (const e of r.examples) {
19228
21240
  lines.push(` ${e}`);
19229
21241
  }
21242
+ lines.push("", "Browser workflow:", " 1. go -> open the live tab you want to work in", " 2. snap -> inspect refs and confirm the page state", " 3. click/fill/eval -> set real page state", " 4. submit -> prefer DOM submit; auto-flush current capture; fall back to same-origin rehydrate", " 5. sync -> flush any additional captured routes after a successful step", " 6. close -> finish capture + indexing");
21243
+ lines.push("", "JS-heavy forms:", " Prefer real calendar/time clicks before submit.", " If the UI is flaky, inspect hidden inputs/cookies with eval, then submit the real form.");
19230
21244
  lines.push("");
19231
21245
  process.stderr.write(lines.join(`
19232
21246
  `));
@@ -19271,6 +21285,29 @@ async function cmdUpgrade(flags) {
19271
21285
  info(`Could not check for updates: ${err.message}`);
19272
21286
  }
19273
21287
  }
21288
+ async function cmdMcp(flags) {
21289
+ const entrypoint = resolveSiblingEntrypoint2(import.meta.url, "mcp");
21290
+ const child = spawn3(process.execPath, [...runtimeArgsForEntrypoint2(import.meta.url, entrypoint), ...flags["no-auto-start"] ? ["--no-auto-start"] : []], {
21291
+ cwd: process.cwd(),
21292
+ stdio: "inherit",
21293
+ env: {
21294
+ ...process.env,
21295
+ MCP_SERVER_MODE: "1"
21296
+ }
21297
+ });
21298
+ const code = await new Promise((resolve, reject) => {
21299
+ child.once("error", reject);
21300
+ child.once("exit", (exitCode, signal) => {
21301
+ if (signal) {
21302
+ process.kill(process.pid, signal);
21303
+ return;
21304
+ }
21305
+ resolve(exitCode ?? 1);
21306
+ });
21307
+ });
21308
+ if (code !== 0)
21309
+ process.exit(code);
21310
+ }
19274
21311
  async function cmdSiteHelp(pack, flags) {
19275
21312
  if (flags.deps) {
19276
21313
  const graph = buildDepsGraph(pack);
@@ -19383,6 +21420,21 @@ async function cmdGo(args, flags) {
19383
21420
  die("Usage: unbrowse go <url>");
19384
21421
  output(await api3("POST", "/v1/browse/go", { url }), !!flags.pretty);
19385
21422
  }
21423
+ async function cmdSubmit(flags) {
21424
+ const body = {};
21425
+ if (typeof flags["form-selector"] === "string")
21426
+ body.form_selector = flags["form-selector"];
21427
+ if (typeof flags["submit-selector"] === "string")
21428
+ body.submit_selector = flags["submit-selector"];
21429
+ if (typeof flags["wait-for"] === "string")
21430
+ body.wait_for = flags["wait-for"];
21431
+ if (typeof flags["timeout-ms"] === "string")
21432
+ body.timeout_ms = Number(flags["timeout-ms"]);
21433
+ if (flags["same-origin-fetch-fallback"] !== undefined) {
21434
+ body.same_origin_fetch_fallback = flags["same-origin-fetch-fallback"] !== "false";
21435
+ }
21436
+ output(await api3("POST", "/v1/browse/submit", body), !!flags.pretty);
21437
+ }
19386
21438
  async function cmdSnap(flags) {
19387
21439
  const filter = flags.filter;
19388
21440
  const result = await api3("POST", "/v1/browse/snap", { filter });
@@ -19462,6 +21514,9 @@ async function cmdBack() {
19462
21514
  async function cmdForward() {
19463
21515
  output(await api3("POST", "/v1/browse/forward"), false);
19464
21516
  }
21517
+ async function cmdSync(flags) {
21518
+ output(await api3("POST", "/v1/browse/sync"), !!flags.pretty);
21519
+ }
19465
21520
  async function cmdClose() {
19466
21521
  output(await api3("POST", "/v1/browse/close"), false);
19467
21522
  }
@@ -19524,6 +21579,8 @@ async function main() {
19524
21579
  await cmdSetup(flags);
19525
21580
  return;
19526
21581
  }
21582
+ if (command === "mcp")
21583
+ return cmdMcp(flags);
19527
21584
  if (command === "status")
19528
21585
  return cmdStatus(flags);
19529
21586
  if (command === "stop") {
@@ -19538,6 +21595,7 @@ async function main() {
19538
21595
  return cmdConnectChrome();
19539
21596
  const KNOWN_COMMANDS = new Set([
19540
21597
  "health",
21598
+ "mcp",
19541
21599
  "setup",
19542
21600
  "resolve",
19543
21601
  "execute",
@@ -19557,6 +21615,7 @@ async function main() {
19557
21615
  "upgrade",
19558
21616
  "update",
19559
21617
  "go",
21618
+ "submit",
19560
21619
  "snap",
19561
21620
  "click",
19562
21621
  "fill",
@@ -19571,6 +21630,7 @@ async function main() {
19571
21630
  "eval",
19572
21631
  "back",
19573
21632
  "forward",
21633
+ "sync",
19574
21634
  "close",
19575
21635
  "connect-chrome"
19576
21636
  ]);
@@ -19596,6 +21656,8 @@ async function main() {
19596
21656
  switch (command) {
19597
21657
  case "health":
19598
21658
  return cmdHealth(flags);
21659
+ case "mcp":
21660
+ return cmdMcp(flags);
19599
21661
  case "setup":
19600
21662
  return cmdSetup(flags);
19601
21663
  case "resolve":
@@ -19622,6 +21684,8 @@ async function main() {
19622
21684
  return cmdSessions(flags);
19623
21685
  case "go":
19624
21686
  return cmdGo(args, flags);
21687
+ case "submit":
21688
+ return cmdSubmit(flags);
19625
21689
  case "snap":
19626
21690
  return cmdSnap(flags);
19627
21691
  case "click":
@@ -19650,6 +21714,8 @@ async function main() {
19650
21714
  return cmdBack();
19651
21715
  case "forward":
19652
21716
  return cmdForward();
21717
+ case "sync":
21718
+ return cmdSync(flags);
19653
21719
  case "close":
19654
21720
  return cmdClose();
19655
21721
  case "connect-chrome":