@iflow-mcp/omnifocus-mcp 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QUERY_TOOL_EXAMPLES.md +298 -0
- package/QUERY_TOOL_REFERENCE.md +228 -0
- package/README.md +250 -0
- package/assets/omnifocus-mcp-logo.png +0 -0
- package/cli.cjs +9 -0
- package/dist/omnifocustypes.js +48 -0
- package/dist/server.js +44 -0
- package/dist/tools/definitions/addOmniFocusTask.js +76 -0
- package/dist/tools/definitions/addProject.js +61 -0
- package/dist/tools/definitions/batchAddItems.js +89 -0
- package/dist/tools/definitions/batchRemoveItems.js +74 -0
- package/dist/tools/definitions/dumpDatabase.js +259 -0
- package/dist/tools/definitions/editItem.js +88 -0
- package/dist/tools/definitions/getPerspectiveView.js +107 -0
- package/dist/tools/definitions/listPerspectives.js +65 -0
- package/dist/tools/definitions/queryOmnifocus.js +190 -0
- package/dist/tools/definitions/removeItem.js +80 -0
- package/dist/tools/dumpDatabase.js +121 -0
- package/dist/tools/dumpDatabaseOptimized.js +192 -0
- package/dist/tools/primitives/addOmniFocusTask.js +227 -0
- package/dist/tools/primitives/addProject.js +132 -0
- package/dist/tools/primitives/batchAddItems.js +166 -0
- package/dist/tools/primitives/batchRemoveItems.js +44 -0
- package/dist/tools/primitives/editItem.js +443 -0
- package/dist/tools/primitives/getPerspectiveView.js +50 -0
- package/dist/tools/primitives/listPerspectives.js +34 -0
- package/dist/tools/primitives/queryOmnifocus.js +365 -0
- package/dist/tools/primitives/queryOmnifocusDebug.js +135 -0
- package/dist/tools/primitives/removeItem.js +177 -0
- package/dist/types.js +1 -0
- package/dist/utils/cacheManager.js +187 -0
- package/dist/utils/dateFormatting.js +58 -0
- package/dist/utils/omnifocusScripts/getPerspectiveView.js +169 -0
- package/dist/utils/omnifocusScripts/listPerspectives.js +59 -0
- package/dist/utils/omnifocusScripts/omnifocusDump.js +223 -0
- package/dist/utils/scriptExecution.js +113 -0
- package/package.json +37 -0
- package/src/omnifocustypes.ts +89 -0
- package/src/server.ts +109 -0
- package/src/tools/definitions/addOmniFocusTask.ts +80 -0
- package/src/tools/definitions/addProject.ts +67 -0
- package/src/tools/definitions/batchAddItems.ts +98 -0
- package/src/tools/definitions/batchRemoveItems.ts +80 -0
- package/src/tools/definitions/dumpDatabase.ts +311 -0
- package/src/tools/definitions/editItem.ts +96 -0
- package/src/tools/definitions/getPerspectiveView.ts +125 -0
- package/src/tools/definitions/listPerspectives.ts +72 -0
- package/src/tools/definitions/queryOmnifocus.ts +212 -0
- package/src/tools/definitions/removeItem.ts +86 -0
- package/src/tools/dumpDatabase.ts +196 -0
- package/src/tools/dumpDatabaseOptimized.ts +231 -0
- package/src/tools/primitives/addOmniFocusTask.ts +252 -0
- package/src/tools/primitives/addProject.ts +156 -0
- package/src/tools/primitives/batchAddItems.ts +207 -0
- package/src/tools/primitives/batchRemoveItems.ts +64 -0
- package/src/tools/primitives/editItem.ts +507 -0
- package/src/tools/primitives/getPerspectiveView.ts +71 -0
- package/src/tools/primitives/listPerspectives.ts +53 -0
- package/src/tools/primitives/queryOmnifocus.ts +394 -0
- package/src/tools/primitives/queryOmnifocusDebug.ts +139 -0
- package/src/tools/primitives/removeItem.ts +195 -0
- package/src/types.ts +107 -0
- package/src/utils/cacheManager.ts +234 -0
- package/src/utils/dateFormatting.ts +81 -0
- package/src/utils/omnifocusScripts/getPerspectiveView.js +169 -0
- package/src/utils/omnifocusScripts/listPerspectives.js +59 -0
- package/src/utils/omnifocusScripts/omnifocusDump.js +223 -0
- package/src/utils/scriptExecution.ts +128 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// OmniJS script to get the current perspective view in OmniFocus
|
|
2
|
+
(() => {
|
|
3
|
+
try {
|
|
4
|
+
// Note: We can't easily switch perspectives via OmniJS
|
|
5
|
+
// We can only report what's currently visible in the window
|
|
6
|
+
|
|
7
|
+
// Get the current window and its perspective
|
|
8
|
+
const window = document.windows[0];
|
|
9
|
+
if (!window) {
|
|
10
|
+
return JSON.stringify({
|
|
11
|
+
success: false,
|
|
12
|
+
error: "No OmniFocus window is open"
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Get the current perspective
|
|
17
|
+
const currentPerspective = window.perspective;
|
|
18
|
+
let perspectiveName = "Unknown";
|
|
19
|
+
|
|
20
|
+
// Identify the perspective
|
|
21
|
+
if (currentPerspective) {
|
|
22
|
+
if (currentPerspective === Perspective.BuiltIn.Inbox) {
|
|
23
|
+
perspectiveName = "Inbox";
|
|
24
|
+
} else if (currentPerspective === Perspective.BuiltIn.Projects) {
|
|
25
|
+
perspectiveName = "Projects";
|
|
26
|
+
} else if (currentPerspective === Perspective.BuiltIn.Tags) {
|
|
27
|
+
perspectiveName = "Tags";
|
|
28
|
+
} else if (currentPerspective === Perspective.BuiltIn.Forecast) {
|
|
29
|
+
perspectiveName = "Forecast";
|
|
30
|
+
} else if (currentPerspective === Perspective.BuiltIn.Flagged) {
|
|
31
|
+
perspectiveName = "Flagged";
|
|
32
|
+
} else if (currentPerspective === Perspective.BuiltIn.Review) {
|
|
33
|
+
perspectiveName = "Review";
|
|
34
|
+
} else if (currentPerspective.name) {
|
|
35
|
+
// Custom perspective
|
|
36
|
+
perspectiveName = currentPerspective.name;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get visible items based on the perspective
|
|
41
|
+
const items = [];
|
|
42
|
+
const selection = window.selection;
|
|
43
|
+
const selectedTasks = selection.tasks;
|
|
44
|
+
const selectedProjects = selection.projects;
|
|
45
|
+
|
|
46
|
+
// Helper function to format dates
|
|
47
|
+
function formatDate(date) {
|
|
48
|
+
if (!date) return null;
|
|
49
|
+
return date.toISOString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Helper to get task details
|
|
53
|
+
function getTaskDetails(task) {
|
|
54
|
+
const details = {
|
|
55
|
+
id: task.id.primaryKey,
|
|
56
|
+
name: task.name,
|
|
57
|
+
completed: task.completed,
|
|
58
|
+
flagged: task.flagged,
|
|
59
|
+
note: task.note || '',
|
|
60
|
+
dueDate: formatDate(task.dueDate),
|
|
61
|
+
deferDate: formatDate(task.deferDate),
|
|
62
|
+
completionDate: formatDate(task.completionDate),
|
|
63
|
+
estimatedMinutes: task.estimatedMinutes
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Task status
|
|
67
|
+
const statusMap = {
|
|
68
|
+
[Task.Status.Available]: "Available",
|
|
69
|
+
[Task.Status.Blocked]: "Blocked",
|
|
70
|
+
[Task.Status.Completed]: "Completed",
|
|
71
|
+
[Task.Status.Dropped]: "Dropped",
|
|
72
|
+
[Task.Status.DueSoon]: "DueSoon",
|
|
73
|
+
[Task.Status.Next]: "Next",
|
|
74
|
+
[Task.Status.Overdue]: "Overdue"
|
|
75
|
+
};
|
|
76
|
+
details.taskStatus = statusMap[task.taskStatus] || "Unknown";
|
|
77
|
+
|
|
78
|
+
// Project context
|
|
79
|
+
const project = task.containingProject;
|
|
80
|
+
details.projectName = project ? project.name : null;
|
|
81
|
+
|
|
82
|
+
// Tags
|
|
83
|
+
details.tagNames = task.tags.map(tag => tag.name);
|
|
84
|
+
|
|
85
|
+
return details;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get project details
|
|
89
|
+
function getProjectDetails(project) {
|
|
90
|
+
return {
|
|
91
|
+
id: project.id.primaryKey,
|
|
92
|
+
name: project.name,
|
|
93
|
+
type: 'project',
|
|
94
|
+
status: project.status,
|
|
95
|
+
note: project.note || '',
|
|
96
|
+
flagged: project.flagged || false,
|
|
97
|
+
dueDate: formatDate(project.dueDate),
|
|
98
|
+
deferDate: formatDate(project.deferDate),
|
|
99
|
+
folderName: project.parentFolder ? project.parentFolder.name : null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Try to get content based on perspective type
|
|
104
|
+
if (perspectiveName === "Inbox") {
|
|
105
|
+
// Get inbox tasks - inbox is a global in OmniJS
|
|
106
|
+
inbox.forEach(task => {
|
|
107
|
+
items.push(getTaskDetails(task));
|
|
108
|
+
});
|
|
109
|
+
} else if (perspectiveName === "Projects") {
|
|
110
|
+
// Get all projects - using flattenedProjects global
|
|
111
|
+
flattenedProjects.forEach(project => {
|
|
112
|
+
if (project.status === Project.Status.Active) {
|
|
113
|
+
items.push(getProjectDetails(project));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
} else if (perspectiveName === "Tags") {
|
|
117
|
+
// Get tagged tasks - using flattenedTags global
|
|
118
|
+
flattenedTags.forEach(tag => {
|
|
119
|
+
tag.remainingTasks.forEach(task => {
|
|
120
|
+
const taskDetail = getTaskDetails(task);
|
|
121
|
+
if (!items.some(item => item.id === taskDetail.id)) {
|
|
122
|
+
items.push(taskDetail);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
} else if (perspectiveName === "Flagged") {
|
|
127
|
+
// Get flagged items - using flattenedTasks global
|
|
128
|
+
flattenedTasks.forEach(task => {
|
|
129
|
+
if (task.flagged && !task.completed) {
|
|
130
|
+
items.push(getTaskDetails(task));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
// For other perspectives, try to get selected or visible items
|
|
135
|
+
if (selectedTasks.length > 0) {
|
|
136
|
+
selectedTasks.forEach(task => {
|
|
137
|
+
items.push(getTaskDetails(task));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (selectedProjects.length > 0) {
|
|
141
|
+
selectedProjects.forEach(project => {
|
|
142
|
+
items.push(getProjectDetails(project));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If no selection, get some available tasks
|
|
147
|
+
if (items.length === 0) {
|
|
148
|
+
const availableTasks = flattenedTasks.filter(task =>
|
|
149
|
+
task.taskStatus === Task.Status.Available && !task.completed
|
|
150
|
+
);
|
|
151
|
+
availableTasks.slice(0, 100).forEach(task => {
|
|
152
|
+
items.push(getTaskDetails(task));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return JSON.stringify({
|
|
158
|
+
success: true,
|
|
159
|
+
perspectiveName: perspectiveName,
|
|
160
|
+
items: items.slice(0, 100) // Limit to 100 items by default
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return JSON.stringify({
|
|
165
|
+
success: false,
|
|
166
|
+
error: error.toString()
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
})()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// OmniJS script to list available perspectives in OmniFocus
|
|
2
|
+
(() => {
|
|
3
|
+
try {
|
|
4
|
+
const perspectives = [];
|
|
5
|
+
|
|
6
|
+
// Get all built-in perspectives
|
|
7
|
+
// According to the API: Perspective.BuiltIn has these properties
|
|
8
|
+
const builtInPerspectives = [
|
|
9
|
+
{ obj: Perspective.BuiltIn.Inbox, name: 'Inbox' },
|
|
10
|
+
{ obj: Perspective.BuiltIn.Projects, name: 'Projects' },
|
|
11
|
+
{ obj: Perspective.BuiltIn.Tags, name: 'Tags' },
|
|
12
|
+
{ obj: Perspective.BuiltIn.Forecast, name: 'Forecast' },
|
|
13
|
+
{ obj: Perspective.BuiltIn.Flagged, name: 'Flagged' },
|
|
14
|
+
{ obj: Perspective.BuiltIn.Review, name: 'Review' }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Add built-in perspectives
|
|
18
|
+
builtInPerspectives.forEach(p => {
|
|
19
|
+
perspectives.push({
|
|
20
|
+
id: 'builtin_' + p.name.toLowerCase(),
|
|
21
|
+
name: p.name,
|
|
22
|
+
type: 'builtin',
|
|
23
|
+
isBuiltIn: true,
|
|
24
|
+
canModify: false
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Get all custom perspectives
|
|
29
|
+
// According to the API: Perspective.Custom.all returns all custom perspectives
|
|
30
|
+
try {
|
|
31
|
+
const customPerspectives = Perspective.Custom.all;
|
|
32
|
+
if (customPerspectives && customPerspectives.length > 0) {
|
|
33
|
+
customPerspectives.forEach(p => {
|
|
34
|
+
perspectives.push({
|
|
35
|
+
id: p.identifier || 'custom_' + p.name.toLowerCase().replace(/\s+/g, '_'),
|
|
36
|
+
name: p.name,
|
|
37
|
+
type: 'custom',
|
|
38
|
+
isBuiltIn: false,
|
|
39
|
+
canModify: true
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Custom perspectives might not be available (Standard edition)
|
|
45
|
+
// This is not a fatal error
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
success: true,
|
|
50
|
+
perspectives: perspectives
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
success: false,
|
|
56
|
+
error: error.toString()
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
})()
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// OmniJS script to export active tasks from OmniFocus database - Optimized
|
|
2
|
+
(() => {
|
|
3
|
+
try {
|
|
4
|
+
const startTime = new Date();
|
|
5
|
+
|
|
6
|
+
// Helper function to format dates consistently or return null
|
|
7
|
+
function formatDate(date) {
|
|
8
|
+
if (!date) return null;
|
|
9
|
+
return date.toISOString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Helper function to safely get enum values - Simplified with direct mapping
|
|
13
|
+
const taskStatusMap = {
|
|
14
|
+
[Task.Status.Available]: "Available",
|
|
15
|
+
[Task.Status.Blocked]: "Blocked",
|
|
16
|
+
[Task.Status.Completed]: "Completed",
|
|
17
|
+
[Task.Status.Dropped]: "Dropped",
|
|
18
|
+
[Task.Status.DueSoon]: "DueSoon",
|
|
19
|
+
[Task.Status.Next]: "Next",
|
|
20
|
+
[Task.Status.Overdue]: "Overdue"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const projectStatusMap = {
|
|
24
|
+
[Project.Status.Active]: "Active",
|
|
25
|
+
[Project.Status.Done]: "Done",
|
|
26
|
+
[Project.Status.Dropped]: "Dropped",
|
|
27
|
+
[Project.Status.OnHold]: "OnHold"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const folderStatusMap = {
|
|
31
|
+
[Folder.Status.Active]: "Active",
|
|
32
|
+
[Folder.Status.Dropped]: "Dropped"
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function getEnumValue(enumObj, mapObj) {
|
|
36
|
+
if (enumObj === null || enumObj === undefined) return null;
|
|
37
|
+
return mapObj[enumObj] || "Unknown";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create database export object using Maps for faster lookups
|
|
41
|
+
const exportData = {
|
|
42
|
+
exportDate: new Date().toISOString(),
|
|
43
|
+
tasks: [],
|
|
44
|
+
projects: {},
|
|
45
|
+
folders: {},
|
|
46
|
+
tags: {}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Filter active projects first to avoid unnecessary processing
|
|
50
|
+
const activeProjects = flattenedProjects.filter(project =>
|
|
51
|
+
project.status !== Project.Status.Done &&
|
|
52
|
+
project.status !== Project.Status.Dropped
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Pre-filter active tasks to avoid repeated filtering
|
|
56
|
+
const activeTasks = flattenedTasks.filter(task =>
|
|
57
|
+
task.taskStatus !== Task.Status.Completed &&
|
|
58
|
+
task.taskStatus !== Task.Status.Dropped
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Pre-filter active folders
|
|
62
|
+
const activeFolders = flattenedFolders.filter(folder =>
|
|
63
|
+
folder.status !== Folder.Status.Dropped
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Pre-filter active tags
|
|
67
|
+
const activeTags = flattenedTags.filter(tag => tag.active);
|
|
68
|
+
|
|
69
|
+
// Process projects in a single pass and store in Map for O(1) lookups
|
|
70
|
+
const projectsMap = new Map();
|
|
71
|
+
activeProjects.forEach(project => {
|
|
72
|
+
try {
|
|
73
|
+
const projectId = project.id.primaryKey;
|
|
74
|
+
const projectData = {
|
|
75
|
+
id: projectId,
|
|
76
|
+
name: project.name,
|
|
77
|
+
status: getEnumValue(project.status, projectStatusMap),
|
|
78
|
+
folderID: project.parentFolder ? project.parentFolder.id.primaryKey : null,
|
|
79
|
+
sequential: project.task.sequential || false,
|
|
80
|
+
effectiveDueDate: formatDate(project.effectiveDueDate),
|
|
81
|
+
effectiveDeferDate: formatDate(project.effectiveDeferDate),
|
|
82
|
+
dueDate: formatDate(project.dueDate),
|
|
83
|
+
deferDate: formatDate(project.deferDate),
|
|
84
|
+
completedByChildren: project.completedByChildren,
|
|
85
|
+
containsSingletonActions: project.containsSingletonActions,
|
|
86
|
+
note: project.note || "",
|
|
87
|
+
tasks: [] // Will be populated in the task loop
|
|
88
|
+
};
|
|
89
|
+
projectsMap.set(projectId, projectData);
|
|
90
|
+
exportData.projects[projectId] = projectData;
|
|
91
|
+
} catch (projectError) {
|
|
92
|
+
// Silently handle project processing errors
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Process folders in a single pass
|
|
97
|
+
const foldersMap = new Map();
|
|
98
|
+
activeFolders.forEach(folder => {
|
|
99
|
+
try {
|
|
100
|
+
const folderId = folder.id.primaryKey;
|
|
101
|
+
const folderData = {
|
|
102
|
+
id: folderId,
|
|
103
|
+
name: folder.name,
|
|
104
|
+
parentFolderID: folder.parent ? folder.parent.id.primaryKey : null,
|
|
105
|
+
status: getEnumValue(folder.status, folderStatusMap),
|
|
106
|
+
projects: [],
|
|
107
|
+
subfolders: []
|
|
108
|
+
};
|
|
109
|
+
foldersMap.set(folderId, folderData);
|
|
110
|
+
exportData.folders[folderId] = folderData;
|
|
111
|
+
} catch (folderError) {
|
|
112
|
+
// Silently handle folder processing errors
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Process tags in a single pass
|
|
117
|
+
const tagsMap = new Map();
|
|
118
|
+
activeTags.forEach(tag => {
|
|
119
|
+
try {
|
|
120
|
+
const tagId = tag.id.primaryKey;
|
|
121
|
+
const tagData = {
|
|
122
|
+
id: tagId,
|
|
123
|
+
name: tag.name,
|
|
124
|
+
parentTagID: tag.parent ? tag.parent.id.primaryKey : null,
|
|
125
|
+
active: tag.active,
|
|
126
|
+
allowsNextAction: tag.allowsNextAction,
|
|
127
|
+
tasks: []
|
|
128
|
+
};
|
|
129
|
+
tagsMap.set(tagId, tagData);
|
|
130
|
+
exportData.tags[tagId] = tagData;
|
|
131
|
+
} catch (tagError) {
|
|
132
|
+
// Silently handle tag processing errors
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log("Building relationships and processing tasks simultaneously...");
|
|
137
|
+
|
|
138
|
+
// Build folder relationships and project-folder relationships as we go
|
|
139
|
+
foldersMap.forEach((folder, folderId) => {
|
|
140
|
+
if (folder.parentFolderID && foldersMap.has(folder.parentFolderID)) {
|
|
141
|
+
const parentFolder = foldersMap.get(folder.parentFolderID);
|
|
142
|
+
if (!parentFolder.subfolders.includes(folder.id)) {
|
|
143
|
+
parentFolder.subfolders.push(folder.id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
console.log(`Processing ${activeTasks.length} active tasks...`);
|
|
149
|
+
|
|
150
|
+
// Process tasks with an optimized approach
|
|
151
|
+
// Process in batches of 100 to prevent UI freezing
|
|
152
|
+
const BATCH_SIZE = 100;
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < activeTasks.length; i += BATCH_SIZE) {
|
|
155
|
+
const taskBatch = activeTasks.slice(i, i + BATCH_SIZE);
|
|
156
|
+
|
|
157
|
+
taskBatch.forEach(task => {
|
|
158
|
+
try {
|
|
159
|
+
// Get task data with minimal processing
|
|
160
|
+
const taskTags = task.tags.map(tag => tag.id.primaryKey);
|
|
161
|
+
const projectID = task.containingProject ? task.containingProject.id.primaryKey : null;
|
|
162
|
+
|
|
163
|
+
const taskData = {
|
|
164
|
+
id: task.id.primaryKey,
|
|
165
|
+
name: task.name,
|
|
166
|
+
note: task.note || "",
|
|
167
|
+
taskStatus: getEnumValue(task.taskStatus, taskStatusMap),
|
|
168
|
+
flagged: task.flagged,
|
|
169
|
+
dueDate: formatDate(task.dueDate),
|
|
170
|
+
deferDate: formatDate(task.deferDate),
|
|
171
|
+
effectiveDueDate: formatDate(task.effectiveDueDate),
|
|
172
|
+
effectiveDeferDate: formatDate(task.effectiveDeferDate),
|
|
173
|
+
estimatedMinutes: task.estimatedMinutes,
|
|
174
|
+
completedByChildren: task.completedByChildren,
|
|
175
|
+
sequential: task.sequential || false,
|
|
176
|
+
tags: taskTags,
|
|
177
|
+
projectID: projectID,
|
|
178
|
+
parentTaskID: task.parent ? task.parent.id.primaryKey : null,
|
|
179
|
+
children: task.children.map(child => child.id.primaryKey),
|
|
180
|
+
inInbox: task.inInbox
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Add task to export
|
|
184
|
+
exportData.tasks.push(taskData);
|
|
185
|
+
|
|
186
|
+
// Add task ID to associated project (if it exists)
|
|
187
|
+
if (projectID && projectsMap.has(projectID)) {
|
|
188
|
+
projectsMap.get(projectID).tasks.push(taskData.id);
|
|
189
|
+
|
|
190
|
+
// Update folder-project relationship (only once per project)
|
|
191
|
+
const project = projectsMap.get(projectID);
|
|
192
|
+
if (project.folderID && foldersMap.has(project.folderID)) {
|
|
193
|
+
const folder = foldersMap.get(project.folderID);
|
|
194
|
+
if (!folder.projects.includes(project.id)) {
|
|
195
|
+
folder.projects.push(project.id);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Add task ID to associated tags
|
|
201
|
+
taskTags.forEach(tagID => {
|
|
202
|
+
if (tagsMap.has(tagID)) {
|
|
203
|
+
tagsMap.get(tagID).tasks.push(taskData.id);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
} catch (taskError) {
|
|
207
|
+
// Silently handle task processing errors
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Return the complete database export
|
|
213
|
+
const jsonData = JSON.stringify(exportData);
|
|
214
|
+
return jsonData;
|
|
215
|
+
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return JSON.stringify({
|
|
218
|
+
success: false,
|
|
219
|
+
error: `Error exporting database: ${error}`
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
)();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
|
|
12
|
+
// Helper function to execute OmniFocus scripts
|
|
13
|
+
export async function executeJXA(script: string): Promise<any[]> {
|
|
14
|
+
try {
|
|
15
|
+
// Write the script to a temporary file in the system temp directory
|
|
16
|
+
const tempFile = join(tmpdir(), `jxa_script_${Date.now()}.js`);
|
|
17
|
+
|
|
18
|
+
// Write the script to the temporary file
|
|
19
|
+
writeFileSync(tempFile, script);
|
|
20
|
+
|
|
21
|
+
// Execute the script using osascript
|
|
22
|
+
const { stdout, stderr } = await execAsync(`osascript -l JavaScript ${tempFile}`);
|
|
23
|
+
|
|
24
|
+
if (stderr) {
|
|
25
|
+
console.error("Script stderr output:", stderr);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Clean up the temporary file
|
|
29
|
+
unlinkSync(tempFile);
|
|
30
|
+
|
|
31
|
+
// Parse the output as JSON
|
|
32
|
+
try {
|
|
33
|
+
const result = JSON.parse(stdout);
|
|
34
|
+
return result;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error("Failed to parse script output as JSON:", e);
|
|
37
|
+
|
|
38
|
+
// If this contains a "Found X tasks" message, treat it as a successful non-JSON response
|
|
39
|
+
if (stdout.includes("Found") && stdout.includes("tasks")) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("Failed to execute JXA script:", error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Function to execute scripts in OmniFocus using the URL scheme
|
|
52
|
+
// Update src/utils/scriptExecution.ts
|
|
53
|
+
export async function executeOmniFocusScript(scriptPath: string, args?: any): Promise<any> {
|
|
54
|
+
try {
|
|
55
|
+
// Get the actual script path (existing code remains the same)
|
|
56
|
+
let actualPath;
|
|
57
|
+
if (scriptPath.startsWith('@')) {
|
|
58
|
+
const scriptName = scriptPath.substring(1);
|
|
59
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
60
|
+
const __dirname = dirname(__filename);
|
|
61
|
+
|
|
62
|
+
const distPath = join(__dirname, '..', 'utils', 'omnifocusScripts', scriptName);
|
|
63
|
+
const srcPath = join(__dirname, '..', '..', 'src', 'utils', 'omnifocusScripts', scriptName);
|
|
64
|
+
|
|
65
|
+
if (existsSync(distPath)) {
|
|
66
|
+
actualPath = distPath;
|
|
67
|
+
} else if (existsSync(srcPath)) {
|
|
68
|
+
actualPath = srcPath;
|
|
69
|
+
} else {
|
|
70
|
+
actualPath = join(__dirname, '..', 'omnifocusScripts', scriptName);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
actualPath = scriptPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Read the script file
|
|
77
|
+
const scriptContent = readFileSync(actualPath, 'utf8');
|
|
78
|
+
|
|
79
|
+
// Create a temporary file for our JXA wrapper script
|
|
80
|
+
const tempFile = join(tmpdir(), `jxa_wrapper_${Date.now()}.js`);
|
|
81
|
+
|
|
82
|
+
// Escape the script content properly for use in JXA
|
|
83
|
+
const escapedScript = scriptContent.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
84
|
+
|
|
85
|
+
// Create a JXA script that will execute our OmniJS script in OmniFocus
|
|
86
|
+
const jxaScript = `
|
|
87
|
+
function run() {
|
|
88
|
+
try {
|
|
89
|
+
const app = Application('OmniFocus');
|
|
90
|
+
app.includeStandardAdditions = true;
|
|
91
|
+
|
|
92
|
+
// Run the OmniJS script in OmniFocus and capture the output
|
|
93
|
+
const result = app.evaluateJavascript(\`${escapedScript}\`);
|
|
94
|
+
|
|
95
|
+
// Return the result
|
|
96
|
+
return result;
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return JSON.stringify({ error: e.message });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
// Write the JXA script to the temporary file
|
|
104
|
+
writeFileSync(tempFile, jxaScript);
|
|
105
|
+
|
|
106
|
+
// Execute the JXA script using osascript
|
|
107
|
+
const { stdout, stderr } = await execAsync(`osascript -l JavaScript ${tempFile}`);
|
|
108
|
+
|
|
109
|
+
// Clean up the temporary file
|
|
110
|
+
unlinkSync(tempFile);
|
|
111
|
+
|
|
112
|
+
if (stderr) {
|
|
113
|
+
console.error("Script stderr output:", stderr);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Parse the output as JSON
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(stdout);
|
|
119
|
+
} catch (parseError) {
|
|
120
|
+
console.error("Error parsing script output:", parseError);
|
|
121
|
+
return stdout;
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error("Failed to execute OmniFocus script:", error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|