@cloudflare/codemode 0.1.0 → 0.1.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/CHANGELOG.md +20 -0
- package/README.md +2 -2
- package/dist/ai.js +13 -4
- package/dist/ai.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types-CpkEgXwN.js +392 -0
- package/dist/types-CpkEgXwN.js.map +1 -0
- package/package.json +2 -2
- package/src/tests/schema-conversion.test.ts +1068 -0
- package/src/tests/types.test.ts +275 -0
- package/src/tool.ts +25 -4
- package/src/types.ts +527 -52
- package/dist/types-B9g5T2nd.js +0 -138
- package/dist/types-B9g5T2nd.js.map +0 -1
package/src/tests/types.test.ts
CHANGED
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
import { describe, it, expect } from "vitest";
|
|
5
5
|
import { generateTypes, sanitizeToolName } from "../types";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import { fromJSONSchema } from "zod/v4";
|
|
8
|
+
import { jsonSchema } from "ai";
|
|
9
|
+
import type { ToolSet } from "ai";
|
|
7
10
|
import type { ToolDescriptors } from "../types";
|
|
8
11
|
|
|
12
|
+
// Helper: cast loosely-typed tool objects for generateTypes
|
|
13
|
+
function genTypes(tools: Record<string, unknown>): string {
|
|
14
|
+
return generateTypes(tools as unknown as ToolSet);
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
describe("sanitizeToolName", () => {
|
|
10
18
|
it("should replace hyphens with underscores", () => {
|
|
11
19
|
expect(sanitizeToolName("get-weather")).toBe("get_weather");
|
|
@@ -168,4 +176,271 @@ describe("generateTypes", () => {
|
|
|
168
176
|
// toCamelCase("get_weather") → "GetWeather"
|
|
169
177
|
expect(result).toContain("GetWeatherInput");
|
|
170
178
|
});
|
|
179
|
+
|
|
180
|
+
it("should handle MCP tools with input and output schemas (fromJSONSchema)", () => {
|
|
181
|
+
// MCP tools use JSON Schema format for both input and output
|
|
182
|
+
const inputSchema = {
|
|
183
|
+
type: "object" as const,
|
|
184
|
+
properties: {
|
|
185
|
+
city: { type: "string" as const, description: "City name" },
|
|
186
|
+
units: {
|
|
187
|
+
type: "string" as const,
|
|
188
|
+
enum: ["celsius", "fahrenheit"],
|
|
189
|
+
description: "Temperature units"
|
|
190
|
+
},
|
|
191
|
+
includeForecast: { type: "boolean" as const }
|
|
192
|
+
},
|
|
193
|
+
required: ["city"]
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const outputSchema = {
|
|
197
|
+
type: "object" as const,
|
|
198
|
+
properties: {
|
|
199
|
+
temperature: { type: "number" as const, description: "Current temp" },
|
|
200
|
+
humidity: { type: "number" as const },
|
|
201
|
+
conditions: { type: "string" as const },
|
|
202
|
+
forecast: {
|
|
203
|
+
type: "array" as const,
|
|
204
|
+
items: {
|
|
205
|
+
type: "object" as const,
|
|
206
|
+
properties: {
|
|
207
|
+
day: { type: "string" as const },
|
|
208
|
+
high: { type: "number" as const },
|
|
209
|
+
low: { type: "number" as const }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
required: ["temperature", "conditions"]
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const tools: ToolDescriptors = {
|
|
218
|
+
getWeather: {
|
|
219
|
+
description: "Get weather for a city",
|
|
220
|
+
inputSchema: fromJSONSchema(inputSchema),
|
|
221
|
+
outputSchema: fromJSONSchema(outputSchema)
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const result = generateTypes(tools);
|
|
226
|
+
|
|
227
|
+
// Input schema types
|
|
228
|
+
expect(result).toContain("type GetWeatherInput");
|
|
229
|
+
expect(result).toContain("city: string");
|
|
230
|
+
expect(result).toContain("units?:");
|
|
231
|
+
expect(result).toContain("includeForecast?: boolean");
|
|
232
|
+
|
|
233
|
+
// Output schema types (not unknown)
|
|
234
|
+
expect(result).toContain("type GetWeatherOutput");
|
|
235
|
+
expect(result).not.toContain("GetWeatherOutput = unknown");
|
|
236
|
+
expect(result).toContain("temperature: number");
|
|
237
|
+
expect(result).toContain("humidity?: number");
|
|
238
|
+
expect(result).toContain("conditions: string");
|
|
239
|
+
expect(result).toContain("forecast?:");
|
|
240
|
+
expect(result).toContain("day?: string");
|
|
241
|
+
expect(result).toContain("high?: number");
|
|
242
|
+
expect(result).toContain("low?: number");
|
|
243
|
+
|
|
244
|
+
// JSDoc
|
|
245
|
+
expect(result).toContain("@param input.city - City name");
|
|
246
|
+
expect(result).toContain("/** Current temp */");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should handle Zod schemas with input and output schemas", () => {
|
|
250
|
+
// Direct ToolDescriptors with Zod schemas (what generateTypes operates on)
|
|
251
|
+
const tools: ToolDescriptors = {
|
|
252
|
+
getWeather: {
|
|
253
|
+
description: "Get weather for a city",
|
|
254
|
+
inputSchema: z.object({
|
|
255
|
+
city: z.string().describe("City name"),
|
|
256
|
+
units: z.enum(["celsius", "fahrenheit"]).optional()
|
|
257
|
+
}),
|
|
258
|
+
outputSchema: z.object({
|
|
259
|
+
temperature: z.number().describe("Current temperature"),
|
|
260
|
+
humidity: z.number().describe("Humidity percentage"),
|
|
261
|
+
conditions: z.string().describe("Weather conditions"),
|
|
262
|
+
forecast: z.array(
|
|
263
|
+
z.object({
|
|
264
|
+
day: z.string(),
|
|
265
|
+
high: z.number(),
|
|
266
|
+
low: z.number()
|
|
267
|
+
})
|
|
268
|
+
)
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const result = generateTypes(tools);
|
|
274
|
+
|
|
275
|
+
// Verify input schema
|
|
276
|
+
expect(result).toContain("type GetWeatherInput");
|
|
277
|
+
expect(result).toContain("city: string");
|
|
278
|
+
expect(result).toContain("units?:");
|
|
279
|
+
expect(result).toContain('"celsius"');
|
|
280
|
+
expect(result).toContain('"fahrenheit"');
|
|
281
|
+
|
|
282
|
+
// Verify output schema is properly typed (not unknown)
|
|
283
|
+
expect(result).toContain("type GetWeatherOutput");
|
|
284
|
+
expect(result).not.toContain("GetWeatherOutput = unknown");
|
|
285
|
+
expect(result).toContain("temperature: number");
|
|
286
|
+
expect(result).toContain("humidity: number");
|
|
287
|
+
expect(result).toContain("conditions: string");
|
|
288
|
+
expect(result).toContain("forecast:");
|
|
289
|
+
expect(result).toContain("day: string");
|
|
290
|
+
expect(result).toContain("high: number");
|
|
291
|
+
expect(result).toContain("low: number");
|
|
292
|
+
|
|
293
|
+
// Verify JSDoc comments from .describe()
|
|
294
|
+
expect(result).toContain("/** City name */");
|
|
295
|
+
expect(result).toContain("/** Current temperature */");
|
|
296
|
+
expect(result).toContain("@param input.city - City name");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should handle null inputSchema gracefully", () => {
|
|
300
|
+
const tools = {
|
|
301
|
+
broken: {
|
|
302
|
+
description: "Broken tool",
|
|
303
|
+
inputSchema: null
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const result = genTypes(tools);
|
|
308
|
+
|
|
309
|
+
expect(result).toContain("type BrokenInput = unknown");
|
|
310
|
+
expect(result).toContain("type BrokenOutput = unknown");
|
|
311
|
+
expect(result).toContain("broken:");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("should handle undefined inputSchema gracefully", () => {
|
|
315
|
+
const tools = {
|
|
316
|
+
broken: {
|
|
317
|
+
description: "Broken tool",
|
|
318
|
+
inputSchema: undefined
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const result = genTypes(tools);
|
|
323
|
+
|
|
324
|
+
expect(result).toContain("type BrokenInput = unknown");
|
|
325
|
+
expect(result).toContain("type BrokenOutput = unknown");
|
|
326
|
+
expect(result).toContain("broken:");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("should handle string inputSchema gracefully", () => {
|
|
330
|
+
const tools = {
|
|
331
|
+
broken: {
|
|
332
|
+
description: "Broken tool",
|
|
333
|
+
inputSchema: "not a schema"
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const result = genTypes(tools);
|
|
338
|
+
|
|
339
|
+
expect(result).toContain("type BrokenInput = unknown");
|
|
340
|
+
expect(result).toContain("broken:");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("should isolate errors: one throwing tool does not break others", () => {
|
|
344
|
+
// Create a tool with a getter that throws
|
|
345
|
+
const throwingSchema = {
|
|
346
|
+
get jsonSchema(): never {
|
|
347
|
+
throw new Error("Schema explosion");
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const tools = {
|
|
352
|
+
good1: {
|
|
353
|
+
description: "Good first",
|
|
354
|
+
inputSchema: jsonSchema({
|
|
355
|
+
type: "object" as const,
|
|
356
|
+
properties: { a: { type: "string" as const } }
|
|
357
|
+
})
|
|
358
|
+
},
|
|
359
|
+
bad: {
|
|
360
|
+
description: "Bad tool",
|
|
361
|
+
inputSchema: throwingSchema
|
|
362
|
+
},
|
|
363
|
+
good2: {
|
|
364
|
+
description: "Good second",
|
|
365
|
+
inputSchema: jsonSchema({
|
|
366
|
+
type: "object" as const,
|
|
367
|
+
properties: { b: { type: "number" as const } }
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const result = genTypes(tools);
|
|
373
|
+
|
|
374
|
+
// Good tools should work fine
|
|
375
|
+
expect(result).toContain("type Good1Input");
|
|
376
|
+
expect(result).toContain("a?: string;");
|
|
377
|
+
expect(result).toContain("type Good2Input");
|
|
378
|
+
expect(result).toContain("b?: number;");
|
|
379
|
+
|
|
380
|
+
// Bad tool should degrade to unknown
|
|
381
|
+
expect(result).toContain("type BadInput = unknown");
|
|
382
|
+
expect(result).toContain("type BadOutput = unknown");
|
|
383
|
+
|
|
384
|
+
// All three tools should appear in the codemode declaration
|
|
385
|
+
expect(result).toContain("good1:");
|
|
386
|
+
expect(result).toContain("bad:");
|
|
387
|
+
expect(result).toContain("good2:");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("should handle AI SDK jsonSchema wrapper (MCP tools)", () => {
|
|
391
|
+
// This is what MCP tools look like when using the AI SDK jsonSchema wrapper
|
|
392
|
+
const inputJsonSchema = {
|
|
393
|
+
type: "object" as const,
|
|
394
|
+
properties: {
|
|
395
|
+
query: { type: "string" as const, description: "Search query" },
|
|
396
|
+
limit: { type: "number" as const, description: "Max results" }
|
|
397
|
+
},
|
|
398
|
+
required: ["query"]
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const outputJsonSchema = {
|
|
402
|
+
type: "object" as const,
|
|
403
|
+
properties: {
|
|
404
|
+
results: {
|
|
405
|
+
type: "array" as const,
|
|
406
|
+
items: {
|
|
407
|
+
type: "object" as const,
|
|
408
|
+
properties: {
|
|
409
|
+
title: { type: "string" as const },
|
|
410
|
+
url: { type: "string" as const }
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
total: { type: "number" as const }
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// Use AI SDK jsonSchema wrapper (what MCP client returns)
|
|
419
|
+
const tools = {
|
|
420
|
+
search: {
|
|
421
|
+
description: "Search the web",
|
|
422
|
+
inputSchema: jsonSchema(inputJsonSchema),
|
|
423
|
+
outputSchema: jsonSchema(outputJsonSchema)
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const result = generateTypes(tools as unknown as ToolDescriptors);
|
|
428
|
+
|
|
429
|
+
// Input schema types
|
|
430
|
+
expect(result).toContain("type SearchInput");
|
|
431
|
+
expect(result).toContain("query: string");
|
|
432
|
+
expect(result).toContain("limit?: number");
|
|
433
|
+
|
|
434
|
+
// Output schema types (not unknown)
|
|
435
|
+
expect(result).toContain("type SearchOutput");
|
|
436
|
+
expect(result).not.toContain("SearchOutput = unknown");
|
|
437
|
+
expect(result).toContain("results?:");
|
|
438
|
+
expect(result).toContain("title?: string");
|
|
439
|
+
expect(result).toContain("url?: string");
|
|
440
|
+
expect(result).toContain("total?: number");
|
|
441
|
+
|
|
442
|
+
// JSDoc from JSON Schema descriptions
|
|
443
|
+
expect(result).toContain("@param input.query - Search query");
|
|
444
|
+
expect(result).toContain("@param input.limit - Max results");
|
|
445
|
+
});
|
|
171
446
|
});
|
package/src/tool.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { tool, type Tool } from "ai";
|
|
1
|
+
import { tool, type Tool, asSchema } from "ai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { ToolSet } from "ai";
|
|
4
4
|
import * as acorn from "acorn";
|
|
@@ -10,7 +10,8 @@ const DEFAULT_DESCRIPTION = `Execute code to achieve a goal.
|
|
|
10
10
|
Available:
|
|
11
11
|
{{types}}
|
|
12
12
|
|
|
13
|
-
Write an async arrow function that returns the result.
|
|
13
|
+
Write an async arrow function in JavaScript that returns the result.
|
|
14
|
+
Do NOT use TypeScript syntax — no type annotations, interfaces, or generics.
|
|
14
15
|
Do NOT define named functions then call them — just write the arrow function body directly.
|
|
15
16
|
|
|
16
17
|
Example: async () => { const r = await codemode.searchWeb({ query: "test" }); return r; }`;
|
|
@@ -97,7 +98,9 @@ export function createCodeTool(
|
|
|
97
98
|
description,
|
|
98
99
|
inputSchema: codeSchema,
|
|
99
100
|
execute: async ({ code }) => {
|
|
100
|
-
// Extract execute functions from tools, keyed by name
|
|
101
|
+
// Extract execute functions from tools, keyed by name.
|
|
102
|
+
// Wrap each with its schema so arguments from the sandbox
|
|
103
|
+
// are validated before reaching the tool function.
|
|
101
104
|
const fns: Record<string, (...args: unknown[]) => Promise<unknown>> = {};
|
|
102
105
|
|
|
103
106
|
for (const [name, t] of Object.entries(tools)) {
|
|
@@ -106,7 +109,25 @@ export function createCodeTool(
|
|
|
106
109
|
? (t.execute as (args: unknown) => Promise<unknown>)
|
|
107
110
|
: undefined;
|
|
108
111
|
if (execute) {
|
|
109
|
-
|
|
112
|
+
const rawSchema =
|
|
113
|
+
"inputSchema" in t
|
|
114
|
+
? t.inputSchema
|
|
115
|
+
: "parameters" in t
|
|
116
|
+
? (t as Record<string, unknown>).parameters
|
|
117
|
+
: undefined;
|
|
118
|
+
|
|
119
|
+
// Use AI SDK's asSchema() to normalize any schema type
|
|
120
|
+
// (Zod v3/v4, Standard Schema, JSON Schema) into a unified
|
|
121
|
+
// Schema with an optional .validate() method.
|
|
122
|
+
const schema = rawSchema != null ? asSchema(rawSchema) : undefined;
|
|
123
|
+
|
|
124
|
+
fns[sanitizeToolName(name)] = schema?.validate
|
|
125
|
+
? async (args: unknown) => {
|
|
126
|
+
const result = await schema.validate!(args);
|
|
127
|
+
if (!result.success) throw result.error;
|
|
128
|
+
return execute(result.value);
|
|
129
|
+
}
|
|
130
|
+
: execute;
|
|
110
131
|
}
|
|
111
132
|
}
|
|
112
133
|
|