claude-remote 0.4.4 → 0.4.6
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/package.json +1 -1
- package/server.js +287 -172
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -5,7 +5,7 @@ const os = require('os');
|
|
|
5
5
|
const pty = require('node-pty');
|
|
6
6
|
const { WebSocketServer, WebSocket } = require('ws');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
|
-
const { execSync, spawn } = require('child_process');
|
|
8
|
+
const { execSync, spawn } = require('child_process');
|
|
9
9
|
|
|
10
10
|
// --- CLI argument parsing ---
|
|
11
11
|
// Separate bridge args (CWD positional) from claude passthrough flags.
|
|
@@ -158,12 +158,12 @@ let CWD = _parsedCwd;
|
|
|
158
158
|
const CLAUDE_HOME = path.join(os.homedir(), '.claude');
|
|
159
159
|
const CLAUDE_STATE_FILE = path.join(os.homedir(), '.claude.json');
|
|
160
160
|
const PROJECTS_DIR = path.join(CLAUDE_HOME, 'projects');
|
|
161
|
-
const AUTH_HELLO_TIMEOUT_MS = 5000;
|
|
162
|
-
const WS_CLOSE_AUTH_FAILED = 4001;
|
|
163
|
-
const WS_CLOSE_AUTH_TIMEOUT = 4002;
|
|
164
|
-
const WS_CLOSE_REASON_AUTH_FAILED = 'auth_failed';
|
|
165
|
-
const WS_CLOSE_REASON_AUTH_TIMEOUT = 'auth_timeout';
|
|
166
|
-
const DEBUG_TTY_INPUT = process.env.CLAUDE_REMOTE_DEBUG_TTY_INPUT === '1';
|
|
161
|
+
const AUTH_HELLO_TIMEOUT_MS = 5000;
|
|
162
|
+
const WS_CLOSE_AUTH_FAILED = 4001;
|
|
163
|
+
const WS_CLOSE_AUTH_TIMEOUT = 4002;
|
|
164
|
+
const WS_CLOSE_REASON_AUTH_FAILED = 'auth_failed';
|
|
165
|
+
const WS_CLOSE_REASON_AUTH_TIMEOUT = 'auth_timeout';
|
|
166
|
+
const DEBUG_TTY_INPUT = process.env.CLAUDE_REMOTE_DEBUG_TTY_INPUT === '1';
|
|
167
167
|
|
|
168
168
|
// --- State ---
|
|
169
169
|
let claudeProc = null;
|
|
@@ -185,7 +185,8 @@ let tailRemainder = Buffer.alloc(0);
|
|
|
185
185
|
let tailCatchingUp = false; // true while reading historical transcript content
|
|
186
186
|
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
187
187
|
const LEGACY_REPLAY_DELAY_MS = 1500;
|
|
188
|
-
const IMAGE_UPLOAD_TTL_MS = 15 * 60 * 1000;
|
|
188
|
+
const IMAGE_UPLOAD_TTL_MS = 15 * 60 * 1000;
|
|
189
|
+
const LINUX_CLIPBOARD_READY_GRACE_MS = 400;
|
|
189
190
|
const IMAGE_UPLOAD_DIR = path.join(CLAUDE_HOME, 'remote-uploads');
|
|
190
191
|
let turnStateVersion = 0;
|
|
191
192
|
let turnState = {
|
|
@@ -198,6 +199,7 @@ let ttyInputForwarderAttached = false;
|
|
|
198
199
|
let ttyInputHandler = null;
|
|
199
200
|
let ttyResizeHandler = null;
|
|
200
201
|
let activeLinuxClipboardProc = null;
|
|
202
|
+
let linuxImagePasteInFlight = false;
|
|
201
203
|
|
|
202
204
|
// --- Permission approval state ---
|
|
203
205
|
let approvalSeq = 0;
|
|
@@ -210,27 +212,27 @@ const PARTIAL_AUTO_ALLOW = new Set(['Read', 'Glob', 'Grep', 'Write', 'Edit']);
|
|
|
210
212
|
// --- Logging → file only (never pollute the terminal) ---
|
|
211
213
|
const LOG_FILE = path.join(os.homedir(), '.claude', 'bridge.log');
|
|
212
214
|
fs.writeFileSync(LOG_FILE, `--- Bridge started ${new Date().toISOString()} ---\n`);
|
|
213
|
-
function log(msg) {
|
|
214
|
-
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
215
|
-
fs.appendFileSync(LOG_FILE, line);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function formatTtyInputChunk(chunk) {
|
|
219
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
220
|
-
return `len=${buf.length} hex=${buf.toString('hex')} base64=${buf.toString('base64')} utf8=${JSON.stringify(buf.toString('utf8'))}`;
|
|
221
|
-
}
|
|
222
|
-
|
|
215
|
+
function log(msg) {
|
|
216
|
+
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
217
|
+
fs.appendFileSync(LOG_FILE, line);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function formatTtyInputChunk(chunk) {
|
|
221
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
222
|
+
return `len=${buf.length} hex=${buf.toString('hex')} base64=${buf.toString('base64')} utf8=${JSON.stringify(buf.toString('utf8'))}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
223
225
|
function clearActiveLinuxClipboardProc(reason = '') {
|
|
224
226
|
if (!activeLinuxClipboardProc) return;
|
|
225
227
|
const { child, tool } = activeLinuxClipboardProc;
|
|
226
228
|
activeLinuxClipboardProc = null;
|
|
227
|
-
try {
|
|
228
|
-
child.kill('SIGTERM');
|
|
229
|
-
log(`Linux clipboard process terminated (${tool}) reason=${reason || 'cleanup'}`);
|
|
230
|
-
} catch (err) {
|
|
231
|
-
log(`Linux clipboard process terminate error (${tool}): ${err.message}`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
229
|
+
try {
|
|
230
|
+
child.kill('SIGTERM');
|
|
231
|
+
log(`Linux clipboard process terminated (${tool}) reason=${reason || 'cleanup'}`);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
log(`Linux clipboard process terminate error (${tool}): ${err.message}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
234
236
|
|
|
235
237
|
function wsLabel(ws) {
|
|
236
238
|
const clientId = ws && ws._clientInstanceId ? ` client=${ws._clientInstanceId}` : '';
|
|
@@ -263,6 +265,7 @@ function getTurnStatePayload() {
|
|
|
263
265
|
sessionId: turnState.sessionId,
|
|
264
266
|
version: turnState.version,
|
|
265
267
|
updatedAt: turnState.updatedAt,
|
|
268
|
+
reason: turnState.reason || '',
|
|
266
269
|
};
|
|
267
270
|
}
|
|
268
271
|
|
|
@@ -284,6 +287,7 @@ function setTurnState(phase, { sessionId = currentSessionId, reason = '', force
|
|
|
284
287
|
sessionId: normalizedSessionId,
|
|
285
288
|
version: ++turnStateVersion,
|
|
286
289
|
updatedAt: Date.now(),
|
|
290
|
+
reason,
|
|
287
291
|
};
|
|
288
292
|
|
|
289
293
|
log(`Turn state -> phase=${turnState.phase} session=${turnState.sessionId ?? 'null'} version=${turnState.version}${reason ? ` reason=${reason}` : ''}`);
|
|
@@ -291,19 +295,41 @@ function setTurnState(phase, { sessionId = currentSessionId, reason = '', force
|
|
|
291
295
|
return true;
|
|
292
296
|
}
|
|
293
297
|
|
|
298
|
+
function emitInterrupt(source) {
|
|
299
|
+
const interruptEvent = {
|
|
300
|
+
type: 'interrupt',
|
|
301
|
+
source,
|
|
302
|
+
timestamp: Date.now(),
|
|
303
|
+
uuid: crypto.randomUUID(),
|
|
304
|
+
};
|
|
305
|
+
const record = { seq: ++eventSeq, event: interruptEvent };
|
|
306
|
+
eventBuffer.push(record);
|
|
307
|
+
if (eventBuffer.length > EVENT_BUFFER_MAX) {
|
|
308
|
+
eventBuffer = eventBuffer.slice(-Math.round(EVENT_BUFFER_MAX * 0.8));
|
|
309
|
+
}
|
|
310
|
+
broadcast({ type: 'log_event', seq: record.seq, event: interruptEvent });
|
|
311
|
+
setTurnState('idle', { reason: `${source}_interrupt` });
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
function attachTtyForwarders() {
|
|
295
|
-
if (!isTTY || ttyInputForwarderAttached) return;
|
|
296
|
-
|
|
297
|
-
ttyInputHandler = (chunk) => {
|
|
298
|
-
if (DEBUG_TTY_INPUT) {
|
|
299
|
-
try {
|
|
300
|
-
log(`TTY input ${formatTtyInputChunk(chunk)}`);
|
|
301
|
-
} catch (err) {
|
|
302
|
-
log(`TTY input log error: ${err.message}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
if (claudeProc) claudeProc.write(chunk);
|
|
306
|
-
|
|
315
|
+
if (!isTTY || ttyInputForwarderAttached) return;
|
|
316
|
+
|
|
317
|
+
ttyInputHandler = (chunk) => {
|
|
318
|
+
if (DEBUG_TTY_INPUT) {
|
|
319
|
+
try {
|
|
320
|
+
log(`TTY input ${formatTtyInputChunk(chunk)}`);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
log(`TTY input log error: ${err.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (claudeProc) claudeProc.write(chunk);
|
|
326
|
+
// Detect Ctrl+C (0x03) from local terminal and sync state
|
|
327
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
328
|
+
if (buf.includes(0x03) && turnState.phase === 'running') {
|
|
329
|
+
log('Terminal Ctrl+C detected — injecting interrupt event');
|
|
330
|
+
emitInterrupt('terminal');
|
|
331
|
+
}
|
|
332
|
+
};
|
|
307
333
|
ttyResizeHandler = () => {
|
|
308
334
|
if (claudeProc) claudeProc.resize(process.stdout.columns, process.stdout.rows);
|
|
309
335
|
};
|
|
@@ -657,79 +683,134 @@ function cleanupClientUploads(ws) {
|
|
|
657
683
|
}
|
|
658
684
|
}
|
|
659
685
|
|
|
660
|
-
function createTempImageFile(buffer, mediaType, uploadId) {
|
|
661
|
-
const isLinux = process.platform !== 'win32' && process.platform !== 'darwin';
|
|
662
|
-
const tmpDir = isLinux
|
|
663
|
-
? IMAGE_UPLOAD_DIR
|
|
664
|
-
: (process.env.CLAUDE_CODE_TMPDIR || os.tmpdir());
|
|
686
|
+
function createTempImageFile(buffer, mediaType, uploadId) {
|
|
687
|
+
const isLinux = process.platform !== 'win32' && process.platform !== 'darwin';
|
|
688
|
+
const tmpDir = isLinux
|
|
689
|
+
? IMAGE_UPLOAD_DIR
|
|
690
|
+
: (process.env.CLAUDE_CODE_TMPDIR || os.tmpdir());
|
|
665
691
|
const type = String(mediaType || 'image/png').toLowerCase();
|
|
666
692
|
const ext = type.includes('jpeg') || type.includes('jpg') ? '.jpg' : '.png';
|
|
667
693
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
668
694
|
const tmpFile = path.join(tmpDir, `bridge_upload_${uploadId}_${Date.now()}${ext}`);
|
|
669
|
-
fs.writeFileSync(tmpFile, buffer);
|
|
670
|
-
return tmpFile;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function
|
|
674
|
-
if (process.platform === 'win32' || process.platform === 'darwin') return null;
|
|
675
|
-
try {
|
|
676
|
-
execSync('command -v xclip >/dev/null 2>&1', {
|
|
677
|
-
stdio: 'ignore',
|
|
678
|
-
shell: '/bin/sh',
|
|
679
|
-
timeout: 2000,
|
|
680
|
-
});
|
|
681
|
-
return 'xclip';
|
|
682
|
-
} catch {}
|
|
695
|
+
fs.writeFileSync(tmpFile, buffer);
|
|
696
|
+
return tmpFile;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function isLinuxClipboardToolInstalled(tool) {
|
|
683
700
|
try {
|
|
684
|
-
execSync(
|
|
701
|
+
execSync(`command -v ${tool} >/dev/null 2>&1`, {
|
|
685
702
|
stdio: 'ignore',
|
|
686
703
|
shell: '/bin/sh',
|
|
687
704
|
timeout: 2000,
|
|
688
705
|
});
|
|
689
|
-
return
|
|
690
|
-
} catch {
|
|
691
|
-
|
|
706
|
+
return true;
|
|
707
|
+
} catch {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function setLinuxImagePasteInFlight(active, reason = '') {
|
|
713
|
+
linuxImagePasteInFlight = !!active;
|
|
714
|
+
if (reason) log(`Linux image paste lock=${linuxImagePasteInFlight ? 'on' : 'off'} reason=${reason}`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getLinuxClipboardToolCandidates() {
|
|
718
|
+
if (process.platform === 'win32' || process.platform === 'darwin') return [];
|
|
719
|
+
const preferred = [];
|
|
720
|
+
if (process.env.WAYLAND_DISPLAY) preferred.push('wl-copy');
|
|
721
|
+
if (process.env.DISPLAY) preferred.push('xclip');
|
|
722
|
+
return preferred;
|
|
692
723
|
}
|
|
693
724
|
|
|
694
725
|
function assertLinuxClipboardAvailable() {
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
726
|
+
const candidates = getLinuxClipboardToolCandidates();
|
|
727
|
+
const available = candidates.filter(isLinuxClipboardToolInstalled);
|
|
728
|
+
if (available.length > 0) return available;
|
|
729
|
+
if (!process.env.WAYLAND_DISPLAY && !process.env.DISPLAY) {
|
|
730
|
+
throw new Error('Linux image paste requires an active graphical session (WAYLAND_DISPLAY or DISPLAY).');
|
|
731
|
+
}
|
|
732
|
+
throw new Error('Linux image paste requires wl-copy or xclip on the server. Install a matching clipboard tool and try again.');
|
|
698
733
|
}
|
|
699
734
|
|
|
700
|
-
function
|
|
735
|
+
function spawnLinuxClipboardTool(tool, imageBuffer, type) {
|
|
736
|
+
return new Promise((resolve, reject) => {
|
|
737
|
+
const args = tool === 'xclip'
|
|
738
|
+
? ['-selection', 'clipboard', '-t', type, '-i']
|
|
739
|
+
: ['--type', type];
|
|
740
|
+
const child = spawn(tool, args, {
|
|
741
|
+
detached: true,
|
|
742
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
743
|
+
});
|
|
744
|
+
let settled = false;
|
|
745
|
+
let stderr = '';
|
|
746
|
+
let readyTimer = null;
|
|
747
|
+
|
|
748
|
+
const settleFailure = (message) => {
|
|
749
|
+
if (settled) return;
|
|
750
|
+
settled = true;
|
|
751
|
+
if (readyTimer) clearTimeout(readyTimer);
|
|
752
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
753
|
+
reject(new Error(message));
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const settleSuccess = () => {
|
|
757
|
+
if (settled) return;
|
|
758
|
+
settled = true;
|
|
759
|
+
if (readyTimer) clearTimeout(readyTimer);
|
|
760
|
+
activeLinuxClipboardProc = { child, tool };
|
|
761
|
+
child.unref();
|
|
762
|
+
resolve(tool);
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
child.on('error', (err) => {
|
|
766
|
+
log(`Linux clipboard process error (${tool}): ${err.message}`);
|
|
767
|
+
settleFailure(`Linux clipboard tool ${tool} failed: ${err.message}`);
|
|
768
|
+
});
|
|
769
|
+
child.stderr.on('data', (chunk) => {
|
|
770
|
+
stderr += chunk.toString('utf8');
|
|
771
|
+
if (stderr.length > 2000) stderr = stderr.slice(-2000);
|
|
772
|
+
});
|
|
773
|
+
child.on('exit', (code, signal) => {
|
|
774
|
+
if (activeLinuxClipboardProc && activeLinuxClipboardProc.child === child) activeLinuxClipboardProc = null;
|
|
775
|
+
const extra = stderr.trim() ? ` stderr=${JSON.stringify(stderr.trim())}` : '';
|
|
776
|
+
log(`Linux clipboard process exited (${tool}) code=${code ?? 'null'} signal=${signal ?? 'null'}${extra}`);
|
|
777
|
+
if (!settled) {
|
|
778
|
+
const detail = stderr.trim() || `exit code ${code ?? 'null'} signal ${signal ?? 'null'}`;
|
|
779
|
+
settleFailure(`Linux clipboard tool ${tool} exited before paste: ${detail}`);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
child.stdin.on('error', (err) => {
|
|
783
|
+
if (err.code === 'EPIPE') {
|
|
784
|
+
settleFailure(`Linux clipboard tool ${tool} closed its input early`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
log(`Linux clipboard stdin error (${tool}): ${err.message}`);
|
|
788
|
+
settleFailure(`Linux clipboard tool ${tool} stdin failed: ${err.message}`);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
child.stdin.end(imageBuffer);
|
|
792
|
+
log(`Linux clipboard process started (${tool}) pid=${child.pid ?? 'null'} type=${type} bytes=${imageBuffer.length}`);
|
|
793
|
+
readyTimer = setTimeout(() => settleSuccess(), LINUX_CLIPBOARD_READY_GRACE_MS);
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function startLinuxClipboardImage(tmpFile, mediaType) {
|
|
701
798
|
const type = String(mediaType || 'image/png').toLowerCase();
|
|
702
|
-
const tool = assertLinuxClipboardAvailable();
|
|
703
799
|
const imageBuffer = fs.readFileSync(tmpFile);
|
|
800
|
+
const availableTools = assertLinuxClipboardAvailable();
|
|
704
801
|
clearActiveLinuxClipboardProc('replace');
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
child.stderr.on('data', (chunk) => {
|
|
718
|
-
stderr += chunk.toString('utf8');
|
|
719
|
-
if (stderr.length > 2000) stderr = stderr.slice(-2000);
|
|
720
|
-
});
|
|
721
|
-
child.on('exit', (code, signal) => {
|
|
722
|
-
if (activeLinuxClipboardProc && activeLinuxClipboardProc.child === child) activeLinuxClipboardProc = null;
|
|
723
|
-
const extra = stderr.trim() ? ` stderr=${JSON.stringify(stderr.trim())}` : '';
|
|
724
|
-
log(`Linux clipboard process exited (${tool}) code=${code ?? 'null'} signal=${signal ?? 'null'}${extra}`);
|
|
725
|
-
});
|
|
726
|
-
child.stdin.on('error', (err) => {
|
|
727
|
-
if (err.code !== 'EPIPE') log(`Linux clipboard stdin error (${tool}): ${err.message}`);
|
|
728
|
-
});
|
|
729
|
-
child.stdin.end(imageBuffer);
|
|
730
|
-
child.unref();
|
|
731
|
-
log(`Linux clipboard process started (${tool}) pid=${child.pid ?? 'null'} type=${type} bytes=${imageBuffer.length}`);
|
|
732
|
-
return tool;
|
|
802
|
+
|
|
803
|
+
let lastErr = null;
|
|
804
|
+
for (const tool of availableTools) {
|
|
805
|
+
try {
|
|
806
|
+
return await spawnLinuxClipboardTool(tool, imageBuffer, type);
|
|
807
|
+
} catch (err) {
|
|
808
|
+
lastErr = err;
|
|
809
|
+
log(`Linux clipboard arm failed (${tool}): ${err.message}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
throw lastErr || new Error('Linux clipboard could not be initialized');
|
|
733
814
|
}
|
|
734
815
|
|
|
735
816
|
setInterval(() => {
|
|
@@ -899,6 +980,14 @@ wss.on('connection', (ws, req) => {
|
|
|
899
980
|
// Raw terminal keystrokes from xterm.js in WebUI
|
|
900
981
|
if (claudeProc) claudeProc.write(msg.data);
|
|
901
982
|
break;
|
|
983
|
+
case 'interrupt': {
|
|
984
|
+
// User pressed stop button in app — send Ctrl+C to PTY
|
|
985
|
+
if (!claudeProc || turnState.phase !== 'running') break;
|
|
986
|
+
log(`Interrupt from ${wsLabel(ws)} — sending Ctrl+C to PTY`);
|
|
987
|
+
claudeProc.write('\x03');
|
|
988
|
+
emitInterrupt('app');
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
902
991
|
case 'expect_clear':
|
|
903
992
|
// Plan mode option 1 triggers /clear inside Claude Code;
|
|
904
993
|
// client notifies us so we can detect the session switch.
|
|
@@ -966,25 +1055,25 @@ wss.on('connection', (ws, req) => {
|
|
|
966
1055
|
}
|
|
967
1056
|
break;
|
|
968
1057
|
}
|
|
969
|
-
case 'image_upload_init': {
|
|
970
|
-
const uploadId = String(msg.uploadId || '');
|
|
971
|
-
if (!uploadId) {
|
|
972
|
-
sendUploadStatus(ws, '', 'error', { message: 'Missing uploadId' });
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
1058
|
+
case 'image_upload_init': {
|
|
1059
|
+
const uploadId = String(msg.uploadId || '');
|
|
1060
|
+
if (!uploadId) {
|
|
1061
|
+
sendUploadStatus(ws, '', 'error', { message: 'Missing uploadId' });
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
975
1064
|
cleanupImageUpload(uploadId);
|
|
976
1065
|
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
977
1066
|
try {
|
|
978
|
-
const
|
|
979
|
-
log(`Linux clipboard preflight OK: ${
|
|
1067
|
+
const tools = assertLinuxClipboardAvailable();
|
|
1068
|
+
log(`Linux clipboard preflight OK: ${tools.join(', ')}`);
|
|
980
1069
|
} catch (err) {
|
|
981
1070
|
sendUploadStatus(ws, uploadId, 'error', { message: err.message });
|
|
982
1071
|
break;
|
|
983
1072
|
}
|
|
984
1073
|
}
|
|
985
|
-
pendingImageUploads.set(uploadId, {
|
|
986
|
-
id: uploadId,
|
|
987
|
-
owner: ws,
|
|
1074
|
+
pendingImageUploads.set(uploadId, {
|
|
1075
|
+
id: uploadId,
|
|
1076
|
+
owner: ws,
|
|
988
1077
|
mediaType: msg.mediaType || 'image/png',
|
|
989
1078
|
name: msg.name || 'image',
|
|
990
1079
|
totalBytes: Number.isFinite(msg.totalBytes) ? msg.totalBytes : 0,
|
|
@@ -1082,15 +1171,15 @@ wss.on('connection', (ws, req) => {
|
|
|
1082
1171
|
if (!upload || !upload.tmpFile) {
|
|
1083
1172
|
sendUploadStatus(ws, uploadId, 'error', { message: 'Upload not ready' });
|
|
1084
1173
|
break;
|
|
1085
|
-
}
|
|
1174
|
+
}
|
|
1086
1175
|
try {
|
|
1087
|
-
handlePreparedImageUpload({
|
|
1176
|
+
await handlePreparedImageUpload({
|
|
1088
1177
|
tmpFile: upload.tmpFile,
|
|
1089
1178
|
mediaType: upload.mediaType,
|
|
1090
1179
|
text: msg.text || '',
|
|
1091
1180
|
logLabel: upload.name || uploadId,
|
|
1092
1181
|
onCleanup: () => cleanupImageUpload(uploadId),
|
|
1093
|
-
});
|
|
1182
|
+
});
|
|
1094
1183
|
upload.submitted = true;
|
|
1095
1184
|
upload.updatedAt = Date.now();
|
|
1096
1185
|
setTurnState('running', { reason: 'image_submit' });
|
|
@@ -1717,81 +1806,104 @@ function restartClaude(newCwd) {
|
|
|
1717
1806
|
// ============================================================
|
|
1718
1807
|
// 5. Image Upload → Clipboard Injection
|
|
1719
1808
|
// ============================================================
|
|
1720
|
-
function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', onCleanup = null }) {
|
|
1721
|
-
if (!claudeProc) throw new Error('Claude not running');
|
|
1722
|
-
if (!tmpFile || !fs.existsSync(tmpFile)) throw new Error('Prepared image file missing');
|
|
1723
|
-
|
|
1724
|
-
const isWin = process.platform === 'win32';
|
|
1725
|
-
const isMac = process.platform === 'darwin';
|
|
1809
|
+
async function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', onCleanup = null }) {
|
|
1810
|
+
if (!claudeProc) throw new Error('Claude not running');
|
|
1811
|
+
if (!tmpFile || !fs.existsSync(tmpFile)) throw new Error('Prepared image file missing');
|
|
1812
|
+
|
|
1813
|
+
const isWin = process.platform === 'win32';
|
|
1814
|
+
const isMac = process.platform === 'darwin';
|
|
1815
|
+
const isLinux = !isWin && !isMac;
|
|
1816
|
+
if (isLinux && linuxImagePasteInFlight) {
|
|
1817
|
+
throw new Error('Another Linux image paste is still in progress. Please wait a moment and try again.');
|
|
1818
|
+
}
|
|
1726
1819
|
try {
|
|
1727
|
-
const stat = fs.statSync(tmpFile);
|
|
1728
|
-
log(`Image ready: ${logLabel || path.basename(tmpFile)} (${stat.size} bytes)`);
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1820
|
+
const stat = fs.statSync(tmpFile);
|
|
1821
|
+
log(`Image ready: ${logLabel || path.basename(tmpFile)} (${stat.size} bytes)`);
|
|
1822
|
+
if (isLinux) setLinuxImagePasteInFlight(true, 'prepare_upload');
|
|
1823
|
+
|
|
1824
|
+
if (isWin) {
|
|
1825
|
+
const psCmd = `Add-Type -AssemblyName System.Drawing; Add-Type -AssemblyName System.Windows.Forms; $img = [System.Drawing.Image]::FromFile('${tmpFile.replace(/'/g, "''")}'); [System.Windows.Forms.Clipboard]::SetImage($img); $img.Dispose()`;
|
|
1826
|
+
execSync(`powershell -NoProfile -STA -Command "${psCmd}"`, { timeout: 10000 });
|
|
1827
|
+
} else if (isMac) {
|
|
1734
1828
|
execSync(`osascript -e 'set the clipboard to (read POSIX file "${tmpFile}" as 芦class PNGf禄)'`, { timeout: 10000 });
|
|
1735
1829
|
} else {
|
|
1736
|
-
const tool = startLinuxClipboardImage(tmpFile, mediaType);
|
|
1830
|
+
const tool = await startLinuxClipboardImage(tmpFile, mediaType);
|
|
1737
1831
|
log(`Linux clipboard armed with ${tool}`);
|
|
1738
1832
|
}
|
|
1739
|
-
log('Clipboard set with image');
|
|
1740
|
-
|
|
1741
|
-
const pasteDelayMs = isWin || isMac ? 0 : 150;
|
|
1742
|
-
setTimeout(() => {
|
|
1743
|
-
if (!claudeProc) return;
|
|
1744
|
-
if (isWin) claudeProc.write('\x1bv');
|
|
1745
|
-
else claudeProc.write('\x16');
|
|
1746
|
-
log('Sent image paste keypress to PTY');
|
|
1747
|
-
}, pasteDelayMs);
|
|
1833
|
+
log('Clipboard set with image');
|
|
1748
1834
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
if (claudeProc) claudeProc.write('\r');
|
|
1756
|
-
log('Sent Enter after image paste' + (trimmedText ? ` + text: "${trimmedText.substring(0, 60)}"` : ''));
|
|
1757
|
-
|
|
1758
|
-
if (!isWin && !isMac) {
|
|
1759
|
-
setTimeout(() => clearActiveLinuxClipboardProc('post-paste'), 1000);
|
|
1835
|
+
const pasteDelayMs = isWin || isMac ? 0 : 150;
|
|
1836
|
+
await new Promise((resolve, reject) => {
|
|
1837
|
+
setTimeout(() => {
|
|
1838
|
+
if (!claudeProc) {
|
|
1839
|
+
reject(new Error('Claude stopped before image paste'));
|
|
1840
|
+
return;
|
|
1760
1841
|
}
|
|
1842
|
+
if (isWin) claudeProc.write('\x1bv');
|
|
1843
|
+
else claudeProc.write('\x16');
|
|
1844
|
+
log('Sent image paste keypress to PTY');
|
|
1761
1845
|
|
|
1762
1846
|
setTimeout(() => {
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1847
|
+
if (!claudeProc) {
|
|
1848
|
+
reject(new Error('Claude stopped before image prompt'));
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
const trimmedText = (text || '').trim();
|
|
1852
|
+
if (trimmedText) claudeProc.write(trimmedText);
|
|
1853
|
+
|
|
1854
|
+
setTimeout(() => {
|
|
1855
|
+
if (!claudeProc) {
|
|
1856
|
+
reject(new Error('Claude stopped before image submit'));
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
claudeProc.write('\r');
|
|
1860
|
+
log('Sent Enter after image paste' + (trimmedText ? ` + text: "${trimmedText.substring(0, 60)}"` : ''));
|
|
1861
|
+
|
|
1862
|
+
if (isLinux) {
|
|
1863
|
+
setTimeout(() => clearActiveLinuxClipboardProc('post-paste'), 1000);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
setTimeout(() => {
|
|
1867
|
+
if (isLinux) setLinuxImagePasteInFlight(false, 'cleanup');
|
|
1868
|
+
if (onCleanup) onCleanup();
|
|
1869
|
+
else {
|
|
1870
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1871
|
+
}
|
|
1872
|
+
}, 5000);
|
|
1873
|
+
resolve();
|
|
1874
|
+
}, 150);
|
|
1875
|
+
}, 1000);
|
|
1876
|
+
}, pasteDelayMs);
|
|
1877
|
+
});
|
|
1878
|
+
} catch (err) {
|
|
1879
|
+
log(`Image upload error: ${err.message}`);
|
|
1880
|
+
if (isLinux) {
|
|
1881
|
+
clearActiveLinuxClipboardProc('error');
|
|
1882
|
+
setLinuxImagePasteInFlight(false, 'error');
|
|
1883
|
+
}
|
|
1884
|
+
if (onCleanup) onCleanup();
|
|
1885
|
+
else {
|
|
1886
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1887
|
+
}
|
|
1776
1888
|
throw err;
|
|
1777
1889
|
}
|
|
1778
1890
|
}
|
|
1779
1891
|
|
|
1780
|
-
function handleImageUpload(msg) {
|
|
1781
|
-
if (!claudeProc) {
|
|
1782
|
-
log('Image upload ignored: Claude not running');
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
if (!msg.base64) {
|
|
1786
|
-
log('Image upload ignored: no base64 data');
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
let tmpFile = null;
|
|
1790
|
-
|
|
1892
|
+
function handleImageUpload(msg) {
|
|
1893
|
+
if (!claudeProc) {
|
|
1894
|
+
log('Image upload ignored: Claude not running');
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
if (!msg.base64) {
|
|
1898
|
+
log('Image upload ignored: no base64 data');
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
let tmpFile = null;
|
|
1902
|
+
|
|
1791
1903
|
try {
|
|
1792
1904
|
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
1793
|
-
const
|
|
1794
|
-
log(`Linux clipboard preflight OK (legacy upload): ${
|
|
1905
|
+
const tools = assertLinuxClipboardAvailable();
|
|
1906
|
+
log(`Linux clipboard preflight OK (legacy upload): ${tools.join(', ')}`);
|
|
1795
1907
|
}
|
|
1796
1908
|
const buf = Buffer.from(msg.base64, 'base64');
|
|
1797
1909
|
tmpFile = createTempImageFile(buf, msg.mediaType, `legacy_${Date.now()}`);
|
|
@@ -1800,13 +1912,16 @@ function handleImageUpload(msg) {
|
|
|
1800
1912
|
tmpFile,
|
|
1801
1913
|
mediaType: msg.mediaType,
|
|
1802
1914
|
text: msg.text || '',
|
|
1915
|
+
}).then(() => {
|
|
1916
|
+
setTurnState('running', { reason: 'legacy_image_upload' });
|
|
1917
|
+
}).catch((err) => {
|
|
1918
|
+
log(`Image upload error: ${err.message}`);
|
|
1803
1919
|
});
|
|
1804
|
-
setTurnState('running', { reason: 'legacy_image_upload' });
|
|
1805
1920
|
} catch (err) {
|
|
1806
|
-
log(`Image upload error: ${err.message}`);
|
|
1807
|
-
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1921
|
+
log(`Image upload error: ${err.message}`);
|
|
1922
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1810
1925
|
|
|
1811
1926
|
// ============================================================
|
|
1812
1927
|
// 6. Hook Auto-Setup
|