@coda-console/plugin-check 0.5.6 → 0.5.8

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,6 +11,7 @@ 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/grok-bot/ Grok Bot:把 xAI 云端 Bot 接成长期会话,Webhook 消息、定时巡检与用户审批;依赖 Coda 远端 Agent 入口
14
15
  plugins/opencode/ OpenCode:在运行机的 opencode acp 与 Coda 之间转发,送入会话指令与所选模型服务,模型按来源分组
15
16
  plugins/codex-accounts/ Codex 多账号:管理登录、自动续期与账号选择,接管 Codex 额度面板
16
17
  plugins/cursor/ Cursor:在运行机的 cursor-agent acp 与 Coda 之间转发,送入会话指令
@@ -19,6 +20,7 @@ plugins/magpie/ Magpie:把本机的 Magpie 网关作为模型服务
19
20
  plugins/network-log/ 网络记录:只记录不拦截,每秒读连接表记下建连卡住的连接(进程、对端、网卡、mihomo 有没有收到),每分钟分段探测常用服务
20
21
  plugins/review/ PR 生命流程:会话开的 PR 先跑项目的检查、再交叉评审,你批准后合并、部署与发布;也是写 PR 流程的样例
21
22
  plugins/testbed/ 测试机:长插在 Mac 上的 iPhone、iPad 只经 use-testbed 使用,排到后装 App、交给会话操作、用完切黑屏;依赖宿主资源
23
+ plugins/ultra/ Ultra:输入框的 Ultra 模式,主 Agent 主动把能并行的活按名单派给子会话,汇总后验证;名单与模板在模式参数和设置里
22
24
  templates/agent/ 最小的 Agent 插件:手写 ACP 的回声 Agent,演示会话指令、插话、Agent 活动与自主轮次
23
25
  templates/panel/ 最小的面板插件:一张卡片、一条进度条、一个按钮
24
26
  bin/、check/ 契约检查(仓库根的 package.json 就是它,@coda-console/plugin-check);check/coda/ 是 Coda 同步来的规则副本,不要手改
@@ -125,6 +125,155 @@
125
125
  }
126
126
  ]
127
127
  },
