@doist/todoist-ai 4.15.1 → 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.
@@ -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' },
@@ -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 {
@@ -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":"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.1",
3
+ "version": "4.16.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -46,9 +46,9 @@
46
46
  "prepare": "husky"
47
47
  },
48
48
  "dependencies": {
49
+ "@doist/todoist-api-typescript": "6.0.0",
49
50
  "@modelcontextprotocol/sdk": "^1.11.1",
50
51
  "date-fns": "^4.1.0",
51
- "@doist/todoist-api-typescript": "5.9.0",
52
52
  "dotenv": "^17.0.0",
53
53
  "zod": "^3.25.7"
54
54
  },