opencode-idea 0.0.4 → 0.0.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.6
4
+
5
+ - **修复:工具报错不再注销 IDE MCP**。旧实现在 `tool.execute.after` 里把**任何** `idea_*` 报错都当成「端点已死」并注销整个注册 —— 业务错误 (例如路径不存在返回 `File not found`) 和一次传输抖动都会触发;注销后紧跟的 `ctx.mcp.reload()` 还会掐断同一批正在执行的其他调用。现在插件**不注册**该钩子:连接状态以 OpenCode 自己维护的工具快照为准 (`context` 钩子收到的 `event.tools` 里有没有 `idea_*`),只有 `/open-in-idea` 会 (重新) 连接。
6
+ - **隐藏 31 个用不到的 IDEA 工具** (`HIDDEN_IDEA_TOOLS`):VCS、Router 派发工具、Debugger (全部 `idea_xdebug_*`)、Dev Kit MCP、Inspection KTS MCP、Python Environment MCP、数据库建/改数据源、终端 `execute_terminal_command`。模型可见的 `idea_*` 从 64 降到 33。
7
+ - **引导词重写**:边界从「所有操作」收窄为「**代码与文件操作**」(未暴露的领域自然走原生,不再列例外);新增数据库段 (走 IDEA Database 工具 + 用户配数据源 + 不索取凭据);删除终端命令段 (该工具已隐藏)。
8
+ - **新增 `idea-run-config` skill**:教模型在 `.run/*.run.xml` 里建/改/删 run configuration,并用 `get_run_configurations` 校验、`execute_run_configuration` 实跑 (IDE MCP 没有增删改能力)。仅 JetBrains 项目注册,与 `/open-in-idea` 无关。
9
+ - **`/open-in-idea` 改为问注册表要连接状态**:活着只刷新,死了才换注册 (OpenCode 对未变更的 MCP 配置不会重连)。
10
+ - **默认 MCP 端口收敛**:`DEFAULT_PORTS` 从 `[64342, 6420, 6421, 63342]` 改为 `[64342]`。
11
+ - **修复发布脚本**:`assertCleanGit()` 原先排在版本递增之后,导致正式发布必然中途失败并留下已被改动的版本号;现在移到递增之前。
12
+
13
+ ## 0.0.5
14
+
15
+ - **冷启动等待 MCP 端点**:`/open-in-idea` 在 `open -a` 之后按 `mcpStartTimeoutMs`(默认 60s)轮询,直到 IDEA 自带的 MCP server 开始监听。冷启动时一次命令即可连上,不再像之前那样探测一次失败就报「未检测到 MCP 服务」。
16
+ - **工具就绪判定**:连接后轮询 `ctx.tool.list()`,确认 `idea_read_file` / `idea_apply_patch` 已注册。未就绪时状态为 `tools-loading` 并提示稍后重试,不再在原生工具表尚未就绪时谎称「已连接」。
17
+ - **重复执行刷新工具列表**:再次执行 `/open-in-idea` 会重建 MCP server 并依次 `ctx.mcp.reload()` + `ctx.tool.reload()`,让已经捕获过工具快照的会话立即看到最新工具,而不是停留在旧快照。
18
+ - **IDE 掉线自动注销**:每次请求前复检端点,IDEA 关闭或某次 `idea_*` 工具调用失败时,立即移除 `idea_*` 工具并恢复原生工具,避免后续请求继续调用已失效的 IDE 工具;只有再次执行 `/open-in-idea` 才能重新注册。
19
+ - **清理**:移除调试日志;删除过时的 `scripts/publish.sh`(旧包名 + bypass token 流程,已由 `scripts/release.mjs` 取代);`package.json` 的 `files` 移除早已删除的 `src/idea-tool-descriptions.js`。
20
+
3
21
  ## 0.0.4
4
22
 
5
23
  - **`/open-in-idea` 刷新工具列表**:在 `ctx.mcp.reload()` 之后显式调用 `ctx.tool.reload()`。之前只有 MCP 层重建目录,已经捕获过工具快照的会话仍然看不到新的 `idea_*` 原生工具;现在下一次模型请求即可用,无需重开会话。
package/README.md CHANGED
@@ -1,12 +1,16 @@
1
1
  # opencode-idea
2
2
 
3
- OpenCode 插件,面向 JetBrains 项目(IntelliJ IDEA / PyCharm / WebStorm 等),做三件事:
3
+ OpenCode 插件,面向 JetBrains 项目 (IntelliJ IDEA / PyCharm / WebStorm 等),做三件事:
4
4
 
5
- 1. **接入 IDE MCP** —— 用 `/open-in-idea` 把当前项目在 IntelliJ IDEA 中打开,并把这个 IDE 的 MCP 服务(60 个工具:检索、读、改、构建、重构、调试、数据库……)注册进 OpenCode。
6
- 2. **优先使用 IDE 工具** —— IDE 可用时,向模型注入"项目内操作优先使用 IDEA MCP"的系统提示引导 + 能力映射;IDE 不可用时回退原生工具(正常回退,无需声明)。
7
- 3. **注入 IDE 环境变量** —— 从 IDE 集成终端读取它实际运行的环境(版本化管理的 Node、goenv 的 Go、SDKMAN 的 Java、Maven……)注入每个 OpenCode shell。
5
+ 1. **接入 IDE MCP** —— 用 `/open-in-idea` 把当前项目在 IntelliJ IDEA 中打开,并把这个 IDE 的 MCP 服务 (60 个工具:
6
+ 检索、读、改、构建、重构、调试、数据库……)注册进 OpenCode。
7
+ 2. **优先使用 IDE 工具** —— IDE 可用时,向模型注入"项目内操作优先使用 IDEA MCP"的系统提示引导 + 能力映射;IDE 不可用时回退原生工具
8
+ (正常回退,无需声明)。
9
+ 3. **注入 IDE 环境变量** —— 从 IDE 集成终端读取它实际运行的环境 (版本化管理的 Node、goenv 的 Go、SDKMAN 的 Java、Maven……)
10
+ 注入每个 OpenCode shell。
8
11
 
9
- 非 JetBrains 项目完全不受影响。**全部手动**:插件启动不探测、不连接 IDE,只有跑 `/open-in-idea` 时才连接。
12
+ 非 JetBrains 项目完全不受影响。 **全手动**:插件启动不探测、不连接 IDE,只有跑 `/open-in-idea` 才接入;接入之后也没有
13
+ 任何自主动作(环境变量只在 `/open-in-idea` 时刷新)。
10
14
 
11
15
  ## 工作原理
12
16
 
@@ -29,55 +33,82 @@ setup(启动):
29
33
  6. 发一条结果通知(用户看到干净气泡,模型只回"收到")