128
+ {
129
+ "id": "composer",
130
+ "displayName": "输入框模式",
131
+ "description": "输入框的三种模式:交接到新会话、新建子会话,以及打 ! 执行命令。停用后输入框里没有这几种模式。",
132
+ "version": "1.1.0",
133
+ "category": "工作流程",
134
+ "source": {
135
+ "kind": "builtin"
136
+ },
137
+ "dir": "src/resources/plugins/composer",
138
+ "title": "输入框模式",
139
+ "properties": {},
140
+ "main": "main.mjs",
141
+ "workflows": [],
142
+ "capabilities": [],
143
+ "services": [],
144
+ "groups": [],
145
+ "commands": [],
146
+ "gitCredentials": [],
147
+ "endpoints": [],
148
+ "views": [],
149
+ "composerModes": [
150
+ {
151
+ "id": "handoff",
152
+ "title": "交接到新会话",
153
+ "icon": "arrow.right.square",
154
+ "hint": "新会话承接这里的上下文,共享当前工作目录及其中的文件;这个会话保留,仍可继续使用。",
155
+ "placeholder": "接下来要完成什么?",
156
+ "sendLabel": "交接",
157
+ "agent": "own",
158
+ "in": [
159
+ "session"
160
+ ]
161
+ },
162
+ {
163
+ "id": "subagent",
164
+ "title": "子会话",
165
+ "icon": "arrow.triangle.branch",
166
+ "hint": "承接父会话近期上下文,沿用父会话工作目录;只启动子会话,不向父 Agent 发送这段要求。",
167
+ "placeholder": "描述交给子会话的任务…",
168
+ "sendLabel": "启动子会话",
169
+ "agent": "own",
170
+ "in": [
171
+ "session"
172
+ ],
173
+ "parameters": {
174
+ "properties": {
175
+ "replyTo": {
176
+ "type": "string",
177
+ "enum": [
178
+ "parent",
179
+ "sender"
180
+ ],
181
+ "enumItemLabels": [
182
+ "完成后回报父会话",
183
+ "完成后回报你"
184
+ ],
185
+ "enumDescriptions": [
186
+ "结果交给父 Agent,由它接着处理",
187
+ "做完提醒你,不叫醒父 Agent"
188
+ ],
189
+ "title": "回报对象",
190
+ "default": "parent",
191
+ "order": 0
192
+ }
193
+ }
194
+ },
195
+ "schema": {
196
+ "type": "object",
197
+ "properties": {
198
+ "replyTo": {
199
+ "title": "回报对象",
200
+ "type": "string",
201
+ "enum": [
202
+ "parent",
203
+ "sender"
204
+ ],
205
+ "enumItemLabels": [
206
+ "完成后回报父会话",
207
+ "完成后回报你"
208
+ ],
209
+ "enumDescriptions": [
210
+ "结果交给父 Agent,由它接着处理",
211
+ "做完提醒你,不叫醒父 Agent"
212
+ ],
213
+ "default": "parent"
214
+ }
215
+ }
216
+ }
217
+ },
218
+ {
219
+ "id": "shell",
220
+ "title": "执行命令",
221
+ "icon": "terminal",
222
+ "hint": "在会话工作目录执行,命令和结果作为这条消息发给 Agent。",
223
+ "placeholder": "输入要执行的命令…",
224
+ "sendLabel": "执行",
225
+ "agent": "off",
226
+ "trigger": "!",
227
+ "in": [
228
+ "session",
229
+ "new"
230
+ ]
231
+ }
232
+ ],
233
+ "agents": [],
234
+ "dependencies": [],
235
+ "extensionPoints": {},
236
+ "extensions": []
237
+ },
238
+ {
239
+ "id": "diagnostics",
240
+ "displayName": "机器诊断",
241
+ "description": "给巡检会话提供只读工具,查看空间各台机器的健康状态、网络诊断与客户端性能。需要时安装,再按目录启用或用 Tag 限定。",
242
+ "version": "1.0.0",
243
+ "category": "机器与资源",
244
+ "source": {
245
+ "kind": "builtin"
246
+ },
247
+ "dir": "src/resources/plugins/diagnostics",
248
+ "title": "机器诊断",
249
+ "properties": {},
250
+ "workflows": [],
251
+ "capabilities": [],
252
+ "services": [],
253
+ "groups": [],
254
+ "commands": [],
255
+ "gitCredentials": [],
256
+ "endpoints": [],
257
+ "views": [],
258
+ "agents": [],
259
+ "dependencies": [
260
+ "space"
261
+ ],
262
+ "extensionPoints": {},
263
+ "extensions": [],
264
+ "mcpServers": [
265
+ {
266
+ "name": "diagnostics",
267
+ "command": "${CODA_NODE}",
268
+ "args": [
269
+ "${CODA_PLUGIN_PATH}/mcp.mjs",
270
+ "--state",
271
+ "${CODA_PLUGIN_STATE}"
272
+ ],
273
+ "env": []
274
+ }
275
+ ]
276
+ },
128
277
  {
129
278
  "id": "github",
130
279
  "displayName": "GitHub",
@@ -442,7 +591,7 @@
442
591
  {
443
592
  "id": "session",
444
593
  "displayName": "会话生命周期",
445
- "description": "所有会话共用:存续期间管理空闲进程和任务转交,归档宽限后收尾,按设置回收托管工作树。停用后不自动执行这些政策。",
594
+ "description": "所有会话共用:存续期间管理空闲进程和任务转交,归档宽限后收尾,按设置回收托管工作树,限定同时执行的子会话。停用后不自动执行这些政策。",
446
595
  "version": "1.0.0",
447
596
  "category": "工作流程",
448
597
  "source": {
@@ -488,6 +637,15 @@
488
637
  "title": "Agent 停下多久后转给你",
489
638
  "order": 9,
490
639
  "default": "PT2M"
640
+ },
641
+ "session.subagents": {
642
+ "type": "integer",
643
+ "minimum": 1,
644
+ "maximum": 64,
645
+ "description": "Agent 开的子会话为父会话干活时各占一个名额;满了再开会当场失败,Agent 等其中一个做完再开,或者自己做。按这台机器算。",
646
+ "title": "同时执行的子会话",
647
+ "order": 10,
648
+ "default": 5
491
649
  }
492
650
  },
493
651
  "workflows": [
@@ -495,7 +653,12 @@
495
653
  ],
496
654
  "capabilities": [],
497
655
  "services": [],
498
- "groups": [],
656
+ "groups": [
657
+ {
658
+ "name": "subagents",
659
+ "capacity": "session.subagents"
660
+ }
661
+ ],
499
662
  "commands": [],
500
663
  "gitCredentials": [],
501
664
  "endpoints": [],
@@ -51,7 +51,7 @@ const PACKAGE_ROOT = fileURLToPath(new URL('../../../', import.meta.url));
51
51
  // What a plugin may contribute to Coda itself; anything else in `contributes` is for a point another plugin declares to
52
52
  // take it (the resources plugin's `panels`), refused when none does, as any other way of writing Coda cannot read.
53
53
  export const CONTRIBUTES = ['configuration', 'capabilities', 'instructions', 'agents', 'extensionPoints', 'extensions', 'groups', 'events', 'commands', 'gitCredentials',
54
- 'repositories', 'services', 'setup', 'skills', 'environment', 'path', 'endpoints', 'mcpServers', 'views', 'modelProviders', 'executors'];
54
+ 'repositories', 'services', 'setup', 'skills', 'environment', 'path', 'endpoints', 'mcpServers', 'views', 'modelProviders', 'executors', 'composerModes'];
55
55
  // The string formats a setting may have, beside `coda-agent` (an object) and `coda-agent-match` (a table's row names):
56
56
  // `coda-model-provider` names a model service another plugin provides (docs/plugins.md「模型服务」); `coda-model-url` is the
57
57
  // address of one, its Key and API in fields beside it (`modelKey`, `modelApi`), which Coda checks (「检查可用模型」).
