feat: add reminder edit and delete commands

This commit is contained in:
Adolfo Reyna
2026-08-17 11:18:55 -04:00
parent bdf3b55a98
commit c445093186
2 changed files with 100 additions and 0 deletions
+64
View File
@@ -209,6 +209,20 @@ def gitea_repo(name: str, json_output: bool = typer.Option(False, "--json")):
fail(str(exc), json_output) fail(str(exc), json_output)
def run_remindctl(args: List[str]) -> Any:
"""Execute the supported Reminders CLI without invoking a shell."""
result = subprocess.run(["remindctl", *args], capture_output=True, text=True, check=False)
if result.returncode:
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "remindctl command failed")
output = result.stdout.strip()
if not output:
return {}
try:
return json.loads(output)
except json.JSONDecodeError:
return {"output": output}
def get_required_device(name: str, json_output: bool = False) -> Device: def get_required_device(name: str, json_output: bool = False) -> Device:
device = get_device(name) device = get_device(name)
if not device: if not device:
@@ -2139,6 +2153,56 @@ def macmini_reminders_create(
fail(str(exc), json_output) fail(str(exc), json_output)
@macmini_reminders_app.command("edit")
def macmini_reminders_edit(
reminder_id: str,
title: Optional[str] = typer.Option(None, "--title"),
list_name: Optional[str] = typer.Option(None, "--list"),
due: Optional[str] = typer.Option(None, "--due"),
notes: Optional[str] = typer.Option(None, "--notes"),
priority: Optional[str] = typer.Option(None, "--priority"),
clear_due: bool = typer.Option(False, "--clear-due"),
complete: bool = typer.Option(False, "--complete"),
incomplete: bool = typer.Option(False, "--incomplete"),
json_output: bool = typer.Option(False, "--json"),
):
"""Edit a reminder by its ID or ID prefix returned by ``reminders list``."""
if complete and incomplete:
fail("Use only one of --complete or --incomplete", json_output)
if not any((title is not None, list_name is not None, due is not None, notes is not None, priority is not None, clear_due, complete, incomplete)):
fail("Specify at least one field to edit", json_output)
args = ["edit", reminder_id]
for flag, value in (("--title", title), ("--list", list_name), ("--due", due), ("--notes", notes), ("--priority", priority)):
if value is not None:
args.extend([flag, value])
if clear_due:
args.append("--clear-due")
if complete:
args.append("--complete")
if incomplete:
args.append("--incomplete")
args.extend(["--json", "--no-input"])
try:
emit({"ok": True, "source": "remindctl", "result": run_remindctl(args)}, json_output)
except Exception as exc:
fail(str(exc), json_output)
@macmini_reminders_app.command("delete")
def macmini_reminders_delete(
reminder_ids: List[str] = typer.Argument(..., help="Reminder IDs or ID prefixes returned by reminders list."),
force: bool = typer.Option(False, "--force", help="Required: permanently delete without an interactive prompt."),
json_output: bool = typer.Option(False, "--json"),
):
"""Permanently delete one or more reminders; requires explicit --force."""
if not force:
fail("Deletion requires --force", json_output)
try:
emit({"ok": True, "source": "remindctl", "result": run_remindctl(["delete", *reminder_ids, "--force", "--json", "--no-input"])}, json_output)
except Exception as exc:
fail(str(exc), json_output)
def emit_deco_result(tool: str, result: Dict[str, Any], json_output: bool = False) -> None: def emit_deco_result(tool: str, result: Dict[str, Any], json_output: bool = False) -> None:
emit({"ok": True, "source": "direct", "tool": tool, "result": result}, json_output) emit({"ok": True, "source": "direct", "tool": tool, "result": result}, json_output)
+36
View File
@@ -248,6 +248,42 @@ def test_cli_reminders_no_mcp_tool_call_remaining():
assert 'call_macmini_tool("reminders_create"' not in src assert 'call_macmini_tool("reminders_create"' not in src
def test_cli_reminders_edit_uses_remindctl(monkeypatch):
captured = {}
def fake_run(args):
captured["args"] = args
return {"updated": {"id": "r1", "title": "New title"}}
monkeypatch.setattr("reyna_cli.cli.run_remindctl", fake_run)
res = runner.invoke(app, ["macmini", "reminders", "edit", "r1", "--title", "New title", "--due", "tomorrow", "--json"])
assert res.exit_code == 0
assert captured["args"] == ["edit", "r1", "--title", "New title", "--due", "tomorrow", "--json", "--no-input"]
assert json.loads(res.stdout)["source"] == "remindctl"
def test_cli_reminders_delete_requires_force(monkeypatch):
res = runner.invoke(app, ["macmini", "reminders", "delete", "r1", "--json"])
assert res.exit_code == 1
assert "--force" in res.stdout
def test_cli_reminders_delete_uses_remindctl_with_force(monkeypatch):
captured = {}
def fake_run(args):
captured["args"] = args
return {"deleted": ["r1"]}
monkeypatch.setattr("reyna_cli.cli.run_remindctl", fake_run)
res = runner.invoke(app, ["macmini", "reminders", "delete", "r1", "--force", "--json"])
assert res.exit_code == 0
assert captured["args"] == ["delete", "r1", "--force", "--json", "--no-input"]
def test_privacy_host_cli_has_reminders_authorize(): def test_privacy_host_cli_has_reminders_authorize():
res = runner.invoke(app, ["privacy-host", "--help"]) res = runner.invoke(app, ["privacy-host", "--help"])
assert res.exit_code == 0 assert res.exit_code == 0