From 6225e87ed0a64a771d96132b5302e8697d0663d1 Mon Sep 17 00:00:00 2001 From: rztao Date: Sun, 12 Apr 2026 14:08:32 +0800 Subject: [PATCH] feat: add Qoder platform support Add Qoder as a supported platform alongside existing AI coding tools. This includes: - Platform config for Qoder (mcp.json path, hooks support) - install_qoder_skills() to copy skills to .qoder/skills/ - QODER.md instruction injection support - Qoder hooks installation (settings.json) - Updated platform list in docs and diagrams - New skills: debug-issue, explore-codebase, refactor-safely, review-changes The implementation follows the same pattern as existing platforms (claude, cursor, windsurf, etc.) for consistency. --- .gitignore | 4 +++ CLAUDE.md | 39 +++++++++++++++++++++ README.md | 4 +-- code_review_graph/cli.py | 31 +++++++++++------ code_review_graph/skills.py | 59 ++++++++++++++++++++++++++++++-- diagrams/generate_diagrams.py | 6 ++-- docs/USAGE.md | 5 ++- skills/debug-issue/SKILL.md | 27 +++++++++++++++ skills/explore-codebase/SKILL.md | 28 +++++++++++++++ skills/refactor-safely/SKILL.md | 28 +++++++++++++++ skills/review-changes/SKILL.md | 29 ++++++++++++++++ tests/test_skills.py | 57 ++++++++++++++++++++++++++++-- 12 files changed, 296 insertions(+), 21 deletions(-) create mode 100644 skills/debug-issue/SKILL.md create mode 100644 skills/explore-codebase/SKILL.md create mode 100644 skills/refactor-safely/SKILL.md create mode 100644 skills/review-changes/SKILL.md diff --git a/.gitignore b/.gitignore index 5036fd6..333995b 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,10 @@ code-review-graph-vscode/dist/ .claude/ .claude-plugin/ +# Qoder +.qoder/ +QODER.md + # Coverage htmlcov/ .coverage diff --git a/CLAUDE.md b/CLAUDE.md index 682cfea..a9adb7a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,3 +113,42 @@ uv run code-review-graph eval # Run evaluation benchmarks - **type-check**: mypy - **security**: bandit scan - **test**: pytest matrix (3.10, 3.11, 3.12, 3.13) with 50% coverage minimum + + +## MCP Tools: code-review-graph + +**IMPORTANT: This project has a knowledge graph. ALWAYS use the +code-review-graph MCP tools BEFORE using Grep/Glob/Read to explore +the codebase.** The graph is faster, cheaper (fewer tokens), and gives +you structural context (callers, dependents, test coverage) that file +scanning cannot. + +### When to use graph tools FIRST + +- **Exploring code**: `semantic_search_nodes` or `query_graph` instead of Grep +- **Understanding impact**: `get_impact_radius` instead of manually tracing imports +- **Code review**: `detect_changes` + `get_review_context` instead of reading entire files +- **Finding relationships**: `query_graph` with callers_of/callees_of/imports_of/tests_for +- **Architecture questions**: `get_architecture_overview` + `list_communities` + +Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need. + +### Key Tools + +| Tool | Use when | +|------|----------| +| `detect_changes` | Reviewing code changes — gives risk-scored analysis | +| `get_review_context` | Need source snippets for review — token-efficient | +| `get_impact_radius` | Understanding blast radius of a change | +| `get_affected_flows` | Finding which execution paths are impacted | +| `query_graph` | Tracing callers, callees, imports, tests, dependencies | +| `semantic_search_nodes` | Finding functions/classes by name or keyword | +| `get_architecture_overview` | Understanding high-level codebase structure | +| `refactor_tool` | Planning renames, finding dead code | + +### Workflow + +1. The graph auto-updates on file changes (via hooks). +2. Use `detect_changes` for code review. +3. Use `get_affected_flows` to understand impact. +4. Use `query_graph` pattern="tests_for" to check coverage. diff --git a/README.md b/README.md index 31760d9..80e33cc 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ code-review-graph build # parse your codebase One command sets up everything. `install` detects which AI coding tools you have, writes the correct MCP configuration for each one, and injects graph-aware instructions into your platform rules. It auto-detects whether you installed via `uvx` or `pip`/`pipx` and generates the right config. Restart your editor/tool after installing.

