@xbrowser/cli 0.16.0 → 1.0.2

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 (52) hide show
  1. package/README.md +17 -26
  2. package/dist/{browser-R7B255ML.js → browser-GITRHHFO.js} +4 -1
  3. package/dist/{browser-GWBH6OJK.js → browser-R56O3CW6.js} +3 -1
  4. package/dist/{browser-I2HJZ7IP.js → browser-ZJOZB5CR.js} +4 -2
  5. package/dist/cdp-driver-BE3FOMRN.js +2803 -0
  6. package/dist/cdp-driver-TOPYJIFL.js +47 -0
  7. package/dist/chunk-2SVQTI2O.js +2794 -0
  8. package/dist/{chunk-KDYXFLAC.js → chunk-ACFE6PKF.js} +1015 -121
  9. package/dist/chunk-BBMRDUYQ.js +260 -0
  10. package/dist/chunk-CAFNSGYM.js +4834 -0
  11. package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
  12. package/dist/{chunk-RS6YYWTK.js → chunk-JPA2ZT2R.js} +140 -72
  13. package/dist/chunk-JPHCY4TC.js +260 -0
  14. package/dist/chunk-KFQGP6VL.js +33 -0
  15. package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
  16. package/dist/chunk-OZKD3W4X.js +417 -0
  17. package/dist/chunk-PPG4D2EW.js +2796 -0
  18. package/dist/{chunk-ATFTAKMN.js → chunk-Q4IGYTKR.js} +39 -7
  19. package/dist/{chunk-F3ZWFCJJ.js → chunk-QIK2I3VQ.js} +141 -72
  20. package/dist/chunk-WJRE55TN.js +83 -0
  21. package/dist/cli.js +2358 -1086
  22. package/dist/{convert-4DUWZIKH.js → convert-LB3GJTLR.js} +4 -2
  23. package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
  24. package/dist/{daemon-client-GX2UYIW4.js → daemon-client-DRCUMNHK.js} +45 -72
  25. package/dist/{daemon-client-XWSSQBEA.js → daemon-client-UZZEHHIV.js} +8 -1
  26. package/dist/daemon-main.js +3067 -1688
  27. package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
  28. package/dist/{extract-EGRXZSSK.js → extract-BSYBM4MR.js} +2 -0
  29. package/dist/{filter-OLAE26HN.js → filter-KCFO4RSV.js} +2 -0
  30. package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
  31. package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
  32. package/dist/index.d.ts +745 -148
  33. package/dist/index.js +3488 -1719
  34. package/dist/launcher-QUJ4M2VS.js +19 -0
  35. package/dist/launcher-YARP45UY.js +19 -0
  36. package/dist/{network-store-YAF5OIBH.js → network-store-XGZ25FFC.js} +1 -0
  37. package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
  38. package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
  39. package/dist/{proxy-WKGUCH2C.js → proxy-LV4BJ5RC.js} +1 -1
  40. package/dist/session-recorder-RTDGURIJ.js +8 -0
  41. package/dist/session-recorder-YI7YYM36.js +7 -0
  42. package/dist/session-replayer-GLTUICSD.js +276 -0
  43. package/dist/site-knowledge-SYC6VCDB.js +23 -0
  44. package/package.json +6 -6
  45. package/dist/chunk-2ONMTDLK.js +0 -2050
  46. package/dist/daemon-client-3IJD6X4B.js +0 -59
  47. package/dist/network-store-2S5HATEV.js +0 -194
  48. package/dist/parse-action-dsl-DRSPBALP.js +0 -72
  49. package/dist/screenshot-CWAWMXVA.js +0 -28
  50. package/dist/screenshot-MB6R7RSS.js +0 -26
  51. package/dist/session-recorder-ILSSV2UC.js +0 -6
  52. package/dist/session-recorder-XET3DNML.js +0 -7
package/dist/cli.js CHANGED
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  SessionRecorder
4
- } from "./chunk-KDYXFLAC.js";
4
+ } from "./chunk-ACFE6PKF.js";
5
+ import {
6
+ addKnownIssue,
7
+ getKnowledgePath,
8
+ init_site_knowledge,
9
+ listSiteKnowledge,
10
+ readSiteKnowledge,
11
+ readSiteKnowledgeMarkdown
12
+ } from "./chunk-OZKD3W4X.js";
5
13
  import {
6
14
  closeAllSessions,
7
15
  closeEphemeralContext,
@@ -17,7 +25,8 @@ import {
17
25
  resolveLaunchOpts,
18
26
  saveSessionDiskMeta,
19
27
  setActivePage
20
- } from "./chunk-2ONMTDLK.js";
28
+ } from "./chunk-CAFNSGYM.js";
29
+ import "./chunk-BBMRDUYQ.js";
21
30
  import {
22
31
  forwardCommandLog,
23
32
  forwardNetworkAnalyze,
@@ -38,7 +47,6 @@ import {
38
47
  forwardRecordSummary,
39
48
  forwardReplay,
40
49
  forwardSessionClose,
41
- forwardSessionCreate,
42
50
  forwardSessionList,
43
51
  forwardViewerCheckSelector,
44
52
  getDaemonConfig,
@@ -46,10 +54,11 @@ import {
46
54
  killAllDaemonProcesses,
47
55
  startDaemonProcess,
48
56
  stopDaemonProcess
49
- } from "./chunk-ATFTAKMN.js";
57
+ } from "./chunk-Q4IGYTKR.js";
58
+ import "./chunk-KFQGP6VL.js";
50
59
 
51
60
  // src/router.ts
52
- import { parseArgs, outputFormatter as outputFormatter2, isCommandResult as isCommandResult2, helpGenerator as helpGenerator2 } from "@dyyz1993/xcli-core";
61
+ import { parseArgs, outputFormatter as outputFormatter2, isCommandResult as isCommandResult2, helpGenerator as helpGenerator2, TipCollector as TipCollector2, normalizeTips as normalizeTips7, tip as makeTip } from "@dyyz1993/xcli-core";
53
62
 
54
63
  // src/utils/positional-params.ts
55
64
  import { unquote } from "@dyyz1993/xcli-core";
@@ -172,10 +181,13 @@ import {
172
181
  ok as ok25,
173
182
  fail as fail7,
174
183
  isCommandResult,
184
+ CompositeStorage,
185
+ TipCollector,
186
+ normalizeTips as normalizeTips6,
175
187
  configureArchiveStore,
176
188
  appendCommandToArchive,
177
189
  checkGuard,
178
- PluginStorage
190
+ unquote as unquote2
179
191
  } from "@dyyz1993/xcli-core";
180
192
 
181
193
  // src/commands/navigation.ts
@@ -322,10 +334,18 @@ var urlCommand = registerCommand({
322
334
  return ok({ url: ctx.page.url() });
323
335
  }
324
336
  });
337
+ registerCommand({
338
+ name: "open",
339
+ description: "Navigate to URL (alias for goto)",
340
+ scope: "page",
341
+ parameters: gotoCommand.parameters,
342
+ result: gotoCommand.result,
343
+ handler: gotoCommand.handler
344
+ });
325
345
 
326
346
  // src/commands/interaction.ts
327
347
  import { z as z2 } from "zod";
328
- import { ok as ok2 } from "@dyyz1993/xcli-core";
348
+ import { ok as ok2, normalizeTips } from "@dyyz1993/xcli-core";
329
349
 
330
350
  // src/lib/captcha.ts
331
351
  var CAPTCHA_SELECTORS = {
@@ -393,15 +413,15 @@ var clickCommand = registerCommand({
393
413
  let detectedNewPage;
394
414
  let cleanup;
395
415
  if (ctx.browserContext?.on) {
396
- const pagePromise = new Promise((resolve16) => {
416
+ const pagePromise = new Promise((resolve10) => {
397
417
  const timer = setTimeout(() => {
398
418
  ctx.browserContext.off("page", handler);
399
- resolve16(void 0);
419
+ resolve10(void 0);
400
420
  }, 3e3);
401
421
  const handler = (page2) => {
402
422
  clearTimeout(timer);
403
423
  ctx.browserContext.off("page", handler);
404
- resolve16(page2);
424
+ resolve10(page2);
405
425
  };
406
426
  ctx.browserContext.on("page", handler);
407
427
  });
@@ -436,7 +456,7 @@ var clickCommand = registerCommand({
436
456
  selector: p.selector,
437
457
  newTab: { url: newUrl, title: newTitle }
438
458
  });
439
- result.tips = [`\u65B0 Tab \u5DF2\u6253\u5F00: ${newTitle ? newTitle + " \u2014 " : ""}${newUrl}`];
459
+ result.tips = normalizeTips([`\u65B0 Tab \u5DF2\u6253\u5F00: ${newTitle ? newTitle + " \u2014 " : ""}${newUrl}`]);
440
460
  return result;
441
461
  }
442
462
  const captchaInfo = await detectCaptcha(page);
@@ -450,7 +470,7 @@ var clickCommand = registerCommand({
450
470
  tips.push(solved ? "\u2705 CAPTCHA solved!" : "\u274C CAPTCHA timeout");
451
471
  }
452
472
  const result = ok2({ selector: p.selector, captcha: captchaInfo });
453
- result.tips = tips;
473
+ result.tips = normalizeTips(tips);
454
474
  return result;
455
475
  }
456
476
  return ok2({ selector: p.selector });
@@ -535,7 +555,7 @@ var pressCommand = registerCommand({
535
555
  key: z2.string()
536
556
  }),
537
557
  handler: async (p, ctx) => {
538
- await ctx.page.press(p.selector || "body", p.key, { delay: p.delay, timeout: 1e4 });
558
+ await ctx.page.press(p.selector || "body", p.key, { timeout: 1e4 });
539
559
  return ok2({ key: p.key });
540
560
  }
541
561
  });
@@ -554,7 +574,7 @@ var selectCommand = registerCommand({
554
574
  }),
555
575
  handler: async (p, ctx) => {
556
576
  const values = typeof p.value === "string" ? [p.value] : p.value;
557
- await ctx.page.selectOption(p.selector, values, { force: true, timeout: 1e4 });
577
+ await ctx.page.selectOption(p.selector, values);
558
578
  return ok2({ selector: p.selector, value: p.value });
559
579
  }
560
580
  });
@@ -570,7 +590,7 @@ var checkCommand = registerCommand({
570
590
  selector: z2.string()
571
591
  }),
572
592
  handler: async (p, ctx) => {
573
- await ctx.page.check(p.selector, { force: true, timeout: 1e4 });
593
+ await ctx.page.check(p.selector, { timeout: 1e4 });
574
594
  return ok2({ selector: p.selector });
575
595
  }
576
596
  });
@@ -587,7 +607,7 @@ var hoverCommand = registerCommand({
587
607
  selector: z2.string()
588
608
  }),
589
609
  handler: async (p, ctx) => {
590
- await ctx.page.hover(p.selector, { modifiers: p.modifiers, force: true, timeout: 1e4 });
610
+ await ctx.page.hover(p.selector, { timeout: 1e4 });
591
611
  return ok2({ selector: p.selector });
592
612
  }
593
613
  });
