@cloudflare/codemode 0.0.8 → 0.1.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.
package/src/tool.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { tool, type Tool } 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 that returns the result.
14
+ Do NOT define named functions then call them — just write the arrow function body directly.
15
+
16
+ Example: async () => { const r = await codemode.searchWeb({ query: "test" }); return r; }`;
17
+
18
+ export interface CreateCodeToolOptions {
19
+ tools: ToolDescriptors | ToolSet;
20
+ executor: Executor;
21
+ /**
22
+ * Custom tool description. Use {{types}} as a placeholder for the generated type definitions.
23
+ */
24
+ description?: string;
25
+ }
26
+
27
+ const codeSchema = z.object({
28
+ code: z.string().describe("JavaScript async arrow function to execute")
29
+ });
30
+
31
+ type CodeInput = z.infer<typeof codeSchema>;
32
+ type CodeOutput = { code: string; result: unknown; logs?: string[] };
33
+
34
+ function normalizeCode(code: string): string {
35
+ const trimmed = code.trim();
36
+ if (!trimmed) return "async () => {}";
37
+
38
+ try {
39
+ const ast = acorn.parse(trimmed, {
40
+ ecmaVersion: "latest",
41
+ sourceType: "module"
42
+ });
43
+
44
+ // Already an arrow function — pass through
45
+ if (ast.body.length === 1 && ast.body[0].type === "ExpressionStatement") {
46
+ const expr = (ast.body[0] as acorn.ExpressionStatement).expression;
47
+ if (expr.type === "ArrowFunctionExpression") return trimmed;
48
+ }
49
+
50
+ // Last statement is expression → splice in return
51
+ const last = ast.body[ast.body.length - 1];
52
+ if (last?.type === "ExpressionStatement") {
53
+ const exprStmt = last as acorn.ExpressionStatement;
54
+ const before = trimmed.slice(0, last.start);
55
+ const exprText = trimmed.slice(
56
+ exprStmt.expression.start,
57
+ exprStmt.expression.end
58
+ );
59
+ return `async () => {\n${before}return (${exprText})\n}`;
60
+ }
61
+
62
+ return `async () => {\n${trimmed}\n}`;
63
+ } catch {
64
+ return `async () => {\n${trimmed}\n}`;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Create a codemode tool that allows LLMs to write and execute code
70
+ * with access to your tools in a sandboxed environment.
71
+ *
72
+ * Returns an AI SDK compatible tool.
73
+ */
74
+ function hasNeedsApproval(t: Record<string, unknown>): boolean {
75
+ return "needsApproval" in t && t.needsApproval != null;
76
+ }
77
+
78
+ export function createCodeTool(
79
+ options: CreateCodeToolOptions
80
+ ): Tool<CodeInput, CodeOutput> {
81
+ const tools: ToolDescriptors | ToolSet = {};
82
+ for (const [name, t] of Object.entries(options.tools)) {
83
+ if (!hasNeedsApproval(t as Record<string, unknown>)) {
84
+ (tools as Record<string, unknown>)[name] = t;
85
+ }
86
+ }
87
+
88
+ const types = generateTypes(tools);
89
+ const executor = options.executor;
90
+
91
+ const description = (options.description ?? DEFAULT_DESCRIPTION).replace(
92
+ "{{types}}",
93
+ types
94
+ );
95
+
96
+ return tool({
97
+ description,
98
+ inputSchema: codeSchema,
99
+ execute: async ({ code }) => {
100
+ // Extract execute functions from tools, keyed by name
101
+ const fns: Record<string, (...args: unknown[]) => Promise<unknown>> = {};
102
+
103
+ for (const [name, t] of Object.entries(tools)) {
104
+ const execute =
105
+ "execute" in t
106
+ ? (t.execute as (args: unknown) => Promise<unknown>)
107
+ : undefined;
108
+ if (execute) {
109
+ fns[sanitizeToolName(name)] = execute;
110
+ }
111
+ }
112
+
113
+ const normalizedCode = normalizeCode(code);
114
+
115
+ const executeResult = await executor.execute(normalizedCode, fns);
116
+
117
+ if (executeResult.error) {
118
+ const logCtx = executeResult.logs?.length
119
+ ? `\n\nConsole output:\n${executeResult.logs.join("\n")}`
120
+ : "";
121
+ throw new Error(
122
+ `Code execution failed: ${executeResult.error}${logCtx}`
123
+ );
124
+ }
125
+
126
+ const output: CodeOutput = { code, result: executeResult.result };
127
+ if (executeResult.logs) output.logs = executeResult.logs;
128
+ return output;
129
+ }
130
+ });
131
+ }
package/src/types.ts ADDED
@@ -0,0 +1,202 @@
1
+ import {
2
+ zodToTs,
3
+ printNode as printNodeZodToTs,
4
+ createTypeAlias,
5
+ createAuxiliaryTypeStore
6
+ } from "zod-to-ts";
7
+ import type { ZodType } from "zod";
8
+ import type { ToolSet } from "ai";
9
+
10
+ const JS_RESERVED = new Set([
11
+ "abstract",
12
+ "arguments",
13
+ "await",
14
+ "boolean",
15
+ "break",
16
+ "byte",
17
+ "case",
18
+ "catch",
19
+ "char",
20
+ "class",
21
+ "const",
22
+ "continue",
23
+ "debugger",
24
+ "default",
25
+ "delete",
26
+ "do",
27
+ "double",
28
+ "else",
29
+ "enum",
30
+ "eval",
31
+ "export",
32
+ "extends",
33
+ "false",
34
+ "final",
35
+ "finally",
36
+ "float",
37
+ "for",
38
+ "function",
39
+ "goto",
40
+ "if",
41
+ "implements",
42
+ "import",
43
+ "in",
44
+ "instanceof",
45
+ "int",
46
+ "interface",
47
+ "let",
48
+ "long",
49
+ "native",
50
+ "new",
51
+ "null",
52
+ "package",
53
+ "private",
54
+ "protected",
55
+ "public",
56
+ "return",
57
+ "short",
58
+ "static",
59
+ "super",
60
+ "switch",
61
+ "synchronized",
62
+ "this",
63
+ "throw",
64
+ "throws",
65
+ "transient",
66
+ "true",
67
+ "try",
68
+ "typeof",
69
+ "undefined",
70
+ "var",
71
+ "void",
72
+ "volatile",
73
+ "while",
74
+ "with",
75
+ "yield"
76
+ ]);
77
+
78
+ /**
79
+ * Sanitize a tool name into a valid JavaScript identifier.
80
+ * Replaces hyphens, dots, and spaces with `_`, strips other invalid chars,
81
+ * prefixes digit-leading names with `_`, and appends `_` to JS reserved words.
82
+ */
83
+ export function sanitizeToolName(name: string): string {
84
+ if (!name) return "_";
85
+
86
+ // Replace common separators with underscores
87
+ let sanitized = name.replace(/[-.\s]/g, "_");
88
+
89
+ // Strip any remaining non-identifier characters
90
+ sanitized = sanitized.replace(/[^a-zA-Z0-9_$]/g, "");
91
+
92
+ if (!sanitized) return "_";
93
+
94
+ // Prefix with _ if starts with a digit
95
+ if (/^[0-9]/.test(sanitized)) {
96
+ sanitized = "_" + sanitized;
97
+ }
98
+
99
+ // Append _ to reserved words
100
+ if (JS_RESERVED.has(sanitized)) {
101
+ sanitized = sanitized + "_";
102
+ }
103
+
104
+ return sanitized;
105
+ }
106
+
107
+ function toCamelCase(str: string) {
108
+ return str
109
+ .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
110
+ .replace(/^[a-z]/, (letter) => letter.toUpperCase());
111
+ }
112
+
113
+ /**
114
+ * Extract field descriptions from a Zod object schema's `.shape`, if available.
115
+ * Returns an array of `@param input.fieldName - description` lines.
116
+ */
117
+ function extractParamDescriptions(schema: ZodType): string[] {
118
+ const descriptions: string[] = [];
119
+ const shape = (schema as { shape?: Record<string, ZodType> }).shape;
120
+ if (!shape || typeof shape !== "object") return descriptions;
121
+
122
+ for (const [fieldName, fieldSchema] of Object.entries(shape)) {
123
+ const desc = (fieldSchema as { description?: string }).description;
124
+ if (desc) {
125
+ descriptions.push(`@param input.${fieldName} - ${desc}`);
126
+ }
127
+ }
128
+ return descriptions;
129
+ }
130
+
131
+ export interface ToolDescriptor {
132
+ description?: string;
133
+ inputSchema: ZodType;
134
+ outputSchema?: ZodType;
135
+ execute?: (args: unknown) => Promise<unknown>;
136
+ }
137
+
138
+ export type ToolDescriptors = Record<string, ToolDescriptor>;
139
+
140
+ /**
141
+ * Generate TypeScript type definitions from tool descriptors or an AI SDK ToolSet.
142
+ * These types can be included in tool descriptions to help LLMs write correct code.
143
+ */
144
+ export function generateTypes(tools: ToolDescriptors | ToolSet): string {
145
+ let availableTools = "";
146
+ let availableTypes = "";
147
+
148
+ const auxiliaryTypeStore = createAuxiliaryTypeStore();
149
+
150
+ for (const [toolName, tool] of Object.entries(tools)) {
151
+ // Handle both our ToolDescriptor and AI SDK Tool types
152
+ const inputSchema =
153
+ "inputSchema" in tool ? tool.inputSchema : tool.parameters;
154
+ const outputSchema = "outputSchema" in tool ? tool.outputSchema : undefined;
155
+ const description = tool.description;
156
+
157
+ const safeName = sanitizeToolName(toolName);
158
+
159
+ const inputType = printNodeZodToTs(
160
+ createTypeAlias(
161
+ zodToTs(inputSchema as ZodType, { auxiliaryTypeStore }).node,
162
+ `${toCamelCase(safeName)}Input`
163
+ )
164
+ );
165
+
166
+ const outputType = outputSchema
167
+ ? printNodeZodToTs(
168
+ createTypeAlias(
169
+ zodToTs(outputSchema as ZodType, { auxiliaryTypeStore }).node,
170
+ `${toCamelCase(safeName)}Output`
171
+ )
172
+ )
173
+ : `type ${toCamelCase(safeName)}Output = unknown`;
174
+
175
+ availableTypes += `\n${inputType.trim()}`;
176
+ availableTypes += `\n${outputType.trim()}`;
177
+
178
+ // Build JSDoc comment with description and param descriptions
179
+ const paramDescs = extractParamDescriptions(inputSchema as ZodType);
180
+ const jsdocLines: string[] = [];
181
+ if (description?.trim()) {
182
+ jsdocLines.push(description.trim());
183
+ } else {
184
+ jsdocLines.push(toolName);
185
+ }
186
+ for (const pd of paramDescs) {
187
+ jsdocLines.push(pd);
188
+ }
189
+
190
+ const jsdocBody = jsdocLines.map((l) => `\t * ${l}`).join("\n");
191
+ availableTools += `\n\t/**\n${jsdocBody}\n\t */`;
192
+ availableTools += `\n\t${safeName}: (input: ${toCamelCase(safeName)}Input) => Promise<${toCamelCase(safeName)}Output>;`;
193
+ availableTools += "\n";
194
+ }
195
+
196
+ availableTools = `\ndeclare const codemode: {${availableTools}}`;
197
+
198
+ return `
199
+ ${availableTypes}
200
+ ${availableTools}
201
+ `.trim();
202
+ }
@@ -0,0 +1,17 @@
1
+ import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
2
+
3
+ export default defineWorkersConfig({
4
+ test: {
5
+ name: "workers",
6
+ include: ["src/tests/**/*.test.ts"],
7
+ poolOptions: {
8
+ workers: {
9
+ isolatedStorage: false,
10
+ singleWorker: true,
11
+ wrangler: {
12
+ configPath: "./wrangler.jsonc"
13
+ }
14
+ }
15
+ }
16
+ }
17
+ });
package/wrangler.jsonc ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compatibility_date": "2026-01-28",
3
+ "compatibility_flags": [
4
+ "nodejs_compat",
5
+ "enable_nodejs_tty_module",
6
+ "enable_nodejs_fs_module",
7
+ "enable_nodejs_http_modules",
8
+ "enable_nodejs_perf_hooks_module"
9
+ ],
10
+ "worker_loaders": [
11
+ {
12
+ "binding": "LOADER"
13
+ }
14
+ ],
15
+ "main": "src/index.ts"
16
+ }