renote-server 1.0.1 → 1.0.3

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.
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.claudeChatService = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const logger_1 = require("../utils/logger");
6
+ class ClaudeChatService {
7
+ constructor() {
8
+ // 存储正在运行的进程,按 sessionId 索引
9
+ this.runningProcesses = new Map();
10
+ }
11
+ /**
12
+ * 发送消息给 Claude CLI
13
+ * 使用 -p (print) 模式,消息通过 stdin 传入,响应通过 jsonl 文件监听获取
14
+ */
15
+ async sendMessage(options) {
16
+ const { workspaceDirName, sessionId, newSessionId, message, cwd, allowedTools } = options;
17
+ // 确定实际使用的 sessionId
18
+ const effectiveSessionId = sessionId || newSessionId;
19
+ // 检查该会话是否已有进程在运行
20
+ if (effectiveSessionId && this.runningProcesses.has(effectiveSessionId)) {
21
+ return {
22
+ success: false,
23
+ error: 'A message is already being processed for this session',
24
+ sessionId: effectiveSessionId,
25
+ };
26
+ }
27
+ const args = ['-p'];
28
+ // 恢复现有会话
29
+ if (sessionId) {
30
+ args.push('--resume', sessionId);
31
+ }
32
+ // 新建会话并指定 ID
33
+ else if (newSessionId) {
34
+ args.push('--session-id', newSessionId);
35
+ }
36
+ // 添加允许的工具参数
37
+ if (allowedTools && allowedTools.length > 0) {
38
+ args.push('--allowedTools', allowedTools.join(','));
39
+ }
40
+ logger_1.logger.info(`[ChatService] Sending message to Claude CLI`);
41
+ logger_1.logger.info(`[ChatService] Args: ${args.join(' ')}`);
42
+ logger_1.logger.info(`[ChatService] CWD: ${cwd || 'default'}`);
43
+ logger_1.logger.info(`[ChatService] Message length: ${message.length}`);
44
+ if (allowedTools && allowedTools.length > 0) {
45
+ logger_1.logger.info(`[ChatService] Allowed tools: ${allowedTools.join(', ')}`);
46
+ }
47
+ return new Promise((resolve) => {
48
+ const proc = (0, child_process_1.spawn)('claude', args, {
49
+ cwd: cwd || process.cwd(),
50
+ stdio: ['pipe', 'pipe', 'pipe'],
51
+ env: {
52
+ ...process.env,
53
+ // 确保 Claude CLI 不会等待用户输入
54
+ TERM: 'dumb',
55
+ },
56
+ });
57
+ // 记录进程
58
+ if (effectiveSessionId) {
59
+ this.runningProcesses.set(effectiveSessionId, proc);
60
+ }
61
+ // 写入消息到 stdin
62
+ proc.stdin.write(message);
63
+ proc.stdin.end();
64
+ // 收集输出(主要用于错误检测)
65
+ let stdout = '';
66
+ let stderr = '';
67
+ proc.stdout.on('data', (data) => {
68
+ stdout += data.toString();
69
+ // 可以选择性地记录 stdout,但主要响应通过 jsonl 监听获取
70
+ logger_1.logger.debug(`[ChatService] stdout: ${data.toString().substring(0, 200)}...`);
71
+ });
72
+ proc.stderr.on('data', (data) => {
73
+ stderr += data.toString();
74
+ logger_1.logger.warn(`[ChatService] stderr: ${data.toString()}`);
75
+ });
76
+ proc.on('close', (code) => {
77
+ // 移除进程记录
78
+ if (effectiveSessionId) {
79
+ this.runningProcesses.delete(effectiveSessionId);
80
+ }
81
+ if (code === 0) {
82
+ logger_1.logger.info(`[ChatService] Claude CLI completed successfully`);
83
+ resolve({
84
+ success: true,
85
+ sessionId: effectiveSessionId,
86
+ });
87
+ }
88
+ else {
89
+ logger_1.logger.error(`[ChatService] Claude CLI exited with code ${code}`);
90
+ resolve({
91
+ success: false,
92
+ error: stderr || `Exit code: ${code}`,
93
+ sessionId: effectiveSessionId,
94
+ });
95
+ }
96
+ });
97
+ proc.on('error', (err) => {
98
+ // 移除进程记录
99
+ if (effectiveSessionId) {
100
+ this.runningProcesses.delete(effectiveSessionId);
101
+ }
102
+ logger_1.logger.error(`[ChatService] Failed to spawn Claude CLI:`, err);
103
+ resolve({
104
+ success: false,
105
+ error: err.message,
106
+ sessionId: effectiveSessionId,
107
+ });
108
+ });
109
+ });
110
+ }
111
+ /**
112
+ * 检查指定会话是否有正在进行的消息处理
113
+ */
114
+ isProcessing(sessionId) {
115
+ return this.runningProcesses.has(sessionId);
116
+ }
117
+ /**
118
+ * 取消正在进行的消息处理
119
+ */
120
+ cancelMessage(sessionId) {
121
+ const proc = this.runningProcesses.get(sessionId);
122
+ if (proc) {
123
+ proc.kill('SIGTERM');
124
+ this.runningProcesses.delete(sessionId);
125
+ logger_1.logger.info(`[ChatService] Cancelled message processing for session ${sessionId}`);
126
+ return true;
127
+ }
128
+ return false;
129
+ }
130
+ }
131
+ exports.claudeChatService = new ClaudeChatService();
package/dist/config.js CHANGED
@@ -4,13 +4,36 @@ exports.CONFIG = void 0;
4
4
  const dotenv_1 = require("dotenv");
