@doist/todoist-ai 8.12.2 → 9.0.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 (170) hide show
  1. package/README.md +11 -206
  2. package/bin/_launcher.js +16 -0
  3. package/bin/todoist-ai-http.js +4 -0
  4. package/bin/todoist-ai.js +4 -0
  5. package/index.d.ts +1 -0
  6. package/index.mjs +1 -0
  7. package/package.json +20 -108
  8. package/LICENSE +0 -21
  9. package/dist/filter-helpers.d.ts +0 -51
  10. package/dist/filter-helpers.d.ts.map +0 -1
  11. package/dist/index.d.ts +0 -2954
  12. package/dist/index.d.ts.map +0 -1
  13. package/dist/index.js +0 -103
  14. package/dist/main-http.d.ts +0 -3
  15. package/dist/main-http.d.ts.map +0 -1
  16. package/dist/main-http.js +0 -41
  17. package/dist/main.d.ts +0 -3
  18. package/dist/main.d.ts.map +0 -1
  19. package/dist/main.js +0 -17
  20. package/dist/mcp-apps/index.html +0 -98
  21. package/dist/mcp-helpers.d.ts +0 -58
  22. package/dist/mcp-helpers.d.ts.map +0 -1
  23. package/dist/mcp-server-DO3M1GWl.js +0 -6132
  24. package/dist/mcp-server.d.ts +0 -16
  25. package/dist/mcp-server.d.ts.map +0 -1
  26. package/dist/middleware/require-valid-todoist-token.d.ts +0 -38
  27. package/dist/middleware/require-valid-todoist-token.d.ts.map +0 -1
  28. package/dist/prompts/productivity-analysis.d.ts +0 -58
  29. package/dist/prompts/productivity-analysis.d.ts.map +0 -1
  30. package/dist/require-valid-todoist-token-CpbXZfcm.js +0 -72
  31. package/dist/todoist-tool.d.ts +0 -62
  32. package/dist/todoist-tool.d.ts.map +0 -1
  33. package/dist/tool-execution-error.d.ts +0 -7
  34. package/dist/tool-execution-error.d.ts.map +0 -1
  35. package/dist/tool-helpers.d.ts +0 -265
  36. package/dist/tool-helpers.d.ts.map +0 -1
  37. package/dist/tools/add-comments.d.ts +0 -83
  38. package/dist/tools/add-comments.d.ts.map +0 -1
  39. package/dist/tools/add-filters.d.ts +0 -94
  40. package/dist/tools/add-filters.d.ts.map +0 -1
  41. package/dist/tools/add-labels.d.ts +0 -92
  42. package/dist/tools/add-labels.d.ts.map +0 -1
  43. package/dist/tools/add-projects.d.ts +0 -112
  44. package/dist/tools/add-projects.d.ts.map +0 -1
  45. package/dist/tools/add-reminders.d.ts +0 -148
  46. package/dist/tools/add-reminders.d.ts.map +0 -1
  47. package/dist/tools/add-sections.d.ts +0 -40
  48. package/dist/tools/add-sections.d.ts.map +0 -1
  49. package/dist/tools/add-tasks.d.ts +0 -118
  50. package/dist/tools/add-tasks.d.ts.map +0 -1
  51. package/dist/tools/analyze-project-health.d.ts +0 -45
  52. package/dist/tools/analyze-project-health.d.ts.map +0 -1
  53. package/dist/tools/complete-tasks.d.ts +0 -42
  54. package/dist/tools/complete-tasks.d.ts.map +0 -1
  55. package/dist/tools/delete-object.d.ts +0 -54
  56. package/dist/tools/delete-object.d.ts.map +0 -1
  57. package/dist/tools/fetch-object.d.ts +0 -199
  58. package/dist/tools/fetch-object.d.ts.map +0 -1
  59. package/dist/tools/fetch.d.ts +0 -41
  60. package/dist/tools/fetch.d.ts.map +0 -1
  61. package/dist/tools/find-activity.d.ts +0 -91
  62. package/dist/tools/find-activity.d.ts.map +0 -1
  63. package/dist/tools/find-comments.d.ts +0 -89
  64. package/dist/tools/find-comments.d.ts.map +0 -1
  65. package/dist/tools/find-completed-tasks.d.ts +0 -115
  66. package/dist/tools/find-completed-tasks.d.ts.map +0 -1
  67. package/dist/tools/find-filters.d.ts +0 -92
  68. package/dist/tools/find-filters.d.ts.map +0 -1
  69. package/dist/tools/find-labels.d.ts +0 -81
  70. package/dist/tools/find-labels.d.ts.map +0 -1
  71. package/dist/tools/find-project-collaborators.d.ts +0 -94
  72. package/dist/tools/find-project-collaborators.d.ts.map +0 -1
  73. package/dist/tools/find-projects.d.ts +0 -87
  74. package/dist/tools/find-projects.d.ts.map +0 -1
  75. package/dist/tools/find-reminders.d.ts +0 -95
  76. package/dist/tools/find-reminders.d.ts.map +0 -1
  77. package/dist/tools/find-sections.d.ts +0 -41
  78. package/dist/tools/find-sections.d.ts.map +0 -1
  79. package/dist/tools/find-tasks-by-date.d.ts +0 -133
  80. package/dist/tools/find-tasks-by-date.d.ts.map +0 -1
  81. package/dist/tools/find-tasks.d.ts +0 -116
  82. package/dist/tools/find-tasks.d.ts.map +0 -1
  83. package/dist/tools/get-overview.d.ts +0 -123
  84. package/dist/tools/get-overview.d.ts.map +0 -1
  85. package/dist/tools/get-productivity-stats.d.ts +0 -160
  86. package/dist/tools/get-productivity-stats.d.ts.map +0 -1
  87. package/dist/tools/get-project-activity-stats.d.ts +0 -48
  88. package/dist/tools/get-project-activity-stats.d.ts.map +0 -1
  89. package/dist/tools/get-project-health.d.ts +0 -112
  90. package/dist/tools/get-project-health.d.ts.map +0 -1
  91. package/dist/tools/get-workspace-insights.d.ts +0 -65
  92. package/dist/tools/get-workspace-insights.d.ts.map +0 -1
  93. package/dist/tools/list-workspaces.d.ts +0 -54
  94. package/dist/tools/list-workspaces.d.ts.map +0 -1
  95. package/dist/tools/manage-assignments.d.ts +0 -72
  96. package/dist/tools/manage-assignments.d.ts.map +0 -1
  97. package/dist/tools/project-management.d.ts +0 -78
  98. package/dist/tools/project-management.d.ts.map +0 -1
  99. package/dist/tools/project-move.d.ts +0 -88
  100. package/dist/tools/project-move.d.ts.map +0 -1
  101. package/dist/tools/reorder-objects.d.ts +0 -50
  102. package/dist/tools/reorder-objects.d.ts.map +0 -1
  103. package/dist/tools/reschedule-tasks.d.ts +0 -78
  104. package/dist/tools/reschedule-tasks.d.ts.map +0 -1
  105. package/dist/tools/search.d.ts +0 -43
  106. package/dist/tools/search.d.ts.map +0 -1
  107. package/dist/tools/uncomplete-tasks.d.ts +0 -42
  108. package/dist/tools/uncomplete-tasks.d.ts.map +0 -1
  109. package/dist/tools/update-comments.d.ts +0 -87
  110. package/dist/tools/update-comments.d.ts.map +0 -1
  111. package/dist/tools/update-filters.d.ts +0 -106
  112. package/dist/tools/update-filters.d.ts.map +0 -1
  113. package/dist/tools/update-labels.d.ts +0 -122
  114. package/dist/tools/update-labels.d.ts.map +0 -1
  115. package/dist/tools/update-projects.d.ts +0 -120
  116. package/dist/tools/update-projects.d.ts.map +0 -1
  117. package/dist/tools/update-reminders.d.ts +0 -148
  118. package/dist/tools/update-reminders.d.ts.map +0 -1
  119. package/dist/tools/update-sections.d.ts +0 -42
  120. package/dist/tools/update-sections.d.ts.map +0 -1
  121. package/dist/tools/update-tasks.d.ts +0 -113
  122. package/dist/tools/update-tasks.d.ts.map +0 -1
  123. package/dist/tools/user-info.d.ts +0 -57
  124. package/dist/tools/user-info.d.ts.map +0 -1
  125. package/dist/tools/view-attachment.d.ts +0 -25
  126. package/dist/tools/view-attachment.d.ts.map +0 -1
  127. package/dist/usage-tracking.d.ts +0 -27
  128. package/dist/usage-tracking.d.ts.map +0 -1
  129. package/dist/utils/assignment-validator.d.ts +0 -69
  130. package/dist/utils/assignment-validator.d.ts.map +0 -1
  131. package/dist/utils/colors.d.ts +0 -68
  132. package/dist/utils/colors.d.ts.map +0 -1
  133. package/dist/utils/constants.d.ts +0 -53
  134. package/dist/utils/constants.d.ts.map +0 -1
  135. package/dist/utils/date.d.ts +0 -18
  136. package/dist/utils/date.d.ts.map +0 -1
  137. package/dist/utils/duration-parser.d.ts +0 -36
  138. package/dist/utils/duration-parser.d.ts.map +0 -1
  139. package/dist/utils/filter-resolver.d.ts +0 -27
  140. package/dist/utils/filter-resolver.d.ts.map +0 -1
  141. package/dist/utils/labels.d.ts +0 -13
  142. package/dist/utils/labels.d.ts.map +0 -1
  143. package/dist/utils/output-schemas.d.ts +0 -199
  144. package/dist/utils/output-schemas.d.ts.map +0 -1
  145. package/dist/utils/priorities.d.ts +0 -14
  146. package/dist/utils/priorities.d.ts.map +0 -1
  147. package/dist/utils/reminder-schemas.d.ts +0 -33
  148. package/dist/utils/reminder-schemas.d.ts.map +0 -1
  149. package/dist/utils/response-builders.d.ts +0 -93
  150. package/dist/utils/response-builders.d.ts.map +0 -1
  151. package/dist/utils/retry.d.ts +0 -10
  152. package/dist/utils/retry.d.ts.map +0 -1
  153. package/dist/utils/sanitize-data.d.ts +0 -10
  154. package/dist/utils/sanitize-data.d.ts.map +0 -1
  155. package/dist/utils/schema-helpers.d.ts +0 -10
  156. package/dist/utils/schema-helpers.d.ts.map +0 -1
  157. package/dist/utils/test-helpers.d.ts +0 -91
  158. package/dist/utils/test-helpers.d.ts.map +0 -1
  159. package/dist/utils/tool-names.d.ts +0 -56
  160. package/dist/utils/tool-names.d.ts.map +0 -1
  161. package/dist/utils/user-resolver.d.ts +0 -39
  162. package/dist/utils/user-resolver.d.ts.map +0 -1
  163. package/dist/utils/validate-todoist-token.d.ts +0 -9
  164. package/dist/utils/validate-todoist-token.d.ts.map +0 -1
  165. package/dist/utils/workspace-resolver.d.ts +0 -31
  166. package/dist/utils/workspace-resolver.d.ts.map +0 -1
  167. package/scripts/bump-plugin-version.mjs +0 -14
  168. package/scripts/run-tool.ts +0 -240
  169. package/scripts/test-executable.cjs +0 -69
  170. package/scripts/validate-schemas.ts +0 -284
