wu-framework 1.1.14 → 1.1.16

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.
Files changed (90) hide show
  1. package/LICENSE +39 -39
  2. package/README.md +408 -408
  3. package/dist/wu-framework.cjs.js.map +1 -1
  4. package/dist/wu-framework.dev.js +15151 -15151
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js.map +1 -1
  7. package/dist/wu-framework.umd.js.map +1 -1
  8. package/integrations/astro/README.md +127 -127
  9. package/integrations/astro/WuApp.astro +63 -63
  10. package/integrations/astro/WuShell.astro +39 -39
  11. package/integrations/astro/index.js +68 -68
  12. package/integrations/astro/package.json +38 -38
  13. package/integrations/astro/types.d.ts +53 -53
  14. package/package.json +161 -161
  15. package/src/adapters/angular/ai.js +30 -30
  16. package/src/adapters/angular/index.d.ts +154 -154
  17. package/src/adapters/angular/index.js +932 -932
  18. package/src/adapters/angular.d.ts +3 -3
  19. package/src/adapters/angular.js +3 -3
  20. package/src/adapters/index.js +168 -168
  21. package/src/adapters/lit/ai.js +20 -20
  22. package/src/adapters/lit/index.d.ts +120 -120
  23. package/src/adapters/lit/index.js +721 -721
  24. package/src/adapters/lit.d.ts +3 -3
  25. package/src/adapters/lit.js +3 -3
  26. package/src/adapters/preact/ai.js +33 -33
  27. package/src/adapters/preact/index.d.ts +108 -108
  28. package/src/adapters/preact/index.js +661 -661
  29. package/src/adapters/preact.d.ts +3 -3
  30. package/src/adapters/preact.js +3 -3
  31. package/src/adapters/react/index.js +48 -54
  32. package/src/adapters/react.d.ts +3 -3
  33. package/src/adapters/react.js +3 -3
  34. package/src/adapters/shared.js +64 -64
  35. package/src/adapters/solid/ai.js +32 -32
  36. package/src/adapters/solid/index.d.ts +101 -101
  37. package/src/adapters/solid/index.js +586 -586
  38. package/src/adapters/solid.d.ts +3 -3
  39. package/src/adapters/solid.js +3 -3
  40. package/src/adapters/svelte/ai.js +31 -31
  41. package/src/adapters/svelte/index.d.ts +166 -166
  42. package/src/adapters/svelte/index.js +798 -798
  43. package/src/adapters/svelte.d.ts +3 -3
  44. package/src/adapters/svelte.js +3 -3
  45. package/src/adapters/vanilla/ai.js +30 -30
  46. package/src/adapters/vanilla/index.d.ts +179 -179
  47. package/src/adapters/vanilla/index.js +785 -785
  48. package/src/adapters/vanilla.d.ts +3 -3
  49. package/src/adapters/vanilla.js +3 -3
  50. package/src/adapters/vue/ai.js +52 -52
  51. package/src/adapters/vue/index.d.ts +299 -299
  52. package/src/adapters/vue/index.js +610 -610
  53. package/src/adapters/vue.d.ts +3 -3
  54. package/src/adapters/vue.js +3 -3
  55. package/src/ai/wu-ai-actions.js +261 -261
  56. package/src/ai/wu-ai-agent.js +546 -546
  57. package/src/ai/wu-ai-browser-primitives.js +354 -354
  58. package/src/ai/wu-ai-browser.js +380 -380
  59. package/src/ai/wu-ai-context.js +332 -332
  60. package/src/ai/wu-ai-conversation.js +613 -613
  61. package/src/ai/wu-ai-orchestrate.js +1021 -1021
  62. package/src/ai/wu-ai-permissions.js +381 -381
  63. package/src/ai/wu-ai-provider.js +700 -700
  64. package/src/ai/wu-ai-schema.js +225 -225
  65. package/src/ai/wu-ai-triggers.js +396 -396
  66. package/src/ai/wu-ai.js +804 -804
  67. package/src/core/wu-app.js +236 -236
  68. package/src/core/wu-cache.js +477 -477
  69. package/src/core/wu-core.js +1398 -1398
  70. package/src/core/wu-error-boundary.js +382 -382
  71. package/src/core/wu-event-bus.js +348 -348
  72. package/src/core/wu-hooks.js +350 -350
  73. package/src/core/wu-html-parser.js +190 -190
  74. package/src/core/wu-iframe-sandbox.js +328 -328
  75. package/src/core/wu-loader.js +272 -272
  76. package/src/core/wu-logger.js +134 -134
  77. package/src/core/wu-manifest.js +509 -509
  78. package/src/core/wu-mcp-bridge.js +432 -432
  79. package/src/core/wu-overrides.js +510 -510
  80. package/src/core/wu-performance.js +228 -228
  81. package/src/core/wu-plugin.js +348 -348
  82. package/src/core/wu-prefetch.js +414 -414
  83. package/src/core/wu-proxy-sandbox.js +476 -476
  84. package/src/core/wu-sandbox.js +779 -779
  85. package/src/core/wu-script-executor.js +113 -113
  86. package/src/core/wu-snapshot-sandbox.js +227 -227
  87. package/src/core/wu-strategies.js +256 -256
  88. package/src/core/wu-style-bridge.js +477 -477
  89. package/src/index.js +224 -224
  90. package/src/utils/dependency-resolver.js +327 -327
