claude-remote-cli 1.4.3 → 1.6.0
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/dist/server/clipboard.js +56 -0
- package/dist/server/index.js +48 -1
- package/dist/server/sessions.js +13 -1
- package/package.json +1 -1
- package/public/app.js +128 -0
- package/public/index.html +11 -0
- package/public/style.css +48 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execFile, execFileSync } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const SUPPORTED_MIME = {
|
|
5
|
+
'image/png': { ext: '.png', osascriptClass: '«class PNGf»' },
|
|
6
|
+
'image/jpeg': { ext: '.jpg', osascriptClass: '«class JPEG»' },
|
|
7
|
+
'image/gif': { ext: '.gif', osascriptClass: '«class GIFf»' },
|
|
8
|
+
'image/webp': { ext: '.webp', osascriptClass: '«class PNGf»' },
|
|
9
|
+
};
|
|
10
|
+
let cachedTool;
|
|
11
|
+
export function detectClipboardTool() {
|
|
12
|
+
if (cachedTool !== undefined)
|
|
13
|
+
return cachedTool;
|
|
14
|
+
if (process.platform === 'darwin') {
|
|
15
|
+
cachedTool = 'osascript';
|
|
16
|
+
return cachedTool;
|
|
17
|
+
}
|
|
18
|
+
if (process.env['DISPLAY'] || process.env['WAYLAND_DISPLAY']) {
|
|
19
|
+
try {
|
|
20
|
+
execFileSync('which', ['xclip'], { stdio: 'ignore' });
|
|
21
|
+
cachedTool = 'xclip';
|
|
22
|
+
return cachedTool;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// xclip not found
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
cachedTool = null;
|
|
29
|
+
return cachedTool;
|
|
30
|
+
}
|
|
31
|
+
function mimeInfo(mimeType) {
|
|
32
|
+
const info = SUPPORTED_MIME[mimeType];
|
|
33
|
+
if (!info)
|
|
34
|
+
throw new Error(`Unsupported MIME type: ${mimeType}`);
|
|
35
|
+
return info;
|
|
36
|
+
}
|
|
37
|
+
export function extensionForMime(mimeType) {
|
|
38
|
+
return mimeInfo(mimeType).ext;
|
|
39
|
+
}
|
|
40
|
+
export async function setClipboardImage(filePath, mimeType) {
|
|
41
|
+
const tool = detectClipboardTool();
|
|
42
|
+
const info = mimeInfo(mimeType); // throws if unsupported
|
|
43
|
+
if (tool === 'osascript') {
|
|
44
|
+
const script = `set the clipboard to (read (POSIX file "${filePath}") as ${info.osascriptClass})`;
|
|
45
|
+
await execFileAsync('osascript', ['-e', script]);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (tool === 'xclip') {
|
|
49
|
+
await execFileAsync('xclip', ['-selection', 'clipboard', '-t', mimeType, '-i', filePath]);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
export function _resetForTesting() {
|
|
55
|
+
cachedTool = undefined;
|
|
56
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import http from 'node:http';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import readline from 'node:readline';
|
|
@@ -13,6 +14,7 @@ import * as sessions from './sessions.js';
|
|
|
13
14
|
import { setupWebSocket } from './ws.js';
|
|
14
15
|
import { WorktreeWatcher } from './watcher.js';
|
|
15
16
|
import { isInstalled as serviceIsInstalled } from './service.js';
|
|
17
|
+
import { extensionForMime, setClipboardImage } from './clipboard.js';
|
|
16
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
19
|
const __dirname = path.dirname(__filename);
|
|
18
20
|
const execFileAsync = promisify(execFile);
|
|
@@ -127,7 +129,7 @@ async function main() {
|
|
|
127
129
|
}
|
|
128
130
|
const authenticatedTokens = new Set();
|
|
129
131
|
const app = express();
|
|
130
|
-
app.use(express.json());
|
|
132
|
+
app.use(express.json({ limit: '15mb' }));
|
|
131
133
|
app.use(cookieParser());
|
|
132
134
|
app.use(express.static(path.join(__dirname, '..', '..', 'public')));
|
|
133
135
|
const requireAuth = (req, res, next) => {
|
|
@@ -377,6 +379,51 @@ async function main() {
|
|
|
377
379
|
res.status(404).json({ error: 'Session not found' });
|
|
378
380
|
}
|
|
379
381
|
});
|
|
382
|
+
// POST /sessions/:id/image — upload clipboard image, proxy to system clipboard
|
|
383
|
+
const ALLOWED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
384
|
+
app.post('/sessions/:id/image', requireAuth, async (req, res) => {
|
|
385
|
+
const { data, mimeType } = req.body;
|
|
386
|
+
if (!data || !mimeType) {
|
|
387
|
+
res.status(400).json({ error: 'data and mimeType are required' });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (!ALLOWED_IMAGE_TYPES.includes(mimeType)) {
|
|
391
|
+
res.status(400).json({ error: 'Unsupported image type: ' + mimeType });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// base64 is ~33% larger than binary; 10MB binary ≈ 13.3MB base64
|
|
395
|
+
if (data.length > 14 * 1024 * 1024) {
|
|
396
|
+
res.status(413).json({ error: 'Image too large (max 10MB)' });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const sessionId = req.params['id'];
|
|
400
|
+
if (!sessions.get(sessionId)) {
|
|
401
|
+
res.status(404).json({ error: 'Session not found' });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const ext = extensionForMime(mimeType);
|
|
406
|
+
const dir = path.join(os.tmpdir(), 'claude-remote-cli', sessionId);
|
|
407
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
408
|
+
const filePath = path.join(dir, 'paste-' + Date.now() + ext);
|
|
409
|
+
fs.writeFileSync(filePath, Buffer.from(data, 'base64'));
|
|
410
|
+
let clipboardSet = false;
|
|
411
|
+
try {
|
|
412
|
+
clipboardSet = await setClipboardImage(filePath, mimeType);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
// Clipboard tools failed — fall back to path
|
|
416
|
+
}
|
|
417
|
+
if (clipboardSet) {
|
|
418
|
+
sessions.write(sessionId, '\x16');
|
|
419
|
+
}
|
|
420
|
+
res.json({ path: filePath, clipboardSet });
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
const message = err instanceof Error ? err.message : 'Image upload failed';
|
|
424
|
+
res.status(500).json({ error: message });
|
|
425
|
+
}
|
|
426
|
+
});
|
|
380
427
|
// GET /version — check current vs latest
|
|
381
428
|
app.get('/version', requireAuth, async (_req, res) => {
|
|
382
429
|
const current = getCurrentVersion();
|
package/dist/server/sessions.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import pty from 'node-pty';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
3
6
|
// In-memory registry: id -> Session
|
|
4
7
|
const sessions = new Map();
|
|
5
8
|
function create({ repoName, repoPath, root, worktreeName, displayName, command, args = [], cols = 80, rows = 24 }) {
|
|
@@ -43,6 +46,8 @@ function create({ repoName, repoPath, root, worktreeName, displayName, command,
|
|
|
43
46
|
});
|
|
44
47
|
ptyProcess.onExit(() => {
|
|
45
48
|
sessions.delete(id);
|
|
49
|
+
const tmpDir = path.join(os.tmpdir(), 'claude-remote-cli', id);
|
|
50
|
+
fs.rm(tmpDir, { recursive: true, force: true }, () => { });
|
|
46
51
|
});
|
|
47
52
|
return { id, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt };
|
|
48
53
|
}
|
|
@@ -85,4 +90,11 @@ function resize(id, cols, rows) {
|
|
|
85
90
|
}
|
|
86
91
|
session.pty.resize(cols, rows);
|
|
87
92
|
}
|
|
88
|
-
|
|
93
|
+
function write(id, data) {
|
|
94
|
+
const session = sessions.get(id);
|
|
95
|
+
if (!session) {
|
|
96
|
+
throw new Error(`Session not found: ${id}`);
|
|
97
|
+
}
|
|
98
|
+
session.pty.write(data);
|
|
99
|
+
}
|
|
100
|
+
export { create, get, list, kill, resize, updateDisplayName, write };
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
var updateToastText = document.getElementById('update-toast-text');
|
|
47
47
|
var updateToastBtn = document.getElementById('update-toast-btn');
|
|
48
48
|
var updateToastDismiss = document.getElementById('update-toast-dismiss');
|
|
49
|
+
var imageToast = document.getElementById('image-toast');
|
|
50
|
+
var imageToastText = document.getElementById('image-toast-text');
|
|
51
|
+
var imageToastInsert = document.getElementById('image-toast-insert');
|
|
52
|
+
var imageToastDismiss = document.getElementById('image-toast-dismiss');
|
|
49
53
|
|
|
50
54
|
// Context menu state
|
|
51
55
|
var contextMenuTarget = null; // stores { worktreePath, repoPath, name }
|
|
@@ -851,6 +855,130 @@
|
|
|
851
855
|
}
|
|
852
856
|
});
|
|
853
857
|
|
|
858
|
+
// ── Image Paste Handling ─────────────────────────────────────────────────────
|
|
859
|
+
|
|
860
|
+
var imageUploadInProgress = false;
|
|
861
|
+
var pendingImagePath = null;
|
|
862
|
+
|
|
863
|
+
function showImageToast(text, showInsert) {
|
|
864
|
+
imageToastText.textContent = text;
|
|
865
|
+
imageToastInsert.hidden = !showInsert;
|
|
866
|
+
imageToast.hidden = false;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function hideImageToast() {
|
|
870
|
+
imageToast.hidden = true;
|
|
871
|
+
pendingImagePath = null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function autoDismissImageToast(ms) {
|
|
875
|
+
setTimeout(function () {
|
|
876
|
+
if (!pendingImagePath) {
|
|
877
|
+
hideImageToast();
|
|
878
|
+
}
|
|
879
|
+
}, ms);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function uploadImage(blob, mimeType) {
|
|
883
|
+
if (imageUploadInProgress) return;
|
|
884
|
+
if (!activeSessionId) return;
|
|
885
|
+
|
|
886
|
+
imageUploadInProgress = true;
|
|
887
|
+
showImageToast('Pasting image\u2026', false);
|
|
888
|
+
|
|
889
|
+
var reader = new FileReader();
|
|
890
|
+
reader.onload = function () {
|
|
891
|
+
var base64 = reader.result.split(',')[1];
|
|
892
|
+
|
|
893
|
+
fetch('/sessions/' + activeSessionId + '/image', {
|
|
894
|
+
method: 'POST',
|
|
895
|
+
headers: { 'Content-Type': 'application/json' },
|
|
896
|
+
body: JSON.stringify({ data: base64, mimeType: mimeType }),
|
|
897
|
+
})
|
|
898
|
+
.then(function (res) {
|
|
899
|
+
if (res.status === 413) {
|
|
900
|
+
showImageToast('Image too large (max 10MB)', false);
|
|
901
|
+
autoDismissImageToast(4000);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (!res.ok) {
|
|
905
|
+
return res.json().then(function (data) {
|
|
906
|
+
showImageToast(data.error || 'Image upload failed', false);
|
|
907
|
+
autoDismissImageToast(4000);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return res.json().then(function (data) {
|
|
911
|
+
if (data.clipboardSet) {
|
|
912
|
+
showImageToast('Image pasted', false);
|
|
913
|
+
autoDismissImageToast(2000);
|
|
914
|
+
} else {
|
|
915
|
+
pendingImagePath = data.path;
|
|
916
|
+
showImageToast(data.path, true);
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
})
|
|
920
|
+
.catch(function () {
|
|
921
|
+
showImageToast('Image upload failed', false);
|
|
922
|
+
autoDismissImageToast(4000);
|
|
923
|
+
})
|
|
924
|
+
.then(function () {
|
|
925
|
+
imageUploadInProgress = false;
|
|
926
|
+
});
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
reader.readAsDataURL(blob);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
terminalContainer.addEventListener('paste', function (e) {
|
|
933
|
+
if (!e.clipboardData || !e.clipboardData.items) return;
|
|
934
|
+
|
|
935
|
+
var items = e.clipboardData.items;
|
|
936
|
+
for (var i = 0; i < items.length; i++) {
|
|
937
|
+
if (items[i].type.indexOf('image/') === 0) {
|
|
938
|
+
e.preventDefault();
|
|
939
|
+
e.stopPropagation();
|
|
940
|
+
var blob = items[i].getAsFile();
|
|
941
|
+
if (blob) {
|
|
942
|
+
uploadImage(blob, items[i].type);
|
|
943
|
+
}
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
terminalContainer.addEventListener('dragover', function (e) {
|
|
950
|
+
if (e.dataTransfer && e.dataTransfer.types.indexOf('Files') !== -1) {
|
|
951
|
+
e.preventDefault();
|
|
952
|
+
terminalContainer.classList.add('drag-over');
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
terminalContainer.addEventListener('dragleave', function () {
|
|
957
|
+
terminalContainer.classList.remove('drag-over');
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
terminalContainer.addEventListener('drop', function (e) {
|
|
961
|
+
e.preventDefault();
|
|
962
|
+
terminalContainer.classList.remove('drag-over');
|
|
963
|
+
if (!e.dataTransfer || !e.dataTransfer.files.length) return;
|
|
964
|
+
|
|
965
|
+
var file = e.dataTransfer.files[0];
|
|
966
|
+
if (file.type.indexOf('image/') === 0) {
|
|
967
|
+
uploadImage(file, file.type);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
imageToastInsert.addEventListener('click', function () {
|
|
972
|
+
if (pendingImagePath && ws && ws.readyState === WebSocket.OPEN) {
|
|
973
|
+
ws.send(pendingImagePath);
|
|
974
|
+
}
|
|
975
|
+
hideImageToast();
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
imageToastDismiss.addEventListener('click', function () {
|
|
979
|
+
hideImageToast();
|
|
980
|
+
});
|
|
981
|
+
|
|
854
982
|
// ── Keyboard-Aware Viewport ─────────────────────────────────────────────────
|
|
855
983
|
|
|
856
984
|
(function () {
|
package/public/index.html
CHANGED
|
@@ -89,6 +89,17 @@
|
|
|
89
89
|
</div>
|
|
90
90
|
</div>
|
|
91
91
|
|
|
92
|
+
<!-- Image Paste Toast -->
|
|
93
|
+
<div id="image-toast" hidden>
|
|
94
|
+
<div id="image-toast-content">
|
|
95
|
+
<span id="image-toast-text"></span>
|
|
96
|
+
<div id="image-toast-actions">
|
|
97
|
+
<button id="image-toast-insert" class="btn-accent" hidden>Insert Path</button>
|
|
98
|
+
<button id="image-toast-dismiss" aria-label="Dismiss">×</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
92
103
|
</div>
|
|
93
104
|
|
|
94
105
|
<!-- New Session Dialog -->
|
package/public/style.css
CHANGED
|
@@ -417,6 +417,7 @@ html, body {
|
|
|
417
417
|
|
|
418
418
|
/* ===== Touch Toolbar ===== */
|
|
419
419
|
#toolbar {
|
|
420
|
+
display: none;
|
|
420
421
|
flex-shrink: 0;
|
|
421
422
|
background: var(--surface);
|
|
422
423
|
border-top: 1px solid var(--border);
|
|
@@ -993,6 +994,10 @@ dialog#delete-worktree-dialog h2 {
|
|
|
993
994
|
.tb-btn.tb-arrow {
|
|
994
995
|
font-size: 1.2rem;
|
|
995
996
|
}
|
|
997
|
+
|
|
998
|
+
#toolbar {
|
|
999
|
+
display: block;
|
|
1000
|
+
}
|
|
996
1001
|
}
|
|
997
1002
|
|
|
998
1003
|
/* ===== Scrollbar ===== */
|
|
@@ -1008,3 +1013,46 @@ dialog#delete-worktree-dialog h2 {
|
|
|
1008
1013
|
background: var(--border);
|
|
1009
1014
|
border-radius: 4px;
|
|
1010
1015
|
}
|
|
1016
|
+
|
|
1017
|
+
/* ===== Image Paste Toast ===== */
|
|
1018
|
+
#image-toast {
|
|
1019
|
+
position: fixed;
|
|
1020
|
+
bottom: 60px;
|
|
1021
|
+
left: 50%;
|
|
1022
|
+
transform: translateX(-50%);
|
|
1023
|
+
z-index: 1000;
|
|
1024
|
+
background: #2d2d2d;
|
|
1025
|
+
border: 1px solid #555;
|
|
1026
|
+
border-radius: 8px;
|
|
1027
|
+
padding: 8px 14px;
|
|
1028
|
+
color: #d4d4d4;
|
|
1029
|
+
font-size: 13px;
|
|
1030
|
+
max-width: 90vw;
|
|
1031
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
#image-toast-content {
|
|
1035
|
+
display: flex;
|
|
1036
|
+
align-items: center;
|
|
1037
|
+
gap: 10px;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
#image-toast-actions {
|
|
1041
|
+
display: flex;
|
|
1042
|
+
gap: 6px;
|
|
1043
|
+
align-items: center;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
#image-toast-dismiss {
|
|
1047
|
+
background: none;
|
|
1048
|
+
border: none;
|
|
1049
|
+
color: #999;
|
|
1050
|
+
cursor: pointer;
|
|
1051
|
+
font-size: 16px;
|
|
1052
|
+
padding: 2px 6px;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
#terminal-container.drag-over {
|
|
1056
|
+
outline: 2px dashed #007acc;
|
|
1057
|
+
outline-offset: -2px;
|
|
1058
|
+
}
|