5
5
  const os_1 = require("os");
6
6
  (0, dotenv_1.config)();
7
+ /**
8
+ * Parse CLI arguments: --port, --host, --token, --claude-home
9
+ * These take precedence over environment variables.
10
+ */
11
+ function parseArgs() {
12
+ const args = {};
13
+ const argv = process.argv.slice(2);
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const arg = argv[i];
16
+ if (arg.startsWith('--') && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
17
+ const key = arg.slice(2);
18
+ args[key] = argv[i + 1];
19
+ i++;
20
+ }
21
+ else if (arg.startsWith('--') && arg.includes('=')) {
22
+ const [key, value] = arg.slice(2).split('=');
23
+ args[key] = value;
24
+ }
25
+ }
26
+ return args;
27
+ }
28
+ const args = parseArgs();
7
29
  exports.CONFIG = {
8
- port: parseInt(process.env.PORT || '8080'),
9
- authToken: process.env.AUTH_TOKEN || '',
10
- claudeHome: process.env.CLAUDE_HOME || `${(0, os_1.homedir)()}/.claude`,
30
+ host: args['host'] || process.env.HOST || '0.0.0.0',
31
+ port: parseInt(args['port'] || process.env.PORT || '9080'),
32
+ authToken: args['token'] || process.env.AUTH_TOKEN || '',
33
+ claudeHome: args['claude-home'] || process.env.CLAUDE_HOME || `${(0, os_1.homedir)()}/.claude`,
11
34
  maxFileSize: parseInt(process.env.MAX_FILE_SIZE || '10485760'),
12
35
  searchTimeout: parseInt(process.env.SEARCH_TIMEOUT || '5000'),
13
- logLevel: process.env.LOG_LEVEL || 'info',
36
+ logLevel: args['log-level'] || process.env.LOG_LEVEL || 'info',
14
37
  };
15
38
  if (!exports.CONFIG.authToken) {
16
39
  console.warn('WARNING: AUTH_TOKEN not set. Generate: openssl rand -hex 32');
@@ -21,8 +21,8 @@ function createHttpServer() {
21
21
  res.json({ token: config_1.CONFIG.authToken });
22
22
  });
23
23
  const httpPort = config_1.CONFIG.port + 1;
