@xbrowser/cli 1.9.4 → 1.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1812,6 +1812,13 @@ interface RecordingStep {
1812
1812
  networkId: number;
1813
1813
  paramName: string;
1814
1814
  }>;
1815
+ signals: ActionSignal[];
1816
+ }
1817
+ /** Success / verification signal attached to a recording step */
1818
+ interface ActionSignal {
1819
+ type: 'network_success' | 'url_change' | 'dialog' | 'dom_change';
1820
+ value?: string;
1821
+ label: string;
1815
1822
  }
1816
1823
  interface RecordingSummary {
1817
1824
  startUrl: string;
@@ -1871,12 +1878,17 @@ declare class SessionRecorder {
1871
1878
  private lastActionTs;
1872
1879
  private activePages;
1873
1880
  private lastKnownUrl;
1874
- /** Dedup window: after a CDP command action, ignore matching action signals within this window */
1875
- private cdpActionDedup;
1881
+ /** Dedup map: key = normalizedType|tag|value expiration timestamp.
1882
+ * Replaces old single-entry cdpActionDedup for bidirectional dedup. */
1883
+ private dedupMap;
1884
+ private dedupActionCount;
1876
1885
  /** Network dedup: last request key for short-window dedup */
1877
1886
  private _lastRequestKey;
1878
1887
  private _lastRequestTs;
1879
1888
  private _isRecording;
1889
+ private streamMode;
1890
+ /** Ambient action types: filtered out in clean mode */
1891
+ private static readonly AMBIENT_TYPES;
1880
1892
  constructor(context: XBContext, page: XBPage, sessionName: string);
1881
1893
  get isRecording(): boolean;
1882
1894
  get actionCount(): number;
@@ -1888,6 +1900,8 @@ declare class SessionRecorder {
1888
1900
  url?: string;
1889
1901
  element?: UserAction['element'];
1890
1902
  }): Promise<void>;
1903
+ /** Generate dedup key for an action (normalized type + selector/tag + value). */
1904
+ private dedupKey;
1891
1905
  get networkCount(): number;
1892
1906
  getLiveData(): RecordingData;
1893
1907
  addManualCheckpoint(type: string, hint: string, selector?: string): CheckpointEntry;
@@ -1898,7 +1912,9 @@ declare class SessionRecorder {
1898
1912
  get controlFilePath(): string;
1899
1913
  /** Path to the stop signal file (written by `record stop`). */
1900
1914
  get stopSignalPath(): string;
1901
- start(url?: string): Promise<void>;
1915
+ start(url?: string, options?: {
1916
+ stream?: 'clean' | 'raw';
1917
+ }): Promise<void>;
1902
1918
  stop(): Promise<{
1903
1919
  data: RecordingData;
1904
1920
  summary: RecordingSummary;
@@ -1932,6 +1948,11 @@ declare class SessionRecorder {
1932
1948
  */
1933
1949
  private captureClickContext;
1934
1950
  private detectCheckpoints;
1951
+ /**
1952
+ * After a key action (click/submit/input), check for success signals:
1953
+ * URL changes, recent successful network responses, dialog appearances.
1954
+ */
1955
+ private verifyAction;
1935
1956
  private flushToDisk;
1936
1957
  private writeFinalOutput;
1937
1958
  private buildData;
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  startDaemonProcess,
45
45
  stopDaemonProcess,
46
46
  version
47
- } from "./chunk-53LRNYPG.js";
47
+ } from "./chunk-YAUG3NSI.js";
48
48
  import {
49
49
  generateBashScript,
50
50
  generateJSScript,
@@ -61,7 +61,7 @@ import {
61
61
  } from "./chunk-ANVL2ID2.js";
62
62
  import {
63
63
  SessionRecorder
64
- } from "./chunk-YSCY52UJ.js";
64
+ } from "./chunk-CH5ZQV64.js";
65
65
  import {
66
66
  addKnownIssue,
67
67
  getKnowledgePath,
@@ -86,7 +86,7 @@ import {
86
86
  resolveLaunchOpts,
87
87
  saveSessionDiskMeta,
88
88
  setActivePage
89
- } from "./chunk-MSQIPXKE.js";
89
+ } from "./chunk-5JSOGINQ.js";
90
90
  import "./chunk-VJNMAWPZ.js";
91
91
  import "./chunk-TNEN6VQ2.js";
92
92
  import {
@@ -6678,6 +6678,22 @@ async function checkPluginLoginRequired(options) {
6678
6678
  const requiresLogin = command.requiresLogin === true || site.config?.requiresLogin === true || loginConfig?.requiresLogin === true || command.loginRequired === "required";
6679
6679
  if (!requiresLogin) return { ok: true };
6680
6680
  const pluginName = site.name || "plugin";
6681
+ const siteConfig = site.config;
6682
+ const configIsLogin = siteConfig?.isLogin;
6683
+ if (configIsLogin) {
6684
+ try {
6685
+ const loggedIn = await configIsLogin(ctx);
6686
+ if (loggedIn) return { ok: true };
6687
+ return buildLoginRequired({
6688
+ plugin: pluginName,
6689
+ command: commandName,
6690
+ reason: "config.isLogin returned false",
6691
+ sessionName,
6692
+ loginConfig
6693
+ });
6694
+ } catch {
6695
+ }
6696
+ }
6681
6697
  if (typeof site.isLoggedIn === "function") {
6682
6698
  try {
6683
6699
  const loggedIn = await site.isLoggedIn(ctx);
@@ -7390,7 +7406,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7390
7406
  }
7391
7407
  let targetPageOverride = null;
7392
7408
  if (_target && extraOpts?.cdpEndpoint) {
7393
- const { findTargetPage } = await import("./browser-C63PFGPT.js");
7409
+ const { findTargetPage } = await import("./browser-ZPQP6VW7.js");
7394
7410
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
7395
7411
  if (!targetPageOverride) {
7396
7412
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -7407,7 +7423,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7407
7423
  params = result.data;
7408
7424
  }
7409
7425
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
7410
- const { forwardExec } = await import("./daemon-client-TOUDMIY5.js");
7426
+ const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
7411
7427
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
7412
7428
  if (result) return result;
7413
7429
  }
@@ -8852,7 +8868,7 @@ var PluginInstaller = class {
8852
8868
  import { outputFormatter, OutputFormatter } from "@dyyz1993/xcli-core";
8853
8869
  var formatter = new OutputFormatter();
8854
8870
  function outputResult(result, mode = "text") {
8855
- if (typeof result === "object" && result !== null) {
8871
+ if (mode !== "json" && mode !== "yaml" && typeof result === "object" && result !== null) {
8856
8872
  const r = result;
8857
8873
  if (r.success === false) {
8858
8874
  outputError(r.message ? String(r.message) : "Unknown error");
@@ -10866,7 +10882,7 @@ async function handlePlugin(args, options, mode) {
10866
10882
  } catch {
10867
10883
  }
10868
10884
  try {
10869
- const { daemonPing } = await import("./daemon-client-TOUDMIY5.js");
10885
+ const { daemonPing } = await import("./daemon-client-CMRAWXTN.js");
10870
10886
  if (await daemonPing()) {
10871
10887
  await fetch("http://localhost:9224/rpc", {
10872
10888
  method: "POST",
@@ -11024,7 +11040,8 @@ async function handleRecord(args, options, mode) {
11024
11040
  const url = options.url;
11025
11041
  const sessionName = options.session || "default";
11026
11042
  const cdpEndpoint = options.cdp;
11027
- const result = await forwardRecordStart(sessionName, url, cdpEndpoint);
11043
+ const stream = options.stream || "clean";
11044
+ const result = await forwardRecordStart(sessionName, url, cdpEndpoint, stream);
11028
11045
  if (!result.ok) {
11029
11046
  outputError(String(result.error || "Failed to start recording"));
11030
11047
  return;
@@ -11319,7 +11336,15 @@ async function handleReplay(args, options, mode) {
11319
11336
  const absPath = await import("path").then((p) => p.resolve(filePath));
11320
11337
  const result = await forwardReplay(absPath, sessionName, slowMo);
11321
11338
  if (!result.ok) {
11322
- outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
11339
+ if (mode === "json" || mode === "yaml") {
11340
+ outputResult({
11341
+ ...result,
11342
+ success: false,
11343
+ message: result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"
11344
+ }, mode);
11345
+ } else {
11346
+ outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
11347
+ }
11323
11348
  return;
11324
11349
  }
11325
11350
  outputResult(result, mode);
@@ -11423,7 +11448,7 @@ async function handleFilter(args, _mode, options) {
11423
11448
  }
11424
11449
  }
11425
11450
  async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
11426
- const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-KPU4YQAE.js");
11451
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RFOPX4SF.js");
11427
11452
  const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
11428
11453
  const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync13 } = await import("fs");
11429
11454
  const { join: join14 } = await import("path");
@@ -13360,7 +13385,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13360
13385
  }
13361
13386
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
13362
13387
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
13363
- const { forwardExec } = await import("./daemon-client-TOUDMIY5.js");
13388
+ const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
13364
13389
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
13365
13390
  const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
13366
13391
  const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
@@ -13400,7 +13425,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13400
13425
  const targetPage = pages[cmdTabIndex];
13401
13426
  await targetPage.bringToFront().catch(() => {
13402
13427
  });
13403
- const { setActivePage: setActivePage2 } = await import("./browser-C63PFGPT.js");
13428
+ const { setActivePage: setActivePage2 } = await import("./browser-ZPQP6VW7.js");
13404
13429
  setActivePage2(session, targetPage);
13405
13430
  }
13406
13431
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SessionRecorder
3
- } from "./chunk-TTZNR3QP.js";
3
+ } from "./chunk-C4O73DQL.js";
4
4
  import "./chunk-KFQGP6VL.js";
5
5
  export {
6
6
  SessionRecorder
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SessionRecorder
3
- } from "./chunk-YSCY52UJ.js";
3
+ } from "./chunk-CH5ZQV64.js";
4
4
  import "./chunk-OZKD3W4X.js";
5
5
  import "./chunk-KFQGP6VL.js";
6
6
  export {
@@ -56,15 +56,17 @@ var SessionReplayer = class {
56
56
  await this.replayTrajectory(action.trajectory);
57
57
  }
58
58
  await this.replayAction(action);
59
+ if (action.type !== "resize" && action.type !== "clipboard" && action.type !== "visibility") {
60
+ try {
61
+ await this.page.waitForLoadState("domcontentloaded", this.opts.stepTimeout);
62
+ } catch {
63
+ }
64
+ }
59
65
  success++;
60
66
  } catch (e) {
61
67
  const err = e instanceof Error ? e : new Error(String(e));
62
68
  this.opts.onError?.(action, err);
63
- if (action.type === "navigation") {
64
- skipped++;
65
- } else {
66
- failed++;
67
- }
69
+ failed++;
68
70
  }
69
71
  if (i < actions.length - 1) {
70
72
  await new Promise((r) => setTimeout(r, this.opts.stepDelay));
@@ -78,41 +80,36 @@ var SessionReplayer = class {
78
80
  const timeout = this.opts.stepTimeout;
79
81
  switch (action.type) {
80
82
  case "navigation":
81
- await page.goto(action.url, { waitUntil: "domcontentloaded", timeout });
83
+ await page.goto(action.url, { waitUntil: "load", timeout });
82
84
  break;
83
85
  case "goto":
84
- await page.goto(action.url, { waitUntil: "domcontentloaded", timeout });
86
+ await page.goto(action.url, { waitUntil: "load", timeout });
85
87
  break;
86
88
  case "click":
87
89
  case "cdp-click": {
88
- const selector = this.resolveSelector(action);
89
- await page.waitForSelector(selector, { state: "visible", timeout });
90
+ const selector = await this.resolveAndWait(action);
90
91
  await page.click(selector, { timeout });
91
92
  break;
92
93
  }
93
94
  case "input": {
94
- const selector = this.resolveSelector(action);
95
- await page.waitForSelector(selector, { state: "visible", timeout });
95
+ const selector = await this.resolveAndWait(action);
96
96
  await page.fill(selector, action.value ?? "", { timeout });
97
97
  break;
98
98
  }
99
99
  case "cdp-fill": {
100
- const selector = this.resolveSelector(action);
101
- await page.waitForSelector(selector, { state: "visible", timeout });
100
+ const selector = await this.resolveAndWait(action);
102
101
  await page.fill(selector, action.value ?? "", { timeout });
103
102
  break;
104
103
  }
105
104
  case "change": {
106
- const selector = this.resolveSelector(action);
107
- await page.waitForSelector(selector, { state: "visible", timeout });
105
+ const selector = await this.resolveAndWait(action);
108
106
  if (action.value) {
109
107
  await page.selectOption(selector, action.value);
110
108
  }
111
109
  break;
112
110
  }
113
111
  case "filechooser": {
114
- const selector = this.resolveSelector(action);
115
- await page.waitForSelector(selector, { state: "visible", timeout });
112
+ const selector = await this.resolveAndWait(action);
116
113
  const files = this.resolveFiles(action);
117
114
  if (files.length > 0) {
118
115
  await page.setInputFiles(selector, files);
@@ -155,11 +152,8 @@ var SessionReplayer = class {
155
152
  break;
156
153
  }
157
154
  case "hover": {
158
- const selector = this.resolveSelector(action);
159
- if (selector) {
160
- await page.waitForSelector(selector, { state: "visible", timeout });
161
- await page.hover(selector);
162
- }
155
+ const selector = await this.resolveAndWait(action);
156
+ await page.hover(selector);
163
157
  break;
164
158
  }
165
159
  case "drag": {
@@ -199,10 +193,9 @@ var SessionReplayer = class {
199
193
  break;
200
194
  }
201
195
  case "focus": {
202
- const selector = this.resolveSelector(action);
203
- if (selector && action.focus?.focusType === "focus") {
204
- await page.waitForSelector(selector, { state: "visible", timeout });
205
- await page.click(selector, { timeout });
196
+ const selector = await this.resolveAndWait(action);
197
+ if (action.focus?.focusType === "focus") {
198
+ await page.locator(selector).focus();
206
199
  }
207
200
  break;
208
201
  }
@@ -210,10 +203,16 @@ var SessionReplayer = class {
210
203
  break;
211
204
  }
212
205
  case "submit": {
213
- const selector = this.resolveSelector(action);
214
- if (selector) {
215
- await page.click(selector, { timeout });
216
- }
206
+ const selector = await this.resolveAndWait(action);
207
+ await page.evaluate((sel) => {
208
+ const form = document.querySelector(sel);
209
+ if (!form) return;
210
+ if (typeof form.requestSubmit === "function") {
211
+ form.requestSubmit();
212
+ } else {
213
+ form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
214
+ }
215
+ }, selector);
217
216
  break;
218
217
  }
219
218
  case "scroll": {
@@ -239,12 +238,10 @@ var SessionReplayer = class {
239
238
  await page.mouse.move(x, y);
240
239
  }
241
240
  }
242
- /** Resolve the best selector for an action */
241
+ /** Resolve the best selector for an action (primary selector only) */
243
242
  resolveSelector(action) {
244
243
  const el = action.element;
245
244
  if (!el) return "";
246
- if (el.textFallback) {
247
- }
248
245
  if (el.selector) {
249
246
  return el.selector;
250
247
  }
@@ -253,6 +250,41 @@ var SessionReplayer = class {
253
250
  }
254
251
  return "";
255
252
  }
253
+ /**
254
+ * X2: Wait for an element using the best available selector, with
255
+ * confidence-based fallback support.
256
+ *
257
+ * Returns the first matching selector, or throws if none match.
258
+ * Fallback order:
259
+ * 1. Primary CSS selector (always tried first)
260
+ * 2. textFallback selector (for low-confidence selectors when primary fails)
261
+ * 3. Tag-based fallback (last resort)
262
+ */
263
+ async resolveAndWait(action) {
264
+ const el = action.element;
265
+ if (!el) throw new Error("No element metadata");
266
+ const page = this.page;
267
+ const timeout = this.opts.stepTimeout;
268
+ const candidates = [];
269
+ if (el.selector) candidates.push(el.selector);
270
+ if (el.confidence === "low" && el.textFallback?.selector) {
271
+ if (!candidates.includes(el.textFallback.selector)) {
272
+ candidates.push(el.textFallback.selector);
273
+ }
274
+ }
275
+ if (el.tag && !candidates.includes(el.tag)) {
276
+ candidates.push(el.tag);
277
+ }
278
+ if (candidates.length === 0) throw new Error("No selector available for element");
279
+ for (const sel of candidates) {
280
+ try {
281
+ await page.waitForSelector(sel, { state: "visible", timeout });
282
+ return sel;
283
+ } catch {
284
+ }
285
+ }
286
+ throw new Error(`Element not found, tried: ${candidates.join(", ")}`);
287
+ }
256
288
  /** Resolve file payloads from a filechooser action */
257
289
  resolveFiles(action) {
258
290
  if (!action.files?.fileData) return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbrowser/cli",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
4
4
  "description": "Browser automation CLI for web scraping, headless browsing, SEO analysis, and AI agent workflows. A command-line alternative to Playwright, Puppeteer, and Selenium.",
5
5
  "type": "module",
6
6
  "bin": {