@mearl/client 2.18.0 → 2.19.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
@@ -127,7 +127,29 @@ Mearl 默认聚合当前机器和已配置 cloud-server 下的浏览器,并通
127
127
  | `--server <url>` | 覆盖自动读取的 cloud-server 地址 |
128
128
 
129
129
  `browser_list --help` 和 `browser_launch --help` 会按场景展示宿主选择参数;连接或传输排障运行
130
- `mearl check --help`。其中 `--json` 仅用于输出机器可读的检查结果。
130
+ `mearl check --help`。`mearl check` 检查连接与浏览器状态;`mearl check --updates` 还会并发查询
131
+ client、native-host、cloud-connector 和扩展的最新发布版本。发布版本查询最长等待约 1.5 秒,
132
+ 无法访问发布源时会提示无法确认更新,不影响浏览器可用性检查。`--json` 输出机器可读的检查结果,
133
+ 与 `--updates` 同用时包含更新状态。发现更新时会提示适用的命令:client/native-host 使用
134
+ `npx @mearl/setup update`,setup 管理的扩展使用 `npx @mearl/setup extension`。
135
+
136
+ 全局安装的 `mearl` CLI 默认在每天首次使用后启动独立后台进程,查询最新发布版本,并比较
137
+ client、native-host、cloud-server 和 MCP server 的已安装版本。发现更新时调用
138
+ `@mearl/setup update --yes --skip-skill` 更新版本落后的运行环境及其共享 client,也支持独立安装的 CLI。
139
+ 当前命令不等待网络或安装;检查和安装结果记录在
140
+ `~/.mearl/cli-auto-update-state.json`。项目依赖和临时 `npx` 副本不会被自动修改。
141
+ 浏览器扩展沿用其独立的更新入口。
142
+ 在 `~/.mearl/config.json` 中设置间隔天数或关闭自动更新:
143
+
144
+ ```json
145
+ { "cliAutoUpdate": { "intervalDays": 7 } }
146
+ ```
147
+
148
+ ```json
149
+ { "cliAutoUpdate": false }
150
+ ```
151
+
152
+ 间隔按本地日历日计算。需要立即检查各组件的发布版本时,仍可运行 `mearl check --updates`。
131
153
 
132
154
  ## Skill 插件
133
155
 
@@ -136,7 +158,7 @@ Mearl 默认聚合当前机器和已配置 cloud-server 下的浏览器,并通
136
158
  ```bash
137
159
  npx @mearl/setup plugin install trip/mearl --skill yuque-doc-fetch --yes
138
160
  npx @mearl/setup plugin install trip/mearl --skill buc-request --yes
139
- npx @mearl/setup plugin yuque /absolute/path/to/yuque-doc-fetch
161
+ npx @mearl/setup plugin /absolute/path/to/yuque-doc-fetch
140
162
  npx @mearl/setup plugin uninstall yuque
141
163
  mearl yuque read "https://aliyuque.antfin.com/group/book/doc"
142
164
  mearl yuque --payload '{"command":"read","input":{"url":"https://aliyuque.antfin.com/group/book/doc"}}'
@@ -151,7 +173,7 @@ Provider 与插件仍可独立安装、更新和卸载,帮助聚合不会启
151
173
  目录默认使用 `scripts/main.mjs` 或 `scripts/main.py`;两者同时存在时使用 JavaScript 入口。JavaScript 入口默认导出 `register(app)`,Python 入口导出 `register(app)` 函数;两种入口都可声明简短的 `name` 和 `description`,并通过 `app.command(signature, handler, description?)`
152
174
  注册动作、`app.invoke(action, payload, options?)` 复用当前浏览器上下文,并用 `app.progress(message)` 报告可选进度。JavaScript 插件还可传入 `{ message, data }`,让工具页通过可 JSON 序列化的 `data` 获取稳定的阶段、对象 ID 等机器信息;CLI 仍只显示 `message`。Python 的 `app.invoke` 是同步调用,handler 可以是普通函数或 `async def`。Python 插件要求 `python3 >= 3.9`,可用 `MEARL_PYTHON` 指定解释器路径;Mearl 不安装 Python 或执行 `pip install`。调用另一个插件时,payload 使用 `{ command, input }` envelope;嵌套调用继承宿主选项并拒绝循环依赖。CLI 默认只输出最终结果,传入
153
175
  `--verbose` 时才把进度写入 stderr。handler 返回文本或 JSON,宿主统一处理 `--output`、连接和错误。写命令在用户调用后直接执行,业务模块负责目标、状态、幂等和结果校验。
154
- 自动注册默认以 skill 安装目录名作为命令名,也可在入口声明 `name` 指定短名称。
176
+ 注册名优先使用显式名称,其次读取入口的 `name` metadata,最后回退到 skill 目录名;常规注册只传路径。
155
177
  setup 会补齐缺失或低于自身版本的 client、native-host;安装参数透传给 Ali Skills,并固定全局共享安装。
156
178
 
157
179
  需要单独检查插件时运行 `mearl <name> --version`。命令会解析注册记录并加载入口,但不连接浏览器;
@@ -176,7 +198,7 @@ const result = await invokePlugin(
176
198
  );
177
199
  ```
178
200
 
179
- MCP Server 通过固定工具 `mearl_plugin` 接受同样的 `plugin`、`command` 和 `input`,不会把插件子命令动态扩展成 MCP 工具。SDK 与 MCP 不接受任意入口路径;调用尚未注册的 `buc` 或 `tdbank` 时,会通过匹配当前 client 版本的 setup 自动安装并重试一次。其他未注册名称返回 `PLUGIN_NOT_REGISTERED`。
201
+ MCP Server 通过固定工具 `plugin_invoke` 接受同样的 `plugin`、`command` 和 `input`,不会把插件子命令动态扩展成 MCP 工具。SDK 与 MCP 不接受任意入口路径;调用尚未注册的 `buc` 或 `tdbank` 时,会通过匹配当前 client 版本的 setup 自动安装并重试一次。其他未注册名称返回 `PLUGIN_NOT_REGISTERED`。
180
202
 
181
203
  注册记录在 `~/.mearl/plugins/<name>.json`,保存入口路径和可选的简短描述;skill 业务代码原地更新直接生效,换路径或更新描述后重新注册。
182
204
  `plugin uninstall <name>` 会对 `~/.agents/skills` 中的全局 Skill 调用 `ali-skills remove --global --yes`,
