@mearl/client 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mearl/client
2
2
 
3
- 本地客户端 SDK 与 CLI,通过 **Unix Domain Socket** 与 Chrome 扩展的 native host 通信,调用浏览器调试与操作能力。
3
+ 统一浏览器 SDK 与 CLI。它会发现当前机器及已配置 cloud-server 下所有 connector 的浏览器,并根据 `browser_list` 返回的全局 `browserId` 自动选择本地 Socket 或云端 WebSocket 链路。
4
4
 
5
5
  提供两种用法:类型安全的具名方法(`getRequests`、`sendRequest` 等),以及灵活的底层 `invoke(action, payload)`。
6
6
 
@@ -27,6 +27,10 @@ const result = await client.sendRequest({
27
27
 
28
28
  // 底层通用方法(灵活扩展)
29
29
  const data = await client.invoke('send_request', { url: '...' });
30
+
31
+ // browser_list 默认聚合本机和所有远端 connector
32
+ const browsers = await client.browserList();
33
+ await client.getLogs({}, { browser: browsers.browsers[0].browserId });
30
34
  ```
31
35
 
32
36
  ## CLI 使用
@@ -62,7 +66,7 @@ mearl <action> --help
62
66
  mearl browser_launch --payload '{"name":"account-a","accountId":12345}'
63
67
  mearl browser_launch --payload '{"name":"account-b","query":"test_account"}'
64
68
  mearl browser_launch --payload '{"name":"cookie-copy","copyCookieDomains":["example.com"]}'
65
- mearl browser_launch --payload '{"name":"cloud-login","provider":"agentbay","userAgentMode":"desktop","copyCookieDomains":["example.com"],"url":"https://example.com/account"}'
69
+ mearl browser_launch --payload '{"name":"cloud-login","provider":"agentbay","persistent":true,"userAgentMode":"desktop","copyCookieDomains":["example.com"],"url":"https://example.com/account"}'
66
70
  mearl browser_launch --payload '{"name":"figma","userAgentMode":"desktop","copyCookieDomains":["figma.com"]}'
67
71
  mearl browser_list
68
72
  mearl browser_release
@@ -78,6 +82,9 @@ mearl browser_release
78
82
  | `--compact` | 紧凑输出 |
79
83
  | `--output <path>` | 将结果写入文件(如截图) |
80
84
  | `--browser <id\|名称>` | 指定目标浏览器 |
85
+ | `--connector <id\|名称>` | 限制到一台远端机器 |
86
+ | `--local` | 只使用当前机器 |
87
+ | `--server <url>` | 覆盖 cloud-server 地址 |
81
88
 
82
89
  ## 支持的操作
83
90
 
@@ -105,19 +112,18 @@ mearl browser_release
105
112
 
106
113
  `browser_release` 仅在用户明确要求结束调试、退出接管或清理会话资源时使用,不作为普通任务收尾。它会释放当前目标浏览器的 debugger/CDP 控制和会话级临时状态(包括设备/时区模拟),保留浏览器及全部标签页;后续浏览器操作可自动重新建立控制。
107
114
 
108
- 托管浏览器使用独立 Profile。控制浏览器需先登录 TDBank;生成的 SSO 地址在本地内部传递,本地实例可以使用 `headless: true`(默认)完成测试账号登录。AgentBay 固定为非 headless,传 `headless: true` 会被拒绝;`imageId` 可指定镜像别名或具体镜像 ID,省略时使用已验证的 Linux Browser Use 内网镜像。`copyCookieDomains` 可把控制浏览器指定域的 Cookie 复制到本地或 AgentBay 新实例,Cookie 值不会经过 cloud-server,也不会出现在命令结果或日志中;`userAgentMode: "desktop"` 让本地实例使用匹配本机 Chrome 版本的桌面 UA,AgentBay 则复用当前控制浏览器的桌面 UA。`browser_list` 通过 `agentbayImageId` 返回实例的实际镜像;当服务返回无影浏览器串流入口时,还会通过 `agentbayAccessUrl` 暴露,该地址可能包含临时访问凭据,应按敏感信息处理。完整设计见 [托管浏览器与 TDBank 多账号设计](../../docs/managed-browsers.md)。
115
+ 托管浏览器使用独立 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)。
109
116
 
110
117
  ## 架构
111
118
 
112
119
  ```
113
120
  @mearl/client (CLI / SDK)
114
- ↓ (Unix Socket)
115
- @mearl/native-host
116
- ↓ (Native Messaging)
117
- Chrome Extension / CDP
121
+ ├─ 本机 → @mearl/native-host → Chrome Extension / CDP
122
+ └─ WebSocket → @mearl/cloud-server → @mearl/cloud-connector
123
+ → @mearl/native-host → Chrome Extension / CDP
118
124
  ```
119
125
 
120
- > 云端远程调用场景请使用 [@mearl/cloud-client](../cloud/cloud-client),其操作集与本包完全一致。
126
+ 基础设施组件必须避免递归路由:`@mearl/cloud-connector` 使用 `@mearl/client/local` 子路径直连所在机器的 native-host。普通 Agent、CLI 与 MCP 集成均使用包根入口。
121
127
 
122
128
  ## 构建
123
129
 
@@ -1,5 +1,7 @@
1
1
  import { PAGE_ACT_ACTIONS } from './generated-browser-action-protocol.js';
2
2
  const SEND_REQUEST_FILE_TRANSFER_TIMEOUT_MS = 120_000;
3
+ // AgentBay context sync can poll for about 12 minutes before session deletion waits up to 5 more.
4
+ const BROWSER_CLOSE_TIMEOUT_SEC = 18 * 60;
3
5
  function boundedNumber(value, fallback, minimum, maximum) {
4
6
  const numeric = Number(value);
5
7
  if (!Number.isFinite(numeric))
@@ -35,7 +37,9 @@ export function resolveActionTimeoutSec(action, data, fallback = 15) {
35
37
  return Math.max(fallback, 40);
36
38
  if (action === 'browser_launch')
37
39
  return Math.max(fallback, 90);
38
- if (action === 'browser_close' || action === 'tdbank_account')
40
+ if (action === 'browser_close')
41
+ return Math.max(fallback, BROWSER_CLOSE_TIMEOUT_SEC);
42
+ if (action === 'tdbank_account')
39
43
  return Math.max(fallback, 20);
40
44
  if (action === 'run_actions') {
41
45
  const actions = Array.isArray(data.actions) ? data.actions : [{}];
package/dist/check.d.ts CHANGED
@@ -1 +1,22 @@
1
- export declare function runCheck(): Promise<void>;
1
+ import { type UnifiedClientOptions, type UnifiedInvokeOptions } from './unified.js';
2
+ interface CheckClient {
3
+ invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
4
+ disconnect(): void;
5
+ }
6
+ export interface UnifiedCheckOptions {
7
+ clientVersion: string;
8
+ serverUrl?: string;
9
+ serverUrlSource?: string;
10
+ timeoutSec?: number;
11
+ browser?: string;
12
+ connector?: string;
13
+ local?: boolean;
14
+ transport?: 'cdp';
15
+ }
16
+ export interface UnifiedCheckDependencies {
17
+ createClient?: (options: UnifiedClientOptions) => CheckClient;
18
+ writeLine?: (line: string) => void;
19
+ }
20
+ export declare function parseCheckTimeout(value: string | undefined): number | null;
21
+ export declare function runUnifiedCheck(options: UnifiedCheckOptions, dependencies?: UnifiedCheckDependencies): Promise<boolean>;
22
+ export {};
package/dist/check.js CHANGED
@@ -1,146 +1,288 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { execSync } from 'node:child_process';
5
- import { invoke, CLIENT_VERSION, SOCKET_PATH } from './socket.js';
6
- import { listLiveBrowsers, describeBrowser } from './browserRegistry.js';
7
- const EXTENSION_INSTALL_URL = 'https://chromewebstore.google.com/detail/mearl/aoehhjnofngknnjefamjbplchbolghkm';
8
- function resolveManifestDir() {
9
- switch (process.platform) {
10
- case 'darwin':
11
- return path.join(os.homedir(), 'Library/Application Support/Google/Chrome/NativeMessagingHosts');
12
- case 'linux':
13
- return path.join(os.homedir(), '.config/google-chrome/NativeMessagingHosts');
14
- case 'win32':
15
- return path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Google\\Chrome\\NativeMessagingHosts');
16
- default:
17
- return '';
18
- }
1
+ import { UnifiedClient } from './unified.js';
2
+ function formatError(error) {
3
+ return error instanceof Error ? error.message : String(error);
19
4
  }
20
- function checkCommandExists(command) {
5
+ function redactServerUrl(serverUrl) {
21
6
  try {
22
- return execSync(`${command} --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
7
+ const parsed = new URL(serverUrl);
8
+ for (const key of parsed.searchParams.keys()) {
9
+ if (key.toLowerCase().includes('token'))
10
+ parsed.searchParams.set(key, 'redacted');
11
+ }
12
+ return parsed.toString();
23
13
  }
24
14
  catch {
25
- return null;
15
+ return serverUrl.replace(/([?&][^=&\s]*token[^=&\s]*=)[^&\s]*/gi, '$1redacted');
26
16
  }
27
17
  }
