claude-remote 0.6.0 → 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/logger.js CHANGED
@@ -1,48 +1,81 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const { WebSocket } = require('ws');
5
- const crypto = require('crypto');
6
- const { state, LOG_FILE, EVENT_BUFFER_MAX } = require('./state');
7
- const APPROVAL_MODE_ORDER = { default: 0, partial: 1, all: 2 };
8
-
9
- // --- Logging → file only (never pollute the terminal) ---
10
- fs.writeFileSync(LOG_FILE, `--- Bridge started ${new Date().toISOString()} ---\n`);
11
-
12
- function log(msg) {
13
- const line = `[${new Date().toISOString()}] ${msg}\n`;
14
- fs.appendFileSync(LOG_FILE, line);
15
- }
16
-
17
- function wsLabel(ws) {
18
- const clientId = ws && ws._clientInstanceId ? ` client=${ws._clientInstanceId}` : '';
19
- return `ws#${ws && ws._bridgeId ? ws._bridgeId : '?'}${clientId}`;
20
- }
21
-
22
- function isAuthenticatedClient(ws) {
23
- return !!ws && ws.readyState === WebSocket.OPEN && !!ws._authenticated;
24
- }
25
-
26
- function normalizeApprovalMode(mode) {
27
- const normalized = String(mode || '').toLowerCase();
28
- return Object.prototype.hasOwnProperty.call(APPROVAL_MODE_ORDER, normalized) ? normalized : 'default';
29
- }
30
-
31
- function computeConnectedHighestApprovalMode() {
32
- if (!state.wss) return 'default';
33
- let best = 'default';
34
- let bestScore = APPROVAL_MODE_ORDER.default;
35
- for (const ws of state.wss.clients) {
36
- if (!isAuthenticatedClient(ws)) continue;
37
- const mode = normalizeApprovalMode(ws._approvalMode);
38
- const score = APPROVAL_MODE_ORDER[mode];
39
- if (score > bestScore) {
40
- best = mode;
41
- bestScore = score;
42
- }
43
- }
44
- return best;
45
- }
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { WebSocket } = require('ws');
6
+ const crypto = require('crypto');
7
+ const { state, LOG_FILE, EVENT_BUFFER_MAX } = require('./state');
8
+ const APPROVAL_MODE_ORDER = { default: 0, partial: 1, all: 2 };
9
+
10
+ // --- Logging file only (never pollute the terminal) ---
11
+ let loggerFilePath = LOG_FILE;
12
+ let loggerInitialized = false;
13
+ let loggerEnabled = false;
14
+
15
+ function initLogger({ filePath = LOG_FILE, reset = true } = {}) {
16
+ loggerFilePath = filePath;
17
+ loggerInitialized = true;
18
+ try {
19
+ fs.mkdirSync(path.dirname(loggerFilePath), { recursive: true });
20
+ const header = `--- Bridge started ${new Date().toISOString()} ---\n`;
21
+ if (reset) {
22
+ fs.writeFileSync(loggerFilePath, header);
23
+ } else {
24
+ fs.appendFileSync(loggerFilePath, header);
25
+ }
26
+ loggerEnabled = true;
27
+ } catch {
28
+ loggerEnabled = false;
29
+ }
30
+ return loggerEnabled;
31
+ }
32
+
33
+ function ensureLoggerReady() {
34
+ if (loggerInitialized) return loggerEnabled;
35
+ return initLogger({ filePath: loggerFilePath, reset: false });
36
+ }
37
+
38
+ function log(msg) {
39
+ if (!ensureLoggerReady()) return false;
40
+ const line = `[${new Date().toISOString()}] ${msg}\n`;
41
+ try {
42
+ fs.appendFileSync(loggerFilePath, line);
43
+ return true;
44
+ } catch {
45
+ loggerEnabled = false;
46
+ return false;
47
+ }
48
+ }
49
+
50
+ function wsLabel(ws) {
51
+ const clientId = ws && ws._clientInstanceId ? ` client=${ws._clientInstanceId}` : '';
52
+ return `ws#${ws && ws._bridgeId ? ws._bridgeId : '?'}${clientId}`;
53
+ }
54
+
55
+ function isAuthenticatedClient(ws) {
56
+ return !!ws && ws.readyState === WebSocket.OPEN && !!ws._authenticated;
57
+ }
58
+
59
+ function normalizeApprovalMode(mode) {
60
+ const normalized = String(mode || '').toLowerCase();
61
+ return Object.prototype.hasOwnProperty.call(APPROVAL_MODE_ORDER, normalized) ? normalized : 'default';
62
+ }
63
+
64
+ function computeConnectedHighestApprovalMode() {
65
+ if (!state.wss) return 'default';
66
+ let best = 'default';
67
+ let bestScore = APPROVAL_MODE_ORDER.default;
68
+ for (const ws of state.wss.clients) {
69
+ if (!isAuthenticatedClient(ws)) continue;
70
+ const mode = normalizeApprovalMode(ws._approvalMode);
71
+ const score = APPROVAL_MODE_ORDER[mode];
72
+ if (score > bestScore) {
73
+ best = mode;
74
+ bestScore = score;
75
+ }
76
+ }
77
+ return best;
78
+ }
46
79
 
