opencode-idea 0.0.18 → 0.0.19

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.
@@ -23,15 +23,18 @@
23
23
  - 下面这些 IDE 工具也是原生工具:直接调用(参数按各工具 schema 传),不要包进 `execute`;它们不在 Code Mode 的工具目录里,`search` 搜不到。
24
24
  - **只有 IDE 才有的能力才用它们**:符号级检索、项目模型、检查与构建、重构与格式化、run configuration。
25
25
  - **读写与搜索用原生工具**(`read` / `write` / `edit` / `grep` / `glob`):不要为读一个文件、写一个文件、搜一段文本去绕 IDE 工具。
26
- - **唯一例外**:压缩包内的条目(路径带 `.jar!/`,原生 `read` 打不开)—— 见下面「读压缩包内的东西」。
27
- - 某条功能下面没有你正要操作的那个项目的命令,就是那个项目没接上:提示用户执行 `/open-in-idea`,不要重试,也不要去别处找。
26
+ - **改完代码自己先查一遍**(`lint_files` / `build_project`,怎么挑见下面「校验」):什么时候查、查几次由你判断,但**别把校验整段省掉** —— 自己查一次,远比让用户替你发现或留到构建 / 测试炸了才暴露要便宜。
27
+ - **要看某个类 / 方法 / 字段在哪定义、内部怎么实现的**(见下面「符号」):`<P>_search_symbol`(`include_external: true`)找到位置 → `<P>_read_file` 原样读它给的路径。
28
+ - 某条功能下面没有你正要操作的那个项目的命令,就是那个项目没接上:提示用户执行 `/open-in-ide`,不要重试,也不要去别处找。
28
29
 
29
30
  ## 工具(每条第一行是各项目的命令,接着是它的用法)
30
31
 
31
32
  ### 符号(只有 IDE 的语义索引能给)
32
33
 
34
+ **要读任何类 / 方法 / 字段的源码,就两步**:`<P>_search_symbol`(`q` 传名字,**带 `include_external: true`**)找到位置 → `<P>_read_file` 原样读它给的路径。**依赖库 / 三方包 / 平台 jar 里的实现,正是它最该用的场合**,也是 `javap` / `unzip` / 解包 jar / 按 jar 路径 `grep` 唯一的正解:**IDE 索引里那份就是项目编译时真正用的依赖**(自己拼的 jar 路径常常是另一份副本,版本对不上),读出来还是反编译源码 —— 字节码清单要来回十几次才能拼出逻辑,这边一次到位。
35
+
33
36
  - {{cmd_search_symbol}}
34
- **按名字找符号的声明位置**(`q` 传查询文本,**固定带上 `include_external: true`**):本插件最不可替代的能力 —— 它搜的是**符号**(类 / 接口 / 方法 / 函数 / 字段 / 类型 / 常量这类**有名字的定义**)的**声明位置**,不是"哪段文本提到过它"(那是 `grep` 的事);带上 `include_external` 才会连**依赖库和标准库**一起搜,于是你能直接定位第三方库里的实现 —— 这是原生 `grep` / `glob` 够不着的地方(IDE 自己的 `search_text` / `search_file` 也只搜项目内)。
37
+ **`q` 传查询文本,固定带上 `include_external: true`。** 本插件最不可替代的能力 —— 它搜的是**符号**(类 / 接口 / 方法 / 函数 / 字段 / 类型 / 常量这类**有名字的定义**)的**声明位置**,不是"哪段文本提到过它"(那是 `grep` 的事);带上 `include_external` 才会连**依赖库和标准库**一起搜,于是你能直接定位第三方库里的实现 —— 这是原生 `grep` / `glob` 够不着的地方(IDE 自己的 `search_text` / `search_file` 也只搜项目内)。
35
38
  - **典型用法 ①:代码里引用了某个库的 API(最常见)** —— 读代码时看到 `from fastapi import FastAPI`、`import org.apache.poi...FormulaParser`、`gorm.DeletedAt`、`from 'vue'` 这类引用时,**包名就在代码里**,直接**用「包名+符号名」当 `q` 搜**,一次定位到它的实现:
36
39
  - **Java**:全限定名 —— `com.nimbusds.jose.util.Base64`(实测 1 条命中 `~/.m2/.../poi-5.3.0-sources.jar!/...`)
37
40
  - **Go**:包名.符号 —— `gorm.DeletedAt`(实测 2 条,全在 `go/pkg/mod/gorm.io/gorm@...`)
@@ -82,6 +85,19 @@
82
85
  - {{cmd_build_project}}
83
86
  **构建校验**:`filesToRebuild` 可只构建指定文件;有问题返回在 `problems`,没返回即没问题。
84
87
 
88
+ **改完代码,自己先查一遍再收尾。** 这不是流程要求,是你自己的判断 —— 查一次很便宜,而问题一旦漏到用户
89
+ 看见、或留到构建 / 测试炸了才暴露,回头纠正的代价就高一个量级了。
90
+
91
+ - **这次要改一大片**(多文件、跨模块、"顺手把这几处一起对齐")→ 全部改完再 `build_project` 一次。中途不查:
92
+ 半成品状态下的检查结果只会误导你。
93
+ - **这次只改一处,或在同一个文件上反复微调** → 告一段落时,把这轮改过的文件一次 `lint_files` 掉;要看单个
94
+ 文件的细节再上 `get_file_problems`。
95
+ - **改了签名 / 接口 / import / 依赖**这类"别处会跟着坏"的地方 → 走 `build_project`;只动了文件内部的实现 →
96
+ lint 就够。
97
+ - 查出来的问题**改掉,或说清为什么不改**,别把没处理的问题原样交给用户。
98
+ - 校验走上面这些工具,**不要改用 `shell` 里临时自己编一遍代替**。
99
+ - 也别先猜"索引还没同步 / 那个环境是另一个进程"就绕开 —— 先调一次,它真看不到会明说。
100
+
85
101
  ### 读压缩包内的东西
