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 CHANGED
@@ -276,8 +276,9 @@ opencode-idea 对这一组工具做三件事:
276
276
  建配置是落盘成 `.run/*.run.xml` 供反复用)。判据只写在这一处,**「命令执行」段不重复**,只留
277
277
  「配置一律不由你写」这条无条件的,以及"该建的用哪条路建"那一条按工具是否存在分流:有工具时指向上面那条、
278
278
  并说明非 Shell 类型与删 / 改交给用户调 `idea-run-config-manager` skill;
279
- 没有工具时换成「讲清楚配置名 / 命令 / 工作目录 / 理由,请用户在 IDE 里自己建」—— 那时**连那个 skill 也不会出现**
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
- - **没装配套插件** → 那个 skill 也不存在(它的门就是 create 工具),此时才直接请用户自己在 IDE 里建。
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
- 1. **项目配置** —— `/open-in-ide` 写入配置时注册、`/delete-in-ide` 删配置时注销;以前配置过的 root 在 `setup` 时(读回配置)
313
- 也会走这一步。未配置的 root 不受影响。
314
- 2. **IDE 真的在提供这个 skill 依赖的工具** —— skill 在自己的 frontmatter 里用 `requires:` 声明它绕着哪个工具写,而且写的是**后缀**
315
- (例如 `idea-standalone` 声明 `rerun_run_configuration`、`idea-run-config-manager` 声明 `create_shell_run_configuration`,真实工具名是
316
- `<项目前缀>rerun_run_configuration` / `<项目前缀>create_shell_run_configuration`)。**没这个工具就不注册这个 skill**:
317
- 一个教模型去调"它根本看不到的工具"的 skill,不是知识,是一次注定的失败调用。工具的可见性是双向跟随的 —— `setup` 时还没
318
- 连过 IDE、目录未知,所以那时**不注册**;等 IDE 应答(`/open-in-ide` 或重连)后目录才算已知,该出现的才出现;反过来,某个
319
- 请求里工具没了,skill 也跟着撤(不需要等重连)。
320
- `requires` 必须写 **OpenCode 侧的名字**(IDE 工具带 `ide_` 服务器前缀),因为门就是拿它跟 `ctx.tool.list()` 比;
321
- 写成 IDE 的原始名(`rerun_run_configuration`)会让这个 skill 在任何机器上都不出现,而且没有任何报错。
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
- **它比 `idea-standalone` 多一道门**:frontmatter 里 `requires: create_shell_run_configuration` —— 它绕着"新建配置"这件事写,而新建只有
422
- 配套插件 `idea-mcp-enhance` 能做,所以**没装插件的 IDE 看不到它**(`idea-standalone` 只要 `rerun_run_configuration`,门更松)。这是刻意的:
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,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-idea",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "OpenCode plugin that connects to JetBrains IDE MCP servers and prefers IDE tools when the current project is open in the IDE",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: idea-run-config-manager
3
- requires: create_shell_run_configuration
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
- **`<!-- 生成提示:… -->` 是给生成过程的说明,产物里整块删掉** —— 产物是给 AI 当规则读的,里面不该留指令。
8
+ `{...}` 是待替换的元变量,不留进产物。未涉及的条目与小节整块删掉,不留空标题。 **`<!-- 生成提示:… -->`
9
+ 是给生成过程的说明,产物里整块删掉** —— 产物是给 AI 当规则读的,里面不该留指令。
10
10
  <!-- 生成提示:end -->
11
11
 
12
- 本项目里**除服务化应用之外**的 run configuration:每个配置**是什么、怎么跑、什么时候跑**;服务化(长驻应用)的配置登记在 `idea-standalone` 块里,不在这里。
12
+ 本项目里 **除服务化应用之外**的 run configuration:每个配置 **是什么、怎么跑、什么时候跑**;服务化 (长驻应用)的配置登记在
13
+ `idea-standalone` 块里,不在这里。
13
14
 
14
15
  **谁能起它,只有三种** —— 读这个块的人照它决定动不动手:
15
16
 
