feat: add hermes backend support
- Add myhermes entrypoint and myagents hermes subcommand - Hermes sessions read from global SQLite store (~/.hermes/state.db) - Default args: --yolo - Update README, AGENTS.md, Makefile, pyproject.toml - Add tests for hermes subcommand, passthrough, and session listing
This commit is contained in:
@@ -45,7 +45,7 @@ myagents/
|
||||
├── myagents/ # Python 源码
|
||||
│ ├── cli.py # CLI entrypoint
|
||||
│ ├── launcher.py # Agent 启动器
|
||||
│ ├── entrypoints.py # myclaude / mykimi 独立入口
|
||||
│ ├── entrypoints.py # myclaude / mykimi / mycodex / myhermes 独立入口
|
||||
│ ├── commands/ # 子命令
|
||||
│ └── ...
|
||||
├── templates/ # 模板目录
|
||||
|
||||
@@ -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 mycodex
|
||||
COMMANDS := myagents myclaude mykimi mycodex myhermes
|
||||
|
||||
.PHONY: help install uninstall _symlink-commands
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# MyAgents
|
||||
|
||||
一个统一的 AI coding agent 启动器,封装 Claude Code、Kimi Code CLI、OpenAI Codex 等工具。
|
||||
一个统一的 AI coding agent 启动器,封装 Claude Code、Kimi Code CLI、OpenAI Codex、Hermes Agent 等工具。
|
||||
|
||||
## 功能
|
||||
|
||||
- 🤖 **多 Agent 统一入口**:`myagents claude` / `myagents kimi` / `myagents codex`
|
||||
- 🚀 **独立快捷命令**:`myclaude`、`mykimi`、`mycodex`
|
||||
- 🤖 **多 Agent 统一入口**:`myagents claude` / `myagents kimi` / `myagents codex` / `myagents hermes`
|
||||
- 🚀 **独立快捷命令**:`myclaude`、`mykimi`、`mycodex`、`myhermes`
|
||||
- 📂 **自动 Workspace 解析**:默认 `~/workspace`,可通过环境变量覆盖
|
||||
- 📝 **Session 管理**:`--list` 列出可恢复会话
|
||||
- ➡️ **原生参数透传**:任意底层 agent 支持的参数直接透传
|
||||
@@ -16,7 +16,7 @@
|
||||
myagents/
|
||||
├── myagents/
|
||||
│ ├── cli.py # myagents 统一入口
|
||||
│ ├── entrypoints.py # myclaude / mykimi 独立入口
|
||||
│ ├── entrypoints.py # myclaude / mykimi / mycodex / myhermes 独立入口
|
||||
│ ├── launcher.py # Agent 启动逻辑
|
||||
│ ├── project_root.py # 项目/工作区根目录解析
|
||||
│ └── commands/ # update / upgrade 子命令
|
||||
@@ -48,7 +48,7 @@ make install
|
||||
|
||||
`make install` 会:
|
||||
1. 同步/创建虚拟环境并做可编辑安装
|
||||
2. 在 `~/.local/bin/` 创建 `myagents`、`myclaude`、`mykimi`、`mycodex` 符号链接
|
||||
2. 在 `~/.local/bin/` 创建 `myagents`、`myclaude`、`mykimi`、`mycodex`、`myhermes` 符号链接
|
||||
3. 自动安装 shell 补全到 `~/.local/share/zsh/site-functions` 和 `~/.local/share/bash-completion/completions`
|
||||
|
||||
请确保 `~/.local/bin` 在 `PATH` 中,并在 `~/.zshrc` 里加上:
|
||||
@@ -84,6 +84,9 @@ myagents kimi -S <session-id> # 恢复 Kimi session
|
||||
myagents codex # 启动 OpenAI Codex CLI
|
||||
myagents codex -r <session-id> # 恢复 Codex session(映射为 resume)
|
||||
myagents codex resume <id> # 原生 resume 子命令
|
||||
myagents hermes # 启动 Hermes Agent(默认 --yolo)
|
||||
myagents hermes -l # 列出最近的 Hermes sessions(全局 SQLite 存储)
|
||||
myagents hermes --resume <id> # 恢复 Hermes session
|
||||
myagents update # 重装 myagents 并同步 workspace 链接
|
||||
myagents upgrade # update 别名
|
||||
myagents completion # 安装/查看/卸载 shell 补全
|
||||
@@ -96,9 +99,11 @@ mykimi -S <session-id>
|
||||
mycodex
|
||||
mycodex -r <session-id>
|
||||
mycodex resume <session-id>
|
||||
myhermes
|
||||
myhermes --resume <session-id>
|
||||
```
|
||||
|
||||
各后端原生参数会原样透传。例如 Kimi npm 版支持 `--session`、`-c`(continue)、`-y`(yolo)等,Codex 支持 `resume`、`continue`、`exec` 等子命令;`mycodex` 还会将 `-r` / `--resume` 映射为 `codex resume`,与 `myclaude` 体验保持一致。
|
||||
各后端原生参数会原样透传。例如 Kimi npm 版支持 `--session`、`-c`(continue)、`-y`(yolo)等,Codex 支持 `resume`、`continue`、`exec` 等子命令;`mycodex` 还会将 `-r` / `--resume` 映射为 `codex resume`,与 `myclaude` 体验保持一致。Hermes 默认带 `--yolo` 启动,支持 `--resume <id>` / `--continue` 透传;其 sessions 存在全局 SQLite(`~/.hermes/state.db`),不区分工作目录,`-l` 列出最近 20 条。
|
||||
|
||||
## 配置
|
||||
|
||||
|
||||
+10
-2
@@ -11,13 +11,19 @@ from myagents.launcher import build_cli
|
||||
|
||||
def _progs():
|
||||
"""Return (prog_name, cli_factory) pairs that need shell completions."""
|
||||
from myagents.entrypoints import claude_cli, codex_cli, kimi_cli
|
||||
from myagents.entrypoints import (
|
||||
claude_cli,
|
||||
codex_cli,
|
||||
hermes_cli,
|
||||
kimi_cli,
|
||||
)
|
||||
|
||||
return [
|
||||
("myagents", lambda: cli),
|
||||
("myclaude", lambda: claude_cli),
|
||||
("mykimi", lambda: kimi_cli),
|
||||
("mycodex", lambda: codex_cli),
|
||||
("myhermes", lambda: hermes_cli),
|
||||
]
|
||||
|
||||
|
||||
@@ -34,7 +40,8 @@ def _package_version() -> str:
|
||||
def cli(ctx: click.Context) -> None:
|
||||
"""Myagents: unified launcher for AI coding agents.
|
||||
|
||||
Use ``myagents claude``, ``myagents kimi`` or ``myagents codex`` to start an agent in workspace/.
|
||||
Use ``myagents claude``, ``myagents kimi``, ``myagents codex`` or
|
||||
``myagents hermes`` to start an agent in workspace/.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
click.echo(ctx.get_help())
|
||||
@@ -44,6 +51,7 @@ 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(build_cli("hermes"), name="hermes")
|
||||
cli.add_command(update_cmd)
|
||||
cli.add_command(upgrade_cmd, name="upgrade")
|
||||
cli.add_command(build_completion_group(_progs))
|
||||
|
||||
@@ -39,6 +39,10 @@ def _print_script(prog_name: str, shell: str) -> None:
|
||||
from myagents.entrypoints import codex_cli
|
||||
|
||||
cli_obj = codex_cli
|
||||
elif prog_name == "myhermes":
|
||||
from myagents.entrypoints import hermes_cli
|
||||
|
||||
cli_obj = hermes_cli
|
||||
else:
|
||||
raise click.ClickException(f"Unknown command: {prog_name}")
|
||||
|
||||
@@ -60,7 +64,7 @@ def build_completion_group(progs_fn: ProgListFn) -> click.Group:
|
||||
|
||||
@completion.command("install")
|
||||
def install_cmd() -> None:
|
||||
"""Write completion scripts for myagents, myclaude, mykimi and mycodex."""
|
||||
"""Write completion scripts for all myagents entrypoints."""
|
||||
installed = install_completions(progs_fn())
|
||||
if not installed:
|
||||
raise click.ClickException(
|
||||
|
||||
+13
-1
@@ -1,10 +1,11 @@
|
||||
"""Standalone entrypoints for myclaude, mykimi, and mycodex command names."""
|
||||
"""Standalone entrypoints for myclaude, mykimi, mycodex, and myhermes."""
|
||||
|
||||
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")
|
||||
hermes_cli = build_cli("hermes", prog_name="myhermes")
|
||||
|
||||
|
||||
def _progs():
|
||||
@@ -15,6 +16,7 @@ def _progs():
|
||||
("myclaude", lambda: claude_cli),
|
||||
("mykimi", lambda: kimi_cli),
|
||||
("mycodex", lambda: codex_cli),
|
||||
("myhermes", lambda: hermes_cli),
|
||||
]
|
||||
|
||||
|
||||
@@ -46,3 +48,13 @@ def codex_main() -> None:
|
||||
|
||||
ensure_completions_installed(_progs())
|
||||
codex_cli()
|
||||
|
||||
|
||||
def hermes_main() -> None:
|
||||
"""Run ``myhermes``."""
|
||||
from myagents.commands.completion_install import (
|
||||
ensure_completions_installed,
|
||||
)
|
||||
|
||||
ensure_completions_installed(_progs())
|
||||
hermes_cli()
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
@@ -53,6 +54,18 @@ _BACKENDS: dict[str, dict] = {
|
||||
"[cyan]npm install -g @openai/codex[/cyan] or set [cyan]CODEX_BIN[/cyan]."
|
||||
),
|
||||
},
|
||||
"hermes": {
|
||||
"binary": "hermes",
|
||||
"env_bin": "HERMES_BIN",
|
||||
# Hermes keeps sessions in a global SQLite store, not per-cwd files.
|
||||
"state_db": lambda: Path.home() / ".hermes" / "state.db",
|
||||
"default_args": ["--yolo"],
|
||||
"not_found_msg": (
|
||||
"[red]hermes CLI not found in PATH.[/red] Install via the official "
|
||||
"Hermes app or [cyan]hermes-agent.nousresearch.com/install.sh[/cyan], "
|
||||
"or set [cyan]HERMES_BIN[/cyan]."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -273,8 +286,64 @@ def _resume_syntax(backend: str) -> str:
|
||||
return "--resume, -r <id>"
|
||||
|
||||
|
||||
_HERMES_SESSIONS_SQL = """
|
||||
SELECT s.id, s.started_at, s.title,
|
||||
(SELECT m.content FROM messages m
|
||||
WHERE m.session_id = s.id AND m.role = 'user'
|
||||
ORDER BY m.timestamp ASC LIMIT 1) AS first_message
|
||||
FROM sessions s
|
||||
ORDER BY s.started_at DESC
|
||||
LIMIT 20
|
||||
"""
|
||||
|
||||
|
||||
def _list_sessions_hermes(chat_cwd: Path) -> None:
|
||||
"""List recent hermes sessions from the global SQLite store.
|
||||
|
||||
Hermes sessions are not tied to a cwd, so the newest sessions are shown
|
||||
regardless of chat_cwd. Snippet is the session title, falling back to the
|
||||
first user message.
|
||||
"""
|
||||
db_path = _BACKENDS["hermes"]["state_db"]()
|
||||
rows: list[tuple] = []
|
||||
if db_path.is_file():
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
try:
|
||||
rows = conn.execute(_HERMES_SESSIONS_SQL).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
except sqlite3.Error as exc:
|
||||
stderr_console.print(
|
||||
f"[red]Failed to read hermes session store:[/red] {exc}"
|
||||
)
|
||||
return
|
||||
|
||||
if not rows:
|
||||
stderr_console.print(f"[yellow]No sessions for[/yellow] {chat_cwd}")
|
||||
return
|
||||
|
||||
console.print(
|
||||
f"[bold]Sessions in[/bold] {chat_cwd} "
|
||||
"[dim](hermes sessions are global)[/dim]"
|
||||
)
|
||||
for session_id, started_at, title, first_message in rows:
|
||||
mtime = datetime.fromtimestamp(started_at)
|
||||
snippet = " ".join(str(title or first_message or "").split())[:80]
|
||||
console.print(
|
||||
f" [cyan]{session_id}[/cyan] "
|
||||
f"[dim]{mtime:%Y-%m-%d %H:%M}[/dim] {snippet or '[dim](empty)[/dim]'}"
|
||||
)
|
||||
console.print(
|
||||
"\n[dim]Resume with[/dim] [green]hermes --resume <id>[/green]"
|
||||
)
|
||||
|
||||
|
||||
def _list_sessions(backend: str, chat_cwd: Path) -> None:
|
||||
"""Print resumable sessions for chat_cwd, newest first."""
|
||||
if backend == "hermes":
|
||||
_list_sessions_hermes(chat_cwd)
|
||||
return
|
||||
files = _session_files(backend, chat_cwd)
|
||||
if not files:
|
||||
stderr_console.print(f"[yellow]No sessions for[/yellow] {chat_cwd}")
|
||||
|
||||
@@ -13,6 +13,7 @@ myagents = "myagents.cli:cli"
|
||||
myclaude = "myagents.entrypoints:claude_main"
|
||||
mykimi = "myagents.entrypoints:kimi_main"
|
||||
mycodex = "myagents.entrypoints:codex_main"
|
||||
myhermes = "myagents.entrypoints:hermes_main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest>=8.0"]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove ~/.local/bin/{myagents,myclaude,mykimi,mycodex} symlinks to this repo's venv."""
|
||||
"""Remove ~/.local/bin/{myagents,myclaude,mykimi,mycodex,myhermes} symlinks to this repo's venv."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
_COMMANDS = ("myagents", "myclaude", "mykimi", "mycodex")
|
||||
_COMMANDS = ("myagents", "myclaude", "mykimi", "mycodex", "myhermes")
|
||||
|
||||
|
||||
def _remove_link(link: str, want: str) -> bool:
|
||||
|
||||
+135
-1
@@ -1,6 +1,7 @@
|
||||
"""Tests for myagents CLI."""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -14,13 +15,14 @@ class TestMyagentsHelp:
|
||||
"""Tests for top-level myagents command."""
|
||||
|
||||
def test_help_shows_agent_subcommands(self) -> None:
|
||||
"""--help should list claude, kimi and codex subcommands."""
|
||||
"""--help should list claude, kimi, codex and hermes 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 "hermes" in result.output
|
||||
assert "update" in result.output
|
||||
assert "upgrade" in result.output
|
||||
|
||||
@@ -400,3 +402,135 @@ class TestCodexListSessions:
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "019aeb67-0620-71c1-8cbb-756bc8845c6e" in result.output
|
||||
assert "hello codex" in result.output
|
||||
|
||||
|
||||
class TestHermesSubcommand:
|
||||
"""Tests for ``myagents hermes``."""
|
||||
|
||||
def test_help_shows_options(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["hermes", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "--cwd" in result.output
|
||||
assert "--list" in result.output
|
||||
|
||||
def test_runs_hermes(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch(
|
||||
"myagents.launcher.shutil.which", return_value="/usr/bin/hermes"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["hermes"])
|
||||
assert result.exit_code == 0
|
||||
mock_run.assert_called_once()
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/hermes", "--yolo"]
|
||||
|
||||
def test_missing_binary_error(self) -> None:
|
||||
runner = CliRunner()
|
||||
with patch("myagents.launcher.shutil.which", return_value=None):
|
||||
result = runner.invoke(cli, ["hermes"])
|
||||
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/hermes"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(cli, ["hermes", "--cwd", str(test_dir)])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args.kwargs.get("cwd") == str(test_dir.resolve())
|
||||
|
||||
|
||||
class TestHermesPassthrough:
|
||||
"""Unknown leading flags forward to hermes 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/hermes"
|
||||
),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(
|
||||
cli, ["hermes", "--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_with_session_id(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["--resume", "abc123"], tmp_path)
|
||||
assert cmd == ["/usr/bin/hermes", "--yolo", "--resume", "abc123"]
|
||||
|
||||
def test_continue_flag_passes_through(self, tmp_path: Path) -> None:
|
||||
cmd = self._invoke(["--continue"], tmp_path)
|
||||
assert cmd[-1] == "--continue"
|
||||
|
||||
|
||||
class TestHermesListSessions:
|
||||
"""``myagents hermes --list`` reads the global SQLite session store."""
|
||||
|
||||
def _patch_state_db(self, db_path: Path):
|
||||
return patch(
|
||||
"myagents.launcher._BACKENDS",
|
||||
{
|
||||
"hermes": {
|
||||
**myagents.launcher._BACKENDS["hermes"],
|
||||
"state_db": lambda: db_path,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def test_list_missing_database(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with self._patch_state_db(tmp_path / "missing.db"):
|
||||
result = runner.invoke(
|
||||
cli, ["hermes", "--cwd", str(tmp_path), "--list"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "no sessions" in result.output.lower()
|
||||
|
||||
def test_list_shows_session_id_and_snippet(self, tmp_path: Path) -> None:
|
||||
db_path = tmp_path / "state.db"
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute(
|
||||
"CREATE TABLE sessions (id TEXT PRIMARY KEY, source TEXT NOT NULL,"
|
||||
" started_at REAL NOT NULL, title TEXT)"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE TABLE messages (id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
" session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT,"
|
||||
" timestamp REAL NOT NULL)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO sessions (id, source, started_at)"
|
||||
" VALUES ('sess-abc123', 'cli', 1767220800)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO messages (session_id, role, content, timestamp)"
|
||||
" VALUES ('sess-abc123', 'user', 'hello hermes', 1767220801)"
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
runner = CliRunner()
|
||||
with self._patch_state_db(db_path):
|
||||
result = runner.invoke(
|
||||
cli, ["hermes", "--cwd", str(tmp_path), "--list"]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "sess-abc123" in result.output
|
||||
assert "hello hermes" in result.output
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Tests for standalone myclaude / mykimi / mycodex entrypoints."""
|
||||
"""Tests for standalone myclaude / mykimi / mycodex / myhermes entrypoints."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from myagents.entrypoints import claude_cli, codex_cli, kimi_cli
|
||||
from myagents.entrypoints import claude_cli, codex_cli, hermes_cli, kimi_cli
|
||||
|
||||
|
||||
class TestMyclaudeEntrypoint:
|
||||
@@ -105,3 +105,37 @@ class TestMycodexEntrypoint:
|
||||
result = runner.invoke(codex_cli, ["--cwd", str(tmp_path), "-r", "abc123"])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/codex", "--dangerously-bypass-approvals-and-sandbox", "resume", "abc123"]
|
||||
|
||||
|
||||
class TestMyhermesEntrypoint:
|
||||
"""``myhermes`` standalone entrypoint."""
|
||||
|
||||
def test_runs_hermes(self) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/hermes"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(hermes_cli, [])
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0] == ["/usr/bin/hermes", "--yolo"]
|
||||
|
||||
def test_version_shows_myhermes(self) -> None:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(hermes_cli, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert "myhermes" in result.output
|
||||
|
||||
def test_passthrough(self, tmp_path: Path) -> None:
|
||||
runner = CliRunner()
|
||||
with (
|
||||
patch("myagents.launcher.shutil.which", return_value="/usr/bin/hermes"),
|
||||
patch("myagents.launcher.subprocess.run") as mock_run,
|
||||
):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
result = runner.invoke(
|
||||
hermes_cli, ["--cwd", str(tmp_path), "--resume", "abc123"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert mock_run.call_args[0][0][-2:] == ["--resume", "abc123"]
|
||||
|
||||
Reference in New Issue
Block a user