@doist/todoist-ai 4.9.3 → 4.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,yCAAyC,CAAA;AACtF,OAAO,KAAK,EAAc,CAAC,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAkBpD;;;;;;;GAOG;AACH,iBAAS,aAAa,CAAC,iBAAiB,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtE,WAAW,EACX,iBAAiB,GACpB,EAAE;IACC,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,iBAAiB,CAAA;CACvC;;;;;;;;;;;;;;;;;EAeA;AASD;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,EAC9C,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,UAAU,QAqBrB;AAED,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA"}
1
+ {"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,yCAAyC,CAAA;AACtF,OAAO,KAAK,EAAc,CAAC,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmBpD;;;;;;;GAOG;AACH,iBAAS,aAAa,CAAC,iBAAiB,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtE,WAAW,EACX,iBAAiB,GACpB,EAAE;IACC,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,iBAAiB,CAAA;CACvC;;;;;;;;;;;;;;;;;EAkBA;AASD;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,EAC9C,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,UAAU,QAqBrB;AAED,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { removeNullFields } from './utils/sanitize-data.js';
1
2
  /**
2
3
  * Wether to return the structured content directly, vs. in the `content` part of the output.
3
4
  *
@@ -21,13 +22,15 @@ const USE_STRUCTURED_CONTENT = process.env.USE_STRUCTURED_CONTENT === 'true' ||
21
22
  * @see USE_STRUCTURED_CONTENT - Wether to use the structured content feature of the MCP protocol.
22
23
  */
23
24
  function getToolOutput({ textContent, structuredContent, }) {
25
+ // Remove null fields from structured content before returning
26
+ const sanitizedContent = removeNullFields(structuredContent);
24
27
  if (USE_STRUCTURED_CONTENT) {
25
28
  return {
26
29
  content: [{ type: 'text', text: textContent }],
27
- structuredContent,
30
+ structuredContent: sanitizedContent,
28
31
  };
29
32
  }
30
- const json = JSON.stringify(structuredContent);
33
+ const json = JSON.stringify(sanitizedContent);
31
34
  return {
32
35
  content: [
33
36
  { type: 'text', text: textContent },
@@ -143,7 +143,11 @@ describe(`${ADD_PROJECTS} tool`, () => {
143
143
  // Verify structured content
144
144
  const structuredContent = extractStructuredContent(result);
145
145
  expect(structuredContent).toEqual(expect.objectContaining({
146
- projects: mockProjects,
146
+ projects: expect.arrayContaining([
147
+ expect.objectContaining({ id: 'project-1', name: 'First Project' }),
148
+ expect.objectContaining({ id: 'project-2', name: 'Second Project' }),
149
+ expect.objectContaining({ id: 'project-3', name: 'Third Project' }),
150
+ ]),
147
151
  totalCount: 3,
148
152
  }));
149
153
  });
@@ -122,7 +122,11 @@ describe(`${ADD_SECTIONS} tool`, () => {
122
122
  // Verify structured content
123
123
  const structuredContent = extractStructuredContent(result);
124
124
  expect(structuredContent).toEqual(expect.objectContaining({
125
- sections: mockSections,
125
+ sections: expect.arrayContaining([
126
+ expect.objectContaining({ id: 'section-1', name: 'First Section' }),
127
+ expect.objectContaining({ id: 'section-2', name: 'Second Section' }),
128
+ expect.objectContaining({ id: 'section-3', name: 'Third Section' }),
129
+ ]),
126
130
  totalCount: 3,
127
131
  }));
128
132
  });
@@ -311,7 +311,7 @@ describe('Assignment Integration Tests', () => {
311
311
  projectId: 'project-123',
312
312
  }, mockTodoistApi);
313
313
  expect(extractTextContent(result)).toContain('is not shared and has no collaborators');
314
- expect(extractStructuredContent(result).collaborators).toHaveLength(0);
314
+ expect(extractStructuredContent(result).collaborators).toBeUndefined(); // Empty arrays are removed
315
315
  });
316
316
  it('should handle project not found', async () => {
317
317
  mockTodoistApi.getProject.mockRejectedValueOnce(new Error('Project not found'));
@@ -24,13 +24,13 @@ describe(`${COMPLETE_TASKS} tool`, () => {
24
24
  expect(extractTextContent(result)).toMatchSnapshot();
25
25
  // Verify structured content
26
26
  const { structuredContent } = result;
27
- expect(structuredContent).toEqual(expect.objectContaining({
27
+ expect(structuredContent).toEqual({
28
28
  completed: ['task-1', 'task-2', 'task-3'],
29
- failures: [],
29
+ // failures array is removed when empty
30
30
  totalRequested: 3,
31
31
  successCount: 3,
32
32
  failureCount: 0,
33
- }));
33
+ });
34
34
  });
35
35
  it('should complete single task', async () => {
36
36
  mockTodoistApi.closeTask.mockResolvedValue(true);
@@ -40,13 +40,13 @@ describe(`${COMPLETE_TASKS} tool`, () => {
40
40
  expect(extractTextContent(result)).toMatchSnapshot();
41
41
  // Verify structured content
42
42
  const { structuredContent } = result;
43
- expect(structuredContent).toEqual(expect.objectContaining({
43
+ expect(structuredContent).toEqual({
44
44
  completed: ['8485093748'],
45
- failures: [],
45
+ // failures array is removed when empty
46
46
  totalRequested: 1,
47
47
  successCount: 1,
48
48
  failureCount: 0,
49
- }));
49
+ });
50
50
  });
51
51
  it('should handle partial failures gracefully', async () => {
52
52
  // Mock first and third tasks to succeed, second to fail
@@ -55,7 +55,6 @@ describe(`${FIND_COMMENTS} tool`, () => {
55
55
  searchType: 'task',
56
56
  searchId: 'task123',
57
57
  hasMore: false,
58
- nextCursor: null,
59
58
  totalCount: 2,
60
59
  }));
61
60
  });
@@ -128,7 +127,6 @@ describe(`${FIND_COMMENTS} tool`, () => {
128
127
  searchType: 'project',
129
128
  searchId: 'project456',
130
129
  hasMore: false,
131
- nextCursor: null,
132
130
  totalCount: 1,
133
131
  }));
134
132
  });
@@ -155,13 +153,11 @@ describe(`${FIND_COMMENTS} tool`, () => {
155
153
  id: 'comment789',
156
154
  content: 'Single comment content',
157
155
  taskId: 'task123',
158
- fileAttachment: null,
159
156
  }),
160
157
  ]),
