claude-remote-cli 1.6.0 → 1.7.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/dist/server/config.js +29 -0
- package/dist/server/index.js +14 -7
- package/dist/server/sessions.js +22 -1
- package/package.json +1 -1
- package/public/app.js +85 -2
- package/public/style.css +13 -0
package/dist/server/config.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
2
4
|
export const DEFAULTS = {
|
|
3
5
|
host: '0.0.0.0',
|
|
4
6
|
port: 3456,
|
|
@@ -18,3 +20,30 @@ export function loadConfig(configPath) {
|
|
|
18
20
|
export function saveConfig(configPath, config) {
|
|
19
21
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
20
22
|
}
|
|
23
|
+
function metaDir(configPath) {
|
|
24
|
+
return path.join(path.dirname(configPath), 'worktree-meta');
|
|
25
|
+
}
|
|
26
|
+
function metaFilePath(configPath, worktreePath) {
|
|
27
|
+
const hash = crypto.createHash('sha256').update(worktreePath).digest('hex').slice(0, 16);
|
|
28
|
+
return path.join(metaDir(configPath), hash + '.json');
|
|
29
|
+
}
|
|
30
|
+
export function ensureMetaDir(configPath) {
|
|
31
|
+
const dir = metaDir(configPath);
|
|
32
|
+
if (!fs.existsSync(dir)) {
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function readMeta(configPath, worktreePath) {
|
|
37
|
+
const fp = metaFilePath(configPath, worktreePath);
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
40
|
+
}
|
|
41
|
+
catch (_) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function writeMeta(configPath, meta) {
|
|
46
|
+
const fp = metaFilePath(configPath, meta.worktreePath);
|
|
47
|
+
ensureMetaDir(configPath);
|
|
48
|
+
fs.writeFileSync(fp, JSON.stringify(meta, null, 2), 'utf8');
|
|
49
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { execFile } from 'node:child_process';
|
|
|
8
8
|
import { promisify } from 'node:util';
|
|
9
9
|
import express from 'express';
|
|
10
10
|
import cookieParser from 'cookie-parser';
|
|
11
|
-
import { loadConfig, saveConfig, DEFAULTS } from './config.js';
|
|
11
|
+
import { loadConfig, saveConfig, DEFAULTS, readMeta, writeMeta, ensureMetaDir } from './config.js';
|
|
12
12
|
import * as auth from './auth.js';
|
|
13
13
|
import * as sessions from './sessions.js';
|
|
14
14
|
import { setupWebSocket } from './ws.js';
|
|
@@ -21,6 +21,8 @@ const execFileAsync = promisify(execFile);
|
|
|
21
21
|
// When run via CLI bin, config lives in ~/.config/claude-remote-cli/
|
|
22
22
|
// When run directly (development), fall back to local config.json
|
|
23
23
|
const CONFIG_PATH = process.env.CLAUDE_REMOTE_CONFIG || path.join(__dirname, '..', '..', 'config.json');
|
|
24
|
+
// Ensure worktree metadata directory exists alongside config
|
|
25
|
+
ensureMetaDir(CONFIG_PATH);
|
|
24
26
|
const VERSION_CACHE_TTL = 5 * 60 * 1000;
|
|
25
27
|
let versionCache = null;
|
|
26
28
|
function getCurrentVersion() {
|
|
@@ -216,12 +218,16 @@ async function main() {
|
|
|
216
218
|
for (const entry of entries) {
|
|
217
219
|
if (!entry.isDirectory())
|
|
218
220
|
continue;
|
|
221
|
+
const wtPath = path.join(worktreeDir, entry.name);
|
|
222
|
+
const meta = readMeta(CONFIG_PATH, wtPath);
|
|
219
223
|
worktrees.push({
|
|
220
224
|
name: entry.name,
|
|
221
|
-
path:
|
|
225
|
+
path: wtPath,
|
|
222
226
|
repoName: repo.name,
|
|
223
227
|
repoPath: repo.path,
|
|
224
228
|
root: repo.root,
|
|
229
|
+
displayName: meta ? meta.displayName : '',
|
|
230
|
+
lastActivity: meta ? meta.lastActivity : '',
|
|
225
231
|
});
|
|
226
232
|
}
|
|
227
233
|
}
|
|
@@ -327,8 +333,8 @@ async function main() {
|
|
|
327
333
|
let cwd;
|
|
328
334
|
let worktreeName;
|
|
329
335
|
if (worktreePath) {
|
|
330
|
-
// Resume existing worktree — run claude inside the worktree directory
|
|
331
|
-
args = [...baseArgs];
|
|
336
|
+
// Resume existing worktree — run claude --continue inside the worktree directory
|
|
337
|
+
args = ['--continue', ...baseArgs];
|
|
332
338
|
cwd = worktreePath;
|
|
333
339
|
worktreeName = worktreePath.split('/').pop() || '';
|
|
334
340
|
}
|
|
@@ -346,6 +352,7 @@ async function main() {
|
|
|
346
352
|
displayName: worktreeName,
|
|
347
353
|
command: config.claudeCommand,
|
|
348
354
|
args,
|
|
355
|
+
configPath: CONFIG_PATH,
|
|
349
356
|
});
|
|
350
357
|
res.status(201).json(session);
|
|
351
358
|
});
|
|
@@ -359,7 +366,7 @@ async function main() {
|
|
|
359
366
|
res.status(404).json({ error: 'Session not found' });
|
|
360
367
|
}
|
|
361
368
|
});
|
|
362
|
-
// PATCH /sessions/:id — update displayName and
|
|
369
|
+
// PATCH /sessions/:id — update displayName and persist to metadata
|
|
363
370
|
app.patch('/sessions/:id', requireAuth, (req, res) => {
|
|
364
371
|
const { displayName } = req.body;
|
|
365
372
|
if (!displayName) {
|
|
@@ -370,8 +377,8 @@ async function main() {
|
|
|
370
377
|
const id = req.params['id'];
|
|
371
378
|
const updated = sessions.updateDisplayName(id, displayName);
|
|
372
379
|
const session = sessions.get(id);
|
|
373
|
-
if (session
|
|
374
|
-
|
|
380
|
+
if (session) {
|
|
381
|
+
writeMeta(CONFIG_PATH, { worktreePath: session.repoPath, displayName, lastActivity: session.lastActivity });
|
|
375
382
|
}
|
|
376
383
|
res.json(updated);
|
|
377
384
|
}
|
package/dist/server/sessions.js
CHANGED
|
@@ -3,9 +3,10 @@ import crypto from 'node:crypto';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { readMeta, writeMeta } from './config.js';
|
|
6
7
|
// In-memory registry: id -> Session
|
|
7
8
|
const sessions = new Map();
|
|
8
|
-
function create({ repoName, repoPath, root, worktreeName, displayName, command, args = [], cols = 80, rows = 24 }) {
|
|
9
|
+
function create({ repoName, repoPath, root, worktreeName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
|
|
9
10
|
const id = crypto.randomBytes(8).toString('hex');
|
|
10
11
|
const createdAt = new Date().toISOString();
|
|
11
12
|
// Strip CLAUDECODE env var to allow spawning claude inside a claude-managed server
|
|
@@ -35,6 +36,15 @@ function create({ repoName, repoPath, root, worktreeName, displayName, command,
|
|
|
35
36
|
scrollback,
|
|
36
37
|
};
|
|
37
38
|
sessions.set(id, session);
|
|
39
|
+
// Load existing metadata to preserve a previously-set displayName
|
|
40
|
+
if (configPath && worktreeName) {
|
|
41
|
+
const existing = readMeta(configPath, repoPath);
|
|
42
|
+
if (existing && existing.displayName) {
|
|
43
|
+
session.displayName = existing.displayName;
|
|
44
|
+
}
|
|
45
|
+
writeMeta(configPath, { worktreePath: repoPath, displayName: session.displayName, lastActivity: createdAt });
|
|
46
|
+
}
|
|
47
|
+
let metaFlushTimer = null;
|
|
38
48
|
ptyProcess.onData((data) => {
|
|
39
49
|
session.lastActivity = new Date().toISOString();
|
|
40
50
|
scrollback.push(data);
|
|
@@ -43,8 +53,19 @@ function create({ repoName, repoPath, root, worktreeName, displayName, command,
|
|
|
43
53
|
while (scrollbackBytes > MAX_SCROLLBACK && scrollback.length > 1) {
|
|
44
54
|
scrollbackBytes -= scrollback.shift().length;
|
|
45
55
|
}
|
|
56
|
+
if (configPath && worktreeName && !metaFlushTimer) {
|
|
57
|
+
metaFlushTimer = setTimeout(() => {
|
|
58
|
+
metaFlushTimer = null;
|
|
59
|
+
writeMeta(configPath, { worktreePath: repoPath, displayName: session.displayName, lastActivity: session.lastActivity });
|
|
60
|
+
}, 5000);
|
|
61
|
+
}
|
|
46
62
|
});
|
|
47
63
|
ptyProcess.onExit(() => {
|
|
64
|
+
if (metaFlushTimer)
|
|
65
|
+
clearTimeout(metaFlushTimer);
|
|
66
|
+
if (configPath && worktreeName) {
|
|
67
|
+
writeMeta(configPath, { worktreePath: repoPath, displayName: session.displayName, lastActivity: session.lastActivity });
|
|
68
|
+
}
|
|
48
69
|
sessions.delete(id);
|
|
49
70
|
const tmpDir = path.join(os.tmpdir(), 'claude-remote-cli', id);
|
|
50
71
|
fs.rm(tmpDir, { recursive: true, force: true }, () => { });
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -155,6 +155,60 @@
|
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
// On Windows/Linux, Ctrl+V is the paste shortcut but xterm.js intercepts it
|
|
159
|
+
// internally without firing a native paste event, so our image paste handler
|
|
160
|
+
// on terminalContainer never runs. Intercept Ctrl+V here to check for images.
|
|
161
|
+
// On macOS, Ctrl+V sends a raw \x16 to the terminal (used by vim etc.), so
|
|
162
|
+
// we only intercept on non-Mac platforms.
|
|
163
|
+
var isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform || '');
|
|
164
|
+
if (!isMac) {
|
|
165
|
+
term.attachCustomKeyEventHandler(function (e) {
|
|
166
|
+
if (e.type === 'keydown' && e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey &&
|
|
167
|
+
(e.key === 'v' || e.key === 'V')) {
|
|
168
|
+
if (navigator.clipboard && navigator.clipboard.read) {
|
|
169
|
+
navigator.clipboard.read().then(function (clipboardItems) {
|
|
170
|
+
var imageBlob = null;
|
|
171
|
+
var imageType = null;
|
|
172
|
+
|
|
173
|
+
for (var i = 0; i < clipboardItems.length; i++) {
|
|
174
|
+
var types = clipboardItems[i].types;
|
|
175
|
+
for (var j = 0; j < types.length; j++) {
|
|
176
|
+
if (types[j].indexOf('image/') === 0) {
|
|
177
|
+
imageType = types[j];
|
|
178
|
+
imageBlob = clipboardItems[i];
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (imageBlob) break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (imageBlob) {
|
|
186
|
+
imageBlob.getType(imageType).then(function (blob) {
|
|
187
|
+
uploadImage(blob, imageType);
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
navigator.clipboard.readText().then(function (text) {
|
|
191
|
+
if (text) term.paste(text);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}).catch(function () {
|
|
195
|
+
// Clipboard read failed (permission denied, etc.) — fall back to text.
|
|
196
|
+
// If readText also fails, paste is lost for this keypress; this only
|
|
197
|
+
// happens when clipboard permission is fully denied, which is rare
|
|
198
|
+
// for user-gesture-triggered reads on HTTPS origins.
|
|
199
|
+
if (navigator.clipboard.readText) {
|
|
200
|
+
navigator.clipboard.readText().then(function (text) {
|
|
201
|
+
if (text) term.paste(text);
|
|
202
|
+
}).catch(function () {});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return false; // Prevent xterm from handling Ctrl+V
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true; // Let xterm handle all other keys
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
158
212
|
var resizeObserver = new ResizeObserver(function () {
|
|
159
213
|
fitAddon.fit();
|
|
160
214
|
sendResize();
|
|
@@ -320,6 +374,24 @@
|
|
|
320
374
|
return path.split('/').filter(Boolean).pop() || path;
|
|
321
375
|
}
|
|
322
376
|
|
|
377
|
+
function formatRelativeTime(isoString) {
|
|
378
|
+
if (!isoString) return '';
|
|
379
|
+
var now = Date.now();
|
|
380
|
+
var then = new Date(isoString).getTime();
|
|
381
|
+
var diffSec = Math.floor((now - then) / 1000);
|
|
382
|
+
if (diffSec < 60) return 'just now';
|
|
383
|
+
var diffMin = Math.floor(diffSec / 60);
|
|
384
|
+
if (diffMin < 60) return diffMin + ' min ago';
|
|
385
|
+
var diffHr = Math.floor(diffMin / 60);
|
|
386
|
+
if (diffHr < 24) return diffHr + (diffHr === 1 ? ' hour ago' : ' hours ago');
|
|
387
|
+
var diffDay = Math.floor(diffHr / 24);
|
|
388
|
+
if (diffDay === 1) return 'yesterday';
|
|
389
|
+
if (diffDay < 7) return diffDay + ' days ago';
|
|
390
|
+
var d = new Date(isoString);
|
|
391
|
+
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
392
|
+
return months[d.getMonth()] + ' ' + d.getDate();
|
|
393
|
+
}
|
|
394
|
+
|
|
323
395
|
function renderUnifiedList() {
|
|
324
396
|
var rootFilter = sidebarRootFilter.value;
|
|
325
397
|
var repoFilter = sidebarRepoFilter.value;
|
|
@@ -396,6 +468,11 @@
|
|
|
396
468
|
infoDiv.appendChild(nameSpan);
|
|
397
469
|
infoDiv.appendChild(subSpan);
|
|
398
470
|
|
|
471
|
+
var timeSpan = document.createElement('span');
|
|
472
|
+
timeSpan.className = 'session-time';
|
|
473
|
+
timeSpan.textContent = formatRelativeTime(session.lastActivity);
|
|
474
|
+
infoDiv.appendChild(timeSpan);
|
|
475
|
+
|
|
399
476
|
var actionsDiv = document.createElement('div');
|
|
400
477
|
actionsDiv.className = 'session-actions';
|
|
401
478
|
|
|
@@ -439,8 +516,9 @@
|
|
|
439
516
|
|
|
440
517
|
var nameSpan = document.createElement('span');
|
|
441
518
|
nameSpan.className = 'session-name';
|
|
442
|
-
|
|
443
|
-
nameSpan.
|
|
519
|
+
var wtDisplayName = wt.displayName || wt.name;
|
|
520
|
+
nameSpan.textContent = wtDisplayName;
|
|
521
|
+
nameSpan.title = wtDisplayName;
|
|
444
522
|
|
|
445
523
|
var subSpan = document.createElement('span');
|
|
446
524
|
subSpan.className = 'session-sub';
|
|
@@ -449,6 +527,11 @@
|
|
|
449
527
|
infoDiv.appendChild(nameSpan);
|
|
450
528
|
infoDiv.appendChild(subSpan);
|
|
451
529
|
|
|
530
|
+
var timeSpan = document.createElement('span');
|
|
531
|
+
timeSpan.className = 'session-time';
|
|
532
|
+
timeSpan.textContent = formatRelativeTime(wt.lastActivity);
|
|
533
|
+
infoDiv.appendChild(timeSpan);
|
|
534
|
+
|
|
452
535
|
li.appendChild(infoDiv);
|
|
453
536
|
|
|
454
537
|
// Click to resume (but not if context menu just opened or long-press fired)
|
package/public/style.css
CHANGED
|
@@ -242,6 +242,10 @@ html, body {
|
|
|
242
242
|
color: rgba(255, 255, 255, 0.7);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
#session-list li.active-session.active .session-time {
|
|
246
|
+
color: rgba(255, 255, 255, 0.5);
|
|
247
|
+
}
|
|
248
|
+
|
|
245
249
|
/* Inactive worktree (outline) */
|
|
246
250
|
#session-list li.inactive-worktree {
|
|
247
251
|
background: transparent;
|
|
@@ -280,6 +284,15 @@ html, body {
|
|
|
280
284
|
white-space: nowrap;
|
|
281
285
|
}
|
|
282
286
|
|
|
287
|
+
.session-time {
|
|
288
|
+
font-size: 0.65rem;
|
|
289
|
+
color: var(--text-muted);
|
|
290
|
+
opacity: 0.6;
|
|
291
|
+
overflow: hidden;
|
|
292
|
+
text-overflow: ellipsis;
|
|
293
|
+
white-space: nowrap;
|
|
294
|
+
}
|
|
295
|
+
|
|
283
296
|
.session-actions {
|
|
284
297
|
display: flex;
|
|
285
298
|
align-items: center;
|