wu-framework 1.1.15 → 1.1.17

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 (88) hide show
  1. package/README.md +52 -20
  2. package/dist/wu-framework.cjs.js +1 -1
  3. package/dist/wu-framework.cjs.js.map +1 -1
  4. package/dist/wu-framework.dev.js +15511 -15146
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js +1 -1
  7. package/dist/wu-framework.esm.js.map +1 -1
  8. package/dist/wu-framework.umd.js +1 -1
  9. package/dist/wu-framework.umd.js.map +1 -1
  10. package/package.json +166 -161
  11. package/src/adapters/angular/ai.js +30 -30
  12. package/src/adapters/angular/index.d.ts +154 -154
  13. package/src/adapters/angular/index.js +932 -932
  14. package/src/adapters/angular.d.ts +3 -3
  15. package/src/adapters/angular.js +3 -3
  16. package/src/adapters/index.js +168 -168
  17. package/src/adapters/lit/ai.js +20 -20
  18. package/src/adapters/lit/index.d.ts +120 -120
  19. package/src/adapters/lit/index.js +721 -721
  20. package/src/adapters/lit.d.ts +3 -3
  21. package/src/adapters/lit.js +3 -3
  22. package/src/adapters/preact/ai.js +33 -33
  23. package/src/adapters/preact/index.d.ts +108 -108
  24. package/src/adapters/preact/index.js +661 -661
  25. package/src/adapters/preact.d.ts +3 -3
  26. package/src/adapters/preact.js +3 -3
  27. package/src/adapters/react/index.js +48 -54
  28. package/src/adapters/react.d.ts +3 -3
  29. package/src/adapters/react.js +3 -3
  30. package/src/adapters/shared.js +64 -64
  31. package/src/adapters/solid/ai.js +32 -32
  32. package/src/adapters/solid/index.d.ts +101 -101
  33. package/src/adapters/solid/index.js +586 -586
  34. package/src/adapters/solid.d.ts +3 -3
  35. package/src/adapters/solid.js +3 -3
  36. package/src/adapters/svelte/ai.js +31 -31
  37. package/src/adapters/svelte/index.d.ts +166 -166
  38. package/src/adapters/svelte/index.js +798 -798
  39. package/src/adapters/svelte.d.ts +3 -3
  40. package/src/adapters/svelte.js +3 -3
  41. package/src/adapters/vanilla/ai.js +30 -30
  42. package/src/adapters/vanilla/index.d.ts +179 -179
  43. package/src/adapters/vanilla/index.js +785 -785
  44. package/src/adapters/vanilla.d.ts +3 -3
  45. package/src/adapters/vanilla.js +3 -3
  46. package/src/adapters/vue/ai.js +52 -52
  47. package/src/adapters/vue/index.d.ts +299 -299
  48. package/src/adapters/vue/index.js +610 -610
  49. package/src/adapters/vue.d.ts +3 -3
  50. package/src/adapters/vue.js +3 -3
  51. package/src/ai/wu-ai-actions.js +261 -261
  52. package/src/ai/wu-ai-agent.js +546 -546
  53. package/src/ai/wu-ai-browser-primitives.js +354 -354
  54. package/src/ai/wu-ai-browser.js +380 -380
  55. package/src/ai/wu-ai-context.js +332 -332
  56. package/src/ai/wu-ai-conversation.js +613 -613
  57. package/src/ai/wu-ai-orchestrate.js +1021 -1021
  58. package/src/ai/wu-ai-permissions.js +381 -381
  59. package/src/ai/wu-ai-provider.js +700 -700
  60. package/src/ai/wu-ai-schema.js +225 -225
  61. package/src/ai/wu-ai-triggers.js +396 -396
  62. package/src/ai/wu-ai.js +804 -804
  63. package/src/core/wu-app.js +236 -236
  64. package/src/core/wu-cache.js +498 -477
  65. package/src/core/wu-core.js +1412 -1398
  66. package/src/core/wu-error-boundary.js +396 -382
  67. package/src/core/wu-event-bus.js +390 -348
  68. package/src/core/wu-hooks.js +350 -350
  69. package/src/core/wu-html-parser.js +199 -190
  70. package/src/core/wu-iframe-sandbox.js +328 -328
  71. package/src/core/wu-loader.js +385 -273
  72. package/src/core/wu-logger.js +142 -134
  73. package/src/core/wu-manifest.js +532 -509
  74. package/src/core/wu-mcp-bridge.js +432 -432
  75. package/src/core/wu-overrides.js +510 -510
  76. package/src/core/wu-performance.js +228 -228
  77. package/src/core/wu-plugin.js +401 -348
  78. package/src/core/wu-prefetch.js +414 -414
  79. package/src/core/wu-proxy-sandbox.js +477 -476
  80. package/src/core/wu-sandbox.js +779 -779
  81. package/src/core/wu-script-executor.js +161 -113
  82. package/src/core/wu-snapshot-sandbox.js +227 -227
  83. package/src/core/wu-store.js +13 -3
  84. package/src/core/wu-strategies.js +256 -256
  85. package/src/core/wu-style-bridge.js +477 -477
  86. package/src/index.d.ts +317 -0
  87. package/src/index.js +234 -224
  88. 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
+ }