@xbrowser/cli 1.0.0 → 1.0.3

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 (48) hide show
  1. package/README.md +17 -26
  2. package/dist/{browser-DSVV4GHS.js → browser-5CTOA2WS.js} +4 -3
  3. package/dist/{browser-53KUFEEM.js → browser-ITLZZDHJ.js} +5 -5
  4. package/dist/{browser-GURRY444.js → browser-IUJXXNBT.js} +6 -3
  5. package/dist/{cdp-driver-MNPR3HZH.js → cdp-driver-4X3DK6PS.js} +339 -59
  6. package/dist/{cdp-driver-SSXUGXP6.js → cdp-driver-D6WMSMWX.js} +4 -3
  7. package/dist/chunk-2SVQTI2O.js +2794 -0
  8. package/dist/{chunk-IDVD44ED.js → chunk-6WOSXSCQ.js} +23 -7
  9. package/dist/{chunk-ZZ2TFWIV.js → chunk-ABXMBNQ6.js} +1 -1
  10. package/dist/{chunk-2MFXKN32.js → chunk-ACFE6PKF.js} +1013 -119
  11. package/dist/chunk-AMI64BSD.js +268 -0
  12. package/dist/{chunk-E4O5ZU3H.js → chunk-DKWR54XQ.js} +412 -98
  13. package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
  14. package/dist/chunk-GDKLH7ZY.js +8 -0
  15. package/dist/chunk-KFQGP6VL.js +33 -0
  16. package/dist/{chunk-2BQZIT3S.js → chunk-LRBSUKUZ.js} +85 -2497
  17. package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
  18. package/dist/{chunk-42RPMJ76.js → chunk-N2JFPWMI.js} +342 -60
  19. package/dist/chunk-OZKD3W4X.js +417 -0
  20. package/dist/{chunk-T4J4C2NZ.js → chunk-TNEN6VQ2.js} +17 -4
  21. package/dist/{chunk-YKOHDEFV.js → chunk-TWWOIJM7.js} +74 -38
  22. package/dist/chunk-WJRE55TN.js +83 -0
  23. package/dist/cli.js +1558 -1122
  24. package/dist/{convert-EGFYNICZ.js → convert-LB3GJTLR.js} +3 -3
  25. package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
  26. package/dist/{daemon-client-YAVQ343A.js → daemon-client-3JOKX2L2.js} +3 -2
  27. package/dist/{daemon-client-3VM7VU7O.js → daemon-client-DIEHGP5B.js} +28 -74
  28. package/dist/daemon-main.js +2296 -1722
  29. package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
  30. package/dist/{extract-L2IW3IUB.js → extract-BSYBM4MR.js} +1 -1
  31. package/dist/{filter-HC4RA7JY.js → filter-KCFO4RSV.js} +1 -1
  32. package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
  33. package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
  34. package/dist/index.d.ts +166 -109
  35. package/dist/index.js +2668 -1742
  36. package/dist/launcher-L2JNDB2H.js +20 -0
  37. package/dist/{launcher-KA7J32K5.js → launcher-OZXJQPNG.js} +1 -1
  38. package/dist/{network-store-66A2RATI.js → network-store-XGZ25FFC.js} +1 -1
  39. package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
  40. package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
  41. package/dist/{proxy-WKGUCH2C.js → proxy-C6CK3UH5.js} +2 -2
  42. package/dist/session-recorder-RTDGURIJ.js +8 -0
  43. package/dist/session-recorder-YI7YYM36.js +7 -0
  44. package/dist/session-replayer-MY27H4DX.js +276 -0
  45. package/dist/site-knowledge-SYC6VCDB.js +23 -0
  46. package/package.json +5 -4
  47. package/dist/screenshot-CWAWMXVA.js +0 -28
  48. package/dist/session-recorder-MA75PKTQ.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-2MFXKN32.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,8 +25,8 @@ import {
17
25
  resolveLaunchOpts,
18
26
  saveSessionDiskMeta,
19
27
  setActivePage
20
- } from "./chunk-2BQZIT3S.js";
21
- import "./chunk-T4J4C2NZ.js";
28
+ } from "./chunk-DKWR54XQ.js";
29
+ import "./chunk-TNEN6VQ2.js";
22
30
  import {
23
31
  forwardCommandLog,
24
32
  forwardNetworkAnalyze,
@@ -39,7 +47,6 @@ import {
39
47
  forwardRecordSummary,
40
48
  forwardReplay,
41
49
  forwardSessionClose,
42
- forwardSessionCreate,
43
50
  forwardSessionList,
44
51
  forwardViewerCheckSelector,
45
52
  getDaemonConfig,
@@ -47,11 +54,16 @@ import {
47
54
  killAllDaemonProcesses,
48
55
  startDaemonProcess,
49
56
  stopDaemonProcess
50
- } from "./chunk-IDVD44ED.js";
51
- import "./chunk-3RG5ZIWI.js";
57
+ } from "./chunk-6WOSXSCQ.js";
58
+ import {
59
+ errMsg
60
+ } from "./chunk-GDKLH7ZY.js";
61
+ import {
62
+ __require
63
+ } from "./chunk-KFQGP6VL.js";
52
64
 
53
65
  // src/router.ts
54
- import { parseArgs, outputFormatter as outputFormatter2, isCommandResult as isCommandResult2, helpGenerator as helpGenerator2 } from "@dyyz1993/xcli-core";
66
+ import { parseArgs, outputFormatter as outputFormatter2, isCommandResult as isCommandResult2, helpGenerator as helpGenerator2, TipCollector as TipCollector3, normalizeTips as normalizeTips7, tip as makeTip } from "@dyyz1993/xcli-core";
55
67
 
56
68
  // src/utils/positional-params.ts
57
69
  import { unquote } from "@dyyz1993/xcli-core";
@@ -163,6 +175,11 @@ function parsePluginParams(args, schema, base = {}) {
163
175
  return result;
164
176
  }
165
177
 
178
+ // src/utils/zod-internal.ts
179
+ function asZodSchema(value) {
180
+ return value;
181
+ }
182
+
166
183
  // src/version.ts
167
184
  import { createRequire } from "module";
168
185
  var require2 = createRequire(import.meta.url);
@@ -171,15 +188,79 @@ var version = pkg.version;
171
188
 
172
189
  // src/executor.ts
173
190
  import {
174
- ok as ok26,
191
+ ok as ok25,
175
192
  fail as fail7,
176
193
  isCommandResult,
194
+ CompositeStorage as CompositeStorage2,
195
+ TipCollector as TipCollector2,
196
+ normalizeTips as normalizeTips6,
177
197
  configureArchiveStore,
178
198
  appendCommandToArchive,
179
199
  checkGuard,
180
- PluginStorage
200
+ unquote as unquote2
181
201
  } from "@dyyz1993/xcli-core";
182
202
 
203
+ // src/utils/stub-context.ts
204
+ import { TipCollector, CompositeStorage } from "@dyyz1993/xcli-core";
205
+ var CONFIG_DIR = __require("path").join(__require("os").homedir(), ".xbrowser");
206
+ var NoopSiteInstance = class {
207
+ name = "stub";
208
+ url = "";
209
+ config = { name: "stub" };
210
+ command() {
211
+ return this;
212
+ }
213
+ group() {
214
+ return this;
215
+ }
216
+ login() {
217
+ return this;
218
+ }
219
+ logout() {
220
+ return this;
221
+ }
222
+ async isLoggedIn() {
223
+ return true;
224
+ }
225
+ async requireLogin() {
226
+ }
227
+ getStorage() {
228
+ return new CompositeStorage("stub", CONFIG_DIR, "xbrowser");
229
+ }
230
+ getAllCommands() {
231
+ return [];
232
+ }
233
+ getCommand() {
234
+ return null;
235
+ }
236
+ getOriginalHandler() {
237
+ return void 0;
238
+ }
239
+ async executeLogin() {
240
+ }
241
+ async executeLogout() {
242
+ }
243
+ async restoreLogin() {
244
+ return false;
245
+ }
246
+ };
247
+ function createStubContext(pluginName) {
248
+ return {
249
+ args: [],
250
+ options: {},
251
+ cwd: process.cwd(),
252
+ storage: new CompositeStorage(pluginName, CONFIG_DIR, "xbrowser"),
253
+ output: { mode: "text", showTips: false, color: false, emoji: false },
254
+ error: (msg) => {
255
+ throw new Error(msg);
256
+ },
257
+ config: {},
258
+ site: new NoopSiteInstance(),
259
+ cliName: "xbrowser",
260
+ tips: new TipCollector()
261
+ };
262
+ }
263
+
183
264
  // src/commands/navigation.ts
184
265
  import { z } from "zod";
185
266
  import { ok } from "@dyyz1993/xcli-core";
@@ -227,7 +308,7 @@ async function detectSsr(page) {
227
308
  try {
228
309
  const result = await page.evaluate((vars) => {
229
310
  for (const varName of vars) {
230
- const value = window[varName];
311
+ const value = Reflect.get(window, varName);
231
312
  if (value != null && typeof value === "object") {
232
313
  const keys = Object.keys(value).slice(0, 10);
233
314
  return { variable: varName, keys };
@@ -324,10 +405,18 @@ var urlCommand = registerCommand({
324
405
  return ok({ url: ctx.page.url() });
325
406
  }
326
407
  });
408
+ registerCommand({
409
+ name: "open",
410
+ description: "Navigate to URL (alias for goto)",
411
+ scope: "page",
412
+ parameters: gotoCommand.parameters,
413
+ result: gotoCommand.result,
414
+ handler: gotoCommand.handler
415
+ });
327
416
 
328
417
  // src/commands/interaction.ts
329
418
  import { z as z2 } from "zod";
330
- import { ok as ok2 } from "@dyyz1993/xcli-core";
419
+ import { ok as ok2, normalizeTips } from "@dyyz1993/xcli-core";
331
420
 
332
421
  // src/lib/captcha.ts
333
422
  var CAPTCHA_SELECTORS = {
@@ -395,15 +484,15 @@ var clickCommand = registerCommand({
395
484
  let detectedNewPage;
396
485
  let cleanup;
397
486
  if (ctx.browserContext?.on) {
398
- const pagePromise = new Promise((resolve16) => {
487
+ const pagePromise = new Promise((resolve10) => {
399
488
  const timer = setTimeout(() => {
400
489
  ctx.browserContext.off("page", handler);
401
- resolve16(void 0);
490
+ resolve10(void 0);
402
491
  }, 3e3);
403
492
  const handler = (page2) => {
404
493
  clearTimeout(timer);
405
494
  ctx.browserContext.off("page", handler);
406
- resolve16(page2);
495
+ resolve10(page2);
407
496
  };
408
497
  ctx.browserContext.on("page", handler);
409
498
  });
@@ -438,7 +527,7 @@ var clickCommand = registerCommand({
438
527
  selector: p.selector,
439
528
  newTab: { url: newUrl, title: newTitle }
440
529
  });
441
- result.tips = [`\u65B0 Tab \u5DF2\u6253\u5F00: ${newTitle ? newTitle + " \u2014 " : ""}${newUrl}`];
530
+ result.tips = normalizeTips([`\u65B0 Tab \u5DF2\u6253\u5F00: ${newTitle ? newTitle + " \u2014 " : ""}${newUrl}`]);
442
531
  return result;
443
532
  }
444
533
  const captchaInfo = await detectCaptcha(page);
@@ -452,7 +541,7 @@ var clickCommand = registerCommand({
452
541
  tips.push(solved ? "\u2705 CAPTCHA solved!" : "\u274C CAPTCHA timeout");
453
542
  }
454
543
  const result = ok2({ selector: p.selector, captcha: captchaInfo });
455
- result.tips = tips;
544
+ result.tips = normalizeTips(tips);
456
545
  return result;
457
546
  }
458
547
  return ok2({ selector: p.selector });
@@ -787,7 +876,7 @@ var mouseCommand = registerCommand({
787
876
 
788
877
  // src/commands/evaluate.ts
789
878
  import { z as z7 } from "zod";
790
- import { ok as ok7 } from "@dyyz1993/xcli-core";
879
+ import { ok as ok7, normalizeTips as normalizeTips2 } from "@dyyz1993/xcli-core";
791
880
  var evaluateCommand = registerCommand({
792
881
  name: "eval",
793
882
  description: "Evaluate JavaScript expression in the browser",
@@ -808,10 +897,10 @@ var evaluateCommand = registerCommand({
808
897
  const result = await ctx.page.evaluate(p.expression);
809
898
  const response = ok7({ result });
810
899
  if (decision && decision.severity === "danger") {
811
- response.tips = [
900
+ response.tips = normalizeTips2([
812
901
  `\u26A0\uFE0F CDP Firewall: ${decision.reason}`,
813
902
  `\u{1F4A1} Fix: ${decision.suggestion}`
814
- ];
903
+ ]);
815
904
  }
816
905
  return response;
817
906
  }
@@ -934,7 +1023,19 @@ var clearLocalStorageCommand = registerCommand({
934
1023
  // src/commands/screenshot.ts
935
1024
  import { z as z9 } from "zod";
936
1025
  import { ok as ok9 } from "@dyyz1993/xcli-core";
937
- import { writeFileSync } from "fs";
1026
+ import { writeFileSync, mkdirSync } from "fs";
1027
+ import { join } from "path";
1028
+ import { homedir } from "os";
1029
+ var SCREENSHOTS_DIR = join(homedir(), ".xbrowser", "screenshots");
1030
+ function ensureScreenshotsDir() {
1031
+ mkdirSync(SCREENSHOTS_DIR, { recursive: true });
1032
+ }
1033
+ function generateScreenshotPath(format) {
1034
+ const timestamp = Date.now();
1035
+ const random = Math.random().toString(36).slice(2, 8);
1036
+ const ext = format === "jpeg" ? "jpg" : "png";
1037
+ return join(SCREENSHOTS_DIR, `screenshot-${timestamp}-${random}.${ext}`);
1038
+ }
938
1039
  var screenshotCommand = registerCommand({
939
1040
  name: "screenshot",
940
1041
  description: "Take a screenshot of the page or element",
@@ -944,7 +1045,8 @@ var screenshotCommand = registerCommand({
944
1045
  selector: z9.string().optional(),
945
1046
  type: z9.enum(["png", "jpeg"]).optional(),
946
1047
  fullPage: z9.boolean().optional(),
947
- output: z9.string().optional()
1048
+ output: z9.string().optional(),
1049
+ base64: z9.boolean().optional().describe("Return base64 data instead of file path")
948
1050
  }),
949
1051
  result: z9.union([
950
1052
  z9.object({
@@ -959,8 +1061,9 @@ var screenshotCommand = registerCommand({
959
1061
  })
960
1062
  ]),
961
1063
  handler: async (p, ctx) => {
1064
+ const format = p.type || "png";
962
1065
  const options = {
963
- type: p.type || "png",
1066
+ type: format,
964
1067
  fullPage: p.fullPage || false
965
1068
  };
966
1069
  let buffer;
@@ -973,13 +1076,23 @@ var screenshotCommand = registerCommand({
973
1076
  writeFileSync(p.output, buffer);
974
1077
  return ok9({
975
1078
  output: p.output,
976
- format: p.type || "png",
1079
+ format,
1080
+ size: buffer.length
1081
+ });
1082
+ }
1083
+ if (p.base64) {
1084
+ return ok9({
1085
+ data: buffer.toString("base64"),
1086
+ format,
977
1087
  size: buffer.length
978
1088
  });
979
1089
  }
1090
+ ensureScreenshotsDir();
1091
+ const screenshotPath = generateScreenshotPath(format);
1092
+ writeFileSync(screenshotPath, buffer);
980
1093
  return ok9({
981
- data: buffer.toString("base64"),
982
- format: p.type || "png",
1094
+ output: screenshotPath,
1095
+ format,
983
1096
  size: buffer.length
984
1097
  });
985
1098
  }
@@ -1161,7 +1274,7 @@ var consoleCheckCommand = registerCommand({
1161
1274
  await page.goto(p.url, { waitUntil: "domcontentloaded" });
1162
1275
  }
1163
1276
  const messages = await page.evaluate((args) => {
1164
- return new Promise((resolve16) => {
1277
+ return new Promise((resolve10) => {
1165
1278
  const collected = [];
1166
1279
  const originalConsole = {
1167
1280
  log: console.log,
@@ -1228,7 +1341,7 @@ ${a.stack || ""}`;
1228
1341
  console.warn = originalConsole.warn;
1229
1342
  console.error = originalConsole.error;
1230
1343
  console.info = originalConsole.info;
1231
- resolve16(collected);
1344
+ resolve10(collected);
1232
1345
  }, args.duration);
1233
1346
  });
1234
1347
  }, { duration: p.duration });
@@ -1544,7 +1657,7 @@ var healthCheckCommand = registerCommand({
1544
1657
  issues.push({
1545
1658
  severity: "error",
1546
1659
  category: "links",
1547
- message: `Broken link (fetch error): ${href} \u2014 ${err.message || "unknown"}`
1660
+ message: `Broken link (fetch error): ${href} \u2014 ${errMsg(err) || "unknown"}`
1548
1661
  });
1549
1662
  }
1550
1663
  }
@@ -1577,6 +1690,19 @@ var healthCheckCommand = registerCommand({
1577
1690
  // src/commands/actions.ts
1578
1691
  import { z as z14 } from "zod";
1579
1692
  import { ok as ok14 } from "@dyyz1993/xcli-core";
1693
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
1694
+ import { join as join2 } from "path";
1695
+ import { homedir as homedir2 } from "os";
1696
+ var SCREENSHOTS_DIR2 = join2(homedir2(), ".xbrowser", "screenshots");
1697
+ function ensureScreenshotsDir2() {
1698
+ mkdirSync2(SCREENSHOTS_DIR2, { recursive: true });
1699
+ }
1700
+ function generateScreenshotPath2(format) {
1701
+ const timestamp = Date.now();
1702
+ const random = Math.random().toString(36).slice(2, 8);
1703
+ const ext = format === "jpeg" ? "jpg" : "png";
1704
+ return join2(SCREENSHOTS_DIR2, `screenshot-${timestamp}-${random}.${ext}`);
1705
+ }
1580
1706
  var waitActionSchema = z14.object({
1581
1707
  type: z14.literal("wait"),
1582
1708
  milliseconds: z14.number().positive().optional(),
@@ -1594,7 +1720,8 @@ var screenshotActionSchema = z14.object({
1594
1720
  type: z14.literal("screenshot"),
1595
1721
  fullPage: z14.boolean().optional(),
1596
1722
  quality: z14.number().min(1).max(100).optional(),
1597
- viewport: z14.object({ width: z14.number().int().positive(), height: z14.number().int().positive() }).optional()
1723
+ viewport: z14.object({ width: z14.number().int().positive(), height: z14.number().int().positive() }).optional(),
1724
+ base64: z14.boolean().optional().describe("Return base64 data instead of file path")
1598
1725
  });
1599
1726
  var writeActionSchema = z14.object({
1600
1727
  type: z14.literal("write"),
@@ -1640,7 +1767,7 @@ async function executeAction(page, action) {
1640
1767
  if (action.selector) {
1641
1768
  await page.waitForSelector(action.selector, { timeout: 3e4 });
1642
1769
  } else if (action.milliseconds) {
1643
- await new Promise((resolve16) => setTimeout(resolve16, action.milliseconds));
1770
+ await new Promise((resolve10) => setTimeout(resolve10, action.milliseconds));
1644
1771
  } else {
1645
1772
  throw new Error("wait action requires either milliseconds or selector");
1646
1773
  }
@@ -1661,7 +1788,13 @@ async function executeAction(page, action) {
1661
1788
  quality: action.quality ?? 80,
1662
1789
  ...action.viewport ? { clip: { x: 0, y: 0, ...action.viewport } } : {}
1663
1790
  });
1664
- return { type: "screenshot", result: buf.toString("base64") };
1791
+ if (action.base64) {
1792
+ return { type: "screenshot", result: buf.toString("base64"), base64: true };
1793
+ }
1794
+ ensureScreenshotsDir2();
1795
+ const screenshotPath = generateScreenshotPath2("jpg");
1796
+ writeFileSync2(screenshotPath, buf);
1797
+ return { type: "screenshot", result: screenshotPath };
1665
1798
  }
1666
1799
  case "write":
1667
1800
  await page.keyboard.type(action.text);
@@ -1723,8 +1856,8 @@ var actionsCommand = registerCommand({
1723
1856
  results.push(result);
1724
1857
  }
1725
1858
  })();
1726
- const timeoutPromise = new Promise((resolve16) => {
1727
- setTimeout(resolve16, timeoutMs);
1859
+ const timeoutPromise = new Promise((resolve10) => {
1860
+ setTimeout(resolve10, timeoutMs);
1728
1861
  });
1729
1862
  await Promise.race([executionPromise, timeoutPromise]);
1730
1863
  const title = await ctx.page.title();
@@ -2398,11 +2531,11 @@ async function navigateForMap(page, url, timeout = 15e3) {
2398
2531
  }
2399
2532
  async function extractPageLinks(page, baseUrl) {
2400
2533
  await navigateForMap(page, baseUrl);
2401
- await new Promise((resolve16) => setTimeout(resolve16, 2e3));
2534
+ await new Promise((resolve10) => setTimeout(resolve10, 2e3));
2402
2535
  await page.evaluate(() => {
2403
2536
  window.scrollTo(0, document.body.scrollHeight);
2404
2537
  });
2405
- await new Promise((resolve16) => setTimeout(resolve16, 1e3));
2538
+ await new Promise((resolve10) => setTimeout(resolve10, 1e3));
2406
2539
  const origin = new URL(baseUrl).origin;
2407
2540
  const rawLinks = await page.evaluate((evalOrigin) => {
2408
2541
  return Array.from(document.querySelectorAll("a[href]")).map((a) => {
@@ -3828,7 +3961,7 @@ var ENGINE_KEY_ENUM = z20.enum(ALL_ENGINE_KEYS);
3828
3961
 
3829
3962
  // src/commands/snapshot.ts
3830
3963
  import { z as z21 } from "zod";
3831
- import { ok as ok20, fail as fail4 } from "@dyyz1993/xcli-core";
3964
+ import { ok as ok20, fail as fail4, normalizeTips as normalizeTips3 } from "@dyyz1993/xcli-core";
3832
3965
 
3833
3966
  // src/runtime/ref-store.ts
3834
3967
  var sessions = /* @__PURE__ */ new Map();
@@ -4069,9 +4202,9 @@ async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
4069
4202
  }
4070
4203
 
4071
4204
  // src/utils/site-semantics.ts
4072
- import { writeFileSync as writeFileSync2, mkdirSync, existsSync, readFileSync } from "fs";
4073
- import { join, dirname } from "path";
4074
- import { homedir } from "os";
4205
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync, readFileSync } from "fs";
4206
+ import { join as join3, dirname } from "path";
4207
+ import { homedir as homedir3 } from "os";
4075
4208
  import { stringify, parse } from "yaml";
4076
4209
  import { execFile } from "child_process";
4077
4210
  var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
@@ -4148,10 +4281,10 @@ function inferAction(role, label) {
4148
4281
  return {};
4149
4282
  }
4150
4283
  function getSemanticsDir() {
4151
- return join(homedir(), ".xbrowser", "site-semantics");
4284
+ return join3(homedir3(), ".xbrowser", "site-semantics");
4152
4285
  }
4153
4286
  function getSemanticsPath(domain) {
4154
- return join(getSemanticsDir(), `${domain}.yaml`);
4287
+ return join3(getSemanticsDir(), `${domain}.yaml`);
4155
4288
  }
4156
4289
  function extractDomain2(url) {
4157
4290
  try {
@@ -4188,9 +4321,9 @@ function saveSemantics(domain, pagePath, url, elements) {
4188
4321
  }
4189
4322
  site.updated_at = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4190
4323
  if (!existsSync(dir)) {
4191
- mkdirSync(dir, { recursive: true });
4324
+ mkdirSync3(dir, { recursive: true });
4192
4325
  }
4193
- writeFileSync2(filePath, stringify(site, { lineWidth: 0 }));
4326
+ writeFileSync3(filePath, stringify(site, { lineWidth: 0 }));
4194
4327
  }
4195
4328
  function loadSemantics(domain) {
4196
4329
  const filePath = getSemanticsPath(domain);
@@ -4260,25 +4393,25 @@ aria snapshot\uFF1A
4260
4393
  async function analyzeWithLLM(ariaSnapshot) {
4261
4394
  const piBin = process.env.PI_CLI_PATH || "pi";
4262
4395
  const prompt = LLM_PROMPT.replace("{snapshot}", ariaSnapshot.slice(0, 4e3));
4263
- return new Promise((resolve16) => {
4396
+ return new Promise((resolve10) => {
4264
4397
  execFile(
4265
4398
  piBin,
4266
4399
  ["--provider", LLM_PROVIDER, "--model", LLM_MODEL, prompt],
4267
4400
  { timeout: LLM_TIMEOUT_MS, maxBuffer: 1024 * 1024 },
4268
4401
  (err, stdout, _stderr) => {
4269
4402
  if (err) {
4270
- resolve16(null);
4403
+ resolve10(null);
4271
4404
  return;
4272
4405
  }
4273
4406
  const output = (stdout || "").trim();
4274
4407
  if (!output) {
4275
- resolve16(null);
4408
+ resolve10(null);
4276
4409
  return;
4277
4410
  }
4278
4411
  try {
4279
4412
  const parsed = parse(output);
4280
4413
  if (!parsed || typeof parsed !== "object") {
4281
- resolve16(null);
4414
+ resolve10(null);
4282
4415
  return;
4283
4416
  }
4284
4417
  const elements = {};
@@ -4293,9 +4426,9 @@ async function analyzeWithLLM(ariaSnapshot) {
4293
4426
  };
4294
4427
  }
4295
4428
  }
4296
- resolve16(Object.keys(elements).length > 0 ? elements : null);
4429
+ resolve10(Object.keys(elements).length > 0 ? elements : null);
4297
4430
  } catch {
4298
- resolve16(null);
4431
+ resolve10(null);
4299
4432
  }
4300
4433
  }
4301
4434
  );
@@ -4623,7 +4756,7 @@ async function actOnPage(page, sessionId, input) {
4623
4756
  ref: normalizedRef,
4624
4757
  success: false,
4625
4758
  reason: "browser_error",
4626
- message: error.message,
4759
+ message: errMsg(error),
4627
4760
  stale,
4628
4761
  screenHash: hash,
4629
4762
  target: refMatch?.target
@@ -4650,7 +4783,7 @@ async function pollUntil(timeout, pollInterval, predicate) {
4650
4783
  const startedAt = Date.now();
4651
4784
  while (Date.now() - startedAt <= timeout) {
4652
4785
  if (await predicate()) return true;
4653
- await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
4786
+ await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
4654
4787
  }
4655
4788
  return false;
4656
4789
  }
@@ -4707,7 +4840,7 @@ async function waitForPage(page, input) {
4707
4840
  matched: input.selector ? "selector" : input.text ? "text" : input.url ? "url" : input.load ? "load" : input.fn ? "fn" : "screenHashChanged",
4708
4841
  timeout,
4709
4842
  elapsed: Date.now() - startedAt,
4710
- message: error.message
4843
+ message: errMsg(error)
4711
4844
  };
4712
4845
  }
4713
4846
  return {
@@ -4732,7 +4865,7 @@ var snapshotCommand = registerCommand({
4732
4865
  interactive: z21.boolean().optional().default(false).describe("Return interactive agent refs only"),
4733
4866
  interactiveOnly: z21.boolean().optional().default(false).describe("Alias for interactive"),
4734
4867
  i: z21.boolean().optional().default(false).describe("Short alias for interactive"),
4735
- compact: z21.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
4868
+ compact: z21.boolean().optional().default(false).describe("Include compact xbrowser style snapshot text"),
4736
4869
  c: z21.boolean().optional().default(false).describe("Short alias for compact"),
4737
4870
  selectors: z21.boolean().optional().default(false).describe("Include ref to CSS selector map"),
4738
4871
  all: z21.boolean().optional().default(false).describe("Include hidden interactive targets when using interactive snapshot")
@@ -4756,15 +4889,15 @@ var snapshotCommand = registerCommand({
4756
4889
  if (p.compact || p.c || p.interactive || p.interactiveOnly || p.i) {
4757
4890
  observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
4758
4891
  }
4759
- return ok20(observation, [
4892
+ return ok20(observation, normalizeTips3([
4760
4893
  `refs refreshed for ${observation.targets.length} targets; use click @e1 or fill @e2 "text"`
4761
- ]);
4894
+ ]));
4762
4895
  }
4763
4896
  if (p.type === "aria") {
4764
4897
  const aria = await captureAriaSnapshot(page, p.selector, p.depth);
4765
4898
  const tips = await buildRefTips(page, aria);
4766
4899
  persistSemantics(url, aria);
4767
- return ok20({ url, title, aria }, tips);
4900
+ return ok20({ url, title, aria }, normalizeTips3(tips));
4768
4901
  }
4769
4902
  if (p.type === "text") {
4770
4903
  const text = await captureTextSnapshot(page, p.selector);
@@ -4782,7 +4915,7 @@ var snapshotCommand = registerCommand({
4782
4915
  ]);
4783
4916
  const tips = await buildRefTips(page, aria);
4784
4917
  persistSemantics(url, aria);
4785
- return ok20({ url, title, aria, text, dom }, tips);
4918
+ return ok20({ url, title, aria, text, dom }, normalizeTips3(tips));
4786
4919
  }
4787
4920
  return fail4(`Unknown snapshot type: ${p.type}`);
4788
4921
  }
@@ -4862,7 +4995,7 @@ async function captureDomSnapshot(page, selector, maxDepth) {
4862
4995
 
4863
4996
  // src/commands/agent.ts
4864
4997
  import { z as z22 } from "zod";
4865
- import { ok as ok21 } from "@dyyz1993/xcli-core";
4998
+ import { ok as ok21, normalizeTips as normalizeTips4 } from "@dyyz1993/xcli-core";
4866
4999
  var observeCommand = registerCommand({
4867
5000
  name: "observe",
4868
5001
  description: "Observe the current page as structured agent targets with session refs",
@@ -4870,9 +5003,14 @@ var observeCommand = registerCommand({
4870
5003
  parameters: z22.object({
4871
5004
  includeHidden: z22.boolean().optional().default(false).describe("Include hidden elements in the target list"),
4872
5005
  limit: z22.number().int().positive().max(300).optional().default(80).describe("Maximum number of targets to return"),
4873
- compact: z22.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
5006
+ compact: z22.boolean().optional().default(false).describe("Include compact xbrowser style snapshot text"),
4874
5007
  selectors: z22.boolean().optional().default(false).describe("Include ref to stable CSS selector map")
4875
5008
  }),
5009
+ result: z22.object({
5010
+ targets: z22.array(z22.record(z22.unknown())),
5011
+ selectors: z22.record(z22.unknown()).optional(),
5012
+ compact: z22.string().optional()
5013
+ }).passthrough(),
4876
5014
  handler: async (p, ctx) => {
4877
5015
  const observation = await observePage(ctx.page, ctx.sessionId, {
4878
5016
  includeHidden: p.includeHidden,
@@ -4880,9 +5018,9 @@ var observeCommand = registerCommand({
4880
5018
  });
4881
5019
  if (p.selectors) observation.selectors = buildSelectorMap(observation);
4882
5020
  if (p.compact) observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
4883
- return ok21(observation, [
5021
+ return ok21(observation, normalizeTips4([
4884
5022
  `refs refreshed for ${observation.targets.length} targets; use act --ref @e1 --action click or click @e1`
4885
- ]);
5023
+ ]));
4886
5024
  }
4887
5025
  });
4888
5026
  var actCommand = registerCommand({
@@ -4908,10 +5046,10 @@ var actCommand = registerCommand({
4908
5046
  success: false,
4909
5047
  data: result,
4910
5048
  message: result.message || result.reason || "Action failed",
4911
- tips: result.stale ? ["run observe again to refresh refs"] : []
5049
+ tips: normalizeTips4(result.stale ? ["run observe again to refresh refs"] : [])
4912
5050
  };
4913
5051
  }
4914
- return ok21(result, result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []);
5052
+ return ok21(result, normalizeTips4(result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []));
4915
5053
  }
4916
5054
  });
4917
5055
  var waitForCommand = registerCommand({
@@ -5087,7 +5225,8 @@ import { ok as ok23 } from "@dyyz1993/xcli-core";
5087
5225
  import { readFileSync as readFileSync2 } from "fs";
5088
5226
 
5089
5227
  // src/chain-parser.ts
5090
- import { unquote as unquote2 } from "@dyyz1993/xcli-core";
5228
+ import { splitCommand, parseCommandArgs } from "@dyyz1993/xcli-core";
5229
+ import { registerCommandDefinition } from "@dyyz1993/xcli-core";
5091
5230
  function parseCommandChain(input, options) {
5092
5231
  const result = [];
5093
5232
  let currentPipeline = [];
@@ -5178,149 +5317,46 @@ function isSpaceAround(input, pos, tokenLen) {
5178
5317
  const after = pos + tokenLen < input.length && input[pos + tokenLen] === " ";
5179
5318
  return before && after;
5180
5319
  }
5181
- function splitCommand(cmdStr) {
5182
- const parts = [];
5183
- let current = "";
5184
- let inQuote = null;
5185
- for (let i = 0; i < cmdStr.length; i++) {
5186
- const char = cmdStr[i];
5187
- if (!inQuote && (char === "'" || char === '"')) {
5188
- inQuote = char;
5189
- current += char;
5190
- continue;
5191
- }
5192
- if (inQuote && char === inQuote) {
5193
- inQuote = null;
5194
- current += char;
5195
- continue;
5196
- }
5197
- if (!inQuote && /\s/.test(char)) {
5198
- if (current.trim()) {
5199
- parts.push(current.trim());
5200
- current = "";
5201
- }
5202
- continue;
5203
- }
5204
- current += char;
5205
- }
5206
- if (current.trim()) {
5207
- parts.push(current.trim());
5208
- }
5209
- return parts;
5210
- }
5211
- var SHORT_FLAG_MAP = {
5212
- s: "selector",
5213
- v: "value"
5214
- };
5215
- function coerceValue2(raw) {
5216
- const v = unquote2(raw);
5217
- if (v === "true") return true;
5218
- if (v === "false") return false;
5219
- if (/^\d+$/.test(v)) return parseInt(v, 10);
5220
- if (/^\d+\.\d+$/.test(v)) return parseFloat(v);
5221
- if (v.startsWith("[") || v.startsWith("{")) {
5222
- try {
5223
- return JSON.parse(v);
5224
- } catch {
5225
- return v;
5226
- }
5227
- }
5228
- return v;
5229
- }
5230
- function parseCommandArgs(name, args) {
5231
- const definitions = getCommandDefinitions();
5232
- const def = definitions[name];
5233
- const positionalKeys = def ? def.positional : [];
5234
- const params = {};
5235
- let positionalIndex = 0;
5236
- for (let i = 0; i < args.length; i++) {
5237
- const raw = args[i];
5238
- const arg = unquote2(raw);
5239
- if (raw.startsWith("--")) {
5240
- const key = raw.slice(2);
5241
- const value = args[i + 1];
5242
- if (value && !value.startsWith("-")) {
5243
- params[key] = coerceValue2(value);
5244
- i++;
5245
- } else {
5246
- params[key] = true;
5247
- }
5248
- } else if (raw.startsWith("-") && raw.length === 2) {
5249
- const flag = raw[1];
5250
- const mappedKey = SHORT_FLAG_MAP[flag];
5251
- const value = args[i + 1];
5252
- if (mappedKey && value && !value.startsWith("-")) {
5253
- params[mappedKey] = coerceValue2(value);
5254
- i++;
5255
- } else if (value && !value.startsWith("-")) {
5256
- params[flag] = coerceValue2(value);
5257
- i++;
5258
- } else {
5259
- params[mappedKey || flag] = true;
5260
- }
5261
- } else {
5262
- if (positionalIndex < positionalKeys.length) {
5263
- const isLast = positionalIndex === positionalKeys.length - 1;
5264
- if (isLast && (name === "eval" || name === "find")) {
5265
- const remaining = args.slice(i).map(unquote2).join(" ");
5266
- params[positionalKeys[positionalIndex]] = remaining;
5267
- break;
5268
- }
5269
- params[positionalKeys[positionalIndex]] = arg;
5270
- positionalIndex++;
5271
- }
5272
- }
5273
- }
5274
- return { command: name, params };
5275
- }
5276
- var commandDefCache = {
5277
- goto: { positional: ["url"] },
5278
- click: { positional: ["selector"] },
5279
- fill: { positional: ["selector", "value"] },
5280
- type: { positional: ["selector", "text"] },
5281
- press: { positional: ["selector", "key"] },
5282
- select: { positional: ["selector", "value"] },
5283
- check: { positional: ["selector"] },
5284
- uncheck: { positional: ["selector"] },
5285
- hover: { positional: ["selector"] },
5286
- dblclick: { positional: ["selector"] },
5287
- wait: { positional: ["selector"] },
5288
- screenshot: { positional: [] },
5289
- eval: { positional: ["expression"] },
5290
- scroll: { positional: ["direction"] },
5291
- title: { positional: [] },
5292
- url: { positional: [] },
5293
- html: { positional: [] },
5294
- text: { positional: [] },
5295
- back: { positional: [] },
5296
- forward: { positional: [] },
5297
- refresh: { positional: [] },
5298
- console: { positional: [] },
5299
- network: { positional: [] },
5300
- perf: { positional: [] },
5301
- health: { positional: [] },
5302
- scrape: { positional: ["url"] },
5303
- structure: { positional: [] },
5304
- "get-cookies": { positional: [] },
5305
- "set-cookie": { positional: [] },
5306
- "clear-cookies": { positional: [] },
5307
- "get-local-storage": { positional: [] },
5308
- "set-local-storage": { positional: [] },
5309
- "clear-local-storage": { positional: [] },
5310
- "set-viewport": { positional: [] },
5311
- frames: { positional: [] },
5312
- frame: { positional: ["selector"] },
5313
- actions: { positional: ["url"] },
5314
- find: { positional: ["strategy", "value", "operation"] },
5315
- addinitscript: { positional: ["script"] },
5316
- tab: { positional: ["subcommand"] }
5317
- };
5318
- function getCommandDefinitions() {
5319
- return commandDefCache;
5320
- }
5321
- function registerCommandDefinition(name, positional) {
5322
- commandDefCache[name] = { positional };
5323
- }
5320
+ registerCommandDefinition("goto", ["url"]);
5321
+ registerCommandDefinition("click", ["selector"]);
5322
+ registerCommandDefinition("fill", ["selector", "value"]);
5323
+ registerCommandDefinition("type", ["selector", "text"]);
5324
+ registerCommandDefinition("press", ["selector", "key"]);
5325
+ registerCommandDefinition("select", ["selector", "value"]);
5326
+ registerCommandDefinition("check", ["selector"]);
5327
+ registerCommandDefinition("uncheck", ["selector"]);
5328
+ registerCommandDefinition("hover", ["selector"]);
5329
+ registerCommandDefinition("dblclick", ["selector"]);
5330
+ registerCommandDefinition("wait", ["selector"]);
5331
+ registerCommandDefinition("screenshot", []);
5332
+ registerCommandDefinition("eval", ["expression"]);
5333
+ registerCommandDefinition("scroll", ["direction"]);
5334
+ registerCommandDefinition("title", []);
5335
+ registerCommandDefinition("url", []);
5336
+ registerCommandDefinition("html", []);
5337
+ registerCommandDefinition("text", []);
5338
+ registerCommandDefinition("back", []);
5339
+ registerCommandDefinition("forward", []);
5340
+ registerCommandDefinition("refresh", []);
5341
+ registerCommandDefinition("console", []);
5342
+ registerCommandDefinition("network", []);
5343
+ registerCommandDefinition("perf", []);
5344
+ registerCommandDefinition("health", []);
5345
+ registerCommandDefinition("scrape", ["url"]);
5346
+ registerCommandDefinition("structure", []);
5347
+ registerCommandDefinition("get-cookies", []);
5348
+ registerCommandDefinition("set-cookie", []);
5349
+ registerCommandDefinition("clear-cookies", []);
5350
+ registerCommandDefinition("get-local-storage", []);
5351
+ registerCommandDefinition("set-local-storage", []);
5352
+ registerCommandDefinition("clear-local-storage", []);
5353
+ registerCommandDefinition("set-viewport", []);
5354
+ registerCommandDefinition("frames", []);
5355
+ registerCommandDefinition("frame", ["selector"]);
5356
+ registerCommandDefinition("actions", ["url"]);
5357
+ registerCommandDefinition("find", ["strategy", "value", "operation"]);
5358
+ registerCommandDefinition("addinitscript", ["script"]);
5359
+ registerCommandDefinition("tab", ["subcommand"]);
5324
5360
 
5325
5361
  // src/commands/addinitscript.ts
5326
5362
  var InitScriptParams = z24.object({
@@ -5350,12 +5386,12 @@ function resolveScriptContent(params) {
5350
5386
  }
5351
5387
  async function readStdin() {
5352
5388
  const { createReadStream } = await import("fs");
5353
- const { createInterface: createInterface2 } = await import("readline");
5354
- return new Promise((resolve16, reject) => {
5389
+ const { createInterface } = await import("readline");
5390
+ return new Promise((resolve10, reject) => {
5355
5391
  const lines = [];
5356
- const rl = createInterface2({ input: createReadStream("/dev/stdin") });
5392
+ const rl = createInterface({ input: createReadStream("/dev/stdin") });
5357
5393
  rl.on("line", (line) => lines.push(line));
5358
- rl.on("close", () => resolve16(lines.join("\n")));
5394
+ rl.on("close", () => resolve10(lines.join("\n")));
5359
5395
  rl.on("error", reject);
5360
5396
  });
5361
5397
  }
@@ -5364,6 +5400,15 @@ var addInitScriptCommand = registerCommand({
5364
5400
  description: "Add an initialization script that runs on every page load",
5365
5401
  scope: "page",
5366
5402
  parameters: InitScriptParams,
5403
+ result: z24.object({
5404
+ scripts: z24.array(z24.object({ name: z24.string(), size: z24.number(), preview: z24.string() })).optional(),
5405
+ removed: z24.string().optional(),
5406
+ existed: z24.boolean().optional(),
5407
+ error: z24.string().optional(),
5408
+ registered: z24.string().optional(),
5409
+ hint: z24.string().optional(),
5410
+ executedImmediately: z24.boolean().optional()
5411
+ }).passthrough(),
5367
5412
  handler: async (params, ctx) => {
5368
5413
  if (params.list) {
5369
5414
  const scripts = Array.from(registeredScripts.entries()).map(([n, content2]) => ({
@@ -5399,7 +5444,7 @@ registerCommandDefinition("addinitscript", ["script"]);
5399
5444
 
5400
5445
  // src/commands/find.ts
5401
5446
  import { z as z25 } from "zod";
5402
- import { ok as ok24, fail as fail6 } from "@dyyz1993/xcli-core";
5447
+ import { ok as ok24, fail as fail6, normalizeTips as normalizeTips5 } from "@dyyz1993/xcli-core";
5403
5448
  var actionSchema2 = z25.enum(["click", "fill", "type", "select", "hover", "check"]);
5404
5449
  var findCommand = registerCommand({
5405
5450
  name: "find",
@@ -5475,7 +5520,7 @@ var findCommand = registerCommand({
5475
5520
  });
5476
5521
  function okWithTips(data, tips) {
5477
5522
  const result = ok24(data);
5478
- if (tips.length > 0) result.tips = tips;
5523
+ if (tips.length > 0) result.tips = normalizeTips5(tips);
5479
5524
  return result;
5480
5525
  }
5481
5526
  function parseOperation(operation) {
@@ -5730,16 +5775,11 @@ async function detectWebdriverExposure(page) {
5730
5775
  try {
5731
5776
  const webdriver = await page.evaluate(() => {
5732
5777
  return {
5733
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5734
- webdriver: window.navigator?.webdriver,
5735
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5778
+ webdriver: navigator.webdriver,
5736
5779
  webdriverScriptFn: !!window.__webdriver_script_fn,
5737
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5738
5780
  webdriverEvaluate: !!window.__webdriver_evaluate,
5739
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5740
5781
  chrome: !!window.chrome,
5741
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5742
- permissions: window.navigator?.permissions
5782
+ permissions: navigator.permissions
5743
5783
  };
5744
5784
  }).catch(() => null);
5745
5785
  if (!webdriver) {
@@ -5819,377 +5859,23 @@ function formatDetectionMessage(result) {
5819
5859
  Action: ${action}`;
5820
5860
  }
5821
5861
 
5822
- // src/commands/promo.ts
5823
- import { z as z26 } from "zod";
5824
- import { ok as ok25 } from "@dyyz1993/xcli-core";
5825
- import { existsSync as existsSync2, readFileSync as readFileSync8 } from "fs";
5826
- import { resolve as resolve6 } from "path";
5827
-
5828
- // src/promo/devto.ts
5829
- import { readFileSync as readFileSync3 } from "fs";
5830
- import { resolve } from "path";
5831
- import { execSync } from "child_process";
5832
- var DEVTO_NEW_URL = "https://dev.to/new";
5833
- function ab(config) {
5834
- const parts = ["agent-browser"];
5835
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
5836
- if (config.session) parts.push("--session", config.session);
5837
- return parts.join(" ");
5838
- }
5839
- function extractTitleFromMarkdown(content) {
5840
- const match = content.match(/^#\s+(.+)$/m);
5841
- if (match) return match[1].trim();
5842
- const lines = content.split("\n").filter((l) => l.trim().length > 0);
5843
- return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
5844
- }
5845
- function stripTitleFromMarkdown(content) {
5846
- return content.replace(/^#\s+.+$\n?/m, "").trim();
5847
- }
5848
- async function publishToDevto(config) {
5849
- const cli = ab(config);
5850
- try {
5851
- const filePath = resolve(config.file);
5852
- const raw = readFileSync3(filePath, "utf-8");
5853
- const title = config.title ?? extractTitleFromMarkdown(raw);
5854
- const body = stripTitleFromMarkdown(raw);
5855
- execSync(`${cli} open ${DEVTO_NEW_URL}`, { encoding: "utf-8", timeout: 3e4 });
5856
- const snapshot = execSync(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
5857
- if (snapshot.includes("Log in") && !snapshot.includes("Notifications")) {
5858
- const viewer = execSync(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
5859
- return {
5860
- success: false,
5861
- error: `Not logged in to Dev.to. Please log in via viewer: ${viewer}`,
5862
- platform: "devto"
5863
- };
5864
- }
5865
- execSync(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
5866
- const escapedBody = JSON.stringify(body);
5867
- execSync(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
5868
- if (config.tags) {
5869
- const tags = config.tags.split(",").map((t) => t.trim()).slice(0, 4).join(", ");
5870
- execSync(`${cli} find text "tags" click`, { encoding: "utf-8", timeout: 1e4 });
5871
- execSync(`${cli} type ${JSON.stringify(tags)}`, { encoding: "utf-8", timeout: 1e4 });
5872
- }
5873
- execSync(`${cli} find text "Publish" click`, { encoding: "utf-8", timeout: 15e3 });
5874
- const postUrl = execSync(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
5875
- if (postUrl && postUrl !== DEVTO_NEW_URL && postUrl.includes("dev.to")) {
5876
- return { success: true, url: postUrl, platform: "devto" };
5877
- }
5878
- return { success: true, url: postUrl || void 0, platform: "devto" };
5879
- } catch (err) {
5880
- const message = err instanceof Error ? err.message : String(err);
5881
- return { success: false, error: message, platform: "devto" };
5882
- }
5883
- }
5884
-
5885
- // src/promo/medium.ts
5886
- import { readFileSync as readFileSync4 } from "fs";
5887
- import { resolve as resolve2 } from "path";
5888
- import { execSync as execSync2 } from "child_process";
5889
- var MEDIUM_NEW_URL = "https://medium.com/new-story";
5890
- function ab2(config) {
5891
- const parts = ["agent-browser"];
5892
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
5893
- if (config.session) parts.push("--session", config.session);
5894
- return parts.join(" ");
5895
- }
5896
- function extractTitleFromMarkdown2(content) {
5897
- const match = content.match(/^#\s+(.+)$/m);
5898
- if (match) return match[1].trim();
5899
- const lines = content.split("\n").filter((l) => l.trim().length > 0);
5900
- return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
5901
- }
5902
- function markdownToPlainText(content) {
5903
- return content.replace(/^#+\s+.+$/gm, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/`(.+?)`/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/!\[.*?\]\(.+?\)/g, "").trim();
5904
- }
5905
- async function publishToMedium(config) {
5906
- const cli = ab2(config);
5907
- try {
5908
- const filePath = resolve2(config.file);
5909
- const raw = readFileSync4(filePath, "utf-8");
5910
- const title = config.title ?? extractTitleFromMarkdown2(raw);
5911
- const body = markdownToPlainText(raw);
5912
- execSync2(`${cli} open ${MEDIUM_NEW_URL}`, { encoding: "utf-8", timeout: 3e4 });
5913
- const snapshot = execSync2(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
5914
- if (snapshot.includes("Sign in") && !snapshot.includes("Write")) {
5915
- const viewer = execSync2(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
5916
- return {
5917
- success: false,
5918
- error: `Not logged in to Medium. Please log in via viewer: ${viewer}`,
5919
- platform: "medium"
5920
- };
5921
- }
5922
- execSync2(`${cli} click @e_title`, { encoding: "utf-8", timeout: 1e4 });
5923
- execSync2(`${cli} type ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
5924
- execSync2(`${cli} click @e_content`, { encoding: "utf-8", timeout: 1e4 });
5925
- const lines = body.split("\n");
5926
- for (const line of lines) {
5927
- execSync2(`${cli} type ${JSON.stringify(line)}`, { encoding: "utf-8", timeout: 1e4 });
5928
- execSync2(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
5929
- }
5930
- if (config.tags) {
5931
- execSync2(`${cli} find text "Tags" click`, { encoding: "utf-8", timeout: 1e4 });
5932
- const tags = config.tags.split(",").map((t) => t.trim()).join(", ");
5933
- execSync2(`${cli} type ${JSON.stringify(tags)}`, { encoding: "utf-8", timeout: 1e4 });
5934
- }
5935
- execSync2(`${cli} find text "Publish" click`, { encoding: "utf-8", timeout: 15e3 });
5936
- const postUrl = execSync2(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
5937
- return { success: true, url: postUrl || void 0, platform: "medium" };
5938
- } catch (err) {
5939
- const message = err instanceof Error ? err.message : String(err);
5940
- return { success: false, error: message, platform: "medium" };
5941
- }
5942
- }
5943
-
5944
- // src/promo/csdn.ts
5945
- import { readFileSync as readFileSync5 } from "fs";
5946
- import { resolve as resolve3 } from "path";
5947
- import { execSync as execSync3 } from "child_process";
5948
- var CSDN_EDITOR_URL = "https://mp.csdn.net/mp_blog/creation/editor";
5949
- function ab3(config) {
5950
- const parts = ["agent-browser"];
5951
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
5952
- if (config.session) parts.push("--session", config.session);
5953
- return parts.join(" ");
5954
- }
5955
- function extractTitleFromMarkdown3(content) {
5956
- const match = content.match(/^#\s+(.+)$/m);
5957
- if (match) return match[1].trim();
5958
- const lines = content.split("\n").filter((l) => l.trim().length > 0);
5959
- return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
5960
- }
5961
- async function publishToCsdn(config) {
5962
- const cli = ab3(config);
5963
- try {
5964
- const filePath = resolve3(config.file);
5965
- const raw = readFileSync5(filePath, "utf-8");
5966
- const title = config.title ?? extractTitleFromMarkdown3(raw);
5967
- const body = raw.replace(/^#\s+.+$\n?/m, "").trim();
5968
- execSync3(`${cli} open ${CSDN_EDITOR_URL}`, { encoding: "utf-8", timeout: 3e4 });
5969
- const snapshot = execSync3(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
5970
- if (snapshot.includes("\u767B\u5F55") && !snapshot.includes("\u5199\u6587\u7AE0")) {
5971
- const viewer = execSync3(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
5972
- return {
5973
- success: false,
5974
- error: `Not logged in to CSDN. Please log in via viewer: ${viewer}`,
5975
- platform: "csdn"
5976
- };
5977
- }
5978
- execSync3(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
5979
- const escapedBody = JSON.stringify(body);
5980
- execSync3(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
5981
- if (config.tags) {
5982
- const tags = config.tags.split(",").map((t) => t.trim());
5983
- for (const tag of tags) {
5984
- execSync3(`${cli} find text "\u6DFB\u52A0\u6807\u7B7E" click`, { encoding: "utf-8", timeout: 1e4 });
5985
- execSync3(`${cli} type ${JSON.stringify(tag)}`, { encoding: "utf-8", timeout: 1e4 });
5986
- execSync3(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
5987
- }
5988
- }
5989
- execSync3(`${cli} find text "\u53D1\u5E03" click`, { encoding: "utf-8", timeout: 15e3 });
5990
- const postUrl = execSync3(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
5991
- return { success: true, url: postUrl || void 0, platform: "csdn" };
5992
- } catch (err) {
5993
- const message = err instanceof Error ? err.message : String(err);
5994
- return { success: false, error: message, platform: "csdn" };
5995
- }
5996
- }
5997
-
5998
- // src/promo/juejin.ts
5999
- import { readFileSync as readFileSync6 } from "fs";
6000
- import { resolve as resolve4 } from "path";
6001
- import { execSync as execSync4 } from "child_process";
6002
- var JUEJIN_EDITOR_URL = "https://juejin.cn/editor/draft/new";
6003
- function ab4(config) {
6004
- const parts = ["agent-browser"];
6005
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
6006
- if (config.session) parts.push("--session", config.session);
6007
- return parts.join(" ");
6008
- }
6009
- function extractTitleFromMarkdown4(content) {
6010
- const match = content.match(/^#\s+(.+)$/m);
6011
- if (match) return match[1].trim();
6012
- const lines = content.split("\n").filter((l) => l.trim().length > 0);
6013
- return lines[0] ? lines[0].replace(/^#+\s*/, "").trim() : "Untitled";
6014
- }
6015
- async function publishToJuejin(config) {
6016
- const cli = ab4(config);
6017
- try {
6018
- const filePath = resolve4(config.file);
6019
- const raw = readFileSync6(filePath, "utf-8");
6020
- const title = config.title ?? extractTitleFromMarkdown4(raw);
6021
- const body = raw.replace(/^#\s+.+$\n?/m, "").trim();
6022
- execSync4(`${cli} open ${JUEJIN_EDITOR_URL}`, { encoding: "utf-8", timeout: 3e4 });
6023
- const snapshot = execSync4(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
6024
- if (snapshot.includes("\u767B\u5F55") && snapshot.includes("\u6CE8\u518C") && !snapshot.includes("\u521B\u4F5C\u8005\u4E2D\u5FC3")) {
6025
- const viewer = execSync4(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
6026
- return {
6027
- success: false,
6028
- error: `Not logged in to Juejin. Please log in via viewer: ${viewer}`,
6029
- platform: "juejin"
6030
- };
6031
- }
6032
- execSync4(`${cli} fill @e_title ${JSON.stringify(title)}`, { encoding: "utf-8", timeout: 1e4 });
6033
- const escapedBody = JSON.stringify(body);
6034
- execSync4(`${cli} fill @e_content ${escapedBody}`, { encoding: "utf-8", timeout: 15e3 });
6035
- if (config.tags) {
6036
- const tags = config.tags.split(",").map((t) => t.trim());
6037
- for (const tag of tags) {
6038
- execSync4(`${cli} find text "\u6DFB\u52A0\u6807\u7B7E" click`, { encoding: "utf-8", timeout: 1e4 });
6039
- execSync4(`${cli} type ${JSON.stringify(tag)}`, { encoding: "utf-8", timeout: 1e4 });
6040
- execSync4(`${cli} keyboard Enter`, { encoding: "utf-8", timeout: 5e3 });
6041
- }
6042
- }
6043
- execSync4(`${cli} find text "\u53D1\u5E03" click`, { encoding: "utf-8", timeout: 15e3 });
6044
- const postUrl = execSync4(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
6045
- return { success: true, url: postUrl || void 0, platform: "juejin" };
6046
- } catch (err) {
6047
- const message = err instanceof Error ? err.message : String(err);
6048
- return { success: false, error: message, platform: "juejin" };
6049
- }
6050
- }
6051
-
6052
- // src/promo/quora.ts
6053
- import { readFileSync as readFileSync7 } from "fs";
6054
- import { resolve as resolve5 } from "path";
6055
- import { execSync as execSync5 } from "child_process";
6056
- function ab5(config) {
6057
- const parts = ["agent-browser"];
6058
- if (config.cdpEndpoint) parts.push("--cdp", config.cdpEndpoint);
6059
- if (config.session) parts.push("--session", config.session);
6060
- return parts.join(" ");
6061
- }
6062
- async function publishToQuora(config) {
6063
- const cli = ab5(config);
6064
- try {
6065
- if (!config.search) {
6066
- return {
6067
- success: false,
6068
- error: "Quora requires --search parameter to find relevant questions",
6069
- platform: "quora"
6070
- };
6071
- }
6072
- const filePath = resolve5(config.file);
6073
- const raw = readFileSync7(filePath, "utf-8");
6074
- const answer = raw.trim();
6075
- const searchUrl = `https://www.quora.com/search?q=${encodeURIComponent(config.search)}`;
6076
- execSync5(`${cli} open ${searchUrl}`, { encoding: "utf-8", timeout: 3e4 });
6077
- const snapshot = execSync5(`${cli} snapshot -i -s body`, { encoding: "utf-8", timeout: 15e3 });
6078
- if (!snapshot.includes("Add question")) {
6079
- const viewer = execSync5(`${cli} viewer --json`, { encoding: "utf-8", timeout: 1e4 }).trim();
6080
- return {
6081
- success: false,
6082
- error: `Not logged in to Quora. Please log in via viewer: ${viewer}`,
6083
- platform: "quora"
6084
- };
6085
- }
6086
- const answerMatch = snapshot.match(/Answer\s*(?:button|link)/i);
6087
- if (!answerMatch) {
6088
- return {
6089
- success: false,
6090
- error: "No answerable questions found for the given search query. Try a different search term.",
6091
- platform: "quora"
6092
- };
6093
- }
6094
- execSync5(`${cli} find text "Answer" click`, { encoding: "utf-8", timeout: 1e4 });
6095
- const escapedAnswer = JSON.stringify(answer);
6096
- execSync5(`${cli} fill @e_answer ${escapedAnswer}`, { encoding: "utf-8", timeout: 15e3 });
6097
- execSync5(`${cli} find text "Submit" click`, { encoding: "utf-8", timeout: 15e3 });
6098
- const postUrl = execSync5(`${cli} get url`, { encoding: "utf-8", timeout: 15e3 }).trim();
6099
- return { success: true, url: postUrl || void 0, platform: "quora" };
6100
- } catch (err) {
6101
- const message = err instanceof Error ? err.message : String(err);
6102
- return { success: false, error: message, platform: "quora" };
6103
- }
6104
- }
6105
-
6106
- // src/promo/index.ts
6107
- var PUBLISHERS = {
6108
- devto: publishToDevto,
6109
- medium: publishToMedium,
6110
- csdn: publishToCsdn,
6111
- juejin: publishToJuejin,
6112
- quora: publishToQuora
6113
- };
6114
- async function dispatchPromo(config) {
6115
- const publisher = PUBLISHERS[config.platform];
6116
- if (!publisher) {
6117
- return {
6118
- success: false,
6119
- error: `Unknown platform: ${config.platform}. Supported: ${Object.keys(PUBLISHERS).join(", ")}`,
6120
- platform: config.platform
6121
- };
6122
- }
6123
- return publisher(config);
6124
- }
6125
-
6126
- // src/commands/promo.ts
6127
- var promoParams = z26.object({
6128
- platform: z26.enum(["devto", "medium", "csdn", "juejin", "quora"]).describe("Target platform for promotion"),
6129
- file: z26.string().describe("Path to Markdown file to publish"),
6130
- tags: z26.string().optional().describe("Comma-separated tags"),
6131
- title: z26.string().optional().describe("Custom title (default: extracted from file first heading)"),
6132
- search: z26.string().optional().describe("Quora: search query to find questions"),
6133
- cdpEndpoint: z26.string().optional().describe("CDP endpoint for agent-browser"),
6134
- session: z26.string().optional().describe("agent-browser session name")
6135
- }).refine(
6136
- (data) => data.platform !== "quora" || !!data.search,
6137
- { message: "Quora platform requires --search parameter" }
6138
- ).refine(
6139
- (data) => existsSync2(resolve6(data.file)),
6140
- { message: "File does not exist" }
6141
- );
6142
- var promoCommand = registerCommand({
6143
- name: "promo",
6144
- description: "Publish promotional articles to various platforms (devto, medium, csdn, juejin, quora)",
6145
- scope: "project",
6146
- parameters: promoParams,
6147
- result: z26.object({
6148
- success: z26.boolean(),
6149
- url: z26.string().optional(),
6150
- error: z26.string().optional(),
6151
- platform: z26.string()
6152
- }),
6153
- handler: async (p, _ctx) => {
6154
- const filePath = resolve6(p.file);
6155
- const content = readFileSync8(filePath, "utf-8");
6156
- if (content.trim().length === 0) {
6157
- return ok25({
6158
- success: false,
6159
- error: `File is empty: ${filePath}`,
6160
- platform: p.platform
6161
- });
6162
- }
6163
- const result = await dispatchPromo({
6164
- platform: p.platform,
6165
- file: filePath,
6166
- tags: p.tags,
6167
- title: p.title,
6168
- search: p.search,
6169
- cdpEndpoint: p.cdpEndpoint ?? _ctx.cdpEndpoint,
6170
- session: p.session ?? p.platform
6171
- });
6172
- return ok25(result);
6173
- }
6174
- });
6175
-
6176
5862
  // src/plugin/loader.ts
6177
5863
  import {
6178
5864
  Core
6179
5865
  } from "@dyyz1993/xcli-core";
6180
- import { resolve as resolve8 } from "path";
6181
- import { existsSync as existsSync5, readdirSync } from "fs";
6182
- import { homedir as homedir2 } from "os";
5866
+ import { resolve as resolve2 } from "path";
5867
+ import { existsSync as existsSync4, readdirSync } from "fs";
5868
+ import { homedir as homedir4 } from "os";
6183
5869
 
6184
5870
  // src/plugin/metadata-parser.ts
6185
- import { existsSync as existsSync3 } from "fs";
6186
- import { resolve as resolve7 } from "path";
5871
+ import { existsSync as existsSync2 } from "fs";
5872
+ import { resolve } from "path";
6187
5873
 
6188
5874
  // src/utils/json-file.ts
6189
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
5875
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
6190
5876
  function readJsonFile(filePath, defaultValue) {
6191
5877
  try {
6192
- const content = readFileSync9(filePath, "utf-8");
5878
+ const content = readFileSync3(filePath, "utf-8");
6193
5879
  return JSON.parse(content);
6194
5880
  } catch {
6195
5881
  return defaultValue;
@@ -6200,8 +5886,8 @@ function readJsonFile(filePath, defaultValue) {
6200
5886
  var PluginMetadataParser = class {
6201
5887
  static XBROWSER_KEYWORDS = ["xbrowser", "xbrowser-plugin"];
6202
5888
  static parseFromPackageJson(pluginPath) {
6203
- const packageJsonPath = resolve7(pluginPath, "package.json");
6204
- if (!existsSync3(packageJsonPath)) {
5889
+ const packageJsonPath = resolve(pluginPath, "package.json");
5890
+ if (!existsSync2(packageJsonPath)) {
6205
5891
  return null;
6206
5892
  }
6207
5893
  const packageJson = readJsonFile(packageJsonPath, null);
@@ -6265,20 +5951,20 @@ var PluginMetadataParser = class {
6265
5951
  };
6266
5952
 
6267
5953
  // src/plugin/ensure-deps.ts
6268
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
6269
- import { join as join2 } from "path";
6270
- import { execSync as execSync6 } from "child_process";
5954
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
5955
+ import { join as join4 } from "path";
5956
+ import { execSync } from "child_process";
6271
5957
  var SHARED_PLUGIN_DEPENDENCIES = {
6272
5958
  "zod": "^3.24.0",
6273
5959
  "@dyyz1993/xcli-core": "^0.12.1"
6274
5960
  };
6275
5961
  function ensurePluginDependencies(pluginsDir) {
6276
- const zodPath = join2(pluginsDir, "node_modules", "zod");
6277
- if (existsSync4(zodPath)) return;
6278
- mkdirSync2(pluginsDir, { recursive: true });
6279
- const pkgPath = join2(pluginsDir, "package.json");
5962
+ const zodPath = join4(pluginsDir, "node_modules", "zod");
5963
+ if (existsSync3(zodPath)) return;
5964
+ mkdirSync4(pluginsDir, { recursive: true });
5965
+ const pkgPath = join4(pluginsDir, "package.json");
6280
5966
  let pkg2 = {};
6281
- if (existsSync4(pkgPath)) {
5967
+ if (existsSync3(pkgPath)) {
6282
5968
  try {
6283
5969
  pkg2 = readJsonFile(pkgPath, {});
6284
5970
  } catch {
@@ -6292,13 +5978,13 @@ function ensurePluginDependencies(pluginsDir) {
6292
5978
  needsInstall = true;
6293
5979
  }
6294
5980
  }
6295
- if (!needsInstall && existsSync4(join2(pluginsDir, "node_modules"))) return;
5981
+ if (!needsInstall && existsSync3(join4(pluginsDir, "node_modules"))) return;
6296
5982
  pkg2.dependencies = existingDeps;
6297
5983
  pkg2.private = true;
6298
5984
  pkg2.description = pkg2.description || "xbrowser plugins \u2014 shared dependencies";
6299
- writeFileSync4(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
5985
+ writeFileSync5(pkgPath, JSON.stringify(pkg2, null, 2) + "\n", "utf-8");
6300
5986
  try {
6301
- execSync6("npm install --production --no-package-lock --no-fund --no-audit", {
5987
+ execSync("npm install --production --no-package-lock --no-fund --no-audit", {
6302
5988
  cwd: pluginsDir,
6303
5989
  stdio: "pipe",
6304
5990
  timeout: 6e4,
@@ -6310,8 +5996,15 @@ function ensurePluginDependencies(pluginsDir) {
6310
5996
  }
6311
5997
 
6312
5998
  // src/plugin/contract.ts
5999
+ import {
6000
+ unwrapZod,
6001
+ fieldsFromZodObjectReflected,
6002
+ zodTypeToContractType
6003
+ } from "@dyyz1993/xcli-core";
6313
6004
  function buildPluginContract(site) {
6314
- const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command));
6005
+ const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command, {
6006
+ siteRequiresLogin: site.config?.requiresLogin
6007
+ }));
6315
6008
  return {
6316
6009
  version: 2,
6317
6010
  plugin: {
@@ -6323,18 +6016,19 @@ function buildPluginContract(site) {
6323
6016
  commands
6324
6017
  };
6325
6018
  }
6326
- function buildCommandContract(command) {
6019
+ function buildCommandContract(command, options = {}) {
6327
6020
  const extension = command.xbrowser || {};
6328
6021
  const inferredFields = fieldsFromZodObject(command.parameters);
6329
6022
  const fields = mergeFields(inferredFields, extension.form?.fields || []);
6330
6023
  const positional = extension.positional || fields.filter((field) => field.positional).map((field) => field.name);
6331
- const capabilities = extension.capabilities || inferCapabilities(command.scope || "project", command.requiresLogin);
6024
+ const requiresLogin = command.requiresLogin === true || options.siteRequiresLogin === true && command.name !== "login" && command.name !== "logout";
6025
+ const capabilities = extension.capabilities || inferCapabilities(command.scope || "project", requiresLogin);
6332
6026
  const outputSchema = command.result ? summarizeZod(command.result) : void 0;
6333
6027
  return {
6334
6028
  name: command.name,
6335
6029
  description: command.description || "",
6336
6030
  scope: command.scope || "project",
6337
- requiresLogin: command.requiresLogin === true,
6031
+ requiresLogin,
6338
6032
  category: extension.category,
6339
6033
  capabilities,
6340
6034
  positional,
@@ -6348,25 +6042,21 @@ function buildCommandContract(command) {
6348
6042
  };
6349
6043
  }
6350
6044
  function fieldsFromZodObject(schema) {
6351
- const shape = getShape(schema);
6352
- if (!shape) return [];
6353
- return Object.entries(shape).map(([name, field]) => fieldFromZod(name, field));
6354
- }
6355
- function fieldFromZod(name, schema) {
6356
- const unwrapped = unwrapZod(schema);
6357
- const type = zodTypeToContractType(unwrapped.typeName);
6358
- const enumValues = extractEnumValues(unwrapped.schema);
6359
- return {
6360
- name,
6361
- label: toLabel(name),
6362
- type,
6363
- widget: widgetFor(type, enumValues, unwrapped.schema),
6364
- required: !unwrapped.optional,
6365
- ...unwrapped.description ? { description: unwrapped.description } : {},
6366
- ...unwrapped.defaultValue !== void 0 ? { default: unwrapped.defaultValue } : {},
6367
- ...enumValues ? { enum: enumValues } : {},
6368
- ...type === "array" ? { multiple: true } : {}
6369
- };
6045
+ const reflected = fieldsFromZodObjectReflected(schema);
6046
+ return reflected.map((field) => {
6047
+ const widget = widgetFor(field.type, field.enum);
6048
+ return {
6049
+ name: field.name,
6050
+ label: toLabel(field.name),
6051
+ type: field.type,
6052
+ widget,
6053
+ required: field.required,
6054
+ ...field.description ? { description: field.description } : {},
6055
+ ...field.default !== void 0 ? { default: field.default } : {},
6056
+ ...field.enum ? { enum: field.enum } : {},
6057
+ ...field.type === "array" ? { multiple: true } : {}
6058
+ };
6059
+ });
6370
6060
  }
6371
6061
  function mergeFields(inferred, overrides) {
6372
6062
  if (overrides.length === 0) return inferred;
@@ -6397,72 +6087,14 @@ function inferCapabilities(scope, requiresLogin) {
6397
6087
  if (requiresLogin) caps.push("auth.login");
6398
6088
  return caps;
6399
6089
  }
6400
- function getShape(schema) {
6401
- const zod = schema;
6402
- const shapeOrFn = zod?.shape ?? zod?._def?.shape;
6403
- if (!shapeOrFn) return void 0;
6404
- return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
6405
- }
6406
- function unwrapZod(schema) {
6407
- let current = schema;
6408
- let optional = typeof current?.isOptional === "function" ? current.isOptional() : false;
6409
- let description = current?._def?.description;
6410
- let defaultValue;
6411
- for (let i = 0; i < 8; i++) {
6412
- const def = current?._def;
6413
- const typeName = def?.typeName || "unknown";
6414
- if (def?.description) description = def.description;
6415
- if (!def) return { schema: current, typeName, optional, description, defaultValue };
6416
- if (typeName === "ZodDefault") {
6417
- optional = true;
6418
- defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
6419
- current = def.innerType || def.type;
6420
- continue;
6421
- }
6422
- if (typeName === "ZodOptional" || typeName === "ZodNullable") {
6423
- optional = true;
6424
- current = def.innerType || def.type;
6425
- continue;
6426
- }
6427
- return { schema: current, typeName, optional, description, defaultValue };
6428
- }
6429
- return { schema: current, typeName: current?._def?.typeName || "unknown", optional, description, defaultValue };
6430
- }
6431
- function zodTypeToContractType(typeName) {
6432
- switch (typeName) {
6433
- case "ZodString":
6434
- return "string";
6435
- case "ZodNumber":
6436
- return "number";
6437
- case "ZodBoolean":
6438
- return "boolean";
6439
- case "ZodEnum":
6440
- case "ZodNativeEnum":
6441
- return "enum";
6442
- case "ZodArray":
6443
- return "array";
6444
- case "ZodObject":
6445
- return "object";
6446
- default:
6447
- return typeName.replace(/^Zod/, "").toLowerCase() || "unknown";
6448
- }
6449
- }
6450
- function widgetFor(type, enumValues, schema) {
6090
+ function widgetFor(type, enumValues) {
6451
6091
  if (enumValues) return "select";
6452
6092
  if (type === "boolean") return "checkbox";
6453
6093
  if (type === "number") return "number";
6454
6094
  if (type === "array") return "multi-select";
6455
6095
  if (type === "object") return "json";
6456
- const checks = schema?._def?.checks;
6457
- if (checks?.some((check) => check.kind === "url")) return "url";
6458
6096
  return "text";
6459
6097
  }
6460
- function extractEnumValues(schema) {
6461
- const def = schema?._def;
6462
- const values = def?.values;
6463
- if (Array.isArray(values)) return values.map(String);
6464
- return void 0;
6465
- }
6466
6098
  function summarizeZod(schema) {
6467
6099
  const unwrapped = unwrapZod(schema);
6468
6100
  if (unwrapped.typeName === "ZodArray") {
@@ -6472,7 +6104,7 @@ function summarizeZod(schema) {
6472
6104
  items: summarizeZod(def?.type || def?.innerType)
6473
6105
  };
6474
6106
  }
6475
- const shape = getShape(schema);
6107
+ const shape = getObjectShape(schema);
6476
6108
  if (!shape) {
6477
6109
  return {
6478
6110
  type: zodTypeToContractType(unwrapped.typeName),
@@ -6482,19 +6114,53 @@ function summarizeZod(schema) {
6482
6114
  }
6483
6115
  return Object.fromEntries(
6484
6116
  Object.entries(shape).map(([name, field]) => {
6485
- const unwrapped2 = unwrapZod(field);
6117
+ const inner = unwrapZod(field);
6486
6118
  return [name, {
6487
- type: zodTypeToContractType(unwrapped2.typeName),
6488
- required: !unwrapped2.optional,
6489
- ...unwrapped2.description ? { description: unwrapped2.description } : {}
6119
+ type: zodTypeToContractType(inner.typeName),
6120
+ required: !inner.optional,
6121
+ ...inner.description ? { description: inner.description } : {}
6490
6122
  }];
6491
6123
  })
6492
6124
  );
6493
6125
  }
6126
+ function getObjectShape(schema) {
6127
+ const zod = schema;
6128
+ const shapeOrFn = zod?.shape ?? zod?._def?.shape;
6129
+ if (!shapeOrFn) return void 0;
6130
+ return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
6131
+ }
6494
6132
  function toLabel(name) {
6495
6133
  return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
6496
6134
  }
6497
6135
 
6136
+ // src/plugin/login-required-patch.ts
6137
+ import { SiteInstanceImpl } from "@dyyz1993/xcli-core";
6138
+ var patched = false;
6139
+ function patchLoginRequired() {
6140
+ if (patched) return;
6141
+ patched = true;
6142
+ const target = SiteInstanceImpl.prototype;
6143
+ const originalCommand = target.command;
6144
+ const wrapped = function(...args) {
6145
+ const result = originalCommand.apply(this, args);
6146
+ const [name, cmd] = args;
6147
+ const loginRequired = cmd.loginRequired;
6148
+ if (loginRequired) {
6149
+ const commands = this.commands;
6150
+ const entry = commands?.get(name);
6151
+ if (entry) {
6152
+ entry.loginRequired = loginRequired;
6153
+ }
6154
+ }
6155
+ return result;
6156
+ };
6157
+ Object.defineProperty(target, "command", {
6158
+ value: wrapped,
6159
+ writable: true,
6160
+ configurable: true
6161
+ });
6162
+ }
6163
+
6498
6164
  // src/plugin/loader.ts
6499
6165
  var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
6500
6166
  var XBrowserPluginLoader = class {
@@ -6502,6 +6168,7 @@ var XBrowserPluginLoader = class {
6502
6168
  loader;
6503
6169
  options;
6504
6170
  constructor(options) {
6171
+ patchLoginRequired();
6505
6172
  this.options = options ?? {};
6506
6173
  const cwd = this.options.cwd || process.cwd();
6507
6174
  const coreConfig = {
@@ -6512,7 +6179,7 @@ var XBrowserPluginLoader = class {
6512
6179
  envPrefix: "XBROWSER",
6513
6180
  pluginDirs: [
6514
6181
  ...DEFAULT_PLUGIN_DIRS,
6515
- resolve8(cwd, ".xcli/plugins")
6182
+ resolve2(cwd, ".xcli/plugins")
6516
6183
  ]
6517
6184
  };
6518
6185
  this.core = new Core(coreConfig);
@@ -6558,31 +6225,31 @@ var XBrowserPluginLoader = class {
6558
6225
  }
6559
6226
  async scanAndLoad() {
6560
6227
  const cwd = this.options.cwd || process.cwd();
6561
- const globalDir = this.options.globalDir || resolve8(homedir2(), ".xbrowser/plugins");
6228
+ const globalDir = this.options.globalDir || resolve2(homedir4(), ".xbrowser/plugins");
6562
6229
  ensurePluginDependencies(globalDir);
6563
6230
  const dirs = [
6564
- resolve8(cwd, ".xcli/plugins"),
6565
- resolve8(cwd, "../.xcli/plugins"),
6566
- this.options.userDir || resolve8(homedir2(), ".xcli/plugins"),
6231
+ resolve2(cwd, ".xcli/plugins"),
6232
+ resolve2(cwd, "../.xcli/plugins"),
6233
+ this.options.userDir || resolve2(homedir4(), ".xcli/plugins"),
6567
6234
  globalDir
6568
6235
  ];
6569
6236
  const loaded = [];
6570
6237
  const seen = /* @__PURE__ */ new Set();
6571
6238
  for (const dir of dirs) {
6572
- if (!existsSync5(dir)) continue;
6239
+ if (!existsSync4(dir)) continue;
6573
6240
  const entries = readdirSync(dir, { withFileTypes: true });
6574
6241
  for (const entry of entries) {
6575
6242
  if (!entry.isDirectory()) continue;
6576
6243
  if (seen.has(entry.name)) continue;
6577
6244
  seen.add(entry.name);
6578
- const pluginDir = resolve8(dir, entry.name);
6579
- let indexPath = resolve8(pluginDir, "index.js");
6580
- if (!existsSync5(indexPath)) {
6581
- indexPath = resolve8(pluginDir, "index.ts");
6245
+ const pluginDir = resolve2(dir, entry.name);
6246
+ let indexPath = resolve2(pluginDir, "index.js");
6247
+ if (!existsSync4(indexPath)) {
6248
+ indexPath = resolve2(pluginDir, "index.ts");
6582
6249
  }
6583
- if (!existsSync5(indexPath)) continue;
6250
+ if (!existsSync4(indexPath)) continue;
6584
6251
  try {
6585
- if (!existsSync5(resolve8(pluginDir, "package.json"))) {
6252
+ if (!existsSync4(resolve2(pluginDir, "package.json"))) {
6586
6253
  console.warn(`\u26A0\uFE0F Plugin "${entry.name}" has no package.json. Use "xbrowser create ${entry.name} --template static" for proper structure.`);
6587
6254
  } else {
6588
6255
  const metadata = PluginMetadataParser.parseFromPackageJson(pluginDir);
@@ -6620,28 +6287,132 @@ async function getPluginLoader() {
6620
6287
  return pluginLoader;
6621
6288
  }
6622
6289
 
6623
- // src/tips/dom-watcher.ts
6624
- var DOM_WATCHER_SCRIPT = `
6625
- (function() {
6626
- if (window.__xbrowserDomWatcher) return;
6627
- window.__xbrowserDomWatcher = true;
6628
-
6629
- window.__xbrowserDetectedElements = [];
6290
+ // src/utils/viewer-url.ts
6291
+ function buildViewerUrl(sessionName = "default") {
6292
+ try {
6293
+ const status = getDaemonProcessStatus();
6294
+ if (!status.running) return void 0;
6295
+ const port = status.port || getDaemonConfig().basePort;
6296
+ return `http://localhost:${port}/preview/${encodeURIComponent(sessionName)}`;
6297
+ } catch {
6298
+ return void 0;
6299
+ }
6300
+ }
6630
6301
 
6631
- function getSelector(el) {
6632
- if (el.id) return '#' + CSS.escape(el.id);
6633
- if (el.getAttribute('aria-label')) {
6634
- return el.tagName.toLowerCase() + '[aria-label="' + el.getAttribute('aria-label') + '"]';
6302
+ // src/plugin/login-guard.ts
6303
+ async function checkPluginLoginRequired(options) {
6304
+ const { site, command, commandName, ctx, page, sessionName } = options;
6305
+ if (commandName === "login" || commandName === "logout") return { ok: true };
6306
+ const loginConfig = site.config?.loginConfig;
6307
+ const requiresLogin = command.requiresLogin === true || site.config?.requiresLogin === true || loginConfig?.requiresLogin === true;
6308
+ if (!requiresLogin) return { ok: true };
6309
+ const pluginName = site.name || "plugin";
6310
+ if (typeof site.isLoggedIn === "function") {
6311
+ try {
6312
+ const loggedIn = await site.isLoggedIn(ctx);
6313
+ if (loggedIn) return { ok: true };
6314
+ return buildLoginRequired({
6315
+ plugin: pluginName,
6316
+ command: commandName,
6317
+ reason: "plugin isLoggedIn returned false",
6318
+ sessionName,
6319
+ loginConfig
6320
+ });
6321
+ } catch {
6635
6322
  }
6636
- const cls = el.className && typeof el.className === 'string'
6637
- ? el.className.trim().split(/\\s+/).filter(c => c.length > 0 && !c.startsWith('__')).slice(0, 2).join('.')
6638
- : '';
6639
- if (cls) return el.tagName.toLowerCase() + '.' + cls;
6640
- return el.tagName.toLowerCase();
6641
6323
  }
6642
-
6643
- function isPopupLike(el) {
6644
- const tag = el.tagName?.toLowerCase();
6324
+ if (page && loginConfig) {
6325
+ const generic = await detectLoginFromPage(page, loginConfig);
6326
+ if (generic === "logged-in") return { ok: true };
6327
+ if (generic === "logged-out") {
6328
+ return buildLoginRequired({
6329
+ plugin: pluginName,
6330
+ command: commandName,
6331
+ reason: "generic loginConfig detected logged-out page",
6332
+ sessionName,
6333
+ loginConfig
6334
+ });
6335
+ }
6336
+ }
6337
+ return { ok: true };
6338
+ }
6339
+ function buildLoginRequired(options) {
6340
+ const viewerUrl = buildViewerUrl(options.sessionName);
6341
+ const loginUrl = options.loginConfig?.loginUrl;
6342
+ const message = options.loginConfig?.loginPrompt || `Plugin "${options.plugin}" requires login before running "${options.command}".`;
6343
+ const tips = [
6344
+ message,
6345
+ ...viewerUrl ? [`Open viewer to complete login: ${viewerUrl}`] : [],
6346
+ ...loginUrl ? [`Login page: ${loginUrl}`] : [],
6347
+ `After login, retry: xbrowser ${options.plugin} ${options.command} --session ${options.sessionName}`
6348
+ ];
6349
+ return {
6350
+ ok: false,
6351
+ data: {
6352
+ code: "LOGIN_REQUIRED",
6353
+ plugin: options.plugin,
6354
+ command: options.command,
6355
+ reason: options.reason,
6356
+ ...viewerUrl ? { viewerUrl } : {},
6357
+ ...loginUrl ? { loginUrl } : {}
6358
+ },
6359
+ message,
6360
+ tips
6361
+ };
6362
+ }
6363
+ async function detectLoginFromPage(page, config) {
6364
+ const url = page.url();
6365
+ if (config.loginUrls?.some((part) => url.includes(part))) return "logged-out";
6366
+ const result = await page.evaluate((cfg) => {
6367
+ const visible = (selector) => {
6368
+ try {
6369
+ const el = document.querySelector(selector);
6370
+ if (!el) return false;
6371
+ const rect = el.getBoundingClientRect();
6372
+ const style = window.getComputedStyle(el);
6373
+ return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
6374
+ } catch {
6375
+ return false;
6376
+ }
6377
+ };
6378
+ if (cfg.loggedInSelectors?.some(visible)) return "logged-in";
6379
+ if (cfg.loginSelectors?.some(visible)) return "logged-out";
6380
+ const bodyText = document.body?.innerText || "";
6381
+ const keywords = cfg.loginKeywords || [];
6382
+ if (keywords.length > 0 && keywords.every((keyword) => bodyText.includes(keyword))) {
6383
+ return "logged-out";
6384
+ }
6385
+ return "unknown";
6386
+ }, {
6387
+ loginSelectors: config.loginSelectors || [],
6388
+ loginKeywords: config.loginKeywords || [],
6389
+ loggedInSelectors: config.loggedInSelectors || []
6390
+ });
6391
+ return result;
6392
+ }
6393
+
6394
+ // src/tips/dom-watcher.ts
6395
+ var DOM_WATCHER_SCRIPT = `
6396
+ (function() {
6397
+ if (window.__xbrowserDomWatcher) return;
6398
+ window.__xbrowserDomWatcher = true;
6399
+
6400
+ window.__xbrowserDetectedElements = [];
6401
+
6402
+ function getSelector(el) {
6403
+ if (el.id) return '#' + CSS.escape(el.id);
6404
+ if (el.getAttribute('aria-label')) {
6405
+ return el.tagName.toLowerCase() + '[aria-label="' + el.getAttribute('aria-label') + '"]';
6406
+ }
6407
+ const cls = el.className && typeof el.className === 'string'
6408
+ ? el.className.trim().split(/\\s+/).filter(c => c.length > 0 && !c.startsWith('__')).slice(0, 2).join('.')
6409
+ : '';
6410
+ if (cls) return el.tagName.toLowerCase() + '.' + cls;
6411
+ return el.tagName.toLowerCase();
6412
+ }
6413
+
6414
+ function isPopupLike(el) {
6415
+ const tag = el.tagName?.toLowerCase();
6645
6416
  if (tag === 'dialog') return true;
6646
6417
  if (tag === 'body' || tag === 'html') return false;
6647
6418
 
@@ -6999,7 +6770,7 @@ var TipsManager = class {
6999
6770
  }
7000
6771
  }
7001
6772
  debounce() {
7002
- return new Promise((resolve16) => setTimeout(resolve16, DEBOUNCE_MS));
6773
+ return new Promise((resolve10) => setTimeout(resolve10, DEBOUNCE_MS));
7003
6774
  }
7004
6775
  formatTips(tips) {
7005
6776
  return tips.map((tip) => {
@@ -7027,33 +6798,104 @@ function getTipsManager() {
7027
6798
  }
7028
6799
 
7029
6800
  // src/hooks/loader.ts
7030
- var builtinHooks = {
7031
- screenshot: () => import("./screenshot-CWAWMXVA.js").then((m) => m.screenshotHook)
6801
+ var FAIL_KEYWORDS = [
6802
+ "\u767B\u5F55",
6803
+ "login",
6804
+ "Login",
6805
+ "\u672A\u767B\u5F55",
6806
+ "not logged in",
6807
+ "cdp",
6808
+ "CDP",
6809
+ "\u9A8C\u8BC1\u7801",
6810
+ "\u9A8C\u8BC1",
6811
+ "captcha",
6812
+ "\u9700\u8981\u767B\u5F55",
6813
+ "requires login",
6814
+ "blocked",
6815
+ "403",
6816
+ "404"
6817
+ ];
6818
+ var HOOK_REGISTRY = {
6819
+ viewer: {
6820
+ name: "viewer",
6821
+ onAfterCommand: async (ctx) => {
6822
+ const result = ctx.result;
6823
+ if (!result || result.success !== false) return void 0;
6824
+ const msg = [
6825
+ result.message,
6826
+ ...result.tips || []
6827
+ ].filter(Boolean).join(" ").toLowerCase();
6828
+ if (!FAIL_KEYWORDS.some((k) => msg.includes(k))) return void 0;
6829
+ const viewerUrl = buildViewerUrl();
6830
+ if (!viewerUrl) return void 0;
6831
+ const tips = result.tips || [];
6832
+ if (!tips.some((t) => t.includes("viewer") || t.includes("Viewer"))) {
6833
+ tips.push(`Open viewer: ${viewerUrl}`);
6834
+ }
6835
+ result.tips = tips;
6836
+ result.viewerUrl = viewerUrl;
6837
+ return void 0;
6838
+ }
6839
+ },
6840
+ screenshot: {
6841
+ name: "screenshot",
6842
+ onAfterCommand: async (ctx) => {
6843
+ try {
6844
+ const buf = await ctx.page.screenshot({ type: "jpeg", quality: 40 }).catch(() => null);
6845
+ if (!buf) return;
6846
+ return { screenshot: { url: `data:image/jpeg;base64,${buf.toString("base64").slice(0, 50)}...` } };
6847
+ } catch {
6848
+ return;
6849
+ }
6850
+ }
6851
+ },
6852
+ recorder: {
6853
+ name: "recorder",
6854
+ onAfterCommand: async (ctx) => {
6855
+ const logs = ("__commandLogs" in ctx ? ctx.__commandLogs : void 0) || [];
6856
+ logs.push({
6857
+ timestamp: Date.now(),
6858
+ command: ctx.command,
6859
+ params: JSON.parse(JSON.stringify(ctx.params)),
6860
+ duration: ctx.duration
6861
+ });
6862
+ Reflect.set(ctx, "__commandLogs", logs);
6863
+ return void 0;
6864
+ }
6865
+ }
7032
6866
  };
6867
+ var customHooks = {};
7033
6868
  async function loadHooks() {
7034
- const names = process.env.XBROWSER_HOOKS;
7035
- if (!names) return [];
6869
+ const env = process.env.XBROWSER_HOOKS;
6870
+ if (!env) return [];
6871
+ const names = env.split(",").map((n) => n.trim()).filter(Boolean);
6872
+ if (names.length === 0) return [];
7036
6873
  const hooks = [];
7037
- for (const name of names.split(",")) {
7038
- const trimmed = name.trim();
7039
- const factory = builtinHooks[trimmed];
7040
- if (factory) {
7041
- hooks.push(await factory());
6874
+ for (const name of names) {
6875
+ const hook = HOOK_REGISTRY[name];
6876
+ if (hook) {
6877
+ hooks.push(hook);
6878
+ continue;
6879
+ }
6880
+ const customFactory = customHooks[name];
6881
+ if (customFactory) {
6882
+ const customHook = await customFactory();
6883
+ if (customHook) hooks.push(customHook);
7042
6884
  }
7043
6885
  }
7044
6886
  return hooks;
7045
6887
  }
7046
6888
 
7047
6889
  // src/executor.ts
7048
- import { homedir as homedir3 } from "os";
7049
- import { join as join3 } from "path";
6890
+ import { homedir as homedir5 } from "os";
6891
+ import { join as join5 } from "path";
7050
6892
  var NAVIGATION_COMMANDS = /* @__PURE__ */ new Set(["goto", "back", "forward", "refresh"]);
7051
6893
  var snapshotHintShown = /* @__PURE__ */ new WeakSet();
7052
- var STORAGE_DIR = join3(homedir3(), ".xbrowser", "storage");
6894
+ var CONFIG_DIR2 = join5(homedir5(), ".xbrowser");
7053
6895
  var storageCache = /* @__PURE__ */ new Map();
7054
6896
  function getPluginStorage(pluginName) {
7055
6897
  if (!storageCache.has(pluginName)) {
7056
- storageCache.set(pluginName, new PluginStorage(pluginName, STORAGE_DIR));
6898
+ storageCache.set(pluginName, new CompositeStorage2(pluginName, CONFIG_DIR2, "xbrowser"));
7057
6899
  }
7058
6900
  return storageCache.get(pluginName);
7059
6901
  }
@@ -7061,7 +6903,7 @@ var archiveInitialized = false;
7061
6903
  function ensureArchiveInit() {
7062
6904
  if (!archiveInitialized) {
7063
6905
  try {
7064
- configureArchiveStore({ archiveDir: join3(homedir3(), ".xbrowser", "archives") });
6906
+ configureArchiveStore({ archiveDir: join5(homedir5(), ".xbrowser", "archives") });
7065
6907
  } catch {
7066
6908
  }
7067
6909
  archiveInitialized = true;
@@ -7087,6 +6929,10 @@ async function guardCheck(commandName) {
7087
6929
  function errorResult(message) {
7088
6930
  return { ...fail7(message), duration: 0 };
7089
6931
  }
6932
+ function tipsToMessages(tips) {
6933
+ if (!tips || tips.length === 0) return [];
6934
+ return tips.map((t) => typeof t === "string" ? t : t.message);
6935
+ }
7090
6936
  var wsServer = null;
7091
6937
  function streamCommandEvent(sessionId, message) {
7092
6938
  if (!wsServer || !wsServer.getRunning()) return;
@@ -7114,7 +6960,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7114
6960
  }
7115
6961
  let targetPageOverride = null;
7116
6962
  if (_target && extraOpts?.cdpEndpoint) {
7117
- const { findTargetPage } = await import("./browser-DSVV4GHS.js");
6963
+ const { findTargetPage } = await import("./browser-5CTOA2WS.js");
7118
6964
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
7119
6965
  if (!targetPageOverride) {
7120
6966
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -7131,7 +6977,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7131
6977
  params = result.data;
7132
6978
  }
7133
6979
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
7134
- const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
6980
+ const { forwardExec } = await import("./daemon-client-3JOKX2L2.js");
7135
6981
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
7136
6982
  if (result) return result;
7137
6983
  }
@@ -7157,18 +7003,10 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7157
7003
  });
7158
7004
  }
7159
7005
  }
7160
- } else if ((command.scope === "page" || command.scope === "project") && params.url) {
7006
+ } else if (command.scope !== "project") {
7161
7007
  session = await createSession(sessionName, params.url, {
7162
7008
  cdpEndpoint: extraOpts?.cdpEndpoint
7163
7009
  });
7164
- } else if (command.scope === "browser") {
7165
- session = await createSession(sessionName, void 0, {
7166
- cdpEndpoint: extraOpts?.cdpEndpoint
7167
- });
7168
- } else if (command.scope !== "project") {
7169
- return errorResult(
7170
- `Session '${sessionName}' not found. Run "xbrowser session open <url>" first.`
7171
- );
7172
7010
  }
7173
7011
  const ctx = {
7174
7012
  page: session?.page,
@@ -7190,8 +7028,9 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7190
7028
  throw new Error(msg);
7191
7029
  },
7192
7030
  config: {},
7193
- site: {},
7194
- cliName: "xbrowser"
7031
+ site: new NoopSiteInstance(),
7032
+ cliName: "xbrowser",
7033
+ tips: new TipCollector2()
7195
7034
  };
7196
7035
  const start = Date.now();
7197
7036
  if (session) {
@@ -7212,7 +7051,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7212
7051
  const cache = /* @__PURE__ */ new Map();
7213
7052
  const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache, session.id);
7214
7053
  if (resolved.tips.length > 0) {
7215
- refTips = resolved.tips;
7054
+ refTips = normalizeTips6(resolved.tips);
7216
7055
  params = resolved.params;
7217
7056
  }
7218
7057
  }
@@ -7268,36 +7107,43 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7268
7107
  snapshotHintShown.add(session);
7269
7108
  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";
7270
7109
  }
7271
- const merged = [...raw.tips || [], ...smartTips || [], ...snapshotHint ? [snapshotHint] : [], ...refTips];
7110
+ const merged = [
7111
+ ...raw.tips || [],
7112
+ ...normalizeTips6(smartTips),
7113
+ ...snapshotHint ? normalizeTips6([snapshotHint]) : [],
7114
+ ...refTips
7115
+ ];
7272
7116
  const isSuccess = raw.success !== false;
7117
+ const mergedOrRaw = merged.length > 0 ? merged : raw.tips || [];
7273
7118
  recordArchive(session?.id, sessionName, {
7274
7119
  step: 0,
7275
7120
  command: commandName,
7276
7121
  params,
7277
- result: { success: isSuccess, data: raw.data, message: raw.message, tips: merged.length > 0 ? merged : raw.tips || [] },
7122
+ result: { success: isSuccess, data: raw.data, message: raw.message, tips: tipsToMessages(mergedOrRaw) },
7278
7123
  toolCalls: [],
7279
7124
  duration,
7280
7125
  timestamp: start
7281
7126
  });
7282
7127
  if (isSuccess) {
7283
- return { ...ok26(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
7128
+ return { ...ok25(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
7284
7129
  }
7285
- return { success: false, data: raw.data, message: raw.message, tips: merged.length > 0 ? merged : raw.tips || [], duration, ...hookOutputs ? { hookOutputs } : {} };
7130
+ return { success: false, data: raw.data, message: raw.message, tips: mergedOrRaw, duration, ...hookOutputs ? { hookOutputs } : {} };
7286
7131
  }
7132
+ const smartTipNormalized = normalizeTips6(smartTips);
7287
7133
  recordArchive(session?.id, sessionName, {
7288
7134
  step: 0,
7289
7135
  command: commandName,
7290
7136
  params,
7291
- result: { success: true, data: raw, tips: smartTips || [] },
7137
+ result: { success: true, data: raw, tips: tipsToMessages(smartTipNormalized) },
7292
7138
  toolCalls: [],
7293
7139
  duration,
7294
7140
  timestamp: start
7295
7141
  });
7296
- return { ...ok26(raw, smartTips), duration, ...hookOutputs ? { hookOutputs } : {} };
7142
+ return { ...ok25(raw, smartTipNormalized), duration, ...hookOutputs ? { hookOutputs } : {} };
7297
7143
  } catch (err) {
7298
7144
  const end = Date.now();
7299
7145
  const duration = end - start;
7300
- const errorMessage = err.message;
7146
+ const errorMessage = errMsg(err);
7301
7147
  if (session) {
7302
7148
  streamCommandEvent(session.id, {
7303
7149
  sessionId: session.id,
@@ -7343,8 +7189,8 @@ async function executeChain(input, options) {
7343
7189
  });
7344
7190
  }
7345
7191
  try {
7346
- for (const pipeline2 of pipelines) {
7347
- const { type, pipeline: commands } = pipeline2;
7192
+ for (const pipeline of pipelines) {
7193
+ const { type, pipeline: commands } = pipeline;
7348
7194
  for (const cmdStr of commands) {
7349
7195
  const parts = splitCommand(cmdStr);
7350
7196
  if (parts.length === 0) continue;
@@ -7409,10 +7255,51 @@ async function executeChain(input, options) {
7409
7255
  },
7410
7256
  config: {},
7411
7257
  site,
7412
- cliName: "xbrowser"
7258
+ cliName: "xbrowser",
7259
+ tips: new TipCollector2()
7413
7260
  };
7414
7261
  const start2 = Date.now();
7415
7262
  try {
7263
+ const loginGuard = await checkPluginLoginRequired({
7264
+ site,
7265
+ command: cmdEntry,
7266
+ commandName: subCommand,
7267
+ ctx: pluginCtx,
7268
+ page: session?.page,
7269
+ sessionName
7270
+ });
7271
+ if (!loginGuard.ok) {
7272
+ const duration3 = Date.now() - start2;
7273
+ const data2 = loginGuard.data ?? null;
7274
+ recordArchive(session.id, sessionName, {
7275
+ step: results.length,
7276
+ command: `${cmdName} ${subCommand}`,
7277
+ params: pluginParams,
7278
+ result: { success: false, data: data2, message: loginGuard.message, tips: loginGuard.tips || [] },
7279
+ toolCalls: [],
7280
+ duration: duration3,
7281
+ timestamp: start2
7282
+ });
7283
+ results.push({
7284
+ command: `${cmdName} ${subCommand}`,
7285
+ raw: cmdStr,
7286
+ success: false,
7287
+ data: data2,
7288
+ message: loginGuard.message,
7289
+ tips: normalizeTips6(loginGuard.tips),
7290
+ duration: duration3
7291
+ });
7292
+ if (type === "and") {
7293
+ return {
7294
+ success: false,
7295
+ steps: results,
7296
+ totalDuration: Date.now() - totalStart,
7297
+ stoppedAt: results.length,
7298
+ stoppedReason: `Command '${cmdName} ${subCommand}' failed (&& chain): ${loginGuard.message}`
7299
+ };
7300
+ }
7301
+ continue;
7302
+ }
7416
7303
  const hooks = await loadHooks();
7417
7304
  if (hooks.length > 0) {
7418
7305
  await Promise.all(hooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${cmdName} ${subCommand}`, params: pluginParams })));
@@ -7433,7 +7320,7 @@ async function executeChain(input, options) {
7433
7320
  step: results.length,
7434
7321
  command: `${cmdName} ${subCommand}`,
7435
7322
  params: pluginParams,
7436
- result: { success: true, data, tips: raw?.tips || [] },
7323
+ result: { success: true, data, tips: tipsToMessages(raw?.tips) },
7437
7324
  toolCalls: [],
7438
7325
  duration: duration2,
7439
7326
  timestamp: start2
@@ -7441,7 +7328,7 @@ async function executeChain(input, options) {
7441
7328
  results.push({
7442
7329
  command: `${cmdName} ${subCommand}`,
7443
7330
  raw: cmdStr,
7444
- ...ok26(data),
7331
+ ...ok25(data),
7445
7332
  duration: duration2,
7446
7333
  ...hookOutputs ? { hookOutputs } : {}
7447
7334
  });
@@ -7456,7 +7343,7 @@ async function executeChain(input, options) {
7456
7343
  }
7457
7344
  } catch (err) {
7458
7345
  const duration2 = Date.now() - start2;
7459
- const errorMessage = err.message;
7346
+ const errorMessage = errMsg(err);
7460
7347
  recordArchive(session.id, sessionName, {
7461
7348
  step: results.length,
7462
7349
  command: `${cmdName} ${subCommand}`,
@@ -7484,7 +7371,7 @@ async function executeChain(input, options) {
7484
7371
  }
7485
7372
  continue;
7486
7373
  }
7487
- const { params } = parseCommandArgs(cmdName, cmdArgs);
7374
+ const { params } = parseCommandArgs(cmdName, cmdArgs, unquote2);
7488
7375
  if (cmdName === "goto" && params.url) {
7489
7376
  const existing2 = await findOrRestoreSession(sessionName, options?.cdpEndpoint);
7490
7377
  if (!existing2) {
@@ -7545,15 +7432,6 @@ function isChainInput(input) {
7545
7432
  }
7546
7433
 
7547
7434
  // src/session/session-client.ts
7548
- function sessionToInfo(s) {
7549
- return { id: s.id, name: s.name, url: s.page.url(), createdAt: s.createdAt, cdpEndpoint: s.cdpEndpoint };
7550
- }
7551
- async function openSession(name, url, options) {
7552
- const session = await createSession(name, url, { cdpEndpoint: options?.cdpEndpoint });
7553
- const info = sessionToInfo(session);
7554
- saveSessionDiskMeta(name, info);
7555
- return info;
7556
- }
7557
7435
  async function closeSession(name) {
7558
7436
  await closeSessionByName(name);
7559
7437
  }
@@ -7567,53 +7445,22 @@ function handleSessionHelp() {
7567
7445
  "Usage: xbrowser session <command> [options]",
7568
7446
  "",
7569
7447
  "Commands:",
7570
- " open <url> [--name <name>] Open browser and create session",
7571
7448
  " close [--name <name>] Close session",
7572
7449
  " list, ls List active sessions",
7573
7450
  " kill [--name <name>] Kill session forcefully",
7451
+ " kill-all Kill all sessions and daemon",
7574
7452
  "",
7575
7453
  "Options:",
7576
7454
  ' --name <name> Session name (default: "default")',
7577
7455
  "",
7456
+ "Note: Sessions are auto-created via --session global option.",
7457
+ "",
7578
7458
  "Examples:",
7579
- " xbrowser session open https://example.com",
7580
- " xbrowser session open https://example.com --name mypage",
7459
+ " xbrowser goto https://example.com --session mypage",
7581
7460
  " xbrowser session close --name mypage",
7582
7461
  " xbrowser session list"
7583
7462
  ].join("\n");
7584
7463
  }
7585
- var sessionOpenBuiltin = {
7586
- name: "session open",
7587
- description: "Open browser and create session",
7588
- help: {
7589
- usage: "xbrowser session open <url> [--name <name>]",
7590
- description: "Open URL and create a browser session",
7591
- options: [{ name: "--name <name>", description: 'Session name (default: "default")' }],
7592
- examples: [
7593
- { cmd: "xbrowser session open https://example.com", description: "Open example.com" },
7594
- {
7595
- cmd: "xbrowser session open https://example.com --name test",
7596
- description: "Open with custom name"
7597
- }
7598
- ]
7599
- },
7600
- execute: async (args, options) => {
7601
- const [url] = args;
7602
- const name = options["name"] || "default";
7603
- if (!url) {
7604
- console.log("Usage: xbrowser session open <url> [--name <name>]");
7605
- process.exit(1);
7606
- }
7607
- try {
7608
- const info = await openSession(name, url);
7609
- console.log(`Session "${info.name}" opened: ${info.url}`);
7610
- console.log(`ID: ${info.id}`);
7611
- } catch (e) {
7612
- console.error("Error:", e instanceof Error ? e.message : String(e));
7613
- process.exit(1);
7614
- }
7615
- }
7616
- };
7617
7464
  var sessionCloseBuiltin = {
7618
7465
  name: "session close",
7619
7466
  description: "Close browser session",
@@ -7695,22 +7542,17 @@ var sessionKillBuiltin = {
7695
7542
  };
7696
7543
 
7697
7544
  // src/config.ts
7698
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
7699
- import { join as join4 } from "path";
7700
- import { homedir as homedir4, tmpdir } from "os";
7701
- function getConfigFile() {
7702
- return join4(homedir4() || tmpdir(), ".xbrowser", "config.json");
7545
+ import { homedir as homedir6, tmpdir } from "os";
7546
+ import { join as join6 } from "path";
7547
+ import { loadConfig as coreLoadConfig, saveConfig as coreSaveConfig } from "@dyyz1993/xcli-core";
7548
+ function getConfigSource() {
7549
+ return { configDir: join6(homedir6() || tmpdir(), ".xbrowser") };
7703
7550
  }
7704
7551
  function loadConfig() {
7705
- const configFile = getConfigFile();
7706
- if (!existsSync6(configFile)) return {};
7707
- return readJsonFile(configFile, {});
7552
+ return coreLoadConfig(getConfigSource());
7708
7553
  }
7709
7554
  function saveConfig(config) {
7710
- const dir = join4(homedir4() || tmpdir(), ".xbrowser");
7711
- const configFile = getConfigFile();
7712
- if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
7713
- writeFileSync5(configFile, JSON.stringify(config, null, 2), "utf-8");
7555
+ coreSaveConfig(getConfigSource(), config);
7714
7556
  }
7715
7557
  function getConfigValue(key) {
7716
7558
  return loadConfig()[key];
@@ -7818,38 +7660,69 @@ var configBuiltin = {
7818
7660
 
7819
7661
  // src/plugin/installer.ts
7820
7662
  import {
7821
- existsSync as existsSync13,
7822
- readdirSync as readdirSync3,
7663
+ existsSync as existsSync10,
7664
+ readdirSync as readdirSync2,
7823
7665
  mkdirSync as mkdirSync8,
7824
- rmSync as rmSync7
7666
+ rmSync as rmSync6
7825
7667
  } from "fs";
7826
- import { resolve as resolve15, basename as basename2 } from "path";
7827
- import { homedir as homedir5 } from "os";
7668
+ import { resolve as resolve8, basename as basename2 } from "path";
7669
+ import { homedir as homedir7 } from "os";
7828
7670
 
7829
7671
  // src/plugin/install-sources/local.ts
7830
- import { existsSync as existsSync8, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
7831
- import { resolve as resolve10 } from "path";
7672
+ import { existsSync as existsSync5, cpSync, rmSync } from "fs";
7673
+ import { resolve as resolve3 } from "path";
7674
+ import { verifyPlugin, safeCleanup } from "@dyyz1993/xcli-core";
7675
+ async function installFromLocal(source, name, targetDir) {
7676
+ const srcPath = resolve3(source);
7677
+ if (!existsSync5(srcPath)) {
7678
+ throw new Error(`Local path does not exist: ${srcPath}`);
7679
+ }
7680
+ const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
7681
+ let warnings = [];
7682
+ try {
7683
+ cpSync(srcPath, tmpTarget, { recursive: true });
7684
+ const verify = verifyPlugin(tmpTarget, { metadataField: "xbrowser" });
7685
+ warnings = verify.warnings ?? [];
7686
+ if (!verify.valid) {
7687
+ safeCleanup(tmpTarget);
7688
+ throw new Error(`Invalid plugin: ${verify.error}`);
7689
+ }
7690
+ if (existsSync5(targetDir)) {
7691
+ rmSync(targetDir, { recursive: true, force: true });
7692
+ }
7693
+ cpSync(tmpTarget, targetDir, { recursive: true, force: true });
7694
+ safeCleanup(tmpTarget);
7695
+ } catch (err) {
7696
+ safeCleanup(tmpTarget);
7697
+ throw err;
7698
+ }
7699
+ return {
7700
+ id: name,
7701
+ name,
7702
+ path: targetDir,
7703
+ source: "local",
7704
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
7705
+ warnings
7706
+ };
7707
+ }
7832
7708
 
7833
- // src/plugin/install-utils.ts
7709
+ // src/plugin/install-sources/npm.ts
7710
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync6, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
7711
+ import { resolve as resolve4, join as join7 } from "path";
7712
+ import { tmpdir as tmpdir2 } from "os";
7834
7713
  import {
7835
- existsSync as existsSync7,
7836
- readdirSync as readdirSync2,
7837
- cpSync,
7838
- rmSync,
7839
- mkdirSync as mkdirSync4,
7840
- readFileSync as readFileSync10,
7841
- createWriteStream
7842
- } from "fs";
7843
- import { resolve as resolve9 } from "path";
7844
- import { execSync as execSync7 } from "child_process";
7845
- import { pipeline } from "stream/promises";
7846
- import { Readable } from "stream";
7714
+ downloadToFile,
7715
+ extractTarGz,
7716
+ flattenPackageRoot,
7717
+ verifyPlugin as verifyPlugin2,
7718
+ safeCleanup as safeCleanup2
7719
+ } from "@dyyz1993/xcli-core";
7847
7720
 
7848
7721
  // src/utils/proxy-fetch.ts
7849
- var patched = false;
7722
+ var patched2 = false;
7850
7723
  async function ensureProxyFetch() {
7851
- if (patched) return;
7852
- patched = true;
7724
+ if (patched2) return;
7725
+ patched2 = true;
7853
7726
  if (process.env.https_proxy && !process.env.HTTPS_PROXY) {
7854
7727
  process.env.HTTPS_PROXY = process.env.https_proxy;
7855
7728
  }
@@ -7873,7 +7746,8 @@ async function ensureProxyFetch() {
7873
7746
  const body = init?.body;
7874
7747
  if (body instanceof globalThis.FormData && !(body instanceof UFormData)) {
7875
7748
  const ufd = new UFormData();
7876
- body.forEach((value, key) => {
7749
+ const domFormData = body;
7750
+ domFormData.forEach((value, key) => {
7877
7751
  if (value instanceof Blob) {
7878
7752
  ufd.append(key, value, value.name || "file");
7879
7753
  } else {
@@ -7889,113 +7763,7 @@ async function ensureProxyFetch() {
7889
7763
  }
7890
7764
  }
7891
7765
 
7892
- // src/plugin/install-utils.ts
7893
- async function downloadToFile(url, destPath) {
7894
- await ensureProxyFetch();
7895
- if (url.startsWith("file://")) {
7896
- const filePath = decodeURIComponent(new URL(url).pathname);
7897
- cpSync(filePath, destPath, { force: true });
7898
- return;
7899
- }
7900
- const res = await fetch(url);
7901
- if (!res.ok) {
7902
- throw new Error(`Download failed: HTTP ${res.status} from ${url}`);
7903
- }
7904
- if (!res.body) {
7905
- throw new Error(`No response body from ${url}`);
7906
- }
7907
- const nodeStream = Readable.fromWeb(res.body);
7908
- await pipeline(nodeStream, createWriteStream(destPath));
7909
- }
7910
- function extractTarGz(tarballPath, targetDir) {
7911
- mkdirSync4(targetDir, { recursive: true });
7912
- execSync7(`tar -xzf "${tarballPath}" -C "${targetDir}"`, { stdio: "pipe" });
7913
- }
7914
- function flattenPackageRoot(targetDir) {
7915
- const entries = readdirSync2(targetDir, { withFileTypes: true });
7916
- const dirs = entries.filter((e) => e.isDirectory());
7917
- const files = entries.filter((e) => !e.isDirectory());
7918
- if (dirs.length === 1 && files.length === 0) {
7919
- const pkgDir = resolve9(targetDir, dirs[0].name);
7920
- const items = readdirSync2(pkgDir);
7921
- for (const item of items) {
7922
- const src = resolve9(pkgDir, item);
7923
- const dst = resolve9(targetDir, item);
7924
- cpSync(src, dst, { recursive: true, force: true });
7925
- }
7926
- rmSync(pkgDir, { recursive: true, force: true });
7927
- }
7928
- }
7929
- async function verifyPlugin(dir) {
7930
- const warnings = [];
7931
- const indexPath = resolve9(dir, "index.ts");
7932
- if (!existsSync7(indexPath)) {
7933
- const indexJs = resolve9(dir, "index.js");
7934
- if (!existsSync7(indexJs)) {
7935
- return { valid: false, error: "No index.ts or index.js entry point found", warnings };
7936
- }
7937
- }
7938
- const pkgPath = resolve9(dir, "package.json");
7939
- if (!existsSync7(pkgPath)) {
7940
- warnings.push("No package.json found");
7941
- } else {
7942
- try {
7943
- const pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
7944
- if (!pkg2.xbrowser) {
7945
- warnings.push("No xbrowser metadata in package.json");
7946
- }
7947
- } catch {
7948
- warnings.push("Invalid package.json");
7949
- }
7950
- }
7951
- return { valid: true, warnings };
7952
- }
7953
- function safeCleanup(dir) {
7954
- try {
7955
- rmSync(dir, { recursive: true, force: true });
7956
- } catch {
7957
- }
7958
- }
7959
-
7960
- // src/plugin/install-sources/local.ts
7961
- async function installFromLocal(source, name, targetDir) {
7962
- const srcPath = resolve10(source);
7963
- if (!existsSync8(srcPath)) {
7964
- throw new Error(`Local path does not exist: ${srcPath}`);
7965
- }
7966
- const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
7967
- let warnings = [];
7968
- try {
7969
- cpSync2(srcPath, tmpTarget, { recursive: true });
7970
- const verify = await verifyPlugin(tmpTarget);
7971
- warnings = verify.warnings ?? [];
7972
- if (!verify.valid) {
7973
- safeCleanup(tmpTarget);
7974
- throw new Error(`Invalid plugin: ${verify.error}`);
7975
- }
7976
- if (existsSync8(targetDir)) {
7977
- rmSync2(targetDir, { recursive: true, force: true });
7978
- }
7979
- cpSync2(tmpTarget, targetDir, { recursive: true, force: true });
7980
- safeCleanup(tmpTarget);
7981
- } catch (err) {
7982
- safeCleanup(tmpTarget);
7983
- throw err;
7984
- }
7985
- return {
7986
- id: name,
7987
- name,
7988
- path: targetDir,
7989
- source: "local",
7990
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
7991
- warnings
7992
- };
7993
- }
7994
-
7995
7766
  // src/plugin/install-sources/npm.ts
7996
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
7997
- import { resolve as resolve11, join as join5 } from "path";
7998
- import { tmpdir as tmpdir2 } from "os";
7999
7767
  async function installFromNpm(packageName, name, targetDir) {
8000
7768
  await ensureProxyFetch();
8001
7769
  const encodedName = encodeURIComponent(packageName);
@@ -8013,34 +7781,34 @@ async function installFromNpm(packageName, name, targetDir) {
8013
7781
  throw new Error(`No tarball URL for ${packageName}@${latestVersion}`);
8014
7782
  }
8015
7783
  const tarballUrl = versionMeta.dist.tarball;
8016
- const tmpDir = join5(tmpdir2(), `xbrowser-npm-${Date.now()}`);
7784
+ const tmpDir = join7(tmpdir2(), `xbrowser-npm-${Date.now()}`);
8017
7785
  mkdirSync5(tmpDir, { recursive: true });
8018
7786
  let warnings = [];
8019
7787
  try {
8020
- const tarballPath = join5(tmpDir, `${name}.tgz`);
7788
+ const tarballPath = join7(tmpDir, `${name}.tgz`);
8021
7789
  await downloadToFile(tarballUrl, tarballPath);
8022
- const extractDir = join5(tmpDir, "extracted");
7790
+ const extractDir = join7(tmpDir, "extracted");
8023
7791
  extractTarGz(tarballPath, extractDir);
8024
7792
  flattenPackageRoot(extractDir);
8025
- const verify = await verifyPlugin(extractDir);
7793
+ const verify = verifyPlugin2(extractDir, { metadataField: "xbrowser" });
8026
7794
  warnings = verify.warnings ?? [];
8027
7795
  if (!verify.valid) {
8028
7796
  throw new Error(`Invalid npm plugin: ${verify.error}`);
8029
7797
  }
8030
- if (existsSync9(targetDir)) {
8031
- rmSync3(targetDir, { recursive: true, force: true });
7798
+ if (existsSync6(targetDir)) {
7799
+ rmSync2(targetDir, { recursive: true, force: true });
8032
7800
  }
8033
- cpSync3(extractDir, targetDir, { recursive: true, force: true });
8034
- const pkgPath = resolve11(targetDir, "package.json");
8035
- if (existsSync9(pkgPath)) {
8036
- const pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7801
+ cpSync2(extractDir, targetDir, { recursive: true, force: true });
7802
+ const pkgPath = resolve4(targetDir, "package.json");
7803
+ if (existsSync6(pkgPath)) {
7804
+ const pkg2 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
8037
7805
  if (!pkg2._npmSource) {
8038
7806
  pkg2._npmSource = { name: packageName, version: latestVersion };
8039
7807
  writeFileSync6(pkgPath, JSON.stringify(pkg2, null, 2));
8040
7808
  }
8041
7809
  }
8042
7810
  } finally {
8043
- safeCleanup(tmpDir);
7811
+ safeCleanup2(tmpDir);
8044
7812
  }
8045
7813
  return {
8046
7814
  id: name,
@@ -8053,35 +7821,36 @@ async function installFromNpm(packageName, name, targetDir) {
8053
7821
  }
8054
7822
 
8055
7823
  // src/plugin/install-sources/git.ts
8056
- import { existsSync as existsSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync7, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
8057
- import { resolve as resolve12, join as join6 } from "path";
7824
+ import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
7825
+ import { resolve as resolve5, join as join8 } from "path";
8058
7826
  import { tmpdir as tmpdir3 } from "os";
8059
- import { execSync as execSync8 } from "child_process";
7827
+ import { execSync as execSync2 } from "child_process";
7828
+ import { verifyPlugin as verifyPlugin3, safeCleanup as safeCleanup3 } from "@dyyz1993/xcli-core";
8060
7829
  async function installFromGit(gitUrl, name, targetDir) {
8061
- const tmpDir = join6(tmpdir3(), `xbrowser-git-${Date.now()}`);
7830
+ const tmpDir = join8(tmpdir3(), `xbrowser-git-${Date.now()}`);
8062
7831
  let warnings = [];
8063
7832
  try {
8064
- execSync8(`git clone --depth 1 "${gitUrl}" "${tmpDir}"`, { stdio: "pipe" });
8065
- const verify = await verifyPlugin(tmpDir);
7833
+ execSync2(`git clone --depth 1 "${gitUrl}" "${tmpDir}"`, { stdio: "pipe" });
7834
+ const verify = verifyPlugin3(tmpDir, { metadataField: "xbrowser" });
8066
7835
  warnings = verify.warnings ?? [];
8067
7836
  if (!verify.valid) {
8068
7837
  throw new Error(`Invalid git plugin: ${verify.error}`);
8069
7838
  }
8070
- if (existsSync10(targetDir)) {
8071
- rmSync4(targetDir, { recursive: true, force: true });
7839
+ if (existsSync7(targetDir)) {
7840
+ rmSync3(targetDir, { recursive: true, force: true });
8072
7841
  }
8073
- cpSync4(tmpDir, targetDir, { recursive: true, force: true });
8074
- rmSync4(resolve12(targetDir, ".git"), { recursive: true, force: true });
8075
- const pkgPath = resolve12(targetDir, "package.json");
8076
- if (existsSync10(pkgPath)) {
8077
- const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7842
+ cpSync3(tmpDir, targetDir, { recursive: true, force: true });
7843
+ rmSync3(resolve5(targetDir, ".git"), { recursive: true, force: true });
7844
+ const pkgPath = resolve5(targetDir, "package.json");
7845
+ if (existsSync7(pkgPath)) {
7846
+ const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
8078
7847
  if (!pkg2._gitSource) {
8079
7848
  pkg2._gitSource = { url: gitUrl };
8080
7849
  writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
8081
7850
  }
8082
7851
  }
8083
7852
  } finally {
8084
- safeCleanup(tmpDir);
7853
+ safeCleanup3(tmpDir);
8085
7854
  }
8086
7855
  return {
8087
7856
  id: name,
@@ -8094,39 +7863,46 @@ async function installFromGit(gitUrl, name, targetDir) {
8094
7863
  }
8095
7864
 
8096
7865
  // src/plugin/install-sources/url.ts
8097
- import { existsSync as existsSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, rmSync as rmSync5, cpSync as cpSync5 } from "fs";
8098
- import { resolve as resolve13, join as join7, basename } from "path";
7866
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync8, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
7867
+ import { resolve as resolve6, join as join9, basename } from "path";
8099
7868
  import { tmpdir as tmpdir4 } from "os";
7869
+ import {
7870
+ downloadToFile as downloadToFile2,
7871
+ extractTarGz as extractTarGz2,
7872
+ flattenPackageRoot as flattenPackageRoot2,
7873
+ verifyPlugin as verifyPlugin4,
7874
+ safeCleanup as safeCleanup4
7875
+ } from "@dyyz1993/xcli-core";
8100
7876
  async function installFromUrl(url, name, targetDir) {
8101
- const tmpDir = join7(tmpdir4(), `xbrowser-url-${Date.now()}`);
7877
+ const tmpDir = join9(tmpdir4(), `xbrowser-url-${Date.now()}`);
8102
7878
  mkdirSync6(tmpDir, { recursive: true });
8103
7879
  let warnings = [];
8104
7880
  try {
8105
7881
  const fileName = basename(new URL(url).pathname) || "plugin.tar.gz";
8106
- const tarballPath = join7(tmpDir, fileName);
8107
- await downloadToFile(url, tarballPath);
8108
- const extractDir = join7(tmpDir, "extracted");
8109
- extractTarGz(tarballPath, extractDir);
8110
- flattenPackageRoot(extractDir);
8111
- const verify = await verifyPlugin(extractDir);
7882
+ const tarballPath = join9(tmpDir, fileName);
7883
+ await downloadToFile2(url, tarballPath);
7884
+ const extractDir = join9(tmpDir, "extracted");
7885
+ extractTarGz2(tarballPath, extractDir);
7886
+ flattenPackageRoot2(extractDir);
7887
+ const verify = verifyPlugin4(extractDir, { metadataField: "xbrowser" });
8112
7888
  warnings = verify.warnings ?? [];
8113
7889
  if (!verify.valid) {
8114
7890
  throw new Error(`Invalid plugin from URL: ${verify.error}`);
8115
7891
  }
8116
- if (existsSync11(targetDir)) {
8117
- rmSync5(targetDir, { recursive: true, force: true });
7892
+ if (existsSync8(targetDir)) {
7893
+ rmSync4(targetDir, { recursive: true, force: true });
8118
7894
  }
8119
- cpSync5(extractDir, targetDir, { recursive: true, force: true });
8120
- const pkgPath = resolve13(targetDir, "package.json");
8121
- if (existsSync11(pkgPath)) {
8122
- const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
7895
+ cpSync4(extractDir, targetDir, { recursive: true, force: true });
7896
+ const pkgPath = resolve6(targetDir, "package.json");
7897
+ if (existsSync8(pkgPath)) {
7898
+ const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
8123
7899
  if (!pkg2._urlSource) {
8124
7900
  pkg2._urlSource = { url };
8125
7901
  writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
8126
7902
  }
8127
7903
  }
8128
7904
  } finally {
8129
- safeCleanup(tmpDir);
7905
+ safeCleanup4(tmpDir);
8130
7906
  }
8131
7907
  return {
8132
7908
  id: name,
@@ -8140,16 +7916,23 @@ async function installFromUrl(url, name, targetDir) {
8140
7916
 
8141
7917
  // src/plugin/install-sources/marketplace.ts
8142
7918
  import {
8143
- existsSync as existsSync12,
7919
+ existsSync as existsSync9,
8144
7920
  mkdirSync as mkdirSync7,
8145
7921
  writeFileSync as writeFileSync9,
8146
- readFileSync as readFileSync14,
8147
- rmSync as rmSync6,
8148
- cpSync as cpSync6
7922
+ readFileSync as readFileSync7,
7923
+ rmSync as rmSync5,
7924
+ cpSync as cpSync5
8149
7925
  } from "fs";
8150
- import { resolve as resolve14, join as join8, dirname as dirname2 } from "path";
7926
+ import { resolve as resolve7, join as join10, dirname as dirname2 } from "path";
8151
7927
  import { tmpdir as tmpdir5 } from "os";
8152
7928
  import { gunzipSync } from "zlib";
7929
+ import {
7930
+ downloadToFile as downloadToFile3,
7931
+ extractTarGz as extractTarGz3,
7932
+ flattenPackageRoot as flattenPackageRoot3,
7933
+ verifyPlugin as verifyPlugin5,
7934
+ safeCleanup as safeCleanup5
7935
+ } from "@dyyz1993/xcli-core";
8153
7936
  async function installFromMarketplace(pluginsDir, slug, options) {
8154
7937
  await ensureProxyFetch();
8155
7938
  const baseUrl = getMarketplaceUrl();
@@ -8164,24 +7947,24 @@ async function installFromMarketplace(pluginsDir, slug, options) {
8164
7947
  }
8165
7948
  const plugin = detailData.data;
8166
7949
  const name = options?.name || String(plugin.slug || slug);
8167
- const targetDir = resolve14(pluginsDir, name);
8168
- if (existsSync12(targetDir) && !options?.force) {
7950
+ const targetDir = resolve7(pluginsDir, name);
7951
+ if (existsSync9(targetDir) && !options?.force) {
8169
7952
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
8170
7953
  }
8171
7954
  mkdirSync7(targetDir, { recursive: true });
8172
- const tmpDir = join8(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
7955
+ const tmpDir = join10(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
8173
7956
  mkdirSync7(tmpDir, { recursive: true });
8174
7957
  const realSlug = String(plugin.slug || slug);
8175
7958
  try {
8176
7959
  await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
8177
7960
  } finally {
8178
- safeCleanup(tmpDir);
7961
+ safeCleanup5(tmpDir);
8179
7962
  }
8180
7963
  writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir);
8181
7964
  ensureIndexFile(plugin, name, targetDir);
8182
- const verify = await verifyPlugin(targetDir);
7965
+ const verify = verifyPlugin5(targetDir, { metadataField: "xbrowser" });
8183
7966
  if (!verify.valid) {
8184
- safeCleanup(targetDir);
7967
+ safeCleanup5(targetDir);
8185
7968
  throw new Error(`Invalid marketplace plugin: ${verify.error}`);
8186
7969
  }
8187
7970
  const trackUrl = `${baseUrl}/api/plugins/${realSlug}/install`;
@@ -8200,12 +7983,12 @@ function isManifestArray(data) {
8200
7983
  return Array.isArray(data) && data.length > 0 && typeof data[0].path === "string" && typeof data[0].content === "string";
8201
7984
  }
8202
7985
  function extractManifestToDir(manifest, targetDir) {
8203
- if (existsSync12(targetDir)) {
8204
- rmSync6(targetDir, { recursive: true, force: true });
7986
+ if (existsSync9(targetDir)) {
7987
+ rmSync5(targetDir, { recursive: true, force: true });
8205
7988
  }
8206
7989
  mkdirSync7(targetDir, { recursive: true });
8207
7990
  for (const file of manifest) {
8208
- const filePath = resolve14(targetDir, file.path);
7991
+ const filePath = resolve7(targetDir, file.path);
8209
7992
  mkdirSync7(dirname2(filePath), { recursive: true });
8210
7993
  writeFileSync9(filePath, Buffer.from(file.content, "base64"));
8211
7994
  }
@@ -8232,21 +8015,21 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
8232
8015
  }
8233
8016
  if (tarballRes.status === 302 || tarballRes.headers.get("location")) {
8234
8017
  const redirectUrl = tarballRes.headers.get("location");
8235
- const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
8236
- await downloadToFile(redirectUrl, tarballPath);
8237
- const buffer = readFileSync14(tarballPath);
8018
+ const tarballPath = join10(tmpDir, `${slug}.tar.gz`);
8019
+ await downloadToFile3(redirectUrl, tarballPath);
8020
+ const buffer = readFileSync7(tarballPath);
8238
8021
  const manifest = tryParseAsGzippedManifest(buffer);
8239
8022
  if (manifest) {
8240
8023
  extractManifestToDir(manifest, targetDir);
8241
8024
  return;
8242
8025
  }
8243
- const extractDir = join8(tmpDir, "extracted");
8244
- extractTarGz(tarballPath, extractDir);
8245
- flattenPackageRoot(extractDir);
8246
- if (existsSync12(targetDir)) {
8247
- rmSync6(targetDir, { recursive: true, force: true });
8026
+ const extractDir = join10(tmpDir, "extracted");
8027
+ extractTarGz3(tarballPath, extractDir);
8028
+ flattenPackageRoot3(extractDir);
8029
+ if (existsSync9(targetDir)) {
8030
+ rmSync5(targetDir, { recursive: true, force: true });
8248
8031
  }
8249
- cpSync6(extractDir, targetDir, { recursive: true, force: true });
8032
+ cpSync5(extractDir, targetDir, { recursive: true, force: true });
8250
8033
  } else {
8251
8034
  const buffer = Buffer.from(await tarballRes.arrayBuffer());
8252
8035
  const manifest = tryParseAsGzippedManifest(buffer);
@@ -8254,16 +8037,16 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
8254
8037
  extractManifestToDir(manifest, targetDir);
8255
8038
  return;
8256
8039
  }
8257
- const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
8040
+ const tarballPath = join10(tmpDir, `${slug}.tar.gz`);
8258
8041
  writeFileSync9(tarballPath, buffer);
8259
8042
  try {
8260
- const extractDir = join8(tmpDir, "extracted");
8261
- extractTarGz(tarballPath, extractDir);
8262
- flattenPackageRoot(extractDir);
8263
- if (existsSync12(targetDir)) {
8264
- rmSync6(targetDir, { recursive: true, force: true });
8043
+ const extractDir = join10(tmpDir, "extracted");
8044
+ extractTarGz3(tarballPath, extractDir);
8045
+ flattenPackageRoot3(extractDir);
8046
+ if (existsSync9(targetDir)) {
8047
+ rmSync5(targetDir, { recursive: true, force: true });
8265
8048
  }
8266
- cpSync6(extractDir, targetDir, { recursive: true, force: true });
8049
+ cpSync5(extractDir, targetDir, { recursive: true, force: true });
8267
8050
  } catch {
8268
8051
  throw new Error(
8269
8052
  `Downloaded tarball for "${slug}" is neither a gzipped JSON manifest nor a valid tar.gz archive.`
@@ -8295,12 +8078,12 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
8295
8078
  url: baseUrl
8296
8079
  }
8297
8080
  };
8298
- const pkgPath = resolve14(targetDir, "package.json");
8299
- if (!existsSync12(pkgPath)) {
8081
+ const pkgPath = resolve7(targetDir, "package.json");
8082
+ if (!existsSync9(pkgPath)) {
8300
8083
  writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
8301
8084
  } else {
8302
8085
  try {
8303
- const existing = JSON.parse(readFileSync14(pkgPath, "utf-8"));
8086
+ const existing = JSON.parse(readFileSync7(pkgPath, "utf-8"));
8304
8087
  const merged = {
8305
8088
  ...existing,
8306
8089
  xbrowser: { ...existing.xbrowser, ...packageJson.xbrowser },
@@ -8313,7 +8096,7 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
8313
8096
  }
8314
8097
  }
8315
8098
  function ensureIndexFile(plugin, name, targetDir) {
8316
- if (!existsSync12(resolve14(targetDir, "index.ts")) && !existsSync12(resolve14(targetDir, "index.js"))) {
8099
+ if (!existsSync9(resolve7(targetDir, "index.ts")) && !existsSync9(resolve7(targetDir, "index.js"))) {
8317
8100
  const commands = plugin.commands || [];
8318
8101
  const commandHandlers = commands.length > 0 ? commands.map((cmd) => {
8319
8102
  return [
@@ -8329,7 +8112,7 @@ function ensureIndexFile(plugin, name, targetDir) {
8329
8112
  ` });`
8330
8113
  ].join("\n");
8331
8114
  writeFileSync9(
8332
- resolve14(targetDir, "index.ts"),
8115
+ resolve7(targetDir, "index.ts"),
8333
8116
  [
8334
8117
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
8335
8118
  ``,
@@ -8350,7 +8133,7 @@ function ensureIndexFile(plugin, name, targetDir) {
8350
8133
  var PluginInstaller = class {
8351
8134
  pluginsDir;
8352
8135
  constructor(pluginsDir) {
8353
- this.pluginsDir = pluginsDir || resolve15(homedir5(), ".xbrowser/plugins");
8136
+ this.pluginsDir = pluginsDir || resolve8(homedir7(), ".xbrowser/plugins");
8354
8137
  }
8355
8138
  getPluginsDir() {
8356
8139
  return this.pluginsDir;
@@ -8366,8 +8149,8 @@ var PluginInstaller = class {
8366
8149
  async install(source, options) {
8367
8150
  const type = this.detectSourceType(source);
8368
8151
  const name = options?.name || this.deriveName(source, type);
8369
- const targetDir = resolve15(this.pluginsDir, name);
8370
- if (existsSync13(targetDir) && !options?.force) {
8152
+ const targetDir = resolve8(this.pluginsDir, name);
8153
+ if (existsSync10(targetDir) && !options?.force) {
8371
8154
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
8372
8155
  }
8373
8156
  mkdirSync8(targetDir, { recursive: true });
@@ -8427,11 +8210,11 @@ var PluginInstaller = class {
8427
8210
  * @throws If the plugin is not installed.
8428
8211
  */
8429
8212
  async uninstall(name) {
8430
- const targetDir = resolve15(this.pluginsDir, name);
8431
- if (!existsSync13(targetDir)) {
8213
+ const targetDir = resolve8(this.pluginsDir, name);
8214
+ if (!existsSync10(targetDir)) {
8432
8215
  throw new Error(`Plugin "${name}" not found`);
8433
8216
  }
8434
- rmSync7(targetDir, { recursive: true, force: true });
8217
+ rmSync6(targetDir, { recursive: true, force: true });
8435
8218
  }
8436
8219
  /**
8437
8220
  * List all installed plugins with metadata.
@@ -8439,18 +8222,18 @@ var PluginInstaller = class {
8439
8222
  * @returns Array of installed plugin information.
8440
8223
  */
8441
8224
  async list(_options) {
8442
- if (!existsSync13(this.pluginsDir)) return [];
8443
- const entries = readdirSync3(this.pluginsDir, { withFileTypes: true });
8225
+ if (!existsSync10(this.pluginsDir)) return [];
8226
+ const entries = readdirSync2(this.pluginsDir, { withFileTypes: true });
8444
8227
  const plugins = [];
8445
8228
  for (const entry of entries) {
8446
8229
  if (!entry.isDirectory()) continue;
8447
- const pluginPath = resolve15(this.pluginsDir, entry.name);
8448
- const indexPath = resolve15(pluginPath, "index.ts");
8449
- const indexJsPath = resolve15(pluginPath, "index.js");
8450
- if (!existsSync13(indexPath) && !existsSync13(indexJsPath)) continue;
8230
+ const pluginPath = resolve8(this.pluginsDir, entry.name);
8231
+ const indexPath = resolve8(pluginPath, "index.ts");
8232
+ const indexJsPath = resolve8(pluginPath, "index.js");
8233
+ if (!existsSync10(indexPath) && !existsSync10(indexJsPath)) continue;
8451
8234
  const metadata = PluginMetadataParser.parseFromPackageJson(pluginPath);
8452
8235
  let source = "local";
8453
- const pkg2 = readJsonFile(resolve15(pluginPath, "package.json"), {});
8236
+ const pkg2 = readJsonFile(resolve8(pluginPath, "package.json"), {});
8454
8237
  if (pkg2._marketplace) source = "marketplace";
8455
8238
  else if (pkg2._npmSource) source = "npm";
8456
8239
  else if (pkg2._gitSource) source = "git";
@@ -8474,10 +8257,10 @@ var PluginInstaller = class {
8474
8257
  }
8475
8258
  if (source.startsWith("file://")) {
8476
8259
  const filePath = decodeURIComponent(new URL(source).pathname);
8477
- if (existsSync13(filePath)) return "url";
8260
+ if (existsSync10(filePath)) return "url";
8478
8261
  }
8479
8262
  if (source.endsWith(".git") || source.includes("github.com/")) return "git";
8480
- if (existsSync13(resolve15(source))) return "local";
8263
+ if (existsSync10(resolve8(source))) return "local";
8481
8264
  return "npm";
8482
8265
  }
8483
8266
  deriveName(source, type) {
@@ -8879,7 +8662,7 @@ async function searchFromMarketplacePlugin(options, loader) {
8879
8662
  site: options.site,
8880
8663
  limit: options.limit
8881
8664
  },
8882
- {}
8665
+ createStubContext("marketplace")
8883
8666
  );
8884
8667
  const items = extractItems(result);
8885
8668
  return items.map((item) => ({
@@ -9311,7 +9094,7 @@ var previewBuiltin = {
9311
9094
  if (options.json) {
9312
9095
  outputResult({ running: false }, "json");
9313
9096
  } else {
9314
- console.log("Daemon is not running. Start with: xbrowser daemon start");
9097
+ console.log("Daemon is not running. It will start automatically when needed.");
9315
9098
  console.log("");
9316
9099
  console.log("Preview is automatically available when the daemon is running.");
9317
9100
  }
@@ -9335,9 +9118,199 @@ var previewBuiltin = {
9335
9118
  }
9336
9119
  };
9337
9120
 
9121
+ // src/builtins/knowledge.ts
9122
+ init_site_knowledge();
9123
+ import { existsSync as existsSync12 } from "fs";
9124
+ var knowledgeBuiltin = {
9125
+ name: "knowledge",
9126
+ description: "View LLM-readable site knowledge base (selectors, forms, APIs)",
9127
+ aliases: ["know"],
9128
+ help: {
9129
+ usage: "xbrowser knowledge <list|show|search|issue|path> [domain] [options]",
9130
+ description: "Manage auto-generated site knowledge from recordings. Knowledge is stored at ~/.xbrowser/knowledge/{domain}.md and is designed for LLM consumption.",
9131
+ options: [
9132
+ { name: "list", description: "List all domains with knowledge bases" },
9133
+ { name: "show <domain>", description: "Show full knowledge for a domain (markdown)" },
9134
+ { name: "search <domain> <query>", description: "Search selectors/APIs by keyword" },
9135
+ { name: "selectors <domain>", description: "List all selectors for a domain" },
9136
+ { name: "api <domain>", description: "List all API endpoints for a domain" },
9137
+ { name: "issue <domain> <text>", description: "Add a known issue to a domain" },
9138
+ { name: "path <domain>", description: "Show file path for a domain's knowledge" }
9139
+ ],
9140
+ examples: [
9141
+ { cmd: "xbrowser knowledge list", description: "List all known sites" },
9142
+ { cmd: "xbrowser knowledge show juejin.cn", description: "Show juejin.cn knowledge" },
9143
+ { cmd: "xbrowser knowledge search juejin.cn publish", description: 'Find selectors related to "publish"' },
9144
+ { cmd: "xbrowser knowledge selectors juejin.cn", description: "List all selectors" },
9145
+ { cmd: "xbrowser knowledge api juejin.cn", description: "List API endpoints" },
9146
+ { cmd: 'xbrowser knowledge issue juejin.cn "Title selector changed"', description: "Report an issue" }
9147
+ ]
9148
+ },
9149
+ execute: async (args, _options, _ctx) => {
9150
+ const [subcommand, ...rest] = args;
9151
+ if (!subcommand || subcommand === "list") {
9152
+ const domains = listSiteKnowledge();
9153
+ if (domains.length === 0) {
9154
+ console.log("No site knowledge bases found.");
9155
+ console.log("Knowledge is auto-generated when you run `xbrowser record stop`.");
9156
+ return;
9157
+ }
9158
+ console.log("Site Knowledge Bases:");
9159
+ console.log("");
9160
+ for (const domain of domains) {
9161
+ const kb = readSiteKnowledge(domain);
9162
+ if (kb) {
9163
+ const pageCount = Object.keys(kb.pages).length;
9164
+ const selCount = Object.values(kb.pages).reduce((sum, p) => sum + p.selectors.length, 0);
9165
+ const apiCount = Object.keys(kb.apiEndpoints).length;
9166
+ console.log(
9167
+ ` ${domain} \u2014 ${kb.recordingCount} recordings, ${pageCount} pages, ${selCount} selectors, ${apiCount} APIs`
9168
+ );
9169
+ }
9170
+ }
9171
+ return;
9172
+ }
9173
+ if (subcommand === "show") {
9174
+ const domain = rest[0];
9175
+ if (!domain) {
9176
+ console.error("Usage: xbrowser knowledge show <domain>");
9177
+ process.exit(1);
9178
+ }
9179
+ const md = readSiteKnowledgeMarkdown(domain);
9180
+ if (!md) {
9181
+ console.error(`No knowledge base found for ${domain}`);
9182
+ console.error("Run `xbrowser knowledge list` to see available domains.");
9183
+ process.exit(1);
9184
+ }
9185
+ console.log(md);
9186
+ return;
9187
+ }
9188
+ if (subcommand === "selectors") {
9189
+ const domain = rest[0];
9190
+ if (!domain) {
9191
+ console.error("Usage: xbrowser knowledge selectors <domain>");
9192
+ process.exit(1);
9193
+ }
9194
+ const kb = readSiteKnowledge(domain);
9195
+ if (!kb) {
9196
+ console.error(`No knowledge base found for ${domain}`);
9197
+ process.exit(1);
9198
+ }
9199
+ console.log(`Selectors for ${domain} (${kb.recordingCount} recordings):`);
9200
+ console.log("");
9201
+ for (const [pagePath, page] of Object.entries(kb.pages)) {
9202
+ if (page.selectors.length === 0) continue;
9203
+ console.log(` ${pagePath}:`);
9204
+ for (const sel of page.selectors) {
9205
+ const status = sel.status === "deprecated" ? " \u26A0\uFE0F" : "";
9206
+ console.log(
9207
+ ` ${sel.selector.padEnd(30)} ${sel.tag.padEnd(8)} ${sel.actionType.padEnd(10)} ${sel.confidence.padEnd(6)} ${sel.timesSeen}x${status}`
9208
+ );
9209
+ if (sel.description) console.log(` \u2192 ${sel.description}`);
9210
+ }
9211
+ console.log("");
9212
+ }
9213
+ return;
9214
+ }
9215
+ if (subcommand === "api") {
9216
+ const domain = rest[0];
9217
+ if (!domain) {
9218
+ console.error("Usage: xbrowser knowledge api <domain>");
9219
+ process.exit(1);
9220
+ }
9221
+ const kb = readSiteKnowledge(domain);
9222
+ if (!kb) {
9223
+ console.error(`No knowledge base found for ${domain}`);
9224
+ process.exit(1);
9225
+ }
9226
+ const endpoints = Object.values(kb.apiEndpoints);
9227
+ if (endpoints.length === 0) {
9228
+ console.log(`No API endpoints recorded for ${domain}`);
9229
+ return;
9230
+ }
9231
+ console.log(`API Endpoints for ${domain}:`);
9232
+ console.log("");
9233
+ for (const ep of endpoints.sort((a, b) => b.timesSeen - a.timesSeen)) {
9234
+ const params = ep.params.length > 0 ? ep.params.join(", ") : "-";
9235
+ console.log(` ${ep.method} ${ep.path} (${ep.timesSeen}x)`);
9236
+ console.log(` Params: ${params}`);
9237
+ if (ep.responseFields.length > 0) {
9238
+ console.log(` Response: ${ep.responseFields.slice(0, 5).join(", ")}`);
9239
+ }
9240
+ console.log("");
9241
+ }
9242
+ return;
9243
+ }
9244
+ if (subcommand === "search") {
9245
+ const domain = rest[0];
9246
+ const query = rest.slice(1).join(" ").toLowerCase();
9247
+ if (!domain || !query) {
9248
+ console.error("Usage: xbrowser knowledge search <domain> <query>");
9249
+ process.exit(1);
9250
+ }
9251
+ const kb = readSiteKnowledge(domain);
9252
+ if (!kb) {
9253
+ console.error(`No knowledge base found for ${domain}`);
9254
+ process.exit(1);
9255
+ }
9256
+ console.log(`Search results for "${query}" in ${domain}:`);
9257
+ console.log("");
9258
+ let found = 0;
9259
+ for (const [pagePath, page] of Object.entries(kb.pages)) {
9260
+ const matches = page.selectors.filter(
9261
+ (s) => s.selector.toLowerCase().includes(query) || s.description.toLowerCase().includes(query) || (s.text || "").toLowerCase().includes(query)
9262
+ );
9263
+ for (const m of matches) {
9264
+ console.log(` [${pagePath}] ${m.selector} \u2192 ${m.description} (${m.actionType}, ${m.confidence}, ${m.timesSeen}x)`);
9265
+ found++;
9266
+ }
9267
+ }
9268
+ for (const ep of Object.values(kb.apiEndpoints)) {
9269
+ if (ep.path.toLowerCase().includes(query) || ep.params.some((p) => p.toLowerCase().includes(query))) {
9270
+ console.log(` [API] ${ep.method} ${ep.path} (${ep.timesSeen}x)`);
9271
+ found++;
9272
+ }
9273
+ }
9274
+ if (found === 0) {
9275
+ console.log(" No matches found.");
9276
+ }
9277
+ return;
9278
+ }
9279
+ if (subcommand === "issue") {
9280
+ const domain = rest[0];
9281
+ const text = rest.slice(1).join(" ");
9282
+ if (!domain || !text) {
9283
+ console.error("Usage: xbrowser knowledge issue <domain> <description>");
9284
+ process.exit(1);
9285
+ }
9286
+ if (!readSiteKnowledge(domain)) {
9287
+ console.error(`No knowledge base found for ${domain}`);
9288
+ process.exit(1);
9289
+ }
9290
+ addKnownIssue(domain, text);
9291
+ console.log(`Added issue to ${domain}: ${text}`);
9292
+ return;
9293
+ }
9294
+ if (subcommand === "path") {
9295
+ const domain = rest[0];
9296
+ if (!domain) {
9297
+ console.error("Usage: xbrowser knowledge path <domain>");
9298
+ process.exit(1);
9299
+ }
9300
+ const mdPath = getKnowledgePath(domain, "md");
9301
+ const jsonPath = getKnowledgePath(domain, "json");
9302
+ console.log(`Markdown: ${mdPath} (${existsSync12(mdPath) ? "exists" : "not found"})`);
9303
+ console.log(`JSON: ${jsonPath} (${existsSync12(jsonPath) ? "exists" : "not found"})`);
9304
+ return;
9305
+ }
9306
+ console.error(`Unknown subcommand: ${subcommand}`);
9307
+ console.error("Usage: xbrowser knowledge <list|show|selectors|api|search|issue|path>");
9308
+ process.exit(1);
9309
+ }
9310
+ };
9311
+
9338
9312
  // src/builtins/index.ts
9339
9313
  var allBuiltins = [
9340
- sessionOpenBuiltin,
9341
9314
  sessionCloseBuiltin,
9342
9315
  sessionListBuiltin,
9343
9316
  sessionKillBuiltin,
@@ -9348,7 +9321,8 @@ var allBuiltins = [
9348
9321
  pluginListBuiltin,
9349
9322
  pluginReloadBuiltin,
9350
9323
  createBuiltin,
9351
- previewBuiltin
9324
+ previewBuiltin,
9325
+ knowledgeBuiltin
9352
9326
  ];
9353
9327
 
9354
9328
  // src/utils/selector.ts
@@ -9464,16 +9438,16 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9464
9438
  if (cmdDef) {
9465
9439
  if (mode === "json") {
9466
9440
  const paramsList = [];
9467
- const schema = cmdDef.parameters;
9441
+ const schema = asZodSchema(cmdDef.parameters);
9468
9442
  const shape = schema?.shape ?? schema?._def?.shape;
9469
9443
  if (shape) {
9470
9444
  for (const [key, value] of Object.entries(shape)) {
9471
- const fieldSchema = value;
9445
+ const fieldSchema = asZodSchema(value);
9472
9446
  const fieldDef = fieldSchema._def;
9473
9447
  const description = fieldSchema.description || fieldDef?.description || "";
9474
9448
  const typeName = fieldDef?.typeName || "";
9475
9449
  const isOptional = typeName === "ZodOptional" || typeof fieldSchema.isOptional === "function" && fieldSchema.isOptional();
9476
- const innerType = fieldDef?.innerType;
9450
+ const innerType = asZodSchema(fieldDef?.innerType);
9477
9451
  const innerTypeName = innerType?._def ? innerType._def.typeName : typeName;
9478
9452
  let type = "unknown";
9479
9453
  if (innerTypeName === "ZodString" || typeName === "ZodString") type = "string";
@@ -9571,7 +9545,8 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9571
9545
  } else {
9572
9546
  switch (command) {
9573
9547
  case "goto":
9574
- if (!args[0]) outputError(`Usage: xbrowser goto <url>`);
9548
+ case "open":
9549
+ if (!args[0]) outputError(`Usage: xbrowser ${command} <url>`);
9575
9550
  cmdName = "goto";
9576
9551
  params = {
9577
9552
  url: /^https?:\/\//i.test(args[0]) || /^wss?:\/\//i.test(args[0]) ? args[0] : "https://" + args[0],
@@ -9583,7 +9558,9 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9583
9558
  params = {
9584
9559
  fullPage: !!(options["full-page"] || options.fullPage),
9585
9560
  type: options.type,
9586
- selector: options.selector || options.s
9561
+ selector: options.selector || options.s,
9562
+ base64: !!options.base64,
9563
+ output: options.output || options.o
9587
9564
  };
9588
9565
  break;
9589
9566
  case "eval":
@@ -9660,7 +9637,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9660
9637
  let parsedActions;
9661
9638
  if (options.action) {
9662
9639
  const actionList = Array.isArray(options.action) ? options.action : [options.action];
9663
- const { parseActionDsl } = await import("./parse-action-dsl-T3DYC33D.js");
9640
+ const { parseActionDsl } = await import("./parse-action-dsl-UM333TL2.js");
9664
9641
  parsedActions = actionList.map((a) => parseActionDsl(a));
9665
9642
  } else if (options["actions-file"]) {
9666
9643
  const fs3 = await import("fs");
@@ -9788,36 +9765,27 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
9788
9765
  }
9789
9766
 
9790
9767
  // src/cli/session-routes.ts
9791
- import { homedir as homedir6 } from "os";
9792
- import { join as join10 } from "path";
9793
- import { readdirSync as readdirSync4, rmSync as rmSync8 } from "fs";
9768
+ import { homedir as homedir8 } from "os";
9769
+ import { join as join12 } from "path";
9770
+ import { readdirSync as readdirSync3, rmSync as rmSync7 } from "fs";
9794
9771
  function cleanSessionFiles() {
9795
- const dir = join10(homedir6(), ".xbrowser", "sessions");
9772
+ const dir = join12(homedir8(), ".xbrowser", "sessions");
9796
9773
  let count = 0;
9797
9774
  try {
9798
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
9799
- const p = join10(dir, entry.name);
9800
- rmSync8(p, { recursive: true, force: true });
9775
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
9776
+ const p = join12(dir, entry.name);
9777
+ rmSync7(p, { recursive: true, force: true });
9801
9778
  count++;
9802
9779
  }
9803
9780
  } catch {
9804
9781
  }
9805
9782
  return count;
9806
9783
  }
9807
- async function handleSession(args, options, mode, cdpEndpoint) {
9784
+ async function handleSession(args, options, mode, _cdpEndpoint) {
9808
9785
  const sub = args[0];
9809
9786
  switch (sub) {
9810
- case "open": {
9811
- const url = args[1];
9812
- const name = options.name || process.env.XBROWSER_SESSION || "default";
9813
- if (!url)
9814
- outputError("Usage: xbrowser session open <url> [--name <name>] [--cdp <endpoint>]");
9815
- const info = await forwardSessionCreate(name, url, cdpEndpoint);
9816
- outputResult({ ok: true, ...info }, mode);
9817
- break;
9818
- }
9819
9787
  case "close": {
9820
- const name = options.name || process.env.XBROWSER_SESSION || "default";
9788
+ const name = options.session || options.name || process.env.XBROWSER_SESSION || "default";
9821
9789
  try {
9822
9790
  await forwardSessionClose(name);
9823
9791
  } catch {
@@ -9838,7 +9806,7 @@ async function handleSession(args, options, mode, cdpEndpoint) {
9838
9806
  break;
9839
9807
  }
9840
9808
  case "kill": {
9841
- const name = options.name || process.env.XBROWSER_SESSION || "default";
9809
+ const name = options.session || options.name || process.env.XBROWSER_SESSION || "default";
9842
9810
  try {
9843
9811
  await forwardSessionClose(name);
9844
9812
  } catch {
@@ -9889,8 +9857,7 @@ async function buildRuntimePluginInfo() {
9889
9857
  const cmds = site.getAllCommands();
9890
9858
  const commandNames = cmds.map((c) => c.name);
9891
9859
  if (commandNames.length === 0) continue;
9892
- const anySite = site;
9893
- const hasLoginHandler = typeof anySite.hasLoginCommand === "function" && anySite.hasLoginCommand();
9860
+ const hasLoginHandler = "hasLoginCommand" in site && typeof site.hasLoginCommand === "function" && site.hasLoginCommand();
9894
9861
  const configRequiresLogin = !!site.config.requiresLogin;
9895
9862
  const hasLogin = hasLoginHandler || configRequiresLogin;
9896
9863
  let loggedIn = null;
@@ -9940,7 +9907,7 @@ async function searchFromMarketplacePlugin2(options, loader) {
9940
9907
  site: options.site,
9941
9908
  limit: options.limit
9942
9909
  },
9943
- {}
9910
+ createStubContext("marketplace")
9944
9911
  );
9945
9912
  const items = extractItems2(result);
9946
9913
  return items.map((item) => ({ ...item, source: "marketplace" }));
@@ -9955,7 +9922,7 @@ async function infoFromMarketplacePlugin(slug, loader) {
9955
9922
  const infoCmd = marketplaceSite.getCommand("info");
9956
9923
  if (!infoCmd) return null;
9957
9924
  try {
9958
- const result = await infoCmd.handler({ slug }, {});
9925
+ const result = await infoCmd.handler({ slug }, createStubContext("marketplace"));
9959
9926
  if (!result || typeof result !== "object") return null;
9960
9927
  const r = result;
9961
9928
  let plugin = null;
@@ -10063,7 +10030,7 @@ async function handlePluginInfo(args, options, mode) {
10063
10030
  }
10064
10031
  console.error(`\u63D2\u4EF6 '${slug}' \u672A\u627E\u5230`);
10065
10032
  } catch (err) {
10066
- console.error("\u67E5\u8BE2\u5931\u8D25:", err.message);
10033
+ console.error("\u67E5\u8BE2\u5931\u8D25:", errMsg(err));
10067
10034
  }
10068
10035
  }
10069
10036
  async function handlePluginSchema(args, mode) {
@@ -10256,7 +10223,7 @@ function handleDaemon(args, options, mode) {
10256
10223
  break;
10257
10224
  }
10258
10225
  default:
10259
- console.log("Usage: xbrowser daemon <start|stop|status> [--port <port>]");
10226
+ console.log("Daemon starts automatically. No manual action needed.");
10260
10227
  }
10261
10228
  }
10262
10229
 
@@ -10267,7 +10234,8 @@ async function handleRecord(args, options, mode) {
10267
10234
  case "start": {
10268
10235
  const url = options.url;
10269
10236
  const sessionName = options.session || "default";
10270
- const result = await forwardRecordStart(sessionName, url);
10237
+ const cdpEndpoint = options.cdp;
10238
+ const result = await forwardRecordStart(sessionName, url, cdpEndpoint);
10271
10239
  if (!result.ok) {
10272
10240
  outputError(String(result.error || "Failed to start recording"));
10273
10241
  return;
@@ -10351,6 +10319,13 @@ async function handleRecord(args, options, mode) {
10351
10319
  outputResult(result, mode);
10352
10320
  break;
10353
10321
  }
10322
+ case "generate-plugin": {
10323
+ const sessionName = options.session || args[1] || "default";
10324
+ const pluginName = options.name || "";
10325
+ const outputDir = options.output || "";
10326
+ await handleGeneratePlugin(sessionName, pluginName, outputDir);
10327
+ break;
10328
+ }
10354
10329
  default:
10355
10330
  console.log("Usage:");
10356
10331
  console.log(" xbrowser record start [--url <url>] [--session <name>]");
@@ -10358,6 +10333,7 @@ async function handleRecord(args, options, mode) {
10358
10333
  console.log(" xbrowser record status [--session <name>]");
10359
10334
  console.log(" xbrowser record summary [--session <name>] [--json]");
10360
10335
  console.log(' xbrowser record checkpoint --type <type> --hint "description" [--selector <sel>] [--session <name>]');
10336
+ console.log(" xbrowser record generate-plugin [--session <name>] [--name <plugin>] [--output <dir>]");
10361
10337
  console.log("");
10362
10338
  console.log("Checkpoint types: dialog, captcha, login, iframe, slider, custom");
10363
10339
  }
@@ -10546,7 +10522,7 @@ async function handleConvert(args, _mode) {
10546
10522
  const fs3 = await import("fs");
10547
10523
  const path3 = await import("path");
10548
10524
  const { default: yaml } = await import("yaml");
10549
- const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-EGFYNICZ.js");
10525
+ const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-LB3GJTLR.js");
10550
10526
  const content = fs3.readFileSync(filePath, "utf-8");
10551
10527
  const recording = yaml.parse(content);
10552
10528
  const ext = path3.extname(outputPath).toLowerCase();
@@ -10571,7 +10547,7 @@ async function handleExtract(args, _mode) {
10571
10547
  console.error("Usage: xbrowser extract <recording.yaml>");
10572
10548
  process.exit(1);
10573
10549
  }
10574
- const { extractAndSave, printExtractSummary } = await import("./extract-L2IW3IUB.js");
10550
+ const { extractAndSave, printExtractSummary } = await import("./extract-BSYBM4MR.js");
10575
10551
  const { summary, outputPath } = extractAndSave(filePath);
10576
10552
  printExtractSummary(summary);
10577
10553
  console.log(`
@@ -10584,40 +10560,203 @@ async function handleFilter(args, _mode) {
10584
10560
  console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude-types=type1,type2]");
10585
10561
  process.exit(1);
10586
10562
  }
10587
- const { filterRecording, parseExcludeTypes } = await import("./filter-HC4RA7JY.js");
10563
+ const { filterRecording, parseExcludeTypes } = await import("./filter-KCFO4RSV.js");
10588
10564
  const excludeTypes = parseExcludeTypes(args.slice(2));
10589
10565
  const result = filterRecording(filePath, outputPath, excludeTypes);
10590
10566
  console.log(`Filtered ${filePath} -> ${outputPath}`);
10591
10567
  console.log(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)`);
10592
10568
  }
10593
-
10594
- // src/stdin.ts
10595
- import { createInterface } from "readline";
10596
- import { readFileSync as readFileSync15 } from "fs";
10597
- async function readStdin2() {
10598
- if (process.stdin.isTTY) return [];
10599
- const lines = [];
10600
- const rl = createInterface({ input: process.stdin });
10601
- for await (const line of rl) {
10602
- const trimmed = line.trim();
10603
- if (trimmed && !trimmed.startsWith("#")) {
10604
- lines.push(trimmed);
10569
+ async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
10570
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RTDGURIJ.js");
10571
+ const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
10572
+ const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync11 } = await import("fs");
10573
+ const { join: join13 } = await import("path");
10574
+ const data = SessionRecorder2.readData(sessionName);
10575
+ if (!data) {
10576
+ outputError(`No recording found for session "${sessionName}". Run \`xbrowser record stop --session ${sessionName}\` first.`);
10577
+ return;
10578
+ }
10579
+ let domain = "unknown";
10580
+ try {
10581
+ domain = new URL(data.startUrl).hostname.replace(/^www\./, "");
10582
+ } catch {
10583
+ }
10584
+ const finalPluginName = pluginName || domain.split(".")[0] || "my-site";
10585
+ const finalOutputDir = outputDir || join13(process.cwd(), ".xcli", "plugins", finalPluginName);
10586
+ const knowledge = readSiteKnowledge2(domain);
10587
+ const knowledgeMd = knowledge ? toMarkdown(knowledge) : "";
10588
+ const pluginCode = generatePluginCode(finalPluginName, domain, data, knowledgeMd);
10589
+ mkdirSync10(join13(finalOutputDir), { recursive: true });
10590
+ writeFileSync11(join13(finalOutputDir, "index.ts"), pluginCode, "utf-8");
10591
+ if (knowledgeMd) {
10592
+ writeFileSync11(join13(finalOutputDir, "SITE_KNOWLEDGE.md"), knowledgeMd, "utf-8");
10593
+ }
10594
+ console.log("");
10595
+ console.log("=== Plugin Generated ===");
10596
+ console.log(` Plugin: ${finalPluginName}`);
10597
+ console.log(` Domain: ${domain}`);
10598
+ console.log(` Output: ${finalOutputDir}/index.ts`);
10599
+ if (knowledgeMd) {
10600
+ console.log(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md`);
10601
+ }
10602
+ console.log(` Actions: ${data.actions.length}`);
10603
+ console.log(` APIs: ${data.network.filter((n) => n.contentType.includes("json") || n.url.includes("/api/")).length}`);
10604
+ console.log("");
10605
+ console.log("Next steps:");
10606
+ console.log(` 1. Review and edit: ${finalOutputDir}/index.ts`);
10607
+ console.log(` 2. Test: xbrowser ${finalPluginName} <command>`);
10608
+ console.log(` 3. Reference: ${finalOutputDir}/SITE_KNOWLEDGE.md (for LLM)`);
10609
+ }
10610
+ function generatePluginCode(pluginName, domain, data, _knowledgeMd) {
10611
+ const pagePaths = /* @__PURE__ */ new Set();
10612
+ for (const action of data.actions) {
10613
+ if (action.url) {
10614
+ try {
10615
+ pagePaths.add(new URL(action.url).pathname);
10616
+ } catch {
10617
+ }
10605
10618
  }
10606
10619
  }
10607
- return lines;
10620
+ const clickSelectors = [];
10621
+ const inputSelectors = [];
10622
+ for (const action of data.actions) {
10623
+ const el = action.element;
10624
+ if (!el) continue;
10625
+ const sel = el.selector;
10626
+ if (!sel) continue;
10627
+ if (action.type === "click" && !clickSelectors.includes(sel)) {
10628
+ clickSelectors.push(sel);
10629
+ }
10630
+ if (action.type === "input" && !inputSelectors.some((s) => s.selector === sel)) {
10631
+ inputSelectors.push({ selector: sel, placeholder: el.placeholder });
10632
+ }
10633
+ }
10634
+ const apis = data.network.filter(
10635
+ (n) => (n.contentType || "").includes("json") || n.url.includes("/api/")
10636
+ );
10637
+ const commands = [];
10638
+ commands.push(` site.command({
10639
+ name: 'open',
10640
+ description: 'Open ${domain}',
10641
+ scope: 'browser',
10642
+ handler: async (_p: Record<string, unknown>, ctx: CommandContext) => {
10643
+ const page = ensurePage(ctx);
10644
+ await page.goto('${data.startUrl}', { waitUntil: 'domcontentloaded' });
10645
+ return ok({ url: page.url() });
10646
+ },
10647
+ });`);
10648
+ if (inputSelectors.length > 0) {
10649
+ const params = inputSelectors.slice(0, 5).map(
10650
+ (s, i) => ` ${s.selector.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+/, "").toLowerCase() || `field${i}`}: z.string().describe('${s.placeholder || s.selector}'),`
10651
+ ).join("\n");
10652
+ const fills = inputSelectors.slice(0, 5).map(
10653
+ (s) => ` await page.fill('${s.selector}', p.${s.selector.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+/, "").toLowerCase() || "field0"}');`
10654
+ ).join("\n");
10655
+ commands.push(` site.command({
10656
+ name: 'fill',
10657
+ description: 'Fill form on ${domain}',
10658
+ scope: 'page',
10659
+ parameters: z.object({
10660
+ ${params}
10661
+ }),
10662
+ handler: async (p: Record<string, unknown>, ctx: CommandContext) => {
10663
+ const page = ensurePage(ctx);
10664
+ ${fills}
10665
+ return ok({ filled: true });
10666
+ },
10667
+ });`);
10668
+ }
10669
+ if (clickSelectors.length > 0) {
10670
+ commands.push(` site.command({
10671
+ name: 'click',
10672
+ description: 'Click element on ${domain}',
10673
+ scope: 'page',
10674
+ parameters: z.object({
10675
+ selector: z.string().describe('CSS selector of element to click'),
10676
+ }),
10677
+ handler: async (p: Record<string, unknown>, ctx: CommandContext) => {
10678
+ const page = ensurePage(ctx);
10679
+ await page.click(p.selector as string);
10680
+ return ok({ clicked: p.selector });
10681
+ },
10682
+ });`);
10683
+ }
10684
+ if (apis.length > 0) {
10685
+ commands.push(` site.command({
10686
+ name: 'scrape',
10687
+ description: 'Scrape data from ${domain}',
10688
+ scope: 'page',
10689
+ handler: async (_p: Record<string, unknown>, ctx: CommandContext) => {
10690
+ const page = ensurePage(ctx);
10691
+ const data = await page.evaluate(() => {
10692
+ return {
10693
+ title: document.title,
10694
+ url: location.href,
10695
+ content: document.body?.innerText?.substring(0, 5000) || '',
10696
+ };
10697
+ });
10698
+ return ok(data);
10699
+ },
10700
+ });`);
10701
+ }
10702
+ return `/**
10703
+ * ${pluginName} \u2014 Auto-generated plugin for ${domain}
10704
+ *
10705
+ * Generated from xbrowser recording session.
10706
+ * Review and customize before using in production.
10707
+ *
10708
+ * Site Knowledge: See SITE_KNOWLEDGE.md for LLM-readable selector/API reference.
10709
+ */
10710
+
10711
+ import { z } from 'zod';
10712
+ import { ok } from '@dyyz1993/xcli-core';
10713
+ import { createSite, type CommandContext } from '@dyyz1993/xcli-core';
10714
+
10715
+ interface XBPage {
10716
+ url(): string;
10717
+ goto(url: string, opts?: Record<string, unknown>): Promise<unknown>;
10718
+ click(selector: string, opts?: Record<string, unknown>): Promise<unknown>;
10719
+ fill(selector: string, value: string, opts?: Record<string, unknown>): Promise<unknown>;
10720
+ evaluate<T>(fn: string | (() => T)): Promise<T>;
10608
10721
  }
10609
- function readCommandFile(filePath) {
10610
- const content = readFileSync15(filePath, "utf-8");
10611
- return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
10722
+
10723
+ function ensurePage(ctx: CommandContext): XBPage {
10724
+ const page = 'page' in ctx ? (ctx as Record<string, unknown>).page : undefined;
10725
+ if (!page) throw new Error('No active page. Start a session first.');
10726
+ return page as XBPage;
10612
10727
  }
10613
10728
 
10729
+ export default createSite({
10730
+ name: '${pluginName}',
10731
+ domain: '${domain}',
10732
+ description: 'Auto-generated plugin for ${domain} from recording',
10733
+
10734
+ login: {
10735
+ url: '${data.startUrl}',
10736
+ detect: async (ctx: CommandContext) => {
10737
+ const page = ensurePage(ctx);
10738
+ // TODO: Add login detection logic
10739
+ return false;
10740
+ },
10741
+ },
10742
+
10743
+ setup(site) {
10744
+ ${commands.join("\n\n")}
10745
+ },
10746
+ });
10747
+ `;
10748
+ }
10749
+
10750
+ // src/stdin.ts
10751
+ import { readStdin as readStdin2, readCommandFile, splitFileLine } from "@dyyz1993/xcli-core";
10752
+
10614
10753
  // src/cli/run-routes.ts
10615
10754
  async function handleRun(filePath, options) {
10616
10755
  let commands;
10617
10756
  try {
10618
10757
  commands = readCommandFile(filePath);
10619
10758
  } catch (e) {
10620
- outputError(`Failed to read file '${filePath}': ${e.message}`);
10759
+ outputError(`Failed to read file '${filePath}': ${errMsg(e)}`);
10621
10760
  return;
10622
10761
  }
10623
10762
  if (commands.length === 0) {
@@ -10654,10 +10793,10 @@ async function handleRun(filePath, options) {
10654
10793
  async function handleViewer(_args, options, mode, _cdpEndpoint) {
10655
10794
  const name = options.name || process.env.XBROWSER_SESSION || "default";
10656
10795
  const selector = options.selector;
10657
- const status = getDaemonProcessStatus();
10796
+ let status = getDaemonProcessStatus();
10658
10797
  if (!status.running) {
10659
- outputError("Daemon is not running. Start with: xbrowser daemon start");
10660
- return;
10798
+ await startDaemonProcess();
10799
+ status = getDaemonProcessStatus();
10661
10800
  }
10662
10801
  const port = status.port || getDaemonConfig().basePort;
10663
10802
  let url = `http://localhost:${port}/preview/${name}`;
@@ -10973,7 +11112,236 @@ async function handleNetCommand(args, options, mode, sessionName) {
10973
11112
  outputError(`Unknown net sub-command: ${subCommand}. Use: list, clear, top, log, around, analyze, curl, replay, inspect, like, dislike, export`);
10974
11113
  }
10975
11114
  } catch (err) {
10976
- outputError(err.message || "Network command failed");
11115
+ outputError(errMsg(err) || "Network command failed");
11116
+ }
11117
+ }
11118
+
11119
+ // src/cli/test-routes.ts
11120
+ import { execSync as execSync3 } from "child_process";
11121
+ import { readFileSync as readFileSync8 } from "fs";
11122
+ import { resolve as resolve9 } from "path";
11123
+ function findPluginPath(plugin) {
11124
+ const candidates = [
11125
+ resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts"),
11126
+ resolve9(process.cwd(), "node_modules/@xbrowser/cli/.xcli/plugins", plugin, "index.ts")
11127
+ ];
11128
+ for (const p of candidates) {
11129
+ try {
11130
+ readFileSync8(p, "utf-8");
11131
+ return p;
11132
+ } catch {
11133
+ }
11134
+ }
11135
+ return resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts");
11136
+ }
11137
+ function extractSchema(plugin, command) {
11138
+ const pluginPath = findPluginPath(plugin);
11139
+ let src;
11140
+ try {
11141
+ src = readFileSync8(pluginPath, "utf-8");
11142
+ } catch {
11143
+ return null;
11144
+ }
11145
+ const cmdIdx = src.indexOf(`.command('${command}'`);
11146
+ if (cmdIdx < 0) return null;
11147
+ const after = src.slice(cmdIdx);
11148
+ const resultIdx = after.indexOf("result:");
11149
+ if (resultIdx < 0) return null;
11150
+ let block = after.slice(resultIdx + 7);
11151
+ let depth = 0;
11152
+ let schemaStr = "";
11153
+ for (const ch of block) {
11154
+ if (ch === "{" || ch === "(" || ch === "[") depth++;
11155
+ if (ch === "}" || ch === ")" || ch === "]") {
11156
+ depth--;
11157
+ if (depth < 0) break;
11158
+ }
11159
+ if (depth === 0 && /\w/.test(ch) && schemaStr.trim().endsWith(",")) {
11160
+ break;
11161
+ }
11162
+ schemaStr += ch;
11163
+ }
11164
+ const objStart = schemaStr.indexOf("z.object({");
11165
+ const objEnd = objStart >= 0 ? schemaStr.indexOf("})", objStart) : -1;
11166
+ const objStr = objStart >= 0 && objEnd > objStart ? schemaStr.slice(objStart + 10, objEnd) : schemaStr;
11167
+ const fields = [];
11168
+ const SKIP_NAMES = /* @__PURE__ */ new Set(["passthrough", "optional", "describe", "default"]);
11169
+ const fieldRegex = /(\w+)\s*:\s*z\.(\w+)/g;
11170
+ let match;
11171
+ while ((match = fieldRegex.exec(objStr)) !== null) {
11172
+ const name = match[1];
11173
+ const type = match[2];
11174
+ if (SKIP_NAMES.has(name) || type === "union" || type === "enum") continue;
11175
+ const afterField = objStr.slice(match.index + match[0].length);
11176
+ const isOptional = afterField.trimStart().startsWith(".optional()");
11177
+ fields.push({
11178
+ name,
11179
+ type: type === "string" ? "string" : type === "number" ? "number" : type === "boolean" ? "boolean" : type === "array" ? "array" : type,
11180
+ optional: isOptional || name === "index"
11181
+ });
11182
+ }
11183
+ return fields.length > 0 ? fields : null;
11184
+ }
11185
+ async function runTest(plugin, command, cmdArgs, options) {
11186
+ const cdp = options.cdp || options.cdpEndpoint || "http://localhost:9221";
11187
+ const argsStr = cmdArgs.filter((a) => !a.startsWith("--cdp")).join(" ");
11188
+ const schema = extractSchema(plugin, command);
11189
+ const fullCmd = `npx xbrowser ${plugin} ${command} ${argsStr} --cdp ${cdp} --json --timeout 60000`;
11190
+ let stdout = "";
11191
+ try {
11192
+ stdout = execSync3(fullCmd, {
11193
+ timeout: 65e3,
11194
+ encoding: "utf-8",
11195
+ stdio: ["pipe", "pipe", "pipe"],
11196
+ env: { ...process.env, FORCE_COLOR: "0" }
11197
+ });
11198
+ } catch (e) {
11199
+ const err = e;
11200
+ stdout = err.stdout?.toString() || "";
11201
+ const jsonLine = stdout.split("\n").find((l) => {
11202
+ try {
11203
+ JSON.parse(l);
11204
+ return true;
11205
+ } catch {
11206
+ return false;
11207
+ }
11208
+ });
11209
+ if (jsonLine) {
11210
+ try {
11211
+ const parsed2 = JSON.parse(jsonLine);
11212
+ const code = parsed2?.data?.code || "";
11213
+ if (code === "LOGIN_REQUIRED") {
11214
+ return { status: "LOGIN_REQUIRED", message: parsed2.message || "\u9700\u8981\u767B\u5F55", viewerUrl: "http://localhost:9224/preview/default" };
11215
+ }
11216
+ } catch {
11217
+ }
11218
+ }
11219
+ const stderr = err.stderr?.toString() || "";
11220
+ if (stdout.includes("captcha") || stderr.includes("captcha") || stdout.includes("CAPTCHA")) {
11221
+ return { status: "CAPTCHA", message: "\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
11222
+ }
11223
+ return { status: "EXEC_ERROR", message: (err.message || "").slice(0, 200) || "\u6267\u884C\u5931\u8D25" };
11224
+ }
11225
+ const allLines = stdout.split("\n");
11226
+ const jsonStart = allLines.findIndex((l) => l.trim().startsWith("{"));
11227
+ const jsonStr = jsonStart >= 0 ? allLines.slice(jsonStart).join("\n") : "";
11228
+ let parsed = {};
11229
+ try {
11230
+ parsed = JSON.parse(jsonStr);
11231
+ } catch {
11232
+ return { status: "EXEC_ERROR", message: "\u65E0\u6CD5\u89E3\u6790 CLI \u8F93\u51FA" };
11233
+ }
11234
+ const rawData = parsed.data;
11235
+ const rawTips = parsed.tips || [];
11236
+ if (parsed.success === false) {
11237
+ const code = rawData?.code || "";
11238
+ if (code === "LOGIN_REQUIRED") {
11239
+ return { status: "LOGIN_REQUIRED", message: parsed.message || "\u9700\u8981\u767B\u5F55", viewerUrl: "http://localhost:9224/preview/default" };
11240
+ }
11241
+ if (rawTips.join(" ").includes("captcha") || rawTips.join(" ").includes("CAPTCHA")) {
11242
+ return { status: "CAPTCHA", message: parsed.message || "\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
11243
+ }
11244
+ }
11245
+ const data = rawData;
11246
+ if (data === null || data === void 0) {
11247
+ const msg = parsed.message || "";
11248
+ const tips = rawTips.join(" ");
11249
+ const viewerUrl = parsed.viewerUrl || rawData?.viewerUrl || "";
11250
+ let status = "NO_DATA";
11251
+ if (msg.includes("block") || msg.includes("anti-bot") || msg.includes("captcha") || tips.includes("viewer")) {
11252
+ status = "BLOCKED";
11253
+ } else if (msg.includes("\u767B\u5F55") || msg.includes("login")) {
11254
+ status = "LOGIN_REQUIRED";
11255
+ }
11256
+ const ret = { status, message: msg || "\u6682\u65E0\u6570\u636E" };
11257
+ if (viewerUrl) ret.viewerUrl = viewerUrl;
11258
+ return ret;
11259
+ }
11260
+ if (!schema) {
11261
+ return { status: "OK", note: "\u65E0 result schema", data: JSON.stringify(data).slice(0, 200) };
11262
+ }
11263
+ const errors = [];
11264
+ const items = Array.isArray(data) ? data.slice(0, 3) : [data];
11265
+ for (const item of items) {
11266
+ if (typeof item !== "object" || item === null) {
11267
+ errors.push("\u6570\u636E\u9879\u4E0D\u662F\u5BF9\u8C61");
11268
+ continue;
11269
+ }
11270
+ for (const field of schema) {
11271
+ const val = item[field.name];
11272
+ if (val === void 0) {
11273
+ if (!field.optional) errors.push(`\u7F3A\u5C11: ${field.name}`);
11274
+ continue;
11275
+ }
11276
+ if (field.type === "array") {
11277
+ if (!Array.isArray(val)) errors.push(`${field.name}: \u671F\u671B array`);
11278
+ } else if (typeof val !== field.type) {
11279
+ errors.push(`${field.name}: \u671F\u671B ${field.type}, \u5B9E\u9645 ${typeof val}`);
11280
+ }
11281
+ }
11282
+ }
11283
+ if (errors.length > 0) {
11284
+ return { status: "SCHEMA_ERROR", errors, data: JSON.stringify(data).slice(0, 200) };
11285
+ }
11286
+ const count = Array.isArray(data) ? data.length : 1;
11287
+ return { status: "OK", count, data: JSON.stringify(data).slice(0, 200) };
11288
+ }
11289
+ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
11290
+ const plugin = cmdArgs[0];
11291
+ const command = cmdArgs[1];
11292
+ if (!plugin || !command) {
11293
+ console.error("\u7528\u6CD5: xbrowser test <plugin> <command> [\u53C2\u6570...]");
11294
+ console.error("\u793A\u4F8B: xbrowser test doubao list --cdp 9221");
11295
+ return;
11296
+ }
11297
+ const loader = await getPluginLoader();
11298
+ const internalLoader = loader.getCore().loader;
11299
+ const site = internalLoader.getSite(plugin);
11300
+ if (!site) {
11301
+ console.error(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
11302
+ return;
11303
+ }
11304
+ const cmdEntry = site.getCommand(command);
11305
+ if (!cmdEntry) {
11306
+ console.error(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
11307
+ return;
11308
+ }
11309
+ const testArgs = cmdArgs.slice(2);
11310
+ const mergedOptions = { ...options, cdp: cdpEndpoint || options.cdp };
11311
+ const result = await runTest(plugin, command, testArgs, mergedOptions);
11312
+ if (mode === "json") {
11313
+ console.log(JSON.stringify(result, null, 2));
11314
+ return;
11315
+ }
11316
+ const r = result;
11317
+ const icons = {
11318
+ OK: "\u2705",
11319
+ LOGIN_REQUIRED: "\u{1F511}",
11320
+ CAPTCHA: "\u{1F6A8}",
11321
+ SCHEMA_ERROR: "\u274C",
11322
+ BLOCKED: "\u{1F6A7}",
11323
+ NO_DATA: "\u{1F4ED}",
11324
+ EXEC_ERROR: "\u{1F4A5}"
11325
+ };
11326
+ const status = String(r.status);
11327
+ const icon = icons[status] || "\u2753";
11328
+ console.log(`
11329
+ ${icon} ${plugin}.${command}`);
11330
+ console.log(` \u72B6\u6001: ${status}`);
11331
+ if (status === "OK") {
11332
+ if (r.count) console.log(` \u6570\u636E: ${r.count} \u9879`);
11333
+ if (r.data) console.log(` \u9884\u89C8: ${String(r.data).slice(0, 150)}`);
11334
+ } else if (status === "LOGIN_REQUIRED" || status === "CAPTCHA") {
11335
+ console.log(` \u4FE1\u606F: ${String(r.message)}`);
11336
+ console.log(` Viewer: ${String(r.viewerUrl)}`);
11337
+ } else if (status === "SCHEMA_ERROR") {
11338
+ const errs = r.errors;
11339
+ if (errs) console.log(` \u9519\u8BEF: ${errs.join("; ")}`);
11340
+ } else if (["NO_DATA", "BLOCKED"].includes(status)) {
11341
+ console.log(` \u4FE1\u606F: ${String(r.message)}`);
11342
+ if (r.viewerUrl) console.log(` Viewer: ${String(r.viewerUrl)}`);
11343
+ } else {
11344
+ console.log(` \u4FE1\u606F: ${String(r.message)}`);
10977
11345
  }
10978
11346
  }
10979
11347
 
@@ -10990,11 +11358,11 @@ Usage:
10990
11358
  xbrowser -e cmd1 -e cmd2 Execute multiple -e commands
10991
11359
 
10992
11360
  Commands:
10993
- session open <url> [--name <n>] Open browser session
10994
- session close [--name <n>] Close session
11361
+ session close [--session <name>] Close session
10995
11362
  session list List sessions
10996
- session kill [--name <n>] Kill session
11363
+ session kill [--session <name>] Kill session
10997
11364
  goto <url> Navigate to URL
11365
+ open <url> Navigate to URL (alias for goto)
10998
11366
  click <selector> Click element (-s <sel>)
10999
11367
  fill <selector> <value> Fill input (-s <sel> -v <val>)
11000
11368
  type <selector> <text> Type text (-s <sel> -v <text>)
@@ -11004,7 +11372,7 @@ Commands:
11004
11372
  dblclick <selector> Double click (-s <sel>)
11005
11373
  check <selector> Check checkbox (-s <sel>)
11006
11374
  uncheck <selector> Uncheck checkbox (-s <sel>)
11007
- screenshot [--full-page] Take screenshot
11375
+ screenshot [--full-page] [--base64] Take screenshot (saves to ~/.xbrowser/screenshots/; use --base64 for inline data)
11008
11376
  eval <expression> Evaluate JS
11009
11377
  wait <selector> [--timeout <ms>] Wait for element (-s <sel>)
11010
11378
  scroll <direction> [--distance N] Scroll page
@@ -11026,9 +11394,6 @@ Commands:
11026
11394
  plugin list List plugins
11027
11395
  plugin reload <name> Reload plugin
11028
11396
  create <name> --template <type> Create plugin
11029
- daemon start [--port <port>] Start daemon
11030
- daemon stop Stop daemon
11031
- daemon status Check status
11032
11397
  serve [--port <port>] [--token <t>] Start HTTP server for remote access
11033
11398
  remote <url> [command] [--token <t>] Execute command on remote server
11034
11399
  record start --url <url> Start recording
@@ -11272,7 +11637,7 @@ async function createSessionHandler(req) {
11272
11637
  createdAt: session.createdAt
11273
11638
  });
11274
11639
  } catch (err) {
11275
- return errorResponse(500, "INTERNAL_ERROR", err.message);
11640
+ return errorResponse(500, "INTERNAL_ERROR", errMsg(err));
11276
11641
  }
11277
11642
  }
11278
11643
  async function closeSession2(req) {
@@ -11367,7 +11732,7 @@ async function route(method, url, headers, body) {
11367
11732
  try {
11368
11733
  return await match.route.handler(req);
11369
11734
  } catch (err) {
11370
- return errorResponse(500, "INTERNAL_ERROR", err.message);
11735
+ return errorResponse(500, "INTERNAL_ERROR", errMsg(err));
11371
11736
  }
11372
11737
  }
11373
11738
  async function handleRequest(req, res, validateAuthFn) {
@@ -11401,19 +11766,19 @@ function headersToObject(headers) {
11401
11766
  return result;
11402
11767
  }
11403
11768
  function readBody(req) {
11404
- return new Promise((resolve16, reject) => {
11769
+ return new Promise((resolve10, reject) => {
11405
11770
  const chunks = [];
11406
11771
  req.on("data", (chunk) => chunks.push(chunk));
11407
11772
  req.on("end", () => {
11408
11773
  const raw = Buffer.concat(chunks).toString("utf-8");
11409
11774
  if (!raw) {
11410
- resolve16(null);
11775
+ resolve10(null);
11411
11776
  return;
11412
11777
  }
11413
11778
  try {
11414
- resolve16(JSON.parse(raw));
11779
+ resolve10(JSON.parse(raw));
11415
11780
  } catch {
11416
- resolve16(null);
11781
+ resolve10(null);
11417
11782
  }
11418
11783
  });
11419
11784
  req.on("error", reject);
@@ -11458,7 +11823,7 @@ var HTTPServer = class {
11458
11823
  res.end(JSON.stringify({ error: "INTERNAL_ERROR", message, statusCode: 500 }));
11459
11824
  });
11460
11825
  });
11461
- return new Promise((resolve16, reject) => {
11826
+ return new Promise((resolve10, reject) => {
11462
11827
  const server = this.server;
11463
11828
  server.on("error", (err) => {
11464
11829
  this.server = null;
@@ -11470,7 +11835,7 @@ var HTTPServer = class {
11470
11835
  this.port = addr.port;
11471
11836
  }
11472
11837
  console.log(`HTTP server listening on http://${this.host}:${this.port}`);
11473
- resolve16({ port: this.port, host: this.host });
11838
+ resolve10({ port: this.port, host: this.host });
11474
11839
  });
11475
11840
  });
11476
11841
  }
@@ -11481,13 +11846,13 @@ var HTTPServer = class {
11481
11846
  */
11482
11847
  async stop() {
11483
11848
  if (!this.server) return;
11484
- return new Promise((resolve16, reject) => {
11849
+ return new Promise((resolve10, reject) => {
11485
11850
  this.server.close((err) => {
11486
11851
  if (err) {
11487
11852
  reject(err);
11488
11853
  } else {
11489
11854
  this.server = null;
11490
- resolve16();
11855
+ resolve10();
11491
11856
  }
11492
11857
  });
11493
11858
  });
@@ -11525,8 +11890,9 @@ function showCommandHelp(siteName, cmd, siteConfig, mode) {
11525
11890
  if (mode === "json") {
11526
11891
  const paramsList = [];
11527
11892
  if (c.parameters) {
11528
- const def = c.parameters._def;
11529
- const shape = def.shape?.();
11893
+ const def = asZodSchema(c.parameters)._def;
11894
+ const rawShape = def?.shape;
11895
+ const shape = typeof rawShape === "function" ? rawShape() : rawShape;
11530
11896
  if (shape) {
11531
11897
  for (const [key, value] of Object.entries(shape)) {
11532
11898
  const info = extractZodFieldInfo(value);
@@ -11546,6 +11912,7 @@ function showCommandHelp(siteName, cmd, siteConfig, mode) {
11546
11912
  command: c.name,
11547
11913
  description: c.description,
11548
11914
  scope: c.scope,
11915
+ ...c.loginRequired ? { loginRequired: c.loginRequired } : {},
11549
11916
  parameters: paramsList
11550
11917
  }, mode);
11551
11918
  } else {
@@ -11558,9 +11925,25 @@ function showCommandHelp(siteName, cmd, siteConfig, mode) {
11558
11925
  examples: c.examples
11559
11926
  }, { color: false, emoji: false });
11560
11927
  console.log(text);
11928
+ if (c.loginRequired) {
11929
+ console.log(` Login: ${c.loginRequired}`);
11930
+ }
11561
11931
  console.log("");
11562
11932
  }
11563
11933
  }
11934
+ function outputLoginRequired(result, mode) {
11935
+ if (mode === "json" || mode === "yaml") {
11936
+ console.log(outputFormatter2.format(result, { mode, color: false, emoji: false }));
11937
+ return;
11938
+ }
11939
+ const message = result.message || "Login required";
11940
+ console.error(message);
11941
+ for (const tip of result.tips || []) {
11942
+ const text = typeof tip === "string" ? tip : tip.message;
11943
+ if (text !== message) console.error(` \u{1F4A1} ${text}`);
11944
+ }
11945
+ process.exit(1);
11946
+ }
11564
11947
  function extractZodFieldInfo(value) {
11565
11948
  const field = value;
11566
11949
  const fieldDef = field._def;
@@ -11624,7 +12007,7 @@ function extractCdpFromArgv(argv) {
11624
12007
  if (argv[i] === "--cdp" && argv[i + 1]) return argv[i + 1];
11625
12008
  if (typeof argv[i] === "string" && argv[i].startsWith("--cdp=")) return argv[i].slice(6);
11626
12009
  }
11627
- return void 0;
12010
+ return process.env.XBROWSER_CDP;
11628
12011
  }
11629
12012
  async function handleStdinMode(stdinCommands, argv) {
11630
12013
  const chain = stdinCommands.join(" && ");
@@ -11687,7 +12070,7 @@ async function routeCommand(argv, stdinCommands) {
11687
12070
  const cmdArgs = positional.slice(1);
11688
12071
  const mode = options.json ? "json" : options.yaml ? "yaml" : "text";
11689
12072
  const sessionName = options.session || process.env.XBROWSER_SESSION || "default";
11690
- const cdpEndpoint = options.cdp;
12073
+ const cdpEndpoint = options.cdp || process.env.XBROWSER_CDP;
11691
12074
  if (options.version || options.v) {
11692
12075
  console.log(`xbrowser v${version}`);
11693
12076
  return;
@@ -11736,16 +12119,16 @@ async function routeCommand(argv, stdinCommands) {
11736
12119
  if (builtinCmd) {
11737
12120
  if (mode === "json") {
11738
12121
  const paramsList = [];
11739
- const schema = builtinCmd.parameters;
12122
+ const schema = asZodSchema(builtinCmd.parameters);
11740
12123
  const shape = schema?.shape ?? schema?._def?.shape;
11741
12124
  if (shape) {
11742
12125
  for (const [key, value] of Object.entries(shape)) {
11743
- const fieldSchema = value;
12126
+ const fieldSchema = asZodSchema(value);
11744
12127
  const fieldDef = fieldSchema._def;
11745
12128
  const description = fieldSchema.description || fieldDef?.description || "";
11746
12129
  const typeName = fieldDef?.typeName || "";
11747
12130
  const isOptional = typeName === "ZodOptional" || typeof fieldSchema.isOptional === "function" && fieldSchema.isOptional();
11748
- const innerType = fieldDef?.innerType;
12131
+ const innerType = asZodSchema(fieldDef?.innerType);
11749
12132
  const innerTypeName = innerType?._def ? innerType._def.typeName : typeName;
11750
12133
  let type = "unknown";
11751
12134
  if (innerTypeName === "ZodString" || typeName === "ZodString") type = "string";
@@ -11820,12 +12203,21 @@ async function routeCommand(argv, stdinCommands) {
11820
12203
  if (builtin) await builtin.execute(cmdArgs, options, { cwd: process.cwd() });
11821
12204
  break;
11822
12205
  }
12206
+ case "knowledge":
12207
+ case "know": {
12208
+ const builtin = allBuiltins.find((b) => b.name === "knowledge");
12209
+ if (builtin) await builtin.execute(cmdArgs, options, { cwd: process.cwd() });
12210
+ break;
12211
+ }
11823
12212
  case "viewer":
11824
12213
  await handleViewer(cmdArgs, options, mode, cdpEndpoint);
11825
12214
  break;
11826
12215
  case "help":
11827
12216
  showMainHelp();
11828
12217
  break;
12218
+ case "test":
12219
+ await handleTest(cmdArgs, options, mode, cdpEndpoint);
12220
+ break;
11829
12221
  case "net":
11830
12222
  await handleNetCommand(cmdArgs, options, mode, sessionName);
11831
12223
  break;
@@ -11911,7 +12303,7 @@ Run "xbrowser ${command} --help" to see available commands.`
11911
12303
  const rawPluginArgs = subCmdIdx >= 0 ? argv.slice(subCmdIdx + 1) : [];
11912
12304
  const params = parsePluginParams(rawPluginArgs, cmdEntry.parameters);
11913
12305
  if (cmdEntry.parameters) {
11914
- const schemaAny = cmdEntry.parameters;
12306
+ const schemaAny = asZodSchema(cmdEntry.parameters);
11915
12307
  const def = schemaAny._def;
11916
12308
  const shapeOrFn = def?.shape ?? schemaAny.shape;
11917
12309
  const shapeObj = typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
@@ -11935,9 +12327,14 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
11935
12327
  }
11936
12328
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
11937
12329
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
11938
- const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
12330
+ const { forwardExec } = await import("./daemon-client-3JOKX2L2.js");
11939
12331
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
11940
12332
  const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
12333
+ const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
12334
+ if (result && result.success === false && resultData?.code === "LOGIN_REQUIRED") {
12335
+ outputLoginRequired(result, mode);
12336
+ return;
12337
+ }
11941
12338
  if (result && result.success !== false) {
11942
12339
  if (isCommandResult2(result)) {
11943
12340
  if (mode === "json" || mode === "yaml") {
@@ -11981,10 +12378,33 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
11981
12378
  cliName: "xbrowser",
11982
12379
  waitForHuman: async (_opts) => {
11983
12380
  return { solved: false, timedOut: true };
11984
- }
12381
+ },
12382
+ tips: new TipCollector3()
11985
12383
  };
11986
12384
  try {
11987
12385
  const cmdStart = Date.now();
12386
+ const loginGuard = await checkPluginLoginRequired({
12387
+ site,
12388
+ command: cmdEntry,
12389
+ commandName: subCommand,
12390
+ ctx,
12391
+ page: needsBrowser ? session?.page : null,
12392
+ sessionName
12393
+ });
12394
+ if (!loginGuard.ok) {
12395
+ const result2 = {
12396
+ success: false,
12397
+ data: loginGuard.data ?? null,
12398
+ message: loginGuard.message,
12399
+ tips: normalizeTips7(loginGuard.tips)
12400
+ };
12401
+ if (mode === "json" || mode === "yaml") {
12402
+ outputLoginRequired(result2, mode);
12403
+ } else {
12404
+ outputLoginRequired(result2, mode);
12405
+ }
12406
+ return;
12407
+ }
11988
12408
  const cmdHooks = await loadHooks();
11989
12409
  if (cmdHooks.length > 0 && session?.page) {
11990
12410
  await Promise.all(cmdHooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${command} ${subCommand}`, params })));
@@ -12003,23 +12423,39 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
12003
12423
  saveSessionDiskMeta(sessionName, { conversationUrl: convUrl, cdpEndpoint });
12004
12424
  }
12005
12425
  }
12426
+ let injectedViewerUrl;
12427
+ 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"];
12428
+ const tipTexts = (result.tips || []).map((t) => typeof t === "string" ? t : t.message);
12429
+ const isLoginFail = isCommandResult2(result) && result.success === false && [result.message, ...tipTexts].filter(Boolean).join(" ").toLowerCase().match(new RegExp(LOGIN_FAIL_KEYWORDS.join("|"), "i"));
12430
+ if (isLoginFail) {
12431
+ injectedViewerUrl = buildViewerUrl(sessionName);
12432
+ if (injectedViewerUrl) {
12433
+ result.tips = [...result.tips || [], makeTip.info(`Open viewer to complete login: ${injectedViewerUrl}`)];
12434
+ }
12435
+ }
12006
12436
  const outputData = isCommandResult2(result) ? result.data : result && typeof result === "object" ? result.data ?? result : result;
12007
12437
  const tips = isCommandResult2(result) ? result.tips : result && typeof result === "object" ? result.tips : void 0;
12008
12438
  if (mode === "json" || mode === "yaml") {
12009
12439
  const finalOutput = {
12010
12440
  data: outputData
12011
12441
  };
12442
+ if (injectedViewerUrl) {
12443
+ finalOutput.viewerUrl = injectedViewerUrl;
12444
+ }
12445
+ if (tips?.length) {
12446
+ finalOutput.tips = tips;
12447
+ }
12012
12448
  if (hookOutputs.length > 0) {
12013
12449
  finalOutput.hooks = hookOutputs;
12014
12450
  }
12015
12451
  console.log(outputFormatter2.format(finalOutput, { mode, color: false, emoji: false }));
12016
12452
  if (tips?.length) {
12017
- for (const tip of tips) console.error(`\u{1F4A1} ${tip}`);
12453
+ for (const tip of tips) console.error(`\u{1F4A1} ${typeof tip === "string" ? tip : tip.message}`);
12018
12454
  }
12019
12455
  } else {
12020
12456
  console.log(outputFormatter2.format(outputData, { mode: "text", color: true, emoji: true }));
12021
12457
  if (tips?.length) {
12022
- for (const tip of tips) console.log(` \u{1F4A1} ${tip}`);
12458
+ for (const tip of tips) console.log(` \u{1F4A1} ${typeof tip === "string" ? tip : tip.message}`);
12023
12459
  }
12024
12460
  if (hookOutputs.length > 0) {
12025
12461
  for (const ho of hookOutputs) {
@@ -12197,7 +12633,7 @@ async function main() {
12197
12633
  const command = process.argv[2];
12198
12634
  const isLongRunning = command === "preview" || command === "serve";
12199
12635
  if (!isLongRunning) {
12200
- const { ensureProcessCanExit } = await import("./browser-DSVV4GHS.js");
12636
+ const { ensureProcessCanExit } = await import("./browser-5CTOA2WS.js");
12201
12637
  await ensureProcessCanExit().catch(() => {
12202
12638
  });
12203
12639
  process.exit(exitCode);