@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.
@@ -65,16 +65,28 @@ function getSelectorGeneratorScript() {
65
65
  if (isUnique(root, s)) return { selector: s, strategy: 'id+tag', confidence: 'high' };
66
66
  }
67
67
 
68
- // [data-testid]
69
- for (var si = 0; si < STABLE_ATTRS.length; si++) {
70
- var v = el.getAttribute(STABLE_ATTRS[si]);
71
- if (v) {
72
- var s = '[' + STABLE_ATTRS[si] + '="' + esc(v) + '"]';
73
- if (isUnique(root, s)) return { selector: s, strategy: 'testid', confidence: 'high' };
74
- s = tag + s;
75
- if (isUnique(root, s)) return { selector: s, strategy: 'testid+tag', confidence: 'high' };
76
- }
77
- }
68
+ // [data-testid]
69
+ for (var si = 0; si < STABLE_ATTRS.length; si++) {
70
+ var v = el.getAttribute(STABLE_ATTRS[si]);
71
+ if (v) {
72
+ var s = '[' + STABLE_ATTRS[si] + '="' + esc(v) + '"]';
73
+ if (isUnique(root, s)) return { selector: s, strategy: 'testid', confidence: 'high' };
74
+ s = tag + s;
75
+ if (isUnique(root, s)) return { selector: s, strategy: 'testid+tag', confidence: 'high' };
76
+ }
77
+ }
78
+
79
+ // [data-*] \u2014 other data attributes beyond known testids
80
+ for (var ai = 0; ai < el.attributes.length; ai++) {
81
+ var a = el.attributes[ai];
82
+ if (!a.name.startsWith('data-')) continue;
83
+ if (STABLE_ATTRS.indexOf(a.name) !== -1) continue;
84
+ if (!a.value || a.value.length > 60 || a.value.indexOf('http') === 0) continue;
85
+ var s = '[' + a.name + '="' + esc(a.value) + '"]';
86
+ if (isUnique(root, s)) return { selector: s, strategy: 'data-attr', confidence: 'high' };
87
+ s = tag + s;
88
+ if (isUnique(root, s)) return { selector: s, strategy: 'data-attr+tag', confidence: 'high' };
89
+ }
78
90
 
79
91
  // [name]
80
92
  var name = el.getAttribute('name');
@@ -83,14 +95,35 @@ function getSelectorGeneratorScript() {
83
95
  if (isUnique(root, s)) return { selector: s, strategy: 'name', confidence: 'high' };
84
96
  }
85
97
 
86
- // [aria-label]
87
- var aria = el.getAttribute('aria-label');
88
- if (aria) {
89
- var s = '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
90
- if (isUnique(root, s)) return { selector: s, strategy: 'aria-label', confidence: 'high' };
91
- s = tag + s;
92
- if (isUnique(root, s)) return { selector: s, strategy: 'aria-label+tag', confidence: 'high' };
93
- }
98
+ // [aria-label]
99
+ var aria = el.getAttribute('aria-label');
100
+ if (aria) {
101
+ var s = '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
102
+ if (isUnique(root, s)) return { selector: s, strategy: 'aria-label', confidence: 'high' };
103
+ s = tag + s;
104
+ if (isUnique(root, s)) return { selector: s, strategy: 'aria-label+tag', confidence: 'high' };
105
+ }
106
+
107
+ // [role][name] combo
108
+ var role = el.getAttribute('role');
109
+ if (role) {
110
+ var name2 = el.getAttribute('name');
111
+ if (name2) {
112
+ var s = '[role="' + role + '"][name="' + esc(name2) + '"]';
113
+ if (isUnique(root, s)) return { selector: s, strategy: 'role+name', confidence: 'high' };
114
+ }
115
+ // [role][aria-label] combo
116
+ if (aria) {
117
+ var s = '[role="' + role + '"][aria-label="' + esc(aria.substring(0, 60)) + '"]';
118
+ if (isUnique(root, s)) return { selector: s, strategy: 'role+aria-label', confidence: 'high' };
119
+ }
120
+ // [role] alone
121
+ var text2 = (el.textContent || '').trim();
122
+ if (text2) {
123
+ var s = '[role="' + role + '"]';
124
+ if (isUnique(root, s)) return { selector: s, strategy: 'role', confidence: 'high' };
125
+ }
126
+ }
94
127
 
95
128
  // [placeholder]
96
129
  var ph = el.getAttribute('placeholder');
