UNPKG

omnifocus-mcp

Version:

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

163 lines (158 loc) 6.55 kB
import { writeFileSync, unlinkSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { createDateOutsideTellBlock } from '../../utils/dateFormatting.js'; import { escapeAppleScriptString, escapeForJsonInAppleScript, generateFolderLookupScript, JSON_ESCAPE_HANDLER, } from '../../utils/appleScriptHelpers.js'; import { repetitionRuleRecord } from '../../utils/repetitionRule.js'; import { runOsascriptFile } from '../../utils/scriptExecution.js'; /** * Generate pure AppleScript for project creation */ export function generateAppleScript(params) { // Sanitize and prepare parameters for AppleScript const name = escapeAppleScriptString(params.name); const note = params.note ? escapeAppleScriptString(params.note, { preserveNewlines: true }) : ''; const dueDate = params.dueDate || ''; const deferDate = params.deferDate || ''; const flagged = params.flagged === true; const estimatedMinutes = params.estimatedMinutes?.toString() || ''; const tags = params.tags || []; const sequential = params.sequential === true; // Generate date constructions outside tell blocks let datePreScript = ''; let dueDateVar = ''; let deferDateVar = ''; if (dueDate) { dueDateVar = `dueDate${Math.random().toString(36).substr(2, 9)}`; datePreScript += createDateOutsideTellBlock(dueDate, dueDateVar) + '\n\n'; } if (deferDate) { deferDateVar = `deferDate${Math.random().toString(36).substr(2, 9)}`; datePreScript += createDateOutsideTellBlock(deferDate, deferDateVar) + '\n\n'; } // Build project creation block — either at root or in a folder let projectCreationBlock; if (params.folderName) { // escapeForJsonInAppleScript, not escapeAppleScriptString: this name lands // inside a JSON payload, where an AppleScript-escaped quote re-materializes // raw and corrupts the result (#103). const errorJson = `{\\\"success\\\":false,\\\"error\\\":\\\"Folder not found: ${escapeForJsonInAppleScript(params.folderName)}\\\"}`; const folderLookup = generateFolderLookupScript(params.folderName, 'theFolder', errorJson); projectCreationBlock = ` -- Find the folder (supports nested paths like "Work/Engineering") ${folderLookup} set newProject to make new project with properties {name:"${name}"} at end of projects of theFolder`; } else { projectCreationBlock = ` -- Create project at the root level set newProject to make new project with properties {name:"${name}"}`; } // Construct AppleScript with error handling let script = JSON_ESCAPE_HANDLER + datePreScript + ` try tell application "OmniFocus" tell front document ${projectCreationBlock} -- Set project properties ${note ? `set note of newProject to "${note}"` : ''} ${dueDate ? ` -- Set due date set due date of newProject to ` + dueDateVar : ''} ${deferDate ? ` -- Set defer date set defer date of newProject to ` + deferDateVar : ''} ${flagged ? `set flagged of newProject to true` : ''} ${estimatedMinutes ? `set estimated minutes of newProject to ${estimatedMinutes}` : ''} ${`set sequential of newProject to ${sequential}`} ${params.repeat ? ` -- Set the repetition rule (#116). Whole-record assignment only; see -- the note in addOmniFocusTask. set repetition rule of newProject to ${repetitionRuleRecord(params.repeat)}` : ''} -- Get the project ID set projectId to id of newProject as string -- Add tags if provided ${tags.length > 0 ? tags.map(tag => { const sanitizedTag = escapeAppleScriptString(tag); return ` try set theTag to first flattened tag where name = "${sanitizedTag}" add theTag to tags of newProject on error -- Tag might not exist, try to create it try set theTag to make new tag with properties {name:"${sanitizedTag}"} add theTag to tags of newProject on error -- Could not create or add tag end try end try`; }).join('\n') : ''} -- Return success with project ID. The name is deliberately NOT echoed: -- the caller already knows it, and splicing it into hand-built JSON is -- how quotes in a name corrupted the payload (#103). return "{\\\"success\\\":true,\\\"projectId\\\":\\"" & projectId & "\\"}" end tell end tell on error errorMessage return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape(errorMessage) & "\\"}" end try `; return script; } /** * Add a project to OmniFocus */ export async function addProject(params) { let tempFile; try { // Generate AppleScript const script = generateAppleScript(params); console.error("Executing AppleScript via temp file..."); // Write to a temporary AppleScript file to avoid shell escaping issues tempFile = join(tmpdir(), `add_project_${crypto.randomUUID()}.applescript`); writeFileSync(tempFile, script, { encoding: 'utf8' }); // Execute AppleScript from file const { stdout, stderr } = await runOsascriptFile(tempFile); // Clean up temp file try { unlinkSync(tempFile); } catch { } 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, projectId: result.projectId, error: result.error }; } catch (parseError) { console.error("Error parsing AppleScript result:", parseError); return { success: false, error: `Failed to parse result: ${stdout}` }; } } catch (error) { // Clean up temp file if it exists if (tempFile) { try { unlinkSync(tempFile); } catch { } } console.error("Error in addProject:", error); return { success: false, error: error?.message || "Unknown error in addProject" }; } }