@mearl/native-host 2.7.0 → 2.7.2

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.
@@ -42,6 +42,23 @@ export interface AgentBaySessionLike {
42
42
  errorMessage?: string;
43
43
  }>;
44
44
  };
45
+ fileSystem?: {
46
+ getFileTransferContextPath(): Promise<string | null>;
47
+ createDirectory(path: string): Promise<{
48
+ success?: boolean;
49
+ error?: string;
50
+ errorMessage?: string;
51
+ }>;
52
+ uploadFile(localPath: string, remotePath: string, options?: {
53
+ wait?: boolean;
54
+ waitTimeout?: number;
55
+ }): Promise<{
56
+ success?: boolean;
57
+ error?: string;
58
+ errorMessage?: string;
59
+ error_message?: string;
60
+ }>;
61
+ };
45
62
  delete(syncContext?: boolean): Promise<{
46
63
  success: boolean;
47
64
  errorMessage?: string;
@@ -130,6 +147,7 @@ export declare function normalizeDesktopUserAgent(value: unknown): string;
130
147
  export declare function normalizeAgentBayAccessUrl(value: unknown): string | undefined;
131
148
  export declare function hasAgentBayBrowser(selector: string): boolean;
132
149
  export declare function listAgentBayBrowsers(): AgentBayBrowserSummary[];
150
+ export declare function getAgentBaySession(sessionId: string, dependencies?: Pick<AgentBayBrowserManagerDeps, 'createAgentBayClient' | 'resolveApiKey'>): Promise<AgentBaySessionLike>;
133
151
  export declare function getAgentBayAccessUrl(sessionId: string, dependencies?: Pick<AgentBayBrowserManagerDeps, 'createAgentBayClient' | 'resolveApiKey'>): Promise<string>;
134
152
  export declare function resolveAgentBayEndpoint(sessionId: string): Promise<string>;
135
153
  export declare function keepAgentBaySessionAlive(sessionId: string): Promise<void>;
@@ -0,0 +1,12 @@
1
+ import { type AgentBaySessionLike } from './agentbay-browser.js';
2
+ interface AgentBayPageUploadDependencies {
3
+ getSession?: (sessionId: string) => Promise<AgentBaySessionLike>;
4
+ createTransferId?: () => string;
5
+ }
6
+ /**
7
+ * Copies Native Host-local files into the active AgentBay session before CDP selects them.
8
+ * Each file gets an isolated directory so the browser-visible filename stays unchanged.
9
+ * Staged files remain session-scoped because Chromium may read them after the CDP call returns.
10
+ */
11
+ export declare function stageAgentBayPageUploads(sessionId: string, action: string, data: any, dependencies?: AgentBayPageUploadDependencies): Promise<any>;
12
+ export {};
@@ -45,6 +45,7 @@ export interface CdpBuffers {
45
45
  capture: CaptureSequenceState & {
46
46
  droppedRequestsThroughSequence: number;
47
47
  droppedLogsThroughSequence: number;
48
+ droppedEventsThroughSequence: number;
48
49
  };
49
50
  }
50
51
  export declare function createBuffers(tabId?: number): CdpBuffers;
@@ -10,9 +10,8 @@ import type { CdpSession } from './cdp-session.js';
10
10
  import { type ControlSource, type PageActAction, type RunActionsParams } from '@mearl/browser-core';
11
11
  export declare function cdpRunActions(session: CdpSession, data: RunActionsParams, controlSource?: ControlSource): Promise<any>;
12
12
  /**
13
- * Runs a page interaction action with built-in observation and diagnostic
14
- * deltas. `observe: false` disables DOM/navigation observation;
15
- * `diagnostics: false` also disables the atomic console/network result.
13
+ * Runs a page interaction action with built-in observation. Diagnostics are
14
+ * collected only when the caller requests them.
16
15
  * `run_actions` stays lightweight and never enters this wrapper.
17
16
  */
18
17
  export declare function cdpObservedPageAction(session: CdpSession, action: PageActAction, data: any, controlSource?: ControlSource): Promise<any>;
package/dist/index.js CHANGED
@@ -1002,6 +1002,83 @@ function parseTrackingRequest(url2, postData, options = {}) {
1002
1002
  return armsEvent ? [armsEvent] : [];
1003
1003
  }
1004
1004
 
1005
+ // ../browser-core/dist/capture.js
1006
+ var CURSOR_PREFIX = "mearl-capture-v1";
1007
+ function createCaptureGeneration() {
1008
+ return crypto.randomUUID();
1009
+ }
1010
+ function encodeCaptureCursor(cursor) {
1011
+ return `${CURSOR_PREFIX}:${cursor.tabId}:${cursor.generation}:${cursor.scope}:${cursor.sequence}`;
1012
+ }
1013
+ function decodeCaptureCursor(value) {
1014
+ const match = value.match(/^mearl-capture-v1:(\d+):([^:]+):(all|logs|requests|events):(\d+)$/);
1015
+ if (!match)
1016
+ throw new Error("Invalid capture cursor");
1017
+ const tabId = Number(match[1]);
1018
+ const sequence = Number(match[4]);
1019
+ if (!Number.isSafeInteger(tabId) || !Number.isSafeInteger(sequence)) {
1020
+ throw new Error("Invalid capture cursor");
1021
+ }
1022
+ return {
1023
+ tabId,
1024
+ generation: match[2],
1025
+ scope: match[3],
1026
+ sequence
1027
+ };
1028
+ }
1029
+ function captureCheckpoint(state) {
1030
+ return encodeCaptureCursor({
1031
+ tabId: state.tabId,
1032
+ generation: state.generation,
1033
+ sequence: state.currentSequence,
1034
+ scope: "all"
1035
+ });
1036
+ }
1037
+ function nextCaptureSequence(state) {
1038
+ state.currentSequence += 1;
1039
+ return state.currentSequence;
1040
+ }
1041
+ function resolveAfterSequence(after, state, scope) {
1042
+ if (!after)
1043
+ return 0;
1044
+ const cursor = decodeCaptureCursor(after);
1045
+ if (cursor.tabId !== state.tabId) {
1046
+ throw new Error(`Capture cursor belongs to tab ${cursor.tabId}, not tab ${state.tabId}`);
1047
+ }
1048
+ if (cursor.generation !== state.generation) {
1049
+ throw new Error("Capture cursor expired because the tab collector was restarted");
1050
+ }
1051
+ if (cursor.scope !== "all" && cursor.scope !== scope) {
1052
+ throw new Error(`Capture cursor is scoped to ${cursor.scope}, not ${scope}`);
1053
+ }
1054
+ if (cursor.sequence > state.currentSequence) {
1055
+ throw new Error("Capture cursor is ahead of the current collector state");
1056
+ }
1057
+ return cursor.sequence;
1058
+ }
1059
+ function queryCapturedEntries(buffer, state, options, matches, droppedThroughSequence = 0, scope = "all") {
1060
+ const { after, since, now = Date.now() } = options;
1061
+ const limit = Math.max(1, Math.floor(options.limit ?? 100));
1062
+ const afterSequence = resolveAfterSequence(after, state, scope);
1063
+ const sinceTimestampMs = since === void 0 ? void 0 : now - since * 1e3;
1064
+ const matched = buffer.filter((entry) => entry.captureSequence > afterSequence && (sinceTimestampMs === void 0 || entry.timestampMs >= sinceTimestampMs) && matches(entry));
1065
+ const hasMore = matched.length > limit;
1066
+ const entries = after ? matched.slice(0, limit) : matched.slice(-limit);
1067
+ const nextSequence = after && hasMore && entries.length > 0 ? entries[entries.length - 1].captureSequence : state.currentSequence;
1068
+ return {
1069
+ entries,
1070
+ totalMatched: matched.length,
1071
+ nextCursor: encodeCaptureCursor({
1072
+ tabId: state.tabId,
1073
+ generation: state.generation,
1074
+ sequence: nextSequence,
1075
+ scope
1076
+ }),
1077
+ hasMore,
1078
+ gap: after !== void 0 && afterSequence < droppedThroughSequence
1079
+ };
1080
+ }
1081
+
1005
1082
  // ../browser-core/dist/eventFilters.js
1006
1083
  var TRACKING_CAPTURE_ARMED_HINT = "Event capture is active, but this tab has no captured history yet. Reload the current tab or repeat the triggering action, then call get_events again.";
1007
1084
  function compileTextPattern(value) {
@@ -1030,15 +1107,31 @@ function compactEventUrl(value) {
1030
1107
  }
1031
1108
  }
1032
1109
  function formatTrackingEvent(event, includeRaw = false) {
1110
+ const { captureSequence: _discardedCaptureSequence, ...publicEvent } = event;
1033
1111
  if (includeRaw)
1034
- return event;
1035
- const { _raw: _discardedRaw, _context, url: url2, ...summary } = event;
1112
+ return publicEvent;
1113
+ const { _raw: _discardedRaw, _context, url: url2, ...summary } = publicEvent;
1036
1114
  const compactUrl = compactEventUrl(url2 ?? _context.requestUrl);
1037
1115
  return {
1038
1116
  ...summary,
1039
1117
  ...compactUrl ? { url: compactUrl } : {}
1040
1118
  };
1041
1119
  }
1120
+ function matchesTrackingEvent(event, options) {
1121
+ const { source = "all", event_type, filter } = options;
1122
+ if (source !== "all" && event.source !== source)
1123
+ return false;
1124
+ if (event_type && !matchesTextPattern(event.event_type, compileTextPattern(event_type))) {
1125
+ return false;
1126
+ }
1127
+ if (filter) {
1128
+ const pattern = compileTextPattern(filter);
1129
+ if (!matchesTextPattern(event.name, pattern) && !matchesTextPattern(event.url, pattern)) {
1130
+ return false;
1131
+ }
1132
+ }
1133
+ return true;
1134
+ }
1042
1135
  function queryTrackingEvents(buffer, options = {}) {
1043
1136
  const { source = "all", event_type, filter, since, limit = 20, now = Date.now() } = options;
1044
1137
  let events = [...buffer].reverse();
@@ -1063,6 +1156,84 @@ function queryTrackingEvents(buffer, options = {}) {
1063
1156
  totalInBuffer: buffer.length
1064
1157
  };
1065
1158
  }
1159
+ function appendCapturedTrackingBatch(buffer, events, maxSize) {
1160
+ if (events.length === 0)
1161
+ return [];
1162
+ const captureSequence = events[0].captureSequence;
1163
+ const insertionIndex = Number.isSafeInteger(captureSequence) ? buffer.findIndex((event) => Number.isSafeInteger(event.captureSequence) && event.captureSequence > captureSequence) : -1;
1164
+ buffer.splice(insertionIndex < 0 ? buffer.length : insertionIndex, 0, ...events);
1165
+ if (buffer.length <= maxSize)
1166
+ return [];
1167
+ let removeCount = buffer.length - maxSize;
1168
+ const boundarySequence = buffer[removeCount - 1]?.captureSequence;
1169
+ while (boundarySequence !== void 0 && buffer[removeCount]?.captureSequence === boundarySequence) {
1170
+ removeCount += 1;
1171
+ }
1172
+ return buffer.splice(0, removeCount);
1173
+ }
1174
+ function queryCapturedTrackingEvents(buffer, state, options = {}, droppedThroughSequence = 0) {
1175
+ const limit = Math.max(1, Math.floor(options.limit ?? 20));
1176
+ if (!options.after) {
1177
+ const snapshot = queryTrackingEvents(buffer, options);
1178
+ return {
1179
+ ...snapshot,
1180
+ nextCursor: encodeCaptureCursor({
1181
+ tabId: state.tabId,
1182
+ generation: state.generation,
1183
+ sequence: state.currentSequence,
1184
+ scope: "events"
1185
+ }),
1186
+ hasMore: snapshot.totalMatched > limit,
1187
+ gap: false
1188
+ };
1189
+ }
1190
+ const sinceTimestampMs = options.since === void 0 ? void 0 : (options.now ?? Date.now()) - options.since * 1e3;
1191
+ const batchesBySequence = /* @__PURE__ */ new Map();
1192
+ for (const event of buffer) {
1193
+ if (!Number.isSafeInteger(event.captureSequence))
1194
+ continue;
1195
+ if (sinceTimestampMs !== void 0 && event.batchTimestampMs < sinceTimestampMs)
1196
+ continue;
1197
+ if (!matchesTrackingEvent(event, options))
1198
+ continue;
1199
+ const captureSequence = event.captureSequence;
1200
+ const batch = batchesBySequence.get(captureSequence);
1201
+ if (batch)
1202
+ batch.events.push(event);
1203
+ else {
1204
+ batchesBySequence.set(captureSequence, {
1205
+ captureSequence,
1206
+ timestampMs: event.batchTimestampMs,
1207
+ events: [event]
1208
+ });
1209
+ }
1210
+ }
1211
+ const batches = [...batchesBySequence.values()].sort((left, right) => left.captureSequence - right.captureSequence);
1212
+ const captured = queryCapturedEntries(batches, state, { after: options.after, limit: Number.MAX_SAFE_INTEGER }, () => true, droppedThroughSequence, "events");
1213
+ const selected = [];
1214
+ let selectedCount = 0;
1215
+ for (const batch of captured.entries) {
1216
+ if (selected.length > 0 && selectedCount + batch.events.length > limit)
1217
+ break;
1218
+ selected.push(batch);
1219
+ selectedCount += batch.events.length;
1220
+ }
1221
+ const hasMore = selected.length < captured.entries.length;
1222
+ const nextSequence = hasMore ? selected[selected.length - 1].captureSequence : state.currentSequence;
1223
+ return {
1224
+ events: selected.flatMap((batch) => batch.events),
1225
+ totalMatched: captured.entries.reduce((total, batch) => total + batch.events.length, 0),
1226
+ totalInBuffer: buffer.length,
1227
+ nextCursor: encodeCaptureCursor({
1228
+ tabId: state.tabId,
1229
+ generation: state.generation,
1230
+ sequence: nextSequence,
1231
+ scope: "events"
1232
+ }),
1233
+ hasMore,
1234
+ gap: captured.gap
1235
+ };
1236
+ }
1066
1237
 
1067
1238
  // ../browser-core/dist/requestSerializer.js
1068
1239
  function parsePath(path15) {
@@ -2091,8 +2262,8 @@ function resolvePageDragInput(data) {
2091
2262
  if (!data || typeof data !== "object") {
2092
2263
  throw new Error("page_drag: payload is required");
2093
2264
  }
2094
- if (data.dragMode !== void 0 && !["auto", "mouse", "touch"].includes(data.dragMode)) {
2095
- throw new Error('page_drag: "dragMode" must be one of auto, mouse, touch');
2265
+ if (data.dragMode !== void 0 && !["auto", "dom", "mouse", "touch"].includes(data.dragMode)) {
2266
+ throw new Error('page_drag: "dragMode" must be one of auto, dom, mouse, touch');
2096
2267
  }
2097
2268
  const durationMs = data.durationMs ?? PAGE_DRAG_DEFAULT_DURATION_MS;
2098
2269
  if (!Number.isFinite(durationMs) || durationMs < 0 || durationMs > PAGE_DRAG_MAX_DURATION_MS) {
@@ -2105,6 +2276,16 @@ function resolvePageDragInput(data) {
2105
2276
  durationMs: Math.round(durationMs)
2106
2277
  };
2107
2278
  }
2279
+ function resolvePageDragDispatchPolicy(dragMode, visibilityState, touchEmulationEnabled) {
2280
+ const requestedMode = dragMode ?? "auto";
2281
+ const pointerType = requestedMode === "mouse" || requestedMode === "touch" ? requestedMode : touchEmulationEnabled ? "touch" : "mouse";
2282
+ if (requestedMode === "dom")
2283
+ return { dispatchMode: "dom", pointerType };
2284
+ if (requestedMode === "auto" && visibilityState === "hidden") {
2285
+ return { dispatchMode: "dom", pointerType, fallbackReason: "page-hidden" };
2286
+ }
2287
+ return { dispatchMode: "trusted-input", pointerType };
2288
+ }
2108
2289
  function buildPageDragPath(from, to, durationMs) {
2109
2290
  const steps = Math.max(PAGE_DRAG_MIN_STEPS, Math.min(PAGE_DRAG_MAX_STEPS, Math.ceil(durationMs / PAGE_DRAG_STEP_INTERVAL_MS)));
2110
2291
  const points = Array.from({ length: steps }, (_, index) => {
@@ -2116,6 +2297,116 @@ function buildPageDragPath(from, to, durationMs) {
2116
2297
  });
2117
2298
  return { points, intervalMs: durationMs / steps };
2118
2299
  }
2300
+ function dragBetweenPointsInPage(from, points, pointerType) {
2301
+ const target = document.elementFromPoint(from.x, from.y);
2302
+ if (!target) {
2303
+ return {
2304
+ dragged: false,
2305
+ steps: 0,
2306
+ error: `No element found at drag start point (${from.x}, ${from.y})`
2307
+ };
2308
+ }
2309
+ if (points.length === 0) {
2310
+ return { dragged: false, steps: 0, error: "Drag path must contain at least one point" };
2311
+ }
2312
+ const view = target.ownerDocument.defaultView ?? window;
2313
+ const realm = view;
2314
+ const pointerId = 1;
2315
+ const pointerInit = (point, buttons, pressure) => ({
2316
+ bubbles: true,
2317
+ cancelable: true,
2318
+ composed: true,
2319
+ view,
2320
+ clientX: point.x,
2321
+ clientY: point.y,
2322
+ screenX: point.x,
2323
+ screenY: point.y,
2324
+ button: 0,
2325
+ buttons,
2326
+ pointerId,
2327
+ pointerType,
2328
+ isPrimary: true,
2329
+ pressure,
2330
+ width: 1,
2331
+ height: 1
2332
+ });
2333
+ const dispatchPointer = (type, point, buttons, pressure) => {
2334
+ if (typeof realm.PointerEvent !== "function")
2335
+ return false;
2336
+ target.dispatchEvent(new realm.PointerEvent(type, pointerInit(point, buttons, pressure)));
2337
+ return true;
2338
+ };
2339
+ const dispatchMouse = (type, point, buttons) => {
2340
+ if (typeof realm.MouseEvent !== "function")
2341
+ return false;
2342
+ target.dispatchEvent(new realm.MouseEvent(type, {
2343
+ bubbles: true,
2344
+ cancelable: true,
2345
+ composed: true,
2346
+ view,
2347
+ clientX: point.x,
2348
+ clientY: point.y,
2349
+ screenX: point.x,
2350
+ screenY: point.y,
2351
+ button: 0,
2352
+ buttons
2353
+ }));
2354
+ return true;
2355
+ };
2356
+ const createTouch = (point) => {
2357
+ const init = {
2358
+ identifier: pointerId,
2359
+ target,
2360
+ clientX: point.x,
2361
+ clientY: point.y,
2362
+ screenX: point.x,
2363
+ screenY: point.y,
2364
+ pageX: point.x + view.scrollX,
2365
+ pageY: point.y + view.scrollY,
2366
+ radiusX: 1,
2367
+ radiusY: 1,
2368
+ rotationAngle: 0,
2369
+ force: 0.5
2370
+ };
2371
+ return typeof realm.Touch === "function" ? new realm.Touch(init) : init;
2372
+ };
2373
+ const dispatchTouch = (type, point) => {
2374
+ if (typeof realm.TouchEvent !== "function")
2375
+ return false;
2376
+ const touch = createTouch(point);
2377
+ const activeTouches = type === "touchend" ? [] : [touch];
2378
+ target.dispatchEvent(new realm.TouchEvent(type, {
2379
+ bubbles: true,
2380
+ cancelable: true,
2381
+ composed: true,
2382
+ touches: activeTouches,
2383
+ targetTouches: activeTouches,
2384
+ changedTouches: [touch]
2385
+ }));
2386
+ return true;
2387
+ };
2388
+ let dispatched = dispatchPointer("pointerdown", from, 1, 0.5);
2389
+ dispatched = pointerType === "touch" ? dispatchTouch("touchstart", from) || dispatched : dispatchMouse("mousedown", from, 1) || dispatched;
2390
+ for (const point of points) {
2391
+ dispatched = dispatchPointer("pointermove", point, 1, 0.5) || dispatched;
2392
+ dispatched = pointerType === "touch" ? dispatchTouch("touchmove", point) || dispatched : dispatchMouse("mousemove", point, 1) || dispatched;
2393
+ }
2394
+ const end = points[points.length - 1];
2395
+ dispatched = pointerType === "touch" ? dispatchTouch("touchend", end) || dispatched : dispatchMouse("mouseup", end, 0) || dispatched;
2396
+ dispatched = dispatchPointer("pointerup", end, 0, 0) || dispatched;
2397
+ return dispatched ? { dragged: true, steps: points.length } : { dragged: false, steps: 0, error: "This page does not support synthetic drag events" };
2398
+ }
2399
+ var DRAG_BETWEEN_POINTS_IN_PAGE_SOURCE = dragBetweenPointsInPage.toString();
2400
+ function buildPageDragDomExpression(from, points, pointerType) {
2401
+ return `(${DRAG_BETWEEN_POINTS_IN_PAGE_SOURCE})(${JSON.stringify(from)}, ${JSON.stringify(points)}, ${JSON.stringify(pointerType)})`;
2402
+ }
2403
+ function unwrapPageDragDomResult(result) {
2404
+ if (!result)
2405
+ throw new Error("page_drag: DOM simulation returned no result");
2406
+ if (!result.dragged)
2407
+ throw new Error(`page_drag: ${result.error}`);
2408
+ return result;
2409
+ }
2119
2410
  function getDragCoordsFromNode() {
2120
2411
  const el = this.nodeType === Node.TEXT_NODE ? this.parentElement : this;
2121
2412
  if (!el || typeof el.getBoundingClientRect !== "function")
@@ -3458,6 +3749,7 @@ var PAGE_ACT_ACTIONS = [
3458
3749
  "page_eval"
3459
3750
  ];
3460
3751
  var BROWSER_COMMAND_ACTIONS = {
3752
+ tab_checkpoint: true,
3461
3753
  capture_checkpoint: true,
3462
3754
  get_requests: true,
3463
3755
  get_logs: true,
@@ -3502,6 +3794,7 @@ var BROWSER_COMMAND_ACTIONS = {
3502
3794
  run_actions: true
3503
3795
  };
3504
3796
  var EXTENSION_BROWSER_ACTIONS = {
3797
+ tab_checkpoint: true,
3505
3798
  capture_checkpoint: true,
3506
3799
  get_requests: true,
3507
3800
  get_logs: true,
@@ -3554,22 +3847,53 @@ var DEFAULT_PAGE_DIAGNOSTICS = {
3554
3847
  logs: true,
3555
3848
  requests: true
3556
3849
  };
3850
+ var PageActionPreconditionError = class extends Error {
3851
+ pageActionFailureStage = "precondition";
3852
+ constructor(message, options) {
3853
+ super(message, options);
3854
+ this.name = "PageActionPreconditionError";
3855
+ }
3856
+ };
3857
+ function isPageActionPreconditionError(error48) {
3858
+ return typeof error48 === "object" && error48 !== null && error48.pageActionFailureStage === "precondition";
3859
+ }
3860
+ function asPageActionPreconditionError(error48) {
3861
+ if (isPageActionPreconditionError(error48))
3862
+ return error48;
3863
+ return new PageActionPreconditionError(error48 instanceof Error ? error48.message : String(error48), {
3864
+ cause: error48
3865
+ });
3866
+ }
3867
+ function checkPageActionPrecondition(check2) {
3868
+ try {
3869
+ return check2();
3870
+ } catch (error48) {
3871
+ throw asPageActionPreconditionError(error48);
3872
+ }
3873
+ }
3874
+ function describePageActionFailure(error48) {
3875
+ return {
3876
+ error: error48 instanceof Error ? error48.message : String(error48),
3877
+ ...isPageActionPreconditionError(error48) ? { stage: error48.pageActionFailureStage } : {}
3878
+ };
3879
+ }
3557
3880
  function isPageActAction(action) {
3558
3881
  return PAGE_ACT_ACTIONS.includes(action);
3559
3882
  }
3560
3883
  function shouldObservePageAction(action, data) {
3561
3884
  if (!isPageActAction(action))
3562
3885
  return false;
3563
- const diagnosticsEnabled = data?.diagnostics !== void 0 && data.diagnostics !== false;
3886
+ const diagnosticsSetting = data?.diagnostics;
3887
+ const diagnosticsEnabled = diagnosticsSetting === true || diagnosticsSetting !== null && typeof diagnosticsSetting === "object" && !Array.isArray(diagnosticsSetting);
3564
3888
  if (action === "page_eval") {
3565
3889
  return Boolean(diagnosticsEnabled || data?.observe && typeof data.observe === "object" && !Array.isArray(data.observe));
3566
3890
  }
3567
- return data?.observe !== false || data?.diagnostics !== false;
3891
+ return data?.observe !== false || diagnosticsEnabled;
3568
3892
  }
3569
3893
  function splitObservedActionPayload(action, data) {
3570
3894
  const { observe, diagnostics, ...actionData } = data ?? {};
3571
3895
  const setting = diagnostics;
3572
- const resolvedDiagnostics = setting === false ? void 0 : setting === true ? DEFAULT_PAGE_DIAGNOSTICS : setting && typeof setting === "object" && !Array.isArray(setting) ? setting : action === "page_eval" ? void 0 : DEFAULT_PAGE_DIAGNOSTICS;
3896
+ const resolvedDiagnostics = setting === false ? void 0 : setting === true ? DEFAULT_PAGE_DIAGNOSTICS : setting && typeof setting === "object" && !Array.isArray(setting) ? setting : void 0;
3573
3897
  if (observe === false)
3574
3898
  return { actionData, observe: false, diagnostics: resolvedDiagnostics };
3575
3899
  if (action === "page_eval" && (!observe || typeof observe !== "object")) {
@@ -3632,11 +3956,25 @@ async function runPageAct(input, dependencies) {
3632
3956
  sourceTabId
3633
3957
  }).catch(() => null);
3634
3958
  let actionResult;
3635
- let actionError;
3959
+ let actionFailure;
3636
3960
  try {
3637
3961
  actionResult = await dependencies.executeAction(typedAction, actionData);
3638
3962
  } catch (error48) {
3639
- actionError = error48 instanceof Error ? error48.message : String(error48);
3963
+ actionFailure = describePageActionFailure(error48);
3964
+ }
3965
+ if (actionFailure?.stage === "precondition") {
3966
+ await Promise.allSettled([
3967
+ Promise.resolve().then(() => navigationObserver?.dispose()),
3968
+ Promise.resolve().then(() => dependencies.disposeObservation?.())
3969
+ ]);
3970
+ return {
3971
+ action: {
3972
+ name: typedAction,
3973
+ success: false,
3974
+ error: actionFailure.error,
3975
+ stage: actionFailure.stage
3976
+ }
3977
+ };
3640
3978
  }
3641
3979
  let observation;
3642
3980
  try {
@@ -3645,13 +3983,13 @@ async function runPageAct(input, dependencies) {
3645
3983
  observation = unavailableObservation(checkpoint, error48);
3646
3984
  }
3647
3985
  try {
3648
- if (navigationObserver && actionError === void 0 && (navigationObserver.hasStarted() || shouldWaitForPotentialNavigation(typedAction, observation))) {
3986
+ if (navigationObserver && actionFailure === void 0 && (navigationObserver.hasStarted() || shouldWaitForPotentialNavigation(typedAction, observation))) {
3649
3987
  const didNavigate = navigationObserver.hasStarted() || await navigationObserver.waitForStart(navigationOptions.graceMs);
3650
3988
  if (didNavigate) {
3651
3989
  observation = await navigationObserver.waitForResult(navigationOptions.timeoutMs);
3652
3990
  }
3653
3991
  }
3654
- if (navigationObserver && actionError === void 0 && observation.mode !== "navigation" && navigationObserver.hasNetworkActivity()) {
3992
+ if (navigationObserver && actionFailure === void 0 && observation.mode !== "navigation" && navigationObserver.hasNetworkActivity()) {
3655
3993
  const network = await navigationObserver.waitForNetworkIdle(navigationOptions.timeoutMs);
3656
3994
  if (navigationObserver.hasStarted()) {
3657
3995
  observation = await navigationObserver.waitForResult(navigationOptions.timeoutMs);
@@ -3686,9 +4024,9 @@ async function runPageAct(input, dependencies) {
3686
4024
  return {
3687
4025
  action: {
3688
4026
  name: typedAction,
3689
- success: actionError === void 0,
4027
+ success: actionFailure === void 0,
3690
4028
  ...actionResult !== void 0 ? { data: actionResult } : {},
3691
- ...actionError ? { error: actionError } : {}
4029
+ ...actionFailure ? actionFailure : {}
3692
4030
  },
3693
4031
  observation: compactObservationResponse(observation, checkpoint)
3694
4032
  };
@@ -4446,83 +4784,6 @@ ${SOURCE_BADGES[source]}
4446
4784
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
4447
4785
  }
4448
4786
 
4449
- // ../browser-core/dist/capture.js
4450
- var CURSOR_PREFIX = "mearl-capture-v1";
4451
- function createCaptureGeneration() {
4452
- return crypto.randomUUID();
4453
- }
4454
- function encodeCaptureCursor(cursor) {
4455
- return `${CURSOR_PREFIX}:${cursor.tabId}:${cursor.generation}:${cursor.scope}:${cursor.sequence}`;
4456
- }
4457
- function decodeCaptureCursor(value) {
4458
- const match = value.match(/^mearl-capture-v1:(\d+):([^:]+):(all|logs|requests):(\d+)$/);
4459
- if (!match)
4460
- throw new Error("Invalid capture cursor");
4461
- const tabId = Number(match[1]);
4462
- const sequence = Number(match[4]);
4463
- if (!Number.isSafeInteger(tabId) || !Number.isSafeInteger(sequence)) {
4464
- throw new Error("Invalid capture cursor");
4465
- }
4466
- return {
4467
- tabId,
4468
- generation: match[2],
4469
- scope: match[3],
4470
- sequence
4471
- };
4472
- }
4473
- function captureCheckpoint(state) {
4474
- return encodeCaptureCursor({
4475
- tabId: state.tabId,
4476
- generation: state.generation,
4477
- sequence: state.currentSequence,
4478
- scope: "all"
4479
- });
4480
- }
4481
- function nextCaptureSequence(state) {
4482
- state.currentSequence += 1;
4483
- return state.currentSequence;
4484
- }
4485
- function resolveAfterSequence(after, state, scope) {
4486
- if (!after)
4487
- return 0;
4488
- const cursor = decodeCaptureCursor(after);
4489
- if (cursor.tabId !== state.tabId) {
4490
- throw new Error(`Capture cursor belongs to tab ${cursor.tabId}, not tab ${state.tabId}`);
4491
- }
4492
- if (cursor.generation !== state.generation) {
4493
- throw new Error("Capture cursor expired because the tab collector was restarted");
4494
- }
4495
- if (cursor.scope !== "all" && cursor.scope !== scope) {
4496
- throw new Error(`Capture cursor is scoped to ${cursor.scope}, not ${scope}`);
4497
- }
4498
- if (cursor.sequence > state.currentSequence) {
4499
- throw new Error("Capture cursor is ahead of the current collector state");
4500
- }
4501
- return cursor.sequence;
4502
- }
4503
- function queryCapturedEntries(buffer, state, options, matches, droppedThroughSequence = 0, scope = "all") {
4504
- const { after, since, now = Date.now() } = options;
4505
- const limit = Math.max(1, Math.floor(options.limit ?? 100));
4506
- const afterSequence = resolveAfterSequence(after, state, scope);
4507
- const sinceTimestampMs = since === void 0 ? void 0 : now - since * 1e3;
4508
- const matched = buffer.filter((entry) => entry.captureSequence > afterSequence && (sinceTimestampMs === void 0 || entry.timestampMs >= sinceTimestampMs) && matches(entry));
4509
- const hasMore = matched.length > limit;
4510
- const entries = after ? matched.slice(0, limit) : matched.slice(-limit);
4511
- const nextSequence = after && hasMore && entries.length > 0 ? entries[entries.length - 1].captureSequence : state.currentSequence;
4512
- return {
4513
- entries,
4514
- totalMatched: matched.length,
4515
- nextCursor: encodeCaptureCursor({
4516
- tabId: state.tabId,
4517
- generation: state.generation,
4518
- sequence: nextSequence,
4519
- scope
4520
- }),
4521
- hasMore,
4522
- gap: after !== void 0 && afterSequence < droppedThroughSequence
4523
- };
4524
- }
4525
-
4526
4787
  // src/cdp-backend/domain-manager.ts
4527
4788
  var enabledDomains = /* @__PURE__ */ new WeakMap();
4528
4789
  function ensureCdpDomain(client2, domain2, params = {}) {
@@ -4558,7 +4819,8 @@ function createBuffers(tabId = 0) {
4558
4819
  generation: createCaptureGeneration(),
4559
4820
  currentSequence: 0,
4560
4821
  droppedRequestsThroughSequence: 0,
4561
- droppedLogsThroughSequence: 0
4822
+ droppedLogsThroughSequence: 0,
4823
+ droppedEventsThroughSequence: 0
4562
4824
  }
4563
4825
  };
4564
4826
  }
@@ -4621,15 +4883,20 @@ function parsePostData(postData) {
4621
4883
  }
4622
4884
  }
4623
4885
  }
4624
- function appendTrackingEvents(buffers, url2, postData, sequence) {
4886
+ function appendTrackingEvents(buffers, url2, postData, batchSequence, captureSequence) {
4625
4887
  const events = parseTrackingRequest(url2, postData, {
4626
- sequence,
4888
+ sequence: batchSequence,
4627
4889
  now: Date.now()
4628
- });
4890
+ }).map((event) => ({ ...event, captureSequence }));
4629
4891
  if (events.length === 0) return false;
4630
- buffers.events.push(...events);
4631
- if (buffers.events.length > MAX_EVENTS) {
4632
- buffers.events.splice(0, buffers.events.length - MAX_EVENTS);
4892
+ const removed = appendCapturedTrackingBatch(buffers.events, events, MAX_EVENTS);
4893
+ for (const event of removed) {
4894
+ if (event.captureSequence !== void 0) {
4895
+ buffers.capture.droppedEventsThroughSequence = Math.max(
4896
+ buffers.capture.droppedEventsThroughSequence,
4897
+ event.captureSequence
4898
+ );
4899
+ }
4633
4900
  }
4634
4901
  return true;
4635
4902
  }
@@ -4667,10 +4934,19 @@ async function setupBufferListeners(client2, buffers) {
4667
4934
  const url2 = params.request?.url ?? "";
4668
4935
  if (!url2.startsWith("http")) return;
4669
4936
  if (isTrackingUrl(url2)) {
4670
- const sequence = ++buffers.requestCounter;
4671
- const captured = appendTrackingEvents(buffers, url2, params.request?.postData, sequence);
4937
+ const batchSequence = ++buffers.requestCounter;
4938
+ const captureSequence2 = nextCaptureSequence(buffers.capture);
4939
+ const captured = appendTrackingEvents(
4940
+ buffers,
4941
+ url2,
4942
+ params.request?.postData,
4943
+ batchSequence,
4944
+ captureSequence2
4945
+ );
4672
4946
  if (!captured && params.request?.hasPostData && !params.request?.postData) {
4673
- client2.sendCommand("Network.getRequestPostData", { requestId: params.requestId }, 1e4).then((result) => appendTrackingEvents(buffers, url2, result?.postData, sequence)).catch(() => {
4947
+ client2.sendCommand("Network.getRequestPostData", { requestId: params.requestId }, 1e4).then(
4948
+ (result) => appendTrackingEvents(buffers, url2, result?.postData, batchSequence, captureSequence2)
4949
+ ).catch(() => {
4674
4950
  });
4675
4951
  }
4676
4952
  return;
@@ -6470,17 +6746,27 @@ async function collectCdpPageDiagnostics(session, after, diagnostics) {
6470
6746
  };
6471
6747
  result.requests = await cdpGetRequests(session, { ...options, after });
6472
6748
  }
6749
+ if (diagnostics.events) {
6750
+ const options = diagnostics.events === true ? { source: "all", limit: 100 } : { source: "all", limit: 100, ...diagnostics.events };
6751
+ result.events = await cdpGetEvents(session, { ...options, after });
6752
+ }
6473
6753
  return result;
6474
6754
  }
6475
6755
  async function cdpGetEvents(session, data) {
6476
- const { source = "all", event_type, since, limit = 20, filter, includeRaw = false } = data;
6477
- const response = queryTrackingEvents(session.buffers.events, {
6478
- source,
6479
- event_type,
6480
- since,
6481
- limit,
6482
- filter
6483
- });
6756
+ const { source = "all", event_type, since, after, limit = 20, filter, includeRaw = false } = data;
6757
+ const response = queryCapturedTrackingEvents(
6758
+ session.buffers.events,
6759
+ session.buffers.capture,
6760
+ {
6761
+ source,
6762
+ event_type,
6763
+ since,
6764
+ after,
6765
+ limit,
6766
+ filter
6767
+ },
6768
+ session.buffers.capture.droppedEventsThroughSequence
6769
+ );
6484
6770
  const captureStatus = response.totalInBuffer === 0 ? "armed" : "active";
6485
6771
  const hint = response.events.length === 0 && captureStatus === "armed" ? TRACKING_CAPTURE_ARMED_HINT : void 0;
6486
6772
  return {
@@ -6492,12 +6778,17 @@ async function cdpGetEvents(session, data) {
6492
6778
  captureStatus,
6493
6779
  captureSource: "cdp",
6494
6780
  includeRaw,
6781
+ after,
6782
+ nextCursor: response.nextCursor,
6783
+ hasMore: response.hasMore,
6784
+ gap: response.gap,
6495
6785
  hint
6496
6786
  },
6497
6787
  events: response.events.map((event) => formatTrackingEvent(event, includeRaw))
6498
6788
  };
6499
6789
  }
6500
6790
  registerActions([
6791
+ { name: "tab_checkpoint", session: "page", handler: cdpCaptureCheckpoint },
6501
6792
  { name: "capture_checkpoint", session: "page", handler: cdpCaptureCheckpoint },
6502
6793
  { name: "get_requests", session: "page", handler: cdpGetRequests },
6503
6794
  { name: "get_logs", session: "page", handler: cdpGetLogs },
@@ -6588,19 +6879,23 @@ async function cdpObservedPageAction(session, action, data, controlSource = "loc
6588
6879
  if (!diagnostics) return dispatchCdpAction(session, action, actionData, controlSource);
6589
6880
  const cursor = captureCheckpoint(pageSession.buffers.capture);
6590
6881
  let actionResult;
6591
- let actionError;
6882
+ let actionFailure;
6592
6883
  try {
6593
6884
  actionResult = await dispatchCdpAction(session, action, actionData, controlSource);
6594
6885
  } catch (error48) {
6595
- actionError = error48 instanceof Error ? error48.message : String(error48);
6886
+ actionFailure = describePageActionFailure(error48);
6596
6887
  }
6597
- return {
6888
+ const result2 = {
6598
6889
  action: {
6599
6890
  name: action,
6600
- success: actionError === void 0,
6891
+ success: actionFailure === void 0,
6601
6892
  ...actionResult !== void 0 ? { data: actionResult } : {},
6602
- ...actionError ? { error: actionError } : {}
6603
- },
6893
+ ...actionFailure ? actionFailure : {}
6894
+ }
6895
+ };
6896
+ if (actionFailure?.stage === "precondition") return result2;
6897
+ return {
6898
+ ...result2,
6604
6899
  diagnostics: await collectCdpPageDiagnostics(pageSession, cursor, diagnostics)
6605
6900
  };
6606
6901
  }
@@ -6624,7 +6919,7 @@ async function cdpObservedPageAction(session, action, data, controlSource = "loc
6624
6919
  disposeObservation: () => disposePageObservation(pageSession)
6625
6920
  }
6626
6921
  );
6627
- if (!diagnostics || !diagnosticsCursor) return result;
6922
+ if (!diagnostics || !diagnosticsCursor || result.action.stage === "precondition") return result;
6628
6923
  return {
6629
6924
  ...result,
6630
6925
  diagnostics: await collectCdpPageDiagnostics(pageSession, diagnosticsCursor, diagnostics)
@@ -8109,7 +8404,9 @@ var GET_DRAG_COORDS_FROM_NODE_SRC = getDragCoordsFromNode.toString();
8109
8404
  function resolveSnapshotRef(session, ref) {
8110
8405
  const entry = resolvePageRef(session, ref);
8111
8406
  if (!entry) {
8112
- throw new Error(`Ref ${ref} not found. Run page_snapshot first to get fresh refs.`);
8407
+ throw new PageActionPreconditionError(
8408
+ `Ref ${ref} not found. Run page_snapshot first to get fresh refs.`
8409
+ );
8113
8410
  }
8114
8411
  return entry;
8115
8412
  }
@@ -8144,7 +8441,7 @@ async function clickBySelectorJs(session, selector) {
8144
8441
  });
8145
8442
  const value = result.result?.value;
8146
8443
  if (!value) throw new Error("click evaluation returned no value");
8147
- if (value.error) throw new Error(value.error);
8444
+ if (value.error) throw new PageActionPreconditionError(value.error);
8148
8445
  return { ...value, dispatchMode: "dom" };
8149
8446
  }
8150
8447
  async function clickByBackendNodeJs(session, backendNodeId) {
@@ -8155,6 +8452,9 @@ async function clickByBackendNodeJs(session, backendNodeId) {
8155
8452
  returnByValue: true
8156
8453
  });
8157
8454
  const value = result.result?.value;
8455
+ if (value === null) {
8456
+ throw new PageActionPreconditionError("Cannot resolve clickable element");
8457
+ }
8158
8458
  if (!value) throw new Error("click evaluation returned no value");
8159
8459
  return {
8160
8460
  tag: value.tag,
@@ -8175,11 +8475,12 @@ async function clickByPointJs(session, point) {
8175
8475
  returnByValue: true
8176
8476
  });
8177
8477
  const value = result.result?.value;
8178
- if (!value) {
8179
- throw new Error(
8478
+ if (value === null) {
8479
+ throw new PageActionPreconditionError(
8180
8480
  `page_click: no element found at point (${point.x}, ${point.y}) in the current viewport.`
8181
8481
  );
8182
8482
  }
8483
+ if (!value) throw new Error("click evaluation returned no value");
8183
8484
  return {
8184
8485
  tag: value.tag,
8185
8486
  text: value.text,
@@ -8217,7 +8518,7 @@ async function getElementCoordsBySelector(session, selector) {
8217
8518
  });
8218
8519
  const value = result.result?.value;
8219
8520
  if (!value) throw new Error("coord evaluation returned no value");
8220
- if (value.error) throw new Error(value.error);
8521
+ if (value.error) throw new PageActionPreconditionError(value.error);
8221
8522
  return value;
8222
8523
  }
8223
8524
  async function getElementCoordsByBackendNodeId(session, backendNodeId) {
@@ -8228,13 +8529,16 @@ async function getElementCoordsByBackendNodeId(session, backendNodeId) {
8228
8529
  returnByValue: true
8229
8530
  });
8230
8531
  const value = result.result?.value;
8231
- if (!value) {
8232
- throw new Error(`Cannot resolve clickable element for backendNodeId ${backendNodeId}`);
8532
+ if (value === null) {
8533
+ throw new PageActionPreconditionError(
8534
+ `Cannot resolve clickable element for backendNodeId ${backendNodeId}`
8535
+ );
8233
8536
  }
8537
+ if (!value) throw new Error("coord evaluation returned no value");
8234
8538
  return value;
8235
8539
  }
8236
8540
  async function resolveScopeBackendNodeId(session, scope) {
8237
- validatePageElementSelector(scope, "page_click scope");
8541
+ checkPageActionPrecondition(() => validatePageElementSelector(scope, "page_click scope"));
8238
8542
  if (scope.startsWith("@e")) {
8239
8543
  return resolveSnapshotRef(session, scope).backendNodeId;
8240
8544
  }
@@ -8244,17 +8548,17 @@ async function resolveScopeBackendNodeId(session, scope) {
8244
8548
  });
8245
8549
  if (evaluated.exceptionDetails) {
8246
8550
  const message = evaluated.exceptionDetails.exception?.description ?? evaluated.exceptionDetails.text ?? "invalid selector";
8247
- throw new Error(`page_click: invalid scope "${scope}": ${message}`);
8551
+ throw new PageActionPreconditionError(`page_click: invalid scope "${scope}": ${message}`);
8248
8552
  }
8249
8553
  const objectId = evaluated.result?.objectId;
8250
8554
  if (!objectId || evaluated.result?.subtype === "null") {
8251
- throw new Error(`page_click: scope not found: ${scope}`);
8555
+ throw new PageActionPreconditionError(`page_click: scope not found: ${scope}`);
8252
8556
  }
8253
8557
  try {
8254
8558
  const described = await session.client.sendCommand("DOM.describeNode", { objectId });
8255
8559
  const backendNodeId = described.node?.backendNodeId;
8256
8560
  if (backendNodeId === void 0) {
8257
- throw new Error(`page_click: cannot resolve scope: ${scope}`);
8561
+ throw new PageActionPreconditionError(`page_click: cannot resolve scope: ${scope}`);
8258
8562
  }
8259
8563
  return backendNodeId;
8260
8564
  } finally {
@@ -8271,21 +8575,28 @@ async function cdpPageClick(session, data) {
8271
8575
  Boolean
8272
8576
  );
8273
8577
  if (clickMode !== void 0 && !["auto", "dom", "mouse", "touch"].includes(clickMode)) {
8274
- throw new Error('page_click: "clickMode" must be one of auto, dom, mouse, touch');
8578
+ throw new PageActionPreconditionError(
8579
+ 'page_click: "clickMode" must be one of auto, dom, mouse, touch'
8580
+ );
8275
8581
  }
8276
8582
  if (scope !== void 0 && !hasText) {
8277
- throw new Error('page_click: "scope" is only supported with the "text" target');
8583
+ throw new PageActionPreconditionError(
8584
+ 'page_click: "scope" is only supported with the "text" target'
8585
+ );
8278
8586
  }
8279
8587
  if (scope !== void 0) {
8280
- validatePageElementSelector(scope, "page_click scope");
8588
+ checkPageActionPrecondition(() => validatePageElementSelector(scope, "page_click scope"));
8589
+ }
8590
+ if (hasSelector) {
8591
+ checkPageActionPrecondition(() => validatePageElementSelector(selector, "page_click"));
8281
8592
  }
8282
8593
  if (provided.length === 0) {
8283
- throw new Error(
8594
+ throw new PageActionPreconditionError(
8284
8595
  'page_click: provide exactly one of "selector", "text", or "point". For text matching, pass "text" (and optionally "role"/"exact"). For coordinate clicks, pass "point": { x, y } in viewport pixels.'
8285
8596
  );
8286
8597
  }
8287
8598
  if (provided.length > 1) {
8288
- throw new Error(
8599
+ throw new PageActionPreconditionError(
8289
8600
  `page_click: provide exactly one of "selector", "text", or "point" — got ${provided.length} (${provided.join(", ")}).`
8290
8601
  );
8291
8602
  }
@@ -8363,13 +8674,13 @@ async function cdpPageClick(session, data) {
8363
8674
  if (hits.length === 0) {
8364
8675
  const exactSuffix = exact === false ? " (substring match)" : "";
8365
8676
  const roleSuffix = role ? ` with role "${role}"` : "";
8366
- throw new Error(
8677
+ throw new PageActionPreconditionError(
8367
8678
  `page_click: text "${text}"${roleSuffix}${exactSuffix}${scope ? ` in scope "${scope}"` : ""} not found. Run page_snapshot to inspect the page, or try exact: false for substring matching.`
8368
8679
  );
8369
8680
  }
8370
8681
  if (hits.length > 1) {
8371
8682
  const candidates = formatAXTextMatchCandidates(hits);
8372
- throw new Error(
8683
+ throw new PageActionPreconditionError(
8373
8684
  `page_click: text "${text}"${scope ? ` in scope "${scope}"` : ""} matched ${hits.length} elements. Refine with "role" or "exact: true", or run page_snapshot and use an "@eN" ref:
8374
8685
  ${candidates}`
8375
8686
  );
@@ -8399,7 +8710,7 @@ ${candidates}`
8399
8710
  )
8400
8711
  );
8401
8712
  }
8402
- if (!point) throw new Error("page_click point is required");
8713
+ if (!point) throw new PageActionPreconditionError("page_click point is required");
8403
8714
  if (effectiveClickMode === "dom") {
8404
8715
  const dispatchTarget = await clickByPointJs(session, point);
8405
8716
  return finalize2(createPageClickResult({ kind: "point", point }, dispatchTarget));
@@ -8488,19 +8799,53 @@ async function resolveDragEndpoint(session, endpoint) {
8488
8799
  };
8489
8800
  }
8490
8801
  async function cdpPageDrag(session, data) {
8491
- const input = resolvePageDragInput(data);
8802
+ const input = checkPageActionPrecondition(() => resolvePageDragInput(data));
8803
+ const visibilityResult = input.dragMode === "auto" ? await session.client.sendCommand("Runtime.evaluate", {
8804
+ expression: "document.visibilityState",
8805
+ returnByValue: true
8806
+ }) : void 0;
8807
+ const touchEmulationEnabled = input.dragMode === "auto" || input.dragMode === "dom" ? await prefersTouchInput(session) : input.dragMode === "touch";
8808
+ const policy = resolvePageDragDispatchPolicy(
8809
+ input.dragMode,
8810
+ visibilityResult?.result?.value,
8811
+ touchEmulationEnabled
8812
+ );
8492
8813
  await scrollDragSourceIntoView(session, input.from);
8493
8814
  const from = await resolveDragEndpoint(session, input.from);
8494
8815
  const to = await resolveDragEndpoint(session, input.to);
8495
8816
  if (Math.hypot(to.point.x - from.point.x, to.point.y - from.point.y) < 1) {
8496
8817
  throw new Error("page_drag: resolved endpoints must be at least 1 viewport pixel apart");
8497
8818
  }
8498
- const pointerType = input.dragMode === "auto" ? await prefersTouchInput(session) ? "touch" : "mouse" : input.dragMode;
8819
+ if (policy.dispatchMode === "dom") {
8820
+ const path15 = buildPageDragPath(from.point, to.point, input.durationMs);
8821
+ const result = await session.client.sendCommand("Runtime.evaluate", {
8822
+ expression: buildPageDragDomExpression(from.point, path15.points, policy.pointerType),
8823
+ returnByValue: true
8824
+ });
8825
+ if (result.exceptionDetails) {
8826
+ throw new Error(
8827
+ result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "page_drag: DOM simulation failed"
8828
+ );
8829
+ }
8830
+ const { steps: steps2 } = unwrapPageDragDomResult(
8831
+ result.result?.value
8832
+ );
8833
+ return {
8834
+ dragged: true,
8835
+ from,
8836
+ to,
8837
+ dispatchMode: "dom",
8838
+ pointerType: policy.pointerType,
8839
+ durationMs: input.durationMs,
8840
+ steps: steps2,
8841
+ ...policy.fallbackReason ? { fallbackReason: policy.fallbackReason } : {}
8842
+ };
8843
+ }
8499
8844
  const { steps } = await dispatchCdpDrag(
8500
8845
  session,
8501
8846
  from.point,
8502
8847
  to.point,
8503
- pointerType,
8848
+ policy.pointerType,
8504
8849
  input.durationMs
8505
8850
  );
8506
8851
  return {
@@ -8508,7 +8853,7 @@ async function cdpPageDrag(session, data) {
8508
8853
  from,
8509
8854
  to,
8510
8855
  dispatchMode: "trusted-input",
8511
- pointerType,
8856
+ pointerType: policy.pointerType,
8512
8857
  durationMs: input.durationMs,
8513
8858
  steps
8514
8859
  };
@@ -8516,11 +8861,14 @@ async function cdpPageDrag(session, data) {
8516
8861
  async function cdpPageType(session, data) {
8517
8862
  const { selector, text, clearFirst = true } = data;
8518
8863
  if (!selector || typeof selector !== "string") {
8519
- throw new Error(
8864
+ throw new PageActionPreconditionError(
8520
8865
  `"selector" must be a non-empty string. Pass a native CSS selector (e.g. "[aria-label='提交']") or a snapshot ref like "@e3". Note: the field name is "selector", not "ref".`
8521
8866
  );
8522
8867
  }
8523
- if (typeof text !== "string") throw new Error('"text" must be a string.');
8868
+ checkPageActionPrecondition(() => validatePageElementSelector(selector, "page_type"));
8869
+ if (typeof text !== "string") {
8870
+ throw new PageActionPreconditionError('"text" must be a string.');
8871
+ }
8524
8872
  if (selector.startsWith("@e")) {
8525
8873
  return withSnapshotRef(session, selector, async (entry) => {
8526
8874
  const objectId = await resolveBackendNodeObjectId(session, entry.backendNodeId);
@@ -8534,7 +8882,7 @@ async function cdpPageType(session, data) {
8534
8882
  });
8535
8883
  const value2 = result.result?.value;
8536
8884
  if (!value2) throw new Error("cdp_page_type: evaluation returned no value");
8537
- if (value2.error) throw new Error(value2.error);
8885
+ if (value2.error) throw new PageActionPreconditionError(value2.error);
8538
8886
  return { typed: true, tag: value2.tag };
8539
8887
  });
8540
8888
  }
@@ -8552,12 +8900,12 @@ async function cdpPageType(session, data) {
8552
8900
  });
8553
8901
  const value = r.result?.value;
8554
8902
  if (!value) throw new Error("cdp_page_type: evaluation returned no value");
8555
- if (value.error) throw new Error(value.error);
8556
- if (!value.typed) throw new Error("cdp_page_type: failed to type");
8903
+ if (value.error) throw new PageActionPreconditionError(value.error);
8904
+ if (!value.typed) throw new PageActionPreconditionError("cdp_page_type: failed to type");
8557
8905
  return { typed: true, tag: value.tag };
8558
8906
  }
8559
8907
  async function cdpPageScroll(session, data) {
8560
- const input = resolvePageScrollInput(data ?? {});
8908
+ const input = checkPageActionPrecondition(() => resolvePageScrollInput(data ?? {}));
8561
8909
  if (input.selector?.startsWith("@e")) {
8562
8910
  return withSnapshotRef(session, input.selector, async (entry) => {
8563
8911
  const objectId = await resolveBackendNodeObjectId(session, entry.backendNodeId);
@@ -8595,7 +8943,7 @@ async function cdpPageScroll(session, data) {
8595
8943
  async function cdpPageEval(session, data) {
8596
8944
  const { expression, awaitPromise = true } = data;
8597
8945
  if (!expression || typeof expression !== "string") {
8598
- throw new Error("Invalid expression");
8946
+ throw new PageActionPreconditionError("Invalid expression");
8599
8947
  }
8600
8948
  const result = await session.client.sendCommand("Runtime.evaluate", {
8601
8949
  expression,
@@ -8611,7 +8959,7 @@ async function cdpPageEval(session, data) {
8611
8959
  }
8612
8960
  async function cdpPagePress(session, data) {
8613
8961
  const { key } = data;
8614
- if (!key || typeof key !== "string") throw new Error("Invalid key");
8962
+ if (!key || typeof key !== "string") throw new PageActionPreconditionError("Invalid key");
8615
8963
  await session.client.sendCommand("Input.dispatchKeyEvent", {
8616
8964
  type: "keyDown",
8617
8965
  key
@@ -8729,10 +9077,15 @@ async function waitForHistoryIndex(session, targetIndex, timeoutMs) {
8729
9077
  async function cdpPageUpload(session, data) {
8730
9078
  const { selector, filePaths } = data ?? {};
8731
9079
  if (typeof selector !== "string" || selector.length === 0) {
8732
- throw new Error('page_upload: "selector" is required (CSS selector or @eN snapshot ref)');
9080
+ throw new PageActionPreconditionError(
9081
+ 'page_upload: "selector" is required (CSS selector or @eN snapshot ref)'
9082
+ );
8733
9083
  }
9084
+ checkPageActionPrecondition(() => validatePageElementSelector(selector, "page_upload"));
8734
9085
  if (!Array.isArray(filePaths) || filePaths.length === 0 || filePaths.some((filePath) => typeof filePath !== "string" || filePath.length === 0)) {
8735
- throw new Error("Invalid filePaths: must be a non-empty array of file path strings");
9086
+ throw new PageActionPreconditionError(
9087
+ "Invalid filePaths: must be a non-empty array of file path strings"
9088
+ );
8736
9089
  }
8737
9090
  let described;
8738
9091
  if (selector.startsWith("@e")) {
@@ -8749,16 +9102,20 @@ async function cdpPageUpload(session, data) {
8749
9102
  nodeId: documentResult.root?.nodeId,
8750
9103
  selector
8751
9104
  });
8752
- if (!queryResult.nodeId) throw new Error(`Element not found: ${selector}`);
9105
+ if (!queryResult.nodeId) {
9106
+ throw new PageActionPreconditionError(`Element not found: ${selector}`);
9107
+ }
8753
9108
  described = await session.client.sendCommand("DOM.describeNode", {
8754
9109
  nodeId: queryResult.nodeId
8755
9110
  });
8756
9111
  }
8757
9112
  const backendNodeId = described.node?.backendNodeId;
8758
9113
  const tag = String(described.node?.nodeName ?? "").toLowerCase();
8759
- if (backendNodeId === void 0) throw new Error(`Cannot resolve file input: ${selector}`);
9114
+ if (backendNodeId === void 0) {
9115
+ throw new PageActionPreconditionError(`Cannot resolve file input: ${selector}`);
9116
+ }
8760
9117
  if (tag !== "input") {
8761
- throw new Error(
9118
+ throw new PageActionPreconditionError(
8762
9119
  `Element <${tag || "unknown"}> is not a file input. selector must point to an <input type="file"> element.`
8763
9120
  );
8764
9121
  }
@@ -8817,8 +9174,11 @@ async function cdpSetDeviceEmulation(session, data) {
8817
9174
  async function cdpPageHover(session, data) {
8818
9175
  const { selector } = data ?? {};
8819
9176
  if (typeof selector !== "string" || selector.length === 0) {
8820
- throw new Error('page_hover: "selector" is required (CSS selector or @eN snapshot ref)');
9177
+ throw new PageActionPreconditionError(
9178
+ 'page_hover: "selector" is required (CSS selector or @eN snapshot ref)'
9179
+ );
8821
9180
  }
9181
+ checkPageActionPrecondition(() => validatePageElementSelector(selector, "page_hover"));
8822
9182
  const coord = selector.startsWith("@e") ? await withSnapshotRef(
8823
9183
  session,
8824
9184
  selector,
@@ -10085,6 +10445,11 @@ async function requireSessionFromClient(agentBay, sessionId) {
10085
10445
  }
10086
10446
  return result.session;
10087
10447
  }
10448
+ async function getAgentBaySession(sessionId, dependencies = {}) {
10449
+ const apiKey = (dependencies.resolveApiKey ?? resolveAgentBayApiKey)();
10450
+ const agentBay = dependencies.createAgentBayClient ? await dependencies.createAgentBayClient(apiKey) : await createClient(apiKey);
10451
+ return requireSessionFromClient(agentBay, sessionId);
10452
+ }
10088
10453
  async function getAgentBayAccessUrl(sessionId, dependencies = {}) {
10089
10454
  const apiKey = (dependencies.resolveApiKey ?? resolveAgentBayApiKey)();
10090
10455
  const agentBay = dependencies.createAgentBayClient ? await dependencies.createAgentBayClient(apiKey) : await createClient(apiKey);
@@ -10438,6 +10803,83 @@ async function closeAgentBayBrowser(selector, options = {}) {
10438
10803
  }
10439
10804
  }
10440
10805
 
10806
+ // src/agentbay-file-upload.ts
10807
+ import { randomUUID as randomUUID4 } from "node:crypto";
10808
+ import { basename as basename2, posix } from "node:path";
10809
+ var AGENTBAY_FILE_UPLOAD_WAIT_SECONDS = 120;
10810
+ function pageUploadTargets(action, data) {
10811
+ if (action === "page_upload") return [{ data }];
10812
+ if (action !== "run_actions" || !Array.isArray(data?.actions)) return [];
10813
+ return data.actions.flatMap(
10814
+ (step, index) => step?.action === "page_upload" ? [{ data: step.data, step: index }] : []
10815
+ );
10816
+ }
10817
+ function validateFilePaths(target) {
10818
+ const filePaths = target.data?.filePaths;
10819
+ if (!Array.isArray(filePaths) || filePaths.length === 0 || filePaths.some((filePath) => typeof filePath !== "string" || filePath.length === 0)) {
10820
+ const prefix = target.step === void 0 ? "" : `Step ${target.step}: `;
10821
+ throw new Error(`${prefix}Invalid filePaths: must be a non-empty array of file path strings`);
10822
+ }
10823
+ return filePaths;
10824
+ }
10825
+ function uploadError(result) {
10826
+ return result.errorMessage ?? result.error_message ?? result.error ?? "unknown error";
10827
+ }
10828
+ async function stageAgentBayPageUploads(sessionId, action, data, dependencies = {}) {
10829
+ const targets = pageUploadTargets(action, data);
10830
+ if (targets.length === 0) return data;
10831
+ const targetFilePaths = targets.map((target) => validateFilePaths(target));
10832
+ const session = await (dependencies.getSession ?? getAgentBaySession)(sessionId);
10833
+ const fileSystem = session.fileSystem;
10834
+ if (!fileSystem) {
10835
+ throw new Error("The installed AgentBay SDK does not support session file transfer");
10836
+ }
10837
+ const contextPath = await fileSystem.getFileTransferContextPath();
10838
+ if (!contextPath) {
10839
+ throw new Error(`AgentBay file transfer context is unavailable for session ${sessionId}`);
10840
+ }
10841
+ const createTransferId = dependencies.createTransferId ?? randomUUID4;
10842
+ const stagedPaths = await Promise.all(
10843
+ targetFilePaths.map(
10844
+ (filePaths) => Promise.all(
10845
+ filePaths.map(async (localPath) => {
10846
+ const remoteDirectory = posix.join(contextPath, createTransferId());
10847
+ const directoryResult = await fileSystem.createDirectory(remoteDirectory);
10848
+ if (directoryResult?.success === false) {
10849
+ throw new Error(
10850
+ `Failed to prepare AgentBay upload for ${basename2(localPath)}: ${uploadError(directoryResult)}`
10851
+ );
10852
+ }
10853
+ const remotePath = posix.join(remoteDirectory, basename2(localPath));
10854
+ const result = await fileSystem.uploadFile(localPath, remotePath, {
10855
+ wait: true,
10856
+ waitTimeout: AGENTBAY_FILE_UPLOAD_WAIT_SECONDS
10857
+ });
10858
+ if (result?.success !== true) {
10859
+ throw new Error(
10860
+ `Failed to upload ${basename2(localPath)} to AgentBay: ${uploadError(result)}`
10861
+ );
10862
+ }
10863
+ return remotePath;
10864
+ })
10865
+ )
10866
+ )
10867
+ );
10868
+ if (action === "page_upload") {
10869
+ return { ...data, filePaths: stagedPaths[0] };
10870
+ }
10871
+ const runActions = data;
10872
+ let targetIndex = 0;
10873
+ return {
10874
+ ...runActions,
10875
+ actions: runActions.actions.map((step) => {
10876
+ if (step.action !== "page_upload") return step;
10877
+ const filePaths = stagedPaths[targetIndex++];
10878
+ return { ...step, data: { ...step.data, filePaths } };
10879
+ })
10880
+ };
10881
+ }
10882
+
10441
10883
  // src/browser-registry.ts
10442
10884
  var BROWSERS_SUBDIR = "browsers";
10443
10885
  var STALE_RECORD_TTL_MS = 5 * 6e4;
@@ -10631,10 +11073,10 @@ function listBrowserInventory() {
10631
11073
  import fs3 from "node:fs";
10632
11074
  import path3 from "node:path";
10633
11075
  import os2 from "node:os";
10634
- import { randomUUID as randomUUID4 } from "node:crypto";
11076
+ import { randomUUID as randomUUID5 } from "node:crypto";
10635
11077
 
10636
11078
  // src/version.ts
10637
- var NATIVE_HOST_VERSION = true ? "2.7.0" : "unknown";
11079
+ var NATIVE_HOST_VERSION = true ? "2.7.2" : "unknown";
10638
11080
 
10639
11081
  // src/analytics.ts
10640
11082
  var MEASUREMENT_ID = "G-W84LJDZ6PH";
@@ -10659,7 +11101,7 @@ function writeConfig(config2) {
10659
11101
  }
10660
11102
  }
10661
11103
  var clientId = null;
10662
- function createClientId(seed = randomUUID4()) {
11104
+ function createClientId(seed = randomUUID5()) {
10663
11105
  let first = 2166136261;
10664
11106
  let second = 2246822519;
10665
11107
  for (let index = 0; index < seed.length; index++) {
@@ -31907,11 +32349,6 @@ async function handleViaCdp(action, data, _timeoutMs, controlSource) {
31907
32349
  `${action} requires the Mearl browser extension. Install it from https://chromewebstore.google.com/detail/mearl/aoehhjnofngknnjefamjbplchbolghkm`
31908
32350
  );
31909
32351
  }
31910
- if (AGENTBAY_SESSION_ID && action === "page_upload") {
31911
- throw new Error(
31912
- "page_upload is not available for AgentBay CDP sessions because file paths resolve on the remote browser host."
31913
- );
31914
- }
31915
32352
  const available = AGENTBAY_SESSION_ID ? await getCdpSession().getBrowserClient().then(
31916
32353
  () => true,
31917
32354
  () => false
@@ -31926,10 +32363,11 @@ async function handleViaCdp(action, data, _timeoutMs, controlSource) {
31926
32363
  `Chrome DevTools remote debugging is not available on port ${CDP_PORT}. Enable it via chrome://inspect/#remote-debugging (Chrome 145+) or install the Mearl extension.`
31927
32364
  );
31928
32365
  }
31929
- if (shouldObservePageAction(action, data)) {
31930
- return cdpObservedPageAction(getCdpSession(), action, data, controlSource);
32366
+ const actionData = AGENTBAY_SESSION_ID ? await stageAgentBayPageUploads(AGENTBAY_SESSION_ID, action, data) : data;
32367
+ if (shouldObservePageAction(action, actionData)) {
32368
+ return cdpObservedPageAction(getCdpSession(), action, actionData, controlSource);
31931
32369
  }
31932
- return dispatchCdpAction(getCdpSession(), action, data, controlSource);
32370
+ return dispatchCdpAction(getCdpSession(), action, actionData, controlSource);
31933
32371
  }
31934
32372
  function onSocketConnection(socket) {
31935
32373
  let buffer = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/native-host",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "description": "Native Messaging Host for Mearl — bridges Chrome Extension and local socket server",
5
5
  "type": "module",
6
6
  "main": "dist/install.js",
@@ -37,9 +37,9 @@
37
37
  "@types/ws": "^8.18.1",
38
38
  "esbuild": "^0.25.2",
39
39
  "typescript": "^5.8.3",
40
- "@mearl/browser-core": "2.7.0",
41
- "@mearl/daemon-core": "2.7.0",
42
- "@mearl/setup": "2.7.0"
40
+ "@mearl/browser-core": "2.7.2",
41
+ "@mearl/daemon-core": "2.7.2",
42
+ "@mearl/setup": "2.7.2"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "rm -rf dist/ && node build.mjs",