@mearl/client 2.22.0 → 2.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -150,6 +150,10 @@ stdout 在管道或重定向时默认输出紧凑 JSON,在交互终端默认
150
150
  需要同时读取全文时显式传 `includeBody:true`。省略正文时不返回 `postData` / `responseBody` 字段;
151
151
  未指定字段时默认返回正文。
152
152
 
153
+ `send_request` 默认携带所选浏览器中匹配完整目标 URL 的未分区 Cookie,保留不同路径下的同名 Cookie;不额外拼入父站点的 host-only Cookie,也不猜测页面的 Cookie 分区。需要页面分区、页面签名或完整页面身份时,使用目标页面中的 `page_eval` 请求。
154
+ 响应中的 `Set-Cookie` 会更新原浏览器,后续调用可以使用刷新后的登录态;`withCookies:false` 同时关闭自动携带与响应回写。扩展由 Chrome 处理响应 Cookie;CDP 的 Node 请求在返回前通过原页面的浏览器连接回写,支持 HttpOnly、默认路径、过期时间和删除。CDP 响应存在 `Set-Cookie` 时附带 `cookieSync: { updatedCount, rejectedCount, failedCount }`,不返回 Cookie 值;无法确定分区的 Partitioned 响应 Cookie 计入拒绝数。同步失败不撤销已完成的 HTTP 操作,应检查状态而非直接重发业务请求。MTop 普通请求及流式请求使用相同的响应刷新机制。
155
+ 扩展的临时凭据注入仅匹配原始完整请求 URL,重定向由 Chrome 按新 URL 选择 Cookie。CDP 的 Node 请求(含 MTop 流式请求)不自动跟随重定向,避免将原 URL 的 Cookie 或签名传到其他地址。
156
+
153
157
  Mearl 默认聚合当前机器和已配置 cloud-server 下的浏览器,并通过 `browser_list` 返回的全局
154
158
  `browserId` 自动路由。以下覆盖参数仅在限定宿主或连接、传输排障时使用,因此不出现在默认 help:
155
159
 
