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.
- package/README.md +96 -0
- package/README.zh.md +96 -0
- package/dist/tools/definitions/batchAddItems.js +3 -3
- package/dist/tools/definitions/plannedDateSchemas.test.js +7 -0
- package/dist/tools/primitives/addOmniFocusTask.js +16 -11
- package/dist/tools/primitives/addOmniFocusTask.test.js +18 -0
- package/dist/tools/primitives/addProject.js +12 -7
- package/dist/tools/primitives/addProject.test.js +18 -0
- package/dist/tools/primitives/batchAddItems.js +21 -1
- package/dist/tools/primitives/batchAddItems.test.js +17 -0
- package/dist/tools/primitives/editItem.js +26 -22
- package/dist/tools/primitives/editItem.test.js +32 -0
- package/dist/tools/primitives/removeItem.js +8 -4
- package/dist/utils/appleScriptJson.js +27 -0
- package/dist/utils/appleScriptString.js +11 -0
- package/dist/utils/appleScriptString.test.js +7 -0
- package/docs/roadmap/2026-02-25-batch-move-tasks-plan.md +28 -0
- package/docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md +28 -0
- package/package.json +3 -3
- package/src/tools/definitions/batchAddItems.ts +3 -3
- package/src/tools/definitions/plannedDateSchemas.test.ts +9 -0
- package/src/tools/primitives/addOmniFocusTask.test.ts +22 -0
- package/src/tools/primitives/addOmniFocusTask.ts +16 -11
- package/src/tools/primitives/addProject.test.ts +22 -0
- package/src/tools/primitives/addProject.ts +12 -7
- package/src/tools/primitives/batchAddItems.test.ts +22 -0
- package/src/tools/primitives/batchAddItems.ts +27 -2
- package/src/tools/primitives/editItem.test.ts +38 -0
- package/src/tools/primitives/editItem.ts +26 -22
- package/src/tools/primitives/removeItem.ts +9 -5
- package/src/utils/appleScriptJson.ts +27 -0
- package/src/utils/appleScriptString.test.ts +9 -0
- 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
|
|
16
|
-
parentTaskId: z.string().optional().describe("For tasks: The ID
|
|
17
|
-
parentTaskName: z.string().optional().describe("For tasks: The name
|
|
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
|
|
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
|
|
30
|
-
const note = params.note
|
|
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
|
|
40
|
-
const parentTaskId = params.parentTaskId
|
|
41
|
-
const parentTaskName = params.parentTaskName
|
|
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\\\"
|
|
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\\\"
|
|
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\\\"
|
|
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\\\":\\"
|
|
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
|
|
9
|
-
const note = params.note
|
|
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
|
|
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
|
|
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\\\"
|
|
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\\\":\\"
|
|
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
|
+
});
|