Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/src/content/docs/reference/targets-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/apm_cli/core/target_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 15 additions & 2 deletions src/apm_cli/core/target_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def agents_alias_was_detected() -> bool:
"antigravity",
"windsurf",
"kiro",
"kimi",
"agent-skills",
"all",
"minimal",
Expand Down Expand Up @@ -114,6 +115,7 @@ def agents_alias_was_detected() -> bool:
"grok-build",
"windsurf",
"kiro",
"kimi",
"agent-skills",
"all",
"minimal",
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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":
Expand All @@ -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:
Expand All @@ -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/")

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -274,6 +285,7 @@ def should_compile_agents_md(target: CompileTargetType) -> bool:
"grok-build",
"windsurf",
"kiro",
"kimi",
"hermes",
"all",
"minimal",
Expand Down Expand Up @@ -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")
Expand Down
15 changes: 15 additions & 0 deletions src/apm_cli/integration/targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
38 changes: 38 additions & 0 deletions tests/unit/core/test_target_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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."""
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/integration/test_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down