@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.
- package/dist/index.js +36233 -32
- package/dist/interfaces/ui/terminal/cli/index.js +34318 -158
- package/dist/interfaces/ui/terminal/native/README.md +53 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
- package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
- package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
- package/dist/interfaces/ui/terminal/native/index.js +43 -0
- package/dist/interfaces/ui/terminal/native/index.node +0 -0
- package/dist/interfaces/ui/terminal/native/package.json +34 -0
- package/dist/native/README.md +53 -0
- package/dist/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/native/claude_code_native.dylib +0 -0
- package/dist/native/index.d.ts +0 -480
- package/dist/native/index.darwin-arm64.node +0 -0
- package/dist/native/index.js +43 -1625
- package/dist/native/index.node +0 -0
- package/dist/native/package.json +34 -0
- package/native/index.darwin-arm64.node +0 -0
- package/native/index.js +33 -19
- package/package.json +3 -2
- package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
- package/packages/src/core/agent-loop/compaction.ts +6 -2
- package/packages/src/core/agent-loop/index.ts +2 -0
- package/packages/src/core/agent-loop/loop-state.ts +1 -1
- package/packages/src/core/agent-loop/turn-executor.ts +4 -0
- package/packages/src/core/agent-loop/types.ts +4 -0
- package/packages/src/core/api-client-impl.ts +377 -176
- package/packages/src/core/cognitive-security/hooks.ts +2 -1
- package/packages/src/core/config/todo +7 -0
- package/packages/src/core/context/__tests__/integration.test.ts +334 -0
- package/packages/src/core/context/compaction.ts +170 -0
- package/packages/src/core/context/constants.ts +58 -0
- package/packages/src/core/context/extraction.ts +85 -0
- package/packages/src/core/context/index.ts +66 -0
- package/packages/src/core/context/summarization.ts +251 -0
- package/packages/src/core/context/token-estimation.ts +98 -0
- package/packages/src/core/context/types.ts +59 -0
- package/packages/src/core/models.ts +81 -4
- package/packages/src/core/normalizers/todo +5 -1
- package/packages/src/core/providers/README.md +230 -0
- package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
- package/packages/src/core/providers/index.ts +419 -0
- package/packages/src/core/providers/types.ts +132 -0
- package/packages/src/core/retry.ts +10 -0
- package/packages/src/ecosystem/tools/index.ts +174 -0
- package/packages/src/index.ts +23 -2
- package/packages/src/interfaces/ui/index.ts +17 -20
- package/packages/src/interfaces/ui/spinner.ts +2 -2
- package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
- package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
- package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
- package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
- package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
- package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
- package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
- package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +402 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
- package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
- package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
- package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
- package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
- package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
- package/packages/src/native/index.ts +404 -27
- package/packages/src/native/tui_v2_types.ts +39 -0
- package/packages/src/teammates/coordination.test.ts +279 -0
- package/packages/src/teammates/coordination.ts +646 -0
- package/packages/src/teammates/index.ts +95 -25
- package/packages/src/teammates/integration.test.ts +272 -0
- package/packages/src/teammates/runner.test.ts +235 -0
- package/packages/src/teammates/runner.ts +750 -0
- package/packages/src/teammates/schemas.ts +673 -0
- package/packages/src/types/index.ts +1 -0
- package/packages/src/core/context-compaction.ts +0 -578
- package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
- package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
- package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
- package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
- package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
- package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
- package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
- package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
- package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
- package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
- package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
- package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
- package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
- package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
- package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
- package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
- package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
- package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
- package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
- package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
- package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
- package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
- package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
- 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;
|