@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.
Files changed (69) hide show
  1. package/QUERY_TOOL_EXAMPLES.md +298 -0
  2. package/QUERY_TOOL_REFERENCE.md +228 -0
  3. package/README.md +250 -0
  4. package/assets/omnifocus-mcp-logo.png +0 -0
  5. package/cli.cjs +9 -0
  6. package/dist/omnifocustypes.js +48 -0
  7. package/dist/server.js +44 -0
  8. package/dist/tools/definitions/addOmniFocusTask.js +76 -0
  9. package/dist/tools/definitions/addProject.js +61 -0
  10. package/dist/tools/definitions/batchAddItems.js +89 -0
  11. package/dist/tools/definitions/batchRemoveItems.js +74 -0
  12. package/dist/tools/definitions/dumpDatabase.js +259 -0
  13. package/dist/tools/definitions/editItem.js +88 -0
  14. package/dist/tools/definitions/getPerspectiveView.js +107 -0
  15. package/dist/tools/definitions/listPerspectives.js +65 -0
  16. package/dist/tools/definitions/queryOmnifocus.js +190 -0
  17. package/dist/tools/definitions/removeItem.js +80 -0
  18. package/dist/tools/dumpDatabase.js +121 -0
  19. package/dist/tools/dumpDatabaseOptimized.js +192 -0
  20. package/dist/tools/primitives/addOmniFocusTask.js +227 -0
  21. package/dist/tools/primitives/addProject.js +132 -0
  22. package/dist/tools/primitives/batchAddItems.js +166 -0
  23. package/dist/tools/primitives/batchRemoveItems.js +44 -0
  24. package/dist/tools/primitives/editItem.js +443 -0
  25. package/dist/tools/primitives/getPerspectiveView.js +50 -0
  26. package/dist/tools/primitives/listPerspectives.js +34 -0
  27. package/dist/tools/primitives/queryOmnifocus.js +365 -0
  28. package/dist/tools/primitives/queryOmnifocusDebug.js +135 -0
  29. package/dist/tools/primitives/removeItem.js +177 -0
  30. package/dist/types.js +1 -0
  31. package/dist/utils/cacheManager.js +187 -0
  32. package/dist/utils/dateFormatting.js +58 -0
  33. package/dist/utils/omnifocusScripts/getPerspectiveView.js +169 -0
  34. package/dist/utils/omnifocusScripts/listPerspectives.js +59 -0
  35. package/dist/utils/omnifocusScripts/omnifocusDump.js +223 -0
  36. package/dist/utils/scriptExecution.js +113 -0
  37. package/package.json +37 -0
  38. package/src/omnifocustypes.ts +89 -0
  39. package/src/server.ts +109 -0
  40. package/src/tools/definitions/addOmniFocusTask.ts +80 -0
  41. package/src/tools/definitions/addProject.ts +67 -0
  42. package/src/tools/definitions/batchAddItems.ts +98 -0
  43. package/src/tools/definitions/batchRemoveItems.ts +80 -0
  44. package/src/tools/definitions/dumpDatabase.ts +311 -0
  45. package/src/tools/definitions/editItem.ts +96 -0
  46. package/src/tools/definitions/getPerspectiveView.ts +125 -0
  47. package/src/tools/definitions/listPerspectives.ts +72 -0
  48. package/src/tools/definitions/queryOmnifocus.ts +212 -0
  49. package/src/tools/definitions/removeItem.ts +86 -0
  50. package/src/tools/dumpDatabase.ts +196 -0
  51. package/src/tools/dumpDatabaseOptimized.ts +231 -0
  52. package/src/tools/primitives/addOmniFocusTask.ts +252 -0
  53. package/src/tools/primitives/addProject.ts +156 -0
  54. package/src/tools/primitives/batchAddItems.ts +207 -0
  55. package/src/tools/primitives/batchRemoveItems.ts +64 -0
  56. package/src/tools/primitives/editItem.ts +507 -0
  57. package/src/tools/primitives/getPerspectiveView.ts +71 -0
  58. package/src/tools/primitives/listPerspectives.ts +53 -0
  59. package/src/tools/primitives/queryOmnifocus.ts +394 -0
  60. package/src/tools/primitives/queryOmnifocusDebug.ts +139 -0
  61. package/src/tools/primitives/removeItem.ts +195 -0
  62. package/src/types.ts +107 -0
  63. package/src/utils/cacheManager.ts +234 -0
  64. package/src/utils/dateFormatting.ts +81 -0
  65. package/src/utils/omnifocusScripts/getPerspectiveView.js +169 -0
  66. package/src/utils/omnifocusScripts/listPerspectives.js +59 -0
  67. package/src/utils/omnifocusScripts/omnifocusDump.js +223 -0
  68. package/src/utils/scriptExecution.ts +128 -0
  69. package/tsconfig.json +15 -0
