@xbrowser/cli 1.9.5 → 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
@@ -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");
@@ -10496,7 +10496,7 @@ async function handlePlugin(args, options, mode) {
10496
10496
  } catch {
10497
10497
  }
10498
10498
  try {
10499
- const { daemonPing } = await import("./daemon-client-TOUDMIY5.js");
10499
+ const { daemonPing } = await import("./daemon-client-CMRAWXTN.js");
10500
10500
  if (await daemonPing()) {
10501
10501
  await fetch("http://localhost:9224/rpc", {
10502
10502
  method: "POST",
@@ -10654,7 +10654,8 @@ async function handleRecord(args, options, mode) {
10654
10654
  const url = options.url;
10655
10655
  const sessionName = options.session || "default";
10656
10656
  const cdpEndpoint = options.cdp;
10657
- const result = await forwardRecordStart(sessionName, url, cdpEndpoint);
10657
+ const stream = options.stream || "clean";
10658
+ const result = await forwardRecordStart(sessionName, url, cdpEndpoint, stream);
10658
10659
  if (!result.ok) {
10659
10660
  outputError(String(result.error || "Failed to start recording"));
10660
10661
  return;
@@ -10949,7 +10950,15 @@ async function handleReplay(args, options, mode) {
10949
10950
  const absPath = await import("path").then((p) => p.resolve(filePath));
10950
10951
  const result = await forwardReplay(absPath, sessionName, slowMo);
10951
10952
  if (!result.ok) {
10952
- 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
+ }
10953
10962
  return;
10954
10963
  }
10955
10964
  outputResult(result, mode);
@@ -11053,7 +11062,7 @@ async function handleFilter(args, _mode, options) {
11053
11062
  }
11054
11063
  }
11055
11064
  async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
11056
- const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-KPU4YQAE.js");
11065
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-RFOPX4SF.js");
11057
11066
  const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
11058
11067
  const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync12 } = await import("fs");
11059
11068
  const { join: join13 } = await import("path");
@@ -12990,7 +12999,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
12990
12999
  }
12991
13000
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
12992
13001
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
12993
- const { forwardExec } = await import("./daemon-client-TOUDMIY5.js");
13002
+ const { forwardExec } = await import("./daemon-client-CMRAWXTN.js");
12994
13003
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
12995
13004
  const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
12996
13005
  const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
@@ -13030,7 +13039,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13030
13039
  const targetPage = pages[cmdTabIndex];
13031
13040
  await targetPage.bringToFront().catch(() => {
13032
13041
  });
13033
- const { setActivePage: setActivePage2 } = await import("./browser-4KRHALJ3.js");
13042
+ const { setActivePage: setActivePage2 } = await import("./browser-JXIJF4VL.js");
13034
13043
  setActivePage2(session, targetPage);
13035
13044
  }
13036
13045
  }
@@ -13306,7 +13315,7 @@ async function main() {
13306
13315
  const command = process.argv[2];
13307
13316
  const isLongRunning = command === "preview" || command === "serve";
13308
13317
  if (!isLongRunning) {
13309
- const { ensureProcessCanExit } = await import("./browser-4KRHALJ3.js");
13318
+ const { ensureProcessCanExit } = await import("./browser-JXIJF4VL.js");
13310
13319
  await ensureProcessCanExit().catch(() => {
13311
13320
  });
13312
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 {
@@ -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);
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;