@doist/todoist-ai 4.9.3 → 4.10.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.
Files changed (54) hide show
  1. package/README.md +11 -0
  2. package/dist/index.d.ts +37 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +8 -1
  5. package/dist/mcp-helpers.d.ts +8 -1
  6. package/dist/mcp-helpers.d.ts.map +1 -1
  7. package/dist/mcp-helpers.js +6 -3
  8. package/dist/mcp-server.d.ts.map +1 -1
  9. package/dist/mcp-server.js +5 -0
  10. package/dist/tool-helpers.d.ts +8 -1
  11. package/dist/tool-helpers.d.ts.map +1 -1
  12. package/dist/tool-helpers.js +13 -10
  13. package/dist/tools/__tests__/add-projects.test.js +5 -1
  14. package/dist/tools/__tests__/add-sections.test.js +5 -1
  15. package/dist/tools/__tests__/assignment-integration.test.js +1 -1
  16. package/dist/tools/__tests__/complete-tasks.test.js +6 -6
  17. package/dist/tools/__tests__/fetch.test.d.ts +2 -0
  18. package/dist/tools/__tests__/fetch.test.d.ts.map +1 -0
  19. package/dist/tools/__tests__/fetch.test.js +275 -0
  20. package/dist/tools/__tests__/find-comments.test.js +3 -9
  21. package/dist/tools/__tests__/find-projects.test.js +0 -2
  22. package/dist/tools/__tests__/find-sections.test.js +1 -5
  23. package/dist/tools/__tests__/find-tasks.test.js +3 -5
  24. package/dist/tools/__tests__/get-overview.test.js +7 -9
  25. package/dist/tools/__tests__/search.test.d.ts +2 -0
  26. package/dist/tools/__tests__/search.test.d.ts.map +1 -0
  27. package/dist/tools/__tests__/search.test.js +208 -0
  28. package/dist/tools/__tests__/update-comments.test.js +0 -2
  29. package/dist/tools/__tests__/update-projects.test.js +14 -2
  30. package/dist/tools/__tests__/update-sections.test.js +14 -2
  31. package/dist/tools/__tests__/update-tasks.test.js +1 -1
  32. package/dist/tools/add-tasks.d.ts.map +1 -1
  33. package/dist/tools/add-tasks.js +8 -2
  34. package/dist/tools/fetch.d.ts +26 -0
  35. package/dist/tools/fetch.d.ts.map +1 -0
  36. package/dist/tools/fetch.js +99 -0
  37. package/dist/tools/find-projects.d.ts +2 -2
  38. package/dist/tools/search.d.ts +26 -0
  39. package/dist/tools/search.d.ts.map +1 -0
  40. package/dist/tools/search.js +64 -0
  41. package/dist/tools/update-tasks.d.ts.map +1 -1
  42. package/dist/tools/update-tasks.js +8 -2
  43. package/dist/utils/constants.d.ts +1 -1
  44. package/dist/utils/constants.js +1 -1
  45. package/dist/utils/sanitize-data.d.ts +9 -0
  46. package/dist/utils/sanitize-data.d.ts.map +1 -0
  47. package/dist/utils/sanitize-data.js +37 -0
  48. package/dist/utils/sanitize-data.test.d.ts +2 -0
  49. package/dist/utils/sanitize-data.test.d.ts.map +1 -0
  50. package/dist/utils/sanitize-data.test.js +93 -0
  51. package/dist/utils/tool-names.d.ts +2 -0
  52. package/dist/utils/tool-names.d.ts.map +1 -1
  53. package/dist/utils/tool-names.js +3 -0
  54. package/package.json +2 -2
@@ -52,7 +52,6 @@ describe(`${UPDATE_COMMENTS} tool`, () => {
52
52
  id: '98765',
53
53
  content: 'Updated content here',
54
54
  taskId: 'task456',
55
- fileAttachment: null,
56
55
  }),
57
56
  ],
58
57
  totalCount: 1,
