@doist/todoist-ai 4.11.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.
Files changed (34) hide show
  1. package/dist/filter-helpers.d.ts +51 -0
  2. package/dist/filter-helpers.d.ts.map +1 -0
  3. package/dist/filter-helpers.js +79 -0
  4. package/dist/index.d.ts +47 -23
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/tool-helpers.d.ts +8 -20
  7. package/dist/tool-helpers.d.ts.map +1 -1
  8. package/dist/tool-helpers.js +6 -23
  9. package/dist/tools/__tests__/find-tasks-by-date.test.js +118 -20
  10. package/dist/tools/__tests__/find-tasks.test.js +2 -0
  11. package/dist/tools/add-projects.d.ts +3 -3
  12. package/dist/tools/add-tasks.d.ts +6 -3
  13. package/dist/tools/add-tasks.d.ts.map +1 -1
  14. package/dist/tools/delete-object.d.ts +1 -1
  15. package/dist/tools/find-completed-tasks.d.ts +16 -10
  16. package/dist/tools/find-completed-tasks.d.ts.map +1 -1
  17. package/dist/tools/find-completed-tasks.js +23 -4
  18. package/dist/tools/find-tasks-by-date.d.ts +9 -0
  19. package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
  20. package/dist/tools/find-tasks-by-date.js +39 -15
  21. package/dist/tools/find-tasks.d.ts +5 -2
  22. package/dist/tools/find-tasks.d.ts.map +1 -1
  23. package/dist/tools/find-tasks.js +20 -36
  24. package/dist/tools/update-comments.d.ts +3 -3
  25. package/dist/tools/update-sections.d.ts +3 -3
  26. package/dist/tools/update-tasks.d.ts +9 -6
  27. package/dist/tools/update-tasks.d.ts.map +1 -1
  28. package/dist/utils/test-helpers.d.ts +3 -0
  29. package/dist/utils/test-helpers.d.ts.map +1 -1
  30. package/dist/utils/test-helpers.js +3 -0
  31. package/dist/utils/user-resolver.d.ts +2 -4
  32. package/dist/utils/user-resolver.d.ts.map +1 -1
  33. package/dist/utils/user-resolver.js +5 -5
  34. package/package.json +4 -4
@@ -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 the tool helpers
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({ startDate: 'today', daysCount: 1, limit: 50 }, mockTodoistApi);
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({ overdueOption: 'overdue-only', daysCount: 1, limit: 50 }, mockTodoistApi);
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AAgI1C,OAAO,EAAE,QAAQ,EAAE,CAAA"}
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,21 +12,23 @@ 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
  };
