@vibebrowser/cli 0.2.8
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 +13 -0
- package/dist/browser-cli.js +1881 -0
- package/dist/browser-main.js +11 -0
- package/dist/connection.js +536 -0
- package/dist/devtools-fallback.js +172 -0
- package/dist/relay-daemon.js +13 -0
- package/dist/relay.js +813 -0
- package/dist/types.js +22 -0
- package/dist/version.js +15 -0
- package/package.json +56 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { registerStandaloneBrowserCli } from './browser-cli.js';
|
|
4
|
+
import { getPackageVersion } from './version.js';
|
|
5
|
+
program
|
|
6
|
+
.name('vibebrowser-cli')
|
|
7
|
+
.description('OpenClaw-compatible browser CLI (relay mode by default, or chrome-devtools with --devtools)')
|
|
8
|
+
.version(getPackageVersion());
|
|
9
|
+
registerStandaloneBrowserCli(program);
|
|
10
|
+
program.parse();
|
|
11
|
+
//# sourceMappingURL=browser-main.js.map
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibe MCP Server - Relay Connection
|
|
3
|
+
*
|
|
4
|
+
* Connects to the relay server as a WebSocket client.
|
|
5
|
+
* Supports two modes:
|
|
6
|
+
* - Local: connects to local relay daemon at ws://127.0.0.1:19888
|
|
7
|
+
* - Remote: connects to public relay at wss://relay.api.vibebrowser.app/<uuid>
|
|
8
|
+
*/
|
|
9
|
+
import WebSocket from 'ws';
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { dirname, join } from 'path';
|
|
13
|
+
import { isRelayRunning, AGENT_PORT } from './relay.js';
|
|
14
|
+
const NO_CONNECTION_MESSAGE = `No connection to Vibe extension. Please:
|
|
15
|
+
1. Install the Vibe AI Browser extension from https://vibebrowser.app
|
|
16
|
+
2. Click the Vibe extension icon in Chrome
|
|
17
|
+
3. Enable "MCP External Control" in Settings`;
|
|
18
|
+
const NO_CONNECTION_REMOTE_MESSAGE = `No connection to Vibe extension via remote relay. Please:
|
|
19
|
+
1. Install the Vibe AI Browser extension from https://vibebrowser.app
|
|
20
|
+
2. Open extension Settings > MCP External
|
|
21
|
+
3. Select "Remote" mode and note your Extension UUID
|
|
22
|
+
4. Make sure the extension is connected to the relay`;
|
|
23
|
+
const RELAY_CONNECT_TIMEOUT = 10000;
|
|
24
|
+
const RELAY_RECONNECT_DELAY = 2000;
|
|
25
|
+
const DEFAULT_RELAY_URL = 'wss://relay.api.vibebrowser.app';
|
|
26
|
+
function isRemoteRelayUrl(value) {
|
|
27
|
+
return /^wss?:\/\//i.test(value);
|
|
28
|
+
}
|
|
29
|
+
export function parseRemoteRelayUrl(value) {
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = new URL(value);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
36
|
+
throw new Error(`Invalid remote relay URL: ${message}`);
|
|
37
|
+
}
|
|
38
|
+
if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
|
|
39
|
+
throw new Error('Invalid remote relay URL: protocol must be ws:// or wss://');
|
|
40
|
+
}
|
|
41
|
+
const pathSegments = parsed.pathname.split('/').filter(Boolean);
|
|
42
|
+
const uuid = pathSegments[pathSegments.length - 1];
|
|
43
|
+
if (!uuid) {
|
|
44
|
+
throw new Error('Invalid remote relay URL: missing UUID path segment');
|
|
45
|
+
}
|
|
46
|
+
pathSegments.pop();
|
|
47
|
+
parsed.pathname = pathSegments.length > 0 ? `/${pathSegments.join('/')}` : '';
|
|
48
|
+
parsed.search = '';
|
|
49
|
+
parsed.hash = '';
|
|
50
|
+
return {
|
|
51
|
+
relayUrl: parsed.toString().replace(/\/$/, ''),
|
|
52
|
+
uuid,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function normalizeRemoteConfig(remote) {
|
|
56
|
+
if (!remote) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const relayUrl = remote.relayUrl?.replace(/\/$/, '');
|
|
60
|
+
if (!isRemoteRelayUrl(remote.uuid)) {
|
|
61
|
+
return { uuid: remote.uuid, relayUrl };
|
|
62
|
+
}
|
|
63
|
+
const parsed = parseRemoteRelayUrl(remote.uuid);
|
|
64
|
+
if (relayUrl && relayUrl !== parsed.relayUrl) {
|
|
65
|
+
throw new Error(`Remote relay URL mismatch: remote URL includes ${parsed.relayUrl}, but configured relay URL is ${relayUrl}`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
uuid: parsed.uuid,
|
|
69
|
+
relayUrl: relayUrl || parsed.relayUrl,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Relay connection manager
|
|
74
|
+
*
|
|
75
|
+
* Connects to a relay server (local or remote) to reach the extension.
|
|
76
|
+
*/
|
|
77
|
+
export class ExtensionConnection extends EventEmitter {
|
|
78
|
+
ws = null;
|
|
79
|
+
status = 'disconnected';
|
|
80
|
+
pendingRequests = new Map();
|
|
81
|
+
requestIdCounter = 0;
|
|
82
|
+
port;
|
|
83
|
+
debug;
|
|
84
|
+
tools = [];
|
|
85
|
+
sessions = [];
|
|
86
|
+
reconnectTimer = null;
|
|
87
|
+
extensionConnected = false;
|
|
88
|
+
remoteConfig = null;
|
|
89
|
+
localSessionConfig;
|
|
90
|
+
stopping = false;
|
|
91
|
+
constructor(port = AGENT_PORT, debug = false, remote, localSessionConfig) {
|
|
92
|
+
super();
|
|
93
|
+
this.port = port;
|
|
94
|
+
this.debug = debug;
|
|
95
|
+
this.remoteConfig = normalizeRemoteConfig(remote) || null;
|
|
96
|
+
this.localSessionConfig = localSessionConfig || {};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Start connection to relay server.
|
|
100
|
+
* In local mode: spawns relay daemon if needed, then connects.
|
|
101
|
+
* In remote mode: connects directly to public relay.
|
|
102
|
+
*/
|
|
103
|
+
async start() {
|
|
104
|
+
this.stopping = false;
|
|
105
|
+
if (this.remoteConfig) {
|
|
106
|
+
this.log(`Remote mode: connecting to relay for UUID ${this.remoteConfig.uuid}`);
|
|
107
|
+
await this.connectToRelay();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Local mode: check if relay is already running
|
|
111
|
+
if (!isRelayRunning()) {
|
|
112
|
+
this.log('Starting relay daemon...');
|
|
113
|
+
await this.spawnRelay();
|
|
114
|
+
// Wait for relay to start
|
|
115
|
+
await this.waitForRelay();
|
|
116
|
+
}
|
|
117
|
+
// Connect to relay
|
|
118
|
+
await this.connectToRelay();
|
|
119
|
+
}
|
|
120
|
+
async setRemoteUrl(url) {
|
|
121
|
+
const parsed = parseRemoteRelayUrl(url);
|
|
122
|
+
this.stopping = true;
|
|
123
|
+
this.clearReconnectTimer();
|
|
124
|
+
this.rejectPendingRequests(new Error('Remote relay changed'));
|
|
125
|
+
this.closeSocket();
|
|
126
|
+
this.remoteConfig = { uuid: parsed.uuid, relayUrl: parsed.relayUrl };
|
|
127
|
+
this.tools = [];
|
|
128
|
+
this.sessions = [];
|
|
129
|
+
this.extensionConnected = false;
|
|
130
|
+
this.status = 'disconnected';
|
|
131
|
+
this.emit('tools_updated', this.tools);
|
|
132
|
+
this.emit('extension_status', false);
|
|
133
|
+
this.stopping = false;
|
|
134
|
+
await this.connectToRelay();
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Spawn relay daemon as detached process (local mode only)
|
|
139
|
+
*/
|
|
140
|
+
async spawnRelay() {
|
|
141
|
+
// Use __dirname equivalent for ESM
|
|
142
|
+
const relayScript = join(dirname(new URL(import.meta.url).pathname), 'relay-daemon.js');
|
|
143
|
+
const child = spawn(process.execPath, [relayScript, this.debug ? '--debug' : ''], {
|
|
144
|
+
detached: true,
|
|
145
|
+
stdio: 'ignore',
|
|
146
|
+
env: process.env,
|
|
147
|
+
});
|
|
148
|
+
child.unref();
|
|
149
|
+
this.log('Relay daemon spawned');
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Wait for local relay to become available
|
|
153
|
+
*/
|
|
154
|
+
async waitForRelay() {
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
while (Date.now() - startTime < RELAY_CONNECT_TIMEOUT) {
|
|
157
|
+
try {
|
|
158
|
+
// Try to connect
|
|
159
|
+
await new Promise((resolve, reject) => {
|
|
160
|
+
const ws = new WebSocket(`ws://127.0.0.1:${this.port}`);
|
|
161
|
+
const timeout = setTimeout(() => {
|
|
162
|
+
ws.removeAllListeners();
|
|
163
|
+
ws.terminate();
|
|
164
|
+
reject(new Error('Timeout'));
|
|
165
|
+
}, 1000);
|
|
166
|
+
ws.on('open', () => {
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
ws.removeAllListeners();
|
|
169
|
+
ws.terminate();
|
|
170
|
+
resolve();
|
|
171
|
+
});
|
|
172
|
+
ws.on('error', () => {
|
|
173
|
+
clearTimeout(timeout);
|
|
174
|
+
reject(new Error('Connection failed'));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
this.log('Relay is ready');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
// Relay not ready yet, wait and retry
|
|
182
|
+
await new Promise(r => setTimeout(r, 200));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw new Error('Relay failed to start within timeout');
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get the WebSocket URL for connection.
|
|
189
|
+
* Local mode: ws://127.0.0.1:<port>
|
|
190
|
+
* Remote mode: wss://relay.api.vibebrowser.app/<uuid>
|
|
191
|
+
*/
|
|
192
|
+
getRelayUrl() {
|
|
193
|
+
if (this.remoteConfig) {
|
|
194
|
+
const base = this.remoteConfig.relayUrl || DEFAULT_RELAY_URL;
|
|
195
|
+
return `${base}/${this.remoteConfig.uuid}`;
|
|
196
|
+
}
|
|
197
|
+
return `ws://127.0.0.1:${this.port}`;
|
|
198
|
+
}
|
|
199
|
+
getRemoteConfig() {
|
|
200
|
+
return this.remoteConfig ? { ...this.remoteConfig } : null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Connect to the relay server (local or remote)
|
|
204
|
+
*/
|
|
205
|
+
async connectToRelay() {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const url = this.getRelayUrl();
|
|
208
|
+
this.log(`Connecting to relay at ${url}...`);
|
|
209
|
+
try {
|
|
210
|
+
this.ws = new WebSocket(url);
|
|
211
|
+
this.ws.on('open', () => {
|
|
212
|
+
this.log('Connected to relay');
|
|
213
|
+
this.status = 'connected';
|
|
214
|
+
this.emit('connected');
|
|
215
|
+
resolve();
|
|
216
|
+
});
|
|
217
|
+
this.ws.on('message', (data) => {
|
|
218
|
+
try {
|
|
219
|
+
const message = JSON.parse(data.toString());
|
|
220
|
+
this.handleMessage(message);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
this.log(`Failed to parse message: ${error}`);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
this.ws.on('close', () => {
|
|
227
|
+
this.log('Disconnected from relay');
|
|
228
|
+
this.ws = null;
|
|
229
|
+
this.status = 'disconnected';
|
|
230
|
+
// Reject all pending requests — responses will never arrive on a
|
|
231
|
+
// closed socket. Without this, requests sit until their individual
|
|
232
|
+
// timeouts fire, and if the server reconnects before that the MCP
|
|
233
|
+
// client may retry, causing duplicate tool execution.
|
|
234
|
+
for (const [id, request] of this.pendingRequests) {
|
|
235
|
+
clearTimeout(request.timeout);
|
|
236
|
+
request.reject(new Error('Relay connection lost'));
|
|
237
|
+
}
|
|
238
|
+
this.pendingRequests.clear();
|
|
239
|
+
this.emit('disconnected');
|
|
240
|
+
if (!this.stopping) {
|
|
241
|
+
// Schedule reconnect
|
|
242
|
+
this.scheduleReconnect();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
this.ws.on('error', (error) => {
|
|
246
|
+
this.log(`WebSocket error: ${error.message}`);
|
|
247
|
+
reject(error);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
reject(error);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Schedule reconnection attempt
|
|
257
|
+
*/
|
|
258
|
+
scheduleReconnect() {
|
|
259
|
+
if (this.reconnectTimer) {
|
|
260
|
+
clearTimeout(this.reconnectTimer);
|
|
261
|
+
}
|
|
262
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
263
|
+
this.log('Attempting to reconnect to relay...');
|
|
264
|
+
try {
|
|
265
|
+
await this.connectToRelay();
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
this.log(`Reconnect failed: ${error}`);
|
|
269
|
+
this.scheduleReconnect();
|
|
270
|
+
}
|
|
271
|
+
}, RELAY_RECONNECT_DELAY);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Stop the connection
|
|
275
|
+
*/
|
|
276
|
+
async stop() {
|
|
277
|
+
this.stopping = true;
|
|
278
|
+
this.clearReconnectTimer();
|
|
279
|
+
this.rejectPendingRequests(new Error('Connection closed'));
|
|
280
|
+
this.closeSocket();
|
|
281
|
+
this.status = 'disconnected';
|
|
282
|
+
}
|
|
283
|
+
clearReconnectTimer() {
|
|
284
|
+
if (this.reconnectTimer) {
|
|
285
|
+
clearTimeout(this.reconnectTimer);
|
|
286
|
+
this.reconnectTimer = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
rejectPendingRequests(error) {
|
|
290
|
+
for (const [, request] of this.pendingRequests) {
|
|
291
|
+
clearTimeout(request.timeout);
|
|
292
|
+
request.reject(error);
|
|
293
|
+
}
|
|
294
|
+
this.pendingRequests.clear();
|
|
295
|
+
}
|
|
296
|
+
closeSocket() {
|
|
297
|
+
if (!this.ws) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// Remove listeners so a deliberate reconnect does not trigger stale close
|
|
301
|
+
// handlers or schedule a reconnect against the previous relay URL.
|
|
302
|
+
this.ws.removeAllListeners();
|
|
303
|
+
this.ws.terminate();
|
|
304
|
+
this.ws = null;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Handle message from relay
|
|
308
|
+
*/
|
|
309
|
+
handleMessage(message) {
|
|
310
|
+
this.log(`Received: ${message.type}`);
|
|
311
|
+
// Handle extension status updates
|
|
312
|
+
if (message.type === 'extension_status') {
|
|
313
|
+
this.extensionConnected = message.connected ?? false;
|
|
314
|
+
this.emit('extension_status', this.extensionConnected);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (message.type === 'extension_disconnected') {
|
|
318
|
+
this.extensionConnected = false;
|
|
319
|
+
this.tools = [];
|
|
320
|
+
this.sessions = [];
|
|
321
|
+
this.emit('extension_disconnected');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Handle responses to pending requests
|
|
325
|
+
if (message.requestId) {
|
|
326
|
+
const pending = this.pendingRequests.get(message.requestId);
|
|
327
|
+
if (pending) {
|
|
328
|
+
clearTimeout(pending.timeout);
|
|
329
|
+
this.pendingRequests.delete(message.requestId);
|
|
330
|
+
if (message.type === 'error') {
|
|
331
|
+
pending.reject(new Error(message.error || 'Unknown error'));
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
let payload = message.data;
|
|
335
|
+
if (message.type === 'sessions_list') {
|
|
336
|
+
payload = Array.isArray(message.sessions) ? message.sessions : message.data;
|
|
337
|
+
this.sessions = Array.isArray(payload) ? payload : [];
|
|
338
|
+
if (!this.remoteConfig) {
|
|
339
|
+
const selected = this.resolveRequestedSessionId(this.sessions);
|
|
340
|
+
this.extensionConnected = this.sessions.some((session) => session.sessionId === selected && session.connected);
|
|
341
|
+
}
|
|
342
|
+
this.emit('sessions_updated', this.sessions);
|
|
343
|
+
}
|
|
344
|
+
pending.resolve(payload);
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (message.type === 'sessions_list') {
|
|
350
|
+
this.sessions = Array.isArray(message.sessions)
|
|
351
|
+
? message.sessions
|
|
352
|
+
: Array.isArray(message.data)
|
|
353
|
+
? message.data
|
|
354
|
+
: [];
|
|
355
|
+
if (!this.remoteConfig) {
|
|
356
|
+
const selected = this.resolveRequestedSessionId(this.sessions);
|
|
357
|
+
this.extensionConnected = this.sessions.some((session) => session.sessionId === selected && session.connected);
|
|
358
|
+
}
|
|
359
|
+
this.emit('sessions_updated', this.sessions);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Handle unsolicited messages
|
|
363
|
+
switch (message.type) {
|
|
364
|
+
case 'tools_list':
|
|
365
|
+
this.tools = message.data;
|
|
366
|
+
if (this.remoteConfig) {
|
|
367
|
+
this.extensionConnected = true;
|
|
368
|
+
}
|
|
369
|
+
else if (this.sessions.length === 0) {
|
|
370
|
+
this.extensionConnected = true;
|
|
371
|
+
}
|
|
372
|
+
this.emit('tools_updated', this.tools);
|
|
373
|
+
break;
|
|
374
|
+
case 'error':
|
|
375
|
+
this.log(`Error: ${message.error}`);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Send a message to the extension via relay and wait for response
|
|
381
|
+
*/
|
|
382
|
+
async sendRequest(type, data, timeoutMs = 30000) {
|
|
383
|
+
if (!this.ws || this.status !== 'connected') {
|
|
384
|
+
throw new Error('Not connected to relay');
|
|
385
|
+
}
|
|
386
|
+
const requestId = `req_${++this.requestIdCounter}`;
|
|
387
|
+
return new Promise((resolve, reject) => {
|
|
388
|
+
const timeout = setTimeout(() => {
|
|
389
|
+
this.pendingRequests.delete(requestId);
|
|
390
|
+
reject(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
391
|
+
}, timeoutMs);
|
|
392
|
+
this.pendingRequests.set(requestId, {
|
|
393
|
+
resolve: resolve,
|
|
394
|
+
reject,
|
|
395
|
+
timeout,
|
|
396
|
+
});
|
|
397
|
+
const enrichedData = this.withSessionSelection(data);
|
|
398
|
+
const message = { type, requestId, data: enrichedData };
|
|
399
|
+
this.ws.send(JSON.stringify(message));
|
|
400
|
+
this.log(`Sent: ${type} (${requestId})`);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
withSessionSelection(data) {
|
|
404
|
+
if (this.remoteConfig) {
|
|
405
|
+
return data;
|
|
406
|
+
}
|
|
407
|
+
const sessionId = this.resolveRequestedSessionId(this.sessions);
|
|
408
|
+
if (!sessionId) {
|
|
409
|
+
return data;
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
...(data || {}),
|
|
413
|
+
sessionId,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
resolveRequestedSessionId(sessions) {
|
|
417
|
+
if (this.localSessionConfig.sessionId) {
|
|
418
|
+
return this.localSessionConfig.sessionId;
|
|
419
|
+
}
|
|
420
|
+
const firstConnected = sessions.find((session) => session.connected);
|
|
421
|
+
return firstConnected?.sessionId;
|
|
422
|
+
}
|
|
423
|
+
getRequestedSessionConnectionError(sessions = this.sessions) {
|
|
424
|
+
if (this.remoteConfig || !this.localSessionConfig.sessionId || sessions.length === 0) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
const requested = this.localSessionConfig.sessionId;
|
|
428
|
+
const matched = sessions.find((session) => session.sessionId === requested);
|
|
429
|
+
if (!matched || !matched.connected) {
|
|
430
|
+
return `No browser session connected for sessionId=${requested}`;
|
|
431
|
+
}
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
getConnectionErrorMessage() {
|
|
435
|
+
if (this.remoteConfig) {
|
|
436
|
+
return NO_CONNECTION_REMOTE_MESSAGE;
|
|
437
|
+
}
|
|
438
|
+
return this.getRequestedSessionConnectionError() || NO_CONNECTION_MESSAGE;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Refresh available tools from extension
|
|
442
|
+
*/
|
|
443
|
+
async refreshTools(timeoutMs = 30_000) {
|
|
444
|
+
const tools = await this.sendRequest('list_tools', undefined, timeoutMs);
|
|
445
|
+
this.tools = tools;
|
|
446
|
+
return tools;
|
|
447
|
+
}
|
|
448
|
+
async listSessions(timeoutMs = 5_000) {
|
|
449
|
+
if (this.remoteConfig) {
|
|
450
|
+
const session = {
|
|
451
|
+
sessionId: this.remoteConfig.uuid,
|
|
452
|
+
connected: this.extensionConnected,
|
|
453
|
+
toolCount: this.tools.length,
|
|
454
|
+
};
|
|
455
|
+
this.sessions = [session];
|
|
456
|
+
return this.sessions;
|
|
457
|
+
}
|
|
458
|
+
const sessions = await this.sendRequest('list_sessions', undefined, timeoutMs);
|
|
459
|
+
this.sessions = Array.isArray(sessions) ? sessions : [];
|
|
460
|
+
const selected = this.resolveRequestedSessionId(this.sessions);
|
|
461
|
+
this.extensionConnected = this.sessions.some((session) => session.sessionId === selected && session.connected);
|
|
462
|
+
return this.sessions;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Wait briefly for a tools update event without forcing a request.
|
|
466
|
+
* This keeps MCP startup responsive when extension announces tools asynchronously.
|
|
467
|
+
*/
|
|
468
|
+
async waitForToolsUpdate(timeoutMs = 1_500) {
|
|
469
|
+
if (this.tools.length > 0) {
|
|
470
|
+
return this.tools;
|
|
471
|
+
}
|
|
472
|
+
return new Promise((resolve) => {
|
|
473
|
+
const onToolsUpdated = (tools) => {
|
|
474
|
+
cleanup();
|
|
475
|
+
resolve(tools);
|
|
476
|
+
};
|
|
477
|
+
const onExtensionDisconnected = () => {
|
|
478
|
+
cleanup();
|
|
479
|
+
resolve(this.tools);
|
|
480
|
+
};
|
|
481
|
+
const timer = setTimeout(() => {
|
|
482
|
+
cleanup();
|
|
483
|
+
resolve(this.tools);
|
|
484
|
+
}, timeoutMs);
|
|
485
|
+
const cleanup = () => {
|
|
486
|
+
clearTimeout(timer);
|
|
487
|
+
this.off('tools_updated', onToolsUpdated);
|
|
488
|
+
this.off('extension_disconnected', onExtensionDisconnected);
|
|
489
|
+
};
|
|
490
|
+
this.on('tools_updated', onToolsUpdated);
|
|
491
|
+
this.on('extension_disconnected', onExtensionDisconnected);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Get cached list of available tools
|
|
496
|
+
*/
|
|
497
|
+
getTools() {
|
|
498
|
+
return this.tools;
|
|
499
|
+
}
|
|
500
|
+
getSessions() {
|
|
501
|
+
return this.sessions;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Call a tool on the extension
|
|
505
|
+
*/
|
|
506
|
+
async callTool(name, args, timeoutMs) {
|
|
507
|
+
return this.sendRequest('call_tool', { name, arguments: args }, timeoutMs);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Check if extension is connected (via relay)
|
|
511
|
+
*/
|
|
512
|
+
isConnected() {
|
|
513
|
+
return this.status === 'connected' && this.extensionConnected;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Get connection status
|
|
517
|
+
*/
|
|
518
|
+
getStatus() {
|
|
519
|
+
return this.status;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Check if extension is connected to relay
|
|
523
|
+
*/
|
|
524
|
+
isExtensionConnected() {
|
|
525
|
+
return this.extensionConnected;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Log message if debug is enabled
|
|
529
|
+
*/
|
|
530
|
+
log(message) {
|
|
531
|
+
if (this.debug) {
|
|
532
|
+
console.error(`[vibebrowser-mcp] ${message}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
//# sourceMappingURL=connection.js.map
|