@doist/todoist-ai 4.15.0 → 4.16.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/index.d.ts CHANGED
@@ -605,7 +605,6 @@ declare const tools: {
605
605
  projects: import("zod").ZodArray<import("zod").ZodObject<{
606
606
  id: import("zod").ZodString;
607
607
  name: import("zod").ZodOptional<import("zod").ZodString>;
608
- parentId: import("zod").ZodOptional<import("zod").ZodString>;
609
608
  isFavorite: import("zod").ZodOptional<import("zod").ZodBoolean>;
610
609
  viewStyle: import("zod").ZodOptional<import("zod").ZodEnum<["list", "board", "calendar"]>>;
611
610
  }, "strip", import("zod").ZodTypeAny, {
@@ -613,13 +612,11 @@ declare const tools: {
613
612
  name?: string | undefined;
614
613
  isFavorite?: boolean | undefined;
615
614
  viewStyle?: "list" | "board" | "calendar" | undefined;
616
- parentId?: string | undefined;
617
615
  }, {
618
616
  id: string;
619
617
  name?: string | undefined;
620
618
  isFavorite?: boolean | undefined;
621
619
  viewStyle?: "list" | "board" | "calendar" | undefined;
622
- parentId?: string | undefined;
623
620
  }>, "many">;
624
621
  };
625
622
  execute(args: {
@@ -628,7 +625,6 @@ declare const tools: {
628
625
  name?: string | undefined;
629
626
  isFavorite?: boolean | undefined;
630
627
  viewStyle?: "list" | "board" | "calendar" | undefined;
631
- parentId?: string | undefined;
632
628
  }[];
633
629
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
634
630
  content: {
@@ -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,10 +1,11 @@
1
1
  import { jest } from '@jest/globals';
2
- import { createMockTask, extractStructuredContent, extractTextContent, TODAY, } from '../../utils/test-helpers.js';
2
+ import { createMockTask, createMockUser, extractStructuredContent, extractTextContent, TEST_IDS, TODAY, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { addTasks } from '../add-tasks.js';
5
5
  // Mock the Todoist API
6
6
  const mockTodoistApi = {
7
7
  addTask: jest.fn(),
8
+ getUser: jest.fn(),
8
9
  };
9
10
  const { ADD_TASKS, GET_OVERVIEW } = ToolNames;
10
11
  describe(`${ADD_TASKS} tool`, () => {
@@ -530,4 +531,76 @@ describe(`${ADD_TASKS} tool`, () => {
530
531
  }, mockTodoistApi)).rejects.toThrow('Task "Task with assignment but no project": Cannot assign tasks without specifying project context. Please specify a projectId, sectionId, or parentId.');
531
532
  });
532
533
  });
534
+ describe('inbox project ID resolution', () => {
535
+ it('should resolve "inbox" to actual inbox project ID', async () => {
536
+ const mockUser = createMockUser({
537
+ inboxProjectId: TEST_IDS.PROJECT_INBOX,
538
+ });
539
+ const mockApiResponse = createMockTask({
540
+ id: '8485093760',
541
+ content: 'Task for inbox',
542
+ projectId: TEST_IDS.PROJECT_INBOX,
543
+ url: 'https://todoist.com/showTask?id=8485093760',
544
+ addedAt: '2025-08-13T22:09:56.123456Z',
545
+ });
546
+ // Mock the API calls
547
+ mockTodoistApi.getUser.mockResolvedValue(mockUser);
548
+ mockTodoistApi.addTask.mockResolvedValue(mockApiResponse);
549
+ const result = await addTasks.execute({
550
+ tasks: [
551
+ {
552
+ content: 'Task for inbox',
553
+ projectId: 'inbox',
554
+ },
555
+ ],
556
+ }, mockTodoistApi);
557
+ // Verify getUser was called to resolve inbox
558
+ expect(mockTodoistApi.getUser).toHaveBeenCalledTimes(1);
559
+ // Verify addTask was called with resolved inbox project ID
560
+ expect(mockTodoistApi.addTask).toHaveBeenCalledWith({
561
+ content: 'Task for inbox',
562
+ projectId: TEST_IDS.PROJECT_INBOX,
563
+ sectionId: undefined,
564
+ parentId: undefined,
565
+ labels: undefined,
566
+ });
567
+ // Verify result contains the task
568
+ const structuredContent = extractStructuredContent(result);
569
+ expect(structuredContent.totalCount).toBe(1);
570
+ expect(structuredContent.tasks).toEqual(expect.arrayContaining([
571
+ expect.objectContaining({
572
+ id: '8485093760',
573
+ projectId: TEST_IDS.PROJECT_INBOX,
574
+ }),
575
+ ]));
576
+ });
577
+ it('should not call getUser when projectId is not "inbox"', async () => {
578
+ const mockApiResponse = createMockTask({
579
+ id: '8485093761',
580
+ content: 'Regular task',
581
+ projectId: '6cfCcrrCFg2xP94Q',
582
+ url: 'https://todoist.com/showTask?id=8485093761',
583
+ addedAt: '2025-08-13T22:09:56.123456Z',
584
+ });
585
+ mockTodoistApi.addTask.mockResolvedValue(mockApiResponse);
586
+ await addTasks.execute({
587
+ tasks: [
588
+ {
589
+ content: 'Regular task',
590
+ projectId: '6cfCcrrCFg2xP94Q',
591
+ },
592
+ ],
593
+ }, mockTodoistApi);
594
+ // Verify getUser was NOT called for regular project ID
595
+ expect(mockTodoistApi.getUser).not.toHaveBeenCalled();
596
+ // Verify addTask was called with original project ID
597
+ expect(mockTodoistApi.addTask).toHaveBeenCalledWith({
598
+ content: 'Regular task',
599
+ projectId: '6cfCcrrCFg2xP94Q',
600
+ sectionId: undefined,
601
+ parentId: undefined,
602
+ labels: undefined,
603
+ });
604
+ });
605
+ });
533
606
  });
@@ -1,5 +1,5 @@
1
1
  import { jest } from '@jest/globals';
2
- import { createMockTask, extractTextContent } from '../../utils/test-helpers.js';
2
+ import { createMockTask, createMockUser, extractTextContent, TEST_IDS, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { findCompletedTasks } from '../find-completed-tasks.js';
5
5
  // Mock the Todoist API
@@ -321,4 +321,103 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
321
321
  }, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
322
322
  });
323
323
  });
