@mearl/client
Advanced tools
| /** | ||
| * 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 {}; |
+562
| 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!'); | ||
| } | ||
| } |
+5
-13
| #!/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 { |
+46
-45
| /** | ||
| * @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'>>; |
+83
-83
| /** | ||
| * @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); | ||
| } |
+23
-2
| { | ||
| "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 @@ }, |
+13
-7
| # @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 @@ ## 构建 |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
223827
22.52%26
30%4898
20.43%139
4.51%1
-50%2
Infinity%3
50%16
33.33%+ Added
+ Added
+ Added
+ Added