@@ -1,240 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * Run any Todoist tool directly without going through MCP.
4
- *
5
- * Usage:
6
- * npx tsx scripts/run-tool.ts <tool-name> '<json-args>'
7
- * npx tsx scripts/run-tool.ts <tool-name> --file <args.json>
8
- * npx tsx scripts/run-tool.ts --list
9
- *
10
- * Examples:
11
- * npx tsx scripts/run-tool.ts add-tasks '{"tasks":[{"content":"Test task","order":1}]}'
12
- * npx tsx scripts/run-tool.ts find-tasks '{"searchText":"meeting"}'
13
- * npx tsx scripts/run-tool.ts get-overview '{}'
14
- *
15
- * Requires TODOIST_API_KEY in .env file (and optionally TODOIST_BASE_URL).
16
- */
17
- import { readFileSync } from 'node:fs'
18
- import { TodoistApi } from '@doist/todoist-sdk'
19
- import { config } from 'dotenv'
20
- import { addComments } from '../src/tools/add-comments.js'
21
- import { addFilters } from '../src/tools/add-filters.js'
22
- import { addLabels } from '../src/tools/add-labels.js'
23
- import { addProjects } from '../src/tools/add-projects.js'
24
- import { addSections } from '../src/tools/add-sections.js'
25
- import { addTasks } from '../src/tools/add-tasks.js'
26
- import { analyzeProjectHealth } from '../src/tools/analyze-project-health.js'
27
- import { completeTasks } from '../src/tools/complete-tasks.js'
28
- import { deleteObject } from '../src/tools/delete-object.js'
29
- import { fetchObject } from '../src/tools/fetch-object.js'
30
- import { fetch } from '../src/tools/fetch.js'
31
- import { findActivity } from '../src/tools/find-activity.js'
32
- import { findComments } from '../src/tools/find-comments.js'
33
- import { findCompletedTasks } from '../src/tools/find-completed-tasks.js'
34
- import { findFilters } from '../src/tools/find-filters.js'
35
- import { findLabels } from '../src/tools/find-labels.js'
36
- import { findProjectCollaborators } from '../src/tools/find-project-collaborators.js'
37
- import { findProjects } from '../src/tools/find-projects.js'
38
- import { findSections } from '../src/tools/find-sections.js'
39
- import { findTasksByDate } from '../src/tools/find-tasks-by-date.js'
40
- import { findTasks } from '../src/tools/find-tasks.js'
41
- import { getOverview } from '../src/tools/get-overview.js'
42
- import { getProjectActivityStats } from '../src/tools/get-project-activity-stats.js'
43
- import { getProjectHealth } from '../src/tools/get-project-health.js'
44
- import { getWorkspaceInsights } from '../src/tools/get-workspace-insights.js'
45
- import { listWorkspaces } from '../src/tools/list-workspaces.js'
46
- import { manageAssignments } from '../src/tools/manage-assignments.js'
47
- import { projectManagement } from '../src/tools/project-management.js'
48
- import { projectMove } from '../src/tools/project-move.js'
49
- import { reorderObjects } from '../src/tools/reorder-objects.js'
50
- import { rescheduleTasks } from '../src/tools/reschedule-tasks.js'
51
- import { search } from '../src/tools/search.js'
52
- import { uncompleteTasks } from '../src/tools/uncomplete-tasks.js'
53
- import { updateComments } from '../src/tools/update-comments.js'
54
- import { updateFilters } from '../src/tools/update-filters.js'
55
- import { updateLabels } from '../src/tools/update-labels.js'
56
- import { updateProjects } from '../src/tools/update-projects.js'
57
- import { updateSections } from '../src/tools/update-sections.js'
58
- import { updateTasks } from '../src/tools/update-tasks.js'
59
- import { userInfo } from '../src/tools/user-info.js'
60
- import { viewAttachment } from '../src/tools/view-attachment.js'
61
- import { createTodoistClient, runWithUsageTrackingContext } from '../src/usage-tracking.js'
62
-
63
- config()
64
-
65
- // Define a minimal type for tool execution that works with any tool
66
- type ExecutableTool = {
67
- name: string
68
- description: string
69
- execute: (
70
- // oxlint-disable-next-line @typescript-eslint/no-explicit-any -- tools have varying parameter schemas
71
- args: any,
72
- client: TodoistApi,
73
- ) => Promise<{ textContent?: string; structuredContent?: unknown; contentItems?: unknown[] }>
74
- }
75
-
76
- const tools: Record<string, ExecutableTool> = {
77
- 'add-tasks': addTasks,
78
- 'add-projects': addProjects,
79
- 'add-filters': addFilters,
80
- 'add-sections': addSections,
81
- 'add-comments': addComments,
82
- 'add-labels': addLabels,
83
- 'complete-tasks': completeTasks,
84
- 'uncomplete-tasks': uncompleteTasks,
85
- 'delete-object': deleteObject,
86
- fetch: fetch,
87
- 'fetch-object': fetchObject,
88
- 'find-activity': findActivity,
89
- 'find-comments': findComments,
90
- 'find-filters': findFilters,
91
- 'find-completed-tasks': findCompletedTasks,
92
- 'find-labels': findLabels,
93
- 'find-project-collaborators': findProjectCollaborators,
94
- 'find-projects': findProjects,
95
- 'find-sections': findSections,
96
- 'find-tasks': findTasks,
97
- 'find-tasks-by-date': findTasksByDate,
98
- 'get-overview': getOverview,
99
- 'get-project-health': getProjectHealth,
100
- 'get-project-activity-stats': getProjectActivityStats,
101
- 'analyze-project-health': analyzeProjectHealth,
102
- 'get-workspace-insights': getWorkspaceInsights,
103
- 'list-workspaces': listWorkspaces,
104
- 'manage-assignments': manageAssignments,
105
- 'project-management': projectManagement,
106
- 'project-move': projectMove,
107
- 'reorder-objects': reorderObjects,
108
- 'reschedule-tasks': rescheduleTasks,
109
- search: search,
110
- 'update-comments': updateComments,
111
- 'update-filters': updateFilters,
112
- 'update-labels': updateLabels,
113
- 'update-projects': updateProjects,
114
- 'update-sections': updateSections,
115
- 'update-tasks': updateTasks,
116
- 'user-info': userInfo,
117
- 'view-attachment': viewAttachment,
118
- }
119
-
120
- function printUsage() {
121
- console.log(`
122
- Usage:
123
- npx tsx scripts/run-tool.ts <tool-name> '<json-args>'
124
- npx tsx scripts/run-tool.ts <tool-name> --file <args.json>
125
- npx tsx scripts/run-tool.ts --list
126
-
127
- Available tools:
128
- ${Object.keys(tools)
129
- .sort()
130
- .map((name) => ` - ${name}`)
131
- .join('\n')}
132
- `)
133
- }
134
-
135
- async function main() {
136
- const args = process.argv.slice(2)
137
-
138
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
139
- printUsage()
140
- process.exit(0)
141
- }
142
-
143
- if (args[0] === '--list') {
144
- console.log('Available tools:')
145
- for (const name of Object.keys(tools).sort()) {
146
- const tool = tools[name]
147
- console.log(`\n${name}:`)
148
- console.log(` ${tool.description}`)
149
- }
150
- process.exit(0)
151
- }
152
-
153
- const toolName = args[0]
154
- const tool = tools[toolName]
155
-
156
- if (!tool) {
157
- console.error(`Unknown tool: ${toolName}`)
158
- console.error(`Available tools: ${Object.keys(tools).sort().join(', ')}`)
159
- process.exit(1)
160
- }
161
-
162
- let jsonArgs: string
163
- if (args[1] === '--file') {
164
- if (!args[2]) {
165
- console.error('--file requires a path argument')
166
- process.exit(1)
167
- }
168
- jsonArgs = readFileSync(args[2], 'utf-8')
169
- } else {
170
- jsonArgs = args[1] || '{}'
171
- }
172
-
173
- let parsedArgs: unknown
174
- try {
175
- parsedArgs = JSON.parse(jsonArgs)
176
- } catch (e) {
177
- console.error('Invalid JSON args:', e)
178
- process.exit(1)
179
- }
180
-
181
- const apiKey = process.env.TODOIST_API_KEY
182
- if (!apiKey) {
183
- console.error('TODOIST_API_KEY not found in environment or .env file')
184
- process.exit(1)
185
- }
186
-
187
- const baseUrl = process.env.TODOIST_BASE_URL
188
- const client = createTodoistClient(apiKey, {
189
- baseUrl,
190
- // Local direct runs are a dev helper, not real MCP traffic.
191
- tracking: { enabled: false },
192
- })
193
-
194
- console.log(`Running ${toolName} with args:`)
195
- console.log(JSON.stringify(parsedArgs, null, 2))
196
- console.log('---')
197
-
198
- try {
199
- const result = await runWithUsageTrackingContext(tool.name, () =>
200
- tool.execute(parsedArgs, client),
201
- )
202
-
203
- if (result.textContent) {
204
- console.log('\nText output:')
205
- console.log(result.textContent)
206
- }
207
-
208
- if (result.structuredContent) {
209
- console.log('\nStructured output:')
210
- console.log(JSON.stringify(result.structuredContent, null, 2))
211
- }
212
-
213
- if (result.contentItems?.length) {
214
- console.log(`\nContent items: ${result.contentItems.length}`)
215
- for (const item of result.contentItems) {
216
- const entry = item as Record<string, unknown>
217
- if (entry.type === 'image') {
218
- const data = entry.data as string
219
- console.log(
220
- ` [image] ${entry.mimeType} (${Math.round((data.length * 0.75) / 1024)}KB base64)`,
221
- )
222
- } else if (entry.type === 'text') {
223
- const text = entry.text as string
224
- console.log(` [text] ${text.length > 200 ? `${text.slice(0, 200)}...` : text}`)
225
- } else if (entry.type === 'resource') {
226
- const resource = entry.resource as Record<string, unknown>
227
- const blob = resource.blob as string
228
- console.log(
229
- ` [resource] ${resource.mimeType} (${Math.round((blob.length * 0.75) / 1024)}KB base64)`,
230
- )
231
- }
232
- }
233
- }
234
- } catch (error) {
235
- console.error('Tool execution failed:', error)
236
- process.exit(1)
237
- }
238
- }
239
-
240
- main()
@@ -1,69 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const { spawn } = require('node:child_process')
4
- const path = require('node:path')
5
-
6
- console.log('Testing MCP server executable...')
7
-
8
- const mainJs = path.join(__dirname, '..', 'dist', 'main.js')
9
- const child = spawn('node', [mainJs], {
10
- stdio: ['pipe', 'pipe', 'pipe'],
11
- })
12
-
13
- let _stdoutOutput = ''
14
- let stderrOutput = ''
15
- let hasError = false
16
-
17
- child.stdout.on('data', (data) => {
18
- _stdoutOutput += data.toString()
19
- })
20
-
21
- child.stderr.on('data', (data) => {
22
- const output = data.toString()
23
- stderrOutput += output
24
-
25
- // Only consider it an error if it's not related to graceful shutdown
26
- if (output.includes('Error:') && !output.includes('SIGTERM') && !output.includes('SIGKILL')) {
27
- console.error('Server startup error detected:', output)
28
- hasError = true
29
- }
30
- })
31
-
32
- child.on('error', (error) => {
33
- console.error('Failed to start MCP server:', error.message)
34
- hasError = true
35
- process.exit(1)
36
- })
37
-
38
- child.on('exit', (code, signal) => {
39
- // Expected signals when we kill the process
40
- if (signal === 'SIGTERM' || signal === 'SIGKILL') {
41
- return // This is expected
42
- }
43
-
44
- // Unexpected exit codes during startup
45
- if (code !== null && code !== 0) {
46
- console.error(`Server exited unexpectedly with code ${code}`)
47
- hasError = true
48
- }
49
- })
50
-
51
- // Kill the process after 2 seconds (MCP server should start successfully)
52
- setTimeout(() => {
53
- if (hasError) {
54
- console.error('❌ MCP server failed to start properly')
55
- if (stderrOutput.trim()) {
56
- console.error('Error output:', stderrOutput.trim())
57
- }
58
- process.exit(1)
59
- }
60
-
61
- // Gracefully terminate
62
- child.kill('SIGTERM')
63
-
64
- setTimeout(() => {
65
- console.log('✅ MCP server executable test passed')
66
- console.log('Server started successfully and is ready to accept connections')
67
- process.exit(0)
68
- }, 200) // Give it a moment to clean up
69
- }, 2000)
@@ -1,284 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Schema Validation Script for Todoist AI MCP Server
5
- *
6
- * This script validates that all tool parameter schemas follow Gemini API compatibility rules.
7
- * Specifically, it checks that no Zod string schemas use both .nullable() and .optional().
8
- *
9
- * This version imports the actual tools from the compiled index.js and validates their
10
- * runtime Zod schemas for maximum accuracy.
11
- *
12
- * Usage:
13
- * npm run build && node scripts/validate-schemas.js
14
- * npm run build && node scripts/validate-schemas.js --verbose
15
- * npm run build && node scripts/validate-schemas.js --json
16
- */
17
-
18
- import { z } from 'zod'
19
-
20
- type ValidationIssue = {
21
- toolName: string
22
- parameterPath: string
23
- issue: string
24
- suggestion: string
25
- }
26
-
27
- type ValidationResult = {
28
- success: boolean
29
- issues: ValidationIssue[]
30
- toolsChecked: number
31
- parametersChecked: number
32
- }
33
-
34
- type AnyZodSchema = z.ZodTypeAny | { _zod: { def: unknown } }
35
-
36
- /**
37
- * Recursively walk a Zod schema and detect problematic patterns
38
- */
39
- function walkZodSchema(
40
- schema: AnyZodSchema,
41
- path: string,
42
- issues: ValidationIssue[],
43
- toolName: string,
44
- ): void {
45
- // Check for ZodOptional containing a ZodNullable ZodString
46
- if (schema instanceof z.ZodOptional) {
47
- const innerSchema = schema.unwrap()
48
- if (innerSchema instanceof z.ZodNullable) {
49
- const nullableInner = innerSchema.unwrap()
50
- if (nullableInner instanceof z.ZodString) {
51
- issues.push({
52
- toolName,
53
- parameterPath: path,
54
- issue: 'GEMINI_API_INCOMPATIBLE: z.string().nullable().optional() pattern detected',
55
- suggestion:
56
- 'REQUIRED FIX: Change "z.string().nullable().optional()" to "z.string().optional()" and use special strings like "remove" or "unassign" in description to handle clearing. This pattern causes HTTP 400 errors in Google Gemini API due to OpenAPI 3.1 nullable type incompatibility.',
57
- })
58
- }
59
- }
60
- }
61
-
62
- // Check for ZodNullable containing a ZodOptional ZodString
63
- if (schema instanceof z.ZodNullable) {
64
- const innerSchema = schema.unwrap()
65
- if (innerSchema instanceof z.ZodOptional) {
66
- const optionalInner = innerSchema.unwrap()
67
- if (optionalInner instanceof z.ZodString) {
68
- issues.push({
69
- toolName,
70
- parameterPath: path,
71
- issue: 'GEMINI_API_INCOMPATIBLE: z.string().optional().nullable() pattern detected',
72
- suggestion:
73
- 'REQUIRED FIX: Change "z.string().optional().nullable()" to "z.string().optional()" and use special strings like "remove" or "unassign" in description to handle clearing. This pattern causes HTTP 400 errors in Google Gemini API due to OpenAPI 3.1 nullable type incompatibility.',
74
- })
75
- }
76
- }
77
- }
78
-
79
- // Recursively check nested schemas
80
- if (schema instanceof z.ZodObject) {
81
- const shape = schema.shape
82
- for (const [key, value] of Object.entries(shape)) {
83
- const newPath = path ? `${path}.${key}` : key
84
- walkZodSchema(value as AnyZodSchema, newPath, issues, toolName)
85
- }
86
- } else if (schema instanceof z.ZodArray) {
87
- const element = (schema as unknown as { _zod: { def: { element: AnyZodSchema } } })._zod.def
88
- .element
89
- walkZodSchema(element, `${path}[]`, issues, toolName)
90
- } else if (
91
- schema instanceof z.ZodOptional ||
92
- schema instanceof z.ZodNullable ||
93
- schema instanceof z.ZodDefault
94
- ) {
95
- walkZodSchema(schema.unwrap() as AnyZodSchema, path, issues, toolName)
96
- } else if (schema instanceof z.ZodUnion) {
97
- const options = (schema as unknown as { _zod: { def: { options: AnyZodSchema[] } } })._zod
98
- .def.options
99
- options.forEach((option: AnyZodSchema, index: number) => {
100
- walkZodSchema(option, `${path}[union:${index}]`, issues, toolName)
101
- })
102
- } else if (schema instanceof z.ZodDiscriminatedUnion) {
103
- const options = (schema as unknown as { _zod: { def: { options: AnyZodSchema[] } } })._zod
104
- .def.options
105
- options.forEach((option: AnyZodSchema, index: number) => {
106
- walkZodSchema(option, `${path}[union:${index}]`, issues, toolName)
107
- })
108
- } else if (schema instanceof z.ZodIntersection) {
109
- const left = (schema as unknown as { _zod: { def: { left: AnyZodSchema } } })._zod.def.left
110
- const right = (schema as unknown as { _zod: { def: { right: AnyZodSchema } } })._zod.def
111
- .right
112
- walkZodSchema(left, `${path}[left]`, issues, toolName)
113
- walkZodSchema(right, `${path}[right]`, issues, toolName)
114
- } else if (schema instanceof z.ZodRecord) {
115
- const valueType = (schema as unknown as { _zod: { def: { valueType: AnyZodSchema } } })._zod
116
- .def.valueType
117
- walkZodSchema(valueType, `${path}[value]`, issues, toolName)
118
- } else if (schema instanceof z.ZodTuple) {
119
- const items = (schema as unknown as { _zod: { def: { items: AnyZodSchema[] } } })._zod.def
120
- .items
121
- items.forEach((item: AnyZodSchema, index: number) => {
122
- walkZodSchema(item, `${path}[${index}]`, issues, toolName)
123
- })
124
- }
125
- }
126
-
127
- /**
128
- * Validate a single tool's parameter schema
129
- */
130
- function validateToolSchema(tool: {
131
- name?: string
132
- parameters?: Record<string, z.ZodTypeAny>
133
- }): ValidationIssue[] {
134
- const issues: ValidationIssue[] = []
135
- const toolName = tool.name || 'unknown'
136
-
137
- if (!tool.parameters) {
138
- return issues
139
- }
140
-
141
- try {
142
- const schema = z.object(tool.parameters)
143
- walkZodSchema(schema, '', issues, toolName)
144
- } catch (error) {
145
- issues.push({
146
- toolName,
147
- parameterPath: 'root',
148
- issue: `Failed to analyze schema: ${error}`,
149
- suggestion: 'Check that the tool parameters are valid Zod schemas',
150
- })
151
- }
152
-
153
- return issues
154
- }
155
-
156
- /**
157
- * Main validation function using runtime schema analysis
158
- */
159
- async function validateAllSchemas(verbose: boolean = false): Promise<ValidationResult> {
160
- try {
161
- const { tools } = await import(`${process.cwd()}/dist/index.js`)
162
-
163
- const allIssues: ValidationIssue[] = []
164
- let totalParameters = 0
165
- const toolNames = Object.keys(tools)
166
-
167
- for (const toolName of toolNames) {
168
- const tool = tools[toolName]
169
- const toolIssues = validateToolSchema(tool)
170
- allIssues.push(...toolIssues)
171
-
172
- // Count parameters for stats
173
- if (tool.parameters) {
174
- try {
175
- const schema = z.object(tool.parameters)
176
- const shape = schema.shape
177
- if (shape) {
178
- totalParameters += Object.keys(shape).length
179
- }
180
- } catch {
181
- // Skip counting if schema is invalid
182
- }
183
- }
184
-
185
- if (verbose) {
186
- const issueCount = toolIssues.length
187
- const status = issueCount === 0 ? '✅' : `❌ (${issueCount} issues)`
188
- const paramCount = tool.parameters ? Object.keys(tool.parameters).length : 0
189
- console.log(`${status} ${toolName} (${paramCount} parameters)`)
190
-
191
- if (issueCount > 0) {
192
- toolIssues.forEach((issue) => {
193
- console.log(` ${issue.parameterPath}: ${issue.issue}`)
194
- })
195
- }
196
- }
197
- }
198
-
199
- return {
200
- success: allIssues.length === 0,
201
- issues: allIssues,
202
- toolsChecked: toolNames.length,
203
- parametersChecked: totalParameters,
204
- }
205
- } catch (error) {
206
- return {
207
- success: false,
208
- issues: [
209
- {
210
- toolName: 'system',
211
- parameterPath: 'import',
212
- issue: `Failed to import tools: ${error}`,
213
- suggestion: 'Ensure the project is built and dist/index.js exists',
214
- },
215
- ],
216
- toolsChecked: 0,
217
- parametersChecked: 0,
218
- }
219
- }
220
- }
221
-
222
- /**
223
- * CLI interface
224
- */
225
- async function main() {
226
- const args = process.argv.slice(2)
227
- const verbose = args.includes('--verbose')
228
- const jsonOutput = args.includes('--json')
229
-
230
- try {
231
- const result = await validateAllSchemas(verbose)
232
-
233
- if (jsonOutput) {
234
- console.log(JSON.stringify(result, null, 2))
235
- } else {
236
- if (result.success) {
237
- console.log('✅ Schema validation passed!')
238
- console.log(
239
- ` Checked ${result.toolsChecked} tools with ${result.parametersChecked} parameters`,
240
- )
241
- console.log(
242
- ' All schemas are Gemini API compatible (no .nullable() on optional strings)',
243
- )
244
- } else {
245
- console.log('❌ Schema validation failed!')
246
- console.log(
247
- ` Found ${result.issues.length} issue(s) in ${result.toolsChecked} tools:\n`,
248
- )
249
-
250
- result.issues.forEach((issue, index) => {
251
- console.log(`\n${index + 1}. 🚫 VALIDATION FAILURE`)
252
- console.log(` Tool: ${issue.toolName}`)
253
- console.log(` Parameter: ${issue.parameterPath}`)
254
- console.log(` Issue: ${issue.issue}`)
255
- console.log(` Action Required: ${issue.suggestion}`)
256
- console.log(` File Location: src/tools/${issue.toolName}.ts`)
257
- console.log(` Fix Pattern: Remove .nullable() from the parameter schema`)
258
- console.log(
259
- ` Example Fix: Change z.string().nullable().optional() → z.string().optional()`,
260
- )
261
- console.log(
262
- ` ⚠️ This validation failure will cause Gemini API HTTP 400 errors\n`,
263
- )
264
- })
265
- }
266
- }
267
-
268
- process.exit(result.success ? 0 : 1)
269
- } catch (error) {
270
- console.error('Fatal error during schema validation:', error)
271
- process.exit(1)
272
- }
273
- }
274
-
275
- // Run if this script is executed directly
276
- if (
277
- process.argv[1]?.endsWith('validate-schemas.ts') ||
278
- process.argv[1]?.endsWith('validate-schemas.js')
279
- ) {
280
- main()
281
- }
282
-
283
- export type { ValidationIssue, ValidationResult }
284
- export { validateAllSchemas }