@doist/todoist-ai 4.4.0 → 4.6.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.
Files changed (80) hide show
  1. package/dist/index.d.ts +168 -35
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +24 -16
  4. package/dist/mcp-helpers.d.ts.map +1 -1
  5. package/dist/mcp-helpers.js +1 -1
  6. package/dist/mcp-server.d.ts.map +1 -1
  7. package/dist/mcp-server.js +78 -17
  8. package/dist/tool-helpers.d.ts +4 -0
  9. package/dist/tool-helpers.d.ts.map +1 -1
  10. package/dist/tool-helpers.js +2 -0
  11. package/dist/tools/__tests__/add-projects.test.js +1 -1
  12. package/dist/tools/__tests__/add-sections.test.js +1 -1
  13. package/dist/tools/__tests__/add-tasks.test.js +182 -13
  14. package/dist/tools/__tests__/assignment-integration.test.d.ts +2 -0
  15. package/dist/tools/__tests__/assignment-integration.test.d.ts.map +1 -0
  16. package/dist/tools/__tests__/assignment-integration.test.js +415 -0
  17. package/dist/tools/__tests__/find-projects.test.js +1 -1
  18. package/dist/tools/__tests__/find-sections.test.js +1 -1
  19. package/dist/tools/__tests__/find-tasks-by-date.test.js +1 -1
  20. package/dist/tools/__tests__/find-tasks.test.js +3 -3
  21. package/dist/tools/__tests__/get-overview.test.js +1 -1
  22. package/dist/tools/__tests__/update-tasks.test.js +82 -6
  23. package/dist/tools/__tests__/user-info.test.js +1 -1
  24. package/dist/tools/add-comments.d.ts +3 -3
  25. package/dist/tools/add-comments.d.ts.map +1 -1
  26. package/dist/tools/add-comments.js +1 -1
  27. package/dist/tools/add-projects.d.ts.map +1 -1
  28. package/dist/tools/add-projects.js +1 -1
  29. package/dist/tools/add-sections.d.ts.map +1 -1
  30. package/dist/tools/add-sections.js +1 -1
  31. package/dist/tools/add-tasks.d.ts +17 -7
  32. package/dist/tools/add-tasks.d.ts.map +1 -1
  33. package/dist/tools/add-tasks.js +51 -4
  34. package/dist/tools/find-comments.d.ts +2 -2
  35. package/dist/tools/find-completed-tasks.d.ts +4 -2
  36. package/dist/tools/find-completed-tasks.d.ts.map +1 -1
  37. package/dist/tools/find-completed-tasks.js +2 -2
  38. package/dist/tools/find-project-collaborators.d.ts +64 -0
  39. package/dist/tools/find-project-collaborators.d.ts.map +1 -0
  40. package/dist/tools/find-project-collaborators.js +151 -0
  41. package/dist/tools/find-tasks-by-date.d.ts +2 -0
  42. package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
  43. package/dist/tools/find-tasks-by-date.js +2 -2
  44. package/dist/tools/find-tasks.d.ts +7 -2
  45. package/dist/tools/find-tasks.d.ts.map +1 -1
  46. package/dist/tools/find-tasks.js +128 -33
  47. package/dist/tools/get-overview.d.ts +2 -2
  48. package/dist/tools/get-overview.d.ts.map +1 -1
  49. package/dist/tools/get-overview.js +1 -1
  50. package/dist/tools/manage-assignments.d.ts +52 -0
  51. package/dist/tools/manage-assignments.d.ts.map +1 -0
  52. package/dist/tools/manage-assignments.js +337 -0
  53. package/dist/tools/update-comments.d.ts.map +1 -1
  54. package/dist/tools/update-comments.js +1 -1
  55. package/dist/tools/update-sections.d.ts.map +1 -1
  56. package/dist/tools/update-sections.js +1 -1
  57. package/dist/tools/update-tasks.d.ts +17 -7
  58. package/dist/tools/update-tasks.d.ts.map +1 -1
  59. package/dist/tools/update-tasks.js +40 -10
  60. package/dist/utils/assignment-validator.d.ts +69 -0
  61. package/dist/utils/assignment-validator.d.ts.map +1 -0
  62. package/dist/utils/assignment-validator.js +253 -0
  63. package/dist/utils/duration-parser.d.ts +2 -2
  64. package/dist/utils/duration-parser.d.ts.map +1 -1
  65. package/dist/utils/priorities.d.ts +8 -0
  66. package/dist/utils/priorities.d.ts.map +1 -0
  67. package/dist/utils/priorities.js +15 -0
  68. package/dist/utils/response-builders.d.ts +2 -2
  69. package/dist/utils/response-builders.d.ts.map +1 -1
  70. package/dist/utils/response-builders.js +8 -1
  71. package/dist/utils/test-helpers.d.ts +2 -0
  72. package/dist/utils/test-helpers.d.ts.map +1 -1
  73. package/dist/utils/test-helpers.js +2 -0
  74. package/dist/utils/tool-names.d.ts +2 -0
  75. package/dist/utils/tool-names.d.ts.map +1 -1
  76. package/dist/utils/tool-names.js +3 -0
  77. package/dist/utils/user-resolver.d.ts +39 -0
  78. package/dist/utils/user-resolver.d.ts.map +1 -0
  79. package/dist/utils/user-resolver.js +179 -0
  80. package/package.json +6 -6
