@xbrowser/cli 0.15.1 → 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.
Files changed (30) hide show
  1. package/dist/{browser-R7B255ML.js → browser-53KUFEEM.js} +5 -1
  2. package/dist/{browser-I2HJZ7IP.js → browser-DSVV4GHS.js} +2 -2
  3. package/dist/{browser-GWBH6OJK.js → browser-GURRY444.js} +3 -1
  4. package/dist/cdp-driver-MNPR3HZH.js +2537 -0
  5. package/dist/cdp-driver-SSXUGXP6.js +47 -0
  6. package/dist/{chunk-2ONMTDLK.js → chunk-2BQZIT3S.js} +2535 -50
  7. package/dist/{chunk-KDYXFLAC.js → chunk-2MFXKN32.js} +2 -2
  8. package/dist/chunk-42RPMJ76.js +2530 -0
  9. package/dist/{chunk-F3ZWFCJJ.js → chunk-E4O5ZU3H.js} +2535 -50
  10. package/dist/{chunk-ATFTAKMN.js → chunk-IDVD44ED.js} +20 -0
  11. package/dist/chunk-T4J4C2NZ.js +250 -0
  12. package/dist/{chunk-RS6YYWTK.js → chunk-YKOHDEFV.js} +73 -38
  13. package/dist/cli.js +1176 -281
  14. package/dist/{convert-4DUWZIKH.js → convert-EGFYNICZ.js} +2 -0
  15. package/dist/{daemon-client-GX2UYIW4.js → daemon-client-3VM7VU7O.js} +22 -0
  16. package/dist/{daemon-client-3IJD6X4B.js → daemon-client-YAVQ343A.js} +7 -1
  17. package/dist/daemon-main.js +1004 -161
  18. package/dist/{extract-EGRXZSSK.js → extract-L2IW3IUB.js} +2 -0
  19. package/dist/{filter-OLAE26HN.js → filter-HC4RA7JY.js} +2 -0
  20. package/dist/index.d.ts +581 -41
  21. package/dist/index.js +1220 -321
  22. package/dist/launcher-KA7J32K5.js +19 -0
  23. package/dist/{network-store-YAF5OIBH.js → network-store-66A2RATI.js} +1 -0
  24. package/dist/{session-recorder-XET3DNML.js → session-recorder-MA75PKTQ.js} +1 -1
  25. package/package.json +3 -4
  26. package/dist/daemon-client-XWSSQBEA.js +0 -58
  27. package/dist/network-store-2S5HATEV.js +0 -194
  28. package/dist/parse-action-dsl-DRSPBALP.js +0 -72
  29. package/dist/screenshot-MB6R7RSS.js +0 -26
  30. 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-KDYXFLAC.js";
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-2ONMTDLK.js";
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-ATFTAKMN.js";
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,12 +171,13 @@ var version = pkg.version;
169
171
 
170
172
  // src/executor.ts
171
173
  import {
172
- ok as ok25,
174
+ ok as ok26,
173
175
  fail as fail7,
174
176
  isCommandResult,
175
177
  configureArchiveStore,
176
178
  appendCommandToArchive,
177
- checkGuard
179
+ checkGuard,
180
+ PluginStorage
178
181
  } from "@dyyz1993/xcli-core";
179
182
 
180
183
  // src/commands/navigation.ts
@@ -534,7 +537,7 @@ var pressCommand = registerCommand({
534
537
  key: z2.string()
535
538
  }),
536
539
  handler: async (p, ctx) => {
537
- await ctx.page.press(p.selector || "body", p.key, { delay: p.delay, timeout: 1e4 });
540
+ await ctx.page.press(p.selector || "body", p.key, { timeout: 1e4 });
538
541
  return ok2({ key: p.key });
539
542
  }
540
543
  });
@@ -553,7 +556,7 @@ var selectCommand = registerCommand({
553
556
  }),
554
557
  handler: async (p, ctx) => {
555
558
  const values = typeof p.value === "string" ? [p.value] : p.value;
556
- await ctx.page.selectOption(p.selector, values, { force: true, timeout: 1e4 });
559
+ await ctx.page.selectOption(p.selector, values);
557
560
  return ok2({ selector: p.selector, value: p.value });
558
561
  }
559
562
  });
@@ -569,7 +572,7 @@ var checkCommand = registerCommand({
569
572
  selector: z2.string()
570
573
  }),
571
574
  handler: async (p, ctx) => {
572
- await ctx.page.check(p.selector, { force: true, timeout: 1e4 });
575
+ await ctx.page.check(p.selector, { timeout: 1e4 });
573
576
  return ok2({ selector: p.selector });
574
577
  }
575
578
  });
@@ -586,7 +589,7 @@ var hoverCommand = registerCommand({
586
589
  selector: z2.string()
587
590
  }),
588
591
  handler: async (p, ctx) => {
589
- await ctx.page.hover(p.selector, { modifiers: p.modifiers, force: true, timeout: 1e4 });
592
+ await ctx.page.hover(p.selector, { timeout: 1e4 });
590
593
  return ok2({ selector: p.selector });
591
594
  }
592
595
  });
