@coda-console/plugin-check 0.4.5 → 0.4.7

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
@@ -11,24 +11,40 @@ plugins/codex/ Codex:入口在同一进程里运行锁定版本的 c
11
11
  plugins/claude/ Claude SDK:基于 Claude Agent SDK 的 ACP 入口(npm 依赖),面板「Claude」
12
12
  plugins/kimi/ Kimi Code:在运行机的 kimi acp 与 Coda 之间转发,面板「Kimi」
13
13
  plugins/grok/ Grok:转发运行机上 Grok CLI 的 ACP,并适配审批模式,面板「Grok」
14
- plugins/opencode/ OpenCode:在运行机的 opencode acp 与 Coda 之间转发,送入会话指令
14
+ plugins/opencode/ OpenCode:在运行机的 opencode acp 与 Coda 之间转发,送入会话指令与所选模型服务,模型按来源分组
15
15
  plugins/codex-accounts/ Codex 多账号:管理登录、自动续期与账号选择,接管 Codex 额度面板
16
16
  plugins/cursor/ Cursor:在运行机的 cursor-agent acp 与 Coda 之间转发,送入会话指令
17
+ plugins/magpie/ Magpie:把本机的 Magpie 网关作为模型服务提供给 Pi 等插件,安装时装好 Magpie 并让网关常驻
17
18
  plugins/review/ PR 生命流程:会话开的 PR 先跑项目的检查、再交叉评审,你批准后合并、部署与发布;也是写 PR 流程的样例
18
19
  templates/agent/ 最小的 Agent 插件:手写 ACP 的回声 Agent,演示会话指令、插话、Agent 活动与自主轮次
19
20
  templates/panel/ 最小的面板插件:一张卡片、一条进度条、一个按钮
20
21
  bin/、check/ 契约检查(仓库根的 package.json 就是它,@coda-console/plugin-check)
21
22
  test/ 检查包的测试;test/plugins/ 是各插件的测试(插件目录里只放运行要用的文件)
22
23
  docs/agent-protocol.md Coda 接入协议正文的公开副本
