opencode-idea 0.0.16 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-idea",
3
- "version": "0.0.16",
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",
@@ -8,7 +8,12 @@
8
8
  "index.js",
9
9
  "src/index.js",
10
10
  "src/plugin.js",
11
+ "src/template.js",
12
+ "src/ide-guidance.template.md",
13
+ "src/ide-projects.js",
11
14
  "src/mcp/idea.js",
15
+ "src/mcp/enhance.js",
16
+ "src/mcp/idea-tool-defaults.js",
12
17
  "src/project.js",
13
18
  "src/ide-launcher.js",
14
19
  "src/ide-guidance.js",
@@ -35,6 +40,9 @@
35
40
  ],
36
41
  "author": "yutao",
37
42
  "license": "MIT",
43
+ "publishConfig": {
44
+ "registry": "https://registry.npmjs.org"
45
+ },
38
46
  "repository": {
39
47
  "type": "git",
40
48
  "url": "git+https://github.com/syllr/opencode-idea.git"
@@ -1,93 +1,43 @@
1
1
  ---
2
2
  name: idea-standalone
3
- description: "为项目生成/维护 standalone(经 IDEA 启动、本机进程直跑)模式的纪律 rule,写在项目根 `.opencode/rules/idea-standalone.md`。当用户要做 standalone 规范、要固化「本机跑哪几个应用」「哪些应用热重载、哪些改了必须重启」「AI 能不能启停本机进程」这类项目级纪律时使用。产出必须写清四段:standalone 是什么、standalone 下经 IDEA run configuration 启动哪些应用、逐应用的热重载与重启提示纪律、以及用户补充的其它条款。"
3
+ requires: rerun_run_configuration
4
+ description: "为项目生成/维护 standalone(启动收口只有 IDEA run configuration)模式的纪律,写进**项目根 `AGENTS.md`** 里由 `<!-- idea-standalone:begin -->` 与 `<!-- idea-standalone:end -->` 包裹的标记块。当用户要做 standalone 规范、要固化「本机跑哪几个应用」「哪些应用热重载、哪些改了必须重启」「启停走哪个工具」这类项目级纪律时使用。产物固定三段:standalone 是什么、经 IDEA run configuration 启动哪些应用、逐应用的热重载与重启提示纪律。"
4
5
  ---
5
6
 
6
- # Standalone 模式纪律 rule 生成器
7
-
8
- ## 产出物
9
-
10
- 一份 **项目级 rule**:`<项目根>/.opencode/rules/idea-standalone.md`,把这个项目在 **本机 standalone 模式**下怎么跑、AI
11
- 能做什么不能做什么固化下来。
12
-
13
- - 位置固定:`.opencode/rules/`(项目级规则目录),文件名 `idea-standalone.md`。
14
- - frontmatter 照抄项目里既有 rule 的格式 (`description` + `alwaysApply: true`)。
15
- - 只对这个项目生效; **不写**全局规则目录 (`~/.config/opencode/rules/`)。
16
-
17
- ## 四段结构 (缺一不可)
18
-
19
- ### 第一段:什么是 standalone 模式
20
-
21
- **standalone = 通过 IDEA 启动** —— 应用在本机进程直跑,不容器化、不部署到远端。给"本项目在本机的运行形态"下定义,至少覆盖:
22
-
23
- - **通过 IDEA 启动**:每个应用的启动入口就是 IDEA 的 run configuration(Run / Debug),不是手搓命令行。
24
- - **应用在本机进程直跑**:不容器化、不部署到远端。本机 **不跑**什么也要写明 (例如需要 GPU 的服务、或本机直连的远端数据库)。
25
- - **只启动其中一部分是常态** —— standalone 不要求所有应用都起来,用户可能只开一两个。
26
- - **启停一律由用户在 IDEA 手动完成**:AI 不得代为启停本机进程 —— 包括 `go run` / `pnpm dev` / `uvicorn` /
27
- `python -m ...` 这类起进程、`kill` / `pkill` 业务进程,以及经 `idea_execute_run_configuration` 启停。AI 若判断需要重启,
28
- **先向用户说明并取得明确授权**,绝不自行执行。
29
- - **与部署链纪律互不构成许可**:standalone 的禁令不适用于部署流程,部署链的步骤也不构成"可以动本机进程"的许可 —— 两阶段纪律互相独立。
30
-
31
- ### 第二段:standalone 下启动哪些应用 (必须经 IDEA run configuration)
32
-
33
- 表格,一行一个应用:
34
-
35
- | 应用 | run configuration | 目录 | 端口 | 说明 |
36
- |------|-------------------|------|------|------|
37
-
38
- 要点:
39
-
40
- - `run configuration` 列 **必须是 `idea_get_run_configurations` 里真实存在的名字**(不凭印象写);缺配置时先用
41
- `idea-run-config` skill 建项目级 `.run/*.run.xml`,不要手搓启动命令。
42
- - **启动路径只能是 IDEA run configuration**:不写 `pnpm dev &`、`nohup ... &` 这类后台命令,也不建议用户这么干。
43
- - 端口写清"固定 / 默认 / 被占用即启动失败"; **哪个应用必须先起来**也要写 (例如前端 dev server 的 proxy 指向本机后端)。
44
- - 端口读不到 (配置里没有、也没写在项目配置里)就 **问用户**,不要编。
45
-
46
- ### 第三段:热重载矩阵 + 改代码后要不要提醒重启
47
-
48
- 逐应用写清 **是 / 不是**热重载,以及对应的提醒纪律:
49
-
50
- | 应用 | 热重载 | AI 改代码后 |
51
- |-------------------------------------------------------|--------|-------------------------------------------------------|
52
- | 带 HMR 的 dev server(如 Vite) | 是 | 不提醒重启 |
53
- | 编译型 / 无热重载进程(如 `go run .`、`python -m ...`) | 否 | **必须**在回复里显式点名「需要重启 <应用>」并说明原因 |
54
- | 只能跑在远端、本机起不来的服务 | 不适用 | 本机无法验证,按部署链部署后再验证 |
55
-
56
- 铁律:
57
-
58
- - 非热重载的应用改动后, **不得**在没有提示的情况下宣称"改动完成 / 可以去验证了";重启动作由用户手动做。
59
- - 热重载列必须有依据 (运行方式 → 有没有 HMR),不确定就问用户,不猜。
60
- - `go run .` 这类 **没有热重载**的写法要写进 rule,别让 AI 以为改了代码就自动生效。
61
-
62
- ### 第四段起:项目补充条款
63
-
64
- 用户针对这个项目给的其它纪律, **按顺序续写在后面**(第四段、第五段……),例如:
65
-
66
- - 文件写入位置 (禁止写哪些目录、跑测/调试产物必须落在哪儿)
67
- - 测试纪律 (用例归属、什么时候才允许新增 UT、端到端测试在哪台机器上跑)
68
- - 访问归属 (数据库 / 第三方接口只经哪个工具,不旁路直连)
69
- - dev 模式特有限制 (会真实计费的操作、凭据存储方式等)
70
-
71
- 处理方式:用户给的条款 **逐条保留原意**、编号连续, **不合并不改写语义**。
72
-
73
- ## 流程
74
-
75
- 1. **收集事实 (不猜)**:
76
- - `idea_get_run_configurations` → 真实存在的配置名 (第二段"run configuration"列只能来自这里)。
77
- - `idea_search_file` 传 `q="*.run.xml"` + `paths: [".run"]` → 每个配置实际干什么 (命令 / 脚本 / npm script)。
78
- - `package.json` / `pom.xml` / `go.mod` / `pyproject.toml` / `Cargo.toml` → 项目类型 → 决定第三段的热重载列。
79
- - 端口:从 run configuration 的 option 或项目自身配置里读。
80
- 2. **补齐缺口**:哪些应用进 standalone、哪些配置名对不上、端口与热重载不确定 —— 逐条向用户确认,不编。
81
- 3. **写文件**:首次用 `idea_create_new_file` 写 `.opencode/rules/idea-standalone.md`;后续更新用 `idea_apply_patch`
82
- **定向改**,不整文件重写。
83
- 4. **校验**:第二段表格里每个配置名都能在 `idea_get_run_configurations` 里查到 —— **查不到就是错的**,不许用
84
- "应该生效了"结案。
85
-
86
- ## 铁律 (汇总)
87
-
88
- 1. 四段结构不能缺:standalone 定义 → 应用与 run configuration → 热重载与重启提醒 → 用户补充条款。
89
- 2. 第二段的启动方式 **只能是 IDEA run configuration**;不写手搓启动命令。
90
- 3. AI 不启停本机进程 (含 `idea_execute_run_configuration`);判断需要重启时先取得用户明确授权。
91
- 4. 配置名、端口、热重载三项都必须有依据;不确定就问用户。
92
- 5. 用户补充条款保留原意、编号连续,不合并不改写。
93
- 6. rule 只写项目级 `.opencode/rules/idea-standalone.md`,不写全局规则目录。
7
+ # idea-standalone — standalone 模式纪律
8
+
9
+ ## 定位与管辖文档
10
+
11
+ 本 skill 为项目固化它 **standalone 模式**(启动收口只有 IDEA run configuration)下怎么跑、AI 能做什么不能做什么。
12
+
13
+ - **产物位置固定:项目根 `AGENTS.md`**,作为其中一块,用 `<!-- idea-standalone:begin -->` 与 `<!-- idea-standalone:end -->` 包裹。
14
+ 项目根 `AGENTS.md` 是 AI 在本项目工作时必读的文件,所以写在这里天然"总是生效",不需要额外的规则文件与开关。
15
+ - **标记之内由本 skill 生成与维护;标记之外的项目内容一律不动。**
16
+ - **目标产物的骨架、各段内容约定与写作规范以 [assets/AGENTS.template.md](assets/AGENTS.template.md) 为准**,本 skill 不另行维护
17
+ 章节骨架或逐段规则;生成或更新前必须先读该文件,并严格沿用其中的标题层级、标记与生成提示。
18
+ - 只对这个项目生效; **不写**全局配置目录 (`~/.config/opencode/`),也不另建规则文件。
19
+
20
+ ## 读取
21
+
22
+ - **工具名前缀跟着项目走**:本项目的工具前缀由它接入时的名字决定 —— root 是 `ide`,子项目是它的目录名。下文的
23
+ `<前缀>_` 就是它(例如 `ide_get_run_configurations` 或 `modelserver_get_run_configurations`)。
24
+ - 读 `<项目根>/AGENTS.md`,确认是否已存在 `idea-standalone` 标记块(更新)或需要在文件末尾追加(新增)。
25
+ - **该项目的 `<前缀>_get_run_configurations` 直接调用,不传任何参数** → 项目里已有的 run configuration 就是**用户已经在 IDEA 里配好的那些**,
26
+ 这一次调用会把它们的名字连同启动细节(程序参数、工作目录、环境变量、是否支持启动时覆盖)一并给出。
27
+ 第二段的 `run configuration` 列、以及端口,**只能来自这里** —— 不需要再去逐个翻 `.run/*.run.xml`,那份列表已经是配置的结果。
28
+ - 项目类型(`package.json` / `pom.xml` / `go.mod` / `pyproject.toml` / `Cargo.toml`)→ 决定第三段的热重载列。
29
+ - 端口:先看上面那份启动细节里有没有;项目自身配置里也读不到就**问用户**,不要编。
30
+ - `run configuration` 列表里缺某个应用:**交给用户** —— 请他在 IDEA 里自己建一个,然后**重新查一次**。不要手搓启动命令,也不要自己写 `.run/*.run.xml`。
31
+ - 补齐缺口:哪些应用进 standalone、哪些配置名对不上、端口与热重载不确定 —— **逐条向用户确认**,不猜。
32
+ - 生成或更新前先读 [assets/AGENTS.template.md](assets/AGENTS.template.md),确认三段与各段生成提示。
33
+
34
+ ## 生成与更新
35
+
36
+ 1. 归属判断:属于三段骨架的内容写进 `idea-standalone` 标记块;不属于的(项目自己的其它纪律)留在标记外,本 skill 不写。
37
+ 2. 先读 [assets/AGENTS.template.md](assets/AGENTS.template.md),按其中的生成提示逐段填充。**`<!-- 生成提示:begin -->…<!-- 生成提示:end -->` 是给生成过程的说明,产物里整块删掉** —— 产物是给 AI 当规则读的,里面不该留指令。
38
+ 同时:**未涉及的表格行与小节整块删掉**,不留空标题、不留 `{...}`。
39
+ 3. **新增**(文件里还没有标记块):在 `AGENTS.md` **末尾追加**标记块,文件既有内容原样保留;文件不存在则创建它。
40
+ 只替换标记内内容、不动标记外 —— 尤其**不要触碰**其他 skill 的标记块(例如 `project-scaffold`)。
41
+ 4. **更新**(已有标记块):只替换 `<!-- idea-standalone:begin -->` 与 `<!-- idea-standalone:end -->` 之间的内容,块外一字不动。
42
+ 5. 用原生 `edit` 做定向替换;文件不存在时才用 `write` 创建。**不要整文件重写**。
43
+ 6. 复读并核对:标记块内逐段与模板一致,块外内容与改动前逐字一致,报告产物清单。
@@ -0,0 +1,66 @@
1
+ <!-- idea-standalone:begin -->
2
+
3
+ # Standalone 模式纪律
4
+
5
+ <!-- 生成提示:begin -->
6
+
7
+ 本块由 `idea-standalone` skill 生成与维护。标记内只写 standalone 模式的纪律;标记之外是项目自己在 `AGENTS.md` 里维护的内容,本 skill 一律不动。
8
+ `{...}` 是待替换的元变量,不留进产物。未涉及的表格行与小节整块删掉,不留空标题。
9
+ **`<!-- 生成提示:… -->` 是给生成过程的说明,产物里整块删掉** —— 产物是给 AI 当规则读的,里面不该留指令。
10
+ <!-- 生成提示:end -->
11
+
12
+ ## 一、什么是 standalone 模式
13
+
14
+ **standalone = 启动收口只有 IDEA run configuration** —— 每个应用的启动入口是 IDEA 的 run configuration(Run / Debug),不是手搓命令行;至于配置内部怎么起(**本机进程 / 容器 / 部署到远端**),由它自己决定,本段不预设。给"本项目怎么起、起在哪"下定义,至少覆盖:
15
+
16
+ - **入口唯一(唯一绝对的约束)**:每个应用的启动入口就是 IDEA 的 run configuration,**不是手搓命令行**。
17
+ - **形态不限**:配置可以起本机进程,也可以是容器(Compose / K8s)、或部署到远端 —— 怎么起是配置的事,**不要假设它一定在本机直跑**。相应地,哪些应用**不在本机跑、或本机起不来**要单独写明(需要 GPU 的服务、本机直连的远端数据库、只能部署后验证的服务),别当成"本机直跑"来验证。
18
+ - **只启动其中一部分是常态** —— 不要求所有应用都起来,用户可能只开一两个。
19
+ - **跑应用只用 `<前缀>_rerun_run_configuration`**:在跑就重启、没在跑就启动 —— 不必分辨"这是启动还是重启",直接 rerun 就完事。
20
+ 常驻进程化 / 服务化的 run configuration 一律走这一条。
21
+ - **不要用 `<前缀>_execute_run_configuration` 跑这些应用**:它是给**命令 / Job 类型**的配置跑的,standalone 的配置是**常驻型**(起来就不退),
22
+ 用它只会起出重复实例。
23
+ - **也不要绕过 IDEA**:不用 `go run` / `pnpm dev` / `uvicorn` / `python -m ...` 这类裸命令起进程,更不要 `kill` / `pkill` 业务进程 ——
24
+ 外部杀掉的进程 IDE 并不知道(Run 窗口仍显示运行中,下次启动就撞端口)。
25
+ - **重启不必先问**:改完代码要重启哪个应用,直接 `<前缀>_rerun_run_configuration` —— 这条链是无人化的,先征求同意只会把重启卡住。
26
+ - 工具列表里没有 `<前缀>_rerun_run_configuration` 时(没装配套插件):交回用户,请他在 IDEA 里手动跑,AI 不动手。
27
+ - **与部署链纪律互不构成许可**:本段管的是 **standalone 跑应用的方式**,不适用于部署流程;反过来,部署链里的步骤也不构成"可以绕开本段、用裸命令在本机起应用"的许可 —— 两阶段纪律互相独立。
28
+
29
+ ## 二、standalone 下启动哪些应用(必须经 IDEA run configuration)
30
+
31
+ <!-- 生成提示:begin -->
32
+
33
+ 一行一个应用,**按操作系统分成两张表**:macOS / Linux 一张,Windows 一张。同一份项目在不同平台上的启动方式往往不一样(解释器路径、脚本、
34
+ 甚至该用哪个配置都不同 —— 比如某个配置的 `INTERPRETER_PATH` 指向 Windows PowerShell,在 macOS 上根本起不来),
35
+ 所以**每张表里的 `run configuration` 必须是该平台上真正存在、真正能跑的那个名字**,不要把 Windows 上能用的配置写进 macOS 那张表。
36
+ 某个应用在某平台上起不来,就在该表里注明(或干脆不列),别让 AI 在一个平台上去跑另一个平台的配置。
37
+
38
+ `run configuration` 列**必须是 `<前缀>_get_run_configurations` 里真实存在的名字**,不凭印象写;缺配置就**交给用户**:
39
+ 请他在 IDEA 里自己建一个,不要手搓启动命令,也不要自己写 `.run/*.run.xml`。端口写清"固定 / 默认 / 被占用即启动失败",读不到就**问用户**,不要编。
40
+ **启动前流程**列:**多数应用留空**。只有确实"得先做点什么才能起"时才写,而且**按顺序写清第 1 步、第 2 步、第 3 步**(例如 `1. 装依赖 2. 构建 3. 起前置服务`);
41
+ 这些事**用文字写出来**(AI 照这段文本自己按序去做)。这类前置事项往往没被写成 run configuration,所以必须在这里说明,否则 AI 直接 rerun 会拿到一个起不来的进程。
42
+ **哪个应用必须先起来**也要写明(例如前端 dev server 的 proxy 指向本机后端)。与本项目无关的应用不写。
43
+ <!-- 生成提示:end -->
44
+
45
+ ### macOS / Linux
46
+
47
+ | 应用 | run configuration | 目录 | 端口 | 启动前流程 | 说明 |
48
+ | ---- | ----------------- | ---- | ---- | ---------- | ---- |
49
+
50
+ ### Windows
51
+
52
+ | 应用 | run configuration | 目录 | 端口 | 启动前流程 | 说明 |
53
+ | ---- | ----------------- | ---- | ---- | ---------- | ---- |
54
+
55
+ ## 三、热重载矩阵:改代码后要不要提醒重启
56
+
57
+ <!-- 生成提示:begin -->
58
+
59
+ 逐应用判定。热重载列必须有依据(运行方式 → 有没有 HMR),不确定就问用户,不猜。非热重载的应用改动后,**不得**在没有提示的情况下宣称
60
+ "改动完成 / 可以去验证了";重启动作由用户手动做。`go run .` 这类**没有热重载**的写法要写进来,别让 AI 以为改了代码就自动生效。
61
+ <!-- 生成提示:end -->
62
+
63
+ | 应用 | 热重载 | AI 改代码后 |
64
+ | ---- | ------ | ----------- |
65
+
66
+ <!-- idea-standalone:end -->
@@ -1,96 +1,138 @@
1
- // IDEA MCP routing guidance injected into the model's system prompt.
1
+ // IDE MCP routing guidance injected into the model's system prompt.
2
2
  //
3
- // Two rules for this file:
3
+ // The TEXT lives in `ide-guidance.template.md`, not here. It is a prompt document
4
+ // a human has to review as a document — with the conditionals visible as
5
+ // `{{#if …}}` around the lines they control — and building it by concatenating
6
+ // arrays made the result impossible to read without running the code. The
7
+ // template renders byte-for-byte to what the model receives; see `src/template.js`
8
+ // for why the renderer is 60 lines here rather than an engine dependency.
9
+ //
10
+ // Two rules for the document itself:
4
11
  // 1. Per tool: what it does, its parameters, and the positive flow for using
5
12
  // it. No caveats or edge cases, and nothing about languages or frameworks —
6
13
  // the project's own setup decides that.
7
- // 2. The boundary is the project path. Inside it -> idea_* tools.
8
- // Outside it -> whatever else is available.
14
+ // 2. Only IDE-only capability is routed here; reading, writing and searching
15
+ // stay on the native tools.
16
+ //
17
+ // ## 一个会话,多个项目,多套前缀
18
+ //
19
+ // 每个接入的项目都用**自己的** MCP server 注册(root = `ide`,子项目 = 目录名),
20
+ // 所以同一个 IDE 工具会以 `ide_search_symbol` / `modelserver_search_symbol` 这样多份
21
+ // 出现。但**功能是同一套,只有前缀不同** —— 所以模板只写一遍功能说明,每条功能后面用
22
+ // `{{cmd_xxx}}` 列出它在各项目下的命令名(由 `commandVariables` 扫模板自动生成)。
23
+ //
24
+ // 这样模型同时拿到"这条功能是什么"和"这个项目里它叫什么",不需要自己拼前缀 —— 少一层
25
+ // 推理,就少一类"拿错项目工具"的失败;而且文本不会随项目数成倍增长。
9
26
  //
10
- // The IDE MCP server is registered with `codemode: false`, so every idea_*
11
- // tool is exposed directly on the provider tool list with its full schema. This
12
- // text only says which tool to use for which operation, plus the parameters and
13
- // result shapes that are not obvious from the schema alone.
27
+ // The `{{#if can…}}` blocks gate the companion plugin's tools (`mcp/enhance.js`),
28
+ // which only exist when the user installed it. Nothing in the document says so:
29
+ // which plugin a tool came from is this plugin's plumbing, not the model's
30
+ // business — it either sees the tool or it does not.
31
+
32
+ import { readFileSync } from 'node:fs';
33
+
34
+ import { IDE_LABELS } from './ide-projects.js';
35
+ import { runLifecycleCapabilities } from './mcp/enhance.js';
36
+ import { renderTemplate } from './template.js';
37
+
38
+ /** The prompt document, next to this module so it ships with it. */
39
+ const TEMPLATE_PATH = new URL('ide-guidance.template.md', import.meta.url);
40
+
41
+ /** Read once: the file is fixed for the life of the process, and this runs per request. */
42
+ const TEMPLATE = readFileSync(TEMPLATE_PATH, 'utf8');
43
+
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
+ }
14
112
 
