@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.
@@ -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