claude-remote 0.4.2 → 0.4.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +77 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Remote control bridge for Claude Code REPL - drive from phone/WebUI",
5
5
  "main": "server.js",
6
6
  "bin": {
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 } = 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.
@@ -194,9 +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;
197
+ let ttyInputForwarderAttached = false;
198
+ let ttyInputHandler = null;
199
+ let ttyResizeHandler = null;
200
+ let activeLinuxClipboardProc = null;
200
201
 
201
202
  // --- Permission approval state ---
202
203
  let approvalSeq = 0;
@@ -218,6 +219,18 @@ function formatTtyInputChunk(chunk) {
218
219
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
219
220
  return `len=${buf.length} hex=${buf.toString('hex')} base64=${buf.toString('base64')} utf8=${JSON.stringify(buf.toString('utf8'))}`;
220
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
+ }
221
234
 
222
235
  function wsLabel(ws) {
223
236
  const clientId = ws && ws._clientInstanceId ? ` client=${ws._clientInstanceId}` : '';
@@ -657,10 +670,6 @@ function createTempImageFile(buffer, mediaType, uploadId) {
657
670
  return tmpFile;
658
671
  }
659
672
 
660
- function shellQuote(value) {
661
- return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
662
- }
663
-
664
673
  function getLinuxClipboardTool() {
665
674
  if (process.platform === 'win32' || process.platform === 'darwin') return null;
666
675
  try {
@@ -687,6 +696,41 @@ function assertLinuxClipboardAvailable() {
687
696
  if (tool) return tool;
688
697
  throw new Error('Linux image paste requires xclip or wl-copy on the server. Install one and try again.');
689
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
+ }
690
734
 
691
735
  setInterval(() => {
692
736
  const now = Date.now();
@@ -1689,46 +1733,40 @@ function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', on
1689
1733
  } else if (isMac) {
1690
1734
  execSync(`osascript -e 'set the clipboard to (read POSIX file "${tmpFile}" as 芦class PNGf禄)'`, { timeout: 10000 });
1691
1735
  } else {
1692
- const type = String(mediaType || 'image/png').toLowerCase();
1693
- const tool = assertLinuxClipboardAvailable();
1694
- const quotedPath = shellQuote(tmpFile);
1695
- const quotedType = shellQuote(type);
1696
- if (tool === 'xclip') {
1697
- execSync(`xclip -selection clipboard -t ${quotedType} -i < ${quotedPath}`, {
1698
- timeout: 10000,
1699
- shell: '/bin/sh',
1700
- });
1701
- } else {
1702
- execSync(`wl-copy --type ${quotedType} < ${quotedPath}`, {
1703
- timeout: 10000,
1704
- shell: '/bin/sh',
1705
- });
1706
- }
1707
- log(`Linux clipboard set with ${tool} (${type})`);
1736
+ const tool = startLinuxClipboardImage(tmpFile, mediaType);
1737
+ log(`Linux clipboard armed with ${tool}`);
1708
1738
  }
1709
1739
  log('Clipboard set with image');
1710
1740
 
1711
- if (isWin) claudeProc.write('\x1bv');
1712
- else claudeProc.write('\x16');
1713
- log('Sent image paste keypress to PTY');
1714
-
1715
- setTimeout(() => {
1716
- if (!claudeProc) return;
1717
- const trimmedText = (text || '').trim();
1718
- if (trimmedText) claudeProc.write(trimmedText);
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);
1719
1753
 
1720
1754
  setTimeout(() => {
1721
- if (claudeProc) claudeProc.write('\r');
1722
- log('Sent Enter after image paste' + (trimmedText ? ` + text: "${trimmedText.substring(0, 60)}"` : ''));
1723
-
1724
- setTimeout(() => {
1725
- if (onCleanup) onCleanup();
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();
1726
1764
  else {
1727
1765
  try { fs.unlinkSync(tmpFile); } catch {}
1728
1766
  }
1729
1767
  }, 5000);
1730
- }, 150);
1731
- }, 1000);
1768
+ }, 150);
1769
+ }, 1000 + pasteDelayMs);
1732
1770
  } catch (err) {
1733
1771
  log(`Image upload error: ${err.message}`);
1734
1772
  if (onCleanup) onCleanup();