omnifocus-mcp-enhanced 1.6.9 → 1.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +96 -0
  2. package/README.zh.md +96 -0
  3. package/dist/tools/definitions/batchAddItems.js +3 -3
  4. package/dist/tools/definitions/plannedDateSchemas.test.js +7 -0
  5. package/dist/tools/primitives/addOmniFocusTask.js +16 -11
  6. package/dist/tools/primitives/addOmniFocusTask.test.js +18 -0
  7. package/dist/tools/primitives/addProject.js +12 -7
  8. package/dist/tools/primitives/addProject.test.js +18 -0
  9. package/dist/tools/primitives/batchAddItems.js +21 -1
  10. package/dist/tools/primitives/batchAddItems.test.js +17 -0
  11. package/dist/tools/primitives/editItem.js +26 -22
  12. package/dist/tools/primitives/editItem.test.js +32 -0
  13. package/dist/tools/primitives/removeItem.js +8 -4
  14. package/dist/utils/appleScriptJson.js +27 -0
  15. package/dist/utils/appleScriptString.js +11 -0
  16. package/dist/utils/appleScriptString.test.js +7 -0
  17. package/docs/roadmap/2026-02-25-batch-move-tasks-plan.md +28 -0
  18. package/docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md +28 -0
  19. package/package.json +3 -3
  20. package/src/tools/definitions/batchAddItems.ts +3 -3
  21. package/src/tools/definitions/plannedDateSchemas.test.ts +9 -0
  22. package/src/tools/primitives/addOmniFocusTask.test.ts +22 -0
  23. package/src/tools/primitives/addOmniFocusTask.ts +16 -11
  24. package/src/tools/primitives/addProject.test.ts +22 -0
  25. package/src/tools/primitives/addProject.ts +12 -7
  26. package/src/tools/primitives/batchAddItems.test.ts +22 -0
  27. package/src/tools/primitives/batchAddItems.ts +27 -2
  28. package/src/tools/primitives/editItem.test.ts +38 -0
  29. package/src/tools/primitives/editItem.ts +26 -22
  30. package/src/tools/primitives/removeItem.ts +9 -5
  31. package/src/utils/appleScriptJson.ts +27 -0
  32. package/src/utils/appleScriptString.test.ts +9 -0
  33. package/src/utils/appleScriptString.ts +11 -0
package/README.md CHANGED
@@ -17,6 +17,7 @@ Enhanced Model Context Protocol (MCP) server for OmniFocus featuring **native cu
17
17
 
18
18
  ## 🆕 Latest Release
19
19
 
20
+ - **v1.6.10** - Fixed Inbox task completion via `edit_item`, fixed AppleScript special-character handling for apostrophes/backslashes, fixed JSON result escaping for special characters, and clarified `batch_add_items` / `mcporter` usage with working examples.
20
21
  - **v1.6.9** - Added task attachment support: `get_task_by_id` now lists attachment metadata, `dump_database` exports attachment/link metadata, and new `read_task_attachment` returns image attachments as MCP image content when possible.
21
22
  - **v1.6.8** - Added stable task move support via `move_task` and `edit_item` (`newProjectId/newProjectName/newParentTaskId/newParentTaskName/moveToInbox`) with duplicate-name protection and cycle-prevention checks.
22
23
  - **v1.6.6** - Added full Planned Date support (create/edit/read/filter/sort/export), including `plannedDate`/`newPlannedDate` and updated task displays.
@@ -217,6 +218,101 @@ Efficiently manage multiple tasks:
217
218
  }
