@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/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
SessionRecorder
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2MFXKN32.js";
|
|
5
5
|
import {
|
|
6
6
|
closeAllSessions,
|
|
7
7
|
closeEphemeralContext,
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
resolveLaunchOpts,
|
|
18
18
|
saveSessionDiskMeta,
|
|
19
19
|
setActivePage
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-2BQZIT3S.js";
|
|
21
|
+
import "./chunk-T4J4C2NZ.js";
|
|
21
22
|
import {
|
|
22
23
|
forwardCommandLog,
|
|
23
24
|
forwardNetworkAnalyze,
|
|
@@ -46,7 +47,8 @@ import {
|
|
|
46
47
|
killAllDaemonProcesses,
|
|
47
48
|
startDaemonProcess,
|
|
48
49
|
stopDaemonProcess
|
|
49
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-IDVD44ED.js";
|
|
51
|
+
import "./chunk-3RG5ZIWI.js";
|
|
50
52
|
|
|
51
53
|
// src/router.ts
|
|
52
54
|
import { parseArgs, outputFormatter as outputFormatter2, isCommandResult as isCommandResult2, helpGenerator as helpGenerator2 } from "@dyyz1993/xcli-core";
|
|
@@ -169,7 +171,7 @@ var version = pkg.version;
|
|
|
169
171
|
|
|
170
172
|
// src/executor.ts
|
|
171
173
|
import {
|
|
172
|
-
ok as
|
|
174
|
+
ok as ok26,
|
|
173
175
|
fail as fail7,
|
|
174
176
|
isCommandResult,
|
|
175
177
|
configureArchiveStore,
|
|
@@ -535,7 +537,7 @@ var pressCommand = registerCommand({
|
|
|
535
537
|
key: z2.string()
|
|
536
538
|
}),
|
|
537
539
|
handler: async (p, ctx) => {
|
|
538
|
-
await ctx.page.press(p.selector || "body", p.key, {
|
|
540
|
+
await ctx.page.press(p.selector || "body", p.key, { timeout: 1e4 });
|
|
539
541
|
return ok2({ key: p.key });
|
|
540
542
|
}
|
|
541
543
|
});
|
|
@@ -554,7 +556,7 @@ var selectCommand = registerCommand({
|
|
|
554
556
|
}),
|
|
555
557
|
handler: async (p, ctx) => {
|
|
556
558
|
const values = typeof p.value === "string" ? [p.value] : p.value;
|
|
557
|
-
await ctx.page.selectOption(p.selector, values
|
|
559
|
+
await ctx.page.selectOption(p.selector, values);
|
|
558
560
|
return ok2({ selector: p.selector, value: p.value });
|
|
559
561
|
}
|
|
560
562
|
});
|
|
@@ -570,7 +572,7 @@ var checkCommand = registerCommand({
|
|
|
570
572
|
selector: z2.string()
|
|
571
573
|
}),
|
|
572
574
|
handler: async (p, ctx) => {
|
|
573
|
-
await ctx.page.check(p.selector, {
|
|
575
|
+
await ctx.page.check(p.selector, { timeout: 1e4 });
|
|
574
576
|
return ok2({ selector: p.selector });
|
|
575
577
|
}
|
|
576
578
|
});
|
|
@@ -587,7 +589,7 @@ var hoverCommand = registerCommand({
|
|
|
587
589
|
selector: z2.string()
|
|
588
590
|
}),
|
|
589
591
|
handler: async (p, ctx) => {
|
|
590
|
-
await ctx.page.hover(p.selector, {
|
|
592
|
+
await ctx.page.hover(p.selector, { timeout: 1e4 });
|
|
591
593
|
return ok2({ selector: p.selector });
|
|
592
594
|
}
|
|
593
595
|
});
|
|
@@ -2116,7 +2118,7 @@ var scrapeCommand = registerCommand({
|
|
|
2116
2118
|
await page.goto(p.url, { waitUntil: "commit", timeout: p.timeout });
|
|
2117
2119
|
await page.waitForSelector("body", { timeout: p.timeout }).catch(() => {
|
|
2118
2120
|
});
|
|
2119
|
-
await page.waitForLoadState("networkidle",
|
|
2121
|
+
await page.waitForLoadState("networkidle", Math.min(p.timeout, 8e3)).catch(() => {
|
|
2120
2122
|
});
|
|
2121
2123
|
await page.waitForTimeout(p.waitAfterLoad > 0 ? p.waitAfterLoad : 2e3);
|
|
2122
2124
|
if (p.selector) {
|
|
@@ -3121,7 +3123,7 @@ async function fetchFullContent(urls, timeout, cdpEndpoint) {
|
|
|
3121
3123
|
const pg = await context.newPage();
|
|
3122
3124
|
try {
|
|
3123
3125
|
await pg.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
3124
|
-
await pg.waitForLoadState("networkidle",
|
|
3126
|
+
await pg.waitForLoadState("networkidle", timeout).catch(() => {
|
|
3125
3127
|
});
|
|
3126
3128
|
const html = await pg.content();
|
|
3127
3129
|
contentMap.set(url, htmlToMarkdown(html, { onlyMainContent: true }));
|
|
@@ -3517,7 +3519,7 @@ var networkCommand = registerCommand({
|
|
|
3517
3519
|
waitUntil: "domcontentloaded",
|
|
3518
3520
|
timeout: p.timeout
|
|
3519
3521
|
});
|
|
3520
|
-
await page.waitForLoadState("networkidle",
|
|
3522
|
+
await page.waitForLoadState("networkidle", p.timeout).catch(() => {
|
|
3521
3523
|
});
|
|
3522
3524
|
await page.waitForTimeout(p.wait);
|
|
3523
3525
|
const totalCount = captures.length;
|
|
@@ -3828,6 +3830,24 @@ var ENGINE_KEY_ENUM = z20.enum(ALL_ENGINE_KEYS);
|
|
|
3828
3830
|
import { z as z21 } from "zod";
|
|
3829
3831
|
import { ok as ok20, fail as fail4 } from "@dyyz1993/xcli-core";
|
|
3830
3832
|
|
|
3833
|
+
// src/runtime/ref-store.ts
|
|
3834
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
3835
|
+
function normalizeAgentRef(ref) {
|
|
3836
|
+
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
3837
|
+
}
|
|
3838
|
+
function replaceRefs(sessionKey2, screenHash, targets) {
|
|
3839
|
+
sessions.set(sessionKey2, {
|
|
3840
|
+
screenHash,
|
|
3841
|
+
targets: new Map(targets.map((target) => [target.ref, target]))
|
|
3842
|
+
});
|
|
3843
|
+
}
|
|
3844
|
+
function getRefTarget(sessionKey2, ref) {
|
|
3845
|
+
const session = sessions.get(sessionKey2);
|
|
3846
|
+
const target = session?.targets.get(normalizeAgentRef(ref));
|
|
3847
|
+
if (!session || !target) return null;
|
|
3848
|
+
return { screenHash: session.screenHash, target };
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3831
3851
|
// src/utils/resolve-selector.ts
|
|
3832
3852
|
function buildElementSelector(el) {
|
|
3833
3853
|
function isUnique(sel) {
|
|
@@ -3999,9 +4019,9 @@ async function resolveSelectors(page, ariaSnapshot) {
|
|
|
3999
4019
|
}
|
|
4000
4020
|
return results;
|
|
4001
4021
|
}
|
|
4002
|
-
var REF_ONLY =
|
|
4022
|
+
var REF_ONLY = /^@?(e\d+)$/;
|
|
4003
4023
|
var refCache = /* @__PURE__ */ new Map();
|
|
4004
|
-
async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
4024
|
+
async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
|
|
4005
4025
|
const tips = [];
|
|
4006
4026
|
const newParams = { ...params };
|
|
4007
4027
|
if (!selectorKeys || selectorKeys.length === 0) {
|
|
@@ -4010,7 +4030,17 @@ async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
|
4010
4030
|
for (const key of selectorKeys) {
|
|
4011
4031
|
const val = params[key];
|
|
4012
4032
|
if (typeof val !== "string" || !REF_ONLY.test(val)) continue;
|
|
4013
|
-
const
|
|
4033
|
+
const match = val.match(REF_ONLY);
|
|
4034
|
+
if (!match) continue;
|
|
4035
|
+
const ref = normalizeAgentRef(match[1]);
|
|
4036
|
+
if (sessionId) {
|
|
4037
|
+
const runtimeTarget = getRefTarget(sessionId, ref);
|
|
4038
|
+
if (runtimeTarget) {
|
|
4039
|
+
tips.push(`ref=@${ref} (${key}) => ${runtimeTarget.target.selector} (observe)`);
|
|
4040
|
+
newParams[key] = runtimeTarget.target.selector;
|
|
4041
|
+
continue;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4014
4044
|
const activeCache = cache ?? refCache;
|
|
4015
4045
|
const cached = activeCache.get(ref);
|
|
4016
4046
|
if (cached) {
|
|
@@ -4291,6 +4321,404 @@ async function enhanceSemanticsWithLLM(url, ariaSnapshot, ruleBasedElements) {
|
|
|
4291
4321
|
saveSemantics(domain, pathKey, url, llmElements);
|
|
4292
4322
|
}
|
|
4293
4323
|
|
|
4324
|
+
// src/runtime/agent-runtime.ts
|
|
4325
|
+
function sessionKey(sessionId) {
|
|
4326
|
+
return sessionId || "default";
|
|
4327
|
+
}
|
|
4328
|
+
async function observePage(page, sessionId, options = {}) {
|
|
4329
|
+
const [title, raw] = await Promise.all([
|
|
4330
|
+
page.title().catch(() => ""),
|
|
4331
|
+
page.evaluate(
|
|
4332
|
+
({ includeHidden, limit }) => {
|
|
4333
|
+
function hash(input) {
|
|
4334
|
+
let h = 2166136261;
|
|
4335
|
+
for (let i = 0; i < input.length; i++) {
|
|
4336
|
+
h ^= input.charCodeAt(i);
|
|
4337
|
+
h = Math.imul(h, 16777619);
|
|
4338
|
+
}
|
|
4339
|
+
return (h >>> 0).toString(16);
|
|
4340
|
+
}
|
|
4341
|
+
function cssEscape(value) {
|
|
4342
|
+
const css = globalThis.CSS;
|
|
4343
|
+
return css?.escape ? css.escape(value) : value.replace(/["\\#.:,[\]>+~*]/g, "\\$&");
|
|
4344
|
+
}
|
|
4345
|
+
function isUnique(selector2) {
|
|
4346
|
+
try {
|
|
4347
|
+
return document.querySelectorAll(selector2).length === 1;
|
|
4348
|
+
} catch {
|
|
4349
|
+
return false;
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
function nthOfType(el) {
|
|
4353
|
+
const tag = el.tagName.toLowerCase();
|
|
4354
|
+
const parent = el.parentElement;
|
|
4355
|
+
if (!parent) return tag;
|
|
4356
|
+
const same = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
4357
|
+
if (same.length === 1) return tag;
|
|
4358
|
+
return `${tag}:nth-of-type(${same.indexOf(el) + 1})`;
|
|
4359
|
+
}
|
|
4360
|
+
function selectorFor(el) {
|
|
4361
|
+
const tag = el.tagName.toLowerCase();
|
|
4362
|
+
const id = el.getAttribute("id");
|
|
4363
|
+
if (id) {
|
|
4364
|
+
const selector2 = `#${cssEscape(id)}`;
|
|
4365
|
+
if (isUnique(selector2)) return selector2;
|
|
4366
|
+
}
|
|
4367
|
+
for (const attr of ["data-testid", "data-test", "data-qa", "name", "aria-label"]) {
|
|
4368
|
+
const value = el.getAttribute(attr);
|
|
4369
|
+
if (!value) continue;
|
|
4370
|
+
const selector2 = `[${attr}="${cssEscape(value)}"]`;
|
|
4371
|
+
if (isUnique(selector2)) return selector2;
|
|
4372
|
+
const tagged = `${tag}${selector2}`;
|
|
4373
|
+
if (isUnique(tagged)) return tagged;
|
|
4374
|
+
}
|
|
4375
|
+
const classes = Array.from(el.classList).slice(0, 3);
|
|
4376
|
+
for (const cls of classes) {
|
|
4377
|
+
const selector2 = `${tag}.${cssEscape(cls)}`;
|
|
4378
|
+
if (isUnique(selector2)) return selector2;
|
|
4379
|
+
}
|
|
4380
|
+
const parts = [];
|
|
4381
|
+
let cur = el;
|
|
4382
|
+
while (cur && cur !== document.body && cur !== document.documentElement && parts.length < 6) {
|
|
4383
|
+
parts.unshift(nthOfType(cur));
|
|
4384
|
+
const selector2 = parts.join(" > ");
|
|
4385
|
+
if (isUnique(selector2)) return selector2;
|
|
4386
|
+
cur = cur.parentElement;
|
|
4387
|
+
}
|
|
4388
|
+
return parts.join(" > ") || tag;
|
|
4389
|
+
}
|
|
4390
|
+
function roleFor(el) {
|
|
4391
|
+
const explicit = el.getAttribute("role");
|
|
4392
|
+
if (explicit) return explicit;
|
|
4393
|
+
const tag = el.tagName.toLowerCase();
|
|
4394
|
+
if (tag === "a") return "link";
|
|
4395
|
+
if (tag === "button") return "button";
|
|
4396
|
+
if (tag === "select") return "combobox";
|
|
4397
|
+
if (tag === "textarea") return "textbox";
|
|
4398
|
+
if (tag === "input") {
|
|
4399
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
4400
|
+
if (type === "checkbox") return "checkbox";
|
|
4401
|
+
if (type === "radio") return "radio";
|
|
4402
|
+
if (type === "button" || type === "submit" || type === "reset") return "button";
|
|
4403
|
+
return "textbox";
|
|
4404
|
+
}
|
|
4405
|
+
return tag;
|
|
4406
|
+
}
|
|
4407
|
+
function textName(el) {
|
|
4408
|
+
const input = el;
|
|
4409
|
+
const direct = el.getAttribute("aria-label") || el.getAttribute("placeholder") || el.getAttribute("title") || input.value || el.textContent || "";
|
|
4410
|
+
return direct.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
4411
|
+
}
|
|
4412
|
+
function actionsFor(role, editable) {
|
|
4413
|
+
const actions = [];
|
|
4414
|
+
if (editable) actions.push("fill", "type");
|
|
4415
|
+
if (role === "combobox") actions.push("select");
|
|
4416
|
+
if (role === "checkbox" || role === "radio") actions.push("check");
|
|
4417
|
+
actions.push("click", "hover");
|
|
4418
|
+
return Array.from(new Set(actions));
|
|
4419
|
+
}
|
|
4420
|
+
const selector = [
|
|
4421
|
+
"a[href]",
|
|
4422
|
+
"button",
|
|
4423
|
+
"input",
|
|
4424
|
+
"textarea",
|
|
4425
|
+
"select",
|
|
4426
|
+
"summary",
|
|
4427
|
+
"label",
|
|
4428
|
+
"[role]",
|
|
4429
|
+
"[tabindex]",
|
|
4430
|
+
'[contenteditable="true"]'
|
|
4431
|
+
].join(",");
|
|
4432
|
+
const candidates = Array.from(document.querySelectorAll(selector));
|
|
4433
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4434
|
+
const targets2 = [];
|
|
4435
|
+
for (const el of candidates) {
|
|
4436
|
+
const rect = el.getBoundingClientRect();
|
|
4437
|
+
const style = window.getComputedStyle(el);
|
|
4438
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4439
|
+
if (!includeHidden && !visible) continue;
|
|
4440
|
+
const selectorValue = selectorFor(el);
|
|
4441
|
+
if (!selectorValue || seen.has(selectorValue)) continue;
|
|
4442
|
+
seen.add(selectorValue);
|
|
4443
|
+
const input = el;
|
|
4444
|
+
const tag = el.tagName.toLowerCase();
|
|
4445
|
+
const role = roleFor(el);
|
|
4446
|
+
const editable = tag === "textarea" || tag === "select" || tag === "input" && !["button", "submit", "reset", "checkbox", "radio"].includes((input.type || "").toLowerCase()) || el.isContentEditable;
|
|
4447
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4448
|
+
const checked = typeof input.checked === "boolean" && ["checkbox", "radio"].includes((input.type || "").toLowerCase()) ? input.checked : void 0;
|
|
4449
|
+
targets2.push({
|
|
4450
|
+
selector: selectorValue,
|
|
4451
|
+
role,
|
|
4452
|
+
name: textName(el),
|
|
4453
|
+
tag,
|
|
4454
|
+
visible,
|
|
4455
|
+
enabled,
|
|
4456
|
+
editable,
|
|
4457
|
+
...checked !== void 0 ? { checked } : {},
|
|
4458
|
+
...editable && input.value ? { value: input.value.slice(0, 120) } : {},
|
|
4459
|
+
...visible ? { box: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) } } : {},
|
|
4460
|
+
actions: actionsFor(role, editable)
|
|
4461
|
+
});
|
|
4462
|
+
if (targets2.length >= limit) break;
|
|
4463
|
+
}
|
|
4464
|
+
const stateText = [
|
|
4465
|
+
location.href,
|
|
4466
|
+
document.title,
|
|
4467
|
+
document.body?.innerText?.slice(0, 5e3) || ""
|
|
4468
|
+
].join("\n");
|
|
4469
|
+
return { screenHash: hash(stateText), targets: targets2 };
|
|
4470
|
+
},
|
|
4471
|
+
{ includeHidden: !!options.includeHidden, limit: options.limit ?? 80 }
|
|
4472
|
+
)
|
|
4473
|
+
]);
|
|
4474
|
+
const targets = raw.targets.map((target, index) => ({
|
|
4475
|
+
ref: `e${index + 1}`,
|
|
4476
|
+
...target
|
|
4477
|
+
}));
|
|
4478
|
+
replaceRefs(sessionKey(sessionId), raw.screenHash, targets);
|
|
4479
|
+
return {
|
|
4480
|
+
url: page.url(),
|
|
4481
|
+
title,
|
|
4482
|
+
screenHash: raw.screenHash,
|
|
4483
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4484
|
+
targets
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
function quoteName(name) {
|
|
4488
|
+
const cleaned = name.replace(/\s+/g, " ").trim();
|
|
4489
|
+
if (!cleaned) return "";
|
|
4490
|
+
return ` "${cleaned.replace(/"/g, '\\"')}"`;
|
|
4491
|
+
}
|
|
4492
|
+
function targetFlags(target) {
|
|
4493
|
+
const flags = [target.role || target.tag];
|
|
4494
|
+
if (!target.enabled) flags.push("disabled");
|
|
4495
|
+
if (target.editable) flags.push("editable");
|
|
4496
|
+
if (target.checked !== void 0) flags.push(target.checked ? "checked" : "unchecked");
|
|
4497
|
+
return flags.join(" ");
|
|
4498
|
+
}
|
|
4499
|
+
function buildSelectorMap(observation) {
|
|
4500
|
+
return Object.fromEntries(observation.targets.map((target) => [target.ref, target.selector]));
|
|
4501
|
+
}
|
|
4502
|
+
function formatObservationCompact(observation, options = {}) {
|
|
4503
|
+
const lines = [
|
|
4504
|
+
`Page: ${observation.title || "(untitled)"}`,
|
|
4505
|
+
`URL: ${observation.url}`,
|
|
4506
|
+
`Screen: ${observation.screenHash}`,
|
|
4507
|
+
""
|
|
4508
|
+
];
|
|
4509
|
+
if (observation.targets.length === 0) {
|
|
4510
|
+
lines.push("(no interactive targets)");
|
|
4511
|
+
} else {
|
|
4512
|
+
for (const target of observation.targets) {
|
|
4513
|
+
lines.push(`@${target.ref} [${targetFlags(target)}]${quoteName(target.name)}`);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
if (options.selectors && observation.targets.length > 0) {
|
|
4517
|
+
lines.push("", "## Selectors");
|
|
4518
|
+
lines.push(observation.targets.map((target) => `${target.ref}: ${target.selector}`).join(" | "));
|
|
4519
|
+
}
|
|
4520
|
+
return lines.join("\n");
|
|
4521
|
+
}
|
|
4522
|
+
async function getPageScreenHash(page) {
|
|
4523
|
+
const observation = await page.evaluate(() => {
|
|
4524
|
+
let h = 2166136261;
|
|
4525
|
+
const input = [location.href, document.title, document.body?.innerText?.slice(0, 5e3) || ""].join("\n");
|
|
4526
|
+
for (let i = 0; i < input.length; i++) {
|
|
4527
|
+
h ^= input.charCodeAt(i);
|
|
4528
|
+
h = Math.imul(h, 16777619);
|
|
4529
|
+
}
|
|
4530
|
+
return (h >>> 0).toString(16);
|
|
4531
|
+
});
|
|
4532
|
+
return observation;
|
|
4533
|
+
}
|
|
4534
|
+
async function actionability(page, selector) {
|
|
4535
|
+
return await page.evaluate((sel) => {
|
|
4536
|
+
const el = document.querySelector(sel);
|
|
4537
|
+
if (!el) return { ok: false, reason: "not_found" };
|
|
4538
|
+
const rect = el.getBoundingClientRect();
|
|
4539
|
+
const style = window.getComputedStyle(el);
|
|
4540
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4541
|
+
if (!visible) return { ok: false, reason: "not_visible" };
|
|
4542
|
+
const input = el;
|
|
4543
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4544
|
+
if (!enabled) return { ok: false, reason: "disabled" };
|
|
4545
|
+
const cx = rect.left + rect.width / 2;
|
|
4546
|
+
const cy = rect.top + rect.height / 2;
|
|
4547
|
+
const hit = document.elementFromPoint(cx, cy);
|
|
4548
|
+
if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
|
|
4549
|
+
return { ok: false, reason: "covered" };
|
|
4550
|
+
}
|
|
4551
|
+
return { ok: true };
|
|
4552
|
+
}, selector);
|
|
4553
|
+
}
|
|
4554
|
+
async function actOnPage(page, sessionId, input) {
|
|
4555
|
+
const normalizedRef = input.ref ? normalizeAgentRef(input.ref) : void 0;
|
|
4556
|
+
const refMatch = normalizedRef ? getRefTarget(sessionKey(sessionId), normalizedRef) : null;
|
|
4557
|
+
const selector = input.selector || refMatch?.target.selector;
|
|
4558
|
+
if (!selector) {
|
|
4559
|
+
return {
|
|
4560
|
+
action: input.action,
|
|
4561
|
+
selector: "",
|
|
4562
|
+
ref: normalizedRef,
|
|
4563
|
+
success: false,
|
|
4564
|
+
reason: input.ref ? "unknown_ref" : "missing_target",
|
|
4565
|
+
message: normalizedRef ? `Ref "${input.ref}" not found. Run observe again.` : "Provide ref or selector."
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
const hash = await getPageScreenHash(page).catch(() => void 0);
|
|
4569
|
+
const stale = !!(refMatch && hash && hash !== refMatch.screenHash);
|
|
4570
|
+
if (!input.force) {
|
|
4571
|
+
const check = await actionability(page, selector);
|
|
4572
|
+
if (!check.ok) {
|
|
4573
|
+
return {
|
|
4574
|
+
action: input.action,
|
|
4575
|
+
selector,
|
|
4576
|
+
ref: normalizedRef,
|
|
4577
|
+
success: false,
|
|
4578
|
+
reason: stale ? "stale_ref" : check.reason,
|
|
4579
|
+
message: stale ? `Ref "${input.ref}" may be stale. Run observe again.` : `Target is not actionable: ${check.reason}`,
|
|
4580
|
+
stale,
|
|
4581
|
+
screenHash: hash,
|
|
4582
|
+
target: refMatch?.target
|
|
4583
|
+
};
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
const timeout = input.timeout ?? 1e4;
|
|
4587
|
+
try {
|
|
4588
|
+
switch (input.action) {
|
|
4589
|
+
case "click":
|
|
4590
|
+
await page.locator(selector).first().click({ timeout, force: !!input.force });
|
|
4591
|
+
break;
|
|
4592
|
+
case "fill":
|
|
4593
|
+
if (input.value === void 0) throw new Error("fill requires value");
|
|
4594
|
+
await page.locator(selector).first().fill(input.value, { timeout, force: !!input.force });
|
|
4595
|
+
break;
|
|
4596
|
+
case "type":
|
|
4597
|
+
if (input.value === void 0) throw new Error("type requires value");
|
|
4598
|
+
await page.locator(selector).first().pressSequentially(input.value, { timeout });
|
|
4599
|
+
break;
|
|
4600
|
+
case "press":
|
|
4601
|
+
if (!input.key) throw new Error("press requires key");
|
|
4602
|
+
await page.locator(selector).first().press(input.key, { timeout });
|
|
4603
|
+
break;
|
|
4604
|
+
case "select":
|
|
4605
|
+
if (input.value === void 0) throw new Error("select requires value");
|
|
4606
|
+
await page.locator(selector).first().selectOption(input.value);
|
|
4607
|
+
break;
|
|
4608
|
+
case "check":
|
|
4609
|
+
await page.locator(selector).first().check({ timeout });
|
|
4610
|
+
break;
|
|
4611
|
+
case "hover":
|
|
4612
|
+
await page.locator(selector).first().hover({ timeout });
|
|
4613
|
+
break;
|
|
4614
|
+
default: {
|
|
4615
|
+
const neverAction = input.action;
|
|
4616
|
+
throw new Error(`Unsupported action: ${neverAction}`);
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
} catch (error) {
|
|
4620
|
+
return {
|
|
4621
|
+
action: input.action,
|
|
4622
|
+
selector,
|
|
4623
|
+
ref: normalizedRef,
|
|
4624
|
+
success: false,
|
|
4625
|
+
reason: "browser_error",
|
|
4626
|
+
message: error.message,
|
|
4627
|
+
stale,
|
|
4628
|
+
screenHash: hash,
|
|
4629
|
+
target: refMatch?.target
|
|
4630
|
+
};
|
|
4631
|
+
}
|
|
4632
|
+
return {
|
|
4633
|
+
action: input.action,
|
|
4634
|
+
selector,
|
|
4635
|
+
ref: normalizedRef,
|
|
4636
|
+
success: true,
|
|
4637
|
+
stale,
|
|
4638
|
+
screenHash: hash,
|
|
4639
|
+
target: refMatch?.target
|
|
4640
|
+
};
|
|
4641
|
+
}
|
|
4642
|
+
function matchUrlPattern(url, pattern) {
|
|
4643
|
+
if (pattern.includes("*")) {
|
|
4644
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
4645
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
4646
|
+
}
|
|
4647
|
+
return url.includes(pattern);
|
|
4648
|
+
}
|
|
4649
|
+
async function pollUntil(timeout, pollInterval, predicate) {
|
|
4650
|
+
const startedAt = Date.now();
|
|
4651
|
+
while (Date.now() - startedAt <= timeout) {
|
|
4652
|
+
if (await predicate()) return true;
|
|
4653
|
+
await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
|
|
4654
|
+
}
|
|
4655
|
+
return false;
|
|
4656
|
+
}
|
|
4657
|
+
async function waitForPage(page, input) {
|
|
4658
|
+
const timeout = input.timeout ?? 3e4;
|
|
4659
|
+
const pollInterval = input.pollInterval ?? 200;
|
|
4660
|
+
const startedAt = Date.now();
|
|
4661
|
+
try {
|
|
4662
|
+
if (input.selector) {
|
|
4663
|
+
const state = input.state ?? "visible";
|
|
4664
|
+
await page.locator(input.selector).first().waitFor({ state, timeout });
|
|
4665
|
+
return { success: true, matched: "selector", timeout, elapsed: Date.now() - startedAt };
|
|
4666
|
+
}
|
|
4667
|
+
if (input.text) {
|
|
4668
|
+
await page.getByText(input.text).first().waitFor({ state: "visible", timeout });
|
|
4669
|
+
return { success: true, matched: "text", timeout, elapsed: Date.now() - startedAt };
|
|
4670
|
+
}
|
|
4671
|
+
if (input.url) {
|
|
4672
|
+
const matched = await pollUntil(timeout, pollInterval, async () => matchUrlPattern(page.url(), input.url));
|
|
4673
|
+
return {
|
|
4674
|
+
success: matched,
|
|
4675
|
+
matched: "url",
|
|
4676
|
+
timeout,
|
|
4677
|
+
elapsed: Date.now() - startedAt,
|
|
4678
|
+
...matched ? {} : { message: `Timed out waiting for URL pattern: ${input.url}` }
|
|
4679
|
+
};
|
|
4680
|
+
}
|
|
4681
|
+
if (input.load) {
|
|
4682
|
+
await page.waitForLoadState(input.load, timeout);
|
|
4683
|
+
return { success: true, matched: "load", timeout, elapsed: Date.now() - startedAt };
|
|
4684
|
+
}
|
|
4685
|
+
if (input.fn) {
|
|
4686
|
+
await page.waitForFunction(input.fn, void 0, { timeout });
|
|
4687
|
+
return { success: true, matched: "fn", timeout, elapsed: Date.now() - startedAt };
|
|
4688
|
+
}
|
|
4689
|
+
if (input.screenHashChanged) {
|
|
4690
|
+
let screenHash = await getPageScreenHash(page);
|
|
4691
|
+
const matched = await pollUntil(timeout, pollInterval, async () => {
|
|
4692
|
+
screenHash = await getPageScreenHash(page);
|
|
4693
|
+
return screenHash !== input.screenHashChanged;
|
|
4694
|
+
});
|
|
4695
|
+
return {
|
|
4696
|
+
success: matched,
|
|
4697
|
+
matched: "screenHashChanged",
|
|
4698
|
+
timeout,
|
|
4699
|
+
elapsed: Date.now() - startedAt,
|
|
4700
|
+
screenHash,
|
|
4701
|
+
...matched ? {} : { message: `Timed out waiting for screen hash to change: ${input.screenHashChanged}` }
|
|
4702
|
+
};
|
|
4703
|
+
}
|
|
4704
|
+
} catch (error) {
|
|
4705
|
+
return {
|
|
4706
|
+
success: false,
|
|
4707
|
+
matched: input.selector ? "selector" : input.text ? "text" : input.url ? "url" : input.load ? "load" : input.fn ? "fn" : "screenHashChanged",
|
|
4708
|
+
timeout,
|
|
4709
|
+
elapsed: Date.now() - startedAt,
|
|
4710
|
+
message: error.message
|
|
4711
|
+
};
|
|
4712
|
+
}
|
|
4713
|
+
return {
|
|
4714
|
+
success: false,
|
|
4715
|
+
matched: "selector",
|
|
4716
|
+
timeout,
|
|
4717
|
+
elapsed: Date.now() - startedAt,
|
|
4718
|
+
message: "Provide one wait predicate: selector, text, url, load, fn, or screenHashChanged."
|
|
4719
|
+
};
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4294
4722
|
// src/commands/snapshot.ts
|
|
4295
4723
|
var snapshotCommand = registerCommand({
|
|
4296
4724
|
name: "snapshot",
|
|
@@ -4300,7 +4728,14 @@ var snapshotCommand = registerCommand({
|
|
|
4300
4728
|
parameters: z21.object({
|
|
4301
4729
|
type: z21.enum(["aria", "text", "dom", "all"]).default("aria").describe("Snapshot type: aria (accessibility tree), text (visible text), dom (element summary), all (combined)"),
|
|
4302
4730
|
selector: z21.string().optional().describe("Scope to a specific element"),
|
|
4303
|
-
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree")
|
|
4731
|
+
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree"),
|
|
4732
|
+
interactive: z21.boolean().optional().default(false).describe("Return interactive agent refs only"),
|
|
4733
|
+
interactiveOnly: z21.boolean().optional().default(false).describe("Alias for interactive"),
|
|
4734
|
+
i: z21.boolean().optional().default(false).describe("Short alias for interactive"),
|
|
4735
|
+
compact: z21.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
|
|
4736
|
+
c: z21.boolean().optional().default(false).describe("Short alias for compact"),
|
|
4737
|
+
selectors: z21.boolean().optional().default(false).describe("Include ref to CSS selector map"),
|
|
4738
|
+
all: z21.boolean().optional().default(false).describe("Include hidden interactive targets when using interactive snapshot")
|
|
4304
4739
|
}),
|
|
4305
4740
|
result: z21.object({
|
|
4306
4741
|
url: z21.string(),
|
|
@@ -4313,6 +4748,18 @@ var snapshotCommand = registerCommand({
|
|
|
4313
4748
|
const page = ctx.page;
|
|
4314
4749
|
const url = page.url();
|
|
4315
4750
|
const title = await page.title().catch(() => "");
|
|
4751
|
+
if (p.interactive || p.interactiveOnly || p.i || p.compact || p.c || p.selectors) {
|
|
4752
|
+
const observation = await observePage(page, ctx.sessionId, {
|
|
4753
|
+
includeHidden: p.all
|
|
4754
|
+
});
|
|
4755
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
4756
|
+
if (p.compact || p.c || p.interactive || p.interactiveOnly || p.i) {
|
|
4757
|
+
observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4758
|
+
}
|
|
4759
|
+
return ok20(observation, [
|
|
4760
|
+
`refs refreshed for ${observation.targets.length} targets; use click @e1 or fill @e2 "text"`
|
|
4761
|
+
]);
|
|
4762
|
+
}
|
|
4316
4763
|
if (p.type === "aria") {
|
|
4317
4764
|
const aria = await captureAriaSnapshot(page, p.selector, p.depth);
|
|
4318
4765
|
const tips = await buildRefTips(page, aria);
|
|
@@ -4363,10 +4810,10 @@ async function buildRefTips(page, aria) {
|
|
|
4363
4810
|
return [];
|
|
4364
4811
|
}
|
|
4365
4812
|
}
|
|
4366
|
-
async function captureAriaSnapshot(page, selector,
|
|
4813
|
+
async function captureAriaSnapshot(page, selector, _depth) {
|
|
4367
4814
|
try {
|
|
4368
4815
|
const locator = selector ? page.locator(selector).first() : page.locator("body");
|
|
4369
|
-
return await locator.ariaSnapshot(
|
|
4816
|
+
return await locator.ariaSnapshot();
|
|
4370
4817
|
} catch {
|
|
4371
4818
|
try {
|
|
4372
4819
|
return await page.locator("body").ariaSnapshot();
|
|
@@ -4377,7 +4824,7 @@ async function captureAriaSnapshot(page, selector, depth) {
|
|
|
4377
4824
|
}
|
|
4378
4825
|
async function captureTextSnapshot(page, selector) {
|
|
4379
4826
|
if (selector) {
|
|
4380
|
-
return await page.locator(selector).first().innerText(
|
|
4827
|
+
return await page.locator(selector).first().innerText().catch(() => "");
|
|
4381
4828
|
}
|
|
4382
4829
|
return await page.evaluate(() => document.body?.innerText || "").catch(() => "");
|
|
4383
4830
|
}
|
|
@@ -4413,22 +4860,108 @@ async function captureDomSnapshot(page, selector, maxDepth) {
|
|
|
4413
4860
|
).catch(() => ({ tag: "error" }));
|
|
4414
4861
|
}
|
|
4415
4862
|
|
|
4416
|
-
// src/commands/
|
|
4863
|
+
// src/commands/agent.ts
|
|
4417
4864
|
import { z as z22 } from "zod";
|
|
4418
|
-
import { ok as ok21
|
|
4419
|
-
var
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4865
|
+
import { ok as ok21 } from "@dyyz1993/xcli-core";
|
|
4866
|
+
var observeCommand = registerCommand({
|
|
4867
|
+
name: "observe",
|
|
4868
|
+
description: "Observe the current page as structured agent targets with session refs",
|
|
4869
|
+
scope: "page",
|
|
4870
|
+
parameters: z22.object({
|
|
4871
|
+
includeHidden: z22.boolean().optional().default(false).describe("Include hidden elements in the target list"),
|
|
4872
|
+
limit: z22.number().int().positive().max(300).optional().default(80).describe("Maximum number of targets to return"),
|
|
4873
|
+
compact: z22.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
|
|
4874
|
+
selectors: z22.boolean().optional().default(false).describe("Include ref to stable CSS selector map")
|
|
4875
|
+
}),
|
|
4876
|
+
handler: async (p, ctx) => {
|
|
4877
|
+
const observation = await observePage(ctx.page, ctx.sessionId, {
|
|
4878
|
+
includeHidden: p.includeHidden,
|
|
4879
|
+
limit: p.limit
|
|
4880
|
+
});
|
|
4881
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
4882
|
+
if (p.compact) observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4883
|
+
return ok21(observation, [
|
|
4884
|
+
`refs refreshed for ${observation.targets.length} targets; use act --ref @e1 --action click or click @e1`
|
|
4885
|
+
]);
|
|
4886
|
+
}
|
|
4887
|
+
});
|
|
4888
|
+
var actCommand = registerCommand({
|
|
4889
|
+
name: "act",
|
|
4890
|
+
description: "Perform an agent action using an observe ref or explicit selector",
|
|
4891
|
+
scope: "element",
|
|
4892
|
+
selectorParams: ["selector"],
|
|
4893
|
+
parameters: z22.object({
|
|
4894
|
+
action: z22.enum(["click", "fill", "type", "press", "select", "check", "hover"]).default("click"),
|
|
4895
|
+
ref: z22.string().optional().describe("Session-scoped ref returned by observe, such as e1"),
|
|
4896
|
+
selector: z22.string().optional().describe("CSS selector fallback when no ref is available"),
|
|
4897
|
+
value: z22.string().optional().describe("Value for fill/type/select"),
|
|
4898
|
+
key: z22.string().optional().describe("Key for press"),
|
|
4899
|
+
force: z22.boolean().optional().default(false).describe("Bypass actionability checks"),
|
|
4900
|
+
timeout: z22.number().optional().default(1e4).describe("Playwright action timeout in milliseconds")
|
|
4901
|
+
}).refine((p) => !!p.ref || !!p.selector, {
|
|
4902
|
+
message: "Either ref or selector is required"
|
|
4903
|
+
}),
|
|
4904
|
+
handler: async (p, ctx) => {
|
|
4905
|
+
const result = await actOnPage(ctx.page, ctx.sessionId, { ...p });
|
|
4906
|
+
if (!result.success) {
|
|
4907
|
+
return {
|
|
4908
|
+
success: false,
|
|
4909
|
+
data: result,
|
|
4910
|
+
message: result.message || result.reason || "Action failed",
|
|
4911
|
+
tips: result.stale ? ["run observe again to refresh refs"] : []
|
|
4912
|
+
};
|
|
4913
|
+
}
|
|
4914
|
+
return ok21(result, result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []);
|
|
4915
|
+
}
|
|
4916
|
+
});
|
|
4917
|
+
var waitForCommand = registerCommand({
|
|
4918
|
+
name: "waitFor",
|
|
4919
|
+
description: "Wait for agent predicates such as text, URL, load state, selector state, or screen hash changes",
|
|
4920
|
+
scope: "page",
|
|
4921
|
+
selectorParams: ["selector"],
|
|
4922
|
+
parameters: z22.object({
|
|
4923
|
+
selector: z22.string().optional().describe("CSS selector or observe ref to wait for"),
|
|
4924
|
+
state: z22.enum(["attached", "detached", "visible", "hidden"]).optional().default("visible"),
|
|
4925
|
+
text: z22.string().optional().describe("Visible text to wait for"),
|
|
4926
|
+
url: z22.string().optional().describe("URL substring or glob pattern to wait for"),
|
|
4927
|
+
load: z22.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Load state to wait for"),
|
|
4928
|
+
fn: z22.string().optional().describe("JavaScript predicate to wait for"),
|
|
4929
|
+
screenHashChanged: z22.string().optional().describe("Previous screenHash from observe"),
|
|
4930
|
+
timeout: z22.number().optional().default(3e4),
|
|
4931
|
+
pollInterval: z22.number().optional().default(200)
|
|
4932
|
+
}).refine((p) => [p.selector, p.text, p.url, p.load, p.fn, p.screenHashChanged].filter(Boolean).length === 1, {
|
|
4933
|
+
message: "Provide exactly one wait predicate: selector, text, url, load, fn, or screenHashChanged"
|
|
4934
|
+
}),
|
|
4935
|
+
handler: async (p, ctx) => {
|
|
4936
|
+
const result = await waitForPage(ctx.page, { ...p });
|
|
4937
|
+
if (!result.success) {
|
|
4938
|
+
return {
|
|
4939
|
+
success: false,
|
|
4940
|
+
data: result,
|
|
4941
|
+
message: result.message || `Timed out waiting for ${result.matched}`,
|
|
4942
|
+
tips: []
|
|
4943
|
+
};
|
|
4944
|
+
}
|
|
4945
|
+
return ok21(result);
|
|
4946
|
+
}
|
|
4947
|
+
});
|
|
4948
|
+
|
|
4949
|
+
// src/commands/tab.ts
|
|
4950
|
+
import { z as z23 } from "zod";
|
|
4951
|
+
import { ok as ok22, fail as fail5 } from "@dyyz1993/xcli-core";
|
|
4952
|
+
var TabParams = z23.object({
|
|
4953
|
+
subcommand: z23.enum(["list", "new", "close", "switch"]),
|
|
4954
|
+
url: z23.string().optional(),
|
|
4955
|
+
index: z23.number().int().min(0).optional()
|
|
4423
4956
|
});
|
|
4424
4957
|
var tabCommand = registerCommand({
|
|
4425
4958
|
name: "tab",
|
|
4426
4959
|
description: "Manage browser tabs: list, new, close, switch",
|
|
4427
4960
|
scope: "page",
|
|
4428
4961
|
parameters: TabParams,
|
|
4429
|
-
result:
|
|
4430
|
-
success:
|
|
4431
|
-
data:
|
|
4962
|
+
result: z23.object({
|
|
4963
|
+
success: z23.boolean(),
|
|
4964
|
+
data: z23.unknown()
|
|
4432
4965
|
}),
|
|
4433
4966
|
handler: async (p, ctx) => {
|
|
4434
4967
|
const pages = ctx.browserContext.pages();
|
|
@@ -4468,7 +5001,7 @@ function handleList(pages, ctx) {
|
|
|
4468
5001
|
active: i === currentIndex
|
|
4469
5002
|
};
|
|
4470
5003
|
});
|
|
4471
|
-
return
|
|
5004
|
+
return ok22({ tabs, total: tabs.length, activeIndex: currentIndex });
|
|
4472
5005
|
}
|
|
4473
5006
|
async function handleNew(p, _pages, ctx) {
|
|
4474
5007
|
const newPage = await ctx.browserContext.newPage();
|
|
@@ -4490,7 +5023,7 @@ async function handleNew(p, _pages, ctx) {
|
|
|
4490
5023
|
const title = await newPage.title().catch(() => "");
|
|
4491
5024
|
const allPages = ctx.browserContext.pages();
|
|
4492
5025
|
const newIndex = allPages.indexOf(newPage);
|
|
4493
|
-
return
|
|
5026
|
+
return ok22({
|
|
4494
5027
|
index: newIndex >= 0 ? newIndex : allPages.length - 1,
|
|
4495
5028
|
url: newPage.url(),
|
|
4496
5029
|
title,
|
|
@@ -4518,7 +5051,7 @@ async function handleClose(p, pages, ctx) {
|
|
|
4518
5051
|
}
|
|
4519
5052
|
ctx.page = newActivePage;
|
|
4520
5053
|
}
|
|
4521
|
-
return
|
|
5054
|
+
return ok22({
|
|
4522
5055
|
closedIndex: closeIndex,
|
|
4523
5056
|
total: remainingPages.length,
|
|
4524
5057
|
activeIndex: isActivePage ? closeIndex < remainingPages.length ? closeIndex : remainingPages.length - 1 : pages.indexOf(ctx.page)
|
|
@@ -4540,7 +5073,7 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4540
5073
|
}
|
|
4541
5074
|
ctx.page = targetPage;
|
|
4542
5075
|
const title = await targetPage.title().catch(() => "");
|
|
4543
|
-
return
|
|
5076
|
+
return ok22({
|
|
4544
5077
|
index: p.index,
|
|
4545
5078
|
url: targetPage.url(),
|
|
4546
5079
|
title,
|
|
@@ -4549,8 +5082,8 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4549
5082
|
}
|
|
4550
5083
|
|
|
4551
5084
|
// src/commands/addinitscript.ts
|
|
4552
|
-
import { z as
|
|
4553
|
-
import { ok as
|
|
5085
|
+
import { z as z24 } from "zod";
|
|
5086
|
+
import { ok as ok23 } from "@dyyz1993/xcli-core";
|
|
4554
5087
|
import { readFileSync as readFileSync2 } from "fs";
|
|
4555
5088
|
|
|
4556
5089
|
// src/chain-parser.ts
|
|
@@ -4728,7 +5261,7 @@ function parseCommandArgs(name, args) {
|
|
|
4728
5261
|
} else {
|
|
4729
5262
|
if (positionalIndex < positionalKeys.length) {
|
|
4730
5263
|
const isLast = positionalIndex === positionalKeys.length - 1;
|
|
4731
|
-
if (isLast && name === "eval") {
|
|
5264
|
+
if (isLast && (name === "eval" || name === "find")) {
|
|
4732
5265
|
const remaining = args.slice(i).map(unquote2).join(" ");
|
|
4733
5266
|
params[positionalKeys[positionalIndex]] = remaining;
|
|
4734
5267
|
break;
|
|
@@ -4778,7 +5311,7 @@ var commandDefCache = {
|
|
|
4778
5311
|
frames: { positional: [] },
|
|
4779
5312
|
frame: { positional: ["selector"] },
|
|
4780
5313
|
actions: { positional: ["url"] },
|
|
4781
|
-
find: { positional: ["strategy", "value"] },
|
|
5314
|
+
find: { positional: ["strategy", "value", "operation"] },
|
|
4782
5315
|
addinitscript: { positional: ["script"] },
|
|
4783
5316
|
tab: { positional: ["subcommand"] }
|
|
4784
5317
|
};
|
|
@@ -4790,14 +5323,14 @@ function registerCommandDefinition(name, positional) {
|
|
|
4790
5323
|
}
|
|
4791
5324
|
|
|
4792
5325
|
// src/commands/addinitscript.ts
|
|
4793
|
-
var InitScriptParams =
|
|
4794
|
-
script:
|
|
4795
|
-
file:
|
|
4796
|
-
stdin:
|
|
4797
|
-
name:
|
|
4798
|
-
list:
|
|
4799
|
-
remove:
|
|
4800
|
-
base64:
|
|
5326
|
+
var InitScriptParams = z24.object({
|
|
5327
|
+
script: z24.string().optional(),
|
|
5328
|
+
file: z24.string().optional(),
|
|
5329
|
+
stdin: z24.boolean().optional(),
|
|
5330
|
+
name: z24.string().optional(),
|
|
5331
|
+
list: z24.boolean().optional(),
|
|
5332
|
+
remove: z24.string().optional(),
|
|
5333
|
+
base64: z24.string().optional()
|
|
4801
5334
|
});
|
|
4802
5335
|
var registeredScripts = /* @__PURE__ */ new Map();
|
|
4803
5336
|
function resolveScriptContent(params) {
|
|
@@ -4838,15 +5371,15 @@ var addInitScriptCommand = registerCommand({
|
|
|
4838
5371
|
size: content2.length,
|
|
4839
5372
|
preview: content2.slice(0, 80)
|
|
4840
5373
|
}));
|
|
4841
|
-
return
|
|
5374
|
+
return ok23({ scripts });
|
|
4842
5375
|
}
|
|
4843
5376
|
if (params.remove) {
|
|
4844
5377
|
const existed = registeredScripts.delete(params.remove);
|
|
4845
|
-
return
|
|
5378
|
+
return ok23({ removed: params.remove, existed });
|
|
4846
5379
|
}
|
|
4847
5380
|
let content = params.stdin ? await readStdin() : resolveScriptContent(params);
|
|
4848
5381
|
if (!content) {
|
|
4849
|
-
return
|
|
5382
|
+
return ok23({ error: "No script content provided. Use --script, --file, --stdin, or --base64" });
|
|
4850
5383
|
}
|
|
4851
5384
|
const scriptName = params.name ?? `script-${Date.now()}`;
|
|
4852
5385
|
registeredScripts.set(scriptName, content);
|
|
@@ -4854,74 +5387,134 @@ var addInitScriptCommand = registerCommand({
|
|
|
4854
5387
|
try {
|
|
4855
5388
|
await ctx.page.evaluate(content);
|
|
4856
5389
|
} catch {
|
|
4857
|
-
return
|
|
5390
|
+
return ok23({
|
|
4858
5391
|
registered: scriptName,
|
|
4859
5392
|
hint: "Script registered for future page loads; immediate execution skipped (page may not be ready)"
|
|
4860
5393
|
});
|
|
4861
5394
|
}
|
|
4862
|
-
return
|
|
5395
|
+
return ok23({ registered: scriptName, executedImmediately: true });
|
|
4863
5396
|
}
|
|
4864
5397
|
});
|
|
4865
5398
|
registerCommandDefinition("addinitscript", ["script"]);
|
|
4866
5399
|
|
|
4867
5400
|
// src/commands/find.ts
|
|
4868
|
-
import { z as
|
|
4869
|
-
import { ok as
|
|
5401
|
+
import { z as z25 } from "zod";
|
|
5402
|
+
import { ok as ok24, fail as fail6 } from "@dyyz1993/xcli-core";
|
|
5403
|
+
var actionSchema2 = z25.enum(["click", "fill", "type", "select", "hover", "check"]);
|
|
4870
5404
|
var findCommand = registerCommand({
|
|
4871
5405
|
name: "find",
|
|
4872
5406
|
description: "Find elements by semantic strategy (text/role/label/placeholder/testid) and optionally perform an action",
|
|
4873
5407
|
scope: "page",
|
|
4874
|
-
parameters:
|
|
4875
|
-
strategy:
|
|
4876
|
-
value:
|
|
4877
|
-
name:
|
|
4878
|
-
exact:
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5408
|
+
parameters: z25.object({
|
|
5409
|
+
strategy: z25.enum(["text", "role", "label", "placeholder", "testid", "alt", "title", "first", "last", "nth"]),
|
|
5410
|
+
value: z25.string(),
|
|
5411
|
+
name: z25.string().optional(),
|
|
5412
|
+
exact: z25.boolean().optional().default(false),
|
|
5413
|
+
operation: z25.string().optional().describe('Trailing operation syntax, e.g. click, fill "text", type "text"'),
|
|
5414
|
+
action: actionSchema2.optional().describe("Action to perform when not using trailing operation syntax"),
|
|
5415
|
+
actionValue: z25.string().optional().describe("Value for fill/type/select when using action"),
|
|
5416
|
+
index: z25.number().int().optional().describe("Index for nth strategy"),
|
|
5417
|
+
click: z25.boolean().optional().default(false),
|
|
5418
|
+
fill: z25.string().optional(),
|
|
5419
|
+
type: z25.string().optional(),
|
|
5420
|
+
select: z25.string().optional(),
|
|
5421
|
+
hover: z25.boolean().optional().default(false),
|
|
5422
|
+
check: z25.boolean().optional().default(false),
|
|
5423
|
+
timeout: z25.number().optional().default(1e4)
|
|
4884
5424
|
}),
|
|
4885
|
-
result:
|
|
4886
|
-
matched:
|
|
4887
|
-
selector:
|
|
4888
|
-
action:
|
|
5425
|
+
result: z25.object({
|
|
5426
|
+
matched: z25.number(),
|
|
5427
|
+
selector: z25.string(),
|
|
5428
|
+
action: z25.string().optional()
|
|
4889
5429
|
}),
|
|
4890
5430
|
handler: async (p, ctx) => {
|
|
4891
5431
|
const page = ctx.page;
|
|
4892
|
-
const
|
|
5432
|
+
const normalized = normalizeFindParams({ ...p });
|
|
5433
|
+
const parsedOperation = parseOperation(normalized.operation);
|
|
5434
|
+
const actionName = parsedOperation.action || p.action || inferLegacyAction(p);
|
|
5435
|
+
const actionValue = parsedOperation.value ?? p.actionValue ?? p.fill ?? p.type ?? p.select;
|
|
5436
|
+
const locator = buildLocator(page, normalized.strategy, normalized.value, {
|
|
4893
5437
|
name: p.name,
|
|
4894
|
-
exact: p.exact
|
|
5438
|
+
exact: p.exact,
|
|
5439
|
+
index: normalized.index
|
|
4895
5440
|
});
|
|
4896
5441
|
const count = await locator.count();
|
|
4897
5442
|
if (count === 0) {
|
|
4898
5443
|
return fail6(`No element found with ${p.strategy}="${p.value}"`);
|
|
4899
5444
|
}
|
|
4900
5445
|
const tips = [];
|
|
4901
|
-
const target = locator.
|
|
5446
|
+
const target = selectTarget(locator, p.strategy);
|
|
4902
5447
|
if (count > 1) {
|
|
4903
|
-
tips.push(`\u26A0\uFE0F Matched ${count} elements,
|
|
5448
|
+
tips.push(`\u26A0\uFE0F Matched ${count} elements, used first match. Use 'find nth <index> ${normalized.strategy} "${normalized.value}" ${actionName || "click"}' for a specific match.`);
|
|
4904
5449
|
}
|
|
4905
|
-
const selector = describeSelector(
|
|
4906
|
-
|
|
4907
|
-
if (p.click) {
|
|
5450
|
+
const selector = describeSelector(normalized.strategy, normalized.value, p.name);
|
|
5451
|
+
if (actionName === "click") {
|
|
4908
5452
|
await target.click({ timeout: p.timeout, force: true });
|
|
4909
|
-
action
|
|
4910
|
-
} else if (
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
5453
|
+
return okWithTips({ matched: count, selector, action: "click" }, tips);
|
|
5454
|
+
} else if (actionName === "fill") {
|
|
5455
|
+
if (actionValue === void 0) return fail6("find fill requires a value");
|
|
5456
|
+
await target.fill(actionValue, { timeout: p.timeout, force: true });
|
|
5457
|
+
return okWithTips({ matched: count, selector, action: `fill("${actionValue}")` }, tips);
|
|
5458
|
+
} else if (actionName === "type") {
|
|
5459
|
+
if (actionValue === void 0) return fail6("find type requires a value");
|
|
5460
|
+
await target.type(actionValue, { delay: 10, timeout: p.timeout });
|
|
5461
|
+
return okWithTips({ matched: count, selector, action: `type("${actionValue}")` }, tips);
|
|
5462
|
+
} else if (actionName === "select") {
|
|
5463
|
+
if (actionValue === void 0) return fail6("find select requires a value");
|
|
5464
|
+
await target.selectOption(actionValue);
|
|
5465
|
+
return okWithTips({ matched: count, selector, action: `select("${actionValue}")` }, tips);
|
|
5466
|
+
} else if (actionName === "hover") {
|
|
5467
|
+
await target.hover({ timeout: p.timeout, force: true });
|
|
5468
|
+
return okWithTips({ matched: count, selector, action: "hover" }, tips);
|
|
5469
|
+
} else if (actionName === "check") {
|
|
5470
|
+
await target.check({ timeout: p.timeout });
|
|
5471
|
+
return okWithTips({ matched: count, selector, action: "check" }, tips);
|
|
5472
|
+
}
|
|
5473
|
+
return okWithTips({ matched: count, selector }, tips);
|
|
4923
5474
|
}
|
|
4924
5475
|
});
|
|
5476
|
+
function okWithTips(data, tips) {
|
|
5477
|
+
const result = ok24(data);
|
|
5478
|
+
if (tips.length > 0) result.tips = tips;
|
|
5479
|
+
return result;
|
|
5480
|
+
}
|
|
5481
|
+
function parseOperation(operation) {
|
|
5482
|
+
if (!operation) return {};
|
|
5483
|
+
const match = operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5484
|
+
if (!match) return {};
|
|
5485
|
+
const maybeAction = match[1];
|
|
5486
|
+
const parsed = actionSchema2.safeParse(maybeAction);
|
|
5487
|
+
if (!parsed.success) return {};
|
|
5488
|
+
const rawValue = match[2];
|
|
5489
|
+
const value = rawValue?.replace(/^["']|["']$/g, "");
|
|
5490
|
+
return { action: parsed.data, ...value !== void 0 ? { value } : {} };
|
|
5491
|
+
}
|
|
5492
|
+
function normalizeFindParams(p) {
|
|
5493
|
+
if (p.strategy !== "nth") return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5494
|
+
const parsedIndex = Number(p.value);
|
|
5495
|
+
if (!Number.isInteger(parsedIndex) || !p.operation) {
|
|
5496
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5497
|
+
}
|
|
5498
|
+
const match = p.operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5499
|
+
if (!match) {
|
|
5500
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: parsedIndex };
|
|
5501
|
+
}
|
|
5502
|
+
return {
|
|
5503
|
+
strategy: p.strategy,
|
|
5504
|
+
value: match[1].replace(/^["']|["']$/g, ""),
|
|
5505
|
+
...match[2] ? { operation: match[2] } : {},
|
|
5506
|
+
index: parsedIndex
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
function inferLegacyAction(p) {
|
|
5510
|
+
if (p.click) return "click";
|
|
5511
|
+
if (p.fill !== void 0) return "fill";
|
|
5512
|
+
if (p.type !== void 0) return "type";
|
|
5513
|
+
if (p.select !== void 0) return "select";
|
|
5514
|
+
if (p.hover) return "hover";
|
|
5515
|
+
if (p.check) return "check";
|
|
5516
|
+
return void 0;
|
|
5517
|
+
}
|
|
4925
5518
|
function buildLocator(page, strategy, value, opts) {
|
|
4926
5519
|
switch (strategy) {
|
|
4927
5520
|
case "text":
|
|
@@ -4934,10 +5527,24 @@ function buildLocator(page, strategy, value, opts) {
|
|
|
4934
5527
|
return page.getByPlaceholder(value, { exact: opts.exact });
|
|
4935
5528
|
case "testid":
|
|
4936
5529
|
return page.getByTestId(value);
|
|
5530
|
+
case "alt":
|
|
5531
|
+
return page.getByAltText(value, { exact: opts.exact });
|
|
5532
|
+
case "title":
|
|
5533
|
+
return page.getByTitle(value, { exact: opts.exact });
|
|
5534
|
+
case "first":
|
|
5535
|
+
return page.locator(value).first();
|
|
5536
|
+
case "last":
|
|
5537
|
+
return page.locator(value).last();
|
|
5538
|
+
case "nth":
|
|
5539
|
+
return page.locator(value).nth(opts.index ?? 0);
|
|
4937
5540
|
default:
|
|
4938
5541
|
return page.getByText(value, { exact: opts.exact });
|
|
4939
5542
|
}
|
|
4940
5543
|
}
|
|
5544
|
+
function selectTarget(locator, strategy) {
|
|
5545
|
+
if (strategy === "first" || strategy === "last" || strategy === "nth") return locator;
|
|
5546
|
+
return locator.first();
|
|
5547
|
+
}
|
|
4941
5548
|
function describeSelector(strategy, value, name) {
|
|
4942
5549
|
switch (strategy) {
|
|
4943
5550
|
case "role":
|
|
@@ -4950,6 +5557,16 @@ function describeSelector(strategy, value, name) {
|
|
|
4950
5557
|
return `getByPlaceholder("${value}")`;
|
|
4951
5558
|
case "testid":
|
|
4952
5559
|
return `getByTestId("${value}")`;
|
|
5560
|
+
case "alt":
|
|
5561
|
+
return `getByAltText("${value}")`;
|
|
5562
|
+
case "title":
|
|
5563
|
+
return `getByTitle("${value}")`;
|
|
5564
|
+
case "first":
|
|
5565
|
+
return `first("${value}")`;
|
|
5566
|
+
case "last":
|
|
5567
|
+
return `last("${value}")`;
|
|
5568
|
+
case "nth":
|
|
5569
|
+
return `nth("${value}")`;
|
|
4953
5570
|
default:
|
|
4954
5571
|
return `${strategy}("${value}")`;
|
|
4955
5572
|
}
|
|
@@ -5072,7 +5689,7 @@ async function detectCaptcha2(page) {
|
|
|
5072
5689
|
}
|
|
5073
5690
|
async function detectWarningText(page) {
|
|
5074
5691
|
try {
|
|
5075
|
-
const pageText = await page.textContent("body"
|
|
5692
|
+
const pageText = await page.textContent("body").catch(() => "") || "";
|
|
5076
5693
|
const lowerText = pageText.toLowerCase();
|
|
5077
5694
|
for (const { text, severity } of WARNING_TEXTS) {
|
|
5078
5695
|
if (lowerText.includes(text.toLowerCase())) {
|
|
@@ -5203,8 +5820,8 @@ function formatDetectionMessage(result) {
|
|
|
5203
5820
|
}
|
|
5204
5821
|
|
|
5205
5822
|
// src/commands/promo.ts
|
|
5206
|
-
import { z as
|
|
5207
|
-
import { ok as
|
|
5823
|
+
import { z as z26 } from "zod";
|
|
5824
|
+
import { ok as ok25 } from "@dyyz1993/xcli-core";
|
|
5208
5825
|
import { existsSync as existsSync2, readFileSync as readFileSync8 } from "fs";
|
|
5209
5826
|
import { resolve as resolve6 } from "path";
|
|
5210
5827
|
|
|
@@ -5507,14 +6124,14 @@ async function dispatchPromo(config) {
|
|
|
5507
6124
|
}
|
|
5508
6125
|
|
|
5509
6126
|
// src/commands/promo.ts
|
|
5510
|
-
var promoParams =
|
|
5511
|
-
platform:
|
|
5512
|
-
file:
|
|
5513
|
-
tags:
|
|
5514
|
-
title:
|
|
5515
|
-
search:
|
|
5516
|
-
cdpEndpoint:
|
|
5517
|
-
session:
|
|
6127
|
+
var promoParams = z26.object({
|
|
6128
|
+
platform: z26.enum(["devto", "medium", "csdn", "juejin", "quora"]).describe("Target platform for promotion"),
|
|
6129
|
+
file: z26.string().describe("Path to Markdown file to publish"),
|
|
6130
|
+
tags: z26.string().optional().describe("Comma-separated tags"),
|
|
6131
|
+
title: z26.string().optional().describe("Custom title (default: extracted from file first heading)"),
|
|
6132
|
+
search: z26.string().optional().describe("Quora: search query to find questions"),
|
|
6133
|
+
cdpEndpoint: z26.string().optional().describe("CDP endpoint for agent-browser"),
|
|
6134
|
+
session: z26.string().optional().describe("agent-browser session name")
|
|
5518
6135
|
}).refine(
|
|
5519
6136
|
(data) => data.platform !== "quora" || !!data.search,
|
|
5520
6137
|
{ message: "Quora platform requires --search parameter" }
|
|
@@ -5527,17 +6144,17 @@ var promoCommand = registerCommand({
|
|
|
5527
6144
|
description: "Publish promotional articles to various platforms (devto, medium, csdn, juejin, quora)",
|
|
5528
6145
|
scope: "project",
|
|
5529
6146
|
parameters: promoParams,
|
|
5530
|
-
result:
|
|
5531
|
-
success:
|
|
5532
|
-
url:
|
|
5533
|
-
error:
|
|
5534
|
-
platform:
|
|
6147
|
+
result: z26.object({
|
|
6148
|
+
success: z26.boolean(),
|
|
6149
|
+
url: z26.string().optional(),
|
|
6150
|
+
error: z26.string().optional(),
|
|
6151
|
+
platform: z26.string()
|
|
5535
6152
|
}),
|
|
5536
6153
|
handler: async (p, _ctx) => {
|
|
5537
6154
|
const filePath = resolve6(p.file);
|
|
5538
6155
|
const content = readFileSync8(filePath, "utf-8");
|
|
5539
6156
|
if (content.trim().length === 0) {
|
|
5540
|
-
return
|
|
6157
|
+
return ok25({
|
|
5541
6158
|
success: false,
|
|
5542
6159
|
error: `File is empty: ${filePath}`,
|
|
5543
6160
|
platform: p.platform
|
|
@@ -5552,7 +6169,7 @@ var promoCommand = registerCommand({
|
|
|
5552
6169
|
cdpEndpoint: p.cdpEndpoint ?? _ctx.cdpEndpoint,
|
|
5553
6170
|
session: p.session ?? p.platform
|
|
5554
6171
|
});
|
|
5555
|
-
return
|
|
6172
|
+
return ok25(result);
|
|
5556
6173
|
}
|
|
5557
6174
|
});
|
|
5558
6175
|
|
|
@@ -5692,6 +6309,192 @@ function ensurePluginDependencies(pluginsDir) {
|
|
|
5692
6309
|
}
|
|
5693
6310
|
}
|
|
5694
6311
|
|
|
6312
|
+
// src/plugin/contract.ts
|
|
6313
|
+
function buildPluginContract(site) {
|
|
6314
|
+
const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command));
|
|
6315
|
+
return {
|
|
6316
|
+
version: 2,
|
|
6317
|
+
plugin: {
|
|
6318
|
+
name: site.name,
|
|
6319
|
+
url: site.url,
|
|
6320
|
+
description: site.config?.description,
|
|
6321
|
+
requiresLogin: site.config?.requiresLogin
|
|
6322
|
+
},
|
|
6323
|
+
commands
|
|
6324
|
+
};
|
|
6325
|
+
}
|
|
6326
|
+
function buildCommandContract(command) {
|
|
6327
|
+
const extension = command.xbrowser || {};
|
|
6328
|
+
const inferredFields = fieldsFromZodObject(command.parameters);
|
|
6329
|
+
const fields = mergeFields(inferredFields, extension.form?.fields || []);
|
|
6330
|
+
const positional = extension.positional || fields.filter((field) => field.positional).map((field) => field.name);
|
|
6331
|
+
const capabilities = extension.capabilities || inferCapabilities(command.scope || "project", command.requiresLogin);
|
|
6332
|
+
const outputSchema = command.result ? summarizeZod(command.result) : void 0;
|
|
6333
|
+
return {
|
|
6334
|
+
name: command.name,
|
|
6335
|
+
description: command.description || "",
|
|
6336
|
+
scope: command.scope || "project",
|
|
6337
|
+
requiresLogin: command.requiresLogin === true,
|
|
6338
|
+
category: extension.category,
|
|
6339
|
+
capabilities,
|
|
6340
|
+
positional,
|
|
6341
|
+
form: {
|
|
6342
|
+
title: extension.form?.title || command.description || command.name,
|
|
6343
|
+
description: extension.form?.description,
|
|
6344
|
+
submitLabel: extension.form?.submitLabel || "Run",
|
|
6345
|
+
fields
|
|
6346
|
+
},
|
|
6347
|
+
output: extension.output || (outputSchema ? { schema: outputSchema } : void 0)
|
|
6348
|
+
};
|
|
6349
|
+
}
|
|
6350
|
+
function fieldsFromZodObject(schema) {
|
|
6351
|
+
const shape = getShape(schema);
|
|
6352
|
+
if (!shape) return [];
|
|
6353
|
+
return Object.entries(shape).map(([name, field]) => fieldFromZod(name, field));
|
|
6354
|
+
}
|
|
6355
|
+
function fieldFromZod(name, schema) {
|
|
6356
|
+
const unwrapped = unwrapZod(schema);
|
|
6357
|
+
const type = zodTypeToContractType(unwrapped.typeName);
|
|
6358
|
+
const enumValues = extractEnumValues(unwrapped.schema);
|
|
6359
|
+
return {
|
|
6360
|
+
name,
|
|
6361
|
+
label: toLabel(name),
|
|
6362
|
+
type,
|
|
6363
|
+
widget: widgetFor(type, enumValues, unwrapped.schema),
|
|
6364
|
+
required: !unwrapped.optional,
|
|
6365
|
+
...unwrapped.description ? { description: unwrapped.description } : {},
|
|
6366
|
+
...unwrapped.defaultValue !== void 0 ? { default: unwrapped.defaultValue } : {},
|
|
6367
|
+
...enumValues ? { enum: enumValues } : {},
|
|
6368
|
+
...type === "array" ? { multiple: true } : {}
|
|
6369
|
+
};
|
|
6370
|
+
}
|
|
6371
|
+
function mergeFields(inferred, overrides) {
|
|
6372
|
+
if (overrides.length === 0) return inferred;
|
|
6373
|
+
const byName = new Map(inferred.map((field) => [field.name, field]));
|
|
6374
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6375
|
+
const merged = [];
|
|
6376
|
+
for (const override of overrides) {
|
|
6377
|
+
if (!override.name) continue;
|
|
6378
|
+
const base = byName.get(override.name) || {
|
|
6379
|
+
name: override.name,
|
|
6380
|
+
label: toLabel(override.name),
|
|
6381
|
+
type: "string",
|
|
6382
|
+
widget: "text",
|
|
6383
|
+
required: false
|
|
6384
|
+
};
|
|
6385
|
+
merged.push({ ...base, ...override, name: override.name });
|
|
6386
|
+
seen.add(override.name);
|
|
6387
|
+
}
|
|
6388
|
+
for (const field of inferred) {
|
|
6389
|
+
if (!seen.has(field.name)) merged.push(field);
|
|
6390
|
+
}
|
|
6391
|
+
return merged;
|
|
6392
|
+
}
|
|
6393
|
+
function inferCapabilities(scope, requiresLogin) {
|
|
6394
|
+
const caps = [];
|
|
6395
|
+
if (scope === "page") caps.push("browser.page");
|
|
6396
|
+
if (scope === "browser") caps.push("browser.context");
|
|
6397
|
+
if (requiresLogin) caps.push("auth.login");
|
|
6398
|
+
return caps;
|
|
6399
|
+
}
|
|
6400
|
+
function getShape(schema) {
|
|
6401
|
+
const zod = schema;
|
|
6402
|
+
const shapeOrFn = zod?.shape ?? zod?._def?.shape;
|
|
6403
|
+
if (!shapeOrFn) return void 0;
|
|
6404
|
+
return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
|
|
6405
|
+
}
|
|
6406
|
+
function unwrapZod(schema) {
|
|
6407
|
+
let current = schema;
|
|
6408
|
+
let optional = typeof current?.isOptional === "function" ? current.isOptional() : false;
|
|
6409
|
+
let description = current?._def?.description;
|
|
6410
|
+
let defaultValue;
|
|
6411
|
+
for (let i = 0; i < 8; i++) {
|
|
6412
|
+
const def = current?._def;
|
|
6413
|
+
const typeName = def?.typeName || "unknown";
|
|
6414
|
+
if (def?.description) description = def.description;
|
|
6415
|
+
if (!def) return { schema: current, typeName, optional, description, defaultValue };
|
|
6416
|
+
if (typeName === "ZodDefault") {
|
|
6417
|
+
optional = true;
|
|
6418
|
+
defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
6419
|
+
current = def.innerType || def.type;
|
|
6420
|
+
continue;
|
|
6421
|
+
}
|
|
6422
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable") {
|
|
6423
|
+
optional = true;
|
|
6424
|
+
current = def.innerType || def.type;
|
|
6425
|
+
continue;
|
|
6426
|
+
}
|
|
6427
|
+
return { schema: current, typeName, optional, description, defaultValue };
|
|
6428
|
+
}
|
|
6429
|
+
return { schema: current, typeName: current?._def?.typeName || "unknown", optional, description, defaultValue };
|
|
6430
|
+
}
|
|
6431
|
+
function zodTypeToContractType(typeName) {
|
|
6432
|
+
switch (typeName) {
|
|
6433
|
+
case "ZodString":
|
|
6434
|
+
return "string";
|
|
6435
|
+
case "ZodNumber":
|
|
6436
|
+
return "number";
|
|
6437
|
+
case "ZodBoolean":
|
|
6438
|
+
return "boolean";
|
|
6439
|
+
case "ZodEnum":
|
|
6440
|
+
case "ZodNativeEnum":
|
|
6441
|
+
return "enum";
|
|
6442
|
+
case "ZodArray":
|
|
6443
|
+
return "array";
|
|
6444
|
+
case "ZodObject":
|
|
6445
|
+
return "object";
|
|
6446
|
+
default:
|
|
6447
|
+
return typeName.replace(/^Zod/, "").toLowerCase() || "unknown";
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
function widgetFor(type, enumValues, schema) {
|
|
6451
|
+
if (enumValues) return "select";
|
|
6452
|
+
if (type === "boolean") return "checkbox";
|
|
6453
|
+
if (type === "number") return "number";
|
|
6454
|
+
if (type === "array") return "multi-select";
|
|
6455
|
+
if (type === "object") return "json";
|
|
6456
|
+
const checks = schema?._def?.checks;
|
|
6457
|
+
if (checks?.some((check) => check.kind === "url")) return "url";
|
|
6458
|
+
return "text";
|
|
6459
|
+
}
|
|
6460
|
+
function extractEnumValues(schema) {
|
|
6461
|
+
const def = schema?._def;
|
|
6462
|
+
const values = def?.values;
|
|
6463
|
+
if (Array.isArray(values)) return values.map(String);
|
|
6464
|
+
return void 0;
|
|
6465
|
+
}
|
|
6466
|
+
function summarizeZod(schema) {
|
|
6467
|
+
const unwrapped = unwrapZod(schema);
|
|
6468
|
+
if (unwrapped.typeName === "ZodArray") {
|
|
6469
|
+
const def = unwrapped.schema?._def;
|
|
6470
|
+
return {
|
|
6471
|
+
type: "array",
|
|
6472
|
+
items: summarizeZod(def?.type || def?.innerType)
|
|
6473
|
+
};
|
|
6474
|
+
}
|
|
6475
|
+
const shape = getShape(schema);
|
|
6476
|
+
if (!shape) {
|
|
6477
|
+
return {
|
|
6478
|
+
type: zodTypeToContractType(unwrapped.typeName),
|
|
6479
|
+
required: !unwrapped.optional,
|
|
6480
|
+
...unwrapped.description ? { description: unwrapped.description } : {}
|
|
6481
|
+
};
|
|
6482
|
+
}
|
|
6483
|
+
return Object.fromEntries(
|
|
6484
|
+
Object.entries(shape).map(([name, field]) => {
|
|
6485
|
+
const unwrapped2 = unwrapZod(field);
|
|
6486
|
+
return [name, {
|
|
6487
|
+
type: zodTypeToContractType(unwrapped2.typeName),
|
|
6488
|
+
required: !unwrapped2.optional,
|
|
6489
|
+
...unwrapped2.description ? { description: unwrapped2.description } : {}
|
|
6490
|
+
}];
|
|
6491
|
+
})
|
|
6492
|
+
);
|
|
6493
|
+
}
|
|
6494
|
+
function toLabel(name) {
|
|
6495
|
+
return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
|
|
6496
|
+
}
|
|
6497
|
+
|
|
5695
6498
|
// src/plugin/loader.ts
|
|
5696
6499
|
var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
|
|
5697
6500
|
var XBrowserPluginLoader = class {
|
|
@@ -5734,6 +6537,13 @@ var XBrowserPluginLoader = class {
|
|
|
5734
6537
|
getLoadedPlugins() {
|
|
5735
6538
|
return this.loader.getLoadedPlugins();
|
|
5736
6539
|
}
|
|
6540
|
+
getPluginContract(siteName, commandName) {
|
|
6541
|
+
const site = this.core.loader.getSite(siteName);
|
|
6542
|
+
if (!site) return void 0;
|
|
6543
|
+
const contract = buildPluginContract(site);
|
|
6544
|
+
if (!commandName) return contract;
|
|
6545
|
+
return contract.commands.find((command) => command.name === commandName);
|
|
6546
|
+
}
|
|
5737
6547
|
async loadPlugin(pluginPath, id) {
|
|
5738
6548
|
return this.loader.loadPlugin(pluginPath, id);
|
|
5739
6549
|
}
|
|
@@ -6218,7 +7028,7 @@ function getTipsManager() {
|
|
|
6218
7028
|
|
|
6219
7029
|
// src/hooks/loader.ts
|
|
6220
7030
|
var builtinHooks = {
|
|
6221
|
-
screenshot: () => import("./screenshot-
|
|
7031
|
+
screenshot: () => import("./screenshot-CWAWMXVA.js").then((m) => m.screenshotHook)
|
|
6222
7032
|
};
|
|
6223
7033
|
async function loadHooks() {
|
|
6224
7034
|
const names = process.env.XBROWSER_HOOKS;
|
|
@@ -6304,7 +7114,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6304
7114
|
}
|
|
6305
7115
|
let targetPageOverride = null;
|
|
6306
7116
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
6307
|
-
const { findTargetPage } = await import("./browser-
|
|
7117
|
+
const { findTargetPage } = await import("./browser-DSVV4GHS.js");
|
|
6308
7118
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
6309
7119
|
if (!targetPageOverride) {
|
|
6310
7120
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -6321,7 +7131,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6321
7131
|
params = result.data;
|
|
6322
7132
|
}
|
|
6323
7133
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
6324
|
-
const { forwardExec } = await import("./daemon-client-
|
|
7134
|
+
const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
|
|
6325
7135
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
6326
7136
|
if (result) return result;
|
|
6327
7137
|
}
|
|
@@ -6329,7 +7139,18 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6329
7139
|
const existing = await findOrRestoreSession(sessionName, extraOpts?.cdpEndpoint);
|
|
6330
7140
|
if (existing) {
|
|
6331
7141
|
session = existing;
|
|
6332
|
-
if (
|
|
7142
|
+
if (session.page) {
|
|
7143
|
+
try {
|
|
7144
|
+
await Promise.race([
|
|
7145
|
+
session.page.evaluate(() => true),
|
|
7146
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
|
|
7147
|
+
]);
|
|
7148
|
+
} catch {
|
|
7149
|
+
await closeSessionByName(session.name);
|
|
7150
|
+
session = void 0;
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7153
|
+
if (session && targetPageOverride && session.page) {
|
|
6333
7154
|
const currentUrl = session.page.url();
|
|
6334
7155
|
if (currentUrl !== targetPageOverride.url) {
|
|
6335
7156
|
await session.page.goto(targetPageOverride.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
@@ -6354,7 +7175,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6354
7175
|
browser: session?.context.browser(),
|
|
6355
7176
|
browserContext: session?.context,
|
|
6356
7177
|
sessionId: session?.id,
|
|
6357
|
-
cdpEndpoint:
|
|
7178
|
+
cdpEndpoint: session?.cdpEndpoint || extraOpts?.cdpEndpoint,
|
|
6358
7179
|
args: [],
|
|
6359
7180
|
options: {},
|
|
6360
7181
|
cwd: process.cwd(),
|
|
@@ -6389,7 +7210,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6389
7210
|
let refTips = [];
|
|
6390
7211
|
if (session?.page && command.selectorParams && command.selectorParams.length > 0) {
|
|
6391
7212
|
const cache = /* @__PURE__ */ new Map();
|
|
6392
|
-
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache);
|
|
7213
|
+
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache, session.id);
|
|
6393
7214
|
if (resolved.tips.length > 0) {
|
|
6394
7215
|
refTips = resolved.tips;
|
|
6395
7216
|
params = resolved.params;
|
|
@@ -6459,7 +7280,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6459
7280
|
timestamp: start
|
|
6460
7281
|
});
|
|
6461
7282
|
if (isSuccess) {
|
|
6462
|
-
return { ...
|
|
7283
|
+
return { ...ok26(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6463
7284
|
}
|
|
6464
7285
|
return { success: false, data: raw.data, message: raw.message, tips: merged.length > 0 ? merged : raw.tips || [], duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6465
7286
|
}
|
|
@@ -6472,7 +7293,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6472
7293
|
duration,
|
|
6473
7294
|
timestamp: start
|
|
6474
7295
|
});
|
|
6475
|
-
return { ...
|
|
7296
|
+
return { ...ok26(raw, smartTips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6476
7297
|
} catch (err) {
|
|
6477
7298
|
const end = Date.now();
|
|
6478
7299
|
const duration = end - start;
|
|
@@ -6620,7 +7441,7 @@ async function executeChain(input, options) {
|
|
|
6620
7441
|
results.push({
|
|
6621
7442
|
command: `${cmdName} ${subCommand}`,
|
|
6622
7443
|
raw: cmdStr,
|
|
6623
|
-
...
|
|
7444
|
+
...ok26(data),
|
|
6624
7445
|
duration: duration2,
|
|
6625
7446
|
...hookOutputs ? { hookOutputs } : {}
|
|
6626
7447
|
});
|
|
@@ -6837,13 +7658,13 @@ var sessionListBuiltin = {
|
|
|
6837
7658
|
},
|
|
6838
7659
|
execute: async () => {
|
|
6839
7660
|
try {
|
|
6840
|
-
const
|
|
6841
|
-
if (
|
|
7661
|
+
const sessions2 = await listSessions();
|
|
7662
|
+
if (sessions2.length === 0) {
|
|
6842
7663
|
console.log("No active sessions");
|
|
6843
7664
|
return;
|
|
6844
7665
|
}
|
|
6845
7666
|
console.log("Active sessions:");
|
|
6846
|
-
for (const s of
|
|
7667
|
+
for (const s of sessions2) {
|
|
6847
7668
|
console.log(` ${s.name} (${s.id})`);
|
|
6848
7669
|
}
|
|
6849
7670
|
} catch (e) {
|
|
@@ -7731,6 +8552,7 @@ function handlePluginHelp() {
|
|
|
7731
8552
|
" install <slug> --from-marketplace Install from marketplace",
|
|
7732
8553
|
" uninstall <name> Uninstall a plugin",
|
|
7733
8554
|
" list [--json] List installed plugins",
|
|
8555
|
+
" schema <name> [command] [--json] Show plugin contract and command forms",
|
|
7734
8556
|
" reload <name> Reload a plugin",
|
|
7735
8557
|
"",
|
|
7736
8558
|
"Examples:",
|
|
@@ -7739,6 +8561,7 @@ function handlePluginHelp() {
|
|
|
7739
8561
|
" xbrowser plugin install ./my-plugin",
|
|
7740
8562
|
" xbrowser plugin uninstall my-plugin",
|
|
7741
8563
|
" xbrowser plugin list",
|
|
8564
|
+
" xbrowser plugin schema my-plugin --json",
|
|
7742
8565
|
" xbrowser plugin reload my-plugin"
|
|
7743
8566
|
].join("\n");
|
|
7744
8567
|
}
|
|
@@ -8796,6 +9619,25 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
8796
9619
|
cmdName = "text";
|
|
8797
9620
|
params = { selector: options.selector || options.s };
|
|
8798
9621
|
break;
|
|
9622
|
+
case "find": {
|
|
9623
|
+
if (!args[0] || !args[1]) {
|
|
9624
|
+
outputError("Usage: xbrowser find <text|role|label|placeholder|testid|alt|title|first|last|nth> <value> [action] [--name <name>]");
|
|
9625
|
+
}
|
|
9626
|
+
const strategy = args[0];
|
|
9627
|
+
const value = args[1];
|
|
9628
|
+
const operation = args.slice(2).join(" ") || void 0;
|
|
9629
|
+
cmdName = "find";
|
|
9630
|
+
params = {
|
|
9631
|
+
strategy,
|
|
9632
|
+
value,
|
|
9633
|
+
...operation ? { operation } : {},
|
|
9634
|
+
name: options.name,
|
|
9635
|
+
exact: !!options.exact,
|
|
9636
|
+
timeout: options.timeout ? Number(options.timeout) : void 0,
|
|
9637
|
+
index: options.index ? Number(options.index) : void 0
|
|
9638
|
+
};
|
|
9639
|
+
break;
|
|
9640
|
+
}
|
|
8799
9641
|
case "back":
|
|
8800
9642
|
cmdName = "back";
|
|
8801
9643
|
params = {};
|
|
@@ -8818,7 +9660,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
8818
9660
|
let parsedActions;
|
|
8819
9661
|
if (options.action) {
|
|
8820
9662
|
const actionList = Array.isArray(options.action) ? options.action : [options.action];
|
|
8821
|
-
const { parseActionDsl } = await import("./parse-action-dsl-
|
|
9663
|
+
const { parseActionDsl } = await import("./parse-action-dsl-T3DYC33D.js");
|
|
8822
9664
|
parsedActions = actionList.map((a) => parseActionDsl(a));
|
|
8823
9665
|
} else if (options["actions-file"]) {
|
|
8824
9666
|
const fs3 = await import("fs");
|
|
@@ -8987,11 +9829,11 @@ async function handleSession(args, options, mode, cdpEndpoint) {
|
|
|
8987
9829
|
case "list":
|
|
8988
9830
|
case "ls": {
|
|
8989
9831
|
try {
|
|
8990
|
-
const
|
|
8991
|
-
outputResult({ sessions }, mode);
|
|
9832
|
+
const sessions2 = await forwardSessionList();
|
|
9833
|
+
outputResult({ sessions: sessions2 }, mode);
|
|
8992
9834
|
} catch {
|
|
8993
|
-
const
|
|
8994
|
-
outputResult({ sessions }, mode);
|
|
9835
|
+
const sessions2 = await listSessions();
|
|
9836
|
+
outputResult({ sessions: sessions2 }, mode);
|
|
8995
9837
|
}
|
|
8996
9838
|
break;
|
|
8997
9839
|
}
|
|
@@ -9011,8 +9853,8 @@ async function handleSession(args, options, mode, cdpEndpoint) {
|
|
|
9011
9853
|
}
|
|
9012
9854
|
case "kill-all": {
|
|
9013
9855
|
try {
|
|
9014
|
-
const
|
|
9015
|
-
for (const s of
|
|
9856
|
+
const sessions2 = await forwardSessionList();
|
|
9857
|
+
for (const s of sessions2) {
|
|
9016
9858
|
try {
|
|
9017
9859
|
await forwardSessionClose(s.name);
|
|
9018
9860
|
} catch {
|
|
@@ -9039,15 +9881,28 @@ function getPluginLoader2() {
|
|
|
9039
9881
|
if (!pluginLoader3) pluginLoader3 = new XBrowserPluginLoader();
|
|
9040
9882
|
return pluginLoader3;
|
|
9041
9883
|
}
|
|
9042
|
-
async function
|
|
9884
|
+
async function buildRuntimePluginInfo() {
|
|
9043
9885
|
const loader = await getPluginLoader();
|
|
9044
9886
|
const sites = loader.getCore().loader.getSites();
|
|
9045
9887
|
const map = /* @__PURE__ */ new Map();
|
|
9046
9888
|
for (const site of sites) {
|
|
9047
|
-
const cmds = site.getAllCommands()
|
|
9048
|
-
|
|
9049
|
-
|
|
9889
|
+
const cmds = site.getAllCommands();
|
|
9890
|
+
const commandNames = cmds.map((c) => c.name);
|
|
9891
|
+
if (commandNames.length === 0) continue;
|
|
9892
|
+
const anySite = site;
|
|
9893
|
+
const hasLoginHandler = typeof anySite.hasLoginCommand === "function" && anySite.hasLoginCommand();
|
|
9894
|
+
const configRequiresLogin = !!site.config.requiresLogin;
|
|
9895
|
+
const hasLogin = hasLoginHandler || configRequiresLogin;
|
|
9896
|
+
let loggedIn = null;
|
|
9897
|
+
if (hasLogin) {
|
|
9898
|
+
try {
|
|
9899
|
+
loggedIn = await site.isLoggedIn();
|
|
9900
|
+
} catch {
|
|
9901
|
+
loggedIn = null;
|
|
9902
|
+
}
|
|
9050
9903
|
}
|
|
9904
|
+
const requiresLoginCommands = cmds.filter((c) => c.requiresLogin === true).map((c) => c.name);
|
|
9905
|
+
map.set(site.name, { commands: commandNames, hasLogin, loggedIn, requiresLoginCommands });
|
|
9051
9906
|
}
|
|
9052
9907
|
return map;
|
|
9053
9908
|
}
|
|
@@ -9211,6 +10066,54 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
9211
10066
|
console.error("\u67E5\u8BE2\u5931\u8D25:", err.message);
|
|
9212
10067
|
}
|
|
9213
10068
|
}
|
|
10069
|
+
async function handlePluginSchema(args, mode) {
|
|
10070
|
+
const pluginName = args[0];
|
|
10071
|
+
const commandName = args[1];
|
|
10072
|
+
if (!pluginName) outputError("Usage: xbrowser plugin schema <name> [command] [--json]");
|
|
10073
|
+
const loader = await getPluginLoader();
|
|
10074
|
+
const contract = loader.getPluginContract(pluginName, commandName);
|
|
10075
|
+
if (!contract) {
|
|
10076
|
+
outputError(commandName ? `Command "${commandName}" not found in plugin "${pluginName}"` : `Plugin "${pluginName}" not found`);
|
|
10077
|
+
return;
|
|
10078
|
+
}
|
|
10079
|
+
if (mode === "json") {
|
|
10080
|
+
outputResult(contract, mode);
|
|
10081
|
+
return;
|
|
10082
|
+
}
|
|
10083
|
+
if ("commands" in contract) {
|
|
10084
|
+
printPluginContract(contract);
|
|
10085
|
+
} else {
|
|
10086
|
+
printCommandContract(pluginName, contract);
|
|
10087
|
+
}
|
|
10088
|
+
}
|
|
10089
|
+
function printPluginContract(contract) {
|
|
10090
|
+
console.log(`${contract.plugin.name} contract v${contract.version}`);
|
|
10091
|
+
if (contract.plugin.description) console.log(contract.plugin.description);
|
|
10092
|
+
console.log("");
|
|
10093
|
+
for (const command of contract.commands) {
|
|
10094
|
+
printCommandContract(contract.plugin.name, command);
|
|
10095
|
+
}
|
|
10096
|
+
}
|
|
10097
|
+
function printCommandContract(pluginName, command) {
|
|
10098
|
+
console.log(`${pluginName} ${command.name}`);
|
|
10099
|
+
if (command.description) console.log(` ${command.description}`);
|
|
10100
|
+
console.log(` scope: ${command.scope}`);
|
|
10101
|
+
if (command.capabilities.length > 0) {
|
|
10102
|
+
console.log(` capabilities: ${command.capabilities.join(", ")}`);
|
|
10103
|
+
}
|
|
10104
|
+
if (command.positional.length > 0) {
|
|
10105
|
+
console.log(` positional: ${command.positional.join(", ")}`);
|
|
10106
|
+
}
|
|
10107
|
+
if (command.form.fields.length > 0) {
|
|
10108
|
+
console.log(" fields:");
|
|
10109
|
+
for (const field of command.form.fields) {
|
|
10110
|
+
const required = field.required ? "required" : "optional";
|
|
10111
|
+
const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
|
|
10112
|
+
console.log(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}`);
|
|
10113
|
+
}
|
|
10114
|
+
}
|
|
10115
|
+
console.log("");
|
|
10116
|
+
}
|
|
9214
10117
|
async function handlePlugin(args, options, mode) {
|
|
9215
10118
|
const sub = args[0];
|
|
9216
10119
|
const subArgs = args.slice(1);
|
|
@@ -9251,17 +10154,20 @@ async function handlePlugin(args, options, mode) {
|
|
|
9251
10154
|
}
|
|
9252
10155
|
case "list": {
|
|
9253
10156
|
const plugins = await installer.list();
|
|
9254
|
-
const
|
|
10157
|
+
const runtimeInfo = await buildRuntimePluginInfo();
|
|
9255
10158
|
const enrichedPlugins = plugins.map((p) => {
|
|
9256
10159
|
const metadata = p.metadata;
|
|
9257
10160
|
const staticCommands = metadata?.commands;
|
|
9258
|
-
const
|
|
9259
|
-
const commands =
|
|
10161
|
+
const rt = runtimeInfo.get(p.name);
|
|
10162
|
+
const commands = rt?.commands || staticCommands;
|
|
9260
10163
|
return {
|
|
9261
10164
|
...p,
|
|
9262
10165
|
commands,
|
|
9263
10166
|
version: metadata?.version,
|
|
9264
|
-
description: metadata?.description
|
|
10167
|
+
description: metadata?.description,
|
|
10168
|
+
hasLogin: rt?.hasLogin ?? false,
|
|
10169
|
+
loggedIn: rt?.loggedIn ?? null,
|
|
10170
|
+
requiresLoginCommands: rt?.requiresLoginCommands ?? []
|
|
9265
10171
|
};
|
|
9266
10172
|
});
|
|
9267
10173
|
if (mode === "json") {
|
|
@@ -9272,20 +10178,27 @@ async function handlePlugin(args, options, mode) {
|
|
|
9272
10178
|
return;
|
|
9273
10179
|
}
|
|
9274
10180
|
for (const p of enrichedPlugins) {
|
|
10181
|
+
const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
|
|
9275
10182
|
if (p.version && p.description) {
|
|
9276
|
-
console.log(`${p.name} (${p.version}) - ${p.description}`);
|
|
10183
|
+
console.log(`${p.name} (${p.version}) - ${p.description}${loginTag}`);
|
|
9277
10184
|
} else {
|
|
9278
|
-
console.log(p.name);
|
|
10185
|
+
console.log(`${p.name}${loginTag}`);
|
|
9279
10186
|
}
|
|
9280
10187
|
if (p.commands && p.commands.length > 0) {
|
|
9281
10188
|
console.log(` ${p.commands.join(", ")}`);
|
|
9282
10189
|
}
|
|
10190
|
+
if (p.requiresLoginCommands.length > 0) {
|
|
10191
|
+
console.log(` requires login: ${p.requiresLoginCommands.join(", ")}`);
|
|
10192
|
+
}
|
|
9283
10193
|
}
|
|
9284
10194
|
console.log(`
|
|
9285
10195
|
Total: ${enrichedPlugins.length} plugins`);
|
|
9286
10196
|
}
|
|
9287
10197
|
break;
|
|
9288
10198
|
}
|
|
10199
|
+
case "schema":
|
|
10200
|
+
await handlePluginSchema(subArgs, mode);
|
|
10201
|
+
break;
|
|
9289
10202
|
case "reload": {
|
|
9290
10203
|
const name = subArgs[0];
|
|
9291
10204
|
if (!name) outputError("Usage: xbrowser plugin reload <name>");
|
|
@@ -9633,7 +10546,7 @@ async function handleConvert(args, _mode) {
|
|
|
9633
10546
|
const fs3 = await import("fs");
|
|
9634
10547
|
const path3 = await import("path");
|
|
9635
10548
|
const { default: yaml } = await import("yaml");
|
|
9636
|
-
const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-
|
|
10549
|
+
const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-EGFYNICZ.js");
|
|
9637
10550
|
const content = fs3.readFileSync(filePath, "utf-8");
|
|
9638
10551
|
const recording = yaml.parse(content);
|
|
9639
10552
|
const ext = path3.extname(outputPath).toLowerCase();
|
|
@@ -9658,7 +10571,7 @@ async function handleExtract(args, _mode) {
|
|
|
9658
10571
|
console.error("Usage: xbrowser extract <recording.yaml>");
|
|
9659
10572
|
process.exit(1);
|
|
9660
10573
|
}
|
|
9661
|
-
const { extractAndSave, printExtractSummary } = await import("./extract-
|
|
10574
|
+
const { extractAndSave, printExtractSummary } = await import("./extract-L2IW3IUB.js");
|
|
9662
10575
|
const { summary, outputPath } = extractAndSave(filePath);
|
|
9663
10576
|
printExtractSummary(summary);
|
|
9664
10577
|
console.log(`
|
|
@@ -9671,7 +10584,7 @@ async function handleFilter(args, _mode) {
|
|
|
9671
10584
|
console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude-types=type1,type2]");
|
|
9672
10585
|
process.exit(1);
|
|
9673
10586
|
}
|
|
9674
|
-
const { filterRecording, parseExcludeTypes } = await import("./filter-
|
|
10587
|
+
const { filterRecording, parseExcludeTypes } = await import("./filter-HC4RA7JY.js");
|
|
9675
10588
|
const excludeTypes = parseExcludeTypes(args.slice(2));
|
|
9676
10589
|
const result = filterRecording(filePath, outputPath, excludeTypes);
|
|
9677
10590
|
console.log(`Filtered ${filePath} -> ${outputPath}`);
|
|
@@ -10326,14 +11239,14 @@ async function listCommands() {
|
|
|
10326
11239
|
return jsonResponse(200, { commands });
|
|
10327
11240
|
}
|
|
10328
11241
|
async function listSessions2() {
|
|
10329
|
-
const
|
|
11242
|
+
const sessions2 = getAllSessions().map((s) => ({
|
|
10330
11243
|
id: s.id,
|
|
10331
11244
|
name: s.name,
|
|
10332
11245
|
url: s.page?.url() ?? null,
|
|
10333
11246
|
createdAt: s.createdAt,
|
|
10334
11247
|
isCDP: s.isCDP ?? false
|
|
10335
11248
|
}));
|
|
10336
|
-
return jsonResponse(200, { sessions });
|
|
11249
|
+
return jsonResponse(200, { sessions: sessions2 });
|
|
10337
11250
|
}
|
|
10338
11251
|
async function createSessionHandler(req) {
|
|
10339
11252
|
const body = req.body;
|
|
@@ -11022,9 +11935,9 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11022
11935
|
}
|
|
11023
11936
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
11024
11937
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
11025
|
-
const { forwardExec } = await import("./daemon-client-
|
|
11938
|
+
const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
|
|
11026
11939
|
const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
|
|
11027
|
-
const result = await forwardExec(command
|
|
11940
|
+
const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
|
|
11028
11941
|
if (result && result.success !== false) {
|
|
11029
11942
|
if (isCommandResult2(result)) {
|
|
11030
11943
|
if (mode === "json" || mode === "yaml") {
|
|
@@ -11057,6 +11970,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11057
11970
|
browser: needsBrowser ? session.context.browser() : null,
|
|
11058
11971
|
browserContext: needsBrowser ? session.context : null,
|
|
11059
11972
|
sessionId: needsBrowser ? session.id : "",
|
|
11973
|
+
cdpEndpoint: cdpEndpoint || (needsBrowser ? session?.cdpEndpoint : void 0),
|
|
11060
11974
|
storage: getPluginStorage(command),
|
|
11061
11975
|
output: { mode, showTips: true, color: true, emoji: true },
|
|
11062
11976
|
error: (msg) => {
|
|
@@ -11283,7 +12197,7 @@ async function main() {
|
|
|
11283
12197
|
const command = process.argv[2];
|
|
11284
12198
|
const isLongRunning = command === "preview" || command === "serve";
|
|
11285
12199
|
if (!isLongRunning) {
|
|
11286
|
-
const { ensureProcessCanExit } = await import("./browser-
|
|
12200
|
+
const { ensureProcessCanExit } = await import("./browser-DSVV4GHS.js");
|
|
11287
12201
|
await ensureProcessCanExit().catch(() => {
|
|
11288
12202
|
});
|
|
11289
12203
|
process.exit(exitCode);
|