@epiphytic/claudecodeui 1.0.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.
Files changed (142) hide show
  1. package/LICENSE +675 -0
  2. package/README.md +414 -0
  3. package/dist/api-docs.html +879 -0
  4. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  5. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  6. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  23. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  32. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  35. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  47. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  50. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  53. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  55. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  56. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  58. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  61. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  63. package/dist/assets/index-DfR9xEkp.css +32 -0
  64. package/dist/assets/index-DvlVn6Eb.js +1231 -0
  65. package/dist/assets/vendor-codemirror-CJLzwpLB.js +39 -0
  66. package/dist/assets/vendor-react-DcyRfQm3.js +59 -0
  67. package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
  68. package/dist/clear-cache.html +85 -0
  69. package/dist/convert-icons.md +53 -0
  70. package/dist/favicon.png +0 -0
  71. package/dist/favicon.svg +9 -0
  72. package/dist/generate-icons.js +49 -0
  73. package/dist/icons/claude-ai-icon.svg +1 -0
  74. package/dist/icons/codex-white.svg +3 -0
  75. package/dist/icons/codex.svg +3 -0
  76. package/dist/icons/cursor-white.svg +12 -0
  77. package/dist/icons/cursor.svg +1 -0
  78. package/dist/icons/generate-icons.md +19 -0
  79. package/dist/icons/icon-128x128.png +0 -0
  80. package/dist/icons/icon-128x128.svg +12 -0
  81. package/dist/icons/icon-144x144.png +0 -0
  82. package/dist/icons/icon-144x144.svg +12 -0
  83. package/dist/icons/icon-152x152.png +0 -0
  84. package/dist/icons/icon-152x152.svg +12 -0
  85. package/dist/icons/icon-192x192.png +0 -0
  86. package/dist/icons/icon-192x192.svg +12 -0
  87. package/dist/icons/icon-384x384.png +0 -0
  88. package/dist/icons/icon-384x384.svg +12 -0
  89. package/dist/icons/icon-512x512.png +0 -0
  90. package/dist/icons/icon-512x512.svg +12 -0
  91. package/dist/icons/icon-72x72.png +0 -0
  92. package/dist/icons/icon-72x72.svg +12 -0
  93. package/dist/icons/icon-96x96.png +0 -0
  94. package/dist/icons/icon-96x96.svg +12 -0
  95. package/dist/icons/icon-template.svg +12 -0
  96. package/dist/index.html +52 -0
  97. package/dist/logo-128.png +0 -0
  98. package/dist/logo-256.png +0 -0
  99. package/dist/logo-32.png +0 -0
  100. package/dist/logo-512.png +0 -0
  101. package/dist/logo-64.png +0 -0
  102. package/dist/logo.svg +17 -0
  103. package/dist/manifest.json +61 -0
  104. package/dist/screenshots/cli-selection.png +0 -0
  105. package/dist/screenshots/desktop-main.png +0 -0
  106. package/dist/screenshots/mobile-chat.png +0 -0
  107. package/dist/screenshots/tools-modal.png +0 -0
  108. package/dist/sw.js +107 -0
  109. package/package.json +120 -0
  110. package/server/claude-sdk.js +721 -0
  111. package/server/cli.js +469 -0
  112. package/server/cursor-cli.js +267 -0
  113. package/server/database/db.js +554 -0
  114. package/server/database/init.sql +54 -0
  115. package/server/index.js +2120 -0
  116. package/server/middleware/auth.js +161 -0
  117. package/server/openai-codex.js +389 -0
  118. package/server/orchestrator/client.js +989 -0
  119. package/server/orchestrator/github-auth.js +308 -0
  120. package/server/orchestrator/index.js +216 -0
  121. package/server/orchestrator/protocol.js +299 -0
  122. package/server/orchestrator/proxy.js +364 -0
  123. package/server/orchestrator/status-tracker.js +226 -0
  124. package/server/projects.js +1604 -0
  125. package/server/routes/agent.js +1230 -0
  126. package/server/routes/auth.js +135 -0
  127. package/server/routes/cli-auth.js +341 -0
  128. package/server/routes/codex.js +345 -0
  129. package/server/routes/commands.js +521 -0
  130. package/server/routes/cursor.js +795 -0
  131. package/server/routes/git.js +1128 -0
  132. package/server/routes/mcp-utils.js +48 -0
  133. package/server/routes/mcp.js +650 -0
  134. package/server/routes/projects.js +378 -0
  135. package/server/routes/settings.js +178 -0
  136. package/server/routes/taskmaster.js +1963 -0
  137. package/server/routes/user.js +106 -0
  138. package/server/utils/commandParser.js +303 -0
  139. package/server/utils/gitConfig.js +24 -0
  140. package/server/utils/mcp-detector.js +198 -0
  141. package/server/utils/taskmaster-websocket.js +129 -0
  142. package/shared/modelConstants.js +65 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Orchestrator Protocol
