@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 +0 -4
- package/dist/index.d.ts.map +1 -1
- 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/__tests__/update-projects.test.js +0 -18
- 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-activity.js +1 -1
- 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/find-tasks.d.ts.map +1 -1
- package/dist/tools/find-tasks.js +8 -3
- package/dist/tools/update-projects.d.ts +0 -4
- package/dist/tools/update-projects.d.ts.map +1 -1
- package/dist/tools/update-projects.js +0 -4
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/tools/update-tasks.js +12 -3
- package/package.json +14 -4
- package/scripts/validate-schemas.ts +280 -0
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: {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAErD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAErD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAExC,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AAEpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAA;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,QAAA,MAAM,KAAK
|
|
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;
|
|
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":"find-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,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"}
|
package/dist/tools/find-tasks.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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;
|
|
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;
|
|
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",
|
|
@@ -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
|
-
"
|
|
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
|
|
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 }
|