218
219
  ```
219
220
 
221
+ CLI tip for `mcporter`:
222
+
223
+ ```bash
224
+ # Prefer explicit JSON args for complex arrays / nested objects
225
+ mcporter call omnifocus.batch_add_items --args '{
226
+ "items": [
227
+ {
228
+ "type": "task",
229
+ "name": "Website Technical SEO",
230
+ "projectName": "SEO Project"
231
+ }
232
+ ]
233
+ }'
234
+ ```
235
+
236
+ If you pass a subtask with `parentTaskId` or `parentTaskName`, do not also pass `projectName`. Subtasks inherit the project from their parent task.
237
+
238
+ Working `mcporter` examples:
239
+
240
+ ```bash
241
+ # 1) Batch-create top-level tasks in a project
242
+ mcporter call omnifocus.batch_add_items --args '{
243
+ "items": [
244
+ {
245
+ "type": "task",
246
+ "name": "Parent: Category A",
247
+ "projectName": "OmniFocus MCP Batch Test"
248
+ },
249
+ {
250
+ "type": "task",
251
+ "name": "Parent: Category B",
252
+ "projectName": "OmniFocus MCP Batch Test"
253
+ }
254
+ ]
255
+ }'
256
+ ```
257
+
258
+ ```bash
259
+ # 2) Create parent + child in one batch
260
+ mcporter call omnifocus.batch_add_items --args '{
261
+ "items": [
262
+ {
263
+ "type": "task",
264
+ "name": "Parent: Category A",
265
+ "projectName": "OmniFocus MCP Batch Test"
266
+ },
267
+ {
268
+ "type": "task",
269
+ "name": "Child: A1",
270
+ "parentTaskName": "Parent: Category A"
271
+ }
272
+ ]
273
+ }'
274
+ ```
275
+
276
+ ```bash
277
+ # 3) Safer two-step flow when adding many subtasks to existing parents
278
+ mcporter call omnifocus.batch_add_items --args '{
279
+ "items": [
280
+ {
281
+ "type": "task",
282
+ "name": "Child: A1",
283
+ "parentTaskName": "Parent: Category A"
284
+ },
285
+ {
286
+ "type": "task",
287
+ "name": "Child: A2",
288
+ "parentTaskName": "Parent: Category A"
289
+ },
290
+ {
291
+ "type": "task",
292
+ "name": "Child: B1",
293
+ "parentTaskName": "Parent: Category B"
294
+ }
295
+ ]
296
+ }'
297
+ ```
298
+
299
+ This will fail, by design:
300
+
301
+ ```bash
302
+ mcporter call omnifocus.batch_add_items --args '{
303
+ "items": [
304
+ {
305
+ "type": "task",
306
+ "name": "Child: A1",
307
+ "projectName": "OmniFocus MCP Batch Test",
308
+ "parentTaskName": "Parent: Category A"
309
+ }
310
+ ]
311
+ }'
312
+ ```
313
+
314
+ Because a subtask must inherit its project from the parent task.
315
+
220
316
  ### 6. 🖼️ Attachment Inspection
221
317
 
222
318
  Discover images and linked files on a task first, then read only the attachment you need:
package/README.zh.md CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  ## 🆕 最新版本
15
15
 
16
+ - **v1.6.10** - 修复 `edit_item` 无法完成 Inbox 任务的问题,修复 AppleScript 对单引号/反斜杠等特殊字符的处理,修复特殊字符导致的 JSON 返回解析失败,并补充 `batch_add_items` / `mcporter` 的可直接运行示例与说明。
16
17
  - **v1.6.9** - 新增任务附件支持:`get_task_by_id` 会返回附件元信息,`dump_database` 导出附件/链接元信息,并新增 `read_task_attachment`,可在支持时直接把图片附件作为 MCP 图片内容返回。
17
18
  - **v1.6.6** - 新增 Planned Date(计划日期)全链路支持:创建/编辑/读取/过滤/排序/导出,包含 `plannedDate` / `newPlannedDate`。
18
19
 
@@ -212,6 +213,101 @@ get_custom_perspective_tasks {
212
213
  }
213
214
  ```
214
215
 