324
+ describe('inbox project ID resolution', () => {
325
+ it('should resolve "inbox" to actual inbox project ID', async () => {
326
+ const mockUser = createMockUser({
327
+ inboxProjectId: TEST_IDS.PROJECT_INBOX,
328
+ tzInfo: {
329
+ timezone: 'UTC',
330
+ gmtString: '+00:00',
331
+ hours: 0,
332
+ minutes: 0,
333
+ isDst: 0,
334
+ },
335
+ });
336
+ const mockCompletedTasks = [
337
+ createMockTask({
338
+ id: '8485093760',
339
+ content: 'Completed inbox task',
340
+ projectId: TEST_IDS.PROJECT_INBOX,
341
+ completedAt: '2025-08-15T12:00:00Z',
342
+ url: 'https://todoist.com/showTask?id=8485093760',
343
+ addedAt: '2025-08-13T22:09:56.123456Z',
344
+ }),
345
+ ];
346
+ // Mock getUser to return our mock user with inbox ID
347
+ mockTodoistApi.getUser.mockResolvedValue(mockUser);
348
+ // Mock the API response
349
+ mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue({
350
+ items: mockCompletedTasks,
351
+ nextCursor: null,
352
+ });
353
+ const result = await findCompletedTasks.execute({
354
+ getBy: 'completion',
355
+ since: '2025-08-15',
356
+ until: '2025-08-15',
357
+ projectId: 'inbox',
358
+ labels: [],
359
+ labelsOperator: 'or',
360
+ limit: 50,
361
+ }, mockTodoistApi);
362
+ // Verify getUser was called
363
+ expect(mockTodoistApi.getUser).toHaveBeenCalledTimes(1);
364
+ // Verify getCompletedTasksByCompletionDate was called with resolved inbox project ID
365
+ expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith(expect.objectContaining({
366
+ projectId: TEST_IDS.PROJECT_INBOX,
367
+ since: '2025-08-15T00:00:00.000Z',
368
+ until: '2025-08-15T23:59:59.000Z',
369
+ limit: 50,
370
+ }));
371
+ // Verify result contains the completed tasks
372
+ const textContent = extractTextContent(result);
373
+ expect(textContent).toContain('Completed tasks');
374
+ expect(textContent).toContain('Completed inbox task');
375
+ });
376
+ it('should use regular project ID when not "inbox"', async () => {
377
+ const mockUser = createMockUser({
378
+ tzInfo: {
379
+ timezone: 'UTC',
380
+ gmtString: '+00:00',
381
+ hours: 0,
382
+ minutes: 0,
383
+ isDst: 0,
384
+ },
385
+ });
386
+ const mockCompletedTasks = [
387
+ createMockTask({
388
+ id: '8485093761',
389
+ content: 'Completed regular task',
390
+ projectId: '6cfCcrrCFg2xP94Q',
391
+ completedAt: '2025-08-15T12:00:00Z',
392
+ url: 'https://todoist.com/showTask?id=8485093761',
393
+ addedAt: '2025-08-13T22:09:56.123456Z',
394
+ }),
395
+ ];
396
+ // Mock getUser (will be called for timezone, but inbox resolution won't happen)
397
+ mockTodoistApi.getUser.mockResolvedValue(mockUser);
398
+ // Mock the API response
399
+ mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue({
400
+ items: mockCompletedTasks,
401
+ nextCursor: null,
402
+ });
403
+ await findCompletedTasks.execute({
404
+ getBy: 'completion',
405
+ since: '2025-08-15',
406
+ until: '2025-08-15',
407
+ projectId: '6cfCcrrCFg2xP94Q',
408
+ labels: [],
409
+ labelsOperator: 'or',
410
+ limit: 50,
411
+ }, mockTodoistApi);
412
+ // Verify getUser was called (for timezone info)
413
+ expect(mockTodoistApi.getUser).toHaveBeenCalledTimes(1);
414
+ // Verify getCompletedTasksByCompletionDate was called with original project ID
415
+ expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith(expect.objectContaining({
416
+ projectId: '6cfCcrrCFg2xP94Q',
417
+ since: '2025-08-15T00:00:00.000Z',
418
+ until: '2025-08-15T23:59:59.000Z',
419
+ limit: 50,
420
+ }));
421
+ });
422
+ });
324
423
  });
@@ -1,10 +1,11 @@
1
1
  import { jest } from '@jest/globals';
