@ebowwa/coder 0.7.64 → 0.7.66

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.
Files changed (101) hide show
  1. package/dist/index.js +36233 -32
  2. package/dist/interfaces/ui/terminal/cli/index.js +34318 -158
  3. package/dist/interfaces/ui/terminal/native/README.md +53 -0
  4. package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
  5. package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
  6. package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
  7. package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
  8. package/dist/interfaces/ui/terminal/native/index.js +43 -0
  9. package/dist/interfaces/ui/terminal/native/index.node +0 -0
  10. package/dist/interfaces/ui/terminal/native/package.json +34 -0
  11. package/dist/native/README.md +53 -0
  12. package/dist/native/claude_code_native.darwin-x64.node +0 -0
  13. package/dist/native/claude_code_native.dylib +0 -0
  14. package/dist/native/index.d.ts +0 -480
  15. package/dist/native/index.darwin-arm64.node +0 -0
  16. package/dist/native/index.js +43 -1625
  17. package/dist/native/index.node +0 -0
  18. package/dist/native/package.json +34 -0
  19. package/native/index.darwin-arm64.node +0 -0
  20. package/native/index.js +33 -19
  21. package/package.json +3 -2
  22. package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
  23. package/packages/src/core/agent-loop/compaction.ts +6 -2
  24. package/packages/src/core/agent-loop/index.ts +2 -0
  25. package/packages/src/core/agent-loop/loop-state.ts +1 -1
  26. package/packages/src/core/agent-loop/turn-executor.ts +4 -0
  27. package/packages/src/core/agent-loop/types.ts +4 -0
  28. package/packages/src/core/api-client-impl.ts +377 -176
  29. package/packages/src/core/cognitive-security/hooks.ts +2 -1
  30. package/packages/src/core/config/todo +7 -0
  31. package/packages/src/core/context/__tests__/integration.test.ts +334 -0
  32. package/packages/src/core/context/compaction.ts +170 -0
  33. package/packages/src/core/context/constants.ts +58 -0
  34. package/packages/src/core/context/extraction.ts +85 -0
  35. package/packages/src/core/context/index.ts +66 -0
  36. package/packages/src/core/context/summarization.ts +251 -0
  37. package/packages/src/core/context/token-estimation.ts +98 -0
  38. package/packages/src/core/context/types.ts +59 -0
  39. package/packages/src/core/models.ts +81 -4
  40. package/packages/src/core/normalizers/todo +5 -1
  41. package/packages/src/core/providers/README.md +230 -0
  42. package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
  43. package/packages/src/core/providers/index.ts +419 -0
  44. package/packages/src/core/providers/types.ts +132 -0
  45. package/packages/src/core/retry.ts +10 -0
  46. package/packages/src/ecosystem/tools/index.ts +174 -0
  47. package/packages/src/index.ts +23 -2
  48. package/packages/src/interfaces/ui/index.ts +17 -20
  49. package/packages/src/interfaces/ui/spinner.ts +2 -2
  50. package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
  51. package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
  52. package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
  53. package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
  54. package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
  55. package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
  56. package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
  57. package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
  58. package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +402 -0
  59. package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
  60. package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
  61. package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
  62. package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
  63. package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
  64. package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
  65. package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
  66. package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
  67. package/packages/src/native/index.ts +404 -27
  68. package/packages/src/native/tui_v2_types.ts +39 -0
  69. package/packages/src/teammates/coordination.test.ts +279 -0
  70. package/packages/src/teammates/coordination.ts +646 -0
  71. package/packages/src/teammates/index.ts +95 -25
  72. package/packages/src/teammates/integration.test.ts +272 -0
  73. package/packages/src/teammates/runner.test.ts +235 -0
  74. package/packages/src/teammates/runner.ts +750 -0
  75. package/packages/src/teammates/schemas.ts +673 -0
  76. package/packages/src/types/index.ts +1 -0
  77. package/packages/src/core/context-compaction.ts +0 -578
  78. package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
  79. package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
  80. package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
  81. package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
  82. package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
  83. package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
  84. package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
  85. package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
  86. package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
  87. package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
  88. package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
  89. package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
  90. package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
  91. package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
  92. package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
  93. package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
  94. package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
  95. package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
  96. package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
  97. package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
  98. package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
  99. package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
  100. package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
  101. package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
