@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 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 a sandbox runtime (pick one):
18
- pnpm add isolated-vm # V8 isolates — fastest, recommended
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
- 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
- );
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 as a global
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 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).
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` | `{ memoryMB?, timeoutMs? }` | `{ 64, 30000 }` | Sandbox resource limits |
113
- | `executor` | `Executor` | auto-detect | Custom sandbox 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 full OpenAPI 3.x document:
174
+ The `spec` global is the preprocessed OpenAPI spec with all `$ref` pointers resolved inline:
146
175
 
147
176
  ```javascript
148
- // Find endpoints by keyword
177
+ // Find endpoints by tag
149
178
  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
- );
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 a specific schema
162
- async () => spec.components?.schemas?.Product
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
- return api.request({
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 auto-detects your installed sandbox runtime. You can also pass one explicitly:
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({ memoryMB: 128, timeoutMs: 60_000 }),
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
- const logs = [];
14
+ let context;
13
15
  try {
14
- const context = await isolate.createContext();
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: (...args) => __log(...args),
28
- warn: (...args) => __log(...args),
29
- error: (...args) => __log(...args),
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
- const result = await script.run(context, {
79
- timeout: this.timeoutMs,
80
- promise: true,
81
- copy: true
82
- });
83
- context.release();
84
- return { result, logs };
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-NSUQUO7S.js.map
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 implementations:
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
- /** Execution timeout in ms (default: 30000) */
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, auto-detects
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-DbXujxeq.js';
2
- export { C as CodeMode, b as CodeModeOptions, O as OpenAPISpec, c as SpecProvider, T as ToolCallResult, d as ToolDefinition } from './codemode-DbXujxeq.js';
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
- * Executor implementation using quickjs-emscripten (QuickJS compiled to WASM).
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, QuickJSExecutor, RequestHandler, SandboxOptions, type SandboxRequestOptions, type SandboxResponse, createExecutor, createRequestBridge };
99
+ export { ExecuteResult, Executor, IsolatedVMExecutor, OpenAPISpec, RequestHandler, SandboxOptions, type SandboxRequestOptions, type SandboxResponse, createExecutor, createRequestBridge, extractServerBasePath, extractTags, processSpec, resolveRefs, truncateResponse };