claude-relay 2.2.0 → 2.2.2

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/README.md CHANGED
@@ -185,6 +185,10 @@ npx claude-relay -p 8080 # Specify port
185
185
  npx claude-relay --no-https # Disable HTTPS
186
186
  npx claude-relay --no-update # Skip update check
187
187
  npx claude-relay --debug # Enable debug panel
188
+ npx claude-relay --add . # Add current directory to running daemon
189
+ npx claude-relay --add /path # Add a project by path
190
+ npx claude-relay --remove . # Remove a project
191
+ npx claude-relay --list # List registered projects
188
192
  ```
189
193
 
190
194
  ## Requirements
package/bin/cli.js CHANGED
@@ -18,6 +18,9 @@ var debugMode = false;
18
18
  var autoYes = false;
19
19
  var cliPin = null;
20
20
  var shutdownMode = false;
21
+ var addPath = null;
22
+ var removePath = null;
23
+ var listMode = false;
21
24
 
22
25
  for (var i = 0; i < args.length; i++) {
23
26
  if (args[i] === "-p" || args[i] === "--port") {
@@ -40,8 +43,19 @@ for (var i = 0; i < args.length; i++) {
40
43
  i++;
41
44
  } else if (args[i] === "--shutdown") {
42
45
  shutdownMode = true;
46
+ } else if (args[i] === "--add") {
47
+ addPath = args[i + 1] || ".";
48
+ i++;
49
+ } else if (args[i] === "--remove") {
50
+ removePath = args[i + 1] || null;
51
+ i++;
52
+ } else if (args[i] === "--list") {
53
+ listMode = true;
43
54
  } else if (args[i] === "-h" || args[i] === "--help") {
44
55
  console.log("Usage: claude-relay [-p|--port <port>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown]");
56
+ console.log(" claude-relay --add <path> Add a project to the running daemon");
57
+ console.log(" claude-relay --remove <path> Remove a project from the running daemon");
58
+ console.log(" claude-relay --list List registered projects");
45
59
  console.log("");
46
60
  console.log("Options:");
47
61
  console.log(" -p, --port <port> Port to listen on (default: 2633)");
@@ -51,6 +65,9 @@ for (var i = 0; i < args.length; i++) {
51
65
  console.log(" -y, --yes Skip interactive prompts (accept defaults)");
52
66
  console.log(" --pin <pin> Set 6-digit PIN (use with --yes)");
53
67
  console.log(" --shutdown Shut down the running relay daemon");
68
+ console.log(" --add <path> Add a project directory (use '.' for current)");
69
+ console.log(" --remove <path> Remove a project directory");
70
+ console.log(" --list List all registered projects");
54
71
  process.exit(0);
55
72
  }
56
73
  }
@@ -75,6 +92,93 @@ if (shutdownMode) {
75
92
  return;
76
93
  }
77
94
 
95
+ // --- Handle --add before anything else ---
96
+ if (addPath !== null) {
97
+ var absAdd = path.resolve(addPath);
98
+ try {
99
+ var stat = fs.statSync(absAdd);
100
+ if (!stat.isDirectory()) {
101
+ console.error("Not a directory: " + absAdd);
102
+ process.exit(1);
103
+ }
104
+ } catch (e) {
105
+ console.error("Directory not found: " + absAdd);
106
+ process.exit(1);
107
+ }
108
+ var addConfig = loadConfig();
109
+ isDaemonAliveAsync(addConfig).then(function (alive) {
110
+ if (!alive) {
111
+ console.error("No running daemon. Start with: npx claude-relay");
112
+ process.exit(1);
113
+ }
114
+ sendIPCCommand(socketPath(), { cmd: "add_project", path: absAdd }).then(function (res) {
115
+ if (res.ok) {
116
+ if (res.existing) {
117
+ console.log("Already registered: " + res.slug);
118
+ } else {
119
+ console.log("Added: " + res.slug + " \u2192 " + absAdd);
120
+ }
121
+ process.exit(0);
122
+ } else {
123
+ console.error("Failed: " + (res.error || "unknown error"));
124
+ process.exit(1);
125
+ }
126
+ });
127
+ });
128
+ return;
129
+ }
130
+
131
+ // --- Handle --remove before anything else ---
132
+ if (removePath !== null) {
133
+ var absRemove = path.resolve(removePath);
134
+ var removeConfig = loadConfig();
135
+ isDaemonAliveAsync(removeConfig).then(function (alive) {
136
+ if (!alive) {
137
+ console.error("No running daemon. Start with: npx claude-relay");
138
+ process.exit(1);
139
+ }
140
+ sendIPCCommand(socketPath(), { cmd: "remove_project", path: absRemove }).then(function (res) {
141
+ if (res.ok) {
142
+ console.log("Removed: " + path.basename(absRemove));
143
+ process.exit(0);
144
+ } else {
145
+ console.error("Failed: " + (res.error || "project not found"));
146
+ process.exit(1);
147
+ }
148
+ });
149
+ });
150
+ return;
151
+ }
152
+
153
+ // --- Handle --list before anything else ---
154
+ if (listMode) {
155
+ var listConfig = loadConfig();
156
+ isDaemonAliveAsync(listConfig).then(function (alive) {
157
+ if (!alive) {
158
+ console.error("No running daemon. Start with: npx claude-relay");
159
+ process.exit(1);
160
+ }
161
+ sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (res) {
162
+ if (!res.ok || !res.projects || res.projects.length === 0) {
163
+ console.log("No projects registered.");
164
+ process.exit(0);
165
+ return;
166
+ }
167
+ console.log("Projects (" + res.projects.length + "):\n");
168
+ for (var p = 0; p < res.projects.length; p++) {
169
+ var proj = res.projects[p];
170
+ var label = " " + proj.slug;
171
+ if (proj.title) label += " (" + proj.title + ")";
172
+ label += "\n " + proj.path;
173
+ console.log(label);
174
+ }
175
+ console.log("");
176
+ process.exit(0);
177
+ });
178
+ });
179
+ return;
180
+ }
181
+
78
182
  var cwd = process.cwd();
79
183
 
80
184
  // --- ANSI helpers ---
package/lib/project.js CHANGED
@@ -4,7 +4,6 @@ var { createSessionManager } = require("./sessions");
4
4
  var { createSDKBridge } = require("./sdk-bridge");
5
5
  var { createTerminalManager } = require("./terminal-manager");
6
6
  var { fetchLatestVersion, isNewer } = require("./updater");
7
- var { fetchUsageData } = require("./usage");
8
7
  var { execFileSync } = require("child_process");
9
8
 
10
9
  // SDK loaded dynamically (ESM module)
@@ -317,14 +316,6 @@ function createProjectContext(opts) {
317
316
  return;
318
317
  }
319
318
 
320
- if (msg.type === "get_usage") {
321
- fetchUsageData().then(function (data) {
322
- sendTo(ws, { type: "usage_data", data: data });
323
- }).catch(function (err) {
324
- sendTo(ws, { type: "usage_data", error: err.message || "Failed to fetch usage data" });
325
- });
326
- return;
327
- }
328
319
 
329
320
  if (msg.type === "set_model" && msg.model) {
330
321
  var session = sm.getActiveSession();
package/lib/public/app.js CHANGED
@@ -502,8 +502,6 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
502
502
  connectOverlay.classList.add("hidden");
503
503
  stopVerbCycle();
504
504
  updateFavicon("#57AB5A");
505
- if (usageFab) usageFab.classList.remove("hidden");
506
- if (usageHeaderBtn) usageHeaderBtn.classList.remove("hidden");
507
505
  } else if (status === "processing") {
508
506
  statusDot.classList.add("processing");
509
507
  processing = true;
@@ -605,8 +603,6 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
605
603
  // --- Usage panel ---
606
604
  var usagePanel = $("usage-panel");
607
605
  var usagePanelClose = $("usage-panel-close");
608
- var usageFab = $("usage-fab");
609
- var usageHeaderBtn = $("usage-header-btn");
610
606
  var usageCostEl = $("usage-cost");
611
607
  var usageInputEl = $("usage-input");
612
608
  var usageOutputEl = $("usage-output");
@@ -615,126 +611,12 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
615
611
  var usageTurnsEl = $("usage-turns");
616
612
  var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
617
613
 
618
- // Rate limit bar elements
619
- var usageLoading = $("usage-loading");
620
- var usageError = $("usage-error");
621
- var usageBars = $("usage-bars");
622
- var usageRateLimitFetched = false;
623
-
624
614
  function formatTokens(n) {
625
615
  if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
626
616
  if (n >= 1000) return (n / 1000).toFixed(1) + "K";
627
617
  return String(n);
628
618
  }
629
619
 
630
- function formatTimeUntil(isoStr) {
631
- if (!isoStr) return "";
632
- var d = new Date(isoStr);
633
- var now = Date.now();
634
- var diff = d.getTime() - now;
635
- if (diff <= 0) return "Resets soon";
636
- var hours = Math.floor(diff / 3600000);
637
- var minutes = Math.floor((diff % 3600000) / 60000);
638
- var relative;
639
- if (hours > 24) {
640
- var days = Math.floor(hours / 24);
641
- relative = days + "d " + (hours % 24) + "h";
642
- } else if (hours > 0) {
643
- relative = hours + "h " + minutes + "m";
644
- } else {
645
- relative = minutes + "m";
646
- }
647
- // Absolute time: "Mon 2/17 15:00" or "15:00" if today
648
- var nowDate = new Date(now);
649
- var month = d.getMonth() + 1;
650
- var day = d.getDate();
651
- var hh = String(d.getHours()).padStart(2, "0");
652
- var mm = String(d.getMinutes()).padStart(2, "0");
653
- var sameDay = d.getFullYear() === nowDate.getFullYear() && d.getMonth() === nowDate.getMonth() && d.getDate() === nowDate.getDate();
654
- var abs = sameDay ? hh + ":" + mm : month + "/" + day + " " + hh + ":" + mm;
655
- return "Resets in " + relative + " (" + abs + ")";
656
- }
657
-
658
- function updateRateLimitBar(prefix, utilization, resetsAt) {
659
- var pctEl = $("usage-pct-" + prefix);
660
- var fillEl = $("usage-fill-" + prefix);
661
- var resetEl = $("usage-reset-" + prefix);
662
- var groupEl = $("usage-bar-" + prefix);
663
- if (!pctEl || !fillEl || !resetEl) return;
664
-
665
- if (utilization == null) {
666
- if (groupEl) groupEl.classList.add("hidden");
667
- return;
668
- }
669
- if (groupEl) groupEl.classList.remove("hidden");
670
-
671
- var pct = Math.max(0, Math.min(100, Math.round(utilization)));
672
- pctEl.textContent = pct + "%";
673
- fillEl.style.width = pct + "%";
674
- fillEl.className = "usage-bar-fill";
675
- if (prefix === "extra") fillEl.classList.add("usage-bar-fill-extra");
676
- if (pct >= 90) fillEl.classList.add("critical");
677
- else if (pct >= 70) fillEl.classList.add("warn");
678
-
679
- resetEl.textContent = formatTimeUntil(resetsAt);
680
- }
681
-
682
- function handleUsageData(msg) {
683
- if (!usageLoading || !usageError || !usageBars) return;
684
- usageLoading.style.display = "none";
685
-
686
- if (msg.error) {
687
- usageError.textContent = msg.error;
688
- usageError.classList.remove("hidden");
689
- usageBars.classList.add("hidden");
690
- return;
691
- }
692
-
693
- usageError.classList.add("hidden");
694
- usageBars.classList.remove("hidden");
695
- var data = msg.data || {};
696
-
697
- // Session (five_hour)
698
- var session = data.five_hour || {};
699
- updateRateLimitBar("session", session.utilization, session.resets_at);
700
-
701
- // Weekly all models (seven_day)
702
- var weekly = data.seven_day || {};
703
- updateRateLimitBar("weekly", weekly.utilization, weekly.resets_at);
704
-
705
- // Weekly Sonnet only (seven_day_sonnet)
706
- var sonnet = data.seven_day_sonnet || {};
707
- updateRateLimitBar("sonnet", sonnet.utilization, sonnet.resets_at);
708
-
709
- // Extra usage
710
- var extra = data.extra_usage || {};
711
- var extraGroup = $("usage-bar-extra");
712
- if (extra.is_enabled) {
713
- if (extraGroup) extraGroup.classList.remove("hidden");
714
- // Compute reset time as first of next month
715
- var now = new Date();
716
- var nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
717
- updateRateLimitBar("extra", extra.utilization, nextMonth.toISOString());
718
- var extraResetEl = $("usage-reset-extra");
719
- if (extraResetEl && extra.monthly_limit != null) {
720
- var usedDollars = (extra.used_credits / 100).toFixed(2);
721
- var limitDollars = (extra.monthly_limit / 100).toFixed(2);
722
- extraResetEl.textContent = "$" + usedDollars + " / $" + limitDollars;
723
- }
724
- } else {
725
- if (extraGroup) extraGroup.classList.add("hidden");
726
- }
727
-
728
- usageRateLimitFetched = true;
729
- }
730
-
731
- function requestUsageData() {
732
- if (!ws || ws.readyState !== 1) return;
733
- if (usageLoading) usageLoading.style.display = "";
734
- if (usageError) usageError.classList.add("hidden");
735
- ws.send(JSON.stringify({ type: "get_usage" }));
736
- }
737
-
738
620
  function updateUsagePanel() {
739
621
  if (!usageCostEl) return;
740
622
  usageCostEl.textContent = "$" + sessionUsage.cost.toFixed(4);
@@ -761,40 +643,17 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
761
643
  sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
762
644
  updateUsagePanel();
763
645
  if (usagePanel) usagePanel.classList.add("hidden");
764
- if (usageFab) usageFab.classList.remove("active");
765
- if (usageHeaderBtn) usageHeaderBtn.classList.remove("active");
766
- usageRateLimitFetched = false;
767
646
  }
768
647
 
769
648
  function toggleUsagePanel() {
770
649
  if (!usagePanel) return;
771
- var isHidden = usagePanel.classList.toggle("hidden");
772
- if (usageFab) usageFab.classList.toggle("active", !isHidden);
773
- if (usageHeaderBtn) usageHeaderBtn.classList.toggle("active", !isHidden);
774
- // Fetch rate limit data when opening
775
- if (!isHidden) {
776
- requestUsageData();
777
- }
650
+ usagePanel.classList.toggle("hidden");
778
651
  refreshIcons();
779
652
  }
780
653
 
781
654
  if (usagePanelClose) {
782
655
  usagePanelClose.addEventListener("click", function () {
783
656
  usagePanel.classList.add("hidden");
784
- if (usageFab) usageFab.classList.remove("active");
785
- if (usageHeaderBtn) usageHeaderBtn.classList.remove("active");
786
- });
787
- }
788
-
789
- if (usageFab) {
790
- usageFab.addEventListener("click", function () {
791
- toggleUsagePanel();
792
- });
793
- }
794
-
795
- if (usageHeaderBtn) {
796
- usageHeaderBtn.addEventListener("click", function () {
797
- toggleUsagePanel();
798
657
  });
799
658
  }
800
659
 
@@ -1195,10 +1054,6 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1195
1054
  updateModelSelector(msg.model, msg.models || []);
1196
1055
  break;
1197
1056
 
1198
- case "usage_data":
1199
- handleUsageData(msg);
1200
- break;
1201
-
1202
1057
  case "client_count":
1203
1058
  var countEl = document.getElementById("client-count");
1204
1059
  if (countEl) {
@@ -1623,6 +1478,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1623
1478
  sessionListEl: sessionListEl,
1624
1479
  scrollToBottom: scrollToBottom,
1625
1480
  basePath: basePath,
1481
+ toggleUsagePanel: toggleUsagePanel,
1626
1482
  });
1627
1483
 
1628
1484
  // --- QR code ---
@@ -85,24 +85,6 @@
85
85
  flex-shrink: 0;
86
86
  }
87
87
 
88
- /* --- Usage header button (mobile only) --- */
89
- #usage-header-btn {
90
- display: none;
91
- align-items: center;
92
- justify-content: center;
93
- background: none;
94
- border: 1px solid transparent;
95
- border-radius: 8px;
96
- color: var(--text-dimmer);
97
- cursor: pointer;
98
- padding: 4px;
99
- transition: color 0.15s, background 0.15s, border-color 0.15s;
100
- }
101
- #usage-header-btn .lucide { width: 15px; height: 15px; }
102
- #usage-header-btn:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); border-color: var(--border); }
103
- #usage-header-btn.active { color: var(--accent); border-color: var(--accent); }
104
- #usage-header-btn.hidden { display: none !important; }
105
-
106
88
  /* --- Terminal toggle button --- */
107
89
  #terminal-toggle-btn {
108
90
  display: flex;
@@ -485,68 +467,29 @@
485
467
  padding: 8px 14px 12px;
486
468
  }
487
469
 
488
- /* --- Rate limit bars --- */
489
- .usage-loading {
490
- color: var(--text-muted);
491
- padding: 8px 0;
492
- text-align: center;
493
- }
494
-
495
- .usage-error {
496
- color: var(--error);
497
- padding: 6px 0;
498
- font-size: 11px;
499
- line-height: 1.4;
500
- }
501
-
502
- .usage-error.hidden { display: none; }
503
-
504
- .usage-bar-group {
505
- margin-bottom: 10px;
506
- }
507
-
508
- .usage-bar-group:last-child { margin-bottom: 0; }
509
- .usage-bar-group.hidden { display: none; }
510
-
511
- .usage-bar-label {
470
+ /* --- Plan usage link --- */
471
+ .usage-external-link {
512
472
  display: flex;
513
- justify-content: space-between;
514
473
  align-items: center;
515
- margin-bottom: 4px;
516
- color: var(--text-secondary);
517
- font-size: 11px;
518
- }
519
-
520
- .usage-bar-pct {
521
- font-family: "SF Mono", Menlo, Monaco, monospace;
522
- font-weight: 600;
523
- font-size: 11px;
524
- }
525
-
526
- .usage-bar-track {
527
- height: 6px;
528
- background: var(--input-bg);
474
+ justify-content: center;
475
+ gap: 6px;
476
+ padding: 8px 12px;
477
+ border-radius: 6px;
529
478
  border: 1px solid var(--border-subtle);
530
- border-radius: 3px;
531
- overflow: hidden;
479
+ background: var(--input-bg);
480
+ color: var(--text-secondary);
481
+ font-size: 12px;
482
+ text-decoration: none;
483
+ transition: background 0.15s, color 0.15s;
532
484
  }
533
-
534
- .usage-bar-fill {
535
- height: 100%;
536
- border-radius: 3px;
537
- background: var(--accent);
538
- transition: width 0.4s ease;
539
- min-width: 0;
485
+ .usage-external-link:hover {
486
+ background: var(--hover-bg);
487
+ color: var(--text-primary);
540
488
  }
541
-
542
- .usage-bar-fill.warn { background: var(--warning, #E5A84B); }
543
- .usage-bar-fill.critical { background: var(--error); }
544
- .usage-bar-fill-extra { background: var(--text-dimmer); }
545
-
546
- .usage-bar-reset {
547
- font-size: 10px;
548
- color: var(--text-muted);
549
- margin-top: 2px;
489
+ .usage-external-link svg {
490
+ width: 13px;
491
+ height: 13px;
492
+ flex-shrink: 0;
550
493
  }
551
494
 
552
495
  .usage-divider {
@@ -580,31 +523,6 @@
580
523
  font-weight: 500;
581
524
  }
582
525
 
583
- /* --- Usage FAB --- */
584
- #usage-fab {
585
- position: fixed;
586
- bottom: calc(var(--safe-bottom, 0px) + 16px);
587
- right: 16px;
588
- width: 40px;
589
- height: 40px;
590
- border-radius: 50%;
591
- border: 1px solid var(--border);
592
- background: var(--bg-alt);
593
- color: var(--text-muted);
594
- cursor: pointer;
595
- display: flex;
596
- align-items: center;
597
- justify-content: center;
598
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
599
- z-index: 90;
600
- transition: color 0.15s, background 0.15s, border-color 0.15s;
601
- }
602
-
603
- #usage-fab.hidden { display: none; }
604
- #usage-fab .lucide { width: 18px; height: 18px; }
605
- #usage-fab:hover { color: var(--text); border-color: var(--text-dimmer); }
606
- #usage-fab.active { color: var(--accent); border-color: var(--accent); }
607
-
608
526
  @media (max-width: 768px) {
609
527
  #usage-panel {
610
528
  top: auto;
@@ -614,8 +532,6 @@
614
532
  width: auto;
615
533
  max-height: 60vh;
616
534
  }
617
- #usage-fab { display: none !important; }
618
- #usage-header-btn:not(.hidden) { display: flex; }
619
535
  }
620
536
 
621
537
  .status-dot.connected { background: var(--success); }
@@ -46,6 +46,21 @@
46
46
  #todo-sticky .todo-sticky-chevron { display: inline-flex; color: var(--text-muted); transition: transform 0.2s; }
47
47
  #todo-sticky .todo-sticky-chevron .lucide { width: 12px; height: 12px; }
48
48
  #todo-sticky.collapsed .todo-sticky-chevron { transform: rotate(-90deg); }
49
+ #todo-sticky .todo-sticky-active { display: none; }
50
+ #todo-sticky.collapsed .todo-sticky-active {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: 4px;
54
+ flex: 1;
55
+ min-width: 0;
56
+ font-size: 11px;
57
+ color: var(--text-secondary);
58
+ overflow: hidden;
59
+ text-overflow: ellipsis;
60
+ white-space: nowrap;
61
+ }
62
+ #todo-sticky.collapsed .todo-sticky-active .lucide { width: 12px; height: 12px; color: var(--accent); flex-shrink: 0; }
63
+ #todo-sticky.collapsed .todo-sticky-title { flex: none; }
49
64
 
50
65
  #todo-sticky .todo-sticky-progress { height: 2px; background: var(--border); }
51
66
  #todo-sticky .todo-sticky-progress-bar { height: 100%; background: var(--success); transition: width 0.3s ease; }
@@ -80,9 +80,6 @@
80
80
  <i data-lucide="ellipsis" style="width:14px;height:14px"></i>
81
81
  </button>
82
82
  <div id="sidebar-footer-menu" class="hidden">
83
- <button class="sidebar-menu-item" id="footer-usage">
84
- <i data-lucide="gauge"></i> <span>Usage</span>
85
- </button>
86
83
  <a class="sidebar-menu-item" href="https://github.com/chadbyte/claude-relay" target="_blank" rel="noopener">
87
84
  <i data-lucide="github"></i> <span>GitHub</span>
88
85
  </a>
@@ -125,7 +122,6 @@
125
122
  </label>
126
123
  </div>
127
124
  </div>
128
- <button id="usage-header-btn" class="hidden" title="Usage"><i data-lucide="dollar-sign"></i></button>
129
125
  <button id="terminal-toggle-btn" title="Terminal"><i data-lucide="square-terminal"></i><span id="terminal-count" class="hidden"></span></button>
130
126
  <button id="qr-btn" title="Share"><i data-lucide="share"></i></button>
131
127
  <div id="qr-overlay" class="hidden">
@@ -173,33 +169,11 @@
173
169
  <button id="usage-panel-close" aria-label="Close"><i data-lucide="x"></i></button>
174
170
  </div>
175
171
  <div class="usage-panel-body">
176
- <div id="usage-rate-limits">
177
- <div id="usage-loading" class="usage-loading">Loading...</div>
178
- <div id="usage-error" class="usage-error hidden"></div>
179
- <div id="usage-bars" class="hidden">
180
- <div class="usage-bar-group" id="usage-bar-session">
181
- <div class="usage-bar-label"><span>Current session</span><span class="usage-bar-pct" id="usage-pct-session"></span></div>
182
- <div class="usage-bar-track"><div class="usage-bar-fill" id="usage-fill-session"></div></div>
183
- <div class="usage-bar-reset" id="usage-reset-session"></div>
184
- </div>
185
- <div class="usage-bar-group" id="usage-bar-weekly">
186
- <div class="usage-bar-label"><span>Current week (all models)</span><span class="usage-bar-pct" id="usage-pct-weekly"></span></div>
187
- <div class="usage-bar-track"><div class="usage-bar-fill" id="usage-fill-weekly"></div></div>
188
- <div class="usage-bar-reset" id="usage-reset-weekly"></div>
189
- </div>
190
- <div class="usage-bar-group" id="usage-bar-sonnet">
191
- <div class="usage-bar-label"><span>Current week (Sonnet only)</span><span class="usage-bar-pct" id="usage-pct-sonnet"></span></div>
192
- <div class="usage-bar-track"><div class="usage-bar-fill" id="usage-fill-sonnet"></div></div>
193
- <div class="usage-bar-reset" id="usage-reset-sonnet"></div>
194
- </div>
195
- <div class="usage-bar-group hidden" id="usage-bar-extra">
196
- <div class="usage-bar-label"><span>Extra usage</span><span class="usage-bar-pct" id="usage-pct-extra"></span></div>
197
- <div class="usage-bar-track"><div class="usage-bar-fill usage-bar-fill-extra" id="usage-fill-extra"></div></div>
198
- <div class="usage-bar-reset" id="usage-reset-extra"></div>
199
- </div>
200
- </div>
201
- </div>
202
- <div class="usage-divider" id="usage-session-divider"></div>
172
+ <a href="https://claude.ai/settings/usage" target="_blank" rel="noopener" class="usage-external-link">
173
+ <span>Check plan usage on claude.ai</span>
174
+ <i data-lucide="external-link"></i>
175
+ </a>
176
+ <div class="usage-divider"></div>
203
177
  <div class="usage-section-label">Session</div>
204
178
  <div class="usage-row"><span class="usage-label">Cost</span><span id="usage-cost" class="usage-value">$0.0000</span></div>
205
179
  <div class="usage-row"><span class="usage-label">Input tokens</span><span id="usage-input" class="usage-value">0</span></div>
@@ -211,7 +185,6 @@
211
185
  </div>
212
186
  <div id="todo-sticky" class="hidden"></div>
213
187
  <button id="new-msg-btn" class="hidden" aria-label="Scroll to bottom"></button>
214
- <button id="usage-fab" class="hidden" aria-label="Usage"><i data-lucide="gauge"></i></button>
215
188
  <div id="input-area">
216
189
  <div id="input-wrapper">
217
190
  <div id="slash-menu"></div>
@@ -137,7 +137,6 @@ export function initNotifications(_ctx) {
137
137
  var footerBtn = $("sidebar-footer-btn");
138
138
  var footerMenu = $("sidebar-footer-menu");
139
139
  var footerUpdateCheck = $("footer-update-check");
140
- var footerUsage = $("footer-usage");
141
140
  if (!footerBtn || !footerMenu) return;
142
141
 
143
142
  footerBtn.addEventListener("click", function (e) {
@@ -151,14 +150,6 @@ export function initNotifications(_ctx) {
151
150
  }
152
151
  });
153
152
 
154
- if (footerUsage && ctx.toggleUsagePanel) {
155
- footerUsage.addEventListener("click", function (e) {
156
- e.stopPropagation();
157
- footerMenu.classList.add("hidden");
158
- ctx.toggleUsagePanel();
159
- });
160
- }
161
-
162
153
  function setUpdateIcon(name, spin) {
163
154
  var el = footerUpdateCheck.querySelector(".lucide, [data-lucide]");
164
155
  if (!el) return;
@@ -693,10 +693,16 @@ function updateTodoSticky() {
693
693
  var pct = Math.round(completed / todoItems.length * 100);
694
694
  var wasCollapsed = stickyEl.classList.contains("collapsed");
695
695
 
696
+ var inProgressItem = null;
697
+ for (var j = 0; j < todoItems.length; j++) {
698
+ if (todoItems[j].status === "in_progress") { inProgressItem = todoItems[j]; break; }
699
+ }
700
+
696
701
  var html = '<div class="todo-sticky-inner">' +
697
702
  '<div class="todo-sticky-header">' +
698
703
  '<span class="todo-sticky-icon">' + iconHtml("list-checks") + '</span>' +
699
704
  '<span class="todo-sticky-title">Tasks</span>' +
705
+ (inProgressItem ? '<span class="todo-sticky-active">' + iconHtml("loader", "icon-spin") + ' ' + escapeHtml(inProgressItem.activeForm || inProgressItem.content) + '</span>' : '') +
700
706
  '<span class="todo-sticky-count">' + completed + '/' + todoItems.length + '</span>' +
701
707
  '<span class="todo-sticky-chevron">' + iconHtml("chevron-down") + '</span>' +
702
708
  '</div>' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-relay",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "claude-relay": "./bin/cli.js"
package/lib/usage.js DELETED
@@ -1,90 +0,0 @@
1
- var os = require("os");
2
- var path = require("path");
3
- var fs = require("fs");
4
- var { execFileSync } = require("child_process");
5
- var https = require("https");
6
-
7
- var BASE_API_URL = "https://api.anthropic.com";
8
-
9
- function readOAuthToken() {
10
- // Priority 1: env var override
11
- if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
12
- return process.env.CLAUDE_CODE_OAUTH_TOKEN;
13
- }
14
-
15
- // Priority 2: macOS keychain
16
- if (process.platform === "darwin") {
17
- try {
18
- var user = os.userInfo().username;
19
- var data = execFileSync("security", [
20
- "find-generic-password", "-a", user, "-w", "-s", "Claude Code-credentials"
21
- ], { encoding: "utf8", timeout: 5000 }).trim();
22
- if (data) {
23
- var parsed = JSON.parse(data);
24
- if (parsed.claudeAiOauth && parsed.claudeAiOauth.accessToken) {
25
- return parsed.claudeAiOauth.accessToken;
26
- }
27
- }
28
- } catch (e) { /* fall through */ }
29
- }
30
-
31
- // Priority 3: plaintext credentials file
32
- var configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
33
- var credFile = path.join(configDir, ".credentials.json");
34
- try {
35
- var content = fs.readFileSync(credFile, "utf8");
36
- var parsed = JSON.parse(content);
37
- if (parsed.claudeAiOauth && parsed.claudeAiOauth.accessToken) {
38
- return parsed.claudeAiOauth.accessToken;
39
- }
40
- } catch (e) { /* fall through */ }
41
-
42
- return null;
43
- }
44
-
45
- function fetchUsageData() {
46
- return new Promise(function (resolve, reject) {
47
- var token = readOAuthToken();
48
- if (!token) {
49
- reject(new Error("No OAuth token available"));
50
- return;
51
- }
52
-
53
- var url = new URL(BASE_API_URL + "/api/oauth/usage");
54
- var options = {
55
- hostname: url.hostname,
56
- port: url.port || 443,
57
- path: url.pathname,
58
- method: "GET",
59
- headers: {
60
- "Content-Type": "application/json",
61
- "Authorization": "Bearer " + token,
62
- "anthropic-beta": "oauth-2025-04-20",
63
- "User-Agent": "claude-code/2.0.0",
64
- },
65
- timeout: 5000,
66
- };
67
-
68
- var req = https.request(options, function (res) {
69
- var body = "";
70
- res.on("data", function (chunk) { body += chunk; });
71
- res.on("end", function () {
72
- if (res.statusCode !== 200) {
73
- reject(new Error("Usage API returned " + res.statusCode));
74
- return;
75
- }
76
- try {
77
- resolve(JSON.parse(body));
78
- } catch (e) {
79
- reject(new Error("Invalid JSON from usage API"));
80
- }
81
- });
82
- });
83
-
84
- req.on("error", function (err) { reject(err); });
85
- req.on("timeout", function () { req.destroy(); reject(new Error("Usage API timeout")); });
86
- req.end();
87
- });
88
- }
89
-
90
- module.exports = { fetchUsageData: fetchUsageData };