lobsterboard 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/lobsterboard.css +1 -1
- package/dist/lobsterboard.esm.js +1 -1
- package/dist/lobsterboard.esm.min.js +1 -1
- package/dist/lobsterboard.umd.js +1 -1
- package/dist/lobsterboard.umd.min.js +1 -1
- package/js/widgets.js +67 -52
- package/package.json +1 -1
- package/server.cjs +76 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-03-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Enhanced Gemini CLI integration** — auto-detect all available Gemini CLI quota buckets (including new 3.x models) instead of hardcoded 2.x allowlist — thanks @mastash3ff!
|
|
7
|
+
- **Auto-refresh OAuth tokens** — Gemini CLI tokens now automatically refresh to survive multi-machine rotation, preventing authentication failures — thanks @mastash3ff!
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Future-proof model support** — Gemini CLI collector now automatically surfaces new quota windows when Google adds them
|
|
11
|
+
|
|
3
12
|
## [0.3.1] - 2026-02-28
|
|
4
13
|
|
|
5
14
|
### Fixed
|
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ Track your AI coding subscriptions in real-time. Inspired by [OpenUsage](https:/
|
|
|
169
169
|
| Codex CLI | Session, weekly, code reviews | Run `codex` once |
|
|
170
170
|
| GitHub Copilot | Premium, chat, completions | Run `gh auth login` |
|
|
171
171
|
| Cursor | Credits, usage breakdown | Just use Cursor IDE |
|
|
172
|
-
| Gemini CLI |
|
|
172
|
+
| Gemini CLI | All available Gemini CLI quota buckets | Run `gemini` once |
|
|
173
173
|
| Amp | Free tier, credits | Run `amp` once |
|
|
174
174
|
| Factory / Droid | Standard, premium tokens | Run `factory` once |
|
|
175
175
|
| Kimi Code | Session, weekly | Run `kimi` once |
|
package/dist/lobsterboard.css
CHANGED
package/dist/lobsterboard.esm.js
CHANGED
package/dist/lobsterboard.umd.js
CHANGED
package/js/widgets.js
CHANGED
|
@@ -304,7 +304,7 @@ const WIDGETS = {
|
|
|
304
304
|
name: 'Local Weather',
|
|
305
305
|
icon: '🌡️',
|
|
306
306
|
category: 'small',
|
|
307
|
-
description: 'Shows current weather for a single location using
|
|
307
|
+
description: 'Shows current weather for a single location using Open-Meteo (no API key needed).',
|
|
308
308
|
defaultWidth: 200,
|
|
309
309
|
defaultHeight: 120,
|
|
310
310
|
hasApiKey: false,
|
|
@@ -332,29 +332,35 @@ const WIDGETS = {
|
|
|
332
332
|
</div>
|
|
333
333
|
</div>`,
|
|
334
334
|
generateJs: (props) => `
|
|
335
|
-
// Weather Widget: ${props.id} (uses free
|
|
335
|
+
// Weather Widget: ${props.id} (uses free Open-Meteo API - no key needed)
|
|
336
|
+
const WMO_DESC = {0:'Clear sky',1:'Mainly clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Light drizzle',53:'Drizzle',55:'Dense drizzle',61:'Slight rain',63:'Moderate rain',65:'Heavy rain',71:'Slight snow',73:'Moderate snow',75:'Heavy snow',80:'Slight showers',81:'Moderate showers',82:'Violent showers',95:'Thunderstorm',96:'Hail thunderstorm',99:'Heavy hail'};
|
|
337
|
+
function wmoIcon(code) {
|
|
338
|
+
if (code <= 1) return 'weather-sunny';
|
|
339
|
+
if (code <= 3) return 'weather-cloudy';
|
|
340
|
+
if (code >= 51 && code <= 82) return 'weather-rainy';
|
|
341
|
+
if (code >= 71 && code <= 77) return 'weather-snowy';
|
|
342
|
+
if (code >= 95) return 'weather-rainy';
|
|
343
|
+
return 'weather';
|
|
344
|
+
}
|
|
336
345
|
async function update_${props.id.replace(/-/g, '_')}() {
|
|
337
346
|
const valEl = document.getElementById('${props.id}-value');
|
|
338
347
|
const labelEl = document.getElementById('${props.id}-label');
|
|
339
348
|
const iconEl = document.getElementById('${props.id}-icon');
|
|
340
349
|
try {
|
|
341
|
-
const
|
|
342
|
-
const
|
|
350
|
+
const loc = '${props.location || 'Atlanta'}';
|
|
351
|
+
const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(loc) + '&count=1');
|
|
352
|
+
const geoData = await geoRes.json();
|
|
353
|
+
if (!geoData.results || !geoData.results.length) throw new Error('City not found');
|
|
354
|
+
const {latitude, longitude} = geoData.results[0];
|
|
355
|
+
const tempUnit = '${props.units}' === 'C' ? 'celsius' : 'fahrenheit';
|
|
356
|
+
const res = await fetch('https://api.open-meteo.com/v1/forecast?latitude=' + latitude + '&longitude=' + longitude + '¤t=temperature_2m,weathercode,windspeed_10m&temperature_unit=' + tempUnit);
|
|
343
357
|
const data = await res.json();
|
|
344
|
-
const
|
|
345
|
-
const temp = '${props.units}' === 'C' ? current.temp_C : current.temp_F;
|
|
358
|
+
const c = data.current;
|
|
346
359
|
const unit = '${props.units}' === 'C' ? '°C' : '°F';
|
|
347
|
-
valEl.textContent =
|
|
348
|
-
labelEl.textContent =
|
|
349
|
-
|
|
350
|
-
const code = parseInt(current.weatherCode);
|
|
351
|
-
let iconId = 'weather';
|
|
352
|
-
if (code === 113) iconId = 'weather-sunny';
|
|
353
|
-
else if (code === 116 || code === 119) iconId = 'weather-cloudy';
|
|
354
|
-
else if (code >= 176 && code <= 359) iconId = 'weather-rainy';
|
|
355
|
-
else if (code >= 368 && code <= 395) iconId = 'weather-snowy';
|
|
360
|
+
valEl.textContent = Math.round(c.temperature_2m) + unit;
|
|
361
|
+
labelEl.textContent = WMO_DESC[c.weathercode] || 'Unknown';
|
|
362
|
+
const iconId = wmoIcon(c.weathercode);
|
|
356
363
|
iconEl.setAttribute('data-icon', iconId);
|
|
357
|
-
// Update emoji fallback for non-themed views
|
|
358
364
|
const icons = window.WIDGET_ICONS || {};
|
|
359
365
|
iconEl.textContent = icons[iconId] ? icons[iconId].emoji : '🌡️';
|
|
360
366
|
} catch (e) {
|
|
@@ -399,35 +405,42 @@ const WIDGETS = {
|
|
|
399
405
|
</div>
|
|
400
406
|
</div>`,
|
|
401
407
|
generateJs: (props) => `
|
|
402
|
-
// Multi Weather Widget: ${props.id} (uses free
|
|
408
|
+
// Multi Weather Widget: ${props.id} (uses free Open-Meteo API - no key needed)
|
|
409
|
+
const WMO_DESC2 = {0:'Clear',1:'Clear',2:'Partly cloudy',3:'Overcast',45:'Fog',48:'Rime fog',51:'Drizzle',53:'Drizzle',55:'Drizzle',61:'Rain',63:'Rain',65:'Heavy rain',71:'Snow',73:'Snow',75:'Heavy snow',80:'Showers',81:'Showers',82:'Showers',95:'Storm',96:'Hail',99:'Hail'};
|
|
410
|
+
function wmoIcon2(code) {
|
|
411
|
+
if (code <= 1) return 'weather-sunny';
|
|
412
|
+
if (code <= 3) return 'weather-cloudy';
|
|
413
|
+
if (code >= 51 && code <= 82) return 'weather-rainy';
|
|
414
|
+
if (code >= 71 && code <= 77) return 'weather-snowy';
|
|
415
|
+
if (code >= 95) return 'weather-rainy';
|
|
416
|
+
return 'weather';
|
|
417
|
+
}
|
|
403
418
|
async function update_${props.id.replace(/-/g, '_')}() {
|
|
404
419
|
const locations = '${props.locations || 'New York; London; Tokyo'}'.split(';').map(l => l.trim());
|
|
405
420
|
const container = document.getElementById('${props.id}-list');
|
|
406
|
-
const
|
|
407
|
-
const unitSymbol =
|
|
421
|
+
const tempUnit = '${props.units}' === 'C' ? 'celsius' : 'fahrenheit';
|
|
422
|
+
const unitSymbol = '${props.units}' === 'C' ? '°C' : '°F';
|
|
408
423
|
|
|
409
424
|
const results = await Promise.all(locations.map(async (loc) => {
|
|
410
425
|
try {
|
|
411
|
-
const
|
|
426
|
+
const geoRes = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(loc) + '&count=1');
|
|
427
|
+
const geoData = await geoRes.json();
|
|
428
|
+
if (!geoData.results || !geoData.results.length) return { loc, temp: 'N/A', iconId: 'weather', emoji: '❓' };
|
|
429
|
+
const {latitude, longitude} = geoData.results[0];
|
|
430
|
+
const res = await fetch('https://api.open-meteo.com/v1/forecast?latitude=' + latitude + '&longitude=' + longitude + '¤t=temperature_2m,weathercode&temperature_unit=' + tempUnit);
|
|
412
431
|
const data = await res.json();
|
|
413
|
-
const
|
|
414
|
-
const
|
|
415
|
-
const code = parseInt(current.weatherCode);
|
|
416
|
-
let iconId = 'weather';
|
|
417
|
-
if (code === 113) iconId = 'weather-sunny';
|
|
418
|
-
else if (code === 116 || code === 119) iconId = 'weather-cloudy';
|
|
419
|
-
else if (code >= 176 && code <= 359) iconId = 'weather-rainy';
|
|
420
|
-
else if (code >= 368 && code <= 395) iconId = 'weather-snowy';
|
|
432
|
+
const c = data.current;
|
|
433
|
+
const iconId = wmoIcon2(c.weathercode);
|
|
421
434
|
const icons = window.WIDGET_ICONS || {};
|
|
422
435
|
const emoji = icons[iconId] ? icons[iconId].emoji : '🌡️';
|
|
423
|
-
return { loc, temp, iconId, emoji
|
|
436
|
+
return { loc, temp: Math.round(c.temperature_2m), iconId, emoji };
|
|
424
437
|
} catch (e) {
|
|
425
|
-
return { loc, temp: 'N/A', iconId: 'weather', emoji: '❓'
|
|
438
|
+
return { loc, temp: 'N/A', iconId: 'weather', emoji: '❓' };
|
|
426
439
|
}
|
|
427
440
|
}));
|
|
428
441
|
|
|
429
442
|
container.innerHTML = results.map(r =>
|
|
430
|
-
'<div class="weather-row"><span class="weather-icon lb-icon" data-icon="' + _esc(r.iconId) + '">' + _esc(r.emoji) + '</span><span class="weather-loc">' + _esc(r.loc) + '</span><span class="weather-temp">' + _esc(r.temp) + _esc(unitSymbol) + '</span></div>'
|
|
443
|
+
'<div class="weather-row"><span class="weather-icon lb-icon" data-icon="' + _esc(r.iconId) + '">' + _esc(r.emoji) + '</span><span class="weather-loc">' + _esc(r.loc) + '</span><span class="weather-temp">' + _esc(String(r.temp)) + _esc(unitSymbol) + '</span></div>'
|
|
431
444
|
).join('');
|
|
432
445
|
}
|
|
433
446
|
update_${props.id.replace(/-/g, '_')}();
|
|
@@ -3728,32 +3741,34 @@ const WIDGETS = {
|
|
|
3728
3741
|
</div>
|
|
3729
3742
|
</div>`,
|
|
3730
3743
|
generateJs: (props) => `
|
|
3731
|
-
// World Clock Widget: ${props.id} (
|
|
3744
|
+
// World Clock Widget: ${props.id} (pure Intl.DateTimeFormat - no API needed)
|
|
3745
|
+
const CITY_TZ_MAP = {
|
|
3746
|
+
'New York': 'America/New_York', 'Los Angeles': 'America/Los_Angeles', 'Chicago': 'America/Chicago',
|
|
3747
|
+
'London': 'Europe/London', 'Paris': 'Europe/Paris', 'Berlin': 'Europe/Berlin',
|
|
3748
|
+
'Tokyo': 'Asia/Tokyo', 'Sydney': 'Australia/Sydney', 'Dubai': 'Asia/Dubai',
|
|
3749
|
+
'Singapore': 'Asia/Singapore', 'Hong Kong': 'Asia/Hong_Kong', 'Mumbai': 'Asia/Kolkata',
|
|
3750
|
+
'Shanghai': 'Asia/Shanghai', 'Seoul': 'Asia/Seoul', 'Moscow': 'Europe/Moscow',
|
|
3751
|
+
'Istanbul': 'Europe/Istanbul', 'Bangkok': 'Asia/Bangkok', 'Toronto': 'America/Toronto',
|
|
3752
|
+
'Heidenheim': 'Europe/Berlin', 'Vienna': 'Europe/Vienna', 'Zurich': 'Europe/Zurich',
|
|
3753
|
+
'Amsterdam': 'Europe/Amsterdam', 'Rome': 'Europe/Rome', 'Madrid': 'Europe/Madrid',
|
|
3754
|
+
'São Paulo': 'America/Sao_Paulo', 'Mexico City': 'America/Mexico_City',
|
|
3755
|
+
'Graz': 'Europe/Vienna', 'Munich': 'Europe/Berlin', 'Frankfurt': 'Europe/Berlin',
|
|
3756
|
+
'Santiago': 'America/Santiago', 'Lima': 'America/Lima'
|
|
3757
|
+
};
|
|
3732
3758
|
const locs_${props.id.replace(/-/g, '_')} = '${props.locations || 'New York; London; Tokyo'}'.split(';').map(s => s.trim());
|
|
3733
3759
|
const hour12_${props.id.replace(/-/g, '_')} = ${!props.format24h};
|
|
3734
3760
|
|
|
3735
|
-
|
|
3761
|
+
function update_${props.id.replace(/-/g, '_')}() {
|
|
3736
3762
|
const container = document.getElementById('${props.id}-clocks');
|
|
3737
|
-
const
|
|
3763
|
+
const now = new Date();
|
|
3764
|
+
const results = locs_${props.id.replace(/-/g, '_')}.map(loc => {
|
|
3765
|
+
const tz = CITY_TZ_MAP[loc] || CITY_TZ_MAP[Object.keys(CITY_TZ_MAP).find(k => k.toLowerCase() === loc.toLowerCase())] || null;
|
|
3766
|
+
if (!tz) return { city: loc, time: '(unknown tz)' };
|
|
3738
3767
|
try {
|
|
3739
|
-
const
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
const localTime = data.current_condition[0].localObsDateTime;
|
|
3744
|
-
// Parse the time from format "2026-02-07 12:30 AM"
|
|
3745
|
-
const timePart = localTime.split(' ').slice(1).join(' ');
|
|
3746
|
-
let displayTime = timePart;
|
|
3747
|
-
if (!hour12_${props.id.replace(/-/g, '_')}) {
|
|
3748
|
-
// Convert to 24h if needed
|
|
3749
|
-
const d = new Date('2000-01-01 ' + timePart);
|
|
3750
|
-
displayTime = d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
3751
|
-
}
|
|
3752
|
-
return { city, time: displayTime, ok: true };
|
|
3753
|
-
} catch (e) {
|
|
3754
|
-
return { city: loc, time: '—', ok: false };
|
|
3755
|
-
}
|
|
3756
|
-
}));
|
|
3768
|
+
const fmt = new Intl.DateTimeFormat('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: hour12_${props.id.replace(/-/g, '_')} });
|
|
3769
|
+
return { city: loc, time: fmt.format(now) };
|
|
3770
|
+
} catch(e) { return { city: loc, time: '—' }; }
|
|
3771
|
+
});
|
|
3757
3772
|
container.innerHTML = results.map(r =>
|
|
3758
3773
|
'<div class="tz-row"><span class="tz-city">' + r.city + '</span><span class="tz-time">' + r.time + '</span></div>'
|
|
3759
3774
|
).join('');
|
package/package.json
CHANGED
package/server.cjs
CHANGED
|
@@ -1241,6 +1241,42 @@ async function fetchCursorUsage() {
|
|
|
1241
1241
|
}
|
|
1242
1242
|
}
|
|
1243
1243
|
|
|
1244
|
+
// Refresh Gemini CLI OAuth token.
|
|
1245
|
+
// These client credentials are intentionally public — gemini-cli is an installed
|
|
1246
|
+
// application and Google's own guidance permits embedding them in the source:
|
|
1247
|
+
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/oauth2.ts
|
|
1248
|
+
const GEMINI_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
|
|
1249
|
+
const GEMINI_CLIENT_SECRET = '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';
|
|
1250
|
+
|
|
1251
|
+
async function refreshGeminiToken(credsPath, creds) {
|
|
1252
|
+
if (!creds.refresh_token) return { error: 'No refresh token available.' };
|
|
1253
|
+
try {
|
|
1254
|
+
const resp = await fetch('https://oauth2.googleapis.com/token', {
|
|
1255
|
+
method: 'POST',
|
|
1256
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1257
|
+
body: new URLSearchParams({
|
|
1258
|
+
grant_type: 'refresh_token',
|
|
1259
|
+
refresh_token: creds.refresh_token,
|
|
1260
|
+
client_id: GEMINI_CLIENT_ID,
|
|
1261
|
+
client_secret: GEMINI_CLIENT_SECRET,
|
|
1262
|
+
}),
|
|
1263
|
+
});
|
|
1264
|
+
if (!resp.ok) return { error: 'Refresh failed (HTTP ' + resp.status + ')' };
|
|
1265
|
+
const data = await resp.json();
|
|
1266
|
+
const updated = {
|
|
1267
|
+
...creds,
|
|
1268
|
+
access_token: data.access_token,
|
|
1269
|
+
expiry_date: Date.now() + (data.expires_in * 1000),
|
|
1270
|
+
};
|
|
1271
|
+
if (data.refresh_token) updated.refresh_token = data.refresh_token;
|
|
1272
|
+
if (data.id_token) updated.id_token = data.id_token;
|
|
1273
|
+
try { fs.writeFileSync(credsPath, JSON.stringify(updated, null, 2)); } catch (_) {}
|
|
1274
|
+
return { accessToken: data.access_token, creds: updated };
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
return { error: e.message };
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1244
1280
|
// Fetch Gemini usage
|
|
1245
1281
|
async function fetchGeminiUsage() {
|
|
1246
1282
|
const baseInfo = {
|
|
@@ -1248,34 +1284,53 @@ async function fetchGeminiUsage() {
|
|
|
1248
1284
|
name: AI_PROVIDERS.gemini.name,
|
|
1249
1285
|
icon: AI_PROVIDERS.gemini.icon,
|
|
1250
1286
|
};
|
|
1251
|
-
|
|
1287
|
+
|
|
1252
1288
|
const credsPath = AI_PROVIDERS.gemini.credPaths[0];
|
|
1253
1289
|
if (!fs.existsSync(credsPath)) {
|
|
1254
1290
|
return { ...baseInfo, error: 'Not logged in. Run `gemini auth` first.' };
|
|
1255
1291
|
}
|
|
1256
|
-
|
|
1292
|
+
|
|
1257
1293
|
let creds;
|
|
1258
1294
|
try {
|
|
1259
1295
|
creds = JSON.parse(fs.readFileSync(credsPath, 'utf8'));
|
|
1260
1296
|
} catch (e) {
|
|
1261
1297
|
return { ...baseInfo, error: 'Invalid credentials file.' };
|
|
1262
1298
|
}
|
|
1263
|
-
|
|
1264
|
-
if (!creds.access_token) {
|
|
1299
|
+
|
|
1300
|
+
if (!creds.access_token && !creds.refresh_token) {
|
|
1265
1301
|
return { ...baseInfo, error: 'No access token found.' };
|
|
1266
1302
|
}
|
|
1267
|
-
|
|
1303
|
+
|
|
1304
|
+
// Proactively refresh if the token is expired or expires within 5 minutes.
|
|
1305
|
+
// This handles the case where gemini-cli on another machine has rotated the
|
|
1306
|
+
// shared OAuth token, leaving the stored access_token stale.
|
|
1307
|
+
const FIVE_MINUTES = 5 * 60 * 1000;
|
|
1308
|
+
if (!creds.access_token || (creds.expiry_date && Date.now() >= creds.expiry_date - FIVE_MINUTES)) {
|
|
1309
|
+
const refreshed = await refreshGeminiToken(credsPath, creds);
|
|
1310
|
+
if (refreshed.error) return { ...baseInfo, error: 'Session expired. Run `gemini auth` to re-auth.' };
|
|
1311
|
+
creds = refreshed.creds;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const callQuotaApi = (token) => fetch('https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota', {
|
|
1315
|
+
method: 'POST',
|
|
1316
|
+
headers: {
|
|
1317
|
+
'Authorization': `Bearer ${token}`,
|
|
1318
|
+
'Content-Type': 'application/json',
|
|
1319
|
+
},
|
|
1320
|
+
body: '{}',
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1268
1323
|
try {
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1324
|
+
let resp = await callQuotaApi(creds.access_token);
|
|
1325
|
+
|
|
1326
|
+
// On auth failure, attempt a token refresh and retry once before giving up.
|
|
1327
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
1328
|
+
const refreshed = await refreshGeminiToken(credsPath, creds);
|
|
1329
|
+
if (refreshed.error) return { ...baseInfo, error: 'Session expired. Run `gemini auth` to re-auth.' };
|
|
1330
|
+
creds = refreshed.creds;
|
|
1331
|
+
resp = await callQuotaApi(creds.access_token);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1279
1334
|
if (!resp.ok) {
|
|
1280
1335
|
if (resp.status === 401 || resp.status === 403) {
|
|
1281
1336
|
return { ...baseInfo, error: 'Session expired. Run `gemini auth` to re-auth.' };
|
|
@@ -1289,16 +1344,16 @@ async function fetchGeminiUsage() {
|
|
|
1289
1344
|
// Parse quota buckets (API returns 'buckets' with 'remainingFraction')
|
|
1290
1345
|
const buckets = data.buckets || data.quotaBuckets || [];
|
|
1291
1346
|
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1347
|
+
// Track all non-Vertex Gemini request buckets instead of a hardcoded
|
|
1348
|
+
// 2.x allowlist so new Gemini CLI quota windows surface automatically.
|
|
1294
1349
|
const seen = new Set();
|
|
1295
1350
|
|
|
1296
1351
|
for (const bucket of buckets) {
|
|
1297
1352
|
if (bucket.tokenType !== 'REQUESTS') continue;
|
|
1298
1353
|
// Skip vertex variants
|
|
1299
1354
|
if (bucket.modelId?.includes('_vertex')) continue;
|
|
1300
|
-
// Only show
|
|
1301
|
-
if (!
|
|
1355
|
+
// Only show Gemini model buckets
|
|
1356
|
+
if (!bucket.modelId?.startsWith('gemini-')) continue;
|
|
1302
1357
|
// Dedupe
|
|
1303
1358
|
if (seen.has(bucket.modelId)) continue;
|
|
1304
1359
|
seen.add(bucket.modelId);
|
|
@@ -1313,6 +1368,8 @@ async function fetchGeminiUsage() {
|
|
|
1313
1368
|
});
|
|
1314
1369
|
}
|
|
1315
1370
|
|
|
1371
|
+
metrics.sort((a, b) => String(a.label || '').localeCompare(String(b.label || '')));
|
|
1372
|
+
|
|
1316
1373
|
return {
|
|
1317
1374
|
...baseInfo,
|
|
1318
1375
|
plan: 'Gemini CLI',
|