@@ -785,7 +805,7 @@ var mouseCommand = registerCommand({
785
805
 
786
806
  // src/commands/evaluate.ts
787
807
  import { z as z7 } from "zod";
788
- import { ok as ok7 } from "@dyyz1993/xcli-core";
808
+ import { ok as ok7, normalizeTips as normalizeTips2 } from "@dyyz1993/xcli-core";
789
809
  var evaluateCommand = registerCommand({
790
810
  name: "eval",
791
811
  description: "Evaluate JavaScript expression in the browser",
@@ -806,10 +826,10 @@ var evaluateCommand = registerCommand({
806
826
  const result = await ctx.page.evaluate(p.expression);
807
827
  const response = ok7({ result });
808
828
  if (decision && decision.severity === "danger") {
809
- response.tips = [
829
+ response.tips = normalizeTips2([
810
830
  `\u26A0\uFE0F CDP Firewall: ${decision.reason}`,
811
831
  `\u{1F4A1} Fix: ${decision.suggestion}`
812
- ];
832
+ ]);
813
833
  }
814
834
  return response;
815
835
  }
@@ -932,7 +952,19 @@ var clearLocalStorageCommand = registerCommand({
932
952
  // src/commands/screenshot.ts
933
953
  import { z as z9 } from "zod";
934
954
  import { ok as ok9 } from "@dyyz1993/xcli-core";
935
- import { writeFileSync } from "fs";
955
+ import { writeFileSync, mkdirSync } from "fs";
956
+ import { join } from "path";
957
+ import { homedir } from "os";
958
+ var SCREENSHOTS_DIR = join(homedir(), ".xbrowser", "screenshots");
959
+ function ensureScreenshotsDir() {
960
+ mkdirSync(SCREENSHOTS_DIR, { recursive: true });
961
+ }
962
+ function generateScreenshotPath(format) {
963
+ const timestamp = Date.now();
964
+ const random = Math.random().toString(36).slice(2, 8);
965
+ const ext = format === "jpeg" ? "jpg" : "png";
966
+ return join(SCREENSHOTS_DIR, `screenshot-${timestamp}-${random}.${ext}`);
967
+ }
936
968
  var screenshotCommand = registerCommand({
937
969
  name: "screenshot",
938
970
  description: "Take a screenshot of the page or element",
@@ -942,7 +974,8 @@ var screenshotCommand = registerCommand({
942
974
  selector: z9.string().optional(),
943
975
  type: z9.enum(["png", "jpeg"]).optional(),
944
976
  fullPage: z9.boolean().optional(),
945
- output: z9.string().optional()
977
+ output: z9.string().optional(),
978
+ base64: z9.boolean().optional().describe("Return base64 data instead of file path")
946
979
  }),
947
980
  result: z9.union([
948
981
  z9.object({
@@ -957,8 +990,9 @@ var screenshotCommand = registerCommand({
957
990
  })
958
991
  ]),
959
992
  handler: async (p, ctx) => {
993
+ const format = p.type || "png";
960
994
  const options = {
961
- type: p.type || "png",
995
+ type: format,
962
996
  fullPage: p.fullPage || false
963
997
  };
964
998
  let buffer;
@@ -971,13 +1005,23 @@ var screenshotCommand = registerCommand({
971
1005
  writeFileSync(p.output, buffer);
972
1006
  return ok9({
973
1007
  output: p.output,
974
- format: p.type || "png",
1008
+ format,
1009
+ size: buffer.length
1010
+ });
1011
+ }
1012
+ if (p.base64) {
1013
+ return ok9({
1014
+ data: buffer.toString("base64"),
1015
+ format,
975
1016
  size: buffer.length
976
1017
  });
977
1018
  }
1019
+ ensureScreenshotsDir();
1020
+ const screenshotPath = generateScreenshotPath(format);
1021
+ writeFileSync(screenshotPath, buffer);
978
1022
  return ok9({
979
- data: buffer.toString("base64"),
980
- format: p.type || "png",
1023
+ output: screenshotPath,
1024
+ format,
981
1025
  size: buffer.length
982
1026
  });
983
1027
  }
@@ -1159,7 +1203,7 @@ var consoleCheckCommand = registerCommand({
1159
1203
  await page.goto(p.url, { waitUntil: "domcontentloaded" });
1160
1204
  }
1161
1205
  const messages = await page.evaluate((args) => {
1162
- return new Promise((resolve16) => {
1206
+ return new Promise((resolve10) => {
1163
1207
  const collected = [];
1164
1208
  const originalConsole = {
1165
1209
  log: console.log,
@@ -1226,7 +1270,7 @@ ${a.stack || ""}`;
1226
1270
  console.warn = originalConsole.warn;
1227
1271
  console.error = originalConsole.error;
1228
1272
  console.info = originalConsole.info;
1229
- resolve16(collected);
1273
+ resolve10(collected);
1230
1274
  }, args.duration);
1231
1275
  });
1232
1276
  }, { duration: p.duration });
@@ -1575,6 +1619,19 @@ var healthCheckCommand = registerCommand({
1575
1619
  // src/commands/actions.ts
1576
1620
  import { z as z14 } from "zod";
1577
1621
  import { ok as ok14 } from "@dyyz1993/xcli-core";
1622
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
1623
+ import { join as join2 } from "path";
1624
+ import { homedir as homedir2 } from "os";
1625
+ var SCREENSHOTS_DIR2 = join2(homedir2(), ".xbrowser", "screenshots");
1626
+ function ensureScreenshotsDir2() {
1627
+ mkdirSync2(SCREENSHOTS_DIR2, { recursive: true });
1628
+ }
1629
+ function generateScreenshotPath2(format) {
1630
+ const timestamp = Date.now();
1631
+ const random = Math.random().toString(36).slice(2, 8);
1632
+ const ext = format === "jpeg" ? "jpg" : "png";
1633
+ return join2(SCREENSHOTS_DIR2, `screenshot-${timestamp}-${random}.${ext}`);
1634
+ }
1578
1635
  var waitActionSchema = z14.object({
1579
1636
  type: z14.literal("wait"),
1580
1637
  milliseconds: z14.number().positive().optional(),
@@ -1592,7 +1649,8 @@ var screenshotActionSchema = z14.object({
1592
1649
  type: z14.literal("screenshot"),
1593
1650
  fullPage: z14.boolean().optional(),
1594
1651
  quality: z14.number().min(1).max(100).optional(),
1595
- viewport: z14.object({ width: z14.number().int().positive(), height: z14.number().int().positive() }).optional()
1652
+ viewport: z14.object({ width: z14.number().int().positive(), height: z14.number().int().positive() }).optional(),
1653
+ base64: z14.boolean().optional().describe("Return base64 data instead of file path")
1596
1654
  });
1597
1655
  var writeActionSchema = z14.object({
1598
1656
  type: z14.literal("write"),
@@ -1638,7 +1696,7 @@ async function executeAction(page, action) {
1638
1696
  if (action.selector) {
1639
1697
  await page.waitForSelector(action.selector, { timeout: 3e4 });
1640
1698
  } else if (action.milliseconds) {
1641
- await new Promise((resolve16) => setTimeout(resolve16, action.milliseconds));
1699
+ await new Promise((resolve10) => setTimeout(resolve10, action.milliseconds));
1642
1700
  } else {
1643
1701
  throw new Error("wait action requires either milliseconds or selector");
1644
1702
  }
@@ -1659,7 +1717,13 @@ async function executeAction(page, action) {
1659
1717
  quality: action.quality ?? 80,
1660
1718
  ...action.viewport ? { clip: { x: 0, y: 0, ...action.viewport } } : {}
1661
1719
  });
1662
- return { type: "screenshot", result: buf.toString("base64") };
1720
+ if (action.base64) {
1721
+ return { type: "screenshot", result: buf.toString("base64"), base64: true };
1722
+ }
1723
+ ensureScreenshotsDir2();
1724
+ const screenshotPath = generateScreenshotPath2("jpg");
1725
+ writeFileSync2(screenshotPath, buf);
1726
+ return { type: "screenshot", result: screenshotPath };
1663
1727
  }
1664
1728
  case "write":
1665
1729
  await page.keyboard.type(action.text);
@@ -1721,8 +1785,8 @@ var actionsCommand = registerCommand({
1721
1785
  results.push(result);
1722
1786
  }
1723
1787
  })();
1724
- const timeoutPromise = new Promise((resolve16) => {
1725
- setTimeout(resolve16, timeoutMs);
1788
+ const timeoutPromise = new Promise((resolve10) => {
1789
+ setTimeout(resolve10, timeoutMs);
1726
1790
  });
1727
1791
  await Promise.race([executionPromise, timeoutPromise]);
1728
1792
  const title = await ctx.page.title();
@@ -2116,7 +2180,7 @@ var scrapeCommand = registerCommand({
2116
2180
  await page.goto(p.url, { waitUntil: "commit", timeout: p.timeout });
2117
2181
  await page.waitForSelector("body", { timeout: p.timeout }).catch(() => {
2118
2182
  });
2119
- await page.waitForLoadState("networkidle", { timeout: Math.min(p.timeout, 8e3) }).catch(() => {
2183
+ await page.waitForLoadState("networkidle", Math.min(p.timeout, 8e3)).catch(() => {
2120
2184
  });
2121
2185
  await page.waitForTimeout(p.waitAfterLoad > 0 ? p.waitAfterLoad : 2e3);
2122
2186
  if (p.selector) {
@@ -2396,11 +2460,11 @@ async function navigateForMap(page, url, timeout = 15e3) {
2396
2460
  }
2397
2461
  async function extractPageLinks(page, baseUrl) {
2398
2462
  await navigateForMap(page, baseUrl);
2399
- await new Promise((resolve16) => setTimeout(resolve16, 2e3));
2463
+ await new Promise((resolve10) => setTimeout(resolve10, 2e3));
2400
2464
  await page.evaluate(() => {
2401
2465
  window.scrollTo(0, document.body.scrollHeight);
2402
2466
  });
2403
- await new Promise((resolve16) => setTimeout(resolve16, 1e3));
2467
+ await new Promise((resolve10) => setTimeout(resolve10, 1e3));
2404
2468
  const origin = new URL(baseUrl).origin;
2405
2469
  const rawLinks = await page.evaluate((evalOrigin) => {
2406
2470
  return Array.from(document.querySelectorAll("a[href]")).map((a) => {
@@ -3121,7 +3185,7 @@ async function fetchFullContent(urls, timeout, cdpEndpoint) {
3121
3185
  const pg = await context.newPage();
3122
3186
  try {
3123
3187
  await pg.goto(url, { waitUntil: "domcontentloaded", timeout });
3124
- await pg.waitForLoadState("networkidle", { timeout }).catch(() => {
3188
+ await pg.waitForLoadState("networkidle", timeout).catch(() => {
3125
3189
  });
3126
3190
  const html = await pg.content();
3127
3191
  contentMap.set(url, htmlToMarkdown(html, { onlyMainContent: true }));
@@ -3517,7 +3581,7 @@ var networkCommand = registerCommand({
3517
3581
  waitUntil: "domcontentloaded",
3518
3582
  timeout: p.timeout
3519
3583
  });
3520
- await page.waitForLoadState("networkidle", { timeout: p.timeout }).catch(() => {
3584
+ await page.waitForLoadState("networkidle", p.timeout).catch(() => {
3521
3585
  });
3522
3586
  await page.waitForTimeout(p.wait);
3523
3587
  const totalCount = captures.length;
@@ -3826,7 +3890,25 @@ var ENGINE_KEY_ENUM = z20.enum(ALL_ENGINE_KEYS);
3826
3890
 
3827
3891
  // src/commands/snapshot.ts
3828
3892
  import { z as z21 } from "zod";
3829
- import { ok as ok20, fail as fail4 } from "@dyyz1993/xcli-core";
3893
+ import { ok as ok20, fail as fail4, normalizeTips as normalizeTips3 } from "@dyyz1993/xcli-core";
3894
+
3895
+ // src/runtime/ref-store.ts
3896
+ var sessions = /* @__PURE__ */ new Map();
3897
+ function normalizeAgentRef(ref) {
3898
+ return ref.startsWith("@") ? ref.slice(1) : ref;
3899
+ }
3900
+ function replaceRefs(sessionKey2, screenHash, targets) {
3901
+ sessions.set(sessionKey2, {
3902
+ screenHash,
3903
+ targets: new Map(targets.map((target) => [target.ref, target]))
3904
+ });
3905
+ }
3906
+ function getRefTarget(sessionKey2, ref) {
3907
+ const session = sessions.get(sessionKey2);
3908
+ const target = session?.targets.get(normalizeAgentRef(ref));
3909
+ if (!session || !target) return null;
3910
+ return { screenHash: session.screenHash, target };
3911
+ }
3830
3912
 
3831
3913
  // src/utils/resolve-selector.ts
3832
3914
  function buildElementSelector(el) {
@@ -3999,9 +4081,9 @@ async function resolveSelectors(page, ariaSnapshot) {
3999
4081
  }
4000
4082
  return results;
4001
4083
  }
4002
- var REF_ONLY = /^(e\d+)$/;
4084
+ var REF_ONLY = /^@?(e\d+)$/;
4003
4085
  var refCache = /* @__PURE__ */ new Map();
4004
- async function resolveRefParams(page, params, selectorKeys, cache) {
4086
+ async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
4005
4087
  const tips = [];
4006
4088
  const newParams = { ...params };
4007
4089
  if (!selectorKeys || selectorKeys.length === 0) {
@@ -4010,7 +4092,17 @@ async function resolveRefParams(page, params, selectorKeys, cache) {
4010
4092
  for (const key of selectorKeys) {
4011
4093
  const val = params[key];
4012
4094
  if (typeof val !== "string" || !REF_ONLY.test(val)) continue;
4013
- const ref = val;
4095
+ const match = val.match(REF_ONLY);
4096
+ if (!match) continue;
4097
+ const ref = normalizeAgentRef(match[1]);
4098
+ if (sessionId) {
4099
+ const runtimeTarget = getRefTarget(sessionId, ref);
4100
+ if (runtimeTarget) {
4101
+ tips.push(`ref=@${ref} (${key}) => ${runtimeTarget.target.selector} (observe)`);
4102
+ newParams[key] = runtimeTarget.target.selector;
4103
+ continue;
4104
+ }
4105
+ }
4014
4106
  const activeCache = cache ?? refCache;
4015
4107
  const cached = activeCache.get(ref);
4016
4108
  if (cached) {
@@ -4039,9 +4131,9 @@ async function resolveRefParams(page, params, selectorKeys, cache) {
4039
4131
  }
4040
4132
 
4041
4133
  // src/utils/site-semantics.ts
4042
- import { writeFileSync as writeFileSync2, mkdirSync, existsSync, readFileSync } from "fs";
4043
- import { join, dirname } from "path";
4044
- import { homedir } from "os";
4134
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync, readFileSync } from "fs";
4135
+ import { join as join3, dirname } from "path";
4136
+ import { homedir as homedir3 } from "os";
4045
4137
  import { stringify, parse } from "yaml";
4046
4138
  import { execFile } from "child_process";
4047
4139
  var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
@@ -4118,10 +4210,10 @@ function inferAction(role, label) {
4118
4210
  return {};
4119
4211
  }
4120
4212
  function getSemanticsDir() {
4121
- return join(homedir(), ".xbrowser", "site-semantics");
4213
+ return join3(homedir3(), ".xbrowser", "site-semantics");
4122
4214
  }
4123
4215
  function getSemanticsPath(domain) {
4124
- return join(getSemanticsDir(), `${domain}.yaml`);
4216
+ return join3(getSemanticsDir(), `${domain}.yaml`);
4125
4217
  }
4126
4218
  function extractDomain2(url) {
4127
4219
  try {
@@ -4158,9 +4250,9 @@ function saveSemantics(domain, pagePath, url, elements) {
4158
4250
  }
4159
4251
  site.updated_at = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4160
4252
  if (!existsSync(dir)) {
4161
- mkdirSync(dir, { recursive: true });
4253
+ mkdirSync3(dir, { recursive: true });
4162
4254
  }
4163
- writeFileSync2(filePath, stringify(site, { lineWidth: 0 }));
4255
+ writeFileSync3(filePath, stringify(site, { lineWidth: 0 }));
4164
4256
  }
4165
4257
  function loadSemantics(domain) {
4166
4258
  const filePath = getSemanticsPath(domain);
@@ -4230,25 +4322,25 @@ aria snapshot\uFF1A
4230
4322
  async function analyzeWithLLM(ariaSnapshot) {
4231
4323
  const piBin = process.env.PI_CLI_PATH || "pi";
4232
4324
  const prompt = LLM_PROMPT.replace("{snapshot}", ariaSnapshot.slice(0, 4e3));
4233
- return new Promise((resolve16) => {
4325
+ return new Promise((resolve10) => {
4234
4326
  execFile(
4235
4327
  piBin,
4236
4328
  ["--provider", LLM_PROVIDER, "--model", LLM_MODEL, prompt],
4237
4329
  { timeout: LLM_TIMEOUT_MS, maxBuffer: 1024 * 1024 },
4238
4330
  (err, stdout, _stderr) => {
4239
4331
  if (err) {
4240
- resolve16(null);
4332
+ resolve10(null);
4241
4333
  return;
4242
4334
  }
4243
4335
  const output = (stdout || "").trim();
4244
4336
  if (!output) {
4245
- resolve16(null);
4337
+ resolve10(null);
4246
4338
  return;
4247
4339
  }
4248
4340
  try {
4249
4341
  const parsed = parse(output);
4250
4342
  if (!parsed || typeof parsed !== "object") {
4251
- resolve16(null);
4343
+ resolve10(null);
4252
4344
  return;
4253
4345
  }
4254
4346
  const elements = {};
@@ -4263,9 +4355,9 @@ async function analyzeWithLLM(ariaSnapshot) {
4263
4355
  };
4264
4356
  }
4265
4357
  }
4266
- resolve16(Object.keys(elements).length > 0 ? elements : null);
4358
+ resolve10(Object.keys(elements).length > 0 ? elements : null);
4267
4359
  } catch {
4268
- resolve16(null);
4360
+ resolve10(null);
4269
4361
  }
4270
4362
  }
4271
4363
  );
@@ -4291,6 +4383,404 @@ async function enhanceSemanticsWithLLM(url, ariaSnapshot, ruleBasedElements) {
4291
4383
  saveSemantics(domain, pathKey, url, llmElements);
4292
4384
  }
4293
4385
 
4386
+ // src/runtime/agent-runtime.ts
4387
+ function sessionKey(sessionId) {
4388
+ return sessionId || "default";
4389
+ }
4390
+ async function observePage(page, sessionId, options = {}) {
4391
+ const [title, raw] = await Promise.all([
4392
+ page.title().catch(() => ""),
4393
+ page.evaluate(
4394
+ ({ includeHidden, limit }) => {
4395
+ function hash(input) {
4396
+ let h = 2166136261;
4397
+ for (let i = 0; i < input.length; i++) {
4398
+ h ^= input.charCodeAt(i);
4399
+ h = Math.imul(h, 16777619);
4400
+ }
4401
+ return (h >>> 0).toString(16);
4402
+ }
4403
+ function cssEscape(value) {
4404
+ const css = globalThis.CSS;
4405
+ return css?.escape ? css.escape(value) : value.replace(/["\\#.:,[\]>+~*]/g, "\\$&");
4406
+ }
4407
+ function isUnique(selector2) {
4408
+ try {
4409
+ return document.querySelectorAll(selector2).length === 1;
4410
+ } catch {
4411
+ return false;
4412
+ }
4413
+ }
4414
+ function nthOfType(el) {
4415
+ const tag = el.tagName.toLowerCase();
4416
+ const parent = el.parentElement;
4417
+ if (!parent) return tag;
4418
+ const same = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
4419
+ if (same.length === 1) return tag;
4420
+ return `${tag}:nth-of-type(${same.indexOf(el) + 1})`;
4421
+ }
4422
+ function selectorFor(el) {
4423
+ const tag = el.tagName.toLowerCase();
4424
+ const id = el.getAttribute("id");
4425
+ if (id) {
4426
+ const selector2 = `#${cssEscape(id)}`;
4427
+ if (isUnique(selector2)) return selector2;
4428
+ }
4429
+ for (const attr of ["data-testid", "data-test", "data-qa", "name", "aria-label"]) {
4430
+ const value = el.getAttribute(attr);
4431
+ if (!value) continue;
4432
+ const selector2 = `[${attr}="${cssEscape(value)}"]`;
4433
+ if (isUnique(selector2)) return selector2;
4434
+ const tagged = `${tag}${selector2}`;
4435
+ if (isUnique(tagged)) return tagged;
4436
+ }
4437
+ const classes = Array.from(el.classList).slice(0, 3);
4438
+ for (const cls of classes) {
4439
+ const selector2 = `${tag}.${cssEscape(cls)}`;
4440
+ if (isUnique(selector2)) return selector2;
4441
+ }
4442
+ const parts = [];
4443
+ let cur = el;
4444
+ while (cur && cur !== document.body && cur !== document.documentElement && parts.length < 6) {
4445
+ parts.unshift(nthOfType(cur));
4446
+ const selector2 = parts.join(" > ");
4447
+ if (isUnique(selector2)) return selector2;
4448
+ cur = cur.parentElement;
4449
+ }
4450
+ return parts.join(" > ") || tag;
4451
+ }
4452
+ function roleFor(el) {
4453
+ const explicit = el.getAttribute("role");
4454
+ if (explicit) return explicit;
4455
+ const tag = el.tagName.toLowerCase();
4456
+ if (tag === "a") return "link";
4457
+ if (tag === "button") return "button";
4458
+ if (tag === "select") return "combobox";
4459
+ if (tag === "textarea") return "textbox";
4460
+ if (tag === "input") {
4461
+ const type = (el.getAttribute("type") || "text").toLowerCase();
4462
+ if (type === "checkbox") return "checkbox";
4463
+ if (type === "radio") return "radio";
4464
+ if (type === "button" || type === "submit" || type === "reset") return "button";
4465
+ return "textbox";
4466
+ }
4467
+ return tag;
4468
+ }
4469
+ function textName(el) {
4470
+ const input = el;
4471
+ const direct = el.getAttribute("aria-label") || el.getAttribute("placeholder") || el.getAttribute("title") || input.value || el.textContent || "";
4472
+ return direct.replace(/\s+/g, " ").trim().slice(0, 120);
4473
+ }
4474
+ function actionsFor(role, editable) {
4475
+ const actions = [];
4476
+ if (editable) actions.push("fill", "type");
4477
+ if (role === "combobox") actions.push("select");
4478
+ if (role === "checkbox" || role === "radio") actions.push("check");
4479
+ actions.push("click", "hover");
4480
+ return Array.from(new Set(actions));
4481
+ }
4482
+ const selector = [
4483
+ "a[href]",
4484
+ "button",
4485
+ "input",
4486
+ "textarea",
4487
+ "select",
4488
+ "summary",
4489
+ "label",
4490
+ "[role]",
4491
+ "[tabindex]",
4492
+ '[contenteditable="true"]'
4493
+ ].join(",");
4494
+ const candidates = Array.from(document.querySelectorAll(selector));
4495
+ const seen = /* @__PURE__ */ new Set();
4496
+ const targets2 = [];
4497
+ for (const el of candidates) {
4498
+ const rect = el.getBoundingClientRect();
4499
+ const style = window.getComputedStyle(el);
4500
+ const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
4501
+ if (!includeHidden && !visible) continue;
4502
+ const selectorValue = selectorFor(el);
4503
+ if (!selectorValue || seen.has(selectorValue)) continue;
4504
+ seen.add(selectorValue);
4505
+ const input = el;
4506
+ const tag = el.tagName.toLowerCase();
4507
+ const role = roleFor(el);
4508
+ const editable = tag === "textarea" || tag === "select" || tag === "input" && !["button", "submit", "reset", "checkbox", "radio"].includes((input.type || "").toLowerCase()) || el.isContentEditable;
4509
+ const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
4510
+ const checked = typeof input.checked === "boolean" && ["checkbox", "radio"].includes((input.type || "").toLowerCase()) ? input.checked : void 0;
4511
+ targets2.push({
4512
+ selector: selectorValue,
4513
+ role,
4514
+ name: textName(el),
4515
+ tag,
4516
+ visible,
4517
+ enabled,
4518
+ editable,
4519
+ ...checked !== void 0 ? { checked } : {},
4520
+ ...editable && input.value ? { value: input.value.slice(0, 120) } : {},
4521
+ ...visible ? { box: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) } } : {},
4522
+ actions: actionsFor(role, editable)
4523
+ });
4524
+ if (targets2.length >= limit) break;
4525
+ }
4526
+ const stateText = [
4527
+ location.href,
4528
+ document.title,
4529
+ document.body?.innerText?.slice(0, 5e3) || ""
4530
+ ].join("\n");
4531
+ return { screenHash: hash(stateText), targets: targets2 };
4532
+ },
4533
+ { includeHidden: !!options.includeHidden, limit: options.limit ?? 80 }
4534
+ )
4535
+ ]);
4536
+ const targets = raw.targets.map((target, index) => ({
4537
+ ref: `e${index + 1}`,
4538
+ ...target
4539
+ }));
4540
+ replaceRefs(sessionKey(sessionId), raw.screenHash, targets);
4541
+ return {
4542
+ url: page.url(),
4543
+ title,
4544
+ screenHash: raw.screenHash,
4545
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4546
+ targets
4547
+ };
4548
+ }
4549
+ function quoteName(name) {
4550
+ const cleaned = name.replace(/\s+/g, " ").trim();
4551
+ if (!cleaned) return "";
4552
+ return ` "${cleaned.replace(/"/g, '\\"')}"`;
4553
+ }
4554
+ function targetFlags(target) {
4555
+ const flags = [target.role || target.tag];
4556
+ if (!target.enabled) flags.push("disabled");
4557
+ if (target.editable) flags.push("editable");
4558
+ if (target.checked !== void 0) flags.push(target.checked ? "checked" : "unchecked");
4559
+ return flags.join(" ");
4560
+ }
4561
+ function buildSelectorMap(observation) {
4562
+ return Object.fromEntries(observation.targets.map((target) => [target.ref, target.selector]));
4563
+ }
4564
+ function formatObservationCompact(observation, options = {}) {
4565
+ const lines = [
4566
+ `Page: ${observation.title || "(untitled)"}`,
4567
+ `URL: ${observation.url}`,
4568
+ `Screen: ${observation.screenHash}`,
4569
+ ""
4570
+ ];
4571
+ if (observation.targets.length === 0) {
4572
+ lines.push("(no interactive targets)");
4573
+ } else {
4574
+ for (const target of observation.targets) {
4575
+ lines.push(`@${target.ref} [${targetFlags(target)}]${quoteName(target.name)}`);
4576
+ }
4577
+ }
4578
+ if (options.selectors && observation.targets.length > 0) {
4579
+ lines.push("", "## Selectors");
4580
+ lines.push(observation.targets.map((target) => `${target.ref}: ${target.selector}`).join(" | "));
4581
+ }
4582
+ return lines.join("\n");
4583
+ }
4584
+ async function getPageScreenHash(page) {
4585
+ const observation = await page.evaluate(() => {
4586
+ let h = 2166136261;
4587
+ const input = [location.href, document.title, document.body?.innerText?.slice(0, 5e3) || ""].join("\n");
4588
+ for (let i = 0; i < input.length; i++) {
4589
+ h ^= input.charCodeAt(i);
4590
+ h = Math.imul(h, 16777619);
4591
+ }
4592
+ return (h >>> 0).toString(16);
4593
+ });
4594
+ return observation;
4595
+ }
4596
+ async function actionability(page, selector) {
4597
+ return await page.evaluate((sel) => {
4598
+ const el = document.querySelector(sel);
4599
+ if (!el) return { ok: false, reason: "not_found" };
4600
+ const rect = el.getBoundingClientRect();
4601
+ const style = window.getComputedStyle(el);
4602
+ const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
4603
+ if (!visible) return { ok: false, reason: "not_visible" };
4604
+ const input = el;
4605
+ const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
4606
+ if (!enabled) return { ok: false, reason: "disabled" };
4607
+ const cx = rect.left + rect.width / 2;
4608
+ const cy = rect.top + rect.height / 2;
4609
+ const hit = document.elementFromPoint(cx, cy);
4610
+ if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
4611
+ return { ok: false, reason: "covered" };
4612
+ }
4613
+ return { ok: true };
4614
+ }, selector);
4615
+ }
4616
+ async function actOnPage(page, sessionId, input) {
4617
+ const normalizedRef = input.ref ? normalizeAgentRef(input.ref) : void 0;
4618
+ const refMatch = normalizedRef ? getRefTarget(sessionKey(sessionId), normalizedRef) : null;
4619
+ const selector = input.selector || refMatch?.target.selector;
4620
+ if (!selector) {
4621
+ return {
4622
+ action: input.action,
4623
+ selector: "",
4624
+ ref: normalizedRef,
4625
+ success: false,
4626
+ reason: input.ref ? "unknown_ref" : "missing_target",
4627
+ message: normalizedRef ? `Ref "${input.ref}" not found. Run observe again.` : "Provide ref or selector."
4628
+ };
4629
+ }
4630
+ const hash = await getPageScreenHash(page).catch(() => void 0);
4631
+ const stale = !!(refMatch && hash && hash !== refMatch.screenHash);
4632
+ if (!input.force) {
4633
+ const check = await actionability(page, selector);
4634
+ if (!check.ok) {
4635
+ return {
4636
+ action: input.action,
4637
+ selector,
4638
+ ref: normalizedRef,
4639
+ success: false,
4640
+ reason: stale ? "stale_ref" : check.reason,
4641
+ message: stale ? `Ref "${input.ref}" may be stale. Run observe again.` : `Target is not actionable: ${check.reason}`,
4642
+ stale,
4643
+ screenHash: hash,
4644
+ target: refMatch?.target
4645
+ };
4646
+ }
4647
+ }
4648
+ const timeout = input.timeout ?? 1e4;
4649
+ try {
4650
+ switch (input.action) {
4651
+ case "click":
4652
+ await page.locator(selector).first().click({ timeout, force: !!input.force });
4653
+ break;
4654
+ case "fill":
4655
+ if (input.value === void 0) throw new Error("fill requires value");
4656
+ await page.locator(selector).first().fill(input.value, { timeout, force: !!input.force });
4657
+ break;
4658
+ case "type":
4659
+ if (input.value === void 0) throw new Error("type requires value");
4660
+ await page.locator(selector).first().pressSequentially(input.value, { timeout });
4661
+ break;
4662
+ case "press":
4663
+ if (!input.key) throw new Error("press requires key");
4664
+ await page.locator(selector).first().press(input.key, { timeout });
4665
+ break;
4666
+ case "select":
4667
+ if (input.value === void 0) throw new Error("select requires value");
4668
+ await page.locator(selector).first().selectOption(input.value);
4669
+ break;
4670
+ case "check":
4671
+ await page.locator(selector).first().check({ timeout });
4672
+ break;
4673
+ case "hover":
4674
+ await page.locator(selector).first().hover({ timeout });
4675
+ break;
4676
+ default: {
4677
+ const neverAction = input.action;
4678
+ throw new Error(`Unsupported action: ${neverAction}`);
4679
+ }
4680
+ }
4681
+ } catch (error) {
4682
+ return {
4683
+ action: input.action,
4684
+ selector,
4685
+ ref: normalizedRef,
4686
+ success: false,
4687
+ reason: "browser_error",
4688
+ message: error.message,
4689
+ stale,
4690
+ screenHash: hash,
4691
+ target: refMatch?.target
4692
+ };
4693
+ }
4694
+ return {
4695
+ action: input.action,
4696
+ selector,
4697
+ ref: normalizedRef,
4698
+ success: true,
4699
+ stale,
4700
+ screenHash: hash,
4701
+ target: refMatch?.target
4702
+ };
4703
+ }
4704
+ function matchUrlPattern(url, pattern) {
4705
+ if (pattern.includes("*")) {
4706
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
4707
+ return new RegExp(`^${escaped}$`).test(url);
4708
+ }
4709
+ return url.includes(pattern);
4710
+ }
4711
+ async function pollUntil(timeout, pollInterval, predicate) {
4712
+ const startedAt = Date.now();
4713
+ while (Date.now() - startedAt <= timeout) {
4714
+ if (await predicate()) return true;
4715
+ await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
4716
+ }
4717
+ return false;
4718
+ }
4719
+ async function waitForPage(page, input) {
4720
+ const timeout = input.timeout ?? 3e4;
4721
+ const pollInterval = input.pollInterval ?? 200;
4722
+ const startedAt = Date.now();
4723
+ try {
4724
+ if (input.selector) {
4725
+ const state = input.state ?? "visible";
4726
+ await page.locator(input.selector).first().waitFor({ state, timeout });
4727
+ return { success: true, matched: "selector", timeout, elapsed: Date.now() - startedAt };
4728
+ }
4729
+ if (input.text) {
4730
+ await page.getByText(input.text).first().waitFor({ state: "visible", timeout });
4731
+ return { success: true, matched: "text", timeout, elapsed: Date.now() - startedAt };
4732
+ }
4733
+ if (input.url) {
4734
+ const matched = await pollUntil(timeout, pollInterval, async () => matchUrlPattern(page.url(), input.url));
4735
+ return {
4736
+ success: matched,
4737
+ matched: "url",
4738
+ timeout,
4739
+ elapsed: Date.now() - startedAt,
4740
+ ...matched ? {} : { message: `Timed out waiting for URL pattern: ${input.url}` }
4741
+ };
4742
+ }
4743
+ if (input.load) {
4744
+ await page.waitForLoadState(input.load, timeout);
4745
+ return { success: true, matched: "load", timeout, elapsed: Date.now() - startedAt };
4746
+ }
4747
+ if (input.fn) {
4748
+ await page.waitForFunction(input.fn, void 0, { timeout });
4749
+ return { success: true, matched: "fn", timeout, elapsed: Date.now() - startedAt };
4750
+ }
4751
+ if (input.screenHashChanged) {
4752
+ let screenHash = await getPageScreenHash(page);
4753
+ const matched = await pollUntil(timeout, pollInterval, async () => {
4754
+ screenHash = await getPageScreenHash(page);
4755
+ return screenHash !== input.screenHashChanged;
4756
+ });
4757
+ return {
4758
+ success: matched,
4759
+ matched: "screenHashChanged",
4760
+ timeout,
4761
+ elapsed: Date.now() - startedAt,
4762
+ screenHash,
4763
+ ...matched ? {} : { message: `Timed out waiting for screen hash to change: ${input.screenHashChanged}` }
4764
+ };
4765
+ }
4766
+ } catch (error) {
4767
+ return {
4768
+ success: false,
4769
+ matched: input.selector ? "selector" : input.text ? "text" : input.url ? "url" : input.load ? "load" : input.fn ? "fn" : "screenHashChanged",
4770
+ timeout,
4771
+ elapsed: Date.now() - startedAt,
4772
+ message: error.message
4773
+ };
4774
+ }
4775
+ return {
4776
+ success: false,
4777
+ matched: "selector",
4778
+ timeout,
4779
+ elapsed: Date.now() - startedAt,
4780
+ message: "Provide one wait predicate: selector, text, url, load, fn, or screenHashChanged."
4781
+ };
4782
+ }
4783
+
4294
4784
  // src/commands/snapshot.ts
4295
4785
  var snapshotCommand = registerCommand({
4296
4786
  name: "snapshot",
@@ -4300,7 +4790,14 @@ var snapshotCommand = registerCommand({
4300
4790
  parameters: z21.object({
4301
4791
  type: z21.enum(["aria", "text", "dom", "all"]).default("aria").describe("Snapshot type: aria (accessibility tree), text (visible text), dom (element summary), all (combined)"),
4302
4792
  selector: z21.string().optional().describe("Scope to a specific element"),
4303
- depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree")
4793
+ depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree"),
4794
+ interactive: z21.boolean().optional().default(false).describe("Return interactive agent refs only"),
4795
+ interactiveOnly: z21.boolean().optional().default(false).describe("Alias for interactive"),
4796
+ i: z21.boolean().optional().default(false).describe("Short alias for interactive"),
4797
+ compact: z21.boolean().optional().default(false).describe("Include compact xbrowser style snapshot text"),
4798
+ c: z21.boolean().optional().default(false).describe("Short alias for compact"),
4799
+ selectors: z21.boolean().optional().default(false).describe("Include ref to CSS selector map"),
4800
+ all: z21.boolean().optional().default(false).describe("Include hidden interactive targets when using interactive snapshot")
4304
4801
  }),
4305
4802
  result: z21.object({
4306
4803
  url: z21.string(),
@@ -4313,11 +4810,23 @@ var snapshotCommand = registerCommand({
4313
4810
  const page = ctx.page;
4314
4811
  const url = page.url();
4315
4812
  const title = await page.title().catch(() => "");
4813
+ if (p.interactive || p.interactiveOnly || p.i || p.compact || p.c || p.selectors) {
4814
+ const observation = await observePage(page, ctx.sessionId, {
4815
+ includeHidden: p.all
4816
+ });
4817
+ if (p.selectors) observation.selectors = buildSelectorMap(observation);
4818
+ if (p.compact || p.c || p.interactive || p.interactiveOnly || p.i) {
4819
+ observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
4820
+ }
4821
+ return ok20(observation, normalizeTips3([
4822
+ `refs refreshed for ${observation.targets.length} targets; use click @e1 or fill @e2 "text"`
4823
+ ]));
4824
+ }
4316
4825
  if (p.type === "aria") {
4317
4826
  const aria = await captureAriaSnapshot(page, p.selector, p.depth);
4318
4827
  const tips = await buildRefTips(page, aria);
4319
4828
  persistSemantics(url, aria);
4320
- return ok20({ url, title, aria }, tips);
4829
+ return ok20({ url, title, aria }, normalizeTips3(tips));
4321
4830
  }
4322
4831
  if (p.type === "text") {
4323
4832
  const text = await captureTextSnapshot(page, p.selector);
@@ -4335,7 +4844,7 @@ var snapshotCommand = registerCommand({
4335
4844
  ]);
4336
4845
  const tips = await buildRefTips(page, aria);
4337
4846
  persistSemantics(url, aria);
4338
- return ok20({ url, title, aria, text, dom }, tips);
4847
+ return ok20({ url, title, aria, text, dom }, normalizeTips3(tips));
4339
4848
  }
4340
4849
  return fail4(`Unknown snapshot type: ${p.type}`);
4341
4850
  }
@@ -4363,10 +4872,10 @@ async function buildRefTips(page, aria) {
4363
4872
  return [];
4364
4873
  }
4365
4874
  }
4366
- async function captureAriaSnapshot(page, selector, depth) {
4875
+ async function captureAriaSnapshot(page, selector, _depth) {
4367
4876
  try {
4368
4877
  const locator = selector ? page.locator(selector).first() : page.locator("body");
4369
- return await locator.ariaSnapshot({ depth });
4878
+ return await locator.ariaSnapshot();
4370
4879
  } catch {
4371
4880
  try {
4372
4881
  return await page.locator("body").ariaSnapshot();
@@ -4377,7 +4886,7 @@ async function captureAriaSnapshot(page, selector, depth) {
4377
4886
  }
4378
4887
  async function captureTextSnapshot(page, selector) {
4379
4888
  if (selector) {
4380
- return await page.locator(selector).first().innerText({ timeout: 5e3 }).catch(() => "");
4889
+ return await page.locator(selector).first().innerText().catch(() => "");
4381
4890
  }
4382
4891
  return await page.evaluate(() => document.body?.innerText || "").catch(() => "");
4383
4892
  }
@@ -4413,22 +4922,113 @@ async function captureDomSnapshot(page, selector, maxDepth) {
4413
4922
  ).catch(() => ({ tag: "error" }));
4414
4923
  }
4415
4924
 
4416
- // src/commands/tab.ts
4925
+ // src/commands/agent.ts
4417
4926
  import { z as z22 } from "zod";
4418
- import { ok as ok21, fail as fail5 } from "@dyyz1993/xcli-core";
4419
- var TabParams = z22.object({
4420
- subcommand: z22.enum(["list", "new", "close", "switch"]),
4421
- url: z22.string().optional(),
4422
- index: z22.number().int().min(0).optional()
4927
+ import { ok as ok21, normalizeTips as normalizeTips4 } from "@dyyz1993/xcli-core";
4928
+ var observeCommand = registerCommand({
4929
+ name: "observe",
4930
+ description: "Observe the current page as structured agent targets with session refs",
4931
+ scope: "page",
4932
+ parameters: z22.object({
4933
+ includeHidden: z22.boolean().optional().default(false).describe("Include hidden elements in the target list"),
4934
+ limit: z22.number().int().positive().max(300).optional().default(80).describe("Maximum number of targets to return"),
4935
+ compact: z22.boolean().optional().default(false).describe("Include compact xbrowser style snapshot text"),
4936
+ selectors: z22.boolean().optional().default(false).describe("Include ref to stable CSS selector map")
4937
+ }),
4938
+ result: z22.object({
4939
+ targets: z22.array(z22.record(z22.unknown())),
4940
+ selectors: z22.record(z22.unknown()).optional(),
4941
+ compact: z22.string().optional()
4942
+ }).passthrough(),
4943
+ handler: async (p, ctx) => {
4944
+ const observation = await observePage(ctx.page, ctx.sessionId, {
4945
+ includeHidden: p.includeHidden,
4946
+ limit: p.limit
4947
+ });
4948
+ if (p.selectors) observation.selectors = buildSelectorMap(observation);
4949
+ if (p.compact) observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
4950
+ return ok21(observation, normalizeTips4([
4951
+ `refs refreshed for ${observation.targets.length} targets; use act --ref @e1 --action click or click @e1`
4952
+ ]));
4953
+ }
4954
+ });
4955
+ var actCommand = registerCommand({
4956
+ name: "act",
4957
+ description: "Perform an agent action using an observe ref or explicit selector",
4958
+ scope: "element",
4959
+ selectorParams: ["selector"],
4960
+ parameters: z22.object({
4961
+ action: z22.enum(["click", "fill", "type", "press", "select", "check", "hover"]).default("click"),
4962
+ ref: z22.string().optional().describe("Session-scoped ref returned by observe, such as e1"),
4963
+ selector: z22.string().optional().describe("CSS selector fallback when no ref is available"),
4964
+ value: z22.string().optional().describe("Value for fill/type/select"),
4965
+ key: z22.string().optional().describe("Key for press"),
4966
+ force: z22.boolean().optional().default(false).describe("Bypass actionability checks"),
4967
+ timeout: z22.number().optional().default(1e4).describe("Playwright action timeout in milliseconds")
4968
+ }).refine((p) => !!p.ref || !!p.selector, {
4969
+ message: "Either ref or selector is required"
4970
+ }),
4971
+ handler: async (p, ctx) => {
4972
+ const result = await actOnPage(ctx.page, ctx.sessionId, { ...p });
4973
+ if (!result.success) {
4974
+ return {
4975
+ success: false,
4976
+ data: result,
4977
+ message: result.message || result.reason || "Action failed",
4978
+ tips: normalizeTips4(result.stale ? ["run observe again to refresh refs"] : [])
4979
+ };
4980
+ }
4981
+ return ok21(result, normalizeTips4(result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []));
4982
+ }
4983
+ });
4984
+ var waitForCommand = registerCommand({
4985
+ name: "waitFor",
4986
+ description: "Wait for agent predicates such as text, URL, load state, selector state, or screen hash changes",
4987
+ scope: "page",
4988
+ selectorParams: ["selector"],
4989
+ parameters: z22.object({
4990
+ selector: z22.string().optional().describe("CSS selector or observe ref to wait for"),
4991
+ state: z22.enum(["attached", "detached", "visible", "hidden"]).optional().default("visible"),
4992
+ text: z22.string().optional().describe("Visible text to wait for"),
4993
+ url: z22.string().optional().describe("URL substring or glob pattern to wait for"),
4994
+ load: z22.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Load state to wait for"),
4995
+ fn: z22.string().optional().describe("JavaScript predicate to wait for"),
4996
+ screenHashChanged: z22.string().optional().describe("Previous screenHash from observe"),
4997
+ timeout: z22.number().optional().default(3e4),
4998
+ pollInterval: z22.number().optional().default(200)
4999
+ }).refine((p) => [p.selector, p.text, p.url, p.load, p.fn, p.screenHashChanged].filter(Boolean).length === 1, {
5000
+ message: "Provide exactly one wait predicate: selector, text, url, load, fn, or screenHashChanged"
5001
+ }),
5002
+ handler: async (p, ctx) => {
5003
+ const result = await waitForPage(ctx.page, { ...p });
5004
+ if (!result.success) {
5005
+ return {
5006
+ success: false,
5007
+ data: result,
5008
+ message: result.message || `Timed out waiting for ${result.matched}`,
5009
+ tips: []
5010
+ };
5011
+ }
5012
+ return ok21(result);
5013
+ }
5014
+ });
5015
+
5016
+ // src/commands/tab.ts
5017
+ import { z as z23 } from "zod";
5018
+ import { ok as ok22, fail as fail5 } from "@dyyz1993/xcli-core";
5019
+ var TabParams = z23.object({
5020
+ subcommand: z23.enum(["list", "new", "close", "switch"]),
5021
+ url: z23.string().optional(),
5022
+ index: z23.number().int().min(0).optional()
4423
5023
  });
4424
5024
  var tabCommand = registerCommand({
4425
5025
  name: "tab",
4426
5026
  description: "Manage browser tabs: list, new, close, switch",
4427
5027
  scope: "page",
4428
5028
  parameters: TabParams,
4429
- result: z22.object({
4430
- success: z22.boolean(),
4431
- data: z22.unknown()
5029
+ result: z23.object({
5030
+ success: z23.boolean(),
5031
+ data: z23.unknown()
4432
5032
  }),
4433
5033
  handler: async (p, ctx) => {
4434
5034
  const pages = ctx.browserContext.pages();
@@ -4468,7 +5068,7 @@ function handleList(pages, ctx) {
4468
5068
  active: i === currentIndex
4469
5069
  };
4470
5070
  });
4471
- return ok21({ tabs, total: tabs.length, activeIndex: currentIndex });
5071
+ return ok22({ tabs, total: tabs.length, activeIndex: currentIndex });
4472
5072
  }
4473
5073
  async function handleNew(p, _pages, ctx) {
4474
5074
  const newPage = await ctx.browserContext.newPage();
@@ -4490,7 +5090,7 @@ async function handleNew(p, _pages, ctx) {
4490
5090
  const title = await newPage.title().catch(() => "");
4491
5091
  const allPages = ctx.browserContext.pages();
4492
5092
  const newIndex = allPages.indexOf(newPage);
4493
- return ok21({
5093
+ return ok22({
4494
5094
  index: newIndex >= 0 ? newIndex : allPages.length - 1,
4495
5095
  url: newPage.url(),
4496
5096
  title,
@@ -4518,7 +5118,7 @@ async function handleClose(p, pages, ctx) {
4518
5118
  }
4519
5119
  ctx.page = newActivePage;
4520
5120
  }
4521
- return ok21({
5121
+ return ok22({
4522
5122
  closedIndex: closeIndex,
4523
5123
  total: remainingPages.length,
4524
5124
  activeIndex: isActivePage ? closeIndex < remainingPages.length ? closeIndex : remainingPages.length - 1 : pages.indexOf(ctx.page)
@@ -4540,7 +5140,7 @@ async function handleSwitch(p, pages, ctx) {
4540
5140
  }
4541
5141
  ctx.page = targetPage;
4542
5142
  const title = await targetPage.title().catch(() => "");
4543
- return ok21({
5143
+ return ok22({
4544
5144
  index: p.index,
4545
5145
  url: targetPage.url(),
4546
5146
  title,
@@ -4549,12 +5149,13 @@ async function handleSwitch(p, pages, ctx) {
4549
5149
  }
4550
5150
 
4551
5151
  // src/commands/addinitscript.ts
4552
- import { z as z23 } from "zod";
4553
- import { ok as ok22 } from "@dyyz1993/xcli-core";
5152
+ import { z as z24 } from "zod";
5153
+ import { ok as ok23 } from "@dyyz1993/xcli-core";
4554
5154
  import { readFileSync as readFileSync2 } from "fs";
4555
5155
 
4556
5156
  // src/chain-parser.ts
4557
- import { unquote as unquote2 } from "@dyyz1993/xcli-core";
5157
+ import { splitCommand, parseCommandArgs } from "@dyyz1993/xcli-core";
5158
+ import { registerCommandDefinition } from "@dyyz1993/xcli-core";
4558
5159
  function parseCommandChain(input, options) {
4559
5160
  const result = [];
4560
5161
  let currentPipeline = [];
@@ -4645,159 +5246,56 @@ function isSpaceAround(input, pos, tokenLen) {
4645
5246
  const after = pos + tokenLen < input.length && input[pos + tokenLen] === " ";
4646
5247
  return before && after;
4647
5248
  }
4648
- function splitCommand(cmdStr) {
4649
- const parts = [];
4650
- let current = "";
4651
- let inQuote = null;
4652
- for (let i = 0; i < cmdStr.length; i++) {
4653
- const char = cmdStr[i];
4654
- if (!inQuote && (char === "'" || char === '"')) {
4655
- inQuote = char;
4656
- current += char;
4657
- continue;
4658
- }
4659
- if (inQuote && char === inQuote) {
4660
- inQuote = null;
4661
- current += char;
4662
- continue;
4663
- }
4664
- if (!inQuote && /\s/.test(char)) {
4665
- if (current.trim()) {
4666
- parts.push(current.trim());
4667
- current = "";
4668
- }
4669
- continue;
4670
- }
4671
- current += char;
4672
- }
4673
- if (current.trim()) {
4674
- parts.push(current.trim());
4675
- }
4676
- return parts;
4677
- }
4678
- var SHORT_FLAG_MAP = {
4679
- s: "selector",
4680
- v: "value"
4681
- };
4682
- function coerceValue2(raw) {
4683
- const v = unquote2(raw);
4684
- if (v === "true") return true;
4685
- if (v === "false") return false;
4686
- if (/^\d+$/.test(v)) return parseInt(v, 10);
4687
- if (/^\d+\.\d+$/.test(v)) return parseFloat(v);
4688
- if (v.startsWith("[") || v.startsWith("{")) {
4689
- try {
4690
- return JSON.parse(v);
4691
- } catch {
4692
- return v;
4693
- }
4694
- }
4695
- return v;
4696
- }
4697
- function parseCommandArgs(name, args) {
4698
- const definitions = getCommandDefinitions();
4699
- const def = definitions[name];
4700
- const positionalKeys = def ? def.positional : [];
4701
- const params = {};
4702
- let positionalIndex = 0;
4703
- for (let i = 0; i < args.length; i++) {
4704
- const raw = args[i];
4705
- const arg = unquote2(raw);
4706
- if (raw.startsWith("--")) {
4707
- const key = raw.slice(2);
4708
- const value = args[i + 1];
4709
- if (value && !value.startsWith("-")) {
4710
- params[key] = coerceValue2(value);
4711
- i++;
4712
- } else {
4713
- params[key] = true;
4714
- }
4715
- } else if (raw.startsWith("-") && raw.length === 2) {
4716
- const flag = raw[1];
4717
- const mappedKey = SHORT_FLAG_MAP[flag];
4718
- const value = args[i + 1];
4719
- if (mappedKey && value && !value.startsWith("-")) {
4720
- params[mappedKey] = coerceValue2(value);
4721
- i++;
4722
- } else if (value && !value.startsWith("-")) {
4723
- params[flag] = coerceValue2(value);
4724
- i++;
4725
- } else {
4726
- params[mappedKey || flag] = true;
4727
- }
4728
- } else {
4729
- if (positionalIndex < positionalKeys.length) {
4730
- const isLast = positionalIndex === positionalKeys.length - 1;
4731
- if (isLast && name === "eval") {
4732
- const remaining = args.slice(i).map(unquote2).join(" ");
4733
- params[positionalKeys[positionalIndex]] = remaining;
4734
- break;
4735
- }
4736
- params[positionalKeys[positionalIndex]] = arg;
4737
- positionalIndex++;
4738
- }
4739
- }
4740
- }
4741
- return { command: name, params };
4742
- }
4743
- var commandDefCache = {
4744
- goto: { positional: ["url"] },
4745
- click: { positional: ["selector"] },
4746
- fill: { positional: ["selector", "value"] },
4747
- type: { positional: ["selector", "text"] },
4748
- press: { positional: ["selector", "key"] },
4749
- select: { positional: ["selector", "value"] },
4750
- check: { positional: ["selector"] },
4751
- uncheck: { positional: ["selector"] },
4752
- hover: { positional: ["selector"] },
4753
- dblclick: { positional: ["selector"] },
4754
- wait: { positional: ["selector"] },
4755
- screenshot: { positional: [] },
4756
- eval: { positional: ["expression"] },
4757
- scroll: { positional: ["direction"] },
4758
- title: { positional: [] },
4759
- url: { positional: [] },
4760
- html: { positional: [] },
4761
- text: { positional: [] },
4762
- back: { positional: [] },
4763
- forward: { positional: [] },
4764
- refresh: { positional: [] },
4765
- console: { positional: [] },
4766
- network: { positional: [] },
4767
- perf: { positional: [] },
4768
- health: { positional: [] },
4769
- scrape: { positional: ["url"] },
4770
- structure: { positional: [] },
4771
- "get-cookies": { positional: [] },
4772
- "set-cookie": { positional: [] },
4773
- "clear-cookies": { positional: [] },
4774
- "get-local-storage": { positional: [] },
4775
- "set-local-storage": { positional: [] },
4776
- "clear-local-storage": { positional: [] },
4777
- "set-viewport": { positional: [] },
4778
- frames: { positional: [] },
4779
- frame: { positional: ["selector"] },
4780
- actions: { positional: ["url"] },
4781
- find: { positional: ["strategy", "value"] },
4782
- addinitscript: { positional: ["script"] },
4783
- tab: { positional: ["subcommand"] }
4784
- };
4785
- function getCommandDefinitions() {
4786
- return commandDefCache;
4787
- }
4788
- function registerCommandDefinition(name, positional) {
4789
- commandDefCache[name] = { positional };
4790
- }
5249
+ registerCommandDefinition("goto", ["url"]);
5250
+ registerCommandDefinition("click", ["selector"]);
5251
+ registerCommandDefinition("fill", ["selector", "value"]);
5252
+ registerCommandDefinition("type", ["selector", "text"]);
5253
+ registerCommandDefinition("press", ["selector", "key"]);
5254
+ registerCommandDefinition("select", ["selector", "value"]);
5255
+ registerCommandDefinition("check", ["selector"]);
5256
+ registerCommandDefinition("uncheck", ["selector"]);
5257
+ registerCommandDefinition("hover", ["selector"]);
5258
+ registerCommandDefinition("dblclick", ["selector"]);
5259
+ registerCommandDefinition("wait", ["selector"]);
5260
+ registerCommandDefinition("screenshot", []);
5261
+ registerCommandDefinition("eval", ["expression"]);
5262
+ registerCommandDefinition("scroll", ["direction"]);
5263
+ registerCommandDefinition("title", []);
5264
+ registerCommandDefinition("url", []);
5265
+ registerCommandDefinition("html", []);
5266
+ registerCommandDefinition("text", []);
5267
+ registerCommandDefinition("back", []);
5268
+ registerCommandDefinition("forward", []);
5269
+ registerCommandDefinition("refresh", []);
5270
+ registerCommandDefinition("console", []);
5271
+ registerCommandDefinition("network", []);
5272
+ registerCommandDefinition("perf", []);
5273
+ registerCommandDefinition("health", []);
5274
+ registerCommandDefinition("scrape", ["url"]);
5275
+ registerCommandDefinition("structure", []);
5276
+ registerCommandDefinition("get-cookies", []);
5277
+ registerCommandDefinition("set-cookie", []);
5278
+ registerCommandDefinition("clear-cookies", []);
5279
+ registerCommandDefinition("get-local-storage", []);
5280
+ registerCommandDefinition("set-local-storage", []);
5281
+ registerCommandDefinition("clear-local-storage", []);
5282
+ registerCommandDefinition("set-viewport", []);
5283
+ registerCommandDefinition("frames", []);
5284
+ registerCommandDefinition("frame", ["selector"]);
5285
+ registerCommandDefinition("actions", ["url"]);
5286
+ registerCommandDefinition("find", ["strategy", "value", "operation"]);
5287
+ registerCommandDefinition("addinitscript", ["script"]);
5288
+ registerCommandDefinition("tab", ["subcommand"]);
4791
5289
 
4792
5290
  // src/commands/addinitscript.ts
4793
- var InitScriptParams = z23.object({
4794
- script: z23.string().optional(),
4795
- file: z23.string().optional(),
4796
- stdin: z23.boolean().optional(),
4797
- name: z23.string().optional(),
4798
- list: z23.boolean().optional(),
4799
- remove: z23.string().optional(),
4800
- base64: z23.string().optional()
5291
+ var InitScriptParams = z24.object({
5292
+ script: z24.string().optional(),
5293
+ file: z24.string().optional(),
5294
+ stdin: z24.boolean().optional(),
5295
+ name: z24.string().optional(),
5296
+ list: z24.boolean().optional(),
5297
+ remove: z24.string().optional(),
5298
+ base64: z24.string().optional()
4801
5299
  });
4802
5300
  var registeredScripts = /* @__PURE__ */ new Map();
4803
5301
  function resolveScriptContent(params) {
@@ -4817,12 +5315,12 @@ function resolveScriptContent(params) {
4817
5315
  }
4818
5316
  async function readStdin() {
4819
5317
  const { createReadStream } = await import("fs");
4820
- const { createInterface: createInterface2 } = await import("readline");
4821
- return new Promise((resolve16, reject) => {
5318
+ const { createInterface } = await import("readline");
5319
+ return new Promise((resolve10, reject) => {
4822
5320
  const lines = [];
4823
- const rl = createInterface2({ input: createReadStream("/dev/stdin") });
5321
+ const rl = createInterface({ input: createReadStream("/dev/stdin") });
4824
5322
  rl.on("line", (line) => lines.push(line));
4825
- rl.on("close", () => resolve16(lines.join("\n")));
5323
+ rl.on("close", () => resolve10(lines.join("\n")));
4826
5324
  rl.on("error", reject);
4827
5325
  });
4828
5326
  }
@@ -4831,6 +5329,15 @@ var addInitScriptCommand = registerCommand({
4831
5329
  description: "Add an initialization script that runs on every page load",
4832
5330
  scope: "page",
4833
5331
  parameters: InitScriptParams,
5332
+ result: z24.object({
5333
+ scripts: z24.array(z24.object({ name: z24.string(), size: z24.number(), preview: z24.string() })).optional(),
5334
+ removed: z24.string().optional(),
5335
+ existed: z24.boolean().optional(),
5336
+ error: z24.string().optional(),
5337
+ registered: z24.string().optional(),
5338
+ hint: z24.string().optional(),
5339
+ executedImmediately: z24.boolean().optional()
5340
+ }).passthrough(),
4834
5341
  handler: async (params, ctx) => {
4835
5342
  if (params.list) {
4836
5343
  const scripts = Array.from(registeredScripts.entries()).map(([n, content2]) => ({
@@ -4838,15 +5345,15 @@ var addInitScriptCommand = registerCommand({
4838
5345
  size: content2.length,
4839
5346
  preview: content2.slice(0, 80)
4840
5347
  }));
4841
- return ok22({ scripts });
5348
+ return ok23({ scripts });
4842
5349
  }
4843
5350
  if (params.remove) {
4844
5351
  const existed = registeredScripts.delete(params.remove);
4845
- return ok22({ removed: params.remove, existed });
5352
+ return ok23({ removed: params.remove, existed });
4846
5353
  }
4847
5354
  let content = params.stdin ? await readStdin() : resolveScriptContent(params);
4848
5355
  if (!content) {
4849
- return ok22({ error: "No script content provided. Use --script, --file, --stdin, or --base64" });
5356
+ return ok23({ error: "No script content provided. Use --script, --file, --stdin, or --base64" });
4850
5357
  }
4851
5358
  const scriptName = params.name ?? `script-${Date.now()}`;
4852
5359
  registeredScripts.set(scriptName, content);
@@ -4854,74 +5361,134 @@ var addInitScriptCommand = registerCommand({
4854
5361
  try {
4855
5362
  await ctx.page.evaluate(content);
4856
5363
  } catch {
4857
- return ok22({
5364
+ return ok23({
4858
5365
  registered: scriptName,
4859
5366
  hint: "Script registered for future page loads; immediate execution skipped (page may not be ready)"
4860
5367
  });
4861
5368
  }
4862
- return ok22({ registered: scriptName, executedImmediately: true });
5369
+ return ok23({ registered: scriptName, executedImmediately: true });
4863
5370
  }
4864
5371
  });
4865
5372
  registerCommandDefinition("addinitscript", ["script"]);
4866
5373
 
4867
5374
  // src/commands/find.ts
4868
- import { z as z24 } from "zod";
4869
- import { ok as ok23, fail as fail6 } from "@dyyz1993/xcli-core";
5375
+ import { z as z25 } from "zod";
5376
+ import { ok as ok24, fail as fail6, normalizeTips as normalizeTips5 } from "@dyyz1993/xcli-core";
5377
+ var actionSchema2 = z25.enum(["click", "fill", "type", "select", "hover", "check"]);
4870
5378
  var findCommand = registerCommand({
4871
5379
  name: "find",
4872
5380
  description: "Find elements by semantic strategy (text/role/label/placeholder/testid) and optionally perform an action",
4873
5381
  scope: "page",
4874
- parameters: z24.object({
4875
- strategy: z24.enum(["text", "role", "label", "placeholder", "testid"]),
4876
- value: z24.string(),
4877
- name: z24.string().optional(),
4878
- exact: z24.boolean().optional().default(false),
4879
- click: z24.boolean().optional().default(false),
4880
- fill: z24.string().optional(),
4881
- type: z24.string().optional(),
4882
- select: z24.string().optional(),
4883
- timeout: z24.number().optional().default(1e4)
5382
+ parameters: z25.object({
5383
+ strategy: z25.enum(["text", "role", "label", "placeholder", "testid", "alt", "title", "first", "last", "nth"]),
5384
+ value: z25.string(),
5385
+ name: z25.string().optional(),
5386
+ exact: z25.boolean().optional().default(false),
5387
+ operation: z25.string().optional().describe('Trailing operation syntax, e.g. click, fill "text", type "text"'),
5388
+ action: actionSchema2.optional().describe("Action to perform when not using trailing operation syntax"),
5389
+ actionValue: z25.string().optional().describe("Value for fill/type/select when using action"),
5390
+ index: z25.number().int().optional().describe("Index for nth strategy"),
5391
+ click: z25.boolean().optional().default(false),
5392
+ fill: z25.string().optional(),
5393
+ type: z25.string().optional(),
5394
+ select: z25.string().optional(),
5395
+ hover: z25.boolean().optional().default(false),
5396
+ check: z25.boolean().optional().default(false),
5397
+ timeout: z25.number().optional().default(1e4)
4884
5398
  }),
4885
- result: z24.object({
4886
- matched: z24.number(),
4887
- selector: z24.string(),
4888
- action: z24.string().optional()
5399
+ result: z25.object({
5400
+ matched: z25.number(),
5401
+ selector: z25.string(),
5402
+ action: z25.string().optional()
4889
5403
  }),
4890
5404
  handler: async (p, ctx) => {
4891
5405
  const page = ctx.page;
4892
- const locator = buildLocator(page, p.strategy, p.value, {
5406
+ const normalized = normalizeFindParams({ ...p });
5407
+ const parsedOperation = parseOperation(normalized.operation);
5408
+ const actionName = parsedOperation.action || p.action || inferLegacyAction(p);
5409
+ const actionValue = parsedOperation.value ?? p.actionValue ?? p.fill ?? p.type ?? p.select;
5410
+ const locator = buildLocator(page, normalized.strategy, normalized.value, {
4893
5411
  name: p.name,
4894
- exact: p.exact
5412
+ exact: p.exact,
5413
+ index: normalized.index
4895
5414
  });
4896
5415
  const count = await locator.count();
4897
5416
  if (count === 0) {
4898
5417
  return fail6(`No element found with ${p.strategy}="${p.value}"`);
4899
5418
  }
4900
5419
  const tips = [];
4901
- const target = locator.first();
5420
+ const target = selectTarget(locator, p.strategy);
4902
5421
  if (count > 1) {
4903
- tips.push(`\u26A0\uFE0F Matched ${count} elements, using first`);
5422
+ tips.push(`\u26A0\uFE0F Matched ${count} elements, used first match. Use 'find nth <index> ${normalized.strategy} "${normalized.value}" ${actionName || "click"}' for a specific match.`);
4904
5423
  }
4905
- const selector = describeSelector(p.strategy, p.value, p.name);
4906
- let action;
4907
- if (p.click) {
5424
+ const selector = describeSelector(normalized.strategy, normalized.value, p.name);
5425
+ if (actionName === "click") {
4908
5426
  await target.click({ timeout: p.timeout, force: true });
4909
- action = "click";
4910
- } else if (p.fill !== void 0) {
4911
- await target.fill(p.fill, { timeout: p.timeout, force: true });
4912
- action = `fill("${p.fill}")`;
4913
- } else if (p.type !== void 0) {
4914
- await target.type(p.type, { delay: 10, timeout: p.timeout });
4915
- action = `type("${p.type}")`;
4916
- } else if (p.select !== void 0) {
4917
- await target.selectOption(p.select, { timeout: p.timeout, force: true });
4918
- action = `select("${p.select}")`;
4919
- }
4920
- const result = ok23({ matched: count, selector, action });
4921
- if (tips.length > 0) result.tips = tips;
4922
- return result;
5427
+ return okWithTips({ matched: count, selector, action: "click" }, tips);
5428
+ } else if (actionName === "fill") {
5429
+ if (actionValue === void 0) return fail6("find fill requires a value");
5430
+ await target.fill(actionValue, { timeout: p.timeout, force: true });
5431
+ return okWithTips({ matched: count, selector, action: `fill("${actionValue}")` }, tips);
5432
+ } else if (actionName === "type") {
5433
+ if (actionValue === void 0) return fail6("find type requires a value");
5434
+ await target.type(actionValue, { delay: 10, timeout: p.timeout });
5435
+ return okWithTips({ matched: count, selector, action: `type("${actionValue}")` }, tips);
5436
+ } else if (actionName === "select") {
5437
+ if (actionValue === void 0) return fail6("find select requires a value");
5438
+ await target.selectOption(actionValue);
5439
+ return okWithTips({ matched: count, selector, action: `select("${actionValue}")` }, tips);
5440
+ } else if (actionName === "hover") {
5441
+ await target.hover({ timeout: p.timeout, force: true });
5442
+ return okWithTips({ matched: count, selector, action: "hover" }, tips);
5443
+ } else if (actionName === "check") {
5444
+ await target.check({ timeout: p.timeout });
5445
+ return okWithTips({ matched: count, selector, action: "check" }, tips);
5446
+ }
5447
+ return okWithTips({ matched: count, selector }, tips);
4923
5448
  }
4924
5449
  });
5450
+ function okWithTips(data, tips) {
5451
+ const result = ok24(data);
5452
+ if (tips.length > 0) result.tips = normalizeTips5(tips);
5453
+ return result;
5454
+ }
5455
+ function parseOperation(operation) {
5456
+ if (!operation) return {};
5457
+ const match = operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
5458
+ if (!match) return {};
5459
+ const maybeAction = match[1];
5460
+ const parsed = actionSchema2.safeParse(maybeAction);
5461
+ if (!parsed.success) return {};
5462
+ const rawValue = match[2];
5463
+ const value = rawValue?.replace(/^["']|["']$/g, "");
5464
+ return { action: parsed.data, ...value !== void 0 ? { value } : {} };
5465
+ }
5466
+ function normalizeFindParams(p) {
5467
+ if (p.strategy !== "nth") return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
5468
+ const parsedIndex = Number(p.value);
5469
+ if (!Number.isInteger(parsedIndex) || !p.operation) {
5470
+ return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
5471
+ }
5472
+ const match = p.operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
5473
+ if (!match) {
5474
+ return { strategy: p.strategy, value: p.value, operation: p.operation, index: parsedIndex };
5475
+ }
5476
+ return {
5477
+ strategy: p.strategy,
5478
+ value: match[1].replace(/^["']|["']$/g, ""),
5479
+ ...match[2] ? { operation: match[2] } : {},
5480
+ index: parsedIndex
5481
+ };
5482
+ }
5483
+ function inferLegacyAction(p) {
5484
+ if (p.click) return "click";
5485
+ if (p.fill !== void 0) return "fill";
5486
+ if (p.type !== void 0) return "type";
5487
+ if (p.select !== void 0) return "select";
5488
+ if (p.hover) return "hover";
5489
+ if (p.check) return "check";
5490
+ return void 0;
5491
+ }
4925
5492
  function buildLocator(page, strategy, value, opts) {
4926
5493
  switch (strategy) {
4927
5494
  case "text":
@@ -4934,10 +5501,24 @@ function buildLocator(page, strategy, value, opts) {
4934
5501
  return page.getByPlaceholder(value, { exact: opts.exact });
4935
5502
  case "testid":
4936
5503
  return page.getByTestId(value);
5504
+ case "alt":
5505
+ return page.getByAltText(value, { exact: opts.exact });
5506
+ case "title":
5507
+ return page.getByTitle(value, { exact: opts.exact });
5508
+ case "first":
5509
+ return page.locator(value).first();
5510
+ case "last":
5511
+ return page.locator(value).last();
5512
+ case "nth":
5513
+ return page.locator(value).nth(opts.index ?? 0);
4937
5514
  default:
4938
5515
  return page.getByText(value, { exact: opts.exact });
4939
5516
  }
4940
5517
  }
5518
+ function selectTarget(locator, strategy) {
5519
+ if (strategy === "first" || strategy === "last" || strategy === "nth") return locator;
5520
+ return locator.first();
5521
+ }
4941
5522
  function describeSelector(strategy, value, name) {
4942
5523
  switch (strategy) {
4943
5524
  case "role":
@@ -4950,6 +5531,16 @@ function describeSelector(strategy, value, name) {
4950
5531
  return `getByPlaceholder("${value}")`;
4951
5532
  case "testid":
4952
5533
  return `getByTestId("${value}")`;
5534
+ case "alt":
5535
+ return `getByAltText("${value}")`;
5536
+ case "title":
5537
+ return `getByTitle("${value}")`;
5538
+ case "first":
5539
+ return `first("${value}")`;
5540
+ case "last":
5541
+ return `last("${value}")`;
5542
+ case "nth":
5543
+ return `nth("${value}")`;
4953
5544
  default:
4954
5545
  return `${strategy}("${value}")`;
4955
5546
  }
@@ -5072,7 +5663,7 @@ async function detectCaptcha2(page) {
5072
5663
  }
5073
5664
  async function detectWarningText(page) {
5074
5665
  try {
5075
- const pageText = await page.textContent("body", { timeout: 1e3 }).catch(() => "") || "";
5666
+ const pageText = await page.textContent("body").catch(() => "") || "";
5076
5667
  const lowerText = pageText.toLowerCase();
5077
5668
  for (const { text, severity } of WARNING_TEXTS) {
5078
5669
  if (lowerText.includes(text.toLowerCase())) {
@@ -5113,16 +5704,11 @@ async function detectWebdriverExposure(page) {
5113
5704
  try {
5114
5705
  const webdriver = await page.evaluate(() => {
5115
5706
  return {
5116
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5117
- webdriver: window.navigator?.webdriver,
5118
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5707
+ webdriver: window.navigator && window.navigator instanceof Object ? window.navigator.webdriver : void 0,
5119
5708
  webdriverScriptFn: !!window.__webdriver_script_fn,
5120
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5121
5709
  webdriverEvaluate: !!window.__webdriver_evaluate,
5122
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5123
5710
  chrome: !!window.chrome,
5124
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5125
- permissions: window.navigator?.permissions
5711
+ permissions: window.navigator && window.navigator instanceof Object ? window.navigator.permissions : void 0
5126
5712
  };
5127
5713
  }).catch(() => null);
5128
5714
  if (!webdriver) {
@@ -5202,380 +5788,26 @@ function formatDetectionMessage(result) {
5202
5788
  Action: ${action}`;
5203
5789
  }
5204
5790
 
5205
- // src/commands/promo.ts
5206
- import { z as z25 } from "zod";
5207
- import { ok as ok24 } from "@dyyz1993/xcli-core";
5208
- import { existsSync as existsSync2, readFileSync as readFileSync8 } from "fs";
5209
- import { resolve as resolve6 } from "path";
5791
+ // src/plugin/loader.ts
5792
+ import {
5793
+ Core
5794
+ } from "@dyyz1993/xcli-core";
5795
+ import { resolve as resolve2 } from "path";
5796
+ import { existsSync as existsSync4, readdirSync } from "fs";
5797
+ import { homedir as homedir4 } from "os";
5210
5798
 
5211
- // src/promo/devto.ts
5212
- import { readFileSync as readFileSync3 } from "fs";
5799
+ // src/plugin/metadata-parser.ts
5800
+ import { existsSync as existsSync2 } from "fs";
5213
5801
  import { resolve } from "path";
5214
- import { execSync } from "child_process";
5215
- var DEVTO_NEW_URL = "https://dev.to/new";
5216
- function ab(config) {
5217
- const parts = ["agent-browser"];
5218
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
5219
- if (config.session) parts.push("--session", config.session);
5220
- return parts.join(" ");
5221
- }
5222
- function extractTitleFromMarkdown(content) {
5223
- const match = content.match(/^#\s+(.+)$/m);
5224
- if (match) return match[1].trim();
5225
- const lines = content.split("\n").filter((l) => l.trim().length > 0);
5226
- return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
5227
- }
5228
- function stripTitleFromMarkdown(content) {
5229
- return content.replace(/^#\s+.+$\n?/m, "").trim();
5230
- }
5231
- async function publishToDevto(config) {
5232
- const cli = ab(config);
5233
- try {
5234
- const filePath = resolve(config.file);
5235
- const raw = readFileSync3(filePath, "utf-8");
5236
- const title = config.title ?? extractTitleFromMarkdown(raw);
5237
- const body = stripTitleFromMarkdown(raw);
5238
- execSync(`${cli} open ${DEVTO_NEW_URL}`, { encoding: "utf-8", timeout: 3e4 });
5239
- const snapshot = execSync(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
5240
- if (snapshot.includes("Log in") && !snapshot.includes("Notifications")) {
5241
- const viewer = execSync(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
5242
- return {
5243
- success: false,
5244
- error: `Not logged in to Dev.to. Please log in via viewer: ${viewer}`,
5245
- platform: "devto"
5246
- };
5247
- }
5248
- execSync(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
5249
- const escapedBody = JSON.stringify(body);
5250
- execSync(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
5251
- if (config.tags) {
5252
- const tags = config.tags.split(",").map((t) => t.trim()).slice(0, 4).join(", ");
5253
- execSync(`${cli} find text "tags" click`, { encoding: "utf-8", timeout: 1e4 });
5254
- execSync(`${cli} type ${JSON.stringify(tags)}`, { encoding: "utf-8", timeout: 1e4 });
5255
- }
5256
- execSync(`${cli} find text "Publish" click`, { encoding: "utf-8", timeout: 15e3 });
5257
- const postUrl = execSync(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
5258
- if (postUrl && postUrl !== DEVTO_NEW_URL && postUrl.includes("dev.to")) {
5259
- return { success: true, url: postUrl, platform: "devto" };
5260
- }
5261
- return { success: true, url: postUrl || void 0, platform: "devto" };
5262
- } catch (err) {
5263
- const message = err instanceof Error ? err.message : String(err);
5264
- return { success: false, error: message, platform: "devto" };
5265
- }
5266
- }
5267
5802
 
5268
- // src/promo/medium.ts
5269
- import { readFileSync as readFileSync4 } from "fs";
5270
- import { resolve as resolve2 } from "path";
5271
- import { execSync as execSync2 } from "child_process";
5272
- var MEDIUM_NEW_URL = "https://medium.com/new-story";
5273
- function ab2(config) {
5274
- const parts = ["agent-browser"];
5275
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
5276
- if (config.session) parts.push("--session", config.session);
5277
- return parts.join(" ");
5278
- }
5279
- function extractTitleFromMarkdown2(content) {
5280
- const match = content.match(/^#\s+(.+)$/m);
5281
- if (match) return match[1].trim();
5282
- const lines = content.split("\n").filter((l) => l.trim().length > 0);
5283
- return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
5284
- }
5285
- function markdownToPlainText(content) {
5286
- return content.replace(/^#+\s+.+$/gm, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/`(.+?)`/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/!\[.*?\]\(.+?\)/g, "").trim();
5287
- }
5288
- async function publishToMedium(config) {
5289
- const cli = ab2(config);
5803
+ // src/utils/json-file.ts
5804
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
5805
+ function readJsonFile(filePath, defaultValue) {
5290
5806
  try {
5291
- const filePath = resolve2(config.file);
5292
- const raw = readFileSync4(filePath, "utf-8");
5293
- const title = config.title ?? extractTitleFromMarkdown2(raw);
5294
- const body = markdownToPlainText(raw);
5295
- execSync2(`${cli} open ${MEDIUM_NEW_URL}`, { encoding: "utf-8", timeout: 3e4 });
5296
- const snapshot = execSync2(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
5297
- if (snapshot.includes("Sign in") && !snapshot.includes("Write")) {
5298
- const viewer = execSync2(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
5299
- return {
5300
- success: false,
5301
- error: `Not logged in to Medium. Please log in via viewer: ${viewer}`,
5302
- platform: "medium"
5303
- };
5304
- }
5305
- execSync2(`${cli} click @e_title`, { encoding: "utf-8", timeout: 1e4 });
5306
- execSync2(`${cli} type ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
5307
- execSync2(`${cli} click @e_content`, { encoding: "utf-8", timeout: 1e4 });
5308
- const lines = body.split("\n");
5309
- for (const line of lines) {
5310
- execSync2(`${cli} type ${JSON.stringify(line)}`, { encoding: "utf-8", timeout: 1e4 });
5311
- execSync2(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
5312
- }
5313
- if (config.tags) {
5314
- execSync2(`${cli} find text "Tags" click`, { encoding: "utf-8", timeout: 1e4 });
5315
- const tags = config.tags.split(",").map((t) => t.trim()).join(", ");
5316
- execSync2(`${cli} type ${JSON.stringify(tags)}`, { encoding: "utf-8", timeout: 1e4 });
5317
- }
5318
- execSync2(`${cli} find text "Publish" click`, { encoding: "utf-8", timeout: 15e3 });
5319
- const postUrl = execSync2(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
5320
- return { success: true, url: postUrl || void 0, platform: "medium" };
5321
- } catch (err) {
5322
- const message = err instanceof Error ? err.message : String(err);
5323
- return { success: false, error: message, platform: "medium" };
5324
- }
5325
- }
5326
-
5327
- // src/promo/csdn.ts
5328
- import { readFileSync as readFileSync5 } from "fs";
5329
- import { resolve as resolve3 } from "path";
5330
- import { execSync as execSync3 } from "child_process";
5331
- var CSDN_EDITOR_URL = "https://mp.csdn.net/mp_blog/creation/editor";
5332
- function ab3(config) {
5333
- const parts = ["agent-browser"];
5334
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
5335
- if (config.session) parts.push("--session", config.session);
5336
- return parts.join(" ");
5337
- }
5338
- function extractTitleFromMarkdown3(content) {
5339
- const match = content.match(/^#\s+(.+)$/m);
5340
- if (match) return match[1].trim();
5341
- const lines = content.split("\n").filter((l) => l.trim().length > 0);
5342
- return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
5343
- }
5344
- async function publishToCsdn(config) {
5345
- const cli = ab3(config);
5346
- try {
5347
- const filePath = resolve3(config.file);
5348
- const raw = readFileSync5(filePath, "utf-8");
5349
- const title = config.title ?? extractTitleFromMarkdown3(raw);
5350
- const body = raw.replace(/^#\s+.+$\n?/m, "").trim();
5351
- execSync3(`${cli} open ${CSDN_EDITOR_URL}`, { encoding: "utf-8", timeout: 3e4 });
5352
- const snapshot = execSync3(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
5353
- if (snapshot.includes("\u767B\u5F55") && !snapshot.includes("\u5199\u6587\u7AE0")) {
5354
- const viewer = execSync3(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
5355
- return {
5356
- success: false,
5357
- error: `Not logged in to CSDN. Please log in via viewer: ${viewer}`,
5358
- platform: "csdn"
5359
- };
5360
- }
5361
- execSync3(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
5362
- const escapedBody = JSON.stringify(body);
5363
- execSync3(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
5364
- if (config.tags) {
5365
- const tags = config.tags.split(",").map((t) => t.trim());
5366
- for (const tag of tags) {
5367
- execSync3(`${cli} find text "\u6DFB\u52A0\u6807\u7B7E" click`, { encoding: "utf-8", timeout: 1e4 });
5368
- execSync3(`${cli} type ${JSON.stringify(tag)}`, { encoding: "utf-8", timeout: 1e4 });
5369
- execSync3(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
5370
- }
5371
- }
5372
- execSync3(`${cli} find text "\u53D1\u5E03" click`, { encoding: "utf-8", timeout: 15e3 });
5373
- const postUrl = execSync3(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
5374
- return { success: true, url: postUrl || void 0, platform: "csdn" };
5375
- } catch (err) {
5376
- const message = err instanceof Error ? err.message : String(err);
5377
- return { success: false, error: message, platform: "csdn" };
5378
- }
5379
- }
5380
-
5381
- // src/promo/juejin.ts
5382
- import { readFileSync as readFileSync6 } from "fs";
5383
- import { resolve as resolve4 } from "path";
5384
- import { execSync as execSync4 } from "child_process";
5385
- var JUEJIN_EDITOR_URL = "https://juejin.cn/editor/draft/new";
5386
- function ab4(config) {
5387
- const parts = ["agent-browser"];
5388
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
5389
- if (config.session) parts.push("--session", config.session);
5390
- return parts.join(" ");
5391
- }
5392
- function extractTitleFromMarkdown4(content) {
5393
- const match = content.match(/^#\s+(.+)$/m);
5394
- if (match) return match[1].trim();
5395
- const lines = content.split("\n").filter((l) => l.trim().length > 0);
5396
- return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
5397
- }
5398
- async function publishToJuejin(config) {
5399
- const cli = ab4(config);
5400
- try {
5401
- const filePath = resolve4(config.file);
5402
- const raw = readFileSync6(filePath, "utf-8");
5403
- const title = config.title ?? extractTitleFromMarkdown4(raw);
5404
- const body = raw.replace(/^#\s+.+$\n?/m, "").trim();
5405
- execSync4(`${cli} open ${JUEJIN_EDITOR_URL}`, { encoding: "utf-8", timeout: 3e4 });
5406
- const snapshot = execSync4(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
5407
- if (snapshot.includes("\u767B\u5F55") && snapshot.includes("\u6CE8\u518C") && !snapshot.includes("\u521B\u4F5C\u8005\u4E2D\u5FC3")) {
5408
- const viewer = execSync4(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
5409
- return {
5410
- success: false,
5411
- error: `Not logged in to Juejin. Please log in via viewer: ${viewer}`,
5412
- platform: "juejin"
5413
- };
5414
- }
5415
- execSync4(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
5416
- const escapedBody = JSON.stringify(body);
5417
- execSync4(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
5418
- if (config.tags) {
5419
- const tags = config.tags.split(",").map((t) => t.trim());
5420
- for (const tag of tags) {
5421
- execSync4(`${cli} find text "\u6DFB\u52A0\u6807\u7B7E" click`, { encoding: "utf-8", timeout: 1e4 });
5422
- execSync4(`${cli} type ${JSON.stringify(tag)}`, { encoding: "utf-8", timeout: 1e4 });
5423
- execSync4(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
5424
- }
5425
- }
5426
- execSync4(`${cli} find text "\u53D1\u5E03" click`, { encoding: "utf-8", timeout: 15e3 });
5427
- const postUrl = execSync4(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
5428
- return { success: true, url: postUrl || void 0, platform: "juejin" };
5429
- } catch (err) {
5430
- const message = err instanceof Error ? err.message : String(err);
5431
- return { success: false, error: message, platform: "juejin" };
5432
- }
5433
- }
5434
-
5435
- // src/promo/quora.ts
5436
- import { readFileSync as readFileSync7 } from "fs";
5437
- import { resolve as resolve5 } from "path";
5438
- import { execSync as execSync5 } from "child_process";
5439
- function ab5(config) {
5440
- const parts = ["agent-browser"];
5441
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
5442
- if (config.session) parts.push("--session", config.session);
5443
- return parts.join(" ");
5444
- }
5445
- async function publishToQuora(config) {
5446
- const cli = ab5(config);
5447
- try {
5448
- if (!config.search) {
5449
- return {
5450
- success: false,
5451
- error: "Quora requires --search parameter to find relevant questions",
5452
- platform: "quora"
5453
- };
5454
- }
5455
- const filePath = resolve5(config.file);
5456
- const raw = readFileSync7(filePath, "utf-8");
5457
- const answer = raw.trim();
5458
- const searchUrl = `https://www.quora.com/search?q=${encodeURIComponent(config.search)}`;
5459
- execSync5(`${cli} open ${searchUrl}`, { encoding: "utf-8", timeout: 3e4 });
5460
- const snapshot = execSync5(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
5461
- if (!snapshot.includes("Add question")) {
5462
- const viewer = execSync5(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
5463
- return {
5464
- success: false,
5465
- error: `Not logged in to Quora. Please log in via viewer: ${viewer}`,
5466
- platform: "quora"
5467
- };
5468
- }
5469
- const answerMatch = snapshot.match(/Answer\s*(?:button|link)/i);
5470
- if (!answerMatch) {
5471
- return {
5472
- success: false,
5473
- error: "No answerable questions found for the given search query. Try a different search term.",
5474
- platform: "quora"
5475
- };
5476
- }
5477
- execSync5(`${cli} find text "Answer" click`, { encoding: "utf-8", timeout: 1e4 });
5478
- const escapedAnswer = JSON.stringify(answer);
5479
- execSync5(`${cli} fill @e_answer ${escapedAnswer}`, { encoding: "utf-8", timeout: 15e3 });
5480
- execSync5(`${cli} find text "Submit" click`, { encoding: "utf-8", timeout: 15e3 });
5481
- const postUrl = execSync5(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
5482
- return { success: true, url: postUrl || void 0, platform: "quora" };
5483
- } catch (err) {
5484
- const message = err instanceof Error ? err.message : String(err);
5485
- return { success: false, error: message, platform: "quora" };
5486
- }
5487
- }
5488
-
5489
- // src/promo/index.ts
5490
- var PUBLISHERS = {
5491
- devto: publishToDevto,
5492
- medium: publishToMedium,
5493
- csdn: publishToCsdn,
5494
- juejin: publishToJuejin,
5495
- quora: publishToQuora
5496
- };
5497
- async function dispatchPromo(config) {
5498
- const publisher = PUBLISHERS[config.platform];
5499
- if (!publisher) {
5500
- return {
5501
- success: false,
5502
- error: `Unknown platform: ${config.platform}. Supported: ${Object.keys(PUBLISHERS).join(", ")}`,
5503
- platform: config.platform
5504
- };
5505
- }
5506
- return publisher(config);
5507
- }
5508
-
5509
- // src/commands/promo.ts
5510
- var promoParams = z25.object({
5511
- platform: z25.enum(["devto", "medium", "csdn", "juejin", "quora"]).describe("Target platform for promotion"),
5512
- file: z25.string().describe("Path to Markdown file to publish"),
5513
- tags: z25.string().optional().describe("Comma-separated tags"),
5514
- title: z25.string().optional().describe("Custom title (default: extracted from file first heading)"),
5515
- search: z25.string().optional().describe("Quora: search query to find questions"),
5516
- cdpEndpoint: z25.string().optional().describe("CDP endpoint for agent-browser"),
5517
- session: z25.string().optional().describe("agent-browser session name")
5518
- }).refine(
5519
- (data) => data.platform !== "quora" || !!data.search,
5520
- { message: "Quora platform requires --search parameter" }
5521
- ).refine(
5522
- (data) => existsSync2(resolve6(data.file)),
5523
- { message: "File does not exist" }
5524
- );
5525
- var promoCommand = registerCommand({
5526
- name: "promo",
5527
- description: "Publish promotional articles to various platforms (devto, medium, csdn, juejin, quora)",
5528
- scope: "project",
5529
- parameters: promoParams,
5530
- result: z25.object({
5531
- success: z25.boolean(),
5532
- url: z25.string().optional(),
5533
- error: z25.string().optional(),
5534
- platform: z25.string()
5535
- }),
5536
- handler: async (p, _ctx) => {
5537
- const filePath = resolve6(p.file);
5538
- const content = readFileSync8(filePath, "utf-8");
5539
- if (content.trim().length === 0) {
5540
- return ok24({
5541
- success: false,
5542
- error: `File is empty: ${filePath}`,
5543
- platform: p.platform
5544
- });
5545
- }
5546
- const result = await dispatchPromo({
5547
- platform: p.platform,
5548
- file: filePath,
5549
- tags: p.tags,
5550
- title: p.title,
5551
- search: p.search,
5552
- cdpEndpoint: p.cdpEndpoint ?? _ctx.cdpEndpoint,
5553
- session: p.session ?? p.platform
5554
- });
5555
- return ok24(result);
5556
- }
5557
- });
5558
-
5559
- // src/plugin/loader.ts
5560
- import {
5561
- Core
5562
- } from "@dyyz1993/xcli-core";
5563
- import { resolve as resolve8 } from "path";
5564
- import { existsSync as existsSync5, readdirSync } from "fs";
5565
- import { homedir as homedir2 } from "os";
5566
-
5567
- // src/plugin/metadata-parser.ts
5568
- import { existsSync as existsSync3 } from "fs";
5569
- import { resolve as resolve7 } from "path";
5570
-
5571
- // src/utils/json-file.ts
5572
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
5573
- function readJsonFile(filePath, defaultValue) {
5574
- try {
5575
- const content = readFileSync9(filePath, "utf-8");
5576
- return JSON.parse(content);
5577
- } catch {
5578
- return defaultValue;
5807
+ const content = readFileSync3(filePath, "utf-8");
5808
+ return JSON.parse(content);
5809
+ } catch {
5810
+ return defaultValue;
5579
5811
  }
5580
5812
  }
5581
5813
 
@@ -5583,8 +5815,8 @@ function readJsonFile(filePath, defaultValue) {
5583
5815
  var PluginMetadataParser = class {
5584
5816
  static XBROWSER_KEYWORDS = ["xbrowser", "xbrowser-plugin"];
5585
5817
  static parseFromPackageJson(pluginPath) {
5586
- const packageJsonPath = resolve7(pluginPath, "package.json");
5587
- if (!existsSync3(packageJsonPath)) {
5818
+ const packageJsonPath = resolve(pluginPath, "package.json");
5819
+ if (!existsSync2(packageJsonPath)) {
5588
5820
  return null;
5589
5821
  }
5590
5822
  const packageJson = readJsonFile(packageJsonPath, null);
@@ -5648,20 +5880,20 @@ var PluginMetadataParser = class {
5648
5880
  };
5649
5881
 
5650
5882
  // src/plugin/ensure-deps.ts
5651
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
5652
- import { join as join2 } from "path";
5653
- import { execSync as execSync6 } from "child_process";
5883
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
5884
+ import { join as join4 } from "path";
5885
+ import { execSync } from "child_process";
5654
5886
  var SHARED_PLUGIN_DEPENDENCIES = {
5655
5887
  "zod": "^3.24.0",
5656
5888
  "@dyyz1993/xcli-core": "^0.12.1"
5657
5889
  };
5658
5890
  function ensurePluginDependencies(pluginsDir) {
5659
- const zodPath = join2(pluginsDir, "node_modules", "zod");
5660
- if (existsSync4(zodPath)) return;
5661
- mkdirSync2(pluginsDir, { recursive: true });
5662
- const pkgPath = join2(pluginsDir, "package.json");
5891
+ const zodPath = join4(pluginsDir, "node_modules", "zod");
5892
+ if (existsSync3(zodPath)) return;
5893
+ mkdirSync4(pluginsDir, { recursive: true });
5894
+ const pkgPath = join4(pluginsDir, "package.json");
5663
5895
  let pkg2 = {};
5664
- if (existsSync4(pkgPath)) {
5896
+ if (existsSync3(pkgPath)) {
5665
5897
  try {
5666
5898
  pkg2 = readJsonFile(pkgPath, {});
5667
5899
  } catch {
@@ -5675,13 +5907,13 @@ function ensurePluginDependencies(pluginsDir) {
5675
5907
  needsInstall = true;
5676
5908
  }
5677
5909
  }
5678
- if (!needsInstall && existsSync4(join2(pluginsDir, "node_modules"))) return;
5910
+ if (!needsInstall && existsSync3(join4(pluginsDir, "node_modules"))) return;
5679
5911
  pkg2.dependencies = existingDeps;
5680
5912
  pkg2.private = true;
5681
5913
  pkg2.description = pkg2.description || "xbrowser plugins \u2014 shared dependencies";
5682
- writeFileSync4(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
5914
+ writeFileSync5(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
5683
5915
  try {
5684
- execSync6("npm install --production --no-package-lock --no-fund --no-audit", {
5916
+ execSync("npm install --production --no-package-lock --no-fund --no-audit", {
5685
5917
  cwd: pluginsDir,
5686
5918
  stdio: "pipe",
5687
5919
  timeout: 6e4,
@@ -5692,6 +5924,165 @@ function ensurePluginDependencies(pluginsDir) {
5692
5924
  }
5693
5925
  }
5694
5926
 
5927
+ // src/plugin/contract.ts
5928
+ import {
5929
+ unwrapZod,
5930
+ fieldsFromZodObjectReflected,
5931
+ zodTypeToContractType
5932
+ } from "@dyyz1993/xcli-core";
5933
+ function buildPluginContract(site) {
5934
+ const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command, {
5935
+ siteRequiresLogin: site.config?.requiresLogin
5936
+ }));
5937
+ return {
5938
+ version: 2,
5939
+ plugin: {
5940
+ name: site.name,
5941
+ url: site.url,
5942
+ description: site.config?.description,
5943
+ requiresLogin: site.config?.requiresLogin
5944
+ },
5945
+ commands
5946
+ };
5947
+ }
5948
+ function buildCommandContract(command, options = {}) {
5949
+ const extension = command.xbrowser || {};
5950
+ const inferredFields = fieldsFromZodObject(command.parameters);
5951
+ const fields = mergeFields(inferredFields, extension.form?.fields || []);
5952
+ const positional = extension.positional || fields.filter((field) => field.positional).map((field) => field.name);
5953
+ const requiresLogin = command.requiresLogin === true || options.siteRequiresLogin === true && command.name !== "login" && command.name !== "logout";
5954
+ const capabilities = extension.capabilities || inferCapabilities(command.scope || "project", requiresLogin);
5955
+ const outputSchema = command.result ? summarizeZod(command.result) : void 0;
5956
+ return {
5957
+ name: command.name,
5958
+ description: command.description || "",
5959
+ scope: command.scope || "project",
5960
+ requiresLogin,
5961
+ category: extension.category,
5962
+ capabilities,
5963
+ positional,
5964
+ form: {
5965
+ title: extension.form?.title || command.description || command.name,
5966
+ description: extension.form?.description,
5967
+ submitLabel: extension.form?.submitLabel || "Run",
5968
+ fields
5969
+ },
5970
+ output: extension.output || (outputSchema ? { schema: outputSchema } : void 0)
5971
+ };
5972
+ }
5973
+ function fieldsFromZodObject(schema) {
5974
+ const reflected = fieldsFromZodObjectReflected(schema);
5975
+ return reflected.map((field) => {
5976
+ const widget = widgetFor(field.type, field.enum);
5977
+ return {
5978
+ name: field.name,
5979
+ label: toLabel(field.name),
5980
+ type: field.type,
5981
+ widget,
5982
+ required: field.required,
5983
+ ...field.description ? { description: field.description } : {},
5984
+ ...field.default !== void 0 ? { default: field.default } : {},
5985
+ ...field.enum ? { enum: field.enum } : {},
5986
+ ...field.type === "array" ? { multiple: true } : {}
5987
+ };
5988
+ });
5989
+ }
5990
+ function mergeFields(inferred, overrides) {
5991
+ if (overrides.length === 0) return inferred;
5992
+ const byName = new Map(inferred.map((field) => [field.name, field]));
5993
+ const seen = /* @__PURE__ */ new Set();
5994
+ const merged = [];
5995
+ for (const override of overrides) {
5996
+ if (!override.name) continue;
5997
+ const base = byName.get(override.name) || {
5998
+ name: override.name,
5999
+ label: toLabel(override.name),
6000
+ type: "string",
6001
+ widget: "text",
6002
+ required: false
6003
+ };
6004
+ merged.push({ ...base, ...override, name: override.name });
6005
+ seen.add(override.name);
6006
+ }
6007
+ for (const field of inferred) {
6008
+ if (!seen.has(field.name)) merged.push(field);
6009
+ }
6010
+ return merged;
6011
+ }
6012
+ function inferCapabilities(scope, requiresLogin) {
6013
+ const caps = [];
6014
+ if (scope === "page") caps.push("browser.page");
6015
+ if (scope === "browser") caps.push("browser.context");
6016
+ if (requiresLogin) caps.push("auth.login");
6017
+ return caps;
6018
+ }
6019
+ function widgetFor(type, enumValues) {
6020
+ if (enumValues) return "select";
6021
+ if (type === "boolean") return "checkbox";
6022
+ if (type === "number") return "number";
6023
+ if (type === "array") return "multi-select";
6024
+ if (type === "object") return "json";
6025
+ return "text";
6026
+ }
6027
+ function summarizeZod(schema) {
6028
+ const unwrapped = unwrapZod(schema);
6029
+ if (unwrapped.typeName === "ZodArray") {
6030
+ const def = unwrapped.schema?._def;
6031
+ return {
6032
+ type: "array",
6033
+ items: summarizeZod(def?.type || def?.innerType)
6034
+ };
6035
+ }
6036
+ const shape = getObjectShape(schema);
6037
+ if (!shape) {
6038
+ return {
6039
+ type: zodTypeToContractType(unwrapped.typeName),
6040
+ required: !unwrapped.optional,
6041
+ ...unwrapped.description ? { description: unwrapped.description } : {}
6042
+ };
6043
+ }
6044
+ return Object.fromEntries(
6045
+ Object.entries(shape).map(([name, field]) => {
6046
+ const inner = unwrapZod(field);
6047
+ return [name, {
6048
+ type: zodTypeToContractType(inner.typeName),
6049
+ required: !inner.optional,
6050
+ ...inner.description ? { description: inner.description } : {}
6051
+ }];
6052
+ })
6053
+ );
6054
+ }
6055
+ function getObjectShape(schema) {
6056
+ const zod = schema;
6057
+ const shapeOrFn = zod?.shape ?? zod?._def?.shape;
6058
+ if (!shapeOrFn) return void 0;
6059
+ return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
6060
+ }
6061
+ function toLabel(name) {
6062
+ return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
6063
+ }
6064
+
6065
+ // src/plugin/login-required-patch.ts
6066
+ import { SiteInstanceImpl } from "@dyyz1993/xcli-core";
6067
+ var patched = false;
6068
+ function patchLoginRequired() {
6069
+ if (patched) return;
6070
+ patched = true;
6071
+ const proto = SiteInstanceImpl.prototype;
6072
+ const original = proto.command;
6073
+ proto.command = function(name, cmd) {
6074
+ const result = original.call(this, name, cmd);
6075
+ const loginRequired = cmd.loginRequired;
6076
+ if (loginRequired) {
6077
+ const entry = this.commands.get(name);
6078
+ if (entry) {
6079
+ entry.loginRequired = loginRequired;
6080
+ }
6081
+ }
6082
+ return result;
6083
+ };
6084
+ }
6085
+
5695
6086
  // src/plugin/loader.ts
5696
6087
  var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
5697
6088
  var XBrowserPluginLoader = class {
@@ -5699,6 +6090,7 @@ var XBrowserPluginLoader = class {
5699
6090
  loader;
5700
6091
  options;
5701
6092
  constructor(options) {
6093
+ patchLoginRequired();
5702
6094
  this.options = options ?? {};
5703
6095
  const cwd = this.options.cwd || process.cwd();
5704
6096
  const coreConfig = {
@@ -5709,7 +6101,7 @@ var XBrowserPluginLoader = class {
5709
6101
  envPrefix: "XBROWSER",
5710
6102
  pluginDirs: [
5711
6103
  ...DEFAULT_PLUGIN_DIRS,
5712
- resolve8(cwd, ".xcli/plugins")
6104
+ resolve2(cwd, ".xcli/plugins")
5713
6105
  ]
5714
6106
  };
5715
6107
  this.core = new Core(coreConfig);
@@ -5734,6 +6126,13 @@ var XBrowserPluginLoader = class {
5734
6126
  getLoadedPlugins() {
5735
6127
  return this.loader.getLoadedPlugins();
5736
6128
  }
6129
+ getPluginContract(siteName, commandName) {
6130
+ const site = this.core.loader.getSite(siteName);
6131
+ if (!site) return void 0;
6132
+ const contract = buildPluginContract(site);
6133
+ if (!commandName) return contract;
6134
+ return contract.commands.find((command) => command.name === commandName);
6135
+ }
5737
6136
  async loadPlugin(pluginPath, id) {
5738
6137
  return this.loader.loadPlugin(pluginPath, id);
5739
6138
  }
@@ -5748,31 +6147,31 @@ var XBrowserPluginLoader = class {
5748
6147
  }
5749
6148
  async scanAndLoad() {
5750
6149
  const cwd = this.options.cwd || process.cwd();
5751
- const globalDir = this.options.globalDir || resolve8(homedir2(), ".xbrowser/plugins");
6150
+ const globalDir = this.options.globalDir || resolve2(homedir4(), ".xbrowser/plugins");
5752
6151
  ensurePluginDependencies(globalDir);
5753
6152
  const dirs = [
5754
- resolve8(cwd, ".xcli/plugins"),
5755
- resolve8(cwd, "../.xcli/plugins"),
5756
- this.options.userDir || resolve8(homedir2(), ".xcli/plugins"),
6153
+ resolve2(cwd, ".xcli/plugins"),
6154
+ resolve2(cwd, "../.xcli/plugins"),
6155
+ this.options.userDir || resolve2(homedir4(), ".xcli/plugins"),
5757
6156
  globalDir
5758
6157
  ];
5759
6158
  const loaded = [];
5760
6159
  const seen = /* @__PURE__ */ new Set();
5761
6160
  for (const dir of dirs) {
5762
- if (!existsSync5(dir)) continue;
6161
+ if (!existsSync4(dir)) continue;
5763
6162
  const entries = readdirSync(dir, { withFileTypes: true });
5764
6163
  for (const entry of entries) {
5765
6164
  if (!entry.isDirectory()) continue;
5766
6165
  if (seen.has(entry.name)) continue;
5767
6166
  seen.add(entry.name);
5768
- const pluginDir = resolve8(dir, entry.name);
5769
- let indexPath = resolve8(pluginDir, "index.js");
5770
- if (!existsSync5(indexPath)) {
5771
- indexPath = resolve8(pluginDir, "index.ts");
6167
+ const pluginDir = resolve2(dir, entry.name);
6168
+ let indexPath = resolve2(pluginDir, "index.js");
6169
+ if (!existsSync4(indexPath)) {
6170
+ indexPath = resolve2(pluginDir, "index.ts");
5772
6171
  }
5773
- if (!existsSync5(indexPath)) continue;
6172
+ if (!existsSync4(indexPath)) continue;
5774
6173
  try {
5775
- if (!existsSync5(resolve8(pluginDir, "package.json"))) {
6174
+ if (!existsSync4(resolve2(pluginDir, "package.json"))) {
5776
6175
  console.warn(`\u26A0\uFE0F Plugin "${entry.name}" has no package.json. Use "xbrowser create ${entry.name} --template static" for proper structure.`);
5777
6176
  } else {
5778
6177
  const metadata = PluginMetadataParser.parseFromPackageJson(pluginDir);
@@ -5810,6 +6209,110 @@ async function getPluginLoader() {
5810
6209
  return pluginLoader;
5811
6210
  }
5812
6211
 
6212
+ // src/utils/viewer-url.ts
6213
+ function buildViewerUrl(sessionName = "default") {
6214
+ try {
6215
+ const status = getDaemonProcessStatus();
6216
+ if (!status.running) return void 0;
6217
+ const port = status.port || getDaemonConfig().basePort;
6218
+ return `http://localhost:${port}/preview/${encodeURIComponent(sessionName)}`;
6219
+ } catch {
6220
+ return void 0;
6221
+ }
6222
+ }
6223
+
6224
+ // src/plugin/login-guard.ts
6225
+ async function checkPluginLoginRequired(options) {
6226
+ const { site, command, commandName, ctx, page, sessionName } = options;
6227
+ if (commandName === "login" || commandName === "logout") return { ok: true };
6228
+ const loginConfig = site.config?.loginConfig;
6229
+ const requiresLogin = command.requiresLogin === true || site.config?.requiresLogin === true || loginConfig?.requiresLogin === true;
6230
+ if (!requiresLogin) return { ok: true };
6231
+ const pluginName = site.name || "plugin";
6232
+ if (typeof site.isLoggedIn === "function") {
6233
+ try {
6234
+ const loggedIn = await site.isLoggedIn(ctx);
6235
+ if (loggedIn) return { ok: true };
6236
+ return buildLoginRequired({
6237
+ plugin: pluginName,
6238
+ command: commandName,
6239
+ reason: "plugin isLoggedIn returned false",
6240
+ sessionName,
6241
+ loginConfig
6242
+ });
6243
+ } catch {
6244
+ }
6245
+ }
6246
+ if (page && loginConfig) {
6247
+ const generic = await detectLoginFromPage(page, loginConfig);
6248
+ if (generic === "logged-in") return { ok: true };
6249
+ if (generic === "logged-out") {
6250
+ return buildLoginRequired({
6251
+ plugin: pluginName,
6252
+ command: commandName,
6253
+ reason: "generic loginConfig detected logged-out page",
6254
+ sessionName,
6255
+ loginConfig
6256
+ });
6257
+ }
6258
+ }
6259
+ return { ok: true };
6260
+ }
6261
+ function buildLoginRequired(options) {
6262
+ const viewerUrl = buildViewerUrl(options.sessionName);
6263
+ const loginUrl = options.loginConfig?.loginUrl;
6264
+ const message = options.loginConfig?.loginPrompt || `Plugin "${options.plugin}" requires login before running "${options.command}".`;
6265
+ const tips = [
6266
+ message,
6267
+ ...viewerUrl ? [`Open viewer to complete login: ${viewerUrl}`] : [],
6268
+ ...loginUrl ? [`Login page: ${loginUrl}`] : [],
6269
+ `After login, retry: xbrowser ${options.plugin} ${options.command} --session ${options.sessionName}`
6270
+ ];
6271
+ return {
6272
+ ok: false,
6273
+ data: {
6274
+ code: "LOGIN_REQUIRED",
6275
+ plugin: options.plugin,
6276
+ command: options.command,
6277
+ reason: options.reason,
6278
+ ...viewerUrl ? { viewerUrl } : {},
6279
+ ...loginUrl ? { loginUrl } : {}
6280
+ },
6281
+ message,
6282
+ tips
6283
+ };
6284
+ }
6285
+ async function detectLoginFromPage(page, config) {
6286
+ const url = page.url();
6287
+ if (config.loginUrls?.some((part) => url.includes(part))) return "logged-out";
6288
+ const result = await page.evaluate((cfg) => {
6289
+ const visible = (selector) => {
6290
+ try {
6291
+ const el = document.querySelector(selector);
6292
+ if (!el) return false;
6293
+ const rect = el.getBoundingClientRect();
6294
+ const style = window.getComputedStyle(el);
6295
+ return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
6296
+ } catch {
6297
+ return false;
6298
+ }
6299
+ };
6300
+ if (cfg.loggedInSelectors?.some(visible)) return "logged-in";
6301
+ if (cfg.loginSelectors?.some(visible)) return "logged-out";
6302
+ const bodyText = document.body?.innerText || "";
6303
+ const keywords = cfg.loginKeywords || [];
6304
+ if (keywords.length > 0 && keywords.every((keyword) => bodyText.includes(keyword))) {
6305
+ return "logged-out";
6306
+ }
6307
+ return "unknown";
6308
+ }, {
6309
+ loginSelectors: config.loginSelectors || [],
6310
+ loginKeywords: config.loginKeywords || [],
6311
+ loggedInSelectors: config.loggedInSelectors || []
6312
+ });
6313
+ return result;
6314
+ }
6315
+
5813
6316
  // src/tips/dom-watcher.ts
5814
6317
  var DOM_WATCHER_SCRIPT = `
5815
6318
  (function() {
@@ -6189,7 +6692,7 @@ var TipsManager = class {
6189
6692
  }
6190
6693
  }
6191
6694
  debounce() {
6192
- return new Promise((resolve16) => setTimeout(resolve16, DEBOUNCE_MS));
6695
+ return new Promise((resolve10) => setTimeout(resolve10, DEBOUNCE_MS));
6193
6696
  }
6194
6697
  formatTips(tips) {
6195
6698
  return tips.map((tip) => {
@@ -6217,33 +6720,105 @@ function getTipsManager() {
6217
6720
  }
6218
6721
 
6219
6722
  // src/hooks/loader.ts
6220
- var builtinHooks = {
6221
- screenshot: () => import("./screenshot-MB6R7RSS.js").then((m) => m.screenshotHook)
6723
+ var FAIL_KEYWORDS = [
6724
+ "\u767B\u5F55",
6725
+ "login",
6726
+ "Login",
6727
+ "\u672A\u767B\u5F55",
6728
+ "not logged in",
6729
+ "cdp",
6730
+ "CDP",
6731
+ "\u9A8C\u8BC1\u7801",
6732
+ "\u9A8C\u8BC1",
6733
+ "captcha",
6734
+ "\u9700\u8981\u767B\u5F55",
6735
+ "requires login",
6736
+ "blocked",
6737
+ "403",
6738
+ "404"
6739
+ ];
6740
+ var HOOK_REGISTRY = {
6741
+ viewer: {
6742
+ name: "viewer",
6743
+ onAfterCommand: async (ctx) => {
6744
+ const result = ctx.result;
6745
+ if (!result || result.success !== false) return void 0;
6746
+ const msg = [
6747
+ result.message,
6748
+ ...result.tips || []
6749
+ ].filter(Boolean).join(" ").toLowerCase();
6750
+ if (!FAIL_KEYWORDS.some((k) => msg.includes(k))) return void 0;
6751
+ const viewerUrl = buildViewerUrl();
6752
+ if (!viewerUrl) return void 0;
6753
+ const tips = result.tips || [];
6754
+ if (!tips.some((t) => t.includes("viewer") || t.includes("Viewer"))) {
6755
+ tips.push(`Open viewer: ${viewerUrl}`);
6756
+ }
6757
+ result.tips = tips;
6758
+ result.viewerUrl = viewerUrl;
6759
+ return void 0;
6760
+ }
6761
+ },
6762
+ screenshot: {
6763
+ name: "screenshot",
6764
+ onAfterCommand: async (ctx) => {
6765
+ try {
6766
+ const buf = await ctx.page.screenshot({ type: "jpeg", quality: 40 }).catch(() => null);
6767
+ if (!buf) return;
6768
+ return { screenshot: { url: `data:image/jpeg;base64,${buf.toString("base64").slice(0, 50)}...` } };
6769
+ } catch {
6770
+ return;
6771
+ }
6772
+ }
6773
+ },
6774
+ recorder: {
6775
+ name: "recorder",
6776
+ onAfterCommand: async (ctx) => {
6777
+ const ctxAny = ctx;
6778
+ const logs = ctxAny.__commandLogs || [];
6779
+ logs.push({
6780
+ timestamp: Date.now(),
6781
+ command: ctx.command,
6782
+ params: JSON.parse(JSON.stringify(ctx.params)),
6783
+ duration: ctx.duration
6784
+ });
6785
+ ctxAny.__commandLogs = logs;
6786
+ return void 0;
6787
+ }
6788
+ }
6222
6789
  };
6790
+ var customHooks = {};
6223
6791
  async function loadHooks() {
6224
- const names = process.env.XBROWSER_HOOKS;
6225
- if (!names) return [];
6792
+ const env = process.env.XBROWSER_HOOKS;
6793
+ if (!env) return [];
6794
+ const names = env.split(",").map((n) => n.trim()).filter(Boolean);
6795
+ if (names.length === 0) return [];
6226
6796
  const hooks = [];
6227
- for (const name of names.split(",")) {
6228
- const trimmed = name.trim();
6229
- const factory = builtinHooks[trimmed];
6230
- if (factory) {
6231
- hooks.push(await factory());
6797
+ for (const name of names) {
6798
+ const hook = HOOK_REGISTRY[name];
6799
+ if (hook) {
6800
+ hooks.push(hook);
6801
+ continue;
6802
+ }
6803
+ const customFactory = customHooks[name];
6804
+ if (customFactory) {
6805
+ const customHook = await customFactory();
6806
+ if (customHook) hooks.push(customHook);
6232
6807
  }
6233
6808
  }
6234
6809
  return hooks;
6235
6810
  }
6236
6811
 
6237
6812
  // src/executor.ts
6238
- import { homedir as homedir3 } from "os";
6239
- import { join as join3 } from "path";
6813
+ import { homedir as homedir5 } from "os";
6814
+ import { join as join5 } from "path";
6240
6815
  var NAVIGATION_COMMANDS = /* @__PURE__ */ new Set(["goto", "back", "forward", "refresh"]);
6241
6816
  var snapshotHintShown = /* @__PURE__ */ new WeakSet();
6242
- var STORAGE_DIR = join3(homedir3(), ".xbrowser", "storage");
6817
+ var CONFIG_DIR = join5(homedir5(), ".xbrowser");
6243
6818
  var storageCache = /* @__PURE__ */ new Map();
6244
6819
  function getPluginStorage(pluginName) {
6245
6820
  if (!storageCache.has(pluginName)) {
6246
- storageCache.set(pluginName, new PluginStorage(pluginName, STORAGE_DIR));
6821
+ storageCache.set(pluginName, new CompositeStorage(pluginName, CONFIG_DIR, "xbrowser"));
6247
6822
  }
6248
6823
  return storageCache.get(pluginName);
6249
6824
  }
@@ -6251,7 +6826,7 @@ var archiveInitialized = false;
6251
6826
  function ensureArchiveInit() {
6252
6827
  if (!archiveInitialized) {
6253
6828
  try {
6254
- configureArchiveStore({ archiveDir: join3(homedir3(), ".xbrowser", "archives") });
6829
+ configureArchiveStore({ archiveDir: join5(homedir5(), ".xbrowser", "archives") });
6255
6830
  } catch {
6256
6831
  }
6257
6832
  archiveInitialized = true;
@@ -6277,6 +6852,10 @@ async function guardCheck(commandName) {
6277
6852
  function errorResult(message) {
6278
6853
  return { ...fail7(message), duration: 0 };
6279
6854
  }
6855
+ function tipsToMessages(tips) {
6856
+ if (!tips || tips.length === 0) return [];
6857
+ return tips.map((t) => typeof t === "string" ? t : t.message);
6858
+ }
6280
6859
  var wsServer = null;
6281
6860
  function streamCommandEvent(sessionId, message) {
6282
6861
  if (!wsServer || !wsServer.getRunning()) return;
@@ -6304,7 +6883,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6304
6883
  }
6305
6884
  let targetPageOverride = null;
6306
6885
  if (_target && extraOpts?.cdpEndpoint) {
6307
- const { findTargetPage } = await import("./browser-GWBH6OJK.js");
6886
+ const { findTargetPage } = await import("./browser-R56O3CW6.js");
6308
6887
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
6309
6888
  if (!targetPageOverride) {
6310
6889
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -6321,7 +6900,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6321
6900
  params = result.data;
6322
6901
  }
6323
6902
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
6324
- const { forwardExec } = await import("./daemon-client-XWSSQBEA.js");
6903
+ const { forwardExec } = await import("./daemon-client-UZZEHHIV.js");
6325
6904
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
6326
6905
  if (result) return result;
6327
6906
  }
@@ -6329,32 +6908,35 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6329
6908
  const existing = await findOrRestoreSession(sessionName, extraOpts?.cdpEndpoint);
6330
6909
  if (existing) {
6331
6910
  session = existing;
6332
- if (targetPageOverride && session.page) {
6911
+ if (session.page) {
6912
+ try {
6913
+ await Promise.race([
6914
+ session.page.evaluate(() => true),
6915
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
6916
+ ]);
6917
+ } catch {
6918
+ await closeSessionByName(session.name);
6919
+ session = void 0;
6920
+ }
6921
+ }
6922
+ if (session && targetPageOverride && session.page) {
6333
6923
  const currentUrl = session.page.url();
6334
6924
  if (currentUrl !== targetPageOverride.url) {
6335
6925
  await session.page.goto(targetPageOverride.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
6336
6926
  });
6337
6927
  }
6338
6928
  }
6339
- } else if ((command.scope === "page" || command.scope === "project") && params.url) {
6929
+ } else if (command.scope !== "project") {
6340
6930
  session = await createSession(sessionName, params.url, {
6341
6931
  cdpEndpoint: extraOpts?.cdpEndpoint
6342
6932
  });
6343
- } else if (command.scope === "browser") {
6344
- session = await createSession(sessionName, void 0, {
6345
- cdpEndpoint: extraOpts?.cdpEndpoint
6346
- });
6347
- } else if (command.scope !== "project") {
6348
- return errorResult(
6349
- `Session '${sessionName}' not found. Run "xbrowser session open <url>" first.`
6350
- );
6351
6933
  }
6352
6934
  const ctx = {
6353
6935
  page: session?.page,
6354
6936
  browser: session?.context.browser(),
6355
6937
  browserContext: session?.context,
6356
6938
  sessionId: session?.id,
6357
- cdpEndpoint: extraOpts?.cdpEndpoint || session?.cdpEndpoint,
6939
+ cdpEndpoint: session?.cdpEndpoint || extraOpts?.cdpEndpoint,
6358
6940
  args: [],
6359
6941
  options: {},
6360
6942
  cwd: process.cwd(),
@@ -6370,7 +6952,8 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6370
6952
  },
6371
6953
  config: {},
6372
6954
  site: {},
6373
- cliName: "xbrowser"
6955
+ cliName: "xbrowser",
6956
+ tips: new TipCollector()
6374
6957
  };
6375
6958
  const start = Date.now();
6376
6959
  if (session) {
@@ -6389,9 +6972,9 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6389
6972
  let refTips = [];
6390
6973
  if (session?.page && command.selectorParams && command.selectorParams.length > 0) {
6391
6974
  const cache = /* @__PURE__ */ new Map();
6392
- const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache);
6975
+ const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache, session.id);
6393
6976
  if (resolved.tips.length > 0) {
6394
- refTips = resolved.tips;
6977
+ refTips = normalizeTips6(resolved.tips);
6395
6978
  params = resolved.params;
6396
6979
  }
6397
6980
  }
@@ -6447,13 +7030,19 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6447
7030
  snapshotHintShown.add(session);
6448
7031
  snapshotHint = "\u{1F4A1} \u4F7F\u7528 snapshot \u547D\u4EE4\u83B7\u53D6\u9875\u9762\u5FEB\u7167\u548C ref \u7F16\u53F7\uFF0C\u7136\u540E\u7528 ref \u5FEB\u901F\u5B9A\u4F4D\u5143\u7D20\uFF08\u5982 click --selector e1\uFF09";
6449
7032
  }
6450
- const merged = [...raw.tips || [], ...smartTips || [], ...snapshotHint ? [snapshotHint] : [], ...refTips];
7033
+ const merged = [
7034
+ ...raw.tips || [],
7035
+ ...normalizeTips6(smartTips),
7036
+ ...snapshotHint ? normalizeTips6([snapshotHint]) : [],
7037
+ ...refTips
7038
+ ];
6451
7039
  const isSuccess = raw.success !== false;
7040
+ const mergedOrRaw = merged.length > 0 ? merged : raw.tips || [];
6452
7041
  recordArchive(session?.id, sessionName, {
6453
7042
  step: 0,
6454
7043
  command: commandName,
6455
7044
  params,
6456
- result: { success: isSuccess, data: raw.data, message: raw.message, tips: merged.length > 0 ? merged : raw.tips || [] },
7045
+ result: { success: isSuccess, data: raw.data, message: raw.message, tips: tipsToMessages(mergedOrRaw) },
6457
7046
  toolCalls: [],
6458
7047
  duration,
6459
7048
  timestamp: start
@@ -6461,18 +7050,19 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6461
7050
  if (isSuccess) {
6462
7051
  return { ...ok25(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
6463
7052
  }
6464
- return { success: false, data: raw.data, message: raw.message, tips: merged.length > 0 ? merged : raw.tips || [], duration, ...hookOutputs ? { hookOutputs } : {} };
7053
+ return { success: false, data: raw.data, message: raw.message, tips: mergedOrRaw, duration, ...hookOutputs ? { hookOutputs } : {} };
6465
7054
  }
7055
+ const smartTipNormalized = normalizeTips6(smartTips);
6466
7056
  recordArchive(session?.id, sessionName, {
6467
7057
  step: 0,
6468
7058
  command: commandName,
6469
7059
  params,
6470
- result: { success: true, data: raw, tips: smartTips || [] },
7060
+ result: { success: true, data: raw, tips: tipsToMessages(smartTipNormalized) },
6471
7061
  toolCalls: [],
6472
7062
  duration,
6473
7063
  timestamp: start
6474
7064
  });
6475
- return { ...ok25(raw, smartTips), duration, ...hookOutputs ? { hookOutputs } : {} };
7065
+ return { ...ok25(raw, smartTipNormalized), duration, ...hookOutputs ? { hookOutputs } : {} };
6476
7066
  } catch (err) {
6477
7067
  const end = Date.now();
6478
7068
  const duration = end - start;
@@ -6522,8 +7112,8 @@ async function executeChain(input, options) {
6522
7112
  });
6523
7113
  }
6524
7114
  try {
6525
- for (const pipeline2 of pipelines) {
6526
- const { type, pipeline: commands } = pipeline2;
7115
+ for (const pipeline of pipelines) {
7116
+ const { type, pipeline: commands } = pipeline;
6527
7117
  for (const cmdStr of commands) {
6528
7118
  const parts = splitCommand(cmdStr);
6529
7119
  if (parts.length === 0) continue;
@@ -6588,10 +7178,51 @@ async function executeChain(input, options) {
6588
7178
  },
6589
7179
  config: {},
6590
7180
  site,
6591
- cliName: "xbrowser"
7181
+ cliName: "xbrowser",
7182
+ tips: new TipCollector()
6592
7183
  };
6593
7184
  const start2 = Date.now();
6594
7185
  try {
7186
+ const loginGuard = await checkPluginLoginRequired({
7187
+ site,
7188
+ command: cmdEntry,
7189
+ commandName: subCommand,
7190
+ ctx: pluginCtx,
7191
+ page: session?.page,
7192
+ sessionName
7193
+ });
7194
+ if (!loginGuard.ok) {
7195
+ const duration3 = Date.now() - start2;
7196
+ const data2 = loginGuard.data ?? null;
7197
+ recordArchive(session.id, sessionName, {
7198
+ step: results.length,
7199
+ command: `${cmdName} ${subCommand}`,
7200
+ params: pluginParams,
7201
+ result: { success: false, data: data2, message: loginGuard.message, tips: loginGuard.tips || [] },
7202
+ toolCalls: [],
7203
+ duration: duration3,
7204
+ timestamp: start2
7205
+ });
7206
+ results.push({
7207
+ command: `${cmdName} ${subCommand}`,
7208
+ raw: cmdStr,
7209
+ success: false,
7210
+ data: data2,
7211
+ message: loginGuard.message,
7212
+ tips: normalizeTips6(loginGuard.tips),
7213
+ duration: duration3
7214
+ });
7215
+ if (type === "and") {
7216
+ return {
7217
+ success: false,
7218
+ steps: results,
7219
+ totalDuration: Date.now() - totalStart,
7220
+ stoppedAt: results.length,
7221
+ stoppedReason: `Command '${cmdName} ${subCommand}' failed (&& chain): ${loginGuard.message}`
7222
+ };
7223
+ }
7224
+ continue;
7225
+ }
6595
7226
  const hooks = await loadHooks();
6596
7227
  if (hooks.length > 0) {
6597
7228
  await Promise.all(hooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${cmdName} ${subCommand}`, params: pluginParams })));
@@ -6612,7 +7243,7 @@ async function executeChain(input, options) {
6612
7243
  step: results.length,
6613
7244
  command: `${cmdName} ${subCommand}`,
6614
7245
  params: pluginParams,
6615
- result: { success: true, data, tips: raw?.tips || [] },
7246
+ result: { success: true, data, tips: tipsToMessages(raw?.tips) },
6616
7247
  toolCalls: [],
6617
7248
  duration: duration2,
6618
7249
  timestamp: start2
@@ -6663,7 +7294,7 @@ async function executeChain(input, options) {
6663
7294
  }
6664
7295
  continue;
6665
7296
  }
6666
- const { params } = parseCommandArgs(cmdName, cmdArgs);
7297
+ const { params } = parseCommandArgs(cmdName, cmdArgs, unquote2);
6667
7298
  if (cmdName === "goto" && params.url) {
6668
7299
  const existing2 = await findOrRestoreSession(sessionName, options?.cdpEndpoint);
6669
7300
  if (!existing2) {
@@ -6724,15 +7355,6 @@ function isChainInput(input) {
6724
7355
  }
6725
7356
 
6726
7357
  // src/session/session-client.ts
6727
- function sessionToInfo(s) {
6728
- return { id: s.id, name: s.name, url: s.page.url(), createdAt: s.createdAt, cdpEndpoint: s.cdpEndpoint };
6729
- }
6730
- async function openSession(name, url, options) {
6731
- const session = await createSession(name, url, { cdpEndpoint: options?.cdpEndpoint });
6732
- const info = sessionToInfo(session);
6733
- saveSessionDiskMeta(name, info);
6734
- return info;
6735
- }
6736
7358
  async function closeSession(name) {
6737
7359
  await closeSessionByName(name);
6738
7360
  }
@@ -6746,53 +7368,22 @@ function handleSessionHelp() {
6746
7368
  "Usage: xbrowser session <command> [options]",
6747
7369
  "",
6748
7370
  "Commands:",
6749
- " open <url> [--name <name>] Open browser and create session",
6750
7371
  " close [--name <name>] Close session",
6751
7372
  " list, ls List active sessions",
6752
7373
  " kill [--name <name>] Kill session forcefully",
7374
+ " kill-all Kill all sessions and daemon",
6753
7375
  "",
6754
7376
  "Options:",
6755
7377
  ' --name <name> Session name (default: "default")',
6756
7378
  "",
7379
+ "Note: Sessions are auto-created via --session global option.",
7380
+ "",
6757
7381
  "Examples:",
6758
- " xbrowser session open https://example.com",
6759
- " xbrowser session open https://example.com --name mypage",
7382
+ " xbrowser goto https://example.com --session mypage",
6760
7383
  " xbrowser session close --name mypage",
6761
7384
  " xbrowser session list"
6762
7385
  ].join("\n");
6763
7386
  }
6764
- var sessionOpenBuiltin = {
6765
- name: "session open",
6766
- description: "Open browser and create session",
6767
- help: {
6768
- usage: "xbrowser session open <url> [--name <name>]",
6769
- description: "Open URL and create a browser session",
6770
- options: [{ name: "--name <name>", description: 'Session name (default: "default")' }],
6771
- examples: [
6772
- { cmd: "xbrowser session open https://example.com", description: "Open example.com" },
6773
- {
6774
- cmd: "xbrowser session open https://example.com --name test",
6775
- description: "Open with custom name"
6776
- }
6777
- ]
6778
- },
6779
- execute: async (args, options) => {
6780
- const [url] = args;
6781
- const name = options["name"] || "default";
6782
- if (!url) {
6783
- console.log("Usage: xbrowser session open <url> [--name <name>]");
6784
- process.exit(1);
6785
- }
6786
- try {
6787
- const info = await openSession(name, url);
6788
- console.log(`Session "${info.name}" opened: ${info.url}`);
6789
- console.log(`ID: ${info.id}`);
6790
- } catch (e) {
6791
- console.error("Error:", e instanceof Error ? e.message : String(e));
6792
- process.exit(1);
6793
- }
6794
- }
6795
- };
6796
7387
  var sessionCloseBuiltin = {
6797
7388
  name: "session close",
6798
7389
  description: "Close browser session",
@@ -6837,13 +7428,13 @@ var sessionListBuiltin = {
6837
7428
  },
6838
7429
  execute: async () => {
6839
7430
  try {
6840
- const sessions = await listSessions();
6841
- if (sessions.length === 0) {
7431
+ const sessions2 = await listSessions();
7432
+ if (sessions2.length === 0) {
6842
7433
  console.log("No active sessions");
6843
7434
  return;
6844
7435
  }
6845
7436
  console.log("Active sessions:");
6846
- for (const s of sessions) {
7437
+ for (const s of sessions2) {
6847
7438
  console.log(` ${s.name} (${s.id})`);
6848
7439
  }
6849
7440
  } catch (e) {
@@ -6874,22 +7465,17 @@ var sessionKillBuiltin = {
6874
7465
  };
6875
7466
 
6876
7467
  // src/config.ts
6877
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
6878
- import { join as join4 } from "path";
6879
- import { homedir as homedir4, tmpdir } from "os";
6880
- function getConfigFile() {
6881
- return join4(homedir4() || tmpdir(), ".xbrowser", "config.json");
7468
+ import { homedir as homedir6, tmpdir } from "os";
7469
+ import { join as join6 } from "path";
7470
+ import { loadConfig as coreLoadConfig, saveConfig as coreSaveConfig } from "@dyyz1993/xcli-core";
7471
+ function getConfigSource() {
7472
+ return { configDir: join6(homedir6() || tmpdir(), ".xbrowser") };
6882
7473
  }
6883
7474
  function loadConfig() {
6884
- const configFile = getConfigFile();
6885
- if (!existsSync6(configFile)) return {};
6886
- return readJsonFile(configFile, {});
7475
+ return coreLoadConfig(getConfigSource());
6887
7476
  }
6888
7477
  function saveConfig(config) {
6889
- const dir = join4(homedir4() || tmpdir(), ".xbrowser");
6890
- const configFile = getConfigFile();
6891
- if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
6892
- writeFileSync5(configFile, JSON.stringify(config, null, 2), "utf-8");
7478
+ coreSaveConfig(getConfigSource(), config);
6893
7479
  }
6894
7480
  function getConfigValue(key) {
6895
7481
  return loadConfig()[key];
@@ -6997,38 +7583,69 @@ var configBuiltin = {
6997
7583
 
6998
7584
  // src/plugin/installer.ts
6999
7585
  import {
7000
- existsSync as existsSync13,
7001
- readdirSync as readdirSync3,
7586
+ existsSync as existsSync10,
7587
+ readdirSync as readdirSync2,
7002
7588
  mkdirSync as mkdirSync8,
7003
- rmSync as rmSync7
7589
+ rmSync as rmSync6
7004
7590
  } from "fs";
7005
- import { resolve as resolve15, basename as basename2 } from "path";
7006
- import { homedir as homedir5 } from "os";
7591
+ import { resolve as resolve8, basename as basename2 } from "path";
7592
+ import { homedir as homedir7 } from "os";
7007
7593
 
7008
7594
  // src/plugin/install-sources/local.ts
7009
- import { existsSync as existsSync8, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
7010
- import { resolve as resolve10 } from "path";
7595
+ import { existsSync as existsSync5, cpSync, rmSync } from "fs";
7596
+ import { resolve as resolve3 } from "path";
7597
+ import { verifyPlugin, safeCleanup } from "@dyyz1993/xcli-core";
7598
+ async function installFromLocal(source, name, targetDir) {
7599
+ const srcPath = resolve3(source);
7600
+ if (!existsSync5(srcPath)) {
7601
+ throw new Error(`Local path does not exist: ${srcPath}`);
7602
+ }
7603
+ const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
7604
+ let warnings = [];
7605
+ try {
7606
+ cpSync(srcPath, tmpTarget, { recursive: true });
7607
+ const verify = verifyPlugin(tmpTarget, { metadataField: "xbrowser" });
7608
+ warnings = verify.warnings ?? [];
7609
+ if (!verify.valid) {
7610
+ safeCleanup(tmpTarget);
7611
+ throw new Error(`Invalid plugin: ${verify.error}`);
7612
+ }
7613
+ if (existsSync5(targetDir)) {
7614
+ rmSync(targetDir, { recursive: true, force: true });
7615
+ }
7616
+ cpSync(tmpTarget, targetDir, { recursive: true, force: true });
7617
+ safeCleanup(tmpTarget);
7618
+ } catch (err) {
7619
+ safeCleanup(tmpTarget);
7620
+ throw err;
7621
+ }
7622
+ return {
7623
+ id: name,
7624
+ name,
7625
+ path: targetDir,
7626
+ source: "local",
7627
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
7628
+ warnings
7629
+ };
7630
+ }
7011
7631
 
7012
- // src/plugin/install-utils.ts
7632
+ // src/plugin/install-sources/npm.ts
7633
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync6, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
7634
+ import { resolve as resolve4, join as join7 } from "path";
7635
+ import { tmpdir as tmpdir2 } from "os";
7013
7636
  import {
7014
- existsSync as existsSync7,
7015
- readdirSync as readdirSync2,
7016
- cpSync,
7017
- rmSync,
7018
- mkdirSync as mkdirSync4,
7019
- readFileSync as readFileSync10,
7020
- createWriteStream
7021
- } from "fs";
7022
- import { resolve as resolve9 } from "path";
7023
- import { execSync as execSync7 } from "child_process";
7024
- import { pipeline } from "stream/promises";
7025
- import { Readable } from "stream";
7637
+ downloadToFile,
7638
+ extractTarGz,
7639
+ flattenPackageRoot,
7640
+ verifyPlugin as verifyPlugin2,
7641
+ safeCleanup as safeCleanup2
7642
+ } from "@dyyz1993/xcli-core";
7026
7643
 
7027
7644
  // src/utils/proxy-fetch.ts
7028
- var patched = false;
7645
+ var patched2 = false;
7029
7646
  async function ensureProxyFetch() {
7030
- if (patched) return;
7031
- patched = true;
7647
+ if (patched2) return;
7648
+ patched2 = true;
7032
7649
  if (process.env.https_proxy && !process.env.HTTPS_PROXY) {
7033
7650
  process.env.HTTPS_PROXY = process.env.https_proxy;
7034
7651
  }
@@ -7068,113 +7685,7 @@ async function ensureProxyFetch() {
7068
7685
  }
7069
7686
  }
7070
7687
 
7071
- // src/plugin/install-utils.ts
7072
- async function downloadToFile(url, destPath) {
7073
- await ensureProxyFetch();
7074
- if (url.startsWith("file://")) {
7075
- const filePath = decodeURIComponent(new URL(url).pathname);
7076
- cpSync(filePath, destPath, { force: true });
7077
- return;
7078
- }
7079
- const res = await fetch(url);
7080
- if (!res.ok) {
7081
- throw new Error(`Download failed: HTTP ${res.status} from ${url}`);
7082
- }
7083
- if (!res.body) {
7084
- throw new Error(`No response body from ${url}`);
7085
- }
7086
- const nodeStream = Readable.fromWeb(res.body);
7087
- await pipeline(nodeStream, createWriteStream(destPath));
7088
- }
7089
- function extractTarGz(tarballPath, targetDir) {
7090
- mkdirSync4(targetDir, { recursive: true });
7091
- execSync7(`tar -xzf "${tarballPath}" -C "${targetDir}"`, { stdio: "pipe" });
7092
- }
7093
- function flattenPackageRoot(targetDir) {
7094
- const entries = readdirSync2(targetDir, { withFileTypes: true });
7095
- const dirs = entries.filter((e) => e.isDirectory());
7096
- const files = entries.filter((e) => !e.isDirectory());
7097
- if (dirs.length === 1 && files.length === 0) {
7098
- const pkgDir = resolve9(targetDir, dirs[0].name);
7099
- const items = readdirSync2(pkgDir);
7100
- for (const item of items) {
7101
- const src = resolve9(pkgDir, item);
7102
- const dst = resolve9(targetDir, item);
7103
- cpSync(src, dst, { recursive: true, force: true });
7104
- }
7105
- rmSync(pkgDir, { recursive: true, force: true });
7106
- }
7107
- }
7108
- async function verifyPlugin(dir) {
7109
- const warnings = [];
7110
- const indexPath = resolve9(dir, "index.ts");
7111
- if (!existsSync7(indexPath)) {
7112
- const indexJs = resolve9(dir, "index.js");
7113
- if (!existsSync7(indexJs)) {
7114
- return { valid: false, error: "No index.ts or index.js entry point found", warnings };
7115
- }
7116
- }
7117
- const pkgPath = resolve9(dir, "package.json");
7118
- if (!existsSync7(pkgPath)) {
7119
- warnings.push("No package.json found");
7120
- } else {
7121
- try {
7122
- const pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
7123
- if (!pkg2.xbrowser) {
7124
- warnings.push("No xbrowser metadata in package.json");
7125
- }
7126
- } catch {
7127
- warnings.push("Invalid package.json");
7128
- }
7129
- }
7130
- return { valid: true, warnings };
7131
- }
7132
- function safeCleanup(dir) {
7133
- try {
7134
- rmSync(dir, { recursive: true, force: true });
7135
- } catch {
7136
- }
7137
- }
7138
-
7139
- // src/plugin/install-sources/local.ts
7140
- async function installFromLocal(source, name, targetDir) {
7141
- const srcPath = resolve10(source);
7142
- if (!existsSync8(srcPath)) {
7143
- throw new Error(`Local path does not exist: ${srcPath}`);
7144
- }
7145
- const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
7146
- let warnings = [];
7147
- try {
7148
- cpSync2(srcPath, tmpTarget, { recursive: true });
7149
- const verify = await verifyPlugin(tmpTarget);
7150
- warnings = verify.warnings ?? [];
7151
- if (!verify.valid) {
7152
- safeCleanup(tmpTarget);
7153
- throw new Error(`Invalid plugin: ${verify.error}`);
7154
- }
7155
- if (existsSync8(targetDir)) {
7156
- rmSync2(targetDir, { recursive: true, force: true });
7157
- }
7158
- cpSync2(tmpTarget, targetDir, { recursive: true, force: true });
7159
- safeCleanup(tmpTarget);
7160
- } catch (err) {
7161
- safeCleanup(tmpTarget);
7162
- throw err;
7163
- }
7164
- return {
7165
- id: name,
7166
- name,
7167
- path: targetDir,
7168
- source: "local",
7169
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
7170
- warnings
7171
- };
7172
- }
7173
-
7174
7688
  // src/plugin/install-sources/npm.ts
7175
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
7176
- import { resolve as resolve11, join as join5 } from "path";
7177
- import { tmpdir as tmpdir2 } from "os";
7178
7689
  async function installFromNpm(packageName, name, targetDir) {
7179
7690
  await ensureProxyFetch();
7180
7691
  const encodedName = encodeURIComponent(packageName);
@@ -7192,34 +7703,34 @@ async function installFromNpm(packageName, name, targetDir) {
7192
7703
  throw new Error(`No tarball URL for ${packageName}@${latestVersion}`);
7193
7704
  }
7194
7705
  const tarballUrl = versionMeta.dist.tarball;
7195
- const tmpDir = join5(tmpdir2(), `xbrowser-npm-${Date.now()}`);
7706
+ const tmpDir = join7(tmpdir2(), `xbrowser-npm-${Date.now()}`);
7196
7707
  mkdirSync5(tmpDir, { recursive: true });
7197
7708
  let warnings = [];
7198
7709
  try {
7199
- const tarballPath = join5(tmpDir, `${name}.tgz`);
7710
+ const tarballPath = join7(tmpDir, `${name}.tgz`);
7200
7711
  await downloadToFile(tarballUrl, tarballPath);
7201
- const extractDir = join5(tmpDir, "extracted");
7712
+ const extractDir = join7(tmpDir, "extracted");
7202
7713
  extractTarGz(tarballPath, extractDir);
7203
7714
  flattenPackageRoot(extractDir);
7204
- const verify = await verifyPlugin(extractDir);
7715
+ const verify = verifyPlugin2(extractDir, { metadataField: "xbrowser" });
7205
7716
  warnings = verify.warnings ?? [];
7206
7717
  if (!verify.valid) {
7207
7718
  throw new Error(`Invalid npm plugin: ${verify.error}`);
7208
7719
  }
7209
- if (existsSync9(targetDir)) {
7210
- rmSync3(targetDir, { recursive: true, force: true });
7720
+ if (existsSync6(targetDir)) {
7721
+ rmSync2(targetDir, { recursive: true, force: true });
7211
7722
  }
7212
- cpSync3(extractDir, targetDir, { recursive: true, force: true });
7213
- const pkgPath = resolve11(targetDir, "package.json");
7214
- if (existsSync9(pkgPath)) {
7215
- const pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7723
+ cpSync2(extractDir, targetDir, { recursive: true, force: true });
7724
+ const pkgPath = resolve4(targetDir, "package.json");
7725
+ if (existsSync6(pkgPath)) {
7726
+ const pkg2 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
7216
7727
  if (!pkg2._npmSource) {
7217
7728
  pkg2._npmSource = { name: packageName, version: latestVersion };
7218
7729
  writeFileSync6(pkgPath, JSON.stringify(pkg2, null, 2));
7219
7730
  }
7220
7731
  }
7221
7732
  } finally {
7222
- safeCleanup(tmpDir);
7733
+ safeCleanup2(tmpDir);
7223
7734
  }
7224
7735
  return {
7225
7736
  id: name,
@@ -7232,35 +7743,36 @@ async function installFromNpm(packageName, name, targetDir) {
7232
7743
  }
7233
7744
 
7234
7745
  // src/plugin/install-sources/git.ts
7235
- import { existsSync as existsSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync7, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
7236
- import { resolve as resolve12, join as join6 } from "path";
7746
+ import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
7747
+ import { resolve as resolve5, join as join8 } from "path";
7237
7748
  import { tmpdir as tmpdir3 } from "os";
7238
- import { execSync as execSync8 } from "child_process";
7749
+ import { execSync as execSync2 } from "child_process";
7750
+ import { verifyPlugin as verifyPlugin3, safeCleanup as safeCleanup3 } from "@dyyz1993/xcli-core";
7239
7751
  async function installFromGit(gitUrl, name, targetDir) {
7240
- const tmpDir = join6(tmpdir3(), `xbrowser-git-${Date.now()}`);
7752
+ const tmpDir = join8(tmpdir3(), `xbrowser-git-${Date.now()}`);
7241
7753
  let warnings = [];
7242
7754
  try {
7243
- execSync8(`git clone --depth 1 "${gitUrl}" "${tmpDir}"`, { stdio: "pipe" });
7244
- const verify = await verifyPlugin(tmpDir);
7755
+ execSync2(`git clone --depth 1 "${gitUrl}" "${tmpDir}"`, { stdio: "pipe" });
7756
+ const verify = verifyPlugin3(tmpDir, { metadataField: "xbrowser" });
7245
7757
  warnings = verify.warnings ?? [];
7246
7758
  if (!verify.valid) {
7247
7759
  throw new Error(`Invalid git plugin: ${verify.error}`);
7248
7760
  }
7249
- if (existsSync10(targetDir)) {
7250
- rmSync4(targetDir, { recursive: true, force: true });
7761
+ if (existsSync7(targetDir)) {
7762
+ rmSync3(targetDir, { recursive: true, force: true });
7251
7763
  }
7252
- cpSync4(tmpDir, targetDir, { recursive: true, force: true });
7253
- rmSync4(resolve12(targetDir, ".git"), { recursive: true, force: true });
7254
- const pkgPath = resolve12(targetDir, "package.json");
7255
- if (existsSync10(pkgPath)) {
7256
- const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7764
+ cpSync3(tmpDir, targetDir, { recursive: true, force: true });
7765
+ rmSync3(resolve5(targetDir, ".git"), { recursive: true, force: true });
7766
+ const pkgPath = resolve5(targetDir, "package.json");
7767
+ if (existsSync7(pkgPath)) {
7768
+ const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
7257
7769
  if (!pkg2._gitSource) {
7258
7770
  pkg2._gitSource = { url: gitUrl };
7259
7771
  writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
7260
7772
  }
7261
7773
  }
7262
7774
  } finally {
7263
- safeCleanup(tmpDir);
7775
+ safeCleanup3(tmpDir);
7264
7776
  }
7265
7777
  return {
7266
7778
  id: name,
@@ -7273,39 +7785,46 @@ async function installFromGit(gitUrl, name, targetDir) {
7273
7785
  }
7274
7786
 
7275
7787
  // src/plugin/install-sources/url.ts
7276
- import { existsSync as existsSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, rmSync as rmSync5, cpSync as cpSync5 } from "fs";
7277
- import { resolve as resolve13, join as join7, basename } from "path";
7788
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync8, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
7789
+ import { resolve as resolve6, join as join9, basename } from "path";
7278
7790
  import { tmpdir as tmpdir4 } from "os";
7791
+ import {
7792
+ downloadToFile as downloadToFile2,
7793
+ extractTarGz as extractTarGz2,
7794
+ flattenPackageRoot as flattenPackageRoot2,
7795
+ verifyPlugin as verifyPlugin4,
7796
+ safeCleanup as safeCleanup4
7797
+ } from "@dyyz1993/xcli-core";
7279
7798
  async function installFromUrl(url, name, targetDir) {
7280
- const tmpDir = join7(tmpdir4(), `xbrowser-url-${Date.now()}`);
7799
+ const tmpDir = join9(tmpdir4(), `xbrowser-url-${Date.now()}`);
7281
7800
  mkdirSync6(tmpDir, { recursive: true });
7282
7801
  let warnings = [];
7283
7802
  try {
7284
7803
  const fileName = basename(new URL(url).pathname) || "plugin.tar.gz";
7285
- const tarballPath = join7(tmpDir, fileName);
7286
- await downloadToFile(url, tarballPath);
7287
- const extractDir = join7(tmpDir, "extracted");
7288
- extractTarGz(tarballPath, extractDir);
7289
- flattenPackageRoot(extractDir);
7290
- const verify = await verifyPlugin(extractDir);
7804
+ const tarballPath = join9(tmpDir, fileName);
7805
+ await downloadToFile2(url, tarballPath);
7806
+ const extractDir = join9(tmpDir, "extracted");
7807
+ extractTarGz2(tarballPath, extractDir);
7808
+ flattenPackageRoot2(extractDir);
7809
+ const verify = verifyPlugin4(extractDir, { metadataField: "xbrowser" });
7291
7810
  warnings = verify.warnings ?? [];
7292
7811
  if (!verify.valid) {
7293
7812
  throw new Error(`Invalid plugin from URL: ${verify.error}`);
7294
7813
  }
7295
- if (existsSync11(targetDir)) {
7296
- rmSync5(targetDir, { recursive: true, force: true });
7814
+ if (existsSync8(targetDir)) {
7815
+ rmSync4(targetDir, { recursive: true, force: true });
7297
7816
  }
7298
- cpSync5(extractDir, targetDir, { recursive: true, force: true });
7299
- const pkgPath = resolve13(targetDir, "package.json");
7300
- if (existsSync11(pkgPath)) {
7301
- const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
7817
+ cpSync4(extractDir, targetDir, { recursive: true, force: true });
7818
+ const pkgPath = resolve6(targetDir, "package.json");
7819
+ if (existsSync8(pkgPath)) {
7820
+ const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
7302
7821
  if (!pkg2._urlSource) {
7303
7822
  pkg2._urlSource = { url };
7304
7823
  writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
7305
7824
  }
7306
7825
  }
7307
7826
  } finally {
7308
- safeCleanup(tmpDir);
7827
+ safeCleanup4(tmpDir);
7309
7828
  }
7310
7829
  return {
7311
7830
  id: name,
@@ -7319,16 +7838,23 @@ async function installFromUrl(url, name, targetDir) {
7319
7838
 
7320
7839
  // src/plugin/install-sources/marketplace.ts
7321
7840
  import {
7322
- existsSync as existsSync12,
7841
+ existsSync as existsSync9,
7323
7842
  mkdirSync as mkdirSync7,
7324
7843
  writeFileSync as writeFileSync9,
7325
- readFileSync as readFileSync14,
7326
- rmSync as rmSync6,
7327
- cpSync as cpSync6
7844
+ readFileSync as readFileSync7,
7845
+ rmSync as rmSync5,
7846
+ cpSync as cpSync5
7328
7847
  } from "fs";
7329
- import { resolve as resolve14, join as join8, dirname as dirname2 } from "path";
7848
+ import { resolve as resolve7, join as join10, dirname as dirname2 } from "path";
7330
7849
  import { tmpdir as tmpdir5 } from "os";
7331
7850
  import { gunzipSync } from "zlib";
7851
+ import {
7852
+ downloadToFile as downloadToFile3,
7853
+ extractTarGz as extractTarGz3,
7854
+ flattenPackageRoot as flattenPackageRoot3,
7855
+ verifyPlugin as verifyPlugin5,
7856
+ safeCleanup as safeCleanup5
7857
+ } from "@dyyz1993/xcli-core";
7332
7858
  async function installFromMarketplace(pluginsDir, slug, options) {
7333
7859
  await ensureProxyFetch();
7334
7860
  const baseUrl = getMarketplaceUrl();
@@ -7343,24 +7869,24 @@ async function installFromMarketplace(pluginsDir, slug, options) {
7343
7869
  }
7344
7870
  const plugin = detailData.data;
7345
7871
  const name = options?.name || String(plugin.slug || slug);
7346
- const targetDir = resolve14(pluginsDir, name);
7347
- if (existsSync12(targetDir) && !options?.force) {
7872
+ const targetDir = resolve7(pluginsDir, name);
7873
+ if (existsSync9(targetDir) && !options?.force) {
7348
7874
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
7349
7875
  }
7350
7876
  mkdirSync7(targetDir, { recursive: true });
7351
- const tmpDir = join8(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
7877
+ const tmpDir = join10(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
7352
7878
  mkdirSync7(tmpDir, { recursive: true });
7353
7879
  const realSlug = String(plugin.slug || slug);
7354
7880
  try {
7355
7881
  await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
7356
7882
  } finally {
7357
- safeCleanup(tmpDir);
7883
+ safeCleanup5(tmpDir);
7358
7884
  }
7359
7885
  writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir);
7360
7886
  ensureIndexFile(plugin, name, targetDir);
7361
- const verify = await verifyPlugin(targetDir);
7887
+ const verify = verifyPlugin5(targetDir, { metadataField: "xbrowser" });
7362
7888
  if (!verify.valid) {
7363
- safeCleanup(targetDir);
7889
+ safeCleanup5(targetDir);
7364
7890
  throw new Error(`Invalid marketplace plugin: ${verify.error}`);
7365
7891
  }
7366
7892
  const trackUrl = `${baseUrl}/api/plugins/${realSlug}/install`;
@@ -7379,12 +7905,12 @@ function isManifestArray(data) {
7379
7905
  return Array.isArray(data) && data.length > 0 && typeof data[0].path === "string" && typeof data[0].content === "string";
7380
7906
  }
7381
7907
  function extractManifestToDir(manifest, targetDir) {
7382
- if (existsSync12(targetDir)) {
7383
- rmSync6(targetDir, { recursive: true, force: true });
7908
+ if (existsSync9(targetDir)) {
7909
+ rmSync5(targetDir, { recursive: true, force: true });
7384
7910
  }
7385
7911
  mkdirSync7(targetDir, { recursive: true });
7386
7912
  for (const file of manifest) {
7387
- const filePath = resolve14(targetDir, file.path);
7913
+ const filePath = resolve7(targetDir, file.path);
7388
7914
  mkdirSync7(dirname2(filePath), { recursive: true });
7389
7915
  writeFileSync9(filePath, Buffer.from(file.content, "base64"));
7390
7916
  }
@@ -7411,21 +7937,21 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7411
7937
  }
7412
7938
  if (tarballRes.status === 302 || tarballRes.headers.get("location")) {
7413
7939
  const redirectUrl = tarballRes.headers.get("location");
7414
- const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
7415
- await downloadToFile(redirectUrl, tarballPath);
7416
- const buffer = readFileSync14(tarballPath);
7940
+ const tarballPath = join10(tmpDir, `${slug}.tar.gz`);
7941
+ await downloadToFile3(redirectUrl, tarballPath);
7942
+ const buffer = readFileSync7(tarballPath);
7417
7943
  const manifest = tryParseAsGzippedManifest(buffer);
7418
7944
  if (manifest) {
7419
7945
  extractManifestToDir(manifest, targetDir);
7420
7946
  return;
7421
7947
  }
7422
- const extractDir = join8(tmpDir, "extracted");
7423
- extractTarGz(tarballPath, extractDir);
7424
- flattenPackageRoot(extractDir);
7425
- if (existsSync12(targetDir)) {
7426
- rmSync6(targetDir, { recursive: true, force: true });
7948
+ const extractDir = join10(tmpDir, "extracted");
7949
+ extractTarGz3(tarballPath, extractDir);
7950
+ flattenPackageRoot3(extractDir);
7951
+ if (existsSync9(targetDir)) {
7952
+ rmSync5(targetDir, { recursive: true, force: true });
7427
7953
  }
7428
- cpSync6(extractDir, targetDir, { recursive: true, force: true });
7954
+ cpSync5(extractDir, targetDir, { recursive: true, force: true });
7429
7955
  } else {
7430
7956
  const buffer = Buffer.from(await tarballRes.arrayBuffer());
7431
7957
  const manifest = tryParseAsGzippedManifest(buffer);
@@ -7433,16 +7959,16 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7433
7959
  extractManifestToDir(manifest, targetDir);
7434
7960
  return;
7435
7961
  }
7436
- const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
7962
+ const tarballPath = join10(tmpDir, `${slug}.tar.gz`);
7437
7963
  writeFileSync9(tarballPath, buffer);
7438
7964
  try {
7439
- const extractDir = join8(tmpDir, "extracted");
7440
- extractTarGz(tarballPath, extractDir);
7441
- flattenPackageRoot(extractDir);
7442
- if (existsSync12(targetDir)) {
7443
- rmSync6(targetDir, { recursive: true, force: true });
7965
+ const extractDir = join10(tmpDir, "extracted");
7966
+ extractTarGz3(tarballPath, extractDir);
7967
+ flattenPackageRoot3(extractDir);
7968
+ if (existsSync9(targetDir)) {
7969
+ rmSync5(targetDir, { recursive: true, force: true });
7444
7970
  }
7445
- cpSync6(extractDir, targetDir, { recursive: true, force: true });
7971
+ cpSync5(extractDir, targetDir, { recursive: true, force: true });
7446
7972
  } catch {
7447
7973
  throw new Error(
7448
7974
  `Downloaded tarball for "${slug}" is neither a gzipped JSON manifest nor a valid tar.gz archive.`
@@ -7474,12 +8000,12 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
7474
8000
  url: baseUrl
7475
8001
  }
7476
8002
  };
7477
- const pkgPath = resolve14(targetDir, "package.json");
7478
- if (!existsSync12(pkgPath)) {
8003
+ const pkgPath = resolve7(targetDir, "package.json");
8004
+ if (!existsSync9(pkgPath)) {
7479
8005
  writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
7480
8006
  } else {
7481
8007
  try {
7482
- const existing = JSON.parse(readFileSync14(pkgPath, "utf-8"));
8008
+ const existing = JSON.parse(readFileSync7(pkgPath, "utf-8"));
7483
8009
  const merged = {
7484
8010
  ...existing,
7485
8011
  xbrowser: { ...existing.xbrowser, ...packageJson.xbrowser },
@@ -7492,7 +8018,7 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
7492
8018
  }
7493
8019
  }
7494
8020
  function ensureIndexFile(plugin, name, targetDir) {
7495
- if (!existsSync12(resolve14(targetDir, "index.ts")) && !existsSync12(resolve14(targetDir, "index.js"))) {
8021
+ if (!existsSync9(resolve7(targetDir, "index.ts")) && !existsSync9(resolve7(targetDir, "index.js"))) {
7496
8022
  const commands = plugin.commands || [];
7497
8023
  const commandHandlers = commands.length > 0 ? commands.map((cmd) => {
7498
8024
  return [
@@ -7508,7 +8034,7 @@ function ensureIndexFile(plugin, name, targetDir) {
7508
8034
  ` });`
7509
8035
  ].join("\n");
7510
8036
  writeFileSync9(
7511
- resolve14(targetDir, "index.ts"),
8037
+ resolve7(targetDir, "index.ts"),
7512
8038
  [
7513
8039
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
7514
8040
  ``,
@@ -7529,7 +8055,7 @@ function ensureIndexFile(plugin, name, targetDir) {
7529
8055
  var PluginInstaller = class {
7530
8056
  pluginsDir;
7531
8057
  constructor(pluginsDir) {
7532
- this.pluginsDir = pluginsDir || resolve15(homedir5(), ".xbrowser/plugins");
8058
+ this.pluginsDir = pluginsDir || resolve8(homedir7(), ".xbrowser/plugins");
7533
8059
  }
7534
8060
  getPluginsDir() {
7535
8061
  return this.pluginsDir;
@@ -7545,8 +8071,8 @@ var PluginInstaller = class {
7545
8071
  async install(source, options) {
7546
8072
  const type = this.detectSourceType(source);
7547
8073
  const name = options?.name || this.deriveName(source, type);
7548
- const targetDir = resolve15(this.pluginsDir, name);
7549
- if (existsSync13(targetDir) && !options?.force) {
8074
+ const targetDir = resolve8(this.pluginsDir, name);
8075
+ if (existsSync10(targetDir) && !options?.force) {
7550
8076
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
7551
8077
  }
7552
8078
  mkdirSync8(targetDir, { recursive: true });
@@ -7606,11 +8132,11 @@ var PluginInstaller = class {
7606
8132
  * @throws If the plugin is not installed.
7607
8133
  */
7608
8134
  async uninstall(name) {
7609
- const targetDir = resolve15(this.pluginsDir, name);
7610
- if (!existsSync13(targetDir)) {
8135
+ const targetDir = resolve8(this.pluginsDir, name);
8136
+ if (!existsSync10(targetDir)) {
7611
8137
  throw new Error(`Plugin "${name}" not found`);
7612
8138
  }
7613
- rmSync7(targetDir, { recursive: true, force: true });
8139
+ rmSync6(targetDir, { recursive: true, force: true });
7614
8140
  }
7615
8141
  /**
7616
8142
  * List all installed plugins with metadata.
@@ -7618,18 +8144,18 @@ var PluginInstaller = class {
7618
8144
  * @returns Array of installed plugin information.
7619
8145
  */
7620
8146
  async list(_options) {
7621
- if (!existsSync13(this.pluginsDir)) return [];
7622
- const entries = readdirSync3(this.pluginsDir, { withFileTypes: true });
8147
+ if (!existsSync10(this.pluginsDir)) return [];
8148
+ const entries = readdirSync2(this.pluginsDir, { withFileTypes: true });
7623
8149
  const plugins = [];
7624
8150
  for (const entry of entries) {
7625
8151
  if (!entry.isDirectory()) continue;
7626
- const pluginPath = resolve15(this.pluginsDir, entry.name);
7627
- const indexPath = resolve15(pluginPath, "index.ts");
7628
- const indexJsPath = resolve15(pluginPath, "index.js");
7629
- if (!existsSync13(indexPath) && !existsSync13(indexJsPath)) continue;
8152
+ const pluginPath = resolve8(this.pluginsDir, entry.name);
8153
+ const indexPath = resolve8(pluginPath, "index.ts");
8154
+ const indexJsPath = resolve8(pluginPath, "index.js");
8155
+ if (!existsSync10(indexPath) && !existsSync10(indexJsPath)) continue;
7630
8156
  const metadata = PluginMetadataParser.parseFromPackageJson(pluginPath);
7631
8157
  let source = "local";
7632
- const pkg2 = readJsonFile(resolve15(pluginPath, "package.json"), {});
8158
+ const pkg2 = readJsonFile(resolve8(pluginPath, "package.json"), {});
7633
8159
  if (pkg2._marketplace) source = "marketplace";
7634
8160
  else if (pkg2._npmSource) source = "npm";
7635
8161
  else if (pkg2._gitSource) source = "git";
@@ -7653,10 +8179,10 @@ var PluginInstaller = class {
7653
8179
  }
7654
8180
  if (source.startsWith("file://")) {
7655
8181
  const filePath = decodeURIComponent(new URL(source).pathname);
7656
- if (existsSync13(filePath)) return "url";
8182
+ if (existsSync10(filePath)) return "url";
7657
8183
  }
7658
8184
  if (source.endsWith(".git") || source.includes("github.com/")) return "git";
7659
- if (existsSync13(resolve15(source))) return "local";
8185
+ if (existsSync10(resolve8(source))) return "local";
7660
8186
  return "npm";
7661
8187
  }
7662
8188
  deriveName(source, type) {
@@ -7731,6 +8257,7 @@ function handlePluginHelp() {
7731
8257
  " install <slug> --from-marketplace Install from marketplace",
7732
8258
  " uninstall <name> Uninstall a plugin",
7733
8259
  " list [--json] List installed plugins",
8260
+ " schema <name> [command] [--json] Show plugin contract and command forms",
7734
8261
  " reload <name> Reload a plugin",
7735
8262
  "",
7736
8263
  "Examples:",
@@ -7739,6 +8266,7 @@ function handlePluginHelp() {
7739
8266
  " xbrowser plugin install ./my-plugin",
7740
8267
  " xbrowser plugin uninstall my-plugin",
7741
8268
  " xbrowser plugin list",
8269
+ " xbrowser plugin schema my-plugin --json",
7742
8270
  " xbrowser plugin reload my-plugin"
7743
8271
  ].join("\n");
7744
8272
  }
@@ -8488,7 +9016,7 @@ var previewBuiltin = {
8488
9016
  if (options.json) {
8489
9017
  outputResult({ running: false }, "json");
8490
9018
  } else {
8491
- console.log("Daemon is not running. Start with: xbrowser daemon start");
9019
+ console.log("Daemon is not running. It will start automatically when needed.");
8492
9020
  console.log("");
8493
9021
  console.log("Preview is automatically available when the daemon is running.");
8494
9022
  }
@@ -8512,9 +9040,199 @@ var previewBuiltin = {
8512
9040
  }
8513
9041
  };
8514
9042
 
9043
+ // src/builtins/knowledge.ts
9044
+ init_site_knowledge();
9045
+ import { existsSync as existsSync12 } from "fs";
9046
+ var knowledgeBuiltin = {
9047
+ name: "knowledge",
9048
+ description: "View LLM-readable site knowledge base (selectors, forms, APIs)",
9049
+ aliases: ["know"],
9050
+ help: {
9051
+ usage: "xbrowser knowledge <list|show|search|issue|path> [domain] [options]",
9052
+ description: "Manage auto-generated site knowledge from recordings. Knowledge is stored at ~/.xbrowser/knowledge/{domain}.md and is designed for LLM consumption.",
9053
+ options: [
9054
+ { name: "list", description: "List all domains with knowledge bases" },
9055
+ { name: "show <domain>", description: "Show full knowledge for a domain (markdown)" },
9056
+ { name: "search <domain> <query>", description: "Search selectors/APIs by keyword" },
9057
+ { name: "selectors <domain>", description: "List all selectors for a domain" },
9058
+ { name: "api <domain>", description: "List all API endpoints for a domain" },
9059
+ { name: "issue <domain> <text>", description: "Add a known issue to a domain" },
9060
+ { name: "path <domain>", description: "Show file path for a domain's knowledge" }
9061
+ ],
9062
+ examples: [
9063
+ { cmd: "xbrowser knowledge list", description: "List all known sites" },
9064
+ { cmd: "xbrowser knowledge show juejin.cn", description: "Show juejin.cn knowledge" },
9065
+ { cmd: "xbrowser knowledge search juejin.cn publish", description: 'Find selectors related to "publish"' },
9066
+ { cmd: "xbrowser knowledge selectors juejin.cn", description: "List all selectors" },
9067
+ { cmd: "xbrowser knowledge api juejin.cn", description: "List API endpoints" },
9068
+ { cmd: 'xbrowser knowledge issue juejin.cn "Title selector changed"', description: "Report an issue" }
9069
+ ]
9070
+ },
9071
+ execute: async (args, _options, _ctx) => {
9072
+ const [subcommand, ...rest] = args;
9073
+ if (!subcommand || subcommand === "list") {
9074
+ const domains = listSiteKnowledge();
9075
+ if (domains.length === 0) {
9076
+ console.log("No site knowledge bases found.");
9077
+ console.log("Knowledge is auto-generated when you run `xbrowser record stop`.");
9078
+ return;
9079
+ }
9080
+ console.log("Site Knowledge Bases:");
9081
+ console.log("");
9082
+ for (const domain of domains) {
9083
+ const kb = readSiteKnowledge(domain);
9084
+ if (kb) {
9085
+ const pageCount = Object.keys(kb.pages).length;
9086
+ const selCount = Object.values(kb.pages).reduce((sum, p) => sum + p.selectors.length, 0);
9087
+ const apiCount = Object.keys(kb.apiEndpoints).length;
9088
+ console.log(
9089
+ ` ${domain} \u2014 ${kb.recordingCount} recordings, ${pageCount} pages, ${selCount} selectors, ${apiCount} APIs`
9090
+ );
9091
+ }
9092
+ }
9093
+ return;
9094
+ }
9095
+ if (subcommand === "show") {
9096
+ const domain = rest[0];
9097
+ if (!domain) {
9098
+ console.error("Usage: xbrowser knowledge show <domain>");
9099
+ process.exit(1);
9100
+ }
9101
+ const md = readSiteKnowledgeMarkdown(domain);
9102
+ if (!md) {
9103
+ console.error(`No knowledge base found for ${domain}`);
9104
+ console.error("Run `xbrowser knowledge list` to see available domains.");
9105
+ process.exit(1);
9106
+ }
9107
+ console.log(md);
9108
+ return;
9109
+ }
9110
+ if (subcommand === "selectors") {
9111
+ const domain = rest[0];
9112
+ if (!domain) {
9113
+ console.error("Usage: xbrowser knowledge selectors <domain>");
9114
+ process.exit(1);
9115
+ }
9116
+ const kb = readSiteKnowledge(domain);
9117
+ if (!kb) {
9118
+ console.error(`No knowledge base found for ${domain}`);
9119
+ process.exit(1);
9120
+ }
9121
+ console.log(`Selectors for ${domain} (${kb.recordingCount} recordings):`);
9122
+ console.log("");
9123
+ for (const [pagePath, page] of Object.entries(kb.pages)) {
9124
+ if (page.selectors.length === 0) continue;
9125
+ console.log(` ${pagePath}:`);
9126
+ for (const sel of page.selectors) {
9127
+ const status = sel.status === "deprecated" ? " \u26A0\uFE0F" : "";
9128
+ console.log(
9129
+ ` ${sel.selector.padEnd(30)} ${sel.tag.padEnd(8)} ${sel.actionType.padEnd(10)} ${sel.confidence.padEnd(6)} ${sel.timesSeen}x${status}`
9130
+ );
9131
+ if (sel.description) console.log(` \u2192 ${sel.description}`);
9132
+ }
9133
+ console.log("");
9134
+ }
9135
+ return;
9136
+ }
9137
+ if (subcommand === "api") {
9138
+ const domain = rest[0];
9139
+ if (!domain) {
9140
+ console.error("Usage: xbrowser knowledge api <domain>");
9141
+ process.exit(1);
9142
+ }
9143
+ const kb = readSiteKnowledge(domain);
9144
+ if (!kb) {
9145
+ console.error(`No knowledge base found for ${domain}`);
9146
+ process.exit(1);
9147
+ }
9148
+ const endpoints = Object.values(kb.apiEndpoints);
9149
+ if (endpoints.length === 0) {
9150
+ console.log(`No API endpoints recorded for ${domain}`);
9151
+ return;
9152
+ }
9153
+ console.log(`API Endpoints for ${domain}:`);
9154
+ console.log("");
9155
+ for (const ep of endpoints.sort((a, b) => b.timesSeen - a.timesSeen)) {
9156
+ const params = ep.params.length > 0 ? ep.params.join(", ") : "-";
9157
+ console.log(` ${ep.method} ${ep.path} (${ep.timesSeen}x)`);
9158
+ console.log(` Params: ${params}`);
9159
+ if (ep.responseFields.length > 0) {
9160
+ console.log(` Response: ${ep.responseFields.slice(0, 5).join(", ")}`);
9161
+ }
9162
+ console.log("");
9163
+ }
9164
+ return;
9165
+ }
9166
+ if (subcommand === "search") {
9167
+ const domain = rest[0];
9168
+ const query = rest.slice(1).join(" ").toLowerCase();
9169
+ if (!domain || !query) {
9170
+ console.error("Usage: xbrowser knowledge search <domain> <query>");
9171
+ process.exit(1);
9172
+ }
9173
+ const kb = readSiteKnowledge(domain);
9174
+ if (!kb) {
9175
+ console.error(`No knowledge base found for ${domain}`);
9176
+ process.exit(1);
9177
+ }
9178
+ console.log(`Search results for "${query}" in ${domain}:`);
9179
+ console.log("");
9180
+ let found = 0;
9181
+ for (const [pagePath, page] of Object.entries(kb.pages)) {
9182
+ const matches = page.selectors.filter(
9183
+ (s) => s.selector.toLowerCase().includes(query) || s.description.toLowerCase().includes(query) || (s.text || "").toLowerCase().includes(query)
9184
+ );
9185
+ for (const m of matches) {
9186
+ console.log(` [${pagePath}] ${m.selector} \u2192 ${m.description} (${m.actionType}, ${m.confidence}, ${m.timesSeen}x)`);
9187
+ found++;
9188
+ }
9189
+ }
9190
+ for (const ep of Object.values(kb.apiEndpoints)) {
9191
+ if (ep.path.toLowerCase().includes(query) || ep.params.some((p) => p.toLowerCase().includes(query))) {
9192
+ console.log(` [API] ${ep.method} ${ep.path} (${ep.timesSeen}x)`);
9193
+ found++;
9194
+ }
9195
+ }
9196
+ if (found === 0) {
9197
+ console.log(" No matches found.");
9198
+ }
9199
+ return;
9200
+ }
9201
+ if (subcommand === "issue") {
9202
+ const domain = rest[0];
9203
+ const text = rest.slice(1).join(" ");
9204
+ if (!domain || !text) {
9205
+ console.error("Usage: xbrowser knowledge issue <domain> <description>");
9206
+ process.exit(1);
9207
+ }
9208
+ if (!readSiteKnowledge(domain)) {
9209
+ console.error(`No knowledge base found for ${domain}`);
9210
+ process.exit(1);
9211
+ }
9212
+ addKnownIssue(domain, text);
9213
+ console.log(`Added issue to ${domain}: ${text}`);
9214
+ return;
9215
+ }
9216
+ if (subcommand === "path") {
9217
+ const domain = rest[0];
9218
+ if (!domain) {
9219
+ console.error("Usage: xbrowser knowledge path <domain>");
9220
+ process.exit(1);
9221
+ }
9222
+ const mdPath = getKnowledgePath(domain, "md");
9223
+ const jsonPath = getKnowledgePath(domain, "json");
9224
+ console.log(`Markdown: ${mdPath} (${existsSync12(mdPath) ? "exists" : "not found"})`);
9225
+ console.log(`JSON: ${jsonPath} (${existsSync12(jsonPath) ? "exists" : "not found"})`);
9226
+ return;
9227
+ }
9228
+ console.error(`Unknown subcommand: ${subcommand}`);
9229
+ console.error("Usage: xbrowser knowledge <list|show|selectors|api|search|issue|path>");
9230
+ process.exit(1);
9231
+ }
9232
+ };
9233
+
8515
9234
  // src/builtins/index.ts
8516
9235
  var allBuiltins = [
8517
- sessionOpenBuiltin,
8518
9236
  sessionCloseBuiltin,
8519
9237
  sessionListBuiltin,
8520
9238
  sessionKillBuiltin,
@@ -8525,7 +9243,8 @@ var allBuiltins = [
8525
9243
  pluginListBuiltin,
8526
9244
  pluginReloadBuiltin,
8527
9245
  createBuiltin,
8528
- previewBuiltin
9246
+ previewBuiltin,
9247
+ knowledgeBuiltin
8529
9248
  ];
8530
9249
 
8531
9250
  // src/utils/selector.ts
@@ -8748,7 +9467,8 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
8748
9467
  } else {
8749
9468
  switch (command) {
8750
9469
  case "goto":
8751
- if (!args[0]) outputError(`Usage: xbrowser goto <url>`);
9470
+ case "open":
9471
+ if (!args[0]) outputError(`Usage: xbrowser ${command} <url>`);
8752
9472
  cmdName = "goto";
8753
9473
  params = {
8754
9474
  url: /^https?:\/\//i.test(args[0]) || /^wss?:\/\//i.test(args[0]) ? args[0] : "https://" + args[0],
@@ -8760,7 +9480,9 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
8760
9480
  params = {
8761
9481
  fullPage: !!(options["full-page"] || options.fullPage),
8762
9482
  type: options.type,
8763
- selector: options.selector || options.s
9483
+ selector: options.selector || options.s,
9484
+ base64: !!options.base64,
9485
+ output: options.output || options.o
8764
9486
  };
8765
9487
  break;
8766
9488
  case "eval":
@@ -8796,6 +9518,25 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
8796
9518
  cmdName = "text";
8797
9519
  params = { selector: options.selector || options.s };
8798
9520
  break;
9521
+ case "find": {
9522
+ if (!args[0] || !args[1]) {
9523
+ outputError("Usage: xbrowser find <text|role|label|placeholder|testid|alt|title|first|last|nth> <value> [action] [--name <name>]");
9524
+ }
9525
+ const strategy = args[0];
9526
+ const value = args[1];
9527
+ const operation = args.slice(2).join(" ") || void 0;
9528
+ cmdName = "find";
9529
+ params = {
9530
+ strategy,
9531
+ value,
9532
+ ...operation ? { operation } : {},
9533
+ name: options.name,
9534
+ exact: !!options.exact,
9535
+ timeout: options.timeout ? Number(options.timeout) : void 0,
9536
+ index: options.index ? Number(options.index) : void 0
9537
+ };
9538
+ break;
9539
+ }
8799
9540
  case "back":
8800
9541
  cmdName = "back";
8801
9542
  params = {};
@@ -8818,7 +9559,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
8818
9559
  let parsedActions;
8819
9560
  if (options.action) {
8820
9561
  const actionList = Array.isArray(options.action) ? options.action : [options.action];
8821
- const { parseActionDsl } = await import("./parse-action-dsl-DRSPBALP.js");
9562
+ const { parseActionDsl } = await import("./parse-action-dsl-UM333TL2.js");
8822
9563
  parsedActions = actionList.map((a) => parseActionDsl(a));
8823
9564
  } else if (options["actions-file"]) {
8824
9565
  const fs3 = await import("fs");
@@ -8946,36 +9687,27 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
8946
9687
  }
8947
9688
 
8948
9689
  // src/cli/session-routes.ts
8949
- import { homedir as homedir6 } from "os";
8950
- import { join as join10 } from "path";
8951
- import { readdirSync as readdirSync4, rmSync as rmSync8 } from "fs";
9690
+ import { homedir as homedir8 } from "os";
9691
+ import { join as join12 } from "path";
9692
+ import { readdirSync as readdirSync3, rmSync as rmSync7 } from "fs";
8952
9693
  function cleanSessionFiles() {
8953
- const dir = join10(homedir6(), ".xbrowser", "sessions");
9694
+ const dir = join12(homedir8(), ".xbrowser", "sessions");
8954
9695
  let count = 0;
8955
9696
  try {
8956
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
8957
- const p = join10(dir, entry.name);
8958
- rmSync8(p, { recursive: true, force: true });
9697
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
9698
+ const p = join12(dir, entry.name);
9699
+ rmSync7(p, { recursive: true, force: true });
8959
9700
  count++;
8960
9701
  }
8961
9702
  } catch {
8962
9703
  }
8963
9704
  return count;
8964
9705
  }
8965
- async function handleSession(args, options, mode, cdpEndpoint) {
9706
+ async function handleSession(args, options, mode, _cdpEndpoint) {
8966
9707
  const sub = args[0];
8967
9708
  switch (sub) {
8968
- case "open": {
8969
- const url = args[1];
8970
- const name = options.name || process.env.XBROWSER_SESSION || "default";
8971
- if (!url)
8972
- outputError("Usage: xbrowser session open <url> [--name <name>] [--cdp <endpoint>]");
8973
- const info = await forwardSessionCreate(name, url, cdpEndpoint);
8974
- outputResult({ ok: true, ...info }, mode);
8975
- break;
8976
- }
8977
9709
  case "close": {
8978
- const name = options.name || process.env.XBROWSER_SESSION || "default";
9710
+ const name = options.session || options.name || process.env.XBROWSER_SESSION || "default";
8979
9711
  try {
8980
9712
  await forwardSessionClose(name);
8981
9713
  } catch {
@@ -8987,16 +9719,16 @@ async function handleSession(args, options, mode, cdpEndpoint) {
8987
9719
  case "list":
8988
9720
  case "ls": {
8989
9721
  try {
8990
- const sessions = await forwardSessionList();
8991
- outputResult({ sessions }, mode);
9722
+ const sessions2 = await forwardSessionList();
9723
+ outputResult({ sessions: sessions2 }, mode);
8992
9724
  } catch {
8993
- const sessions = await listSessions();
8994
- outputResult({ sessions }, mode);
9725
+ const sessions2 = await listSessions();
9726
+ outputResult({ sessions: sessions2 }, mode);
8995
9727
  }
8996
9728
  break;
8997
9729
  }
8998
9730
  case "kill": {
8999
- const name = options.name || process.env.XBROWSER_SESSION || "default";
9731
+ const name = options.session || options.name || process.env.XBROWSER_SESSION || "default";
9000
9732
  try {
9001
9733
  await forwardSessionClose(name);
9002
9734
  } catch {
@@ -9011,8 +9743,8 @@ async function handleSession(args, options, mode, cdpEndpoint) {
9011
9743
  }
9012
9744
  case "kill-all": {
9013
9745
  try {
9014
- const sessions = await forwardSessionList();
9015
- for (const s of sessions) {
9746
+ const sessions2 = await forwardSessionList();
9747
+ for (const s of sessions2) {
9016
9748
  try {
9017
9749
  await forwardSessionClose(s.name);
9018
9750
  } catch {
@@ -9039,15 +9771,28 @@ function getPluginLoader2() {
9039
9771
  if (!pluginLoader3) pluginLoader3 = new XBrowserPluginLoader();
9040
9772
  return pluginLoader3;
9041
9773
  }
9042
- async function buildRuntimeCommandsMap() {
9774
+ async function buildRuntimePluginInfo() {
9043
9775
  const loader = await getPluginLoader();
9044
9776
  const sites = loader.getCore().loader.getSites();
9045
9777
  const map = /* @__PURE__ */ new Map();
9046
9778
  for (const site of sites) {
9047
- const cmds = site.getAllCommands().map((c) => c.name);
9048
- if (cmds.length > 0) {
9049
- map.set(site.name, cmds);
9779
+ const cmds = site.getAllCommands();
9780
+ const commandNames = cmds.map((c) => c.name);
9781
+ if (commandNames.length === 0) continue;
9782
+ const anySite = site;
9783
+ const hasLoginHandler = typeof anySite.hasLoginCommand === "function" && anySite.hasLoginCommand();
9784
+ const configRequiresLogin = !!site.config.requiresLogin;
9785
+ const hasLogin = hasLoginHandler || configRequiresLogin;
9786
+ let loggedIn = null;
9787
+ if (hasLogin) {
9788
+ try {
9789
+ loggedIn = await site.isLoggedIn();
9790
+ } catch {
9791
+ loggedIn = null;
9792
+ }
9050
9793
  }
9794
+ const requiresLoginCommands = cmds.filter((c) => c.requiresLogin === true).map((c) => c.name);
9795
+ map.set(site.name, { commands: commandNames, hasLogin, loggedIn, requiresLoginCommands });
9051
9796
  }
9052
9797
  return map;
9053
9798
  }
@@ -9211,6 +9956,54 @@ async function handlePluginInfo(args, options, mode) {
9211
9956
  console.error("\u67E5\u8BE2\u5931\u8D25:", err.message);
9212
9957
  }
9213
9958
  }
9959
+ async function handlePluginSchema(args, mode) {
9960
+ const pluginName = args[0];
9961
+ const commandName = args[1];
9962
+ if (!pluginName) outputError("Usage: xbrowser plugin schema <name> [command] [--json]");
9963
+ const loader = await getPluginLoader();
9964
+ const contract = loader.getPluginContract(pluginName, commandName);
9965
+ if (!contract) {
9966
+ outputError(commandName ? `Command "${commandName}" not found in plugin "${pluginName}"` : `Plugin "${pluginName}" not found`);
9967
+ return;
9968
+ }
9969
+ if (mode === "json") {
9970
+ outputResult(contract, mode);
9971
+ return;
9972
+ }
9973
+ if ("commands" in contract) {
9974
+ printPluginContract(contract);
9975
+ } else {
9976
+ printCommandContract(pluginName, contract);
9977
+ }
9978
+ }
9979
+ function printPluginContract(contract) {
9980
+ console.log(`${contract.plugin.name} contract v${contract.version}`);
9981
+ if (contract.plugin.description) console.log(contract.plugin.description);
9982
+ console.log("");
9983
+ for (const command of contract.commands) {
9984
+ printCommandContract(contract.plugin.name, command);
9985
+ }
9986
+ }
9987
+ function printCommandContract(pluginName, command) {
9988
+ console.log(`${pluginName} ${command.name}`);
9989
+ if (command.description) console.log(` ${command.description}`);
9990
+ console.log(` scope: ${command.scope}`);
9991
+ if (command.capabilities.length > 0) {
9992
+ console.log(` capabilities: ${command.capabilities.join(", ")}`);
9993
+ }
9994
+ if (command.positional.length > 0) {
9995
+ console.log(` positional: ${command.positional.join(", ")}`);
9996
+ }
9997
+ if (command.form.fields.length > 0) {
9998
+ console.log(" fields:");
9999
+ for (const field of command.form.fields) {
10000
+ const required = field.required ? "required" : "optional";
10001
+ const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
10002
+ console.log(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}`);
10003
+ }
10004
+ }
10005
+ console.log("");
10006
+ }
9214
10007
  async function handlePlugin(args, options, mode) {
9215
10008
  const sub = args[0];
9216
10009
  const subArgs = args.slice(1);
@@ -9251,17 +10044,20 @@ async function handlePlugin(args, options, mode) {
9251
10044
  }
9252
10045
  case "list": {
9253
10046
  const plugins = await installer.list();
9254
- const runtimeCommands = await buildRuntimeCommandsMap();
10047
+ const runtimeInfo = await buildRuntimePluginInfo();
9255
10048
  const enrichedPlugins = plugins.map((p) => {
9256
10049
  const metadata = p.metadata;
9257
10050
  const staticCommands = metadata?.commands;
9258
- const dynamicCommands = runtimeCommands.get(p.name);
9259
- const commands = dynamicCommands || staticCommands;
10051
+ const rt = runtimeInfo.get(p.name);
10052
+ const commands = rt?.commands || staticCommands;
9260
10053
  return {
9261
10054
  ...p,
9262
10055
  commands,
9263
10056
  version: metadata?.version,
9264
- description: metadata?.description
10057
+ description: metadata?.description,
10058
+ hasLogin: rt?.hasLogin ?? false,
10059
+ loggedIn: rt?.loggedIn ?? null,
10060
+ requiresLoginCommands: rt?.requiresLoginCommands ?? []
9265
10061
  };
9266
10062
  });
9267
10063
  if (mode === "json") {
@@ -9272,20 +10068,27 @@ async function handlePlugin(args, options, mode) {
9272
10068
  return;
9273
10069
  }
9274
10070
  for (const p of enrichedPlugins) {
10071
+ const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
9275
10072
  if (p.version && p.description) {
9276
- console.log(`${p.name} (${p.version}) - ${p.description}`);
10073
+ console.log(`${p.name} (${p.version}) - ${p.description}${loginTag}`);
9277
10074
  } else {
9278
- console.log(p.name);
10075
+ console.log(`${p.name}${loginTag}`);
9279
10076
  }
9280
10077
  if (p.commands && p.commands.length > 0) {
9281
10078
  console.log(` ${p.commands.join(", ")}`);
9282
10079
  }
10080
+ if (p.requiresLoginCommands.length > 0) {
10081
+ console.log(` requires login: ${p.requiresLoginCommands.join(", ")}`);
10082
+ }
9283
10083
  }
9284
10084
  console.log(`
9285
10085
  Total: ${enrichedPlugins.length} plugins`);
9286
10086
  }
9287
10087
  break;
9288
10088
  }
10089
+ case "schema":
10090
+ await handlePluginSchema(subArgs, mode);
10091
+ break;
9289
10092
  case "reload": {
9290
10093
  const name = subArgs[0];
9291
10094
  if (!name) outputError("Usage: xbrowser plugin reload <name>");
@@ -9343,7 +10146,7 @@ function handleDaemon(args, options, mode) {
9343
10146
  break;
9344
10147
  }
9345
10148
  default:
9346
- console.log("Usage: xbrowser daemon <start|stop|status> [--port <port>]");
10149
+ console.log("Daemon starts automatically. No manual action needed.");
9347
10150
  }
9348
10151
  }
9349
10152
 
@@ -9354,7 +10157,8 @@ async function handleRecord(args, options, mode) {
9354
10157
  case "start": {
9355
10158
  const url = options.url;
9356
10159
  const sessionName = options.session || "default";
9357
- const result = await forwardRecordStart(sessionName, url);
10160
+ const cdpEndpoint = options.cdp;
10161
+ const result = await forwardRecordStart(sessionName, url, cdpEndpoint);
9358
10162
  if (!result.ok) {
9359
10163
  outputError(String(result.error || "Failed to start recording"));
9360
10164
  return;
@@ -9438,6 +10242,13 @@ async function handleRecord(args, options, mode) {
9438
10242
  outputResult(result, mode);
9439
10243
  break;
9440
10244
  }
10245
+ case "generate-plugin": {
10246
+ const sessionName = options.session || args[1] || "default";
10247
+ const pluginName = options.name || "";
10248
+ const outputDir = options.output || "";
10249
+ await handleGeneratePlugin(sessionName, pluginName, outputDir);
10250
+ break;
10251
+ }
9441
10252
  default:
9442
10253
  console.log("Usage:");
9443
10254
  console.log(" xbrowser record start [--url <url>] [--session <name>]");
@@ -9445,6 +10256,7 @@ async function handleRecord(args, options, mode) {
9445
10256
  console.log(" xbrowser record status [--session <name>]");
9446
10257
  console.log(" xbrowser record summary [--session <name>] [--json]");
9447
10258
  console.log(' xbrowser record checkpoint --type <type> --hint "description" [--selector <sel>] [--session <name>]');
10259
+ console.log(" xbrowser record generate-plugin [--session <name>] [--name <plugin>] [--output <dir>]");
9448
10260
  console.log("");
9449
10261
  console.log("Checkpoint types: dialog, captcha, login, iframe, slider, custom");
9450
10262
  }
@@ -9633,7 +10445,7 @@ async function handleConvert(args, _mode) {
9633
10445
  const fs3 = await import("fs");
9634
10446
  const path3 = await import("path");
9635
10447
  const { default: yaml } = await import("yaml");
9636
- const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-4DUWZIKH.js");
10448
+ const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-LB3GJTLR.js");
9637
10449
  const content = fs3.readFileSync(filePath, "utf-8");
9638
10450
  const recording = yaml.parse(content);
9639
10451
  const ext = path3.extname(outputPath).toLowerCase();
@@ -9658,7 +10470,7 @@ async function handleExtract(args, _mode) {
9658
10470
  console.error("Usage: xbrowser extract <recording.yaml>");
9659
10471
  process.exit(1);
9660
10472
  }
9661
- const { extractAndSave, printExtractSummary } = await import("./extract-EGRXZSSK.js");
10473
+ const { extractAndSave, printExtractSummary } = await import("./extract-BSYBM4MR.js");
9662
10474
  const { summary, outputPath } = extractAndSave(filePath);
9663
10475
  printExtractSummary(summary);
9664
10476
  console.log(`
@@ -9671,33 +10483,196 @@ async function handleFilter(args, _mode) {
9671
10483
  console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude-types=type1,type2]");
9672
10484
  process.exit(1);
9673
10485
  }
9674
- const { filterRecording, parseExcludeTypes } = await import("./filter-OLAE26HN.js");
10486
+ const { filterRecording, parseExcludeTypes } = await import("./filter-KCFO4RSV.js");
9675
10487
  const excludeTypes = parseExcludeTypes(args.slice(2));
9676
10488
  const result = filterRecording(filePath, outputPath, excludeTypes);
9677
10489
  console.log(`Filtered ${filePath} -> ${outputPath}`);
9678
10490
  console.log(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)`);
9679
10491
  }
9680
-
9681
- // src/stdin.ts
9682
- import { createInterface } from "readline";
9683
- import { readFileSync as readFileSync15 } from "fs";
9684
- async function readStdin2() {
9685
- if (process.stdin.isTTY) return [];
9686
- const lines = [];
9687
- const rl = createInterface({ input: process.stdin });
9688
- for await (const line of rl) {
9689
- const trimmed = line.trim();
9690
- if (trimmed && !trimmed.startsWith("#")) {
9691
- lines.push(trimmed);
10492
+ async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
10493
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RTDGURIJ.js");
10494
+ const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
10495
+ const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync11 } = await import("fs");
10496
+ const { join: join13 } = await import("path");
10497
+ const data = SessionRecorder2.readData(sessionName);
10498
+ if (!data) {
10499
+ outputError(`No recording found for session "${sessionName}". Run \`xbrowser record stop --session ${sessionName}\` first.`);
10500
+ return;
10501
+ }
10502
+ let domain = "unknown";
10503
+ try {
10504
+ domain = new URL(data.startUrl).hostname.replace(/^www\./, "");
10505
+ } catch {
10506
+ }
10507
+ const finalPluginName = pluginName || domain.split(".")[0] || "my-site";
10508
+ const finalOutputDir = outputDir || join13(process.cwd(), ".xcli", "plugins", finalPluginName);
10509
+ const knowledge = readSiteKnowledge2(domain);
10510
+ const knowledgeMd = knowledge ? toMarkdown(knowledge) : "";
10511
+ const pluginCode = generatePluginCode(finalPluginName, domain, data, knowledgeMd);
10512
+ mkdirSync10(join13(finalOutputDir), { recursive: true });
10513
+ writeFileSync11(join13(finalOutputDir, "index.ts"), pluginCode, "utf-8");
10514
+ if (knowledgeMd) {
10515
+ writeFileSync11(join13(finalOutputDir, "SITE_KNOWLEDGE.md"), knowledgeMd, "utf-8");
10516
+ }
10517
+ console.log("");
10518
+ console.log("=== Plugin Generated ===");
10519
+ console.log(` Plugin: ${finalPluginName}`);
10520
+ console.log(` Domain: ${domain}`);
10521
+ console.log(` Output: ${finalOutputDir}/index.ts`);
10522
+ if (knowledgeMd) {
10523
+ console.log(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md`);
10524
+ }
10525
+ console.log(` Actions: ${data.actions.length}`);
10526
+ console.log(` APIs: ${data.network.filter((n) => n.contentType.includes("json") || n.url.includes("/api/")).length}`);
10527
+ console.log("");
10528
+ console.log("Next steps:");
10529
+ console.log(` 1. Review and edit: ${finalOutputDir}/index.ts`);
10530
+ console.log(` 2. Test: xbrowser ${finalPluginName} <command>`);
10531
+ console.log(` 3. Reference: ${finalOutputDir}/SITE_KNOWLEDGE.md (for LLM)`);
10532
+ }
10533
+ function generatePluginCode(pluginName, domain, data, _knowledgeMd) {
10534
+ const pagePaths = /* @__PURE__ */ new Set();
10535
+ for (const action of data.actions) {
10536
+ if (action.url) {
10537
+ try {
10538
+ pagePaths.add(new URL(action.url).pathname);
10539
+ } catch {
10540
+ }
10541
+ }
10542
+ }
10543
+ const clickSelectors = [];
10544
+ const inputSelectors = [];
10545
+ for (const action of data.actions) {
10546
+ const el = action.element;
10547
+ if (!el) continue;
10548
+ const sel = el.selector;
10549
+ if (!sel) continue;
10550
+ if (action.type === "click" && !clickSelectors.includes(sel)) {
10551
+ clickSelectors.push(sel);
9692
10552
  }
10553
+ if (action.type === "input" && !inputSelectors.some((s) => s.selector === sel)) {
10554
+ inputSelectors.push({ selector: sel, placeholder: el.placeholder });
10555
+ }
10556
+ }
10557
+ const apis = data.network.filter(
10558
+ (n) => (n.contentType || "").includes("json") || n.url.includes("/api/")
10559
+ );
10560
+ const commands = [];
10561
+ commands.push(` site.command({
10562
+ name: 'open',
10563
+ description: 'Open ${domain}',
10564
+ scope: 'browser',
10565
+ handler: async (_p: Record<string, unknown>, ctx: CommandContext) => {
10566
+ const page = ensurePage(ctx);
10567
+ await page.goto('${data.startUrl}', { waitUntil: 'domcontentloaded' });
10568
+ return ok({ url: page.url() });
10569
+ },
10570
+ });`);
10571
+ if (inputSelectors.length > 0) {
10572
+ const params = inputSelectors.slice(0, 5).map(
10573
+ (s, i) => ` ${s.selector.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+/, "").toLowerCase() || `field${i}`}: z.string().describe('${s.placeholder || s.selector}'),`
10574
+ ).join("\n");
10575
+ const fills = inputSelectors.slice(0, 5).map(
10576
+ (s) => ` await page.fill('${s.selector}', p.${s.selector.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+/, "").toLowerCase() || "field0"}');`
10577
+ ).join("\n");
10578
+ commands.push(` site.command({
10579
+ name: 'fill',
10580
+ description: 'Fill form on ${domain}',
10581
+ scope: 'page',
10582
+ parameters: z.object({
10583
+ ${params}
10584
+ }),
10585
+ handler: async (p: Record<string, unknown>, ctx: CommandContext) => {
10586
+ const page = ensurePage(ctx);
10587
+ ${fills}
10588
+ return ok({ filled: true });
10589
+ },
10590
+ });`);
10591
+ }
10592
+ if (clickSelectors.length > 0) {
10593
+ commands.push(` site.command({
10594
+ name: 'click',
10595
+ description: 'Click element on ${domain}',
10596
+ scope: 'page',
10597
+ parameters: z.object({
10598
+ selector: z.string().describe('CSS selector of element to click'),
10599
+ }),
10600
+ handler: async (p: Record<string, unknown>, ctx: CommandContext) => {
10601
+ const page = ensurePage(ctx);
10602
+ await page.click(p.selector as string);
10603
+ return ok({ clicked: p.selector });
10604
+ },
10605
+ });`);
10606
+ }
10607
+ if (apis.length > 0) {
10608
+ commands.push(` site.command({
10609
+ name: 'scrape',
10610
+ description: 'Scrape data from ${domain}',
10611
+ scope: 'page',
10612
+ handler: async (_p: Record<string, unknown>, ctx: CommandContext) => {
10613
+ const page = ensurePage(ctx);
10614
+ const data = await page.evaluate(() => {
10615
+ return {
10616
+ title: document.title,
10617
+ url: location.href,
10618
+ content: document.body?.innerText?.substring(0, 5000) || '',
10619
+ };
10620
+ });
10621
+ return ok(data);
10622
+ },
10623
+ });`);
9693
10624
  }
9694
- return lines;
10625
+ return `/**
10626
+ * ${pluginName} \u2014 Auto-generated plugin for ${domain}
10627
+ *
10628
+ * Generated from xbrowser recording session.
10629
+ * Review and customize before using in production.
10630
+ *
10631
+ * Site Knowledge: See SITE_KNOWLEDGE.md for LLM-readable selector/API reference.
10632
+ */
10633
+
10634
+ import { z } from 'zod';
10635
+ import { ok } from '@dyyz1993/xcli-core';
10636
+ import { createSite, type CommandContext } from '@dyyz1993/xcli-core';
10637
+
10638
+ interface XBPage {
10639
+ url(): string;
10640
+ goto(url: string, opts?: Record<string, unknown>): Promise<unknown>;
10641
+ click(selector: string, opts?: Record<string, unknown>): Promise<unknown>;
10642
+ fill(selector: string, value: string, opts?: Record<string, unknown>): Promise<unknown>;
10643
+ evaluate<T>(fn: string | (() => T)): Promise<T>;
10644
+ }
10645
+
10646
+ function ensurePage(ctx: CommandContext): XBPage {
10647
+ const page = (ctx as Record<string, unknown>).page;
10648
+ if (!page) throw new Error('No active page. Start a session first.');
10649
+ return page as unknown as XBPage;
9695
10650
  }
9696
- function readCommandFile(filePath) {
9697
- const content = readFileSync15(filePath, "utf-8");
9698
- return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
10651
+
10652
+ export default createSite({
10653
+ name: '${pluginName}',
10654
+ domain: '${domain}',
10655
+ description: 'Auto-generated plugin for ${domain} from recording',
10656
+
10657
+ login: {
10658
+ url: '${data.startUrl}',
10659
+ detect: async (ctx: CommandContext) => {
10660
+ const page = ensurePage(ctx);
10661
+ // TODO: Add login detection logic
10662
+ return false;
10663
+ },
10664
+ },
10665
+
10666
+ setup(site) {
10667
+ ${commands.join("\n\n")}
10668
+ },
10669
+ });
10670
+ `;
9699
10671
  }
9700
10672
 
10673
+ // src/stdin.ts
10674
+ import { readStdin as readStdin2, readCommandFile, splitFileLine } from "@dyyz1993/xcli-core";
10675
+
9701
10676
  // src/cli/run-routes.ts
9702
10677
  async function handleRun(filePath, options) {
9703
10678
  let commands;
@@ -9741,10 +10716,10 @@ async function handleRun(filePath, options) {
9741
10716
  async function handleViewer(_args, options, mode, _cdpEndpoint) {
9742
10717
  const name = options.name || process.env.XBROWSER_SESSION || "default";
9743
10718
  const selector = options.selector;
9744
- const status = getDaemonProcessStatus();
10719
+ let status = getDaemonProcessStatus();
9745
10720
  if (!status.running) {
9746
- outputError("Daemon is not running. Start with: xbrowser daemon start");
9747
- return;
10721
+ await startDaemonProcess();
10722
+ status = getDaemonProcessStatus();
9748
10723
  }
9749
10724
  const port = status.port || getDaemonConfig().basePort;
9750
10725
  let url = `http://localhost:${port}/preview/${name}`;
@@ -10064,6 +11039,235 @@ async function handleNetCommand(args, options, mode, sessionName) {
10064
11039
  }
10065
11040
  }
10066
11041
 
11042
+ // src/cli/test-routes.ts
11043
+ import { execSync as execSync3 } from "child_process";
11044
+ import { readFileSync as readFileSync8 } from "fs";
11045
+ import { resolve as resolve9 } from "path";
11046
+ function findPluginPath(plugin) {
11047
+ const candidates = [
11048
+ resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts"),
11049
+ resolve9(process.cwd(), "node_modules/@xbrowser/cli/.xcli/plugins", plugin, "index.ts")
11050
+ ];
11051
+ for (const p of candidates) {
11052
+ try {
11053
+ readFileSync8(p, "utf-8");
11054
+ return p;
11055
+ } catch {
11056
+ }
11057
+ }
11058
+ return resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts");
11059
+ }
11060
+ function extractSchema(plugin, command) {
11061
+ const pluginPath = findPluginPath(plugin);
11062
+ let src;
11063
+ try {
11064
+ src = readFileSync8(pluginPath, "utf-8");
11065
+ } catch {
11066
+ return null;
11067
+ }
11068
+ const cmdIdx = src.indexOf(`.command('${command}'`);
11069
+ if (cmdIdx < 0) return null;
11070
+ const after = src.slice(cmdIdx);
11071
+ const resultIdx = after.indexOf("result:");
11072
+ if (resultIdx < 0) return null;
11073
+ let block = after.slice(resultIdx + 7);
11074
+ let depth = 0;
11075
+ let schemaStr = "";
11076
+ for (const ch of block) {
11077
+ if (ch === "{" || ch === "(" || ch === "[") depth++;
11078
+ if (ch === "}" || ch === ")" || ch === "]") {
11079
+ depth--;
11080
+ if (depth < 0) break;
11081
+ }
11082
+ if (depth === 0 && /\w/.test(ch) && schemaStr.trim().endsWith(",")) {
11083
+ break;
11084
+ }
11085
+ schemaStr += ch;
11086
+ }
11087
+ const objStart = schemaStr.indexOf("z.object({");
11088
+ const objEnd = objStart >= 0 ? schemaStr.indexOf("})", objStart) : -1;
11089
+ const objStr = objStart >= 0 && objEnd > objStart ? schemaStr.slice(objStart + 10, objEnd) : schemaStr;
11090
+ const fields = [];
11091
+ const SKIP_NAMES = /* @__PURE__ */ new Set(["passthrough", "optional", "describe", "default"]);
11092
+ const fieldRegex = /(\w+)\s*:\s*z\.(\w+)/g;
11093
+ let match;
11094
+ while ((match = fieldRegex.exec(objStr)) !== null) {
11095
+ const name = match[1];
11096
+ const type = match[2];
11097
+ if (SKIP_NAMES.has(name) || type === "union" || type === "enum") continue;
11098
+ const afterField = objStr.slice(match.index + match[0].length);
11099
+ const isOptional = afterField.trimStart().startsWith(".optional()");
11100
+ fields.push({
11101
+ name,
11102
+ type: type === "string" ? "string" : type === "number" ? "number" : type === "boolean" ? "boolean" : type === "array" ? "array" : type,
11103
+ optional: isOptional || name === "index"
11104
+ });
11105
+ }
11106
+ return fields.length > 0 ? fields : null;
11107
+ }
11108
+ async function runTest(plugin, command, cmdArgs, options) {
11109
+ const cdp = options.cdp || options.cdpEndpoint || "http://localhost:9221";
11110
+ const argsStr = cmdArgs.filter((a) => !a.startsWith("--cdp")).join(" ");
11111
+ const schema = extractSchema(plugin, command);
11112
+ const fullCmd = `npx xbrowser ${plugin} ${command} ${argsStr} --cdp ${cdp} --json --timeout 60000`;
11113
+ let stdout = "";
11114
+ try {
11115
+ stdout = execSync3(fullCmd, {
11116
+ timeout: 65e3,
11117
+ encoding: "utf-8",
11118
+ stdio: ["pipe", "pipe", "pipe"],
11119
+ env: { ...process.env, FORCE_COLOR: "0" }
11120
+ });
11121
+ } catch (e) {
11122
+ const err = e;
11123
+ stdout = err.stdout?.toString() || "";
11124
+ const jsonLine = stdout.split("\n").find((l) => {
11125
+ try {
11126
+ JSON.parse(l);
11127
+ return true;
11128
+ } catch {
11129
+ return false;
11130
+ }
11131
+ });
11132
+ if (jsonLine) {
11133
+ try {
11134
+ const parsed2 = JSON.parse(jsonLine);
11135
+ const code = parsed2?.data?.code || "";
11136
+ if (code === "LOGIN_REQUIRED") {
11137
+ return { status: "LOGIN_REQUIRED", message: parsed2.message || "\u9700\u8981\u767B\u5F55", viewerUrl: "http://localhost:9224/preview/default" };
11138
+ }
11139
+ } catch {
11140
+ }
11141
+ }
11142
+ const stderr = err.stderr?.toString() || "";
11143
+ if (stdout.includes("captcha") || stderr.includes("captcha") || stdout.includes("CAPTCHA")) {
11144
+ return { status: "CAPTCHA", message: "\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
11145
+ }
11146
+ return { status: "EXEC_ERROR", message: (err.message || "").slice(0, 200) || "\u6267\u884C\u5931\u8D25" };
11147
+ }
11148
+ const allLines = stdout.split("\n");
11149
+ const jsonStart = allLines.findIndex((l) => l.trim().startsWith("{"));
11150
+ const jsonStr = jsonStart >= 0 ? allLines.slice(jsonStart).join("\n") : "";
11151
+ let parsed = {};
11152
+ try {
11153
+ parsed = JSON.parse(jsonStr);
11154
+ } catch {
11155
+ return { status: "EXEC_ERROR", message: "\u65E0\u6CD5\u89E3\u6790 CLI \u8F93\u51FA" };
11156
+ }
11157
+ const rawData = parsed.data;
11158
+ const rawTips = parsed.tips || [];
11159
+ if (parsed.success === false) {
11160
+ const code = rawData?.code || "";
11161
+ if (code === "LOGIN_REQUIRED") {
11162
+ return { status: "LOGIN_REQUIRED", message: parsed.message || "\u9700\u8981\u767B\u5F55", viewerUrl: "http://localhost:9224/preview/default" };
11163
+ }
11164
+ if (rawTips.join(" ").includes("captcha") || rawTips.join(" ").includes("CAPTCHA")) {
11165
+ return { status: "CAPTCHA", message: parsed.message || "\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
11166
+ }
11167
+ }
11168
+ const data = rawData;
11169
+ if (data === null || data === void 0) {
11170
+ const msg = parsed.message || "";
11171
+ const tips = rawTips.join(" ");
11172
+ const viewerUrl = parsed.viewerUrl || rawData?.viewerUrl || "";
11173
+ let status = "NO_DATA";
11174
+ if (msg.includes("block") || msg.includes("anti-bot") || msg.includes("captcha") || tips.includes("viewer")) {
11175
+ status = "BLOCKED";
11176
+ } else if (msg.includes("\u767B\u5F55") || msg.includes("login")) {
11177
+ status = "LOGIN_REQUIRED";
11178
+ }
11179
+ const ret = { status, message: msg || "\u6682\u65E0\u6570\u636E" };
11180
+ if (viewerUrl) ret.viewerUrl = viewerUrl;
11181
+ return ret;
11182
+ }
11183
+ if (!schema) {
11184
+ return { status: "OK", note: "\u65E0 result schema", data: JSON.stringify(data).slice(0, 200) };
11185
+ }
11186
+ const errors = [];
11187
+ const items = Array.isArray(data) ? data.slice(0, 3) : [data];
11188
+ for (const item of items) {
11189
+ if (typeof item !== "object" || item === null) {
11190
+ errors.push("\u6570\u636E\u9879\u4E0D\u662F\u5BF9\u8C61");
11191
+ continue;
11192
+ }
11193
+ for (const field of schema) {
11194
+ const val = item[field.name];
11195
+ if (val === void 0) {
11196
+ if (!field.optional) errors.push(`\u7F3A\u5C11: ${field.name}`);
11197
+ continue;
11198
+ }
11199
+ if (field.type === "array") {
11200
+ if (!Array.isArray(val)) errors.push(`${field.name}: \u671F\u671B array`);
11201
+ } else if (typeof val !== field.type) {
11202
+ errors.push(`${field.name}: \u671F\u671B ${field.type}, \u5B9E\u9645 ${typeof val}`);
11203
+ }
11204
+ }
11205
+ }
11206
+ if (errors.length > 0) {
11207
+ return { status: "SCHEMA_ERROR", errors, data: JSON.stringify(data).slice(0, 200) };
11208
+ }
11209
+ const count = Array.isArray(data) ? data.length : 1;
11210
+ return { status: "OK", count, data: JSON.stringify(data).slice(0, 200) };
11211
+ }
11212
+ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
11213
+ const plugin = cmdArgs[0];
11214
+ const command = cmdArgs[1];
11215
+ if (!plugin || !command) {
11216
+ console.error("\u7528\u6CD5: xbrowser test <plugin> <command> [\u53C2\u6570...]");
11217
+ console.error("\u793A\u4F8B: xbrowser test doubao list --cdp 9221");
11218
+ return;
11219
+ }
11220
+ const loader = await getPluginLoader();
11221
+ const internalLoader = loader.getCore().loader;
11222
+ const site = internalLoader.getSite(plugin);
11223
+ if (!site) {
11224
+ console.error(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
11225
+ return;
11226
+ }
11227
+ const cmdEntry = site.getCommand(command);
11228
+ if (!cmdEntry) {
11229
+ console.error(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
11230
+ return;
11231
+ }
11232
+ const testArgs = cmdArgs.slice(2);
11233
+ const mergedOptions = { ...options, cdp: cdpEndpoint || options.cdp };
11234
+ const result = await runTest(plugin, command, testArgs, mergedOptions);
11235
+ if (mode === "json") {
11236
+ console.log(JSON.stringify(result, null, 2));
11237
+ return;
11238
+ }
11239
+ const r = result;
11240
+ const icons = {
11241
+ OK: "\u2705",
11242
+ LOGIN_REQUIRED: "\u{1F511}",
11243
+ CAPTCHA: "\u{1F6A8}",
11244
+ SCHEMA_ERROR: "\u274C",
11245
+ BLOCKED: "\u{1F6A7}",
11246
+ NO_DATA: "\u{1F4ED}",
11247
+ EXEC_ERROR: "\u{1F4A5}"
11248
+ };
11249
+ const status = String(r.status);
11250
+ const icon = icons[status] || "\u2753";
11251
+ console.log(`
11252
+ ${icon} ${plugin}.${command}`);
11253
+ console.log(` \u72B6\u6001: ${status}`);
11254
+ if (status === "OK") {
11255
+ if (r.count) console.log(` \u6570\u636E: ${r.count} \u9879`);
11256
+ if (r.data) console.log(` \u9884\u89C8: ${String(r.data).slice(0, 150)}`);
11257
+ } else if (status === "LOGIN_REQUIRED" || status === "CAPTCHA") {
11258
+ console.log(` \u4FE1\u606F: ${String(r.message)}`);
11259
+ console.log(` Viewer: ${String(r.viewerUrl)}`);
11260
+ } else if (status === "SCHEMA_ERROR") {
11261
+ const errs = r.errors;
11262
+ if (errs) console.log(` \u9519\u8BEF: ${errs.join("; ")}`);
11263
+ } else if (["NO_DATA", "BLOCKED"].includes(status)) {
11264
+ console.log(` \u4FE1\u606F: ${String(r.message)}`);
11265
+ if (r.viewerUrl) console.log(` Viewer: ${String(r.viewerUrl)}`);
11266
+ } else {
11267
+ console.log(` \u4FE1\u606F: ${String(r.message)}`);
11268
+ }
11269
+ }
11270
+
10067
11271
  // src/cli/help.ts
10068
11272
  function showMainHelp() {
10069
11273
  console.log(`
@@ -10077,11 +11281,11 @@ Usage:
10077
11281
  xbrowser -e cmd1 -e cmd2 Execute multiple -e commands
10078
11282
 
10079
11283
  Commands:
10080
- session open <url> [--name <n>] Open browser session
10081
- session close [--name <n>] Close session
11284
+ session close [--session <name>] Close session
10082
11285
  session list List sessions
10083
- session kill [--name <n>] Kill session
11286
+ session kill [--session <name>] Kill session
10084
11287
  goto <url> Navigate to URL
11288
+ open <url> Navigate to URL (alias for goto)
10085
11289
  click <selector> Click element (-s <sel>)
10086
11290
  fill <selector> <value> Fill input (-s <sel> -v <val>)
10087
11291
  type <selector> <text> Type text (-s <sel> -v <text>)
@@ -10091,7 +11295,7 @@ Commands:
10091
11295
  dblclick <selector> Double click (-s <sel>)
10092
11296
  check <selector> Check checkbox (-s <sel>)
10093
11297
  uncheck <selector> Uncheck checkbox (-s <sel>)
10094
- screenshot [--full-page] Take screenshot
11298
+ screenshot [--full-page] [--base64] Take screenshot (saves to ~/.xbrowser/screenshots/; use --base64 for inline data)
10095
11299
  eval <expression> Evaluate JS
10096
11300
  wait <selector> [--timeout <ms>] Wait for element (-s <sel>)
10097
11301
  scroll <direction> [--distance N] Scroll page
@@ -10113,9 +11317,6 @@ Commands:
10113
11317
  plugin list List plugins
10114
11318
  plugin reload <name> Reload plugin
10115
11319
  create <name> --template <type> Create plugin
10116
- daemon start [--port <port>] Start daemon
10117
- daemon stop Stop daemon
10118
- daemon status Check status
10119
11320
  serve [--port <port>] [--token <t>] Start HTTP server for remote access
10120
11321
  remote <url> [command] [--token <t>] Execute command on remote server
10121
11322
  record start --url <url> Start recording
@@ -10326,14 +11527,14 @@ async function listCommands() {
10326
11527
  return jsonResponse(200, { commands });
10327
11528
  }
10328
11529
  async function listSessions2() {
10329
- const sessions = getAllSessions().map((s) => ({
11530
+ const sessions2 = getAllSessions().map((s) => ({
10330
11531
  id: s.id,
10331
11532
  name: s.name,
10332
11533
  url: s.page?.url() ?? null,
10333
11534
  createdAt: s.createdAt,
10334
11535
  isCDP: s.isCDP ?? false
10335
11536
  }));
10336
- return jsonResponse(200, { sessions });
11537
+ return jsonResponse(200, { sessions: sessions2 });
10337
11538
  }
10338
11539
  async function createSessionHandler(req) {
10339
11540
  const body = req.body;
@@ -10488,19 +11689,19 @@ function headersToObject(headers) {
10488
11689
  return result;
10489
11690
  }
10490
11691
  function readBody(req) {
10491
- return new Promise((resolve16, reject) => {
11692
+ return new Promise((resolve10, reject) => {
10492
11693
  const chunks = [];
10493
11694
  req.on("data", (chunk) => chunks.push(chunk));
10494
11695
  req.on("end", () => {
10495
11696
  const raw = Buffer.concat(chunks).toString("utf-8");
10496
11697
  if (!raw) {
10497
- resolve16(null);
11698
+ resolve10(null);
10498
11699
  return;
10499
11700
  }
10500
11701
  try {
10501
- resolve16(JSON.parse(raw));
11702
+ resolve10(JSON.parse(raw));
10502
11703
  } catch {
10503
- resolve16(null);
11704
+ resolve10(null);
10504
11705
  }
10505
11706
  });
10506
11707
  req.on("error", reject);
@@ -10545,7 +11746,7 @@ var HTTPServer = class {
10545
11746
  res.end(JSON.stringify({ error: "INTERNAL_ERROR", message, statusCode: 500 }));
10546
11747
  });
10547
11748
  });
10548
- return new Promise((resolve16, reject) => {
11749
+ return new Promise((resolve10, reject) => {
10549
11750
  const server = this.server;
10550
11751
  server.on("error", (err) => {
10551
11752
  this.server = null;
@@ -10557,7 +11758,7 @@ var HTTPServer = class {
10557
11758
  this.port = addr.port;
10558
11759
  }
10559
11760
  console.log(`HTTP server listening on http://${this.host}:${this.port}`);
10560
- resolve16({ port: this.port, host: this.host });
11761
+ resolve10({ port: this.port, host: this.host });
10561
11762
  });
10562
11763
  });
10563
11764
  }
@@ -10568,13 +11769,13 @@ var HTTPServer = class {
10568
11769
  */
10569
11770
  async stop() {
10570
11771
  if (!this.server) return;
10571
- return new Promise((resolve16, reject) => {
11772
+ return new Promise((resolve10, reject) => {
10572
11773
  this.server.close((err) => {
10573
11774
  if (err) {
10574
11775
  reject(err);
10575
11776
  } else {
10576
11777
  this.server = null;
10577
- resolve16();
11778
+ resolve10();
10578
11779
  }
10579
11780
  });
10580
11781
  });
@@ -10633,6 +11834,7 @@ function showCommandHelp(siteName, cmd, siteConfig, mode) {
10633
11834
  command: c.name,
10634
11835
  description: c.description,
10635
11836
  scope: c.scope,
11837
+ ...c.loginRequired ? { loginRequired: c.loginRequired } : {},
10636
11838
  parameters: paramsList
10637
11839
  }, mode);
10638
11840
  } else {
@@ -10645,9 +11847,25 @@ function showCommandHelp(siteName, cmd, siteConfig, mode) {
10645
11847
  examples: c.examples
10646
11848
  }, { color: false, emoji: false });
10647
11849
  console.log(text);
11850
+ if (c.loginRequired) {
11851
+ console.log(` Login: ${c.loginRequired}`);
11852
+ }
10648
11853
  console.log("");
10649
11854
  }
10650
11855
  }
11856
+ function outputLoginRequired(result, mode) {
11857
+ if (mode === "json" || mode === "yaml") {
11858
+ console.log(outputFormatter2.format(result, { mode, color: false, emoji: false }));
11859
+ return;
11860
+ }
11861
+ const message = result.message || "Login required";
11862
+ console.error(message);
11863
+ for (const tip of result.tips || []) {
11864
+ const text = typeof tip === "string" ? tip : tip.message;
11865
+ if (text !== message) console.error(` \u{1F4A1} ${text}`);
11866
+ }
11867
+ process.exit(1);
11868
+ }
10651
11869
  function extractZodFieldInfo(value) {
10652
11870
  const field = value;
10653
11871
  const fieldDef = field._def;
@@ -10711,7 +11929,7 @@ function extractCdpFromArgv(argv) {
10711
11929
  if (argv[i] === "--cdp" && argv[i + 1]) return argv[i + 1];
10712
11930
  if (typeof argv[i] === "string" && argv[i].startsWith("--cdp=")) return argv[i].slice(6);
10713
11931
  }
10714
- return void 0;
11932
+ return process.env.XBROWSER_CDP;
10715
11933
  }
10716
11934
  async function handleStdinMode(stdinCommands, argv) {
10717
11935
  const chain = stdinCommands.join(" && ");
@@ -10774,7 +11992,7 @@ async function routeCommand(argv, stdinCommands) {
10774
11992
  const cmdArgs = positional.slice(1);
10775
11993
  const mode = options.json ? "json" : options.yaml ? "yaml" : "text";
10776
11994
  const sessionName = options.session || process.env.XBROWSER_SESSION || "default";
10777
- const cdpEndpoint = options.cdp;
11995
+ const cdpEndpoint = options.cdp || process.env.XBROWSER_CDP;
10778
11996
  if (options.version || options.v) {
10779
11997
  console.log(`xbrowser v${version}`);
10780
11998
  return;
@@ -10907,12 +12125,21 @@ async function routeCommand(argv, stdinCommands) {
10907
12125
  if (builtin) await builtin.execute(cmdArgs, options, { cwd: process.cwd() });
10908
12126
  break;
10909
12127
  }
12128
+ case "knowledge":
12129
+ case "know": {
12130
+ const builtin = allBuiltins.find((b) => b.name === "knowledge");
12131
+ if (builtin) await builtin.execute(cmdArgs, options, { cwd: process.cwd() });
12132
+ break;
12133
+ }
10910
12134
  case "viewer":
10911
12135
  await handleViewer(cmdArgs, options, mode, cdpEndpoint);
10912
12136
  break;
10913
12137
  case "help":
10914
12138
  showMainHelp();
10915
12139
  break;
12140
+ case "test":
12141
+ await handleTest(cmdArgs, options, mode, cdpEndpoint);
12142
+ break;
10916
12143
  case "net":
10917
12144
  await handleNetCommand(cmdArgs, options, mode, sessionName);
10918
12145
  break;
@@ -11022,9 +12249,14 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
11022
12249
  }
11023
12250
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
11024
12251
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
11025
- const { forwardExec } = await import("./daemon-client-XWSSQBEA.js");
12252
+ const { forwardExec } = await import("./daemon-client-UZZEHHIV.js");
11026
12253
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
11027
- const result = await forwardExec(command, params, sessionName, cdpEndpoint, userTimeout);
12254
+ const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
12255
+ const resultData = result && typeof result === "object" ? result.data : void 0;
12256
+ if (result && result.success === false && resultData?.code === "LOGIN_REQUIRED") {
12257
+ outputLoginRequired(result, mode);
12258
+ return;
12259
+ }
11028
12260
  if (result && result.success !== false) {
11029
12261
  if (isCommandResult2(result)) {
11030
12262
  if (mode === "json" || mode === "yaml") {
@@ -11057,6 +12289,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
11057
12289
  browser: needsBrowser ? session.context.browser() : null,
11058
12290
  browserContext: needsBrowser ? session.context : null,
11059
12291
  sessionId: needsBrowser ? session.id : "",
12292
+ cdpEndpoint: cdpEndpoint || (needsBrowser ? session?.cdpEndpoint : void 0),
11060
12293
  storage: getPluginStorage(command),
11061
12294
  output: { mode, showTips: true, color: true, emoji: true },
11062
12295
  error: (msg) => {
@@ -11067,10 +12300,33 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
11067
12300
  cliName: "xbrowser",
11068
12301
  waitForHuman: async (_opts) => {
11069
12302
  return { solved: false, timedOut: true };
11070
- }
12303
+ },
12304
+ tips: new TipCollector2()
11071
12305
  };
11072
12306
  try {
11073
12307
  const cmdStart = Date.now();
12308
+ const loginGuard = await checkPluginLoginRequired({
12309
+ site,
12310
+ command: cmdEntry,
12311
+ commandName: subCommand,
12312
+ ctx,
12313
+ page: needsBrowser ? session?.page : null,
12314
+ sessionName
12315
+ });
12316
+ if (!loginGuard.ok) {
12317
+ const result2 = {
12318
+ success: false,
12319
+ data: loginGuard.data ?? null,
12320
+ message: loginGuard.message,
12321
+ tips: normalizeTips7(loginGuard.tips)
12322
+ };
12323
+ if (mode === "json" || mode === "yaml") {
12324
+ outputLoginRequired(result2, mode);
12325
+ } else {
12326
+ outputLoginRequired(result2, mode);
12327
+ }
12328
+ return;
12329
+ }
11074
12330
  const cmdHooks = await loadHooks();
11075
12331
  if (cmdHooks.length > 0 && session?.page) {
11076
12332
  await Promise.all(cmdHooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${command} ${subCommand}`, params })));
@@ -11089,23 +12345,39 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
11089
12345
  saveSessionDiskMeta(sessionName, { conversationUrl: convUrl, cdpEndpoint });
11090
12346
  }
11091
12347
  }
12348
+ let injectedViewerUrl;
12349
+ const LOGIN_FAIL_KEYWORDS = ["\u767B\u5F55", "login", "Login", "\u672A\u767B\u5F55", "not logged in", "cdp", "CDP", "\u9A8C\u8BC1\u7801", "\u9A8C\u8BC1", "captcha", "\u9700\u8981\u767B\u5F55", "requires login"];
12350
+ const tipTexts = (result.tips || []).map((t) => typeof t === "string" ? t : t.message);
12351
+ const isLoginFail = isCommandResult2(result) && result.success === false && [result.message, ...tipTexts].filter(Boolean).join(" ").toLowerCase().match(new RegExp(LOGIN_FAIL_KEYWORDS.join("|"), "i"));
12352
+ if (isLoginFail) {
12353
+ injectedViewerUrl = buildViewerUrl(sessionName);
12354
+ if (injectedViewerUrl) {
12355
+ result.tips = [...result.tips || [], makeTip.info(`Open viewer to complete login: ${injectedViewerUrl}`)];
12356
+ }
12357
+ }
11092
12358
  const outputData = isCommandResult2(result) ? result.data : result && typeof result === "object" ? result.data ?? result : result;
11093
12359
  const tips = isCommandResult2(result) ? result.tips : result && typeof result === "object" ? result.tips : void 0;
11094
12360
  if (mode === "json" || mode === "yaml") {
11095
12361
  const finalOutput = {
11096
12362
  data: outputData
11097
12363
  };
12364
+ if (injectedViewerUrl) {
12365
+ finalOutput.viewerUrl = injectedViewerUrl;
12366
+ }
12367
+ if (tips?.length) {
12368
+ finalOutput.tips = tips;
12369
+ }
11098
12370
  if (hookOutputs.length > 0) {
11099
12371
  finalOutput.hooks = hookOutputs;
11100
12372
  }
11101
12373
  console.log(outputFormatter2.format(finalOutput, { mode, color: false, emoji: false }));
11102
12374
  if (tips?.length) {
11103
- for (const tip of tips) console.error(`\u{1F4A1} ${tip}`);
12375
+ for (const tip of tips) console.error(`\u{1F4A1} ${typeof tip === "string" ? tip : tip.message}`);
11104
12376
  }
11105
12377
  } else {
11106
12378
  console.log(outputFormatter2.format(outputData, { mode: "text", color: true, emoji: true }));
11107
12379
  if (tips?.length) {
11108
- for (const tip of tips) console.log(` \u{1F4A1} ${tip}`);
12380
+ for (const tip of tips) console.log(` \u{1F4A1} ${typeof tip === "string" ? tip : tip.message}`);
11109
12381
  }
11110
12382
  if (hookOutputs.length > 0) {
11111
12383
  for (const ho of hookOutputs) {
@@ -11283,7 +12555,7 @@ async function main() {
11283
12555
  const command = process.argv[2];
11284
12556
  const isLongRunning = command === "preview" || command === "serve";
11285
12557
  if (!isLongRunning) {
11286
- const { ensureProcessCanExit } = await import("./browser-GWBH6OJK.js");
12558
+ const { ensureProcessCanExit } = await import("./browser-R56O3CW6.js");
11287
12559
  await ensureProcessCanExit().catch(() => {
11288
12560
  });
11289
12561
  process.exit(exitCode);