opencode-idea 0.0.15 → 0.0.16

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
@@ -64,6 +64,9 @@ setup(启动, 每项目一次):
64
64
 
65
65
  在会话里输入 `/open-in-idea`:
66
66
 
67
+ > **非 macOS**:第 1、3 步的自动打开被跳过(没有对应的启动命令),请**先手动打开 IDE 和当前项目**,再执行本命令 ——
68
+ > 端口在监听就会正常接入,并同样等到 `idea_*` 工具就绪才返回。
69
+
67
70
  1. **让运行中的 IDEA 实例打开/激活当前项目**:`open -a "IntelliJ IDEA" <项目目录>` —— **复用已运行的实例**(不会重复开新实例),
68
71
  项目已打开时只把它切到前台。**无论 MCP 是否已就绪都会执行这一步**:端点能应答只说明 IDEA 在运行(可能开的是**别的**项目),
69
72
  不代表当前项目已打开;
@@ -325,6 +328,10 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
325
328
  (`get_project_modules`,带 `IJ_MCP_SERVER_PROJECT_PATH`):返回模块 = 已打开 → 跳过 `open -a`;报
326
329
  `Unable to determine the target project` = 未打开 → `open -a` 打开。MCP 关闭时会用一次短探测并立即给出设置提示。
327
330
  项目刚打开时的短暂"项目未就绪"由工具失败分类兜底(「IDE 在、但项目没打开」→ 重新打开并让模型重试)。
331
+ - **项目路径按 IDE 的写法归一化**:IDE 收的是系统无关形式(正斜杠),其自带配置示例在 Windows 上也是
332
+ `"E:/gitea/secondev-demo"`。OpenCode 的 `location` 在 Windows 上是原生反斜杠,所以 `src/mcp/idea.js` 的
333
+ `ideProjectPath()` 把它转成正斜杠,**注册与探测走同一个函数**,两者必须一致,否则会用一种写法注册、又用另一种
334
+ 写法去问,永远匹配不上。macOS 上是原样透传。
328
335
  - IDE **2026.2+** 提供 Streamable HTTP 端点 `/stream`,这是插件**唯一**使用的传输(`type: "remote"`)。注册本身不启动任何子进程,
329
336
  所以没有"桥进程被 IDE 拖死后变成僵尸"这一类问题。`/open-in-idea` 仍要求 IDE MCP 已在监听(冷启动约数十秒),因此会先等就绪再注册。
330
337
  - **空闲驱逐(已实测)**:OpenCode 把一个 Location 的服务缓存在 `LayerMap` 里,`idleTimeToLive` = **60 分钟**;空闲到期即整体驱逐,
@@ -339,7 +346,9 @@ export { default } from "/Users/yutao/Projects/opencode-idea/src/index.js";
339
346
  - `/close-in-idea` 会**注销 IDE MCP** 并删标记 + 清空注入的环境。
340
347
  - 插件**完全不再检查 `.idea` 目录**:是否是 IDEA 项目只由项目标记(`/open-in-idea` 设置、`/close-in-idea` 清除)决定。
341
348
  (`/open-in-idea` 会让 IDEA 打开并导入该项目,`.idea` 由 IDEA 自己生成,插件无需也不应据此判断。)
342
- - 打开 IDE 仅实现 macOS (`open -a`);首次打开若弹 Trust 对话框,需确认后重试。
349
+ - **自动拉起 IDE 仅实现 macOS** (`open -a`)。其他平台(包括 Windows)没有对应的启动命令,**请手动打开 IDE 和项目**,再执行
350
+ `/open-in-idea` —— 端口在监听就会正常接入,其余能力(注册、探测、自愈、工具暴露)与 macOS 完全一致。
351
+ IDE 里打开 MCP 服务(2026.2+)并启用 Brave Mode;首次打开若弹 Trust 对话框,需确认后重试。
343
352
  - IDE MCP 没有「打开项目」工具,因此打开动作走 OS/CLI,而非 MCP。
344
353
  - 插件热重载会断开 IDE 连接,需重跑 `/open-in-idea`。
345
354
  - 插件不修改任何工具的定义、描述或可用性;IDE MCP 工具以 `idea_*` 直接暴露给模型 (`codemode: false`),不用 `execute` 包裹。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-idea",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
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",
@@ -16,6 +16,23 @@ import { spawn } from 'node:child_process';
16
16
  /** Default macOS app name for IntelliJ IDEA. */
17
17
  export const DEFAULT_IDE_APP = 'IntelliJ IDEA';
18
18
 
