@jeffreycao/copilot-api 1.1.9 → 1.2.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/README.md +2 -2
- package/dist/{config-5U36xkbl.js → config-Zv1NG6Kd.js} +3 -15
- package/dist/config-Zv1NG6Kd.js.map +1 -0
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/{server-yNK3IkCT.js → server-BSFMHogd.js} +13 -4
- package/dist/{server-yNK3IkCT.js.map → server-BSFMHogd.js.map} +1 -1
- package/package.json +3 -2
- package/pages/index.html +556 -0
- package/dist/config-5U36xkbl.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffreycao/copilot-api",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code Or Codex Or Opencode!",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"proxy",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"copilot-api": "./dist/main.js"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
|
-
"dist"
|
|
23
|
+
"dist",
|
|
24
|
+
"pages"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsdown",
|
package/pages/index.html
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Copilot API Usage Dashboard</title>
|
|
7
|
+
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
11
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
12
|
+
<link
|
|
13
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
14
|
+
rel="stylesheet"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<script src="https://unpkg.com/lucide-react@0.378.0/dist/umd/lucide.min.js"></script>
|
|
18
|
+
|
|
19
|
+
<style>
|
|
20
|
+
/* Gruvbox-themed color palette */
|
|
21
|
+
:root {
|
|
22
|
+
/* Main Color Palette */
|
|
23
|
+
--color-red: #cc241d;
|
|
24
|
+
--color-green: #98971a;
|
|
25
|
+
--color-yellow: #d79921;
|
|
26
|
+
--color-blue: #458588;
|
|
27
|
+
--color-purple: #b16286;
|
|
28
|
+
--color-aqua: #689d6a;
|
|
29
|
+
--color-orange: #d65d0e;
|
|
30
|
+
--color-gray: #a89984;
|
|
31
|
+
|
|
32
|
+
/* Accent/Lighter/Darker Shades of Main Colors */
|
|
33
|
+
--color-red-accent: #fb4934;
|
|
34
|
+
--color-green-accent: #b8bb26;
|
|
35
|
+
--color-yellow-accent: #fabd2f;
|
|
36
|
+
--color-blue-accent: #83a598;
|
|
37
|
+
--color-purple-accent: #d3869b;
|
|
38
|
+
--color-aqua-accent: #8ec07c;
|
|
39
|
+
--color-orange-accent: #fe8019;
|
|
40
|
+
--color-gray-accent: #928374;
|
|
41
|
+
|
|
42
|
+
/* Background Colors */
|
|
43
|
+
--color-bg-darkest: #1d2021; /* bg0_h */
|
|
44
|
+
--color-bg: #282828; /* bg and bg0 */
|
|
45
|
+
--color-bg-light-1: #3c3836; /* bg1 */
|
|
46
|
+
--color-bg-light-2: #504945; /* bg2 */
|
|
47
|
+
--color-bg-light-3: #665c54; /* bg3 */
|
|
48
|
+
--color-bg-light-4: #7c6f64; /* bg4 */
|
|
49
|
+
--color-bg-soft: #32302f; /* bg0_s */
|
|
50
|
+
|
|
51
|
+
/* Foreground Colors */
|
|
52
|
+
--color-fg-darker: #a89984; /* fg4 - duplicate of gray */
|
|
53
|
+
--color-fg-dark: #bdae93; /* fg3 */
|
|
54
|
+
--color-fg-medium: #d5c4a1; /* fg2 */
|
|
55
|
+
--color-fg-light: #ebdbb2; /* fg and fg1 */
|
|
56
|
+
--color-fg-lightest: #fbf1c7; /* fg0 */
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Custom styles using the new palette */
|
|
60
|
+
body {
|
|
61
|
+
font-family: "Inter", sans-serif;
|
|
62
|
+
background-color: var(--color-bg-darkest);
|
|
63
|
+
color: var(--color-fg-light);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Custom progress bar styles */
|
|
67
|
+
.progress-bar-bg {
|
|
68
|
+
background-color: var(--color-bg-light-1);
|
|
69
|
+
}
|
|
70
|
+
.progress-bar-fg {
|
|
71
|
+
transition: width 0.5s ease-in-out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Custom scrollbar for the raw data view */
|
|
75
|
+
.code-block::-webkit-scrollbar {
|
|
76
|
+
width: 8px;
|
|
77
|
+
height: 8px;
|
|
78
|
+
}
|
|
79
|
+
.code-block::-webkit-scrollbar-track {
|
|
80
|
+
background: var(--color-bg);
|
|
81
|
+
}
|
|
82
|
+
.code-block::-webkit-scrollbar-thumb {
|
|
83
|
+
background: var(--color-bg-light-3);
|
|
84
|
+
}
|
|
85
|
+
.code-block::-webkit-scrollbar-thumb:hover {
|
|
86
|
+
background: var(--color-bg-light-4);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Style for focus rings to use variables */
|
|
90
|
+
.input-focus:focus {
|
|
91
|
+
--tw-ring-color: var(--color-blue);
|
|
92
|
+
border-color: var(--color-blue);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
</style>
|
|
97
|
+
</head>
|
|
98
|
+
<body class="antialiased">
|
|
99
|
+
<div id="app" class="min-h-screen p-4 sm:p-6">
|
|
100
|
+
<div class="max-w-7xl mx-auto">
|
|
101
|
+
<!-- Header Section -->
|
|
102
|
+
<header class="mb-6">
|
|
103
|
+
<h1
|
|
104
|
+
class="text-2xl font-bold flex items-center gap-2"
|
|
105
|
+
style="color: var(--color-fg-lightest)"
|
|
106
|
+
>
|
|
107
|
+
<svg
|
|
108
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
109
|
+
width="24"
|
|
110
|
+
height="24"
|
|
111
|
+
viewBox="0 0 24 24"
|
|
112
|
+
fill="none"
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
stroke-width="2"
|
|
115
|
+
stroke-linecap="round"
|
|
116
|
+
stroke-linejoin="round"
|
|
117
|
+
class="lucide lucide-gauge-circle h-7 w-7"
|
|
118
|
+
style="color: var(--color-aqua-accent)"
|
|
119
|
+
>
|
|
120
|
+
<path d="M15.6 3.3a10 10 0 1 0 5.1 5.1" />
|
|
121
|
+
<path
|
|
122
|
+
d="M12 12a1 1 0 0 0-1-1v4a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-3z"
|
|
123
|
+
/>
|
|
124
|
+
<path d="M12 6.8a10 10 0 0 0 -3.2 7.2" />
|
|
125
|
+
</svg>
|
|
126
|
+
<span>Copilot API Usage Dashboard</span>
|
|
127
|
+
</h1>
|
|
128
|
+
<p class="mt-1 text-sm" style="color: var(--color-gray)">
|
|
129
|
+
Should be the same as the one in VSCode
|
|
130
|
+
</p>
|
|
131
|
+
</header>
|
|
132
|
+
|
|
133
|
+
<!-- Form Section -->
|
|
134
|
+
<div
|
|
135
|
+
class="mb-6 p-4 border"
|
|
136
|
+
style="
|
|
137
|
+
background-color: var(--color-bg-soft);
|
|
138
|
+
border-color: var(--color-bg-light-2);
|
|
139
|
+
"
|
|
140
|
+
>
|
|
141
|
+
<form
|
|
142
|
+
id="endpoint-form"
|
|
143
|
+
class="flex flex-col sm:flex-row items-center gap-3"
|
|
144
|
+
>
|
|
145
|
+
<label
|
|
146
|
+
for="endpoint-url"
|
|
147
|
+
class="font-semibold whitespace-nowrap text-sm"
|
|
148
|
+
style="color: var(--color-fg-lightest)"
|
|
149
|
+
>API Endpoint URL</label
|
|
150
|
+
>
|
|
151
|
+
<input
|
|
152
|
+
type="text"
|
|
153
|
+
id="endpoint-url"
|
|
154
|
+
class="w-full px-3 py-1.5 border focus:ring-1 transition input-focus text-sm"
|
|
155
|
+
style="
|
|
156
|
+
background-color: var(--color-bg-darkest);
|
|
157
|
+
border-color: var(--color-bg-light-3);
|
|
158
|
+
color: var(--color-fg-medium);
|
|
159
|
+
"
|
|
160
|
+
placeholder="http://localhost:4141/usage"
|
|
161
|
+
/>
|
|
162
|
+
<button
|
|
163
|
+
id="fetch-button"
|
|
164
|
+
type="submit"
|
|
165
|
+
class="w-full sm:w-auto font-bold py-1.5 px-5 transition-colors flex items-center justify-center gap-2 text-sm"
|
|
166
|
+
style="
|
|
167
|
+
background-color: var(--color-blue);
|
|
168
|
+
color: var(--color-bg-darkest);
|
|
169
|
+
"
|
|
170
|
+
>
|
|
171
|
+
<svg
|
|
172
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
173
|
+
width="24"
|
|
174
|
+
height="24"
|
|
175
|
+
viewBox="0 0 24 24"
|
|
176
|
+
fill="none"
|
|
177
|
+
stroke="currentColor"
|
|
178
|
+
stroke-width="2"
|
|
179
|
+
stroke-linecap="round"
|
|
180
|
+
stroke-linejoin="round"
|
|
181
|
+
class="lucide lucide-refresh-cw h-4 w-4"
|
|
182
|
+
>
|
|
183
|
+
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
|
184
|
+
<path d="M21 3v5h-5" />
|
|
185
|
+
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
|
186
|
+
<path d="M3 21v-5h5" />
|
|
187
|
+
</svg>
|
|
188
|
+
<span>Fetch</span>
|
|
189
|
+
</button>
|
|
190
|
+
</form>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<!-- Content Area for dynamic data -->
|
|
194
|
+
<main id="content-area"></main>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<script>
|
|
199
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
200
|
+
const endpointForm = document.getElementById("endpoint-form");
|
|
201
|
+
const endpointUrlInput = document.getElementById("endpoint-url");
|
|
202
|
+
const contentArea = document.getElementById("content-area");
|
|
203
|
+
const fetchButton = document.getElementById("fetch-button");
|
|
204
|
+
|
|
205
|
+
// Apply hover effect for fetch button via JS
|
|
206
|
+
fetchButton.addEventListener("mouseenter", () => {
|
|
207
|
+
fetchButton.style.backgroundColor = "var(--color-blue-accent)";
|
|
208
|
+
});
|
|
209
|
+
fetchButton.addEventListener("mouseleave", () => {
|
|
210
|
+
fetchButton.style.backgroundColor = "var(--color-blue)";
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const DEFAULT_ENDPOINT = "http://localhost:4141/usage";
|
|
214
|
+
|
|
215
|
+
// --- State Management ---
|
|
216
|
+
const state = {
|
|
217
|
+
isLoading: false,
|
|
218
|
+
error: null,
|
|
219
|
+
data: null,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// --- Rendering Logic ---
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Safely calls lucide.createIcons() if the library is available.
|
|
226
|
+
*/
|
|
227
|
+
function createIcons() {
|
|
228
|
+
if (typeof lucide !== "undefined") {
|
|
229
|
+
lucide.createIcons();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Renders the entire UI based on the current state.
|
|
235
|
+
*/
|
|
236
|
+
function render() {
|
|
237
|
+
if (state.isLoading) {
|
|
238
|
+
contentArea.innerHTML = renderSpinner();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (state.error) {
|
|
242
|
+
contentArea.innerHTML = renderError(state.error);
|
|
243
|
+
} else if (state.data) {
|
|
244
|
+
contentArea.innerHTML = `
|
|
245
|
+
${renderUsageQuotas(state.data.quota_snapshots)}
|
|
246
|
+
${renderDetailedData(state.data)}
|
|
247
|
+
`;
|
|
248
|
+
} else {
|
|
249
|
+
contentArea.innerHTML = renderWelcomeMessage();
|
|
250
|
+
}
|
|
251
|
+
// Replace placeholder icons with actual Lucide icons
|
|
252
|
+
createIcons();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Renders the "Usage Quotas" section with progress bars.
|
|
257
|
+
* @param {object} snapshots - The quota_snapshots object from the API response.
|
|
258
|
+
* @returns {string} HTML string for the usage quotas section.
|
|
259
|
+
*/
|
|
260
|
+
function renderUsageQuotas(snapshots) {
|
|
261
|
+
if (!snapshots) return "";
|
|
262
|
+
|
|
263
|
+
const quotaCards = Object.entries(snapshots)
|
|
264
|
+
.map(([key, value]) => {
|
|
265
|
+
return renderQuotaCard(key, value);
|
|
266
|
+
})
|
|
267
|
+
.join("");
|
|
268
|
+
|
|
269
|
+
return `
|
|
270
|
+
<section id="usage-quotas" class="mb-6">
|
|
271
|
+
<h2 class="text-xl font-bold mb-3 flex items-center gap-2" style="color: var(--color-fg-lightest);">
|
|
272
|
+
<i data-lucide="bar-chart-big"></i> Usage Quotas
|
|
273
|
+
</h2>
|
|
274
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
275
|
+
${quotaCards}
|
|
276
|
+
</div>
|
|
277
|
+
</section>
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Renders a single quota card.
|
|
283
|
+
* @param {string} title - The name of the quota (e.g., 'chat').
|
|
284
|
+
* @param {object} details - The details object for the quota.
|
|
285
|
+
* @returns {string} HTML string for a single card.
|
|
286
|
+
*/
|
|
287
|
+
function renderQuotaCard(title, details) {
|
|
288
|
+
const { entitlement, remaining, percent_remaining, unlimited } =
|
|
289
|
+
details;
|
|
290
|
+
|
|
291
|
+
const percentUsed = unlimited ? 0 : 100 - percent_remaining;
|
|
292
|
+
const used = unlimited
|
|
293
|
+
? "N/A"
|
|
294
|
+
: (entitlement - remaining).toLocaleString();
|
|
295
|
+
|
|
296
|
+
let progressBarColor = "var(--color-green)";
|
|
297
|
+
if (percentUsed > 75) progressBarColor = "var(--color-yellow)";
|
|
298
|
+
if (percentUsed > 90) progressBarColor = "var(--color-red)";
|
|
299
|
+
if (unlimited) progressBarColor = "var(--color-blue)";
|
|
300
|
+
|
|
301
|
+
return `
|
|
302
|
+
<div class="p-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
|
|
303
|
+
<div class="flex justify-between items-center mb-2">
|
|
304
|
+
<h3 class="text-md font-semibold capitalize" style="color: var(--color-fg-lightest);">${title.replace(/_/g, " ")}</h3>
|
|
305
|
+
${
|
|
306
|
+
unlimited
|
|
307
|
+
? `<span class="px-2 py-0.5 text-xs font-medium" style="color: var(--color-blue-accent); background-color: var(--color-bg-light-1);">Unlimited</span>`
|
|
308
|
+
: `<span class="text-sm font-mono" style="color: var(--color-fg-medium);">${percentUsed.toFixed(1)}% Used</span>`
|
|
309
|
+
}
|
|
310
|
+
</div>
|
|
311
|
+
<div class="mb-3">
|
|
312
|
+
<div class="w-full progress-bar-bg h-2">
|
|
313
|
+
<div class="progress-bar-fg h-2" style="width: ${unlimited ? 100 : percentUsed}%; background-color: ${progressBarColor};"></div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="flex justify-between text-xs font-mono" style="color: var(--color-fg-dark);">
|
|
317
|
+
<span>${used} / ${unlimited ? "∞" : entitlement.toLocaleString()}</span>
|
|
318
|
+
<span>${unlimited ? "∞" : remaining.toLocaleString()} remaining</span>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Recursively builds a formatted HTML list from a JSON object.
|
|
326
|
+
* @param {object} obj - The object to format.
|
|
327
|
+
* @returns {string} HTML string for the formatted list.
|
|
328
|
+
*/
|
|
329
|
+
function formatObject(obj) {
|
|
330
|
+
if (obj === null || typeof obj !== "object") {
|
|
331
|
+
return `<span style="color: var(--color-green-accent);">${JSON.stringify(obj)}</span>`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
'<div class="pl-4">' +
|
|
336
|
+
Object.entries(obj)
|
|
337
|
+
.map(([key, value]) => {
|
|
338
|
+
const formattedKey = key.replace(/_/g, " ");
|
|
339
|
+
let displayValue;
|
|
340
|
+
|
|
341
|
+
if (Array.isArray(value)) {
|
|
342
|
+
displayValue =
|
|
343
|
+
value.length > 0
|
|
344
|
+
? `<span style='color: var(--color-gray-accent)'>[...${value.length} items]</span>`
|
|
345
|
+
: `<span style='color: var(--color-gray-accent)'>[]</span>`;
|
|
346
|
+
} else if (typeof value === "object" && value !== null) {
|
|
347
|
+
displayValue = formatObject(value);
|
|
348
|
+
} else if (typeof value === "boolean") {
|
|
349
|
+
displayValue = `<span class="font-semibold" style="color: ${value ? "var(--color-green-accent)" : "var(--color-red-accent)"}">${value}</span>`;
|
|
350
|
+
} else {
|
|
351
|
+
displayValue = `<span style="color: var(--color-blue-accent);">${JSON.stringify(value)}</span>`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return `<div class="mt-1">
|
|
355
|
+
<span class="capitalize font-semibold" style="color: var(--color-fg-medium);">${formattedKey}:</span>
|
|
356
|
+
${typeof value === "object" && value !== null && !Array.isArray(value) ? displayValue : ` ${displayValue}`}
|
|
357
|
+
</div>`;
|
|
358
|
+
})
|
|
359
|
+
.join("") +
|
|
360
|
+
"</div>"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Renders the section with the full, formatted API response.
|
|
366
|
+
* @param {object} data - The full API response data.
|
|
367
|
+
* @returns {string} HTML string for the full data section.
|
|
368
|
+
*/
|
|
369
|
+
function renderDetailedData(data) {
|
|
370
|
+
const formattedDetails = formatObject(data);
|
|
371
|
+
return `
|
|
372
|
+
<section id="detailed-data">
|
|
373
|
+
<h2 class="text-xl font-bold mb-3 flex items-center gap-2" style="color: var(--color-fg-lightest);">
|
|
374
|
+
<i data-lucide="file-text"></i> Detailed Information
|
|
375
|
+
</h2>
|
|
376
|
+
<div class="border p-4 relative font-mono text-xs" style="background-color: var(--color-bg-darkest); border-color: var(--color-bg-light-2);">
|
|
377
|
+
${formattedDetails}
|
|
378
|
+
</div>
|
|
379
|
+
</section>
|
|
380
|
+
`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Renders a loading spinner.
|
|
385
|
+
* @returns {string} HTML string for the spinner.
|
|
386
|
+
*/
|
|
387
|
+
function renderSpinner() {
|
|
388
|
+
return `
|
|
389
|
+
<div class="flex justify-center items-center py-20">
|
|
390
|
+
<div class="animate-spin h-12 w-12 rounded-full border-4 border-transparent border-t-4" style="border-top-color: var(--color-blue);"></div>
|
|
391
|
+
</div>`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Renders an error message box.
|
|
396
|
+
* @param {string} message - The error message to display.
|
|
397
|
+
* @returns {string} HTML string for the error message.
|
|
398
|
+
*/
|
|
399
|
+
function renderError(message) {
|
|
400
|
+
const container = document.createElement("div");
|
|
401
|
+
container.className = "p-3 border";
|
|
402
|
+
container.style.backgroundColor = "rgba(204, 36, 29, 0.2)";
|
|
403
|
+
container.style.borderColor = "var(--color-red)";
|
|
404
|
+
container.style.color = "var(--color-red-accent)";
|
|
405
|
+
container.setAttribute("role", "alert");
|
|
406
|
+
|
|
407
|
+
container.innerHTML = `
|
|
408
|
+
<div class="flex items-center">
|
|
409
|
+
<i data-lucide="alert-triangle" class="h-5 w-5 mr-3"></i>
|
|
410
|
+
<div>
|
|
411
|
+
<p class="font-bold text-sm">An Error Occurred</p>
|
|
412
|
+
<p class="text-xs">${message}</p>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
`;
|
|
416
|
+
// Must create icons *after* innerHTML is set
|
|
417
|
+
setTimeout(
|
|
418
|
+
() =>
|
|
419
|
+
lucide.createIcons({
|
|
420
|
+
nodes: [container.querySelector("[data-lucide]")],
|
|
421
|
+
}),
|
|
422
|
+
0
|
|
423
|
+
);
|
|
424
|
+
return container.outerHTML;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Renders a welcome message when the page first loads.
|
|
429
|
+
* @returns {string} HTML string for the welcome message.
|
|
430
|
+
*/
|
|
431
|
+
function renderWelcomeMessage() {
|
|
432
|
+
return `
|
|
433
|
+
<div class="text-center py-16 px-4 border" style="background-color: var(--color-bg-soft); border-color: var(--color-bg-light-2);">
|
|
434
|
+
<i data-lucide="info" class="mx-auto h-10 w-10" style="color: var(--color-gray-accent);"></i>
|
|
435
|
+
<h3 class="mt-2 text-lg font-semibold" style="color: var(--color-fg-lightest);">Welcome!</h3>
|
|
436
|
+
<p class="mt-1 text-sm" style="color: var(--color-gray);">Enter an API endpoint URL and click "Fetch" to see usage data.</p>
|
|
437
|
+
</div>
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// --- Data Fetching ---
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Fetches data from the specified API endpoint.
|
|
445
|
+
*/
|
|
446
|
+
async function fetchData() {
|
|
447
|
+
const url = endpointUrlInput.value.trim();
|
|
448
|
+
if (!url) {
|
|
449
|
+
state.error = "Endpoint URL cannot be empty.";
|
|
450
|
+
state.isLoading = false;
|
|
451
|
+
render();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
state.isLoading = true;
|
|
456
|
+
state.error = null;
|
|
457
|
+
render();
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const response = await fetch(url);
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
`Request failed with status ${response.status}: ${response.statusText}`
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const jsonData = await response.json();
|
|
467
|
+
state.data = jsonData;
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error("Fetch error:", error);
|
|
470
|
+
state.data = null;
|
|
471
|
+
state.error = error.message;
|
|
472
|
+
} finally {
|
|
473
|
+
state.isLoading = false;
|
|
474
|
+
render();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// --- Event Handlers & Initialization ---
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Handles the form submission to trigger a data fetch.
|
|
482
|
+
* @param {Event} event - The form submission event.
|
|
483
|
+
*/
|
|
484
|
+
function handleFormSubmit(event) {
|
|
485
|
+
event.preventDefault();
|
|
486
|
+
const url = endpointUrlInput.value.trim();
|
|
487
|
+
|
|
488
|
+
// Update URL query parameter, catching potential security errors in sandboxed environments
|
|
489
|
+
try {
|
|
490
|
+
const currentUrl = new URL(window.location);
|
|
491
|
+
currentUrl.searchParams.set("endpoint", url);
|
|
492
|
+
window.history.pushState({}, "", currentUrl);
|
|
493
|
+
} catch (e) {
|
|
494
|
+
console.warn("Could not update URL: ", e.message);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
fetchData();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Initializes the application.
|
|
502
|
+
*/
|
|
503
|
+
function init() {
|
|
504
|
+
endpointForm.addEventListener("submit", handleFormSubmit);
|
|
505
|
+
|
|
506
|
+
// Get endpoint from URL param on load
|
|
507
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
508
|
+
const endpointFromUrl = urlParams.get("endpoint");
|
|
509
|
+
|
|
510
|
+
if (endpointFromUrl) {
|
|
511
|
+
endpointUrlInput.value = endpointFromUrl;
|
|
512
|
+
fetchData();
|
|
513
|
+
} else {
|
|
514
|
+
endpointUrlInput.value = DEFAULT_ENDPOINT;
|
|
515
|
+
render(); // Render initial welcome message
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Start the app
|
|
520
|
+
init();
|
|
521
|
+
});
|
|
522
|
+
</script>
|
|
523
|
+
|
|
524
|
+
<footer
|
|
525
|
+
class="text-center py-4 text-xs"
|
|
526
|
+
style="color: var(--color-gray-accent)"
|
|
527
|
+
>
|
|
528
|
+
<p>
|
|
529
|
+
Vibe coded by
|
|
530
|
+
<a
|
|
531
|
+
href="https://gemini.google.com"
|
|
532
|
+
target="_blank"
|
|
533
|
+
rel="noopener noreferrer"
|
|
534
|
+
class="underline transition-colors"
|
|
535
|
+
style="color: var(--color-fg-dark)"
|
|
536
|
+
onmouseover="this.style.color='var(--color-fg-light)'"
|
|
537
|
+
onmouseout="this.style.color='var(--color-fg-dark)'"
|
|
538
|
+
>
|
|
539
|
+
Gemini
|
|
540
|
+
</a>
|
|
541
|
+
- Based on
|
|
542
|
+
<a
|
|
543
|
+
href="https://github.com/uheej0625/copilot-usage-viewer"
|
|
544
|
+
target="_blank"
|
|
545
|
+
rel="noopener noreferrer"
|
|
546
|
+
class="underline transition-colors"
|
|
547
|
+
style="color: var(--color-fg-dark)"
|
|
548
|
+
onmouseover="this.style.color='var(--color-fg-light)'"
|
|
549
|
+
onmouseout="this.style.color='var(--color-fg-dark)'"
|
|
550
|
+
>
|
|
551
|
+
copilot-usage-viewer</a
|
|
552
|
+
>
|
|
553
|
+
</p>
|
|
554
|
+
</footer>
|
|
555
|
+
</body>
|
|
556
|
+
</html>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config-5U36xkbl.js","names":["state: State","state","headers: Record<string, string>","errorJson: unknown","defaultConfig: AppConfig","cachedConfig: AppConfig | null","fs"],"sources":["../src/lib/paths.ts","../src/lib/state.ts","../src/lib/api-config.ts","../src/lib/error.ts","../src/services/copilot/get-models.ts","../src/services/get-vscode-version.ts","../src/lib/utils.ts","../src/services/github/get-copilot-usage.ts","../src/lib/config.ts"],"sourcesContent":["import fs from \"node:fs/promises\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nconst DEFAULT_DIR = path.join(os.homedir(), \".local\", \"share\", \"copilot-api\")\nconst APP_DIR = process.env.COPILOT_API_HOME || DEFAULT_DIR\n\nconst GITHUB_TOKEN_PATH = path.join(APP_DIR, \"github_token\")\nconst CONFIG_PATH = path.join(APP_DIR, \"config.json\")\n\nexport const PATHS = {\n APP_DIR,\n GITHUB_TOKEN_PATH,\n CONFIG_PATH,\n}\n\nexport async function ensurePaths(): Promise<void> {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n await ensureFile(PATHS.GITHUB_TOKEN_PATH)\n await ensureFile(PATHS.CONFIG_PATH)\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath, fs.constants.W_OK)\n } catch {\n await fs.writeFile(filePath, \"\")\n await fs.chmod(filePath, 0o600)\n }\n}\n","import type { ModelsResponse } from \"~/services/copilot/get-models\"\n\nexport interface State {\n githubToken?: string\n copilotToken?: string\n\n accountType: string\n models?: ModelsResponse\n vsCodeVersion?: string\n\n manualApprove: boolean\n rateLimitWait: boolean\n showToken: boolean\n\n // Rate limiting configuration\n rateLimitSeconds?: number\n lastRequestTimestamp?: number\n verbose: boolean\n}\n\nexport const state: State = {\n accountType: \"individual\",\n manualApprove: false,\n rateLimitWait: false,\n showToken: false,\n verbose: false,\n}\n","import { randomUUID } from \"node:crypto\"\n\nimport type { State } from \"./state\"\n\nexport const standardHeaders = () => ({\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n})\n\nconst COPILOT_VERSION = \"0.37.6\"\nconst EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`\nconst USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`\n\nconst API_VERSION = \"2025-10-01\"\n\nexport const copilotBaseUrl = (state: State) =>\n state.accountType === \"individual\" ?\n \"https://api.githubcopilot.com\"\n : `https://api.${state.accountType}.githubcopilot.com`\nexport const copilotHeaders = (state: State, vision: boolean = false) => {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${state.copilotToken}`,\n \"content-type\": standardHeaders()[\"content-type\"],\n \"copilot-integration-id\": \"vscode-chat\",\n \"editor-version\": `vscode/${state.vsCodeVersion}`,\n \"editor-plugin-version\": EDITOR_PLUGIN_VERSION,\n \"user-agent\": USER_AGENT,\n \"openai-intent\": \"conversation-agent\",\n \"x-github-api-version\": API_VERSION,\n \"x-request-id\": randomUUID(),\n \"x-vscode-user-agent-library-version\": \"electron-fetch\",\n }\n\n if (vision) headers[\"copilot-vision-request\"] = \"true\"\n\n return headers\n}\n\nexport const GITHUB_API_BASE_URL = \"https://api.github.com\"\nexport const githubHeaders = (state: State) => ({\n ...standardHeaders(),\n authorization: `token ${state.githubToken}`,\n \"editor-version\": `vscode/${state.vsCodeVersion}`,\n \"editor-plugin-version\": EDITOR_PLUGIN_VERSION,\n \"user-agent\": USER_AGENT,\n \"x-github-api-version\": API_VERSION,\n \"x-vscode-user-agent-library-version\": \"electron-fetch\",\n})\n\nexport const GITHUB_BASE_URL = \"https://github.com\"\nexport const GITHUB_CLIENT_ID = \"Iv1.b507a08c87ecfe98\"\nexport const GITHUB_APP_SCOPES = [\"read:user\"].join(\" \")\n","import type { Context } from \"hono\"\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\"\n\nimport consola from \"consola\"\n\nexport class HTTPError extends Error {\n response: Response\n\n constructor(message: string, response: Response) {\n super(message)\n this.response = response\n }\n}\n\nexport async function forwardError(c: Context, error: unknown) {\n consola.error(\"Error occurred:\", error)\n\n if (error instanceof HTTPError) {\n const errorText = await error.response.text()\n let errorJson: unknown\n try {\n errorJson = JSON.parse(errorText)\n } catch {\n errorJson = errorText\n }\n consola.error(\"HTTP error:\", errorJson)\n return c.json(\n {\n error: {\n message: errorText,\n type: \"error\",\n },\n },\n error.response.status as ContentfulStatusCode,\n )\n }\n\n return c.json(\n {\n error: {\n message: (error as Error).message,\n type: \"error\",\n },\n },\n 500,\n )\n}\n","import { copilotBaseUrl, copilotHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\n\nexport const getModels = async () => {\n const response = await fetch(`${copilotBaseUrl(state)}/models`, {\n headers: copilotHeaders(state),\n })\n\n if (!response.ok) throw new HTTPError(\"Failed to get models\", response)\n\n return (await response.json()) as ModelsResponse\n}\n\nexport interface ModelsResponse {\n data: Array<Model>\n object: string\n}\n\ninterface ModelLimits {\n max_context_window_tokens?: number\n max_output_tokens?: number\n max_prompt_tokens?: number\n max_inputs?: number\n}\n\ninterface ModelSupports {\n max_thinking_budget?: number\n min_thinking_budget?: number\n tool_calls?: boolean\n parallel_tool_calls?: boolean\n dimensions?: boolean\n streaming?: boolean\n structured_outputs?: boolean\n vision?: boolean\n adaptive_thinking?: boolean\n}\n\ninterface ModelCapabilities {\n family: string\n limits: ModelLimits\n object: string\n supports: ModelSupports\n tokenizer: string\n type: string\n}\n\nexport interface Model {\n capabilities: ModelCapabilities\n id: string\n model_picker_enabled: boolean\n name: string\n object: string\n preview: boolean\n vendor: string\n version: string\n policy?: {\n state: string\n terms: string\n }\n supported_endpoints?: Array<string>\n}\n","const FALLBACK = \"1.109.3\"\n\nexport async function getVSCodeVersion() {\n const controller = new AbortController()\n const timeout = setTimeout(() => {\n controller.abort()\n }, 5000)\n\n try {\n const response = await fetch(\n \"https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin\",\n {\n signal: controller.signal,\n },\n )\n\n const pkgbuild = await response.text()\n const pkgverRegex = /pkgver=([0-9.]+)/\n const match = pkgbuild.match(pkgverRegex)\n\n if (match) {\n return match[1]\n }\n\n return FALLBACK\n } catch {\n return FALLBACK\n } finally {\n clearTimeout(timeout)\n }\n}\n\nawait getVSCodeVersion()\n","import consola from \"consola\"\n\nimport { getModels } from \"~/services/copilot/get-models\"\nimport { getVSCodeVersion } from \"~/services/get-vscode-version\"\n\nimport { state } from \"./state\"\n\nexport const sleep = (ms: number) =>\n new Promise((resolve) => {\n setTimeout(resolve, ms)\n })\n\nexport const isNullish = (value: unknown): value is null | undefined =>\n value === null || value === undefined\n\nexport async function cacheModels(): Promise<void> {\n const models = await getModels()\n state.models = models\n}\n\nexport const cacheVSCodeVersion = async () => {\n const response = await getVSCodeVersion()\n state.vsCodeVersion = response\n\n consola.info(`Using VSCode version: ${response}`)\n}\n","import { GITHUB_API_BASE_URL, githubHeaders } from \"~/lib/api-config\"\nimport { HTTPError } from \"~/lib/error\"\nimport { state } from \"~/lib/state\"\n\nexport const getCopilotUsage = async (): Promise<CopilotUsageResponse> => {\n const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, {\n headers: githubHeaders(state),\n })\n\n if (!response.ok) {\n throw new HTTPError(\"Failed to get Copilot usage\", response)\n }\n\n return (await response.json()) as CopilotUsageResponse\n}\n\nexport interface QuotaDetail {\n entitlement: number\n overage_count: number\n overage_permitted: boolean\n percent_remaining: number\n quota_id: string\n quota_remaining: number\n remaining: number\n unlimited: boolean\n}\n\ninterface QuotaSnapshots {\n chat: QuotaDetail\n completions: QuotaDetail\n premium_interactions: QuotaDetail\n}\n\ninterface CopilotUsageResponse {\n access_type_sku: string\n analytics_tracking_id: string\n assigned_date: string\n can_signup_for_limited: boolean\n chat_enabled: boolean\n copilot_plan: string\n organization_login_list: Array<unknown>\n organization_list: Array<unknown>\n quota_reset_date: string\n quota_snapshots: QuotaSnapshots\n}\n","import consola from \"consola\"\nimport fs from \"node:fs\"\n\nimport { PATHS } from \"./paths\"\n\nexport interface AppConfig {\n auth?: {\n apiKeys?: Array<string>\n }\n extraPrompts?: Record<string, string>\n smallModel?: string\n modelReasoningEfforts?: Record<\n string,\n \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"\n >\n useFunctionApplyPatch?: boolean\n compactUseSmallModel?: boolean\n}\n\nconst gpt5ExplorationPrompt = `## Exploration and reading files\n- **Think first.** Before any tool call, decide ALL files/resources you will need.\n- **Batch everything.** If you need multiple files (even from different places), read them together.\n- **multi_tool_use.parallel** Use multi_tool_use.parallel to parallelize tool calls and only this.\n- **Only make sequential calls if you truly cannot know the next file without seeing a result first.**\n- **Workflow:** (a) plan all needed reads → (b) issue one parallel batch → (c) analyze results → (d) repeat if new, unpredictable reads arise.`\n\nconst gpt5CommentaryPrompt = `# Working with the user\n\nYou interact with the user through a terminal. You have 2 ways of communicating with the users: \n- Share intermediary updates in \\`commentary\\` channel. \n- After you have completed all your work, send a message to the \\`final\\` channel. \n\n## Intermediary updates\n\n- Intermediary updates go to the \\`commentary\\` channel.\n- User updates are short updates while you are working, they are NOT final answers.\n- You use 1-2 sentence user updates to communicate progress and new information to the user as you are doing work.\n- Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements (“Done —”, “Got it”, “Great question, ”) or framing phrases.\n- You provide user updates frequently, every 20s.\n- Before exploring or doing substantial work, you start with a user update acknowledging the request and explaining your first step. You should include your understanding of the user request and explain what you will do. Avoid commenting on the request or using starters such as \"Got it -\" or \"Understood -\" etc.\n- When exploring, e.g. searching, reading files, you provide user updates as you go, every 20s, explaining what context you are gathering and what you've learned. Vary your sentence structure when providing these updates to avoid sounding repetitive - in particular, don't start each sentence the same way.\n- After you have sufficient context, and the work is substantial, you provide a longer plan (this is the only user update that may be longer than 2 sentences and can contain formatting).\n- Before performing file edits of any kind, you provide updates explaining what edits you are making.\n- As you are thinking, you very frequently provide updates even if not taking any actions, informing the user of your progress. You interrupt your thinking and send multiple updates in a row if thinking for more than 100 words.\n- Tone of your updates MUST match your personality.`\n\nconst defaultConfig: AppConfig = {\n auth: {\n apiKeys: [],\n },\n extraPrompts: {\n \"gpt-5-mini\": gpt5ExplorationPrompt,\n \"gpt-5.1-codex-max\": gpt5ExplorationPrompt,\n \"gpt-5.3-codex\": gpt5CommentaryPrompt,\n },\n smallModel: \"gpt-5-mini\",\n modelReasoningEfforts: {\n \"gpt-5-mini\": \"low\",\n },\n useFunctionApplyPatch: true,\n compactUseSmallModel: true,\n}\n\nlet cachedConfig: AppConfig | null = null\n\nfunction ensureConfigFile(): void {\n try {\n fs.accessSync(PATHS.CONFIG_PATH, fs.constants.R_OK | fs.constants.W_OK)\n } catch {\n fs.mkdirSync(PATHS.APP_DIR, { recursive: true })\n fs.writeFileSync(\n PATHS.CONFIG_PATH,\n `${JSON.stringify(defaultConfig, null, 2)}\\n`,\n \"utf8\",\n )\n try {\n fs.chmodSync(PATHS.CONFIG_PATH, 0o600)\n } catch {\n return\n }\n }\n}\n\nfunction readConfigFromDisk(): AppConfig {\n ensureConfigFile()\n try {\n const raw = fs.readFileSync(PATHS.CONFIG_PATH, \"utf8\")\n if (!raw.trim()) {\n fs.writeFileSync(\n PATHS.CONFIG_PATH,\n `${JSON.stringify(defaultConfig, null, 2)}\\n`,\n \"utf8\",\n )\n return defaultConfig\n }\n return JSON.parse(raw) as AppConfig\n } catch (error) {\n consola.error(\"Failed to read config file, using default config\", error)\n return defaultConfig\n }\n}\n\nfunction mergeDefaultExtraPrompts(config: AppConfig): {\n mergedConfig: AppConfig\n changed: boolean\n} {\n const extraPrompts = config.extraPrompts ?? {}\n const defaultExtraPrompts = defaultConfig.extraPrompts ?? {}\n\n const missingExtraPromptModels = Object.keys(defaultExtraPrompts).filter(\n (model) => !Object.hasOwn(extraPrompts, model),\n )\n\n if (missingExtraPromptModels.length === 0) {\n return { mergedConfig: config, changed: false }\n }\n\n return {\n mergedConfig: {\n ...config,\n extraPrompts: {\n ...defaultExtraPrompts,\n ...extraPrompts,\n },\n },\n changed: true,\n }\n}\n\nexport function mergeConfigWithDefaults(): AppConfig {\n const config = readConfigFromDisk()\n const { mergedConfig, changed } = mergeDefaultExtraPrompts(config)\n\n if (changed) {\n try {\n fs.writeFileSync(\n PATHS.CONFIG_PATH,\n `${JSON.stringify(mergedConfig, null, 2)}\\n`,\n \"utf8\",\n )\n } catch (writeError) {\n consola.warn(\n \"Failed to write merged extraPrompts to config file\",\n writeError,\n )\n }\n }\n\n cachedConfig = mergedConfig\n return mergedConfig\n}\n\nexport function getConfig(): AppConfig {\n cachedConfig ??= readConfigFromDisk()\n return cachedConfig\n}\n\nexport function getExtraPromptForModel(model: string): string {\n const config = getConfig()\n return config.extraPrompts?.[model] ?? \"\"\n}\n\nexport function getSmallModel(): string {\n const config = getConfig()\n return config.smallModel ?? \"gpt-5-mini\"\n}\n\nexport function getReasoningEffortForModel(\n model: string,\n): \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" {\n const config = getConfig()\n return config.modelReasoningEfforts?.[model] ?? \"high\"\n}\n\nexport function shouldCompactUseSmallModel(): boolean {\n const config = getConfig()\n return config.compactUseSmallModel ?? true\n}\n"],"mappings":"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"}
|