@doist/todoist-ai 3.0.0 → 4.1.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 +2 -18
- package/dist/index.d.ts +296 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -6
- package/dist/main.js +2 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +16 -4
- package/dist/tools/__tests__/add-comments.test.d.ts +2 -0
- package/dist/tools/__tests__/add-comments.test.d.ts.map +1 -0
- package/dist/tools/__tests__/add-comments.test.js +241 -0
- package/dist/tools/__tests__/add-projects.test.d.ts +2 -0
- package/dist/tools/__tests__/add-projects.test.d.ts.map +1 -0
- package/dist/tools/__tests__/add-projects.test.js +152 -0
- package/dist/tools/__tests__/add-sections.test.d.ts +2 -0
- package/dist/tools/__tests__/add-sections.test.d.ts.map +1 -0
- package/dist/tools/__tests__/add-sections.test.js +181 -0
- package/dist/tools/__tests__/add-tasks.test.js +16 -10
- package/dist/tools/__tests__/find-comments.test.d.ts +2 -0
- package/dist/tools/__tests__/find-comments.test.d.ts.map +1 -0
- package/dist/tools/__tests__/find-comments.test.js +242 -0
- package/dist/tools/__tests__/find-sections.test.js +2 -2
- package/dist/tools/__tests__/update-comments.test.d.ts +2 -0
- package/dist/tools/__tests__/update-comments.test.d.ts.map +1 -0
- package/dist/tools/__tests__/update-comments.test.js +296 -0
- package/dist/tools/__tests__/update-projects.test.d.ts +2 -0
- package/dist/tools/__tests__/update-projects.test.d.ts.map +1 -0
- package/dist/tools/__tests__/update-projects.test.js +205 -0
- package/dist/tools/__tests__/update-sections.test.d.ts +2 -0
- package/dist/tools/__tests__/update-sections.test.d.ts.map +1 -0
- package/dist/tools/__tests__/update-sections.test.js +156 -0
- package/dist/tools/add-comments.d.ts +51 -0
- package/dist/tools/add-comments.d.ts.map +1 -0
- package/dist/tools/add-comments.js +79 -0
- package/dist/tools/add-projects.d.ts +50 -0
- package/dist/tools/add-projects.d.ts.map +1 -0
- package/dist/tools/add-projects.js +59 -0
- package/dist/tools/{manage-sections.d.ts → add-sections.d.ts} +21 -13
- package/dist/tools/add-sections.d.ts.map +1 -0
- package/dist/tools/add-sections.js +61 -0
- package/dist/tools/add-tasks.d.ts +15 -8
- package/dist/tools/add-tasks.d.ts.map +1 -1
- package/dist/tools/add-tasks.js +46 -37
- package/dist/tools/delete-object.d.ts +3 -3
- package/dist/tools/delete-object.d.ts.map +1 -1
- package/dist/tools/delete-object.js +13 -3
- package/dist/tools/find-comments.d.ts +46 -0
- package/dist/tools/find-comments.d.ts.map +1 -0
- package/dist/tools/find-comments.js +143 -0
- package/dist/tools/find-projects.js +2 -2
- package/dist/tools/find-sections.js +4 -4
- package/dist/tools/update-comments.d.ts +50 -0
- package/dist/tools/update-comments.d.ts.map +1 -0
- package/dist/tools/update-comments.js +82 -0
- package/dist/tools/update-projects.d.ts +59 -0
- package/dist/tools/update-projects.d.ts.map +1 -0
- package/dist/tools/update-projects.js +84 -0
- package/dist/tools/update-sections.d.ts +47 -0
- package/dist/tools/update-sections.d.ts.map +1 -0
- package/dist/tools/update-sections.js +70 -0
- package/dist/utils/constants.d.ts +4 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +4 -0
- package/dist/utils/tool-names.d.ts +7 -2
- package/dist/utils/tool-names.d.ts.map +1 -1
- package/dist/utils/tool-names.js +8 -2
- package/package.json +1 -1
- package/dist/tools/__tests__/manage-projects.test.d.ts +0 -2
- package/dist/tools/__tests__/manage-projects.test.d.ts.map +0 -1
- package/dist/tools/__tests__/manage-projects.test.js +0 -109
- package/dist/tools/__tests__/manage-sections.test.d.ts +0 -2
- package/dist/tools/__tests__/manage-sections.test.d.ts.map +0 -1
- package/dist/tools/__tests__/manage-sections.test.js +0 -162
- package/dist/tools/manage-projects.d.ts +0 -35
- package/dist/tools/manage-projects.d.ts.map +0 -1
- package/dist/tools/manage-projects.js +0 -63
- package/dist/tools/manage-sections.d.ts.map +0 -1
- package/dist/tools/manage-sections.js +0 -78
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { createMockProject, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
|
|
3
|
+
import { ToolNames } from '../../utils/tool-names.js';
|
|
4
|
+
import { updateProjects } from '../update-projects.js';
|
|
5
|
+
// Mock the Todoist API
|
|
6
|
+
const mockTodoistApi = {
|
|
7
|
+
updateProject: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
const { FIND_PROJECTS, UPDATE_PROJECTS, GET_OVERVIEW } = ToolNames;
|
|
10
|
+
describe(`${UPDATE_PROJECTS} tool`, () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
describe('updating a single project', () => {
|
|
15
|
+
it('should update a project when id and name are provided', async () => {
|
|
16
|
+
const mockApiResponse = {
|
|
17
|
+
url: 'https://todoist.com/projects/existing-project-123',
|
|
18
|
+
id: 'existing-project-123',
|
|
19
|
+
parentId: null,
|
|
20
|
+
isDeleted: false,
|
|
21
|
+
updatedAt: '2025-08-13T22:10:30.000000Z',
|
|
22
|
+
childOrder: 1,
|
|
23
|
+
description: '',
|
|
24
|
+
isCollapsed: false,
|
|
25
|
+
canAssignTasks: false,
|
|
26
|
+
color: 'red',
|
|
27
|
+
isFavorite: false,
|
|
28
|
+
isFrozen: false,
|
|
29
|
+
name: 'Updated Project Name',
|
|
30
|
+
viewStyle: 'list',
|
|
31
|
+
isArchived: false,
|
|
32
|
+
inboxProject: false,
|
|
33
|
+
isShared: false,
|
|
34
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
35
|
+
defaultOrder: 0,
|
|
36
|
+
};
|
|
37
|
+
mockTodoistApi.updateProject.mockResolvedValue(mockApiResponse);
|
|
38
|
+
const result = await updateProjects.execute({ projects: [{ id: 'existing-project-123', name: 'Updated Project Name' }] }, mockTodoistApi);
|
|
39
|
+
expect(mockTodoistApi.updateProject).toHaveBeenCalledWith('existing-project-123', {
|
|
40
|
+
name: 'Updated Project Name',
|
|
41
|
+
});
|
|
42
|
+
const textContent = extractTextContent(result);
|
|
43
|
+
expect(textContent).toMatchSnapshot();
|
|
44
|
+
expect(textContent).toContain('Updated 1 project:');
|
|
45
|
+
expect(textContent).toContain('Updated Project Name (id=existing-project-123)');
|
|
46
|
+
expect(textContent).toContain(`Use ${GET_OVERVIEW} with projectId=existing-project-123`);
|
|
47
|
+
// Verify structured content
|
|
48
|
+
const structuredContent = extractStructuredContent(result);
|
|
49
|
+
expect(structuredContent).toEqual(expect.objectContaining({
|
|
50
|
+
projects: [mockApiResponse],
|
|
51
|
+
totalCount: 1,
|
|
52
|
+
updatedProjectIds: ['existing-project-123'],
|
|
53
|
+
appliedOperations: {
|
|
54
|
+
updateCount: 1,
|
|
55
|
+
skippedCount: 0,
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
});
|
|
59
|
+
it('should update project with isFavorite and viewStyle options', async () => {
|
|
60
|
+
const mockApiResponse = {
|
|
61
|
+
url: 'https://todoist.com/projects/project-123',
|
|
62
|
+
id: 'project-123',
|
|
63
|
+
parentId: null,
|
|
64
|
+
isDeleted: false,
|
|
65
|
+
updatedAt: '2025-08-13T22:10:30.000000Z',
|
|
66
|
+
childOrder: 1,
|
|
67
|
+
description: '',
|
|
68
|
+
isCollapsed: false,
|
|
69
|
+
canAssignTasks: false,
|
|
70
|
+
color: 'red',
|
|
71
|
+
isFavorite: true,
|
|
72
|
+
isFrozen: false,
|
|
73
|
+
name: 'Updated Favorite Project',
|
|
74
|
+
viewStyle: 'board',
|
|
75
|
+
isArchived: false,
|
|
76
|
+
inboxProject: false,
|
|
77
|
+
isShared: false,
|
|
78
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
79
|
+
defaultOrder: 0,
|
|
80
|
+
};
|
|
81
|
+
mockTodoistApi.updateProject.mockResolvedValue(mockApiResponse);
|
|
82
|
+
const result = await updateProjects.execute({
|
|
83
|
+
projects: [
|
|
84
|
+
{
|
|
85
|
+
id: 'project-123',
|
|
86
|
+
name: 'Updated Favorite Project',
|
|
87
|
+
isFavorite: true,
|
|
88
|
+
viewStyle: 'board',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}, mockTodoistApi);
|
|
92
|
+
expect(mockTodoistApi.updateProject).toHaveBeenCalledWith('project-123', {
|
|
93
|
+
name: 'Updated Favorite Project',
|
|
94
|
+
isFavorite: true,
|
|
95
|
+
viewStyle: 'board',
|
|
96
|
+
});
|
|
97
|
+
const textContent = extractTextContent(result);
|
|
98
|
+
expect(textContent).toMatchSnapshot();
|
|
99
|
+
expect(textContent).toContain('Updated 1 project:');
|
|
100
|
+
expect(textContent).toContain('Updated Favorite Project (id=project-123)');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('updating multiple projects', () => {
|
|
104
|
+
it('should update multiple projects and return mapped results', async () => {
|
|
105
|
+
const mockProjects = [
|
|
106
|
+
createMockProject({ id: 'project-1', name: 'Updated First Project' }),
|
|
107
|
+
createMockProject({ id: 'project-2', name: 'Updated Second Project' }),
|
|
108
|
+
createMockProject({ id: 'project-3', name: 'Updated Third Project' }),
|
|
109
|
+
];
|
|
110
|
+
const [project1, project2, project3] = mockProjects;
|
|
111
|
+
mockTodoistApi.updateProject
|
|
112
|
+
.mockResolvedValueOnce(project1)
|
|
113
|
+
.mockResolvedValueOnce(project2)
|
|
114
|
+
.mockResolvedValueOnce(project3);
|
|
115
|
+
const result = await updateProjects.execute({
|
|
116
|
+
projects: [
|
|
117
|
+
{ id: 'project-1', name: 'Updated First Project' },
|
|
118
|
+
{ id: 'project-2', name: 'Updated Second Project' },
|
|
119
|
+
{ id: 'project-3', name: 'Updated Third Project' },
|
|
120
|
+
],
|
|
121
|
+
}, mockTodoistApi);
|
|
122
|
+
// Verify API was called correctly for each project
|
|
123
|
+
expect(mockTodoistApi.updateProject).toHaveBeenCalledTimes(3);
|
|
124
|
+
expect(mockTodoistApi.updateProject).toHaveBeenNthCalledWith(1, 'project-1', {
|
|
125
|
+
name: 'Updated First Project',
|
|
126
|
+
});
|
|
127
|
+
expect(mockTodoistApi.updateProject).toHaveBeenNthCalledWith(2, 'project-2', {
|
|
128
|
+
name: 'Updated Second Project',
|
|
129
|
+
});
|
|
130
|
+
expect(mockTodoistApi.updateProject).toHaveBeenNthCalledWith(3, 'project-3', {
|
|
131
|
+
name: 'Updated Third Project',
|
|
132
|
+
});
|
|
133
|
+
const textContent = extractTextContent(result);
|
|
134
|
+
expect(textContent).toMatchSnapshot();
|
|
135
|
+
expect(textContent).toContain('Updated 3 projects:');
|
|
136
|
+
expect(textContent).toContain('Updated First Project (id=project-1)');
|
|
137
|
+
expect(textContent).toContain('Updated Second Project (id=project-2)');
|
|
138
|
+
expect(textContent).toContain('Updated Third Project (id=project-3)');
|
|
139
|
+
expect(textContent).toContain(`Use ${FIND_PROJECTS} to see all projects`);
|
|
140
|
+
// Verify structured content
|
|
141
|
+
const structuredContent = extractStructuredContent(result);
|
|
142
|
+
expect(structuredContent).toEqual(expect.objectContaining({
|
|
143
|
+
projects: mockProjects,
|
|
144
|
+
totalCount: 3,
|
|
145
|
+
updatedProjectIds: ['project-1', 'project-2', 'project-3'],
|
|
146
|
+
appliedOperations: {
|
|
147
|
+
updateCount: 3,
|
|
148
|
+
skippedCount: 0,
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
});
|
|
152
|
+
it('should skip projects with no updates and report correctly', async () => {
|
|
153
|
+
const mockProject = createMockProject({
|
|
154
|
+
id: 'project-1',
|
|
155
|
+
name: 'Updated Project',
|
|
156
|
+
});
|
|
157
|
+
mockTodoistApi.updateProject.mockResolvedValue(mockProject);
|
|
158
|
+
const result = await updateProjects.execute({
|
|
159
|
+
projects: [
|
|
160
|
+
{ id: 'project-1', name: 'Updated Project' },
|
|
161
|
+
{ id: 'project-2' }, // No name provided, should be skipped
|
|
162
|
+
],
|
|
163
|
+
}, mockTodoistApi);
|
|
164
|
+
// Should only call API once for the project with actual updates
|
|
165
|
+
expect(mockTodoistApi.updateProject).toHaveBeenCalledTimes(1);
|
|
166
|
+
expect(mockTodoistApi.updateProject).toHaveBeenCalledWith('project-1', {
|
|
167
|
+
name: 'Updated Project',
|
|
168
|
+
});
|
|
169
|
+
const textContent = extractTextContent(result);
|
|
170
|
+
expect(textContent).toMatchSnapshot();
|
|
171
|
+
expect(textContent).toContain('Updated 1 project (1 skipped - no changes):');
|
|
172
|
+
expect(textContent).toContain('Updated Project (id=project-1)');
|
|
173
|
+
// Verify structured content reflects skipped count
|
|
174
|
+
const structuredContent = extractStructuredContent(result);
|
|
175
|
+
expect(structuredContent).toEqual(expect.objectContaining({
|
|
176
|
+
appliedOperations: {
|
|
177
|
+
updateCount: 1,
|
|
178
|
+
skippedCount: 1,
|
|
179
|
+
},
|
|
180
|
+
}));
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe('error handling', () => {
|
|
184
|
+
it('should propagate API errors', async () => {
|
|
185
|
+
const apiError = new Error('API Error: Project not found');
|
|
186
|
+
mockTodoistApi.updateProject.mockRejectedValue(apiError);
|
|
187
|
+
await expect(updateProjects.execute({ projects: [{ id: 'nonexistent', name: 'New Name' }] }, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
|
|
188
|
+
});
|
|
189
|
+
it('should handle partial failures in multiple projects', async () => {
|
|
190
|
+
const mockProject = createMockProject({
|
|
191
|
+
id: 'project-1',
|
|
192
|
+
name: 'Updated Project',
|
|
193
|
+
});
|
|
194
|
+
mockTodoistApi.updateProject
|
|
195
|
+
.mockResolvedValueOnce(mockProject)
|
|
196
|
+
.mockRejectedValueOnce(new Error('API Error: Project not found'));
|
|
197
|
+
await expect(updateProjects.execute({
|
|
198
|
+
projects: [
|
|
199
|
+
{ id: 'project-1', name: 'Updated Project' },
|
|
200
|
+
{ id: 'nonexistent', name: 'New Name' },
|
|
201
|
+
],
|
|
202
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Project not found');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-sections.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/update-sections.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { createMockSection, extractStructuredContent, extractTextContent, } from '../../utils/test-helpers.js';
|
|
3
|
+
import { ToolNames } from '../../utils/tool-names.js';
|
|
4
|
+
import { updateSections } from '../update-sections.js';
|
|
5
|
+
// Mock the Todoist API
|
|
6
|
+
const mockTodoistApi = {
|
|
7
|
+
updateSection: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
const { FIND_TASKS, UPDATE_SECTIONS, GET_OVERVIEW } = ToolNames;
|
|
10
|
+
describe(`${UPDATE_SECTIONS} tool`, () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
describe('updating a single section', () => {
|
|
15
|
+
it('should update a section when id and name are provided', async () => {
|
|
16
|
+
const mockApiResponse = {
|
|
17
|
+
id: 'existing-section-123',
|
|
18
|
+
projectId: '6cfCcrrCFg2xP94Q',
|
|
19
|
+
sectionOrder: 1,
|
|
20
|
+
userId: 'test-user',
|
|
21
|
+
addedAt: '2024-01-01T00:00:00Z',
|
|
22
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
23
|
+
archivedAt: null,
|
|
24
|
+
isArchived: false,
|
|
25
|
+
isDeleted: false,
|
|
26
|
+
isCollapsed: false,
|
|
27
|
+
name: 'Updated Section Name',
|
|
28
|
+
};
|
|
29
|
+
mockTodoistApi.updateSection.mockResolvedValue(mockApiResponse);
|
|
30
|
+
const result = await updateSections.execute({ sections: [{ id: 'existing-section-123', name: 'Updated Section Name' }] }, mockTodoistApi);
|
|
31
|
+
expect(mockTodoistApi.updateSection).toHaveBeenCalledWith('existing-section-123', {
|
|
32
|
+
name: 'Updated Section Name',
|
|
33
|
+
});
|
|
34
|
+
const textContent = extractTextContent(result);
|
|
35
|
+
expect(textContent).toMatchSnapshot();
|
|
36
|
+
expect(textContent).toContain('Updated 1 section:');
|
|
37
|
+
expect(textContent).toContain('Updated Section Name (id=existing-section-123, projectId=6cfCcrrCFg2xP94Q)');
|
|
38
|
+
expect(textContent).toContain(`Use ${FIND_TASKS} with sectionId=existing-section-123`);
|
|
39
|
+
// Verify structured content
|
|
40
|
+
const structuredContent = extractStructuredContent(result);
|
|
41
|
+
expect(structuredContent).toEqual(expect.objectContaining({
|
|
42
|
+
sections: [mockApiResponse],
|
|
43
|
+
totalCount: 1,
|
|
44
|
+
updatedSectionIds: ['existing-section-123'],
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('updating multiple sections', () => {
|
|
49
|
+
it('should update multiple sections and return mapped results', async () => {
|
|
50
|
+
const mockSections = [
|
|
51
|
+
createMockSection({
|
|
52
|
+
id: 'section-1',
|
|
53
|
+
projectId: 'project-1',
|
|
54
|
+
name: 'Updated First Section',
|
|
55
|
+
}),
|
|
56
|
+
createMockSection({
|
|
57
|
+
id: 'section-2',
|
|
58
|
+
projectId: 'project-1',
|
|
59
|
+
name: 'Updated Second Section',
|
|
60
|
+
}),
|
|
61
|
+
createMockSection({
|
|
62
|
+
id: 'section-3',
|
|
63
|
+
projectId: 'project-2',
|
|
64
|
+
name: 'Updated Third Section',
|
|
65
|
+
}),
|
|
66
|
+
];
|
|
67
|
+
const [section1, section2, section3] = mockSections;
|
|
68
|
+
mockTodoistApi.updateSection
|
|
69
|
+
.mockResolvedValueOnce(section1)
|
|
70
|
+
.mockResolvedValueOnce(section2)
|
|
71
|
+
.mockResolvedValueOnce(section3);
|
|
72
|
+
const result = await updateSections.execute({
|
|
73
|
+
sections: [
|
|
74
|
+
{ id: 'section-1', name: 'Updated First Section' },
|
|
75
|
+
{ id: 'section-2', name: 'Updated Second Section' },
|
|
76
|
+
{ id: 'section-3', name: 'Updated Third Section' },
|
|
77
|
+
],
|
|
78
|
+
}, mockTodoistApi);
|
|
79
|
+
// Verify API was called correctly for each section
|
|
80
|
+
expect(mockTodoistApi.updateSection).toHaveBeenCalledTimes(3);
|
|
81
|
+
expect(mockTodoistApi.updateSection).toHaveBeenNthCalledWith(1, 'section-1', {
|
|
82
|
+
name: 'Updated First Section',
|
|
83
|
+
});
|
|
84
|
+
expect(mockTodoistApi.updateSection).toHaveBeenNthCalledWith(2, 'section-2', {
|
|
85
|
+
name: 'Updated Second Section',
|
|
86
|
+
});
|
|
87
|
+
expect(mockTodoistApi.updateSection).toHaveBeenNthCalledWith(3, 'section-3', {
|
|
88
|
+
name: 'Updated Third Section',
|
|
89
|
+
});
|
|
90
|
+
const textContent = extractTextContent(result);
|
|
91
|
+
expect(textContent).toMatchSnapshot();
|
|
92
|
+
expect(textContent).toContain('Updated 3 sections:');
|
|
93
|
+
expect(textContent).toContain('Updated First Section (id=section-1, projectId=project-1)');
|
|
94
|
+
expect(textContent).toContain('Updated Second Section (id=section-2, projectId=project-1)');
|
|
95
|
+
expect(textContent).toContain('Updated Third Section (id=section-3, projectId=project-2)');
|
|
96
|
+
// Verify structured content
|
|
97
|
+
const structuredContent = extractStructuredContent(result);
|
|
98
|
+
expect(structuredContent).toEqual(expect.objectContaining({
|
|
99
|
+
sections: mockSections,
|
|
100
|
+
totalCount: 3,
|
|
101
|
+
updatedSectionIds: ['section-1', 'section-2', 'section-3'],
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
it('should handle sections from the same project', async () => {
|
|
105
|
+
const mockSections = [
|
|
106
|
+
createMockSection({
|
|
107
|
+
id: 'section-1',
|
|
108
|
+
projectId: 'same-project',
|
|
109
|
+
name: 'Backlog',
|
|
110
|
+
}),
|
|
111
|
+
createMockSection({
|
|
112
|
+
id: 'section-2',
|
|
113
|
+
projectId: 'same-project',
|
|
114
|
+
name: 'Done',
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
117
|
+
const [section1, section2] = mockSections;
|
|
118
|
+
mockTodoistApi.updateSection
|
|
119
|
+
.mockResolvedValueOnce(section1)
|
|
120
|
+
.mockResolvedValueOnce(section2);
|
|
121
|
+
const result = await updateSections.execute({
|
|
122
|
+
sections: [
|
|
123
|
+
{ id: 'section-1', name: 'Backlog' },
|
|
124
|
+
{ id: 'section-2', name: 'Done' },
|
|
125
|
+
],
|
|
126
|
+
}, mockTodoistApi);
|
|
127
|
+
const textContent = extractTextContent(result);
|
|
128
|
+
expect(textContent).toMatchSnapshot();
|
|
129
|
+
expect(textContent).toContain('Updated 2 sections:');
|
|
130
|
+
expect(textContent).toContain(`Use ${GET_OVERVIEW} with projectId=same-project`);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('error handling', () => {
|
|
134
|
+
it('should propagate API errors', async () => {
|
|
135
|
+
const apiError = new Error('API Error: Section not found');
|
|
136
|
+
mockTodoistApi.updateSection.mockRejectedValue(apiError);
|
|
137
|
+
await expect(updateSections.execute({ sections: [{ id: 'nonexistent', name: 'New Name' }] }, mockTodoistApi)).rejects.toThrow('API Error: Section not found');
|
|
138
|
+
});
|
|
139
|
+
it('should handle partial failures in multiple sections', async () => {
|
|
140
|
+
const mockSection = createMockSection({
|
|
141
|
+
id: 'section-1',
|
|
142
|
+
projectId: 'project-1',
|
|
143
|
+
name: 'Updated Section',
|
|
144
|
+
});
|
|
145
|
+
mockTodoistApi.updateSection
|
|
146
|
+
.mockResolvedValueOnce(mockSection)
|
|
147
|
+
.mockRejectedValueOnce(new Error('API Error: Section not found'));
|
|
148
|
+
await expect(updateSections.execute({
|
|
149
|
+
sections: [
|
|
150
|
+
{ id: 'section-1', name: 'Updated Section' },
|
|
151
|
+
{ id: 'nonexistent', name: 'New Name' },
|
|
152
|
+
],
|
|
153
|
+
}, mockTodoistApi)).rejects.toThrow('API Error: Section not found');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Comment } from '@doist/todoist-api-typescript';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
declare const addComments: {
|
|
4
|
+
name: "add-comments";
|
|
5
|
+
description: string;
|
|
6
|
+
parameters: {
|
|
7
|
+
comments: z.ZodArray<z.ZodObject<{
|
|
8
|
+
taskId: z.ZodOptional<z.ZodString>;
|
|
9
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
10
|
+
content: z.ZodString;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
content: string;
|
|
13
|
+
projectId?: string | undefined;
|
|
14
|
+
taskId?: string | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
content: string;
|
|
17
|
+
projectId?: string | undefined;
|
|
18
|
+
taskId?: string | undefined;
|
|
19
|
+
}>, "many">;
|
|
20
|
+
};
|
|
21
|
+
execute(args: {
|
|
22
|
+
comments: {
|
|
23
|
+
content: string;
|
|
24
|
+
projectId?: string | undefined;
|
|
25
|
+
taskId?: string | undefined;
|
|
26
|
+
}[];
|
|
27
|
+
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
28
|
+
content: {
|
|
29
|
+
type: "text";
|
|
30
|
+
text: string;
|
|
31
|
+
}[];
|
|
32
|
+
structuredContent: {
|
|
33
|
+
comments: Comment[];
|
|
34
|
+
totalCount: number;
|
|
35
|
+
addedCommentIds: string[];
|
|
36
|
+
};
|
|
37
|
+
} | {
|
|
38
|
+
content: ({
|
|
39
|
+
type: "text";
|
|
40
|
+
text: string;
|
|
41
|
+
mimeType?: undefined;
|
|
42
|
+
} | {
|
|
43
|
+
type: "text";
|
|
44
|
+
mimeType: string;
|
|
45
|
+
text: string;
|
|
46
|
+
})[];
|
|
47
|
+
structuredContent?: undefined;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
export { addComments };
|
|
51
|
+
//# sourceMappingURL=add-comments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-comments.d.ts","sourceRoot":"","sources":["../../src/tools/add-comments.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,+BAA+B,CAAA;AAC5E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAkBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CyB,CAAA;AA6C1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getToolOutput } from '../mcp-helpers.js';
|
|
3
|
+
import { formatNextSteps } from '../utils/response-builders.js';
|
|
4
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
5
|
+
const { FIND_COMMENTS, UPDATE_COMMENTS, DELETE_OBJECT } = ToolNames;
|
|
6
|
+
const CommentSchema = z.object({
|
|
7
|
+
taskId: z.string().optional().describe('The ID of the task to comment on.'),
|
|
8
|
+
projectId: z.string().optional().describe('The ID of the project to comment on.'),
|
|
9
|
+
content: z.string().min(1).describe('The content of the comment.'),
|
|
10
|
+
});
|
|
11
|
+
const ArgsSchema = {
|
|
12
|
+
comments: z.array(CommentSchema).min(1).describe('The array of comments to add.'),
|
|
13
|
+
};
|
|
14
|
+
const addComments = {
|
|
15
|
+
name: ToolNames.ADD_COMMENTS,
|
|
16
|
+
description: 'Add multiple comments to tasks or projects. Each comment must specify either taskId or projectId.',
|
|
17
|
+
parameters: ArgsSchema,
|
|
18
|
+
async execute(args, client) {
|
|
19
|
+
const { comments } = args;
|
|
20
|
+
// Validate each comment
|
|
21
|
+
for (const [index, comment] of comments.entries()) {
|
|
22
|
+
if (!comment.taskId && !comment.projectId) {
|
|
23
|
+
throw new Error(`Comment ${index + 1}: Either taskId or projectId must be provided.`);
|
|
24
|
+
}
|
|
25
|
+
if (comment.taskId && comment.projectId) {
|
|
26
|
+
throw new Error(`Comment ${index + 1}: Cannot provide both taskId and projectId. Choose one.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const addCommentPromises = comments.map(async ({ content, taskId, projectId }) => await client.addComment({
|
|
30
|
+
content,
|
|
31
|
+
...(taskId ? { taskId } : { projectId }),
|
|
32
|
+
}));
|
|
33
|
+
const newComments = await Promise.all(addCommentPromises);
|
|
34
|
+
const textContent = generateTextContent({ comments: newComments });
|
|
35
|
+
return getToolOutput({
|
|
36
|
+
textContent,
|
|
37
|
+
structuredContent: {
|
|
38
|
+
comments: newComments,
|
|
39
|
+
totalCount: newComments.length,
|
|
40
|
+
addedCommentIds: newComments.map((comment) => comment.id),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
function generateTextContent({ comments, }) {
|
|
46
|
+
// Group comments by entity type and count
|
|
47
|
+
const taskComments = comments.filter((c) => c.taskId).length;
|
|
48
|
+
const projectComments = comments.filter((c) => c.projectId).length;
|
|
49
|
+
// Generate summary text
|
|
50
|
+
const parts = [];
|
|
51
|
+
if (taskComments > 0) {
|
|
52
|
+
const commentsLabel = taskComments > 1 ? 'comments' : 'comment';
|
|
53
|
+
parts.push(`${taskComments} task ${commentsLabel}`);
|
|
54
|
+
}
|
|
55
|
+
if (projectComments > 0) {
|
|
56
|
+
const commentsLabel = projectComments > 1 ? 'comments' : 'comment';
|
|
57
|
+
parts.push(`${projectComments} project ${commentsLabel}`);
|
|
58
|
+
}
|
|
59
|
+
const summary = parts.length > 0 ? `Added ${parts.join(' and ')}` : 'No comments added';
|
|
60
|
+
// Context-aware next steps
|
|
61
|
+
const nextSteps = [];
|
|
62
|
+
if (comments.length > 0) {
|
|
63
|
+
if (comments.length === 1 && comments[0]) {
|
|
64
|
+
const comment = comments[0];
|
|
65
|
+
const targetId = comment.taskId || comment.projectId || '';
|
|
66
|
+
const targetType = comment.taskId ? 'task' : 'project';
|
|
67
|
+
nextSteps.push(`Use ${FIND_COMMENTS} with ${targetType}Id=${targetId} to see all comments`);
|
|
68
|
+
nextSteps.push(`Use ${UPDATE_COMMENTS} with id=${comment.id} to edit content`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
nextSteps.push(`Use ${FIND_COMMENTS} to view comments by task or project`);
|
|
72
|
+
nextSteps.push(`Use ${UPDATE_COMMENTS} to edit any comment content`);
|
|
73
|
+
}
|
|
74
|
+
nextSteps.push(`Use ${DELETE_OBJECT} with type=comment to remove comments`);
|
|
75
|
+
}
|
|
76
|
+
const next = formatNextSteps(nextSteps);
|
|
77
|
+
return `${summary}\n${next}`;
|
|
78
|
+
}
|
|
79
|
+
export { addComments };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { PersonalProject, WorkspaceProject } from '@doist/todoist-api-typescript';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
declare const addProjects: {
|
|
4
|
+
name: "add-projects";
|
|
5
|
+
description: string;
|
|
6
|
+
parameters: {
|
|
7
|
+
projects: z.ZodArray<z.ZodObject<{
|
|
8
|
+
name: z.ZodString;
|
|
9
|
+
isFavorite: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
viewStyle: z.ZodOptional<z.ZodEnum<["list", "board", "calendar"]>>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
name: string;
|
|
13
|
+
isFavorite?: boolean | undefined;
|
|
14
|
+
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
name: string;
|
|
17
|
+
isFavorite?: boolean | undefined;
|
|
18
|
+
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
19
|
+
}>, "many">;
|
|
20
|
+
};
|
|
21
|
+
execute({ projects }: {
|
|
22
|
+
projects: {
|
|
23
|
+
name: string;
|
|
24
|
+
isFavorite?: boolean | undefined;
|
|
25
|
+
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
26
|
+
}[];
|
|
27
|
+
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
28
|
+
content: {
|
|
29
|
+
type: "text";
|
|
30
|
+
text: string;
|
|
31
|
+
}[];
|
|
32
|
+
structuredContent: {
|
|
33
|
+
projects: (PersonalProject | WorkspaceProject)[];
|
|
34
|
+
totalCount: number;
|
|
35
|
+
};
|
|
36
|
+
} | {
|
|
37
|
+
content: ({
|
|
38
|
+
type: "text";
|
|
39
|
+
text: string;
|
|
40
|
+
mimeType?: undefined;
|
|
41
|
+
} | {
|
|
42
|
+
type: "text";
|
|
43
|
+
mimeType: string;
|
|
44
|
+
text: string;
|
|
45
|
+
})[];
|
|
46
|
+
structuredContent?: undefined;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
export { addProjects };
|
|
50
|
+
//# sourceMappingURL=add-projects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-projects.d.ts","sourceRoot":"","sources":["../../src/tools/add-projects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AACtF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAwBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgByB,CAAA;AAmC1C,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getToolOutput } from '../mcp-helpers.js';
|
|
3
|
+
import { formatNextSteps } from '../utils/response-builders.js';
|
|
4
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
5
|
+
const { ADD_SECTIONS, ADD_TASKS, FIND_PROJECTS, GET_OVERVIEW } = ToolNames;
|
|
6
|
+
const ProjectSchema = z.object({
|
|
7
|
+
name: z.string().min(1).describe('The name of the project.'),
|
|
8
|
+
isFavorite: z
|
|
9
|
+
.boolean()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Whether the project is a favorite. Defaults to false.'),
|
|
12
|
+
viewStyle: z
|
|
13
|
+
.enum(['list', 'board', 'calendar'])
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('The project view style. Defaults to "list".'),
|
|
16
|
+
});
|
|
17
|
+
const ArgsSchema = {
|
|
18
|
+
projects: z.array(ProjectSchema).min(1).describe('The array of projects to add.'),
|
|
19
|
+
};
|
|
20
|
+
const addProjects = {
|
|
21
|
+
name: ToolNames.ADD_PROJECTS,
|
|
22
|
+
description: 'Add one or more new projects.',
|
|
23
|
+
parameters: ArgsSchema,
|
|
24
|
+
async execute({ projects }, client) {
|
|
25
|
+
const newProjects = await Promise.all(projects.map((project) => client.addProject(project)));
|
|
26
|
+
const textContent = generateTextContent({ projects: newProjects });
|
|
27
|
+
return getToolOutput({
|
|
28
|
+
textContent,
|
|
29
|
+
structuredContent: {
|
|
30
|
+
projects: newProjects,
|
|
31
|
+
totalCount: newProjects.length,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
function generateTextContent({ projects, }) {
|
|
37
|
+
const count = projects.length;
|
|
38
|
+
const projectList = projects.map((project) => `• ${project.name} (id=${project.id})`).join('\n');
|
|
39
|
+
const summary = `Added ${count} project${count === 1 ? '' : 's'}:\n${projectList}`;
|
|
40
|
+
// Context-aware next steps for new projects
|
|
41
|
+
const nextSteps = [];
|
|
42
|
+
if (count === 1) {
|
|
43
|
+
const project = projects[0];
|
|
44
|
+
if (project) {
|
|
45
|
+
nextSteps.push(`Use ${ADD_SECTIONS} to organize new project with sections`);
|
|
46
|
+
nextSteps.push(`Use ${ADD_TASKS} to add your first tasks to this project`);
|
|
47
|
+
nextSteps.push(`Use ${GET_OVERVIEW} with projectId=${project.id} to see project structure`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
nextSteps.push(`Use ${ADD_SECTIONS} to organize these projects with sections`);
|
|
52
|
+
nextSteps.push(`Use ${ADD_TASKS} to add tasks to these projects`);
|
|
53
|
+
nextSteps.push(`Use ${FIND_PROJECTS} to see all projects including the new ones`);
|
|
54
|
+
nextSteps.push(`Use ${GET_OVERVIEW} to see updated project hierarchy`);
|
|
55
|
+
}
|
|
56
|
+
const next = formatNextSteps(nextSteps);
|
|
57
|
+
return `${summary}\n${next}`;
|
|
58
|
+
}
|
|
59
|
+
export { addProjects };
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import type { Section } from '@doist/todoist-api-typescript';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
declare const
|
|
4
|
-
name: "
|
|
3
|
+
declare const addSections: {
|
|
4
|
+
name: "add-sections";
|
|
5
5
|
description: string;
|
|
6
6
|
parameters: {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
sections: z.ZodArray<z.ZodObject<{
|
|
8
|
+
name: z.ZodString;
|
|
9
|
+
projectId: z.ZodString;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
name: string;
|
|
12
|
+
projectId: string;
|
|
13
|
+
}, {
|
|
14
|
+
name: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
}>, "many">;
|
|
10
17
|
};
|
|
11
|
-
execute(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
execute({ sections }: {
|
|
19
|
+
sections: {
|
|
20
|
+
name: string;
|
|
21
|
+
projectId: string;
|
|
22
|
+
}[];
|
|
15
23
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
16
24
|
content: {
|
|
17
25
|
type: "text";
|
|
18
26
|
text: string;
|
|
19
27
|
}[];
|
|
20
28
|
structuredContent: {
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
sections: Section[];
|
|
30
|
+
totalCount: number;
|
|
23
31
|
};
|
|
24
32
|
} | {
|
|
25
33
|
content: ({
|
|
@@ -34,5 +42,5 @@ declare const manageSections: {
|
|
|
34
42
|
structuredContent?: undefined;
|
|
35
43
|
}>;
|
|
36
44
|
};
|
|
37
|
-
export {
|
|
38
|
-
//# sourceMappingURL=
|
|
45
|
+
export { addSections };
|
|
46
|
+
//# sourceMappingURL=add-sections.d.ts.map
|