@@ -223,8 +245,14 @@ export default function register(app) {
223
245
  ```
224
246
 
225
247
  声明 `agent` 后工具页可以把任务交给对话 Agent;`layout` 默认为 `drawer`,宿主会提供顶部入口。
226
- 页面也可在 `setAgentPanelVisible` 或 `runAgentTask` 中为单次交互指定 `drawer` / `workspace`。带固定
248
+ 页面也可在 `setAgentPanelVisible` 或 `runAgentTask` 中为单次交互指定 `drawer` / `workspace` / `inline`;
249
+ Agent 对话会保存在 Codex 会话历史中。带固定
227
250
  18rem 左侧导航的工作台可默认使用 `workspace`,由导航项切换 Agent,并占满导航栏以外的主内容区;详情页仍可按需唤起右侧抽屉。
251
+ `inline` 用于把 Agent 嵌入工具页中的卡片或分区,调用时同时传入作为占位区域的 `anchor` 元素和稳定的
252
+ `key`;宿主会跟随该元素的滚动与尺寸变化同步面板位置。`key` 也可用于抽屉或工作区布局,为不同业务对象
253
+ 隔离会话与输入草稿;`setAgentPanelVisible` 可同时传入绝对路径 `cwd`,让首次创建的会话直接绑定该目录。
254
+ 传入 `context` 可为面板提供不显示为聊天消息的背景上下文;它会在用户首次发送消息、创建会话时作为
255
+ developer instructions 交给 Agent。
228
256
 
229
257
  页面通过 `await window.mearl.ready()` 取得 `{ locale, theme, title, fontSize }`,通过
230
258
  `window.mearl.invoke(command, input?)` 调用 `commands` 白名单中的同一插件命令。页面运行在无扩展
@@ -0,0 +1,33 @@
1
+ export type InstalledMearlEnvironment = {
2
+ profile: 'local' | 'cloud' | 'mcp';
3
+ version: string;
4
+ };
5
+ interface ScheduleOptions {
6
+ intervalDays?: number | null;
7
+ packageRoot?: string | null;
8
+ now?: Date;
9
+ statePath?: string;
10
+ lockPath?: string;
11
+ workerPath?: string;
12
+ launch?: (worker: string, installedVersion: string, packageRoot: string, token: string) => void;
13
+ }
14
+ interface WorkerOptions {
15
+ fetchLatestVersion?: () => Promise<string>;
16
+ getInstalledEnvironments?: () => InstalledMearlEnvironment[];
17
+ update?: (version: string, packageRoot: string, profiles: InstalledMearlEnvironment['profile'][], updateClient: boolean) => Promise<void>;
18
+ now?: Date;
19
+ statePath?: string;
20
+ lockPath?: string;
21
+ }
22
+ export declare function isCliUpdateDue(lastCheckDate: string | undefined, now: Date, intervalDays: number): boolean;
23
+ export declare function isGlobalClientPackageRoot(packageRoot: string, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): boolean;
24
+ /** Queue a detached check after the CLI command has finished. */
25
+ export declare function scheduleCliAutoUpdate(installedVersion: string, options?: ScheduleOptions): void;
26
+ export declare function setupUpdateCommand(version: string, packageRoot: string, profiles: InstalledMearlEnvironment['profile'][], updateClient: boolean): {
27
+ command: string;
28
+ args: string[];
29
+ env: NodeJS.ProcessEnv;
30
+ };
31
+ /** Runs only in the detached worker process. */
32
+ export declare function runCliAutoUpdate(installedVersion: string, packageRoot: string, token: string, options?: WorkerOptions): Promise<void>;
33
+ export {};
@@ -0,0 +1,359 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { delimiter, dirname, join, resolve, win32 } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { isNewerRelease, parseReleaseVersion } from './check.js';
8
+ import { loadCliAutoUpdateIntervalDays } from './unifiedConfig.js';
9
+ const CONFIG_DIR = join(homedir(), '.mearl');
10
+ const STATE_PATH = join(CONFIG_DIR, 'cli-auto-update-state.json');
11
+ const LOCK_PATH = join(CONFIG_DIR, 'cli-auto-update.lock');
12
+ const LOCK_STALE_MS = 10 * 60_000;
13
+ const UPDATE_TIMEOUT_MS = 5 * 60_000;
14
+ const VERSION_URL = 'https://registry.npmjs.org/@mearl%2Fclient/latest';
15
+ function localDate(date) {
16
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
17
+ }
18
+ function dayNumber(value) {
19
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
20
+ if (!match)
21
+ return null;
22
+ const year = Number(match[1]);
23
+ const month = Number(match[2]);
24
+ const day = Number(match[3]);
25
+ const timestamp = Date.UTC(year, month - 1, day);
26
+ const date = new Date(timestamp);
27
+ return date.getUTCFullYear() === year &&
28
+ date.getUTCMonth() + 1 === month &&
29
+ date.getUTCDate() === day
30
+ ? timestamp / 86_400_000
31
+ : null;
32
+ }
33
+ export function isCliUpdateDue(lastCheckDate, now, intervalDays) {
34
+ const previous = typeof lastCheckDate === 'string' ? dayNumber(lastCheckDate) : null;
35
+ const today = dayNumber(localDate(now));
36
+ return (previous === null || today === null || today < previous || today - previous >= intervalDays);
37
+ }
38
+ export function isGlobalClientPackageRoot(packageRoot, platform = process.platform, env = process.env) {
39
+ const resolvedRoot = platform === 'win32' ? win32.resolve(packageRoot) : resolve(packageRoot);
40
+ const normalized = resolvedRoot.replaceAll('\\', '/').toLowerCase();
41
+ if (!normalized.endsWith('/node_modules/@mearl/client'))
42
+ return false;
43
+ const pnpmGlobalRoot = /\/global\/\d+\/(?:\.pnpm\/.*\/)?node_modules\//.exec(normalized);
44
+ if (pnpmGlobalRoot) {
45
+ const pnpmHome = resolvedRoot.slice(0, pnpmGlobalRoot.index);
46
+ return existsSync(join(pnpmHome, platform === 'win32' ? 'mearl.cmd' : 'mearl'));
47
+ }
48
+ if (normalized.includes('/.volta/tools/image/packages/'))
49
+ return true;
50
+ const npmSuffix = '/lib/node_modules/@mearl/client';
51
+ if (platform !== 'win32' && normalized.endsWith(npmSuffix)) {
52
+ const prefix = resolvedRoot.slice(0, -npmSuffix.length);
53
+ try {
54
+ return (realpathSync(join(prefix, 'bin', 'mearl')) ===
55
+ realpathSync(join(packageRoot, 'dist', 'bin.js')));
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ const windowsNpmRoot = platform === 'win32' && env.APPDATA
62
+ ? `${win32.resolve(env.APPDATA).replaceAll('\\', '/').toLowerCase()}/npm/node_modules/@mearl/client`
63
+ : null;
64
+ return (normalized.includes('/.bun/install/global/') ||
65
+ /\/(?:\.config\/yarn|\.yarn|yarn\/data|yarn)\/global\//.test(normalized) ||
66
+ (platform === 'win32' && normalized === windowsNpmRoot));
67
+ }
68
+ function globalSearchPath(pathValue) {
69
+ return pathValue
70
+ ?.split(delimiter)
71
+ .filter(directory => !/(?:^|[\\/])node_modules[\\/]\.bin[\\/]?$/i.test(directory))
72
+ .join(delimiter);
73
+ }
74
+ function currentGlobalClientRoot() {
75
+ let directory = dirname(realpathSync(fileURLToPath(import.meta.url)));
76
+ for (let depth = 0; depth < 12; depth += 1) {
77
+ try {
78
+ const manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
79
+ if (manifest.name === '@mearl/client') {
80
+ return isGlobalClientPackageRoot(directory) ? directory : null;
81
+ }
82
+ }
83
+ catch {
84
+ // Continue toward the package root.
85
+ }
86
+ const parent = dirname(directory);
87
+ if (parent === directory)
88
+ break;
89
+ directory = parent;
90
+ }
91
+ return null;
92
+ }
93
+ function readState(statePath) {
94
+ try {
95
+ const value = JSON.parse(readFileSync(statePath, 'utf8'));
96
+ return value && typeof value === 'object' && !Array.isArray(value)
97
+ ? value
98
+ : {};
99
+ }
100
+ catch {
101
+ return {};
102
+ }
103
+ }
104
+ function writeState(statePath, state) {
105
+ mkdirSync(dirname(statePath), { recursive: true, mode: 0o700 });
106
+ const temporary = `${statePath}.${process.pid}.tmp`;
107
+ try {
108
+ writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
109
+ renameSync(temporary, statePath);
110
+ }
111
+ catch (error) {
112
+ try {
113
+ unlinkSync(temporary);
114
+ }
115
+ catch {
116
+ /* No temporary file to remove. */
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+ function acquireLock(lockPath, token) {
122
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
123
+ for (let attempt = 0; attempt < 2; attempt += 1) {
124
+ try {
125
+ const descriptor = openSync(lockPath, 'wx', 0o600);
126
+ try {
127
+ writeFileSync(descriptor, token);
128
+ }
129
+ finally {
130
+ closeSync(descriptor);
131
+ }
132
+ return true;
133
+ }
134
+ catch (error) {
135
+ if (!(error instanceof Error) || !('code' in error) || error.code !== 'EEXIST')
136
+ throw error;
137
+ try {
138
+ if (Date.now() - statSync(lockPath).mtimeMs <= LOCK_STALE_MS)
139
+ return false;
140
+ unlinkSync(lockPath);
141
+ }
142
+ catch {
143
+ return false;
144
+ }
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+ function releaseLock(lockPath, token) {
150
+ try {
151
+ if (readFileSync(lockPath, 'utf8') === token)
152
+ unlinkSync(lockPath);
153
+ }
154
+ catch {
155
+ // A missing lock does not affect the current command or update result.
156
+ }
157
+ }
158
+ function launchWorker(worker, version, packageRoot, token) {
159
+ const child = spawn(process.execPath, [worker, version, packageRoot, token], {
160
+ detached: true,
161
+ stdio: 'ignore',
162
+ windowsHide: true,
163
+ });
164
+ child.once('error', () => releaseLock(LOCK_PATH, token));
165
+ child.unref();
166
+ }
167
+ /** Queue a detached check after the CLI command has finished. */
168
+ export function scheduleCliAutoUpdate(installedVersion, options = {}) {
169
+ try {
170
+ const intervalDays = options.intervalDays === undefined ? loadCliAutoUpdateIntervalDays() : options.intervalDays;
171
+ if (intervalDays === null)
172
+ return;
173
+ const packageRoot = options.packageRoot === undefined ? currentGlobalClientRoot() : options.packageRoot;
174
+ if (!packageRoot)
175
+ return;
176
+ const now = options.now ?? new Date();
177
+ const statePath = options.statePath ?? STATE_PATH;
178
+ const lockPath = options.lockPath ?? LOCK_PATH;
179
+ if (!isCliUpdateDue(readState(statePath).lastCheckDate, now, intervalDays))
180
+ return;
181
+ const token = randomUUID();
182
+ if (!acquireLock(lockPath, token))
183
+ return;
184
+ let launched = false;
185
+ try {
186
+ if (!isCliUpdateDue(readState(statePath).lastCheckDate, now, intervalDays))
187
+ return;
188
+ const worker = options.workerPath ?? fileURLToPath(new URL('./autoUpdateWorker.js', import.meta.url));
189
+ if (!existsSync(worker))
190
+ return;
191
+ (options.launch ?? launchWorker)(worker, installedVersion, packageRoot, token);
192
+ launched = true;
193
+ }
194
+ finally {
195
+ if (!launched)
196
+ releaseLock(lockPath, token);
197
+ }
198
+ }
199
+ catch {
200
+ // Update scheduling must never change CLI behavior.
201
+ }
202
+ }
203
+ export function setupUpdateCommand(version, packageRoot, profiles, updateClient) {
204
+ if (profiles.length === 0 && !updateClient) {
205
+ throw new Error('An update target is required');
206
+ }
207
+ if (!parseReleaseVersion(version))
208
+ throw new Error('A valid Mearl release is required');
209
+ const [major, minor] = version.split('.');
210
+ const env = { ...process.env };
211
+ delete env.MEARL_AUTO_UPDATE_CLIENT_ROOT;
212
+ delete env.MEARL_AUTO_UPDATE_CLIENT_ONLY;
213
+ delete env.MEARL_AUTO_UPDATE_PROFILES;
214
+ env.MEARL_AUTO_UPDATE_SEARCH_PATH = globalSearchPath(env.PATH);
215
+ if (updateClient || profiles.some(profile => profile === 'local' || profile === 'cloud')) {
216
+ env.MEARL_AUTO_UPDATE_CLIENT_ROOT = packageRoot;
217
+ }
218
+ if (profiles.length === 0)
219
+ env.MEARL_AUTO_UPDATE_CLIENT_ONLY = '1';
220
+ else
221
+ env.MEARL_AUTO_UPDATE_PROFILES = profiles.join(',');
222
+ return {
223
+ command: 'npx',
224
+ args: [
225
+ '--yes',
226
+ `@mearl/setup@${version}`,
227
+ 'update',
228
+ '--yes',
229
+ '--target-line',
230
+ `${major}.${minor}.x`,
231
+ '--skip-skill',
232
+ ],
233
+ env,
234
+ };
235
+ }
236
+ function updateWithSetup(version, packageRoot, profiles, updateClient) {
237
+ const { command, args, env } = setupUpdateCommand(version, packageRoot, profiles, updateClient);
238
+ return new Promise((resolve, reject) => {
239
+ const child = spawn(command, args, {
240
+ env,
241
+ stdio: 'ignore',
242
+ windowsHide: true,
243
+ shell: process.platform === 'win32',
244
+ });
245
+ const timeout = setTimeout(() => child.kill(), UPDATE_TIMEOUT_MS);
246
+ child.once('error', error => {
247
+ clearTimeout(timeout);
248
+ reject(error);
249
+ });
250
+ child.once('close', code => {
251
+ clearTimeout(timeout);
252
+ if (code === 0)
253
+ resolve();
254
+ else
255
+ reject(new Error(`Setup exited with code ${code ?? 'unknown'}`));
256
+ });
257
+ });
258
+ }
259
+ function installedEnvironments() {
260
+ const versions = [];
261
+ const env = { ...process.env, PATH: globalSearchPath(process.env.PATH) };
262
+ const executables = [
263
+ { profile: 'local', executable: 'mearl-native-host' },
264
+ { profile: 'cloud', executable: 'mearl-cloud-server' },
265
+ { profile: 'mcp', executable: 'mearl-mcp-server' },
266
+ ];
267
+ for (const { profile, executable } of executables) {
268
+ try {
269
+ const result = spawnSync(executable, ['--version'], {
270
+ encoding: 'utf8',
271
+ timeout: 5_000,
272
+ maxBuffer: 1_024,
273
+ windowsHide: true,
274
+ shell: process.platform === 'win32',
275
+ env,
276
+ });
277
+ const version = result.stdout?.trim();
278
+ if (result.status === 0 && typeof version === 'string' && parseReleaseVersion(version)) {
279
+ versions.push({ profile, version });
280
+ }
281
+ }
282
+ catch {
283
+ // Other installed environments can still be checked and updated.
284
+ }
285
+ }
286
+ return versions;
287
+ }
288
+ async function fetchLatestClientVersion() {
289
+ const response = await fetch(VERSION_URL, { signal: AbortSignal.timeout(2_000) });
290
+ if (!response.ok)
291
+ throw new Error(`Registry returned HTTP ${response.status}`);
292
+ const payload = (await response.json());
293
+ const version = payload.version;
294
+ if (typeof version !== 'string') {
295
+ throw new Error('Registry returned an invalid release version');
296
+ }
297
+ return version;
298
+ }
299
+ /** Runs only in the detached worker process. */
300
+ export async function runCliAutoUpdate(installedVersion, packageRoot, token, options = {}) {
301
+ const statePath = options.statePath ?? STATE_PATH;
302
+ const lockPath = options.lockPath ?? LOCK_PATH;
303
+ try {
304
+ writeState(statePath, {
305
+ ...readState(statePath),
306
+ lastCheckDate: localDate(options.now ?? new Date()),
307
+ });
308
+ }
309
+ catch {
310
+ releaseLock(lockPath, token);
311
+ return;
312
+ }
313
+ let result;
314
+ try {
315
+ const version = await (options.fetchLatestVersion ?? fetchLatestClientVersion)();
316
+ if (!parseReleaseVersion(version))
317
+ throw new Error('Registry returned an invalid release version');
318
+ const environments = (options.getInstalledEnvironments ?? installedEnvironments)();
319
+ const installedVersions = [
320
+ installedVersion,
321
+ ...environments.map(environment => environment.version),
322
+ ];
323
+ const updateClient = isNewerRelease(version, installedVersion);
324
+ const staleProfiles = environments
325
+ .filter(environment => isNewerRelease(version, environment.version))
326
+ .map(environment => environment.profile);
327
+ const updateAvailable = updateClient || staleProfiles.length > 0;
328
+ const newerInstalled = installedVersions.some(installed => isNewerRelease(installed, version));
329
+ if (updateAvailable && newerInstalled) {
330
+ result = {
331
+ checkedAt: new Date().toISOString(),
332
+ status: 'skipped',
333
+ version,
334
+ error: 'An installed component is newer than the latest published release',
335
+ };
336
+ }
337
+ else if (updateAvailable) {
338
+ await (options.update ?? updateWithSetup)(version, packageRoot, staleProfiles, updateClient);
339
+ result = { checkedAt: new Date().toISOString(), status: 'updated', version };
340
+ }
341
+ else {
342
+ result = { checkedAt: new Date().toISOString(), status: 'current', version };
343
+ }
344
+ }
345
+ catch (error) {
346
+ result = {
347
+ checkedAt: new Date().toISOString(),
348
+ status: 'failed',
349
+ error: error instanceof Error ? error.message : String(error),
350
+ };
351
+ }
352
+ try {
353
+ writeState(statePath, { ...readState(statePath), lastResult: result });
354
+ }
355
+ catch {
356
+ /* The result is best effort. */
357
+ }
358
+ releaseLock(lockPath, token);
359
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { runCliAutoUpdate } from './autoUpdate.js';
2
+ const [installedVersion, packageRoot, token] = process.argv.slice(2);
3
+ if (installedVersion && packageRoot && token) {
4
+ await runCliAutoUpdate(installedVersion, packageRoot, token);
5
+ }
package/dist/bin.js CHANGED
@@ -1,3 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { runMearlCli } from './cli.js';
3
+ import { CLIENT_VERSION } from './socket.js';
4
+ import { scheduleCliAutoUpdate } from './autoUpdate.js';
3
5
  process.exitCode = await runMearlCli(process.argv.slice(2));
6
+ scheduleCliAutoUpdate(CLIENT_VERSION);
package/dist/check.d.ts CHANGED
@@ -67,6 +67,13 @@ export interface UnifiedCheckResult {
67
67
  connectors: CloudConnectorInfo[];
68
68
  environment: CheckItem[];
69
69
  ok: boolean;
70
+ updates?: UnifiedCheckUpdate[];
71
+ }
72
+ export interface UnifiedCheckUpdate {
73
+ component: string;
74
+ installed: string;
75
+ latest?: string;
76
+ status: 'current' | 'available' | 'unavailable';
70
77
  }
71
78
  interface CheckClient {
72
79
  invoke(action: string, data?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
@@ -82,12 +89,16 @@ export interface UnifiedCheckOptions {
82
89
  local?: boolean;
83
90
  transport?: 'cdp';
84
91
  json?: boolean;
92
+ checkUpdates?: boolean;
85
93
  }
86
94
  export interface UnifiedCheckDependencies {
87
95
  createClient?: (options: UnifiedClientOptions) => CheckClient;
88
96
  writeJson?: (json: string) => void;
89
97
  writeLine?: (line: string) => void;
98
+ fetchLatestVersion?: (component: string) => Promise<string>;
90
99
  }
100
+ export declare function parseReleaseVersion(value: unknown): number[] | null;
101
+ export declare function isNewerRelease(latest: string, installed: string): boolean;
91
102
  export declare function parseCheckTimeout(value: string | undefined): number | null;
92
103
  export declare function runUnifiedCheck(options: UnifiedCheckOptions & {
93
104
  json: true;
package/dist/check.js CHANGED
@@ -1,4 +1,45 @@
1
1
  import { UnifiedClient, normalizeBrowserSelector, } from './unified.js';
2
+ const UPDATE_SOURCES = {
3
+ client: 'https://registry.npmjs.org/@mearl%2Fclient/latest',
4
+ 'native-host': 'https://registry.npmjs.org/@mearl%2Fnative-host/latest',
5
+ 'cloud-connector': 'https://registry.npmjs.org/@mearl%2Fcloud-connector/latest',
6
+ extension: 'https://mearl.alibaba-inc.com/config.json',
7
+ };
8
+ export function parseReleaseVersion(value) {
9
+ if (typeof value !== 'string')
10
+ return null;
11
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-beta\.(0|[1-9]\d*))?$/.exec(value);
12
+ if (!match)
13
+ return null;
14
+ const parts = [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4] ?? 0)];
15
+ if (!parts.every(Number.isSafeInteger))
16
+ return null;
17
+ return [...parts.slice(0, 3), match[4] === undefined ? 1 : 0, parts[3]];
18
+ }
19
+ export function isNewerRelease(latest, installed) {
20
+ const latestParts = parseReleaseVersion(latest);
21
+ const installedParts = parseReleaseVersion(installed);
22
+ if (!latestParts || !installedParts)
23
+ return false;
24
+ for (let index = 0; index < latestParts.length; index++) {
25
+ if (latestParts[index] !== installedParts[index]) {
26
+ return latestParts[index] > installedParts[index];
27
+ }
28
+ }
29
+ return false;
30
+ }
31
+ async function fetchLatestVersion(component) {
32
+ const response = await fetch(UPDATE_SOURCES[component], {
33
+ signal: AbortSignal.timeout(1500),
34
+ });
35
+ if (!response.ok)
36
+ throw new Error(`HTTP ${response.status}`);
37
+ const payload = (await response.json());
38
+ const version = component === 'extension' ? payload.latestVersion : payload.version;
39
+ if (!parseReleaseVersion(version))
40
+ throw new Error('Invalid release version');
41
+ return version;
42
+ }
2
43
  function formatError(error) {
3
44
  return error instanceof Error ? error.message : String(error);
4
45
  }
@@ -98,6 +139,9 @@ export async function runUnifiedCheck(options, dependencies = {}) {
98
139
  connectTimeoutSec: timeoutSec,
99
140
  requestTimeoutSec: timeoutSec,
100
141
  });
142
+ const latestVersions = options.checkUpdates
143
+ ? Promise.allSettled(Object.keys(UPDATE_SOURCES).map(component => (dependencies.fetchLatestVersion ?? fetchLatestVersion)(component)))
144
+ : null;
101
145
  const environment = [
102
146
  { status: 'ok', label: `Unified client v${options.clientVersion}` },
103
147
  ];
@@ -301,6 +345,67 @@ export async function runUnifiedCheck(options, dependencies = {}) {
301
345
  });
302
346
  }
303
347
  }));
348
+ let updates;
349
+ if (latestVersions) {
350
+ const latestResults = await latestVersions;
351
+ const components = Object.keys(UPDATE_SOURCES);
352
+ const installed = new Map();
353
+ const addInstalled = (component, version) => {
354
+ if (!version)
355
+ return;
356
+ const versions = installed.get(component) ?? new Set();
357
+ versions.add(version);
358
+ installed.set(component, versions);
359
+ };
360
+ addInstalled('client', options.clientVersion);
361
+ for (const check of browserChecks) {
362
+ addInstalled('native-host', check.versions?.nativeHost);
363
+ addInstalled('extension', check.versions?.extension);
364
+ }
365
+ for (const connector of connectors)
366
+ addInstalled('cloud-connector', connector.version);
367
+ updates = components.flatMap((component, index) => {
368
+ const result = latestResults[index];
369
+ return [...(installed.get(component) ?? [])].map(version => ({
370
+ component,
371
+ installed: version,
372
+ ...(result.status === 'fulfilled' ? { latest: result.value } : {}),
373
+ status: result.status === 'rejected' ||
374
+ !parseReleaseVersion(version) ||
375
+ !parseReleaseVersion(result.value)
376
+ ? 'unavailable'
377
+ : isNewerRelease(result.value, version)
378
+ ? 'available'
379
+ : 'current',
380
+ }));
381
+ });
382
+ const available = updates.filter(update => update.status === 'available');
383
+ const unavailable = updates.filter(update => update.status === 'unavailable');
384
+ const packageUpdateAvailable = available.some(update => update.component === 'client' || update.component === 'native-host');
385
+ const extensionUpdateAvailable = available.some(update => update.component === 'extension');
386
+ const connectorUpdateAvailable = available.some(update => update.component === 'cloud-connector');
387
+ environment.push({
388
+ status: available.length || unavailable.length ? 'warning' : 'ok',
389
+ label: available.length
390
+ ? `${available.length} Mearl update${available.length === 1 ? '' : 's'} available`
391
+ : unavailable.length
392
+ ? 'Mearl update check incomplete'
393
+ : 'Mearl packages are current',
394
+ details: [
395
+ ...available.map(update => `${update.component}: ${update.installed} → ${update.latest}`),
396
+ ...(packageUpdateAvailable
397
+ ? ['Update on the affected machine: npx @mearl/setup update']
398
+ : []),
399
+ ...(extensionUpdateAvailable
400
+ ? ['Update a setup-managed extension: npx @mearl/setup extension']
401
+ : []),
402
+ ...(connectorUpdateAvailable
403
+ ? ['Update cloud-connector with its package manager on the connector machine.']
404
+ : []),
405
+ ...unavailable.map(update => `${update.component}: latest version unavailable`),
406
+ ],
407
+ });
408
+ }
304
409
  const hasError = environment.some(item => item.status === 'error') ||
305
410
  browserChecks.some(check => check.item.status === 'error');
306
411
  const listedBrowsers = browserList?.browsers ?? [];
@@ -336,6 +441,7 @@ export async function runUnifiedCheck(options, dependencies = {}) {
336
441
  connectors,
337
442
  environment,
338
443
  ok: !hasError,
444
+ ...(updates ? { updates } : {}),
339
445
  };
340
446
  writeJson(JSON.stringify(result, null, 2));
341
447
  return result;
package/dist/cli.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface ParsedArgs {
10
10
  forceCdp: boolean;
11
11
  json: boolean;
12
12
  local: boolean;
13
+ updates: boolean;
13
14
  outputPath?: string;
14
15
  payload: Record<string, any>;
15
16
  serverUrl?: string;
package/dist/cli.js CHANGED
@@ -77,6 +77,7 @@ const PLUGIN_HOST_OPTIONS = new Set([
77
77
  '--help',
78
78
  '--json',
79
79
  '--local',
80
+ '--updates',
80
81
  '--output',
81
82
  '--payload',
82
83
  '--payload-file',
@@ -266,6 +267,7 @@ function printCheckHelp(commandName) {
266
267
  ' --server <url> 覆盖 cloud-server WebSocket URL',
267
268
  ' --timeout <seconds> 单项检查超时时间,默认 5 秒',
268
269
  ' --json 输出机器可读 JSON',
270
+ ' --updates 查询已安装组件的发布版本',
269
271
  ' -h, --help 查看帮助',
270
272
  ].join('\n'));
271
273
  }
@@ -409,6 +411,7 @@ export function parseClientArgs(args, plugin = false) {
409
411
  .option('--browser <id-or-name>')
410
412
  .option('--connector <id-or-name>')
411
413
  .option('--local')
414
+ .option('--updates')
412
415
  .option('--cdp')
413
416
  .option('--server <url>');
414
417
  if (plugin)
@@ -442,6 +445,8 @@ export function parseClientArgs(args, plugin = false) {
442
445
  const json = options.json === true;
443
446
  if (json && action !== 'check')
444
447
  throw new Error('--json 仅适用于 check');
448
+ if (options.updates && action !== 'check')
449
+ throw new Error('--updates 仅适用于 check');
445
450
  const parsedPayload = parsePayload(options.payload, options.payloadFile);
446
451
  const { payload, outputPath } = normalizeScreenshotOutput(action, parsedPayload, options.output);
447
452
  const fallbackTimeout = resolveActionTimeoutSec(action, payload, 60);
@@ -459,6 +464,7 @@ export function parseClientArgs(args, plugin = false) {
459
464
  forceCdp,
460
465
  json,
461
466
  local,
467
+ updates: options.updates === true,
462
468
  ...(outputPath ? { outputPath } : {}),
463
469
  ...(options.browser ? { browser: options.browser } : {}),
464
470
  ...(connector ? { connector } : {}),
@@ -645,6 +651,7 @@ export async function runMearlCli(args, options = {}) {
645
651
  ...(parsed.connector ? { connector: parsed.connector } : {}),
646
652
  ...(parsed.local ? { local: true } : {}),
647
653
  ...(parsed.forceCdp ? { transport: 'cdp' } : {}),
654
+ ...(parsed.updates ? { checkUpdates: true } : {}),
648
655
  };
649
656
  if (parsed.json) {
650
657
  const result = await runUnifiedCheck({ ...checkOptions, json: true });
@@ -348,7 +348,7 @@ export interface PageScreenshotParams {
348
348
  format?: 'png' | 'jpeg';
349
349
  quality?: number;
350
350
  }
351
- export declare const BROWSER_DEVICE_PRESET_NAMES: readonly ["iphone-17-pro", "iphone-17e", "pixel-11", "ipad-mini-a17-pro", "galaxy-s26", "pura-x-max", "iphone-15-pro", "iphone-se", "pixel-8", "ipad-mini", "galaxy-s23"];
351
+ export declare const BROWSER_DEVICE_PRESET_NAMES: readonly ["iphone-duo-folded", "iphone-duo-unfolded", "iphone-17-pro", "iphone-17e", "pixel-11", "ipad-mini-a17-pro", "galaxy-s26", "pura-x-max", "iphone-15-pro", "iphone-se", "pixel-8", "ipad-mini", "galaxy-s23"];
352
352
  export type BrowserDevicePresetName = (typeof BROWSER_DEVICE_PRESET_NAMES)[number];
353
353
  export interface BrowserEmulationOptions {
354
354
  preset?: BrowserDevicePresetName;
@@ -365,7 +365,7 @@ export interface BrowserEmulationOptions {
365
365
  /** App 容器横屏时将根页面与业务页面分置左右 WebView;仅与 deviceShell、App 档案同时生效。 */
366
366
  splitScreen?: boolean;
367
367
  }
368
- export type AppBridgeConfigBehavior = 'navigate' | 'closeAll' | 'closeBackData' | 'postMessage' | 'notifySubscribe' | 'notifyUnsubscribe' | 'notifyPost' | 'sharedFileRead' | 'sharedFileWrite' | 'sharedDataGet' | 'sharedDataSet' | 'legacyBroadcastCreate' | 'legacyBroadcastPost' | 'legacyBroadcastClose' | 'memoryGet' | 'memorySet' | 'memoryDelete' | 'availabilityMap' | 'userInfo' | 'networkInfo' | 'geolocation' | 'permission' | 'imagePicker' | 'prefetch' | 'fptTrack' | 'mtopStream' | 'previewFile' | 'toast' | 'unsupportedNativeInput' | 'payment' | 'userTrack' | 'calendar' | 'networkQualityMonitor' | 'beaconRequest' | 'voiceRecognition';
368
+ export type AppBridgeConfigBehavior = 'navigate' | 'closeAll' | 'closeBackData' | 'postMessage' | 'notifySubscribe' | 'notifyUnsubscribe' | 'notifyPost' | 'sharedFileRead' | 'sharedFileWrite' | 'sharedDataGet' | 'sharedDataSet' | 'legacyBroadcastCreate' | 'legacyBroadcastPost' | 'legacyBroadcastClose' | 'memoryGet' | 'memorySet' | 'memoryDelete' | 'availabilityMap' | 'userInfo' | 'authCode' | 'networkInfo' | 'geolocation' | 'permission' | 'imagePicker' | 'prefetch' | 'fptTrack' | 'mtopStream' | 'previewFile' | 'toast' | 'unsupportedNativeInput' | 'payment' | 'userTrack' | 'calendar' | 'networkQualityMonitor' | 'beaconRequest' | 'voiceRecognition';
369
369
  /** 单个桥方法的声明式覆盖;行为、固定回包、成功空实现三选一。 */
370
370
  export interface AppBridgeMethodConfig {
371
371
  behavior?: AppBridgeConfigBehavior;
@@ -421,6 +421,8 @@ export interface SetAppProfileParams {
421
421
  profile?: AppProfileName;
422
422
  os?: 'ios' | 'android';
423
423
  appVersion?: string;
424
+ /** 支付宝容器语言;其他容器忽略。 */
425
+ language?: 'zh-CN' | 'zh-TW' | 'zh-HK' | 'en-US';
424
426
  deviceModel?: string;
425
427
  /** 按方法覆盖档案内置桥配置,仅支持受控行为、固定 JSON 回包和成功空实现 */
426
428
  bridgeConfig?: AppBridgeConfig;
@@ -7,6 +7,8 @@ export const SEND_REQUEST_MAX_TOTAL_FILE_BYTES = 40 * 1024 * 1024;
7
7
  // the 10 MiB cloud message limit.
8
8
  export const SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES = 7 * 1024 * 1024;
9
9
  export const BROWSER_DEVICE_PRESET_NAMES = [
10
+ 'iphone-duo-folded',
11
+ 'iphone-duo-unfolded',
10
12
  'iphone-17-pro',
11
13
  'iphone-17e',
12
14
  'pixel-11',
@@ -84,6 +84,32 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
84
84
  await requestHost('clipboard-write', { text });
85
85
  };
86
86
 
87
+ const openExternalLink = async url => {
88
+ if (typeof url !== 'string' || !url || url.length > 8192) {
89
+ throw new Error('External link must be a non-empty URL no longer than 8192 characters');
90
+ }
91
+ await requestHost('open-external-link', { url });
92
+ };
93
+
94
+ const handleExternalLinkClick = event => {
95
+ if (event.defaultPrevented || event.button !== 0) return;
96
+ const link = event.target?.closest?.('a[href]');
97
+ if (!link || String(link.target).toLowerCase() !== '_blank' || link.hasAttribute?.('download')) {
98
+ return;
99
+ }
100
+ let url;
101
+ try {
102
+ url = new URL(link.href);
103
+ } catch {
104
+ return;
105
+ }
106
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return;
107
+ event.preventDefault();
108
+ void openExternalLink(url.href).catch(error => {
109
+ console.error('Failed to open external link', error);
110
+ });
111
+ };
112
+
87
113
  const toast = async (message, options = {}) => {
88
114
  if (typeof message !== 'string' || !message.trim() || message.length > 4000) {
89
115
  throw new Error('Toast message must be a non-empty string no longer than 4000 characters');
@@ -106,16 +132,213 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
106
132
  throw new Error('Agent panel options must be an object');
107
133
  }
108
134
  const layout = options.layout;
109
- if (layout !== undefined && layout !== 'drawer' && layout !== 'workspace') {
110
- throw new Error('Agent panel layout must be drawer or workspace');
135
+ if (layout !== undefined && layout !== 'drawer' && layout !== 'workspace' && layout !== 'inline') {
136
+ throw new Error('Agent panel layout must be drawer, workspace, or inline');
111
137
  }
112
138
  return layout;
113
139
  };
114
140
 
141
+ const agentPanelKey = (options, required = false) => {
142
+ const key = typeof options.key === 'string' ? options.key.trim() : '';
143
+ if ((required || options.key !== undefined) && (!key || key.length > 128)) {
144
+ throw new Error('Agent panel key must be a non-empty string no longer than 128 characters');
145
+ }
146
+ return key || null;
147
+ };
148
+
149
+ const agentPanelCwd = options => {
150
+ const cwd = options.cwd;
151
+ if (
152
+ cwd !== undefined &&
153
+ (typeof cwd !== 'string' ||
154
+ !cwd.trim() ||
155
+ cwd.length > 4096 ||
156
+ !/^(?:\/|[a-zA-Z]:[\\/])/.test(cwd.trim()))
157
+ ) {
158
+ throw new Error('Agent panel cwd must be an absolute path no longer than 4096 characters');
159
+ }
160
+ return cwd === undefined ? null : cwd.trim();
161
+ };
162
+
163
+ const agentPanelContext = options => {
164
+ const context = options.context;
165
+ if (
166
+ context !== undefined &&
167
+ (typeof context !== 'string' || !context.trim() || context.length > 65536)
168
+ ) {
169
+ throw new Error('Agent panel context must be a non-empty string no longer than 65536 characters');
170
+ }
171
+ return context === undefined ? null : context.trim();
172
+ };
173
+
174
+ const inlineAgentPanels = new Map();
175
+
176
+ const stopInlineAgentPanelTracking = key => {
177
+ if (key) {
178
+ inlineAgentPanels.get(key)?.cleanup();
179
+ inlineAgentPanels.delete(key);
180
+ return;
181
+ }
182
+ for (const tracked of inlineAgentPanels.values()) tracked.cleanup();
183
+ inlineAgentPanels.clear();
184
+ };
185
+
186
+ const inlineAgentPanelAnchor = (options, key) => {
187
+ const anchor = options.anchor;
188
+ if (!(anchor instanceof Element)) {
189
+ throw new Error('Inline Agent panel anchor must be an element');
190
+ }
191
+ const rect = anchor.getBoundingClientRect();
192
+ if (![rect.top, rect.left, rect.width, rect.height].every(Number.isFinite)) {
193
+ throw new Error('Inline Agent panel anchor returned invalid bounds');
194
+ }
195
+ return {
196
+ element: anchor,
197
+ key,
198
+ bounds: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
199
+ };
200
+ };
201
+
202
+ const trackInlineAgentPanel = presentation => {
203
+ stopInlineAgentPanelTracking(presentation.key);
204
+ let frame = null;
205
+ const update = () => {
206
+ frame = null;
207
+ if (!presentation.element.isConnected || !port) return;
208
+ const rect = presentation.element.getBoundingClientRect();
209
+ if (![rect.top, rect.left, rect.width, rect.height].every(Number.isFinite)) return;
210
+ const bounds = { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
211
+ if (
212
+ bounds.top === presentation.bounds.top &&
213
+ bounds.left === presentation.bounds.left &&
214
+ bounds.width === presentation.bounds.width &&
215
+ bounds.height === presentation.bounds.height
216
+ ) return;
217
+ presentation.bounds = bounds;
218
+ port.postMessage({
219
+ type: 'agent-panel-position',
220
+ key: presentation.key,
221
+ bounds,
222
+ });
223
+ };
224
+ const schedule = () => {
225
+ if (frame !== null) return;
226
+ frame = requestAnimationFrame(update);
227
+ };
228
+ const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(schedule) : null;
229
+ observer?.observe(presentation.element);
230
+ observer?.observe(document.documentElement);
231
+ const mutationObserver = typeof MutationObserver === 'function'
232
+ ? new MutationObserver(schedule)
233
+ : null;
234
+ mutationObserver?.observe(document.documentElement, {
235
+ attributes: true,
236
+ childList: true,
237
+ characterData: true,
238
+ subtree: true,
239
+ });
240
+ window.addEventListener('resize', schedule);
241
+ document.addEventListener('scroll', schedule, true);
242
+ const cleanup = () => {
243
+ if (frame !== null) cancelAnimationFrame(frame);
244
+ observer?.disconnect();
245
+ mutationObserver?.disconnect();
246
+ window.removeEventListener('resize', schedule);
247
+ document.removeEventListener('scroll', schedule, true);
248
+ };
249
+ inlineAgentPanels.set(presentation.key, { presentation, cleanup });
250
+ };
251
+
252
+ const scrollInlineAgentPage = (presentation, deltaX, deltaY) => {
253
+ const scrollAxis = (axis, delta) => {
254
+ if (!delta) return;
255
+ let element = presentation.element;
256
+ while (element) {
257
+ const style = window.getComputedStyle(element);
258
+ const overflow = axis === 'x' ? style.overflowX : style.overflowY;
259
+ const scrollSize = axis === 'x' ? element.scrollWidth : element.scrollHeight;
260
+ const clientSize = axis === 'x' ? element.clientWidth : element.clientHeight;
261
+ if (/^(auto|scroll|overlay)$/.test(overflow) && scrollSize > clientSize) {
262
+ const scrollPosition = axis === 'x' ? element.scrollLeft : element.scrollTop;
263
+ const maximum = scrollSize - clientSize;
264
+ if ((delta < 0 && scrollPosition > 0) || (delta > 0 && scrollPosition < maximum - 1)) {
265
+ if (axis === 'x') element.scrollLeft += delta;
266
+ else element.scrollTop += delta;
267
+ return;
268
+ }
269
+ }
270
+ element = element.parentElement;
271
+ }
272
+ window.scrollBy(axis === 'x' ? delta : 0, axis === 'y' ? delta : 0);
273
+ };
274
+ scrollAxis('x', deltaX);
275
+ scrollAxis('y', deltaY);
276
+ };
277
+
278
+ const agentPanelPresentation = (options, layout, key) => {
279
+ if (layout !== 'inline') return null;
280
+ return inlineAgentPanelAnchor(options, key);
281
+ };
282
+
115
283
  const setAgentPanelVisible = async (visible, options = {}) => {
116
284
  if (typeof visible !== 'boolean') throw new Error('Agent panel visibility must be a boolean');
117
285
  const layout = agentPanelLayout(options);
118
- await requestHost('agent-panel', { visible, ...(layout ? { layout } : {}) });
286
+ const key = agentPanelKey(options, visible && layout === 'inline');
287
+ const cwd = visible ? agentPanelCwd(options) : null;
288
+ const context = visible ? agentPanelContext(options) : null;
289
+ const presentation = visible ? agentPanelPresentation(options, layout, key) : null;
290
+ const request = requestHost('agent-panel', {
291
+ visible,
292
+ ...(layout ? { layout } : {}),
293
+ ...(key ? { key } : {}),
294
+ ...(cwd ? { cwd } : {}),
295
+ ...(context ? { context } : {}),
296
+ ...(presentation ? { bounds: presentation.bounds } : {}),
297
+ });
298
+ if (presentation) {
299
+ await ready;
300
+ trackInlineAgentPanel(presentation);
301
+ } else if (!visible && key) {
302
+ stopInlineAgentPanelTracking(key);
303
+ } else {
304
+ stopInlineAgentPanelTracking();
305
+ }
306
+ try {
307
+ await request;
308
+ } catch (error) {
309
+ if (presentation) stopInlineAgentPanelTracking(presentation.key);
310
+ throw error;
311
+ }
312
+ };
313
+
314
+ const agentGuideText = (value, label, maximumLength) => {
315
+ const normalized = typeof value === 'string' ? value.trim() : '';
316
+ if (!normalized || normalized.length > maximumLength) {
317
+ throw new Error(label + ' must be a non-empty string no longer than ' + maximumLength + ' characters');
318
+ }
319
+ return normalized;
320
+ };
321
+
322
+ const setAgentGuides = async guides => {
323
+ if (!Array.isArray(guides) || guides.length === 0 || guides.length > 6) {
324
+ throw new Error('Agent guides must contain between 1 and 6 items');
325
+ }
326
+ const normalized = guides.map((guide, index) => {
327
+ if (!guide || typeof guide !== 'object' || Array.isArray(guide)) {
328
+ throw new Error('Agent guide ' + index + ' must be an object');
329
+ }
330
+ const icon = guide.icon === undefined ? undefined : agentGuideText(guide.icon, 'Agent guide icon', 40);
331
+ if (icon && !/^[a-z][a-z0-9-]*$/.test(icon)) {
332
+ throw new Error('Agent guide icon must use kebab-case');
333
+ }
334
+ return {
335
+ title: agentGuideText(guide.title, 'Agent guide title', 80),
336
+ description: agentGuideText(guide.description, 'Agent guide description', 160),
337
+ prompt: agentGuideText(guide.prompt, 'Agent guide prompt', 4000),
338
+ ...(icon ? { icon } : {}),
339
+ };
340
+ });
341
+ await requestHost('agent-guides', { guides: normalized });
119
342
  };
120
343
 
121
344
  const runAgentTask = async (prompt, options = {}) => {
@@ -126,7 +349,13 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
126
349
  throw new Error('Agent task options must be an object');
127
350
  }
128
351
  const cwd = options.cwd;
352
+ const ephemeral = options.ephemeral;
129
353
  const layout = agentPanelLayout(options);
354
+ const key = agentPanelKey(options, layout === 'inline');
355
+ const presentation = agentPanelPresentation(options, layout, key);
356
+ if (ephemeral !== undefined && ephemeral !== false) {
357
+ throw new Error('Temporary Agent conversations are not supported');
358
+ }
130
359
  if (
131
360
  cwd !== undefined &&
132
361
  (typeof cwd !== 'string' ||
@@ -136,11 +365,25 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
136
365
  ) {
137
366
  throw new Error('Agent task cwd must be an absolute path no longer than 4096 characters');
138
367
  }
139
- await requestHost('agent-task', {
368
+ const request = requestHost('agent-task', {
140
369
  prompt: prompt.trim(),
141
370
  ...(cwd !== undefined ? { cwd: cwd.trim() } : {}),
142
371
  ...(layout ? { layout } : {}),
372
+ ...(key ? { key } : {}),
373
+ ...(presentation ? { bounds: presentation.bounds } : {}),
143
374
  });
375
+ if (presentation) {
376
+ await ready;
377
+ trackInlineAgentPanel(presentation);
378
+ } else {
379
+ stopInlineAgentPanelTracking();
380
+ }
381
+ try {
382
+ await request;
383
+ } catch (error) {
384
+ if (presentation) stopInlineAgentPanelTracking(presentation.key);
385
+ throw error;
386
+ }
144
387
  };
145
388
 
146
389
  const setQueryParam = async (name, value) => {
@@ -155,7 +398,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
155
398
  return query;
156
399
  };
157
400
 
158
- const api = { copyText, invoke, operation, ready: () => ready, runAgentTask, runningOperations, selectDirectory, setAgentPanelVisible, setQueryParam, start, toast };
401
+ const api = { copyText, invoke, operation, ready: () => ready, runAgentTask, runningOperations, selectDirectory, setAgentGuides, setAgentPanelVisible, setQueryParam, start, toast };
159
402
  const ui = globalThis.__mearlToolboxUi;
160
403
  if (ui !== undefined) {
161
404
  if (!ui || typeof ui !== 'object' || ui.runtime !== 'preact') {
@@ -166,6 +409,7 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
166
409
  }
167
410
  Object.defineProperty(api, 'context', { enumerable: true, get: () => context });
168
411
  Object.defineProperty(window, 'mearl', { configurable: false, value: Object.freeze(api) });
412
+ document.addEventListener('click', handleExternalLinkClick);
169
413
 
170
414
  const applyContext = nextContext => {
171
415
  const theme =
@@ -198,6 +442,22 @@ const PAGE_BRIDGE_SCRIPT = String.raw `(() => {
198
442
  applyContext(response.context);
199
443
  return;
200
444
  }
445
+ if (
446
+ response?.type === 'agent-panel-wheel' &&
447
+ typeof response.key === 'string' &&
448
+ inlineAgentPanels.has(response.key) &&
449
+ typeof response.deltaX === 'number' &&
450
+ Number.isFinite(response.deltaX) &&
451
+ typeof response.deltaY === 'number' &&
452
+ Number.isFinite(response.deltaY)
453
+ ) {
454
+ scrollInlineAgentPage(
455
+ inlineAgentPanels.get(response.key).presentation,
456
+ response.deltaX,
457
+ response.deltaY,
458
+ );
459
+ return;
460
+ }
201
461
  if (response?.type !== 'result' || typeof response.requestId !== 'string') return;
202
462
  const request = pending.get(response.requestId);
203
463
  if (!request) return;
@@ -1,6 +1,8 @@
1
1
  export interface CloudServerConfig {
2
2
  server: string;
3
3
  }
4
+ /** Number of local calendar days between CLI update checks; null disables them. */
5
+ export declare function loadCliAutoUpdateIntervalDays(configPath?: string): number | null;
4
6
  export declare function getCloudServerConfigPath(): string;
5
7
  export declare function loadCloudServerConfig(): CloudServerConfig | null;
6
8
  export declare function loadLocalConnectorId(): string | null;
@@ -2,6 +2,29 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  const MEARL_CONFIG_DIR = join(homedir(), '.mearl');
5
+ /** Number of local calendar days between CLI update checks; null disables them. */
6
+ export function loadCliAutoUpdateIntervalDays(configPath = join(MEARL_CONFIG_DIR, 'config.json')) {
7
+ try {
8
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
9
+ if (config.cliAutoUpdate === false)
10
+ return null;
11
+ if (config.cliAutoUpdate === undefined)
12
+ return 1;
13
+ if (typeof config.cliAutoUpdate === 'object' &&
14
+ config.cliAutoUpdate !== null &&
15
+ !Array.isArray(config.cliAutoUpdate)) {
16
+ const { intervalDays } = config.cliAutoUpdate;
17
+ if (typeof intervalDays === 'number' &&
18
+ Number.isSafeInteger(intervalDays) &&
19
+ intervalDays > 0)
20
+ return intervalDays;
21
+ }
22
+ }
23
+ catch {
24
+ // A missing or invalid config uses the daily default.
25
+ }
26
+ return 1;
27
+ }
5
28
  export function getCloudServerConfigPath() {
6
29
  return join(MEARL_CONFIG_DIR, 'cloud-server.json');
7
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/client",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "description": "Unified Mearl SDK & CLI for local and remote browsers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -59,8 +59,8 @@
59
59
  "commander": "^15.0.0",
60
60
  "fastest-levenshtein": "^1.0.16",
61
61
  "ws": "^8.18.0",
62
- "@mearl/cloud-types": "2.18.0",
63
- "@mearl/provider": "2.18.0"
62
+ "@mearl/provider": "2.19.0",
63
+ "@mearl/cloud-types": "2.19.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^24.9.1",