feat: add reminders_remove tool for single-reminder removal
- reminders_remove(index) removes one reminder by 1-based index - reminders_list now returns numbered output (1. heading / body) so any model can easily identify which index to pass - _parse_sections() / _sections_to_text() helpers for clean round-trip - Not in CONFIRM_REQUIRED — targeted removal is safe without a gate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@ from tools.cron import (
|
|||||||
from tools.reminders import (
|
from tools.reminders import (
|
||||||
reminders_add as _reminders_add,
|
reminders_add as _reminders_add,
|
||||||
reminders_list as _reminders_list,
|
reminders_list as _reminders_list,
|
||||||
|
reminders_remove as _reminders_remove,
|
||||||
reminders_clear as _reminders_clear,
|
reminders_clear as _reminders_clear,
|
||||||
)
|
)
|
||||||
from tools.scratch import (
|
from tools.scratch import (
|
||||||
@@ -308,6 +309,7 @@ _CALLABLES: dict[str, callable] = {
|
|||||||
"cron_toggle": _cron_toggle,
|
"cron_toggle": _cron_toggle,
|
||||||
"reminders_add": _reminders_add,
|
"reminders_add": _reminders_add,
|
||||||
"reminders_list": _reminders_list,
|
"reminders_list": _reminders_list,
|
||||||
|
"reminders_remove": _reminders_remove,
|
||||||
"reminders_clear": _reminders_clear,
|
"reminders_clear": _reminders_clear,
|
||||||
"scratch_read": _scratch_read,
|
"scratch_read": _scratch_read,
|
||||||
"scratch_write": _scratch_write,
|
"scratch_write": _scratch_write,
|
||||||
@@ -584,6 +586,24 @@ _reminders_list_declaration = types.FunctionDeclaration(
|
|||||||
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_reminders_remove_declaration = types.FunctionDeclaration(
|
||||||
|
name="reminders_remove",
|
||||||
|
description=(
|
||||||
|
"Remove a single reminder by its number. "
|
||||||
|
"Call reminders_list first to get the numbered list, then pass the number of the reminder to remove."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"index": types.Schema(
|
||||||
|
type=types.Type.INTEGER,
|
||||||
|
description="The number of the reminder to remove (1 = first item in reminders_list output).",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
required=["index"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
_reminders_clear_declaration = types.FunctionDeclaration(
|
_reminders_clear_declaration = types.FunctionDeclaration(
|
||||||
name="reminders_clear",
|
name="reminders_clear",
|
||||||
description=(
|
description=(
|
||||||
@@ -864,6 +884,7 @@ _ALL_DECLARATIONS: list[types.FunctionDeclaration] = [
|
|||||||
_cron_toggle_declaration,
|
_cron_toggle_declaration,
|
||||||
_reminders_add_declaration,
|
_reminders_add_declaration,
|
||||||
_reminders_list_declaration,
|
_reminders_list_declaration,
|
||||||
|
_reminders_remove_declaration,
|
||||||
_reminders_clear_declaration,
|
_reminders_clear_declaration,
|
||||||
_scratch_read_declaration,
|
_scratch_read_declaration,
|
||||||
_scratch_write_declaration,
|
_scratch_write_declaration,
|
||||||
|
|||||||
@@ -27,6 +27,28 @@ def _now_label() -> str:
|
|||||||
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_sections(text: str) -> list[tuple[str, str]]:
|
||||||
|
"""Split REMINDERS.md into (heading, body) tuples, one per ## section."""
|
||||||
|
sections: list[tuple[str, str]] = []
|
||||||
|
heading: str | None = None
|
||||||
|
body_lines: list[str] = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
if line.startswith("## "):
|
||||||
|
if heading is not None:
|
||||||
|
sections.append((heading, "\n".join(body_lines).strip()))
|
||||||
|
heading = line[3:].strip()
|
||||||
|
body_lines = []
|
||||||
|
elif heading is not None:
|
||||||
|
body_lines.append(line)
|
||||||
|
if heading is not None:
|
||||||
|
sections.append((heading, "\n".join(body_lines).strip()))
|
||||||
|
return sections
|
||||||
|
|
||||||
|
|
||||||
|
def _sections_to_text(sections: list[tuple[str, str]]) -> str:
|
||||||
|
return "".join(f"\n## {h}\n\n{b}\n" for h, b in sections)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Sync implementations
|
# Sync implementations
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -35,7 +57,18 @@ def _reminders_list() -> str:
|
|||||||
p = _reminders_path()
|
p = _reminders_path()
|
||||||
if not p.exists() or not p.read_text().strip():
|
if not p.exists() or not p.read_text().strip():
|
||||||
return "No pending reminders."
|
return "No pending reminders."
|
||||||
return p.read_text()
|
sections = _parse_sections(p.read_text())
|
||||||
|
if not sections:
|
||||||
|
return "No pending reminders."
|
||||||
|
lines = []
|
||||||
|
for i, (heading, body) in enumerate(sections, 1):
|
||||||
|
lines.append(f"{i}. {heading}")
|
||||||
|
if body:
|
||||||
|
# Indent body so it reads as belonging to the numbered item
|
||||||
|
for bline in body.splitlines()[:4]: # cap at 4 lines for brevity
|
||||||
|
lines.append(f" {bline}")
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines).rstrip()
|
||||||
|
|
||||||
|
|
||||||
def _reminders_add(text: str, label: str | None = None) -> str:
|
def _reminders_add(text: str, label: str | None = None) -> str:
|
||||||
@@ -47,6 +80,26 @@ def _reminders_add(text: str, label: str | None = None) -> str:
|
|||||||
return f"Reminder added: {heading}"
|
return f"Reminder added: {heading}"
|
||||||
|
|
||||||
|
|
||||||
|
def _reminders_remove(index: int) -> str:
|
||||||
|
p = _reminders_path()
|
||||||
|
if not p.exists() or not p.read_text().strip():
|
||||||
|
return "No reminders to remove."
|
||||||
|
sections = _parse_sections(p.read_text())
|
||||||
|
if not sections:
|
||||||
|
return "No reminders to remove."
|
||||||
|
if index < 1 or index > len(sections):
|
||||||
|
return (
|
||||||
|
f"Index {index} is out of range. "
|
||||||
|
f"There {'is' if len(sections) == 1 else 'are'} {len(sections)} "
|
||||||
|
f"reminder{'s' if len(sections) != 1 else ''} (1–{len(sections)}). "
|
||||||
|
"Call reminders_list to see them."
|
||||||
|
)
|
||||||
|
removed_heading = sections[index - 1][0]
|
||||||
|
sections.pop(index - 1)
|
||||||
|
p.write_text(_sections_to_text(sections))
|
||||||
|
return f"Removed reminder {index}: {removed_heading}"
|
||||||
|
|
||||||
|
|
||||||
def _reminders_clear() -> str:
|
def _reminders_clear() -> str:
|
||||||
p = _reminders_path()
|
p = _reminders_path()
|
||||||
p.write_text("")
|
p.write_text("")
|
||||||
@@ -65,5 +118,9 @@ async def reminders_add(text: str, label: str | None = None) -> str:
|
|||||||
return await asyncio.to_thread(_reminders_add, text, label)
|
return await asyncio.to_thread(_reminders_add, text, label)
|
||||||
|
|
||||||
|
|
||||||
|
async def reminders_remove(index: int) -> str:
|
||||||
|
return await asyncio.to_thread(_reminders_remove, index)
|
||||||
|
|
||||||
|
|
||||||
async def reminders_clear() -> str:
|
async def reminders_clear() -> str:
|
||||||
return await asyncio.to_thread(_reminders_clear)
|
return await asyncio.to_thread(_reminders_clear)
|
||||||
|
|||||||
Reference in New Issue
Block a user