omnifocus-mcp
Version:
Model Context Protocol (MCP) server that integrates with OmniFocus for AI assistant interaction
189 lines (188 loc) • 8.85 kB
JavaScript
import { addOmniFocusTask } from './addOmniFocusTask.js';
import { addProject } from './addProject.js';
/**
* Add multiple items (tasks or projects) to OmniFocus
*/
export async function batchAddItems(items) {
try {
const results = new Array(items.length);
const processed = new Array(items.length).fill(false);
const tempResolved = new Map();
// Pre-validate cycles in tempId -> parentTempId references
const tempIndex = new Map();
items.forEach((it, idx) => { if (it.tempId)
tempIndex.set(it.tempId, idx); });
// Detect cycles using DFS and capture cycle paths
const visiting = new Set();
const visited = new Set();
const inCycle = new Set();
const cycleMessageByTempId = new Map();
const stack = [];
function dfs(tempId) {
if (visited.has(tempId) || inCycle.has(tempId))
return;
if (visiting.has(tempId))
return; // already on stack, handled by caller
visiting.add(tempId);
stack.push(tempId);
const idx = tempIndex.get(tempId);
const parentTemp = items[idx].parentTempId;
if (parentTemp && tempIndex.has(parentTemp)) {
if (visiting.has(parentTemp)) {
// Found a cycle; construct path
const startIdx = stack.indexOf(parentTemp);
const cycleIds = stack.slice(startIdx).concat(parentTemp);
const cycleNames = cycleIds.map(tid => {
const i = tempIndex.get(tid);
return items[i].name || tid;
});
const pathText = `${cycleNames.join(' -> ')}`;
for (const tid of cycleIds) {
inCycle.add(tid);
cycleMessageByTempId.set(tid, `Cycle detected: ${pathText}`);
}
}
else {
dfs(parentTemp);
}
}
stack.pop();
visiting.delete(tempId);
visited.add(tempId);
}
for (const tid of tempIndex.keys())
dfs(tid);
// Mark items that participate in cycles as failed early
for (const tid of inCycle) {
const idx = tempIndex.get(tid);
const msg = cycleMessageByTempId.get(tid) || `Cycle detected involving tempId: ${tid}`;
results[idx] = { success: false, error: msg };
processed[idx] = true;
}
// Mark items with unknown parentTempId (and no explicit parentTaskId) as invalid early
items.forEach((it, idx) => {
if (processed[idx])
return;
if (it.parentTempId && !tempIndex.has(it.parentTempId) && !it.parentTaskId) {
results[idx] = { success: false, error: `Unknown parentTempId: ${it.parentTempId}` };
processed[idx] = true;
}
});
// Stable order: sort by hierarchyLevel (undefined -> 0), then original index
const indexed = items.map((it, idx) => ({ ...it, __index: idx }));
indexed.sort((a, b) => (a.hierarchyLevel ?? 0) - (b.hierarchyLevel ?? 0) || a.__index - b.__index);
let madeProgress = true;
while (processed.some(p => !p) && madeProgress) {
madeProgress = false;
for (const item of indexed) {
const i = item.__index;
if (processed[i])
continue;
try {
if (item.type === 'project') {
const projectParams = {
name: item.name,
note: item.note,
dueDate: item.dueDate,
deferDate: item.deferDate,
flagged: item.flagged,
estimatedMinutes: item.estimatedMinutes,
tags: item.tags,
folderName: item.folderName,
sequential: item.sequential,
repeat: item.repeat
};
const projectResult = await addProject(projectParams);
results[i] = {
success: projectResult.success,
id: projectResult.projectId,
error: projectResult.error
};
if (item.tempId && projectResult.projectId && projectResult.success) {
tempResolved.set(item.tempId, { id: projectResult.projectId, type: 'project', name: item.name });
}
processed[i] = true;
madeProgress = true;
continue;
}
// task
let parentTaskId = item.parentTaskId;
let projectId = item.projectId;
let projectName = item.projectName;
if (!parentTaskId && item.parentTempId) {
const resolved = tempResolved.get(item.parentTempId);
if (!resolved) {
// Parent not created yet; skip this round
continue;
}
if (resolved.type === 'project') {
// Target the project we just created by id, not by name. Both ids
// here are the AppleScript namespace (addProject returns `id of
// project`, which is what addOmniFocusTask looks up), and a batch
// that creates a project whose name already exists elsewhere would
// otherwise attach children to the wrong one.
projectId = resolved.id;
projectName = undefined;
}
else {
parentTaskId = resolved.id;
}
}
const taskParams = {
name: item.name,
note: item.note,
dueDate: item.dueDate,
deferDate: item.deferDate,
plannedDate: item.plannedDate,
flagged: item.flagged,
estimatedMinutes: item.estimatedMinutes,
tags: item.tags,
projectId,
projectName,
parentTaskId,
parentTaskName: item.parentTaskName,
hierarchyLevel: item.hierarchyLevel,
repeat: item.repeat
};
const taskResult = await addOmniFocusTask(taskParams);
results[i] = {
success: taskResult.success,
id: taskResult.taskId,
error: taskResult.error,
placement: taskResult.placement
};
if (item.tempId && taskResult.taskId && taskResult.success) {
tempResolved.set(item.tempId, { id: taskResult.taskId, type: 'task', name: item.name });
}
processed[i] = true;
madeProgress = true;
}
catch (itemError) {
results[i] = {
success: false,
error: itemError?.message || 'Unknown error processing item'
};
processed[i] = true; // avoid infinite loop on thrown errors
madeProgress = true;
}
}
}
// Any unprocessed due to dependencies/cycles -> fail with message
for (const item of indexed) {
const i = item.__index;
if (!processed[i]) {
const reason = item.parentTempId && !tempResolved.has(item.parentTempId)
? `Unresolved parentTempId: ${item.parentTempId}`
: 'Unresolved dependency or cycle';
results[i] = { success: false, error: reason };
processed[i] = true;
}
}
const overallSuccess = results.some(r => r?.success);
return { success: overallSuccess, results };
}
catch (error) {
console.error('Error in batchAddItems:', error);
return { success: false, results: [], error: error?.message || 'Unknown error in batchAddItems' };
}
}