@mobileaidev/ai-app-bridge 0.2.8 → 0.2.10

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.
@@ -0,0 +1,618 @@
1
+ const crypto = require('crypto');
2
+ const http = require('http');
3
+ const { URL } = require('url');
4
+ const { WebSocketServer } = require('ws');
5
+
6
+ const defaultHost = '127.0.0.1';
7
+ const defaultPort = 18180;
8
+ const defaultPath = '/ai-app-bridge-web';
9
+ const maxCaptureEntries = 500;
10
+ const defaultCommandTimeoutMs = 5000;
11
+
12
+ class WebBridgeProvider {
13
+ constructor() {
14
+ this.server = null;
15
+ this.wss = null;
16
+ this.host = defaultHost;
17
+ this.port = defaultPort;
18
+ this.path = defaultPath;
19
+ this.token = '';
20
+ this.sessions = new Map();
21
+ this.pendingCommands = new Map();
22
+ this.captureSequence = 0;
23
+ }
24
+
25
+ async run(command, args = {}) {
26
+ switch (command) {
27
+ case 'web-provider-status':
28
+ return this.providerStatus();
29
+ case 'web-session-start':
30
+ return this.start(args);
31
+ case 'web-connect-info':
32
+ return this.connectInfo();
33
+ case 'web-sessions':
34
+ return this.listSessions();
35
+ case 'web-status':
36
+ return this.sessionStatus(args);
37
+ case 'web-dom':
38
+ return this.dom(args);
39
+ case 'web-logs':
40
+ return this.captureResponse(args, 'logs');
41
+ case 'web-network':
42
+ return this.captureResponse(args, 'network');
43
+ case 'web-state':
44
+ return this.stateResponse(args);
45
+ case 'web-events':
46
+ return this.captureResponse(args, 'events');
47
+ case 'web-command':
48
+ return this.command(args);
49
+ case 'web-click':
50
+ return this.command({ ...args, name: 'click' });
51
+ case 'web-input':
52
+ return this.command({ ...args, name: 'input' });
53
+ case 'web-wait':
54
+ return this.command({ ...args, name: 'waitFor' });
55
+ case 'web-scroll':
56
+ return this.command({ ...args, name: 'scroll' });
57
+ default:
58
+ return { ok: false, error: 'unknown_web_command', command };
59
+ }
60
+ }
61
+
62
+ async start(args = {}) {
63
+ if (this.server) {
64
+ return { ok: true, alreadyRunning: true, ...this.connectInfo() };
65
+ }
66
+
67
+ this.host = stringArg(args.host, defaultHost);
68
+ this.port = numberArg(args.webPort, numberArg(args.port, defaultPort));
69
+ this.path = stringArg(args.path, defaultPath);
70
+ this.token = stringArg(args.token, randomToken());
71
+
72
+ this.server = http.createServer((request, response) => this.handleHttp(request, response));
73
+ this.wss = new WebSocketServer({ server: this.server });
74
+ this.wss.on('connection', (socket, request) => this.handleConnection(socket, request));
75
+
76
+ await new Promise((resolve, reject) => {
77
+ const onError = (error) => {
78
+ this.server = null;
79
+ this.wss = null;
80
+ reject(error);
81
+ };
82
+ this.server.once('error', onError);
83
+ this.server.listen(this.port, this.host, () => {
84
+ this.server.off('error', onError);
85
+ const address = this.server.address();
86
+ this.port = typeof address === 'object' && address ? address.port : this.port;
87
+ resolve();
88
+ });
89
+ });
90
+
91
+ return { ok: true, started: true, ...this.connectInfo() };
92
+ }
93
+
94
+ providerStatus() {
95
+ return {
96
+ ok: true,
97
+ running: Boolean(this.server),
98
+ host: this.host,
99
+ port: this.server ? this.port : null,
100
+ path: this.path,
101
+ sessionCount: this.sessions.size,
102
+ updatedAtMs: Date.now(),
103
+ };
104
+ }
105
+
106
+ connectInfo() {
107
+ if (!this.server) {
108
+ return {
109
+ ok: false,
110
+ running: false,
111
+ error: 'web_provider_not_running',
112
+ suggestion: 'Run web-session-start first.',
113
+ };
114
+ }
115
+ const endpoint = `ws://${this.host}:${this.port}${this.path}`;
116
+ return {
117
+ ok: true,
118
+ running: true,
119
+ endpoint,
120
+ httpEndpoint: `http://${this.host}:${this.port}`,
121
+ token: this.token,
122
+ sessionCount: this.sessions.size,
123
+ updatedAtMs: Date.now(),
124
+ };
125
+ }
126
+
127
+ listSessions() {
128
+ return {
129
+ ok: true,
130
+ running: Boolean(this.server),
131
+ sessions: [...this.sessions.values()].map((session) => this.sessionSummary(session)),
132
+ count: this.sessions.size,
133
+ updatedAtMs: Date.now(),
134
+ };
135
+ }
136
+
137
+ sessionStatus(args = {}) {
138
+ const session = this.requireSession(args);
139
+ if (!session.ok) return session;
140
+ return {
141
+ ok: true,
142
+ session: this.sessionSummary(session.value),
143
+ capture: captureCounts(session.value),
144
+ updatedAtMs: Date.now(),
145
+ };
146
+ }
147
+
148
+ async dom(args = {}) {
149
+ const sessionResult = this.requireSession(args);
150
+ if (!sessionResult.ok) return sessionResult;
151
+ const session = sessionResult.value;
152
+ const targetId = stringArg(args.targetId, 'main');
153
+ const refresh = args.refresh !== false;
154
+ if (refresh && session.socket?.readyState === 1) {
155
+ try {
156
+ const result = await this.sendCommand(session, {
157
+ name: 'domSnapshot',
158
+ targetId,
159
+ args: {
160
+ selector: args.selector,
161
+ maxControls: args.maxControls,
162
+ },
163
+ }, numberArg(args.timeoutMs, defaultCommandTimeoutMs));
164
+ const dom = result.dom || result.value || result;
165
+ if (dom && typeof dom === 'object') {
166
+ session.domByTarget.set(targetId, { ...dom, targetId, updatedAtMs: Date.now() });
167
+ }
168
+ } catch (error) {
169
+ if (!session.domByTarget.has(targetId)) {
170
+ return { ok: false, error: 'web_dom_unavailable', message: error.message };
171
+ }
172
+ }
173
+ }
174
+ const dom = session.domByTarget.get(targetId);
175
+ if (!dom) {
176
+ return { ok: false, error: 'web_dom_absent', sessionId: session.sessionId, targetId };
177
+ }
178
+ return {
179
+ ok: true,
180
+ sessionId: session.sessionId,
181
+ targetId,
182
+ dom,
183
+ updatedAtMs: Date.now(),
184
+ };
185
+ }
186
+
187
+ captureResponse(args = {}, stream) {
188
+ const sessionResult = this.requireSession(args);
189
+ if (!sessionResult.ok) return sessionResult;
190
+ const session = sessionResult.value;
191
+ const filter = captureFilter(args);
192
+ const source = session.captures[stream] || [];
193
+ const filtered = source.filter((item) => captureMatches(item, filter));
194
+ const items = filtered.length > filter.limit ? filtered.slice(-filter.limit) : filtered;
195
+ return {
196
+ ok: true,
197
+ type: stream,
198
+ sessionId: session.sessionId,
199
+ items: items.map((item) => ({ ...item })),
200
+ count: items.length,
201
+ sinceId: filter.sinceId ?? null,
202
+ sinceMs: filter.sinceMs ?? null,
203
+ limit: filter.limit,
204
+ updatedAtMs: Date.now(),
205
+ };
206
+ }
207
+
208
+ stateResponse(args = {}) {
209
+ const sessionResult = this.requireSession(args);
210
+ if (!sessionResult.ok) return sessionResult;
211
+ const session = sessionResult.value;
212
+ const filter = captureFilter(args);
213
+ const values = {};
214
+ const items = [];
215
+ for (const [key, item] of session.stateEntries.entries()) {
216
+ if (!captureMatches(item, filter)) continue;
217
+ values[key] = item.value;
218
+ items.push({ ...item });
219
+ }
220
+ const limited = items.length > filter.limit ? items.slice(-filter.limit) : items;
221
+ return {
222
+ ok: true,
223
+ type: 'state',
224
+ sessionId: session.sessionId,
225
+ values,
226
+ items: limited,
227
+ count: limited.length,
228
+ sinceId: filter.sinceId ?? null,
229
+ sinceMs: filter.sinceMs ?? null,
230
+ limit: filter.limit,
231
+ updatedAtMs: Date.now(),
232
+ };
233
+ }
234
+
235
+ async command(args = {}) {
236
+ const sessionResult = this.requireSession(args);
237
+ if (!sessionResult.ok) return sessionResult;
238
+ const session = sessionResult.value;
239
+ const name = stringArg(args.name || args.action || args.webCommand, '');
240
+ if (!name) {
241
+ return { ok: false, error: 'web_command_name_required' };
242
+ }
243
+ try {
244
+ const result = await this.sendCommand(session, {
245
+ name,
246
+ targetId: stringArg(args.targetId, 'main'),
247
+ args: args.arguments && typeof args.arguments === 'object' ? args.arguments : commandArgs(args),
248
+ }, numberArg(args.timeoutMs, defaultCommandTimeoutMs));
249
+ return {
250
+ ok: result.ok !== false,
251
+ sessionId: session.sessionId,
252
+ targetId: stringArg(args.targetId, 'main'),
253
+ command: name,
254
+ result,
255
+ updatedAtMs: Date.now(),
256
+ };
257
+ } catch (error) {
258
+ return {
259
+ ok: false,
260
+ error: 'web_command_failed',
261
+ message: error.message,
262
+ sessionId: session.sessionId,
263
+ command: name,
264
+ };
265
+ }
266
+ }
267
+
268
+ sendCommand(session, command, timeoutMs) {
269
+ if (!session.socket || session.socket.readyState !== 1) {
270
+ return Promise.reject(new Error('web_session_not_connected'));
271
+ }
272
+ const commandId = `web-command-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
273
+ const payload = {
274
+ type: 'command',
275
+ commandId,
276
+ command,
277
+ sentAtMs: Date.now(),
278
+ };
279
+ return new Promise((resolve, reject) => {
280
+ const timer = setTimeout(() => {
281
+ this.pendingCommands.delete(commandId);
282
+ reject(new Error('web_command_timeout'));
283
+ }, timeoutMs);
284
+ this.pendingCommands.set(commandId, {
285
+ sessionId: session.sessionId,
286
+ resolve: (value) => {
287
+ clearTimeout(timer);
288
+ resolve(value);
289
+ },
290
+ reject: (error) => {
291
+ clearTimeout(timer);
292
+ reject(error);
293
+ },
294
+ });
295
+ session.socket.send(JSON.stringify(payload), (error) => {
296
+ if (!error) return;
297
+ const pending = this.pendingCommands.get(commandId);
298
+ this.pendingCommands.delete(commandId);
299
+ if (pending) {
300
+ clearTimeout(timer);
301
+ reject(error);
302
+ }
303
+ });
304
+ });
305
+ }
306
+
307
+ handleHttp(request, response) {
308
+ setCors(response);
309
+ if (request.method === 'OPTIONS') {
310
+ response.writeHead(204);
311
+ response.end();
312
+ return;
313
+ }
314
+ if (request.method === 'GET' && request.url?.startsWith('/health')) {
315
+ writeJson(response, this.providerStatus());
316
+ return;
317
+ }
318
+ if (request.method === 'GET' && request.url?.startsWith('/sessions')) {
319
+ writeJson(response, this.listSessions());
320
+ return;
321
+ }
322
+ writeJson(response, { ok: false, error: 'not_found' }, 404);
323
+ }
324
+
325
+ handleConnection(socket, request) {
326
+ const parsed = new URL(request.url || '/', `http://${this.host}:${this.port}`);
327
+ if (parsed.pathname !== this.path) {
328
+ socket.close(1008, 'invalid_path');
329
+ return;
330
+ }
331
+ if (this.token && parsed.searchParams.get('token') !== this.token) {
332
+ socket.close(1008, 'invalid_token');
333
+ return;
334
+ }
335
+
336
+ socket.on('message', (raw) => this.handleSocketMessage(socket, raw));
337
+ socket.on('close', () => this.markSocketClosed(socket));
338
+ socket.on('error', () => this.markSocketClosed(socket));
339
+ }
340
+
341
+ handleSocketMessage(socket, raw) {
342
+ let message;
343
+ try {
344
+ message = JSON.parse(raw.toString());
345
+ } catch (_) {
346
+ socket.send(JSON.stringify({ type: 'error', error: 'invalid_json' }));
347
+ return;
348
+ }
349
+ switch (message.type) {
350
+ case 'hello':
351
+ this.registerSession(socket, message);
352
+ break;
353
+ case 'capture':
354
+ this.recordCapture(socket, message);
355
+ break;
356
+ case 'dom':
357
+ this.recordDom(socket, message);
358
+ break;
359
+ case 'commandResult':
360
+ this.resolveCommand(socket, message);
361
+ break;
362
+ case 'heartbeat':
363
+ this.touchSession(socket);
364
+ break;
365
+ default:
366
+ socket.send(JSON.stringify({ type: 'error', error: 'unknown_message_type', messageType: message.type || '' }));
367
+ break;
368
+ }
369
+ }
370
+
371
+ registerSession(socket, message) {
372
+ const sessionId = stringArg(message.sessionId, `web-session-${randomToken(6)}`);
373
+ const targetId = stringArg(message.targetId, 'main');
374
+ const existing = this.sessions.get(sessionId);
375
+ const session = existing || createSession(sessionId);
376
+ session.socket = socket;
377
+ session.connected = true;
378
+ session.appName = stringArg(message.appName, session.appName || '');
379
+ session.url = stringArg(message.url, session.url || '');
380
+ session.origin = stringArg(message.origin, session.origin || '');
381
+ session.route = stringArg(message.route, session.route || '');
382
+ session.capabilities = message.capabilities && typeof message.capabilities === 'object' ? message.capabilities : session.capabilities;
383
+ session.targets.set(targetId, {
384
+ targetId,
385
+ url: session.url,
386
+ origin: session.origin,
387
+ updatedAtMs: Date.now(),
388
+ });
389
+ session.updatedAtMs = Date.now();
390
+ socket.aiAppBridgeSessionId = sessionId;
391
+ this.sessions.set(sessionId, session);
392
+ socket.send(JSON.stringify({
393
+ type: 'helloAck',
394
+ ok: true,
395
+ sessionId,
396
+ targetId,
397
+ serverTimeMs: Date.now(),
398
+ }));
399
+ }
400
+
401
+ recordCapture(socket, message) {
402
+ const session = this.sessionForSocket(socket);
403
+ if (!session) return;
404
+ const stream = normalizeStream(message.stream || message.typeName);
405
+ if (!stream) return;
406
+ const rawItem = message.item || message.record || {};
407
+ const item = this.baseRecord(stream, session, rawItem);
408
+ if (stream === 'state') {
409
+ const namespace = stringArg(item.namespace, 'app');
410
+ const key = stringArg(item.key, 'value');
411
+ item.namespace = namespace;
412
+ item.key = key;
413
+ session.stateEntries.set(`${namespace}.${key}`, item);
414
+ } else {
415
+ boundedPush(session.captures[stream], item, maxCaptureEntries);
416
+ }
417
+ session.updatedAtMs = Date.now();
418
+ }
419
+
420
+ recordDom(socket, message) {
421
+ const session = this.sessionForSocket(socket);
422
+ if (!session) return;
423
+ const targetId = stringArg(message.targetId, 'main');
424
+ const dom = message.dom && typeof message.dom === 'object' ? message.dom : message;
425
+ session.domByTarget.set(targetId, {
426
+ ...dom,
427
+ targetId,
428
+ sessionId: session.sessionId,
429
+ updatedAtMs: Date.now(),
430
+ });
431
+ session.updatedAtMs = Date.now();
432
+ }
433
+
434
+ resolveCommand(socket, message) {
435
+ const pending = this.pendingCommands.get(message.commandId);
436
+ if (!pending) return;
437
+ const session = this.sessionForSocket(socket);
438
+ if (!session || session.sessionId !== pending.sessionId) {
439
+ pending.reject(new Error('web_command_session_mismatch'));
440
+ this.pendingCommands.delete(message.commandId);
441
+ return;
442
+ }
443
+ this.pendingCommands.delete(message.commandId);
444
+ if (message.ok === false) {
445
+ pending.resolve({
446
+ ok: false,
447
+ error: message.error || 'web_command_error',
448
+ value: message.value,
449
+ });
450
+ return;
451
+ }
452
+ pending.resolve(message.result !== undefined ? message.result : { ok: true, value: message.value });
453
+ }
454
+
455
+ baseRecord(stream, session, rawItem) {
456
+ const type = stream === 'logs' ? 'log' : stream === 'events' ? 'event' : stream;
457
+ return {
458
+ id: ++this.captureSequence,
459
+ type,
460
+ source: stringArg(rawItem.source, 'web-sdk'),
461
+ timestampMs: numberArg(rawItem.timestampMs, Date.now()),
462
+ sessionId: session.sessionId,
463
+ targetId: stringArg(rawItem.targetId, 'main'),
464
+ ...rawItem,
465
+ };
466
+ }
467
+
468
+ requireSession(args = {}) {
469
+ if (!this.server) {
470
+ return { ok: false, error: 'web_provider_not_running', suggestion: 'Run web-session-start first.' };
471
+ }
472
+ const sessionId = stringArg(args.sessionId, '');
473
+ if (sessionId) {
474
+ const session = this.sessions.get(sessionId);
475
+ if (!session) return { ok: false, error: 'web_session_not_found', sessionId };
476
+ return { ok: true, value: session };
477
+ }
478
+ const live = [...this.sessions.values()].filter((session) => session.connected);
479
+ if (live.length === 1) return { ok: true, value: live[0] };
480
+ return {
481
+ ok: false,
482
+ error: live.length === 0 ? 'web_session_required' : 'web_session_ambiguous',
483
+ sessions: live.map((session) => this.sessionSummary(session)),
484
+ };
485
+ }
486
+
487
+ sessionForSocket(socket) {
488
+ const sessionId = socket.aiAppBridgeSessionId;
489
+ return sessionId ? this.sessions.get(sessionId) : null;
490
+ }
491
+
492
+ touchSession(socket) {
493
+ const session = this.sessionForSocket(socket);
494
+ if (session) session.updatedAtMs = Date.now();
495
+ }
496
+
497
+ markSocketClosed(socket) {
498
+ const session = this.sessionForSocket(socket);
499
+ if (!session || session.socket !== socket) return;
500
+ session.connected = false;
501
+ session.socket = null;
502
+ session.updatedAtMs = Date.now();
503
+ }
504
+
505
+ sessionSummary(session) {
506
+ return {
507
+ sessionId: session.sessionId,
508
+ appName: session.appName,
509
+ connected: session.connected,
510
+ url: session.url,
511
+ origin: session.origin,
512
+ route: session.route,
513
+ capabilities: session.capabilities,
514
+ targets: [...session.targets.values()],
515
+ capture: captureCounts(session),
516
+ connectedAtMs: session.connectedAtMs,
517
+ updatedAtMs: session.updatedAtMs,
518
+ };
519
+ }
520
+ }
521
+
522
+ function createSession(sessionId) {
523
+ return {
524
+ sessionId,
525
+ appName: '',
526
+ connected: false,
527
+ socket: null,
528
+ url: '',
529
+ origin: '',
530
+ route: '',
531
+ capabilities: {},
532
+ targets: new Map(),
533
+ captures: {
534
+ logs: [],
535
+ network: [],
536
+ events: [],
537
+ },
538
+ stateEntries: new Map(),
539
+ domByTarget: new Map(),
540
+ connectedAtMs: Date.now(),
541
+ updatedAtMs: Date.now(),
542
+ };
543
+ }
544
+
545
+ function commandArgs(args) {
546
+ const omitted = new Set(['sessionId', 'targetId', 'name', 'action', 'webCommand', 'timeoutMs']);
547
+ const result = {};
548
+ for (const [key, value] of Object.entries(args)) {
549
+ if (!omitted.has(key)) result[key] = value;
550
+ }
551
+ return result;
552
+ }
553
+
554
+ function captureCounts(session) {
555
+ return {
556
+ logs: session.captures.logs.length,
557
+ network: session.captures.network.length,
558
+ state: session.stateEntries.size,
559
+ events: session.captures.events.length,
560
+ dom: session.domByTarget.size,
561
+ };
562
+ }
563
+
564
+ function captureFilter(args = {}) {
565
+ return {
566
+ sinceId: args.sinceId === undefined ? null : Number(args.sinceId),
567
+ sinceMs: args.sinceMs === undefined ? null : Number(args.sinceMs),
568
+ limit: Number.isInteger(Number(args.limit)) ? Math.max(1, Math.min(Number(args.limit), maxCaptureEntries)) : 200,
569
+ };
570
+ }
571
+
572
+ function captureMatches(item, filter) {
573
+ if (filter.sinceId !== null && Number(item.id) <= filter.sinceId) return false;
574
+ if (filter.sinceMs !== null && Number(item.timestampMs) < filter.sinceMs) return false;
575
+ return true;
576
+ }
577
+
578
+ function normalizeStream(value) {
579
+ const stream = String(value || '').trim();
580
+ if (stream === 'log') return 'logs';
581
+ if (stream === 'event') return 'events';
582
+ if (stream === 'logs' || stream === 'network' || stream === 'state' || stream === 'events') return stream;
583
+ return '';
584
+ }
585
+
586
+ function boundedPush(target, item, maxSize) {
587
+ while (target.length >= maxSize) target.shift();
588
+ target.push(item);
589
+ }
590
+
591
+ function setCors(response) {
592
+ response.setHeader('Access-Control-Allow-Origin', '*');
593
+ response.setHeader('Access-Control-Allow-Headers', 'content-type,x-ai-app-bridge-token');
594
+ response.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
595
+ }
596
+
597
+ function writeJson(response, payload, statusCode = 200) {
598
+ response.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
599
+ response.end(JSON.stringify(payload));
600
+ }
601
+
602
+ function randomToken(bytes = 18) {
603
+ return crypto.randomBytes(bytes).toString('base64url');
604
+ }
605
+
606
+ function stringArg(value, fallback) {
607
+ const text = value === undefined || value === null ? '' : String(value);
608
+ return text.trim() ? text : fallback;
609
+ }
610
+
611
+ function numberArg(value, fallback) {
612
+ const number = Number(value);
613
+ return Number.isFinite(number) ? number : fallback;
614
+ }
615
+
616
+ module.exports = {
617
+ WebBridgeProvider,
618
+ };
package/package.json CHANGED
@@ -1,35 +1,36 @@
1
- {
2
- "name": "@mobileaidev/ai-app-bridge",
3
- "version": "0.2.8",
4
- "description": "Desktop CLI and MCP server for AI App Bridge.",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/mobileAiDev/ai-app-bridge.git",
8
- "directory": "desktop/ai-app-bridge-cli"
9
- },
10
- "homepage": "https://github.com/mobileAiDev/ai-app-bridge#readme",
11
- "bugs": {
12
- "url": "https://github.com/mobileAiDev/ai-app-bridge/issues"
13
- },
14
- "bin": {
15
- "ai-app-bridge": "bin/ai-app-bridge.js",
16
- "ai-app-bridge-mcp": "bin/mcp-server.js"
17
- },
1
+ {
2
+ "name": "@mobileaidev/ai-app-bridge",
3
+ "version": "0.2.10",
4
+ "description": "Desktop CLI and MCP server for AI App Bridge.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/mobileAiDev/ai-app-bridge.git",
8
+ "directory": "desktop/ai-app-bridge-cli"
9
+ },
10
+ "homepage": "https://github.com/mobileAiDev/ai-app-bridge#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/mobileAiDev/ai-app-bridge/issues"
13
+ },
14
+ "bin": {
15
+ "ai-app-bridge": "bin/ai-app-bridge.js",
16
+ "ai-app-bridge-mcp": "bin/mcp-server.js"
17
+ },
18
18
  "files": [
19
19
  "bin/ai-app-bridge.js",
20
20
  "bin/mcp-server.js",
21
+ "bin/web-provider.js",
21
22
  "skills/ai-app-bridge-use",
22
23
  "README.md"
23
24
  ],
24
- "scripts": {
25
- "check": "node -c bin/ai-app-bridge.js && node -c bin/mcp-server.js && node --test",
26
- "test": "node --test"
27
- },
28
- "license": "Apache-2.0",
29
- "dependencies": {
30
- "ws": "^8.18.0"
31
- },
32
- "publishConfig": {
33
- "access": "public"
34
- }
35
- }
25
+ "scripts": {
26
+ "check": "node -c bin/ai-app-bridge.js && node -c bin/mcp-server.js && node --test",
27
+ "test": "node --test"
28
+ },
29
+ "license": "Apache-2.0",
30
+ "dependencies": {
31
+ "ws": "^8.18.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }