omnifocus-mcp-enhanced 1.1.0

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.
@@ -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,113 @@
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
+ const execAsync = promisify(exec);
10
+ // Helper function to execute OmniFocus scripts
11
+ export async function executeJXA(script) {
12
+ try {
13
+ // Write the script to a temporary file in the system temp directory
14
+ const tempFile = join(tmpdir(), `jxa_script_${Date.now()}.js`);
15
+ // Write the script to the temporary file
16
+ writeFileSync(tempFile, script);
17
+ // Execute the script using osascript
18
+ const { stdout, stderr } = await execAsync(`osascript -l JavaScript ${tempFile}`);
19
+ if (stderr) {
20
+ console.error("Script stderr output:", stderr);
21
+ }
22
+ // Clean up the temporary file
23
+ unlinkSync(tempFile);
24
+ // Parse the output as JSON
25
+ try {
26
+ const result = JSON.parse(stdout);
27
+ return result;
28
+ }
29
+ catch (e) {
30
+ console.error("Failed to parse script output as JSON:", e);
31
+ // If this contains a "Found X tasks" message, treat it as a successful non-JSON response
32
+ if (stdout.includes("Found") && stdout.includes("tasks")) {
33
+ return [];
34
+ }
35
+ return [];
36
+ }
37
+ }
38
+ catch (error) {
39
+ console.error("Failed to execute JXA script:", error);
40
+ throw error;
41
+ }
42
+ }
43
+ // Function to execute scripts in OmniFocus using the URL scheme
44
+ // Update src/utils/scriptExecution.ts
45
+ export async function executeOmniFocusScript(scriptPath, args) {
46
+ try {
47
+ // Get the actual script path (existing code remains the same)
48
+ let actualPath;
49
+ if (scriptPath.startsWith('@')) {
50
+ const scriptName = scriptPath.substring(1);
51
+ const __filename = fileURLToPath(import.meta.url);
52
+ const __dirname = dirname(__filename);
53
+ const distPath = join(__dirname, '..', 'utils', 'omnifocusScripts', scriptName);
54
+ const srcPath = join(__dirname, '..', '..', 'src', 'utils', 'omnifocusScripts', scriptName);
55
+ if (existsSync(distPath)) {
56
+ actualPath = distPath;
57
+ }
58
+ else if (existsSync(srcPath)) {
59
+ actualPath = srcPath;
60
+ }
61
+ else {
62
+ actualPath = join(__dirname, '..', 'omnifocusScripts', scriptName);
63
+ }
64
+ }
65
+ else {
66
+ actualPath = scriptPath;
67
+ }
68
+ // Read the script file
69
+ const scriptContent = readFileSync(actualPath, 'utf8');
70
+ // Create a temporary file for our JXA wrapper script
71
+ const tempFile = join(tmpdir(), `jxa_wrapper_${Date.now()}.js`);
72
+ // Escape the script content properly for use in JXA
73
+ const escapedScript = scriptContent.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
74
+ // Create a JXA script that will execute our OmniJS script in OmniFocus
75
+ const jxaScript = `
76
+ function run() {
77
+ try {
78
+ const app = Application('OmniFocus');
79
+ app.includeStandardAdditions = true;
80
+
81
+ // Run the OmniJS script in OmniFocus and capture the output
82
+ const result = app.evaluateJavascript(\`${escapedScript}\`);
83
+
84
+ // Return the result
85
+ return result;
86
+ } catch (e) {
87
+ return JSON.stringify({ error: e.message });
88
+ }
89
+ }
90
+ `;
91
+ // Write the JXA script to the temporary file
92
+ writeFileSync(tempFile, jxaScript);
93
+ // Execute the JXA script using osascript
94
+ const { stdout, stderr } = await execAsync(`osascript -l JavaScript ${tempFile}`);
95
+ // Clean up the temporary file
96
+ unlinkSync(tempFile);
97
+ if (stderr) {
98
+ console.error("Script stderr output:", stderr);
99
+ }
100
+ // Parse the output as JSON
101
+ try {
102
+ return JSON.parse(stdout);
103
+ }
104
+ catch (parseError) {
105
+ console.error("Error parsing script output:", parseError);
106
+ return stdout;
107
+ }
108
+ }
109
+ catch (error) {
110
+ console.error("Failed to execute OmniFocus script:", error);
111
+ throw error;
112
+ }
113
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "omnifocus-mcp-enhanced",
3
+ "type": "module",
4
+ "version": "1.1.0",
5
+ "description": "Enhanced Model Context Protocol (MCP) server for OmniFocus with complete subtask support and advanced task management features",
6
+ "main": "dist/server.js",
7
+ "bin": {
8
+ "omnifocus-mcp-enhanced": "./cli.cjs"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc && npm run copy-files && chmod 755 dist/server.js",
12
+ "copy-files": "mkdir -p dist/utils/omnifocusScripts && cp src/utils/omnifocusScripts/*.js dist/utils/omnifocusScripts/",
13
+ "start": "node dist/server.js",
14
+ "dev": "tsc -w",
15
+ "prepublishOnly": "npm run build",
16
+ "test": "echo \"Test passed - manual testing required with OmniFocus\""
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.8.0",
20
+ "zod": "^3.22.4"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^20.11.19",
24
+ "typescript": "^5.3.3"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/your-username/omnifocus-mcp-enhanced.git"
29
+ },
30
+ "homepage": "https://github.com/your-username/omnifocus-mcp-enhanced",
31
+ "keywords": [
32
+ "omnifocus",
33
+ "mcp",
34
+ "claude",
35
+ "task-management",
36
+ "ai",
37
+ "subtasks",
38
+ "enhanced",
39
+ "productivity",
40
+ "macos"
41
+ ],
42
+ "author": {
43
+ "name": "Your Name",
44
+ "email": "your.email@example.com"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ },
49
+ "os": ["darwin"],
50
+ "license": "MIT"
51
+ }
package/publish.sh ADDED
@@ -0,0 +1,148 @@
1
+ #!/bin/bash
2
+
3
+ # OmniFocus MCP Enhanced 发布脚本
4
+ # 用于发布到 NPM
5
+
6
+ set -e
7
+
8
+ echo "🚀 OmniFocus MCP Enhanced 发布脚本"
9
+ echo "================================="
10
+
11
+ # 检查是否已经登录 npm
12
+ if ! npm whoami > /dev/null 2>&1; then
13
+ echo "❌ 请先登录 NPM: npm login"
14
+ exit 1
15
+ fi
16
+
17
+ echo "👤 当前 NPM 用户: $(npm whoami)"
18
+
19
+ # 检查参数
20
+ if [ "$1" = "check" ]; then
21
+ echo "🔍 检查发布准备状态..."
22
+
23
+ # 检查必要文件
24
+ echo "📝 检查必要文件..."
25
+ required_files=("package.json" "README.md" "CLAUDE.md" "cli.cjs" ".npmignore")
26
+ for file in "${required_files[@]}"; do
27
+ if [ -f "$file" ]; then
28
+ echo " ✅ $file"
29
+ else
30
+ echo " ❌ $file (缺失)"
31
+ exit 1
32
+ fi
33
+ done
34
+
35
+ # 检查构建产物
36
+ echo "🔨 检查构建产物..."
37
+ if [ -d "dist" ]; then
38
+ echo " ✅ dist/ 目录存在"
39
+ if [ -f "dist/server.js" ]; then
40
+ echo " ✅ dist/server.js 存在"
41
+ else
42
+ echo " ❌ dist/server.js 不存在,请运行 npm run build"
43
+ exit 1
44
+ fi
45
+ else
46
+ echo " ❌ dist/ 目录不存在,请运行 npm run build"
47
+ exit 1
48
+ fi
49
+
50
+ # 检查 package.json 信息
51
+ echo "📦 检查 package.json..."
52
+ PACKAGE_NAME=$(node -p "require('./package.json').name")
53
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
54
+ echo " 📋 包名: $PACKAGE_NAME"
55
+ echo " 🏷️ 版本: $PACKAGE_VERSION"
56
+
57
+ # 检查版本是否已存在
58
+ echo "🔍 检查版本冲突..."
59
+ if npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version > /dev/null 2>&1; then
60
+ echo " ⚠️ 版本 $PACKAGE_VERSION 已存在于 NPM"
61
+ echo " 💡 请更新 package.json 中的版本号"
62
+ exit 1
63
+ else
64
+ echo " ✅ 版本 $PACKAGE_VERSION 可用"
65
+ fi
66
+
67
+ # 测试打包
68
+ echo "📦 测试打包..."
69
+ npm pack --dry-run > /dev/null
70
+ echo " ✅ 打包测试通过"
71
+
72
+ echo ""
73
+ echo "✅ 所有检查通过!可以发布。"
74
+ echo "🚀 运行 './publish.sh release' 开始发布"
75
+
76
+ elif [ "$1" = "release" ]; then
77
+ echo "🚀 开始发布流程..."
78
+
79
+ # 先运行检查
80
+ echo "🔍 运行发布前检查..."
81
+ ./publish.sh check
82
+
83
+ PACKAGE_NAME=$(node -p "require('./package.json').name")
84
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
85
+
86
+ echo ""
87
+ echo "📦 准备发布:"
88
+ echo " 📋 包名: $PACKAGE_NAME"
89
+ echo " 🏷️ 版本: $PACKAGE_VERSION"
90
+ echo ""
91
+
92
+ read -p "确认发布? (y/N): " -n 1 -r
93
+ echo
94
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
95
+ echo "❌ 发布已取消"
96
+ exit 1
97
+ fi
98
+
99
+ # 清理并重新构建
100
+ echo "🔨 重新构建项目..."
101
+ rm -rf dist/
102
+ npm run build
103
+
104
+ # 发布到 NPM
105
+ echo "📤 发布到 NPM..."
106
+ npm publish
107
+
108
+ echo ""
109
+ echo "🎉 发布成功!"
110
+ echo ""
111
+ echo "📋 用户可以通过以下方式安装:"
112
+ echo ""
113
+ echo "# Claude Code 一键安装"
114
+ echo "claude mcp add omnifocus-enhanced -- npx -y $PACKAGE_NAME"
115
+ echo ""
116
+ echo "# 或者全局安装"
117
+ echo "npm install -g $PACKAGE_NAME"
118
+ echo "claude mcp add omnifocus-enhanced -- $PACKAGE_NAME"
119
+ echo ""
120
+ echo "🔗 NPM 包地址: https://www.npmjs.com/package/$PACKAGE_NAME"
121
+
122
+ elif [ "$1" = "version" ]; then
123
+ if [ -z "$2" ]; then
124
+ echo "❌ 请指定版本类型: patch, minor, major"
125
+ echo "用法: ./publish.sh version patch"
126
+ exit 1
127
+ fi
128
+
129
+ echo "🏷️ 更新版本..."
130
+ npm version "$2" --no-git-tag-version
131
+ NEW_VERSION=$(node -p "require('./package.json').version")
132
+ echo "✅ 版本已更新为: $NEW_VERSION"
133
+ echo "💡 现在可以运行 './publish.sh release' 发布新版本"
134
+
135
+ else
136
+ echo "用法:"
137
+ echo " ./publish.sh check # 检查发布准备状态"
138
+ echo " ./publish.sh version <type> # 更新版本 (patch/minor/major)"
139
+ echo " ./publish.sh release # 发布到 NPM"
140
+ echo ""
141
+ echo "发布流程:"
142
+ echo " 1. ./publish.sh check # 检查状态"
143
+ echo " 2. ./publish.sh version patch # 更新版本(可选)"
144
+ echo " 3. ./publish.sh release # 发布"
145
+ echo ""
146
+ echo "发布后用户安装命令:"
147
+ echo " claude mcp add omnifocus-enhanced -- npx -y omnifocus-mcp-enhanced"
148
+ fi