feat: add mycursor backend; drop myagents xiaohe entrypoint

Cursor Agent CLI wrapper (mycursor) with resume/session listing.
Product xiaohe CLI stays in xiaohe-api only — no dual entry.
This commit is contained in:
Zhengshou Lai
2026-07-29 21:54:30 +08:00
parent 6ec2a3ebe6
commit ffac0ba37d
16 changed files with 426 additions and 156 deletions
+197 -1
View File
@@ -15,7 +15,7 @@ class TestMyagentsHelp:
"""Tests for top-level myagents command."""
def test_help_shows_agent_subcommands(self) -> None:
"""--help should list claude, kimi, codex and hermes subcommands."""
"""--help should list claude, kimi, codex, hermes and cursor subcommands."""
runner = CliRunner()
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
@@ -23,6 +23,7 @@ class TestMyagentsHelp:
assert "kimi" in result.output
assert "codex" in result.output
assert "hermes" in result.output
assert "cursor" in result.output
assert "update" in result.output
assert "upgrade" in result.output
@@ -49,6 +50,7 @@ class TestMyagentsHelp:
assert "kimi" in result.output
assert "codex" in result.output
assert "hermes" in result.output
assert "cursor" in result.output
def test_help_lists_backends(self) -> None:
"""--help should list backends subcommand."""
@@ -598,3 +600,197 @@ class TestHermesListSessions:
assert result.exit_code == 0, result.output
assert "sess-abc123" in result.output
assert "hello hermes" in result.output
class TestCursorSubcommand:
"""Tests for ``myagents cursor``."""
def test_help_shows_options(self) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["cursor", "--help"])
assert result.exit_code == 0
assert "--cwd" in result.output
assert "--list" in result.output
def test_runs_agent(self) -> None:
runner = CliRunner()
with (
patch(
"myagents.launcher.shutil.which", return_value="/usr/bin/agent"
),
patch("myagents.launcher.subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
result = runner.invoke(cli, ["cursor"])
assert result.exit_code == 0
mock_run.assert_called_once()
assert mock_run.call_args[0][0] == ["/usr/bin/agent", "--force"]
def test_falls_back_to_cursor_agent(self) -> None:
runner = CliRunner()
def _which(name: str) -> str | None:
return "/usr/bin/cursor-agent" if name == "cursor-agent" else None
with (
patch("myagents.launcher.shutil.which", side_effect=_which),
patch("myagents.launcher.subprocess.run") as mock_run,
patch.dict("os.environ", {}, clear=False) as env,
):
env.pop("CURSOR_BIN", None)
mock_run.return_value = MagicMock(returncode=0)
result = runner.invoke(cli, ["cursor"])
assert result.exit_code == 0
assert mock_run.call_args[0][0] == [
"/usr/bin/cursor-agent",
"--force",
]
def test_missing_binary_error(self) -> None:
runner = CliRunner()
with (
patch("myagents.launcher.shutil.which", return_value=None),
patch.dict("os.environ", {}, clear=False) as env,
):
env.pop("CURSOR_BIN", None)
result = runner.invoke(cli, ["cursor"])
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/agent"
),
patch("myagents.launcher.subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
result = runner.invoke(cli, ["cursor", "--cwd", str(test_dir)])
assert result.exit_code == 0
assert mock_run.call_args.kwargs.get("cwd") == str(test_dir.resolve())
class TestCursorPassthrough:
"""Map myagents-style resume flags to Cursor Agent CLI flags."""
def _invoke(self, args: list[str], tmp_path: Path) -> list[str]:
runner = CliRunner()
with (
patch(
"myagents.launcher.shutil.which", return_value="/usr/bin/agent"
),
patch("myagents.launcher.subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
result = runner.invoke(
cli, ["cursor", "--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_r_flag_maps_to_resume(self, tmp_path: Path) -> None:
cmd = self._invoke(["-r", "abc123"], tmp_path)
assert cmd == ["/usr/bin/agent", "--force", "--resume", "abc123"]
def test_bare_r_flag_maps_to_resume(self, tmp_path: Path) -> None:
cmd = self._invoke(["-r"], tmp_path)
assert cmd == ["/usr/bin/agent", "--force", "--resume"]
def test_resume_flag_passes_through(self, tmp_path: Path) -> None:
cmd = self._invoke(["--resume", "abc123"], tmp_path)
assert cmd == ["/usr/bin/agent", "--force", "--resume", "abc123"]
def test_c_flag_maps_to_continue(self, tmp_path: Path) -> None:
cmd = self._invoke(["-c"], tmp_path)
assert cmd == ["/usr/bin/agent", "--force", "--continue"]
def test_continue_flag_passes_through(self, tmp_path: Path) -> None:
cmd = self._invoke(["--continue"], tmp_path)
assert cmd == ["/usr/bin/agent", "--force", "--continue"]
def test_last_flag_maps_to_continue(self, tmp_path: Path) -> None:
cmd = self._invoke(["--last"], tmp_path)
assert cmd == ["/usr/bin/agent", "--force", "--continue"]
class TestCursorListSessions:
"""``myagents cursor --list`` reads ~/.cursor/chats/<cwd-md5>/."""
def test_list_empty_directory(self, tmp_path: Path) -> None:
runner = CliRunner()
with patch(
"myagents.launcher._BACKENDS",
{
"cursor": {
**myagents.launcher._BACKENDS["cursor"],
"sessions_root": lambda: tmp_path / "chats",
}
},
):
result = runner.invoke(
cli, ["cursor", "--cwd", str(tmp_path), "--list"]
)
assert result.exit_code == 0
assert "no sessions" in result.output.lower()
def test_list_shows_session_id_and_name(self, tmp_path: Path) -> None:
import hashlib
import json
import sqlite3
cwd = tmp_path / "proj"
cwd.mkdir()
chats_root = tmp_path / "chats"
munged = hashlib.md5(str(cwd.resolve()).encode("utf-8")).hexdigest()
session_dir = chats_root / munged / "chat-abc123"
session_dir.mkdir(parents=True)
db_path = session_dir / "store.db"
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)")
conn.execute("CREATE TABLE blobs (id TEXT PRIMARY KEY, data BLOB)")
meta = {
"agentId": "chat-abc123",
"name": "Fix mycursor entrypoint",
"mode": "default",
"createdAt": 1,
}
conn.execute(
"INSERT INTO meta (key, value) VALUES ('0', ?)",
(json.dumps(meta).encode().hex(),),
)
conn.execute(
"INSERT INTO blobs (id, data) VALUES ('1', ?)",
(
json.dumps(
{
"role": "user",
"content": "<user_query>\nhello cursor\n</user_query>",
}
).encode(),
),
)
conn.commit()
conn.close()
runner = CliRunner()
with patch(
"myagents.launcher._BACKENDS",
{
"cursor": {
**myagents.launcher._BACKENDS["cursor"],
"sessions_root": lambda: chats_root,
}
},
):
result = runner.invoke(
cli, ["cursor", "--cwd", str(cwd), "--list"]
)
assert result.exit_code == 0, result.output
assert "chat-abc123" in result.output
assert "Fix mycursor entrypoint" in result.output
assert "mycursor" in result.output