@doist/todoist-ai 0.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.
Files changed (119) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +93 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/main.d.ts +2 -0
  7. package/dist/main.d.ts.map +1 -0
  8. package/dist/main.js +23 -0
  9. package/dist/mcp-helpers.d.ts +13 -0
  10. package/dist/mcp-helpers.d.ts.map +1 -0
  11. package/dist/mcp-helpers.js +52 -0
  12. package/dist/mcp-server.d.ts +11 -0
  13. package/dist/mcp-server.d.ts.map +1 -0
  14. package/dist/mcp-server.js +66 -0
  15. package/dist/todoist-tool.d.ts +35 -0
  16. package/dist/todoist-tool.d.ts.map +1 -0
  17. package/dist/todoist-tool.js +1 -0
  18. package/dist/tools/account-overview.d.ts +9 -0
  19. package/dist/tools/account-overview.d.ts.map +1 -0
  20. package/dist/tools/account-overview.js +94 -0
  21. package/dist/tools/index.d.ts +23 -0
  22. package/dist/tools/index.d.ts.map +1 -0
  23. package/dist/tools/index.js +22 -0
  24. package/dist/tools/project-overview.d.ts +14 -0
  25. package/dist/tools/project-overview.d.ts.map +1 -0
  26. package/dist/tools/project-overview.js +107 -0
  27. package/dist/tools/projects-add-one.d.ts +22 -0
  28. package/dist/tools/projects-add-one.d.ts.map +1 -0
  29. package/dist/tools/projects-add-one.js +15 -0
  30. package/dist/tools/projects-delete-one.d.ts +15 -0
  31. package/dist/tools/projects-delete-one.d.ts.map +1 -0
  32. package/dist/tools/projects-delete-one.js +14 -0
  33. package/dist/tools/projects-list.d.ts +18 -0
  34. package/dist/tools/projects-list.d.ts.map +1 -0
  35. package/dist/tools/projects-list.js +30 -0
  36. package/dist/tools/projects-search.d.ts +29 -0
  37. package/dist/tools/projects-search.d.ts.map +1 -0
  38. package/dist/tools/projects-search.js +39 -0
  39. package/dist/tools/projects-update-one.d.ts +15 -0
  40. package/dist/tools/projects-update-one.d.ts.map +1 -0
  41. package/dist/tools/projects-update-one.js +16 -0
  42. package/dist/tools/sections-add-one.d.ts +15 -0
  43. package/dist/tools/sections-add-one.d.ts.map +1 -0
  44. package/dist/tools/sections-add-one.js +21 -0
  45. package/dist/tools/sections-delete-one.d.ts +15 -0
  46. package/dist/tools/sections-delete-one.d.ts.map +1 -0
  47. package/dist/tools/sections-delete-one.js +14 -0
  48. package/dist/tools/sections-search.d.ts +18 -0
  49. package/dist/tools/sections-search.d.ts.map +1 -0
  50. package/dist/tools/sections-search.js +30 -0
  51. package/dist/tools/sections-update-one.d.ts +15 -0
  52. package/dist/tools/sections-update-one.d.ts.map +1 -0
  53. package/dist/tools/sections-update-one.js +16 -0
  54. package/dist/tools/shared.d.ts +55 -0
  55. package/dist/tools/shared.d.ts.map +1 -0
  56. package/dist/tools/shared.js +71 -0
  57. package/dist/tools/subtasks-list-for-parent-task.d.ts +31 -0
  58. package/dist/tools/subtasks-list-for-parent-task.d.ts.map +1 -0
  59. package/dist/tools/subtasks-list-for-parent-task.js +37 -0
  60. package/dist/tools/tasks-add-multiple.d.ts +50 -0
  61. package/dist/tools/tasks-add-multiple.d.ts.map +1 -0
  62. package/dist/tools/tasks-add-multiple.js +47 -0
  63. package/dist/tools/tasks-by-date-range.d.ts +33 -0
  64. package/dist/tools/tasks-by-date-range.d.ts.map +1 -0
  65. package/dist/tools/tasks-by-date-range.js +47 -0
  66. package/dist/tools/tasks-by-project.d.ts +31 -0
  67. package/dist/tools/tasks-by-project.d.ts.map +1 -0
  68. package/dist/tools/tasks-by-project.js +37 -0
  69. package/dist/tools/tasks-complete-multiple.d.ts +16 -0
  70. package/dist/tools/tasks-complete-multiple.d.ts.map +1 -0
  71. package/dist/tools/tasks-complete-multiple.js +26 -0
  72. package/dist/tools/tasks-delete-one.d.ts +15 -0
  73. package/dist/tools/tasks-delete-one.d.ts.map +1 -0
  74. package/dist/tools/tasks-delete-one.js +14 -0
  75. package/dist/tools/tasks-list-for-section.d.ts +31 -0
  76. package/dist/tools/tasks-list-for-section.d.ts.map +1 -0
  77. package/dist/tools/tasks-list-for-section.js +37 -0
  78. package/dist/tools/tasks-list-overdue.d.ts +29 -0
  79. package/dist/tools/tasks-list-overdue.d.ts.map +1 -0
  80. package/dist/tools/tasks-list-overdue.js +29 -0
  81. package/dist/tools/tasks-organize-multiple.d.ts +37 -0
  82. package/dist/tools/tasks-organize-multiple.d.ts.map +1 -0
  83. package/dist/tools/tasks-organize-multiple.js +40 -0
  84. package/dist/tools/tasks-search.d.ts +31 -0
  85. package/dist/tools/tasks-search.d.ts.map +1 -0
  86. package/dist/tools/tasks-search.js +30 -0
  87. package/dist/tools/tasks-update-one.d.ts +27 -0
  88. package/dist/tools/tasks-update-one.d.ts.map +1 -0
  89. package/dist/tools/tasks-update-one.js +42 -0
  90. package/package.json +60 -0
  91. package/src/index.ts +1 -0
  92. package/src/main.ts +26 -0
  93. package/src/mcp-helpers.ts +76 -0
  94. package/src/mcp-server.ts +79 -0
  95. package/src/todoist-tool.ts +42 -0
  96. package/src/tools/account-overview.ts +130 -0
  97. package/src/tools/index.ts +26 -0
  98. package/src/tools/project-overview.ts +130 -0
  99. package/src/tools/projects-add-one.ts +19 -0
  100. package/src/tools/projects-delete-one.ts +18 -0
  101. package/src/tools/projects-list.ts +36 -0
  102. package/src/tools/projects-search.ts +49 -0
  103. package/src/tools/projects-update-one.ts +20 -0
  104. package/src/tools/sections-add-one.ts +25 -0
  105. package/src/tools/sections-delete-one.ts +18 -0
  106. package/src/tools/sections-search.ts +39 -0
  107. package/src/tools/sections-update-one.ts +20 -0
  108. package/src/tools/shared.ts +94 -0
  109. package/src/tools/subtasks-list-for-parent-task.ts +43 -0
  110. package/src/tools/tasks-add-multiple.ts +53 -0
  111. package/src/tools/tasks-by-date-range.ts +58 -0
  112. package/src/tools/tasks-by-project.ts +43 -0
  113. package/src/tools/tasks-complete-multiple.ts +29 -0
  114. package/src/tools/tasks-delete-one.ts +18 -0
  115. package/src/tools/tasks-list-for-section.ts +43 -0
  116. package/src/tools/tasks-list-overdue.ts +35 -0
  117. package/src/tools/tasks-organize-multiple.ts +45 -0
  118. package/src/tools/tasks-search.ts +36 -0
  119. package/src/tools/tasks-update-one.ts +48 -0
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ const ArgsSchema = {
3
+ id: z.string().min(1).describe("The ID of the task to update."),
4
+ content: z.string().optional().describe("The new content of the task."),
5
+ description: z
6
+ .string()
7
+ .optional()
8
+ .describe("The new description of the task."),
9
+ projectId: z
10
+ .string()
11
+ .optional()
12
+ .describe("The new project ID for the task."),
13
+ sectionId: z
14
+ .string()
15
+ .optional()
16
+ .describe("The new section ID for the task."),
17
+ parentId: z
18
+ .string()
19
+ .optional()
20
+ .describe("The new parent task ID (for subtasks)."),
21
+ priority: z
22
+ .number()
23
+ .int()
24
+ .min(1)
25
+ .max(4)
26
+ .optional()
27
+ .describe("The new priority of the task (1-4)."),
28
+ dueString: z
29
+ .string()
30
+ .optional()
31
+ .describe("The new due date for the task, in natural language (e.g., 'tomorrow at 5pm')."),
32
+ };
33
+ const tasksUpdateOne = {
34
+ name: "tasks-update-one",
35
+ description: "Update an existing task with new values.",
36
+ parameters: ArgsSchema,
37
+ async execute(args, client) {
38
+ const { id, ...updateArgs } = args;
39
+ return await client.updateTask(id, updateArgs);
40
+ },
41
+ };
42
+ export { tasksUpdateOne };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@doist/todoist-ai",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": ["dist", "src", "package.json", "LICENSE.txt", "README.md"],
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./tools": "./dist/tools/index.js"
11
+ },
12
+ "license": "MIT",
13
+ "description": "A collection of tools for Todoist using AI",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/doist/todoist-ai.git"
17
+ },
18
+ "keywords": ["todoist", "ai", "tools"],
19
+ "scripts": {
20
+ "test": "echo \"Error: no test specified\" && exit 1",
21
+ "build": "rimraf dist && tsc",
22
+ "dev": "concurrently \"tsc --watch\" \"nodemon --watch dist --ext js --exec 'npx -y @modelcontextprotocol/inspector npx node dist/main.js'\"",
23
+ "setup": "cp .env.example .env && npm install && npm run build",
24
+ "type-check": "tsc --noEmit",
25
+ "biome:sort-imports": "biome check --formatter-enabled=false --linter-enabled=false --organize-imports-enabled=true --write .",
26
+ "lint:check": "biome lint",
27
+ "lint:write": "biome lint --write",
28
+ "format:check": "biome format",
29
+ "format:write": "biome format --write",
30
+ "check": "biome check",
31
+ "check:fix": "biome check --fix --unsafe",
32
+ "prepare": "husky"
33
+ },
34
+ "dependencies": {
35
+ "@doist/todoist-api-typescript": "^4.0.4",
36
+ "@modelcontextprotocol/sdk": "^1.11.1",
37
+ "date-fns": "^4.1.0",
38
+ "dotenv": "^16.5.0",
39
+ "zod": "^3.25.7"
40
+ },
41
+ "devDependencies": {
42
+ "@biomejs/biome": "1.9.4",
43
+ "@types/express": "^5.0.2",
44
+ "@types/morgan": "^1.9.9",
45
+ "@types/node": "^22.15.17",
46
+ "concurrently": "^8.2.2",
47
+ "express": "^4.19.2",
48
+ "husky": "^9.1.7",
49
+ "lint-staged": "^16.0.0",
50
+ "morgan": "^1.10.0",
51
+ "nodemon": "^3.1.10",
52
+ "rimraf": "^6.0.1",
53
+ "typescript": "^5.8.3"
54
+ },
55
+ "lint-staged": {
56
+ "*": [
57
+ "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
58
+ ]
59
+ }
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { getMcpServer } from "./mcp-server.js";
package/src/main.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import dotenv from "dotenv";
3
+ import { getMcpServer } from "./mcp-server.js";
4
+
5
+ function main() {
6
+ const todoistApiKey = process.env.TODOIST_API_KEY;
7
+ if (!todoistApiKey) {
8
+ throw new Error("TODOIST_API_KEY is not set");
9
+ }
10
+
11
+ const server = getMcpServer({ todoistApiKey });
12
+ const transport = new StdioServerTransport();
13
+ server
14
+ .connect(transport)
15
+ .then(() => {
16
+ // We use console.error because standard I/O is being used for the MCP server communication.
17
+ console.error("Server started");
18
+ })
19
+ .catch((error) => {
20
+ console.error("Error starting the Todoist MCP server:", error);
21
+ process.exit(1);
22
+ });
23
+ }
24
+
25
+ dotenv.config();
26
+ main();
@@ -0,0 +1,76 @@
1
+ import type { TodoistApi } from "@doist/todoist-api-typescript";
2
+ import type {
3
+ McpServer,
4
+ ToolCallback,
5
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import type { ZodTypeAny, z } from "zod";
7
+ import type { TodoistTool } from "./todoist-tool.js";
8
+
9
+ function textContent(text: string) {
10
+ return {
11
+ content: [{ type: "text" as const, text }],
12
+ };
13
+ }
14
+
15
+ function jsonContent(data: unknown) {
16
+ return {
17
+ content: [
18
+ {
19
+ type: "text" as const,
20
+ mimeType: "application/json",
21
+ text: JSON.stringify(data, null, 2),
22
+ },
23
+ ],
24
+ };
25
+ }
26
+
27
+ function textOrJsonContent(data: unknown) {
28
+ return typeof data === "string" ? textContent(data) : jsonContent(data);
29
+ }
30
+
31
+ function errorContent(error: string) {
32
+ return {
33
+ ...textContent(error),
34
+ isError: true,
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Register a Todoist tool in an MCP server.
40
+ * @param tool - The tool to register.
41
+ * @param server - The server to register the tool on.
42
+ * @param client - The Todoist API client to use to execute the tool.
43
+ */
44
+ function registerTool<Params extends z.ZodRawShape>(
45
+ tool: TodoistTool<Params>,
46
+ server: McpServer,
47
+ client: TodoistApi,
48
+ ) {
49
+ // @ts-ignore I give up
50
+ const cb: ToolCallback<Params> = async (
51
+ args: z.objectOutputType<Params, ZodTypeAny>,
52
+ _context,
53
+ ) => {
54
+ try {
55
+ const result = await tool.execute(
56
+ args as z.infer<z.ZodObject<Params>>,
57
+ client,
58
+ );
59
+ return textOrJsonContent(result);
60
+ } catch (error) {
61
+ console.error(`Error executing tool ${tool.name}:`, {
62
+ args,
63
+ error,
64
+ });
65
+ const message =
66
+ error instanceof Error
67
+ ? error.message
68
+ : "An unknown error occurred";
69
+ return errorContent(message);
70
+ }
71
+ };
72
+
73
+ server.tool(tool.name, tool.description, tool.parameters, cb);
74
+ }
75
+
76
+ export { registerTool };
@@ -0,0 +1,79 @@
1
+ import { TodoistApi } from "@doist/todoist-api-typescript";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { registerTool } from "./mcp-helpers.js";
4
+
5
+ import { projectsAddOne } from "./tools/projects-add-one.js";
6
+ import { projectsDeleteOne } from "./tools/projects-delete-one.js";
7
+ import { projectsList } from "./tools/projects-list.js";
8
+ import { projectsSearch } from "./tools/projects-search.js";
9
+ import { projectsUpdateOne } from "./tools/projects-update-one.js";
10
+
11
+ import { sectionsAddOne } from "./tools/sections-add-one.js";
12
+ import { sectionsDeleteOne } from "./tools/sections-delete-one.js";
13
+ import { sectionsSearch } from "./tools/sections-search.js";
14
+ import { sectionsUpdateOne } from "./tools/sections-update-one.js";
15
+
16
+ import { subtasksListForParentTask } from "./tools/subtasks-list-for-parent-task.js";
17
+
18
+ import { accountOverview } from "./tools/account-overview.js";
19
+ import { projectOverview } from "./tools/project-overview.js";
20
+ import { tasksAddMultiple } from "./tools/tasks-add-multiple.js";
21
+ import { tasksListByDate } from "./tools/tasks-by-date-range.js";
22
+ import { tasksListForProject } from "./tools/tasks-by-project.js";
23
+ import { tasksCompleteMultiple } from "./tools/tasks-complete-multiple.js";
24
+ import { tasksDeleteOne } from "./tools/tasks-delete-one.js";
25
+ import { tasksListForSection } from "./tools/tasks-list-for-section.js";
26
+ import { tasksListOverdue } from "./tools/tasks-list-overdue.js";
27
+ import { tasksOrganizeMultiple } from "./tools/tasks-organize-multiple.js";
28
+ import { tasksSearch } from "./tools/tasks-search.js";
29
+ import { tasksUpdateOne } from "./tools/tasks-update-one.js";
30
+
31
+ const instructions = `
32
+ Tools to help you manage your todoist tasks.
33
+ `;
34
+
35
+ /**
36
+ * Create the MCP server.
37
+ * @param todoistApiKey - The API key for the todoist account.
38
+ * @returns the MCP server.
39
+ */
40
+ function getMcpServer({ todoistApiKey }: { todoistApiKey: string }) {
41
+ const server = new McpServer(
42
+ { name: "todoist-mcp-server", version: "0.1.0" },
43
+ {
44
+ capabilities: {
45
+ tools: { listChanged: true },
46
+ },
47
+ instructions,
48
+ },
49
+ );
50
+
51
+ const todoist = new TodoistApi(todoistApiKey);
52
+
53
+ registerTool(tasksListByDate, server, todoist);
54
+ registerTool(tasksListOverdue, server, todoist);
55
+ registerTool(tasksListForProject, server, todoist);
56
+ registerTool(tasksSearch, server, todoist);
57
+ registerTool(projectsList, server, todoist);
58
+ registerTool(tasksAddMultiple, server, todoist);
59
+ registerTool(tasksUpdateOne, server, todoist);
60
+ registerTool(tasksDeleteOne, server, todoist);
61
+ registerTool(tasksCompleteMultiple, server, todoist);
62
+ registerTool(projectsAddOne, server, todoist);
63
+ registerTool(projectsUpdateOne, server, todoist);
64
+ registerTool(sectionsAddOne, server, todoist);
65
+ registerTool(sectionsUpdateOne, server, todoist);
66
+ registerTool(tasksOrganizeMultiple, server, todoist);
67
+ registerTool(subtasksListForParentTask, server, todoist);
68
+ registerTool(tasksListForSection, server, todoist);
69
+ registerTool(projectsDeleteOne, server, todoist);
70
+ registerTool(projectsSearch, server, todoist);
71
+ registerTool(sectionsDeleteOne, server, todoist);
72
+ registerTool(sectionsSearch, server, todoist);
73
+ registerTool(accountOverview, server, todoist);
74
+ registerTool(projectOverview, server, todoist);
75
+
76
+ return server;
77
+ }
78
+
79
+ export { getMcpServer };
@@ -0,0 +1,42 @@
1
+ import type { TodoistApi } from "@doist/todoist-api-typescript";
2
+ import type { z } from "zod";
3
+
4
+ /**
5
+ * A Todoist tool that can be used in an MCP server or other conversational AI interfaces.
6
+ */
7
+ type TodoistTool<Params extends z.ZodRawShape> = {
8
+ /**
9
+ * The name of the tool.
10
+ */
11
+ name: string;
12
+
13
+ /**
14
+ * The description of the tool. This is important for the LLM to understand what the tool does,
15
+ * and how to use it.
16
+ */
17
+ description: string;
18
+
19
+ /**
20
+ * The schema of the parameters of the tool.
21
+ *
22
+ * This is used to validate the parameters of the tool, as well as to let the LLM know what the
23
+ * parameters are.
24
+ */
25
+ parameters: Params;
26
+
27
+ /**
28
+ * The function that executes the tool.
29
+ *
30
+ * This is the main function that will be called when the tool is used.
31
+ *
32
+ * @param args - The arguments of the tool.
33
+ * @param client - The Todoist API client used to make requests to the Todoist API.
34
+ * @returns The result of the tool.
35
+ */
36
+ execute: (
37
+ args: z.infer<z.ZodObject<Params>>,
38
+ client: TodoistApi,
39
+ ) => Promise<unknown>;
40
+ };
41
+
42
+ export type { TodoistTool };
@@ -0,0 +1,130 @@
1
+ import type {
2
+ Project,
3
+ Section,
4
+ TodoistApi,
5
+ } from "@doist/todoist-api-typescript";
6
+ import type { TodoistTool } from "../todoist-tool.js";
7
+
8
+ const ArgsSchema = {};
9
+
10
+ type ProjectWithChildren = Project & {
11
+ children: ProjectWithChildren[];
12
+ childOrder: number;
13
+ };
14
+
15
+ function buildProjectTree(projects: Project[]): Project[] {
16
+ // Sort projects by childOrder, then build a tree
17
+ const byId: Record<string, ProjectWithChildren> = {};
18
+ for (const p of projects) {
19
+ byId[p.id] = {
20
+ ...p,
21
+ children: [],
22
+ childOrder: (p as { childOrder?: number }).childOrder ?? 0,
23
+ };
24
+ }
25
+ const roots: ProjectWithChildren[] = [];
26
+ for (const p of projects) {
27
+ const current = byId[p.id];
28
+ if (!current) continue;
29
+ if (p.parentId) {
30
+ const parent = byId[p.parentId];
31
+ if (parent) {
32
+ parent.children.push(current);
33
+ } else {
34
+ roots.push(current);
35
+ }
36
+ } else {
37
+ roots.push(current);
38
+ }
39
+ }
40
+ function sortTree(nodes: ProjectWithChildren[]) {
41
+ nodes.sort((a, b) => a.childOrder - b.childOrder);
42
+ for (const n of nodes) {
43
+ sortTree(n.children);
44
+ }
45
+ }
46
+ sortTree(roots);
47
+ return roots;
48
+ }
49
+
50
+ async function getSectionsByProject(
51
+ client: TodoistApi,
52
+ projectIds: string[],
53
+ ): Promise<Record<string, Section[]>> {
54
+ const result: Record<string, Section[]> = {};
55
+ await Promise.all(
56
+ projectIds.map(async (projectId) => {
57
+ const { results } = await client.getSections({ projectId });
58
+ result[projectId] = results;
59
+ }),
60
+ );
61
+ return result;
62
+ }
63
+
64
+ function renderProjectMarkdown(
65
+ project: ProjectWithChildren,
66
+ sectionsByProject: Record<string, Section[]>,
67
+ indent = "",
68
+ ): string[] {
69
+ const lines: string[] = [];
70
+ lines.push(`${indent}- Project: ${project.name} (id=${project.id})`);
71
+ const sections = sectionsByProject[project.id] || [];
72
+ for (const section of sections) {
73
+ lines.push(`${indent} - Section: ${section.name} (id=${section.id})`);
74
+ }
75
+ for (const child of project.children) {
76
+ lines.push(
77
+ ...renderProjectMarkdown(child, sectionsByProject, `${indent} `),
78
+ );
79
+ }
80
+ return lines;
81
+ }
82
+
83
+ const accountOverview = {
84
+ name: "account-overview",
85
+ description:
86
+ "Get a Markdown overview of all projects (with hierarchy and sections) and the inbox project. Useful in almost any context before engaging with Todoist further.",
87
+ parameters: ArgsSchema,
88
+ async execute(_args, client) {
89
+ const { results: projects } = await client.getProjects({});
90
+ const inbox = projects.find((p) => p.isInboxProject === true);
91
+ const nonInbox = projects.filter((p) => p.isInboxProject !== true);
92
+ const tree = buildProjectTree(nonInbox);
93
+ const allProjectIds = projects.map((p) => p.id);
94
+ const sectionsByProject = await getSectionsByProject(
95
+ client,
96
+ allProjectIds,
97
+ );
98
+
99
+ const lines: string[] = ["# Personal Projects", ""];
100
+ if (inbox) {
101
+ lines.push(`- Inbox Project: ${inbox.name} (id=${inbox.id})`);
102
+ for (const section of sectionsByProject[inbox.id] || []) {
103
+ lines.push(` - Section: ${section.name} (id=${section.id})`);
104
+ }
105
+ }
106
+ if (tree.length) {
107
+ for (const project of tree as ProjectWithChildren[]) {
108
+ lines.push(
109
+ ...renderProjectMarkdown(project, sectionsByProject),
110
+ );
111
+ }
112
+ } else {
113
+ lines.push("_No projects found._");
114
+ }
115
+ lines.push("");
116
+ // Add explanation about nesting if there are nested projects
117
+ const hasNested = (tree as ProjectWithChildren[]).some(
118
+ (p) => p.children.length > 0,
119
+ );
120
+ if (hasNested) {
121
+ lines.push(
122
+ "_Note: Indentation indicates that a project is a sub-project of the one above it. This allows for organizing projects hierarchically, with parent projects containing related sub-projects._",
123
+ "",
124
+ );
125
+ }
126
+ return lines.join("\n");
127
+ },
128
+ } satisfies TodoistTool<typeof ArgsSchema>;
129
+
130
+ export { accountOverview };
@@ -0,0 +1,26 @@
1
+ export { projectsList } from "./projects-list.js";
2
+ export { projectsSearch } from "./projects-search.js";
3
+ export { projectsAddOne } from "./projects-add-one.js";
4
+ export { projectsUpdateOne } from "./projects-update-one.js";
5
+ export { projectsDeleteOne } from "./projects-delete-one.js";
6
+
7
+ export { sectionsSearch } from "./sections-search.js";
8
+ export { sectionsAddOne } from "./sections-add-one.js";
9
+ export { sectionsUpdateOne } from "./sections-update-one.js";
10
+ export { sectionsDeleteOne } from "./sections-delete-one.js";
11
+
12
+ export { tasksListByDate } from "./tasks-by-date-range.js";
13
+ export { tasksDeleteOne } from "./tasks-delete-one.js";
14
+ export { tasksCompleteMultiple } from "./tasks-complete-multiple.js";
15
+ export { tasksListForProject } from "./tasks-by-project.js";
16
+ export { tasksListOverdue } from "./tasks-list-overdue.js";
17
+ export { tasksSearch } from "./tasks-search.js";
18
+ export { tasksAddMultiple } from "./tasks-add-multiple.js";
19
+ export { tasksUpdateOne } from "./tasks-update-one.js";
20
+ export { tasksOrganizeMultiple } from "./tasks-organize-multiple.js";
21
+ export { tasksListForSection } from "./tasks-list-for-section.js";
22
+
23
+ export { subtasksListForParentTask } from "./subtasks-list-for-parent-task.js";
24
+
25
+ export { accountOverview } from "./account-overview.js";
26
+ export { projectOverview } from "./project-overview.js";
@@ -0,0 +1,130 @@
1
+ import type {
2
+ Project,
3
+ Section,
4
+ TodoistApi,
5
+ } from "@doist/todoist-api-typescript";
6
+ import { z } from "zod";
7
+ import type { TodoistTool } from "../todoist-tool.js";
8
+ import { mapTask } from "./shared.js";
9
+
10
+ const ArgsSchema = {
11
+ projectId: z
12
+ .string()
13
+ .min(1)
14
+ .describe("The ID of the project to get an overview for."),
15
+ };
16
+
17
+ type MappedTask = ReturnType<typeof mapTask>;
18
+
19
+ type TaskTreeNode = MappedTask & { children: TaskTreeNode[] };
20
+
21
+ function buildTaskTree(tasks: MappedTask[]): TaskTreeNode[] {
22
+ const byId: Record<string, TaskTreeNode> = {};
23
+ for (const task of tasks) {
24
+ byId[task.id] = { ...task, children: [] };
25
+ }
26
+ const roots: TaskTreeNode[] = [];
27
+ for (const task of tasks) {
28
+ const node = byId[task.id];
29
+ if (!node) continue;
30
+ if (!task.parentId) {
31
+ roots.push(node);
32
+ continue;
33
+ }
34
+ const parent = byId[task.parentId];
35
+ if (parent) {
36
+ parent.children.push(node);
37
+ } else {
38
+ roots.push(node);
39
+ }
40
+ }
41
+ return roots;
42
+ }
43
+
44
+ function renderTaskTreeMarkdown(tasks: TaskTreeNode[], indent = ""): string[] {
45
+ const lines: string[] = [];
46
+ for (const task of tasks) {
47
+ const idPart = `id=${task.id}`;
48
+ const duePart = task.dueDate ? `; due=${task.dueDate}` : "";
49
+ const contentPart = `; content=${task.content}`;
50
+ lines.push(`${indent}- ${idPart}${duePart}${contentPart}`);
51
+ if (task.children.length > 0) {
52
+ lines.push(...renderTaskTreeMarkdown(task.children, `${indent} `));
53
+ }
54
+ }
55
+ return lines;
56
+ }
57
+
58
+ async function getAllTasksForProject(
59
+ client: TodoistApi,
60
+ projectId: string,
61
+ ): Promise<MappedTask[]> {
62
+ let allTasks: MappedTask[] = [];
63
+ let cursor: string | undefined = undefined;
64
+ do {
65
+ const { results, nextCursor } = await client.getTasks({
66
+ projectId,
67
+ limit: 50,
68
+ cursor: cursor ?? undefined,
69
+ });
70
+ allTasks = allTasks.concat(results.map(mapTask));
71
+ cursor = nextCursor ?? undefined;
72
+ } while (cursor);
73
+ return allTasks;
74
+ }
75
+
76
+ async function getProjectSections(
77
+ client: TodoistApi,
78
+ projectId: string,
79
+ ): Promise<Section[]> {
80
+ const { results } = await client.getSections({ projectId });
81
+ return results;
82
+ }
83
+
84
+ const projectOverview = {
85
+ name: "project-overview",
86
+ description:
87
+ "Get a Markdown overview of a single project, including its sections and all tasks. Tasks are grouped by section, with tasks not in any section listed first. Each task is listed as '- id=TASKID; due=YYYY-MM-DD; content=TASK CONTENT' (omit due if not present). Subtasks are nested as indented list items.",
88
+ parameters: ArgsSchema,
89
+ async execute(args, client) {
90
+ const { projectId } = args;
91
+ const project: Project = await client.getProject(projectId);
92
+ const sections = await getProjectSections(client, projectId);
93
+ const allTasks = await getAllTasksForProject(client, projectId);
94
+
95
+ // Group tasks by sectionId
96
+ const tasksBySection: Record<string, MappedTask[]> = {};
97
+ for (const section of sections) {
98
+ tasksBySection[section.id] = [];
99
+ }
100
+ const tasksWithoutSection: MappedTask[] = [];
101
+ for (const task of allTasks) {
102
+ if (task.sectionId && tasksBySection[task.sectionId]) {
103
+ // biome-ignore lint/style/noNonNullAssertion: the "if" above ensures that it is defined
104
+ tasksBySection[task.sectionId]!.push(task);
105
+ } else {
106
+ tasksWithoutSection.push(task);
107
+ }
108
+ }
109
+
110
+ const lines: string[] = [`# ${project.name}`];
111
+ if (tasksWithoutSection.length > 0) {
112
+ lines.push("");
113
+ const tree = buildTaskTree(tasksWithoutSection);
114
+ lines.push(...renderTaskTreeMarkdown(tree));
115
+ }
116
+ for (const section of sections) {
117
+ lines.push("");
118
+ lines.push(`## ${section.name}`);
119
+ const sectionTasks = tasksBySection[section.id];
120
+ if (!sectionTasks?.length) {
121
+ continue;
122
+ }
123
+ const tree = buildTaskTree(sectionTasks);
124
+ lines.push(...renderTaskTreeMarkdown(tree));
125
+ }
126
+ return lines.join("\n");
127
+ },
128
+ } satisfies TodoistTool<typeof ArgsSchema>;
129
+
130
+ export { projectOverview };
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import type { TodoistTool } from "../todoist-tool.js";
3
+ import { mapProject } from "./shared.js";
4
+
5
+ const ArgsSchema = {
6
+ name: z.string().min(1).describe("The name of the project to add."),
7
+ };
8
+
9
+ const projectsAddOne = {
10
+ name: "projects-add-one",
11
+ description: "Add a new project.",
12
+ parameters: ArgsSchema,
13
+ async execute(args, client) {
14
+ const project = await client.addProject({ name: args.name });
15
+ return mapProject(project);
16
+ },
17
+ } satisfies TodoistTool<typeof ArgsSchema>;
18
+
19
+ export { projectsAddOne };
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import type { TodoistTool } from "../todoist-tool.js";
3
+
4
+ const ArgsSchema = {
5
+ id: z.string().min(1).describe("The ID of the project to delete."),
6
+ };
7
+
8
+ const projectsDeleteOne = {
9
+ name: "projects-delete-one",
10
+ description: "Delete a project by its ID.",
11
+ parameters: ArgsSchema,
12
+ async execute(args, client) {
13
+ await client.deleteProject(args.id);
14
+ return { success: true };
15
+ },
16
+ } satisfies TodoistTool<typeof ArgsSchema>;
17
+
18
+ export { projectsDeleteOne };