28
- export async function runCheck() {
29
- const env = [];
30
- // 1. native-host CLI on PATH
31
- const cliVersion = checkCommandExists('mearl-native-host');
32
- env.push(cliVersion
33
- ? { ok: true, label: 'native-host CLI' }
34
- : {
35
- ok: false,
36
- label: 'native-host CLI not found',
37
- fix: ['npm install -g @mearl/native-host'],
38
- });
39
- // 2. Native Messaging manifest
40
- const manifestDir = resolveManifestDir();
41
- const manifestName = 'com.alibaba.mearl.skills.json';
42
- const manifestPath = manifestDir ? path.join(manifestDir, manifestName) : '';
43
- env.push(manifestPath && fs.existsSync(manifestPath)
44
- ? { ok: true, label: 'Native Messaging manifest' }
45
- : {
46
- ok: false,
47
- label: 'Native Messaging manifest not found',
48
- fix: ['mearl-native-host --init'],
49
- });
50
- // 3. Socket file (Windows uses Named Pipes — assume present, probe via invoke)
51
- const isWindows = process.platform === 'win32';
52
- const socketExists = isWindows || fs.existsSync(SOCKET_PATH);
53
- if (isWindows) {
54
- env.push({ ok: true, label: `Socket (Named Pipe)` });
55
- }
56
- else if (socketExists) {
57
- env.push({ ok: true, label: 'Socket file' });
58
- }
59
- else {
60
- env.push({
61
- ok: false,
62
- label: `Socket file not found at ${SOCKET_PATH}`,
63
- fix: [
64
- 'native host is not running',
65
- 'Extension mode: ensure Chrome is running and the extension is enabled',
66
- 'CDP mode: chrome://inspect/#remote-debugging → "Discover network targets"',
67
- ],
68
- });
18
+ function browserMatches(browser, selector) {
19
+ const normalized = selector.trim().toLowerCase();
20
+ return (browser.browserId.toLowerCase() === normalized ||
21
+ browser.originalBrowserId?.toLowerCase() === normalized ||
22
+ browser.name.toLowerCase() === normalized ||
23
+ browser.originalName?.toLowerCase() === normalized);
24
+ }
25
+ function findBrowsers(browsers, selector) {
26
+ const exact = browsers.filter(browser => browserMatches(browser, selector));
27
+ if (exact.length > 0)
28
+ return exact;
29
+ const normalized = selector.trim().toLowerCase();
30
+ return browsers.filter(browser => browser.browserId.toLowerCase().includes(normalized) ||
31
+ browser.originalBrowserId?.toLowerCase().includes(normalized) ||
32
+ browser.name.toLowerCase().includes(normalized) ||
33
+ browser.originalName?.toLowerCase().includes(normalized));
34
+ }
35
+ function shortBrowserId(browserId) {
36
+ const parts = browserId.split(':');
37
+ const raw = decodeURIComponent(parts.at(-1) ?? browserId);
38
+ return raw.length > 12 ? `${raw.slice(0, 8)}…` : raw;
39
+ }
40
+ function formatBrowser(browser, isDefault) {
41
+ const transport = browser.transport === 'cdp'
42
+ ? ' [CDP]'
43
+ : browser.transport === 'extension'
44
+ ? ' [Extension]'
45
+ : '';
46
+ const type = browser.type === 'managed' ? ' [Managed]' : '';
47
+ return ` ${isDefault ? '★' : ' '} ${browser.name} [${shortBrowserId(browser.browserId)}]${type}${transport}`;
48
+ }
49
+ function formatConnector(connector) {
50
+ const id = connector.connectorId.length > 12
51
+ ? `${connector.connectorId.slice(0, 8)}…`
52
+ : connector.connectorId;
53
+ const version = connector.version ? ` [v${connector.version}]` : '';
54
+ return ` ${connector.name} [${id}]${version}`;
55
+ }
56
+ function printSection(writeLine, title, items) {
57
+ writeLine(title);
58
+ for (const item of items) {
59
+ const marker = item.status === 'ok' ? '✅' : item.status === 'warning' ? '⚠️ ' : '❌';
60
+ writeLine(` ${marker} ${item.label}`);
61
+ for (const detail of item.details ?? [])
62
+ writeLine(` ${detail}`);
69
63
  }
70
- // 4. Connection + version fetch
71
- let runtime = null;
72
- let outdatedDaemon = false;
73
- if (socketExists) {
64
+ writeLine('');
65
+ }
66
+ export function parseCheckTimeout(value) {
67
+ const input = value ?? '5';
68
+ if (!/^[1-9]\d*$/.test(input))
69
+ return null;
70
+ const timeoutSec = Number(input);
71
+ return Number.isSafeInteger(timeoutSec) ? timeoutSec : null;
72
+ }
73
+ export async function runUnifiedCheck(options, dependencies = {}) {
74
+ const timeoutSec = options.timeoutSec ?? 5;
75
+ const writeLine = dependencies.writeLine ?? (line => console.error(line));
76
+ const createClient = dependencies.createClient ??
77
+ ((clientOptions) => new UnifiedClient(clientOptions));
78
+ const client = createClient({
79
+ ...(options.serverUrl ? { serverUrl: options.serverUrl } : {}),
80
+ connectTimeoutSec: timeoutSec,
81
+ requestTimeoutSec: timeoutSec,
82
+ });
83
+ const environment = [
84
+ { status: 'ok', label: `Unified client v${options.clientVersion}` },
85
+ ];
86
+ let connectors = [];
87
+ let browserList = null;
88
+ try {
89
+ if (options.local) {
90
+ environment.push({
91
+ status: 'ok',
92
+ label: options.transport === 'cdp'
93
+ ? 'Browser scope: this machine (forced CDP)'
94
+ : 'Browser scope: this machine',
95
+ });
96
+ }
97
+ else if (options.serverUrl) {
98
+ const source = options.serverUrlSource ? ` (${options.serverUrlSource})` : '';
99
+ try {
100
+ const connectorList = (await client.invoke('connector_list', {}, { timeoutSec }));
101
+ connectors = Array.isArray(connectorList?.connectors) ? connectorList.connectors : [];
102
+ environment.push({
103
+ status: 'ok',
104
+ label: `Cloud server${source}`,
105
+ details: [
106
+ redactServerUrl(options.serverUrl),
107
+ `${connectors.length} connector${connectors.length === 1 ? '' : 's'} connected`,
108
+ ],
109
+ });
110
+ }
111
+ catch (error) {
112
+ environment.push({
113
+ status: 'error',
114
+ label: `Cloud server or connector discovery failed${source}`,
115
+ details: [formatError(error), redactServerUrl(options.serverUrl)],
116
+ });
117
+ }
118
+ }
119
+ else {
120
+ environment.push({ status: 'ok', label: 'Local mode (no cloud-server configuration)' });
121
+ }
122
+ const inventoryOptions = {
123
+ timeoutSec,
124
+ ...(options.local ? { local: true } : {}),
125
+ ...(options.transport ? { transport: options.transport } : {}),
126
+ ...(!options.local && options.connector ? { connector: options.connector } : {}),
127
+ };
74
128
  try {
75
- runtime = (await invoke('get_versions', {}, { timeoutSec: 5 }));
76
- env.push({ ok: true, label: 'Connection' });
129
+ if (options.serverUrl &&
130
+ !options.local &&
131
+ environment.some(item => item.status === 'error')) {
132
+ browserList = (await client.invoke('browser_list', {}, {
133
+ timeoutSec,
134
+ local: true,
135
+ }));
136
+ }
137
+ else {
138
+ browserList = (await client.invoke('browser_list', {}, inventoryOptions));
139
+ }
140
+ environment.push({ status: 'ok', label: 'Browser inventory' });
141
+ if (browserList.warnings?.length) {
142
+ environment.push({
143
+ status: 'warning',
144
+ label: 'Some browser sources could not be inspected',
145
+ details: browserList.warnings,
146
+ });
147
+ }
77
148
  }
78
149
  catch (error) {
79
- const msg = error instanceof Error ? error.message : String(error);
80
- if (msg.includes('Unknown action')) {
81
- // Old native-host responded but doesn't know get_versions — connection itself is fine.
82
- outdatedDaemon = true;
83
- env.push({ ok: true, label: 'Connection' });
150
+ environment.push({
151
+ status: 'error',
152
+ label: 'Browser inventory failed',
153
+ details: [formatError(error)],
154
+ });
155
+ }
156
+ const connected = (browserList?.browsers ?? []).filter(browser => browser.status === 'connected');
157
+ let targets = connected;
158
+ if (options.browser) {
159
+ const matches = findBrowsers(connected, options.browser);
160
+ if (matches.length === 0) {
161
+ environment.push({
162
+ status: 'error',
163
+ label: `Browser "${options.browser}" is not connected`,
164
+ details: ['Run `mearl browser_list` and use an exact global browserId.'],
165
+ });
166
+ targets = [];
167
+ }
168
+ else if (matches.length > 1) {
169
+ environment.push({
170
+ status: 'error',
171
+ label: `Browser selector "${options.browser}" is ambiguous`,
172
+ details: matches.map(browser => browser.browserId),
173
+ });
174
+ targets = [];
84
175
  }
85
176
  else {
86
- env.push({ ok: false, label: 'Connection failed', fix: [msg] });
177
+ targets = matches;
87
178
  }
88
179
  }
89
- }
90
- // Build versions list
91
- const versions = [['client', `v${CLIENT_VERSION}`]];
92
- const nhVersion = runtime?.nativeHost ?? cliVersion;
93
- if (nhVersion)
94
- versions.push(['native-host', `v${nhVersion}`]);
95
- if (runtime?.extension) {
96
- versions.push(['extension', `v${runtime.extension}`]);
97
- }
98
- if (runtime?.browser) {
99
- versions.push(['browser', `Chrome ${runtime.browser}`]);
100
- }
101
- // Print Versions section
102
- const nameWidth = Math.max(...versions.map(([n]) => n.length));
103
- console.error('\nVersions');
104
- for (const [name, value] of versions) {
105
- console.error(` ${name.padEnd(nameWidth)} ${value}`);
106
- }
107
- console.error('');
108
- // Print Environment section
109
- console.error('Environment');
110
- let hasError = false;
111
- for (const e of env) {
112
- console.error(` ${e.ok ? '✅' : '❌'} ${e.label}`);
113
- if (!e.ok) {
114
- hasError = true;
115
- for (const fix of e.fix ?? [])
116
- console.error(` ${fix}`);
180
+ if (!options.browser && connected.length === 0 && browserList) {
181
+ environment.push({
182
+ status: 'error',
183
+ label: 'No connected browser',
184
+ details: [
185
+ 'Open Chrome with the Mearl extension, start a managed browser, or connect a browser host.',
186
+ ],
187
+ });
117
188
  }
118
- }
119
- if (outdatedDaemon) {
120
- console.error(' ⚠️ native-host daemon is outdated (missing get_versions). Restart Chrome or re-init: mearl-native-host --init');
121
- }
122
- console.error('');
123
- // Connected browsers (multi-browser addressing)
124
- const browsers = listLiveBrowsers();
125
- if (browsers.length > 0) {
126
- console.error('Connected browsers');
127
- browsers.forEach((b, i) => {
128
- const marker = i === 0 ? '★' : ' ';
129
- const via = b.transport === 'cdp' ? ' [CDP]' : '';
130
- console.error(` ${marker} ${describeBrowser(b)}${via}`);
131
- });
132
- if (browsers.length > 1) {
133
- console.error(' ★ = default target (most-recently focused). Use --browser to pick another.');
189
+ const browserChecks = [];
190
+ await Promise.all(targets.map(async (browser) => {
191
+ try {
192
+ const versions = (await client.invoke('get_versions', {}, {
193
+ timeoutSec,
194
+ browser: browser.browserId,
195
+ ...(options.transport ? { transport: options.transport } : {}),
196
+ }));
197
+ const details = [
198
+ versions.nativeHost ? `native-host v${versions.nativeHost}` : null,
199
+ versions.extension ? `extension v${versions.extension}` : null,
200
+ versions.browser ? `Chrome ${versions.browser}` : null,
201
+ ].filter((value) => value !== null);
202
+ let accessUrlWarning;
203
+ if (browser.provider === 'agentbay' && options.browser) {
204
+ try {
205
+ const access = (await client.invoke('get_agentbay_access_url', {}, {
206
+ timeoutSec,
207
+ browser: browser.browserId,
208
+ }));
209
+ if (!access.agentbayAccessUrl) {
210
+ throw new Error('AgentBay did not return a browser access URL');
211
+ }
212
+ details.push(`AgentBay access URL (temporary, sensitive): ${access.agentbayAccessUrl}`);
213
+ }
214
+ catch (error) {
215
+ accessUrlWarning = formatError(error);
216
+ details.push(`AgentBay access URL unavailable: ${accessUrlWarning}`);
217
+ }
218
+ }
219
+ browserChecks.push({
220
+ browser,
221
+ item: {
222
+ status: accessUrlWarning ? 'warning' : 'ok',
223
+ label: accessUrlWarning
224
+ ? 'Browser command path ready; AgentBay access URL unavailable'
225
+ : 'Browser command path',
226
+ details,
227
+ },
228
+ });
229
+ }
230
+ catch (error) {
231
+ const message = formatError(error);
232
+ browserChecks.push({
233
+ browser,
234
+ item: message.includes('Unknown action')
235
+ ? {
236
+ status: 'warning',
237
+ label: 'Browser is reachable but runtime version details are unavailable',
238
+ details: ['Update @mearl/native-host for complete diagnostics.'],
239
+ }
240
+ : {
241
+ status: 'error',
242
+ label: 'Browser command path failed',
243
+ details: [message],
244
+ },
245
+ });
246
+ }
247
+ }));
248
+ writeLine('');
249
+ printSection(writeLine, 'Environment', environment);
250
+ if (connectors.length > 0) {
251
+ writeLine('Cloud connectors');
252
+ for (const connector of connectors)
253
+ writeLine(formatConnector(connector));
254
+ writeLine('');
134
255
  }
135
- console.error('');
136
- }
137
- // Summary
138
- if (hasError) {
139
- console.error('⚠️ Some checks failed. See above for fix instructions.');
140
- console.error(` Install extension: ${EXTENSION_INSTALL_URL}`);
141
- process.exit(1);
256
+ const listedBrowsers = browserList?.browsers ?? [];
257
+ if (listedBrowsers.length > 0) {
258
+ writeLine('Browsers');
259
+ for (const browser of listedBrowsers) {
260
+ writeLine(formatBrowser(browser, browser.browserId === browserList?.defaultBrowserId));
261
+ const check = browserChecks.find(item => item.browser.browserId === browser.browserId);
262
+ if (check) {
263
+ const marker = check.item.status === 'ok' ? '✅' : check.item.status === 'warning' ? '⚠️ ' : '❌';
264
+ writeLine(` ${marker} ${check.item.label}`);
265
+ for (const detail of check.item.details ?? [])
266
+ writeLine(` ${detail}`);
267
+ }
268
+ else if (browser.status !== 'connected') {
269
+ writeLine(` ⚪ ${browser.status}`);
270
+ }
271
+ }
272
+ writeLine('');
273
+ }
274
+ const hasError = environment.some(item => item.status === 'error') ||
275
+ browserChecks.some(check => check.item.status === 'error');
276
+ if (hasError) {
277
+ writeLine('⚠️ Mearl checks failed. See the browser or source details above.');
278
+ return false;
279
+ }
280
+ writeLine(options.browser
281
+ ? '🎉 Selected browser is ready!'
282
+ : `🎉 All ${targets.length} connected browser${targets.length === 1 ? '' : 's'} are ready!`);
283
+ return true;
142
284
  }
143
- else {
144
- console.error('🎉 All checks passed!');
285
+ finally {
286
+ client.disconnect();
145
287
  }
146
288
  }
package/dist/cli.d.ts CHANGED
@@ -1,14 +1,6 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * mearl CLI — 通过命令行调用 Mearl 浏览器插件能力
4
- *
5
- * 用法:
6
- * mearl <action> [--payload <json> | --payload-file <path>] [--timeout <seconds>] [--compact] [--output <path>]
7
- *
8
- * 示例:
9
- * mearl get_requests --payload '{"count":5}'
10
- * mearl send_request --payload '{"url":"https://api.example.com","method":"GET"}'
11
- * mearl page_screenshot --output ./screenshot.png
12
- * mearl set_mock --payload-file ./mock-data.json
13
- */
14
- export {};
2
+ export interface MearlCliOptions {
3
+ commandName?: string;
4
+ clientVersion?: string;
5
+ }
6
+ export declare function runMearlCli(args: string[], options?: MearlCliOptions): Promise<number>;