19
+ /**
20
+ * Can this platform launch the IDE at all?
21
+ *
22
+ * Launching goes through macOS `open -a`, which is the only launcher this
23
+ * plugin implements. On Windows there is no `open` command, so a spawn there
24
+ * fails with ENOENT — silently, as a rejected promise that resolves `false`.
25
+ * Rather than pretend, other platforms resolve no app at all and the caller
26
+ * asks the user to open the IDE by hand.
27
+ *
28
+ * A function, not a constant: the answer is read when a command runs, so tests
29
+ * can simulate another platform by replacing it. A constant would be bound at
30
+ * import time and never re-read.
31
+ */
32
+ export function autoLaunchSupported() {
33
+ return process.platform === 'darwin';
34
+ }
35
+
19
36
  /** Aliases accepted by the `openInIde` option. */
20
37
  export const IDE_APPS = {
21
38
  idea: 'IntelliJ IDEA',
@@ -26,10 +43,16 @@ export const IDE_APPS = {
26
43
  /**
27
44
  * Resolve the `openInIde` option to a macOS app name, or `undefined` to disable.
28
45
  *
46
+ * `undefined` covers two distinct reasons, and the caller tells them apart via
47
+ * `autoLaunchSupported()`: the user turned launching off, or this platform has
48
+ * no launcher. They deserve different user-facing wording.
49
+ *
29
50
  * @param {boolean | string | undefined} value
51
+ * @param {{ platform?: string }} [options]
30
52
  * @returns {string | undefined}
31
53
  */
32
- export function resolveIdeApp(value) {
54
+ export function resolveIdeApp(value, { platform = process.platform } = {}) {
55
+ if (platform !== 'darwin') return undefined;
33
56
  if (value === false) return undefined;
34
57
  if (typeof value !== 'string') return DEFAULT_IDE_APP;
35
58
  return IDE_APPS[value.toLowerCase()] ?? value;
package/src/mcp/idea.js CHANGED
@@ -18,6 +18,25 @@
18
18
 
19
19
  export const PROJECT_HEADER = 'IJ_MCP_SERVER_PROJECT_PATH';
20
20
 
21
+ /**
22
+ * The project path in the form the IDE matches against.
23
+ *
24
+ * The IDE takes the project selector in system-independent form (forward
25
+ * slashes) — its own MCP client configuration ships
26
+ * `IJ_MCP_SERVER_PROJECT_PATH: "E:/gitea/secondev-demo"`, even on Windows.
27
+ * OpenCode's `location` is the platform-native path, so on Windows it carries
28
+ * backslashes (`E:\gitea\secondev-demo`) that the IDE does not necessarily
29
+ * match. Normalizing here keeps one spelling on the wire for BOTH senders
30
+ * below; they must agree, or the plugin asks about the project with one path
31
+ * and registers it with another and never matches. On macOS this is a no-op.
32
+ *
33
+ * @param {string} projectPath
34
+ * @returns {string}
35
+ */
36
+ export function ideProjectPath(projectPath) {
37
+ return projectPath.replace(/\\/g, '/');
38
+ }
39
+
21
40
  /** OpenCode MCP server name registered for the IDE. */
22
41
  export const IDEA_SERVER_NAME = 'idea';
23
42
 
@@ -51,7 +70,7 @@ export function serverConfig(port, projectPath, options = {}) {
51
70
  const config = {
52
71
  type: 'remote',
53
72
  url: `http://127.0.0.1:${port}${MCP_STREAM_PATH}`,
54
- headers: { [PROJECT_HEADER]: projectPath },
73
+ headers: { [PROJECT_HEADER]: ideProjectPath(projectPath) },
55
74
  codemode: false,
56
75
  };
57
76
  return withExecutionTimeout(config, options);
@@ -103,7 +122,7 @@ export async function probePort(port, options = {}) {
103
122
  params: {
104
123
  protocolVersion: PROTOCOL_VERSION,
105
124
  capabilities: {},
106
- clientInfo: { name: 'opencode-idea', version: '0.0.15' },
125
+ clientInfo: { name: 'opencode-idea', version: '0.0.16' },
107
126
  },
108
127
  }),
109
128
  signal: controller.signal,
@@ -160,7 +179,7 @@ export async function callTool(port, projectPath, name, args = {}, options = {})
160
179
  headers: {
161
180
  'Content-Type': 'application/json',
162
181
  Accept: 'application/json, text/event-stream',
163
- [PROJECT_HEADER]: projectPath,
182
+ [PROJECT_HEADER]: ideProjectPath(projectPath),
164
183
  ...(sessionId ? { 'Mcp-Session-Id': sessionId } : {}),
165
184
  },
166
185
  body: JSON.stringify(payload),
@@ -174,7 +193,7 @@ export async function callTool(port, projectPath, name, args = {}, options = {})
174
193
  params: {
175
194
  protocolVersion: PROTOCOL_VERSION,
176
195
  capabilities: {},
177
- clientInfo: { name: 'opencode-idea', version: '0.0.15' },
196
+ clientInfo: { name: 'opencode-idea', version: '0.0.16' },
178
197
  },
179
198
  });
180
199
  // The IDE's Streamable-HTTP endpoint binds follow-up requests to the session
package/src/plugin.js CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  } from './mcp/idea.js';
54
54
  import { currentProjectPath } from './project.js';
55
55
  import {
56
+ autoLaunchSupported,
56
57
  createLaunchGuard,
57
58
  openInIde,
58
59
  resolveIdeApp,
@@ -296,7 +297,17 @@ function rewriteToolError(event, message) {
296
297
 
297
298
  /**
298
299
  * @typedef {{
299
- * status: 'connected' | 'tools-loading' | 'mcp-unavailable' | 'launch-failed' | 'disabled' | 'closed',
300
+ * status:
301
+ * | 'connected'
302
+ * | 'tools-loading'
303
+ * | 'mcp-unavailable'
304
+ * | 'launch-failed'
305
+ * | 'disabled'
306
+ * | 'manual-launch'
307
+ * | 'closed',
308
+ * // `manual-launch` = this platform has no launcher at all (the user must
309
+ * // open the IDE by hand); `disabled` = the user turned launching off. Both
310
+ * // mean "no app to open", so only `autoLaunchSupported()` tells them apart.
300
311
  * port?: number,
301
312
  * opened?: boolean,
302
313
  * unavailableNoticeSent?: boolean
@@ -332,6 +343,8 @@ export function ideFeedback(result) {
332
343
  return '无法拉起 IntelliJ IDEA(未找到应用或启动失败)。请确认已安装 IDEA,或手动打开后重试 /open-in-idea。';
333
344
  case 'disabled':
334
345
  return '插件已通过 openInIde: false 关闭 IDE 启动。请手动打开 IntelliJ IDEA,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后重试 /open-in-idea。';
346
+ case 'manual-launch':
347
+ return '当前系统不支持自动打开 IDE(仅 macOS 实现了启动)。请手动打开 IntelliJ IDEA,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后再次执行 /open-in-idea。';
335
348
  case 'closed':
336
349
  return '已关闭当前项目的 IDEA 接入:已注销 IDE MCP 并清除项目标记。如需重新接入,请再次执行 /open-in-idea。';
337
350
  default:
@@ -692,7 +705,11 @@ export default {
692
705
  }
693
706
  return { status: 'connected', port: serving, unavailableNoticeSent: false };
694
707
  }
695
- if (!ideApp) return { status: 'disabled' };
708
+ // No app to open. Either the user turned launching off, or this platform
709
+ // has no launcher — say which, so the notice is actionable.
710
+ if (!ideApp) {
711
+ return { status: autoLaunchSupported() ? 'disabled' : 'manual-launch' };
712
+ }
696
713
  if (guard.exhausted()) {
697
714
  await reportUnavailable(false);
698
715
  return { status: 'mcp-unavailable', opened: false, unavailableNoticeSent: launchNoticeSent };
@@ -983,9 +1000,14 @@ export default {
983
1000
  if (!(await readMark())) return;
984
1001
  const kind = classifyIdeaFailure(event.error);
985
1002
  if (kind === 'none') return;
986
- if (kind === 'project' && ideApp) {
987
- // The IDE is up; the project simply is not open in it. Ask the IDE
988
- // to open the project again, and tell the model to retry.
1003
+ if (kind === 'project') {
1004
+ // The IDE is up and answering; only the PROJECT is missing. That is
1005
+ // never a connection problem, so this must never fall through to
1006
+ // the reconnect below — on a platform with no launcher there is
1007
+ // nothing to call, and falling through would report "reconnected"
1008
+ // and send the model round after round after a call that can only
1009
+ // fail. Leave the IDE's own message, which names the real cause.
1010
+ if (!ideApp) return;
989
1011
  await openInIde(projectPath, { app: ideApp });
990
1012
  rewriteToolError(event, '该项目已在 IDEA 中重新打开,请重试刚才的操作。');
991
1013
  return;