@cloudflare/codemode 0.1.1 → 0.1.2

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,446 +0,0 @@
1
- /**
2
- * Tests for generateTypes and sanitizeToolName.
3
- */
4
- import { describe, it, expect } from "vitest";
5
- import { generateTypes, sanitizeToolName } from "../types";
6
- import { z } from "zod";
7
- import { fromJSONSchema } from "zod/v4";
8
- import { jsonSchema } from "ai";
9
- import type { ToolSet } from "ai";
10
- import type { ToolDescriptors } from "../types";
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
-
17
- describe("sanitizeToolName", () => {
18
- it("should replace hyphens with underscores", () => {
19
- expect(sanitizeToolName("get-weather")).toBe("get_weather");
20
- });
21
-
22
- it("should replace dots with underscores", () => {
23
- expect(sanitizeToolName("api.v2.search")).toBe("api_v2_search");
24
- });
25
-
26
- it("should replace spaces with underscores", () => {
27
- expect(sanitizeToolName("my tool")).toBe("my_tool");
28
- });
29
-
30
- it("should prefix digit-leading names with underscore", () => {
31
- expect(sanitizeToolName("3drender")).toBe("_3drender");
32
- });
33
-
34
- it("should append underscore to reserved words", () => {
35
- expect(sanitizeToolName("class")).toBe("class_");
36
- expect(sanitizeToolName("return")).toBe("return_");
37
- expect(sanitizeToolName("delete")).toBe("delete_");
38
- });
39
-
40
- it("should strip special characters", () => {
41
- expect(sanitizeToolName("hello@world!")).toBe("helloworld");
42
- });
43
-
44
- it("should handle empty string", () => {
45
- expect(sanitizeToolName("")).toBe("_");
46
- });
47
-
48
- it("should handle string with only special characters", () => {
49
- // $ is a valid identifier character, so "@#$" → "$"
50
- expect(sanitizeToolName("@#$")).toBe("$");
51
- expect(sanitizeToolName("@#!")).toBe("_");
52
- });
53
-
54
- it("should leave valid identifiers unchanged", () => {
55
- expect(sanitizeToolName("getWeather")).toBe("getWeather");
56
- expect(sanitizeToolName("_private")).toBe("_private");
57
- expect(sanitizeToolName("$jquery")).toBe("$jquery");
58
- });
59
- });
60
-
61
- describe("generateTypes", () => {
62
- it("should generate types for simple tools", () => {
63
- const tools: ToolDescriptors = {
64
- getWeather: {
65
- description: "Get weather for a location",
66
- inputSchema: z.object({ location: z.string() })
67
- }
68
- };
69
-
70
- const result = generateTypes(tools);
71
- expect(result).toContain("GetWeatherInput");
72
- expect(result).toContain("GetWeatherOutput");
73
- expect(result).toContain("declare const codemode");
74
- expect(result).toContain("getWeather");
75
- expect(result).toContain("Get weather for a location");
76
- });
77
-
78
- it("should generate types for nested schemas", () => {
79
- const tools: ToolDescriptors = {
80
- createUser: {
81
- description: "Create a user",
82
- inputSchema: z.object({
83
- name: z.string(),
84
- address: z.object({
85
- street: z.string(),
86
- city: z.string()
87
- })
88
- })
89
- }
90
- };
91
-
92
- const result = generateTypes(tools);
93
- expect(result).toContain("CreateUserInput");
94
- expect(result).toContain("name");
95
- expect(result).toContain("address");
96
- });
97
-
98
- it("should handle optional fields", () => {
99
- const tools: ToolDescriptors = {
100
- search: {
101
- description: "Search",
102
- inputSchema: z.object({
103
- query: z.string(),
104
- limit: z.number().optional()
105
- })
106
- }
107
- };
108
-
109
- const result = generateTypes(tools);
110
- expect(result).toContain("SearchInput");
111
- expect(result).toContain("query");
112
- expect(result).toContain("limit");
113
- });
114
-
115
- it("should handle enums", () => {
116
- const tools: ToolDescriptors = {
117
- sort: {
118
- description: "Sort items",
119
- inputSchema: z.object({
120
- order: z.enum(["asc", "desc"])
121
- })
122
- }
123
- };
124
-
125
- const result = generateTypes(tools);
126
- expect(result).toContain("SortInput");
127
- });
128
-
129
- it("should handle arrays", () => {
130
- const tools: ToolDescriptors = {
131
- batch: {
132
- description: "Batch process",
133
- inputSchema: z.object({
134
- items: z.array(z.string())
135
- })
136
- }
137
- };
138
-
139
- const result = generateTypes(tools);
140
- expect(result).toContain("BatchInput");
141
- expect(result).toContain("items");
142
- });
143
-
144
- it("should handle empty tool set", () => {
145
- const result = generateTypes({});
146
- expect(result).toContain("declare const codemode");
147
- });
148
-
149
- it("should include JSDoc param descriptions from z.describe()", () => {
150
- const tools: ToolDescriptors = {
151
- search: {
152
- description: "Search the web",
153
- inputSchema: z.object({
154
- query: z.string().describe("The search query"),
155
- limit: z.number().describe("Max results to return")
156
- })
157
- }
158
- };
159
-
160
- const result = generateTypes(tools);
161
- expect(result).toContain("@param input.query - The search query");
162
- expect(result).toContain("@param input.limit - Max results to return");
163
- });
164
-
165
- it("should sanitize tool names with hyphens", () => {
166
- const tools: ToolDescriptors = {
167
- "get-weather": {
168
- description: "Get weather",
169
- inputSchema: z.object({ location: z.string() })
170
- }
171
- };
172
-
173
- const result = generateTypes(tools);
174
- // Tool name in codemode declaration is sanitized
175
- expect(result).toContain("get_weather");
176
- // toCamelCase("get_weather") → "GetWeather"
177
- expect(result).toContain("GetWeatherInput");
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
- });
446
- });
package/src/tool.ts DELETED
@@ -1,152 +0,0 @@
1
- import { tool, type Tool, asSchema } from "ai";
2
- import { z } from "zod";
3
- import type { ToolSet } from "ai";
4
- import * as acorn from "acorn";
5
- import { generateTypes, sanitizeToolName, type ToolDescriptors } from "./types";
6
- import type { Executor } from "./executor";
7
-
8
- const DEFAULT_DESCRIPTION = `Execute code to achieve a goal.
9
-
10
- Available:
11
- {{types}}
12
-
13
- Write an async arrow function in JavaScript that returns the result.
14
- Do NOT use TypeScript syntax — no type annotations, interfaces, or generics.
15
- Do NOT define named functions then call them — just write the arrow function body directly.
16
-
17
- Example: async () => { const r = await codemode.searchWeb({ query: "test" }); return r; }`;
18
-
19
- export interface CreateCodeToolOptions {
20
- tools: ToolDescriptors | ToolSet;
21
- executor: Executor;
22
- /**
23
- * Custom tool description. Use {{types}} as a placeholder for the generated type definitions.
24
- */
25
- description?: string;
26
- }
27
-
28
- const codeSchema = z.object({
29
- code: z.string().describe("JavaScript async arrow function to execute")
30
- });
31
-
32
- type CodeInput = z.infer<typeof codeSchema>;
33
- type CodeOutput = { code: string; result: unknown; logs?: string[] };
34
-
35
- function normalizeCode(code: string): string {
36
- const trimmed = code.trim();
37
- if (!trimmed) return "async () => {}";
38
-
39
- try {
40
- const ast = acorn.parse(trimmed, {
41
- ecmaVersion: "latest",
42
- sourceType: "module"
43
- });
44
-
45
- // Already an arrow function — pass through
46
- if (ast.body.length === 1 && ast.body[0].type === "ExpressionStatement") {
47
- const expr = (ast.body[0] as acorn.ExpressionStatement).expression;
48
- if (expr.type === "ArrowFunctionExpression") return trimmed;
49
- }
50
-
51
- // Last statement is expression → splice in return
52
- const last = ast.body[ast.body.length - 1];
53
- if (last?.type === "ExpressionStatement") {
54
- const exprStmt = last as acorn.ExpressionStatement;
55
- const before = trimmed.slice(0, last.start);
56
- const exprText = trimmed.slice(
57
- exprStmt.expression.start,
58
- exprStmt.expression.end
59
- );
60
- return `async () => {\n${before}return (${exprText})\n}`;
61
- }
62
-
63
- return `async () => {\n${trimmed}\n}`;
64
- } catch {
65
- return `async () => {\n${trimmed}\n}`;
66
- }
67
- }
68
-
69
- /**
70
- * Create a codemode tool that allows LLMs to write and execute code
71
- * with access to your tools in a sandboxed environment.
72
- *
73
- * Returns an AI SDK compatible tool.
74
- */
75
- function hasNeedsApproval(t: Record<string, unknown>): boolean {
76
- return "needsApproval" in t && t.needsApproval != null;
77
- }
78
-
79
- export function createCodeTool(
80
- options: CreateCodeToolOptions
81
- ): Tool<CodeInput, CodeOutput> {
82
- const tools: ToolDescriptors | ToolSet = {};
83
- for (const [name, t] of Object.entries(options.tools)) {
84
- if (!hasNeedsApproval(t as Record<string, unknown>)) {
85
- (tools as Record<string, unknown>)[name] = t;
86
- }
87
- }
88
-
89
- const types = generateTypes(tools);
90
- const executor = options.executor;
91
-
92
- const description = (options.description ?? DEFAULT_DESCRIPTION).replace(
93
- "{{types}}",
94
- types
95
- );
96
-
97
- return tool({
98
- description,
99
- inputSchema: codeSchema,
100
- execute: async ({ code }) => {
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.
104
- const fns: Record<string, (...args: unknown[]) => Promise<unknown>> = {};
105
-
106
- for (const [name, t] of Object.entries(tools)) {
107
- const execute =
108
- "execute" in t
109
- ? (t.execute as (args: unknown) => Promise<unknown>)
110
- : undefined;
111
- if (execute) {
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;
131
- }
132
- }
133
-
134
- const normalizedCode = normalizeCode(code);
135
-
136
- const executeResult = await executor.execute(normalizedCode, fns);
137
-
138
- if (executeResult.error) {
139
- const logCtx = executeResult.logs?.length
140
- ? `\n\nConsole output:\n${executeResult.logs.join("\n")}`
141
- : "";
142
- throw new Error(
143
- `Code execution failed: ${executeResult.error}${logCtx}`
144
- );
145
- }
146
-
147
- const output: CodeOutput = { code, result: executeResult.result };
148
- if (executeResult.logs) output.logs = executeResult.logs;
149
- return output;
150
- }
151
- });
152
- }