claude-remote 0.4.4 → 0.4.5
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 +228 -196
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;
|
|
@@ -194,10 +194,10 @@ let turnState = {
|
|
|
194
194
|
version: 0,
|
|
195
195
|
updatedAt: Date.now(),
|
|
196
196
|
};
|
|
197
|
-
let ttyInputForwarderAttached = false;
|
|
198
|
-
let ttyInputHandler = null;
|
|
199
|
-
let ttyResizeHandler = null;
|
|
200
|
-
let activeLinuxClipboardProc = null;
|
|
197
|
+
let ttyInputForwarderAttached = false;
|
|
198
|
+
let ttyInputHandler = null;
|
|
199
|
+
let ttyResizeHandler = null;
|
|
200
|
+
let activeLinuxClipboardProc = null;
|
|
201
201
|
|
|
202
202
|
// --- Permission approval state ---
|
|
203
203
|
let approvalSeq = 0;
|
|
@@ -210,27 +210,27 @@ const PARTIAL_AUTO_ALLOW = new Set(['Read', 'Glob', 'Grep', 'Write', 'Edit']);
|
|
|
210
210
|
// --- Logging → file only (never pollute the terminal) ---
|
|
211
211
|
const LOG_FILE = path.join(os.homedir(), '.claude', 'bridge.log');
|
|
212
212
|
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
|
-
|
|
223
|
-
function clearActiveLinuxClipboardProc(reason = '') {
|
|
224
|
-
if (!activeLinuxClipboardProc) return;
|
|
225
|
-
const { child, tool } = activeLinuxClipboardProc;
|
|
226
|
-
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
|
-
}
|
|
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
|
+
|
|
223
|
+
function clearActiveLinuxClipboardProc(reason = '') {
|
|
224
|
+
if (!activeLinuxClipboardProc) return;
|
|
225
|
+
const { child, tool } = activeLinuxClipboardProc;
|
|
226
|
+
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
|
+
}
|
|
234
234
|
|
|
235
235
|
function wsLabel(ws) {
|
|
236
236
|
const clientId = ws && ws._clientInstanceId ? ` client=${ws._clientInstanceId}` : '';
|
|
@@ -263,6 +263,7 @@ function getTurnStatePayload() {
|
|
|
263
263
|
sessionId: turnState.sessionId,
|
|
264
264
|
version: turnState.version,
|
|
265
265
|
updatedAt: turnState.updatedAt,
|
|
266
|
+
reason: turnState.reason || '',
|
|
266
267
|
};
|
|
267
268
|
}
|
|
268
269
|
|
|
@@ -284,6 +285,7 @@ function setTurnState(phase, { sessionId = currentSessionId, reason = '', force
|
|
|
284
285
|
sessionId: normalizedSessionId,
|
|
285
286
|
version: ++turnStateVersion,
|
|
286
287
|
updatedAt: Date.now(),
|
|
288
|
+
reason,
|
|
287
289
|
};
|
|
288
290
|
|
|
289
291
|
log(`Turn state -> phase=${turnState.phase} session=${turnState.sessionId ?? 'null'} version=${turnState.version}${reason ? ` reason=${reason}` : ''}`);
|
|
@@ -291,19 +293,41 @@ function setTurnState(phase, { sessionId = currentSessionId, reason = '', force
|
|
|
291
293
|
return true;
|
|
292
294
|
}
|
|
293
295
|
|
|
296
|
+
function emitInterrupt(source) {
|
|
297
|
+
const interruptEvent = {
|
|
298
|
+
type: 'interrupt',
|
|
299
|
+
source,
|
|
300
|
+
timestamp: Date.now(),
|
|
301
|
+
uuid: crypto.randomUUID(),
|
|
302
|
+
};
|
|
303
|
+
const record = { seq: ++eventSeq, event: interruptEvent };
|
|
304
|
+
eventBuffer.push(record);
|
|
305
|
+
if (eventBuffer.length > EVENT_BUFFER_MAX) {
|
|
306
|
+
eventBuffer = eventBuffer.slice(-Math.round(EVENT_BUFFER_MAX * 0.8));
|
|
307
|
+
}
|
|
308
|
+
broadcast({ type: 'log_event', seq: record.seq, event: interruptEvent });
|
|
309
|
+
setTurnState('idle', { reason: `${source}_interrupt` });
|
|
310
|
+
}
|
|
311
|
+
|
|
294
312
|
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
|
-
|
|
313
|
+
if (!isTTY || ttyInputForwarderAttached) return;
|
|
314
|
+
|
|
315
|
+
ttyInputHandler = (chunk) => {
|
|
316
|
+
if (DEBUG_TTY_INPUT) {
|
|
317
|
+
try {
|
|
318
|
+
log(`TTY input ${formatTtyInputChunk(chunk)}`);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
log(`TTY input log error: ${err.message}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (claudeProc) claudeProc.write(chunk);
|
|
324
|
+
// Detect Ctrl+C (0x03) from local terminal and sync state
|
|
325
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
326
|
+
if (buf.includes(0x03) && turnState.phase === 'running') {
|
|
327
|
+
log('Terminal Ctrl+C detected — injecting interrupt event');
|
|
328
|
+
emitInterrupt('terminal');
|
|
329
|
+
}
|
|
330
|
+
};
|
|
307
331
|
ttyResizeHandler = () => {
|
|
308
332
|
if (claudeProc) claudeProc.resize(process.stdout.columns, process.stdout.rows);
|
|
309
333
|
};
|
|
@@ -657,80 +681,80 @@ function cleanupClientUploads(ws) {
|
|
|
657
681
|
}
|
|
658
682
|
}
|
|
659
683
|
|
|
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());
|
|
684
|
+
function createTempImageFile(buffer, mediaType, uploadId) {
|
|
685
|
+
const isLinux = process.platform !== 'win32' && process.platform !== 'darwin';
|
|
686
|
+
const tmpDir = isLinux
|
|
687
|
+
? IMAGE_UPLOAD_DIR
|
|
688
|
+
: (process.env.CLAUDE_CODE_TMPDIR || os.tmpdir());
|
|
665
689
|
const type = String(mediaType || 'image/png').toLowerCase();
|
|
666
690
|
const ext = type.includes('jpeg') || type.includes('jpg') ? '.jpg' : '.png';
|
|
667
691
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
668
692
|
const tmpFile = path.join(tmpDir, `bridge_upload_${uploadId}_${Date.now()}${ext}`);
|
|
669
|
-
fs.writeFileSync(tmpFile, buffer);
|
|
670
|
-
return tmpFile;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function getLinuxClipboardTool() {
|
|
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 {}
|
|
683
|
-
try {
|
|
684
|
-
execSync('command -v wl-copy >/dev/null 2>&1', {
|
|
685
|
-
stdio: 'ignore',
|
|
686
|
-
shell: '/bin/sh',
|
|
687
|
-
timeout: 2000,
|
|
688
|
-
});
|
|
689
|
-
return 'wl-copy';
|
|
690
|
-
} catch {}
|
|
691
|
-
return null;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function assertLinuxClipboardAvailable() {
|
|
695
|
-
const tool = getLinuxClipboardTool();
|
|
696
|
-
if (tool) return tool;
|
|
697
|
-
throw new Error('Linux image paste requires xclip or wl-copy on the server. Install one and try again.');
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function startLinuxClipboardImage(tmpFile, mediaType) {
|
|
701
|
-
const type = String(mediaType || 'image/png').toLowerCase();
|
|
702
|
-
const tool = assertLinuxClipboardAvailable();
|
|
703
|
-
const imageBuffer = fs.readFileSync(tmpFile);
|
|
704
|
-
clearActiveLinuxClipboardProc('replace');
|
|
705
|
-
const args = tool === 'xclip'
|
|
706
|
-
? ['-selection', 'clipboard', '-t', type, '-i']
|
|
707
|
-
: ['--type', type];
|
|
708
|
-
const child = spawn(tool, args, {
|
|
709
|
-
detached: true,
|
|
710
|
-
stdio: ['pipe', 'ignore', 'pipe'],
|
|
711
|
-
});
|
|
712
|
-
activeLinuxClipboardProc = { child, tool };
|
|
713
|
-
let stderr = '';
|
|
714
|
-
child.on('error', (err) => {
|
|
715
|
-
log(`Linux clipboard process error (${tool}): ${err.message}`);
|
|
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;
|
|
733
|
-
}
|
|
693
|
+
fs.writeFileSync(tmpFile, buffer);
|
|
694
|
+
return tmpFile;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function getLinuxClipboardTool() {
|
|
698
|
+
if (process.platform === 'win32' || process.platform === 'darwin') return null;
|
|
699
|
+
try {
|
|
700
|
+
execSync('command -v xclip >/dev/null 2>&1', {
|
|
701
|
+
stdio: 'ignore',
|
|
702
|
+
shell: '/bin/sh',
|
|
703
|
+
timeout: 2000,
|
|
704
|
+
});
|
|
705
|
+
return 'xclip';
|
|
706
|
+
} catch {}
|
|
707
|
+
try {
|
|
708
|
+
execSync('command -v wl-copy >/dev/null 2>&1', {
|
|
709
|
+
stdio: 'ignore',
|
|
710
|
+
shell: '/bin/sh',
|
|
711
|
+
timeout: 2000,
|
|
712
|
+
});
|
|
713
|
+
return 'wl-copy';
|
|
714
|
+
} catch {}
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function assertLinuxClipboardAvailable() {
|
|
719
|
+
const tool = getLinuxClipboardTool();
|
|
720
|
+
if (tool) return tool;
|
|
721
|
+
throw new Error('Linux image paste requires xclip or wl-copy on the server. Install one and try again.');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function startLinuxClipboardImage(tmpFile, mediaType) {
|
|
725
|
+
const type = String(mediaType || 'image/png').toLowerCase();
|
|
726
|
+
const tool = assertLinuxClipboardAvailable();
|
|
727
|
+
const imageBuffer = fs.readFileSync(tmpFile);
|
|
728
|
+
clearActiveLinuxClipboardProc('replace');
|
|
729
|
+
const args = tool === 'xclip'
|
|
730
|
+
? ['-selection', 'clipboard', '-t', type, '-i']
|
|
731
|
+
: ['--type', type];
|
|
732
|
+
const child = spawn(tool, args, {
|
|
733
|
+
detached: true,
|
|
734
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
735
|
+
});
|
|
736
|
+
activeLinuxClipboardProc = { child, tool };
|
|
737
|
+
let stderr = '';
|
|
738
|
+
child.on('error', (err) => {
|
|
739
|
+
log(`Linux clipboard process error (${tool}): ${err.message}`);
|
|
740
|
+
});
|
|
741
|
+
child.stderr.on('data', (chunk) => {
|
|
742
|
+
stderr += chunk.toString('utf8');
|
|
743
|
+
if (stderr.length > 2000) stderr = stderr.slice(-2000);
|
|
744
|
+
});
|
|
745
|
+
child.on('exit', (code, signal) => {
|
|
746
|
+
if (activeLinuxClipboardProc && activeLinuxClipboardProc.child === child) activeLinuxClipboardProc = null;
|
|
747
|
+
const extra = stderr.trim() ? ` stderr=${JSON.stringify(stderr.trim())}` : '';
|
|
748
|
+
log(`Linux clipboard process exited (${tool}) code=${code ?? 'null'} signal=${signal ?? 'null'}${extra}`);
|
|
749
|
+
});
|
|
750
|
+
child.stdin.on('error', (err) => {
|
|
751
|
+
if (err.code !== 'EPIPE') log(`Linux clipboard stdin error (${tool}): ${err.message}`);
|
|
752
|
+
});
|
|
753
|
+
child.stdin.end(imageBuffer);
|
|
754
|
+
child.unref();
|
|
755
|
+
log(`Linux clipboard process started (${tool}) pid=${child.pid ?? 'null'} type=${type} bytes=${imageBuffer.length}`);
|
|
756
|
+
return tool;
|
|
757
|
+
}
|
|
734
758
|
|
|
735
759
|
setInterval(() => {
|
|
736
760
|
const now = Date.now();
|
|
@@ -899,6 +923,14 @@ wss.on('connection', (ws, req) => {
|
|
|
899
923
|
// Raw terminal keystrokes from xterm.js in WebUI
|
|
900
924
|
if (claudeProc) claudeProc.write(msg.data);
|
|
901
925
|
break;
|
|
926
|
+
case 'interrupt': {
|
|
927
|
+
// User pressed stop button in app — send Ctrl+C to PTY
|
|
928
|
+
if (!claudeProc || turnState.phase !== 'running') break;
|
|
929
|
+
log(`Interrupt from ${wsLabel(ws)} — sending Ctrl+C to PTY`);
|
|
930
|
+
claudeProc.write('\x03');
|
|
931
|
+
emitInterrupt('app');
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
902
934
|
case 'expect_clear':
|
|
903
935
|
// Plan mode option 1 triggers /clear inside Claude Code;
|
|
904
936
|
// client notifies us so we can detect the session switch.
|
|
@@ -966,25 +998,25 @@ wss.on('connection', (ws, req) => {
|
|
|
966
998
|
}
|
|
967
999
|
break;
|
|
968
1000
|
}
|
|
969
|
-
case 'image_upload_init': {
|
|
970
|
-
const uploadId = String(msg.uploadId || '');
|
|
971
|
-
if (!uploadId) {
|
|
972
|
-
sendUploadStatus(ws, '', 'error', { message: 'Missing uploadId' });
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
cleanupImageUpload(uploadId);
|
|
976
|
-
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
977
|
-
try {
|
|
978
|
-
const tool = assertLinuxClipboardAvailable();
|
|
979
|
-
log(`Linux clipboard preflight OK: ${tool}`);
|
|
980
|
-
} catch (err) {
|
|
981
|
-
sendUploadStatus(ws, uploadId, 'error', { message: err.message });
|
|
982
|
-
break;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
pendingImageUploads.set(uploadId, {
|
|
986
|
-
id: uploadId,
|
|
987
|
-
owner: ws,
|
|
1001
|
+
case 'image_upload_init': {
|
|
1002
|
+
const uploadId = String(msg.uploadId || '');
|
|
1003
|
+
if (!uploadId) {
|
|
1004
|
+
sendUploadStatus(ws, '', 'error', { message: 'Missing uploadId' });
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
cleanupImageUpload(uploadId);
|
|
1008
|
+
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
1009
|
+
try {
|
|
1010
|
+
const tool = assertLinuxClipboardAvailable();
|
|
1011
|
+
log(`Linux clipboard preflight OK: ${tool}`);
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
sendUploadStatus(ws, uploadId, 'error', { message: err.message });
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
pendingImageUploads.set(uploadId, {
|
|
1018
|
+
id: uploadId,
|
|
1019
|
+
owner: ws,
|
|
988
1020
|
mediaType: msg.mediaType || 'image/png',
|
|
989
1021
|
name: msg.name || 'image',
|
|
990
1022
|
totalBytes: Number.isFinite(msg.totalBytes) ? msg.totalBytes : 0,
|
|
@@ -1082,15 +1114,15 @@ wss.on('connection', (ws, req) => {
|
|
|
1082
1114
|
if (!upload || !upload.tmpFile) {
|
|
1083
1115
|
sendUploadStatus(ws, uploadId, 'error', { message: 'Upload not ready' });
|
|
1084
1116
|
break;
|
|
1085
|
-
}
|
|
1086
|
-
try {
|
|
1087
|
-
handlePreparedImageUpload({
|
|
1088
|
-
tmpFile: upload.tmpFile,
|
|
1089
|
-
mediaType: upload.mediaType,
|
|
1090
|
-
text: msg.text || '',
|
|
1091
|
-
logLabel: upload.name || uploadId,
|
|
1092
|
-
onCleanup: () => cleanupImageUpload(uploadId),
|
|
1093
|
-
});
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
handlePreparedImageUpload({
|
|
1120
|
+
tmpFile: upload.tmpFile,
|
|
1121
|
+
mediaType: upload.mediaType,
|
|
1122
|
+
text: msg.text || '',
|
|
1123
|
+
logLabel: upload.name || uploadId,
|
|
1124
|
+
onCleanup: () => cleanupImageUpload(uploadId),
|
|
1125
|
+
});
|
|
1094
1126
|
upload.submitted = true;
|
|
1095
1127
|
upload.updatedAt = Date.now();
|
|
1096
1128
|
setTurnState('running', { reason: 'image_submit' });
|
|
@@ -1723,7 +1755,7 @@ function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', on
|
|
|
1723
1755
|
|
|
1724
1756
|
const isWin = process.platform === 'win32';
|
|
1725
1757
|
const isMac = process.platform === 'darwin';
|
|
1726
|
-
try {
|
|
1758
|
+
try {
|
|
1727
1759
|
const stat = fs.statSync(tmpFile);
|
|
1728
1760
|
log(`Image ready: ${logLabel || path.basename(tmpFile)} (${stat.size} bytes)`);
|
|
1729
1761
|
|
|
@@ -1732,41 +1764,41 @@ function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', on
|
|
|
1732
1764
|
execSync(`powershell -NoProfile -STA -Command "${psCmd}"`, { timeout: 10000 });
|
|
1733
1765
|
} else if (isMac) {
|
|
1734
1766
|
execSync(`osascript -e 'set the clipboard to (read POSIX file "${tmpFile}" as 芦class PNGf禄)'`, { timeout: 10000 });
|
|
1735
|
-
} else {
|
|
1736
|
-
const tool = startLinuxClipboardImage(tmpFile, mediaType);
|
|
1737
|
-
log(`Linux clipboard armed with ${tool}`);
|
|
1738
|
-
}
|
|
1767
|
+
} else {
|
|
1768
|
+
const tool = startLinuxClipboardImage(tmpFile, mediaType);
|
|
1769
|
+
log(`Linux clipboard armed with ${tool}`);
|
|
1770
|
+
}
|
|
1739
1771
|
log('Clipboard set with image');
|
|
1740
1772
|
|
|
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);
|
|
1748
|
-
|
|
1749
|
-
setTimeout(() => {
|
|
1750
|
-
if (!claudeProc) return;
|
|
1751
|
-
const trimmedText = (text || '').trim();
|
|
1752
|
-
if (trimmedText) claudeProc.write(trimmedText);
|
|
1773
|
+
const pasteDelayMs = isWin || isMac ? 0 : 150;
|
|
1774
|
+
setTimeout(() => {
|
|
1775
|
+
if (!claudeProc) return;
|
|
1776
|
+
if (isWin) claudeProc.write('\x1bv');
|
|
1777
|
+
else claudeProc.write('\x16');
|
|
1778
|
+
log('Sent image paste keypress to PTY');
|
|
1779
|
+
}, pasteDelayMs);
|
|
1780
|
+
|
|
1781
|
+
setTimeout(() => {
|
|
1782
|
+
if (!claudeProc) return;
|
|
1783
|
+
const trimmedText = (text || '').trim();
|
|
1784
|
+
if (trimmedText) claudeProc.write(trimmedText);
|
|
1753
1785
|
|
|
1754
1786
|
setTimeout(() => {
|
|
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);
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
setTimeout(() => {
|
|
1763
|
-
if (onCleanup) onCleanup();
|
|
1787
|
+
if (claudeProc) claudeProc.write('\r');
|
|
1788
|
+
log('Sent Enter after image paste' + (trimmedText ? ` + text: "${trimmedText.substring(0, 60)}"` : ''));
|
|
1789
|
+
|
|
1790
|
+
if (!isWin && !isMac) {
|
|
1791
|
+
setTimeout(() => clearActiveLinuxClipboardProc('post-paste'), 1000);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
setTimeout(() => {
|
|
1795
|
+
if (onCleanup) onCleanup();
|
|
1764
1796
|
else {
|
|
1765
1797
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1766
1798
|
}
|
|
1767
1799
|
}, 5000);
|
|
1768
|
-
}, 150);
|
|
1769
|
-
}, 1000 + pasteDelayMs);
|
|
1800
|
+
}, 150);
|
|
1801
|
+
}, 1000 + pasteDelayMs);
|
|
1770
1802
|
} catch (err) {
|
|
1771
1803
|
log(`Image upload error: ${err.message}`);
|
|
1772
1804
|
if (onCleanup) onCleanup();
|
|
@@ -1777,32 +1809,32 @@ function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', on
|
|
|
1777
1809
|
}
|
|
1778
1810
|
}
|
|
1779
1811
|
|
|
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
|
-
|
|
1791
|
-
try {
|
|
1792
|
-
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
1793
|
-
const tool = assertLinuxClipboardAvailable();
|
|
1794
|
-
log(`Linux clipboard preflight OK (legacy upload): ${tool}`);
|
|
1795
|
-
}
|
|
1796
|
-
const buf = Buffer.from(msg.base64, 'base64');
|
|
1797
|
-
tmpFile = createTempImageFile(buf, msg.mediaType, `legacy_${Date.now()}`);
|
|
1798
|
-
log(`Image saved: ${tmpFile} (${buf.length} bytes)`);
|
|
1799
|
-
handlePreparedImageUpload({
|
|
1800
|
-
tmpFile,
|
|
1801
|
-
mediaType: msg.mediaType,
|
|
1802
|
-
text: msg.text || '',
|
|
1803
|
-
});
|
|
1804
|
-
setTurnState('running', { reason: 'legacy_image_upload' });
|
|
1805
|
-
} catch (err) {
|
|
1812
|
+
function handleImageUpload(msg) {
|
|
1813
|
+
if (!claudeProc) {
|
|
1814
|
+
log('Image upload ignored: Claude not running');
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (!msg.base64) {
|
|
1818
|
+
log('Image upload ignored: no base64 data');
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
let tmpFile = null;
|
|
1822
|
+
|
|
1823
|
+
try {
|
|
1824
|
+
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
1825
|
+
const tool = assertLinuxClipboardAvailable();
|
|
1826
|
+
log(`Linux clipboard preflight OK (legacy upload): ${tool}`);
|
|
1827
|
+
}
|
|
1828
|
+
const buf = Buffer.from(msg.base64, 'base64');
|
|
1829
|
+
tmpFile = createTempImageFile(buf, msg.mediaType, `legacy_${Date.now()}`);
|
|
1830
|
+
log(`Image saved: ${tmpFile} (${buf.length} bytes)`);
|
|
1831
|
+
handlePreparedImageUpload({
|
|
1832
|
+
tmpFile,
|
|
1833
|
+
mediaType: msg.mediaType,
|
|
1834
|
+
text: msg.text || '',
|
|
1835
|
+
});
|
|
1836
|
+
setTurnState('running', { reason: 'legacy_image_upload' });
|
|
1837
|
+
} catch (err) {
|
|
1806
1838
|
log(`Image upload error: ${err.message}`);
|
|
1807
1839
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1808
1840
|
}
|