mirror of
https://github.com/cita-777/metapi.git
synced 2026-11-01 20:55:09 +08:00
958 lines
33 KiB
Python
Executable File
958 lines
33 KiB
Python
Executable File
"""
|
||
多平台 CLI 适配器。
|
||
|
||
抽象 Claude Code、OpenCode、Cursor、iFlow、Codex、Kilo、Kiro Code、Gemini CLI、
|
||
Antigravity、Devin、Qoder、CodeBuddy、GitHub Copilot、Factory Droid 和 Pi Agent
|
||
接口之间的差异。
|
||
|
||
支持的平台:
|
||
- claude:Claude Code(默认)
|
||
- opencode:OpenCode
|
||
- cursor:Cursor IDE
|
||
- iflow:iFlow CLI
|
||
- codex:Codex CLI(基于 skills)
|
||
- kilo:Kilo CLI
|
||
- kiro:Kiro Code(基于 skills)
|
||
- gemini:Gemini CLI
|
||
- antigravity:Antigravity(基于 workflow)
|
||
- devin:Devin(原名 Windsurf;基于 workflow)
|
||
- qoder:Qoder
|
||
- codebuddy:CodeBuddy
|
||
- copilot:GitHub Copilot(VS Code)
|
||
- droid:Factory Droid(基于 commands)
|
||
- pi:Pi Agent(基于 extension)
|
||
- trae:Trae IDE(仅 IDE、基于 hooks)
|
||
- omp:Oh My Pi
|
||
- grok:Grok Build(基于 pull 的 skills/agents;不注入 hook context)
|
||
- kimi:Kimi Code(基于 pull 的 skills;commands 作为 skills 提供;不注入 hook context)
|
||
|
||
用法:
|
||
from common.cli_adapter import CLIAdapter
|
||
|
||
adapter = CLIAdapter("opencode")
|
||
cmd = adapter.build_run_command(
|
||
agent="dispatch",
|
||
session_id="abc123",
|
||
prompt="开始流水线"
|
||
)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
from typing import ClassVar, Literal
|
||
|
||
Platform = Literal[
|
||
"claude",
|
||
"opencode",
|
||
"cursor",
|
||
"iflow",
|
||
"codex",
|
||
"kilo",
|
||
"kiro",
|
||
"gemini",
|
||
"antigravity",
|
||
"devin",
|
||
"qoder",
|
||
"codebuddy",
|
||
"copilot",
|
||
"droid",
|
||
"pi",
|
||
"trae",
|
||
"omp",
|
||
"grok",
|
||
"kimi",
|
||
]
|
||
|
||
|
||
@dataclass
|
||
class CLIAdapter:
|
||
"""适配不同的 AI 编程 CLI 工具。"""
|
||
|
||
platform: Platform
|
||
|
||
# =========================================================================
|
||
# Agent 名称映射
|
||
# =========================================================================
|
||
|
||
# OpenCode 有不能覆盖的内置 agent。
|
||
# 参见:https://github.com/sst/opencode/issues/4271
|
||
# 注意:这是类级常量,不是 dataclass 字段。
|
||
_AGENT_NAME_MAP: ClassVar[dict[Platform, dict[str, str]]] = {
|
||
"claude": {}, # 不需要映射
|
||
"opencode": {
|
||
"plan": "trellis-plan", # OpenCode 内置了 'plan'
|
||
},
|
||
}
|
||
|
||
def get_agent_name(self, agent: str) -> str:
|
||
"""获取平台专用的 agent 名称。
|
||
|
||
参数:
|
||
agent:原始 agent 名称(例如 'plan'、'dispatch')
|
||
|
||
返回:
|
||
平台专用的 agent 名称(例如 OpenCode 使用 'trellis-plan')
|
||
"""
|
||
mapping = self._AGENT_NAME_MAP.get(self.platform, {})
|
||
return mapping.get(agent, agent)
|
||
|
||
# =========================================================================
|
||
# Agent 路径
|
||
# =========================================================================
|
||
|
||
@property
|
||
def config_dir_name(self) -> str:
|
||
"""获取平台专用的配置目录名称。
|
||
|
||
返回:
|
||
目录名称('.claude'、'.opencode'、'.cursor'、'.iflow'、'.codex'、
|
||
'.kilocode'、'.kiro'、'.gemini'、'.agent'、'.devin'、'.qoder'、
|
||
'.codebuddy'、'.github/copilot'、'.factory'、'.pi' 或 '.trae')
|
||
"""
|
||
if self.platform == "opencode":
|
||
return ".opencode"
|
||
elif self.platform == "cursor":
|
||
return ".cursor"
|
||
elif self.platform == "iflow":
|
||
return ".iflow"
|
||
elif self.platform == "codex":
|
||
return ".codex"
|
||
elif self.platform == "kilo":
|
||
return ".kilocode"
|
||
elif self.platform == "kiro":
|
||
return ".kiro"
|
||
elif self.platform == "gemini":
|
||
return ".gemini"
|
||
elif self.platform == "antigravity":
|
||
return ".agent"
|
||
elif self.platform == "devin":
|
||
return ".devin"
|
||
elif self.platform == "qoder":
|
||
return ".qoder"
|
||
elif self.platform == "codebuddy":
|
||
return ".codebuddy"
|
||
elif self.platform == "copilot":
|
||
return ".github/copilot"
|
||
elif self.platform == "droid":
|
||
return ".factory"
|
||
elif self.platform == "pi":
|
||
return ".pi"
|
||
elif self.platform == "trae":
|
||
return ".trae"
|
||
elif self.platform == "omp":
|
||
return ".omp"
|
||
elif self.platform == "grok":
|
||
return ".grok"
|
||
elif self.platform == "kimi":
|
||
return ".kimi-code"
|
||
else:
|
||
return ".claude"
|
||
|
||
def get_config_dir(self, project_root: Path) -> Path:
|
||
"""获取平台专用的配置目录。
|
||
|
||
参数:
|
||
project_root:项目根目录
|
||
|
||
返回:
|
||
配置目录路径(.claude、.opencode、.cursor、.iflow、.codex、.kilocode、
|
||
.kiro、.gemini、.agent、.devin、.qoder、.codebuddy、.github/copilot、
|
||
.factory、.pi 或 .trae)
|
||
"""
|
||
return project_root / self.config_dir_name
|
||
|
||
def get_agent_path(self, agent: str, project_root: Path) -> Path:
|
||
"""获取 agent 定义文件路径。
|
||
|
||
参数:
|
||
agent:agent 名称(映射前的原始名称)
|
||
project_root:项目根目录
|
||
|
||
返回:
|
||
agent 定义文件路径(大多数平台使用 .md,Codex 使用 .toml)
|
||
"""
|
||
mapped_name = self.get_agent_name(agent)
|
||
if self.platform == "codex":
|
||
return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.toml"
|
||
return self.get_config_dir(project_root) / "agents" / f"{mapped_name}.md"
|
||
|
||
def get_commands_path(self, project_root: Path, *parts: str) -> Path:
|
||
"""获取 commands 目录或具体 command 文件的路径。
|
||
|
||
参数:
|
||
project_root:项目根目录
|
||
*parts:额外路径片段(例如 'trellis'、'finish-work.md')
|
||
|
||
返回:
|
||
commands 目录或文件路径
|
||
|
||
说明:
|
||
Cursor 使用前缀命名:.cursor/commands/trellis-<name>.md
|
||
Antigravity 使用 workflow 目录:.agent/workflows/<name>.md
|
||
Devin 使用 workflow 目录:.devin/workflows/trellis-<name>.md
|
||
Copilot 使用 prompt 文件:.github/prompts/<name>.prompt.md
|
||
Pi 使用 prompt 模板:.pi/prompts/trellis-<name>.md
|
||
Claude/OpenCode 使用子目录:.claude/commands/trellis/<name>.md
|
||
"""
|
||
if self.platform == "pi":
|
||
prompts_dir = self.get_config_dir(project_root) / "prompts"
|
||
if not parts:
|
||
return prompts_dir
|
||
if len(parts) >= 2 and parts[0] == "trellis":
|
||
filename = parts[-1]
|
||
if filename.endswith(".md"):
|
||
filename = filename[:-3]
|
||
return prompts_dir / f"trellis-{filename}.md"
|
||
return prompts_dir / Path(*parts)
|
||
# OMP 和 Grok:扁平的 slash command 路径为 .{platform}/commands/trellis-<name>.md
|
||
if self.platform in ("omp", "grok"):
|
||
commands_dir = self.get_config_dir(project_root) / "commands"
|
||
if not parts:
|
||
return commands_dir
|
||
if len(parts) >= 2 and parts[0] == "trellis":
|
||
filename = parts[-1]
|
||
if filename.endswith(".md"):
|
||
filename = filename[:-3]
|
||
return commands_dir / f"trellis-{filename}.md"
|
||
return commands_dir / Path(*parts)
|
||
|
||
# Kimi:commands 作为 skills,路径为 .kimi-code/skills/trellis-<name>/SKILL.md
|
||
if self.platform == "kimi":
|
||
skills_dir = self.get_config_dir(project_root) / "skills"
|
||
if not parts:
|
||
return skills_dir
|
||
if len(parts) >= 2 and parts[0] == "trellis":
|
||
filename = parts[-1]
|
||
if filename.endswith(".md"):
|
||
filename = filename[:-3]
|
||
return skills_dir / f"trellis-{filename}" / "SKILL.md"
|
||
return skills_dir / Path(*parts)
|
||
|
||
if self.platform == "devin":
|
||
workflow_dir = self.get_config_dir(project_root) / "workflows"
|
||
if not parts:
|
||
return workflow_dir
|
||
if len(parts) >= 2 and parts[0] == "trellis":
|
||
filename = parts[-1]
|
||
return workflow_dir / f"trellis-{filename}"
|
||
return workflow_dir / Path(*parts)
|
||
|
||
if self.platform in ("antigravity", "kilo"):
|
||
workflow_dir = self.get_config_dir(project_root) / "workflows"
|
||
if not parts:
|
||
return workflow_dir
|
||
if len(parts) >= 2 and parts[0] == "trellis":
|
||
filename = parts[-1]
|
||
return workflow_dir / filename
|
||
return workflow_dir / Path(*parts)
|
||
|
||
if self.platform == "copilot":
|
||
prompts_dir = project_root / ".github" / "prompts"
|
||
if not parts:
|
||
return prompts_dir
|
||
if len(parts) >= 2 and parts[0] == "trellis":
|
||
filename = parts[-1]
|
||
if filename.endswith(".md"):
|
||
filename = filename[:-3]
|
||
return prompts_dir / f"{filename}.prompt.md"
|
||
return prompts_dir / Path(*parts)
|
||
|
||
if not parts:
|
||
return self.get_config_dir(project_root) / "commands"
|
||
|
||
# Cursor 使用前缀命名,而不是子目录
|
||
if self.platform == "cursor" and len(parts) >= 2 and parts[0] == "trellis":
|
||
# 将 trellis/<name>.md 转换为 trellis-<name>.md
|
||
filename = parts[-1]
|
||
return (
|
||
self.get_config_dir(project_root) / "commands" / f"trellis-{filename}"
|
||
)
|
||
|
||
return self.get_config_dir(project_root) / "commands" / Path(*parts)
|
||
|
||
def get_trellis_command_path(self, name: str) -> str:
|
||
"""获取 Trellis command 文件的相对路径。
|
||
|
||
参数:
|
||
name:不带扩展名的 command 名称(例如 'finish-work'、'check')
|
||
|
||
返回:
|
||
用于 JSONL 条目的相对路径字符串
|
||
|
||
说明:
|
||
Cursor:.cursor/commands/trellis-<name>.md
|
||
Codex:.agents/skills/trellis-<name>/SKILL.md
|
||
Kiro:.kiro/skills/trellis-<name>/SKILL.md
|
||
Gemini:.gemini/commands/trellis/<name>.toml
|
||
Antigravity:.agent/workflows/<name>.md
|
||
Devin:.devin/workflows/trellis-<name>.md
|
||
Pi:.pi/prompts/trellis-<name>.md
|
||
其他平台:.{platform}/commands/trellis/<name>.md
|
||
"""
|
||
if self.platform == "cursor":
|
||
return f".cursor/commands/trellis-{name}.md"
|
||
elif self.platform == "codex":
|
||
# 0.5.0-beta.0 将所有 skill 目录重命名为带 `trellis-` 前缀
|
||
# (60 多条重命名记录见该版本的 manifest)。
|
||
return f".agents/skills/trellis-{name}/SKILL.md"
|
||
elif self.platform == "kiro":
|
||
return f".kiro/skills/trellis-{name}/SKILL.md"
|
||
elif self.platform == "gemini":
|
||
return f".gemini/commands/trellis/{name}.toml"
|
||
elif self.platform == "antigravity":
|
||
return f".agent/workflows/{name}.md"
|
||
elif self.platform == "devin":
|
||
return f".devin/workflows/trellis-{name}.md"
|
||
elif self.platform == "kilo":
|
||
return f".kilocode/workflows/{name}.md"
|
||
elif self.platform == "copilot":
|
||
return f".github/prompts/{name}.prompt.md"
|
||
elif self.platform == "droid":
|
||
return f".factory/commands/trellis/{name}.md"
|
||
elif self.platform == "pi":
|
||
return f".pi/prompts/trellis-{name}.md"
|
||
elif self.platform in ("omp", "grok"):
|
||
return f"{self.config_dir_name}/commands/trellis-{name}.md"
|
||
elif self.platform == "kimi":
|
||
return f".kimi-code/skills/trellis-{name}/SKILL.md"
|
||
else:
|
||
return f"{self.config_dir_name}/commands/trellis/{name}.md"
|
||
|
||
# =========================================================================
|
||
# 环境变量
|
||
# =========================================================================
|
||
|
||
def get_non_interactive_env(self) -> dict[str, str]:
|
||
"""获取非交互模式所需的环境变量。
|
||
|
||
返回:
|
||
要设置的环境变量字典
|
||
"""
|
||
if self.platform == "opencode":
|
||
return {"OPENCODE_NON_INTERACTIVE": "1"}
|
||
elif self.platform == "iflow":
|
||
return {"IFLOW_NON_INTERACTIVE": "1"}
|
||
elif self.platform == "codex":
|
||
return {"CODEX_NON_INTERACTIVE": "1"}
|
||
elif self.platform == "kiro":
|
||
return {"KIRO_NON_INTERACTIVE": "1"}
|
||
elif self.platform == "gemini":
|
||
return {} # Gemini CLI 没有非交互环境变量
|
||
elif self.platform == "antigravity":
|
||
return {}
|
||
elif self.platform == "devin":
|
||
return {}
|
||
elif self.platform == "qoder":
|
||
return {}
|
||
elif self.platform == "codebuddy":
|
||
return {}
|
||
elif self.platform == "copilot":
|
||
return {}
|
||
elif self.platform == "droid":
|
||
return {}
|
||
elif self.platform == "pi":
|
||
return {}
|
||
elif self.platform == "trae":
|
||
return {}
|
||
elif self.platform == "omp":
|
||
return {}
|
||
elif self.platform == "grok":
|
||
return {}
|
||
elif self.platform == "kimi":
|
||
return {}
|
||
else:
|
||
return {"CLAUDE_NON_INTERACTIVE": "1"}
|
||
|
||
# =========================================================================
|
||
# CLI command 构建
|
||
# =========================================================================
|
||
|
||
def build_run_command(
|
||
self,
|
||
agent: str,
|
||
prompt: str,
|
||
session_id: str | None = None,
|
||
skip_permissions: bool = True,
|
||
verbose: bool = True,
|
||
json_output: bool = True,
|
||
) -> list[str]:
|
||
"""构建运行 agent 的 CLI command。
|
||
|
||
参数:
|
||
agent:agent 名称(必要时会映射)
|
||
prompt:发送给 agent 的 prompt
|
||
session_id:可选 session ID(仅 Claude Code 创建时支持)
|
||
skip_permissions:是否跳过权限提示
|
||
verbose:是否启用详细输出
|
||
json_output:是否使用 JSON 输出格式
|
||
|
||
返回:
|
||
command 参数列表
|
||
"""
|
||
mapped_agent = self.get_agent_name(agent)
|
||
|
||
if self.platform == "opencode":
|
||
cmd = ["opencode", "run"]
|
||
cmd.extend(["--agent", mapped_agent])
|
||
|
||
# 注意:OpenCode 的 'run' 模式默认是非交互的。
|
||
# 没有等价于 Claude Code --dangerously-skip-permissions 的选项。
|
||
# 参见:https://github.com/anomalyco/opencode/issues/9070
|
||
|
||
if json_output:
|
||
cmd.extend(["--format", "json"])
|
||
|
||
if verbose:
|
||
cmd.extend(["--log-level", "DEBUG", "--print-logs"])
|
||
|
||
# 注意:OpenCode 创建时不支持 --session-id。
|
||
# session ID 必须在启动后从日志提取。
|
||
|
||
cmd.append(prompt)
|
||
|
||
elif self.platform == "iflow":
|
||
cmd = ["iflow", "-y", "-p"]
|
||
cmd.append(f"${mapped_agent} {prompt}")
|
||
elif self.platform == "codex":
|
||
cmd = ["codex", "exec"]
|
||
cmd.append(prompt)
|
||
elif self.platform == "kiro":
|
||
cmd = ["kiro", "run", prompt]
|
||
elif self.platform == "gemini":
|
||
cmd = ["gemini"]
|
||
cmd.append(prompt)
|
||
elif self.platform == "antigravity":
|
||
raise ValueError(
|
||
"Antigravity workflow 是 UI slash command;不支持 CLI agent run。"
|
||
)
|
||
elif self.platform == "devin":
|
||
raise ValueError(
|
||
"Devin workflow 是 UI slash command;不支持 CLI agent run。"
|
||
)
|
||
elif self.platform == "qoder":
|
||
cmd = ["qodercli", "-p", prompt]
|
||
elif self.platform == "codebuddy":
|
||
raise ValueError(
|
||
"CodeBuddy 不支持 non-interactive mode(没有 CLI agent)。"
|
||
)
|
||
elif self.platform == "copilot":
|
||
raise ValueError(
|
||
"GitHub Copilot 仅支持 IDE;不支持 CLI agent run。"
|
||
)
|
||
elif self.platform == "droid":
|
||
raise ValueError(
|
||
"Factory Droid 暂不支持 CLI agent run。"
|
||
)
|
||
elif self.platform == "pi":
|
||
cmd = ["pi", "-p", prompt]
|
||
elif self.platform == "trae":
|
||
raise ValueError(
|
||
"Trae 仅支持 IDE;不支持 CLI agent run。"
|
||
)
|
||
elif self.platform == "omp":
|
||
raise ValueError(
|
||
"OMP 使用原生 task tool 运行 agent;不支持 CLI agent run。"
|
||
)
|
||
elif self.platform == "grok":
|
||
# 无头单 prompt;sub-agent 使用进程内 spawn_subagent。
|
||
cmd = ["grok", "-p", prompt, "--yolo"]
|
||
elif self.platform == "kimi":
|
||
# 无头单 prompt 并自动批准;sub-agent 是在 session 内分发的内置
|
||
# coder/explore/plan agent。
|
||
cmd = ["kimi", "-p", prompt, "--yolo"]
|
||
|
||
else: # claude
|
||
cmd = ["claude", "-p"]
|
||
cmd.extend(["--agent", mapped_agent])
|
||
|
||
if session_id:
|
||
cmd.extend(["--session-id", session_id])
|
||
|
||
if skip_permissions:
|
||
cmd.append("--dangerously-skip-permissions")
|
||
|
||
if json_output:
|
||
cmd.extend(["--output-format", "stream-json"])
|
||
|
||
if verbose:
|
||
cmd.append("--verbose")
|
||
|
||
cmd.append(prompt)
|
||
|
||
return cmd
|
||
|
||
def build_resume_command(self, session_id: str) -> list[str]:
|
||
"""构建恢复 session 的 CLI command。
|
||
|
||
参数:
|
||
session_id:要恢复的 session ID(iFlow 会忽略)
|
||
|
||
返回:
|
||
command 参数列表
|
||
"""
|
||
if self.platform == "opencode":
|
||
return ["opencode", "run", "--session", session_id]
|
||
elif self.platform == "iflow":
|
||
# iFlow 使用 -c 继续最近的 conversation。
|
||
# iFlow 不支持 session ID,因此忽略 session_id。
|
||
return ["iflow", "-c"]
|
||
elif self.platform == "codex":
|
||
return ["codex", "resume", session_id]
|
||
elif self.platform == "kiro":
|
||
return ["kiro", "resume", session_id]
|
||
elif self.platform == "gemini":
|
||
return ["gemini", "--resume", session_id]
|
||
elif self.platform == "antigravity":
|
||
raise ValueError(
|
||
"Antigravity workflow 是 UI slash command;不支持 CLI resume。"
|
||
)
|
||
elif self.platform == "devin":
|
||
raise ValueError(
|
||
"Devin workflow 是 UI slash command;不支持 CLI resume。"
|
||
)
|
||
elif self.platform == "qoder":
|
||
return ["qodercli", "--resume", session_id]
|
||
elif self.platform == "codebuddy":
|
||
raise ValueError(
|
||
"CodeBuddy 不支持 non-interactive mode(没有 CLI agent)。"
|
||
)
|
||
elif self.platform == "copilot":
|
||
raise ValueError(
|
||
"GitHub Copilot 仅支持 IDE;不支持 CLI resume。"
|
||
)
|
||
elif self.platform == "droid":
|
||
raise ValueError(
|
||
"Factory Droid 暂不支持 CLI resume。"
|
||
)
|
||
elif self.platform == "pi":
|
||
return ["pi", "-c", session_id]
|
||
elif self.platform == "trae":
|
||
raise ValueError(
|
||
"Trae 仅支持 IDE;不支持 CLI resume。"
|
||
)
|
||
elif self.platform == "omp":
|
||
raise ValueError(
|
||
"OMP 使用原生 task tool 运行 agent;不支持 CLI resume。"
|
||
)
|
||
elif self.platform == "grok":
|
||
return ["grok", "-c"]
|
||
elif self.platform == "kimi":
|
||
return ["kimi", "--session", session_id]
|
||
else:
|
||
return ["claude", "--resume", session_id]
|
||
|
||
def get_resume_command_str(self, session_id: str, cwd: str | None = None) -> str:
|
||
"""获取供人阅读的恢复 command 字符串。
|
||
|
||
参数:
|
||
session_id:要恢复的 session ID
|
||
cwd:可选的工作目录(先执行 cd)
|
||
|
||
返回:
|
||
用于展示的 command 字符串
|
||
"""
|
||
cmd = self.build_resume_command(session_id)
|
||
cmd_str = " ".join(cmd)
|
||
|
||
if cwd:
|
||
return f"cd {cwd} && {cmd_str}"
|
||
return cmd_str
|
||
|
||
# =========================================================================
|
||
# 平台检测辅助方法
|
||
# =========================================================================
|
||
|
||
@property
|
||
def is_opencode(self) -> bool:
|
||
"""判断平台是否为 OpenCode。"""
|
||
return self.platform == "opencode"
|
||
|
||
@property
|
||
def is_claude(self) -> bool:
|
||
"""判断平台是否为 Claude Code。"""
|
||
return self.platform == "claude"
|
||
|
||
@property
|
||
def is_cursor(self) -> bool:
|
||
"""判断平台是否为 Cursor。"""
|
||
return self.platform == "cursor"
|
||
|
||
@property
|
||
def is_iflow(self) -> bool:
|
||
"""判断平台是否为 iFlow CLI。"""
|
||
return self.platform == "iflow"
|
||
|
||
@property
|
||
def cli_name(self) -> str:
|
||
"""获取 CLI 可执行文件名称。
|
||
|
||
注意:Cursor 没有 CLI 工具,此处返回类似 None 的占位值。
|
||
"""
|
||
if self.is_opencode:
|
||
return "opencode"
|
||
elif self.is_cursor:
|
||
return "cursor" # 注意:Cursor 仅支持 IDE,没有 CLI
|
||
elif self.platform == "iflow":
|
||
return "iflow"
|
||
elif self.platform == "kiro":
|
||
return "kiro"
|
||
elif self.platform == "gemini":
|
||
return "gemini"
|
||
elif self.platform == "antigravity":
|
||
return "agy"
|
||
elif self.platform == "devin":
|
||
return "devin"
|
||
elif self.platform == "qoder":
|
||
return "qodercli"
|
||
elif self.platform == "codebuddy":
|
||
return "codebuddy"
|
||
elif self.platform == "copilot":
|
||
return "copilot"
|
||
elif self.platform == "droid":
|
||
return "droid"
|
||
elif self.platform == "pi":
|
||
return "pi"
|
||
elif self.platform == "trae":
|
||
return "trae"
|
||
elif self.platform == "omp":
|
||
return "omp"
|
||
elif self.platform == "grok":
|
||
return "grok"
|
||
elif self.platform == "kimi":
|
||
return "kimi"
|
||
else:
|
||
return "claude"
|
||
|
||
@property
|
||
def supports_cli_agents(self) -> bool:
|
||
"""判断平台是否支持通过 CLI 运行 agent。
|
||
|
||
Claude Code、OpenCode、iFlow、Codex 支持 CLI agent 执行;Cursor 仅支持 IDE,
|
||
不支持 CLI agent。
|
||
"""
|
||
return self.platform in (
|
||
"claude",
|
||
"opencode",
|
||
"iflow",
|
||
"codex",
|
||
"pi",
|
||
"grok",
|
||
"kimi",
|
||
)
|
||
|
||
@property
|
||
def requires_agent_definition_file(self) -> bool:
|
||
"""判断平台是否需要 agent 定义文件(.md/.toml)才能运行。
|
||
|
||
Claude Code、OpenCode、iFlow:需要 agent .md 文件(--agent flag)。
|
||
Codex:自动发现 .codex/agents/*.toml,不需要 --agent flag。
|
||
"""
|
||
return self.platform in ("claude", "opencode", "iflow")
|
||
|
||
# =========================================================================
|
||
# Session ID 处理
|
||
# =========================================================================
|
||
|
||
@property
|
||
def supports_session_id_on_create(self) -> bool:
|
||
"""判断平台是否支持创建时指定 session ID。
|
||
|
||
Claude Code:支持(--session-id)。
|
||
OpenCode:不支持(自动生成,需从日志提取)。
|
||
iFlow:不支持(没有 session ID 能力)。
|
||
"""
|
||
return self.platform == "claude"
|
||
|
||
def extract_session_id_from_log(self, log_content: str) -> str | None:
|
||
"""从日志输出提取 session ID(仅 OpenCode)。
|
||
|
||
OpenCode 生成的 session ID 格式为:ses_xxx。
|
||
|
||
参数:
|
||
log_content:日志文件内容
|
||
|
||
返回:
|
||
找到时返回 session ID,否则返回 None
|
||
"""
|
||
import re
|
||
|
||
# OpenCode session ID 模式
|
||
match = re.search(r"ses_[a-zA-Z0-9]+", log_content)
|
||
if match:
|
||
return match.group(0)
|
||
return None
|
||
|
||
|
||
# =============================================================================
|
||
# 工厂函数
|
||
# =============================================================================
|
||
|
||
|
||
def get_cli_adapter(platform: str = "claude") -> CLIAdapter:
|
||
"""获取指定平台的 CLI 适配器。
|
||
|
||
参数:
|
||
platform:平台名称('claude'、'opencode'、'cursor'、'iflow'、'codex'、
|
||
'kilo'、'kiro'、'gemini'、'antigravity'、'devin'、'qoder'、'codebuddy'、
|
||
'copilot'、'droid'、'pi' 或 'trae')
|
||
|
||
返回:
|
||
CLIAdapter 实例
|
||
|
||
异常:
|
||
ValueError:平台不受支持时抛出
|
||
|
||
说明:
|
||
'windsurf' 作为 'devin' 的弃用别名接受(Windsurf 已更名为 Devin),并在
|
||
校验前归一化。
|
||
"""
|
||
# 弃用别名:Windsurf 已更名为 Devin。
|
||
if platform == "windsurf":
|
||
platform = "devin"
|
||
if platform not in (
|
||
"claude",
|
||
"opencode",
|
||
"cursor",
|
||
"iflow",
|
||
"codex",
|
||
"kilo",
|
||
"kiro",
|
||
"gemini",
|
||
"antigravity",
|
||
"devin",
|
||
"qoder",
|
||
"codebuddy",
|
||
"copilot",
|
||
"droid",
|
||
"pi",
|
||
"trae",
|
||
"omp",
|
||
"grok",
|
||
"kimi",
|
||
):
|
||
raise ValueError(
|
||
f"不支持的平台:{platform}(必须是 'claude'、'opencode'、'cursor'、'iflow'、'codex'、'kilo'、'kiro'、'gemini'、'antigravity'、'devin'、'qoder'、'codebuddy'、'copilot'、'droid'、'pi'、'trae'、'omp'、'grok' 或 'kimi')"
|
||
)
|
||
|
||
return CLIAdapter(platform=platform) # type: ignore
|
||
|
||
|
||
_ALL_PLATFORM_CONFIG_DIRS = (
|
||
".claude",
|
||
".cursor",
|
||
".iflow",
|
||
".opencode",
|
||
".codex",
|
||
".kilocode",
|
||
".kiro",
|
||
".gemini",
|
||
".agent",
|
||
".devin",
|
||
".windsurf", # 已弃用:Devin 重命名前的配置目录,仍作为平台信号。
|
||
".qoder",
|
||
".codebuddy",
|
||
".github/copilot",
|
||
".factory",
|
||
".pi",
|
||
".trae",
|
||
".omp",
|
||
".grok",
|
||
".kimi-code",
|
||
)
|
||
"""detect_platform 排除检查使用的平台配置目录名称。
|
||
|
||
这里不列出 `.agents/skills/`:它是跨平台共享层(由 Codex 写入,也可由
|
||
Amp/Cline/Warp 等通过 agentskills.io 标准消费),不能作为单一平台信号。它的存在
|
||
不应阻止 Kiro、Antigravity、Devin 或其他平台的检测。
|
||
"""
|
||
|
||
|
||
def _has_other_platform_dir(project_root: Path, exclude: set[str]) -> bool:
|
||
"""检查除 *exclude* 外是否存在其他平台配置目录。"""
|
||
return any(
|
||
(project_root / d).is_dir()
|
||
for d in _ALL_PLATFORM_CONFIG_DIRS
|
||
if d not in exclude
|
||
)
|
||
|
||
|
||
def detect_platform(project_root: Path) -> Platform:
|
||
"""根据现有配置目录自动检测平台。
|
||
|
||
检测顺序:
|
||
1. 设置了 TRELLIS_PLATFORM 环境变量时使用它;
|
||
2. 存在 .opencode 目录 → opencode;
|
||
3. 存在 .iflow 目录 → iflow;
|
||
4. 存在 .cursor 且不存在 .claude → cursor;
|
||
5. 存在 .gemini 目录 → gemini;
|
||
6. 存在 .codex 且没有其他平台目录 → codex;
|
||
7. 存在 .kilocode 目录 → kilo;
|
||
8. 存在 .kiro/skills 且没有其他平台目录 → kiro;
|
||
9. 存在 .agent/workflows 且没有其他平台目录 → antigravity;
|
||
10. 存在 .devin/workflows(或旧的 .windsurf/workflows)且没有其他平台目录 → devin;
|
||
11. 存在 .codebuddy 目录 → codebuddy;
|
||
12. 存在 .qoder 目录 → qoder;
|
||
13. 存在 .github/copilot 目录 → copilot;
|
||
14. 存在 .factory 目录 → droid;
|
||
15. 存在 .pi 目录 → pi;
|
||
16. 存在 .trae 目录 → trae;
|
||
17. 默认 → claude。
|
||
|
||
参数:
|
||
project_root:项目根目录
|
||
|
||
返回:
|
||
检测到的平台('claude'、'opencode'、'cursor'、'iflow'、'codex'、'kilo'、
|
||
'kiro'、'gemini'、'antigravity'、'devin'、'qoder'、'codebuddy'、'copilot'、
|
||
'droid'、'pi'、'trae',或默认的 'claude')
|
||
"""
|
||
import os
|
||
|
||
# 先检查环境变量。
|
||
env_platform = os.environ.get("TRELLIS_PLATFORM", "").lower()
|
||
# 弃用别名:Windsurf 已更名为 Devin。
|
||
if env_platform == "windsurf":
|
||
env_platform = "devin"
|
||
if env_platform in (
|
||
"claude",
|
||
"opencode",
|
||
"cursor",
|
||
"iflow",
|
||
"codex",
|
||
"kilo",
|
||
"kiro",
|
||
"gemini",
|
||
"antigravity",
|
||
"devin",
|
||
"qoder",
|
||
"codebuddy",
|
||
"copilot",
|
||
"droid",
|
||
"pi",
|
||
"trae",
|
||
"omp",
|
||
"grok",
|
||
"kimi",
|
||
):
|
||
return env_platform # type: ignore
|
||
|
||
# 检查 .opencode 目录(OpenCode 专用)。
|
||
if (project_root / ".opencode").is_dir():
|
||
return "opencode"
|
||
|
||
# 检查 .iflow 目录(iFlow 专用)。
|
||
if (project_root / ".iflow").is_dir():
|
||
return "iflow"
|
||
|
||
# 检查 .cursor 目录(Cursor 专用)。
|
||
# 只有在 .claude 不存在时才检测为 cursor,以免混淆。
|
||
if (project_root / ".cursor").is_dir() and not (project_root / ".claude").is_dir():
|
||
return "cursor"
|
||
|
||
# 检查 .gemini 目录(Gemini CLI 专用)。
|
||
if (project_root / ".gemini").is_dir():
|
||
return "gemini"
|
||
|
||
# 检查 .codex 目录(Codex 专用)。
|
||
# 仅有 .agents/skills/ 不会触发 codex 检测(它是共享标准)。
|
||
if (project_root / ".codex").is_dir() and not _has_other_platform_dir(
|
||
project_root, {".codex", ".agents"}
|
||
):
|
||
return "codex"
|
||
|
||
# 检查 .kilocode 目录(Kilo 专用)。
|
||
if (project_root / ".kilocode").is_dir():
|
||
return "kilo"
|
||
|
||
# 只有不存在其他平台配置时才检查 Kiro skills 目录。
|
||
if (project_root / ".kiro" / "skills").is_dir() and not _has_other_platform_dir(
|
||
project_root, {".kiro"}
|
||
):
|
||
return "kiro"
|
||
|
||
# 只有不存在其他平台配置时才检查 Antigravity workflow 目录。
|
||
if (
|
||
project_root / ".agent" / "workflows"
|
||
).is_dir() and not _has_other_platform_dir(
|
||
project_root, {".agent", ".gemini"}
|
||
):
|
||
return "antigravity"
|
||
|
||
# 只有不存在其他平台配置时才检查 Devin workflow 目录。`.windsurf/workflows` 是
|
||
# 更名之前的旧路径(在用户通过 `trellis update --migrate` 迁移前,为兼容性仍检测为 devin)。
|
||
if (
|
||
(project_root / ".devin" / "workflows").is_dir()
|
||
or (project_root / ".windsurf" / "workflows").is_dir()
|
||
) and not _has_other_platform_dir(
|
||
project_root, {".devin", ".windsurf"}
|
||
):
|
||
return "devin"
|
||
|
||
# 检查 .codebuddy 目录(CodeBuddy 专用)。
|
||
if (project_root / ".codebuddy").is_dir():
|
||
return "codebuddy"
|
||
|
||
# 检查 .qoder 目录(Qoder 专用)。
|
||
if (project_root / ".qoder").is_dir():
|
||
return "qoder"
|
||
|
||
# 检查 .github/copilot 目录(GitHub Copilot 专用)。
|
||
if (project_root / ".github" / "copilot").is_dir():
|
||
return "copilot"
|
||
|
||
# 检查 .factory 目录(Factory Droid 专用)。
|
||
if (project_root / ".factory").is_dir():
|
||
return "droid"
|
||
|
||
# 检查 .pi 目录(Pi Agent 专用)。
|
||
if (project_root / ".pi").is_dir():
|
||
return "pi"
|
||
|
||
# 检查 .trae 目录(Trae IDE 专用)。
|
||
if (project_root / ".trae").is_dir():
|
||
return "trae"
|
||
|
||
# 检查 .omp 目录(OMP 专用)。
|
||
if (project_root / ".omp").is_dir():
|
||
return "omp"
|
||
|
||
# 检查 .grok 目录(Grok Build 专用)。
|
||
if (project_root / ".grok").is_dir():
|
||
return "grok"
|
||
|
||
# 检查 .kimi-code 目录(Kimi Code 专用)。
|
||
if (project_root / ".kimi-code").is_dir():
|
||
return "kimi"
|
||
|
||
# 回退:检出目录只有 Codex 共享 skills 层(.agents/skills/trellis-* 目录),没有
|
||
# 明确的平台配置目录。常见于新克隆:.codex/ 被 gitignore 或缺失,但共享 skills
|
||
# 已提交到 Git。必须防止 .claude/ 或其他平台目录同时存在的情况——.agents/skills/
|
||
# 可以合法地与任何平台共存,作为 Amp/Cline/Warp 等共享消费层。
|
||
agents_skills = project_root / ".agents" / "skills"
|
||
if agents_skills.is_dir() and not _has_other_platform_dir(
|
||
project_root, set()
|
||
):
|
||
try:
|
||
for entry in agents_skills.iterdir():
|
||
if entry.is_dir() and entry.name.startswith("trellis-"):
|
||
return "codex"
|
||
except OSError:
|
||
pass
|
||
|
||
return "claude"
|
||
|
||
|
||
def get_cli_adapter_auto(project_root: Path) -> CLIAdapter:
|
||
"""获取自动检测平台对应的 CLI 适配器。
|
||
|
||
参数:
|
||
project_root:项目根目录
|
||
|
||
返回:
|
||
检测平台对应的 CLIAdapter 实例
|
||
"""
|
||
platform = detect_platform(project_root)
|
||
return CLIAdapter(platform=platform)
|