15
- /** Build the guidance text for one project path. */
16
- export function buildIdeGuidance(projectPath) {
17
- return [
18
- `# 项目内操作必须使用 IDEA MCP 工具`,
19
- ``,
20
- `项目路径: \`${projectPath}\``,
21
- ``,
22
- `## 总则`,
23
- `- 路径在 \`${projectPath}\` 内的代码与文件操作,必须使用 \`idea_*\` 工具。`,
24
- `- 路径在 \`${projectPath}\` 外:用其他可用工具。`,
25
- `- 直接调用工具,不要用 \`execute\` 包裹;参数按各工具 schema 传。`,
26
- `- \`idea_*\` 工具缺失、连接失败或超时:提示用户执行 \`/open-in-idea\`,不要重试。`,
27
- ``,
28
- `## 工具补充(每条只写补充说明;工具定义以自身 schema 为准)`,
29
- ``,
30
- `### 检索`,
31
- `- \`idea_search_text\`:不要用 \`grep\`。`,
32
- `- \`idea_search_regex\`:不要用 \`grep -E\`;结果带行列坐标。`,
33
- `- \`idea_search_file\`:不要用 \`glob\`。`,
34
- `- \`idea_search_symbol\`:按名字找符号的声明位置;要看签名 / 文档接着用 \`idea_get_symbol_info\`。`,
35
- ``,
36
- `### 阅读`,
37
- `- \`idea_read_file\`:不要用 \`read\`;支持 \`<jar>!/<entry>\` 与 \`jrt://\` 形式的包内读取。`,
38
- `- \`idea_list_directory_tree\`:不要用 \`ls\` / \`find\`;\`maxDepth\` 控深度。`,
39
- `- \`idea_get_symbol_info\`:一个符号的签名 / 文档 / 声明位置,\`filePath\` + \`line\` + \`column\`。`,
40
- `- \`idea_get_all_open_file_paths\`:用户在 IDE 里打开了哪些文件(含当前激活的那个)。`,
41
- `- \`idea_open_file_in_editor\`:把文件在 IDE 里打开并激活,给用户看。`,
42
- ``,
43
- `### 写入`,
44
- `- \`idea_apply_patch\`:不要用 \`edit\` / \`patch\`;只接受项目内路径。`,
45
- `- \`idea_create_new_file\`:不要用 \`write\`;\`overwrite\` 控制同名覆盖。`,
46
- `- \`idea_rename_refactoring\`:按符号改名,项目内的引用一并更新。`,
47
- `- \`idea_reformat_file\`:按 IDE 的代码风格重排。`,
48
- ``,
49
- `### 校验`,
50
- `- \`idea_lint_files\`:批量检查,\`files\` 传数组;只列有问题的文件。`,
51
- `- \`idea_get_file_problems\`:单文件检查,\`filePath\`;结果在 \`errors\`。`,
52
- `- \`idea_build_project\`:构建校验,\`filesToRebuild\` 可只构建指定文件;有问题返回在 \`problems\`,没返回即没问题。`,
53
- ``,
54
- `### 分析`,
55
- `- \`idea_analyze_calls\`:调用关系,\`symbolFqn\` 传符号名,\`analysisKind\` 取 \`INCOMING_CALLS\` / \`OUTGOING_CALLS\`;\`depth\` 控层数,\`childOffset\` 翻页。`,
56
- `- \`idea_get_project_modules\`:项目模块及类型。`,
57
- `- \`idea_get_project_dependencies\`:项目直接声明的依赖。`,
58
- ``,
59
- `### 运行配置`,
60
- `- \`idea_get_run_configurations\`:读项目级 \`.run/*.run.xml\`(增删改直接改文件);传 \`filePath\` 可列该文件的运行入口。`,
61
- `- \`idea_execute_run_configuration\`:跑已有配置;Shell Script 要 \`EXECUTE_IN_TERMINAL: false\`;覆盖参数(\`programArguments\` / \`workingDirectory\` / \`envs\`)需配置支持 \`supportsDynamicLaunchOverrides\`。`,
62
- ``,
63
- `### 终端`,
64
- `- 白名单制:命令用于本项目自身的构建 / 测试 / 运行,且首个可执行文件命中下表,才用 \`idea_execute_terminal_command\`;其余一律走原生 \`shell\`。`,
65
- `- 白名单(按本项目类型取对应一行) ——`,
66
- ` - Node / 前端(\`package.json\`):\`node\` / \`npm\` / \`npx\` / \`pnpm\` / \`yarn\` / \`vite\` / \`webpack\` / \`esbuild\` / \`tsc\` / \`jest\` / \`vitest\`。`,
67
- ` - Java(\`pom.xml\` / \`build.gradle\`):\`java\` / \`javac\` / \`mvn\` / \`mvnw\` / \`gradle\` / \`gradlew\`。`,
68
- ` - Python(\`pyproject.toml\` / \`requirements.txt\`):\`python\` / \`python3\` / \`pip\` / \`pip3\` / \`uv\` / \`poetry\` / \`pytest\`。`,
69
- ` - Go(\`go.mod\`):\`go\` / \`gofmt\` / \`golangci-lint\`。`,
70
- ` - \`executeInShell: true\`:在用户真实 shell 里执行,继承 IDE 配好的运行时环境;原生 shell 可能用错版本。`,
71
- ` - 按预期耗时传 \`timeout\`(毫秒),长任务给足;必要时传 \`maxLinesCount\` 控制返回行数。`,
72
- ` - 连续命令传 \`reuseExistingTerminalWindow: true\`;结果带 \`command_exit_code\` 与 \`command_output\`,\`cd\` 不跨调用保留,多步用 \`&&\`。`,
73
- ``,
74
- `### 数据库 (数据源由用户在 IDE 里配置:不要自己建/改,不要向用户索取数据库凭据)`,
75
- `- \`idea_list_database_connections\`:先看有没有现成数据源;每条给 \`id\` / \`dbms\` / \`jdbcUrl\` / \`readOnly\` / \`isDDL\`。`,
76
- `- \`idea_test_database_connection\`(\`id\`):\`hasProblems\` 为 \`NO\` 才算通,另带 DBMS / 驱动版本与 ping。`,
77
- `- \`idea_list_database_schemas\`(\`connectionId\`):MySQL 下 \`databaseName\` 为空、库名在 \`schemaName\`;带 \`isIntrospected\`。`,
78
- `- \`idea_list_schema_object_kinds\`(\`connectionId\`):先看有哪些类型可查(\`table\` / \`view\` / \`routine\` / \`scheduled-event\`)。`,
79
- `- \`idea_list_schema_objects\`(\`connectionId\` + \`databaseName\` + \`schemaName\` + \`kind\`):列该 schema 下的对象。`,
80
- `- \`idea_introspect_schema\`(\`connectionId\` + \`databaseName\` + \`schemaName\`):拉元数据,返回该 schema 的 \`isIntrospected\`。`,
81
- `- \`idea_get_database_object_description\`(\`kind\` + \`objectName\`):层级文本,含列类型 / 列注释 / 索引 / 主键。`,
82
- `- \`idea_execute_sql_query\`:跑 SQL;结果在 \`text\`(CSV)与 \`resultSetId\`,一次 10 行,后续用 \`fetch_query_result\` 翻页。`,
83
- `- \`idea_fetch_query_result\`(\`resultSetId\` + \`offset\`):按页继续取结果,10 行一页。`,
84
- `- \`idea_preview_table_data\`(\`tableName\` + \`maxRowCount\`):直接回 CSV。`,
85
- `- \`idea_list_recent_sql_queries\`(\`connectionId\`):最近(含正在跑)的查询,带 \`sessionId\` / \`state\` / \`status\`。`,
86
- `- \`idea_cancel_sql_query\`(\`sessionId\`):取消正在跑的查询,成功后该条 \`status\` 变 \`CANCELLED\`。`,
87
- `- 没有数据源时:若目标库在 IDEA Database Tools 支持范围内(PostgreSQL / MySQL / MariaDB / Oracle / SQL Server / SQLite / DB2 / H2 / Derby / Sybase / Vertica / Greenplum / ClickHouse / CockroachDB / Snowflake / BigQuery / Redshift / MongoDB / Redis / Cassandra / Couchbase / Hive / DynamoDB / Exasol 等,或任何提供 JDBC 驱动的库),先建议用户在 IDEA 里配好数据源,再回来用工具操作。`,
88
- ].join('\n');
113
+ /**
114
+ * 把本会话接入的全部项目渲染成**一份**引导文本。
115
+ *
116
+ * 只渲染一次,不按项目重复:功能说明与项目无关,只有命令名带项目前缀 —— 所以每条功能后面直接
117
+ * 列出各项目的命令名(`cmd_*` 变量),而不是把整份工具清单按前缀抄 N 遍。
118
+ *
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 工具在不在。
122
+ * @returns {string}
123
+ */
124
+ export function buildIdeGuidanceText(projects, options = {}) {
125
+ return renderTemplate(TEMPLATE, {
126
+ projectCount: projects.length,
127
+ routeTable: routeTableFor(projects),
128
+ ...mergeRunLifecycle(projects, options.tools),
129
+ ...commandVariables(projects),
130
+ });
89
131
  }
90
132
 
91
133
  export const MCP_RECOVERY_GUIDANCE = [
92
- '### IDEA MCP failure recovery',
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.',
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.',
94
136
  ].join('\n');
95
137
 
96
138
  /**
@@ -99,12 +141,12 @@ export const MCP_RECOVERY_GUIDANCE = [
99
141
  * single connection error is all it saw — the reconnect is already happening.
100
142
  */
101
143
  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.',
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.',
104
146
  ].join('\n');
