@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/README.md +35 -3
- package/dist/actionTimeouts.js +2 -0
- package/dist/artifacts.d.ts +17 -0
- package/dist/artifacts.js +80 -0
- package/dist/check.js +16 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +47 -3
- package/dist/commands.d.ts +138 -14
- package/dist/commands.js +276 -9
- package/dist/generated-browser-action-protocol.d.ts +122 -9
- package/dist/generated-browser-action-protocol.js +24 -6
- package/dist/index.d.ts +10 -1
- package/dist/index.js +15 -3
- package/dist/plugin.d.ts +5 -2
- package/dist/plugin.js +66 -21
- package/dist/pluginPageBundle.d.ts +1 -0
- package/dist/pluginPageBundle.js +54 -1
- package/dist/pluginRegistry.d.ts +5 -0
- package/dist/pluginRegistry.js +28 -0
- package/dist/pluginSource.d.ts +2 -0
- package/dist/pluginSource.js +8 -0
- package/dist/socket.d.ts +1 -0
- package/dist/socket.js +208 -52
- package/dist/unified.d.ts +17 -0
- package/dist/unified.js +119 -9
- package/package.json +3 -3
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.
|
|
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.
|
|
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.
|
|
63
|
-
"@mearl/provider": "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",
|