@doist/todoist-ai 4.15.1 → 4.16.1

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 (167) hide show
  1. package/dist/filter-helpers.d.ts +1 -1
  2. package/dist/index.d.ts +175 -175
  3. package/dist/index.js +61 -81
  4. package/dist/main.js +15 -23
  5. package/dist/mcp-helpers.d.ts +4 -4
  6. package/dist/mcp-server-6tm7Rhyz.js +2840 -0
  7. package/dist/todoist-tool.d.ts +2 -2
  8. package/dist/tool-helpers.d.ts +1 -1
  9. package/dist/tools/add-comments.d.ts +1 -1
  10. package/dist/tools/add-comments.d.ts.map +1 -1
  11. package/dist/tools/add-projects.d.ts +4 -4
  12. package/dist/tools/add-projects.d.ts.map +1 -1
  13. package/dist/tools/add-sections.d.ts +1 -1
  14. package/dist/tools/add-sections.d.ts.map +1 -1
  15. package/dist/tools/add-tasks.d.ts +4 -4
  16. package/dist/tools/add-tasks.d.ts.map +1 -1
  17. package/dist/tools/complete-tasks.d.ts +1 -1
  18. package/dist/tools/complete-tasks.d.ts.map +1 -1
  19. package/dist/tools/delete-object.d.ts +3 -3
  20. package/dist/tools/delete-object.d.ts.map +1 -1
  21. package/dist/tools/fetch.d.ts +1 -1
  22. package/dist/tools/find-activity.d.ts +5 -5
  23. package/dist/tools/find-activity.d.ts.map +1 -1
  24. package/dist/tools/find-comments.d.ts +2 -2
  25. package/dist/tools/find-comments.d.ts.map +1 -1
  26. package/dist/tools/find-completed-tasks.d.ts +3 -3
  27. package/dist/tools/find-completed-tasks.d.ts.map +1 -1
  28. package/dist/tools/find-project-collaborators.d.ts +2 -2
  29. package/dist/tools/find-projects.d.ts +1 -1
  30. package/dist/tools/find-projects.d.ts.map +1 -1
  31. package/dist/tools/find-sections.d.ts +1 -1
  32. package/dist/tools/find-sections.d.ts.map +1 -1
  33. package/dist/tools/find-tasks-by-date.d.ts +1 -1
  34. package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
  35. package/dist/tools/find-tasks.d.ts +3 -3
  36. package/dist/tools/find-tasks.d.ts.map +1 -1
  37. package/dist/tools/get-overview.d.ts +1 -1
  38. package/dist/tools/manage-assignments.d.ts +1 -1
  39. package/dist/tools/search.d.ts +1 -1
  40. package/dist/tools/update-comments.d.ts +4 -4
  41. package/dist/tools/update-comments.d.ts.map +1 -1
  42. package/dist/tools/update-projects.d.ts +1 -1
  43. package/dist/tools/update-projects.d.ts.map +1 -1
  44. package/dist/tools/update-sections.d.ts +4 -4
  45. package/dist/tools/update-sections.d.ts.map +1 -1
  46. package/dist/tools/update-tasks.d.ts +7 -7
  47. package/dist/tools/update-tasks.d.ts.map +1 -1
  48. package/dist/tools/user-info.d.ts +1 -1
  49. package/dist/utils/assignment-validator.d.ts +2 -2
  50. package/dist/utils/response-builders.d.ts +1 -3
  51. package/dist/utils/response-builders.d.ts.map +1 -1
  52. package/dist/utils/test-helpers.d.ts +1 -1
  53. package/dist/utils/user-resolver.d.ts +1 -1
  54. package/package.json +11 -9
  55. package/dist/filter-helpers.js +0 -79
  56. package/dist/mcp-helpers.js +0 -71
  57. package/dist/mcp-server.js +0 -142
  58. package/dist/todoist-tool.js +0 -1
  59. package/dist/tool-helpers.js +0 -125
  60. package/dist/tool-helpers.test.d.ts +0 -2
  61. package/dist/tool-helpers.test.d.ts.map +0 -1
  62. package/dist/tool-helpers.test.js +0 -223
  63. package/dist/tools/__tests__/add-comments.test.d.ts +0 -2
  64. package/dist/tools/__tests__/add-comments.test.d.ts.map +0 -1
  65. package/dist/tools/__tests__/add-comments.test.js +0 -241
  66. package/dist/tools/__tests__/add-projects.test.d.ts +0 -2
  67. package/dist/tools/__tests__/add-projects.test.d.ts.map +0 -1
  68. package/dist/tools/__tests__/add-projects.test.js +0 -174
  69. package/dist/tools/__tests__/add-sections.test.d.ts +0 -2
  70. package/dist/tools/__tests__/add-sections.test.d.ts.map +0 -1
  71. package/dist/tools/__tests__/add-sections.test.js +0 -185
  72. package/dist/tools/__tests__/add-tasks.test.d.ts +0 -2
  73. package/dist/tools/__tests__/add-tasks.test.d.ts.map +0 -1
  74. package/dist/tools/__tests__/add-tasks.test.js +0 -533
  75. package/dist/tools/__tests__/assignment-integration.test.d.ts +0 -2
  76. package/dist/tools/__tests__/assignment-integration.test.d.ts.map +0 -1
  77. package/dist/tools/__tests__/assignment-integration.test.js +0 -428
  78. package/dist/tools/__tests__/complete-tasks.test.d.ts +0 -2
  79. package/dist/tools/__tests__/complete-tasks.test.d.ts.map +0 -1
  80. package/dist/tools/__tests__/complete-tasks.test.js +0 -206
  81. package/dist/tools/__tests__/delete-object.test.d.ts +0 -2
  82. package/dist/tools/__tests__/delete-object.test.d.ts.map +0 -1
  83. package/dist/tools/__tests__/delete-object.test.js +0 -110
  84. package/dist/tools/__tests__/fetch.test.d.ts +0 -2
  85. package/dist/tools/__tests__/fetch.test.d.ts.map +0 -1
  86. package/dist/tools/__tests__/fetch.test.js +0 -279
  87. package/dist/tools/__tests__/find-activity.test.d.ts +0 -2
  88. package/dist/tools/__tests__/find-activity.test.d.ts.map +0 -1
  89. package/dist/tools/__tests__/find-activity.test.js +0 -229
  90. package/dist/tools/__tests__/find-comments.test.d.ts +0 -2
  91. package/dist/tools/__tests__/find-comments.test.d.ts.map +0 -1
  92. package/dist/tools/__tests__/find-comments.test.js +0 -236
  93. package/dist/tools/__tests__/find-completed-tasks.test.d.ts +0 -2
  94. package/dist/tools/__tests__/find-completed-tasks.test.d.ts.map +0 -1
  95. package/dist/tools/__tests__/find-completed-tasks.test.js +0 -324
  96. package/dist/tools/__tests__/find-projects.test.d.ts +0 -2
  97. package/dist/tools/__tests__/find-projects.test.d.ts.map +0 -1
  98. package/dist/tools/__tests__/find-projects.test.js +0 -154
  99. package/dist/tools/__tests__/find-sections.test.d.ts +0 -2
  100. package/dist/tools/__tests__/find-sections.test.d.ts.map +0 -1
  101. package/dist/tools/__tests__/find-sections.test.js +0 -245
  102. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts +0 -2
  103. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts.map +0 -1
  104. package/dist/tools/__tests__/find-tasks-by-date.test.js +0 -528
  105. package/dist/tools/__tests__/find-tasks.test.d.ts +0 -2
  106. package/dist/tools/__tests__/find-tasks.test.d.ts.map +0 -1
  107. package/dist/tools/__tests__/find-tasks.test.js +0 -771
  108. package/dist/tools/__tests__/get-overview.test.d.ts +0 -2
  109. package/dist/tools/__tests__/get-overview.test.d.ts.map +0 -1
  110. package/dist/tools/__tests__/get-overview.test.js +0 -225
  111. package/dist/tools/__tests__/search.test.d.ts +0 -2
  112. package/dist/tools/__tests__/search.test.d.ts.map +0 -1
  113. package/dist/tools/__tests__/search.test.js +0 -206
  114. package/dist/tools/__tests__/update-comments.test.d.ts +0 -2
  115. package/dist/tools/__tests__/update-comments.test.d.ts.map +0 -1
  116. package/dist/tools/__tests__/update-comments.test.js +0 -294
  117. package/dist/tools/__tests__/update-projects.test.d.ts +0 -2
  118. package/dist/tools/__tests__/update-projects.test.d.ts.map +0 -1
  119. package/dist/tools/__tests__/update-projects.test.js +0 -217
  120. package/dist/tools/__tests__/update-sections.test.d.ts +0 -2
  121. package/dist/tools/__tests__/update-sections.test.d.ts.map +0 -1
  122. package/dist/tools/__tests__/update-sections.test.js +0 -169
  123. package/dist/tools/__tests__/update-tasks.test.d.ts +0 -2
  124. package/dist/tools/__tests__/update-tasks.test.d.ts.map +0 -1
  125. package/dist/tools/__tests__/update-tasks.test.js +0 -788
  126. package/dist/tools/__tests__/user-info.test.d.ts +0 -2
  127. package/dist/tools/__tests__/user-info.test.d.ts.map +0 -1
  128. package/dist/tools/__tests__/user-info.test.js +0 -139
  129. package/dist/tools/add-comments.js +0 -79
  130. package/dist/tools/add-projects.js +0 -63
  131. package/dist/tools/add-sections.js +0 -61
  132. package/dist/tools/add-tasks.js +0 -160
  133. package/dist/tools/complete-tasks.js +0 -68
  134. package/dist/tools/delete-object.js +0 -79
  135. package/dist/tools/fetch.js +0 -102
  136. package/dist/tools/find-activity.js +0 -221
  137. package/dist/tools/find-comments.js +0 -143
  138. package/dist/tools/find-completed-tasks.js +0 -161
  139. package/dist/tools/find-project-collaborators.js +0 -151
  140. package/dist/tools/find-projects.js +0 -101
  141. package/dist/tools/find-sections.js +0 -96
  142. package/dist/tools/find-tasks-by-date.js +0 -198
  143. package/dist/tools/find-tasks.js +0 -329
  144. package/dist/tools/get-overview.js +0 -249
  145. package/dist/tools/manage-assignments.js +0 -337
  146. package/dist/tools/search.js +0 -65
  147. package/dist/tools/update-comments.js +0 -82
  148. package/dist/tools/update-projects.js +0 -84
  149. package/dist/tools/update-sections.js +0 -70
  150. package/dist/tools/update-tasks.js +0 -170
  151. package/dist/tools/user-info.js +0 -142
  152. package/dist/utils/assignment-validator.js +0 -253
  153. package/dist/utils/constants.js +0 -45
  154. package/dist/utils/duration-parser.js +0 -96
  155. package/dist/utils/duration-parser.test.d.ts +0 -2
  156. package/dist/utils/duration-parser.test.d.ts.map +0 -1
  157. package/dist/utils/duration-parser.test.js +0 -147
  158. package/dist/utils/labels.js +0 -18
  159. package/dist/utils/priorities.js +0 -20
  160. package/dist/utils/response-builders.js +0 -210
  161. package/dist/utils/sanitize-data.js +0 -37
  162. package/dist/utils/sanitize-data.test.d.ts +0 -2
  163. package/dist/utils/sanitize-data.test.d.ts.map +0 -1
  164. package/dist/utils/sanitize-data.test.js +0 -93
  165. package/dist/utils/test-helpers.js +0 -237
  166. package/dist/utils/tool-names.js +0 -40
  167. package/dist/utils/user-resolver.js +0 -179