@@ -0,0 +1,259 @@
1
+ import { z } from 'zod';
2
+ import { dumpDatabase } from '../dumpDatabase.js';
3
+ export const schema = z.object({
4
+ hideCompleted: z.boolean().optional().describe("Set to false to show completed and dropped tasks (default: true)"),
5
+ hideRecurringDuplicates: z.boolean().optional().describe("Set to true to hide duplicate instances of recurring tasks (default: true)")
6
+ });
7
+ export async function handler(args, extra) {
8
+ try {
9
+ // Get raw database
10
+ const database = await dumpDatabase();
11
+ // Format as compact report
12
+ const formattedReport = formatCompactReport(database, {
13
+ hideCompleted: args.hideCompleted !== false, // Default to true
14
+ hideRecurringDuplicates: args.hideRecurringDuplicates !== false // Default to true
15
+ });
16
+ return {
17
+ content: [{
18
+ type: "text",
19
+ text: formattedReport
20
+ }]
21
+ };
22
+ }
23
+ catch (err) {
24
+ return {
25
+ content: [{
26
+ type: "text",
27
+ text: `Error generating report. Please ensure OmniFocus is running and try again.`
28
+ }],
29
+ isError: true
30
+ };
31
+ }
32
+ }
33
+ // Function to format date in compact format (M/D)
34
+ function formatCompactDate(isoDate) {
35
+ if (!isoDate)
36
+ return '';
37
+ const date = new Date(isoDate);
38
+ return `${date.getMonth() + 1}/${date.getDate()}`;
39
+ }
40
+ // Function to format the database in the compact report format
41
+ function formatCompactReport(database, options) {
42
+ const { hideCompleted, hideRecurringDuplicates } = options;
43
+ // Get current date for the header
44
+ const today = new Date();
45
+ const dateStr = today.toISOString().split('T')[0];
46
+ let output = `# OMNIFOCUS [${dateStr}]\n\n`;
47
+ // Add legend
48
+ output += `FORMAT LEGEND:
49
+ F: Folder | P: Project | •: Task | 🚩: Flagged
50
+ Dates: [M/D] | Duration: (30m) or (2h) | Tags: <tag1,tag2>
51
+ Status: #next #avail #block #due #over #compl #drop\n\n`;
52
+ // Map of folder IDs to folder objects for quick lookup
53
+ const folderMap = new Map();
54
+ Object.values(database.folders).forEach((folder) => {
55
+ folderMap.set(folder.id, folder);
56
+ });
57
+ // Get all tag names to compute minimum unique prefixes
58
+ const allTagNames = Object.values(database.tags).map((tag) => tag.name);
59
+ const tagPrefixMap = computeMinimumUniquePrefixes(allTagNames);
60
+ // Function to get folder hierarchy path
61
+ function getFolderPath(folderId) {
62
+ const path = [];
63
+ let currentId = folderId;
64
+ while (currentId) {
65
+ const folder = folderMap.get(currentId);
66
+ if (!folder)
67
+ break;
68
+ path.unshift(folder.name);
69
+ currentId = folder.parentFolderID;
70
+ }
71
+ return path;
72
+ }
73
+ // Get root folders (no parent)
74
+ const rootFolders = Object.values(database.folders).filter((folder) => !folder.parentFolderID);
75
+ // Process folders recursively
76
+ function processFolder(folder, level) {
77
+ const indent = ' '.repeat(level);
78
+ let folderOutput = `${indent}F: ${folder.name}\n`;
79
+ // Process subfolders
80
+ if (folder.subfolders && folder.subfolders.length > 0) {
81
+ for (const subfolderId of folder.subfolders) {
82
+ const subfolder = database.folders[subfolderId];
83
+ if (subfolder) {
84
+ folderOutput += `${processFolder(subfolder, level + 1)}`;
85
+ }
86
+ }
87
+ }
88
+ // Process projects in this folder
89
+ if (folder.projects && folder.projects.length > 0) {
90
+ for (const projectId of folder.projects) {
91
+ const project = database.projects[projectId];
92
+ if (project) {
93
+ folderOutput += processProject(project, level + 1);
94
+ }
95
+ }
96
+ }
97
+ return folderOutput;
98
+ }
99
+ // Process a project
100
+ function processProject(project, level) {
101
+ const indent = ' '.repeat(level);
102
+ // Skip if it's completed or dropped and we're hiding completed items
103
+ if (hideCompleted && (project.status === 'Done' || project.status === 'Dropped')) {
104
+ return '';
105
+ }
106
+ // Format project status info
107
+ let statusInfo = '';
108
+ if (project.status === 'OnHold') {
109
+ statusInfo = ' [OnHold]';
110
+ }
111
+ else if (project.status === 'Dropped') {
112
+ statusInfo = ' [Dropped]';
113
+ }
114
+ // Add due date if present
115
+ if (project.dueDate) {
116
+ const dueDateStr = formatCompactDate(project.dueDate);
117
+ statusInfo += statusInfo ? ` [DUE:${dueDateStr}]` : ` [DUE:${dueDateStr}]`;
118
+ }
119
+ // Add flag if present
120
+ const flaggedSymbol = project.flagged ? ' 🚩' : '';
121
+ let projectOutput = `${indent}P: ${project.name}${flaggedSymbol}${statusInfo}\n`;
122
+ // Process tasks in this project
123
+ const projectTasks = database.tasks.filter((task) => task.projectId === project.id && !task.parentId);
124
+ if (projectTasks.length > 0) {
125
+ for (const task of projectTasks) {
126
+ projectOutput += processTask(task, level + 1);
127
+ }
128
+ }
129
+ return projectOutput;
130
+ }
131
+ // Process a task
132
+ function processTask(task, level) {
133
+ const indent = ' '.repeat(level);
134
+ // Skip if it's completed or dropped and we're hiding completed items
135
+ if (hideCompleted && (task.completed || task.taskStatus === 'Completed' || task.taskStatus === 'Dropped')) {
136
+ return '';
137
+ }
138
+ // Flag symbol
139
+ const flagSymbol = task.flagged ? '🚩 ' : '';
140
+ // Format dates
141
+ let dateInfo = '';
142
+ if (task.dueDate) {
143
+ const dueDateStr = formatCompactDate(task.dueDate);
144
+ dateInfo += ` [DUE:${dueDateStr}]`;
145
+ }
146
+ if (task.deferDate) {
147
+ const deferDateStr = formatCompactDate(task.deferDate);
148
+ dateInfo += ` [defer:${deferDateStr}]`;
149
+ }
150
+ // Format duration
151
+ let durationStr = '';
152
+ if (task.estimatedMinutes) {
153
+ // Convert to hours if >= 60 minutes
154
+ if (task.estimatedMinutes >= 60) {
155
+ const hours = Math.floor(task.estimatedMinutes / 60);
156
+ durationStr = ` (${hours}h)`;
157
+ }
158
+ else {
159
+ durationStr = ` (${task.estimatedMinutes}m)`;
160
+ }
161
+ }
162
+ // Format tags
163
+ let tagsStr = '';
164
+ if (task.tagNames && task.tagNames.length > 0) {
165
+ // Use minimum unique prefixes for tag names
166
+ const abbreviatedTags = task.tagNames.map((tag) => {
167
+ return tagPrefixMap.get(tag) || tag;
168
+ });
169
+ tagsStr = ` <${abbreviatedTags.join(',')}>`;
170
+ }
171
+ // Format status
172
+ let statusStr = '';
173
+ switch (task.taskStatus) {
174
+ case 'Next':
175
+ statusStr = ' #next';
176
+ break;
177
+ case 'Available':
178
+ statusStr = ' #avail';
179
+ break;
180
+ case 'Blocked':
181
+ statusStr = ' #block';
182
+ break;
183
+ case 'DueSoon':
184
+ statusStr = ' #due';
185
+ break;
186
+ case 'Overdue':
187
+ statusStr = ' #over';
188
+ break;
189
+ case 'Completed':
190
+ statusStr = ' #compl';
191
+ break;
192
+ case 'Dropped':
193
+ statusStr = ' #drop';
194
+ break;
195
+ }
196
+ let taskOutput = `${indent}• ${flagSymbol}${task.name}${dateInfo}${durationStr}${tagsStr}${statusStr}\n`;
197
+ // Process subtasks
198
+ if (task.childIds && task.childIds.length > 0) {
199
+ const childTasks = database.tasks.filter((t) => task.childIds.includes(t.id));
200
+ for (const childTask of childTasks) {
201
+ taskOutput += processTask(childTask, level + 1);
202
+ }
203
+ }
204
+ return taskOutput;
205
+ }
206
+ // Process all root folders
207
+ for (const folder of rootFolders) {
208
+ output += processFolder(folder, 0);
209
+ }
210
+ // Process projects not in any folder (if any)
211
+ const rootProjects = Object.values(database.projects).filter((project) => !project.folderID);
212
+ for (const project of rootProjects) {
213
+ output += processProject(project, 0);
214
+ }
215
+ // Process tasks in the Inbox (not in any project)
216
+ const inboxTasks = database.tasks.filter(function (task) {
217
+ return !task.projectId;
218
+ });
219
+ if (inboxTasks.length > 0) {
220
+ output += `\nP: Inbox\n`;
221
+ for (const task of inboxTasks) {
222
+ output += processTask(task, 0);
223
+ }
224
+ }
225
+ return output;
226
+ }
227
+ // Compute minimum unique prefixes for all tags (minimum 3 characters)
228
+ function computeMinimumUniquePrefixes(tagNames) {
229
+ const prefixMap = new Map();
230
+ // For each tag name
231
+ for (const tagName of tagNames) {
232
+ // Start with minimum length of 3
233
+ let prefixLength = 3;
234
+ let isUnique = false;
235
+ // Keep increasing prefix length until we find a unique prefix
236
+ while (!isUnique && prefixLength <= tagName.length) {
237
+ const prefix = tagName.substring(0, prefixLength);
238
+ // Check if this prefix uniquely identifies the tag
239
+ isUnique = tagNames.every(otherTag => {
240
+ // If it's the same tag, skip comparison
241
+ if (otherTag === tagName)
242
+ return true;
243
+ // If the other tag starts with the same prefix, it's not unique
244
+ return !otherTag.startsWith(prefix);
245
+ });
246
+ if (isUnique) {
247
+ prefixMap.set(tagName, prefix);
248
+ }
249
+ else {
250
+ prefixLength++;
251
+ }
252
+ }
253
+ // If we couldn't find a unique prefix, use the full tag name
254
+ if (!isUnique) {
255
+ prefixMap.set(tagName, tagName);
256
+ }
257
+ }
258
+ return prefixMap;
259
+ }
@@ -0,0 +1,88 @@
1
+ import { z } from 'zod';
2
+ import { editItem } from '../primitives/editItem.js';
3
+ export const schema = z.object({
4
+ id: z.string().optional().describe("The ID of the task or project to edit"),
5
+ name: z.string().optional().describe("The name of the task or project to edit (as fallback if ID not provided)"),
6
+ itemType: z.enum(['task', 'project']).describe("Type of item to edit ('task' or 'project')"),
7
+ // Common editable fields
8
+ newName: z.string().optional().describe("New name for the item"),
9
+ newNote: z.string().optional().describe("New note for the item"),
10
+ newDueDate: z.string().optional().describe("New due date in ISO format (YYYY-MM-DD or full ISO date); set to empty string to clear"),
11
+ newDeferDate: z.string().optional().describe("New defer date in ISO format (YYYY-MM-DD or full ISO date); set to empty string to clear"),
12
+ newFlagged: z.boolean().optional().describe("Set flagged status (set to false for no flag, true for flag)"),
13
+ newEstimatedMinutes: z.number().optional().describe("New estimated minutes"),
14
+ // Task-specific fields
15
+ newStatus: z.enum(['incomplete', 'completed', 'dropped']).optional().describe("New status for tasks (incomplete, completed, dropped)"),
16
+ addTags: z.array(z.string()).optional().describe("Tags to add to the task"),
17
+ removeTags: z.array(z.string()).optional().describe("Tags to remove from the task"),
18
+ replaceTags: z.array(z.string()).optional().describe("Tags to replace all existing tags with"),
19
+ // Project-specific fields
20
+ newSequential: z.boolean().optional().describe("Whether the project should be sequential"),
21
+ newFolderName: z.string().optional().describe("New folder to move the project to"),
22
+ newProjectStatus: z.enum(['active', 'completed', 'dropped', 'onHold']).optional().describe("New status for projects")
23
+ });
24
+ export async function handler(args, extra) {
25
+ try {
26
+ // Validate that either id or name is provided
27
+ if (!args.id && !args.name) {
28
+ return {
29
+ content: [{
30
+ type: "text",
31
+ text: "Either id or name must be provided to edit an item."
32
+ }],
33
+ isError: true
34
+ };
35
+ }
36
+ // Call the editItem function
37
+ const result = await editItem(args);
38
+ if (result.success) {
39
+ // Item was edited successfully
40
+ const itemTypeLabel = args.itemType === 'task' ? 'Task' : 'Project';
41
+ let changedText = '';
42
+ if (result.changedProperties) {
43
+ changedText = ` (${result.changedProperties})`;
44
+ }
45
+ return {
46
+ content: [{
47
+ type: "text",
48
+ text: `āœ… ${itemTypeLabel} "${result.name}" updated successfully${changedText}.`
49
+ }]
50
+ };
51
+ }
52
+ else {
53
+ // Item editing failed
54
+ let errorMsg = `Failed to update ${args.itemType}`;
55
+ if (result.error) {
56
+ if (result.error.includes("Item not found")) {
57
+ errorMsg = `${args.itemType.charAt(0).toUpperCase() + args.itemType.slice(1)} not found`;
58
+ if (args.id)
59
+ errorMsg += ` with ID "${args.id}"`;
60
+ if (args.name)
61
+ errorMsg += `${args.id ? ' or' : ' with'} name "${args.name}"`;
62
+ errorMsg += '.';
63
+ }
64
+ else {
65
+ errorMsg += `: ${result.error}`;
66
+ }
67
+ }
68
+ return {
69
+ content: [{
70
+ type: "text",
71
+ text: errorMsg
72
+ }],
73
+ isError: true
74
+ };
75
+ }
76
+ }
77
+ catch (err) {
78
+ const error = err;
79
+ console.error(`Tool execution error: ${error.message}`);
80
+ return {
81
+ content: [{
82
+ type: "text",
83
+ text: `Error updating ${args.itemType}: ${error.message}`
84
+ }],
85
+ isError: true
86
+ };
87
+ }
88
+ }
@@ -0,0 +1,107 @@
1
+ import { z } from 'zod';
2
+ import { getPerspectiveView } from '../primitives/getPerspectiveView.js';
3
+ export const schema = z.object({
4
+ perspectiveName: z.string().describe("Name of the perspective to view (e.g., 'Inbox', 'Projects', 'Flagged', or custom perspective name)"),
5
+ limit: z.number().optional().describe("Maximum number of items to return. Default: 100"),
6
+ includeMetadata: z.boolean().optional().describe("Include additional metadata like project names, tags, dates. Default: true"),
7
+ fields: z.array(z.string()).optional().describe("Specific fields to include in the response. Reduces response size. Available fields: id, name, note, flagged, dueDate, deferDate, completionDate, taskStatus, projectName, tagNames, estimatedMinutes")
8
+ });
9
+ export async function handler(args, extra) {
10
+ try {
11
+ const result = await getPerspectiveView({
12
+ perspectiveName: args.perspectiveName,
13
+ limit: args.limit ?? 100,
14
+ includeMetadata: args.includeMetadata ?? true,
15
+ fields: args.fields
16
+ });
17
+ if (result.success) {
18
+ const items = result.items || [];
19
+ // Format the output
20
+ let output = `## ${args.perspectiveName} Perspective (${items.length} items)\n\n`;
21
+ if (items.length === 0) {
22
+ output += "No items visible in this perspective.";
23
+ }
24
+ else {
25
+ // Format each item
26
+ items.forEach(item => {
27
+ const parts = [];
28
+ // Core display
29
+ const flag = item.flagged ? '🚩 ' : '';
30
+ const checkbox = item.completed ? 'ā˜‘' : '☐';
31
+ parts.push(`${checkbox} ${flag}${item.name || 'Unnamed'}`);
32
+ // Project context
33
+ if (item.projectName) {
34
+ parts.push(`(${item.projectName})`);
35
+ }
36
+ // Due date
37
+ if (item.dueDate) {
38
+ const date = new Date(item.dueDate);
39
+ const dateStr = `${date.getMonth() + 1}/${date.getDate()}`;
40
+ parts.push(`[due: ${dateStr}]`);
41
+ }
42
+ // Defer date
43
+ if (item.deferDate) {
44
+ const date = new Date(item.deferDate);
45
+ const dateStr = `${date.getMonth() + 1}/${date.getDate()}`;
46
+ parts.push(`[defer: ${dateStr}]`);
47
+ }
48
+ // Time estimate
49
+ if (item.estimatedMinutes) {
50
+ const hours = item.estimatedMinutes >= 60
51
+ ? `${Math.floor(item.estimatedMinutes / 60)}h${item.estimatedMinutes % 60 > 0 ? (item.estimatedMinutes % 60) + 'm' : ''}`
52
+ : `${item.estimatedMinutes}m`;
53
+ parts.push(`(${hours})`);
54
+ }
55
+ // Tags
56
+ if (item.tagNames && item.tagNames.length > 0) {
57
+ parts.push(`<${item.tagNames.join(',')}>`);
58
+ }
59
+ // Status
60
+ if (item.taskStatus && item.taskStatus !== 'Available') {
61
+ parts.push(`#${item.taskStatus.toLowerCase()}`);
62
+ }
63
+ // ID for reference
64
+ if (item.id) {
65
+ parts.push(`[${item.id}]`);
66
+ }
67
+ output += `• ${parts.join(' ')}\n`;
68
+ // Add note preview if present and not too long
69
+ if (item.note && item.note.trim()) {
70
+ const notePreview = item.note.trim().split('\n')[0].substring(0, 80);
71
+ const ellipsis = item.note.length > 80 || item.note.includes('\n') ? '...' : '';
72
+ output += ` └─ ${notePreview}${ellipsis}\n`;
73
+ }
74
+ });
75
+ }
76
+ if (items.length === args.limit) {
77
+ output += `\nāš ļø Results limited to ${args.limit} items. More may be available in this perspective.`;
78
+ }
79
+ return {
80
+ content: [{
81
+ type: "text",
82
+ text: output
83
+ }]
84
+ };
85
+ }
86
+ else {
87
+ return {
88
+ content: [{
89
+ type: "text",
90
+ text: `Failed to get perspective view: ${result.error}`
91
+ }],
92
+ isError: true
93
+ };
94
+ }
95
+ }
96
+ catch (err) {
97
+ const error = err;
98
+ console.error(`Error getting perspective view: ${error.message}`);
99
+ return {
100
+ content: [{
101
+ type: "text",
102
+ text: `Error getting perspective view: ${error.message}`
103
+ }],
104
+ isError: true
105
+ };
106
+ }
107
+ }
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ import { listPerspectives } from '../primitives/listPerspectives.js';
3
+ export const schema = z.object({
4
+ includeBuiltIn: z.boolean().optional().describe("Include built-in perspectives (Inbox, Projects, Tags, etc.). Default: true"),
5
+ includeCustom: z.boolean().optional().describe("Include custom perspectives (Pro feature). Default: true")
6
+ });
7
+ export async function handler(args, extra) {
8
+ try {
9
+ const result = await listPerspectives({
10
+ includeBuiltIn: args.includeBuiltIn ?? true,
11
+ includeCustom: args.includeCustom ?? true
12
+ });
13
+ if (result.success) {
14
+ const perspectives = result.perspectives || [];
15
+ // Format the perspectives in a readable way
16
+ let output = `## Available Perspectives (${perspectives.length})\n\n`;
17
+ // Group by type
18
+ const builtIn = perspectives.filter(p => p.type === 'builtin');
19
+ const custom = perspectives.filter(p => p.type === 'custom');
20
+ if (builtIn.length > 0) {
21
+ output += `### Built-in Perspectives\n`;
22
+ builtIn.forEach(p => {
23
+ output += `• ${p.name}\n`;
24
+ });
25
+ }
26
+ if (custom.length > 0) {
27
+ if (builtIn.length > 0)
28
+ output += '\n';
29
+ output += `### Custom Perspectives\n`;
30
+ custom.forEach(p => {
31
+ output += `• ${p.name}\n`;
32
+ });
33
+ }
34
+ if (perspectives.length === 0) {
35
+ output = "No perspectives found.";
36
+ }
37
+ return {
38
+ content: [{
39
+ type: "text",
40
+ text: output
41
+ }]
42
+ };
43
+ }
44
+ else {
45
+ return {
46
+ content: [{
47
+ type: "text",
48
+ text: `Failed to list perspectives: ${result.error}`
49
+ }],
50
+ isError: true
51
+ };
52
+ }
53
+ }
54
+ catch (err) {
55
+ const error = err;
56
+ console.error(`Error listing perspectives: ${error.message}`);
57
+ return {
58
+ content: [{
59
+ type: "text",
60
+ text: `Error listing perspectives: ${error.message}`
61
+ }],
62
+ isError: true
63
+ };
64
+ }
65
+ }