@cloudflare/codemode 0.0.7 → 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/CHANGELOG.md +22 -0
- package/README.md +174 -247
- package/dist/ai.d.ts +27 -27
- package/dist/ai.js +67 -136
- package/dist/ai.js.map +1 -1
- package/dist/executor-Czw9jKZH.d.ts +96 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/types-B9g5T2nd.js +138 -0
- package/dist/types-B9g5T2nd.js.map +1 -0
- package/e2e/codemode.spec.ts +124 -0
- package/e2e/playwright.config.ts +24 -0
- package/e2e/worker.ts +144 -0
- package/e2e/wrangler.jsonc +14 -0
- package/package.json +15 -4
- package/scripts/build.ts +1 -2
- package/src/ai.ts +1 -247
- package/src/executor.ts +170 -0
- package/src/index.ts +13 -0
- package/src/tests/cloudflare-test.d.ts +5 -0
- package/src/tests/executor.test.ts +224 -0
- package/src/tests/tool.test.ts +454 -0
- package/src/tests/tsconfig.json +10 -0
- package/src/tests/types.test.ts +171 -0
- package/src/tool.ts +131 -0
- package/src/types.ts +202 -0
- package/vitest.config.ts +17 -0
- package/wrangler.jsonc +16 -0
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
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
}
|