opencode-idea 0.0.10 → 0.0.12
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 +151 -88
- package/package.json +1 -3
- package/src/ide-guidance.js +29 -4
- package/src/ide-launcher.js +58 -0
- package/src/mcp/idea.js +91 -17
- package/src/plugin.js +331 -166
- package/src/env.js +0 -39
- package/src/mcp/ide-env.js +0 -101
package/README.md
CHANGED
|
@@ -1,59 +1,75 @@
|
|
|
1
1
|
# opencode-idea
|
|
2
2
|
|
|
3
|
-
OpenCode 插件,面向 JetBrains 项目 (IntelliJ IDEA / PyCharm / WebStorm 等)
|
|
3
|
+
OpenCode 插件,面向 JetBrains 项目 (IntelliJ IDEA / PyCharm / WebStorm 等),做两件事:
|
|
4
4
|
|
|
5
5
|
1. **接入 IDE MCP** —— 用 `/open-in-idea` 把当前项目在 IntelliJ IDEA 中打开,并把这个 IDE 的 MCP 服务 (60 个工具:
|
|
6
6
|
检索、读、改、构建、重构、调试、数据库……)注册进 OpenCode。
|
|
7
7
|
2. **优先使用 IDE 工具** —— IDE 可用时,向模型注入"项目内操作优先使用 IDEA MCP"的系统提示引导 + 能力映射;IDE 不可用时回退原生工具
|
|
8
8
|
(正常回退,无需声明)。
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
> **项目命令走 IDE 终端**:跑 `java` / `python` / `npm` 这类项目命令时,引导模型用 `idea_execute_terminal_command`
|
|
11
|
+
> (`executeInShell: true`)而不是原生 shell —— 它在 IDE 集成终端里执行,**天然继承用户在 IDE 里配好的 JDK / Node / Python 版本**
|
|
12
|
+
> (含 fnm / SDKMAN 这类靠 shell init 切版本的场景)。详见「项目命令执行」。
|
|
10
13
|
|
|
11
14
|
非 JetBrains 项目完全不受影响。 **手动接入**:插件启动不探测、不连接 IDE,只有跑 `/open-in-idea` 才接入。跑过一次后,该**项目**
|
|
12
|
-
被「标记」(持久化在 `ctx.storage`,重启 OpenCode 后仍在)
|
|
13
|
-
|
|
15
|
+
被「标记」(持久化在 `ctx.storage`,重启 OpenCode 后仍在):之后插件在后台**每 5s 探活**该 IDE——**掉线就注销 `idea` MCP**(杀掉不会自愈的
|
|
16
|
+
stdio 僵尸桥 + 移除 `idea_*` 工具,让模型调不到、**不会挂起**),并锁存"待重建";**下一条用户消息**读到锁存后**静默重建**(用户无感)。
|
|
17
|
+
`/close-in-idea` 注销 IDE MCP + 删除标记。没跑过 `/open-in-idea` 的项目保持全手动、零自主动作。
|
|
14
18
|
|
|
15
19
|
## 工作原理
|
|
16
20
|
|
|
17
21
|
```text
|
|
18
22
|
setup(启动, 每项目一次):
|
|
19
23
|
a. 读项目标记(ctx.storage: project/<项目路径>; OpenCode 重启后仍生效)
|
|
20
|
-
b.
|
|
21
|
-
c. 注册 /open-in-idea 与 /close-in-idea 命令 + session context/prompt 钩子
|
|
24
|
+
b. 注册 /open-in-idea 与 /close-in-idea 命令 + session context 钩子 + tool execute.after 钩子
|
|
22
25
|
不探测、不注册 MCP
|
|
23
26
|
|
|
24
|
-
/open-in-idea(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
4.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
6. 发一条结果通知(用户看到干净气泡,模型只回"收到")
|
|
27
|
+
/open-in-idea(手动触发, 阻塞到就绪):
|
|
28
|
+
1. 写项目标记 = true + 同步内存缓存;注册 `idea-run-config` skill
|
|
29
|
+
2. 探测本地 JetBrains IDE MCP 服务(/stream)
|
|
30
|
+
- 已就绪 → 再问 IDE「当前项目是否已打开」(`get_project_modules`,带项目路径)
|
|
31
|
+
· 已打开 → 不重复 open;未打开 → open -a 打开
|
|
32
|
+
- 未就绪(冷启动)→ open -a 拉起,再轮询等到就绪(上限 60s)
|
|
33
|
+
4. **先注销旧的 idea MCP(杀掉旧的 stdio 桥子进程),再重新注册**
|
|
34
|
+
(`idea stdioMcpServer`,env 带项目路径 + 端口;每项目一个子进程)
|
|
35
|
+
—— 每次执行都重建,因为断掉的桥不会自愈、也不能靠 refresh 复活
|
|
36
|
+
5. 等 `idea_*` 工具进入注册表;读 IDE 终端环境更新 shell 环境;启用引导
|
|
37
|
+
6. 发结果通知(用户看到干净气泡,模型只回"收到")
|
|
36
38
|
|
|
37
39
|
/close-in-idea(手动触发):
|
|
38
|
-
1. 删项目标记 +
|
|
39
|
-
2.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
1.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
1. 删项目标记 + 同步内存缓存 + 注销 IDE MCP + 清空注入的环境
|
|
41
|
+
2. 发结果通知
|
|
42
|
+
|
|
43
|
+
工具失败触发(仅在被标记的项目里;需先 /open-in-idea 过一次):
|
|
44
|
+
1. `idea_*` 工具调用失败 → tool execute.after 钩子**分类**:
|
|
45
|
+
- IDE MCP 不可用(掉了)→ 重跑接入流程
|
|
46
|
+
- IDE 在、但项目没打开("Unable to determine the target project" / "doesn't correspond to any open project")→ 让 IDEA 重新打开该项目
|
|
47
|
+
- 普通业务错误(File not found 等)→ 不动作
|
|
48
|
+
2. 触发成功 → 把该错误改写为"已重连 / 已重新打开,请重试",模型同一回合重试;失败 → 保持原错误
|
|
49
|
+
|
|
50
|
+
后台探活(心跳;仅"已标记 + 桥已注册"的项目):
|
|
51
|
+
1. 每 5s 探测 IDE 端口(/stream);连续 2 次不通 →
|
|
52
|
+
2. **注销 `idea` MCP**:杀掉僵尸桥子进程 + 从注册表移除 `idea_*` 工具
|
|
53
|
+
(→ 模型调不到,也就不会挂起;这一步是"从根上避免挂起"的关键)
|
|
54
|
+
3. 锁存 `needRebuild = true`(不会因端口恢复而清,因为桥不会自愈)
|
|
55
|
+
4. 下一条用户消息:纯内存读到锁存 → 自动重跑接入流程(IDE 不在则 `open -a` 拉起),成功后清锁存
|
|
56
|
+
—— 全程静默,用户无感
|
|
57
|
+
※ 这是**每个 Location(项目)各跑一份**探活;IDE 只有一个,所以所有项目的桥几乎同时被注销、再各自重建
|
|
46
58
|
```
|
|
47
59
|
|
|
48
60
|
## 在 IDE 中打开项目 (`/open-in-idea`)
|
|
49
61
|
|
|
50
62
|
在会话里输入 `/open-in-idea`:
|
|
51
63
|
|
|
52
|
-
1.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
1. **让运行中的 IDEA 实例打开/激活当前项目**:`open -a "IntelliJ IDEA" <项目目录>` —— **复用已运行的实例**(不会重复开新实例),
|
|
65
|
+
项目已打开时只把它切到前台。**无论 MCP 是否已就绪都会执行这一步**:端点能应答只说明 IDEA 在运行(可能开的是**别的**项目),
|
|
66
|
+
不代表当前项目已打开;
|
|
67
|
+
2. 再探测 `/stream`;已就绪 → 直接进入下一步;
|
|
68
|
+
3. 未就绪 → 说明 IDEA 是冷启动,继续用 `open -a` 拉起并**轮询等到 MCP 就绪**(冷启动约数十秒,上限 60s);
|
|
69
|
+
4. 等不到(例如 IDE 里 MCP 服务未开启)→ 提示在 IDEA 的 `Settings → MCP Server` 开启 MCP 服务并启用 **Brave Mode**,用户设置好后
|
|
70
|
+
再次执行 `/open-in-idea`;
|
|
71
|
+
5. 就绪后 **先注销旧的 `idea` MCP(杀掉旧的 stdio 桥子进程)→ 再重新注册**(每次执行都重建,因为断掉的桥不会自愈),然后重读
|
|
72
|
+
IDE 环境、启用系统引导,并**等到 `idea_*` 工具进入注册表**才返回。
|
|
57
73
|
|
|
58
74
|
> **通知不触发 AI 干活**:默认 `feedback: "message"` 把结果作为一条 **标注清楚的通知**发到会话里 —— 用户看到干净的通知气泡
|
|
59
75
|
> (经 `metadata.displayText`),模型只看到"这是通知,请只回复『收到』"的指令,因此不会执行任何操作、不会调用工具。
|
|
@@ -68,8 +84,8 @@ Brave Mode 后再次执行 `/open-in-idea` 即可接上。
|
|
|
68
84
|
|
|
69
85
|
## 关闭接入 (`/close-in-idea`)
|
|
70
86
|
|
|
71
|
-
输入 `/close-in-idea
|
|
72
|
-
|
|
87
|
+
输入 `/close-in-idea`:**注销当前项目的 IDE MCP** + 删除项目标记(持久化 + 内存缓存一并清),并清空注入的环境变量。
|
|
88
|
+
之后 `idea_*` 工具不再可用,也不会再因失败自动接入。想重新接入,再次执行 `/open-in-idea`。
|
|
73
89
|
|
|
74
90
|
## IDE 工具优先 (直接调用引导)
|
|
75
91
|
|
|
@@ -108,39 +124,65 @@ IDE MCP 只能 **查**(`get_run_configurations`)和 **执行**(`execute_run_conf
|
|
|
108
124
|
`setup` 时(读回标记)就会注册。未标记的项目不受影响。文档放在 `skills/<id>/SKILL.md`(每个 skill 一个目录,`name` /
|
|
109
125
|
`description` 走 YAML frontmatter),由 `src/skill/loader.js` 扫描加载 —— **加新 skill 只需加一个目录,不用改代码**。
|
|
110
126
|
|
|
111
|
-
##
|
|
127
|
+
## 项目命令执行 (IDE 终端)
|
|
128
|
+
|
|
129
|
+
跑项目相关的命令时,模型使用 **`idea_execute_terminal_command`**,而不是 OpenCode 原生 shell。判据是**命令是否依赖项目的
|
|
130
|
+
工具链 / 运行时 / 依赖环境**,而不是语言或工具名:
|
|
112
131
|
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
- **走 IDE 终端**:任何会调用项目工具链的命令 —— Java / Python / Node / Go 的运行时,以及 Maven / Gradle、uv / pip /
|
|
133
|
+
poetry、cargo、bundler、make / cmake、包管理器、构建、测试、代码生成等。
|
|
134
|
+
- **走原生 shell**:纯操作系统级的通用命令(`ls` / `cat` / `cp` / `mkdir` / `git` 等)。
|
|
115
135
|
|
|
116
|
-
|
|
117
|
-
`PYTHONHOME` / `GOROOT` / `GOPATH` / `GOBIN` / `NODE_PATH` / `NVM_BIN` / `NVM_DIR` / `MAVEN_HOME` / `M2_HOME` /
|
|
118
|
-
`GRADLE_HOME` / `SDKMAN_DIR` / `CONDA_PREFIX`。
|
|
136
|
+
原因:
|
|
119
137
|
|
|
120
|
-
-
|
|
121
|
-
|
|
138
|
+
- **`executeInShell: true`**(必开):命令在用户真实 shell(`zsh`/`bash`)里执行,**继承 IDE 集成终端的环境** —— 也就是用户在
|
|
139
|
+
IDE 里配好的 JDK / Node / Python / Go / Maven / uv 等。原生 shell 未必能拿到同一套版本,尤其是 fnm / SDKMAN / pyenv 这类
|
|
140
|
+
"选择写在 shell init 里"的版本管理器,靠几个环境变量复现不出来。
|
|
141
|
+
- `timeout`(毫秒)按预期耗时给足;`maxLinesCount` 控制返回行数,超 2000 行会截断。
|
|
142
|
+
- `reuseExistingTerminalWindow: true` 复用同一个 IDE 终端窗口,不刷屏。
|
|
143
|
+
- 返回 `command_exit_code` + `command_output`;`cd` **不跨调用保留**,多步操作用 `&&` 串在一条命令里。
|
|
144
|
+
- 需要 IDE 里开启 **Brave Mode**,否则每条命令都要用户确认一次。
|
|
122
145
|
|
|
123
|
-
|
|
124
|
-
> `.opencode/env-loader/*` 的 **人工** KEY=VALUE 覆盖;本插件负责从 **IDE 实际环境**推导。可以同时使用。
|
|
146
|
+
引导文本由 `src/ide-guidance.js` 注入(`### 终端` 段),只在 IDE MCP 可用时生效。
|
|
125
147
|
|
|
126
148
|
## 前置条件
|
|
127
149
|
|
|
128
150
|
- JetBrains IDE 已开启 MCP 服务 (IDE 设置里的 MCP Server / AI Assistant MCP),版本需 **2026.2+**(提供 Streamable HTTP 端点
|
|
129
151
|
`/stream`);若关闭,`/open-in-idea` 会立即提示如何重新开启。
|
|
130
152
|
- 要读 IDE 终端环境,需要在 IDE 里开启 **Brave Mode**(否则 `execute_terminal_command` 会等确认);MCP 未就绪时通知会同时提醒开启它。
|
|
131
|
-
- 插件注册给 OpenCode 的 MCP
|
|
153
|
+
- 插件注册给 OpenCode 的 MCP 配置。默认用 **stdio 桥**:
|
|
132
154
|
|
|
133
155
|
```jsonc
|
|
134
156
|
{
|
|
135
|
-
"type": "
|
|
136
|
-
|
|
137
|
-
|
|
157
|
+
"type": "local",
|
|
158
|
+
// OpenCode 的 `LocalConfig.command` 是【字符串数组】:可执行文件 + 参数
|
|
159
|
+
// (源码 `client.ts` 里 `const [command, ...args] = config.command`)。没有单独的 `args` 字段。
|
|
160
|
+
"command": ["/Applications/IntelliJ IDEA.app/Contents/MacOS/idea", "stdioMcpServer"],
|
|
161
|
+
"environment": {
|
|
162
|
+
"IJ_MCP_SERVER_PROJECT_PATH": "<项目路径>",
|
|
163
|
+
"IJ_MCP_SERVER_PORT": "64342",
|
|
164
|
+
},
|
|
138
165
|
}
|
|
139
166
|
```
|
|
140
167
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
168
|
+
`idea stdioMcpServer` 是 IDE 自带的桥:OpenCode 通过 **stdin/stdout 子进程管道**跟它通信,它在后端连 IDE 的 MCP。
|
|
169
|
+
**客户端侧是持久管道,不存在 HTTP 空闲断流**——这正是默认选它的原因。每个项目一个插件实例、各带自己的
|
|
170
|
+
`IJ_MCP_SERVER_PROJECT_PATH`,所以每个项目会起一个独立的 stdio 客户端进程。
|
|
171
|
+
|
|
172
|
+
> **为什么不用远程 `/stream`**:Streamable HTTP 的客户端会挂一条用于 server→client 通知的 SSE 流;这条流**空闲一段时间会
|
|
173
|
+
> 被关**,而**远程传输在客户端侧不重连**(OpenCode 只为 legacy session 过期重连)。于是"闲置一会儿"就会掉。stdio 桥没有这个
|
|
174
|
+
> 问题。`transport: "http"` 可切回远程模式:
|
|
175
|
+
>
|
|
176
|
+
> ```jsonc
|
|
177
|
+
> {
|
|
178
|
+
> "type": "remote",
|
|
179
|
+
> "url": "http://127.0.0.1:64342/stream",
|
|
180
|
+
> "headers": { "IJ_MCP_SERVER_PROJECT_PATH": "<项目路径>" },
|
|
181
|
+
> }
|
|
182
|
+
> ```
|
|
183
|
+
>
|
|
184
|
+
> **为什么不用 `/sse`**:OpenCode V2 的远程 MCP 客户端**只支持 Streamable HTTP**,没有旧版 SSE 传输。IDE 的 `/sse` 是旧版
|
|
185
|
+
> SSE (POST 返回 405),V2 连不上。插件对 IDE 的**探测**仍走 `/stream`(一次 `initialize`),用来确认 IDE MCP 已就绪并拿到端口。
|
|
144
186
|
|
|
145
187
|
插件按 `ports` 顺序选择第一个可用端口,但会并发探测以避免多个端口的超时叠加;默认 `[64342]`。
|
|
146
188
|
|
|
@@ -202,12 +244,16 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
|
|
|
202
244
|
"package": "opencode-idea",
|
|
203
245
|
"options": {
|
|
204
246
|
"ports": [64342], // IDE MCP 端口;默认 [64342]
|
|
205
|
-
"injectEnv": true,
|
|
206
247
|
"injectGuidance": true, // IDE 可用时向系统提示注入「优先使用 IDEA MCP」引导
|
|
207
248
|
"openInIde": "idea", // false 关闭;或任意 macOS 应用名(如 "IntelliJ IDEA")
|
|
249
|
+
"transport": "stdio", // "stdio"(默认,本地桥,不会空闲断流)| "http"(远程 Streamable HTTP)
|
|
250
|
+
"ideExecutable": undefined, // 可显式指定 IDE 可执行文件;默认从 .app 里推断
|
|
208
251
|
"launchCooldownMs": 120000, // 启动保护冷却
|
|
209
252
|
"launchMaxAttempts": 5,
|
|
210
253
|
"mcpProbeTimeoutMs": 1000, // 单个 MCP 端口的短探测超时
|
|
254
|
+
"executionTimeoutMs": 600000, // 单次 idea_* 工具调用硬上限;默认 10 分钟(OpenCode 默认 12 小时)
|
|
255
|
+
"heartbeatIntervalMs": 5000, // 后台端口探活间隔
|
|
256
|
+
"heartbeatFailures": 2, // 连续失败几次后注销 idea MCP
|
|
211
257
|
"feedback": "message", // "message"(默认,会话通知,AI 只回"收到")| false(静默)
|
|
212
258
|
},
|
|
213
259
|
},
|
|
@@ -217,36 +263,45 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
|
|
|
217
263
|
|
|
218
264
|
不传 options 时全部使用下表默认值。
|
|
219
265
|
|
|
220
|
-
| 选项
|
|
221
|
-
|
|
|
222
|
-
| `ports`
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
266
|
+
| 选项 | 类型 | 默认 | 说明 |
|
|
267
|
+
| --------------------- | -------------------- | ----------- | --------------------------------------------------------------------------------------- |
|
|
268
|
+
| `ports` | `number[]` | `[64342]` | 依次探测的 IDE MCP 端口 |
|
|
269
|
+
| `injectGuidance` | `boolean` | `true` | IDE 可用时是否向系统提示注入「优先使用 IDEA MCP」引导 |
|
|
270
|
+
| `openInIde` | `boolean \| string` | `"idea"` | 拉起哪个 IDE;`false` 关闭,`"idea"` 映射到 IntelliJ IDEA |
|
|
271
|
+
| `transport` | `"stdio" \| "http"` | `"stdio"` | `stdio` 注册 IDE 自带的 stdio 桥(本地子进程,不会空闲断流);`http` 用远程 Streamable HTTP |
|
|
272
|
+
| `ideExecutable` | `string` | (推断) | IDE 可执行文件绝对路径;默认从 `<App>.app/Contents/MacOS/` 推断 |
|
|
273
|
+
| `launchCooldownMs` | `number` | `120000` | 启动保护冷却:冷却期内不重复 spawn |
|
|
274
|
+
| `launchMaxAttempts` | `number` | `5` | 单次会话内最多 spawn 次数,超出后停止重试 |
|
|
275
|
+
| `mcpProbeTimeoutMs` | `number` | `1000` | 单个 MCP 端口的短探测超时 |
|
|
276
|
+
| `executionTimeoutMs` | `number` | `600000` | 单次 `idea_*` 工具调用的硬上限(毫秒);防止桥挂起后无限等待(OpenCode 默认 12 小时) |
|
|
277
|
+
| `heartbeatIntervalMs` | `number` | `5000` | 后台 IDE 端口探活间隔(毫秒);仅"已标记 + 桥已注册"的项目有效 |
|
|
278
|
+
| `heartbeatFailures` | `number` | `2` | 连续探活失败几次后注销 `idea` MCP(杀僵尸桥 + 移除工具),下一条消息自动重建 |
|
|
279
|
+
| `feedback` | `"message" \| false` | `"message"` | 结果反馈:`message` 会话通知(用户可见,AI 只回"收到");`false` 静默 |
|
|
230
280
|
|
|
231
281
|
## 行为
|
|
232
282
|
|
|
233
|
-
- **被标记的项目**:启动读回标记后挂上 shell 环境钩子 (环境值待接入后填充)、注册 `idea-run-config` skill;
|
|
234
|
-
`/open-in-idea`
|
|
283
|
+
- **被标记的项目**:启动读回标记后挂上 shell 环境钩子 (环境值待接入后填充)、注册 `idea-run-config` skill;IDE MCP 由
|
|
284
|
+
`/open-in-idea` 注册。
|
|
235
285
|
- **未标记的项目**:不改环境、不注册、不注入任何 IDEA 提示 (每请求零开销;setup 时只做一次项目标记读取)。
|
|
236
|
-
-
|
|
237
|
-
`
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
(
|
|
245
|
-
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
-
|
|
249
|
-
|
|
286
|
+
- **手动接入 + 就绪即返回**:`/open-in-idea` 会 `open -a` 拉起 IDEA 并**等 IDE MCP 就绪**(冷启动数十秒,上限 60s);就绪后
|
|
287
|
+
**先注销旧的 `idea`(杀掉旧 stdio 桥)→ 再以 stdio 重新注册**,等 `idea_*` 工具就绪,最后才通知。**每次执行都重建**,所以断掉的
|
|
288
|
+
僵尸桥只要重跑一次命令就能恢复。命令返回即可用;`/close-in-idea` 注销 `idea` + 删标记 + 清环境。
|
|
289
|
+
- **`context` 钩子是请求的纯函数**:按「每次模型调用」触发 (含工具续跑),只看本次请求自带的工具快照 —— 有 `idea_*` 就剥离隐藏
|
|
290
|
+
工具并注入引导;没有则只在项目被标记时给恢复提示。该钩子本身**不做任何 I/O**(只看内存缓存),也不订阅任何事件。
|
|
291
|
+
- **后台心跳(仅"已标记 + 桥已注册"的项目)**:每 `heartbeatIntervalMs`(默认 5s)探一次 IDE 端口;连续 `heartbeatFailures`
|
|
292
|
+
(默认 2)次不通就**注销 `idea` MCP** —— 杀掉僵尸桥子进程 + 移除 `idea_*` 工具,于是模型**调不到、也就不会挂起** —— 并锁存
|
|
293
|
+
`needRebuild`。下一条用户消息在 `prompt` 钩子里读到锁存 → 自动重跑接入流程(IDE 不在则 `open -a` 拉起),成功后清锁存。**全程静默**。
|
|
294
|
+
探活是**每个 Location(项目)各一份**;IDE 只有一个,所以 IDE 一挂、所有项目的桥几乎同时被注销,再由各自的消息触发重建。
|
|
295
|
+
- **失败触发的自动接入(仅被标记项目)**:`tool.execute.after` 钩子监听 `idea_*` 工具报错并**分类**:
|
|
296
|
+
- IDE MCP 不可用(掉了)→ 重跑 `/open-in-idea` 的接入流程;
|
|
297
|
+
- IDE 在、但项目没打开(`Unable to determine the target project` / `doesn't correspond to any open project`)→ 让 IDEA 重新打开该项目;
|
|
298
|
+
- 普通业务错误(`File not found` 等)→ **不动作**。判定只认**传输层专有标记**(`MCP server "idea" is not available` /
|
|
299
|
+
`MCP server is not connected` / `Error POSTing to endpoint` / `SseClientTransport is closed`),**不认**工具输出里出现的通用串
|
|
300
|
+
(例如 SQL 工具报的 `Connection refused`) —— MCP 工具错误的 message 就是工具自己的输出,认通用串会把正常业务失败误判成掉线并重建。
|
|
301
|
+
触发成功会把该工具错误改写为"已重连 / 已重新打开,请重试",模型在同一回合内重试;失败则保持原错误。非 `idea_*` 工具、未标记
|
|
302
|
+
项目都不受影响。启动保护 (`launchCooldownMs` / `launchMaxAttempts`) 限制重复拉起 IDE。
|
|
303
|
+
- **`/open-in-idea`**:手动打开当前项目;**等 IDE MCP 就绪 → 先注销旧的 `idea`(杀旧桥)→ 再重新注册 → 等 `idea_*` 工具就绪**才返回。
|
|
304
|
+
**`/close-in-idea`**:注销 IDE MCP + 删项目标记 + 清空注入的环境。
|
|
250
305
|
|
|
251
306
|
## 限制
|
|
252
307
|
|
|
@@ -256,21 +311,29 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
|
|
|
256
311
|
- skill 定义的字段名**以 schema 为准**,不要照官方文档:`Skill.Info` 需要 `path` (AbsolutePath),文档里写的 `location`
|
|
257
312
|
不存在 —— 用错会让 `ctx.skill.transform` 抛 `SchemaError`,而**任何 transform 失败都会禁用整个插件**(0.0.6 就是这样丢了
|
|
258
313
|
`/open-in-idea`)。同理,`Skill.Info` 的完整字段是 `id` / `name` / `description?` / `autoinvoke?` / `path` / `content`。
|
|
259
|
-
-
|
|
314
|
+
- 项目命令走 `idea_execute_terminal_command` + **Brave Mode**;没开 Brave Mode 时每条命令都要用户确认一次。
|
|
260
315
|
- 一批用不到的 IDEA MCP 工具被硬编码隐藏 (`src/plugin.js` 的 `HIDDEN_IDEA_TOOLS`,共 31 个):VCS (`idea_git_status` /
|
|
261
316
|
`idea_get_repositories`,版本控制走原生 `git`)、Router 派发工具 (`idea_execute_tool`,未启用 router-only 时冗余)、Debugger
|
|
262
317
|
(全部 `idea_xdebug_*`)、Dev Kit MCP、Inspection KTS MCP、解释器环境 MCP、以及数据库的建/改数据源 (数据源由用户在
|
|
263
|
-
IDEA 里配置,AI 只读和查询)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
"
|
|
268
|
-
|
|
269
|
-
|
|
318
|
+
IDEA 里配置,AI 只读和查询)。**`idea_execute_terminal_command` 不隐藏**:项目命令要在 IDE 环境里跑,模型需要它。
|
|
319
|
+
- 探测只确认「MCP 服务是否在监听」;**「当前项目是否已在 IDE 打开」靠一次项目作用域的 MCP 调用判断**
|
|
320
|
+
(`get_project_modules`,带 `IJ_MCP_SERVER_PROJECT_PATH`):返回模块 = 已打开 → 跳过 `open -a`;报
|
|
321
|
+
`Unable to determine the target project` = 未打开 → `open -a` 打开。MCP 关闭时会用一次短探测并立即给出设置提示。
|
|
322
|
+
项目刚打开时的短暂"项目未就绪"由工具失败分类兜底(「IDE 在、但项目没打开」→ 重新打开并让模型重试)。
|
|
323
|
+
- IDE **2026.2+** 提供官方 stdio 桥 (`idea stdioMcpServer`),插件默认用它注册。stdio 是子进程管道,不像 Streamable HTTP 那样
|
|
324
|
+
会空闲断流;但它**启动时要求 IDE MCP 已在监听**,所以 `/open-in-idea` 必须先等就绪再注册(冷启动约数十秒)。`transport: "http"`
|
|
325
|
+
可切回远程 Streamable HTTP(OpenCode 会因连接关闭把 server 置 failed 并移除工具,不自动重连)。
|
|
326
|
+
- **stdio 的边界(已实测)**:IDE 中途退出后,桥进程会变成"僵尸"——进程还在、调用挂起、**不回任何错误**。两种情况都实测过:**调用之后**IDE 挂、
|
|
327
|
+
以及**调用进行中**IDE 挂(在 IDE 里跑 `sleep 120` 时 `kill` 掉 IDEA),桥都只在 stderr 打一行错误(`SseClientTransport is closed!` /
|
|
328
|
+
`Request timeout has expired`),**stdout 一个字节都不回**。而且**重开 IDEA 也不会自愈**(实测:重开 IDEA 后连续探测 5 分钟仍全部 TIMEOUT);
|
|
329
|
+
OpenCode 依旧显示 connected,`refresh`/`reload` 只会 `reconcile`,配置没变就跳过,不会重启它。所以:
|
|
330
|
+
- **后台心跳(见上)自动兜底**:5s 探活、连续 2 次不通就注销 `idea`(杀僵尸桥 + 移除工具),下一条消息自动重建 —— 常态下**不需要用户手动做什么**;
|
|
331
|
+
- 手动重建仍可用:重开 IDEA 后再执行一次 `/open-in-idea`(先注销旧 `idea`、再重新注册);
|
|
332
|
+
- 插件给 `idea` server 配 `executionTimeoutMs`(默认 **10 分钟**)作为硬上限:兜住"心跳尚未触发、工具已在途"的那次挂起,不再沿用 OpenCode 默认的 12 小时;
|
|
333
|
+
- "IDE 被关掉 → 工具挂起"这类**不会**走工具失败分类(没有 error);能自动分类处理的是"IDE 在、项目没打开"和"server 掉了(启动即失败)"。
|
|
270
334
|
- 项目标记存在插件名下 KV (`plugin:opencode-idea:project/<项目根路径>`)。插件实例按 **Location**(项目/打开目录)创建,标记按
|
|
271
|
-
**项目根路径**共享;但两个不同 Location(例如项目根与某子目录)各有自己的内存缓存 ——
|
|
272
|
-
|
|
273
|
-
- `/close-in-idea` **只删标记,不断开已建立的连接**:已注册的 IDEA 工具会保留到连接自然中断,期间标记与实际连接可能短暂不一致。
|
|
335
|
+
**项目根路径**共享;但两个不同 Location(例如项目根与某子目录)各有自己的内存缓存 —— 单目录打开项目的常见用法不受影响。
|
|
336
|
+
- `/close-in-idea` 会**注销 IDE MCP** 并删标记 + 清空注入的环境。
|
|
274
337
|
- 插件**完全不再检查 `.idea` 目录**:是否是 IDEA 项目只由项目标记(`/open-in-idea` 设置、`/close-in-idea` 清除)决定。
|
|
275
338
|
(`/open-in-idea` 会让 IDEA 打开并导入该项目,`.idea` 由 IDEA 自己生成,插件无需也不应据此判断。)
|
|
276
339
|
- 打开 IDE 仅实现 macOS (`open -a`);首次打开若弹 Trust 对话框,需确认后重试。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-idea",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
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",
|
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
"src/index.js",
|
|
10
10
|
"src/plugin.js",
|
|
11
11
|
"src/mcp/idea.js",
|
|
12
|
-
"src/mcp/ide-env.js",
|
|
13
12
|
"src/project.js",
|
|
14
|
-
"src/env.js",
|
|
15
13
|
"src/ide-launcher.js",
|
|
16
14
|
"src/ide-guidance.js",
|
|
17
15
|
"src/skill/loader.js",
|
package/src/ide-guidance.js
CHANGED
|
@@ -60,6 +60,17 @@ export function buildIdeGuidance(projectPath) {
|
|
|
60
60
|
`- \`idea_get_run_configurations\`:读项目级 \`.run/*.run.xml\`(增删改直接改文件);传 \`filePath\` 可列该文件的运行入口。`,
|
|
61
61
|
`- \`idea_execute_run_configuration\`:跑已有配置;Shell Script 要 \`EXECUTE_IN_TERMINAL: false\`;覆盖参数(\`programArguments\` / \`workingDirectory\` / \`envs\`)需配置支持 \`supportsDynamicLaunchOverrides\`。`,
|
|
62
62
|
``,
|
|
63
|
+
`### 终端`,
|
|
64
|
+
`- \`idea_execute_terminal_command\`:**项目内的一切命令执行都用它**,不要用原生 \`shell\`。`,
|
|
65
|
+
` 判据是"命令是否依赖项目的工具链 / 运行时 / 依赖环境",不是语言或工具名 —— Java、Python、Node、Go 之外,`,
|
|
66
|
+
` Maven / Gradle、uv / pip / poetry、cargo、bundler、make / cmake、包管理器与构建、测试、代码生成等,全都算。`,
|
|
67
|
+
` 例外:纯操作系统级的通用命令(\`ls\` / \`cat\` / \`cp\` / \`mkdir\` / \`git\` 等)用原生 \`shell\` 即可。`,
|
|
68
|
+
` 记不清就不确定时:只要命令会调用项目的工具链,就用它,不要退回原生 \`shell\`。`,
|
|
69
|
+
` - 必须 \`executeInShell: true\`:在用户真实 shell 里执行,**天然继承 IDE 里配好的运行时环境**(JDK / Node / Python / Go / Maven / uv 等及各版本管理器);原生 shell 可能用错版本。`,
|
|
70
|
+
` - 按预期耗时传 \`timeout\`(毫秒),长任务给足;必要时传 \`maxLinesCount\` 控制返回行数。`,
|
|
71
|
+
` - 连续命令传 \`reuseExistingTerminalWindow: true\`,复用同一个 IDE 终端窗口。`,
|
|
72
|
+
` - 结果带 \`command_exit_code\` 与 \`command_output\`;\`cd\` 不跨调用保留,多步时在一条命令里用 \`&&\`。`,
|
|
73
|
+
``,
|
|
63
74
|
`### 数据库 (数据源由用户在 IDE 里配置:不要自己建/改,不要向用户索取数据库凭据)`,
|
|
64
75
|
`- \`idea_list_database_connections\`:先看有没有现成数据源;每条给 \`id\` / \`dbms\` / \`jdbcUrl\` / \`readOnly\` / \`isDDL\`。`,
|
|
65
76
|
`- \`idea_test_database_connection\`(\`id\`):\`hasProblems\` 为 \`NO\` 才算通,另带 DBMS / 驱动版本与 ping。`,
|
|
@@ -82,6 +93,16 @@ export const MCP_RECOVERY_GUIDANCE = [
|
|
|
82
93
|
'If an idea.* / idea_* tool is missing, MCP is disconnected, the connection is refused, or a tool times out: stop retrying immediately; do not run extra probes or wait in tool calls. Tell the user that IDEA MCP is unavailable and ask them to run /open-in-idea to reconnect.',
|
|
83
94
|
].join('\n');
|
|
84
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Recovery instruction for a marked project, where the plugin reconnects the
|
|
98
|
+
* IDE MCP on its own. The model must not fall back to "ask the user" when a
|
|
99
|
+
* single connection error is all it saw — the reconnect is already happening.
|
|
100
|
+
*/
|
|
101
|
+
export const MCP_RECOVERY_GUIDANCE_MARKED = [
|
|
102
|
+
'### IDEA MCP failure recovery',
|
|
103
|
+
'This project is IDEA-connected and the plugin reconnects the IDE MCP automatically. When an idea_* call fails with a connection error (for example `MCP server "idea" is not available`), do not run extra probes or wait in tool calls: the reconnect is already under way, so retry the operation once. Only if it still fails, tell the user that IDEA MCP is unavailable and to run /open-in-idea.',
|
|
104
|
+
].join('\n');
|
|
105
|
+
|
|
85
106
|
/**
|
|
86
107
|
* Push the guidance onto a session `system` array. Called for every primary
|
|
87
108
|
* model request, so it stays idempotent per request.
|
|
@@ -97,9 +118,13 @@ export function appendIdeGuidance(system, projectPath) {
|
|
|
97
118
|
system.push({ type: 'text', text });
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
121
|
+
/**
|
|
122
|
+
* @param {unknown} system
|
|
123
|
+
* @param {{ marked?: boolean }} [options] `marked` picks the auto-reconnect variant
|
|
124
|
+
*/
|
|
125
|
+
export function appendIdeRecoveryGuidance(system, { marked = false } = {}) {
|
|
102
126
|
if (!Array.isArray(system)) return;
|
|
103
|
-
|
|
104
|
-
system.
|
|
127
|
+
const text = marked ? MCP_RECOVERY_GUIDANCE_MARKED : MCP_RECOVERY_GUIDANCE;
|
|
128
|
+
if (system.some((part) => part?.text === text)) return;
|
|
129
|
+
system.push({ type: 'text', text });
|
|
105
130
|
}
|
package/src/ide-launcher.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
// unavailable; this module only requests opening/activating the project.
|
|
13
13
|
|
|
14
14
|
import { spawn } from 'node:child_process';
|
|
15
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
15
17
|
|
|
16
18
|
/** Default macOS app name for IntelliJ IDEA. */
|
|
17
19
|
export const DEFAULT_IDE_APP = 'IntelliJ IDEA';
|
|
@@ -35,6 +37,62 @@ export function resolveIdeApp(value) {
|
|
|
35
37
|
return IDE_APPS[value.toLowerCase()] ?? value;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/** Preferred launcher binary name per app, used when `Contents/MacOS` has several. */
|
|
41
|
+
const IDE_EXECUTABLES = {
|
|
42
|
+
'IntelliJ IDEA': 'idea',
|
|
43
|
+
PyCharm: 'pycharm',
|
|
44
|
+
'PyCharm Community': 'pycharm',
|
|
45
|
+
WebStorm: 'webstorm',
|
|
46
|
+
GoLand: 'goland',
|
|
47
|
+
CLion: 'clion',
|
|
48
|
+
PhpStorm: 'phpstorm',
|
|
49
|
+
RubyMine: 'rubymine',
|
|
50
|
+
Rider: 'rider',
|
|
51
|
+
DataGrip: 'datagrip',
|
|
52
|
+
'Android Studio': 'studio',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Absolute path to the IDE launcher binary (`<App>.app/Contents/MacOS/<bin>`).
|
|
57
|
+
* The stdio bridge is started by running this binary with `stdioMcpServer`.
|
|
58
|
+
*
|
|
59
|
+
* Returns undefined when the app bundle cannot be found — the caller then falls
|
|
60
|
+
* back to the remote transport.
|
|
61
|
+
*
|
|
62
|
+
* @param {string | undefined} app macOS app name, e.g. "IntelliJ IDEA"
|
|
63
|
+
* @param {{ dirs?: string[] }} [options] search roots (defaults to the two
|
|
64
|
+
* standard Applications folders); tests pass their own.
|
|
65
|
+
* @returns {string | undefined}
|
|
66
|
+
*/
|
|
67
|
+
export function resolveIdeExecutable(app, options = {}) {
|
|
68
|
+
if (typeof app !== 'string' || app.length === 0) return undefined;
|
|
69
|
+
const home = process.env.HOME;
|
|
70
|
+
const dirs = options.dirs ?? [home ? path.join(home, 'Applications') : undefined, '/Applications'].filter(
|
|
71
|
+
(value) => typeof value === 'string',
|
|
72
|
+
);
|
|
73
|
+
const preferred = IDE_EXECUTABLES[app];
|
|
74
|
+
for (const dir of dirs) {
|
|
75
|
+
const macos = path.join(dir, `${app}.app`, 'Contents', 'MacOS');
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync(macos);
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const executable = (name) => {
|
|
83
|
+
try {
|
|
84
|
+
const stats = statSync(path.join(macos, name));
|
|
85
|
+
return stats.isFile() && (stats.mode & 0o111) !== 0;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const chosen = (preferred && entries.includes(preferred) && preferred) || entries.find(executable);
|
|
91
|
+
if (chosen) return path.join(macos, chosen);
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
38
96
|
/**
|
|
39
97
|
* Open `directory` in the IDE, reusing a running instance when possible.
|
|
40
98
|
* Never throws; resolves `true` when `open` reported success.
|