30
34
  ```
31
35
 
32
- ## 在 IDE 中打开项目(`/open-in-idea`)
36
+ ## 在 IDE 中打开项目 (`/open-in-idea`)
33
37
 
34
38
  在会话里输入 `/open-in-idea`:
35
39
 
36
40
  1. 先探测 `/stream`;已就绪 → 直接加载能力;
37
41
  2. 未就绪 → 用 `open -a "IntelliJ IDEA" <项目目录>` 拉起或激活 IDEA —— **复用已运行的实例**(不会重复开新实例);
38
- 3. 如果 MCP 仍未就绪,命令会**立即 fast-fail**,提示在 IDEA 的 `Settings → MCP Server` 开启 MCP 服务并启用 **Brave Mode**;
42
+ 3. 如果 MCP 仍未就绪,命令会 **立即 fast-fail**,提示在 IDEA 的 `Settings → MCP Server` 开启 MCP 服务并启用 **Brave
43
+ Mode**;
39
44
  4. 用户手动完成设置后,再次执行 `/open-in-idea`;探测成功后才注册 IDE MCP、重读 IDE 环境并启用系统引导与 live tool catalog。
40
45
 
41
- > **通知不触发 AI 干活**:默认 `feedback: "message"` 把结果作为一条**标注清楚的通知**发到会话里 —— 用户看到干净的通知气泡(经 `metadata.displayText`),模型只看到"这是通知,请只回复『收到』"的指令,因此不会执行任何操作、不会调用工具。`feedback: false` 则完全静默。
46
+ > **通知不触发 AI 干活**:默认 `feedback: "message"` 把结果作为一条 **标注清楚的通知**发到会话里 —— 用户看到干净的通知气泡
47
+ > (经 `metadata.displayText`),模型只看到"这是通知,请只回复『收到』"的指令,因此不会执行任何操作、不会调用工具。
48
+ > `feedback: false` 则完全静默。
42
49
 
43
- **可重入 / 启动保护**:IDEA 冷启动、索引或等待「Trust and Open Project」弹窗期间探测会失败。命令只发一次 MCP/Brave Mode 提示并 fast-fail,不会在后台轮询;重复触发共享同一次打开请求,按目录维护 `lastSpawnAt` / `attempts`,避免重复 spawn。开启 MCP + Brave Mode 后再次执行 `/open-in-idea` 即可接上。
50
+ **可重入 / 启动保护**:IDEA 冷启动、索引或等待「Trust and Open Project」弹窗期间探测会失败。命令只发一次 MCP/Brave Mode 提示并
51
+ fast-fail,不会在后台轮询;重复触发共享同一次打开请求,按目录维护 `lastSpawnAt` / `attempts`,避免重复 spawn。开启 MCP +
52
+ Brave Mode 后再次执行 `/open-in-idea` 即可接上。
44
53
 
45
- **热更新会断开**:插件源码变更会触发 V2 热重载,注册随旧实例一起释放,`activePort` 归零。这是手动模式的预期行为 —— **改完插件后重跑一次 `/open-in-idea`** 即可恢复。
54
+ **热更新会断开**:插件源码变更会触发 V2 热重载,注册随旧实例一起释放,`activePort` 归零。这是手动模式的预期行为 ——
55
+ **改完插件后重跑一次 `/open-in-idea`** 即可恢复。
46
56
 
47
- ## IDE 工具优先(直接调用引导)
57
+ ## IDE 工具优先 (直接调用引导)
48
58
 
49
- IDE MCP 可用时,插件通过两层轻量引导提高模型主动使用 IDEA MCP 的概率;不会修改原生工具的定义、描述或可用性:
59
+ IDE MCP 可用时,插件通过 **系统提示注入**提高模型主动使用 IDEA MCP 的概率;不修改任何工具 (原生或 IDEA)
60
+ 的定义、描述或可用性:
50
61
 
51
- 1. **系统提示注入**(主):通过 `ctx.session.hook("context", …)` 往每次请求的 system 里追加一段「项目内操作优先使用 IDEA MCP」的直接调用引导 + 能力映射表(`src/ide-guidance.js`),含工具名(`idea_<name>`)与关键参数名:
62
+ 1. **系统提示注入**:通过 `ctx.session.hook("context", …)` 往每次请求的 system 里追加一段「项目内操作优先使用 IDEA
63
+ MCP」的直接调用引导 + 能力映射表 (`src/ide-guidance.js`),含工具名 (`idea_<name>`)与关键参数名:
52
64
  - 检索 → `idea_search_text` / `idea_search_regex` / `idea_search_file`(参数 `q`,可带 `paths` 收窄)
53
65
  - 读文件 → `idea_read_file`(`file_path` / `offset` / `limit`);目录 → `idea_list_directory_tree`
54
66
  - 改文件 → `idea_apply_patch`(`input`);新建 → `idea_create_new_file`(`pathInProject`)
55
67
  - 校验 → `idea_lint_files` / `idea_get_file_problems` / `idea_build_project`
56
- - 重构/格式化 → `idea_rename_refactoring` / `idea_reformat_file`;版本 → `idea_git_status`
57
- - 例外(直接用原生):IDE 不索引的目录、`git diff/log/blame`、MCP 无等价能力、IDE MCP 未连接/未注册
68
+ - 重构/格式化 → `idea_rename_refactoring` / `idea_reformat_file`
69
+ - 数据库 → `idea_list_database_connections` / `idea_execute_sql_query` / `idea_preview_table_data` 等;没有数据源时先建议用户在
70
+ IDEA 里配置
71
+ - 边界:只约束 **代码与文件操作**;未暴露的领域 (版本控制、shell/终端等) 工具表里没有 `idea_*` 选项,模型自然走原生
58
72
 
59
- 连接成功后,插件只对 `idea_*` IDEA MCP 工具追加一段简短描述,要求模型直接调用、严格按当前 schema 传参;不会把整份工具目录重复塞进 system prompt,也不要求模型额外包一层 JavaScript 编排代码。
73
+ 不会把整份工具目录重复塞进 system prompt,也不要求模型包一层 JavaScript 编排代码。IDEA MCP 工具自身的
74
+ description/schema 仍是参数使用的权威来源;工具返回业务错误时先修正参数,不要误判成工具不可用。MCP 不可用时立即
75
+ 提示用户执行 `/open-in-idea`,不重复探测或等待。
60
76
 
61
- IDEA MCP 工具自身的 description/schema 仍是参数使用的权威来源;工具返回业务错误时先修正参数,不要误判成工具不可用。MCP 不可用时立即提示用户执行 `/open-in-idea`,不重复探测或等待。
77
+ 引导只在 **IDE MCP 可用**时生效;MCP 不可用时仅保留 fast-fail 恢复提示。`injectGuidance: false` 可关闭系统提示注入。
62
78
 
63
- 2. **IDEA MCP 工具描述增强**:插件通过 `ctx.tool.transform` 只更新 `idea_*` / `idea.*` 工具的 description,强调直接调用和按 schema 传参;原生 `edit` / `write` / `patch` / `shell` 完全不变。
79
+ ## Run Configuration 管理 (skill)
64
80
 
65
- 引导只在 **IDE MCP 可用**时生效;MCP 不可用时仅保留 fast-fail 恢复提示。`injectGuidance: false` 可关闭系统提示注入。
81
+ IDE MCP 只能 **查**(`get_run_configurations`)和 **执行**(`execute_run_configuration`)run configuration, **没有增删改**
82
+ 。插件在 **JetBrains 项目**(存在 `.idea`)里注册一个 `idea-run-config` skill 来补上这块:
83
+
84
+ - 只操作 **项目级** `.run/<name>.run.xml`(可提交 git、团队共享), **不碰** `.idea/workspace.xml`;
85
+ - 不内置任何按类型的模板 —— 先找项目里的 **真实样本**(`**/*.run.xml`,或让用户在 IDEA 里勾 "Store as project file"
86
+ 建一个)当参照,再写;
87
+ - 写完必须用 `idea_get_run_configurations` **校验它能被列出**、并用 `idea_execute_run_configuration` **实跑确认**;
88
+ - 改配置用 `idea_apply_patch` **定向改 option**,不整文件重写。
89
+
90
+ skill 由 `ctx.skill.transform` 在插件 `setup` 时注册, **与 `/open-in-idea` 无关**;非 JetBrains 项目不受影响。定义在
91
+ `src/ide-run-config-skill.js`。
66
92
 
67
93
  ## 环境变量注入
68
94
 
69
- **唯一来源:IDE 集成终端**(`execute_terminal_command printenv`)。它反映 IDE 实际运行的环境 —— 版本化管理的 Node(如 fnm v14)、goenv 的 Go、SDKMAN 的 Java 等 —— 这些 .idea 配置里往往读不到。
95
+ **唯一来源:IDE 集成终端**(`execute_terminal_command printenv`)。它反映 IDE 实际运行的环境 —— 版本化管理的 Node (如 fnm
96
+ v14)、goenv 的 Go、SDKMAN 的 Java 等 —— 这些 .idea 配置里往往读不到。
70
97
 
71
- 抽取的变量(`src/ide-env.js` 的 `SDK_ENV_KEYS`):`JAVA_HOME` / `JRE_HOME` / `JDK_HOME` / `VIRTUAL_ENV` / `PYTHONPATH` / `PYTHONHOME` / `GOROOT` / `GOPATH` / `GOBIN` / `NODE_PATH` / `NVM_BIN` / `NVM_DIR` / `MAVEN_HOME` / `M2_HOME` / `GRADLE_HOME` / `SDKMAN_DIR` / `CONDA_PREFIX`。
98
+ 抽取的变量 (`src/ide-env.js` 的 `SDK_ENV_KEYS`):`JAVA_HOME` / `JRE_HOME` / `JDK_HOME` / `VIRTUAL_ENV` / `PYTHONPATH` /
99
+ `PYTHONHOME` / `GOROOT` / `GOPATH` / `GOBIN` / `NODE_PATH` / `NVM_BIN` / `NVM_DIR` / `MAVEN_HOME` / `M2_HOME` /
100
+ `GRADLE_HOME` / `SDKMAN_DIR` / `CONDA_PREFIX`。
72
101
 
73
102
  - 注入通过 V2 的 `shell` `create.before` 钩子:只补这些变量 + 前置 PATH,不覆盖你原有的 PATH。
74
- - 需要 IDE MCP 可达 + **Brave Mode**;拿不到时环境为空(不影响其他功能)。
103
+ - 需要 IDE MCP 可达 + **Brave Mode**;拿不到时环境为空 (不影响其他功能)。
75
104
 
76
- > 与 [opencode-env-loader](https://github.com/syllr/opencode-env-loader) 的关系:两者独立。env-loader 负责 `.opencode/env-loader/*` 的**人工** KEY=VALUE 覆盖;本插件负责从 **IDE 实际环境**推导。可以同时使用。
105
+ > 与 [opencode-env-loader](https://github.com/syllr/opencode-env-loader) 的关系:两者独立。env-loader 负责
106
+ > `.opencode/env-loader/*` 的 **人工** KEY=VALUE 覆盖;本插件负责从 **IDE 实际环境**推导。可以同时使用。
77
107
 
78
108
  ## 前置条件
79
109
 
80
- - JetBrains IDE 已开启 MCP 服务(IDE 设置里的 MCP Server / AI Assistant MCP),版本需 **2026.2+**(提供 Streamable HTTP 端点 `/stream`);若关闭,`/open-in-idea` 会立即提示如何重新开启。
110
+ - JetBrains IDE 已开启 MCP 服务 (IDE 设置里的 MCP Server / AI Assistant MCP),版本需 **2026.2+**(提供 Streamable HTTP 端点
111
+ `/stream`);若关闭,`/open-in-idea` 会立即提示如何重新开启。
81
112
  - 要读 IDE 终端环境,需要在 IDE 里开启 **Brave Mode**(否则 `execute_terminal_command` 会等确认);MCP 未就绪时通知会同时提醒开启它。
82
113
  - 插件注册给 OpenCode 的 MCP 配置形如:
83
114
 
@@ -89,9 +120,11 @@ IDE MCP 可用时,插件通过两层轻量引导提高模型主动使用 IDEA MC
89
120
  }
90
121
  ```
91
122
 
92
- > **为什么是 `/stream` 而不是 `/sse`**:OpenCode V2 的远程 MCP 客户端**只支持 Streamable HTTP**,没有旧版 SSE 传输。IDE 的 `/sse` 是旧版 SSE(POST 会返回 405),V2 连不上(报 `Error POSTing to endpoint`)。IDE 2026.2 起额外提供 `/stream` 这个 Streamable HTTP 端点。**插件的探测也走 `/stream`**(一次 `initialize`),协议统一。
123
+ > **为什么是 `/stream` 而不是 `/sse`**:OpenCode V2 的远程 MCP 客户端 **只支持 Streamable HTTP**,没有旧版 SSE 传输。IDE 的
124
+ > `/sse` 是旧版 SSE (POST 会返回 405),V2 连不上 (报 `Error POSTing to endpoint`)。IDE 2026.2 起额外提供 `/stream` 这个
125
+ > Streamable HTTP 端点。 **插件的探测也走 `/stream`**(一次 `initialize`),协议统一。
93
126
 
94
- 插件按 `ports` 顺序选择第一个可用端口,但会并发探测以避免多个端口的超时叠加;默认 `[64342, 6420, 6421, 63342]`。
127
+ 插件按 `ports` 顺序选择第一个可用端口,但会并发探测以避免多个端口的超时叠加;默认 `[64342]`。
95
128
 
96
129
  ## 安装
97
130
 
@@ -101,7 +134,7 @@ IDE MCP 可用时,插件通过两层轻量引导提高模型主动使用 IDEA MC
101
134
  opencode plugin add opencode-idea
102
135
  ```
103
136
 
104
- 等价的配置写法(V2 使用 `plugins` 数组):
137
+ 等价的配置写法 (V2 使用 `plugins` 数组):
105
138
 
106
139
  ```jsonc
107
140
  {
@@ -119,7 +152,8 @@ opencode plugin list
119
152
 
120
153
  ### 本地开发 / 真机验证
121
154
 
122
- 插件未发布时,可以直接指向本地仓库目录。V2 解析**目录**插件时找的是 `<dir>/index.*`(或 `<dir>/server.*`),不会读 `package.json#main`,所以仓库根目录提供了 `index.js` 转发到 `src/index.js`。
155
+ 插件未发布时,可以直接指向本地仓库目录。V2 解析 **目录**插件时找的是 `<dir>/index.*`(或 `<dir>/server.*`),不会读
156
+ `package.json#main`,所以仓库根目录提供了 `index.js` 转发到 `src/index.js`。
123
157
 
124
158
  全局配置 `~/.config/opencode/opencode.json`(V2 桌面端/CLI 共用):
125
159
 
@@ -129,9 +163,10 @@ opencode plugin list
129
163
  }
130
164
  ```
131
165
 
132
- 配置保存后 V2 会监听并自动重载(日志里能看到它开始 watch `index.js` / `src/*.js`),无需重启。改插件源码也会触发重载(此时需重跑 `/open-in-idea`)。
166
+ 配置保存后 V2 会监听并自动重载 (日志里能看到它开始 watch `index.js` / `src/*.js`),无需重启。改插件源码也会触发重载 (此时需重跑
167
+ `/open-in-idea`)。
133
168
 
134
- 另一种零配置方式:在 `~/.config/opencode/plugins/` 放一个转发文件(自动发现,但不能传 options):
169
+ 另一种零配置方式:在 `~/.config/opencode/plugins/` 放一个转发文件 (自动发现,但不能传 options):
135
170
 
136
171
  ```js
137
172
  // ~/.config/opencode/plugins/opencode-idea.js
@@ -148,7 +183,7 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
148
183
  {
149
184
  "package": "opencode-idea",
150
185
  "options": {
151
- "ports": [64342, 6420, 6421, 63342],
186
+ "ports": [64342], // IDE MCP 端口;默认 [64342]
152
187
  "injectEnv": true,
153
188
  "injectGuidance": true, // IDE 可用时向系统提示注入「优先使用 IDEA MCP」引导
154
189
  "openInIde": "idea", // false 关闭;或任意 macOS 应用名(如 "IntelliJ IDEA")
@@ -164,35 +199,90 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
164
199
 
165
200
  不传 options 时全部使用下表默认值。
166
201
 
167
- | 选项 | 类型 | 默认 | 说明 |
168
- | ------------------- | -------------------- | ---------------------------- | ---------------------------------------------------------------- |
169
- | `ports` | `number[]` | `[64342, 6420, 6421, 63342]` | 依次探测的 IDE MCP 端口 |
170
- | `injectEnv` | `boolean` | `true` | 是否注入 IDE 终端环境变量 |
171
- | `injectGuidance` | `boolean` | `true` | IDE 可用时是否向系统提示注入「优先使用 IDEA MCP」引导 |
172
- | `openInIde` | `boolean \| string` | `"idea"` | 拉起哪个 IDE;`false` 关闭,`"idea"` 映射到 IntelliJ IDEA |
173
- | `launchCooldownMs` | `number` | `120000` | 启动保护冷却:冷却期内不重复 spawn |
174
- | `launchMaxAttempts` | `number` | `5` | 单次会话内最多 spawn 次数,超出后停止重试 |
175
- | `mcpProbeTimeoutMs` | `number` | `1000` | 单个 MCP 端口的短探测超时 |
176
- | `feedback` | `"message" \| false` | `"message"` | 结果反馈:`message` 会话通知(用户可见,AI 只回"收到");`false` 静默 |
202
+ | 选项 | 类型 | 默认 | 说明 |
203
+ | ------------------- | -------------------- | ----------- | ---------------------------------------------------------------- |
204
+ | `ports` | `number[]` | `[64342]` | 依次探测的 IDE MCP 端口 |
205
+ | `injectEnv` | `boolean` | `true` | 是否注入 IDE 终端环境变量 |
206
+ | `injectGuidance` | `boolean` | `true` | IDE 可用时是否向系统提示注入「优先使用 IDEA MCP」引导 |
207
+ | `openInIde` | `boolean \| string` | `"idea"` | 拉起哪个 IDE;`false` 关闭,`"idea"` 映射到 IntelliJ IDEA |
208
+ | `launchCooldownMs` | `number` | `120000` | 启动保护冷却:冷却期内不重复 spawn |
209
+ | `launchMaxAttempts` | `number` | `5` | 单次会话内最多 spawn 次数,超出后停止重试 |
210
+ | `mcpProbeTimeoutMs` | `number` | `1000` | 单个 MCP 端口的短探测超时 |
211
+ | `feedback` | `"message" \| false` | `"message"` | 结果反馈:`message` 会话通知(用户可见,AI 只回"收到");`false` 静默 |
177
212
 
178
213
  ## 行为
179
214
 
180
- - **JetBrains 项目**:启动即注入 IDE 终端环境(若此前已连过 IDE);`/open-in-idea` 接入 IDE MCP + 启用引导。
181
- - **非 JetBrains 项目**:不改环境、不注册、不改描述。
182
- - **纯手动**:启动**不探测、不连接 IDE MCP**;IDE MCP 注册、环境重读、系统提示引导全部由 `/open-in-idea` 触发。IDE 中途关闭不会自动撤销,需再执行一次 `/open-in-idea` 重新同步。
183
- - **`/open-in-idea`**:手动打开当前项目并即时加载能力(已在 IDE 打开时则只重新加载);MCP 未开启时只发一次提示并 fast-fail,用户手动开启 MCP + Brave Mode 后重试。
184
- - **IDEA 工具增强**:通过 `ctx.tool.transform` 只更新 `idea_*` / `idea.*` 工具 description,原生 `edit` / `write` / `patch` / `shell` 的定义、描述和可用性均不变。
215
+ - **JetBrains 项目**:启动即注入 IDE 终端环境 (若此前已连过 IDE);`/open-in-idea` 接入 IDE MCP + 启用引导。
216
+ - **非 JetBrains 项目**:不改环境、不注册、不注入任何 IDEA 提示 (每请求零开销)。
217
+ - **手动接入 + 零自主动作**:启动 **不探测、不连接 IDE MCP**;注册、**环境重读**、引导注入全部由 `/open-in-idea` 触发。
218
+ `context` 钩子按「每次模型调用」触发 (含工具续跑),是**请求的纯函数**:它只看本次请求自带的工具快照 ——
219
+ 有 `idea_*` 就剥离隐藏工具并注入引导,没有就只给恢复提示。**不做任何 I/O,没有缓存状态,没有定时器,没有事件订阅**。
220
+ 一次失败的调用不会触发探测或注销,断开的连接也不会被自动重连 —— **恢复的唯一方式是用户再次执行 `/open-in-idea`**。
221
+ - **`/open-in-idea`**:手动打开当前项目并即时加载能力 (已在 IDE 打开时则只重新加载);MCP 未开启时只发一次提示并
222
+ fast-fail,用户手动开启 MCP + Brave Mode 后重试。
223
+ - **不响应工具报错**:插件**不注册** `tool.execute.after` 钩子。一次 `idea_*` 调用失败 (业务错误如 `File not found`、
224
+ 或偶发传输抖动) 不会触发任何探测、注销或重连 —— 连接状态只由 `/open-in-idea` 决定。
185
225
 
186
226
  ## 限制
187
227
 
188
- - 仅支持 **OpenCode V2** 插件 API(`ctx.tool` / `ctx.mcp` / `ctx.shell` / `ctx.command` / `ctx.session` / `ctx.location`)。
228
+ - 仅支持 **OpenCode V2** 插件 API (`ctx.tool` / `ctx.mcp` / `ctx.shell` / `ctx.command` / `ctx.session` /
229
+ `ctx.location`)。
189
230
  - IDE MCP 注册需要 IDE **2026.2+** 的 Streamable HTTP 端点 `/stream`;更早版本只有 `/sse`,OpenCode V2 无法连接。
190
231
  - 环境变量依赖 `execute_terminal_command` + Brave Mode;拿不到时环境为空。
232
+ - 一批用不到的 IDEA MCP 工具被硬编码隐藏 (`src/plugin.js` 的 `HIDDEN_IDEA_TOOLS`,共 31 个):VCS (`idea_git_status` /
233
+ `idea_get_repositories`,版本控制走原生 `git`)、Router 派发工具 (`idea_execute_tool`,未启用 router-only 时冗余)、Debugger
234
+ (全部 `idea_xdebug_*`)、Dev Kit MCP、Inspection KTS MCP、Python Environment MCP、以及数据库的建/改数据源 (数据源由用户在
235
+ IDEA 里配置,AI 只读和查询)。`idea_execute_terminal_command` 也不暴露给模型 (实测:约 60 秒硬截止且拿不到后续输出;
236
+ "看结尾"被原生覆盖)—— 它只由插件内部读取 IDE 环境,而那是插件直接调 MCP,不经过模型工具表。
191
237
  - 探测只确认「MCP 服务是否在监听」,不校验「当前项目是否已在 IDE 打开」;MCP 关闭时会用一次短探测并立即给出设置提示。
192
- - 打开 IDE 仅实现 macOS(`open -a`);首次打开若弹 Trust 对话框,需确认后重试。
238
+ - OpenCode 的 MCP 自动重连**只覆盖 legacy Streamable HTTP 的 session 过期**(`SessionExpiredError`:有 session id + 404 /
239
+ "Server not initialized" → `recover()` + 重试该次调用);modern 端点 (IDE 2026.2,protocol 2025-06-18,不带 session id)
240
+ 的连接被关只会置 `failed` 并移除工具,**不会自动重连**。插件刻意不补这个缺口:恢复一律走手动 `/open-in-idea`。
241
+ - 打开 IDE 仅实现 macOS (`open -a`);首次打开若弹 Trust 对话框,需确认后重试。
193
242
  - IDE MCP 没有「打开项目」工具,因此打开动作走 OS/CLI,而非 MCP。
194
243
  - 插件热重载会断开 IDE 连接,需重跑 `/open-in-idea`。
195
- - 插件只修改 IDEA MCP 工具 description,不修改原生工具定义、描述或可用性;默认调用方式为直接 `idea_*` 工具调用。
244
+ - 插件不修改任何工具的定义、描述或可用性;IDE MCP 工具以 `idea_*` 直接暴露给模型 (`codemode: false`),不用 `execute` 包裹。
245
+
246
+ ### `execute_terminal_command` 与原生 shell:互补,不是替代
247
+
248
+ > **结论:已加入 `HIDDEN_IDEA_TOOLS`,不暴露给模型** —— 下面的对比数据用于说明这个决定,也供手工调用时参考。
249
+ > 理由:① AI 拿不到长任务结果 (约 60 秒硬截止 + 没有工具能读终端缓冲);② "看结尾"被原生覆盖 (原生超限时保尾部,
250
+ > 并把完整输出存成文件);③ 唯一实质价值是"让用户看到 / 接手",而模型不需要它 —— 插件自己的 `printenv` 是
251
+ > **直接调 MCP**(`src/ide-env.js`),不经过模型工具表。
252
+
253
+ 两者能力相近但取舍不同 (macOS + IDEA 2026.2.3 实测):
254
+
255
+ | 维度 | 原生 `shell` | IDE `execute_terminal_command` |
256
+ | ---------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
257
+ | 每次调用增量开销 | ~9ms(OS spawn) | ~90ms(多一次 MCP 往返) |
258
+ | 会话常驻 | ❌ 每次新 shell | ❌ 每次新 shell(`reuseExistingTerminalWindow` 也不保留变量 / `cd`) |
259
+ | `timeout` | ✅ 生效:到点即中止并杀掉进程(长任务须自行调大) | **约 60 秒硬截止**,`timeout` 参数无法改变:到点返回已有输出 + `is_timed_out`,**不杀进程** |
260
+ | 输出截断 | `tool_output` 上限(默认 8000 行 / 256KB);想看结尾得在命令里自己接 `tail` | `maxLinesCount` + `truncateMode`,可**只留结尾** |
261
+ | 用户可见性 | 只在 agent 侧 | 在 IDE 里,用户可看 / 可接手 |
262
+
263
+ **各自优势**
264
+
265
+ - **原生**:更快、超时可控 (到点杀进程)、输出上限更宽 —— **默认走这条**。
266
+ - **IDE**:
267
+ ① **长任务**(安装 / 构建 / 迁移):`reuseExistingTerminalWindow: true` 让它在 IDE 终端里跑,用户 **能看进度、能随时取消**。
268
+ 但工具 **约 60 秒**就返回已产生的输出 (`is_timed_out: true`),之后命令仍在后台跑、AI 拿不到后续 —— 所以它适合
269
+ "**交给用户盯**",不适合"AI 等最终结果"(那要用原生,或让命令把结果写文件再读)。
270
+ ② **精确控制返回哪一段 + 行数**:`truncateMode` 取 `START`(保头)/`MIDDLE`(头+尾)/`END`(保尾)+ `maxLinesCount`。
271
+ 其中 **`MIDDLE` 是真正独有的** —— 原生超限时 **固定保尾部**,并把 **完整输出存成文件**返回路径,所以 `END` 与原生默认重复,
272
+ 而原生那份全量文件反而是 IDE 侧拿不到的 (IDE 丢弃的部分只留在终端)。
273
+
274
+ 判据 (手工调用时的参考):**要"人看" 或 要"头 + 尾一起看" (`MIDDLE`) → IDE;其余 → 原生。**
275
+
276
+ **`truncateMode` 速查 (语义反直觉,以实测为准)**
277
+
278
+ | 传值 | 保留哪部分 |
279
+ | -------------- | -------------------------- |
280
+ | 不传 / `START` | 开头 |
281
+ | `MIDDLE` | 头 + 尾 |
282
+ | **`END`** | **结尾**(截断标记在最上面) |
283
+ | `NONE` | 全部,不截断 |
284
+
285
+ 默认 `maxLinesCount` ≈ 1000 行,要更多就显式传。注意 `END` 是"**保留结尾**",不是"截掉结尾"。
196
286
 
197
287
  ## 测试
198
288
 
@@ -200,7 +290,8 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
200
290
  npm test
201
291
  ```
202
292
 
203
- 使用 [Vitest](https://vitest.dev/),含纯函数测试、IDE 环境解析、环境合并、启动保护状态机、IDE 启动器、引导文本,以及「假 IDE MCP 服务」的端到端探测与插件集成测试。
293
+ 使用 [Vitest](https://vitest.dev/),含纯函数测试、IDE 环境解析、环境合并、启动保护状态机、IDE 启动器、引导文本,以及「假 IDE MCP
294
+ 服务」的端到端探测与插件集成测试。
204
295
 
205
296
  ## License
206
297
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-idea",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
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",
@@ -14,7 +14,8 @@
14
14
  "src/env.js",
15
15
  "src/ide-launcher.js",
16
16
  "src/ide-guidance.js",
17
- "src/idea-tool-descriptions.js",
17
+ "src/ide-run-config-skill.js",
18
+ "src/ide-run-config-skill.md",
18
19
  "README.md",
19
20
  "CHANGELOG.md"
20
21
  ],
@@ -1,7 +1,7 @@
1
1
  // IDEA MCP routing guidance injected into the model's system prompt.
2
2
  //
3
3
  // Two rules for this file:
4
- // 1. State requirements only. No explanations, no asides.
4
+ // 1. State requirements and exact parameters only. No rationale.
5
5
  // 2. The boundary is the project path. Inside it -> idea_* tools.
6
6
  // Outside it -> whatever else is available.
7
7
  //
@@ -18,7 +18,7 @@ export function buildIdeGuidance(projectPath) {
18
18
  `项目路径: \`${projectPath}\``,
19
19
  ``,
20
20
  `## 强制要求`,
21
- `- 路径在 \`${projectPath}\` 内的所有操作,必须使用 \`idea_*\` 工具。`,
21
+ `- 路径在 \`${projectPath}\` 内的代码与文件操作,必须使用 \`idea_*\` 工具。`,
22
22
  `- 直接调用工具,不要用 \`execute\` 包裹。`,
23
23
  `- 检索内容用 \`idea_search_text\` / \`idea_search_regex\`,不要用 \`grep\`。`,
24
24
  `- 查找文件名用 \`idea_search_file\`,不要用 \`glob\`。`,
@@ -27,11 +27,16 @@ export function buildIdeGuidance(projectPath) {
27
27
  `- 修改已有文件用 \`idea_apply_patch\`,不要用 \`edit\` / \`patch\`。`,
28
28
  `- 新建文件用 \`idea_create_new_file\`,不要用 \`write\`。`,
29
29
  `- 改完代码用 \`idea_lint_files\` 或 \`idea_get_file_problems\` 校验。`,
30
- `- 看 git 状态用 \`idea_git_status\`。`,
31
30
  `- 工具参数按该工具的 schema 传。`,
32
31
  ``,
32
+ `## 数据库`,
33
+ `- 数据库操作走 IDEA 的 Database 工具(\`idea_list_database_connections\` / \`idea_introspect_schema\` / \`idea_execute_sql_query\` / \`idea_preview_table_data\` 等),不要用原生 CLI 客户端。`,
34
+ `- 先 \`idea_list_database_connections\` 看有没有现成数据源。`,
35
+ `- 没有数据源时:若目标数据库在 IDEA Database Tools 支持范围内(PostgreSQL / MySQL / MariaDB / Oracle / SQL Server / SQLite / DB2 / H2 / Derby / Sybase / Vertica / Greenplum / ClickHouse / CockroachDB / Snowflake / BigQuery / Redshift / MongoDB / Redis / Cassandra / Couchbase / Hive / DynamoDB / Exasol 等,或任何提供 JDBC 驱动的库),先建议用户在 IDEA 里配好数据源,再回来用工具操作。`,
36
+ `- 不要自己建或改数据源,不要向用户索取数据库凭据。`,
37
+ ``,
33
38
  `## 范围`,
34
- `- \`${projectPath}\` 内:只用 \`idea_*\` 工具。`,
39
+ `- \`${projectPath}\` 内的代码与文件操作:只用 \`idea_*\` 工具。`,
35
40
  `- \`${projectPath}\` 外:用其他可用工具。`,
36
41
  `- \`idea_*\` 工具缺失、连接失败或超时:提示用户执行 \`/open-in-idea\`,不要重试。`,
37
42
  ].join('\n');
@@ -0,0 +1,57 @@
1
+ // The `idea-run-config` skill registered by the plugin.
2
+ //
3
+ // Run configurations are plain XML files under `.run/`. The IDE MCP server can
4
+ // list and execute them (`get_run_configurations` / `execute_run_configuration`)
5
+ // but has no create/update/delete. Hard-coding a template per configuration type
6
+ // would be an open-ended maintenance hole (Application, JUnit, Gradle, npm,
7
+ // Shell Script, Spring Boot, Docker, Compound, ...), so the plugin ships a skill
8
+ // instead: it teaches the model where the files live, how to bootstrap a schema
9
+ // from a real example, and how to verify a write.
10
+ //
11
+ // The text lives in `ide-run-config-skill.md` next to this module, so it is a
12
+ // real skill document and the XML snippets it contains are not mistaken for
13
+ // markup inside a string literal.
14
+ //
15
+ // The skill is registered at setup and does NOT depend on `/open-in-idea`.
16
+
17
+ import { readFileSync } from 'node:fs';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ /** Skill id used by the registry. */
21
+ export const RUN_CONFIG_SKILL_ID = 'idea-run-config';
22
+
23
+ /** Description shown to the model when picking a skill. */
24
+ export const RUN_CONFIG_SKILL_DESCRIPTION = [
25
+ '管理 JetBrains IDE 的 run configuration:在项目级 `.run/*.run.xml` 里创建、修改、删除运行配置。',
26
+ '当用户要新建运行配置、修改或删除已有配置、把脚本或执行流程包成可运行的配置,',
27
+ '或让 AI 执行用户手配的配置时使用。只操作项目级 `.run/` 文件,不碰 `.idea/workspace.xml`。',
28
+ ].join('');
29
+
30
+ const SKILL_FILE = new URL('./ide-run-config-skill.md', import.meta.url);
31
+
32
+ /**
33
+ * Read the skill document shipped next to this module.
34
+ *
35
+ * @returns {string}
36
+ */
37
+ export function readRunConfigSkillContent() {
38
+ return readFileSync(SKILL_FILE, 'utf8').trimEnd();
39
+ }
40
+
41
+ /**
42
+ * Build the skill definition registered through `ctx.skill.transform`.
43
+ *
44
+ * `location` points at the shipped `.md` — the real skill document — so any
45
+ * path-shaped validation is satisfied without inventing a virtual filename.
46
+ *
47
+ * @returns {{ id: string, name: string, description: string, location: string, content: string }}
48
+ */
49
+ export function runConfigSkill() {
50
+ return {
51
+ id: RUN_CONFIG_SKILL_ID,
52
+ name: 'idea-run-config',
53
+ description: RUN_CONFIG_SKILL_DESCRIPTION,
54
+ location: fileURLToPath(SKILL_FILE),
55
+ content: readRunConfigSkillContent(),
56
+ };
57
+ }
@@ -0,0 +1,92 @@
1
+ # JetBrains Run Configuration 管理
2
+
3
+ 本 skill 用于 **管理 JetBrains IDEA 的 run configuration (运行配置)**:创建、修改、删除、查看,并触发运行与调试。配置以 XML
4
+ 文件形式存放在项目里,AI 通过读写这些文件完成管理,再用 IDE MCP 工具做校验和执行。
5
+
6
+ ## 存放位置:只有 `.run/`
7
+
8
+ 所有配置都写 **项目根 `.run/<name>.run.xml`**。这是 AI 唯一能可靠写入、且 IDEA 会 **即时加载**的位置。文件要不要提交到
9
+ git,由用户自己决定,AI 不代劳、也不必追问。
10
+
11
+ ### 为什么不用 `.idea/`
12
+
13
+ - `.idea/workspace.xml` → `RunManager`:IDEA 的原生私有位置,但 **IDEA 是这份文件的主人** ——
14
+ 工作区状态缓存在内存里、保存时整体重写,还有"工作区恢复"机制会覆盖或还原外部改动。AI 写进去不可靠。
15
+ - `.idea/runConfigurations/*.xml`:这是 **2020.1 之前的共享位置**(不是私有位置),而且 `.idea` 常被 gitignore,反而更难共享。
16
+
17
+ 用户已有的、存在 `.idea` 里的配置,AI 仍可 **读取和运行**(`idea_get_run_configurations` 能看到),只是不负责创建、修改或删除。
18
+
19
+ ## 位置速查
20
+
21
+ 这是 **事实参考**(IDEA 实际有三个位置),供识别用户已有配置用:
22
+
23
+ | 位置 | 谁在用 | AI 能否写 |
24
+ |--------------------------------------|--------------------------------------------|-----------------------------------------|
25
+ | `.run/<name>.run.xml`(项目根) | 2020.1+ 的项目级位置 | 能写,IDEA 即时加载 |
26
+ | `.idea/runConfigurations/*.xml` | 2020.1 **之前**的共享位置;现在仍兼容读取 | 一般不动,老项目可沿用 |
27
+ | `.idea/workspace.xml` → `RunManager` | **默认**位置(不勾 "Store as project file") | **不写**(会被 IDEA 覆盖);但能读取和运行 |
28
+
29
+ 新建配置时 `.run/` 的结构:
30
+
31
+ ```xml
32
+
33
+ <component name="ProjectRunConfigurationManager">
34
+ <configuration default="false" name="<名字>" type="<类型 ID>">
35
+ <option name="..." value="..."/>
36
+ <envs/>
37
+ <method v="2"/>
38
+ </configuration>
39
+ </component>
40
+ ```
41
+
42
+ ## 铁律
43
+
44
+ 1. 不要凭空写 option 名。先找真实样本:
45
+ - 用 `idea_search_file` 传 `q="**/*.run.xml"` 看项目里已有的配置。
46
+ - 没有目标类型 → 请用户在 IDEA 里新建一个配置并勾选 "Store as project file",然后读生成的 XML 当模板。
47
+ - 仍然没有 → 直接说明需要该类型的真实样本,不要猜 option 名。
48
+ 2. 写完必须校验:
49
+ - 在 `idea_get_run_configurations` 里能看到才算写对;看不到说明 `type` 写错了,改了重试。
50
+ - 能看到不等于能跑:`idea_execute_run_configuration` 传 `configurationName` 实跑一次确认。
51
+ - IDE MCP 不可用 → 提示用户执行 `/open-in-idea` 后再校验,不要跳过校验当成成功。
52
+ 3. 改配置用定向修改:只用 `idea_apply_patch` 改需要的那几行 option,不要整文件重写。
53
+ 4. 删除配置 = 删除对应的 `.run/<name>.run.xml`;不要试图从 `.idea/workspace.xml` 里删。
54
+
55
+ ## 常见字段
56
+
57
+ - `$PROJECT_DIR$`:项目根;`$MODULE_WORKING_DIR$`:模块目录。
58
+ - 环境变量:
59
+
60
+ ```xml
61
+
62
+ <envs>
63
+ <env name="KEY" value="VALUE"/>
64
+ </envs>
65
+ ```
66
+
67
+ - Before launch 流程:`<method v="2">` 里加 `Make` 选项会先构建。
68
+
69
+ ## 起步参考 (不是契约;动手前一律以真实样本为准)
70
+
71
+ | 用途 | type | 关键 option |
72
+ |------------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
73
+ | Shell 脚本 | `ShConfigurationType` | 内联脚本:`SCRIPT_TEXT` + `EXECUTE_SCRIPT_FILE=false`;脚本文件:`SCRIPT_PATH` + `EXECUTE_SCRIPT_FILE=true`;`INTERPRETER_PATH`;`SCRIPT_WORKING_DIRECTORY` |
74
+ | Java 应用 | `Application` | `MAIN_CLASS_NAME`;`WORKING_DIRECTORY`;`PROGRAM_PARAMETERS`;`VM_PARAMETERS` |
75
+ | JUnit | `JUnit` | `TEST_OBJECT`;`MAIN_CLASS_NAME`;`WORKING_DIRECTORY` |
76
+ | Gradle | `GradleRunConfiguration` | `ExternalProjectPath`;`TaskList` |
77
+ | npm | `js.build_tools.npm` | `package-json`;`command`;`scripts` |
78
+ | 串行流程 | `CompoundRunConfiguration` | `method` 里多个 `RunConfigurationTask`(带 `run_configuration_name` / `run_configuration_type`) |
79
+
80
+ ## 命名
81
+
82
+ - 把 `name` 转成文件名:去掉 `/ \ : * ? " < > |` 等非法字符,写成 `.run/<净化后的名字>.run.xml`。
83
+ - 目标文件已存在时,先确认是要覆盖。
84
+
85
+ ## 流程
86
+
87
+ 1. `idea_search_file` 找 `**/*.run.xml` 样本。
88
+ 2. 有同类样本 → 照它的结构改写;没有 → 请用户建一个。
89
+ 3. `idea_create_new_file` 写 `.run/<name>.run.xml`。
90
+ 4. `idea_get_run_configurations` 校验它出现。
91
+ 5. `idea_execute_run_configuration` 实跑确认。
92
+ 6. 修改用 `idea_apply_patch` 定向改 option;删除后确认它从列表里消失。
package/src/idea-mcp.js CHANGED
@@ -24,8 +24,8 @@ export const IDEA_SERVER_NAME = 'idea';
24
24
  /** Streamable-HTTP endpoint exposed by the IDE MCP server (IntelliJ 2026.2+). */