- One Install, Every Platform: auto-detects Codex, Claude Code, Cursor, Windsurf, Zed, Continue, OpenCode, and Antigravity + One Install, Every Platform: auto-detects Codex, Claude Code, Cursor, Windsurf, Zed, Continue, OpenCode, Antigravity, Qwen, and Qoder

To target a specific platform: @@ -331,5 +331,5 @@ MIT. See [LICENSE](LICENSE).
code-review-graph.com

pip install code-review-graph && code-review-graph install
-Works with Codex, Claude Code, Cursor, Windsurf, Zed, Continue, OpenCode, and Antigravity +Works with Codex, Claude Code, Cursor, Windsurf, Zed, Continue, OpenCode, Antigravity, Qwen, and Qoder

diff --git a/code_review_graph/cli.py b/code_review_graph/cli.py index 4598861..4a730e0 100644 --- a/code_review_graph/cli.py +++ b/code_review_graph/cli.py @@ -38,6 +38,12 @@ logger = logging.getLogger(__name__) +# Shared platform choices for install and init commands +_PLATFORM_CHOICES = [ + "codex", "claude", "claude-code", "cursor", "windsurf", "zed", + "continue", "opencode", "antigravity", "qwen", "qoder", "all", +] + def _get_version() -> str: """Get the installed package version.""" @@ -211,6 +217,7 @@ def _handle_init(args: argparse.Namespace) -> None: inject_platform_instructions, install_git_hook, install_hooks, + install_qoder_skills, ) if not skip_skills: @@ -237,9 +244,17 @@ def _handle_init(args: argparse.Namespace) -> None: elif skip_instructions: print("Skipped instruction injection (--no-instructions).") - if not skip_hooks and target in ("claude", "all"): - install_hooks(repo_root) - print(f"Installed hooks in {repo_root / '.claude' / 'settings.json'}") + + # Install Qoder skills (global user-level skills directory) + if not skip_skills and target in ("qoder", "all"): + qoder_skills_dir = install_qoder_skills(repo_root) + if qoder_skills_dir: + print(f"Installed Qoder skills to {qoder_skills_dir}") + if not skip_hooks and target in ("claude", "qoder", "all"): + platforms_to_install = [target] if target != "all" else ["claude", "qoder"] + for plat in platforms_to_install: + install_hooks(repo_root, platform=plat) + print(f"Installed hooks in {repo_root / f'.{plat}' / 'settings.json'}") git_hook = install_git_hook(repo_root) if git_hook: print(f"Installed git pre-commit hook in {git_hook}") @@ -293,10 +308,7 @@ def main() -> None: help=argparse.SUPPRESS) install_cmd.add_argument( "--platform", - choices=[ - "codex", "claude", "claude-code", "cursor", "windsurf", "zed", - "continue", "opencode", "antigravity", "qwen", "all", - ], + choices=_PLATFORM_CHOICES, default="all", help="Target platform for MCP config (default: all detected)", ) @@ -331,10 +343,7 @@ def main() -> None: help=argparse.SUPPRESS) init_cmd.add_argument( "--platform", - choices=[ - "codex", "claude", "claude-code", "cursor", "windsurf", "zed", - "continue", "opencode", "antigravity", "qwen", "all", - ], + choices=_PLATFORM_CHOICES, default="all", help="Target platform for MCP config (default: all detected)", ) diff --git a/code_review_graph/skills.py b/code_review_graph/skills.py index fe3abab..72fcb46 100644 --- a/code_review_graph/skills.py +++ b/code_review_graph/skills.py @@ -100,6 +100,14 @@ def _zed_settings_path() -> Path: "format": "object", "needs_type": True, }, + "qoder": { + "name": "Qoder", + "config_path": lambda root: root / ".qoder" / "mcp.json", + "key": "mcpServers", + "detect": lambda: True, + "format": "object", + "needs_type": True, + }, } @@ -481,16 +489,20 @@ def install_git_hook(repo_root: Path) -> Path | None: return hook_path -def install_hooks(repo_root: Path) -> None: - """Write hooks config to .claude/settings.json. +def install_hooks(repo_root: Path, platform: str = "claude") -> None: + """Write hooks config to platform-specific settings.json. Merges with existing settings if present, preserving non-hook configuration. Args: repo_root: Repository root directory. + platform: Target platform ("claude" or "qoder"). """ - settings_dir = repo_root / ".claude" + if platform == "qoder": + settings_dir = repo_root / ".qoder" + else: + settings_dir = repo_root / ".claude" settings_dir.mkdir(parents=True, exist_ok=True) settings_path = settings_dir / "settings.json" @@ -591,6 +603,7 @@ def inject_claude_md(repo_root: Path) -> None: "GEMINI.md": ("antigravity",), ".cursorrules": ("cursor",), ".windsurfrules": ("windsurf",), + "QODER.md": ("qoder",), } @@ -615,3 +628,43 @@ def inject_platform_instructions(repo_root: Path, target: str = "all") -> list[s if _inject_instructions(path, _CLAUDE_MD_SECTION_MARKER, _CLAUDE_MD_SECTION): updated.append(filename) return updated + + +def install_qoder_skills(repo_root: Path) -> Path | None: + """Install skills to Qoder's project-level skills directory. + + Qoder expects skills in .qoder/skills/{skillName}/SKILL.md format within the project. + This function copies the project's skills/ directory contents to that location. + + Args: + repo_root: Repository root directory (where the skills/ folder is located). + + Returns: + Path to the Qoder skills directory, or None if installation failed. + """ + # Qoder skills directory (project-level) + qoder_skills_dir = repo_root / ".qoder" / "skills" + qoder_skills_dir.mkdir(parents=True, exist_ok=True) + + # Source skills directory in the project + source_skills_dir = repo_root / "skills" + if not source_skills_dir.exists(): + logger.warning("No skills/ directory found in %s", repo_root) + return None + + installed_count = 0 + for skill_dir in source_skills_dir.iterdir(): + if skill_dir.is_dir(): + skill_file = skill_dir / "SKILL.md" + if skill_file.exists(): + target_dir = qoder_skills_dir / skill_dir.name + target_dir.mkdir(parents=True, exist_ok=True) + target_file = target_dir / "SKILL.md" + target_file.write_text(skill_file.read_text(encoding="utf-8"), encoding="utf-8") + logger.info("Installed Qoder skill: %s", skill_dir.name) + installed_count += 1 + + if installed_count > 0: + logger.info("Installed %d skill(s) to %s", installed_count, qoder_skills_dir) + return qoder_skills_dir + return None diff --git a/diagrams/generate_diagrams.py b/diagrams/generate_diagrams.py index 5d577d3..baf9fa7 100644 --- a/diagrams/generate_diagrams.py +++ b/diagrams/generate_diagrams.py @@ -595,6 +595,8 @@ def d8(): ("Continue", "~/.continue/config.json", YLW, YLW_BG), ("OpenCode", ".opencode.json", RED, RED_BG), ("Antigravity", "~/.gemini/antigravity/mcp_config.json",GRY, GRY_BG), + ("Qwen Code", "~/.qwen/settings.json", BLU, PRP_BG), # blue-purple + ("Qoder", ".qoder/mcp.json", GRN, ORG_BG), # green-orange ] # Central "install" node @@ -604,8 +606,8 @@ def d8(): # Fan out to platforms cols = len(platforms) - card_w, card_h = 140, 80 - total_w = cols * card_w + (cols-1) * 20 + card_w, card_h = 120, 80 # narrower cards for 9 platforms + total_w = cols * card_w + (cols-1) * 15 # tighter spacing x0 = center_x - total_w/2 card_y = 360 diff --git a/docs/USAGE.md b/docs/USAGE.md index b521f08..e2fa865 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -30,7 +30,10 @@ code-review-graph install --platform claude-code | **Windsurf** | `.windsurf/mcp.json` | | **Zed** | `.zed/settings.json` | | **Continue** | `.continue/config.json` | -| **OpenCode** | `.opencode/config.json` | +| **OpenCode** | `.opencode.json` | +| **Antigravity** | `~/.gemini/antigravity/mcp_config.json` | +| **Qwen Code** | `~/.qwen/settings.json` | +| **Qoder** | `.qoder/mcp.json` | ## Core Workflow diff --git a/skills/debug-issue/SKILL.md b/skills/debug-issue/SKILL.md new file mode 100644 index 0000000..ef1b38a --- /dev/null +++ b/skills/debug-issue/SKILL.md @@ -0,0 +1,27 @@ +--- +name: Debug Issue +description: Systematically debug issues using graph-powered code navigation +--- + +## Debug Issue + +Use the knowledge graph to systematically trace and debug issues. + +### Steps + +1. Use `semantic_search_nodes` to find code related to the issue. +2. Use `query_graph` with `callers_of` and `callees_of` to trace call chains. +3. Use `get_flow` to see full execution paths through suspected areas. +4. Run `detect_changes` to check if recent changes caused the issue. +5. Use `get_impact_radius` on suspected files to see what else is affected. + +### Tips + +- Check both callers and callees to understand the full context. +- Look at affected flows to find the entry point that triggers the bug. +- Recent changes are the most common source of new issues. + +## Token Efficiency Rules +- ALWAYS start with `get_minimal_context(task="")` before any other graph tool. +- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient. +- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens. diff --git a/skills/explore-codebase/SKILL.md b/skills/explore-codebase/SKILL.md new file mode 100644 index 0000000..dc7ad10 --- /dev/null +++ b/skills/explore-codebase/SKILL.md @@ -0,0 +1,28 @@ +--- +name: Explore Codebase +description: Navigate and understand codebase structure using the knowledge graph +--- + +## Explore Codebase + +Use the code-review-graph MCP tools to explore and understand the codebase. + +### Steps + +1. Run `list_graph_stats` to see overall codebase metrics. +2. Run `get_architecture_overview` for high-level community structure. +3. Use `list_communities` to find major modules, then `get_community` for details. +4. Use `semantic_search_nodes` to find specific functions or classes. +5. Use `query_graph` with patterns like `callers_of`, `callees_of`, `imports_of` to trace relationships. +6. Use `list_flows` and `get_flow` to understand execution paths. + +### Tips + +- Start broad (stats, architecture) then narrow down to specific areas. +- Use `children_of` on a file to see all its functions and classes. +- Use `find_large_functions` to identify complex code. + +## Token Efficiency Rules +- ALWAYS start with `get_minimal_context(task="")` before any other graph tool. +- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient. +- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens. diff --git a/skills/refactor-safely/SKILL.md b/skills/refactor-safely/SKILL.md new file mode 100644 index 0000000..cf84420 --- /dev/null +++ b/skills/refactor-safely/SKILL.md @@ -0,0 +1,28 @@ +--- +name: Refactor Safely +description: Plan and execute safe refactoring using dependency analysis +--- + +## Refactor Safely + +Use the knowledge graph to plan and execute refactoring with confidence. + +### Steps + +1. Use `refactor_tool` with mode="suggest" for community-driven refactoring suggestions. +2. Use `refactor_tool` with mode="dead_code" to find unreferenced code. +3. For renames, use `refactor_tool` with mode="rename" to preview all affected locations. +4. Use `apply_refactor_tool` with the refactor_id to apply renames. +5. After changes, run `detect_changes` to verify the refactoring impact. + +### Safety Checks + +- Always preview before applying (rename mode gives you an edit list). +- Check `get_impact_radius` before major refactors. +- Use `get_affected_flows` to ensure no critical paths are broken. +- Run `find_large_functions` to identify decomposition targets. + +## Token Efficiency Rules +- ALWAYS start with `get_minimal_context(task="")` before any other graph tool. +- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient. +- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens. diff --git a/skills/review-changes/SKILL.md b/skills/review-changes/SKILL.md new file mode 100644 index 0000000..6bb3558 --- /dev/null +++ b/skills/review-changes/SKILL.md @@ -0,0 +1,29 @@ +--- +name: Review Changes +description: Perform a structured code review using change detection and impact +--- + +## Review Changes + +Perform a thorough, risk-aware code review using the knowledge graph. + +### Steps + +1. Run `detect_changes` to get risk-scored change analysis. +2. Run `get_affected_flows` to find impacted execution paths. +3. For each high-risk function, run `query_graph` with pattern="tests_for" to check test coverage. +4. Run `get_impact_radius` to understand the blast radius. +5. For any untested changes, suggest specific test cases. + +### Output Format + +Provide findings grouped by risk level (high/medium/low) with: +- What changed and why it matters +- Test coverage status +- Suggested improvements +- Overall merge recommendation + +## Token Efficiency Rules +- ALWAYS start with `get_minimal_context(task="")` before any other graph tool. +- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient. +- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens. diff --git a/tests/test_skills.py b/tests/test_skills.py index 15c5eeb..a922dc9 100644 --- a/tests/test_skills.py +++ b/tests/test_skills.py @@ -181,6 +181,27 @@ def test_creates_claude_directory(self, tmp_path): install_hooks(tmp_path) assert (tmp_path / ".claude").is_dir() + def test_install_qoder_hooks(self, tmp_path): + install_hooks(tmp_path, platform="qoder") + settings_path = tmp_path / ".qoder" / "settings.json" + assert settings_path.exists() + data = json.loads(settings_path.read_text()) + assert "hooks" in data + assert "PostToolUse" in data["hooks"] + assert "SessionStart" in data["hooks"] + + def test_install_qoder_hooks_merges_existing(self, tmp_path): + settings_dir = tmp_path / ".qoder" + settings_dir.mkdir(parents=True) + existing = {"customSetting": True} + (settings_dir / "settings.json").write_text(json.dumps(existing)) + + install_hooks(tmp_path, platform="qoder") + + data = json.loads((settings_dir / "settings.json").read_text()) + assert data["customSetting"] is True + assert "hooks" in data + class TestInjectClaudeMd: def test_creates_section_in_new_file(self, tmp_path): @@ -228,11 +249,11 @@ def test_idempotent_with_existing_content(self, tmp_path): class TestInjectPlatformInstructionsFiltering: def test_all_writes_every_file(self, tmp_path): updated = inject_platform_instructions(tmp_path, target="all") - assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules"} + assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules", "QODER.md"} def test_default_is_all(self, tmp_path): updated = inject_platform_instructions(tmp_path) - assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules"} + assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules", "QODER.md"} def test_claude_writes_nothing(self, tmp_path): updated = inject_platform_instructions(tmp_path, target="claude") @@ -241,12 +262,14 @@ def test_claude_writes_nothing(self, tmp_path): assert not (tmp_path / "GEMINI.md").exists() assert not (tmp_path / ".cursorrules").exists() assert not (tmp_path / ".windsurfrules").exists() + assert not (tmp_path / "QODER.md").exists() def test_cursor_writes_only_cursor_files(self, tmp_path): updated = inject_platform_instructions(tmp_path, target="cursor") assert set(updated) == {"AGENTS.md", ".cursorrules"} assert not (tmp_path / "GEMINI.md").exists() assert not (tmp_path / ".windsurfrules").exists() + assert not (tmp_path / "QODER.md").exists() def test_windsurf_writes_only_windsurfrules(self, tmp_path): updated = inject_platform_instructions(tmp_path, target="windsurf") @@ -260,6 +283,14 @@ def test_opencode_writes_only_agents(self, tmp_path): updated = inject_platform_instructions(tmp_path, target="opencode") assert updated == ["AGENTS.md"] + def test_qoder_writes_only_qoder_md(self, tmp_path): + updated = inject_platform_instructions(tmp_path, target="qoder") + assert updated == ["QODER.md"] + assert not (tmp_path / "AGENTS.md").exists() + assert not (tmp_path / "GEMINI.md").exists() + assert not (tmp_path / ".cursorrules").exists() + assert not (tmp_path / ".windsurfrules").exists() + class TestInstallPlatformConfigs: def test_install_codex_config(self, tmp_path): @@ -534,3 +565,25 @@ def test_continue_array_no_duplicate(self, tmp_path): install_platform_configs(tmp_path, target="continue") data = json.loads(config_path.read_text()) assert len(data["mcpServers"]) == 1 + + def test_install_qoder_config(self, tmp_path): + qoder_config = tmp_path / ".qoder" / "mcp.json" + with patch.dict( + PLATFORMS, + { + "qoder": { + **PLATFORMS["qoder"], + "config_path": lambda root: qoder_config, + "detect": lambda: True, + }, + }, + ): + configured = install_platform_configs(tmp_path, target="qoder") + assert "Qoder" in configured + data = json.loads(qoder_config.read_text()) + assert "mcpServers" in data + assert "code-review-graph" in data["mcpServers"] + assert data["mcpServers"]["code-review-graph"]["type"] == "stdio" + import shutil + expected_cmd = "uvx" if shutil.which("uvx") else "code-review-graph" + assert data["mcpServers"]["code-review-graph"]["command"] == expected_cmd