@@ -81,19 +81,22 @@ const SYMBOL = /^[a-z0-9]+(\.[a-z0-9]+)*$/;
81
81
  // ---- Declarations ----
82
82
  // The JSON Schema Coda reads: a scalar, a choice of several among fixed options (an array of `items.anyOf` constants, as a
83
83
  // form's multi-select), several model services (an array of `items` of format `coda-model-provider`, which the
84
- // machine's model services are offered for), or an object whose entries all share one schema (VS Code shows such a setting as a table): a single
85
- // value, an Agent combination, or a row of named columns, each a single value, secrets among them (docs/plugins.md「类型」). Anything else is refused when the plugin is read, not ignored. `level`: what is read, a
86
- // setting, a table's entry, or a column of its rows.
84
+ // machine's model services are offered for), a list of rows (an array of `items` an object of named columns), or an object
85
+ // whose entries all share one schema (VS Code shows such a setting as a table): a single value, an Agent combination, or a
86
+ // row of named columns, each a single value or an Agent combination, secrets among them (docs/plugins.md「类型」). Anything
87
+ // else is refused when the plugin is read, not ignored. `level`: what is read, a setting, a table's entry, or a column of its rows.
87
88
  const COLUMN = /^[a-z][A-Za-z0-9]{0,39}$/, OPTION = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