16
- - **用户手动触发**:用户自己在 IDE 里跑,AI **完全不碰**。这类只登记在末尾「用户手动触发」一节,**只有名字**,没有别的说明。
17
- - **用户授权触发**:用户开口之后 AI 才跑(用户也可以在这个会话里一次性授权);**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
- **枚举以工具为准**:各项目各有自己的工具前缀(root 是 `ide`,子项目是它的目录名),看哪个项目就用**它自己那个前缀**的工具 —— `<前缀>_get_run_configurations` 直接调用,不传参数,它给出 IDE 真正认的配置(含存在 `.idea/` 里的那些,单翻 `.run/` 目录看不到)。**项目与各子项目里真正存在的、过滤后剩下的都要有。**
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 一节。同一份流程在两个平台上常常不是同一条命令(解释器不同、脚本后缀不同、路径写法不同),所以**按这个配置真正能跑的那个平台归节**,不要把 Windows 上才成立的命令写进 macOS 那一节。判断依据看配置里的解释器路径与脚本内容(例如 `INTERPRETER_PATH` 指向 PowerShell、命令里有 `\` 路径的,就是 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
- - **AI 自己触发**:AI 判断到这个场景就自己跑;后面**必须把场景写具体**(改完某类代码要生成代码 / 改了依赖要装依赖 / 改了 schema 要同步某个环境的 DDL),不写"需要时""合适时"。
39
+ - **用户授权触发**:用户开口 (或在本会话里授权)之后 AI 才跑;后面写用户通常在什么情况下会要它跑 (发版前 / 出安装包 /
40
+ 回归)。
41
+ - **AI 自己触发**:AI 判断到这个场景就自己跑;后面 **必须把场景写具体**(改完某类代码要生成代码 / 改了依赖要装依赖 / 改了
42
+ schema 要同步某个环境的 DDL),不写"需要时""合适时"。
34
43
 
35
44
  第三种 —— **用户手动触发**(用户自己在 IDE 里跑、AI 完全不碰)**不要写进上面的条目**,登记到末尾「用户手动触发」一节,只写名字。
36
- 判断不了就问用户。**有副作用的流程(改远端、动数据、出产物)说不清场景就别标成 AI 自己触发** —— 归"用户授权触发"。
37
-
38
- **before run** 写**跑之前要先做的事**(先装依赖 / 先构建 / 先起某个服务):**多数没有 —— 写 `无`**(这一项始终在,别省略)。确实得先做点什么才写,而且**按顺序写清第 1 步、第 2 步**。前置事项往往**不是**一个 run configuration(没有对应配置),所以只能写在这里 —— 漏了它,照着本块跑的人会直接跑出一个起不来的流程。
39
- **run** 写走哪条工具:跑完即退的用 `<前缀>_execute_run_configuration`;长驻进程用 `<前缀>_rerun_run_configuration`(在跑就重启、没在跑就启动)。**不要**用 `execute` 跑长驻进程 —— 它是 Run 路径,对常驻进程**它会起出重复实例**。
40
- **after run** 写**跑完之后的结果**:产物落在哪(路径 / 文件名 / 被谁读走),以及**接下来要不要做什么**(要发布 / 要拷贝到某处 / 要通知谁 / 什么都不用做)。**没有产物、跑完就完的,写 `无`。**
41
- **desc** 是这个条目唯一的自由文本项:**它是什么、干什么的**(同步某个环境的 DDL / 生成代码 / 跑迁移 …),以及有什么坑(必须先备份 / 只对某个环境生效 / 幂等与否 …)。
42
- 分两件事都往这里写,**不要再开新项**。写清它到底跑什么命令,就读它所在项目的落地文件 `.run/<name>.run.xml` 看里面的内容,拿不准就**问用户**,不猜。
43
- 本块的**主要价值**就在 **desc** 与 **before run**:下次要办同类事情先在这里搜关键词,找得到就直接用,**不要再建一个重复的**。
44
- **目录、端口这类"配置里本来就有"的字段不要抄进来** —— 它们会跟配置变旧而对不上;确实要交代依赖关系,就写进 `desc` 或 `before run`。
45
- **五项的名字与顺序不要改**(`when` / `before run` / `run` / `after run` / `desc`),并且**与 `idea-standalone` 应用表的列同名同序**(`before run` / `run` / `after run` / `desc` 这四项两边一致);本块多 `when`,那边多 `app` / `run configuration` / `port`(服务化应用就是"改完代码要起它来测"的时候起,没有别的时机可写)。**五项始终都在**:`before run` / `after run` 真没有内容就写 **`无`** —— 不省略,也不写 `-` / `N/A`。
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
- - when:{用户授权触发 / AI 自己触发} —— {什么场景}
52
- - before run:{第 1 步 / 第 2 步 / 无}
53
- - run:{<前缀>_execute_run_configuration}
54
- - after run:{产物落在哪 / 接下来要做什么 / 无}
55
- - desc:{它是什么 + 有什么坑}
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
- - when:{用户授权触发 / AI 自己触发} —— {什么场景}
61
- - before run:{第 1 步 / 第 2 步 / 无}
62
- - run:{<前缀>_execute_run_configuration}
63
- - after run:{产物落在哪 / 接下来要做什么 / 无}
64
- - desc:{它是什么 + 有什么坑}
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
- 只写名字(带项目),**不要**写 run / when / before run / after run / desc:没人会照它跑,写了只是噪音。判断不了某一项是"授权"还是"手动"就**问用户**。
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.22' },
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
- const ready = await waitForIdeTools();
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. They pass two independent gates,
1072
- // because a skill can be wrong twice over:
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
- // * the project config — an unrelated project never sees an IDE skill;
1075
- // * the tools the IDE actually serves — a skill that teaches a call the
1076
- // model cannot make is not knowledge, it is a failed call waiting to
1077
- // happen. Both skills drive tools that only the companion plugin
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 catalog is unknown at setup: the config is read from storage offline,
1085
- // before anything has connected to the IDE. So this is a reconcile rather
1086
- // than a one-shot — every catalog refresh re-runs it, and it offers exactly
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 syncSkillsOnce = async () => {
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 wanted) editor.add(skill);
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
- // A project configured in an earlier run gets its skills as soon as the IDE
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.
@@ -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
- // One frontmatter key is the loader's own rather than OpenCode's: `requires`, a
26
- // comma-separated list of the tools the skill is written around. It is read here
27
- // and deliberately kept OUT of the registered definition — the caller filters on
28
- // it instead (see `readSkills`), because a skill whose tools are absent must not
29
- // be registered at all.
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
- * A skill plus the tools it needs before it is worth offering.
123
+ * One skill package.
135
124
  *
136
- * The two are kept apart on purpose: `skill` must satisfy `Skill.Info` exactly,
137
- * while `requires` is this package's own metadata and must NOT be registered —
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 }, requires: 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, each with the tools it requires.
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 }, requires: 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 })