claude-remote-cli 1.2.0 → 1.4.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.
@@ -12,12 +12,49 @@ import * as auth from './auth.js';
12
12
  import * as sessions from './sessions.js';
13
13
  import { setupWebSocket } from './ws.js';
14
14
  import { WorktreeWatcher } from './watcher.js';
15
+ import { isInstalled as serviceIsInstalled } from './service.js';
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
17
18
  const execFileAsync = promisify(execFile);
18
19
  // When run via CLI bin, config lives in ~/.config/claude-remote-cli/
19
20
  // When run directly (development), fall back to local config.json
20
21
  const CONFIG_PATH = process.env.CLAUDE_REMOTE_CONFIG || path.join(__dirname, '..', '..', 'config.json');
22
+ const VERSION_CACHE_TTL = 5 * 60 * 1000;
23
+ let versionCache = null;
24
+ function getCurrentVersion() {
25
+ const pkgPath = path.join(__dirname, '..', '..', 'package.json');
26
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
27
+ return pkg.version;
28
+ }
29
+ function semverLessThan(a, b) {
30
+ const parse = (v) => v.split('.').map(Number);
31
+ const [aMaj = 0, aMin = 0, aPat = 0] = parse(a);
32
+ const [bMaj = 0, bMin = 0, bPat = 0] = parse(b);
33
+ if (aMaj !== bMaj)
34
+ return aMaj < bMaj;
35
+ if (aMin !== bMin)
36
+ return aMin < bMin;
37
+ return aPat < bPat;
38
+ }
39
+ async function getLatestVersion() {
40
+ const now = Date.now();
41
+ if (versionCache && now - versionCache.fetchedAt < VERSION_CACHE_TTL) {
42
+ return versionCache.latest;
43
+ }
44
+ try {
45
+ const res = await fetch('https://registry.npmjs.org/claude-remote-cli/latest');
46
+ if (!res.ok)
47
+ return null;
48
+ const data = await res.json();
49
+ if (!data.version)
50
+ return null;
51
+ versionCache = { latest: data.version, fetchedAt: now };
52
+ return data.version;
53
+ }
54
+ catch (_) {
55
+ return null;
56
+ }
57
+ }
21
58
  function parseTTL(ttl) {
22
59
  if (typeof ttl !== 'string')
23
60
  return 24 * 60 * 60 * 1000;
@@ -280,7 +317,7 @@ async function main() {
280
317
  return;
281
318
  }
282
319
  const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
283
- const baseArgs = claudeArgs || config.claudeArgs || [];
320
+ const baseArgs = [...(config.claudeArgs || []), ...(claudeArgs || [])];
284
321
  // Compute root by matching repoPath against configured rootDirs
285
322
  const roots = config.rootDirs || [];
286
323
  const root = roots.find(function (r) { return repoPath.startsWith(r); }) || '';
@@ -340,6 +377,28 @@ async function main() {
340
377
  res.status(404).json({ error: 'Session not found' });
341
378
  }
342
379
  });
