@robinbraemer/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/README.md +91 -40
- package/dist/{chunk-NSUQUO7S.js → chunk-M4AL5G4U.js} +28 -31
- package/dist/chunk-M4AL5G4U.js.map +1 -0
- package/dist/{codemode-DbXujxeq.d.ts → codemode-C8y0tnb7.d.ts} +44 -7
- package/dist/index.d.ts +55 -25
- package/dist/index.js +485 -116
- package/dist/index.js.map +1 -1
- package/dist/{isolated-vm-57EIYEJM.js → isolated-vm-7REUQAB5.js} +2 -2
- package/dist/mcp.d.ts +5 -12
- package/dist/mcp.js +5 -3
- package/dist/mcp.js.map +1 -1
- package/package.json +5 -8
- package/dist/chunk-NSUQUO7S.js.map +0 -1
- package/dist/chunk-ZWSO33DZ.js +0 -148
- package/dist/chunk-ZWSO33DZ.js.map +0 -1
- package/dist/quickjs-BGVQS2YE.js +0 -8
- package/dist/quickjs-BGVQS2YE.js.map +0 -1
- /package/dist/{isolated-vm-57EIYEJM.js.map → isolated-vm-7REUQAB5.js.map} +0 -0
package/README.md
CHANGED
|
@@ -9,20 +9,33 @@ Instead of defining individual MCP tools for every API endpoint (`list-pods`, `c
|
|
|
9
9
|
|
|
10
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
11
|
|
|
12
|
+
## Try It
|
|
13
|
+
|
|
14
|
+
Requires [mise](https://mise.jdx.dev/) for tooling (Node.js, pnpm, Task):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
git clone https://github.com/cnap-tech/codemode.git
|
|
18
|
+
cd codemode
|
|
19
|
+
mise install # installs Node 24, pnpm 10, Task
|
|
20
|
+
task install # installs dependencies
|
|
21
|
+
task example # runs the Petstore demo
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Fetches the real Petstore OpenAPI spec from the web, then runs search + execute against a local Hono mock — no API keys needed.
|
|
25
|
+
|
|
12
26
|
## Install
|
|
13
27
|
|
|
14
28
|
```bash
|
|
15
|
-
pnpm add codemode
|
|
29
|
+
pnpm add @robinbraemer/codemode
|
|
16
30
|
|
|
17
|
-
# Install
|
|
18
|
-
pnpm add isolated-vm # V8 isolates
|
|
19
|
-
pnpm add quickjs-emscripten # WASM — portable fallback
|
|
31
|
+
# Install the sandbox runtime:
|
|
32
|
+
pnpm add isolated-vm # V8 isolates
|
|
20
33
|
```
|
|
21
34
|
|
|
22
35
|
## Quick Start
|
|
23
36
|
|
|
24
37
|
```typescript
|
|
25
|
-
import { CodeMode } from 'codemode';
|
|
38
|
+
import { CodeMode } from '@robinbraemer/codemode';
|
|
26
39
|
import { Hono } from 'hono';
|
|
27
40
|
|
|
28
41
|
const app = new Hono();
|
|
@@ -40,13 +53,15 @@ const codemode = new CodeMode({
|
|
|
40
53
|
// The agent searches the spec to discover endpoints...
|
|
41
54
|
const search = await codemode.callTool('search', {
|
|
42
55
|
code: `async () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
const results = [];
|
|
57
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
58
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
59
|
+
if (op.tags?.some(t => t.toLowerCase() === 'clusters')) {
|
|
60
|
+
results.push({ method: method.toUpperCase(), path, summary: op.summary });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
50
65
|
}`
|
|
51
66
|
});
|
|
52
67
|
|
|
@@ -64,8 +79,8 @@ const result = await codemode.callTool('execute', {
|
|
|
64
79
|
```typescript
|
|
65
80
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
66
81
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
67
|
-
import { CodeMode } from 'codemode';
|
|
68
|
-
import { registerTools } from 'codemode/mcp';
|
|
82
|
+
import { CodeMode } from '@robinbraemer/codemode';
|
|
83
|
+
import { registerTools } from '@robinbraemer/codemode/mcp';
|
|
69
84
|
|
|
70
85
|
const codemode = new CodeMode({
|
|
71
86
|
spec: () => fetchOpenAPISpec(),
|
|
@@ -87,7 +102,8 @@ AI Agent
|
|
|
87
102
|
▼
|
|
88
103
|
CodeMode MCP Server
|
|
89
104
|
│
|
|
90
|
-
├─ search(code) → runs JS with OpenAPI spec
|
|
105
|
+
├─ search(code) → runs JS with preprocessed OpenAPI spec
|
|
106
|
+
│ → all $refs resolved inline, only essential fields kept
|
|
91
107
|
│ → agent discovers endpoints, schemas, parameters
|
|
92
108
|
│
|
|
93
109
|
└─ execute(code) → runs JS with injected request client
|
|
@@ -95,7 +111,7 @@ CodeMode MCP Server
|
|
|
95
111
|
→ no network hop, auth handled automatically
|
|
96
112
|
```
|
|
97
113
|
|
|
98
|
-
All code runs in an isolated
|
|
114
|
+
All code runs in an isolated V8 sandbox. 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
115
|
|
|
100
116
|
Each tool call gets a fresh sandbox with no state carried over between calls.
|
|
101
117
|
|
|
@@ -107,10 +123,23 @@ Each tool call gets a fresh sandbox with no state carried over between calls.
|
|
|
107
123
|
|--------|------|---------|-------------|
|
|
108
124
|
| `spec` | `OpenAPISpec \| () => OpenAPISpec \| Promise<OpenAPISpec>` | required | OpenAPI 3.x spec or async getter |
|
|
109
125
|
| `request` | `(input, init?) => Response` | required | Fetch-compatible handler (`app.request.bind(app)` for Hono) |
|
|
110
|
-
| `namespace` | `string` | `"api"` | Client name in sandbox (`api.request(...)`) |
|
|
126
|
+
| `namespace` | `string` | `"api"` | Client name in sandbox (`api.request(...)`). Must be a valid JS identifier, not a reserved name. |
|
|
111
127
|
| `baseUrl` | `string` | `"http://localhost"` | Base URL for relative paths |
|
|
112
|
-
| `sandbox` | `
|
|
113
|
-
| `executor` | `Executor` |
|
|
128
|
+
| `sandbox` | `SandboxOptions` | see below | Sandbox resource limits |
|
|
129
|
+
| `executor` | `Executor` | `IsolatedVMExecutor` | Custom sandbox executor |
|
|
130
|
+
| `maxResponseTokens` | `number` | `25000` | Token limit for response truncation (0 to disable) |
|
|
131
|
+
| `maxRequests` | `number` | `50` | Max requests per `execute()` call |
|
|
132
|
+
| `maxResponseBytes` | `number` | `10485760` | Max response body size in bytes (10MB) |
|
|
133
|
+
| `allowedHeaders` | `string[]` | `undefined` | Header whitelist. When unset, a blocklist strips `Authorization`, `Cookie`, `Host`, `X-Forwarded-*`, `Proxy-*`. |
|
|
134
|
+
| `maxRefDepth` | `number` | `50` | Max `$ref` resolution depth |
|
|
135
|
+
|
|
136
|
+
#### `SandboxOptions`
|
|
137
|
+
|
|
138
|
+
| Option | Type | Default | Description |
|
|
139
|
+
|--------|------|---------|-------------|
|
|
140
|
+
| `memoryMB` | `number` | `64` | V8 isolate memory limit |
|
|
141
|
+
| `timeoutMs` | `number` | `30000` | CPU timeout in ms (caps pure compute) |
|
|
142
|
+
| `wallTimeMs` | `number` | `60000` | Wall-clock timeout in ms (caps total elapsed time including async I/O) |
|
|
114
143
|
|
|
115
144
|
### Methods
|
|
116
145
|
|
|
@@ -142,24 +171,27 @@ Clean up sandbox resources.
|
|
|
142
171
|
|
|
143
172
|
### Inside `search`
|
|
144
173
|
|
|
145
|
-
The `spec` global is the
|
|
174
|
+
The `spec` global is the preprocessed OpenAPI spec with all `$ref` pointers resolved inline:
|
|
146
175
|
|
|
147
176
|
```javascript
|
|
148
|
-
// Find endpoints by
|
|
177
|
+
// Find endpoints by tag
|
|
149
178
|
async () => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
179
|
+
const results = [];
|
|
180
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
181
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
182
|
+
if (op.tags?.some(t => t.toLowerCase() === 'clusters')) {
|
|
183
|
+
results.push({ method: method.toUpperCase(), path, summary: op.summary });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return results;
|
|
159
188
|
}
|
|
160
189
|
|
|
161
|
-
// Get
|
|
162
|
-
async () =>
|
|
190
|
+
// Get endpoint with requestBody schema (refs are already resolved)
|
|
191
|
+
async () => {
|
|
192
|
+
const op = spec.paths['/v1/products']?.post;
|
|
193
|
+
return { summary: op?.summary, requestBody: op?.requestBody };
|
|
194
|
+
}
|
|
163
195
|
|
|
164
196
|
// Spec metadata
|
|
165
197
|
async () => ({
|
|
@@ -186,11 +218,12 @@ async () => {
|
|
|
186
218
|
|
|
187
219
|
// POST with body
|
|
188
220
|
async () => {
|
|
189
|
-
|
|
221
|
+
const res = await api.request({
|
|
190
222
|
method: "POST",
|
|
191
223
|
path: "/v1/products",
|
|
192
224
|
body: { name: "Redis", chart: "bitnami/redis" },
|
|
193
225
|
});
|
|
226
|
+
return { status: res.status, body: res.body };
|
|
194
227
|
}
|
|
195
228
|
|
|
196
229
|
// Chain calls
|
|
@@ -217,33 +250,51 @@ async () => {
|
|
|
217
250
|
|
|
218
251
|
**Response:** `{ status: number, headers: Record<string, string>, body: unknown }`
|
|
219
252
|
|
|
253
|
+
## Spec Preprocessing
|
|
254
|
+
|
|
255
|
+
CodeMode automatically preprocesses your OpenAPI spec before passing it to the search sandbox:
|
|
256
|
+
|
|
257
|
+
- **`$ref` resolution** — all `$ref` pointers are resolved inline (circular refs become `{ $circular: ref }`)
|
|
258
|
+
- **Field extraction** — only essential fields kept per operation: `summary`, `description`, `tags`, `operationId`, `parameters`, `requestBody`, `responses`
|
|
259
|
+
- **Metadata preserved** — `info`, `servers`, and `components.schemas` are kept alongside processed paths
|
|
260
|
+
|
|
261
|
+
You can also use the preprocessing utilities directly:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { resolveRefs, processSpec, extractTags } from '@robinbraemer/codemode';
|
|
265
|
+
|
|
266
|
+
const processed = processSpec(rawSpec);
|
|
267
|
+
const tags = extractTags(rawSpec);
|
|
268
|
+
```
|
|
269
|
+
|
|
220
270
|
## Executors
|
|
221
271
|
|
|
222
|
-
CodeMode
|
|
272
|
+
CodeMode uses `isolated-vm` (V8 isolates) for sandboxed execution. You can pass a custom instance:
|
|
223
273
|
|
|
224
274
|
```typescript
|
|
225
|
-
import { CodeMode, IsolatedVMExecutor } from 'codemode';
|
|
275
|
+
import { CodeMode, IsolatedVMExecutor } from '@robinbraemer/codemode';
|
|
226
276
|
|
|
227
277
|
const codemode = new CodeMode({
|
|
228
278
|
spec,
|
|
229
279
|
request: handler,
|
|
230
|
-
executor: new IsolatedVMExecutor({
|
|
280
|
+
executor: new IsolatedVMExecutor({
|
|
281
|
+
memoryMB: 128,
|
|
282
|
+
timeoutMs: 60_000, // CPU time limit
|
|
283
|
+
wallTimeMs: 120_000, // total elapsed time limit
|
|
284
|
+
}),
|
|
231
285
|
});
|
|
232
286
|
```
|
|
233
287
|
|
|
234
288
|
| Executor | Package | Performance | Portability |
|
|
235
289
|
|----------|---------|-------------|-------------|
|
|
236
290
|
| `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
291
|
|
|
241
292
|
### Custom Executor
|
|
242
293
|
|
|
243
294
|
Implement the `Executor` interface to use your own sandbox:
|
|
244
295
|
|
|
245
296
|
```typescript
|
|
246
|
-
import { CodeMode, type Executor, type ExecuteResult } from 'codemode';
|
|
297
|
+
import { CodeMode, type Executor, type ExecuteResult } from '@robinbraemer/codemode';
|
|
247
298
|
|
|
248
299
|
class MyExecutor implements Executor {
|
|
249
300
|
async execute(code: string, globals: Record<string, unknown>): Promise<ExecuteResult> {
|
|
@@ -2,31 +2,25 @@
|
|
|
2
2
|
var IsolatedVMExecutor = class {
|
|
3
3
|
memoryMB;
|
|
4
4
|
timeoutMs;
|
|
5
|
+
wallTimeMs;
|
|
5
6
|
constructor(options = {}) {
|
|
6
7
|
this.memoryMB = options.memoryMB ?? 64;
|
|
7
8
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
9
|
+
this.wallTimeMs = options.wallTimeMs ?? 6e4;
|
|
8
10
|
}
|
|
9
11
|
async execute(code, globals) {
|
|
10
12
|
const ivm = (await import("isolated-vm")).default ?? await import("isolated-vm");
|
|
11
13
|
const isolate = new ivm.Isolate({ memoryLimit: this.memoryMB });
|
|
12
|
-
|
|
14
|
+
let context;
|
|
13
15
|
try {
|
|
14
|
-
|
|
16
|
+
context = await isolate.createContext();
|
|
15
17
|
const jail = context.global;
|
|
16
18
|
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
19
|
await context.eval(`
|
|
26
20
|
globalThis.console = {
|
|
27
|
-
log: (
|
|
28
|
-
warn: (
|
|
29
|
-
error: (
|
|
21
|
+
log: () => {},
|
|
22
|
+
warn: () => {},
|
|
23
|
+
error: () => {},
|
|
30
24
|
};
|
|
31
25
|
`);
|
|
32
26
|
let refCounter = 0;
|
|
@@ -75,20 +69,31 @@ var IsolatedVMExecutor = class {
|
|
|
75
69
|
}
|
|
76
70
|
const wrappedCode = `(${code})()`;
|
|
77
71
|
const script = await isolate.compileScript(wrappedCode);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
let wallTimer;
|
|
73
|
+
const result = await Promise.race([
|
|
74
|
+
script.run(context, {
|
|
75
|
+
timeout: this.timeoutMs,
|
|
76
|
+
promise: true,
|
|
77
|
+
copy: true
|
|
78
|
+
}).finally(() => clearTimeout(wallTimer)),
|
|
79
|
+
new Promise((_, reject) => {
|
|
80
|
+
wallTimer = setTimeout(
|
|
81
|
+
() => reject(new Error("Wall-clock timeout exceeded")),
|
|
82
|
+
this.wallTimeMs
|
|
83
|
+
);
|
|
84
|
+
if (typeof wallTimer === "object" && wallTimer !== null && "unref" in wallTimer) {
|
|
85
|
+
wallTimer.unref();
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
]);
|
|
89
|
+
return { result };
|
|
85
90
|
} catch (err) {
|
|
86
91
|
return {
|
|
87
92
|
result: void 0,
|
|
88
|
-
error: err instanceof Error ? err.message : String(err)
|
|
89
|
-
logs
|
|
93
|
+
error: err instanceof Error ? err.message : String(err)
|
|
90
94
|
};
|
|
91
95
|
} finally {
|
|
96
|
+
context?.release();
|
|
92
97
|
if (!isolate.isDisposed) {
|
|
93
98
|
isolate.dispose();
|
|
94
99
|
}
|
|
@@ -100,16 +105,8 @@ function isNamespaceWithMethods(value) {
|
|
|
100
105
|
(v) => typeof v === "function"
|
|
101
106
|
);
|
|
102
107
|
}
|
|
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
108
|
|
|
112
109
|
export {
|
|
113
110
|
IsolatedVMExecutor
|
|
114
111
|
};
|
|
115
|
-
//# sourceMappingURL=chunk-
|
|
112
|
+
//# sourceMappingURL=chunk-M4AL5G4U.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 private wallTimeMs: number;\n\n constructor(options: SandboxOptions = {}) {\n this.memoryMB = options.memoryMB ?? 64;\n this.timeoutMs = options.timeoutMs ?? 30_000;\n this.wallTimeMs = options.wallTimeMs ?? 60_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\n let context: Awaited<ReturnType<typeof isolate.createContext>> | undefined;\n try {\n context = await isolate.createContext();\n const jail = context.global;\n await jail.set(\"global\", jail.derefInto());\n\n // No-op console — sandbox code should return data, not log it.\n // Injecting a real console would create an OOM vector since logs\n // accumulate in the host process outside the isolate memory limit.\n await context.eval(`\n globalThis.console = {\n log: () => {},\n warn: () => {},\n error: () => {},\n };\n `);\n\n // Inject globals — sequential awaits required: each jail.set/context.eval\n // depends on prior state (ref counters, globalThis assignments).\n /* oxlint-disable no-await-in-loop */\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 /* oxlint-enable no-await-in-loop */\n\n // Execute the code with both CPU timeout and wall-clock timeout.\n // The ivm timeout only covers CPU time; async host calls (request bridge)\n // can stall indefinitely without a wall-clock guard.\n const wrappedCode = `(${code})()`;\n const script = await isolate.compileScript(wrappedCode);\n\n let wallTimer: ReturnType<typeof setTimeout> | undefined;\n const result = await Promise.race([\n script.run(context, {\n timeout: this.timeoutMs,\n promise: true,\n copy: true,\n }).finally(() => clearTimeout(wallTimer)),\n new Promise<never>((_, reject) => {\n wallTimer = setTimeout(\n () => reject(new Error(\"Wall-clock timeout exceeded\")),\n this.wallTimeMs,\n );\n // Don't prevent process exit\n if (typeof wallTimer === \"object\" && wallTimer !== null && \"unref\" in wallTimer) {\n (wallTimer as { unref(): void }).unref();\n }\n }),\n ]);\n\n return { result };\n } catch (err) {\n return {\n result: undefined,\n error: err instanceof Error ? err.message : String(err),\n };\n } finally {\n context?.release();\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"],"mappings":"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","names":[]}
|
|
@@ -4,14 +4,12 @@
|
|
|
4
4
|
interface ExecuteResult {
|
|
5
5
|
result: unknown;
|
|
6
6
|
error?: string;
|
|
7
|
-
logs: string[];
|
|
8
7
|
}
|
|
9
8
|
/**
|
|
10
9
|
* Sandbox executor interface. Implement this to use a custom sandbox runtime.
|
|
11
10
|
*
|
|
12
|
-
* Built-in
|
|
11
|
+
* Built-in implementation:
|
|
13
12
|
* - `IsolatedVMExecutor` (requires `isolated-vm` peer dependency)
|
|
14
|
-
* - `QuickJSExecutor` (requires `quickjs-emscripten` peer dependency)
|
|
15
13
|
*/
|
|
16
14
|
interface Executor {
|
|
17
15
|
/**
|
|
@@ -33,8 +31,10 @@ interface Executor {
|
|
|
33
31
|
interface SandboxOptions {
|
|
34
32
|
/** Memory limit in MB (default: 64) */
|
|
35
33
|
memoryMB?: number;
|
|
36
|
-
/**
|
|
34
|
+
/** CPU timeout in ms — caps pure compute time (default: 30000) */
|
|
37
35
|
timeoutMs?: number;
|
|
36
|
+
/** Wall-clock timeout in ms — caps total elapsed time including async I/O (default: 60000) */
|
|
37
|
+
wallTimeMs?: number;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* A fetch-compatible request handler.
|
|
@@ -95,10 +95,35 @@ interface CodeModeOptions {
|
|
|
95
95
|
*/
|
|
96
96
|
sandbox?: SandboxOptions;
|
|
97
97
|
/**
|
|
98
|
-
* Custom executor instance. If not provided,
|
|
99
|
-
* isolated-vm or quickjs-emscripten from installed peer dependencies.
|
|
98
|
+
* Custom executor instance. If not provided, uses isolated-vm.
|
|
100
99
|
*/
|
|
101
100
|
executor?: Executor;
|
|
101
|
+
/**
|
|
102
|
+
* Maximum tokens for response truncation.
|
|
103
|
+
* Default: 25000 (~100KB). Set to 0 to disable truncation.
|
|
104
|
+
*/
|
|
105
|
+
maxResponseTokens?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Maximum number of requests per execution.
|
|
108
|
+
* Default: 50.
|
|
109
|
+
*/
|
|
110
|
+
maxRequests?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Maximum response body size in bytes.
|
|
113
|
+
* Default: 10MB (10_485_760).
|
|
114
|
+
*/
|
|
115
|
+
maxResponseBytes?: number;
|
|
116
|
+
/**
|
|
117
|
+
* Allowed headers whitelist. When set, only these headers are forwarded.
|
|
118
|
+
* When undefined, a default blocklist strips dangerous headers
|
|
119
|
+
* (Authorization, Cookie, Host, X-Forwarded-*, Proxy-*).
|
|
120
|
+
*/
|
|
121
|
+
allowedHeaders?: string[];
|
|
122
|
+
/**
|
|
123
|
+
* Maximum $ref resolution depth.
|
|
124
|
+
* Default: 50.
|
|
125
|
+
*/
|
|
126
|
+
maxRefDepth?: number;
|
|
102
127
|
}
|
|
103
128
|
/**
|
|
104
129
|
* MCP tool definition (compatible with @modelcontextprotocol/sdk).
|
|
@@ -155,13 +180,18 @@ interface ToolCallResult {
|
|
|
155
180
|
*/
|
|
156
181
|
declare class CodeMode {
|
|
157
182
|
private specProvider;
|
|
158
|
-
private requestBridge;
|
|
159
183
|
private namespace;
|
|
160
184
|
private executor;
|
|
161
185
|
private executorPromise;
|
|
162
186
|
private options;
|
|
163
187
|
private searchToolName;
|
|
164
188
|
private executeToolName;
|
|
189
|
+
private maxResponseTokens;
|
|
190
|
+
private bridgeHandler;
|
|
191
|
+
private bridgeBaseUrl;
|
|
192
|
+
private bridgeOptions;
|
|
193
|
+
private processedSpec;
|
|
194
|
+
private specContext;
|
|
165
195
|
constructor(options: CodeModeOptions);
|
|
166
196
|
/**
|
|
167
197
|
* Override the default tool names.
|
|
@@ -180,6 +210,7 @@ declare class CodeMode {
|
|
|
180
210
|
/**
|
|
181
211
|
* Execute a search against the OpenAPI spec.
|
|
182
212
|
* The code runs in a sandbox with `spec` available as a global.
|
|
213
|
+
* All $refs are pre-resolved inline.
|
|
183
214
|
*/
|
|
184
215
|
search(code: string): Promise<ToolCallResult>;
|
|
185
216
|
/**
|
|
@@ -192,7 +223,13 @@ declare class CodeMode {
|
|
|
192
223
|
*/
|
|
193
224
|
dispose(): void;
|
|
194
225
|
private resolveSpec;
|
|
226
|
+
/**
|
|
227
|
+
* Get the processed spec (refs resolved, fields extracted).
|
|
228
|
+
* Caches the result after first call.
|
|
229
|
+
*/
|
|
230
|
+
private getProcessedSpec;
|
|
195
231
|
private getExecutor;
|
|
232
|
+
private formatResult;
|
|
196
233
|
}
|
|
197
234
|
|
|
198
235
|
export { CodeMode as C, type Executor as E, type OpenAPISpec as O, type RequestHandler as R, type SandboxOptions as S, type ToolCallResult as T, type ExecuteResult as a, type CodeModeOptions as b, type SpecProvider as c, type ToolDefinition as d };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as Executor, S as SandboxOptions, a as ExecuteResult, R as RequestHandler } from './codemode-
|
|
2
|
-
export { C as CodeMode, b as CodeModeOptions,
|
|
1
|
+
import { E as Executor, S as SandboxOptions, a as ExecuteResult, R as RequestHandler, O as OpenAPISpec } from './codemode-C8y0tnb7.js';
|
|
2
|
+
export { C as CodeMode, b as CodeModeOptions, c as SpecProvider, T as ToolCallResult, d as ToolDefinition } from './codemode-C8y0tnb7.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Executor implementation using isolated-vm (V8 isolates).
|
|
@@ -12,32 +12,13 @@ export { C as CodeMode, b as CodeModeOptions, O as OpenAPISpec, c as SpecProvide
|
|
|
12
12
|
declare class IsolatedVMExecutor implements Executor {
|
|
13
13
|
private memoryMB;
|
|
14
14
|
private timeoutMs;
|
|
15
|
+
private wallTimeMs;
|
|
15
16
|
constructor(options?: SandboxOptions);
|
|
16
17
|
execute(code: string, globals: Record<string, unknown>): Promise<ExecuteResult>;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
* Requires `quickjs-emscripten` as a peer dependency.
|
|
22
|
-
*
|
|
23
|
-
* Advantages over isolated-vm:
|
|
24
|
-
* - Pure WASM, no native dependencies (works everywhere including Bun)
|
|
25
|
-
* - WASM-level sandbox isolation
|
|
26
|
-
*
|
|
27
|
-
* Tradeoffs:
|
|
28
|
-
* - ~3-5x slower than V8 for compute (negligible for API orchestration)
|
|
29
|
-
* - Only one async suspension at a time per module
|
|
30
|
-
*/
|
|
31
|
-
declare class QuickJSExecutor implements Executor {
|
|
32
|
-
private memoryBytes;
|
|
33
|
-
private timeoutMs;
|
|
34
|
-
constructor(options?: SandboxOptions);
|
|
35
|
-
execute(code: string, globals: Record<string, unknown>): Promise<ExecuteResult>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Auto-detect and create an executor from available peer dependencies.
|
|
40
|
-
* Tries isolated-vm first, then quickjs-emscripten.
|
|
21
|
+
* Create an executor using the isolated-vm peer dependency.
|
|
41
22
|
*/
|
|
42
23
|
declare function createExecutor(options?: SandboxOptions): Promise<Executor>;
|
|
43
24
|
|
|
@@ -60,10 +41,59 @@ interface SandboxResponse {
|
|
|
60
41
|
headers: Record<string, string>;
|
|
61
42
|
body: unknown;
|
|
62
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Options for configuring the request bridge.
|
|
46
|
+
*/
|
|
47
|
+
interface RequestBridgeOptions {
|
|
48
|
+
/** Maximum number of requests per bridge instance. Default: 50. */
|
|
49
|
+
maxRequests?: number;
|
|
50
|
+
/** Maximum response body size in bytes. Default: 10MB. */
|
|
51
|
+
maxResponseBytes?: number;
|
|
52
|
+
/** Allowed headers whitelist. When undefined, uses default blocklist. */
|
|
53
|
+
allowedHeaders?: string[];
|
|
54
|
+
}
|
|
63
55
|
/**
|
|
64
56
|
* Creates the `request()` function that gets injected into the execute sandbox.
|
|
65
57
|
* Bridges sandbox API calls to the host request handler (Hono app.request, fetch, etc.).
|
|
66
58
|
*/
|
|
67
|
-
declare function createRequestBridge(handler: RequestHandler, baseUrl: string): (options: SandboxRequestOptions) => Promise<SandboxResponse>;
|
|
59
|
+
declare function createRequestBridge(handler: RequestHandler, baseUrl: string, options?: RequestBridgeOptions): (options: SandboxRequestOptions) => Promise<SandboxResponse>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Recursively resolve all `$ref` pointers in an OpenAPI spec inline.
|
|
63
|
+
* Circular references are replaced with `{ $circular: ref }`.
|
|
64
|
+
*
|
|
65
|
+
* The `seen` set tracks the current ancestor chain only (not globally),
|
|
66
|
+
* so the same $ref used in sibling positions resolves correctly.
|
|
67
|
+
* A memoization cache avoids re-resolving the same $ref multiple times.
|
|
68
|
+
*
|
|
69
|
+
* @param maxDepth - Maximum $ref resolution depth (default: 50)
|
|
70
|
+
*/
|
|
71
|
+
declare function resolveRefs(obj: unknown, root: Record<string, unknown>, seen?: Set<string>, maxDepth?: number, _cache?: Map<string, unknown>): unknown;
|
|
72
|
+
/**
|
|
73
|
+
* Extract the base path from the first server URL in the spec.
|
|
74
|
+
* e.g. "https://petstore.io/api/v3" → "/api/v3"
|
|
75
|
+
* e.g. "/api/v3" → "/api/v3"
|
|
76
|
+
* e.g. "https://api.example.com" → ""
|
|
77
|
+
*/
|
|
78
|
+
declare function extractServerBasePath(spec: OpenAPISpec): string;
|
|
79
|
+
/**
|
|
80
|
+
* Process an OpenAPI spec into a simplified format for the search tool.
|
|
81
|
+
* Resolves all $refs inline and extracts only the fields needed for search.
|
|
82
|
+
* Prepends the server base path to all path keys so they're directly usable.
|
|
83
|
+
* Preserves info and components.schemas alongside processed paths.
|
|
84
|
+
*
|
|
85
|
+
* @param maxRefDepth - Maximum $ref resolution depth (default: 50)
|
|
86
|
+
*/
|
|
87
|
+
declare function processSpec(spec: OpenAPISpec, maxRefDepth?: number): Record<string, unknown>;
|
|
88
|
+
/**
|
|
89
|
+
* Extract unique tags from the spec, sorted by frequency (most common first).
|
|
90
|
+
*/
|
|
91
|
+
declare function extractTags(spec: OpenAPISpec): string[];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Truncate a response to fit within a token budget.
|
|
95
|
+
* Uses a ~4 chars/token estimate.
|
|
96
|
+
*/
|
|
97
|
+
declare function truncateResponse(content: unknown, maxTokens?: number): string;
|
|
68
98
|
|
|
69
|
-
export { ExecuteResult, Executor, IsolatedVMExecutor,
|
|
99
|
+
export { ExecuteResult, Executor, IsolatedVMExecutor, OpenAPISpec, RequestHandler, SandboxOptions, type SandboxRequestOptions, type SandboxResponse, createExecutor, createRequestBridge, extractServerBasePath, extractTags, processSpec, resolveRefs, truncateResponse };
|