24
- app.listen(httpPort, () => {
25
- logger_1.logger.info(`HTTP server running on port ${httpPort}`);
24
+ app.listen(httpPort, config_1.CONFIG.host, () => {
25
+ logger_1.logger.info(`HTTP server running on ${config_1.CONFIG.host}:${httpPort}`);
26
26
  });
27
27
  return app;
28
28
  }
package/dist/index.js CHANGED
File without changes
@@ -38,6 +38,8 @@ class TerminalWebSocketHandler {
38
38
  const type = (url.searchParams.get('type') || 'shell');
39
39
  const cols = parseInt(url.searchParams.get('cols') || '80', 10);
40
40
  const rows = parseInt(url.searchParams.get('rows') || '24', 10);
41
+ const claudeArgsParam = url.searchParams.get('claudeArgs');
42
+ const cwd = url.searchParams.get('cwd') || undefined;
41
43
  // Validate token
42
44
  if (!this.authManager.validateToken(token)) {
43
45
  logger_1.logger.warn('Terminal WebSocket: invalid token');
@@ -54,7 +56,13 @@ class TerminalWebSocketHandler {
54
56
  this.wsToSession.set(ws, { clientId, sessionId });
55
57
  logger_1.logger.info(`Terminal WebSocket connected: clientId=${clientId}, sessionId=${sessionId}, type=${type}, ${cols}x${rows}`);
56
58
  const connection = localTerminalManager_1.localTerminalManager.getOrCreateConnection(clientId);
57
- const options = { type, cols, rows };
59
+ const options = {
60
+ type,
61
+ cols,
62
+ rows,
63
+ cwd,
64
+ claudeArgs: claudeArgsParam ? JSON.parse(decodeURIComponent(claudeArgsParam)) : undefined,
65
+ };
58
66
  const success = connection.startTerminal(sessionId, (data) => {
59
67
  // Send terminal output as text frame
60
68
  if (ws.readyState === ws_1.default.OPEN) {
@@ -16,6 +16,7 @@ const sessionBrowser_1 = require("../claude/sessionBrowser");
16
16
  const terminal_1 = require("../terminal");
17
17
  const git_1 = require("../git");
18
18
  const terminalWebSocket_1 = require("../terminal/terminalWebSocket");
19
+ const chatService_1 = require("../claude/chatService");
19
20
  class WebSocketServer {
20
21
  constructor() {
21
22
  this.clients = new Map();
@@ -27,51 +28,60 @@ class WebSocketServer {
27
28
  this.gitHandler = new git_1.GitHandler(this.send.bind(this));
28
29
  // Handle HTTP upgrade requests
29
30
  server.on('upgrade', (request, socket, head) => {
31
+ const clientIp = request.socket.remoteAddress;
32
+ const clientPort = request.socket.remotePort;
33
+ logger_1.logger.info(`[WS Upgrade] Request from ${clientIp}:${clientPort}, URL: ${request.url}`);
30
34
  // Route /terminal to terminal direct WebSocket handler
31
35
  if (terminalWebSocket_1.terminalWebSocketHandler.shouldHandle(request)) {
36
+ logger_1.logger.info(`[WS Upgrade] Routing to terminal handler`);
32
37
  const terminalWss = new ws_1.default.Server({ noServer: true });
33
38
  terminalWss.handleUpgrade(request, socket, head, (ws) => {
39
+ logger_1.logger.info(`[WS Upgrade] Terminal upgrade complete`);
34
40
  terminalWebSocket_1.terminalWebSocketHandler.handleConnection(ws, request);
35
41
  });
36
42
  }
37
43
  else {
38
44
  // Default: main WebSocket for JSON-RPC style messages
45
+ logger_1.logger.info(`[WS Upgrade] Routing to main handler`);
39
46
  this.wss.handleUpgrade(request, socket, head, (ws) => {
47
+ logger_1.logger.info(`[WS Upgrade] Main upgrade complete`);
40
48
  this.wss.emit('connection', ws, request);
41
49
  });
42
50
  }
43
51
  });
44
52
  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`);
53
+ server.listen(config_1.CONFIG.port, config_1.CONFIG.host, () => {
54
+ logger_1.logger.info(`WebSocket server running on ${config_1.CONFIG.host}:${config_1.CONFIG.port}`);
55
+ logger_1.logger.info(` - Main API: ws://${config_1.CONFIG.host}:${config_1.CONFIG.port}/`);
56
+ logger_1.logger.info(` - Terminal direct: ws://${config_1.CONFIG.host}:${config_1.CONFIG.port}/terminal`);
49
57
  });
50
58
  }
51
59
  setupWebSocket() {
52
- this.wss.on('connection', (ws) => {
53
- logger_1.logger.info('New client connection attempt');
60
+ this.wss.on('connection', (ws, request) => {
61
+ const clientIp = request?.socket?.remoteAddress || 'unknown';
62
+ logger_1.logger.info(`[WS Connection] New client from ${clientIp}`);
54
63
  ws.on('message', async (data) => {
55
64
  try {
56
65
  const message = JSON.parse(data.toString());
66
+ logger_1.logger.info(`[WS Message] Type: ${message.type}, from ${clientIp}`);
57
67
  await this.handleMessage(ws, message);
58
68
  }
59
69
  catch (error) {
60
- logger_1.logger.error('Error parsing message:', error);
70
+ logger_1.logger.error(`[WS Message] Parse error from ${clientIp}:`, error);
61
71
  this.sendError(ws, 'Invalid message format');
62
72
  }
63
73
  });
64
- ws.on('close', () => {
74
+ ws.on('close', (code, reason) => {
65
75
  const clientId = this.getClientId(ws);
76
+ logger_1.logger.info(`[WS Close] Client ${clientId || 'unknown'} closed with code ${code}, reason: ${reason?.toString() || 'none'}`);
66
77
  if (clientId) {
67
78
  this.terminalHandler.cleanup(clientId);
68
79
  (0, sessionBrowser_1.unwatchSession)(clientId);
69
80
  this.clients.delete(clientId);
70
- logger_1.logger.info(`Client ${clientId} disconnected`);
71
81
  }
72
82
  });
73
83
  ws.on('error', (error) => {
74
- logger_1.logger.error('WebSocket error:', error);
84
+ logger_1.logger.error('[WS Error]:', error);
75
85
  });
76
86
  });
77
87
  }
@@ -126,6 +136,9 @@ class WebSocketServer {
126
136
  case 'get_session_folder_info':
127
137
  await this.handleGetSessionFolderInfo(ws, message);
128
138
  break;
139
+ case 'send_claude_message':
140
+ await this.handleSendClaudeMessage(ws, message);
141
+ break;
129
142
  default:
130
143
  const clientId = this.getClientId(ws);
131
144
  if (!clientId) {
@@ -465,7 +478,58 @@ class WebSocketServer {
465
478
  this.sendError(ws, `Failed to get session folder info: ${error}`);
466
479
  }
467
480
  }
481
+ async handleSendClaudeMessage(ws, message) {
482
+ const clientId = this.getClientId(ws);
483
+ if (!clientId) {
484
+ this.sendError(ws, 'Not authenticated');
485
+ return;
486
+ }
487
+ try {
488
+ const { data } = message;
489
+ if (!data || !data.workspaceDirName || !data.message) {
490
+ this.sendError(ws, 'workspaceDirName and message are required');
491
+ return;
492
+ }
493
+ const { workspaceDirName, sessionId, newSessionId, message: userMessage, allowedTools } = data;
494
+ // 解码 workspaceDirName 为实际路径
495
+ const cwd = '/' + workspaceDirName.replace(/-/g, '/');
496
+ logger_1.logger.info(`[SendClaudeMessage] Sending message to Claude CLI`);
497
+ logger_1.logger.info(`[SendClaudeMessage] CWD: ${cwd}`);
498
+ logger_1.logger.info(`[SendClaudeMessage] SessionId: ${sessionId || 'none'}`);
499
+ logger_1.logger.info(`[SendClaudeMessage] NewSessionId: ${newSessionId || 'none'}`);
500
+ if (allowedTools && allowedTools.length > 0) {
501
+ logger_1.logger.info(`[SendClaudeMessage] AllowedTools: ${allowedTools.join(', ')}`);
502
+ }
503
+ // 调用 chatService 发送消息
504
+ const result = await chatService_1.claudeChatService.sendMessage({
505
+ workspaceDirName,
506
+ sessionId,
507
+ newSessionId,
508
+ message: userMessage,
509
+ cwd,
510
+ allowedTools,
511
+ });
512
+ // 响应客户端
513
+ this.send(ws, {
514
+ type: 'send_claude_message_response',
515
+ data: {
516
+ success: result.success,
517
+ error: result.error,
518
+ sessionId: result.sessionId,
519
+ },
520
+ });
521
+ // 如果成功,响应会通过现有的 watchSession 机制推送
522
+ if (result.success) {
523
+ logger_1.logger.info(`[SendClaudeMessage] Message sent successfully, responses will be pushed via watchSession`);
524
+ }
525
+ }
526
+ catch (error) {
527
+ logger_1.logger.error('Error sending Claude message:', error);
528
+ this.sendError(ws, `Failed to send message: ${error}`);
529
+ }
530
+ }
468
531
  async handleAuth(ws, token) {
532
+ logger_1.logger.info(`[Auth] Validating token: "${token ? '***' : '(empty)'}"`);
469
533
  if (this.authManager.validateToken(token)) {
470
534
  const clientId = this.authManager.generateClientId();
471
535
  this.clients.set(clientId, ws);
@@ -474,9 +538,10 @@ class WebSocketServer {
474
538
  type: 'auth_success',
475
539
  data: { clientId }
476
540
  });
477
- logger_1.logger.info(`Client ${clientId} authenticated`);
541
+ logger_1.logger.info(`[Auth] Client ${clientId} authenticated successfully`);
478
542
  }
479
543
  else {
544
+ logger_1.logger.warn(`[Auth] Invalid token rejected`);
480
545
  this.sendError(ws, 'Invalid token');
481
546
  ws.close();
482
547
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "renote-server",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "WebSocket server for Renote - mobile remote development client with Claude Code integration",
5
5
  "main": "dist/index.js",
6
6
  "bin": {