@doist/todoist-ai 6.0.0 → 6.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AA+FnE;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,aAqD5F;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAwGnE;;;;;GAKG;AACH,iBAAS,YAAY,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,aA0E5F;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -44,6 +44,12 @@ type TodoistTool<Params extends z.ZodRawShape, Output extends z.ZodRawShape> = {
44
44
  * This is used to generate appropriate MCP annotation hints (readOnlyHint, destructiveHint).
45
45
  */
46
46
  mutability: ToolMutability;
47
+ /**
48
+ * The meta data of the tool.
49
+ *
50
+ * This is used to store additional information about the tool.
51
+ */
52
+ _meta?: Record<string, unknown>;
47
53
  /**
48
54
  * The function that executes the tool.
49
55
  *
@@ -1 +1 @@
1
- {"version":3,"file":"todoist-tool.d.ts","sourceRoot":"","sources":["../src/todoist-tool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAE5B,KAAK,aAAa,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,IAAI,OAAO,CAAC;IACvD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;CACnD,CAAC,CAAA;AAEF;;;;;;GAMG;AACH,KAAK,cAAc,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,CAAA;AAE1D;;GAEG;AACH,KAAK,WAAW,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,EAAE,MAAM,SAAS,CAAC,CAAC,WAAW,IAAI;IAC3E;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IAEZ;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAA;IAEnB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAA;IAElB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAA;IAEpB;;;;OAIG;IACH,UAAU,EAAE,cAAc,CAAA;IAE1B;;;;;;;;OAQG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,KAAK,aAAa,CAAC,MAAM,CAAC,CAAA;CAC7F,CAAA;AAED,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"todoist-tool.d.ts","sourceRoot":"","sources":["../src/todoist-tool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAE5B,KAAK,aAAa,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,IAAI,OAAO,CAAC;IACvD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;CACnD,CAAC,CAAA;AAEF;;;;;;GAMG;AACH,KAAK,cAAc,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,CAAA;AAE1D;;GAEG;AACH,KAAK,WAAW,CAAC,MAAM,SAAS,CAAC,CAAC,WAAW,EAAE,MAAM,SAAS,CAAC,CAAC,WAAW,IAAI;IAC3E;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IAEZ;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAA;IAEnB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAA;IAElB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAA;IAEpB;;;;OAIG;IACH,UAAU,EAAE,cAAc,CAAA;IAE1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAE/B;;;;;;;;OAQG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,KAAK,aAAa,CAAC,MAAM,CAAC,CAAA;CAC7F,CAAA;AAED,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,168 @@
1
+ import { z } from 'zod';
2
+ declare const fetchObject: {
3
+ name: "fetch-object";
4
+ description: string;
5
+ parameters: {
6
+ type: z.ZodEnum<{
7
+ task: "task";
8
+ comment: "comment";
9
+ project: "project";
10
+ section: "section";
11
+ }>;
12
+ id: z.ZodString;
13
+ };
14
+ outputSchema: {
15
+ type: z.ZodEnum<{
16
+ task: "task";
17
+ comment: "comment";
18
+ project: "project";
19
+ section: "section";
20
+ }>;
21
+ id: z.ZodString;
22
+ object: z.ZodUnion<readonly [z.ZodObject<{
23
+ id: z.ZodString;
24
+ content: z.ZodString;
25
+ description: z.ZodString;
26
+ dueDate: z.ZodOptional<z.ZodString>;
27
+ recurring: z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>;
28
+ deadlineDate: z.ZodOptional<z.ZodString>;
29
+ priority: z.ZodEnum<{
30
+ p1: "p1";
31
+ p2: "p2";
32
+ p3: "p3";
33
+ p4: "p4";
34
+ }>;
35
+ projectId: z.ZodString;
36
+ sectionId: z.ZodOptional<z.ZodString>;
37
+ parentId: z.ZodOptional<z.ZodString>;
38
+ labels: z.ZodOptional<z.ZodArray<z.ZodString>>;
39
+ duration: z.ZodOptional<z.ZodString>;
40
+ responsibleUid: z.ZodOptional<z.ZodString>;
41
+ isUncompletable: z.ZodOptional<z.ZodBoolean>;
42
+ assignedByUid: z.ZodOptional<z.ZodString>;
43
+ checked: z.ZodBoolean;
44
+ completedAt: z.ZodOptional<z.ZodString>;
45
+ }, z.core.$strip>, z.ZodObject<{
46
+ id: z.ZodString;
47
+ name: z.ZodString;
48
+ color: z.ZodString;
49
+ isFavorite: z.ZodBoolean;
50
+ isShared: z.ZodBoolean;
51
+ parentId: z.ZodOptional<z.ZodString>;
52
+ inboxProject: z.ZodBoolean;
53
+ viewStyle: z.ZodString;
54
+ }, z.core.$strip>, z.ZodObject<{
55
+ id: z.ZodString;
56
+ taskId: z.ZodOptional<z.ZodString>;
57
+ projectId: z.ZodOptional<z.ZodString>;
58
+ content: z.ZodString;
59
+ postedAt: z.ZodString;
60
+ postedUid: z.ZodOptional<z.ZodString>;
61
+ fileAttachment: z.ZodOptional<z.ZodObject<{
62
+ resourceType: z.ZodString;
63
+ fileName: z.ZodOptional<z.ZodString>;
64
+ fileSize: z.ZodOptional<z.ZodNumber>;
65
+ fileType: z.ZodOptional<z.ZodString>;
66
+ fileUrl: z.ZodOptional<z.ZodString>;
67
+ fileDuration: z.ZodOptional<z.ZodNumber>;
68
+ uploadState: z.ZodOptional<z.ZodEnum<{
69
+ pending: "pending";
70
+ completed: "completed";
71
+ }>>;
72
+ url: z.ZodOptional<z.ZodString>;
73
+ title: z.ZodOptional<z.ZodString>;
74
+ image: z.ZodOptional<z.ZodString>;
75
+ imageWidth: z.ZodOptional<z.ZodNumber>;
76
+ imageHeight: z.ZodOptional<z.ZodNumber>;
77
+ }, z.core.$strip>>;
78
+ }, z.core.$strip>, z.ZodObject<{
79
+ id: z.ZodString;
80
+ name: z.ZodString;
81
+ }, z.core.$strip>]>;
82
+ };
83
+ mutability: "readonly";
84
+ execute(args: {
85
+ type: "task" | "comment" | "project" | "section";
86
+ id: string;
87
+ }, client: import('@doist/todoist-api-typescript').TodoistApi): Promise<{
88
+ textContent: string;
89
+ structuredContent: {
90
+ type: "task";
91
+ id: string;
92
+ object: {
93
+ id: string;
94
+ content: string;
95
+ description: string;
96
+ dueDate: string | undefined;
97
+ recurring: string | boolean;
98
+ deadlineDate: string | undefined;
99
+ priority: "p1" | "p2" | "p3" | "p4";
100
+ projectId: string;
101
+ sectionId: string | undefined;
102
+ parentId: string | undefined;
103
+ labels: string[];
104
+ duration: string | undefined;
105
+ responsibleUid: string | undefined;
106
+ assignedByUid: string | undefined;
107
+ checked: boolean;
108
+ completedAt: string | undefined;
109
+ };
110
+ };
111
+ } | {
112
+ textContent: string;
113
+ structuredContent: {
114
+ type: "project";
115
+ id: string;
116
+ object: {
117
+ id: string;
118
+ name: string;
119
+ color: string;
120
+ isFavorite: boolean;
121
+ isShared: boolean;
122
+ parentId: string | undefined;
123
+ inboxProject: boolean;
124
+ viewStyle: string;
125
+ };
126
+ };
127
+ } | {
128
+ textContent: string;
129
+ structuredContent: {
130
+ type: "comment";
131
+ id: string;
132
+ object: {
133
+ id: string;
134
+ taskId: string | undefined;
135
+ projectId: string | undefined;
136
+ content: string;
137
+ postedAt: string;
138
+ postedUid: string;
139
+ fileAttachment: {
140
+ resourceType: string;
141
+ fileName: string | undefined;
142
+ fileSize: number | undefined;
143
+ fileType: string | undefined;
144
+ fileUrl: string | undefined;
145
+ fileDuration: number | undefined;
146
+ uploadState: "pending" | "completed" | undefined;
147
+ url: string | undefined;
148
+ title: string | undefined;
149
+ image: string | undefined;
150
+ imageWidth: number | undefined;
151
+ imageHeight: number | undefined;
152
+ } | undefined;
153
+ };
154
+ };
155
+ } | {
156
+ textContent: string;
157
+ structuredContent: {
158
+ type: "section";
159
+ id: string;
160
+ object: {
161
+ id: string;
162
+ name: string;
163
+ };
164
+ };
165
+ }>;
166
+ };
167
+ export { fetchObject };
168
+ //# sourceMappingURL=fetch-object.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-object.d.ts","sourceRoot":"","sources":["../../src/tools/fetch-object.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAqBvB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+E8C,CAAA;AAE/D,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -1,4 +1,11 @@
1
1
  import { z } from 'zod';
2
+ type FetchResult = {
3
+ id: string;
4
+ title: string;
5
+ text: string;
6
+ url: string;
7
+ metadata?: Record<string, unknown>;
8
+ };
2
9
  /**
3
10
  * OpenAI MCP fetch tool - retrieves the full contents of a task or project by ID.
4
11
  *
@@ -23,6 +30,7 @@ declare const fetch: {
23
30
  id: string;
24
31
  }, client: import('@doist/todoist-api-typescript').TodoistApi): Promise<{
25
32
  textContent: string;
33
+ structuredContent: FetchResult;
26
34
  }>;
27
35
  };
28
36
  export { fetch };
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/tools/fetch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiCvB;;;;;GAKG;AACH,QAAA,MAAM,KAAK;;;;;;;;;;;;;;;;;;;CAwFoD,CAAA;AAE/D,OAAO,EAAE,KAAK,EAAE,CAAA"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/tools/fetch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAcvB,KAAK,WAAW,GAAG;IACf,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACrC,CAAA;AAaD;;;;;GAKG;AACH,QAAA,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;CA2FoD,CAAA;AAE/D,OAAO,EAAE,KAAK,EAAE,CAAA"}
@@ -35,6 +35,9 @@ declare const findComments: {
35
35
  imageHeight: z.ZodOptional<z.ZodNumber>;
36
36
  }, z.core.$strip>>;
37
37
  }, z.core.$strip>>;
38
+ searchType: z.ZodString;
39
+ searchId: z.ZodString;
40
+ hasMore: z.ZodBoolean;
38
41
  nextCursor: z.ZodOptional<z.ZodString>;
39
42
  totalCount: z.ZodNumber;
40
43
  };
@@ -1 +1 @@
1
- {"version":3,"file":"find-comments.d.ts","sourceRoot":"","sources":["../../src/tools/find-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiCvB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8E6C,CAAA;AAmD/D,OAAO,EAAE,YAAY,EAAE,CAAA"}
1
+ {"version":3,"file":"find-comments.d.ts","sourceRoot":"","sources":["../../src/tools/find-comments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAwCvB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8E6C,CAAA;AAmD/D,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -0,0 +1,28 @@
1
+ declare function createFindTasksByDateResource(uri: string, rawHtml: string): {
2
+ name: string;
3
+ uri: string;
4
+ mimeType: string;
5
+ text: string;
6
+ _meta: {
7
+ /**
8
+ * Renders the widget within a rounded border and shadow.
9
+ * Otherwise, the HTML is rendered full-bleed in the conversation
10
+ */
11
+ 'openai/widgetDescription': string;
12
+ 'openai/widgetPrefersBorder': boolean;
13
+ 'openai/widgetAccessible': boolean;
14
+ 'openai/toolInvocation/invoking': string;
15
+ 'openai/toolInvocation/invoked': string;
16
+ 'openai/widgetDomain': string;
17
+ /**
18
+ * Required to make external network requests from the HTML code.
19
+ * Also used to validate `openai.openExternal()` requests.
20
+ */
21
+ 'openai/widgetCSP': {
22
+ connect_domains: string[];
23
+ resource_domains: string[];
24
+ };
25
+ };
26
+ };
27
+ export { createFindTasksByDateResource };
28
+ //# sourceMappingURL=find-tasks-by-date.resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-tasks-by-date.resource.d.ts","sourceRoot":"","sources":["../../src/tools/find-tasks-by-date.resource.ts"],"names":[],"mappings":"AAAA,iBAAS,6BAA6B,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;;;;;QAOvD;;;WAGG;;;;;;;QAOH;;;WAGG;;;;;;EASd;AAED,OAAO,EAAE,6BAA6B,EAAE,CAAA"}
@@ -26,6 +26,7 @@ export declare const ToolNames: {
26
26
  readonly FIND_ACTIVITY: "find-activity";
27
27
  readonly GET_OVERVIEW: "get-overview";
28
28
  readonly DELETE_OBJECT: "delete-object";
29
+ readonly FETCH_OBJECT: "fetch-object";
29
30
  readonly USER_INFO: "user-info";
30
31
  readonly SEARCH: "search";
31
32
  readonly FETCH: "fetch";
@@ -1 +1 @@
1
- {"version":3,"file":"tool-names.d.ts","sourceRoot":"","sources":["../../src/utils/tool-names.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;CAuCZ,CAAA;AAGV,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAA"}
1
+ {"version":3,"file":"tool-names.d.ts","sourceRoot":"","sources":["../../src/utils/tool-names.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;CAwCZ,CAAA;AAGV,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAA"}
@@ -0,0 +1,7 @@
1
+ type TaskListWidget = {
2
+ fileName: string;
3
+ content: string;
4
+ };
5
+ declare function loadTaskListWidget(loadedWidget?: TaskListWidget): TaskListWidget;
6
+ export { loadTaskListWidget };
7
+ //# sourceMappingURL=widget-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-loader.d.ts","sourceRoot":"","sources":["../../src/utils/widget-loader.ts"],"names":[],"mappings":"AAEA,KAAK,cAAc,GAAG;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAClB,CAAA;AAYD,iBAAS,kBAAkB,CAAC,YAAY,GAAE,cAA+B,GAAG,cAAc,CAMzF;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-ai",
3
- "version": "6.0.0",
3
+ "version": "6.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "dist",
13
+ "!dist/dev",
13
14
  "scripts",
14
15
  "package.json",
15
16
  "LICENSE.txt",
@@ -34,6 +35,7 @@
34
35
  "postbuild": "chmod +x dist/main.js",
35
36
  "start": "npm run build && npx @modelcontextprotocol/inspector node dist/main.js",
36
37
  "dev": "concurrently \"vite build --watch\" \"npx @modelcontextprotocol/inspector npx nodemon --quiet --watch dist --ext js --exec node dist/main.js\"",
38
+ "dev:widget": "vite build --watch --minify false",
37
39
  "setup": "cp .env.example .env && npm install && npm run build",
38
40
  "test:executable": "npm run build && node scripts/test-executable.cjs",
39
41
  "type-check": "npx tsc --noEmit",
@@ -51,14 +53,18 @@
51
53
  "@doist/todoist-api-typescript": "6.2.1",
52
54
  "@modelcontextprotocol/sdk": "1.24.3",
53
55
  "date-fns": "4.1.0",
56
+ "dompurify": "3.3.1",
54
57
  "dotenv": "17.2.3",
55
58
  "zod": "4.1.13"
56
59
  },
57
60
  "devDependencies": {
58
61
  "@biomejs/biome": "2.3.8",
62
+ "@types/dompurify": "3.0.5",
59
63
  "@types/express": "5.0.6",
60
64
  "@types/morgan": "1.9.10",
61
65
  "@types/node": "22.19.2",
66
+ "@types/react": "19.2.2",
67
+ "@types/react-dom": "19.2.2",
62
68
  "concurrently": "9.2.1",
63
69
  "express": "5.2.1",
64
70
  "husky": "9.1.7",
@@ -66,6 +72,10 @@
66
72
  "morgan": "1.10.1",
67
73
  "nodemon": "3.1.11",
68
74
  "rimraf": "6.1.2",
75
+ "react": "19.2.0",
76
+ "react-dom": "19.2.0",
77
+ "snarkdown": "2.0.0",
78
+ "tsx": "4.20.6",
69
79
  "typescript": "5.9.3",
70
80
  "vite": "7.2.7",
71
81
  "vite-plugin-dts": "4.5.4",
@@ -0,0 +1,87 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, join, resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import type { BuildResult } from 'esbuild'
5
+ import { build } from 'esbuild'
6
+
7
+ const __filename = fileURLToPath(import.meta.url)
8
+ const __dirname = dirname(__filename)
9
+
10
+ const PLACEHOLDER = '<!-- INLINE_WIDGET_SCRIPT -->'
11
+
12
+ type InlineWidget = {
13
+ entryFile: string
14
+ htmlTemplate: string
15
+ outputName: string
16
+ }
17
+
18
+ export type InlineWidgetBuildResult = {
19
+ fileName: string
20
+ content: string
21
+ }
22
+
23
+ export async function buildInlineWidget(
24
+ widget: InlineWidget,
25
+ { buildTimestamp }: { buildTimestamp: string },
26
+ ): Promise<InlineWidgetBuildResult> {
27
+ const projectRoot = resolve(__dirname, '..')
28
+ const entryPath = join(projectRoot, widget.entryFile)
29
+ const templatePath = join(projectRoot, widget.htmlTemplate)
30
+ const outputFileName = `${widget.outputName}-${buildTimestamp}.html`
31
+
32
+ if (!existsSync(templatePath)) {
33
+ throw new Error(`HTML template not found: ${templatePath}`)
34
+ }
35
+ const htmlTemplateContent = readFileSync(templatePath, 'utf-8')
36
+ if (!htmlTemplateContent.includes(PLACEHOLDER)) {
37
+ throw new Error(`Placeholder ${PLACEHOLDER} not found in ${templatePath}`)
38
+ }
39
+
40
+ const result: BuildResult = await build({
41
+ entryPoints: [entryPath],
42
+ bundle: true,
43
+ write: false,
44
+ format: 'iife',
45
+ platform: 'browser',
46
+ target: 'es2020',
47
+ minify: true,
48
+ jsx: 'automatic',
49
+ globalName: 'Widget',
50
+ outdir: 'out',
51
+ loader: {
52
+ '.svg': 'dataurl',
53
+ },
54
+ })
55
+
56
+ if (!result.outputFiles || result.outputFiles.length === 0) {
57
+ throw new Error(`Failed to build ${widget.entryFile}`)
58
+ }
59
+
60
+ const jsFile = result.outputFiles.find((file) => file.path.endsWith('.js'))
61
+ const cssFile = result.outputFiles.find((file) => file.path.endsWith('.css'))
62
+
63
+ if (!jsFile) {
64
+ throw new Error(`No JavaScript output found for ${widget.entryFile}`)
65
+ }
66
+
67
+ const bundledCode = jsFile.text
68
+ const sanitizedBundledCode = bundledCode
69
+ .replace(/<\/script/gi, '<\\/script')
70
+ .replace(/<!--/g, '<\\!--')
71
+
72
+ // Build the inline style tag if CSS exists
73
+ const inlinedStyle = cssFile ? `<style>${cssFile.text}</style>` : ''
74
+
75
+ const inlinedScript = `
76
+ ${inlinedStyle}
77
+ <script>
78
+ ${sanitizedBundledCode}
79
+ if (typeof Widget !== 'undefined' && typeof Widget.renderWidget === 'function') {
80
+ Widget.renderWidget();
81
+ }
82
+ </script>
83
+ `
84
+ const finalHtml = htmlTemplateContent.replace(PLACEHOLDER, () => inlinedScript)
85
+
86
+ return { fileName: outputFileName, content: finalHtml }
87
+ }
@@ -0,0 +1,134 @@
1
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join, resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import type { Plugin, PluginContext } from 'vite'
5
+
6
+ import { buildInlineWidget, type InlineWidgetBuildResult } from './inline-widget-builder.js'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = dirname(__filename)
10
+
11
+ type InlineWidgetConfig = {
12
+ entryFile: string
13
+ htmlTemplate: string
14
+ outputName: string
15
+ }
16
+
17
+ const TASK_LIST_WIDGET: InlineWidgetConfig = {
18
+ entryFile: 'src/widgets/task-list/widget.tsx',
19
+ htmlTemplate: 'src/widgets/task-list/template.html',
20
+ outputName: 'task-list-widget',
21
+ }
22
+
23
+ const WIDGET_SOURCE_PATTERN = /src[\\/]widgets[\\/].+\.(tsx?|css|html|svg)$/
24
+ const WIDGET_FILE_EXTENSIONS = /\.(tsx?|css|html|svg)$/
25
+ const WIDGET_SOURCE_DIR = 'src/widgets'
26
+
27
+ function getWidgetSourceFiles(rootDir: string): string[] {
28
+ const widgetsDir = join(rootDir, WIDGET_SOURCE_DIR)
29
+ if (!existsSync(widgetsDir)) {
30
+ return []
31
+ }
32
+
33
+ const files: string[] = []
34
+ const entries = readdirSync(widgetsDir, { withFileTypes: true, recursive: true })
35
+
36
+ for (const entry of entries) {
37
+ if (entry.isFile() && WIDGET_FILE_EXTENSIONS.test(entry.name)) {
38
+ const parentPath = entry.parentPath ?? entry.path
39
+ files.push(join(parentPath, entry.name))
40
+ }
41
+ }
42
+
43
+ return files
44
+ }
45
+
46
+ export function inlineWidgetsVitePlugin(): Plugin {
47
+ let cachedResult: InlineWidgetBuildResult | undefined
48
+ let isWatchMode = false
49
+ let projectRoot: string
50
+ const virtualModuleId = 'virtual:todoist-ai-widgets'
51
+ const resolvedVirtualModuleId = `\0${virtualModuleId}`
52
+
53
+ async function rebuildWidget(): Promise<void> {
54
+ const buildTimestamp = process.env.BUILD_TIMESTAMP ?? Date.now().toString()
55
+ cachedResult = await buildInlineWidget(TASK_LIST_WIDGET, { buildTimestamp })
56
+ }
57
+
58
+ function writePreviewHtml(): void {
59
+ if (!cachedResult) {
60
+ return
61
+ }
62
+
63
+ const previewDir = resolve(projectRoot, 'dist', 'dev')
64
+ if (!existsSync(previewDir)) {
65
+ mkdirSync(previewDir, { recursive: true })
66
+ }
67
+
68
+ const previewPath = resolve(previewDir, `${TASK_LIST_WIDGET.outputName}.html`)
69
+ writeFileSync(previewPath, cachedResult.content, 'utf-8')
70
+ console.log(`[widgets] Preview written to ${previewPath}`)
71
+ }
72
+
73
+ function addWidgetFilesToWatch(ctx: PluginContext): void {
74
+ const widgetFiles = getWidgetSourceFiles(projectRoot)
75
+ for (const file of widgetFiles) {
76
+ ctx.addWatchFile(file)
77
+ }
78
+ if (widgetFiles.length > 0) {
79
+ console.log(`[widgets] Watching ${widgetFiles.length} widget source files`)
80
+ }
81
+ }
82
+
83
+ return {
84
+ name: 'todoist-ai-inline-widgets',
85
+ enforce: 'pre' as const,
86
+
87
+ configResolved(config) {
88
+ projectRoot = config.root
89
+ isWatchMode = config.command === 'build' && Boolean(config.build.watch)
90
+ },
91
+
92
+ async buildStart() {
93
+ await rebuildWidget()
94
+ addWidgetFilesToWatch(this)
95
+ },
96
+
97
+ async watchChange(id) {
98
+ if (WIDGET_SOURCE_PATTERN.test(id)) {
99
+ console.log(`[widgets] Detected change in ${id}, rebuilding...`)
100
+ await rebuildWidget()
101
+
102
+ // Write preview HTML in watch mode
103
+ if (isWatchMode) {
104
+ writePreviewHtml()
105
+ }
106
+ }
107
+ },
108
+
109
+ writeBundle() {
110
+ writePreviewHtml()
111
+ },
112
+
113
+ resolveId(id: string) {
114
+ if (id === virtualModuleId) {
115
+ return resolvedVirtualModuleId
116
+ }
117
+
118
+ return null
119
+ },
120
+
121
+ load(id: string) {
122
+ if (id !== resolvedVirtualModuleId || !cachedResult) {
123
+ return null
124
+ }
125
+
126
+ const moduleSource = `
127
+ const taskListWidget = ${JSON.stringify(cachedResult)};
128
+ export { taskListWidget };
129
+ export default { taskListWidget };
130
+ `
131
+ return moduleSource
132
+ },
133
+ }
134
+ }