@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 +33 -5
- package/dist/autoUpdate.d.ts +33 -0
- package/dist/autoUpdate.js +359 -0
- package/dist/autoUpdateWorker.d.ts +1 -0
- package/dist/autoUpdateWorker.js +5 -0
- package/dist/bin.js +3 -0
- package/dist/check.d.ts +11 -0
- package/dist/check.js +106 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +7 -0
- package/dist/generated-browser-action-protocol.d.ts +4 -2
- package/dist/generated-browser-action-protocol.js +2 -0
- package/dist/pluginPageBundle.js +265 -5
- package/dist/unifiedConfig.d.ts +2 -0
- package/dist/unifiedConfig.js +23 -0
- package/package.json +3 -3
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
|
|
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
|
|
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
|
-
|
|
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 通过固定工具 `
|
|
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 {};
|
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
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',
|
package/dist/pluginPageBundle.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/unifiedConfig.d.ts
CHANGED
|
@@ -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;
|
package/dist/unifiedConfig.js
CHANGED
|
@@ -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.
|
|
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/
|
|
63
|
-
"@mearl/
|
|
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",
|