86
102
 
87
103
  - {{cmd_read_file}}
@@ -95,7 +111,11 @@
95
111
  **跑已有配置**:Shell Script 要 `EXECUTE_IN_TERMINAL: false`;覆盖参数(`programArguments` / `workingDirectory` / `envs`)需配置支持 `supportsDynamicLaunchOverrides`。
96
112
  {{#if canCreateRunConfiguration}}
97
113
  - {{cmd_create_shell_run_configuration}}
98
- **新建项目级 Shell 配置**(`configurationName` + `scriptPath` / `scriptText` / `cloneFrom` 三选一;写 `.run/<name>.run.xml`):**重操作** —— 只给固定要跑、反复要用的流程建,**且建之前必须先取得用户同意**;只支持 Shell 类型,**其他类型请用户在 IDE 里建**。
114
+ **新建项目级 Shell 配置**(`configurationName` + `scriptPath` / `scriptText` / `cloneFrom` 三选一;写 `.run/<name>.run.xml`):只支持 Shell 类型。它是**项目资产**,不是"跑命令"的手段 —— 只是要跑一条命令就用原生 `shell`。判据只有一条:**这件事以后会不会以同样的形态再发生?**
115
+ - **这里只是"建"的一个入口,不是全部**:非 Shell 类型,以及**删 / 改**,都走 `idea-run-config-manager` skill —— 让用户调它(它有逐类型模板;没有模板的类型,它会让用户在 IDE 里配)。
116
+ - **会 → 该建,而且你要主动提**。典型信号:用户让你做的是**重复性动作**,还会对别的环境 / 别的分支 / 下一次改表再做一遍 —— 例如改完表结构要**同步某个环境的 DDL**、跑数据库迁移或回滚、生成代码、打包、起 dev server、往某个环境部署、按固定步骤清洗数据。这时**别默默每次手搓命令**(下次还得从头猜一遍),也**别自己动手建**:**先问用户**"这个流程以后要反复跑,要不要固化成一个 Shell run configuration?",并把方案摊开:配置名、实际执行的命令(`scriptText`,或它指向的 `scriptPath`)、工作目录、为什么值得留。**同意才建**;用户说这次先这样,就照办,别再追着问。
117
+ - **不会 → 用原生 `shell` 直接跑,别建**。这就是两者的分界线:原生 `shell` 是**就地执行、用完不留痕**(命令只存在于会话里);`create_shell_run_configuration` 是**把命令落盘成 `.run/<name>.run.xml`、往后反复用**(它产出的是项目文件,IDE 的 Run 窗口里也会多一项)。所以一次性的、探索性的、调试性的、参数每次都不一样的操作(查条数据、看个日志、试一条命令、临时换个路径跑一下)一律走 `shell` 跑完算 —— 这类东西建配置只是给项目留垃圾。确需项目环境的解释器时,在 `shell` 命令里显式指定、或在已有配置上单次覆盖参数,也别为这一次性的事新建配置。
118
+ - **建之前先查,防泛滥**:先 `get_run_configurations` 看有没有已在做同一件事的配置 —— 有就**复用**(必要时让用户改它),别新建一个几乎一样的;同一件事**不要按参数 / 按文件 / 按环境拆成一堆配置**,变化的入参用 `programArguments` / `envs` 单次覆盖。一个项目通常就那么**几个**(构建、装依赖、起服务、迁移这类固定流程);配置数量明显长起来就是滥用信号 —— 动手前先问自己:这是项目固定流程,还是我只是想省一条 shell 命令?
99
119
  {{/if}}
100
120
  {{#if canListRunningConfigurations}}
101
121
  - {{cmd_list_running_configurations}}
@@ -109,10 +129,37 @@
109
129
  - {{cmd_rerun_run_configuration}}
110
130
  **改完代码重启无热重载的进程**:在跑就重启、没在跑就启动;读 `started` 判断新进程是否真的起来了。不要用 `pkill` / `kill` / `shell` 代替。
111
131
  {{/if}}
132
+ {{#if canListDeploymentServers}}
133
+
134
+ ### 服务器文件同步
135
+
136
+ 往服务器传文件 / 从服务器取文件,走这一组 —— **别用 `ssh` / `rsync` / `scp` 自己拼一份**:IDE 的连接、路径映射、排除规则都在它们后面,自己拼的不是 IDE 在管的那一份。
137
+
138
+ - {{cmd_list_deployment_servers}}
139
+ **本项目能用的部署服务器**:名称(后面几条用它当 `serverName`)、项目级还是应用级、协议、host / port、在本项目里的路径映射,以及**项目级**的部署选项与危险组合告警。**只读 IDE 配置、不联网**;动手前先看 `warnings[]`。
140
+ {{#if canUploadToDeployment}}
141
+ - {{cmd_upload_to_deployment}}
142
+ **把映射到的本地目录传到服务器**(等同 IDE 的 Upload to …)。`mode` 默认 `mirror` —— **会删掉服务器上多出来的文件**,使两边一致;要留住它们就传 `upload`。**破坏性操作**。
143
+ {{/if}}
144
+ {{#if canDownloadFromDeployment}}
145
+ - {{cmd_download_from_deployment}}
146
+ **把服务器上映射目录的内容拉回本地**(等同 IDE 的 Download from …),**以服务器为准**:两边都有的文件会被远端覆盖(内容相同则不写)。**没有"保留我的版本"这个选项**。
147
+ {{/if}}
148
+ {{#if canGetDeploymentTaskStatus}}
149
+ - {{cmd_get_deployment_task_status}}
150
+ **查一次传输的状态**:`handle` 用上传 / 下载返回的那个 —— **它自己不启动任何传输**。
151
+ {{/if}}
152
+
153
+ 上传 / 下载都在 IDE 后台跑,单次调用只等到 `waitMs`(默认 10 秒)为止;返回 `in_progress` 是**正常**的,不是失败 —— 拿 `handle` 去查进度,**别用重复调上传 / 下载代替**(那样拿不到进度)。
154
+ {{/if}}
112
155
 
113
156
  ## 命令执行(所有项目通用)
114
157
 
115
158
  - 需要项目环境的命令(依赖 IDE 里配好的 JDK / Node / Python 版本等):用该项目前缀的 run configuration 工具,不要用原生 `shell`。
116
- - run configuration 是**重资产**:一个项目通常只建几个(固定要跑的构建 / 装依赖 / dev server 之类),**不是随手一条命令就建一个**。要新建时先确认它**反复要用、且属于项目固定流程**,再**先告诉用户要建什么、为什么,取得同意才动手**,别自己决定。
117
- - **配置一律不由你写**:不要手写 `.run/*.run.xml`,也不要手搓启动命令。
159
+ - 写 `.run/*.run.xml` **只走 `idea-run-config-manager` skill**:它有逐类型模板(`assets/types/`),写完还要复查。**平时不要自己手写配置,也不要用一条裸命令代替配置。**
160
+ {{#if canCreateRunConfiguration}}
161
+ - 要固化的固定流程(判据见上:会不会以同样形态重复发生):Shell 类型用上面那个「新建项目级 Shell 配置」建;非 Shell 类型与删 / 改,交给用户调 `idea-run-config-manager` skill。
162
+ {{else}}
163
+ - 要固化的固定流程:把配置名、实际执行的命令、工作目录、为什么值得留讲清楚,**请用户在 IDE 里自己建** —— 当前没有能建它的工具(`idea-run-config-manager` skill 的门就是这个工具,所以它这次也不会出现),不要手写文件凑。
164
+ {{/if}}
118
165
  - 不需要项目环境的命令:用原生 `shell`。
@@ -246,7 +246,7 @@ export function parseSubprojectArgument(text) {
246
246
  /**
247
247
  * 从参数文本里切出 IDE 名。
248
248
  *
249
- * `/open-in-idea pycharm` 是"AI 不配合时的直通逃生口":正常流程由 AI 向用户提问后
249
+ * `/open-in-ide pycharm` 是"AI 不配合时的直通逃生口":正常流程由 AI 向用户提问后
250
250
  * 调工具写回,带参只是保险。空串表示"没给",由调用方决定是提问还是用默认值。
251
251
  *
252
252
  * @param {unknown} text
@@ -8,17 +8,27 @@
8
8
  // * native -> `idea.js` (always present; the IDE owns the catalog)
9
9
  // * plugin -> this file (present only if the user installed the plugin)
10
10
  //
11
- // The companion plugin (https://github.com/syllr/idea-mcp-enhance) fills the gaps
12
- // the bundled server leaves in run configuration handling: the bundled server can
13
- // only START an existing configuration — it cannot create one, stop one, restart
14
- // one, or say what is currently running.
11
+ // The companion plugin (https://github.com/syllr/idea-mcp-enhance) fills gaps the
12
+ // bundled server leaves in two places:
13
+ //
14
+ // * run configuration — the bundled server can only START an existing
15
+ // configuration; it cannot create one, stop one, restart one, or say what is
16
+ // currently running;
17
+ // * deployment — the bundled server has nothing at all for the IDE's
18
+ // Deployment settings or for moving files to/from a server.
19
+ //
20
+ // The two are kept as separate groups everywhere (here, in the capability
21
+ // functions, and in the guidance), because they are NOT the same precondition:
22
+ // deployment additionally needs `com.jetbrains.plugins.webDeployment`, which
23
+ // exists only in Ultimate. One boolean covering both would let a deployment tool
24
+ // answer "are the run-lifecycle tools here?".
15
25
  //
16
26
  // Consequence of the split, and the reason it matters: a stock IDE serves a
17
27
  // SHORTER catalog than an enhanced one. Nothing here may be assumed — every use
18
- // has to be gated on the tools really being in the request (`servingRunLifecycleTools`).
28
+ // has to be gated on the tools really being in the request.
19
29
 
20
30
  /**
21
- * The enhance plugin's tool names, as **suffixes**.
31
+ * The enhance plugin's **run-lifecycle** tool names, as **suffixes**.
22
32
  *
23
33
  * OpenCode namespaces an MCP server's tools with the server name, and each
24
34
  * project is registered under its own prefix (root = `ide`, a subproject = its
@@ -45,6 +55,48 @@ export const RUN_LIFECYCLE_SUFFIXES = [
45
55
  RERUN_RUN_CONFIGURATION_SUFFIX,
46
56
  ];
47
57
 
58
+ /**
59
+ * The enhance plugin's **deployment** tool names, as suffixes — same contract and
60
+ * same reason for suffixes as {@link RUN_LIFECYCLE_SUFFIXES}.
61
+ *
62
+ * Keep in sync with `DeploymentSchemas.kt` in the enhance repository.
63
+ *
64
+ * The four are gated together on one plugin (`com.jetbrains.plugins.webDeployment`,
65
+ * Ultimate only) rather than on version drift, so they usually appear or vanish as
66
+ * a set — the guidance still names them one by one, because "usually" is not
67
+ * "always" and a named tool the model cannot call is only a failed call.
68
+ */
69
+ export const LIST_DEPLOYMENT_SERVERS_SUFFIX = 'list_deployment_servers';
70
+ export const UPLOAD_TO_DEPLOYMENT_SUFFIX = 'upload_to_deployment';
71
+ export const DOWNLOAD_FROM_DEPLOYMENT_SUFFIX = 'download_from_deployment';
72
+ export const GET_DEPLOYMENT_TASK_STATUS_SUFFIX = 'get_deployment_task_status';
73
+
74
+ /** All of them: look at the servers, upload, download, then poll a transfer. */
75
+ export const DEPLOYMENT_SUFFIXES = [
76
+ LIST_DEPLOYMENT_SERVERS_SUFFIX,
77
+ UPLOAD_TO_DEPLOYMENT_SUFFIX,
78
+ DOWNLOAD_FROM_DEPLOYMENT_SUFFIX,
79
+ GET_DEPLOYMENT_TASK_STATUS_SUFFIX,
80
+ ];
81
+
82
+ /**
83
+ * Does this request carry the tool called `suffix` under `prefix`?
84
+ *
85
+ * A malformed or missing snapshot answers "no": assuming a tool exists is the one
86
+ * mistake that cannot be walked back once the model has read the guidance.
87
+ *
88
+ * Exported because the guidance asks this twice. The capability flags (below) ask
89
+ * "does this request have the tool somewhere" (any project will do); the command
90
+ * list next to each tool asks "under WHICH prefix", and that answer must be exact —
91
+ * 提示词里点名一个模型调不到的命令,是这个模块唯一要防的失败。
92
+ *
93
+ * @param {Record<string, unknown> | undefined} tools
94
+ * @param {string} prefix 该项目的工具前缀(含下划线)。
95
+ * @param {string} suffix
96
+ */
97
+ export const servesTool = (tools, prefix, suffix) =>
98
+ Boolean(tools) && typeof tools === 'object' && `${prefix}${suffix}` in tools;
99
+
48
100
  /**
49
101
  * Which of the enhance plugin's tools **this project's prefix** carries in this
50
102
  * request.
@@ -60,12 +112,30 @@ export const RUN_LIFECYCLE_SUFFIXES = [
60
112
  * @param {string} prefix 该项目的工具前缀(含下划线)。
61
113
  */
62
114
  export function runLifecycleCapabilities(tools, prefix) {
63
- const has = (suffix) =>
64
- Boolean(tools) && typeof tools === 'object' && `${prefix}${suffix}` in tools;
65
115
  return {
66
- canCreateRunConfiguration: has(CREATE_SHELL_RUN_CONFIGURATION_SUFFIX),
67
- canListRunningConfigurations: has(LIST_RUNNING_CONFIGURATIONS_SUFFIX),
68
- canStopRunConfiguration: has(STOP_RUN_CONFIGURATION_SUFFIX),
69
- canRerunRunConfiguration: has(RERUN_RUN_CONFIGURATION_SUFFIX),
116
+ canCreateRunConfiguration: servesTool(tools, prefix, CREATE_SHELL_RUN_CONFIGURATION_SUFFIX),
117
+ canListRunningConfigurations: servesTool(tools, prefix, LIST_RUNNING_CONFIGURATIONS_SUFFIX),
118
+ canStopRunConfiguration: servesTool(tools, prefix, STOP_RUN_CONFIGURATION_SUFFIX),
119
+ canRerunRunConfiguration: servesTool(tools, prefix, RERUN_RUN_CONFIGURATION_SUFFIX),
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Same question for the deployment group, and the same answer shape — see
125
+ * {@link runLifecycleCapabilities}.
126
+ *
127
+ * The names repeat the tool's subject rather than the group (`canUploadToDeployment`,
128
+ * not `canUpload`): the guidance is one document about two groups, and a flag whose
129
+ * owner is unclear is how the wrong section gets the wrong text.
130
+ *
131
+ * @param {Record<string, unknown> | undefined} tools
132
+ * @param {string} prefix 该项目的工具前缀(含下划线)。
133
+ */
134
+ export function deploymentCapabilities(tools, prefix) {
135
+ return {
136
+ canListDeploymentServers: servesTool(tools, prefix, LIST_DEPLOYMENT_SERVERS_SUFFIX),
137
+ canUploadToDeployment: servesTool(tools, prefix, UPLOAD_TO_DEPLOYMENT_SUFFIX),
138
+ canDownloadFromDeployment: servesTool(tools, prefix, DOWNLOAD_FROM_DEPLOYMENT_SUFFIX),
139
+ canGetDeploymentTaskStatus: servesTool(tools, prefix, GET_DEPLOYMENT_TASK_STATUS_SUFFIX),
70
140
  };
71
141
  }
package/src/mcp/idea.js CHANGED
@@ -13,7 +13,7 @@
13
13
  // HTTP header `IJ_MCP_SERVER_PROJECT_PATH` (see `serverConfig`).
14
14
  // * Availability probe: a single `initialize` POST to `/stream` succeeds when
15
15
  // the MCP server is up. We do NOT check whether a specific project is open —
16
- // the user triggers `/open-in-idea` for the project they want, so "server is
16
+ // the user triggers `/open-in-ide` for the project they want, so "server is
17
17
  // up" is the only thing worth knowing.
18
18
 
19
19
  export const PROJECT_HEADER = 'IJ_MCP_SERVER_PROJECT_PATH';
@@ -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.18' },
130
+ clientInfo: { name: 'opencode-idea', version: '0.0.19' },
131
131
  },
132
132
  });
133
133
  // The IDE's Streamable-HTTP endpoint binds follow-up requests to the session
package/src/plugin.js CHANGED
@@ -14,17 +14,18 @@
14
14
  // its own project through the `IJ_MCP_SERVER_PROJECT_PATH` header; injects
15
15
  // per-project direct-call guidance; and hides the IDE tools that must not
16
16
  // reach the model (for every prefix);
17
- // 2. registers `/open-in-idea` (接入 root 与其全部子项目), `/delete-in-ide`
17
+ // 2. registers `/open-in-ide` (接入 root 与其全部子项目), `/delete-in-ide`
18
18
  // (删掉整个配置) and `/add-sub-module-in-ide <路径>` (登记一个子项目),
19
19
  // plus the `configure_ide_project` tool the MODEL calls to write the IDE a
20
20
  // user picked into the config;
21
- // 3. registers the `idea-standalone` skill (marked projects only).
21
+ // 3. registers the shipped skills (`skills/<id>/SKILL.md`) — marked projects
22
+ // only, and each one also gated on the IDE tools it drives.
22
23
  //
23
24
  // A root that was never configured is left untouched. Everything is manual:
24
25
  // setup does NOT probe or connect any IDE. The commands probe, open the IDE, and
25
26
  // load the MCP servers + guidance on demand.
26
27
  //
27
- // `/open-in-idea` opens each configured project in ITS OWN IDE, waits until its
28
+ // `/open-in-ide` opens each configured project in ITS OWN IDE, waits until its
28
29
  // MCP endpoint is ready, registers it under its prefix, and keeps the config in
29
30
  // `ctx.storage`. A live endpoint alone does NOT mean our project is open there
30
31
  // (the running IDE may have a different project open), so the open flow asks the
@@ -38,7 +39,7 @@
38
39
  // after OpenCode evicted the project's Location services (the Location's
39
40
  // LayerMap uses `idleTimeToLive: "60 minutes"`) or after the process
40
41
  // restarted — setup deliberately does not connect, so without this the
41
- // session would stay tool-less until the user ran `/open-in-idea` by hand.
42
+ // session would stay tool-less until the user ran `/open-in-ide` by hand.
42
43
  // `prompt` is awaited before the request is built, so the message that
43
44
  // triggers it already carries the tools;
44
45
  // * the `context` hook, when a request that HAD IDE tools no longer does;
@@ -54,7 +55,6 @@ import { existsSync } from 'node:fs';
54
55
  import { resolve } from 'node:path';
55
56
 
56
57
  import { callTool, serverConfig } from './mcp/idea.js';
57
- import { runLifecycleCapabilities } from './mcp/enhance.js';
58
58
  import { applyIdeToolDefaults, hideForcedArguments } from './mcp/idea-tool-defaults.js';
59
59
  import { currentProjectPath } from './project.js';
60
60
  import {
@@ -405,26 +405,26 @@ export function ideFeedback(result) {
405
405
  return list ? `IDE MCP 已连接并接入:${list}。` : 'IDE MCP 已连接。';
406
406
  }
407
407
  case 'tools-loading':
408
- return 'IDE MCP 已连接,但原生 IDE 工具列表尚未完成注册。请稍后再次执行 /open-in-idea。';
408
+ return 'IDE MCP 已连接,但原生 IDE 工具列表尚未完成注册。请稍后再次执行 /open-in-ide。';
409
409
  case 'mcp-unavailable': {
410
410
  const where = nameOf(result?.project);
411
411
  const ide = appOf(result?.project);
412
412
  return result?.opened
413
- ? `已请求打开/激活 ${where} 并等待 MCP 服务启动,但未检测到 MCP 服务。请在 ${ide} 中开启 MCP 服务(Settings → MCP Server,需 2026.2+)并启用 Brave Mode,然后再次执行 /open-in-idea。`
414
- : `未检测到 ${where} 的 MCP 服务。请确认已打开,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后再次执行 /open-in-idea。`;
413
+ ? `已请求打开/激活 ${where} 并等待 MCP 服务启动,但未检测到 MCP 服务。请在 ${ide} 中开启 MCP 服务(Settings → MCP Server,需 2026.2+)并启用 Brave Mode,然后再次执行 /open-in-ide。`
414
+ : `未检测到 ${where} 的 MCP 服务。请确认已打开,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后再次执行 /open-in-ide。`;
415
415
  }
416
416
  case 'launch-failed':
417
- return `无法拉起 ${nameOf(result?.project)}(未找到应用或启动失败)。请确认它已安装 —— 没装的话先装上,再重试 /open-in-idea。`;
417
+ return `无法拉起 ${nameOf(result?.project)}(未找到应用或启动失败)。请确认它已安装 —— 没装的话先装上,再重试 /open-in-ide。`;
418
418
  case 'disabled':
419
- return '插件已通过 openInIde: false 关闭 IDE 启动。请手动打开 IDE,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后重试 /open-in-idea。';
419
+ return '插件已通过 openInIde: false 关闭 IDE 启动。请手动打开 IDE,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后重试 /open-in-ide。';
420
420
  case 'manual-launch':
421
- return '当前系统不支持自动打开 IDE(仅 macOS 实现了启动)。请手动打开 IDE,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后再次执行 /open-in-idea。';
421
+ return '当前系统不支持自动打开 IDE(仅 macOS 实现了启动)。请手动打开 IDE,在 Settings → MCP Server 开启 MCP 服务并启用 Brave Mode,然后再次执行 /open-in-ide。';
422
422
  case 'deleted':
423
- return '已删除本项目的 IDE 配置:已注销全部 IDE MCP 并清除配置。如需重新接入,请执行 /open-in-idea。';
423
+ return '已删除本项目的 IDE 配置:已注销全部 IDE MCP 并清除配置。如需重新接入,请执行 /open-in-ide。';
424
424
  case 'bad-argument':
425
425
  return `命令参数有误:${result?.message ?? ''}`;
426
426
  case 'not-configured':
427
- return '当前项目还没有 IDE 配置。请先执行 /open-in-idea 接入根项目,再登记子项目。';
427
+ return '当前项目还没有 IDE 配置。请先执行 /open-in-ide 接入根项目,再登记子项目。';
428
428
  case 'invalid-subproject':
429
429
  return `无法登记子项目:${result?.message ?? ''}`;
430
430
  case 'needs-ide':
@@ -435,7 +435,7 @@ export function ideFeedback(result) {
435
435
  }
436
436
 
437
437
  /**
438
- * The text the MODEL sees for `/open-in-idea` feedback. It is explicitly a
438
+ * The text the MODEL sees for `/open-in-ide` feedback. It is explicitly a
439
439
  * notification, not a task: the model must only acknowledge it. The user sees
440
440
  * the clean notice instead, via `metadata.displayText`.
441
441
  */
@@ -850,7 +850,7 @@ export default {
850
850
  * `open -a` only requests activation/opening. A cold IDE needs time before
851
851
  * its bundled MCP server listens, so poll until the endpoint appears AND
852
852
  * answers for THIS project. This is the step that makes a single
853
- * `/open-in-idea` enough on a cold start.
853
+ * `/open-in-ide` enough on a cold start.
854
854
  *
855
855
  * @param {{ path: string, app: string }} project
856
856
  * @returns {Promise<number | undefined>}
@@ -946,7 +946,7 @@ export default {
946
946
  };
947
947
 
948
948
  /**
949
- * `/open-in-idea` 的主流程:让每个已登记项目的 IDE 都就绪,然后把它们注册进
949
+ * `/open-in-ide` 的主流程:让每个已登记项目的 IDE 都就绪,然后把它们注册进
950
950
  * OpenCode。
951
951
  *
952
952
  * 每个项目各自拉起**自己的** IDE —— root 在 IDEA、某子项目在 PyCharm 是常态,
@@ -970,7 +970,7 @@ export default {
970
970
  let toolsReady = false;
971
971
  try {
972
972
  // 重新探测一次并整体注册:ensureProject 只保证 IDE 就绪,注册要按"当前真正
973
- // 打开着哪些项目"重来一遍,这样 `/open-in-idea` 每次都是同一套结果。
973
+ // 打开着哪些项目"重来一遍,这样 `/open-in-ide` 每次都是同一套结果。
974
974
  toolsReady = (await reconcile({ probeIde: true })) === true;
975
975
  } catch {
976
976
  // capability load is best effort; feedback still matters
@@ -987,7 +987,7 @@ export default {
987
987
  * The `context` hook latches the rebuild the moment a request shows the tools
988
988
  * are gone, but a latch alone only fires on the NEXT user message — one whole
989
989
  * turn later, which is exactly the gap that made a dead session sit there
990
- * until `/open-in-idea` was run by hand. This runs out of band (it never
990
+ * until `/open-in-ide` was run by hand. This runs out of band (it never
991
991
  * blocks the request) so the next request — the following agent-loop step, or
992
992
  * any later message — already carries the tools again.
993
993
  *
@@ -1065,20 +1065,21 @@ export default {
1065
1065
  const isSameSkillSet = (skills, ids) =>
1066
1066
  skills.length === ids.length && skills.every((skill, index) => skill.id === ids[index]);
1067
1067
 
1068
- // The shipped skills are static knowledge (`skills/<id>/SKILL.md`), e.g. the
1069
- // standalone-mode discipline (every app starts from an IDEA run
1070
- // configuration, and starting one is the user's call, so the skill has to be
1071
- // there to write the project's own rules). They pass two independent gates,
1068
+ // The shipped skills are static knowledge (`skills/<id>/SKILL.md`) that write
1069
+ // a project's own rules into its `AGENTS.md`: `idea-standalone` fixes how
1070
+ // resident apps are started, `idea-run-config-manager` fixes which repeated
1071
+ // flows are solidified into configurations. They pass two independent gates,
1072
1072
  // because a skill can be wrong twice over:
1073
1073
  //
1074
1074
  // * the project config — an unrelated project never sees an IDE skill;
1075
1075
  // * the tools the IDE actually serves — a skill that teaches a call the
1076
1076
  // model cannot make is not knowledge, it is a failed call waiting to
1077
- // happen. `idea-standalone` drives `rerun_run_configuration`, which only
1078
- // the companion plugin `idea-mcp-enhance` serves, so a stock IDE must not
1079
- // see it at all. Each skill declares its own dependency in its frontmatter
1080
- // (`requires:`), as a SUFFIX — tool names carry a per-project prefix, so
1081
- // a hard-coded `idea_...` could only ever match one project.
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.
1082
1083
  //
1083
1084
  // The catalog is unknown at setup: the config is read from storage offline,
1084
1085
  // before anything has connected to the IDE. So this is a reconcile rather
@@ -1089,7 +1090,7 @@ export default {
1089
1090
  // Deliberately NOT wrapped in a catch: these are core features of the plugin,
1090
1091
  // so a rejected definition must fail loudly. Swallowing it would ship a
1091
1092
  // silently incomplete plugin — which is exactly how 0.0.6 lost
1092
- // `/open-in-idea` without anyone noticing until the command was gone.
1093
+ // `/open-in-ide` without anyone noticing until the command was gone.
1093
1094
  const syncSkillsOnce = async () => {
1094
1095
  if (typeof ctx.skill?.transform !== 'function') return;
1095
1096
 
@@ -1138,7 +1139,7 @@ export default {
1138
1139
  };
1139
1140
  // A project configured in an earlier run gets its skills as soon as the IDE
1140
1141
  // has answered; a project configured for the first time gets them when
1141
- // `/open-in-idea` runs (both paths go through `writeConfig` / this setup hook).
1142
+ // `/open-in-ide` runs (both paths go through `writeConfig` / this setup hook).
1142
1143
  await syncSkills();
1143
1144
 
1144
1145
  /**
@@ -1227,7 +1228,7 @@ export default {
1227
1228
  return { content: `已把根项目配置为 ${IDE_LABELS[ide]} 并接入。` };
1228
1229
  }
1229
1230
  if (!current) {
1230
- return { content: '当前项目还没有 IDE 配置。请先执行 /open-in-idea。' };
1231
+ return { content: '当前项目还没有 IDE 配置。请先执行 /open-in-ide。' };
1231
1232
  }
1232
1233
  const absPath = resolve(projectPath, rawPath);
1233
1234
  if (!existsSync(absPath)) {
@@ -1268,15 +1269,15 @@ export default {
1268
1269
  execute: async (args) => configureIdeProject(args),
1269
1270
  });
1270
1271
 
1271
- // `/open-in-idea` 接入 root 与其全部子项目;`/add-sub-module-in-ide <路径>` 登记
1272
+ // `/open-in-ide` 接入 root 与其全部子项目;`/add-sub-module-in-ide <路径>` 登记
1272
1273
  // 一个子项目(IDE 由模型问用户后写回);`/delete-in-ide` 删除整个配置。
1273
1274
  //
1274
- // 命名说明:命令名里的 `idea` 是历史包袱(工具前缀早已改成与产品无关的 `ide_`),
1275
- // 保留它是为了不改用户天天敲的名字;它的语义是"接入 IDE",不是"接入 IntelliJ"。
1275
+ // 三个名字都带 `-ide` 而不是 `-idea`:这里说的是"接入 IDE",与具体产品无关 —— root 可能是
1276
+ // IDEA、子项目可能是 PyCharm,和工具前缀(`ide_*` / `<目录名>_*`)是同一套口径。
1276
1277
  if (typeof ctx.command?.transform === 'function') {
1277
1278
  commandRegistration = await ctx.command.transform((editor) => {
1278
1279
  editor.add({
1279
- name: 'open-in-idea',
1280
+ name: 'open-in-ide',
1280
1281
  description: '接入当前项目:root 及其全部已登记子项目,各自用配置好的 IDE 打开',
1281
1282
  execute: async (input) => {
1282
1283
  const parsed = parseIdeArgument(input?.prompt?.text);
@@ -1350,7 +1351,7 @@ export default {
1350
1351
  execute: async (input) => {
1351
1352
  await writeConfig(null);
1352
1353
  // 注销全部 server。走串行化的 reconcile,所以在飞的重建(另一个
1353
- // `/open-in-idea`、请求期自愈)不会在配置已删之后再注册回来。
1354
+ // `/open-in-ide`、请求期自愈)不会在配置已删之后再注册回来。
1354
1355
  await reconcile({ probeIde: false });
1355
1356
  await sendFeedback(input, { status: 'deleted' });
1356
1357
  },
@@ -1420,8 +1421,8 @@ export default {
1420
1421
  // they are actually in this request.
1421
1422
  if (!serving) return;
1422
1423
  // 逐项目注入:每个项目那一节只写它自己的前缀,模型不需要自己换算。
1423
- // run-lifecycle 工具(来自配套插件 `idea-mcp-enhance`)也按每个项目分别判断,
1424
- // 所以一个项目装了 enhance、另一个没装时,两节的内容本来就不一样。
1424
+ // 配套插件 `idea-mcp-enhance` 的两组工具(run-lifecycle / deployment)也按每个项目分别判断,
1425
+ // 所以一个项目装了 enhance、另一个没装时,各节的内容本来就不一样。
1425
1426
  appendIdeGuidance(event?.system, currentProjects(), { tools });
1426
1427
  });
1427
1428
 
@@ -1434,7 +1435,7 @@ export default {
1434
1435
  // * `registrations.length === 0` — 本插件实例压根没有注册任何 IDE server,这
1435
1436
  // 正是 OpenCode 回收 Location 服务(LayerMap idleTimeToLive = 60 分钟)
1436
1437
  // 或进程重启后新实例的样子。setup 故意不连接,没有这一步会话就会永久
1437
- // 没有工具,只能靠用户手敲 `/open-in-idea`。
1438
+ // 没有工具,只能靠用户手敲 `/open-in-ide`。
1438
1439
  //
1439
1440
  // 只探测、不启动 IDE:走 `reconcile({ probeIde: true })`,而不是会 `open -a`
1440
1441
  // 的 `reviveIde()` —— 一条用户消息不该把 IDE 拉起来。
@@ -27,6 +27,13 @@
27
27
  // and deliberately kept OUT of the registered definition — the caller filters on
28
28
  // it instead (see `readSkills`), because a skill whose tools are absent must not
29
29
  // be registered at all.
30
+ //
31
+ // The other key is OpenCode's own, written in the portable frontmatter spelling
32
+ // OpenCode documents: a nested `metadata:` block holding
33
+ // `opencode/autoinvoke: false`. It is translated to the definition's
34
+ // `autoinvoke` — the name the registration API uses — which is what makes a
35
+ // skill user-invoked only: OpenCode keeps it registered and loadable on request,
36
+ // but leaves it out of the model's available list.
30
37
 
31
38
  import { readFileSync, readdirSync } from 'node:fs';
32
39
  import { fileURLToPath } from 'node:url';
@@ -87,11 +94,37 @@ function splitList(value) {
87
94
  .filter((entry) => entry !== '');
88
95
  }
89
96
 
97
+ /**
98
+ * Read `opencode/autoinvoke` out of the frontmatter, or `undefined` when absent.
99
+ *
100
+ * The spelling is OpenCode's portable one — a nested `metadata:` block holding
101
+ * `opencode/autoinvoke: false`. That shape is exactly what the flat reader above
102
+ * already produces: the indented line trims to `opencode/autoinvoke: false` and
103
+ * lands under that namespaced key, while the bare `metadata:` line comes out
104
+ * empty and is ignored. One nesting level is therefore supported on purpose, not
105
+ * by accident.
106
+ *
107
+ * The value must be `true` or `false`. Anything else throws: a typo that
108
+ * silently left the skill model-invocable is the failure this whole mechanism
109
+ * exists to prevent, so it must not pass quietly.
110
+ *
111
+ * @param {Record<string, string>} fields
112
+ * @param {string} source Used in error messages.
113
+ * @returns {boolean | undefined}
114
+ */
115
+ export function readAutoinvoke(fields, source) {
116
+ const raw = fields['opencode/autoinvoke'];
117
+ if (raw === undefined) return undefined;
118
+ if (raw === 'true') return true;
119
+ if (raw === 'false') return false;
120
+ throw new Error(`${source}: "opencode/autoinvoke" must be true or false, got "${raw}"`);
121
+ }
122
+
90
123
  /**
91
124
  * Load one skill directory into a `Skill.Info`-shaped definition.
92
125
  *
93
126
  * @param {string} id Directory name, used as the skill id.
94
- * @returns {{ id: string, name: string, description: string, path: string, content: string }}
127
+ * @returns {{ id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string }}
95
128
  */
96
129
  export function readSkill(id) {
97
130
  return readSkillPackage(id).skill;
@@ -104,8 +137,13 @@ export function readSkill(id) {
104
137
  * while `requires` is this package's own metadata and must NOT be registered —
105
138
  * it is what the caller filters on.
106
139
  *
140
+ * `autoinvoke` is OpenCode's, so it goes the other way: it ships in the
141
+ * frontmatter as `opencode/autoinvoke` and is translated onto the definition,
142
+ * where it picks user-invoked-only over model-invocable. It is only set when the
143
+ * frontmatter asks for it, so a skill that says nothing keeps the plain shape.
144
+ *
107
145
  * @param {string} id Directory name, used as the skill id.
108
- * @returns {{ skill: { id: string, name: string, description: string, path: string, content: string }, requires: string[] }}
146
+ * @returns {{ skill: { id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string }, requires: string[] }}
109
147
  */
110
148
  export function readSkillPackage(id) {
111
149
  const file = new URL(`${id}/${SKILL_FILE_NAME}`, SKILLS_DIR);
@@ -116,11 +154,13 @@ export function readSkillPackage(id) {
116
154
  if (!description) {
117
155
  throw new Error(`${source}: frontmatter needs a "description", otherwise the skill is never offered`);
118
156
  }
157
+ const autoinvoke = readAutoinvoke(fields, source);
119
158
  return {
120
159
  skill: {
121
160
  id,
122
161
  name: fields.name || id,
123
162
  description,
163
+ ...(autoinvoke === undefined ? {} : { autoinvoke }),
124
164
  path: fileURLToPath(file),
125
165
  content: text.replace(FRONTMATTER, '').replace(/^\s+/, '').trimEnd(),
126
166
  },
@@ -131,7 +171,7 @@ export function readSkillPackage(id) {
131
171
  /**
132
172
  * Every shipped skill, sorted by id, each with the tools it requires.
133
173
  *
134
- * @returns {Array<{ skill: { id: string, name: string, description: string, path: string, content: string }, requires: string[] }>}
174
+ * @returns {Array<{ skill: { id: string, name: string, description: string, autoinvoke?: boolean, path: string, content: string }, requires: string[] }>}
135
175
  */
136
176
  export function readSkills() {
137
177
  return readdirSync(SKILLS_DIR, { withFileTypes: true })