3
+ *
4
+ * Defines message types and serialization/parsing for communication
5
+ * between claudecodeui and the orchestrator server.
6
+ */
7
+
8
+ /**
9
+ * Message Types (Outbound: claudecodeui → Orchestrator)
10
+ */
11
+ export const OutboundMessageTypes = {
12
+ REGISTER: "register",
13
+ STATUS_UPDATE: "status_update",
14
+ PING: "ping",
15
+ RESPONSE: "response",
16
+ RESPONSE_CHUNK: "response_chunk",
17
+ RESPONSE_COMPLETE: "response_complete",
18
+ ERROR: "error",
19
+ HTTP_PROXY_RESPONSE: "http_proxy_response",
20
+ };
21
+
22
+ /**
23
+ * Message Types (Inbound: Orchestrator → claudecodeui)
24
+ */
25
+ export const InboundMessageTypes = {
26
+ REGISTERED: "registered",
27
+ PONG: "pong",
28
+ COMMAND: "command",
29
+ ERROR: "error",
30
+ USER_REQUEST: "user_request",
31
+ USER_REQUEST_FOLLOW_UP: "user_request_follow_up",
32
+ HTTP_PROXY_REQUEST: "http_proxy_request",
33
+ };
34
+
35
+ /**
36
+ * Status values for status updates
37
+ */
38
+ export const StatusValues = {
39
+ IDLE: "idle",
40
+ ACTIVE: "active",
41
+ BUSY: "busy",
42
+ };
43
+
44
+ /**
45
+ * Command types from orchestrator
46
+ */
47
+ export const CommandTypes = {
48
+ DISCONNECT: "disconnect",
49
+ REFRESH_STATUS: "refresh_status",
50
+ };
51
+
52
+ /**
53
+ * Creates a registration message
54
+ * @param {string} clientId - Unique client identifier
55
+ * @param {string} userToken - Authentication token from orchestrator
56
+ * @param {Object} metadata - Additional client metadata
57
+ * @returns {Object} Registration message
58
+ */
59
+ export function createRegisterMessage(clientId, userToken, metadata = {}) {
60
+ return {
61
+ type: OutboundMessageTypes.REGISTER,
62
+ client_id: clientId,
63
+ user_token: userToken,
64
+ metadata: {
65
+ hostname: metadata.hostname || "",
66
+ project: metadata.project || "",
67
+ status: metadata.status || StatusValues.IDLE,
68
+ version: metadata.version || "1.0.0",
69
+ ...metadata,
70
+ },
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Creates a status update message
76
+ * @param {string} clientId - Unique client identifier
77
+ * @param {string} status - Current status (idle, active, busy)
78
+ * @returns {Object} Status update message
79
+ */
80
+ export function createStatusUpdateMessage(clientId, status) {
81
+ return {
82
+ type: OutboundMessageTypes.STATUS_UPDATE,
83
+ client_id: clientId,
84
+ status,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Creates a ping message
90
+ * @param {string} clientId - Unique client identifier
91
+ * @returns {Object} Ping message
92
+ */
93
+ export function createPingMessage(clientId) {
94
+ return {
95
+ type: OutboundMessageTypes.PING,
96
+ client_id: clientId,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Creates a response message (for proxied requests)
102
+ * @param {string} requestId - Original request ID
103
+ * @param {Object} data - Response data
104
+ * @returns {Object} Response message
105
+ */
106
+ export function createResponseMessage(requestId, data) {
107
+ return {
108
+ type: OutboundMessageTypes.RESPONSE,
109
+ request_id: requestId,
110
+ data,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Creates a response chunk message (for streaming)
116
+ * @param {string} requestId - Original request ID
117
+ * @param {Object} data - Chunk data
118
+ * @returns {Object} Response chunk message
119
+ */
120
+ export function createResponseChunkMessage(requestId, data) {
121
+ return {
122
+ type: OutboundMessageTypes.RESPONSE_CHUNK,
123
+ request_id: requestId,
124
+ data,
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Creates a response complete message
130
+ * @param {string} requestId - Original request ID
131
+ * @param {Object} data - Final data (optional)
132
+ * @returns {Object} Response complete message
133
+ */
134
+ export function createResponseCompleteMessage(requestId, data = null) {
135
+ return {
136
+ type: OutboundMessageTypes.RESPONSE_COMPLETE,
137
+ request_id: requestId,
138
+ ...(data !== null && data !== undefined && { data }),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Creates an error message
144
+ * @param {string} requestId - Request ID (if applicable)
145
+ * @param {string} message - Error message
146
+ * @returns {Object} Error message
147
+ */
148
+ export function createErrorMessage(requestId, message) {
149
+ return {
150
+ type: OutboundMessageTypes.ERROR,
151
+ ...(requestId && { request_id: requestId }),
152
+ message,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Serializes a message to JSON string
158
+ * @param {Object} message - Message object
159
+ * @returns {string} JSON string
160
+ */
161
+ export function serialize(message) {
162
+ return JSON.stringify(message);
163
+ }
164
+
165
+ /**
166
+ * Parses a JSON string to message object
167
+ * @param {string} data - JSON string
168
+ * @returns {Object|null} Parsed message or null if invalid
169
+ */
170
+ export function parse(data) {
171
+ try {
172
+ return JSON.parse(data);
173
+ } catch (error) {
174
+ console.error("[ORCHESTRATOR] Failed to parse message:", error.message);
175
+ return null;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Validates an inbound message has required fields
181
+ * @param {Object} message - Message to validate
182
+ * @returns {boolean} True if valid
183
+ */
184
+ export function validateInboundMessage(message) {
185
+ if (!message || typeof message !== "object") {
186
+ return false;
187
+ }
188
+
189
+ if (!message.type) {
190
+ return false;
191
+ }
192
+
193
+ // Validate specific message types
194
+ switch (message.type) {
195
+ case InboundMessageTypes.REGISTERED:
196
+ return typeof message.success === "boolean";
197
+
198
+ case InboundMessageTypes.PONG:
199
+ return true;
200
+
201
+ case InboundMessageTypes.COMMAND:
202
+ return typeof message.command === "string";
203
+
204
+ case InboundMessageTypes.ERROR:
205
+ return typeof message.message === "string";
206
+
207
+ case InboundMessageTypes.USER_REQUEST:
208
+ // user_request message structure:
209
+ // {
210
+ // type: "user_request",
211
+ // request_id: string, // Unique request identifier
212
+ // action: string, // Action to perform (e.g., "claude-command")
213
+ // payload: {
214
+ // auth_token?: string, // GitHub OAuth token for pass-through auth
215
+ // user?: { // Optional pre-validated user info
216
+ // id: string,
217
+ // username: string,
218
+ // email?: string
219
+ // },
220
+ // ... // Action-specific payload data
221
+ // }
222
+ // }
223
+ return (
224
+ typeof message.request_id === "string" &&
225
+ typeof message.action === "string"
226
+ );
227
+
228
+ case InboundMessageTypes.USER_REQUEST_FOLLOW_UP:
229
+ // user_request_follow_up message structure:
230
+ // {
231
+ // type: "user_request_follow_up",
232
+ // request_id: string, // Request ID of existing session
233
+ // payload: {
234
+ // ... // Follow-up message data
235
+ // }
236
+ // }
237
+ return typeof message.request_id === "string";
238
+
239
+ case InboundMessageTypes.HTTP_PROXY_REQUEST:
240
+ // http_proxy_request message structure:
241
+ // {
242
+ // type: "http_proxy_request",
243
+ // request_id: string, // Unique request identifier
244
+ // method: string, // HTTP method (GET, POST, etc.)
245
+ // path: string, // Request path
246
+ // headers: [[string, string]], // Headers as key-value pairs
247
+ // body?: string, // Optional request body
248
+ // query?: string, // Optional query string
249
+ // proxy_base?: string, // Base path for URL rewriting (e.g., "/clients/{id}/proxy")
250
+ // }
251
+ return (
252
+ typeof message.request_id === "string" &&
253
+ typeof message.method === "string" &&
254
+ typeof message.path === "string"
255
+ );
256
+
257
+ default:
258
+ // Unknown message types are considered valid (forward compatibility)
259
+ return true;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Creates an auth error response message
265
+ * @param {string} requestId - Request ID
266
+ * @param {string} error - Error message
267
+ * @returns {Object} Auth error message
268
+ */
269
+ export function createAuthErrorMessage(requestId, error) {
270
+ return {
271
+ type: OutboundMessageTypes.ERROR,
272
+ request_id: requestId,
273
+ auth_error: true,
274
+ message: error,
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Creates an HTTP proxy response message
280
+ * @param {string} requestId - Original request ID
281
+ * @param {number} status - HTTP status code
282
+ * @param {Array<[string, string]>} headers - Response headers as [key, value] pairs
283
+ * @param {string} body - Response body
284
+ * @returns {Object} HTTP proxy response message
285
+ */
286
+ export function createHttpProxyResponseMessage(
287
+ requestId,
288
+ status,
289
+ headers,
290
+ body,
291
+ ) {
292
+ return {
293
+ type: OutboundMessageTypes.HTTP_PROXY_RESPONSE,
294
+ request_id: requestId,
295
+ status,
296
+ headers,
297
+ body,
298
+ };
299
+ }
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Orchestrator Proxy
3
+ *
4
+ * Provides a proxy layer that allows existing WebSocket handlers to work
5
+ * with requests forwarded through the orchestrator. The OrchestratorProxySocket
6
+ * implements a WebSocket-like interface so existing handlers can be reused unchanged.
7
+ */
8
+
9
+ import { EventEmitter } from "events";
10
+ import { createAuthErrorMessage, InboundMessageTypes } from "./protocol.js";
11
+ import { createGitHubAuthFromEnv } from "./github-auth.js";
12
+
13
+ /**
14
+ * OrchestratorProxySocket
15
+ *
16
+ * A "virtual WebSocket" that wraps orchestrator client communication.
17
+ * Allows existing handleChatConnection and related handlers to work
18
+ * with orchestrator-proxied requests without modification.
19
+ */
20
+ export class OrchestratorProxySocket extends EventEmitter {
21
+ /**
22
+ * Creates a new proxy socket
23
+ * @param {OrchestratorClient} orchestratorClient - The orchestrator client
24
+ * @param {string} requestId - The request ID for this proxied session
25
+ * @param {Object} [options] - Additional options
26
+ * @param {Object} [options.user] - User info to attach (like normal WebSocket)
27
+ */
28
+ constructor(orchestratorClient, requestId, options = {}) {
29
+ super();
30
+
31
+ this.orchestratorClient = orchestratorClient;
32
+ this.requestId = requestId;
33
+ this.user = options.user || {
34
+ id: "orchestrator",
35
+ username: "orchestrator",
36
+ };
37
+
38
+ // Mimic WebSocket readyState constants
39
+ this.CONNECTING = 0;
40
+ this.OPEN = 1;
41
+ this.CLOSING = 2;
42
+ this.CLOSED = 3;
43
+
44
+ // Start in OPEN state since the orchestrator connection is already established
45
+ this._readyState = this.OPEN;
46
+
47
+ // Track if this socket has been closed
48
+ this._closed = false;
49
+ }
50
+
51
+ /**
52
+ * Gets the current ready state (WebSocket-compatible)
53
+ * @returns {number} Ready state constant
54
+ */
55
+ get readyState() {
56
+ if (this._closed) {
57
+ return this.CLOSED;
58
+ }
59
+ if (!this.orchestratorClient || !this.orchestratorClient.isConnected) {
60
+ return this.CLOSED;
61
+ }
62
+ return this._readyState;
63
+ }
64
+
65
+ /**
66
+ * Sends data through the orchestrator connection
67
+ * This is the key method that existing handlers use to send responses.
68
+ * @param {string|Object} data - Data to send
69
+ */
70
+ send(data) {
71
+ if (this._closed || !this.orchestratorClient) {
72
+ console.warn("[ORCHESTRATOR-PROXY] Cannot send, socket closed");
73
+ return;
74
+ }
75
+
76
+ // Parse data if it's a string
77
+ let parsedData = data;
78
+ if (typeof data === "string") {
79
+ try {
80
+ parsedData = JSON.parse(data);
81
+ } catch (e) {
82
+ // Not JSON, send as-is wrapped in data field
83
+ parsedData = { raw: data };
84
+ }
85
+ }
86
+
87
+ // Forward through orchestrator as a response chunk
88
+ this.orchestratorClient.sendResponseChunk(this.requestId, parsedData);
89
+ }
90
+
91
+ /**
92
+ * Closes the proxy socket
93
+ * @param {number} [code] - Close code
94
+ * @param {string} [reason] - Close reason
95
+ */
96
+ close(code, reason) {
97
+ if (this._closed) {
98
+ return;
99
+ }
100
+
101
+ this._closed = true;
102
+ this._readyState = this.CLOSED;
103
+
104
+ // Send completion message through orchestrator
105
+ if (this.orchestratorClient && this.orchestratorClient.isConnected) {
106
+ this.orchestratorClient.sendResponseComplete(this.requestId, {
107
+ code: code || 1000,
108
+ reason: reason || "Normal close",
109
+ });
110
+ }
111
+
112
+ this.emit("close", code, reason);
113
+ }
114
+
115
+ /**
116
+ * Terminates the connection immediately
117
+ */
118
+ terminate() {
119
+ this.close(1006, "Terminated");
120
+ }
121
+
122
+ /**
123
+ * Simulates receiving a message (for orchestrator to inject user messages)
124
+ * @param {Object} data - Message data from user via orchestrator
125
+ */
126
+ injectMessage(data) {
127
+ if (this._closed) {
128
+ return;
129
+ }
130
+
131
+ // Emit as if it came from the WebSocket
132
+ this.emit("message", JSON.stringify(data));
133
+ }
134
+
135
+ /**
136
+ * Simulates an error (for orchestrator to inject errors)
137
+ * @param {Error} error - Error object
138
+ */
139
+ injectError(error) {
140
+ this.emit("error", error);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * OrchestratorProxyWriter
146
+ *
147
+ * Wraps OrchestratorProxySocket to match the WebSocketWriter interface
148
+ * used in server/index.js handleChatConnection
149
+ */
150
+ export class OrchestratorProxyWriter {
151
+ /**
152
+ * Creates a new proxy writer
153
+ * @param {OrchestratorProxySocket} proxySocket - The proxy socket
154
+ */
155
+ constructor(proxySocket) {
156
+ this.proxySocket = proxySocket;
157
+ this.sessionId = null;
158
+ this.isWebSocketWriter = true;
159
+ }
160
+
161
+ /**
162
+ * Sends data through the proxy
163
+ * @param {Object} data - Data to send
164
+ */
165
+ send(data) {
166
+ if (this.proxySocket.readyState === this.proxySocket.OPEN) {
167
+ // Send as JSON (matches WebSocketWriter behavior)
168
+ this.proxySocket.send(data);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Sets the session ID
174
+ * @param {string} sessionId - Session ID
175
+ */
176
+ setSessionId(sessionId) {
177
+ this.sessionId = sessionId;
178
+ }
179
+
180
+ /**
181
+ * Gets the session ID
182
+ * @returns {string|null} Session ID
183
+ */
184
+ getSessionId() {
185
+ return this.sessionId;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Creates a request handler for orchestrator user requests
191
+ *
192
+ * This factory creates a handler that:
193
+ * 1. Authenticates the request using pass-through GitHub OAuth
194
+ * 2. Creates a proxy socket for each request
195
+ * 3. Routes the request to the appropriate existing handler
196
+ * 4. Manages the proxy socket lifecycle
197
+ *
198
+ * @param {Object} handlers - Object containing handler functions
199
+ * @param {Function} handlers.handleChatMessage - Function to handle chat messages
200
+ * @param {Object} statusHooks - Status tracking hooks
201
+ * @param {Object} [authConfig] - Authentication configuration override
202
+ * @returns {Object} Object with handleUserRequest and setupFollowUpListener functions
203
+ */
204
+ export function createUserRequestHandler(
205
+ handlers,
206
+ statusHooks,
207
+ authConfig = null,
208
+ ) {
209
+ // Track active proxy sockets by request ID
210
+ const activeProxySockets = new Map();
211
+
212
+ // Initialize GitHub auth validator from env or config
213
+ const githubAuth = authConfig || createGitHubAuthFromEnv();
214
+
215
+ // Track if follow-up listener has been registered
216
+ let followUpListenerRegistered = false;
217
+
218
+ /**
219
+ * Sets up the follow-up message listener on the orchestrator client
220
+ * @param {OrchestratorClient} orchestratorClient - The orchestrator client
221
+ */
222
+ function setupFollowUpListener(orchestratorClient) {
223
+ if (followUpListenerRegistered) {
224
+ return;
225
+ }
226
+
227
+ // Listen for follow-up messages
228
+ orchestratorClient.on(
229
+ InboundMessageTypes.USER_REQUEST_FOLLOW_UP,
230
+ (message) => {
231
+ const { request_id: requestId, payload } = message;
232
+ const handled = handleFollowUpMessage(
233
+ activeProxySockets,
234
+ requestId,
235
+ payload || {},
236
+ );
237
+ if (!handled) {
238
+ console.warn(
239
+ `[ORCHESTRATOR-PROXY] No active session found for follow-up request ${requestId}`,
240
+ );
241
+ }
242
+ },
243
+ );
244
+
245
+ followUpListenerRegistered = true;
246
+ console.log("[ORCHESTRATOR-PROXY] Follow-up message listener registered");
247
+ }
248
+
249
+ async function handleUserRequest(orchestratorClient, message) {
250
+ const { request_id: requestId, action } = message;
251
+ // Initialize payload to prevent undefined mutation errors
252
+ let payload = message.payload || {};
253
+
254
+ // Ensure follow-up listener is registered
255
+ setupFollowUpListener(orchestratorClient);
256
+
257
+ // Check if authentication is required (GitHub auth configured)
258
+ if (githubAuth.isConfigured) {
259
+ // Extract auth token from payload
260
+ const authToken = payload.auth_token;
261
+
262
+ // Validate the token
263
+ const authResult = await githubAuth.validate(authToken);
264
+
265
+ if (!authResult.authenticated) {
266
+ console.warn(
267
+ `[ORCHESTRATOR-PROXY] Authentication failed for request ${requestId}: ${authResult.error}`,
268
+ );
269
+ // Send auth error back through orchestrator
270
+ const errorMsg = createAuthErrorMessage(requestId, authResult.error);
271
+ orchestratorClient.ws?.send(JSON.stringify(errorMsg));
272
+ return null;
273
+ }
274
+
275
+ // Authentication succeeded - attach user info to payload
276
+ console.log(
277
+ `[ORCHESTRATOR-PROXY] Authenticated user: ${authResult.user.username} (${authResult.user.authMethod})`,
278
+ );
279
+ // Use the validated user info
280
+ payload.user = authResult.user;
281
+ }
282
+
283
+ // Create proxy socket for this request
284
+ const proxySocket = new OrchestratorProxySocket(
285
+ orchestratorClient,
286
+ requestId,
287
+ { user: payload?.user },
288
+ );
289
+
290
+ // Track connection for status
291
+ const connectionId = `orchestrator-${requestId}`;
292
+ statusHooks?.onConnectionOpen?.(connectionId);
293
+
294
+ // Store in active map
295
+ activeProxySockets.set(requestId, proxySocket);
296
+
297
+ // Clean up on close
298
+ proxySocket.on("close", () => {
299
+ activeProxySockets.delete(requestId);
300
+ statusHooks?.onConnectionClose?.(connectionId);
301
+ });
302
+
303
+ // Create writer wrapper
304
+ const writer = new OrchestratorProxyWriter(proxySocket);
305
+
306
+ // Route based on action
307
+ switch (action) {
308
+ case "claude-command":
309
+ case "cursor-command":
310
+ case "codex-command":
311
+ case "abort-session":
312
+ case "claude-permission-response":
313
+ case "cursor-abort":
314
+ case "check-session-status":
315
+ case "get-active-sessions":
316
+ // Inject the message as if it came from a real WebSocket
317
+ // The existing handler will process it via the 'message' event
318
+ if (handlers.handleChatMessage) {
319
+ handlers.handleChatMessage(
320
+ proxySocket,
321
+ writer,
322
+ JSON.stringify({ ...payload, type: action }),
323
+ );
324
+ }
325
+ break;
326
+
327
+ case "close":
328
+ // Handle close request
329
+ proxySocket.close(payload?.code, payload?.reason);
330
+ break;
331
+
332
+ default:
333
+ console.warn(`[ORCHESTRATOR-PROXY] Unknown action: ${action}`);
334
+ orchestratorClient.sendError(requestId, `Unknown action: ${action}`);
335
+ proxySocket.close(4000, "Unknown action");
336
+ }
337
+
338
+ return proxySocket;
339
+ }
340
+
341
+ // Return both the handler and setup function
342
+ // The handler also auto-registers the listener on first call for convenience
343
+ return handleUserRequest;
344
+ }
345
+
346
+ /**
347
+ * Handles follow-up messages for an existing proxied session
348
+ *
349
+ * @param {Map} activeProxySockets - Map of active proxy sockets
350
+ * @param {string} requestId - Request ID
351
+ * @param {Object} payload - Message payload
352
+ * @returns {boolean} True if handled, false if socket not found
353
+ */
354
+ export function handleFollowUpMessage(activeProxySockets, requestId, payload) {
355
+ const proxySocket = activeProxySockets.get(requestId);
356
+ if (!proxySocket) {
357
+ return false;
358
+ }
359
+
360
+ proxySocket.injectMessage(payload);
361
+ return true;
362
+ }
363
+
364
+ export default OrchestratorProxySocket;