renote-server 1.0.1
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/__tests__/auth.test.js +49 -0
- package/dist/__tests__/watcher.test.js +58 -0
- package/dist/claude/sessionBrowser.js +689 -0
- package/dist/claude/watcher.js +242 -0
- package/dist/config.js +17 -0
- package/dist/files/browser.js +127 -0
- package/dist/files/reader.js +159 -0
- package/dist/files/search.js +124 -0
- package/dist/git/gitHandler.js +95 -0
- package/dist/git/gitService.js +237 -0
- package/dist/git/index.js +8 -0
- package/dist/http/server.js +28 -0
- package/dist/index.js +77 -0
- package/dist/ssh/index.js +9 -0
- package/dist/ssh/sshHandler.js +205 -0
- package/dist/ssh/sshManager.js +329 -0
- package/dist/terminal/index.js +11 -0
- package/dist/terminal/localTerminalHandler.js +144 -0
- package/dist/terminal/localTerminalManager.js +465 -0
- package/dist/terminal/terminalWebSocket.js +128 -0
- package/dist/types.js +2 -0
- package/dist/utils/logger.js +42 -0
- package/dist/websocket/auth.js +18 -0
- package/dist/websocket/server.js +512 -0
- package/package.json +64 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WebSocketServer = void 0;
|
|
7
|
+
const ws_1 = __importDefault(require("ws"));
|
|
8
|
+
const http_1 = require("http");
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
const auth_1 = require("./auth");
|
|
12
|
+
const browser_1 = require("../files/browser");
|
|
13
|
+
const reader_1 = require("../files/reader");
|
|
14
|
+
const search_1 = require("../files/search");
|
|
15
|
+
const sessionBrowser_1 = require("../claude/sessionBrowser");
|
|
16
|
+
const terminal_1 = require("../terminal");
|
|
17
|
+
const git_1 = require("../git");
|
|
18
|
+
const terminalWebSocket_1 = require("../terminal/terminalWebSocket");
|
|
19
|
+
class WebSocketServer {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.clients = new Map();
|
|
22
|
+
const server = (0, http_1.createServer)();
|
|
23
|
+
// Use noServer mode for path-based routing
|
|
24
|
+
this.wss = new ws_1.default.Server({ noServer: true });
|
|
25
|
+
this.authManager = new auth_1.AuthManager();
|
|
26
|
+
this.terminalHandler = new terminal_1.LocalTerminalHandler(this.send.bind(this));
|
|
27
|
+
this.gitHandler = new git_1.GitHandler(this.send.bind(this));
|
|
28
|
+
// Handle HTTP upgrade requests
|
|
29
|
+
server.on('upgrade', (request, socket, head) => {
|
|
30
|
+
// Route /terminal to terminal direct WebSocket handler
|
|
31
|
+
if (terminalWebSocket_1.terminalWebSocketHandler.shouldHandle(request)) {
|
|
32
|
+
const terminalWss = new ws_1.default.Server({ noServer: true });
|
|
33
|
+
terminalWss.handleUpgrade(request, socket, head, (ws) => {
|
|
34
|
+
terminalWebSocket_1.terminalWebSocketHandler.handleConnection(ws, request);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Default: main WebSocket for JSON-RPC style messages
|
|
39
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
40
|
+
this.wss.emit('connection', ws, request);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
this.setupWebSocket();
|
|
45
|
+
server.listen(config_1.CONFIG.port, () => {
|
|
46
|
+
logger_1.logger.info(`WebSocket server running on port ${config_1.CONFIG.port}`);
|
|
47
|
+
logger_1.logger.info(` - Main API: ws://host:${config_1.CONFIG.port}/`);
|
|
48
|
+
logger_1.logger.info(` - Terminal direct: ws://host:${config_1.CONFIG.port}/terminal`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
setupWebSocket() {
|
|
52
|
+
this.wss.on('connection', (ws) => {
|
|
53
|
+
logger_1.logger.info('New client connection attempt');
|
|
54
|
+
ws.on('message', async (data) => {
|
|
55
|
+
try {
|
|
56
|
+
const message = JSON.parse(data.toString());
|
|
57
|
+
await this.handleMessage(ws, message);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logger_1.logger.error('Error parsing message:', error);
|
|
61
|
+
this.sendError(ws, 'Invalid message format');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
ws.on('close', () => {
|
|
65
|
+
const clientId = this.getClientId(ws);
|
|
66
|
+
if (clientId) {
|
|
67
|
+
this.terminalHandler.cleanup(clientId);
|
|
68
|
+
(0, sessionBrowser_1.unwatchSession)(clientId);
|
|
69
|
+
this.clients.delete(clientId);
|
|
70
|
+
logger_1.logger.info(`Client ${clientId} disconnected`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
ws.on('error', (error) => {
|
|
74
|
+
logger_1.logger.error('WebSocket error:', error);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async handleMessage(ws, message) {
|
|
79
|
+
switch (message.type) {
|
|
80
|
+
case 'auth':
|
|
81
|
+
await this.handleAuth(ws, message.token);
|
|
82
|
+
break;
|
|
83
|
+
case 'ping':
|
|
84
|
+
// Heartbeat: respond immediately with pong
|
|
85
|
+
this.send(ws, { type: 'pong', timestamp: message.timestamp });
|
|
86
|
+
break;
|
|
87
|
+
case 'file_tree':
|
|
88
|
+
await this.handleFileTree(ws, message);
|
|
89
|
+
break;
|
|
90
|
+
case 'file_read':
|
|
91
|
+
await this.handleFileRead(ws, message);
|
|
92
|
+
break;
|
|
93
|
+
case 'search':
|
|
94
|
+
await this.handleSearch(ws, message);
|
|
95
|
+
break;
|
|
96
|
+
case 'list_workspaces':
|
|
97
|
+
await this.handleListWorkspaces(ws);
|
|
98
|
+
break;
|
|
99
|
+
case 'list_sessions':
|
|
100
|
+
await this.handleListSessions(ws, message);
|
|
101
|
+
break;
|
|
102
|
+
case 'get_session_messages':
|
|
103
|
+
await this.handleGetSessionMessages(ws, message);
|
|
104
|
+
break;
|
|
105
|
+
case 'get_session_messages_page':
|
|
106
|
+
await this.handleGetSessionMessagesPage(ws, message);
|
|
107
|
+
break;
|
|
108
|
+
case 'watch_session':
|
|
109
|
+
await this.handleWatchSession(ws, message);
|
|
110
|
+
break;
|
|
111
|
+
case 'unwatch_session':
|
|
112
|
+
this.handleUnwatchSession(ws);
|
|
113
|
+
break;
|
|
114
|
+
case 'list_subagents':
|
|
115
|
+
await this.handleListSubagents(ws, message);
|
|
116
|
+
break;
|
|
117
|
+
case 'get_subagent_messages':
|
|
118
|
+
await this.handleGetSubagentMessages(ws, message);
|
|
119
|
+
break;
|
|
120
|
+
case 'list_tool_results':
|
|
121
|
+
await this.handleListToolResults(ws, message);
|
|
122
|
+
break;
|
|
123
|
+
case 'get_tool_result_content':
|
|
124
|
+
await this.handleGetToolResultContent(ws, message);
|
|
125
|
+
break;
|
|
126
|
+
case 'get_session_folder_info':
|
|
127
|
+
await this.handleGetSessionFolderInfo(ws, message);
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
const clientId = this.getClientId(ws);
|
|
131
|
+
if (!clientId) {
|
|
132
|
+
this.sendError(ws, 'Not authenticated');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Route terminal messages to terminalHandler
|
|
136
|
+
if (this.terminalHandler.canHandle(message.type)) {
|
|
137
|
+
await this.terminalHandler.handle(ws, clientId, message);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Route Git messages to gitHandler
|
|
141
|
+
if (this.gitHandler.canHandle(message.type)) {
|
|
142
|
+
await this.gitHandler.handle(ws, clientId, message);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
logger_1.logger.warn(`Unknown message type: ${message.type}`);
|
|
146
|
+
this.sendError(ws, 'Unknown message type');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async handleFileTree(ws, message) {
|
|
150
|
+
const clientId = this.getClientId(ws);
|
|
151
|
+
if (!clientId) {
|
|
152
|
+
this.sendError(ws, 'Not authenticated');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const path = message.path || process.cwd();
|
|
157
|
+
const tree = await browser_1.fileBrowser.generateTree(path);
|
|
158
|
+
this.send(ws, {
|
|
159
|
+
type: 'file_tree_response',
|
|
160
|
+
data: tree,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
logger_1.logger.error('Error generating file tree:', error);
|
|
165
|
+
this.sendError(ws, `Failed to generate file tree: ${error}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async handleFileRead(ws, message) {
|
|
169
|
+
const clientId = this.getClientId(ws);
|
|
170
|
+
if (!clientId) {
|
|
171
|
+
this.sendError(ws, 'Not authenticated');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const { path } = message;
|
|
176
|
+
if (!path) {
|
|
177
|
+
this.sendError(ws, 'File path is required');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const fileContent = await reader_1.fileReader.readFile(path);
|
|
181
|
+
this.send(ws, {
|
|
182
|
+
type: 'file_read_response',
|
|
183
|
+
data: fileContent,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
logger_1.logger.error('Error reading file:', error);
|
|
188
|
+
this.sendError(ws, `Failed to read file: ${error}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async handleSearch(ws, message) {
|
|
192
|
+
const clientId = this.getClientId(ws);
|
|
193
|
+
if (!clientId) {
|
|
194
|
+
this.sendError(ws, 'Not authenticated');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const { query, path, options } = message;
|
|
199
|
+
if (!query) {
|
|
200
|
+
this.sendError(ws, 'Search query is required');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const searchPath = path || process.cwd();
|
|
204
|
+
const results = await search_1.searchService.search(query, searchPath, options);
|
|
205
|
+
this.send(ws, {
|
|
206
|
+
type: 'search_response',
|
|
207
|
+
data: {
|
|
208
|
+
query,
|
|
209
|
+
results,
|
|
210
|
+
count: results.length,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
logger_1.logger.error('Error searching:', error);
|
|
216
|
+
this.sendError(ws, `Search failed: ${error}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async handleListWorkspaces(ws) {
|
|
220
|
+
const clientId = this.getClientId(ws);
|
|
221
|
+
if (!clientId) {
|
|
222
|
+
this.sendError(ws, 'Not authenticated');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const workspaces = await (0, sessionBrowser_1.listWorkspaces)();
|
|
227
|
+
this.send(ws, {
|
|
228
|
+
type: 'list_workspaces_response',
|
|
229
|
+
data: workspaces,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
logger_1.logger.error('Error listing workspaces:', error);
|
|
234
|
+
this.sendError(ws, `Failed to list workspaces: ${error}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async handleListSessions(ws, message) {
|
|
238
|
+
const clientId = this.getClientId(ws);
|
|
239
|
+
if (!clientId) {
|
|
240
|
+
this.sendError(ws, 'Not authenticated');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const { workspace } = message;
|
|
245
|
+
if (!workspace) {
|
|
246
|
+
this.sendError(ws, 'Workspace is required');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const sessions = await (0, sessionBrowser_1.listSessions)(workspace);
|
|
250
|
+
this.send(ws, {
|
|
251
|
+
type: 'list_sessions_response',
|
|
252
|
+
data: sessions,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
logger_1.logger.error('Error listing sessions:', error);
|
|
257
|
+
this.sendError(ws, `Failed to list sessions: ${error}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async handleGetSessionMessages(ws, message) {
|
|
261
|
+
const clientId = this.getClientId(ws);
|
|
262
|
+
if (!clientId) {
|
|
263
|
+
this.sendError(ws, 'Not authenticated');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const { workspace, sessionId } = message;
|
|
268
|
+
if (!workspace || !sessionId) {
|
|
269
|
+
this.sendError(ws, 'Workspace and sessionId are required');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const messages = await (0, sessionBrowser_1.getSessionMessages)(workspace, sessionId);
|
|
273
|
+
this.send(ws, {
|
|
274
|
+
type: 'get_session_messages_response',
|
|
275
|
+
data: messages,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
logger_1.logger.error('Error getting session messages:', error);
|
|
280
|
+
this.sendError(ws, `Failed to get session messages: ${error}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async handleGetSessionMessagesPage(ws, message) {
|
|
284
|
+
const clientId = this.getClientId(ws);
|
|
285
|
+
if (!clientId) {
|
|
286
|
+
this.sendError(ws, 'Not authenticated');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const { workspace, sessionId, limit, beforeIndex } = message;
|
|
291
|
+
if (!workspace || !sessionId) {
|
|
292
|
+
this.sendError(ws, 'Workspace and sessionId are required');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const page = await (0, sessionBrowser_1.getSessionMessagesPage)(workspace, sessionId, limit || 50, beforeIndex);
|
|
296
|
+
const isInitial = beforeIndex === undefined || beforeIndex === null;
|
|
297
|
+
this.send(ws, {
|
|
298
|
+
type: 'get_session_messages_page_response',
|
|
299
|
+
data: {
|
|
300
|
+
messages: page.messages,
|
|
301
|
+
hasMore: page.hasMore,
|
|
302
|
+
oldestIndex: page.oldestIndex,
|
|
303
|
+
totalCount: page.totalCount,
|
|
304
|
+
isInitial,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
logger_1.logger.error('Error getting session messages page:', error);
|
|
310
|
+
this.sendError(ws, `Failed to get session messages page: ${error}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async handleWatchSession(ws, message) {
|
|
314
|
+
const clientId = this.getClientId(ws);
|
|
315
|
+
if (!clientId) {
|
|
316
|
+
this.sendError(ws, 'Not authenticated');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const { workspace, sessionId } = message;
|
|
321
|
+
if (!workspace || !sessionId) {
|
|
322
|
+
this.sendError(ws, 'Workspace and sessionId are required');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await (0, sessionBrowser_1.watchSession)(clientId, workspace, sessionId, (newMessages) => {
|
|
326
|
+
this.send(ws, {
|
|
327
|
+
type: 'session_update',
|
|
328
|
+
data: newMessages,
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
this.send(ws, {
|
|
332
|
+
type: 'watch_session_response',
|
|
333
|
+
data: { success: true },
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
logger_1.logger.error('Error watching session:', error);
|
|
338
|
+
this.sendError(ws, `Failed to watch session: ${error}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
handleUnwatchSession(ws) {
|
|
342
|
+
const clientId = this.getClientId(ws);
|
|
343
|
+
if (!clientId) {
|
|
344
|
+
this.sendError(ws, 'Not authenticated');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
(0, sessionBrowser_1.unwatchSession)(clientId);
|
|
348
|
+
this.send(ws, {
|
|
349
|
+
type: 'unwatch_session_response',
|
|
350
|
+
data: { success: true },
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async handleListSubagents(ws, message) {
|
|
354
|
+
const clientId = this.getClientId(ws);
|
|
355
|
+
if (!clientId) {
|
|
356
|
+
this.sendError(ws, 'Not authenticated');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const { workspace, sessionId } = message;
|
|
361
|
+
if (!workspace || !sessionId) {
|
|
362
|
+
this.sendError(ws, 'Workspace and sessionId are required');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const subagents = await (0, sessionBrowser_1.listSubagents)(workspace, sessionId);
|
|
366
|
+
this.send(ws, {
|
|
367
|
+
type: 'list_subagents_response',
|
|
368
|
+
data: subagents,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
logger_1.logger.error('Error listing subagents:', error);
|
|
373
|
+
this.sendError(ws, `Failed to list subagents: ${error}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async handleGetSubagentMessages(ws, message) {
|
|
377
|
+
const clientId = this.getClientId(ws);
|
|
378
|
+
if (!clientId) {
|
|
379
|
+
this.sendError(ws, 'Not authenticated');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const { workspace, sessionId, agentId } = message;
|
|
384
|
+
if (!workspace || !sessionId || !agentId) {
|
|
385
|
+
this.sendError(ws, 'Workspace, sessionId, and agentId are required');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const messages = await (0, sessionBrowser_1.getSubagentMessages)(workspace, sessionId, agentId);
|
|
389
|
+
this.send(ws, {
|
|
390
|
+
type: 'get_subagent_messages_response',
|
|
391
|
+
data: messages,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
logger_1.logger.error('Error getting subagent messages:', error);
|
|
396
|
+
this.sendError(ws, `Failed to get subagent messages: ${error}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async handleListToolResults(ws, message) {
|
|
400
|
+
const clientId = this.getClientId(ws);
|
|
401
|
+
if (!clientId) {
|
|
402
|
+
this.sendError(ws, 'Not authenticated');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const { workspace, sessionId } = message;
|
|
407
|
+
if (!workspace || !sessionId) {
|
|
408
|
+
this.sendError(ws, 'Workspace and sessionId are required');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const results = await (0, sessionBrowser_1.listToolResults)(workspace, sessionId);
|
|
412
|
+
this.send(ws, {
|
|
413
|
+
type: 'list_tool_results_response',
|
|
414
|
+
data: results,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
logger_1.logger.error('Error listing tool results:', error);
|
|
419
|
+
this.sendError(ws, `Failed to list tool results: ${error}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async handleGetToolResultContent(ws, message) {
|
|
423
|
+
const clientId = this.getClientId(ws);
|
|
424
|
+
if (!clientId) {
|
|
425
|
+
this.sendError(ws, 'Not authenticated');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const { workspace, sessionId, toolUseId } = message;
|
|
430
|
+
if (!workspace || !sessionId || !toolUseId) {
|
|
431
|
+
this.sendError(ws, 'Workspace, sessionId, and toolUseId are required');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const content = await (0, sessionBrowser_1.getToolResultContent)(workspace, sessionId, toolUseId);
|
|
435
|
+
this.send(ws, {
|
|
436
|
+
type: 'get_tool_result_content_response',
|
|
437
|
+
data: { toolUseId, content },
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
logger_1.logger.error('Error getting tool result content:', error);
|
|
442
|
+
this.sendError(ws, `Failed to get tool result content: ${error}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async handleGetSessionFolderInfo(ws, message) {
|
|
446
|
+
const clientId = this.getClientId(ws);
|
|
447
|
+
if (!clientId) {
|
|
448
|
+
this.sendError(ws, 'Not authenticated');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const { workspace, sessionId } = message;
|
|
453
|
+
if (!workspace || !sessionId) {
|
|
454
|
+
this.sendError(ws, 'Workspace and sessionId are required');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const info = await (0, sessionBrowser_1.getSessionFolderInfo)(workspace, sessionId);
|
|
458
|
+
this.send(ws, {
|
|
459
|
+
type: 'get_session_folder_info_response',
|
|
460
|
+
data: info,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
logger_1.logger.error('Error getting session folder info:', error);
|
|
465
|
+
this.sendError(ws, `Failed to get session folder info: ${error}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async handleAuth(ws, token) {
|
|
469
|
+
if (this.authManager.validateToken(token)) {
|
|
470
|
+
const clientId = this.authManager.generateClientId();
|
|
471
|
+
this.clients.set(clientId, ws);
|
|
472
|
+
ws.clientId = clientId;
|
|
473
|
+
this.send(ws, {
|
|
474
|
+
type: 'auth_success',
|
|
475
|
+
data: { clientId }
|
|
476
|
+
});
|
|
477
|
+
logger_1.logger.info(`Client ${clientId} authenticated`);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
this.sendError(ws, 'Invalid token');
|
|
481
|
+
ws.close();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
getClientId(ws) {
|
|
485
|
+
return ws.clientId || null;
|
|
486
|
+
}
|
|
487
|
+
broadcast(message) {
|
|
488
|
+
const data = JSON.stringify(message);
|
|
489
|
+
let count = 0;
|
|
490
|
+
this.clients.forEach((ws) => {
|
|
491
|
+
if (ws.readyState === ws_1.default.OPEN) {
|
|
492
|
+
ws.send(data);
|
|
493
|
+
count++;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
if (count > 0) {
|
|
497
|
+
logger_1.logger.debug(`Broadcast to ${count} clients: ${message.type}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
send(ws, message) {
|
|
501
|
+
if (ws.readyState === ws_1.default.OPEN) {
|
|
502
|
+
ws.send(JSON.stringify(message));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
sendError(ws, error) {
|
|
506
|
+
this.send(ws, { type: 'error', error });
|
|
507
|
+
}
|
|
508
|
+
getClientCount() {
|
|
509
|
+
return this.clients.size;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
exports.WebSocketServer = WebSocketServer;
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "renote-server",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "WebSocket server for Renote - mobile remote development client with Claude Code integration",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"renote-server": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/**/*"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "nodemon --exec ts-node src/index.ts",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"test": "jest",
|
|
17
|
+
"test:watch": "jest --watch",
|
|
18
|
+
"test:coverage": "jest --coverage",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"postinstall": "cd node_modules/node-pty && npx node-gyp rebuild 2>/dev/null || true"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chokidar": "^5.0.0",
|
|
28
|
+
"dotenv": "^17.2.3",
|
|
29
|
+
"express": "^5.2.1",
|
|
30
|
+
"node-pty": "^1.1.0",
|
|
31
|
+
"ssh2": "^1.17.0",
|
|
32
|
+
"ws": "^8.19.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/express": "^5.0.6",
|
|
36
|
+
"@types/jest": "^30.0.0",
|
|
37
|
+
"@types/node": "^25.2.0",
|
|
38
|
+
"@types/ssh2": "^1.15.5",
|
|
39
|
+
"@types/ws": "^8.18.1",
|
|
40
|
+
"jest": "^30.2.0",
|
|
41
|
+
"nodemon": "^3.1.11",
|
|
42
|
+
"ts-jest": "^29.4.6",
|
|
43
|
+
"ts-node": "^10.9.2",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
},
|
|
46
|
+
"jest": {
|
|
47
|
+
"preset": "ts-jest",
|
|
48
|
+
"testEnvironment": "node",
|
|
49
|
+
"testMatch": [
|
|
50
|
+
"**/__tests__/**/*.test.ts"
|
|
51
|
+
],
|
|
52
|
+
"collectCoverageFrom": [
|
|
53
|
+
"src/**/*.ts",
|
|
54
|
+
"!src/**/*.d.ts",
|
|
55
|
+
"!src/__tests__/**"
|
|
56
|
+
],
|
|
57
|
+
"transformIgnorePatterns": [
|
|
58
|
+
"node_modules/(?!(chokidar)/)"
|
|
59
|
+
],
|
|
60
|
+
"moduleNameMapper": {
|
|
61
|
+
"^chokidar$": "<rootDir>/node_modules/chokidar/index.js"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|