18
19
  execute(args: {
19
- getBy: "completion" | "due";
20
+ limit: number;
21
+ getBy: "due" | "completion";
20
22
  since: string;
21
23
  until: string;
22
- limit: number;
23
- labels?: string[] | undefined;
24
- labelsOperator?: "and" | "or" | undefined;
24
+ parentId?: string | undefined;
25
25
  workspaceId?: string | undefined;
26
26
  projectId?: string | undefined;
27
27
  sectionId?: string | undefined;
28
- parentId?: string | undefined;
28
+ labels?: string[] | undefined;
29
29
  cursor?: string | undefined;
30
+ responsibleUser?: string | undefined;
31
+ labelsOperator?: "and" | "or" | undefined;
30
32
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
31
33
  content: {
32
34
  type: "text";
@@ -47,22 +49,26 @@ 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;
53
58
  hasMore: boolean;
54
59
  appliedFilters: {
55
- getBy: "completion" | "due";
60
+ limit: number;
61
+ getBy: "due" | "completion";
56
62
  since: string;
57
63
  until: string;
58
- limit: number;
59
- labels?: string[] | undefined;
60
- labelsOperator?: "and" | "or" | undefined;
64
+ parentId?: string | undefined;
61
65
  workspaceId?: string | undefined;
62
66
  projectId?: string | undefined;
63
67
  sectionId?: string | undefined;
64
- parentId?: string | undefined;
68
+ labels?: string[] | undefined;
65
69
  cursor?: string | undefined;
70
+ responsibleUser?: string | undefined;
71
+ labelsOperator?: "and" | "or" | undefined;
66
72
  };
67
73
  };
68
74
  } | {
@@ -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;AAiDvB,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDkB,CAAA;AAmE1C,OAAO,EAAE,kBAAkB,EAAE,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,8 +48,16 @@ const findCompletedTasks = {
43
48
  description: 'Get completed tasks.',
44
49
  parameters: ArgsSchema,
45
50
  async execute(args, client) {
46
- const { getBy, labels, labelsOperator, since, until, ...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
+ }
48
61
  // Get user timezone to convert local dates to UTC
49
62
  const user = await client.getUser();
50
63
  const userGmtOffset = user.tzInfo?.gmtString || '+00:00';
@@ -60,19 +73,20 @@ const findCompletedTasks = {
60
73
  ...rest,
61
74
  since: sinceDateTime,
62
75
  until: untilDateTime,
63
- ...(labelsFilter ? { filterQuery: labelsFilter, filterLang: 'en' } : {}),
76
+ ...(filterQuery ? { filterQuery, filterLang: 'en' } : {}),
64
77
  })
65
78
  : await client.getCompletedTasksByDueDate({
66
79
  ...rest,
67
80
  since: sinceDateTime,
68
81
  until: untilDateTime,
69
- ...(labelsFilter ? { filterQuery: labelsFilter, filterLang: 'en' } : {}),
82
+ ...(filterQuery ? { filterQuery, filterLang: 'en' } : {}),
70
83
  });
71
84
  const mappedTasks = items.map(mapTask);
72
85
  const textContent = generateTextContent({
73
86
  tasks: mappedTasks,
74
87
  args,
75
88
  nextCursor,
89
+ assigneeEmail,
76
90
  });
77
91
  return getToolOutput({
78
92
  textContent,
@@ -86,7 +100,7 @@ const findCompletedTasks = {
86
100
  });
87
101
  },
88
102
  };
89
- function generateTextContent({ tasks, args, nextCursor, }) {
103
+ function generateTextContent({ tasks, args, nextCursor, assigneeEmail, }) {
90
104
  // Generate subject description
91
105
  const getByText = args.getBy === 'completion' ? 'completed' : 'due';
92
106
  const subject = `Completed tasks (by ${getByText} date)`;
@@ -108,6 +122,11 @@ function generateTextContent({ tasks, args, nextCursor, }) {
108
122
  .join(args.labelsOperator === 'and' ? ' & ' : ' | ');
109
123
  filterHints.push(`labels: ${labelText}`);
110
124
  }
125
+ // Add responsible user filter information
126
+ if (args.responsibleUser) {
127
+ const email = assigneeEmail || args.responsibleUser;
128
+ filterHints.push(`assigned to: ${email}`);
129
+ }
111
130
  // Generate helpful suggestions for empty results
112
131
  const zeroReasonHints = [];
113
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;AAmDvB,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmEqB,CAAA;AAwF1C,OAAO,EAAE,eAAe,EAAE,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"}
@@ -1,7 +1,8 @@
1
1
  import { addDays, formatISO } from 'date-fns';
2
2
  import { z } from 'zod';
3
+ import { appendToQuery, buildResponsibleUserQueryFilter, RESPONSIBLE_USER_FILTERING, resolveResponsibleUser, } from '../filter-helpers.js';
3
4
  import { getToolOutput } from '../mcp-helpers.js';
4
- import { filterTasksByResponsibleUser, getTasksByFilter } from '../tool-helpers.js';
5
+ import { getTasksByFilter } from '../tool-helpers.js';
5
6
  import { ApiLimits } from '../utils/constants.js';
6
7
  import { generateLabelsFilter, LabelsSchema } from '../utils/labels.js';
7
8
  import { generateTaskNextSteps, getDateString, previewTasks, summarizeList, } from '../utils/response-builders.js';
@@ -34,6 +35,14 @@ const ArgsSchema = {
34
35
  .string()
35
36
  .optional()
36
37
  .describe('The cursor to get the next page of tasks (cursor is obtained from the previous call to this tool, with the same parameters).'),
38
+ responsibleUser: z
39
+ .string()
40
+ .optional()
41
+ .describe('Find tasks assigned to this user. Can be a user ID, name, or email address.'),
42
+ responsibleUserFiltering: z
43
+ .enum(RESPONSIBLE_USER_FILTERING)
44
+ .optional()
45
+ .describe('How to filter by responsible user when responsibleUser is not provided. "assigned" = only tasks assigned to others; "unassignedOrMe" = only unassigned tasks or tasks assigned to me; "all" = all tasks regardless of assignment. Default is "unassignedOrMe".'),
37
46
  ...LabelsSchema,
38
47
  };
39
48
  const findTasksByDate = {
@@ -44,14 +53,18 @@ const findTasksByDate = {
44
53
  if (!args.startDate && args.overdueOption !== 'overdue-only') {
45
54
  throw new Error('Either startDate must be provided or overdueOption must be set to overdue-only');
46
55
  }
56
+ // Resolve assignee name to user ID if provided
57
+ const resolved = await resolveResponsibleUser(client, args.responsibleUser);
58
+ const resolvedAssigneeId = resolved?.userId;
59
+ const assigneeEmail = resolved?.email;
47
60
  let query = '';
48
- const todoistUser = await client.getUser();
49
61
  if (args.overdueOption === 'overdue-only') {
50
62
  query = 'overdue';
51
63
  }
52
64
  else if (args.startDate === 'today') {
53
65
  // For 'today', include overdue unless explicitly excluded
54
- query = args.overdueOption === 'exclude-overdue' ? 'today' : 'today | overdue';
66
+ // Use parentheses to ensure correct operator precedence when combining with other filters
67
+ query = args.overdueOption === 'exclude-overdue' ? 'today' : '(today | overdue)';
55
68
  }
56
69
  else if (args.startDate) {
57
70
  // For specific dates, never include overdue tasks
@@ -60,30 +73,31 @@ const findTasksByDate = {
60
73
  const endDateStr = formatISO(endDate, { representation: 'date' });
61
74
  query = `(due after: ${startDate} | due: ${startDate}) & due before: ${endDateStr}`;
62
75
  }
76
+ // Add labels filter
63
77
  const labelsFilter = generateLabelsFilter(args.labels, args.labelsOperator);
64
78
  if (labelsFilter.length > 0) {
65
- // If there is already a query, we need to append the & operator first
66
- if (query.length > 0)
67
- query += ' & ';
68
- // Add the labels to the filter
69
- query += `(${labelsFilter})`;
79
+ query = appendToQuery(query, `(${labelsFilter})`);
70
80
  }
81
+ // Add responsible user filtering to the query (backend filtering)
82
+ const responsibleUserFilter = buildResponsibleUserQueryFilter({
83
+ resolvedAssigneeId,
84
+ assigneeEmail,
85
+ responsibleUserFiltering: args.responsibleUserFiltering,
86
+ });
87
+ query = appendToQuery(query, responsibleUserFilter);
71
88
  const result = await getTasksByFilter({
72
89
  client,
73
90
  query,
74
91
  cursor: args.cursor,
75
92
  limit: args.limit,
76
93
  });
77
- // Apply responsible user filtering - only show unassigned tasks or tasks assigned to current user
78
- const filteredTasks = filterTasksByResponsibleUser({
79
- tasks: result.tasks,
80
- resolvedAssigneeId: undefined,
81
- currentUserId: todoistUser.id,
82
- });
94
+ // No need for post-fetch filtering since it's handled in the query
95
+ const filteredTasks = result.tasks;
83
96
  const textContent = generateTextContent({
84
97
  tasks: filteredTasks,
85
98
  args,
86
99
  nextCursor: result.nextCursor,
100
+ assigneeEmail,
87
101
  });
88
102
  return getToolOutput({
89
103
  textContent,
@@ -97,7 +111,7 @@ const findTasksByDate = {
97
111
  });
98
112
  },
99
113
  };
100
- function generateTextContent({ tasks, args, nextCursor, }) {
114
+ function generateTextContent({ tasks, args, nextCursor, assigneeEmail, }) {
101
115
  // Generate filter description
102
116
  const filterHints = [];
103
117
  if (args.overdueOption === 'overdue-only') {
@@ -120,6 +134,11 @@ function generateTextContent({ tasks, args, nextCursor, }) {
120
134
  .join(args.labelsOperator === 'and' ? ' & ' : ' | ');
121
135
  filterHints.push(`labels: ${labelText}`);
122
136
  }
137
+ // Add responsible user filter information
138
+ if (args.responsibleUser) {
139
+ const email = assigneeEmail || args.responsibleUser;
140
+ filterHints.push(`assigned to: ${email}`);
141
+ }
123
142
  // Generate subject description
124
143
  let subject = '';
125
144
  if (args.overdueOption === 'overdue-only') {
@@ -135,6 +154,11 @@ function generateTextContent({ tasks, args, nextCursor, }) {
135
154
  else {
136
155
  subject = 'Tasks';
137
156
  }
157
+ // Append responsible user to subject if provided
158
+ if (args.responsibleUser) {
159
+ const email = assigneeEmail || args.responsibleUser;
160
+ subject += ` assigned to ${email}`;
161
+ }
138
162
  // Generate helpful suggestions for empty results
139
163
  const zeroReasonHints = [];
140
164
  if (tasks.length === 0) {
@@ -16,9 +16,9 @@ declare const findTasks: {
16
16
  };
17
17
  execute(args: {
18
18
  limit: number;
19
- projectId?: string | undefined;
20
19
  parentId?: string | undefined;
21
20
  responsibleUserFiltering?: "assigned" | "unassignedOrMe" | "all" | undefined;
21
+ projectId?: string | undefined;
22
22
  sectionId?: string | undefined;
23
23
  labels?: string[] | undefined;
24
24
  cursor?: string | undefined;
@@ -45,15 +45,18 @@ declare const findTasks: {
45
45
  duration: string | null;
46
46
  responsibleUid: string | null;
47
47
  assignedByUid: string | null;
48
+ checked: boolean;
49
+ completedAt: string | null;
50
+ updatedAt: string | null;
48
51
  }[];
49
52
  nextCursor: string | null;
50
53
  totalCount: number;
51
54
  hasMore: boolean;
52
55
  appliedFilters: {
53
56
  limit: number;
54
- projectId?: string | undefined;
55
57
  parentId?: string | undefined;
56
58
  responsibleUserFiltering?: "assigned" | "unassignedOrMe" | "all" | undefined;
59
+ projectId?: string | undefined;
57
60
  sectionId?: string | undefined;
58
61
  labels?: string[] | undefined;
59
62
  cursor?: string | undefined;