@papicandela/mcx-core 0.2.6 → 0.2.8

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 CHANGED
@@ -6488,13 +6488,165 @@ 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
+
6540
+ /**
6541
+ * Get lines around a specific line number (1-indexed).
6542
+ * @param file - File object with .lines array
6543
+ * @param line - Line number (1-indexed)
6544
+ * @param ctx - Context lines before/after (default 10)
6545
+ */
6546
+ globalThis.around = (file, line, ctx = 10) => {
6547
+ const lines = file?.lines;
6548
+ if (!Array.isArray(lines)) return [];
6549
+ if (line < 1 || line > lines.length) return [];
6550
+ const idx = line - 1;
6551
+ return lines.slice(Math.max(0, idx - ctx), Math.min(lines.length, idx + ctx + 1));
6552
+ };
6553
+
6554
+ /**
6555
+ * Extract code block containing a line (detects by indentation).
6556
+ * @param file - File object with .lines array
6557
+ * @param line - Line number (1-indexed)
6558
+ */
6559
+ globalThis.block = (file, line) => {
6560
+ const lines = file?.lines;
6561
+ if (!Array.isArray(lines)) return [];
6562
+ const idx = line - 1;
6563
+ if (idx < 0 || idx >= lines.length) return [];
6564
+
6565
+ const targetIndent = lines[idx].search(/\\S/);
6566
+ if (targetIndent < 0) return [lines[idx]];
6567
+
6568
+ // Find block start
6569
+ let start = idx;
6570
+ for (let i = idx - 1; i >= 0; i--) {
6571
+ const lineIndent = lines[i].search(/\\S/);
6572
+ if (lineIndent >= 0 && lineIndent < targetIndent) { start = i; break; }
6573
+ if (lineIndent === 0 && lines[i].trim()) { start = i; break; }
6574
+ }
6575
+
6576
+ // Find block end
6577
+ const startIndent = lines[start].search(/\\S/);
6578
+ let end = idx;
6579
+ for (let i = idx + 1; i < lines.length; i++) {
6580
+ const lineIndent = lines[i].search(/\\S/);
6581
+ if (lineIndent >= 0 && lineIndent <= startIndent && lines[i].trim()) { end = i - 1; break; }
6582
+ end = i;
6583
+ }
6584
+
6585
+ return lines.slice(start, end + 1);
6586
+ };
6587
+
6588
+ /**
6589
+ * Grep with context lines.
6590
+ * @param file - File object with .lines array
6591
+ * @param pattern - String or regex pattern
6592
+ * @param ctx - Context lines (default 3)
6593
+ */
6594
+ globalThis.grep = (file, pattern, ctx = 3) => {
6595
+ const lines = file?.lines;
6596
+ if (!Array.isArray(lines)) return [];
6597
+ const regex = typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern;
6598
+ const results = [];
6599
+
6600
+ for (let i = 0; i < lines.length; i++) {
6601
+ if (regex.test(lines[i])) {
6602
+ results.push({
6603
+ line: i + 1,
6604
+ match: lines[i],
6605
+ context: lines.slice(Math.max(0, i - ctx), i + ctx + 1)
6606
+ });
6607
+ }
6608
+ }
6609
+ return results;
6610
+ };
6611
+
6612
+ /**
6613
+ * Extract outline (function/class signatures).
6614
+ * @param file - File object with .lines array
6615
+ */
6616
+ globalThis.outline = (file) => {
6617
+ const lines = file?.lines;
6618
+ if (!Array.isArray(lines)) return [];
6619
+ const signatures = [];
6620
+ const patterns = [
6621
+ /^(export\\s+)?(async\\s+)?function\\s+\\w+/,
6622
+ /^(export\\s+)?(const|let|var)\\s+\\w+\\s*=\\s*(async\\s+)?\\(/,
6623
+ /^(export\\s+)?(const|let|var)\\s+\\w+\\s*=\\s*(async\\s+)?function/,
6624
+ /^(export\\s+)?class\\s+\\w+/,
6625
+ /^(export\\s+)?interface\\s+\\w+/,
6626
+ /^(export\\s+)?type\\s+\\w+/,
6627
+ /^\\s+(async\\s+)?\\w+\\s*\\([^)]*\\)\\s*[:{]/,
6628
+ ];
6629
+
6630
+ for (let i = 0; i < lines.length; i++) {
6631
+ const line = lines[i];
6632
+ for (const pat of patterns) {
6633
+ if (pat.test(line)) {
6634
+ signatures.push((i + 1) + ': ' + line.trim().slice(0, 80));
6635
+ break;
6636
+ }
6637
+ }
6638
+ }
6639
+ return signatures;
6640
+ };
6641
+
6491
6642
  // SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
6492
6643
  const RESERVED_KEYS = new Set([
6493
6644
  'onmessage', 'postMessage', 'close', 'terminate', 'self',
6494
6645
  'constructor', 'prototype', '__proto__',
6495
6646
  'pendingCalls', 'callId', 'logs', 'console', 'adapters',
6496
6647
  'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
6497
- 'pick', 'table', 'count', 'sum', 'first', 'safeStr'
6648
+ 'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor',
6649
+ 'around', 'block', 'grep', 'outline'
6498
6650
  ]);
6499
6651
 
6500
6652
  self.onmessage = async (event) => {
@@ -6538,6 +6690,12 @@ class BunWorkerSandbox {
6538
6690
  return matrix[b.length][a.length];
6539
6691
  };
6540
6692
 
6693
+ // Convert camelCase to snake_case
6694
+ const toSnakeCase = (str) => str
6695
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
6696
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
6697
+ .toLowerCase();
6698
+
6541
6699
  // Find similar method names
6542
6700
  const findSimilar = (name, methods, maxDist = 3) => {
6543
6701
  const normalized = name.toLowerCase().replace(/[-_]/g, '');
@@ -6549,6 +6707,22 @@ class BunWorkerSandbox {
6549
6707
  .map(x => x.method);
6550
6708
  };
6551
6709
 
6710
+ // Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
6711
+ const findAutoCorrect = (name, methods) => {
6712
+ // Try exact snake_case conversion first
6713
+ const snake = toSnakeCase(name);
6714
+ if (methods.includes(snake)) return snake;
6715
+
6716
+ // Try normalized fuzzy match
6717
+ const normalized = name.toLowerCase().replace(/[-_]/g, '');
6718
+ const matches = methods
6719
+ .map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
6720
+ .filter(x => x.dist <= 2) // Only auto-correct if very close
6721
+ .sort((a, b) => a.dist - b.dist);
6722
+
6723
+ return matches.length > 0 ? matches[0].method : null;
6724
+ };
6725
+
6552
6726
  // Create adapter proxies with helpful error messages
6553
6727
  const adaptersObj = {};
6554
6728
  for (const [adapterName, methods] of Object.entries(adapterMethods)) {
@@ -6569,16 +6743,45 @@ class BunWorkerSandbox {
6569
6743
  };
6570
6744
  }
6571
6745
 
6572
- // Use Proxy to intercept undefined method calls
6746
+ // Precompute auto-correct lookup (O(1) instead of O(n) per access)
6747
+ const autoCorrectMap = new Map();
6748
+ for (const m of methods) {
6749
+ // Map snake_case variants: execute_sql -> execute_sql (identity)
6750
+ autoCorrectMap.set(m, m);
6751
+ // Map normalized: executesql -> execute_sql
6752
+ autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
6753
+ }
6754
+
6755
+ // Use Proxy to intercept undefined method calls with auto-correction
6573
6756
  const adapterProxy = new Proxy(methodsImpl, {
6574
6757
  get(target, prop) {
6575
6758
  if (prop in target) return target[prop];
6576
6759
  if (typeof prop === 'symbol') return undefined;
6577
- const similar = findSimilar(String(prop), methods);
6760
+
6761
+ const propStr = String(prop);
6762
+
6763
+ // Fast path: check precomputed map (O(1))
6764
+ const snake = toSnakeCase(propStr);
6765
+ if (autoCorrectMap.has(snake)) {
6766
+ return target[autoCorrectMap.get(snake)];
6767
+ }
6768
+ const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
6769
+ if (autoCorrectMap.has(normalized)) {
6770
+ return target[autoCorrectMap.get(normalized)];
6771
+ }
6772
+
6773
+ // Slow path: fuzzy match for typos (only if fast path failed)
6774
+ const corrected = findAutoCorrect(propStr, methods);
6775
+ if (corrected && corrected in target) {
6776
+ return target[corrected];
6777
+ }
6778
+
6779
+ // No auto-correct possible, throw with suggestions
6780
+ const similar = findSimilar(propStr, methods);
6578
6781
  const suggestion = similar.length > 0
6579
6782
  ? '. Did you mean: ' + similar.join(', ') + '?'
6580
6783
  : '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
6581
- throw new Error(adapterName + '.' + String(prop) + ' is not a function' + suggestion);
6784
+ throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
6582
6785
  }
6583
6786
  });
6584
6787
 
@@ -6615,7 +6818,23 @@ class BunWorkerSandbox {
6615
6818
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
6616
6819
  const fn = new AsyncFunction(data.code);
6617
6820
  const result = await fn();
6618
- self.postMessage({ type: 'result', success: true, value: result, logs });
6821
+
6822
+ // SECURITY: Hard cap on output size to prevent OOM
6823
+ const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
6824
+ const serialized = safeStr(result);
6825
+ if (serialized.length > HARD_CAP_BYTES) {
6826
+ self.postMessage({
6827
+ type: 'result',
6828
+ success: false,
6829
+ error: {
6830
+ name: 'OutputSizeError',
6831
+ message: 'Output exceeds 100MB limit. Use pagination or filtering.'
6832
+ },
6833
+ logs
6834
+ });
6835
+ } else {
6836
+ self.postMessage({ type: 'result', success: true, value: result, logs });
6837
+ }
6619
6838
  } catch (err) {
6620
6839
  // Truncate stack to 5 lines to prevent context bloat
6621
6840
  const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papicandela/mcx-core",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "MCX Core - MCP Code Execution Framework",
5
5
  "author": "papicandela",
6
6
  "license": "MIT",
@@ -285,13 +285,165 @@ 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
+
337
+ /**
338
+ * Get lines around a specific line number (1-indexed).
339
+ * @param file - File object with .lines array
340
+ * @param line - Line number (1-indexed)
341
+ * @param ctx - Context lines before/after (default 10)
342
+ */
343
+ globalThis.around = (file, line, ctx = 10) => {
344
+ const lines = file?.lines;
345
+ if (!Array.isArray(lines)) return [];
346
+ if (line < 1 || line > lines.length) return [];
347
+ const idx = line - 1;
348
+ return lines.slice(Math.max(0, idx - ctx), Math.min(lines.length, idx + ctx + 1));
349
+ };
350
+
351
+ /**
352
+ * Extract code block containing a line (detects by indentation).
353
+ * @param file - File object with .lines array
354
+ * @param line - Line number (1-indexed)
355
+ */
356
+ globalThis.block = (file, line) => {
357
+ const lines = file?.lines;
358
+ if (!Array.isArray(lines)) return [];
359
+ const idx = line - 1;
360
+ if (idx < 0 || idx >= lines.length) return [];
361
+
362
+ const targetIndent = lines[idx].search(/\\S/);
363
+ if (targetIndent < 0) return [lines[idx]];
364
+
365
+ // Find block start
366
+ let start = idx;
367
+ for (let i = idx - 1; i >= 0; i--) {
368
+ const lineIndent = lines[i].search(/\\S/);
369
+ if (lineIndent >= 0 && lineIndent < targetIndent) { start = i; break; }
370
+ if (lineIndent === 0 && lines[i].trim()) { start = i; break; }
371
+ }
372
+
373
+ // Find block end
374
+ const startIndent = lines[start].search(/\\S/);
375
+ let end = idx;
376
+ for (let i = idx + 1; i < lines.length; i++) {
377
+ const lineIndent = lines[i].search(/\\S/);
378
+ if (lineIndent >= 0 && lineIndent <= startIndent && lines[i].trim()) { end = i - 1; break; }
379
+ end = i;
380
+ }
381
+
382
+ return lines.slice(start, end + 1);
383
+ };
384
+
385
+ /**
386
+ * Grep with context lines.
387
+ * @param file - File object with .lines array
388
+ * @param pattern - String or regex pattern
389
+ * @param ctx - Context lines (default 3)
390
+ */
391
+ globalThis.grep = (file, pattern, ctx = 3) => {
392
+ const lines = file?.lines;
393
+ if (!Array.isArray(lines)) return [];
394
+ const regex = typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern;
395
+ const results = [];
396
+
397
+ for (let i = 0; i < lines.length; i++) {
398
+ if (regex.test(lines[i])) {
399
+ results.push({
400
+ line: i + 1,
401
+ match: lines[i],
402
+ context: lines.slice(Math.max(0, i - ctx), i + ctx + 1)
403
+ });
404
+ }
405
+ }
406
+ return results;
407
+ };
408
+
409
+ /**
410
+ * Extract outline (function/class signatures).
411
+ * @param file - File object with .lines array
412
+ */
413
+ globalThis.outline = (file) => {
414
+ const lines = file?.lines;
415
+ if (!Array.isArray(lines)) return [];
416
+ const signatures = [];
417
+ const patterns = [
418
+ /^(export\\s+)?(async\\s+)?function\\s+\\w+/,
419
+ /^(export\\s+)?(const|let|var)\\s+\\w+\\s*=\\s*(async\\s+)?\\(/,
420
+ /^(export\\s+)?(const|let|var)\\s+\\w+\\s*=\\s*(async\\s+)?function/,
421
+ /^(export\\s+)?class\\s+\\w+/,
422
+ /^(export\\s+)?interface\\s+\\w+/,
423
+ /^(export\\s+)?type\\s+\\w+/,
424
+ /^\\s+(async\\s+)?\\w+\\s*\\([^)]*\\)\\s*[:{]/,
425
+ ];
426
+
427
+ for (let i = 0; i < lines.length; i++) {
428
+ const line = lines[i];
429
+ for (const pat of patterns) {
430
+ if (pat.test(line)) {
431
+ signatures.push((i + 1) + ': ' + line.trim().slice(0, 80));
432
+ break;
433
+ }
434
+ }
435
+ }
436
+ return signatures;
437
+ };
438
+
288
439
  // SECURITY: Reserved keys that must not be overwritten by user-provided variables/globals
289
440
  const RESERVED_KEYS = new Set([
290
441
  'onmessage', 'postMessage', 'close', 'terminate', 'self',
291
442
  'constructor', 'prototype', '__proto__',
292
443
  'pendingCalls', 'callId', 'logs', 'console', 'adapters',
293
444
  'fetch', 'XMLHttpRequest', 'WebSocket', 'EventSource',
294
- 'pick', 'table', 'count', 'sum', 'first', 'safeStr'
445
+ 'pick', 'table', 'count', 'sum', 'first', 'safeStr', 'poll', 'waitFor',
446
+ 'around', 'block', 'grep', 'outline'
295
447
  ]);
296
448
 
297
449
  self.onmessage = async (event) => {
@@ -335,6 +487,12 @@ export class BunWorkerSandbox implements ISandbox {
335
487
  return matrix[b.length][a.length];
336
488
  };
337
489
 
490
+ // Convert camelCase to snake_case
491
+ const toSnakeCase = (str) => str
492
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
493
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
494
+ .toLowerCase();
495
+
338
496
  // Find similar method names
339
497
  const findSimilar = (name, methods, maxDist = 3) => {
340
498
  const normalized = name.toLowerCase().replace(/[-_]/g, '');
@@ -346,6 +504,22 @@ export class BunWorkerSandbox implements ISandbox {
346
504
  .map(x => x.method);
347
505
  };
348
506
 
507
+ // Find best auto-correctable match (distance ≤ 2 = safe to auto-correct)
508
+ const findAutoCorrect = (name, methods) => {
509
+ // Try exact snake_case conversion first
510
+ const snake = toSnakeCase(name);
511
+ if (methods.includes(snake)) return snake;
512
+
513
+ // Try normalized fuzzy match
514
+ const normalized = name.toLowerCase().replace(/[-_]/g, '');
515
+ const matches = methods
516
+ .map(m => ({ method: m, dist: levenshtein(normalized, m.toLowerCase().replace(/[-_]/g, '')) }))
517
+ .filter(x => x.dist <= 2) // Only auto-correct if very close
518
+ .sort((a, b) => a.dist - b.dist);
519
+
520
+ return matches.length > 0 ? matches[0].method : null;
521
+ };
522
+
349
523
  // Create adapter proxies with helpful error messages
350
524
  const adaptersObj = {};
351
525
  for (const [adapterName, methods] of Object.entries(adapterMethods)) {
@@ -366,16 +540,45 @@ export class BunWorkerSandbox implements ISandbox {
366
540
  };
367
541
  }
368
542
 
369
- // Use Proxy to intercept undefined method calls
543
+ // Precompute auto-correct lookup (O(1) instead of O(n) per access)
544
+ const autoCorrectMap = new Map();
545
+ for (const m of methods) {
546
+ // Map snake_case variants: execute_sql -> execute_sql (identity)
547
+ autoCorrectMap.set(m, m);
548
+ // Map normalized: executesql -> execute_sql
549
+ autoCorrectMap.set(m.toLowerCase().replace(/[-_]/g, ''), m);
550
+ }
551
+
552
+ // Use Proxy to intercept undefined method calls with auto-correction
370
553
  const adapterProxy = new Proxy(methodsImpl, {
371
554
  get(target, prop) {
372
555
  if (prop in target) return target[prop];
373
556
  if (typeof prop === 'symbol') return undefined;
374
- const similar = findSimilar(String(prop), methods);
557
+
558
+ const propStr = String(prop);
559
+
560
+ // Fast path: check precomputed map (O(1))
561
+ const snake = toSnakeCase(propStr);
562
+ if (autoCorrectMap.has(snake)) {
563
+ return target[autoCorrectMap.get(snake)];
564
+ }
565
+ const normalized = propStr.toLowerCase().replace(/[-_]/g, '');
566
+ if (autoCorrectMap.has(normalized)) {
567
+ return target[autoCorrectMap.get(normalized)];
568
+ }
569
+
570
+ // Slow path: fuzzy match for typos (only if fast path failed)
571
+ const corrected = findAutoCorrect(propStr, methods);
572
+ if (corrected && corrected in target) {
573
+ return target[corrected];
574
+ }
575
+
576
+ // No auto-correct possible, throw with suggestions
577
+ const similar = findSimilar(propStr, methods);
375
578
  const suggestion = similar.length > 0
376
579
  ? '. Did you mean: ' + similar.join(', ') + '?'
377
580
  : '. Available: ' + methods.slice(0, 5).join(', ') + (methods.length > 5 ? '...' : '');
378
- throw new Error(adapterName + '.' + String(prop) + ' is not a function' + suggestion);
581
+ throw new Error(adapterName + '.' + propStr + ' is not a function' + suggestion);
379
582
  }
380
583
  });
381
584
 
@@ -412,7 +615,23 @@ export class BunWorkerSandbox implements ISandbox {
412
615
  const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
413
616
  const fn = new AsyncFunction(data.code);
414
617
  const result = await fn();
415
- self.postMessage({ type: 'result', success: true, value: result, logs });
618
+
619
+ // SECURITY: Hard cap on output size to prevent OOM
620
+ const HARD_CAP_BYTES = 100 * 1024 * 1024; // 100MB
621
+ const serialized = safeStr(result);
622
+ if (serialized.length > HARD_CAP_BYTES) {
623
+ self.postMessage({
624
+ type: 'result',
625
+ success: false,
626
+ error: {
627
+ name: 'OutputSizeError',
628
+ message: 'Output exceeds 100MB limit. Use pagination or filtering.'
629
+ },
630
+ logs
631
+ });
632
+ } else {
633
+ self.postMessage({ type: 'result', success: true, value: result, logs });
634
+ }
416
635
  } catch (err) {
417
636
  // Truncate stack to 5 lines to prevent context bloat
418
637
  const stack = err.stack ? err.stack.split('\\n').slice(0, 5).join('\\n') : undefined;