Skip to content

Commit 2df0c49

Browse files
committed
feat(remove): drop items instead of deleting them
Change remove_item and batch_remove_items to mark items as "dropped" status rather than physically deleting them from OmniFocus. This preserves data and allows recovery if needed.
1 parent a951263 commit 2df0c49

File tree

3 files changed

+45
-15
lines changed

3 files changed

+45
-15
lines changed

src/omnifocus_mcp/mcp_tools/batch/batch_remove.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"""Batch remove items tool for OmniFocus."""
1+
"""Batch remove items tool for OmniFocus.
2+
3+
Note: This tool drops items (marks them as dropped) instead of physically deleting them.
4+
This preserves data and allows recovery if needed.
5+
"""
26

37
import json
48
from typing import Any
@@ -13,6 +17,9 @@ async def batch_remove_items(
1317
"""
1418
Remove multiple tasks or projects from OmniFocus in a single operation.
1519
20+
Note: Items are dropped (marked as dropped status) rather than physically deleted.
21+
This preserves data and allows recovery if needed.
22+
1623
Args:
1724
items: List of items to remove. Each item is a dict with:
1825
- id: Optional ID of the item (preferred)
@@ -23,7 +30,7 @@ async def batch_remove_items(
2330
JSON string with results in the following format:
2431
{
2532
"total": <number of items attempted>,
26-
"success": <number of items removed successfully>,
33+
"success": <number of items dropped successfully>,
2734
"failed": <number of items that failed>,
2835
"results": [
2936
{

src/omnifocus_mcp/mcp_tools/tasks/remove_item.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"""Remove item tool for OmniFocus."""
1+
"""Remove item tool for OmniFocus.
2+
3+
Note: This tool drops items (marks them as dropped) instead of physically deleting them.
4+
This preserves data and allows recovery if needed.
5+
"""
26

37
import asyncio
48

@@ -13,6 +17,9 @@ async def remove_item(
1317
"""
1418
Remove a task or project from OmniFocus.
1519
20+
Note: Items are dropped (marked as dropped status) rather than physically deleted.
21+
This preserves data and allows recovery if needed.
22+
1623
Args:
1724
name: The name of the task or project to remove (used if id not provided)
1825
id: The ID of the task or project to remove (preferred)
@@ -31,15 +38,21 @@ async def remove_item(
3138

3239
# Build result message
3340
if id:
34-
result_msg = f"{item_type.capitalize()} removed successfully (by ID)"
41+
result_msg = f"{item_type.capitalize()} dropped successfully (by ID)"
42+
else:
43+
result_msg = f"{item_type.capitalize()} dropped successfully: {name}"
44+
45+
# Drop items instead of deleting - different syntax for tasks vs projects
46+
if item_type == "project":
47+
drop_statement = "set status of theItem to dropped status"
3548
else:
36-
result_msg = f"{item_type.capitalize()} removed successfully: {name}"
49+
drop_statement = "set dropped of theItem to true"
3750

3851
script = f'''
3952
tell application "OmniFocus"
4053
tell default document
4154
{find_clause}
42-
delete theItem
55+
{drop_statement}
4356
end tell
4457
end tell
4558
return "{result_msg}"

tests/test_tasks.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -301,37 +301,42 @@ async def test_edit_task_parent_and_position_change(self):
301301

302302

303303
class TestRemoveItem:
304-
"""Tests for remove_item function."""
304+
"""Tests for remove_item function (drops items instead of deleting)."""
305305

306306
@pytest.mark.asyncio
307307
async def test_remove_task(self):
308-
"""Test removing a task."""
308+
"""Test removing (dropping) a task."""
309309
with patch(
310310
"omnifocus_mcp.mcp_tools.tasks.remove_item.asyncio.create_subprocess_exec"
311311
) as mock_exec:
312312
# Setup mock
313313
mock_process = AsyncMock()
314-
mock_process.communicate.return_value = (b"Task removed successfully: Test Task", b"")
314+
mock_process.communicate.return_value = (b"Task dropped successfully: Test Task", b"")
315315
mock_process.returncode = 0
316316
mock_exec.return_value = mock_process
317317

318318
# Execute
319319
result = await remove_item(name="Test Task")
320320

321321
# Verify
322-
assert "Task removed successfully" in result
322+
assert "Task dropped successfully" in result
323323
mock_exec.assert_called_once()
324+
# Verify the script uses 'set dropped' instead of 'delete'
325+
call_args = mock_exec.call_args
326+
script = call_args[0][2]
327+
assert "set dropped of theItem to true" in script
328+
assert "delete" not in script
324329

325330
@pytest.mark.asyncio
326331
async def test_remove_project(self):
327-
"""Test removing a project."""
332+
"""Test removing (dropping) a project."""
328333
with patch(
329334
"omnifocus_mcp.mcp_tools.tasks.remove_item.asyncio.create_subprocess_exec"
330335
) as mock_exec:
331336
# Setup mock
332337
mock_process = AsyncMock()
333338
mock_process.communicate.return_value = (
334-
b"Project removed successfully: Test Project",
339+
b"Project dropped successfully: Test Project",
335340
b"",
336341
)
337342
mock_process.returncode = 0
@@ -341,8 +346,13 @@ async def test_remove_project(self):
341346
result = await remove_item(name="Test Project", item_type="project")
342347

343348
# Verify
344-
assert "Project removed successfully" in result
349+
assert "Project dropped successfully" in result
345350
mock_exec.assert_called_once()
351+
# Verify the script uses 'dropped status' instead of 'delete'
352+
call_args = mock_exec.call_args
353+
script = call_args[0][2]
354+
assert "set status of theItem to dropped status" in script
355+
assert "delete" not in script
346356

347357
@pytest.mark.asyncio
348358
async def test_remove_item_escapes_special_characters(self):
@@ -352,15 +362,15 @@ async def test_remove_item_escapes_special_characters(self):
352362
) as mock_exec:
353363
# Setup mock
354364
mock_process = AsyncMock()
355-
mock_process.communicate.return_value = (b"Task removed successfully", b"")
365+
mock_process.communicate.return_value = (b"Task dropped successfully", b"")
356366
mock_process.returncode = 0
357367
mock_exec.return_value = mock_process
358368

359369
# Execute with special characters
360370
result = await remove_item(name='Task "with" quotes')
361371

362372
# Verify
363-
assert "Task removed successfully" in result
373+
assert "Task dropped successfully" in result
364374
# Check that osascript was called with escaped string
365375
call_args = mock_exec.call_args
366376
script = call_args[0][2]

0 commit comments

Comments
 (0)