opencode-idea 0.0.17 → 0.0.18
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 +227 -159
- package/package.json +5 -1
- package/skills/idea-standalone/SKILL.md +4 -2
- package/skills/idea-standalone/assets/AGENTS.template.md +5 -5
- package/src/ide-guidance.js +109 -39
- package/src/ide-guidance.template.md +86 -39
- package/src/ide-launcher.js +87 -27
- package/src/ide-projects.js +262 -0
- package/src/mcp/enhance.js +36 -24
- package/src/mcp/idea-tool-defaults.js +27 -12
- package/src/mcp/idea.js +8 -74
- package/src/plugin.js +715 -355
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-idea",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
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",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"src/plugin.js",
|
|
11
11
|
"src/template.js",
|
|
12
12
|
"src/ide-guidance.template.md",
|
|
13
|
+
"src/ide-projects.js",
|
|
13
14
|
"src/mcp/idea.js",
|
|
14
15
|
"src/mcp/enhance.js",
|
|
15
16
|
"src/mcp/idea-tool-defaults.js",
|
|
@@ -39,6 +40,9 @@
|
|
|
39
40
|
],
|
|
40
41
|
"author": "yutao",
|
|
41
42
|
"license": "MIT",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"registry": "https://registry.npmjs.org"
|
|
45
|
+
},
|
|
42
46
|
"repository": {
|
|
43
47
|
"type": "git",
|
|
44
48
|
"url": "git+https://github.com/syllr/opencode-idea.git"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: idea-standalone
|
|
3
|
-
requires:
|
|
3
|
+
requires: rerun_run_configuration
|
|
4
4
|
description: "为项目生成/维护 standalone(启动收口只有 IDEA run configuration)模式的纪律,写进**项目根 `AGENTS.md`** 里由 `<!-- idea-standalone:begin -->` 与 `<!-- idea-standalone:end -->` 包裹的标记块。当用户要做 standalone 规范、要固化「本机跑哪几个应用」「哪些应用热重载、哪些改了必须重启」「启停走哪个工具」这类项目级纪律时使用。产物固定三段:standalone 是什么、经 IDEA run configuration 启动哪些应用、逐应用的热重载与重启提示纪律。"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -19,8 +19,10 @@ description: "为项目生成/维护 standalone(启动收口只有 IDEA run conf
|
|
|
19
19
|
|
|
20
20
|
## 读取
|
|
21
21
|
|
|
22
|
+
- **工具名前缀跟着项目走**:本项目的工具前缀由它接入时的名字决定 —— root 是 `ide`,子项目是它的目录名。下文的
|
|
23
|
+
`<前缀>_` 就是它(例如 `ide_get_run_configurations` 或 `modelserver_get_run_configurations`)。
|
|
22
24
|
- 读 `<项目根>/AGENTS.md`,确认是否已存在 `idea-standalone` 标记块(更新)或需要在文件末尾追加(新增)。
|
|
23
|
-
-
|
|
25
|
+
- **该项目的 `<前缀>_get_run_configurations` 直接调用,不传任何参数** → 项目里已有的 run configuration 就是**用户已经在 IDEA 里配好的那些**,
|
|
24
26
|
这一次调用会把它们的名字连同启动细节(程序参数、工作目录、环境变量、是否支持启动时覆盖)一并给出。
|
|
25
27
|
第二段的 `run configuration` 列、以及端口,**只能来自这里** —— 不需要再去逐个翻 `.run/*.run.xml`,那份列表已经是配置的结果。
|
|
26
28
|
- 项目类型(`package.json` / `pom.xml` / `go.mod` / `pyproject.toml` / `Cargo.toml`)→ 决定第三段的热重载列。
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
- **入口唯一(唯一绝对的约束)**:每个应用的启动入口就是 IDEA 的 run configuration,**不是手搓命令行**。
|
|
17
17
|
- **形态不限**:配置可以起本机进程,也可以是容器(Compose / K8s)、或部署到远端 —— 怎么起是配置的事,**不要假设它一定在本机直跑**。相应地,哪些应用**不在本机跑、或本机起不来**要单独写明(需要 GPU 的服务、本机直连的远端数据库、只能部署后验证的服务),别当成"本机直跑"来验证。
|
|
18
18
|
- **只启动其中一部分是常态** —— 不要求所有应用都起来,用户可能只开一两个。
|
|
19
|
-
- **跑应用只用
|
|
19
|
+
- **跑应用只用 `<前缀>_rerun_run_configuration`**:在跑就重启、没在跑就启动 —— 不必分辨"这是启动还是重启",直接 rerun 就完事。
|
|
20
20
|
常驻进程化 / 服务化的 run configuration 一律走这一条。
|
|
21
|
-
- **不要用 `
|
|
21
|
+
- **不要用 `<前缀>_execute_run_configuration` 跑这些应用**:它是给**命令 / Job 类型**的配置跑的,standalone 的配置是**常驻型**(起来就不退),
|
|
22
22
|
用它只会起出重复实例。
|
|
23
23
|
- **也不要绕过 IDEA**:不用 `go run` / `pnpm dev` / `uvicorn` / `python -m ...` 这类裸命令起进程,更不要 `kill` / `pkill` 业务进程 ——
|
|
24
24
|
外部杀掉的进程 IDE 并不知道(Run 窗口仍显示运行中,下次启动就撞端口)。
|
|
25
|
-
- **重启不必先问**:改完代码要重启哪个应用,直接 `
|
|
26
|
-
- 工具列表里没有 `
|
|
25
|
+
- **重启不必先问**:改完代码要重启哪个应用,直接 `<前缀>_rerun_run_configuration` —— 这条链是无人化的,先征求同意只会把重启卡住。
|
|
26
|
+
- 工具列表里没有 `<前缀>_rerun_run_configuration` 时(没装配套插件):交回用户,请他在 IDEA 里手动跑,AI 不动手。
|
|
27
27
|
- **与部署链纪律互不构成许可**:本段管的是 **standalone 跑应用的方式**,不适用于部署流程;反过来,部署链里的步骤也不构成"可以绕开本段、用裸命令在本机起应用"的许可 —— 两阶段纪律互相独立。
|
|
28
28
|
|
|
29
29
|
## 二、standalone 下启动哪些应用(必须经 IDEA run configuration)
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
所以**每张表里的 `run configuration` 必须是该平台上真正存在、真正能跑的那个名字**,不要把 Windows 上能用的配置写进 macOS 那张表。
|
|
36
36
|
某个应用在某平台上起不来,就在该表里注明(或干脆不列),别让 AI 在一个平台上去跑另一个平台的配置。
|
|
37
37
|
|
|
38
|
-
`run configuration` 列**必须是 `
|
|
38
|
+
`run configuration` 列**必须是 `<前缀>_get_run_configurations` 里真实存在的名字**,不凭印象写;缺配置就**交给用户**:
|
|
39
39
|
请他在 IDEA 里自己建一个,不要手搓启动命令,也不要自己写 `.run/*.run.xml`。端口写清"固定 / 默认 / 被占用即启动失败",读不到就**问用户**,不要编。
|
|
40
40
|
**启动前流程**列:**多数应用留空**。只有确实"得先做点什么才能起"时才写,而且**按顺序写清第 1 步、第 2 步、第 3 步**(例如 `1. 装依赖 2. 构建 3. 起前置服务`);
|
|
41
41
|
这些事**用文字写出来**(AI 照这段文本自己按序去做)。这类前置事项往往没被写成 run configuration,所以必须在这里说明,否则 AI 直接 rerun 会拿到一个起不来的进程。
|
package/src/ide-guidance.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// IDE MCP routing guidance injected into the model's system prompt.
|
|
2
2
|
//
|
|
3
3
|
// The TEXT lives in `ide-guidance.template.md`, not here. It is a prompt document
|
|
4
4
|
// a human has to review as a document — with the conditionals visible as
|
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
// 2. Only IDE-only capability is routed here; reading, writing and searching
|
|
15
15
|
// stay on the native tools.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// ## 一个会话,多个项目,多套前缀
|
|
18
|
+
//
|
|
19
|
+
// 每个接入的项目都用**自己的** MCP server 注册(root = `ide`,子项目 = 目录名),
|
|
20
|
+
// 所以同一个 IDE 工具会以 `ide_search_symbol` / `modelserver_search_symbol` 这样多份
|
|
21
|
+
// 出现。但**功能是同一套,只有前缀不同** —— 所以模板只写一遍功能说明,每条功能后面用
|
|
22
|
+
// `{{cmd_xxx}}` 列出它在各项目下的命令名(由 `commandVariables` 扫模板自动生成)。
|
|
23
|
+
//
|
|
24
|
+
// 这样模型同时拿到"这条功能是什么"和"这个项目里它叫什么",不需要自己拼前缀 —— 少一层
|
|
25
|
+
// 推理,就少一类"拿错项目工具"的失败;而且文本不会随项目数成倍增长。
|
|
21
26
|
//
|
|
22
27
|
// The `{{#if can…}}` blocks gate the companion plugin's tools (`mcp/enhance.js`),
|
|
23
28
|
// which only exist when the user installed it. Nothing in the document says so:
|
|
@@ -26,12 +31,8 @@
|
|
|
26
31
|
|
|
27
32
|
import { readFileSync } from 'node:fs';
|
|
28
33
|
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
LIST_RUNNING_CONFIGURATIONS_TOOL,
|
|
32
|
-
RERUN_RUN_CONFIGURATION_TOOL,
|
|
33
|
-
STOP_RUN_CONFIGURATION_TOOL,
|
|
34
|
-
} from './mcp/enhance.js';
|
|
34
|
+
import { IDE_LABELS } from './ide-projects.js';
|
|
35
|
+
import { runLifecycleCapabilities } from './mcp/enhance.js';
|
|
35
36
|
import { renderTemplate } from './template.js';
|
|
36
37
|
|
|
37
38
|
/** The prompt document, next to this module so it ships with it. */
|
|
@@ -41,28 +42,97 @@ const TEMPLATE_PATH = new URL('ide-guidance.template.md', import.meta.url);
|
|
|
41
42
|
const TEMPLATE = readFileSync(TEMPLATE_PATH, 'utf8');
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
+
* 「本会话有哪些项目、各自用哪个 MCP 工具前缀」的清单。
|
|
46
|
+
*
|
|
47
|
+
* 放在公共段最前面,是因为模型**先要知道"该用哪个前缀"**,再去看具体工具。没有这张清单,它得从
|
|
48
|
+
* 每一节的标题里自己重建映射 —— 那是多一次推理,而推理错了的代价是"拿 root 的工具去操作
|
|
49
|
+
* 子项目",恰恰是这套多前缀设计要防的事。
|
|
50
|
+
*
|
|
51
|
+
* 明确写出"根项目 / 子项目"这个层级词:前缀本身(root `ide` vs 子项目目录名)不足以让模型一眼
|
|
52
|
+
* 看出谁是谁,而它的判断依据是"这个文件在哪个目录下"。
|
|
53
|
+
*
|
|
54
|
+
* @param {ReadonlyArray<{ label: string, path: string, app: string, server: string, isRoot: boolean }>} projects
|
|
55
|
+
*/
|
|
56
|
+
function routeTableFor(projects) {
|
|
57
|
+
const hasChildren = projects.some((project) => !project.isRoot);
|
|
58
|
+
return projects
|
|
59
|
+
.map((project) => {
|
|
60
|
+
const kind = project.isRoot ? '根项目' : '子项目';
|
|
61
|
+
const ide = IDE_LABELS[project.app] ?? project.app;
|
|
62
|
+
// 根项目的那一行必须写明"不含下列子项目" —— 子项目就在它的目录树里,不写清楚,
|
|
63
|
+
// "modelserver 的文件算根项目还是子项目"就成了模型要自己猜的问题。
|
|
64
|
+
const scope = project.isRoot && hasChildren ? ',**不含下列子项目**' : '';
|
|
65
|
+
return `- **${kind}** \`${project.label}\`(${ide} 打开)→ MCP 工具前缀 \`${project.server}_*\`,项目路径 \`${project.path}\`${scope}`;
|
|
66
|
+
})
|
|
67
|
+
.join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 每条功能在各项目下的命令名,预渲染成一行。
|
|
72
|
+
*
|
|
73
|
+
* 变量名**直接扫模板得到**:模板里写 `{{cmd_search_symbol}}`,这里就填
|
|
74
|
+
* `` `.` `ide_search_symbol` | `modelserver` `modelserver_search_symbol` ``。
|
|
75
|
+
* 所以模板增删一条工具不需要动这里的代码,也不会出现"模板里写了、代码忘了给值"的空行。
|
|
76
|
+
*
|
|
77
|
+
* @param {ReadonlyArray<{ label: string, server: string }>} projects
|
|
78
|
+
*/
|
|
79
|
+
function commandVariables(projects) {
|
|
80
|
+
/** @type {Record<string, string>} */
|
|
81
|
+
const variables = {};
|
|
82
|
+
for (const match of TEMPLATE.matchAll(/\{\{\s*(cmd_[A-Za-z0-9_]+)\s*\}\}/g)) {
|
|
83
|
+
const name = match[1];
|
|
84
|
+
const suffix = name.slice('cmd_'.length);
|
|
85
|
+
variables[name] = projects
|
|
86
|
+
// 每条都写清"哪个项目 → 哪个命令":根项目直接用它的角色名,子项目用目录名。
|
|
87
|
+
// 不写 `.`/目录名这种需要回看清单才能解读的代号 —— 模型读每条功能时不必回头查表。
|
|
88
|
+
.map((project) => `${project.isRoot ? '根项目' : project.label} \`${project.server}_${suffix}\``)
|
|
89
|
+
.join(' | ');
|
|
90
|
+
}
|
|
91
|
+
return variables;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 本会话里这些 run-lifecycle 能力可不可用。
|
|
96
|
+
*
|
|
97
|
+
* 提示词只有一份,所以这里要回答的是"这套能力在本次会话里存不存在" —— 任一项目提供即算存在。
|
|
98
|
+
* 同一个人的几个 IDE 装不装配套插件通常是一致的;万一不一致,模板里那句"没装就交给用户"兜住。
|
|
99
|
+
*
|
|
100
|
+
* @param {ReadonlyArray<{ server: string }>} projects
|
|
101
|
+
* @param {Record<string, unknown> | undefined} tools
|
|
102
|
+
*/
|
|
103
|
+
function mergeRunLifecycle(projects, tools) {
|
|
104
|
+
const all = projects.map((project) => runLifecycleCapabilities(tools, `${project.server}_`));
|
|
105
|
+
return {
|
|
106
|
+
canCreateRunConfiguration: all.some((c) => c.canCreateRunConfiguration),
|
|
107
|
+
canListRunningConfigurations: all.some((c) => c.canListRunningConfigurations),
|
|
108
|
+
canStopRunConfiguration: all.some((c) => c.canStopRunConfiguration),
|
|
109
|
+
canRerunRunConfiguration: all.some((c) => c.canRerunRunConfiguration),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 把本会话接入的全部项目渲染成**一份**引导文本。
|
|
115
|
+
*
|
|
116
|
+
* 只渲染一次,不按项目重复:功能说明与项目无关,只有命令名带项目前缀 —— 所以每条功能后面直接
|
|
117
|
+
* 列出各项目的命令名(`cmd_*` 变量),而不是把整份工具清单按前缀抄 N 遍。
|
|
45
118
|
*
|
|
46
|
-
* @param {string}
|
|
47
|
-
* @param {{
|
|
48
|
-
*
|
|
49
|
-
* omit for a stock IDE.
|
|
119
|
+
* @param {ReadonlyArray<{ path: string, server: string, app: string, label: string, isRoot: boolean }>} projects
|
|
120
|
+
* @param {{ tools?: Record<string, unknown> }} [options] `tools` 是本次请求的工具快照
|
|
121
|
+
* (`event.tools`),用来判断 run-lifecycle 工具在不在。
|
|
50
122
|
* @returns {string}
|
|
51
123
|
*/
|
|
52
|
-
export function
|
|
53
|
-
const present = options.runLifecycleTools ?? [];
|
|
124
|
+
export function buildIdeGuidanceText(projects, options = {}) {
|
|
54
125
|
return renderTemplate(TEMPLATE, {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
canRerunRunConfiguration: present.includes(RERUN_RUN_CONFIGURATION_TOOL),
|
|
126
|
+
projectCount: projects.length,
|
|
127
|
+
routeTable: routeTableFor(projects),
|
|
128
|
+
...mergeRunLifecycle(projects, options.tools),
|
|
129
|
+
...commandVariables(projects),
|
|
60
130
|
});
|
|
61
131
|
}
|
|
62
132
|
|
|
63
133
|
export const MCP_RECOVERY_GUIDANCE = [
|
|
64
|
-
'###
|
|
65
|
-
'If an
|
|
134
|
+
'### IDE MCP failure recovery',
|
|
135
|
+
'If an IDE MCP tool (the `<prefix>_*` ones) 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 IDE MCP is unavailable and ask them to run /open-in-idea to reconnect.',
|
|
66
136
|
].join('\n');
|
|
67
137
|
|
|
68
138
|
/**
|
|
@@ -71,12 +141,12 @@ export const MCP_RECOVERY_GUIDANCE = [
|
|
|
71
141
|
* single connection error is all it saw — the reconnect is already happening.
|
|
72
142
|
*/
|
|
73
143
|
export const MCP_RECOVERY_GUIDANCE_MARKED = [
|
|
74
|
-
'###
|
|
75
|
-
'This project is
|
|
144
|
+
'### IDE MCP failure recovery',
|
|
145
|
+
'This project is IDE-connected and the plugin reconnects the IDE MCP automatically. When an IDE MCP call fails with a connection error (for example `MCP server "ide" 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 IDE MCP is unavailable and to run /open-in-idea.',
|
|
76
146
|
].join('\n');
|
|
77
147
|
|
|
78
148
|
/**
|
|
79
|
-
* Recovery instruction for a marked project whose
|
|
149
|
+
* Recovery instruction for a marked project whose IDE tools are NOT in this
|
|
80
150
|
* request's tool list. The connection dropped and the plugin is already
|
|
81
151
|
* reconnecting, but the reconnect only lands on a LATER request — so within this
|
|
82
152
|
* one a call can only fail with `No tool named "..." is currently available`.
|
|
@@ -84,26 +154,26 @@ export const MCP_RECOVERY_GUIDANCE_MARKED = [
|
|
|
84
154
|
* exactly that doomed call.
|
|
85
155
|
*/
|
|
86
156
|
export const MCP_RECOVERY_GUIDANCE_RECONNECTING = [
|
|
87
|
-
'###
|
|
88
|
-
'No
|
|
157
|
+
'### IDE MCP failure recovery',
|
|
158
|
+
'No IDE MCP tool (the `<prefix>_*` ones) is in this request: the IDE connection dropped and the plugin is reconnecting in the background. Do NOT call any of them now — it will fail with `No tool named "..." is currently available` — and do not go looking for them through Code Mode either: they are only ever listed as native tools, so a search finds nothing. Do not fall back to shell/read/write for in-project work. Continue with what you can, or tell the user the IDE is reconnecting; if the tools are still missing on a later message, ask the user to run /open-in-idea.',
|
|
89
159
|
].join('\n');
|
|
90
160
|
|
|
91
161
|
/**
|
|
92
162
|
* Push the guidance onto a session `system` array. Called for every primary
|
|
93
163
|
* model request, so it stays idempotent per request.
|
|
94
164
|
*
|
|
95
|
-
* The text depends on which
|
|
96
|
-
* idempotence check compares the built text rather
|
|
165
|
+
* The text depends on which projects are connected and which run-lifecycle tools
|
|
166
|
+
* this request carries, so the idempotence check compares the built text rather
|
|
167
|
+
* than assuming one variant.
|
|
97
168
|
*
|
|
98
169
|
* @param {unknown} system
|
|
99
|
-
* @param {string}
|
|
100
|
-
* @param {{
|
|
101
|
-
* tools present in this request's tool snapshot.
|
|
170
|
+
* @param {ReadonlyArray<{ path: string, server: string, app: string, label: string, isRoot: boolean }>} projects
|
|
171
|
+
* @param {{ tools?: Record<string, unknown> }} [options]
|
|
102
172
|
*/
|
|
103
|
-
export function appendIdeGuidance(system,
|
|
173
|
+
export function appendIdeGuidance(system, projects, options = {}) {
|
|
104
174
|
if (!Array.isArray(system)) return;
|
|
105
|
-
if (!
|
|
106
|
-
const text =
|
|
175
|
+
if (!Array.isArray(projects) || projects.length === 0) return;
|
|
176
|
+
const text = buildIdeGuidanceText(projects, options);
|
|
107
177
|
if (system.some((part) => part?.text === text)) return;
|
|
108
178
|
system.push({ type: 'text', text });
|
|
109
179
|
}
|
|
@@ -112,7 +182,7 @@ export function appendIdeGuidance(system, projectPath, options = {}) {
|
|
|
112
182
|
* @param {unknown} system
|
|
113
183
|
* @param {{ marked?: boolean, serving?: boolean }} [options] `marked` picks the
|
|
114
184
|
* auto-reconnect variant; `serving: false` additionally picks the variant for
|
|
115
|
-
* a request that has no
|
|
185
|
+
* a request that has no IDE tools at all, where a call can only fail.
|
|
116
186
|
*/
|
|
117
187
|
export function appendIdeRecoveryGuidance(system, { marked = false, serving = true } = {}) {
|
|
118
188
|
if (!Array.isArray(system)) return;
|
|
@@ -1,71 +1,118 @@
|
|
|
1
|
-
#
|
|
1
|
+
# IDE MCP:只有非 IDE 不可的能力才走 IDE
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本会话接入了 {{projectCount}} 个 JetBrains 项目。**每个功能在所有项目下都各有一份工具**,命令名就是
|
|
4
|
+
`<项目前缀>_<工具名>`;下面每条功能后面直接列出它在各项目里的命令名。
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## 本会话接入的项目与各自的 MCP 工具
|
|
7
|
+
|
|
8
|
+
{{routeTable}}
|
|
9
|
+
|
|
10
|
+
**子项目就是根目录下的目录**,只是被另一个 IDE 单独打开、配了另一套工具。所以这里说的"根项目",指的是
|
|
11
|
+
**根目录中扣掉这些子项目之后的部分** —— 操作某个子项目里的文件,就用那个子项目的命令,不要因为它"在根目录下"
|
|
12
|
+
就用根项目的工具。
|
|
13
|
+
|
|
14
|
+
**规则:操作哪个目录下的文件/代码,就用那个目录对应的命令。** IDE 是按项目解析的 —— 跨项目调用要么直接报错,
|
|
15
|
+
要么被解析到另一个项目上、作用在错误的代码上。
|
|
16
|
+
|
|
17
|
+
按需读文件:要了解某个子项目自己的约定,读它的 `<子项目目录>/AGENTS.md`(它不会自动进入本会话的上下文,
|
|
18
|
+
本会话是 root 打开的那一个)。
|
|
19
|
+
|
|
20
|
+
## 总则(所有项目通用)
|
|
6
21
|
|
|
7
22
|
- 原生工具(当前工具列表里直接列出的那些)**直接调用**;不需要、也不应该用 Code Mode 的 `search` 去找。
|
|
8
|
-
-
|
|
9
|
-
- **只有 IDE
|
|
10
|
-
- **读写与搜索用原生工具**(`read` / `write` / `edit` / `grep` / `glob`):不要为读一个文件、写一个文件、搜一段文本去绕
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- 工具列表里看不到 `idea_*` 就是没接上:提示用户执行 `/open-in-idea`,不要重试,也不要去别处找。
|
|
23
|
+
- 下面这些 IDE 工具也是原生工具:直接调用(参数按各工具 schema 传),不要包进 `execute`;它们不在 Code Mode 的工具目录里,`search` 搜不到。
|
|
24
|
+
- **只有 IDE 才有的能力才用它们**:符号级检索、项目模型、检查与构建、重构与格式化、run configuration。
|
|
25
|
+
- **读写与搜索用原生工具**(`read` / `write` / `edit` / `grep` / `glob`):不要为读一个文件、写一个文件、搜一段文本去绕 IDE 工具。
|
|
26
|
+
- **唯一例外**:压缩包内的条目(路径带 `.jar!/`,原生 `read` 打不开)—— 见下面「读压缩包内的东西」。
|
|
27
|
+
- 某条功能下面没有你正要操作的那个项目的命令,就是那个项目没接上:提示用户执行 `/open-in-idea`,不要重试,也不要去别处找。
|
|
14
28
|
|
|
15
|
-
##
|
|
29
|
+
## 工具(每条第一行是各项目的命令,接着是它的用法)
|
|
16
30
|
|
|
17
31
|
### 符号(只有 IDE 的语义索引能给)
|
|
18
32
|
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
-
|
|
33
|
+
- {{cmd_search_symbol}}
|
|
34
|
+
**按名字找符号的声明位置**(`q` 传查询文本,**固定带上 `include_external: true`**):本插件最不可替代的能力 —— 它搜的是**符号**(类 / 接口 / 方法 / 函数 / 字段 / 类型 / 常量这类**有名字的定义**)的**声明位置**,不是"哪段文本提到过它"(那是 `grep` 的事);带上 `include_external` 才会连**依赖库和标准库**一起搜,于是你能直接定位第三方库里的实现 —— 这是原生 `grep` / `glob` 够不着的地方(IDE 自己的 `search_text` / `search_file` 也只搜项目内)。
|
|
35
|
+
- **典型用法 ①:代码里引用了某个库的 API(最常见)** —— 读代码时看到 `from fastapi import FastAPI`、`import org.apache.poi...FormulaParser`、`gorm.DeletedAt`、`from 'vue'` 这类引用时,**包名就在代码里**,直接**用「包名+符号名」当 `q` 搜**,一次定位到它的实现:
|
|
36
|
+
- **Java**:全限定名 —— `com.nimbusds.jose.util.Base64`(实测 1 条命中 `~/.m2/.../poi-5.3.0-sources.jar!/...`)
|
|
37
|
+
- **Go**:包名.符号 —— `gorm.DeletedAt`(实测 2 条,全在 `go/pkg/mod/gorm.io/gorm@...`)
|
|
38
|
+
- **Python**:模块路径给得越全越准 —— `torch.nn.modules.module.Module`(目标全在最前);`torch.nn.Module` 也能把目标排进前 2
|
|
39
|
+
- **Node/TS**:用 **import 的那层包名**(不是子模块路径)—— `vue.Teleport` 有效,`@vue/runtime-core.TeleportProps` 搜不到
|
|
40
|
+
- **典型用法 ②:调研一个库有哪些 API** —— 包名从依赖清单里拿(pom.xml / package.json / go.mod / requirements.txt),**直接拿包名当 `q` 搜**,结果基本就落在这个库里,再读入口:
|
|
41
|
+
- **Python**:`fastapi` → `fastapi/applications.py`、`fastapi/__init__.py`(**入口就是导出清单**:直接看到 `FastAPI`、`HTTPException` 这些公开名字)
|
|
42
|
+
- **Java**:`poi` → `~/.m2/.../poi-5.3.0-sources.jar!/org/apache/poi/...`;读该库的入口类
|
|
43
|
+
- **Go**:`gorm` → `go/pkg/mod/gorm.io/gorm@v1.31.0/...`
|
|
44
|
+
- **Node/TS 例外**:包名当 `q` 搜不到(实测 `element-plus` → 0 条)。要用**代码里 import 的符号名**;包名是简单标识符时可以「包名.符号」(`vue.Teleport` 有效),但**带 `-` 或 `@scope/` 的这么拼是 0 条**(`element-plus.ElMessage`、`@vue/runtime-core.TeleportProps` 都搜不到)。而裸符号名会跨多个子项目(`ElMessage` 命中 user / admin / vcop 三处),这时用 `paths` 圈到那个 `node_modules` —— `paths: ['frontend/user/**']` 就收敛到一处。
|
|
45
|
+
- **它只覆盖「项目已经引入」的依赖**:搜不到某个库,基本就是这个库还没进依赖 —— 别反复试:要么先把它加进依赖(pom / requirements / package.json),要么改用 web 搜索 / GitHub 看它的源码。
|
|
46
|
+
- **同名符号往往有多处定义**(标准库一份、几个依赖各一份,还有测试和示例),所以**按代码上下文选**:看调用处 import 的是哪个包、传参是什么类型、返回路径属于哪个库 —— 别把第一行当答案。
|
|
47
|
+
- **它返回的路径,原样交给同一个项目的 `<P>_read_file` 就是源码**:两者同属一个项目、相对路径的基准一致,所以不用拼绝对路径、不用管是什么语言 —— 压缩包里的条目它也照样读(`.class` 还会反编译)。
|
|
48
|
+
例:`<P>_search_symbol({ q: "UvicornWorker", include_external: true })` 给出 `../../../Library/.../uvicorn/workers.py` → `<P>_read_file({ file_path: "那个字符串" })`。
|
|
49
|
+
- **依赖索引是按项目的**:任何依赖都用**它所在项目**的那个 MCP 工具去搜 —— 根项目的依赖用根项目的命令,子项目的依赖用子项目的命令。拿一个项目的工具去搜另一个项目的依赖,只会在那个项目自己的文件里找,**拿不到库**。
|
|
50
|
+
- **别把这条路径交给原生 `read`**:原生 read 的基准是本会话的工作目录(根项目),拿子项目的相对路径去读会落到错误的位置。
|
|
51
|
+
- {{cmd_get_symbol_info}}
|
|
52
|
+
**一个符号的签名 / 文档 / 声明位置**:`filePath` + `line` + `column`。
|
|
53
|
+
- {{cmd_analyze_calls}}
|
|
54
|
+
**调用关系**:`symbolFqn` 传符号名,`analysisKind` 取 `INCOMING_CALLS` / `OUTGOING_CALLS`;`depth` 控层数,`childOffset` 翻页。
|
|
22
55
|
|
|
23
56
|
### 项目与编辑器
|
|
24
57
|
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
-
|
|
58
|
+
- {{cmd_list_directory_tree}}
|
|
59
|
+
**项目视角的目录树**(尊重内容根与排除项):`maxDepth` 控深度。
|
|
60
|
+
- {{cmd_get_project_modules}}
|
|
61
|
+
**项目模块及类型**。
|
|
62
|
+
- {{cmd_get_project_dependencies}}
|
|
63
|
+
**项目直接声明的依赖**。
|
|
64
|
+
- {{cmd_get_all_open_file_paths}}
|
|
65
|
+
**用户在 IDE 里打开了哪些文件**(含当前激活的那个)。
|
|
66
|
+
- {{cmd_open_file_in_editor}}
|
|
67
|
+
**把文件在 IDE 里打开并激活**,给用户看。
|
|
30
68
|
|
|
31
69
|
### 重构与格式化
|
|
32
70
|
|
|
33
|
-
-
|
|
34
|
-
|
|
71
|
+
- {{cmd_rename_refactoring}}
|
|
72
|
+
**按符号改名**,项目内的引用一并更新。
|
|
73
|
+
- {{cmd_reformat_file}}
|
|
74
|
+
**按 IDE 的代码风格重排**。
|
|
35
75
|
|
|
36
76
|
### 校验
|
|
37
77
|
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
-
|
|
78
|
+
- {{cmd_lint_files}}
|
|
79
|
+
**批量检查**:`files` 传数组;只列有问题的文件。
|
|
80
|
+
- {{cmd_get_file_problems}}
|
|
81
|
+
**单文件检查**:`filePath`;结果在 `errors`。
|
|
82
|
+
- {{cmd_build_project}}
|
|
83
|
+
**构建校验**:`filesToRebuild` 可只构建指定文件;有问题返回在 `problems`,没返回即没问题。
|
|
84
|
+
|
|
85
|
+
### 读压缩包内的东西
|
|
86
|
+
|
|
87
|
+
- {{cmd_read_file}}
|
|
88
|
+
**压缩包内的条目**(路径形如 `libs/foo.jar!/com/x/Bar.class` 或 `jar://` URL)只有它能读 —— 原生 `read` 打不开压缩包,而它还能把 `.class` **反编译成源码**;「符号」那条搜出来的路径也用它(原样传即可)。
|
|
41
89
|
|
|
42
90
|
### 运行配置
|
|
43
91
|
|
|
44
|
-
-
|
|
45
|
-
|
|
92
|
+
- {{cmd_get_run_configurations}}
|
|
93
|
+
**项目里有哪些运行配置**:传 `filePath` 可列该文件的运行入口。**不要翻 `.run/` 目录** —— 存在 `.idea/` 里的那些看不到。
|
|
94
|
+
- {{cmd_execute_run_configuration}}
|
|
95
|
+
**跑已有配置**:Shell Script 要 `EXECUTE_IN_TERMINAL: false`;覆盖参数(`programArguments` / `workingDirectory` / `envs`)需配置支持 `supportsDynamicLaunchOverrides`。
|
|
46
96
|
{{#if canCreateRunConfiguration}}
|
|
47
|
-
-
|
|
97
|
+
- {{cmd_create_shell_run_configuration}}
|
|
98
|
+
**新建项目级 Shell 配置**(`configurationName` + `scriptPath` / `scriptText` / `cloneFrom` 三选一;写 `.run/<name>.run.xml`):**重操作** —— 只给固定要跑、反复要用的流程建,**且建之前必须先取得用户同意**;只支持 Shell 类型,**其他类型请用户在 IDE 里建**。
|
|
48
99
|
{{/if}}
|
|
49
100
|
{{#if canListRunningConfigurations}}
|
|
50
|
-
-
|
|
101
|
+
- {{cmd_list_running_configurations}}
|
|
102
|
+
**当前真正在跑的配置**(几个实例 / 各自 pid / 退出码):停 / 重启前后都先用它看状态。
|
|
51
103
|
{{/if}}
|
|
52
104
|
{{#if canStopRunConfiguration}}
|
|
53
|
-
-
|
|
105
|
+
- {{cmd_stop_run_configuration}}
|
|
106
|
+
**停掉该配置的全部实例并等它们退出**:没在跑也返回成功。
|
|
54
107
|
{{/if}}
|
|
55
108
|
{{#if canRerunRunConfiguration}}
|
|
56
|
-
-
|
|
109
|
+
- {{cmd_rerun_run_configuration}}
|
|
110
|
+
**改完代码重启无热重载的进程**:在跑就重启、没在跑就启动;读 `started` 判断新进程是否真的起来了。不要用 `pkill` / `kill` / `shell` 代替。
|
|
57
111
|
{{/if}}
|
|
58
112
|
|
|
59
|
-
|
|
113
|
+
## 命令执行(所有项目通用)
|
|
60
114
|
|
|
61
|
-
- 需要项目环境的命令(依赖 IDE 里配好的 JDK / Node / Python 版本等)
|
|
62
|
-
- run configuration 是**重资产**:一个项目通常只建几个(固定要跑的构建 / 装依赖 / dev server 之类)
|
|
63
|
-
- 要新建:先确认它**反复要用、且属于项目固定流程**;确认后**先告诉用户要建什么、为什么,取得同意再动手**,别自己决定。
|
|
115
|
+
- 需要项目环境的命令(依赖 IDE 里配好的 JDK / Node / Python 版本等):用该项目前缀的 run configuration 工具,不要用原生 `shell`。
|
|
116
|
+
- run configuration 是**重资产**:一个项目通常只建几个(固定要跑的构建 / 装依赖 / dev server 之类),**不是随手一条命令就建一个**。要新建时先确认它**反复要用、且属于项目固定流程**,再**先告诉用户要建什么、为什么,取得同意才动手**,别自己决定。
|
|
64
117
|
- **配置一律不由你写**:不要手写 `.run/*.run.xml`,也不要手搓启动命令。
|
|
65
|
-
- **要查有哪些配置,用 `idea_get_run_configurations`,不要翻 `.run/` 目录**:工具给的是 IDE 当前认的配置;翻目录只能看到项目级文件,存在 `.idea/` 里的那些看不到。
|
|
66
|
-
{{#if canCreateRunConfiguration}}
|
|
67
|
-
- **只有 Shell 类型**是例外:用 `idea_create_shell_run_configuration`。**其他类型请用户在 IDEA 里建。**
|
|
68
|
-
{{else}}
|
|
69
|
-
- 缺配置就请用户在 IDEA 里建。
|
|
70
|
-
{{/if}}
|
|
71
118
|
- 不需要项目环境的命令:用原生 `shell`。
|
package/src/ide-launcher.js
CHANGED
|
@@ -12,50 +12,110 @@
|
|
|
12
12
|
// unavailable; this module only requests opening/activating the project.
|
|
13
13
|
|
|
14
14
|
import { spawn } from 'node:child_process';
|
|
15
|
+
import { readdirSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
15
18
|
|
|
16
|
-
/**
|
|
19
|
+
/** 兜底 macOS 应用名:调用方没给 app 时用它。 */
|
|
17
20
|
export const DEFAULT_IDE_APP = 'IntelliJ IDEA';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
|
-
*
|
|
23
|
+
* 每个 IDE 选项值对应的 macOS 应用名。
|
|
21
24
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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.
|
|
25
|
+
* 只包含受支持的 IDE(见 `ide-projects.js` 的 `SUPPORTED_IDES`)—— 应用名由项目
|
|
26
|
+
* 配置决定,不再由用户在 `openInIde` 里自由指定,所以这里是一张封闭的表。
|
|
31
27
|
*/
|
|
32
|
-
export function autoLaunchSupported() {
|
|
33
|
-
return process.platform === 'darwin';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Aliases accepted by the `openInIde` option. */
|
|
37
28
|
export const IDE_APPS = {
|
|
38
29
|
idea: 'IntelliJ IDEA',
|
|
39
|
-
|
|
40
|
-
'intellij-idea': 'IntelliJ IDEA',
|
|
30
|
+
pycharm: 'PyCharm',
|
|
41
31
|
};
|
|
42
32
|
|
|
43
33
|
/**
|
|
44
|
-
*
|
|
34
|
+
* 该 IDE 的 macOS 应用名,`undefined` 表示"不能自动拉起"。
|
|
45
35
|
*
|
|
46
|
-
* `undefined`
|
|
47
|
-
*
|
|
48
|
-
* no launcher. They deserve different user-facing wording.
|
|
36
|
+
* `undefined` 有两种原因,调用方用 `autoLaunchSupported()` 区分:这个平台没有
|
|
37
|
+
* 启动器,或这个 IDE 不在支持列表里。两者给用户的建议不同,不能混为一谈。
|
|
49
38
|
*
|
|
50
|
-
* @param {
|
|
39
|
+
* @param {string} app
|
|
51
40
|
* @param {{ platform?: string }} [options]
|
|
52
41
|
* @returns {string | undefined}
|
|
53
42
|
*/
|
|
54
|
-
export function
|
|
43
|
+
export function appNameFor(app, { platform = process.platform } = {}) {
|
|
55
44
|
if (platform !== 'darwin') return undefined;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
return IDE_APPS[app];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 各平台上看"装了哪些 IDE"的目录。
|
|
50
|
+
*
|
|
51
|
+
* macOS 那两个位置是**实测**的(这台机器上就是这么装的);Windows / Linux 列的是各自最
|
|
52
|
+
* 常见的安装位置(Toolbox 默认 + 系统级目录),**未在真机上验证过** —— 探错的后果只是少一
|
|
53
|
+
* 行提示、让模型照常问用户,不会挡住任何选择,所以先按常见值给出来。
|
|
54
|
+
*/
|
|
55
|
+
function ideInstallDirs(platform = process.platform) {
|
|
56
|
+
const home = homedir();
|
|
57
|
+
if (platform === 'darwin') return ['/Applications', join(home, 'Applications')];
|
|
58
|
+
if (platform === 'win32') {
|
|
59
|
+
const local = process.env.LOCALAPPDATA ?? join(home, 'AppData', 'Local');
|
|
60
|
+
return [
|
|
61
|
+
join(local, 'Programs', 'JetBrains'),
|
|
62
|
+
join(local, 'JetBrains', 'Toolbox', 'apps'),
|
|
63
|
+
'C:\\Program Files\\JetBrains',
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
return [join(home, '.local', 'share', 'JetBrains', 'Toolbox', 'apps'), '/opt', '/usr/local'];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 本机装了哪些受支持的 IDE(`IDE_APPS` 的键)。
|
|
71
|
+
*
|
|
72
|
+
* 按**安装目录**判断,不看进程 —— 用 `ps aux` 之类探进程只能发现"正在运行"的 IDE,装了
|
|
73
|
+
* 但没打开就会漏(实测踩过:PyCharm 明明装着,进程探测报"未发现")。一次目录读取就够,
|
|
74
|
+
* 比模型自己去跑命令快几个数量级。
|
|
75
|
+
*
|
|
76
|
+
* 返回空数组表示"**没探测出来**",不能当成"没装":调用方要么照常问用户,要么在提示里说明
|
|
77
|
+
* 是"没找到"。
|
|
78
|
+
*
|
|
79
|
+
* @param {{ platform?: string, dirs?: string[] }} [options] 仅供测试注入。
|
|
80
|
+
* @returns {string[]}
|
|
81
|
+
*/
|
|
82
|
+
export function detectInstalledIdes(options = {}) {
|
|
83
|
+
const platform = options.platform ?? process.platform;
|
|
84
|
+
const dirs = options.dirs ?? ideInstallDirs(platform);
|
|
85
|
+
/** @type {Set<string>} */
|
|
86
|
+
const found = new Set();
|
|
87
|
+
for (const dir of dirs) {
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = readdirSync(dir);
|
|
91
|
+
} catch {
|
|
92
|
+
// 目录不存在(例如没有 ~/Applications)就是没装,继续看下一个。
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const bundle = entry.endsWith('.app') ? entry.slice(0, -'.app'.length) : entry;
|
|
97
|
+
for (const [app, label] of Object.entries(IDE_APPS)) {
|
|
98
|
+
// `IntelliJ IDEA.app` 精确匹配;`PyCharm CE.app`、`PyCharm 2026.2`、Toolbox 的
|
|
99
|
+
// `PyCharm` 目录都是产品名开头,按前缀匹配。
|
|
100
|
+
if (bundle === label || bundle.startsWith(label)) found.add(app);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return [...found];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 这个平台有没有自动拉起 IDE 的能力?
|
|
109
|
+
*
|
|
110
|
+
* 只有 macOS 实现了启动(`open -a`)。Windows 上没有 `open` 命令,spawn 会以
|
|
111
|
+
* ENOENT 失败——表现为一个静默 resolve 成 `false` 的 promise。与其假装能启动,
|
|
112
|
+
* 不如让调用方直接告诉用户手动打开 IDE。
|
|
113
|
+
*
|
|
114
|
+
* 写成函数而不是常量:答案在命令真正执行时读取,测试才能用替换它的方式模拟
|
|
115
|
+
* 其它平台;常量会在 import 时定死,之后再也不重读。
|
|
116
|
+
*/
|
|
117
|
+
export function autoLaunchSupported() {
|
|
118
|
+
return process.platform === 'darwin';
|
|
59
119
|
}
|
|
60
120
|
|
|
61
121
|
/**
|