@@ -113,11 +146,12 @@ function getSelectorGeneratorScript() {
113
146
  if (isUnique(root, s)) return { selector: s, strategy: 'title', confidence: 'high' };
114
147
  }
115
148
 
116
- // Unique attribute (skip URL-like, long)
117
- var skipAttr = {class:1,style:1,id:1,name:1,'aria-label':1,placeholder:1,alt:1,title:1,role:1,src:1,href:1,action:1,'data-src':1,'data-href':1};
118
- for (var ai = 0; ai < el.attributes.length; ai++) {
119
- var a = el.attributes[ai];
120
- if (skipAttr[a.name] || a.name.startsWith('data-') || a.name.startsWith('aria-')) continue;
149
+ // Unique attribute (skip URL-like, long, and common attrs handled above)
150
+ // Note: non-testid data-* attrs are already tried in [data-*] section above
151
+ var skipAttr = {class:1,style:1,id:1,name:1,'aria-label':1,placeholder:1,alt:1,title:1,role:1,src:1,href:1,action:1,'data-src':1,'data-href':1};
152
+ for (var ai = 0; ai < el.attributes.length; ai++) {
153
+ var a = el.attributes[ai];
154
+ if (skipAttr[a.name] || a.name.startsWith('data-') || a.name.startsWith('aria-')) continue;
121
155
  if (a.value && a.value.length > 2 && a.value.length <= 60) {
122
156
  var s = tag + '[' + a.name + '="' + esc(a.value) + '"]';
123
157
  if (isUnique(root, s)) return { selector: s, strategy: 'attribute', confidence: 'medium' };
@@ -154,10 +188,16 @@ function getSelectorGeneratorScript() {
154
188
  var s = tag + '.' + esc(cls[i]) + '.' + esc(cls[j]);
155
189
  if (isUnique(root, s)) return { selector: s, strategy: 'tag+class-combo', confidence: 'medium' };
156
190
  }
157
- }
158
- }
191
+ }
192
+ }
193
+
194
+ // tag[aria-label] fallback \u2014 aria-label exists but wasn't globally unique
195
+ if (aria) {
196
+ var s = tag + '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
197
+ if (isUnique(root, s)) return { selector: s, strategy: 'tag+aria-label', confidence: 'medium' };
198
+ }
159
199
 
160
- // Parent scope
200
+ // Parent scope
161
201
  var parent = el.parentElement;
