@mearl/client 2.19.3 → 2.20.1

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.
@@ -72,6 +72,7 @@ export const TAB_ID_REQUIRED_ACTIONS = [
72
72
  'page_wait',
73
73
  'page_navigate',
74
74
  'page_upload',
75
+ 'page_download',
75
76
  'page_frames',
76
77
  'set_device_emulation',
77
78
  'set_app_profile',
@@ -128,6 +129,8 @@ export const BROWSER_ACTIONS = {
128
129
  browser_list: true,
129
130
  browser_select_browser: true,
130
131
  browser_release: true,
132
+ browser_control: true,
133
+ page_download: true,
131
134
  browser_launch: true,
132
135
  browser_close: true,
133
136
  page_screenshot: true,
@@ -170,6 +173,8 @@ export const BROWSER_COMMAND_ACTIONS = {
170
173
  send_mtop_request: true,
171
174
  browser_list: true,
172
175
  browser_release: true,
176
+ browser_control: true,
177
+ page_download: true,
173
178
  browser_launch: true,
174
179
  browser_close: true,
175
180
  page_screenshot: true,
@@ -237,6 +242,10 @@ export const EXTENSION_BROWSER_ACTIONS = {
237
242
  request_domain_permission: true,
238
243
  run_actions: true,
239
244
  };
245
+ export const BROWSER_CAPABILITY_ACTIONS = {
246
+ ...EXTENSION_BROWSER_ACTIONS,
247
+ page_download: true,
248
+ };
240
249
  export function isBrowserAction(value) {
241
250
  return typeof value === 'string' && Object.hasOwn(BROWSER_ACTIONS, value);
242
251
  }
@@ -244,7 +253,7 @@ export function isBrowserCommandAction(value) {
244
253
  return typeof value === 'string' && Object.hasOwn(BROWSER_COMMAND_ACTIONS, value);
245
254
  }
246
255
  export function isBrowserCapabilityAction(value) {
247
- return isExtensionBrowserAction(value);
256
+ return typeof value === 'string' && Object.hasOwn(BROWSER_CAPABILITY_ACTIONS, value);
248
257
  }
249
258
  export function isExtensionBrowserAction(value) {
250
259
  return typeof value === 'string' && Object.hasOwn(EXTENSION_BROWSER_ACTIONS, value);
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, invokePlugin, listCloudConnectors, } from './unified.js';
8
8
  export type { UnifiedClientOptions, UnifiedInvokeOptions, UnifiedInvokeOptions as InvokeOptions, } from './unified.js';
9
9
  export type { PluginInvocation, PluginInvokeOptions, PluginProgress, StructuredPluginProgress, } from './plugin.js';
10
- export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
10
+ export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_CAPABILITY_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
11
11
  export type { ActionStep, AppProfileName, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCapabilities, BrowserCapabilityAction, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserJsonValue, BrowserLaunchParams, BrowserProviderOperation, BrowserListResult, BrowserPairingInfo, BrowserReleaseFailure, BrowserReleaseResult, BrowserProviderDefinition, BrowserProviderIdentity, BrowserProviderInputSchema, BrowserProviderListResult, BrowserReference, BrowserDetails, BrowserSource, BrowserStatus, BrowserScreenshotKind, BrowserTransport, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, ManagedBrowserInfo, PageClickParams, PageDragEndpoint, PageDragParams, PageDragPoint, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageInspectFilter, PageInspectParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, } from './generated-browser-action-protocol.js';
12
12
  import { type UnifiedInvokeOptions } from './unified.js';
13
13
  import type * as Protocol from './generated-browser-action-protocol.js';
@@ -24,6 +24,7 @@ export declare function sendMtopRequest(params: Protocol.SendMtopRequestParams,
24
24
  export declare function browserList(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_list'>>;
25
25
  export declare function selectBrowser(params?: Protocol.SelectBrowserParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_select_browser'>>;
26
26
  export declare function browserRelease(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
27
+ export declare function browserRelease(params: Protocol.BrowserReleaseParams, options: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
27
28
  export declare function browserLaunch(params: Protocol.BrowserLaunchParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_launch'>>;
28
29
  export declare function browserClose(params: Protocol.BrowserCloseParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_close'>>;
29
30
  export declare function setRule(params: Protocol.SetRuleParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_rule'>>;
@@ -52,3 +53,11 @@ export declare function setTimezone(params: Protocol.SetTimezoneParams, options?
52
53
  export declare function getCookie(params: Protocol.GetCookieParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_cookie'>>;
53
54
  export declare function setCookie(params: Protocol.SetCookieParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_cookie'>>;
54
55
  export declare function runActions(params: Protocol.RunActionsParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'run_actions'>>;
56
+ export declare function browserControl(params: Protocol.BrowserControlParams, options?: UnifiedInvokeOptions): Promise<any>;
57
+ export declare function pageDownload(params: Protocol.PageDownloadParams, options?: UnifiedInvokeOptions): Promise<any>;
58
+ export declare function saveArtifact(artifact: Protocol.BrowserArtifact & {
59
+ browserId?: string;
60
+ }, outputPath: string, options?: UnifiedInvokeOptions): Promise<{
61
+ outputPath: string;
62
+ bytes: number;
63
+ }>;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * then routes actions using the global browser ids returned by browser_list.
6
6
  */
7
7
  export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, invokePlugin, listCloudConnectors, } from './unified.js';
8
- export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
8
+ export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_CAPABILITY_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
9
9
  import { invoke } from './unified.js';
10
10
  export function tabCheckpoint(params, options) {
11
11
  return invoke('tab_checkpoint', params, options);
@@ -41,8 +41,10 @@ export function browserList(options) {
41
41
  export function selectBrowser(params = {}, options) {
42
42
  return invoke('browser_select_browser', params, options);
43
43
  }
44
- export function browserRelease(options) {
45
- return invoke('browser_release', {}, options);
44
+ export function browserRelease(paramsOrOptions = {}, options) {
45
+ return options
46
+ ? invoke('browser_release', paramsOrOptions, options)
47
+ : invoke('browser_release', {}, paramsOrOptions);
46
48
  }
47
49
  export function browserLaunch(params, options) {
48
50
  return invoke('browser_launch', params, options);
@@ -128,3 +130,13 @@ export function setCookie(params, options) {
128
130
  export function runActions(params, options) {
129
131
  return invoke('run_actions', params, options);
130
132
  }
133
+ export function browserControl(params, options) {
134
+ return invoke('browser_control', params, options);
135
+ }
136
+ export function pageDownload(params, options) {
137
+ return invoke('page_download', params, options);
138
+ }
139
+ export async function saveArtifact(artifact, outputPath, options = {}) {
140
+ const { exportArtifact } = await import('./artifacts.js');
141
+ return exportArtifact({ invoke }, artifact, outputPath, options);
142
+ }
@@ -32,6 +32,40 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
32
32
  let resolveReady;
33
33
  const ready = new Promise(resolve => { resolveReady = resolve; });
34
34
 
35
+ let pageModalOpen = null;
36
+ let modalRestoreFrame = null;
37
+ const syncPageModal = () => {
38
+ if (!port) return;
39
+ const open = Boolean(document.querySelector?.('dialog:modal'));
40
+ if (open && modalRestoreFrame !== null) {
41
+ cancelAnimationFrame(modalRestoreFrame);
42
+ modalRestoreFrame = null;
43
+ }
44
+ if (open === pageModalOpen) return;
45
+ if (!open && pageModalOpen === true) {
46
+ if (modalRestoreFrame !== null) return;
47
+ // Closing a dialog restores focus and can scroll the anchors before the next paint.
48
+ modalRestoreFrame = requestAnimationFrame(() => {
49
+ modalRestoreFrame = null;
50
+ if (document.querySelector?.('dialog:modal')) return;
51
+ const positions = [];
52
+ for (const { presentation } of anchoredAgentPanels.values()) {
53
+ if (!presentation.element.isConnected) continue;
54
+ const rect = presentation.element.getBoundingClientRect();
55
+ if (![rect.top, rect.left, rect.width, rect.height].every(Number.isFinite)) continue;
56
+ const bounds = { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
57
+ presentation.bounds = bounds;
58
+ positions.push({ key: presentation.key, bounds });
59
+ }
60
+ pageModalOpen = false;
61
+ port.postMessage({ type: 'page-modal', open: false, positions });
62
+ });
63
+ return;
64
+ }
65
+ pageModalOpen = open;
66
+ port.postMessage({ type: 'page-modal', open });
67
+ };
68
+
35
69
  const requestHost = async (type, payload = {}) => {
36
70
  await ready;
37
71
  const requestId = String(++requestSequence);
@@ -172,36 +206,41 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
172
206
  return context === undefined ? null : context.trim();
173
207
  };
174
208
 
175
- const inlineAgentPanels = new Map();
209
+ const anchoredAgentPanels = new Map();
176
210
 
177
- const stopInlineAgentPanelTracking = key => {
211
+ const stopAgentAnchorTracking = key => {
178
212
  if (key) {
179
- inlineAgentPanels.get(key)?.cleanup();
180
- inlineAgentPanels.delete(key);
213
+ anchoredAgentPanels.get(key)?.cleanup();
214
+ anchoredAgentPanels.delete(key);
181
215
  return;
182
216
  }
183
- for (const tracked of inlineAgentPanels.values()) tracked.cleanup();
184
- inlineAgentPanels.clear();
217
+ for (const tracked of anchoredAgentPanels.values()) tracked.cleanup();
218
+ anchoredAgentPanels.clear();
185
219
  };
186
220
 
187
- const inlineAgentPanelAnchor = (options, key) => {
221
+ const agentPanelAnchor = (options, key, layout) => {
188
222
  const anchor = options.anchor;
189
223
  if (!(anchor instanceof Element)) {
190
- throw new Error('Inline Agent panel anchor must be an element');
224
+ throw new Error('Agent panel anchor must be an element');
191
225
  }
192
226
  const rect = anchor.getBoundingClientRect();
193
227
  if (![rect.top, rect.left, rect.width, rect.height].every(Number.isFinite)) {
194
- throw new Error('Inline Agent panel anchor returned invalid bounds');
228
+ throw new Error('Agent panel anchor returned invalid bounds');
195
229
  }
196
230
  return {
197
231
  element: anchor,
198
232
  key,
233
+ layout,
199
234
  bounds: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
200
235
  };
201
236
  };
202
237
 
203
- const trackInlineAgentPanel = presentation => {
204
- stopInlineAgentPanelTracking(presentation.key);
238
+ const trackAgentAnchor = presentation => {
239
+ if (presentation.layout === 'workspace' || [...anchoredAgentPanels.values()].some(item => item.presentation.layout === 'workspace')) {
240
+ stopAgentAnchorTracking();
241
+ } else {
242
+ stopAgentAnchorTracking(presentation.key);
243
+ }
205
244
  let frame = null;
206
245
  const update = () => {
207
246
  frame = null;
@@ -247,7 +286,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
247
286
  window.removeEventListener('resize', schedule);
248
287
  document.removeEventListener('scroll', schedule, true);
249
288
  };
250
- inlineAgentPanels.set(presentation.key, { presentation, cleanup });
289
+ anchoredAgentPanels.set(presentation.key, { presentation, cleanup });
251
290
  };
252
291
 
253
292
  const scrollInlineAgentPage = (presentation, deltaX, deltaY) => {
@@ -277,8 +316,11 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
277
316
  };
278
317
 
279
318
  const agentPanelPresentation = (options, layout, key) => {
280
- if (layout !== 'inline') return null;
281
- return inlineAgentPanelAnchor(options, key);
319
+ if (layout === 'inline') return agentPanelAnchor(options, key, layout);
320
+ if (layout === 'workspace' && options.anchor !== undefined) {
321
+ return agentPanelAnchor(options, key || 'toolbox-workspace', layout);
322
+ }
323
+ return null;
282
324
  };
283
325
 
284
326
  const setAgentPanelVisible = async (visible, options = {}) => {
@@ -298,16 +340,16 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
298
340
  });
299
341
  if (presentation) {
300
342
  await ready;
301
- trackInlineAgentPanel(presentation);
343
+ trackAgentAnchor(presentation);
302
344
  } else if (!visible && key) {
303
- stopInlineAgentPanelTracking(key);
345
+ stopAgentAnchorTracking(key);
304
346
  } else {
305
- stopInlineAgentPanelTracking();
347
+ stopAgentAnchorTracking();
306
348
  }
307
349
  try {
308
350
  await request;
309
351
  } catch (error) {
310
- if (presentation) stopInlineAgentPanelTracking(presentation.key);
352
+ if (presentation) stopAgentAnchorTracking(presentation.key);
311
353
  throw error;
312
354
  }
313
355
  };
@@ -375,14 +417,14 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
375
417
  });
376
418
  if (presentation) {
377
419
  await ready;
378
- trackInlineAgentPanel(presentation);
420
+ trackAgentAnchor(presentation);
379
421
  } else {
380
- stopInlineAgentPanelTracking();
422
+ stopAgentAnchorTracking();
381
423
  }
382
424
  try {
383
425
  await request;
384
426
  } catch (error) {
385
- if (presentation) stopInlineAgentPanelTracking(presentation.key);
427
+ if (presentation) stopAgentAnchorTracking(presentation.key);
386
428
  throw error;
387
429
  }
388
430
  };
@@ -446,14 +488,14 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
446
488
  if (
447
489
  response?.type === 'agent-panel-wheel' &&
448
490
  typeof response.key === 'string' &&
449
- inlineAgentPanels.has(response.key) &&
491
+ anchoredAgentPanels.has(response.key) &&
450
492
  typeof response.deltaX === 'number' &&
451
493
  Number.isFinite(response.deltaX) &&
452
494
  typeof response.deltaY === 'number' &&
453
495
  Number.isFinite(response.deltaY)
454
496
  ) {
455
497
  scrollInlineAgentPage(
456
- inlineAgentPanels.get(response.key).presentation,
498
+ anchoredAgentPanels.get(response.key).presentation,
457
499
  response.deltaX,
458
500
  response.deltaY,
459
501
  );
@@ -469,6 +511,19 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
469
511
  port.start();
470
512
  resolveReady(context);
471
513
  port.postMessage({ type: 'ready' });
514
+ // Dialogs in the sandbox cannot cover Agent overlays in the parent document.
515
+ syncPageModal();
516
+ const modalObserver = typeof MutationObserver === 'function'
517
+ ? new MutationObserver(syncPageModal)
518
+ : null;
519
+ modalObserver?.observe(document.documentElement, {
520
+ attributes: true,
521
+ attributeFilter: ['open'],
522
+ childList: true,
523
+ subtree: true,
524
+ });
525
+ document.addEventListener('toggle', syncPageModal, true);
526
+ document.addEventListener('close', syncPageModal, true);
472
527
  };
473
528
  window.addEventListener('message', connect);
474
529
  window.parent.postMessage({ type: 'mearl-plugin-ready' }, '*');
package/dist/socket.d.ts CHANGED
@@ -17,6 +17,7 @@ export declare const SOCKET_PATH: string;
17
17
  export type ControlSource = 'local' | 'remote';
18
18
  type CompatibleControlSource = ControlSource | 'cloud';
19
19
  export interface InvokeOptions {
20
+ controlSessionId?: string;
20
21
  /** 超时时间(秒),默认 15 */
21
22
  timeoutSec?: number;
22
23
  /** Force CDP transport instead of extension */
package/dist/socket.js CHANGED
@@ -13,7 +13,7 @@ import os from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { createRequire } from 'node:module';
15
15
  import { StringDecoder } from 'node:string_decoder';
16
- import { listInstalledProviders } from '@mearl/provider/host';
16
+ import { ensureProviderControlHost, listInstalledProviders } from '@mearl/provider/host';
17
17
  import { readLiveProvider } from '@mearl/provider/runtime';
18
18
  import { resolveFileRefs } from './fileRefs.js';
19
19
  import { listLiveBrowsers, findBrowser, describeBrowser, } from './browserRegistry.js';
@@ -32,6 +32,7 @@ export const SOCKET_PATH = process.platform === 'win32'
32
32
  function toControlSourceWireValue(source) {
33
33
  return source === 'remote' || source === 'cloud' ? 'cloud' : 'local';
34
34
  }
35
+ const SESSION_OPTION = Symbol('control-session');
35
36
  /**
36
37
  * Process-wide default browser selector. Lets a long-lived host (e.g. the MCP
37
38
  * server) pin a target once via `select_browser` without threading it through
@@ -129,6 +130,17 @@ function isOrdinaryLocalBrowser(browser) {
129
130
  (browser.transport === 'extension' || browser.transport === 'cdp'));
130
131
  }
131
132
  function supportsAction(browser, action, data) {
133
+ if (browser.transport === 'provider') {
134
+ if (action === 'browser_control' ||
135
+ (action === 'browser_release' && data[SESSION_OPTION]))
136
+ return true;
137
+ if (action.startsWith('__artifact_'))
138
+ return (browser.capabilities?.actions.includes('page_upload') === true ||
139
+ ((action === '__artifact_read' || action === '__artifact_remove') &&
140
+ browser.capabilities?.actions.includes('page_download') === true));
141
+ if (action === 'run_actions' && data[SESSION_OPTION])
142
+ return true;
143
+ }
132
144
  if (action === 'get_versions')
133
145
  return true;
134
146
  if (action === 'get_browser_access_url') {
@@ -328,6 +340,7 @@ export function invoke(action, data = {}, options = {}) {
328
340
  const { timeoutSec = resolveActionTimeoutSec(action, data), transport, browser, controlSource = 'local', } = options;
329
341
  // 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
330
342
  const resolvedData = resolveFileRefs(data);
343
+ Object.defineProperty(resolvedData, SESSION_OPTION, { value: options.controlSessionId });
331
344
  if (action === 'browser_list') {
332
345
  return dispatchInvoke(action, resolvedData, timeoutSec, transport, undefined, controlSource).then(result => decorateBrowserListResult(transport === 'cdp' ? cdpOnlyBrowserList(result) : result));
333
346
  }
@@ -413,14 +426,15 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
413
426
  if (match.status === 'pairing' &&
414
427
  action !== 'get_versions' &&
415
428
  action !== 'browser_close' &&
416
- action !== 'browser_release') {
429
+ action !== 'browser_release' &&
430
+ action !== 'browser_control') {
417
431
  throw new Error(`Browser ${describeBrowser(match)} is waiting for device pairing`);
418
432
  }
419
433
  if (!supportsAction(match, action, data)) {
420
434
  throw new Error(`Browser ${describeBrowser(match)} does not support ${action}`);
421
435
  }
422
436
  try {
423
- return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.providerBrowserId);
437
+ return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.transport === 'provider' ? match.browserId : undefined);
424
438
  }
425
439
  catch (error) {
426
440
  if (isConnError(error) || isSocketSandboxError(error)) {
@@ -439,7 +453,7 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
439
453
  if (routableBrowsers.length > 0) {
440
454
  const target = routableBrowsers[0];
441
455
  try {
442
- return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.providerBrowserId);
456
+ return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined);
443
457
  }
444
458
  catch (error) {
445
459
  if (isSocketSandboxError(error)) {
@@ -486,7 +500,7 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
486
500
  !(transport === 'cdp' && browser.transport === 'provider') &&
487
501
  supportsAction(browser, action, data));
488
502
  if (target) {
489
- return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.providerBrowserId);
503
+ return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined);
490
504
  }
491
505
  return invokeViaLegacy(action, data, timeoutSec, transport, controlSource,
492
506
  /* isRetry */ true);
@@ -497,7 +511,11 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
497
511
  throw new Error(formatConnectError(err, SOCKET_PATH), { cause: error });
498
512
  }
499
513
  }
500
- function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource, providerBrowserId) {
514
+ async function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource, providerTarget) {
515
+ if (providerTarget) {
516
+ const host = await ensureProviderControlHost(Math.min(timeoutSec * 1000, 10_000));
517
+ socketPath = host.socketPath;
518
+ }
501
519
  const id = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
502
520
  return new Promise((resolve, reject) => {
503
521
  if (process.platform !== 'win32' && fs.existsSync(socketPath) && process.getuid) {
@@ -536,10 +554,12 @@ function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSour
536
554
  version: CLIENT_VERSION,
537
555
  controlSource: toControlSourceWireValue(controlSource),
538
556
  };
557
+ if (data[SESSION_OPTION])
558
+ message.controlSessionId = data[SESSION_OPTION];
539
559
  if (transport)
540
560
  message.transport = transport;
541
- if (providerBrowserId)
542
- message.browserId = providerBrowserId;
561
+ if (providerTarget)
562
+ message.providerTarget = providerTarget;
543
563
  socket.write(JSON.stringify(message) + '\n');
544
564
  });
545
565
  socket.on('data', chunk => {
package/dist/unified.d.ts CHANGED
@@ -1,6 +1,11 @@
1
+ import type { BrowserArtifact } from './generated-browser-action-protocol.js';
1
2
  import { type CloudConnectorListResult } from '@mearl/cloud-types';
2
3
  import type { PluginInvocation, PluginInvokeOptions } from './plugin.js';
3
4
  export interface UnifiedInvokeOptions {
5
+ /** Use new on the first tab_open/page_snapshot, then reuse the returned id. */
6
+ session?: string;
7
+ /** Source-local session id used by transport adapters. */
8
+ controlSessionId?: string;
4
9
  timeoutSec?: number;
5
10
  /** Global browser id, source-local browser id, or unique browser name. */
6
11
  browser?: string;
@@ -25,6 +30,11 @@ interface BrowserTarget {
25
30
  }
26
31
  export declare function decodeBrowserTarget(selector: string | undefined): BrowserTarget | undefined;
27
32
  export declare function normalizeBrowserSelector(selector: string): string;
33
+ export declare function decodeControlSessionId(id: string): {
34
+ browser: string;
35
+ id: string;
36
+ transport: 'extension' | 'cdp' | 'provider';
37
+ };
28
38
  export declare class UnifiedClient {
29
39
  private readonly options;
30
40
  private defaultBrowser;
@@ -39,6 +49,13 @@ export declare class UnifiedClient {
39
49
  disconnect(): void;
40
50
  invokePlugin(name: string, invocation: PluginInvocation, options?: PluginInvokeOptions): Promise<unknown>;
41
51
  invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
52
+ saveArtifact(artifact: BrowserArtifact & {
53
+ browserId?: string;
54
+ }, outputPath: string, options?: UnifiedInvokeOptions): Promise<{
55
+ outputPath: string;
56
+ bytes: number;
57
+ }>;
58
+ private invokeRouted;
42
59
  private invokeExplicitLocal;
43
60
  private invokeLaunchWithoutControlBrowser;
44
61
  private selectDefaultBrowser;
package/dist/unified.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { stageFile, exportArtifact } from './artifacts.js';
1
2
  import WebSocket from 'ws';
2
3
  import { DEFAULT_CONNECT_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_REQUEST_TIMEOUT, MAX_BUFFER_SIZE, isHeartbeatMessage, resolveCloudConnectorSelector, } from '@mearl/cloud-types';
3
4
  import { resolveActionTimeoutSec } from './actionTimeouts.js';
@@ -48,6 +49,16 @@ export function normalizeBrowserSelector(selector) {
48
49
  const decodedTarget = decodeBrowserTarget(selector);
49
50
  return (decodedTarget ? encodeBrowserTarget(decodedTarget) : selector.trim()).toLowerCase();
50
51
  }
52
+ export function decodeControlSessionId(id) {
53
+ const match = /^mearl-session:([^:]+):(cs_[a-f0-9-]+):(extension|cdp|provider)$/.exec(id);
54
+ if (!match)
55
+ throw new Error('Invalid session id; reuse session.sessionId from the first response');
56
+ return {
57
+ browser: decodeURIComponent(match[1]),
58
+ id: match[2],
59
+ transport: match[3],
60
+ };
61
+ }
51
62
  function decorateBrowser(browser, source) {
52
63
  const safeBrowser = omitTransientBrowserSecrets(browser);
53
64
  const originalBrowserId = String(browser.browserId);
@@ -127,6 +138,67 @@ export class UnifiedClient {
127
138
  }, options);
128
139
  }
129
140
  async invoke(action, data = {}, options = {}) {
141
+ if (options.session && options.session !== 'new') {
142
+ const session = decodeControlSessionId(options.session);
143
+ const selected = decodeBrowserTarget(options.browser);
144
+ const pinned = decodeBrowserTarget(session.browser);
145
+ if (options.browser &&
146
+ (!selected ||
147
+ selected.browser !== pinned?.browser ||
148
+ selected.connector !== pinned?.connector)) {
149
+ throw new Error('The selected browser does not match the control session');
150
+ }
151
+ if (options.transport && session.transport !== options.transport)
152
+ throw new Error('A control session cannot change transport');
153
+ options = {
154
+ ...options,
155
+ browser: session.browser,
156
+ controlSessionId: session.id,
157
+ ...(session.transport === 'cdp' && !pinned?.connector ? { transport: 'cdp' } : {}),
158
+ };
159
+ }
160
+ else if (options.session === 'new') {
161
+ if (!['tab_open', 'page_snapshot', 'page_screenshot'].includes(action))
162
+ throw new Error('--session new requires tab_open, page_snapshot or page_screenshot');
163
+ options = { ...options, controlSessionId: 'new' };
164
+ }
165
+ if (action === 'page_upload' &&
166
+ data.files !== undefined &&
167
+ (!Array.isArray(data.files) ||
168
+ !data.files.length ||
169
+ data.files.some((file) => !file || (typeof file.filePath === 'string') === (typeof file.artifactId === 'string'))))
170
+ throw new Error('files requires exactly one filePath or artifactId per entry');
171
+ if (action === 'page_upload' &&
172
+ Array.isArray(data.files) &&
173
+ data.files.some((file) => file.filePath)) {
174
+ if (data.filePaths)
175
+ throw new Error('Use files or filePaths, not both');
176
+ const staged = [];
177
+ const temporary = [];
178
+ try {
179
+ for (const file of data.files) {
180
+ if (file.filePath) {
181
+ const uploaded = await stageFile(this, file.filePath, options);
182
+ options = uploaded.options;
183
+ temporary.push(uploaded.artifactId);
184
+ staged.push({ artifactId: uploaded.artifactId });
185
+ }
186
+ else
187
+ staged.push(file);
188
+ }
189
+ return await this.invokeRouted(action, { ...data, files: staged }, options);
190
+ }
191
+ finally {
192
+ for (const id of temporary)
193
+ await this.invoke('__artifact_remove', { artifactId: id }, options).catch(() => { });
194
+ }
195
+ }
196
+ return this.invokeRouted(action, data, options);
197
+ }
198
+ saveArtifact(artifact, outputPath, options = {}) {
199
+ return exportArtifact(this, artifact, outputPath, options);
200
+ }
201
+ async invokeRouted(action, data = {}, options = {}) {
130
202
  validateRequiredActionTabId(action, data);
131
203
  const timeoutSec = options.timeoutSec ?? resolveActionTimeoutSec(action, data, this.options.requestTimeoutSec);
132
204
  if (action === 'connector_list')
@@ -162,7 +234,7 @@ export class UnifiedClient {
162
234
  if (options.connector) {
163
235
  throw new Error('A local browser cannot be used together with --connector.');
164
236
  }
165
- return this.invokeLocal(action, requestData, timeoutSec, target.browser);
237
+ return this.invokeLocal(action, requestData, timeoutSec, target.browser, undefined, options.controlSessionId);
166
238
  }
167
239
  if (connector) {
168
240
  const selected = target?.connector && !options.connector && !this.defaultConnector
@@ -171,10 +243,10 @@ export class UnifiedClient {
171
243
  if (target?.connector && selected.connectorId !== target.connector) {
172
244
  throw new Error('The selected browser belongs to a different cloud connector.');
173
245
  }
174
- return this.invokeRemote(action, requestData, timeoutSec, browser, selected);
246
+ return this.invokeRemote(action, requestData, timeoutSec, browser, selected, options.controlSessionId);
175
247
  }
176
248
  if (!this.options.serverUrl) {
177
- return this.invokeLocal(action, requestData, timeoutSec, options.browser ?? this.defaultBrowser);
249
+ return this.invokeLocal(action, requestData, timeoutSec, options.browser ?? this.defaultBrowser, undefined, options.controlSessionId);
178
250
  }
179
251
  const selector = options.browser ??
180
252
  this.defaultBrowser ??
@@ -204,7 +276,7 @@ export class UnifiedClient {
204
276
  }
205
277
  const browser = optionTarget?.browser ?? defaultTarget?.browser ?? options.browser ?? this.defaultBrowser;
206
278
  const requestData = closeTarget ? { ...data, browser: closeTarget.browser } : data;
207
- return this.invokeLocal(action, requestData, timeoutSec, browser, options.transport);
279
+ return this.invokeLocal(action, requestData, timeoutSec, browser, options.transport, options.controlSessionId);
208
280
  }
209
281
  async invokeLaunchWithoutControlBrowser(data, timeoutSec) {
210
282
  const local = await this.tryLocalBrowserList(timeoutSec);
@@ -410,17 +482,18 @@ export class UnifiedClient {
410
482
  ...(warnings.length > 0 ? { warnings } : {}),
411
483
  });
412
484
  }
413
- async invokeLocal(action, data, timeoutSec, browser, transport) {
485
+ async invokeLocal(action, data, timeoutSec, browser, transport, controlSessionId) {
414
486
  const result = await invokeLocalBrowser(action, data, {
415
487
  timeoutSec,
416
488
  ...(browser ? { browser } : {}),
417
489
  ...(transport ? { transport } : {}),
418
490
  controlSource: 'local',
491
+ ...(controlSessionId ? { controlSessionId } : {}),
419
492
  });
420
493
  return this.decorateActionResult(action, result, { kind: 'local' });
421
494
  }
422
- async invokeRemote(action, data, timeoutSec, browser, connector) {
423
- const result = await this.requestRemote(action, data, timeoutSec, browser, connector.connectorId);
495
+ async invokeRemote(action, data, timeoutSec, browser, connector, controlSessionId) {
496
+ const result = await this.requestRemote(action, data, timeoutSec, browser, connector.connectorId, controlSessionId);
424
497
  return this.decorateActionResult(action, result, {
425
498
  kind: 'remote',
426
499
  connectorId: connector.connectorId,
@@ -428,6 +501,40 @@ export class UnifiedClient {
428
501
  });
429
502
  }
430
503
  decorateActionResult(action, result, source) {
504
+ if (result?.session?.sessionId && !result.session.sessionId.startsWith('mearl-session:')) {
505
+ const browserId = encodeBrowserTarget({
506
+ browser: result.session.browserId,
507
+ ...(source.connectorId ? { connector: source.connectorId } : {}),
508
+ });
509
+ result = {
510
+ ...result,
511
+ session: {
512
+ ...result.session,
513
+ browserId,
514
+ sessionId: `mearl-session:${encodeURIComponent(browserId)}:${result.session.sessionId}:${result.session.transport}`,
515
+ },
516
+ };
517
+ }
518
+ if (typeof result?.browserId === 'string' && action.startsWith('__artifact_')) {
519
+ return {
520
+ ...result,
521
+ browserId: encodeBrowserTarget({
522
+ browser: result.browserId,
523
+ ...(source.connectorId ? { connector: source.connectorId } : {}),
524
+ }),
525
+ };
526
+ }
527
+ if (result?.download?.browserId)
528
+ result = {
529
+ ...result,
530
+ download: {
531
+ ...result.download,
532
+ browserId: encodeBrowserTarget({
533
+ browser: result.download.browserId,
534
+ ...(source.connectorId ? { connector: source.connectorId } : {}),
535
+ }),
536
+ },
537
+ };
431
538
  if (action !== 'browser_launch' || typeof result?.browserId !== 'string')
432
539
  return result;
433
540
  return decorateBrowser(result, source);
@@ -526,7 +633,7 @@ export class UnifiedClient {
526
633
  else
527
634
  pending.reject(new Error(response.error || 'Unknown error from cloud-server'));
528
635
  }
529
- async requestRemote(action, data, timeoutSec, browser, connector) {
636
+ async requestRemote(action, data, timeoutSec, browser, connector, controlSessionId) {
530
637
  await this.ensureConnected();
531
638
  const ws = this.ws;
532
639
  if (!ws || ws.readyState !== WebSocket.OPEN)
@@ -537,6 +644,8 @@ export class UnifiedClient {
537
644
  message.browser = browser;
538
645
  if (connector)
539
646
  message.connector = connector;
647
+ if (controlSessionId)
648
+ message.controlSessionId = controlSessionId;
540
649
  const serialized = `${JSON.stringify(message)}\n`;
541
650
  const bytes = Buffer.byteLength(serialized, 'utf8');
542
651
  if (bytes > MAX_BUFFER_SIZE) {