@doist/todoist-ai 4.16.0 → 4.17.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 (178) hide show
  1. package/dist/filter-helpers.d.ts +1 -1
  2. package/dist/index.d.ts +1044 -196
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +61 -81
  5. package/dist/main.js +15 -23
  6. package/dist/mcp-helpers.d.ts +5 -5
  7. package/dist/mcp-helpers.d.ts.map +1 -1
  8. package/dist/mcp-server-BADReNAy.js +3092 -0
  9. package/dist/todoist-tool.d.ts +9 -3
  10. package/dist/todoist-tool.d.ts.map +1 -1
  11. package/dist/tool-helpers.d.ts +1 -1
  12. package/dist/tools/add-comments.d.ts +69 -3
  13. package/dist/tools/add-comments.d.ts.map +1 -1
  14. package/dist/tools/add-projects.d.ts +34 -3
  15. package/dist/tools/add-projects.d.ts.map +1 -1
  16. package/dist/tools/add-sections.d.ts +14 -1
  17. package/dist/tools/add-sections.d.ts.map +1 -1
  18. package/dist/tools/add-tasks.d.ts +65 -10
  19. package/dist/tools/add-tasks.d.ts.map +1 -1
  20. package/dist/tools/complete-tasks.d.ts +20 -1
  21. package/dist/tools/complete-tasks.d.ts.map +1 -1
  22. package/dist/tools/delete-object.d.ts +16 -3
  23. package/dist/tools/delete-object.d.ts.map +1 -1
  24. package/dist/tools/fetch.d.ts +8 -1
  25. package/dist/tools/fetch.d.ts.map +1 -1
  26. package/dist/tools/find-activity.d.ts +44 -7
  27. package/dist/tools/find-activity.d.ts.map +1 -1
  28. package/dist/tools/find-comments.d.ts +69 -3
  29. package/dist/tools/find-comments.d.ts.map +1 -1
  30. package/dist/tools/find-completed-tasks.d.ts +63 -5
  31. package/dist/tools/find-completed-tasks.d.ts.map +1 -1
  32. package/dist/tools/find-project-collaborators.d.ts +33 -2
  33. package/dist/tools/find-project-collaborators.d.ts.map +1 -1
  34. package/dist/tools/find-projects.d.ts +35 -1
  35. package/dist/tools/find-projects.d.ts.map +1 -1
  36. package/dist/tools/find-sections.d.ts +15 -1
  37. package/dist/tools/find-sections.d.ts.map +1 -1
  38. package/dist/tools/find-tasks-by-date.d.ts +61 -3
  39. package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
  40. package/dist/tools/find-tasks.d.ts +63 -5
  41. package/dist/tools/find-tasks.d.ts.map +1 -1
  42. package/dist/tools/get-overview.d.ts +24 -1
  43. package/dist/tools/get-overview.d.ts.map +1 -1
  44. package/dist/tools/manage-assignments.d.ts +39 -2
  45. package/dist/tools/manage-assignments.d.ts.map +1 -1
  46. package/dist/tools/search.d.ts +17 -1
  47. package/dist/tools/search.d.ts.map +1 -1
  48. package/dist/tools/update-comments.d.ts +76 -3
  49. package/dist/tools/update-comments.d.ts.map +1 -1
  50. package/dist/tools/update-projects.d.ts +43 -1
  51. package/dist/tools/update-projects.d.ts.map +1 -1
  52. package/dist/tools/update-sections.d.ts +17 -3
  53. package/dist/tools/update-sections.d.ts.map +1 -1
  54. package/dist/tools/update-tasks.d.ts +79 -13
  55. package/dist/tools/update-tasks.d.ts.map +1 -1
  56. package/dist/tools/user-info.d.ts +19 -1
  57. package/dist/tools/user-info.d.ts.map +1 -1
  58. package/dist/utils/assignment-validator.d.ts +2 -2
  59. package/dist/utils/output-schemas.d.ts +233 -0
  60. package/dist/utils/output-schemas.d.ts.map +1 -0
  61. package/dist/utils/response-builders.d.ts +1 -3
  62. package/dist/utils/response-builders.d.ts.map +1 -1
  63. package/dist/utils/test-helpers.d.ts +1 -1
  64. package/dist/utils/user-resolver.d.ts +1 -1
  65. package/package.json +10 -8
  66. package/dist/filter-helpers.js +0 -79
  67. package/dist/mcp-helpers.js +0 -71
  68. package/dist/mcp-server.js +0 -142
  69. package/dist/todoist-tool.js +0 -1
  70. package/dist/tool-helpers.js +0 -125
  71. package/dist/tool-helpers.test.d.ts +0 -2
  72. package/dist/tool-helpers.test.d.ts.map +0 -1
  73. package/dist/tool-helpers.test.js +0 -223
  74. package/dist/tools/__tests__/add-comments.test.d.ts +0 -2
  75. package/dist/tools/__tests__/add-comments.test.d.ts.map +0 -1
  76. package/dist/tools/__tests__/add-comments.test.js +0 -241
  77. package/dist/tools/__tests__/add-projects.test.d.ts +0 -2
  78. package/dist/tools/__tests__/add-projects.test.d.ts.map +0 -1
  79. package/dist/tools/__tests__/add-projects.test.js +0 -174
  80. package/dist/tools/__tests__/add-sections.test.d.ts +0 -2
  81. package/dist/tools/__tests__/add-sections.test.d.ts.map +0 -1
  82. package/dist/tools/__tests__/add-sections.test.js +0 -185
  83. package/dist/tools/__tests__/add-tasks.test.d.ts +0 -2
  84. package/dist/tools/__tests__/add-tasks.test.d.ts.map +0 -1
  85. package/dist/tools/__tests__/add-tasks.test.js +0 -606
  86. package/dist/tools/__tests__/assignment-integration.test.d.ts +0 -2
  87. package/dist/tools/__tests__/assignment-integration.test.d.ts.map +0 -1
  88. package/dist/tools/__tests__/assignment-integration.test.js +0 -428
  89. package/dist/tools/__tests__/complete-tasks.test.d.ts +0 -2
  90. package/dist/tools/__tests__/complete-tasks.test.d.ts.map +0 -1
  91. package/dist/tools/__tests__/complete-tasks.test.js +0 -206
  92. package/dist/tools/__tests__/delete-object.test.d.ts +0 -2
  93. package/dist/tools/__tests__/delete-object.test.d.ts.map +0 -1
  94. package/dist/tools/__tests__/delete-object.test.js +0 -110
  95. package/dist/tools/__tests__/fetch.test.d.ts +0 -2
  96. package/dist/tools/__tests__/fetch.test.d.ts.map +0 -1
  97. package/dist/tools/__tests__/fetch.test.js +0 -279
  98. package/dist/tools/__tests__/find-activity.test.d.ts +0 -2
  99. package/dist/tools/__tests__/find-activity.test.d.ts.map +0 -1
  100. package/dist/tools/__tests__/find-activity.test.js +0 -229
  101. package/dist/tools/__tests__/find-comments.test.d.ts +0 -2
  102. package/dist/tools/__tests__/find-comments.test.d.ts.map +0 -1
  103. package/dist/tools/__tests__/find-comments.test.js +0 -236
  104. package/dist/tools/__tests__/find-completed-tasks.test.d.ts +0 -2
  105. package/dist/tools/__tests__/find-completed-tasks.test.d.ts.map +0 -1
  106. package/dist/tools/__tests__/find-completed-tasks.test.js +0 -423
  107. package/dist/tools/__tests__/find-projects.test.d.ts +0 -2
  108. package/dist/tools/__tests__/find-projects.test.d.ts.map +0 -1
  109. package/dist/tools/__tests__/find-projects.test.js +0 -154
  110. package/dist/tools/__tests__/find-sections.test.d.ts +0 -2
  111. package/dist/tools/__tests__/find-sections.test.d.ts.map +0 -1
  112. package/dist/tools/__tests__/find-sections.test.js +0 -313
  113. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts +0 -2
  114. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts.map +0 -1
  115. package/dist/tools/__tests__/find-tasks-by-date.test.js +0 -528
  116. package/dist/tools/__tests__/find-tasks.test.d.ts +0 -2
  117. package/dist/tools/__tests__/find-tasks.test.d.ts.map +0 -1
  118. package/dist/tools/__tests__/find-tasks.test.js +0 -771
  119. package/dist/tools/__tests__/get-overview.test.d.ts +0 -2
  120. package/dist/tools/__tests__/get-overview.test.d.ts.map +0 -1
  121. package/dist/tools/__tests__/get-overview.test.js +0 -225
  122. package/dist/tools/__tests__/search.test.d.ts +0 -2
  123. package/dist/tools/__tests__/search.test.d.ts.map +0 -1
  124. package/dist/tools/__tests__/search.test.js +0 -206
  125. package/dist/tools/__tests__/update-comments.test.d.ts +0 -2
  126. package/dist/tools/__tests__/update-comments.test.d.ts.map +0 -1
  127. package/dist/tools/__tests__/update-comments.test.js +0 -294
  128. package/dist/tools/__tests__/update-projects.test.d.ts +0 -2
  129. package/dist/tools/__tests__/update-projects.test.d.ts.map +0 -1
  130. package/dist/tools/__tests__/update-projects.test.js +0 -217
  131. package/dist/tools/__tests__/update-sections.test.d.ts +0 -2
  132. package/dist/tools/__tests__/update-sections.test.d.ts.map +0 -1
  133. package/dist/tools/__tests__/update-sections.test.js +0 -169
  134. package/dist/tools/__tests__/update-tasks.test.d.ts +0 -2
  135. package/dist/tools/__tests__/update-tasks.test.d.ts.map +0 -1
  136. package/dist/tools/__tests__/update-tasks.test.js +0 -788
  137. package/dist/tools/__tests__/user-info.test.d.ts +0 -2
  138. package/dist/tools/__tests__/user-info.test.d.ts.map +0 -1
  139. package/dist/tools/__tests__/user-info.test.js +0 -139
  140. package/dist/tools/add-comments.js +0 -89
  141. package/dist/tools/add-projects.js +0 -63
  142. package/dist/tools/add-sections.js +0 -74
  143. package/dist/tools/add-tasks.js +0 -169
  144. package/dist/tools/complete-tasks.js +0 -68
  145. package/dist/tools/delete-object.js +0 -79
  146. package/dist/tools/fetch.js +0 -102
  147. package/dist/tools/find-activity.js +0 -221
  148. package/dist/tools/find-comments.js +0 -148
  149. package/dist/tools/find-completed-tasks.js +0 -168
  150. package/dist/tools/find-project-collaborators.js +0 -151
  151. package/dist/tools/find-projects.js +0 -101
  152. package/dist/tools/find-sections.js +0 -101
  153. package/dist/tools/find-tasks-by-date.js +0 -198
  154. package/dist/tools/find-tasks.js +0 -329
  155. package/dist/tools/get-overview.js +0 -249
  156. package/dist/tools/manage-assignments.js +0 -337
  157. package/dist/tools/search.js +0 -65
  158. package/dist/tools/update-comments.js +0 -82
  159. package/dist/tools/update-projects.js +0 -84
  160. package/dist/tools/update-sections.js +0 -70
  161. package/dist/tools/update-tasks.js +0 -179
  162. package/dist/tools/user-info.js +0 -142
  163. package/dist/utils/assignment-validator.js +0 -253
  164. package/dist/utils/constants.js +0 -45
  165. package/dist/utils/duration-parser.js +0 -96
  166. package/dist/utils/duration-parser.test.d.ts +0 -2
  167. package/dist/utils/duration-parser.test.d.ts.map +0 -1
  168. package/dist/utils/duration-parser.test.js +0 -147
  169. package/dist/utils/labels.js +0 -18
  170. package/dist/utils/priorities.js +0 -20
  171. package/dist/utils/response-builders.js +0 -210
  172. package/dist/utils/sanitize-data.js +0 -37
  173. package/dist/utils/sanitize-data.test.d.ts +0 -2
  174. package/dist/utils/sanitize-data.test.d.ts.map +0 -1
  175. package/dist/utils/sanitize-data.test.js +0 -93
  176. package/dist/utils/test-helpers.js +0 -237
  177. package/dist/utils/tool-names.js +0 -40
  178. package/dist/utils/user-resolver.js +0 -179
