@mearl/setup 2.8.0 → 2.8.1

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
@@ -5,6 +5,7 @@ Mearl 环境的一次性安装、更新、卸载与配置工具。无需先全
5
5
  ```bash
6
6
  npx @mearl/setup [install] [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
7
7
  npx @mearl/setup <update|uninstall> [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
8
+ npx @mearl/setup extension [--force] [--yes]
8
9
  ```
9
10
 
10
11
  配置 AgentBay API Key 时只需执行一次下面的命令,然后在隐藏输入提示中粘贴 Key。
@@ -44,6 +45,28 @@ npx @mearl/setup agentbay
44
45
  可用 `--skip-skill` 跳过。
45
46
  - 卸载一个环境但另一个仍依赖全局 mearl skill 时会保留 skill。
46
47
  - `--mcp` 的客户端配置和清理由 `@mearl/mcp-server` 自己的命令完成。
48
+ - 默认的 `install` / `update` 不处理浏览器扩展。`extension` 是独立命令:从
49
+ `https://mearl.alibaba-inc.com/` 识别并下载最新的 `mearl-chrome-v*.zip`,原子解压到
50
+ `~/.mearl/chrome-extension`,再通过浏览器级 CDP 的 `Extensions.loadUnpacked` 完成首次
51
+ 安装或后续重载。固定目录和 manifest 中的 `key` 保证扩展 ID 与浏览器存储保持稳定。
52
+ - 下载前会通过 CDP 检查同 ID 插件的来源。未安装,或来源为固定目录中的 `UNPACKED`
53
+ 插件时可继续;Chrome 商店、企业策略或其他本地目录安装的版本默认保持不变。只有显式
54
+ 传入 `--force` 才会接管并替换这些版本。
55
+
56
+ ## 安装或更新浏览器扩展
57
+
58
+ ```bash
59
+ npx @mearl/setup extension
60
+ ```
61
+
62
+ 如果 Chrome 尚未开放 CDP,交互命令会提示打开
63
+ `chrome://inspect/#remote-debugging`、启用远程调试并允许连接,然后等待安装完成。无 TTY
64
+ 环境使用 `npx @mearl/setup extension --yes`;它只尝试一次。CDP 不可用时不会下载或修改
65
+ 插件目录,打开 CDP 后重新执行同一命令即可。
66
+
67
+ 该命令每次都读取下载页上的最新插件版本,不要求与 setup、CLI 或 Native Host 同版本。
68
+ 已经从固定目录安装过时,新版本会先替换目录,再对同一路径调用
69
+ `Extensions.loadUnpacked`,Chrome 会重载现有 unpacked 扩展。
47
70
 
48
71
  ## 跟随浏览器扩展自动更新
49
72
 
@@ -87,6 +110,9 @@ npx @mearl/setup --local --cloud
87
110
  # 自动更新当前 PATH 中检测到的全部 Mearl 环境
88
111
  npx @mearl/setup update
89
112
 
113
+ # 独立安装或更新到最新浏览器扩展
114
+ npx @mearl/setup extension
115
+
90
116
  # 只卸载本地环境
91
117
  npx @mearl/setup uninstall --local
92
118
 