25
25
  export const MCP_STREAM_PATH = '/stream';
26
26
 
27
- /** Ports to try when the IDE MCP port is not configured explicitly. */
28
- export const DEFAULT_PORTS = [64342, 6420, 6421, 63342];
27
+ /** Port the IDE MCP server listens on when not configured explicitly. */
28
+ export const DEFAULT_PORTS = [64342];
29
29
 
30
30
  const PROTOCOL_VERSION = '2025-06-18';
31
31
  // A probe is only a local availability check. Keep it short: a disabled IDE
@@ -81,7 +81,7 @@ export async function probePort(port, options = {}) {
81
81
  params: {
82
82
  protocolVersion: PROTOCOL_VERSION,
83
83
  capabilities: {},
84
- clientInfo: { name: 'opencode-idea', version: '0.0.4' },
84
+ clientInfo: { name: 'opencode-idea', version: '0.0.6' },
85
85
  },
86
86
  }),
87
87
  signal: controller.signal,
@@ -151,7 +151,7 @@ export async function callTool(port, projectPath, name, args = {}, options = {})
151
151
  params: {
152
152
  protocolVersion: PROTOCOL_VERSION,
153
153
  capabilities: {},
154
- clientInfo: { name: 'opencode-idea', version: '0.0.4' },
154
+ clientInfo: { name: 'opencode-idea', version: '0.0.6' },
155
155
  },
