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"
};
}
}