@xbrowser/cli 0.16.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{browser-R7B255ML.js → browser-53KUFEEM.js} +5 -1
- package/dist/{browser-I2HJZ7IP.js → browser-DSVV4GHS.js} +2 -2
- package/dist/{browser-GWBH6OJK.js → browser-GURRY444.js} +3 -1
- package/dist/cdp-driver-MNPR3HZH.js +2537 -0
- package/dist/cdp-driver-SSXUGXP6.js +47 -0
- package/dist/{chunk-2ONMTDLK.js → chunk-2BQZIT3S.js} +2535 -50
- package/dist/{chunk-KDYXFLAC.js → chunk-2MFXKN32.js} +2 -2
- package/dist/chunk-42RPMJ76.js +2530 -0
- package/dist/{chunk-F3ZWFCJJ.js → chunk-E4O5ZU3H.js} +2535 -50
- package/dist/{chunk-ATFTAKMN.js → chunk-IDVD44ED.js} +20 -0
- package/dist/chunk-T4J4C2NZ.js +250 -0
- package/dist/{chunk-RS6YYWTK.js → chunk-YKOHDEFV.js} +73 -38
- package/dist/cli.js +1054 -140
- package/dist/{convert-4DUWZIKH.js → convert-EGFYNICZ.js} +2 -0
- package/dist/{daemon-client-GX2UYIW4.js → daemon-client-3VM7VU7O.js} +22 -0
- package/dist/{daemon-client-3IJD6X4B.js → daemon-client-YAVQ343A.js} +7 -1
- package/dist/daemon-main.js +984 -114
- package/dist/{extract-EGRXZSSK.js → extract-L2IW3IUB.js} +2 -0
- package/dist/{filter-OLAE26HN.js → filter-HC4RA7JY.js} +2 -0
- package/dist/index.d.ts +581 -41
- package/dist/index.js +1100 -182
- package/dist/launcher-KA7J32K5.js +19 -0
- package/dist/{network-store-YAF5OIBH.js → network-store-66A2RATI.js} +1 -0
- package/dist/{session-recorder-XET3DNML.js → session-recorder-MA75PKTQ.js} +1 -1
- package/package.json +2 -3
- package/dist/daemon-client-XWSSQBEA.js +0 -58
- package/dist/network-store-2S5HATEV.js +0 -194
- package/dist/parse-action-dsl-DRSPBALP.js +0 -72
- package/dist/screenshot-MB6R7RSS.js +0 -26
- package/dist/session-recorder-ILSSV2UC.js +0 -6
package/dist/daemon-main.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-VEDJ5XSQ.js";
|
|
6
6
|
import {
|
|
7
7
|
SessionRecorder
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-2MFXKN32.js";
|
|
9
9
|
import {
|
|
10
10
|
closeEphemeralContext,
|
|
11
11
|
closeSessionByName,
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
resolveLaunchOpts,
|
|
22
22
|
saveSessionDiskMeta,
|
|
23
23
|
setActivePage
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-E4O5ZU3H.js";
|
|
25
|
+
import "./chunk-T4J4C2NZ.js";
|
|
26
|
+
import "./chunk-3RG5ZIWI.js";
|
|
25
27
|
|
|
26
28
|
// src/daemon/daemon-main.ts
|
|
27
29
|
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5, appendFileSync } from "fs";
|
|
@@ -40,7 +42,7 @@ import {
|
|
|
40
42
|
|
|
41
43
|
// src/executor.ts
|
|
42
44
|
import {
|
|
43
|
-
ok as
|
|
45
|
+
ok as ok26,
|
|
44
46
|
fail as fail7,
|
|
45
47
|
isCommandResult,
|
|
46
48
|
configureArchiveStore,
|
|
@@ -516,7 +518,7 @@ var pressCommand = registerCommand({
|
|
|
516
518
|
key: z2.string()
|
|
517
519
|
}),
|
|
518
520
|
handler: async (p, ctx) => {
|
|
519
|
-
await ctx.page.press(p.selector || "body", p.key, {
|
|
521
|
+
await ctx.page.press(p.selector || "body", p.key, { timeout: 1e4 });
|
|
520
522
|
return ok2({ key: p.key });
|
|
521
523
|
}
|
|
522
524
|
});
|
|
@@ -535,7 +537,7 @@ var selectCommand = registerCommand({
|
|
|
535
537
|
}),
|
|
536
538
|
handler: async (p, ctx) => {
|
|
537
539
|
const values = typeof p.value === "string" ? [p.value] : p.value;
|
|
538
|
-
await ctx.page.selectOption(p.selector, values
|
|
540
|
+
await ctx.page.selectOption(p.selector, values);
|
|
539
541
|
return ok2({ selector: p.selector, value: p.value });
|
|
540
542
|
}
|
|
541
543
|
});
|
|
@@ -551,7 +553,7 @@ var checkCommand = registerCommand({
|
|
|
551
553
|
selector: z2.string()
|
|
552
554
|
}),
|
|
553
555
|
handler: async (p, ctx) => {
|
|
554
|
-
await ctx.page.check(p.selector, {
|
|
556
|
+
await ctx.page.check(p.selector, { timeout: 1e4 });
|
|
555
557
|
return ok2({ selector: p.selector });
|
|
556
558
|
}
|
|
557
559
|
});
|
|
@@ -568,7 +570,7 @@ var hoverCommand = registerCommand({
|
|
|
568
570
|
selector: z2.string()
|
|
569
571
|
}),
|
|
570
572
|
handler: async (p, ctx) => {
|
|
571
|
-
await ctx.page.hover(p.selector, {
|
|
573
|
+
await ctx.page.hover(p.selector, { timeout: 1e4 });
|
|
572
574
|
return ok2({ selector: p.selector });
|
|
573
575
|
}
|
|
574
576
|
});
|
|
@@ -2097,7 +2099,7 @@ var scrapeCommand = registerCommand({
|
|
|
2097
2099
|
await page.goto(p.url, { waitUntil: "commit", timeout: p.timeout });
|
|
2098
2100
|
await page.waitForSelector("body", { timeout: p.timeout }).catch(() => {
|
|
2099
2101
|
});
|
|
2100
|
-
await page.waitForLoadState("networkidle",
|
|
2102
|
+
await page.waitForLoadState("networkidle", Math.min(p.timeout, 8e3)).catch(() => {
|
|
2101
2103
|
});
|
|
2102
2104
|
await page.waitForTimeout(p.waitAfterLoad > 0 ? p.waitAfterLoad : 2e3);
|
|
2103
2105
|
if (p.selector) {
|
|
@@ -3102,7 +3104,7 @@ async function fetchFullContent(urls, timeout, cdpEndpoint) {
|
|
|
3102
3104
|
const pg = await context.newPage();
|
|
3103
3105
|
try {
|
|
3104
3106
|
await pg.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
3105
|
-
await pg.waitForLoadState("networkidle",
|
|
3107
|
+
await pg.waitForLoadState("networkidle", timeout).catch(() => {
|
|
3106
3108
|
});
|
|
3107
3109
|
const html = await pg.content();
|
|
3108
3110
|
contentMap.set(url, htmlToMarkdown(html, { onlyMainContent: true }));
|
|
@@ -3498,7 +3500,7 @@ var networkCommand = registerCommand({
|
|
|
3498
3500
|
waitUntil: "domcontentloaded",
|
|
3499
3501
|
timeout: p.timeout
|
|
3500
3502
|
});
|
|
3501
|
-
await page.waitForLoadState("networkidle",
|
|
3503
|
+
await page.waitForLoadState("networkidle", p.timeout).catch(() => {
|
|
3502
3504
|
});
|
|
3503
3505
|
await page.waitForTimeout(p.wait);
|
|
3504
3506
|
const totalCount = captures.length;
|
|
@@ -3809,6 +3811,24 @@ var ENGINE_KEY_ENUM = z20.enum(ALL_ENGINE_KEYS);
|
|
|
3809
3811
|
import { z as z21 } from "zod";
|
|
3810
3812
|
import { ok as ok20, fail as fail4 } from "@dyyz1993/xcli-core";
|
|
3811
3813
|
|
|
3814
|
+
// src/runtime/ref-store.ts
|
|
3815
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
3816
|
+
function normalizeAgentRef(ref) {
|
|
3817
|
+
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
3818
|
+
}
|
|
3819
|
+
function replaceRefs(sessionKey2, screenHash, targets) {
|
|
3820
|
+
sessions.set(sessionKey2, {
|
|
3821
|
+
screenHash,
|
|
3822
|
+
targets: new Map(targets.map((target) => [target.ref, target]))
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
3825
|
+
function getRefTarget(sessionKey2, ref) {
|
|
3826
|
+
const session = sessions.get(sessionKey2);
|
|
3827
|
+
const target = session?.targets.get(normalizeAgentRef(ref));
|
|
3828
|
+
if (!session || !target) return null;
|
|
3829
|
+
return { screenHash: session.screenHash, target };
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3812
3832
|
// src/utils/resolve-selector.ts
|
|
3813
3833
|
function buildElementSelector(el) {
|
|
3814
3834
|
function isUnique(sel) {
|
|
@@ -3980,9 +4000,9 @@ async function resolveSelectors(page, ariaSnapshot) {
|
|
|
3980
4000
|
}
|
|
3981
4001
|
return results;
|
|
3982
4002
|
}
|
|
3983
|
-
var REF_ONLY =
|
|
4003
|
+
var REF_ONLY = /^@?(e\d+)$/;
|
|
3984
4004
|
var refCache = /* @__PURE__ */ new Map();
|
|
3985
|
-
async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
4005
|
+
async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
|
|
3986
4006
|
const tips = [];
|
|
3987
4007
|
const newParams = { ...params };
|
|
3988
4008
|
if (!selectorKeys || selectorKeys.length === 0) {
|
|
@@ -3991,7 +4011,17 @@ async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
|
3991
4011
|
for (const key of selectorKeys) {
|
|
3992
4012
|
const val = params[key];
|
|
3993
4013
|
if (typeof val !== "string" || !REF_ONLY.test(val)) continue;
|
|
3994
|
-
const
|
|
4014
|
+
const match = val.match(REF_ONLY);
|
|
4015
|
+
if (!match) continue;
|
|
4016
|
+
const ref = normalizeAgentRef(match[1]);
|
|
4017
|
+
if (sessionId) {
|
|
4018
|
+
const runtimeTarget = getRefTarget(sessionId, ref);
|
|
4019
|
+
if (runtimeTarget) {
|
|
4020
|
+
tips.push(`ref=@${ref} (${key}) => ${runtimeTarget.target.selector} (observe)`);
|
|
4021
|
+
newParams[key] = runtimeTarget.target.selector;
|
|
4022
|
+
continue;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
3995
4025
|
const activeCache = cache ?? refCache;
|
|
3996
4026
|
const cached = activeCache.get(ref);
|
|
3997
4027
|
if (cached) {
|
|
@@ -4272,6 +4302,404 @@ async function enhanceSemanticsWithLLM(url, ariaSnapshot, ruleBasedElements) {
|
|
|
4272
4302
|
saveSemantics(domain, pathKey, url, llmElements);
|
|
4273
4303
|
}
|
|
4274
4304
|
|
|
4305
|
+
// src/runtime/agent-runtime.ts
|
|
4306
|
+
function sessionKey(sessionId) {
|
|
4307
|
+
return sessionId || "default";
|
|
4308
|
+
}
|
|
4309
|
+
async function observePage(page, sessionId, options = {}) {
|
|
4310
|
+
const [title, raw] = await Promise.all([
|
|
4311
|
+
page.title().catch(() => ""),
|
|
4312
|
+
page.evaluate(
|
|
4313
|
+
({ includeHidden, limit }) => {
|
|
4314
|
+
function hash(input) {
|
|
4315
|
+
let h = 2166136261;
|
|
4316
|
+
for (let i = 0; i < input.length; i++) {
|
|
4317
|
+
h ^= input.charCodeAt(i);
|
|
4318
|
+
h = Math.imul(h, 16777619);
|
|
4319
|
+
}
|
|
4320
|
+
return (h >>> 0).toString(16);
|
|
4321
|
+
}
|
|
4322
|
+
function cssEscape(value) {
|
|
4323
|
+
const css = globalThis.CSS;
|
|
4324
|
+
return css?.escape ? css.escape(value) : value.replace(/["\\#.:,[\]>+~*]/g, "\\$&");
|
|
4325
|
+
}
|
|
4326
|
+
function isUnique(selector2) {
|
|
4327
|
+
try {
|
|
4328
|
+
return document.querySelectorAll(selector2).length === 1;
|
|
4329
|
+
} catch {
|
|
4330
|
+
return false;
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
function nthOfType(el) {
|
|
4334
|
+
const tag = el.tagName.toLowerCase();
|
|
4335
|
+
const parent = el.parentElement;
|
|
4336
|
+
if (!parent) return tag;
|
|
4337
|
+
const same = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
4338
|
+
if (same.length === 1) return tag;
|
|
4339
|
+
return `${tag}:nth-of-type(${same.indexOf(el) + 1})`;
|
|
4340
|
+
}
|
|
4341
|
+
function selectorFor(el) {
|
|
4342
|
+
const tag = el.tagName.toLowerCase();
|
|
4343
|
+
const id = el.getAttribute("id");
|
|
4344
|
+
if (id) {
|
|
4345
|
+
const selector2 = `#${cssEscape(id)}`;
|
|
4346
|
+
if (isUnique(selector2)) return selector2;
|
|
4347
|
+
}
|
|
4348
|
+
for (const attr of ["data-testid", "data-test", "data-qa", "name", "aria-label"]) {
|
|
4349
|
+
const value = el.getAttribute(attr);
|
|
4350
|
+
if (!value) continue;
|
|
4351
|
+
const selector2 = `[${attr}="${cssEscape(value)}"]`;
|
|
4352
|
+
if (isUnique(selector2)) return selector2;
|
|
4353
|
+
const tagged = `${tag}${selector2}`;
|
|
4354
|
+
if (isUnique(tagged)) return tagged;
|
|
4355
|
+
}
|
|
4356
|
+
const classes = Array.from(el.classList).slice(0, 3);
|
|
4357
|
+
for (const cls of classes) {
|
|
4358
|
+
const selector2 = `${tag}.${cssEscape(cls)}`;
|
|
4359
|
+
if (isUnique(selector2)) return selector2;
|
|
4360
|
+
}
|
|
4361
|
+
const parts = [];
|
|
4362
|
+
let cur = el;
|
|
4363
|
+
while (cur && cur !== document.body && cur !== document.documentElement && parts.length < 6) {
|
|
4364
|
+
parts.unshift(nthOfType(cur));
|
|
4365
|
+
const selector2 = parts.join(" > ");
|
|
4366
|
+
if (isUnique(selector2)) return selector2;
|
|
4367
|
+
cur = cur.parentElement;
|
|
4368
|
+
}
|
|
4369
|
+
return parts.join(" > ") || tag;
|
|
4370
|
+
}
|
|
4371
|
+
function roleFor(el) {
|
|
4372
|
+
const explicit = el.getAttribute("role");
|
|
4373
|
+
if (explicit) return explicit;
|
|
4374
|
+
const tag = el.tagName.toLowerCase();
|
|
4375
|
+
if (tag === "a") return "link";
|
|
4376
|
+
if (tag === "button") return "button";
|
|
4377
|
+
if (tag === "select") return "combobox";
|
|
4378
|
+
if (tag === "textarea") return "textbox";
|
|
4379
|
+
if (tag === "input") {
|
|
4380
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
4381
|
+
if (type === "checkbox") return "checkbox";
|
|
4382
|
+
if (type === "radio") return "radio";
|
|
4383
|
+
if (type === "button" || type === "submit" || type === "reset") return "button";
|
|
4384
|
+
return "textbox";
|
|
4385
|
+
}
|
|
4386
|
+
return tag;
|
|
4387
|
+
}
|
|
4388
|
+
function textName(el) {
|
|
4389
|
+
const input = el;
|
|
4390
|
+
const direct = el.getAttribute("aria-label") || el.getAttribute("placeholder") || el.getAttribute("title") || input.value || el.textContent || "";
|
|
4391
|
+
return direct.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
4392
|
+
}
|
|
4393
|
+
function actionsFor(role, editable) {
|
|
4394
|
+
const actions = [];
|
|
4395
|
+
if (editable) actions.push("fill", "type");
|
|
4396
|
+
if (role === "combobox") actions.push("select");
|
|
4397
|
+
if (role === "checkbox" || role === "radio") actions.push("check");
|
|
4398
|
+
actions.push("click", "hover");
|
|
4399
|
+
return Array.from(new Set(actions));
|
|
4400
|
+
}
|
|
4401
|
+
const selector = [
|
|
4402
|
+
"a[href]",
|
|
4403
|
+
"button",
|
|
4404
|
+
"input",
|
|
4405
|
+
"textarea",
|
|
4406
|
+
"select",
|
|
4407
|
+
"summary",
|
|
4408
|
+
"label",
|
|
4409
|
+
"[role]",
|
|
4410
|
+
"[tabindex]",
|
|
4411
|
+
'[contenteditable="true"]'
|
|
4412
|
+
].join(",");
|
|
4413
|
+
const candidates = Array.from(document.querySelectorAll(selector));
|
|
4414
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4415
|
+
const targets2 = [];
|
|
4416
|
+
for (const el of candidates) {
|
|
4417
|
+
const rect = el.getBoundingClientRect();
|
|
4418
|
+
const style = window.getComputedStyle(el);
|
|
4419
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4420
|
+
if (!includeHidden && !visible) continue;
|
|
4421
|
+
const selectorValue = selectorFor(el);
|
|
4422
|
+
if (!selectorValue || seen.has(selectorValue)) continue;
|
|
4423
|
+
seen.add(selectorValue);
|
|
4424
|
+
const input = el;
|
|
4425
|
+
const tag = el.tagName.toLowerCase();
|
|
4426
|
+
const role = roleFor(el);
|
|
4427
|
+
const editable = tag === "textarea" || tag === "select" || tag === "input" && !["button", "submit", "reset", "checkbox", "radio"].includes((input.type || "").toLowerCase()) || el.isContentEditable;
|
|
4428
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4429
|
+
const checked = typeof input.checked === "boolean" && ["checkbox", "radio"].includes((input.type || "").toLowerCase()) ? input.checked : void 0;
|
|
4430
|
+
targets2.push({
|
|
4431
|
+
selector: selectorValue,
|
|
4432
|
+
role,
|
|
4433
|
+
name: textName(el),
|
|
4434
|
+
tag,
|
|
4435
|
+
visible,
|
|
4436
|
+
enabled,
|
|
4437
|
+
editable,
|
|
4438
|
+
...checked !== void 0 ? { checked } : {},
|
|
4439
|
+
...editable && input.value ? { value: input.value.slice(0, 120) } : {},
|
|
4440
|
+
...visible ? { box: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) } } : {},
|
|
4441
|
+
actions: actionsFor(role, editable)
|
|
4442
|
+
});
|
|
4443
|
+
if (targets2.length >= limit) break;
|
|
4444
|
+
}
|
|
4445
|
+
const stateText = [
|
|
4446
|
+
location.href,
|
|
4447
|
+
document.title,
|
|
4448
|
+
document.body?.innerText?.slice(0, 5e3) || ""
|
|
4449
|
+
].join("\n");
|
|
4450
|
+
return { screenHash: hash(stateText), targets: targets2 };
|
|
4451
|
+
},
|
|
4452
|
+
{ includeHidden: !!options.includeHidden, limit: options.limit ?? 80 }
|
|
4453
|
+
)
|
|
4454
|
+
]);
|
|
4455
|
+
const targets = raw.targets.map((target, index) => ({
|
|
4456
|
+
ref: `e${index + 1}`,
|
|
4457
|
+
...target
|
|
4458
|
+
}));
|
|
4459
|
+
replaceRefs(sessionKey(sessionId), raw.screenHash, targets);
|
|
4460
|
+
return {
|
|
4461
|
+
url: page.url(),
|
|
4462
|
+
title,
|
|
4463
|
+
screenHash: raw.screenHash,
|
|
4464
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4465
|
+
targets
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
function quoteName(name) {
|
|
4469
|
+
const cleaned = name.replace(/\s+/g, " ").trim();
|
|
4470
|
+
if (!cleaned) return "";
|
|
4471
|
+
return ` "${cleaned.replace(/"/g, '\\"')}"`;
|
|
4472
|
+
}
|
|
4473
|
+
function targetFlags(target) {
|
|
4474
|
+
const flags = [target.role || target.tag];
|
|
4475
|
+
if (!target.enabled) flags.push("disabled");
|
|
4476
|
+
if (target.editable) flags.push("editable");
|
|
4477
|
+
if (target.checked !== void 0) flags.push(target.checked ? "checked" : "unchecked");
|
|
4478
|
+
return flags.join(" ");
|
|
4479
|
+
}
|
|
4480
|
+
function buildSelectorMap(observation) {
|
|
4481
|
+
return Object.fromEntries(observation.targets.map((target) => [target.ref, target.selector]));
|
|
4482
|
+
}
|
|
4483
|
+
function formatObservationCompact(observation, options = {}) {
|
|
4484
|
+
const lines = [
|
|
4485
|
+
`Page: ${observation.title || "(untitled)"}`,
|
|
4486
|
+
`URL: ${observation.url}`,
|
|
4487
|
+
`Screen: ${observation.screenHash}`,
|
|
4488
|
+
""
|
|
4489
|
+
];
|
|
4490
|
+
if (observation.targets.length === 0) {
|
|
4491
|
+
lines.push("(no interactive targets)");
|
|
4492
|
+
} else {
|
|
4493
|
+
for (const target of observation.targets) {
|
|
4494
|
+
lines.push(`@${target.ref} [${targetFlags(target)}]${quoteName(target.name)}`);
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
if (options.selectors && observation.targets.length > 0) {
|
|
4498
|
+
lines.push("", "## Selectors");
|
|
4499
|
+
lines.push(observation.targets.map((target) => `${target.ref}: ${target.selector}`).join(" | "));
|
|
4500
|
+
}
|
|
4501
|
+
return lines.join("\n");
|
|
4502
|
+
}
|
|
4503
|
+
async function getPageScreenHash(page) {
|
|
4504
|
+
const observation = await page.evaluate(() => {
|
|
4505
|
+
let h = 2166136261;
|
|
4506
|
+
const input = [location.href, document.title, document.body?.innerText?.slice(0, 5e3) || ""].join("\n");
|
|
4507
|
+
for (let i = 0; i < input.length; i++) {
|
|
4508
|
+
h ^= input.charCodeAt(i);
|
|
4509
|
+
h = Math.imul(h, 16777619);
|
|
4510
|
+
}
|
|
4511
|
+
return (h >>> 0).toString(16);
|
|
4512
|
+
});
|
|
4513
|
+
return observation;
|
|
4514
|
+
}
|
|
4515
|
+
async function actionability(page, selector) {
|
|
4516
|
+
return await page.evaluate((sel) => {
|
|
4517
|
+
const el = document.querySelector(sel);
|
|
4518
|
+
if (!el) return { ok: false, reason: "not_found" };
|
|
4519
|
+
const rect = el.getBoundingClientRect();
|
|
4520
|
+
const style = window.getComputedStyle(el);
|
|
4521
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4522
|
+
if (!visible) return { ok: false, reason: "not_visible" };
|
|
4523
|
+
const input = el;
|
|
4524
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4525
|
+
if (!enabled) return { ok: false, reason: "disabled" };
|
|
4526
|
+
const cx = rect.left + rect.width / 2;
|
|
4527
|
+
const cy = rect.top + rect.height / 2;
|
|
4528
|
+
const hit = document.elementFromPoint(cx, cy);
|
|
4529
|
+
if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
|
|
4530
|
+
return { ok: false, reason: "covered" };
|
|
4531
|
+
}
|
|
4532
|
+
return { ok: true };
|
|
4533
|
+
}, selector);
|
|
4534
|
+
}
|
|
4535
|
+
async function actOnPage(page, sessionId, input) {
|
|
4536
|
+
const normalizedRef = input.ref ? normalizeAgentRef(input.ref) : void 0;
|
|
4537
|
+
const refMatch = normalizedRef ? getRefTarget(sessionKey(sessionId), normalizedRef) : null;
|
|
4538
|
+
const selector = input.selector || refMatch?.target.selector;
|
|
4539
|
+
if (!selector) {
|
|
4540
|
+
return {
|
|
4541
|
+
action: input.action,
|
|
4542
|
+
selector: "",
|
|
4543
|
+
ref: normalizedRef,
|
|
4544
|
+
success: false,
|
|
4545
|
+
reason: input.ref ? "unknown_ref" : "missing_target",
|
|
4546
|
+
message: normalizedRef ? `Ref "${input.ref}" not found. Run observe again.` : "Provide ref or selector."
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
const hash = await getPageScreenHash(page).catch(() => void 0);
|
|
4550
|
+
const stale = !!(refMatch && hash && hash !== refMatch.screenHash);
|
|
4551
|
+
if (!input.force) {
|
|
4552
|
+
const check = await actionability(page, selector);
|
|
4553
|
+
if (!check.ok) {
|
|
4554
|
+
return {
|
|
4555
|
+
action: input.action,
|
|
4556
|
+
selector,
|
|
4557
|
+
ref: normalizedRef,
|
|
4558
|
+
success: false,
|
|
4559
|
+
reason: stale ? "stale_ref" : check.reason,
|
|
4560
|
+
message: stale ? `Ref "${input.ref}" may be stale. Run observe again.` : `Target is not actionable: ${check.reason}`,
|
|
4561
|
+
stale,
|
|
4562
|
+
screenHash: hash,
|
|
4563
|
+
target: refMatch?.target
|
|
4564
|
+
};
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
const timeout = input.timeout ?? 1e4;
|
|
4568
|
+
try {
|
|
4569
|
+
switch (input.action) {
|
|
4570
|
+
case "click":
|
|
4571
|
+
await page.locator(selector).first().click({ timeout, force: !!input.force });
|
|
4572
|
+
break;
|
|
4573
|
+
case "fill":
|
|
4574
|
+
if (input.value === void 0) throw new Error("fill requires value");
|
|
4575
|
+
await page.locator(selector).first().fill(input.value, { timeout, force: !!input.force });
|
|
4576
|
+
break;
|
|
4577
|
+
case "type":
|
|
4578
|
+
if (input.value === void 0) throw new Error("type requires value");
|
|
4579
|
+
await page.locator(selector).first().pressSequentially(input.value, { timeout });
|
|
4580
|
+
break;
|
|
4581
|
+
case "press":
|
|
4582
|
+
if (!input.key) throw new Error("press requires key");
|
|
4583
|
+
await page.locator(selector).first().press(input.key, { timeout });
|
|
4584
|
+
break;
|
|
4585
|
+
case "select":
|
|
4586
|
+
if (input.value === void 0) throw new Error("select requires value");
|
|
4587
|
+
await page.locator(selector).first().selectOption(input.value);
|
|
4588
|
+
break;
|
|
4589
|
+
case "check":
|
|
4590
|
+
await page.locator(selector).first().check({ timeout });
|
|
4591
|
+
break;
|
|
4592
|
+
case "hover":
|
|
4593
|
+
await page.locator(selector).first().hover({ timeout });
|
|
4594
|
+
break;
|
|
4595
|
+
default: {
|
|
4596
|
+
const neverAction = input.action;
|
|
4597
|
+
throw new Error(`Unsupported action: ${neverAction}`);
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
} catch (error) {
|
|
4601
|
+
return {
|
|
4602
|
+
action: input.action,
|
|
4603
|
+
selector,
|
|
4604
|
+
ref: normalizedRef,
|
|
4605
|
+
success: false,
|
|
4606
|
+
reason: "browser_error",
|
|
4607
|
+
message: error.message,
|
|
4608
|
+
stale,
|
|
4609
|
+
screenHash: hash,
|
|
4610
|
+
target: refMatch?.target
|
|
4611
|
+
};
|
|
4612
|
+
}
|
|
4613
|
+
return {
|
|
4614
|
+
action: input.action,
|
|
4615
|
+
selector,
|
|
4616
|
+
ref: normalizedRef,
|
|
4617
|
+
success: true,
|
|
4618
|
+
stale,
|
|
4619
|
+
screenHash: hash,
|
|
4620
|
+
target: refMatch?.target
|
|
4621
|
+
};
|
|
4622
|
+
}
|
|
4623
|
+
function matchUrlPattern(url, pattern) {
|
|
4624
|
+
if (pattern.includes("*")) {
|
|
4625
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
4626
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
4627
|
+
}
|
|
4628
|
+
return url.includes(pattern);
|
|
4629
|
+
}
|
|
4630
|
+
async function pollUntil(timeout, pollInterval, predicate) {
|
|
4631
|
+
const startedAt = Date.now();
|
|
4632
|
+
while (Date.now() - startedAt <= timeout) {
|
|
4633
|
+
if (await predicate()) return true;
|
|
4634
|
+
await new Promise((resolve9) => setTimeout(resolve9, pollInterval));
|
|
4635
|
+
}
|
|
4636
|
+
return false;
|
|
4637
|
+
}
|
|
4638
|
+
async function waitForPage(page, input) {
|
|
4639
|
+
const timeout = input.timeout ?? 3e4;
|
|
4640
|
+
const pollInterval = input.pollInterval ?? 200;
|
|
4641
|
+
const startedAt = Date.now();
|
|
4642
|
+
try {
|
|
4643
|
+
if (input.selector) {
|
|
4644
|
+
const state = input.state ?? "visible";
|
|
4645
|
+
await page.locator(input.selector).first().waitFor({ state, timeout });
|
|
4646
|
+
return { success: true, matched: "selector", timeout, elapsed: Date.now() - startedAt };
|
|
4647
|
+
}
|
|
4648
|
+
if (input.text) {
|
|
4649
|
+
await page.getByText(input.text).first().waitFor({ state: "visible", timeout });
|
|
4650
|
+
return { success: true, matched: "text", timeout, elapsed: Date.now() - startedAt };
|
|
4651
|
+
}
|
|
4652
|
+
if (input.url) {
|
|
4653
|
+
const matched = await pollUntil(timeout, pollInterval, async () => matchUrlPattern(page.url(), input.url));
|
|
4654
|
+
return {
|
|
4655
|
+
success: matched,
|
|
4656
|
+
matched: "url",
|
|
4657
|
+
timeout,
|
|
4658
|
+
elapsed: Date.now() - startedAt,
|
|
4659
|
+
...matched ? {} : { message: `Timed out waiting for URL pattern: ${input.url}` }
|
|
4660
|
+
};
|
|
4661
|
+
}
|
|
4662
|
+
if (input.load) {
|
|
4663
|
+
await page.waitForLoadState(input.load, timeout);
|
|
4664
|
+
return { success: true, matched: "load", timeout, elapsed: Date.now() - startedAt };
|
|
4665
|
+
}
|
|
4666
|
+
if (input.fn) {
|
|
4667
|
+
await page.waitForFunction(input.fn, void 0, { timeout });
|
|
4668
|
+
return { success: true, matched: "fn", timeout, elapsed: Date.now() - startedAt };
|
|
4669
|
+
}
|
|
4670
|
+
if (input.screenHashChanged) {
|
|
4671
|
+
let screenHash = await getPageScreenHash(page);
|
|
4672
|
+
const matched = await pollUntil(timeout, pollInterval, async () => {
|
|
4673
|
+
screenHash = await getPageScreenHash(page);
|
|
4674
|
+
return screenHash !== input.screenHashChanged;
|
|
4675
|
+
});
|
|
4676
|
+
return {
|
|
4677
|
+
success: matched,
|
|
4678
|
+
matched: "screenHashChanged",
|
|
4679
|
+
timeout,
|
|
4680
|
+
elapsed: Date.now() - startedAt,
|
|
4681
|
+
screenHash,
|
|
4682
|
+
...matched ? {} : { message: `Timed out waiting for screen hash to change: ${input.screenHashChanged}` }
|
|
4683
|
+
};
|
|
4684
|
+
}
|
|
4685
|
+
} catch (error) {
|
|
4686
|
+
return {
|
|
4687
|
+
success: false,
|
|
4688
|
+
matched: input.selector ? "selector" : input.text ? "text" : input.url ? "url" : input.load ? "load" : input.fn ? "fn" : "screenHashChanged",
|
|
4689
|
+
timeout,
|
|
4690
|
+
elapsed: Date.now() - startedAt,
|
|
4691
|
+
message: error.message
|
|
4692
|
+
};
|
|
4693
|
+
}
|
|
4694
|
+
return {
|
|
4695
|
+
success: false,
|
|
4696
|
+
matched: "selector",
|
|
4697
|
+
timeout,
|
|
4698
|
+
elapsed: Date.now() - startedAt,
|
|
4699
|
+
message: "Provide one wait predicate: selector, text, url, load, fn, or screenHashChanged."
|
|
4700
|
+
};
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4275
4703
|
// src/commands/snapshot.ts
|
|
4276
4704
|
var snapshotCommand = registerCommand({
|
|
4277
4705
|
name: "snapshot",
|
|
@@ -4281,7 +4709,14 @@ var snapshotCommand = registerCommand({
|
|
|
4281
4709
|
parameters: z21.object({
|
|
4282
4710
|
type: z21.enum(["aria", "text", "dom", "all"]).default("aria").describe("Snapshot type: aria (accessibility tree), text (visible text), dom (element summary), all (combined)"),
|
|
4283
4711
|
selector: z21.string().optional().describe("Scope to a specific element"),
|
|
4284
|
-
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree")
|
|
4712
|
+
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree"),
|
|
4713
|
+
interactive: z21.boolean().optional().default(false).describe("Return interactive agent refs only"),
|
|
4714
|
+
interactiveOnly: z21.boolean().optional().default(false).describe("Alias for interactive"),
|
|
4715
|
+
i: z21.boolean().optional().default(false).describe("Short alias for interactive"),
|
|
4716
|
+
compact: z21.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
|
|
4717
|
+
c: z21.boolean().optional().default(false).describe("Short alias for compact"),
|
|
4718
|
+
selectors: z21.boolean().optional().default(false).describe("Include ref to CSS selector map"),
|
|
4719
|
+
all: z21.boolean().optional().default(false).describe("Include hidden interactive targets when using interactive snapshot")
|
|
4285
4720
|
}),
|
|
4286
4721
|
result: z21.object({
|
|
4287
4722
|
url: z21.string(),
|
|
@@ -4294,6 +4729,18 @@ var snapshotCommand = registerCommand({
|
|
|
4294
4729
|
const page = ctx.page;
|
|
4295
4730
|
const url = page.url();
|
|
4296
4731
|
const title = await page.title().catch(() => "");
|
|
4732
|
+
if (p.interactive || p.interactiveOnly || p.i || p.compact || p.c || p.selectors) {
|
|
4733
|
+
const observation = await observePage(page, ctx.sessionId, {
|
|
4734
|
+
includeHidden: p.all
|
|
4735
|
+
});
|
|
4736
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
4737
|
+
if (p.compact || p.c || p.interactive || p.interactiveOnly || p.i) {
|
|
4738
|
+
observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4739
|
+
}
|
|
4740
|
+
return ok20(observation, [
|
|
4741
|
+
`refs refreshed for ${observation.targets.length} targets; use click @e1 or fill @e2 "text"`
|
|
4742
|
+
]);
|
|
4743
|
+
}
|
|
4297
4744
|
if (p.type === "aria") {
|
|
4298
4745
|
const aria = await captureAriaSnapshot(page, p.selector, p.depth);
|
|
4299
4746
|
const tips = await buildRefTips(page, aria);
|
|
@@ -4344,10 +4791,10 @@ async function buildRefTips(page, aria) {
|
|
|
4344
4791
|
return [];
|
|
4345
4792
|
}
|
|
4346
4793
|
}
|
|
4347
|
-
async function captureAriaSnapshot(page, selector,
|
|
4794
|
+
async function captureAriaSnapshot(page, selector, _depth) {
|
|
4348
4795
|
try {
|
|
4349
4796
|
const locator = selector ? page.locator(selector).first() : page.locator("body");
|
|
4350
|
-
return await locator.ariaSnapshot(
|
|
4797
|
+
return await locator.ariaSnapshot();
|
|
4351
4798
|
} catch {
|
|
4352
4799
|
try {
|
|
4353
4800
|
return await page.locator("body").ariaSnapshot();
|
|
@@ -4358,7 +4805,7 @@ async function captureAriaSnapshot(page, selector, depth) {
|
|
|
4358
4805
|
}
|
|
4359
4806
|
async function captureTextSnapshot(page, selector) {
|
|
4360
4807
|
if (selector) {
|
|
4361
|
-
return await page.locator(selector).first().innerText(
|
|
4808
|
+
return await page.locator(selector).first().innerText().catch(() => "");
|
|
4362
4809
|
}
|
|
4363
4810
|
return await page.evaluate(() => document.body?.innerText || "").catch(() => "");
|
|
4364
4811
|
}
|
|
@@ -4394,22 +4841,108 @@ async function captureDomSnapshot(page, selector, maxDepth) {
|
|
|
4394
4841
|
).catch(() => ({ tag: "error" }));
|
|
4395
4842
|
}
|
|
4396
4843
|
|
|
4397
|
-
// src/commands/
|
|
4844
|
+
// src/commands/agent.ts
|
|
4398
4845
|
import { z as z22 } from "zod";
|
|
4399
|
-
import { ok as ok21
|
|
4400
|
-
var
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4846
|
+
import { ok as ok21 } from "@dyyz1993/xcli-core";
|
|
4847
|
+
var observeCommand = registerCommand({
|
|
4848
|
+
name: "observe",
|
|
4849
|
+
description: "Observe the current page as structured agent targets with session refs",
|
|
4850
|
+
scope: "page",
|
|
4851
|
+
parameters: z22.object({
|
|
4852
|
+
includeHidden: z22.boolean().optional().default(false).describe("Include hidden elements in the target list"),
|
|
4853
|
+
limit: z22.number().int().positive().max(300).optional().default(80).describe("Maximum number of targets to return"),
|
|
4854
|
+
compact: z22.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
|
|
4855
|
+
selectors: z22.boolean().optional().default(false).describe("Include ref to stable CSS selector map")
|
|
4856
|
+
}),
|
|
4857
|
+
handler: async (p, ctx) => {
|
|
4858
|
+
const observation = await observePage(ctx.page, ctx.sessionId, {
|
|
4859
|
+
includeHidden: p.includeHidden,
|
|
4860
|
+
limit: p.limit
|
|
4861
|
+
});
|
|
4862
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
4863
|
+
if (p.compact) observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4864
|
+
return ok21(observation, [
|
|
4865
|
+
`refs refreshed for ${observation.targets.length} targets; use act --ref @e1 --action click or click @e1`
|
|
4866
|
+
]);
|
|
4867
|
+
}
|
|
4868
|
+
});
|
|
4869
|
+
var actCommand = registerCommand({
|
|
4870
|
+
name: "act",
|
|
4871
|
+
description: "Perform an agent action using an observe ref or explicit selector",
|
|
4872
|
+
scope: "element",
|
|
4873
|
+
selectorParams: ["selector"],
|
|
4874
|
+
parameters: z22.object({
|
|
4875
|
+
action: z22.enum(["click", "fill", "type", "press", "select", "check", "hover"]).default("click"),
|
|
4876
|
+
ref: z22.string().optional().describe("Session-scoped ref returned by observe, such as e1"),
|
|
4877
|
+
selector: z22.string().optional().describe("CSS selector fallback when no ref is available"),
|
|
4878
|
+
value: z22.string().optional().describe("Value for fill/type/select"),
|
|
4879
|
+
key: z22.string().optional().describe("Key for press"),
|
|
4880
|
+
force: z22.boolean().optional().default(false).describe("Bypass actionability checks"),
|
|
4881
|
+
timeout: z22.number().optional().default(1e4).describe("Playwright action timeout in milliseconds")
|
|
4882
|
+
}).refine((p) => !!p.ref || !!p.selector, {
|
|
4883
|
+
message: "Either ref or selector is required"
|
|
4884
|
+
}),
|
|
4885
|
+
handler: async (p, ctx) => {
|
|
4886
|
+
const result = await actOnPage(ctx.page, ctx.sessionId, { ...p });
|
|
4887
|
+
if (!result.success) {
|
|
4888
|
+
return {
|
|
4889
|
+
success: false,
|
|
4890
|
+
data: result,
|
|
4891
|
+
message: result.message || result.reason || "Action failed",
|
|
4892
|
+
tips: result.stale ? ["run observe again to refresh refs"] : []
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
return ok21(result, result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []);
|
|
4896
|
+
}
|
|
4897
|
+
});
|
|
4898
|
+
var waitForCommand = registerCommand({
|
|
4899
|
+
name: "waitFor",
|
|
4900
|
+
description: "Wait for agent predicates such as text, URL, load state, selector state, or screen hash changes",
|
|
4901
|
+
scope: "page",
|
|
4902
|
+
selectorParams: ["selector"],
|
|
4903
|
+
parameters: z22.object({
|
|
4904
|
+
selector: z22.string().optional().describe("CSS selector or observe ref to wait for"),
|
|
4905
|
+
state: z22.enum(["attached", "detached", "visible", "hidden"]).optional().default("visible"),
|
|
4906
|
+
text: z22.string().optional().describe("Visible text to wait for"),
|
|
4907
|
+
url: z22.string().optional().describe("URL substring or glob pattern to wait for"),
|
|
4908
|
+
load: z22.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Load state to wait for"),
|
|
4909
|
+
fn: z22.string().optional().describe("JavaScript predicate to wait for"),
|
|
4910
|
+
screenHashChanged: z22.string().optional().describe("Previous screenHash from observe"),
|
|
4911
|
+
timeout: z22.number().optional().default(3e4),
|
|
4912
|
+
pollInterval: z22.number().optional().default(200)
|
|
4913
|
+
}).refine((p) => [p.selector, p.text, p.url, p.load, p.fn, p.screenHashChanged].filter(Boolean).length === 1, {
|
|
4914
|
+
message: "Provide exactly one wait predicate: selector, text, url, load, fn, or screenHashChanged"
|
|
4915
|
+
}),
|
|
4916
|
+
handler: async (p, ctx) => {
|
|
4917
|
+
const result = await waitForPage(ctx.page, { ...p });
|
|
4918
|
+
if (!result.success) {
|
|
4919
|
+
return {
|
|
4920
|
+
success: false,
|
|
4921
|
+
data: result,
|
|
4922
|
+
message: result.message || `Timed out waiting for ${result.matched}`,
|
|
4923
|
+
tips: []
|
|
4924
|
+
};
|
|
4925
|
+
}
|
|
4926
|
+
return ok21(result);
|
|
4927
|
+
}
|
|
4928
|
+
});
|
|
4929
|
+
|
|
4930
|
+
// src/commands/tab.ts
|
|
4931
|
+
import { z as z23 } from "zod";
|
|
4932
|
+
import { ok as ok22, fail as fail5 } from "@dyyz1993/xcli-core";
|
|
4933
|
+
var TabParams = z23.object({
|
|
4934
|
+
subcommand: z23.enum(["list", "new", "close", "switch"]),
|
|
4935
|
+
url: z23.string().optional(),
|
|
4936
|
+
index: z23.number().int().min(0).optional()
|
|
4404
4937
|
});
|
|
4405
4938
|
var tabCommand = registerCommand({
|
|
4406
4939
|
name: "tab",
|
|
4407
4940
|
description: "Manage browser tabs: list, new, close, switch",
|
|
4408
4941
|
scope: "page",
|
|
4409
4942
|
parameters: TabParams,
|
|
4410
|
-
result:
|
|
4411
|
-
success:
|
|
4412
|
-
data:
|
|
4943
|
+
result: z23.object({
|
|
4944
|
+
success: z23.boolean(),
|
|
4945
|
+
data: z23.unknown()
|
|
4413
4946
|
}),
|
|
4414
4947
|
handler: async (p, ctx) => {
|
|
4415
4948
|
const pages = ctx.browserContext.pages();
|
|
@@ -4449,7 +4982,7 @@ function handleList(pages, ctx) {
|
|
|
4449
4982
|
active: i === currentIndex
|
|
4450
4983
|
};
|
|
4451
4984
|
});
|
|
4452
|
-
return
|
|
4985
|
+
return ok22({ tabs, total: tabs.length, activeIndex: currentIndex });
|
|
4453
4986
|
}
|
|
4454
4987
|
async function handleNew(p, _pages, ctx) {
|
|
4455
4988
|
const newPage = await ctx.browserContext.newPage();
|
|
@@ -4471,7 +5004,7 @@ async function handleNew(p, _pages, ctx) {
|
|
|
4471
5004
|
const title = await newPage.title().catch(() => "");
|
|
4472
5005
|
const allPages = ctx.browserContext.pages();
|
|
4473
5006
|
const newIndex = allPages.indexOf(newPage);
|
|
4474
|
-
return
|
|
5007
|
+
return ok22({
|
|
4475
5008
|
index: newIndex >= 0 ? newIndex : allPages.length - 1,
|
|
4476
5009
|
url: newPage.url(),
|
|
4477
5010
|
title,
|
|
@@ -4499,7 +5032,7 @@ async function handleClose(p, pages, ctx) {
|
|
|
4499
5032
|
}
|
|
4500
5033
|
ctx.page = newActivePage;
|
|
4501
5034
|
}
|
|
4502
|
-
return
|
|
5035
|
+
return ok22({
|
|
4503
5036
|
closedIndex: closeIndex,
|
|
4504
5037
|
total: remainingPages.length,
|
|
4505
5038
|
activeIndex: isActivePage ? closeIndex < remainingPages.length ? closeIndex : remainingPages.length - 1 : pages.indexOf(ctx.page)
|
|
@@ -4521,7 +5054,7 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4521
5054
|
}
|
|
4522
5055
|
ctx.page = targetPage;
|
|
4523
5056
|
const title = await targetPage.title().catch(() => "");
|
|
4524
|
-
return
|
|
5057
|
+
return ok22({
|
|
4525
5058
|
index: p.index,
|
|
4526
5059
|
url: targetPage.url(),
|
|
4527
5060
|
title,
|
|
@@ -4530,8 +5063,8 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4530
5063
|
}
|
|
4531
5064
|
|
|
4532
5065
|
// src/commands/addinitscript.ts
|
|
4533
|
-
import { z as
|
|
4534
|
-
import { ok as
|
|
5066
|
+
import { z as z24 } from "zod";
|
|
5067
|
+
import { ok as ok23 } from "@dyyz1993/xcli-core";
|
|
4535
5068
|
import { readFileSync as readFileSync2 } from "fs";
|
|
4536
5069
|
|
|
4537
5070
|
// src/chain-parser.ts
|
|
@@ -4709,7 +5242,7 @@ function parseCommandArgs(name, args) {
|
|
|
4709
5242
|
} else {
|
|
4710
5243
|
if (positionalIndex < positionalKeys.length) {
|
|
4711
5244
|
const isLast = positionalIndex === positionalKeys.length - 1;
|
|
4712
|
-
if (isLast && name === "eval") {
|
|
5245
|
+
if (isLast && (name === "eval" || name === "find")) {
|
|
4713
5246
|
const remaining = args.slice(i).map(unquote2).join(" ");
|
|
4714
5247
|
params[positionalKeys[positionalIndex]] = remaining;
|
|
4715
5248
|
break;
|
|
@@ -4759,7 +5292,7 @@ var commandDefCache = {
|
|
|
4759
5292
|
frames: { positional: [] },
|
|
4760
5293
|
frame: { positional: ["selector"] },
|
|
4761
5294
|
actions: { positional: ["url"] },
|
|
4762
|
-
find: { positional: ["strategy", "value"] },
|
|
5295
|
+
find: { positional: ["strategy", "value", "operation"] },
|
|
4763
5296
|
addinitscript: { positional: ["script"] },
|
|
4764
5297
|
tab: { positional: ["subcommand"] }
|
|
4765
5298
|
};
|
|
@@ -4771,14 +5304,14 @@ function registerCommandDefinition(name, positional) {
|
|
|
4771
5304
|
}
|
|
4772
5305
|
|
|
4773
5306
|
// src/commands/addinitscript.ts
|
|
4774
|
-
var InitScriptParams =
|
|
4775
|
-
script:
|
|
4776
|
-
file:
|
|
4777
|
-
stdin:
|
|
4778
|
-
name:
|
|
4779
|
-
list:
|
|
4780
|
-
remove:
|
|
4781
|
-
base64:
|
|
5307
|
+
var InitScriptParams = z24.object({
|
|
5308
|
+
script: z24.string().optional(),
|
|
5309
|
+
file: z24.string().optional(),
|
|
5310
|
+
stdin: z24.boolean().optional(),
|
|
5311
|
+
name: z24.string().optional(),
|
|
5312
|
+
list: z24.boolean().optional(),
|
|
5313
|
+
remove: z24.string().optional(),
|
|
5314
|
+
base64: z24.string().optional()
|
|
4782
5315
|
});
|
|
4783
5316
|
var registeredScripts = /* @__PURE__ */ new Map();
|
|
4784
5317
|
function resolveScriptContent(params) {
|
|
@@ -4819,15 +5352,15 @@ var addInitScriptCommand = registerCommand({
|
|
|
4819
5352
|
size: content2.length,
|
|
4820
5353
|
preview: content2.slice(0, 80)
|
|
4821
5354
|
}));
|
|
4822
|
-
return
|
|
5355
|
+
return ok23({ scripts });
|
|
4823
5356
|
}
|
|
4824
5357
|
if (params.remove) {
|
|
4825
5358
|
const existed = registeredScripts.delete(params.remove);
|
|
4826
|
-
return
|
|
5359
|
+
return ok23({ removed: params.remove, existed });
|
|
4827
5360
|
}
|
|
4828
5361
|
let content = params.stdin ? await readStdin() : resolveScriptContent(params);
|
|
4829
5362
|
if (!content) {
|
|
4830
|
-
return
|
|
5363
|
+
return ok23({ error: "No script content provided. Use --script, --file, --stdin, or --base64" });
|
|
4831
5364
|
}
|
|
4832
5365
|
const scriptName = params.name ?? `script-${Date.now()}`;
|
|
4833
5366
|
registeredScripts.set(scriptName, content);
|
|
@@ -4835,74 +5368,134 @@ var addInitScriptCommand = registerCommand({
|
|
|
4835
5368
|
try {
|
|
4836
5369
|
await ctx.page.evaluate(content);
|
|
4837
5370
|
} catch {
|
|
4838
|
-
return
|
|
5371
|
+
return ok23({
|
|
4839
5372
|
registered: scriptName,
|
|
4840
5373
|
hint: "Script registered for future page loads; immediate execution skipped (page may not be ready)"
|
|
4841
5374
|
});
|
|
4842
5375
|
}
|
|
4843
|
-
return
|
|
5376
|
+
return ok23({ registered: scriptName, executedImmediately: true });
|
|
4844
5377
|
}
|
|
4845
5378
|
});
|
|
4846
5379
|
registerCommandDefinition("addinitscript", ["script"]);
|
|
4847
5380
|
|
|
4848
5381
|
// src/commands/find.ts
|
|
4849
|
-
import { z as
|
|
4850
|
-
import { ok as
|
|
5382
|
+
import { z as z25 } from "zod";
|
|
5383
|
+
import { ok as ok24, fail as fail6 } from "@dyyz1993/xcli-core";
|
|
5384
|
+
var actionSchema2 = z25.enum(["click", "fill", "type", "select", "hover", "check"]);
|
|
4851
5385
|
var findCommand = registerCommand({
|
|
4852
5386
|
name: "find",
|
|
4853
5387
|
description: "Find elements by semantic strategy (text/role/label/placeholder/testid) and optionally perform an action",
|
|
4854
5388
|
scope: "page",
|
|
4855
|
-
parameters:
|
|
4856
|
-
strategy:
|
|
4857
|
-
value:
|
|
4858
|
-
name:
|
|
4859
|
-
exact:
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
5389
|
+
parameters: z25.object({
|
|
5390
|
+
strategy: z25.enum(["text", "role", "label", "placeholder", "testid", "alt", "title", "first", "last", "nth"]),
|
|
5391
|
+
value: z25.string(),
|
|
5392
|
+
name: z25.string().optional(),
|
|
5393
|
+
exact: z25.boolean().optional().default(false),
|
|
5394
|
+
operation: z25.string().optional().describe('Trailing operation syntax, e.g. click, fill "text", type "text"'),
|
|
5395
|
+
action: actionSchema2.optional().describe("Action to perform when not using trailing operation syntax"),
|
|
5396
|
+
actionValue: z25.string().optional().describe("Value for fill/type/select when using action"),
|
|
5397
|
+
index: z25.number().int().optional().describe("Index for nth strategy"),
|
|
5398
|
+
click: z25.boolean().optional().default(false),
|
|
5399
|
+
fill: z25.string().optional(),
|
|
5400
|
+
type: z25.string().optional(),
|
|
5401
|
+
select: z25.string().optional(),
|
|
5402
|
+
hover: z25.boolean().optional().default(false),
|
|
5403
|
+
check: z25.boolean().optional().default(false),
|
|
5404
|
+
timeout: z25.number().optional().default(1e4)
|
|
4865
5405
|
}),
|
|
4866
|
-
result:
|
|
4867
|
-
matched:
|
|
4868
|
-
selector:
|
|
4869
|
-
action:
|
|
5406
|
+
result: z25.object({
|
|
5407
|
+
matched: z25.number(),
|
|
5408
|
+
selector: z25.string(),
|
|
5409
|
+
action: z25.string().optional()
|
|
4870
5410
|
}),
|
|
4871
5411
|
handler: async (p, ctx) => {
|
|
4872
5412
|
const page = ctx.page;
|
|
4873
|
-
const
|
|
5413
|
+
const normalized = normalizeFindParams({ ...p });
|
|
5414
|
+
const parsedOperation = parseOperation(normalized.operation);
|
|
5415
|
+
const actionName = parsedOperation.action || p.action || inferLegacyAction(p);
|
|
5416
|
+
const actionValue = parsedOperation.value ?? p.actionValue ?? p.fill ?? p.type ?? p.select;
|
|
5417
|
+
const locator = buildLocator(page, normalized.strategy, normalized.value, {
|
|
4874
5418
|
name: p.name,
|
|
4875
|
-
exact: p.exact
|
|
5419
|
+
exact: p.exact,
|
|
5420
|
+
index: normalized.index
|
|
4876
5421
|
});
|
|
4877
5422
|
const count = await locator.count();
|
|
4878
5423
|
if (count === 0) {
|
|
4879
5424
|
return fail6(`No element found with ${p.strategy}="${p.value}"`);
|
|
4880
5425
|
}
|
|
4881
5426
|
const tips = [];
|
|
4882
|
-
const target = locator.
|
|
5427
|
+
const target = selectTarget(locator, p.strategy);
|
|
4883
5428
|
if (count > 1) {
|
|
4884
|
-
tips.push(`\u26A0\uFE0F Matched ${count} elements,
|
|
5429
|
+
tips.push(`\u26A0\uFE0F Matched ${count} elements, used first match. Use 'find nth <index> ${normalized.strategy} "${normalized.value}" ${actionName || "click"}' for a specific match.`);
|
|
4885
5430
|
}
|
|
4886
|
-
const selector = describeSelector(
|
|
4887
|
-
|
|
4888
|
-
if (p.click) {
|
|
5431
|
+
const selector = describeSelector(normalized.strategy, normalized.value, p.name);
|
|
5432
|
+
if (actionName === "click") {
|
|
4889
5433
|
await target.click({ timeout: p.timeout, force: true });
|
|
4890
|
-
action
|
|
4891
|
-
} else if (
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
5434
|
+
return okWithTips({ matched: count, selector, action: "click" }, tips);
|
|
5435
|
+
} else if (actionName === "fill") {
|
|
5436
|
+
if (actionValue === void 0) return fail6("find fill requires a value");
|
|
5437
|
+
await target.fill(actionValue, { timeout: p.timeout, force: true });
|
|
5438
|
+
return okWithTips({ matched: count, selector, action: `fill("${actionValue}")` }, tips);
|
|
5439
|
+
} else if (actionName === "type") {
|
|
5440
|
+
if (actionValue === void 0) return fail6("find type requires a value");
|
|
5441
|
+
await target.type(actionValue, { delay: 10, timeout: p.timeout });
|
|
5442
|
+
return okWithTips({ matched: count, selector, action: `type("${actionValue}")` }, tips);
|
|
5443
|
+
} else if (actionName === "select") {
|
|
5444
|
+
if (actionValue === void 0) return fail6("find select requires a value");
|
|
5445
|
+
await target.selectOption(actionValue);
|
|
5446
|
+
return okWithTips({ matched: count, selector, action: `select("${actionValue}")` }, tips);
|
|
5447
|
+
} else if (actionName === "hover") {
|
|
5448
|
+
await target.hover({ timeout: p.timeout, force: true });
|
|
5449
|
+
return okWithTips({ matched: count, selector, action: "hover" }, tips);
|
|
5450
|
+
} else if (actionName === "check") {
|
|
5451
|
+
await target.check({ timeout: p.timeout });
|
|
5452
|
+
return okWithTips({ matched: count, selector, action: "check" }, tips);
|
|
5453
|
+
}
|
|
5454
|
+
return okWithTips({ matched: count, selector }, tips);
|
|
4904
5455
|
}
|
|
4905
5456
|
});
|
|
5457
|
+
function okWithTips(data, tips) {
|
|
5458
|
+
const result = ok24(data);
|
|
5459
|
+
if (tips.length > 0) result.tips = tips;
|
|
5460
|
+
return result;
|
|
5461
|
+
}
|
|
5462
|
+
function parseOperation(operation) {
|
|
5463
|
+
if (!operation) return {};
|
|
5464
|
+
const match = operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5465
|
+
if (!match) return {};
|
|
5466
|
+
const maybeAction = match[1];
|
|
5467
|
+
const parsed = actionSchema2.safeParse(maybeAction);
|
|
5468
|
+
if (!parsed.success) return {};
|
|
5469
|
+
const rawValue = match[2];
|
|
5470
|
+
const value = rawValue?.replace(/^["']|["']$/g, "");
|
|
5471
|
+
return { action: parsed.data, ...value !== void 0 ? { value } : {} };
|
|
5472
|
+
}
|
|
5473
|
+
function normalizeFindParams(p) {
|
|
5474
|
+
if (p.strategy !== "nth") return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5475
|
+
const parsedIndex = Number(p.value);
|
|
5476
|
+
if (!Number.isInteger(parsedIndex) || !p.operation) {
|
|
5477
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5478
|
+
}
|
|
5479
|
+
const match = p.operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5480
|
+
if (!match) {
|
|
5481
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: parsedIndex };
|
|
5482
|
+
}
|
|
5483
|
+
return {
|
|
5484
|
+
strategy: p.strategy,
|
|
5485
|
+
value: match[1].replace(/^["']|["']$/g, ""),
|
|
5486
|
+
...match[2] ? { operation: match[2] } : {},
|
|
5487
|
+
index: parsedIndex
|
|
5488
|
+
};
|
|
5489
|
+
}
|
|
5490
|
+
function inferLegacyAction(p) {
|
|
5491
|
+
if (p.click) return "click";
|
|
5492
|
+
if (p.fill !== void 0) return "fill";
|
|
5493
|
+
if (p.type !== void 0) return "type";
|
|
5494
|
+
if (p.select !== void 0) return "select";
|
|
5495
|
+
if (p.hover) return "hover";
|
|
5496
|
+
if (p.check) return "check";
|
|
5497
|
+
return void 0;
|
|
5498
|
+
}
|
|
4906
5499
|
function buildLocator(page, strategy, value, opts) {
|
|
4907
5500
|
switch (strategy) {
|
|
4908
5501
|
case "text":
|
|
@@ -4915,10 +5508,24 @@ function buildLocator(page, strategy, value, opts) {
|
|
|
4915
5508
|
return page.getByPlaceholder(value, { exact: opts.exact });
|
|
4916
5509
|
case "testid":
|
|
4917
5510
|
return page.getByTestId(value);
|
|
5511
|
+
case "alt":
|
|
5512
|
+
return page.getByAltText(value, { exact: opts.exact });
|
|
5513
|
+
case "title":
|
|
5514
|
+
return page.getByTitle(value, { exact: opts.exact });
|
|
5515
|
+
case "first":
|
|
5516
|
+
return page.locator(value).first();
|
|
5517
|
+
case "last":
|
|
5518
|
+
return page.locator(value).last();
|
|
5519
|
+
case "nth":
|
|
5520
|
+
return page.locator(value).nth(opts.index ?? 0);
|
|
4918
5521
|
default:
|
|
4919
5522
|
return page.getByText(value, { exact: opts.exact });
|
|
4920
5523
|
}
|
|
4921
5524
|
}
|
|
5525
|
+
function selectTarget(locator, strategy) {
|
|
5526
|
+
if (strategy === "first" || strategy === "last" || strategy === "nth") return locator;
|
|
5527
|
+
return locator.first();
|
|
5528
|
+
}
|
|
4922
5529
|
function describeSelector(strategy, value, name) {
|
|
4923
5530
|
switch (strategy) {
|
|
4924
5531
|
case "role":
|
|
@@ -4931,6 +5538,16 @@ function describeSelector(strategy, value, name) {
|
|
|
4931
5538
|
return `getByPlaceholder("${value}")`;
|
|
4932
5539
|
case "testid":
|
|
4933
5540
|
return `getByTestId("${value}")`;
|
|
5541
|
+
case "alt":
|
|
5542
|
+
return `getByAltText("${value}")`;
|
|
5543
|
+
case "title":
|
|
5544
|
+
return `getByTitle("${value}")`;
|
|
5545
|
+
case "first":
|
|
5546
|
+
return `first("${value}")`;
|
|
5547
|
+
case "last":
|
|
5548
|
+
return `last("${value}")`;
|
|
5549
|
+
case "nth":
|
|
5550
|
+
return `nth("${value}")`;
|
|
4934
5551
|
default:
|
|
4935
5552
|
return `${strategy}("${value}")`;
|
|
4936
5553
|
}
|
|
@@ -5053,7 +5670,7 @@ async function detectCaptcha2(page) {
|
|
|
5053
5670
|
}
|
|
5054
5671
|
async function detectWarningText(page) {
|
|
5055
5672
|
try {
|
|
5056
|
-
const pageText = await page.textContent("body"
|
|
5673
|
+
const pageText = await page.textContent("body").catch(() => "") || "";
|
|
5057
5674
|
const lowerText = pageText.toLowerCase();
|
|
5058
5675
|
for (const { text, severity } of WARNING_TEXTS) {
|
|
5059
5676
|
if (lowerText.includes(text.toLowerCase())) {
|
|
@@ -5184,8 +5801,8 @@ function formatDetectionMessage(result) {
|
|
|
5184
5801
|
}
|
|
5185
5802
|
|
|
5186
5803
|
// src/commands/promo.ts
|
|
5187
|
-
import { z as
|
|
5188
|
-
import { ok as
|
|
5804
|
+
import { z as z26 } from "zod";
|
|
5805
|
+
import { ok as ok25 } from "@dyyz1993/xcli-core";
|
|
5189
5806
|
import { existsSync as existsSync2, readFileSync as readFileSync8 } from "fs";
|
|
5190
5807
|
import { resolve as resolve6 } from "path";
|
|
5191
5808
|
|
|
@@ -5488,14 +6105,14 @@ async function dispatchPromo(config) {
|
|
|
5488
6105
|
}
|
|
5489
6106
|
|
|
5490
6107
|
// src/commands/promo.ts
|
|
5491
|
-
var promoParams =
|
|
5492
|
-
platform:
|
|
5493
|
-
file:
|
|
5494
|
-
tags:
|
|
5495
|
-
title:
|
|
5496
|
-
search:
|
|
5497
|
-
cdpEndpoint:
|
|
5498
|
-
session:
|
|
6108
|
+
var promoParams = z26.object({
|
|
6109
|
+
platform: z26.enum(["devto", "medium", "csdn", "juejin", "quora"]).describe("Target platform for promotion"),
|
|
6110
|
+
file: z26.string().describe("Path to Markdown file to publish"),
|
|
6111
|
+
tags: z26.string().optional().describe("Comma-separated tags"),
|
|
6112
|
+
title: z26.string().optional().describe("Custom title (default: extracted from file first heading)"),
|
|
6113
|
+
search: z26.string().optional().describe("Quora: search query to find questions"),
|
|
6114
|
+
cdpEndpoint: z26.string().optional().describe("CDP endpoint for agent-browser"),
|
|
6115
|
+
session: z26.string().optional().describe("agent-browser session name")
|
|
5499
6116
|
}).refine(
|
|
5500
6117
|
(data) => data.platform !== "quora" || !!data.search,
|
|
5501
6118
|
{ message: "Quora platform requires --search parameter" }
|
|
@@ -5508,17 +6125,17 @@ var promoCommand = registerCommand({
|
|
|
5508
6125
|
description: "Publish promotional articles to various platforms (devto, medium, csdn, juejin, quora)",
|
|
5509
6126
|
scope: "project",
|
|
5510
6127
|
parameters: promoParams,
|
|
5511
|
-
result:
|
|
5512
|
-
success:
|
|
5513
|
-
url:
|
|
5514
|
-
error:
|
|
5515
|
-
platform:
|
|
6128
|
+
result: z26.object({
|
|
6129
|
+
success: z26.boolean(),
|
|
6130
|
+
url: z26.string().optional(),
|
|
6131
|
+
error: z26.string().optional(),
|
|
6132
|
+
platform: z26.string()
|
|
5516
6133
|
}),
|
|
5517
6134
|
handler: async (p, _ctx) => {
|
|
5518
6135
|
const filePath = resolve6(p.file);
|
|
5519
6136
|
const content = readFileSync8(filePath, "utf-8");
|
|
5520
6137
|
if (content.trim().length === 0) {
|
|
5521
|
-
return
|
|
6138
|
+
return ok25({
|
|
5522
6139
|
success: false,
|
|
5523
6140
|
error: `File is empty: ${filePath}`,
|
|
5524
6141
|
platform: p.platform
|
|
@@ -5533,7 +6150,7 @@ var promoCommand = registerCommand({
|
|
|
5533
6150
|
cdpEndpoint: p.cdpEndpoint ?? _ctx.cdpEndpoint,
|
|
5534
6151
|
session: p.session ?? p.platform
|
|
5535
6152
|
});
|
|
5536
|
-
return
|
|
6153
|
+
return ok25(result);
|
|
5537
6154
|
}
|
|
5538
6155
|
});
|
|
5539
6156
|
|
|
@@ -5673,6 +6290,192 @@ function ensurePluginDependencies(pluginsDir) {
|
|
|
5673
6290
|
}
|
|
5674
6291
|
}
|
|
5675
6292
|
|
|
6293
|
+
// src/plugin/contract.ts
|
|
6294
|
+
function buildPluginContract(site) {
|
|
6295
|
+
const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command));
|
|
6296
|
+
return {
|
|
6297
|
+
version: 2,
|
|
6298
|
+
plugin: {
|
|
6299
|
+
name: site.name,
|
|
6300
|
+
url: site.url,
|
|
6301
|
+
description: site.config?.description,
|
|
6302
|
+
requiresLogin: site.config?.requiresLogin
|
|
6303
|
+
},
|
|
6304
|
+
commands
|
|
6305
|
+
};
|
|
6306
|
+
}
|
|
6307
|
+
function buildCommandContract(command) {
|
|
6308
|
+
const extension = command.xbrowser || {};
|
|
6309
|
+
const inferredFields = fieldsFromZodObject(command.parameters);
|
|
6310
|
+
const fields = mergeFields(inferredFields, extension.form?.fields || []);
|
|
6311
|
+
const positional = extension.positional || fields.filter((field) => field.positional).map((field) => field.name);
|
|
6312
|
+
const capabilities = extension.capabilities || inferCapabilities(command.scope || "project", command.requiresLogin);
|
|
6313
|
+
const outputSchema = command.result ? summarizeZod(command.result) : void 0;
|
|
6314
|
+
return {
|
|
6315
|
+
name: command.name,
|
|
6316
|
+
description: command.description || "",
|
|
6317
|
+
scope: command.scope || "project",
|
|
6318
|
+
requiresLogin: command.requiresLogin === true,
|
|
6319
|
+
category: extension.category,
|
|
6320
|
+
capabilities,
|
|
6321
|
+
positional,
|
|
6322
|
+
form: {
|
|
6323
|
+
title: extension.form?.title || command.description || command.name,
|
|
6324
|
+
description: extension.form?.description,
|
|
6325
|
+
submitLabel: extension.form?.submitLabel || "Run",
|
|
6326
|
+
fields
|
|
6327
|
+
},
|
|
6328
|
+
output: extension.output || (outputSchema ? { schema: outputSchema } : void 0)
|
|
6329
|
+
};
|
|
6330
|
+
}
|
|
6331
|
+
function fieldsFromZodObject(schema) {
|
|
6332
|
+
const shape = getShape(schema);
|
|
6333
|
+
if (!shape) return [];
|
|
6334
|
+
return Object.entries(shape).map(([name, field]) => fieldFromZod(name, field));
|
|
6335
|
+
}
|
|
6336
|
+
function fieldFromZod(name, schema) {
|
|
6337
|
+
const unwrapped = unwrapZod(schema);
|
|
6338
|
+
const type = zodTypeToContractType(unwrapped.typeName);
|
|
6339
|
+
const enumValues = extractEnumValues(unwrapped.schema);
|
|
6340
|
+
return {
|
|
6341
|
+
name,
|
|
6342
|
+
label: toLabel(name),
|
|
6343
|
+
type,
|
|
6344
|
+
widget: widgetFor(type, enumValues, unwrapped.schema),
|
|
6345
|
+
required: !unwrapped.optional,
|
|
6346
|
+
...unwrapped.description ? { description: unwrapped.description } : {},
|
|
6347
|
+
...unwrapped.defaultValue !== void 0 ? { default: unwrapped.defaultValue } : {},
|
|
6348
|
+
...enumValues ? { enum: enumValues } : {},
|
|
6349
|
+
...type === "array" ? { multiple: true } : {}
|
|
6350
|
+
};
|
|
6351
|
+
}
|
|
6352
|
+
function mergeFields(inferred, overrides) {
|
|
6353
|
+
if (overrides.length === 0) return inferred;
|
|
6354
|
+
const byName = new Map(inferred.map((field) => [field.name, field]));
|
|
6355
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6356
|
+
const merged = [];
|
|
6357
|
+
for (const override of overrides) {
|
|
6358
|
+
if (!override.name) continue;
|
|
6359
|
+
const base = byName.get(override.name) || {
|
|
6360
|
+
name: override.name,
|
|
6361
|
+
label: toLabel(override.name),
|
|
6362
|
+
type: "string",
|
|
6363
|
+
widget: "text",
|
|
6364
|
+
required: false
|
|
6365
|
+
};
|
|
6366
|
+
merged.push({ ...base, ...override, name: override.name });
|
|
6367
|
+
seen.add(override.name);
|
|
6368
|
+
}
|
|
6369
|
+
for (const field of inferred) {
|
|
6370
|
+
if (!seen.has(field.name)) merged.push(field);
|
|
6371
|
+
}
|
|
6372
|
+
return merged;
|
|
6373
|
+
}
|
|
6374
|
+
function inferCapabilities(scope, requiresLogin) {
|
|
6375
|
+
const caps = [];
|
|
6376
|
+
if (scope === "page") caps.push("browser.page");
|
|
6377
|
+
if (scope === "browser") caps.push("browser.context");
|
|
6378
|
+
if (requiresLogin) caps.push("auth.login");
|
|
6379
|
+
return caps;
|
|
6380
|
+
}
|
|
6381
|
+
function getShape(schema) {
|
|
6382
|
+
const zod = schema;
|
|
6383
|
+
const shapeOrFn = zod?.shape ?? zod?._def?.shape;
|
|
6384
|
+
if (!shapeOrFn) return void 0;
|
|
6385
|
+
return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
|
|
6386
|
+
}
|
|
6387
|
+
function unwrapZod(schema) {
|
|
6388
|
+
let current = schema;
|
|
6389
|
+
let optional = typeof current?.isOptional === "function" ? current.isOptional() : false;
|
|
6390
|
+
let description = current?._def?.description;
|
|
6391
|
+
let defaultValue;
|
|
6392
|
+
for (let i = 0; i < 8; i++) {
|
|
6393
|
+
const def = current?._def;
|
|
6394
|
+
const typeName = def?.typeName || "unknown";
|
|
6395
|
+
if (def?.description) description = def.description;
|
|
6396
|
+
if (!def) return { schema: current, typeName, optional, description, defaultValue };
|
|
6397
|
+
if (typeName === "ZodDefault") {
|
|
6398
|
+
optional = true;
|
|
6399
|
+
defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
6400
|
+
current = def.innerType || def.type;
|
|
6401
|
+
continue;
|
|
6402
|
+
}
|
|
6403
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable") {
|
|
6404
|
+
optional = true;
|
|
6405
|
+
current = def.innerType || def.type;
|
|
6406
|
+
continue;
|
|
6407
|
+
}
|
|
6408
|
+
return { schema: current, typeName, optional, description, defaultValue };
|
|
6409
|
+
}
|
|
6410
|
+
return { schema: current, typeName: current?._def?.typeName || "unknown", optional, description, defaultValue };
|
|
6411
|
+
}
|
|
6412
|
+
function zodTypeToContractType(typeName) {
|
|
6413
|
+
switch (typeName) {
|
|
6414
|
+
case "ZodString":
|
|
6415
|
+
return "string";
|
|
6416
|
+
case "ZodNumber":
|
|
6417
|
+
return "number";
|
|
6418
|
+
case "ZodBoolean":
|
|
6419
|
+
return "boolean";
|
|
6420
|
+
case "ZodEnum":
|
|
6421
|
+
case "ZodNativeEnum":
|
|
6422
|
+
return "enum";
|
|
6423
|
+
case "ZodArray":
|
|
6424
|
+
return "array";
|
|
6425
|
+
case "ZodObject":
|
|
6426
|
+
return "object";
|
|
6427
|
+
default:
|
|
6428
|
+
return typeName.replace(/^Zod/, "").toLowerCase() || "unknown";
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
function widgetFor(type, enumValues, schema) {
|
|
6432
|
+
if (enumValues) return "select";
|
|
6433
|
+
if (type === "boolean") return "checkbox";
|
|
6434
|
+
if (type === "number") return "number";
|
|
6435
|
+
if (type === "array") return "multi-select";
|
|
6436
|
+
if (type === "object") return "json";
|
|
6437
|
+
const checks = schema?._def?.checks;
|
|
6438
|
+
if (checks?.some((check) => check.kind === "url")) return "url";
|
|
6439
|
+
return "text";
|
|
6440
|
+
}
|
|
6441
|
+
function extractEnumValues(schema) {
|
|
6442
|
+
const def = schema?._def;
|
|
6443
|
+
const values = def?.values;
|
|
6444
|
+
if (Array.isArray(values)) return values.map(String);
|
|
6445
|
+
return void 0;
|
|
6446
|
+
}
|
|
6447
|
+
function summarizeZod(schema) {
|
|
6448
|
+
const unwrapped = unwrapZod(schema);
|
|
6449
|
+
if (unwrapped.typeName === "ZodArray") {
|
|
6450
|
+
const def = unwrapped.schema?._def;
|
|
6451
|
+
return {
|
|
6452
|
+
type: "array",
|
|
6453
|
+
items: summarizeZod(def?.type || def?.innerType)
|
|
6454
|
+
};
|
|
6455
|
+
}
|
|
6456
|
+
const shape = getShape(schema);
|
|
6457
|
+
if (!shape) {
|
|
6458
|
+
return {
|
|
6459
|
+
type: zodTypeToContractType(unwrapped.typeName),
|
|
6460
|
+
required: !unwrapped.optional,
|
|
6461
|
+
...unwrapped.description ? { description: unwrapped.description } : {}
|
|
6462
|
+
};
|
|
6463
|
+
}
|
|
6464
|
+
return Object.fromEntries(
|
|
6465
|
+
Object.entries(shape).map(([name, field]) => {
|
|
6466
|
+
const unwrapped2 = unwrapZod(field);
|
|
6467
|
+
return [name, {
|
|
6468
|
+
type: zodTypeToContractType(unwrapped2.typeName),
|
|
6469
|
+
required: !unwrapped2.optional,
|
|
6470
|
+
...unwrapped2.description ? { description: unwrapped2.description } : {}
|
|
6471
|
+
}];
|
|
6472
|
+
})
|
|
6473
|
+
);
|
|
6474
|
+
}
|
|
6475
|
+
function toLabel(name) {
|
|
6476
|
+
return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
|
|
6477
|
+
}
|
|
6478
|
+
|
|
5676
6479
|
// src/plugin/loader.ts
|
|
5677
6480
|
var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
|
|
5678
6481
|
var XBrowserPluginLoader = class {
|
|
@@ -5715,6 +6518,13 @@ var XBrowserPluginLoader = class {
|
|
|
5715
6518
|
getLoadedPlugins() {
|
|
5716
6519
|
return this.loader.getLoadedPlugins();
|
|
5717
6520
|
}
|
|
6521
|
+
getPluginContract(siteName, commandName) {
|
|
6522
|
+
const site = this.core.loader.getSite(siteName);
|
|
6523
|
+
if (!site) return void 0;
|
|
6524
|
+
const contract = buildPluginContract(site);
|
|
6525
|
+
if (!commandName) return contract;
|
|
6526
|
+
return contract.commands.find((command) => command.name === commandName);
|
|
6527
|
+
}
|
|
5718
6528
|
async loadPlugin(pluginPath, id) {
|
|
5719
6529
|
return this.loader.loadPlugin(pluginPath, id);
|
|
5720
6530
|
}
|
|
@@ -6199,7 +7009,7 @@ function getTipsManager() {
|
|
|
6199
7009
|
|
|
6200
7010
|
// src/hooks/loader.ts
|
|
6201
7011
|
var builtinHooks = {
|
|
6202
|
-
screenshot: () => import("./screenshot-
|
|
7012
|
+
screenshot: () => import("./screenshot-CWAWMXVA.js").then((m) => m.screenshotHook)
|
|
6203
7013
|
};
|
|
6204
7014
|
async function loadHooks() {
|
|
6205
7015
|
const names = process.env.XBROWSER_HOOKS;
|
|
@@ -6285,7 +7095,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6285
7095
|
}
|
|
6286
7096
|
let targetPageOverride = null;
|
|
6287
7097
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
6288
|
-
const { findTargetPage } = await import("./browser-
|
|
7098
|
+
const { findTargetPage } = await import("./browser-GURRY444.js");
|
|
6289
7099
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
6290
7100
|
if (!targetPageOverride) {
|
|
6291
7101
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -6302,7 +7112,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6302
7112
|
params = result.data;
|
|
6303
7113
|
}
|
|
6304
7114
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
6305
|
-
const { forwardExec } = await import("./daemon-client-
|
|
7115
|
+
const { forwardExec } = await import("./daemon-client-3VM7VU7O.js");
|
|
6306
7116
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
6307
7117
|
if (result) return result;
|
|
6308
7118
|
}
|
|
@@ -6310,7 +7120,18 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6310
7120
|
const existing = await findOrRestoreSession(sessionName, extraOpts?.cdpEndpoint);
|
|
6311
7121
|
if (existing) {
|
|
6312
7122
|
session = existing;
|
|
6313
|
-
if (
|
|
7123
|
+
if (session.page) {
|
|
7124
|
+
try {
|
|
7125
|
+
await Promise.race([
|
|
7126
|
+
session.page.evaluate(() => true),
|
|
7127
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
|
|
7128
|
+
]);
|
|
7129
|
+
} catch {
|
|
7130
|
+
await closeSessionByName(session.name);
|
|
7131
|
+
session = void 0;
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7134
|
+
if (session && targetPageOverride && session.page) {
|
|
6314
7135
|
const currentUrl = session.page.url();
|
|
6315
7136
|
if (currentUrl !== targetPageOverride.url) {
|
|
6316
7137
|
await session.page.goto(targetPageOverride.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
@@ -6335,7 +7156,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6335
7156
|
browser: session?.context.browser(),
|
|
6336
7157
|
browserContext: session?.context,
|
|
6337
7158
|
sessionId: session?.id,
|
|
6338
|
-
cdpEndpoint:
|
|
7159
|
+
cdpEndpoint: session?.cdpEndpoint || extraOpts?.cdpEndpoint,
|
|
6339
7160
|
args: [],
|
|
6340
7161
|
options: {},
|
|
6341
7162
|
cwd: process.cwd(),
|
|
@@ -6370,7 +7191,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6370
7191
|
let refTips = [];
|
|
6371
7192
|
if (session?.page && command.selectorParams && command.selectorParams.length > 0) {
|
|
6372
7193
|
const cache = /* @__PURE__ */ new Map();
|
|
6373
|
-
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache);
|
|
7194
|
+
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache, session.id);
|
|
6374
7195
|
if (resolved.tips.length > 0) {
|
|
6375
7196
|
refTips = resolved.tips;
|
|
6376
7197
|
params = resolved.params;
|
|
@@ -6440,7 +7261,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6440
7261
|
timestamp: start
|
|
6441
7262
|
});
|
|
6442
7263
|
if (isSuccess) {
|
|
6443
|
-
return { ...
|
|
7264
|
+
return { ...ok26(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6444
7265
|
}
|
|
6445
7266
|
return { success: false, data: raw.data, message: raw.message, tips: merged.length > 0 ? merged : raw.tips || [], duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6446
7267
|
}
|
|
@@ -6453,7 +7274,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6453
7274
|
duration,
|
|
6454
7275
|
timestamp: start
|
|
6455
7276
|
});
|
|
6456
|
-
return { ...
|
|
7277
|
+
return { ...ok26(raw, smartTips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6457
7278
|
} catch (err) {
|
|
6458
7279
|
const end = Date.now();
|
|
6459
7280
|
const duration = end - start;
|
|
@@ -6601,7 +7422,7 @@ async function executeChain(input, options) {
|
|
|
6601
7422
|
results.push({
|
|
6602
7423
|
command: `${cmdName} ${subCommand}`,
|
|
6603
7424
|
raw: cmdStr,
|
|
6604
|
-
...
|
|
7425
|
+
...ok26(data),
|
|
6605
7426
|
duration: duration2,
|
|
6606
7427
|
...hookOutputs ? { hookOutputs } : {}
|
|
6607
7428
|
});
|
|
@@ -7334,6 +8155,12 @@ function createRPCHandler() {
|
|
|
7334
8155
|
return handleExec(params);
|
|
7335
8156
|
case "chain":
|
|
7336
8157
|
return handleChain(params);
|
|
8158
|
+
case "agent:observe":
|
|
8159
|
+
return handleAgentObserve(params);
|
|
8160
|
+
case "agent:act":
|
|
8161
|
+
return handleAgentAct(params);
|
|
8162
|
+
case "agent:wait":
|
|
8163
|
+
return handleAgentWait(params);
|
|
7337
8164
|
// ── Utility ──
|
|
7338
8165
|
case "ping":
|
|
7339
8166
|
return { ok: true, pid: process.pid };
|
|
@@ -7465,7 +8292,11 @@ function createRPCHandler() {
|
|
|
7465
8292
|
const existingSession = findSession(sessionName);
|
|
7466
8293
|
let endpoint;
|
|
7467
8294
|
if (cdp) {
|
|
7468
|
-
|
|
8295
|
+
try {
|
|
8296
|
+
endpoint = await resolveCDPEndpoint(cdp);
|
|
8297
|
+
} catch {
|
|
8298
|
+
endpoint = cdp;
|
|
8299
|
+
}
|
|
7469
8300
|
} else if (existingSession?.cdpEndpoint) {
|
|
7470
8301
|
endpoint = existingSession.cdpEndpoint;
|
|
7471
8302
|
} else {
|
|
@@ -7500,6 +8331,45 @@ function createRPCHandler() {
|
|
|
7500
8331
|
registerSessionIfNew(sessionName);
|
|
7501
8332
|
return result;
|
|
7502
8333
|
}
|
|
8334
|
+
async function handleAgentObserve(params) {
|
|
8335
|
+
const sessionName = params.session || "default";
|
|
8336
|
+
const commandParams = {
|
|
8337
|
+
includeHidden: !!params.includeHidden,
|
|
8338
|
+
...typeof params.limit === "number" ? { limit: params.limit } : {}
|
|
8339
|
+
};
|
|
8340
|
+
const result = await executeCommand("observe", commandParams, sessionName, {
|
|
8341
|
+
cdpEndpoint: params.cdpEndpoint
|
|
8342
|
+
});
|
|
8343
|
+
registerSessionIfNew(sessionName);
|
|
8344
|
+
return result;
|
|
8345
|
+
}
|
|
8346
|
+
async function handleAgentAct(params) {
|
|
8347
|
+
const sessionName = params.session || "default";
|
|
8348
|
+
const commandParams = {
|
|
8349
|
+
action: params.action || "click",
|
|
8350
|
+
force: !!params.force
|
|
8351
|
+
};
|
|
8352
|
+
for (const key of ["ref", "selector", "value", "key", "timeout"]) {
|
|
8353
|
+
if (params[key] !== void 0) commandParams[key] = params[key];
|
|
8354
|
+
}
|
|
8355
|
+
const result = await executeCommand("act", commandParams, sessionName, {
|
|
8356
|
+
cdpEndpoint: params.cdpEndpoint
|
|
8357
|
+
});
|
|
8358
|
+
registerSessionIfNew(sessionName);
|
|
8359
|
+
return result;
|
|
8360
|
+
}
|
|
8361
|
+
async function handleAgentWait(params) {
|
|
8362
|
+
const sessionName = params.session || "default";
|
|
8363
|
+
const commandParams = {};
|
|
8364
|
+
for (const key of ["selector", "state", "text", "url", "load", "fn", "screenHashChanged", "timeout", "pollInterval"]) {
|
|
8365
|
+
if (params[key] !== void 0) commandParams[key] = params[key];
|
|
8366
|
+
}
|
|
8367
|
+
const result = await executeCommand("waitFor", commandParams, sessionName, {
|
|
8368
|
+
cdpEndpoint: params.cdpEndpoint
|
|
8369
|
+
});
|
|
8370
|
+
registerSessionIfNew(sessionName);
|
|
8371
|
+
return result;
|
|
8372
|
+
}
|
|
7503
8373
|
function handleNetworkList(params) {
|
|
7504
8374
|
const sessionName = params.session || "default";
|
|
7505
8375
|
const opts = {};
|
|
@@ -8414,7 +9284,7 @@ var WSServer = class extends EventEmitter {
|
|
|
8414
9284
|
const vp = this.lastFrameViewport || initSc.page.viewportSize() || await initSc.page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
8415
9285
|
this.sendToClient(clientId, {
|
|
8416
9286
|
type: "status",
|
|
8417
|
-
data: { status: "connected", sessionId: client.sessionId || void 0, viewport: vp }
|
|
9287
|
+
data: { status: "connected", sessionId: client.sessionId || void 0, viewport: vp ?? void 0 }
|
|
8418
9288
|
});
|
|
8419
9289
|
} catch {
|
|
8420
9290
|
}
|
|
@@ -8832,7 +9702,7 @@ var WSServer = class extends EventEmitter {
|
|
|
8832
9702
|
const vp = this.lastFrameViewport || sc.page.viewportSize() || await sc.page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
8833
9703
|
this.sendToClient(clientId, {
|
|
8834
9704
|
type: "status",
|
|
8835
|
-
data: { status: "connected", sessionId, viewport: vp }
|
|
9705
|
+
data: { status: "connected", sessionId, viewport: vp ?? void 0 }
|
|
8836
9706
|
});
|
|
8837
9707
|
} catch {
|
|
8838
9708
|
}
|