@doist/todoist-ai 4.15.0 → 4.15.1
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 +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/tools/__tests__/update-projects.test.js +0 -18
- package/dist/tools/find-activity.js +1 -1
- package/dist/tools/find-tasks.d.ts.map +1 -1
- package/dist/tools/find-tasks.js +8 -3
- package/dist/tools/update-projects.d.ts +0 -4
- package/dist/tools/update-projects.d.ts.map +1 -1
- package/dist/tools/update-projects.js +0 -4
- package/package.json +14 -4
- package/scripts/validate-schemas.ts +280 -0
package/dist/index.d.ts
CHANGED
|
@@ -605,7 +605,6 @@ declare const tools: {
|
|
|
605
605
|
projects: import("zod").ZodArray<import("zod").ZodObject<{
|
|
606
606
|
id: import("zod").ZodString;
|
|
607
607
|
name: import("zod").ZodOptional<import("zod").ZodString>;
|
|
608
|
-
parentId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
609
608
|
isFavorite: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
610
609
|
viewStyle: import("zod").ZodOptional<import("zod").ZodEnum<["list", "board", "calendar"]>>;
|
|
611
610
|
}, "strip", import("zod").ZodTypeAny, {
|
|
@@ -613,13 +612,11 @@ declare const tools: {
|
|
|
613
612
|
name?: string | undefined;
|
|
614
613
|
isFavorite?: boolean | undefined;
|
|
615
614
|
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
616
|
-
parentId?: string | undefined;
|
|
617
615
|
}, {
|
|
618
616
|
id: string;
|
|
619
617
|
name?: string | undefined;
|
|
620
618
|
isFavorite?: boolean | undefined;
|
|
621
619
|
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
622
|
-
parentId?: string | undefined;
|
|
623
620
|
}>, "many">;
|
|
624
621
|
};
|
|
625
622
|
execute(args: {
|
|
@@ -628,7 +625,6 @@ declare const tools: {
|
|
|
628
625
|
name?: string | undefined;
|
|
629
626
|
isFavorite?: boolean | undefined;
|
|
630
627
|
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
631
|
-
parentId?: string | undefined;
|
|
632
628
|
}[];
|
|
633
629
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
634
630
|
content: {
|
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,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAExC,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,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC1C,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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"}
|
|
@@ -104,24 +104,6 @@ describe(`${UPDATE_PROJECTS} tool`, () => {
|
|
|
104
104
|
expect(textContent).toContain('Updated 1 project:');
|
|
105
105
|
expect(textContent).toContain('Updated Favorite Project (id=project-123)');
|
|
106
106
|
});
|
|
107
|
-
it('should update project with parentId to move it to a sub-project', async () => {
|
|
108
|
-
const mockApiResponse = createMockProject({
|
|
109
|
-
id: 'project-child',
|
|
110
|
-
name: 'Child Project',
|
|
111
|
-
parentId: 'project-parent',
|
|
112
|
-
});
|
|
113
|
-
mockTodoistApi.updateProject.mockResolvedValue(mockApiResponse);
|
|
114
|
-
const result = await updateProjects.execute({
|
|
115
|
-
projects: [{ id: 'project-child', parentId: 'project-parent' }],
|
|
116
|
-
}, mockTodoistApi);
|
|
117
|
-
expect(mockTodoistApi.updateProject).toHaveBeenCalledWith('project-child', {
|
|
118
|
-
parentId: 'project-parent',
|
|
119
|
-
});
|
|
120
|
-
const textContent = extractTextContent(result);
|
|
121
|
-
expect(textContent).toMatchSnapshot();
|
|
122
|
-
expect(textContent).toContain('Updated 1 project:');
|
|
123
|
-
expect(textContent).toContain('Child Project (id=project-child)');
|
|
124
|
-
});
|
|
125
107
|
});
|
|
126
108
|
describe('updating multiple projects', () => {
|
|
127
109
|
it('should update multiple projects and return mapped results', async () => {
|
|
@@ -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;AA2DvB,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwL2B,CAAA;AA2K1C,OAAO,EAAE,SAAS,EAAE,CAAA"}
|
package/dist/tools/find-tasks.js
CHANGED
|
@@ -9,7 +9,10 @@ import { ToolNames } from '../utils/tool-names.js';
|
|
|
9
9
|
const { FIND_COMPLETED_TASKS, ADD_TASKS } = ToolNames;
|
|
10
10
|
const ArgsSchema = {
|
|
11
11
|
searchText: z.string().optional().describe('The text to search for in tasks.'),
|
|
12
|
-
projectId: z
|
|
12
|
+
projectId: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Find tasks in this project. Project ID should be an ID string, or the text "inbox", for inbox tasks.'),
|
|
13
16
|
sectionId: z.string().optional().describe('Find tasks in this section.'),
|
|
14
17
|
parentId: z.string().optional().describe('Find subtasks of this parent task.'),
|
|
15
18
|
responsibleUser: z
|
|
@@ -60,8 +63,10 @@ const findTasks = {
|
|
|
60
63
|
limit,
|
|
61
64
|
cursor: cursor ?? null,
|
|
62
65
|
};
|
|
63
|
-
if (projectId)
|
|
64
|
-
taskParams.projectId =
|
|
66
|
+
if (projectId) {
|
|
67
|
+
taskParams.projectId =
|
|
68
|
+
projectId === 'inbox' ? todoistUser.inboxProjectId : projectId;
|
|
69
|
+
}
|
|
65
70
|
if (sectionId)
|
|
66
71
|
taskParams.sectionId = sectionId;
|
|
67
72
|
if (parentId)
|
|
@@ -6,7 +6,6 @@ declare const updateProjects: {
|
|
|
6
6
|
projects: z.ZodArray<z.ZodObject<{
|
|
7
7
|
id: z.ZodString;
|
|
8
8
|
name: z.ZodOptional<z.ZodString>;
|
|
9
|
-
parentId: z.ZodOptional<z.ZodString>;
|
|
10
9
|
isFavorite: z.ZodOptional<z.ZodBoolean>;
|
|
11
10
|
viewStyle: z.ZodOptional<z.ZodEnum<["list", "board", "calendar"]>>;
|
|
12
11
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -14,13 +13,11 @@ declare const updateProjects: {
|
|
|
14
13
|
name?: string | undefined;
|
|
15
14
|
isFavorite?: boolean | undefined;
|
|
16
15
|
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
17
|
-
parentId?: string | undefined;
|
|
18
16
|
}, {
|
|
19
17
|
id: string;
|
|
20
18
|
name?: string | undefined;
|
|
21
19
|
isFavorite?: boolean | undefined;
|
|
22
20
|
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
23
|
-
parentId?: string | undefined;
|
|
24
21
|
}>, "many">;
|
|
25
22
|
};
|
|
26
23
|
execute(args: {
|
|
@@ -29,7 +26,6 @@ declare const updateProjects: {
|
|
|
29
26
|
name?: string | undefined;
|
|
30
27
|
isFavorite?: boolean | undefined;
|
|
31
28
|
viewStyle?: "list" | "board" | "calendar" | undefined;
|
|
32
|
-
parentId?: string | undefined;
|
|
33
29
|
}[];
|
|
34
30
|
}, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
|
|
35
31
|
content: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-projects.d.ts","sourceRoot":"","sources":["../../src/tools/update-projects.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"update-projects.d.ts","sourceRoot":"","sources":["../../src/tools/update-projects.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAqBvB,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCsB,CAAA;AAuD1C,OAAO,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -6,10 +6,6 @@ const { FIND_PROJECTS, FIND_TASKS, GET_OVERVIEW } = ToolNames;
|
|
|
6
6
|
const ProjectUpdateSchema = z.object({
|
|
7
7
|
id: z.string().min(1).describe('The ID of the project to update.'),
|
|
8
8
|
name: z.string().min(1).optional().describe('The new name of the project.'),
|
|
9
|
-
parentId: z
|
|
10
|
-
.string()
|
|
11
|
-
.optional()
|
|
12
|
-
.describe('The ID of the parent project. If provided, moves this project to be a sub-project.'),
|
|
13
9
|
isFavorite: z.boolean().optional().describe('Whether the project is a favorite.'),
|
|
14
10
|
viewStyle: z.enum(['list', 'board', 'calendar']).optional().describe('The project view style.'),
|
|
15
11
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/todoist-ai",
|
|
3
|
-
"version": "4.15.
|
|
3
|
+
"version": "4.15.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -40,19 +40,20 @@
|
|
|
40
40
|
"lint:write": "biome lint --write",
|
|
41
41
|
"format:check": "biome format",
|
|
42
42
|
"format:write": "biome format --write",
|
|
43
|
-
"
|
|
43
|
+
"lint:schemas": "npm run build && npx tsc scripts/validate-schemas.ts --outDir dist/scripts --moduleResolution node --module ESNext --target es2021 --esModuleInterop --skipLibCheck --declaration false && node dist/scripts/validate-schemas.js",
|
|
44
|
+
"check": "biome check && npm run lint:schemas",
|
|
44
45
|
"check:fix": "biome check --fix --unsafe",
|
|
45
46
|
"prepare": "husky"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@modelcontextprotocol/sdk": "^1.11.1",
|
|
49
50
|
"date-fns": "^4.1.0",
|
|
50
|
-
"@doist/todoist-api-typescript": "5.
|
|
51
|
+
"@doist/todoist-api-typescript": "5.9.0",
|
|
51
52
|
"dotenv": "^17.0.0",
|
|
52
53
|
"zod": "^3.25.7"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"@biomejs/biome": "2.2
|
|
56
|
+
"@biomejs/biome": "2.3.2",
|
|
56
57
|
"@types/express": "^5.0.2",
|
|
57
58
|
"@types/jest": "30.0.0",
|
|
58
59
|
"@types/morgan": "^1.9.9",
|
|
@@ -71,6 +72,15 @@
|
|
|
71
72
|
"lint-staged": {
|
|
72
73
|
"*": [
|
|
73
74
|
"biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
|
|
75
|
+
],
|
|
76
|
+
"src/tools/*.ts": [
|
|
77
|
+
"npm run lint:schemas"
|
|
78
|
+
],
|
|
79
|
+
"src/index.ts": [
|
|
80
|
+
"npm run lint:schemas"
|
|
81
|
+
],
|
|
82
|
+
"scripts/validate-schemas.ts": [
|
|
83
|
+
"npm run lint:schemas"
|
|
74
84
|
]
|
|
75
85
|
}
|
|
76
86
|
}
|
|
@@ -0,0 +1,280 @@
|
|
|
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
|
+
interface ValidationIssue {
|
|
21
|
+
toolName: string
|
|
22
|
+
parameterPath: string
|
|
23
|
+
issue: string
|
|
24
|
+
suggestion: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ValidationResult {
|
|
28
|
+
success: boolean
|
|
29
|
+
issues: ValidationIssue[]
|
|
30
|
+
toolsChecked: number
|
|
31
|
+
parametersChecked: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively walk a Zod schema and detect problematic patterns
|
|
36
|
+
*/
|
|
37
|
+
function walkZodSchema(
|
|
38
|
+
schema: z.ZodTypeAny,
|
|
39
|
+
path: string,
|
|
40
|
+
issues: ValidationIssue[],
|
|
41
|
+
toolName: string,
|
|
42
|
+
): void {
|
|
43
|
+
const typeName = schema._def.typeName
|
|
44
|
+
|
|
45
|
+
// Check for ZodOptional containing a ZodNullable ZodString
|
|
46
|
+
if (typeName === 'ZodOptional') {
|
|
47
|
+
const innerSchema = schema._def.innerType
|
|
48
|
+
if (innerSchema._def.typeName === 'ZodNullable') {
|
|
49
|
+
const nullableInner = innerSchema._def.innerType
|
|
50
|
+
if (nullableInner._def.typeName === '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 (typeName === 'ZodNullable') {
|
|
64
|
+
const innerSchema = schema._def.innerType
|
|
65
|
+
if (innerSchema._def.typeName === 'ZodOptional') {
|
|
66
|
+
const optionalInner = innerSchema._def.innerType
|
|
67
|
+
if (optionalInner._def.typeName === '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
|
+
switch (typeName) {
|
|
81
|
+
case 'ZodObject': {
|
|
82
|
+
const shape = schema._def.shape()
|
|
83
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
84
|
+
const newPath = path ? `${path}.${key}` : key
|
|
85
|
+
walkZodSchema(value as z.ZodTypeAny, newPath, issues, toolName)
|
|
86
|
+
}
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
case 'ZodArray':
|
|
90
|
+
walkZodSchema(schema._def.type, `${path}[]`, issues, toolName)
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
case 'ZodOptional':
|
|
94
|
+
case 'ZodNullable':
|
|
95
|
+
case 'ZodDefault':
|
|
96
|
+
walkZodSchema(schema._def.innerType, path, issues, toolName)
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
case 'ZodUnion':
|
|
100
|
+
case 'ZodDiscriminatedUnion': {
|
|
101
|
+
const options = schema._def.options || schema._def.discriminatedUnion
|
|
102
|
+
if (Array.isArray(options)) {
|
|
103
|
+
options.forEach((option: z.ZodTypeAny, index: number) => {
|
|
104
|
+
walkZodSchema(option, `${path}[union:${index}]`, issues, toolName)
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
case 'ZodIntersection':
|
|
110
|
+
walkZodSchema(schema._def.left, `${path}[left]`, issues, toolName)
|
|
111
|
+
walkZodSchema(schema._def.right, `${path}[right]`, issues, toolName)
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
case 'ZodRecord':
|
|
115
|
+
if (schema._def.valueType) {
|
|
116
|
+
walkZodSchema(schema._def.valueType, `${path}[value]`, issues, toolName)
|
|
117
|
+
}
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
case 'ZodTuple':
|
|
121
|
+
if (schema._def.items) {
|
|
122
|
+
schema._def.items.forEach((item: z.ZodTypeAny, index: number) => {
|
|
123
|
+
walkZodSchema(item, `${path}[${index}]`, issues, toolName)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate a single tool's parameter schema
|
|
132
|
+
*/
|
|
133
|
+
// biome-ignore lint/suspicious/noExplicitAny: this is a tooling script
|
|
134
|
+
function validateToolSchema(tool: any): ValidationIssue[] {
|
|
135
|
+
const issues: ValidationIssue[] = []
|
|
136
|
+
const toolName = tool.name || 'unknown'
|
|
137
|
+
|
|
138
|
+
if (!tool.parameters) {
|
|
139
|
+
return issues
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const schema = z.object(tool.parameters)
|
|
144
|
+
walkZodSchema(schema, '', issues, toolName)
|
|
145
|
+
} catch (error) {
|
|
146
|
+
issues.push({
|
|
147
|
+
toolName,
|
|
148
|
+
parameterPath: 'root',
|
|
149
|
+
issue: `Failed to analyze schema: ${error}`,
|
|
150
|
+
suggestion: 'Check that the tool parameters are valid Zod schemas',
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return issues
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Main validation function using runtime schema analysis
|
|
159
|
+
*/
|
|
160
|
+
async function validateAllSchemas(verbose: boolean = false): Promise<ValidationResult> {
|
|
161
|
+
try {
|
|
162
|
+
const { tools } = await import(`${process.cwd()}/dist/index.js`)
|
|
163
|
+
|
|
164
|
+
const allIssues: ValidationIssue[] = []
|
|
165
|
+
let totalParameters = 0
|
|
166
|
+
const toolNames = Object.keys(tools)
|
|
167
|
+
|
|
168
|
+
for (const toolName of toolNames) {
|
|
169
|
+
const tool = tools[toolName]
|
|
170
|
+
const toolIssues = validateToolSchema(tool)
|
|
171
|
+
allIssues.push(...toolIssues)
|
|
172
|
+
|
|
173
|
+
// Count parameters for stats
|
|
174
|
+
if (tool.parameters) {
|
|
175
|
+
try {
|
|
176
|
+
const schema = z.object(tool.parameters)
|
|
177
|
+
const shape = schema._def.shape()
|
|
178
|
+
totalParameters += Object.keys(shape).length
|
|
179
|
+
} catch {
|
|
180
|
+
// Skip counting if schema is invalid
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (verbose) {
|
|
185
|
+
const issueCount = toolIssues.length
|
|
186
|
+
const status = issueCount === 0 ? '✅' : `❌ (${issueCount} issues)`
|
|
187
|
+
const paramCount = tool.parameters ? Object.keys(tool.parameters).length : 0
|
|
188
|
+
console.log(`${status} ${toolName} (${paramCount} parameters)`)
|
|
189
|
+
|
|
190
|
+
if (issueCount > 0) {
|
|
191
|
+
toolIssues.forEach((issue) => {
|
|
192
|
+
console.log(` ${issue.parameterPath}: ${issue.issue}`)
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
success: allIssues.length === 0,
|
|
200
|
+
issues: allIssues,
|
|
201
|
+
toolsChecked: toolNames.length,
|
|
202
|
+
parametersChecked: totalParameters,
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
issues: [
|
|
208
|
+
{
|
|
209
|
+
toolName: 'system',
|
|
210
|
+
parameterPath: 'import',
|
|
211
|
+
issue: `Failed to import tools: ${error}`,
|
|
212
|
+
suggestion: 'Ensure the project is built and dist/index.js exists',
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
toolsChecked: 0,
|
|
216
|
+
parametersChecked: 0,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* CLI interface
|
|
223
|
+
*/
|
|
224
|
+
async function main() {
|
|
225
|
+
const args = process.argv.slice(2)
|
|
226
|
+
const verbose = args.includes('--verbose')
|
|
227
|
+
const jsonOutput = args.includes('--json')
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const result = await validateAllSchemas(verbose)
|
|
231
|
+
|
|
232
|
+
if (jsonOutput) {
|
|
233
|
+
console.log(JSON.stringify(result, null, 2))
|
|
234
|
+
} else {
|
|
235
|
+
if (result.success) {
|
|
236
|
+
console.log('✅ Schema validation passed!')
|
|
237
|
+
console.log(
|
|
238
|
+
` Checked ${result.toolsChecked} tools with ${result.parametersChecked} parameters`,
|
|
239
|
+
)
|
|
240
|
+
console.log(
|
|
241
|
+
' All schemas are Gemini API compatible (no .nullable() on optional strings)',
|
|
242
|
+
)
|
|
243
|
+
} else {
|
|
244
|
+
console.log('❌ Schema validation failed!')
|
|
245
|
+
console.log(
|
|
246
|
+
` Found ${result.issues.length} issue(s) in ${result.toolsChecked} tools:\n`,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
result.issues.forEach((issue, index) => {
|
|
250
|
+
console.log(`\n${index + 1}. 🚫 VALIDATION FAILURE`)
|
|
251
|
+
console.log(` Tool: ${issue.toolName}`)
|
|
252
|
+
console.log(` Parameter: ${issue.parameterPath}`)
|
|
253
|
+
console.log(` Issue: ${issue.issue}`)
|
|
254
|
+
console.log(` Action Required: ${issue.suggestion}`)
|
|
255
|
+
console.log(` File Location: src/tools/${issue.toolName}.ts`)
|
|
256
|
+
console.log(` Fix Pattern: Remove .nullable() from the parameter schema`)
|
|
257
|
+
console.log(
|
|
258
|
+
` Example Fix: Change z.string().nullable().optional() → z.string().optional()`,
|
|
259
|
+
)
|
|
260
|
+
console.log(
|
|
261
|
+
` ⚠️ This validation failure will cause Gemini API HTTP 400 errors\n`,
|
|
262
|
+
)
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
process.exit(result.success ? 0 : 1)
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error('Fatal error during schema validation:', error)
|
|
270
|
+
process.exit(1)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Run if this script is executed directly
|
|
275
|
+
if (process.argv[1]?.endsWith('validate-schemas.js')) {
|
|
276
|
+
main()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export type { ValidationResult, ValidationIssue }
|
|
280
|
+
export { validateAllSchemas }
|