@@ -250,7 +254,7 @@ const result = await invokePlugin(
250
254
 
251
255
  MCP Server 通过固定工具 `plugin_invoke` 接受同样的 `plugin`、`command` 和 `input`,不会把插件子命令动态扩展成 MCP 工具。SDK 与 MCP 不接受任意入口路径;调用尚未注册的 `buc`、`authx` 或 `tdbank` 时,会通过匹配当前 client 版本的 setup 自动安装并重试一次。其他未注册名称返回 `PLUGIN_NOT_REGISTERED`。
252
256
 
253
- 长命令使用本机持久任务服务。服务按注册目录隔离并自动启动,CLI、SDK、MCP 与工具箱共享任务记录;调用方退出不取消执行。服务重启后将未完成任务标记为 `interrupted`,不自动重放。后台任务固定接受时的源码快照、工作目录与环境;静态第三方 JS 依赖需要预先打包进 Skill,状态文件放在源码目录之外。
257
+ 长命令使用本机持久任务服务。服务按注册目录隔离并自动启动,CLI、SDK、MCP 与工具箱共享任务记录;调用方退出不取消执行。服务重启后将未完成任务标记为 `interrupted`,不自动重放。后台任务固定接受时的源码快照、工作目录与环境;第三方 JS 依赖应预先打包进 Skill,状态文件放在源码目录之外。快照只固定源码和相对资源,不预解析 JS 依赖;缺失依赖和语法错误由执行器加载代码时报告。
254
258
 
255
259
  ```ts
256
260
  import { PluginTaskClient } from '@mearl/client';
@@ -270,6 +274,8 @@ try {
270
274
 
271
275
  `wait` 超时或其 signal 中止只结束等待;`cancel(taskId)` 才取消执行。浏览器任务需显式传 `browser`,名称会在接受前解析为固定 ID。`get/list` 返回任务快照与结果引用,`list` 返回有界分页及 `nextCursor`;`events` 返回游标、缺口标记和最新快照,`subscribe` 封装长轮询。`result(taskId)` 分块读取并校验完整业务结果,最大 64 MiB;终态记录和结果保留 7 天。
272
276
 
277
+ 插件宿主握手与 `tasks.status()` 返回 `version/buildId`,构建标识用于定位运行代码,兼容性由握手中的协议版本决定。协议相同时,即使版本、构建标识不同或旧宿主未提供标识,任务、模型、页面和状态请求仍可继续使用现有服务,不中断活动任务或页面实例。需要切换到新构建时,确认没有活动任务及页面操作后调用 `tasks.stopHost()`,下次业务调用将启动当前构建;重新注册插件不会重启宿主。本地修改客户端或宿主代码后执行根目录 `pnpm build`,同步生成指纹并构建双方产物;仅运行 `tsc --watch` 不会更新构建指纹。
278
+
273
279
  任务能力由 SDK `PluginTaskClient` 与工具箱 `host.task.*` 使用。对外命令由具体插件按业务需要提供,CLI 使用 `mearl <plugin> <command>`,MCP 使用 `plugin_invoke`。任务、模型、状态、页面实例和宿主诊断不单独暴露为全局 CLI/MCP 命令。仅注册工具箱内部命令的插件不会列入 CLI 插件列表;直接运行其名称会提示工具的打开位置。工具页可用 `window.mearl.waitTask(taskId)`、`readTaskResult(taskId)`、`listTasks()`,范围限定为所属插件的工具箱任务。
274
280
 
275
281
  `tasks.diagnose({ taskId?, plugin?, instanceId?, includeErrorMessages?, includeLocalPaths? })` 读取当前调用范围内的有界诊断报告。默认不包含业务输入、提示词、进度正文、检查点数据、结果正文、完整 URL、环境变量和凭证,也省略错误消息及本地路径;后两项可显式选择包含。报告的 coverage 和历史 gap 表示采集范围及保留历史限制。
@@ -284,7 +290,15 @@ try {
284
290
 
285
291
  插件可选导出 `toolbox`,注册后会出现在扩展 Popup 的工具箱中。页面资源放在 Skill 目录内,注册时由
286
292
  Mearl 将入口 HTML 引用的本地 CSS、JavaScript 和图片内联为不可变页面包,不需要启动 HTTP 服务,
287
- 也不要求普通 HTML/CSS/JS 页面自行构建:
293
+ 也不要求普通 HTML/CSS/JS 页面自行构建。
294
+
295
+ 右键工具箱中的工具可单独禁用或启用对应 Skill;禁用状态显示在标题右侧,工具页面和命令仍可使用。禁用将该目录的
296
+ `SKILL.md` 重命名为 `SKILL.md.mearl-disabled`,启用时恢复,作用于共享此目录的所有 Agent;
297
+ 软链接安装会作用于源目录。已有会话可能仍保留已载入的 Skill,需要重启 Agent 或新建会话。
298
+ 工具箱内更新和重新安装会保留禁用状态;外部安装器的更新不受此状态管理,若出现两份清单,需先处理冲突。
299
+ 独立复制安装的同名 Skill 需分别禁用。没有 Skill 清单的普通插件不显示此开关。
300
+
301
+ 工具箱元数据示例:
288
302
 
289
303
  ```javascript
290
304
  export const toolbox = {
@@ -462,7 +476,7 @@ ISC
462
476
 
463
477
  ### 插件页面实例
464
478
 
465
- `PluginPageClient` 通过本地插件服务管理 attach/get/list/send/detach/events/activation。插件导出独立 `pageRuntime` 声明并用 `app.page()` 调用。扩展与纯 CDP 共用实例、事件、激活和文档运行时;实例绑定 documentId,同插件同文档的所有入口共享初始化,导航后旧实例失效,启用的激活规则负责重建。`frame` 接受 `main`、`business` 或 `page_frames` 返回的 frame ID(扩展为数字,CDP 为字符串)。外部 Provider 仍须明确声明 `page_runtime` 能力。
479
+ `PluginPageClient` 通过本地插件服务管理 attach/get/list/send/detach/events/activation。插件导出独立 `pageRuntime` 声明并用 `app.page()` 调用。宿主用 Node 内置编译器检查 code 的表达式语法,不执行代码;代码必须在网页中求值得到自包含函数,非函数或外部引用错误在页面运行时报告。扩展与纯 CDP 共用实例、事件、激活和文档运行时;实例绑定 documentId,同插件同文档的所有入口共享初始化,导航后旧实例失效,启用的激活规则负责重建。`frame` 接受 `main`、`business` 或 `page_frames` 返回的 frame ID(扩展为数字,CDP 为字符串)。外部 Provider 仍须明确声明 `page_runtime` 能力。
466
480
 
467
481
  显式手动 `attach` 复用自动激活的实例时,实例转为手动归属,初始化与在途任务继续复用;随后收窄自动激活范围不会清理该实例。
468
482
 
@@ -476,7 +490,7 @@ try {
476
490
  } finally { pages.disconnect(); }
477
491
  ```
478
492
 
479
- 后台事件声明 schema 和串行、latest 合并或有限并发策略,处理器用 `app.runtimeCommand()` 注册并通过 `context.page` 访问固定文档。文档绑定任务可用 `app.page('activation')` 只读查询本插件激活规则,不允许设置或关闭规则。页面消息/结果各上限 24 MiB,宿主自动分块并校验;事件负载上限 32 KiB。工具箱使用 `host.page.*`,UI 订阅实例历史与后台执行独立。完整契约见 [插件宿主能力](../native-host/README.md#页面运行时和事件)。
493
+ 后台事件声明 schema 和串行、latest 合并或有限并发策略;schema 是格式元数据,事件处理器负责验证负载。处理器用 `app.runtimeCommand()` 注册并通过 `context.page` 访问固定文档。文档绑定任务可用 `app.page('activation')` 只读查询本插件激活规则,不允许设置或关闭规则。页面消息/结果各上限 24 MiB,宿主自动分块并校验;事件负载上限 32 KiB。工具箱使用 `host.page.*`,UI 订阅实例历史与后台执行独立。完整契约见 [插件宿主能力](../native-host/README.md#页面运行时和事件)。
480
494
 
481
495
  Python 入口同样导出 `pageRuntime` 字典,其中 `code` 仍是运行于网页中的 JavaScript 函数字符串;后台初始化与事件处理器通过 `app.runtimeCommand(...)` 或其装饰器注册。`app.page(operation, payload)`、`context.page.send(message)` 和 `context.page.get()` 都是同步调用,后两者仅在绑定页面实例的处理器中可用。JS 和 Python 的 `context.page.send(message, {timeoutSec: 120})` 可指定单次响应超时(大于 0 且最多 300 秒,Python 使用字典);`app.page("send", {instanceId, message, timeoutSec})` 和 SDK `pages.send(plugin, instanceId, message, {timeoutSec})` 同样支持。该设置不改变其他请求,任务取消和文档解绑仍立即中断执行,传输失败也会取消对应网页请求。runtime-only 命令不出现在公开命令帮助中,也不能由 CLI 或工具箱直接执行;声明在页面事件中的普通 `app.command` 仍保留公开调用能力。
482
496
 
@@ -496,7 +510,7 @@ try {
496
510
  } finally { models.disconnect(); }
497
511
  ```
498
512
 
499
- `start` 返回持久 taskId;`run` 的 signal 取消模型执行并等待清理。`app.model(request, { onProgress })` / `context.model(request, { onProgress })` 可订阅当前这一次调用的 `model.delta`,并发调用的回调互相隔离;Python 使用 `context.model(request, on_progress=callback)`。增量 `data` 含 `delta/index/partial`,可用时带 `itemId/phase`;按消息隔离、跳过 commentary,序号缺失时回退到最终结果。增量只供临时展示,最终结果来自成功 turn 的完整消息并经过宿主 schema 校验;临时显示必须在失败时撤回,不能直接作为缓存或业务成功依据。模型目录与执行通过 `PluginModelClient` 或插件上下文使用。工具页使用 `host.model.capabilities/list/start` 和 `waitTask`。
513
+ `start` 返回持久 taskId;`run` 的 signal 取消模型执行并等待清理。`app.model(request, { onProgress })` / `context.model(request, { onProgress })` 可订阅当前这一次调用的 `model.delta`,并发调用的回调互相隔离;Python 使用 `context.model(request, on_progress=callback)`。增量 `data` 含 `delta/index/partial`,可用时带 `itemId/phase`;按消息隔离、跳过 commentary,序号缺失时回退到最终结果。增量只供临时展示,最终结果来自成功 turn 的完整消息;outputSchema 透传模型 Provider,宿主解析 JSON,插件负责验证结果结构与业务约束;临时显示必须在失败时撤回,不能直接作为缓存或业务成功依据。模型目录与执行通过 `PluginModelClient` 或插件上下文使用。工具页使用 `host.model.capabilities/list/start` 和 `waitTask`。
500
514
 
501
515
  插件入口声明状态集合,然后从命令上下文访问:
502
516
 
@@ -512,11 +526,11 @@ export default app => app.command('save', async (input, context) => {
512
526
  });
513
527
  ```
514
528
 
515
- SDK `PluginStateClient.get/getMany/list/transact` 与工具箱 `host.state.*` 共享集合。每次写入需 CAS,冲突后重新读取、合并再重试;`schemaVersion` 升级用显式迁移事务。浏览器会话/实例可声明 `cache` 或 `memory`,页面身份由宿主验证。`memory` 只保存在插件执行宿主进程中,支持同样的 CAS、schema、TTL 与分块值限制;刷新文档时保留浏览器会话范围,浏览器会话变化后隔离,执行宿主重启时清空。内存状态全宿主合计最多 128 MiB / 512 个集合命名空间,按需占用;单集合 `maxBytes` 最大 128 MiB,实例结束释放其内存。容量、TTL 和完整契约见[插件宿主能力](../native-host/README.md#状态和激活)。二进制资产继续使用文件或资源通道。
529
+ SDK `PluginStateClient.get/getMany/list/transact` 与工具箱 `host.state.*` 共享集合。schema 保留为格式描述和版本身份,写入与迁移的数据由插件自行验证;宿主检查 JSON、大小、版本和事务约束。每次写入需 CAS,冲突后重新读取、合并再重试;`schemaVersion` 升级用显式迁移事务。浏览器会话/实例可声明 `cache` 或 `memory`,页面身份由宿主验证。`memory` 只保存在插件执行宿主进程中,支持同样的 CAS、格式版本、TTL 与分块值限制;刷新文档时保留浏览器会话范围,浏览器会话变化后隔离,执行宿主重启时清空。内存状态全宿主合计最多 128 MiB / 512 个集合命名空间,按需占用;单集合 `maxBytes` 最大 128 MiB,实例结束释放其内存。容量、TTL 和完整契约见[插件宿主能力](../native-host/README.md#状态和激活)。二进制资产继续使用文件或资源通道。
516
530
 
517
531
  ### 显式任务恢复
518
532
 
519
- 插件声明 `taskRecovery`(每个命令的 checkpointVersion/schema)并注册 `app.recoveryCommand(command, handler)`,命令通过 `context.checkpoint(data, expectedSequence)` 保存最多 64 KiB 的恢复依据。检查点数据、版本、宿主 schemaHash、序号和事件与任务记录原子写入;写入必须 CAS。
533
+ 插件声明 `taskRecovery`(每个命令的 checkpointVersion/schema)并注册 `app.recoveryCommand(command, handler)`,命令通过 `context.checkpoint(data, expectedSequence)` 保存最多 64 KiB 的恢复依据。检查点数据、版本、宿主 schemaHash、序号和事件与任务记录原子写入;写入必须 CAS。schema 用于声明身份和格式兼容比较,检查点及迁移数据的结构由插件验证。
520
534
 
521
535
  `tasks.checkpoint(taskId)` 读取检查点;`tasks.resume(taskId, { environment?, revision? })` 为终态根命令建立新 attempt,保持旧任务终态,固定原安装、源码快照、输入、目录和路由。默认提交调用进程的新环境,旧环境不持久化。恢复入口读取 `context.recovery`,先核对外部操作状态;宿主不重放原 handler,也不保证外部提交与检查点原子。
522
536
 
@@ -297,7 +297,7 @@ export declare const COMMANDS: readonly [{
297
297
  readonly type: "string";
298
298
  readonly description: "规则说明";
299
299
  }];
300
- readonly notes: readonly ["扩展模式规则持久化;CDP 仅支持会话级 modifyHeaders 请求头 set,不支持响应头、initiatorDomains 或其他动作。Provider 是否支持以其能力声明为准。"];
300
+ readonly notes: readonly ["扩展模式规则持久化;CDP 仅支持会话级 modifyHeaders 请求头 set,不支持响应头、initiatorDomains 或其他动作。Provider 是否支持以其能力声明为准。", "扩展模式会检查规则请求 URL 和显式来源域名的访问权限;缺失时仍保存规则,返回 warnings 和 missingOrigins,不自动弹窗。可在请求规则页面授权,或按提示调用 request_domain_permission。复杂 URL 表达式无法完整推断域名时会提示手动检查。"];
301
301
  readonly examples: ["set_rule --payload '{\"actionType\":\"block\",\"filter\":\"^https://example\\\\.com/ads/.*\"}'", "set_rule --payload '{\"ruleId\":42,\"enabled\":false}'"];
302
302
  }, {
303
303
  readonly name: "get_rules";
@@ -1093,7 +1093,7 @@ export declare const COMMANDS: readonly [{
1093
1093
  }, {
1094
1094
  readonly name: "mtopEnv";
1095
1095
  readonly type: "\"default\" | \"prod\" | \"pre\"";
1096
- readonly description: "可选 MTop 页面接口环境;default 关闭环境改写,省略时保持当前设置。扩展模式全局生效,CDP 模式仅作用于目标 tab";
1096
+ readonly description: "可选 MTop 页面接口环境;default 关闭环境改写,省略时保持当前设置。仅作用于目标 tab";
1097
1097
  }, {
1098
1098
  readonly name: "emulation";
1099
1099
  readonly type: "object";
@@ -1211,7 +1211,7 @@ export declare const COMMANDS: readonly [{
1211
1211
  readonly required: true;
1212
1212
  readonly description: "需要授权的域名,如 example.com";
1213
1213
  }];
1214
- readonly notes: readonly ["domain 使用纯主机名,不传 URL。仅在非 BUC send_request 报域权限缺失时申请,BUC 不使用浏览器域权限。", "弹窗等待用户选择,90 秒超时或关闭按拒绝处理;granted=true 后才重试原请求。"];
1214
+ readonly notes: readonly ["domain 使用纯主机名,不传 URL。在非 BUC send_request 报域权限缺失,或 set_rule 提示缺少域名权限时申请;BUC 不使用浏览器域权限。", "弹窗等待用户选择,90 秒超时或关闭按拒绝处理;granted=true 后才重试原请求。"];
1215
1215
  readonly result: readonly ["返回 granted、domain,授权成功时包含 origin。"];
1216
1216
  readonly examples: ["request_domain_permission --payload '{\"domain\":\"example.com\"}'"];
1217
1217
  }, {
package/dist/commands.js CHANGED
@@ -298,6 +298,7 @@ export const COMMANDS = [
298
298
  ],
299
299
  notes: [
300
300
  '扩展模式规则持久化;CDP 仅支持会话级 modifyHeaders 请求头 set,不支持响应头、initiatorDomains 或其他动作。Provider 是否支持以其能力声明为准。',
301
+ '扩展模式会检查规则请求 URL 和显式来源域名的访问权限;缺失时仍保存规则,返回 warnings 和 missingOrigins,不自动弹窗。可在请求规则页面授权,或按提示调用 request_domain_permission。复杂 URL 表达式无法完整推断域名时会提示手动检查。',
301
302
  ],
302
303
  examples: [
303
304
  `set_rule --payload '{"actionType":"block","filter":"^https://example\\\\.com/ads/.*"}'`,
@@ -1200,7 +1201,7 @@ export const COMMANDS = [
1200
1201
  {
1201
1202
  name: 'mtopEnv',
1202
1203
  type: '"default" | "prod" | "pre"',
1203
- description: '可选 MTop 页面接口环境;default 关闭环境改写,省略时保持当前设置。扩展模式全局生效,CDP 模式仅作用于目标 tab',
1204
+ description: '可选 MTop 页面接口环境;default 关闭环境改写,省略时保持当前设置。仅作用于目标 tab',
1204
1205
  },
1205
1206
  {
1206
1207
  name: 'emulation',
@@ -1352,7 +1353,7 @@ export const COMMANDS = [
1352
1353
  },
1353
1354
  ],
1354
1355
  notes: [
1355
- 'domain 使用纯主机名,不传 URL。仅在非 BUC send_request 报域权限缺失时申请,BUC 不使用浏览器域权限。',
1356
+ 'domain 使用纯主机名,不传 URL。在非 BUC send_request 报域权限缺失,或 set_rule 提示缺少域名权限时申请;BUC 不使用浏览器域权限。',
1356
1357
  '弹窗等待用户选择,90 秒超时或关闭按拒绝处理;granted=true 后才重试原请求。',
1357
1358
  ],
1358
1359
  result: ['返回 granted、domain,授权成功时包含 origin。'],
@@ -534,7 +534,7 @@ export interface SetAppProfileParams {
534
534
  bridgeConfig?: AppBridgeConfig;
535
535
  /**
536
536
  * 可选 MTop 接口环境。default 关闭环境改写;省略时保持当前设置。
537
- * 扩展模式与插件设置页共享并全局生效;CDP 模式仅作用于目标 tab。
537
+ * 仅作用于目标 tab,扩展模式与该 tab 的快捷设置共享。
538
538
  */
539
539
  mtopEnv?: MtopEnvironment;
540
540
  /**
@@ -0,0 +1 @@
1
+ export declare const PLUGIN_HOST_BUILD_ID = "1d85713299314f3ff82faecc749a025177411fdedd16949cee34e6712b93e1ef";
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/generate-plugin-host-build.mjs.
2
+ export const PLUGIN_HOST_BUILD_ID = '1d85713299314f3ff82faecc749a025177411fdedd16949cee34e6712b93e1ef';
package/dist/plugin.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './plugins/sdk/plugin.js';
2
+ export * from './plugins/skillManifest.js';
package/dist/plugin.js CHANGED
@@ -1 +1,2 @@
1
1
  export * from './plugins/sdk/plugin.js';
2
+ export * from './plugins/skillManifest.js';
@@ -16,6 +16,8 @@ export interface PluginHostClientOptions extends PluginHostConnectionOptions {
16
16
  }
17
17
  export interface HostStatus {
18
18
  protocolVersion: number;
19
+ version?: string;
20
+ buildId?: string;
19
21
  host: PluginExecutionHost;
20
22
  pid: number;
21
23
  }
@@ -243,7 +243,9 @@ export class PluginHostConnection {
243
243
  this.connection = undefined;
244
244
  });
245
245
  }
246
- return (await this.connection).request(method, input, timeoutMs);
246
+ const connection = await this.connection;
247
+ // The handshake protocol defines compatibility; build identity is diagnostic metadata.
248
+ return connection.request(method, input, timeoutMs);
247
249
  }
248
250
  disconnect() {
249
251
  this.generation++;
@@ -1,7 +1,7 @@
1
1
  import { type AppProfileContribution } from '../generated/app-profile-definition.js';
2
2
  export declare class PluginError extends Error {
3
3
  readonly code: string;
4
- constructor(code: string, message: string);
4
+ constructor(code: string, message: string, options?: ErrorOptions);
5
5
  }
6
6
  export interface PluginToolboxSandboxPage {
7
7
  type: 'sandbox';
@@ -17,8 +17,8 @@ const RESERVED_NAMES = new Set([
17
17
  ]);
18
18
  export class PluginError extends Error {
19
19
  code;
20
- constructor(code, message) {
21
- super(`${code}: ${message}`);
20
+ constructor(code, message, options) {
21
+ super(`${code}: ${message}`, options);
22
22
  this.code = code;
23
23
  }
24
24
  }
@@ -405,8 +405,15 @@ export function findPluginRegistration(name) {
405
405
  ...(source ? { source } : {}),
406
406
  };
407
407
  }
408
- catch {
409
- throw new PluginError('PLUGIN_REGISTRY_INVALID', `无效的插件记录: ${file}`);
408
+ catch (cause) {
409
+ const reason = cause instanceof SyntaxError
410
+ ? 'JSON 格式错误'
411
+ : cause instanceof Error
412
+ ? cause.message
413
+ : '未知校验错误';
414
+ throw new PluginError('PLUGIN_REGISTRY_INVALID', `无效的插件记录: ${file} (${reason})`, {
415
+ cause,
416
+ });
410
417
  }
411
418
  }
412
419
  export function readPluginPageBundleChunk(name, hash, index) {
@@ -1,3 +1,5 @@
1
+ import { CLIENT_VERSION } from '../../runtimePaths.js';
2
+ import { PLUGIN_HOST_BUILD_ID } from '../../generated/plugin-host-build.js';
1
3
  import { randomUUID } from 'node:crypto';
2
4
  import { createServer } from 'node:net';
3
5
  import { chmodSync, rmSync } from 'node:fs';
@@ -17,6 +19,8 @@ export async function startPluginHostServer(configuration = {}) {
17
19
  const owner = {
18
20
  pid: process.pid,
19
21
  protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
22
+ version: CLIENT_VERSION,
23
+ buildId: PLUGIN_HOST_BUILD_ID,
20
24
  processIdentity: processIdentity(process.pid),
21
25
  };
22
26
  if (!claimRecord(paths.lease, owner))
@@ -103,6 +107,8 @@ export async function startPluginHostServer(configuration = {}) {
103
107
  };
104
108
  return {
105
109
  protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
110
+ version: CLIENT_VERSION,
111
+ buildId: PLUGIN_HOST_BUILD_ID,
106
112
  host: manager.host,
107
113
  pid: process.pid,
108
114
  };
@@ -112,6 +118,8 @@ export async function startPluginHostServer(configuration = {}) {
112
118
  if (method === 'status')
113
119
  return {
114
120
  protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
121
+ version: CLIENT_VERSION,
122
+ buildId: PLUGIN_HOST_BUILD_ID,
115
123
  host: manager.host,
116
124
  pid: process.pid,
117
125
  };
@@ -5,7 +5,6 @@ import { readFileSync, statSync } from 'node:fs';
5
5
  import { isAbsolute, join } from 'node:path';
6
6
  import { capturePluginRevision } from './pluginRevision.js';
7
7
  import { findPluginRegistration } from '../sdk/plugin.js';
8
- import { compilePluginPageSchema } from '../sdk/pluginPageDefinition.js';
9
8
  import { UnifiedClient, decodeBrowserTarget } from '../../unified.js';
10
9
  import { pluginHostPaths } from './pluginHostPaths.js';
11
10
  import { serializePluginExecutionError, pluginExecutionError } from './pluginExecutorProtocol.js';
@@ -726,9 +725,6 @@ export class PluginPageManager {
726
725
  const binding = record.runtime.events?.[name];
727
726
  if (!binding || Buffer.byteLength(JSON.stringify(input.payload ?? null)) > 32 * 1024)
728
727
  throw new Error('Page event is not declared or exceeds 32 KiB');
729
- const validate = compilePluginPageSchema(binding.schema);
730
- if (!validate(input.payload))
731
- throw failure('PLUGIN_PAGE_EVENT_INVALID', 'Page event payload does not match its schema');
732
728
  const waiting = record.deliveries.filter(item => item.name === name && item.state === 'queued');
733
729
  if (binding.policy === 'latest') {
734
730
  for (const previous of waiting) {
@@ -2,12 +2,10 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { claimRecord, readRecord, writeRecord } from '@mearl/daemon-core';
5
- import { isBuiltin } from 'node:module';
6
- import { parse } from 'acorn';
7
5
  import { pluginHostPaths } from './pluginHostPaths.js';
6
+ import { skillManifestPath } from '../skillManifest.js';
8
7
  import { findPluginRegistration, PluginError } from '../pluginRegistry.js';
9
8
  const MAX_REVISION_BYTES = 64 * 1024 * 1024;
10
- const validatedSources = new Set();
11
9
  function within(root, file) {
12
10
  const path = relative(root, file);
13
11
  return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path));
@@ -15,7 +13,7 @@ function within(root, file) {
15
13
  function sourceRoot(entry) {
16
14
  let candidate = dirname(entry);
17
15
  for (let depth = 0; depth < 6; depth++) {
18
- if (existsSync(join(candidate, 'SKILL.md')))
16
+ if (skillManifestPath(candidate))
19
17
  return candidate;
20
18
  const parent = dirname(candidate);
21
19
  if (parent === candidate)
@@ -67,48 +65,6 @@ function readSources(root) {
67
65
  visit(root, new Set());
68
66
  return files;
69
67
  }
70
- function validateDependencies(files) {
71
- const paths = new Set(files.map(file => file.path));
72
- for (const file of files) {
73
- if (!/\.[cm]?js$/.test(file.path))
74
- continue;
75
- const tree = parse(file.bytes.toString('utf8'), {
76
- ecmaVersion: 'latest',
77
- sourceType: 'module',
78
- allowReturnOutsideFunction: true,
79
- });
80
- const check = (specifier) => {
81
- if (typeof specifier !== 'string' || isBuiltin(specifier))
82
- return;
83
- if (!specifier.startsWith('./') && !specifier.startsWith('../'))
84
- throw new PluginError('PLUGIN_SOURCE_NOT_SELF_CONTAINED', `后台任务需要自包含 Skill,请先打包依赖: ${file.path}: ${specifier}`);
85
- const local = resolve('/source', dirname(file.path), specifier);
86
- const normalized = relative('/source', local).split(sep).join('/');
87
- if (!within('/source', local) ||
88
- ![normalized, `${normalized}.js`, `${normalized}.json`, `${normalized}/index.js`].some(path => paths.has(path)))
89
- throw new PluginError('PLUGIN_SOURCE_NOT_SELF_CONTAINED', `依赖不在 Skill 快照中: ${file.path}: ${specifier}`);
90
- };
91
- const visit = (node) => {
92
- if (!node || typeof node !== 'object')
93
- return;
94
- if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(node.type))
95
- check(node.source?.value);
96
- if (node.type === 'ImportExpression')
97
- check(node.source?.value);
98
- if (node.type === 'CallExpression' &&
99
- node.callee?.type === 'Identifier' &&
100
- node.callee.name === 'require')
101
- check(node.arguments[0]?.value);
102
- for (const value of Object.values(node)) {
103
- if (Array.isArray(value))
104
- value.forEach(visit);
105
- else if (value && typeof value === 'object')
106
- visit(value);
107
- }
108
- };
109
- visit(tree);
110
- }
111
- }
112
68
  function fingerprint(files, entry) {
113
69
  return createHash('sha256')
114
70
  .update(JSON.stringify({ entry, files: files.map(file => [file.path, file.hash, file.executable]) }))
@@ -138,12 +94,6 @@ export function capturePluginRevision(plugin) {
138
94
  const relativeEntry = relative(root, entry).split(sep).join('/');
139
95
  const files = readSources(root);
140
96
  const revision = fingerprint(files, relativeEntry);
141
- if (!validatedSources.has(revision)) {
142
- validateDependencies(files);
143
- if (validatedSources.size >= 64)
144
- validatedSources.delete(validatedSources.values().next().value);
145
- validatedSources.add(revision);
146
- }
147
97
  const revisionRoot = join(paths.revisions, installation.installationId, revision);
148
98
  if (!existsSync(revisionRoot)) {
149
99
  const temporary = `${revisionRoot}.${randomUUID()}.tmp`;
@@ -3,7 +3,6 @@ import { existsSync, readFileSync, readdirSync, statSync, rmSync } from 'node:fs
3
3
  import { join, sep } from 'node:path';
4
4
  import { pluginHostPaths } from './pluginHostPaths.js';
5
5
  import { writePluginHostFile } from './pluginHostFiles.js';
6
- import { compilePluginStateSchema } from '../sdk/pluginStateDefinition.js';
7
6
  function fail(code, message) {
8
7
  throw Object.assign(new Error(message), { code });
9
8
  }
@@ -316,7 +315,6 @@ export class PluginStateStore {
316
315
  else
317
316
  delete entry.expiresAt;
318
317
  }
319
- const validate = compilePluginStateSchema(definition.schema);
320
318
  for (const operation of input.operations) {
321
319
  key(operation.key);
322
320
  if (names.has(operation.key) ||
@@ -328,9 +326,9 @@ export class PluginStateStore {
328
326
  delete entries[operation.key];
329
327
  continue;
330
328
  }
331
- if (!validate(operation.value) ||
332
- Buffer.byteLength(JSON.stringify(operation.value)) > 256 * 1024)
333
- fail('PLUGIN_STATE_SCHEMA_INVALID', 'State value does not match the declared schema or exceeds 256 KiB');
329
+ const json = JSON.stringify(operation.value);
330
+ if (json === undefined || Buffer.byteLength(json) > 256 * 1024)
331
+ fail('PLUGIN_STATE_INVALID', 'State value must be JSON within 256 KiB');
334
332
  const ttl = operation.ttlMs ?? definition.ttlMs ?? 86400000;
335
333
  if (definition.storage === 'persistent' && operation.ttlMs !== undefined)
336
334
  fail('PLUGIN_STATE_INVALID', 'Persistent state does not accept TTL');
@@ -342,9 +340,6 @@ export class PluginStateStore {
342
340
  ...(definition.storage !== 'persistent' ? { expiresAt: now + ttl } : {}),
343
341
  };
344
342
  }
345
- for (const entry of Object.values(entries))
346
- if (!validate(entry.value))
347
- fail('PLUGIN_STATE_SCHEMA_INVALID', 'Migration leaves entries incompatible with the target schema');
348
343
  const commit = {
349
344
  revision,
350
345
  schemaVersion: definition.schemaVersion,
@@ -1,7 +1,6 @@
1
1
  import { normalizeModelDiagnostic, diagnosticBinding } from './pluginDiagnostics.js';
2
2
  import { pluginModelCapabilities, normalizePluginModel, validatePluginModelResult, } from '../sdk/pluginModelDefinition.js';
3
3
  import { pluginRecoverySignature } from '../sdk/pluginRecoveryDefinition.js';
4
- import { compilePluginStateSchema } from '../sdk/pluginStateDefinition.js';
5
4
  import { createHash, randomUUID } from 'node:crypto';
6
5
  import { EventEmitter } from 'node:events';
7
6
  import { isAbsolute, join, relative, sep } from 'node:path';
@@ -482,8 +481,7 @@ export class PluginTaskManager {
482
481
  signal.throwIfAborted();
483
482
  if (!sourceDefinition ||
484
483
  sourceDefinition.checkpointVersion !== record.checkpoint.version ||
485
- digest(sourceDefinition.schema) !== record.checkpoint.schemaHash ||
486
- !compilePluginStateSchema(sourceDefinition.schema)(record.checkpoint.data))
484
+ digest(sourceDefinition.schema) !== record.checkpoint.schemaHash)
487
485
  throw error('PLUGIN_TASK_RECOVERY_UNAVAILABLE', 'Checkpoint does not match the retained recovery declaration');
488
486
  let checkpoint = structuredClone(record.checkpoint);
489
487
  let declarationHash = pluginRecoverySignature(sourceDefinition);
@@ -542,8 +540,6 @@ export class PluginTaskManager {
542
540
  data: JSON.parse(json),
543
541
  };
544
542
  }
545
- if (!compilePluginStateSchema(target.schema)(checkpoint.data))
546
- throw error('PLUGIN_TASK_CHECKPOINT_INVALID', 'Migrated checkpoint does not match the target schema');
547
543
  }
548
544
  signal.throwIfAborted();
549
545
  // Admission is synchronous after both immutable snapshots and execution ownership are verified.
@@ -809,8 +805,8 @@ export class PluginTaskManager {
809
805
  if (declaration &&
810
806
  payload.expectedDeclaration !== pluginRecoverySignature(declaration))
811
807
  throw error('PLUGIN_RECOVERY_DECLARATION_CHANGED', 'Checkpoint declaration differs from the executing command');
812
- if (!declaration || !compilePluginStateSchema(declaration.schema)(data))
813
- throw error('PLUGIN_TASK_CHECKPOINT_INVALID', 'Checkpoint is undeclared or does not match its schema');
808
+ if (!declaration)
809
+ throw error('PLUGIN_TASK_CHECKPOINT_INVALID', 'Checkpoint is undeclared');
814
810
  const current = this.store.read(taskId);
815
811
  if (current.task.state !== 'running' ||
816
812
  payload.expectedSequence !== (current.checkpoint?.sequence ?? 0))
@@ -4,6 +4,7 @@ import { normalizePluginPageRuntime } from './pluginPageDefinition.js';
4
4
  import { loadPluginAppProfiles } from './pluginAppProfiles.js';
5
5
  export { invokePageEvalWithArgs } from '../../pageEvalArguments.js';
6
6
  import { readFileSync } from 'node:fs';
7
+ import { skillManifestPath } from '../skillManifest.js';
7
8
  import { dirname, extname, resolve } from 'node:path';
8
9
  import { pathToFileURL } from 'node:url';
9
10
  import { Command } from 'commander';
@@ -22,7 +23,10 @@ export { assertPluginName, findPluginDirectoryEntry, findPluginEntry, findPlugin
22
23
  export { findOfficialPlugin, listOfficialPluginNames, OFFICIAL_PLUGIN_MANAGER, OFFICIAL_PLUGINS, } from '../officialPluginCatalog.js';
23
24
  function readPluginVersion(entry) {
24
25
  try {
25
- const content = readFileSync(resolve(dirname(entry), '..', 'SKILL.md'), 'utf8');
26
+ const manifest = skillManifestPath(resolve(dirname(entry), '..'));
27
+ if (!manifest)
28
+ return undefined;
29
+ const content = readFileSync(manifest, 'utf8');
26
30
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
27
31
  const versionLine = frontmatter?.split(/\r?\n/).find(line => /^version:\s*/.test(line));
28
32
  return versionLine?.match(/^version:\s*["']?([^"'#\s]+)["']?\s*$/)?.[1];
@@ -1,5 +1,4 @@
1
1
  import { CODEX_MODEL_CAPABILITY } from '@mearl/agent-runtime/capabilities';
2
- import { compilePluginStateSchema } from './pluginStateDefinition.js';
3
2
  export function pluginModelCapabilities() {
4
3
  return { providers: [CODEX_MODEL_CAPABILITY] };
5
4
  }
@@ -44,7 +43,6 @@ export function normalizePluginModel(value) {
44
43
  request.outputSchema.$async ||
45
44
  Buffer.byteLength(JSON.stringify(request.outputSchema)) > 64 * 1024)
46
45
  throw new Error('Invalid model output schema');
47
- compilePluginStateSchema(request.outputSchema);
48
46
  }
49
47
  return JSON.parse(JSON.stringify(request));
50
48
  }
@@ -65,10 +63,6 @@ export function validatePluginModelResult(request, value) {
65
63
  code: 'PLUGIN_MODEL_SCHEMA_INVALID',
66
64
  });
67
65
  }
68
- if (!compilePluginStateSchema(request.outputSchema)(output))
69
- throw Object.assign(new Error('Model result does not match the output schema'), {
70
- code: 'PLUGIN_MODEL_SCHEMA_INVALID',
71
- });
72
66
  return { ...result, output };
73
67
  }
74
68
  return result;
@@ -1,3 +1,2 @@
1
1
  import type { PluginPageRuntime } from '../../generated/plugin-pages.js';
2
2
  export declare function normalizePluginPageRuntime(value: unknown): PluginPageRuntime | undefined;
3
- export declare function compilePluginPageSchema(schema: Record<string, unknown>): import("ajv/dist/core.js").AnyValidateFunction<unknown>;
@@ -1,7 +1,5 @@
1
- import { parse } from 'acorn';
2
- import { Ajv } from 'ajv';
1
+ import { Script } from 'node:vm';
3
2
  import { isPluginPageMatchPattern } from '../../generated/plugin-pages.js';
4
- const validators = new WeakMap();
5
3
  export function normalizePluginPageRuntime(value) {
6
4
  if (value === undefined)
7
5
  return undefined;
@@ -16,12 +14,7 @@ export function normalizePluginPageRuntime(value) {
16
14
  Buffer.byteLength(runtime.code) > 384 * 1024 ||
17
15
  Buffer.byteLength(JSON.stringify(runtime)) > 448 * 1024)
18
16
  throw new Error('Invalid plugin pageRuntime declaration');
19
- const program = parse(`(${runtime.code}\n)`, { ecmaVersion: 'latest' });
20
- const statement = program.body[0];
21
- if (program.body.length !== 1 ||
22
- statement.type !== 'ExpressionStatement' ||
23
- !['ArrowFunctionExpression', 'FunctionExpression'].includes(statement.expression?.type ?? ''))
24
- throw new Error('pageRuntime.code must be a self-contained function expression');
17
+ new Script(`(${runtime.code}\n)`, { filename: 'plugin-page-runtime.js' });
25
18
  if (runtime.events !== undefined &&
26
19
  (!runtime.events ||
27
20
  typeof runtime.events !== 'object' ||
@@ -46,17 +39,8 @@ export function normalizePluginPageRuntime(value) {
46
39
  event.maxPending > 64)) ||
47
40
  event.schema.$async)
48
41
  throw new Error(`Invalid page event binding: ${name}`);
49
- compilePluginPageSchema(event.schema);
50
42
  }
51
43
  if (runtime.initialize !== undefined && !/^[a-z][a-z0-9-]*$/.test(runtime.initialize))
52
44
  throw new Error('Invalid page runtime initialize command');
53
45
  return JSON.parse(JSON.stringify(runtime));
54
46
  }
55
- export function compilePluginPageSchema(schema) {
56
- let validate = validators.get(schema);
57
- if (!validate) {
58
- validate = new Ajv({ strict: true, allErrors: false }).compile(schema);
59
- validators.set(schema, validate);
60
- }
61
- return validate;
62
- }
@@ -1,5 +1,4 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { compilePluginStateSchema } from './pluginStateDefinition.js';
3
2
  export function normalizePluginTaskRecovery(value) {
4
3
  if (value === undefined)
5
4
  return;
@@ -22,7 +21,6 @@ export function normalizePluginTaskRecovery(value) {
22
21
  Array.isArray(definition.schema) ||
23
22
  definition.schema.$async)
24
23
  throw new Error('Invalid task checkpoint definition');
25
- compilePluginStateSchema(definition.schema);
26
24
  if (definition.migrateFrom !== undefined) {
27
25
  if (!Array.isArray(definition.migrateFrom) ||
28
26
  definition.migrateFrom.length < 1 ||
@@ -40,7 +38,6 @@ export function normalizePluginTaskRecovery(value) {
40
38
  Array.isArray(source.schema) ||
41
39
  source.schema.$async)
42
40
  throw new Error('Checkpoint migrations require distinct older versions and schemas');
43
- compilePluginStateSchema(source.schema);
44
41
  versions.add(source.version);
45
42
  }
46
43
  }
@@ -1,4 +1,3 @@
1
1
  import type { PluginStateDeclaration } from '../../generated/plugin-state.js';
2
- export declare function compilePluginStateSchema(schema: Record<string, unknown>): import("ajv/dist/core.js").AnyValidateFunction<unknown>;
3
2
  export declare function normalizePluginState(value: unknown): PluginStateDeclaration | undefined;
4
3
  export declare function pluginStateSignature(collection: import('../../generated/plugin-state.js').PluginStateCollection): string;
@@ -1,14 +1,4 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { Ajv } from 'ajv';
3
- const validators = new WeakMap();
4
- export function compilePluginStateSchema(schema) {
5
- let validate = validators.get(schema);
6
- if (!validate) {
7
- validate = new Ajv({ strict: true }).compile(schema);
8
- validators.set(schema, validate);
9
- }
10
- return validate;
11
- }
12
2
  export function normalizePluginState(value) {
13
3
  if (value === undefined)
14
4
  return;
@@ -55,7 +45,6 @@ export function normalizePluginState(value) {
55
45
  throw new Error('State TTL must be between 1 ms and 7 days');
56
46
  if (collection.storage === 'persistent' && collection.ttlMs !== undefined)
57
47
  throw new Error('Persistent state does not accept TTL');
58
- compilePluginStateSchema(collection.schema);
59
48
  }
60
49
  return declaration;
61
50
  }
@@ -0,0 +1,5 @@
1
+ export declare const DISABLED_SKILL_MANIFEST = "SKILL.md.mearl-disabled";
2
+ /** 保留的清单供工具运行时读取,但不参与 Agent 的 SKILL.md 发现。 */
3
+ export declare function skillManifestPath(directory: string): string | undefined;
4
+ export declare function getSkillEnabled(directory: string): boolean | undefined;
5
+ export declare function setSkillEnabled(directory: string, enabled: boolean): void;
@@ -0,0 +1,35 @@
1
+ import { existsSync, lstatSync, renameSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export const DISABLED_SKILL_MANIFEST = 'SKILL.md.mearl-disabled';
4
+ /** 保留的清单供工具运行时读取,但不参与 Agent 的 SKILL.md 发现。 */
5
+ export function skillManifestPath(directory) {
6
+ for (const name of ['SKILL.md', DISABLED_SKILL_MANIFEST]) {
7
+ const file = join(directory, name);
8
+ if (existsSync(file))
9
+ return file;
10
+ }
11
+ return undefined;
12
+ }
13
+ export function getSkillEnabled(directory) {
14
+ const manifest = skillManifestPath(directory);
15
+ return manifest ? manifest === join(directory, 'SKILL.md') : undefined;
16
+ }
17
+ export function setSkillEnabled(directory, enabled) {
18
+ const active = join(directory, 'SKILL.md');
19
+ const disabled = join(directory, DISABLED_SKILL_MANIFEST);
20
+ const source = enabled ? disabled : active;
21
+ const destination = enabled ? active : disabled;
22
+ const sourceStatus = lstatSync(source, { throwIfNoEntry: false });
23
+ const destinationStatus = lstatSync(destination, { throwIfNoEntry: false });
24
+ if (sourceStatus && destinationStatus) {
25
+ throw new Error('Skill 清单存在冲突,请检查 SKILL.md 和 SKILL.md.mearl-disabled');
26
+ }
27
+ if (!sourceStatus) {
28
+ if (destinationStatus?.isFile())
29
+ return;
30
+ throw new Error('找不到 Skill 清单');
31
+ }
32
+ if (!sourceStatus.isFile())
33
+ throw new Error('Skill 清单必须是普通文件');
34
+ renameSync(source, destination);
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/client",
3
- "version": "2.22.0",
3
+ "version": "2.22.1",
4
4
  "description": "Unified Mearl SDK & CLI for local and remote browsers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -60,15 +60,13 @@
60
60
  "registry": "https://registry.npmjs.org"
61
61
  },
62
62
  "dependencies": {
63
- "acorn": "^8.16.0",
64
- "ajv": "^8.18.0",
65
63
  "commander": "^15.0.0",
66
64
  "fastest-levenshtein": "^1.0.16",
67
65
  "ws": "^8.18.0",
68
- "@mearl/daemon-core": "2.22.0",
69
- "@mearl/cloud-types": "2.22.0",
70
- "@mearl/provider": "2.22.0",
71
- "@mearl/agent-runtime": "2.22.0"
66
+ "@mearl/cloud-types": "2.22.1",
67
+ "@mearl/daemon-core": "2.22.1",
68
+ "@mearl/provider": "2.22.1",
69
+ "@mearl/agent-runtime": "2.22.1"
72
70
  },
73
71
  "devDependencies": {
74
72
  "@types/node": "^24.9.1",
@@ -76,7 +74,7 @@
76
74
  "typescript": "^5.8.3"
77
75
  },
78
76
  "scripts": {
79
- "build": "node ../../scripts/generate-client-action-protocol.mjs --check && rm -rf dist/ && tsc && chmod +x dist/bin.js",
77
+ "build": "node ../../scripts/generate-client-action-protocol.mjs --check && node ../../scripts/generate-plugin-host-build.mjs && rm -rf dist/ && tsc && chmod +x dist/bin.js",
80
78
  "dev": "tsc --watch",
81
79
  "typecheck": "node ../../scripts/generate-client-action-protocol.mjs --check && tsc --noEmit",
82
80
  "test": "vitest run"