@willjackson/claude-code-bridge 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/LICENSE +21 -0
- package/README.md +690 -0
- package/dist/chunk-BRH476VK.js +1993 -0
- package/dist/chunk-BRH476VK.js.map +1 -0
- package/dist/cli.d.ts +40 -0
- package/dist/cli.js +844 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1848 -0
- package/dist/index.js +976 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,1993 @@
|
|
|
1
|
+
// src/utils/logger.ts
|
|
2
|
+
import pino from "pino";
|
|
3
|
+
function isDevelopment() {
|
|
4
|
+
return process.env.NODE_ENV !== "production";
|
|
5
|
+
}
|
|
6
|
+
function getDefaultLevel() {
|
|
7
|
+
const envLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
8
|
+
const validLevels = ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
9
|
+
if (envLevel && validLevels.includes(envLevel)) {
|
|
10
|
+
return envLevel;
|
|
11
|
+
}
|
|
12
|
+
return "info";
|
|
13
|
+
}
|
|
14
|
+
function createLogger(name, level) {
|
|
15
|
+
const logLevel = level ?? getDefaultLevel();
|
|
16
|
+
const options = {
|
|
17
|
+
name,
|
|
18
|
+
level: logLevel
|
|
19
|
+
};
|
|
20
|
+
if (isDevelopment()) {
|
|
21
|
+
options.transport = {
|
|
22
|
+
target: "pino-pretty",
|
|
23
|
+
options: {
|
|
24
|
+
colorize: true,
|
|
25
|
+
translateTime: "SYS:standard",
|
|
26
|
+
ignore: "pid,hostname"
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return pino(options);
|
|
31
|
+
}
|
|
32
|
+
function createChildLogger(parent, bindings) {
|
|
33
|
+
return parent.child(bindings);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/bridge/protocol.ts
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
import { v4 as uuidv4 } from "uuid";
|
|
39
|
+
var MessageType = z.enum([
|
|
40
|
+
"request",
|
|
41
|
+
"response",
|
|
42
|
+
"context_sync",
|
|
43
|
+
"task_delegate",
|
|
44
|
+
"notification"
|
|
45
|
+
]);
|
|
46
|
+
var FileChunkSchema = z.object({
|
|
47
|
+
path: z.string(),
|
|
48
|
+
content: z.string(),
|
|
49
|
+
startLine: z.number().optional(),
|
|
50
|
+
endLine: z.number().optional(),
|
|
51
|
+
language: z.string().optional()
|
|
52
|
+
});
|
|
53
|
+
var DirectoryTreeSchema = z.lazy(
|
|
54
|
+
() => z.object({
|
|
55
|
+
name: z.string(),
|
|
56
|
+
type: z.enum(["file", "directory"]),
|
|
57
|
+
children: z.array(DirectoryTreeSchema).optional()
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
var ArtifactSchema = z.object({
|
|
61
|
+
path: z.string(),
|
|
62
|
+
action: z.enum(["created", "modified", "deleted"]),
|
|
63
|
+
diff: z.string().optional()
|
|
64
|
+
});
|
|
65
|
+
var ContextSchema = z.object({
|
|
66
|
+
files: z.array(FileChunkSchema).optional(),
|
|
67
|
+
tree: DirectoryTreeSchema.optional(),
|
|
68
|
+
summary: z.string().optional(),
|
|
69
|
+
variables: z.record(z.any()).optional()
|
|
70
|
+
});
|
|
71
|
+
var TaskRequestSchema = z.object({
|
|
72
|
+
id: z.string(),
|
|
73
|
+
description: z.string(),
|
|
74
|
+
scope: z.enum(["execute", "analyze", "suggest"]),
|
|
75
|
+
constraints: z.array(z.string()).optional(),
|
|
76
|
+
returnFormat: z.enum(["full", "summary", "diff"]).optional(),
|
|
77
|
+
timeout: z.number().optional()
|
|
78
|
+
});
|
|
79
|
+
var TaskResultSchema = z.object({
|
|
80
|
+
taskId: z.string().optional(),
|
|
81
|
+
success: z.boolean(),
|
|
82
|
+
data: z.any(),
|
|
83
|
+
artifacts: z.array(ArtifactSchema).optional(),
|
|
84
|
+
followUp: z.string().optional(),
|
|
85
|
+
error: z.string().optional()
|
|
86
|
+
});
|
|
87
|
+
var BridgeMessageSchema = z.object({
|
|
88
|
+
id: z.string().uuid(),
|
|
89
|
+
type: MessageType,
|
|
90
|
+
source: z.string(),
|
|
91
|
+
timestamp: z.number(),
|
|
92
|
+
context: ContextSchema.optional(),
|
|
93
|
+
task: TaskRequestSchema.optional(),
|
|
94
|
+
result: TaskResultSchema.optional()
|
|
95
|
+
});
|
|
96
|
+
function createMessage(type, source) {
|
|
97
|
+
return {
|
|
98
|
+
id: uuidv4(),
|
|
99
|
+
type,
|
|
100
|
+
source,
|
|
101
|
+
timestamp: Date.now()
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function validateMessage(data) {
|
|
105
|
+
return BridgeMessageSchema.parse(data);
|
|
106
|
+
}
|
|
107
|
+
function safeValidateMessage(data) {
|
|
108
|
+
return BridgeMessageSchema.safeParse(data);
|
|
109
|
+
}
|
|
110
|
+
function serializeMessage(message) {
|
|
111
|
+
return JSON.stringify(message);
|
|
112
|
+
}
|
|
113
|
+
function deserializeMessage(json) {
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(json);
|
|
117
|
+
} catch {
|
|
118
|
+
throw new Error("Invalid JSON");
|
|
119
|
+
}
|
|
120
|
+
return validateMessage(parsed);
|
|
121
|
+
}
|
|
122
|
+
function safeDeserializeMessage(json) {
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(json);
|
|
126
|
+
} catch {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: new z.ZodError([
|
|
130
|
+
{
|
|
131
|
+
code: "custom",
|
|
132
|
+
message: "Invalid JSON",
|
|
133
|
+
path: []
|
|
134
|
+
}
|
|
135
|
+
])
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return BridgeMessageSchema.safeParse(parsed);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/transport/interface.ts
|
|
142
|
+
var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
|
|
143
|
+
ConnectionState2["DISCONNECTED"] = "DISCONNECTED";
|
|
144
|
+
ConnectionState2["CONNECTING"] = "CONNECTING";
|
|
145
|
+
ConnectionState2["CONNECTED"] = "CONNECTED";
|
|
146
|
+
ConnectionState2["RECONNECTING"] = "RECONNECTING";
|
|
147
|
+
return ConnectionState2;
|
|
148
|
+
})(ConnectionState || {});
|
|
149
|
+
|
|
150
|
+
// src/transport/websocket.ts
|
|
151
|
+
import WebSocket from "ws";
|
|
152
|
+
var logger = createLogger("websocket-transport");
|
|
153
|
+
var DEFAULT_RECONNECT_INTERVAL = 1e3;
|
|
154
|
+
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
|
|
155
|
+
var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
|
|
156
|
+
var HEARTBEAT_TIMEOUT = 1e4;
|
|
157
|
+
var WebSocketTransport = class {
|
|
158
|
+
ws = null;
|
|
159
|
+
state = "DISCONNECTED" /* DISCONNECTED */;
|
|
160
|
+
config = null;
|
|
161
|
+
// Reconnection state
|
|
162
|
+
reconnectAttempts = 0;
|
|
163
|
+
reconnectTimer = null;
|
|
164
|
+
intentionalDisconnect = false;
|
|
165
|
+
// Message queue for offline messages
|
|
166
|
+
messageQueue = [];
|
|
167
|
+
// Heartbeat state
|
|
168
|
+
heartbeatInterval = null;
|
|
169
|
+
heartbeatTimeout = null;
|
|
170
|
+
awaitingPong = false;
|
|
171
|
+
// Event handlers
|
|
172
|
+
messageHandlers = [];
|
|
173
|
+
disconnectHandlers = [];
|
|
174
|
+
errorHandlers = [];
|
|
175
|
+
reconnectingHandlers = [];
|
|
176
|
+
/**
|
|
177
|
+
* Build the WebSocket URL from the connection configuration
|
|
178
|
+
*/
|
|
179
|
+
buildUrl(config) {
|
|
180
|
+
if (config.url) {
|
|
181
|
+
return config.url;
|
|
182
|
+
}
|
|
183
|
+
const host = config.host ?? "localhost";
|
|
184
|
+
const port = config.port ?? 8765;
|
|
185
|
+
return `ws://${host}:${port}`;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Establish connection to a remote peer
|
|
189
|
+
*/
|
|
190
|
+
async connect(config) {
|
|
191
|
+
if (this.state === "CONNECTED" /* CONNECTED */) {
|
|
192
|
+
throw new Error("Already connected");
|
|
193
|
+
}
|
|
194
|
+
this.config = config;
|
|
195
|
+
this.intentionalDisconnect = false;
|
|
196
|
+
this.reconnectAttempts = 0;
|
|
197
|
+
return this.establishConnection();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Internal method to establish WebSocket connection
|
|
201
|
+
* Used for both initial connection and reconnection attempts
|
|
202
|
+
*/
|
|
203
|
+
async establishConnection() {
|
|
204
|
+
if (!this.config) {
|
|
205
|
+
throw new Error("No configuration set");
|
|
206
|
+
}
|
|
207
|
+
this.state = "CONNECTING" /* CONNECTING */;
|
|
208
|
+
const url = this.buildUrl(this.config);
|
|
209
|
+
logger.debug({ url, attempt: this.reconnectAttempts }, "Connecting to WebSocket server");
|
|
210
|
+
return new Promise((resolve, reject) => {
|
|
211
|
+
try {
|
|
212
|
+
this.ws = new WebSocket(url);
|
|
213
|
+
this.ws.on("open", () => {
|
|
214
|
+
this.state = "CONNECTED" /* CONNECTED */;
|
|
215
|
+
this.reconnectAttempts = 0;
|
|
216
|
+
logger.info({ url }, "WebSocket connection established");
|
|
217
|
+
this.startHeartbeat();
|
|
218
|
+
this.flushMessageQueue();
|
|
219
|
+
resolve();
|
|
220
|
+
});
|
|
221
|
+
this.ws.on("message", (data) => {
|
|
222
|
+
this.handleIncomingMessage(data);
|
|
223
|
+
});
|
|
224
|
+
this.ws.on("pong", () => {
|
|
225
|
+
this.handlePong();
|
|
226
|
+
});
|
|
227
|
+
this.ws.on("close", (code, reason) => {
|
|
228
|
+
const wasConnected = this.state === "CONNECTED" /* CONNECTED */;
|
|
229
|
+
const wasReconnecting = this.state === "RECONNECTING" /* RECONNECTING */;
|
|
230
|
+
this.stopHeartbeat();
|
|
231
|
+
logger.info({ code, reason: reason.toString() }, "WebSocket connection closed");
|
|
232
|
+
if (wasConnected) {
|
|
233
|
+
this.notifyDisconnect();
|
|
234
|
+
}
|
|
235
|
+
if (!this.intentionalDisconnect && this.shouldReconnect()) {
|
|
236
|
+
this.scheduleReconnect();
|
|
237
|
+
} else if (!wasReconnecting) {
|
|
238
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
this.ws.on("error", (error) => {
|
|
242
|
+
logger.error({ error: error.message }, "WebSocket error");
|
|
243
|
+
if (this.state === "CONNECTING" /* CONNECTING */ && this.reconnectAttempts === 0) {
|
|
244
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
245
|
+
reject(error);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this.notifyError(error);
|
|
249
|
+
});
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
252
|
+
reject(error);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Cleanly close the current connection
|
|
258
|
+
*/
|
|
259
|
+
async disconnect() {
|
|
260
|
+
this.intentionalDisconnect = true;
|
|
261
|
+
this.clearReconnectTimer();
|
|
262
|
+
this.stopHeartbeat();
|
|
263
|
+
this.messageQueue = [];
|
|
264
|
+
if (!this.ws || this.state === "DISCONNECTED" /* DISCONNECTED */) {
|
|
265
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
logger.debug("Disconnecting WebSocket");
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
if (!this.ws) {
|
|
271
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
272
|
+
resolve();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const onClose = () => {
|
|
276
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
277
|
+
this.ws = null;
|
|
278
|
+
resolve();
|
|
279
|
+
};
|
|
280
|
+
if (this.ws.readyState === WebSocket.CLOSED) {
|
|
281
|
+
onClose();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const timeout = setTimeout(() => {
|
|
285
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
286
|
+
this.ws = null;
|
|
287
|
+
resolve();
|
|
288
|
+
}, 5e3);
|
|
289
|
+
this.ws.once("close", () => {
|
|
290
|
+
clearTimeout(timeout);
|
|
291
|
+
onClose();
|
|
292
|
+
});
|
|
293
|
+
this.ws.close(1e3, "Disconnect requested");
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Send a message to the connected peer
|
|
298
|
+
* If disconnected and reconnection is enabled, queues the message for later delivery
|
|
299
|
+
*/
|
|
300
|
+
async send(message) {
|
|
301
|
+
if (this.ws && this.state === "CONNECTED" /* CONNECTED */) {
|
|
302
|
+
return this.sendImmediate(message);
|
|
303
|
+
}
|
|
304
|
+
if (this.shouldReconnect() && (this.state === "RECONNECTING" /* RECONNECTING */ || this.state === "DISCONNECTED" /* DISCONNECTED */)) {
|
|
305
|
+
this.queueMessage(message);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
throw new Error("Not connected");
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Immediately send a message over the WebSocket
|
|
312
|
+
*/
|
|
313
|
+
async sendImmediate(message) {
|
|
314
|
+
if (!this.ws || this.state !== "CONNECTED" /* CONNECTED */) {
|
|
315
|
+
throw new Error("Not connected");
|
|
316
|
+
}
|
|
317
|
+
const serialized = serializeMessage(message);
|
|
318
|
+
logger.debug({ messageId: message.id, type: message.type }, "Sending message");
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
this.ws.send(serialized, (error) => {
|
|
321
|
+
if (error) {
|
|
322
|
+
logger.error({ error: error.message, messageId: message.id }, "Failed to send message");
|
|
323
|
+
reject(error);
|
|
324
|
+
} else {
|
|
325
|
+
resolve();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Queue a message for later delivery when reconnected
|
|
332
|
+
*/
|
|
333
|
+
queueMessage(message) {
|
|
334
|
+
this.messageQueue.push(message);
|
|
335
|
+
logger.debug({ messageId: message.id, queueLength: this.messageQueue.length }, "Message queued for delivery");
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Flush all queued messages after reconnection
|
|
339
|
+
*/
|
|
340
|
+
async flushMessageQueue() {
|
|
341
|
+
if (this.messageQueue.length === 0) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
logger.info({ queueLength: this.messageQueue.length }, "Flushing message queue");
|
|
345
|
+
const messages = [...this.messageQueue];
|
|
346
|
+
this.messageQueue = [];
|
|
347
|
+
for (const message of messages) {
|
|
348
|
+
try {
|
|
349
|
+
await this.sendImmediate(message);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
logger.error({ error: error.message, messageId: message.id }, "Failed to send queued message");
|
|
352
|
+
this.messageQueue.unshift(message);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Register a handler for incoming messages
|
|
359
|
+
*/
|
|
360
|
+
onMessage(handler) {
|
|
361
|
+
this.messageHandlers.push(handler);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Register a handler for disconnect events
|
|
365
|
+
*/
|
|
366
|
+
onDisconnect(handler) {
|
|
367
|
+
this.disconnectHandlers.push(handler);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Register a handler for error events
|
|
371
|
+
*/
|
|
372
|
+
onError(handler) {
|
|
373
|
+
this.errorHandlers.push(handler);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Register a handler for reconnecting events
|
|
377
|
+
*/
|
|
378
|
+
onReconnecting(handler) {
|
|
379
|
+
this.reconnectingHandlers.push(handler);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Check if the transport is currently connected
|
|
383
|
+
*/
|
|
384
|
+
isConnected() {
|
|
385
|
+
return this.state === "CONNECTED" /* CONNECTED */;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get the current connection state
|
|
389
|
+
*/
|
|
390
|
+
getState() {
|
|
391
|
+
return this.state;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Handle incoming WebSocket messages
|
|
395
|
+
*/
|
|
396
|
+
handleIncomingMessage(data) {
|
|
397
|
+
const messageString = data.toString();
|
|
398
|
+
logger.debug({ dataLength: messageString.length }, "Received message");
|
|
399
|
+
const result = safeDeserializeMessage(messageString);
|
|
400
|
+
if (!result.success) {
|
|
401
|
+
logger.warn({ error: result.error.message }, "Failed to parse incoming message");
|
|
402
|
+
this.notifyError(new Error(`Invalid message format: ${result.error.message}`));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const message = result.data;
|
|
406
|
+
logger.debug({ messageId: message.id, type: message.type }, "Parsed message");
|
|
407
|
+
for (const handler of this.messageHandlers) {
|
|
408
|
+
try {
|
|
409
|
+
handler(message);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
logger.error({ error: error.message }, "Message handler threw error");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Notify all disconnect handlers
|
|
417
|
+
*/
|
|
418
|
+
notifyDisconnect() {
|
|
419
|
+
for (const handler of this.disconnectHandlers) {
|
|
420
|
+
try {
|
|
421
|
+
handler();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
logger.error({ error: error.message }, "Disconnect handler threw error");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Notify all error handlers
|
|
429
|
+
*/
|
|
430
|
+
notifyError(error) {
|
|
431
|
+
for (const handler of this.errorHandlers) {
|
|
432
|
+
try {
|
|
433
|
+
handler(error);
|
|
434
|
+
} catch (handlerError) {
|
|
435
|
+
logger.error({ error: handlerError.message }, "Error handler threw error");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Notify all reconnecting handlers
|
|
441
|
+
*/
|
|
442
|
+
notifyReconnecting(attempt, maxAttempts) {
|
|
443
|
+
for (const handler of this.reconnectingHandlers) {
|
|
444
|
+
try {
|
|
445
|
+
handler(attempt, maxAttempts);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
logger.error({ error: error.message }, "Reconnecting handler threw error");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// ============================================================================
|
|
452
|
+
// Reconnection Methods
|
|
453
|
+
// ============================================================================
|
|
454
|
+
/**
|
|
455
|
+
* Check if reconnection should be attempted
|
|
456
|
+
*/
|
|
457
|
+
shouldReconnect() {
|
|
458
|
+
if (!this.config?.reconnect) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const maxAttempts = this.config.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
462
|
+
return this.reconnectAttempts < maxAttempts;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Schedule a reconnection attempt
|
|
466
|
+
*/
|
|
467
|
+
scheduleReconnect() {
|
|
468
|
+
if (this.reconnectTimer) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.state = "RECONNECTING" /* RECONNECTING */;
|
|
472
|
+
this.reconnectAttempts++;
|
|
473
|
+
const maxAttempts = this.config?.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
474
|
+
const interval = this.config?.reconnectInterval ?? DEFAULT_RECONNECT_INTERVAL;
|
|
475
|
+
logger.info(
|
|
476
|
+
{ attempt: this.reconnectAttempts, maxAttempts, interval },
|
|
477
|
+
"Scheduling reconnection attempt"
|
|
478
|
+
);
|
|
479
|
+
this.notifyReconnecting(this.reconnectAttempts, maxAttempts);
|
|
480
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
481
|
+
this.reconnectTimer = null;
|
|
482
|
+
try {
|
|
483
|
+
await this.establishConnection();
|
|
484
|
+
logger.info({ attempts: this.reconnectAttempts }, "Reconnection successful");
|
|
485
|
+
} catch (error) {
|
|
486
|
+
logger.warn({ error: error.message }, "Reconnection attempt failed");
|
|
487
|
+
if (this.shouldReconnect()) {
|
|
488
|
+
this.scheduleReconnect();
|
|
489
|
+
} else {
|
|
490
|
+
logger.error({ maxAttempts }, "Max reconnection attempts reached, giving up");
|
|
491
|
+
this.state = "DISCONNECTED" /* DISCONNECTED */;
|
|
492
|
+
this.notifyError(new Error(`Failed to reconnect after ${maxAttempts} attempts`));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}, interval);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Clear any pending reconnection timer
|
|
499
|
+
*/
|
|
500
|
+
clearReconnectTimer() {
|
|
501
|
+
if (this.reconnectTimer) {
|
|
502
|
+
clearTimeout(this.reconnectTimer);
|
|
503
|
+
this.reconnectTimer = null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// ============================================================================
|
|
507
|
+
// Heartbeat Methods
|
|
508
|
+
// ============================================================================
|
|
509
|
+
/**
|
|
510
|
+
* Start the heartbeat monitoring
|
|
511
|
+
*/
|
|
512
|
+
startHeartbeat() {
|
|
513
|
+
this.stopHeartbeat();
|
|
514
|
+
this.heartbeatInterval = setInterval(() => {
|
|
515
|
+
this.sendPing();
|
|
516
|
+
}, DEFAULT_HEARTBEAT_INTERVAL);
|
|
517
|
+
logger.debug({ interval: DEFAULT_HEARTBEAT_INTERVAL }, "Heartbeat monitoring started");
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Stop the heartbeat monitoring
|
|
521
|
+
*/
|
|
522
|
+
stopHeartbeat() {
|
|
523
|
+
if (this.heartbeatInterval) {
|
|
524
|
+
clearInterval(this.heartbeatInterval);
|
|
525
|
+
this.heartbeatInterval = null;
|
|
526
|
+
}
|
|
527
|
+
if (this.heartbeatTimeout) {
|
|
528
|
+
clearTimeout(this.heartbeatTimeout);
|
|
529
|
+
this.heartbeatTimeout = null;
|
|
530
|
+
}
|
|
531
|
+
this.awaitingPong = false;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Send a ping to the peer
|
|
535
|
+
*/
|
|
536
|
+
sendPing() {
|
|
537
|
+
if (!this.ws || this.state !== "CONNECTED" /* CONNECTED */) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (this.awaitingPong) {
|
|
541
|
+
logger.warn("No pong received, connection may be dead");
|
|
542
|
+
this.handleHeartbeatTimeout();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
this.awaitingPong = true;
|
|
546
|
+
this.ws.ping();
|
|
547
|
+
this.heartbeatTimeout = setTimeout(() => {
|
|
548
|
+
if (this.awaitingPong) {
|
|
549
|
+
this.handleHeartbeatTimeout();
|
|
550
|
+
}
|
|
551
|
+
}, HEARTBEAT_TIMEOUT);
|
|
552
|
+
logger.debug("Ping sent");
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Handle pong response from peer
|
|
556
|
+
*/
|
|
557
|
+
handlePong() {
|
|
558
|
+
this.awaitingPong = false;
|
|
559
|
+
if (this.heartbeatTimeout) {
|
|
560
|
+
clearTimeout(this.heartbeatTimeout);
|
|
561
|
+
this.heartbeatTimeout = null;
|
|
562
|
+
}
|
|
563
|
+
logger.debug("Pong received");
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Handle heartbeat timeout (no pong received)
|
|
567
|
+
*/
|
|
568
|
+
handleHeartbeatTimeout() {
|
|
569
|
+
logger.warn("Heartbeat timeout - closing connection");
|
|
570
|
+
this.awaitingPong = false;
|
|
571
|
+
if (this.heartbeatTimeout) {
|
|
572
|
+
clearTimeout(this.heartbeatTimeout);
|
|
573
|
+
this.heartbeatTimeout = null;
|
|
574
|
+
}
|
|
575
|
+
if (this.ws) {
|
|
576
|
+
this.ws.terminate();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// ============================================================================
|
|
580
|
+
// Getters for testing/debugging
|
|
581
|
+
// ============================================================================
|
|
582
|
+
/**
|
|
583
|
+
* Get the current message queue length (for testing)
|
|
584
|
+
*/
|
|
585
|
+
getQueueLength() {
|
|
586
|
+
return this.messageQueue.length;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Get the number of reconnection attempts (for testing)
|
|
590
|
+
*/
|
|
591
|
+
getReconnectAttempts() {
|
|
592
|
+
return this.reconnectAttempts;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// src/bridge/messages.ts
|
|
597
|
+
function createContextSyncMessage(source, context) {
|
|
598
|
+
const message = createMessage("context_sync", source);
|
|
599
|
+
return {
|
|
600
|
+
...message,
|
|
601
|
+
context
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function createTaskDelegateMessage(source, task) {
|
|
605
|
+
const message = createMessage("task_delegate", source);
|
|
606
|
+
return {
|
|
607
|
+
...message,
|
|
608
|
+
task
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function createTaskResponseMessage(source, taskId, result) {
|
|
612
|
+
const message = createMessage("response", source);
|
|
613
|
+
return {
|
|
614
|
+
...message,
|
|
615
|
+
result: {
|
|
616
|
+
...result,
|
|
617
|
+
taskId
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function createContextRequestMessage(source, query) {
|
|
622
|
+
const message = createMessage("request", source);
|
|
623
|
+
return {
|
|
624
|
+
...message,
|
|
625
|
+
context: {
|
|
626
|
+
summary: query
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function createNotificationMessage(source, notification) {
|
|
631
|
+
const message = createMessage("notification", source);
|
|
632
|
+
return {
|
|
633
|
+
...message,
|
|
634
|
+
context: {
|
|
635
|
+
summary: notification.message,
|
|
636
|
+
variables: {
|
|
637
|
+
notificationType: notification.type,
|
|
638
|
+
...notification.data
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/bridge/core.ts
|
|
645
|
+
import { WebSocketServer } from "ws";
|
|
646
|
+
import { v4 as uuidv42 } from "uuid";
|
|
647
|
+
var logger2 = createLogger("bridge");
|
|
648
|
+
var Bridge = class {
|
|
649
|
+
config;
|
|
650
|
+
server = null;
|
|
651
|
+
clientTransport = null;
|
|
652
|
+
peers = /* @__PURE__ */ new Map();
|
|
653
|
+
started = false;
|
|
654
|
+
// Event handlers
|
|
655
|
+
peerConnectedHandlers = [];
|
|
656
|
+
peerDisconnectedHandlers = [];
|
|
657
|
+
messageReceivedHandlers = [];
|
|
658
|
+
taskReceivedHandler = null;
|
|
659
|
+
contextReceivedHandlers = [];
|
|
660
|
+
contextRequestedHandler = null;
|
|
661
|
+
// Task correlation
|
|
662
|
+
pendingTasks = /* @__PURE__ */ new Map();
|
|
663
|
+
// Context request correlation
|
|
664
|
+
pendingContextRequests = /* @__PURE__ */ new Map();
|
|
665
|
+
// Auto-sync interval timer
|
|
666
|
+
autoSyncIntervalId = null;
|
|
667
|
+
/**
|
|
668
|
+
* Create a new Bridge instance
|
|
669
|
+
* @param config Bridge configuration
|
|
670
|
+
*/
|
|
671
|
+
constructor(config) {
|
|
672
|
+
this.config = config;
|
|
673
|
+
this.validateConfig();
|
|
674
|
+
logger2.info({ instanceName: config.instanceName, mode: config.mode }, "Bridge instance created");
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Validate the configuration based on mode requirements
|
|
678
|
+
*/
|
|
679
|
+
validateConfig() {
|
|
680
|
+
const { mode, listen, connect } = this.config;
|
|
681
|
+
if (mode === "host" && !listen) {
|
|
682
|
+
throw new Error("'host' mode requires 'listen' configuration");
|
|
683
|
+
}
|
|
684
|
+
if (mode === "client" && !connect) {
|
|
685
|
+
throw new Error("'client' mode requires 'connect' configuration");
|
|
686
|
+
}
|
|
687
|
+
if (mode === "peer" && !listen && !connect) {
|
|
688
|
+
throw new Error("'peer' mode requires either 'listen' or 'connect' configuration (or both)");
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Start the bridge based on configured mode
|
|
693
|
+
* - 'host': Starts WebSocket server
|
|
694
|
+
* - 'client': Connects to remote bridge
|
|
695
|
+
* - 'peer': Both starts server and connects to remote
|
|
696
|
+
*/
|
|
697
|
+
async start() {
|
|
698
|
+
if (this.started) {
|
|
699
|
+
throw new Error("Bridge is already started");
|
|
700
|
+
}
|
|
701
|
+
const { mode } = this.config;
|
|
702
|
+
logger2.info({ mode }, "Starting bridge");
|
|
703
|
+
try {
|
|
704
|
+
if ((mode === "host" || mode === "peer") && this.config.listen) {
|
|
705
|
+
await this.startServer();
|
|
706
|
+
}
|
|
707
|
+
if ((mode === "client" || mode === "peer") && this.config.connect) {
|
|
708
|
+
await this.connectToRemote();
|
|
709
|
+
}
|
|
710
|
+
this.started = true;
|
|
711
|
+
logger2.info({ mode, instanceName: this.config.instanceName }, "Bridge started successfully");
|
|
712
|
+
} catch (error) {
|
|
713
|
+
await this.cleanup();
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Stop the bridge and close all connections
|
|
719
|
+
*/
|
|
720
|
+
async stop() {
|
|
721
|
+
if (!this.started) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
logger2.info("Stopping bridge");
|
|
725
|
+
await this.cleanup();
|
|
726
|
+
this.started = false;
|
|
727
|
+
logger2.info("Bridge stopped");
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Get list of connected peers
|
|
731
|
+
*/
|
|
732
|
+
getPeers() {
|
|
733
|
+
return Array.from(this.peers.values()).map((p) => p.info);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Connect to a remote bridge
|
|
737
|
+
* @param url WebSocket URL to connect to
|
|
738
|
+
*/
|
|
739
|
+
async connectToPeer(url) {
|
|
740
|
+
const transport = new WebSocketTransport();
|
|
741
|
+
transport.onMessage((message) => {
|
|
742
|
+
this.handleMessage(message, transport);
|
|
743
|
+
});
|
|
744
|
+
transport.onDisconnect(() => {
|
|
745
|
+
this.handleClientDisconnect(transport);
|
|
746
|
+
});
|
|
747
|
+
try {
|
|
748
|
+
await transport.connect({
|
|
749
|
+
url,
|
|
750
|
+
reconnect: true,
|
|
751
|
+
reconnectInterval: 1e3,
|
|
752
|
+
maxReconnectAttempts: 10
|
|
753
|
+
});
|
|
754
|
+
const peerId = uuidv42();
|
|
755
|
+
const peerInfo = {
|
|
756
|
+
id: peerId,
|
|
757
|
+
name: "remote",
|
|
758
|
+
// Will be updated when we receive peer info
|
|
759
|
+
connectedAt: Date.now(),
|
|
760
|
+
lastActivity: Date.now()
|
|
761
|
+
};
|
|
762
|
+
this.peers.set(peerId, {
|
|
763
|
+
info: peerInfo,
|
|
764
|
+
transport
|
|
765
|
+
});
|
|
766
|
+
transport._peerId = peerId;
|
|
767
|
+
this.notifyPeerConnected(peerInfo);
|
|
768
|
+
logger2.info({ peerId, url }, "Connected to remote peer");
|
|
769
|
+
} catch (error) {
|
|
770
|
+
logger2.error({ error: error.message, url }, "Failed to connect to remote peer");
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Disconnect from a specific peer
|
|
776
|
+
* @param peerId ID of the peer to disconnect from
|
|
777
|
+
*/
|
|
778
|
+
async disconnectFromPeer(peerId) {
|
|
779
|
+
const peer = this.peers.get(peerId);
|
|
780
|
+
if (!peer) {
|
|
781
|
+
throw new Error(`Peer not found: ${peerId}`);
|
|
782
|
+
}
|
|
783
|
+
if (peer.transport) {
|
|
784
|
+
await peer.transport.disconnect();
|
|
785
|
+
}
|
|
786
|
+
if (peer.ws) {
|
|
787
|
+
peer.ws.close(1e3, "Disconnect requested");
|
|
788
|
+
}
|
|
789
|
+
this.peers.delete(peerId);
|
|
790
|
+
this.notifyPeerDisconnected(peer.info);
|
|
791
|
+
logger2.info({ peerId }, "Disconnected from peer");
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Send a message to a specific peer
|
|
795
|
+
* @param peerId ID of the peer to send to
|
|
796
|
+
* @param message Message to send
|
|
797
|
+
*/
|
|
798
|
+
async sendToPeer(peerId, message) {
|
|
799
|
+
const peer = this.peers.get(peerId);
|
|
800
|
+
if (!peer) {
|
|
801
|
+
throw new Error(`Peer not found: ${peerId}`);
|
|
802
|
+
}
|
|
803
|
+
if (peer.transport) {
|
|
804
|
+
await peer.transport.send(message);
|
|
805
|
+
} else if (peer.ws) {
|
|
806
|
+
const serialized = serializeMessage(message);
|
|
807
|
+
await new Promise((resolve, reject) => {
|
|
808
|
+
peer.ws.send(serialized, (error) => {
|
|
809
|
+
if (error) {
|
|
810
|
+
reject(error);
|
|
811
|
+
} else {
|
|
812
|
+
resolve();
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
} else {
|
|
817
|
+
throw new Error("No transport available for peer");
|
|
818
|
+
}
|
|
819
|
+
logger2.debug({ peerId, messageId: message.id, type: message.type }, "Sent message to peer");
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Broadcast a message to all connected peers
|
|
823
|
+
* @param message Message to broadcast
|
|
824
|
+
*/
|
|
825
|
+
async broadcast(message) {
|
|
826
|
+
const sendPromises = Array.from(this.peers.keys()).map(
|
|
827
|
+
(peerId) => this.sendToPeer(peerId, message).catch((error) => {
|
|
828
|
+
logger2.error({ error: error.message, peerId }, "Failed to send to peer");
|
|
829
|
+
})
|
|
830
|
+
);
|
|
831
|
+
await Promise.all(sendPromises);
|
|
832
|
+
logger2.debug({ messageId: message.id, peerCount: this.peers.size }, "Broadcast message sent");
|
|
833
|
+
}
|
|
834
|
+
// ============================================================================
|
|
835
|
+
// Event Registration
|
|
836
|
+
// ============================================================================
|
|
837
|
+
/**
|
|
838
|
+
* Register a handler for peer connection events
|
|
839
|
+
*/
|
|
840
|
+
onPeerConnected(handler) {
|
|
841
|
+
this.peerConnectedHandlers.push(handler);
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Register a handler for peer disconnection events
|
|
845
|
+
*/
|
|
846
|
+
onPeerDisconnected(handler) {
|
|
847
|
+
this.peerDisconnectedHandlers.push(handler);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Register a handler for incoming messages
|
|
851
|
+
*/
|
|
852
|
+
onMessage(handler) {
|
|
853
|
+
this.messageReceivedHandlers.push(handler);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Register a handler for incoming task delegation requests
|
|
857
|
+
* Only one handler can be registered at a time
|
|
858
|
+
* @param handler Function that receives a TaskRequest and returns a TaskResult
|
|
859
|
+
*/
|
|
860
|
+
onTaskReceived(handler) {
|
|
861
|
+
this.taskReceivedHandler = handler;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Register a handler for incoming context synchronization
|
|
865
|
+
* Multiple handlers can be registered
|
|
866
|
+
* @param handler Function that receives context and peerId
|
|
867
|
+
*/
|
|
868
|
+
onContextReceived(handler) {
|
|
869
|
+
this.contextReceivedHandlers.push(handler);
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Register a handler for incoming context requests
|
|
873
|
+
* Only one handler can be registered at a time
|
|
874
|
+
* @param handler Function that receives a query and returns FileChunk[]
|
|
875
|
+
*/
|
|
876
|
+
onContextRequested(handler) {
|
|
877
|
+
this.contextRequestedHandler = handler;
|
|
878
|
+
}
|
|
879
|
+
// ============================================================================
|
|
880
|
+
// Task Delegation
|
|
881
|
+
// ============================================================================
|
|
882
|
+
/**
|
|
883
|
+
* Delegate a task to a peer and wait for the result
|
|
884
|
+
* @param task The task request to delegate
|
|
885
|
+
* @param peerId Optional peer ID to send to (defaults to first peer)
|
|
886
|
+
* @returns Promise that resolves with the task result
|
|
887
|
+
* @throws Error if no peers are connected or task times out
|
|
888
|
+
*/
|
|
889
|
+
async delegateTask(task, peerId) {
|
|
890
|
+
if (!peerId) {
|
|
891
|
+
const peers = this.getPeers();
|
|
892
|
+
if (peers.length === 0) {
|
|
893
|
+
throw new Error("No peers connected to delegate task to");
|
|
894
|
+
}
|
|
895
|
+
peerId = peers[0].id;
|
|
896
|
+
}
|
|
897
|
+
if (!this.peers.has(peerId)) {
|
|
898
|
+
throw new Error(`Peer not found: ${peerId}`);
|
|
899
|
+
}
|
|
900
|
+
const timeout = task.timeout ?? this.config.taskTimeout ?? 3e5;
|
|
901
|
+
return new Promise((resolve, reject) => {
|
|
902
|
+
const message = createTaskDelegateMessage(this.config.instanceName, task);
|
|
903
|
+
const timeoutId = setTimeout(() => {
|
|
904
|
+
const pending = this.pendingTasks.get(task.id);
|
|
905
|
+
if (pending) {
|
|
906
|
+
this.pendingTasks.delete(task.id);
|
|
907
|
+
reject(new Error(`Task '${task.id}' timed out after ${timeout}ms`));
|
|
908
|
+
}
|
|
909
|
+
}, timeout);
|
|
910
|
+
this.pendingTasks.set(task.id, {
|
|
911
|
+
taskId: task.id,
|
|
912
|
+
peerId,
|
|
913
|
+
resolve,
|
|
914
|
+
reject,
|
|
915
|
+
timeoutId
|
|
916
|
+
});
|
|
917
|
+
this.sendToPeer(peerId, message).catch((error) => {
|
|
918
|
+
clearTimeout(timeoutId);
|
|
919
|
+
this.pendingTasks.delete(task.id);
|
|
920
|
+
reject(error);
|
|
921
|
+
});
|
|
922
|
+
logger2.debug({ taskId: task.id, peerId, timeout }, "Task delegated");
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
// ============================================================================
|
|
926
|
+
// Context Synchronization
|
|
927
|
+
// ============================================================================
|
|
928
|
+
/**
|
|
929
|
+
* Synchronize context with connected peers
|
|
930
|
+
* @param context Optional context to sync. If not provided, broadcasts to all peers
|
|
931
|
+
* @param peerId Optional peer ID to send to (defaults to all peers)
|
|
932
|
+
*/
|
|
933
|
+
async syncContext(context, peerId) {
|
|
934
|
+
const contextToSync = context ?? {};
|
|
935
|
+
const message = createContextSyncMessage(this.config.instanceName, contextToSync);
|
|
936
|
+
if (peerId) {
|
|
937
|
+
await this.sendToPeer(peerId, message);
|
|
938
|
+
logger2.debug({ peerId, messageId: message.id }, "Context synced to peer");
|
|
939
|
+
} else {
|
|
940
|
+
await this.broadcast(message);
|
|
941
|
+
logger2.debug({ peerCount: this.peers.size, messageId: message.id }, "Context synced to all peers");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Request context from a peer based on a query
|
|
946
|
+
* @param query Description of what context is being requested
|
|
947
|
+
* @param peerId Optional peer ID to request from (defaults to first peer)
|
|
948
|
+
* @param timeout Optional timeout in milliseconds (default: 30000)
|
|
949
|
+
* @returns Promise that resolves with FileChunk[] from the peer
|
|
950
|
+
* @throws Error if no peers are connected or request times out
|
|
951
|
+
*/
|
|
952
|
+
async requestContext(query, peerId, timeout = 3e4) {
|
|
953
|
+
if (!peerId) {
|
|
954
|
+
const peers = this.getPeers();
|
|
955
|
+
if (peers.length === 0) {
|
|
956
|
+
throw new Error("No peers connected to request context from");
|
|
957
|
+
}
|
|
958
|
+
peerId = peers[0].id;
|
|
959
|
+
}
|
|
960
|
+
if (!this.peers.has(peerId)) {
|
|
961
|
+
throw new Error(`Peer not found: ${peerId}`);
|
|
962
|
+
}
|
|
963
|
+
return new Promise((resolve, reject) => {
|
|
964
|
+
const message = createContextRequestMessage(this.config.instanceName, query);
|
|
965
|
+
const timeoutId = setTimeout(() => {
|
|
966
|
+
const pending = this.pendingContextRequests.get(message.id);
|
|
967
|
+
if (pending) {
|
|
968
|
+
this.pendingContextRequests.delete(message.id);
|
|
969
|
+
reject(new Error(`Context request timed out after ${timeout}ms`));
|
|
970
|
+
}
|
|
971
|
+
}, timeout);
|
|
972
|
+
this.pendingContextRequests.set(message.id, {
|
|
973
|
+
requestId: message.id,
|
|
974
|
+
peerId,
|
|
975
|
+
resolve,
|
|
976
|
+
reject,
|
|
977
|
+
timeoutId
|
|
978
|
+
});
|
|
979
|
+
this.sendToPeer(peerId, message).catch((error) => {
|
|
980
|
+
clearTimeout(timeoutId);
|
|
981
|
+
this.pendingContextRequests.delete(message.id);
|
|
982
|
+
reject(error);
|
|
983
|
+
});
|
|
984
|
+
logger2.debug({ requestId: message.id, peerId, query }, "Context requested");
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Start automatic context synchronization
|
|
989
|
+
* Uses interval from config.contextSharing.syncInterval (default: 5000ms)
|
|
990
|
+
* @param contextProvider Optional function that returns context to sync
|
|
991
|
+
*/
|
|
992
|
+
startAutoSync(contextProvider) {
|
|
993
|
+
this.stopAutoSync();
|
|
994
|
+
const interval = this.config.contextSharing?.syncInterval ?? 5e3;
|
|
995
|
+
this.autoSyncIntervalId = setInterval(async () => {
|
|
996
|
+
try {
|
|
997
|
+
const context = contextProvider ? await contextProvider() : void 0;
|
|
998
|
+
await this.syncContext(context);
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
logger2.error({ error: error.message }, "Auto-sync error");
|
|
1001
|
+
}
|
|
1002
|
+
}, interval);
|
|
1003
|
+
logger2.info({ interval }, "Auto-sync started");
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Stop automatic context synchronization
|
|
1007
|
+
*/
|
|
1008
|
+
stopAutoSync() {
|
|
1009
|
+
if (this.autoSyncIntervalId) {
|
|
1010
|
+
clearInterval(this.autoSyncIntervalId);
|
|
1011
|
+
this.autoSyncIntervalId = null;
|
|
1012
|
+
logger2.info("Auto-sync stopped");
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// ============================================================================
|
|
1016
|
+
// Private Methods - Server
|
|
1017
|
+
// ============================================================================
|
|
1018
|
+
/**
|
|
1019
|
+
* Start the WebSocket server
|
|
1020
|
+
*/
|
|
1021
|
+
async startServer() {
|
|
1022
|
+
const { listen } = this.config;
|
|
1023
|
+
if (!listen) {
|
|
1024
|
+
throw new Error("Listen configuration is required");
|
|
1025
|
+
}
|
|
1026
|
+
return new Promise((resolve, reject) => {
|
|
1027
|
+
const host = listen.host ?? "0.0.0.0";
|
|
1028
|
+
const port = listen.port;
|
|
1029
|
+
logger2.debug({ host, port }, "Starting WebSocket server");
|
|
1030
|
+
this.server = new WebSocketServer({ host, port });
|
|
1031
|
+
this.server.on("listening", () => {
|
|
1032
|
+
logger2.info({ host, port }, "WebSocket server listening");
|
|
1033
|
+
resolve();
|
|
1034
|
+
});
|
|
1035
|
+
this.server.on("error", (error) => {
|
|
1036
|
+
logger2.error({ error: error.message }, "WebSocket server error");
|
|
1037
|
+
reject(error);
|
|
1038
|
+
});
|
|
1039
|
+
this.server.on("connection", (ws, request) => {
|
|
1040
|
+
this.handleNewConnection(ws, request);
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Handle a new incoming connection
|
|
1046
|
+
*/
|
|
1047
|
+
handleNewConnection(ws, request) {
|
|
1048
|
+
const peerId = uuidv42();
|
|
1049
|
+
const peerInfo = {
|
|
1050
|
+
id: peerId,
|
|
1051
|
+
name: "client",
|
|
1052
|
+
// Will be updated when we receive peer info
|
|
1053
|
+
connectedAt: Date.now(),
|
|
1054
|
+
lastActivity: Date.now()
|
|
1055
|
+
};
|
|
1056
|
+
this.peers.set(peerId, {
|
|
1057
|
+
info: peerInfo,
|
|
1058
|
+
ws
|
|
1059
|
+
});
|
|
1060
|
+
logger2.info({ peerId, url: request.url }, "New peer connected");
|
|
1061
|
+
ws.on("message", (data) => {
|
|
1062
|
+
const messageString = data.toString();
|
|
1063
|
+
const result = safeDeserializeMessage(messageString);
|
|
1064
|
+
if (result.success) {
|
|
1065
|
+
peerInfo.lastActivity = Date.now();
|
|
1066
|
+
this.handleMessage(result.data, ws, peerId);
|
|
1067
|
+
} else {
|
|
1068
|
+
logger2.warn({ peerId, error: result.error.message }, "Invalid message received");
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
ws.on("close", (code, reason) => {
|
|
1072
|
+
logger2.info({ peerId, code, reason: reason.toString() }, "Peer disconnected");
|
|
1073
|
+
this.peers.delete(peerId);
|
|
1074
|
+
this.notifyPeerDisconnected(peerInfo);
|
|
1075
|
+
});
|
|
1076
|
+
ws.on("error", (error) => {
|
|
1077
|
+
logger2.error({ peerId, error: error.message }, "Peer connection error");
|
|
1078
|
+
});
|
|
1079
|
+
this.notifyPeerConnected(peerInfo);
|
|
1080
|
+
}
|
|
1081
|
+
// ============================================================================
|
|
1082
|
+
// Private Methods - Client
|
|
1083
|
+
// ============================================================================
|
|
1084
|
+
/**
|
|
1085
|
+
* Connect to a remote bridge as a client
|
|
1086
|
+
*/
|
|
1087
|
+
async connectToRemote() {
|
|
1088
|
+
const { connect } = this.config;
|
|
1089
|
+
if (!connect) {
|
|
1090
|
+
throw new Error("Connect configuration is required");
|
|
1091
|
+
}
|
|
1092
|
+
let url = connect.url;
|
|
1093
|
+
if (!url) {
|
|
1094
|
+
const host = connect.hostGateway ? "host.docker.internal" : "localhost";
|
|
1095
|
+
const port = connect.port ?? 8765;
|
|
1096
|
+
url = `ws://${host}:${port}`;
|
|
1097
|
+
}
|
|
1098
|
+
this.clientTransport = new WebSocketTransport();
|
|
1099
|
+
this.clientTransport.onMessage((message) => {
|
|
1100
|
+
this.handleMessage(message, this.clientTransport);
|
|
1101
|
+
});
|
|
1102
|
+
this.clientTransport.onDisconnect(() => {
|
|
1103
|
+
this.handleClientDisconnect(this.clientTransport);
|
|
1104
|
+
});
|
|
1105
|
+
try {
|
|
1106
|
+
await this.clientTransport.connect({
|
|
1107
|
+
url,
|
|
1108
|
+
reconnect: true,
|
|
1109
|
+
reconnectInterval: 1e3,
|
|
1110
|
+
maxReconnectAttempts: 10
|
|
1111
|
+
});
|
|
1112
|
+
const peerId = uuidv42();
|
|
1113
|
+
const peerInfo = {
|
|
1114
|
+
id: peerId,
|
|
1115
|
+
name: "server",
|
|
1116
|
+
connectedAt: Date.now(),
|
|
1117
|
+
lastActivity: Date.now()
|
|
1118
|
+
};
|
|
1119
|
+
this.peers.set(peerId, {
|
|
1120
|
+
info: peerInfo,
|
|
1121
|
+
transport: this.clientTransport
|
|
1122
|
+
});
|
|
1123
|
+
this.clientTransport._peerId = peerId;
|
|
1124
|
+
this.notifyPeerConnected(peerInfo);
|
|
1125
|
+
logger2.info({ peerId, url }, "Connected to remote bridge");
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
logger2.error({ error: error.message }, "Failed to connect to remote bridge");
|
|
1128
|
+
this.clientTransport = null;
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Handle client transport disconnect
|
|
1134
|
+
*/
|
|
1135
|
+
handleClientDisconnect(transport) {
|
|
1136
|
+
const typedTransport = transport;
|
|
1137
|
+
const peerId = typedTransport._peerId;
|
|
1138
|
+
if (peerId) {
|
|
1139
|
+
const peer = this.peers.get(peerId);
|
|
1140
|
+
if (peer) {
|
|
1141
|
+
this.peers.delete(peerId);
|
|
1142
|
+
this.notifyPeerDisconnected(peer.info);
|
|
1143
|
+
logger2.info({ peerId }, "Client transport disconnected");
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// ============================================================================
|
|
1148
|
+
// Private Methods - Message Handling
|
|
1149
|
+
// ============================================================================
|
|
1150
|
+
/**
|
|
1151
|
+
* Handle an incoming message
|
|
1152
|
+
*/
|
|
1153
|
+
handleMessage(message, source, peerId) {
|
|
1154
|
+
if (!peerId) {
|
|
1155
|
+
const typedSource = source;
|
|
1156
|
+
peerId = typedSource._peerId;
|
|
1157
|
+
}
|
|
1158
|
+
if (!peerId) {
|
|
1159
|
+
for (const [id, peer2] of this.peers) {
|
|
1160
|
+
if (peer2.ws === source || peer2.transport === source) {
|
|
1161
|
+
peerId = id;
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (!peerId) {
|
|
1167
|
+
logger2.warn({ messageId: message.id }, "Received message from unknown peer");
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const peer = this.peers.get(peerId);
|
|
1171
|
+
if (peer) {
|
|
1172
|
+
peer.info.lastActivity = Date.now();
|
|
1173
|
+
}
|
|
1174
|
+
logger2.debug({ peerId, messageId: message.id, type: message.type }, "Received message");
|
|
1175
|
+
if (message.type === "task_delegate" && message.task) {
|
|
1176
|
+
this.handleTaskDelegate(message, peerId);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (message.type === "response" && message.result?.taskId) {
|
|
1180
|
+
this.handleTaskResponse(message);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (message.type === "context_sync" && message.context) {
|
|
1184
|
+
this.handleContextSync(message, peerId);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (message.type === "request" && message.context?.summary) {
|
|
1188
|
+
this.handleContextRequest(message, peerId);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (message.type === "response" && message.context?.files !== void 0) {
|
|
1192
|
+
this.handleContextResponse(message);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
this.notifyMessageReceived(message, peerId);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Handle incoming task delegation request
|
|
1199
|
+
*/
|
|
1200
|
+
async handleTaskDelegate(message, peerId) {
|
|
1201
|
+
const task = message.task;
|
|
1202
|
+
logger2.debug({ taskId: task.id, peerId }, "Received task delegation");
|
|
1203
|
+
if (!this.taskReceivedHandler) {
|
|
1204
|
+
logger2.warn({ taskId: task.id }, "No task handler registered");
|
|
1205
|
+
const response = createTaskResponseMessage(
|
|
1206
|
+
this.config.instanceName,
|
|
1207
|
+
task.id,
|
|
1208
|
+
{
|
|
1209
|
+
success: false,
|
|
1210
|
+
data: null,
|
|
1211
|
+
error: "No task handler registered on peer"
|
|
1212
|
+
}
|
|
1213
|
+
);
|
|
1214
|
+
await this.sendToPeer(peerId, response).catch((err) => {
|
|
1215
|
+
logger2.error({ error: err.message, taskId: task.id }, "Failed to send error response");
|
|
1216
|
+
});
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
const result = await this.taskReceivedHandler(task, peerId);
|
|
1221
|
+
const response = createTaskResponseMessage(
|
|
1222
|
+
this.config.instanceName,
|
|
1223
|
+
task.id,
|
|
1224
|
+
result
|
|
1225
|
+
);
|
|
1226
|
+
await this.sendToPeer(peerId, response);
|
|
1227
|
+
logger2.debug({ taskId: task.id, success: result.success }, "Task response sent");
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
const response = createTaskResponseMessage(
|
|
1230
|
+
this.config.instanceName,
|
|
1231
|
+
task.id,
|
|
1232
|
+
{
|
|
1233
|
+
success: false,
|
|
1234
|
+
data: null,
|
|
1235
|
+
error: error.message
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
await this.sendToPeer(peerId, response).catch((err) => {
|
|
1239
|
+
logger2.error({ error: err.message, taskId: task.id }, "Failed to send error response");
|
|
1240
|
+
});
|
|
1241
|
+
logger2.error({ taskId: task.id, error: error.message }, "Task handler error");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Handle task response message (correlate with pending task)
|
|
1246
|
+
*/
|
|
1247
|
+
handleTaskResponse(message) {
|
|
1248
|
+
const taskId = message.result.taskId;
|
|
1249
|
+
const pending = this.pendingTasks.get(taskId);
|
|
1250
|
+
if (!pending) {
|
|
1251
|
+
logger2.warn({ taskId }, "Received response for unknown task");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
clearTimeout(pending.timeoutId);
|
|
1255
|
+
this.pendingTasks.delete(taskId);
|
|
1256
|
+
const result = {
|
|
1257
|
+
taskId: message.result.taskId,
|
|
1258
|
+
success: message.result.success,
|
|
1259
|
+
data: message.result.data,
|
|
1260
|
+
artifacts: message.result.artifacts,
|
|
1261
|
+
followUp: message.result.followUp,
|
|
1262
|
+
error: message.result.error
|
|
1263
|
+
};
|
|
1264
|
+
logger2.debug({ taskId, success: result.success }, "Task result received");
|
|
1265
|
+
pending.resolve(result);
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Handle incoming context sync message
|
|
1269
|
+
*/
|
|
1270
|
+
handleContextSync(message, peerId) {
|
|
1271
|
+
const context = message.context;
|
|
1272
|
+
logger2.debug({ peerId, messageId: message.id }, "Received context sync");
|
|
1273
|
+
this.notifyContextReceived(context, peerId);
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Handle incoming context request message
|
|
1277
|
+
*/
|
|
1278
|
+
async handleContextRequest(message, peerId) {
|
|
1279
|
+
const query = message.context.summary;
|
|
1280
|
+
logger2.debug({ peerId, messageId: message.id, query }, "Received context request");
|
|
1281
|
+
if (!this.contextRequestedHandler) {
|
|
1282
|
+
logger2.warn({ messageId: message.id }, "No context request handler registered");
|
|
1283
|
+
const response = createContextSyncMessage(this.config.instanceName, { files: [] });
|
|
1284
|
+
const responseMessage = {
|
|
1285
|
+
...response,
|
|
1286
|
+
type: "response",
|
|
1287
|
+
context: {
|
|
1288
|
+
...response.context,
|
|
1289
|
+
variables: { requestId: message.id }
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
await this.sendToPeer(peerId, responseMessage).catch((err) => {
|
|
1293
|
+
logger2.error({ error: err.message, messageId: message.id }, "Failed to send empty context response");
|
|
1294
|
+
});
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
const files = await this.contextRequestedHandler(query, peerId);
|
|
1299
|
+
const response = createContextSyncMessage(this.config.instanceName, { files });
|
|
1300
|
+
const responseMessage = {
|
|
1301
|
+
...response,
|
|
1302
|
+
type: "response",
|
|
1303
|
+
context: {
|
|
1304
|
+
...response.context,
|
|
1305
|
+
variables: { requestId: message.id }
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
await this.sendToPeer(peerId, responseMessage);
|
|
1309
|
+
logger2.debug({ messageId: message.id, fileCount: files.length }, "Context response sent");
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
const response = createContextSyncMessage(this.config.instanceName, {
|
|
1312
|
+
files: [],
|
|
1313
|
+
summary: error.message
|
|
1314
|
+
});
|
|
1315
|
+
const responseMessage = {
|
|
1316
|
+
...response,
|
|
1317
|
+
type: "response",
|
|
1318
|
+
context: {
|
|
1319
|
+
...response.context,
|
|
1320
|
+
variables: { requestId: message.id, error: error.message }
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
await this.sendToPeer(peerId, responseMessage).catch((err) => {
|
|
1324
|
+
logger2.error({ error: err.message, messageId: message.id }, "Failed to send error context response");
|
|
1325
|
+
});
|
|
1326
|
+
logger2.error({ messageId: message.id, error: error.message }, "Context request handler error");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Handle context response message (correlate with pending context request)
|
|
1331
|
+
*/
|
|
1332
|
+
handleContextResponse(message) {
|
|
1333
|
+
const requestId = message.context?.variables?.requestId;
|
|
1334
|
+
if (!requestId) {
|
|
1335
|
+
logger2.warn({ messageId: message.id }, "Context response without requestId");
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const pending = this.pendingContextRequests.get(requestId);
|
|
1339
|
+
if (!pending) {
|
|
1340
|
+
logger2.warn({ requestId }, "Received response for unknown context request");
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
clearTimeout(pending.timeoutId);
|
|
1344
|
+
this.pendingContextRequests.delete(requestId);
|
|
1345
|
+
const error = message.context?.variables?.error;
|
|
1346
|
+
if (error) {
|
|
1347
|
+
pending.reject(new Error(error));
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const files = message.context?.files ?? [];
|
|
1351
|
+
logger2.debug({ requestId, fileCount: files.length }, "Context response received");
|
|
1352
|
+
pending.resolve(files);
|
|
1353
|
+
}
|
|
1354
|
+
// ============================================================================
|
|
1355
|
+
// Private Methods - Event Notification
|
|
1356
|
+
// ============================================================================
|
|
1357
|
+
notifyPeerConnected(peer) {
|
|
1358
|
+
for (const handler of this.peerConnectedHandlers) {
|
|
1359
|
+
try {
|
|
1360
|
+
handler(peer);
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
logger2.error({ error: error.message }, "Peer connected handler error");
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
notifyPeerDisconnected(peer) {
|
|
1367
|
+
for (const [taskId, pending] of this.pendingTasks) {
|
|
1368
|
+
if (pending.peerId === peer.id) {
|
|
1369
|
+
clearTimeout(pending.timeoutId);
|
|
1370
|
+
this.pendingTasks.delete(taskId);
|
|
1371
|
+
pending.reject(new Error(`Peer '${peer.id}' disconnected while task '${taskId}' was pending`));
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
for (const [requestId, pending] of this.pendingContextRequests) {
|
|
1375
|
+
if (pending.peerId === peer.id) {
|
|
1376
|
+
clearTimeout(pending.timeoutId);
|
|
1377
|
+
this.pendingContextRequests.delete(requestId);
|
|
1378
|
+
pending.reject(new Error(`Peer '${peer.id}' disconnected while context request '${requestId}' was pending`));
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
for (const handler of this.peerDisconnectedHandlers) {
|
|
1382
|
+
try {
|
|
1383
|
+
handler(peer);
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
logger2.error({ error: error.message }, "Peer disconnected handler error");
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
notifyMessageReceived(message, peerId) {
|
|
1390
|
+
for (const handler of this.messageReceivedHandlers) {
|
|
1391
|
+
try {
|
|
1392
|
+
handler(message, peerId);
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
logger2.error({ error: error.message }, "Message received handler error");
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
notifyContextReceived(context, peerId) {
|
|
1399
|
+
for (const handler of this.contextReceivedHandlers) {
|
|
1400
|
+
try {
|
|
1401
|
+
handler(context, peerId);
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
logger2.error({ error: error.message }, "Context received handler error");
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
// ============================================================================
|
|
1408
|
+
// Private Methods - Cleanup
|
|
1409
|
+
// ============================================================================
|
|
1410
|
+
/**
|
|
1411
|
+
* Clean up all resources
|
|
1412
|
+
*/
|
|
1413
|
+
async cleanup() {
|
|
1414
|
+
logger2.debug("Starting cleanup");
|
|
1415
|
+
this.stopAutoSync();
|
|
1416
|
+
logger2.debug("Auto-sync stopped");
|
|
1417
|
+
const pendingTaskCount = this.pendingTasks.size;
|
|
1418
|
+
for (const [taskId, pending] of this.pendingTasks) {
|
|
1419
|
+
clearTimeout(pending.timeoutId);
|
|
1420
|
+
pending.reject(new Error("Bridge is shutting down"));
|
|
1421
|
+
}
|
|
1422
|
+
this.pendingTasks.clear();
|
|
1423
|
+
if (pendingTaskCount > 0) {
|
|
1424
|
+
logger2.debug({ count: pendingTaskCount }, "Pending tasks cancelled");
|
|
1425
|
+
}
|
|
1426
|
+
const pendingContextCount = this.pendingContextRequests.size;
|
|
1427
|
+
for (const [requestId, pending] of this.pendingContextRequests) {
|
|
1428
|
+
clearTimeout(pending.timeoutId);
|
|
1429
|
+
pending.reject(new Error("Bridge is shutting down"));
|
|
1430
|
+
}
|
|
1431
|
+
this.pendingContextRequests.clear();
|
|
1432
|
+
if (pendingContextCount > 0) {
|
|
1433
|
+
logger2.debug({ count: pendingContextCount }, "Pending context requests cancelled");
|
|
1434
|
+
}
|
|
1435
|
+
if (this.clientTransport) {
|
|
1436
|
+
logger2.debug("Disconnecting client transport");
|
|
1437
|
+
try {
|
|
1438
|
+
await this.clientTransport.disconnect();
|
|
1439
|
+
logger2.debug("Client transport disconnected");
|
|
1440
|
+
} catch {
|
|
1441
|
+
}
|
|
1442
|
+
this.clientTransport = null;
|
|
1443
|
+
}
|
|
1444
|
+
const peerCount = this.peers.size;
|
|
1445
|
+
if (peerCount > 0) {
|
|
1446
|
+
logger2.debug({ count: peerCount }, "Closing peer connections");
|
|
1447
|
+
}
|
|
1448
|
+
for (const [peerId, peer] of this.peers) {
|
|
1449
|
+
if (peer.ws) {
|
|
1450
|
+
try {
|
|
1451
|
+
peer.ws.close(1e3, "Bridge stopping");
|
|
1452
|
+
} catch {
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (peer.transport) {
|
|
1456
|
+
try {
|
|
1457
|
+
await peer.transport.disconnect();
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
logger2.debug({ peerId }, "Peer disconnected");
|
|
1462
|
+
}
|
|
1463
|
+
this.peers.clear();
|
|
1464
|
+
if (this.server) {
|
|
1465
|
+
logger2.debug("Closing WebSocket server");
|
|
1466
|
+
await new Promise((resolve) => {
|
|
1467
|
+
this.server.close(() => {
|
|
1468
|
+
resolve();
|
|
1469
|
+
});
|
|
1470
|
+
});
|
|
1471
|
+
this.server = null;
|
|
1472
|
+
logger2.debug("WebSocket server closed");
|
|
1473
|
+
}
|
|
1474
|
+
logger2.debug("Cleanup complete");
|
|
1475
|
+
}
|
|
1476
|
+
// ============================================================================
|
|
1477
|
+
// Getters for State Inspection
|
|
1478
|
+
// ============================================================================
|
|
1479
|
+
/**
|
|
1480
|
+
* Check if the bridge is started
|
|
1481
|
+
*/
|
|
1482
|
+
isStarted() {
|
|
1483
|
+
return this.started;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Get the instance name
|
|
1487
|
+
*/
|
|
1488
|
+
getInstanceName() {
|
|
1489
|
+
return this.config.instanceName;
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Get the operation mode
|
|
1493
|
+
*/
|
|
1494
|
+
getMode() {
|
|
1495
|
+
return this.config.mode;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Get the number of connected peers
|
|
1499
|
+
*/
|
|
1500
|
+
getPeerCount() {
|
|
1501
|
+
return this.peers.size;
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
// src/transport/discovery.ts
|
|
1506
|
+
import { execSync } from "child_process";
|
|
1507
|
+
var logger3 = createLogger("transport:discovery");
|
|
1508
|
+
function parseDockerLabels(labels) {
|
|
1509
|
+
if (!labels) {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
const labelMap = {};
|
|
1513
|
+
for (const label of labels.split(",")) {
|
|
1514
|
+
const [key, value] = label.split("=");
|
|
1515
|
+
if (key && value) {
|
|
1516
|
+
labelMap[key.trim()] = value.trim();
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (labelMap["claude.bridge.enabled"] !== "true") {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
return {
|
|
1523
|
+
enabled: true,
|
|
1524
|
+
port: labelMap["claude.bridge.port"] ? parseInt(labelMap["claude.bridge.port"], 10) : void 0,
|
|
1525
|
+
name: labelMap["claude.bridge.name"]
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function isDockerAvailable() {
|
|
1529
|
+
try {
|
|
1530
|
+
execSync("docker info", { stdio: "pipe", timeout: 5e3 });
|
|
1531
|
+
return true;
|
|
1532
|
+
} catch {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function discoverDockerPeers() {
|
|
1537
|
+
const peers = [];
|
|
1538
|
+
if (!isDockerAvailable()) {
|
|
1539
|
+
logger3.debug("Docker is not available");
|
|
1540
|
+
return peers;
|
|
1541
|
+
}
|
|
1542
|
+
try {
|
|
1543
|
+
const output = execSync(
|
|
1544
|
+
'docker ps --format "{{.ID}}|{{.Names}}|{{.Labels}}|{{.Status}}|{{.Ports}}"',
|
|
1545
|
+
{
|
|
1546
|
+
encoding: "utf-8",
|
|
1547
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1548
|
+
timeout: 1e4
|
|
1549
|
+
}
|
|
1550
|
+
);
|
|
1551
|
+
const lines = output.trim().split("\n").filter(Boolean);
|
|
1552
|
+
for (const line of lines) {
|
|
1553
|
+
const [id, names, labels, status, ports] = line.split("|");
|
|
1554
|
+
if (!id || !names) continue;
|
|
1555
|
+
const bridgeConfig = parseDockerLabels(labels);
|
|
1556
|
+
if (!bridgeConfig) continue;
|
|
1557
|
+
let port = bridgeConfig.port;
|
|
1558
|
+
if (!port && ports) {
|
|
1559
|
+
const portMatch = ports.match(/0\.0\.0\.0:(\d+)/);
|
|
1560
|
+
if (portMatch) {
|
|
1561
|
+
port = parseInt(portMatch[1], 10);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
if (port) {
|
|
1565
|
+
peers.push({
|
|
1566
|
+
name: bridgeConfig.name || names,
|
|
1567
|
+
source: "docker",
|
|
1568
|
+
url: `ws://localhost:${port}`,
|
|
1569
|
+
containerId: id,
|
|
1570
|
+
status
|
|
1571
|
+
});
|
|
1572
|
+
logger3.debug(
|
|
1573
|
+
{ containerId: id, name: bridgeConfig.name || names, port },
|
|
1574
|
+
"Found bridge-enabled container"
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
logger3.debug(
|
|
1580
|
+
{ error: error.message },
|
|
1581
|
+
"Failed to discover Docker peers"
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
return peers;
|
|
1585
|
+
}
|
|
1586
|
+
function discoverDocksalProjects() {
|
|
1587
|
+
const peers = [];
|
|
1588
|
+
try {
|
|
1589
|
+
execSync("which fin", { stdio: "pipe" });
|
|
1590
|
+
const output = execSync('fin project list 2>/dev/null || echo ""', {
|
|
1591
|
+
encoding: "utf-8",
|
|
1592
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1593
|
+
timeout: 1e4
|
|
1594
|
+
});
|
|
1595
|
+
const lines = output.trim().split("\n").filter(Boolean);
|
|
1596
|
+
for (const line of lines) {
|
|
1597
|
+
if (line.includes("NAME") || line.startsWith("-")) continue;
|
|
1598
|
+
const parts = line.trim().split(/\s+/);
|
|
1599
|
+
const projectName = parts[0];
|
|
1600
|
+
const status = parts.slice(1).join(" ");
|
|
1601
|
+
if (projectName && status.toLowerCase().includes("running")) {
|
|
1602
|
+
peers.push({
|
|
1603
|
+
name: projectName,
|
|
1604
|
+
source: "docksal",
|
|
1605
|
+
url: `ws://${projectName}.docksal:8765`,
|
|
1606
|
+
status: "running"
|
|
1607
|
+
});
|
|
1608
|
+
logger3.debug({ projectName }, "Found Docksal project");
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
logger3.debug(
|
|
1613
|
+
{ error: error.message },
|
|
1614
|
+
"Docksal not available or no projects found"
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
return peers;
|
|
1618
|
+
}
|
|
1619
|
+
function discoverDdevProjects() {
|
|
1620
|
+
const peers = [];
|
|
1621
|
+
try {
|
|
1622
|
+
execSync("which ddev", { stdio: "pipe" });
|
|
1623
|
+
const output = execSync('ddev list --json-output 2>/dev/null || echo "[]"', {
|
|
1624
|
+
encoding: "utf-8",
|
|
1625
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1626
|
+
timeout: 1e4
|
|
1627
|
+
});
|
|
1628
|
+
try {
|
|
1629
|
+
const data = JSON.parse(output);
|
|
1630
|
+
const projects = data.raw || [];
|
|
1631
|
+
for (const project of projects) {
|
|
1632
|
+
if (project.status === "running") {
|
|
1633
|
+
peers.push({
|
|
1634
|
+
name: project.name,
|
|
1635
|
+
source: "ddev",
|
|
1636
|
+
url: `ws://${project.name}.ddev.site:8765`,
|
|
1637
|
+
status: "running"
|
|
1638
|
+
});
|
|
1639
|
+
logger3.debug({ projectName: project.name }, "Found DDEV project");
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
} catch {
|
|
1643
|
+
logger3.debug("Failed to parse DDEV JSON output, trying text parsing");
|
|
1644
|
+
const lines = output.trim().split("\n").filter(Boolean);
|
|
1645
|
+
for (const line of lines) {
|
|
1646
|
+
if (line.includes("running")) {
|
|
1647
|
+
const parts = line.trim().split(/\s+/);
|
|
1648
|
+
if (parts[0]) {
|
|
1649
|
+
peers.push({
|
|
1650
|
+
name: parts[0],
|
|
1651
|
+
source: "ddev",
|
|
1652
|
+
url: `ws://${parts[0]}.ddev.site:8765`,
|
|
1653
|
+
status: "running"
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
logger3.debug(
|
|
1661
|
+
{ error: error.message },
|
|
1662
|
+
"DDEV not available or no projects found"
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
return peers;
|
|
1666
|
+
}
|
|
1667
|
+
function discoverLandoProjects() {
|
|
1668
|
+
const peers = [];
|
|
1669
|
+
try {
|
|
1670
|
+
execSync("which lando", { stdio: "pipe" });
|
|
1671
|
+
const output = execSync('lando list --format json 2>/dev/null || echo "[]"', {
|
|
1672
|
+
encoding: "utf-8",
|
|
1673
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1674
|
+
timeout: 1e4
|
|
1675
|
+
});
|
|
1676
|
+
try {
|
|
1677
|
+
const apps = JSON.parse(output);
|
|
1678
|
+
for (const app of apps) {
|
|
1679
|
+
if (app.running) {
|
|
1680
|
+
peers.push({
|
|
1681
|
+
name: app.app || app.name,
|
|
1682
|
+
source: "lando",
|
|
1683
|
+
url: `ws://localhost:8765`,
|
|
1684
|
+
// Would need to check app config for actual port
|
|
1685
|
+
status: "running"
|
|
1686
|
+
});
|
|
1687
|
+
logger3.debug({ appName: app.app || app.name }, "Found Lando app");
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
} catch {
|
|
1691
|
+
logger3.debug("Failed to parse Lando JSON output");
|
|
1692
|
+
}
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
logger3.debug(
|
|
1695
|
+
{ error: error.message },
|
|
1696
|
+
"Lando not available or no projects found"
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
return peers;
|
|
1700
|
+
}
|
|
1701
|
+
function discoverAllPeers() {
|
|
1702
|
+
const allPeers = [];
|
|
1703
|
+
const dockerPeers = discoverDockerPeers();
|
|
1704
|
+
allPeers.push(...dockerPeers);
|
|
1705
|
+
logger3.debug({ count: dockerPeers.length }, "Docker peers discovered");
|
|
1706
|
+
const docksalPeers = discoverDocksalProjects();
|
|
1707
|
+
allPeers.push(...docksalPeers);
|
|
1708
|
+
logger3.debug({ count: docksalPeers.length }, "Docksal peers discovered");
|
|
1709
|
+
const ddevPeers = discoverDdevProjects();
|
|
1710
|
+
allPeers.push(...ddevPeers);
|
|
1711
|
+
logger3.debug({ count: ddevPeers.length }, "DDEV peers discovered");
|
|
1712
|
+
const landoPeers = discoverLandoProjects();
|
|
1713
|
+
allPeers.push(...landoPeers);
|
|
1714
|
+
logger3.debug({ count: landoPeers.length }, "Lando peers discovered");
|
|
1715
|
+
return allPeers;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/environment/detect.ts
|
|
1719
|
+
import { existsSync } from "fs";
|
|
1720
|
+
function isInDocker() {
|
|
1721
|
+
return existsSync("/.dockerenv");
|
|
1722
|
+
}
|
|
1723
|
+
function isDocksalEnvironment() {
|
|
1724
|
+
return !!process.env.DOCKSAL_STACK;
|
|
1725
|
+
}
|
|
1726
|
+
function isDdevEnvironment() {
|
|
1727
|
+
return process.env.IS_DDEV_PROJECT === "true";
|
|
1728
|
+
}
|
|
1729
|
+
function isLandoEnvironment() {
|
|
1730
|
+
return process.env.LANDO === "ON";
|
|
1731
|
+
}
|
|
1732
|
+
function isDockerComposeEnvironment() {
|
|
1733
|
+
return !!process.env.COMPOSE_PROJECT_NAME;
|
|
1734
|
+
}
|
|
1735
|
+
function detectEnvironment() {
|
|
1736
|
+
const platform = process.platform;
|
|
1737
|
+
const inDocker = isInDocker();
|
|
1738
|
+
if (isDocksalEnvironment()) {
|
|
1739
|
+
return {
|
|
1740
|
+
type: "docksal",
|
|
1741
|
+
isContainer: true,
|
|
1742
|
+
projectName: process.env.DOCKSAL_PROJECT,
|
|
1743
|
+
projectRoot: process.env.PROJECT_ROOT,
|
|
1744
|
+
hostGateway: getHostGateway(),
|
|
1745
|
+
platform,
|
|
1746
|
+
meta: {
|
|
1747
|
+
stack: process.env.DOCKSAL_STACK || "",
|
|
1748
|
+
environment: process.env.DOCKSAL_ENVIRONMENT || ""
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
if (isDdevEnvironment()) {
|
|
1753
|
+
return {
|
|
1754
|
+
type: "ddev",
|
|
1755
|
+
isContainer: true,
|
|
1756
|
+
projectName: process.env.DDEV_PROJECT,
|
|
1757
|
+
projectRoot: process.env.DDEV_DOCROOT,
|
|
1758
|
+
hostGateway: getHostGateway(),
|
|
1759
|
+
platform,
|
|
1760
|
+
meta: {
|
|
1761
|
+
hostname: process.env.DDEV_HOSTNAME || "",
|
|
1762
|
+
primaryUrl: process.env.DDEV_PRIMARY_URL || ""
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
if (isLandoEnvironment()) {
|
|
1767
|
+
return {
|
|
1768
|
+
type: "lando",
|
|
1769
|
+
isContainer: true,
|
|
1770
|
+
projectName: process.env.LANDO_APP_NAME,
|
|
1771
|
+
projectRoot: process.env.LANDO_APP_ROOT,
|
|
1772
|
+
hostGateway: getHostGateway(),
|
|
1773
|
+
platform,
|
|
1774
|
+
meta: {
|
|
1775
|
+
service: process.env.LANDO_SERVICE_NAME || "",
|
|
1776
|
+
type: process.env.LANDO_SERVICE_TYPE || ""
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
if (isDockerComposeEnvironment() && inDocker) {
|
|
1781
|
+
return {
|
|
1782
|
+
type: "docker-compose",
|
|
1783
|
+
isContainer: true,
|
|
1784
|
+
projectName: process.env.COMPOSE_PROJECT_NAME,
|
|
1785
|
+
hostGateway: getHostGateway(),
|
|
1786
|
+
platform,
|
|
1787
|
+
meta: {
|
|
1788
|
+
service: process.env.COMPOSE_SERVICE || ""
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
if (inDocker) {
|
|
1793
|
+
return {
|
|
1794
|
+
type: "docker",
|
|
1795
|
+
isContainer: true,
|
|
1796
|
+
hostGateway: getHostGateway(),
|
|
1797
|
+
platform
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
return {
|
|
1801
|
+
type: "native",
|
|
1802
|
+
isContainer: false,
|
|
1803
|
+
platform
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
function getHostGateway() {
|
|
1807
|
+
if (process.env.DOCKER_HOST_GATEWAY) {
|
|
1808
|
+
return process.env.DOCKER_HOST_GATEWAY;
|
|
1809
|
+
}
|
|
1810
|
+
if (process.platform === "darwin" || process.platform === "win32") {
|
|
1811
|
+
return "host.docker.internal";
|
|
1812
|
+
}
|
|
1813
|
+
if (isInDocker()) {
|
|
1814
|
+
return "host.docker.internal";
|
|
1815
|
+
}
|
|
1816
|
+
return "localhost";
|
|
1817
|
+
}
|
|
1818
|
+
function getDefaultConfig(env) {
|
|
1819
|
+
const baseConfig = {
|
|
1820
|
+
mode: "peer",
|
|
1821
|
+
instanceName: env.projectName || `${env.type}-instance`
|
|
1822
|
+
};
|
|
1823
|
+
if (env.isContainer) {
|
|
1824
|
+
return {
|
|
1825
|
+
...baseConfig,
|
|
1826
|
+
listen: {
|
|
1827
|
+
port: 8765,
|
|
1828
|
+
host: "0.0.0.0"
|
|
1829
|
+
},
|
|
1830
|
+
connect: {
|
|
1831
|
+
url: `ws://${env.hostGateway || "host.docker.internal"}:8766`,
|
|
1832
|
+
hostGateway: true
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
return {
|
|
1837
|
+
...baseConfig,
|
|
1838
|
+
listen: {
|
|
1839
|
+
port: 8766,
|
|
1840
|
+
host: "0.0.0.0"
|
|
1841
|
+
},
|
|
1842
|
+
connect: {
|
|
1843
|
+
url: "ws://localhost:8765",
|
|
1844
|
+
hostGateway: false
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// src/utils/config.ts
|
|
1850
|
+
import * as fs from "fs";
|
|
1851
|
+
import * as path from "path";
|
|
1852
|
+
import * as os from "os";
|
|
1853
|
+
import { parse as parseYaml } from "yaml";
|
|
1854
|
+
var DEFAULT_CONFIG = {
|
|
1855
|
+
listen: {
|
|
1856
|
+
port: 8765,
|
|
1857
|
+
host: "0.0.0.0"
|
|
1858
|
+
},
|
|
1859
|
+
contextSharing: {
|
|
1860
|
+
autoSync: true,
|
|
1861
|
+
syncInterval: 5e3,
|
|
1862
|
+
maxChunkTokens: 4e3,
|
|
1863
|
+
includePatterns: ["src/**/*.ts", "src/**/*.tsx", "*.json"],
|
|
1864
|
+
excludePatterns: ["node_modules/**", "dist/**", ".git/**"]
|
|
1865
|
+
},
|
|
1866
|
+
interaction: {
|
|
1867
|
+
requireConfirmation: false,
|
|
1868
|
+
notifyOnActivity: true,
|
|
1869
|
+
taskTimeout: 3e5
|
|
1870
|
+
// 5 minutes
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
function mergeConfig(partial) {
|
|
1874
|
+
return {
|
|
1875
|
+
...DEFAULT_CONFIG,
|
|
1876
|
+
...partial,
|
|
1877
|
+
listen: {
|
|
1878
|
+
...DEFAULT_CONFIG.listen,
|
|
1879
|
+
...partial.listen ?? {}
|
|
1880
|
+
},
|
|
1881
|
+
connect: partial.connect ? {
|
|
1882
|
+
...partial.connect
|
|
1883
|
+
} : void 0,
|
|
1884
|
+
contextSharing: {
|
|
1885
|
+
...DEFAULT_CONFIG.contextSharing,
|
|
1886
|
+
...partial.contextSharing ?? {}
|
|
1887
|
+
},
|
|
1888
|
+
interaction: {
|
|
1889
|
+
...DEFAULT_CONFIG.interaction,
|
|
1890
|
+
...partial.interaction ?? {}
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
function readConfigFile(filePath) {
|
|
1895
|
+
try {
|
|
1896
|
+
if (!fs.existsSync(filePath)) {
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1900
|
+
const parsed = parseYaml(content);
|
|
1901
|
+
return parsed;
|
|
1902
|
+
} catch {
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
function getDefaultConfigPaths() {
|
|
1907
|
+
const homeDir = os.homedir();
|
|
1908
|
+
const cwd = process.cwd();
|
|
1909
|
+
return [
|
|
1910
|
+
// Project-local config takes priority
|
|
1911
|
+
path.join(cwd, ".claude-bridge.yml"),
|
|
1912
|
+
path.join(cwd, ".claude-bridge.yaml"),
|
|
1913
|
+
// User home config as fallback
|
|
1914
|
+
path.join(homeDir, ".claude-bridge", "config.yml"),
|
|
1915
|
+
path.join(homeDir, ".claude-bridge", "config.yaml")
|
|
1916
|
+
];
|
|
1917
|
+
}
|
|
1918
|
+
async function loadConfig(configPath) {
|
|
1919
|
+
if (configPath) {
|
|
1920
|
+
const parsed = readConfigFile(configPath);
|
|
1921
|
+
if (parsed) {
|
|
1922
|
+
return mergeConfig(parsed);
|
|
1923
|
+
}
|
|
1924
|
+
return { ...DEFAULT_CONFIG };
|
|
1925
|
+
}
|
|
1926
|
+
const searchPaths = getDefaultConfigPaths();
|
|
1927
|
+
for (const searchPath of searchPaths) {
|
|
1928
|
+
const parsed = readConfigFile(searchPath);
|
|
1929
|
+
if (parsed) {
|
|
1930
|
+
return mergeConfig(parsed);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
return { ...DEFAULT_CONFIG };
|
|
1934
|
+
}
|
|
1935
|
+
function loadConfigSync(configPath) {
|
|
1936
|
+
if (configPath) {
|
|
1937
|
+
const parsed = readConfigFile(configPath);
|
|
1938
|
+
if (parsed) {
|
|
1939
|
+
return mergeConfig(parsed);
|
|
1940
|
+
}
|
|
1941
|
+
return { ...DEFAULT_CONFIG };
|
|
1942
|
+
}
|
|
1943
|
+
const searchPaths = getDefaultConfigPaths();
|
|
1944
|
+
for (const searchPath of searchPaths) {
|
|
1945
|
+
const parsed = readConfigFile(searchPath);
|
|
1946
|
+
if (parsed) {
|
|
1947
|
+
return mergeConfig(parsed);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return { ...DEFAULT_CONFIG };
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
export {
|
|
1954
|
+
createLogger,
|
|
1955
|
+
createChildLogger,
|
|
1956
|
+
MessageType,
|
|
1957
|
+
FileChunkSchema,
|
|
1958
|
+
DirectoryTreeSchema,
|
|
1959
|
+
ArtifactSchema,
|
|
1960
|
+
ContextSchema,
|
|
1961
|
+
TaskRequestSchema,
|
|
1962
|
+
TaskResultSchema,
|
|
1963
|
+
BridgeMessageSchema,
|
|
1964
|
+
createMessage,
|
|
1965
|
+
validateMessage,
|
|
1966
|
+
safeValidateMessage,
|
|
1967
|
+
serializeMessage,
|
|
1968
|
+
deserializeMessage,
|
|
1969
|
+
safeDeserializeMessage,
|
|
1970
|
+
ConnectionState,
|
|
1971
|
+
WebSocketTransport,
|
|
1972
|
+
createContextSyncMessage,
|
|
1973
|
+
createTaskDelegateMessage,
|
|
1974
|
+
createTaskResponseMessage,
|
|
1975
|
+
createContextRequestMessage,
|
|
1976
|
+
createNotificationMessage,
|
|
1977
|
+
Bridge,
|
|
1978
|
+
parseDockerLabels,
|
|
1979
|
+
isDockerAvailable,
|
|
1980
|
+
discoverDockerPeers,
|
|
1981
|
+
discoverDocksalProjects,
|
|
1982
|
+
discoverDdevProjects,
|
|
1983
|
+
discoverLandoProjects,
|
|
1984
|
+
discoverAllPeers,
|
|
1985
|
+
detectEnvironment,
|
|
1986
|
+
getHostGateway,
|
|
1987
|
+
getDefaultConfig,
|
|
1988
|
+
DEFAULT_CONFIG,
|
|
1989
|
+
mergeConfig,
|
|
1990
|
+
loadConfig,
|
|
1991
|
+
loadConfigSync
|
|
1992
|
+
};
|
|
1993
|
+
//# sourceMappingURL=chunk-BRH476VK.js.map
|