opencode-idea 0.0.22 → 0.0.23
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 +19 -18
- package/package.json +1 -1
- package/skills/idea-run-config-manager/SKILL.md +3 -2
- package/skills/idea-run-config-manager/assets/AGENTS.template.md +102 -37
- package/skills/idea-standalone/SKILL.md +0 -1
- package/src/ide-guidance.template.md +1 -1
- package/src/mcp/idea.js +1 -1
- package/src/plugin.js +21 -103
- package/src/skill/loader.js +14 -27
package/README.md
CHANGED
|
@@ -276,8 +276,9 @@ opencode-idea 对这一组工具做三件事:
|
|
|
276
276
|
建配置是落盘成 `.run/*.run.xml` 供反复用)。判据只写在这一处,**「命令执行」段不重复**,只留
|
|
277
277
|
「配置一律不由你写」这条无条件的,以及"该建的用哪条路建"那一条按工具是否存在分流:有工具时指向上面那条、
|
|
278
278
|
并说明非 Shell 类型与删 / 改交给用户调 `idea-run-config-manager` skill;
|
|
279
|
-
没有工具时换成「讲清楚配置名 / 命令 / 工作目录 / 理由,请用户在 IDE 里自己建」——
|
|
280
|
-
|
|
279
|
+
没有工具时换成「讲清楚配置名 / 命令 / 工作目录 / 理由,请用户在 IDE 里自己建」—— 那时 skill 本身仍在(它不按工具门禁),
|
|
280
|
+
但缺 create 工具时它也只能把流程交回用户,所以引导里顺手把这一点说明了,免得以后有人把它当成漏写而"补"上一句调不到的指令。
|
|
281
|
+
**没装插件时绝不会提到那个工具。**
|
|
281
282
|
3. **工具名清单**写在 `src/mcp/enhance.js`(与原生 IDE 的 `src/mcp/idea.js` 分开放:原生走原生,插件的走插件的),
|
|
282
283
|
与该仓库的 `RunLifecycleSchemas.kt` 保持同步。工具按**存在的子集**逐个点名(而不是「有就全写」),这样旧版本的配套
|
|
283
284
|
插件少给一两个工具时,引导也不会提到它们。同理,**每条命令名只列真的提供它的项目**(逐项目核对,见「条件注入」)——
|
|
@@ -301,27 +302,26 @@ opencode-idea 对这一组工具做三件事:
|
|
|
301
302
|
- 无论哪条路,**写完都要用 `get_run_configurations` 复查** —— `type` 写错 IDE 会静默忽略整个文件,这是唯一的判据;
|
|
302
303
|
- 这些事**不再散着说"让用户自己去 IDE 里搞"**,而是统一指向那个 skill:**让用户调它**(`@idea-run-config-manager`)—— 它是本项目 run
|
|
303
304
|
configuration 增 / 删 / 改的唯一流程入口,**只能由用户触发**(理由见下面的「而且只能用户调」);
|
|
304
|
-
- **没装配套插件** →
|
|
305
|
+
- **没装配套插件** → skill 仍然在(它现在不按工具门禁),只是缺 create 工具时它也只能把流程交回用户,请你在 IDE 里建。
|
|
305
306
|
|
|
306
307
|
> **曾经有一个 `idea-run-config` skill 教模型凭记忆手写 `.run/*.run.xml`,已经删掉。** 理由不是"不许写",而是**不许凭空写**:`type` 写错 IDE 会
|
|
307
308
|
> **静默忽略整个文件、日志里也不报**,凭记忆猜 option 名就是这么错的。现在这件事由 `idea-run-config-manager` 接手 —— 但**每个类型都得有一份实测模板**
|
|
308
309
|
> (`assets/types/`,字段名全部来自真实文件),没模板的类型宁可交给用户。
|
|
309
310
|
|
|
310
|
-
**skill
|
|
311
|
+
**skill 在插件加载时一次性注册,不跟随工具目录**:
|
|
311
312
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
313
|
+
- **判据只有"插件加载了"** —— 不看项目有没有 `/open-in-ide`、也不看 IDE 此刻serve 了哪些工具。
|
|
314
|
+
- **为什么不再按工具门禁**:早先的做法是"IDE 真的提供了这个 skill 绕着的工具才注册"(frontmatter 的 `requires:` + `ctx.tool.list()`)。
|
|
315
|
+
门本身是对的,但**注册时机错了**:它发生在 `/open-in-ide` 完成之后,而客户端在这之前就已经把 skill 列表快照给当前会话了 ——
|
|
316
|
+
结果是"服务端注册了、`@` 补全里看不到",**必须刷新页面才出现**。注册提前到 `setup`,就落在快照之前。
|
|
317
|
+
- **工具缺失的代价放回正文** —— skill 里本来就写了"工具列表里没有 `<前缀>_rerun_run_configuration` 时交回用户"这类话;
|
|
318
|
+
一个缺席的工具是模型能读到并处理的事实,一个看不见的 skill 不是。
|
|
319
|
+
- **`requires:` 这个键已彻底删除** —— 它曾是本插件自己的门禁元数据,OpenCode 不认,注册走 `ctx.skill.transform` 时也不会带上它;
|
|
320
|
+
门禁取消后它就成了"看起来在设门、实际没人读"的死字段,留着只会误导下一个改代码的人。skill 绕着哪几件工具写,由正文自己说明。
|
|
321
|
+
|
|
322
|
+
文档放在 `skills/<id>/SKILL.md`(每个 skill 一个目录,`name` / `description` 走 YAML frontmatter,需要"只由用户调"时再加
|
|
323
|
+
`metadata: opencode/autoinvoke: false`),由 `src/skill/loader.js` 扫描加载 —— **加新 skill 只需加一个目录,不用改代码**。
|
|
322
324
|
|
|
323
|
-
文档放在 `skills/<id>/SKILL.md`(每个 skill 一个目录,`name` / `description` / `requires` 走 YAML frontmatter,`requires` 由
|
|
324
|
-
loader 消费、**不会进入注册给 OpenCode 的定义**),由 `src/skill/loader.js` 扫描加载 —— **加新 skill 只需加一个目录,不用改代码**。
|
|
325
325
|
|
|
326
326
|
还可以用 OpenCode 官方的 frontmatter 写法**关掉模型主动调用**:
|
|
327
327
|
|
|
@@ -366,6 +366,7 @@ loader 读的是 OpenCode 文档里的那个嵌套写法(它自己的读取器
|
|
|
366
366
|
2. **建档**:过滤之后剩下的每个 run configuration 在**项目根 `AGENTS.md`** 的标记块(`<!-- idea-run-config-manager:begin -->` /
|
|
367
367
|
`<!-- idea-run-config-manager:end -->`)里占**一个条目** —— 标题是配置名(括号里写它属于哪个项目),下面五项固定:
|
|
368
368
|
`when` / `before run` / `run` / `after run` / `desc`;
|
|
369
|
+
**每一项各选一种写法**(行内 / 子列表),同一配置里可以混用 —— 全行内、全子列表、混合(例如 `when` 一句、`after run` 三条)都合法,判据只有"这一项自己有几句话",模板里给了两种形态的可照抄例子;
|
|
369
370
|
`when` 回答的是**谁能起它**,而这件事只有三种:`用户授权触发`(用户开口 / 在本会话里一次性授权之后 AI 才跑,后面写用户通常在什么情况下要它跑)、
|
|
370
371
|
`AI 自己触发`(AI 判断到场景就自己跑,**必须把场景写具体**)、`用户手动触发` ——
|
|
371
372
|
最后这一种**不进条目**,只登记在块末尾「用户手动触发(线下,AI 不跑)」一节,**只有名字**,五项一律不写(没人会照它跑);
|
|
@@ -418,8 +419,8 @@ loader 读的是 OpenCode 文档里的那个嵌套写法(它自己的读取器
|
|
|
418
419
|
**别的文档都指向这里**:引导文本与 `idea-standalone` skill 碰到"要建 / 删 / 改一个配置",不再让用户自己去 IDE 里摸,而是让用户调本 skill ——
|
|
419
420
|
增 / 删 / 改的流程入口只有这一个,而它只由用户触发。
|
|
420
421
|
|
|
421
|
-
|
|
422
|
-
|
|
422
|
+
**它绕着"新建配置"这件事写**(新建只有配套插件 `idea-mcp-enhance` 能做),但**这不影响它出不出现** —— 两个 skill 都在插件加载时注册,
|
|
423
|
+
不缺任何工具也照样在列表里;缺工具时由正文把流程交回用户(`idea-standalone` 只要 `rerun_run_configuration`,门更松)。这是刻意的:
|
|
423
424
|
一个只教模型"去建配置"、却没给建配置工具的 skill,只会把用户拖进一次注定失败的调用。
|
|
424
425
|
|
|
425
426
|
**而且只能用户调**:它的 frontmatter 带 `metadata: opencode/autoinvoke: false`,loader 翻成定义的 `autoinvoke: false` ——
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: idea-run-config-manager
|
|
3
|
-
|
|
4
|
-
description: "管理本项目的 run configuration:直接增 / 删 / 改(新建走工具,改和删直接编辑 `.run/<配置名>.run.xml` —— 都照 `assets/types/` 里**逐类型实测模板**写,没有模板的类型交给用户在 IDE 里配),并把配置档案同步进**项目根 `AGENTS.md`** 里由 `<!-- idea-run-config-manager:begin -->` 与 `<!-- idea-run-config-manager:end -->` 包裹的标记块 —— 每个 run configuration 一个条目,写清它干什么、怎么跑、什么时候跑;服务化的那些不在这里,它们归 `idea-standalone` 的块。当用户要建 / 删 / 改 run configuration,或要一份"这个项目有哪些配置、各自干什么"的说明时使用。"
|
|
3
|
+
description: "管理本项目的 run configuration:直接增 / 删 / 改(新建走工具,改和删直接编辑 `.run/<配置名>.run.xml` —— 都照 `assets/types/` 里**逐类型实测模板**写,没有模板的类型交给用户在 IDE 里配),并把配置档案同步进**项目根 `AGENTS.md`** 里由 `<!-- idea-run-config-manager:begin -->` 与 `<!-- idea-run-config-manager:end -->` 包裹的标记块 —— 每个 run configuration 一个条目,写清它干什么、怎么跑、什么时候跑;服务化的那些不在这里,它们归 `idea-standalone` 的块。当用户要建 / 删 / 改 run configuration,或要一份「这个项目有哪些配置、各自干什么」的说明时使用。"
|
|
5
4
|
metadata:
|
|
6
5
|
opencode/autoinvoke: false
|
|
7
6
|
---
|
|
@@ -10,6 +9,8 @@ metadata:
|
|
|
10
9
|
|
|
11
10
|
**联动是硬要求**:配置被**新建 / 删除 / 修改**,文档就跟着**加条目 / 删条目 / 改条目** —— 无论那次改动是谁做的(你做的、用户自己做的、用户删掉的,都算)。改了配置却忘了同步这个档案块,等于它在骗下一个读者。
|
|
12
11
|
|
|
12
|
+
**工具不在时**:先看工具表里有没有 `<前缀>_get_run_configurations` / `<前缀>_create_shell_run_configuration`。没有,就是项目还没接入 IDE(或那个 IDE 没装 `idea-mcp-enhance`)—— 让用户执行 `/open-in-ide`,接入之前**不要手写 `.run/*.run.xml` 假装做完**(IDE 不认,复查也过不了)。
|
|
13
|
+
|
|
13
14
|
## 流程
|
|
14
15
|
|
|
15
16
|
1. **读**:项目根 `AGENTS.md` —— 有没有 `<!-- idea-run-config-manager:begin -->` … `<!-- idea-run-config-manager:end -->` 标记块(有则更新,没有就在文件末尾追加);同时读同文件里 `idea-standalone` 的标记块,它登记过的配置不进本块。
|
|
@@ -5,73 +5,138 @@
|
|
|
5
5
|
<!-- 生成提示:begin -->
|
|
6
6
|
|
|
7
7
|
标记之外是项目自己在 `AGENTS.md` 里维护的内容,本 skill 一律不动;也只写这个项目 —— 不写全局配置目录,不另建规则文件。
|
|
8
|
-
`{...}` 是待替换的元变量,不留进产物。未涉及的条目与小节整块删掉,不留空标题。
|
|
9
|
-
|
|
8
|
+
`{...}` 是待替换的元变量,不留进产物。未涉及的条目与小节整块删掉,不留空标题。 **`<!-- 生成提示:… -->`
|
|
9
|
+
是给生成过程的说明,产物里整块删掉** —— 产物是给 AI 当规则读的,里面不该留指令。
|
|
10
10
|
<!-- 生成提示:end -->
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
本项目里 **除服务化应用之外**的 run configuration:每个配置 **是什么、怎么跑、什么时候跑**;服务化 (长驻应用)的配置登记在
|
|
13
|
+
`idea-standalone` 块里,不在这里。
|
|
13
14
|
|
|
14
15
|
**谁能起它,只有三种** —— 读这个块的人照它决定动不动手:
|
|
15
16
|
|
|
16
|
-
- **用户手动触发**:用户自己在 IDE 里跑,AI
|
|
17
|
-
- **用户授权触发**:用户开口之后 AI 才跑(用户也可以在这个会话里一次性授权)
|
|
17
|
+
- **用户手动触发**:用户自己在 IDE 里跑,AI **完全不碰**。这类只登记在末尾「用户手动触发」一节, **只有名字**,没有别的说明。
|
|
18
|
+
- **用户授权触发**:用户开口之后 AI 才跑 (用户也可以在这个会话里一次性授权); **AI 不自己挑时机**。
|
|
18
19
|
- **AI 自己触发**:AI 判断到场景就自己跑;这类条目的 `when` **必须把那个场景写具体**。
|
|
19
20
|
|
|
20
21
|
## run configuration 一览
|
|
21
22
|
|
|
22
23
|
<!-- 生成提示:begin -->
|
|
23
24
|
|
|
24
|
-
**先过滤**:动手前读 `<项目根>/AGENTS.md` 里 `idea-standalone` 的标记块,把它登记过的 run configuration 名字收集起来 ——
|
|
25
|
-
|
|
25
|
+
**先过滤**:动手前读 `<项目根>/AGENTS.md` 里 `idea-standalone` 的标记块,把它登记过的 run configuration 名字收集起来 ——
|
|
26
|
+
**这些一律不进本块**(它们服务化 / 长驻,端口与热重载已在那边写清)。剩下的才是本块内容;没有那个块就不过滤。
|
|
27
|
+
**枚举以工具为准**:各项目各有自己的工具前缀 (root 是 `ide`,子项目是它的目录名),看哪个项目就用 **它自己那个前缀**
|
|
28
|
+
的工具 —— `<前缀>_get_run_configurations` 直接调用,不传参数,它给出 IDE 真正认的配置 (含存在 `.idea/` 里的那些,单翻
|
|
29
|
+
`.run/` 目录看不到)。 **项目与各子项目里真正存在的、过滤后剩下的都要有。**
|
|
26
30
|
**项目写在标题的括号里**(那套工具的前缀:root 写 `ide`,子项目写它的目录名):一条配置属于谁、该用哪套工具去看和跑,由它决定。
|
|
27
|
-
|
|
28
|
-
**按操作系统分成两节**:macOS / Linux 一节,Windows 一节。同一份流程在两个平台上常常不是同一条命令
|
|
31
|
+
标题里的配置名必须是该前缀下 **真实存在的名字**,不凭印象写;块里有、项目里已经没有的 **整个条目删掉**。
|
|
32
|
+
**按操作系统分成两节**:macOS / Linux 一节,Windows 一节。同一份流程在两个平台上常常不是同一条命令
|
|
33
|
+
(解释器不同、脚本后缀不同、路径写法不同),所以 **按这个配置真正能跑的那个平台归节**,不要把 Windows 上才成立的命令写进 macOS
|
|
34
|
+
那一节。判断依据看配置里的解释器路径与脚本内容 (例如 `INTERPRETER_PATH` 指向 PowerShell、命令里有 `\` 路径的,就是
|
|
35
|
+
Windows); **判断不了就问用户**,不猜。只在一个平台成立的流程就只写那一节。
|
|
29
36
|
|
|
30
|
-
**when**
|
|
37
|
+
**when** 写 **谁能起它、什么场景起它** —— 会进这份列表的只有两种,写明是哪一种:
|
|
31
38
|
|
|
32
|
-
- **用户授权触发**:用户开口(或在本会话里授权)之后 AI 才跑;后面写用户通常在什么情况下会要它跑(发版前 / 出安装包 /
|
|
33
|
-
|
|
39
|
+
- **用户授权触发**:用户开口 (或在本会话里授权)之后 AI 才跑;后面写用户通常在什么情况下会要它跑 (发版前 / 出安装包 /
|
|
40
|
+
回归)。
|
|
41
|
+
- **AI 自己触发**:AI 判断到这个场景就自己跑;后面 **必须把场景写具体**(改完某类代码要生成代码 / 改了依赖要装依赖 / 改了
|
|
42
|
+
schema 要同步某个环境的 DDL),不写"需要时""合适时"。
|
|
34
43
|
|
|
35
44
|
第三种 —— **用户手动触发**(用户自己在 IDE 里跑、AI 完全不碰)**不要写进上面的条目**,登记到末尾「用户手动触发」一节,只写名字。
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
判断不了就问用户。 **有副作用的流程 (改远端、动数据、出产物)说不清场景就别标成 AI 自己触发** —— 归"用户授权触发"。
|
|
46
|
+
|
|
47
|
+
**每一项都有两种写法,逐项判断,不要给整条定一个"简单/复杂"模式**:
|
|
48
|
+
|
|
49
|
+
- **行内形态** —— 这一项只有一句话:写在项名同一行,`when:用户授权触发 —— 改了 x 后 y`。
|
|
50
|
+
- **子列表形态** —— 这一项有好几条 (多个触发条件 / 多个步骤 / 多个结果 / 多个事实):项名冒号后留空,缩进两个空格各起一条
|
|
51
|
+
`-`。
|
|
52
|
+
|
|
53
|
+
所以同一个条目里 **混着两种写法是常态**,下面三种都可以:
|
|
54
|
+
|
|
55
|
+
| 情况 | 长什么样 | 典型配置 |
|
|
56
|
+
|----------|------------------------------------------------|------------------------------------------------|
|
|
57
|
+
| 全行内 | 五项各一句,没有任何子项 | 跑完即退、一句话能说清的命令(推迁移、跑生成器) |
|
|
58
|
+
| 全子列表 | 五项里多数各有多条 | 带产物、要同步、要重启的构建 / 部署类 |
|
|
59
|
+
| 混合 | 例如 `when` 一句行内、`after run` 三件事拆三条 | 最常见 —— 只看**那一项自己**有几句 |
|
|
60
|
+
|
|
61
|
+
判断只看"这一项有几句话",与配置本身复杂不复杂无关;别为了整齐把一句硬拆成几条,也别把五六条压成一行。也可以只对少数项例外:
|
|
62
|
+
`before run:无(脚本自己跑 mvn package)` 这种一句带解释,就留在行内。
|
|
63
|
+
|
|
64
|
+
示例 (混合形态):
|
|
65
|
+
|
|
66
|
+
```markdown
|
|
67
|
+
- `connector-build`(项目 ide)
|
|
68
|
+
- when:AI 自己触发
|
|
69
|
+
- 改了 `poc/文件数据源解析与检索/解析程序` 下的 Java 源码后,重新出连接器 jar 并装进
|
|
70
|
+
`middleware/seatunnel/connectors/`
|
|
71
|
+
- before run:无 (脚本自己跑 mvn package)
|
|
72
|
+
- run:ide_execute_run_configuration
|
|
73
|
+
- after run:
|
|
74
|
+
- 产物 `middleware/seatunnel/connectors/connector-fileparse-<ST 版本>.jar`(78M,已 gitignore)
|
|
75
|
+
- 接着同步到部署机:`ide_upload_to_deployment`,`serverName` 传 `mbpmiddleware`、`mode` 传 `upload`(默认 `mirror`
|
|
76
|
+
会删远端多余文件,别用);`in_progress` 就按 handle 轮 `ide_get_deployment_task_status`
|
|
77
|
+
- 同步完在部署机上 `docker compose restart seatunnel`
|
|
78
|
+
- desc:
|
|
79
|
+
- 一条命令完成「构建 → 拷进 connectors → 打印 md5 并与 compose 引用比对」
|
|
80
|
+
- 产物改名会在输出里点名 compose 失配;脚本本身不管同步与重启
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
示例 (全行内形态,同一块里出现很正常):
|
|
84
|
+
|
|
85
|
+
```markdown
|
|
86
|
+
- `db-migrate`(项目 ide)
|
|
87
|
+
- when:用户授权触发 —— 改了 `packages/db` 的 schema 后要重新生成客户端并推迁移
|
|
88
|
+
- before run:无
|
|
89
|
+
- run:ide_execute_run_configuration
|
|
90
|
+
- after run:数据库结构更新,生成的客户端代码落在 `packages/db`
|
|
91
|
+
- desc:先 `pnpm -C packages/db generate` 再 `migrate:deploy`;动远端数据,不由 AI 自行触发
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**before run** 写 **跑之前要先做的事**(先装依赖 / 先构建 / 先起某个服务):**一条一步,按顺序排**。 **多数没有 —— 写 `无`**
|
|
95
|
+
(这一项始终在,别省略)。前置事项往往 **不是**一个 run configuration (没有对应配置),所以只能写在这里 ——
|
|
96
|
+
漏了它,照着本块跑的人会直接跑出一个起不来的流程。 **run** 写走哪条工具:跑完即退的用 `<前缀>_execute_run_configuration`
|
|
97
|
+
;长驻进程用 `<前缀>_rerun_run_configuration`(在跑就重启、没在跑就启动)。 **不要**用 `execute` 跑长驻进程 —— 它是 Run
|
|
98
|
+
路径,对常驻进程 **它会起出重复实例**。 **after run** 写 **跑完之后的结果**:**产物落在哪**(路径 / 文件名 / 被谁读走)一条,
|
|
99
|
+
**接下来要不要做什么**(要发布 / 要同步到某台机器 / 要重启什么 / 什么都不用做)一条。 **没有产物、跑完就完的,写 `无`。**
|
|
100
|
+
**desc** 是这个条目唯一的自由文本项:**它是什么、干什么的**(同步某个环境的 DDL / 生成代码 / 跑迁移 …)一条,有什么坑
|
|
101
|
+
(必须先备份 / 只对某个环境生效 / 幂等与否 …)一条。
|
|
102
|
+
分两件事都往这里写, **不要再开新项**。写清它到底跑什么命令,就读它所在项目的落地文件 `.run/<name>.run.xml` 看里面的内容,拿不准就
|
|
103
|
+
**问用户**,不猜。
|
|
104
|
+
本块的 **主要价值**就在 **desc** 与 **before run**:下次要办同类事情先在这里搜关键词,找得到就直接用,
|
|
105
|
+
**不要再建一个重复的**。 **目录、端口这类"配置里本来就有"的字段不要抄进来** —— 它们会跟配置变旧而对不上;确实要交代依赖关系,就写进
|
|
106
|
+
`desc` 或 `before run`。 **五项的名字与顺序不要改**(`when` / `before run` / `run` / `after run` / `desc`),并且 **与
|
|
107
|
+
`idea-standalone` 应用表的列同名同序**(`before run` / `run` / `after run` / `desc` 这四项两边一致);本块多 `when`,那边多
|
|
108
|
+
`app` / `run configuration` / `port`(服务化应用就是"改完代码要起它来测"的时候起,没有别的时机可写)。 **五项始终都在**:
|
|
109
|
+
`before run` / `after run` 真没有内容就写 **`无`** —— 不省略,也不写 `-` / `N/A`。
|
|
46
110
|
<!-- 生成提示:end -->
|
|
47
111
|
|
|
48
112
|
### macOS / Linux
|
|
49
113
|
|
|
50
|
-
- {配置名}(项目 {前缀})
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
114
|
+
- {配置名} (项目 {前缀})
|
|
115
|
+
- when:{用户授权触发 / AI 自己触发} —— {什么场景}
|
|
116
|
+
- before run:{第 1 步 / 第 2 步 / 无}
|
|
117
|
+
- run:{<前缀>_execute_run_configuration}
|
|
118
|
+
- after run:{产物落在哪 / 接下来要做什么 / 无}
|
|
119
|
+
- desc:{它是什么 + 有什么坑}
|
|
56
120
|
|
|
57
121
|
### Windows
|
|
58
122
|
|
|
59
|
-
- {配置名}(项目 {前缀})
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
123
|
+
- {配置名} (项目 {前缀})
|
|
124
|
+
- when:{用户授权触发 / AI 自己触发} —— {什么场景}
|
|
125
|
+
- before run:{第 1 步 / 第 2 步 / 无}
|
|
126
|
+
- run:{<前缀>_execute_run_configuration}
|
|
127
|
+
- after run:{产物落在哪 / 接下来要做什么 / 无}
|
|
128
|
+
- desc:{它是什么 + 有什么坑}
|
|
65
129
|
|
|
66
|
-
## 用户手动触发(线下,AI 不跑)
|
|
130
|
+
## 用户手动触发 (线下,AI 不跑)
|
|
67
131
|
|
|
68
132
|
用户自己在 IDE 里跑的那种配置 —— AI 不碰,这里只登记名字。
|
|
69
133
|
|
|
70
134
|
<!-- 生成提示:begin -->
|
|
71
135
|
|
|
72
|
-
只写名字(带项目)
|
|
136
|
+
只写名字 (带项目), **不要**写 run / when / before run / after run / desc:
|
|
137
|
+
没人会照它跑,写了只是噪音。判断不了某一项是"授权"还是"手动"就 **问用户**。
|
|
73
138
|
<!-- 生成提示:end -->
|
|
74
139
|
|
|
75
|
-
- {配置名}(项目 {前缀})
|
|
140
|
+
- {配置名} (项目 {前缀})
|
|
76
141
|
|
|
77
142
|
<!-- idea-run-config-manager:end -->
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: idea-standalone
|
|
3
|
-
requires: rerun_run_configuration
|
|
4
3
|
description: "为项目生成/维护 standalone(启动收口只有 IDEA run configuration)模式的纪律,写进**项目根 `AGENTS.md`** 里由 `<!-- idea-standalone:begin -->` 与 `<!-- idea-standalone:end -->` 包裹的标记块。当用户要做 standalone 规范、要固化「本机跑哪几个应用」「哪些应用热重载、哪些改了必须重启」「启停走哪个工具」这类项目级纪律时使用。产物固定三段:standalone 是什么、经 IDEA run configuration 启动哪些应用、逐应用的热重载与重启提示纪律。"
|
|
5
4
|
---
|
|
6
5
|
|
|
@@ -160,6 +160,6 @@
|
|
|
160
160
|
{{#if canCreateRunConfiguration}}
|
|
161
161
|
- 要固化的固定流程(判据见上:会不会以同样形态重复发生):Shell 类型用上面那个「新建项目级 Shell 配置」建;非 Shell 类型与删 / 改,交给用户调 `idea-run-config-manager` skill。
|
|
162
162
|
{{else}}
|
|
163
|
-
- 要固化的固定流程:把配置名、实际执行的命令、工作目录、为什么值得留讲清楚,**请用户在 IDE 里自己建** —— 当前没有能建它的工具(`idea-run-config-manager` skill
|
|
163
|
+
- 要固化的固定流程:把配置名、实际执行的命令、工作目录、为什么值得留讲清楚,**请用户在 IDE 里自己建** —— 当前没有能建它的工具(`idea-run-config-manager` skill 仍可加载,但缺这件工具时它也做不了新建),不要手写文件凑。
|
|
164
164
|
{{/if}}
|
|
165
165
|
- 不需要项目环境的命令:用原生 `shell`。
|
package/src/mcp/idea.js
CHANGED
|
@@ -127,7 +127,7 @@ export async function callTool(port, projectPath, name, args = {}, options = {})
|
|
|
127
127
|
params: {
|
|
128
128
|
protocolVersion: PROTOCOL_VERSION,
|
|
129
129
|
capabilities: {},
|
|
130
|
-
clientInfo: { name: 'opencode-idea', version: '0.0.
|
|
130
|
+
clientInfo: { name: 'opencode-idea', version: '0.0.23' },
|
|
131
131
|
},
|
|
132
132
|
});
|
|
133
133
|
// The IDE's Streamable-HTTP endpoint binds follow-up requests to the session
|
package/src/plugin.js
CHANGED
|
@@ -564,8 +564,6 @@ export default {
|
|
|
564
564
|
let commandRegistration;
|
|
565
565
|
/** @type {{ dispose: () => Promise<void> | void } | undefined} */
|
|
566
566
|
let skillRegistration;
|
|
567
|
-
/** Ids of the skills currently registered, so a sync can tell "unchanged". */
|
|
568
|
-
let registeredSkillIds = [];
|
|
569
567
|
let disposed = false;
|
|
570
568
|
|
|
571
569
|
// Latches when a request showed the IDE tools are gone. It is cleared
|
|
@@ -622,8 +620,6 @@ export default {
|
|
|
622
620
|
// best effort
|
|
623
621
|
}
|
|
624
622
|
config = next;
|
|
625
|
-
// skill 生命周期跟随配置:配置在就注册 IDEA skill,删掉配置就移除。
|
|
626
|
-
await syncSkills();
|
|
627
623
|
};
|
|
628
624
|
|
|
629
625
|
/** 当前接入的全部项目(root 在前),每个带工具前缀与展示标签。 */
|
|
@@ -707,11 +703,7 @@ export default {
|
|
|
707
703
|
// Without this, a session that already captured a snapshot keeps the old
|
|
708
704
|
// tool list until the session is reopened.
|
|
709
705
|
if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
|
|
710
|
-
|
|
711
|
-
// The catalog decides which of the shipped skills may be offered, so this
|
|
712
|
-
// is the first moment that question has an answer; see `syncSkills`.
|
|
713
|
-
await syncSkills();
|
|
714
|
-
return ready;
|
|
706
|
+
return await waitForIdeTools();
|
|
715
707
|
};
|
|
716
708
|
|
|
717
709
|
/**
|
|
@@ -1042,105 +1034,40 @@ export default {
|
|
|
1042
1034
|
}
|
|
1043
1035
|
};
|
|
1044
1036
|
|
|
1045
|
-
/**
|
|
1046
|
-
* The IDEA tools this project's IDE is serving right now.
|
|
1047
|
-
*
|
|
1048
|
-
* Empty is the honest answer for "does the IDE serve X?" before anything has
|
|
1049
|
-
* connected — and it fails that way on error too, because the alternative
|
|
1050
|
-
* (assuming a tool exists) is the one mistake that cannot be walked back
|
|
1051
|
-
* after the model has seen it.
|
|
1052
|
-
*
|
|
1053
|
-
* @returns {Promise<Set<string>>}
|
|
1054
|
-
*/
|
|
1055
|
-
const servedIdeaToolNames = async () => {
|
|
1056
|
-
if (typeof ctx.tool?.list !== 'function') return new Set();
|
|
1057
|
-
try {
|
|
1058
|
-
return new Set(toolIds(await ctx.tool.list()));
|
|
1059
|
-
} catch {
|
|
1060
|
-
return new Set();
|
|
1061
|
-
}
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
|
-
/** Same ids in the same order — `readSkills()` sorts, so the order is stable. */
|
|
1065
|
-
const isSameSkillSet = (skills, ids) =>
|
|
1066
|
-
skills.length === ids.length && skills.every((skill, index) => skill.id === ids[index]);
|
|
1067
|
-
|
|
1068
1037
|
// The shipped skills are static knowledge (`skills/<id>/SKILL.md`) that write
|
|
1069
1038
|
// a project's own rules into its `AGENTS.md`: `idea-standalone` fixes how
|
|
1070
1039
|
// resident apps are started, `idea-run-config-manager` fixes which repeated
|
|
1071
|
-
// flows are solidified into configurations.
|
|
1072
|
-
//
|
|
1040
|
+
// flows are solidified into configurations.
|
|
1041
|
+
//
|
|
1042
|
+
// They are registered **once, when the plugin loads** — with no condition on
|
|
1043
|
+
// the project config and none on the IDE's tool catalog. That is a deliberate
|
|
1044
|
+
// reversal of the earlier design, which offered a skill only after the tools
|
|
1045
|
+
// it drives were actually served. The gate looked right in isolation and was
|
|
1046
|
+
// wrong in use: the client snapshots the skill list when the session starts,
|
|
1047
|
+
// so anything registered later (by `/open-in-ide` finishing, or by a catalog
|
|
1048
|
+
// refresh) stays invisible until the page is reloaded — the user reads that
|
|
1049
|
+
// as "the skill is missing" while the registry already has it.
|
|
1073
1050
|
//
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1078
|
-
// `idea-mcp-enhance` serves (rerun for standalone, create for the
|
|
1079
|
-
// manager), so a stock IDE must not see them at all. Each skill declares
|
|
1080
|
-
// its own dependency in its frontmatter (`requires:`), as a SUFFIX — tool
|
|
1081
|
-
// names carry a per-project prefix, so a hard-coded `idea_...` could only
|
|
1082
|
-
// ever match one project.
|
|
1051
|
+
// What the old gate protected against is handled where it belongs: the skill
|
|
1052
|
+
// text itself says what to do when a tool is absent ("hand it back to the
|
|
1053
|
+
// user", "run /open-in-ide"). A missing tool is a fact the model can read and
|
|
1054
|
+
// act on; an invisible skill is not.
|
|
1083
1055
|
//
|
|
1084
|
-
// The
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
// what is justified right now. Until the IDE has answered, the gated skills
|
|
1088
|
-
// simply do not exist.
|
|
1056
|
+
// The frontmatter `requires:` key is gone with the gate. It was this
|
|
1057
|
+
// package's own metadata, never read by OpenCode, and keeping it after the
|
|
1058
|
+
// gate disappeared would look like it still decided something.
|
|
1089
1059
|
//
|
|
1090
1060
|
// Deliberately NOT wrapped in a catch: these are core features of the plugin,
|
|
1091
1061
|
// so a rejected definition must fail loudly. Swallowing it would ship a
|
|
1092
1062
|
// silently incomplete plugin — which is exactly how 0.0.6 lost
|
|
1093
1063
|
// `/open-in-ide` without anyone noticing until the command was gone.
|
|
1094
|
-
const
|
|
1064
|
+
const registerSkills = async () => {
|
|
1095
1065
|
if (typeof ctx.skill?.transform !== 'function') return;
|
|
1096
|
-
|
|
1097
|
-
const served = await servedIdeaToolNames();
|
|
1098
|
-
const wanted = (config ? readSkills() : [])
|
|
1099
|
-
.filter(({ requires }) =>
|
|
1100
|
-
requires.every((suffix) => prefixes().some((prefix) => served.has(`${prefix}${suffix}`))),
|
|
1101
|
-
)
|
|
1102
|
-
.map(({ skill }) => skill);
|
|
1103
|
-
if (isSameSkillSet(wanted, registeredSkillIds)) return;
|
|
1104
|
-
|
|
1105
|
-
// Tear down and register again, the same way the MCP server is
|
|
1106
|
-
// re-registered: a transform that only ever adds would leave a stale skill
|
|
1107
|
-
// behind, and one that only removes could never bring it back.
|
|
1108
|
-
registeredSkillIds = [];
|
|
1109
|
-
const previous = skillRegistration;
|
|
1110
|
-
skillRegistration = undefined;
|
|
1111
|
-
if (previous) {
|
|
1112
|
-
try {
|
|
1113
|
-
await previous.dispose();
|
|
1114
|
-
} catch {
|
|
1115
|
-
// best effort
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
if (wanted.length === 0) return;
|
|
1119
1066
|
skillRegistration = await ctx.skill.transform((editor) => {
|
|
1120
|
-
for (const skill of
|
|
1067
|
+
for (const { skill } of readSkills()) editor.add(skill);
|
|
1121
1068
|
});
|
|
1122
|
-
registeredSkillIds = wanted.map((skill) => skill.id);
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
// Serialized like the reconciler, and for the same reason: one call disposes
|
|
1126
|
-
// a registration before it makes the next, so two overlapping calls would
|
|
1127
|
-
// leak one of them. That overlap is easy to reach — `/delete-in-ide` can land
|
|
1128
|
-
// while a reconnect is still in flight. With the chain in place, callers may
|
|
1129
|
-
// fire this without awaiting.
|
|
1130
|
-
let skillSyncChain = Promise.resolve();
|
|
1131
|
-
const syncSkills = () => {
|
|
1132
|
-
const run = () => syncSkillsOnce();
|
|
1133
|
-
const next = skillSyncChain.then(run, run);
|
|
1134
|
-
skillSyncChain = next.then(
|
|
1135
|
-
() => {},
|
|
1136
|
-
() => {},
|
|
1137
|
-
);
|
|
1138
|
-
return next;
|
|
1139
1069
|
};
|
|
1140
|
-
|
|
1141
|
-
// has answered; a project configured for the first time gets them when
|
|
1142
|
-
// `/open-in-ide` runs (both paths go through `writeConfig` / this setup hook).
|
|
1143
|
-
await syncSkills();
|
|
1070
|
+
await registerSkills();
|
|
1144
1071
|
|
|
1145
1072
|
/**
|
|
1146
1073
|
* 问用户"这个项目用哪个 IDE",由模型转达并回写。
|
|
@@ -1407,15 +1334,6 @@ export default {
|
|
|
1407
1334
|
if (config || serving) {
|
|
1408
1335
|
appendIdeRecoveryGuidance(event?.system, { marked: Boolean(config), serving });
|
|
1409
1336
|
}
|
|
1410
|
-
// A skill follows its tools in both directions, and this is the one place
|
|
1411
|
-
// that sees the answer change WITHOUT a reconnect: a request that has
|
|
1412
|
-
// lost the enhance tools must not keep the skill that drives them. Fired,
|
|
1413
|
-
// not awaited — the hook is synchronous, and the chain in `syncSkills`
|
|
1414
|
-
// makes that safe. `refreshIdeTools` covers the other direction.
|
|
1415
|
-
//
|
|
1416
|
-
// Skipped when there is nothing it could change, so a project that never
|
|
1417
|
-
// touched the IDE does not pay a catalog read on every request.
|
|
1418
|
-
if (config || registeredSkillIds.length > 0) void syncSkills();
|
|
1419
1337
|
if (!injectGuidance) return;
|
|
1420
1338
|
// Guidance promises tools the model can call, so it only goes out when
|
|
1421
1339
|
// they are actually in this request.
|
package/src/skill/loader.js
CHANGED
|
@@ -22,11 +22,14 @@
|
|
|
22
22
|
// The frontmatter reader below handles only the flat `key: value` form this
|
|
23
23
|
// package writes. It is not a YAML engine and must not grow into one.
|
|
24
24
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
25
|
+
// The skills used to carry a `requires:` key (the tools each one is written
|
|
26
|
+
// around), and the plugin registered a skill only when those tools were being
|
|
27
|
+
// served. That gate is gone, together with the key: skills are registered once,
|
|
28
|
+
// when the plugin loads (see `plugin.js`), and a missing tool is handled inside
|
|
29
|
+
// the skill text. Registering only after the tools appeared made the skill
|
|
30
|
+
// invisible until the client reloaded the page. A key left behind would look
|
|
31
|
+
// like it still decided something — it does not: OpenCode never reads it, and
|
|
32
|
+
// nothing in this package does either.
|
|
30
33
|
//
|
|
31
34
|
// The other key is OpenCode's own, written in the portable frontmatter spelling
|
|
32
35
|
// OpenCode documents: a nested `metadata:` block holding
|
|
@@ -80,20 +83,6 @@ export function parseSkillFrontmatter(text, source) {
|
|
|
80
83
|
return fields;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
/**
|
|
84
|
-
* Split a frontmatter list value: `a, b` -> `['a', 'b']`.
|
|
85
|
-
*
|
|
86
|
-
* @param {string | undefined} value
|
|
87
|
-
* @returns {string[]}
|
|
88
|
-
*/
|
|
89
|
-
function splitList(value) {
|
|
90
|
-
if (!value) return [];
|
|
91
|
-
return value
|
|
92
|
-
.split(',')
|
|
93
|
-
.map((entry) => entry.trim())
|
|
94
|
-
.filter((entry) => entry !== '');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
86
|
/**
|
|
98
87
|
* Read `opencode/autoinvoke` out of the frontmatter, or `undefined` when absent.
|
|
99
88
|
*
|
|
@@ -131,11 +120,10 @@ export function readSkill(id) {
|
|
|
131
120
|
}
|
|
132
121
|
|
|
133
122
|
/**
|
|
134
|
-
*
|
|
123
|
+
* One skill package.
|
|
135
124
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* it is what the caller filters on.
|
|
125
|
+
* `skill` must satisfy `Skill.Info` exactly — nothing of this package's own
|
|
126
|
+
* metadata rides along into the definition OpenCode validates.
|
|
139
127
|
*
|
|
140
128
|
* `autoinvoke` is OpenCode's, so it goes the other way: it ships in the
|
|
141
129
|
* frontmatter as `opencode/autoinvoke` and is translated onto the definition,
|
|
@@ -143,7 +131,7 @@ export function readSkill(id) {
|
|
|
143
131
|
* frontmatter asks for it, so a skill that says nothing keeps the plain shape.
|
|
144
132
|
*
|
|
145
133
|
* @param {string} id Directory name, used as the skill id.
|
|
146
|
-
* @returns {{ skill: { id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string }
|
|
134
|
+
* @returns {{ skill: { id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string } }}
|
|
147
135
|
*/
|
|
148
136
|
export function readSkillPackage(id) {
|
|
149
137
|
const file = new URL(`${id}/${SKILL_FILE_NAME}`, SKILLS_DIR);
|
|
@@ -164,14 +152,13 @@ export function readSkillPackage(id) {
|
|
|
164
152
|
path: fileURLToPath(file),
|
|
165
153
|
content: text.replace(FRONTMATTER, '').replace(/^\s+/, '').trimEnd(),
|
|
166
154
|
},
|
|
167
|
-
requires: splitList(fields.requires),
|
|
168
155
|
};
|
|
169
156
|
}
|
|
170
157
|
|
|
171
158
|
/**
|
|
172
|
-
* Every shipped skill, sorted by id
|
|
159
|
+
* Every shipped skill, sorted by id.
|
|
173
160
|
*
|
|
174
|
-
* @returns {Array<{ skill: { id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string }
|
|
161
|
+
* @returns {Array<{ skill: { id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string } }>}
|
|
175
162
|
*/
|
|
176
163
|
export function readSkills() {
|
|
177
164
|
return readdirSync(SKILLS_DIR, { withFileTypes: true })
|