@@ -0,0 +1,45 @@
1
+ export declare const MEARL_EXTENSION_DOWNLOAD_PAGE = "https://mearl.alibaba-inc.com/";
2
+ export declare const MEARL_EXTENSION_DIRECTORY_NAME = "chrome-extension";
3
+ export declare const MEARL_EXTENSION_ID = "aoehhjnofngknnjefamjbplchbolghkm";
4
+ export interface BrowserExtensionInstallOptions {
5
+ force?: boolean;
6
+ targetDirectory?: string;
7
+ chromeUserDataDirectory?: string;
8
+ waitForCdpMs?: number;
9
+ writeLine?: (line: string) => void;
10
+ }
11
+ export interface BrowserExtensionInstallResult {
12
+ extensionId?: string;
13
+ loaded: boolean;
14
+ path: string;
15
+ version: string;
16
+ archiveUrl: string;
17
+ cdpError?: string;
18
+ }
19
+ export interface BrowserExtensionDependencies {
20
+ extractArchive?: (archivePath: string, destination: string) => Promise<void>;
21
+ fetch?: typeof fetch;
22
+ inspectInstallation?: typeof inspectMearlExtensionViaCdp;
23
+ loadUnpacked?: typeof loadUnpackedViaCdp;
24
+ sleep?: (milliseconds: number) => Promise<void>;
25
+ }
26
+ export interface InstalledBrowserExtension {
27
+ id: string;
28
+ location: string;
29
+ name: string;
30
+ path: string;
31
+ version: string;
32
+ }
33
+ export declare class BrowserExtensionConflictError extends Error {
34
+ readonly installed: InstalledBrowserExtension;
35
+ constructor(installed: InstalledBrowserExtension);
36
+ }
37
+ export declare function resolveChromeUserDataDirectory(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string;
38
+ export declare function resolveMearlExtensionDirectory(home?: string): string;
39
+ export declare function findLatestExtensionVersion(html: string): string | null;
40
+ export declare function extensionArchiveUrl(version: string): string;
41
+ export declare function loadUnpackedViaCdp(extensionDirectory: string, chromeUserDataDirectory?: string): Promise<{
42
+ extensionId: string;
43
+ }>;
44
+ export declare function inspectMearlExtensionViaCdp(chromeUserDataDirectory?: string): Promise<InstalledBrowserExtension | null>;
45
+ export declare function installMearlBrowserExtension(options?: BrowserExtensionInstallOptions, dependencies?: BrowserExtensionDependencies): Promise<BrowserExtensionInstallResult>;
@@ -0,0 +1,406 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import extractZip from 'extract-zip';
6
+ import WebSocket from 'ws';
7
+ import { isMearlReleaseVersion, parseMearlReleaseVersion } from './version.js';
8
+ export const MEARL_EXTENSION_DOWNLOAD_PAGE = 'https://mearl.alibaba-inc.com/';
9
+ export const MEARL_EXTENSION_DIRECTORY_NAME = 'chrome-extension';
10
+ export const MEARL_EXTENSION_ID = 'aoehhjnofngknnjefamjbplchbolghkm';
11
+ const ARCHIVE_PATTERN = /mearl-chrome-v(\d+\.\d+\.\d+(?:-beta\.\d+)?)\.zip/g;
12
+ const MAX_ARCHIVE_BYTES = 64 * 1024 * 1024;
13
+ export class BrowserExtensionConflictError extends Error {
14
+ installed;
15
+ constructor(installed) {
16
+ super(`Mearl extension ${installed.id} is already installed from ${installed.location} at ${installed.path || '(managed location)'}. Re-run with --force to replace it.`);
17
+ this.installed = installed;
18
+ this.name = 'BrowserExtensionConflictError';
19
+ }
20
+ }
21
+ function compareVersions(left, right) {
22
+ const parsedLeft = parseMearlReleaseVersion(left);
23
+ const parsedRight = parseMearlReleaseVersion(right);
24
+ for (const key of ['major', 'minor', 'patch']) {
25
+ if (parsedLeft[key] !== parsedRight[key])
26
+ return parsedLeft[key] - parsedRight[key];
27
+ }
28
+ if (parsedLeft.channel !== parsedRight.channel)
29
+ return parsedLeft.channel === 'stable' ? 1 : -1;
30
+ return (parsedLeft.beta ?? 0) - (parsedRight.beta ?? 0);
31
+ }
32
+ export function resolveChromeUserDataDirectory(platform = process.platform, home = os.homedir(), env = process.env) {
33
+ if (platform === 'darwin') {
34
+ return path.join(home, 'Library', 'Application Support', 'Google', 'Chrome');
35
+ }
36
+ if (platform === 'win32') {
37
+ return path.join(env.LOCALAPPDATA ?? home, 'Google', 'Chrome', 'User Data');
38
+ }
39
+ return path.join(home, '.config', 'google-chrome');
40
+ }
41
+ export function resolveMearlExtensionDirectory(home = os.homedir()) {
42
+ return path.join(home, '.mearl', MEARL_EXTENSION_DIRECTORY_NAME);
43
+ }
44
+ export function findLatestExtensionVersion(html) {
45
+ const versions = Array.from(html.matchAll(ARCHIVE_PATTERN), match => match[1]).filter(isMearlReleaseVersion);
46
+ return versions.sort(compareVersions).at(-1) ?? null;
47
+ }
48
+ export function extensionArchiveUrl(version) {
49
+ if (!isMearlReleaseVersion(version)) {
50
+ throw new Error(`Invalid Mearl browser extension version: ${version}`);
51
+ }
52
+ return new URL(`mearl-chrome-v${version}.zip`, MEARL_EXTENSION_DOWNLOAD_PAGE).toString();
53
+ }
54
+ async function resolveExtensionArchive(fetchImpl) {
55
+ const response = await fetchImpl(MEARL_EXTENSION_DOWNLOAD_PAGE, { redirect: 'follow' });
56
+ if (!response.ok) {
57
+ throw new Error(`Extension release page returned HTTP ${response.status}`);
58
+ }
59
+ const version = findLatestExtensionVersion(await response.text());
60
+ if (!version)
61
+ throw new Error('No Mearl browser extension archive was found on the release page');
62
+ return { version, archiveUrl: extensionArchiveUrl(version) };
63
+ }
64
+ function readDevToolsActivePort(userDataDirectory) {
65
+ try {
66
+ const content = fs
67
+ .readFileSync(path.join(userDataDirectory, 'DevToolsActivePort'), 'utf8')
68
+ .trim();
69
+ const [portValue, browserWsPath = '/devtools/browser'] = content
70
+ .split(/\r?\n/)
71
+ .map(value => value.trim());
72
+ const port = Number(portValue);
73
+ if (!Number.isInteger(port) || port <= 0 || port > 65_535)
74
+ return null;
75
+ if (!browserWsPath.startsWith('/'))
76
+ return null;
77
+ return { port, browserWsPath };
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ function rawDataToString(data) {
84
+ if (Array.isArray(data))
85
+ return Buffer.concat(data).toString('utf8');
86
+ if (Buffer.isBuffer(data))
87
+ return data.toString('utf8');
88
+ return Buffer.from(data).toString('utf8');
89
+ }
90
+ class CdpConnection {
91
+ socket;
92
+ closed = false;
93
+ nextId = 1;
94
+ pending = new Map();
95
+ constructor(socket) {
96
+ this.socket = socket;
97
+ socket.on('message', data => this.handleMessage(data));
98
+ socket.on('close', () => this.close(new Error('CDP WebSocket closed before responding')));
99
+ socket.on('error', error => this.close(error));
100
+ }
101
+ static connect(endpoint) {
102
+ return new Promise((resolve, reject) => {
103
+ const socket = new WebSocket(endpoint, {
104
+ handshakeTimeout: 5_000,
105
+ maxPayload: 8 * 1024 * 1024,
106
+ perMessageDeflate: false,
107
+ });
108
+ let settled = false;
109
+ const finish = (error) => {
110
+ if (settled)
111
+ return;
112
+ settled = true;
113
+ if (error) {
114
+ socket.terminate();
115
+ reject(error);
116
+ }
117
+ else {
118
+ resolve(new CdpConnection(socket));
119
+ }
120
+ };
121
+ socket.once('open', () => finish());
122
+ socket.once('error', error => finish(error));
123
+ socket.once('unexpected-response', (_request, response) => {
124
+ response.resume();
125
+ finish(new Error(`CDP WebSocket returned HTTP ${response.statusCode ?? 0}`));
126
+ });
127
+ });
128
+ }
129
+ send(method, params = {}, sessionId, timeoutMs = 10_000) {
130
+ return new Promise((resolve, reject) => {
131
+ const id = this.nextId++;
132
+ const timer = setTimeout(() => {
133
+ this.pending.delete(id);
134
+ reject(new Error(`CDP command timed out: ${method}`));
135
+ }, timeoutMs);
136
+ this.pending.set(id, { reject, resolve, timer });
137
+ this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }), error => {
138
+ if (!error)
139
+ return;
140
+ const pending = this.pending.get(id);
141
+ if (!pending)
142
+ return;
143
+ clearTimeout(pending.timer);
144
+ this.pending.delete(id);
145
+ pending.reject(error);
146
+ });
147
+ });
148
+ }
149
+ close(error = new Error('CDP connection closed')) {
150
+ if (this.closed)
151
+ return;
152
+ this.closed = true;
153
+ for (const pending of this.pending.values()) {
154
+ clearTimeout(pending.timer);
155
+ pending.reject(error);
156
+ }
157
+ this.pending.clear();
158
+ this.socket.terminate();
159
+ }
160
+ handleMessage(data) {
161
+ try {
162
+ const message = JSON.parse(rawDataToString(data));
163
+ if (message.id === undefined)
164
+ return;
165
+ const pending = this.pending.get(message.id);
166
+ if (!pending)
167
+ return;
168
+ clearTimeout(pending.timer);
169
+ this.pending.delete(message.id);
170
+ if (message.error) {
171
+ pending.reject(new Error(message.error.message ?? 'CDP command failed'));
172
+ }
173
+ else {
174
+ pending.resolve(message.result ?? {});
175
+ }
176
+ }
177
+ catch {
178
+ // Ignore unrelated malformed protocol events and let command timeouts report failure.
179
+ }
180
+ }
181
+ }
182
+ function sameExtensionPath(left, right) {
183
+ const normalizedLeft = path.normalize(path.resolve(left));
184
+ const normalizedRight = path.normalize(path.resolve(right));
185
+ return process.platform === 'win32'
186
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
187
+ : normalizedLeft === normalizedRight;
188
+ }
189
+ function isSetupManagedInstallation(installed, targetDirectory) {
190
+ return installed.location === 'UNPACKED' && sameExtensionPath(installed.path, targetDirectory);
191
+ }
192
+ function browserCdpEndpoint(chromeUserDataDirectory) {
193
+ const activePort = readDevToolsActivePort(chromeUserDataDirectory);
194
+ if (!activePort)
195
+ throw new Error('Chrome remote debugging is not enabled');
196
+ return `ws://127.0.0.1:${activePort.port}${activePort.browserWsPath}`;
197
+ }
198
+ export async function loadUnpackedViaCdp(extensionDirectory, chromeUserDataDirectory = resolveChromeUserDataDirectory()) {
199
+ const connection = await CdpConnection.connect(browserCdpEndpoint(chromeUserDataDirectory));
200
+ try {
201
+ const loaded = await connection.send('Extensions.loadUnpacked', {
202
+ path: path.resolve(extensionDirectory),
203
+ });
204
+ if (loaded.id !== MEARL_EXTENSION_ID) {
205
+ throw new Error(`Extensions.loadUnpacked returned unexpected extension ID: ${String(loaded.id)}`);
206
+ }
207
+ const inventory = await connection.send('Extensions.getExtensions');
208
+ const extension = Array.isArray(inventory.extensions)
209
+ ? inventory.extensions.find(candidate => candidate?.id === loaded.id)
210
+ : null;
211
+ if (!extension?.enabled) {
212
+ throw new Error('Chrome loaded the extension but did not report it as enabled');
213
+ }
214
+ return { extensionId: loaded.id };
215
+ }
216
+ finally {
217
+ connection.close();
218
+ }
219
+ }
220
+ export async function inspectMearlExtensionViaCdp(chromeUserDataDirectory = resolveChromeUserDataDirectory()) {
221
+ const connection = await CdpConnection.connect(browserCdpEndpoint(chromeUserDataDirectory));
222
+ const target = await connection.send('Target.createTarget', {
223
+ background: true,
224
+ url: 'chrome://extensions-internals',
225
+ });
226
+ try {
227
+ const attached = await connection.send('Target.attachToTarget', {
228
+ flatten: true,
229
+ targetId: target.targetId,
230
+ });
231
+ const evaluated = await connection.send('Runtime.evaluate', {
232
+ awaitPromise: true,
233
+ expression: "document.readyState === 'complete' ? document.body.innerText : new Promise(resolve => addEventListener('load', () => resolve(document.body.innerText), {once:true}))",
234
+ returnByValue: true,
235
+ }, attached.sessionId);
236
+ const text = evaluated.result?.value;
237
+ if (typeof text !== 'string') {
238
+ throw new Error('chrome://extensions-internals did not return extension inventory');
239
+ }
240
+ const inventory = JSON.parse(text);
241
+ const installed = inventory.find(extension => extension.id === MEARL_EXTENSION_ID);
242
+ if (!installed)
243
+ return null;
244
+ if (typeof installed.location !== 'string' ||
245
+ typeof installed.name !== 'string' ||
246
+ typeof installed.path !== 'string' ||
247
+ typeof installed.version !== 'string') {
248
+ throw new Error('Chrome returned incomplete Mearl extension metadata');
249
+ }
250
+ return {
251
+ id: MEARL_EXTENSION_ID,
252
+ location: installed.location,
253
+ name: installed.name,
254
+ path: installed.path,
255
+ version: installed.version,
256
+ };
257
+ }
258
+ finally {
259
+ if (typeof target.targetId === 'string') {
260
+ await connection.send('Target.closeTarget', { targetId: target.targetId }).catch(() => { });
261
+ }
262
+ connection.close();
263
+ }
264
+ }
265
+ async function replaceDirectory(stagingDirectory, targetDirectory) {
266
+ const backupDirectory = `${targetDirectory}.backup-${randomUUID()}`;
267
+ const targetExists = fs.existsSync(targetDirectory);
268
+ try {
269
+ if (targetExists)
270
+ await fs.promises.rename(targetDirectory, backupDirectory);
271
+ await fs.promises.rename(stagingDirectory, targetDirectory);
272
+ }
273
+ catch (error) {
274
+ if (!fs.existsSync(targetDirectory) && fs.existsSync(backupDirectory)) {
275
+ await fs.promises.rename(backupDirectory, targetDirectory);
276
+ }
277
+ throw error;
278
+ }
279
+ if (targetExists) {
280
+ await fs.promises.rm(backupDirectory, { force: true, recursive: true });
281
+ }
282
+ }
283
+ function readExtensionManifest(directory) {
284
+ const manifestPath = path.join(directory, 'manifest.json');
285
+ let parsed;
286
+ try {
287
+ parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
288
+ }
289
+ catch (error) {
290
+ throw new Error(`Downloaded extension has no valid manifest.json: ${error instanceof Error ? error.message : error}`, { cause: error });
291
+ }
292
+ if (parsed.manifest_version !== 3 ||
293
+ typeof parsed.name !== 'string' ||
294
+ typeof parsed.key !== 'string' ||
295
+ parsed.key.length === 0) {
296
+ throw new Error('Downloaded archive is not a valid Manifest V3 extension');
297
+ }
298
+ if (!isMearlReleaseVersion(parsed.version)) {
299
+ throw new Error('Downloaded extension manifest has an invalid version');
300
+ }
301
+ return parsed;
302
+ }
303
+ export async function installMearlBrowserExtension(options = {}, dependencies = {}) {
304
+ const fetchImpl = dependencies.fetch ?? fetch;
305
+ const extractArchive = dependencies.extractArchive ??
306
+ ((archivePath, destination) => extractZip(archivePath, { dir: destination }));
307
+ const loadUnpacked = dependencies.loadUnpacked ?? loadUnpackedViaCdp;
308
+ const inspectInstallation = dependencies.inspectInstallation ?? inspectMearlExtensionViaCdp;
309
+ const sleep = dependencies.sleep ??
310
+ (milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)));
311
+ const writeLine = options.writeLine ?? (line => console.error(line));
312
+ const targetDirectory = path.resolve(options.targetDirectory ?? resolveMearlExtensionDirectory());
313
+ const parentDirectory = path.dirname(targetDirectory);
314
+ const stagingDirectory = `${targetDirectory}.staging-${randomUUID()}`;
315
+ const archivePath = path.join(parentDirectory, `.mearl-extension-${randomUUID()}.zip`);
316
+ const release = await resolveExtensionArchive(fetchImpl);
317
+ const chromeUserDataDirectory = options.chromeUserDataDirectory ?? resolveChromeUserDataDirectory();
318
+ const waitForCdpMs = Math.max(0, options.waitForCdpMs ?? 0);
319
+ const deadline = Date.now() + waitForCdpMs;
320
+ let lastFailure;
321
+ let lastNoticeAt = 0;
322
+ do {
323
+ try {
324
+ const installed = await inspectInstallation(chromeUserDataDirectory);
325
+ if (installed && !isSetupManagedInstallation(installed, targetDirectory) && !options.force) {
326
+ throw new BrowserExtensionConflictError(installed);
327
+ }
328
+ lastFailure = undefined;
329
+ break;
330
+ }
331
+ catch (error) {
332
+ if (error instanceof BrowserExtensionConflictError)
333
+ throw error;
334
+ const message = error instanceof Error ? error.message : String(error);
335
+ lastFailure = message;
336
+ if (waitForCdpMs === 0 || /Method not (?:available|found)|wasn't found/i.test(message)) {
337
+ break;
338
+ }
339
+ if (lastNoticeAt === 0) {
340
+ writeLine('Waiting for Chrome remote debugging before checking the installed extension...');
341
+ writeLine('Open chrome://inspect/#remote-debugging, enable remote debugging, and approve the connection.');
342
+ lastNoticeAt = Date.now();
343
+ }
344
+ else if (Date.now() - lastNoticeAt >= 15_000) {
345
+ writeLine('Still waiting for Chrome remote debugging...');
346
+ lastNoticeAt = Date.now();
347
+ }
348
+ await sleep(Math.min(1_000, Math.max(0, deadline - Date.now())));
349
+ }
350
+ } while (Date.now() < deadline);
351
+ if (lastFailure) {
352
+ return {
353
+ loaded: false,
354
+ path: targetDirectory,
355
+ version: release.version,
356
+ archiveUrl: release.archiveUrl,
357
+ cdpError: lastFailure,
358
+ };
359
+ }
360
+ await fs.promises.mkdir(parentDirectory, { recursive: true });
361
+ try {
362
+ const response = await fetchImpl(release.archiveUrl, { redirect: 'follow' });
363
+ if (!response.ok) {
364
+ throw new Error(`Extension archive returned HTTP ${response.status}: ${release.archiveUrl}`);
365
+ }
366
+ const contentLength = Number(response.headers.get('content-length') ?? 0);
367
+ if (contentLength > MAX_ARCHIVE_BYTES) {
368
+ throw new Error(`Extension archive exceeds ${MAX_ARCHIVE_BYTES} bytes`);
369
+ }
370
+ const archive = Buffer.from(await response.arrayBuffer());
371
+ if (archive.length === 0 || archive.length > MAX_ARCHIVE_BYTES) {
372
+ throw new Error('Extension archive is empty or too large');
373
+ }
374
+ await fs.promises.writeFile(archivePath, archive, { mode: 0o600 });
375
+ await fs.promises.mkdir(stagingDirectory, { recursive: true });
376
+ await extractArchive(archivePath, stagingDirectory);
377
+ const manifest = readExtensionManifest(stagingDirectory);
378
+ if (manifest.version !== release.version) {
379
+ throw new Error(`Extension archive version ${String(manifest.version)} does not match ${release.version}`);
380
+ }
381
+ await replaceDirectory(stagingDirectory, targetDirectory);
382
+ }
383
+ finally {
384
+ await fs.promises.rm(archivePath, { force: true });
385
+ await fs.promises.rm(stagingDirectory, { force: true, recursive: true });
386
+ }
387
+ try {
388
+ const loaded = await loadUnpacked(targetDirectory, chromeUserDataDirectory);
389
+ return {
390
+ extensionId: loaded.extensionId,
391
+ loaded: true,
392
+ path: targetDirectory,
393
+ version: release.version,
394
+ archiveUrl: release.archiveUrl,
395
+ };
396
+ }
397
+ catch (error) {
398
+ return {
399
+ loaded: false,
400
+ path: targetDirectory,
401
+ version: release.version,
402
+ archiveUrl: release.archiveUrl,
403
+ cdpError: error instanceof Error ? error.message : String(error),
404
+ };
405
+ }
406
+ }
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin } from './agentBay.js';
3
+ import { installMearlBrowserExtension } from './browserExtension.js';
3
4
  import { runMearlLifecycle, type MearlLifecycleOperation } from './lifecycle.js';
