Sign In

@mearl/client

Package Overview
Dependencies
Maintainers
2
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mearl/client - npm Package Compare versions

Comparing version
2.5.1
to
2.6.0
+7
dist/local.d.ts
/**
* Machine-local transport for infrastructure components such as cloud-connector.
* User-facing integrations should import from `@mearl/client` so browser routing
* remains consistent across local and remote sources.
*/
export { CLIENT_VERSION, SOCKET_PATH, decorateBrowserListResult, getDefaultBrowser, invoke, setDefaultBrowser, } from './socket.js';
export type { InvokeOptions } from './socket.js';
/**
* Machine-local transport for infrastructure components such as cloud-connector.
* User-facing integrations should import from `@mearl/client` so browser routing
* remains consistent across local and remote sources.
*/
export { CLIENT_VERSION, SOCKET_PATH, decorateBrowserListResult, getDefaultBrowser, invoke, setDefaultBrowser, } from './socket.js';
import { type CloudConnectorListResult } from '@mearl/cloud-types';
export interface UnifiedInvokeOptions {
timeoutSec?: number;
/** Global browser id, source-local browser id, or unique browser name. */
browser?: string;
/** Restrict the action to one remote connector. */
connector?: string;
/** Restrict the action to browsers on this machine. */
local?: boolean;
/** Force CDP on this machine. Implies local routing. */
transport?: 'cdp';
}
export interface UnifiedClientOptions {
serverUrl?: string;
connectTimeoutSec?: number;
requestTimeoutSec?: number;
heartbeatInterval?: number;
defaultBrowser?: string;
defaultConnector?: string;
}
interface BrowserTarget {
browser: string;
connector?: string;
}
export declare function decodeBrowserTarget(selector: string | undefined): BrowserTarget | undefined;
export declare class UnifiedClient {
private readonly options;
private defaultBrowser;
private defaultConnector;
private ws;
private connectPromise;
private messageBuffer;
private pendingRequests;
private heartbeatTimer;
private closing;
constructor(options?: UnifiedClientOptions);
disconnect(): void;
invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
private invokeExplicitLocal;
private invokeLaunchWithoutControlBrowser;
private selectDefaultBrowser;
private resolveUnifiedBrowser;
listConnectors(timeoutSec?: number): Promise<CloudConnectorListResult>;
private resolveConnector;
private listLocalBrowsers;
private tryLocalBrowserList;
private decorateListing;
private applyConfiguredDefault;
private listConnectorBrowsers;
private listAllBrowsers;
private invokeLocal;
private invokeRemote;
private decorateActionResult;
private ensureConnected;
private connect;
private startHeartbeat;
private handleMessage;
private handleResponse;
private requestRemote;
private rejectAllPending;
}
export declare function invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
export declare function listCloudConnectors(): Promise<CloudConnectorListResult>;
export {};
import WebSocket from 'ws';
import { DEFAULT_CONNECT_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_REQUEST_TIMEOUT, MAX_BUFFER_SIZE, isHeartbeatMessage, resolveCloudConnectorSelector, } from '@mearl/cloud-types';
import { resolveActionTimeoutSec } from './actionTimeouts.js';
import { invoke as invokeLocalBrowser } from './socket.js';
import { loadCloudServerConfig, loadLocalConnectorId } from './unifiedConfig.js';
const RESPONSE_GRACE_MS = 1_000;
const GLOBAL_BROWSER_PREFIX = 'mearl:';
function encodeBrowserTarget(target) {
const browser = encodeURIComponent(target.browser);
return target.connector
? `${GLOBAL_BROWSER_PREFIX}remote:${encodeURIComponent(target.connector)}:${browser}`
: `${GLOBAL_BROWSER_PREFIX}local:${browser}`;
}
function omitTransientBrowserSecrets(browser) {
const safeBrowser = { ...browser };
delete safeBrowser.agentbayAccessUrl;
delete safeBrowser.accessUrl;
return safeBrowser;
}
export function decodeBrowserTarget(selector) {
if (!selector?.startsWith(GLOBAL_BROWSER_PREFIX))
return undefined;
try {
const parts = selector.slice(GLOBAL_BROWSER_PREFIX.length).split(':');
if (parts[0] === 'local' && parts.length === 2) {
const browser = decodeURIComponent(parts[1]);
return browser ? { browser } : undefined;
}
if (parts[0] === 'remote' && parts.length === 3) {
const connector = decodeURIComponent(parts[1]);
const browser = decodeURIComponent(parts[2]);
return connector && browser ? { connector, browser } : undefined;
}
}
catch {
return undefined;
}
return undefined;
}
function decorateBrowser(browser, source) {
const safeBrowser = omitTransientBrowserSecrets(browser);
const originalBrowserId = String(browser.browserId);
const originalName = String(browser.name);
return {
...safeBrowser,
browserId: encodeBrowserTarget({
browser: originalBrowserId,
...(source.connectorId ? { connector: source.connectorId } : {}),
}),
name: source.kind === 'local'
? `[this machine] ${originalName}`
: `[${source.connectorName}] ${originalName}`,
originalBrowserId,
originalName,
status: typeof browser.status === 'string' ? browser.status : 'disconnected',
source,
};
}
function browserMatches(browser, selector) {
const normalized = selector.trim().toLowerCase();
return (browser.browserId.toLowerCase() === normalized ||
browser.originalBrowserId?.toLowerCase() === normalized ||
browser.name.toLowerCase() === normalized ||
browser.originalName?.toLowerCase() === normalized);
}
function findBrowsers(browsers, selector, connectedOnly = true) {
const candidates = connectedOnly
? browsers.filter(browser => browser.status === 'connected')
: browsers;
const exact = candidates.filter(browser => browserMatches(browser, selector));
if (exact.length > 0)
return exact;
const normalized = selector.trim().toLowerCase();
return candidates.filter(browser => browser.browserId.toLowerCase().includes(normalized) ||
browser.originalBrowserId?.toLowerCase().includes(normalized) ||
browser.name.toLowerCase().includes(normalized) ||
browser.originalName?.toLowerCase().includes(normalized));
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
export class UnifiedClient {
options;
defaultBrowser;
defaultConnector;
ws = null;
connectPromise = null;
messageBuffer = '';
pendingRequests = new Map();
heartbeatTimer = null;
closing = false;
constructor(options = {}) {
this.options = {
...(options.serverUrl ? { serverUrl: options.serverUrl } : {}),
connectTimeoutSec: options.connectTimeoutSec ?? DEFAULT_CONNECT_TIMEOUT,
requestTimeoutSec: options.requestTimeoutSec ?? DEFAULT_REQUEST_TIMEOUT,
heartbeatInterval: options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
};
this.defaultBrowser =
options.defaultBrowser ?? (process.env.MEARL_BROWSER?.trim() || undefined);
this.defaultConnector = options.defaultConnector;
}
disconnect() {
this.closing = true;
if (this.heartbeatTimer)
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
this.rejectAllPending(new Error('Connection closed by user'));
this.ws?.close();
this.ws = null;
}
async invoke(action, data = {}, options = {}) {
const timeoutSec = options.timeoutSec ?? resolveActionTimeoutSec(action, data, this.options.requestTimeoutSec);
if (action === 'connector_list')
return this.listConnectors(timeoutSec);
if (action === 'browser_list') {
if (options.local || options.transport === 'cdp') {
return this.listLocalBrowsers(timeoutSec, options.transport);
}
const connector = options.connector ?? this.defaultConnector;
return connector
? this.listConnectorBrowsers(connector, timeoutSec)
: this.listAllBrowsers(timeoutSec);
}
if (action === 'browser_select_browser') {
return this.selectDefaultBrowser(data, options, timeoutSec);
}
if (options.local || options.transport === 'cdp') {
return this.invokeExplicitLocal(action, data, options, timeoutSec);
}
const optionTarget = decodeBrowserTarget(options.browser);
const defaultTarget = options.browser ? undefined : decodeBrowserTarget(this.defaultBrowser);
const closeTarget = action === 'browser_close' && typeof data.browser === 'string'
? decodeBrowserTarget(data.browser)
: undefined;
const target = optionTarget ?? closeTarget ?? defaultTarget;
const connector = target?.connector ?? options.connector ?? this.defaultConnector;
const browser = optionTarget?.browser ??
defaultTarget?.browser ??
(options.browser && !optionTarget ? options.browser : undefined) ??
(this.defaultBrowser && !defaultTarget ? this.defaultBrowser : undefined);
const requestData = closeTarget ? { ...data, browser: closeTarget.browser } : data;
if (target && !target.connector) {
if (options.connector) {
throw new Error('A local browser cannot be used together with --connector.');
}
return this.invokeLocal(action, requestData, timeoutSec, target.browser);
}
if (connector) {
const selected = target?.connector && !options.connector && !this.defaultConnector
? { connectorId: target.connector, name: target.connector }
: await this.resolveConnector(connector, timeoutSec);
if (target?.connector && selected.connectorId !== target.connector) {
throw new Error('The selected browser belongs to a different cloud connector.');
}
return this.invokeRemote(action, requestData, timeoutSec, browser, selected);
}
if (!this.options.serverUrl) {
return this.invokeLocal(action, requestData, timeoutSec, options.browser ?? this.defaultBrowser);
}
const selector = options.browser ??
this.defaultBrowser ??
(action === 'browser_close' && typeof data.browser === 'string' ? data.browser : undefined);
const resolved = await this.resolveUnifiedBrowser(selector, timeoutSec, action === 'browser_close');
if (resolved) {
if (action === 'browser_close') {
const nextOptions = { ...options };
delete nextOptions.browser;
return this.invoke(action, { ...data, browser: resolved }, nextOptions);
}
return this.invoke(action, data, { ...options, browser: resolved });
}
if (action === 'browser_launch') {
return this.invokeLaunchWithoutControlBrowser(data, timeoutSec);
}
throw new Error('No connected browser is available. Run `mearl browser_list` to inspect all sources.');
}
async invokeExplicitLocal(action, data, options, timeoutSec) {
const optionTarget = decodeBrowserTarget(options.browser);
const defaultTarget = options.browser ? undefined : decodeBrowserTarget(this.defaultBrowser);
const closeTarget = action === 'browser_close' && typeof data.browser === 'string'
? decodeBrowserTarget(data.browser)
: undefined;
if (optionTarget?.connector || defaultTarget?.connector || closeTarget?.connector) {
throw new Error('A remote browser cannot be used together with --local or --cdp.');
}
const browser = optionTarget?.browser ?? defaultTarget?.browser ?? options.browser ?? this.defaultBrowser;
const requestData = closeTarget ? { ...data, browser: closeTarget.browser } : data;
return this.invokeLocal(action, requestData, timeoutSec, browser, options.transport);
}
async invokeLaunchWithoutControlBrowser(data, timeoutSec) {
const local = await this.tryLocalBrowserList(timeoutSec);
const discoveredConnectors = await this.listConnectors(timeoutSec).then(result => result.connectors);
const localAvailable = local !== null;
const localConnectorId = localAvailable ? loadLocalConnectorId() : null;
const connectors = localConnectorId
? discoveredConnectors.filter(connector => connector.connectorId !== localConnectorId)
: discoveredConnectors;
if (localAvailable && connectors.length === 0) {
return this.invokeLocal('browser_launch', data, timeoutSec, undefined);
}
if (!localAvailable && connectors.length === 1) {
return this.invokeRemote('browser_launch', data, timeoutSec, undefined, connectors[0]);
}
if (!localAvailable && connectors.length === 0) {
throw new Error('No browser host is available. Install @mearl/native-host locally or connect a cloud connector.');
}
throw new Error('The browser host for browser_launch is ambiguous. Pass --local, --connector <id|name>, or --browser <global-id>.');
}
async selectDefaultBrowser(data, options, timeoutSec) {
const selector = typeof data.browser === 'string' ? data.browser.trim() : '';
if (!selector) {
this.defaultBrowser = undefined;
this.defaultConnector = undefined;
return { selected: null, note: 'Cleared the process-wide default browser.' };
}
const listing = (await this.invoke('browser_list', {}, {
timeoutSec,
...(options.local ? { local: true } : {}),
...(options.transport ? { transport: options.transport } : {}),
...(options.connector ? { connector: options.connector } : {}),
}));
const matches = findBrowsers(listing.browsers, selector);
if (matches.length > 1) {
throw new Error(`Browser selector "${selector}" is ambiguous. Use an exact global browserId.`);
}
const match = matches[0];
if (!match)
throw new Error(`No connected browser matches "${selector}".`);
this.defaultBrowser = match.browserId;
this.defaultConnector = undefined;
return { selected: { browserId: match.browserId, name: match.name } };
}
async resolveUnifiedBrowser(selector, timeoutSec, includeDisconnected) {
const listing = await this.listAllBrowsers(timeoutSec);
if (selector) {
const matches = findBrowsers(listing.browsers, selector, !includeDisconnected);
if (matches.length > 1) {
throw new Error(`Browser selector "${selector}" is ambiguous. Use an exact global browserId from browser_list.`);
}
if (matches.length === 0) {
throw new Error(`No browser matches "${selector}". Run \`mearl browser_list\`.`);
}
return matches[0].browserId;
}
if (listing.defaultBrowserId)
return listing.defaultBrowserId;
const connected = listing.browsers.filter(browser => browser.status === 'connected');
if (connected.length === 1)
return connected[0].browserId;
if (connected.length > 1) {
throw new Error('Multiple browsers are connected and there is no unique default. Pass --browser <global-id>.');
}
return undefined;
}
async listConnectors(timeoutSec = this.options.requestTimeoutSec) {
if (!this.options.serverUrl)
return { count: 0, connectors: [] };
return this.requestRemote('connector_list', {}, timeoutSec, undefined, undefined);
}
async resolveConnector(selector, timeoutSec) {
const connectors = (await this.listConnectors(timeoutSec)).connectors;
const selection = resolveCloudConnectorSelector(connectors, selector);
if (selection.status === 'ambiguous') {
throw new Error(`Cloud connector selector "${selector}" is ambiguous. Use an exact connectorId.`);
}
if (selection.status !== 'matched') {
throw new Error(`No cloud connector matches "${selector}".`);
}
return selection.connector;
}
async listLocalBrowsers(timeoutSec, transport) {
const result = await invokeLocalBrowser('browser_list', {}, { timeoutSec, ...(transport ? { transport } : {}), controlSource: 'local' });
return this.applyConfiguredDefault(this.decorateListing(result, { kind: 'local' }));
}
async tryLocalBrowserList(timeoutSec) {
try {
return await invokeLocalBrowser('browser_list', {}, { timeoutSec, controlSource: 'local' });
}
catch {
return null;
}
}
decorateListing(result, source) {
const rawBrowsers = Array.isArray(result?.browsers)
? result.browsers
: [];
const browsers = rawBrowsers.map(browser => decorateBrowser(browser, source));
return {
count: browsers.length,
connectedCount: browsers.filter(browser => browser.status === 'connected').length,
defaultBrowserId: typeof result?.defaultBrowserId === 'string'
? encodeBrowserTarget({
browser: result.defaultBrowserId,
...(source.connectorId ? { connector: source.connectorId } : {}),
})
: null,
browsers,
};
}
applyConfiguredDefault(listing) {
if (!this.defaultBrowser)
return listing;
const matches = findBrowsers(listing.browsers, this.defaultBrowser);
return {
...listing,
defaultBrowserId: matches.length === 1 ? matches[0].browserId : null,
};
}
async listConnectorBrowsers(selector, timeoutSec) {
const connector = await this.resolveConnector(selector, timeoutSec);
const result = await this.requestRemote('browser_list', {}, timeoutSec, undefined, connector.connectorId);
return this.applyConfiguredDefault(this.decorateListing(result, {
kind: 'remote',
connectorId: connector.connectorId,
connectorName: connector.name,
}));
}
async listAllBrowsers(timeoutSec) {
const warnings = [];
const localPromise = this.tryLocalBrowserList(timeoutSec);
let connectors = [];
if (this.options.serverUrl) {
try {
connectors = (await this.listConnectors(timeoutSec)).connectors;
}
catch (error) {
warnings.push(`cloud-server: ${errorMessage(error)}`);
}
}
const localResult = await localPromise;
if (!localResult)
warnings.push('this machine: local browser runtime is unavailable');
const localConnectorId = localResult ? loadLocalConnectorId() : null;
const remoteConnectors = localConnectorId
? connectors.filter(connector => connector.connectorId !== localConnectorId)
: connectors;
const settled = await Promise.allSettled(remoteConnectors.map(async (connector) => ({
connector,
listing: await this.requestRemote('browser_list', {}, timeoutSec, undefined, connector.connectorId),
})));
const remoteListings = [];
for (let index = 0; index < settled.length; index += 1) {
const result = settled[index];
const connector = remoteConnectors[index];
if (result.status === 'fulfilled')
remoteListings.push(result.value);
else
warnings.push(`${connector.name}: ${errorMessage(result.reason)}`);
}
const listings = [];
if (localResult)
listings.push(this.decorateListing(localResult, { kind: 'local' }));
for (const { connector, listing } of remoteListings) {
listings.push(this.decorateListing(listing, {
kind: 'remote',
connectorId: connector.connectorId,
connectorName: connector.name,
}));
}
const browsers = listings.flatMap(listing => listing.browsers);
const defaults = listings
.map(listing => listing.defaultBrowserId)
.filter((browserId) => typeof browserId === 'string');
let defaultBrowserId = null;
if (this.defaultBrowser &&
browsers.some(browser => browser.browserId === this.defaultBrowser)) {
defaultBrowserId = this.defaultBrowser;
}
else if (defaults.length === 1) {
defaultBrowserId = defaults[0];
}
return this.applyConfiguredDefault({
count: browsers.length,
connectedCount: browsers.filter(browser => browser.status === 'connected').length,
defaultBrowserId,
browsers,
...(warnings.length > 0 ? { warnings } : {}),
});
}
async invokeLocal(action, data, timeoutSec, browser, transport) {
const result = await invokeLocalBrowser(action, data, {
timeoutSec,
...(browser ? { browser } : {}),
...(transport ? { transport } : {}),
controlSource: 'local',
});
return this.decorateActionResult(action, result, { kind: 'local' });
}
async invokeRemote(action, data, timeoutSec, browser, connector) {
const result = await this.requestRemote(action, data, timeoutSec, browser, connector.connectorId);
return this.decorateActionResult(action, result, {
kind: 'remote',
connectorId: connector.connectorId,
connectorName: connector.name,
});
}
decorateActionResult(action, result, source) {
if (action !== 'browser_launch' || typeof result?.browserId !== 'string')
return result;
const safeResult = omitTransientBrowserSecrets(result);
const originalBrowserId = result.browserId;
return {
...safeResult,
browserId: encodeBrowserTarget({
browser: originalBrowserId,
...(source.connectorId ? { connector: source.connectorId } : {}),
}),
originalBrowserId,
source,
};
}
async ensureConnected() {
if (this.ws?.readyState === WebSocket.OPEN)
return;
if (this.connectPromise)
return this.connectPromise;
this.closing = false;
this.connectPromise = this.connect();
try {
await this.connectPromise;
}
finally {
this.connectPromise = null;
}
}
connect() {
return new Promise((resolve, reject) => {
if (!this.options.serverUrl) {
reject(new Error('Cloud server URL is required. Start cloud-server, pass --server, or set MEARL_SERVER_URL.'));
return;
}
const ws = new WebSocket(this.options.serverUrl);
this.ws = ws;
let settled = false;
const timer = setTimeout(() => {
ws.close();
if (!settled)
reject(new Error(`Connection timeout after ${this.options.connectTimeoutSec}s`));
}, this.options.connectTimeoutSec * 1_000);
ws.on('open', () => {
settled = true;
clearTimeout(timer);
ws.send(JSON.stringify({ type: 'agent_hello' }) + '\n');
this.startHeartbeat();
resolve();
});
ws.on('message', data => this.handleMessage(data));
ws.on('close', (code, reason) => {
clearTimeout(timer);
this.rejectAllPending(new Error(`Connection closed (${code}: ${reason.toString()})`));
if (!this.closing)
this.ws = null;
});
ws.on('error', error => {
clearTimeout(timer);
if (!settled)
reject(error);
});
});
}
startHeartbeat() {
if (this.heartbeatTimer)
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }) + '\n');
}
}, this.options.heartbeatInterval * 1_000);
}
handleMessage(data) {
this.messageBuffer += data.toString('utf8');
if (this.messageBuffer.length > MAX_BUFFER_SIZE) {
this.disconnect();
return;
}
let newlineIndex;
while ((newlineIndex = this.messageBuffer.indexOf('\n')) !== -1) {
const line = this.messageBuffer.slice(0, newlineIndex);
this.messageBuffer = this.messageBuffer.slice(newlineIndex + 1);
if (!line.trim())
continue;
try {
const message = JSON.parse(line);
if (!isHeartbeatMessage(message))
this.handleResponse(message);
}
catch {
// Ignore malformed or unrelated frames; pending requests retain their timeout.
}
}
}
handleResponse(response) {
const pending = this.pendingRequests.get(response.id);
if (!pending)
return;
this.pendingRequests.delete(response.id);
clearTimeout(pending.timer);
if (response.versionWarning)
console.warn(response.versionWarning);
if (response.success)
pending.resolve(response.data);
else
pending.reject(new Error(response.error || 'Unknown error from cloud-server'));
}
async requestRemote(action, data, timeoutSec, browser, connector) {
await this.ensureConnected();
const ws = this.ws;
if (!ws || ws.readyState !== WebSocket.OPEN)
throw new Error('Connection is not open');
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
const message = { id, action, data, timeoutSec };
if (browser)
message.browser = browser;
if (connector)
message.connector = connector;
const serialized = `${JSON.stringify(message)}\n`;
const bytes = Buffer.byteLength(serialized, 'utf8');
if (bytes > MAX_BUFFER_SIZE) {
throw new Error(`Cloud request exceeds ${MAX_BUFFER_SIZE} byte message limit (${bytes} bytes)`);
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`Request timeout after ${timeoutSec}s`));
}, timeoutSec * 1_000 + RESPONSE_GRACE_MS);
this.pendingRequests.set(id, { resolve, reject, timer });
ws.send(serialized);
});
}
rejectAllPending(error) {
for (const pending of this.pendingRequests.values()) {
clearTimeout(pending.timer);
pending.reject(error);
}
this.pendingRequests.clear();
}
}
let defaultBrowser;
let defaultConnector;
export async function invoke(action, data = {}, options = {}) {
const serverUrl = process.env.MEARL_SERVER_URL ?? loadCloudServerConfig()?.server;
const client = new UnifiedClient({
...(serverUrl ? { serverUrl } : {}),
defaultBrowser,
defaultConnector,
});
try {
const result = await client.invoke(action, data, options);
if (action === 'browser_select_browser') {
defaultBrowser = result?.selected?.browserId;
defaultConnector = undefined;
}
return result;
}
finally {
client.disconnect();
}
}
export function listCloudConnectors() {
return invoke('connector_list', {});
}
export interface CloudServerConfig {
server: string;
}
export declare function getCloudServerConfigPath(): string;
export declare function loadCloudServerConfig(): CloudServerConfig | null;
export declare function loadLocalConnectorId(): string | null;
import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
const MEARL_CONFIG_DIR = join(homedir(), '.mearl');
export function getCloudServerConfigPath() {
return join(MEARL_CONFIG_DIR, 'cloud-server.json');
}
export function loadCloudServerConfig() {
try {
const configPath = getCloudServerConfigPath();
if (!existsSync(configPath))
return null;
const config = JSON.parse(readFileSync(configPath, 'utf8'));
return typeof config.server === 'string' && config.server.trim()
? { server: config.server }
: null;
}
catch {
return null;
}
}
export function loadLocalConnectorId() {
const explicit = process.env.MEARL_CONNECTOR_ID?.trim();
if (explicit)
return explicit;
try {
const config = JSON.parse(readFileSync(join(MEARL_CONFIG_DIR, 'cloud-connector-identity.json'), 'utf8'));
return typeof config.connectorId === 'string' && config.connectorId.trim()
? config.connectorId
: null;
}
catch {
return null;
}
}
+22
-1

