@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.
@@ -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
@@ -6333,6 +6333,22 @@ async function checkPluginLoginRequired(options) {
6333
6333
  const requiresLogin = command.requiresLogin === true || site.config?.requiresLogin === true || loginConfig?.requiresLogin === true || command.loginRequired === "required";
6334
6334
  if (!requiresLogin) return { ok: true };
6335
6335
  const pluginName = site.name || "plugin";
6336
+ const siteConfig = site.config;
6337
+ const configIsLogin = siteConfig?.isLogin;
6338
+ if (configIsLogin) {
6339
+ try {
6340
+ const loggedIn = await configIsLogin(ctx);
6341
+ if (loggedIn) return { ok: true };
6342
+ return buildLoginRequired({
6343
+ plugin: pluginName,
6344
+ command: commandName,
6345
+ reason: "config.isLogin returned false",
6346
+ sessionName,
6347
+ loginConfig
6348
+ });
6349
+ } catch {
6350
+ }
6351
+ }
6336
6352
  if (typeof site.isLoggedIn === "function") {
6337
6353
  try {
6338
6354
  const loggedIn = await site.isLoggedIn(ctx);
@@ -7042,7 +7058,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7042
7058
  }
7043
7059
  let targetPageOverride = null;
7044
7060
  if (_target && extraOpts?.cdpEndpoint) {
7045
- const { findTargetPage } = await import("./browser-4KRHALJ3.js");
7061
+ const { findTargetPage } = await import("./browser-JXIJF4VL.js");
7046
7062
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
7047
7063
  if (!targetPageOverride) {
7048
7064
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -7059,7 +7075,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7059
7075
  params = result.data;
7060
7076
  }
7061
7077
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
7062
- const { forwardExec } = await import("./daemon-client-TOUDMIY5.js");
7078
+ const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
7063
7079
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
7064
7080
  if (result) return result;
7065
7081
  }
@@ -8471,7 +8487,7 @@ var PluginInstaller = class {
8471
8487
  import { outputFormatter, OutputFormatter } from "@dyyz1993/xcli-core";
8472
8488
  var formatter = new OutputFormatter();
8473
8489
  function outputResult(result, mode = "text") {
8474
- if (typeof result === "object" && result !== null) {
8490
+ if (mode !== "json" && mode !== "yaml" && typeof result === "object" && result !== null) {
8475
8491
  const r = result;
8476
8492
  if (r.success === false) {
8477
8493
  outputError(r.message ? String(r.message) : "Unknown error");
@@ -10480,7 +10496,7 @@ async function handlePlugin(args, options, mode) {
10480
10496
  } catch {
10481
10497
  }
10482
10498
  try {
10483
- const { daemonPing } = await import("./daemon-client-TOUDMIY5.js");
10499
+ const { daemonPing } = await import("./daemon-client-CMRAWXTN.js");
10484
10500
  if (await daemonPing()) {
10485
10501
  await fetch("http://localhost:9224/rpc", {
10486
10502
  method: "POST",
@@ -10638,7 +10654,8 @@ async function handleRecord(args, options, mode) {
10638
10654
  const url = options.url;
10639
10655
  const sessionName = options.session || "default";
10640
10656
  const cdpEndpoint = options.cdp;
10641
- const result = await forwardRecordStart(sessionName, url, cdpEndpoint);
10657
+ const stream = options.stream || "clean";
10658
+ const result = await forwardRecordStart(sessionName, url, cdpEndpoint, stream);
10642
10659
  if (!result.ok) {
10643
10660
  outputError(String(result.error || "Failed to start recording"));
10644
10661
  return;
@@ -10933,7 +10950,15 @@ async function handleReplay(args, options, mode) {
10933
10950
  const absPath = await import("path").then((p) => p.resolve(filePath));
10934
10951
  const result = await forwardReplay(absPath, sessionName, slowMo);
10935
10952
  if (!result.ok) {
10936
- outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
10953
+ if (mode === "json" || mode === "yaml") {
10954
+ outputResult({
10955
+ ...result,
10956
+ success: false,
10957
+ message: result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"
10958
+ }, mode);
10959
+ } else {
10960
+ outputError(String(result.errors ? result.errors.map((e) => e.error).join("; ") : result.error || "Replay failed"));
10961
+ }
10937
10962
  return;
10938
10963
  }
10939
10964
  outputResult(result, mode);
@@ -11037,7 +11062,7 @@ async function handleFilter(args, _mode, options) {
11037
11062
  }
11038
11063
  }
11039
11064
  async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
11040
- const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-KPU4YQAE.js");
11065
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RFOPX4SF.js");
11041
11066
  const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
11042
11067
  const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync12 } = await import("fs");
11043
11068
  const { join: join13 } = await import("path");
@@ -12974,7 +12999,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
12974
12999
  }
12975
13000
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
12976
13001
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
12977
- const { forwardExec } = await import("./daemon-client-TOUDMIY5.js");
13002
+ const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
12978
13003
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
12979
13004
  const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
12980
13005
  const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
@@ -13014,7 +13039,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13014
13039
  const targetPage = pages[cmdTabIndex];
13015
13040
  await targetPage.bringToFront().catch(() => {
13016
13041
  });
13017
- const { setActivePage: setActivePage2 } = await import("./browser-4KRHALJ3.js");
13042
+ const { setActivePage: setActivePage2 } = await import("./browser-JXIJF4VL.js");
13018
13043
  setActivePage2(session, targetPage);
13019
13044
  }
13020
13045
  }
@@ -13290,7 +13315,7 @@ async function main() {
13290
13315
  const command = process.argv[2];
13291
13316
  const isLongRunning = command === "preview" || command === "serve";
13292
13317
  if (!isLongRunning) {
13293
- const { ensureProcessCanExit } = await import("./browser-4KRHALJ3.js");
13318
+ const { ensureProcessCanExit } = await import("./browser-JXIJF4VL.js");
13294
13319
  await ensureProcessCanExit().catch(() => {
13295
13320
  });
13296
13321
  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 {
@@ -5850,6 +5850,22 @@ async function checkPluginLoginRequired(options) {
5850
5850
  const requiresLogin = command.requiresLogin === true || site.config?.requiresLogin === true || loginConfig?.requiresLogin === true || command.loginRequired === "required";
5851
5851
  if (!requiresLogin) return { ok: true };
5852
5852
  const pluginName = site.name || "plugin";
5853
+ const siteConfig = site.config;
5854
+ const configIsLogin = siteConfig?.isLogin;
5855
+ if (configIsLogin) {
5856
+ try {
5857
+ const loggedIn = await configIsLogin(ctx);
5858
+ if (loggedIn) return { ok: true };
5859
+ return buildLoginRequired({
5860
+ plugin: pluginName,
5861
+ command: commandName,
5862
+ reason: "config.isLogin returned false",
5863
+ sessionName,
5864
+ loginConfig
5865
+ });
5866
+ } catch {
5867
+ }
5868
+ }
5853
5869
  if (typeof site.isLoggedIn === "function") {
5854
5870
  try {
5855
5871
  const loggedIn = await site.isLoggedIn(ctx);
@@ -6559,7 +6575,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6559
6575
  }
6560
6576
  let targetPageOverride = null;
6561
6577
  if (_target && extraOpts?.cdpEndpoint) {
6562
- const { findTargetPage } = await import("./browser-6WJHQDPV.js");
6578
+ const { findTargetPage } = await import("./browser-ALPLJNH2.js");
6563
6579
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
6564
6580
  if (!targetPageOverride) {
6565
6581
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -6576,7 +6592,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6576
6592
  params = result.data;
6577
6593
  }
6578
6594
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
6579
- const { forwardExec } = await import("./daemon-client-PZX2KOLQ.js");
6595
+ const { forwardExec } = await import("./daemon-client-4HJENICO.js");
6580
6596
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
6581
6597
  if (result) return result;
6582
6598
  }
@@ -7997,6 +8013,7 @@ function createRPCHandler() {
7997
8013
  const sessionName = params.session || "default";
7998
8014
  const url = params.url;
7999
8015
  const cdpEndpoint = params.cdpEndpoint;
8016
+ const stream = params.stream || "clean";
8000
8017
  if (activeRecorders.has(sessionName)) {
8001
8018
  return { ok: false, error: "Recording already in progress for session: " + sessionName };
8002
8019
  }
@@ -8028,7 +8045,7 @@ function createRPCHandler() {
8028
8045
  }
8029
8046
  try {
8030
8047
  const recorder = new SessionRecorder(session.context, session.page, sessionName);
8031
- await recorder.start(url);
8048
+ await recorder.start(url, { stream });
8032
8049
  activeRecorders.set(sessionName, recorder);
8033
8050
  return { ok: true, session: sessionName, startUrl: url || session.page.url() };
8034
8051
  } catch (e) {
@@ -8179,7 +8196,8 @@ function createRPCHandler() {
8179
8196
  const isNewFormat = Array.isArray(parsed.actions);
8180
8197
  if (isNewFormat) {
8181
8198
  try {
8182
- const { SessionReplayer } = await import("./session-replayer-RWFOWH3F.js");
8199
+ const replayErrors = [];
8200
+ const { SessionReplayer } = await import("./session-replayer-3HEIJBJ2.js");
8183
8201
  const replayer = new SessionReplayer({
8184
8202
  page: session.page,
8185
8203
  stepDelay: slowMo * 500,
@@ -8187,7 +8205,9 @@ function createRPCHandler() {
8187
8205
  console.log(`[replay] Step ${index + 1}/${total}: ${action.type} ${action.element?.selector || action.url || ""}`);
8188
8206
  },
8189
8207
  onError: (action, error) => {
8190
- 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 });
8191
8211
  }
8192
8212
  });
8193
8213
  if (!Array.isArray(parsed.actions) || typeof parsed.startUrl !== "string") {
@@ -8203,7 +8223,7 @@ function createRPCHandler() {
8203
8223
  duration,
8204
8224
  eventsPlayed: result2.success,
8205
8225
  totalEvents: result2.success + result2.failed + result2.skipped,
8206
- errors: []
8226
+ errors: replayErrors
8207
8227
  };
8208
8228
  } catch (e) {
8209
8229
  const msg = e instanceof Error ? e.message : String(e);