@doist/todoist-ai 4.9.0 → 4.9.2
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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/tool-helpers.d.ts +20 -1
- package/dist/tool-helpers.d.ts.map +1 -1
- package/dist/tool-helpers.js +24 -3
- package/dist/tool-helpers.test.js +47 -0
- package/dist/tools/__tests__/add-tasks.test.js +38 -0
- package/dist/tools/__tests__/find-tasks-by-date.test.js +80 -5
- package/dist/tools/__tests__/find-tasks.test.js +191 -1
- package/dist/tools/add-tasks.js +2 -2
- package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
- package/dist/tools/find-tasks-by-date.js +11 -4
- package/dist/tools/find-tasks.d.ts +3 -0
- package/dist/tools/find-tasks.d.ts.map +1 -1
- package/dist/tools/find-tasks.js +19 -10
- package/dist/utils/test-helpers.d.ts +6 -1
- package/dist/utils/test-helpers.d.ts.map +1 -1
- package/dist/utils/test-helpers.js +40 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -257,6 +257,7 @@ declare const tools: {
|
|
|
257
257
|
sectionId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
258
258
|
parentId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
259
259
|
responsibleUser: import("zod").ZodOptional<import("zod").ZodString>;
|
|
260
|
+
responsibleUserFiltering: import("zod").ZodOptional<import("zod").ZodEnum<["assigned", "unassignedOrMe", "all"]>>;
|
|
260
261
|
limit: import("zod").ZodDefault<import("zod").ZodNumber>;
|
|
261
262
|
cursor: import("zod").ZodOptional<import("zod").ZodString>;
|
|
262
263
|
};
|
|
@@ -264,6 +265,7 @@ declare const tools: {
|
|
|
264
265
|
limit: number;
|
|
265
266
|
projectId?: string | undefined;
|
|
266
267
|
parentId?: string | undefined;
|
|
268
|
+
responsibleUserFiltering?: "assigned" | "unassignedOrMe" | "all" | undefined;
|
|
267
269
|
sectionId?: string | undefined;
|
|
268
270
|
labels?: string[] | undefined;
|
|
269
271
|
cursor?: string | undefined;
|
|
@@ -298,6 +300,7 @@ declare const tools: {
|
|
|
298
300
|
limit: number;
|
|
299
301
|
projectId?: string | undefined;
|
|
300
302
|
parentId?: string | undefined;
|
|
303
|
+
responsibleUserFiltering?: "assigned" | "unassignedOrMe" | "all" | undefined;
|
|
301
304
|
sectionId?: string | undefined;
|
|
302
305
|
labels?: string[] | undefined;
|
|
303
306
|
cursor?: string | undefined;
|
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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"}
|
package/dist/tool-helpers.d.ts
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MoveTaskArgs, PersonalProject, Task, TodoistApi, WorkspaceProject } from '@doist/todoist-api-typescript';
|
|
2
|
+
export declare const RESPONSIBLE_USER_FILTERING: readonly ["assigned", "unassignedOrMe", "all"];
|
|
3
|
+
export type ResponsibleUserFiltering = (typeof RESPONSIBLE_USER_FILTERING)[number];
|
|
2
4
|
export type Project = PersonalProject | WorkspaceProject;
|
|
3
5
|
export declare function isPersonalProject(project: Project): project is PersonalProject;
|
|
4
6
|
export declare function isWorkspaceProject(project: Project): project is WorkspaceProject;
|
|
7
|
+
/**
|
|
8
|
+
* Filters tasks based on responsible user logic:
|
|
9
|
+
* - If resolvedAssigneeId is provided: returns only tasks assigned to that user
|
|
10
|
+
* - If no resolvedAssigneeId: returns only unassigned tasks or tasks assigned to current user
|
|
11
|
+
* @param tasks - Array of tasks to filter (must have responsibleUid property)
|
|
12
|
+
* @param resolvedAssigneeId - The resolved assignee ID to filter by (optional)
|
|
13
|
+
* @param currentUserId - The current authenticated user's ID
|
|
14
|
+
* @returns Filtered array of tasks
|
|
15
|
+
*/
|
|
16
|
+
export declare function filterTasksByResponsibleUser<T extends {
|
|
17
|
+
responsibleUid: string | null;
|
|
18
|
+
}>({ tasks, resolvedAssigneeId, currentUserId, responsibleUserFiltering, }: {
|
|
19
|
+
tasks: T[];
|
|
20
|
+
resolvedAssigneeId: string | undefined;
|
|
21
|
+
currentUserId: string;
|
|
22
|
+
responsibleUserFiltering?: ResponsibleUserFiltering;
|
|
23
|
+
}): T[];
|
|
5
24
|
/**
|
|
6
25
|
* Creates a MoveTaskArgs object from move parameters, validating that exactly one is provided.
|
|
7
26
|
* @param taskId - The task ID (used for error messages)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-helpers.d.ts","sourceRoot":"","sources":["../src/tool-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"tool-helpers.d.ts","sourceRoot":"","sources":["../src/tool-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,YAAY,EACZ,eAAe,EACf,IAAI,EACJ,UAAU,EACV,gBAAgB,EACnB,MAAM,+BAA+B,CAAA;AAItC,eAAO,MAAM,0BAA0B,gDAAiD,CAAA;AACxF,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAA;AAElF,MAAM,MAAM,OAAO,GAAG,eAAe,GAAG,gBAAgB,CAAA;AAExD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,eAAe,CAE9E;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,gBAAgB,CAEhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,SAAS;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,EACtF,KAAK,EACL,kBAAkB,EAClB,aAAa,EACb,wBAA2C,GAC9C,EAAE;IACC,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,aAAa,EAAE,MAAM,CAAA;IACrB,wBAAwB,CAAC,EAAE,wBAAwB,CAAA;CACtD,GAAG,CAAC,EAAE,CAUN;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAC9B,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAClB,YAAY,CAsBd;AAED;;;;GAIG;AACH,iBAAS,OAAO,CAAC,IAAI,EAAE,IAAI;;;;;;;;;;;;;;EAgB1B;AAED;;;;GAIG;AACH,iBAAS,UAAU,CAAC,OAAO,EAAE,OAAO;;;;;;;;;EAWnC;AAWD,iBAAe,gBAAgB,CAAC,EAC5B,MAAM,EACN,KAAK,EACL,KAAK,EACL,MAAM,GACT,EAAE;IACC,MAAM,EAAE,UAAU,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;;;;;;;;;;;;;;;;;GAyBA;AAED,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,UAAU,EAAE,CAAA"}
|
package/dist/tool-helpers.js
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
|
-
import { getSanitizedContent, } from '@doist/todoist-api-typescript';
|
|
2
1
|
import z from 'zod';
|
|
3
2
|
import { formatDuration } from './utils/duration-parser.js';
|
|
3
|
+
export const RESPONSIBLE_USER_FILTERING = ['assigned', 'unassignedOrMe', 'all'];
|
|
4
4
|
export function isPersonalProject(project) {
|
|
5
5
|
return 'inboxProject' in project;
|
|
6
6
|
}
|
|
7
7
|
export function isWorkspaceProject(project) {
|
|
8
8
|
return 'accessLevel' in project;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Filters tasks based on responsible user logic:
|
|
12
|
+
* - If resolvedAssigneeId is provided: returns only tasks assigned to that user
|
|
13
|
+
* - If no resolvedAssigneeId: returns only unassigned tasks or tasks assigned to current user
|
|
14
|
+
* @param tasks - Array of tasks to filter (must have responsibleUid property)
|
|
15
|
+
* @param resolvedAssigneeId - The resolved assignee ID to filter by (optional)
|
|
16
|
+
* @param currentUserId - The current authenticated user's ID
|
|
17
|
+
* @returns Filtered array of tasks
|
|
18
|
+
*/
|
|
19
|
+
export function filterTasksByResponsibleUser({ tasks, resolvedAssigneeId, currentUserId, responsibleUserFiltering = 'unassignedOrMe', }) {
|
|
20
|
+
if (resolvedAssigneeId) {
|
|
21
|
+
// If responsibleUser provided, only return tasks assigned to that user
|
|
22
|
+
return tasks.filter((task) => task.responsibleUid === resolvedAssigneeId);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// If no responsibleUser, only return unassigned tasks or tasks assigned to current user
|
|
26
|
+
return responsibleUserFiltering === 'unassignedOrMe'
|
|
27
|
+
? tasks.filter((task) => !task.responsibleUid || task.responsibleUid === currentUserId)
|
|
28
|
+
: tasks;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
10
31
|
/**
|
|
11
32
|
* Creates a MoveTaskArgs object from move parameters, validating that exactly one is provided.
|
|
12
33
|
* @param taskId - The task ID (used for error messages)
|
|
@@ -43,8 +64,8 @@ export function createMoveTaskArgs(taskId, projectId, sectionId, parentId) {
|
|
|
43
64
|
function mapTask(task) {
|
|
44
65
|
return {
|
|
45
66
|
id: task.id,
|
|
46
|
-
content:
|
|
47
|
-
description:
|
|
67
|
+
content: task.content,
|
|
68
|
+
description: task.description,
|
|
48
69
|
dueDate: task.due?.date,
|
|
49
70
|
recurring: task.due?.isRecurring && task.due.string ? task.due.string : false,
|
|
50
71
|
priority: task.priority,
|
|
@@ -73,6 +73,53 @@ describe('shared utilities', () => {
|
|
|
73
73
|
const result = mapTask(mockTask);
|
|
74
74
|
expect(result.duration).toBe('2h30m');
|
|
75
75
|
});
|
|
76
|
+
it('should preserve markdown links and formatting in content and description', () => {
|
|
77
|
+
const mockTask = {
|
|
78
|
+
id: '123',
|
|
79
|
+
content: 'Task with **bold** and [link](https://example.com)',
|
|
80
|
+
description: `Rich markdown description:
|
|
81
|
+
|
|
82
|
+
### Links
|
|
83
|
+
[Wikipedia](https://en.wikipedia.org/wiki/Test)
|
|
84
|
+
[GitHub](https://github.com/example/repo)
|
|
85
|
+
|
|
86
|
+
### Formatting
|
|
87
|
+
**Bold text**
|
|
88
|
+
*Italic text*
|
|
89
|
+
\`code block\`
|
|
90
|
+
|
|
91
|
+
End of description.`,
|
|
92
|
+
projectId: 'proj-1',
|
|
93
|
+
sectionId: null,
|
|
94
|
+
parentId: null,
|
|
95
|
+
labels: [],
|
|
96
|
+
priority: 1,
|
|
97
|
+
};
|
|
98
|
+
const result = mapTask(mockTask);
|
|
99
|
+
// Verify exact preservation of markdown content
|
|
100
|
+
expect(result.content).toBe('Task with **bold** and [link](https://example.com)');
|
|
101
|
+
expect(result.description).toBe(`Rich markdown description:
|
|
102
|
+
|
|
103
|
+
### Links
|
|
104
|
+
[Wikipedia](https://en.wikipedia.org/wiki/Test)
|
|
105
|
+
[GitHub](https://github.com/example/repo)
|
|
106
|
+
|
|
107
|
+
### Formatting
|
|
108
|
+
**Bold text**
|
|
109
|
+
*Italic text*
|
|
110
|
+
\`code block\`
|
|
111
|
+
|
|
112
|
+
End of description.`);
|
|
113
|
+
// Verify specific URLs are preserved
|
|
114
|
+
expect(result.content).toContain('[link](https://example.com)');
|
|
115
|
+
expect(result.description).toContain('[Wikipedia](https://en.wikipedia.org/wiki/Test)');
|
|
116
|
+
expect(result.description).toContain('[GitHub](https://github.com/example/repo)');
|
|
117
|
+
// Verify other markdown formatting is preserved
|
|
118
|
+
expect(result.content).toContain('**bold**');
|
|
119
|
+
expect(result.description).toContain('**Bold text**');
|
|
120
|
+
expect(result.description).toContain('*Italic text*');
|
|
121
|
+
expect(result.description).toContain('`code block`');
|
|
122
|
+
});
|
|
76
123
|
});
|
|
77
124
|
describe('mapProject', () => {
|
|
78
125
|
it('should map a personal project correctly', () => {
|
|
@@ -448,4 +448,42 @@ describe(`${ADD_TASKS} tool`, () => {
|
|
|
448
448
|
expect(textContent).toContain(`Use ${GET_OVERVIEW} to see your updated project organization`);
|
|
449
449
|
});
|
|
450
450
|
});
|
|
451
|
+
describe('tasks without project context', () => {
|
|
452
|
+
it('should allow creating tasks with only content (goes to Inbox)', async () => {
|
|
453
|
+
const mockApiResponse = createMockTask({
|
|
454
|
+
id: '8485093758',
|
|
455
|
+
content: 'Simple inbox task',
|
|
456
|
+
url: 'https://todoist.com/showTask?id=8485093758',
|
|
457
|
+
addedAt: '2025-08-13T22:09:56.123456Z',
|
|
458
|
+
});
|
|
459
|
+
mockTodoistApi.addTask.mockResolvedValue(mockApiResponse);
|
|
460
|
+
const result = await addTasks.execute({
|
|
461
|
+
tasks: [
|
|
462
|
+
{
|
|
463
|
+
content: 'Simple inbox task',
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
}, mockTodoistApi);
|
|
467
|
+
expect(mockTodoistApi.addTask).toHaveBeenCalledWith({
|
|
468
|
+
content: 'Simple inbox task',
|
|
469
|
+
labels: undefined,
|
|
470
|
+
projectId: undefined,
|
|
471
|
+
sectionId: undefined,
|
|
472
|
+
parentId: undefined,
|
|
473
|
+
});
|
|
474
|
+
const textContent = extractTextContent(result);
|
|
475
|
+
expect(textContent).toContain('Added 1 task');
|
|
476
|
+
expect(textContent).toContain('Simple inbox task');
|
|
477
|
+
});
|
|
478
|
+
it('should prevent assignment without project context', async () => {
|
|
479
|
+
await expect(addTasks.execute({
|
|
480
|
+
tasks: [
|
|
481
|
+
{
|
|
482
|
+
content: 'Task with assignment but no project',
|
|
483
|
+
responsibleUser: 'user@example.com',
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
}, mockTodoistApi)).rejects.toThrow('Task "Task with assignment but no project": Cannot assign tasks without specifying project context. Please specify a projectId, sectionId, or parentId.');
|
|
487
|
+
});
|
|
488
|
+
});
|
|
451
489
|
});
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
2
|
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
-
import { createMappedTask, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
|
|
3
|
+
import { createMappedTask, createMockUser, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, } from '../../utils/test-helpers.js';
|
|
4
4
|
import { ToolNames } from '../../utils/tool-names.js';
|
|
5
5
|
import { findTasksByDate } from '../find-tasks-by-date.js';
|
|
6
6
|
// Mock the tool helpers
|
|
7
|
-
jest.mock('../../tool-helpers', () =>
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
jest.mock('../../tool-helpers', () => {
|
|
8
|
+
const actual = jest.requireActual('../../tool-helpers');
|
|
9
|
+
return {
|
|
10
|
+
getTasksByFilter: jest.fn(),
|
|
11
|
+
filterTasksByResponsibleUser: actual.filterTasksByResponsibleUser,
|
|
12
|
+
};
|
|
13
|
+
});
|
|
10
14
|
const mockGetTasksByFilter = getTasksByFilter;
|
|
11
15
|
// Mock the Todoist API (not directly used by find-tasks-by-date, but needed for type)
|
|
12
|
-
const mockTodoistApi = {
|
|
16
|
+
const mockTodoistApi = {
|
|
17
|
+
getUser: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
// Mock the Todoist User
|
|
20
|
+
const mockTodoistUser = createMockUser();
|
|
13
21
|
// Mock date-fns functions to make tests deterministic
|
|
14
22
|
jest.mock('date-fns', () => ({
|
|
15
23
|
addDays: jest.fn(() => new Date('2025-08-16')), // Return predictable end date
|
|
@@ -30,6 +38,7 @@ const { FIND_TASKS_BY_DATE, UPDATE_TASKS } = ToolNames;
|
|
|
30
38
|
describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
31
39
|
beforeEach(() => {
|
|
32
40
|
jest.clearAllMocks();
|
|
41
|
+
mockTodoistApi.getUser.mockResolvedValue(mockTodoistUser);
|
|
33
42
|
// Mock current date to make tests deterministic
|
|
34
43
|
jest.spyOn(Date, 'now').mockReturnValue(new Date('2025-08-15T10:00:00Z').getTime());
|
|
35
44
|
});
|
|
@@ -310,6 +319,72 @@ describe(`${FIND_TASKS_BY_DATE} tool`, () => {
|
|
|
310
319
|
expect(textContent).toMatchSnapshot();
|
|
311
320
|
});
|
|
312
321
|
});
|
|
322
|
+
describe('responsible user filtering', () => {
|
|
323
|
+
it('should filter results to show only unassigned tasks or tasks assigned to current user', async () => {
|
|
324
|
+
const mockTasks = [
|
|
325
|
+
createMappedTask({
|
|
326
|
+
id: TEST_IDS.TASK_1,
|
|
327
|
+
content: 'My task',
|
|
328
|
+
dueDate: '2025-08-15',
|
|
329
|
+
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
330
|
+
}),
|
|
331
|
+
createMappedTask({
|
|
332
|
+
id: TEST_IDS.TASK_2,
|
|
333
|
+
content: 'Unassigned task',
|
|
334
|
+
dueDate: '2025-08-15',
|
|
335
|
+
responsibleUid: null, // Unassigned
|
|
336
|
+
}),
|
|
337
|
+
createMappedTask({
|
|
338
|
+
id: TEST_IDS.TASK_3,
|
|
339
|
+
content: 'Someone else task',
|
|
340
|
+
dueDate: '2025-08-15',
|
|
341
|
+
responsibleUid: 'other-user-id', // Assigned to someone else
|
|
342
|
+
}),
|
|
343
|
+
];
|
|
344
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
345
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
346
|
+
const result = await findTasksByDate.execute({ startDate: 'today', daysCount: 1, limit: 50 }, mockTodoistApi);
|
|
347
|
+
const structuredContent = extractStructuredContent(result);
|
|
348
|
+
// Should only return tasks 1 and 2, not task 3
|
|
349
|
+
expect(structuredContent.tasks).toHaveLength(2);
|
|
350
|
+
expect(structuredContent.tasks.map((t) => t.id)).toEqual([
|
|
351
|
+
TEST_IDS.TASK_1,
|
|
352
|
+
TEST_IDS.TASK_2,
|
|
353
|
+
]);
|
|
354
|
+
});
|
|
355
|
+
it('should filter overdue results to show only unassigned tasks or tasks assigned to current user', async () => {
|
|
356
|
+
const mockTasks = [
|
|
357
|
+
createMappedTask({
|
|
358
|
+
id: TEST_IDS.TASK_1,
|
|
359
|
+
content: 'My overdue task',
|
|
360
|
+
dueDate: '2025-08-10',
|
|
361
|
+
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
362
|
+
}),
|
|
363
|
+
createMappedTask({
|
|
364
|
+
id: TEST_IDS.TASK_2,
|
|
365
|
+
content: 'Unassigned overdue task',
|
|
366
|
+
dueDate: '2025-08-10',
|
|
367
|
+
responsibleUid: null, // Unassigned
|
|
368
|
+
}),
|
|
369
|
+
createMappedTask({
|
|
370
|
+
id: TEST_IDS.TASK_3,
|
|
371
|
+
content: 'Someone else overdue task',
|
|
372
|
+
dueDate: '2025-08-10',
|
|
373
|
+
responsibleUid: 'other-user-id', // Assigned to someone else
|
|
374
|
+
}),
|
|
375
|
+
];
|
|
376
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
377
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
378
|
+
const result = await findTasksByDate.execute({ startDate: 'overdue', daysCount: 1, limit: 50 }, mockTodoistApi);
|
|
379
|
+
const structuredContent = extractStructuredContent(result);
|
|
380
|
+
// Should only return tasks 1 and 2, not task 3
|
|
381
|
+
expect(structuredContent.tasks).toHaveLength(2);
|
|
382
|
+
expect(structuredContent.tasks.map((t) => t.id)).toEqual([
|
|
383
|
+
TEST_IDS.TASK_1,
|
|
384
|
+
TEST_IDS.TASK_2,
|
|
385
|
+
]);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
313
388
|
describe('error handling', () => {
|
|
314
389
|
it.each([
|
|
315
390
|
{
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
2
|
import { getTasksByFilter } from '../../tool-helpers.js';
|
|
3
|
-
import { createMappedTask, createMockApiResponse, createMockTask, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, TODAY, } from '../../utils/test-helpers.js';
|
|
3
|
+
import { createMappedTask, createMockApiResponse, createMockTask, createMockUser, extractStructuredContent, extractTextContent, TEST_ERRORS, TEST_IDS, TODAY, } from '../../utils/test-helpers.js';
|
|
4
4
|
import { ToolNames } from '../../utils/tool-names.js';
|
|
5
|
+
import { resolveUserNameToId } from '../../utils/user-resolver.js';
|
|
5
6
|
import { findTasks } from '../find-tasks.js';
|
|
6
7
|
jest.mock('../../tool-helpers', () => {
|
|
7
8
|
const actual = jest.requireActual('../../tool-helpers');
|
|
8
9
|
return {
|
|
9
10
|
getTasksByFilter: jest.fn(),
|
|
10
11
|
mapTask: actual.mapTask,
|
|
12
|
+
filterTasksByResponsibleUser: actual.filterTasksByResponsibleUser,
|
|
11
13
|
};
|
|
12
14
|
});
|
|
15
|
+
jest.mock('../../utils/user-resolver', () => ({
|
|
16
|
+
resolveUserNameToId: jest.fn(),
|
|
17
|
+
}));
|
|
13
18
|
const { FIND_TASKS, UPDATE_TASKS, FIND_COMPLETED_TASKS } = ToolNames;
|
|
14
19
|
const mockGetTasksByFilter = getTasksByFilter;
|
|
20
|
+
const mockResolveUserNameToId = resolveUserNameToId;
|
|
15
21
|
// Mock the Todoist API
|
|
16
22
|
const mockTodoistApi = {
|
|
17
23
|
getTasks: jest.fn(),
|
|
24
|
+
getUser: jest.fn(),
|
|
18
25
|
};
|
|
26
|
+
// Mock the Todoist User
|
|
27
|
+
const mockTodoistUser = createMockUser();
|
|
19
28
|
describe(`${FIND_TASKS} tool`, () => {
|
|
20
29
|
beforeEach(() => {
|
|
30
|
+
mockTodoistApi.getUser.mockResolvedValue(mockTodoistUser);
|
|
21
31
|
jest.clearAllMocks();
|
|
22
32
|
});
|
|
23
33
|
describe('searching tasks', () => {
|
|
@@ -547,6 +557,186 @@ describe(`${FIND_TASKS} tool`, () => {
|
|
|
547
557
|
}));
|
|
548
558
|
});
|
|
549
559
|
});
|
|
560
|
+
describe('markdown content preservation', () => {
|
|
561
|
+
it('should preserve markdown links and formatting in task content and description', async () => {
|
|
562
|
+
const richMarkdownContent = 'Test **bold** task with [link](https://example.com)';
|
|
563
|
+
const richMarkdownDescription = `This is a **comprehensive test** of markdown syntax in Todoist task descriptions:
|
|
564
|
+
|
|
565
|
+
### Links
|
|
566
|
+
[Wikipedia - Test Link](https://en.wikipedia.org/wiki/Test)
|
|
567
|
+
[GitHub Repository](https://github.com/Doist/todoist-ai)
|
|
568
|
+
[Google Search](https://www.google.com)
|
|
569
|
+
|
|
570
|
+
### Text Formatting
|
|
571
|
+
**Bold text here**
|
|
572
|
+
*Italic text here*
|
|
573
|
+
***Bold and italic***
|
|
574
|
+
\`inline code\`
|
|
575
|
+
|
|
576
|
+
### Lists
|
|
577
|
+
- Bullet point 1
|
|
578
|
+
- Bullet point 2
|
|
579
|
+
- Nested item
|
|
580
|
+
|
|
581
|
+
1. Numbered item 1
|
|
582
|
+
2. Numbered item 2
|
|
583
|
+
|
|
584
|
+
End of test content.`;
|
|
585
|
+
const mockTasks = [
|
|
586
|
+
createMappedTask({
|
|
587
|
+
id: TEST_IDS.TASK_1,
|
|
588
|
+
content: richMarkdownContent,
|
|
589
|
+
description: richMarkdownDescription,
|
|
590
|
+
}),
|
|
591
|
+
];
|
|
592
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
593
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
594
|
+
const result = await findTasks.execute({ searchText: 'markdown test', limit: 10 }, mockTodoistApi);
|
|
595
|
+
const structuredContent = extractStructuredContent(result);
|
|
596
|
+
// Verify that markdown links and formatting are preserved exactly as provided
|
|
597
|
+
expect(structuredContent.tasks).toHaveLength(1);
|
|
598
|
+
expect(structuredContent.tasks).toEqual(expect.arrayContaining([
|
|
599
|
+
expect.objectContaining({
|
|
600
|
+
content: richMarkdownContent,
|
|
601
|
+
description: richMarkdownDescription,
|
|
602
|
+
}),
|
|
603
|
+
]));
|
|
604
|
+
});
|
|
605
|
+
it('should preserve markdown links in container-based searches', async () => {
|
|
606
|
+
const taskWithLinks = createMockTask({
|
|
607
|
+
id: TEST_IDS.TASK_1,
|
|
608
|
+
content: 'Task with [external link](https://todoist.com)',
|
|
609
|
+
description: 'See this [documentation](https://docs.example.com) for details.',
|
|
610
|
+
});
|
|
611
|
+
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse([taskWithLinks]));
|
|
612
|
+
const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_TEST, limit: 10 }, mockTodoistApi);
|
|
613
|
+
const structuredContent = extractStructuredContent(result);
|
|
614
|
+
// Verify URLs are preserved in container-based searches too
|
|
615
|
+
expect(structuredContent.tasks).toHaveLength(1);
|
|
616
|
+
expect(structuredContent.tasks).toEqual(expect.arrayContaining([
|
|
617
|
+
expect.objectContaining({
|
|
618
|
+
content: 'Task with [external link](https://todoist.com)',
|
|
619
|
+
description: 'See this [documentation](https://docs.example.com) for details.',
|
|
620
|
+
}),
|
|
621
|
+
]));
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
describe('responsible user filtering', () => {
|
|
625
|
+
describe('when no responsibleUser is provided', () => {
|
|
626
|
+
it('should filter text search results to show only unassigned tasks or tasks assigned to current user', async () => {
|
|
627
|
+
const mockTasks = [
|
|
628
|
+
createMappedTask({
|
|
629
|
+
id: TEST_IDS.TASK_1,
|
|
630
|
+
content: 'My task',
|
|
631
|
+
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
632
|
+
}),
|
|
633
|
+
createMappedTask({
|
|
634
|
+
id: TEST_IDS.TASK_2,
|
|
635
|
+
content: 'Unassigned task',
|
|
636
|
+
responsibleUid: null, // Unassigned
|
|
637
|
+
}),
|
|
638
|
+
createMappedTask({
|
|
639
|
+
id: TEST_IDS.TASK_3,
|
|
640
|
+
content: 'Someone else task',
|
|
641
|
+
responsibleUid: 'other-user-id', // Assigned to someone else
|
|
642
|
+
}),
|
|
643
|
+
];
|
|
644
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
645
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
646
|
+
const result = await findTasks.execute({ searchText: 'task', limit: 10 }, mockTodoistApi);
|
|
647
|
+
const structuredContent = extractStructuredContent(result);
|
|
648
|
+
// Should only return tasks 1 and 2, not task 3
|
|
649
|
+
expect(structuredContent.tasks).toHaveLength(2);
|
|
650
|
+
expect(structuredContent.tasks.map((t) => t.id)).toEqual([TEST_IDS.TASK_1, TEST_IDS.TASK_2]);
|
|
651
|
+
});
|
|
652
|
+
it('should filter container-based results to show only unassigned tasks or tasks assigned to current user', async () => {
|
|
653
|
+
const mockTasks = [
|
|
654
|
+
createMockTask({
|
|
655
|
+
id: TEST_IDS.TASK_1,
|
|
656
|
+
content: 'My project task',
|
|
657
|
+
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
658
|
+
}),
|
|
659
|
+
createMockTask({
|
|
660
|
+
id: TEST_IDS.TASK_2,
|
|
661
|
+
content: 'Unassigned project task',
|
|
662
|
+
responsibleUid: null, // Unassigned
|
|
663
|
+
}),
|
|
664
|
+
createMockTask({
|
|
665
|
+
id: TEST_IDS.TASK_3,
|
|
666
|
+
content: 'Someone else project task',
|
|
667
|
+
responsibleUid: 'other-user-id', // Assigned to someone else
|
|
668
|
+
}),
|
|
669
|
+
];
|
|
670
|
+
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(mockTasks));
|
|
671
|
+
const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_WORK, limit: 10 }, mockTodoistApi);
|
|
672
|
+
const structuredContent = extractStructuredContent(result);
|
|
673
|
+
// Should only return tasks 1 and 2, not task 3
|
|
674
|
+
expect(structuredContent.tasks).toHaveLength(2);
|
|
675
|
+
expect(structuredContent.tasks.map((t) => t.id)).toEqual([TEST_IDS.TASK_1, TEST_IDS.TASK_2]);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
describe('when responsibleUser is provided', () => {
|
|
679
|
+
it('should filter text search results to show only tasks assigned to specified user', async () => {
|
|
680
|
+
const mockTasks = [
|
|
681
|
+
createMappedTask({
|
|
682
|
+
id: TEST_IDS.TASK_1,
|
|
683
|
+
content: 'Task for John',
|
|
684
|
+
responsibleUid: 'specific-user-id', // Assigned to specified user
|
|
685
|
+
}),
|
|
686
|
+
createMappedTask({
|
|
687
|
+
id: TEST_IDS.TASK_2,
|
|
688
|
+
content: 'My task',
|
|
689
|
+
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
690
|
+
}),
|
|
691
|
+
createMappedTask({
|
|
692
|
+
id: TEST_IDS.TASK_3,
|
|
693
|
+
content: 'Unassigned task',
|
|
694
|
+
responsibleUid: null, // Unassigned
|
|
695
|
+
}),
|
|
696
|
+
];
|
|
697
|
+
const mockResponse = { tasks: mockTasks, nextCursor: null };
|
|
698
|
+
mockGetTasksByFilter.mockResolvedValue(mockResponse);
|
|
699
|
+
mockResolveUserNameToId.mockResolvedValue({
|
|
700
|
+
userId: 'specific-user-id',
|
|
701
|
+
displayName: 'John Doe',
|
|
702
|
+
});
|
|
703
|
+
const result = await findTasks.execute({ searchText: 'task', responsibleUser: 'John Doe', limit: 10 }, mockTodoistApi);
|
|
704
|
+
const structuredContent = extractStructuredContent(result);
|
|
705
|
+
// Should only return task 1 (assigned to John)
|
|
706
|
+
expect(structuredContent.tasks).toHaveLength(1);
|
|
707
|
+
expect(structuredContent.tasks[0]?.id).toBe(TEST_IDS.TASK_1);
|
|
708
|
+
});
|
|
709
|
+
it('should filter container-based results to show only tasks assigned to specified user', async () => {
|
|
710
|
+
const mockTasks = [
|
|
711
|
+
createMockTask({
|
|
712
|
+
id: TEST_IDS.TASK_1,
|
|
713
|
+
content: 'Task for John',
|
|
714
|
+
responsibleUid: 'specific-user-id', // Assigned to specified user
|
|
715
|
+
}),
|
|
716
|
+
createMockTask({
|
|
717
|
+
id: TEST_IDS.TASK_2,
|
|
718
|
+
content: 'My task',
|
|
719
|
+
responsibleUid: TEST_IDS.USER_ID, // Assigned to current user
|
|
720
|
+
}),
|
|
721
|
+
createMockTask({
|
|
722
|
+
id: TEST_IDS.TASK_3,
|
|
723
|
+
content: 'Unassigned task',
|
|
724
|
+
responsibleUid: null, // Unassigned
|
|
725
|
+
}),
|
|
726
|
+
];
|
|
727
|
+
mockTodoistApi.getTasks.mockResolvedValue(createMockApiResponse(mockTasks));
|
|
728
|
+
mockResolveUserNameToId.mockResolvedValue({
|
|
729
|
+
userId: 'specific-user-id',
|
|
730
|
+
displayName: 'John Doe',
|
|
731
|
+
});
|
|
732
|
+
const result = await findTasks.execute({ projectId: TEST_IDS.PROJECT_WORK, responsibleUser: 'John Doe', limit: 10 }, mockTodoistApi);
|
|
733
|
+
const structuredContent = extractStructuredContent(result);
|
|
734
|
+
// Should only return task 1 (assigned to John)
|
|
735
|
+
expect(structuredContent.tasks).toHaveLength(1);
|
|
736
|
+
expect(structuredContent.tasks[0]?.id).toBe(TEST_IDS.TASK_1);
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
});
|
|
550
740
|
describe('error handling', () => {
|
|
551
741
|
it.each([
|
|
552
742
|
{
|
package/dist/tools/add-tasks.js
CHANGED
|
@@ -55,8 +55,8 @@ async function processTask(task, client) {
|
|
|
55
55
|
if (priority) {
|
|
56
56
|
taskArgs.priority = convertPriorityToNumber(priority);
|
|
57
57
|
}
|
|
58
|
-
//
|
|
59
|
-
if (!projectId && !sectionId && !parentId) {
|
|
58
|
+
// Only prevent assignment (not task creation) without sufficient project context
|
|
59
|
+
if (responsibleUser && !projectId && !sectionId && !parentId) {
|
|
60
60
|
throw new Error(`Task "${task.content}": Cannot assign tasks without specifying project context. Please specify a projectId, sectionId, or parentId.`);
|
|
61
61
|
}
|
|
62
62
|
// Parse duration if provided
|
|
@@ -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;AA0CvB,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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;AA0CvB,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0DqB,CAAA;AAoE1C,OAAO,EAAE,eAAe,EAAE,CAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { addDays, formatISO } from 'date-fns';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getToolOutput } from '../mcp-helpers.js';
|
|
4
|
-
import { getTasksByFilter } from '../tool-helpers.js';
|
|
4
|
+
import { filterTasksByResponsibleUser, getTasksByFilter } from '../tool-helpers.js';
|
|
5
5
|
import { ApiLimits } from '../utils/constants.js';
|
|
6
6
|
import { generateLabelsFilter, LabelsSchema } from '../utils/labels.js';
|
|
7
7
|
import { generateTaskNextSteps, getDateString, previewTasks, summarizeList, } from '../utils/response-builders.js';
|
|
@@ -37,6 +37,7 @@ const findTasksByDate = {
|
|
|
37
37
|
parameters: ArgsSchema,
|
|
38
38
|
async execute(args, client) {
|
|
39
39
|
let query = '';
|
|
40
|
+
const todoistUser = await client.getUser();
|
|
40
41
|
if (args.startDate === 'today') {
|
|
41
42
|
query = 'today | overdue';
|
|
42
43
|
}
|
|
@@ -60,17 +61,23 @@ const findTasksByDate = {
|
|
|
60
61
|
cursor: args.cursor,
|
|
61
62
|
limit: args.limit,
|
|
62
63
|
});
|
|
63
|
-
|
|
64
|
+
// Apply responsible user filtering - only show unassigned tasks or tasks assigned to current user
|
|
65
|
+
const filteredTasks = filterTasksByResponsibleUser({
|
|
64
66
|
tasks: result.tasks,
|
|
67
|
+
resolvedAssigneeId: undefined,
|
|
68
|
+
currentUserId: todoistUser.id,
|
|
69
|
+
});
|
|
70
|
+
const textContent = generateTextContent({
|
|
71
|
+
tasks: filteredTasks,
|
|
65
72
|
args,
|
|
66
73
|
nextCursor: result.nextCursor,
|
|
67
74
|
});
|
|
68
75
|
return getToolOutput({
|
|
69
76
|
textContent,
|
|
70
77
|
structuredContent: {
|
|
71
|
-
tasks:
|
|
78
|
+
tasks: filteredTasks,
|
|
72
79
|
nextCursor: result.nextCursor,
|
|
73
|
-
totalCount:
|
|
80
|
+
totalCount: filteredTasks.length,
|
|
74
81
|
hasMore: Boolean(result.nextCursor),
|
|
75
82
|
appliedFilters: args,
|
|
76
83
|
},
|
|
@@ -10,6 +10,7 @@ declare const findTasks: {
|
|
|
10
10
|
sectionId: z.ZodOptional<z.ZodString>;
|
|
11
11
|
parentId: z.ZodOptional<z.ZodString>;
|
|
12
12
|
responsibleUser: z.ZodOptional<z.ZodString>;
|
|
13
|
+
responsibleUserFiltering: z.ZodOptional<z.ZodEnum<["assigned", "unassignedOrMe", "all"]>>;
|
|
13
14
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
14
15
|
cursor: z.ZodOptional<z.ZodString>;
|
|
15
16
|
};
|
|
@@ -17,6 +18,7 @@ declare const findTasks: {
|
|
|
17
18
|
limit: number;
|
|
18
19
|
projectId?: string | undefined;
|
|
19
20
|
parentId?: string | undefined;
|
|
21
|
+
responsibleUserFiltering?: "assigned" | "unassignedOrMe" | "all" | undefined;
|
|
20
22
|
sectionId?: string | undefined;
|
|
21
23
|
labels?: string[] | undefined;
|
|
22
24
|
cursor?: string | undefined;
|
|
@@ -51,6 +53,7 @@ declare const findTasks: {
|
|
|
51
53
|
limit: number;
|
|
52
54
|
projectId?: string | undefined;
|
|
53
55
|
parentId?: string | undefined;
|
|
56
|
+
responsibleUserFiltering?: "assigned" | "unassignedOrMe" | "all" | undefined;
|
|
54
57
|
sectionId?: string | undefined;
|
|
55
58
|
labels?: string[] | undefined;
|
|
56
59
|
cursor?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"find-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAsDvB,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqM2B,CAAA;AA2K1C,OAAO,EAAE,SAAS,EAAE,CAAA"}
|
package/dist/tools/find-tasks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { getToolOutput } from '../mcp-helpers.js';
|
|
3
|
-
import { getTasksByFilter, mapTask } from '../tool-helpers.js';
|
|
3
|
+
import { filterTasksByResponsibleUser, getTasksByFilter, mapTask, RESPONSIBLE_USER_FILTERING, } from '../tool-helpers.js';
|
|
4
4
|
import { ApiLimits } from '../utils/constants.js';
|
|
5
5
|
import { generateLabelsFilter, LabelsSchema } from '../utils/labels.js';
|
|
6
6
|
import { generateTaskNextSteps, getDateString, previewTasks, summarizeList, } from '../utils/response-builders.js';
|
|
@@ -16,6 +16,10 @@ const ArgsSchema = {
|
|
|
16
16
|
.string()
|
|
17
17
|
.optional()
|
|
18
18
|
.describe('Find tasks assigned to this user. Can be a user ID, name, or email address.'),
|
|
19
|
+
responsibleUserFiltering: z
|
|
20
|
+
.enum(RESPONSIBLE_USER_FILTERING)
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('How to filter by responsible user when responsibleUser is not provided. "assigned" = only tasks assigned to others; "unassignedOrMe" = only unassigned tasks or tasks assigned to me; "all" = all tasks regardless of assignment. Default value will be `unassignedOrMe`.'),
|
|
19
23
|
limit: z
|
|
20
24
|
.number()
|
|
21
25
|
.int()
|
|
@@ -34,7 +38,8 @@ const findTasks = {
|
|
|
34
38
|
description: 'Find tasks by text search, or by project/section/parent container/responsible user. At least one filter must be provided.',
|
|
35
39
|
parameters: ArgsSchema,
|
|
36
40
|
async execute(args, client) {
|
|
37
|
-
const { searchText, projectId, sectionId, parentId, responsibleUser, limit, cursor, labels, labelsOperator, } = args;
|
|
41
|
+
const { searchText, projectId, sectionId, parentId, responsibleUser, responsibleUserFiltering, limit, cursor, labels, labelsOperator, } = args;
|
|
42
|
+
const todoistUser = await client.getUser();
|
|
38
43
|
// Validate at least one filter is provided
|
|
39
44
|
const hasLabels = labels && labels.length > 0;
|
|
40
45
|
if (!searchText &&
|
|
@@ -78,9 +83,12 @@ const findTasks = {
|
|
|
78
83
|
task.description?.toLowerCase().includes(searchText.toLowerCase()))
|
|
79
84
|
: mappedTasks;
|
|
80
85
|
// Apply responsibleUid filter
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
filteredTasks = filterTasksByResponsibleUser({
|
|
87
|
+
tasks: filteredTasks,
|
|
88
|
+
resolvedAssigneeId,
|
|
89
|
+
currentUserId: todoistUser.id,
|
|
90
|
+
responsibleUserFiltering,
|
|
91
|
+
});
|
|
84
92
|
// Apply label filter
|
|
85
93
|
if (labels && labels.length > 0) {
|
|
86
94
|
filteredTasks =
|
|
@@ -156,11 +164,12 @@ const findTasks = {
|
|
|
156
164
|
cursor: args.cursor,
|
|
157
165
|
limit: args.limit,
|
|
158
166
|
});
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
const tasks = filterTasksByResponsibleUser({
|
|
168
|
+
tasks: result.tasks,
|
|
169
|
+
resolvedAssigneeId,
|
|
170
|
+
currentUserId: todoistUser.id,
|
|
171
|
+
responsibleUserFiltering,
|
|
172
|
+
});
|
|
164
173
|
const textContent = generateTextContent({
|
|
165
174
|
tasks,
|
|
166
175
|
args,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PersonalProject, Section, Task } from '@doist/todoist-api-typescript';
|
|
1
|
+
import type { CurrentUser, PersonalProject, Section, Task } from '@doist/todoist-api-typescript';
|
|
2
2
|
import { getToolOutput } from '../mcp-helpers';
|
|
3
3
|
/**
|
|
4
4
|
* Mapped task type matching the output of mapTask function.
|
|
@@ -95,4 +95,9 @@ export declare const TEST_IDS: {
|
|
|
95
95
|
* Use this instead of new Date() in tests to avoid snapshot drift.
|
|
96
96
|
*/
|
|
97
97
|
export declare const TODAY: "2025-08-17";
|
|
98
|
+
/**
|
|
99
|
+
* Creates a mock CurrentUser with all required properties and sensible defaults.
|
|
100
|
+
* Pass only the properties you want to override for your specific test.
|
|
101
|
+
*/
|
|
102
|
+
export declare function createMockUser(overrides?: Partial<CurrentUser>): CurrentUser;
|
|
98
103
|
//# sourceMappingURL=test-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../src/utils/test-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../src/utils/test-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAA;AAChG,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE9C;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,SAAS,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B,CAAA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,GAAE,OAAO,CAAC,IAAI,CAAM,GAAG,IAAI,CA8BlE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,OAAO,CAAC,OAAO,CAAM,GAAG,OAAO,CAgB3E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,GAAE,OAAO,CAAC,eAAe,CAAM,GAAG,eAAe,CAuB3F;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EACnC,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,GAAE,MAAM,GAAG,IAAW,GACjC;IACC,OAAO,EAAE,CAAC,EAAE,CAAA;IACZ,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B,CAKA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,GAAE,OAAO,CAAC,UAAU,CAAM,GAAG,UAAU,CAiBhF;AAED;;GAEG;AACH,eAAO,MAAM,WAAW;;;;;CAKd,CAAA;AAEV;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,EAC1C,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;CAAE,CAAC;UAAjC,MAAM;WAAS,CAAC;eAAa,CAAC;IAGtD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,CAqB9D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,GACzC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuBzB;AAED;;GAEG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;CAUX,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAG,YAAqB,CAAA;AAE1C;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAmChF"}
|
|
@@ -192,3 +192,43 @@ export const TEST_IDS = {
|
|
|
192
192
|
* Use this instead of new Date() in tests to avoid snapshot drift.
|
|
193
193
|
*/
|
|
194
194
|
export const TODAY = '2025-08-17';
|
|
195
|
+
/**
|
|
196
|
+
* Creates a mock CurrentUser with all required properties and sensible defaults.
|
|
197
|
+
* Pass only the properties you want to override for your specific test.
|
|
198
|
+
*/
|
|
199
|
+
export function createMockUser(overrides = {}) {
|
|
200
|
+
return {
|
|
201
|
+
id: TEST_IDS.USER_ID,
|
|
202
|
+
email: 'test@example.com',
|
|
203
|
+
fullName: 'Test User',
|
|
204
|
+
businessAccountId: null,
|
|
205
|
+
isPremium: false,
|
|
206
|
+
dateFormat: 0,
|
|
207
|
+
timeFormat: 0,
|
|
208
|
+
weeklyGoal: 5,
|
|
209
|
+
dailyGoal: 5,
|
|
210
|
+
completedCount: 0,
|
|
211
|
+
completedToday: 0,
|
|
212
|
+
daysOff: [],
|
|
213
|
+
inboxProjectId: TEST_IDS.PROJECT_INBOX,
|
|
214
|
+
karma: 0,
|
|
215
|
+
karmaTrend: 'up',
|
|
216
|
+
lang: 'en',
|
|
217
|
+
nextWeek: 1,
|
|
218
|
+
startDay: 1,
|
|
219
|
+
startPage: 'today',
|
|
220
|
+
weekendStartDay: 6,
|
|
221
|
+
tzInfo: {
|
|
222
|
+
timezone: 'UTC',
|
|
223
|
+
gmtString: '+00:00',
|
|
224
|
+
hours: 0,
|
|
225
|
+
minutes: 0,
|
|
226
|
+
isDst: 0,
|
|
227
|
+
},
|
|
228
|
+
avatarBig: null,
|
|
229
|
+
avatarMedium: null,
|
|
230
|
+
avatarS640: null,
|
|
231
|
+
avatarSmall: null,
|
|
232
|
+
...overrides,
|
|
233
|
+
};
|
|
234
|
+
}
|