@@ -1,337 +0,0 @@
1
- import { z } from 'zod';
2
- import { getToolOutput } from '../mcp-helpers.js';
3
- import { assignmentValidator, } from '../utils/assignment-validator.js';
4
- import { ToolNames } from '../utils/tool-names.js';
5
- import { userResolver } from '../utils/user-resolver.js';
6
- const { FIND_TASKS, FIND_PROJECT_COLLABORATORS, UPDATE_TASKS } = ToolNames;
7
- // Maximum tasks per operation to prevent abuse and timeouts
8
- const MAX_TASKS_PER_OPERATION = 50;
9
- const ArgsSchema = {
10
- operation: z
11
- .enum(['assign', 'unassign', 'reassign'])
12
- .describe('The assignment operation to perform.'),
13
- taskIds: z
14
- .array(z.string())
15
- .min(1)
16
- .max(MAX_TASKS_PER_OPERATION)
17
- .describe('The IDs of the tasks to operate on (max 50).'),
18
- responsibleUser: z
19
- .string()
20
- .optional()
21
- .describe('The user to assign tasks to. Can be user ID, name, or email. Required for assign and reassign operations.'),
22
- fromAssigneeUser: z
23
- .string()
24
- .optional()
25
- .describe('For reassign operations: the current assignee to reassign from. Can be user ID, name, or email. Optional - if not provided, reassigns from any current assignee.'),
26
- dryRun: z
27
- .boolean()
28
- .optional()
29
- .default(false)
30
- .describe('If true, validates operations without executing them.'),
31
- };
32
- const manageAssignments = {
33
- name: ToolNames.MANAGE_ASSIGNMENTS,
34
- description: 'Bulk assignment operations for multiple tasks. Supports assign, unassign, and reassign operations with atomic rollback on failures.',
35
- parameters: ArgsSchema,
36
- async execute(args, client) {
37
- const { operation, taskIds, responsibleUser, fromAssigneeUser, dryRun } = args;
38
- // Validate required parameters based on operation
39
- if ((operation === 'assign' || operation === 'reassign') && !responsibleUser) {
40
- throw new Error(`${operation} operation requires responsibleUser parameter`);
41
- }
42
- // Fetch all tasks first to validate they exist and get project information
43
- const tasks = await Promise.allSettled(taskIds.map(async (taskId) => {
44
- try {
45
- return await client.getTask(taskId);
46
- }
47
- catch (_error) {
48
- throw new Error(`Task ${taskId} not found or not accessible`);
49
- }
50
- }));
51
- const validTasks = [];
52
- const taskErrors = [];
53
- for (let i = 0; i < tasks.length; i++) {
54
- const result = tasks[i];
55
- if (result && result.status === 'fulfilled') {
56
- validTasks.push(result.value);
57
- }
58
- else if (result && result.status === 'rejected') {
59
- taskErrors.push({
60
- taskId: taskIds[i] || 'invalid-task-id',
61
- success: false,
62
- error: result.reason?.message || 'Task not accessible',
63
- });
64
- }
65
- else {
66
- taskErrors.push({
67
- taskId: taskIds[i] || 'invalid-task-id',
68
- success: false,
69
- error: 'Task not accessible',
70
- });
71
- }
72
- }
73
- if (validTasks.length === 0) {
74
- const textContent = generateTextContent({
75
- operation,
76
- results: taskErrors,
77
- dryRun,
78
- });
79
- return getToolOutput({
80
- textContent,
81
- structuredContent: {
82
- operation,
83
- results: taskErrors,
84
- totalRequested: taskIds.length,
85
- successful: 0,
86
- failed: taskErrors.length,
87
- dryRun,
88
- },
89
- });
90
- }
91
- // Pre-resolve fromAssigneeUser once for reassign operations
92
- let resolvedFromUserId;
93
- if (operation === 'reassign' && fromAssigneeUser) {
94
- const fromUser = await userResolver.resolveUser(client, fromAssigneeUser);
95
- resolvedFromUserId = fromUser?.userId || fromAssigneeUser;
96
- }
97
- // Build assignments for validation
98
- const assignments = [];
99
- for (const task of validTasks) {
100
- // For reassign operations, check if we need to filter by current assignee
101
- if (operation === 'reassign' && resolvedFromUserId) {
102
- // Skip tasks not assigned to the specified user
103
- if (task.responsibleUid !== resolvedFromUserId) {
104
- continue;
105
- }
106
- }
107
- assignments.push({
108
- taskId: task.id,
109
- projectId: task.projectId,
110
- responsibleUid: responsibleUser || '', // Will be validated appropriately
111
- });
112
- }
113
- // Handle unassign operations (no validation needed for unassignment)
114
- if (operation === 'unassign') {
115
- if (dryRun) {
116
- const results = validTasks.map((task) => ({
117
- taskId: task.id,
118
- success: true,
119
- originalAssigneeId: task.responsibleUid,
120
- newAssigneeId: null,
121
- }));
122
- const textContent = generateTextContent({
123
- operation,
124
- results,
125
- dryRun: true,
126
- });
127
- return getToolOutput({
128
- textContent,
129
- structuredContent: {
130
- operation,
131
- results,
132
- totalRequested: taskIds.length,
133
- successful: results.length,
134
- failed: taskErrors.length,
135
- dryRun: true,
136
- },
137
- });
138
- }
139
- // Execute unassign operations
140
- const unassignPromises = validTasks.map(async (task) => {
141
- try {
142
- await client.updateTask(task.id, { assigneeId: null });
143
- return {
144
- taskId: task.id,
145
- success: true,
146
- originalAssigneeId: task.responsibleUid,
147
- newAssigneeId: null,
148
- };
149
- }
150
- catch (error) {
151
- return {
152
- taskId: task.id,
153
- success: false,
154
- error: error instanceof Error ? error.message : 'Update failed',
155
- originalAssigneeId: task.responsibleUid,
156
- };
157
- }
158
- });
159
- const unassignResults = await Promise.all(unassignPromises);
160
- const allResults = [...unassignResults, ...taskErrors];
161
- const textContent = generateTextContent({
162
- operation,
163
- results: allResults,
164
- dryRun: false,
165
- });
166
- return getToolOutput({
167
- textContent,
168
- structuredContent: {
169
- operation,
170
- results: allResults,
171
- totalRequested: taskIds.length,
172
- successful: unassignResults.filter((r) => r.success).length,
173
- failed: allResults.filter((r) => !r.success).length,
174
- dryRun: false,
175
- },
176
- });
177
- }
178
- // Validate all assignments
179
- const validationResults = await assignmentValidator.validateBulkAssignment(client, assignments);
180
- // Process validation results
181
- const validAssignments = [];
182
- const validationErrors = [];
183
- for (let i = 0; i < assignments.length; i++) {
184
- const assignment = assignments[i];
185
- const validation = validationResults[i];
186
- if (assignment && validation && validation.isValid) {
187
- validAssignments.push({ assignment, validation });
188
- }
189
- else if (assignment?.taskId) {
190
- validationErrors.push({
191
- taskId: assignment.taskId,
192
- success: false,
193
- error: validation?.error?.message || 'Validation failed',
194
- });
195
- }
196
- }
197
- // Helper function to process assignments for both dry run and execution
198
- async function processAssignments(assignments, execute) {
199
- const filteredAssignments = assignments.filter((item) => item.assignment != null && item.validation != null);
200
- if (!execute) {
201
- // Dry run: just map to successful results
202
- return filteredAssignments.map(({ assignment, validation }) => {
203
- const task = validTasks.find((t) => t.id === assignment.taskId);
204
- if (!assignment.taskId || !validation.resolvedUser?.userId) {
205
- throw new Error('Invalid assignment or validation data - this should not happen');
206
- }
207
- return {
208
- taskId: assignment.taskId,
209
- success: true,
210
- originalAssigneeId: task?.responsibleUid || null,
211
- newAssigneeId: validation.resolvedUser.userId,
212
- };
213
- });
214
- }
215
- // Execute: perform actual updates
216
- const executePromises = filteredAssignments.map(async ({ assignment, validation }) => {
217
- const task = validTasks.find((t) => t.id === assignment.taskId);
218
- if (!assignment.taskId || !validation.resolvedUser?.userId) {
219
- return {
220
- taskId: assignment.taskId || 'unknown-task',
221
- success: false,
222
- error: 'Invalid assignment data - missing task ID or resolved user',
223
- originalAssigneeId: task?.responsibleUid || null,
224
- };
225
- }
226
- try {
227
- await client.updateTask(assignment.taskId, {
228
- assigneeId: validation.resolvedUser.userId,
229
- });
230
- return {
231
- taskId: assignment.taskId,
232
- success: true,
233
- originalAssigneeId: task?.responsibleUid || null,
234
- newAssigneeId: validation.resolvedUser.userId,
235
- };
236
- }
237
- catch (error) {
238
- return {
239
- taskId: assignment.taskId,
240
- success: false,
241
- error: error instanceof Error ? error.message : 'Update failed',
242
- originalAssigneeId: task?.responsibleUid || null,
243
- };
244
- }
245
- });
246
- return Promise.all(executePromises);
247
- }
248
- // Handle assign/reassign operations - validate then execute
249
- const assignmentResults = await processAssignments(validAssignments, !dryRun);
250
- const allResults = [...assignmentResults, ...validationErrors, ...taskErrors];
251
- const textContent = generateTextContent({
252
- operation,
253
- results: allResults,
254
- dryRun,
255
- });
256
- return getToolOutput({
257
- textContent,
258
- structuredContent: {
259
- operation,
260
- results: allResults,
261
- totalRequested: taskIds.length,
262
- successful: assignmentResults.filter((r) => r.success).length,
263
- failed: allResults.filter((r) => !r.success).length,
264
- dryRun,
265
- },
266
- });
267
- },
268
- };
269
- function generateTextContent({ operation, results, dryRun, }) {
270
- const successful = results.filter((r) => r.success);
271
- const failed = results.filter((r) => !r.success);
272
- const operationVerb = dryRun ? 'would be' : 'were';
273
- const operationPastTense = {
274
- assign: 'assigned',
275
- unassign: 'unassigned',
276
- reassign: 'reassigned',
277
- }[operation];
278
- let summary = `**${dryRun ? 'Dry Run: ' : ''}Bulk ${operation} operation**\n\n`;
279
- if (successful.length > 0) {
280
- summary += `**${successful.length} task${successful.length === 1 ? '' : 's'} ${operationVerb} successfully ${operationPastTense}**\n`;
281
- // Show first few successful operations
282
- const preview = successful.slice(0, 5);
283
- for (const result of preview) {
284
- let changeDesc = '';
285
- if (operation === 'unassign') {
286
- changeDesc = ' (unassigned from previous assignee)';
287
- }
288
- else if (result.newAssigneeId) {
289
- changeDesc = ` → ${result.newAssigneeId}`;
290
- }
291
- summary += ` • Task ${result.taskId}${changeDesc}\n`;
292
- }
293
- if (successful.length > 5) {
294
- summary += ` • ... and ${successful.length - 5} more\n`;
295
- }
296
- summary += '\n';
297
- }
298
- if (failed.length > 0) {
299
- summary += `**${failed.length} task${failed.length === 1 ? '' : 's'} failed**\n`;
300
- // Show first few failures with reasons
301
- const preview = failed.slice(0, 5);
302
- for (const result of preview) {
303
- summary += ` • Task ${result.taskId}: ${result.error}\n`;
304
- }
305
- if (failed.length > 5) {
306
- summary += ` • ... and ${failed.length - 5} more failures\n`;
307
- }
308
- summary += '\n';
309
- }
310
- // Add operational info
311
- if (!dryRun && successful.length > 0) {
312
- summary += '**Next steps:**\n';
313
- summary += `• Use ${FIND_TASKS} with responsibleUser to see ${operation === 'unassign' ? 'unassigned' : 'newly assigned'} tasks\n`;
314
- summary += `• Use ${UPDATE_TASKS} for individual assignment changes\n`;
315
- if (failed.length > 0) {
316
- summary += `• Check failed tasks and use ${FIND_PROJECT_COLLABORATORS} to verify collaborator access\n`;
317
- }
318
- }
319
- else if (dryRun) {
320
- summary += '**To execute:**\n';
321
- summary += '• Remove dryRun parameter and run again to execute changes\n';
322
- if (successful.length > 0) {
323
- summary += `• ${successful.length} task${successful.length === 1 ? '' : 's'} ready for ${operation} operation\n`;
324
- }
325
- if (failed.length > 0) {
326
- summary += `• Fix ${failed.length} validation error${failed.length === 1 ? '' : 's'} before executing\n`;
327
- }
328
- }
329
- else if (successful.length === 0) {
330
- summary += '**Suggestions:**\n';
331
- summary += `• Use ${FIND_PROJECT_COLLABORATORS} to find valid assignees\n`;
332
- summary += '• Check task IDs and assignee permissions\n';
333
- summary += '• Use dryRun=true to validate before executing\n';
334
- }
335
- return summary;
336
- }
337
- export { manageAssignments };
@@ -1,65 +0,0 @@
1
- import { getProjectUrl, getTaskUrl } from '@doist/todoist-api-typescript';
2
- import { z } from 'zod';
3
- import { getErrorOutput } from '../mcp-helpers.js';
4
- import { getTasksByFilter } from '../tool-helpers.js';
5
- import { ApiLimits } from '../utils/constants.js';
6
- import { ToolNames } from '../utils/tool-names.js';
7
- const ArgsSchema = {
8
- query: z.string().min(1).describe('The search query string to find tasks and projects.'),
9
- };
10
- /**
11
- * OpenAI MCP search tool - returns a list of relevant search results from Todoist.
12
- *
13
- * This tool follows the OpenAI MCP search tool specification:
14
- * @see https://platform.openai.com/docs/mcp#search-tool
15
- */
16
- const search = {
17
- name: ToolNames.SEARCH,
18
- description: 'Search across tasks and projects in Todoist. Returns a list of relevant results with IDs, titles, and URLs.',
19
- parameters: ArgsSchema,
20
- async execute(args, client) {
21
- try {
22
- const { query } = args;
23
- // Search both tasks and projects in parallel
24
- // Use TASKS_MAX for search since this tool doesn't support pagination
25
- const [tasksResult, projectsResponse] = await Promise.all([
26
- getTasksByFilter({
27
- client,
28
- query: `search: ${query}`,
29
- limit: ApiLimits.TASKS_MAX,
30
- cursor: undefined,
31
- }),
32
- client.getProjects({ limit: ApiLimits.PROJECTS_MAX }),
33
- ]);
34
- // Filter projects by search query (case-insensitive)
35
- const searchLower = query.toLowerCase();
36
- const matchingProjects = projectsResponse.results.filter((project) => project.name.toLowerCase().includes(searchLower));
37
- // Build results array
38
- const results = [];
39
- // Add task results with composite IDs
40
- for (const task of tasksResult.tasks) {
41
- results.push({
42
- id: `task:${task.id}`,
43
- title: task.content,
44
- url: getTaskUrl(task.id),
45
- });
46
- }
47
- // Add project results with composite IDs
48
- for (const project of matchingProjects) {
49
- results.push({
50
- id: `project:${project.id}`,
51
- title: project.name,
52
- url: getProjectUrl(project.id),
53
- });
54
- }
55
- // Return as JSON-encoded string in a text content item (OpenAI MCP spec)
56
- const jsonText = JSON.stringify({ results });
57
- return { content: [{ type: 'text', text: jsonText }] };
58
- }
59
- catch (error) {
60
- const message = error instanceof Error ? error.message : 'An unknown error occurred';
61
- return getErrorOutput(message);
62
- }
63
- },
64
- };
65
- export { search };
@@ -1,82 +0,0 @@
1
- import { z } from 'zod';
2
- import { getToolOutput } from '../mcp-helpers.js';
3
- import { formatNextSteps } from '../utils/response-builders.js';
4
- import { ToolNames } from '../utils/tool-names.js';
5
- const { FIND_COMMENTS, DELETE_OBJECT } = ToolNames;
6
- const CommentUpdateSchema = z.object({
7
- id: z.string().min(1).describe('The ID of the comment to update.'),
8
- content: z.string().min(1).describe('The new content for the comment.'),
9
- });
10
- const ArgsSchema = {
11
- comments: z.array(CommentUpdateSchema).min(1).describe('The comments to update.'),
12
- };
13
- const updateComments = {
14
- name: ToolNames.UPDATE_COMMENTS,
15
- description: 'Update multiple existing comments with new content.',
16
- parameters: ArgsSchema,
17
- async execute(args, client) {
18
- const { comments } = args;
19
- const updateCommentPromises = comments.map(async (comment) => {
20
- return await client.updateComment(comment.id, { content: comment.content });
21
- });
22
- const updatedComments = await Promise.all(updateCommentPromises);
23
- const textContent = generateTextContent({
24
- comments: updatedComments,
25
- });
26
- return getToolOutput({
27
- textContent,
28
- structuredContent: {
29
- comments: updatedComments,
30
- totalCount: updatedComments.length,
31
- updatedCommentIds: updatedComments.map((comment) => comment.id),
32
- appliedOperations: {
33
- updateCount: updatedComments.length,
34
- },
35
- },
36
- });
37
- },
38
- };
39
- function generateNextSteps(comments) {
40
- const nextSteps = [];
41
- // Early return for empty comments
42
- if (comments.length === 0) {
43
- return nextSteps;
44
- }
45
- // Multiple comments case
46
- if (comments.length > 1) {
47
- nextSteps.push(`Use ${FIND_COMMENTS} to view comments by task or project`);
48
- nextSteps.push(`Use ${DELETE_OBJECT} with type=comment to remove comments`);
49
- return nextSteps;
50
- }
51
- // Single comment case
52
- const comment = comments[0];
53
- if (!comment)
54
- return nextSteps;
55
- if (comment.taskId) {
56
- nextSteps.push(`Use ${FIND_COMMENTS} with taskId=${comment.taskId} to see all task comments`);
57
- }
58
- else if (comment.projectId) {
59
- nextSteps.push(`Use ${FIND_COMMENTS} with projectId=${comment.projectId} to see all project comments`);
60
- }
61
- nextSteps.push(`Use ${DELETE_OBJECT} with type=comment id=${comment.id} to remove comment`);
62
- return nextSteps;
63
- }
64
- function generateTextContent({ comments }) {
65
- // Group comments by entity type and count
66
- const taskComments = comments.filter((c) => c.taskId).length;
67
- const projectComments = comments.filter((c) => c.projectId).length;
68
- const parts = [];
69
- if (taskComments > 0) {
70
- const commentsLabel = taskComments > 1 ? 'comments' : 'comment';
71
- parts.push(`${taskComments} task ${commentsLabel}`);
72
- }
73
- if (projectComments > 0) {
74
- const commentsLabel = projectComments > 1 ? 'comments' : 'comment';
75
- parts.push(`${projectComments} project ${commentsLabel}`);
76
- }
77
- const summary = parts.length > 0 ? `Updated ${parts.join(' and ')}` : 'No comments updated';
78
- const nextSteps = generateNextSteps(comments);
79
- const next = formatNextSteps(nextSteps);
80
- return `${summary}\n${next}`;
81
- }
82
- export { updateComments };
@@ -1,84 +0,0 @@
1
- import { z } from 'zod';
2
- import { getToolOutput } from '../mcp-helpers.js';
3
- import { formatNextSteps } from '../utils/response-builders.js';
4
- import { ToolNames } from '../utils/tool-names.js';
5
- const { FIND_PROJECTS, FIND_TASKS, GET_OVERVIEW } = ToolNames;
6
- const ProjectUpdateSchema = z.object({
7
- id: z.string().min(1).describe('The ID of the project to update.'),
8
- name: z.string().min(1).optional().describe('The new name of the project.'),
9
- isFavorite: z.boolean().optional().describe('Whether the project is a favorite.'),
10
- viewStyle: z.enum(['list', 'board', 'calendar']).optional().describe('The project view style.'),
11
- });
12
- const ArgsSchema = {
13
- projects: z.array(ProjectUpdateSchema).min(1).describe('The projects to update.'),
14
- };
15
- const updateProjects = {
16
- name: ToolNames.UPDATE_PROJECTS,
17
- description: 'Update multiple existing projects with new values.',
18
- parameters: ArgsSchema,
19
- async execute(args, client) {
20
- const { projects } = args;
21
- const updateProjectsPromises = projects.map(async (project) => {
22
- if (!hasUpdatesToMake(project)) {
23
- return undefined;
24
- }
25
- const { id, ...updateArgs } = project;
26
- return await client.updateProject(id, updateArgs);
27
- });
28
- const updatedProjects = (await Promise.all(updateProjectsPromises)).filter((project) => project !== undefined);
29
- const textContent = generateTextContent({
30
- projects: updatedProjects,
31
- args,
32
- });
33
- return getToolOutput({
34
- textContent,
35
- structuredContent: {
36
- projects: updatedProjects,
37
- totalCount: updatedProjects.length,
38
- updatedProjectIds: updatedProjects.map((project) => project.id),
39
- appliedOperations: {
40
- updateCount: updatedProjects.length,
41
- skippedCount: projects.length - updatedProjects.length,
42
- },
43
- },
44
- });
45
- },
46
- };
47
- function generateTextContent({ projects, args, }) {
48
- const totalRequested = args.projects.length;
49
- const actuallyUpdated = projects.length;
50
- const skipped = totalRequested - actuallyUpdated;
51
- const count = projects.length;
52
- const projectList = projects.map((project) => `• ${project.name} (id=${project.id})`).join('\n');
53
- let summary = `Updated ${count} project${count === 1 ? '' : 's'}`;
54
- if (skipped > 0) {
55
- summary += ` (${skipped} skipped - no changes)`;
56
- }
57
- if (count > 0) {
58
- summary += `:\n${projectList}`;
59
- }
60
- // Context-aware next steps for updated projects
61
- const nextSteps = [];
62
- if (projects.length > 0) {
63
- if (count === 1) {
64
- const project = projects[0];
65
- if (project) {
66
- nextSteps.push(`Use ${GET_OVERVIEW} with projectId=${project.id} to see project structure`);
67
- nextSteps.push(`Use ${FIND_TASKS} with projectId=${project.id} to review existing tasks`);
68
- }
69
- }
70
- else {
71
- nextSteps.push(`Use ${FIND_PROJECTS} to see all projects with updated names`);
72
- nextSteps.push(`Use ${GET_OVERVIEW} to see updated project hierarchy`);
73
- }
74
- }
75
- else {
76
- nextSteps.push(`Use ${FIND_PROJECTS} to see current projects`);
77
- }
78
- const next = formatNextSteps(nextSteps);
79
- return `${summary}\n${next}`;
80
- }
81
- function hasUpdatesToMake({ id, ...otherUpdateArgs }) {
82
- return Object.keys(otherUpdateArgs).length > 0;
83
- }
84
- export { updateProjects };
@@ -1,70 +0,0 @@
1
- import { z } from 'zod';
2
- import { getToolOutput } from '../mcp-helpers.js';
3
- import { formatNextSteps } from '../utils/response-builders.js';
4
- import { ToolNames } from '../utils/tool-names.js';
5
- const { FIND_TASKS, GET_OVERVIEW, FIND_SECTIONS } = ToolNames;
6
- const SectionUpdateSchema = z.object({
7
- id: z.string().min(1).describe('The ID of the section to update.'),
8
- name: z.string().min(1).describe('The new name of the section.'),
9
- });
10
- const ArgsSchema = {
11
- sections: z.array(SectionUpdateSchema).min(1).describe('The sections to update.'),
12
- };
13
- const updateSections = {
14
- name: ToolNames.UPDATE_SECTIONS,
15
- description: 'Update multiple existing sections with new values.',
16
- parameters: ArgsSchema,
17
- async execute({ sections }, client) {
18
- const updatedSections = await Promise.all(sections.map((section) => client.updateSection(section.id, { name: section.name })));
19
- const textContent = generateTextContent({
20
- sections: updatedSections,
21
- });
22
- return getToolOutput({
23
- textContent,
24
- structuredContent: {
25
- sections: updatedSections,
26
- totalCount: updatedSections.length,
27
- updatedSectionIds: updatedSections.map((section) => section.id),
28
- },
29
- });
30
- },
31
- };
32
- function generateNextSteps(sections) {
33
- // Handle empty sections first (early return)
34
- if (sections.length === 0) {
35
- return [`Use ${FIND_SECTIONS} to see current sections`];
36
- }
37
- // Handle single section case
38
- if (sections.length === 1) {
39
- const section = sections[0];
40
- if (!section)
41
- return [];
42
- return [
43
- `Use ${FIND_TASKS} with sectionId=${section.id} to see existing tasks`,
44
- `Use ${GET_OVERVIEW} with projectId=${section.projectId} to see project structure`,
45
- 'Consider updating task descriptions if section purpose changed',
46
- ];
47
- }
48
- // Handle multiple sections case
49
- const projectIds = [...new Set(sections.map((s) => s.projectId))];
50
- const steps = [`Use ${FIND_SECTIONS} to see all sections with updated names`];
51
- if (projectIds.length === 1) {
52
- steps.push(`Use ${GET_OVERVIEW} with projectId=${projectIds[0]} to see updated project structure`);
53
- }
54
- else {
55
- steps.push(`Use ${GET_OVERVIEW} to see updated project structures`);
56
- }
57
- steps.push('Consider updating task descriptions if section purposes changed');
58
- return steps;
59
- }
60
- function generateTextContent({ sections }) {
61
- const count = sections.length;
62
- const sectionList = sections
63
- .map((section) => `• ${section.name} (id=${section.id}, projectId=${section.projectId})`)
64
- .join('\n');
65
- const summary = `Updated ${count} section${count === 1 ? '' : 's'}:\n${sectionList}`;
66
- const nextSteps = generateNextSteps(sections);
67
- const next = formatNextSteps(nextSteps);
68
- return `${summary}\n${next}`;
69
- }
70
- export { updateSections };