feat(cli): add delete-note tool command

This commit is contained in:
Adit Karode
2026-06-06 22:41:46 -04:00
committed by GitHub
parent 20bb19f4cd
commit 476239d878
5 changed files with 549 additions and 1 deletions
+81
View File
@@ -15,6 +15,7 @@ from basic_memory.cli.app import app
from basic_memory.cli.commands.command_utils import run_with_cleanup
from basic_memory.cli.commands.routing import force_routing, validate_routing_flags
from basic_memory.mcp.tools import build_context as mcp_build_context
from basic_memory.mcp.tools import delete_note as mcp_delete_note
from basic_memory.mcp.tools import edit_note as mcp_edit_note
from basic_memory.mcp.tools import list_memory_projects as mcp_list_projects
from basic_memory.mcp.tools import list_workspaces as mcp_list_workspaces
@@ -40,6 +41,26 @@ def _print_json(result: Any) -> None:
print(json.dumps(result, indent=2, ensure_ascii=True, default=str))
def _delete_note_failure_message(result: dict[str, Any]) -> str | None:
"""Return the CLI failure message for delete-note JSON results, if any."""
error = result.get("error")
if error:
return str(error)
failed_deletes = result.get("failed_deletes")
# Trigger: directory deletion can partially fail without raising from the service.
# Why: cleanup scripts need a non-zero exit when files remain undeleted.
# Outcome: the CLI fails even if older MCP JSON did not include an error field.
if (
result.get("is_directory") is True
and isinstance(failed_deletes, int)
and failed_deletes > 0
):
return f"Directory delete incomplete: {failed_deletes} file(s) failed"
return None
# --- Commands ---
@@ -178,6 +199,66 @@ def read_note(
raise
@tool_app.command("delete-note")
def delete_note(
identifier: str,
is_directory: bool = typer.Option(
False, "--is-directory", help="Delete a directory instead of a single note"
),
project: Annotated[
Optional[str],
typer.Option(help="The project to use. If not provided, the default project will be used."),
] = None,
project_id: Annotated[
Optional[str],
typer.Option(
"--project-id",
help="Project external_id (UUID). Takes precedence over --project; use to disambiguate same-named projects across cloud workspaces.",
),
] = None,
local: bool = typer.Option(
False, "--local", help="Force local API routing (ignore cloud mode)"
),
cloud: bool = typer.Option(False, "--cloud", help="Force cloud API routing"),
) -> None:
"""Delete a note or directory from the knowledge base.
Examples:
bm tool delete-note notes/old-draft
bm tool delete-note docs/archive --is-directory
"""
try:
validate_routing_flags(local, cloud)
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_delete_note(
identifier=identifier,
is_directory=is_directory,
project=project,
project_id=project_id,
output_format="json",
)
)
if isinstance(result, dict):
failure_message = _delete_note_failure_message(result)
if failure_message:
typer.echo(f"Error: {failure_message}", err=True)
raise typer.Exit(1)
_print_json(result)
except ValueError as e:
typer.echo(f"Error: {e}", err=True)
raise typer.Exit(1)
except Exception as e: # pragma: no cover
if not isinstance(e, typer.Exit):
typer.echo(f"Error during delete_note: {e}", err=True)
raise typer.Exit(1)
raise
@tool_app.command()
def edit_note(
identifier: str,