feat: 添加 mycodex 入口,支持 OpenAI Codex CLI
- 在 launcher 中注册 codex 后端,兼容 ~/.codex/sessions 日期嵌套目录 - 解析 session_meta.payload.cwd 过滤当前目录会话 - 添加 myagents codex 子命令与 mycodex 独立入口、补全及 Makefile 链接 - 更新 pyproject.toml 控制台脚本与 README 文档 - 补充 codex 相关单元测试,45 tests passed
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
ROOT_DIR := $(shell pwd)
|
||||
VENV_BIN_DIR := $(ROOT_DIR)/.venv/bin
|
||||
USER_BIN_DIR := $(HOME)/.local/bin
|
||||
COMMANDS := myagents myclaude mykimi
|
||||
COMMANDS := myagents myclaude mykimi mycodex
|
||||
|
||||
.PHONY: help install uninstall _symlink-commands
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# MyAgents
|
||||
|
||||
一个统一的 AI coding agent 启动器,封装 Claude Code、Kimi Code CLI 等工具。
|
||||
一个统一的 AI coding agent 启动器,封装 Claude Code、Kimi Code CLI、OpenAI Codex 等工具。
|
||||
|
||||
## 功能
|
||||
|
||||
- 🤖 **多 Agent 统一入口**:`myagents claude` / `myagents kimi`
|
||||
- 🚀 **独立快捷命令**:`myclaude`、`mykimi`
|
||||
- 🤖 **多 Agent 统一入口**:`myagents claude` / `myagents kimi` / `myagents codex`
|
||||
- 🚀 **独立快捷命令**:`myclaude`、`mykimi`、`mycodex`
|
||||
- 📂 **自动 Workspace 解析**:默认 `~/workspace`,可通过环境变量覆盖
|
||||
- 📝 **Session 管理**:`--list` 列出可恢复会话
|
||||
- ➡️ **原生参数透传**:任意底层 agent 支持的参数直接透传
|
||||
@@ -48,7 +48,7 @@ make install
|
||||
|
||||
`make install` 会:
|
||||
1. 同步/创建虚拟环境并做可编辑安装
|
||||
2. 在 `~/.local/bin/` 创建 `myagents`、`myclaude`、`mykimi` 符号链接
|
||||
2. 在 `~/.local/bin/` 创建 `myagents`、`myclaude`、`mykimi`、`mycodex` 符号链接
|
||||
3. 自动安装 shell 补全到 `~/.local/share/zsh/site-functions` 和 `~/.local/share/bash-completion/completions`
|
||||
|
||||
请确保 `~/.local/bin` 在 `PATH` 中,并在 `~/.zshrc` 里加上:
|
||||
@@ -81,6 +81,8 @@ myagents claude -l # 列出可恢复 sessions
|
||||
myagents claude -r <session-id> # 恢复指定 session
|
||||
myagents kimi # 启动 Kimi Code CLI
|
||||
myagents kimi -S <session-id> # 恢复 Kimi session
|
||||
myagents codex # 启动 OpenAI Codex CLI
|
||||
myagents codex resume <id> # 恢复 Codex session
|
||||
myagents update # 重装 myagents 并同步 workspace 链接
|
||||
myagents upgrade # update 别名
|
||||
myagents completion # 安装/查看/卸载 shell 补全
|
||||
@@ -90,9 +92,11 @@ myclaude
|
||||
myclaude --cwd . -r <session-id>
|
||||
mykimi
|
||||
mykimi -S <session-id>
|
||||
mycodex
|
||||
mycodex resume <session-id>
|
||||
```
|
||||
|
||||
各后端原生参数会原样透传。例如 Kimi npm 版支持 `--session`、`-c`(continue)、`-y`(yolo)等,直接加在命令后即可。
|
||||
各后端原生参数会原样透传。例如 Kimi npm 版支持 `--session`、`-c`(continue)、`-y`(yolo)等,Codex 支持 `resume`、`continue`、`exec` 等子命令,直接加在命令后即可。
|
||||
|
||||
## 配置
|
||||
|
||||
|
||||
+7
-3
@@ -11,12 +11,13 @@ from myagents.launcher import build_cli
|
||||
|
||||
def _progs():
|
||||
"""Return (prog_name, cli_factory) pairs that need shell completions."""
|
||||
from myagents.entrypoints import claude_cli, kimi_cli
|
||||
from myagents.entrypoints import claude_cli, codex_cli, kimi_cli
|
||||
|
||||
return [
|
||||
("myagents", lambda: cli),
|
||||
("myclaude", lambda: claude_cli),
|
||||
("mykimi", lambda: kimi_cli),
|
||||
("mycodex", lambda: codex_cli),
|
||||
]
|
||||
|
||||
|
||||
@@ -33,7 +34,7 @@ def _package_version() -> str:
|
||||
def cli(ctx: click.Context) -> None:
|
||||
"""Myagents: unified launcher for AI coding agents.
|
||||
|
||||
Use ``myagents claude`` or ``myagents kimi`` to start an agent in workspace/.
|
||||
Use ``myagents claude``, ``myagents kimi`` or ``myagents codex`` to start an agent in workspace/.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
click.echo(ctx.get_help())
|
||||
@@ -42,13 +43,16 @@ def cli(ctx: click.Context) -> None:
|
||||
|
||||
cli.add_command(build_cli("claude"), name="claude")
|
||||
cli.add_command(build_cli("kimi"), name="kimi")
|
||||
cli.add_command(build_cli("codex"), name="codex")
|
||||
cli.add_command(update_cmd)
|
||||
cli.add_command(upgrade_cmd, name="upgrade")
|
||||
cli.add_command(build_completion_group(_progs))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
from myagents.commands.completion_install import ensure_completions_installed
|
||||
from myagents.commands.completion_install import (
|
||||
ensure_completions_installed,
|
||||
)
|
||||
|
||||
ensure_completions_installed(_progs())
|
||||
cli()
|
||||
|
||||
@@ -35,6 +35,10 @@ def _print_script(prog_name: str, shell: str) -> None:
|
||||
from myagents.entrypoints import kimi_cli
|
||||
|
||||
cli_obj = kimi_cli
|
||||
elif prog_name == "mycodex":
|
||||
from myagents.entrypoints import codex_cli
|
||||
|
||||
cli_obj = codex_cli
|
||||
else:
|
||||
raise click.ClickException(f"Unknown command: {prog_name}")
|
||||
|
||||
@@ -56,7 +60,7 @@ def build_completion_group(progs_fn: ProgListFn) -> click.Group:
|
||||
|
||||
@completion.command("install")
|
||||
def install_cmd() -> None:
|
||||
"""Write completion scripts for myagents, myclaude and mykimi."""
|
||||
"""Write completion scripts for myagents, myclaude, mykimi and mycodex."""
|
||||
installed = install_completions(progs_fn())
|
||||
if not installed:
|
||||
raise click.ClickException(
|
||||
|
||||
@@ -9,14 +9,13 @@ from __future__ import annotations
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable, Sequence
|
||||
|
||||
import click
|
||||
from click.shell_completion import get_completion_class
|
||||
|
||||
|
||||
ProgList = Sequence[tuple[str, Callable[[], click.Command]]]
|
||||
|
||||
|
||||
|
||||
+19
-3
@@ -1,9 +1,10 @@
|
||||
"""Standalone entrypoints for myclaude and mykimi command names."""
|
||||
"""Standalone entrypoints for myclaude, mykimi, and mycodex command names."""
|
||||
|
||||
from myagents.launcher import build_cli
|
||||
|
||||
claude_cli = build_cli("claude", prog_name="myclaude")
|
||||
kimi_cli = build_cli("kimi", prog_name="mykimi")
|
||||
codex_cli = build_cli("codex", prog_name="mycodex")
|
||||
|
||||
|
||||
def _progs():
|
||||
@@ -13,12 +14,15 @@ def _progs():
|
||||
("myagents", lambda: cli),
|
||||
("myclaude", lambda: claude_cli),
|
||||
("mykimi", lambda: kimi_cli),
|
||||
("mycodex", lambda: codex_cli),
|
||||
]
|
||||
|
||||
|
||||
def claude_main() -> None:
|
||||
"""Run ``myclaude``."""
|
||||
from myagents.commands.completion_install import ensure_completions_installed
|
||||
from myagents.commands.completion_install import (
|
||||
ensure_completions_installed,
|
||||
)
|
||||
|
||||
ensure_completions_installed(_progs())
|
||||
claude_cli()
|
||||
@@ -26,7 +30,19 @@ def claude_main() -> None:
|
||||
|
||||
def kimi_main() -> None:
|
||||
"""Run ``mykimi``."""
|
||||
from myagents.commands.completion_install import ensure_completions_installed
|
||||
from myagents.commands.completion_install import (
|
||||
ensure_completions_installed,
|
||||
)
|
||||
|
||||
ensure_completions_installed(_progs())
|
||||
kimi_cli()
|
||||
|
||||
|
||||
def codex_main() -> None:
|
||||
"""Run ``mycodex``."""
|
||||
from myagents.commands.completion_install import (
|
||||
ensure_completions_installed,
|
||||
)
|
||||
|
||||
ensure_completions_installed(_progs())
|
||||
codex_cli()
|
||||
|
||||
+94
-3
@@ -42,6 +42,17 @@ _BACKENDS: dict[str, dict] = {
|
||||
"[cyan]KIMI_BIN[/cyan]."
|
||||
),
|
||||
},
|
||||
"codex": {
|
||||
"binary": "codex",
|
||||
"env_bin": "CODEX_BIN",
|
||||
"sessions_root": lambda: Path.home() / ".codex" / "sessions",
|
||||
"session_pattern": "**/*.jsonl",
|
||||
"default_args": [],
|
||||
"not_found_msg": (
|
||||
"[red]codex CLI not found in PATH.[/red] Install with "
|
||||
"[cyan]npm install -g @openai/codex[/cyan] or set [cyan]CODEX_BIN[/cyan]."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +80,12 @@ def _launch(backend: str, chat_cwd: Path, extra: list[str]) -> None:
|
||||
raise SystemExit(proc.returncode)
|
||||
|
||||
|
||||
_SESSION_ID_UUID_RE = re.compile(
|
||||
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _sessions_dir(backend: str, chat_cwd: Path) -> Path:
|
||||
"""Directory where the backend stores sessions for chat_cwd."""
|
||||
config = _BACKENDS[backend]
|
||||
@@ -76,6 +93,9 @@ def _sessions_dir(backend: str, chat_cwd: Path) -> Path:
|
||||
if backend == "kimi":
|
||||
# Kimi hashes the cwd with md5.
|
||||
munged = hashlib.md5(cwd_str.encode("utf-8")).hexdigest() # noqa: S324
|
||||
elif backend == "codex":
|
||||
# Codex stores all sessions under a single dated tree; cwd is in metadata.
|
||||
return config["sessions_root"]()
|
||||
else:
|
||||
munged = re.sub(r"[^A-Za-z0-9]", "-", cwd_str)
|
||||
return config["sessions_root"]() / munged
|
||||
@@ -125,11 +145,71 @@ def _first_prompt_kimi(session_file: Path) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _codex_session_cwd(session_file: Path) -> str | None:
|
||||
"""Read cwd from a codex session's session_meta record."""
|
||||
try:
|
||||
with session_file.open(encoding="utf-8", errors="ignore") as fh:
|
||||
for line in fh:
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if entry.get("type") == "session_meta":
|
||||
return entry.get("payload", {}).get("cwd")
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _first_prompt_codex(session_file: Path) -> str:
|
||||
"""Best-effort snippet of the first human prompt in a codex rollout jsonl."""
|
||||
try:
|
||||
with session_file.open(encoding="utf-8", errors="ignore") as fh:
|
||||
for line in fh:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if entry.get("type") != "user_message":
|
||||
continue
|
||||
payload = entry.get("payload", {})
|
||||
if payload.get("type") != "user_message":
|
||||
continue
|
||||
content = payload.get("message")
|
||||
if isinstance(content, str) and content.strip():
|
||||
return " ".join(content.split())[:80]
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _codex_session_id(session_file: Path) -> str:
|
||||
"""Extract the UUID session id from a codex rollout filename."""
|
||||
match = _SESSION_ID_UUID_RE.search(session_file.stem)
|
||||
return match.group(0) if match else session_file.stem
|
||||
|
||||
|
||||
def _session_files(backend: str, chat_cwd: Path) -> list[Path]:
|
||||
"""Return session files/directories sorted by newest first."""
|
||||
config = _BACKENDS[backend]
|
||||
sessions_dir = _sessions_dir(backend, chat_cwd)
|
||||
|
||||
if backend == "codex":
|
||||
files = [p for p in sessions_dir.glob(config["session_pattern"]) if p.is_file()]
|
||||
cwd_str = str(chat_cwd)
|
||||
|
||||
def _matches_cwd(p: Path) -> bool:
|
||||
stored = _codex_session_cwd(p)
|
||||
if stored is None:
|
||||
return False
|
||||
return stored == cwd_str or Path(stored).resolve() == chat_cwd
|
||||
|
||||
files = [p for p in files if _matches_cwd(p)]
|
||||
files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
return files
|
||||
|
||||
if backend == "claude":
|
||||
files = sorted(
|
||||
sessions_dir.glob(config["session_pattern"]),
|
||||
@@ -153,6 +233,8 @@ def _session_files(backend: str, chat_cwd: Path) -> list[Path]:
|
||||
def _first_prompt(backend: str, session_file: Path) -> str:
|
||||
if backend == "claude":
|
||||
return _first_prompt_claude(session_file)
|
||||
if backend == "codex":
|
||||
return _first_prompt_codex(session_file)
|
||||
return _first_prompt_kimi(session_file)
|
||||
|
||||
|
||||
@@ -160,6 +242,8 @@ def _resume_syntax(backend: str) -> str:
|
||||
"""Return the backend's resume flag syntax for use in help text."""
|
||||
if backend == "kimi":
|
||||
return "--session, -S <id>"
|
||||
if backend == "codex":
|
||||
return "resume <id>"
|
||||
return "--resume, -r <id>"
|
||||
|
||||
|
||||
@@ -174,7 +258,12 @@ def _list_sessions(backend: str, chat_cwd: Path) -> None:
|
||||
for session_file in files:
|
||||
mtime = datetime.fromtimestamp(session_file.stat().st_mtime)
|
||||
snippet = _first_prompt(backend, session_file) or "[dim](empty)[/dim]"
|
||||
session_id = session_file.parent.name if backend == "kimi" else session_file.stem
|
||||
if backend == "kimi":
|
||||
session_id = session_file.parent.name
|
||||
elif backend == "codex":
|
||||
session_id = _codex_session_id(session_file)
|
||||
else:
|
||||
session_id = session_file.stem
|
||||
console.print(
|
||||
f" [cyan]{session_id}[/cyan] "
|
||||
f"[dim]{mtime:%Y-%m-%d %H:%M}[/dim] {snippet}"
|
||||
@@ -204,12 +293,14 @@ class LaunchGroup(click.Group):
|
||||
|
||||
|
||||
def build_cli(backend: str, prog_name: str | None = None) -> click.Group:
|
||||
"""Build a click CLI that wraps ``backend`` (claude or kimi).
|
||||
"""Build a click CLI that wraps ``backend`` (claude, kimi, or codex).
|
||||
|
||||
``prog_name`` is used in --version output. When omitted it defaults to
|
||||
``myagents <backend>`` (suitable for use as a subcommand of ``myagents``).
|
||||
"""
|
||||
backend_title = backend.capitalize()
|
||||
if backend == "codex":
|
||||
backend_title = "OpenAI Codex"
|
||||
|
||||
@click.group(
|
||||
cls=LaunchGroup,
|
||||
@@ -262,7 +353,7 @@ def build_cli(backend: str, prog_name: str | None = None) -> click.Group:
|
||||
_launch(backend, chat_cwd, extra)
|
||||
|
||||
cli.help = (
|
||||
f"Launch {backend_title} Code in workspace/.\n\n"
|
||||
f"Launch {backend_title} in workspace/.\n\n"
|
||||
f"Unknown arguments ({_resume_syntax(backend)}, --continue, …) pass through to {backend}."
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -12,6 +12,7 @@ dependencies = [
|
||||
myagents = "myagents.cli:cli"
|
||||
myclaude = "myagents.entrypoints:claude_main"
|
||||
mykimi = "myagents.entrypoints:kimi_main"
|
||||
mycodex = "myagents.entrypoints:codex_main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest>=8.0"]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove ~/.local/bin/{myagents,myclaude,mykimi} symlinks to this repo's venv."""
|
||||
"""Remove ~/.local/bin/{myagents,myclaude,mykimi,mycodex} symlinks to this repo's venv."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
_COMMANDS = ("myagents", "myclaude", "mykimi")
|
||||
_COMMANDS = ("myagents", "myclaude", "mykimi", "mycodex")
|
||||
|
||||
|
||||
def _remove_link(link: str, want: str) -> bool:
|
||||
|
||||
+137
-1
@@ -1,10 +1,12 @@
|
||||
"""Tests for myagents CLI."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
import myagents.launcher
|
||||
from myagents.cli import cli
|
||||
|
||||
|
||||
@@ -12,12 +14,13 @@ class TestMyagentsHelp:
|
||||
"""Tests for top-level myagents command."""
|
||||
|
||||
def test_help_shows_agent_subcommands(self) -> None:
|
||||
"""--help should list claude and kimi subcommands."""
|
||||
"""--help should list claude, kimi and codex subcommands."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "claude" in result.output
|
||||
assert "kimi" in result.output
|
||||
assert "codex" in result.output
|
||||
assert "update" in result.output
|
||||
assert "upgrade" in result.output
|
||||
|
||||
@@ -226,3 +229,136 @@ class TestUpdateSubcommand:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["upgrade", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
class TestCodexSubcommand:
|
||||
"""Tests for ``myagents codex``."""
|
||||
|
||||
def test_help_shows_options(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["codex", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "--cwd" in result.output
|
||||
assert "--list" in result.output
|
||||
|
||||
def test_runs_codex(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["codex"])
|
||||
assert result.exit_code == 0
|
||||
mock_run.assert_called_once()
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/codex"]
|
||||
|
||||
def test_missing_binary_error(self) -> None:
|
||||
runner = CliRunner()
|
||||
with patch("myagents.launcher.shutil.which", return_value=None):
|
||||
result = runner.invoke(cli, ["codex"])
|
||||
assert result.exit_code == 127
|
||||
assert "not found" in result.output.lower()
|
||||
|
||||
def test_cwd_option_passed(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
test_dir = tmp_path / "test_cwd"
|
||||
test_dir.mkdir()
|
||||
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["codex", "--cwd", str(test_dir)])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args.kwargs.get("cwd") == str(test_dir.resolve())
|
||||
|
||||
|
||||
class TestCodexPassthrough:
|
||||
"""Codex resume subcommand forwards to codex instead of erroring."""
|
||||
|
||||
def _invoke(self, args: list[str], tmp_path: Path) -> list[str]:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(
|
||||
cli, ["codex", "--cwd", str(tmp_path), *args]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_run.assert_called_once()
|
||||
return mock_run.call_args[0][0]
|
||||
|
||||
def test_resume_subcommand_passes_through(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["resume", "abc123"], tmp_path)
|
||||
assert cmd == ["/usr/bin/codex", "resume", "abc123"]
|
||||
|
||||
def test_continue_subcommand_passes_through(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["continue"], tmp_path)
|
||||
assert cmd == ["/usr/bin/codex", "continue"]
|
||||
|
||||
|
||||
class TestCodexListSessions:
|
||||
"""``myagents codex --list`` reports resumable sessions."""
|
||||
|
||||
def test_list_empty_directory(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with patch("myagents.launcher.subprocess.run") as mock_run:
|
||||
result = runner.invoke(cli, ["codex", "--cwd", str(tmp_path), "--list"])
|
||||
assert result.exit_code == 0
|
||||
assert "no sessions" in result.output.lower()
|
||||
mock_run.assert_not_called()
|
||||
|
||||
def test_list_shows_session_id_and_snippet(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
sessions_root = tmp_path / "sessions"
|
||||
sessions_root.mkdir(parents=True)
|
||||
session_file = (
|
||||
sessions_root
|
||||
/ "2026"
|
||||
/ "01"
|
||||
/ "02"
|
||||
/ "rollout-2026-01-02T12-00-00-019aeb67-0620-71c1-8cbb-756bc8845c6e.jsonl"
|
||||
)
|
||||
session_file.parent.mkdir(parents=True)
|
||||
session_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "session_meta",
|
||||
"timestamp": "2026-01-02T12:00:00Z",
|
||||
"payload": {
|
||||
"id": "019aeb67-0620-71c1-8cbb-756bc8845c6e",
|
||||
"cwd": str(tmp_path),
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n"
|
||||
+ json.dumps(
|
||||
{
|
||||
"type": "user_message",
|
||||
"payload": {
|
||||
"type": "user_message",
|
||||
"message": "hello codex",
|
||||
},
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"myagents.launcher._BACKENDS",
|
||||
{
|
||||
"codex": {
|
||||
**myagents.launcher._BACKENDS["codex"],
|
||||
"sessions_root": lambda: sessions_root,
|
||||
}
|
||||
},
|
||||
):
|
||||
result = runner.invoke(cli, ["codex", "--cwd", str(tmp_path), "--list"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "019aeb67-0620-71c1-8cbb-756bc8845c6e" in result.output
|
||||
assert "hello codex" in result.output
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Tests for standalone myclaude / mykimi entrypoints."""
|
||||
"""Tests for standalone myclaude / mykimi / mycodex entrypoints."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from myagents.entrypoints import claude_cli, kimi_cli
|
||||
from myagents.entrypoints import claude_cli, codex_cli, kimi_cli
|
||||
|
||||
|
||||
class TestMyclaudeEntrypoint:
|
||||
@@ -73,3 +73,36 @@ class TestMykimiEntrypoint:
|
||||
result = runner.invoke(kimi_cli, ["--cwd", str(tmp_path), "--resume"])
|
||||
assert result.exit_code == 0
|
||||
assert "--resume" in mock_run.call_args[0][0]
|
||||
|
||||
|
||||
class TestMycodexEntrypoint:
|
||||
"""``mycodex`` standalone entrypoint."""
|
||||
|
||||
def test_runs_codex(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(codex_cli, [])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/codex"]
|
||||
|
||||
def test_version_shows_mycodex(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(codex_cli, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert "mycodex" in result.output
|
||||
|
||||
def test_passthrough(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/codex"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(codex_cli, ["--cwd", str(tmp_path), "resume", "abc123"])
|
||||
assert result.exit_code == 0
|
||||
assert "resume" in mock_run.call_args[0][0]
|
||||
assert "abc123" in mock_run.call_args[0][0]
|
||||
|
||||
Reference in New Issue
Block a user