omnifocus-mcp
Version:
Model Context Protocol (MCP) server that integrates with OmniFocus for AI assistant interaction
182 lines (179 loc) • 6.56 kB
JavaScript
import { writeFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { escapeAppleScriptString, generateOccurrenceGuardScript, JSON_ESCAPE_HANDLER, } from '../../utils/appleScriptHelpers.js';
import { runOsascriptFile } from '../../utils/scriptExecution.js';
/**
* Generate pure AppleScript for item removal
*/
export function generateAppleScript(params) {
// Sanitize and prepare parameters for AppleScript
const id = params.id ? escapeAppleScriptString(params.id) : '';
const name = params.name ? escapeAppleScriptString(params.name) : '';
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 = JSON_ESCAPE_HANDLER + `
try
tell application "OmniFocus"
tell front document
-- Find the item to remove
set foundItem to missing value
`;
// Add ID search if provided — whose clause gives direct references
if (id) {
if (itemType === 'task') {
script += `
-- Find task by ID (flattened tasks includes inbox tasks)
try
set foundItem to first flattened task whose id is "${id}"
end try
`;
}
else {
script += `
-- Find project by ID
try
set foundItem to first flattened project whose id is "${id}"
end try
`;
}
}
// Add name search if provided (and no ID or as fallback)
if (!id && name) {
if (itemType === 'task') {
script += `
-- Find task by name (flattened tasks includes inbox tasks)
try
set foundItem to first flattened task whose name is "${name}"
end try
`;
}
else {
script += `
-- Find project by name
try
set foundItem to first flattened project whose name is "${name}"
end try
`;
}
}
else if (id && name) {
if (itemType === 'task') {
script += `
-- If ID search failed, try to find by name as fallback
if foundItem is missing value then
try
set foundItem to first flattened task whose name is "${name}"
end try
end if
`;
}
else {
script += `
-- If ID search failed, try to find project by name as fallback
if foundItem is missing value then
try
set foundItem to first flattened project whose name is "${name}"
end try
end if
`;
}
}
// Add the rest of the script
script += `
-- If we found the item, remove it
if foundItem is not missing value then
set itemName to name of foundItem
set itemId to id of foundItem as string
${params.allowPastOccurrence ? '' : generateOccurrenceGuardScript('foundItem', `{\\"success\\":false,\\"error\\":\\"This is a completed occurrence of a repeating item, not a duplicate. Mutating it can cascade through the live repeat chain. Query without includeCompleted to get the live occurrence, or pass allowPastOccurrence: true if you really mean this one.\\"}`)}
-- Delete the item
delete foundItem
-- Return success. itemName only exists at script runtime, so it goes
-- through the jsonEscape handler — quotes in an item name would
-- otherwise corrupt the payload (#103).
return "{\\\"success\\\":true,\\\"id\\\":\\"" & itemId & "\\",\\\"name\\\":\\"" & my jsonEscape(itemName) & "\\"}"
else
-- Item not found
return "{\\\"success\\\":false,\\\"error\\\":\\\"Item not found\\\"}"
end if
end tell
end tell
on error errorMessage
return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape(errorMessage) & "\\"}"
end try
`;
return script;
}
/**
* Remove a task or project from OmniFocus
*/
export async function removeItem(params) {
let tempFile;
try {
// Generate AppleScript
const script = generateAppleScript(params);
console.error("Executing AppleScript for removal...");
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);
// Write script to temporary file to avoid shell escaping issues
tempFile = join(tmpdir(), `remove_omnifocus_${crypto.randomUUID()}.applescript`);
writeFileSync(tempFile, script);
// Execute AppleScript from file
const { stdout, stderr } = await runOsascriptFile(tempFile);
// Clean up temp file
try {
unlinkSync(tempFile);
}
catch (cleanupError) {
console.error("Failed to clean up temp file:", cleanupError);
}
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,
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 (cleanupError) {
// Ignore cleanup errors
}
}
console.error("Error in removeItem 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 removeItem"
};
}
}