47
80
  function sendWs(ws, msg, context = '') {
48
81
  if (!ws || ws.readyState !== WebSocket.OPEN) return false;
@@ -59,8 +92,8 @@ function sendWs(ws, msg, context = '') {
59
92
  return true;
60
93
  }
61
94
 
62
- function broadcast(msg) {
63
- if (!state.wss) return;
95
+ function broadcast(msg) {
96
+ if (!state.wss) return;
64
97
  const raw = JSON.stringify(msg);
65
98
  const recipients = [];
66
99
  for (const ws of state.wss.clients) {
@@ -71,62 +104,62 @@ function broadcast(msg) {
71
104
  }
72
105
  if (msg.type === 'status' || msg.type === 'transcript_ready' || msg.type === 'turn_state') {
73
106
  log(`Broadcast ${msg.type} -> ${recipients.length} client(s)${recipients.length ? ` [${recipients.join(', ')}]` : ''}`);
74
- }
75
- }
76
-
77
- function autoResolveAllPendingApprovals(reason = '') {
78
- if (state.pendingApprovals.size === 0) return;
79
- for (const [id, approval] of state.pendingApprovals) {
80
- clearTimeout(approval.timer);
81
- approval.res.writeHead(200, { 'Content-Type': 'application/json' });
82
- approval.res.end(JSON.stringify({ decision: 'allow' }));
83
- log(`Permission #${id}: auto-allowed (${reason || 'effective mode switched to all'})`);
84
- }
85
- state.pendingApprovals.clear();
86
- broadcast({ type: 'clear_permissions' });
87
- }
88
-
89
- function recomputeEffectiveApprovalMode(reason = '') {
90
- const connectedHighest = computeConnectedHighestApprovalMode();
91
- const connectedScore = APPROVAL_MODE_ORDER[connectedHighest];
92
- const turnFloor = normalizeApprovalMode(state.turnApprovalFloorMode);
93
- const turnFloorScore = APPROVAL_MODE_ORDER[turnFloor];
94
- const floorActive = state.turnState.phase === 'running' && !!state.turnApprovalFloorMode;
95
- const nextMode = (floorActive && turnFloorScore > connectedScore) ? turnFloor : connectedHighest;
96
- if (state.approvalMode === nextMode) return nextMode;
97
-
98
- const prevMode = state.approvalMode;
99
- state.approvalMode = nextMode;
100
- log(`Approval mode effective: ${prevMode} -> ${nextMode}${reason ? ` (${reason})` : ''} connected=${connectedHighest} turnFloor=${floorActive ? turnFloor : 'none'} phase=${state.turnState.phase}`);
101
- if (nextMode === 'all' && prevMode !== 'all') {
102
- autoResolveAllPendingApprovals(reason || 'effective mode switched to all');
103
- }
104
- return nextMode;
105
- }
106
-
107
- function setClientApprovalMode(ws, mode, reason = '') {
108
- if (!ws) return state.approvalMode;
109
- const normalized = normalizeApprovalMode(mode);
110
- ws._approvalMode = normalized;
111
- log(`Approval mode reported by ${wsLabel(ws)}: ${normalized}${reason ? ` (${reason})` : ''}`);
112
- return recomputeEffectiveApprovalMode(`client mode update ${wsLabel(ws)}`);
113
- }
114
-
115
- function setTurnApprovalFloorMode(mode, reason = '') {
116
- const normalized = normalizeApprovalMode(mode);
117
- const prev = state.turnApprovalFloorMode || 'none';
118
- state.turnApprovalFloorMode = normalized;
119
- log(`Turn approval floor set: ${prev} -> ${normalized}${reason ? ` (${reason})` : ''}`);
120
- return normalized;
121
- }
122
-
123
- function clearTurnApprovalFloorMode(reason = '') {
124
- if (!state.turnApprovalFloorMode) return state.approvalMode;
125
- const prev = state.turnApprovalFloorMode;
126
- state.turnApprovalFloorMode = '';
127
- log(`Turn approval floor cleared: ${prev}${reason ? ` (${reason})` : ''}`);
128
- return recomputeEffectiveApprovalMode(`turn floor cleared${reason ? `: ${reason}` : ''}`);
129
- }
107
+ }
108
+ }
109
+
110
+ function autoResolveAllPendingApprovals(reason = '') {
111
+ if (state.pendingApprovals.size === 0) return;
112
+ for (const [id, approval] of state.pendingApprovals) {
113
+ clearTimeout(approval.timer);
114
+ approval.res.writeHead(200, { 'Content-Type': 'application/json' });
115
+ approval.res.end(JSON.stringify({ decision: 'allow' }));
116
+ log(`Permission #${id}: auto-allowed (${reason || 'effective mode switched to all'})`);
117
+ }
118
+ state.pendingApprovals.clear();
119
+ broadcast({ type: 'clear_permissions' });
120
+ }
121
+
122
+ function recomputeEffectiveApprovalMode(reason = '') {
123
+ const connectedHighest = computeConnectedHighestApprovalMode();
124
+ const connectedScore = APPROVAL_MODE_ORDER[connectedHighest];
125
+ const turnFloor = normalizeApprovalMode(state.turnApprovalFloorMode);
126
+ const turnFloorScore = APPROVAL_MODE_ORDER[turnFloor];
127
+ const floorActive = state.turnState.phase === 'running' && !!state.turnApprovalFloorMode;
128
+ const nextMode = (floorActive && turnFloorScore > connectedScore) ? turnFloor : connectedHighest;
129
+ if (state.approvalMode === nextMode) return nextMode;
130
+
131
+ const prevMode = state.approvalMode;
132
+ state.approvalMode = nextMode;
133
+ log(`Approval mode effective: ${prevMode} -> ${nextMode}${reason ? ` (${reason})` : ''} connected=${connectedHighest} turnFloor=${floorActive ? turnFloor : 'none'} phase=${state.turnState.phase}`);
134
+ if (nextMode === 'all' && prevMode !== 'all') {
135
+ autoResolveAllPendingApprovals(reason || 'effective mode switched to all');
136
+ }
137
+ return nextMode;
138
+ }
139
+
140
+ function setClientApprovalMode(ws, mode, reason = '') {
141
+ if (!ws) return state.approvalMode;
142
+ const normalized = normalizeApprovalMode(mode);
143
+ ws._approvalMode = normalized;
144
+ log(`Approval mode reported by ${wsLabel(ws)}: ${normalized}${reason ? ` (${reason})` : ''}`);
145
+ return recomputeEffectiveApprovalMode(`client mode update ${wsLabel(ws)}`);
146
+ }
147
+
148
+ function setTurnApprovalFloorMode(mode, reason = '') {
149
+ const normalized = normalizeApprovalMode(mode);
150
+ const prev = state.turnApprovalFloorMode || 'none';
151
+ state.turnApprovalFloorMode = normalized;
152
+ log(`Turn approval floor set: ${prev} -> ${normalized}${reason ? ` (${reason})` : ''}`);
153
+ return normalized;
154
+ }
155
+
156
+ function clearTurnApprovalFloorMode(reason = '') {
157
+ if (!state.turnApprovalFloorMode) return state.approvalMode;
158
+ const prev = state.turnApprovalFloorMode;
159
+ state.turnApprovalFloorMode = '';
160
+ log(`Turn approval floor cleared: ${prev}${reason ? ` (${reason})` : ''}`);
161
+ return recomputeEffectiveApprovalMode(`turn floor cleared${reason ? `: ${reason}` : ''}`);
162
+ }
130
163
 
131
164
  function latestEventSeq() {
132
165
  return state.eventBuffer.length > 0 ? state.eventBuffer[state.eventBuffer.length - 1].seq : 0;
@@ -147,34 +180,34 @@ function sendTurnState(ws, context = '') {
147
180
  return sendWs(ws, getTurnStatePayload(), context);
148
181
  }
149
182
 
150
- function setTurnState(phase, { sessionId = state.currentSessionId, reason = '', force = false } = {}) {
151
- const normalizedPhase = phase === 'running' ? 'running' : 'idle';
152
- const normalizedSessionId = sessionId || null;
183
+ function setTurnState(phase, { sessionId = state.currentSessionId, reason = '', force = false } = {}) {
184
+ const normalizedPhase = phase === 'running' ? 'running' : 'idle';
185
+ const normalizedSessionId = sessionId || null;
153
186
  const changed = force ||
154
187
  state.turnState.phase !== normalizedPhase ||
155
188
  state.turnState.sessionId !== normalizedSessionId;
156
189
 
157
190
  if (!changed) return false;
158
191
 
159
- state.turnState = {
160
- phase: normalizedPhase,
161
- sessionId: normalizedSessionId,
162
- version: ++state.turnStateVersion,
163
- updatedAt: Date.now(),
164
- reason,
165
- };
166
-
167
- const modeReason = reason || `turn_state:${normalizedPhase}`;
168
- if (normalizedPhase !== 'running' && state.turnApprovalFloorMode) {
169
- clearTurnApprovalFloorMode(modeReason);
170
- } else {
171
- recomputeEffectiveApprovalMode(modeReason);
172
- }
173
-
174
- log(`Turn state -> phase=${state.turnState.phase} session=${state.turnState.sessionId ?? 'null'} version=${state.turnState.version}${reason ? ` reason=${reason}` : ''}`);
175
- broadcast(getTurnStatePayload());
176
- return true;
177
- }
192
+ state.turnState = {
193
+ phase: normalizedPhase,
194
+ sessionId: normalizedSessionId,
195
+ version: ++state.turnStateVersion,
196
+ updatedAt: Date.now(),
197
+ reason,
198
+ };
199
+
200
+ const modeReason = reason || `turn_state:${normalizedPhase}`;
201
+ if (normalizedPhase !== 'running' && state.turnApprovalFloorMode) {
202
+ clearTurnApprovalFloorMode(modeReason);
203
+ } else {
204
+ recomputeEffectiveApprovalMode(modeReason);
205
+ }
206
+
207
+ log(`Turn state -> phase=${state.turnState.phase} session=${state.turnState.sessionId ?? 'null'} version=${state.turnState.version}${reason ? ` reason=${reason}` : ''}`);
208
+ broadcast(getTurnStatePayload());
209
+ return true;
210
+ }
178
211
 
179
212
  function emitInterrupt(source) {
180
213
  const interruptEvent = {
@@ -197,20 +230,21 @@ function formatTtyInputChunk(chunk) {
197
230
  return `len=${buf.length} hex=${buf.toString('hex')} base64=${buf.toString('base64')} utf8=${JSON.stringify(buf.toString('utf8'))}`;
198
231
  }
199
232
 
200
- module.exports = {
201
- log,
202
- wsLabel,
203
- isAuthenticatedClient,
233
+ module.exports = {
234
+ initLogger,
235
+ log,
236
+ wsLabel,
237
+ isAuthenticatedClient,
204
238
  sendWs,
205
239
  broadcast,
206
240
  latestEventSeq,
207
241
  getTurnStatePayload,
208
- sendTurnState,
209
- setTurnState,
210
- recomputeEffectiveApprovalMode,
211
- setClientApprovalMode,
212
- setTurnApprovalFloorMode,
213
- clearTurnApprovalFloorMode,
214
- emitInterrupt,
215
- formatTtyInputChunk,
216
- };
242
+ sendTurnState,
243
+ setTurnState,
244
+ recomputeEffectiveApprovalMode,
245
+ setClientApprovalMode,
246
+ setTurnApprovalFloorMode,
247
+ clearTurnApprovalFloorMode,
248
+ emitInterrupt,
249
+ formatTtyInputChunk,
250
+ };
package/lib/state.js CHANGED
@@ -76,12 +76,13 @@ const state = {
76
76
  wss: null,
77
77
  nextWsId: 0,
78
78
 
79
- // Permission approval
80
- approvalSeq: 0,
81
- pendingApprovals: new Map(),
82
- pendingImageUploads: new Map(),
83
- approvalMode: 'default',
84
- turnApprovalFloorMode: '',
79
+ // Permission approval
80
+ approvalSeq: 0,
81
+ pendingApprovals: new Map(),
82
+ pendingImageUploads: new Map(),
83
+ pendingQuestionSubmissions: new Set(),
84
+ approvalMode: 'default',
85
+ turnApprovalFloorMode: '',
85
86
 
86
87
  // TTY forwarders
87
88
  ttyInputForwarderAttached: false,
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 };