@robinbraemer/codemode 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CNAP - Cloud Native Application Platform
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,278 @@
1
+ # codemode
2
+
3
+ Two MCP tools that replace hundreds. Give an AI agent your OpenAPI spec and a request handler — it discovers and calls your entire API by writing JavaScript in a sandboxed runtime.
4
+
5
+ Instead of defining individual MCP tools for every API endpoint (`list-pods`, `create-product`, `get-logs`, ...), CodeMode exposes just **two tools**:
6
+
7
+ - **`search`** — the agent writes JS to filter your OpenAPI spec and discover endpoints
8
+ - **`execute`** — the agent writes JS to call your API via an injected client
9
+
10
+ This is the same pattern [Cloudflare uses](https://blog.cloudflare.com/code-mode-mcp/) to expose 2,500+ API endpoints through just two MCP tools, reducing context window usage by 99.9%.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pnpm add codemode
16
+
17
+ # Install a sandbox runtime (pick one):
18
+ pnpm add isolated-vm # V8 isolates — fastest, recommended
19
+ pnpm add quickjs-emscripten # WASM — portable fallback
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import { CodeMode } from 'codemode';
26
+ import { Hono } from 'hono';
27
+
28
+ const app = new Hono();
29
+ app.get('/v1/clusters', (c) => c.json([{ id: '1', name: 'prod' }]));
30
+ app.post('/v1/clusters', async (c) => {
31
+ const body = await c.req.json();
32
+ return c.json({ id: '2', ...body }, 201);
33
+ });
34
+
35
+ const codemode = new CodeMode({
36
+ spec: myOpenAPISpec, // OpenAPI 3.x spec, or async getter
37
+ request: app.request.bind(app), // in-process, no network hop
38
+ });
39
+
40
+ // The agent searches the spec to discover endpoints...
41
+ const search = await codemode.callTool('search', {
42
+ code: `async () => {
43
+ return Object.entries(spec.paths)
44
+ .filter(([p]) => p.includes('/clusters'))
45
+ .flatMap(([path, methods]) =>
46
+ Object.entries(methods)
47
+ .filter(([m]) => ['get','post','put','delete'].includes(m))
48
+ .map(([method, op]) => ({ method: method.toUpperCase(), path, summary: op.summary }))
49
+ );
50
+ }`
51
+ });
52
+
53
+ // ...then executes API calls
54
+ const result = await codemode.callTool('execute', {
55
+ code: `async () => {
56
+ const res = await api.request({ method: "GET", path: "/v1/clusters" });
57
+ return res.body;
58
+ }`
59
+ });
60
+ ```
61
+
62
+ ## MCP Server Integration
63
+
64
+ ```typescript
65
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
66
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
67
+ import { CodeMode } from 'codemode';
68
+ import { registerTools } from 'codemode/mcp';
69
+
70
+ const codemode = new CodeMode({
71
+ spec: () => fetchOpenAPISpec(),
72
+ request: app.request.bind(app),
73
+ });
74
+
75
+ const server = new McpServer({ name: 'my-api', version: '1.0.0' });
76
+ registerTools(codemode, server);
77
+
78
+ const transport = new StdioServerTransport();
79
+ await server.connect(transport);
80
+ ```
81
+
82
+ ## How It Works
83
+
84
+ ```
85
+ AI Agent
86
+ │ writes JavaScript code
87
+
88
+ CodeMode MCP Server
89
+
90
+ ├─ search(code) → runs JS with OpenAPI spec as a global
91
+ │ → agent discovers endpoints, schemas, parameters
92
+
93
+ └─ execute(code) → runs JS with injected request client
94
+ → api.request() calls your handler in-process
95
+ → no network hop, auth handled automatically
96
+ ```
97
+
98
+ All code runs in an isolated sandbox (V8 isolate or QuickJS WASM). The sandbox has zero I/O by default — no `require`, no `process`, no `fetch`, no filesystem. The only way to interact with the outside world is through the injected globals (`spec` for search, `{namespace}.request()` for execute).
99
+
100
+ Each tool call gets a fresh sandbox with no state carried over between calls.
101
+
102
+ ## API
103
+
104
+ ### `new CodeMode(options)`
105
+
106
+ | Option | Type | Default | Description |
107
+ |--------|------|---------|-------------|
108
+ | `spec` | `OpenAPISpec \| () => OpenAPISpec \| Promise<OpenAPISpec>` | required | OpenAPI 3.x spec or async getter |
109
+ | `request` | `(input, init?) => Response` | required | Fetch-compatible handler (`app.request.bind(app)` for Hono) |
110
+ | `namespace` | `string` | `"api"` | Client name in sandbox (`api.request(...)`) |
111
+ | `baseUrl` | `string` | `"http://localhost"` | Base URL for relative paths |
112
+ | `sandbox` | `{ memoryMB?, timeoutMs? }` | `{ 64, 30000 }` | Sandbox resource limits |
113
+ | `executor` | `Executor` | auto-detect | Custom sandbox executor |
114
+
115
+ ### Methods
116
+
117
+ #### `codemode.tools(): ToolDefinition[]`
118
+
119
+ Returns MCP-compatible tool definitions for `search` and `execute`.
120
+
121
+ #### `codemode.callTool(name, { code }): Promise<ToolCallResult>`
122
+
123
+ Route a tool call. Returns `{ content: [{ type: "text", text }], isError? }`.
124
+
125
+ #### `codemode.search(code): Promise<ToolCallResult>`
126
+
127
+ Run search code directly (shorthand for `callTool('search', { code })`).
128
+
129
+ #### `codemode.execute(code): Promise<ToolCallResult>`
130
+
131
+ Run execute code directly (shorthand for `callTool('execute', { code })`).
132
+
133
+ #### `codemode.setToolNames(search, execute): this`
134
+
135
+ Override default tool names. Useful when running multiple CodeMode instances.
136
+
137
+ #### `codemode.dispose(): void`
138
+
139
+ Clean up sandbox resources.
140
+
141
+ ## Sandbox API
142
+
143
+ ### Inside `search`
144
+
145
+ The `spec` global is the full OpenAPI 3.x document:
146
+
147
+ ```javascript
148
+ // Find endpoints by keyword
149
+ async () => {
150
+ return Object.entries(spec.paths)
151
+ .filter(([p]) => p.includes('/clusters'))
152
+ .flatMap(([path, methods]) =>
153
+ Object.entries(methods)
154
+ .filter(([m]) => ['get','post','put','delete','patch'].includes(m))
155
+ .map(([method, op]) => ({
156
+ method: method.toUpperCase(), path, summary: op.summary
157
+ }))
158
+ );
159
+ }
160
+
161
+ // Get a specific schema
162
+ async () => spec.components?.schemas?.Product
163
+
164
+ // Spec metadata
165
+ async () => ({
166
+ title: spec.info.title,
167
+ version: spec.info.version,
168
+ endpoints: Object.keys(spec.paths).length,
169
+ })
170
+ ```
171
+
172
+ ### Inside `execute`
173
+
174
+ The `{namespace}.request()` function makes API calls through the host handler:
175
+
176
+ ```javascript
177
+ // GET with query params
178
+ async () => {
179
+ const res = await api.request({
180
+ method: "GET",
181
+ path: "/v1/clusters",
182
+ query: { limit: 10 },
183
+ });
184
+ return res.body;
185
+ }
186
+
187
+ // POST with body
188
+ async () => {
189
+ return api.request({
190
+ method: "POST",
191
+ path: "/v1/products",
192
+ body: { name: "Redis", chart: "bitnami/redis" },
193
+ });
194
+ }
195
+
196
+ // Chain calls
197
+ async () => {
198
+ const list = await api.request({ method: "GET", path: "/v1/clusters" });
199
+ const details = await Promise.all(
200
+ list.body.map(c =>
201
+ api.request({ method: "GET", path: `/v1/clusters/${c.id}` })
202
+ )
203
+ );
204
+ return details.map(d => d.body);
205
+ }
206
+ ```
207
+
208
+ **Request options:**
209
+
210
+ | Field | Type | Description |
211
+ |-------|------|-------------|
212
+ | `method` | `string` | HTTP method (`"GET"`, `"POST"`, etc.) |
213
+ | `path` | `string` | API path (`"/v1/clusters"`) |
214
+ | `query` | `Record<string, string \| number \| boolean>` | Query parameters (optional) |
215
+ | `body` | `unknown` | Request body, auto-serialized as JSON (optional) |
216
+ | `headers` | `Record<string, string>` | Additional headers (optional) |
217
+
218
+ **Response:** `{ status: number, headers: Record<string, string>, body: unknown }`
219
+
220
+ ## Executors
221
+
222
+ CodeMode auto-detects your installed sandbox runtime. You can also pass one explicitly:
223
+
224
+ ```typescript
225
+ import { CodeMode, IsolatedVMExecutor } from 'codemode';
226
+
227
+ const codemode = new CodeMode({
228
+ spec,
229
+ request: handler,
230
+ executor: new IsolatedVMExecutor({ memoryMB: 128, timeoutMs: 60_000 }),
231
+ });
232
+ ```
233
+
234
+ | Executor | Package | Performance | Portability |
235
+ |----------|---------|-------------|-------------|
236
+ | `IsolatedVMExecutor` | `isolated-vm` | Native V8 speed | Node.js |
237
+ | `QuickJSExecutor` | `quickjs-emscripten` | ~3-5x slower (still fast) | Node.js, Bun, browsers |
238
+
239
+ Both are optional peer dependencies. Install at least one.
240
+
241
+ ### Custom Executor
242
+
243
+ Implement the `Executor` interface to use your own sandbox:
244
+
245
+ ```typescript
246
+ import { CodeMode, type Executor, type ExecuteResult } from 'codemode';
247
+
248
+ class MyExecutor implements Executor {
249
+ async execute(code: string, globals: Record<string, unknown>): Promise<ExecuteResult> {
250
+ // `code` is an async arrow function as a string: "async () => { ... }"
251
+ // `globals` contains named values to inject:
252
+ // - plain data (objects, arrays, primitives) → read-only values
253
+ // - functions → callable host functions
254
+ // - objects with function values → namespace with callable methods
255
+ return { result: ..., logs: [] };
256
+ }
257
+
258
+ dispose() { /* clean up */ }
259
+ }
260
+
261
+ const codemode = new CodeMode({
262
+ spec,
263
+ request: handler,
264
+ executor: new MyExecutor(),
265
+ });
266
+ ```
267
+
268
+ ## Token Efficiency
269
+
270
+ | Approach | Context Tokens |
271
+ |----------|---------------|
272
+ | Individual MCP tools (15-50+ tools) | ~15,000-50,000+ |
273
+ | Full OpenAPI spec in context | ~1,000,000+ |
274
+ | **CodeMode (2 tools)** | **~1,000** |
275
+
276
+ ## License
277
+
278
+ MIT
@@ -0,0 +1,115 @@
1
+ // src/executor/isolated-vm.ts
2
+ var IsolatedVMExecutor = class {
3
+ memoryMB;
4
+ timeoutMs;
5
+ constructor(options = {}) {
6
+ this.memoryMB = options.memoryMB ?? 64;
7
+ this.timeoutMs = options.timeoutMs ?? 3e4;
8
+ }
9
+ async execute(code, globals) {
10
+ const ivm = (await import("isolated-vm")).default ?? await import("isolated-vm");
11
+ const isolate = new ivm.Isolate({ memoryLimit: this.memoryMB });
12
+ const logs = [];
13
+ try {
14
+ const context = await isolate.createContext();
15
+ const jail = context.global;
16
+ await jail.set("global", jail.derefInto());
17
+ jail.setSync(
18
+ "__log",
19
+ new ivm.Callback((...args) => {
20
+ logs.push(
21
+ args.map((a) => typeof a === "string" ? a : stringify(a)).join(" ")
22
+ );
23
+ })
24
+ );
25
+ await context.eval(`
26
+ globalThis.console = {
27
+ log: (...args) => __log(...args),
28
+ warn: (...args) => __log(...args),
29
+ error: (...args) => __log(...args),
30
+ };
31
+ `);
32
+ let refCounter = 0;
33
+ for (const [name, value] of Object.entries(globals)) {
34
+ if (typeof value === "function") {
35
+ const refName = `__ref${refCounter++}`;
36
+ await jail.set(refName, new ivm.Reference(value));
37
+ await context.eval(`
38
+ globalThis[${JSON.stringify(name)}] = function(...args) {
39
+ return ${refName}.apply(undefined, args, {
40
+ arguments: { copy: true },
41
+ result: { promise: true, copy: true },
42
+ });
43
+ };
44
+ `);
45
+ } else if (isNamespaceWithMethods(value)) {
46
+ const ns = value;
47
+ let nsSetup = `globalThis[${JSON.stringify(name)}] = {};
48
+ `;
49
+ for (const [key, val] of Object.entries(ns)) {
50
+ if (typeof val === "function") {
51
+ const refName = `__ref${refCounter++}`;
52
+ await jail.set(refName, new ivm.Reference(val));
53
+ nsSetup += `
54
+ globalThis[${JSON.stringify(name)}][${JSON.stringify(key)}] = function(...args) {
55
+ return ${refName}.apply(undefined, args, {
56
+ arguments: { copy: true },
57
+ result: { promise: true, copy: true },
58
+ });
59
+ };
60
+ `;
61
+ }
62
+ }
63
+ const dataProps = Object.entries(ns).filter(([, v]) => typeof v !== "function");
64
+ if (dataProps.length > 0) {
65
+ const dataObj = Object.fromEntries(dataProps);
66
+ nsSetup += `Object.assign(globalThis[${JSON.stringify(name)}], ${JSON.stringify(dataObj)});
67
+ `;
68
+ }
69
+ await context.eval(nsSetup);
70
+ } else {
71
+ await context.eval(
72
+ `globalThis[${JSON.stringify(name)}] = ${JSON.stringify(value)};`
73
+ );
74
+ }
75
+ }
76
+ const wrappedCode = `(${code})()`;
77
+ const script = await isolate.compileScript(wrappedCode);
78
+ const result = await script.run(context, {
79
+ timeout: this.timeoutMs,
80
+ promise: true,
81
+ copy: true
82
+ });
83
+ context.release();
84
+ return { result, logs };
85
+ } catch (err) {
86
+ return {
87
+ result: void 0,
88
+ error: err instanceof Error ? err.message : String(err),
89
+ logs
90
+ };
91
+ } finally {
92
+ if (!isolate.isDisposed) {
93
+ isolate.dispose();
94
+ }
95
+ }
96
+ }
97
+ };
98
+ function isNamespaceWithMethods(value) {
99
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).some(
100
+ (v) => typeof v === "function"
101
+ );
102
+ }
103
+ function stringify(value) {
104
+ if (typeof value === "string") return value;
105
+ try {
106
+ return JSON.stringify(value);
107
+ } catch {
108
+ return String(value);
109
+ }
110
+ }
111
+
112
+ export {
113
+ IsolatedVMExecutor
114
+ };
115
+ //# sourceMappingURL=chunk-NSUQUO7S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/executor/isolated-vm.ts"],"sourcesContent":["import type { Executor, ExecuteResult, SandboxOptions } from \"../types.js\";\n\n/**\n * Executor implementation using isolated-vm (V8 isolates).\n * Requires `isolated-vm` v6+ as a peer dependency.\n *\n * Each execute() call creates a fresh V8 isolate with its own heap — no state\n * leaks between calls. The sandbox has zero I/O capabilities by default (no\n * fetch, no fs, no require). The only way out is through injected host functions.\n */\nexport class IsolatedVMExecutor implements Executor {\n private memoryMB: number;\n private timeoutMs: number;\n\n constructor(options: SandboxOptions = {}) {\n this.memoryMB = options.memoryMB ?? 64;\n this.timeoutMs = options.timeoutMs ?? 30_000;\n }\n\n async execute(\n code: string,\n globals: Record<string, unknown>,\n ): Promise<ExecuteResult> {\n // @ts-ignore — optional peer dependency\n const ivm = (await import(\"isolated-vm\")).default ?? (await import(\"isolated-vm\"));\n const isolate = new ivm.Isolate({ memoryLimit: this.memoryMB });\n const logs: string[] = [];\n\n try {\n const context = await isolate.createContext();\n const jail = context.global;\n await jail.set(\"global\", jail.derefInto());\n\n // Inject console (captures logs via Callback)\n jail.setSync(\n \"__log\",\n new ivm.Callback((...args: unknown[]) => {\n logs.push(\n args\n .map((a) => (typeof a === \"string\" ? a : stringify(a)))\n .join(\" \"),\n );\n }),\n );\n await context.eval(`\n globalThis.console = {\n log: (...args) => __log(...args),\n warn: (...args) => __log(...args),\n error: (...args) => __log(...args),\n };\n `);\n\n // Inject globals\n let refCounter = 0;\n for (const [name, value] of Object.entries(globals)) {\n if (typeof value === \"function\") {\n // Async host function: set Reference, wrap with .apply() in isolate\n const refName = `__ref${refCounter++}`;\n await jail.set(refName, new ivm.Reference(value));\n await context.eval(`\n globalThis[${JSON.stringify(name)}] = function(...args) {\n return ${refName}.apply(undefined, args, {\n arguments: { copy: true },\n result: { promise: true, copy: true },\n });\n };\n `);\n } else if (isNamespaceWithMethods(value)) {\n // Namespace object with methods (e.g. { request: fn })\n const ns = value as Record<string, unknown>;\n let nsSetup = `globalThis[${JSON.stringify(name)}] = {};\\n`;\n\n for (const [key, val] of Object.entries(ns)) {\n if (typeof val === \"function\") {\n const refName = `__ref${refCounter++}`;\n await jail.set(refName, new ivm.Reference(val));\n nsSetup += `\n globalThis[${JSON.stringify(name)}][${JSON.stringify(key)}] = function(...args) {\n return ${refName}.apply(undefined, args, {\n arguments: { copy: true },\n result: { promise: true, copy: true },\n });\n };\n `;\n }\n }\n\n // Inject non-function properties as JSON\n const dataProps = Object.entries(ns).filter(([, v]) => typeof v !== \"function\");\n if (dataProps.length > 0) {\n const dataObj = Object.fromEntries(dataProps);\n nsSetup += `Object.assign(globalThis[${JSON.stringify(name)}], ${JSON.stringify(dataObj)});\\n`;\n }\n\n await context.eval(nsSetup);\n } else {\n // Plain data: inject as JSON\n await context.eval(\n `globalThis[${JSON.stringify(name)}] = ${JSON.stringify(value)};`,\n );\n }\n }\n\n // Execute the code\n const wrappedCode = `(${code})()`;\n const script = await isolate.compileScript(wrappedCode);\n const result = await script.run(context, {\n timeout: this.timeoutMs,\n promise: true,\n copy: true,\n });\n\n context.release();\n return { result, logs };\n } catch (err) {\n return {\n result: undefined,\n error: err instanceof Error ? err.message : String(err),\n logs,\n };\n } finally {\n if (!isolate.isDisposed) {\n isolate.dispose();\n }\n }\n }\n}\n\nfunction isNamespaceWithMethods(value: unknown): boolean {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.values(value as Record<string, unknown>).some(\n (v) => typeof v === \"function\",\n )\n );\n}\n\nfunction stringify(value: unknown): string {\n if (typeof value === \"string\") return value;\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,10 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export {
8
+ __export
9
+ };
10
+ //# sourceMappingURL=chunk-PZ5AY32C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,148 @@
1
+ // src/executor/quickjs.ts
2
+ var QuickJSExecutor = class {
3
+ memoryBytes;
4
+ timeoutMs;
5
+ constructor(options = {}) {
6
+ this.memoryBytes = (options.memoryMB ?? 64) * 1024 * 1024;
7
+ this.timeoutMs = options.timeoutMs ?? 3e4;
8
+ }
9
+ async execute(code, globals) {
10
+ const qjs = await import("quickjs-emscripten");
11
+ const logs = [];
12
+ const vm = await qjs.newAsyncContext();
13
+ const runtime = vm.runtime;
14
+ runtime.setMemoryLimit(this.memoryBytes);
15
+ runtime.setMaxStackSize(1024 * 320);
16
+ const deadline = Date.now() + this.timeoutMs;
17
+ runtime.setInterruptHandler(() => Date.now() > deadline);
18
+ try {
19
+ injectConsole(vm, logs);
20
+ for (const [name, value] of Object.entries(globals)) {
21
+ if (typeof value === "function") {
22
+ injectAsyncFunction(vm, name, value);
23
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).some((v) => typeof v === "function")) {
24
+ injectNamespace(vm, name, value);
25
+ } else {
26
+ const jsonStr = JSON.stringify(value);
27
+ const handle = vm.evalCode(`(${jsonStr})`);
28
+ if (handle.error) {
29
+ handle.error.dispose();
30
+ } else {
31
+ vm.setProp(vm.global, name, handle.value);
32
+ handle.value.dispose();
33
+ }
34
+ }
35
+ }
36
+ const wrappedCode = `(${code})()`;
37
+ const resultHandle = await vm.evalCodeAsync(wrappedCode);
38
+ if (resultHandle.error) {
39
+ let error = vm.dump(resultHandle.error);
40
+ try {
41
+ resultHandle.error.dispose();
42
+ } catch {
43
+ }
44
+ if (typeof error === "object" && error?.type === "rejected") {
45
+ error = error.reason;
46
+ }
47
+ return {
48
+ result: void 0,
49
+ error: typeof error === "object" && error?.message ? error.message : String(error),
50
+ logs
51
+ };
52
+ }
53
+ let result = vm.dump(resultHandle.value);
54
+ try {
55
+ resultHandle.value.dispose();
56
+ } catch {
57
+ }
58
+ result = unwrapPromiseResult(result);
59
+ return { result, logs };
60
+ } catch (err) {
61
+ return {
62
+ result: void 0,
63
+ error: err instanceof Error ? err.message : String(err),
64
+ logs
65
+ };
66
+ } finally {
67
+ try {
68
+ vm.dispose();
69
+ } catch {
70
+ }
71
+ try {
72
+ runtime.dispose();
73
+ } catch {
74
+ }
75
+ }
76
+ }
77
+ };
78
+ function injectConsole(vm, logs) {
79
+ const consoleObj = vm.newObject();
80
+ const logFn = vm.newFunction("log", (...args) => {
81
+ const values = args.map((h) => vm.dump(h));
82
+ logs.push(values.map((v) => typeof v === "string" ? v : JSON.stringify(v)).join(" "));
83
+ });
84
+ vm.setProp(consoleObj, "log", logFn);
85
+ vm.setProp(consoleObj, "warn", logFn);
86
+ vm.setProp(consoleObj, "error", logFn);
87
+ vm.setProp(vm.global, "console", consoleObj);
88
+ logFn.dispose();
89
+ consoleObj.dispose();
90
+ }
91
+ function injectAsyncFunction(vm, name, fn) {
92
+ const handle = vm.newAsyncifiedFunction(name, async (...argHandles) => {
93
+ const args = argHandles.map((h) => vm.dump(h));
94
+ const result = await fn(...args);
95
+ return marshalToVM(vm, result);
96
+ });
97
+ vm.setProp(vm.global, name, handle);
98
+ handle.dispose();
99
+ }
100
+ function injectNamespace(vm, name, ns) {
101
+ const nsObj = vm.newObject();
102
+ for (const [key, value] of Object.entries(ns)) {
103
+ if (typeof value === "function") {
104
+ const handle = vm.newAsyncifiedFunction(key, async (...argHandles) => {
105
+ const args = argHandles.map((h) => vm.dump(h));
106
+ const result = await value(...args);
107
+ return marshalToVM(vm, result);
108
+ });
109
+ vm.setProp(nsObj, key, handle);
110
+ handle.dispose();
111
+ } else {
112
+ const jsonStr = JSON.stringify(value);
113
+ const handle = vm.evalCode(`(${jsonStr})`);
114
+ if (!handle.error) {
115
+ vm.setProp(nsObj, key, handle.value);
116
+ handle.value.dispose();
117
+ } else {
118
+ handle.error.dispose();
119
+ }
120
+ }
121
+ }
122
+ vm.setProp(vm.global, name, nsObj);
123
+ nsObj.dispose();
124
+ }
125
+ function unwrapPromiseResult(result) {
126
+ if (typeof result === "object" && result !== null && "type" in result && result.type === "fulfilled" && "value" in result) {
127
+ return result.value;
128
+ }
129
+ return result;
130
+ }
131
+ function marshalToVM(vm, value) {
132
+ if (value === void 0 || value === null) return vm.undefined;
133
+ if (typeof value === "string") return vm.newString(value);
134
+ if (typeof value === "number") return vm.newNumber(value);
135
+ if (typeof value === "boolean") return value ? vm.true : vm.false;
136
+ const jsonStr = JSON.stringify(value);
137
+ const result = vm.evalCode(`(${jsonStr})`);
138
+ if (result.error) {
139
+ result.error.dispose();
140
+ return vm.undefined;
141
+ }
142
+ return result.value;
143
+ }
144
+
145
+ export {
146
+ QuickJSExecutor
147
+ };
148
+ //# sourceMappingURL=chunk-ZWSO33DZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/executor/quickjs.ts"],"sourcesContent":["import type { Executor, ExecuteResult, SandboxOptions } from \"../types.js\";\n\n/**\n * Executor implementation using quickjs-emscripten (QuickJS compiled to WASM).\n * Requires `quickjs-emscripten` as a peer dependency.\n *\n * Advantages over isolated-vm:\n * - Pure WASM, no native dependencies (works everywhere including Bun)\n * - WASM-level sandbox isolation\n *\n * Tradeoffs:\n * - ~3-5x slower than V8 for compute (negligible for API orchestration)\n * - Only one async suspension at a time per module\n */\nexport class QuickJSExecutor implements Executor {\n private memoryBytes: number;\n private timeoutMs: number;\n\n constructor(options: SandboxOptions = {}) {\n this.memoryBytes = (options.memoryMB ?? 64) * 1024 * 1024;\n this.timeoutMs = options.timeoutMs ?? 30_000;\n }\n\n async execute(\n code: string,\n globals: Record<string, unknown>,\n ): Promise<ExecuteResult> {\n const qjs = await import(\"quickjs-emscripten\");\n const logs: string[] = [];\n\n // Use the convenience function that manages module/runtime lifecycle\n const vm = await qjs.newAsyncContext();\n const runtime = vm.runtime;\n\n runtime.setMemoryLimit(this.memoryBytes);\n runtime.setMaxStackSize(1024 * 320); // 320KB stack\n\n // Interrupt after timeout\n const deadline = Date.now() + this.timeoutMs;\n runtime.setInterruptHandler(() => Date.now() > deadline);\n\n try {\n // Inject console\n injectConsole(vm, logs);\n\n // Inject globals\n for (const [name, value] of Object.entries(globals)) {\n if (typeof value === \"function\") {\n injectAsyncFunction(vm, name, value as (...args: unknown[]) => unknown);\n } else if (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.values(value as Record<string, unknown>).some((v) => typeof v === \"function\")\n ) {\n injectNamespace(vm, name, value as Record<string, unknown>);\n } else {\n // Inject as JSON data\n const jsonStr = JSON.stringify(value);\n const handle = vm.evalCode(`(${jsonStr})`);\n if (handle.error) {\n handle.error.dispose();\n } else {\n vm.setProp(vm.global, name, handle.value);\n handle.value.dispose();\n }\n }\n }\n\n // Execute the code\n const wrappedCode = `(${code})()`;\n const resultHandle = await vm.evalCodeAsync(wrappedCode);\n\n if (resultHandle.error) {\n let error = vm.dump(resultHandle.error);\n try { resultHandle.error.dispose(); } catch { /* already disposed */ }\n // Unwrap rejected promise wrapper\n if (typeof error === \"object\" && error?.type === \"rejected\") {\n error = error.reason;\n }\n return {\n result: undefined,\n error: typeof error === \"object\" && error?.message ? error.message : String(error),\n logs,\n };\n }\n\n let result = vm.dump(resultHandle.value);\n // The value handle from async evaluation may already be managed/disposed\n // by the runtime, so we guard the dispose call\n try { resultHandle.value.dispose(); } catch { /* already disposed */ }\n\n // evalCodeAsync wraps async results as { type: 'fulfilled', value } or { type: 'rejected', reason }\n result = unwrapPromiseResult(result);\n\n return { result, logs };\n } catch (err) {\n return {\n result: undefined,\n error: err instanceof Error ? err.message : String(err),\n logs,\n };\n } finally {\n // Dispose context first, then runtime\n // Use try/catch because quickjs-emscripten can throw during\n // cleanup when host references are freed\n try { vm.dispose(); } catch { /* ignore cleanup errors */ }\n try { runtime.dispose(); } catch { /* ignore cleanup errors */ }\n }\n }\n}\n\nfunction injectConsole(vm: any, logs: string[]): void {\n const consoleObj = vm.newObject();\n\n const logFn = vm.newFunction(\"log\", (...args: any[]) => {\n const values = args.map((h: any) => vm.dump(h));\n logs.push(values.map((v: unknown) => (typeof v === \"string\" ? v : JSON.stringify(v))).join(\" \"));\n });\n\n vm.setProp(consoleObj, \"log\", logFn);\n vm.setProp(consoleObj, \"warn\", logFn);\n vm.setProp(consoleObj, \"error\", logFn);\n vm.setProp(vm.global, \"console\", consoleObj);\n\n logFn.dispose();\n consoleObj.dispose();\n}\n\nfunction injectAsyncFunction(\n vm: any,\n name: string,\n fn: (...args: unknown[]) => unknown,\n): void {\n const handle = vm.newAsyncifiedFunction(name, async (...argHandles: any[]) => {\n const args = argHandles.map((h: any) => vm.dump(h));\n const result = await fn(...args);\n return marshalToVM(vm, result);\n });\n vm.setProp(vm.global, name, handle);\n handle.dispose();\n}\n\nfunction injectNamespace(\n vm: any,\n name: string,\n ns: Record<string, unknown>,\n): void {\n const nsObj = vm.newObject();\n\n for (const [key, value] of Object.entries(ns)) {\n if (typeof value === \"function\") {\n const handle = vm.newAsyncifiedFunction(key, async (...argHandles: any[]) => {\n const args = argHandles.map((h: any) => vm.dump(h));\n const result = await value(...args);\n return marshalToVM(vm, result);\n });\n vm.setProp(nsObj, key, handle);\n handle.dispose();\n } else {\n const jsonStr = JSON.stringify(value);\n const handle = vm.evalCode(`(${jsonStr})`);\n if (!handle.error) {\n vm.setProp(nsObj, key, handle.value);\n handle.value.dispose();\n } else {\n handle.error.dispose();\n }\n }\n }\n\n vm.setProp(vm.global, name, nsObj);\n nsObj.dispose();\n}\n\n/**\n * When evalCodeAsync runs an async arrow function, the dump of the result\n * is `{ type: 'fulfilled', value: X }` instead of just `X`.\n * This unwraps it.\n */\nfunction unwrapPromiseResult(result: unknown): unknown {\n if (\n typeof result === \"object\" &&\n result !== null &&\n \"type\" in result &&\n (result as any).type === \"fulfilled\" &&\n \"value\" in result\n ) {\n return (result as any).value;\n }\n return result;\n}\n\nfunction marshalToVM(vm: any, value: unknown): any {\n if (value === undefined || value === null) return vm.undefined;\n if (typeof value === \"string\") return vm.newString(value);\n if (typeof value === \"number\") return vm.newNumber(value);\n if (typeof value === \"boolean\") return value ? vm.true : vm.false;\n\n // For objects/arrays, serialize to JSON and parse inside VM\n const jsonStr = JSON.stringify(value);\n const result = vm.evalCode(`(${jsonStr})`);\n if (result.error) {\n result.error.dispose();\n return vm.undefined;\n }\n return result.value;\n}\n"],"mappings":"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","names":[]}