@@ -1,1 +0,22 @@

export declare function runCheck(): Promise<void>;
import { type UnifiedClientOptions, type UnifiedInvokeOptions } from './unified.js';
interface CheckClient {
invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
disconnect(): void;
}
export interface UnifiedCheckOptions {
clientVersion: string;
serverUrl?: string;
serverUrlSource?: string;
timeoutSec?: number;
browser?: string;
connector?: string;
local?: boolean;
transport?: 'cdp';
}
export interface UnifiedCheckDependencies {
createClient?: (options: UnifiedClientOptions) => CheckClient;
writeLine?: (line: string) => void;
}
export declare function parseCheckTimeout(value: string | undefined): number | null;
export declare function runUnifiedCheck(options: UnifiedCheckOptions, dependencies?: UnifiedCheckDependencies): Promise<boolean>;
export {};
+269
-127

@@ -1,146 +0,288 @@

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execSync } from 'node:child_process';
import { invoke, CLIENT_VERSION, SOCKET_PATH } from './socket.js';
import { listLiveBrowsers, describeBrowser } from './browserRegistry.js';
const EXTENSION_INSTALL_URL = 'https://chromewebstore.google.com/detail/mearl/aoehhjnofngknnjefamjbplchbolghkm';
function resolveManifestDir() {
switch (process.platform) {
case 'darwin':
return path.join(os.homedir(), 'Library/Application Support/Google/Chrome/NativeMessagingHosts');
case 'linux':
return path.join(os.homedir(), '.config/google-chrome/NativeMessagingHosts');
case 'win32':
return path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Google\\Chrome\\NativeMessagingHosts');
default:
return '';
}
import { UnifiedClient } from './unified.js';
function formatError(error) {
return error instanceof Error ? error.message : String(error);
}
function checkCommandExists(command) {
function redactServerUrl(serverUrl) {
try {
return execSync(`${command} --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
const parsed = new URL(serverUrl);
for (const key of parsed.searchParams.keys()) {
if (key.toLowerCase().includes('token'))
parsed.searchParams.set(key, 'redacted');
}
return parsed.toString();
}
catch {
return null;
return serverUrl.replace(/([?&][^=&\s]*token[^=&\s]*=)[^&\s]*/gi, '$1redacted');
}
}
export async function runCheck() {
const env = [];
// 1. native-host CLI on PATH
const cliVersion = checkCommandExists('mearl-native-host');
env.push(cliVersion
? { ok: true, label: 'native-host CLI' }
: {
ok: false,
label: 'native-host CLI not found',
fix: ['npm install -g @mearl/native-host'],
});
// 2. Native Messaging manifest
const manifestDir = resolveManifestDir();
const manifestName = 'com.alibaba.mearl.skills.json';
const manifestPath = manifestDir ? path.join(manifestDir, manifestName) : '';
env.push(manifestPath && fs.existsSync(manifestPath)
? { ok: true, label: 'Native Messaging manifest' }
: {
ok: false,
label: 'Native Messaging manifest not found',
fix: ['mearl-native-host --init'],
});
// 3. Socket file (Windows uses Named Pipes — assume present, probe via invoke)
const isWindows = process.platform === 'win32';
const socketExists = isWindows || fs.existsSync(SOCKET_PATH);
if (isWindows) {
env.push({ ok: true, label: `Socket (Named Pipe)` });
function browserMatches(browser, selector) {
const normalized = selector.trim().toLowerCase();
return (browser.browserId.toLowerCase() === normalized ||
browser.originalBrowserId?.toLowerCase() === normalized ||
browser.name.toLowerCase() === normalized ||
browser.originalName?.toLowerCase() === normalized);
}
function findBrowsers(browsers, selector) {
const exact = browsers.filter(browser => browserMatches(browser, selector));
if (exact.length > 0)
return exact;
const normalized = selector.trim().toLowerCase();
return browsers.filter(browser => browser.browserId.toLowerCase().includes(normalized) ||
browser.originalBrowserId?.toLowerCase().includes(normalized) ||
browser.name.toLowerCase().includes(normalized) ||
browser.originalName?.toLowerCase().includes(normalized));
}
function shortBrowserId(browserId) {
const parts = browserId.split(':');
const raw = decodeURIComponent(parts.at(-1) ?? browserId);
return raw.length > 12 ? `${raw.slice(0, 8)}…` : raw;
}
function formatBrowser(browser, isDefault) {
const transport = browser.transport === 'cdp'
? ' [CDP]'
: browser.transport === 'extension'
? ' [Extension]'
: '';
const type = browser.type === 'managed' ? ' [Managed]' : '';
return ` ${isDefault ? '★' : ' '} ${browser.name} [${shortBrowserId(browser.browserId)}]${type}${transport}`;
}
function formatConnector(connector) {
const id = connector.connectorId.length > 12
? `${connector.connectorId.slice(0, 8)}…`
: connector.connectorId;
const version = connector.version ? ` [v${connector.version}]` : '';
return ` ${connector.name} [${id}]${version}`;
}
function printSection(writeLine, title, items) {
writeLine(title);
for (const item of items) {
const marker = item.status === 'ok' ? '✅' : item.status === 'warning' ? '⚠️ ' : '❌';
writeLine(` ${marker} ${item.label}`);
for (const detail of item.details ?? [])
writeLine(` ${detail}`);
}
else if (socketExists) {
env.push({ ok: true, label: 'Socket file' });
}
else {
env.push({
ok: false,
label: `Socket file not found at ${SOCKET_PATH}`,
fix: [
'native host is not running',
'Extension mode: ensure Chrome is running and the extension is enabled',
'CDP mode: chrome://inspect/#remote-debugging → "Discover network targets"',
],
});
}
// 4. Connection + version fetch
let runtime = null;
let outdatedDaemon = false;
if (socketExists) {
writeLine('');
}
export function parseCheckTimeout(value) {
const input = value ?? '5';
if (!/^[1-9]\d*$/.test(input))
return null;
const timeoutSec = Number(input);
return Number.isSafeInteger(timeoutSec) ? timeoutSec : null;
}
export async function runUnifiedCheck(options, dependencies = {}) {
const timeoutSec = options.timeoutSec ?? 5;
const writeLine = dependencies.writeLine ?? (line => console.error(line));
const createClient = dependencies.createClient ??
((clientOptions) => new UnifiedClient(clientOptions));
const client = createClient({
...(options.serverUrl ? { serverUrl: options.serverUrl } : {}),
connectTimeoutSec: timeoutSec,
requestTimeoutSec: timeoutSec,
});
const environment = [
{ status: 'ok', label: `Unified client v${options.clientVersion}` },
];
let connectors = [];
let browserList = null;
try {
if (options.local) {
environment.push({
status: 'ok',
label: options.transport === 'cdp'
? 'Browser scope: this machine (forced CDP)'
: 'Browser scope: this machine',
});
}
else if (options.serverUrl) {
const source = options.serverUrlSource ? ` (${options.serverUrlSource})` : '';
try {
const connectorList = (await client.invoke('connector_list', {}, { timeoutSec }));
connectors = Array.isArray(connectorList?.connectors) ? connectorList.connectors : [];
environment.push({
status: 'ok',
label: `Cloud server${source}`,
details: [
redactServerUrl(options.serverUrl),
`${connectors.length} connector${connectors.length === 1 ? '' : 's'} connected`,
],
});
}
catch (error) {
environment.push({
status: 'error',
label: `Cloud server or connector discovery failed${source}`,
details: [formatError(error), redactServerUrl(options.serverUrl)],
});
}
}
else {
environment.push({ status: 'ok', label: 'Local mode (no cloud-server configuration)' });
}
const inventoryOptions = {
timeoutSec,
...(options.local ? { local: true } : {}),
...(options.transport ? { transport: options.transport } : {}),
...(!options.local && options.connector ? { connector: options.connector } : {}),
};
try {
runtime = (await invoke('get_versions', {}, { timeoutSec: 5 }));
env.push({ ok: true, label: 'Connection' });
if (options.serverUrl &&
!options.local &&
environment.some(item => item.status === 'error')) {
browserList = (await client.invoke('browser_list', {}, {
timeoutSec,
local: true,
}));
}
else {
browserList = (await client.invoke('browser_list', {}, inventoryOptions));
}
environment.push({ status: 'ok', label: 'Browser inventory' });
if (browserList.warnings?.length) {
environment.push({
status: 'warning',
label: 'Some browser sources could not be inspected',
details: browserList.warnings,
});
}
}
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (msg.includes('Unknown action')) {
// Old native-host responded but doesn't know get_versions — connection itself is fine.
outdatedDaemon = true;
env.push({ ok: true, label: 'Connection' });
environment.push({
status: 'error',
label: 'Browser inventory failed',
details: [formatError(error)],
});
}
const connected = (browserList?.browsers ?? []).filter(browser => browser.status === 'connected');
let targets = connected;
if (options.browser) {
const matches = findBrowsers(connected, options.browser);
if (matches.length === 0) {
environment.push({
status: 'error',
label: `Browser "${options.browser}" is not connected`,
details: ['Run `mearl browser_list` and use an exact global browserId.'],
});
targets = [];
}
else if (matches.length > 1) {
environment.push({
status: 'error',
label: `Browser selector "${options.browser}" is ambiguous`,
details: matches.map(browser => browser.browserId),
});
targets = [];
}
else {
env.push({ ok: false, label: 'Connection failed', fix: [msg] });
targets = matches;
}
}
}
// Build versions list
const versions = [['client', `v${CLIENT_VERSION}`]];
const nhVersion = runtime?.nativeHost ?? cliVersion;
if (nhVersion)
versions.push(['native-host', `v${nhVersion}`]);
if (runtime?.extension) {
versions.push(['extension', `v${runtime.extension}`]);
}
if (runtime?.browser) {
versions.push(['browser', `Chrome ${runtime.browser}`]);
}
// Print Versions section
const nameWidth = Math.max(...versions.map(([n]) => n.length));
console.error('\nVersions');
for (const [name, value] of versions) {
console.error(` ${name.padEnd(nameWidth)} ${value}`);
}
console.error('');
// Print Environment section
console.error('Environment');
let hasError = false;
for (const e of env) {
console.error(` ${e.ok ? '✅' : '❌'} ${e.label}`);
if (!e.ok) {
hasError = true;
for (const fix of e.fix ?? [])
console.error(` ${fix}`);
if (!options.browser && connected.length === 0 && browserList) {
environment.push({
status: 'error',
label: 'No connected browser',
details: [
'Open Chrome with the Mearl extension, start a managed browser, or connect a browser host.',
],
});
}
}
if (outdatedDaemon) {
console.error(' ⚠️ native-host daemon is outdated (missing get_versions). Restart Chrome or re-init: mearl-native-host --init');
}
console.error('');
// Connected browsers (multi-browser addressing)
const browsers = listLiveBrowsers();
if (browsers.length > 0) {
console.error('Connected browsers');
browsers.forEach((b, i) => {
const marker = i === 0 ? '★' : ' ';
const via = b.transport === 'cdp' ? ' [CDP]' : '';
console.error(` ${marker} ${describeBrowser(b)}${via}`);
});
if (browsers.length > 1) {
console.error(' ★ = default target (most-recently focused). Use --browser to pick another.');
const browserChecks = [];
await Promise.all(targets.map(async (browser) => {
try {
const versions = (await client.invoke('get_versions', {}, {
timeoutSec,
browser: browser.browserId,
...(options.transport ? { transport: options.transport } : {}),
}));
const details = [
versions.nativeHost ? `native-host v${versions.nativeHost}` : null,
versions.extension ? `extension v${versions.extension}` : null,
versions.browser ? `Chrome ${versions.browser}` : null,
].filter((value) => value !== null);
let accessUrlWarning;
if (browser.provider === 'agentbay' && options.browser) {
try {
const access = (await client.invoke('get_agentbay_access_url', {}, {
timeoutSec,
browser: browser.browserId,
}));
if (!access.agentbayAccessUrl) {
throw new Error('AgentBay did not return a browser access URL');
}
details.push(`AgentBay access URL (temporary, sensitive): ${access.agentbayAccessUrl}`);
}
catch (error) {
accessUrlWarning = formatError(error);
details.push(`AgentBay access URL unavailable: ${accessUrlWarning}`);
}
}
browserChecks.push({
browser,
item: {
status: accessUrlWarning ? 'warning' : 'ok',
label: accessUrlWarning
? 'Browser command path ready; AgentBay access URL unavailable'
: 'Browser command path',
details,
},
});
}
catch (error) {
const message = formatError(error);
browserChecks.push({
browser,
item: message.includes('Unknown action')
? {
status: 'warning',
label: 'Browser is reachable but runtime version details are unavailable',
details: ['Update @mearl/native-host for complete diagnostics.'],
}
: {
status: 'error',
label: 'Browser command path failed',
details: [message],
},
});
}
}));
writeLine('');
printSection(writeLine, 'Environment', environment);
if (connectors.length > 0) {
writeLine('Cloud connectors');
for (const connector of connectors)
writeLine(formatConnector(connector));
writeLine('');
}
console.error('');
const listedBrowsers = browserList?.browsers ?? [];
if (listedBrowsers.length > 0) {
writeLine('Browsers');
for (const browser of listedBrowsers) {
writeLine(formatBrowser(browser, browser.browserId === browserList?.defaultBrowserId));
const check = browserChecks.find(item => item.browser.browserId === browser.browserId);
if (check) {
const marker = check.item.status === 'ok' ? '✅' : check.item.status === 'warning' ? '⚠️ ' : '❌';
writeLine(` ${marker} ${check.item.label}`);
for (const detail of check.item.details ?? [])
writeLine(` ${detail}`);
}
else if (browser.status !== 'connected') {
writeLine(` ⚪ ${browser.status}`);
}
}
writeLine('');
}
const hasError = environment.some(item => item.status === 'error') ||
browserChecks.some(check => check.item.status === 'error');
if (hasError) {
writeLine('⚠️ Mearl checks failed. See the browser or source details above.');
return false;
}
writeLine(options.browser
? '🎉 Selected browser is ready!'
: `🎉 All ${targets.length} connected browser${targets.length === 1 ? '' : 's'} are ready!`);
return true;
}
// Summary
if (hasError) {
console.error('⚠️ Some checks failed. See above for fix instructions.');
console.error(` Install extension: ${EXTENSION_INSTALL_URL}`);
process.exit(1);
finally {
client.disconnect();
}
else {
console.error('🎉 All checks passed!');
}
}
#!/usr/bin/env node
/**
* mearl CLI — 通过命令行调用 Mearl 浏览器插件能力
*
* 用法:
* mearl <action> [--payload <json> | --payload-file <path>] [--timeout <seconds>] [--compact] [--output <path>]
*
* 示例:
* mearl get_requests --payload '{"count":5}'
* mearl send_request --payload '{"url":"https://api.example.com","method":"GET"}'
* mearl page_screenshot --output ./screenshot.png
* mearl set_mock --payload-file ./mock-data.json
*/
export {};
export interface MearlCliOptions {
commandName?: string;
clientVersion?: string;
}
export declare function runMearlCli(args: string[], options?: MearlCliOptions): Promise<number>;
+208
-222
#!/usr/bin/env node
/**
* mearl CLI — 通过命令行调用 Mearl 浏览器插件能力
*
* 用法:
* mearl <action> [--payload <json> | --payload-file <path>] [--timeout <seconds>] [--compact] [--output <path>]
*
* 示例:
* mearl get_requests --payload '{"count":5}'
* mearl send_request --payload '{"url":"https://api.example.com","method":"GET"}'
* mearl page_screenshot --output ./screenshot.png
* mearl set_mock --payload-file ./mock-data.json
*/
import fs from 'node:fs';
import { invoke, CLIENT_VERSION } from './socket.js';
import { readFileSync, writeFileSync } from 'node:fs';
import path, { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveActionTimeoutSec } from './actionTimeouts.js';
import { runUnifiedCheck, parseCheckTimeout } from './check.js';
import { COMMAND_MAP } from './commands.js';
import { runCheck } from './check.js';
import { resolveActionTimeoutSec } from './actionTimeouts.js';
// ─── Usage / Help ─────────────────────────────────────────────────────────────
import { CLIENT_VERSION } from './socket.js';
import { UnifiedClient } from './unified.js';
import { getCloudServerConfigPath, loadCloudServerConfig } from './unifiedConfig.js';
const CATEGORIES = [

@@ -59,273 +50,268 @@ {

];
function printUsage() {
function optionValue(args, name) {
const index = args.indexOf(name);
return index >= 0 ? args[index + 1] : undefined;
}
function printUsage(commandName, version) {
const lines = [
`${commandName} v${version}`,
'',
'用法:',
' mearl <action> [选项]',
' mearl <action> --help',
' mearl check',
` ${commandName} <action> [选项]`,
` ${commandName} <action> --help`,
` ${commandName} check [--browser <id|名称>] [--connector <id|名称>]`,
'',
'统一目标发现:',
' browser_list 默认列出当前机器和 cloud-server 下所有 connector 的浏览器。',
' 后续操作使用列表返回的全局 browserId,客户端会自动选择本地或远端传输。',
'',
'通用选项:',
' --payload <json> 直接传入 JSON 参数',
' --payload-file <path> 从文件读取整个 JSON 参数(适合大体积 payload)',
' --timeout <seconds> 超时时间(默认按 action 推导,普通 action 为 15 秒)',
' --payload-file <path> 从文件读取整个 JSON 参数',
' --timeout <seconds> 超时时间(默认按 action 推导)',
' --compact 输出紧凑格式 JSON',
' --output <path> 输出文件路径(page_screenshot / page_selected_element 可用)',
' --cdp 强制使用 CDP 模式(跳过插件通道)',
' --browser <id|名称> 指定目标浏览器(多浏览器时;缺省用最近聚焦的那个)',
' --output <path> 保存输出或截图',
' --browser <id|名称> 指定 browser_list 返回的浏览器',
' --connector <id|名称> 限制到一台远端 connector',
' --local 只使用当前机器上的浏览器',
' --cdp 当前机器强制使用 CDP(隐含 --local)',
' --server <url> 覆盖 cloud-server WebSocket URL',
'',
'文件引用(任意 action 通用,减少上下文占用):',
' payload 顶层字段写成 "@<path>"(@ 开头的字符串)即从文件读取该字段值,',
' 默认按 JSON 解析、失败按原始文本;找不到文件则原样保留该字符串。',
` 例:set_mock --payload '{"apiName":"x","mockData":"@/abs/resp.json"}'`,
'',
'内置命令:',
' check 检测环境配置,排查连接问题',
' check 检查指定浏览器,未指定时检查全部已连接浏览器',
' connector_list 列出 cloud-server 下的 connector',
];
for (const cat of CATEGORIES) {
lines.push('');
lines.push(` ${cat.title}`);
for (const name of cat.names) {
const cmd = COMMAND_MAP.get(name);
if (cmd) {
lines.push(` ${name.padEnd(26)}${cmd.description}`);
}
for (const category of CATEGORIES) {
lines.push('', ` ${category.title}`);
for (const name of category.names) {
const command = COMMAND_MAP.get(name);
if (command)
lines.push(` ${name.padEnd(26)}${command.description}`);
}
}
lines.push('');
lines.push('运行 mearl <action> --help 查看某个命令的详细参数');
lines.push('', `运行 ${commandName} <action> --help 查看 action 参数。`);
console.error(lines.join('\n'));
}
function printCommandHelp(cmd) {
const outputOption = cmd.outputFlag ? ' [--output <path>]' : '';
function printCommandHelp(commandName, command) {
const lines = [
`${cmd.name}`,
` ${cmd.description}`,
`${command.name}`,
` ${command.description}`,
'',
'用法:',
` mearl ${cmd.name} [--payload <json>] [--timeout <seconds>]${outputOption}`,
` ${commandName} ${command.name} [--payload <json>] [--browser <id|名称>]`,
'',
];
if (cmd.params && cmd.params.length > 0) {
if (command.params?.length) {
lines.push('参数(--payload JSON 字段):');
for (const p of cmd.params) {
const req = p.required ? '必填' : '可选';
lines.push(` ${p.name.padEnd(22)}${req.padEnd(6)}${p.type.padEnd(28)}${p.description}`);
for (const parameter of command.params) {
const required = parameter.required ? '必填' : '可选';
lines.push(` ${parameter.name.padEnd(22)}${required.padEnd(6)}${parameter.type.padEnd(28)}${parameter.description}`);
}
lines.push('');
}
if (cmd.outputFlag) {
lines.push('CLI 选项:');
lines.push(' --output <path> 将截图保存到本地文件');
lines.push('');
}
if (cmd.examples && cmd.examples.length > 0) {
if (command.examples?.length) {
lines.push('示例:');
for (const ex of cmd.examples) {
lines.push(` mearl ${ex}`);
}
lines.push('');
for (const example of command.examples)
lines.push(` ${commandName} ${example}`);
}
console.error(lines.join('\n'));
}
function parseArgs(argv) {
if (argv.includes('--version') || argv.includes('-v')) {
console.log(CLIENT_VERSION);
process.exit(0);
function parsePositiveTimeout(value, fallback) {
if (value === undefined)
return fallback;
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error('--timeout 必须是大于 0 的数字');
}
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
return { showHelp: true, action: COMMAND_MAP.has(argv[0]) ? argv[0] : undefined };
return parsed;
}
function parsePayload(args) {
const payloadRaw = optionValue(args, '--payload');
const payloadFile = optionValue(args, '--payload-file');
if (payloadRaw !== undefined && payloadFile !== undefined) {
throw new Error('--payload 与 --payload-file 不能同时使用');
}
const action = argv[0];
let payloadRaw = '{}';
let payloadFile = null;
let timeoutSec;
let compact = false;
let outputPath = null;
let forceCdp = false;
let browser;
let i = 1;
while (i < argv.length) {
const arg = argv[i];
if (arg === '--payload') {
if (i + 1 >= argv.length)
throw new Error('缺少 --payload 的参数值');
payloadRaw = argv[i + 1];
i += 2;
continue;
if (payloadRaw !== undefined) {
try {
return JSON.parse(payloadRaw);
}
if (arg === '--payload-file') {
if (i + 1 >= argv.length)
throw new Error('缺少 --payload-file 的参数值');
payloadFile = argv[i + 1];
i += 2;
continue;
catch (error) {
throw new Error('无效的 JSON payload', { cause: error });
}
if (arg === '--timeout') {
if (i + 1 >= argv.length)
throw new Error('缺少 --timeout 的参数值');
const value = Number(argv[i + 1]);
if (!Number.isFinite(value) || value <= 0)
throw new Error('--timeout 必须是大于 0 的数字');
timeoutSec = value;
i += 2;
continue;
}
if (payloadFile !== undefined) {
try {
return JSON.parse(readFileSync(resolve(payloadFile), 'utf8'));
}
if (arg === '--compact') {
compact = true;
i += 1;
continue;
catch (error) {
throw new Error(`无法读取或解析 payload 文件: ${payloadFile}`, { cause: error });
}
if (arg === '--cdp') {
forceCdp = true;
i += 1;
}
return {};
}
function validateOptions(args) {
const optionsWithValues = new Set([
'--payload',
'--payload-file',
'--timeout',
'--output',
'--browser',
'--connector',
'--server',
]);
const flags = new Set(['--compact', '--local', '--cdp', '--help', '-h']);
for (let index = 1; index < args.length; index += 1) {
const arg = args[index];
if (optionsWithValues.has(arg)) {
if (!args[index + 1] || args[index + 1].startsWith('--')) {
throw new Error(`缺少 ${arg} 的参数值`);
}
index += 1;
continue;
}
if (arg === '--browser') {
if (i + 1 >= argv.length)
throw new Error('缺少 --browser 的参数值');
browser = argv[i + 1];
i += 2;
if (flags.has(arg))
continue;
}
if (arg === '--output') {
if (i + 1 >= argv.length)
throw new Error('缺少 --output 的参数值');
outputPath = argv[i + 1];
i += 2;
continue;
}
throw new Error(`未知参数: ${arg}`);
}
}
function resolveServer(args) {
const explicit = optionValue(args, '--server');
if (explicit)
return { serverUrl: explicit, serverUrlSource: '--server' };
if (process.env.MEARL_SERVER_URL) {
return { serverUrl: process.env.MEARL_SERVER_URL, serverUrlSource: 'MEARL_SERVER_URL' };
}
const config = loadCloudServerConfig();
return config?.server
? { serverUrl: config.server, serverUrlSource: getCloudServerConfigPath() }
: {};
}
function parseArgs(args) {
validateOptions(args);
const action = args[0];
const payload = parsePayload(args);
const fallbackTimeout = resolveActionTimeoutSec(action, payload, 60);
const forceCdp = args.includes('--cdp');
const local = forceCdp || args.includes('--local');
const connector = optionValue(args, '--connector');
if (local && connector)
throw new Error('--local/--cdp 不能与 --connector 同时使用');
return {
showHelp: false,
action,
payloadRaw,
payloadFile,
timeoutSec,
compact,
outputPath,
payload,
timeoutSec: parsePositiveTimeout(optionValue(args, '--timeout'), fallbackTimeout),
compact: args.includes('--compact'),
forceCdp,
browser,
local,
...(optionValue(args, '--output') ? { outputPath: optionValue(args, '--output') } : {}),
...(optionValue(args, '--browser') ? { browser: optionValue(args, '--browser') } : {}),
...(connector ? { connector } : {}),
...resolveServer(args),
};
}
function parsePayload(payloadRaw) {
try {
return JSON.parse(payloadRaw);
function screenshotData(action, result) {
if (typeof result === 'string' && result.startsWith('data:image')) {
return result.split(',')[1] ?? null;
}
catch (error) {
throw new Error(`无效的 JSON payload: ${payloadRaw}`, { cause: error });
if (action === 'page_screenshot' && typeof result?.data === 'string')
return result.data;
if (action === 'page_selected_element' && typeof result?.screenshot?.data === 'string') {
return result.screenshot.data;
}
return null;
}
function saveBase64Image(outputPath, base64Data) {
fs.writeFileSync(outputPath, Buffer.from(base64Data, 'base64'));
}
function extractScreenshotResult(action, result) {
if (action === 'page_screenshot' && result?.meta && result?.data) {
return {
label: 'Screenshot',
meta: result.meta,
data: result.data,
extraLines: [` Page: ${result.meta.title}`, ` URL: ${result.meta.url}`],
};
function writeOutput(action, result, outputPath) {
const image = screenshotData(action, result);
if (image) {
writeFileSync(outputPath, Buffer.from(image, 'base64'));
}
if (action === 'page_selected_element' && result?.screenshot?.meta && result?.screenshot?.data) {
return {
label: 'Selected element screenshot',
meta: result.screenshot.meta,
data: result.screenshot.data,
extraLines: [
` Element: ${result.tagName || 'unknown'}`,
` Selector: ${result.selector || 'N/A'}`,
],
};
else {
writeFileSync(outputPath, typeof result === 'string' ? result : `${JSON.stringify(result, null, 2)}\n`, 'utf8');
}
return null;
console.error(`✓ Output saved to: ${outputPath}`);
}
async function main() {
// Handle built-in subcommands before normal arg parsing
if (process.argv[2] === 'check') {
await runCheck();
return;
export async function runMearlCli(args, options = {}) {
const commandName = options.commandName ?? 'mearl';
const clientVersion = options.clientVersion ?? CLIENT_VERSION;
if (args.includes('--version') || args.includes('-v')) {
console.log(clientVersion);
return 0;
}
let args;
if (args.length === 0) {
printUsage(commandName, clientVersion);
return 1;
}
if (args.includes('--help') || args.includes('-h')) {
const command = COMMAND_MAP.get(args[0]);
if (command)
printCommandHelp(commandName, command);
else
printUsage(commandName, clientVersion);
return 0;
}
let parsed;
try {
args = parseArgs(process.argv.slice(2));
parsed = parseArgs(args);
}
catch (error) {
console.error(error instanceof Error ? error.message : String(error));
printUsage();
process.exit(2);
return 2;
}
if (args.showHelp) {
if (args.action) {
printCommandHelp(COMMAND_MAP.get(args.action));
if (parsed.action === 'check') {
const timeoutSec = parseCheckTimeout(optionValue(args, '--timeout'));
if (timeoutSec === null) {
console.error('--timeout 必须是正整数秒');
return 2;
}
else {
printUsage();
}
return;
const ok = await runUnifiedCheck({
clientVersion,
timeoutSec,
...(parsed.serverUrl ? { serverUrl: parsed.serverUrl } : {}),
...(parsed.serverUrlSource ? { serverUrlSource: parsed.serverUrlSource } : {}),
...(parsed.browser ? { browser: parsed.browser } : {}),
...(parsed.connector ? { connector: parsed.connector } : {}),
...(parsed.local ? { local: true } : {}),
...(parsed.forceCdp ? { transport: 'cdp' } : {}),
});
return ok ? 0 : 1;
}
if (!COMMAND_MAP.has(args.action)) {
console.error(`不支持的 action: ${args.action}`);
console.error(`运行 mearl --help 查看所有可用命令`);
process.exit(2);
if (parsed.action !== 'connector_list' && !COMMAND_MAP.has(parsed.action)) {
console.error(`不支持的 action: ${parsed.action}`);
console.error(`运行 ${commandName} --help 查看所有可用命令`);
return 2;
}
let data;
if (parsed.action === 'page_selected_element' && parsed.outputPath) {
parsed.payload = { ...parsed.payload, includeScreenshot: true };
}
const client = new UnifiedClient({
...(parsed.serverUrl ? { serverUrl: parsed.serverUrl } : {}),
requestTimeoutSec: parsed.timeoutSec,
});
try {
if (args.payloadFile) {
if (!fs.existsSync(args.payloadFile)) {
throw new Error(`Payload file not found: ${args.payloadFile}`);
}
const fileContent = fs.readFileSync(args.payloadFile, 'utf-8');
data = JSON.parse(fileContent);
const result = await client.invoke(parsed.action, parsed.payload, {
timeoutSec: parsed.timeoutSec,
...(parsed.browser ? { browser: parsed.browser } : {}),
...(parsed.connector ? { connector: parsed.connector } : {}),
...(parsed.local ? { local: true } : {}),
...(parsed.forceCdp ? { transport: 'cdp' } : {}),
});
if (parsed.outputPath) {
writeOutput(parsed.action, result, parsed.outputPath);
}
else {
data = parsePayload(args.payloadRaw);
process.stdout.write(`${parsed.compact ? JSON.stringify(result) : JSON.stringify(result, null, 2)}\n`);
}
return 0;
}
catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(2);
return 1;
}
try {
if (args.action === 'page_selected_element' && args.outputPath) {
data = { ...data, includeScreenshot: true };
}
const result = await invoke(args.action, data, {
timeoutSec: args.timeoutSec ?? resolveActionTimeoutSec(args.action, data),
transport: args.forceCdp ? 'cdp' : undefined,
browser: args.browser,
});
const screenshot = extractScreenshotResult(args.action, result);
if (screenshot) {
const sizeKB = Math.round(screenshot.meta.size / 1024);
if (args.outputPath) {
try {
saveBase64Image(args.outputPath, screenshot.data);
console.error(`✓ ${screenshot.label} saved to: ${args.outputPath}`);
}
catch (err) {
console.error(`✗ Failed to save ${screenshot.label.toLowerCase()}: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
}
console.error(`✓ ${screenshot.label} captured`);
console.error(` Format: ${screenshot.meta.format.toUpperCase()}`);
console.error(` Size: ${screenshot.meta.width}x${screenshot.meta.height}`);
console.error(` File size: ~${sizeKB}KB`);
screenshot.extraLines.forEach(line => console.error(line));
if (!args.outputPath) {
const output = args.compact ? JSON.stringify(result) : JSON.stringify(result, null, 2);
process.stdout.write(output + '\n');
}
return;
}
const output = args.compact ? JSON.stringify(result) : JSON.stringify(result, null, 2);
process.stdout.write(output + '\n');
finally {
client.disconnect();
}
catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
main();
const currentFile = fileURLToPath(import.meta.url);
if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
process.exitCode = await runMearlCli(process.argv.slice(2));
}

@@ -121,2 +121,7 @@ export interface GetRequestsParams {

export type BrowserStatus = 'connected' | 'running_disconnected' | 'stopped';
export interface BrowserSource {
kind: 'local' | 'remote';
connectorId?: string;
connectorName?: string;
}
export interface BrowserInfoBase {

@@ -126,2 +131,6 @@ browserId: string;

status: BrowserStatus;
/** Source-local values retained when the unified client returns a global browser id and label. */
originalBrowserId?: string;
originalName?: string;
source?: BrowserSource;
extensionVersion?: string;

@@ -153,4 +162,2 @@ nativeHostVersion?: string;

agentbayContextName?: string;
/** Browser-accessible AgentBay session URL when returned by the provider. */
agentbayAccessUrl?: string;
createdAt: string;

@@ -169,2 +176,4 @@ account?: ManagedBrowserAccount;

browsers: BrowserInfo[];
/** Partial discovery failures; successfully inspected browser sources remain in the result. */
warnings?: string[];
}

@@ -171,0 +180,0 @@ export interface SelectBrowserParams {

/**
* @mearl/client — Mearl Socket Client SDK
* @mearl/client — unified Mearl browser client
*
* 通过 Unix Domain Socket 与 Chrome Extension 的 native host 通信,
* 提供类型安全的具名方法和通用的 invoke 底层方法。
* Discovers browser targets on this machine and every configured cloud connector,
* then routes actions using the global browser ids returned by browser_list.
*/
export { invoke } from './socket.js';
export type { InvokeOptions } from './socket.js';
export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, listCloudConnectors, } from './unified.js';
export type { UnifiedClientOptions, UnifiedInvokeOptions, UnifiedInvokeOptions as InvokeOptions, } from './unified.js';
export { 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, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
export type { ActionStep, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserLaunchParams, BrowserListResult, BrowserReleaseFailure, BrowserReleaseResult, BrowserStatus, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetApiSchemaParams, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, GetUserInfoParams, ManagedBrowserAccount, ManagedBrowserInfo, PageClickParams, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSelectedElementParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RecordParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, TdbankAccountParams, } from './generated-browser-action-protocol.js';
export type { ActionStep, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserLaunchParams, BrowserListResult, BrowserReleaseFailure, BrowserReleaseResult, BrowserSource, BrowserStatus, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetApiSchemaParams, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, GetUserInfoParams, ManagedBrowserAccount, ManagedBrowserInfo, PageClickParams, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSelectedElementParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RecordParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, TdbankAccountParams, } from './generated-browser-action-protocol.js';
import { type UnifiedInvokeOptions } from './unified.js';
import type * as Protocol from './generated-browser-action-protocol.js';
export declare function captureCheckpoint(params?: Protocol.CaptureCheckpointParams): Promise<Protocol.BrowserActionResponse<'capture_checkpoint'>>;
export declare function getRequests(params?: Protocol.GetRequestsParams): Promise<Protocol.BrowserActionResponse<'get_requests'>>;
export declare function getLogs(params?: Protocol.GetLogsParams): Promise<Protocol.BrowserActionResponse<'get_logs'>>;
export declare function getEvents(params?: Protocol.GetEventsParams): Promise<Protocol.BrowserActionResponse<'get_events'>>;
export declare function setMock(params: Protocol.SetMockParams): Promise<Protocol.BrowserActionResponse<'set_mock'>>;
export declare function getMocks(params?: Protocol.GetMocksParams): Promise<Protocol.BrowserActionResponse<'get_mocks'>>;
export declare function getApiSchema(params: Protocol.GetApiSchemaParams): Promise<Protocol.BrowserActionResponse<'get_api_schema'>>;
export declare function sendRequest(params: Protocol.SendRequestParams): Promise<Protocol.BrowserActionResponse<'send_request'>>;
export declare function sendMtopRequest(params: Protocol.SendMtopRequestParams): Promise<Protocol.BrowserActionResponse<'send_mtop_request'>>;
export declare function tdbankAccount(params?: Protocol.TdbankAccountParams): Promise<Protocol.BrowserActionResponse<'tdbank_account'>>;
export declare function browserList(): Promise<Protocol.BrowserActionResponse<'browser_list'>>;
export declare function selectBrowser(params?: Protocol.SelectBrowserParams): Promise<Protocol.BrowserActionResponse<'browser_select_browser'>>;
export declare function browserRelease(): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
export declare function browserLaunch(params: Protocol.BrowserLaunchParams): Promise<Protocol.BrowserActionResponse<'browser_launch'>>;
export declare function browserClose(params: Protocol.BrowserCloseParams): Promise<Protocol.BrowserActionResponse<'browser_close'>>;
export declare function setRule(params: Protocol.SetRuleParams): Promise<Protocol.BrowserActionResponse<'set_rule'>>;
export declare function getRules(): Promise<Protocol.BrowserActionResponse<'get_rules'>>;
export declare function pageScreenshot(params: Protocol.PageScreenshotParams): Promise<Protocol.BrowserActionResponse<'page_screenshot'>>;
export declare function pageSelectedElement(params: Protocol.PageSelectedElementParams): Promise<Protocol.BrowserActionResponse<'page_selected_element'>>;
export declare function tabOpen(params: Protocol.TabOpenParams): Promise<Protocol.BrowserActionResponse<'tab_open'>>;
export declare function tabClose(params: Protocol.TabCloseParams): Promise<Protocol.BrowserActionResponse<'tab_close'>>;
export declare function tabList(): Promise<Protocol.BrowserActionResponse<'tab_list'>>;
export declare function pageClick(params: Protocol.PageClickParams): Promise<Protocol.BrowserActionResponse<'page_click'>>;
export declare function pageType(params: Protocol.PageTypeParams): Promise<Protocol.BrowserActionResponse<'page_type'>>;
export declare function pageScroll(params: Protocol.PageScrollParams): Promise<Protocol.BrowserActionResponse<'page_scroll'>>;
export declare function pageHover(params: Protocol.PageHoverParams): Promise<Protocol.BrowserActionResponse<'page_hover'>>;
export declare function pageEval(params: Protocol.PageEvalParams): Promise<Protocol.BrowserActionResponse<'page_eval'>>;
export declare function pageSnapshot(params: Protocol.PageSnapshotParams): Promise<Protocol.BrowserActionResponse<'page_snapshot'>>;
export declare function pagePress(params: Protocol.PagePressParams): Promise<Protocol.BrowserActionResponse<'page_press'>>;
export declare function pageWait(params: Protocol.PageWaitParams): Promise<Protocol.BrowserActionResponse<'page_wait'>>;
export declare function pageNavigate(params: Protocol.PageNavigateParams): Promise<Protocol.BrowserActionResponse<'page_navigate'>>;
export declare function pageUpload(params: Protocol.PageUploadParams): Promise<Protocol.BrowserActionResponse<'page_upload'>>;
export declare function pageFrames(params: Protocol.PageFramesParams): Promise<Protocol.BrowserActionResponse<'page_frames'>>;
export declare function setDeviceEmulation(params: Protocol.SetDeviceEmulationParams): Promise<Protocol.BrowserActionResponse<'set_device_emulation'>>;
export declare function setTimezone(params: Protocol.SetTimezoneParams): Promise<Protocol.BrowserActionResponse<'set_timezone'>>;
export declare function getCookie(params: Protocol.GetCookieParams): Promise<Protocol.BrowserActionResponse<'get_cookie'>>;
export declare function setCookie(params: Protocol.SetCookieParams): Promise<Protocol.BrowserActionResponse<'set_cookie'>>;
export declare function getUserInfo(params?: Protocol.GetUserInfoParams): Promise<Protocol.BrowserActionResponse<'get_user_info'>>;
export declare function runActions(params: Protocol.RunActionsParams): Promise<Protocol.BrowserActionResponse<'run_actions'>>;
export declare function captureCheckpoint(params?: Protocol.CaptureCheckpointParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'capture_checkpoint'>>;
export declare function getRequests(params?: Protocol.GetRequestsParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_requests'>>;
export declare function getLogs(params?: Protocol.GetLogsParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_logs'>>;
export declare function getEvents(params?: Protocol.GetEventsParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_events'>>;
export declare function setMock(params: Protocol.SetMockParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_mock'>>;
export declare function getMocks(params?: Protocol.GetMocksParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_mocks'>>;
export declare function getApiSchema(params: Protocol.GetApiSchemaParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_api_schema'>>;
export declare function sendRequest(params: Protocol.SendRequestParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'send_request'>>;
export declare function sendMtopRequest(params: Protocol.SendMtopRequestParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'send_mtop_request'>>;
export declare function tdbankAccount(params?: Protocol.TdbankAccountParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'tdbank_account'>>;
export declare function browserList(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_list'>>;
export declare function selectBrowser(params?: Protocol.SelectBrowserParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_select_browser'>>;
export declare function browserRelease(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
export declare function browserLaunch(params: Protocol.BrowserLaunchParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_launch'>>;
export declare function browserClose(params: Protocol.BrowserCloseParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_close'>>;
export declare function setRule(params: Protocol.SetRuleParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_rule'>>;
export declare function getRules(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_rules'>>;
export declare function pageScreenshot(params: Protocol.PageScreenshotParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_screenshot'>>;
export declare function pageSelectedElement(params: Protocol.PageSelectedElementParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_selected_element'>>;
export declare function tabOpen(params: Protocol.TabOpenParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'tab_open'>>;
export declare function tabClose(params: Protocol.TabCloseParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'tab_close'>>;
export declare function tabList(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'tab_list'>>;
export declare function pageClick(params: Protocol.PageClickParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_click'>>;
export declare function pageType(params: Protocol.PageTypeParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_type'>>;
export declare function pageScroll(params: Protocol.PageScrollParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_scroll'>>;
export declare function pageHover(params: Protocol.PageHoverParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_hover'>>;
export declare function pageEval(params: Protocol.PageEvalParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_eval'>>;
export declare function pageSnapshot(params: Protocol.PageSnapshotParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_snapshot'>>;
export declare function pagePress(params: Protocol.PagePressParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_press'>>;
export declare function pageWait(params: Protocol.PageWaitParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_wait'>>;
export declare function pageNavigate(params: Protocol.PageNavigateParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_navigate'>>;
export declare function pageUpload(params: Protocol.PageUploadParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_upload'>>;
export declare function pageFrames(params: Protocol.PageFramesParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'page_frames'>>;
export declare function setDeviceEmulation(params: Protocol.SetDeviceEmulationParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_device_emulation'>>;
export declare function setTimezone(params: Protocol.SetTimezoneParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_timezone'>>;
export declare function getCookie(params: Protocol.GetCookieParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_cookie'>>;
export declare function setCookie(params: Protocol.SetCookieParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_cookie'>>;
export declare function getUserInfo(params?: Protocol.GetUserInfoParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_user_info'>>;
export declare function runActions(params: Protocol.RunActionsParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'run_actions'>>;
/**
* @mearl/client — Mearl Socket Client SDK
* @mearl/client — unified Mearl browser client
*
* 通过 Unix Domain Socket 与 Chrome Extension 的 native host 通信,
* 提供类型安全的具名方法和通用的 invoke 底层方法。
* Discovers browser targets on this machine and every configured cloud connector,
* then routes actions using the global browser ids returned by browser_list.
*/
export { invoke } from './socket.js';
export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, listCloudConnectors, } from './unified.js';
export { 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, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
import { invoke } from './socket.js';
export function captureCheckpoint(params = {}) {
return invoke('capture_checkpoint', params);
import { invoke } from './unified.js';
export function captureCheckpoint(params = {}, options) {
return invoke('capture_checkpoint', params, options);
}
export function getRequests(params = {}) {
return invoke('get_requests', params);
export function getRequests(params = {}, options) {
return invoke('get_requests', params, options);
}
export function getLogs(params = {}) {
return invoke('get_logs', params);
export function getLogs(params = {}, options) {
return invoke('get_logs', params, options);
}
export function getEvents(params = {}) {
return invoke('get_events', params);
export function getEvents(params = {}, options) {
return invoke('get_events', params, options);
}
export function setMock(params) {
return invoke('set_mock', params);
export function setMock(params, options) {
return invoke('set_mock', params, options);
}
export function getMocks(params = {}) {
return invoke('get_mocks', params);
export function getMocks(params = {}, options) {
return invoke('get_mocks', params, options);
}
export function getApiSchema(params) {
return invoke('get_api_schema', params);
export function getApiSchema(params, options) {
return invoke('get_api_schema', params, options);
}
export function sendRequest(params) {
return invoke('send_request', params);
export function sendRequest(params, options) {
return invoke('send_request', params, options);
}
export function sendMtopRequest(params) {
return invoke('send_mtop_request', params);
export function sendMtopRequest(params, options) {
return invoke('send_mtop_request', params, options);
}
export function tdbankAccount(params = {}) {
return invoke('tdbank_account', params);
export function tdbankAccount(params = {}, options) {
return invoke('tdbank_account', params, options);
}
export function browserList() {
return invoke('browser_list', {});
export function browserList(options) {
return invoke('browser_list', {}, options);
}
export function selectBrowser(params = {}) {
return invoke('browser_select_browser', params);
export function selectBrowser(params = {}, options) {
return invoke('browser_select_browser', params, options);
}
export function browserRelease() {
return invoke('browser_release', {});
export function browserRelease(options) {
return invoke('browser_release', {}, options);
}
export function browserLaunch(params) {
return invoke('browser_launch', params);
export function browserLaunch(params, options) {
return invoke('browser_launch', params, options);
}
export function browserClose(params) {
return invoke('browser_close', params);
export function browserClose(params, options) {
return invoke('browser_close', params, options);
}
export function setRule(params) {
return invoke('set_rule', params);
export function setRule(params, options) {
return invoke('set_rule', params, options);
}
export function getRules() {
return invoke('get_rules', {});
export function getRules(options) {
return invoke('get_rules', {}, options);
}
export function pageScreenshot(params) {
return invoke('page_screenshot', params);
export function pageScreenshot(params, options) {
return invoke('page_screenshot', params, options);
}
export function pageSelectedElement(params) {
return invoke('page_selected_element', params);
export function pageSelectedElement(params, options) {
return invoke('page_selected_element', params, options);
}
export function tabOpen(params) {
return invoke('tab_open', params);
export function tabOpen(params, options) {
return invoke('tab_open', params, options);
}
export function tabClose(params) {
return invoke('tab_close', params);
export function tabClose(params, options) {
return invoke('tab_close', params, options);
}
export function tabList() {
return invoke('tab_list', {});
export function tabList(options) {
return invoke('tab_list', {}, options);
}
export function pageClick(params) {
return invoke('page_click', params);
export function pageClick(params, options) {
return invoke('page_click', params, options);
}
export function pageType(params) {
return invoke('page_type', params);
export function pageType(params, options) {
return invoke('page_type', params, options);
}
export function pageScroll(params) {
return invoke('page_scroll', params);
export function pageScroll(params, options) {
return invoke('page_scroll', params, options);
}
export function pageHover(params) {
return invoke('page_hover', params);
export function pageHover(params, options) {
return invoke('page_hover', params, options);
}
export function pageEval(params) {
return invoke('page_eval', params);
export function pageEval(params, options) {
return invoke('page_eval', params, options);
}
export function pageSnapshot(params) {
return invoke('page_snapshot', params);
export function pageSnapshot(params, options) {
return invoke('page_snapshot', params, options);
}
export function pagePress(params) {
return invoke('page_press', params);
export function pagePress(params, options) {
return invoke('page_press', params, options);
}
export function pageWait(params) {
return invoke('page_wait', params);
export function pageWait(params, options) {
return invoke('page_wait', params, options);
}
export function pageNavigate(params) {
return invoke('page_navigate', params);
export function pageNavigate(params, options) {
return invoke('page_navigate', params, options);
}
export function pageUpload(params) {
return invoke('page_upload', params);
export function pageUpload(params, options) {
return invoke('page_upload', params, options);
}
export function pageFrames(params) {
return invoke('page_frames', params);
export function pageFrames(params, options) {
return invoke('page_frames', params, options);
}
export function setDeviceEmulation(params) {
return invoke('set_device_emulation', params);
export function setDeviceEmulation(params, options) {
return invoke('set_device_emulation', params, options);
}
export function setTimezone(params) {
return invoke('set_timezone', params);
export function setTimezone(params, options) {
return invoke('set_timezone', params, options);
}
export function getCookie(params) {
return invoke('get_cookie', params);
export function getCookie(params, options) {
return invoke('get_cookie', params, options);
}
export function setCookie(params) {
return invoke('set_cookie', params);
export function setCookie(params, options) {
return invoke('set_cookie', params, options);
}
export function getUserInfo(params = {}) {
return invoke('get_user_info', params);
export function getUserInfo(params = {}, options) {
return invoke('get_user_info', params, options);
}
export function runActions(params) {
return invoke('run_actions', params);
export function runActions(params, options) {
return invoke('run_actions', params, options);
}
{
"name": "@mearl/client",
"version": "2.5.1",
"description": "Client SDK & CLI for Mearl — communicate with Chrome Extension via Unix Socket",
"version": "2.6.0",
"description": "Unified Mearl SDK & CLI for local and remote browsers",
"type": "module",

@@ -13,5 +13,21 @@ "main": "dist/index.js",

},
"./local": {
"types": "./dist/local.d.ts",
"import": "./dist/local.js"
},
"./action-timeouts": {
"types": "./dist/actionTimeouts.d.ts",
"import": "./dist/actionTimeouts.js"
},
"./commands": {
"types": "./dist/commands.d.ts",
"import": "./dist/commands.js"
},
"./check": {
"types": "./dist/check.d.ts",
"import": "./dist/check.js"
},
"./cli": {
"types": "./dist/cli.d.ts",
"import": "./dist/cli.js"
}

@@ -35,4 +51,9 @@ },

},
"dependencies": {
"ws": "^8.18.0",
"@mearl/cloud-types": "2.6.0"
},
"devDependencies": {
"@types/node": "^24.9.1",
"@types/ws": "^8.18.1",
"typescript": "^5.8.3"

@@ -39,0 +60,0 @@ },

# @mearl/client
本地客户端 SDK 与 CLI,通过 **Unix Domain Socket** 与 Chrome 扩展的 native host 通信,调用浏览器调试与操作能力。
统一浏览器 SDK 与 CLI。它会发现当前机器及已配置 cloud-server 下所有 connector 的浏览器,并根据 `browser_list` 返回的全局 `browserId` 自动选择本地 Socket 或云端 WebSocket 链路。

@@ -30,2 +30,6 @@ 提供两种用法:类型安全的具名方法(`getRequests`、`sendRequest` 等),以及灵活的底层 `invoke(action, payload)`。

const data = await client.invoke('send_request', { url: '...' });
// browser_list 默认聚合本机和所有远端 connector
const browsers = await client.browserList();
await client.getLogs({}, { browser: browsers.browsers[0].browserId });
```

@@ -82,2 +86,5 @@

| `--browser <id\|名称>` | 指定目标浏览器 |
| `--connector <id\|名称>` | 限制到一台远端机器 |
| `--local` | 只使用当前机器 |
| `--server <url>` | 覆盖 cloud-server 地址 |

@@ -110,3 +117,3 @@ ## 支持的操作

托管浏览器使用独立 Profile。控制浏览器需先登录 TDBank;生成的 SSO 地址在本地内部传递,本地实例可以使用 `headless: true`(默认)完成测试账号登录。AgentBay 固定为非 headless,传 `headless: true` 会被拒绝;`imageId` 可指定镜像别名或具体镜像 ID,省略时使用已验证的 Linux Browser Use 内网镜像。`persistent: true` 会让本地实例保留 Profile,让 AgentBay 同名实例绑定稳定的云端 Browser Context;正常关闭时同步 Cookie、LocalStorage、IndexedDB 等状态,下次启动无需再次复制本地登录态。`deleteProfile: true` 会显式删除对应 Profile 或 Context。`copyCookieDomains` 可在首次启动或需要刷新登录态时把控制浏览器指定域的 Cookie 复制到新实例,Cookie 值不会经过 cloud-server,也不会出现在命令结果或日志中;`userAgentMode: "desktop"` 让本地实例使用匹配本机 Chrome 版本的桌面 UA,AgentBay 则复用当前控制浏览器的桌面 UA。`browser_list` 通过 `agentbayImageId`、`agentbayContextId` 和 `agentbayContextName` 返回 AgentBay 实例配置;当服务返回无影浏览器串流入口时,还会通过 `agentbayAccessUrl` 暴露,该地址可能包含临时访问凭据,应按敏感信息处理。完整设计见 [托管浏览器与 TDBank 多账号设计](../../docs/managed-browsers.md)。
托管浏览器使用独立 Profile。控制浏览器需先登录 TDBank;生成的 SSO 地址在本地内部传递,本地实例可以使用 `headless: true`(默认)完成测试账号登录。AgentBay 固定为非 headless,传 `headless: true` 会被拒绝;`imageId` 可指定镜像别名或具体镜像 ID,省略时使用已验证的 Linux Browser Use 内网镜像。`persistent: true` 会让本地实例保留 Profile,让 AgentBay 同名实例绑定稳定的云端 Browser Context;正常关闭时同步 Cookie、LocalStorage、IndexedDB 等状态,下次启动无需再次复制本地登录态。`deleteProfile: true` 会显式删除对应 Profile 或 Context。`copyCookieDomains` 可在首次启动或需要刷新登录态时把控制浏览器指定域的 Cookie 复制到新实例,Cookie 值不会经过 cloud-server,也不会出现在命令结果或日志中;`userAgentMode: "desktop"` 让本地实例使用匹配本机 Chrome 版本的桌面 UA,AgentBay 则复用当前控制浏览器的桌面 UA。`browser_list` 仅通过 `agentbayImageId`、`agentbayContextId` 和 `agentbayContextName` 返回 AgentBay 实例的稳定配置。无影浏览器串流入口包含临时访问凭据且会过期,因此不会写入实例记录或列表结果;需要时运行 `mearl check --browser <id|名称>` 实时获取,并按敏感信息处理。完整设计见 [托管浏览器与 TDBank 多账号设计](../../docs/managed-browsers.md)。

@@ -117,9 +124,8 @@ ## 架构

@mearl/client (CLI / SDK)
↓ (Unix Socket)
@mearl/native-host
↓ (Native Messaging)
Chrome Extension / CDP
├─ 本机 → @mearl/native-host → Chrome Extension / CDP
└─ WebSocket → @mearl/cloud-server → @mearl/cloud-connector
→ @mearl/native-host → Chrome Extension / CDP
```
> 云端远程调用场景请使用 [@mearl/cloud-client](../cloud/cloud-client),其操作集与本包完全一致。
基础设施组件必须避免递归路由:`@mearl/cloud-connector` 使用 `@mearl/client/local` 子路径直连所在机器的 native-host。普通 Agent、CLI 与 MCP 集成均使用包根入口。

@@ -126,0 +132,0 @@ ## 构建