claude-remote-cli 1.6.0 → 1.7.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.
@@ -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
+ }
@@ -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: path.join(worktreeDir, entry.name),
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 send /rename through PTY
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 && session.pty) {
374
- session.pty.write('/rename "' + displayName.replace(/"/g, '\\"') + '"\r');
380
+ if (session) {
381
+ writeMeta(CONFIG_PATH, { worktreePath: session.repoPath, displayName, lastActivity: session.lastActivity });
375
382
  }
376
383
  res.json(updated);
377
384
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Remote web interface for Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",
package/public/app.js CHANGED
@@ -320,6 +320,24 @@
320
320
  return path.split('/').filter(Boolean).pop() || path;
321
321
  }
322
322
 
323
+ function formatRelativeTime(isoString) {
324
+ if (!isoString) return '';
325
+ var now = Date.now();
326
+ var then = new Date(isoString).getTime();
327
+ var diffSec = Math.floor((now - then) / 1000);
328
+ if (diffSec < 60) return 'just now';
329
+ var diffMin = Math.floor(diffSec / 60);
330
+ if (diffMin < 60) return diffMin + ' min ago';
331
+ var diffHr = Math.floor(diffMin / 60);
332
+ if (diffHr < 24) return diffHr + (diffHr === 1 ? ' hour ago' : ' hours ago');
333
+ var diffDay = Math.floor(diffHr / 24);
334
+ if (diffDay === 1) return 'yesterday';
335
+ if (diffDay < 7) return diffDay + ' days ago';
336
+ var d = new Date(isoString);
337
+ var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
338
+ return months[d.getMonth()] + ' ' + d.getDate();
339
+ }
340
+
323
341
  function renderUnifiedList() {
324
342
  var rootFilter = sidebarRootFilter.value;
325
343
  var repoFilter = sidebarRepoFilter.value;
@@ -396,6 +414,11 @@
396
414
  infoDiv.appendChild(nameSpan);
397
415
  infoDiv.appendChild(subSpan);
398
416
 
417
+ var timeSpan = document.createElement('span');
418
+ timeSpan.className = 'session-time';
419
+ timeSpan.textContent = formatRelativeTime(session.lastActivity);
420
+ infoDiv.appendChild(timeSpan);
421
+
399
422
  var actionsDiv = document.createElement('div');
400
423
  actionsDiv.className = 'session-actions';
401
424
 
@@ -439,8 +462,9 @@
439
462
 
440
463
  var nameSpan = document.createElement('span');
441
464
  nameSpan.className = 'session-name';
442
- nameSpan.textContent = wt.name;
443
- nameSpan.title = wt.name;
465
+ var wtDisplayName = wt.displayName || wt.name;
466
+ nameSpan.textContent = wtDisplayName;
467
+ nameSpan.title = wtDisplayName;
444
468
 
445
469
  var subSpan = document.createElement('span');
446
470
  subSpan.className = 'session-sub';
@@ -449,6 +473,11 @@
449
473
  infoDiv.appendChild(nameSpan);
450
474
  infoDiv.appendChild(subSpan);
451
475
 
476
+ var timeSpan = document.createElement('span');
477
+ timeSpan.className = 'session-time';
478
+ timeSpan.textContent = formatRelativeTime(wt.lastActivity);
479
+ infoDiv.appendChild(timeSpan);
480
+
452
481
  li.appendChild(infoDiv);
453
482
 
454
483
  // 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;