@@ -89,7 +88,6 @@ describe(`${UPDATE_COMMENTS} tool`, () => {
89
88
  content: 'Updated project comment',
90
89
  taskId: undefined,
91
90
  projectId: 'project789',
92
- fileAttachment: null,
93
91
  }),
94
92
  ],
95
93
  totalCount: 1,
@@ -47,7 +47,12 @@ describe(`${UPDATE_PROJECTS} tool`, () => {
47
47
  // Verify structured content
48
48
  const structuredContent = extractStructuredContent(result);
49
49
  expect(structuredContent).toEqual(expect.objectContaining({
50
- projects: [mockApiResponse],
50
+ projects: expect.arrayContaining([
51
+ expect.objectContaining({
52
+ id: 'existing-project-123',
53
+ name: 'Updated Project Name',
54
+ }),
55
+ ]),
51
56
  totalCount: 1,
52
57
  updatedProjectIds: ['existing-project-123'],
53
58
  appliedOperations: {
@@ -140,7 +145,14 @@ describe(`${UPDATE_PROJECTS} tool`, () => {
140
145
  // Verify structured content
141
146
  const structuredContent = extractStructuredContent(result);
142
147
  expect(structuredContent).toEqual(expect.objectContaining({
143
- projects: mockProjects,
148
+ projects: expect.arrayContaining([
149
+ expect.objectContaining({ id: 'project-1', name: 'Updated First Project' }),
150
+ expect.objectContaining({
151
+ id: 'project-2',
152
+ name: 'Updated Second Project',
153
+ }),
154
+ expect.objectContaining({ id: 'project-3', name: 'Updated Third Project' }),
155
+ ]),
144
156
  totalCount: 3,
145
157
  updatedProjectIds: ['project-1', 'project-2', 'project-3'],
146
158
  appliedOperations: {
@@ -40,7 +40,12 @@ describe(`${UPDATE_SECTIONS} tool`, () => {
40
40
  // Verify structured content
41
41
  const structuredContent = extractStructuredContent(result);
42
42
  expect(structuredContent).toEqual(expect.objectContaining({
43
- sections: [mockApiResponse],
43
+ sections: expect.arrayContaining([
44
+ expect.objectContaining({
45
+ id: 'existing-section-123',
46
+ name: 'Updated Section Name',
47
+ }),
48
+ ]),
44
49
  totalCount: 1,
45
50
  updatedSectionIds: ['existing-section-123'],
46
51
  }));
@@ -97,7 +102,14 @@ describe(`${UPDATE_SECTIONS} tool`, () => {
97
102
  // Verify structured content
98
103
  const structuredContent = extractStructuredContent(result);
99
104
  expect(structuredContent).toEqual(expect.objectContaining({
100
- sections: mockSections,
105
+ sections: expect.arrayContaining([
106
+ expect.objectContaining({ id: 'section-1', name: 'Updated First Section' }),
107
+ expect.objectContaining({
108
+ id: 'section-2',
109
+ name: 'Updated Second Section',
110
+ }),
111
+ expect.objectContaining({ id: 'section-3', name: 'Updated Third Section' }),
112
+ ]),
101
113
  totalCount: 3,
102
114
  updatedSectionIds: ['section-1', 'section-2', 'section-3'],
103
115
  }));
@@ -673,7 +673,7 @@ describe(`${UPDATE_TASKS} tool`, () => {
673
673
  const textContent = extractTextContent(result);
674
674
  expect(textContent).toContain('Updated 0 tasks');
675
675
  const structuredContent = extractStructuredContent(result);
676
- expect(structuredContent.tasks).toHaveLength(0);
676
+ expect(structuredContent.tasks).toBeUndefined(); // Empty arrays are removed
677
677
  expect(structuredContent.totalCount).toBe(0);
678
678
  });
679
679
  });
@@ -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;AA2CvB,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AAgI1C,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;AAqDvB,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AAgI1C,OAAO,EAAE,QAAQ,EAAE,CAAA"}
@@ -7,8 +7,14 @@ import { convertPriorityToNumber, PrioritySchema } from '../utils/priorities.js'
7
7
  import { generateTaskNextSteps, getDateString, summarizeTaskOperation, } from '../utils/response-builders.js';
8
8
  import { ToolNames } from '../utils/tool-names.js';
9
9
  const TaskSchema = z.object({
10
- content: z.string().min(1).describe('The content of the task to create.'),
11
- description: z.string().optional().describe('The description of the task.'),
10
+ content: z
11
+ .string()
12
+ .min(1)
13
+ .describe('The task name/title. Should be concise and actionable (e.g., "Review PR #123", "Call dentist"). For longer content, use the description field instead. Supports Markdown.'),
14
+ description: z
15
+ .string()
16
+ .optional()
17
+ .describe('Additional details, notes, or context for the task. Use this for longer content rather than putting it in the task name. Supports Markdown.'),
12
18
  priority: PrioritySchema.optional().describe('The priority of the task: p1 (highest), p2 (high), p3 (medium), p4 (lowest/default).'),
13
19
  dueString: z.string().optional().describe('The due date for the task, in natural language.'),
14
20
  duration: z
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ type FetchToolOutput = {
3
+ content: {
4
+ type: 'text';
5
+ text: string;
6
+ }[];
7
+ isError?: boolean;
8
+ };
9
+ /**
10
+ * OpenAI MCP fetch tool - retrieves the full contents of a task or project by ID.
11
+ *
12
+ * This tool follows the OpenAI MCP fetch tool specification:
13
+ * @see https://platform.openai.com/docs/mcp#fetch-tool
14
+ */
15
+ declare const fetch: {
16
+ name: "fetch";
17
+ description: string;
18
+ parameters: {
19
+ id: z.ZodString;
20
+ };
21
+ execute(args: {
22
+ id: string;
23
+ }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<FetchToolOutput>;
24
+ };
25
+ export { fetch };
26
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/tools/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAuBvB,KAAK,eAAe,GAAG;IACnB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzC,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,QAAA,MAAM,KAAK;;;;;;;;oEAKsB,OAAO,CAAC,eAAe,CAAC;CAsFf,CAAA;AAE1C,OAAO,EAAE,KAAK,EAAE,CAAA"}
@@ -0,0 +1,99 @@
1
+ import { z } from 'zod';
2
+ import { getErrorOutput } from '../mcp-helpers.js';
3
+ import { buildTodoistUrl, mapProject, mapTask } from '../tool-helpers.js';
4
+ import { ToolNames } from '../utils/tool-names.js';
5
+ const ArgsSchema = {
6
+ id: z
7
+ .string()
8
+ .min(1)
9
+ .describe('A unique identifier for the document in the format "task:{id}" or "project:{id}".'),
10
+ };
11
+ /**
12
+ * OpenAI MCP fetch tool - retrieves the full contents of a task or project by ID.
13
+ *
14
+ * This tool follows the OpenAI MCP fetch tool specification:
15
+ * @see https://platform.openai.com/docs/mcp#fetch-tool
16
+ */
17
+ const fetch = {
18
+ name: ToolNames.FETCH,
19
+ description: 'Fetch the full contents of a task or project by its ID. The ID should be in the format "task:{id}" or "project:{id}".',
20
+ parameters: ArgsSchema,
21
+ async execute(args, client) {
22
+ try {
23
+ const { id } = args;
24
+ // Parse the composite ID
25
+ const [type, objectId] = id.split(':', 2);
26
+ if (!objectId || (type !== 'task' && type !== 'project')) {
27
+ throw new Error('Invalid ID format. Expected "task:{id}" or "project:{id}". Example: "task:8485093748" or "project:6cfCcrrCFg2xP94Q"');
28
+ }
29
+ let result;
30
+ if (type === 'task') {
31
+ // Fetch task
32
+ const task = await client.getTask(objectId);
33
+ const mappedTask = mapTask(task);
34
+ // Build text content
35
+ const textParts = [mappedTask.content];
36
+ if (mappedTask.description) {
37
+ textParts.push(`\n\nDescription: ${mappedTask.description}`);
38
+ }
39
+ if (mappedTask.dueDate) {
40
+ textParts.push(`\nDue: ${mappedTask.dueDate}`);
41
+ }
42
+ if (mappedTask.labels.length > 0) {
43
+ textParts.push(`\nLabels: ${mappedTask.labels.join(', ')}`);
44
+ }
45
+ result = {
46
+ id: `task:${mappedTask.id}`,
47
+ title: mappedTask.content,
48
+ text: textParts.join(''),
49
+ url: buildTodoistUrl('task', mappedTask.id),
50
+ metadata: {
51
+ priority: mappedTask.priority,
52
+ projectId: mappedTask.projectId,
53
+ sectionId: mappedTask.sectionId,
54
+ parentId: mappedTask.parentId,
55
+ recurring: mappedTask.recurring,
56
+ duration: mappedTask.duration,
57
+ responsibleUid: mappedTask.responsibleUid,
58
+ assignedByUid: mappedTask.assignedByUid,
59
+ },
60
+ };
61
+ }
62
+ else {
63
+ // Fetch project
64
+ const project = await client.getProject(objectId);
65
+ const mappedProject = mapProject(project);
66
+ // Build text content
67
+ const textParts = [mappedProject.name];
68
+ if (mappedProject.isShared) {
69
+ textParts.push('\n\nShared project');
70
+ }
71
+ if (mappedProject.isFavorite) {
72
+ textParts.push('\nFavorite: Yes');
73
+ }
74
+ result = {
75
+ id: `project:${mappedProject.id}`,
76
+ title: mappedProject.name,
77
+ text: textParts.join(''),
78
+ url: buildTodoistUrl('project', mappedProject.id),
79
+ metadata: {
80
+ color: mappedProject.color,
81
+ isFavorite: mappedProject.isFavorite,
82
+ isShared: mappedProject.isShared,
83
+ parentId: mappedProject.parentId,
84
+ inboxProject: mappedProject.inboxProject,
85
+ viewStyle: mappedProject.viewStyle,
86
+ },
87
+ };
88
+ }
89
+ // Return as JSON-encoded string in a text content item (OpenAI MCP spec)
90
+ const jsonText = JSON.stringify(result);
91
+ return { content: [{ type: 'text', text: jsonText }] };
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : 'An unknown error occurred';
95
+ return getErrorOutput(message);
96
+ }
97
+ },
98
+ };
99
+ export { fetch };
@@ -9,8 +9,8 @@ declare const findProjects: {
9
9
  };
10
10
  execute(args: {
11
11
  limit: number;
12
- cursor?: string | undefined;
13
12
  search?: string | undefined;
13
+ cursor?: string | undefined;
14
14
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
15
15
  content: {
16
16
  type: "text";
@@ -32,8 +32,8 @@ declare const findProjects: {
32
32
  hasMore: boolean;
33
33
  appliedFilters: {
34
34
  limit: number;
35
- cursor?: string | undefined;
36
35
  search?: string | undefined;
36
+ cursor?: string | undefined;
37
37
  };
38
38
  };
39
39
  } | {
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ type SearchToolOutput = {
3
+ content: {
4
+ type: 'text';
5
+ text: string;
6
+ }[];
7
+ isError?: boolean;
8
+ };
9
+ /**
10
+ * OpenAI MCP search tool - returns a list of relevant search results from Todoist.
11
+ *
12
+ * This tool follows the OpenAI MCP search tool specification:
13
+ * @see https://platform.openai.com/docs/mcp#search-tool
14
+ */
15
+ declare const search: {
16
+ name: "search";
17
+ description: string;
18
+ parameters: {
19
+ query: z.ZodString;
20
+ };
21
+ execute(args: {
22
+ query: string;
23
+ }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<SearchToolOutput>;
24
+ };
25
+ export { search };
26
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,KAAK,gBAAgB,GAAG;IACpB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzC,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,QAAA,MAAM,MAAM;;;;;;;;oEAKqB,OAAO,CAAC,gBAAgB,CAAC;CAmDhB,CAAA;AAE1C,OAAO,EAAE,MAAM,EAAE,CAAA"}
@@ -0,0 +1,64 @@
1
+ import { z } from 'zod';
2
+ import { getErrorOutput } from '../mcp-helpers.js';
3
+ import { buildTodoistUrl, getTasksByFilter } from '../tool-helpers.js';
4
+ import { ApiLimits } from '../utils/constants.js';
5
+ import { ToolNames } from '../utils/tool-names.js';
6
+ const ArgsSchema = {
7
+ query: z.string().min(1).describe('The search query string to find tasks and projects.'),
8
+ };
9
+ /**
10
+ * OpenAI MCP search tool - returns a list of relevant search results from Todoist.
11
+ *
12
+ * This tool follows the OpenAI MCP search tool specification:
13
+ * @see https://platform.openai.com/docs/mcp#search-tool
14
+ */
15
+ const search = {
16
+ name: ToolNames.SEARCH,
17
+ description: 'Search across tasks and projects in Todoist. Returns a list of relevant results with IDs, titles, and URLs.',
18
+ parameters: ArgsSchema,
19
+ async execute(args, client) {
20
+ try {
21
+ const { query } = args;
22
+ // Search both tasks and projects in parallel
23
+ // Use TASKS_MAX for search since this tool doesn't support pagination
24
+ const [tasksResult, projectsResponse] = await Promise.all([
25
+ getTasksByFilter({
26
+ client,
27
+ query: `search: ${query}`,
28
+ limit: ApiLimits.TASKS_MAX,
29
+ cursor: undefined,
30
+ }),
31
+ client.getProjects({ limit: ApiLimits.PROJECTS_MAX }),
32
+ ]);
33
+ // Filter projects by search query (case-insensitive)
34
+ const searchLower = query.toLowerCase();
35
+ const matchingProjects = projectsResponse.results.filter((project) => project.name.toLowerCase().includes(searchLower));
36
+ // Build results array
37
+ const results = [];
38
+ // Add task results with composite IDs
39
+ for (const task of tasksResult.tasks) {
40
+ results.push({
41
+ id: `task:${task.id}`,
42
+ title: task.content,
43
+ url: buildTodoistUrl('task', task.id),
44
+ });
45
+ }
46
+ // Add project results with composite IDs
47
+ for (const project of matchingProjects) {
48
+ results.push({
49
+ id: `project:${project.id}`,
50
+ title: project.name,
51
+ url: buildTodoistUrl('project', project.id),
52
+ });
53
+ }
54
+ // Return as JSON-encoded string in a text content item (OpenAI MCP spec)
55
+ const jsonText = JSON.stringify({ results });
56
+ return { content: [{ type: 'text', text: jsonText }] };
57
+ }
58
+ catch (error) {
59
+ const message = error instanceof Error ? error.message : 'An unknown error occurred';
60
+ return getErrorOutput(message);
61
+ }
62
+ },
63
+ };
64
+ export { search };
@@ -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;AAoDvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkHyB,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;AA8DvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkHyB,CAAA;AAqC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -9,8 +9,14 @@ import { ToolNames } from '../utils/tool-names.js';
9
9
  const { FIND_TASKS_BY_DATE, GET_OVERVIEW } = ToolNames;
10
10
  const TasksUpdateSchema = z.object({
11
11
  id: z.string().min(1).describe('The ID of the task to update.'),
12
- content: z.string().optional().describe('The new content of the task.'),
13
- description: z.string().optional().describe('The new description of the task.'),
12
+ content: z
13
+ .string()
14
+ .optional()
15
+ .describe('The new task name/title. Should be concise and actionable (e.g., "Review PR #123", "Call dentist"). For longer content, use the description field instead. Supports Markdown.'),
16
+ description: z
17
+ .string()
18
+ .optional()
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.'),
14
20
  projectId: z.string().optional().describe('The new project ID for the task.'),
15
21
  sectionId: z.string().optional().describe('The new section ID for the task.'),
16
22
  parentId: z.string().optional().describe('The new parent task ID (for subtasks).'),
@@ -8,7 +8,7 @@ export declare const ApiLimits: {
8
8
  /** Default limit for task listings */
9
9
  readonly TASKS_DEFAULT: 10;
10
10
  /** Maximum limit for task search and list operations */
11
- readonly TASKS_MAX: 50;
11
+ readonly TASKS_MAX: 100;
12
12
  /** Default limit for completed tasks */
13
13
  readonly COMPLETED_TASKS_DEFAULT: 50;
14
14
  /** Maximum limit for completed tasks */
@@ -9,7 +9,7 @@ export const ApiLimits = {
9
9
  /** Default limit for task listings */
10
10
  TASKS_DEFAULT: 10,
11
11
  /** Maximum limit for task search and list operations */
12
- TASKS_MAX: 50,
12
+ TASKS_MAX: 100,
13
13
  /** Default limit for completed tasks */
14
14
  COMPLETED_TASKS_DEFAULT: 50,
15
15
  /** Maximum limit for completed tasks */
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Removes all null fields, empty objects, and empty arrays from an object recursively.
3
+ * This ensures that data sent to agents doesn't include unnecessary empty values.
4
+ *
5
+ * @param obj - The object to sanitize
6
+ * @returns A new object with all null fields, empty objects, and empty arrays removed
7
+ */
8
+ export declare function removeNullFields<T>(obj: T): T;
9
+ //# sourceMappingURL=sanitize-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-data.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize-data.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAqC7C"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Removes all null fields, empty objects, and empty arrays from an object recursively.
3
+ * This ensures that data sent to agents doesn't include unnecessary empty values.
4
+ *
5
+ * @param obj - The object to sanitize
6
+ * @returns A new object with all null fields, empty objects, and empty arrays removed
7
+ */
8
+ export function removeNullFields(obj) {
9
+ if (obj === null || obj === undefined) {
10
+ return obj;
11
+ }
12
+ if (Array.isArray(obj)) {
13
+ return obj.map((item) => removeNullFields(item));
14
+ }
15
+ if (typeof obj === 'object') {
16
+ const sanitized = {};
17
+ for (const [key, value] of Object.entries(obj)) {
18
+ if (value !== null) {
19
+ const cleanedValue = removeNullFields(value);
20
+ // Skip empty arrays
21
+ if (Array.isArray(cleanedValue) && cleanedValue.length === 0) {
22
+ continue;
23
+ }
24
+ // Skip empty objects
25
+ if (cleanedValue !== null &&
26
+ typeof cleanedValue === 'object' &&
27
+ !Array.isArray(cleanedValue) &&
28
+ Object.keys(cleanedValue).length === 0) {
29
+ continue;
30
+ }
31
+ sanitized[key] = cleanedValue;
32
+ }
33
+ }
34
+ return sanitized;
35
+ }
36
+ return obj;
37
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sanitize-data.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-data.test.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize-data.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,93 @@
1
+ import { removeNullFields } from './sanitize-data.js';
2
+ describe('removeNullFields', () => {
3
+ it('should remove null fields from objects including nested objects', () => {
4
+ const input = {
5
+ name: 'John',
6
+ age: null,
7
+ email: 'john@example.com',
8
+ phone: null,
9
+ address: {
10
+ street: '123 Main St',
11
+ city: null,
12
+ country: 'USA',
13
+ },
14
+ };
15
+ const result = removeNullFields(input);
16
+ expect(result).toEqual({
17
+ name: 'John',
18
+ email: 'john@example.com',
19
+ address: {
20
+ street: '123 Main St',
21
+ country: 'USA',
22
+ },
23
+ });
24
+ });
25
+ it('should handle arrays with null values', () => {
26
+ const input = {
27
+ items: [
28
+ { id: 1, value: 'test' },
29
+ { id: 2, value: null },
30
+ { id: 3, value: 'another' },
31
+ ],
32
+ };
33
+ const result = removeNullFields(input);
34
+ expect(result).toEqual({
35
+ items: [{ id: 1, value: 'test' }, { id: 2 }, { id: 3, value: 'another' }],
36
+ });
37
+ });
38
+ it('should handle null objects', () => {
39
+ const result = removeNullFields(null);
40
+ expect(result).toBeNull();
41
+ });
42
+ it('should remove empty objects and empty arrays', () => {
43
+ const input = {
44
+ something: 'hello',
45
+ another: {},
46
+ yetAnother: [],
47
+ };
48
+ const result = removeNullFields(input);
49
+ expect(result).toEqual({
50
+ something: 'hello',
51
+ });
52
+ });
53
+ it('should remove empty objects and empty arrays in nested structures', () => {
54
+ const input = {
55
+ name: 'Test',
56
+ metadata: {},
57
+ tags: [],
58
+ nested: {
59
+ data: 'value',
60
+ emptyObj: {},
61
+ emptyArr: [],
62
+ deepNested: {
63
+ anotherEmpty: {},
64
+ },
65
+ },
66
+ items: [
67
+ { id: 1, data: 'test', empty: {} },
68
+ { id: 2, list: [] },
69
+ ],
70
+ };
71
+ const result = removeNullFields(input);
72
+ expect(result).toEqual({
73
+ name: 'Test',
74
+ nested: {
75
+ data: 'value',
76
+ },
77
+ items: [{ id: 1, data: 'test' }, { id: 2 }],
78
+ });
79
+ });
80
+ it('should keep arrays with values and objects with properties', () => {
81
+ const input = {
82
+ emptyArray: [],
83
+ arrayWithValues: [1, 2, 3],
84
+ emptyObject: {},
85
+ objectWithProps: { key: 'value' },
86
+ };
87
+ const result = removeNullFields(input);
88
+ expect(result).toEqual({
89
+ arrayWithValues: [1, 2, 3],
90
+ objectWithProps: { key: 'value' },
91
+ });
92
+ });
93
+ });
@@ -26,6 +26,8 @@ export declare const ToolNames: {
26
26
  readonly GET_OVERVIEW: "get-overview";
27
27
  readonly DELETE_OBJECT: "delete-object";
28
28
  readonly USER_INFO: "user-info";
29
+ readonly SEARCH: "search";
30
+ readonly FETCH: "fetch";
29
31
  };
30
32
  export type ToolName = (typeof ToolNames)[keyof typeof ToolNames];
31
33
  //# sourceMappingURL=tool-names.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-names.d.ts","sourceRoot":"","sources":["../../src/utils/tool-names.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;CAgCZ,CAAA;AAGV,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAA"}
1
+ {"version":3,"file":"tool-names.d.ts","sourceRoot":"","sources":["../../src/utils/tool-names.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;CAoCZ,CAAA;AAGV,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAA"}
@@ -32,4 +32,7 @@ export const ToolNames = {
32
32
  GET_OVERVIEW: 'get-overview',
33
33
  DELETE_OBJECT: 'delete-object',
34
34
  USER_INFO: 'user-info',
35
+ // OpenAI MCP tools
36
+ SEARCH: 'search',
37
+ FETCH: 'fetch',
35
38
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-ai",
3
- "version": "4.9.3",
3
+ "version": "4.10.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -52,7 +52,7 @@
52
52
  "zod": "^3.25.7"
53
53
  },
54
54
  "devDependencies": {
55
- "@biomejs/biome": "2.2.4",
55
+ "@biomejs/biome": "2.2.5",
56
56
  "@types/express": "^5.0.2",
57
57
  "@types/jest": "30.0.0",
58
58
  "@types/morgan": "^1.9.9",