@@ -1,249 +0,0 @@
1
- import { z } from 'zod';
2
- import { getToolOutput } from '../mcp-helpers.js';
3
- import { isPersonalProject, mapTask } from '../tool-helpers.js';
4
- import { ApiLimits } from '../utils/constants.js';
5
- import { ToolNames } from '../utils/tool-names.js';
6
- const ArgsSchema = {
7
- projectId: z
8
- .string()
9
- .min(1)
10
- .optional()
11
- .describe('Optional project ID. If provided, shows detailed overview of that project. If omitted, shows overview of all projects.'),
12
- };
13
- function buildProjectTree(projects) {
14
- // Sort projects by childOrder, then build a tree
15
- const byId = {};
16
- for (const p of projects) {
17
- byId[p.id] = {
18
- ...p,
19
- children: [],
20
- childOrder: p.childOrder ?? 0,
21
- };
22
- }
23
- const roots = [];
24
- for (const p of projects) {
25
- const current = byId[p.id];
26
- if (!current)
27
- continue;
28
- if (isPersonalProject(p) && p.parentId) {
29
- const parent = byId[p.parentId];
30
- if (parent) {
31
- parent.children.push(current);
32
- }
33
- else {
34
- roots.push(current);
35
- }
36
- }
37
- else {
38
- roots.push(current);
39
- }
40
- }
41
- function sortTree(nodes) {
42
- nodes.sort((a, b) => a.childOrder - b.childOrder);
43
- for (const n of nodes) {
44
- sortTree(n.children);
45
- }
46
- }
47
- sortTree(roots);
48
- return roots;
49
- }
50
- async function getSectionsByProject(client, projectIds) {
51
- const result = {};
52
- await Promise.all(projectIds.map(async (projectId) => {
53
- const { results } = await client.getSections({ projectId });
54
- result[projectId] = results;
55
- }));
56
- return result;
57
- }
58
- function renderProjectMarkdown(project, sectionsByProject, indent = '') {
59
- const lines = [];
60
- lines.push(`${indent}- Project: ${project.name} (id=${project.id})`);
61
- const sections = sectionsByProject[project.id] || [];
62
- for (const section of sections) {
63
- lines.push(`${indent} - Section: ${section.name} (id=${section.id})`);
64
- }
65
- for (const child of project.children) {
66
- lines.push(...renderProjectMarkdown(child, sectionsByProject, `${indent} `));
67
- }
68
- return lines;
69
- }
70
- function buildTaskTree(tasks) {
71
- const byId = {};
72
- for (const task of tasks) {
73
- byId[task.id] = { ...task, children: [] };
74
- }
75
- const roots = [];
76
- for (const task of tasks) {
77
- const node = byId[task.id];
78
- if (!node)
79
- continue;
80
- if (!task.parentId) {
81
- roots.push(node);
82
- continue;
83
- }
84
- const parent = byId[task.parentId];
85
- if (parent) {
86
- parent.children.push(node);
87
- }
88
- else {
89
- roots.push(node);
90
- }
91
- }
92
- return roots;
93
- }
94
- function renderTaskTreeMarkdown(tasks, indent = '') {
95
- const lines = [];
96
- for (const task of tasks) {
97
- const idPart = `id=${task.id}`;
98
- const duePart = task.dueDate ? `; due=${task.dueDate}` : '';
99
- const contentPart = `; content=${task.content}`;
100
- lines.push(`${indent}- ${idPart}${duePart}${contentPart}`);
101
- if (task.children.length > 0) {
102
- lines.push(...renderTaskTreeMarkdown(task.children, `${indent} `));
103
- }
104
- }
105
- return lines;
106
- }
107
- function buildProjectStructure(project, sectionsByProject) {
108
- return {
109
- id: project.id,
110
- name: project.name,
111
- parentId: isPersonalProject(project) ? (project.parentId ?? null) : null,
112
- sections: sectionsByProject[project.id] || [],
113
- children: project.children.map((child) => buildProjectStructure(child, sectionsByProject)),
114
- };
115
- }
116
- async function getAllTasksForProject(client, projectId) {
117
- let allTasks = [];
118
- let cursor;
119
- do {
120
- const { results, nextCursor } = await client.getTasks({
121
- projectId,
122
- limit: ApiLimits.TASKS_BATCH_SIZE,
123
- cursor: cursor ?? undefined,
124
- });
125
- allTasks = allTasks.concat(results.map(mapTask));
126
- cursor = nextCursor ?? undefined;
127
- } while (cursor);
128
- return allTasks;
129
- }
130
- async function getProjectSections(client, projectId) {
131
- const { results } = await client.getSections({ projectId });
132
- return results;
133
- }
134
- async function generateAccountOverview(client) {
135
- const { results: projects } = await client.getProjects({});
136
- const inbox = projects.find((p) => isPersonalProject(p) && p.inboxProject === true);
137
- const nonInbox = projects.filter((p) => !isPersonalProject(p) || p.inboxProject !== true);
138
- const tree = buildProjectTree(nonInbox);
139
- const allProjectIds = projects.map((p) => p.id);
140
- const sectionsByProject = await getSectionsByProject(client, allProjectIds);
141
- // Generate markdown text content
142
- const lines = ['# Personal Projects', ''];
143
- if (inbox) {
144
- lines.push(`- Inbox Project: ${inbox.name} (id=${inbox.id})`);
145
- for (const section of sectionsByProject[inbox.id] || []) {
146
- lines.push(` - Section: ${section.name} (id=${section.id})`);
147
- }
148
- }
149
- if (tree.length) {
150
- for (const project of tree) {
151
- lines.push(...renderProjectMarkdown(project, sectionsByProject));
152
- }
153
- }
154
- else {
155
- lines.push('_No projects found._');
156
- }
157
- lines.push('');
158
- // Add explanation about nesting if there are nested projects
159
- const hasNested = tree.some((p) => p.children.length > 0);
160
- if (hasNested) {
161
- lines.push('_Note: Indentation indicates that a project is a sub-project of the one above it. This allows for organizing projects hierarchically, with parent projects containing related sub-projects._', '');
162
- }
163
- const textContent = lines.join('\n');
164
- // Generate structured content
165
- const structuredContent = {
166
- type: 'account_overview',
167
- inbox: inbox
168
- ? {
169
- id: inbox.id,
170
- name: inbox.name,
171
- sections: sectionsByProject[inbox.id] || [],
172
- }
173
- : null,
174
- projects: tree.map((project) => buildProjectStructure(project, sectionsByProject)),
175
- totalProjects: projects.length,
176
- totalSections: allProjectIds.reduce((total, id) => total + (sectionsByProject[id]?.length || 0), 0),
177
- hasNestedProjects: hasNested,
178
- };
179
- return { textContent, structuredContent };
180
- }
181
- async function generateProjectOverview(client, projectId) {
182
- const project = await client.getProject(projectId);
183
- const sections = await getProjectSections(client, projectId);
184
- const allTasks = await getAllTasksForProject(client, projectId);
185
- // Group tasks by sectionId
186
- const tasksBySection = {};
187
- for (const section of sections) {
188
- tasksBySection[section.id] = [];
189
- }
190
- const tasksWithoutSection = [];
191
- for (const task of allTasks) {
192
- const sectionTasks = task.sectionId
193
- ? (tasksBySection[task.sectionId] ?? tasksWithoutSection)
194
- : tasksWithoutSection;
195
- sectionTasks.push(task);
196
- }
197
- // Generate markdown text content
198
- const lines = [`# ${project.name}`];
199
- if (tasksWithoutSection.length > 0) {
200
- lines.push('');
201
- const tree = buildTaskTree(tasksWithoutSection);
202
- lines.push(...renderTaskTreeMarkdown(tree));
203
- }
204
- for (const section of sections) {
205
- lines.push('');
206
- lines.push(`## ${section.name}`);
207
- const sectionTasks = tasksBySection[section.id];
208
- if (!sectionTasks?.length) {
209
- continue;
210
- }
211
- const tree = buildTaskTree(sectionTasks);
212
- lines.push(...renderTaskTreeMarkdown(tree));
213
- }
214
- const textContent = lines.join('\n');
215
- // Generate structured content
216
- const structuredContent = {
217
- type: 'project_overview',
218
- project: {
219
- id: project.id,
220
- name: project.name,
221
- },
222
- sections: sections,
223
- tasks: allTasks.map((task) => ({
224
- ...task,
225
- children: [], // Tasks already include hierarchical info via parentId
226
- })),
227
- stats: {
228
- totalTasks: allTasks.length,
229
- totalSections: sections.length,
230
- tasksWithoutSection: tasksWithoutSection.length,
231
- },
232
- };
233
- return { textContent, structuredContent };
234
- }
235
- const getOverview = {
236
- name: ToolNames.GET_OVERVIEW,
237
- description: 'Get a Markdown overview. If no projectId is provided, shows all projects with hierarchy and sections (useful for navigation). If projectId is provided, shows detailed overview of that specific project including all tasks grouped by sections.',
238
- parameters: ArgsSchema,
239
- async execute(args, client) {
240
- const result = args.projectId
241
- ? await generateProjectOverview(client, args.projectId)
242
- : await generateAccountOverview(client);
243
- return getToolOutput({
244
- textContent: result.textContent,
245
- structuredContent: result.structuredContent,
246
- });
247
- },
248
- };
249
- export { getOverview };
@@ -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 };