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/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
@@ -38,6 +38,7 @@ const state = {
38
38
  CWD: process.cwd(),
39
39
  AUTH_TOKEN: null,
40
40
  AUTH_DISABLED: false,
41
+ ENABLE_WEB: false,
41
42
  CLAUDE_EXTRA_ARGS: [],
42
43
  DEBUG_TTY_INPUT: false,
43
44
 
@@ -75,12 +76,13 @@ const state = {
75
76
  wss: null,
76
77
  nextWsId: 0,
77
78
 
78
- // Permission approval
79
- approvalSeq: 0,
80
- pendingApprovals: new Map(),
81
- pendingImageUploads: new Map(),
82
- approvalMode: 'default',
83
- turnApprovalFloorMode: '',
79
+ // Permission approval
80
+ approvalSeq: 0,
81
+ pendingApprovals: new Map(),
82
+ pendingImageUploads: new Map(),
83
+ pendingQuestionSubmissions: new Set(),
84
+ approvalMode: 'default',
85
+ turnApprovalFloorMode: '',
84
86
 
85
87
  // TTY forwarders
86
88
  ttyInputForwarderAttached: false,