216
+ `mcporter` 调用提示:
217
+
218
+ ```bash
219
+ # 复杂数组 / 嵌套对象,建议明确使用 --args JSON
220
+ mcporter call omnifocus.batch_add_items --args '{
221
+ "items": [
222
+ {
223
+ "type": "task",
224
+ "name": "网站技术 SEO",
225
+ "projectName": "SEO 项目"
226
+ }
227
+ ]
228
+ }'
229
+ ```
230
+
231
+ 如果某条子任务传了 `parentTaskId` 或 `parentTaskName`,就不要再传 `projectName`。子任务会自动继承父任务所在项目。
232
+
233
+ 可直接运行的 `mcporter` 示例:
234
+
235
+ ```bash
236
+ # 1)批量创建项目下的顶层任务
237
+ mcporter call omnifocus.batch_add_items --args '{
238
+ "items": [
239
+ {
240
+ "type": "task",
241
+ "name": "父任务:分类A",
242
+ "projectName": "OmniFocus MCP 批量测试"
243
+ },
244
+ {
245
+ "type": "task",
246
+ "name": "父任务:分类B",
247
+ "projectName": "OmniFocus MCP 批量测试"
248
+ }
249
+ ]
250
+ }'
251
+ ```
252
+
253
+ ```bash
254
+ # 2)单次批量里同时创建父任务和子任务
255
+ mcporter call omnifocus.batch_add_items --args '{
256
+ "items": [
257
+ {
258
+ "type": "task",
259
+ "name": "父任务:分类A",
260
+ "projectName": "OmniFocus MCP 批量测试"
261
+ },
262
+ {
263
+ "type": "task",
264
+ "name": "子任务:A1",
265
+ "parentTaskName": "父任务:分类A"
266
+ }
267
+ ]
268
+ }'
269
+ ```
270
+
271
+ ```bash
272
+ # 3)更稳妥的两步法:父任务已存在时,再批量创建多个子任务
273
+ mcporter call omnifocus.batch_add_items --args '{
274
+ "items": [
275
+ {
276
+ "type": "task",
277
+ "name": "子任务:A1",
278
+ "parentTaskName": "父任务:分类A"
279
+ },
280
+ {
281
+ "type": "task",
282
+ "name": "子任务:A2",
283
+ "parentTaskName": "父任务:分类A"
284
+ },
285
+ {
286
+ "type": "task",
287
+ "name": "子任务:B1",
288
+ "parentTaskName": "父任务:分类B"
289
+ }
290
+ ]
291
+ }'
292
+ ```
293
+
294
+ 下面这种写法会失败,这属于预期行为:
295
+
296
+ ```bash
297
+ mcporter call omnifocus.batch_add_items --args '{
298
+ "items": [
299
+ {
300
+ "type": "task",
301
+ "name": "子任务:A1",
302
+ "projectName": "OmniFocus MCP 批量测试",
303
+ "parentTaskName": "父任务:分类A"
304
+ }
305
+ ]
306
+ }'
307
+ ```
308
+
309
+ 因为子任务必须继承父任务所在项目,不能再单独传 `projectName`。
310
+
215
311
  ### 6. 🖼️ 附件查看
216
312
 
217
313
  先读取任务和附件元信息,再按需打开具体附件:
