@doist/todoist-ai 4.9.2 → 4.9.4

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/index.d.ts CHANGED
@@ -328,18 +328,20 @@ declare const tools: {
328
328
  parameters: {
329
329
  labels: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString, "many">>;
330
330
  labelsOperator: import("zod").ZodOptional<import("zod").ZodEnum<["and", "or"]>>;
331
- startDate: import("zod").ZodString;
331
+ startDate: import("zod").ZodOptional<import("zod").ZodString>;
332
+ overdueOption: import("zod").ZodOptional<import("zod").ZodEnum<["overdue-only", "include-overdue", "exclude-overdue"]>>;
332
333
  daysCount: import("zod").ZodDefault<import("zod").ZodNumber>;
333
334
  limit: import("zod").ZodDefault<import("zod").ZodNumber>;
334
335
  cursor: import("zod").ZodOptional<import("zod").ZodString>;
335
336
  };
336
337
  execute(args: {
337
338
  limit: number;
338
- startDate: string;
339
339
  daysCount: number;
340
340
  labels?: string[] | undefined;
341
341
  cursor?: string | undefined;
342
342
  labelsOperator?: "and" | "or" | undefined;
343
+ startDate?: string | undefined;
344
+ overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
343
345
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
344
346
  content: {
345
347
  type: "text";
@@ -366,11 +368,12 @@ declare const tools: {
366
368
  hasMore: boolean;
367
369
  appliedFilters: {
368
370
  limit: number;
369
- startDate: string;
370
371
  daysCount: number;
371
372
  labels?: string[] | undefined;
372
373
  cursor?: string | undefined;
373
374
  labelsOperator?: "and" | "or" | undefined;
375
+ startDate?: string | undefined;
376
+ overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
374
377
  };
375
378
  };
376
379
  } | {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,yCAAyC,CAAA;AACtF,OAAO,KAAK,EAAc,CAAC,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAkBpD;;;;;;;GAOG;AACH,iBAAS,aAAa,CAAC,iBAAiB,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtE,WAAW,EACX,iBAAiB,GACpB,EAAE;IACC,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,iBAAiB,CAAA;CACvC;;;;;;;;;;;;;;;;;EAeA;AASD;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,EAC9C,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,UAAU,QAqBrB;AAED,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA"}
1
+ {"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,yCAAyC,CAAA;AACtF,OAAO,KAAK,EAAc,CAAC,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmBpD;;;;;;;GAOG;AACH,iBAAS,aAAa,CAAC,iBAAiB,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtE,WAAW,EACX,iBAAiB,GACpB,EAAE;IACC,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,iBAAiB,CAAA;CACvC;;;;;;;;;;;;;;;;;EAkBA;AASD;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,EAC9C,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,UAAU,QAqBrB;AAED,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { removeNullFields } from './utils/sanitize-data.js';
1
2
  /**
2
3
  * Wether to return the structured content directly, vs. in the `content` part of the output.
3
4
  *
@@ -21,13 +22,15 @@ const USE_STRUCTURED_CONTENT = process.env.USE_STRUCTURED_CONTENT === 'true' ||
21
22
  * @see USE_STRUCTURED_CONTENT - Wether to use the structured content feature of the MCP protocol.
22
23
  */
23
24
  function getToolOutput({ textContent, structuredContent, }) {
25
+ // Remove null fields from structured content before returning
26
+ const sanitizedContent = removeNullFields(structuredContent);
24
27
  if (USE_STRUCTURED_CONTENT) {
25
28
  return {
26
29
  content: [{ type: 'text', text: textContent }],
27
- structuredContent,
30
+ structuredContent: sanitizedContent,
28
31
  };
29
32
  }
30
- const json = JSON.stringify(structuredContent);
33
+ const json = JSON.stringify(sanitizedContent);
31
34
  return {
32
35
  content: [
33
36
  { type: 'text', text: textContent },
@@ -143,7 +143,11 @@ describe(`${ADD_PROJECTS} tool`, () => {
143
143
  // Verify structured content
144
144
  const structuredContent = extractStructuredContent(result);
145
145
  expect(structuredContent).toEqual(expect.objectContaining({
146
- projects: mockProjects,
146
+ projects: expect.arrayContaining([
147
+ expect.objectContaining({ id: 'project-1', name: 'First Project' }),
148
+ expect.objectContaining({ id: 'project-2', name: 'Second Project' }),
149
+ expect.objectContaining({ id: 'project-3', name: 'Third Project' }),
150
+ ]),
147
151
  totalCount: 3,
148
152
  }));
149
153
  });
@@ -122,7 +122,11 @@ describe(`${ADD_SECTIONS} tool`, () => {
122
122
  // Verify structured content
123
123
  const structuredContent = extractStructuredContent(result);
124
124
  expect(structuredContent).toEqual(expect.objectContaining({
125
- sections: mockSections,
125
+ sections: expect.arrayContaining([
126
+ expect.objectContaining({ id: 'section-1', name: 'First Section' }),
127
+ expect.objectContaining({ id: 'section-2', name: 'Second Section' }),
128
+ expect.objectContaining({ id: 'section-3', name: 'Third Section' }),
129
+ ]),
126
130
  totalCount: 3,
127
131
  }));
128
132
  });
@@ -311,7 +311,7 @@ describe('Assignment Integration Tests', () => {
311
311
  projectId: 'project-123',
312
312
  }, mockTodoistApi);
313
313
  expect(extractTextContent(result)).toContain('is not shared and has no collaborators');
314
- expect(extractStructuredContent(result).collaborators).toHaveLength(0);
314
+ expect(extractStructuredContent(result).collaborators).toBeUndefined(); // Empty arrays are removed
315
315
  });
316
316
  it('should handle project not found', async () => {
317
317
  mockTodoistApi.getProject.mockRejectedValueOnce(new Error('Project not found'));
@@ -24,13 +24,13 @@ describe(`${COMPLETE_TASKS} tool`, () => {
24
24
  expect(extractTextContent(result)).toMatchSnapshot();
25
25
  // Verify structured content
26
26
  const { structuredContent } = result;
27
- expect(structuredContent).toEqual(expect.objectContaining({
27
+ expect(structuredContent).toEqual({
28
28
  completed: ['task-1', 'task-2', 'task-3'],
29
- failures: [],
29
+ // failures array is removed when empty
30
30
  totalRequested: 3,
31
31
  successCount: 3,
32
32
  failureCount: 0,
33
- }));
33
+ });
34
34
  });
35
35
  it('should complete single task', async () => {
36
36
  mockTodoistApi.closeTask.mockResolvedValue(true);
@@ -40,13 +40,13 @@ describe(`${COMPLETE_TASKS} tool`, () => {
40
40
  expect(extractTextContent(result)).toMatchSnapshot();
41
41
  // Verify structured content
42
42
  const { structuredContent } = result;
43
- expect(structuredContent).toEqual(expect.objectContaining({
43
+ expect(structuredContent).toEqual({
44
44
  completed: ['8485093748'],
45
- failures: [],
45
+ // failures array is removed when empty
46
46
  totalRequested: 1,
47
47
  successCount: 1,
48
48
  failureCount: 0,
49
- }));
49
+ });
50
50
  });
51
51
  it('should handle partial failures gracefully', async () => {
52
52
  // Mock first and third tasks to succeed, second to fail
@@ -55,7 +55,6 @@ describe(`${FIND_COMMENTS} tool`, () => {
55
55
  searchType: 'task',
56
56
  searchId: 'task123',
57
57
  hasMore: false,
58
- nextCursor: null,
59
58
  totalCount: 2,
60
59
  }));
61
60
  });
@@ -128,7 +127,6 @@ describe(`${FIND_COMMENTS} tool`, () => {
128
127
  searchType: 'project',
129
128
  searchId: 'project456',
130
129
  hasMore: false,
131
- nextCursor: null,
132
130
  totalCount: 1,
133
131
  }));
134
132
  });
@@ -155,13 +153,11 @@ describe(`${FIND_COMMENTS} tool`, () => {
155
153
  id: 'comment789',
156
154
  content: 'Single comment content',
157
155
  taskId: 'task123',
158
- fileAttachment: null,
159
156
  }),
160
157
  ]),
161
158
  searchType: 'single',
162
159
  searchId: 'comment789',
163
160
  hasMore: false,
164
- nextCursor: null,
165
161
  totalCount: 1,
166
162
  }));
167
163
  });