380
+ // GET /version — check current vs latest
381
+ app.get('/version', requireAuth, async (_req, res) => {
382
+ const current = getCurrentVersion();
383
+ const latest = await getLatestVersion();
384
+ const updateAvailable = latest !== null && semverLessThan(current, latest);
385
+ res.json({ current, latest, updateAvailable });
386
+ });
387
+ // POST /update — install latest version from npm
388
+ app.post('/update', requireAuth, async (_req, res) => {
389
+ try {
390
+ await execFileAsync('npm', ['install', '-g', 'claude-remote-cli@latest']);
391
+ const restarting = serviceIsInstalled();
392
+ res.json({ ok: true, restarting });
393
+ if (restarting) {
394
+ setTimeout(() => process.exit(0), 1000);
395
+ }
396
+ }
397
+ catch (err) {
398
+ const message = err instanceof Error ? err.message : 'Update failed';
399
+ res.status(500).json({ ok: false, error: message });
400
+ }
401
+ });
343
402
  server.listen(config.port, config.host, () => {
344
403
  console.log(`claude-remote-cli listening on ${config.host}:${config.port}`);
345
404
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.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
@@ -34,12 +34,18 @@
34
34
  var sidebarRepoFilter = document.getElementById('sidebar-repo-filter');
35
35
  var dialogRootSelect = document.getElementById('dialog-root-select');
36
36
  var dialogRepoSelect = document.getElementById('dialog-repo-select');
37
+ var dialogYolo = document.getElementById('dialog-yolo');
37
38
  var contextMenu = document.getElementById('context-menu');
39
+ var ctxResumeYolo = document.getElementById('ctx-resume-yolo');
38
40
  var ctxDeleteWorktree = document.getElementById('ctx-delete-worktree');
39
41
  var deleteWtDialog = document.getElementById('delete-worktree-dialog');
40
42
  var deleteWtName = document.getElementById('delete-wt-name');
41
43
  var deleteWtCancel = document.getElementById('delete-wt-cancel');
42
44
  var deleteWtConfirm = document.getElementById('delete-wt-confirm');
45
+ var updateToast = document.getElementById('update-toast');
46
+ var updateToastText = document.getElementById('update-toast-text');
47
+ var updateToastBtn = document.getElementById('update-toast-btn');
48
+ var updateToastDismiss = document.getElementById('update-toast-dismiss');
43
49
 
44
50
  // Context menu state
45
51
  var contextMenuTarget = null; // stores { worktreePath, repoPath, name }
@@ -117,6 +123,7 @@
117
123
  loadRepos();
118
124
  refreshAll();
119
125
  connectEventSocket();
126
+ checkForUpdates();
120
127
  }
121
128
 
122
129
  // ── Terminal ────────────────────────────────────────────────────────────────
@@ -545,7 +552,18 @@
545
552
  .catch(function () {});
546
553
  }
547
554
 
548
- // ── Delete Worktree ────────────────────────────────────────────────────────
555
+ // ── Context Menu Actions ──────────────────────────────────────────────────
556
+
557
+ ctxResumeYolo.addEventListener('click', function (e) {
558
+ e.stopPropagation();
559
+ hideContextMenu();
560
+ if (!contextMenuTarget) return;
561
+ startSession(
562
+ contextMenuTarget.repoPath,
563
+ contextMenuTarget.worktreePath,
564
+ ['--dangerously-skip-permissions']
565
+ );
566
+ });
549
567
 
550
568
  ctxDeleteWorktree.addEventListener('click', function (e) {
551
569
  e.stopPropagation();
@@ -653,12 +671,13 @@
653
671
  dialogRepoSelect.disabled = false;
654
672
  });
655
673
 