@@ -12,9 +12,9 @@ export const schema = z.object({
12
12
  estimatedMinutes: z.number().optional().describe("Estimated time to complete the item, in minutes"),
13
13
  tags: z.array(z.string()).optional().describe("Tags to assign to the item"),
14
14
  // Task-specific properties
15
- projectName: z.string().optional().describe("For tasks: The name of the project to add the task to"),
16
- parentTaskId: z.string().optional().describe("For tasks: The ID of the parent task to create this task as a subtask"),
17
- parentTaskName: z.string().optional().describe("For tasks: The name of the parent task to create this task as a subtask"),
15
+ projectName: z.string().optional().describe("For tasks: The project name for top-level tasks. Omit this when parentTaskId or parentTaskName is set."),
16
+ parentTaskId: z.string().optional().describe("For tasks: The parent task ID for subtasks. When this is set, do not also provide projectName."),
17
+ parentTaskName: z.string().optional().describe("For tasks: The parent task name for subtasks. Subtasks inherit project from their parent, so do not also provide projectName."),
18
18
  // Project-specific properties
19
19
  folderName: z.string().optional().describe("For projects: The name of the folder to add the project to"),
20
20
  sequential: z.boolean().optional().describe("For projects: Whether tasks in the project should be sequential")
@@ -37,6 +37,13 @@ test('batch_add_items schema preserves plannedDate for task and project', () =>
37
37
  assert.equal(parsed.items[0].plannedDate, '2026-02-14');
38
38
  assert.equal(parsed.items[1].plannedDate, '2026-02-15');
39
39
  });
40
+ test('batch_add_items schema documents subtask project inheritance', () => {
41
+ const itemSchema = batchAddItemsSchema.shape.items._def.type;
42
+ const projectNameDescription = itemSchema.shape.projectName.description;
43
+ const parentTaskNameDescription = itemSchema.shape.parentTaskName.description;
44
+ assert.match(projectNameDescription, /omit this when parentTaskId or parentTaskName is set/i);
45
+ assert.match(parentTaskNameDescription, /subtasks inherit project from their parent/i);
46
+ });
40
47
  test('edit_item schema preserves newPlannedDate', () => {
41
48
  const parsed = editItemSchema.parse({
42
49
  itemType: 'task',
@@ -1,11 +1,13 @@
1
1
  import { executeAppleScript } from '../../utils/scriptExecution.js';
2
2
  import { appleScriptDateCode } from '../../utils/dateFormatter.js';
3
+ import { buildAppleScriptJsonHelpers } from '../../utils/appleScriptJson.js';
4
+ import { escapeAppleScriptString } from '../../utils/appleScriptString.js';
3
5
  export function buildTagAssignmentScript(tags, targetVar) {
4
6
  if (!tags || tags.length === 0) {
5
7
  return '';
6
8
  }
7
9
  return tags.map(tag => {
8
- const sanitizedTag = tag.replace(/['"\\]/g, '\\$&');
10
+ const sanitizedTag = escapeAppleScriptString(tag);
9
11
  return `
10
12
  try
11
13
  set theTag to missing value
@@ -26,8 +28,8 @@ export function buildTagAssignmentScript(tags, targetVar) {
26
28
  */
27
29
  export function generateAppleScript(params) {
28
30
  // Sanitize and prepare parameters for AppleScript
29
- const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
30
- const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
31
+ const name = escapeAppleScriptString(params.name);
32
+ const note = params.note ? escapeAppleScriptString(params.note) : '';
31
33
  // Build date variables outside OmniFocus tell block to avoid locale parsing issues.
32
34
  const dueDateCode = params.dueDate ? appleScriptDateCode(params.dueDate, 'dueDateValue') : '';
33
35
  const deferDateCode = params.deferDate ? appleScriptDateCode(params.deferDate, 'deferDateValue') : '';
@@ -36,12 +38,14 @@ export function generateAppleScript(params) {
36
38
  const flagged = params.flagged === true;
37
39
  const estimatedMinutes = params.estimatedMinutes?.toString() || '';
38
40
  const tags = params.tags || [];
39
- const projectName = params.projectName?.replace(/['"\\]/g, '\\$&') || '';
40
- const parentTaskId = params.parentTaskId?.replace(/['"\\]/g, '\\$&') || '';
41
- const parentTaskName = params.parentTaskName?.replace(/['"\\]/g, '\\$&') || '';
41
+ const projectName = params.projectName ? escapeAppleScriptString(params.projectName) : '';
42
+ const parentTaskId = params.parentTaskId ? escapeAppleScriptString(params.parentTaskId) : '';
43
+ const parentTaskName = params.parentTaskName ? escapeAppleScriptString(params.parentTaskName) : '';
44
+ const jsonHelpers = buildAppleScriptJsonHelpers();
42
45
  const tagAssignmentScript = buildTagAssignmentScript(tags, 'newTask');
43
46
  // Construct AppleScript with error handling
44
47
  let script = `
48
+ ${jsonHelpers}
45
49
  try
46
50
  ${datePreamble}
47
51
  tell application "OmniFocus"
@@ -53,7 +57,7 @@ ${datePreamble}
53
57
  set theParentTask to first flattened task where id = "${parentTaskId}"
54
58
  set newTask to make new task with properties {name:"${name}"} at end of tasks of theParentTask
55
59
  on error
56
- return "{\\\"success\\\":false,\\\"error\\\":\\\"Parent task not found with ID: ${parentTaskId}\\\"}"
60
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape("Parent task not found with ID: ${parentTaskId}") & "\\\"}"
57
61
  end try
58
62
  else if "${parentTaskName}" is not "" then
59
63
  -- Create subtask using parent task name
@@ -61,7 +65,7 @@ ${datePreamble}
61
65
  set theParentTask to first flattened task where name = "${parentTaskName}"
62
66
  set newTask to make new task with properties {name:"${name}"} at end of tasks of theParentTask
63
67
  on error
64
- return "{\\\"success\\\":false,\\\"error\\\":\\\"Parent task not found with name: ${parentTaskName}\\\"}"
68
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape("Parent task not found with name: ${parentTaskName}") & "\\\"}"
65
69
  end try
66
70
  else if "${projectName}" is not "" then
67
71
  -- Use specified project
@@ -69,7 +73,7 @@ ${datePreamble}
69
73
  set theProject to first flattened project where name = "${projectName}"
70
74
  set newTask to make new task with properties {name:"${name}"} at end of tasks of theProject
71
75
  on error
72
- return "{\\\"success\\\":false,\\\"error\\\":\\\"Project not found: ${projectName}\\\"}"
76
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape("Project not found: ${projectName}") & "\\\"}"
73
77
  end try
74
78
  else
75
79
  -- Use inbox of the document
@@ -86,16 +90,17 @@ ${datePreamble}
86
90
 
87
91
  -- Get the task ID
88
92
  set taskId to id of newTask as string
93
+ set taskNameValue to name of newTask
89
94
 
90
95
  -- Add tags if provided
91
96
  ${tagAssignmentScript}
92
97
 
93
98
  -- Return success with task ID
94
- return "{\\\"success\\\":true,\\\"taskId\\\":\\"" & taskId & "\\",\\\"name\\\":\\"${name}\\"}"
99
+ return "{\\\"success\\\":true,\\\"taskId\\\":\\"" & my jsonEscape(taskId) & "\\",\\\"name\\\":\\"" & my jsonEscape(taskNameValue) & "\\\"}"
95
100
  end tell
96
101
  end tell
97
102
  on error errorMessage
98
- return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
103
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape(errorMessage) & "\\\"}"
99
104
  end try
100
105
  `;
101
106
  return script;
@@ -25,3 +25,21 @@ test('generateAppleScript builds date variables before OmniFocus tell block', ()
25
25
  assert.doesNotMatch(script, /set defer date of newTask to date "/);
26
26
  assert.doesNotMatch(script, /set planned date of newTask to date "/);
27
27
  });
28
+ test('generateAppleScript keeps apostrophes and doubles backslashes in task text fields', () => {
29
+ const script = generateAppleScript({
30
+ name: "Review client's \\ draft",
31
+ note: "Check Bob's file in C:\\Temp"
32
+ });
33
+ assert.match(script, /make new inbox task with properties \{name:"Review client's \\\\ draft"\}/);
34
+ assert.match(script, /set note of newTask to "Check Bob's file in C:\\\\Temp"/);
35
+ assert.doesNotMatch(script, /\\'/);
36
+ });
37
+ test('generateAppleScript escapes JSON response values through AppleScript helper', () => {
38
+ const script = generateAppleScript({
39
+ name: "Review client's \\ draft"
40
+ });
41
+ assert.match(script, /on jsonEscape\(inputText\)/);
42
+ assert.match(script, /set taskNameValue to name of newTask/);
43
+ assert.match(script, /my jsonEscape\(taskId\)/);
44
+ assert.match(script, /my jsonEscape\(taskNameValue\)/);
45
+ });
@@ -1,12 +1,14 @@
1
1
  import { executeAppleScript } from '../../utils/scriptExecution.js';
2
2
  import { appleScriptDateCode } from '../../utils/dateFormatter.js';
3
+ import { buildAppleScriptJsonHelpers } from '../../utils/appleScriptJson.js';
4
+ import { escapeAppleScriptString } from '../../utils/appleScriptString.js';
3
5
  /**
4
6
  * Generate pure AppleScript for project creation
5
7
  */
6
8
  export function generateAppleScript(params) {
7
9
  // Sanitize and prepare parameters for AppleScript
8
- const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
9
- const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
10
+ const name = escapeAppleScriptString(params.name);
11
+ const note = params.note ? escapeAppleScriptString(params.note) : '';
10
12
  // Build date variables outside OmniFocus tell block to avoid locale parsing issues.
11
13
  const dueDateCode = params.dueDate ? appleScriptDateCode(params.dueDate, 'dueDateValue') : '';
12
14
  const deferDateCode = params.deferDate ? appleScriptDateCode(params.deferDate, 'deferDateValue') : '';
@@ -15,11 +17,12 @@ export function generateAppleScript(params) {
15
17
  const flagged = params.flagged === true;
16
18
  const estimatedMinutes = params.estimatedMinutes?.toString() || '';
17
19
  const tags = params.tags || [];
18
- const folderName = params.folderName?.replace(/['"\\]/g, '\\$&') || '';
20
+ const folderName = params.folderName ? escapeAppleScriptString(params.folderName) : '';
19
21
  const sequential = params.sequential === true;
22
+ const jsonHelpers = buildAppleScriptJsonHelpers();
20
23
  const tagAssignmentScript = tags.length > 0
21
24
  ? tags.map(tag => {
22
- const sanitizedTag = tag.replace(/['"\\]/g, '\\$&');
25
+ const sanitizedTag = escapeAppleScriptString(tag);
23
26
  return `
24
27
  try
25
28
  set theTag to missing value
@@ -37,6 +40,7 @@ export function generateAppleScript(params) {
37
40
  : '';
38
41
  // Construct AppleScript with error handling
39
42
  let script = `
43
+ ${jsonHelpers}
40
44
  try
41
45
  ${datePreamble}
42
46
  tell application "OmniFocus"
@@ -51,7 +55,7 @@ ${datePreamble}
51
55
  set theFolder to first flattened folder where name = "${folderName}"
52
56
  set newProject to make new project with properties {name:"${name}"} at end of projects of theFolder
53
57
  on error
54
- return "{\\\"success\\\":false,\\\"error\\\":\\\"Folder not found: ${folderName}\\\"}"
58
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape("Folder not found: ${folderName}") & "\\\"}"
55
59
  end try
56
60
  end if
57
61
 
@@ -66,16 +70,17 @@ ${datePreamble}
66
70
 
67
71
  -- Get the project ID
68
72
  set projectId to id of newProject as string
73
+ set projectNameValue to name of newProject
69
74
 
70
75
  -- Add tags if provided
71
76
  ${tagAssignmentScript}
72
77
 
73
78
  -- Return success with project ID
74
- return "{\\\"success\\\":true,\\\"projectId\\\":\\"" & projectId & "\\",\\\"name\\\":\\"${name}\\"}"
79
+ return "{\\\"success\\\":true,\\\"projectId\\\":\\"" & my jsonEscape(projectId) & "\\",\\\"name\\\":\\"" & my jsonEscape(projectNameValue) & "\\\"}"
75
80
  end tell
76
81
  end tell
77
82
  on error errorMessage
78
- return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
83
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape(errorMessage) & "\\\"}"
79
84
  end try
80
85
  `;
81
86
  return script;
@@ -18,3 +18,21 @@ test('addProject date handling uses preamble variables outside OmniFocus tell bl
18
18
  assert.doesNotMatch(script, /set defer date of newProject to date "/);
19
19
  assert.doesNotMatch(script, /set planned date of newProject to date "/);
20
20
  });
21
+ test('addProject keeps apostrophes and doubles backslashes in text fields', () => {
22
+ const script = generateAppleScript({
23
+ name: "Client's \\ Roadmap",
24
+ note: "Things that didn't work in C:\\Temp"
25
+ });
26
+ assert.match(script, /name:"Client's \\\\ Roadmap"/);
27
+ assert.match(script, /set note of newProject to "Things that didn't work in C:\\\\Temp"/);
28
+ assert.doesNotMatch(script, /\\'/);
29
+ });
30
+ test('addProject escapes JSON response values through AppleScript helper', () => {
31
+ const script = generateAppleScript({
32
+ name: "Client's \\ Roadmap"
33
+ });
34
+ assert.match(script, /on jsonEscape\(inputText\)/);
35
+ assert.match(script, /set projectNameValue to name of newProject/);
36
+ assert.match(script, /my jsonEscape\(projectId\)/);
37
+ assert.match(script, /my jsonEscape\(projectNameValue\)/);
38
+ });
@@ -1,5 +1,17 @@
1
1
  import { addOmniFocusTask } from './addOmniFocusTask.js';
2
2
  import { addProject } from './addProject.js';
3
+ function validateBatchItem(item, index) {
4
+ if (item.type !== 'task') {
5
+ return undefined;
6
+ }
7
+ if (item.parentTaskId && item.parentTaskName) {
8
+ return `Item ${index + 1} ("${item.name}"): Use only one parent reference. Provide parentTaskId or parentTaskName, not both.`;
9
+ }
10
+ if ((item.parentTaskId || item.parentTaskName) && item.projectName) {
11
+ return `Item ${index + 1} ("${item.name}"): Do not provide projectName when parentTaskId or parentTaskName is set. Subtasks inherit project from their parent.`;
12
+ }
13
+ return undefined;
14
+ }
3
15
  /**
4
16
  * Add multiple items (tasks or projects) to OmniFocus
5
17
  */
@@ -8,8 +20,16 @@ export async function batchAddItems(items) {
8
20
  // Results array to track individual operation outcomes
9
21
  const results = [];
10
22
  // Process each item in sequence
11
- for (const item of items) {
23
+ for (const [index, item] of items.entries()) {
12
24
  try {
25
+ const validationError = validateBatchItem(item, index);
26
+ if (validationError) {
27
+ results.push({
28
+ success: false,
29
+ error: validationError
30
+ });
31
+ continue;
32
+ }
13
33
  if (item.type === 'task') {
14
34
  // Extract task-specific params
15
35
  const taskParams = {
@@ -0,0 +1,17 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { batchAddItems } from './batchAddItems.js';
4
+ test('batchAddItems returns a clear validation error when a subtask also includes projectName', async () => {
5
+ const result = await batchAddItems([
6
+ {
7
+ type: 'task',
8
+ name: 'Child task',
9
+ projectName: 'Demo Project',
10
+ parentTaskName: 'Parent task'
11
+ }
12
+ ]);
13
+ assert.equal(result.success, false);
14
+ assert.equal(result.results.length, 1);
15
+ assert.equal(result.results[0].success, false);
16
+ assert.match(result.results[0].error || '', /Item 1 \("Child task"\): Do not provide projectName when parentTaskId or parentTaskName is set/);
17
+ });