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/logger.js
CHANGED
|
@@ -1,48 +1,81 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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,
|