@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/dist/index.js
CHANGED
|
@@ -1,45 +1,158 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IsolatedVMExecutor
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import {
|
|
5
|
-
QuickJSExecutor
|
|
6
|
-
} from "./chunk-ZWSO33DZ.js";
|
|
3
|
+
} from "./chunk-M4AL5G4U.js";
|
|
7
4
|
import "./chunk-PZ5AY32C.js";
|
|
8
5
|
|
|
9
6
|
// src/executor/auto.ts
|
|
10
7
|
async function createExecutor(options = {}) {
|
|
11
8
|
try {
|
|
12
9
|
await import("isolated-vm");
|
|
13
|
-
const { IsolatedVMExecutor: IsolatedVMExecutor2 } = await import("./isolated-vm-
|
|
10
|
+
const { IsolatedVMExecutor: IsolatedVMExecutor2 } = await import("./isolated-vm-7REUQAB5.js");
|
|
14
11
|
return new IsolatedVMExecutor2(options);
|
|
15
12
|
} catch {
|
|
16
13
|
}
|
|
17
|
-
try {
|
|
18
|
-
await import("quickjs-emscripten");
|
|
19
|
-
const { QuickJSExecutor: QuickJSExecutor2 } = await import("./quickjs-BGVQS2YE.js");
|
|
20
|
-
return new QuickJSExecutor2(options);
|
|
21
|
-
} catch {
|
|
22
|
-
}
|
|
23
14
|
throw new Error(
|
|
24
|
-
"No sandbox runtime found. Install
|
|
15
|
+
"No sandbox runtime found. Install isolated-vm:\n npm install isolated-vm # V8 isolates (Node.js)"
|
|
25
16
|
);
|
|
26
17
|
}
|
|
27
18
|
|
|
28
19
|
// src/request-bridge.ts
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
var ALLOWED_METHODS = /* @__PURE__ */ new Set([
|
|
21
|
+
"GET",
|
|
22
|
+
"POST",
|
|
23
|
+
"PUT",
|
|
24
|
+
"PATCH",
|
|
25
|
+
"DELETE",
|
|
26
|
+
"HEAD",
|
|
27
|
+
"OPTIONS"
|
|
28
|
+
]);
|
|
29
|
+
var BLOCKED_HEADER_PATTERNS = [
|
|
30
|
+
/^authorization$/i,
|
|
31
|
+
/^cookie$/i,
|
|
32
|
+
/^host$/i,
|
|
33
|
+
/^origin$/i,
|
|
34
|
+
/^referer$/i,
|
|
35
|
+
/^x-forwarded-/i,
|
|
36
|
+
/^x-real-ip$/i,
|
|
37
|
+
/^x-client-ip$/i,
|
|
38
|
+
/^cf-connecting-ip$/i,
|
|
39
|
+
/^true-client-ip$/i,
|
|
40
|
+
/^proxy-/i,
|
|
41
|
+
/^transfer-encoding$/i,
|
|
42
|
+
/^connection$/i,
|
|
43
|
+
/^upgrade$/i,
|
|
44
|
+
/^te$/i
|
|
45
|
+
];
|
|
46
|
+
var DEFAULT_MAX_REQUESTS = 50;
|
|
47
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
48
|
+
async function readResponseWithLimit(response, maxBytes) {
|
|
49
|
+
const reader = response.body?.getReader();
|
|
50
|
+
if (!reader) {
|
|
51
|
+
const text = await response.text();
|
|
52
|
+
if (text.length > maxBytes) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Response too large: ${text.length} bytes exceeds limit of ${maxBytes} bytes`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
const chunks = [];
|
|
60
|
+
let totalBytes = 0;
|
|
61
|
+
try {
|
|
62
|
+
for (; ; ) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
if (done) break;
|
|
65
|
+
totalBytes += value.byteLength;
|
|
66
|
+
if (totalBytes > maxBytes) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Response too large: exceeded limit of ${maxBytes} bytes`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
chunks.push(value);
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
reader.releaseLock();
|
|
75
|
+
}
|
|
76
|
+
const decoder = new TextDecoder();
|
|
77
|
+
if (chunks.length === 1) return decoder.decode(chunks[0]);
|
|
78
|
+
const combined = new Uint8Array(totalBytes);
|
|
79
|
+
let offset = 0;
|
|
80
|
+
for (const chunk of chunks) {
|
|
81
|
+
combined.set(chunk, offset);
|
|
82
|
+
offset += chunk.byteLength;
|
|
83
|
+
}
|
|
84
|
+
return decoder.decode(combined);
|
|
85
|
+
}
|
|
86
|
+
function validatePath(path) {
|
|
87
|
+
if (path.includes("://")) {
|
|
88
|
+
throw new Error(`Invalid path: must not contain "://" \u2014 got "${path}"`);
|
|
89
|
+
}
|
|
90
|
+
if (!path.startsWith("/")) {
|
|
91
|
+
throw new Error(`Invalid path: must start with "/" \u2014 got "${path}"`);
|
|
92
|
+
}
|
|
93
|
+
if (path.startsWith("//")) {
|
|
94
|
+
throw new Error(`Invalid path: must not start with "//" \u2014 got "${path}"`);
|
|
95
|
+
}
|
|
96
|
+
if (path.includes("\0")) {
|
|
97
|
+
throw new Error("Invalid path: must not contain null bytes");
|
|
98
|
+
}
|
|
99
|
+
if (/[\r\n]/.test(path)) {
|
|
100
|
+
throw new Error("Invalid path: must not contain CR/LF characters");
|
|
101
|
+
}
|
|
102
|
+
if (path.includes("\\")) {
|
|
103
|
+
throw new Error("Invalid path: must not contain backslashes");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function filterHeaders(headers, allowedHeaders) {
|
|
107
|
+
if (!headers) return {};
|
|
108
|
+
if (allowedHeaders) {
|
|
109
|
+
const allowed = new Set(allowedHeaders.map((h) => h.toLowerCase()));
|
|
110
|
+
const filtered2 = {};
|
|
111
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
112
|
+
if (allowed.has(key.toLowerCase())) {
|
|
113
|
+
filtered2[key] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return filtered2;
|
|
117
|
+
}
|
|
118
|
+
const filtered = {};
|
|
119
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
120
|
+
const blocked = BLOCKED_HEADER_PATTERNS.some((p) => p.test(key));
|
|
121
|
+
if (!blocked) {
|
|
122
|
+
filtered[key] = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return filtered;
|
|
126
|
+
}
|
|
127
|
+
function createRequestBridge(handler, baseUrl, options = {}) {
|
|
128
|
+
const maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
|
|
129
|
+
const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
130
|
+
const allowedHeaders = options.allowedHeaders;
|
|
131
|
+
let requestCount = 0;
|
|
132
|
+
return async (opts) => {
|
|
133
|
+
const { method, path, query, body, headers } = opts;
|
|
134
|
+
if (++requestCount > maxRequests) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Request limit exceeded: max ${maxRequests} requests per execution`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const upperMethod = method.toUpperCase();
|
|
140
|
+
if (!ALLOWED_METHODS.has(upperMethod)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Invalid HTTP method: "${method}". Allowed: ${[...ALLOWED_METHODS].join(", ")}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
validatePath(path);
|
|
32
146
|
const url = new URL(path, baseUrl);
|
|
33
147
|
if (query) {
|
|
34
148
|
for (const [key, value] of Object.entries(query)) {
|
|
35
149
|
url.searchParams.set(key, String(value));
|
|
36
150
|
}
|
|
37
151
|
}
|
|
152
|
+
const filteredHeaders = filterHeaders(headers, allowedHeaders);
|
|
38
153
|
const init = {
|
|
39
|
-
method:
|
|
40
|
-
headers: {
|
|
41
|
-
...headers
|
|
42
|
-
}
|
|
154
|
+
method: upperMethod,
|
|
155
|
+
headers: { ...filteredHeaders }
|
|
43
156
|
};
|
|
44
157
|
if (body !== void 0 && body !== null) {
|
|
45
158
|
init.body = JSON.stringify(body);
|
|
@@ -51,15 +164,16 @@ function createRequestBridge(handler, baseUrl) {
|
|
|
51
164
|
responseHeaders[key] = value;
|
|
52
165
|
});
|
|
53
166
|
const contentType = response.headers.get("content-type") ?? "";
|
|
167
|
+
const text = await readResponseWithLimit(response, maxResponseBytes);
|
|
54
168
|
let responseBody;
|
|
55
169
|
if (contentType.includes("application/json")) {
|
|
56
170
|
try {
|
|
57
|
-
responseBody =
|
|
171
|
+
responseBody = JSON.parse(text);
|
|
58
172
|
} catch {
|
|
59
|
-
responseBody =
|
|
173
|
+
responseBody = text;
|
|
60
174
|
}
|
|
61
175
|
} else {
|
|
62
|
-
responseBody =
|
|
176
|
+
responseBody = text;
|
|
63
177
|
}
|
|
64
178
|
return {
|
|
65
179
|
status: response.status,
|
|
@@ -69,46 +183,205 @@ function createRequestBridge(handler, baseUrl) {
|
|
|
69
183
|
};
|
|
70
184
|
}
|
|
71
185
|
|
|
186
|
+
// src/spec.ts
|
|
187
|
+
var HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
|
|
188
|
+
var DEFAULT_MAX_REF_DEPTH = 50;
|
|
189
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
190
|
+
function resolveRefs(obj, root, seen = /* @__PURE__ */ new Set(), maxDepth = DEFAULT_MAX_REF_DEPTH, _cache = /* @__PURE__ */ new Map()) {
|
|
191
|
+
if (obj === null || obj === void 0) return obj;
|
|
192
|
+
if (typeof obj !== "object") return obj;
|
|
193
|
+
if (Array.isArray(obj))
|
|
194
|
+
return obj.map((item) => resolveRefs(item, root, seen, maxDepth, _cache));
|
|
195
|
+
const record = obj;
|
|
196
|
+
if ("$ref" in record && typeof record.$ref === "string") {
|
|
197
|
+
const ref = record.$ref;
|
|
198
|
+
if (seen.has(ref)) return { $circular: ref };
|
|
199
|
+
if (seen.size >= maxDepth) {
|
|
200
|
+
return { $circular: ref, $reason: "max depth exceeded" };
|
|
201
|
+
}
|
|
202
|
+
if (_cache.has(ref)) return _cache.get(ref);
|
|
203
|
+
const parts = ref.replace("#/", "").split("/");
|
|
204
|
+
let resolved = root;
|
|
205
|
+
for (const part of parts) {
|
|
206
|
+
if (DANGEROUS_KEYS.has(part)) return { $ref: ref, $error: "unsafe ref path" };
|
|
207
|
+
resolved = resolved?.[part];
|
|
208
|
+
}
|
|
209
|
+
const branchSeen = new Set(seen);
|
|
210
|
+
branchSeen.add(ref);
|
|
211
|
+
const result2 = resolveRefs(resolved, root, branchSeen, maxDepth, _cache);
|
|
212
|
+
_cache.set(ref, result2);
|
|
213
|
+
return result2;
|
|
214
|
+
}
|
|
215
|
+
const result = {};
|
|
216
|
+
for (const [key, value] of Object.entries(record)) {
|
|
217
|
+
if (DANGEROUS_KEYS.has(key)) continue;
|
|
218
|
+
result[key] = resolveRefs(value, root, seen, maxDepth, _cache);
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
function extractServerBasePath(spec) {
|
|
223
|
+
const servers = spec.servers;
|
|
224
|
+
if (!servers?.length) return "";
|
|
225
|
+
const url = servers[0].url;
|
|
226
|
+
try {
|
|
227
|
+
const parsed = new URL(url);
|
|
228
|
+
return parsed.pathname.replace(/\/+$/, "");
|
|
229
|
+
} catch {
|
|
230
|
+
return url.replace(/\/+$/, "");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function processSpec(spec, maxRefDepth = DEFAULT_MAX_REF_DEPTH) {
|
|
234
|
+
const rawPaths = spec.paths ?? {};
|
|
235
|
+
const basePath = extractServerBasePath(spec);
|
|
236
|
+
const paths = {};
|
|
237
|
+
for (const [path, pathItem] of Object.entries(rawPaths)) {
|
|
238
|
+
if (!pathItem) continue;
|
|
239
|
+
const fullPath = basePath ? basePath + path : path;
|
|
240
|
+
paths[fullPath] = {};
|
|
241
|
+
for (const method of HTTP_METHODS) {
|
|
242
|
+
const op = pathItem[method];
|
|
243
|
+
if (op) {
|
|
244
|
+
paths[fullPath][method] = {
|
|
245
|
+
summary: op.summary,
|
|
246
|
+
description: op.description,
|
|
247
|
+
tags: op.tags,
|
|
248
|
+
operationId: op.operationId,
|
|
249
|
+
parameters: resolveRefs(
|
|
250
|
+
op.parameters,
|
|
251
|
+
spec,
|
|
252
|
+
void 0,
|
|
253
|
+
maxRefDepth
|
|
254
|
+
),
|
|
255
|
+
requestBody: resolveRefs(
|
|
256
|
+
op.requestBody,
|
|
257
|
+
spec,
|
|
258
|
+
void 0,
|
|
259
|
+
maxRefDepth
|
|
260
|
+
),
|
|
261
|
+
responses: resolveRefs(
|
|
262
|
+
op.responses,
|
|
263
|
+
spec,
|
|
264
|
+
void 0,
|
|
265
|
+
maxRefDepth
|
|
266
|
+
)
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const result = { paths };
|
|
272
|
+
if (spec.info) result.info = spec.info;
|
|
273
|
+
if (spec.components) {
|
|
274
|
+
result.components = resolveRefs(
|
|
275
|
+
spec.components,
|
|
276
|
+
spec,
|
|
277
|
+
void 0,
|
|
278
|
+
maxRefDepth
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
function extractTags(spec) {
|
|
284
|
+
const rawPaths = spec.paths;
|
|
285
|
+
if (!rawPaths) return [];
|
|
286
|
+
const tags = /* @__PURE__ */ new Map();
|
|
287
|
+
for (const pathItem of Object.values(rawPaths)) {
|
|
288
|
+
if (!pathItem) continue;
|
|
289
|
+
for (const method of HTTP_METHODS) {
|
|
290
|
+
const op = pathItem[method];
|
|
291
|
+
if (op?.tags) {
|
|
292
|
+
for (const tag of op.tags) {
|
|
293
|
+
tags.set(tag, (tags.get(tag) ?? 0) + 1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return [...tags.entries()].toSorted((a, b) => b[1] - a[1]).map(([t]) => t);
|
|
299
|
+
}
|
|
300
|
+
|
|
72
301
|
// src/tools.ts
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
302
|
+
var SPEC_TYPES = `
|
|
303
|
+
interface OperationInfo {
|
|
304
|
+
summary?: string;
|
|
305
|
+
description?: string;
|
|
306
|
+
tags?: string[];
|
|
307
|
+
operationId?: string;
|
|
308
|
+
parameters?: Array<{ name: string; in: string; required?: boolean; schema?: unknown; description?: string }>;
|
|
309
|
+
requestBody?: { required?: boolean; content?: Record<string, { schema?: unknown }> };
|
|
310
|
+
responses?: Record<string, { description?: string; content?: Record<string, { schema?: unknown }> }>;
|
|
311
|
+
}
|
|
77
312
|
|
|
78
|
-
|
|
313
|
+
interface PathItem {
|
|
314
|
+
get?: OperationInfo;
|
|
315
|
+
post?: OperationInfo;
|
|
316
|
+
put?: OperationInfo;
|
|
317
|
+
patch?: OperationInfo;
|
|
318
|
+
delete?: OperationInfo;
|
|
319
|
+
}
|
|
79
320
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
321
|
+
declare const spec: {
|
|
322
|
+
paths: Record<string, PathItem>;
|
|
323
|
+
components?: { schemas?: Record<string, unknown> };
|
|
324
|
+
info?: { title?: string; version?: string; description?: string };
|
|
325
|
+
};
|
|
326
|
+
`;
|
|
327
|
+
function createSearchToolDefinition(toolName, context) {
|
|
328
|
+
const parts = [];
|
|
329
|
+
parts.push(
|
|
330
|
+
`Search the API specification to discover available endpoints. All $refs are pre-resolved inline.`
|
|
331
|
+
);
|
|
332
|
+
if (context?.tags && context.tags.length > 0) {
|
|
333
|
+
const shown = context.tags.slice(0, 30).join(", ");
|
|
334
|
+
const suffix = context.tags.length > 30 ? `... (${context.tags.length} total)` : "";
|
|
335
|
+
parts.push(`Tags: ${shown}${suffix}`);
|
|
336
|
+
}
|
|
337
|
+
if (context?.endpointCount) {
|
|
338
|
+
parts.push(`Endpoints: ${context.endpointCount}`);
|
|
339
|
+
}
|
|
340
|
+
parts.push(`Types:
|
|
341
|
+
${SPEC_TYPES}`);
|
|
342
|
+
const exampleTag = context?.tags?.[0]?.toLowerCase() ?? "items";
|
|
343
|
+
parts.push(`Your code must be an async arrow function that returns the result.
|
|
85
344
|
|
|
86
345
|
Examples:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
method: method.toUpperCase(), path, summary: op.summary
|
|
96
|
-
}))
|
|
97
|
-
);
|
|
346
|
+
|
|
347
|
+
// List all endpoints
|
|
348
|
+
async () => {
|
|
349
|
+
const results = [];
|
|
350
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
351
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
352
|
+
results.push({ method: method.toUpperCase(), path, summary: op.summary });
|
|
353
|
+
}
|
|
98
354
|
}
|
|
355
|
+
return results;
|
|
356
|
+
}
|
|
99
357
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
358
|
+
// Find endpoints by tag
|
|
359
|
+
async () => {
|
|
360
|
+
const results = [];
|
|
361
|
+
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
362
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
363
|
+
if (op.tags?.some(t => t.toLowerCase() === '${exampleTag}')) {
|
|
364
|
+
results.push({ method: method.toUpperCase(), path, summary: op.summary });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
103
367
|
}
|
|
368
|
+
return results;
|
|
369
|
+
}
|
|
104
370
|
|
|
105
|
-
|
|
371
|
+
// Get full details for a specific endpoint (refs are already resolved)
|
|
372
|
+
async () => {
|
|
373
|
+
const op = spec.paths['/example']?.get;
|
|
374
|
+
return { summary: op?.summary, parameters: op?.parameters, requestBody: op?.requestBody };
|
|
375
|
+
}`);
|
|
376
|
+
return {
|
|
377
|
+
name: toolName,
|
|
378
|
+
description: parts.join("\n\n"),
|
|
106
379
|
inputSchema: {
|
|
107
380
|
type: "object",
|
|
108
381
|
properties: {
|
|
109
382
|
code: {
|
|
110
383
|
type: "string",
|
|
111
|
-
description: "
|
|
384
|
+
description: "JavaScript async arrow function to search the `spec` object"
|
|
112
385
|
}
|
|
113
386
|
},
|
|
114
387
|
required: ["code"]
|
|
@@ -116,55 +389,67 @@ Return the matching endpoints/schemas as a structured result the agent can use t
|
|
|
116
389
|
};
|
|
117
390
|
}
|
|
118
391
|
function createExecuteToolDefinition(toolName, namespace) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
392
|
+
const types = `
|
|
393
|
+
interface RequestOptions {
|
|
394
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
395
|
+
path: string;
|
|
396
|
+
query?: Record<string, string | number | boolean>;
|
|
397
|
+
body?: unknown;
|
|
398
|
+
headers?: Record<string, string>;
|
|
399
|
+
}
|
|
122
400
|
|
|
123
|
-
|
|
401
|
+
interface Response<T = unknown> {
|
|
402
|
+
status: number;
|
|
403
|
+
headers: Record<string, string>;
|
|
404
|
+
body: T;
|
|
405
|
+
}
|
|
124
406
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
407
|
+
declare const ${namespace}: {
|
|
408
|
+
request<T = unknown>(options: RequestOptions): Promise<Response<T>>;
|
|
409
|
+
};
|
|
410
|
+
`;
|
|
411
|
+
return {
|
|
412
|
+
name: toolName,
|
|
413
|
+
description: `Execute API calls by writing JavaScript code. First use the 'search' tool to find the right endpoints.
|
|
131
414
|
|
|
132
|
-
|
|
415
|
+
Available in your code:
|
|
416
|
+
${types}
|
|
417
|
+
Your code must be an async arrow function that returns the result.
|
|
133
418
|
|
|
134
419
|
Examples:
|
|
135
|
-
// List resources
|
|
136
|
-
async () => {
|
|
137
|
-
const res = await ${namespace}.request({ method: "GET", path: "/v1/clusters" });
|
|
138
|
-
return res.body;
|
|
139
|
-
}
|
|
140
420
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
body: { name: "My Product", chart: "nginx" }
|
|
147
|
-
});
|
|
148
|
-
}
|
|
421
|
+
// List resources
|
|
422
|
+
async () => {
|
|
423
|
+
const res = await ${namespace}.request({ method: "GET", path: "/v1/items" });
|
|
424
|
+
return res.body;
|
|
425
|
+
}
|
|
149
426
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
427
|
+
// Create a resource
|
|
428
|
+
async () => {
|
|
429
|
+
const res = await ${namespace}.request({
|
|
430
|
+
method: "POST",
|
|
431
|
+
path: "/v1/items",
|
|
432
|
+
body: { name: "Widget" }
|
|
433
|
+
});
|
|
434
|
+
return { status: res.status, body: res.body };
|
|
435
|
+
}
|
|
160
436
|
|
|
161
|
-
|
|
437
|
+
// Chain multiple calls
|
|
438
|
+
async () => {
|
|
439
|
+
const list = await ${namespace}.request({ method: "GET", path: "/v1/items" });
|
|
440
|
+
const details = await Promise.all(
|
|
441
|
+
list.body.map(item =>
|
|
442
|
+
${namespace}.request({ method: "GET", path: \`/v1/items/\${item.id}\` })
|
|
443
|
+
)
|
|
444
|
+
);
|
|
445
|
+
return details.map(d => d.body);
|
|
446
|
+
}`,
|
|
162
447
|
inputSchema: {
|
|
163
448
|
type: "object",
|
|
164
449
|
properties: {
|
|
165
450
|
code: {
|
|
166
451
|
type: "string",
|
|
167
|
-
description: `
|
|
452
|
+
description: `JavaScript async arrow function that uses \`${namespace}.request()\` to make API calls`
|
|
168
453
|
}
|
|
169
454
|
},
|
|
170
455
|
required: ["code"]
|
|
@@ -172,16 +457,83 @@ Write clean, focused code. Return the data the user needs.`,
|
|
|
172
457
|
};
|
|
173
458
|
}
|
|
174
459
|
|
|
460
|
+
// src/truncate.ts
|
|
461
|
+
var CHARS_PER_TOKEN = 4;
|
|
462
|
+
var DEFAULT_MAX_TOKENS = 25e3;
|
|
463
|
+
function truncateResponse(content, maxTokens = DEFAULT_MAX_TOKENS) {
|
|
464
|
+
const text = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
465
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN;
|
|
466
|
+
if (text.length <= maxChars) {
|
|
467
|
+
return text;
|
|
468
|
+
}
|
|
469
|
+
const truncated = text.slice(0, maxChars);
|
|
470
|
+
const estimatedTokens = Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
471
|
+
return `${truncated}
|
|
472
|
+
|
|
473
|
+
--- TRUNCATED ---
|
|
474
|
+
Response was ~${estimatedTokens.toLocaleString()} tokens (limit: ${maxTokens.toLocaleString()}). Use more specific queries to reduce response size.`;
|
|
475
|
+
}
|
|
476
|
+
|
|
175
477
|
// src/codemode.ts
|
|
478
|
+
var RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
479
|
+
"Object",
|
|
480
|
+
"Array",
|
|
481
|
+
"Promise",
|
|
482
|
+
"Function",
|
|
483
|
+
"String",
|
|
484
|
+
"Number",
|
|
485
|
+
"Boolean",
|
|
486
|
+
"Symbol",
|
|
487
|
+
"Map",
|
|
488
|
+
"Set",
|
|
489
|
+
"WeakMap",
|
|
490
|
+
"WeakSet",
|
|
491
|
+
"Date",
|
|
492
|
+
"RegExp",
|
|
493
|
+
"Error",
|
|
494
|
+
"JSON",
|
|
495
|
+
"Math",
|
|
496
|
+
"Proxy",
|
|
497
|
+
"Reflect",
|
|
498
|
+
"globalThis",
|
|
499
|
+
"undefined",
|
|
500
|
+
"null",
|
|
501
|
+
"NaN",
|
|
502
|
+
"Infinity",
|
|
503
|
+
"console",
|
|
504
|
+
"spec",
|
|
505
|
+
"global"
|
|
506
|
+
]);
|
|
507
|
+
var VALID_JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
508
|
+
function validateNamespace(namespace) {
|
|
509
|
+
if (!VALID_JS_IDENTIFIER.test(namespace)) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
`Invalid namespace "${namespace}": must be a valid JavaScript identifier`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (RESERVED_NAMES.has(namespace)) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`Invalid namespace "${namespace}": conflicts with reserved name`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
176
520
|
var CodeMode = class {
|
|
177
521
|
specProvider;
|
|
178
|
-
requestBridge;
|
|
179
522
|
namespace;
|
|
180
523
|
executor;
|
|
181
524
|
executorPromise = null;
|
|
182
525
|
options;
|
|
183
526
|
searchToolName;
|
|
184
527
|
executeToolName;
|
|
528
|
+
maxResponseTokens;
|
|
529
|
+
// Bridge config — a fresh bridge is created per execute() call
|
|
530
|
+
// so the request counter resets each time.
|
|
531
|
+
bridgeHandler;
|
|
532
|
+
bridgeBaseUrl;
|
|
533
|
+
bridgeOptions;
|
|
534
|
+
// Cached processed spec & context for tool descriptions
|
|
535
|
+
processedSpec = null;
|
|
536
|
+
specContext = null;
|
|
185
537
|
constructor(options) {
|
|
186
538
|
this.options = options;
|
|
187
539
|
this.specProvider = options.spec;
|
|
@@ -189,9 +541,15 @@ var CodeMode = class {
|
|
|
189
541
|
this.executor = options.executor ?? null;
|
|
190
542
|
this.searchToolName = "search";
|
|
191
543
|
this.executeToolName = "execute";
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.
|
|
544
|
+
this.maxResponseTokens = options.maxResponseTokens ?? 25e3;
|
|
545
|
+
validateNamespace(this.namespace);
|
|
546
|
+
this.bridgeHandler = options.request;
|
|
547
|
+
this.bridgeBaseUrl = options.baseUrl ?? "http://localhost";
|
|
548
|
+
this.bridgeOptions = {
|
|
549
|
+
maxRequests: options.maxRequests,
|
|
550
|
+
maxResponseBytes: options.maxResponseBytes,
|
|
551
|
+
allowedHeaders: options.allowedHeaders
|
|
552
|
+
};
|
|
195
553
|
}
|
|
196
554
|
/**
|
|
197
555
|
* Override the default tool names.
|
|
@@ -206,7 +564,7 @@ var CodeMode = class {
|
|
|
206
564
|
*/
|
|
207
565
|
tools() {
|
|
208
566
|
return [
|
|
209
|
-
createSearchToolDefinition(this.searchToolName),
|
|
567
|
+
createSearchToolDefinition(this.searchToolName, this.specContext ?? void 0),
|
|
210
568
|
createExecuteToolDefinition(this.executeToolName, this.namespace)
|
|
211
569
|
];
|
|
212
570
|
}
|
|
@@ -228,12 +586,13 @@ var CodeMode = class {
|
|
|
228
586
|
/**
|
|
229
587
|
* Execute a search against the OpenAPI spec.
|
|
230
588
|
* The code runs in a sandbox with `spec` available as a global.
|
|
589
|
+
* All $refs are pre-resolved inline.
|
|
231
590
|
*/
|
|
232
591
|
async search(code) {
|
|
233
592
|
const executor = await this.getExecutor();
|
|
234
|
-
const spec = await this.
|
|
593
|
+
const spec = await this.getProcessedSpec();
|
|
235
594
|
const result = await executor.execute(code, { spec });
|
|
236
|
-
return formatResult(result);
|
|
595
|
+
return this.formatResult(result);
|
|
237
596
|
}
|
|
238
597
|
/**
|
|
239
598
|
* Execute API calls in the sandbox.
|
|
@@ -241,13 +600,18 @@ var CodeMode = class {
|
|
|
241
600
|
*/
|
|
242
601
|
async execute(code) {
|
|
243
602
|
const executor = await this.getExecutor();
|
|
603
|
+
const bridge = createRequestBridge(
|
|
604
|
+
this.bridgeHandler,
|
|
605
|
+
this.bridgeBaseUrl,
|
|
606
|
+
this.bridgeOptions
|
|
607
|
+
);
|
|
244
608
|
const client = {
|
|
245
|
-
request:
|
|
609
|
+
request: (...args) => bridge(args[0])
|
|
246
610
|
};
|
|
247
611
|
const result = await executor.execute(code, {
|
|
248
612
|
[this.namespace]: client
|
|
249
613
|
});
|
|
250
|
-
return formatResult(result);
|
|
614
|
+
return this.formatResult(result);
|
|
251
615
|
}
|
|
252
616
|
/**
|
|
253
617
|
* Clean up sandbox resources.
|
|
@@ -261,6 +625,19 @@ var CodeMode = class {
|
|
|
261
625
|
}
|
|
262
626
|
return this.specProvider;
|
|
263
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* Get the processed spec (refs resolved, fields extracted).
|
|
630
|
+
* Caches the result after first call.
|
|
631
|
+
*/
|
|
632
|
+
async getProcessedSpec() {
|
|
633
|
+
if (this.processedSpec) return this.processedSpec;
|
|
634
|
+
const raw = await this.resolveSpec();
|
|
635
|
+
this.processedSpec = processSpec(raw, this.options.maxRefDepth);
|
|
636
|
+
const tags = extractTags(raw);
|
|
637
|
+
const endpointCount = Object.keys(raw.paths ?? {}).length;
|
|
638
|
+
this.specContext = { tags, endpointCount };
|
|
639
|
+
return this.processedSpec;
|
|
640
|
+
}
|
|
264
641
|
async getExecutor() {
|
|
265
642
|
if (this.executor) return this.executor;
|
|
266
643
|
if (!this.executorPromise) {
|
|
@@ -273,36 +650,28 @@ var CodeMode = class {
|
|
|
273
650
|
}
|
|
274
651
|
return this.executorPromise;
|
|
275
652
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
${result.logs.join("\n")}`);
|
|
653
|
+
formatResult(result) {
|
|
654
|
+
if (result.error) {
|
|
655
|
+
return {
|
|
656
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
657
|
+
isError: true
|
|
658
|
+
};
|
|
283
659
|
}
|
|
284
|
-
|
|
660
|
+
const resultText = typeof result.result === "string" ? result.result : JSON.stringify(result.result, null, 2);
|
|
285
661
|
return {
|
|
286
|
-
content: [{ type: "text", text:
|
|
287
|
-
isError: true
|
|
662
|
+
content: [{ type: "text", text: truncateResponse(resultText, this.maxResponseTokens) }]
|
|
288
663
|
};
|
|
289
664
|
}
|
|
290
|
-
|
|
291
|
-
if (result.logs.length > 0) {
|
|
292
|
-
parts.push(`Console output:
|
|
293
|
-
${result.logs.join("\n")}`);
|
|
294
|
-
}
|
|
295
|
-
const resultText = typeof result.result === "string" ? result.result : JSON.stringify(result.result, null, 2);
|
|
296
|
-
parts.push(resultText);
|
|
297
|
-
return {
|
|
298
|
-
content: [{ type: "text", text: parts.join("\n\n") }]
|
|
299
|
-
};
|
|
300
|
-
}
|
|
665
|
+
};
|
|
301
666
|
export {
|
|
302
667
|
CodeMode,
|
|
303
668
|
IsolatedVMExecutor,
|
|
304
|
-
QuickJSExecutor,
|
|
305
669
|
createExecutor,
|
|
306
|
-
createRequestBridge
|
|
670
|
+
createRequestBridge,
|
|
671
|
+
extractServerBasePath,
|
|
672
|
+
extractTags,
|
|
673
|
+
processSpec,
|
|
674
|
+
resolveRefs,
|
|
675
|
+
truncateResponse
|
|
307
676
|
};
|
|
308
677
|
//# sourceMappingURL=index.js.map
|