@@ -200,7 +196,6 @@ describe(`${FIND_COMMENTS} tool`, () => {
200
196
  searchType: 'single',
201
197
  searchId: 'comment789',
202
198
  hasMore: false,
203
- nextCursor: null,
204
199
  totalCount: 1,
205
200
  }));
206
201
  });
@@ -229,14 +224,13 @@ describe(`${FIND_COMMENTS} tool`, () => {
229
224
  expect(extractTextContent(result)).toMatchSnapshot();
230
225
  // Verify structured content
231
226
  const structuredContent = extractStructuredContent(result);
232
- expect(structuredContent).toEqual(expect.objectContaining({
233
- comments: [],
227
+ expect(structuredContent).toEqual({
234
228
  searchType: 'task',
235
229
  searchId: 'task123',
236
230
  hasMore: false,
237
- nextCursor: null,
238
231
  totalCount: 0,
239
- }));
232
+ // comments array is removed when empty
233
+ });
240
234
  });
241
235
  });
242
236
  });
@@ -53,7 +53,6 @@ describe(`${FIND_PROJECTS} tool`, () => {
53
53
  projects: expect.any(Array),
54
54
  totalCount: 3,
55
55
  hasMore: false,
56
- nextCursor: null,
57
56
  appliedFilters: {
58
57
  search: undefined,
59
58
  limit: 50,
@@ -112,7 +111,6 @@ describe(`${FIND_PROJECTS} tool`, () => {
112
111
  expect(structuredContent.projects).toHaveLength(2); // Should match filtered results
113
112
  expect(structuredContent.totalCount).toBe(2);
114
113
  expect(structuredContent.hasMore).toBe(false);
115
- expect(structuredContent.nextCursor).toBeNull();
116
114
  expect(structuredContent.appliedFilters).toEqual({
117
115
  search: 'work',
118
116
  limit: 50,
@@ -77,12 +77,8 @@ describe(`${FIND_SECTIONS} tool`, () => {
77
77
  expect(textContent).toContain(`Use ${ADD_SECTIONS} to create sections`);
78
78
  // Verify structured content
79
79
  const structuredContent = extractStructuredContent(result);
80
- expect(structuredContent.sections).toHaveLength(0);
80
+ expect(structuredContent.sections).toBeUndefined(); // Empty arrays are removed
81
81
  expect(structuredContent.totalCount).toBe(0);
82
- expect(structuredContent.appliedFilters).toEqual({
83
- projectId: 'empty-project-id',
84
- search: undefined,
85
- });
86
82
  });
87
83
  });
88
84
  describe('searching sections by name', () => {
@@ -20,18 +20,19 @@ const mockTodoistApi = {
20
20
  const mockTodoistUser = createMockUser();
21
21
  // Mock date-fns functions to make tests deterministic
22
22
  jest.mock('date-fns', () => ({
23
- addDays: jest.fn(() => new Date('2025-08-16')), // Return predictable end date
23
+ addDays: jest.fn((date, amount) => {
24
+ const d = new Date(date);
25
+ d.setDate(d.getDate() + amount);
26
+ return d;
27
+ }),
24
28
  formatISO: jest.fn((date, options) => {
25
29
  if (typeof date === 'string') {
26
30
  return date; // Return string dates as-is
27
31
  }
28
- if (options &&
29
- typeof options === 'object' &&
30
- 'representation' in options &&
31
- options.representation === 'date') {
32
- return '2025-08-15'; // Return predictable date for 'today'
32
+ if (options?.representation === 'date') {
33
+ return date.toISOString().split('T')[0];
33
34
  }
34
- return '2025-08-16'; // Return predictable end date
35
+ return date.toISOString();
35
36
  }),
36
37
  }));
37
38
  const { FIND_TASKS_BY_DATE, UPDATE_TASKS } = ToolNames;
@@ -46,6 +47,23 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
46
47
  jest.restoreAllMocks();
47
48
  });
48
49
  describe('listing tasks by date range', () => {
50
+ it('only returns tasks for the startDate when daysCount is 1', async () => {
51
+ const mockTasks = [
52
+ createMappedTask({ content: 'Task for specific date', dueDate: '2025-08-20' }),
53
+ ];
54
+ const mockResponse = { tasks: mockTasks, nextCursor: null };
55
+ mockGetTasksByFilter.mockResolvedValue(mockResponse);
56
+ const result = await findTasksByDate.execute({ startDate: '2025-08-20', limit: 50, daysCount: 1 }, mockTodoistApi);
57
+ // Verify the query uses daysCount=1 by checking the end date calculation
58
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
59
+ client: mockTodoistApi,
60
+ query: '(due after: 2025-08-20 | due: 2025-08-20) & due before: 2025-08-21',
61
+ cursor: undefined,
62
+ limit: 50,
63
+ });
64
+ const textContent = extractTextContent(result);
65
+ expect(textContent).toMatchSnapshot();
66
+ });
49
67
  it('should get tasks for today when startDate is "today" (includes overdue)', async () => {
50
68
  const mockTasks = [createMappedTask({ content: 'Today task', dueDate: '2025-08-15' })];
51
69
  const mockResponse = { tasks: mockTasks, nextCursor: null };
@@ -375,7 +393,7 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
375
393
  ];
376
394
  const mockResponse = { tasks: mockTasks, nextCursor: null };
377
395
  mockGetTasksByFilter.mockResolvedValue(mockResponse);
378
- const result = await findTasksByDate.execute({ startDate: 'overdue', daysCount: 1, limit: 50 }, mockTodoistApi);
396
+ const result = await findTasksByDate.execute({ overdueOption: 'overdue-only', daysCount: 1, limit: 50 }, mockTodoistApi);
379
397
  const structuredContent = extractStructuredContent(result);
380
398
  // Should only return tasks 1 and 2, not task 3
381
399
  expect(structuredContent.tasks).toHaveLength(2);
@@ -117,7 +117,6 @@ describe(`${FIND_TASKS} tool`, () => {
117
117
  tasks: expect.any(Array),
118
118
  totalCount: 1,
119
119
  hasMore: false,
120
- nextCursor: null,
121
120
  appliedFilters: expect.objectContaining({
122
121
  searchText: params.searchText,
123
122
  limit: expectedLimit,
@@ -144,15 +143,14 @@ describe(`${FIND_TASKS} tool`, () => {
144
143
  expect(extractTextContent(result)).toMatchSnapshot();
145
144
  // Verify structured content for empty results
146
145
  const structuredContent = extractStructuredContent(result);
147
- expect(structuredContent).toEqual(expect.objectContaining({
148
- tasks: [],
146
+ expect(structuredContent).toEqual({
147
+ // tasks array is removed when empty
149
148
  totalCount: 0,
150
149
  hasMore: false,
151
- nextCursor: null,
152
150
  appliedFilters: expect.objectContaining({
153
151
  searchText: searchText,
154
152
  }),
155
- }));
153
+ });
156
154
  });
157
155
  });
158
156
  describe('validation', () => {
@@ -59,7 +59,7 @@ describe(`${GET_OVERVIEW} tool`, () => {
59
59
  inbox: expect.objectContaining({
60
60
  id: TEST_IDS.PROJECT_INBOX,
61
61
  name: 'Inbox',
62
- sections: expect.any(Array),
62
+ // sections array removed if empty
63
63
  }),
64
64
  projects: expect.any(Array),
65
65
  totalProjects: 2,
@@ -75,14 +75,13 @@ describe(`${GET_OVERVIEW} tool`, () => {
75
75
  expect(extractTextContent(result)).toMatchSnapshot();
76
76
  // Test structured content sanity checks
77
77
  const structuredContent = extractStructuredContent(result);
78
- expect(structuredContent).toEqual(expect.objectContaining({
78
+ expect(structuredContent).toEqual({
79
79
  type: 'account_overview',
80
- inbox: null,
81
- projects: [],
80
+ // projects array is removed when empty
82
81
  totalProjects: 0,
83
82
  totalSections: 0,
84
83
  hasNestedProjects: false,
85
- }));
84
+ });
86
85
  });
87
86
  });
88
87
  describe('project overview (with projectId)', () => {
@@ -188,20 +187,19 @@ describe(`${GET_OVERVIEW} tool`, () => {
188
187
  expect(extractTextContent(result)).toMatchSnapshot();
189
188
  // Test structured content sanity checks
190
189
  const structuredContent = extractStructuredContent(result);
191
- expect(structuredContent).toEqual(expect.objectContaining({
190
+ expect(structuredContent).toEqual({
192
191
  type: 'project_overview',
193
192
  project: expect.objectContaining({
194
193
  id: 'empty-project-id',
195
194
  name: 'Empty Project',
196
195
  }),
197
- sections: [],
198
- tasks: [],
196
+ // sections and tasks arrays are removed when empty
199
197
  stats: expect.objectContaining({
200
198
  totalTasks: 0,
201
199
  totalSections: 0,
202
200
  tasksWithoutSection: 0,
203
201
  }),
204
- }));
202
+ });
205
203
  });
206
204
  });
207
205
  describe('error handling', () => {
@@ -52,7 +52,6 @@ describe(`${UPDATE_COMMENTS} tool`, () => {
52
52
  id: '98765',
53
53
  content: 'Updated content here',
54
54
  taskId: 'task456',
55
- fileAttachment: null,
56
55
  }),
57
56
  ],
58
57
  totalCount: 1,
@@ -89,7 +88,6 @@ describe(`${UPDATE_COMMENTS} tool`, () => {
89
88
  content: 'Updated project comment',
90
89
  taskId: undefined,
91
90
  projectId: 'project789',
92
- fileAttachment: null,
93
91
  }),
94
92
  ],
95
93
  totalCount: 1,
@@ -47,7 +47,12 @@ describe(`${UPDATE_PROJECTS} tool`, () => {
47
47
  // Verify structured content
48
48
  const structuredContent = extractStructuredContent(result);
49
49
  expect(structuredContent).toEqual(expect.objectContaining({
50
- projects: [mockApiResponse],
50
+ projects: expect.arrayContaining([
51
+ expect.objectContaining({
52
+ id: 'existing-project-123',
53
+ name: 'Updated Project Name',
54
+ }),
55
+ ]),
51
56
  totalCount: 1,
52
57
  updatedProjectIds: ['existing-project-123'],
53
58
  appliedOperations: {
@@ -140,7 +145,14 @@ describe(`${UPDATE_PROJECTS} tool`, () => {
140
145
  // Verify structured content
141
146
  const structuredContent = extractStructuredContent(result);
142
147
  expect(structuredContent).toEqual(expect.objectContaining({
143
- projects: mockProjects,
148
+ projects: expect.arrayContaining([
149
+ expect.objectContaining({ id: 'project-1', name: 'Updated First Project' }),
150
+ expect.objectContaining({
151
+ id: 'project-2',
152
+ name: 'Updated Second Project',
153
+ }),
154
+ expect.objectContaining({ id: 'project-3', name: 'Updated Third Project' }),
155
+ ]),
144
156
  totalCount: 3,
145
157
  updatedProjectIds: ['project-1', 'project-2', 'project-3'],
146
158
  appliedOperations: {
@@ -40,7 +40,12 @@ describe(`${UPDATE_SECTIONS} tool`, () => {
40
40
  // Verify structured content
41
41
  const structuredContent = extractStructuredContent(result);
42
42
  expect(structuredContent).toEqual(expect.objectContaining({
43
- sections: [mockApiResponse],
43
+ sections: expect.arrayContaining([
44
+ expect.objectContaining({
45
+ id: 'existing-section-123',
46
+ name: 'Updated Section Name',
47
+ }),
48
+ ]),
44
49
  totalCount: 1,
45
50
  updatedSectionIds: ['existing-section-123'],
46
51
  }));
@@ -97,7 +102,14 @@ describe(`${UPDATE_SECTIONS} tool`, () => {
97
102
  // Verify structured content
98
103
  const structuredContent = extractStructuredContent(result);
99
104
  expect(structuredContent).toEqual(expect.objectContaining({
100
- sections: mockSections,
105
+ sections: expect.arrayContaining([
106
+ expect.objectContaining({ id: 'section-1', name: 'Updated First Section' }),
107
+ expect.objectContaining({
108
+ id: 'section-2',
109
+ name: 'Updated Second Section',
110
+ }),
111
+ expect.objectContaining({ id: 'section-3', name: 'Updated Third Section' }),
112
+ ]),
101
113
  totalCount: 3,
102
114
  updatedSectionIds: ['section-1', 'section-2', 'section-3'],
103
115
  }));
@@ -673,7 +673,7 @@ describe(`${UPDATE_TASKS} tool`, () => {
673
673
  const textContent = extractTextContent(result);
674
674
  expect(textContent).toContain('Updated 0 tasks');
675
675
  const structuredContent = extractStructuredContent(result);
676
- expect(structuredContent.tasks).toHaveLength(0);
676
+ expect(structuredContent.tasks).toBeUndefined(); // Empty arrays are removed
677
677
  expect(structuredContent.totalCount).toBe(0);
678
678
  });
679
679
  });
@@ -5,18 +5,20 @@ declare const findTasksByDate: {
5
5
  parameters: {
6
6
  labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
7
7
  labelsOperator: z.ZodOptional<z.ZodEnum<["and", "or"]>>;
8
- startDate: z.ZodString;
8
+ startDate: z.ZodOptional<z.ZodString>;
9
+ overdueOption: z.ZodOptional<z.ZodEnum<["overdue-only", "include-overdue", "exclude-overdue"]>>;
9
10
  daysCount: z.ZodDefault<z.ZodNumber>;
10
11
  limit: z.ZodDefault<z.ZodNumber>;
11
12
  cursor: z.ZodOptional<z.ZodString>;
12
13
  };
13
14
  execute(args: {
14
15
  limit: number;
15
- startDate: string;
16
16
  daysCount: number;
17
17
  labels?: string[] | undefined;
18
18
  cursor?: string | undefined;
19
19
  labelsOperator?: "and" | "or" | undefined;
20
+ startDate?: string | undefined;
21
+ overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
20
22
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
21
23
  content: {
22
24
  type: "text";
@@ -43,11 +45,12 @@ declare const findTasksByDate: {
43
45
  hasMore: boolean;
44
46
  appliedFilters: {
45
47
  limit: number;
46
- startDate: string;
47
48
  daysCount: number;
48
49
  labels?: string[] | undefined;
49
50
  cursor?: string | undefined;
50
51
  labelsOperator?: "and" | "or" | undefined;
52
+ startDate?: string | undefined;
53
+ overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
51
54
  };
52
55
  };
53
56
  } | {
@@ -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;AA0CvB,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0DqB,CAAA;AAoE1C,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;AAmDvB,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmEqB,CAAA;AAwF1C,OAAO,EAAE,eAAe,EAAE,CAAA"}
@@ -10,14 +10,19 @@ const ArgsSchema = {
10
10
  startDate: z
11
11
  .string()
12
12
  .regex(/^(\d{4}-\d{2}-\d{2}|today)$/)
13
+ .optional()
13
14
  .describe("The start date to get the tasks for. Format: YYYY-MM-DD or 'today'."),
15
+ overdueOption: z
16
+ .enum(['overdue-only', 'include-overdue', 'exclude-overdue'])
17
+ .optional()
18
+ .describe("How to handle overdue tasks. 'overdue-only' to get only overdue tasks, 'include-overdue' to include overdue tasks along with tasks for the specified date(s), and 'exclude-overdue' to exclude overdue tasks. Default is 'include-overdue'."),
14
19
  daysCount: z
15
20
  .number()
16
21
  .int()
17
22
  .min(1)
18
23
  .max(30)
19
24
  .default(1)
20
- .describe('The number of days to get the tasks for, starting from the start date.'),
25
+ .describe('The number of days to get the tasks for, starting from the start date. Default is 1 which means only tasks for the start date.'),
21
26
  limit: z
22
27
  .number()
23
28
  .int()
@@ -36,14 +41,22 @@ const findTasksByDate = {
36
41
  description: "Get tasks by date range. Use startDate 'today' to get today's tasks including overdue items, or provide a specific date/date range.",
37
42
  parameters: ArgsSchema,
38
43
  async execute(args, client) {
44
+ if (!args.startDate && args.overdueOption !== 'overdue-only') {
45
+ throw new Error('Either startDate must be provided or overdueOption must be set to overdue-only');
46
+ }
39
47
  let query = '';
40
48
  const todoistUser = await client.getUser();
41
- if (args.startDate === 'today') {
42
- query = 'today | overdue';
49
+ if (args.overdueOption === 'overdue-only') {
50
+ query = 'overdue';
43
51
  }
44
- else {
52
+ else if (args.startDate === 'today') {
53
+ // For 'today', include overdue unless explicitly excluded
54
+ query = args.overdueOption === 'exclude-overdue' ? 'today' : 'today | overdue';
55
+ }
56
+ else if (args.startDate) {
57
+ // For specific dates, never include overdue tasks
45
58
  const startDate = args.startDate;
46
- const endDate = addDays(startDate, args.daysCount + 1);
59
+ const endDate = addDays(startDate, args.daysCount);
47
60
  const endDateStr = formatISO(endDate, { representation: 'date' });
48
61
  query = `(due after: ${startDate} | due: ${startDate}) & due before: ${endDateStr}`;
49
62
  }
@@ -87,11 +100,18 @@ const findTasksByDate = {
87
100
  function generateTextContent({ tasks, args, nextCursor, }) {
88
101
  // Generate filter description
89
102
  const filterHints = [];
90
- if (args.startDate === 'today') {
91
- filterHints.push(`today + overdue tasks${args.daysCount > 1 ? ` + ${args.daysCount - 1} more days` : ''}`);
103
+ if (args.overdueOption === 'overdue-only') {
104
+ filterHints.push('overdue tasks only');
92
105
  }
93
- else {
94
- filterHints.push(`${args.startDate}${args.daysCount > 1 ? ` to ${getDateString(addDays(args.startDate, args.daysCount))}` : ''}`);
106
+ else if (args.startDate === 'today') {
107
+ const overdueText = args.overdueOption === 'exclude-overdue' ? '' : ' + overdue tasks';
108
+ filterHints.push(`today${overdueText}${args.daysCount > 1 ? ` + ${args.daysCount - 1} more days` : ''}`);
109
+ }
110
+ else if (args.startDate) {
111
+ const dateRange = args.daysCount > 1
112
+ ? ` to ${getDateString(addDays(args.startDate, args.daysCount))}`
113
+ : '';
114
+ filterHints.push(`${args.startDate}${dateRange}`);
95
115
  }
96
116
  // Add label filter information
97
117
  if (args.labels && args.labels.length > 0) {
@@ -101,12 +121,29 @@ function generateTextContent({ tasks, args, nextCursor, }) {
101
121
  filterHints.push(`labels: ${labelText}`);
102
122
  }
103
123
  // Generate subject description
104
- const subject = args.startDate === 'today' ? `Today's tasks + overdue` : `Tasks for ${args.startDate}`;
124
+ let subject = '';
125
+ if (args.overdueOption === 'overdue-only') {
126
+ subject = 'Overdue tasks';
127
+ }
128
+ else if (args.startDate === 'today') {
129
+ subject =
130
+ args.overdueOption === 'exclude-overdue' ? `Today's tasks` : `Today's tasks + overdue`;
131
+ }
132
+ else if (args.startDate) {
133
+ subject = `Tasks for ${args.startDate}`;
134
+ }
135
+ else {
136
+ subject = 'Tasks';
137
+ }
105
138
  // Generate helpful suggestions for empty results
106
139
  const zeroReasonHints = [];
107
140
  if (tasks.length === 0) {
108
- if (args.startDate === 'today') {
109
- zeroReasonHints.push('Great job! No tasks for today or overdue');
141
+ if (args.overdueOption === 'overdue-only') {
142
+ zeroReasonHints.push('Great job! No overdue tasks');
143
+ }
144
+ else if (args.startDate === 'today') {
145
+ const overdueNote = args.overdueOption === 'exclude-overdue' ? '' : ' or overdue';
146
+ zeroReasonHints.push(`Great job! No tasks for today${overdueNote}`);
110
147
  }
111
148
  else {
112
149
  zeroReasonHints.push("Expand date range with larger 'daysCount'");
@@ -116,10 +153,12 @@ function generateTextContent({ tasks, args, nextCursor, }) {
116
153
  // Generate contextual next steps
117
154
  const now = new Date();
118
155
  const todayStr = getDateString(now);
156
+ const hasOverdue = args.overdueOption === 'overdue-only' ||
157
+ args.startDate === 'today' ||
158
+ tasks.some((task) => task.dueDate && new Date(task.dueDate) < now);
119
159
  const nextSteps = generateTaskNextSteps('listed', tasks, {
120
160
  hasToday: args.startDate === 'today' || tasks.some((task) => task.dueDate === todayStr),
121
- hasOverdue: args.startDate === 'today' ||
122
- tasks.some((task) => task.dueDate && new Date(task.dueDate) < now),
161
+ hasOverdue,
123
162
  });
124
163
  return summarizeList({
125
164
  subject,
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Removes all null fields, empty objects, and empty arrays from an object recursively.
3
+ * This ensures that data sent to agents doesn't include unnecessary empty values.
4
+ *
5
+ * @param obj - The object to sanitize
6
+ * @returns A new object with all null fields, empty objects, and empty arrays removed
7
+ */
8
+ export declare function removeNullFields<T>(obj: T): T;
9
+ //# sourceMappingURL=sanitize-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-data.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize-data.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAqC7C"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Removes all null fields, empty objects, and empty arrays from an object recursively.
3
+ * This ensures that data sent to agents doesn't include unnecessary empty values.
4
+ *
5
+ * @param obj - The object to sanitize
6
+ * @returns A new object with all null fields, empty objects, and empty arrays removed
7
+ */
8
+ export function removeNullFields(obj) {
9
+ if (obj === null || obj === undefined) {
10
+ return obj;
11
+ }
12
+ if (Array.isArray(obj)) {
13
+ return obj.map((item) => removeNullFields(item));
14
+ }
15
+ if (typeof obj === 'object') {
16
+ const sanitized = {};
17
+ for (const [key, value] of Object.entries(obj)) {
18
+ if (value !== null) {
19
+ const cleanedValue = removeNullFields(value);
20
+ // Skip empty arrays
21
+ if (Array.isArray(cleanedValue) && cleanedValue.length === 0) {
22
+ continue;
23
+ }
24
+ // Skip empty objects
25
+ if (cleanedValue !== null &&
26
+ typeof cleanedValue === 'object' &&
27
+ !Array.isArray(cleanedValue) &&
28
+ Object.keys(cleanedValue).length === 0) {
29
+ continue;
30
+ }
31
+ sanitized[key] = cleanedValue;
32
+ }
33
+ }
34
+ return sanitized;
35
+ }
36
+ return obj;
37
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sanitize-data.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-data.test.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize-data.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,93 @@
1
+ import { removeNullFields } from './sanitize-data.js';
2
+ describe('removeNullFields', () => {
3
+ it('should remove null fields from objects including nested objects', () => {
4
+ const input = {
5
+ name: 'John',
6
+ age: null,
7
+ email: 'john@example.com',
8
+ phone: null,
9
+ address: {
10
+ street: '123 Main St',
11
+ city: null,
12
+ country: 'USA',
13
+ },
14
+ };
15
+ const result = removeNullFields(input);
16
+ expect(result).toEqual({
17
+ name: 'John',
18
+ email: 'john@example.com',
19
+ address: {
20
+ street: '123 Main St',
21
+ country: 'USA',
22
+ },
23
+ });
24
+ });
25
+ it('should handle arrays with null values', () => {
26
+ const input = {
27
+ items: [
28
+ { id: 1, value: 'test' },
29
+ { id: 2, value: null },
30
+ { id: 3, value: 'another' },
31
+ ],
32
+ };
33
+ const result = removeNullFields(input);
34
+ expect(result).toEqual({
35
+ items: [{ id: 1, value: 'test' }, { id: 2 }, { id: 3, value: 'another' }],
36
+ });
37
+ });
38
+ it('should handle null objects', () => {
39
+ const result = removeNullFields(null);
40
+ expect(result).toBeNull();
41
+ });
42
+ it('should remove empty objects and empty arrays', () => {
43
+ const input = {
44
+ something: 'hello',
45
+ another: {},
46
+ yetAnother: [],
47
+ };
48
+ const result = removeNullFields(input);
49
+ expect(result).toEqual({
50
+ something: 'hello',
51
+ });
52
+ });
53
+ it('should remove empty objects and empty arrays in nested structures', () => {
54
+ const input = {
55
+ name: 'Test',
56
+ metadata: {},
57
+ tags: [],
58
+ nested: {
59
+ data: 'value',
60
+ emptyObj: {},
61
+ emptyArr: [],
62
+ deepNested: {
63
+ anotherEmpty: {},
64
+ },
65
+ },
66
+ items: [
67
+ { id: 1, data: 'test', empty: {} },
68
+ { id: 2, list: [] },
69
+ ],
70
+ };
71
+ const result = removeNullFields(input);
72
+ expect(result).toEqual({
73
+ name: 'Test',
74
+ nested: {
75
+ data: 'value',
76
+ },
77
+ items: [{ id: 1, data: 'test' }, { id: 2 }],
78
+ });
79
+ });
80
+ it('should keep arrays with values and objects with properties', () => {
81
+ const input = {
82
+ emptyArray: [],
83
+ arrayWithValues: [1, 2, 3],
84
+ emptyObject: {},
85
+ objectWithProps: { key: 'value' },
86
+ };
87
+ const result = removeNullFields(input);
88
+ expect(result).toEqual({
89
+ arrayWithValues: [1, 2, 3],
90
+ objectWithProps: { key: 'value' },
91
+ });
92
+ });
93
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-ai",
3
- "version": "4.9.2",
3
+ "version": "4.9.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -52,7 +52,7 @@
52
52
  "zod": "^3.25.7"
53
53
  },
54
54
  "devDependencies": {
55
- "@biomejs/biome": "2.2.4",
55
+ "@biomejs/biome": "2.2.5",
56
56
  "@types/express": "^5.0.2",
57
57
  "@types/jest": "30.0.0",
58
58
  "@types/morgan": "^1.9.9",