105
147
 
106
148
  /**
107
- * Recovery instruction for a marked project whose idea_* tools are NOT in this
149
+ * Recovery instruction for a marked project whose IDE tools are NOT in this
108
150
  * request's tool list. The connection dropped and the plugin is already
109
151
  * reconnecting, but the reconnect only lands on a LATER request — so within this
110
152
  * one a call can only fail with `No tool named "..." is currently available`.
@@ -112,21 +154,26 @@ export const MCP_RECOVERY_GUIDANCE_MARKED = [
112
154
  * exactly that doomed call.
113
155
  */
114
156
  export const MCP_RECOVERY_GUIDANCE_RECONNECTING = [
115
- '### IDEA MCP failure recovery',
116
- 'No idea_* tool is in this request: the IDE connection dropped and the plugin is reconnecting in the background. Do NOT call any idea_* tool now — it will fail with `No tool named "..." is currently available` — and 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 idea_* tools are still missing on a later message, ask the user to run /open-in-idea.',
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.',
117
159
  ].join('\n');
118
160
 
119
161
  /**
120
162
  * Push the guidance onto a session `system` array. Called for every primary
121
163
  * model request, so it stays idempotent per request.
122
164
  *
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.
168
+ *
123
169
  * @param {unknown} system
124
- * @param {string} projectPath
170
+ * @param {ReadonlyArray<{ path: string, server: string, app: string, label: string, isRoot: boolean }>} projects
171
+ * @param {{ tools?: Record<string, unknown> }} [options]
125
172
  */
126
- export function appendIdeGuidance(system, projectPath) {
173
+ export function appendIdeGuidance(system, projects, options = {}) {
127
174
  if (!Array.isArray(system)) return;
128
- if (!projectPath) return;
129
- const text = buildIdeGuidance(projectPath);
175
+ if (!Array.isArray(projects) || projects.length === 0) return;
176
+ const text = buildIdeGuidanceText(projects, options);
130
177
  if (system.some((part) => part?.text === text)) return;
131
178
  system.push({ type: 'text', text });
132
179
  }
@@ -135,7 +182,7 @@ export function appendIdeGuidance(system, projectPath) {
135
182
  * @param {unknown} system
136
183
  * @param {{ marked?: boolean, serving?: boolean }} [options] `marked` picks the
137
184
  * auto-reconnect variant; `serving: false` additionally picks the variant for
138
- * a request that has no idea_* tools at all, where a call can only fail.
185
+ * a request that has no IDE tools at all, where a call can only fail.
139
186
  */
140
187
  export function appendIdeRecoveryGuidance(system, { marked = false, serving = true } = {}) {
141
188
  if (!Array.isArray(system)) return;