156
156
  });
157
157
  const response = await post({
package/src/plugin.js CHANGED
@@ -8,7 +8,10 @@
8
8
  // IDEA MCP tools, and updates only IDEA MCP tool descriptions;
9
9
  // 3. registers the `/open-in-idea` command, which opens the current project
10
10
  // in IntelliJ IDEA (reusing a running instance) and then loads the IDE
11
- // capabilities immediately.
11
+ // capabilities immediately;
12
+ // 4. registers the `idea-run-config` skill (JetBrains projects only),
13
+ // which teaches the model how to manage `.run/*.run.xml` run
14
+ // configurations — a capability the IDE MCP server does not expose.
12
15
  //
13
16
  // Non-JetBrains projects are left untouched. Everything is manual: setup does
14
17
  // NOT probe or connect the IDE (only the IDE-terminal env is read when a probe
@@ -37,16 +40,116 @@ import {
37
40
  appendIdeGuidance,
38
41
  appendIdeRecoveryGuidance,
39
42
  } from './ide-guidance.js';
43
+ import { runConfigSkill } from './ide-run-config-skill.js';
40
44
 
41
45
  const DEFAULT_LAUNCH_COOLDOWN_MS = 120000;
42
46
  const DEFAULT_LAUNCH_MAX_ATTEMPTS = 5;
43
47
  // MCP availability is a fast preflight. If it is not ready, fail fast and let
44
48
  // the user enable MCP + Brave Mode in IDEA before retrying the command.
45
49
  const DEFAULT_MCP_PROBE_TIMEOUT_MS = 1000;
50
+ // A cold IDE can take tens of seconds before its bundled MCP server listens.
51
+ // After `open -a`, poll for the endpoint instead of reporting failure once.
52
+ const DEFAULT_MCP_START_TIMEOUT_MS = 60000;
53
+ const MCP_START_POLL_MS = 1000;
54
+ const DEFAULT_TOOL_REFRESH_TIMEOUT_MS = 5000;
55
+ const TOOL_REFRESH_POLL_MS = 25;
56
+ const IDEA_READY_TOOL_IDS = new Set(['idea_read_file', 'idea_apply_patch']);
57
+
58
+ /**
59
+ * IDEA MCP tools that must never reach the model.
60
+ *
61
+ * Grouped by the IDE's Exposed Tools page so the list stays auditable. Anything
62
+ * listed here is stripped from every model request, even while the IDE is up.
63
+ *
64
+ * - VCS: `idea_git_status` returns porcelain codes only and
65
+ * `idea_get_repositories` is covered by the native git tool, which also
66
+ * handles diff/log/blame.
67
+ * - Router: `idea_execute_tool` is the IDE's router-mode dispatcher. Router-only
68
+ * mode is off, so every routed tool is exposed directly with its full schema
69
+ * and this dispatcher is redundant. Revisit if router-only mode is enabled.
70
+ * - Debugger (`xdebug_*`): the Debugger MCP toolset. Rarely needed, and a real
71
+ * debug session is driven from the IDE window anyway.
72
+ * - Dev Kit MCP: IntelliJ Platform plugin-development tools (Split Mode module
73
+ * kinds, the New IntelliJ Module scaffold, Read/Write lock and EDT threading
74
+ * analysis). Useless outside IntelliJ Platform plugin work.
75
+ * - Inspection KTS MCP: inspection.kts authoring helpers (PSI tree, API docs,
76
+ * examples, run). Also IntelliJ Platform plugin-development only.
77
+ * - Python Environment MCP: Python interpreter detection and configuration.
78
+ * Hidden; the IDE-injected shell env already carries the SDK paths.
79
+ * - Database data sources: connections are configured by the user in the IDE,
80
+ * so the AI only reads and queries them; create/edit stay hidden.
81
+ */
82
+ export const HIDDEN_IDEA_TOOLS = [
83
+ // VCS
84
+ 'idea_git_status',
85
+ 'idea_get_repositories',
86
+ // Router
87
+ 'idea_execute_tool',
88
+ // Terminal: the plugin reads the IDE env through its own direct MCP call
89
+ // (`src/ide-env.js`), so the model never needs this tool. Measured value for
90
+ // the agent is nil: ~60s hard cutoff with no way to read the terminal buffer,
91
+ // and the native shell already keeps the output tail plus a full-output file.
92
+ 'idea_execute_terminal_command',
93
+ // Debugger
94
+ 'idea_xdebug_control_session',
95
+ 'idea_xdebug_evaluate_expression',
96
+ 'idea_xdebug_get_debugger_status',
97
+ 'idea_xdebug_get_frame_values',
98
+ 'idea_xdebug_get_stack',
99
+ 'idea_xdebug_get_threads',
100
+ 'idea_xdebug_get_value_by_path',
101
+ 'idea_xdebug_list_breakpoints',
102
+ 'idea_xdebug_remove_breakpoint',
103
+ 'idea_xdebug_run_to_line',
104
+ 'idea_xdebug_set_breakpoint',
105
+ 'idea_xdebug_set_variable',
106
+ 'idea_xdebug_start_debugger_session',
107
+ // Dev Kit MCP
108
+ 'idea_collect_split_mode_compatibility_issues',
109
+ 'idea_create_ij_module',
110
+ 'idea_find_lock_requirements_usages',
111
+ 'idea_find_threading_requirements_usages',
112
+ 'idea_recognize_ij_module_kind',
113
+ 'idea_recognize_split_mode_api_kind',
114
+ // Inspection KTS MCP
115
+ 'idea_generate_inspection_kts_api',
116
+ 'idea_generate_inspection_kts_examples',
117
+ 'idea_generate_psi_tree',
118
+ 'idea_run_inspection_kts',
119
+ // Python Environment MCP
120
+ 'idea_configure_python_interpreter',
121
+ 'idea_get_python_environment',
122
+ // Database data sources: the user configures them in the IDE, the AI only
123
+ // reads and queries them.
124
+ 'idea_create_database_connection',
125
+ 'idea_edit_database_connection',
126
+ ];
127
+
128
+ /**
129
+ * Remove the given IDEA MCP definitions from one request's tool record.
130
+ *
131
+ * @param {Record<string, unknown> | undefined} tools
132
+ * @param {readonly string[]} names
133
+ */
134
+ function removeIdeaToolDefinitions(tools, names) {
135
+ if (!tools || typeof tools !== 'object') return 0;
136
+ let removed = 0;
137
+ for (const name of names) {
138
+ if (!name.startsWith('idea_') || !(name in tools)) continue;
139
+ delete tools[name];
140
+ removed += 1;
141
+ }
142
+ return removed;
143
+ }
144
+
145
+ /** @param {ReadonlyArray<{ id?: string }> | undefined} tools */
146
+ function toolIds(tools) {
147
+ return (tools ?? []).map((tool) => tool.id).filter((id) => typeof id === 'string').sort();
148
+ }
46
149
 
47
150
  /**
48
151
  * @typedef {{
49
- * status: 'connected' | 'mcp-unavailable' | 'launch-failed' | 'disabled',
152
+ * status: 'connected' | 'tools-loading' | 'mcp-unavailable' | 'launch-failed' | 'disabled',
50
153
  * port?: number,
51
154
  * opened?: boolean,
52
155
  * unavailableNoticeSent?: boolean
@@ -71,10 +174,12 @@ const asPositiveNumber = (value, fallback) =>
71
174
  export function ideFeedback(result) {
72
175
  switch (result?.status) {
73
176
  case 'connected':
74
- return 'IDE MCP 已连接;IDE MCP 与项目环境已即时加载。';
177
+ return 'IDE MCP 已连接;IDEA 原生工具已注册并可用于后续请求。';
178
+ case 'tools-loading':
179
+ return 'IDE MCP 已连接,但原生 IDEA 工具列表尚未完成注册。请稍后再次执行 /open-in-idea。';
75
180
  case 'mcp-unavailable':
76
181
  return result?.opened
77
- ? '已请求打开/激活 IntelliJ IDEA,但未检测到 MCP 服务。请在 IDEA 中开启 MCP 服务(Settings → MCP Server,需 2026.2+)并启用 Brave Mode,然后再次执行 /open-in-idea。'
182
+ ? '已请求打开/激活 IntelliJ IDEA 并等待 MCP 服务启动,但未检测到 MCP 服务。请在 IDEA 中开启 MCP 服务(Settings → MCP Server,需 2026.2+)并启用 Brave Mode,然后再次执行 /open-in-idea。'
78
183
  : '未检测到 IntelliJ IDEA 的 MCP 服务。请确认 IDEA 已打开,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后再次执行 /open-in-idea。';
79
184
  case 'launch-failed':
80
185
  return '无法拉起 IntelliJ IDEA(未找到应用或启动失败)。请确认已安装 IDEA,或手动打开后重试 /open-in-idea。';
@@ -112,6 +217,8 @@ export default {
112
217
  * launchCooldownMs?: number,
113
218
  * launchMaxAttempts?: number,
114
219
  * mcpProbeTimeoutMs?: number,
220
+ * mcpStartTimeoutMs?: number,
221
+ * toolRefreshTimeoutMs?: number,
115
222
  * feedback?: false | 'message',
116
223
  * },
117
224
  * command?: {
@@ -121,11 +228,26 @@ export default {
121
228
  * execute: (input: { sessionID: string, prompt: { text: string }, delivery: unknown }) => Promise<void>,
122
229
  * }) => void }) => void) => Promise<{ dispose: () => Promise<void> | void }>,
123
230
  * },
231
+ * skill?: {
232
+ * transform: (cb: (editor: { add: (definition: {
233
+ * id: string,
234
+ * name: string,
235
+ * description: string,
236
+ * location: string,
237
+ * content: string,
238
+ * }) => void }) => void) => Promise<{ dispose: () => Promise<void> | void }>,
239
+ * },
124
240
  * session?: {
125
241
  * prompt: (input: { sessionID: string, text: string, delivery: unknown }) => Promise<unknown>,
126
- * hook: (name: string, cb: (event: { system?: unknown[] }) => void) => Promise<{ dispose: () => Promise<void> | void }>,
242
+ * hook: (name: string, cb: (event: {
243
+ * sessionID?: string,
244
+ * agent?: string,
245
+ * system?: unknown[],
246
+ * tools?: Record<string, unknown>,
247
+ * }) => void | Promise<void>) => Promise<{ dispose: () => Promise<void> | void }>,
127
248
  * },
128
249
  * tool?: {
250
+ * list?: () => Promise<ReadonlyArray<{ id?: string }>>,
129
251
  * reload?: () => Promise<void>,
130
252
  * },
131
253
  * mcp: {
@@ -138,6 +260,11 @@ export default {
138
260
  async setup(ctx) {
139
261
  const projectPath = currentProjectPath(ctx.location);
140
262
  if (!projectPath) return;
263
+ // Where IDEA tools can exist at all. Computed once: the recovery guidance is
264
+ // injected per model request, so it must not cost a filesystem check each
265
+ // time, and unrelated projects must not receive IDEA prompts they can never
266
+ // use.
267
+ const isIdeaProject = hasIdeaDirectory(projectPath);
141
268
 
142
269
  const options = ctx.options || {};
143
270
  const ports = asArray(options.ports, DEFAULT_PORTS);
@@ -145,6 +272,8 @@ export default {
145
272
  const injectGuidance = options.injectGuidance !== false;
146
273
  const ideApp = resolveIdeApp(options.openInIde);
147
274
  const mcpProbeTimeoutMs = asPositiveNumber(options.mcpProbeTimeoutMs, DEFAULT_MCP_PROBE_TIMEOUT_MS);
275
+ const mcpStartTimeoutMs = asPositiveNumber(options.mcpStartTimeoutMs, DEFAULT_MCP_START_TIMEOUT_MS);
276
+ const toolRefreshTimeoutMs = asPositiveNumber(options.toolRefreshTimeoutMs, DEFAULT_TOOL_REFRESH_TIMEOUT_MS);
148
277
  // "message" (default) injects a clearly-labelled notification that the
149
278
  // model only acknowledges ("收到"); the user sees the clean notice via
150
279
  // metadata.displayText. false is silent.
@@ -153,9 +282,6 @@ export default {
153
282
  cooldownMs: asPositiveNumber(options.launchCooldownMs, DEFAULT_LAUNCH_COOLDOWN_MS),
154
283
  maxAttempts: asPositiveNumber(options.launchMaxAttempts, DEFAULT_LAUNCH_MAX_ATTEMPTS),
155
284
  });
156
-
157
- /** @type {number | undefined} */
158
- let activePort;
159
285
  let currentEnv = { env: {}, prependPath: [] };
160
286
 
161
287
  /** @type {Array<{ dispose: () => Promise<void> | void }>} */
@@ -166,10 +292,11 @@ export default {
166
292
  let sessionRegistration;
167
293
  /** @type {{ dispose: () => Promise<void> | void } | undefined} */
168
294
  let commandRegistration;
295
+ /** @type {{ dispose: () => Promise<void> | void } | undefined} */
296
+ let skillRegistration;
169
297
  let disposed = false;
170
298
 
171
299
  const deactivateIde = async () => {
172
- activePort = undefined;
173
300
  const current = registrations.slice();
174
301
  registrations.length = 0;
175
302
  if (current.length === 0) return;
@@ -182,31 +309,66 @@ export default {
182
309
  }
183
310
  };
184
311
 
185
- const activateIde = async (port) => {
186
- const mcpRegistration = await ctx.mcp.transform((editor) => {
187
- editor.set(IDEA_SERVER_NAME, serverConfig(port, projectPath));
188
- });
189
- registrations = [mcpRegistration];
190
- activePort = port;
312
+ const waitForIdeaTools = async () => {
313
+ if (typeof ctx.tool?.list !== 'function') return false;
314
+ const deadline = Date.now() + toolRefreshTimeoutMs;
315
+ while (Date.now() < deadline) {
316
+ try {
317
+ const ids = toolIds(await ctx.tool.list());
318
+ if ([...IDEA_READY_TOOL_IDS].every((id) => ids.includes(id))) return true;
319
+ } catch {
320
+ // The MCP catalog may still be settling; retry within the bounded wait.
321
+ }
322
+ await new Promise((resolve) => setTimeout(resolve, TOOL_REFRESH_POLL_MS));
323
+ }
324
+ return false;
325
+ };
326
+
327
+ const refreshIdeTools = async () => {
191
328
  if (typeof ctx.mcp.reload === 'function') await ctx.mcp.reload();
192
329
  // Reconcile the MCP servers above, then replay the tool registry so the
193
330
  // refreshed catalog lands in the next model request's tool snapshot.
194
331
  // Without this, a session that already captured a snapshot keeps the old
195
332
  // tool list until the session is reopened.
196
333
  if (typeof ctx.tool?.reload === 'function') await ctx.tool.reload();
334
+ return waitForIdeaTools();
335
+ };
336
+
337
+ const activateIde = async (port) => {
338
+ const mcpRegistration = await ctx.mcp.transform((editor) => {
339
+ editor.set(IDEA_SERVER_NAME, serverConfig(port, projectPath));
340
+ });
341
+ registrations = [mcpRegistration];
342
+ return refreshIdeTools();
343
+ };
344
+
345
+ /**
346
+ * Is the IDE MCP server contributing tools right now? OpenCode's registry is
347
+ * the source of truth: it adds the tools when a server connects and drops
348
+ * them when the connection stops (`stopServer` publishes `mcp.tools.changed`
349
+ * and the registry reloads). No probe of our own is involved, and only
350
+ * `/open-in-idea` asks, so the read is user-initiated.
351
+ */
352
+ const ideaToolsRegistered = async () => {
353
+ if (typeof ctx.tool?.list !== 'function') return false;
354
+ try {
355
+ return toolIds(await ctx.tool.list()).some((id) => id.startsWith('idea_'));
356
+ } catch {
357
+ return false;
358
+ }
197
359
  };
198
360
 
199
361
  const reconcileOnce = async ({ probeIde = true } = {}) => {
200
- if (disposed) return;
362
+ if (disposed) return false;
201
363
 
202
364
  // Manual: only `/open-in-idea` probes the IDE. Setup calls this with
203
365
  // `probeIde: false` so it never connects until the command asks.
204
366
  const port = probeIde ? await findIdePort(ports, { timeoutMs: mcpProbeTimeoutMs }) : undefined;
205
- const isJetBrains = hasIdeaDirectory(projectPath) || port !== undefined;
367
+ const isJetBrains = isIdeaProject || port !== undefined;
206
368
  if (!isJetBrains) {
207
369
  currentEnv = { env: {}, prependPath: [] };
208
370
  await deactivateIde();
209
- return;
371
+ return false;
210
372
  }
211
373
 
212
374
  // Environment: read from the IDE integrated terminal, the single source
@@ -220,22 +382,46 @@ export default {
220
382
  shellRegistration = await ctx.shell.hook('create.before', (input) => applyEnv(input, currentEnv));
221
383
  }
222
384
 
223
- if (port === activePort) return;
385
+ // A live server is only refreshed; a dead one must be replaced, because
386
+ // `reconcile()` skips an unchanged config and would never reconnect it.
387
+ // The registry says which case this is.
388
+ if (port !== undefined && (await ideaToolsRegistered())) return refreshIdeTools();
224
389
  await deactivateIde();
225
- if (port !== undefined) await activateIde(port);
390
+ if (port === undefined) return false;
391
+ return activateIde(port);
226
392
  };
227
393
 
228
394
  // Serialize reconciles so overlapping callers can never register/activate
229
395
  // the IDE concurrently (which would leak registrations).
230
396
  let reconcileChain = Promise.resolve();
231
- const reconcile = (options) => {
232
- const next = reconcileChain.then(() => reconcileOnce(options));
397
+ const enqueue = (task) => {
398
+ const next = reconcileChain.then(task);
233
399
  reconcileChain = next.then(
234
400
  () => {},
235
401
  () => {},
236
402
  );
237
403
  return next;
238
404
  };
405
+ const reconcile = (options) => enqueue(() => reconcileOnce(options));
406
+
407
+ /**
408
+ * `open -a` only requests activation/opening. A cold IDE needs time before
409
+ * its bundled MCP server listens, so poll until the endpoint appears. This
410
+ * is the step that makes a single `/open-in-idea` enough on a cold start.
411
+ *
412
+ * @returns {Promise<number | undefined>}
413
+ */
414
+ const waitForIdeServer = async () => {
415
+ const deadline = Date.now() + mcpStartTimeoutMs;
416
+ while (Date.now() < deadline) {
417
+ const port = await findIdePort(ports, { timeoutMs: mcpProbeTimeoutMs }).catch(() => undefined);
418
+ if (port !== undefined) return port;
419
+ const remaining = deadline - Date.now();
420
+ if (remaining <= 0) break;
421
+ await new Promise((resolve) => setTimeout(resolve, Math.min(MCP_START_POLL_MS, remaining)));
422
+ }
423
+ return undefined;
424
+ };
239
425
 
240
426
  // Re-entrant: repeat invocations share one open request so the IDE is not
241
427
  // spawned twice while the application is starting.
@@ -277,9 +463,15 @@ export default {
277
463
  guard.markSpawn();
278
464
  const launched = await openInIde(projectPath, { app: ideApp });
279
465
  if (!launched) return { status: 'launch-failed' };
280
- // `open -a` only requests activation/opening. Fast-fail if the MCP
281
- // endpoint is not already available; the user can enable it in IDEA
282
- // and run `/open-in-idea` again instead of waiting on a timer.
466
+ // `open -a` only requests activation/opening. A cold IDE needs time to
467
+ // start its bundled MCP server, so wait for the endpoint instead of
468
+ // reporting failure immediately.
469
+ const started = await waitForIdeServer();
470
+ if (started !== undefined) {
471
+ guard.reset();
472
+ launchNoticeSent = false;
473
+ return { status: 'connected', port: started, unavailableNoticeSent: false };
474
+ }
283
475
  await reportUnavailable(true);
284
476
  return { status: 'mcp-unavailable', opened: true, unavailableNoticeSent: launchNoticeSent };
285
477
  })();
@@ -312,6 +504,17 @@ export default {
312
504
  }
313
505
  };
314
506
 
507
+ // The run-configuration skill is static knowledge — where `.run/*.run.xml`
508
+ // live, how to bootstrap a schema from a real example, and how to verify a
509
+ // write. JetBrains projects only, and independent of `/open-in-idea`.
510
+ if (isIdeaProject && typeof ctx.skill?.transform === 'function') {
511
+ // Read the document before the transform: callbacks must stay synchronous.
512
+ const runConfigSkillDefinition = runConfigSkill();
513
+ skillRegistration = await ctx.skill.transform((editor) => {
514
+ editor.add(runConfigSkillDefinition);
515
+ });
516
+ }
517
+
315
518
  // `/open-in-idea`: manual, re-entrant trigger that also loads the IDE
316
519
  // capabilities immediately.
317
520
  if (typeof ctx.command?.transform === 'function') {
@@ -328,32 +531,50 @@ export default {
328
531
  return unavailableNoticeSent;
329
532
  },
330
533
  });
331
- const { port, status, unavailableNoticeSent: resultNoticeSent } = result;
534
+ const { port } = result;
535
+ let toolsReady = false;
332
536
  try {
333
537
  // Do not repeat the expensive probe when the preflight already
334
538
  // found no MCP endpoint. The command will retry after the user
335
539
  // enables MCP + Brave Mode.
336
- await reconcile({ probeIde: port !== undefined });
540
+ toolsReady = (await reconcile({ probeIde: port !== undefined })) === true;
337
541
  } catch {
338
542
  // capability load is best effort; feedback still matters
339
543
  }
544
+ const finalResult = result.status === 'connected' && !toolsReady
545
+ ? { ...result, status: 'tools-loading' }
546
+ : result;
340
547
  // The unavailable notice is the final result for this command. If
341
548
  // it was already delivered, do not show the same warning twice.
342
- const unavailableAlreadySent = unavailableNoticeSent || resultNoticeSent;
343
- if (!unavailableAlreadySent || status !== 'mcp-unavailable') {
344
- await sendFeedback(input, result);
549
+ const unavailableAlreadySent = unavailableNoticeSent || result.unavailableNoticeSent;
550
+ if (!unavailableAlreadySent || finalResult.status !== 'mcp-unavailable') {
551
+ await sendFeedback(input, finalResult);
345
552
  }
346
553
  },
347
554
  });