2
- import { createMockSection, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
2
+ import { createMockSection, createMockUser, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
3
3
  import { ToolNames } from '../../utils/tool-names.js';
4
4
  import { findSections } from '../find-sections.js';
5
5
  // Mock the Todoist API
6
6
  const mockTodoistApi = {
7
7
  getSections: jest.fn(),
8
+ getUser: jest.fn(),
8
9
  };
9
10
  const { FIND_SECTIONS, ADD_SECTIONS } = ToolNames;
10
11
  describe(`${FIND_SECTIONS} tool`, () => {
@@ -232,6 +233,73 @@ describe(`${FIND_SECTIONS} tool`, () => {
232
233
  expect(textContent).toContain('Done Soon • id=');
233
234
  });
234
235
  });
236
+ describe('inbox project ID resolution', () => {
237
+ it('should resolve "inbox" to actual inbox project ID', async () => {
238
+ const mockUser = createMockUser({
239
+ inboxProjectId: TEST_IDS.PROJECT_INBOX,
240
+ });
241
+ const mockSections = [
242
+ createMockSection({
243
+ id: TEST_IDS.SECTION_1,
244
+ projectId: TEST_IDS.PROJECT_INBOX,
245
+ name: 'Inbox Section 1',
246
+ }),
247
+ createMockSection({
248
+ id: TEST_IDS.SECTION_2,
249
+ projectId: TEST_IDS.PROJECT_INBOX,
250
+ name: 'Inbox Section 2',
251
+ sectionOrder: 2,
252
+ }),
253
+ ];
254
+ // Mock getUser to return our mock user with inbox ID
255
+ mockTodoistApi.getUser.mockResolvedValue(mockUser);
256
+ // Mock the API response
257
+ mockTodoistApi.getSections.mockResolvedValue({
258
+ results: mockSections,
259
+ nextCursor: null,
260
+ });
261
+ const result = await findSections.execute({ projectId: 'inbox' }, mockTodoistApi);
262
+ // Verify getUser was called to resolve inbox
263
+ expect(mockTodoistApi.getUser).toHaveBeenCalledTimes(1);
264
+ // Verify getSections was called with resolved inbox project ID
265
+ expect(mockTodoistApi.getSections).toHaveBeenCalledWith({
266
+ projectId: TEST_IDS.PROJECT_INBOX,
267
+ });
268
+ // Verify result contains the sections
269
+ const textContent = extractTextContent(result);
270
+ expect(textContent).toContain('Sections in project');
271
+ expect(textContent).toContain('Inbox Section 1');
272
+ expect(textContent).toContain('Inbox Section 2');
273
+ // Verify structured content
274
+ const structuredContent = extractStructuredContent(result);
275
+ expect(structuredContent.totalCount).toBe(2);
276
+ expect(structuredContent.sections).toEqual([
277
+ { id: TEST_IDS.SECTION_1, name: 'Inbox Section 1' },
278
+ { id: TEST_IDS.SECTION_2, name: 'Inbox Section 2' },
279
+ ]);
280
+ });
281
+ it('should not call getUser when projectId is not "inbox"', async () => {
282
+ const mockSections = [
283
+ createMockSection({
284
+ id: TEST_IDS.SECTION_1,
285
+ projectId: TEST_IDS.PROJECT_TEST,
286
+ name: 'Regular Section',
287
+ }),
288
+ ];
289
+ // Mock the API response
290
+ mockTodoistApi.getSections.mockResolvedValue({
291
+ results: mockSections,
292
+ nextCursor: null,
293
+ });
294
+ await findSections.execute({ projectId: TEST_IDS.PROJECT_TEST }, mockTodoistApi);
295
+ // Verify getUser was NOT called for regular project ID
296
+ expect(mockTodoistApi.getUser).not.toHaveBeenCalled();
297
+ // Verify getSections was called with original project ID
298
+ expect(mockTodoistApi.getSections).toHaveBeenCalledWith({
299
+ projectId: TEST_IDS.PROJECT_TEST,
300
+ });
301
+ });
302
+ });
235
303
  describe('error handling', () => {
236
304
  it.each([
237
305
  { error: 'API Error: Project not found', projectId: 'non-existent-project' },
@@ -104,24 +104,6 @@ describe(`${UPDATE_PROJECTS} tool`, () => {
104
104
  expect(textContent).toContain('Updated 1 project:');
105
105
  expect(textContent).toContain('Updated Favorite Project (id=project-123)');
106
106
  });
107
- it('should update project with parentId to move it to a sub-project', async () => {
108
- const mockApiResponse = createMockProject({
109
- id: 'project-child',
110
- name: 'Child Project',
111
- parentId: 'project-parent',
112
- });
113
- mockTodoistApi.updateProject.mockResolvedValue(mockApiResponse);
114
- const result = await updateProjects.execute({
115
- projects: [{ id: 'project-child', parentId: 'project-parent' }],
116
- }, mockTodoistApi);
117
- expect(mockTodoistApi.updateProject).toHaveBeenCalledWith('project-child', {
118
- parentId: 'project-parent',
119
- });
120
- const textContent = extractTextContent(result);
121
- expect(textContent).toMatchSnapshot();
122
- expect(textContent).toContain('Updated 1 project:');
123
- expect(textContent).toContain('Child Project (id=project-child)');
124
- });
125
107
  });
126
108
  describe('updating multiple projects', () => {
127
109
  it('should update multiple projects and return mapped results', async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"add-comments.d.ts","sourceRoot":"","sources":["../../src/tools/add-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAkBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BAoFygV,CAAC;4BAA6C,CAAC;4BAA6C,CAAC;2BAA4C,CAAC;gCAAiD,CAAC;+BAAgD,CAAC;yBAA2D,CAAC;8BAA+C,CAAC;+BAAgD,CAAC;uBAAwC,CAAC;yBAA0C,CAAC;;;;;;;;;;;;;;;;;;;;;;;CA1C98V,CAAA;AAyC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"add-comments.d.ts","sourceRoot":"","sources":["../../src/tools/add-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAuBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BA2F89T,CAAC;4BAA6C,CAAC;4BAA6C,CAAC;2BAA4C,CAAC;gCAAiD,CAAC;+BAAgD,CAAC;yBAA2D,CAAC;8BAA+C,CAAC;+BAAgD,CAAC;uBAAwC,CAAC;yBAA0C,CAAC;;;;;;;;;;;;;;;;;;;;;;;CA1Cn6U,CAAA;AAyC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -5,7 +5,10 @@ import { ToolNames } from '../utils/tool-names.js';
5
5
  const { FIND_COMMENTS, UPDATE_COMMENTS, DELETE_OBJECT } = ToolNames;
6
6
  const CommentSchema = z.object({
7
7
  taskId: z.string().optional().describe('The ID of the task to comment on.'),
8
- projectId: z.string().optional().describe('The ID of the project to comment on.'),
8
+ projectId: z
9
+ .string()
10
+ .optional()
11
+ .describe('The ID of the project to comment on. Project ID should be an ID string, or the text "inbox", for inbox tasks.'),
9
12
  content: z.string().min(1).describe('The content of the comment.'),
10
13
  });
11
14
  const ArgsSchema = {
@@ -26,10 +29,17 @@ const addComments = {
26
29
  throw new Error(`Comment ${index + 1}: Cannot provide both taskId and projectId. Choose one.`);
27
30
  }
28
31
  }
29
- const addCommentPromises = comments.map(async ({ content, taskId, projectId }) => await client.addComment({
30
- content,
31
- ...(taskId ? { taskId } : { projectId }),
32
- }));
32
+ // Check if any comment needs inbox resolution
33
+ const needsInboxResolution = comments.some((comment) => comment.projectId === 'inbox');
34
+ const todoistUser = needsInboxResolution ? await client.getUser() : null;
35
+ const addCommentPromises = comments.map(async ({ content, taskId, projectId }) => {
36
+ // Resolve "inbox" to actual inbox project ID if needed
37
+ const resolvedProjectId = projectId === 'inbox' && todoistUser ? todoistUser.inboxProjectId : projectId;
38
+ return await client.addComment({
39
+ content,
40
+ ...(taskId ? { taskId } : { projectId: resolvedProjectId }),
41
+ });
42
+ });
33
43
  const newComments = await Promise.all(addCommentPromises);
34
44
  const textContent = generateTextContent({ comments: newComments });
35
45
  return getToolOutput({
@@ -1 +1 @@
1
- {"version":3,"file":"add-sections.d.ts","sourceRoot":"","sources":["../../src/tools/add-sections.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgByB,CAAA;AA6C1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"add-sections.d.ts","sourceRoot":"","sources":["../../src/tools/add-sections.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAsBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+ByB,CAAA;AA6C1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -5,7 +5,10 @@ import { ToolNames } from '../utils/tool-names.js';
5
5
  const { ADD_TASKS, FIND_TASKS, GET_OVERVIEW, FIND_SECTIONS } = ToolNames;
6
6
  const SectionSchema = z.object({
7
7
  name: z.string().min(1).describe('The name of the section.'),
8
- projectId: z.string().min(1).describe('The ID of the project to add the section to.'),
8
+ projectId: z
9
+ .string()
10
+ .min(1)
11
+ .describe('The ID of the project to add the section to. Project ID should be an ID string, or the text "inbox", for inbox tasks.'),
9
12
  });
10
13
  const ArgsSchema = {
11
14
  sections: z.array(SectionSchema).min(1).describe('The array of sections to add.'),
@@ -15,7 +18,17 @@ const addSections = {
15
18
  description: 'Add one or more new sections to projects.',
16
19
  parameters: ArgsSchema,
17
20
  async execute({ sections }, client) {
18
- const newSections = await Promise.all(sections.map((section) => client.addSection(section)));
21
+ // Check if any section needs inbox resolution
22
+ const needsInboxResolution = sections.some((section) => section.projectId === 'inbox');
23
+ const todoistUser = needsInboxResolution ? await client.getUser() : null;
24
+ // Resolve inbox project IDs
25
+ const sectionsWithResolvedProjectIds = sections.map((section) => ({
26
+ ...section,
27
+ projectId: section.projectId === 'inbox' && todoistUser
28
+ ? todoistUser.inboxProjectId
29
+ : section.projectId,
30
+ }));
31
+ const newSections = await Promise.all(sectionsWithResolvedProjectIds.map((section) => client.addSection(section)));
19
32
  const textContent = generateTextContent({ sections: newSections });
20
33
  return getToolOutput({
21
34
  textContent,
@@ -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;AA2DvB,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AAwI1C,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;AAgEvB,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AA+I1C,OAAO,EAAE,QAAQ,EAAE,CAAA"}
@@ -26,7 +26,10 @@ const TaskSchema = z.object({
26
26
  .optional()
27
27
  .describe('The duration of the task. Use format: "2h" (hours), "90m" (minutes), "2h30m" (combined), or "1.5h" (decimal hours). Max 24h.'),
28
28
  labels: z.array(z.string()).optional().describe('The labels to attach to the task.'),
29
- projectId: z.string().optional().describe('The project ID to add this task to.'),
29
+ projectId: z
30
+ .string()
31
+ .optional()
32
+ .describe('The project ID to add this task to. Project ID should be an ID string, or the text "inbox", for inbox tasks.'),
30
33
  sectionId: z.string().optional().describe('The section ID to add this task to.'),
31
34
  parentId: z.string().optional().describe('The parent task ID (for subtasks).'),
32
35
  responsibleUser: z
@@ -60,9 +63,15 @@ const addTasks = {
60
63
  };
61
64
  async function processTask(task, client) {
62
65
  const { duration: durationStr, projectId, sectionId, parentId, responsibleUser, priority, labels, deadlineDate, ...otherTaskArgs } = task;
66
+ // Resolve "inbox" to actual inbox project ID if needed
67
+ let resolvedProjectId = projectId;
68
+ if (projectId === 'inbox') {
69
+ const todoistUser = await client.getUser();
70
+ resolvedProjectId = todoistUser.inboxProjectId;
71
+ }
63
72
  let taskArgs = {
64
73
  ...otherTaskArgs,
65
- projectId,
74
+ projectId: resolvedProjectId,
66
75
  sectionId,
67
76
  parentId,
68
77
  labels,
@@ -73,7 +82,7 @@ async function processTask(task, client) {
73
82
  taskArgs.priority = convertPriorityToNumber(priority);
74
83
  }
75
84
  // Only prevent assignment (not task creation) without sufficient project context
76
- if (responsibleUser && !projectId && !sectionId && !parentId) {
85
+ if (responsibleUser && !resolvedProjectId && !sectionId && !parentId) {
77
86
  throw new Error(`Task "${task.content}": Cannot assign tasks without specifying project context. Please specify a projectId, sectionId, or parentId.`);
78
87
  }
79
88
  // Parse duration if provided
@@ -96,7 +105,7 @@ async function processTask(task, client) {
96
105
  // Handle assignment if provided
97
106
  if (responsibleUser) {
98
107
  // Resolve target project for validation
99
- let targetProjectId = projectId;
108
+ let targetProjectId = resolvedProjectId;
100
109
  if (!targetProjectId && parentId) {
101
110
  // For subtasks, get project from parent task
102
111
  try {
@@ -57,7 +57,7 @@ const findActivity = {
57
57
  // Add optional filters
58
58
  if (objectType)
59
59
  apiArgs.objectType = objectType;
60
- if (objectId)
60
+ if (objectId && objectId !== 'remove')
61
61
  apiArgs.objectId = objectId;
62
62
  if (eventType)
63
63
  apiArgs.eventType = eventType;
@@ -1 +1 @@
1
- {"version":3,"file":"find-comments.d.ts","sourceRoot":"","sources":["../../src/tools/find-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAuBvB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BA2J++P,CAAC;4BAA6C,CAAC;4BAA6C,CAAC;2BAA4C,CAAC;gCAAiD,CAAC;+BAAgD,CAAC;yBAA2D,CAAC;8BAA+C,CAAC;+BAAgD,CAAC;uBAAwC,CAAC;yBAA0C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;CArFr7Q,CAAA;AAoF1C,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"find-comments.d.ts","sourceRoot":"","sources":["../../src/tools/find-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA4BvB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BA+JyqP,CAAC;4BAA6C,CAAC;4BAA6C,CAAC;2BAA4C,CAAC;gCAAiD,CAAC;+BAAgD,CAAC;yBAA2D,CAAC;8BAA+C,CAAC;+BAAgD,CAAC;uBAAwC,CAAC;yBAA0C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;CArF/mQ,CAAA;AAoF1C,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -6,7 +6,10 @@ import { ToolNames } from '../utils/tool-names.js';
6
6
  const { ADD_COMMENTS, UPDATE_COMMENTS, DELETE_OBJECT } = ToolNames;
7
7
  const ArgsSchema = {
8
8
  taskId: z.string().optional().describe('Find comments for a specific task.'),
9
- projectId: z.string().optional().describe('Find comments for a specific project.'),
9
+ projectId: z
10
+ .string()
11
+ .optional()
12
+ .describe('Find comments for a specific project. Project ID should be an ID string, or the text "inbox", for inbox tasks.'),
10
13
  commentId: z.string().optional().describe('Get a specific comment by ID.'),
11
14
  cursor: z.string().optional().describe('Pagination cursor for retrieving more results.'),
12
15
  limit: z
@@ -30,6 +33,8 @@ const findComments = {
30
33
  if (searchParams.length > 1) {
31
34
  throw new Error('Cannot provide multiple search parameters. Choose one of: taskId, projectId, or commentId.');
32
35
  }
36
+ // Resolve "inbox" to actual inbox project ID if needed
37
+ const resolvedProjectId = args.projectId === 'inbox' ? (await client.getUser()).inboxProjectId : args.projectId;
33
38
  let comments;
34
39
  let hasMore = false;
35
40
  let nextCursor = null;
@@ -49,10 +54,10 @@ const findComments = {
49
54
  hasMore = response.nextCursor !== null;
50
55
  nextCursor = response.nextCursor;
51
56
  }
52
- else if (args.projectId) {
57
+ else if (resolvedProjectId) {
53
58
  // Get comments by project
54
59
  const response = await client.getComments({
55
- projectId: args.projectId,
60
+ projectId: resolvedProjectId,
56
61
  cursor: args.cursor || null,
57
62
  limit: args.limit || ApiLimits.COMMENTS_DEFAULT,
58
63
  });
@@ -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;AAwDvB,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmEkB,CAAA;AA2E1C,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;AA6DvB,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyEkB,CAAA;AA2E1C,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
@@ -23,7 +23,10 @@ const ArgsSchema = {
23
23
  .regex(/^\d{4}-\d{2}-\d{2}$/)
24
24
  .describe('The start date to get the tasks for. Format: YYYY-MM-DD.'),
25
25
  workspaceId: z.string().optional().describe('The ID of the workspace to get the tasks for.'),
26
- projectId: z.string().optional().describe('The ID of the project to get the tasks for.'),
26
+ projectId: z
27
+ .string()
28
+ .optional()
29
+ .describe('The ID of the project to get the tasks for. Project ID should be an ID string, or the text "inbox", for inbox tasks.'),
27
30
  sectionId: z.string().optional().describe('The ID of the section to get the tasks for.'),
28
31
  parentId: z.string().optional().describe('The ID of the parent task to get the tasks for.'),
29
32
  responsibleUser: z
@@ -48,7 +51,7 @@ const findCompletedTasks = {
48
51
  description: 'Get completed tasks (includes all collaborators by default—use responsibleUser to narrow).',
49
52
  parameters: ArgsSchema,
50
53
  async execute(args, client) {
51
- const { getBy, labels, labelsOperator, since, until, responsibleUser, ...rest } = args;
54
+ const { getBy, labels, labelsOperator, since, until, responsibleUser, projectId, ...rest } = args;
52
55
  // Resolve assignee name to user ID if provided
53
56
  const resolved = await resolveResponsibleUser(client, responsibleUser);
54
57
  const assigneeEmail = resolved?.email;
@@ -61,6 +64,8 @@ const findCompletedTasks = {
61
64
  // Get user timezone to convert local dates to UTC
62
65
  const user = await client.getUser();
63
66
  const userGmtOffset = user.tzInfo?.gmtString || '+00:00';
67
+ // Resolve "inbox" to actual inbox project ID if needed
68
+ const resolvedProjectId = projectId === 'inbox' ? user.inboxProjectId : projectId;
64
69
  // Convert user's local date to UTC timestamps
65
70
  // This ensures we capture the entire day from the user's perspective
66
71
  const sinceWithOffset = `${since}T00:00:00${userGmtOffset}`;
@@ -71,12 +76,14 @@ const findCompletedTasks = {
71
76
  const { items, nextCursor } = getBy === 'completion'
72
77
  ? await client.getCompletedTasksByCompletionDate({
73
78
  ...rest,
79
+ projectId: resolvedProjectId,
74
80
  since: sinceDateTime,
75
81
  until: untilDateTime,
76
82
  ...(filterQuery ? { filterQuery, filterLang: 'en' } : {}),
77
83
  })
78
84
  : await client.getCompletedTasksByDueDate({
79
85
  ...rest,
86
+ projectId: resolvedProjectId,
80
87
  since: sinceDateTime,
81
88
  until: untilDateTime,
82
89
  ...(filterQuery ? { filterQuery, filterLang: 'en' } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"find-sections.d.ts","sourceRoot":"","sources":["../../src/tools/find-sections.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAuBvB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCwB,CAAA;AAqE1C,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"find-sections.d.ts","sourceRoot":"","sources":["../../src/tools/find-sections.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA4BvB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCwB,CAAA;AAqE1C,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -4,7 +4,10 @@ import { summarizeList } from '../utils/response-builders.js';
4
4
  import { ToolNames } from '../utils/tool-names.js';
5
5
  const { ADD_SECTIONS, UPDATE_SECTIONS, FIND_TASKS, UPDATE_TASKS, DELETE_OBJECT } = ToolNames;
6
6
  const ArgsSchema = {
7
- projectId: z.string().min(1).describe('The ID of the project to search sections in.'),
7
+ projectId: z
8
+ .string()
9
+ .min(1)
10
+ .describe('The ID of the project to search sections in. Project ID should be an ID string, or the text "inbox", for inbox tasks.'),
8
11
  search: z
9
12
  .string()
10
13
  .optional()
@@ -15,8 +18,10 @@ const findSections = {
15
18
  description: 'Search for sections by name or other criteria in a project.',
16
19
  parameters: ArgsSchema,
17
20
  async execute(args, client) {
21
+ // Resolve "inbox" to actual inbox project ID if needed
22
+ const resolvedProjectId = args.projectId === 'inbox' ? (await client.getUser()).inboxProjectId : args.projectId;
18
23
  const { results } = await client.getSections({
19
- projectId: args.projectId,
24
+ projectId: resolvedProjectId,
20
25
  });
21
26
  const searchLower = args.search ? args.search.toLowerCase() : undefined;
22
27
  const filtered = searchLower
@@ -1 +1 @@
1
- {"version":3,"file":"find-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAsDvB,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqL2B,CAAA;AA2K1C,OAAO,EAAE,SAAS,EAAE,CAAA"}
1
+ {"version":3,"file":"find-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA2DvB,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwL2B,CAAA;AA2K1C,OAAO,EAAE,SAAS,EAAE,CAAA"}
@@ -9,7 +9,10 @@ import { ToolNames } from '../utils/tool-names.js';
9
9
  const { FIND_COMPLETED_TASKS, ADD_TASKS } = ToolNames;
10
10
  const ArgsSchema = {
11
11
  searchText: z.string().optional().describe('The text to search for in tasks.'),
12
- projectId: z.string().optional().describe('Find tasks in this project.'),
12
+ projectId: z
13
+ .string()
14
+ .optional()
15
+ .describe('Find tasks in this project. Project ID should be an ID string, or the text "inbox", for inbox tasks.'),
13
16
  sectionId: z.string().optional().describe('Find tasks in this section.'),
14
17
  parentId: z.string().optional().describe('Find subtasks of this parent task.'),
15
18
  responsibleUser: z
@@ -60,8 +63,10 @@ const findTasks = {
60
63
  limit,
61
64
  cursor: cursor ?? null,
62
65
  };
63
- if (projectId)
64
- taskParams.projectId = projectId;
66
+ if (projectId) {
67
+ taskParams.projectId =
68
+ projectId === 'inbox' ? todoistUser.inboxProjectId : projectId;
69
+ }
65
70
  if (sectionId)
66
71
  taskParams.sectionId = sectionId;
67
72
  if (parentId)
@@ -6,7 +6,6 @@ declare const updateProjects: {
6
6
  projects: z.ZodArray<z.ZodObject<{
7
7
  id: z.ZodString;
8
8
  name: z.ZodOptional<z.ZodString>;
9
- parentId: z.ZodOptional<z.ZodString>;
10
9
  isFavorite: z.ZodOptional<z.ZodBoolean>;
11
10
  viewStyle: z.ZodOptional<z.ZodEnum<["list", "board", "calendar"]>>;
12
11
  }, "strip", z.ZodTypeAny, {
@@ -14,13 +13,11 @@ declare const updateProjects: {
14
13
  name?: string | undefined;
15
14
  isFavorite?: boolean | undefined;
16
15
  viewStyle?: "list" | "board" | "calendar" | undefined;
17
- parentId?: string | undefined;
18
16
  }, {
19
17
  id: string;
20
18
  name?: string | undefined;
21
19
  isFavorite?: boolean | undefined;
22
20
  viewStyle?: "list" | "board" | "calendar" | undefined;
23
- parentId?: string | undefined;
24
21
  }>, "many">;
25
22
  };
26
23
  execute(args: {
@@ -29,7 +26,6 @@ declare const updateProjects: {
29
26
  name?: string | undefined;
30
27
  isFavorite?: boolean | undefined;
31
28
  viewStyle?: "list" | "board" | "calendar" | undefined;
32
- parentId?: string | undefined;
33
29
  }[];
34
30
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
35
31
  content: {
@@ -1 +1 @@
1
- {"version":3,"file":"update-projects.d.ts","sourceRoot":"","sources":["../../src/tools/update-projects.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA2BvB,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCsB,CAAA;AAuD1C,OAAO,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"update-projects.d.ts","sourceRoot":"","sources":["../../src/tools/update-projects.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAqBvB,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCsB,CAAA;AAuD1C,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -6,10 +6,6 @@ const { FIND_PROJECTS, FIND_TASKS, GET_OVERVIEW } = ToolNames;
6
6
  const ProjectUpdateSchema = z.object({
7
7
  id: z.string().min(1).describe('The ID of the project to update.'),
8
8
  name: z.string().min(1).optional().describe('The new name of the project.'),
9
- parentId: z
10
- .string()
11
- .optional()
12
- .describe('The ID of the parent project. If provided, moves this project to be a sub-project.'),
13
9
  isFavorite: z.boolean().optional().describe('Whether the project is a favorite.'),
14
10
  viewStyle: z.enum(['list', 'board', 'calendar']).optional().describe('The project view style.'),
15
11
  });
@@ -1 +1 @@
1
- {"version":3,"file":"update-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/update-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAmEvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8HyB,CAAA;AAqC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"update-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/update-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAwEvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqIyB,CAAA;AAqC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -17,7 +17,10 @@ const TasksUpdateSchema = z.object({
17
17
  .string()
18
18
  .optional()
19
19
  .describe('New additional details, notes, or context for the task. Use this for longer content rather than putting it in the task name. Supports Markdown.'),
20
- projectId: z.string().optional().describe('The new project ID for the task.'),
20
+ projectId: z
21
+ .string()
22
+ .optional()
23
+ .describe('The new project ID for the task. Project ID should be an ID string, or the text "inbox", for inbox tasks.'),
21
24
  sectionId: z.string().optional().describe('The new section ID for the task.'),
22
25
  parentId: z.string().optional().describe('The new parent task ID (for subtasks).'),
23
26
  order: z.number().optional().describe('The new order of the task within its parent/section.'),
@@ -57,6 +60,12 @@ const updateTasks = {
57
60
  return undefined;
58
61
  }
59
62
  const { id, projectId, sectionId, parentId, duration: durationStr, responsibleUser, priority, labels, deadlineDate, ...otherUpdateArgs } = task;
63
+ // Resolve "inbox" to actual inbox project ID if needed
64
+ let resolvedProjectId = projectId;
65
+ if (projectId === 'inbox') {
66
+ const todoistUser = await client.getUser();
67
+ resolvedProjectId = todoistUser.inboxProjectId;
68
+ }
60
69
  let updateArgs = {
61
70
  ...otherUpdateArgs,
62
71
  ...(labels !== undefined && { labels }),
@@ -112,10 +121,10 @@ const updateTasks = {
112
121
  }
113
122
  }
114
123
  // If no move parameters are provided, use updateTask without moveTask
115
- if (!projectId && !sectionId && !parentId) {
124
+ if (!resolvedProjectId && !sectionId && !parentId) {
116
125
  return await client.updateTask(id, updateArgs);
117
126
  }
118
- const moveArgs = createMoveTaskArgs(id, projectId, sectionId, parentId);
127
+ const moveArgs = createMoveTaskArgs(id, resolvedProjectId, sectionId, parentId);
119
128
  const movedTask = await client.moveTask(id, moveArgs);
120
129
  if (Object.keys(updateArgs).length > 0) {
121
130
  return await client.updateTask(id, updateArgs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-ai",
3
- "version": "4.15.0",
3
+ "version": "4.16.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -40,19 +40,20 @@
40
40
  "lint:write": "biome lint --write",
41
41
  "format:check": "biome format",
42
42
  "format:write": "biome format --write",
43
- "check": "biome check",
43
+ "lint:schemas": "npm run build && npx tsc scripts/validate-schemas.ts --outDir dist/scripts --moduleResolution node --module ESNext --target es2021 --esModuleInterop --skipLibCheck --declaration false && node dist/scripts/validate-schemas.js",
44
+ "check": "biome check && npm run lint:schemas",
44
45
  "check:fix": "biome check --fix --unsafe",
45
46
  "prepare": "husky"
46
47
  },
47
48
  "dependencies": {
49
+ "@doist/todoist-api-typescript": "6.0.0",
48
50
  "@modelcontextprotocol/sdk": "^1.11.1",
49
51
  "date-fns": "^4.1.0",
50
- "@doist/todoist-api-typescript": "5.7.1",
51
52
  "dotenv": "^17.0.0",
52
53
  "zod": "^3.25.7"
53
54
  },
54
55
  "devDependencies": {
55
- "@biomejs/biome": "2.2.6",
56
+ "@biomejs/biome": "2.3.2",
56
57
  "@types/express": "^5.0.2",
57
58
  "@types/jest": "30.0.0",
58
59
  "@types/morgan": "^1.9.9",
@@ -71,6 +72,15 @@
71
72
  "lint-staged": {
72
73
  "*": [
73
74
  "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
75
+ ],
76
+ "src/tools/*.ts": [
77
+ "npm run lint:schemas"
78
+ ],
79
+ "src/index.ts": [
80
+ "npm run lint:schemas"
81
+ ],
82
+ "scripts/validate-schemas.ts": [
83
+ "npm run lint:schemas"
74
84
  ]
75
85
  }
76
86
  }
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Schema Validation Script for Todoist AI MCP Server
5
+ *
6
+ * This script validates that all tool parameter schemas follow Gemini API compatibility rules.
7
+ * Specifically, it checks that no Zod string schemas use both .nullable() and .optional().
8
+ *
9
+ * This version imports the actual tools from the compiled index.js and validates their
10
+ * runtime Zod schemas for maximum accuracy.
11
+ *
12
+ * Usage:
13
+ * npm run build && node scripts/validate-schemas.js
14
+ * npm run build && node scripts/validate-schemas.js --verbose
15
+ * npm run build && node scripts/validate-schemas.js --json
16
+ */
17
+
18
+ import { z } from 'zod'
19
+
20
+ interface ValidationIssue {
21
+ toolName: string
22
+ parameterPath: string
23
+ issue: string
24
+ suggestion: string
25
+ }
26
+
27
+ interface ValidationResult {
28
+ success: boolean
29
+ issues: ValidationIssue[]
30
+ toolsChecked: number
31
+ parametersChecked: number
32
+ }
33
+
34
+ /**
35
+ * Recursively walk a Zod schema and detect problematic patterns
36
+ */
37
+ function walkZodSchema(
38
+ schema: z.ZodTypeAny,
39
+ path: string,
40
+ issues: ValidationIssue[],
41
+ toolName: string,
42
+ ): void {
43
+ const typeName = schema._def.typeName
44
+
45
+ // Check for ZodOptional containing a ZodNullable ZodString
46
+ if (typeName === 'ZodOptional') {
47
+ const innerSchema = schema._def.innerType
48
+ if (innerSchema._def.typeName === 'ZodNullable') {
49
+ const nullableInner = innerSchema._def.innerType
50
+ if (nullableInner._def.typeName === 'ZodString') {
51
+ issues.push({
52
+ toolName,
53
+ parameterPath: path,
54
+ issue: 'GEMINI_API_INCOMPATIBLE: z.string().nullable().optional() pattern detected',
55
+ suggestion:
56
+ 'REQUIRED FIX: Change "z.string().nullable().optional()" to "z.string().optional()" and use special strings like "remove" or "unassign" in description to handle clearing. This pattern causes HTTP 400 errors in Google Gemini API due to OpenAPI 3.1 nullable type incompatibility.',
57
+ })
58
+ }
59
+ }
60
+ }
61
+
62
+ // Check for ZodNullable containing a ZodOptional ZodString
63
+ if (typeName === 'ZodNullable') {
64
+ const innerSchema = schema._def.innerType
65
+ if (innerSchema._def.typeName === 'ZodOptional') {
66
+ const optionalInner = innerSchema._def.innerType
67
+ if (optionalInner._def.typeName === 'ZodString') {
68
+ issues.push({
69
+ toolName,
70
+ parameterPath: path,
71
+ issue: 'GEMINI_API_INCOMPATIBLE: z.string().optional().nullable() pattern detected',
72
+ suggestion:
73
+ 'REQUIRED FIX: Change "z.string().optional().nullable()" to "z.string().optional()" and use special strings like "remove" or "unassign" in description to handle clearing. This pattern causes HTTP 400 errors in Google Gemini API due to OpenAPI 3.1 nullable type incompatibility.',
74
+ })
75
+ }
76
+ }
77
+ }
78
+
79
+ // Recursively check nested schemas
80
+ switch (typeName) {
81
+ case 'ZodObject': {
82
+ const shape = schema._def.shape()
83
+ for (const [key, value] of Object.entries(shape)) {
84
+ const newPath = path ? `${path}.${key}` : key
85
+ walkZodSchema(value as z.ZodTypeAny, newPath, issues, toolName)
86
+ }
87
+ break
88
+ }
89
+ case 'ZodArray':
90
+ walkZodSchema(schema._def.type, `${path}[]`, issues, toolName)
91
+ break
92
+
93
+ case 'ZodOptional':
94
+ case 'ZodNullable':
95
+ case 'ZodDefault':
96
+ walkZodSchema(schema._def.innerType, path, issues, toolName)
97
+ break
98
+
99
+ case 'ZodUnion':
100
+ case 'ZodDiscriminatedUnion': {
101
+ const options = schema._def.options || schema._def.discriminatedUnion
102
+ if (Array.isArray(options)) {
103
+ options.forEach((option: z.ZodTypeAny, index: number) => {
104
+ walkZodSchema(option, `${path}[union:${index}]`, issues, toolName)
105
+ })
106
+ }
107
+ break
108
+ }
109
+ case 'ZodIntersection':
110
+ walkZodSchema(schema._def.left, `${path}[left]`, issues, toolName)
111
+ walkZodSchema(schema._def.right, `${path}[right]`, issues, toolName)
112
+ break
113
+
114
+ case 'ZodRecord':
115
+ if (schema._def.valueType) {
116
+ walkZodSchema(schema._def.valueType, `${path}[value]`, issues, toolName)
117
+ }
118
+ break
119
+
120
+ case 'ZodTuple':
121
+ if (schema._def.items) {
122
+ schema._def.items.forEach((item: z.ZodTypeAny, index: number) => {
123
+ walkZodSchema(item, `${path}[${index}]`, issues, toolName)
124
+ })
125
+ }
126
+ break
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Validate a single tool's parameter schema
132
+ */
133
+ // biome-ignore lint/suspicious/noExplicitAny: this is a tooling script
134
+ function validateToolSchema(tool: any): ValidationIssue[] {
135
+ const issues: ValidationIssue[] = []
136
+ const toolName = tool.name || 'unknown'
137
+
138
+ if (!tool.parameters) {
139
+ return issues
140
+ }
141
+
142
+ try {
143
+ const schema = z.object(tool.parameters)
144
+ walkZodSchema(schema, '', issues, toolName)
145
+ } catch (error) {
146
+ issues.push({
147
+ toolName,
148
+ parameterPath: 'root',
149
+ issue: `Failed to analyze schema: ${error}`,
150
+ suggestion: 'Check that the tool parameters are valid Zod schemas',
151
+ })
152
+ }
153
+
154
+ return issues
155
+ }
156
+
157
+ /**
158
+ * Main validation function using runtime schema analysis
159
+ */
160
+ async function validateAllSchemas(verbose: boolean = false): Promise<ValidationResult> {
161
+ try {
162
+ const { tools } = await import(`${process.cwd()}/dist/index.js`)
163
+
164
+ const allIssues: ValidationIssue[] = []
165
+ let totalParameters = 0
166
+ const toolNames = Object.keys(tools)
167
+
168
+ for (const toolName of toolNames) {
169
+ const tool = tools[toolName]
170
+ const toolIssues = validateToolSchema(tool)
171
+ allIssues.push(...toolIssues)
172
+
173
+ // Count parameters for stats
174
+ if (tool.parameters) {
175
+ try {
176
+ const schema = z.object(tool.parameters)
177
+ const shape = schema._def.shape()
178
+ totalParameters += Object.keys(shape).length
179
+ } catch {
180
+ // Skip counting if schema is invalid
181
+ }
182
+ }
183
+
184
+ if (verbose) {
185
+ const issueCount = toolIssues.length
186
+ const status = issueCount === 0 ? '✅' : `❌ (${issueCount} issues)`
187
+ const paramCount = tool.parameters ? Object.keys(tool.parameters).length : 0
188
+ console.log(`${status} ${toolName} (${paramCount} parameters)`)
189
+
190
+ if (issueCount > 0) {
191
+ toolIssues.forEach((issue) => {
192
+ console.log(` ${issue.parameterPath}: ${issue.issue}`)
193
+ })
194
+ }
195
+ }
196
+ }
197
+
198
+ return {
199
+ success: allIssues.length === 0,
200
+ issues: allIssues,
201
+ toolsChecked: toolNames.length,
202
+ parametersChecked: totalParameters,
203
+ }
204
+ } catch (error) {
205
+ return {
206
+ success: false,
207
+ issues: [
208
+ {
209
+ toolName: 'system',
210
+ parameterPath: 'import',
211
+ issue: `Failed to import tools: ${error}`,
212
+ suggestion: 'Ensure the project is built and dist/index.js exists',
213
+ },
214
+ ],
215
+ toolsChecked: 0,
216
+ parametersChecked: 0,
217
+ }
218
+ }
219
+ }
220
+
221
+ /**
222
+ * CLI interface
223
+ */
224
+ async function main() {
225
+ const args = process.argv.slice(2)
226
+ const verbose = args.includes('--verbose')
227
+ const jsonOutput = args.includes('--json')
228
+
229
+ try {
230
+ const result = await validateAllSchemas(verbose)
231
+
232
+ if (jsonOutput) {
233
+ console.log(JSON.stringify(result, null, 2))
234
+ } else {
235
+ if (result.success) {
236
+ console.log('✅ Schema validation passed!')
237
+ console.log(
238
+ ` Checked ${result.toolsChecked} tools with ${result.parametersChecked} parameters`,
239
+ )
240
+ console.log(
241
+ ' All schemas are Gemini API compatible (no .nullable() on optional strings)',
242
+ )
243
+ } else {
244
+ console.log('❌ Schema validation failed!')
245
+ console.log(
246
+ ` Found ${result.issues.length} issue(s) in ${result.toolsChecked} tools:\n`,
247
+ )
248
+
249
+ result.issues.forEach((issue, index) => {
250
+ console.log(`\n${index + 1}. 🚫 VALIDATION FAILURE`)
251
+ console.log(` Tool: ${issue.toolName}`)
252
+ console.log(` Parameter: ${issue.parameterPath}`)
253
+ console.log(` Issue: ${issue.issue}`)
254
+ console.log(` Action Required: ${issue.suggestion}`)
255
+ console.log(` File Location: src/tools/${issue.toolName}.ts`)
256
+ console.log(` Fix Pattern: Remove .nullable() from the parameter schema`)
257
+ console.log(
258
+ ` Example Fix: Change z.string().nullable().optional() → z.string().optional()`,
259
+ )
260
+ console.log(
261
+ ` ⚠️ This validation failure will cause Gemini API HTTP 400 errors\n`,
262
+ )
263
+ })
264
+ }
265
+ }
266
+
267
+ process.exit(result.success ? 0 : 1)
268
+ } catch (error) {
269
+ console.error('Fatal error during schema validation:', error)
270
+ process.exit(1)
271
+ }
272
+ }
273
+
274
+ // Run if this script is executed directly
275
+ if (process.argv[1]?.endsWith('validate-schemas.js')) {
276
+ main()
277
+ }
278
+
279
+ export type { ValidationResult, ValidationIssue }
280
+ export { validateAllSchemas }