@mobileaidev/ai-app-bridge-web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # AI App Bridge Web
2
+
3
+ Browser-side SDK for AI App Bridge Web sessions. Use it in debug/test builds
4
+ to let an agent communicate with a running web page through the desktop AI App
5
+ Bridge MCP provider.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install --save-dev @mobileaidev/ai-app-bridge-web
11
+ ```
12
+
13
+ The agent side uses the desktop package:
14
+
15
+ ```bash
16
+ npm install -g @mobileaidev/ai-app-bridge
17
+ ```
18
+
19
+ ## Use With Bundlers
20
+
21
+ The SDK is framework-agnostic and can be used from React, Vue, Svelte, vanilla
22
+ JavaScript, Vite, Next.js client code, or any browser-only entry point.
23
+
24
+ ```js
25
+ import { createAiAppBridge } from "@mobileaidev/ai-app-bridge-web";
26
+
27
+ const bridge = createAiAppBridge({
28
+ endpoint: "ws://127.0.0.1:18180/ai-app-bridge-web",
29
+ token: "session-token",
30
+ appName: "demo-web-app",
31
+ capture: {
32
+ console: true,
33
+ errors: true,
34
+ fetch: true,
35
+ xhr: true
36
+ }
37
+ });
38
+
39
+ bridge.start();
40
+ ```
41
+
42
+ For SSR frameworks, initialize it only in client/browser code.
43
+
44
+ ## Script Tag
45
+
46
+ ```html
47
+ <script src="./node_modules/@mobileaidev/ai-app-bridge-web/src/index.js"></script>
48
+ <script>
49
+ const bridge = AiAppBridgeWeb.createAiAppBridge({
50
+ endpoint: "ws://127.0.0.1:18180/ai-app-bridge-web",
51
+ token: "session-token",
52
+ appName: "demo-web-app",
53
+ capture: {
54
+ console: true,
55
+ errors: true,
56
+ fetch: true,
57
+ xhr: true
58
+ }
59
+ });
60
+
61
+ bridge.registerAction("demo.increment", async () => {
62
+ window.count = (window.count || 0) + 1;
63
+ bridge.recordState("demo", "count", window.count);
64
+ return window.count;
65
+ });
66
+
67
+ bridge.start();
68
+ </script>
69
+ ```
70
+
71
+ This package is intended for debug and test builds. Keep command handlers
72
+ whitelisted and do not enable it in production without a deliberate security
73
+ review.
@@ -0,0 +1,156 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>AI App Bridge Web Demo</title>
6
+ <style>
7
+ body {
8
+ color: #1f2937;
9
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
10
+ margin: 32px;
11
+ }
12
+
13
+ main {
14
+ display: grid;
15
+ gap: 16px;
16
+ max-width: 720px;
17
+ }
18
+
19
+ .toolbar {
20
+ display: flex;
21
+ flex-wrap: wrap;
22
+ gap: 8px;
23
+ }
24
+
25
+ button,
26
+ input {
27
+ border: 1px solid #9ca3af;
28
+ border-radius: 6px;
29
+ font: inherit;
30
+ min-height: 38px;
31
+ padding: 8px 10px;
32
+ }
33
+
34
+ button {
35
+ background: #f3f4f6;
36
+ cursor: pointer;
37
+ }
38
+
39
+ button:hover {
40
+ background: #e5e7eb;
41
+ }
42
+
43
+ output,
44
+ pre {
45
+ background: #f9fafb;
46
+ border: 1px solid #d1d5db;
47
+ border-radius: 6px;
48
+ min-height: 24px;
49
+ padding: 10px;
50
+ }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <main>
55
+ <h1>AI App Bridge Web Demo</h1>
56
+ <div class="toolbar">
57
+ <button id="increment" data-testid="increment">Increment</button>
58
+ <button id="log" data-testid="log">Write Log</button>
59
+ <button id="network" data-testid="network">Run Network Probe</button>
60
+ </div>
61
+ <input id="note" aria-label="Note" placeholder="Note" value="ready">
62
+ <output id="count">Count: 0</output>
63
+ <pre id="evidence">Waiting for bridge connection...</pre>
64
+ </main>
65
+ <script src="../src/index.js"></script>
66
+ <script>
67
+ const params = new URLSearchParams(location.search);
68
+ const bridge = AiAppBridgeWeb.createAiAppBridge({
69
+ endpoint: params.get('endpoint') || 'ws://127.0.0.1:18180/ai-app-bridge-web',
70
+ token: params.get('token') || '',
71
+ appName: 'ai-app-bridge-web-demo',
72
+ capture: {
73
+ console: true,
74
+ errors: true,
75
+ fetch: true,
76
+ xhr: true
77
+ }
78
+ });
79
+
80
+ let count = 0;
81
+ let networkStatus = 'idle';
82
+ const countNode = document.querySelector('#count');
83
+ const noteNode = document.querySelector('#note');
84
+ const evidenceNode = document.querySelector('#evidence');
85
+
86
+ function render() {
87
+ countNode.textContent = `Count: ${count}`;
88
+ evidenceNode.textContent = JSON.stringify({
89
+ connected: bridge.isConnected(),
90
+ count,
91
+ note: noteNode.value,
92
+ networkStatus
93
+ }, null, 2);
94
+ }
95
+
96
+ function recordDemoState(reason) {
97
+ bridge.recordState('demo', 'count', count);
98
+ bridge.recordState('demo', 'note', noteNode.value);
99
+ bridge.recordEvent('demo', reason, { count, note: noteNode.value });
100
+ render();
101
+ }
102
+
103
+ document.querySelector('#increment').addEventListener('click', () => {
104
+ count += 1;
105
+ console.info('AI App Bridge demo increment', count);
106
+ recordDemoState('increment_clicked');
107
+ });
108
+
109
+ document.querySelector('#log').addEventListener('click', () => {
110
+ bridge.recordLog('info', 'demo', 'manual log from page', {
111
+ count,
112
+ note: noteNode.value
113
+ });
114
+ recordDemoState('log_clicked');
115
+ });
116
+
117
+ document.querySelector('#network').addEventListener('click', async () => {
118
+ networkStatus = 'loading';
119
+ render();
120
+ try {
121
+ const response = await fetch('/probe.json?from=ai-app-bridge-web-demo');
122
+ networkStatus = `${response.status} ${await response.text()}`;
123
+ } catch (error) {
124
+ networkStatus = `error ${error.message}`;
125
+ }
126
+ recordDemoState('network_probe_finished');
127
+ });
128
+
129
+ noteNode.addEventListener('input', () => {
130
+ recordDemoState('note_changed');
131
+ });
132
+
133
+ bridge.registerAction('demo.increment', async () => {
134
+ document.querySelector('#increment').click();
135
+ return { count };
136
+ });
137
+
138
+ bridge.registerAction('demo.setNote', async (args) => {
139
+ noteNode.value = args.value || '';
140
+ recordDemoState('agent_set_note');
141
+ return { note: noteNode.value };
142
+ });
143
+
144
+ bridge.registerStateProvider('demo', () => ({
145
+ count,
146
+ note: noteNode.value,
147
+ networkStatus
148
+ }));
149
+
150
+ bridge.start();
151
+ setInterval(render, 500);
152
+ bridge.recordLog('info', 'demo', 'web demo started');
153
+ recordDemoState('started');
154
+ </script>
155
+ </body>
156
+ </html>
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@mobileaidev/ai-app-bridge-web",
3
+ "version": "0.1.0",
4
+ "description": "Browser SDK for AI App Bridge Web sessions.",
5
+ "main": "src/index.js",
6
+ "browser": "src/index.js",
7
+ "files": [
8
+ "src",
9
+ "examples",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "test": "node --test",
14
+ "build": "node -c src/index.js"
15
+ },
16
+ "license": "Apache-2.0",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ }
20
+ }
package/src/index.js ADDED
@@ -0,0 +1,602 @@
1
+ (function initAiAppBridgeWeb(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ return;
5
+ }
6
+ root.AiAppBridgeWeb = factory();
7
+ }(typeof globalThis !== 'undefined' ? globalThis : this, function factory() {
8
+ const defaultControlSelector = 'a,button,input,textarea,select,[role],[onclick],[aria-label]';
9
+ const maxCaptureText = 12000;
10
+
11
+ function createAiAppBridge(options = {}) {
12
+ const state = {
13
+ options,
14
+ socket: null,
15
+ queue: [],
16
+ started: false,
17
+ connected: false,
18
+ sessionId: options.sessionId || storedSessionId(options.storageKey || 'ai_app_bridge_web_session_id'),
19
+ actions: new Map(),
20
+ stateProviders: new Map(),
21
+ restores: [],
22
+ };
23
+
24
+ const api = {
25
+ start,
26
+ disconnect,
27
+ recordLog,
28
+ recordNetwork,
29
+ recordState,
30
+ recordEvent,
31
+ registerAction,
32
+ unregisterAction,
33
+ registerStateProvider,
34
+ snapshotDom: (snapshotOptions) => snapshotDom(snapshotOptions),
35
+ sessionId: () => state.sessionId,
36
+ isConnected: () => state.connected,
37
+ };
38
+
39
+ function start() {
40
+ if (state.started) return api;
41
+ state.started = true;
42
+ installCaptures();
43
+ connect();
44
+ return api;
45
+ }
46
+
47
+ function disconnect() {
48
+ state.started = false;
49
+ state.connected = false;
50
+ for (const restore of state.restores.splice(0)) {
51
+ try {
52
+ restore();
53
+ } catch (_) {
54
+ // Best-effort cleanup only.
55
+ }
56
+ }
57
+ if (state.socket) {
58
+ try {
59
+ state.socket.close();
60
+ } catch (_) {
61
+ // Ignore close failures.
62
+ }
63
+ }
64
+ state.socket = null;
65
+ }
66
+
67
+ function connect() {
68
+ const endpoint = options.endpoint;
69
+ if (!endpoint) throw new Error('AiAppBridge endpoint is required');
70
+ const WebSocketCtor = options.WebSocket || globalValue('WebSocket');
71
+ if (!WebSocketCtor) throw new Error('WebSocket is not available');
72
+ const socket = new WebSocketCtor(withToken(endpoint, options.token));
73
+ state.socket = socket;
74
+ socket.onopen = () => {
75
+ state.connected = true;
76
+ send({
77
+ type: 'hello',
78
+ sessionId: state.sessionId,
79
+ appName: options.appName || documentTitle(),
80
+ url: locationHref(),
81
+ origin: locationOrigin(),
82
+ route: locationPath(),
83
+ targetId: 'main',
84
+ capabilities: {
85
+ logs: true,
86
+ network: true,
87
+ state: true,
88
+ events: true,
89
+ dom: true,
90
+ command: true,
91
+ },
92
+ });
93
+ flushQueue();
94
+ };
95
+ socket.onmessage = (event) => handleServerMessage(event.data);
96
+ socket.onclose = () => {
97
+ state.connected = false;
98
+ if (state.started && options.reconnect !== false) {
99
+ setTimeout(connect, Number(options.reconnectDelayMs || 1000));
100
+ }
101
+ };
102
+ socket.onerror = () => {
103
+ state.connected = false;
104
+ };
105
+ }
106
+
107
+ function send(payload) {
108
+ const text = JSON.stringify(payload);
109
+ if (state.socket && state.socket.readyState === 1) {
110
+ state.socket.send(text);
111
+ return;
112
+ }
113
+ state.queue.push(text);
114
+ while (state.queue.length > 100) state.queue.shift();
115
+ }
116
+
117
+ function flushQueue() {
118
+ while (state.queue.length && state.socket && state.socket.readyState === 1) {
119
+ state.socket.send(state.queue.shift());
120
+ }
121
+ }
122
+
123
+ function capture(stream, item) {
124
+ send({
125
+ type: 'capture',
126
+ stream,
127
+ item: {
128
+ source: 'web-sdk',
129
+ timestampMs: Date.now(),
130
+ url: locationHref(),
131
+ route: locationPath(),
132
+ ...item,
133
+ },
134
+ });
135
+ }
136
+
137
+ function recordLog(level, tag, message, data) {
138
+ capture('logs', {
139
+ level: level || 'info',
140
+ tag: tag || 'web',
141
+ message: trimText(message),
142
+ data,
143
+ source: 'web-sdk',
144
+ });
145
+ }
146
+
147
+ function recordNetwork(record) {
148
+ capture('network', {
149
+ method: record.method || 'GET',
150
+ url: record.url || '',
151
+ statusCode: record.statusCode === undefined ? -1 : record.statusCode,
152
+ durationMs: record.durationMs === undefined ? -1 : record.durationMs,
153
+ requestHeaders: record.requestHeaders,
154
+ responseHeaders: record.responseHeaders,
155
+ requestBody: trimText(record.requestBody),
156
+ responseBody: trimText(record.responseBody),
157
+ error: record.error,
158
+ redacted: true,
159
+ source: record.source || 'web-sdk',
160
+ });
161
+ }
162
+
163
+ function recordState(namespace, key, value) {
164
+ capture('state', {
165
+ namespace: namespace || 'app',
166
+ key: key || 'value',
167
+ value,
168
+ });
169
+ }
170
+
171
+ function recordEvent(category, name, data) {
172
+ capture('events', {
173
+ category: category || 'app',
174
+ name: name || 'event',
175
+ data,
176
+ });
177
+ }
178
+
179
+ function registerAction(name, handler) {
180
+ if (!name || typeof handler !== 'function') {
181
+ throw new Error('registerAction requires a name and handler');
182
+ }
183
+ state.actions.set(String(name), handler);
184
+ return api;
185
+ }
186
+
187
+ function unregisterAction(name) {
188
+ state.actions.delete(String(name));
189
+ return api;
190
+ }
191
+
192
+ function registerStateProvider(name, provider) {
193
+ if (!name || typeof provider !== 'function') {
194
+ throw new Error('registerStateProvider requires a name and provider');
195
+ }
196
+ state.stateProviders.set(String(name), provider);
197
+ return api;
198
+ }
199
+
200
+ async function handleServerMessage(raw) {
201
+ let message;
202
+ try {
203
+ message = JSON.parse(raw);
204
+ } catch (_) {
205
+ return;
206
+ }
207
+ if (message.type !== 'command') return;
208
+ const command = message.command || {};
209
+ try {
210
+ const result = await runCommand(command);
211
+ send({
212
+ type: 'commandResult',
213
+ commandId: message.commandId,
214
+ ok: result && result.ok === false ? false : true,
215
+ result,
216
+ });
217
+ } catch (error) {
218
+ send({
219
+ type: 'commandResult',
220
+ commandId: message.commandId,
221
+ ok: false,
222
+ error: error.message || String(error),
223
+ });
224
+ }
225
+ }
226
+
227
+ async function runCommand(command) {
228
+ const args = command.args || {};
229
+ switch (command.name) {
230
+ case 'domSnapshot': {
231
+ const dom = snapshotDom(args);
232
+ send({ type: 'dom', targetId: command.targetId || 'main', dom });
233
+ return { ok: true, dom };
234
+ }
235
+ case 'click':
236
+ return clickElement(args);
237
+ case 'input':
238
+ return inputElement(args);
239
+ case 'waitFor':
240
+ return waitFor(args);
241
+ case 'scroll':
242
+ return scrollTarget(args);
243
+ case 'state': {
244
+ const values = {};
245
+ for (const [name, provider] of state.stateProviders.entries()) {
246
+ values[name] = await provider();
247
+ }
248
+ return { ok: true, values };
249
+ }
250
+ case 'action': {
251
+ const actionName = args.name || args.action;
252
+ const handler = state.actions.get(String(actionName || ''));
253
+ if (!handler) return { ok: false, error: 'action_not_registered', action: actionName || '' };
254
+ return { ok: true, value: await handler(args.arguments || args) };
255
+ }
256
+ default: {
257
+ const handler = state.actions.get(String(command.name || ''));
258
+ if (!handler) return { ok: false, error: 'unknown_command', command: command.name || '' };
259
+ return { ok: true, value: await handler(args) };
260
+ }
261
+ }
262
+ }
263
+
264
+ function installCaptures() {
265
+ const captureOptions = options.capture || {};
266
+ if (captureOptions.console) installConsoleCapture(state, recordLog);
267
+ if (captureOptions.errors) installErrorCapture(state, recordLog);
268
+ if (captureOptions.fetch) installFetchCapture(state, recordNetwork, options);
269
+ if (captureOptions.xhr) installXhrCapture(state, recordNetwork);
270
+ }
271
+
272
+ return api;
273
+ }
274
+
275
+ function snapshotDom(options = {}) {
276
+ const doc = options.document || globalValue('document');
277
+ if (!doc || !doc.querySelectorAll) {
278
+ return { ok: false, error: 'document_unavailable' };
279
+ }
280
+ const selector = options.selector || defaultControlSelector;
281
+ const maxControls = Number(options.maxControls || 200);
282
+ const controls = [];
283
+ const elements = Array.prototype.slice.call(doc.querySelectorAll(selector), 0, maxControls);
284
+ elements.forEach((element, index) => {
285
+ controls.push({
286
+ index,
287
+ tag: text(element.tagName).toLowerCase(),
288
+ id: text(element.id),
289
+ name: attr(element, 'name'),
290
+ type: attr(element, 'type'),
291
+ role: attr(element, 'role'),
292
+ ariaLabel: attr(element, 'aria-label'),
293
+ placeholder: attr(element, 'placeholder'),
294
+ text: trimText(element.innerText || element.value || element.title || attr(element, 'aria-label'), 300),
295
+ href: trimText(element.href, 500),
296
+ disabled: Boolean(element.disabled),
297
+ bounds: bounds(element),
298
+ });
299
+ });
300
+ return {
301
+ ok: true,
302
+ title: text(doc.title),
303
+ url: locationHref(),
304
+ readyState: text(doc.readyState),
305
+ bodyText: trimText(doc.body && doc.body.innerText, 20000),
306
+ controls,
307
+ controlCount: controls.length,
308
+ updatedAtMs: Date.now(),
309
+ };
310
+ }
311
+
312
+ function clickElement(args = {}) {
313
+ const element = findElement(args);
314
+ if (!element) return { ok: false, error: 'element_not_found' };
315
+ element.click();
316
+ return { ok: true };
317
+ }
318
+
319
+ function inputElement(args = {}) {
320
+ const element = findElement(args);
321
+ if (!element) return { ok: false, error: 'element_not_found' };
322
+ const value = args.value === undefined ? '' : String(args.value);
323
+ element.focus && element.focus();
324
+ element.value = value;
325
+ dispatchInputEvents(element);
326
+ return { ok: true, value };
327
+ }
328
+
329
+ function waitFor(args = {}) {
330
+ const timeoutMs = Number(args.timeoutMs || 5000);
331
+ const startedAtMs = Date.now();
332
+ return new Promise((resolve) => {
333
+ const tick = () => {
334
+ if (args.selector && findElement({ selector: args.selector })) {
335
+ resolve({ ok: true, matched: 'selector' });
336
+ return;
337
+ }
338
+ if (args.targetText && bodyText().includes(String(args.targetText))) {
339
+ resolve({ ok: true, matched: 'text' });
340
+ return;
341
+ }
342
+ if (Date.now() - startedAtMs >= timeoutMs) {
343
+ resolve({ ok: false, error: 'wait_timeout' });
344
+ return;
345
+ }
346
+ setTimeout(tick, Number(args.intervalMs || 250));
347
+ };
348
+ tick();
349
+ });
350
+ }
351
+
352
+ function scrollTarget(args = {}) {
353
+ const element = args.selector ? findElement({ selector: args.selector }) : null;
354
+ const deltaX = Number(args.deltaX || 0);
355
+ const deltaY = Number(args.deltaY === undefined ? args.delta || 400 : args.deltaY);
356
+ if (element && element.scrollBy) {
357
+ element.scrollBy(deltaX, deltaY);
358
+ return { ok: true, target: 'element' };
359
+ }
360
+ const win = globalValue('window');
361
+ if (win && win.scrollBy) {
362
+ win.scrollBy(deltaX, deltaY);
363
+ return { ok: true, target: 'window' };
364
+ }
365
+ return { ok: false, error: 'scroll_unavailable' };
366
+ }
367
+
368
+ function findElement(args = {}) {
369
+ const doc = args.document || globalValue('document');
370
+ if (!doc) return null;
371
+ if (args.selector) return doc.querySelector(args.selector);
372
+ const targetText = args.targetText === undefined ? '' : String(args.targetText);
373
+ if (!targetText) return null;
374
+ const elements = Array.prototype.slice.call(doc.querySelectorAll(defaultControlSelector), 0, 500);
375
+ return elements.find((element) => elementMatchesText(element, targetText, Boolean(args.exact))) || null;
376
+ }
377
+
378
+ function elementMatchesText(element, targetText, exact) {
379
+ const values = [
380
+ element.innerText,
381
+ element.value,
382
+ element.title,
383
+ element.id,
384
+ attr(element, 'name'),
385
+ attr(element, 'role'),
386
+ attr(element, 'aria-label'),
387
+ attr(element, 'placeholder'),
388
+ ].map((value) => text(value)).filter(Boolean);
389
+ return values.some((value) => exact ? value === targetText : value.includes(targetText));
390
+ }
391
+
392
+ function installConsoleCapture(state, recordLog) {
393
+ const consoleObject = globalValue('console');
394
+ if (!consoleObject) return;
395
+ ['log', 'info', 'warn', 'error', 'debug'].forEach((method) => {
396
+ const original = consoleObject[method];
397
+ if (typeof original !== 'function') return;
398
+ consoleObject[method] = function capturedConsole(...args) {
399
+ original.apply(consoleObject, args);
400
+ if (!state.started) return;
401
+ recordLog(method === 'log' ? 'info' : method, 'console', args.map(stringifyValue).join(' '), { method });
402
+ };
403
+ state.restores.push(() => {
404
+ consoleObject[method] = original;
405
+ });
406
+ });
407
+ }
408
+
409
+ function installErrorCapture(state, recordLog) {
410
+ const win = globalValue('window');
411
+ if (!win || !win.addEventListener) return;
412
+ const onError = (event) => {
413
+ recordLog('error', 'window.onerror', event.message || 'error', {
414
+ filename: event.filename,
415
+ lineno: event.lineno,
416
+ colno: event.colno,
417
+ });
418
+ };
419
+ const onRejection = (event) => {
420
+ recordLog('error', 'unhandledrejection', stringifyValue(event.reason));
421
+ };
422
+ win.addEventListener('error', onError);
423
+ win.addEventListener('unhandledrejection', onRejection);
424
+ state.restores.push(() => {
425
+ win.removeEventListener('error', onError);
426
+ win.removeEventListener('unhandledrejection', onRejection);
427
+ });
428
+ }
429
+
430
+ function installFetchCapture(state, recordNetwork, options) {
431
+ const win = globalValue('window');
432
+ if (!win || typeof win.fetch !== 'function') return;
433
+ const original = win.fetch;
434
+ win.fetch = async function capturedFetch(input, init = {}) {
435
+ const startedAtMs = Date.now();
436
+ const method = (init && init.method) || (input && input.method) || 'GET';
437
+ const url = typeof input === 'string' ? input : (input && input.url) || '';
438
+ try {
439
+ const response = await original.apply(this, arguments);
440
+ let responseBody;
441
+ if (options.captureResponseBodies && response.clone) {
442
+ try {
443
+ responseBody = await response.clone().text();
444
+ } catch (_) {
445
+ responseBody = undefined;
446
+ }
447
+ }
448
+ recordNetwork({
449
+ source: 'fetch-auto',
450
+ method,
451
+ url,
452
+ statusCode: response.status,
453
+ durationMs: Date.now() - startedAtMs,
454
+ responseBody,
455
+ });
456
+ return response;
457
+ } catch (error) {
458
+ recordNetwork({
459
+ source: 'fetch-auto',
460
+ method,
461
+ url,
462
+ statusCode: -1,
463
+ durationMs: Date.now() - startedAtMs,
464
+ error: error.message || String(error),
465
+ });
466
+ throw error;
467
+ }
468
+ };
469
+ state.restores.push(() => {
470
+ win.fetch = original;
471
+ });
472
+ }
473
+
474
+ function installXhrCapture(state, recordNetwork) {
475
+ const win = globalValue('window');
476
+ if (!win || !win.XMLHttpRequest) return;
477
+ const Xhr = win.XMLHttpRequest;
478
+ const originalOpen = Xhr.prototype.open;
479
+ const originalSend = Xhr.prototype.send;
480
+ Xhr.prototype.open = function capturedOpen(method, url) {
481
+ this.__aiAppBridge = { method, url };
482
+ return originalOpen.apply(this, arguments);
483
+ };
484
+ Xhr.prototype.send = function capturedSend(body) {
485
+ const meta = this.__aiAppBridge || {};
486
+ const startedAtMs = Date.now();
487
+ this.addEventListener('loadend', () => {
488
+ recordNetwork({
489
+ source: 'xhr-auto',
490
+ method: meta.method || 'GET',
491
+ url: meta.url || '',
492
+ statusCode: this.status,
493
+ durationMs: Date.now() - startedAtMs,
494
+ requestBody: trimText(body),
495
+ responseBody: trimText(this.responseText),
496
+ });
497
+ });
498
+ return originalSend.apply(this, arguments);
499
+ };
500
+ state.restores.push(() => {
501
+ Xhr.prototype.open = originalOpen;
502
+ Xhr.prototype.send = originalSend;
503
+ });
504
+ }
505
+
506
+ function withToken(endpoint, token) {
507
+ if (!token) return endpoint;
508
+ const base = locationHref() || 'http://127.0.0.1';
509
+ const url = new URL(endpoint, base);
510
+ url.searchParams.set('token', token);
511
+ return url.toString();
512
+ }
513
+
514
+ function storedSessionId(key) {
515
+ const storage = globalValue('sessionStorage');
516
+ const existing = storage && storage.getItem(key);
517
+ if (existing) return existing;
518
+ const next = `web-${Math.random().toString(36).slice(2)}-${Date.now()}`;
519
+ if (storage) storage.setItem(key, next);
520
+ return next;
521
+ }
522
+
523
+ function dispatchInputEvents(element) {
524
+ const win = globalValue('window');
525
+ if (!win || typeof win.Event !== 'function') return;
526
+ element.dispatchEvent(new win.Event('input', { bubbles: true }));
527
+ element.dispatchEvent(new win.Event('change', { bubbles: true }));
528
+ }
529
+
530
+ function bounds(element) {
531
+ if (!element || !element.getBoundingClientRect) {
532
+ return { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 };
533
+ }
534
+ const rect = element.getBoundingClientRect();
535
+ return {
536
+ left: rect.left,
537
+ top: rect.top,
538
+ right: rect.right,
539
+ bottom: rect.bottom,
540
+ width: rect.width,
541
+ height: rect.height,
542
+ };
543
+ }
544
+
545
+ function attr(element, name) {
546
+ return element && element.getAttribute ? text(element.getAttribute(name)) : '';
547
+ }
548
+
549
+ function bodyText() {
550
+ const doc = globalValue('document');
551
+ return text(doc && doc.body && doc.body.innerText);
552
+ }
553
+
554
+ function documentTitle() {
555
+ const doc = globalValue('document');
556
+ return text(doc && doc.title) || 'web-app';
557
+ }
558
+
559
+ function locationHref() {
560
+ const location = globalValue('location');
561
+ return text(location && location.href);
562
+ }
563
+
564
+ function locationOrigin() {
565
+ const location = globalValue('location');
566
+ return text(location && location.origin);
567
+ }
568
+
569
+ function locationPath() {
570
+ const location = globalValue('location');
571
+ if (!location) return '';
572
+ return `${location.pathname || ''}${location.search || ''}${location.hash || ''}`;
573
+ }
574
+
575
+ function trimText(value, max = maxCaptureText) {
576
+ const raw = text(value);
577
+ return raw.length > max ? raw.slice(0, max) : raw;
578
+ }
579
+
580
+ function text(value) {
581
+ return value === undefined || value === null ? '' : String(value);
582
+ }
583
+
584
+ function stringifyValue(value) {
585
+ if (value instanceof Error) return value.stack || value.message;
586
+ if (typeof value === 'string') return value;
587
+ try {
588
+ return JSON.stringify(value);
589
+ } catch (_) {
590
+ return String(value);
591
+ }
592
+ }
593
+
594
+ function globalValue(name) {
595
+ return typeof globalThis !== 'undefined' ? globalThis[name] : undefined;
596
+ }
597
+
598
+ return {
599
+ createAiAppBridge,
600
+ snapshotDom,
601
+ };
602
+ }));