wu-framework 1.1.8 → 1.1.9

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.
@@ -271,6 +271,142 @@ class WuLoader {
271
271
  }
272
272
  }
273
273
 
274
+ /**
275
+ * 📝 WU-LOGGER: Sistema de logging inteligente para entornos
276
+ * Controla los logs automáticamente según el entorno
277
+ */
278
+
279
+ class WuLogger {
280
+ constructor() {
281
+ // Detectar entorno automáticamente
282
+ this.isDevelopment = this.detectEnvironment();
283
+ // En desarrollo: warn (menos ruido), en producción: error
284
+ this.logLevel = this.isDevelopment ? 'warn' : 'error';
285
+
286
+ this.levels = {
287
+ debug: 0,
288
+ info: 1,
289
+ warn: 2,
290
+ error: 3,
291
+ silent: 4
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Detectar si estamos en desarrollo
297
+ */
298
+ detectEnvironment() {
299
+ // Múltiples formas de detectar desarrollo
300
+ return (
301
+ // Vite development
302
+ window.location.hostname === 'localhost' ||
303
+ window.location.hostname === '127.0.0.1' ||
304
+ window.location.port !== '' ||
305
+ // NODE_ENV si está disponible
306
+ (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') ||
307
+ // URL params para forzar debug
308
+ new URLSearchParams(window.location.search).has('wu-debug') ||
309
+ // Manual override
310
+ window.WU_DEBUG === true
311
+ );
312
+ }
313
+
314
+ /**
315
+ * Configurar nivel de logging
316
+ */
317
+ setLevel(level) {
318
+ this.logLevel = level;
319
+ return this;
320
+ }
321
+
322
+ /**
323
+ * Habilitar/deshabilitar development mode
324
+ */
325
+ setDevelopment(isDev) {
326
+ this.isDevelopment = isDev;
327
+ this.logLevel = isDev ? 'debug' : 'error';
328
+ return this;
329
+ }
330
+
331
+ /**
332
+ * Verificar si debemos mostrar el log
333
+ */
334
+ shouldLog(level) {
335
+ return this.levels[level] >= this.levels[this.logLevel];
336
+ }
337
+
338
+ /**
339
+ * Logging methods
340
+ */
341
+ debug(...args) {
342
+ if (this.shouldLog('debug')) {
343
+ console.log(...args);
344
+ }
345
+ }
346
+
347
+ info(...args) {
348
+ if (this.shouldLog('info')) {
349
+ console.info(...args);
350
+ }
351
+ }
352
+
353
+ warn(...args) {
354
+ if (this.shouldLog('warn')) {
355
+ console.warn(...args);
356
+ }
357
+ }
358
+
359
+ error(...args) {
360
+ if (this.shouldLog('error')) {
361
+ console.error(...args);
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Logging con contexto Wu
367
+ */
368
+ wu(level, ...args) {
369
+ if (this.shouldLog(level)) {
370
+ const method = level === 'debug' ? 'log' : level;
371
+ console[method]('[Wu]', ...args);
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Helper methods específicos para Wu
377
+ */
378
+ wuDebug(...args) { this.wu('debug', ...args); }
379
+ wuInfo(...args) { this.wu('info', ...args); }
380
+ wuWarn(...args) { this.wu('warn', ...args); }
381
+ wuError(...args) { this.wu('error', ...args); }
382
+ }
383
+
384
+ // Singleton instance
385
+ const logger = new WuLogger();
386
+
387
+ /**
388
+ * 🔇 Silenciar todos los logs de Wu Framework
389
+ * Útil en producción para eliminar todo el ruido
390
+ */
391
+ function silenceAllLogs() {
392
+ logger.setLevel('silent');
393
+ }
394
+
395
+ /**
396
+ * 🔊 Restaurar logs (nivel debug)
397
+ */
398
+ function enableAllLogs() {
399
+ logger.setLevel('debug');
400
+ }
401
+
402
+ var wuLogger = /*#__PURE__*/Object.freeze({
403
+ __proto__: null,
404
+ WuLogger: WuLogger,
405
+ enableAllLogs: enableAllLogs,
406
+ logger: logger,
407
+ silenceAllLogs: silenceAllLogs
408
+ });
409
+
274
410
  /**
275
411
  * 🎨 WU-STYLE-BRIDGE: SHADOW DOM STYLE SHARING SYSTEM
276
412
  *
@@ -301,6 +437,7 @@ class WuLoader {
301
437
  * - Riesgo de colisiones: NINGUNO
302
438
  */
303
439
 
440
+
304
441
  class WuStyleBridge {
305
442
  constructor() {
306
443
  this.styleObserver = null;
@@ -329,7 +466,7 @@ class WuStyleBridge {
329
466
  cacheEnabled: true
330
467
  };
331
468
 
332
- console.log('[WuStyleBridge] 🎨 Style sharing system initialized');
469
+ logger.debug('[WuStyleBridge] 🎨 Style sharing system initialized');
333
470
  }
334
471
 
335
472
  /**
@@ -339,7 +476,7 @@ class WuStyleBridge {
339
476
  */
340
477
  registerFullyIsolatedApp(appName, appUrl) {
341
478
  this.fullyIsolatedApps.set(appName, appUrl);
342
- console.log(`[WuStyleBridge] 🛡️ Registered fully-isolated app: ${appName} (${appUrl})`);
479
+ logger.debug(`[WuStyleBridge] 🛡️ Registered fully-isolated app: ${appName} (${appUrl})`);
343
480
  }
344
481
 
345
482
  /**
@@ -437,7 +574,7 @@ class WuStyleBridge {
437
574
 
438
575
  // Filtrar estilos de apps con fully-isolated (después de obtener viteId para mejor detección)
439
576
  if (this.isStyleFromFullyIsolatedApp(style) || (viteId && this.isStyleFromFullyIsolatedApp(viteId))) {
440
- console.log(`[WuStyleBridge] 🛡️ Filtered out style from fully-isolated app: ${viteId || 'unknown'}`);
577
+ logger.debug(`[WuStyleBridge] 🛡️ Filtered out style from fully-isolated app: ${viteId || 'unknown'}`);
441
578
  return;
442
579
  }
443
580
 
@@ -465,7 +602,7 @@ class WuStyleBridge {
465
602
  });
466
603
  }
467
604
 
468
- console.log(`[WuStyleBridge] 🔍 Detected ${styles.length} shareable styles`);
605
+ logger.debug(`[WuStyleBridge] 🔍 Detected ${styles.length} shareable styles`);
469
606
  return styles;
470
607
  }
471
608
 
@@ -521,26 +658,26 @@ class WuStyleBridge {
521
658
  */
522
659
  async injectStylesIntoShadow(shadowRoot, appName, styleMode) {
523
660
  if (!shadowRoot) {
524
- console.warn('[WuStyleBridge] ⚠️ No shadow root provided');
661
+ logger.warn('[WuStyleBridge] ⚠️ No shadow root provided');
525
662
  return 0;
526
663
  }
527
664
 
528
665
  // 🛡️ MODO FULLY-ISOLATED: No inyectar ningún estilo compartido
529
666
  // Los estilos propios se manejan en wu-sandbox.js con injectOwnStylesToShadow
530
667
  if (styleMode === 'fully-isolated') {
531
- console.log(`[WuStyleBridge] 🛡️ Style mode "fully-isolated" for ${appName}, skipping shared style injection`);
668
+ logger.debug(`[WuStyleBridge] 🛡️ Style mode "fully-isolated" for ${appName}, skipping shared style injection`);
532
669
  return 0;
533
670
  }
534
671
 
535
672
  // 🔒 MODO ISOLATED: No inyectar estilos externos - usar encapsulamiento nativo de Shadow DOM
536
673
  // La app debe manejar sus propios estilos (CSS-in-JS, scoped styles, imports directos)
537
674
  if (styleMode === 'isolated') {
538
- console.log(`[WuStyleBridge] 🔒 Style mode "isolated" for ${appName}, using native Shadow DOM encapsulation (no external styles)`);
675
+ logger.debug(`[WuStyleBridge] 🔒 Style mode "isolated" for ${appName}, using native Shadow DOM encapsulation (no external styles)`);
539
676
  return 0;
540
677
  }
541
678
 
542
679
  // 🌐 MODO SHARED (default): Inyectar todos los estilos compartidos del documento
543
- console.log(`[WuStyleBridge] 🌐 Style mode "shared" for ${appName}, injecting all shared styles...`);
680
+ logger.debug(`[WuStyleBridge] 🌐 Style mode "shared" for ${appName}, injecting all shared styles...`);
544
681
 
545
682
  // Detectar estilos del documento
546
683
  const styles = this.detectDocumentStyles();
@@ -566,11 +703,11 @@ class WuStyleBridge {
566
703
  break;
567
704
  }
568
705
  } catch (error) {
569
- console.warn(`[WuStyleBridge] ⚠️ Failed to inject style:`, error);
706
+ logger.warn(`[WuStyleBridge] ⚠️ Failed to inject style:`, error);
570
707
  }
571
708
  }
572
709
 
573
- console.log(`[WuStyleBridge] ✅ Injected ${injectedCount} shared styles into ${appName}`);
710
+ logger.debug(`[WuStyleBridge] ✅ Injected ${injectedCount} shared styles into ${appName}`);
574
711
  return injectedCount;
575
712
  }
576
713
 
@@ -583,7 +720,7 @@ class WuStyleBridge {
583
720
  // Verificar si ya existe
584
721
  const existing = shadowRoot.querySelector(`link[href="${style.href}"]`);
585
722
  if (existing) {
586
- console.log(`[WuStyleBridge] ⏭️ Style already exists: ${style.library || style.href}`);
723
+ logger.debug(`[WuStyleBridge] ⏭️ Style already exists: ${style.library || style.href}`);
587
724
  return;
588
725
  }
589
726
 
@@ -597,7 +734,7 @@ class WuStyleBridge {
597
734
  // Insertar al principio del shadow root (antes de otros estilos)
598
735
  shadowRoot.insertBefore(link, shadowRoot.firstChild);
599
736
 
600
- console.log(`[WuStyleBridge] 🔗 Injected link: ${style.library || style.href}`);
737
+ logger.debug(`[WuStyleBridge] 🔗 Injected link: ${style.library || style.href}`);
601
738
  }
602
739
 
603
740
  /**
@@ -611,7 +748,7 @@ class WuStyleBridge {
611
748
  if (viteId) {
612
749
  const existing = shadowRoot.querySelector(`style[data-wu-vite-id="${viteId}"]`);
613
750
  if (existing) {
614
- console.log(`[WuStyleBridge] ⏭️ Inline style already exists: ${viteId}`);
751
+ logger.debug(`[WuStyleBridge] ⏭️ Inline style already exists: ${viteId}`);
615
752
  return;
616
753
  }
617
754
  }
@@ -628,7 +765,7 @@ class WuStyleBridge {
628
765
  // Insertar al principio del shadow root
629
766
  shadowRoot.insertBefore(styleTag, shadowRoot.firstChild);
630
767
 
631
- console.log(`[WuStyleBridge] 📝 Injected inline style: ${style.library || viteId}`);
768
+ logger.debug(`[WuStyleBridge] 📝 Injected inline style: ${style.library || viteId}`);
632
769
  }
633
770
 
634
771
  /**
@@ -645,7 +782,7 @@ class WuStyleBridge {
645
782
 
646
783
  // Verificar si ya existe
647
784
  if (shadowRoot.adoptedStyleSheets.includes(style.sheet)) {
648
- console.log(`[WuStyleBridge] ⏭️ Adopted stylesheet already exists`);
785
+ logger.debug(`[WuStyleBridge] ⏭️ Adopted stylesheet already exists`);
649
786
  return;
650
787
  }
651
788
 
@@ -654,9 +791,9 @@ class WuStyleBridge {
654
791
  style.sheet
655
792
  ];
656
793
 
657
- console.log(`[WuStyleBridge] 📋 Injected adopted stylesheet`);
794
+ logger.debug(`[WuStyleBridge] 📋 Injected adopted stylesheet`);
658
795
  } catch (error) {
659
- console.warn(`[WuStyleBridge] ⚠️ Failed to inject adopted stylesheet:`, error);
796
+ logger.warn(`[WuStyleBridge] ⚠️ Failed to inject adopted stylesheet:`, error);
660
797
  }
661
798
  }
662
799
 
@@ -690,7 +827,7 @@ class WuStyleBridge {
690
827
  }
691
828
 
692
829
  if (hasStyleChanges && callback) {
693
- console.log('[WuStyleBridge] 🔄 Style changes detected');
830
+ logger.debug('[WuStyleBridge] 🔄 Style changes detected');
694
831
  callback();
695
832
  }
696
833
  });
@@ -701,7 +838,7 @@ class WuStyleBridge {
701
838
  subtree: true
702
839
  });
703
840
 
704
- console.log('[WuStyleBridge] 👀 Observing style changes');
841
+ logger.debug('[WuStyleBridge] 👀 Observing style changes');
705
842
  }
706
843
 
707
844
  /**
@@ -714,7 +851,7 @@ class WuStyleBridge {
714
851
  ...config
715
852
  };
716
853
 
717
- console.log('[WuStyleBridge] ⚙️ Configuration updated:', this.config);
854
+ logger.debug('[WuStyleBridge] ⚙️ Configuration updated:', this.config);
718
855
  }
719
856
 
720
857
  /**
@@ -726,7 +863,7 @@ class WuStyleBridge {
726
863
  this.styleObserver = null;
727
864
  }
728
865
 
729
- console.log('[WuStyleBridge] 🧹 StyleBridge cleaned up');
866
+ logger.debug('[WuStyleBridge] 🧹 StyleBridge cleaned up');
730
867
  }
731
868
 
732
869
  /**
@@ -748,206 +885,70 @@ class WuStyleBridge {
748
885
  }
749
886
 
750
887
  /**
751
- * 📝 WU-LOGGER: Sistema de logging inteligente para entornos
752
- * Controla los logs automáticamente según el entorno
888
+ * WU-PROXY-SANDBOX: Hardened JavaScript Isolation
889
+ *
890
+ * ES6 Proxy-based sandbox with side-effect tracking:
891
+ * - Timer hijacking (setTimeout, setInterval, requestAnimationFrame)
892
+ * - Event listener tracking (window + document addEventListener)
893
+ * - DOM scoping (querySelector/querySelectorAll → shadow root)
894
+ * - Storage scoping (localStorage/sessionStorage → prefixed keys)
895
+ *
896
+ * All tracked side effects are automatically cleaned up on deactivate().
753
897
  */
754
898
 
755
- class WuLogger {
756
- constructor() {
757
- // Detectar entorno automáticamente
758
- this.isDevelopment = this.detectEnvironment();
759
- // En desarrollo: warn (menos ruido), en producción: error
760
- this.logLevel = this.isDevelopment ? 'warn' : 'error';
761
899
 
762
- this.levels = {
763
- debug: 0,
764
- info: 1,
765
- warn: 2,
766
- error: 3,
767
- silent: 4
768
- };
769
- }
900
+ class WuProxySandbox {
901
+ constructor(appName) {
902
+ this.appName = appName;
903
+ this.proxy = null;
904
+ this.fakeWindow = Object.create(null);
905
+ this.active = false;
906
+ this.modifiedKeys = new Set();
770
907
 
771
- /**
772
- * Detectar si estamos en desarrollo
773
- */
774
- detectEnvironment() {
775
- // Múltiples formas de detectar desarrollo
776
- return (
777
- // Vite development
778
- window.location.hostname === 'localhost' ||
779
- window.location.hostname === '127.0.0.1' ||
780
- window.location.port !== '' ||
781
- // NODE_ENV si está disponible
782
- (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') ||
783
- // URL params para forzar debug
784
- new URLSearchParams(window.location.search).has('wu-debug') ||
785
- // Manual override
786
- window.WU_DEBUG === true
787
- );
788
- }
908
+ // --- Side-effect tracking ---
909
+ this._timers = new Set();
910
+ this._intervals = new Set();
911
+ this._rafs = new Set();
912
+ this._eventListeners = []; // [{target, event, handler, options}]
789
913
 
790
- /**
791
- * Configurar nivel de logging
792
- */
793
- setLevel(level) {
794
- this.logLevel = level;
795
- return this;
796
- }
914
+ // --- DOM & Storage scoping ---
915
+ this._container = null;
916
+ this._shadowRoot = null;
917
+ this._scopedDocument = null;
918
+ this._scopedLocalStorage = null;
919
+ this._scopedSessionStorage = null;
797
920
 
798
- /**
799
- * Habilitar/deshabilitar development mode
800
- */
801
- setDevelopment(isDev) {
802
- this.isDevelopment = isDev;
803
- this.logLevel = isDev ? 'debug' : 'error';
804
- return this;
921
+ // --- Window patching state ---
922
+ this._patched = false;
923
+ this._originals = null;
805
924
  }
806
925
 
807
926
  /**
808
- * Verificar si debemos mostrar el log
927
+ * Set the DOM scope for this sandbox.
928
+ * Must be called before activate() for DOM scoping to work.
929
+ * @param {HTMLElement} container - App container element
930
+ * @param {ShadowRoot} shadowRoot - Shadow root containing the container
809
931
  */
810
- shouldLog(level) {
811
- return this.levels[level] >= this.levels[this.logLevel];
932
+ setContainer(container, shadowRoot) {
933
+ this._container = container;
934
+ this._shadowRoot = shadowRoot;
812
935
  }
813
936
 
814
937
  /**
815
- * Logging methods
938
+ * Activate the sandbox. Creates the Proxy and starts tracking.
939
+ * @returns {Proxy} The sandboxed window proxy
816
940
  */
817
- debug(...args) {
818
- if (this.shouldLog('debug')) {
819
- console.log(...args);
820
- }
821
- }
941
+ activate() {
942
+ if (this.active) return this.proxy;
822
943
 
823
- info(...args) {
824
- if (this.shouldLog('info')) {
825
- console.info(...args);
826
- }
827
- }
944
+ const self = this;
828
945
 
829
- warn(...args) {
830
- if (this.shouldLog('warn')) {
831
- console.warn(...args);
832
- }
833
- }
834
-
835
- error(...args) {
836
- if (this.shouldLog('error')) {
837
- console.error(...args);
838
- }
839
- }
840
-
841
- /**
842
- * Logging con contexto Wu
843
- */
844
- wu(level, ...args) {
845
- if (this.shouldLog(level)) {
846
- const method = level === 'debug' ? 'log' : level;
847
- console[method]('[Wu]', ...args);
848
- }
849
- }
850
-
851
- /**
852
- * Helper methods específicos para Wu
853
- */
854
- wuDebug(...args) { this.wu('debug', ...args); }
855
- wuInfo(...args) { this.wu('info', ...args); }
856
- wuWarn(...args) { this.wu('warn', ...args); }
857
- wuError(...args) { this.wu('error', ...args); }
858
- }
859
-
860
- // Singleton instance
861
- const logger = new WuLogger();
862
-
863
- /**
864
- * 🔇 Silenciar todos los logs de Wu Framework
865
- * Útil en producción para eliminar todo el ruido
866
- */
867
- function silenceAllLogs() {
868
- logger.setLevel('silent');
869
- }
870
-
871
- /**
872
- * 🔊 Restaurar logs (nivel debug)
873
- */
874
- function enableAllLogs() {
875
- logger.setLevel('debug');
876
- }
877
-
878
- var wuLogger = /*#__PURE__*/Object.freeze({
879
- __proto__: null,
880
- WuLogger: WuLogger,
881
- enableAllLogs: enableAllLogs,
882
- logger: logger,
883
- silenceAllLogs: silenceAllLogs
884
- });
885
-
886
- /**
887
- * WU-PROXY-SANDBOX: Hardened JavaScript Isolation
888
- *
889
- * ES6 Proxy-based sandbox with side-effect tracking:
890
- * - Timer hijacking (setTimeout, setInterval, requestAnimationFrame)
891
- * - Event listener tracking (window + document addEventListener)
892
- * - DOM scoping (querySelector/querySelectorAll → shadow root)
893
- * - Storage scoping (localStorage/sessionStorage → prefixed keys)
894
- *
895
- * All tracked side effects are automatically cleaned up on deactivate().
896
- */
897
-
898
-
899
- class WuProxySandbox {
900
- constructor(appName) {
901
- this.appName = appName;
902
- this.proxy = null;
903
- this.fakeWindow = Object.create(null);
904
- this.active = false;
905
- this.modifiedKeys = new Set();
906
-
907
- // --- Side-effect tracking ---
908
- this._timers = new Set();
909
- this._intervals = new Set();
910
- this._rafs = new Set();
911
- this._eventListeners = []; // [{target, event, handler, options}]
912
-
913
- // --- DOM & Storage scoping ---
914
- this._container = null;
915
- this._shadowRoot = null;
916
- this._scopedDocument = null;
917
- this._scopedLocalStorage = null;
918
- this._scopedSessionStorage = null;
919
-
920
- // --- Window patching state ---
921
- this._patched = false;
922
- this._originals = null;
923
- }
924
-
925
- /**
926
- * Set the DOM scope for this sandbox.
927
- * Must be called before activate() for DOM scoping to work.
928
- * @param {HTMLElement} container - App container element
929
- * @param {ShadowRoot} shadowRoot - Shadow root containing the container
930
- */
931
- setContainer(container, shadowRoot) {
932
- this._container = container;
933
- this._shadowRoot = shadowRoot;
934
- }
935
-
936
- /**
937
- * Activate the sandbox. Creates the Proxy and starts tracking.
938
- * @returns {Proxy} The sandboxed window proxy
939
- */
940
- activate() {
941
- if (this.active) return this.proxy;
942
-
943
- const self = this;
944
-
945
- this.proxy = new Proxy(window, {
946
- get(target, prop) {
947
- // 1. App's own isolated globals
948
- if (prop in self.fakeWindow) {
949
- return self.fakeWindow[prop];
950
- }
946
+ this.proxy = new Proxy(window, {
947
+ get(target, prop) {
948
+ // 1. App's own isolated globals
949
+ if (prop in self.fakeWindow) {
950
+ return self.fakeWindow[prop];
951
+ }
951
952
 
952
953
  // 2. Intercepted APIs
953
954
  const intercepted = self._intercept(prop, target);
@@ -3419,8 +3420,7 @@ class WuCache {
3419
3420
  maxItems: options.maxItems || 100,
3420
3421
  defaultTTL: options.defaultTTL || 3600000, // 1 hour
3421
3422
  persistent: options.persistent !== false,
3422
- storage: options.storage || 'memory',
3423
- compression: options.compression || false
3423
+ storage: options.storage || 'memory'
3424
3424
  };
3425
3425
 
3426
3426
  // 🔐 Rate limiting configuration
@@ -4788,11 +4788,14 @@ const createPlugin = (config) => {
4788
4788
  permissions: config.permissions || ['events'],
4789
4789
  install: config.install,
4790
4790
  uninstall: config.uninstall,
4791
+ beforeInit: config.beforeInit,
4792
+ afterInit: config.afterInit,
4791
4793
  beforeMount: config.beforeMount,
4792
4794
  afterMount: config.afterMount,
4793
4795
  beforeUnmount: config.beforeUnmount,
4794
4796
  afterUnmount: config.afterUnmount,
4795
- onError: config.onError
4797
+ onError: config.onError,
4798
+ onDestroy: config.onDestroy
4796
4799
  };
4797
4800
  };
4798
4801
 
@@ -7443,7 +7446,7 @@ class WuCore {
7443
7446
 
7444
7447
  /**
7445
7448
  * Registrar una aplicacion
7446
- * @param {Object} appConfig - { name, url }
7449
+ * @param {Object} appConfig - { name, url, keepAlive, sandbox, container, ... }
7447
7450
  */
7448
7451
  async registerApp(appConfig) {
7449
7452
  const { name, url } = appConfig;
@@ -7455,10 +7458,9 @@ class WuCore {
7455
7458
  const manifestData = await this.manifest.load(url);
7456
7459
  this.manifests.set(name, manifestData);
7457
7460
 
7458
- // Registrar la app
7461
+ // Registrar la app — preserve all config fields (keepAlive, sandbox, container, etc.)
7459
7462
  this.apps.set(name, {
7460
- name,
7461
- url,
7463
+ ...appConfig,
7462
7464
  manifest: manifestData,
7463
7465
  status: 'registered'
7464
7466
  });
@@ -8733,6 +8735,24 @@ class OpenAIAdapter extends BaseAdapter {
8733
8735
  if (options.temperature !== undefined) body.temperature = options.temperature;
8734
8736
  if (options.maxTokens) body.max_tokens = options.maxTokens;
8735
8737
  if (options.stream) body.stream = true;
8738
+
8739
+ // Structured output / JSON mode
8740
+ if (options.responseFormat) {
8741
+ const rf = options.responseFormat;
8742
+ if (rf === 'json' || rf?.type === 'json_object') {
8743
+ body.response_format = { type: 'json_object' };
8744
+ } else if (rf?.type === 'json_schema') {
8745
+ body.response_format = {
8746
+ type: 'json_schema',
8747
+ json_schema: {
8748
+ name: rf.name || 'response',
8749
+ schema: rf.schema,
8750
+ strict: rf.strict !== false,
8751
+ },
8752
+ };
8753
+ }
8754
+ }
8755
+
8736
8756
  return body;
8737
8757
  }
8738
8758
 
@@ -8852,6 +8872,26 @@ class AnthropicAdapter extends BaseAdapter {
8852
8872
  if (systemMsgs.length) {
8853
8873
  body.system = systemMsgs.map(m => m.content).join('\n\n');
8854
8874
  }
8875
+
8876
+ // Structured output / JSON mode (Anthropic has no native support)
8877
+ // Strategy: augment system prompt + prefill assistant turn with '{'
8878
+ if (options.responseFormat) {
8879
+ const rf = options.responseFormat;
8880
+ const jsonInstruction = '\n\nYou MUST respond with valid JSON only. No markdown, no explanation.';
8881
+
8882
+ if (rf === 'json' || rf?.type === 'json_object') {
8883
+ body.system = (body.system || '') + jsonInstruction;
8884
+ } else if (rf?.type === 'json_schema') {
8885
+ const schemaStr = JSON.stringify(rf.schema, null, 2);
8886
+ body.system = (body.system || '') +
8887
+ jsonInstruction +
8888
+ `\n\nYour response MUST conform to this JSON schema:\n${schemaStr}`;
8889
+ }
8890
+
8891
+ // Prefill assistant message with '{' to force JSON output
8892
+ body.messages.push({ role: 'assistant', content: '{' });
8893
+ }
8894
+
8855
8895
  if (options.tools?.length) {
8856
8896
  body.tools = options.tools.map(t => ({
8857
8897
  name: t.name,
@@ -8952,6 +8992,17 @@ class OllamaAdapter extends BaseAdapter {
8952
8992
  }
8953
8993
  if (options.temperature !== undefined) body.options = { temperature: options.temperature };
8954
8994
  if (options.stream !== undefined) body.stream = options.stream;
8995
+
8996
+ // Structured output / JSON mode
8997
+ if (options.responseFormat) {
8998
+ const rf = options.responseFormat;
8999
+ if (rf === 'json' || rf?.type === 'json_object') {
9000
+ body.format = 'json';
9001
+ } else if (rf?.type === 'json_schema') {
9002
+ body.format = rf.schema;
9003
+ }
9004
+ }
9005
+
8955
9006
  return body;
8956
9007
  }
8957
9008
 
@@ -9076,9 +9127,7 @@ class WuAIProvider {
9076
9127
  * @returns {Promise<{ content: string, tool_calls?: Array, usage?: object }>}
9077
9128
  */
9078
9129
  async send(messages, options = {}) {
9079
- this._ensureActive();
9080
- const adapter = this._active;
9081
- const config = this._activeConfig;
9130
+ const { adapter, config } = this._resolveProvider(options.provider);
9082
9131
 
9083
9132
  // Custom adapter: call user function directly
9084
9133
  if (adapter.isCustom && adapter._sendFn) {
@@ -9102,7 +9151,25 @@ class WuAIProvider {
9102
9151
  });
9103
9152
 
9104
9153
  const data = await response.json();
9105
- return adapter.parseResponse(data);
9154
+ const result = adapter.parseResponse(data);
9155
+
9156
+ // Anthropic prefill compensation: we prepended '{' to force JSON,
9157
+ // so the response content is the continuation — restore the full JSON
9158
+ if (adapter instanceof AnthropicAdapter && options.responseFormat && result.content) {
9159
+ result.content = '{' + result.content;
9160
+ }
9161
+
9162
+ // Validate JSON when responseFormat was requested
9163
+ if (options.responseFormat && result.content) {
9164
+ try {
9165
+ result.parsed = JSON.parse(result.content);
9166
+ } catch {
9167
+ result.parseError = 'Response is not valid JSON';
9168
+ logger.wuDebug('[wu-ai] responseFormat requested but LLM returned invalid JSON');
9169
+ }
9170
+ }
9171
+
9172
+ return result;
9106
9173
  }
9107
9174
 
9108
9175
  /**
@@ -9113,9 +9180,7 @@ class WuAIProvider {
9113
9180
  * @yields {StreamChunk}
9114
9181
  */
9115
9182
  async *stream(messages, options = {}) {
9116
- this._ensureActive();
9117
- const adapter = this._active;
9118
- const config = this._activeConfig;
9183
+ const { adapter, config } = this._resolveProvider(options.provider);
9119
9184
 
9120
9185
  // Custom adapter: call user generator directly
9121
9186
  if (adapter.isCustom && adapter._streamFn) {
@@ -9147,6 +9212,10 @@ class WuAIProvider {
9147
9212
  const decoder = new TextDecoder();
9148
9213
  let buffer = '';
9149
9214
 
9215
+ // Anthropic prefill compensation for streaming:
9216
+ // emit the '{' we used as prefill before the first real chunk
9217
+ let needsPrefill = adapter instanceof AnthropicAdapter && !!options.responseFormat;
9218
+
9150
9219
  try {
9151
9220
  while (true) {
9152
9221
  const { done, value } = await reader.read();
@@ -9161,7 +9230,13 @@ class WuAIProvider {
9161
9230
  if (!trimmed) continue;
9162
9231
 
9163
9232
  const chunk = adapter.parseStreamChunk(trimmed);
9164
- if (chunk) yield chunk;
9233
+ if (chunk) {
9234
+ if (needsPrefill && chunk.type === 'text') {
9235
+ chunk.content = '{' + chunk.content;
9236
+ needsPrefill = false;
9237
+ }
9238
+ yield chunk;
9239
+ }
9165
9240
  if (chunk?.type === 'done') return;
9166
9241
  }
9167
9242
  }
@@ -9197,10 +9272,13 @@ class WuAIProvider {
9197
9272
  }
9198
9273
  }
9199
9274
 
9200
- // 4xx (except 429) — don't retry
9201
- throw new Error(`[wu-ai] Request failed: ${response.status} ${response.statusText}`);
9275
+ // 4xx (except 429) — don't retry, fail immediately
9276
+ const clientError = new Error(`[wu-ai] Request failed: ${response.status} ${response.statusText}`);
9277
+ clientError._noRetry = true;
9278
+ throw clientError;
9202
9279
  } catch (err) {
9203
9280
  if (err.name === 'AbortError') throw err;
9281
+ if (err._noRetry) throw err; // 4xx — don't retry
9204
9282
  lastError = err;
9205
9283
  if (attempt < this._retryConfig.maxRetries) {
9206
9284
  const delay = this._retryConfig.baseDelayMs * Math.pow(2, attempt);
@@ -9224,6 +9302,25 @@ class WuAIProvider {
9224
9302
  return endpoint;
9225
9303
  }
9226
9304
 
9305
+ /**
9306
+ * Resolve which provider/adapter to use for a request.
9307
+ * Supports per-call selection: options.provider = 'anthropic'
9308
+ *
9309
+ * @param {string} [providerName] - Optional provider name override
9310
+ * @returns {{ adapter: BaseAdapter, config: object }}
9311
+ */
9312
+ _resolveProvider(providerName) {
9313
+ if (providerName) {
9314
+ const entry = this._providers.get(providerName);
9315
+ if (!entry) {
9316
+ throw new Error(`[wu-ai] Provider '${providerName}' not registered. Available: ${[...this._providers.keys()].join(', ')}`);
9317
+ }
9318
+ return { adapter: entry.adapter, config: entry.config };
9319
+ }
9320
+ this._ensureActive();
9321
+ return { adapter: this._active, config: this._activeConfig };
9322
+ }
9323
+
9227
9324
  _ensureActive() {
9228
9325
  if (!this._active) {
9229
9326
  throw new Error(
@@ -9828,6 +9925,34 @@ function validateParams(params, schema) {
9828
9925
  return { valid: errors.length === 0, errors };
9829
9926
  }
9830
9927
 
9928
+ // ─── Context Budget ──────────────────────────────────────────────
9929
+
9930
+ /**
9931
+ * Estimate token count from character count.
9932
+ * Rough heuristic: 1 token ≈ 4 chars for English, ≈ 2 chars for CJK.
9933
+ *
9934
+ * @param {string} text
9935
+ * @param {number} [charRatio=4]
9936
+ * @returns {number}
9937
+ */
9938
+ function estimateTokens(text, charRatio = 4) {
9939
+ return Math.ceil(text.length / charRatio);
9940
+ }
9941
+
9942
+ /**
9943
+ * Truncate text to fit within a token budget.
9944
+ *
9945
+ * @param {string} text
9946
+ * @param {number} maxTokens
9947
+ * @param {number} [charRatio=4]
9948
+ * @returns {string}
9949
+ */
9950
+ function truncateToTokenBudget(text, maxTokens, charRatio = 4) {
9951
+ const maxChars = maxTokens * charRatio;
9952
+ if (text.length <= maxChars) return text;
9953
+ return text.slice(0, maxChars) + '\n...[truncated to fit token budget]';
9954
+ }
9955
+
9831
9956
  /**
9832
9957
  * WU-AI-CONTEXT: Automatic context collector for LLMs
9833
9958
  *
@@ -10445,6 +10570,8 @@ const DEFAULT_CONFIG = {
10445
10570
  systemPrompt: null, // string or function returning string
10446
10571
  temperature: undefined,
10447
10572
  maxTokens: undefined,
10573
+ namespaceTTL: 30 * 60_000, // 30 min — auto-expire inactive namespaces (0 = disabled)
10574
+ gcInterval: 5 * 60_000, // 5 min — how often to sweep for expired namespaces
10448
10575
  };
10449
10576
 
10450
10577
  // ─── Conversation Namespace ──────────────────────────────────────
@@ -10509,6 +10636,7 @@ class WuAIConversation {
10509
10636
  this._config = { ...DEFAULT_CONFIG };
10510
10637
  this._namespaces = new Map();
10511
10638
  this._activeRequests = new Map(); // namespace → promise
10639
+ this._lastGcRun = Date.now();
10512
10640
  }
10513
10641
 
10514
10642
  /**
@@ -10529,6 +10657,7 @@ class WuAIConversation {
10529
10657
  * @param {object} [options.templateVars] - Variables for template interpolation
10530
10658
  * @param {number} [options.temperature] - Override temperature
10531
10659
  * @param {number} [options.maxTokens] - Override max tokens
10660
+ * @param {string|object} [options.responseFormat] - Request JSON output ('json' or { type: 'json_schema', schema, name? })
10532
10661
  * @param {AbortSignal} [options.signal] - External abort signal
10533
10662
  * @returns {Promise<{ content: string, tool_results?: Array, usage?: object, namespace: string }>}
10534
10663
  */
@@ -10577,6 +10706,8 @@ class WuAIConversation {
10577
10706
  tools: tools.length > 0 ? tools : undefined,
10578
10707
  temperature: options.temperature ?? this._config.temperature,
10579
10708
  maxTokens: options.maxTokens ?? this._config.maxTokens,
10709
+ responseFormat: options.responseFormat,
10710
+ provider: options.provider,
10580
10711
  signal,
10581
10712
  });
10582
10713
 
@@ -10701,102 +10832,113 @@ class WuAIConversation {
10701
10832
  this._permissions.rateLimiter.recordStart(ns.name);
10702
10833
 
10703
10834
  try {
10704
- const tools = this._actions.getToolSchemas();
10705
- let fullContent = '';
10706
-
10707
- // Accumulator for streaming tool call deltas
10708
- const toolCallAccumulator = new Map(); // index → { id, name, args }
10709
-
10710
- for await (const chunk of this._provider.stream(ns.getMessages(), {
10711
- tools: tools.length > 0 ? tools : undefined,
10712
- temperature: options.temperature ?? this._config.temperature,
10713
- maxTokens: options.maxTokens ?? this._config.maxTokens,
10714
- signal,
10715
- })) {
10716
- if (chunk.type === 'text') {
10717
- fullContent += chunk.content;
10718
- yield chunk;
10719
- } else if (chunk.type === 'tool_call_start') {
10720
- // Start accumulating a new tool call
10721
- toolCallAccumulator.set(toolCallAccumulator.size, {
10722
- id: chunk.id,
10723
- name: chunk.name,
10724
- args: '',
10725
- });
10726
- } else if (chunk.type === 'tool_call_delta') {
10727
- // Accumulate arguments delta
10728
- const idx = chunk.index ?? (toolCallAccumulator.size - 1);
10729
- const acc = toolCallAccumulator.get(idx);
10730
- if (acc) {
10731
- if (chunk.id) acc.id = chunk.id;
10732
- if (chunk.name) acc.name = chunk.name;
10733
- acc.args += chunk.argumentsDelta || '';
10734
- } else {
10735
- // New tool call via OpenAI format
10736
- toolCallAccumulator.set(idx, {
10737
- id: chunk.id || `tc_${idx}`,
10738
- name: chunk.name || '',
10739
- args: chunk.argumentsDelta || '',
10740
- });
10741
- }
10742
- } else if (chunk.type === 'done') {
10743
- // If we accumulated tool calls, execute them
10744
- if (toolCallAccumulator.size > 0) {
10745
- const toolCalls = [];
10746
- for (const [, acc] of toolCallAccumulator) {
10747
- let parsedArgs = {};
10748
- try { parsedArgs = JSON.parse(acc.args); } catch { /* empty args */ }
10749
- toolCalls.push({ id: acc.id, name: acc.name, arguments: parsedArgs });
10750
- }
10751
-
10752
- // Add assistant message with tool calls
10753
- ns.addMessage({ role: 'assistant', content: fullContent, tool_calls: toolCalls });
10835
+ // Tool call loop — mirrors send() behavior (up to maxToolRounds)
10836
+ let rounds = 0;
10837
+ const maxRounds = this._config.maxToolRounds;
10754
10838
 
10755
- // Execute each tool call
10756
- this._permissions.loopProtection.enter(traceId);
10757
- for (const tc of toolCalls) {
10758
- const result = await this._actions.execute(tc.name, tc.arguments, {
10759
- traceId, depth: 1, callId: tc.id,
10760
- });
10839
+ while (rounds <= maxRounds) {
10840
+ const tools = this._actions.getToolSchemas();
10841
+ let fullContent = '';
10842
+ const toolCallAccumulator = new Map(); // index → { id, name, args }
10843
+ let streamEnded = false;
10761
10844
 
10762
- yield {
10763
- type: 'tool_result',
10764
- tool: tc.name,
10765
- result: result.success ? result.result : { error: result.reason },
10766
- success: result.success,
10767
- };
10768
-
10769
- ns.addMessage({
10770
- role: 'tool',
10771
- content: JSON.stringify(result.success ? result.result : { error: result.reason }),
10772
- tool_call_id: tc.id,
10845
+ for await (const chunk of this._provider.stream(ns.getMessages(), {
10846
+ tools: tools.length > 0 ? tools : undefined,
10847
+ temperature: options.temperature ?? this._config.temperature,
10848
+ maxTokens: options.maxTokens ?? this._config.maxTokens,
10849
+ responseFormat: options.responseFormat,
10850
+ provider: options.provider,
10851
+ signal,
10852
+ })) {
10853
+ if (chunk.type === 'text') {
10854
+ fullContent += chunk.content;
10855
+ yield chunk;
10856
+ } else if (chunk.type === 'tool_call_start') {
10857
+ toolCallAccumulator.set(toolCallAccumulator.size, {
10858
+ id: chunk.id,
10859
+ name: chunk.name,
10860
+ args: '',
10861
+ });
10862
+ } else if (chunk.type === 'tool_call_delta') {
10863
+ const idx = chunk.index ?? (toolCallAccumulator.size - 1);
10864
+ const acc = toolCallAccumulator.get(idx);
10865
+ if (acc) {
10866
+ if (chunk.id) acc.id = chunk.id;
10867
+ if (chunk.name) acc.name = chunk.name;
10868
+ acc.args += chunk.argumentsDelta || '';
10869
+ } else {
10870
+ toolCallAccumulator.set(idx, {
10871
+ id: chunk.id || `tc_${idx}`,
10872
+ name: chunk.name || '',
10873
+ args: chunk.argumentsDelta || '',
10773
10874
  });
10774
10875
  }
10775
- this._permissions.loopProtection.exit(traceId);
10876
+ } else if (chunk.type === 'done') {
10877
+ streamEnded = true;
10878
+ break; // exit inner loop, handle tool calls below
10879
+ } else if (chunk.type === 'usage') {
10880
+ yield chunk;
10881
+ } else if (chunk.type === 'error') {
10882
+ yield chunk;
10883
+ }
10884
+ }
10776
10885
 
10777
- // After tool execution in streaming, yield done — caller can re-stream if needed
10778
- yield { type: 'tool_calls_done', count: toolCalls.length };
10779
- } else {
10780
- // No tool calls — add assistant message and finish
10886
+ // No tool calls final response, we're done
10887
+ if (toolCallAccumulator.size === 0) {
10888
+ if (fullContent) {
10781
10889
  ns.addMessage({ role: 'assistant', content: fullContent });
10782
10890
  }
10783
-
10784
10891
  this._permissions.circuitBreaker.recordSuccess();
10785
10892
  yield { type: 'done' };
10786
10893
  return;
10787
- } else if (chunk.type === 'usage') {
10788
- yield chunk;
10789
- } else if (chunk.type === 'error') {
10790
- yield chunk;
10791
10894
  }
10792
- }
10793
10895
 
10794
- // Stream ended without explicit 'done' chunk
10795
- if (fullContent) {
10796
- ns.addMessage({ role: 'assistant', content: fullContent });
10896
+ // Tool calls detected execute them
10897
+ rounds++;
10898
+ if (rounds > maxRounds) {
10899
+ const msg = `[wu-ai] Tool call loop limit (${maxRounds}) reached in streaming namespace '${ns.name}'`;
10900
+ logger.wuWarn(msg);
10901
+ yield { type: 'error', error: msg };
10902
+ yield { type: 'done' };
10903
+ return;
10904
+ }
10905
+
10906
+ // Parse accumulated tool calls
10907
+ const toolCalls = [];
10908
+ for (const [, acc] of toolCallAccumulator) {
10909
+ let parsedArgs = {};
10910
+ try { parsedArgs = JSON.parse(acc.args); } catch { /* empty args */ }
10911
+ toolCalls.push({ id: acc.id, name: acc.name, arguments: parsedArgs });
10912
+ }
10913
+
10914
+ // Add assistant message with tool calls to history
10915
+ ns.addMessage({ role: 'assistant', content: fullContent, tool_calls: toolCalls });
10916
+
10917
+ // Execute each tool call
10918
+ this._permissions.loopProtection.enter(traceId);
10919
+ for (const tc of toolCalls) {
10920
+ const result = await this._actions.execute(tc.name, tc.arguments, {
10921
+ traceId, depth: rounds, callId: tc.id,
10922
+ });
10923
+
10924
+ yield {
10925
+ type: 'tool_result',
10926
+ tool: tc.name,
10927
+ result: result.success ? result.result : { error: result.reason },
10928
+ success: result.success,
10929
+ };
10930
+
10931
+ ns.addMessage({
10932
+ role: 'tool',
10933
+ content: JSON.stringify(result.success ? result.result : { error: result.reason }),
10934
+ tool_call_id: tc.id,
10935
+ });
10936
+ }
10937
+ this._permissions.loopProtection.exit(traceId);
10938
+
10939
+ yield { type: 'tool_calls_done', count: toolCalls.length };
10940
+ // Loop continues → re-stream with tool results in history
10797
10941
  }
10798
- this._permissions.circuitBreaker.recordSuccess();
10799
- yield { type: 'done' };
10800
10942
 
10801
10943
  } catch (err) {
10802
10944
  this._permissions.circuitBreaker.recordFailure();
@@ -10892,10 +11034,52 @@ class WuAIConversation {
10892
11034
 
10893
11035
  _getOrCreateNamespace(name) {
10894
11036
  const nsName = name || this._config.defaultNamespace;
11037
+
11038
+ // Lazy GC sweep — only runs periodically, not on every access
11039
+ this._maybeGcSweep();
11040
+
10895
11041
  if (!this._namespaces.has(nsName)) {
10896
11042
  this._namespaces.set(nsName, new ConversationNamespace(nsName));
10897
11043
  }
10898
- return this._namespaces.get(nsName);
11044
+ const ns = this._namespaces.get(nsName);
11045
+ ns.lastActivity = Date.now();
11046
+ return ns;
11047
+ }
11048
+
11049
+ /**
11050
+ * Sweep expired namespaces. Called lazily on access, throttled by gcInterval.
11051
+ * Never deletes the default namespace or namespaces with active requests.
11052
+ */
11053
+ _maybeGcSweep() {
11054
+ const ttl = this._config.namespaceTTL;
11055
+ const interval = this._config.gcInterval;
11056
+ if (!ttl || ttl <= 0) return; // GC disabled
11057
+
11058
+ const now = Date.now();
11059
+ if (now - this._lastGcRun < interval) return; // Not time yet
11060
+ this._lastGcRun = now;
11061
+
11062
+ const cutoff = now - ttl;
11063
+ const toDelete = [];
11064
+
11065
+ for (const [name, ns] of this._namespaces) {
11066
+ // Never GC the default namespace
11067
+ if (name === this._config.defaultNamespace) continue;
11068
+ // Don't GC namespaces with active abort controllers (in-flight request)
11069
+ if (ns._abortController) continue;
11070
+ // Expired?
11071
+ if (ns.lastActivity < cutoff) {
11072
+ toDelete.push(name);
11073
+ }
11074
+ }
11075
+
11076
+ for (const name of toDelete) {
11077
+ this._namespaces.delete(name);
11078
+ }
11079
+
11080
+ if (toDelete.length > 0) {
11081
+ logger.wuDebug(`[wu-ai] GC sweep: removed ${toDelete.length} expired namespace(s): ${toDelete.join(', ')}`);
11082
+ }
10899
11083
  }
10900
11084
 
10901
11085
  async _buildSystemPrompt(options) {
@@ -11368,51 +11552,1961 @@ class WuAITriggers {
11368
11552
  }
11369
11553
 
11370
11554
  /**
11371
- * WU-AI Browser Actions
11555
+ * WU-AI-AGENT: Autonomous agent loop (Paradigm 3)
11372
11556
  *
11373
- * Registers browser automation tools into wu.ai so any LLM provider
11374
- * (OpenAI, Claude, Gemini, Ollama, etc.) can autonomously see and
11375
- * control the page no human intervention required.
11557
+ * The agent is the third paradigm of wu.ai:
11558
+ * Paradigm 1 App sends messages to LLM (conversation)
11559
+ * Paradigm 2 External LLM calls into app (tools/WebMCP)
11560
+ * Paradigm 3 — AI as autonomous director (this file)
11376
11561
  *
11377
- * Tools registered:
11378
- * browser_screenshot Capture page/element as PNG (Canvas API)
11379
- * browser_click — Click element by selector or visible text
11380
- * browser_type — Type into inputs (React/Vue/framework compatible)
11381
- * browser_snapshot — Get accessibility tree of the DOM
11382
- * browser_navigate — Navigate SPA routes
11383
- * browser_network — View captured HTTP requests (fetch + XHR)
11384
- * browser_console — View captured console messages
11385
- * browser_info — Get page state: apps, store, URL, viewport
11386
- * browser_select — Select option in dropdowns
11387
- * browser_scroll — Scroll page or element
11562
+ * The agent receives a goal and iterates: send to LLM, execute tool calls,
11563
+ * observe results, repeat until the goal is achieved or limits are hit.
11564
+ * It is an async generator, yielding each step so the caller can observe,
11565
+ * log, render, or intervene at any point.
11388
11566
  *
11389
- * @example
11390
- * // Auto-registered when wu.ai initializes
11391
- * // Any LLM connected via wu.ai.provider can now use these tools:
11392
- * const tools = wu.ai.tools();
11393
- * // includes browser_screenshot, browser_click, etc.
11567
+ * This is a foundation, not a planner. It does not decompose goals into
11568
+ * sub-tasks or maintain a world model. It trusts the LLM to drive the
11569
+ * loop and uses the [DONE] marker or tool-call cessation as termination
11570
+ * signals. More sophisticated planning belongs in userland, composed
11571
+ * on top of this primitive.
11572
+ *
11573
+ * Key features:
11574
+ * - Async generator yielding step results (observable, composable)
11575
+ * - Human-in-the-loop via shouldContinue callback
11576
+ * - Abort support via AbortController
11577
+ * - Permission preflight before every step
11578
+ * - Event emission on start, step, done, error
11579
+ * - Auto-generated system prompt describing agent role and tools
11580
+ * - Configurable step limit (default 10)
11394
11581
  */
11395
11582
 
11396
- // ── Shared capture buffers ──
11397
- const networkLog = [];
11398
- const MAX_NETWORK_LOG = 300;
11399
- const consoleLog = [];
11400
- const MAX_CONSOLE_LOG = 500;
11401
- let interceptorsInstalled = false;
11402
11583
 
11403
- /**
11404
- * Register all browser automation actions into a WuAI instance.
11405
- *
11584
+ // ─── Constants ──────────────────────────────────────────────────
11585
+
11586
+ const DONE_MARKER = '[DONE]';
11587
+
11588
+ const DEFAULT_MAX_STEPS = 10;
11589
+
11590
+ const AGENT_NAMESPACE_PREFIX = 'agent:';
11591
+
11592
+ // ─── Step Result Types ──────────────────────────────────────────
11593
+
11594
+ /**
11595
+ * @typedef {object} AgentStepResult
11596
+ * @property {number} step - Step number (1-indexed)
11597
+ * @property {'thinking'|'tool_call'|'done'|'blocked'|'aborted'|'interrupted'} type
11598
+ * @property {string} [content] - LLM text response for this step
11599
+ * @property {Array} [toolResults] - Tool execution results (if tools were called)
11600
+ * @property {object} [usage] - Token usage for this step
11601
+ * @property {string} [reason] - Reason for termination (done/blocked/aborted)
11602
+ * @property {number} elapsed - Time in ms for this step
11603
+ */
11604
+
11605
+ // ─── Agent Class ────────────────────────────────────────────────
11606
+
11607
+ class WuAIAgent {
11608
+ /**
11609
+ * @param {object} deps - Injected dependencies (same pattern as other wu-ai modules)
11610
+ * @param {import('./wu-ai-conversation.js').WuAIConversation} deps.conversation - Conversation manager
11611
+ * @param {import('./wu-ai-actions.js').WuAIActions} deps.actions - Action registry
11612
+ * @param {import('./wu-ai-context.js').WuAIContext} deps.context - Context collector
11613
+ * @param {import('./wu-ai-permissions.js').WuAIPermissions} deps.permissions - Permission system
11614
+ * @param {object} deps.eventBus - WuEventBus instance
11615
+ */
11616
+ constructor({ conversation, actions, context, permissions, eventBus }) {
11617
+ this._conversation = conversation;
11618
+ this._actions = actions;
11619
+ this._context = context;
11620
+ this._permissions = permissions;
11621
+ this._eventBus = eventBus;
11622
+
11623
+ this._config = {
11624
+ maxSteps: DEFAULT_MAX_STEPS,
11625
+ systemPrompt: null,
11626
+ };
11627
+
11628
+ this._activeRuns = new Map(); // runId -> AbortController
11629
+ this._stats = {
11630
+ totalRuns: 0,
11631
+ totalSteps: 0,
11632
+ completedRuns: 0,
11633
+ abortedRuns: 0,
11634
+ errorRuns: 0,
11635
+ };
11636
+ }
11637
+
11638
+ /**
11639
+ * Post-init configuration.
11640
+ *
11641
+ * @param {object} config
11642
+ * @param {number} [config.maxSteps] - Default max steps for all runs
11643
+ * @param {string|Function} [config.systemPrompt] - Default agent system prompt
11644
+ */
11645
+ configure(config) {
11646
+ if (config.maxSteps !== undefined) this._config.maxSteps = config.maxSteps;
11647
+ if (config.systemPrompt !== undefined) this._config.systemPrompt = config.systemPrompt;
11648
+ }
11649
+
11650
+ /**
11651
+ * Run the agent loop toward a goal.
11652
+ *
11653
+ * The generator yields after each LLM round. The caller can inspect
11654
+ * the result, update UI, or break out of the loop to abort early.
11655
+ *
11656
+ * Termination conditions (checked in order):
11657
+ * 1. AbortController signal fired
11658
+ * 2. shouldContinue() returns false (human-in-the-loop gate)
11659
+ * 3. Permission preflight denied
11660
+ * 4. LLM response contains [DONE] marker
11661
+ * 5. LLM stops requesting tool calls (after previously requesting them)
11662
+ * 6. maxSteps reached
11663
+ *
11664
+ * @param {string} goal - Natural language description of what to achieve
11665
+ * @param {object} [options]
11666
+ * @param {number} [options.maxSteps] - Override max steps for this run
11667
+ * @param {string} [options.provider] - Use a specific registered provider
11668
+ * @param {string} [options.namespace] - Conversation namespace (auto-generated if omitted)
11669
+ * @param {string|Function} [options.systemPrompt] - Override system prompt
11670
+ * @param {Function} [options.onStep] - Callback invoked after each step: (stepResult) => void
11671
+ * @param {Function} [options.shouldContinue] - Async gate: (stepResult) => boolean. Return false to stop.
11672
+ * @param {AbortSignal} [options.signal] - External abort signal
11673
+ * @param {number} [options.temperature] - LLM temperature
11674
+ * @param {number} [options.maxTokens] - LLM max tokens per step
11675
+ * @yields {AgentStepResult}
11676
+ */
11677
+ async *run(goal, options = {}) {
11678
+ const maxSteps = options.maxSteps ?? this._config.maxSteps;
11679
+ const namespace = options.namespace || this._generateNamespace();
11680
+ const runId = this._generateRunId();
11681
+
11682
+ // Abort controller: merge external signal with our own
11683
+ const controller = new AbortController();
11684
+ this._activeRuns.set(runId, controller);
11685
+
11686
+ if (options.signal) {
11687
+ if (options.signal.aborted) {
11688
+ controller.abort();
11689
+ } else {
11690
+ options.signal.addEventListener('abort', () => controller.abort(), { once: true });
11691
+ }
11692
+ }
11693
+
11694
+ this._stats.totalRuns++;
11695
+
11696
+ // Build the agent system prompt
11697
+ const systemPrompt = await this._buildAgentSystemPrompt(goal, options);
11698
+
11699
+ // Emit start event
11700
+ this._eventBus.emit('ai:agent:start', {
11701
+ runId,
11702
+ goal,
11703
+ namespace,
11704
+ maxSteps,
11705
+ }, { appName: 'wu-ai' });
11706
+
11707
+ logger.wuInfo(`[wu-ai] Agent run started: "${goal.slice(0, 80)}${goal.length > 80 ? '...' : ''}" (max ${maxSteps} steps)`);
11708
+
11709
+ let step = 0;
11710
+ let previousHadToolCalls = false;
11711
+ let finalReason = 'max_steps';
11712
+
11713
+ try {
11714
+ // Inject the goal as the first user message via conversation.send
11715
+ // The loop sends the goal on step 1, then follow-up prompts on subsequent steps.
11716
+
11717
+ while (step < maxSteps) {
11718
+ step++;
11719
+ const stepStart = Date.now();
11720
+
11721
+ // ── 1. Check abort ──
11722
+ if (controller.signal.aborted) {
11723
+ const result = this._buildStepResult(step, 'aborted', {
11724
+ reason: 'Aborted by caller',
11725
+ elapsed: Date.now() - stepStart,
11726
+ });
11727
+ this._stats.abortedRuns++;
11728
+ finalReason = 'aborted';
11729
+ this._emitStep(runId, result);
11730
+ yield result;
11731
+ return;
11732
+ }
11733
+
11734
+ // ── 2. Permission preflight ──
11735
+ const traceId = this._permissions.loopProtection.createTraceId();
11736
+ const preflight = this._permissions.preflight({
11737
+ namespace,
11738
+ depth: step,
11739
+ traceId,
11740
+ });
11741
+
11742
+ if (!preflight.allowed) {
11743
+ const result = this._buildStepResult(step, 'blocked', {
11744
+ reason: preflight.reason,
11745
+ elapsed: Date.now() - stepStart,
11746
+ });
11747
+ finalReason = 'blocked';
11748
+ this._emitStep(runId, result);
11749
+ yield result;
11750
+ return;
11751
+ }
11752
+
11753
+ // ── 3. Compose the message for this step ──
11754
+ const message = step === 1
11755
+ ? goal
11756
+ : 'Continue working toward the goal. If you are done, include [DONE] in your response.';
11757
+
11758
+ // ── 4. Send to conversation (handles tool call loops internally) ──
11759
+ let response;
11760
+ try {
11761
+ response = await this._conversation.send(message, {
11762
+ namespace,
11763
+ systemPrompt,
11764
+ provider: options.provider,
11765
+ temperature: options.temperature,
11766
+ maxTokens: options.maxTokens,
11767
+ signal: controller.signal,
11768
+ });
11769
+ } catch (err) {
11770
+ if (err.name === 'AbortError' || controller.signal.aborted) {
11771
+ const result = this._buildStepResult(step, 'aborted', {
11772
+ reason: 'Aborted during LLM call',
11773
+ elapsed: Date.now() - stepStart,
11774
+ });
11775
+ this._stats.abortedRuns++;
11776
+ finalReason = 'aborted';
11777
+ this._emitStep(runId, result);
11778
+ yield result;
11779
+ return;
11780
+ }
11781
+ throw err;
11782
+ }
11783
+
11784
+ const elapsed = Date.now() - stepStart;
11785
+ this._stats.totalSteps++;
11786
+
11787
+ const hasToolResults = response.tool_results && response.tool_results.length > 0;
11788
+ const content = response.content || '';
11789
+
11790
+ // ── 5. Check for DONE marker ──
11791
+ if (content.includes(DONE_MARKER)) {
11792
+ const result = this._buildStepResult(step, 'done', {
11793
+ content: content.replace(DONE_MARKER, '').trim(),
11794
+ toolResults: response.tool_results,
11795
+ usage: response.usage,
11796
+ reason: 'Goal completed (DONE marker)',
11797
+ elapsed,
11798
+ });
11799
+ finalReason = 'done';
11800
+ this._stats.completedRuns++;
11801
+ this._emitStep(runId, result);
11802
+ if (options.onStep) await this._safeCallback(options.onStep, result);
11803
+ yield result;
11804
+ return;
11805
+ }
11806
+
11807
+ // ── 6. Check for tool-call cessation ──
11808
+ // If the LLM was previously calling tools but stopped, it has
11809
+ // settled on a final answer. This is the implicit completion signal.
11810
+ const currentHasToolCalls = hasToolResults;
11811
+ if (previousHadToolCalls && !currentHasToolCalls) {
11812
+ const result = this._buildStepResult(step, 'done', {
11813
+ content,
11814
+ toolResults: response.tool_results,
11815
+ usage: response.usage,
11816
+ reason: 'Goal completed (no further tool calls)',
11817
+ elapsed,
11818
+ });
11819
+ finalReason = 'done';
11820
+ this._stats.completedRuns++;
11821
+ this._emitStep(runId, result);
11822
+ if (options.onStep) await this._safeCallback(options.onStep, result);
11823
+ yield result;
11824
+ return;
11825
+ }
11826
+
11827
+ previousHadToolCalls = currentHasToolCalls;
11828
+
11829
+ // ── 7. Yield the step result ──
11830
+ const stepType = hasToolResults ? 'tool_call' : 'thinking';
11831
+ const result = this._buildStepResult(step, stepType, {
11832
+ content,
11833
+ toolResults: response.tool_results,
11834
+ usage: response.usage,
11835
+ elapsed,
11836
+ });
11837
+
11838
+ this._emitStep(runId, result);
11839
+ if (options.onStep) await this._safeCallback(options.onStep, result);
11840
+ yield result;
11841
+
11842
+ // ── 8. Human-in-the-loop gate ──
11843
+ if (options.shouldContinue) {
11844
+ let shouldGo;
11845
+ try {
11846
+ shouldGo = await options.shouldContinue(result);
11847
+ } catch {
11848
+ shouldGo = false;
11849
+ }
11850
+
11851
+ if (!shouldGo) {
11852
+ const interrupted = this._buildStepResult(step, 'interrupted', {
11853
+ content,
11854
+ reason: 'Stopped by shouldContinue callback',
11855
+ elapsed: 0,
11856
+ });
11857
+ finalReason = 'interrupted';
11858
+ this._emitStep(runId, interrupted);
11859
+ yield interrupted;
11860
+ return;
11861
+ }
11862
+ }
11863
+ }
11864
+
11865
+ // ── Max steps reached ──
11866
+ const maxStepResult = this._buildStepResult(step, 'done', {
11867
+ reason: `Max steps (${maxSteps}) reached`,
11868
+ elapsed: 0,
11869
+ });
11870
+ finalReason = 'max_steps';
11871
+ yield maxStepResult;
11872
+
11873
+ } catch (err) {
11874
+ this._stats.errorRuns++;
11875
+ finalReason = 'error';
11876
+
11877
+ logger.wuWarn(`[wu-ai] Agent run error: ${err.message}`);
11878
+
11879
+ this._eventBus.emit('ai:agent:error', {
11880
+ runId,
11881
+ goal,
11882
+ namespace,
11883
+ step,
11884
+ error: err.message,
11885
+ }, { appName: 'wu-ai' });
11886
+
11887
+ throw err;
11888
+ } finally {
11889
+ // Clean up
11890
+ this._activeRuns.delete(runId);
11891
+
11892
+ // Delete auto-generated agent namespaces to prevent memory leaks
11893
+ // User-provided namespaces are preserved (the user owns their lifecycle)
11894
+ if (!options.namespace && namespace.startsWith(AGENT_NAMESPACE_PREFIX)) {
11895
+ this._conversation.deleteNamespace(namespace);
11896
+ }
11897
+
11898
+ this._eventBus.emit('ai:agent:done', {
11899
+ runId,
11900
+ goal,
11901
+ namespace,
11902
+ totalSteps: step,
11903
+ reason: finalReason,
11904
+ }, { appName: 'wu-ai' });
11905
+
11906
+ logger.wuDebug(`[wu-ai] Agent run finished: ${finalReason} after ${step} step(s)`);
11907
+ }
11908
+ }
11909
+
11910
+ /**
11911
+ * Abort an active agent run by runId.
11912
+ *
11913
+ * @param {string} runId - The run ID to abort
11914
+ */
11915
+ abort(runId) {
11916
+ const controller = this._activeRuns.get(runId);
11917
+ if (controller) {
11918
+ controller.abort();
11919
+ logger.wuDebug(`[wu-ai] Agent run aborted: ${runId}`);
11920
+ }
11921
+ }
11922
+
11923
+ /**
11924
+ * Abort all active agent runs.
11925
+ */
11926
+ abortAll() {
11927
+ for (const [runId, controller] of this._activeRuns) {
11928
+ controller.abort();
11929
+ }
11930
+ this._activeRuns.clear();
11931
+ }
11932
+
11933
+ /**
11934
+ * Get IDs of currently active runs.
11935
+ *
11936
+ * @returns {string[]}
11937
+ */
11938
+ getActiveRuns() {
11939
+ return [...this._activeRuns.keys()];
11940
+ }
11941
+
11942
+ /**
11943
+ * Get agent statistics.
11944
+ *
11945
+ * @returns {object}
11946
+ */
11947
+ getStats() {
11948
+ return {
11949
+ ...this._stats,
11950
+ activeRuns: this._activeRuns.size,
11951
+ config: { ...this._config },
11952
+ };
11953
+ }
11954
+
11955
+ /**
11956
+ * Destroy the agent, aborting all active runs.
11957
+ */
11958
+ destroy() {
11959
+ this.abortAll();
11960
+ this._stats = {
11961
+ totalRuns: 0,
11962
+ totalSteps: 0,
11963
+ completedRuns: 0,
11964
+ abortedRuns: 0,
11965
+ errorRuns: 0,
11966
+ };
11967
+ }
11968
+
11969
+ // ─── Private ──────────────────────────────────────────────────
11970
+
11971
+ /**
11972
+ * Build the agent-specific system prompt. This tells the LLM it is
11973
+ * operating as an autonomous agent with a goal, available tools, and
11974
+ * the [DONE] completion protocol.
11975
+ */
11976
+ async _buildAgentSystemPrompt(goal, options) {
11977
+ // Explicit override takes precedence
11978
+ const basePrompt = options.systemPrompt
11979
+ ?? this._config.systemPrompt
11980
+ ?? null;
11981
+
11982
+ if (basePrompt) {
11983
+ const resolved = typeof basePrompt === 'function'
11984
+ ? await basePrompt(goal)
11985
+ : basePrompt;
11986
+ return resolved;
11987
+ }
11988
+
11989
+ // Auto-generate from context and tools
11990
+ const parts = [];
11991
+
11992
+ parts.push(
11993
+ 'You are an autonomous AI agent connected to a live web application via Wu Framework.',
11994
+ 'You have been given a goal and must work step-by-step to achieve it.',
11995
+ '',
11996
+ 'PROTOCOL:',
11997
+ '- Each message you send is one "step" in your execution.',
11998
+ '- You may call tools to read or modify application state.',
11999
+ '- After each step, you will be prompted to continue.',
12000
+ '- When the goal is fully achieved, include the marker [DONE] in your response.',
12001
+ '- If you determine the goal cannot be achieved, include [DONE] and explain why.',
12002
+ '- Be concise. Each step should make meaningful progress.',
12003
+ '',
12004
+ );
12005
+
12006
+ // Collect context if available
12007
+ if (this._context) {
12008
+ try {
12009
+ await this._context.collect();
12010
+ const snapshot = this._context.getSnapshot();
12011
+ if (snapshot?._mountedApps?.length) {
12012
+ parts.push(`MOUNTED APPS: ${snapshot._mountedApps.join(', ')}`, '');
12013
+ }
12014
+ if (snapshot?._store && Object.keys(snapshot._store).length > 0) {
12015
+ parts.push(`APPLICATION STATE:\n${JSON.stringify(snapshot._store, null, 2)}`, '');
12016
+ }
12017
+ } catch {
12018
+ // Context collection is best-effort
12019
+ }
12020
+ }
12021
+
12022
+ // List available tools
12023
+ const tools = this._actions.getToolSchemas();
12024
+ if (tools.length > 0) {
12025
+ parts.push('AVAILABLE TOOLS:');
12026
+ for (const tool of tools) {
12027
+ const paramKeys = tool.parameters?.properties
12028
+ ? Object.keys(tool.parameters.properties).join(', ')
12029
+ : 'none';
12030
+ parts.push(`- ${tool.name}(${paramKeys}): ${tool.description}`);
12031
+ }
12032
+ parts.push('');
12033
+ }
12034
+
12035
+ parts.push(`GOAL: ${goal}`);
12036
+
12037
+ return parts.join('\n');
12038
+ }
12039
+
12040
+ /**
12041
+ * Build a normalized step result object.
12042
+ *
12043
+ * @param {number} step
12044
+ * @param {string} type
12045
+ * @param {object} data
12046
+ * @returns {AgentStepResult}
12047
+ */
12048
+ _buildStepResult(step, type, data = {}) {
12049
+ return {
12050
+ step,
12051
+ type,
12052
+ content: data.content ?? null,
12053
+ toolResults: data.toolResults ?? null,
12054
+ usage: data.usage ?? null,
12055
+ reason: data.reason ?? null,
12056
+ elapsed: data.elapsed ?? 0,
12057
+ };
12058
+ }
12059
+
12060
+ /**
12061
+ * Emit an ai:agent:step event.
12062
+ */
12063
+ _emitStep(runId, result) {
12064
+ this._eventBus.emit('ai:agent:step', {
12065
+ runId,
12066
+ ...result,
12067
+ }, { appName: 'wu-ai' });
12068
+ }
12069
+
12070
+ /**
12071
+ * Safely invoke a callback, swallowing errors so the agent loop
12072
+ * is never broken by a faulty onStep handler.
12073
+ */
12074
+ async _safeCallback(fn, ...args) {
12075
+ try {
12076
+ const result = fn(...args);
12077
+ if (result && typeof result.then === 'function') {
12078
+ await result;
12079
+ }
12080
+ } catch (err) {
12081
+ logger.wuDebug(`[wu-ai] Agent callback error: ${err.message}`);
12082
+ }
12083
+ }
12084
+
12085
+ /**
12086
+ * Generate a unique namespace for an agent run.
12087
+ */
12088
+ _generateNamespace() {
12089
+ return AGENT_NAMESPACE_PREFIX + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 6);
12090
+ }
12091
+
12092
+ /**
12093
+ * Generate a unique run ID.
12094
+ */
12095
+ _generateRunId() {
12096
+ return 'run_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
12097
+ }
12098
+ }
12099
+
12100
+ /**
12101
+ * WU-AI Browser Primitives
12102
+ *
12103
+ * Shared browser capabilities used by both wu-ai-browser.js (Paradigm 1/2)
12104
+ * and wu-mcp-bridge.js (Paradigm 3). Single source of truth to avoid
12105
+ * duplicating interceptors, DOM traversal, and Canvas rendering.
12106
+ */
12107
+
12108
+ // ── Shared capture buffers (singleton) ──
12109
+
12110
+ const networkLog = [];
12111
+ const MAX_NETWORK_LOG = 300;
12112
+ const consoleLog = [];
12113
+ const MAX_CONSOLE_LOG = 500;
12114
+
12115
+ let _interceptorsInstalled = false;
12116
+
12117
+ /**
12118
+ * Install network + console interceptors (idempotent — only runs once).
12119
+ */
12120
+ function ensureInterceptors() {
12121
+ if (_interceptorsInstalled) return;
12122
+ _installNetworkInterceptor();
12123
+ _installConsoleInterceptor();
12124
+ _interceptorsInstalled = true;
12125
+ }
12126
+
12127
+ /**
12128
+ * Build an accessibility tree representation of a DOM element.
12129
+ * Traverses into Shadow DOM if present.
12130
+ */
12131
+ function buildA11yTree(el, depth = 0, maxDepth = 5) {
12132
+ if (depth > maxDepth || !el) return '';
12133
+
12134
+ const indent = ' '.repeat(depth);
12135
+ const tag = el.tagName?.toLowerCase() || '';
12136
+ const role = el.getAttribute?.('role') || '';
12137
+ const ariaLabel = el.getAttribute?.('aria-label') || '';
12138
+ const text = el.childNodes?.length === 1 && el.childNodes[0].nodeType === 3
12139
+ ? el.textContent?.trim().slice(0, 80) : '';
12140
+
12141
+ let line = `${indent}<${tag}`;
12142
+ if (el.id) line += ` id="${el.id}"`;
12143
+ if (role) line += ` role="${role}"`;
12144
+ if (ariaLabel) line += ` aria-label="${ariaLabel}"`;
12145
+ if (el.className && typeof el.className === 'string') {
12146
+ const cls = el.className.trim().slice(0, 60);
12147
+ if (cls) line += ` class="${cls}"`;
12148
+ }
12149
+ line += '>';
12150
+ if (text) line += ` "${text}"`;
12151
+
12152
+ let result = line + '\n';
12153
+ const root = el.shadowRoot || el;
12154
+ const children = root.children || [];
12155
+
12156
+ for (let i = 0; i < children.length && i < 50; i++) {
12157
+ result += buildA11yTree(children[i], depth + 1, maxDepth);
12158
+ }
12159
+ return result;
12160
+ }
12161
+
12162
+ /**
12163
+ * Recursively inline computed styles from source to clone for Canvas rendering.
12164
+ */
12165
+ function inlineComputedStyles(source, clone) {
12166
+ const props = ['color', 'background', 'background-color', 'font-family',
12167
+ 'font-size', 'font-weight', 'border', 'border-radius', 'padding', 'margin',
12168
+ 'display', 'flex-direction', 'align-items', 'justify-content', 'gap',
12169
+ 'width', 'height', 'max-width', 'max-height', 'overflow', 'opacity',
12170
+ 'box-shadow', 'text-align', 'line-height', 'position', 'top', 'left',
12171
+ 'right', 'bottom', 'z-index', 'transform', 'visibility'];
12172
+
12173
+ try {
12174
+ const style = window.getComputedStyle(source);
12175
+ for (const prop of props) {
12176
+ const val = style.getPropertyValue(prop);
12177
+ if (val) clone.style?.setProperty(prop, val);
12178
+ }
12179
+ } catch (_) { /* skip */ }
12180
+
12181
+ const srcKids = source.children || [];
12182
+ const cloneKids = clone.children || [];
12183
+ const max = Math.min(srcKids.length, cloneKids.length, 200);
12184
+ for (let i = 0; i < max; i++) {
12185
+ inlineComputedStyles(srcKids[i], cloneKids[i]);
12186
+ }
12187
+ }
12188
+
12189
+ /**
12190
+ * Capture a screenshot of a DOM element via Canvas API (SVG foreignObject).
12191
+ * @returns {Promise<{ width, height, format, base64, sizeKB } | { error: string }>}
12192
+ */
12193
+ async function captureScreenshot(selector, quality = 0.8) {
12194
+ const target = selector
12195
+ ? document.querySelector(selector)
12196
+ : document.documentElement;
12197
+
12198
+ if (!target) return { error: `Element not found: ${selector}` };
12199
+
12200
+ const rect = target.getBoundingClientRect();
12201
+ const w = Math.ceil(Math.min(rect.width || window.innerWidth, 1920));
12202
+ const h = Math.ceil(Math.min(rect.height || window.innerHeight, 1080));
12203
+
12204
+ const clone = target.cloneNode(true);
12205
+ inlineComputedStyles(target, clone);
12206
+
12207
+ const serializer = new XMLSerializer();
12208
+ const xhtml = serializer.serializeToString(clone);
12209
+
12210
+ const svgStr = [
12211
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">`,
12212
+ '<foreignObject width="100%" height="100%">',
12213
+ `<div xmlns="http://www.w3.org/1999/xhtml" style="width:${w}px;height:${h}px;overflow:hidden;">`,
12214
+ xhtml,
12215
+ '</div>',
12216
+ '</foreignObject>',
12217
+ '</svg>',
12218
+ ].join('');
12219
+
12220
+ const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
12221
+ const url = URL.createObjectURL(svgBlob);
12222
+
12223
+ const dataUrl = await new Promise((resolve) => {
12224
+ const img = new Image();
12225
+ img.onload = () => {
12226
+ const canvas = document.createElement('canvas');
12227
+ canvas.width = w;
12228
+ canvas.height = h;
12229
+ const ctx = canvas.getContext('2d');
12230
+ ctx.drawImage(img, 0, 0);
12231
+ URL.revokeObjectURL(url);
12232
+ resolve(canvas.toDataURL('image/png', quality));
12233
+ };
12234
+ img.onerror = () => {
12235
+ URL.revokeObjectURL(url);
12236
+ resolve(null);
12237
+ };
12238
+ img.src = url;
12239
+ });
12240
+
12241
+ if (!dataUrl) return { error: 'Canvas rendering failed' };
12242
+
12243
+ const base64 = dataUrl.split(',')[1];
12244
+ return {
12245
+ width: w,
12246
+ height: h,
12247
+ format: 'png',
12248
+ base64,
12249
+ sizeKB: Math.round((base64.length * 3) / 4 / 1024),
12250
+ };
12251
+ }
12252
+
12253
+ /**
12254
+ * Click an element by CSS selector or visible text content.
12255
+ * @returns {{ clicked, text } | { error }}
12256
+ */
12257
+ function clickElement(selector, text) {
12258
+ let el = null;
12259
+
12260
+ if (selector) {
12261
+ el = document.querySelector(selector);
12262
+ }
12263
+
12264
+ if (!el && text) {
12265
+ const candidates = document.querySelectorAll(
12266
+ 'button, a, [role="button"], input[type="submit"], input[type="button"], [data-click], label, [onclick]'
12267
+ );
12268
+ const searchText = text.toLowerCase();
12269
+ for (const candidate of candidates) {
12270
+ if (candidate.textContent?.trim().toLowerCase().includes(searchText)) {
12271
+ el = candidate;
12272
+ break;
12273
+ }
12274
+ }
12275
+ }
12276
+
12277
+ if (!el) return { error: `Element not found: ${selector || `text="${text}"`}` };
12278
+
12279
+ el.scrollIntoView({ behavior: 'instant', block: 'center' });
12280
+ el.click();
12281
+
12282
+ const tag = el.tagName?.toLowerCase();
12283
+ const id = el.id ? `#${el.id}` : '';
12284
+ return {
12285
+ clicked: `${tag}${id}`,
12286
+ text: el.textContent?.trim().slice(0, 100) || '',
12287
+ rect: el.getBoundingClientRect().toJSON?.() || null,
12288
+ };
12289
+ }
12290
+
12291
+ /**
12292
+ * Type into an input, textarea, or contenteditable element.
12293
+ * Works with React, Vue, Angular, and other frameworks.
12294
+ * @returns {{ selector, typed, currentValue, submitted } | { error }}
12295
+ */
12296
+ function typeIntoElement(selector, text, { clear = false, submit = false } = {}) {
12297
+ if (!selector) return { error: 'selector is required' };
12298
+ if (text === undefined) return { error: 'text is required' };
12299
+
12300
+ const el = document.querySelector(selector);
12301
+ if (!el) return { error: `Element not found: ${selector}` };
12302
+
12303
+ el.focus();
12304
+
12305
+ if (clear) {
12306
+ el.value = '';
12307
+ el.dispatchEvent(new Event('input', { bubbles: true }));
12308
+ }
12309
+
12310
+ // Use native setter to trigger framework reactivity (React, Vue, etc.)
12311
+ const nativeSetter = Object.getOwnPropertyDescriptor(
12312
+ window.HTMLInputElement.prototype, 'value'
12313
+ )?.set || Object.getOwnPropertyDescriptor(
12314
+ window.HTMLTextAreaElement.prototype, 'value'
12315
+ )?.set;
12316
+
12317
+ const newValue = clear ? text : (el.value || '') + text;
12318
+ if (nativeSetter) {
12319
+ nativeSetter.call(el, newValue);
12320
+ } else {
12321
+ el.value = newValue;
12322
+ }
12323
+
12324
+ el.dispatchEvent(new Event('input', { bubbles: true }));
12325
+ el.dispatchEvent(new Event('change', { bubbles: true }));
12326
+
12327
+ if (submit) {
12328
+ const form = el.closest('form');
12329
+ if (form) {
12330
+ form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
12331
+ } else {
12332
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
12333
+ }
12334
+ }
12335
+
12336
+ return {
12337
+ selector,
12338
+ typed: text,
12339
+ currentValue: el.value?.slice(0, 200),
12340
+ submitted: !!submit,
12341
+ };
12342
+ }
12343
+
12344
+ /**
12345
+ * Filter and return network log entries.
12346
+ */
12347
+ function getFilteredNetwork(method, status, limit = 30) {
12348
+ let filtered = networkLog;
12349
+ if (method) {
12350
+ filtered = filtered.filter((r) => r.method === method.toUpperCase());
12351
+ }
12352
+ if (status) {
12353
+ if (status === 'error') {
12354
+ filtered = filtered.filter((r) => r.status === 0 || r.status >= 400);
12355
+ } else {
12356
+ filtered = filtered.filter((r) => String(r.status).startsWith(String(status)));
12357
+ }
12358
+ }
12359
+ return {
12360
+ requests: filtered.slice(-limit),
12361
+ total: networkLog.length,
12362
+ showing: Math.min(filtered.length, limit),
12363
+ };
12364
+ }
12365
+
12366
+ /**
12367
+ * Filter and return console log entries.
12368
+ */
12369
+ function getFilteredConsole(level, limit = 30) {
12370
+ const filtered = level && level !== 'all'
12371
+ ? consoleLog.filter((m) => m.level === level)
12372
+ : consoleLog;
12373
+ return {
12374
+ messages: filtered.slice(-limit),
12375
+ total: consoleLog.length,
12376
+ showing: Math.min(filtered.length, limit),
12377
+ };
12378
+ }
12379
+
12380
+ // ── Private: Interceptors ──
12381
+
12382
+ function _installNetworkInterceptor() {
12383
+ // Intercept fetch
12384
+ const originalFetch = window.fetch;
12385
+ window.fetch = async function (...args) {
12386
+ const start = Date.now();
12387
+ const req = args[0];
12388
+ const url = typeof req === 'string' ? req : req?.url || '';
12389
+ const method = (args[1]?.method || req?.method || 'GET').toUpperCase();
12390
+
12391
+ try {
12392
+ const response = await originalFetch.apply(window, args);
12393
+ const size = parseInt(response.headers?.get('content-length') || '0', 10);
12394
+ networkLog.push({
12395
+ type: 'fetch', method, url,
12396
+ status: response.status, statusText: response.statusText,
12397
+ duration: Date.now() - start, size, timestamp: start,
12398
+ });
12399
+ if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
12400
+ return response;
12401
+ } catch (err) {
12402
+ networkLog.push({
12403
+ type: 'fetch', method, url,
12404
+ status: 0, error: err.message,
12405
+ duration: Date.now() - start, timestamp: start,
12406
+ });
12407
+ if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
12408
+ throw err;
12409
+ }
12410
+ };
12411
+
12412
+ // Intercept XMLHttpRequest
12413
+ const origOpen = XMLHttpRequest.prototype.open;
12414
+ const origSend = XMLHttpRequest.prototype.send;
12415
+
12416
+ XMLHttpRequest.prototype.open = function (method, url, ...rest) {
12417
+ this._wuAi = { method: (method || 'GET').toUpperCase(), url: String(url) };
12418
+ return origOpen.call(this, method, url, ...rest);
12419
+ };
12420
+
12421
+ XMLHttpRequest.prototype.send = function (...args) {
12422
+ if (this._wuAi) {
12423
+ this._wuAi.start = Date.now();
12424
+ this.addEventListener('loadend', () => {
12425
+ networkLog.push({
12426
+ type: 'xhr', method: this._wuAi.method, url: this._wuAi.url,
12427
+ status: this.status, statusText: this.statusText,
12428
+ duration: Date.now() - this._wuAi.start,
12429
+ size: parseInt(this.getResponseHeader('content-length') || '0', 10),
12430
+ timestamp: this._wuAi.start,
12431
+ });
12432
+ if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
12433
+ });
12434
+ }
12435
+ return origSend.apply(this, args);
12436
+ };
12437
+ }
12438
+
12439
+ function _installConsoleInterceptor() {
12440
+ const levels = ['log', 'warn', 'error'];
12441
+ for (const level of levels) {
12442
+ const original = console[level];
12443
+ console[level] = (...args) => {
12444
+ consoleLog.push({
12445
+ level,
12446
+ message: args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '),
12447
+ timestamp: Date.now(),
12448
+ });
12449
+ if (consoleLog.length > MAX_CONSOLE_LOG) consoleLog.shift();
12450
+ original.apply(console, args);
12451
+ };
12452
+ }
12453
+ }
12454
+
12455
+ /**
12456
+ * WU-AI-ORCHESTRATE: Cross-Micro-App AI Coordination (Paradigm 4)
12457
+ *
12458
+ * The fourth paradigm of wu.ai:
12459
+ * Paradigm 1 — App sends messages to LLM (conversation)
12460
+ * Paradigm 2 — External LLM calls into app (tools/WebMCP)
12461
+ * Paradigm 3 — AI as autonomous director (agent loop)
12462
+ * Paradigm 4 — AI as microfrontend glue (this file)
12463
+ *
12464
+ * The Problem:
12465
+ * In microfrontend architectures, apps are isolated by design.
12466
+ * Cross-app coordination requires manual wiring through events
12467
+ * and shared state. As apps grow, wiring becomes n² complexity.
12468
+ *
12469
+ * The Solution:
12470
+ * Each micro-app declares its capabilities to the AI layer.
12471
+ * The AI understands the semantic meaning of each capability
12472
+ * and can resolve natural-language intents by calling the right
12473
+ * actions across the right apps — without tight coupling.
12474
+ *
12475
+ * Key Concepts:
12476
+ * - Capability: An action scoped to a specific micro-app
12477
+ * Registered as 'appName:actionName', cleaned up on unmount.
12478
+ * - Intent: A natural-language cross-app request resolved in
12479
+ * a single conversation turn with an orchestrator system prompt.
12480
+ * - Capability Map: The AI's understanding of system topology —
12481
+ * which apps exist and what each can do.
12482
+ *
12483
+ * This module does NOT replace actions, triggers, or agents.
12484
+ * It enriches them with cross-app topology awareness.
12485
+ *
12486
+ * API (accessible via wu.ai):
12487
+ * wu.ai.capability(app, name, config) → Register app-scoped capability
12488
+ * wu.ai.intent(description, options) → Resolve cross-app intent
12489
+ * wu.ai.removeApp(appName) → Cleanup on unmount
12490
+ * wu.ai.workflow(name, config) → Register reusable AI workflow
12491
+ * wu.ai.runWorkflow(name, params, opts) → Execute a registered workflow
12492
+ */
12493
+
12494
+
12495
+ // ─── Constants ──────────────────────────────────────────────────
12496
+
12497
+ const INTENT_NAMESPACE_PREFIX = 'intent:';
12498
+
12499
+ // ─── Deterministic Step Actions ─────────────────────────────────
12500
+
12501
+ /**
12502
+ * Execute a single deterministic workflow step.
12503
+ * No AI needed — directly calls browser primitives.
12504
+ *
12505
+ * @param {object} step - Step definition
12506
+ * @param {string} step.action - 'click' | 'type' | 'navigate' | 'wait' | 'emit' | 'setState'
12507
+ * @param {object} params - Interpolated params
12508
+ * @returns {{ success: boolean, detail?: string, error?: string }}
12509
+ */
12510
+ function executeDeterministicStep(step, eventBus, store) {
12511
+ switch (step.action) {
12512
+ case 'click': {
12513
+ const result = clickElement(step.selector, step.text);
12514
+ if (result.error) return { success: false, error: result.error };
12515
+ return { success: true, detail: `Clicked: ${step.selector || step.text}` };
12516
+ }
12517
+
12518
+ case 'type': {
12519
+ const result = typeIntoElement(step.selector, step.value, {
12520
+ clear: step.clear ?? true,
12521
+ submit: step.submit ?? false,
12522
+ });
12523
+ if (result.error) return { success: false, error: result.error };
12524
+ return { success: true, detail: `Typed "${step.value}" into ${step.selector}` };
12525
+ }
12526
+
12527
+ case 'navigate': {
12528
+ if (eventBus && step.section) {
12529
+ eventBus.emit('nav:section', { section: step.section }, { appName: 'wu-ai' });
12530
+ return { success: true, detail: `Navigated to section: ${step.section}` };
12531
+ }
12532
+ if (step.selector) {
12533
+ const result = clickElement(step.selector, step.text);
12534
+ if (result.error) return { success: false, error: result.error };
12535
+ return { success: true, detail: `Navigated via click: ${step.selector}` };
12536
+ }
12537
+ return { success: false, error: 'navigate requires "section" or "selector"' };
12538
+ }
12539
+
12540
+ case 'wait': {
12541
+ // Wait is handled by the runner (delay + optional selector poll)
12542
+ return { success: true, detail: `Wait: ${step.ms || 0}ms` };
12543
+ }
12544
+
12545
+ case 'emit': {
12546
+ if (!eventBus) return { success: false, error: 'eventBus not available' };
12547
+ eventBus.emit(step.event, step.data || {}, { appName: 'wu-ai' });
12548
+ return { success: true, detail: `Emitted: ${step.event}` };
12549
+ }
12550
+
12551
+ case 'setState': {
12552
+ if (!store) return { success: false, error: 'store not available' };
12553
+ store.set(step.path, step.value);
12554
+ return { success: true, detail: `Set state: ${step.path}` };
12555
+ }
12556
+
12557
+ default:
12558
+ return { success: false, error: `Unknown action: ${step.action}` };
12559
+ }
12560
+ }
12561
+
12562
+ /**
12563
+ * Wait for a selector to appear in the DOM (with timeout).
12564
+ *
12565
+ * @param {string} selector
12566
+ * @param {number} timeout - ms
12567
+ * @returns {Promise<boolean>}
12568
+ */
12569
+ function waitForSelector(selector, timeout = 5000) {
12570
+ return new Promise((resolve) => {
12571
+ if (document.querySelector(selector)) {
12572
+ resolve(true);
12573
+ return;
12574
+ }
12575
+
12576
+ const interval = 100;
12577
+ let elapsed = 0;
12578
+ const timer = setInterval(() => {
12579
+ elapsed += interval;
12580
+ if (document.querySelector(selector)) {
12581
+ clearInterval(timer);
12582
+ resolve(true);
12583
+ } else if (elapsed >= timeout) {
12584
+ clearInterval(timer);
12585
+ resolve(false);
12586
+ }
12587
+ }, interval);
12588
+ });
12589
+ }
12590
+
12591
+ /**
12592
+ * Simple delay.
12593
+ */
12594
+ function delay(ms) {
12595
+ return new Promise((resolve) => setTimeout(resolve, ms));
12596
+ }
12597
+
12598
+ // ─── WuAIOrchestrate ────────────────────────────────────────────
12599
+
12600
+ class WuAIOrchestrate {
12601
+ /**
12602
+ * @param {object} deps
12603
+ * @param {import('./wu-ai-actions.js').WuAIActions} deps.actions
12604
+ * @param {import('./wu-ai-conversation.js').WuAIConversation} deps.conversation
12605
+ * @param {import('./wu-ai-context.js').WuAIContext} deps.context
12606
+ * @param {import('./wu-ai-permissions.js').WuAIPermissions} deps.permissions
12607
+ * @param {object} deps.eventBus
12608
+ */
12609
+ constructor({ actions, conversation, context, permissions, eventBus, agent, store }) {
12610
+ this._actions = actions;
12611
+ this._conversation = conversation;
12612
+ this._context = context;
12613
+ this._permissions = permissions;
12614
+ this._eventBus = eventBus;
12615
+ this._agent = agent; // WuAIAgent — for workflow execution
12616
+ this._store = store; // WuStore — for deterministic setState steps
12617
+
12618
+ // appName → Map<actionName, { description, qualifiedName }>
12619
+ this._capabilities = new Map();
12620
+
12621
+ // name → { goal, steps, parameters, provider, ... }
12622
+ this._workflows = new Map();
12623
+
12624
+ this._config = {
12625
+ defaultProvider: null,
12626
+ defaultTemperature: 0.3, // lower temp for orchestration
12627
+ };
12628
+
12629
+ this._stats = {
12630
+ totalIntents: 0,
12631
+ resolvedIntents: 0,
12632
+ failedIntents: 0,
12633
+ workflowsRegistered: 0,
12634
+ workflowsExecuted: 0,
12635
+ };
12636
+ }
12637
+
12638
+ /**
12639
+ * Post-init configuration.
12640
+ *
12641
+ * @param {object} config
12642
+ * @param {string} [config.defaultProvider] - Default provider for intents
12643
+ * @param {number} [config.defaultTemperature] - Default temperature for intents
12644
+ */
12645
+ configure(config) {
12646
+ if (config.defaultProvider !== undefined) this._config.defaultProvider = config.defaultProvider;
12647
+ if (config.defaultTemperature !== undefined) this._config.defaultTemperature = config.defaultTemperature;
12648
+ }
12649
+
12650
+ // ─── Capability Registration ────────────────────────────────────
12651
+
12652
+ /**
12653
+ * Register a capability scoped to a micro-app.
12654
+ *
12655
+ * Under the hood this registers a normal action with the qualified
12656
+ * name 'appName:actionName' so the LLM can call it directly.
12657
+ * The capability map is tracked separately for lifecycle management
12658
+ * (removeApp) and system prompt enrichment.
12659
+ *
12660
+ * @param {string} appName - The micro-app name (e.g., 'orders', 'dashboard')
12661
+ * @param {string} actionName - The capability name (e.g., 'getRecent', 'updateKPIs')
12662
+ * @param {object} config - Same as wu.ai.action() config:
12663
+ * { description, parameters, handler, confirm?, permissions?, dangerous? }
12664
+ */
12665
+ register(appName, actionName, config) {
12666
+ if (!appName || !actionName) {
12667
+ throw new Error('[wu-ai] capability() requires both appName and actionName');
12668
+ }
12669
+ if (!config || typeof config.handler !== 'function') {
12670
+ throw new Error(`[wu-ai] capability '${appName}:${actionName}' must have a handler function`);
12671
+ }
12672
+
12673
+ const qualifiedName = `${appName}:${actionName}`;
12674
+
12675
+ // Track in capability map
12676
+ if (!this._capabilities.has(appName)) {
12677
+ this._capabilities.set(appName, new Map());
12678
+ }
12679
+ this._capabilities.get(appName).set(actionName, {
12680
+ description: config.description || actionName,
12681
+ qualifiedName,
12682
+ });
12683
+
12684
+ // Register as a normal action with enriched description
12685
+ this._actions.register(qualifiedName, {
12686
+ ...config,
12687
+ description: `[${appName}] ${config.description || actionName}`,
12688
+ });
12689
+
12690
+ logger.wuDebug(`[wu-ai] Capability registered: ${qualifiedName}`);
12691
+ }
12692
+
12693
+ /**
12694
+ * Remove a single capability.
12695
+ *
12696
+ * @param {string} appName
12697
+ * @param {string} actionName
12698
+ */
12699
+ unregister(appName, actionName) {
12700
+ const appCaps = this._capabilities.get(appName);
12701
+ if (!appCaps) return;
12702
+
12703
+ const qualifiedName = `${appName}:${actionName}`;
12704
+ appCaps.delete(actionName);
12705
+ this._actions.unregister(qualifiedName);
12706
+
12707
+ // Clean up empty app entry
12708
+ if (appCaps.size === 0) {
12709
+ this._capabilities.delete(appName);
12710
+ }
12711
+ }
12712
+
12713
+ /**
12714
+ * Remove all capabilities for a micro-app (unmount cleanup).
12715
+ *
12716
+ * Call this when a micro-app is unmounted to prevent stale
12717
+ * capabilities from appearing in the AI's capability map.
12718
+ *
12719
+ * @param {string} appName
12720
+ * @returns {number} Number of capabilities removed
12721
+ */
12722
+ removeApp(appName) {
12723
+ const appCaps = this._capabilities.get(appName);
12724
+ if (!appCaps) return 0;
12725
+
12726
+ let removed = 0;
12727
+ for (const [actionName] of appCaps) {
12728
+ const qualifiedName = `${appName}:${actionName}`;
12729
+ this._actions.unregister(qualifiedName);
12730
+ removed++;
12731
+ }
12732
+
12733
+ this._capabilities.delete(appName);
12734
+
12735
+ logger.wuDebug(`[wu-ai] All capabilities removed for app '${appName}' (${removed})`);
12736
+
12737
+ this._eventBus.emit('ai:app:removed', {
12738
+ appName,
12739
+ capabilitiesRemoved: removed,
12740
+ }, { appName: 'wu-ai' });
12741
+
12742
+ return removed;
12743
+ }
12744
+
12745
+ // ─── Capability Map ─────────────────────────────────────────────
12746
+
12747
+ /**
12748
+ * Get the full capability map grouped by app.
12749
+ *
12750
+ * Used for system prompt enrichment and debugging.
12751
+ *
12752
+ * @returns {object} { appName: [{ action, description }], ... }
12753
+ */
12754
+ getCapabilityMap() {
12755
+ const map = {};
12756
+ for (const [appName, actions] of this._capabilities) {
12757
+ map[appName] = [];
12758
+ for (const [, meta] of actions) {
12759
+ map[appName].push({
12760
+ action: meta.qualifiedName,
12761
+ description: meta.description,
12762
+ });
12763
+ }
12764
+ }
12765
+ return map;
12766
+ }
12767
+
12768
+ /**
12769
+ * Get app names that have registered capabilities.
12770
+ *
12771
+ * @returns {string[]}
12772
+ */
12773
+ getRegisteredApps() {
12774
+ return [...this._capabilities.keys()];
12775
+ }
12776
+
12777
+ /**
12778
+ * Check if an app has registered capabilities.
12779
+ *
12780
+ * @param {string} appName
12781
+ * @returns {boolean}
12782
+ */
12783
+ hasApp(appName) {
12784
+ return this._capabilities.has(appName);
12785
+ }
12786
+
12787
+ /**
12788
+ * Get the total number of registered capabilities across all apps.
12789
+ *
12790
+ * @returns {number}
12791
+ */
12792
+ getTotalCapabilities() {
12793
+ let count = 0;
12794
+ for (const actions of this._capabilities.values()) {
12795
+ count += actions.size;
12796
+ }
12797
+ return count;
12798
+ }
12799
+
12800
+ // ─── Intent Resolution ──────────────────────────────────────────
12801
+
12802
+ /**
12803
+ * Resolve a cross-app intent in a single conversation turn.
12804
+ *
12805
+ * The AI receives:
12806
+ * - The full capability map (what each app can do)
12807
+ * - Current application state (via context)
12808
+ * - Mounted apps list
12809
+ * - All registered tools (capabilities are tools)
12810
+ *
12811
+ * Unlike agent(), this is NOT a multi-step autonomous loop.
12812
+ * The LLM resolves the intent in one logical request (which may
12813
+ * include multiple tool calls within the conversation's tool-call
12814
+ * loop, but conceptually is a single turn).
12815
+ *
12816
+ * Unlike send(), the namespace is ephemeral and auto-cleaned,
12817
+ * and the system prompt is auto-built with the capability map.
12818
+ *
12819
+ * @param {string} description - Natural language intent
12820
+ * e.g., "Show me the top customer by order count"
12821
+ * e.g., "Update dashboard stats and notify the topbar"
12822
+ * @param {object} [options]
12823
+ * @param {string[]} [options.plan] - Optional action sequence hint.
12824
+ * The AI uses this as guidance but can deviate if needed.
12825
+ * e.g., ['orders:getRecent', 'customers:lookup']
12826
+ * @param {string} [options.provider] - LLM provider override
12827
+ * @param {number} [options.temperature] - Temperature override
12828
+ * @param {number} [options.maxTokens] - Max tokens override
12829
+ * @param {AbortSignal} [options.signal] - Abort signal
12830
+ * @param {string|object} [options.responseFormat] - Response format
12831
+ * @returns {Promise<{
12832
+ * content: string,
12833
+ * tool_results: Array,
12834
+ * usage: object|null,
12835
+ * resolved: boolean,
12836
+ * appsInvolved: string[]
12837
+ * }>}
12838
+ */
12839
+ async resolve(description, options = {}) {
12840
+ if (!description || typeof description !== 'string') {
12841
+ throw new Error('[wu-ai] intent() requires a description string');
12842
+ }
12843
+
12844
+ this._stats.totalIntents++;
12845
+ const namespace = this._generateNamespace();
12846
+
12847
+ // Collect fresh context before building the prompt
12848
+ if (this._context) {
12849
+ try {
12850
+ await this._context.collect();
12851
+ } catch {
12852
+ // Context collection is best-effort
12853
+ }
12854
+ }
12855
+
12856
+ const systemPrompt = this._buildOrchestratorPrompt(options);
12857
+
12858
+ this._eventBus.emit('ai:intent:start', {
12859
+ description: description.slice(0, 200),
12860
+ namespace,
12861
+ capabilities: this.getTotalCapabilities(),
12862
+ }, { appName: 'wu-ai' });
12863
+
12864
+ try {
12865
+ const response = await this._conversation.send(description, {
12866
+ namespace,
12867
+ systemPrompt,
12868
+ provider: options.provider || this._config.defaultProvider,
12869
+ temperature: options.temperature ?? this._config.defaultTemperature,
12870
+ maxTokens: options.maxTokens,
12871
+ signal: options.signal,
12872
+ responseFormat: options.responseFormat,
12873
+ });
12874
+
12875
+ const toolResults = response.tool_results || [];
12876
+ const appsInvolved = this._extractInvolvedApps(toolResults);
12877
+ const resolved = !!(response.content);
12878
+
12879
+ if (resolved) {
12880
+ this._stats.resolvedIntents++;
12881
+ } else {
12882
+ this._stats.failedIntents++;
12883
+ }
12884
+
12885
+ const result = {
12886
+ content: response.content || '',
12887
+ tool_results: toolResults,
12888
+ usage: response.usage || null,
12889
+ resolved,
12890
+ appsInvolved,
12891
+ };
12892
+
12893
+ this._eventBus.emit('ai:intent:resolved', {
12894
+ description: description.slice(0, 200),
12895
+ resolved,
12896
+ appsInvolved,
12897
+ }, { appName: 'wu-ai' });
12898
+
12899
+ return result;
12900
+ } catch (err) {
12901
+ this._stats.failedIntents++;
12902
+
12903
+ this._eventBus.emit('ai:intent:error', {
12904
+ description: description.slice(0, 200),
12905
+ error: err.message,
12906
+ }, { appName: 'wu-ai' });
12907
+
12908
+ throw err;
12909
+ } finally {
12910
+ // Always clean up the ephemeral namespace
12911
+ this._conversation.deleteNamespace(namespace);
12912
+ }
12913
+ }
12914
+
12915
+ // ─── System Prompt Builder ──────────────────────────────────────
12916
+
12917
+ /**
12918
+ * Build the orchestrator system prompt with full capability map.
12919
+ *
12920
+ * This method is also available to other modules (triggers, agents)
12921
+ * that want capability-aware system prompts.
12922
+ *
12923
+ * @param {object} [options]
12924
+ * @param {string[]} [options.plan] - Optional action sequence hint
12925
+ * @returns {string}
12926
+ */
12927
+ buildOrchestratorPrompt(options = {}) {
12928
+ return this._buildOrchestratorPrompt(options);
12929
+ }
12930
+
12931
+ /** @private */
12932
+ _buildOrchestratorPrompt(options = {}) {
12933
+ const parts = [];
12934
+
12935
+ parts.push(
12936
+ 'You are an AI orchestrator for a microfrontend application.',
12937
+ 'Multiple independent apps are mounted, each with specific capabilities.',
12938
+ 'Resolve cross-app requests by calling the right capabilities in the right order.',
12939
+ '',
12940
+ 'RULES:',
12941
+ '- Call capabilities (tools) to gather data or trigger actions.',
12942
+ '- You may call multiple capabilities from different apps if needed.',
12943
+ '- Synthesize results into a clear, actionable response.',
12944
+ '- If a required app is not available or lacks a capability, explain what is missing.',
12945
+ '',
12946
+ );
12947
+
12948
+ // Capability map
12949
+ const capMap = this.getCapabilityMap();
12950
+ const appNames = Object.keys(capMap);
12951
+
12952
+ if (appNames.length > 0) {
12953
+ parts.push('CAPABILITY MAP:');
12954
+ for (const appName of appNames) {
12955
+ parts.push(` ${appName}:`);
12956
+ for (const cap of capMap[appName]) {
12957
+ parts.push(` - ${cap.action}: ${cap.description}`);
12958
+ }
12959
+ }
12960
+ parts.push('');
12961
+ } else {
12962
+ parts.push(
12963
+ 'NOTE: No app capabilities are registered. Answer based on available context only.',
12964
+ '',
12965
+ );
12966
+ }
12967
+
12968
+ // Optional plan hint
12969
+ if (options.plan && options.plan.length > 0) {
12970
+ parts.push(
12971
+ 'SUGGESTED PLAN (follow this unless a better approach is evident):',
12972
+ );
12973
+ for (let i = 0; i < options.plan.length; i++) {
12974
+ parts.push(` ${i + 1}. ${options.plan[i]}`);
12975
+ }
12976
+ parts.push('');
12977
+ }
12978
+
12979
+ // Context snapshot (state, mounted apps)
12980
+ const snapshot = this._context?.getSnapshot();
12981
+ if (snapshot?._mountedApps?.length) {
12982
+ parts.push(`MOUNTED APPS: ${snapshot._mountedApps.join(', ')}`, '');
12983
+ }
12984
+ if (snapshot?._store && Object.keys(snapshot._store).length > 0) {
12985
+ parts.push(
12986
+ 'CURRENT STATE:',
12987
+ JSON.stringify(snapshot._store, null, 2),
12988
+ '',
12989
+ );
12990
+ }
12991
+
12992
+ return parts.join('\n');
12993
+ }
12994
+
12995
+ // ─── Workflows ─────────────────────────────────────────────────
12996
+
12997
+ /**
12998
+ * Register a reusable AI workflow.
12999
+ *
13000
+ * A workflow is a named, parameterized recipe that the AI agent
13001
+ * follows step by step. Think of it as a macro: you define it once,
13002
+ * then run it whenever you need with different parameters.
13003
+ *
13004
+ * The AI receives the steps as instructions and uses browser actions
13005
+ * (screenshot, click, type) plus any registered capabilities/actions
13006
+ * to execute them. You can watch every step in real time.
13007
+ *
13008
+ * @param {string} name - Workflow name (e.g., 'register-user')
13009
+ * @param {object} config
13010
+ * @param {string} config.description - What this workflow does
13011
+ * @param {string[]} config.steps - Step-by-step instructions for the AI
13012
+ * @param {object} [config.parameters] - Parameter definitions for interpolation
13013
+ * e.g., { name: { type: 'string', required: true }, email: { type: 'string' } }
13014
+ * @param {number} [config.maxSteps=15] - Max agent steps allowed
13015
+ * @param {string} [config.provider] - LLM provider to use
13016
+ * @param {number} [config.temperature] - Temperature (default: 0.2 for precision)
13017
+ *
13018
+ * @example
13019
+ * // ── AI Mode (default): steps are natural language ──
13020
+ * wu.ai.workflow('register-user', {
13021
+ * description: 'Register a new user in the system',
13022
+ * steps: [
13023
+ * 'Navigate to the Customers section',
13024
+ * 'Click the "Add Customer" button',
13025
+ * 'Fill in the name field with {{name}}',
13026
+ * 'Click Submit',
13027
+ * ],
13028
+ * parameters: { name: { type: 'string', required: true } },
13029
+ * });
13030
+ *
13031
+ * // ── Deterministic Mode: steps are exact actions, NO AI NEEDED ──
13032
+ * wu.ai.workflow('register-user', {
13033
+ * mode: 'deterministic',
13034
+ * description: 'Register a new user',
13035
+ * steps: [
13036
+ * { action: 'navigate', section: 'customers' },
13037
+ * { action: 'click', selector: '#add-customer-btn' },
13038
+ * { action: 'type', selector: '#name', value: '{{name}}' },
13039
+ * { action: 'type', selector: '#email', value: '{{email}}' },
13040
+ * { action: 'click', selector: '#submit-btn' },
13041
+ * { action: 'wait', selector: '.success-message', timeout: 5000 },
13042
+ * ],
13043
+ * parameters: {
13044
+ * name: { type: 'string', required: true },
13045
+ * email: { type: 'string', required: true },
13046
+ * },
13047
+ * });
13048
+ */
13049
+ registerWorkflow(name, config) {
13050
+ if (!name) {
13051
+ throw new Error('[wu-ai] workflow() requires a name');
13052
+ }
13053
+ if (!config || !config.steps || !Array.isArray(config.steps) || config.steps.length === 0) {
13054
+ throw new Error(`[wu-ai] workflow '${name}' must have a non-empty steps array`);
13055
+ }
13056
+
13057
+ // Detect mode: if steps are objects with 'action', it's deterministic
13058
+ const mode = config.mode || (
13059
+ config.steps.length > 0 && typeof config.steps[0] === 'object' && config.steps[0].action
13060
+ ? 'deterministic'
13061
+ : 'ai'
13062
+ );
13063
+
13064
+ this._workflows.set(name, {
13065
+ description: config.description || name,
13066
+ steps: config.steps,
13067
+ mode,
13068
+ parameters: config.parameters || {},
13069
+ maxSteps: config.maxSteps ?? 15,
13070
+ provider: config.provider || null,
13071
+ temperature: config.temperature ?? 0.2,
13072
+ });
13073
+
13074
+ this._stats.workflowsRegistered++;
13075
+
13076
+ logger.wuDebug(`[wu-ai] Workflow registered: '${name}' (${config.steps.length} steps)`);
13077
+ }
13078
+
13079
+ /**
13080
+ * Execute a registered workflow with parameters.
13081
+ *
13082
+ * Returns an async generator (like agent) — you iterate over it
13083
+ * to observe each step in real time.
13084
+ *
13085
+ * @param {string} name - Workflow name
13086
+ * @param {object} [params={}] - Parameters to interpolate into steps
13087
+ * e.g., { name: 'Juan Pérez', email: 'juan@test.com' }
13088
+ * @param {object} [options={}]
13089
+ * @param {Function} [options.onStep] - Callback per step
13090
+ * @param {Function} [options.shouldContinue] - Human-in-the-loop gate
13091
+ * @param {AbortSignal} [options.signal] - Abort signal
13092
+ * @returns {AsyncGenerator<AgentStepResult>}
13093
+ *
13094
+ * @example
13095
+ * for await (const step of wu.ai.runWorkflow('register-user', {
13096
+ * name: 'Juan Pérez',
13097
+ * email: 'juan@test.com',
13098
+ * })) {
13099
+ * console.log(`Paso ${step.step}: ${step.content}`);
13100
+ * if (step.type === 'done') console.log('Workflow completado!');
13101
+ * }
13102
+ *
13103
+ * // With human approval per step:
13104
+ * for await (const step of wu.ai.runWorkflow('register-user', params, {
13105
+ * shouldContinue: (step) => confirm(`¿Continuar? ${step.content?.slice(0, 60)}`),
13106
+ * })) {
13107
+ * renderStep(step);
13108
+ * }
13109
+ */
13110
+ async *executeWorkflow(name, params = {}, options = {}) {
13111
+ const workflow = this._workflows.get(name);
13112
+ if (!workflow) {
13113
+ throw new Error(`[wu-ai] Workflow '${name}' is not registered`);
13114
+ }
13115
+
13116
+ // Validate required parameters
13117
+ for (const [paramName, paramConfig] of Object.entries(workflow.parameters)) {
13118
+ if (paramConfig.required && (params[paramName] === undefined || params[paramName] === null)) {
13119
+ throw new Error(`[wu-ai] Workflow '${name}' requires parameter '${paramName}'`);
13120
+ }
13121
+ }
13122
+
13123
+ this._stats.workflowsExecuted++;
13124
+
13125
+ // Branch on mode
13126
+ if (workflow.mode === 'deterministic') {
13127
+ yield* this._executeDeterministic(name, workflow, params, options);
13128
+ } else {
13129
+ yield* this._executeWithAgent(name, workflow, params, options);
13130
+ }
13131
+ }
13132
+
13133
+ /**
13134
+ * Execute workflow using the AI agent (natural language steps).
13135
+ * @private
13136
+ */
13137
+ async *_executeWithAgent(name, workflow, params, options) {
13138
+ if (!this._agent) {
13139
+ throw new Error('[wu-ai] Agent module not available for workflow execution');
13140
+ }
13141
+
13142
+ // Interpolate parameters into string steps
13143
+ const interpolatedSteps = workflow.steps.map(step => {
13144
+ let result = step;
13145
+ for (const [key, value] of Object.entries(params)) {
13146
+ result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
13147
+ }
13148
+ return result;
13149
+ });
13150
+
13151
+ const goal = this._buildWorkflowGoal(workflow, interpolatedSteps, params);
13152
+
13153
+ this._eventBus.emit('ai:workflow:start', {
13154
+ workflow: name,
13155
+ mode: 'ai',
13156
+ params,
13157
+ steps: interpolatedSteps.length,
13158
+ }, { appName: 'wu-ai' });
13159
+
13160
+ let finalStep = null;
13161
+
13162
+ try {
13163
+ yield* this._agent.run(goal, {
13164
+ maxSteps: workflow.maxSteps,
13165
+ provider: options.provider || workflow.provider || this._config.defaultProvider,
13166
+ temperature: workflow.temperature ?? 0.2,
13167
+ onStep: (step) => {
13168
+ finalStep = step;
13169
+ if (options.onStep) options.onStep(step);
13170
+ },
13171
+ shouldContinue: options.shouldContinue,
13172
+ signal: options.signal,
13173
+ });
13174
+ } finally {
13175
+ this._eventBus.emit('ai:workflow:done', {
13176
+ workflow: name,
13177
+ params,
13178
+ totalSteps: finalStep?.step || 0,
13179
+ result: finalStep?.type || 'unknown',
13180
+ }, { appName: 'wu-ai' });
13181
+ }
13182
+ }
13183
+
13184
+ /**
13185
+ * Execute workflow deterministically — NO AI NEEDED.
13186
+ * Steps are exact actions: { action: 'click', selector: '#btn' }
13187
+ * @private
13188
+ */
13189
+ async *_executeDeterministic(name, workflow, params, options) {
13190
+ // Interpolate parameters into step values
13191
+ const steps = workflow.steps.map(step => {
13192
+ const interpolated = { ...step };
13193
+ for (const [key, value] of Object.entries(params)) {
13194
+ const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
13195
+ for (const field of ['value', 'selector', 'text', 'section', 'event', 'path']) {
13196
+ if (typeof interpolated[field] === 'string') {
13197
+ interpolated[field] = interpolated[field].replace(pattern, String(value));
13198
+ }
13199
+ }
13200
+ }
13201
+ return interpolated;
13202
+ });
13203
+
13204
+ this._eventBus?.emit('ai:workflow:start', {
13205
+ workflow: name,
13206
+ mode: 'deterministic',
13207
+ params,
13208
+ steps: steps.length,
13209
+ }, { appName: 'wu-ai' });
13210
+
13211
+ let lastStep = null;
13212
+
13213
+ try {
13214
+ for (let i = 0; i < steps.length; i++) {
13215
+ const step = steps[i];
13216
+ const stepNum = i + 1;
13217
+ const startTime = Date.now();
13218
+
13219
+ // Check abort
13220
+ if (options.signal?.aborted) {
13221
+ const result = {
13222
+ step: stepNum,
13223
+ type: 'aborted',
13224
+ content: 'Workflow aborted',
13225
+ reason: 'Aborted by caller',
13226
+ elapsed: 0,
13227
+ };
13228
+ lastStep = result;
13229
+ yield result;
13230
+ return;
13231
+ }
13232
+
13233
+ // Handle 'wait' specially — it's async
13234
+ if (step.action === 'wait') {
13235
+ if (step.selector) {
13236
+ const found = await waitForSelector(step.selector, step.timeout || 5000);
13237
+ const elapsed = Date.now() - startTime;
13238
+ const result = {
13239
+ step: stepNum,
13240
+ type: found ? 'action' : 'error',
13241
+ content: found
13242
+ ? `Waited for "${step.selector}" — found`
13243
+ : `Timeout waiting for "${step.selector}"`,
13244
+ elapsed,
13245
+ };
13246
+ lastStep = result;
13247
+ if (options.onStep) options.onStep(result);
13248
+ yield result;
13249
+ if (!found) return; // stop on timeout
13250
+ } else if (step.ms) {
13251
+ await delay(step.ms);
13252
+ const result = {
13253
+ step: stepNum,
13254
+ type: 'action',
13255
+ content: `Waited ${step.ms}ms`,
13256
+ elapsed: step.ms,
13257
+ };
13258
+ lastStep = result;
13259
+ if (options.onStep) options.onStep(result);
13260
+ yield result;
13261
+ }
13262
+ continue;
13263
+ }
13264
+
13265
+ // Execute the step
13266
+ const execResult = executeDeterministicStep(step, this._eventBus, this._store);
13267
+ const elapsed = Date.now() - startTime;
13268
+
13269
+ const stepResult = {
13270
+ step: stepNum,
13271
+ type: execResult.success ? 'action' : 'error',
13272
+ content: execResult.success ? execResult.detail : execResult.error,
13273
+ elapsed,
13274
+ };
13275
+
13276
+ lastStep = stepResult;
13277
+ if (options.onStep) options.onStep(stepResult);
13278
+ yield stepResult;
13279
+
13280
+ // Human-in-the-loop gate
13281
+ if (options.shouldContinue) {
13282
+ let shouldGo;
13283
+ try {
13284
+ shouldGo = await options.shouldContinue(stepResult);
13285
+ } catch {
13286
+ shouldGo = false;
13287
+ }
13288
+ if (!shouldGo) {
13289
+ const interrupted = {
13290
+ step: stepNum,
13291
+ type: 'interrupted',
13292
+ content: 'Stopped by user',
13293
+ reason: 'shouldContinue returned false',
13294
+ elapsed: 0,
13295
+ };
13296
+ lastStep = interrupted;
13297
+ yield interrupted;
13298
+ return;
13299
+ }
13300
+ }
13301
+
13302
+ // Stop on error
13303
+ if (!execResult.success) return;
13304
+
13305
+ // Small delay between steps for UI to update
13306
+ if (i < steps.length - 1) {
13307
+ await delay(step.delay ?? 200);
13308
+ }
13309
+ }
13310
+
13311
+ // All steps completed
13312
+ const done = {
13313
+ step: steps.length,
13314
+ type: 'done',
13315
+ content: `Workflow "${name}" completed (${steps.length} steps)`,
13316
+ reason: 'All steps executed',
13317
+ elapsed: 0,
13318
+ };
13319
+ lastStep = done;
13320
+ if (options.onStep) options.onStep(done);
13321
+ yield done;
13322
+
13323
+ } finally {
13324
+ this._eventBus?.emit('ai:workflow:done', {
13325
+ workflow: name,
13326
+ mode: 'deterministic',
13327
+ params,
13328
+ totalSteps: lastStep?.step || 0,
13329
+ result: lastStep?.type || 'unknown',
13330
+ }, { appName: 'wu-ai' });
13331
+ }
13332
+ }
13333
+
13334
+ /**
13335
+ * Check if a workflow is registered.
13336
+ *
13337
+ * @param {string} name
13338
+ * @returns {boolean}
13339
+ */
13340
+ hasWorkflow(name) {
13341
+ return this._workflows.has(name);
13342
+ }
13343
+
13344
+ /**
13345
+ * Get a workflow definition.
13346
+ *
13347
+ * @param {string} name
13348
+ * @returns {object|null}
13349
+ */
13350
+ getWorkflow(name) {
13351
+ const w = this._workflows.get(name);
13352
+ if (!w) return null;
13353
+ return {
13354
+ description: w.description,
13355
+ steps: [...w.steps],
13356
+ mode: w.mode,
13357
+ parameters: { ...w.parameters },
13358
+ maxSteps: w.maxSteps,
13359
+ };
13360
+ }
13361
+
13362
+ /**
13363
+ * Remove a registered workflow.
13364
+ *
13365
+ * @param {string} name
13366
+ */
13367
+ removeWorkflow(name) {
13368
+ this._workflows.delete(name);
13369
+ }
13370
+
13371
+ /**
13372
+ * Get all workflow names.
13373
+ *
13374
+ * @returns {string[]}
13375
+ */
13376
+ getWorkflowNames() {
13377
+ return [...this._workflows.keys()];
13378
+ }
13379
+
13380
+ /** @private */
13381
+ _buildWorkflowGoal(workflow, steps, params) {
13382
+ const parts = [];
13383
+
13384
+ parts.push(
13385
+ `WORKFLOW: ${workflow.description}`,
13386
+ '',
13387
+ 'You must follow these steps IN ORDER. Use browser tools (screenshot, click, type)',
13388
+ 'to interact with the application. After each step, take a screenshot to verify.',
13389
+ '',
13390
+ 'STEPS:',
13391
+ );
13392
+
13393
+ for (let i = 0; i < steps.length; i++) {
13394
+ parts.push(` ${i + 1}. ${steps[i]}`);
13395
+ }
13396
+
13397
+ parts.push(
13398
+ '',
13399
+ 'After completing all steps successfully, respond with [DONE].',
13400
+ 'If a step fails, explain what went wrong.',
13401
+ );
13402
+
13403
+ // Add capability context if available
13404
+ const capMap = this.getCapabilityMap();
13405
+ const appNames = Object.keys(capMap);
13406
+ if (appNames.length > 0) {
13407
+ parts.push('', 'AVAILABLE APP CAPABILITIES:');
13408
+ for (const appName of appNames) {
13409
+ for (const cap of capMap[appName]) {
13410
+ parts.push(` - ${cap.action}: ${cap.description}`);
13411
+ }
13412
+ }
13413
+ }
13414
+
13415
+ return parts.join('\n');
13416
+ }
13417
+
13418
+ // ─── Stats & Lifecycle ──────────────────────────────────────────
13419
+
13420
+ getStats() {
13421
+ return {
13422
+ ...this._stats,
13423
+ registeredApps: this.getRegisteredApps(),
13424
+ totalCapabilities: this.getTotalCapabilities(),
13425
+ capabilityMap: this.getCapabilityMap(),
13426
+ workflows: this.getWorkflowNames(),
13427
+ config: { ...this._config },
13428
+ };
13429
+ }
13430
+
13431
+ destroy() {
13432
+ // Remove all capabilities from the action registry
13433
+ for (const [appName, actions] of this._capabilities) {
13434
+ for (const [actionName] of actions) {
13435
+ this._actions.unregister(`${appName}:${actionName}`);
13436
+ }
13437
+ }
13438
+ this._capabilities.clear();
13439
+ this._workflows.clear();
13440
+
13441
+ this._stats = {
13442
+ totalIntents: 0,
13443
+ resolvedIntents: 0,
13444
+ failedIntents: 0,
13445
+ workflowsRegistered: 0,
13446
+ workflowsExecuted: 0,
13447
+ };
13448
+ }
13449
+
13450
+ // ─── Private Helpers ────────────────────────────────────────────
13451
+
13452
+ /**
13453
+ * Extract app names from tool results based on qualified action names.
13454
+ * e.g., tool name 'orders:getRecent' → app 'orders'
13455
+ */
13456
+ _extractInvolvedApps(toolResults) {
13457
+ if (!toolResults || !Array.isArray(toolResults)) return [];
13458
+ const apps = new Set();
13459
+ for (const result of toolResults) {
13460
+ const name = result.name || result.tool || '';
13461
+ const colonIdx = name.indexOf(':');
13462
+ if (colonIdx > 0) {
13463
+ apps.add(name.slice(0, colonIdx));
13464
+ }
13465
+ }
13466
+ return [...apps];
13467
+ }
13468
+
13469
+ _generateNamespace() {
13470
+ return INTENT_NAMESPACE_PREFIX + Date.now().toString(36) +
13471
+ '_' + Math.random().toString(36).slice(2, 6);
13472
+ }
13473
+ }
13474
+
13475
+ /**
13476
+ * WU-AI Browser Actions
13477
+ *
13478
+ * Registers browser automation tools into wu.ai so any LLM provider
13479
+ * (OpenAI, Claude, Gemini, Ollama, etc.) can autonomously see and
13480
+ * control the page — no human intervention required.
13481
+ *
13482
+ * Tools registered:
13483
+ * browser_screenshot — Capture page/element as PNG (Canvas API)
13484
+ * browser_click — Click element by selector or visible text
13485
+ * browser_type — Type into inputs (React/Vue/framework compatible)
13486
+ * browser_snapshot — Get accessibility tree of the DOM
13487
+ * browser_navigate — Navigate SPA routes
13488
+ * browser_network — View captured HTTP requests (fetch + XHR)
13489
+ * browser_console — View captured console messages
13490
+ * browser_info — Get page state: apps, store, URL, viewport
13491
+ * browser_select — Select option in dropdowns
13492
+ * browser_scroll — Scroll page or element
13493
+ *
13494
+ * @example
13495
+ * // Auto-registered when wu.ai initializes
13496
+ * // Any LLM connected via wu.ai.provider can now use these tools:
13497
+ * const tools = wu.ai.tools();
13498
+ * // → includes browser_screenshot, browser_click, etc.
13499
+ */
13500
+
13501
+
13502
+ /**
13503
+ * Register all browser automation actions into a WuAI instance.
13504
+ *
11406
13505
  * @param {object} ai - The WuAI instance (wu.ai)
11407
13506
  * @param {object} wu - The Wu Framework instance (window.wu)
11408
13507
  */
11409
13508
  function registerBrowserActions(ai, wu) {
11410
- // Install interceptors only once
11411
- if (!interceptorsInstalled) {
11412
- _installNetworkInterceptor();
11413
- _installConsoleInterceptor();
11414
- interceptorsInstalled = true;
11415
- }
13509
+ ensureInterceptors();
11416
13510
 
11417
13511
  // ════════════════════════════════════════════
11418
13512
  // SCREENSHOT — Canvas API (SVG foreignObject)
@@ -11427,66 +13521,7 @@ function registerBrowserActions(ai, wu) {
11427
13521
  required: false,
11428
13522
  },
11429
13523
  },
11430
- handler: async (params) => {
11431
- const target = params.selector
11432
- ? document.querySelector(params.selector)
11433
- : document.documentElement;
11434
-
11435
- if (!target) return { error: `Element not found: ${params.selector}` };
11436
-
11437
- const rect = target.getBoundingClientRect();
11438
- const w = Math.ceil(Math.min(rect.width || window.innerWidth, 1920));
11439
- const h = Math.ceil(Math.min(rect.height || window.innerHeight, 1080));
11440
-
11441
- // Clone and inline styles for accurate rendering
11442
- const clone = target.cloneNode(true);
11443
- _inlineComputedStyles(target, clone);
11444
-
11445
- const serializer = new XMLSerializer();
11446
- const xhtml = serializer.serializeToString(clone);
11447
-
11448
- const svgStr = [
11449
- `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">`,
11450
- '<foreignObject width="100%" height="100%">',
11451
- `<div xmlns="http://www.w3.org/1999/xhtml" style="width:${w}px;height:${h}px;overflow:hidden;">`,
11452
- xhtml,
11453
- '</div>',
11454
- '</foreignObject>',
11455
- '</svg>',
11456
- ].join('');
11457
-
11458
- const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
11459
- const url = URL.createObjectURL(svgBlob);
11460
-
11461
- const dataUrl = await new Promise((resolve) => {
11462
- const img = new Image();
11463
- img.onload = () => {
11464
- const canvas = document.createElement('canvas');
11465
- canvas.width = w;
11466
- canvas.height = h;
11467
- const ctx = canvas.getContext('2d');
11468
- ctx.drawImage(img, 0, 0);
11469
- URL.revokeObjectURL(url);
11470
- resolve(canvas.toDataURL('image/png', 0.8));
11471
- };
11472
- img.onerror = () => {
11473
- URL.revokeObjectURL(url);
11474
- resolve(null);
11475
- };
11476
- img.src = url;
11477
- });
11478
-
11479
- if (!dataUrl) return { error: 'Canvas rendering failed' };
11480
-
11481
- const base64 = dataUrl.split(',')[1];
11482
- return {
11483
- width: w,
11484
- height: h,
11485
- format: 'png',
11486
- base64,
11487
- sizeKB: Math.round((base64.length * 3) / 4 / 1024),
11488
- };
11489
- },
13524
+ handler: async (params) => captureScreenshot(params.selector),
11490
13525
  permissions: [],
11491
13526
  });
11492
13527
 
@@ -11509,38 +13544,11 @@ function registerBrowserActions(ai, wu) {
11509
13544
  },
11510
13545
  },
11511
13546
  handler: async (params, api) => {
11512
- let el = null;
11513
-
11514
- if (params.selector) {
11515
- el = document.querySelector(params.selector);
11516
- }
11517
-
11518
- if (!el && params.text) {
11519
- const candidates = document.querySelectorAll(
11520
- 'button, a, [role="button"], input[type="submit"], input[type="button"], [data-click], label, [onclick]'
11521
- );
11522
- const searchText = params.text.toLowerCase();
11523
- for (const candidate of candidates) {
11524
- if (candidate.textContent?.trim().toLowerCase().includes(searchText)) {
11525
- el = candidate;
11526
- break;
11527
- }
11528
- }
13547
+ const result = clickElement(params.selector, params.text);
13548
+ if (!result.error) {
13549
+ api.emit?.('browser:clicked', { selector: params.selector, text: params.text });
11529
13550
  }
11530
-
11531
- if (!el) return { error: `Element not found: ${params.selector || `text="${params.text}"`}` };
11532
-
11533
- el.scrollIntoView({ behavior: 'instant', block: 'center' });
11534
- el.click();
11535
-
11536
- const tag = el.tagName?.toLowerCase();
11537
- const id = el.id ? `#${el.id}` : '';
11538
- api.emit?.('browser:clicked', { selector: params.selector, text: params.text });
11539
-
11540
- return {
11541
- clicked: `${tag}${id}`,
11542
- text: el.textContent?.trim().slice(0, 100) || '',
11543
- };
13551
+ return result;
11544
13552
  },
11545
13553
  permissions: ['emitEvents'],
11546
13554
  });
@@ -11574,50 +13582,14 @@ function registerBrowserActions(ai, wu) {
11574
13582
  },
11575
13583
  },
11576
13584
  handler: async (params, api) => {
11577
- const el = document.querySelector(params.selector);
11578
- if (!el) return { error: `Element not found: ${params.selector}` };
11579
-
11580
- el.focus();
11581
-
11582
- if (params.clear) {
11583
- el.value = '';
11584
- el.dispatchEvent(new Event('input', { bubbles: true }));
11585
- }
11586
-
11587
- // Use native setter to trigger framework reactivity (React, Vue, etc.)
11588
- const nativeSetter = Object.getOwnPropertyDescriptor(
11589
- window.HTMLInputElement.prototype, 'value'
11590
- )?.set || Object.getOwnPropertyDescriptor(
11591
- window.HTMLTextAreaElement.prototype, 'value'
11592
- )?.set;
11593
-
11594
- const newValue = params.clear ? params.text : (el.value || '') + params.text;
11595
- if (nativeSetter) {
11596
- nativeSetter.call(el, newValue);
11597
- } else {
11598
- el.value = newValue;
11599
- }
11600
-
11601
- el.dispatchEvent(new Event('input', { bubbles: true }));
11602
- el.dispatchEvent(new Event('change', { bubbles: true }));
11603
-
11604
- if (params.submit) {
11605
- const form = el.closest('form');
11606
- if (form) {
11607
- form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
11608
- } else {
11609
- el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
11610
- }
13585
+ const result = typeIntoElement(params.selector, params.text, {
13586
+ clear: params.clear,
13587
+ submit: params.submit,
13588
+ });
13589
+ if (!result.error) {
13590
+ api.emit?.('browser:typed', { selector: params.selector, length: params.text.length });
11611
13591
  }
11612
-
11613
- api.emit?.('browser:typed', { selector: params.selector, length: params.text.length });
11614
-
11615
- return {
11616
- selector: params.selector,
11617
- typed: params.text,
11618
- currentValue: el.value?.slice(0, 200),
11619
- submitted: !!params.submit,
11620
- };
13592
+ return result;
11621
13593
  },
11622
13594
  permissions: ['emitEvents'],
11623
13595
  });
@@ -11747,7 +13719,7 @@ function registerBrowserActions(ai, wu) {
11747
13719
 
11748
13720
  if (!target) return { error: `Element not found: ${params.selector}` };
11749
13721
 
11750
- const tree = _buildA11yTree(target, 0, params.depth || 5);
13722
+ const tree = buildA11yTree(target, 0, params.depth || 5);
11751
13723
  return { snapshot: tree };
11752
13724
  },
11753
13725
  permissions: [],
@@ -11791,31 +13763,13 @@ function registerBrowserActions(ai, wu) {
11791
13763
  description: 'Filter: "2" (2xx success), "4" (4xx errors), "5" (5xx errors), "error" (all failures)',
11792
13764
  required: false,
11793
13765
  },
11794
- limit: {
11795
- type: 'number',
11796
- description: 'Max requests to return (default: 30)',
11797
- required: false,
11798
- },
11799
- },
11800
- handler: async (params) => {
11801
- let filtered = networkLog;
11802
- if (params.method) {
11803
- filtered = filtered.filter((r) => r.method === params.method.toUpperCase());
11804
- }
11805
- if (params.status) {
11806
- if (params.status === 'error') {
11807
- filtered = filtered.filter((r) => r.status === 0 || r.status >= 400);
11808
- } else {
11809
- filtered = filtered.filter((r) => String(r.status).startsWith(params.status));
11810
- }
11811
- }
11812
- const limit = params.limit || 30;
11813
- return {
11814
- requests: filtered.slice(-limit),
11815
- total: networkLog.length,
11816
- showing: Math.min(filtered.length, limit),
11817
- };
13766
+ limit: {
13767
+ type: 'number',
13768
+ description: 'Max requests to return (default: 30)',
13769
+ required: false,
13770
+ },
11818
13771
  },
13772
+ handler: async (params) => getFilteredNetwork(params.method, params.status, params.limit),
11819
13773
  permissions: [],
11820
13774
  });
11821
13775
 
@@ -11837,17 +13791,7 @@ function registerBrowserActions(ai, wu) {
11837
13791
  required: false,
11838
13792
  },
11839
13793
  },
11840
- handler: async (params) => {
11841
- const filtered = params.level
11842
- ? consoleLog.filter((m) => m.level === params.level)
11843
- : consoleLog;
11844
- const limit = params.limit || 30;
11845
- return {
11846
- messages: filtered.slice(-limit),
11847
- total: consoleLog.length,
11848
- showing: Math.min(filtered.length, limit),
11849
- };
11850
- },
13794
+ handler: async (params) => getFilteredConsole(params.level, params.limit),
11851
13795
  permissions: [],
11852
13796
  });
11853
13797
 
@@ -11895,141 +13839,8 @@ function registerBrowserActions(ai, wu) {
11895
13839
  });
11896
13840
  }
11897
13841
 
11898
- // ── Private: Inline computed styles for Canvas rendering ──
11899
-
11900
- function _inlineComputedStyles(source, clone) {
11901
- const props = ['color', 'background', 'background-color', 'font-family',
11902
- 'font-size', 'font-weight', 'border', 'border-radius', 'padding', 'margin',
11903
- 'display', 'flex-direction', 'align-items', 'justify-content', 'gap',
11904
- 'width', 'height', 'max-width', 'max-height', 'overflow', 'opacity',
11905
- 'box-shadow', 'text-align', 'line-height', 'position', 'top', 'left',
11906
- 'right', 'bottom', 'z-index', 'transform', 'visibility'];
11907
-
11908
- try {
11909
- const style = window.getComputedStyle(source);
11910
- for (const prop of props) {
11911
- const val = style.getPropertyValue(prop);
11912
- if (val) clone.style?.setProperty(prop, val);
11913
- }
11914
- } catch (_) { /* skip */ }
11915
-
11916
- const srcKids = source.children || [];
11917
- const cloneKids = clone.children || [];
11918
- const max = Math.min(srcKids.length, cloneKids.length, 200);
11919
- for (let i = 0; i < max; i++) {
11920
- _inlineComputedStyles(srcKids[i], cloneKids[i]);
11921
- }
11922
- }
11923
-
11924
- // ── Private: Build accessibility tree ──
11925
-
11926
- function _buildA11yTree(el, depth, maxDepth) {
11927
- if (depth > maxDepth || !el) return '';
11928
-
11929
- const indent = ' '.repeat(depth);
11930
- const tag = el.tagName?.toLowerCase() || '';
11931
- const role = el.getAttribute?.('role') || '';
11932
- const ariaLabel = el.getAttribute?.('aria-label') || '';
11933
- const text = el.childNodes?.length === 1 && el.childNodes[0].nodeType === 3
11934
- ? el.textContent?.trim().slice(0, 80) : '';
11935
-
11936
- let line = `${indent}<${tag}`;
11937
- if (el.id) line += ` id="${el.id}"`;
11938
- if (role) line += ` role="${role}"`;
11939
- if (ariaLabel) line += ` aria-label="${ariaLabel}"`;
11940
- if (el.className && typeof el.className === 'string') {
11941
- const cls = el.className.trim().slice(0, 60);
11942
- if (cls) line += ` class="${cls}"`;
11943
- }
11944
- line += '>';
11945
- if (text) line += ` "${text}"`;
11946
-
11947
- let result = line + '\n';
11948
- const root = el.shadowRoot || el;
11949
- const children = root.children || [];
11950
-
11951
- for (let i = 0; i < children.length && i < 50; i++) {
11952
- result += _buildA11yTree(children[i], depth + 1, maxDepth);
11953
- }
11954
- return result;
11955
- }
11956
-
11957
- // ── Private: Network interceptor ──
11958
-
11959
- function _installNetworkInterceptor() {
11960
- // Intercept fetch
11961
- const originalFetch = window.fetch;
11962
- window.fetch = async function (...args) {
11963
- const start = Date.now();
11964
- const req = args[0];
11965
- const url = typeof req === 'string' ? req : req?.url || '';
11966
- const method = (args[1]?.method || req?.method || 'GET').toUpperCase();
11967
-
11968
- try {
11969
- const response = await originalFetch.apply(window, args);
11970
- const size = parseInt(response.headers?.get('content-length') || '0', 10);
11971
- networkLog.push({
11972
- type: 'fetch', method, url,
11973
- status: response.status, statusText: response.statusText,
11974
- duration: Date.now() - start, size, timestamp: start,
11975
- });
11976
- if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
11977
- return response;
11978
- } catch (err) {
11979
- networkLog.push({
11980
- type: 'fetch', method, url,
11981
- status: 0, error: err.message,
11982
- duration: Date.now() - start, timestamp: start,
11983
- });
11984
- if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
11985
- throw err;
11986
- }
11987
- };
11988
-
11989
- // Intercept XMLHttpRequest
11990
- const origOpen = XMLHttpRequest.prototype.open;
11991
- const origSend = XMLHttpRequest.prototype.send;
11992
-
11993
- XMLHttpRequest.prototype.open = function (method, url, ...rest) {
11994
- this._wuAi = { method: (method || 'GET').toUpperCase(), url: String(url) };
11995
- return origOpen.call(this, method, url, ...rest);
11996
- };
11997
-
11998
- XMLHttpRequest.prototype.send = function (...args) {
11999
- if (this._wuAi) {
12000
- this._wuAi.start = Date.now();
12001
- this.addEventListener('loadend', () => {
12002
- networkLog.push({
12003
- type: 'xhr', method: this._wuAi.method, url: this._wuAi.url,
12004
- status: this.status, statusText: this.statusText,
12005
- duration: Date.now() - this._wuAi.start,
12006
- size: parseInt(this.getResponseHeader('content-length') || '0', 10),
12007
- timestamp: this._wuAi.start,
12008
- });
12009
- if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
12010
- });
12011
- }
12012
- return origSend.apply(this, args);
12013
- };
12014
- }
12015
-
12016
- // ── Private: Console interceptor ──
12017
-
12018
- function _installConsoleInterceptor() {
12019
- const levels = ['log', 'warn', 'error'];
12020
- for (const level of levels) {
12021
- const original = console[level];
12022
- console[level] = (...args) => {
12023
- consoleLog.push({
12024
- level,
12025
- message: args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '),
12026
- timestamp: Date.now(),
12027
- });
12028
- if (consoleLog.length > MAX_CONSOLE_LOG) consoleLog.shift();
12029
- original.apply(console, args);
12030
- };
12031
- }
12032
- }
13842
+ // All private helpers (buildA11yTree, inlineComputedStyles, interceptors)
13843
+ // are now in wu-ai-browser-primitives.js — single source of truth.
12033
13844
 
12034
13845
  /**
12035
13846
  * WU-AI: Central orchestrator for AI integration
@@ -12039,19 +13850,35 @@ function _installConsoleInterceptor() {
12039
13850
  *
12040
13851
  * Architecture:
12041
13852
  * WuAI (this file)
12042
- * ├── WuAIProvider → BYOL provider management
12043
- * ├── WuAIPermissions Security, rate limiting, circuit breaker
12044
- * ├── WuAIContext → Auto context collection for LLM
12045
- * ├── WuAIActions → Tool/action registry and execution
12046
- * ├── WuAIConversation → Multi-turn conversation manager
12047
- * └── WuAITriggers → Event-to-AI bridge (reactive AI)
13853
+ * ├── WuAIProvider → BYOL provider management (OpenAI, Anthropic, Ollama, Custom)
13854
+ * ├── WuAIPermissions 4-layer security (perms, rate limit, circuit breaker, loop guard)
13855
+ * ├── WuAIContext → Auto context collection with token budget
13856
+ * ├── WuAIActions → Tool/action registry and sandboxed execution
13857
+ * ├── WuAIConversation → Multi-turn conversation manager with namespaces
13858
+ * ├── WuAITriggers → Event-to-AI reactive bridge
13859
+ * ├── WuAIAgent → Autonomous agent loop (goal → steps → done)
13860
+ * ├── WuAIOrchestrate → Cross-micro-app AI coordination (capabilities + intents)
13861
+ * └── BrowserPrimitives → Shared screenshot, click, type, a11y tree, interceptors
13862
+ *
13863
+ * Four Paradigms:
13864
+ * 1. App → LLM send/stream/json → conversation with tool loops
13865
+ * 2. LLM → App tools/execute/expose → external agents call into the app
13866
+ * 3. AI Director agent(goal) → autonomous multi-step loop
13867
+ * 4. MF Glue capability/intent → cross-app coordination via AI
12048
13868
  *
12049
13869
  * Public API (accessible via wu.ai):
12050
13870
  * wu.ai.provider(name, config) → Register LLM provider
12051
13871
  * wu.ai.send(message, opts) → Send message (non-streaming)
12052
13872
  * wu.ai.stream(message, opts) → Send message (streaming)
13873
+ * wu.ai.json(message, schema?) → Send and get parsed JSON back
13874
+ * wu.ai.agent(goal, opts) → Run autonomous agent loop
12053
13875
  * wu.ai.action(name, config) → Register an action/tool
12054
13876
  * wu.ai.trigger(name, config) → Register an event trigger
13877
+ * wu.ai.capability(app, name, c) → Register app-scoped capability
13878
+ * wu.ai.intent(desc, opts) → Resolve cross-app intent
13879
+ * wu.ai.removeApp(appName) → Remove app capabilities (unmount)
13880
+ * wu.ai.workflow(name, config) → Register reusable AI workflow
13881
+ * wu.ai.runWorkflow(name, params)→ Execute workflow (async generator)
12055
13882
  * wu.ai.context.configure(...) → Configure context collection
12056
13883
  * wu.ai.abort(namespace?) → Abort active request
12057
13884
  *
@@ -12150,10 +13977,38 @@ class WuAI {
12150
13977
  this._modules.triggers.configure(config.triggers);
12151
13978
  }
12152
13979
 
13980
+ // 7. Agent (depends on conversation, actions, context, permissions, eventBus)
13981
+ this._modules.agent = new WuAIAgent({
13982
+ conversation: this._modules.conversation,
13983
+ actions: this._modules.actions,
13984
+ context: this._modules.context,
13985
+ permissions: this._modules.permissions,
13986
+ eventBus: this._eventBus,
13987
+ });
13988
+ if (config.agent) {
13989
+ this._modules.agent.configure(config.agent);
13990
+ }
13991
+
13992
+ // 8. Orchestrate — Paradigm 4: AI as microfrontend glue
13993
+ // Agent ref is passed so workflows can delegate to the agent loop
13994
+ // Store ref is passed for deterministic setState steps
13995
+ this._modules.orchestrate = new WuAIOrchestrate({
13996
+ actions: this._modules.actions,
13997
+ conversation: this._modules.conversation,
13998
+ context: this._modules.context,
13999
+ permissions: this._modules.permissions,
14000
+ eventBus: this._eventBus,
14001
+ agent: this._modules.agent,
14002
+ store: this._store,
14003
+ });
14004
+ if (config.orchestrate) {
14005
+ this._modules.orchestrate.configure(config.orchestrate);
14006
+ }
14007
+
12153
14008
  this._initialized = true;
12154
14009
  logger.wuInfo('[wu-ai] Initialized');
12155
14010
 
12156
- // 7. Browser automation actions (screenshot, click, type, network, etc.)
14011
+ // 9. Browser automation actions (screenshot, click, type, network, etc.)
12157
14012
  // Must be AFTER _initialized = true to prevent recursive init loop
12158
14013
  if (typeof window !== 'undefined') {
12159
14014
  registerBrowserActions(this, this._core);
@@ -12202,7 +14057,12 @@ class WuAI {
12202
14057
  * Send a message to the LLM and get a complete response.
12203
14058
  *
12204
14059
  * @param {string} message - User message
12205
- * @param {object} [options] - { namespace, systemPrompt, templateVars, temperature, maxTokens, signal }
14060
+ * @param {object} [options] - { namespace, systemPrompt, templateVars, temperature, maxTokens, provider, responseFormat, signal }
14061
+ * @param {string} [options.provider] - Use a specific registered provider (e.g., 'anthropic', 'openai')
14062
+ * @param {string|object} [options.responseFormat] - Request JSON output.
14063
+ * - `'json'` — simple JSON mode (OpenAI: json_object, Ollama: format:"json", Anthropic: prompt injection)
14064
+ * - `{ type: 'json_schema', schema: {...}, name?: string }` — structured output with JSON Schema
14065
+ * (OpenAI: native json_schema mode, Ollama: schema in format, Anthropic: schema in system prompt)
12206
14066
  * @returns {Promise<{ content: string, tool_results?: Array, usage?: object, namespace: string }>}
12207
14067
  *
12208
14068
  * @example
@@ -12211,6 +14071,21 @@ class WuAI {
12211
14071
  *
12212
14072
  * // With namespace for separate conversation
12213
14073
  * const response = await wu.ai.send('Analyze this chart', { namespace: 'analytics' });
14074
+ *
14075
+ * // Use a specific provider for this message
14076
+ * const response = await wu.ai.send('Translate this', { provider: 'anthropic' });
14077
+ *
14078
+ * // Simple JSON mode
14079
+ * const response = await wu.ai.send('List 5 colors', { responseFormat: 'json' });
14080
+ *
14081
+ * // Structured output with JSON Schema
14082
+ * const response = await wu.ai.send('List 5 colors', {
14083
+ * responseFormat: {
14084
+ * type: 'json_schema',
14085
+ * schema: { type: 'object', properties: { colors: { type: 'array', items: { type: 'string' } } } },
14086
+ * name: 'color_list',
14087
+ * },
14088
+ * });
12214
14089
  */
12215
14090
  async send(message, options = {}) {
12216
14091
  this._ensureInit();
@@ -12235,6 +14110,71 @@ class WuAI {
12235
14110
  yield* this._modules.conversation.stream(message, options);
12236
14111
  }
12237
14112
 
14113
+ /**
14114
+ * Send a message and get a parsed JSON response.
14115
+ * Shortcut for send() with responseFormat + automatic JSON.parse().
14116
+ *
14117
+ * @param {string} message - User message
14118
+ * @param {object} [options] - All send() options plus:
14119
+ * @param {object} [options.schema] - JSON Schema for structured output
14120
+ * @param {string} [options.schemaName='response'] - Schema name (required by OpenAI)
14121
+ * @returns {Promise<{ data: object|null, raw: string, error?: string, usage?: object, namespace: string }>}
14122
+ *
14123
+ * @example
14124
+ * // Simple JSON (no schema)
14125
+ * const { data } = await wu.ai.json('List 5 colors as a JSON array');
14126
+ * // data = ["red", "blue", ...]
14127
+ *
14128
+ * // With schema
14129
+ * const { data } = await wu.ai.json('List 5 colors', {
14130
+ * schema: { type: 'object', properties: { colors: { type: 'array', items: { type: 'string' } } } },
14131
+ * });
14132
+ * // data = { colors: ["red", "blue", ...] }
14133
+ *
14134
+ * // With schema + provider
14135
+ * const { data } = await wu.ai.json('List 5 colors', {
14136
+ * schema: mySchema,
14137
+ * provider: 'openai',
14138
+ * temperature: 0,
14139
+ * });
14140
+ */
14141
+ async json(message, options = {}) {
14142
+ this._ensureInit();
14143
+
14144
+ const { schema, schemaName, ...rest } = options;
14145
+
14146
+ let responseFormat;
14147
+ if (schema) {
14148
+ responseFormat = { type: 'json_schema', schema, name: schemaName || 'response' };
14149
+ } else {
14150
+ responseFormat = options.responseFormat || 'json';
14151
+ }
14152
+
14153
+ const response = await this._modules.conversation.send(message, { ...rest, responseFormat });
14154
+
14155
+ // The provider already attempts parse and sets response.parsed / response.parseError
14156
+ let data = null;
14157
+ let error;
14158
+
14159
+ if (response.parsed !== undefined) {
14160
+ data = response.parsed;
14161
+ } else if (response.content) {
14162
+ try {
14163
+ data = JSON.parse(response.content);
14164
+ } catch {
14165
+ error = 'LLM response is not valid JSON';
14166
+ }
14167
+ }
14168
+
14169
+ return {
14170
+ data,
14171
+ raw: response.content || '',
14172
+ error: error || response.parseError,
14173
+ usage: response.usage,
14174
+ namespace: response.namespace,
14175
+ };
14176
+ }
14177
+
12238
14178
  /**
12239
14179
  * Abort active request(s).
12240
14180
  *
@@ -12311,18 +14251,211 @@ class WuAI {
12311
14251
  * },
12312
14252
  * });
12313
14253
  */
12314
- trigger(name, config) {
14254
+ trigger(name, config) {
14255
+ this._ensureInit();
14256
+ this._modules.triggers.register(name, config);
14257
+ return this;
14258
+ }
14259
+
14260
+ /**
14261
+ * Fire a trigger manually.
14262
+ */
14263
+ async fireTrigger(name, eventData) {
14264
+ this._ensureInit();
14265
+ return this._modules.triggers.fire(name, eventData);
14266
+ }
14267
+
14268
+ // ─── Agent (Paradigm 3: Autonomous AI) ─────────────────────────
14269
+
14270
+ /**
14271
+ * Run an autonomous agent that pursues a goal using available tools.
14272
+ * Returns an async generator that yields step-by-step results.
14273
+ *
14274
+ * @param {string} goal - What the agent should accomplish
14275
+ * @param {object} [options]
14276
+ * @param {number} [options.maxSteps=10] - Maximum autonomous steps
14277
+ * @param {string} [options.provider] - Which LLM provider to use
14278
+ * @param {string} [options.namespace] - Conversation namespace
14279
+ * @param {string} [options.systemPrompt] - Override system prompt
14280
+ * @param {Function} [options.onStep] - Callback per step: (stepResult) => void
14281
+ * @param {Function} [options.shouldContinue] - Human-in-the-loop: (stepResult) => boolean|Promise<boolean>
14282
+ * @param {AbortSignal} [options.signal] - Abort signal
14283
+ * @returns {AsyncGenerator<AgentStepResult>}
14284
+ *
14285
+ * @example
14286
+ * // Basic usage
14287
+ * for await (const step of wu.ai.agent('Find all orders above $100 and summarize them')) {
14288
+ * console.log(`Step ${step.step}: ${step.content?.slice(0, 100)}`);
14289
+ * if (step.done) console.log('Agent finished!');
14290
+ * }
14291
+ *
14292
+ * // With human-in-the-loop
14293
+ * for await (const step of wu.ai.agent('Reorganize the product catalog', {
14294
+ * shouldContinue: (step) => confirm(`Continue? Step ${step.step}: ${step.content?.slice(0, 50)}`),
14295
+ * })) {
14296
+ * updateUI(step);
14297
+ * }
14298
+ */
14299
+ async *agent(goal, options = {}) {
14300
+ this._ensureInit();
14301
+ yield* this._modules.agent.run(goal, options);
14302
+ }
14303
+
14304
+ // ─── Paradigm 4: AI as Microfrontend Glue ─────────────────────
14305
+
14306
+ /**
14307
+ * Register a capability scoped to a specific micro-app.
14308
+ *
14309
+ * Each micro-app calls this to declare what it can do. The AI uses
14310
+ * the capability map to resolve cross-app intents.
14311
+ *
14312
+ * @param {string} appName - The micro-app name (e.g., 'orders', 'dashboard')
14313
+ * @param {string} actionName - The capability name (e.g., 'getRecent', 'updateKPIs')
14314
+ * @param {object} config - Same as wu.ai.action() config:
14315
+ * { description, parameters, handler, confirm?, permissions?, dangerous? }
14316
+ *
14317
+ * @example
14318
+ * // In orders micro-app (React):
14319
+ * wu.ai.capability('orders', 'getRecent', {
14320
+ * description: 'Get the N most recent orders',
14321
+ * parameters: { limit: { type: 'number' } },
14322
+ * handler: async (params) => fetchOrders({ limit: params.limit || 10 }),
14323
+ * });
14324
+ *
14325
+ * // In dashboard micro-app (Svelte):
14326
+ * wu.ai.capability('dashboard', 'updateKPIs', {
14327
+ * description: 'Refresh the KPI cards with latest data',
14328
+ * handler: async () => { refreshKPIs(); return { updated: true }; },
14329
+ * });
14330
+ */
14331
+ capability(appName, actionName, config) {
14332
+ this._ensureInit();
14333
+ this._modules.orchestrate.register(appName, actionName, config);
14334
+ return this;
14335
+ }
14336
+
14337
+ /**
14338
+ * Resolve a cross-app intent in a single conversation turn.
14339
+ *
14340
+ * The AI receives the full capability map (what each app can do),
14341
+ * current application state, and mounted apps. It resolves the
14342
+ * intent by calling the right capabilities across app boundaries.
14343
+ *
14344
+ * @param {string} description - Natural language intent
14345
+ * @param {object} [options]
14346
+ * @param {string[]} [options.plan] - Optional action sequence hint
14347
+ * @param {string} [options.provider] - LLM provider override
14348
+ * @param {number} [options.temperature] - Temperature override
14349
+ * @param {number} [options.maxTokens] - Max tokens override
14350
+ * @param {AbortSignal} [options.signal] - Abort signal
14351
+ * @param {string|object} [options.responseFormat] - Response format
14352
+ * @returns {Promise<{ content: string, tool_results: Array, usage: object|null, resolved: boolean, appsInvolved: string[] }>}
14353
+ *
14354
+ * @example
14355
+ * // Simple cross-app query
14356
+ * const result = await wu.ai.intent('Show me the top customer by order count');
14357
+ * // AI calls orders:getRecent → aggregates → returns answer
14358
+ *
14359
+ * // With plan hint
14360
+ * const result = await wu.ai.intent('Update all views after a new order', {
14361
+ * plan: ['orders:getRecent', 'dashboard:updateKPIs', 'analytics:refresh'],
14362
+ * });
14363
+ *
14364
+ * // With JSON response
14365
+ * const result = await wu.ai.intent('Get order stats by status', {
14366
+ * responseFormat: 'json',
14367
+ * });
14368
+ */
14369
+ async intent(description, options = {}) {
14370
+ this._ensureInit();
14371
+ return this._modules.orchestrate.resolve(description, options);
14372
+ }
14373
+
14374
+ /**
14375
+ * Remove all capabilities for a micro-app.
14376
+ * Call this when a micro-app is unmounted to prevent stale
14377
+ * capabilities from appearing in the AI's capability map.
14378
+ *
14379
+ * @param {string} appName - The micro-app name
14380
+ *
14381
+ * @example
14382
+ * // In unmount lifecycle:
14383
+ * wu.ai.removeApp('orders');
14384
+ */
14385
+ removeApp(appName) {
14386
+ this._ensureInit();
14387
+ this._modules.orchestrate.removeApp(appName);
14388
+ return this;
14389
+ }
14390
+
14391
+ /**
14392
+ * Register a reusable AI workflow — a named, step-by-step recipe
14393
+ * that the AI agent follows using browser automation.
14394
+ *
14395
+ * @param {string} name - Workflow name (e.g., 'register-user')
14396
+ * @param {object} config
14397
+ * @param {string} config.description - What this workflow does
14398
+ * @param {string[]} config.steps - Step-by-step instructions
14399
+ * Use {{paramName}} for parameter interpolation.
14400
+ * @param {object} [config.parameters] - Parameter definitions
14401
+ * @param {number} [config.maxSteps=15] - Max agent steps
14402
+ * @param {string} [config.provider] - LLM provider
14403
+ *
14404
+ * @example
14405
+ * wu.ai.workflow('register-user', {
14406
+ * description: 'Register a new user in the system',
14407
+ * steps: [
14408
+ * 'Navigate to the Customers section',
14409
+ * 'Click the "Add Customer" button',
14410
+ * 'Type "{{name}}" into the name field',
14411
+ * 'Type "{{email}}" into the email field',
14412
+ * 'Click Submit',
14413
+ * 'Verify the success message appears',
14414
+ * ],
14415
+ * parameters: {
14416
+ * name: { type: 'string', required: true },
14417
+ * email: { type: 'string', required: true },
14418
+ * },
14419
+ * });
14420
+ */
14421
+ workflow(name, config) {
12315
14422
  this._ensureInit();
12316
- this._modules.triggers.register(name, config);
14423
+ this._modules.orchestrate.registerWorkflow(name, config);
12317
14424
  return this;
12318
14425
  }
12319
14426
 
12320
14427
  /**
12321
- * Fire a trigger manually.
14428
+ * Execute a registered workflow. Returns an async generator
14429
+ * so you can observe each step in real time.
14430
+ *
14431
+ * @param {string} name - Workflow name
14432
+ * @param {object} [params={}] - Parameters to fill into the steps
14433
+ * @param {object} [options={}]
14434
+ * @param {Function} [options.onStep] - Callback per step
14435
+ * @param {Function} [options.shouldContinue] - Human-in-the-loop gate
14436
+ * @param {AbortSignal} [options.signal] - Abort signal
14437
+ * @returns {AsyncGenerator<AgentStepResult>}
14438
+ *
14439
+ * @example
14440
+ * // Run and watch every step
14441
+ * for await (const step of wu.ai.runWorkflow('register-user', {
14442
+ * name: 'Juan Pérez',
14443
+ * email: 'juan@test.com',
14444
+ * })) {
14445
+ * console.log(`Step ${step.step}: ${step.content}`);
14446
+ * if (step.type === 'done') console.log('Workflow complete!');
14447
+ * }
14448
+ *
14449
+ * // With human approval per step
14450
+ * for await (const step of wu.ai.runWorkflow('register-user', params, {
14451
+ * shouldContinue: (s) => confirm(`Continue? ${s.content?.slice(0, 60)}`),
14452
+ * })) {
14453
+ * renderStep(step);
14454
+ * }
12322
14455
  */
12323
- async fireTrigger(name, eventData) {
14456
+ async *runWorkflow(name, params = {}, options = {}) {
12324
14457
  this._ensureInit();
12325
- return this._modules.triggers.fire(name, eventData);
14458
+ yield* this._modules.orchestrate.executeWorkflow(name, params, options);
12326
14459
  }
12327
14460
 
12328
14461
  // ─── Context ───────────────────────────────────────────────────
@@ -12461,6 +14594,8 @@ class WuAI {
12461
14594
  actions: this._modules.actions.getStats(),
12462
14595
  conversation: this._modules.conversation.getStats(),
12463
14596
  triggers: this._modules.triggers.getStats(),
14597
+ agent: this._modules.agent.getStats(),
14598
+ orchestrate: this._modules.orchestrate.getStats(),
12464
14599
  };
12465
14600
  }
12466
14601
 
@@ -12470,6 +14605,8 @@ class WuAI {
12470
14605
  destroy() {
12471
14606
  if (!this._initialized) return;
12472
14607
 
14608
+ this._modules.orchestrate.destroy();
14609
+ this._modules.agent.destroy();
12473
14610
  this._modules.conversation.abortAll();
12474
14611
  this._modules.triggers.destroy();
12475
14612
  this._modules = {};
@@ -12495,6 +14632,8 @@ class WuAI {
12495
14632
  if (config.context) this._modules.context.configure(config.context);
12496
14633
  if (config.conversation) this._modules.conversation.configure(config.conversation);
12497
14634
  if (config.triggers) this._modules.triggers.configure(config.triggers);
14635
+ if (config.agent) this._modules.agent.configure(config.agent);
14636
+ if (config.orchestrate) this._modules.orchestrate.configure(config.orchestrate);
12498
14637
  }
12499
14638
  }
12500
14639
 
@@ -12505,14 +14644,21 @@ class WuAI {
12505
14644
  * commands using wu.* APIs. This is the "eyes and hands" of
12506
14645
  * the MCP server inside the browser.
12507
14646
  *
14647
+ * Security:
14648
+ * - Optional auth token sent on first message (handshake)
14649
+ * - All state/event/mount operations check wu.ai permissions
14650
+ * - Mutating operations emit audit events
14651
+ * - Read-only operations (status, list_apps, snapshot, console, network) are unrestricted
14652
+ *
12508
14653
  * @example
12509
- * // Auto-connect (called by wu.mcp.connect())
12510
- * import { createMcpBridge } from './wu-mcp-bridge.js';
14654
+ * // Connect with auth token
14655
+ * wu.mcp.connect('ws://localhost:19100', { token: 'my-secret' });
12511
14656
  *
12512
- * const bridge = createMcpBridge(wuInstance);
12513
- * bridge.connect('ws://localhost:3100');
14657
+ * // Connect without auth (development only)
14658
+ * wu.mcp.connect();
12514
14659
  */
12515
14660
 
14661
+
12516
14662
  /**
12517
14663
  * Create the MCP bridge for a Wu instance.
12518
14664
  *
@@ -12523,6 +14669,8 @@ function createMcpBridge(wu) {
12523
14669
  let ws = null;
12524
14670
  let reconnectTimer = null;
12525
14671
  let reconnectAttempts = 0;
14672
+ let authenticated = false;
14673
+ let authToken = null;
12526
14674
  const MAX_RECONNECT_ATTEMPTS = 10;
12527
14675
  const RECONNECT_DELAY = 2000;
12528
14676
 
@@ -12530,14 +14678,6 @@ function createMcpBridge(wu) {
12530
14678
  const eventLog = [];
12531
14679
  const MAX_EVENT_LOG = 200;
12532
14680
 
12533
- // Console log capture
12534
- const consoleLog = [];
12535
- const MAX_CONSOLE_LOG = 500;
12536
-
12537
- // Network log capture
12538
- const networkLog = [];
12539
- const MAX_NETWORK_LOG = 300;
12540
-
12541
14681
  // Capture events for history
12542
14682
  if (wu.eventBus) {
12543
14683
  wu.eventBus.on('*', (event) => {
@@ -12551,22 +14691,50 @@ function createMcpBridge(wu) {
12551
14691
  });
12552
14692
  }
12553
14693
 
12554
- // Capture console messages
12555
- _interceptConsole();
14694
+ // Install shared interceptors (idempotent — safe if wu-ai-browser already did it)
14695
+ ensureInterceptors();
14696
+
14697
+ // ── Permission helpers ──
14698
+
14699
+ /**
14700
+ * Check a permission flag via wu.ai.permissions if available.
14701
+ * Falls back to deny if wu.ai is not initialized.
14702
+ */
14703
+ function _checkPermission(perm) {
14704
+ if (wu.ai && wu.ai.permissions) {
14705
+ return wu.ai.permissions.check(perm);
14706
+ }
14707
+ // If AI module not initialized, deny write operations, allow reads
14708
+ const readPerms = ['readStore', 'executeActions'];
14709
+ return readPerms.includes(perm);
14710
+ }
12556
14711
 
12557
- // Capture network requests (fetch + XMLHttpRequest)
12558
- _interceptNetwork();
14712
+ /**
14713
+ * Emit an audit event for bridge operations.
14714
+ */
14715
+ function _audit(operation, params, result) {
14716
+ if (wu.eventBus) {
14717
+ wu.eventBus.emit('mcp:bridge:operation', {
14718
+ operation,
14719
+ params,
14720
+ result: result?.error ? { error: result.error } : { success: true },
14721
+ timestamp: Date.now(),
14722
+ }, { appName: 'wu-mcp-bridge' });
14723
+ }
14724
+ }
12559
14725
 
12560
14726
  // ── Command handlers ──
12561
14727
 
12562
14728
  const handlers = {
14729
+ // ── Read-only operations (no permission gates) ──
14730
+
12563
14731
  status() {
12564
14732
  return {
12565
14733
  connected: true,
12566
14734
  framework: 'wu-framework',
12567
14735
  apps: _getAppList(),
12568
14736
  storeKeys: wu.store ? Object.keys(wu.store.get('') || {}) : [],
12569
- actionsCount: wu.ai?._actions ? Object.keys(wu.ai._actions).length : 0,
14737
+ actionsCount: wu.ai ? wu.ai.tools().length : 0,
12570
14738
  eventLogSize: eventLog.length,
12571
14739
  };
12572
14740
  },
@@ -12575,45 +14743,55 @@ function createMcpBridge(wu) {
12575
14743
  return _getAppList();
12576
14744
  },
12577
14745
 
12578
- navigate({ route }) {
12579
- if (!route) return { error: 'Route is required' };
12580
- if (wu.eventBus) {
12581
- wu.eventBus.emit('shell:navigate', { route });
12582
- }
12583
- if (wu.store) {
12584
- wu.store.set('currentPath', route);
12585
- }
12586
- return { navigated: route };
14746
+ list_events({ limit = 20 }) {
14747
+ return eventLog.slice(-limit);
12587
14748
  },
12588
14749
 
12589
- mount_app({ appName, container }) {
12590
- if (!appName) return { error: 'appName is required' };
12591
- try {
12592
- if (wu.mount) {
12593
- wu.mount(appName, container);
12594
- return { mounted: appName, container };
12595
- }
12596
- return { error: 'wu.mount not available' };
12597
- } catch (err) {
12598
- return { error: err.message };
12599
- }
14750
+ list_actions() {
14751
+ if (!wu.ai) return { actions: [], note: 'wu.ai not initialized' };
14752
+ const tools = wu.ai.tools();
14753
+ return { actions: tools, count: tools.length };
12600
14754
  },
12601
14755
 
12602
- unmount_app({ appName }) {
12603
- if (!appName) return { error: 'appName is required' };
14756
+ snapshot({ appName }) {
12604
14757
  try {
12605
- if (wu.unmount) {
12606
- wu.unmount(appName);
12607
- return { unmounted: appName };
12608
- }
12609
- return { error: 'wu.unmount not available' };
14758
+ const target = appName
14759
+ ? document.querySelector(`[data-wu-app="${appName}"]`) || document.querySelector(`#wu-app-${appName}`)
14760
+ : document.body;
14761
+
14762
+ if (!target) return { error: `App "${appName}" not found in DOM` };
14763
+
14764
+ return {
14765
+ app: appName || '(page)',
14766
+ snapshot: buildA11yTree(target, 0, 5),
14767
+ timestamp: Date.now(),
14768
+ };
12610
14769
  } catch (err) {
12611
14770
  return { error: err.message };
12612
14771
  }
12613
14772
  },
12614
14773
 
14774
+ console({ level = 'all', limit = 50 }) {
14775
+ return getFilteredConsole(level, limit);
14776
+ },
14777
+
14778
+ async screenshot({ selector, quality = 0.8 }) {
14779
+ const result = await captureScreenshot(selector, quality);
14780
+ if (!result.error) result.timestamp = Date.now();
14781
+ return result;
14782
+ },
14783
+
14784
+ network({ method, status, limit = 50 }) {
14785
+ return getFilteredNetwork(method, status, limit);
14786
+ },
14787
+
14788
+ // ── Permission-gated operations ──
14789
+
12615
14790
  get_state({ path }) {
12616
14791
  if (!wu.store) return { error: 'wu.store not available' };
14792
+ if (!_checkPermission('readStore')) {
14793
+ return { error: 'Permission denied: readStore is disabled' };
14794
+ }
12617
14795
  const value = wu.store.get(path || '');
12618
14796
  return { path: path || '(root)', value };
12619
14797
  },
@@ -12621,293 +14799,162 @@ function createMcpBridge(wu) {
12621
14799
  set_state({ path, value }) {
12622
14800
  if (!wu.store) return { error: 'wu.store not available' };
12623
14801
  if (!path) return { error: 'path is required' };
14802
+ if (!_checkPermission('writeStore')) {
14803
+ _audit('set_state', { path }, { error: 'Permission denied' });
14804
+ return { error: 'Permission denied: writeStore is disabled' };
14805
+ }
12624
14806
  wu.store.set(path, value);
14807
+ _audit('set_state', { path, value }, { });
12625
14808
  return { path, value, updated: true };
12626
14809
  },
12627
14810
 
12628
14811
  emit_event({ event, data }) {
12629
14812
  if (!wu.eventBus) return { error: 'wu.eventBus not available' };
12630
14813
  if (!event) return { error: 'event name is required' };
12631
- wu.eventBus.emit(event, data);
14814
+ if (!_checkPermission('emitEvents')) {
14815
+ _audit('emit_event', { event }, { error: 'Permission denied' });
14816
+ return { error: 'Permission denied: emitEvents is disabled' };
14817
+ }
14818
+ wu.eventBus.emit(event, data, { appName: 'wu-mcp-bridge' });
14819
+ _audit('emit_event', { event, data }, { });
12632
14820
  return { emitted: event, data };
12633
14821
  },
12634
14822
 
12635
- list_events({ limit = 20 }) {
12636
- return eventLog.slice(-limit);
12637
- },
12638
-
12639
- list_actions() {
12640
- if (!wu.ai?._actions) return { actions: [], note: 'wu.ai not initialized or no actions registered' };
12641
- const actions = Object.entries(wu.ai._actions).map(([name, def]) => ({
12642
- name,
12643
- description: def.description || '',
12644
- parameters: def.parameters ? Object.keys(def.parameters) : [],
12645
- }));
12646
- return { actions, count: actions.length };
14823
+ navigate({ route }) {
14824
+ if (!route) return { error: 'Route is required' };
14825
+ if (!_checkPermission('emitEvents')) {
14826
+ _audit('navigate', { route }, { error: 'Permission denied: emitEvents' });
14827
+ return { error: 'Permission denied: emitEvents is disabled' };
14828
+ }
14829
+ if (wu.eventBus) {
14830
+ wu.eventBus.emit('shell:navigate', { route }, { appName: 'wu-mcp-bridge' });
14831
+ }
14832
+ if (wu.store && _checkPermission('writeStore')) {
14833
+ wu.store.set('currentPath', route);
14834
+ }
14835
+ _audit('navigate', { route }, { });
14836
+ return { navigated: route };
12647
14837
  },
12648
14838
 
12649
- async execute_action({ action, params }) {
12650
- if (!wu.ai) return { error: 'wu.ai not available' };
12651
- if (!action) return { error: 'action name is required' };
12652
-
14839
+ mount_app({ appName, container }) {
14840
+ if (!appName) return { error: 'appName is required' };
14841
+ if (!_checkPermission('modifyDOM')) {
14842
+ _audit('mount_app', { appName }, { error: 'Permission denied' });
14843
+ return { error: 'Permission denied: modifyDOM is disabled' };
14844
+ }
12653
14845
  try {
12654
- // Try to execute via wu.ai action system
12655
- if (wu.ai._actions && wu.ai._actions[action]) {
12656
- const handler = wu.ai._actions[action].handler;
12657
- const result = await handler(params || {}, {
12658
- emit: (e, d) => wu.eventBus?.emit(e, d),
12659
- setState: (p, v) => wu.store?.set(p, v),
12660
- getState: (p) => wu.store?.get(p),
12661
- });
12662
- return { action, result };
14846
+ if (wu.mount) {
14847
+ wu.mount(appName, container);
14848
+ _audit('mount_app', { appName, container }, { success: true });
14849
+ return { mounted: appName, container };
12663
14850
  }
12664
- return { error: `Action "${action}" not found` };
14851
+ return { error: 'wu.mount not available' };
12665
14852
  } catch (err) {
12666
14853
  return { error: err.message };
12667
14854
  }
12668
14855
  },
12669
14856
 
12670
- snapshot({ appName }) {
12671
- try {
12672
- const target = appName
12673
- ? document.querySelector(`[data-wu-app="${appName}"]`) || document.querySelector(`#wu-app-${appName}`)
12674
- : document.body;
12675
-
12676
- if (!target) return { error: `App "${appName}" not found in DOM` };
12677
-
12678
- const tree = _buildA11yTree(target, 0, 5);
12679
- return {
12680
- app: appName || '(page)',
12681
- snapshot: tree,
12682
- timestamp: Date.now(),
12683
- };
12684
- } catch (err) {
12685
- return { error: err.message };
14857
+ unmount_app({ appName }) {
14858
+ if (!appName) return { error: 'appName is required' };
14859
+ if (!_checkPermission('modifyDOM')) {
14860
+ _audit('unmount_app', { appName }, { error: 'Permission denied' });
14861
+ return { error: 'Permission denied: modifyDOM is disabled' };
12686
14862
  }
12687
- },
12688
-
12689
- console({ level = 'all', limit = 50 }) {
12690
- const filtered = level === 'all'
12691
- ? consoleLog
12692
- : consoleLog.filter((m) => m.level === level);
12693
- return filtered.slice(-limit);
12694
- },
12695
-
12696
- async screenshot({ selector, quality = 0.8 }) {
12697
14863
  try {
12698
- const target = selector
12699
- ? document.querySelector(selector)
12700
- : document.documentElement;
12701
-
12702
- if (!target) return { error: `Element not found: ${selector}` };
12703
-
12704
- const rect = target.getBoundingClientRect();
12705
- const w = Math.ceil(Math.min(rect.width || window.innerWidth, 1920));
12706
- const h = Math.ceil(Math.min(rect.height || window.innerHeight, 1080));
12707
-
12708
- // Clone target and inline all computed styles for accurate rendering
12709
- const clone = target.cloneNode(true);
12710
- _inlineComputedStyles(target, clone);
12711
-
12712
- // Serialize to XHTML (required for SVG foreignObject)
12713
- const serializer = new XMLSerializer();
12714
- const xhtml = serializer.serializeToString(clone);
12715
-
12716
- // Build SVG with foreignObject containing the styled DOM
12717
- const svgStr = [
12718
- `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">`,
12719
- '<foreignObject width="100%" height="100%">',
12720
- `<div xmlns="http://www.w3.org/1999/xhtml" style="width:${w}px;height:${h}px;overflow:hidden;">`,
12721
- xhtml,
12722
- '</div>',
12723
- '</foreignObject>',
12724
- '</svg>',
12725
- ].join('');
12726
-
12727
- const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
12728
- const url = URL.createObjectURL(svgBlob);
12729
-
12730
- // Render SVG to Canvas
12731
- const dataUrl = await new Promise((resolve) => {
12732
- const img = new Image();
12733
- img.onload = () => {
12734
- const canvas = document.createElement('canvas');
12735
- canvas.width = w;
12736
- canvas.height = h;
12737
- const ctx = canvas.getContext('2d');
12738
- ctx.drawImage(img, 0, 0);
12739
- URL.revokeObjectURL(url);
12740
- resolve(canvas.toDataURL('image/png', quality));
12741
- };
12742
- img.onerror = () => {
12743
- URL.revokeObjectURL(url);
12744
- resolve(null);
12745
- };
12746
- img.src = url;
12747
- });
12748
-
12749
- if (!dataUrl) return { error: 'Canvas rendering failed' };
12750
-
12751
- // Return base64 without the data:image/png;base64, prefix
12752
- const base64 = dataUrl.split(',')[1];
12753
- return {
12754
- selector: selector || '(page)',
12755
- width: w,
12756
- height: h,
12757
- format: 'png',
12758
- base64,
12759
- sizeKB: Math.round((base64.length * 3) / 4 / 1024),
12760
- timestamp: Date.now(),
12761
- };
14864
+ if (wu.unmount) {
14865
+ wu.unmount(appName);
14866
+ _audit('unmount_app', { appName }, { success: true });
14867
+ return { unmounted: appName };
14868
+ }
14869
+ return { error: 'wu.unmount not available' };
12762
14870
  } catch (err) {
12763
14871
  return { error: err.message };
12764
14872
  }
12765
14873
  },
12766
14874
 
12767
14875
  click({ selector, text }) {
12768
- try {
12769
- let el = null;
12770
-
12771
- if (selector) {
12772
- el = document.querySelector(selector);
12773
- }
12774
-
12775
- // Fallback: find by visible text content
12776
- if (!el && text) {
12777
- const candidates = document.querySelectorAll('button, a, [role="button"], input[type="submit"], [data-click], label');
12778
- for (const candidate of candidates) {
12779
- if (candidate.textContent?.trim().toLowerCase().includes(text.toLowerCase())) {
12780
- el = candidate;
12781
- break;
12782
- }
12783
- }
12784
- }
12785
-
12786
- if (!el) return { error: `Element not found: ${selector || `text="${text}"`}` };
12787
-
12788
- // Scroll into view and click
12789
- el.scrollIntoView({ behavior: 'instant', block: 'center' });
12790
- el.click();
12791
-
12792
- const tag = el.tagName?.toLowerCase();
12793
- const id = el.id ? `#${el.id}` : '';
12794
- const cls = el.className && typeof el.className === 'string' ? `.${el.className.split(' ')[0]}` : '';
12795
- return {
12796
- clicked: `${tag}${id}${cls}`,
12797
- text: el.textContent?.trim().slice(0, 80) || '',
12798
- rect: el.getBoundingClientRect().toJSON(),
12799
- };
12800
- } catch (err) {
12801
- return { error: err.message };
14876
+ if (!_checkPermission('modifyDOM')) {
14877
+ return { error: 'Permission denied: modifyDOM is disabled' };
12802
14878
  }
14879
+ const result = clickElement(selector, text);
14880
+ _audit('click', { selector, text }, result);
14881
+ return result;
12803
14882
  },
12804
14883
 
12805
14884
  type({ selector, text, clear = false, submit = false }) {
12806
- try {
12807
- if (!selector) return { error: 'selector is required' };
12808
- if (text === undefined) return { error: 'text is required' };
12809
-
12810
- const el = document.querySelector(selector);
12811
- if (!el) return { error: `Element not found: ${selector}` };
12812
-
12813
- // Focus the element
12814
- el.focus();
12815
-
12816
- // Clear existing value if requested
12817
- if (clear) {
12818
- el.value = '';
12819
- el.dispatchEvent(new Event('input', { bubbles: true }));
12820
- }
12821
-
12822
- // Set value and fire events (works with React, Vue, etc.)
12823
- const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
12824
- window.HTMLInputElement.prototype, 'value'
12825
- )?.set || Object.getOwnPropertyDescriptor(
12826
- window.HTMLTextAreaElement.prototype, 'value'
12827
- )?.set;
12828
-
12829
- if (nativeInputValueSetter) {
12830
- nativeInputValueSetter.call(el, clear ? text : el.value + text);
12831
- } else {
12832
- el.value = clear ? text : el.value + text;
12833
- }
12834
-
12835
- // Dispatch events that frameworks listen to
12836
- el.dispatchEvent(new Event('input', { bubbles: true }));
12837
- el.dispatchEvent(new Event('change', { bubbles: true }));
12838
-
12839
- // Submit form if requested
12840
- if (submit) {
12841
- const form = el.closest('form');
12842
- if (form) {
12843
- form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
12844
- } else {
12845
- el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
12846
- }
12847
- }
12848
-
12849
- return {
12850
- selector,
12851
- typed: text,
12852
- currentValue: el.value?.slice(0, 200),
12853
- submitted: submit,
12854
- };
12855
- } catch (err) {
12856
- return { error: err.message };
14885
+ if (!_checkPermission('modifyDOM')) {
14886
+ return { error: 'Permission denied: modifyDOM is disabled' };
12857
14887
  }
14888
+ const result = typeIntoElement(selector, text, { clear, submit });
14889
+ _audit('type', { selector, textLength: text?.length }, result);
14890
+ return result;
12858
14891
  },
12859
14892
 
12860
- network({ method, status, limit = 50 }) {
12861
- let filtered = networkLog;
12862
- if (method) filtered = filtered.filter((r) => r.method.toUpperCase() === method.toUpperCase());
12863
- if (status) {
12864
- if (status === 'error') {
12865
- filtered = filtered.filter((r) => r.status === 0 || r.status >= 400);
12866
- } else {
12867
- filtered = filtered.filter((r) => String(r.status).startsWith(String(status)));
12868
- }
12869
- }
12870
- return {
12871
- requests: filtered.slice(-limit),
12872
- total: networkLog.length,
12873
- filtered: filtered.length,
12874
- };
12875
- },
14893
+ async execute_action({ action, params }) {
14894
+ if (!wu.ai) return { error: 'wu.ai not available' };
14895
+ if (!action) return { error: 'action name is required' };
12876
14896
 
12877
- eval({ expression }) {
12878
14897
  try {
12879
- // eslint-disable-next-line no-eval
12880
- const result = eval(expression);
12881
- return {
12882
- expression,
12883
- result: typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result),
12884
- type: typeof result,
12885
- };
14898
+ // Execute through public API (respects permissions, validation, audit)
14899
+ const result = await wu.ai.execute(action, params || {});
14900
+ return { action, ...result };
12886
14901
  } catch (err) {
12887
- return { error: err.message, expression };
14902
+ return { error: err.message };
12888
14903
  }
12889
14904
  },
12890
14905
  };
12891
14906
 
12892
14907
  // ── WebSocket connection ──
12893
14908
 
12894
- function connect(url = 'ws://localhost:19100') {
14909
+ function connect(url = 'ws://localhost:19100', options = {}) {
12895
14910
  if (ws && ws.readyState <= 1) {
12896
14911
  console.warn('[wu-mcp-bridge] Already connected or connecting');
12897
14912
  return;
12898
14913
  }
12899
14914
 
14915
+ authToken = options.token || null;
14916
+ authenticated = !authToken; // No token = auto-authenticated (dev mode)
14917
+
12900
14918
  try {
12901
14919
  ws = new WebSocket(url);
12902
14920
 
12903
14921
  ws.onopen = () => {
12904
14922
  console.log('[wu-mcp-bridge] Connected to wu-mcp-server');
12905
14923
  reconnectAttempts = 0;
14924
+
14925
+ // Send auth handshake if token provided
14926
+ if (authToken) {
14927
+ ws.send(JSON.stringify({
14928
+ type: 'auth',
14929
+ token: authToken,
14930
+ }));
14931
+ }
12906
14932
  };
12907
14933
 
12908
14934
  ws.onmessage = async (event) => {
12909
14935
  try {
12910
14936
  const msg = JSON.parse(event.data);
14937
+
14938
+ // Handle auth response
14939
+ if (msg.type === 'auth_result') {
14940
+ authenticated = msg.success === true;
14941
+ if (!authenticated) {
14942
+ console.error('[wu-mcp-bridge] Authentication failed:', msg.reason || 'Invalid token');
14943
+ disconnect();
14944
+ } else {
14945
+ console.log('[wu-mcp-bridge] Authenticated successfully');
14946
+ }
14947
+ return;
14948
+ }
14949
+
14950
+ // Reject commands if not authenticated
14951
+ if (!authenticated) {
14952
+ if (msg.id) {
14953
+ _respond(msg.id, null, 'Not authenticated. Send auth token first.');
14954
+ }
14955
+ return;
14956
+ }
14957
+
12911
14958
  const { id, command, params } = msg;
12912
14959
 
12913
14960
  if (!id || !command) {
@@ -12935,7 +14982,8 @@ function createMcpBridge(wu) {
12935
14982
  ws.onclose = () => {
12936
14983
  console.log('[wu-mcp-bridge] Disconnected');
12937
14984
  ws = null;
12938
- _scheduleReconnect(url);
14985
+ authenticated = false;
14986
+ _scheduleReconnect(url, options);
12939
14987
  };
12940
14988
 
12941
14989
  ws.onerror = () => {
@@ -12943,7 +14991,7 @@ function createMcpBridge(wu) {
12943
14991
  };
12944
14992
  } catch (err) {
12945
14993
  console.error('[wu-mcp-bridge] Connection failed:', err.message);
12946
- _scheduleReconnect(url);
14994
+ _scheduleReconnect(url, options);
12947
14995
  }
12948
14996
  }
12949
14997
 
@@ -12957,10 +15005,11 @@ function createMcpBridge(wu) {
12957
15005
  ws.close();
12958
15006
  ws = null;
12959
15007
  }
15008
+ authenticated = false;
12960
15009
  }
12961
15010
 
12962
15011
  function isConnected() {
12963
- return ws !== null && ws.readyState === 1;
15012
+ return ws !== null && ws.readyState === 1 && authenticated;
12964
15013
  }
12965
15014
 
12966
15015
  // ── Private helpers ──
@@ -12971,15 +15020,14 @@ function createMcpBridge(wu) {
12971
15020
  ws.send(JSON.stringify(msg));
12972
15021
  }
12973
15022
 
12974
- function _scheduleReconnect(url) {
15023
+ function _scheduleReconnect(url, options) {
12975
15024
  if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
12976
15025
  reconnectAttempts++;
12977
15026
  const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5);
12978
- reconnectTimer = setTimeout(() => connect(url), delay);
15027
+ reconnectTimer = setTimeout(() => connect(url, options), delay);
12979
15028
  }
12980
15029
 
12981
15030
  function _getAppList() {
12982
- // Try to get app info from wu internals
12983
15031
  const apps = [];
12984
15032
 
12985
15033
  if (wu._apps) {
@@ -13007,142 +15055,6 @@ function createMcpBridge(wu) {
13007
15055
  return apps;
13008
15056
  }
13009
15057
 
13010
- function _buildA11yTree(el, depth, maxDepth) {
13011
- if (depth > maxDepth || !el) return '';
13012
-
13013
- const indent = ' '.repeat(depth);
13014
- const tag = el.tagName?.toLowerCase() || '';
13015
- const role = el.getAttribute?.('role') || '';
13016
- const ariaLabel = el.getAttribute?.('aria-label') || '';
13017
- const text = el.childNodes?.length === 1 && el.childNodes[0].nodeType === 3
13018
- ? el.textContent?.trim().slice(0, 80) : '';
13019
-
13020
- let line = `${indent}<${tag}`;
13021
- if (el.id) line += ` id="${el.id}"`;
13022
- if (role) line += ` role="${role}"`;
13023
- if (ariaLabel) line += ` aria-label="${ariaLabel}"`;
13024
- if (el.className && typeof el.className === 'string') {
13025
- const cls = el.className.trim().slice(0, 60);
13026
- if (cls) line += ` class="${cls}"`;
13027
- }
13028
- line += '>';
13029
- if (text) line += ` "${text}"`;
13030
-
13031
- let result = line + '\n';
13032
-
13033
- // Traverse into shadow DOM if present
13034
- const root = el.shadowRoot || el;
13035
- const children = root.children || [];
13036
-
13037
- for (let i = 0; i < children.length && i < 50; i++) {
13038
- result += _buildA11yTree(children[i], depth + 1, maxDepth);
13039
- }
13040
-
13041
- return result;
13042
- }
13043
-
13044
- function _interceptConsole() {
13045
- const levels = ['log', 'warn', 'error'];
13046
- for (const level of levels) {
13047
- const original = console[level];
13048
- console[level] = (...args) => {
13049
- consoleLog.push({
13050
- level,
13051
- message: args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '),
13052
- timestamp: Date.now(),
13053
- });
13054
- if (consoleLog.length > MAX_CONSOLE_LOG) consoleLog.shift();
13055
- original.apply(console, args);
13056
- };
13057
- }
13058
- }
13059
-
13060
- function _interceptNetwork() {
13061
- // ── Intercept fetch() ──
13062
- const originalFetch = window.fetch;
13063
- window.fetch = async function (...args) {
13064
- const start = Date.now();
13065
- const req = args[0];
13066
- const url = typeof req === 'string' ? req : req?.url || '';
13067
- const method = args[1]?.method || (req?.method) || 'GET';
13068
-
13069
- try {
13070
- const response = await originalFetch.apply(window, args);
13071
- const size = parseInt(response.headers?.get('content-length') || '0', 10);
13072
- networkLog.push({
13073
- type: 'fetch', method: method.toUpperCase(), url,
13074
- status: response.status, statusText: response.statusText,
13075
- duration: Date.now() - start, size, timestamp: start,
13076
- });
13077
- if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
13078
- return response;
13079
- } catch (err) {
13080
- networkLog.push({
13081
- type: 'fetch', method: method.toUpperCase(), url,
13082
- status: 0, error: err.message,
13083
- duration: Date.now() - start, timestamp: start,
13084
- });
13085
- if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
13086
- throw err;
13087
- }
13088
- };
13089
-
13090
- // ── Intercept XMLHttpRequest ──
13091
- const origOpen = XMLHttpRequest.prototype.open;
13092
- const origSend = XMLHttpRequest.prototype.send;
13093
-
13094
- XMLHttpRequest.prototype.open = function (method, url, ...rest) {
13095
- this._wuMcp = { method: (method || 'GET').toUpperCase(), url: String(url), start: null };
13096
- return origOpen.call(this, method, url, ...rest);
13097
- };
13098
-
13099
- XMLHttpRequest.prototype.send = function (...args) {
13100
- if (this._wuMcp) {
13101
- this._wuMcp.start = Date.now();
13102
- this.addEventListener('loadend', () => {
13103
- networkLog.push({
13104
- type: 'xhr',
13105
- method: this._wuMcp.method,
13106
- url: this._wuMcp.url,
13107
- status: this.status,
13108
- statusText: this.statusText,
13109
- duration: Date.now() - this._wuMcp.start,
13110
- size: parseInt(this.getResponseHeader('content-length') || '0', 10),
13111
- timestamp: this._wuMcp.start,
13112
- });
13113
- if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
13114
- });
13115
- }
13116
- return origSend.apply(this, args);
13117
- };
13118
- }
13119
-
13120
- function _inlineComputedStyles(source, clone) {
13121
- // Copy computed styles from source to clone for accurate Canvas rendering
13122
- const sourceStyle = window.getComputedStyle(source);
13123
- const important = ['color', 'background', 'background-color', 'font-family',
13124
- 'font-size', 'font-weight', 'border', 'border-radius', 'padding', 'margin',
13125
- 'display', 'flex-direction', 'align-items', 'justify-content', 'gap',
13126
- 'width', 'height', 'max-width', 'max-height', 'overflow', 'opacity',
13127
- 'box-shadow', 'text-align', 'line-height', 'position', 'top', 'left',
13128
- 'right', 'bottom', 'z-index', 'transform', 'visibility'];
13129
-
13130
- for (const prop of important) {
13131
- try {
13132
- const val = sourceStyle.getPropertyValue(prop);
13133
- if (val) clone.style?.setProperty(prop, val);
13134
- } catch (_) { /* skip */ }
13135
- }
13136
-
13137
- // Recurse into children (limit depth for performance)
13138
- const sourceChildren = source.children || [];
13139
- const cloneChildren = clone.children || [];
13140
- const max = Math.min(sourceChildren.length, cloneChildren.length, 200);
13141
- for (let i = 0; i < max; i++) {
13142
- _inlineComputedStyles(sourceChildren[i], cloneChildren[i]);
13143
- }
13144
- }
13145
-
13146
15058
  return { connect, disconnect, isConnected };
13147
15059
  }
13148
15060
 
@@ -13241,12 +15153,12 @@ if (typeof window !== 'undefined') {
13241
15153
  if (!wu.mcp) {
13242
15154
  let _mcpBridge = null;
13243
15155
  wu.mcp = {
13244
- async connect(url = 'ws://localhost:19100') {
15156
+ async connect(url = 'ws://localhost:19100', options = {}) {
13245
15157
  if (!_mcpBridge) {
13246
15158
  const { createMcpBridge } = await Promise.resolve().then(function () { return wuMcpBridge; });
13247
15159
  _mcpBridge = createMcpBridge(wu);
13248
15160
  }
13249
- _mcpBridge.connect(url);
15161
+ _mcpBridge.connect(url, options);
13250
15162
  },
13251
15163
  disconnect() {
13252
15164
  _mcpBridge?.disconnect();
@@ -13302,5 +15214,5 @@ const clearOverrides = () => wu.clearOverrides();
13302
15214
  const usePlugin = (plugin, opts) => wu.pluginSystem.use(plugin, opts);
13303
15215
  const useHook = (phase, middleware, opts) => wu.hooks.use(phase, middleware, opts);
13304
15216
 
13305
- export { WuAI, WuAIActions, WuAIContext, WuAIConversation, WuAIPermissions, WuAIProvider, WuAITriggers, WuApp, WuCache, WuCore, WuErrorBoundary, WuEventBus, WuHtmlParser, WuIframeSandbox, WuLifecycleHooks, WuLoader, WuLoadingStrategy, WuManifest, WuOverrides, WuPerformance, WuPluginSystem, WuPrefetch, WuProxySandbox, WuSandbox, WuScriptExecutor, WuSnapshotSandbox, WuStore, WuStyleBridge, app, clearOverrides, createConditionalHook, createGuardHook, createMcpBridge, createPlugin, createSimpleHook, createTimedHook, createTransformHook, wu_default as default, define, destroy, emit, enableAllLogs, endMeasure, generatePerformanceReport, getOverrides, getState, hide, init, isHidden, mount, off, on, onStateChange, once, override, prefetch, prefetchAll, registerBrowserActions, removeOverride, setState, show, silenceAllLogs, startMeasure, store, unmount, useHook, usePlugin, wu };
15217
+ export { WuAI, WuAIActions, WuAIAgent, WuAIContext, WuAIConversation, WuAIOrchestrate, WuAIPermissions, WuAIProvider, WuAITriggers, WuApp, WuCache, WuCore, WuErrorBoundary, WuEventBus, WuHtmlParser, WuIframeSandbox, WuLifecycleHooks, WuLoader, WuLoadingStrategy, WuManifest, WuOverrides, WuPerformance, WuPluginSystem, WuPrefetch, WuProxySandbox, WuSandbox, WuScriptExecutor, WuSnapshotSandbox, WuStore, WuStyleBridge, app, buildA11yTree, buildToolSchemas, captureScreenshot, clearOverrides, clickElement, createConditionalHook, createGuardHook, createMcpBridge, createPlugin, createSimpleHook, createTimedHook, createTransformHook, wu_default as default, define, destroy, emit, enableAllLogs, endMeasure, ensureInterceptors, estimateTokens, generatePerformanceReport, getFilteredConsole, getFilteredNetwork, getOverrides, getState, hide, init, interpolate, isHidden, mount, normalizeParameters, off, on, onStateChange, once, override, prefetch, prefetchAll, redactSensitive, registerBrowserActions, removeOverride, sanitizeForPrompt, setState, show, silenceAllLogs, startMeasure, store, truncateToTokenBudget, typeIntoElement, unmount, useHook, usePlugin, validateParams, wu };
13306
15218
  //# sourceMappingURL=wu-framework.dev.js.map