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/README.md +97 -10
- package/bin/unbrowse-wrapper.mjs +14 -30
- package/dist/cli.js +2832 -766
- package/dist/mcp.js +20392 -0
- package/package.json +5 -2
- package/runtime-src/agent-outcome.ts +166 -0
- package/runtime-src/analytics-session.ts +27 -5
- package/runtime-src/api/browse-index.ts +254 -0
- package/runtime-src/api/browse-session.ts +177 -0
- package/runtime-src/api/browse-submit.ts +462 -0
- package/runtime-src/api/routes.ts +348 -281
- package/runtime-src/auth/index.ts +68 -23
- package/runtime-src/cli.ts +574 -165
- package/runtime-src/client/index.ts +171 -18
- package/runtime-src/execution/index.ts +60 -10
- package/runtime-src/execution/robots.ts +167 -0
- package/runtime-src/kuri/client.ts +281 -89
- package/runtime-src/mcp.ts +1065 -0
- package/runtime-src/orchestrator/index.ts +66 -38
- package/runtime-src/orchestrator/timing-economics.ts +80 -0
- package/runtime-src/reverse-engineer/index.ts +33 -3
- package/runtime-src/runtime/setup.ts +8 -8
- package/runtime-src/single-binary.ts +20 -2
- package/runtime-src/types/skill.ts +2 -0
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
471
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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
|
|
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
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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) && !
|
|
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
|
|
11712
|
+
const wallet = getLocalWalletContext2();
|
|
11071
11713
|
const gate = await checkPaymentRequirement(skill.skill_id, endpoint.endpoint_id, {
|
|
11072
|
-
wallet_configured: !!
|
|
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
|
|
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
|
-
...
|
|
11268
|
-
...
|
|
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()]
|
|
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:
|
|
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/
|
|
13149
|
-
function
|
|
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
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
14432
|
-
|
|
14433
|
-
|
|
14434
|
-
|
|
14435
|
-
timing.
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
timing.
|
|
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 =
|
|
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}%${
|
|
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/
|
|
16561
|
-
function
|
|
16562
|
-
|
|
16563
|
-
|
|
16564
|
-
|
|
16565
|
-
|
|
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
|
-
|
|
16568
|
-
|
|
16569
|
-
|
|
16570
|
-
|
|
16571
|
-
|
|
16572
|
-
|
|
16573
|
-
|
|
16574
|
-
|
|
16575
|
-
|
|
16576
|
-
|
|
16577
|
-
|
|
16578
|
-
|
|
16579
|
-
|
|
16580
|
-
|
|
16581
|
-
|
|
16582
|
-
|
|
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/
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
16590
|
-
|
|
16591
|
-
|
|
16592
|
-
|
|
16593
|
-
|
|
16594
|
-
|
|
16595
|
-
|
|
16596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16622
|
-
return "failed";
|
|
17447
|
+
return "";
|
|
16623
17448
|
}
|
|
16624
17449
|
}
|
|
16625
|
-
async function
|
|
16626
|
-
|
|
16627
|
-
|
|
16628
|
-
|
|
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
|
|
16633
|
-
|
|
16634
|
-
|
|
16635
|
-
|
|
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
|
|
16638
|
-
|
|
16639
|
-
|
|
16640
|
-
|
|
16641
|
-
|
|
16642
|
-
|
|
16643
|
-
|
|
16644
|
-
|
|
16645
|
-
|
|
16646
|
-
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16650
|
-
|
|
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
|
-
}
|
|
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
|
|
16657
|
-
var
|
|
16658
|
-
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
|
|
16663
|
-
|
|
16664
|
-
|
|
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/
|
|
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 {
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
|
|
16683
|
-
|
|
16684
|
-
|
|
16685
|
-
|
|
16686
|
-
|
|
16687
|
-
|
|
16688
|
-
|
|
16689
|
-
|
|
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((
|
|
16696
|
-
url:
|
|
16697
|
-
method:
|
|
16698
|
-
request_headers: Object.fromEntries((
|
|
16699
|
-
request_body:
|
|
16700
|
-
response_status:
|
|
16701
|
-
response_headers: Object.fromEntries((
|
|
16702
|
-
response_body:
|
|
16703
|
-
timestamp:
|
|
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
|
|
16707
|
-
|
|
16708
|
-
|
|
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(
|
|
17611
|
+
domain = new URL(sessionUrl).hostname;
|
|
16712
17612
|
} catch {
|
|
16713
|
-
|
|
16714
|
-
}
|
|
16715
|
-
const
|
|
16716
|
-
(
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
|
|
16726
|
-
|
|
16727
|
-
|
|
16728
|
-
|
|
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
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
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 ??
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
|
|
17287
|
-
|
|
17288
|
-
|
|
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
|
-
|
|
17296
|
-
|
|
17297
|
-
|
|
17298
|
-
|
|
17299
|
-
|
|
17300
|
-
|
|
17301
|
-
|
|
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
|
-
|
|
18908
|
+
harEntries = entries;
|
|
17305
18909
|
} catch {}
|
|
17306
|
-
session.harActive = false;
|
|
17307
18910
|
}
|
|
17308
|
-
|
|
17309
|
-
|
|
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
|
-
|
|
17312
|
-
|
|
17313
|
-
|
|
17314
|
-
|
|
17315
|
-
|
|
17316
|
-
|
|
17317
|
-
|
|
17318
|
-
|
|
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
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
17330
|
-
|
|
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
|
-
|
|
17333
|
-
|
|
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
|
|
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
|
-
|
|
17346
|
-
|
|
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
|
-
|
|
17354
|
-
|
|
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
|
-
|
|
17362
|
-
|
|
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
|
-
|
|
17370
|
-
|
|
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
|
-
|
|
17378
|
-
|
|
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
|
-
|
|
17384
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
17412
|
-
|
|
17413
|
-
|
|
17414
|
-
|
|
17415
|
-
|
|
17416
|
-
|
|
17417
|
-
|
|
17418
|
-
|
|
17419
|
-
|
|
17420
|
-
|
|
17421
|
-
|
|
17422
|
-
await
|
|
17423
|
-
|
|
17424
|
-
|
|
17425
|
-
|
|
17426
|
-
|
|
17427
|
-
|
|
17428
|
-
|
|
17429
|
-
|
|
17430
|
-
|
|
17431
|
-
|
|
17432
|
-
|
|
17433
|
-
|
|
17434
|
-
|
|
17435
|
-
|
|
17436
|
-
|
|
17437
|
-
|
|
17438
|
-
|
|
17439
|
-
|
|
17440
|
-
|
|
17441
|
-
|
|
17442
|
-
|
|
17443
|
-
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
|
|
17447
|
-
|
|
17448
|
-
|
|
17449
|
-
|
|
17450
|
-
|
|
17451
|
-
|
|
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
|
-
|
|
19157
|
+
const syncResult = await flushBrowseCapture(session, { queueBackgroundPublish: true });
|
|
17618
19158
|
await closeTab(session.tabId).catch(() => {});
|
|
17619
19159
|
browseSessions.delete("default");
|
|
17620
|
-
return reply.send({
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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})
|
|
18686
|
-
install_hint: finalWalletCheck.configured ? undefined : lobsterInstalled ? "
|
|
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
|
|
18811
|
-
|
|
18812
|
-
|
|
18813
|
-
|
|
18814
|
-
|
|
18815
|
-
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
|
|
18826
|
-
|
|
18827
|
-
|
|
18828
|
-
|
|
18829
|
-
|
|
18830
|
-
|
|
18831
|
-
|
|
18832
|
-
|
|
18833
|
-
|
|
18834
|
-
|
|
18835
|
-
|
|
18836
|
-
|
|
18837
|
-
|
|
18838
|
-
|
|
18839
|
-
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
|
|
18845
|
-
|
|
18846
|
-
|
|
18847
|
-
|
|
18848
|
-
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
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
|
-
|
|
18857
|
-
|
|
18858
|
-
|
|
18859
|
-
|
|
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
|
-
|
|
18863
|
-
|
|
18864
|
-
|
|
18865
|
-
|
|
18866
|
-
|
|
18867
|
-
|
|
18868
|
-
|
|
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
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
18977
|
-
|
|
18978
|
-
}
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
18985
|
-
|
|
18986
|
-
|
|
18987
|
-
|
|
18988
|
-
|
|
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
|
-
|
|
19013
|
-
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
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
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
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
|
-
|
|
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: "
|
|
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":
|