@mearl/client 2.19.2 → 2.20.0

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.
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);
@@ -449,7 +556,8 @@ export class UnifiedClient {
449
556
  connect() {
450
557
  return new Promise((resolve, reject) => {
451
558
  if (!this.options.serverUrl) {
452
- reject(new Error('Cloud server URL is required. Start cloud-server, pass --server, or set MEARL_SERVER_URL.'));
559
+ reject(new Error('Cloud server URL is required. Run mearl-cloud-server status on the Agent machine. ' +
560
+ 'Resolution order: --server > MEARL_SERVER_URL > ~/.mearl/cloud-server.json.'));
453
561
  return;
454
562
  }
455
563
  const ws = new WebSocket(this.options.serverUrl);
@@ -525,7 +633,7 @@ export class UnifiedClient {
525
633
  else
526
634
  pending.reject(new Error(response.error || 'Unknown error from cloud-server'));
527
635
  }
528
- async requestRemote(action, data, timeoutSec, browser, connector) {
636
+ async requestRemote(action, data, timeoutSec, browser, connector, controlSessionId) {
529
637
  await this.ensureConnected();
530
638
  const ws = this.ws;
531
639
  if (!ws || ws.readyState !== WebSocket.OPEN)
@@ -536,6 +644,8 @@ export class UnifiedClient {
536
644
  message.browser = browser;
537
645
  if (connector)
538
646
  message.connector = connector;
647
+ if (controlSessionId)
648
+ message.controlSessionId = controlSessionId;
539
649
  const serialized = `${JSON.stringify(message)}\n`;
540
650
  const bytes = Buffer.byteLength(serialized, 'utf8');
541
651
  if (bytes > MAX_BUFFER_SIZE) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/client",
3
- "version": "2.19.2",
3
+ "version": "2.20.0",
4
4
  "description": "Unified Mearl SDK & CLI for local and remote browsers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -59,8 +59,8 @@
59
59
  "commander": "^15.0.0",
60
60
  "fastest-levenshtein": "^1.0.16",
61
61
  "ws": "^8.18.0",
62
- "@mearl/cloud-types": "2.19.2",
63
- "@mearl/provider": "2.19.2"
62
+ "@mearl/cloud-types": "2.20.0",
63
+ "@mearl/provider": "2.20.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^24.9.1",