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 +4 -0
- package/bin/cli.js +104 -0
- package/lib/project.js +0 -9
- package/lib/public/app.js +2 -146
- package/lib/public/css/menus.css +18 -102
- package/lib/public/css/messages.css +15 -0
- package/lib/public/index.html +5 -32
- package/lib/public/modules/notifications.js +0 -9
- package/lib/public/modules/tools.js +6 -0
- package/package.json +1 -1
- package/lib/usage.js +0 -90
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
|
-
|
|
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 ---
|
package/lib/public/css/menus.css
CHANGED
|
@@ -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
|
-
/* ---
|
|
489
|
-
.usage-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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
|
-
|
|
535
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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; }
|
package/lib/public/index.html
CHANGED
|
@@ -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
|
-
<
|
|
177
|
-
<
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
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
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 };
|