348
555
  });
349
556
  }
350
557
 
351
- // Direct-call guidance: while the IDEA MCP is available, append concise
352
- // routing rules to every primary request. Native tools are left untouched.
353
- if (injectGuidance && typeof ctx.session?.hook === 'function') {
558
+ // Hide the tools the model must not see, and point it at the rest. This hook
559
+ // runs for every agent-loop step (including tool-driven continuations), so
560
+ // it is deliberately a pure function of the request: no I/O, no cached
561
+ // connection state, and no action of its own.
562
+ //
563
+ // `event.tools` is this request's tool snapshot, and OpenCode's own registry
564
+ // adds IDEA tools when the server connects and drops them when it stops
565
+ // (`stopServer` publishes `mcp.tools.changed`, which the registry turns into
566
+ // a reload). So the snapshot is also the honest answer to "is the IDE
567
+ // serving?".
568
+ if (typeof ctx.session?.hook === 'function') {
354
569
  sessionRegistration = await ctx.session.hook('context', (event) => {
355
- appendIdeRecoveryGuidance(event?.system);
356
- if (activePort === undefined) return;
570
+ const tools = event?.tools;
571
+ const serving = tools !== undefined && Object.keys(tools).some((name) => name.startsWith('idea_'));
572
+ if (serving) removeIdeaToolDefinitions(tools, HIDDEN_IDEA_TOOLS);
573
+ if (isIdeaProject || serving) appendIdeRecoveryGuidance(event?.system);
574
+ if (!injectGuidance) return;
575
+ // Guidance promises tools the model can call, so it only goes out when
576
+ // they are actually in this request.
577
+ if (!serving) return;
357
578
  appendIdeGuidance(event?.system, projectPath);
358
579
  });
359
580
  }
@@ -381,6 +602,13 @@ export default {
381
602
  // best effort
382
603
  }
383
604
  }
605
+ if (skillRegistration) {
606
+ try {
607
+ await skillRegistration.dispose();
608
+ } catch {
609
+ // best effort
610
+ }
611
+ }
384
612
  await deactivateIde();
385
613
  };
386
614
  },