@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.
- package/dist/tools/__tests__/add-tasks.test.js +74 -1
- package/dist/tools/__tests__/find-completed-tasks.test.js +100 -1
- package/dist/tools/__tests__/find-sections.test.js +69 -1
- package/dist/tools/add-comments.d.ts.map +1 -1
- package/dist/tools/add-comments.js +15 -5
- package/dist/tools/add-sections.d.ts.map +1 -1
- package/dist/tools/add-sections.js +15 -2
- package/dist/tools/add-tasks.d.ts.map +1 -1
- package/dist/tools/add-tasks.js +13 -4
- package/dist/tools/find-comments.d.ts.map +1 -1
- package/dist/tools/find-comments.js +8 -3
- package/dist/tools/find-completed-tasks.d.ts.map +1 -1
- package/dist/tools/find-completed-tasks.js +9 -2
- package/dist/tools/find-sections.d.ts.map +1 -1
- package/dist/tools/find-sections.js +7 -2
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/tools/update-tasks.js +12 -3
- package/package.json +2 -2
|
@@ -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;
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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;
|
|
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
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/tools/add-tasks.js
CHANGED
|
@@ -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
|
|
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 && !
|
|
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 =
|
|
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;
|
|
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
|
|
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 (
|
|
57
|
+
else if (resolvedProjectId) {
|
|
53
58
|
// Get comments by project
|
|
54
59
|
const response = await client.getComments({
|
|
55
|
-
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;
|
|
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
|
|
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;
|
|
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
|
|
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:
|
|
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;
|
|
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
|
|
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 (!
|
|
124
|
+
if (!resolvedProjectId && !sectionId && !parentId) {
|
|
116
125
|
return await client.updateTask(id, updateArgs);
|
|
117
126
|
}
|
|
118
|
-
const moveArgs = createMoveTaskArgs(id,
|
|
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.
|
|
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
|
},
|