@doist/todoist-ai 4.9.2 → 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.
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-helpers.js +5 -2
- package/dist/tools/__tests__/add-projects.test.js +5 -1
- package/dist/tools/__tests__/add-sections.test.js +5 -1
- package/dist/tools/__tests__/assignment-integration.test.js +1 -1
- package/dist/tools/__tests__/complete-tasks.test.js +6 -6
- package/dist/tools/__tests__/find-comments.test.js +3 -9
- package/dist/tools/__tests__/find-projects.test.js +0 -2
- package/dist/tools/__tests__/find-sections.test.js +1 -5
- package/dist/tools/__tests__/find-tasks-by-date.test.js +26 -8
- package/dist/tools/__tests__/find-tasks.test.js +3 -5
- package/dist/tools/__tests__/get-overview.test.js +7 -9
- package/dist/tools/__tests__/update-comments.test.js +0 -2
- package/dist/tools/__tests__/update-projects.test.js +14 -2
- package/dist/tools/__tests__/update-sections.test.js +14 -2
- package/dist/tools/__tests__/update-tasks.test.js +1 -1
- package/dist/tools/find-tasks-by-date.d.ts +6 -3
- package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
- package/dist/tools/find-tasks-by-date.js +53 -14
- package/dist/utils/sanitize-data.d.ts +9 -0
- package/dist/utils/sanitize-data.d.ts.map +1 -0
- package/dist/utils/sanitize-data.js +37 -0
- package/dist/utils/sanitize-data.test.d.ts +2 -0
- package/dist/utils/sanitize-data.test.d.ts.map +1 -0
- package/dist/utils/sanitize-data.test.js +93 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -328,18 +328,20 @@ declare const tools: {
|
|
|
328
328
|
parameters: {
|
|
329
329
|
labels: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString, "many">>;
|
|
330
330
|
labelsOperator: import("zod").ZodOptional<import("zod").ZodEnum<["and", "or"]>>;
|
|
331
|
-
startDate: import("zod").ZodString
|
|
331
|
+
startDate: import("zod").ZodOptional<import("zod").ZodString>;
|
|
332
|
+
overdueOption: import("zod").ZodOptional<import("zod").ZodEnum<["overdue-only", "include-overdue", "exclude-overdue"]>>;
|
|
332
333
|
daysCount: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
333
334
|
limit: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
334
335
|
cursor: import("zod").ZodOptional<import("zod").ZodString>;
|
|
335
336
|
};
|
|
336
337
|
execute(args: {
|
|
337
338
|
limit: number;
|
|
338
|
-
startDate: string;
|
|
339
339
|
daysCount: number;
|
|
340
340
|
labels?: string[] | undefined;
|
|
341
341
|
cursor?: string | undefined;
|
|
342
342
|
labelsOperator?: "and" | "or" | undefined;
|
|
343
|
+
startDate?: string | undefined;
|
|
344
|
+
overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
|
|
343
345
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
344
346
|
content: {
|
|
345
347
|
type: "text";
|
|
@@ -366,11 +368,12 @@ declare const tools: {
|
|
|
366
368
|
hasMore: boolean;
|
|
367
369
|
appliedFilters: {
|
|
368
370
|
limit: number;
|
|
369
|
-
startDate: string;
|
|
370
371
|
daysCount: number;
|
|
371
372
|
labels?: string[] | undefined;
|
|
372
373
|
cursor?: string | undefined;
|
|
373
374
|
labelsOperator?: "and" | "or" | undefined;
|
|
375
|
+
startDate?: string | undefined;
|
|
376
|
+
overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
|
|
374
377
|
};
|
|
375
378
|
};
|
|
376
379
|
} | {
|
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,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,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 +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;
|
|
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"}
|
package/dist/mcp-helpers.js
CHANGED
|
@@ -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(
|
|
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:
|
|
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:
|
|
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).
|
|
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(
|
|
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(
|
|
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(
|
|
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).
|
|
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', () => {
|
|
@@ -20,18 +20,19 @@ const mockTodoistApi = {
|
|
|
20
20
|
const mockTodoistUser = createMockUser();
|
|
21
21
|
// Mock date-fns functions to make tests deterministic
|
|
22
22
|
jest.mock('date-fns', () => ({
|
|
23
|
-
addDays: jest.fn(() =>
|
|
23
|
+
addDays: jest.fn((date, amount) => {
|
|
24
|
+
const d = new Date(date);
|
|
25
|
+
d.setDate(d.getDate() + amount);
|
|
26
|
+
return d;
|
|
27
|
+
}),
|
|
24
28
|
formatISO: jest.fn((date, options) => {
|
|
25
29
|
if (typeof date === 'string') {
|
|
26
30
|
return date; // Return string dates as-is
|
|
27
31
|
}
|
|
28
|
-
if (options
|
|
29
|
-
|
|
30
|
-
'representation' in options &&
|
|
31
|
-
options.representation === 'date') {
|
|
32
|
-
return '2025-08-15'; // Return predictable date for 'today'
|
|
32
|
+
if (options?.representation === 'date') {
|
|
33
|
+
return date.toISOString().split('T')[0];
|
|
33
34
|
}
|
|
34
|
-
return
|
|
35
|
+
return date.toISOString();
|
|
35
36
|
}),
|
|
36
37
|
}));
|
|
37
38
|
const { FIND_TASKS_BY_DATE, UPDATE_TASKS } = ToolNames;
|
|
@@ -46,6 +47,23 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
46
47
|
jest.restoreAllMocks();
|
|
47
48
|
});
|
|
48
49
|
describe('listing tasks by date range', () => {
|
|
50
|
+
it('only returns tasks for the startDate when daysCount is 1', async () => {
|
|
51
|
+
const mockTasks = [
|
|
52
|
+
createMappedTask({ content: 'Task for specific date', dueDate: '2025-08-20' }),
|
|
53
|
+
];
|
|
54
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
55
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
56
|
+
const result = await findTasksByDate.execute({ startDate: '2025-08-20', limit: 50, daysCount: 1 }, mockTodoistApi);
|
|
57
|
+
// Verify the query uses daysCount=1 by checking the end date calculation
|
|
58
|
+
expect(mockGetTasksByFilter).toHaveBeenCalledWith({
|
|
59
|
+
client: mockTodoistApi,
|
|
60
|
+
query: '(due after: 2025-08-20 | due: 2025-08-20) & due before: 2025-08-21',
|
|
61
|
+
cursor: undefined,
|
|
62
|
+
limit: 50,
|
|
63
|
+
});
|
|
64
|
+
const textContent = extractTextContent(result);
|
|
65
|
+
expect(textContent).toMatchSnapshot();
|
|
66
|
+
});
|
|
49
67
|
it('should get tasks for today when startDate is "today" (includes overdue)', async () => {
|
|
50
68
|
const mockTasks = [createMappedTask({ content: 'Today task', dueDate: '2025-08-15' })];
|
|
51
69
|
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
@@ -375,7 +393,7 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
375
393
|
];
|
|
376
394
|
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
377
395
|
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
378
|
-
const result = await findTasksByDate.execute({
|
|
396
|
+
const result = await findTasksByDate.execute({ overdueOption: 'overdue-only', daysCount: 1, limit: 50 }, mockTodoistApi);
|
|
379
397
|
const structuredContent = extractStructuredContent(result);
|
|
380
398
|
// Should only return tasks 1 and 2, not task 3
|
|
381
399
|
expect(structuredContent.tasks).toHaveLength(2);
|
|
@@ -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(
|
|
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
|
|
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(
|
|
78
|
+
expect(structuredContent).toEqual({
|
|
79
79
|
type: 'account_overview',
|
|
80
|
-
|
|
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(
|
|
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: [
|
|
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:
|
|
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: [
|
|
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:
|
|
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).
|
|
676
|
+
expect(structuredContent.tasks).toBeUndefined(); // Empty arrays are removed
|
|
677
677
|
expect(structuredContent.totalCount).toBe(0);
|
|
678
678
|
});
|
|
679
679
|
});
|
|
@@ -5,18 +5,20 @@ declare const findTasksByDate: {
|
|
|
5
5
|
parameters: {
|
|
6
6
|
labels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
7
|
labelsOperator: z.ZodOptional<z.ZodEnum<["and", "or"]>>;
|
|
8
|
-
startDate: z.ZodString
|
|
8
|
+
startDate: z.ZodOptional<z.ZodString>;
|
|
9
|
+
overdueOption: z.ZodOptional<z.ZodEnum<["overdue-only", "include-overdue", "exclude-overdue"]>>;
|
|
9
10
|
daysCount: z.ZodDefault<z.ZodNumber>;
|
|
10
11
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
11
12
|
cursor: z.ZodOptional<z.ZodString>;
|
|
12
13
|
};
|
|
13
14
|
execute(args: {
|
|
14
15
|
limit: number;
|
|
15
|
-
startDate: string;
|
|
16
16
|
daysCount: number;
|
|
17
17
|
labels?: string[] | undefined;
|
|
18
18
|
cursor?: string | undefined;
|
|
19
19
|
labelsOperator?: "and" | "or" | undefined;
|
|
20
|
+
startDate?: string | undefined;
|
|
21
|
+
overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
|
|
20
22
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
21
23
|
content: {
|
|
22
24
|
type: "text";
|
|
@@ -43,11 +45,12 @@ declare const findTasksByDate: {
|
|
|
43
45
|
hasMore: boolean;
|
|
44
46
|
appliedFilters: {
|
|
45
47
|
limit: number;
|
|
46
|
-
startDate: string;
|
|
47
48
|
daysCount: number;
|
|
48
49
|
labels?: string[] | undefined;
|
|
49
50
|
cursor?: string | undefined;
|
|
50
51
|
labelsOperator?: "and" | "or" | undefined;
|
|
52
|
+
startDate?: string | undefined;
|
|
53
|
+
overdueOption?: "overdue-only" | "include-overdue" | "exclude-overdue" | undefined;
|
|
51
54
|
};
|
|
52
55
|
};
|
|
53
56
|
} | {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-tasks-by-date.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks-by-date.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"find-tasks-by-date.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks-by-date.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAmDvB,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmEqB,CAAA;AAwF1C,OAAO,EAAE,eAAe,EAAE,CAAA"}
|
|
@@ -10,14 +10,19 @@ const ArgsSchema = {
|
|
|
10
10
|
startDate: z
|
|
11
11
|
.string()
|
|
12
12
|
.regex(/^(\d{4}-\d{2}-\d{2}|today)$/)
|
|
13
|
+
.optional()
|
|
13
14
|
.describe("The start date to get the tasks for. Format: YYYY-MM-DD or 'today'."),
|
|
15
|
+
overdueOption: z
|
|
16
|
+
.enum(['overdue-only', 'include-overdue', 'exclude-overdue'])
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("How to handle overdue tasks. 'overdue-only' to get only overdue tasks, 'include-overdue' to include overdue tasks along with tasks for the specified date(s), and 'exclude-overdue' to exclude overdue tasks. Default is 'include-overdue'."),
|
|
14
19
|
daysCount: z
|
|
15
20
|
.number()
|
|
16
21
|
.int()
|
|
17
22
|
.min(1)
|
|
18
23
|
.max(30)
|
|
19
24
|
.default(1)
|
|
20
|
-
.describe('The number of days to get the tasks for, starting from the start date.'),
|
|
25
|
+
.describe('The number of days to get the tasks for, starting from the start date. Default is 1 which means only tasks for the start date.'),
|
|
21
26
|
limit: z
|
|
22
27
|
.number()
|
|
23
28
|
.int()
|
|
@@ -36,14 +41,22 @@ const findTasksByDate = {
|
|
|
36
41
|
description: "Get tasks by date range. Use startDate 'today' to get today's tasks including overdue items, or provide a specific date/date range.",
|
|
37
42
|
parameters: ArgsSchema,
|
|
38
43
|
async execute(args, client) {
|
|
44
|
+
if (!args.startDate && args.overdueOption !== 'overdue-only') {
|
|
45
|
+
throw new Error('Either startDate must be provided or overdueOption must be set to overdue-only');
|
|
46
|
+
}
|
|
39
47
|
let query = '';
|
|
40
48
|
const todoistUser = await client.getUser();
|
|
41
|
-
if (args.
|
|
42
|
-
query = '
|
|
49
|
+
if (args.overdueOption === 'overdue-only') {
|
|
50
|
+
query = 'overdue';
|
|
43
51
|
}
|
|
44
|
-
else {
|
|
52
|
+
else if (args.startDate === 'today') {
|
|
53
|
+
// For 'today', include overdue unless explicitly excluded
|
|
54
|
+
query = args.overdueOption === 'exclude-overdue' ? 'today' : 'today | overdue';
|
|
55
|
+
}
|
|
56
|
+
else if (args.startDate) {
|
|
57
|
+
// For specific dates, never include overdue tasks
|
|
45
58
|
const startDate = args.startDate;
|
|
46
|
-
const endDate = addDays(startDate, args.daysCount
|
|
59
|
+
const endDate = addDays(startDate, args.daysCount);
|
|
47
60
|
const endDateStr = formatISO(endDate, { representation: 'date' });
|
|
48
61
|
query = `(due after: ${startDate} | due: ${startDate}) & due before: ${endDateStr}`;
|
|
49
62
|
}
|
|
@@ -87,11 +100,18 @@ const findTasksByDate = {
|
|
|
87
100
|
function generateTextContent({ tasks, args, nextCursor, }) {
|
|
88
101
|
// Generate filter description
|
|
89
102
|
const filterHints = [];
|
|
90
|
-
if (args.
|
|
91
|
-
filterHints.push(
|
|
103
|
+
if (args.overdueOption === 'overdue-only') {
|
|
104
|
+
filterHints.push('overdue tasks only');
|
|
92
105
|
}
|
|
93
|
-
else {
|
|
94
|
-
|
|
106
|
+
else if (args.startDate === 'today') {
|
|
107
|
+
const overdueText = args.overdueOption === 'exclude-overdue' ? '' : ' + overdue tasks';
|
|
108
|
+
filterHints.push(`today${overdueText}${args.daysCount > 1 ? ` + ${args.daysCount - 1} more days` : ''}`);
|
|
109
|
+
}
|
|
110
|
+
else if (args.startDate) {
|
|
111
|
+
const dateRange = args.daysCount > 1
|
|
112
|
+
? ` to ${getDateString(addDays(args.startDate, args.daysCount))}`
|
|
113
|
+
: '';
|
|
114
|
+
filterHints.push(`${args.startDate}${dateRange}`);
|
|
95
115
|
}
|
|
96
116
|
// Add label filter information
|
|
97
117
|
if (args.labels && args.labels.length > 0) {
|
|
@@ -101,12 +121,29 @@ function generateTextContent({ tasks, args, nextCursor, }) {
|
|
|
101
121
|
filterHints.push(`labels: ${labelText}`);
|
|
102
122
|
}
|
|
103
123
|
// Generate subject description
|
|
104
|
-
|
|
124
|
+
let subject = '';
|
|
125
|
+
if (args.overdueOption === 'overdue-only') {
|
|
126
|
+
subject = 'Overdue tasks';
|
|
127
|
+
}
|
|
128
|
+
else if (args.startDate === 'today') {
|
|
129
|
+
subject =
|
|
130
|
+
args.overdueOption === 'exclude-overdue' ? `Today's tasks` : `Today's tasks + overdue`;
|
|
131
|
+
}
|
|
132
|
+
else if (args.startDate) {
|
|
133
|
+
subject = `Tasks for ${args.startDate}`;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
subject = 'Tasks';
|
|
137
|
+
}
|
|
105
138
|
// Generate helpful suggestions for empty results
|
|
106
139
|
const zeroReasonHints = [];
|
|
107
140
|
if (tasks.length === 0) {
|
|
108
|
-
if (args.
|
|
109
|
-
zeroReasonHints.push('Great job! No tasks
|
|
141
|
+
if (args.overdueOption === 'overdue-only') {
|
|
142
|
+
zeroReasonHints.push('Great job! No overdue tasks');
|
|
143
|
+
}
|
|
144
|
+
else if (args.startDate === 'today') {
|
|
145
|
+
const overdueNote = args.overdueOption === 'exclude-overdue' ? '' : ' or overdue';
|
|
146
|
+
zeroReasonHints.push(`Great job! No tasks for today${overdueNote}`);
|
|
110
147
|
}
|
|
111
148
|
else {
|
|
112
149
|
zeroReasonHints.push("Expand date range with larger 'daysCount'");
|
|
@@ -116,10 +153,12 @@ function generateTextContent({ tasks, args, nextCursor, }) {
|
|
|
116
153
|
// Generate contextual next steps
|
|
117
154
|
const now = new Date();
|
|
118
155
|
const todayStr = getDateString(now);
|
|
156
|
+
const hasOverdue = args.overdueOption === 'overdue-only' ||
|
|
157
|
+
args.startDate === 'today' ||
|
|
158
|
+
tasks.some((task) => task.dueDate && new Date(task.dueDate) < now);
|
|
119
159
|
const nextSteps = generateTaskNextSteps('listed', tasks, {
|
|
120
160
|
hasToday: args.startDate === 'today' || tasks.some((task) => task.dueDate === todayStr),
|
|
121
|
-
hasOverdue
|
|
122
|
-
tasks.some((task) => task.dueDate && new Date(task.dueDate) < now),
|
|
161
|
+
hasOverdue,
|
|
123
162
|
});
|
|
124
163
|
return summarizeList({
|
|
125
164
|
subject,
|
|
@@ -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 @@
|
|
|
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
|
+
"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.
|
|
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",
|