@@ -2115,7 +2118,7 @@ var scrapeCommand = registerCommand({
2115
2118
  await page.goto(p.url, { waitUntil: "commit", timeout: p.timeout });
2116
2119
  await page.waitForSelector("body", { timeout: p.timeout }).catch(() => {
2117
2120
  });
2118
- await page.waitForLoadState("networkidle", { timeout: Math.min(p.timeout, 8e3) }).catch(() => {
2121
+ await page.waitForLoadState("networkidle", Math.min(p.timeout, 8e3)).catch(() => {
2119
2122
  });
2120
2123
  await page.waitForTimeout(p.waitAfterLoad > 0 ? p.waitAfterLoad : 2e3);
2121
2124
  if (p.selector) {
@@ -3120,7 +3123,7 @@ async function fetchFullContent(urls, timeout, cdpEndpoint) {
3120
3123
  const pg = await context.newPage();
3121
3124
  try {
3122
3125
  await pg.goto(url, { waitUntil: "domcontentloaded", timeout });
3123
- await pg.waitForLoadState("networkidle", { timeout }).catch(() => {
3126
+ await pg.waitForLoadState("networkidle", timeout).catch(() => {
3124
3127
  });
3125
3128
  const html = await pg.content();
3126
3129
  contentMap.set(url, htmlToMarkdown(html, { onlyMainContent: true }));
@@ -3516,7 +3519,7 @@ var networkCommand = registerCommand({
3516
3519
  waitUntil: "domcontentloaded",
3517
3520
  timeout: p.timeout
3518
3521
  });
3519
- await page.waitForLoadState("networkidle", { timeout: p.timeout }).catch(() => {
3522
+ await page.waitForLoadState("networkidle", p.timeout).catch(() => {
3520
3523
  });
3521
3524
  await page.waitForTimeout(p.wait);
3522
3525
  const totalCount = captures.length;
@@ -3827,6 +3830,24 @@ var ENGINE_KEY_ENUM = z20.enum(ALL_ENGINE_KEYS);
3827
3830
  import { z as z21 } from "zod";
3828
3831
  import { ok as ok20, fail as fail4 } from "@dyyz1993/xcli-core";
3829
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
+
3830
3851
  // src/utils/resolve-selector.ts
3831
3852
  function buildElementSelector(el) {
3832
3853
  function isUnique(sel) {
@@ -3998,9 +4019,9 @@ async function resolveSelectors(page, ariaSnapshot) {
3998
4019
  }
3999
4020
  return results;
4000
4021
  }
4001
- var REF_ONLY = /^(e\d+)$/;
4022
+ var REF_ONLY = /^@?(e\d+)$/;
4002
4023
  var refCache = /* @__PURE__ */ new Map();
4003
- async function resolveRefParams(page, params, selectorKeys, cache) {
4024
+ async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
4004
4025
  const tips = [];
4005
4026
  const newParams = { ...params };
4006
4027
  if (!selectorKeys || selectorKeys.length === 0) {
@@ -4009,7 +4030,17 @@ async function resolveRefParams(page, params, selectorKeys, cache) {
4009
4030
  for (const key of selectorKeys) {
4010
4031
  const val = params[key];
4011
4032
  if (typeof val !== "string" || !REF_ONLY.test(val)) continue;
4012
- const ref = val;
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
+ }
4013
4044
  const activeCache = cache ?? refCache;
4014
4045
  const cached = activeCache.get(ref);
4015
4046
  if (cached) {
@@ -4290,6 +4321,404 @@ async function enhanceSemanticsWithLLM(url, ariaSnapshot, ruleBasedElements) {
4290
4321
  saveSemantics(domain, pathKey, url, llmElements);
4291
4322
  }
4292
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
+
4293
4722
  // src/commands/snapshot.ts
4294
4723
  var snapshotCommand = registerCommand({
4295
4724
  name: "snapshot",
@@ -4299,7 +4728,14 @@ var snapshotCommand = registerCommand({
4299
4728
  parameters: z21.object({
4300
4729
  type: z21.enum(["aria", "text", "dom", "all"]).default("aria").describe("Snapshot type: aria (accessibility tree), text (visible text), dom (element summary), all (combined)"),
4301
4730
  selector: z21.string().optional().describe("Scope to a specific element"),
4302
- 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")
4303
4739
  }),
4304
4740
  result: z21.object({
4305
4741
  url: z21.string(),
@@ -4312,6 +4748,18 @@ var snapshotCommand = registerCommand({
4312
4748
  const page = ctx.page;
4313
4749
  const url = page.url();
4314
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
+ }
4315
4763
  if (p.type === "aria") {
4316
4764
  const aria = await captureAriaSnapshot(page, p.selector, p.depth);
4317
4765
  const tips = await buildRefTips(page, aria);
@@ -4362,10 +4810,10 @@ async function buildRefTips(page, aria) {
4362
4810
  return [];
4363
4811
  }
4364
4812
  }
4365
- async function captureAriaSnapshot(page, selector, depth) {
4813
+ async function captureAriaSnapshot(page, selector, _depth) {
4366
4814
  try {
4367
4815
  const locator = selector ? page.locator(selector).first() : page.locator("body");
4368
- return await locator.ariaSnapshot({ depth });
4816
+ return await locator.ariaSnapshot();
4369
4817
  } catch {
4370
4818
  try {
4371
4819
  return await page.locator("body").ariaSnapshot();
@@ -4376,7 +4824,7 @@ async function captureAriaSnapshot(page, selector, depth) {
4376
4824
  }
4377
4825
  async function captureTextSnapshot(page, selector) {
4378
4826
  if (selector) {
4379
- return await page.locator(selector).first().innerText({ timeout: 5e3 }).catch(() => "");
4827
+ return await page.locator(selector).first().innerText().catch(() => "");
4380
4828
  }
4381
4829
  return await page.evaluate(() => document.body?.innerText || "").catch(() => "");
4382
4830
  }
@@ -4412,22 +4860,108 @@ async function captureDomSnapshot(page, selector, maxDepth) {
4412
4860
  ).catch(() => ({ tag: "error" }));
4413
4861
  }
4414
4862
 
4415
- // src/commands/tab.ts
4863
+ // src/commands/agent.ts
4416
4864
  import { z as z22 } from "zod";
4417
- import { ok as ok21, fail as fail5 } from "@dyyz1993/xcli-core";
4418
- var TabParams = z22.object({
4419
- subcommand: z22.enum(["list", "new", "close", "switch"]),
4420
- url: z22.string().optional(),
4421
- index: z22.number().int().min(0).optional()
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()
4422
4956
  });
4423
4957
  var tabCommand = registerCommand({
4424
4958
  name: "tab",
4425
4959
  description: "Manage browser tabs: list, new, close, switch",
4426
4960
  scope: "page",
4427
4961
  parameters: TabParams,
4428
- result: z22.object({
4429
- success: z22.boolean(),
4430
- data: z22.unknown()
4962
+ result: z23.object({
4963
+ success: z23.boolean(),
4964
+ data: z23.unknown()
4431
4965
  }),
4432
4966
  handler: async (p, ctx) => {
4433
4967
  const pages = ctx.browserContext.pages();
@@ -4467,7 +5001,7 @@ function handleList(pages, ctx) {
4467
5001
  active: i === currentIndex
4468
5002
  };
4469
5003
  });
4470
- return ok21({ tabs, total: tabs.length, activeIndex: currentIndex });
5004
+ return ok22({ tabs, total: tabs.length, activeIndex: currentIndex });
4471
5005
  }
4472
5006
  async function handleNew(p, _pages, ctx) {
4473
5007
  const newPage = await ctx.browserContext.newPage();
@@ -4489,7 +5023,7 @@ async function handleNew(p, _pages, ctx) {
4489
5023
  const title = await newPage.title().catch(() => "");
4490
5024
  const allPages = ctx.browserContext.pages();
4491
5025
  const newIndex = allPages.indexOf(newPage);
4492
- return ok21({
5026
+ return ok22({
4493
5027
  index: newIndex >= 0 ? newIndex : allPages.length - 1,
4494
5028
  url: newPage.url(),
4495
5029
  title,
@@ -4517,7 +5051,7 @@ async function handleClose(p, pages, ctx) {
4517
5051
  }
4518
5052
  ctx.page = newActivePage;
4519
5053
  }
4520
- return ok21({
5054
+ return ok22({
4521
5055
  closedIndex: closeIndex,
4522
5056
  total: remainingPages.length,
4523
5057
  activeIndex: isActivePage ? closeIndex < remainingPages.length ? closeIndex : remainingPages.length - 1 : pages.indexOf(ctx.page)
@@ -4539,7 +5073,7 @@ async function handleSwitch(p, pages, ctx) {
4539
5073
  }
4540
5074
  ctx.page = targetPage;
4541
5075
  const title = await targetPage.title().catch(() => "");
4542
- return ok21({
5076
+ return ok22({
4543
5077
  index: p.index,
4544
5078
  url: targetPage.url(),
4545
5079
  title,
@@ -4548,8 +5082,8 @@ async function handleSwitch(p, pages, ctx) {
4548
5082
  }
4549
5083
 
4550
5084
  // src/commands/addinitscript.ts
4551
- import { z as z23 } from "zod";
4552
- import { ok as ok22 } from "@dyyz1993/xcli-core";
5085
+ import { z as z24 } from "zod";
5086
+ import { ok as ok23 } from "@dyyz1993/xcli-core";
4553
5087
  import { readFileSync as readFileSync2 } from "fs";
4554
5088
 
4555
5089
  // src/chain-parser.ts
@@ -4727,7 +5261,7 @@ function parseCommandArgs(name, args) {
4727
5261
  } else {
4728
5262
  if (positionalIndex < positionalKeys.length) {
4729
5263
  const isLast = positionalIndex === positionalKeys.length - 1;
4730
- if (isLast && name === "eval") {
5264
+ if (isLast && (name === "eval" || name === "find")) {
4731
5265
  const remaining = args.slice(i).map(unquote2).join(" ");
4732
5266
  params[positionalKeys[positionalIndex]] = remaining;
4733
5267
  break;
@@ -4777,7 +5311,7 @@ var commandDefCache = {
4777
5311
  frames: { positional: [] },
4778
5312
  frame: { positional: ["selector"] },
4779
5313
  actions: { positional: ["url"] },
4780
- find: { positional: ["strategy", "value"] },
5314
+ find: { positional: ["strategy", "value", "operation"] },
4781
5315
  addinitscript: { positional: ["script"] },
4782
5316
  tab: { positional: ["subcommand"] }
4783
5317
  };
@@ -4789,14 +5323,14 @@ function registerCommandDefinition(name, positional) {
4789
5323
  }
4790
5324
 
4791
5325
  // src/commands/addinitscript.ts
4792
- var InitScriptParams = z23.object({
4793
- script: z23.string().optional(),
4794
- file: z23.string().optional(),
4795
- stdin: z23.boolean().optional(),
4796
- name: z23.string().optional(),
4797
- list: z23.boolean().optional(),
4798
- remove: z23.string().optional(),
4799
- base64: z23.string().optional()
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()
4800
5334
  });
4801
5335
  var registeredScripts = /* @__PURE__ */ new Map();
4802
5336
  function resolveScriptContent(params) {
@@ -4837,15 +5371,15 @@ var addInitScriptCommand = registerCommand({
4837
5371
  size: content2.length,
4838
5372
  preview: content2.slice(0, 80)
4839
5373
  }));
4840
- return ok22({ scripts });
5374
+ return ok23({ scripts });
4841
5375
  }
4842
5376
  if (params.remove) {
4843
5377
  const existed = registeredScripts.delete(params.remove);
4844
- return ok22({ removed: params.remove, existed });
5378
+ return ok23({ removed: params.remove, existed });
4845
5379
  }
4846
5380
  let content = params.stdin ? await readStdin() : resolveScriptContent(params);
4847
5381
  if (!content) {
4848
- return ok22({ error: "No script content provided. Use --script, --file, --stdin, or --base64" });
5382
+ return ok23({ error: "No script content provided. Use --script, --file, --stdin, or --base64" });
4849
5383
  }
4850
5384
  const scriptName = params.name ?? `script-${Date.now()}`;
4851
5385
  registeredScripts.set(scriptName, content);
@@ -4853,74 +5387,134 @@ var addInitScriptCommand = registerCommand({
4853
5387
  try {
4854
5388
  await ctx.page.evaluate(content);
4855
5389
  } catch {
4856
- return ok22({
5390
+ return ok23({
4857
5391
  registered: scriptName,
4858
5392
  hint: "Script registered for future page loads; immediate execution skipped (page may not be ready)"
4859
5393
  });
4860
5394
  }
4861
- return ok22({ registered: scriptName, executedImmediately: true });
5395
+ return ok23({ registered: scriptName, executedImmediately: true });
4862
5396
  }
4863
5397
  });
4864
5398
  registerCommandDefinition("addinitscript", ["script"]);
4865
5399
 
4866
5400
  // src/commands/find.ts
4867
- import { z as z24 } from "zod";
4868
- import { ok as ok23, fail as fail6 } from "@dyyz1993/xcli-core";
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"]);
4869
5404
  var findCommand = registerCommand({
4870
5405
  name: "find",
4871
5406
  description: "Find elements by semantic strategy (text/role/label/placeholder/testid) and optionally perform an action",
4872
5407
  scope: "page",
4873
- parameters: z24.object({
4874
- strategy: z24.enum(["text", "role", "label", "placeholder", "testid"]),
4875
- value: z24.string(),
4876
- name: z24.string().optional(),
4877
- exact: z24.boolean().optional().default(false),
4878
- click: z24.boolean().optional().default(false),
4879
- fill: z24.string().optional(),
4880
- type: z24.string().optional(),
4881
- select: z24.string().optional(),
4882
- timeout: z24.number().optional().default(1e4)
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)
4883
5424
  }),
4884
- result: z24.object({
4885
- matched: z24.number(),
4886
- selector: z24.string(),
4887
- action: z24.string().optional()
5425
+ result: z25.object({
5426
+ matched: z25.number(),
5427
+ selector: z25.string(),
5428
+ action: z25.string().optional()
4888
5429
  }),
4889
5430
  handler: async (p, ctx) => {
4890
5431
  const page = ctx.page;
4891
- const locator = buildLocator(page, p.strategy, p.value, {
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, {
4892
5437
  name: p.name,
4893
- exact: p.exact
5438
+ exact: p.exact,
5439
+ index: normalized.index
4894
5440
  });
4895
5441
  const count = await locator.count();
4896
5442
  if (count === 0) {
4897
5443
  return fail6(`No element found with ${p.strategy}="${p.value}"`);
4898
5444
  }
4899
5445
  const tips = [];
4900
- const target = locator.first();
5446
+ const target = selectTarget(locator, p.strategy);
4901
5447
  if (count > 1) {
4902
- tips.push(`\u26A0\uFE0F Matched ${count} elements, using first`);
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.`);
4903
5449
  }
4904
- const selector = describeSelector(p.strategy, p.value, p.name);
4905
- let action;
4906
- if (p.click) {
5450
+ const selector = describeSelector(normalized.strategy, normalized.value, p.name);
5451
+ if (actionName === "click") {
4907
5452
  await target.click({ timeout: p.timeout, force: true });
4908
- action = "click";
4909
- } else if (p.fill !== void 0) {
4910
- await target.fill(p.fill, { timeout: p.timeout, force: true });
4911
- action = `fill("${p.fill}")`;
4912
- } else if (p.type !== void 0) {
4913
- await target.type(p.type, { delay: 10, timeout: p.timeout });
4914
- action = `type("${p.type}")`;
4915
- } else if (p.select !== void 0) {
4916
- await target.selectOption(p.select, { timeout: p.timeout, force: true });
4917
- action = `select("${p.select}")`;
4918
- }
4919
- const result = ok23({ matched: count, selector, action });
4920
- if (tips.length > 0) result.tips = tips;
4921
- return result;
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);
4922
5474
  }
4923
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
+ }
4924
5518
  function buildLocator(page, strategy, value, opts) {
4925
5519
  switch (strategy) {
4926
5520
  case "text":
@@ -4933,10 +5527,24 @@ function buildLocator(page, strategy, value, opts) {
4933
5527
  return page.getByPlaceholder(value, { exact: opts.exact });
4934
5528
  case "testid":
4935
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);
4936
5540
  default:
4937
5541
  return page.getByText(value, { exact: opts.exact });
4938
5542
  }
4939
5543
  }
5544
+ function selectTarget(locator, strategy) {
5545
+ if (strategy === "first" || strategy === "last" || strategy === "nth") return locator;
5546
+ return locator.first();
5547
+ }
4940
5548
  function describeSelector(strategy, value, name) {
4941
5549
  switch (strategy) {
4942
5550
  case "role":
@@ -4949,6 +5557,16 @@ function describeSelector(strategy, value, name) {
4949
5557
  return `getByPlaceholder("${value}")`;
4950
5558
  case "testid":
4951
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}")`;
4952
5570
  default:
4953
5571
  return `${strategy}("${value}")`;
4954
5572
  }
@@ -5071,7 +5689,7 @@ async function detectCaptcha2(page) {
5071
5689
  }
5072
5690
  async function detectWarningText(page) {
5073
5691
  try {
5074
- const pageText = await page.textContent("body", { timeout: 1e3 }).catch(() => "") || "";
5692
+ const pageText = await page.textContent("body").catch(() => "") || "";
5075
5693
  const lowerText = pageText.toLowerCase();
5076
5694
  for (const { text, severity } of WARNING_TEXTS) {
5077
5695
  if (lowerText.includes(text.toLowerCase())) {
@@ -5202,8 +5820,8 @@ function formatDetectionMessage(result) {
5202
5820
  }
5203
5821
 
5204
5822
  // src/commands/promo.ts
5205
- import { z as z25 } from "zod";
5206
- import { ok as ok24 } from "@dyyz1993/xcli-core";
5823
+ import { z as z26 } from "zod";
5824
+ import { ok as ok25 } from "@dyyz1993/xcli-core";
5207
5825
  import { existsSync as existsSync2, readFileSync as readFileSync8 } from "fs";
5208
5826
  import { resolve as resolve6 } from "path";
5209
5827
 
@@ -5506,14 +6124,14 @@ async function dispatchPromo(config) {
5506
6124
  }
5507
6125
 
5508
6126
  // src/commands/promo.ts
5509
- var promoParams = z25.object({
5510
- platform: z25.enum(["devto", "medium", "csdn", "juejin", "quora"]).describe("Target platform for promotion"),
5511
- file: z25.string().describe("Path to Markdown file to publish"),
5512
- tags: z25.string().optional().describe("Comma-separated tags"),
5513
- title: z25.string().optional().describe("Custom title (default: extracted from file first heading)"),
5514
- search: z25.string().optional().describe("Quora: search query to find questions"),
5515
- cdpEndpoint: z25.string().optional().describe("CDP endpoint for agent-browser"),
5516
- session: z25.string().optional().describe("agent-browser session name")
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")
5517
6135
  }).refine(
5518
6136
  (data) => data.platform !== "quora" || !!data.search,
5519
6137
  { message: "Quora platform requires --search parameter" }
@@ -5526,17 +6144,17 @@ var promoCommand = registerCommand({
5526
6144
  description: "Publish promotional articles to various platforms (devto, medium, csdn, juejin, quora)",
5527
6145
  scope: "project",
5528
6146
  parameters: promoParams,
5529
- result: z25.object({
5530
- success: z25.boolean(),
5531
- url: z25.string().optional(),
5532
- error: z25.string().optional(),
5533
- platform: z25.string()
6147
+ result: z26.object({
6148
+ success: z26.boolean(),
6149
+ url: z26.string().optional(),
6150
+ error: z26.string().optional(),
6151
+ platform: z26.string()
5534
6152
  }),
5535
6153
  handler: async (p, _ctx) => {
5536
6154
  const filePath = resolve6(p.file);
5537
6155
  const content = readFileSync8(filePath, "utf-8");
5538
6156
  if (content.trim().length === 0) {
5539
- return ok24({
6157
+ return ok25({
5540
6158
  success: false,
5541
6159
  error: `File is empty: ${filePath}`,
5542
6160
  platform: p.platform
@@ -5551,7 +6169,7 @@ var promoCommand = registerCommand({
5551
6169
  cdpEndpoint: p.cdpEndpoint ?? _ctx.cdpEndpoint,
5552
6170
  session: p.session ?? p.platform
5553
6171
  });
5554
- return ok24(result);
6172
+ return ok25(result);
5555
6173
  }
5556
6174
  });
5557
6175
 
@@ -5652,7 +6270,7 @@ import { join as join2 } from "path";
5652
6270
  import { execSync as execSync6 } from "child_process";
5653
6271
  var SHARED_PLUGIN_DEPENDENCIES = {
5654
6272
  "zod": "^3.24.0",
5655
- "@dyyz1993/xcli-core": "^0.9.2"
6273
+ "@dyyz1993/xcli-core": "^0.12.1"
5656
6274
  };
5657
6275
  function ensurePluginDependencies(pluginsDir) {
5658
6276
  const zodPath = join2(pluginsDir, "node_modules", "zod");
@@ -5691,6 +6309,192 @@ function ensurePluginDependencies(pluginsDir) {
5691
6309
  }
5692
6310
  }
5693
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
+
5694
6498
  // src/plugin/loader.ts
5695
6499
  var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
5696
6500
  var XBrowserPluginLoader = class {
@@ -5733,6 +6537,13 @@ var XBrowserPluginLoader = class {
5733
6537
  getLoadedPlugins() {
5734
6538
  return this.loader.getLoadedPlugins();
5735
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
+ }
5736
6547
  async loadPlugin(pluginPath, id) {
5737
6548
  return this.loader.loadPlugin(pluginPath, id);
5738
6549
  }
@@ -6217,7 +7028,7 @@ function getTipsManager() {
6217
7028
 
6218
7029
  // src/hooks/loader.ts
6219
7030
  var builtinHooks = {
6220
- screenshot: () => import("./screenshot-MB6R7RSS.js").then((m) => m.screenshotHook)
7031
+ screenshot: () => import("./screenshot-CWAWMXVA.js").then((m) => m.screenshotHook)
6221
7032
  };
6222
7033
  async function loadHooks() {
6223
7034
  const names = process.env.XBROWSER_HOOKS;
@@ -6236,45 +7047,13 @@ async function loadHooks() {
6236
7047
  // src/executor.ts
6237
7048
  import { homedir as homedir3 } from "os";
6238
7049
  import { join as join3 } from "path";
6239
- import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
6240
7050
  var NAVIGATION_COMMANDS = /* @__PURE__ */ new Set(["goto", "back", "forward", "refresh"]);
6241
7051
  var snapshotHintShown = /* @__PURE__ */ new WeakSet();
6242
7052
  var STORAGE_DIR = join3(homedir3(), ".xbrowser", "storage");
6243
7053
  var storageCache = /* @__PURE__ */ new Map();
6244
7054
  function getPluginStorage(pluginName) {
6245
7055
  if (!storageCache.has(pluginName)) {
6246
- const filePath = join3(STORAGE_DIR, `${pluginName}.json`);
6247
- let data = {};
6248
- const load3 = () => {
6249
- if (existsSync6(filePath)) {
6250
- try {
6251
- data = JSON.parse(readFileSync10(filePath, "utf-8"));
6252
- } catch {
6253
- data = {};
6254
- }
6255
- }
6256
- };
6257
- const save = () => {
6258
- mkdirSync3(STORAGE_DIR, { recursive: true });
6259
- writeFileSync5(filePath, JSON.stringify(data, null, 2), "utf-8");
6260
- };
6261
- load3();
6262
- storageCache.set(pluginName, {
6263
- get: async (key) => data[key] ?? null,
6264
- set: async (key, value) => {
6265
- data[key] = value;
6266
- save();
6267
- },
6268
- delete: async (key) => {
6269
- delete data[key];
6270
- save();
6271
- },
6272
- clear: async () => {
6273
- data = {};
6274
- save();
6275
- },
6276
- keys: async () => Object.keys(data)
6277
- });
7056
+ storageCache.set(pluginName, new PluginStorage(pluginName, STORAGE_DIR));
6278
7057
  }
6279
7058
  return storageCache.get(pluginName);
6280
7059
  }
@@ -6335,7 +7114,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6335
7114
  }
6336
7115
  let targetPageOverride = null;
6337
7116
  if (_target && extraOpts?.cdpEndpoint) {
6338
- const { findTargetPage } = await import("./browser-GWBH6OJK.js");
7117
+ const { findTargetPage } = await import("./browser-DSVV4GHS.js");
6339
7118
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
6340
7119
  if (!targetPageOverride) {
6341
7120
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -6352,7 +7131,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6352
7131
  params = result.data;
6353
7132
  }
6354
7133
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
6355
- const { forwardExec } = await import("./daemon-client-XWSSQBEA.js");
7134
+ const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
6356
7135
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
6357
7136
  if (result) return result;
6358
7137
  }
@@ -6360,7 +7139,18 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6360
7139
  const existing = await findOrRestoreSession(sessionName, extraOpts?.cdpEndpoint);
6361
7140
  if (existing) {
6362
7141
  session = existing;
6363
- if (targetPageOverride && session.page) {
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) {
6364
7154
  const currentUrl = session.page.url();
6365
7155
  if (currentUrl !== targetPageOverride.url) {
6366
7156
  await session.page.goto(targetPageOverride.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
@@ -6371,6 +7161,10 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6371
7161
  session = await createSession(sessionName, params.url, {
6372
7162
  cdpEndpoint: extraOpts?.cdpEndpoint
6373
7163
  });
7164
+ } else if (command.scope === "browser") {
7165
+ session = await createSession(sessionName, void 0, {
7166
+ cdpEndpoint: extraOpts?.cdpEndpoint
7167
+ });
6374
7168
  } else if (command.scope !== "project") {
6375
7169
  return errorResult(
6376
7170
  `Session '${sessionName}' not found. Run "xbrowser session open <url>" first.`
@@ -6381,7 +7175,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6381
7175
  browser: session?.context.browser(),
6382
7176
  browserContext: session?.context,
6383
7177
  sessionId: session?.id,
6384
- cdpEndpoint: extraOpts?.cdpEndpoint || session?.cdpEndpoint,
7178
+ cdpEndpoint: session?.cdpEndpoint || extraOpts?.cdpEndpoint,
6385
7179
  args: [],
6386
7180
  options: {},
6387
7181
  cwd: process.cwd(),
@@ -6416,7 +7210,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6416
7210
  let refTips = [];
6417
7211
  if (session?.page && command.selectorParams && command.selectorParams.length > 0) {
6418
7212
  const cache = /* @__PURE__ */ new Map();
6419
- const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache);
7213
+ const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache, session.id);
6420
7214
  if (resolved.tips.length > 0) {
6421
7215
  refTips = resolved.tips;
6422
7216
  params = resolved.params;
@@ -6486,7 +7280,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6486
7280
  timestamp: start
6487
7281
  });
6488
7282
  if (isSuccess) {
6489
- return { ...ok25(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
7283
+ return { ...ok26(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
6490
7284
  }
6491
7285
  return { success: false, data: raw.data, message: raw.message, tips: merged.length > 0 ? merged : raw.tips || [], duration, ...hookOutputs ? { hookOutputs } : {} };
6492
7286
  }
@@ -6499,7 +7293,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6499
7293
  duration,
6500
7294
  timestamp: start
6501
7295
  });
6502
- return { ...ok25(raw, smartTips), duration, ...hookOutputs ? { hookOutputs } : {} };
7296
+ return { ...ok26(raw, smartTips), duration, ...hookOutputs ? { hookOutputs } : {} };
6503
7297
  } catch (err) {
6504
7298
  const end = Date.now();
6505
7299
  const duration = end - start;
@@ -6647,7 +7441,7 @@ async function executeChain(input, options) {
6647
7441
  results.push({
6648
7442
  command: `${cmdName} ${subCommand}`,
6649
7443
  raw: cmdStr,
6650
- ...ok25(data),
7444
+ ...ok26(data),
6651
7445
  duration: duration2,
6652
7446
  ...hookOutputs ? { hookOutputs } : {}
6653
7447
  });
@@ -6864,13 +7658,13 @@ var sessionListBuiltin = {
6864
7658
  },
6865
7659
  execute: async () => {
6866
7660
  try {
6867
- const sessions = await listSessions();
6868
- if (sessions.length === 0) {
7661
+ const sessions2 = await listSessions();
7662
+ if (sessions2.length === 0) {
6869
7663
  console.log("No active sessions");
6870
7664
  return;
6871
7665
  }
6872
7666
  console.log("Active sessions:");
6873
- for (const s of sessions) {
7667
+ for (const s of sessions2) {
6874
7668
  console.log(` ${s.name} (${s.id})`);
6875
7669
  }
6876
7670
  } catch (e) {
@@ -6901,7 +7695,7 @@ var sessionKillBuiltin = {
6901
7695
  };
6902
7696
 
6903
7697
  // src/config.ts
6904
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
7698
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
6905
7699
  import { join as join4 } from "path";
6906
7700
  import { homedir as homedir4, tmpdir } from "os";
6907
7701
  function getConfigFile() {
@@ -6909,14 +7703,14 @@ function getConfigFile() {
6909
7703
  }
6910
7704
  function loadConfig() {
6911
7705
  const configFile = getConfigFile();
6912
- if (!existsSync7(configFile)) return {};
7706
+ if (!existsSync6(configFile)) return {};
6913
7707
  return readJsonFile(configFile, {});
6914
7708
  }
6915
7709
  function saveConfig(config) {
6916
7710
  const dir = join4(homedir4() || tmpdir(), ".xbrowser");
6917
7711
  const configFile = getConfigFile();
6918
- if (!existsSync7(dir)) mkdirSync4(dir, { recursive: true });
6919
- writeFileSync6(configFile, JSON.stringify(config, null, 2), "utf-8");
7712
+ if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
7713
+ writeFileSync5(configFile, JSON.stringify(config, null, 2), "utf-8");
6920
7714
  }
6921
7715
  function getConfigValue(key) {
6922
7716
  return loadConfig()[key];
@@ -7024,26 +7818,26 @@ var configBuiltin = {
7024
7818
 
7025
7819
  // src/plugin/installer.ts
7026
7820
  import {
7027
- existsSync as existsSync14,
7821
+ existsSync as existsSync13,
7028
7822
  readdirSync as readdirSync3,
7029
- mkdirSync as mkdirSync9,
7823
+ mkdirSync as mkdirSync8,
7030
7824
  rmSync as rmSync7
7031
7825
  } from "fs";
7032
7826
  import { resolve as resolve15, basename as basename2 } from "path";
7033
7827
  import { homedir as homedir5 } from "os";
7034
7828
 
7035
7829
  // src/plugin/install-sources/local.ts
7036
- import { existsSync as existsSync9, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
7830
+ import { existsSync as existsSync8, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
7037
7831
  import { resolve as resolve10 } from "path";
7038
7832
 
7039
7833
  // src/plugin/install-utils.ts
7040
7834
  import {
7041
- existsSync as existsSync8,
7835
+ existsSync as existsSync7,
7042
7836
  readdirSync as readdirSync2,
7043
7837
  cpSync,
7044
7838
  rmSync,
7045
- mkdirSync as mkdirSync5,
7046
- readFileSync as readFileSync11,
7839
+ mkdirSync as mkdirSync4,
7840
+ readFileSync as readFileSync10,
7047
7841
  createWriteStream
7048
7842
  } from "fs";
7049
7843
  import { resolve as resolve9 } from "path";
@@ -7114,7 +7908,7 @@ async function downloadToFile(url, destPath) {
7114
7908
  await pipeline(nodeStream, createWriteStream(destPath));
7115
7909
  }
7116
7910
  function extractTarGz(tarballPath, targetDir) {
7117
- mkdirSync5(targetDir, { recursive: true });
7911
+ mkdirSync4(targetDir, { recursive: true });
7118
7912
  execSync7(`tar -xzf "${tarballPath}" -C "${targetDir}"`, { stdio: "pipe" });
7119
7913
  }
7120
7914
  function flattenPackageRoot(targetDir) {
@@ -7135,18 +7929,18 @@ function flattenPackageRoot(targetDir) {
7135
7929
  async function verifyPlugin(dir) {
7136
7930
  const warnings = [];
7137
7931
  const indexPath = resolve9(dir, "index.ts");
7138
- if (!existsSync8(indexPath)) {
7932
+ if (!existsSync7(indexPath)) {
7139
7933
  const indexJs = resolve9(dir, "index.js");
7140
- if (!existsSync8(indexJs)) {
7934
+ if (!existsSync7(indexJs)) {
7141
7935
  return { valid: false, error: "No index.ts or index.js entry point found", warnings };
7142
7936
  }
7143
7937
  }
7144
7938
  const pkgPath = resolve9(dir, "package.json");
7145
- if (!existsSync8(pkgPath)) {
7939
+ if (!existsSync7(pkgPath)) {
7146
7940
  warnings.push("No package.json found");
7147
7941
  } else {
7148
7942
  try {
7149
- const pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7943
+ const pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
7150
7944
  if (!pkg2.xbrowser) {
7151
7945
  warnings.push("No xbrowser metadata in package.json");
7152
7946
  }
@@ -7166,7 +7960,7 @@ function safeCleanup(dir) {
7166
7960
  // src/plugin/install-sources/local.ts
7167
7961
  async function installFromLocal(source, name, targetDir) {
7168
7962
  const srcPath = resolve10(source);
7169
- if (!existsSync9(srcPath)) {
7963
+ if (!existsSync8(srcPath)) {
7170
7964
  throw new Error(`Local path does not exist: ${srcPath}`);
7171
7965
  }
7172
7966
  const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
@@ -7179,7 +7973,7 @@ async function installFromLocal(source, name, targetDir) {
7179
7973
  safeCleanup(tmpTarget);
7180
7974
  throw new Error(`Invalid plugin: ${verify.error}`);
7181
7975
  }
7182
- if (existsSync9(targetDir)) {
7976
+ if (existsSync8(targetDir)) {
7183
7977
  rmSync2(targetDir, { recursive: true, force: true });
7184
7978
  }
7185
7979
  cpSync2(tmpTarget, targetDir, { recursive: true, force: true });
@@ -7199,7 +7993,7 @@ async function installFromLocal(source, name, targetDir) {
7199
7993
  }
7200
7994
 
7201
7995
  // src/plugin/install-sources/npm.ts
7202
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
7996
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
7203
7997
  import { resolve as resolve11, join as join5 } from "path";
7204
7998
  import { tmpdir as tmpdir2 } from "os";
7205
7999
  async function installFromNpm(packageName, name, targetDir) {
@@ -7220,7 +8014,7 @@ async function installFromNpm(packageName, name, targetDir) {
7220
8014
  }
7221
8015
  const tarballUrl = versionMeta.dist.tarball;
7222
8016
  const tmpDir = join5(tmpdir2(), `xbrowser-npm-${Date.now()}`);
7223
- mkdirSync6(tmpDir, { recursive: true });
8017
+ mkdirSync5(tmpDir, { recursive: true });
7224
8018
  let warnings = [];
7225
8019
  try {
7226
8020
  const tarballPath = join5(tmpDir, `${name}.tgz`);
@@ -7233,16 +8027,16 @@ async function installFromNpm(packageName, name, targetDir) {
7233
8027
  if (!verify.valid) {
7234
8028
  throw new Error(`Invalid npm plugin: ${verify.error}`);
7235
8029
  }
7236
- if (existsSync10(targetDir)) {
8030
+ if (existsSync9(targetDir)) {
7237
8031
  rmSync3(targetDir, { recursive: true, force: true });
7238
8032
  }
7239
8033
  cpSync3(extractDir, targetDir, { recursive: true, force: true });
7240
8034
  const pkgPath = resolve11(targetDir, "package.json");
7241
- if (existsSync10(pkgPath)) {
7242
- const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
8035
+ if (existsSync9(pkgPath)) {
8036
+ const pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7243
8037
  if (!pkg2._npmSource) {
7244
8038
  pkg2._npmSource = { name: packageName, version: latestVersion };
7245
- writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
8039
+ writeFileSync6(pkgPath, JSON.stringify(pkg2, null, 2));
7246
8040
  }
7247
8041
  }
7248
8042
  } finally {
@@ -7259,7 +8053,7 @@ async function installFromNpm(packageName, name, targetDir) {
7259
8053
  }
7260
8054
 
7261
8055
  // src/plugin/install-sources/git.ts
7262
- import { existsSync as existsSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
8056
+ import { existsSync as existsSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync7, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
7263
8057
  import { resolve as resolve12, join as join6 } from "path";
7264
8058
  import { tmpdir as tmpdir3 } from "os";
7265
8059
  import { execSync as execSync8 } from "child_process";
@@ -7273,17 +8067,17 @@ async function installFromGit(gitUrl, name, targetDir) {
7273
8067
  if (!verify.valid) {
7274
8068
  throw new Error(`Invalid git plugin: ${verify.error}`);
7275
8069
  }
7276
- if (existsSync11(targetDir)) {
8070
+ if (existsSync10(targetDir)) {
7277
8071
  rmSync4(targetDir, { recursive: true, force: true });
7278
8072
  }
7279
8073
  cpSync4(tmpDir, targetDir, { recursive: true, force: true });
7280
8074
  rmSync4(resolve12(targetDir, ".git"), { recursive: true, force: true });
7281
8075
  const pkgPath = resolve12(targetDir, "package.json");
7282
- if (existsSync11(pkgPath)) {
7283
- const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
8076
+ if (existsSync10(pkgPath)) {
8077
+ const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7284
8078
  if (!pkg2._gitSource) {
7285
8079
  pkg2._gitSource = { url: gitUrl };
7286
- writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
8080
+ writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
7287
8081
  }
7288
8082
  }
7289
8083
  } finally {
@@ -7300,12 +8094,12 @@ async function installFromGit(gitUrl, name, targetDir) {
7300
8094
  }
7301
8095
 
7302
8096
  // src/plugin/install-sources/url.ts
7303
- import { existsSync as existsSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, rmSync as rmSync5, cpSync as cpSync5 } from "fs";
8097
+ import { existsSync as existsSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, rmSync as rmSync5, cpSync as cpSync5 } from "fs";
7304
8098
  import { resolve as resolve13, join as join7, basename } from "path";
7305
8099
  import { tmpdir as tmpdir4 } from "os";
7306
8100
  async function installFromUrl(url, name, targetDir) {
7307
8101
  const tmpDir = join7(tmpdir4(), `xbrowser-url-${Date.now()}`);
7308
- mkdirSync7(tmpDir, { recursive: true });
8102
+ mkdirSync6(tmpDir, { recursive: true });
7309
8103
  let warnings = [];
7310
8104
  try {
7311
8105
  const fileName = basename(new URL(url).pathname) || "plugin.tar.gz";
@@ -7319,16 +8113,16 @@ async function installFromUrl(url, name, targetDir) {
7319
8113
  if (!verify.valid) {
7320
8114
  throw new Error(`Invalid plugin from URL: ${verify.error}`);
7321
8115
  }
7322
- if (existsSync12(targetDir)) {
8116
+ if (existsSync11(targetDir)) {
7323
8117
  rmSync5(targetDir, { recursive: true, force: true });
7324
8118
  }
7325
8119
  cpSync5(extractDir, targetDir, { recursive: true, force: true });
7326
8120
  const pkgPath = resolve13(targetDir, "package.json");
7327
- if (existsSync12(pkgPath)) {
7328
- const pkg2 = JSON.parse(readFileSync14(pkgPath, "utf-8"));
8121
+ if (existsSync11(pkgPath)) {
8122
+ const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
7329
8123
  if (!pkg2._urlSource) {
7330
8124
  pkg2._urlSource = { url };
7331
- writeFileSync9(pkgPath, JSON.stringify(pkg2, null, 2));
8125
+ writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
7332
8126
  }
7333
8127
  }
7334
8128
  } finally {
@@ -7346,10 +8140,10 @@ async function installFromUrl(url, name, targetDir) {
7346
8140
 
7347
8141
  // src/plugin/install-sources/marketplace.ts
7348
8142
  import {
7349
- existsSync as existsSync13,
7350
- mkdirSync as mkdirSync8,
7351
- writeFileSync as writeFileSync10,
7352
- readFileSync as readFileSync15,
8143
+ existsSync as existsSync12,
8144
+ mkdirSync as mkdirSync7,
8145
+ writeFileSync as writeFileSync9,
8146
+ readFileSync as readFileSync14,
7353
8147
  rmSync as rmSync6,
7354
8148
  cpSync as cpSync6
7355
8149
  } from "fs";
@@ -7371,12 +8165,12 @@ async function installFromMarketplace(pluginsDir, slug, options) {
7371
8165
  const plugin = detailData.data;
7372
8166
  const name = options?.name || String(plugin.slug || slug);
7373
8167
  const targetDir = resolve14(pluginsDir, name);
7374
- if (existsSync13(targetDir) && !options?.force) {
8168
+ if (existsSync12(targetDir) && !options?.force) {
7375
8169
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
7376
8170
  }
7377
- mkdirSync8(targetDir, { recursive: true });
8171
+ mkdirSync7(targetDir, { recursive: true });
7378
8172
  const tmpDir = join8(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
7379
- mkdirSync8(tmpDir, { recursive: true });
8173
+ mkdirSync7(tmpDir, { recursive: true });
7380
8174
  const realSlug = String(plugin.slug || slug);
7381
8175
  try {
7382
8176
  await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
@@ -7406,14 +8200,14 @@ function isManifestArray(data) {
7406
8200
  return Array.isArray(data) && data.length > 0 && typeof data[0].path === "string" && typeof data[0].content === "string";
7407
8201
  }
7408
8202
  function extractManifestToDir(manifest, targetDir) {
7409
- if (existsSync13(targetDir)) {
8203
+ if (existsSync12(targetDir)) {
7410
8204
  rmSync6(targetDir, { recursive: true, force: true });
7411
8205
  }
7412
- mkdirSync8(targetDir, { recursive: true });
8206
+ mkdirSync7(targetDir, { recursive: true });
7413
8207
  for (const file of manifest) {
7414
8208
  const filePath = resolve14(targetDir, file.path);
7415
- mkdirSync8(dirname2(filePath), { recursive: true });
7416
- writeFileSync10(filePath, Buffer.from(file.content, "base64"));
8209
+ mkdirSync7(dirname2(filePath), { recursive: true });
8210
+ writeFileSync9(filePath, Buffer.from(file.content, "base64"));
7417
8211
  }
7418
8212
  }
7419
8213
  function tryParseAsGzippedManifest(buffer) {
@@ -7440,7 +8234,7 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7440
8234
  const redirectUrl = tarballRes.headers.get("location");
7441
8235
  const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
7442
8236
  await downloadToFile(redirectUrl, tarballPath);
7443
- const buffer = readFileSync15(tarballPath);
8237
+ const buffer = readFileSync14(tarballPath);
7444
8238
  const manifest = tryParseAsGzippedManifest(buffer);
7445
8239
  if (manifest) {
7446
8240
  extractManifestToDir(manifest, targetDir);
@@ -7449,7 +8243,7 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7449
8243
  const extractDir = join8(tmpDir, "extracted");
7450
8244
  extractTarGz(tarballPath, extractDir);
7451
8245
  flattenPackageRoot(extractDir);
7452
- if (existsSync13(targetDir)) {
8246
+ if (existsSync12(targetDir)) {
7453
8247
  rmSync6(targetDir, { recursive: true, force: true });
7454
8248
  }
7455
8249
  cpSync6(extractDir, targetDir, { recursive: true, force: true });
@@ -7461,12 +8255,12 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7461
8255
  return;
7462
8256
  }
7463
8257
  const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
7464
- writeFileSync10(tarballPath, buffer);
8258
+ writeFileSync9(tarballPath, buffer);
7465
8259
  try {
7466
8260
  const extractDir = join8(tmpDir, "extracted");
7467
8261
  extractTarGz(tarballPath, extractDir);
7468
8262
  flattenPackageRoot(extractDir);
7469
- if (existsSync13(targetDir)) {
8263
+ if (existsSync12(targetDir)) {
7470
8264
  rmSync6(targetDir, { recursive: true, force: true });
7471
8265
  }
7472
8266
  cpSync6(extractDir, targetDir, { recursive: true, force: true });
@@ -7502,24 +8296,24 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
7502
8296
  }
7503
8297
  };
7504
8298
  const pkgPath = resolve14(targetDir, "package.json");
7505
- if (!existsSync13(pkgPath)) {
7506
- writeFileSync10(pkgPath, JSON.stringify(packageJson, null, 2));
8299
+ if (!existsSync12(pkgPath)) {
8300
+ writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
7507
8301
  } else {
7508
8302
  try {
7509
- const existing = JSON.parse(readFileSync15(pkgPath, "utf-8"));
8303
+ const existing = JSON.parse(readFileSync14(pkgPath, "utf-8"));
7510
8304
  const merged = {
7511
8305
  ...existing,
7512
8306
  xbrowser: { ...existing.xbrowser, ...packageJson.xbrowser },
7513
8307
  _marketplace: packageJson._marketplace
7514
8308
  };
7515
- writeFileSync10(pkgPath, JSON.stringify(merged, null, 2));
8309
+ writeFileSync9(pkgPath, JSON.stringify(merged, null, 2));
7516
8310
  } catch {
7517
- writeFileSync10(pkgPath, JSON.stringify(packageJson, null, 2));
8311
+ writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
7518
8312
  }
7519
8313
  }
7520
8314
  }
7521
8315
  function ensureIndexFile(plugin, name, targetDir) {
7522
- if (!existsSync13(resolve14(targetDir, "index.ts")) && !existsSync13(resolve14(targetDir, "index.js"))) {
8316
+ if (!existsSync12(resolve14(targetDir, "index.ts")) && !existsSync12(resolve14(targetDir, "index.js"))) {
7523
8317
  const commands = plugin.commands || [];
7524
8318
  const commandHandlers = commands.length > 0 ? commands.map((cmd) => {
7525
8319
  return [
@@ -7534,7 +8328,7 @@ function ensureIndexFile(plugin, name, targetDir) {
7534
8328
  ` handler: async () => ({ data: { message: 'Hello from ${name}!' }, tips: [] }),`,
7535
8329
  ` });`
7536
8330
  ].join("\n");
7537
- writeFileSync10(
8331
+ writeFileSync9(
7538
8332
  resolve14(targetDir, "index.ts"),
7539
8333
  [
7540
8334
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
@@ -7573,10 +8367,10 @@ var PluginInstaller = class {
7573
8367
  const type = this.detectSourceType(source);
7574
8368
  const name = options?.name || this.deriveName(source, type);
7575
8369
  const targetDir = resolve15(this.pluginsDir, name);
7576
- if (existsSync14(targetDir) && !options?.force) {
8370
+ if (existsSync13(targetDir) && !options?.force) {
7577
8371
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
7578
8372
  }
7579
- mkdirSync9(targetDir, { recursive: true });
8373
+ mkdirSync8(targetDir, { recursive: true });
7580
8374
  const resolvedSource = type === "npm" ? await resolveNpmPackageWithFallback(source) : source;
7581
8375
  switch (type) {
7582
8376
  case "local":
@@ -7634,7 +8428,7 @@ var PluginInstaller = class {
7634
8428
  */
7635
8429
  async uninstall(name) {
7636
8430
  const targetDir = resolve15(this.pluginsDir, name);
7637
- if (!existsSync14(targetDir)) {
8431
+ if (!existsSync13(targetDir)) {
7638
8432
  throw new Error(`Plugin "${name}" not found`);
7639
8433
  }
7640
8434
  rmSync7(targetDir, { recursive: true, force: true });
@@ -7645,7 +8439,7 @@ var PluginInstaller = class {
7645
8439
  * @returns Array of installed plugin information.
7646
8440
  */
7647
8441
  async list(_options) {
7648
- if (!existsSync14(this.pluginsDir)) return [];
8442
+ if (!existsSync13(this.pluginsDir)) return [];
7649
8443
  const entries = readdirSync3(this.pluginsDir, { withFileTypes: true });
7650
8444
  const plugins = [];
7651
8445
  for (const entry of entries) {
@@ -7653,7 +8447,7 @@ var PluginInstaller = class {
7653
8447
  const pluginPath = resolve15(this.pluginsDir, entry.name);
7654
8448
  const indexPath = resolve15(pluginPath, "index.ts");
7655
8449
  const indexJsPath = resolve15(pluginPath, "index.js");
7656
- if (!existsSync14(indexPath) && !existsSync14(indexJsPath)) continue;
8450
+ if (!existsSync13(indexPath) && !existsSync13(indexJsPath)) continue;
7657
8451
  const metadata = PluginMetadataParser.parseFromPackageJson(pluginPath);
7658
8452
  let source = "local";
7659
8453
  const pkg2 = readJsonFile(resolve15(pluginPath, "package.json"), {});
@@ -7680,10 +8474,10 @@ var PluginInstaller = class {
7680
8474
  }
7681
8475
  if (source.startsWith("file://")) {
7682
8476
  const filePath = decodeURIComponent(new URL(source).pathname);
7683
- if (existsSync14(filePath)) return "url";
8477
+ if (existsSync13(filePath)) return "url";
7684
8478
  }
7685
8479
  if (source.endsWith(".git") || source.includes("github.com/")) return "git";
7686
- if (existsSync14(resolve15(source))) return "local";
8480
+ if (existsSync13(resolve15(source))) return "local";
7687
8481
  return "npm";
7688
8482
  }
7689
8483
  deriveName(source, type) {
@@ -7758,6 +8552,7 @@ function handlePluginHelp() {
7758
8552
  " install <slug> --from-marketplace Install from marketplace",
7759
8553
  " uninstall <name> Uninstall a plugin",
7760
8554
  " list [--json] List installed plugins",
8555
+ " schema <name> [command] [--json] Show plugin contract and command forms",
7761
8556
  " reload <name> Reload a plugin",
7762
8557
  "",
7763
8558
  "Examples:",
@@ -7766,6 +8561,7 @@ function handlePluginHelp() {
7766
8561
  " xbrowser plugin install ./my-plugin",
7767
8562
  " xbrowser plugin uninstall my-plugin",
7768
8563
  " xbrowser plugin list",
8564
+ " xbrowser plugin schema my-plugin --json",
7769
8565
  " xbrowser plugin reload my-plugin"
7770
8566
  ].join("\n");
7771
8567
  }
@@ -8823,6 +9619,25 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
8823
9619
  cmdName = "text";
8824
9620
  params = { selector: options.selector || options.s };
8825
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
+ }
8826
9641
  case "back":
8827
9642
  cmdName = "back";
8828
9643
  params = {};
@@ -8845,7 +9660,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
8845
9660
  let parsedActions;
8846
9661
  if (options.action) {
8847
9662
  const actionList = Array.isArray(options.action) ? options.action : [options.action];
8848
- const { parseActionDsl } = await import("./parse-action-dsl-DRSPBALP.js");
9663
+ const { parseActionDsl } = await import("./parse-action-dsl-T3DYC33D.js");
8849
9664
  parsedActions = actionList.map((a) => parseActionDsl(a));
8850
9665
  } else if (options["actions-file"]) {
8851
9666
  const fs3 = await import("fs");
@@ -9014,11 +9829,11 @@ async function handleSession(args, options, mode, cdpEndpoint) {
9014
9829
  case "list":
9015
9830
  case "ls": {
9016
9831
  try {
9017
- const sessions = await forwardSessionList();
9018
- outputResult({ sessions }, mode);
9832
+ const sessions2 = await forwardSessionList();
9833
+ outputResult({ sessions: sessions2 }, mode);
9019
9834
  } catch {
9020
- const sessions = await listSessions();
9021
- outputResult({ sessions }, mode);
9835
+ const sessions2 = await listSessions();
9836
+ outputResult({ sessions: sessions2 }, mode);
9022
9837
  }
9023
9838
  break;
9024
9839
  }
@@ -9038,8 +9853,8 @@ async function handleSession(args, options, mode, cdpEndpoint) {
9038
9853
  }
9039
9854
  case "kill-all": {
9040
9855
  try {
9041
- const sessions = await forwardSessionList();
9042
- for (const s of sessions) {
9856
+ const sessions2 = await forwardSessionList();
9857
+ for (const s of sessions2) {
9043
9858
  try {
9044
9859
  await forwardSessionClose(s.name);
9045
9860
  } catch {
@@ -9066,15 +9881,28 @@ function getPluginLoader2() {
9066
9881
  if (!pluginLoader3) pluginLoader3 = new XBrowserPluginLoader();
9067
9882
  return pluginLoader3;
9068
9883
  }
9069
- async function buildRuntimeCommandsMap() {
9884
+ async function buildRuntimePluginInfo() {
9070
9885
  const loader = await getPluginLoader();
9071
9886
  const sites = loader.getCore().loader.getSites();
9072
9887
  const map = /* @__PURE__ */ new Map();
9073
9888
  for (const site of sites) {
9074
- const cmds = site.getAllCommands().map((c) => c.name);
9075
- if (cmds.length > 0) {
9076
- map.set(site.name, cmds);
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
+ }
9077
9903
  }
9904
+ const requiresLoginCommands = cmds.filter((c) => c.requiresLogin === true).map((c) => c.name);
9905
+ map.set(site.name, { commands: commandNames, hasLogin, loggedIn, requiresLoginCommands });
9078
9906
  }
9079
9907
  return map;
9080
9908
  }
@@ -9238,6 +10066,54 @@ async function handlePluginInfo(args, options, mode) {
9238
10066
  console.error("\u67E5\u8BE2\u5931\u8D25:", err.message);
9239
10067
  }
9240
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
+ }
9241
10117
  async function handlePlugin(args, options, mode) {
9242
10118
  const sub = args[0];
9243
10119
  const subArgs = args.slice(1);
@@ -9278,17 +10154,20 @@ async function handlePlugin(args, options, mode) {
9278
10154
  }
9279
10155
  case "list": {
9280
10156
  const plugins = await installer.list();
9281
- const runtimeCommands = await buildRuntimeCommandsMap();
10157
+ const runtimeInfo = await buildRuntimePluginInfo();
9282
10158
  const enrichedPlugins = plugins.map((p) => {
9283
10159
  const metadata = p.metadata;
9284
10160
  const staticCommands = metadata?.commands;
9285
- const dynamicCommands = runtimeCommands.get(p.name);
9286
- const commands = dynamicCommands || staticCommands;
10161
+ const rt = runtimeInfo.get(p.name);
10162
+ const commands = rt?.commands || staticCommands;
9287
10163
  return {
9288
10164
  ...p,
9289
10165
  commands,
9290
10166
  version: metadata?.version,
9291
- description: metadata?.description
10167
+ description: metadata?.description,
10168
+ hasLogin: rt?.hasLogin ?? false,
10169
+ loggedIn: rt?.loggedIn ?? null,
10170
+ requiresLoginCommands: rt?.requiresLoginCommands ?? []
9292
10171
  };
9293
10172
  });
9294
10173
  if (mode === "json") {
@@ -9299,20 +10178,27 @@ async function handlePlugin(args, options, mode) {
9299
10178
  return;
9300
10179
  }
9301
10180
  for (const p of enrichedPlugins) {
10181
+ const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
9302
10182
  if (p.version && p.description) {
9303
- console.log(`${p.name} (${p.version}) - ${p.description}`);
10183
+ console.log(`${p.name} (${p.version}) - ${p.description}${loginTag}`);
9304
10184
  } else {
9305
- console.log(p.name);
10185
+ console.log(`${p.name}${loginTag}`);
9306
10186
  }
9307
10187
  if (p.commands && p.commands.length > 0) {
9308
10188
  console.log(` ${p.commands.join(", ")}`);
9309
10189
  }
10190
+ if (p.requiresLoginCommands.length > 0) {
10191
+ console.log(` requires login: ${p.requiresLoginCommands.join(", ")}`);
10192
+ }
9310
10193
  }
9311
10194
  console.log(`
9312
10195
  Total: ${enrichedPlugins.length} plugins`);
9313
10196
  }
9314
10197
  break;
9315
10198
  }
10199
+ case "schema":
10200
+ await handlePluginSchema(subArgs, mode);
10201
+ break;
9316
10202
  case "reload": {
9317
10203
  const name = subArgs[0];
9318
10204
  if (!name) outputError("Usage: xbrowser plugin reload <name>");
@@ -9660,7 +10546,7 @@ async function handleConvert(args, _mode) {
9660
10546
  const fs3 = await import("fs");
9661
10547
  const path3 = await import("path");
9662
10548
  const { default: yaml } = await import("yaml");
9663
- const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-4DUWZIKH.js");
10549
+ const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-EGFYNICZ.js");
9664
10550
  const content = fs3.readFileSync(filePath, "utf-8");
9665
10551
  const recording = yaml.parse(content);
9666
10552
  const ext = path3.extname(outputPath).toLowerCase();
@@ -9685,7 +10571,7 @@ async function handleExtract(args, _mode) {
9685
10571
  console.error("Usage: xbrowser extract <recording.yaml>");
9686
10572
  process.exit(1);
9687
10573
  }
9688
- const { extractAndSave, printExtractSummary } = await import("./extract-EGRXZSSK.js");
10574
+ const { extractAndSave, printExtractSummary } = await import("./extract-L2IW3IUB.js");
9689
10575
  const { summary, outputPath } = extractAndSave(filePath);
9690
10576
  printExtractSummary(summary);
9691
10577
  console.log(`
@@ -9698,7 +10584,7 @@ async function handleFilter(args, _mode) {
9698
10584
  console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude-types=type1,type2]");
9699
10585
  process.exit(1);
9700
10586
  }
9701
- const { filterRecording, parseExcludeTypes } = await import("./filter-OLAE26HN.js");
10587
+ const { filterRecording, parseExcludeTypes } = await import("./filter-HC4RA7JY.js");
9702
10588
  const excludeTypes = parseExcludeTypes(args.slice(2));
9703
10589
  const result = filterRecording(filePath, outputPath, excludeTypes);
9704
10590
  console.log(`Filtered ${filePath} -> ${outputPath}`);
@@ -9707,7 +10593,7 @@ async function handleFilter(args, _mode) {
9707
10593
 
9708
10594
  // src/stdin.ts
9709
10595
  import { createInterface } from "readline";
9710
- import { readFileSync as readFileSync16 } from "fs";
10596
+ import { readFileSync as readFileSync15 } from "fs";
9711
10597
  async function readStdin2() {
9712
10598
  if (process.stdin.isTTY) return [];
9713
10599
  const lines = [];
@@ -9721,7 +10607,7 @@ async function readStdin2() {
9721
10607
  return lines;
9722
10608
  }
9723
10609
  function readCommandFile(filePath) {
9724
- const content = readFileSync16(filePath, "utf-8");
10610
+ const content = readFileSync15(filePath, "utf-8");
9725
10611
  return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
9726
10612
  }
9727
10613
 
@@ -10353,14 +11239,14 @@ async function listCommands() {
10353
11239
  return jsonResponse(200, { commands });
10354
11240
  }
10355
11241
  async function listSessions2() {
10356
- const sessions = getAllSessions().map((s) => ({
11242
+ const sessions2 = getAllSessions().map((s) => ({
10357
11243
  id: s.id,
10358
11244
  name: s.name,
10359
11245
  url: s.page?.url() ?? null,
10360
11246
  createdAt: s.createdAt,
10361
11247
  isCDP: s.isCDP ?? false
10362
11248
  }));
10363
- return jsonResponse(200, { sessions });
11249
+ return jsonResponse(200, { sessions: sessions2 });
10364
11250
  }
10365
11251
  async function createSessionHandler(req) {
10366
11252
  const body = req.body;
@@ -10758,8 +11644,23 @@ async function handleEvalMode(argv) {
10758
11644
  }
10759
11645
  async function handleChainInput(input, argv) {
10760
11646
  const cdpEndpoint = argv ? extractCdpFromArgv(argv) : void 0;
11647
+ const jsonMode = argv ? argv.includes("--json") || argv.includes("-j") : false;
10761
11648
  const chainResult = await executeChain(input, { cdpEndpoint });
10762
- printChainResult(chainResult);
11649
+ if (jsonMode) {
11650
+ const output = {
11651
+ success: chainResult.success,
11652
+ steps: chainResult.steps.map((s) => ({
11653
+ command: s.raw,
11654
+ success: s.success,
11655
+ data: s.data,
11656
+ duration: s.duration,
11657
+ ...s.hookOutputs?.length ? { hooks: s.hookOutputs } : {}
11658
+ }))
11659
+ };
11660
+ console.log(JSON.stringify(output, null, 2));
11661
+ } else {
11662
+ printChainResult(chainResult);
11663
+ }
10763
11664
  if (!chainResult.success) throw new Error("Command failed");
10764
11665
  }
10765
11666
  async function routeCommand(argv, stdinCommands) {
@@ -10784,21 +11685,6 @@ async function routeCommand(argv, stdinCommands) {
10784
11685
  const { positional, options } = parsed;
10785
11686
  const command = positional[0];
10786
11687
  const cmdArgs = positional.slice(1);
10787
- const isPluginCommand = command ? !allBuiltins.find((b) => b.name === command) && !getCommand(command) && command !== "help" : false;
10788
- if (!isPluginCommand && command) {
10789
- const unknownOptions = Object.keys(options).filter((k) => !KNOWN_GLOBAL_OPTIONS.has(k));
10790
- if (unknownOptions.length > 0) {
10791
- const unknown = unknownOptions.map((k) => `--${k}`).join(", ");
10792
- outputError(
10793
- `Unknown option: ${unknown}
10794
- Did you mean to use a global flag? Global flags must come BEFORE the command:
10795
- \u2717 xbrowser goto <url> --cdp-endpoint <endpoint> (treated as unknown)
10796
- \u2713 xbrowser --cdp <endpoint> goto <url> (correct)
10797
- Run "xbrowser --help" to see available global options.`
10798
- );
10799
- return;
10800
- }
10801
- }
10802
11688
  const mode = options.json ? "json" : options.yaml ? "yaml" : "text";
10803
11689
  const sessionName = options.session || process.env.XBROWSER_SESSION || "default";
10804
11690
  const cdpEndpoint = options.cdp;
@@ -11047,11 +11933,11 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
11047
11933
  if (options.target && !params._target) {
11048
11934
  params._target = options.target;
11049
11935
  }
11050
- const needsBrowser = cmdEntry.scope === "page";
11936
+ const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
11051
11937
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
11052
- const { forwardExec } = await import("./daemon-client-XWSSQBEA.js");
11938
+ const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
11053
11939
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
11054
- const result = await forwardExec(command, params, sessionName, cdpEndpoint, userTimeout);
11940
+ const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
11055
11941
  if (result && result.success !== false) {
11056
11942
  if (isCommandResult2(result)) {
11057
11943
  if (mode === "json" || mode === "yaml") {
@@ -11084,6 +11970,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
11084
11970
  browser: needsBrowser ? session.context.browser() : null,
11085
11971
  browserContext: needsBrowser ? session.context : null,
11086
11972
  sessionId: needsBrowser ? session.id : "",
11973
+ cdpEndpoint: cdpEndpoint || (needsBrowser ? session?.cdpEndpoint : void 0),
11087
11974
  storage: getPluginStorage(command),
11088
11975
  output: { mode, showTips: true, color: true, emoji: true },
11089
11976
  error: (msg) => {
@@ -11097,38 +11984,46 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
11097
11984
  }
11098
11985
  };
11099
11986
  try {
11987
+ const cmdStart = Date.now();
11988
+ const cmdHooks = await loadHooks();
11989
+ if (cmdHooks.length > 0 && session?.page) {
11990
+ await Promise.all(cmdHooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${command} ${subCommand}`, params })));
11991
+ }
11100
11992
  const result = await cmdEntry.handler(params, ctx);
11993
+ const hookOutputs = [];
11994
+ if (cmdHooks.length > 0 && session?.page) {
11995
+ for (const h of cmdHooks) {
11996
+ const output = await h.onAfterCommand?.({ page: session.page, command: `${command} ${subCommand}`, params, result, duration: Date.now() - cmdStart });
11997
+ if (output) hookOutputs.push({ _hook: h.name, ...output });
11998
+ }
11999
+ }
11101
12000
  if (session && result && result.data) {
11102
12001
  const convUrl = result.data.conversationUrl;
11103
12002
  if (convUrl) {
11104
12003
  saveSessionDiskMeta(sessionName, { conversationUrl: convUrl, cdpEndpoint });
11105
12004
  }
11106
12005
  }
11107
- if (isCommandResult2(result)) {
11108
- if (mode === "json" || mode === "yaml") {
11109
- console.log(outputFormatter2.format(result.data, { mode, color: false, emoji: false }));
11110
- if (result.tips?.length) {
11111
- for (const tip of result.tips) console.error(`\u{1F4A1} ${tip}`);
11112
- }
11113
- } else {
11114
- console.log(outputFormatter2.format(result.data, { mode: "text", color: true, emoji: true }));
11115
- if (result.tips?.length) {
11116
- for (const tip of result.tips) console.log(` \u{1F4A1} ${tip}`);
11117
- }
12006
+ const outputData = isCommandResult2(result) ? result.data : result && typeof result === "object" ? result.data ?? result : result;
12007
+ const tips = isCommandResult2(result) ? result.tips : result && typeof result === "object" ? result.tips : void 0;
12008
+ if (mode === "json" || mode === "yaml") {
12009
+ const finalOutput = {
12010
+ data: outputData
12011
+ };
12012
+ if (hookOutputs.length > 0) {
12013
+ finalOutput.hooks = hookOutputs;
11118
12014
  }
11119
- } else if (result && typeof result === "object") {
11120
- const obj = result;
11121
- if (mode === "json" || mode === "yaml") {
11122
- console.log(outputFormatter2.format(obj.data ?? obj, { mode, color: false, emoji: false }));
11123
- const tips = obj.tips;
11124
- if (tips?.length) {
11125
- for (const tip of tips) console.error(`\u{1F4A1} ${tip}`);
11126
- }
11127
- } else {
11128
- if (obj.data) outputResult(obj.data, mode);
11129
- const tips = obj.tips;
11130
- if (tips?.length) {
11131
- for (const tip of tips) console.log(` \u{1F4A1} ${tip}`);
12015
+ console.log(outputFormatter2.format(finalOutput, { mode, color: false, emoji: false }));
12016
+ if (tips?.length) {
12017
+ for (const tip of tips) console.error(`\u{1F4A1} ${tip}`);
12018
+ }
12019
+ } else {
12020
+ console.log(outputFormatter2.format(outputData, { mode: "text", color: true, emoji: true }));
12021
+ if (tips?.length) {
12022
+ for (const tip of tips) console.log(` \u{1F4A1} ${tip}`);
12023
+ }
12024
+ if (hookOutputs.length > 0) {
12025
+ for (const ho of hookOutputs) {
12026
+ console.log(` \u{1F4F8} screenshot: ${ho.screenshot?.url || "captured"}`);
11132
12027
  }
11133
12028
  }
11134
12029
  }
@@ -11302,7 +12197,7 @@ async function main() {
11302
12197
  const command = process.argv[2];
11303
12198
  const isLongRunning = command === "preview" || command === "serve";
11304
12199
  if (!isLongRunning) {
11305
- const { ensureProcessCanExit } = await import("./browser-GWBH6OJK.js");
12200
+ const { ensureProcessCanExit } = await import("./browser-DSVV4GHS.js");
11306
12201
  await ensureProcessCanExit().catch(() => {
11307
12202
  });
11308
12203
  process.exit(exitCode);