From d597d4c31014cf26223484acebf73744fbab22af Mon Sep 17 00:00:00 2001 From: pr-relay Date: Sun, 16 Aug 2026 17:32:24 +0000 Subject: [PATCH] Add Kimi Code compile/install target --- .../content/docs/reference/targets-matrix.md | 4 +- src/apm_cli/core/target_catalog.py | 7 ++++ src/apm_cli/core/target_detection.py | 17 ++++++++- src/apm_cli/integration/targets.py | 15 ++++++++ tests/unit/core/test_target_detection.py | 38 +++++++++++++++++++ tests/unit/integration/test_targets.py | 9 +++++ 6 files changed, 87 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/reference/targets-matrix.md b/docs/src/content/docs/reference/targets-matrix.md index 1d989ed776..1913947f8e 100644 --- a/docs/src/content/docs/reference/targets-matrix.md +++ b/docs/src/content/docs/reference/targets-matrix.md @@ -29,12 +29,13 @@ see [Primitive types](../primitive-types/). | opencode | `.opencode/` | [ ] | [ ] | [x] | [x] | [x] | [ ] | [x] | | windsurf | `.windsurf/` + `.agents/` | [x] | [ ] | [ ] | [x] | [x] | [x] | [x] | | kiro | `.kiro/` | [x] | [ ] | [x] | [x] | [ ] | [x] | [x] | +| kimi | `.kimi-code/` | [ ] | [ ] | [x] | [x] | [ ] | [ ] | [ ] | | intellij | user MCP config; files via Copilot | [x] (*) | [x] (*) | [x] (*) | [x] (*) | [ ] | [x] (*) | [x] | | agent-skills | `.agents/` | [ ] | [ ] | [ ] | [x] | [ ] | [ ] | [ ] | Skills deploy to `.agents/skills/` for Copilot, Cursor, OpenCode, Gemini, Antigravity, Codex, and Windsurf by default (see [Skills convergence](#skills-convergence) -below). Claude, Grok Build, and Kiro keep target-native skill directories. +below). Claude, Grok Build, Kiro, and Kimi keep target-native skill directories. (*) For `intellij`, file primitives route through the Copilot profile: instructions, prompts, agents, and hooks use `.github/`, while skills use @@ -88,6 +89,7 @@ runtime-specific configuration while compile only generates project output. Use | opencode | `.opencode/` directory | | windsurf | `.windsurf/` directory | | kiro | `.kiro/` directory | +| kimi | `.kimi-code/` directory | | intellij | Global `github-copilot/intellij/` config directory (MCP runtime discovery only) | IntelliJ-specific integration is MCP-only and writes JetBrains Copilot's diff --git a/src/apm_cli/core/target_catalog.py b/src/apm_cli/core/target_catalog.py index c9d21bdb48..5e5a5c55f8 100644 --- a/src/apm_cli/core/target_catalog.py +++ b/src/apm_cli/core/target_catalog.py @@ -133,6 +133,13 @@ def _build_target_catalog( primitive_profile="kiro", compile_family="agents", ), + _capability( + "kimi", + "Kimi Code native .kimi-code configuration", + in_all=True, + primitive_profile="kimi", + compile_family="agents", + ), _capability( "opencode", "OpenCode native .opencode configuration", diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index 6a6ddf85a9..751b35fd5c 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -72,6 +72,7 @@ def agents_alias_was_detected() -> bool: "antigravity", "windsurf", "kiro", + "kimi", "agent-skills", "all", "minimal", @@ -114,6 +115,7 @@ def agents_alias_was_detected() -> bool: "grok-build", "windsurf", "kiro", + "kimi", "agent-skills", "all", "minimal", @@ -160,6 +162,8 @@ def detect_target( # noqa: PLR0911 return "windsurf", "explicit --target flag" elif explicit_target == "kiro": return "kiro", "explicit --target flag" + elif explicit_target == "kimi": + return "kimi", "explicit --target flag" elif explicit_target == "grok-build": return "grok-build", "explicit --target flag" elif explicit_target == "agent-skills": @@ -187,6 +191,8 @@ def detect_target( # noqa: PLR0911 return "windsurf", "apm.yml target" elif config_target == "kiro": return "kiro", "apm.yml target" + elif config_target == "kimi": + return "kimi", "apm.yml target" elif config_target == "grok-build": return "grok-build", "apm.yml target" elif config_target == "agent-skills": @@ -203,6 +209,7 @@ def detect_target( # noqa: PLR0911 gemini_exists = (project_root / ".gemini").is_dir() windsurf_exists = (project_root / ".windsurf").is_dir() kiro_exists = (project_root / ".kiro").is_dir() + kimi_exists = (project_root / ".kimi-code").is_dir() grok_exists = (project_root / ".grok").is_dir() detected = [] if github_exists: @@ -221,6 +228,8 @@ def detect_target( # noqa: PLR0911 detected.append(".windsurf/") if kiro_exists: detected.append(".kiro/") + if kimi_exists: + detected.append(".kimi-code/") if grok_exists: detected.append(".grok/") @@ -242,6 +251,8 @@ def detect_target( # noqa: PLR0911 return "windsurf", "detected .windsurf/ folder" elif kiro_exists: return "kiro", "detected .kiro/ folder" + elif kimi_exists: + return "kimi", "detected .kimi-code/ folder" elif grok_exists: return "grok-build", "detected .grok/ folder" else: @@ -252,7 +263,7 @@ def should_compile_agents_md(target: CompileTargetType) -> bool: """Check if AGENTS.md should be compiled. AGENTS.md is generated for vscode, cursor, opencode, codex, gemini, - windsurf, kiro, antigravity, grok-build, hermes, all, and minimal targets. + windsurf, kiro, kimi, antigravity, grok-build, hermes, all, and minimal targets. Gemini needs it because GEMINI.md imports AGENTS.md. Args: @@ -274,6 +285,7 @@ def should_compile_agents_md(target: CompileTargetType) -> bool: "grok-build", "windsurf", "kiro", + "kimi", "hermes", "all", "minimal", @@ -412,10 +424,11 @@ def get_target_description(target: UserTargetType) -> str: "grok-build": "AGENTS.md + .grok/rules/ + .grok/agents/ + .grok/commands/ + .grok/skills/", "windsurf": "AGENTS.md + .windsurf/rules/ + .agents/skills/ + .windsurf/workflows/ + .windsurf/hooks.json", "kiro": "AGENTS.md + .kiro/steering/ + .kiro/skills/ + .kiro/hooks/ + .kiro/settings/mcp.json", + "kimi": "AGENTS.md + .kimi-code/agents/ + .kimi-code/skills/", "agent-skills": ".agents/skills/ only (cross-client shared skills -- no agents, hooks, or commands)", "openclaw": ".agents/skills/ (project) or ~/.openclaw/skills/ (--global) -- experimental", "hermes": "AGENTS.md + .agents/skills/ (project) or ~/.hermes/skills/ + config.yaml MCP (--global) -- experimental", - "all": "AGENTS.md + CLAUDE.md + GEMINI.md + .github/copilot-instructions.md + .github/ + .claude/ + .cursor/ + .opencode/ + .codex/ + .gemini/ + .windsurf/ + .kiro/ + .agents/", + "all": "AGENTS.md + CLAUDE.md + GEMINI.md + .github/copilot-instructions.md + .github/ + .claude/ + .cursor/ + .opencode/ + .codex/ + .gemini/ + .windsurf/ + .kiro/ + .kimi-code/ + .agents/", "minimal": "AGENTS.md only (create .github/, .claude/, or .gemini/ for full integration)", } return descriptions.get(normalized, "unknown target") diff --git a/src/apm_cli/integration/targets.py b/src/apm_cli/integration/targets.py index 05c4b20fbc..8fa365223d 100644 --- a/src/apm_cli/integration/targets.py +++ b/src/apm_cli/integration/targets.py @@ -619,6 +619,21 @@ def _encode_copilot_app_locator(path: Path) -> str: user_supported=True, user_root_dir=".kiro", ), + # Kimi Code -- project and user scope both use .kimi-code/. + # Root instructions compile to AGENTS.md; agents use Kimi-compatible + # Markdown/YAML and skills use the open Agent Skills layout. + "kimi": TargetProfile( + capability=TARGET_CAPABILITIES["kimi"], + root_dir=".kimi-code", + primitives={ + "agents": PrimitiveMapping("agents", ".md", "claude_agent"), + "skills": PrimitiveMapping("skills", "/SKILL.md", "skill_standard"), + }, + auto_create=False, + detect_by_dir=True, + user_supported=True, + user_root_dir=".kimi-code", + ), # OpenCode -- at user scope, ~/.config/opencode/ supports skills, agents, # and commands. OpenCode has no hooks concept, so "hooks" is excluded. "opencode": TargetProfile( diff --git a/tests/unit/core/test_target_detection.py b/tests/unit/core/test_target_detection.py index 7012102282..2b244bf7e7 100644 --- a/tests/unit/core/test_target_detection.py +++ b/tests/unit/core/test_target_detection.py @@ -105,6 +105,16 @@ def test_explicit_target_kiro_wins(self, tmp_path): assert target == "kiro" assert reason == "explicit --target flag" + def test_explicit_target_kimi_wins(self, tmp_path): + """Explicit --target kimi always wins.""" + target, reason = detect_target( + project_root=tmp_path, + explicit_target="kimi", + ) + + assert target == "kimi" + assert reason == "explicit --target flag" + def test_config_target_copilot(self, tmp_path): """Config target copilot maps to vscode.""" target, reason = detect_target( @@ -149,6 +159,17 @@ def test_config_target_claude(self, tmp_path): assert target == "claude" assert reason == "apm.yml target" + def test_config_target_kimi(self, tmp_path): + """Config target kimi is used when no explicit target.""" + target, reason = detect_target( + project_root=tmp_path, + explicit_target=None, + config_target="kimi", + ) + + assert target == "kimi" + assert reason == "apm.yml target" + def test_config_target_all(self, tmp_path): """Config target all is used when no explicit target.""" target, reason = detect_target( @@ -213,6 +234,19 @@ def test_auto_detect_kiro_only(self, tmp_path): assert target == "kiro" assert "detected .kiro/ folder" in reason + def test_auto_detect_kimi_only(self, tmp_path): + """Auto-detect kimi when only .kimi-code/ exists.""" + (tmp_path / ".kimi-code").mkdir() + + target, reason = detect_target( + project_root=tmp_path, + explicit_target=None, + config_target=None, + ) + + assert target == "kimi" + assert "detected .kimi-code/ folder" in reason + def test_auto_detect_neither_folder(self, tmp_path): """Auto-detect minimal when neither folder exists.""" target, reason = detect_target( @@ -252,6 +286,10 @@ def test_kiro_target(self): """AGENTS.md compiled for kiro as a cross-harness fallback.""" assert should_compile_agents_md("kiro") is True + def test_kimi_target(self): + """AGENTS.md compiled for kimi as its root instruction format.""" + assert should_compile_agents_md("kimi") is True + class TestShouldCompileClaudeMd: """Tests for should_compile_claude_md function.""" diff --git a/tests/unit/integration/test_targets.py b/tests/unit/integration/test_targets.py index ba7d17c921..06b1ecde57 100644 --- a/tests/unit/integration/test_targets.py +++ b/tests/unit/integration/test_targets.py @@ -79,6 +79,11 @@ def test_only_opencode_returns_opencode(self): targets = active_targets(self.root) assert [t.name for t in targets] == ["opencode"] + def test_only_kimi_returns_kimi(self): + (self.root / ".kimi-code").mkdir() + targets = active_targets(self.root) + assert [t.name for t in targets] == ["kimi"] + def test_github_and_claude_returns_both(self): (self.root / ".github").mkdir() (self.root / ".claude").mkdir() @@ -110,6 +115,10 @@ def test_explicit_claude(self): targets = active_targets(self.root, explicit_target="claude") assert [t.name for t in targets] == ["claude"] + def test_explicit_kimi(self): + targets = active_targets(self.root, explicit_target="kimi") + assert [t.name for t in targets] == ["kimi"] + def test_explicit_all_returns_every_known_target(self): from apm_cli.core.target_catalog import normalize_target_name from apm_cli.core.target_detection import ALL_CANONICAL_TARGETS