claude-remote 0.5.2 → 0.6.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/lib/ws-server.js CHANGED
@@ -13,19 +13,19 @@ const {
13
13
  LEGACY_REPLAY_DELAY_MS,
14
14
  isTTY,
15
15
  } = require('./state');
16
- const {
17
- log,
18
- broadcast,
19
- wsLabel,
20
- sendWs,
21
- sendTurnState,
22
- setTurnState,
23
- recomputeEffectiveApprovalMode,
24
- setClientApprovalMode,
25
- setTurnApprovalFloorMode,
26
- latestEventSeq,
27
- emitInterrupt,
28
- } = require('./logger');
16
+ const {
17
+ log,
18
+ broadcast,
19
+ wsLabel,
20
+ sendWs,
21
+ sendTurnState,
22
+ setTurnState,
23
+ recomputeEffectiveApprovalMode,
24
+ setClientApprovalMode,
25
+ setTurnApprovalFloorMode,
26
+ latestEventSeq,
27
+ emitInterrupt,
28
+ } = require('./logger');
29
29
  const {
30
30
  extractSlashCommand,
31
31
  markExpectingSwitch,
@@ -34,17 +34,23 @@ const {
34
34
  getDirectoryRoots,
35
35
  assertDirectoryPath,
36
36
  } = require('./transcript');
37
- const {
38
- cleanupImageUpload,
39
- cleanupClientUploads,
40
- sendUploadStatus,
41
- handlePreparedImageUpload,
42
- handleImageUpload,
43
- createTempImageFile,
44
- } = require('./image-upload');
45
- const { restartClaude } = require('./pty-manager');
46
-
47
- function sendReplay(ws, lastSeq = null) {
37
+ const {
38
+ cleanupImageUpload,
39
+ cleanupClientUploads,
40
+ sendUploadStatus,
41
+ handlePreparedImageUpload,
42
+ handleImageUpload,
43
+ createTempImageFile,
44
+ } = require('./image-upload');
45
+ const {
46
+ claimAskUserQuestionSubmissionLock,
47
+ releaseAskUserQuestionSubmissionLock,
48
+ buildAskUserQuestionPtyOperations,
49
+ executePtyOperations,
50
+ } = require('./interactive-questions');
51
+ const { restartClaude } = require('./pty-manager');
52
+
53
+ function sendReplay(ws, lastSeq = null) {
48
54
  const normalizedLastSeq = Number.isInteger(lastSeq) && lastSeq >= 0 ? lastSeq : null;
49
55
  const replayFrom = normalizedLastSeq == null ? 0 : normalizedLastSeq;
50
56
  const records = replayFrom > 0
@@ -101,11 +107,11 @@ function setupWebSocketServer(server) {
101
107
  const wss = new WebSocketServer({ server });
102
108
  state.wss = wss;
103
109
 
104
- wss.on('connection', (ws, req) => {
110
+ wss.on('connection', (ws, req) => {
105
111
  ws._bridgeId = ++state.nextWsId;
106
112
  ws._clientInstanceId = '';
107
- ws._authenticated = state.AUTH_DISABLED;
108
- ws._approvalMode = 'default';
113
+ ws._authenticated = state.AUTH_DISABLED;
114
+ ws._approvalMode = 'default';
109
115
  ws._authTimer = null;
110
116
  log(`WS connected: ${wsLabel(ws)} remote=${req.socket.remoteAddress || '?'} ua=${JSON.stringify(req.headers['user-agent'] || '')} authRequired=${!state.AUTH_DISABLED}`);
111
117
 
@@ -226,10 +232,40 @@ function setupWebSocketServer(server) {
226
232
  log(`Foreground probe ack -> ${wsLabel(ws)} probeId=${probeId || 'none'} session=${state.currentSessionId ?? 'null'} lastSeq=${latestEventSeq()}`);
227
233
  break;
228
234
  }
229
- case 'input':
230
- if (state.claudeProc) state.claudeProc.write(msg.data);
231
- break;
232
- case 'interrupt': {
235
+ case 'input':
236
+ if (state.claudeProc) state.claudeProc.write(msg.data);
237
+ break;
238
+ case 'answer_questions': {
239
+ const submissionKey = claimAskUserQuestionSubmissionLock(
240
+ state.pendingQuestionSubmissions,
241
+ msg,
242
+ wsLabel(ws),
243
+ );
244
+ if (!submissionKey) {
245
+ log(`AskUserQuestion duplicate submit ignored from ${wsLabel(ws)} toolUseId=${msg.toolUseId || 'unknown'}`);
246
+ break;
247
+ }
248
+ try {
249
+ if (!state.claudeProc) {
250
+ throw new Error('Claude is not running');
251
+ }
252
+ const operations = buildAskUserQuestionPtyOperations(msg);
253
+ log(`AskUserQuestion submit from ${wsLabel(ws)} toolUseId=${msg.toolUseId || 'unknown'} questions=${Array.isArray(msg.questions) ? msg.questions.length : 0}`);
254
+ await executePtyOperations(state.claudeProc, operations);
255
+ } catch (err) {
256
+ const error = err && err.message ? err.message : 'Failed to submit answers';
257
+ log(`AskUserQuestion submit error from ${wsLabel(ws)} toolUseId=${msg.toolUseId || 'unknown'}: ${error}`);
258
+ sendWs(ws, {
259
+ type: 'question_submission_error',
260
+ toolUseId: typeof msg.toolUseId === 'string' ? msg.toolUseId : '',
261
+ error,
262
+ }, 'answer_questions_error');
263
+ } finally {
264
+ releaseAskUserQuestionSubmissionLock(state.pendingQuestionSubmissions, submissionKey);
265
+ }
266
+ break;
267
+ }
268
+ case 'interrupt': {
233
269
  if (!state.claudeProc || state.turnState.phase !== 'running') break;
234
270
  log(`Interrupt from ${wsLabel(ws)} — sending Ctrl+C to PTY`);
235
271
  state.claudeProc.write('\x03');
@@ -239,23 +275,23 @@ function setupWebSocketServer(server) {
239
275
  case 'expect_clear':
240
276
  markExpectingSwitch();
241
277
  break;
242
- case 'chat':
243
- if (state.claudeProc) {
244
- const text = msg.text;
245
- log(`Chat input → PTY: "${text.substring(0, 80)}"`);
246
- const slashCommand = extractSlashCommand(text);
247
- if (slashCommand === '/clear') {
248
- markExpectingSwitch();
249
- }
250
- if (!slashCommand) {
251
- if (state.turnState.phase !== 'running') {
252
- setTurnApprovalFloorMode(ws._approvalMode || 'default', `chat by ${wsLabel(ws)}`);
253
- }
254
- setTurnState('running', { reason: 'chat' });
255
- }
256
- state.claudeProc.write(text);
257
- setTimeout(() => {
258
- if (state.claudeProc) state.claudeProc.write('\r');
278
+ case 'chat':
279
+ if (state.claudeProc) {
280
+ const text = msg.text;
281
+ log(`Chat input → PTY: "${text.substring(0, 80)}"`);
282
+ const slashCommand = extractSlashCommand(text);
283
+ if (slashCommand === '/clear') {
284
+ markExpectingSwitch();
285
+ }
286
+ if (!slashCommand) {
287
+ if (state.turnState.phase !== 'running') {
288
+ setTurnApprovalFloorMode(ws._approvalMode || 'default', `chat by ${wsLabel(ws)}`);
289
+ }
290
+ setTurnState('running', { reason: 'chat' });
291
+ }
292
+ state.claudeProc.write(text);
293
+ setTimeout(() => {
294
+ if (state.claudeProc) state.claudeProc.write('\r');
259
295
  }, 150);
260
296
  }
261
297
  break;
@@ -264,29 +300,29 @@ function setupWebSocketServer(server) {
264
300
  state.claudeProc.resize(msg.cols, msg.rows);
265
301
  }
266
302
  break;
267
- case 'permission_response': {
268
- const approval = state.pendingApprovals.get(msg.id);
269
- if (approval) {
270
- clearTimeout(approval.timer);
271
- state.pendingApprovals.delete(msg.id);
303
+ case 'permission_response': {
304
+ const approval = state.pendingApprovals.get(msg.id);
305
+ if (approval) {
306
+ clearTimeout(approval.timer);
307
+ state.pendingApprovals.delete(msg.id);
272
308
  approval.res.writeHead(200, { 'Content-Type': 'application/json' });
273
309
  approval.res.end(JSON.stringify({
274
310
  decision: msg.decision,
275
311
  reason: msg.reason || '',
276
- }));
277
- log(`Permission #${msg.id}: ${msg.decision}`);
278
- broadcast({
279
- type: 'permission_resolved',
280
- id: msg.id,
281
- decision: msg.decision,
282
- });
283
- }
284
- break;
285
- }
286
- case 'set_approval_mode': {
287
- setClientApprovalMode(ws, msg.mode);
288
- break;
289
- }
312
+ }));
313
+ log(`Permission #${msg.id}: ${msg.decision}`);
314
+ broadcast({
315
+ type: 'permission_resolved',
316
+ id: msg.id,
317
+ decision: msg.decision,
318
+ });
319
+ }
320
+ break;
321
+ }
322
+ case 'set_approval_mode': {
323
+ setClientApprovalMode(ws, msg.mode);
324
+ break;
325
+ }
290
326
  case 'image_upload_init': {
291
327
  const uploadId = String(msg.uploadId || '');
292
328
  if (!uploadId) {
@@ -388,7 +424,7 @@ function setupWebSocketServer(server) {
388
424
  sendUploadStatus(ws, uploadId, 'aborted');
389
425
  break;
390
426
  }
391
- case 'image_submit': {
427
+ case 'image_submit': {
392
428
  const uploadId = String(msg.uploadId || '');
393
429
  const upload = state.pendingImageUploads.get(uploadId);
394
430
  if (!upload || !upload.tmpFile) {
@@ -396,33 +432,33 @@ function setupWebSocketServer(server) {
396
432
  break;
397
433
  }
398
434
  try {
399
- await handlePreparedImageUpload({
400
- tmpFile: upload.tmpFile,
435
+ await handlePreparedImageUpload({
436
+ tmpFile: upload.tmpFile,
401
437
  mediaType: upload.mediaType,
402
438
  text: msg.text || '',
403
439
  logLabel: upload.name || uploadId,
404
440
  onCleanup: () => cleanupImageUpload(uploadId),
405
- });
406
- upload.submitted = true;
407
- upload.updatedAt = Date.now();
408
- if (state.turnState.phase !== 'running') {
409
- setTurnApprovalFloorMode(ws._approvalMode || 'default', `image_submit by ${wsLabel(ws)}`);
410
- }
411
- setTurnState('running', { reason: 'image_submit' });
412
- sendUploadStatus(ws, uploadId, 'submitted');
413
- } catch (err) {
414
- sendUploadStatus(ws, uploadId, 'error', { message: err.message });
415
- cleanupImageUpload(uploadId);
416
- }
417
- break;
418
- }
419
- case 'image_upload': {
420
- if (state.turnState.phase !== 'running') {
421
- setTurnApprovalFloorMode(ws._approvalMode || 'default', `legacy image_upload by ${wsLabel(ws)}`);
441
+ });
442
+ upload.submitted = true;
443
+ upload.updatedAt = Date.now();
444
+ if (state.turnState.phase !== 'running') {
445
+ setTurnApprovalFloorMode(ws._approvalMode || 'default', `image_submit by ${wsLabel(ws)}`);
446
+ }
447
+ setTurnState('running', { reason: 'image_submit' });
448
+ sendUploadStatus(ws, uploadId, 'submitted');
449
+ } catch (err) {
450
+ sendUploadStatus(ws, uploadId, 'error', { message: err.message });
451
+ cleanupImageUpload(uploadId);
422
452
  }
423
- handleImageUpload(msg);
424
453
  break;
425
454
  }
455
+ case 'image_upload': {
456
+ if (state.turnState.phase !== 'running') {
457
+ setTurnApprovalFloorMode(ws._approvalMode || 'default', `legacy image_upload by ${wsLabel(ws)}`);
458
+ }
459
+ handleImageUpload(msg);
460
+ break;
461
+ }
426
462
  case 'list_sessions': {
427
463
  try {
428
464
  const sessions = scanSessions(state.CWD, 20);
@@ -475,7 +511,7 @@ function setupWebSocketServer(server) {
475
511
  }
476
512
  });
477
513
 
478
- ws.on('close', () => {
514
+ ws.on('close', () => {
479
515
  if (ws._authTimer) {
480
516
  clearTimeout(ws._authTimer);
481
517
  ws._authTimer = null;
@@ -483,12 +519,12 @@ function setupWebSocketServer(server) {
483
519
  if (ws._legacyReplayTimer) {
484
520
  clearTimeout(ws._legacyReplayTimer);
485
521
  ws._legacyReplayTimer = null;
486
- }
487
- log(`WS closed: ${wsLabel(ws)}`);
488
- cleanupClientUploads(ws);
489
- recomputeEffectiveApprovalMode(`client disconnected ${wsLabel(ws)}`);
490
- });
491
- });
492
- }
522
+ }
523
+ log(`WS closed: ${wsLabel(ws)}`);
524
+ cleanupClientUploads(ws);
525
+ recomputeEffectiveApprovalMode(`client disconnected ${wsLabel(ws)}`);
526
+ });
527
+ });
528
+ }
493
529
 
494
530
  module.exports = { setupWebSocketServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "Remote control bridge for Claude Code REPL - drive from phone/WebUI",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "server.js",
11
11
  "lib/",
12
12
  "hooks/",
13
- "bin/"
13
+ "bin/",
14
+ "web/"
14
15
  ],
15
16
  "scripts": {
16
17
  "start": "node server.js"
package/server.js CHANGED
@@ -1,27 +1,29 @@
1
1
  'use strict';
2
2
 
3
- const os = require('os');
4
- const { state, LOG_FILE } = require('./lib/state');
5
- const { initConfig } = require('./lib/cli');
6
- const { log } = require('./lib/logger');
7
- const { createHttpServer } = require('./lib/http-server');
8
- const { setupWebSocketServer } = require('./lib/ws-server');
9
- const { spawnClaude } = require('./lib/pty-manager');
10
- const { setupHooks } = require('./lib/hooks');
11
- const { startUploadCleanup } = require('./lib/image-upload');
3
+ const os = require('os');
4
+ const { state, LOG_FILE } = require('./lib/state');
5
+ const { initConfig } = require('./lib/cli');
6
+ const { initLogger, log } = require('./lib/logger');
7
+ const { createHttpServer } = require('./lib/http-server');
8
+ const { setupWebSocketServer } = require('./lib/ws-server');
9
+ const { spawnClaude } = require('./lib/pty-manager');
10
+ const { setupHooks } = require('./lib/hooks');
11
+ const { startUploadCleanup } = require('./lib/image-upload');
12
12
 
13
13
  // --- Initialize config from CLI args + env ---
14
14
  const config = initConfig();
15
15
  state.PORT = config.PORT;
16
16
  state.CWD = config.CWD;
17
17
  state.AUTH_TOKEN = config.AUTH_TOKEN;
18
- state.AUTH_DISABLED = config.AUTH_DISABLED;
19
- state.CLAUDE_EXTRA_ARGS = config.CLAUDE_EXTRA_ARGS;
20
- state.DEBUG_TTY_INPUT = config.DEBUG_TTY_INPUT;
21
-
22
- // --- Create servers ---
23
- const server = createHttpServer();
24
- setupWebSocketServer(server);
18
+ state.AUTH_DISABLED = config.AUTH_DISABLED;
19
+ state.ENABLE_WEB = config.ENABLE_WEB;
20
+ state.CLAUDE_EXTRA_ARGS = config.CLAUDE_EXTRA_ARGS;
21
+ state.DEBUG_TTY_INPUT = config.DEBUG_TTY_INPUT;
22
+ initLogger();
23
+
24
+ // --- Create servers ---
25
+ const server = createHttpServer();
26
+ setupWebSocketServer(server);
25
27
 
26
28
  // --- Start periodic cleanup ---
27
29
  startUploadCleanup();
@@ -54,6 +56,11 @@ server.listen(state.PORT, '0.0.0.0', () => {
54
56
  } else {
55
57
  banner += ` Token: ${config.AUTH_TOKEN}\n`;
56
58
  }
59
+ if (config.ENABLE_WEB) {
60
+ banner += ` WebUI: ENABLED\n`;
61
+ } else {
62
+ banner += ` WebUI: disabled (set ENABLE_WEB=1 to enable)\n`;
63
+ }
57
64
  if (config.unusedLegacyTokenEnv) {
58
65
  banner += ` Note: Ignoring legacy ${config.LEGACY_AUTH_TOKEN_ENV_VAR}; use ${config.AUTH_TOKEN_ENV_VAR} instead\n`;
59
66
  }