@@ -1,432 +1,432 @@
1
- /**
2
- * WU-MCP Bridge (Browser Side)
3
- *
4
- * Connects to the wu-mcp-server via WebSocket and executes
5
- * commands using wu.* APIs. This is the "eyes and hands" of
6
- * the MCP server inside the browser.
7
- *
8
- * Security:
9
- * - Optional auth token sent on first message (handshake)
10
- * - All state/event/mount operations check wu.ai permissions
11
- * - Mutating operations emit audit events
12
- * - Read-only operations (status, list_apps, snapshot, console, network) are unrestricted
13
- *
14
- * @example
15
- * // Connect with auth token
16
- * wu.mcp.connect('ws://localhost:19100', { token: 'my-secret' });
17
- *
18
- * // Connect without auth (development only)
19
- * wu.mcp.connect();
20
- */
21
-
22
- import {
23
- ensureInterceptors,
24
- networkLog,
25
- consoleLog,
26
- captureScreenshot,
27
- buildA11yTree,
28
- clickElement,
29
- typeIntoElement,
30
- getFilteredNetwork,
31
- getFilteredConsole,
32
- } from '../ai/wu-ai-browser-primitives.js';
33
- import { logger } from './wu-logger.js';
34
-
35
- /**
36
- * Create the MCP bridge for a Wu instance.
37
- *
38
- * @param {object} wu - The Wu Framework instance (window.wu)
39
- * @returns {object} Bridge API: { connect, disconnect, isConnected }
40
- */
41
- export function createMcpBridge(wu) {
42
- let ws = null;
43
- let reconnectTimer = null;
44
- let reconnectAttempts = 0;
45
- let authenticated = false;
46
- let authToken = null;
47
- const MAX_RECONNECT_ATTEMPTS = 10;
48
- const RECONNECT_DELAY = 2000;
49
-
50
- // Event log for wu_list_events
51
- const eventLog = [];
52
- const MAX_EVENT_LOG = 200;
53
-
54
- // Capture events for history
55
- if (wu.eventBus) {
56
- wu.eventBus.on('*', (event) => {
57
- eventLog.push({
58
- name: event.name,
59
- data: event.data,
60
- timestamp: event.timestamp || Date.now(),
61
- source: event.source || 'unknown',
62
- });
63
- if (eventLog.length > MAX_EVENT_LOG) eventLog.shift();
64
- });
65
- }
66
-
67
- // Install shared interceptors (idempotent — safe if wu-ai-browser already did it)
68
- ensureInterceptors();
69
-
70
- // ── Permission helpers ──
71
-
72
- /**
73
- * Check a permission flag via wu.ai.permissions if available.
74
- * Falls back to deny if wu.ai is not initialized.
75
- */
76
- function _checkPermission(perm) {
77
- if (wu.ai && wu.ai.permissions) {
78
- return wu.ai.permissions.check(perm);
79
- }
80
- // If AI module not initialized, deny write operations, allow reads
81
- const readPerms = ['readStore', 'executeActions'];
82
- return readPerms.includes(perm);
83
- }
84
-
85
- /**
86
- * Emit an audit event for bridge operations.
87
- */
88
- function _audit(operation, params, result) {
89
- if (wu.eventBus) {
90
- wu.eventBus.emit('mcp:bridge:operation', {
91
- operation,
92
- params,
93
- result: result?.error ? { error: result.error } : { success: true },
94
- timestamp: Date.now(),
95
- }, { appName: 'wu-mcp-bridge' });
96
- }
97
- }
98
-
99
- // ── Command handlers ──
100
-
101
- const handlers = {
102
- // ── Read-only operations (no permission gates) ──
103
-
104
- status() {
105
- return {
106
- connected: true,
107
- framework: 'wu-framework',
108
- apps: _getAppList(),
109
- storeKeys: wu.store ? Object.keys(wu.store.get('') || {}) : [],
110
- actionsCount: wu.ai ? wu.ai.tools().length : 0,
111
- eventLogSize: eventLog.length,
112
- };
113
- },
114
-
115
- list_apps() {
116
- return _getAppList();
117
- },
118
-
119
- list_events({ limit = 20 }) {
120
- return eventLog.slice(-limit);
121
- },
122
-
123
- list_actions() {
124
- if (!wu.ai) return { actions: [], note: 'wu.ai not initialized' };
125
- const tools = wu.ai.tools();
126
- return { actions: tools, count: tools.length };
127
- },
128
-
129
- snapshot({ appName }) {
130
- try {
131
- const target = appName
132
- ? document.querySelector(`[data-wu-app="${appName}"]`) || document.querySelector(`#wu-app-${appName}`)
133
- : document.body;
134
-
135
- if (!target) return { error: `App "${appName}" not found in DOM` };
136
-
137
- return {
138
- app: appName || '(page)',
139
- snapshot: buildA11yTree(target, 0, 5),
140
- timestamp: Date.now(),
141
- };
142
- } catch (err) {
143
- return { error: err.message };
144
- }
145
- },
146
-
147
- console({ level = 'all', limit = 50 }) {
148
- return getFilteredConsole(level, limit);
149
- },
150
-
151
- async screenshot({ selector, quality = 0.8 }) {
152
- const result = await captureScreenshot(selector, quality);
153
- if (!result.error) result.timestamp = Date.now();
154
- return result;
155
- },
156
-
157
- network({ method, status, limit = 50 }) {
158
- return getFilteredNetwork(method, status, limit);
159
- },
160
-
161
- // ── Permission-gated operations ──
162
-
163
- get_state({ path }) {
164
- if (!wu.store) return { error: 'wu.store not available' };
165
- if (!_checkPermission('readStore')) {
166
- return { error: 'Permission denied: readStore is disabled' };
167
- }
168
- const value = wu.store.get(path || '');
169
- return { path: path || '(root)', value };
170
- },
171
-
172
- set_state({ path, value }) {
173
- if (!wu.store) return { error: 'wu.store not available' };
174
- if (!path) return { error: 'path is required' };
175
- if (!_checkPermission('writeStore')) {
176
- _audit('set_state', { path }, { error: 'Permission denied' });
177
- return { error: 'Permission denied: writeStore is disabled' };
178
- }
179
- wu.store.set(path, value);
180
- _audit('set_state', { path, value }, { success: true });
181
- return { path, value, updated: true };
182
- },
183
-
184
- emit_event({ event, data }) {
185
- if (!wu.eventBus) return { error: 'wu.eventBus not available' };
186
- if (!event) return { error: 'event name is required' };
187
- if (!_checkPermission('emitEvents')) {
188
- _audit('emit_event', { event }, { error: 'Permission denied' });
189
- return { error: 'Permission denied: emitEvents is disabled' };
190
- }
191
- wu.eventBus.emit(event, data, { appName: 'wu-mcp-bridge' });
192
- _audit('emit_event', { event, data }, { success: true });
193
- return { emitted: event, data };
194
- },
195
-
196
- navigate({ route }) {
197
- if (!route) return { error: 'Route is required' };
198
- if (!_checkPermission('emitEvents')) {
199
- _audit('navigate', { route }, { error: 'Permission denied: emitEvents' });
200
- return { error: 'Permission denied: emitEvents is disabled' };
201
- }
202
- if (wu.eventBus) {
203
- wu.eventBus.emit('shell:navigate', { route }, { appName: 'wu-mcp-bridge' });
204
- }
205
- if (wu.store && _checkPermission('writeStore')) {
206
- wu.store.set('currentPath', route);
207
- }
208
- _audit('navigate', { route }, { success: true });
209
- return { navigated: route };
210
- },
211
-
212
- mount_app({ appName, container }) {
213
- if (!appName) return { error: 'appName is required' };
214
- if (!_checkPermission('modifyDOM')) {
215
- _audit('mount_app', { appName }, { error: 'Permission denied' });
216
- return { error: 'Permission denied: modifyDOM is disabled' };
217
- }
218
- try {
219
- if (wu.mount) {
220
- wu.mount(appName, container);
221
- _audit('mount_app', { appName, container }, { success: true });
222
- return { mounted: appName, container };
223
- }
224
- return { error: 'wu.mount not available' };
225
- } catch (err) {
226
- return { error: err.message };
227
- }
228
- },
229
-
230
- unmount_app({ appName }) {
231
- if (!appName) return { error: 'appName is required' };
232
- if (!_checkPermission('modifyDOM')) {
233
- _audit('unmount_app', { appName }, { error: 'Permission denied' });
234
- return { error: 'Permission denied: modifyDOM is disabled' };
235
- }
236
- try {
237
- if (wu.unmount) {
238
- wu.unmount(appName);
239
- _audit('unmount_app', { appName }, { success: true });
240
- return { unmounted: appName };
241
- }
242
- return { error: 'wu.unmount not available' };
243
- } catch (err) {
244
- return { error: err.message };
245
- }
246
- },
247
-
248
- click({ selector, text }) {
249
- if (!_checkPermission('modifyDOM')) {
250
- return { error: 'Permission denied: modifyDOM is disabled' };
251
- }
252
- const result = clickElement(selector, text);
253
- _audit('click', { selector, text }, result);
254
- return result;
255
- },
256
-
257
- type({ selector, text, clear = false, submit = false }) {
258
- if (!_checkPermission('modifyDOM')) {
259
- return { error: 'Permission denied: modifyDOM is disabled' };
260
- }
261
- const result = typeIntoElement(selector, text, { clear, submit });
262
- _audit('type', { selector, textLength: text?.length }, result);
263
- return result;
264
- },
265
-
266
- async execute_action({ action, params }) {
267
- if (!wu.ai) return { error: 'wu.ai not available' };
268
- if (!action) return { error: 'action name is required' };
269
-
270
- try {
271
- // Execute through public API (respects permissions, validation, audit)
272
- const result = await wu.ai.execute(action, params || {});
273
- return { action, ...result };
274
- } catch (err) {
275
- return { error: err.message };
276
- }
277
- },
278
- };
279
-
280
- // ── WebSocket connection ──
281
-
282
- function connect(url = 'ws://localhost:19100', options = {}) {
283
- if (ws && ws.readyState <= 1) {
284
- logger.warn('[wu-mcp-bridge] Already connected or connecting');
285
- return;
286
- }
287
-
288
- authToken = options.token || null;
289
- authenticated = !authToken; // No token = auto-authenticated (dev mode)
290
-
291
- try {
292
- ws = new WebSocket(url);
293
-
294
- ws.onopen = () => {
295
- logger.debug('[wu-mcp-bridge] Connected to wu-mcp-server');
296
- reconnectAttempts = 0;
297
-
298
- // Send auth handshake if token provided
299
- if (authToken) {
300
- ws.send(JSON.stringify({
301
- type: 'auth',
302
- token: authToken,
303
- }));
304
- }
305
- };
306
-
307
- ws.onmessage = async (event) => {
308
- try {
309
- const msg = JSON.parse(event.data);
310
-
311
- // Handle auth response
312
- if (msg.type === 'auth_result') {
313
- authenticated = msg.success === true;
314
- if (!authenticated) {
315
- console.error('[wu-mcp-bridge] Authentication failed:', msg.reason || 'Invalid token');
316
- disconnect();
317
- } else {
318
- logger.debug('[wu-mcp-bridge] Authenticated successfully');
319
- }
320
- return;
321
- }
322
-
323
- // Reject commands if not authenticated
324
- if (!authenticated) {
325
- if (msg.id) {
326
- _respond(msg.id, null, 'Not authenticated. Send auth token first.');
327
- }
328
- return;
329
- }
330
-
331
- const { id, command, params } = msg;
332
-
333
- if (!id || !command) {
334
- logger.warn('[wu-mcp-bridge] Invalid message:', msg);
335
- return;
336
- }
337
-
338
- const handler = handlers[command];
339
- if (!handler) {
340
- _respond(id, null, `Unknown command: ${command}`);
341
- return;
342
- }
343
-
344
- try {
345
- const result = await handler(params || {});
346
- _respond(id, result);
347
- } catch (err) {
348
- _respond(id, null, err.message);
349
- }
350
- } catch (err) {
351
- console.error('[wu-mcp-bridge] Failed to handle message:', err);
352
- }
353
- };
354
-
355
- ws.onclose = () => {
356
- logger.debug('[wu-mcp-bridge] Disconnected');
357
- ws = null;
358
- authenticated = false;
359
- _scheduleReconnect(url, options);
360
- };
361
-
362
- ws.onerror = () => {
363
- // onclose will fire after this
364
- };
365
- } catch (err) {
366
- console.error('[wu-mcp-bridge] Connection failed:', err.message);
367
- _scheduleReconnect(url, options);
368
- }
369
- }
370
-
371
- function disconnect() {
372
- if (reconnectTimer) {
373
- clearTimeout(reconnectTimer);
374
- reconnectTimer = null;
375
- }
376
- reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // prevent reconnect
377
- if (ws) {
378
- ws.close();
379
- ws = null;
380
- }
381
- authenticated = false;
382
- }
383
-
384
- function isConnected() {
385
- return ws !== null && ws.readyState === 1 && authenticated;
386
- }
387
-
388
- // ── Private helpers ──
389
-
390
- function _respond(id, result, error) {
391
- if (!ws || ws.readyState !== 1) return;
392
- const msg = error ? { id, error } : { id, result };
393
- ws.send(JSON.stringify(msg));
394
- }
395
-
396
- function _scheduleReconnect(url, options) {
397
- if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
398
- reconnectAttempts++;
399
- const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5);
400
- reconnectTimer = setTimeout(() => connect(url, options), delay);
401
- }
402
-
403
- function _getAppList() {
404
- const apps = [];
405
-
406
- if (wu._apps) {
407
- for (const [name, app] of Object.entries(wu._apps)) {
408
- apps.push({
409
- name,
410
- mounted: app.mounted || app.isMounted || false,
411
- url: app.url || app.info?.url || '',
412
- status: app.status || app.info?.status || 'unknown',
413
- });
414
- }
415
- }
416
-
417
- // Fallback: scan DOM for wu-app elements
418
- if (apps.length === 0) {
419
- document.querySelectorAll('[data-wu-app]').forEach((el) => {
420
- apps.push({
421
- name: el.getAttribute('data-wu-app'),
422
- mounted: true,
423
- container: `#${el.id || '(no-id)'}`,
424
- });
425
- });
426
- }
427
-
428
- return apps;
429
- }
430
-
431
- return { connect, disconnect, isConnected };
432
- }
1
+ /**
2
+ * WU-MCP Bridge (Browser Side)
3
+ *
4
+ * Connects to the wu-mcp-server via WebSocket and executes
5
+ * commands using wu.* APIs. This is the "eyes and hands" of
6
+ * the MCP server inside the browser.
7
+ *
8
+ * Security:
9
+ * - Optional auth token sent on first message (handshake)
10
+ * - All state/event/mount operations check wu.ai permissions
11
+ * - Mutating operations emit audit events
12
+ * - Read-only operations (status, list_apps, snapshot, console, network) are unrestricted
13
+ *
14
+ * @example
15
+ * // Connect with auth token
16
+ * wu.mcp.connect('ws://localhost:19100', { token: 'my-secret' });
17
+ *
18
+ * // Connect without auth (development only)
19
+ * wu.mcp.connect();
20
+ */
21
+
22
+ import {
23
+ ensureInterceptors,
24
+ networkLog,
25
+ consoleLog,
26
+ captureScreenshot,
27
+ buildA11yTree,
28
+ clickElement,
29
+ typeIntoElement,
30
+ getFilteredNetwork,
31
+ getFilteredConsole,
32
+ } from '../ai/wu-ai-browser-primitives.js';
33
+ import { logger } from './wu-logger.js';
34
+
35
+ /**
36
+ * Create the MCP bridge for a Wu instance.
37
+ *
38
+ * @param {object} wu - The Wu Framework instance (window.wu)
39
+ * @returns {object} Bridge API: { connect, disconnect, isConnected }
40
+ */
41
+ export function createMcpBridge(wu) {
42
+ let ws = null;
43
+ let reconnectTimer = null;
44
+ let reconnectAttempts = 0;
45
+ let authenticated = false;
46
+ let authToken = null;
47
+ const MAX_RECONNECT_ATTEMPTS = 10;
48
+ const RECONNECT_DELAY = 2000;
49
+
50
+ // Event log for wu_list_events
51
+ const eventLog = [];
52
+ const MAX_EVENT_LOG = 200;
53
+
54
+ // Capture events for history
55
+ if (wu.eventBus) {
56
+ wu.eventBus.on('*', (event) => {
57
+ eventLog.push({
58
+ name: event.name,
59
+ data: event.data,
60
+ timestamp: event.timestamp || Date.now(),
61
+ source: event.source || 'unknown',
62
+ });
63
+ if (eventLog.length > MAX_EVENT_LOG) eventLog.shift();
64
+ });
65
+ }
66
+
67
+ // Install shared interceptors (idempotent — safe if wu-ai-browser already did it)
68
+ ensureInterceptors();
69
+
70
+ // ── Permission helpers ──
71
+
72
+ /**
73
+ * Check a permission flag via wu.ai.permissions if available.
74
+ * Falls back to deny if wu.ai is not initialized.
75
+ */
76
+ function _checkPermission(perm) {
77
+ if (wu.ai && wu.ai.permissions) {
78
+ return wu.ai.permissions.check(perm);
79
+ }
80
+ // If AI module not initialized, deny write operations, allow reads
81
+ const readPerms = ['readStore', 'executeActions'];
82
+ return readPerms.includes(perm);
83
+ }
84
+
85
+ /**
86
+ * Emit an audit event for bridge operations.
87
+ */
88
+ function _audit(operation, params, result) {
89
+ if (wu.eventBus) {
90
+ wu.eventBus.emit('mcp:bridge:operation', {
91
+ operation,
92
+ params,
93
+ result: result?.error ? { error: result.error } : { success: true },
94
+ timestamp: Date.now(),
95
+ }, { appName: 'wu-mcp-bridge' });
96
+ }
97
+ }
98
+
99
+ // ── Command handlers ──
100
+
101
+ const handlers = {
102
+ // ── Read-only operations (no permission gates) ──
103
+
104
+ status() {
105
+ return {
106
+ connected: true,
107
+ framework: 'wu-framework',
108
+ apps: _getAppList(),
109
+ storeKeys: wu.store ? Object.keys(wu.store.get('') || {}) : [],
110
+ actionsCount: wu.ai ? wu.ai.tools().length : 0,
111
+ eventLogSize: eventLog.length,
112
+ };
113
+ },
114
+
115
+ list_apps() {
116
+ return _getAppList();
117
+ },
118
+
119
+ list_events({ limit = 20 }) {
120
+ return eventLog.slice(-limit);
121
+ },
122
+
123
+ list_actions() {
124
+ if (!wu.ai) return { actions: [], note: 'wu.ai not initialized' };
125
+ const tools = wu.ai.tools();
126
+ return { actions: tools, count: tools.length };
127
+ },
128
+
129
+ snapshot({ appName }) {
130
+ try {
131
+ const target = appName
132
+ ? document.querySelector(`[data-wu-app="${appName}"]`) || document.querySelector(`#wu-app-${appName}`)
133
+ : document.body;
134
+
135
+ if (!target) return { error: `App "${appName}" not found in DOM` };
136
+
137
+ return {
138
+ app: appName || '(page)',
139
+ snapshot: buildA11yTree(target, 0, 5),
140
+ timestamp: Date.now(),
141
+ };
142
+ } catch (err) {
143
+ return { error: err.message };
144
+ }
145
+ },
146
+
147
+ console({ level = 'all', limit = 50 }) {
148
+ return getFilteredConsole(level, limit);
149
+ },
150
+
151
+ async screenshot({ selector, quality = 0.8 }) {
152
+ const result = await captureScreenshot(selector, quality);
153
+ if (!result.error) result.timestamp = Date.now();
154
+ return result;
155
+ },
156
+
157
+ network({ method, status, limit = 50 }) {
158
+ return getFilteredNetwork(method, status, limit);
159
+ },
160
+
161
+ // ── Permission-gated operations ──
162
+
163
+ get_state({ path }) {
164
+ if (!wu.store) return { error: 'wu.store not available' };
165
+ if (!_checkPermission('readStore')) {
166
+ return { error: 'Permission denied: readStore is disabled' };
167
+ }
168
+ const value = wu.store.get(path || '');
169
+ return { path: path || '(root)', value };
170
+ },
171
+
172
+ set_state({ path, value }) {
173
+ if (!wu.store) return { error: 'wu.store not available' };
174
+ if (!path) return { error: 'path is required' };
175
+ if (!_checkPermission('writeStore')) {
176
+ _audit('set_state', { path }, { error: 'Permission denied' });
177
+ return { error: 'Permission denied: writeStore is disabled' };
178
+ }
179
+ wu.store.set(path, value);
180
+ _audit('set_state', { path, value }, { success: true });
181
+ return { path, value, updated: true };
182
+ },
183
+
184
+ emit_event({ event, data }) {
185
+ if (!wu.eventBus) return { error: 'wu.eventBus not available' };
186
+ if (!event) return { error: 'event name is required' };
187
+ if (!_checkPermission('emitEvents')) {
188
+ _audit('emit_event', { event }, { error: 'Permission denied' });
189
+ return { error: 'Permission denied: emitEvents is disabled' };
190
+ }
191
+ wu.eventBus.emit(event, data, { appName: 'wu-mcp-bridge' });
192
+ _audit('emit_event', { event, data }, { success: true });
193
+ return { emitted: event, data };
194
+ },
195
+
196
+ navigate({ route }) {
197
+ if (!route) return { error: 'Route is required' };
198
+ if (!_checkPermission('emitEvents')) {
199
+ _audit('navigate', { route }, { error: 'Permission denied: emitEvents' });
200
+ return { error: 'Permission denied: emitEvents is disabled' };
201
+ }
202
+ if (wu.eventBus) {
203
+ wu.eventBus.emit('shell:navigate', { route }, { appName: 'wu-mcp-bridge' });
204
+ }
205
+ if (wu.store && _checkPermission('writeStore')) {
206
+ wu.store.set('currentPath', route);
207
+ }
208
+ _audit('navigate', { route }, { success: true });
209
+ return { navigated: route };
210
+ },
211
+
212
+ mount_app({ appName, container }) {
213
+ if (!appName) return { error: 'appName is required' };
214
+ if (!_checkPermission('modifyDOM')) {
215
+ _audit('mount_app', { appName }, { error: 'Permission denied' });
216
+ return { error: 'Permission denied: modifyDOM is disabled' };
217
+ }
218
+ try {
219
+ if (wu.mount) {
220
+ wu.mount(appName, container);
221
+ _audit('mount_app', { appName, container }, { success: true });
222
+ return { mounted: appName, container };
223
+ }
224
+ return { error: 'wu.mount not available' };
225
+ } catch (err) {
226
+ return { error: err.message };
227
+ }
228
+ },
229
+
230
+ unmount_app({ appName }) {
231
+ if (!appName) return { error: 'appName is required' };
232
+ if (!_checkPermission('modifyDOM')) {
233
+ _audit('unmount_app', { appName }, { error: 'Permission denied' });
234
+ return { error: 'Permission denied: modifyDOM is disabled' };
235
+ }
236
+ try {
237
+ if (wu.unmount) {
238
+ wu.unmount(appName);
239
+ _audit('unmount_app', { appName }, { success: true });
240
+ return { unmounted: appName };
241
+ }
242
+ return { error: 'wu.unmount not available' };
243
+ } catch (err) {
244
+ return { error: err.message };
245
+ }
246
+ },
247
+
248
+ click({ selector, text }) {
249
+ if (!_checkPermission('modifyDOM')) {
250
+ return { error: 'Permission denied: modifyDOM is disabled' };
251
+ }
252
+ const result = clickElement(selector, text);
253
+ _audit('click', { selector, text }, result);
254
+ return result;
255
+ },
256
+
257
+ type({ selector, text, clear = false, submit = false }) {
258
+ if (!_checkPermission('modifyDOM')) {
259
+ return { error: 'Permission denied: modifyDOM is disabled' };
260
+ }
261
+ const result = typeIntoElement(selector, text, { clear, submit });
262
+ _audit('type', { selector, textLength: text?.length }, result);
263
+ return result;
264
+ },
265
+
266
+ async execute_action({ action, params }) {
267
+ if (!wu.ai) return { error: 'wu.ai not available' };
268
+ if (!action) return { error: 'action name is required' };
269
+
270
+ try {
271
+ // Execute through public API (respects permissions, validation, audit)
272
+ const result = await wu.ai.execute(action, params || {});
273
+ return { action, ...result };
274
+ } catch (err) {
275
+ return { error: err.message };
276
+ }
277
+ },
278
+ };
279
+
280
+ // ── WebSocket connection ──
281
+
282
+ function connect(url = 'ws://localhost:19100', options = {}) {
283
+ if (ws && ws.readyState <= 1) {
284
+ logger.warn('[wu-mcp-bridge] Already connected or connecting');
285
+ return;
286
+ }
287
+
288
+ authToken = options.token || null;
289
+ authenticated = !authToken; // No token = auto-authenticated (dev mode)
290
+
291
+ try {
292
+ ws = new WebSocket(url);
293
+
294
+ ws.onopen = () => {
295
+ logger.debug('[wu-mcp-bridge] Connected to wu-mcp-server');
296
+ reconnectAttempts = 0;
297
+
298
+ // Send auth handshake if token provided
299
+ if (authToken) {
300
+ ws.send(JSON.stringify({
301
+ type: 'auth',
302
+ token: authToken,
303
+ }));
304
+ }
305
+ };
306
+
307
+ ws.onmessage = async (event) => {
308
+ try {
309
+ const msg = JSON.parse(event.data);
310
+
311
+ // Handle auth response
312
+ if (msg.type === 'auth_result') {
313
+ authenticated = msg.success === true;
314
+ if (!authenticated) {
315
+ console.error('[wu-mcp-bridge] Authentication failed:', msg.reason || 'Invalid token');
316
+ disconnect();
317
+ } else {
318
+ logger.debug('[wu-mcp-bridge] Authenticated successfully');
319
+ }
320
+ return;
321
+ }
322
+
323
+ // Reject commands if not authenticated
324
+ if (!authenticated) {
325
+ if (msg.id) {
326
+ _respond(msg.id, null, 'Not authenticated. Send auth token first.');
327
+ }
328
+ return;
329
+ }
330
+
331
+ const { id, command, params } = msg;
332
+
333
+ if (!id || !command) {
334
+ logger.warn('[wu-mcp-bridge] Invalid message:', msg);
335
+ return;
336
+ }
337
+
338
+ const handler = handlers[command];
339
+ if (!handler) {
340
+ _respond(id, null, `Unknown command: ${command}`);
341
+ return;
342
+ }
343
+
344
+ try {
345
+ const result = await handler(params || {});
346
+ _respond(id, result);
347
+ } catch (err) {
348
+ _respond(id, null, err.message);
349
+ }
350
+ } catch (err) {
351
+ console.error('[wu-mcp-bridge] Failed to handle message:', err);
352
+ }
353
+ };
354
+
355
+ ws.onclose = () => {
356
+ logger.debug('[wu-mcp-bridge] Disconnected');
357
+ ws = null;
358
+ authenticated = false;
359
+ _scheduleReconnect(url, options);
360
+ };
361
+
362
+ ws.onerror = () => {
363
+ // onclose will fire after this
364
+ };
365
+ } catch (err) {
366
+ console.error('[wu-mcp-bridge] Connection failed:', err.message);
367
+ _scheduleReconnect(url, options);
368
+ }
369
+ }
370
+
371
+ function disconnect() {
372
+ if (reconnectTimer) {
373
+ clearTimeout(reconnectTimer);
374
+ reconnectTimer = null;
375
+ }
376
+ reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // prevent reconnect
377
+ if (ws) {
378
+ ws.close();
379
+ ws = null;
380
+ }
381
+ authenticated = false;
382
+ }
383
+
384
+ function isConnected() {
385
+ return ws !== null && ws.readyState === 1 && authenticated;
386
+ }
387
+
388
+ // ── Private helpers ──
389
+
390
+ function _respond(id, result, error) {
391
+ if (!ws || ws.readyState !== 1) return;
392
+ const msg = error ? { id, error } : { id, result };
393
+ ws.send(JSON.stringify(msg));
394
+ }
395
+
396
+ function _scheduleReconnect(url, options) {
397
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
398
+ reconnectAttempts++;
399
+ const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5);
400
+ reconnectTimer = setTimeout(() => connect(url, options), delay);
401
+ }
402
+
403
+ function _getAppList() {
404
+ const apps = [];
405
+
406
+ if (wu._apps) {
407
+ for (const [name, app] of Object.entries(wu._apps)) {
408
+ apps.push({
409
+ name,
410
+ mounted: app.mounted || app.isMounted || false,
411
+ url: app.url || app.info?.url || '',
412
+ status: app.status || app.info?.status || 'unknown',
413
+ });
414
+ }
415
+ }
416
+
417
+ // Fallback: scan DOM for wu-app elements
418
+ if (apps.length === 0) {
419
+ document.querySelectorAll('[data-wu-app]').forEach((el) => {
420
+ apps.push({
421
+ name: el.getAttribute('data-wu-app'),
422
+ mounted: true,
423
+ container: `#${el.id || '(no-id)'}`,
424
+ });
425
+ });
426
+ }
427
+
428
+ return apps;
429
+ }
430
+
431
+ return { connect, disconnect, isConnected };
432
+ }