24
+ .coda/workflows/ 本仓库自己的流程:sync-coda 等新版本发到 npm,再给 Coda 开升级依赖的 PR
23
25
  ```
24
26
 
27
+ ## 商店
28
+
29
+ Coda 插件页的「商店」列出 Coda 自带的插件,以及 npm 上 `@coda-console` 下的包(Coda 的 docs/plugins.md「商店」):
30
+ 本仓库的插件合进 main 后发布为 `@coda-console/<id>`([npm 发布](#npm-发布)),发出来就在商店里,不用另写索引。
31
+
32
+ - **分类**写在插件清单的 `categories` 里(照 VS Code,取第一个),只用这几个,Coda 的插件页按它分组:`Agent`、`模型与账号`、`工作流程`、`机器与资源`、`Skills 与工具`。
33
+ `npm test` 里的 `test/categories.test.mjs` 核对本仓库的每个插件都写了;契约检查对没写的、写了别的给出提醒。
34
+ - **下架**:给它的最新版标 `npm deprecate`,商店就不再列它。
35
+ - 别处的插件(别的仓库、别的 npm 包)不在商店里,用户在 Coda 里填仓库地址或包名添加。
36
+
25
37
  ## Coda 自带的插件
26
38
 
27
39
  `codex`、`claude`、`kimi`、`grok`、`opencode`、`codex-accounts`、`review` 是 Coda 内置的插件,源码从此在这里维护。
28
40
  合进 main 后它们发布为 npm 包 `@coda-console/<id>`([npm 发布](#npm-发布)),Coda 后端的 `package.json` 把它们写成钉住版本号的依赖,
29
41
  随后端的 `npm ci` 装好,新机器首次启动就带着它们。
30
42
 
31
- - **改它们先在这里提 PR、合并**,等发布流程发出新版本,再在 Coda 里把依赖升到那个版本(Coda 的 docs/development.md「coda-plugins 维护的内置插件」)。
43
+ - **改它们先在这里提 PR、合并**,发布流程发出新版本后,再让 Coda 把依赖升到那个版本(Coda 的 docs/development.md「coda-plugins 维护的内置插件」)。
44
+ 升依赖由本仓库的部署流程 `sync-coda`(`.coda/workflows/sync-coda.bpmn`、`scripts/sync-coda.mjs`)做:合并时勾选「部署」,它在合并提交上先等
45
+ `publish.yml` 跑完、npm 读得到这个提交的版本(发布失败就带着那次运行的地址失败),再把 Coda 用到的包升到这些版本(只升不降)、照抄插件的 `overrides`,
46
+ 推到 Coda 的分支 `sync/coda-plugins` 并开 PR(开着的就只更新),PR 由开这边 PR 的会话负责、走 Coda 的 PR 流程;Coda 已是这些版本就什么都不做。
47
+ 也可以单独发起,同步默认分支的最新提交。
32
48
  - **改了哪个插件就升哪个插件的 `version`**(`plugins/<id>/` 下任何文件,含 `package-lock.json`;只改 `test/` 不用):Coda 的插件页与遥测按版本认插件。
33
49
  CI 在 PR 上运行 `node scripts/check-versions.mjs origin/<目标分支>` 核对;Dependabot 升 CLI、SDK 的 PR 也要补一个升版本的提交。
34
50
  - Coda 的「检查更新」可以从 npm 装同一个包的新版本,盖过 Coda 带的版本;只有 `@coda-console/<id>` 这个包能用这些名字。
@@ -42,7 +58,7 @@ docs/agent-protocol.md Coda 接入协议正文的公开副本
42
58
  | `codex-accounts` | `proper-lockfile` 4.1.2;用依赖插件 `codex` 的 Codex CLI | 无;依赖 `codex` 插件 |
43
59
  | `kimi` | 无 | Kimi CLI 2.1.1 及以上(设置动作安装或升级;面板可从 Coda 安装、登录) |
44
60
  | `grok` | 无 | Grok CLI 1.0.52 及以上(设置动作安装或升级;面板可从 Coda 安装、登录) |
45
- | `opencode` | 无 | 已登录的 OpenCode CLI 1.0.151 及以上(设置动作安装或升级) |
61
+ | `opencode` | `jsonc-parser` 3.3.1 | 已登录的 OpenCode CLI 2.1.4 及以上(设置动作安装或升级,OpenCode 1 升到 2) |
46
62
 
47
63
  带依赖的插件各有自己的 `package-lock.json`(`node_modules` 不提交);Coda 在插件的版本目录里 `npm ci` 装它们,
48
64
  这几个插件不是本仓库根的 npm workspaces(根包是契约检查,装它不该带上 Codex、Claude 的平台二进制)。
@@ -57,11 +73,62 @@ Kimi、Grok、OpenCode、Cursor 的 CLI 不接受它:入口不转给 CLI,在
57
73
  把指令包在 `<coda-session-instructions>` 里作为单独的一段文本放在用户输入前面,之后与 `session/load` 恢复的会话不再送;
58
74
  第一条是斜杠命令时留到下一条。Codex 另声明插话:一轮已经结束时收到的 `_session/steering` 回 `promptRequired`,不另开一轮。
59
75
 
76
+ OpenCode 遇到模型服务的 429,会照 `Retry-After` 等(OpenCode 2 每次最多 15 分钟、最多 10 次),并在 ACP 里用 `session_info_update` 的
77
+ `_meta["opencode/retry"]` 说下次什么时候重试、服务报了什么;子 Agent(OpenCode 的 `subagent`)的等待也报在等它的那一轮的会话里。
78
+ Coda 自己对服务故障只等一分钟以内,所以一轮里 OpenCode 要等超过一分钟才重试时,入口先 `session/cancel` 停下这一轮(子 Agent 随之停下),
79
+ 再把这一轮答成失败,原因是服务给的原文加上 OpenCode 要等多久;停下后 OpenCode 回的是什么都不转出。一分钟以内的等待照旧由 OpenCode 自己处理。
80
+ 不在入口里按厂商的错误文字判断额度耗尽。
81
+
60
82
  Kimi、Grok、OpenCode、Cursor 带设置动作(`contributes.setup`,`scripts/setup.mjs`),用户安装或更新插件时 Coda 在每台机器上运行:
61
83
  CLI 不在或运行不了就用它自己的安装程序安装,低于插件要求的版本就升级,较新的版本保留。要求的版本:Kimi 2.1.1(入口读得懂的会话文件布局)、
62
- Grok 1.0.52(审批模式通知核对过的版本)、OpenCode 1.0.151(`opencode acp` 第一个报当前模型、恢复的会话能接着跑的版本:之前 `session/load` 不登记会话,下一轮报 Session not found);Cursor 没有已知的下限,能运行就行(它自己会更新)。
84
+ Grok 1.0.52(审批模式通知核对过的版本)、OpenCode 2.1.4(入口核对过的版本;OpenCode 1 遇到模型服务的 `Retry-After` 会一声不响地等,多久都等,见下文。用 OpenCode 2 自己的安装程序 `https://opencode.ai/v2/install`,装在同一个 `~/.opencode/bin`,OpenCode 1 的会话由它按原 ID 接过去);Cursor 没有已知的下限,能运行就行(它自己会更新)。
63
85
  Codex 与 Claude 的 CLI、SDK 是插件的 npm 依赖,不需要设置动作。
