UNPKG

omnifocus-mcp

Version:

Model Context Protocol (MCP) server that integrates with OmniFocus for AI assistant interaction

189 lines (188 loc) 8.85 kB
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' }; } }