From c3af24ef517678755b016990f572b6269a87ad88 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 21:10:13 -0400 Subject: [PATCH] feat(trash): MCP delete tools soft-delete via trash; add delete_task/project/milestone --- src/fabledassistant/mcp/tools/events.py | 13 +++++++------ src/fabledassistant/mcp/tools/milestones.py | 13 +++++++++++++ src/fabledassistant/mcp/tools/notes.py | 12 +++++++----- src/fabledassistant/mcp/tools/projects.py | 13 +++++++++++++ src/fabledassistant/mcp/tools/rulebooks.py | 16 ++++++++++------ src/fabledassistant/mcp/tools/tasks.py | 13 +++++++++++++ 6 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/fabledassistant/mcp/tools/events.py b/src/fabledassistant/mcp/tools/events.py index 592584d..f4bc4cf 100644 --- a/src/fabledassistant/mcp/tools/events.py +++ b/src/fabledassistant/mcp/tools/events.py @@ -21,6 +21,7 @@ from datetime import datetime, timedelta, timezone from fabledassistant.mcp._context import current_user_id from fabledassistant.services import events as events_svc +from fabledassistant.services import trash as trash_svc def _combine(start_date: str, start_time: str) -> datetime: @@ -149,14 +150,14 @@ async def update_event( return event.to_dict() -async def delete_event(event_id: int) -> str: - """Permanently delete a calendar event by ID. Cannot be undone.""" +async def delete_event(event_id: int) -> dict: + """Move a calendar event to the trash (recoverable). Restore via restore(batch_id).""" uid = current_user_id() - existing = await events_svc.get_event(uid, event_id) - if existing is None: + batch = await trash_svc.delete(uid, "event", event_id) + if batch is None: raise ValueError(f"event {event_id} not found") - await events_svc.delete_event(uid, event_id) - return f"Event {event_id} deleted." + return {"deleted_batch_id": batch, + "message": f"Event {event_id} moved to trash. Restore with restore('{batch}')."} def register(mcp) -> None: diff --git a/src/fabledassistant/mcp/tools/milestones.py b/src/fabledassistant/mcp/tools/milestones.py index c2b004e..6625fd4 100644 --- a/src/fabledassistant/mcp/tools/milestones.py +++ b/src/fabledassistant/mcp/tools/milestones.py @@ -13,6 +13,7 @@ from __future__ import annotations from fabledassistant.mcp._context import current_user_id from fabledassistant.services import milestones as milestones_svc +from fabledassistant.services import trash as trash_svc async def list_milestones(project_id: int) -> dict: @@ -86,10 +87,22 @@ async def update_milestone( return milestone.to_dict() +async def delete_milestone(milestone_id: int) -> dict: + """Move a milestone to the trash (recoverable). Its tasks go with it as one batch. + Restore via restore(batch_id).""" + uid = current_user_id() + batch = await trash_svc.delete(uid, "milestone", milestone_id) + if batch is None: + raise ValueError(f"milestone {milestone_id} not found") + return {"deleted_batch_id": batch, + "message": f"Milestone {milestone_id} + its tasks moved to trash. Restore with restore('{batch}')."} + + def register(mcp) -> None: for fn in ( list_milestones, create_milestone, update_milestone, + delete_milestone, ): mcp.tool(name=fn.__name__)(fn) diff --git a/src/fabledassistant/mcp/tools/notes.py b/src/fabledassistant/mcp/tools/notes.py index e9d04e9..c2eb8b8 100644 --- a/src/fabledassistant/mcp/tools/notes.py +++ b/src/fabledassistant/mcp/tools/notes.py @@ -15,6 +15,7 @@ from __future__ import annotations from fabledassistant.mcp._context import current_user_id from fabledassistant.services import notes as notes_svc +from fabledassistant.services import trash as trash_svc async def list_notes( @@ -113,13 +114,14 @@ async def update_note( return note.to_dict() -async def delete_note(note_id: int) -> str: - """Permanently delete a Scribe note by ID. This cannot be undone.""" +async def delete_note(note_id: int) -> dict: + """Move a Scribe note to the trash (recoverable). Restore via restore(batch_id).""" uid = current_user_id() - deleted = await notes_svc.delete_note(uid, note_id) - if not deleted: + batch = await trash_svc.delete(uid, "note", note_id) + if batch is None: raise ValueError(f"note {note_id} not found") - return f"Note {note_id} deleted." + return {"deleted_batch_id": batch, + "message": f"Note {note_id} moved to trash. Restore with restore('{batch}')."} def register(mcp) -> None: diff --git a/src/fabledassistant/mcp/tools/projects.py b/src/fabledassistant/mcp/tools/projects.py index a6c4928..bb7c37f 100644 --- a/src/fabledassistant/mcp/tools/projects.py +++ b/src/fabledassistant/mcp/tools/projects.py @@ -20,6 +20,7 @@ from fabledassistant.mcp._context import current_user_id from fabledassistant.services import milestones as milestones_svc from fabledassistant.services import projects as projects_svc from fabledassistant.services import rulebooks as rulebooks_svc +from fabledassistant.services import trash as trash_svc async def list_projects() -> dict: @@ -121,11 +122,23 @@ async def update_project( return project.to_dict() +async def delete_project(project_id: int) -> dict: + """Move a project to the trash (recoverable). Its milestones, tasks, and notes + go with it as one batch. Restore via restore(batch_id).""" + uid = current_user_id() + batch = await trash_svc.delete(uid, "project", project_id) + if batch is None: + raise ValueError(f"project {project_id} not found") + return {"deleted_batch_id": batch, + "message": f"Project {project_id} + its contents moved to trash. Restore with restore('{batch}')."} + + def register(mcp) -> None: for fn in ( list_projects, get_project, create_project, update_project, + delete_project, ): mcp.tool(name=fn.__name__)(fn) diff --git a/src/fabledassistant/mcp/tools/rulebooks.py b/src/fabledassistant/mcp/tools/rulebooks.py index adb9ba3..1f82a56 100644 --- a/src/fabledassistant/mcp/tools/rulebooks.py +++ b/src/fabledassistant/mcp/tools/rulebooks.py @@ -11,6 +11,7 @@ from __future__ import annotations from fabledassistant.mcp._context import current_user_id from fabledassistant.services import rulebooks as rulebooks_svc +from fabledassistant.services import trash as trash_svc # ── Rulebook CRUD ─────────────────────────────────────────────────────── @@ -90,8 +91,9 @@ async def delete_rulebook(rulebook_id: int, confirmed: bool = False) -> dict: ), "confirmed_required": True, } - await rulebooks_svc.delete_rulebook(rulebook_id, uid) - return {"deleted": rulebook_id} + batch = await trash_svc.delete(uid, "rulebook", rulebook_id) + return {"deleted": rulebook_id, "deleted_batch_id": batch, + "message": f"Moved to trash. Restore with restore('{batch}')."} # ── Topic CRUD ───────────────────────────────────────────────────────── @@ -159,8 +161,9 @@ async def delete_topic(topic_id: int, confirmed: bool = False) -> dict: ), "confirmed_required": True, } - await rulebooks_svc.delete_topic(topic_id, uid) - return {"deleted": topic_id} + batch = await trash_svc.delete(uid, "topic", topic_id) + return {"deleted": topic_id, "deleted_batch_id": batch, + "message": f"Moved to trash. Restore with restore('{batch}')."} # ── Rule CRUD ────────────────────────────────────────────────────────── @@ -266,8 +269,9 @@ async def delete_rule(rule_id: int, confirmed: bool = False) -> dict: ), "confirmed_required": True, } - await rulebooks_svc.delete_rule(rule_id, uid) - return {"deleted": rule_id} + batch = await trash_svc.delete(uid, "rule", rule_id) + return {"deleted": rule_id, "deleted_batch_id": batch, + "message": f"Moved to trash. Restore with restore('{batch}')."} # ── Subscriptions ────────────────────────────────────────────────────── diff --git a/src/fabledassistant/mcp/tools/tasks.py b/src/fabledassistant/mcp/tools/tasks.py index a1ce839..4cfd4f1 100644 --- a/src/fabledassistant/mcp/tools/tasks.py +++ b/src/fabledassistant/mcp/tools/tasks.py @@ -23,6 +23,7 @@ from fabledassistant.services import notes as notes_svc from fabledassistant.services import planning as planning_svc from fabledassistant.services import rulebooks as rulebooks_svc from fabledassistant.services import task_logs as task_logs_svc +from fabledassistant.services import trash as trash_svc async def list_tasks( @@ -197,6 +198,17 @@ async def start_planning(project_id: int, title: str) -> dict: ) +async def delete_task(task_id: int) -> dict: + """Move a Scribe task (or plan) to the trash (recoverable). Sub-tasks go with it. + Restore via restore(batch_id).""" + uid = current_user_id() + batch = await trash_svc.delete(uid, "task", task_id) + if batch is None: + raise ValueError(f"task {task_id} not found") + return {"deleted_batch_id": batch, + "message": f"Task {task_id} moved to trash. Restore with restore('{batch}')."} + + def register(mcp) -> None: for fn in ( list_tasks, @@ -205,5 +217,6 @@ def register(mcp) -> None: update_task, add_task_log, start_planning, + delete_task, ): mcp.tool(name=fn.__name__)(fn)