162
202
  if (parent && parent !== root) {
163
203
  if (parent.id) {
@@ -1224,12 +1264,28 @@ var SessionRecorder = class _SessionRecorder {
1224
1264
  activePages = /* @__PURE__ */ new Set();
1225
1265
  lastKnownUrl = "";
1226
1266
  // Track URL to detect real navigation changes
1227
- /** Dedup window: after a CDP command action, ignore matching action signals within this window */
1228
- cdpActionDedup = null;
1267
+ /** Dedup map: key = normalizedType|tag|value expiration timestamp.
1268
+ * Replaces old single-entry cdpActionDedup for bidirectional dedup. */
1269
+ dedupMap = /* @__PURE__ */ new Map();
1270
+ dedupActionCount = 0;
1271
+ // counter for periodic cleanup
1229
1272
  /** Network dedup: last request key for short-window dedup */
1230
1273
  _lastRequestKey = "";
1231
1274
  _lastRequestTs = 0;
1232
1275
  _isRecording = false;
1276
+ streamMode = "clean";
1277
+ /** Ambient action types: filtered out in clean mode */
1278
+ static AMBIENT_TYPES = /* @__PURE__ */ new Set([
1279
+ "hover",
1280
+ "focus",
1281
+ "scroll",
1282
+ "visibility",
1283
+ "resize",
1284
+ "clipboard",
1285
+ "touch",
1286
+ "contextmenu",
1287
+ "drag"
1288
+ ]);
1233
1289
  constructor(context, page, sessionName) {
1234
1290
  this.context = context;
1235
1291
  this.page = page;
@@ -1243,13 +1299,10 @@ var SessionRecorder = class _SessionRecorder {
1243
1299
  }
1244
1300
  /** Record an action triggered by a CDP command (e.g. xbrowser fill/click/goto) */
1245
1301
  async recordCommandAction(action) {
1246
- const normalizedType = action.type === "cdp-fill" ? "input" : action.type === "cdp-click" ? "click" : action.type;
1247
- const recent = this.actions[this.actions.length - 1];
1248
- if (recent && Date.now() - recent.timestamp < 1500) {
1249
- const typeMatch = recent.type === action.type || recent.type === normalizedType;
1250
- const valueMatch = !action.value || recent.value === action.value;
1251
- const selectorMatch = !action.selector || recent.element?.selector && (recent.element.selector === action.selector || recent.element.selector.endsWith(" " + action.selector) || action.selector.endsWith(" " + recent.element.selector));
1252
- if (typeMatch && valueMatch && selectorMatch) {
1302
+ const dedupFromAction = this.dedupKey(action.type, action.selector, action.element?.tag, action.value);
1303
+ if (dedupFromAction) {
1304
+ const expires = this.dedupMap.get(dedupFromAction);
1305
+ if (expires && Date.now() < expires) {
1253
1306
  return;
1254
1307
  }
1255
1308
  }
@@ -1271,18 +1324,26 @@ var SessionRecorder = class _SessionRecorder {
1271
1324
  element: actionToPush.element
1272
1325
  });
1273
1326
  this.lastActionTs = ts;
1274
- this.cdpActionDedup = {
1275
- type: normalizedType,
1276
- value: action.value,
1277
- selector: action.selector,
1278
- until: Date.now() + 1500
1279
- };
1327
+ this.dedupMap.set(dedupFromAction, Date.now() + 2e3);
1328
+ this.dedupActionCount++;
1329
+ if (this.dedupActionCount % 200 === 0) {
1330
+ const now = Date.now();
1331
+ for (const [k, v] of this.dedupMap) {
1332
+ if (now >= v) this.dedupMap.delete(k);
1333
+ }
1334
+ }
1280
1335
  if (action.url && action.url !== "about:blank") {
1281
1336
  this.lastKnownUrl = action.url;
1282
1337
  } else if (action.type === "goto" && action.value && action.value !== "about:blank") {
1283
1338
  this.lastKnownUrl = action.value;
1284
1339
  }
1285
1340
  }
1341
+ /** Generate dedup key for an action (normalized type + selector/tag + value). */
1342
+ dedupKey(type, selector, tag, value) {
1343
+ const normType = type === "cdp-click" ? "click" : type === "cdp-fill" ? "input" : type === "cdp-eval" ? "eval" : type;
1344
+ const selOrTag = selector || tag || "";
1345
+ return `${normType}|${selOrTag}|${value || ""}`;
1346
+ }
1286
1347
  get networkCount() {
1287
1348
  return this.network.length;
1288
1349
  }
@@ -1320,7 +1381,7 @@ var SessionRecorder = class _SessionRecorder {
1320
1381
  return join(this.recordingsDir, ".stop");
1321
1382
  }
1322
1383
  // ─── Start ──────────────────────────────────────────────────────
1323
- async start(url) {
1384
+ async start(url, options) {
1324
1385
  if (this._isRecording) throw new Error("Already recording");
1325
1386
  this._isRecording = true;
1326
1387
  this.startedAt = Date.now();
@@ -1329,6 +1390,7 @@ var SessionRecorder = class _SessionRecorder {
1329
1390
  this.contextChanges = [];
1330
1391
  this.checkpoints = [];
1331
1392
  this.checkpointCounter = 0;
1393
+ this.streamMode = options?.stream ?? "clean";
1332
1394
  this.lastKnownUrl = this.page.url();
1333
1395
  await this.page.addInitScript(getSelectorGeneratorScript());
1334
1396
  await this.page.addInitScript(ACTION_SIGNAL_SCRIPT);
@@ -1897,15 +1959,16 @@ var SessionRecorder = class _SessionRecorder {
1897
1959
  }
1898
1960
  for (const raw of pending) {
1899
1961
  if (raw.ts <= this.lastActionTs) continue;
1900
- if (this.cdpActionDedup && Date.now() < this.cdpActionDedup.until) {
1901
- const dedup = this.cdpActionDedup;
1902
- const typeMatch = raw.type === dedup.type;
1903
- const valueMatch = !dedup.value || raw.value === dedup.value;
1904
- const selectorMatch = !dedup.selector || raw.element?.selector && (raw.element.selector === dedup.selector || raw.element.selector.endsWith(" " + dedup.selector) || dedup.selector.endsWith(" " + raw.element.selector));
1905
- if (typeMatch && valueMatch && selectorMatch) {
1962
+ const dedupFromSignal = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
1963
+ if (dedupFromSignal) {
1964
+ const expires = this.dedupMap.get(dedupFromSignal);
1965
+ if (expires && Date.now() < expires) {
1906
1966
  continue;
1907
1967
  }
1908
1968
  }
1969
+ if (this.streamMode === "clean" && _SessionRecorder.AMBIENT_TYPES.has(raw.type)) {
1970
+ continue;
1971
+ }
1909
1972
  this.actionCounter++;
1910
1973
  let clickContext;
1911
1974
  if (raw.type === "click" && raw.x !== void 0 && raw.y !== void 0) {
@@ -1930,6 +1993,18 @@ var SessionRecorder = class _SessionRecorder {
1930
1993
  const pushed = this.actions[this.actions.length - 1];
1931
1994
  pushed.elementScreenshot = await this.captureElementScreenshot(page, raw);
1932
1995
  this.lastActionTs = raw.ts;
1996
+ const dedupKey = this.dedupKey(raw.type, raw.element?.selector, raw.element?.tag, raw.value);
1997
+ if (dedupKey) {
1998
+ this.dedupMap.set(dedupKey, Date.now() + 2e3);
1999
+ this.dedupActionCount++;
2000
+ }
2001
+ const verifyTypes = /* @__PURE__ */ new Set(["click", "submit", "input", "change", "cdp-click", "cdp-fill"]);
2002
+ if (verifyTypes.has(raw.type)) {
2003
+ const signals = await this.verifyAction(page, pushed).catch(() => []);
2004
+ if (signals.length > 0) {
2005
+ pushed.__signals = signals;
2006
+ }
2007
+ }
1933
2008
  if (raw.type === "click" || raw.type === "navigate" || raw.type === "submit") {
1934
2009
  const detected = await this.detectCheckpoints(page);
1935
2010
  for (const cp of detected) {
@@ -2112,6 +2187,44 @@ var SessionRecorder = class _SessionRecorder {
2112
2187
  return [];
2113
2188
  }
2114
2189
  }
2190
+ // ─── Verification: collect success signals after key actions ──────
2191
+ /**
2192
+ * After a key action (click/submit/input), check for success signals:
2193
+ * URL changes, recent successful network responses, dialog appearances.
2194
+ */
2195
+ async verifyAction(page, action) {
2196
+ const signals = [];
2197
+ try {
2198
+ const currentUrl = page.url();
2199
+ if (currentUrl && currentUrl !== "about:blank" && currentUrl !== action.url) {
2200
+ signals.push({ type: "url_change", value: currentUrl, label: `URL changed to ${currentUrl}` });
2201
+ }
2202
+ } catch {
2203
+ }
2204
+ try {
2205
+ const recentNetwork = this.network.filter(
2206
+ (n) => n.timestamp > action.timestamp - 100 && n.timestamp < action.timestamp + 5e3 && n.status >= 200 && n.status < 400
2207
+ );
2208
+ for (const net of recentNetwork.slice(0, 3)) {
2209
+ signals.push({
2210
+ type: "network_success",
2211
+ value: `${net.status}`,
2212
+ label: `${net.method} ${net.path} \u2192 ${net.status}`
2213
+ });
2214
+ }
2215
+ } catch {
2216
+ }
2217
+ try {
2218
+ const recentDialog = [...this.checkpoints].reverse().find(
2219
+ (cp) => cp.type === "dialog" && Math.abs(cp.timestamp - action.timestamp) < 3e3
2220
+ );
2221
+ if (recentDialog) {
2222
+ signals.push({ type: "dialog", value: recentDialog.type, label: recentDialog.hint });
2223
+ }
2224
+ } catch {
2225
+ }
2226
+ return signals;
2227
+ }
2115
2228
  // ─── Periodic disk flush ────────────────────────────────────────
2116
2229
  flushToDisk() {
2117
2230
  const data = this.buildData();
@@ -2182,7 +2295,7 @@ var SessionRecorder = class _SessionRecorder {
2182
2295
  return false;
2183
2296
  };
2184
2297
  const meaningfulNetwork = data.network.filter((n) => !isNoiseNetwork(n));
2185
- const filtered = data.actions.filter((a) => a.type !== "scroll");
2298
+ const filtered = data.actions;
2186
2299
  const groups = [];
2187
2300
  let current = null;
2188
2301
  for (const action of filtered) {
@@ -2209,6 +2322,7 @@ var SessionRecorder = class _SessionRecorder {
2209
2322
  );
2210
2323
  const matchedInputs = inputAction ? this.matchActionToNetwork(inputAction, nearbyNetwork) : [];
2211
2324
  const clickMatches = primary.type === "click" && primary.element?.text ? this.matchActionToNetwork(primary, nearbyNetwork) : [];
2325
+ const signals = primary.__signals || [];
2212
2326
  steps.push({
2213
2327
  step: steps.length + 1,
2214
2328
  ref: getRef(primary),
@@ -2218,7 +2332,8 @@ var SessionRecorder = class _SessionRecorder {
2218
2332
  responseBody: n.responseBody && JSON.stringify(n.responseBody).length > 1e3 ? "[truncated, " + JSON.stringify(n.responseBody).length + " bytes]" : n.responseBody
2219
2333
  })),
2220
2334
  contextChanges: nearbyContext,
2221
- matchedInputs: [...matchedInputs, ...clickMatches]
2335
+ matchedInputs: [...matchedInputs, ...clickMatches],
2336
+ signals
2222
2337
  });
2223
2338
  }
2224
2339
  return {
@@ -300,8 +300,8 @@ async function forwardNetworkExport(sessionName, id, lang) {
300
300
  async function forwardNetworkInspect(sessionName, id) {
301
301
  return rpcCall("network:inspect", { session: sessionName, id }, 1e4);
302
302
  }
303
- async function forwardRecordStart(session, url, cdpEndpoint) {
304
- return rpcCall("record:start", { session, url, cdpEndpoint }, 15e3);
303
+ async function forwardRecordStart(session, url, cdpEndpoint, stream) {
304
+ return rpcCall("record:start", { session, url, cdpEndpoint, stream }, 15e3);
305
305
  }
306
306
  async function forwardRecordStop(session, output) {
307
307
  const params = { session };
package/dist/cli.js CHANGED
@@ -27,10 +27,10 @@ import {
27
27
  startDaemonProcess,
28
28
  stopDaemonProcess,
29
29
  version
30
- } from "./chunk-53LRNYPG.js";
30
+ } from "./chunk-YAUG3NSI.js";
31
31
  import {
32
32
  SessionRecorder
33
- } from "./chunk-YSCY52UJ.js";
33
+ } from "./chunk-CH5ZQV64.js";
34
34
  import {
35
35
  addKnownIssue,
36
36
  getKnowledgePath,
@@ -54,7 +54,7 @@ import {
54
54
  resolveLaunchOpts,
55
55
  saveSessionDiskMeta,
56
56
  setActivePage
57
- } from "./chunk-Z7WC4P2M.js";
57
+ } from "./chunk-74S7UUI2.js";
58
58
  import "./chunk-TNEN6VQ2.js";
59
59
  import {
60
60
  errMsg
@@ -7058,7 +7058,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7058
7058
  }
7059
7059
  let targetPageOverride = null;
7060
7060
  if (_target && extraOpts?.cdpEndpoint) {
7061
- const { findTargetPage } = await import("./browser-4KRHALJ3.js");
7061
+ const { findTargetPage } = await import("./browser-JXIJF4VL.js");
7062
7062
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
7063
7063
  if (!targetPageOverride) {
7064
7064
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -7075,7 +7075,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7075
7075
  params = result.data;
7076
7076
  }
7077
7077
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
7078
- const { forwardExec } = await import("./daemon-client-TOUDMIY5.js");
7078
+ const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
7079
7079
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
7080
7080
  if (result) return result;
7081
7081
  }
@@ -8487,7 +8487,7 @@ var PluginInstaller = class {
8487
8487
  import { outputFormatter, OutputFormatter } from "@dyyz1993/xcli-core";
8488
8488
  var formatter = new OutputFormatter();
8489
8489
  function outputResult(result, mode = "text") {
8490
- if (typeof result === "object" && result !== null) {
8490
+ if (mode !== "json" && mode !== "yaml" && typeof result === "object" && result !== null) {
8491
8491
  const r = result;
8492
8492
  if (r.success === false) {
8493
8493
  outputError(r.message ? String(r.message) : "Unknown error");
@@ -10325,6 +10325,50 @@ async function infoFromMarketplacePlugin(slug, loader) {
10325
10325
  }
10326
10326
  return null;
10327
10327
  }
10328
+ function normalizePluginName(name) {
10329
+ return name.replace(/^@[^/]+\//, "").replace(/^xbrowser-plugin-/, "").toLowerCase();
10330
+ }
10331
+ async function searchLocalPlugins(query) {
10332
+ let installed;
10333
+ try {
10334
+ installed = await new PluginInstaller().list();
10335
+ } catch {
10336
+ return [];
10337
+ }
10338
+ if (!installed || installed.length === 0) return [];
10339
+ const q = normalizePluginName(query);
10340
+ if (!q) return [];
10341
+ const results = [];
10342
+ for (const p of installed) {
10343
+ const plugin = p;
10344
+ const name = String(plugin.name ?? "");
10345
+ const norm = normalizePluginName(name);
10346
+ if (norm === q) {
10347
+ results.push({ plugin, rank: 0 });
10348
+ } else if (norm.startsWith(q) || q.startsWith(norm)) {
10349
+ results.push({ plugin, rank: 1 });
10350
+ } else if (norm.includes(q) || q.includes(norm)) {
10351
+ results.push({ plugin, rank: 2 });
10352
+ }
10353
+ }
10354
+ results.sort((a, b) => a.rank - b.rank);
10355
+ const runtimeInfo = await buildRuntimePluginInfo().catch(() => /* @__PURE__ */ new Map());
10356
+ return results.map(({ plugin }) => {
10357
+ const name = String(plugin.name ?? "");
10358
+ const metadata = plugin.metadata;
10359
+ const staticCommands = metadata?.commands;
10360
+ const rt = runtimeInfo.get(name);
10361
+ const commands = rt?.commands || staticCommands;
10362
+ return {
10363
+ name: "@xbrowser/" + name,
10364
+ slug: name,
10365
+ description: metadata?.description,
10366
+ version: metadata?.version,
10367
+ commands,
10368
+ source: "local"
10369
+ };
10370
+ });
10371
+ }
10328
10372
  async function handleSearch(args, options, mode) {
10329
10373
  const query = args[0] || "";
10330
10374
  applyRegistryOverride(options);
@@ -10332,6 +10376,8 @@ async function handleSearch(args, options, mode) {
10332
10376
  const searchLimit = options.limit ? Number(options.limit) : 20;
10333
10377
  const searchOpts = { query, tag: options.tag, site: options.site, limit: searchLimit };
10334
10378
  const results = [];
10379
+ const localResults = await searchLocalPlugins(query);
10380
+ results.push(...localResults);
10335
10381
  const loader = await getPluginLoader();
10336
10382
  const pluginResults = await searchFromMarketplacePlugin2(searchOpts, loader);
10337
10383
  results.push(...pluginResults);
@@ -10344,15 +10390,24 @@ async function handleSearch(args, options, mode) {
10344
10390
  } catch {
10345
10391
  }
10346
10392
  }
10393
+ const seen = /* @__PURE__ */ new Set();
10394
+ const deduped = [];
10395
+ for (const r of results) {
10396
+ const key = normalizePluginName(String(r.name ?? ""));
10397
+ if (key && !seen.has(key)) {
10398
+ seen.add(key);
10399
+ deduped.push(r);
10400
+ }
10401
+ }
10347
10402
  if (mode === "json") {
10348
- outputEnvelope({ success: true, data: { results, total: results.length } }, { command: "plugin search" }, mode);
10403
+ outputEnvelope({ success: true, data: { results: deduped, total: deduped.length } }, { command: "plugin search" }, mode);
10349
10404
  } else {
10350
- if (results.length === 0) {
10405
+ if (deduped.length === 0) {
10351
10406
  console.log("No plugins found");
10352
10407
  return;
10353
10408
  }
10354
- for (const r of results) {
10355
- const src = r.source === "marketplace" ? "[marketplace]" : "[npm]";
10409
+ for (const r of deduped) {
10410
+ const src = r.source === "marketplace" ? "[marketplace]" : r.source === "local" ? "[local]" : "[npm]";
10356
10411
  const slug = r.slug ? ` (${r.slug})` : "";
10357
10412
  console.log(` ${src} ${r.name}${slug}`);
10358
10413
  if (r.description) console.log(` ${r.description}`);
@@ -10360,7 +10415,7 @@ async function handleSearch(args, options, mode) {
10360
10415
  if (r.downloads) console.log(` Downloads: ${r.downloads}`);
10361
10416
  console.log("");
10362
10417
  }
10363
- console.log(`Total: ${results.length} plugins`);
10418
+ console.log(`Total: ${deduped.length} plugins`);
10364
10419
  }
10365
10420
  }
10366
10421
  async function handlePluginInfo(args, options, mode) {
@@ -10496,7 +10551,7 @@ async function handlePlugin(args, options, mode) {
10496
10551
  } catch {
10497
10552
  }
10498
10553
  try {
10499
- const { daemonPing } = await import("./daemon-client-TOUDMIY5.js");
10554
+ const { daemonPing } = await import("./daemon-client-CMRAWXTN.js");
10500
10555
  if (await daemonPing()) {
10501
10556
  await fetch("http://localhost:9224/rpc", {
10502
10557
  method: "POST",
@@ -10654,7 +10709,8 @@ async function handleRecord(args, options, mode) {
10654
10709
  const url = options.url;
10655
10710
  const sessionName = options.session || "default";
10656
10711
  const cdpEndpoint = options.cdp;
10657
- const result = await forwardRecordStart(sessionName, url, cdpEndpoint);
10712
+ const stream = options.stream || "clean";
10713
+ const result = await forwardRecordStart(sessionName, url, cdpEndpoint, stream);
10658
10714
  if (!result.ok) {
10659
10715
  outputError(String(result.error || "Failed to start recording"));
10660
10716
  return;
@@ -10949,7 +11005,15 @@ async function handleReplay(args, options, mode) {
10949
11005
  const absPath = await import("path").then((p) => p.resolve(filePath));
10950
11006
  const result = await forwardReplay(absPath, sessionName, slowMo);
10951
11007
  if (!result.ok) {
10952
- outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
11008
+ if (mode === "json" || mode === "yaml") {
11009
+ outputResult({
11010
+ ...result,
11011
+ success: false,
11012
+ message: result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"
11013
+ }, mode);
11014
+ } else {
11015
+ outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
11016
+ }
10953
11017
  return;
10954
11018
  }
10955
11019
  outputResult(result, mode);
@@ -11053,7 +11117,7 @@ async function handleFilter(args, _mode, options) {
11053
11117
  }
11054
11118
  }
11055
11119
  async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
11056
- const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-KPU4YQAE.js");
11120
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RFOPX4SF.js");
11057
11121
  const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
11058
11122
  const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync12 } = await import("fs");
11059
11123
  const { join: join13 } = await import("path");
@@ -12990,7 +13054,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
12990
13054
  }
12991
13055
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
12992
13056
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
12993
- const { forwardExec } = await import("./daemon-client-TOUDMIY5.js");
13057
+ const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
12994
13058
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
12995
13059
  const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
12996
13060
  const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
@@ -13030,7 +13094,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13030
13094
  const targetPage = pages[cmdTabIndex];
13031
13095
  await targetPage.bringToFront().catch(() => {
13032
13096
  });
13033
- const { setActivePage: setActivePage2 } = await import("./browser-4KRHALJ3.js");
13097
+ const { setActivePage: setActivePage2 } = await import("./browser-JXIJF4VL.js");
13034
13098
  setActivePage2(session, targetPage);
13035
13099
  }
13036
13100
  }
@@ -13306,7 +13370,7 @@ async function main() {
13306
13370
  const command = process.argv[2];
13307
13371
  const isLongRunning = command === "preview" || command === "serve";
13308
13372
  if (!isLongRunning) {
13309
- const { ensureProcessCanExit } = await import("./browser-4KRHALJ3.js");
13373
+ const { ensureProcessCanExit } = await import("./browser-JXIJF4VL.js");
13310
13374
  await ensureProcessCanExit().catch(() => {
13311
13375
  });
13312
13376
  process.exit(process.exitCode || exitCode);
@@ -179,8 +179,8 @@ async function forwardNetworkExport(sessionName, id, lang) {
179
179
  async function forwardNetworkInspect(sessionName, id) {
180
180
  return rpcCall("network:inspect", { session: sessionName, id }, 1e4);
181
181
  }
182
- async function forwardRecordStart(session, url, cdpEndpoint) {
183
- return rpcCall("record:start", { session, url, cdpEndpoint }, 15e3);
182
+ async function forwardRecordStart(session, url, cdpEndpoint, stream) {
183
+ return rpcCall("record:start", { session, url, cdpEndpoint, stream }, 15e3);
184
184
  }
185
185
  async function forwardRecordStop(session, output) {
186
186
  const params = { session };
@@ -29,7 +29,7 @@ import {
29
29
  forwardSessionList,
30
30
  forwardViewerCheckSelector,
31
31
  isDaemonRunning
32
- } from "./chunk-53LRNYPG.js";
32
+ } from "./chunk-YAUG3NSI.js";
33
33
  import "./chunk-GDKLH7ZY.js";
34
34
  import "./chunk-KFQGP6VL.js";
35
35
  export {
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-VEDJ5XSQ.js";
14
14
  import {
15
15
  SessionRecorder
16
- } from "./chunk-TTZNR3QP.js";
16
+ } from "./chunk-C4O73DQL.js";
17
17
  import {
18
18
  closeEphemeralContext,
19
19
  closeSessionByName,
@@ -29,7 +29,7 @@ import {
29
29
  resolveLaunchOpts,
30
30
  saveSessionDiskMeta,
31
31
  setActivePage
32
- } from "./chunk-OOJ2H7IL.js";
32
+ } from "./chunk-37SHZSUR.js";
33
33
  import "./chunk-VJNMAWPZ.js";
34
34
  import "./chunk-TNEN6VQ2.js";
35
35
  import {
@@ -6575,7 +6575,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6575
6575
  }
6576
6576
  let targetPageOverride = null;
6577
6577
  if (_target && extraOpts?.cdpEndpoint) {
6578
- const { findTargetPage } = await import("./browser-6WJHQDPV.js");
6578
+ const { findTargetPage } = await import("./browser-ALPLJNH2.js");
6579
6579
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
6580
6580
  if (!targetPageOverride) {
6581
6581
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -6592,7 +6592,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6592
6592
  params = result.data;
6593
6593
  }
6594
6594
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
6595
- const { forwardExec } = await import("./daemon-client-PZX2KOLQ.js");
6595
+ const { forwardExec } = await import("./daemon-client-4HJENICO.js");
6596
6596
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
6597
6597
  if (result) return result;
6598
6598
  }
@@ -8013,6 +8013,7 @@ function createRPCHandler() {
8013
8013
  const sessionName = params.session || "default";
8014
8014
  const url = params.url;
8015
8015
  const cdpEndpoint = params.cdpEndpoint;
8016
+ const stream = params.stream || "clean";
8016
8017
  if (activeRecorders.has(sessionName)) {
8017
8018
  return { ok: false, error: "Recording already in progress for session: " + sessionName };
8018
8019
  }
@@ -8044,7 +8045,7 @@ function createRPCHandler() {
8044
8045
  }
8045
8046
  try {
8046
8047
  const recorder = new SessionRecorder(session.context, session.page, sessionName);
8047
- await recorder.start(url);
8048
+ await recorder.start(url, { stream });
8048
8049
  activeRecorders.set(sessionName, recorder);
8049
8050
  return { ok: true, session: sessionName, startUrl: url || session.page.url() };
8050
8051
  } catch (e) {
@@ -8195,7 +8196,8 @@ function createRPCHandler() {
8195
8196
  const isNewFormat = Array.isArray(parsed.actions);
8196
8197
  if (isNewFormat) {
8197
8198
  try {
8198
- const { SessionReplayer } = await import("./session-replayer-RWFOWH3F.js");
8199
+ const replayErrors = [];
8200
+ const { SessionReplayer } = await import("./session-replayer-3HEIJBJ2.js");
8199
8201
  const replayer = new SessionReplayer({
8200
8202
  page: session.page,
8201
8203
  stepDelay: slowMo * 500,
@@ -8203,7 +8205,9 @@ function createRPCHandler() {
8203
8205
  console.log(`[replay] Step ${index + 1}/${total}: ${action.type} ${action.element?.selector || action.url || ""}`);
8204
8206
  },
8205
8207
  onError: (action, error) => {
8206
- console.error(`[replay] Error at step ${action.type}: ${error.message}`);
8208
+ const msg = `[${action?.type || "unknown"}] ${error?.message || String(error)}`;
8209
+ console.error("[replay] Error at step:", msg);
8210
+ replayErrors.push({ eventIndex: -1, error: msg });
8207
8211
  }
8208
8212
  });
8209
8213
  if (!Array.isArray(parsed.actions) || typeof parsed.startUrl !== "string") {
@@ -8219,7 +8223,7 @@ function createRPCHandler() {
8219
8223
  duration,
8220
8224
  eventsPlayed: result2.success,
8221
8225
  totalEvents: result2.success + result2.failed + result2.skipped,
8222
- errors: []
8226
+ errors: replayErrors
8223
8227
  };
8224
8228
  } catch (e) {
8225
8229
  const msg = e instanceof Error ? e.message : String(e);