@@ -0,0 +1,829 @@
1
+ /**
2
+ * TUI Bridge IPC Server
3
+ *
4
+ * Provides IPC server for external control via TUI Bridge MCP.
5
+ * Supports:
6
+ * 1. Unix socket transport (primary)
7
+ * 2. HTTP transport (secondary, optional port)
8
+ *
9
+ * Uses JSON-RPC 2.0 protocol for communication.
10
+ */
11
+
12
+ import { EventEmitter } from "events";
13
+ import { z } from "zod";
14
+ import type { TUIBridge } from "./index.js";
15
+ import type { BridgeEvent, BridgeCommand } from "./types.js";
16
+
17
+ // ============================================================================
18
+ // JSON-RPC 2.0 Types
19
+ // ============================================================================
20
+
21
+ /**
22
+ * JSON-RPC 2.0 Request
23
+ */
24
+ export interface JSONRPCRequest {
25
+ jsonrpc: "2.0";
26
+ id: string | number;
27
+ method: string;
28
+ params?: unknown;
29
+ }
30
+
31
+ /**
32
+ * JSON-RPC 2.0 Response (success)
33
+ */
34
+ export interface JSONRPCSuccessResponse {
35
+ jsonrpc: "2.0";
36
+ id: string | number;
37
+ result: unknown;
38
+ }
39
+
40
+ /**
41
+ * JSON-RPC 2.0 Response (error)
42
+ */
43
+ export interface JSONRPCErrorResponse {
44
+ jsonrpc: "2.0";
45
+ id: string | number | null;
46
+ error: {
47
+ code: number;
48
+ message: string;
49
+ data?: unknown;
50
+ };
51
+ }
52
+
53
+ /**
54
+ * JSON-RPC 2.0 Response
55
+ */
56
+ export type JSONRPCResponse = JSONRPCSuccessResponse | JSONRPCErrorResponse;
57
+
58
+ /**
59
+ * JSON-RPC 2.0 Notification (no id)
60
+ */
61
+ export interface JSONRPCNotification {
62
+ jsonrpc: "2.0";
63
+ method: string;
64
+ params?: unknown;
65
+ }
66
+
67
+ // ============================================================================
68
+ // Zod Schemas for Validation
69
+ // ============================================================================
70
+
71
+ /**
72
+ * Schema for JSON-RPC 2.0 Request
73
+ */
74
+ export const JSONRPCRequestSchema = z.object({
75
+ jsonrpc: z.literal("2.0"),
76
+ id: z.union([z.string(), z.number()]),
77
+ method: z.string(),
78
+ params: z.unknown().optional(),
79
+ });
80
+
81
+ /**
82
+ * Schema for JSON-RPC 2.0 Notification
83
+ */
84
+ export const JSONRPCNotificationSchema = z.object({
85
+ jsonrpc: z.literal("2.0"),
86
+ method: z.string(),
87
+ params: z.unknown().optional(),
88
+ });
89
+
90
+ /**
91
+ * Schema for IPC Server Configuration
92
+ */
93
+ export const IPCServerConfigSchema = z.object({
94
+ /** Session ID for socket path */
95
+ sessionId: z.string().min(1),
96
+ /** Enable Unix socket transport */
97
+ enableSocket: z.boolean().default(true),
98
+ /** Custom socket path (defaults to /tmp/coder-bridge-${sessionId}.sock) */
99
+ socketPath: z.string().optional(),
100
+ /** Enable HTTP transport */
101
+ enableHttp: z.boolean().default(false),
102
+ /** HTTP port (defaults to 9876) */
103
+ httpPort: z.number().int().positive().default(9876),
104
+ /** HTTP host */
105
+ httpHost: z.string().default("localhost"),
106
+ /** Enable CORS for HTTP */
107
+ cors: z.boolean().default(true),
108
+ /** CORS origins */
109
+ corsOrigins: z.array(z.string()).default(["*"]),
110
+ /** Max connections */
111
+ maxConnections: z.number().int().positive().default(100),
112
+ /** Connection timeout in ms */
113
+ connectionTimeout: z.number().int().positive().default(30000),
114
+ });
115
+
116
+ /**
117
+ * IPC Server Configuration
118
+ */
119
+ export type IPCServerConfig = z.infer<typeof IPCServerConfigSchema>;
120
+
121
+ // ============================================================================
122
+ // IPC Server Events
123
+ // ============================================================================
124
+
125
+ /**
126
+ * IPC Server event types
127
+ */
128
+ export type IPCServerEventType =
129
+ | "client_connected"
130
+ | "client_disconnected"
131
+ | "request_received"
132
+ | "response_sent"
133
+ | "notification_sent"
134
+ | "error"
135
+ | "server_started"
136
+ | "server_stopped"
137
+ | "socket_server_started"
138
+ | "http_server_started";
139
+
140
+ /**
141
+ * IPC Server event payload
142
+ */
143
+ export interface IPCServerEvent<T = unknown> {
144
+ type: IPCServerEventType;
145
+ payload: T;
146
+ timestamp: number;
147
+ }
148
+
149
+ /**
150
+ * Client connection info
151
+ */
152
+ export interface ClientConnection {
153
+ id: string;
154
+ type: "socket" | "http";
155
+ connectedAt: number;
156
+ lastActivity: number;
157
+ subscriberId?: string;
158
+ }
159
+
160
+ // ============================================================================
161
+ // IPC Server Implementation
162
+ // ============================================================================
163
+
164
+ /**
165
+ * TUI Bridge IPC Server
166
+ *
167
+ * Provides bidirectional communication between TUI Bridge and external clients
168
+ * via Unix sockets (primary) and HTTP (secondary).
169
+ */
170
+ export class IPCServer extends EventEmitter {
171
+ private config: IPCServerConfig;
172
+ private bridge: TUIBridge;
173
+ private socketServer: unknown = null;
174
+ private httpServer: unknown = null;
175
+ private clients: Map<string, ClientConnection> = new Map();
176
+ private subscribers: Map<string, Set<string>> = new Map();
177
+ private isRunning = false;
178
+ private requestId = 0;
179
+
180
+ constructor(bridge: TUIBridge, config: Partial<IPCServerConfig> = {}) {
181
+ super();
182
+ this.bridge = bridge;
183
+
184
+ const parsed = IPCServerConfigSchema.parse({
185
+ sessionId: config.sessionId || "default",
186
+ ...config,
187
+ });
188
+ this.config = parsed;
189
+
190
+ this.setupBridgeEventForwarding();
191
+ }
192
+
193
+ /**
194
+ * Get socket path for this server
195
+ */
196
+ getSocketPath(): string {
197
+ return this.config.socketPath || `/tmp/coder-bridge-${this.config.sessionId}.sock`;
198
+ }
199
+
200
+ /**
201
+ * Get HTTP URL for this server
202
+ */
203
+ getHttpUrl(): string {
204
+ return `http://${this.config.httpHost}:${this.config.httpPort}`;
205
+ }
206
+
207
+ /**
208
+ * Start the IPC server
209
+ */
210
+ async start(): Promise<void> {
211
+ if (this.isRunning) {
212
+ return;
213
+ }
214
+
215
+ if (this.config.enableSocket) {
216
+ await this.startSocketServer();
217
+ }
218
+
219
+ if (this.config.enableHttp) {
220
+ await this.startHttpServer();
221
+ }
222
+
223
+ this.isRunning = true;
224
+ this.emitEvent("server_started", {
225
+ socketPath: this.getSocketPath(),
226
+ httpUrl: this.config.enableHttp ? this.getHttpUrl() : undefined,
227
+ });
228
+ }
229
+
230
+ /**
231
+ * Stop the IPC server
232
+ */
233
+ async stop(): Promise<void> {
234
+ if (!this.isRunning) {
235
+ return;
236
+ }
237
+
238
+ const clientIds = Array.from(this.clients.keys());
239
+ for (const clientId of clientIds) {
240
+ await this.disconnectClient(clientId);
241
+ }
242
+
243
+ if (this.socketServer) {
244
+ await this.stopSocketServer();
245
+ this.socketServer = null;
246
+ }
247
+
248
+ if (this.httpServer) {
249
+ await this.stopHttpServer();
250
+ this.httpServer = null;
251
+ }
252
+
253
+ this.isRunning = false;
254
+ this.emitEvent("server_stopped", {});
255
+ }
256
+
257
+ /**
258
+ * Check if server is running
259
+ */
260
+ isServerRunning(): boolean {
261
+ return this.isRunning;
262
+ }
263
+
264
+ /**
265
+ * Get connected clients
266
+ */
267
+ getConnectedClients(): ClientConnection[] {
268
+ return Array.from(this.clients.values());
269
+ }
270
+
271
+ /**
272
+ * Get client count
273
+ */
274
+ getClientCount(): number {
275
+ return this.clients.size;
276
+ }
277
+
278
+ /**
279
+ * Disconnect a specific client
280
+ */
281
+ async disconnectClient(clientId: string): Promise<void> {
282
+ const client = this.clients.get(clientId);
283
+ if (!client) {
284
+ return;
285
+ }
286
+
287
+ if (client.subscriberId) {
288
+ const subscriberClients = this.subscribers.get(client.subscriberId);
289
+ if (subscriberClients) {
290
+ subscriberClients.delete(clientId);
291
+ if (subscriberClients.size === 0) {
292
+ this.subscribers.delete(client.subscriberId);
293
+ this.bridge.unsubscribe(client.subscriberId);
294
+ }
295
+ }
296
+ }
297
+
298
+ this.clients.delete(clientId);
299
+ this.emitEvent("client_disconnected", { clientId, client });
300
+ }
301
+
302
+ // ===========================================================================
303
+ // Unix Socket Server (Bun native)
304
+ // ===========================================================================
305
+
306
+ private async startSocketServer(): Promise<void> {
307
+ const socketPath = this.getSocketPath();
308
+
309
+ try {
310
+ const file = Bun.file(socketPath);
311
+ if (await file.exists()) {
312
+ await Bun.$`rm -f ${socketPath}`;
313
+ }
314
+ } catch {
315
+ // Ignore errors
316
+ }
317
+
318
+ this.socketServer = Bun.serve({
319
+ unix: socketPath,
320
+ fetch: this.handleSocketRequest.bind(this),
321
+ });
322
+
323
+ this.emitEvent("socket_server_started", { socketPath });
324
+ }
325
+
326
+ private async stopSocketServer(): Promise<void> {
327
+ if (this.socketServer && typeof this.socketServer === "object" && "stop" in this.socketServer) {
328
+ (this.socketServer as { stop: () => void }).stop();
329
+ }
330
+
331
+ try {
332
+ const socketPath = this.getSocketPath();
333
+ await Bun.$`rm -f ${socketPath}`;
334
+ } catch {
335
+ // Ignore cleanup errors
336
+ }
337
+ }
338
+
339
+ private async handleSocketRequest(request: Request): Promise<Response> {
340
+ const clientId = this.generateClientId("socket");
341
+
342
+ try {
343
+ const body = await request.text();
344
+ const message = JSON.parse(body);
345
+
346
+ this.registerClient(clientId, "socket");
347
+ const response = await this.handleMessage(clientId, message);
348
+
349
+ return new Response(JSON.stringify(response), {
350
+ headers: { "Content-Type": "application/json" },
351
+ });
352
+ } catch (error) {
353
+ return new Response(
354
+ JSON.stringify({
355
+ jsonrpc: "2.0",
356
+ id: null,
357
+ error: {
358
+ code: -32700,
359
+ message: "Parse error",
360
+ data: error instanceof Error ? error.message : "Unknown error",
361
+ },
362
+ }),
363
+ {
364
+ status: 400,
365
+ headers: { "Content-Type": "application/json" },
366
+ }
367
+ );
368
+ }
369
+ }
370
+
371
+ // ===========================================================================
372
+ // HTTP Server (Bun native)
373
+ // ===========================================================================
374
+
375
+ private async startHttpServer(): Promise<void> {
376
+ this.httpServer = Bun.serve({
377
+ port: this.config.httpPort,
378
+ hostname: this.config.httpHost,
379
+ fetch: this.handleHttpRequest.bind(this),
380
+ });
381
+
382
+ this.emitEvent("http_server_started", {
383
+ url: this.getHttpUrl(),
384
+ port: this.config.httpPort,
385
+ });
386
+ }
387
+
388
+ private async stopHttpServer(): Promise<void> {
389
+ if (this.httpServer && typeof this.httpServer === "object" && "stop" in this.httpServer) {
390
+ (this.httpServer as { stop: () => void }).stop();
391
+ }
392
+ }
393
+
394
+ private async handleHttpRequest(request: Request): Promise<Response> {
395
+ const url = new URL(request.url);
396
+
397
+ if (request.method === "OPTIONS") {
398
+ return this.createCorsResponse();
399
+ }
400
+
401
+ if (url.pathname === "/jsonrpc" || url.pathname === "/") {
402
+ return this.handleHttpJsonRpc(request);
403
+ }
404
+
405
+ if (url.pathname === "/health") {
406
+ return this.handleHealthCheck();
407
+ }
408
+
409
+ if (url.pathname === "/sse") {
410
+ return this.handleSseConnection(request);
411
+ }
412
+
413
+ return new Response("Not Found", { status: 404 });
414
+ }
415
+
416
+ private async handleHttpJsonRpc(request: Request): Promise<Response> {
417
+ const corsHeaders = this.getCorsHeaders();
418
+
419
+ if (request.method !== "POST") {
420
+ return new Response("Method Not Allowed", {
421
+ status: 405,
422
+ headers: corsHeaders,
423
+ });
424
+ }
425
+
426
+ const clientId = this.generateClientId("http");
427
+
428
+ try {
429
+ const body = await request.text();
430
+ const message = JSON.parse(body);
431
+
432
+ this.registerClient(clientId, "http");
433
+ const response = await this.handleMessage(clientId, message);
434
+
435
+ return new Response(JSON.stringify(response), {
436
+ headers: {
437
+ "Content-Type": "application/json",
438
+ ...corsHeaders,
439
+ },
440
+ });
441
+ } catch (error) {
442
+ return new Response(
443
+ JSON.stringify({
444
+ jsonrpc: "2.0",
445
+ id: null,
446
+ error: {
447
+ code: -32700,
448
+ message: "Parse error",
449
+ data: error instanceof Error ? error.message : "Unknown error",
450
+ },
451
+ }),
452
+ {
453
+ status: 400,
454
+ headers: {
455
+ "Content-Type": "application/json",
456
+ ...corsHeaders,
457
+ },
458
+ }
459
+ );
460
+ }
461
+ }
462
+
463
+ private handleHealthCheck(): Response {
464
+ return new Response(
465
+ JSON.stringify({
466
+ status: "ok",
467
+ clients: this.clients.size,
468
+ subscribers: this.subscribers.size,
469
+ bridge: {
470
+ enabled: this.bridge.isEnabled(),
471
+ subscriberCount: this.bridge.getSubscriberCount(),
472
+ },
473
+ }),
474
+ {
475
+ headers: {
476
+ "Content-Type": "application/json",
477
+ ...this.getCorsHeaders(),
478
+ },
479
+ }
480
+ );
481
+ }
482
+
483
+ private handleSseConnection(request: Request): Response {
484
+ const clientId = this.generateClientId("http");
485
+ const subscriberId = `sse-${clientId}`;
486
+
487
+ this.registerClient(clientId, "http");
488
+ this.bridge.subscribe(subscriberId);
489
+
490
+ const client = this.clients.get(clientId);
491
+ if (client) {
492
+ client.subscriberId = subscriberId;
493
+ }
494
+
495
+ if (!this.subscribers.has(subscriberId)) {
496
+ this.subscribers.set(subscriberId, new Set());
497
+ }
498
+ this.subscribers.get(subscriberId)!.add(clientId);
499
+
500
+ const stream = new ReadableStream({
501
+ start: (controller) => {
502
+ const encoder = new TextEncoder();
503
+
504
+ controller.enqueue(
505
+ encoder.encode(`data: ${JSON.stringify({ type: "connected", clientId })}\n\n`)
506
+ );
507
+
508
+ const eventHandler = (event: BridgeEvent) => {
509
+ try {
510
+ controller.enqueue(
511
+ encoder.encode(`data: ${JSON.stringify(event)}\n\n`)
512
+ );
513
+ } catch {
514
+ this.bridge.off("event", eventHandler);
515
+ }
516
+ };
517
+
518
+ this.bridge.on("event", eventHandler);
519
+
520
+ request.signal.addEventListener("abort", () => {
521
+ this.bridge.off("event", eventHandler);
522
+ this.disconnectClient(clientId);
523
+ try {
524
+ controller.close();
525
+ } catch {
526
+ // Already closed
527
+ }
528
+ });
529
+ },
530
+ });
531
+
532
+ return new Response(stream, {
533
+ headers: {
534
+ "Content-Type": "text/event-stream",
535
+ "Cache-Control": "no-cache",
536
+ "Connection": "keep-alive",
537
+ ...this.getCorsHeaders(),
538
+ },
539
+ });
540
+ }
541
+
542
+ private getCorsHeaders(): Record<string, string> {
543
+ if (!this.config.cors) {
544
+ return {};
545
+ }
546
+
547
+ return {
548
+ "Access-Control-Allow-Origin": this.config.corsOrigins.join(", "),
549
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
550
+ "Access-Control-Allow-Headers": "Content-Type",
551
+ };
552
+ }
553
+
554
+ private createCorsResponse(): Response {
555
+ return new Response(null, {
556
+ status: 204,
557
+ headers: this.getCorsHeaders(),
558
+ });
559
+ }
560
+
561
+ // ===========================================================================
562
+ // JSON-RPC Message Handling
563
+ // ===========================================================================
564
+
565
+ private async handleMessage(
566
+ clientId: string,
567
+ message: unknown
568
+ ): Promise<JSONRPCResponse> {
569
+ const requestResult = JSONRPCRequestSchema.safeParse(message);
570
+ if (requestResult.success) {
571
+ const request = requestResult.data as JSONRPCRequest;
572
+ return this.handleRequest(clientId, request);
573
+ }
574
+
575
+ const notificationResult = JSONRPCNotificationSchema.safeParse(message);
576
+ if (notificationResult.success) {
577
+ const notification = notificationResult.data as JSONRPCNotification;
578
+ await this.handleNotification(clientId, notification);
579
+ return {
580
+ jsonrpc: "2.0",
581
+ id: ++this.requestId,
582
+ result: { received: true },
583
+ };
584
+ }
585
+
586
+ return {
587
+ jsonrpc: "2.0",
588
+ id: null,
589
+ error: {
590
+ code: -32600,
591
+ message: "Invalid Request",
592
+ data: "Message must be a valid JSON-RPC 2.0 request or notification",
593
+ },
594
+ };
595
+ }
596
+
597
+ private async handleRequest(
598
+ clientId: string,
599
+ request: JSONRPCRequest
600
+ ): Promise<JSONRPCResponse> {
601
+ this.emitEvent("request_received", { clientId, request });
602
+
603
+ try {
604
+ const result = await this.executeMethod(clientId, request.method, request.params);
605
+
606
+ const response: JSONRPCSuccessResponse = {
607
+ jsonrpc: "2.0",
608
+ id: request.id,
609
+ result,
610
+ };
611
+
612
+ this.emitEvent("response_sent", { clientId, response });
613
+ return response;
614
+ } catch (error) {
615
+ const response: JSONRPCErrorResponse = {
616
+ jsonrpc: "2.0",
617
+ id: request.id,
618
+ error: {
619
+ code: -32603,
620
+ message: "Internal error",
621
+ data: error instanceof Error ? error.message : "Unknown error",
622
+ },
623
+ };
624
+
625
+ this.emitEvent("error", { clientId, error, request });
626
+ return response;
627
+ }
628
+ }
629
+
630
+ private async handleNotification(
631
+ clientId: string,
632
+ notification: JSONRPCNotification
633
+ ): Promise<void> {
634
+ this.emitEvent("notification_received", { clientId, notification });
635
+ await this.executeMethod(clientId, notification.method, notification.params);
636
+ }
637
+
638
+ private async executeMethod(
639
+ clientId: string,
640
+ method: string,
641
+ params?: unknown
642
+ ): Promise<unknown> {
643
+ switch (method) {
644
+ case "getState":
645
+ return this.bridge.getState();
646
+
647
+ case "sendMessage": {
648
+ const command: BridgeCommand = {
649
+ type: "send_message",
650
+ content: (params as { content: string })?.content || "",
651
+ };
652
+ return this.bridge.executeCommand(command);
653
+ }
654
+
655
+ case "executeCommand": {
656
+ const command: BridgeCommand = {
657
+ type: "execute_command",
658
+ command: (params as { command: string })?.command || "",
659
+ };
660
+ return this.bridge.executeCommand(command);
661
+ }
662
+
663
+ case "setModel": {
664
+ const command: BridgeCommand = {
665
+ type: "set_model",
666
+ model: (params as { model: string })?.model || "",
667
+ };
668
+ return this.bridge.executeCommand(command);
669
+ }
670
+
671
+ case "clearMessages": {
672
+ const command: BridgeCommand = { type: "clear_messages" };
673
+ return this.bridge.executeCommand(command);
674
+ }
675
+
676
+ case "exportSession": {
677
+ const command: BridgeCommand = {
678
+ type: "export_session",
679
+ format: (params as { format: "jsonl" | "json" | "markdown" })?.format || "jsonl",
680
+ };
681
+ return this.bridge.executeCommand(command);
682
+ }
683
+
684
+ case "getScreen": {
685
+ const command: BridgeCommand = { type: "get_screen" };
686
+ return this.bridge.executeCommand(command);
687
+ }
688
+
689
+ case "subscribe": {
690
+ const subscriberId = (params as { subscriberId?: string })?.subscriberId || clientId;
691
+ const success = this.bridge.subscribe(subscriberId);
692
+
693
+ if (!this.subscribers.has(subscriberId)) {
694
+ this.subscribers.set(subscriberId, new Set());
695
+ }
696
+ this.subscribers.get(subscriberId)!.add(clientId);
697
+
698
+ const client = this.clients.get(clientId);
699
+ if (client) {
700
+ client.subscriberId = subscriberId;
701
+ }
702
+
703
+ return { success, subscriberId };
704
+ }
705
+
706
+ case "unsubscribe": {
707
+ const subscriberId =
708
+ (params as { subscriberId?: string })?.subscriberId ||
709
+ this.clients.get(clientId)?.subscriberId;
710
+
711
+ if (!subscriberId) {
712
+ return { success: false, error: "Not subscribed" };
713
+ }
714
+
715
+ const success = this.bridge.unsubscribe(subscriberId);
716
+
717
+ const subscriberClients = this.subscribers.get(subscriberId);
718
+ if (subscriberClients) {
719
+ subscriberClients.delete(clientId);
720
+ if (subscriberClients.size === 0) {
721
+ this.subscribers.delete(subscriberId);
722
+ }
723
+ }
724
+
725
+ const client = this.clients.get(clientId);
726
+ if (client) {
727
+ client.subscriberId = undefined;
728
+ }
729
+
730
+ return { success };
731
+ }
732
+
733
+ default:
734
+ throw new Error(`Unknown method: ${method}`);
735
+ }
736
+ }
737
+
738
+ // ===========================================================================
739
+ // Bridge Event Forwarding
740
+ // ===========================================================================
741
+
742
+ private setupBridgeEventForwarding(): void {
743
+ this.bridge.on("event", (event: BridgeEvent) => {
744
+ this.broadcastToSubscribers(event);
745
+ });
746
+ }
747
+
748
+ private broadcastToSubscribers(event: BridgeEvent): void {
749
+ const notification: JSONRPCNotification = {
750
+ jsonrpc: "2.0",
751
+ method: "event",
752
+ params: event,
753
+ };
754
+
755
+ const subscriberEntries = Array.from(this.subscribers.entries());
756
+ for (const [, clientIds] of subscriberEntries) {
757
+ const clientIdArray = Array.from(clientIds);
758
+ for (const clientId of clientIdArray) {
759
+ this.sendToClient(clientId, notification).catch(() => {
760
+ // Ignore send errors
761
+ });
762
+ }
763
+ }
764
+
765
+ this.emitEvent("notification_sent", { notification, subscriberCount: this.subscribers.size });
766
+ }
767
+
768
+ private async sendToClient(_clientId: string, _message: unknown): Promise<void> {
769
+ // For stateless HTTP/socket connections, we can't push
770
+ // SSE connections handle their own streaming via the ReadableStream
771
+ }
772
+
773
+ // ===========================================================================
774
+ // Client Management
775
+ // ===========================================================================
776
+
777
+ private registerClient(clientId: string, type: "socket" | "http"): void {
778
+ const client: ClientConnection = {
779
+ id: clientId,
780
+ type,
781
+ connectedAt: Date.now(),
782
+ lastActivity: Date.now(),
783
+ };
784
+
785
+ this.clients.set(clientId, client);
786
+ this.emitEvent("client_connected", { client });
787
+
788
+ if (this.clients.size > this.config.maxConnections) {
789
+ const entries = Array.from(this.clients.entries());
790
+ const oldest = entries.sort((a, b) => a[1].connectedAt - b[1].connectedAt)[0];
791
+ if (oldest) {
792
+ this.disconnectClient(oldest[0]);
793
+ }
794
+ }
795
+ }
796
+
797
+ private generateClientId(type: "socket" | "http"): string {
798
+ return `${type}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
799
+ }
800
+
801
+ // ===========================================================================
802
+ // Event Emission
803
+ // ===========================================================================
804
+
805
+ private emitEvent<T>(type: string, payload: T): void {
806
+ const event = {
807
+ type,
808
+ payload,
809
+ timestamp: Date.now(),
810
+ };
811
+ this.emit("event", event);
812
+ }
813
+ }
814
+
815
+ // ============================================================================
816
+ // Factory Function
817
+ // ============================================================================
818
+
819
+ /**
820
+ * Create an IPC server for TUI Bridge
821
+ */
822
+ export function createIPCServer(
823
+ bridge: TUIBridge,
824
+ config: Partial<IPCServerConfig> = {}
825
+ ): IPCServer {
826
+ return new IPCServer(bridge, config);
827
+ }
828
+
829
+ export default IPCServer;