@papicandela/mcx-core 0.2.5 → 0.2.7
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/dist/index.js +213 -16
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/sandbox/bun-worker.ts +165 -11
- package/src/type-generator.ts +66 -9
- package/src/types.ts +4 -0
package/dist/index.js
CHANGED
|
@@ -6488,13 +6488,62 @@ class BunWorkerSandbox {
|
|
|
6488
6488
|
return arr.slice(0, n);
|
|
6489
6489
|
};
|
|
6490
6490
|
|
|
6491
|
+
/**
|
|
6492
|
+
* Poll a function until condition is met or max iterations reached.
|
|
6493
|
+
* @param fn - Async function to call. Return { done: true, value } to stop.
|
|
6494
|
+
* @param opts - { interval: ms (default 1000), maxIterations: n (default 10) }
|
|
6495
|
+
* @returns Array of all results, or last result if done:true was returned
|
|
6496
|
+
*/
|
|
6497
|
+
globalThis.poll = async (fn, opts = {}) => {
|
|
6498
|
+
const interval = opts.interval ?? 1000;
|
|
6499
|
+
const maxIterations = opts.maxIterations ?? 10;
|
|
6500
|
+
const results = [];
|
|
6501
|
+
|
|
6502
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
6503
|
+
const result = await fn(i);
|
|
6504
|
+
results.push(result);
|
|
6505
|
+
|
|
6506
|
+
// Check for done signal
|
|
6507
|
+
if (result && typeof result === 'object' && result.done) {
|
|
6508
|
+
return result.value !== undefined ? result.value : results;
|
|
6509
|
+
}
|
|
6510
|
+
|
|
6511
|
+
// Wait before next iteration (skip on last)
|
|
6512
|
+
if (i < maxIterations - 1) {
|
|
6513
|
+
await new Promise(r => setTimeout(r, interval));
|
|
6514
|
+
}
|
|
6515
|
+
}
|
|
6516
|
+
|
|
6517
|
+
return results;
|
|
6518
|
+
};
|
|
6519
|
+
|
|
6520
|
+
/**
|
|
6521
|
+
* Wait for a condition to become true.
|
|
6522
|
+
* @param fn - Async function that returns truthy when done
|
|
6523
|
+
* @param opts - { interval: ms (default 500), timeout: ms (default 10000) }
|
|
6524
|
+
* @returns The truthy value returned by fn
|
|
6525
|
+
*/
|
|
6526
|
+
globalThis.waitFor = async (fn, opts = {}) => {
|
|
6527
|
+
const interval = opts.interval ?? 500;
|
|
6528
|
+
const timeout = opts.timeout ?? 10000;
|
|
6529
|
+
const start = Date.now();
|
|
6530
|
+
|
|
6531
|
+
while (Date.now() - start < timeout) {
|
|
6532
|
+
const result = await fn();
|
|
6533
|
+
if (result) return result;
|
|
6534
|
+
await new Promise(r => setTimeout(r, interval));
|
|
6535
|
+
}
|
|
6536
|
+
|
|
6537
|
+
throw new Error('waitFor timeout after ' + timeout + 'ms');
|
|
6538
|
+
};
|
|
6539
|
+
|
|
6491
6540
|
// SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
|
|
6492
6541
|
const RESERVED_KEYS = new Set([
|
|
6493
6542
|
'onmessage', 'postMessage', 'close', 'terminate', 'self',
|
|
6494
6543
|
'constructor', 'prototype', '__proto__',
|
|
6495
6544
|
'pendingCalls', 'callId', 'logs', 'console', 'adapters',
|
|
6496
6545
|
'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
|
|
6497
|
-
'pick', 'table', 'count', 'sum', 'first', 'safeStr'
|
|
6546
|
+
'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor'
|
|
6498
6547
|
]);
|
|
6499
6548
|
|
|
6500
6549
|
self.onmessage = async (event) => {
|
|
@@ -6521,12 +6570,62 @@ class BunWorkerSandbox {
|
|
|
6521
6570
|
globalThis[key] = value;
|
|
6522
6571
|
}
|
|
6523
6572
|
|
|
6524
|
-
//
|
|
6573
|
+
// Levenshtein distance for fuzzy matching
|
|
6574
|
+
const levenshtein = (a, b) => {
|
|
6575
|
+
if (a.length === 0) return b.length;
|
|
6576
|
+
if (b.length === 0) return a.length;
|
|
6577
|
+
const matrix = [];
|
|
6578
|
+
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
|
6579
|
+
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
6580
|
+
for (let i = 1; i <= b.length; i++) {
|
|
6581
|
+
for (let j = 1; j <= a.length; j++) {
|
|
6582
|
+
matrix[i][j] = b[i-1] === a[j-1]
|
|
6583
|
+
? matrix[i-1][j-1]
|
|
6584
|
+
: Math.min(matrix[i-1][j-1] + 1, matrix[i][j-1] + 1, matrix[i-1][j] + 1);
|
|
6585
|
+
}
|
|
6586
|
+
}
|
|
6587
|
+
return matrix[b.length][a.length];
|
|
6588
|
+
};
|
|
6589
|
+
|
|
6590
|
+
// Convert camelCase to snake_case
|
|
6591
|
+
const toSnakeCase = (str) => str
|
|
6592
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
6593
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
6594
|
+
.toLowerCase();
|
|
6595
|
+
|
|
6596
|
+
// Find similar method names
|
|
6597
|
+
const findSimilar = (name, methods, maxDist = 3) => {
|
|
6598
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
6599
|
+
return methods
|
|
6600
|
+
.map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
|
|
6601
|
+
.filter(x => x.dist <= maxDist)
|
|
6602
|
+
.sort((a, b) => a.dist - b.dist)
|
|
6603
|
+
.slice(0, 3)
|
|
6604
|
+
.map(x => x.method);
|
|
6605
|
+
};
|
|
6606
|
+
|
|
6607
|
+
// Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
|
|
6608
|
+
const findAutoCorrect = (name, methods) => {
|
|
6609
|
+
// Try exact snake_case conversion first
|
|
6610
|
+
const snake = toSnakeCase(name);
|
|
6611
|
+
if (methods.includes(snake)) return snake;
|
|
6612
|
+
|
|
6613
|
+
// Try normalized fuzzy match
|
|
6614
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
6615
|
+
const matches = methods
|
|
6616
|
+
.map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
|
|
6617
|
+
.filter(x => x.dist <= 2) // Only auto-correct if very close
|
|
6618
|
+
.sort((a, b) => a.dist - b.dist);
|
|
6619
|
+
|
|
6620
|
+
return matches.length > 0 ? matches[0].method : null;
|
|
6621
|
+
};
|
|
6622
|
+
|
|
6623
|
+
// Create adapter proxies with helpful error messages
|
|
6525
6624
|
const adaptersObj = {};
|
|
6526
6625
|
for (const [adapterName, methods] of Object.entries(adapterMethods)) {
|
|
6527
|
-
const
|
|
6626
|
+
const methodsImpl = {};
|
|
6528
6627
|
for (const methodName of methods) {
|
|
6529
|
-
|
|
6628
|
+
methodsImpl[methodName] = async (...args) => {
|
|
6530
6629
|
const id = ++callId;
|
|
6531
6630
|
return new Promise((resolve, reject) => {
|
|
6532
6631
|
pendingCalls.set(id, { resolve, reject });
|
|
@@ -6540,18 +6639,57 @@ class BunWorkerSandbox {
|
|
|
6540
6639
|
});
|
|
6541
6640
|
};
|
|
6542
6641
|
}
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6642
|
+
|
|
6643
|
+
// Precompute auto-correct lookup (O(1) instead of O(n) per access)
|
|
6644
|
+
const autoCorrectMap = new Map();
|
|
6645
|
+
for (const m of methods) {
|
|
6646
|
+
// Map snake_case variants: execute_sql -> execute_sql (identity)
|
|
6647
|
+
autoCorrectMap.set(m, m);
|
|
6648
|
+
// Map normalized: executesql -> execute_sql
|
|
6649
|
+
autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
|
|
6650
|
+
}
|
|
6651
|
+
|
|
6652
|
+
// Use Proxy to intercept undefined method calls with auto-correction
|
|
6653
|
+
const adapterProxy = new Proxy(methodsImpl, {
|
|
6654
|
+
get(target, prop) {
|
|
6655
|
+
if (prop in target) return target[prop];
|
|
6656
|
+
if (typeof prop === 'symbol') return undefined;
|
|
6657
|
+
|
|
6658
|
+
const propStr = String(prop);
|
|
6659
|
+
|
|
6660
|
+
// Fast path: check precomputed map (O(1))
|
|
6661
|
+
const snake = toSnakeCase(propStr);
|
|
6662
|
+
if (autoCorrectMap.has(snake)) {
|
|
6663
|
+
return target[autoCorrectMap.get(snake)];
|
|
6664
|
+
}
|
|
6665
|
+
const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
|
|
6666
|
+
if (autoCorrectMap.has(normalized)) {
|
|
6667
|
+
return target[autoCorrectMap.get(normalized)];
|
|
6668
|
+
}
|
|
6669
|
+
|
|
6670
|
+
// Slow path: fuzzy match for typos (only if fast path failed)
|
|
6671
|
+
const corrected = findAutoCorrect(propStr, methods);
|
|
6672
|
+
if (corrected && corrected in target) {
|
|
6673
|
+
return target[corrected];
|
|
6674
|
+
}
|
|
6675
|
+
|
|
6676
|
+
// No auto-correct possible, throw with suggestions
|
|
6677
|
+
const similar = findSimilar(propStr, methods);
|
|
6678
|
+
const suggestion = similar.length > 0
|
|
6679
|
+
? '. Did you mean: ' + similar.join(', ') + '?'
|
|
6680
|
+
: '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
|
|
6681
|
+
throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
|
|
6682
|
+
}
|
|
6683
|
+
});
|
|
6684
|
+
|
|
6685
|
+
adaptersObj[adapterName] = adapterProxy;
|
|
6546
6686
|
// Also expose at top level but as non-writable
|
|
6547
6687
|
Object.defineProperty(globalThis, adapterName, {
|
|
6548
|
-
value:
|
|
6688
|
+
value: adapterProxy,
|
|
6549
6689
|
writable: false,
|
|
6550
6690
|
configurable: false
|
|
6551
6691
|
});
|
|
6552
6692
|
}
|
|
6553
|
-
// Freeze the adapters namespace and make it non-writable
|
|
6554
|
-
Object.freeze(adaptersObj);
|
|
6555
6693
|
Object.defineProperty(globalThis, 'adapters', {
|
|
6556
6694
|
value: adaptersObj,
|
|
6557
6695
|
writable: false,
|
|
@@ -6577,7 +6715,23 @@ class BunWorkerSandbox {
|
|
|
6577
6715
|
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
6578
6716
|
const fn = new AsyncFunction(data.code);
|
|
6579
6717
|
const result = await fn();
|
|
6580
|
-
|
|
6718
|
+
|
|
6719
|
+
// SECURITY: Hard cap on output size to prevent OOM
|
|
6720
|
+
const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
|
|
6721
|
+
const serialized = safeStr(result);
|
|
6722
|
+
if (serialized.length > HARD_CAP_BYTES) {
|
|
6723
|
+
self.postMessage({
|
|
6724
|
+
type: 'result',
|
|
6725
|
+
success: false,
|
|
6726
|
+
error: {
|
|
6727
|
+
name: 'OutputSizeError',
|
|
6728
|
+
message: 'Output exceeds 100MB limit. Use pagination or filtering.'
|
|
6729
|
+
},
|
|
6730
|
+
logs
|
|
6731
|
+
});
|
|
6732
|
+
} else {
|
|
6733
|
+
self.postMessage({ type: 'result', success: true, value: result, logs });
|
|
6734
|
+
}
|
|
6581
6735
|
} catch (err) {
|
|
6582
6736
|
// Truncate stack to 5 lines to prevent context bloat
|
|
6583
6737
|
const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;
|
|
@@ -10867,12 +11021,54 @@ function generateTypes(adapters, options = {}) {
|
|
|
10867
11021
|
`).trim();
|
|
10868
11022
|
}
|
|
10869
11023
|
function generateTypesSummary(adapters) {
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
11024
|
+
const byDomain = new Map;
|
|
11025
|
+
for (const adapter of adapters) {
|
|
11026
|
+
const domain = inferDomain(adapter);
|
|
11027
|
+
if (!byDomain.has(domain)) {
|
|
11028
|
+
byDomain.set(domain, []);
|
|
11029
|
+
}
|
|
11030
|
+
byDomain.get(domain).push(adapter);
|
|
11031
|
+
}
|
|
11032
|
+
if (byDomain.size <= 1 || adapters.length < 4) {
|
|
11033
|
+
return adapters.map((adapter) => {
|
|
11034
|
+
const count = Object.keys(adapter.tools).length;
|
|
11035
|
+
return `- ${adapter.name} (${count} methods)`;
|
|
11036
|
+
}).join(`
|
|
11037
|
+
`);
|
|
11038
|
+
}
|
|
11039
|
+
const lines = [];
|
|
11040
|
+
for (const [domain, domainAdapters] of byDomain) {
|
|
11041
|
+
const adapterList = domainAdapters.map((a) => `${a.name}(${Object.keys(a.tools).length})`).join(", ");
|
|
11042
|
+
lines.push(`[${domain}] ${adapterList}`);
|
|
11043
|
+
}
|
|
11044
|
+
return lines.join(`
|
|
10874
11045
|
`);
|
|
10875
11046
|
}
|
|
11047
|
+
function inferDomain(adapter) {
|
|
11048
|
+
if (adapter.domain)
|
|
11049
|
+
return adapter.domain;
|
|
11050
|
+
const name = adapter.name.toLowerCase();
|
|
11051
|
+
const desc = (adapter.description || "").toLowerCase();
|
|
11052
|
+
const combined = `${name} ${desc}`;
|
|
11053
|
+
const domains = {
|
|
11054
|
+
payments: ["stripe", "paypal", "square", "payment", "checkout", "billing", "invoice"],
|
|
11055
|
+
database: ["supabase", "postgres", "mysql", "mongodb", "redis", "database", "sql", "query"],
|
|
11056
|
+
email: ["sendgrid", "mailgun", "postmark", "email", "smtp", "mail"],
|
|
11057
|
+
storage: ["s3", "cloudflare", "storage", "blob", "file", "upload"],
|
|
11058
|
+
auth: ["auth", "oauth", "login", "jwt", "clerk", "auth0"],
|
|
11059
|
+
ai: ["openai", "anthropic", "claude", "gpt", "llm", "ai", "ml"],
|
|
11060
|
+
messaging: ["slack", "discord", "telegram", "twilio", "sms", "chat"],
|
|
11061
|
+
crm: ["hubspot", "salesforce", "crm", "customer"],
|
|
11062
|
+
analytics: ["analytics", "metrics", "tracking", "mixpanel", "amplitude"],
|
|
11063
|
+
devtools: ["github", "gitlab", "jira", "linear", "chrome", "devtools", "ci", "cd"]
|
|
11064
|
+
};
|
|
11065
|
+
for (const [domain, keywords2] of Object.entries(domains)) {
|
|
11066
|
+
if (keywords2.some((k) => combined.includes(k))) {
|
|
11067
|
+
return domain;
|
|
11068
|
+
}
|
|
11069
|
+
}
|
|
11070
|
+
return "general";
|
|
11071
|
+
}
|
|
10876
11072
|
function generateInputInterface(typeName, parameters, includeDescriptions) {
|
|
10877
11073
|
const lines = [`interface ${typeName} {`];
|
|
10878
11074
|
for (const [paramName, param] of Object.entries(parameters)) {
|
|
@@ -10881,7 +11077,7 @@ function generateInputInterface(typeName, parameters, includeDescriptions) {
|
|
|
10881
11077
|
lines.push(` /** ${sanitizeJSDoc(param.description)} */`);
|
|
10882
11078
|
}
|
|
10883
11079
|
const tsType = paramTypeToTS(param.type);
|
|
10884
|
-
const optional = param.required ===
|
|
11080
|
+
const optional = param.required === true ? "" : "?";
|
|
10885
11081
|
lines.push(` ${safeParamName}${optional}: ${tsType};`);
|
|
10886
11082
|
}
|
|
10887
11083
|
lines.push("}");
|
|
@@ -11170,6 +11366,7 @@ export {
|
|
|
11170
11366
|
normalizeCode,
|
|
11171
11367
|
mergeConfigs,
|
|
11172
11368
|
isUrlAllowed,
|
|
11369
|
+
inferDomain,
|
|
11173
11370
|
generateTypesSummary,
|
|
11174
11371
|
generateTypes,
|
|
11175
11372
|
formatFindings,
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -285,13 +285,62 @@ export class BunWorkerSandbox implements ISandbox {
|
|
|
285
285
|
return arr.slice(0, n);
|
|
286
286
|
};
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Poll a function until condition is met or max iterations reached.
|
|
290
|
+
* @param fn - Async function to call. Return { done: true, value } to stop.
|
|
291
|
+
* @param opts - { interval: ms (default 1000), maxIterations: n (default 10) }
|
|
292
|
+
* @returns Array of all results, or last result if done:true was returned
|
|
293
|
+
*/
|
|
294
|
+
globalThis.poll = async (fn, opts = {}) => {
|
|
295
|
+
const interval = opts.interval ?? 1000;
|
|
296
|
+
const maxIterations = opts.maxIterations ?? 10;
|
|
297
|
+
const results = [];
|
|
298
|
+
|
|
299
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
300
|
+
const result = await fn(i);
|
|
301
|
+
results.push(result);
|
|
302
|
+
|
|
303
|
+
// Check for done signal
|
|
304
|
+
if (result && typeof result === 'object' && result.done) {
|
|
305
|
+
return result.value !== undefined ? result.value : results;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Wait before next iteration (skip on last)
|
|
309
|
+
if (i < maxIterations - 1) {
|
|
310
|
+
await new Promise(r => setTimeout(r, interval));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return results;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Wait for a condition to become true.
|
|
319
|
+
* @param fn - Async function that returns truthy when done
|
|
320
|
+
* @param opts - { interval: ms (default 500), timeout: ms (default 10000) }
|
|
321
|
+
* @returns The truthy value returned by fn
|
|
322
|
+
*/
|
|
323
|
+
globalThis.waitFor = async (fn, opts = {}) => {
|
|
324
|
+
const interval = opts.interval ?? 500;
|
|
325
|
+
const timeout = opts.timeout ?? 10000;
|
|
326
|
+
const start = Date.now();
|
|
327
|
+
|
|
328
|
+
while (Date.now() - start < timeout) {
|
|
329
|
+
const result = await fn();
|
|
330
|
+
if (result) return result;
|
|
331
|
+
await new Promise(r => setTimeout(r, interval));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
throw new Error('waitFor timeout after ' + timeout + 'ms');
|
|
335
|
+
};
|
|
336
|
+
|
|
288
337
|
// SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
|
|
289
338
|
const RESERVED_KEYS = new Set([
|
|
290
339
|
'onmessage', 'postMessage', 'close', 'terminate', 'self',
|
|
291
340
|
'constructor', 'prototype', '__proto__',
|
|
292
341
|
'pendingCalls', 'callId', 'logs', 'console', 'adapters',
|
|
293
342
|
'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
|
|
294
|
-
'pick', 'table', 'count', 'sum', 'first', 'safeStr'
|
|
343
|
+
'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor'
|
|
295
344
|
]);
|
|
296
345
|
|
|
297
346
|
self.onmessage = async (event) => {
|
|
@@ -318,12 +367,62 @@ export class BunWorkerSandbox implements ISandbox {
|
|
|
318
367
|
globalThis[key] = value;
|
|
319
368
|
}
|
|
320
369
|
|
|
321
|
-
//
|
|
370
|
+
// Levenshtein distance for fuzzy matching
|
|
371
|
+
const levenshtein = (a, b) => {
|
|
372
|
+
if (a.length === 0) return b.length;
|
|
373
|
+
if (b.length === 0) return a.length;
|
|
374
|
+
const matrix = [];
|
|
375
|
+
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
|
376
|
+
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
377
|
+
for (let i = 1; i <= b.length; i++) {
|
|
378
|
+
for (let j = 1; j <= a.length; j++) {
|
|
379
|
+
matrix[i][j] = b[i-1] === a[j-1]
|
|
380
|
+
? matrix[i-1][j-1]
|
|
381
|
+
: Math.min(matrix[i-1][j-1] + 1, matrix[i][j-1] + 1, matrix[i-1][j] + 1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return matrix[b.length][a.length];
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Convert camelCase to snake_case
|
|
388
|
+
const toSnakeCase = (str) => str
|
|
389
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
390
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
391
|
+
.toLowerCase();
|
|
392
|
+
|
|
393
|
+
// Find similar method names
|
|
394
|
+
const findSimilar = (name, methods, maxDist = 3) => {
|
|
395
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
396
|
+
return methods
|
|
397
|
+
.map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
|
|
398
|
+
.filter(x => x.dist <= maxDist)
|
|
399
|
+
.sort((a, b) => a.dist - b.dist)
|
|
400
|
+
.slice(0, 3)
|
|
401
|
+
.map(x => x.method);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
|
|
405
|
+
const findAutoCorrect = (name, methods) => {
|
|
406
|
+
// Try exact snake_case conversion first
|
|
407
|
+
const snake = toSnakeCase(name);
|
|
408
|
+
if (methods.includes(snake)) return snake;
|
|
409
|
+
|
|
410
|
+
// Try normalized fuzzy match
|
|
411
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
412
|
+
const matches = methods
|
|
413
|
+
.map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
|
|
414
|
+
.filter(x => x.dist <= 2) // Only auto-correct if very close
|
|
415
|
+
.sort((a, b) => a.dist - b.dist);
|
|
416
|
+
|
|
417
|
+
return matches.length > 0 ? matches[0].method : null;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Create adapter proxies with helpful error messages
|
|
322
421
|
const adaptersObj = {};
|
|
323
422
|
for (const [adapterName, methods] of Object.entries(adapterMethods)) {
|
|
324
|
-
const
|
|
423
|
+
const methodsImpl = {};
|
|
325
424
|
for (const methodName of methods) {
|
|
326
|
-
|
|
425
|
+
methodsImpl[methodName] = async (...args) => {
|
|
327
426
|
const id = ++callId;
|
|
328
427
|
return new Promise((resolve, reject) => {
|
|
329
428
|
pendingCalls.set(id, { resolve, reject });
|
|
@@ -337,18 +436,57 @@ export class BunWorkerSandbox implements ISandbox {
|
|
|
337
436
|
});
|
|
338
437
|
};
|
|
339
438
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
439
|
+
|
|
440
|
+
// Precompute auto-correct lookup (O(1) instead of O(n) per access)
|
|
441
|
+
const autoCorrectMap = new Map();
|
|
442
|
+
for (const m of methods) {
|
|
443
|
+
// Map snake_case variants: execute_sql -> execute_sql (identity)
|
|
444
|
+
autoCorrectMap.set(m, m);
|
|
445
|
+
// Map normalized: executesql -> execute_sql
|
|
446
|
+
autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Use Proxy to intercept undefined method calls with auto-correction
|
|
450
|
+
const adapterProxy = new Proxy(methodsImpl, {
|
|
451
|
+
get(target, prop) {
|
|
452
|
+
if (prop in target) return target[prop];
|
|
453
|
+
if (typeof prop === 'symbol') return undefined;
|
|
454
|
+
|
|
455
|
+
const propStr = String(prop);
|
|
456
|
+
|
|
457
|
+
// Fast path: check precomputed map (O(1))
|
|
458
|
+
const snake = toSnakeCase(propStr);
|
|
459
|
+
if (autoCorrectMap.has(snake)) {
|
|
460
|
+
return target[autoCorrectMap.get(snake)];
|
|
461
|
+
}
|
|
462
|
+
const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
|
|
463
|
+
if (autoCorrectMap.has(normalized)) {
|
|
464
|
+
return target[autoCorrectMap.get(normalized)];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Slow path: fuzzy match for typos (only if fast path failed)
|
|
468
|
+
const corrected = findAutoCorrect(propStr, methods);
|
|
469
|
+
if (corrected && corrected in target) {
|
|
470
|
+
return target[corrected];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// No auto-correct possible, throw with suggestions
|
|
474
|
+
const similar = findSimilar(propStr, methods);
|
|
475
|
+
const suggestion = similar.length > 0
|
|
476
|
+
? '. Did you mean: ' + similar.join(', ') + '?'
|
|
477
|
+
: '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
|
|
478
|
+
throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
adaptersObj[adapterName] = adapterProxy;
|
|
343
483
|
// Also expose at top level but as non-writable
|
|
344
484
|
Object.defineProperty(globalThis, adapterName, {
|
|
345
|
-
value:
|
|
485
|
+
value: adapterProxy,
|
|
346
486
|
writable: false,
|
|
347
487
|
configurable: false
|
|
348
488
|
});
|
|
349
489
|
}
|
|
350
|
-
// Freeze the adapters namespace and make it non-writable
|
|
351
|
-
Object.freeze(adaptersObj);
|
|
352
490
|
Object.defineProperty(globalThis, 'adapters', {
|
|
353
491
|
value: adaptersObj,
|
|
354
492
|
writable: false,
|
|
@@ -374,7 +512,23 @@ export class BunWorkerSandbox implements ISandbox {
|
|
|
374
512
|
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
375
513
|
const fn = new AsyncFunction(data.code);
|
|
376
514
|
const result = await fn();
|
|
377
|
-
|
|
515
|
+
|
|
516
|
+
// SECURITY: Hard cap on output size to prevent OOM
|
|
517
|
+
const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
|
|
518
|
+
const serialized = safeStr(result);
|
|
519
|
+
if (serialized.length > HARD_CAP_BYTES) {
|
|
520
|
+
self.postMessage({
|
|
521
|
+
type: 'result',
|
|
522
|
+
success: false,
|
|
523
|
+
error: {
|
|
524
|
+
name: 'OutputSizeError',
|
|
525
|
+
message: 'Output exceeds 100MB limit. Use pagination or filtering.'
|
|
526
|
+
},
|
|
527
|
+
logs
|
|
528
|
+
});
|
|
529
|
+
} else {
|
|
530
|
+
self.postMessage({ type: 'result', success: true, value: result, logs });
|
|
531
|
+
}
|
|
378
532
|
} catch (err) {
|
|
379
533
|
// Truncate stack to 5 lines to prevent context bloat
|
|
380
534
|
const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;
|
package/src/type-generator.ts
CHANGED
|
@@ -85,19 +85,76 @@ export function generateTypes(
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Generate a compact type summary for token-constrained contexts.
|
|
88
|
-
*
|
|
88
|
+
* Groups adapters by domain and shows method count.
|
|
89
89
|
* Use mcx_search to discover specific methods.
|
|
90
90
|
*
|
|
91
91
|
* @param adapters - Array of adapters
|
|
92
|
-
* @returns Compact summary string
|
|
92
|
+
* @returns Compact summary string with domain hints
|
|
93
93
|
*/
|
|
94
94
|
export function generateTypesSummary(adapters: Adapter[]): string {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.
|
|
95
|
+
// Group adapters by domain
|
|
96
|
+
const byDomain = new Map<string, Adapter[]>();
|
|
97
|
+
|
|
98
|
+
for (const adapter of adapters) {
|
|
99
|
+
const domain = inferDomain(adapter);
|
|
100
|
+
if (!byDomain.has(domain)) {
|
|
101
|
+
byDomain.set(domain, []);
|
|
102
|
+
}
|
|
103
|
+
byDomain.get(domain)!.push(adapter);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If only one domain or fewer than 4 adapters, simple list
|
|
107
|
+
if (byDomain.size <= 1 || adapters.length < 4) {
|
|
108
|
+
return adapters
|
|
109
|
+
.map((adapter) => {
|
|
110
|
+
const count = Object.keys(adapter.tools).length;
|
|
111
|
+
return `- ${adapter.name} (${count} methods)`;
|
|
112
|
+
})
|
|
113
|
+
.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Group by domain for better discoverability
|
|
117
|
+
const lines: string[] = [];
|
|
118
|
+
for (const [domain, domainAdapters] of byDomain) {
|
|
119
|
+
const adapterList = domainAdapters
|
|
120
|
+
.map((a) => `${a.name}(${Object.keys(a.tools).length})`)
|
|
121
|
+
.join(", ");
|
|
122
|
+
lines.push(`[${domain}] ${adapterList}`);
|
|
123
|
+
}
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Infer domain from adapter name/description if not explicitly set.
|
|
129
|
+
* Returns the adapter's explicit domain if set, otherwise infers from name/description.
|
|
130
|
+
*/
|
|
131
|
+
export function inferDomain(adapter: Adapter): string {
|
|
132
|
+
if (adapter.domain) return adapter.domain;
|
|
133
|
+
|
|
134
|
+
const name = adapter.name.toLowerCase();
|
|
135
|
+
const desc = (adapter.description || "").toLowerCase();
|
|
136
|
+
const combined = `${name} ${desc}`;
|
|
137
|
+
|
|
138
|
+
const domains: Record<string, string[]> = {
|
|
139
|
+
payments: ["stripe", "paypal", "square", "payment", "checkout", "billing", "invoice"],
|
|
140
|
+
database: ["supabase", "postgres", "mysql", "mongodb", "redis", "database", "sql", "query"],
|
|
141
|
+
email: ["sendgrid", "mailgun", "postmark", "email", "smtp", "mail"],
|
|
142
|
+
storage: ["s3", "cloudflare", "storage", "blob", "file", "upload"],
|
|
143
|
+
auth: ["auth", "oauth", "login", "jwt", "clerk", "auth0"],
|
|
144
|
+
ai: ["openai", "anthropic", "claude", "gpt", "llm", "ai", "ml"],
|
|
145
|
+
messaging: ["slack", "discord", "telegram", "twilio", "sms", "chat"],
|
|
146
|
+
crm: ["hubspot", "salesforce", "crm", "customer"],
|
|
147
|
+
analytics: ["analytics", "metrics", "tracking", "mixpanel", "amplitude"],
|
|
148
|
+
devtools: ["github", "gitlab", "jira", "linear", "chrome", "devtools", "ci", "cd"],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (const [domain, keywords] of Object.entries(domains)) {
|
|
152
|
+
if (keywords.some((k) => combined.includes(k))) {
|
|
153
|
+
return domain;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return "general";
|
|
101
158
|
}
|
|
102
159
|
|
|
103
160
|
/**
|
|
@@ -119,7 +176,7 @@ function generateInputInterface(
|
|
|
119
176
|
}
|
|
120
177
|
|
|
121
178
|
const tsType = paramTypeToTS(param.type);
|
|
122
|
-
const optional = param.required ===
|
|
179
|
+
const optional = param.required === true ? "" : "?";
|
|
123
180
|
lines.push(` ${safeParamName}${optional}: ${tsType};`);
|
|
124
181
|
}
|
|
125
182
|
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface ParameterDefinition {
|
|
|
12
12
|
description?: string;
|
|
13
13
|
required?: boolean;
|
|
14
14
|
default?: unknown;
|
|
15
|
+
/** Example value for this parameter (helps LLMs understand expected format) */
|
|
16
|
+
example?: unknown;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -33,6 +35,8 @@ export interface Adapter<TTools extends Record<string, AdapterTool> = Record<str
|
|
|
33
35
|
name: string;
|
|
34
36
|
description?: string;
|
|
35
37
|
version?: string;
|
|
38
|
+
/** Domain/category for tool discovery (e.g., 'payments', 'database', 'email') */
|
|
39
|
+
domain?: string;
|
|
36
40
|
tools: TTools;
|
|
37
41
|
dispose?: () => Promise<void> | void;
|
|
38
42
|
}
|