@doist/todoist-ai 4.10.0 → 4.13.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.
- package/dist/filter-helpers.d.ts +51 -0
- package/dist/filter-helpers.d.ts.map +1 -0
- package/dist/filter-helpers.js +79 -0
- package/dist/index.d.ts +47 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/tool-helpers.d.ts +8 -20
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +6 -23
- package/dist/tools/__tests__/find-completed-tasks.test.js +71 -12
- package/dist/tools/__tests__/find-tasks-by-date.test.js +118 -20
- package/dist/tools/__tests__/find-tasks.test.js +2 -0
- package/dist/tools/add-projects.d.ts +3 -3
- package/dist/tools/add-tasks.d.ts +6 -3
- package/dist/tools/add-tasks.d.ts.map +1 -1
- package/dist/tools/delete-object.d.ts +1 -1
- package/dist/tools/find-completed-tasks.d.ts +8 -2
- package/dist/tools/find-completed-tasks.d.ts.map +1 -1
- package/dist/tools/find-completed-tasks.js +37 -4
- package/dist/tools/find-tasks-by-date.d.ts +9 -0
- package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
- package/dist/tools/find-tasks-by-date.js +39 -15
- package/dist/tools/find-tasks.d.ts +5 -2
- package/dist/tools/find-tasks.d.ts.map +1 -1
- package/dist/tools/find-tasks.js +20 -36
- package/dist/tools/update-comments.d.ts +3 -3
- package/dist/tools/update-sections.d.ts +3 -3
- package/dist/tools/update-tasks.d.ts +9 -6
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/utils/test-helpers.d.ts +3 -0
- package/dist/utils/test-helpers.d.ts.map +1 -1
- package/dist/utils/test-helpers.js +3 -0
- package/dist/utils/user-resolver.d.ts +2 -4
- package/dist/utils/user-resolver.d.ts.map +1 -1
- package/dist/utils/user-resolver.js +5 -5
- package/package.json +4 -4
|
@@ -6,11 +6,25 @@ import { findCompletedTasks } from '../find-completed-tasks.js';
|
|
|
6
6
|
const mockTodoistApi = {
|
|
7
7
|
getCompletedTasksByCompletionDate: jest.fn(),
|
|
8
8
|
getCompletedTasksByDueDate: jest.fn(),
|
|
9
|
+
getUser: jest.fn(),
|
|
9
10
|
};
|
|
10
11
|
const { FIND_COMPLETED_TASKS } = ToolNames;
|
|
11
12
|
describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
12
13
|
beforeEach(() => {
|
|
13
14
|
jest.clearAllMocks();
|
|
15
|
+
// Mock default user with UTC timezone
|
|
16
|
+
mockTodoistApi.getUser.mockResolvedValue({
|
|
17
|
+
id: 'test-user-id',
|
|
18
|
+
fullName: 'Test User',
|
|
19
|
+
email: 'test@example.com',
|
|
20
|
+
tzInfo: {
|
|
21
|
+
timezone: 'UTC',
|
|
22
|
+
gmtString: '+00:00',
|
|
23
|
+
hours: 0,
|
|
24
|
+
minutes: 0,
|
|
25
|
+
isDst: 0,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
14
28
|
});
|
|
15
29
|
describe('getting completed tasks by completion date (default)', () => {
|
|
16
30
|
it('should get completed tasks by completion date', async () => {
|
|
@@ -46,8 +60,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
46
60
|
labelsOperator: 'or',
|
|
47
61
|
}, mockTodoistApi);
|
|
48
62
|
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
49
|
-
since: '2025-08-
|
|
50
|
-
until: '2025-08-
|
|
63
|
+
since: '2025-08-10T00:00:00.000Z',
|
|
64
|
+
until: '2025-08-15T23:59:59.000Z',
|
|
51
65
|
limit: 50,
|
|
52
66
|
});
|
|
53
67
|
expect(extractTextContent(result)).toMatchSnapshot();
|
|
@@ -68,8 +82,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
68
82
|
labelsOperator: 'or',
|
|
69
83
|
}, mockTodoistApi);
|
|
70
84
|
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
71
|
-
since: '2025-08-
|
|
72
|
-
until: '2025-08-
|
|
85
|
+
since: '2025-08-01T00:00:00.000Z',
|
|
86
|
+
until: '2025-08-31T23:59:59.000Z',
|
|
73
87
|
projectId: 'specific-project-id',
|
|
74
88
|
limit: 100,
|
|
75
89
|
cursor: 'current-cursor',
|
|
@@ -111,8 +125,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
111
125
|
labelsOperator: 'or',
|
|
112
126
|
}, mockTodoistApi);
|
|
113
127
|
expect(mockTodoistApi.getCompletedTasksByDueDate).toHaveBeenCalledWith({
|
|
114
|
-
since: '2025-08-
|
|
115
|
-
until: '2025-08-
|
|
128
|
+
since: '2025-08-10T00:00:00.000Z',
|
|
129
|
+
until: '2025-08-20T23:59:59.000Z',
|
|
116
130
|
limit: 50,
|
|
117
131
|
});
|
|
118
132
|
expect(mockTodoistApi.getCompletedTasksByCompletionDate).not.toHaveBeenCalled();
|
|
@@ -172,8 +186,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
172
186
|
mockMethod.mockResolvedValue(mockResponse);
|
|
173
187
|
const result = await findCompletedTasks.execute(params, mockTodoistApi);
|
|
174
188
|
expect(mockMethod).toHaveBeenCalledWith({
|
|
175
|
-
since: params.since
|
|
176
|
-
until: params.until
|
|
189
|
+
since: `${params.since}T00:00:00.000Z`,
|
|
190
|
+
until: `${params.until}T23:59:59.000Z`,
|
|
177
191
|
limit: params.limit,
|
|
178
192
|
filterQuery: expectedFilter,
|
|
179
193
|
filterLang: 'en',
|
|
@@ -194,8 +208,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
194
208
|
mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue(mockResponse);
|
|
195
209
|
await findCompletedTasks.execute(params, mockTodoistApi);
|
|
196
210
|
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
197
|
-
since: params.since
|
|
198
|
-
until: params.until
|
|
211
|
+
since: `${params.since}T00:00:00.000Z`,
|
|
212
|
+
until: `${params.until}T23:59:59.000Z`,
|
|
199
213
|
limit: params.limit,
|
|
200
214
|
});
|
|
201
215
|
});
|
|
@@ -221,8 +235,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
221
235
|
mockTodoistApi.getCompletedTasksByDueDate.mockResolvedValue(mockResponse);
|
|
222
236
|
const result = await findCompletedTasks.execute(params, mockTodoistApi);
|
|
223
237
|
expect(mockTodoistApi.getCompletedTasksByDueDate).toHaveBeenCalledWith({
|
|
224
|
-
since: params.since
|
|
225
|
-
until: params.until
|
|
238
|
+
since: `${params.since}T00:00:00.000Z`,
|
|
239
|
+
until: `${params.until}T23:59:59.000Z`,
|
|
226
240
|
limit: params.limit,
|
|
227
241
|
projectId: params.projectId,
|
|
228
242
|
sectionId: params.sectionId,
|
|
@@ -233,6 +247,51 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
|
|
|
233
247
|
expect(textContent).toMatchSnapshot();
|
|
234
248
|
});
|
|
235
249
|
});
|
|
250
|
+
describe('timezone handling', () => {
|
|
251
|
+
it('should convert user timezone to UTC correctly (Europe/Madrid)', async () => {
|
|
252
|
+
// Mock user with Madrid timezone
|
|
253
|
+
mockTodoistApi.getUser.mockResolvedValue({
|
|
254
|
+
id: 'test-user-id',
|
|
255
|
+
fullName: 'Test User',
|
|
256
|
+
email: 'test@example.com',
|
|
257
|
+
tzInfo: {
|
|
258
|
+
timezone: 'Europe/Madrid',
|
|
259
|
+
gmtString: '+02:00',
|
|
260
|
+
hours: 2,
|
|
261
|
+
minutes: 0,
|
|
262
|
+
isDst: 0,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const mockCompletedTasks = [
|
|
266
|
+
createMockTask({
|
|
267
|
+
id: '8485093750',
|
|
268
|
+
content: 'Task completed in Madrid timezone',
|
|
269
|
+
completedAt: '2025-10-11T15:30:00Z',
|
|
270
|
+
}),
|
|
271
|
+
];
|
|
272
|
+
mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue({
|
|
273
|
+
items: mockCompletedTasks,
|
|
274
|
+
nextCursor: null,
|
|
275
|
+
});
|
|
276
|
+
const result = await findCompletedTasks.execute({
|
|
277
|
+
getBy: 'completion',
|
|
278
|
+
limit: 50,
|
|
279
|
+
since: '2025-10-11',
|
|
280
|
+
until: '2025-10-11',
|
|
281
|
+
labels: [],
|
|
282
|
+
labelsOperator: 'or',
|
|
283
|
+
}, mockTodoistApi);
|
|
284
|
+
// Should convert Madrid local time to UTC
|
|
285
|
+
// 2025-10-11 00:00:00 +02:00 = 2025-10-10 22:00:00 UTC
|
|
286
|
+
// 2025-10-11 23:59:59 +02:00 = 2025-10-11 21:59:59 UTC
|
|
287
|
+
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
|
|
288
|
+
since: '2025-10-10T22:00:00.000Z',
|
|
289
|
+
until: '2025-10-11T21:59:59.000Z',
|
|
290
|
+
limit: 50,
|
|
291
|
+
});
|
|
292
|
+
expect(extractTextContent(result)).toMatchSnapshot();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
236
295
|
describe('error handling', () => {
|
|
237
296
|
it('should propagate completion date API errors', async () => {
|
|
238
297
|
const apiError = new Error('API Error: Invalid date range');
|
|
@@ -2,16 +2,22 @@ import { jest } from '@jest/globals';
|
|
|
2
2
|
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
3
|
import { createMappedTask, createMockUser, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
|
|
4
4
|
import { ToolNames } from '../../utils/tool-names.js';
|
|
5
|
+
import { resolveUserNameToId } from '../../utils/user-resolver.js';
|
|
5
6
|
import { findTasksByDate } from '../find-tasks-by-date.js';
|
|
6
|
-
// Mock
|
|
7
|
+
// Mock only getTasksByFilter, use actual implementations for everything else
|
|
7
8
|
jest.mock('../../tool-helpers', () => {
|
|
8
9
|
const actual = jest.requireActual('../../tool-helpers');
|
|
9
10
|
return {
|
|
11
|
+
...actual,
|
|
10
12
|
getTasksByFilter: jest.fn(),
|
|
11
|
-
filterTasksByResponsibleUser: actual.filterTasksByResponsibleUser,
|
|
12
13
|
};
|
|
13
14
|
});
|
|
15
|
+
// Mock user resolver
|
|
16
|
+
jest.mock('../../utils/user-resolver', () => ({
|
|
17
|
+
resolveUserNameToId: jest.fn(),
|
|
18
|
+
}));
|
|
14
19
|
const mockGetTasksByFilter = getTasksByFilter;
|
|
20
|
+
const mockResolveUserNameToId = resolveUserNameToId;
|
|
15
21
|
// Mock the Todoist API (not directly used by find-tasks-by-date, but needed for type)
|
|
16
22
|
const mockTodoistApi = {
|
|
17
23
|
getUser: jest.fn(),
|
|
@@ -57,7 +63,7 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
57
63
|
// Verify the query uses daysCount=1 by checking the end date calculation
|
|
58
64
|
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
59
65
|
client: mockTodoistApi,
|
|
60
|
-
query: '(due after: 2025-08-20 | due: 2025-08-20) & due before: 2025-08-21',
|
|
66
|
+
query: '(due after: 2025-08-20 | due: 2025-08-20) & due before: 2025-08-21 & !assigned to: others',
|
|
61
67
|
cursor: undefined,
|
|
62
68
|
limit: 50,
|
|
63
69
|
});
|
|
@@ -71,7 +77,7 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
71
77
|
const result = await findTasksByDate.execute({ startDate: 'today', limit: 50, daysCount: 7 }, mockTodoistApi);
|
|
72
78
|
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
73
79
|
client: mockTodoistApi,
|
|
74
|
-
query: 'today | overdue',
|
|
80
|
+
query: '(today | overdue) & !assigned to: others',
|
|
75
81
|
cursor: undefined,
|
|
76
82
|
limit: 50,
|
|
77
83
|
});
|
|
@@ -236,7 +242,7 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
236
242
|
limit: 50,
|
|
237
243
|
labels: ['work'],
|
|
238
244
|
},
|
|
239
|
-
expectedQueryPattern: 'today | overdue & ((@work))', // Will be combined with date query
|
|
245
|
+
expectedQueryPattern: '(today | overdue) & ((@work)) & !assigned to: others', // Will be combined with date query
|
|
240
246
|
},
|
|
241
247
|
{
|
|
242
248
|
name: 'multiple labels with AND operator',
|
|
@@ -247,7 +253,7 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
247
253
|
labels: ['work', 'urgent'],
|
|
248
254
|
labelsOperator: 'and',
|
|
249
255
|
},
|
|
250
|
-
expectedQueryPattern: 'today | overdue & ((@work & @urgent))',
|
|
256
|
+
expectedQueryPattern: '(today | overdue) & ((@work & @urgent)) & !assigned to: others',
|
|
251
257
|
},
|
|
252
258
|
{
|
|
253
259
|
name: 'multiple labels with OR operator',
|
|
@@ -339,6 +345,7 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
339
345
|
});
|
|
340
346
|
describe('responsible user filtering', () => {
|
|
341
347
|
it('should filter results to show only unassigned tasks or tasks assigned to current user', async () => {
|
|
348
|
+
// Backend filtering: API should only return unassigned + assigned to me
|
|
342
349
|
const mockTasks = [
|
|
343
350
|
createMappedTask({
|
|
344
351
|
id: TEST_IDS.TASK_1,
|
|
@@ -352,16 +359,22 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
352
359
|
dueDate: '2025-08-15',
|
|
353
360
|
responsibleUid: null, // Unassigned
|
|
354
361
|
}),
|
|
355
|
-
createMappedTask({
|
|
356
|
-
id: TEST_IDS.TASK_3,
|
|
357
|
-
content: 'Someone else task',
|
|
358
|
-
dueDate: '2025-08-15',
|
|
359
|
-
responsibleUid: 'other-user-id', // Assigned to someone else
|
|
360
|
-
}),
|
|
361
362
|
];
|
|
362
363
|
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
363
364
|
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
364
|
-
const result = await findTasksByDate.execute({
|
|
365
|
+
const result = await findTasksByDate.execute({
|
|
366
|
+
startDate: 'today',
|
|
367
|
+
daysCount: 1,
|
|
368
|
+
limit: 50,
|
|
369
|
+
responsibleUserFiltering: 'unassignedOrMe',
|
|
370
|
+
}, mockTodoistApi);
|
|
371
|
+
// Verify the query includes the assignment filter
|
|
372
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
373
|
+
client: mockTodoistApi,
|
|
374
|
+
query: '(today | overdue) & !assigned to: others',
|
|
375
|
+
cursor: undefined,
|
|
376
|
+
limit: 50,
|
|
377
|
+
});
|
|
365
378
|
const structuredContent = extractStructuredContent(result);
|
|
366
379
|
// Should only return tasks 1 and 2, not task 3
|
|
367
380
|
expect(structuredContent.tasks).toHaveLength(2);
|
|
@@ -371,6 +384,7 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
371
384
|
]);
|
|
372
385
|
});
|
|
373
386
|
it('should filter overdue results to show only unassigned tasks or tasks assigned to current user', async () => {
|
|
387
|
+
// Backend filtering: API should only return unassigned + assigned to me
|
|
374
388
|
const mockTasks = [
|
|
375
389
|
createMappedTask({
|
|
376
390
|
id: TEST_IDS.TASK_1,
|
|
@@ -384,16 +398,22 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
384
398
|
dueDate: '2025-08-10',
|
|
385
399
|
responsibleUid: null, // Unassigned
|
|
386
400
|
}),
|
|
387
|
-
createMappedTask({
|
|
388
|
-
id: TEST_IDS.TASK_3,
|
|
389
|
-
content: 'Someone else overdue task',
|
|
390
|
-
dueDate: '2025-08-10',
|
|
391
|
-
responsibleUid: 'other-user-id', // Assigned to someone else
|
|
392
|
-
}),
|
|
393
401
|
];
|
|
394
402
|
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
395
403
|
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
396
|
-
const result = await findTasksByDate.execute({
|
|
404
|
+
const result = await findTasksByDate.execute({
|
|
405
|
+
overdueOption: 'overdue-only',
|
|
406
|
+
daysCount: 1,
|
|
407
|
+
limit: 50,
|
|
408
|
+
responsibleUserFiltering: 'unassignedOrMe',
|
|
409
|
+
}, mockTodoistApi);
|
|
410
|
+
// Verify the query includes the assignment filter
|
|
411
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
412
|
+
client: mockTodoistApi,
|
|
413
|
+
query: 'overdue & !assigned to: others',
|
|
414
|
+
cursor: undefined,
|
|
415
|
+
limit: 50,
|
|
416
|
+
});
|
|
397
417
|
const structuredContent = extractStructuredContent(result);
|
|
398
418
|
// Should only return tasks 1 and 2, not task 3
|
|
399
419
|
expect(structuredContent.tasks).toHaveLength(2);
|
|
@@ -403,6 +423,84 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
403
423
|
]);
|
|
404
424
|
});
|
|
405
425
|
});
|
|
426
|
+
describe('responsibleUser parameter', () => {
|
|
427
|
+
it('should filter tasks by specific user email', async () => {
|
|
428
|
+
mockResolveUserNameToId.mockResolvedValue({
|
|
429
|
+
userId: 'user-123',
|
|
430
|
+
displayName: 'John Doe',
|
|
431
|
+
email: 'john@example.com',
|
|
432
|
+
});
|
|
433
|
+
const mockTasks = [
|
|
434
|
+
createMappedTask({
|
|
435
|
+
id: TEST_IDS.TASK_1,
|
|
436
|
+
content: 'Task assigned to John',
|
|
437
|
+
dueDate: '2025-08-15',
|
|
438
|
+
responsibleUid: 'user-123',
|
|
439
|
+
}),
|
|
440
|
+
];
|
|
441
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
442
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
443
|
+
const result = await findTasksByDate.execute({
|
|
444
|
+
startDate: 'today',
|
|
445
|
+
daysCount: 1,
|
|
446
|
+
limit: 50,
|
|
447
|
+
responsibleUser: 'john@example.com',
|
|
448
|
+
}, mockTodoistApi);
|
|
449
|
+
expect(mockResolveUserNameToId).toHaveBeenCalledWith(mockTodoistApi, 'john@example.com');
|
|
450
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
451
|
+
client: mockTodoistApi,
|
|
452
|
+
query: '(today | overdue) & assigned to: john@example.com',
|
|
453
|
+
cursor: undefined,
|
|
454
|
+
limit: 50,
|
|
455
|
+
});
|
|
456
|
+
const textContent = extractTextContent(result);
|
|
457
|
+
expect(textContent).toContain('assigned to john@example.com');
|
|
458
|
+
expect(textContent).toMatchSnapshot();
|
|
459
|
+
});
|
|
460
|
+
it('should throw error when user cannot be resolved', async () => {
|
|
461
|
+
mockResolveUserNameToId.mockResolvedValue(null);
|
|
462
|
+
await expect(findTasksByDate.execute({
|
|
463
|
+
startDate: 'today',
|
|
464
|
+
daysCount: 1,
|
|
465
|
+
limit: 50,
|
|
466
|
+
responsibleUser: 'nonexistent@example.com',
|
|
467
|
+
}, mockTodoistApi)).rejects.toThrow('Could not find user: "nonexistent@example.com". Make sure the user is a collaborator on a shared project.');
|
|
468
|
+
});
|
|
469
|
+
it('should combine responsibleUser with labels and date filters', async () => {
|
|
470
|
+
mockResolveUserNameToId.mockResolvedValue({
|
|
471
|
+
userId: 'user-789',
|
|
472
|
+
displayName: 'Bob Wilson',
|
|
473
|
+
email: 'bob@example.com',
|
|
474
|
+
});
|
|
475
|
+
const mockTasks = [
|
|
476
|
+
createMappedTask({
|
|
477
|
+
id: TEST_IDS.TASK_1,
|
|
478
|
+
content: 'Important task for Bob',
|
|
479
|
+
dueDate: '2025-08-20',
|
|
480
|
+
responsibleUid: 'user-789',
|
|
481
|
+
labels: ['urgent'],
|
|
482
|
+
}),
|
|
483
|
+
];
|
|
484
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
485
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
486
|
+
await findTasksByDate.execute({
|
|
487
|
+
startDate: '2025-08-20',
|
|
488
|
+
daysCount: 1,
|
|
489
|
+
limit: 50,
|
|
490
|
+
responsibleUser: 'bob@example.com',
|
|
491
|
+
labels: ['urgent'],
|
|
492
|
+
}, mockTodoistApi);
|
|
493
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
494
|
+
client: mockTodoistApi,
|
|
495
|
+
query: expect.stringContaining('2025-08-20'),
|
|
496
|
+
cursor: undefined,
|
|
497
|
+
limit: 50,
|
|
498
|
+
});
|
|
499
|
+
const call = mockGetTasksByFilter.mock.calls[0]?.[0];
|
|
500
|
+
expect(call?.query).toContain('(@urgent)');
|
|
501
|
+
expect(call?.query).toContain('assigned to: bob@example.com');
|
|
502
|
+
});
|
|
503
|
+
});
|
|
406
504
|
describe('error handling', () => {
|
|
407
505
|
it.each([
|
|
408
506
|
{
|
|
@@ -697,6 +697,7 @@ End of test content.`;
|
|
|
697
697
|
mockResolveUserNameToId.mockResolvedValue({
|
|
698
698
|
userId: 'specific-user-id',
|
|
699
699
|
displayName: 'John Doe',
|
|
700
|
+
email: 'john@example.com',
|
|
700
701
|
});
|
|
701
702
|
const result = await findTasks.execute({ searchText: 'task', responsibleUser: 'John Doe', limit: 10 }, mockTodoistApi);
|
|
702
703
|
const structuredContent = extractStructuredContent(result);
|
|
@@ -726,6 +727,7 @@ End of test content.`;
|
|
|
726
727
|
mockResolveUserNameToId.mockResolvedValue({
|
|
727
728
|
userId: 'specific-user-id',
|
|
728
729
|
displayName: 'John Doe',
|
|
730
|
+
email: 'john@example.com',
|
|
729
731
|
});
|
|
730
732
|
const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_WORK, responsibleUser: 'John Doe', limit: 10 }, mockTodoistApi);
|
|
731
733
|
const structuredContent = extractStructuredContent(result);
|
|
@@ -10,22 +10,22 @@ declare const addProjects: {
|
|
|
10
10
|
viewStyle: z.ZodOptional<z.ZodEnum<["list", "board", "calendar"]>>;
|
|
11
11
|
}, "strip", z.ZodTypeAny, {
|
|
12
12
|
name: string;
|
|
13
|
-
parentId?: string | undefined;
|
|
14
13
|
isFavorite?: boolean | undefined;
|
|
15
14
|
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
15
|
+
parentId?: string | undefined;
|
|
16
16
|
}, {
|
|
17
17
|
name: string;
|
|
18
|
-
parentId?: string | undefined;
|
|
19
18
|
isFavorite?: boolean | undefined;
|
|
20
19
|
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
20
|
+
parentId?: string | undefined;
|
|
21
21
|
}>, "many">;
|
|
22
22
|
};
|
|
23
23
|
execute({ projects }: {
|
|
24
24
|
projects: {
|
|
25
25
|
name: string;
|
|
26
|
-
parentId?: string | undefined;
|
|
27
26
|
isFavorite?: boolean | undefined;
|
|
28
27
|
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
28
|
+
parentId?: string | undefined;
|
|
29
29
|
}[];
|
|
30
30
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
31
31
|
content: {
|
|
@@ -18,8 +18,8 @@ declare const addTasks: {
|
|
|
18
18
|
}, "strip", z.ZodTypeAny, {
|
|
19
19
|
content: string;
|
|
20
20
|
description?: string | undefined;
|
|
21
|
-
projectId?: string | undefined;
|
|
22
21
|
parentId?: string | undefined;
|
|
22
|
+
projectId?: string | undefined;
|
|
23
23
|
sectionId?: string | undefined;
|
|
24
24
|
labels?: string[] | undefined;
|
|
25
25
|
duration?: string | undefined;
|
|
@@ -29,8 +29,8 @@ declare const addTasks: {
|
|
|
29
29
|
}, {
|
|
30
30
|
content: string;
|
|
31
31
|
description?: string | undefined;
|
|
32
|
-
projectId?: string | undefined;
|
|
33
32
|
parentId?: string | undefined;
|
|
33
|
+
projectId?: string | undefined;
|
|
34
34
|
sectionId?: string | undefined;
|
|
35
35
|
labels?: string[] | undefined;
|
|
36
36
|
duration?: string | undefined;
|
|
@@ -43,8 +43,8 @@ declare const addTasks: {
|
|
|
43
43
|
tasks: {
|
|
44
44
|
content: string;
|
|
45
45
|
description?: string | undefined;
|
|
46
|
-
projectId?: string | undefined;
|
|
47
46
|
parentId?: string | undefined;
|
|
47
|
+
projectId?: string | undefined;
|
|
48
48
|
sectionId?: string | undefined;
|
|
49
49
|
labels?: string[] | undefined;
|
|
50
50
|
duration?: string | undefined;
|
|
@@ -72,6 +72,9 @@ declare const addTasks: {
|
|
|
72
72
|
duration: string | null;
|
|
73
73
|
responsibleUid: string | null;
|
|
74
74
|
assignedByUid: string | null;
|
|
75
|
+
checked: boolean;
|
|
76
|
+
completedAt: string | null;
|
|
77
|
+
updatedAt: string | null;
|
|
75
78
|
}[];
|
|
76
79
|
totalCount: number;
|
|
77
80
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/add-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAClF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAqDvB,QAAA,MAAM,QAAQ
|
|
1
|
+
{"version":3,"file":"add-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/add-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAClF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAqDvB,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AAgI1C,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -7,8 +7,8 @@ declare const deleteObject: {
|
|
|
7
7
|
id: z.ZodString;
|
|
8
8
|
};
|
|
9
9
|
execute(args: {
|
|
10
|
-
type: "task" | "comment" | "project" | "section";
|
|
11
10
|
id: string;
|
|
11
|
+
type: "task" | "comment" | "project" | "section";
|
|
12
12
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
13
13
|
content: {
|
|
14
14
|
type: "text";
|
|
@@ -12,6 +12,7 @@ declare const findCompletedTasks: {
|
|
|
12
12
|
projectId: z.ZodOptional<z.ZodString>;
|
|
13
13
|
sectionId: z.ZodOptional<z.ZodString>;
|
|
14
14
|
parentId: z.ZodOptional<z.ZodString>;
|
|
15
|
+
responsibleUser: z.ZodOptional<z.ZodString>;
|
|
15
16
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
16
17
|
cursor: z.ZodOptional<z.ZodString>;
|
|
17
18
|
};
|
|
@@ -20,12 +21,13 @@ declare const findCompletedTasks: {
|
|
|
20
21
|
getBy: "due" | "completion";
|
|
21
22
|
since: string;
|
|
22
23
|
until: string;
|
|
23
|
-
projectId?: string | undefined;
|
|
24
24
|
parentId?: string | undefined;
|
|
25
25
|
workspaceId?: string | undefined;
|
|
26
|
+
projectId?: string | undefined;
|
|
26
27
|
sectionId?: string | undefined;
|
|
27
28
|
labels?: string[] | undefined;
|
|
28
29
|
cursor?: string | undefined;
|
|
30
|
+
responsibleUser?: string | undefined;
|
|
29
31
|
labelsOperator?: "and" | "or" | undefined;
|
|
30
32
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
31
33
|
content: {
|
|
@@ -47,6 +49,9 @@ declare const findCompletedTasks: {
|
|
|
47
49
|
duration: string | null;
|
|
48
50
|
responsibleUid: string | null;
|
|
49
51
|
assignedByUid: string | null;
|
|
52
|
+
checked: boolean;
|
|
53
|
+
completedAt: string | null;
|
|
54
|
+
updatedAt: string | null;
|
|
50
55
|
}[];
|
|
51
56
|
nextCursor: string | null;
|
|
52
57
|
totalCount: number;
|
|
@@ -56,12 +61,13 @@ declare const findCompletedTasks: {
|
|
|
56
61
|
getBy: "due" | "completion";
|
|
57
62
|
since: string;
|
|
58
63
|
until: string;
|
|
59
|
-
projectId?: string | undefined;
|
|
60
64
|
parentId?: string | undefined;
|
|
61
65
|
workspaceId?: string | undefined;
|
|
66
|
+
projectId?: string | undefined;
|
|
62
67
|
sectionId?: string | undefined;
|
|
63
68
|
labels?: string[] | undefined;
|
|
64
69
|
cursor?: string | undefined;
|
|
70
|
+
responsibleUser?: string | undefined;
|
|
65
71
|
labelsOperator?: "and" | "or" | undefined;
|
|
66
72
|
};
|
|
67
73
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-completed-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/find-completed-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"find-completed-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/find-completed-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAsDvB,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkEkB,CAAA;AA2E1C,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { appendToQuery, resolveResponsibleUser } from '../filter-helpers.js';
|
|
2
3
|
import { getToolOutput } from '../mcp-helpers.js';
|
|
3
4
|
import { mapTask } from '../tool-helpers.js';
|
|
4
5
|
import { ApiLimits } from '../utils/constants.js';
|
|
@@ -25,6 +26,10 @@ const ArgsSchema = {
|
|
|
25
26
|
projectId: z.string().optional().describe('The ID of the project to get the tasks for.'),
|
|
26
27
|
sectionId: z.string().optional().describe('The ID of the section to get the tasks for.'),
|
|
27
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.'),
|
|
28
33
|
limit: z
|
|
29
34
|
.number()
|
|
30
35
|
.int()
|
|
@@ -43,22 +48,45 @@ const findCompletedTasks = {
|
|
|
43
48
|
description: 'Get completed tasks.',
|
|
44
49
|
parameters: ArgsSchema,
|
|
45
50
|
async execute(args, client) {
|
|
46
|
-
const { getBy, labels, labelsOperator, ...rest } = args;
|
|
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)
|
|
47
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();
|
|
48
71
|
const { items, nextCursor } = getBy === 'completion'
|
|
49
72
|
? await client.getCompletedTasksByCompletionDate({
|
|
50
73
|
...rest,
|
|
51
|
-
|
|
74
|
+
since: sinceDateTime,
|
|
75
|
+
until: untilDateTime,
|
|
76
|
+
...(filterQuery ? { filterQuery, filterLang: 'en' } : {}),
|
|
52
77
|
})
|
|
53
78
|
: await client.getCompletedTasksByDueDate({
|
|
54
79
|
...rest,
|
|
55
|
-
|
|
80
|
+
since: sinceDateTime,
|
|
81
|
+
until: untilDateTime,
|
|
82
|
+
...(filterQuery ? { filterQuery, filterLang: 'en' } : {}),
|
|
56
83
|
});
|
|
57
84
|
const mappedTasks = items.map(mapTask);
|
|
58
85
|
const textContent = generateTextContent({
|
|
59
86
|
tasks: mappedTasks,
|
|
60
87
|
args,
|
|
61
88
|
nextCursor,
|
|
89
|
+
assigneeEmail,
|
|
62
90
|
});
|
|
63
91
|
return getToolOutput({
|
|
64
92
|
textContent,
|
|
@@ -72,7 +100,7 @@ const findCompletedTasks = {
|
|
|
72
100
|
});
|
|
73
101
|
},
|
|
74
102
|
};
|
|
75
|
-
function generateTextContent({ tasks, args, nextCursor, }) {
|
|
103
|
+
function generateTextContent({ tasks, args, nextCursor, assigneeEmail, }) {
|
|
76
104
|
// Generate subject description
|
|
77
105
|
const getByText = args.getBy === 'completion' ? 'completed' : 'due';
|
|
78
106
|
const subject = `Completed tasks (by ${getByText} date)`;
|
|
@@ -94,6 +122,11 @@ function generateTextContent({ tasks, args, nextCursor, }) {
|
|
|
94
122
|
.join(args.labelsOperator === 'and' ? ' & ' : ' | ');
|
|
95
123
|
filterHints.push(`labels: ${labelText}`);
|
|
96
124
|
}
|
|
125
|
+
// Add responsible user filter information
|
|
126
|
+
if (args.responsibleUser) {
|
|
127
|
+
const email = assigneeEmail || args.responsibleUser;
|
|
128
|
+
filterHints.push(`assigned to: ${email}`);
|
|
129
|
+
}
|
|
97
130
|
// Generate helpful suggestions for empty results
|
|
98
131
|
const zeroReasonHints = [];
|
|
99
132
|
if (tasks.length === 0) {
|
|
@@ -10,12 +10,16 @@ declare const findTasksByDate: {
|
|
|
10
10
|
daysCount: z.ZodDefault<z.ZodNumber>;
|
|
11
11
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
12
12
|
cursor: z.ZodOptional<z.ZodString>;
|
|
13
|
+
responsibleUser: z.ZodOptional<z.ZodString>;
|
|
14
|
+
responsibleUserFiltering: z.ZodOptional<z.ZodEnum<["assigned", "unassignedOrMe", "all"]>>;
|
|
13
15
|
};
|
|
14
16
|
execute(args: {
|
|
15
17
|
limit: number;
|
|
16
18
|
daysCount: number;
|
|
19
|
+
responsibleUserFiltering?: "assigned" | "unassignedOrMe" | "all" | undefined;
|
|
17
20
|
labels?: string[] | undefined;
|
|
18
21
|
cursor?: string | undefined;
|
|
22
|
+
responsibleUser?: string | undefined;
|
|
19
23
|
labelsOperator?: "and" | "or" | undefined;
|
|
20
24
|
startDate?: string | undefined;
|
|
21
25
|
overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
|
|
@@ -39,6 +43,9 @@ declare const findTasksByDate: {
|
|
|
39
43
|
duration: string | null;
|
|
40
44
|
responsibleUid: string | null;
|
|
41
45
|
assignedByUid: string | null;
|
|
46
|
+
checked: boolean;
|
|
47
|
+
completedAt: string | null;
|
|
48
|
+
updatedAt: string | null;
|
|
42
49
|
}[];
|
|
43
50
|
nextCursor: string | null;
|
|
44
51
|
totalCount: number;
|
|
@@ -46,8 +53,10 @@ declare const findTasksByDate: {
|
|
|
46
53
|
appliedFilters: {
|
|
47
54
|
limit: number;
|
|
48
55
|
daysCount: number;
|
|
56
|
+
responsibleUserFiltering?: "assigned" | "unassignedOrMe" | "all" | undefined;
|
|
49
57
|
labels?: string[] | undefined;
|
|
50
58
|
cursor?: string | undefined;
|
|
59
|
+
responsibleUser?: string | undefined;
|
|
51
60
|
labelsOperator?: "and" | "or" | undefined;
|
|
52
61
|
startDate?: string | undefined;
|
|
53
62
|
overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-tasks-by-date.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks-by-date.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"find-tasks-by-date.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks-by-date.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAmEvB,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2EqB,CAAA;AAsG1C,OAAO,EAAE,eAAe,EAAE,CAAA"}
|