4
5
  import type { MearlProfile } from './profiles.js';
5
6
  export declare const SETUP_VERSION: string;
@@ -7,8 +8,9 @@ interface ParsedArgs {
7
8
  agentBayKeyFile?: string;
8
9
  agentBayReadStdin: boolean;
9
10
  cloudMode?: 'websocket' | 'qoder';
11
+ force?: boolean;
10
12
  help: boolean;
11
- operation?: MearlLifecycleOperation | 'agentbay';
13
+ operation?: MearlLifecycleOperation | 'agentbay' | 'extension';
12
14
  profiles?: MearlProfile[];
13
15
  skipSkill: boolean;
14
16
  targetLine?: string;
@@ -20,6 +22,7 @@ export interface SetupCliDependencies {
20
22
  promptAgentBayKey?: typeof promptAgentBayApiKey;
21
23
  readAgentBayKeyFile?: typeof readAgentBayApiKeyFile;
22
24
  readAgentBayKeyStdin?: typeof readAgentBayApiKeyStdin;
25
+ installBrowserExtension?: typeof installMearlBrowserExtension;
23
26
  runLifecycle?: typeof runMearlLifecycle;
24
27
  }
25
28
  export declare function parseSetupArgs(argv: string[]): ParsedArgs;
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
4
+ import { installMearlBrowserExtension } from './browserExtension.js';
4
5
  import { runMearlLifecycle } from './lifecycle.js';
5
6
  import { isMearlReleaseLine } from './version.js';
6
7
  const require = createRequire(import.meta.url);
@@ -52,6 +53,21 @@ export function parseSetupArgs(argv) {
52
53
  }
53
54
  throw new Error('Usage: npx @mearl/setup agentbay [--stdin | --key-file <path>]. API keys are not accepted as command-line arguments.');
54
55
  }