64
86
 
87
+ ### 模型服务
88
+
89
+ Claude、Codex 与 OpenCode 各有一项设置「模型服务」(`claude.modelProviders`、`codex.modelProviders`、`opencode.modelProviders`),
90
+ 勾选任意几个别的插件提供的[模型服务](https://github.com/coda-console/coda/blob/main/docs/plugins.md#模型服务),如 Magpie、CLIProxy;
91
+ 设置写的是 Coda 的「多个模型服务」(`type: array`,`items.format: coda-model-provider`,`items.modelApis` 写各自接得住的接口,客户端只列这些)。
92
+ 会话的模型按来源分组上报(ACP 选择型配置项的分组,Coda 的 docs/agent-protocol.md「模型的分组」):Agent 自己的一组,每个勾选的服务一组、组名是服务标题,组内只写模型名。
93
+ 只经会话进程的启动参数与按会话的覆盖生效,不改运行机上的 `~/.claude/settings.json`、`~/.codex/config.toml`、`opencode.json`;不选、或提供它的插件卸载了,与原来一样。
94
+
95
+ - **接口**:Claude 只接 `anthropic-messages` 的服务,Codex 只接 `openai-responses` 的(Codex 0.162 已不接受 `wire_api = "chat"`)。
96
+ OpenCode 接 `openai-completions`、`openai-responses` 与 `anthropic-messages`。
97
+ 勾了别的接口的服务(旧设置留下的),入口只在标准错误记一行,不列它的模型。
98
+ - **Claude / Codex 模型**:会话打开时从每个服务的「地址/models」列出模型(各服务同时问),问法同 Coda 的「检查可用模型」(Claude 带 `x-api-key`,Codex 带 `Authorization: Bearer`),
99
+ 按勾选的先后排在 Agent 自己的模型后面:ID 是 `<服务>/<模型>`(如 `magpie/anthropic/deepseek/deepseek-v4-pro`),名字只是模型(如 `deepseek/deepseek-v4-pro`),
100
+ 在服务那一组里,没有推理强度可选。哪个服务列不出来就没有它那一组,其他的照旧。Codex 的分组在 codex-acp 生成模型配置项的地方打补丁做。
101
+ - **Claude / Codex 何时换**:选模型只是记下(Coda 读模型目录时会把每个模型都选一遍,不该因此启动什么);下一轮开始前,所选模型的服务与会话正在用的不同(换到另一个服务、或回到自己的),才把会话换过去。
102
+ - **Claude**:以 `resume` 新起一个 SDK 会话(还没说过话的会话用同一个会话 ID 新起),flag settings 的 `env` 设 `ANTHROPIC_BASE_URL`、
103
+ `ANTHROPIC_AUTH_TOKEN`(服务的 Key,没有 Key 写 `none`)与 `ANTHROPIC_MODEL`、`ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL`、
104
+ `CLAUDE_CODE_SUBAGENT_MODEL`(子 Agent、标题等后台调用也用这个模型)。运行机自己通往 API 的办法一样都不带过去:`ANTHROPIC_API_KEY`、
105
+ `CLAUDE_CODE_OAUTH_TOKEN`、`ANTHROPIC_CUSTOM_HEADERS`、云服务商开关等置空,`apiKeyHelper` 置空(不然 API Key、helper 给的 Key 会作为 `x-api-key` 发给服务)。
106
+ flag settings 高于运行机与项目的 settings,它们 `env` 里的同名变量盖不过它;换回 Claude 自己的模型时新起的会话不带这些,运行机的配置照旧。
107
+ 换回 Claude 自己的模型同样新起一个,不带这些;审批模式与推理强度沿用。还有后台任务、子 Agent 或定时唤醒没结束时,换到另一边的模型被拒绝(-32602),因为它们随原来的进程结束。
108
+ 新起的那个起不来(SDK 当场报错、初始化失败或超时),这一轮失败,会话随即结束(同 SDK 会话自己结束),Coda 下次再打开它。
109
+ - **Codex**:Codex 一个线程只绑一个模型 provider,所以照 codex-acp 处理 ACP `providers/set`、`providers/disable` 的办法:重启 app-server,
110
+ 把服务作为它 OpenAI 槽位的 gateway(`model_providers.custom-gateway`,`wire_api = "responses"`,Key 放在 `Authorization` 头),再 resume 会话的线程;
111
+ 换回 Codex 自己的模型时撤掉 gateway 再 resume。resume 带着会话打开时的 Coda 会话指令与 MCP 服务器,app-server 里的后台命令按 codex-acp 原有的做法报为停止。
112
+ 换的过程中失败(重启的 app-server 起不来、resume 失败)时这一轮失败,下一轮从重启起重新换(会话在哪个 provider 上由入口自己记:
113
+ codex-acp 在 resume 之前就记下了 gateway,新 app-server 起不来时还留着已关闭的旧客户端,都不能拿来当换成了)。
114
+ 一句话都还没说的线程 Codex 没有写下来,resume 不了:先以第一条消息(前 80 字)为名 `thread/name/set` 把它写下,这也就成了会话标题(不再由 Codex 生成)。
115
+ Codex 没有服务的模型的元数据,它为此发的「Model metadata … not found」与恢复时的「recorded with model …」提示不进会话。
116
+ - **OpenCode**:启动 `opencode acp` 前按接口从每个服务的「地址/models」列出模型,通过子进程的 `OPENCODE_CONFIG_CONTENT` 给每个服务加一个独立 Provider,
117
+ ID 为 `coda-<插件>.<服务>`(如 `coda-magpie.openai`),模型 ID 为 `<Provider>/<模型>`,保留模型名里的斜杠。模型列表、选择与恢复由 OpenCode 原生处理,
118
+ 切模型不用重启;服务的标题作为 Provider 名。入口把 OpenCode 报给 Coda 的模型配置项按 Provider 分组(OpenCode 写作「Provider ID/模型名」,
119
+ 组内去掉前缀):服务的组名是它完整的标题,OpenCode 自己的 Provider 各成一组,组名是 Provider ID(OpenCode 只报这个);模型 ID 不变。Chat 用 `@ai-sdk/openai-compatible`,Responses 用 `@ai-sdk/openai`,Anthropic 用 `@ai-sdk/anthropic`,
120
+ 都是 OpenCode 自带的 SDK。Anthropic 请求模型列表用服务给的地址,生成请求的地址若未以 `/v1` 结尾则补上。
121
+ Key 只用服务的;无 Key 时显式传 `none`,避免 SDK 回退到机器自己的环境变量凭据。哪个服务列不出模型、接口不支持或 Key 文件读不到,就不添加那一个,其他的与原来的模型照旧。
122
+ 已有的 inline 配置支持 JSONC(注释、尾逗号),合并时保留用户自己的变量与字符串转义;服务生成片段中的 `{` 在 JSON 字符串内编码成 `\u007b`,
123
+ 防止模型 ID、标题等服务数据触发 OpenCode 的 `{env:…}` / `{file:…}` 展开。真正无效的配置仍明确报错。机器与项目的 Provider 启停限制也仍然有效。
124
+ 用户未加引号的布尔值、数字等变量节点在解析时暂用等长占位节点,合并后原样还原,由 OpenCode 自己展开;合并器不读取它们引用的环境变量或文件。
125
+ `"provider":{env:VAR}` 也可由环境变量提供整个 Provider 对象:合并器只读取该对象的结构,结果放进子进程独有的变量,
126
+ 仍交给 OpenCode 做一次环境变量展开,再处理文件模板。原变量及其他配置入口保持不变;对象里的相对文件引用仍以原生会话目录为基准。
127
+ `/models` 没有上下文与输出上限时暂用 32768 / 4096 tokens,
128
+ 服务给出 `context_length` / `max_output_tokens` 时用它们;不根据模型名猜测图片与推理能力。
129
+ 真 CLI 的本地验证(不使用实际模型服务与登录):`OPENCODE_TEST_BINARY=/absolute/path/to/opencode node --test test/plugins/opencode/native.test.mjs`,
130
+ 覆盖三种接口的请求、服务与原生模型切换、进程重启后恢复,服务模型的变量模板、用户 JSONC 和 Provider 对象模板配置,以及主会话与子 Agent 里长短两种 `Retry-After`。
131
+
65
132
  ### Grok 审批模式
66
133
 
67
134
  Grok 插件通过 ACP 提供原来的「自动审批」「全部放行」两个选项;新会话未选择时沿用运行机配置。
@@ -135,8 +202,8 @@ Coda 按这个通用类别显示「额度不足」,不触发服务故障重试
135
202
  Codex 会话、面板与登录用的 Codex CLI 就是 `codex` 插件依赖的 `@openai/codex`;Claude 的是 `claude` 插件依赖的
136
203
  `@anthropic-ai/claude-agent-sdk`。插件按包名从自己的依赖里找它们,Coda 不另给路径。升级就是改这两个版本:
137
204
  Dependabot(`.github/dependabot.yml`)每周为它们各开一个 PR,连同 `package-lock.json` 一起改;合并后在 Coda「设置 › 插件」
138
- 对该插件点「检查更新」,Coda 随后把依赖升到发布的新版本。升 `@openai/codex` 时 codex-acp 经 `overrides` 跟着用新版本,
139
- Coda 升 `@coda-console/codex` 时也要改它根上的同一条 overrides(npm 只认根的,Coda 的测试会核对);合并前用真实会话跑一轮。
205
+ 对该插件点「检查更新」;合并时勾选「部署」,`sync-coda` 给 Coda 开升级依赖的 PR。升 `@openai/codex` 时 codex-acp 经 `overrides` 跟着用新版本,
206
+ Coda 根上的同一条 overrides 由 `sync-coda` 一起改(npm 只认根的,Coda 的测试会核对);合并前用真实会话跑一轮。
140
207
 
141
208
  ## 在 Coda 里安装
142
209
 
@@ -305,7 +372,7 @@ CI 先装各插件的依赖再跑全部测试,`plugins/*` 只查清单:真
305
372
 
306
373
  MIT,见 [LICENSE](LICENSE)。各插件的品牌图形归各自权利人所有,来源写在插件的 `icons/README.md`。
307
374
 
308
- `plugins/codex/agent/` 里的 `relay.mjs`、`titles.mjs`、`network.mjs`、`process-exit.mjs`、`options.mjs`、`subagents.mjs`、`instructions.mjs`、`steering.mjs` 在加载时给锁定版本的
375
+ `plugins/codex/agent/` 里的 `relay.mjs`、`titles.mjs`、`network.mjs`、`process-exit.mjs`、`options.mjs`、`subagents.mjs`、`instructions.mjs`、`steering.mjs`、`providers.mjs` 在加载时给锁定版本的
309
376
  [codex-acp](https://github.com/agentclientprotocol/codex-acp)(Apache-2.0,Copyright 2025 JetBrains s.r.o.)打补丁,补丁里含 codex-acp 构建产物的原文片段。
310
377
  这些片段按 [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) 使用,许可全文也随 npm 包装在 `node_modules/@agentclientprotocol/codex-acp/LICENSE`;
311
378
  codex-acp 本身作为 npm 依赖安装,不在本仓库里。
package/check/coda.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
6
6
 
7
7
  // The Coda commit these rules were copied from (an empty current mode is none chosen).
8
- export const CODA_COMMIT = '52831f0e2a09b16187f254ad2d19e270723bdc3b';
8
+ export const CODA_COMMIT = 'a52459b1f950957923a6e187075ba981263279f4';
9
9
  // The ACP version Coda speaks: the SDK's (backend/package.json pins @agentclientprotocol/sdk 1.4.0, as package.json here).
10
10
  export const ACP_PROTOCOL_VERSION = PROTOCOL_VERSION;
11
11
 
@@ -76,13 +76,14 @@ export const TERM_GRACE = 800;
76
76
  // adapters/acp.mjs `open` and application/option-cache.mjs: opening a session (initialize, session/new or session/load) may take this.
77
77
  export const OPEN_LIMIT = 60_000;
78
78
 
79
- // ---- What every process Coda runs for a plugin is given (backend/src/domain/plugins.mjs pluginEnv, cleared) ----
79
+ // ---- What every process Coda runs for a plugin is given (backend/src/components/plugins/plugin-rules.mjs pluginEnv, cleared) ----
80
80
  // Its agents', its panels' and the commands it brings alike: Coda's own Node, the plugin's directory, its state, its settings,
81
81
  // what is plugged into its points (none here: the checker installs nothing beside it), where the plugins it depends on are
82
82
  // (`dependencies`, by ID), and CODA_ARBITER only for an instance's own backend, which the checker is not. What a process
83
- // inherits is cleared of them first: it never takes another plugin's for its own.
84
- export const PLUGIN_VARIABLES = ['CODA_NODE', 'CODA_ARBITER', 'CODA_PLUGIN_PATH', 'CODA_PLUGIN_STATE', 'CODA_PLUGIN_SETTINGS', 'CODA_PLUGIN_EXTENSIONS',
85
- 'CODA_PLUGIN_DEPENDENCIES'];
83
+ // inherits is cleared of them first, those it is given only sometimes too (its secrets' files, the model services, the PATH
84
+ // without Coda's directories): it never takes another plugin's for its own.
85
+ export const PLUGIN_VARIABLES = ['CODA_NODE', 'CODA_ARBITER', 'CODA_PLUGIN_PATH', 'CODA_PLUGIN_STATE', 'CODA_PLUGIN_SETTINGS', 'CODA_PLUGIN_SECRETS',
86
+ 'CODA_PLUGIN_EXTENSIONS', 'CODA_PLUGIN_DEPENDENCIES', 'CODA_MODEL_PROVIDERS', 'CODA_USER_PATH'];
86
87
  export const pluginEnv = ({ pluginDir, stateDir, settings = {}, dependencies = {} }) => ({ CODA_NODE: process.execPath, CODA_PLUGIN_PATH: pluginDir,
87
88
  CODA_PLUGIN_STATE: stateDir, CODA_PLUGIN_SETTINGS: JSON.stringify(settings),
88
89
  ...(Object.keys(dependencies).length ? { CODA_PLUGIN_DEPENDENCIES: JSON.stringify(dependencies) } : {}) });
@@ -100,6 +101,11 @@ export const AGENT_ID = /^[a-z][a-z0-9-]{0,39}$/;
100
101
  export const PANEL_ID = /^[a-z][a-z0-9-]{0,63}$/;
101
102
  export const POINT = /^[a-z][a-z0-9-]{0,39}$/;
102
103
  export const SETTING_KEY = /^[a-z][a-z0-9-]*(\.[a-z][A-Za-z0-9]*)+$/;
104
+ // The string formats a setting may have, beside `coda-agent` and `coda-agent-match` (backend/src/components/plugins/plugin-rules.mjs
105
+ // STRING_FORMATS): a duration, a repository, a directory of the machine, a model service another plugin provides
106
+ // (`<插件>/<服务>`, MODEL_PROVIDER), and the address of one.
107
+ export const STRING_FORMATS = ['duration', 'coda-repo', 'coda-directory', 'coda-model-provider', 'coda-model-url'];
108
+ export const MODEL_PROVIDER = /^[a-z][a-z0-9-]{0,39}\/[a-z][a-z0-9-]{0,39}$/;
103
109
  export const CAPABILITY = /^[a-z][a-z0-9-]{0,63}$/;
104
110
  // What a plugin may contribute (backend/src/components/plugins/plugin-rules.mjs CONTRIBUTES); anything else is refused.
105
111
  export const CONTRIBUTES = ['configuration', 'capabilities', 'instructions', 'panels', 'agents', 'extensionPoints', 'extensions', 'groups', 'events', 'commands', 'gitCredentials',
@@ -9,9 +9,11 @@ import { basename, isAbsolute, join, normalize, relative, sep } from 'node:path'
9
9
  import { inflateSync } from 'node:zlib';
10
10
  import { AGENT_ID, AGENT_LIMITS, BUILTIN_AGENTS, BUILTIN_COMMANDS, BUILTIN_GROUPS, BUILTIN_HERE, BUILTIN_SCOPE, CAPABILITY,
11
11
  CODA_COMMANDS, CODA_EVENT_DOMAINS, COMMAND, COMMAND_LIMIT, BUILTIN_ONLY, CONTRIBUTES, EVENT, EVENT_LIMIT, DEPTH, ICON_LIMIT, INSTRUCTIONS_LIMIT, NPM, NPM_LOCK, PANEL_ID, PANEL_LIMITS, PLUGIN_ID, POINT,
12
- SEGMENT, SERVICE, SETTING_KEY, SETUP_KEYS, SIZE_LIMIT } from './coda.mjs';
12
+ MODEL_PROVIDER, SEGMENT, SERVICE, SETTING_KEY, SETUP_KEYS, SIZE_LIMIT, STRING_FORMATS } from './coda.mjs';
13
13
 
14
14
  class Invalid extends Error {}
15
+ // The categories Coda's own plugins use, which its plugin pages list first, in this order.
16
+ export const CATEGORIES = ['Agent', '模型与账号', '工作流程', '机器与资源', 'Skills 与工具'];
15
17
  const check = (condition, message) => { if (!condition) throw new Invalid(message); };
16
18
  const object = (value, where) => check(value && typeof value === 'object' && !Array.isArray(value), `${where} 须为对象`);
17
19
  const keys = (value, allowed, where) => {
@@ -52,12 +54,22 @@ function schema(value, where, entry = false) {
52
54
  const agent = value.format === 'coda-agent';
53
55
  check(['boolean', 'integer', 'number', 'string', 'object', 'array'].includes(value.type), `${where} 的 type 不受支持:${value.type}`);
54
56
  check((value.items === undefined && value.uniqueItems === undefined) || value.type === 'array', `${where}:只有多选(type: array)才写 items 与 uniqueItems`);
55
- check(value.format === undefined || agent || value.format === 'coda-agent-match' || value.format === 'duration', `${where} 的 format 不受支持:${value.format}`);
57
+ check(value.format === undefined || agent || value.format === 'coda-agent-match' || STRING_FORMATS.includes(value.format), `${where} 的 format 不受支持:${value.format}`);
56
58
  check(!agent || value.type === 'object', `${where}:coda-agent 是对象`);
57
- check(value.format !== 'duration' || value.type === 'string', `${where}:duration 是文本`);
59
+ check(!STRING_FORMATS.includes(value.format) || value.type === 'string', `${where}:${value.format} 是文本`);
58
60
  check(value.enum === undefined || (Array.isArray(value.enum) && value.enum.length && ['string', 'integer', 'number'].includes(value.type)), `${where} 的 enum 无效`);
59
61
  const out = { type: value.type, format: value.format, enum: value.enum, minimum: value.minimum, maximum: value.maximum };
60
- if (value.type === 'array') {
62
+ if (value.type === 'array' && value.items?.format === 'coda-model-provider') {
63
+ // Several model services (plugin-rules.mjs `schema`): `<plugin>/<id>` each; `modelApis` the interfaces the plugin takes.
64
+ const items = value.items;
65
+ check(value.format === undefined && value.minimum === undefined && value.maximum === undefined, `${where}:多个模型服务不写 format、minimum 与 maximum`);
66
+ check(value.uniqueItems === undefined || value.uniqueItems === true, `${where}:多个模型服务的 uniqueItems 只能是 true`);
67
+ check(Object.keys(items).every(k => ['type', 'format', 'modelApis'].includes(k)) && items.type === 'string',
68
+ `${where}:多个模型服务写 items: { type: string, format: coda-model-provider, modelApis }`);
69
+ check(items.modelApis === undefined || (Array.isArray(items.modelApis) && items.modelApis.length >= 1 && items.modelApis.length <= 10
70
+ && items.modelApis.every(api => typeof api === 'string' && COMMAND.test(api))), `${where}.items.modelApis 须为 1–10 个接口名,如 anthropic-messages`);
71
+ out.format = 'coda-model-provider';
72
+ } else if (value.type === 'array') {
61
73
  check(value.format === undefined && value.minimum === undefined && value.maximum === undefined, `${where}:多选不写 format、minimum 与 maximum`);
62
74
  check(value.uniqueItems === undefined || value.uniqueItems === true, `${where}:多选的 uniqueItems 只能是 true(每项至多选一次)`);
63
75
  const items = value.items, options = items?.anyOf;
@@ -93,6 +105,10 @@ function checkValue(spec, value, where) {
93
105
  check(value && typeof value === 'object' && !Array.isArray(value) && typeof value.agent === 'string' && AGENT_ID.test(value.agent), `${where} 须为 Agent 组合,并指明 Agent`);
94
106
  return;
95
107
  }
108
+ if (spec.type === 'array' && spec.format === 'coda-model-provider') {
109
+ check(Array.isArray(value) && value.length <= 20 && value.every(v => typeof v === 'string' && MODEL_PROVIDER.test(v)), `${where}须为至多 20 个「插件/服务」,如 cliproxy/cliproxy`);
110
+ return;
111
+ }
96
112
  if (spec.type === 'array') {
97
113
  check(Array.isArray(value) && value.every(v => typeof v === 'string'), `${where}须为选中的几项`);
98
114
  const unknown = value.filter(v => !spec.options.includes(v));
@@ -106,6 +122,9 @@ function checkValue(spec, value, where) {
106
122
  } else if (spec.type === 'string') {
107
123
  check(typeof value === 'string' && value.length <= 20000 && !value.includes('\0'), `${where}须为文本`);
108
124
  check(spec.format !== 'duration' || isDuration(value), `${where}须为 ISO 8601 时长,如 P30D、PT6H`);
125
+ // A repository's address Coda reads as it installs from one; here only the directory and the model service.
126
+ check(spec.format !== 'coda-directory' || value === '' || value.startsWith('/'), `${where}须为绝对目录`);
127
+ check(spec.format !== 'coda-model-provider' || value === '' || MODEL_PROVIDER.test(value), `${where}须为「插件/服务」,如 cliproxy/cliproxy`);
109
128
  } else if (spec.type === 'object') {
110
129
  object(value, where);
111
130
  for (const [k, v] of Object.entries(value)) {
@@ -278,6 +297,14 @@ export function checkManifest(dir, say) {
278
297
  return manifest.contributes;
279
298
  }) ?? {};
280
299
  if (typeof manifest.version !== 'string') say.warn('manifest.version', 'package.json 没有 version:Coda 只认提交,version 只作显示,但安装确认里会空着');
300
+ // Coda's plugin pages group plugins by the first of their `categories` (Coda's docs/plugins.md「商店」), as VS Code writes them.
301
+ part('manifest.categories', () => {
302
+ check(manifest.categories === undefined || (Array.isArray(manifest.categories) && manifest.categories.every(c => typeof c === 'string')), 'categories 须为文本的数组');
303
+ const category = manifest.categories?.[0]?.trim();
304
+ if (!category) say.warn('manifest.categories', `package.json 没有 categories:Coda 的插件页把它归「其他」;分类用 ${CATEGORIES.join('、')} 之一`);
305
+ else if (!CATEGORIES.includes(category)) say.warn('manifest.categories', `分类「${category}」不是 Coda 自己的插件用的(${CATEGORIES.join('、')}):插件页把它排在这些之后`);
306
+ return category;
307
+ }, category => category && `分类 ${category}`);
281
308
  const name = id ?? String(declaredId);
282
309
 
283
310
  const settings = part('manifest.settings', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coda-console/plugin-check",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Coda 插件的契约检查:扮演 Coda 读清单、启动 Agent 讲 ACP 与 _coda/wait、读面板。",
5
5
  "license": "MIT",
6
6
  "repository": {