@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,102 +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 { mapProject, mapTask } from '../tool-helpers.js';
5
- import { ToolNames } from '../utils/tool-names.js';
6
- const ArgsSchema = {
7
- id: z
8
- .string()
9
- .min(1)
10
- .describe('A unique identifier for the document in the format "task:{id}" or "project:{id}".'),
11
- };
12
- /**
13
- * OpenAI MCP fetch tool - retrieves the full contents of a task or project by ID.
14
- *
15
- * This tool follows the OpenAI MCP fetch tool specification:
16
- * @see https://platform.openai.com/docs/mcp#fetch-tool
17
- */
18
- const fetch = {
19
- name: ToolNames.FETCH,
20
- description: 'Fetch the full contents of a task or project by its ID. The ID should be in the format "task:{id}" or "project:{id}".',
21
- parameters: ArgsSchema,
22
- async execute(args, client) {
23
- try {
24
- const { id } = args;
25
- // Parse the composite ID
26
- const [type, objectId] = id.split(':', 2);
27
- if (!objectId || (type !== 'task' && type !== 'project')) {
28
- throw new Error('Invalid ID format. Expected "task:{id}" or "project:{id}". Example: "task:8485093748" or "project:6cfCcrrCFg2xP94Q"');
29
- }
30
- let result;
31
- if (type === 'task') {
32
- // Fetch task
33
- const task = await client.getTask(objectId);
34
- const mappedTask = mapTask(task);
35
- // Build text content
36
- const textParts = [mappedTask.content];
37
- if (mappedTask.description) {
38
- textParts.push(`\n\nDescription: ${mappedTask.description}`);
39
- }
40
- if (mappedTask.dueDate) {
41
- textParts.push(`\nDue: ${mappedTask.dueDate}`);
42
- }
43
- if (mappedTask.labels.length > 0) {
44
- textParts.push(`\nLabels: ${mappedTask.labels.join(', ')}`);
45
- }
46
- result = {
47
- id: `task:${mappedTask.id}`,
48
- title: mappedTask.content,
49
- text: textParts.join(''),
50
- url: getTaskUrl(mappedTask.id),
51
- metadata: {
52
- priority: mappedTask.priority,
53
- projectId: mappedTask.projectId,
54
- sectionId: mappedTask.sectionId,
55
- parentId: mappedTask.parentId,
56
- recurring: mappedTask.recurring,
57
- duration: mappedTask.duration,
58
- responsibleUid: mappedTask.responsibleUid,
59
- assignedByUid: mappedTask.assignedByUid,
60
- checked: mappedTask.checked,
61
- completedAt: mappedTask.completedAt,
62
- },
63
- };
64
- }
65
- else {
66
- // Fetch project
67
- const project = await client.getProject(objectId);
68
- const mappedProject = mapProject(project);
69
- // Build text content
70
- const textParts = [mappedProject.name];
71
- if (mappedProject.isShared) {
72
- textParts.push('\n\nShared project');
73
- }
74
- if (mappedProject.isFavorite) {
75
- textParts.push('\nFavorite: Yes');
76
- }
77
- result = {
78
- id: `project:${mappedProject.id}`,
79
- title: mappedProject.name,
80
- text: textParts.join(''),
81
- url: getProjectUrl(mappedProject.id),
82
- metadata: {
83
- color: mappedProject.color,
84
- isFavorite: mappedProject.isFavorite,
85
- isShared: mappedProject.isShared,
86
- parentId: mappedProject.parentId,
87
- inboxProject: mappedProject.inboxProject,
88
- viewStyle: mappedProject.viewStyle,
89
- },
90
- };
91
- }
92
- // Return as JSON-encoded string in a text content item (OpenAI MCP spec)
93
- const jsonText = JSON.stringify(result);
94
- return { content: [{ type: 'text', text: jsonText }] };
95
- }
96
- catch (error) {
97
- const message = error instanceof Error ? error.message : 'An unknown error occurred';
98
- return getErrorOutput(message);
99
- }
100
- },
101
- };
102
- export { fetch };
@@ -1,221 +0,0 @@
1
- import { z } from 'zod';
2
- import { getToolOutput } from '../mcp-helpers.js';
3
- import { mapActivityEvent } from '../tool-helpers.js';
4
- import { ApiLimits } from '../utils/constants.js';
5
- import { summarizeList } from '../utils/response-builders.js';
6
- import { ToolNames } from '../utils/tool-names.js';
7
- const { FIND_TASKS, USER_INFO } = ToolNames;
8
- const ArgsSchema = {
9
- objectType: z
10
- .enum(['task', 'project', 'comment'])
11
- .optional()
12
- .describe('Type of object to filter by.'),
13
- objectId: z
14
- .string()
15
- .optional()
16
- .describe('Filter by specific object ID (task, project, or comment).'),
17
- eventType: z
18
- .enum([
19
- 'added',
20
- 'updated',
21
- 'deleted',
22
- 'completed',
23
- 'uncompleted',
24
- 'archived',
25
- 'unarchived',
26
- 'shared',
27
- 'left',
28
- ])
29
- .optional()
30
- .describe('Type of event to filter by.'),
31
- projectId: z.string().optional().describe('Filter events by parent project ID.'),
32
- taskId: z.string().optional().describe('Filter events by parent task ID (for subtask events).'),
33
- initiatorId: z.string().optional().describe('Filter by the user ID who initiated the event.'),
34
- limit: z
35
- .number()
36
- .int()
37
- .min(1)
38
- .max(ApiLimits.ACTIVITY_MAX)
39
- .default(ApiLimits.ACTIVITY_DEFAULT)
40
- .describe('Maximum number of activity events to return.'),
41
- cursor: z
42
- .string()
43
- .optional()
44
- .describe('Pagination cursor for retrieving the next page of results.'),
45
- };
46
- const findActivity = {
47
- name: ToolNames.FIND_ACTIVITY,
48
- description: 'Retrieve recent activity logs to monitor and audit changes in Todoist. Shows events from all users by default (use initiatorId to filter by specific user). Track task completions, updates, deletions, project changes, and more with flexible filtering. Note: Date-based filtering is not supported by the Todoist API.',
49
- parameters: ArgsSchema,
50
- async execute(args, client) {
51
- const { objectType, objectId, eventType, projectId, taskId, initiatorId, limit, cursor } = args;
52
- // Build API arguments
53
- const apiArgs = {
54
- limit,
55
- cursor: cursor ?? null,
56
- };
57
- // Add optional filters
58
- if (objectType)
59
- apiArgs.objectType = objectType;
60
- if (objectId && objectId !== 'remove')
61
- apiArgs.objectId = objectId;
62
- if (eventType)
63
- apiArgs.eventType = eventType;
64
- if (projectId)
65
- apiArgs.parentProjectId = projectId;
66
- if (taskId)
67
- apiArgs.parentItemId = taskId;
68
- if (initiatorId)
69
- apiArgs.initiatorId = initiatorId;
70
- // Fetch activity logs from API
71
- const { results, nextCursor } = await client.getActivityLogs(apiArgs);
72
- const events = results.map(mapActivityEvent);
73
- // Generate text content
74
- const textContent = generateTextContent({
75
- events,
76
- args,
77
- nextCursor,
78
- });
79
- return getToolOutput({
80
- textContent,
81
- structuredContent: {
82
- events,
83
- nextCursor,
84
- totalCount: events.length,
85
- hasMore: Boolean(nextCursor),
86
- appliedFilters: args,
87
- },
88
- });
89
- },
90
- };
91
- function generateTextContent({ events, args, nextCursor, }) {
92
- // Generate subject description
93
- let subject = 'Activity events';
94
- // Build subject based on filters
95
- const subjectParts = [];
96
- if (args.eventType) {
97
- subjectParts.push(`${args.eventType}`);
98
- }
99
- if (args.objectType) {
100
- const objectLabel = args.objectType === 'task' ? 'tasks' : `${args.objectType}s`;
101
- subjectParts.push(objectLabel);
102
- }
103
- if (subjectParts.length > 0) {
104
- subject = `Activity: ${subjectParts.join(' ')}`;
105
- }
106
- // Generate filter hints
107
- const filterHints = [];
108
- if (args.objectId) {
109
- filterHints.push(`object ID: ${args.objectId}`);
110
- }
111
- if (args.projectId) {
112
- filterHints.push(`project: ${args.projectId}`);
113
- }
114
- if (args.taskId) {
115
- filterHints.push(`task: ${args.taskId}`);
116
- }
117
- if (args.initiatorId) {
118
- filterHints.push(`initiator: ${args.initiatorId}`);
119
- }
120
- // Generate helpful suggestions for empty results
121
- const zeroReasonHints = [];
122
- if (events.length === 0) {
123
- zeroReasonHints.push('No activity events match the specified filters');
124
- zeroReasonHints.push('Note: Activity logs only show recent events');
125
- if (args.eventType) {
126
- zeroReasonHints.push(`Try removing the eventType filter (${args.eventType})`);
127
- }
128
- if (args.objectType) {
129
- zeroReasonHints.push(`Try removing the objectType filter (${args.objectType})`);
130
- }
131
- if (args.objectId || args.projectId || args.taskId) {
132
- zeroReasonHints.push('Verify the object ID is correct');
133
- }
134
- }
135
- // Generate contextual next steps
136
- const nextSteps = [];
137
- if (events.length > 0) {
138
- // Suggest related tools based on what was found
139
- const hasTaskEvents = events.some((e) => e.objectType === 'task' || e.objectType === 'item');
140
- const hasCompletions = events.some((e) => e.eventType === 'completed');
141
- if (hasTaskEvents) {
142
- nextSteps.push(`Use ${FIND_TASKS} to view current task details`);
143
- }
144
- if (hasCompletions) {
145
- nextSteps.push('Review completed tasks to track productivity');
146
- }
147
- if (args.initiatorId) {
148
- nextSteps.push(`Use ${USER_INFO} to get details about the user`);
149
- }
150
- // Suggest narrowing down if too many results
151
- if (events.length >= args.limit && !nextCursor) {
152
- nextSteps.push('Add more specific filters to narrow down results');
153
- }
154
- }
155
- return summarizeList({
156
- subject,
157
- count: events.length,
158
- limit: args.limit,
159
- nextCursor: nextCursor ?? undefined,
160
- filterHints,
161
- previewLines: previewActivityEvents(events, Math.min(events.length, args.limit)),
162
- zeroReasonHints,
163
- nextSteps,
164
- });
165
- }
166
- /**
167
- * Formats activity events into readable preview lines
168
- */
169
- function previewActivityEvents(events, limit = 10) {
170
- const previewEvents = events.slice(0, limit);
171
- const lines = previewEvents.map(formatActivityEventPreview).join('\n');
172
- // If we're showing fewer events than the total, add an indicator
173
- if (events.length > limit) {
174
- const remaining = events.length - limit;
175
- return `${lines}\n ... and ${remaining} more event${remaining === 1 ? '' : 's'}`;
176
- }
177
- return lines;
178
- }
179
- /**
180
- * Formats a single activity event into a readable preview line
181
- */
182
- function formatActivityEventPreview(event) {
183
- const date = formatEventDate(event.eventDate);
184
- const eventLabel = `${event.eventType} ${event.objectType}`;
185
- // Extract useful content from extraData if available
186
- let contentInfo = '';
187
- if (event.extraData) {
188
- const content = event.extraData.content || event.extraData.name || event.extraData.last_content;
189
- if (content && typeof content === 'string') {
190
- // Truncate long content
191
- const truncated = content.length > 50 ? `${content.substring(0, 47)}...` : content;
192
- contentInfo = ` • "${truncated}"`;
193
- }
194
- }
195
- const objectId = event.objectId ? ` • id=${event.objectId}` : '';
196
- const initiator = event.initiatorId ? ` • by=${event.initiatorId}` : ' • system';
197
- const projectInfo = event.parentProjectId ? ` • project=${event.parentProjectId}` : '';
198
- return ` [${date}] ${eventLabel}${contentInfo}${objectId}${initiator}${projectInfo}`;
199
- }
200
- /**
201
- * Formats an ISO date string to a more readable format
202
- */
203
- function formatEventDate(isoDate) {
204
- try {
205
- const date = new Date(isoDate);
206
- // Format as: Oct 23, 14:30 (in UTC for deterministic snapshots)
207
- const month = date.toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' });
208
- const day = date.toLocaleDateString('en-US', { day: 'numeric', timeZone: 'UTC' });
209
- const time = date.toLocaleTimeString('en-US', {
210
- hour: '2-digit',
211
- minute: '2-digit',
212
- hour12: false,
213
- timeZone: 'UTC',
214
- });
215
- return `${month} ${day}, ${time}`;
216
- }
217
- catch {
218
- return isoDate;
219
- }
220
- }
221
- export { findActivity };
@@ -1,143 +0,0 @@
1
- import { z } from 'zod';
2
- import { getToolOutput } from '../mcp-helpers.js';
3
- import { ApiLimits } from '../utils/constants.js';
4
- import { formatNextSteps } from '../utils/response-builders.js';
5
- import { ToolNames } from '../utils/tool-names.js';
6
- const { ADD_COMMENTS, UPDATE_COMMENTS, DELETE_OBJECT } = ToolNames;
7
- const ArgsSchema = {
8
- taskId: z.string().optional().describe('Find comments for a specific task.'),
9
- projectId: z.string().optional().describe('Find comments for a specific project.'),
10
- commentId: z.string().optional().describe('Get a specific comment by ID.'),
11
- cursor: z.string().optional().describe('Pagination cursor for retrieving more results.'),
12
- limit: z
13
- .number()
14
- .int()
15
- .min(1)
16
- .max(ApiLimits.COMMENTS_MAX)
17
- .optional()
18
- .describe('Maximum number of comments to return'),
19
- };
20
- const findComments = {
21
- name: ToolNames.FIND_COMMENTS,
22
- description: 'Find comments by task, project, or get a specific comment by ID. Exactly one of taskId, projectId, or commentId must be provided.',
23
- parameters: ArgsSchema,
24
- async execute(args, client) {
25
- // Validate that exactly one search parameter is provided
26
- const searchParams = [args.taskId, args.projectId, args.commentId].filter(Boolean);
27
- if (searchParams.length === 0) {
28
- throw new Error('Must provide exactly one of: taskId, projectId, or commentId.');
29
- }
30
- if (searchParams.length > 1) {
31
- throw new Error('Cannot provide multiple search parameters. Choose one of: taskId, projectId, or commentId.');
32
- }
33
- let comments;
34
- let hasMore = false;
35
- let nextCursor = null;
36
- if (args.commentId) {
37
- // Get single comment
38
- const comment = await client.getComment(args.commentId);
39
- comments = [comment];
40
- }
41
- else if (args.taskId) {
42
- // Get comments by task
43
- const response = await client.getComments({
44
- taskId: args.taskId,
45
- cursor: args.cursor || null,
46
- limit: args.limit || ApiLimits.COMMENTS_DEFAULT,
47
- });
48
- comments = response.results;
49
- hasMore = response.nextCursor !== null;
50
- nextCursor = response.nextCursor;
51
- }
52
- else if (args.projectId) {
53
- // Get comments by project
54
- const response = await client.getComments({
55
- projectId: args.projectId,
56
- cursor: args.cursor || null,
57
- limit: args.limit || ApiLimits.COMMENTS_DEFAULT,
58
- });
59
- comments = response.results;
60
- hasMore = response.nextCursor !== null;
61
- nextCursor = response.nextCursor;
62
- }
63
- else {
64
- // This should never happen due to validation, but TypeScript needs it
65
- throw new Error('Invalid state: no search parameter provided');
66
- }
67
- const textContent = generateTextContent({
68
- comments,
69
- searchType: args.commentId ? 'single' : args.taskId ? 'task' : 'project',
70
- searchId: args.commentId || args.taskId || args.projectId || '',
71
- hasMore,
72
- nextCursor,
73
- });
74
- return getToolOutput({
75
- textContent,
76
- structuredContent: {
77
- comments,
78
- searchType: args.commentId ? 'single' : args.taskId ? 'task' : 'project',
79
- searchId: args.commentId || args.taskId || args.projectId || '',
80
- hasMore,
81
- nextCursor,
82
- totalCount: comments.length,
83
- },
84
- });
85
- },
86
- };
87
- function generateTextContent({ comments, searchType, searchId, hasMore, nextCursor, }) {
88
- if (comments.length === 0) {
89
- return `No comments found for ${searchType}${searchType !== 'single' ? ` ${searchId}` : ''}`;
90
- }
91
- // Build summary
92
- let summary;
93
- if (searchType === 'single') {
94
- const comment = comments[0];
95
- if (!comment) {
96
- return 'Comment not found';
97
- }
98
- const hasAttachment = comment.fileAttachment !== null;
99
- const attachmentInfo = hasAttachment
100
- ? ` • Has attachment: ${comment.fileAttachment?.fileName || 'file'}`
101
- : '';
102
- summary = `Found comment${attachmentInfo} • id=${comment.id}`;
103
- }
104
- else {
105
- const attachmentCount = comments.filter((c) => c.fileAttachment !== null).length;
106
- const attachmentInfo = attachmentCount > 0 ? ` (${attachmentCount} with attachments)` : '';
107
- const commentsLabel = comments.length === 1 ? 'comment' : 'comments';
108
- summary = `Found ${comments.length} ${commentsLabel} for ${searchType} ${searchId}${attachmentInfo}`;
109
- if (hasMore) {
110
- summary += ' • More available';
111
- }
112
- }
113
- // Context-aware next steps
114
- const nextSteps = [];
115
- if (searchType === 'single') {
116
- const comment = comments[0];
117
- if (comment) {
118
- nextSteps.push(`Use ${UPDATE_COMMENTS} with id=${comment.id} to edit content`);
119
- nextSteps.push(`Use ${DELETE_OBJECT} with type=comment id=${comment.id} to remove`);
120
- // Suggest viewing related comments
121
- if (comment.taskId) {
122
- nextSteps.push(`Use ${ToolNames.FIND_COMMENTS} with taskId=${comment.taskId} to see all task comments`);
123
- }
124
- else if (comment.projectId) {
125
- nextSteps.push(`Use ${ToolNames.FIND_COMMENTS} with projectId=${comment.projectId} to see all project comments`);
126
- }
127
- }
128
- }
129
- else {
130
- // Multiple comments
131
- nextSteps.push(`Use ${ADD_COMMENTS} with ${searchType}Id=${searchId} to add new comment`);
132
- if (comments.length > 0) {
133
- nextSteps.push(`Use ${ToolNames.FIND_COMMENTS} with commentId to view specific comment details`);
134
- }
135
- // Pagination
136
- if (hasMore && nextCursor) {
137
- nextSteps.push(`Use ${ToolNames.FIND_COMMENTS} with cursor="${nextCursor}" to get more results`);
138
- }
139
- }
140
- const next = formatNextSteps(nextSteps);
141
- return `${summary}\n${next}`;
142
- }
143
- export { findComments };
@@ -1,161 +0,0 @@
1
- import { z } from 'zod';
2
- import { appendToQuery, resolveResponsibleUser } from '../filter-helpers.js';
3
- import { getToolOutput } from '../mcp-helpers.js';
4
- import { mapTask } from '../tool-helpers.js';
5
- import { ApiLimits } from '../utils/constants.js';
6
- import { generateLabelsFilter, LabelsSchema } from '../utils/labels.js';
7
- import { previewTasks, summarizeList } from '../utils/response-builders.js';
8
- import { ToolNames } from '../utils/tool-names.js';
9
- const { FIND_TASKS_BY_DATE, GET_OVERVIEW } = ToolNames;
10
- const ArgsSchema = {
11
- getBy: z
12
- .enum(['completion', 'due'])
13
- .default('completion')
14
- .describe('The method to use to get the tasks: "completion" to get tasks by completion date (ie, when the task was actually completed), "due" to get tasks by due date (ie, when the task was due to be completed by).'),
15
- since: z
16
- .string()
17
- .date()
18
- .regex(/^\d{4}-\d{2}-\d{2}$/)
19
- .describe('The start date to get the tasks for. Format: YYYY-MM-DD.'),
20
- until: z
21
- .string()
22
- .date()
23
- .regex(/^\d{4}-\d{2}-\d{2}$/)
24
- .describe('The start date to get the tasks for. Format: YYYY-MM-DD.'),
25
- workspaceId: z.string().optional().describe('The ID of the workspace to get the tasks for.'),
26
- projectId: z.string().optional().describe('The ID of the project to get the tasks for.'),
27
- sectionId: z.string().optional().describe('The ID of the section to get the tasks for.'),
28
- parentId: z.string().optional().describe('The ID of the parent task to get the tasks for.'),
29
- responsibleUser: z
30
- .string()
31
- .optional()
32
- .describe('Find tasks assigned to this user. Can be a user ID, name, or email address. Defaults to all collaborators when omitted.'),
33
- limit: z
34
- .number()
35
- .int()
36
- .min(1)
37
- .max(ApiLimits.COMPLETED_TASKS_MAX)
38
- .default(ApiLimits.COMPLETED_TASKS_DEFAULT)
39
- .describe('The maximum number of tasks to return.'),
40
- cursor: z
41
- .string()
42
- .optional()
43
- .describe('The cursor to get the next page of tasks (cursor is obtained from the previous call to this tool, with the same parameters).'),
44
- ...LabelsSchema,
45
- };
46
- const findCompletedTasks = {
47
- name: ToolNames.FIND_COMPLETED_TASKS,
48
- description: 'Get completed tasks (includes all collaborators by default—use responsibleUser to narrow).',
49
- parameters: ArgsSchema,
50
- async execute(args, client) {
51
- const { getBy, labels, labelsOperator, since, until, responsibleUser, ...rest } = args;
52
- // Resolve assignee name to user ID if provided
53
- const resolved = await resolveResponsibleUser(client, responsibleUser);
54
- const assigneeEmail = resolved?.email;
55
- // Build combined filter query (labels + assignment)
56
- const labelsFilter = generateLabelsFilter(labels, labelsOperator);
57
- let filterQuery = labelsFilter;
58
- if (resolved && assigneeEmail) {
59
- filterQuery = appendToQuery(filterQuery, `assigned to: ${assigneeEmail}`);
60
- }
61
- // Get user timezone to convert local dates to UTC
62
- const user = await client.getUser();
63
- const userGmtOffset = user.tzInfo?.gmtString || '+00:00';
64
- // Convert user's local date to UTC timestamps
65
- // This ensures we capture the entire day from the user's perspective
66
- const sinceWithOffset = `${since}T00:00:00${userGmtOffset}`;
67
- const untilWithOffset = `${until}T23:59:59${userGmtOffset}`;
68
- // Parse and convert to UTC
69
- const sinceDateTime = new Date(sinceWithOffset).toISOString();
70
- const untilDateTime = new Date(untilWithOffset).toISOString();
71
- const { items, nextCursor } = getBy === 'completion'
72
- ? await client.getCompletedTasksByCompletionDate({
73
- ...rest,
74
- since: sinceDateTime,
75
- until: untilDateTime,
76
- ...(filterQuery ? { filterQuery, filterLang: 'en' } : {}),
77
- })
78
- : await client.getCompletedTasksByDueDate({
79
- ...rest,
80
- since: sinceDateTime,
81
- until: untilDateTime,
82
- ...(filterQuery ? { filterQuery, filterLang: 'en' } : {}),
83
- });
84
- const mappedTasks = items.map(mapTask);
85
- const textContent = generateTextContent({
86
- tasks: mappedTasks,
87
- args,
88
- nextCursor,
89
- assigneeEmail,
90
- });
91
- return getToolOutput({
92
- textContent,
93
- structuredContent: {
94
- tasks: mappedTasks,
95
- nextCursor,
96
- totalCount: mappedTasks.length,
97
- hasMore: Boolean(nextCursor),
98
- appliedFilters: args,
99
- },
100
- });
101
- },
102
- };
103
- function generateTextContent({ tasks, args, nextCursor, assigneeEmail, }) {
104
- // Generate subject description
105
- const getByText = args.getBy === 'completion' ? 'completed' : 'due';
106
- const subject = `Completed tasks (by ${getByText} date)`;
107
- // Generate filter hints
108
- const filterHints = [];
109
- filterHints.push(`${getByText} date: ${args.since} to ${args.until}`);
110
- if (args.projectId)
111
- filterHints.push(`project: ${args.projectId}`);
112
- if (args.sectionId)
113
- filterHints.push(`section: ${args.sectionId}`);
114
- if (args.parentId)
115
- filterHints.push(`parent: ${args.parentId}`);
116
- if (args.workspaceId)
117
- filterHints.push(`workspace: ${args.workspaceId}`);
118
- // Add label filter information
119
- if (args.labels && args.labels.length > 0) {
120
- const labelText = args.labels
121
- .map((label) => `@${label}`)
122
- .join(args.labelsOperator === 'and' ? ' & ' : ' | ');
123
- filterHints.push(`labels: ${labelText}`);
124
- }
125
- // Add responsible user filter information
126
- if (args.responsibleUser) {
127
- const email = assigneeEmail || args.responsibleUser;
128
- filterHints.push(`assigned to: ${email}`);
129
- }
130
- // Generate helpful suggestions for empty results
131
- const zeroReasonHints = [];
132
- if (tasks.length === 0) {
133
- zeroReasonHints.push('No tasks completed in this date range');
134
- zeroReasonHints.push('Try expanding the date range');
135
- if (args.projectId || args.sectionId || args.parentId) {
136
- zeroReasonHints.push('Try removing project/section/parent filters');
137
- }
138
- if (args.getBy === 'due') {
139
- zeroReasonHints.push('Try switching to "completion" date instead');
140
- }
141
- }
142
- // Generate contextual next steps
143
- const nextSteps = [];
144
- if (tasks.length > 0) {
145
- nextSteps.push(`Use ${FIND_TASKS_BY_DATE} for active tasks or ${GET_OVERVIEW} for current productivity.`);
146
- if (tasks.some((task) => task.recurring)) {
147
- nextSteps.push('Recurring tasks will automatically create new instances.');
148
- }
149
- }
150
- return summarizeList({
151
- subject,
152
- count: tasks.length,
153
- limit: args.limit,
154
- nextCursor: nextCursor ?? undefined,
155
- filterHints,
156
- previewLines: previewTasks(tasks, Math.min(tasks.length, args.limit)),
157
- zeroReasonHints,
158
- nextSteps,
159
- });
160
- }
161
- export { findCompletedTasks };