56
+ if (operation === 'extension') {
57
+ const accepted = new Set(['--force', '--yes', '-y', '--non-interactive']);
58
+ const confirmationFlags = new Set(['--yes', '-y', '--non-interactive']);
59
+ const unknown = argv.slice(1).find(arg => !accepted.has(arg));
60
+ if (unknown)
61
+ throw new Error(`Unknown option: ${unknown}`);
62
+ return {
63
+ agentBayReadStdin: false,
64
+ help: false,
65
+ ...(argv.slice(1).includes('--force') ? { force: true } : {}),
66
+ operation,
67
+ skipSkill: false,
68
+ yes: argv.slice(1).some(arg => confirmationFlags.has(arg)),
69
+ };
70
+ }
55
71
  if (operation !== 'install' && operation !== 'update' && operation !== 'uninstall') {
56
72
  throw new Error(`Unknown operation: ${operation}`);
57
73
  }
@@ -118,12 +134,14 @@ function printHelp(toError = false) {
118
134
  Usage:
119
135
  npx @mearl/setup [install] [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
120
136
  npx @mearl/setup <update|uninstall> [--local] [--cloud|--qoder-cloud] [--mcp] [--yes]
137
+ npx @mearl/setup extension [--force] [--yes]
121
138
  npx @mearl/setup agentbay [--stdin | --key-file <path>]
122
139
 
123
140
  Behavior:
124
141
  install Default command; defaults to --local when no environment is specified
125
142
  update Auto-detects installed environments from the current PATH
126
143
  uninstall Auto-detects installed environments and asks for confirmation
144
+ extension Download the latest browser extension and install or reload it through CDP
127
145
  agentbay Save or update AGENTBAY_KEY without changing other Mearl settings
128
146
 
129
147
  Options:
@@ -137,6 +155,7 @@ Options:
137
155
  --target-line <major.minor.x>
138
156
  Install the latest packages from one release line, such as 2.3.x
139
157
  --skip-skill Keep the independently managed Mearl Skill unchanged
158
+ --force Replace an existing store, managed, or other local Mearl extension
140
159
  --yes, -y Run without prompts and accept detected defaults
141
160
  --non-interactive
142
161
  Alias for --yes, intended for Agents and CI
@@ -151,6 +170,7 @@ Examples:
151
170
  npx @mearl/setup --cloud
152
171
  npx @mearl/setup --local --cloud
153
172
  npx @mearl/setup update
173
+ npx @mearl/setup extension
154
174
  npx @mearl/setup agentbay
155
175
  npx @mearl/setup --qoder-cloud --yes
156
176
  npx @mearl/setup update --local --target-line 2.3.x --skip-skill
@@ -203,6 +223,34 @@ export async function main(argv = process.argv.slice(2), interactiveTerminal = p
203
223
  return 1;
204
224
  }
205
225
  }
226
+ if (parsed.operation === 'extension') {
227
+ if (!interactiveTerminal && !parsed.yes) {
228
+ console.error('Non-interactive input detected. Re-run with --yes or --non-interactive.');
229
+ return 2;
230
+ }
231
+ try {
232
+ const result = await (dependencies.installBrowserExtension ?? installMearlBrowserExtension)({
233
+ force: parsed.force,
234
+ waitForCdpMs: interactiveTerminal && !parsed.yes ? 120_000 : 0,
235
+ writeLine: line => console.error(line),
236
+ });
237
+ if (result.loaded) {
238
+ console.error(`Browser extension v${result.version} installed or reloaded through CDP.`);
239
+ console.error(`Extension ID: ${result.extensionId}`);
240
+ console.error(`Extension directory: ${result.path}`);
241
+ return 0;
242
+ }
243
+ console.error(`Browser extension v${result.version} was not installed.`);
244
+ if (result.cdpError)
245
+ console.error(`CDP installation pending: ${result.cdpError}`);
246
+ console.error('Open chrome://inspect/#remote-debugging, enable remote debugging, approve the connection, then rerun: npx @mearl/setup extension');
247
+ return 1;
248
+ }
249
+ catch (error) {
250
+ console.error(error instanceof Error ? error.message : String(error));
251
+ return 1;
252
+ }
253
+ }
206
254
  if (!interactiveTerminal && !parsed.yes && parsed.operation !== 'update') {
207
255
  console.error('Non-interactive input detected. Re-run with --yes or --non-interactive.');
208
256
  return 2;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
2
2
  export { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
3
+ export { BrowserExtensionConflictError, extensionArchiveUrl, findLatestExtensionVersion, installMearlBrowserExtension, inspectMearlExtensionViaCdp, loadUnpackedViaCdp, MEARL_EXTENSION_DIRECTORY_NAME, MEARL_EXTENSION_DOWNLOAD_PAGE, MEARL_EXTENSION_ID, resolveChromeUserDataDirectory, resolveMearlExtensionDirectory, } from './browserExtension.js';
4
+ export type { BrowserExtensionDependencies, BrowserExtensionInstallOptions, BrowserExtensionInstallResult, InstalledBrowserExtension, } from './browserExtension.js';
3
5
  export type { AgentBayConfigurationOptions, AgentBayConfigurationResult, AgentBaySecretInput, AgentBaySecretOutput, } from './agentBay.js';
4
6
  export type { DetectedGlobalPackage, GlobalPackageCommand, GlobalPackageManager, PackageManagerInstallation, PackageOperation, PackageDetectionOptions, } from './packageManager.js';
5
7
  export { runMearlLifecycle } from './lifecycle.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { buildGlobalPackageCommand, detectGlobalPackage, resolveInvokingPackageManager, resolvePackageManagerInstallation, } from './packageManager.js';
2
2
  export { configureAgentBayApiKey, promptAgentBayApiKey, readAgentBayApiKeyFile, readAgentBayApiKeyStdin, } from './agentBay.js';
3
+ export { BrowserExtensionConflictError, extensionArchiveUrl, findLatestExtensionVersion, installMearlBrowserExtension, inspectMearlExtensionViaCdp, loadUnpackedViaCdp, MEARL_EXTENSION_DIRECTORY_NAME, MEARL_EXTENSION_DOWNLOAD_PAGE, MEARL_EXTENSION_ID, resolveChromeUserDataDirectory, resolveMearlExtensionDirectory, } from './browserExtension.js';
3
4
  export { runMearlLifecycle } from './lifecycle.js';
4
5
  export { MEARL_PROFILES, detectMearlProfiles } from './profiles.js';
5
6
  export { MEARL_NATIVE_HOST_NAMES, resolveNativeHostDirectory, uninstallNativeHostArtifacts, } from './nativeHost.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/setup",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "One-shot environment setup and configuration for Mearl",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -51,11 +51,19 @@
51
51
  "uninstall"
52
52
  ],
53
53
  "license": "ISC",
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
54
57
  "publishConfig": {
55
58
  "registry": "https://registry.npmjs.org"
56
59
  },
60
+ "dependencies": {
61
+ "extract-zip": "^2.0.1",
62
+ "ws": "^8.18.3"
63
+ },
57
64
  "devDependencies": {
58
65
  "@types/node": "^24.9.1",
66
+ "@types/ws": "^8.18.1",
59
67
  "typescript": "^5.8.3"
60
68
  },
61
69
  "scripts": {