656
- function startSession(repoPath, worktreePath) {
674
+ function startSession(repoPath, worktreePath, claudeArgs) {
657
675
  var body = {
658
676
  repoPath: repoPath,
659
677
  repoName: repoPath.split('/').filter(Boolean).pop(),
660
678
  };
661
679
  if (worktreePath) body.worktreePath = worktreePath;
680
+ if (claudeArgs) body.claudeArgs = claudeArgs;
662
681
 
663
682
  fetch('/sessions', {
664
683
  method: 'POST',
@@ -678,6 +697,7 @@
678
697
 
679
698
  newSessionBtn.addEventListener('click', function () {
680
699
  customPath.value = '';
700
+ dialogYolo.checked = false;
681
701
  populateDialogRootSelect();
682
702
 
683
703
  var sidebarRoot = sidebarRootFilter.value;
@@ -704,7 +724,8 @@
704
724
  dialogStart.addEventListener('click', function () {
705
725
  var path = customPath.value.trim() || dialogRepoSelect.value;
706
726
  if (!path) return;
707
- startSession(path);
727
+ var args = dialogYolo.checked ? ['--dangerously-skip-permissions'] : undefined;
728
+ startSession(path, undefined, args);
708
729
  });
709
730
 
710
731
  dialogCancel.addEventListener('click', function () {
@@ -851,6 +872,73 @@
851
872
  vv.addEventListener('scroll', onViewportResize);
852
873
  })();
853
874
 
875
+ // ── Update Toast ─────────────────────────────────────────────────────────────
876
+
877
+ function checkForUpdates() {
878
+ fetch('/version')
879
+ .then(function (res) {
880
+ if (!res.ok) return;
881
+ return res.json();
882
+ })
883
+ .then(function (data) {
884
+ if (data && data.updateAvailable) {
885
+ showUpdateToast(data.current, data.latest);
886
+ }
887
+ })
888
+ .catch(function () {
889
+ // Silently ignore version check errors
890
+ });
891
+ }
892
+
893
+ function showUpdateToast(current, latest) {
894
+ updateToastText.textContent = 'Update available: v' + current + ' \u2192 v' + latest;
895
+ updateToast.hidden = false;
896
+ updateToastBtn.disabled = false;
897
+ updateToastBtn.textContent = 'Update Now';
898
+
899
+ updateToastBtn.onclick = function () {
900
+ triggerUpdate(latest);
901
+ };
902
+ }
903
+
904
+ function triggerUpdate(latest) {
905
+ updateToastBtn.disabled = true;
906
+ updateToastBtn.textContent = 'Updating\u2026';
907
+
908
+ fetch('/update', { method: 'POST' })
909
+ .then(function (res) {
910
+ return res.json().then(function (data) {
911
+ return { ok: res.ok, data: data };
912
+ });
913
+ })
914
+ .then(function (result) {
915
+ if (result.ok && result.data.restarting) {
916
+ updateToastText.textContent = 'Updated! Restarting server\u2026';
917
+ updateToastBtn.hidden = true;
918
+ updateToastDismiss.hidden = true;
919
+ setTimeout(function () {
920
+ location.reload();
921
+ }, 5000);
922
+ } else if (result.ok) {
923
+ updateToastText.textContent = 'Updated! Please restart the server manually.';
924
+ updateToastBtn.hidden = true;
925
+ } else {
926
+ updateToastText.textContent = 'Update failed: ' + (result.data.error || 'Unknown error');
927
+ updateToastBtn.disabled = false;
928
+ updateToastBtn.textContent = 'Retry';
929
+ }
930
+ })
931
+ .catch(function () {
932
+ updateToastText.textContent = 'Update failed. Please try again.';
933
+ updateToastBtn.disabled = false;
934
+ updateToastBtn.textContent = 'Retry';
935
+ });
936
+ }
937
+
938
+ updateToastDismiss.addEventListener('click', function () {
939
+ updateToast.hidden = true;
940
+ });
941
+
854
942
  // ── Auto-auth Check ─────────────────────────────────────────────────────────
855
943
 
856
944
  fetch('/sessions')
package/public/index.html CHANGED
@@ -78,6 +78,17 @@
78
78
  </div>
79
79
  </div>
80
80
 
81
+ <!-- Update Toast -->
82
+ <div id="update-toast" hidden>
83
+ <div id="update-toast-content">
84
+ <span id="update-toast-text"></span>
85
+ <div id="update-toast-actions">
86
+ <button id="update-toast-btn" class="btn-accent">Update Now</button>
87
+ <button id="update-toast-dismiss" aria-label="Dismiss">&times;</button>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
81
92
  </div>
82
93
 
83
94
  <!-- New Session Dialog -->
@@ -99,6 +110,13 @@
99
110
  <label for="custom-path-input">Or enter a local path:</label>
100
111
  <input type="text" id="custom-path-input" placeholder="/Users/you/code/my-repo" />
101
112
  </div>
113
+ <div class="dialog-option">
114
+ <label>
115
+ <input type="checkbox" id="dialog-yolo" />
116
+ Yolo mode
117
+ </label>
118
+ <span class="dialog-option-hint">Skip all permission prompts</span>
119
+ </div>
102
120
  <div class="dialog-actions">
103
121
  <button id="dialog-cancel">Cancel</button>
104
122
  <button id="dialog-start" class="btn-accent">New Worktree</button>
@@ -124,7 +142,8 @@
124
142
 
125
143
  <!-- Context Menu -->
126
144
  <div id="context-menu" class="context-menu" hidden>
127
- <button id="ctx-delete-worktree" class="context-menu-item">Delete worktree</button>
145
+ <button id="ctx-resume-yolo" class="context-menu-item">Resume in yolo mode</button>
146
+ <button id="ctx-delete-worktree" class="context-menu-item ctx-danger">Delete worktree</button>
128
147
  </div>
129
148
 
130
149
  <!-- Delete Worktree Confirmation Dialog -->
package/public/style.css CHANGED
@@ -590,6 +590,35 @@ dialog#new-session-dialog h2 {
590
590
  border-color: var(--accent);
591
591
  }
592
592
 
593
+ .dialog-option {
594
+ display: flex;
595
+ flex-direction: column;
596
+ gap: 2px;
597
+ margin-bottom: 1rem;
598
+ }
599
+
600
+ .dialog-option label {
601
+ display: flex;
602
+ align-items: center;
603
+ gap: 8px;
604
+ font-size: 0.875rem;
605
+ color: var(--text);
606
+ cursor: pointer;
607
+ }
608
+
609
+ .dialog-option input[type="checkbox"] {
610
+ width: 16px;
611
+ height: 16px;
612
+ accent-color: var(--accent);
613
+ cursor: pointer;
614
+ }
615
+
616
+ .dialog-option-hint {
617
+ font-size: 0.75rem;
618
+ color: var(--text-muted);
619
+ padding-left: 24px;
620
+ }
621
+
593
622
  .dialog-actions {
594
623
  display: flex;
595
624
  gap: 8px;
@@ -769,6 +798,11 @@ dialog#settings-dialog h2 {
769
798
  color: var(--accent);
770
799
  }
771
800
 
801
+ .context-menu-item.ctx-danger:hover,
802
+ .context-menu-item.ctx-danger:active {
803
+ color: #c0392b;
804
+ }
805
+
772
806
  /* ===== Delete Worktree Dialog ===== */
773
807
  dialog#delete-worktree-dialog {
774
808
  background: var(--surface);
@@ -818,6 +852,84 @@ dialog#delete-worktree-dialog h2 {
818
852
  opacity: 0.85;
819
853
  }
820
854
 
855
+ /* ===== Update Toast ===== */
856
+
857
+ #update-toast {
858
+ position: fixed;
859
+ bottom: 0;
860
+ left: 0;
861
+ right: 0;
862
+ z-index: 150;
863
+ display: flex;
864
+ justify-content: center;
865
+ padding: 12px 12px calc(12px + env(safe-area-inset-bottom));
866
+ pointer-events: none;
867
+ animation: toast-slide-up 0.25s ease-out;
868
+ }
869
+
870
+ @keyframes toast-slide-up {
871
+ from {
872
+ transform: translateY(100%);
873
+ opacity: 0;
874
+ }
875
+ to {
876
+ transform: translateY(0);
877
+ opacity: 1;
878
+ }
879
+ }
880
+
881
+ #update-toast-content {
882
+ display: flex;
883
+ flex-direction: row;
884
+ align-items: center;
885
+ gap: 12px;
886
+ background: var(--surface);
887
+ border: 1px solid var(--border);
888
+ border-radius: 10px;
889
+ padding: 12px 16px;
890
+ max-width: 500px;
891
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
892
+ pointer-events: auto;
893
+ }
894
+
895
+ #update-toast-text {
896
+ flex: 1;
897
+ font-size: 0.85rem;
898
+ color: var(--text);
899
+ }
900
+
901
+ #update-toast-actions {
902
+ display: flex;
903
+ gap: 8px;
904
+ flex-shrink: 0;
905
+ }
906
+
907
+ #update-toast-btn {
908
+ padding: 8px 14px;
909
+ border-radius: 6px;
910
+ font-size: 0.8rem;
911
+ border: none;
912
+ white-space: nowrap;
913
+ }
914
+
915
+ #update-toast-btn:disabled {
916
+ opacity: 0.6;
917
+ cursor: not-allowed;
918
+ }
919
+
920
+ #update-toast-dismiss {
921
+ background: none;
922
+ border: none;
923
+ color: var(--text-muted);
924
+ font-size: 1.2rem;
925
+ padding: 4px 6px;
926
+ cursor: pointer;
927
+ }
928
+
929
+ #update-toast-dismiss:hover {
930
+ color: var(--text);
931
+ }
932
+
821
933
  /* ===== Mobile Responsive ===== */
822
934
  @media (max-width: 600px) {
823
935
  #mobile-header {