Skip to main content
Glama

OmniFocus MCP Enhanced

by jqlts1
editItem.tsโ€ข13 kB
import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); // Status options for tasks and projects type TaskStatus = 'incomplete' | 'completed' | 'dropped'; type ProjectStatus = 'active' | 'completed' | 'dropped' | 'onHold'; // Interface for item edit parameters export interface EditItemParams { id?: string; // ID of the task or project to edit name?: string; // Name of the task or project to edit (as fallback if ID not provided) itemType: 'task' | 'project'; // Type of item to edit // Common editable fields newName?: string; // New name for the item newNote?: string; // New note for the item newDueDate?: string; // New due date in ISO format (empty string to clear) newDeferDate?: string; // New defer date in ISO format (empty string to clear) newFlagged?: boolean; // New flagged status (false to remove flag, true to add flag) newEstimatedMinutes?: number; // New estimated minutes // Task-specific fields newStatus?: TaskStatus; // New status for tasks (incomplete, completed, dropped) addTags?: string[]; // Tags to add to the task removeTags?: string[]; // Tags to remove from the task replaceTags?: string[]; // Tags to replace all existing tags with // Project-specific fields newSequential?: boolean; // Whether the project should be sequential newFolderName?: string; // New folder to move the project to newProjectStatus?: ProjectStatus; // New status for projects } /** * Generate pure AppleScript for item editing */ function generateAppleScript(params: EditItemParams): string { // 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: EditItemParams): Promise<{ success: boolean, id?: string, name?: string, changedProperties?: string, error?: string }> { 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: any) { 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" }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jqlts1/omnifocus-mcp-enhanced'

If you have feedback or need assistance with the MCP directory API, please join our Discord server