@@ -0,0 +1,337 @@
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 +1 @@
1
- {"version":3,"file":"update-comments.d.ts","sourceRoot":"","sources":["../../src/tools/update-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAyF88V,CAAC;4BAA6C,CAAC;4BAA6C,CAAC;2BAA4C,CAAC;gCAAiD,CAAC;+BAAgD,CAAC;yBAA2D,CAAC;8BAA+C,CAAC;+BAAgD,CAAC;uBAAwC,CAAC;yBAA0C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;CA5Dt5W,CAAA;AA2D1C,OAAO,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"update-comments.d.ts","sourceRoot":"","sources":["../../src/tools/update-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAqFu9V,CAAC;4BAA6C,CAAC;4BAA6C,CAAC;2BAA4C,CAAC;gCAAiD,CAAC;+BAAgD,CAAC;yBAA2D,CAAC;8BAA+C,CAAC;+BAAgD,CAAC;uBAAwC,CAAC;yBAA0C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;CAxD/5W,CAAA;AAuD1C,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -61,7 +61,7 @@ function generateNextSteps(comments) {
61
61
  nextSteps.push(`Use ${DELETE_OBJECT} with type=comment id=${comment.id} to remove comment`);
62
62
  return nextSteps;
63
63
  }
64
- function generateTextContent({ comments, }) {
64
+ function generateTextContent({ comments }) {
65
65
  // Group comments by entity type and count
66
66
  const taskComments = comments.filter((c) => c.taskId).length;
67
67
  const projectComments = comments.filter((c) => c.projectId).length;
@@ -1 +1 @@
1
- {"version":3,"file":"update-sections.d.ts","sourceRoot":"","sources":["../../src/tools/update-sections.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsBsB,CAAA;AAsD1C,OAAO,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"update-sections.d.ts","sourceRoot":"","sources":["../../src/tools/update-sections.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsBsB,CAAA;AAkD1C,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -57,7 +57,7 @@ function generateNextSteps(sections) {
57
57
  steps.push('Consider updating task descriptions if section purposes changed');
58
58
  return steps;
59
59
  }
60
- function generateTextContent({ sections, }) {
60
+ function generateTextContent({ sections }) {
61
61
  const count = sections.length;
62
62
  const sectionList = sections
63
63
  .map((section) => `• ${section.name} (id=${section.id}, projectId=${section.projectId})`)
@@ -11,30 +11,36 @@ declare const updateTasks: {
11
11
  sectionId: z.ZodOptional<z.ZodString>;
12
12
  parentId: z.ZodOptional<z.ZodString>;
13
13
  order: z.ZodOptional<z.ZodNumber>;
14
- priority: z.ZodOptional<z.ZodNumber>;
14
+ priority: z.ZodOptional<z.ZodEnum<["p1", "p2", "p3", "p4"]>>;
15
15
  dueString: z.ZodOptional<z.ZodString>;
16
16
  duration: z.ZodOptional<z.ZodString>;
17
+ responsibleUser: z.ZodOptional<z.ZodNullable<z.ZodString>>;
18
+ labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
17
19
  }, "strip", z.ZodTypeAny, {
18
20
  id: string;
19
21
  content?: string | undefined;
20
22
  description?: string | undefined;
21
- parentId?: string | undefined;
22
23
  projectId?: string | undefined;
24
+ parentId?: string | undefined;
23
25
  sectionId?: string | undefined;
26
+ labels?: string[] | undefined;
24
27
  duration?: string | undefined;
25
- priority?: number | undefined;
28
+ priority?: "p1" | "p2" | "p3" | "p4" | undefined;
26
29
  dueString?: string | undefined;
30
+ responsibleUser?: string | null | undefined;
27
31
  order?: number | undefined;
28
32
  }, {
29
33
  id: string;
30
34
  content?: string | undefined;
31
35
  description?: string | undefined;
32
- parentId?: string | undefined;
33
36
  projectId?: string | undefined;
37
+ parentId?: string | undefined;
34
38
  sectionId?: string | undefined;
39
+ labels?: string[] | undefined;
35
40
  duration?: string | undefined;
36
- priority?: number | undefined;
41
+ priority?: "p1" | "p2" | "p3" | "p4" | undefined;
37
42
  dueString?: string | undefined;
43
+ responsibleUser?: string | null | undefined;
38
44
  order?: number | undefined;
39
45
  }>, "many">;
40
46
  };
@@ -43,12 +49,14 @@ declare const updateTasks: {
43
49
  id: string;
44
50
  content?: string | undefined;
45
51
  description?: string | undefined;
46
- parentId?: string | undefined;
47
52
  projectId?: string | undefined;
53
+ parentId?: string | undefined;
48
54
  sectionId?: string | undefined;
55
+ labels?: string[] | undefined;
49
56
  duration?: string | undefined;
50
- priority?: number | undefined;
57
+ priority?: "p1" | "p2" | "p3" | "p4" | undefined;
51
58
  dueString?: string | undefined;
59
+ responsibleUser?: string | null | undefined;
52
60
  order?: number | undefined;
53
61
  }[];
54
62
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
@@ -69,6 +77,8 @@ declare const updateTasks: {
69
77
  parentId: string | null;
70
78
  labels: string[];
71
79
  duration: string | null;
80
+ responsibleUid: string | null;
81
+ assignedByUid: string | null;
72
82
  }[];
73
83
  totalCount: number;
74
84
  updatedTaskIds: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"update-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/update-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA2CvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6EyB,CAAA;AAqC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"update-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/update-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAoDvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkHyB,CAAA;AAqC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -1,7 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { getToolOutput } from '../mcp-helpers.js';
3
3
  import { createMoveTaskArgs, mapTask } from '../tool-helpers.js';
4
+ import { assignmentValidator } from '../utils/assignment-validator.js';
4
5
  import { DurationParseError, parseDuration } from '../utils/duration-parser.js';
6
+ import { convertPriorityToNumber, PrioritySchema } from '../utils/priorities.js';
5
7
  import { summarizeTaskOperation } from '../utils/response-builders.js';
6
8
  import { ToolNames } from '../utils/tool-names.js';
7
9
  const { FIND_TASKS_BY_DATE, GET_OVERVIEW } = ToolNames;
@@ -13,13 +15,7 @@ const TasksUpdateSchema = z.object({
13
15
  sectionId: z.string().optional().describe('The new section ID for the task.'),
14
16
  parentId: z.string().optional().describe('The new parent task ID (for subtasks).'),
15
17
  order: z.number().optional().describe('The new order of the task within its parent/section.'),
16
- priority: z
17
- .number()
18
- .int()
19
- .min(1)
20
- .max(4)
21
- .optional()
22
- .describe('The new priority of the task (1-4).'),
18
+ priority: PrioritySchema.optional().describe('The new priority of the task: p1 (highest), p2 (high), p3 (medium), p4 (lowest/default).'),
23
19
  dueString: z
24
20
  .string()
25
21
  .optional()
@@ -28,13 +24,22 @@ const TasksUpdateSchema = z.object({
28
24
  .string()
29
25
  .optional()
30
26
  .describe('The duration of the task. Use format: "2h" (hours), "90m" (minutes), "2h30m" (combined), or "1.5h" (decimal hours). Max 24h.'),
27
+ responsibleUser: z
28
+ .string()
29
+ .nullable()
30
+ .optional()
31
+ .describe('Change task assignment. Use null to unassign. Can be user ID, name, or email. User must be a project collaborator.'),
32
+ labels: z
33
+ .array(z.string())
34
+ .optional()
35
+ .describe('The new labels for the task. Replaces all existing labels.'),
31
36
  });
32
37
  const ArgsSchema = {
33
38
  tasks: z.array(TasksUpdateSchema).min(1).describe('The tasks to update.'),
34
39
  };
35
40
  const updateTasks = {
36
41
  name: ToolNames.UPDATE_TASKS,
37
- description: 'Update multiple existing tasks with new values.',
42
+ description: 'Update existing tasks including content, dates, priorities, and assignments.',
38
43
  parameters: ArgsSchema,
39
44
  async execute(args, client) {
40
45
  const { tasks } = args;
@@ -42,8 +47,15 @@ const updateTasks = {
42
47
  if (!hasUpdatesToMake(task)) {
43
48
  return undefined;
44
49
  }
45
- const { id, projectId, sectionId, parentId, duration: durationStr, ...otherUpdateArgs } = task;
46
- let updateArgs = { ...otherUpdateArgs };
50
+ const { id, projectId, sectionId, parentId, duration: durationStr, responsibleUser, priority, labels, ...otherUpdateArgs } = task;
51
+ let updateArgs = {
52
+ ...otherUpdateArgs,
53
+ ...(labels !== undefined && { labels }),
54
+ };
55
+ // Handle priority conversion if provided
56
+ if (priority) {
57
+ updateArgs.priority = convertPriorityToNumber(priority);
58
+ }
47
59
  // Parse duration if provided
48
60
  if (durationStr) {
49
61
  try {
@@ -61,6 +73,24 @@ const updateTasks = {
61
73
  throw error;
62
74
  }
63
75
  }
76
+ // Handle assignment changes if provided
77
+ if (responsibleUser !== undefined) {
78
+ if (responsibleUser === null) {
79
+ // Unassign task - no validation needed
80
+ updateArgs = { ...updateArgs, assigneeId: null };
81
+ }
82
+ else {
83
+ // Validate assignment using comprehensive validator
84
+ const validation = await assignmentValidator.validateTaskUpdateAssignment(client, id, responsibleUser);
85
+ if (!validation.isValid) {
86
+ const errorMsg = validation.error?.message || 'Assignment validation failed';
87
+ const suggestions = validation.error?.suggestions?.join('. ') || '';
88
+ throw new Error(`Task ${id}: ${errorMsg}${suggestions ? `. ${suggestions}` : ''}`);
89
+ }
90
+ // Use the validated assignee ID
91
+ updateArgs = { ...updateArgs, assigneeId: validation.resolvedUser?.userId };
92
+ }
93
+ }
64
94
  // If no move parameters are provided, use updateTask without moveTasks
65
95
  if (!projectId && !sectionId && !parentId) {
66
96
  return await client.updateTask(id, updateArgs);
@@ -0,0 +1,69 @@
1
+ import type { TodoistApi } from '@doist/todoist-api-typescript';
2
+ import { type ResolvedUser } from './user-resolver.js';
3
+ export declare const AssignmentErrorType: {
4
+ readonly USER_NOT_FOUND: "USER_NOT_FOUND";
5
+ readonly USER_NOT_COLLABORATOR: "USER_NOT_COLLABORATOR";
6
+ readonly PROJECT_NOT_SHARED: "PROJECT_NOT_SHARED";
7
+ readonly TASK_NOT_ACCESSIBLE: "TASK_NOT_ACCESSIBLE";
8
+ readonly PERMISSION_DENIED: "PERMISSION_DENIED";
9
+ readonly PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND";
10
+ readonly TASK_NOT_FOUND: "TASK_NOT_FOUND";
11
+ };
12
+ export type AssignmentErrorType = (typeof AssignmentErrorType)[keyof typeof AssignmentErrorType];
13
+ export type ValidationError = {
14
+ type: AssignmentErrorType;
15
+ message: string;
16
+ suggestions?: string[];
17
+ };
18
+ export type ValidationResult = {
19
+ isValid: boolean;
20
+ resolvedUser?: ResolvedUser;
21
+ error?: ValidationError;
22
+ taskId?: string;
23
+ projectId?: string;
24
+ };
25
+ export type Assignment = {
26
+ taskId?: string;
27
+ projectId: string;
28
+ responsibleUid: string;
29
+ };
30
+ export declare class AssignmentValidator {
31
+ /**
32
+ * Validate a single assignment operation
33
+ */
34
+ validateAssignment(client: TodoistApi, assignment: Assignment): Promise<ValidationResult>;
35
+ /**
36
+ * Validate multiple assignment operations in bulk
37
+ */
38
+ validateBulkAssignment(client: TodoistApi, assignments: Assignment[]): Promise<ValidationResult[]>;
39
+ /**
40
+ * Validate assignment for task creation (no taskId required)
41
+ */
42
+ validateTaskCreationAssignment(client: TodoistApi, projectId: string, responsibleUid: string): Promise<ValidationResult>;
43
+ /**
44
+ * Validate assignment for task update
45
+ */
46
+ validateTaskUpdateAssignment(client: TodoistApi, taskId: string, responsibleUid: string | null): Promise<ValidationResult>;
47
+ /**
48
+ * Get detailed assignment eligibility information for troubleshooting
49
+ */
50
+ getAssignmentEligibility(client: TodoistApi, projectId: string, responsibleUid: string, taskIds?: string[]): Promise<{
51
+ canAssign: boolean;
52
+ projectInfo: {
53
+ name: string;
54
+ isShared: boolean;
55
+ collaboratorCount: number;
56
+ };
57
+ userInfo?: {
58
+ resolvedName: string;
59
+ isCollaborator: boolean;
60
+ };
61
+ taskInfo?: {
62
+ accessibleTasks: number;
63
+ inaccessibleTasks: number;
64
+ };
65
+ recommendations: string[];
66
+ }>;
67
+ }
68
+ export declare const assignmentValidator: AssignmentValidator;
69
+ //# sourceMappingURL=assignment-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignment-validator.d.ts","sourceRoot":"","sources":["../../src/utils/assignment-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAE/D,OAAO,EAAE,KAAK,YAAY,EAAgB,MAAM,oBAAoB,CAAA;AAEpE,eAAO,MAAM,mBAAmB;;;;;;;;CAQtB,CAAA;AAEV,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,OAAO,mBAAmB,CAAC,CAAA;AAEhG,MAAM,MAAM,eAAe,GAAG;IAC1B,IAAI,EAAE,mBAAmB,CAAA;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,qBAAa,mBAAmB;IAC5B;;OAEG;IACG,kBAAkB,CACpB,MAAM,EAAE,UAAU,EAClB,UAAU,EAAE,UAAU,GACvB,OAAO,CAAC,gBAAgB,CAAC;IAqH5B;;OAEG;IACG,sBAAsB,CACxB,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,UAAU,EAAE,GAC1B,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAS9B;;OAEG;IACG,8BAA8B,CAChC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACvB,OAAO,CAAC,gBAAgB,CAAC;IAO5B;;OAEG;IACG,4BAA4B,CAC9B,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GAAG,IAAI,GAC9B,OAAO,CAAC,gBAAgB,CAAC;IAkC5B;;OAEG;IACG,wBAAwB,CAC1B,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC;QACP,SAAS,EAAE,OAAO,CAAA;QAClB,WAAW,EAAE;YACT,IAAI,EAAE,MAAM,CAAA;YACZ,QAAQ,EAAE,OAAO,CAAA;YACjB,iBAAiB,EAAE,MAAM,CAAA;SAC5B,CAAA;QACD,QAAQ,CAAC,EAAE;YACP,YAAY,EAAE,MAAM,CAAA;YACpB,cAAc,EAAE,OAAO,CAAA;SAC1B,CAAA;QACD,QAAQ,CAAC,EAAE;YACP,eAAe,EAAE,MAAM,CAAA;YACvB,iBAAiB,EAAE,MAAM,CAAA;SAC5B,CAAA;QACD,eAAe,EAAE,MAAM,EAAE,CAAA;KAC5B,CAAC;CAqFL;AAGD,eAAO,MAAM,mBAAmB,qBAA4B,CAAA"}