From da10967b946e109bc13ea863832d23992a90f49c Mon Sep 17 00:00:00 2001 From: hsliu_ustc Date: Sat, 15 Aug 2026 16:36:28 +0800 Subject: [PATCH] =?UTF-8?q?Installer:=20first-class=20ZCode=20target=20?= =?UTF-8?q?=E2=80=94=20merge=20mcp.servers=20into=20~/.zcode/cli/config.js?= =?UTF-8?q?on,=20skills=20in=20~/.zcode/skills?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QUICKSTART.md | 2 +- README.md | 6 +-- doc/MCP.md | 6 +-- scripts/install_mcp.py | 61 +++++++++++++++++++++++++++++- test/test_install_mcp_script.py | 66 +++++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 9 deletions(-) diff --git a/QUICKSTART.md b/QUICKSTART.md index 0044698a..75558da7 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -175,7 +175,7 @@ Repo knowledge lives at the edge in `adapters//` (human-gated `manifest.yaml` + agent-established, evidence-gated `profile/`) — never in `src/`. `PROFILE_BRIEFING_ENABLED=0` runs the {no-profile} eval ablation arm. -## 10. Advanced: MCP server (Claude Code / Codex / Cursor) +## 10. Advanced: MCP server (Claude Code / Codex / Cursor / ZCode) ```bash pip install -e '.[mcp]' # optional extra, kept out of the base install diff --git a/README.md b/README.md index 42b48225..fc70a553 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ install.cmd --repo-path D:\path\to\vllm-omni ./install-mcp.sh --repo-path /path/to/vllm-omni ``` -安装器会识别本机的 Codex、Claude Code 和 Cursor,并安装 MCP、`imreview`、 +安装器会识别本机的 Codex、Claude Code、Cursor 和 ZCode,并安装 MCP、`imreview`、 `imdesign`、`imcifix` 和 `imupdate` Skill。未识别到已知 Agent 时,会生成标准 `infermatrix-copilot.mcp.json` 供其他 MCP 客户端导入。 Codex 重启后用 `$imreview` / `$imdesign` / `$imcifix` / `$imupdate`,也可以先运行 `/skills` 查找; -`/imreview` 不是 Codex 的 Slash Command。Claude Code 和 Cursor 仍使用 +`/imreview` 不是 Codex 的 Slash Command。Claude Code、Cursor 和 ZCode 仍使用 `/imreview` / `/imdesign` / `/imcifix` / `/imupdate`。 安装器还会创建 `~/.infermatrix-copilot/.env`。Direct 不需要模型密钥;使用 @@ -84,7 +84,7 @@ Use InferMatrixCopilot in Direct mode to review this PR. $imreview https://github.com/vllm-project/vllm-omni/pull/5172 $imreview -# Claude Code / Cursor +# Claude Code / Cursor / ZCode /imreview https://github.com/vllm-project/vllm-omni/pull/5172 或者说 “帮我审核一下这个pr xxxx,用知识库” diff --git a/doc/MCP.md b/doc/MCP.md index c48b1de2..894b8379 100644 --- a/doc/MCP.md +++ b/doc/MCP.md @@ -24,8 +24,8 @@ install.cmd --repo-path D:\path\to\vllm-omni 两个入口共用 `scripts/install_mcp.py`,但用户不需要安装或直接运行 Python: 入口会自动安装 `uv`,再由 `uv` 提供隔离运行环境。安装器会: -- 自动识别 Codex、Claude Code 和 Cursor,检测到几个就安装几个; -- 保留 Cursor 已有配置并先备份; +- 自动识别 Codex、Claude Code、Cursor 和 ZCode,检测到几个就安装几个; +- 保留 Cursor / ZCode 已有配置并先备份; - 没识别到已知 Agent 时生成 `infermatrix-copilot.mcp.json`,不会直接失败。 - 创建 `~/.infermatrix-copilot/.env` 作为 Strict 的稳定配置入口;安装时传入 `--repo-path` 会写入本地 vLLM-Omni checkout。模型密钥不会自动复制,使用 @@ -42,7 +42,7 @@ install.cmd --repo-path D:\path\to\vllm-omni $imreview $imupdate -# Claude Code / Cursor +# Claude Code / Cursor / ZCode /imreview /imupdate ``` diff --git a/scripts/install_mcp.py b/scripts/install_mcp.py index 19485e28..660eb50d 100644 --- a/scripts/install_mcp.py +++ b/scripts/install_mcp.py @@ -292,6 +292,60 @@ def _install_cursor(config_root: Path) -> None: _install_skills(cursor_root / "skills") +def _install_zcode(config_root: Path) -> None: + zcode_root = config_root / ".zcode" + config_path = zcode_root / "cli" / "config.json" + config_path.parent.mkdir(parents=True, exist_ok=True) + + if config_path.exists(): + try: + config = json.loads(config_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise InstallError( + f"ZCode config is invalid and was not changed: {config_path}" + ) from exc + if not isinstance(config, dict): + raise InstallError( + f"ZCode config must be an object: {config_path}" + ) + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + shutil.copy2( + config_path, + config_path.with_name(f"{config_path.name}.{timestamp}.bak"), + ) + else: + config = {} + + # ZCode nests servers under mcp.servers and also keeps hooks and plugin + # state in this file, so merge instead of rewriting the document. + mcp = config.setdefault("mcp", {}) + if not isinstance(mcp, dict): + raise InstallError( + f"ZCode config mcp section must be an object: {config_path}" + ) + servers = mcp.setdefault("servers", {}) + if not isinstance(servers, dict): + raise InstallError( + f"ZCode config mcp.servers must be an object: {config_path}" + ) + servers[SERVER_NAME] = { + "type": "stdio", + "command": SERVER_COMMAND[0], + "args": SERVER_COMMAND[1:], + } + config_path.write_text( + json.dumps(config, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + _install_skills(zcode_root / "skills") + + +def _zcode_installed(config_root: Path) -> bool: + if shutil.which("zcode"): + return True + return (config_root / ".zcode").exists() + + def _cursor_installed(config_root: Path) -> bool: if shutil.which("cursor") or shutil.which("cursor-agent"): return True @@ -311,6 +365,8 @@ def _detect_agents(config_root: Path) -> list[str]: agents.append("claude") if _cursor_installed(config_root): agents.append("cursor") + if _zcode_installed(config_root): + agents.append("zcode") return agents @@ -339,7 +395,7 @@ def _parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace: parser.add_argument( "--agent", action="append", - choices=("codex", "claude", "cursor"), + choices=("codex", "claude", "cursor", "zcode"), help="Override automatic Agent detection. May be repeated.", ) parser.add_argument( @@ -392,6 +448,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: "codex": _install_codex, "claude": _install_claude, "cursor": _install_cursor, + "zcode": _install_zcode, } for agent in agents: try: @@ -415,7 +472,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: ) if any(agent != "codex" for agent in agents): print( - "Claude/Cursor: restart, then run: /imreview , " + "Claude/Cursor/ZCode: restart, then run: /imreview , " "/imcifix , or /imupdate " ) return 0 diff --git a/test/test_install_mcp_script.py b/test/test_install_mcp_script.py index f63a0e0d..1a7cc03f 100644 --- a/test/test_install_mcp_script.py +++ b/test/test_install_mcp_script.py @@ -2,6 +2,8 @@ import json from pathlib import Path +import pytest + ROOT = Path(__file__).resolve().parents[1] INSTALLER = ROOT / "scripts" / "install_mcp.py" @@ -103,6 +105,70 @@ def test_cursor_install_preserves_existing_config(tmp_path): assert str(ROOT) in text +def test_zcode_install_merges_nested_mcp_servers(tmp_path): + installer = _load_installer() + config_path = tmp_path / ".zcode" / "cli" / "config.json" + config_path.parent.mkdir(parents=True) + config_path.write_text( + json.dumps( + { + "plugins": {"keep": True}, + "mcp": {"servers": {"existing": {"command": "existing"}}}, + } + ), + encoding="utf-8", + ) + + installer._install_zcode(tmp_path) + + config = json.loads(config_path.read_text(encoding="utf-8")) + assert config["plugins"] == {"keep": True} + assert config["mcp"]["servers"]["existing"]["command"] == "existing" + server = config["mcp"]["servers"]["infermatrix-copilot"] + assert server["type"] == "stdio" + assert server["command"] == "uvx" + assert "infermatrix-copilot-mcp" in server["args"] + assert list(config_path.parent.glob("config.json.*.bak")) + assert (tmp_path / ".zcode" / "skills" / "imreview" / "SKILL.md").is_file() + update_skill = tmp_path / ".zcode" / "skills" / "imupdate" / "SKILL.md" + assert "{{INFERMATRIX_COPILOT_ROOT}}" not in update_skill.read_text( + encoding="utf-8" + ) + + +def test_zcode_install_creates_config_when_missing(tmp_path): + installer = _load_installer() + + installer._install_zcode(tmp_path) + + config_path = tmp_path / ".zcode" / "cli" / "config.json" + config = json.loads(config_path.read_text(encoding="utf-8")) + assert config["mcp"]["servers"]["infermatrix-copilot"]["command"] == "uvx" + assert (tmp_path / ".zcode" / "skills" / "imdesign" / "SKILL.md").is_file() + + +def test_zcode_install_rejects_invalid_config(tmp_path): + installer = _load_installer() + config_path = tmp_path / ".zcode" / "cli" / "config.json" + config_path.parent.mkdir(parents=True) + config_path.write_text("{ not json", encoding="utf-8") + + with pytest.raises(installer.InstallError): + installer._install_zcode(tmp_path) + + assert config_path.read_text(encoding="utf-8") == "{ not json" + + +def test_zcode_detected_via_config_dir(monkeypatch, tmp_path): + installer = _load_installer() + monkeypatch.setattr(installer.shutil, "which", lambda command: None) + + assert not installer._zcode_installed(tmp_path) + (tmp_path / ".zcode").mkdir() + assert installer._zcode_installed(tmp_path) + assert "zcode" in installer._detect_agents(tmp_path) + + def test_codex_install_sets_timeout_and_installs_current_skill_location( monkeypatch, tmp_path ):