@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.
- package/LICENSE +675 -0
- package/README.md +414 -0
- package/dist/api-docs.html +879 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-DfR9xEkp.css +32 -0
- package/dist/assets/index-DvlVn6Eb.js +1231 -0
- package/dist/assets/vendor-codemirror-CJLzwpLB.js +39 -0
- package/dist/assets/vendor-react-DcyRfQm3.js +59 -0
- package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
- package/dist/clear-cache.html +85 -0
- package/dist/convert-icons.md +53 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +9 -0
- package/dist/generate-icons.js +49 -0
- package/dist/icons/claude-ai-icon.svg +1 -0
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/icons/cursor.svg +1 -0
- package/dist/icons/generate-icons.md +19 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-128x128.svg +12 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-144x144.svg +12 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-152x152.svg +12 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-192x192.svg +12 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-384x384.svg +12 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-512x512.svg +12 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-72x72.svg +12 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/icon-96x96.svg +12 -0
- package/dist/icons/icon-template.svg +12 -0
- package/dist/index.html +52 -0
- package/dist/logo-128.png +0 -0
- package/dist/logo-256.png +0 -0
- package/dist/logo-32.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-64.png +0 -0
- package/dist/logo.svg +17 -0
- package/dist/manifest.json +61 -0
- package/dist/screenshots/cli-selection.png +0 -0
- package/dist/screenshots/desktop-main.png +0 -0
- package/dist/screenshots/mobile-chat.png +0 -0
- package/dist/screenshots/tools-modal.png +0 -0
- package/dist/sw.js +107 -0
- package/package.json +120 -0
- package/server/claude-sdk.js +721 -0
- package/server/cli.js +469 -0
- package/server/cursor-cli.js +267 -0
- package/server/database/db.js +554 -0
- package/server/database/init.sql +54 -0
- package/server/index.js +2120 -0
- package/server/middleware/auth.js +161 -0
- package/server/openai-codex.js +389 -0
- package/server/orchestrator/client.js +989 -0
- package/server/orchestrator/github-auth.js +308 -0
- package/server/orchestrator/index.js +216 -0
- package/server/orchestrator/protocol.js +299 -0
- package/server/orchestrator/proxy.js +364 -0
- package/server/orchestrator/status-tracker.js +226 -0
- package/server/projects.js +1604 -0
- package/server/routes/agent.js +1230 -0
- package/server/routes/auth.js +135 -0
- package/server/routes/cli-auth.js +341 -0
- package/server/routes/codex.js +345 -0
- package/server/routes/commands.js +521 -0
- package/server/routes/cursor.js +795 -0
- package/server/routes/git.js +1128 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +650 -0
- package/server/routes/projects.js +378 -0
- package/server/routes/settings.js +178 -0
- package/server/routes/taskmaster.js +1963 -0
- package/server/routes/user.js +106 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/gitConfig.js +24 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- 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;
|