89
+ // How many rows a list of them holds.
90
+ const ROWS_AT_MOST = 50;
88
91
  function schema(value, where, level = 'setting') {
89
92
  check(value && typeof value === 'object' && !Array.isArray(value), 'invalid_plugin', `${where} 须为 JSON Schema 对象`);
90
- const known = ['type', 'default', 'description', 'markdownDescription', 'title', 'enum', 'enumDescriptions', 'minimum', 'maximum', 'format', 'order',
93
+ const known = ['type', 'default', 'description', 'markdownDescription', 'title', 'enum', 'enumDescriptions', 'enumItemLabels', 'minimum', 'maximum', 'format', 'order',
91
94
  ...{ setting: ['additionalProperties', 'propertyNames', 'writeOnly', 'modelKey', 'modelApi', 'items', 'uniqueItems'], entry: ['properties', 'required'],
92
95
  column: ['writeOnly', 'modelKey', 'modelApi'] }[level]];
93
- check(level !== 'column' || value.type !== 'object', 'invalid_plugin', `${where}:一列只能是单个值`);
96
+ check(level !== 'column' || value.type !== 'object' || value.format === 'coda-agent', 'invalid_plugin', `${where}:一列只能是单个值或 Agent 组合`);
94
97
  check(level !== 'entry' || value.type !== 'object' || value.format === 'coda-agent' || value.properties !== undefined, 'invalid_plugin',
95
98
  `${where}:表格的每一项只能是单个值、Agent 组合,或 properties 写明的 1–20 列`);
96
- check(value.type !== 'array' || level === 'setting', 'invalid_plugin', `${where}:多选只能是插件的一项设置,不能放进表格`);
99
+ check(value.type !== 'array' || level === 'setting', 'invalid_plugin', `${where}:多选与一列行只能是插件的一项设置,不能放进表格`);
97
100
  const extra = Object.keys(value).filter(k => !known.includes(k));
98
101
  check(!extra.length, 'invalid_plugin', `${where} 里有 Coda 不支持的键:${extra.join('、')}`);
99
102
  check((value.properties === undefined && value.required === undefined) || (value.type === 'object' && value.format === undefined), 'invalid_plugin',
@@ -104,14 +107,20 @@ function schema(value, where, level = 'setting') {
104
107
  'invalid_plugin', `${where}:密文设置(writeOnly)只能是没有 format、enum 与默认值的文本`);
105
108
  const agent = value.format === 'coda-agent';
106
109
  check(['boolean', 'integer', 'number', 'string', 'object', 'array'].includes(value.type), 'invalid_plugin', `${where} 的 type 不受支持:${value.type}`);
107
- check((value.items === undefined && value.uniqueItems === undefined) || value.type === 'array', 'invalid_plugin', `${where}:只有多选(type: array)才写 items 与 uniqueItems`);
110
+ check((value.items === undefined && value.uniqueItems === undefined) || value.type === 'array', 'invalid_plugin', `${where}:只有多选与一列行(type: array)才写 items 与 uniqueItems`);
108
111
  check(value.format === undefined || agent || value.format === 'coda-agent-match' || STRING_FORMATS.includes(value.format), 'invalid_plugin', `${where} 的 format 不受支持:${value.format}`);
109
112
  check(!agent || value.type === 'object', 'invalid_plugin', `${where}:coda-agent 是对象`);
110
113
  check(!STRING_FORMATS.includes(value.format) || value.type === 'string', 'invalid_plugin', `${where}:${value.format} 是文本`);
111
114
  check(value.enum === undefined || (Array.isArray(value.enum) && value.enum.length && ['string', 'integer', 'number'].includes(value.type)), 'invalid_plugin', `${where} 的 enum 无效`);
115
+ // What each option is shown as, as VS Code's settings say it.
116
+ check(value.enumItemLabels === undefined || (Array.isArray(value.enum) && Array.isArray(value.enumItemLabels) && value.enumItemLabels.length === value.enum.length
117
+ && value.enumItemLabels.every(label => typeof label === 'string' && label.length <= 100)), 'invalid_plugin', `${where} 的 enumItemLabels 须与 enum 一一对应`);
112
118
  check((value.modelKey === undefined && value.modelApi === undefined) || value.format === 'coda-model-url', 'invalid_plugin',
113
119
  `${where}:modelKey 与 modelApi 只写在 coda-model-url 的地址上`);
114
120
  const out = { type: value.type, ...(value.format ? { format: value.format } : {}), ...(value.enum ? { enum: value.enum } : {}),
121
+ ...(value.enumItemLabels ? { enumItemLabels: value.enumItemLabels } : {}),
122
+ ...(Array.isArray(value.enumDescriptions) && value.enum && value.enumDescriptions.length === value.enum.length && value.enumDescriptions.every(d => typeof d === 'string')
123
+ ? { enumDescriptions: value.enumDescriptions } : {}),
115
124
  ...(value.minimum !== undefined ? { minimum: value.minimum } : {}), ...(value.maximum !== undefined ? { maximum: value.maximum } : {}),
116
125
  ...(typeof value.description === 'string' ? { description: value.description } : typeof value.markdownDescription === 'string' ? { description: value.markdownDescription } : {}),
117
126
  ...(typeof value.title === 'string' ? { title: value.title } : {}), ...(Number.isFinite(value.order) ? { order: value.order } : {}),
@@ -129,6 +138,14 @@ function schema(value, where, level = 'setting') {
129
138
  && items.modelApis.every(isModelApi)), 'invalid_plugin', `${where}.items.modelApis 须为 1–10 个接口名,取 ${MODEL_APIS.join('、')}`);
130
139
  out.format = 'coda-model-provider';
131
140
  if (items.modelApis) out.modelApis = [...new Set(items.modelApis)];
141
+ } else if (value.type === 'array' && value.items?.type === 'object') {
142
+ // A list of rows, each of named columns as a table's row is, kept in the order written: what has no name of its own.
143
+ check(value.format === undefined && value.minimum === undefined && value.maximum === undefined && value.uniqueItems === undefined, 'invalid_plugin',
144
+ `${where}:一列行不写 format、minimum、maximum 与 uniqueItems`);
145
+ check(value.items.format === undefined && value.items.properties !== undefined, 'invalid_plugin', `${where}:一列行写 items: { type: object, properties, required }`);
146
+ out.rows = schema(value.items, `${where}.items`, 'entry');
147
+ // Taken whole, as a value is: a secret is written only column by column, as a table's row has it.
148
+ check(!Object.values(out.rows.columns).some(column => column.writeOnly), 'invalid_plugin', `${where}:一列行的列不能是密文,要密文用表格`);
132
149
  } else if (value.type === 'array') {
133
150
  // A choice of several: each option a constant of its own, with what the client shows for it; chosen ones kept in this order.
134
151
  check(value.format === undefined && value.minimum === undefined && value.maximum === undefined, 'invalid_plugin', `${where}:多选不写 format、minimum 与 maximum`);
@@ -184,6 +201,76 @@ function modelFields(fields, where) {
184
201
  }
185
202
  }
186
203
 
204
+ // The modes a composer can be put in (docs/plugins.md「输入框模式」): what it shows then — an icon and a title, a line on
205
+ // what sending does, its placeholder and send button —, whom what is sent is for (`agent`: the session's agent, `own` one
206
+ // chosen for it, which a new session runs, `off` none shown), and its parameters, written as settings are. One entered by a
207
+ // character typed first (`trigger`, as `!`) is the text's: it has no parameters, and what is sent in it is the session's.
208
+ // Which composers offer it (`in`) is its own to say: a session's (`session`), and the one of a session not started yet
209
+ // (`new`, the new task's), where what is sent starts that session in it; one whose messages a new session takes is sent
210
+ // from a session, so only a session's.
211
+ export const MODE_AGENTS = ['session', 'own', 'off'];
212
+ // What a plugin tells a session's agent at once (docs/session-contracts.md「顺带告诉」): a text, at most this long.
213
+ const TELL_LIMIT = 4000;
214
+ export const checkTold = text => check(typeof text === 'string' && text.trim() && text.length <= TELL_LIMIT && !text.includes('\0'), 'invalid_input',
215
+ `要告诉的话须为不超过 ${TELL_LIMIT} 字的文字`);
216
+ export const MODE_PLACES = ['session', 'new'];
217
+ const MODE_KEYS = ['id', 'title', 'icon', 'hint', 'placeholder', 'sendLabel', 'agent', 'trigger', 'in', 'parameters'];
218
+ const oneLine = (text, most) => typeof text === 'string' && !!text.trim() && [...text].length <= most && !/[\0\r\n]/.test(text);
219
+ function composerModeDeclarations(value) {
220
+ const modes = value ?? [];
221
+ check(Array.isArray(modes) && modes.length <= 10, 'invalid_plugin', 'contributes.composerModes 须为数组,至多 10 个');
222
+ const out = modes.map((m, i) => {
223
+ const where = `contributes.composerModes[${i}]`;
224
+ check(m && typeof m === 'object' && !Array.isArray(m) && Object.keys(m).every(k => MODE_KEYS.includes(k)), 'invalid_plugin',
225
+ `${where} 只能写 ${MODE_KEYS.join('、')}`);
226
+ check(typeof m.id === 'string' && COMMAND.test(m.id), 'invalid_plugin', `${where}.id 是小写字母、数字和「-」,以字母开头,不超过 40 个字符`);
227
+ check(oneLine(m.title, 40), 'invalid_plugin', `${where}.title 是不超过 40 字的一行`);
228
+ check(typeof m.icon === 'string' && m.icon.length <= 100 && SYMBOL.test(m.icon), 'invalid_plugin', `${where}.icon 是 SF Symbol 的名字`);
229
+ check(m.hint === undefined || oneLine(m.hint, 200), 'invalid_plugin', `${where}.hint 是不超过 200 字的一行`);
230
+ check(m.placeholder === undefined || oneLine(m.placeholder, 40), 'invalid_plugin', `${where}.placeholder 是不超过 40 字的一行`);
231
+ check(m.sendLabel === undefined || oneLine(m.sendLabel, 20), 'invalid_plugin', `${where}.sendLabel 是不超过 20 字的一行`);
232
+ check(m.agent === undefined || MODE_AGENTS.includes(m.agent), 'invalid_plugin', `${where}.agent 只能是 ${MODE_AGENTS.join('、')}`);
233
+ check(m.trigger === undefined || (typeof m.trigger === 'string' && /^[!#$%&*+:;=?^|~]$/.test(m.trigger)), 'invalid_plugin',
234
+ `${where}.trigger 是一个标点:!#$%&*+:;=?^|~ 之一`);
235
+ check(m.trigger === undefined || (m.parameters === undefined && m.agent !== 'own'), 'invalid_plugin', `${where}:打字进入的模式(trigger)没有参数,也不另选 Agent`);
236
+ check(m.in === undefined || (Array.isArray(m.in) && m.in.length && m.in.every(place => MODE_PLACES.includes(place)) && new Set(m.in).size === m.in.length),
237
+ 'invalid_plugin', `${where}.in 是它出现的输入框,取 ${MODE_PLACES.join('、')},不重复`);
238
+ check(m.agent !== 'own' || !m.in?.includes('new'), 'invalid_plugin', `${where}:开新会话的模式(agent: own)要从会话里发出,不能在 new 里`);
239
+ return { id: m.id, title: m.title.trim(), icon: m.icon, ...(m.hint ? { hint: m.hint.trim() } : {}), ...(m.placeholder ? { placeholder: m.placeholder.trim() } : {}),
240
+ ...(m.sendLabel ? { sendLabel: m.sendLabel.trim() } : {}), agent: m.agent ?? 'session', ...(m.trigger ? { trigger: m.trigger } : {}),
241
+ in: m.in ? MODE_PLACES.filter(place => m.in.includes(place)) : ['session'],
242
+ // As read, which the clients show; as written (JSON Schema), which its hooks are given (components/composer-modes).
243
+ ...(m.parameters !== undefined ? { parameters: modeParameters(m.parameters, `${where}.parameters`), schema: structuredClone(m.parameters) } : {}) };
244
+ });
245
+ check(new Set(out.map(m => m.id)).size === out.length, 'invalid_plugin', 'contributes.composerModes 的 id 不能重复');
246
+ const triggers = out.filter(m => m.trigger).map(m => m.trigger);
247
+ check(new Set(triggers).size === triggers.length, 'invalid_plugin', 'contributes.composerModes 的 trigger 不能重复');
248
+ return out;
249
+ }
250
+ // A mode's parameters: an object of named properties, each read as a setting is but a secret; what it cannot go without, `required`.
251
+ export function modeParameters(value, where) {
252
+ check(value && typeof value === 'object' && !Array.isArray(value) && value.type === 'object' && Object.keys(value).every(k => ['type', 'properties', 'required'].includes(k)),
253
+ 'invalid_plugin', `${where} 须为 { type: object, properties, required? }`);
254
+ const properties = value.properties;
255
+ check(properties && typeof properties === 'object' && !Array.isArray(properties) && Object.keys(properties).length >= 1 && Object.keys(properties).length <= 20,
256
+ 'invalid_plugin', `${where}.properties 须为 1–20 个参数`);
257
+ // In the order written, which the clients show them in: JSON's objects keep none of their own there.
258
+ const ordered = (fields, at) => Object.fromEntries(Object.entries(fields).map(([name, spec], i) => [name, { ...spec, order: spec.order ?? at + i }]));
259
+ const out = Object.fromEntries(Object.entries(properties).map(([name, p], i) => {
260
+ check(COLUMN.test(name), 'invalid_plugin', `${where} 的参数名只用字母和数字,以小写字母开头:${name}`);
261
+ const read = schema(p, `${where}.properties.${name}`);
262
+ const rows = read.rows ? { rows: { ...read.rows, columns: ordered(read.rows.columns, 0) } } : {};
263
+ const spec = { ...read, ...rows, order: read.order ?? i };
264
+ const secret = s => s.writeOnly || Object.values(s.columns ?? s.rows?.columns ?? s.entries?.columns ?? {}).some(c => c.writeOnly);
265
+ check(!secret(spec), 'invalid_plugin', `${where}.properties.${name}:参数不能是密文`);
266
+ return [name, spec];
267
+ }));
268
+ const required = value.required ?? [];
269
+ check(Array.isArray(required) && required.every(name => typeof name === 'string' && Object.hasOwn(out, name)) && new Set(required).size === required.length,
270
+ 'invalid_plugin', `${where}.required 须为本模式的参数名,不重复`);
271
+ return { properties: out, ...(required.length ? { required: [...required] } : {}) };
272
+ }
273
+
187
274
  // The category it is listed under (docs/plugins.md「商店」): the first of its manifest's `categories`, as VS Code's are
188
275
  // written; empty when it names none. Not checked: readPlugin does that.
189
276
  export const manifestCategory = manifest => Array.isArray(manifest?.categories) && typeof manifest.categories[0] === 'string' ? manifest.categories[0].trim().slice(0, 32) : '';
@@ -288,6 +375,7 @@ export function readPlugin(dir, source) {
288
375
  && (v.readout === undefined || VIEW_READOUTS.includes(v.readout)) && (v.checkout === undefined || VIEW_CHECKOUTS.includes(v.checkout)))
289
376
  && new Set(views.map(v => v.id)).size === views.length, 'invalid_plugin',
290
377
  `contributes.views 须为 { id, title, entry, in?, icon?, handles? } 的数组,至多 10 个:id 是小写字母、数字和「-」,以字母开头,不重复;title 是不超过 40 字的一行;entry 是插件目录里的 .html 文件;in 是它常驻的位置(${VIEW_PLACES.join('、')}),不重复;icon 是 SF Symbol 的名字;handles 是它接管打开的引用种类(如 workflow-runs),至多 8 个,不重复;readout 是它的读数(${VIEW_READOUTS.join('、')});checkout 是它只给哪种工作位置的会话(${VIEW_CHECKOUTS.join('、')})`);
378
+ const composerModes = composerModeDeclarations(manifest.contributes?.composerModes);
291
379
  const handled = views.flatMap(v => v.handles ?? []);
292
380
  check(new Set(handled).size === handled.length, 'invalid_plugin', 'contributes.views:同一种引用只能由一个视图接管');
293
381
  for (const v of views) {
@@ -337,6 +425,7 @@ export function readPlugin(dir, source) {
337
425
  views: views.map(v => ({ id: v.id, title: v.title, entry: posix.normalize(v.entry), ...(v.in?.length ? { in: [...v.in] } : {}), ...(v.icon ? { icon: v.icon } : {}),
338
426
  ...(v.handles?.length ? { handles: [...v.handles] } : {}), ...(v.readout ? { readout: v.readout } : {}),
339
427
  ...(v.checkout ? { checkout: v.checkout } : {}) })),
428
+ ...(composerModes.length ? { composerModes } : {}),
340
429
  // What it writes for another plugin's points, as written (components/plugins/plugin-extensions.mjs offerDeclarations).
341
430
  ...(Object.keys(offers).length ? { offers } : {}),
342
431
  agents: agentDeclarations(manifest.contributes?.agents, dir),
@@ -647,6 +736,16 @@ export function checkValue(spec, value, where, code = 'invalid_input') {
647
736
  }));
648
737
  }
649
738
  if (spec.type === 'boolean') { check(typeof value === 'boolean', code, `${where}须为开或关`); return value; }
739
+ // Rows in the order written, taken whole: a column not written is its default, as a table's row is resolved (rowValue),
740
+ // so that one a row cannot go without is there.
741
+ if (spec.type === 'array' && spec.rows) {
742
+ check(Array.isArray(value) && value.length <= ROWS_AT_MOST, code, `${where}须为至多 ${ROWS_AT_MOST} 行`);
743
+ const defaults = Object.entries(spec.rows.columns).filter(([, column]) => column.default !== undefined);
744
+ return value.map((row, i) => {
745
+ const checked = checkValue(spec.rows, row, `${where}第 ${i + 1} 行`, code);
746
+ return { ...Object.fromEntries(defaults.map(([name, column]) => [name, column.default])), ...checked };
747
+ });
748
+ }
650
749
  // Model services, each once, in the order chosen: one named may be of a plugin not installed (yet), as a single one may.
651
750
  if (spec.type === 'array' && spec.format === 'coda-model-provider') {
652
751
  check(Array.isArray(value) && value.length <= MODEL_PROVIDERS_AT_MOST && value.every(v => typeof v === 'string' && MODEL_PROVIDER.test(v)), code,
@@ -761,7 +860,7 @@ function read(item, spec, from) {
761
860
  // Of options chosen once, those the plugin has no more are not chosen; a value stored before the setting was a choice is none.
762
861
  if (spec.type === 'array' && spec.format === 'coda-model-provider')
763
862
  return Array.isArray(item.value) ? { value: item.value.filter(v => typeof v === 'string' && MODEL_PROVIDER.test(v)), from } : null;
764
- if (spec.type === 'array') return Array.isArray(item.value) ? { value: spec.options.map(o => o.value).filter(v => item.value.includes(v)), from } : null;
863
+ if (spec.type === 'array' && spec.options) return Array.isArray(item.value) ? { value: spec.options.map(o => o.value).filter(v => item.value.includes(v)), from } : null;
765
864
  return { value: item.value, from };
766
865
  }
767
866
  // Every setting of a plugin for one project (or for every project, without one): project over plugin over default.
@@ -254,11 +254,13 @@ export function builtinProblem(id, source, builtin, installed) {
254
254
  return `与内置插件 ${id} 同名${copy.source?.repo || copy.source?.package ? ',来源不同' : ''},请先卸载内置插件再安装`;
255
255
  }
256
256
  // What only one plugin may declare, as said to the user: an agent (sessions name it by its ID alone), a concurrency group
257
- // (any process takes it by its name), Git's credentials for an address, opening a kind of reference (a view's `handles`)
257
+ // (any process takes it by its name), Git's credentials for an address, opening a kind of reference (a view's `handles`),
258
+ // a composer mode's name and the character typed to enter one (the composer finds it by them)
258
259
  // and a key of `contributes` a point takes. A command, as a Skill or a variable, is a
259
260
  // project's: one two plugins of a project bring is neither's there (components/plugins/plugins.mjs launch).
260
261
  export const claims = plugin => [...plugin.agents.map(a => `Agent ${a.id}`), ...plugin.groups.map(g => `组 ${g.name}`),
261
262
  ...(plugin.gitCredentials ?? []).map(c => `${c.url} 的 Git 凭据`), ...(plugin.views ?? []).flatMap(v => (v.handles ?? []).map(kind => `打开 ${kind} 引用`)),
263
+ ...(plugin.composerModes ?? []).flatMap(m => [`输入框模式 /${m.id}`, ...(m.trigger ? [`输入框里打 ${m.trigger} 进入的模式`] : [])]),
262
264
  ...Object.values(plugin.extensionPoints ?? {}).filter(p => p.contributes).map(p => `扩展点 contributes.${p.contributes}`)];
263
265
  // Why a plugin cannot sit beside those this machine has (`catalog`: built-in and installed, by ID; `builtin`: the plugins
264
266
  // Coda ships, by ID), or null: an installed built-in one's ID from another source (builtinProblem), or an agent, a group
@@ -328,6 +330,8 @@ export function contributions(plugin, catalog = new Map()) {
328
330
  ...(plugin.views?.length ? { views: plugin.views.map(v => ({ id: v.id, title: v.title, ...(v.in ? { in: v.in } : {}), ...(v.icon ? { icon: v.icon } : {}), ...(v.handles ? { handles: v.handles } : {}), ...(v.readout ? { readout: v.readout } : {}),
329
331
  ...(v.checkout ? { checkout: v.checkout } : {}) })) } : {}),
330
332
  groups: plugin.groups.map(g => ({ name: g.name, capacity: g.capacity })),
333
+ // The modes it puts the composer in, as declared: the clients show them from this.
334
+ ...(plugin.composerModes?.length ? { composerModes: plugin.composerModes.map(({ schema: _, ...mode }) => mode) } : {}),
331
335
  extensions: plugin.extensions.map(e => ({ plugin: e.plugin, point: e.point, command: e.command })),
332
336
  // What it writes for points under keys of `contributes` (the resources plugin's panels): by point, how many and the
333
337
  // commands in them, each with what it is for.
@@ -42,11 +42,20 @@ export function quotes(value = []) {
42
42
  return passages;
43
43
  }
44
44
 
45
+ // The composer mode a message was sent in (docs/plugins.md「输入框模式」), as it was then: `<plugin>/<mode>`, its title and
46
+ // icon, which its bubble shows however the plugin changes later.
47
+ export function modeMark(value) {
48
+ object(value, '输入框模式');
49
+ keys(value, ['id', 'title', 'icon']);
50
+ check(typeof value.id === 'string' && /^[a-z][a-z0-9-]{0,39}\/[a-z][a-z0-9-]{0,39}$/.test(value.id), 'invalid_input', '输入框模式须写成「插件/模式」');
51
+ return { id: value.id, title: text(value.title, '模式名', 40), icon: text(value.icon, '模式图标', 100) };
52
+ }
53
+
45
54
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
46
55
  export function firstMessage(value) {
47
56
  object(value, '首次发送');
48
57
  keys(value, ['requestId', 'projectId', 'title', 'agent', 'checkout', 'settings', 'text', 'attachments', 'quotes', 'handoffSource', 'parentThreadId',
49
- 'replyTo', 'threadId', 'extensionParent']);
58
+ 'replyTo', 'threadId', 'extensionParent', 'mode']);
50
59
  const handoff = value.handoffSource === undefined ? {} : { handoffSource: text(value.handoffSource, '交接来源', 100) };
51
60
  const parent = value.parentThreadId === undefined ? {} : { parentThreadId: text(value.parentThreadId, '父会话', 100) };
52
61
  check(!parent.parentThreadId || handoff.handoffSource === parent.parentThreadId, 'invalid_input', '子会话必须引用父会话上下文');
@@ -96,6 +105,7 @@ export function firstMessage(value) {
96
105
  settings: settings(value.settings),
97
106
  text: message,
98
107
  ...(passages.length ? { quotes: passages } : {}),
108
+ ...(value.mode === undefined ? {} : { mode: modeMark(value.mode) }),
99
109
  }, attachments(value.attachments));
100
110
  }
101
111
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "commit": "93e1f9059276267bbf0aa4cd2979a98e199b6547",
2
+ "commit": "04fd85113f83823020febc7ffc647f10d15a2ddc",
3
3
  "packages": {
4
4
  "@0dep/piso": "5.2.0",
5
5
  "@agentclientprotocol/sdk": "1.4.0",
@@ -9,11 +9,11 @@
9
9
  "check/coda/components/plugins/agent-plugins.mjs": "23016dd0f7350a10f71565a2c44e2c1179299a93e8c859529071099f2ce862d1",
10
10
  "check/coda/components/plugins/model-protocols.mjs": "762244c01266cf61384377bdfbb84bef5ab157b1783f9254ddc1ae3d576a73c5",
11
11
  "check/coda/components/plugins/plugin-extensions.mjs": "e527800a109ec9849975357f841b0c05ecfa50c9d9c34e26d828aef3cb7d3cb3",
12
- "check/coda/components/plugins/plugin-rules.mjs": "78d9431c1b4f6354457f153bb74a0fd085c771eaadd9f7523633915cb700f4f9",
13
- "check/coda/components/plugins/plugin-source-rules.mjs": "d620ee62fb372e31e5389e27f7202308aab74324b6f5552ffef3844afc7d0001",
12
+ "check/coda/components/plugins/plugin-rules.mjs": "8cd1058fc96365f7f35451a4a75bd1c6434e0e036bb4b18f7c7fc37709e66c3e",
13
+ "check/coda/components/plugins/plugin-source-rules.mjs": "e03606bf3056061ee3a78494d34dae9dfbe35d6fb2f139880fab5891663cbedc",
14
14
  "check/coda/components/plugins/skill-document.mjs": "c5b5e815c42cd836572f7b60e22a46af6ac607fd9df0170fa36f955825636ce6",
15
15
  "check/coda/components/prompt-recovery/prompt-recovery.mjs": "f43f21d24c19a58113d3304f9248225b12080ab00ed15eab74bf7b94642732de",
16
- "check/coda/components/session-config/commands.mjs": "96247da877e455f835e0ee41622e531936e2600e911c2e9a4638fda00e987ec3",
16
+ "check/coda/components/session-config/commands.mjs": "2ccc0c089d88b3f76d9098876dc60bd7e06348931773da131732c0d6c6c7a6cf",
17
17
  "check/coda/components/workflows/executor-labels.mjs": "dceca6ae60c0a4f3c3cc72a6c84f1489831790774c93caab96e4ddb493bc89e1",
18
18
  "check/coda/domain/agent-protocol.mjs": "7337cf0af97a9592bbcad98a0e4f3837d8bb4d48b53c22846a8c56a0c88a4630",
19
19
  "check/coda/domain/duration.mjs": "15dd64ef9b02342ad521c66e5968a569432a89dc7568d11afd3e6f3fe37e104b",
@@ -24,7 +24,7 @@
24
24
  "check/coda/resources/plugins/resources/panels/panels.mjs": "aee040febb37fd6eebaf1a1da5b3299d2b6a9dfc982e649d9a628291af298179",
25
25
  "check/coda/resources/plugins/resources/panels/rules.mjs": "c024c5ede7c1516877128aa6d6796fbe5fd0e14642741b0745481bb1c5ca41d3",
26
26
  "check/coda/resources/plugins/resources/panels/sign-in.mjs": "1271a24e33b1b882a1fb9bee03b8e240db8b9bee1b5208f0e1c84499cfb1be7e",
27
- "check/coda/builtin.json": "0e04c5a9bb82db5573d865791df6fe9e52295be470b1bcdf0add719d2d2c0b85",
27
+ "check/coda/builtin.json": "add6de7df1f74456223275750a76919c983fd3eba2f4c0750077f7b998d9d3fc",
28
28
  "docs/agent-protocol.md": "2a40fac029e93bc512d0405105c9d74fa3e5a59e92788b6951dba182775875c1"
29
29
  }
30
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coda-console/plugin-check",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Coda 插件的契约检查:扮演 Coda 读清单、启动 Agent 讲 ACP 与 _coda/wait、读面板。",
5
5
  "license": "MIT",
6
6
  "repository": {