@xbrowser/cli 1.9.5 → 1.9.7

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 {
@@ -7406,7 +7406,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7406
7406
  }
7407
7407
  let targetPageOverride = null;
7408
7408
  if (_target && extraOpts?.cdpEndpoint) {
7409
- const { findTargetPage } = await import("./browser-C63PFGPT.js");
7409
+ const { findTargetPage } = await import("./browser-ZPQP6VW7.js");
7410
7410
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
7411
7411
  if (!targetPageOverride) {
7412
7412
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -7423,7 +7423,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7423
7423
  params = result.data;
7424
7424
  }
7425
7425
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
7426
- const { forwardExec } = await import("./daemon-client-TOUDMIY5.js");
7426
+ const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
7427
7427
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
7428
7428
  if (result) return result;
7429
7429
  }
@@ -8868,7 +8868,7 @@ var PluginInstaller = class {
8868
8868
  import { outputFormatter, OutputFormatter } from "@dyyz1993/xcli-core";
8869
8869
  var formatter = new OutputFormatter();
8870
8870
  function outputResult(result, mode = "text") {
8871
- if (typeof result === "object" && result !== null) {
8871
+ if (mode !== "json" && mode !== "yaml" && typeof result === "object" && result !== null) {
8872
8872
  const r = result;
8873
8873
  if (r.success === false) {
8874
8874
  outputError(r.message ? String(r.message) : "Unknown error");
@@ -10711,6 +10711,50 @@ async function infoFromMarketplacePlugin(slug, loader) {
10711
10711
  }
10712
10712
  return null;
10713
10713
  }
10714
+ function normalizePluginName(name) {
10715
+ return name.replace(/^@[^/]+\//, "").replace(/^xbrowser-plugin-/, "").toLowerCase();
10716
+ }
10717
+ async function searchLocalPlugins(query) {
10718
+ let installed;
10719
+ try {
10720
+ installed = await new PluginInstaller().list();
10721
+ } catch {
10722
+ return [];
10723
+ }
10724
+ if (!installed || installed.length === 0) return [];
10725
+ const q = normalizePluginName(query);
10726
+ if (!q) return [];
10727
+ const results = [];
10728
+ for (const p of installed) {
10729
+ const plugin = p;
10730
+ const name = String(plugin.name ?? "");
10731
+ const norm = normalizePluginName(name);
10732
+ if (norm === q) {
10733
+ results.push({ plugin, rank: 0 });
10734
+ } else if (norm.startsWith(q) || q.startsWith(norm)) {
10735
+ results.push({ plugin, rank: 1 });
10736
+ } else if (norm.includes(q) || q.includes(norm)) {
10737
+ results.push({ plugin, rank: 2 });
10738
+ }
10739
+ }
10740
+ results.sort((a, b) => a.rank - b.rank);
10741
+ const runtimeInfo = await buildRuntimePluginInfo().catch(() => /* @__PURE__ */ new Map());
10742
+ return results.map(({ plugin }) => {
10743
+ const name = String(plugin.name ?? "");
10744
+ const metadata = plugin.metadata;
10745
+ const staticCommands = metadata?.commands;
10746
+ const rt = runtimeInfo.get(name);
10747
+ const commands = rt?.commands || staticCommands;
10748
+ return {
10749
+ name: "@xbrowser/" + name,
10750
+ slug: name,
10751
+ description: metadata?.description,
10752
+ version: metadata?.version,
10753
+ commands,
10754
+ source: "local"
10755
+ };
10756
+ });
10757
+ }
10714
10758
  async function handleSearch(args, options, mode) {
10715
10759
  const query = args[0] || "";
10716
10760
  applyRegistryOverride(options);
@@ -10718,6 +10762,8 @@ async function handleSearch(args, options, mode) {
10718
10762
  const searchLimit = options.limit ? Number(options.limit) : 20;
10719
10763
  const searchOpts = { query, tag: options.tag, site: options.site, limit: searchLimit };
10720
10764
  const results = [];
10765
+ const localResults = await searchLocalPlugins(query);
10766
+ results.push(...localResults);
10721
10767
  const loader = await getPluginLoader();
10722
10768
  const pluginResults = await searchFromMarketplacePlugin2(searchOpts, loader);
10723
10769
  results.push(...pluginResults);
@@ -10730,15 +10776,24 @@ async function handleSearch(args, options, mode) {
10730
10776
  } catch {
10731
10777
  }
10732
10778
  }
10779
+ const seen = /* @__PURE__ */ new Set();
10780
+ const deduped = [];
10781
+ for (const r of results) {
10782
+ const key = normalizePluginName(String(r.name ?? ""));
10783
+ if (key && !seen.has(key)) {
10784
+ seen.add(key);
10785
+ deduped.push(r);
10786
+ }
10787
+ }
10733
10788
  if (mode === "json") {
10734
- outputEnvelope({ success: true, data: { results, total: results.length } }, { command: "plugin search" }, mode);
10789
+ outputEnvelope({ success: true, data: { results: deduped, total: deduped.length } }, { command: "plugin search" }, mode);
10735
10790
  } else {
10736
- if (results.length === 0) {
10791
+ if (deduped.length === 0) {
10737
10792
  console.log("No plugins found");
10738
10793
  return;
10739
10794
  }
10740
- for (const r of results) {
10741
- const src = r.source === "marketplace" ? "[marketplace]" : "[npm]";
10795
+ for (const r of deduped) {
10796
+ const src = r.source === "marketplace" ? "[marketplace]" : r.source === "local" ? "[local]" : "[npm]";
10742
10797
  const slug = r.slug ? ` (${r.slug})` : "";
10743
10798
  console.log(` ${src} ${r.name}${slug}`);
10744
10799
  if (r.description) console.log(` ${r.description}`);
@@ -10746,7 +10801,7 @@ async function handleSearch(args, options, mode) {
10746
10801
  if (r.downloads) console.log(` Downloads: ${r.downloads}`);
10747
10802
  console.log("");
10748
10803
  }
10749
- console.log(`Total: ${results.length} plugins`);
10804
+ console.log(`Total: ${deduped.length} plugins`);
10750
10805
  }
10751
10806
  }
10752
10807
  async function handlePluginInfo(args, options, mode) {
@@ -10882,7 +10937,7 @@ async function handlePlugin(args, options, mode) {
10882
10937
  } catch {
10883
10938
  }
10884
10939
  try {
10885
- const { daemonPing } = await import("./daemon-client-TOUDMIY5.js");
10940
+ const { daemonPing } = await import("./daemon-client-CMRAWXTN.js");
10886
10941
  if (await daemonPing()) {
10887
10942
  await fetch("http://localhost:9224/rpc", {
10888
10943
  method: "POST",
@@ -11040,7 +11095,8 @@ async function handleRecord(args, options, mode) {
11040
11095
  const url = options.url;
11041
11096
  const sessionName = options.session || "default";
11042
11097
  const cdpEndpoint = options.cdp;
11043
- const result = await forwardRecordStart(sessionName, url, cdpEndpoint);
11098
+ const stream = options.stream || "clean";
11099
+ const result = await forwardRecordStart(sessionName, url, cdpEndpoint, stream);
11044
11100
  if (!result.ok) {
11045
11101
  outputError(String(result.error || "Failed to start recording"));
11046
11102
  return;
@@ -11335,7 +11391,15 @@ async function handleReplay(args, options, mode) {
11335
11391
  const absPath = await import("path").then((p) => p.resolve(filePath));
11336
11392
  const result = await forwardReplay(absPath, sessionName, slowMo);
11337
11393
  if (!result.ok) {
11338
- outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
11394
+ if (mode === "json" || mode === "yaml") {
11395
+ outputResult({
11396
+ ...result,
11397
+ success: false,
11398
+ message: result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"
11399
+ }, mode);
11400
+ } else {
11401
+ outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
11402
+ }
11339
11403
  return;
11340
11404
  }
11341
11405
  outputResult(result, mode);
@@ -11439,7 +11503,7 @@ async function handleFilter(args, _mode, options) {
11439
11503
  }
11440
11504
  }
11441
11505
  async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
11442
- const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-KPU4YQAE.js");
11506
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RFOPX4SF.js");
11443
11507
  const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
11444
11508
  const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync13 } = await import("fs");
11445
11509
  const { join: join14 } = await import("path");
@@ -13376,7 +13440,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13376
13440
  }
13377
13441
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
13378
13442
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
13379
- const { forwardExec } = await import("./daemon-client-TOUDMIY5.js");
13443
+ const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
13380
13444
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
13381
13445
  const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
13382
13446
  const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
@@ -13416,7 +13480,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13416
13480
  const targetPage = pages[cmdTabIndex];
13417
13481
  await targetPage.bringToFront().catch(() => {
13418
13482
  });
13419
- const { setActivePage: setActivePage2 } = await import("./browser-C63PFGPT.js");
13483
+ const { setActivePage: setActivePage2 } = await import("./browser-ZPQP6VW7.js");
13420
13484
  setActivePage2(session, targetPage);
13421
13485
  }
13422
13486
  }
@@ -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.5",
3
+ "version": "1.9.7",
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": {