UNPKG

omnifocus-mcp-enhanced

Version:

🚀 NEW: Native Custom Perspective Access! Enhanced MCP server with OmniFocus custom perspective support, hierarchical task display, AI-optimized tool selection, and comprehensive task management

329 lines (323 loc) • 11.9 kB
import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); /** * Generate pure AppleScript for item editing */ function generateAppleScript(params) { // Sanitize and prepare parameters for AppleScript const id = params.id?.replace(/['"\\]/g, '\\$&') || ''; // Escape quotes and backslashes const name = params.name?.replace(/['"\\]/g, '\\$&') || ''; const itemType = params.itemType; // Verify we have at least one identifier if (!id && !name) { return `return "{\\\"success\\\":false,\\\"error\\\":\\\"Either id or name must be provided\\\"}"`; } // Construct AppleScript with error handling let script = ` try tell application "OmniFocus" tell front document -- Find the item to edit set foundItem to missing value `; // Add ID search if provided if (id) { script += ` -- Try to find by ID first try set foundItem to first ${itemType === 'task' ? 'flattened task' : 'flattened project'} where id = "${id}" end try `; } // Add name search if provided (and no ID or as fallback) if (!id && name) { script += ` -- Find by name try set foundItem to first ${itemType === 'task' ? 'flattened task' : 'flattened project'} where name = "${name}" end try `; } else if (id && name) { script += ` -- If ID search failed, try to find by name as fallback if foundItem is missing value then try set foundItem to first ${itemType === 'task' ? 'flattened task' : 'flattened project'} where name = "${name}" end try end if `; } // Add the item editing logic script += ` -- If we found the item, edit it if foundItem is not missing value then set itemName to name of foundItem set itemId to id of foundItem as string set changedProperties to {} `; // Common property updates for both tasks and projects if (params.newName !== undefined) { script += ` -- Update name set name of foundItem to "${params.newName.replace(/['"\\]/g, '\\$&')}" set end of changedProperties to "name" `; } if (params.newNote !== undefined) { script += ` -- Update note set note of foundItem to "${params.newNote.replace(/['"\\]/g, '\\$&')}" set end of changedProperties to "note" `; } if (params.newDueDate !== undefined) { if (params.newDueDate === "") { script += ` -- Clear due date set due date of foundItem to missing value set end of changedProperties to "due date" `; } else { script += ` -- Update due date set due date of foundItem to (current date) + ((((date "${params.newDueDate}") - (current date)) / days) * days) set end of changedProperties to "due date" `; } } if (params.newDeferDate !== undefined) { if (params.newDeferDate === "") { script += ` -- Clear defer date set defer date of foundItem to missing value set end of changedProperties to "defer date" `; } else { script += ` -- Update defer date set defer date of foundItem to (current date) + ((((date "${params.newDeferDate}") - (current date)) / days) * days) set end of changedProperties to "defer date" `; } } if (params.newFlagged !== undefined) { script += ` -- Update flagged status set flagged of foundItem to ${params.newFlagged} set end of changedProperties to "flagged" `; } if (params.newEstimatedMinutes !== undefined) { script += ` -- Update estimated minutes set estimated minutes of foundItem to ${params.newEstimatedMinutes} set end of changedProperties to "estimated minutes" `; } // Task-specific updates if (itemType === 'task') { // Update task status if (params.newStatus !== undefined) { if (params.newStatus === 'completed') { script += ` -- Mark task as completed set completed of foundItem to true set end of changedProperties to "status (completed)" `; } else if (params.newStatus === 'dropped') { script += ` -- Mark task as dropped set dropped of foundItem to true set end of changedProperties to "status (dropped)" `; } else if (params.newStatus === 'incomplete') { script += ` -- Mark task as incomplete set completed of foundItem to false set dropped of foundItem to false set end of changedProperties to "status (incomplete)" `; } } // Handle tag operations if (params.replaceTags && params.replaceTags.length > 0) { const tagsList = params.replaceTags.map(tag => `"${tag.replace(/['"\\]/g, '\\$&')}"`).join(", "); script += ` -- Replace all tags set tagNames to {${tagsList}} set existingTags to tags of foundItem -- First clear all existing tags repeat with existingTag in existingTags tell existingTag to remove tag from foundItem end repeat -- Then add new tags repeat with tagName in tagNames set tagObj to missing value try set tagObj to first flattened tag where name = tagName end try if tagObj is missing value then set tagObj to make new tag with properties {name:tagName} end if tell tagObj to add tag to foundItem end repeat set end of changedProperties to "tags (replaced)" `; } else { // Add tags if specified if (params.addTags && params.addTags.length > 0) { const tagsList = params.addTags.map(tag => `"${tag.replace(/['"\\]/g, '\\$&')}"`).join(", "); script += ` -- Add tags set tagNames to {${tagsList}} repeat with tagName in tagNames set tagObj to missing value try set tagObj to first flattened tag where name = tagName end try if tagObj is missing value then set tagObj to make new tag with properties {name:tagName} end if tell tagObj to add tag to foundItem end repeat set end of changedProperties to "tags (added)" `; } // Remove tags if specified if (params.removeTags && params.removeTags.length > 0) { const tagsList = params.removeTags.map(tag => `"${tag.replace(/['"\\]/g, '\\$&')}"`).join(", "); script += ` -- Remove tags set tagNames to {${tagsList}} repeat with tagName in tagNames try set tagObj to first flattened tag where name = tagName tell tagObj to remove tag from foundItem end try end repeat set end of changedProperties to "tags (removed)" `; } } } // Project-specific updates if (itemType === 'project') { // Update sequential status if (params.newSequential !== undefined) { script += ` -- Update sequential status set sequential of foundItem to ${params.newSequential} set end of changedProperties to "sequential" `; } // Update project status if (params.newProjectStatus !== undefined) { const statusValue = params.newProjectStatus === 'active' ? 'active status' : params.newProjectStatus === 'completed' ? 'done status' : params.newProjectStatus === 'dropped' ? 'dropped status' : 'on hold status'; script += ` -- Update project status set status of foundItem to ${statusValue} set end of changedProperties to "status" `; } // Move to a new folder if (params.newFolderName !== undefined) { const folderName = params.newFolderName.replace(/['"\\]/g, '\\$&'); script += ` -- Move to new folder set destFolder to missing value try set destFolder to first flattened folder where name = "${folderName}" end try if destFolder is missing value then -- Create the folder if it doesn't exist set destFolder to make new folder with properties {name:"${folderName}"} end if -- Move project to the folder move foundItem to destFolder set end of changedProperties to "folder" `; } } script += ` -- Prepare the changed properties as a string set changedPropsText to "" repeat with i from 1 to count of changedProperties set changedPropsText to changedPropsText & item i of changedProperties if i < count of changedProperties then set changedPropsText to changedPropsText & ", " end if end repeat -- Return success with details return "{\\\"success\\\":true,\\\"id\\\":\\"" & itemId & "\\",\\\"name\\\":\\"" & itemName & "\\",\\\"changedProperties\\\":\\"" & changedPropsText & "\\"}" else -- Item not found return "{\\\"success\\\":false,\\\"error\\\":\\\"Item not found\\\"}" end if end tell end tell on error errorMessage return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}" end try `; return script; } /** * Edit a task or project in OmniFocus */ export async function editItem(params) { try { // Generate AppleScript const script = generateAppleScript(params); console.error("Executing AppleScript for editing..."); console.error(`Item type: ${params.itemType}, ID: ${params.id || 'not provided'}, Name: ${params.name || 'not provided'}`); // Log a preview of the script for debugging (first few lines) const scriptPreview = script.split('\n').slice(0, 10).join('\n') + '\n...'; console.error("AppleScript preview:\n", scriptPreview); // Execute AppleScript directly const { stdout, stderr } = await execAsync(`osascript -e '${script}'`); if (stderr) { console.error("AppleScript stderr:", stderr); } console.error("AppleScript stdout:", stdout); // Parse the result try { const result = JSON.parse(stdout); // Return the result return { success: result.success, id: result.id, name: result.name, changedProperties: result.changedProperties, error: result.error }; } catch (parseError) { console.error("Error parsing AppleScript result:", parseError); return { success: false, error: `Failed to parse result: ${stdout}` }; } } catch (error) { console.error("Error in editItem execution:", error); // Include more detailed error information if (error.message && error.message.includes('syntax error')) { console.error("This appears to be an AppleScript syntax error. Review the script generation logic."); } return { success: false, error: error?.message || "Unknown error in editItem" }; } }