@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.
- package/README.md +11 -0
- package/dist/index.d.ts +37 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/mcp-helpers.d.ts +8 -1
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/mcp-helpers.js +6 -3
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +5 -0
- package/dist/tool-helpers.d.ts +8 -1
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +13 -10
- 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__/fetch.test.d.ts +2 -0
- package/dist/tools/__tests__/fetch.test.d.ts.map +1 -0
- package/dist/tools/__tests__/fetch.test.js +275 -0
- 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.test.js +3 -5
- package/dist/tools/__tests__/get-overview.test.js +7 -9
- package/dist/tools/__tests__/search.test.d.ts +2 -0
- package/dist/tools/__tests__/search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/search.test.js +208 -0
- 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/add-tasks.d.ts.map +1 -1
- package/dist/tools/add-tasks.js +8 -2
- package/dist/tools/fetch.d.ts +26 -0
- package/dist/tools/fetch.d.ts.map +1 -0
- package/dist/tools/fetch.js +99 -0
- package/dist/tools/find-projects.d.ts +2 -2
- package/dist/tools/search.d.ts +26 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +64 -0
- package/dist/tools/update-tasks.d.ts.map +1 -1
- package/dist/tools/update-tasks.js +8 -2
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- 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/dist/utils/tool-names.d.ts +2 -0
- package/dist/utils/tool-names.d.ts.map +1 -1
- package/dist/utils/tool-names.js +3 -0
- 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: [
|
|
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
|
});
|
|
@@ -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;AAqDvB,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AAgI1C,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
package/dist/tools/add-tasks.js
CHANGED
|
@@ -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
|
|
11
|
-
|
|
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;
|
|
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
|
|
13
|
-
|
|
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:
|
|
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 */
|
package/dist/utils/constants.js
CHANGED
|
@@ -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:
|
|
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 @@
|
|
|
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
|
|
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"}
|
package/dist/utils/tool-names.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/todoist-ai",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
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",
|