161
158
  searchType: 'single',
162
159
  searchId: 'comment789',
163
160
  hasMore: false,
164
- nextCursor: null,
165
161
  totalCount: 1,
166
162
  }));
167
163
  });
@@ -200,7 +196,6 @@ describe(`${FIND_COMMENTS} tool`, () => {
200
196
  searchType: 'single',
201
197
  searchId: 'comment789',
202
198
  hasMore: false,
203
- nextCursor: null,
204
199
  totalCount: 1,
205
200
  }));
206
201
  });
@@ -229,14 +224,13 @@ describe(`${FIND_COMMENTS} tool`, () => {
229
224
  expect(extractTextContent(result)).toMatchSnapshot();
230
225
  // Verify structured content
231
226
  const structuredContent = extractStructuredContent(result);
232
- expect(structuredContent).toEqual(expect.objectContaining({
233
- comments: [],
227
+ expect(structuredContent).toEqual({
234
228
  searchType: 'task',
235
229
  searchId: 'task123',
236
230
  hasMore: false,
237
- nextCursor: null,
238
231
  totalCount: 0,
239
- }));
232
+ // comments array is removed when empty
233
+ });
240
234
  });
241
235
  });
242
236
  });
@@ -53,7 +53,6 @@ describe(`${FIND_PROJECTS} tool`, () => {
53
53
  projects: expect.any(Array),
54
54
  totalCount: 3,
55
55
  hasMore: false,
56
- nextCursor: null,
57
56
  appliedFilters: {
58
57
  search: undefined,
59
58
  limit: 50,
@@ -112,7 +111,6 @@ describe(`${FIND_PROJECTS} tool`, () => {
112
111
  expect(structuredContent.projects).toHaveLength(2); // Should match filtered results
113
112
  expect(structuredContent.totalCount).toBe(2);
114
113
  expect(structuredContent.hasMore).toBe(false);
115
- expect(structuredContent.nextCursor).toBeNull();
116
114
  expect(structuredContent.appliedFilters).toEqual({
117
115
  search: 'work',
118
116
  limit: 50,
@@ -77,12 +77,8 @@ describe(`${FIND_SECTIONS} tool`, () => {
77
77
  expect(textContent).toContain(`Use ${ADD_SECTIONS} to create sections`);
78
78
  // Verify structured content
79
79
  const structuredContent = extractStructuredContent(result);
80
- expect(structuredContent.sections).toHaveLength(0);
80
+ expect(structuredContent.sections).toBeUndefined(); // Empty arrays are removed
81
81
  expect(structuredContent.totalCount).toBe(0);
82
- expect(structuredContent.appliedFilters).toEqual({
83
- projectId: 'empty-project-id',
84
- search: undefined,
85
- });
86
82
  });
87
83
  });
88
84
  describe('searching sections by name', () => {
@@ -117,7 +117,6 @@ describe(`${FIND_TASKS} tool`, () => {
117
117
  tasks: expect.any(Array),
118
118
  totalCount: 1,
119
119
  hasMore: false,
120
- nextCursor: null,
121
120
  appliedFilters: expect.objectContaining({
122
121
  searchText: params.searchText,
123
122
  limit: expectedLimit,
@@ -144,15 +143,14 @@ describe(`${FIND_TASKS} tool`, () => {
144
143
  expect(extractTextContent(result)).toMatchSnapshot();
145
144
  // Verify structured content for empty results
146
145
  const structuredContent = extractStructuredContent(result);
147
- expect(structuredContent).toEqual(expect.objectContaining({
148
- tasks: [],
146
+ expect(structuredContent).toEqual({
147
+ // tasks array is removed when empty
149
148
  totalCount: 0,
150
149
  hasMore: false,
151
- nextCursor: null,
152
150
  appliedFilters: expect.objectContaining({
153
151
  searchText: searchText,
154
152
  }),
155
- }));
153
+ });
156
154
  });
157
155
  });
158
156
  describe('validation', () => {
@@ -59,7 +59,7 @@ describe(`${GET_OVERVIEW} tool`, () => {
59
59
  inbox: expect.objectContaining({
60
60
  id: TEST_IDS.PROJECT_INBOX,
61
61
  name: 'Inbox',
62
- sections: expect.any(Array),
62
+ // sections array removed if empty
63
63
  }),
64
64
  projects: expect.any(Array),
65
65
  totalProjects: 2,
@@ -75,14 +75,13 @@ describe(`${GET_OVERVIEW} tool`, () => {
75
75
  expect(extractTextContent(result)).toMatchSnapshot();
76
76
  // Test structured content sanity checks
77
77
  const structuredContent = extractStructuredContent(result);
78
- expect(structuredContent).toEqual(expect.objectContaining({
78
+ expect(structuredContent).toEqual({
79
79
  type: 'account_overview',
80
- inbox: null,
81
- projects: [],
80
+ // projects array is removed when empty
82
81
  totalProjects: 0,
83
82
  totalSections: 0,
84
83
  hasNestedProjects: false,
85
- }));
84
+ });
86
85
  });
87
86
  });
88
87
  describe('project overview (with projectId)', () => {
@@ -188,20 +187,19 @@ describe(`${GET_OVERVIEW} tool`, () => {
188
187
  expect(extractTextContent(result)).toMatchSnapshot();
189
188
  // Test structured content sanity checks
190
189
  const structuredContent = extractStructuredContent(result);
191
- expect(structuredContent).toEqual(expect.objectContaining({
190
+ expect(structuredContent).toEqual({
192
191
  type: 'project_overview',
193
192
  project: expect.objectContaining({
194
193
  id: 'empty-project-id',
195
194
  name: 'Empty Project',
196
195
  }),
197
- sections: [],
198
- tasks: [],
196
+ // sections and tasks arrays are removed when empty
199
197
  stats: expect.objectContaining({
200
198
  totalTasks: 0,
201
199
  totalSections: 0,
202
200
  tasksWithoutSection: 0,
203
201
  }),
204
- }));
202
+ });
205
203
  });
206
204
  });
207
205
  describe('error handling', () => {
@@ -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
  });
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-ai",
3
- "version": "4.9.3",
3
+ "version": "4.9.4",
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",