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/bin/claude-remote.js +1 -1
- package/hooks/bridge-session-start.js +32 -32
- package/lib/cli.js +1 -0
- package/lib/http-server.js +60 -22
- package/lib/interactive-questions.js +183 -0
- package/lib/logger.js +172 -138
- package/lib/state.js +8 -6
- package/lib/ws-server.js +132 -96
- package/package.json +3 -2
- package/server.js +23 -16
- package/web/index.html +383 -0
- package/web/main.js +68 -0
- package/web/modules/chat-cache.js +118 -0
- package/web/modules/confirm.js +25 -0
- package/web/modules/constants.js +59 -0
- package/web/modules/debug.js +81 -0
- package/web/modules/dir-picker.js +128 -0
- package/web/modules/hub.js +619 -0
- package/web/modules/image-upload.js +290 -0
- package/web/modules/input.js +279 -0
- package/web/modules/interactions.js +423 -0
- package/web/modules/keyboard.js +78 -0
- package/web/modules/model-picker.js +47 -0
- package/web/modules/permissions.js +94 -0
- package/web/modules/renderer.js +863 -0
- package/web/modules/sessions.js +108 -0
- package/web/modules/settings.js +101 -0
- package/web/modules/state.js +61 -0
- package/web/modules/toast.js +68 -0
- package/web/modules/todo.js +292 -0
- package/web/modules/utils.js +102 -0
- package/web/modules/waiting.js +93 -0
- package/web/modules/websocket.js +486 -0
- package/web/styles.css +1959 -0
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 {
|
|
46
|
-
|
|
47
|
-
|
|
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 '
|
|
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.
|
|
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.
|
|
20
|
-
state.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
}
|