when-does-my-quota-refresh 1.0.1 → 1.2.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/dist/sdk/index.js CHANGED
@@ -421,7 +421,7 @@ var ConnectClient = class {
421
421
  * Fetch model configs (last-resort fallback)
422
422
  */
423
423
  async fetchModelConfigs() {
424
- debug("connect-client", "Fetching GetCascadeModelConfigData");
424
+ debug("connect-client", "Fetching GetCommandModelConfigs");
425
425
  try {
426
426
  return await this.request("POST", ENDPOINTS.modelConfigs, {
427
427
  metadata: {
@@ -568,11 +568,11 @@ var ConnectClient = class {
568
568
  };
569
569
 
570
570
  // src/sources/local/parser.ts
571
- function parseQuotaSummary(summary, email) {
571
+ function parseQuotaSummary(summary, email, method = "local") {
572
572
  debug("parser", `Parsing quota summary: ${summary.groups.length} groups`);
573
573
  return {
574
574
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
575
- method: "local",
575
+ method,
576
576
  email,
577
577
  groups: summary.groups.map((g) => ({
578
578
  displayName: g.displayName,
@@ -587,11 +587,11 @@ function parseQuotaSummary(summary, email) {
587
587
  }))
588
588
  };
589
589
  }
590
- function parseUserStatus(userStatus) {
590
+ function parseUserStatus(userStatus, method = "local") {
591
591
  debug("parser", "Parsing GetUserStatus fallback");
592
592
  const snapshot = {
593
593
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
594
- method: "local",
594
+ method,
595
595
  email: userStatus.email,
596
596
  groups: []
597
597
  };
@@ -665,7 +665,7 @@ function isGeminiModel(modelId) {
665
665
  }
666
666
  function detectAutocomplete(modelId, label) {
667
667
  const text = `${modelId} ${label || ""}`.toLowerCase();
668
- return text.includes("autocomplete") || text.includes("gemini-2.5") || text.includes("gemini 2.5");
668
+ return text.includes("autocomplete");
669
669
  }
670
670
  function clampFraction(value) {
671
671
  if (value === void 0 || value === null) return 1;
@@ -839,10 +839,10 @@ var CliSource = class {
839
839
  const summary = await client.fetchQuotaSummary();
840
840
  if (summary && summary.groups.length > 0) {
841
841
  const email = (await client.fetchUserStatus())?.email;
842
- return parseQuotaSummary(summary, email);
842
+ return parseQuotaSummary(summary, email, "cli");
843
843
  }
844
844
  const userStatus = await client.fetchUserStatus();
845
- if (userStatus) return parseUserStatus(userStatus);
845
+ if (userStatus) return parseUserStatus(userStatus, "cli");
846
846
  } catch {
847
847
  }
848
848
  }
@@ -901,20 +901,25 @@ var OAuthSource = class {
901
901
  }
902
902
  async fetchQuota() {
903
903
  debug("oauth", "Fetching quota via Cloud Code API");
904
+ let email;
905
+ try {
906
+ const userInfo = await this.client.fetchUserInfo();
907
+ email = userInfo.email;
908
+ } catch {
909
+ debug("oauth", "Could not fetch user info for email");
910
+ }
904
911
  try {
905
912
  const summaryRaw = await this.client.fetchQuotaSummary();
906
913
  const summary = this.parseRemoteSummary(summaryRaw);
907
914
  if (summary && summary.groups.length > 0) {
908
- const statusRaw2 = await this.client.fetchUserStatus();
909
- const email = this.extractEmail(statusRaw2);
910
915
  debug("oauth", "Got quota summary via OAuth");
911
- return parseQuotaSummary(summary, email);
916
+ return parseQuotaSummary(summary, email, "oauth");
912
917
  }
913
918
  const statusRaw = await this.client.fetchUserStatus();
914
919
  const status = this.parseRemoteStatus(statusRaw);
915
920
  if (status) {
916
921
  debug("oauth", "Got user status via OAuth");
917
- return parseUserStatus(status);
922
+ return parseUserStatus(status, "oauth");
918
923
  }
919
924
  } catch (err) {
920
925
  debug("oauth", `Fetch failed: ${err}`);
@@ -948,13 +953,12 @@ var OAuthSource = class {
948
953
  */
949
954
  parseRemoteStatus(raw) {
950
955
  if (typeof raw !== "object" || raw === null) return null;
951
- return { raw };
952
- }
953
- extractEmail(raw) {
954
- if (typeof raw !== "object" || raw === null) return void 0;
955
956
  const data = raw;
956
- const userStatus = data.userStatus;
957
- return userStatus?.email || data.email;
957
+ const userStatus = data.userStatus || data;
958
+ return {
959
+ email: userStatus.email,
960
+ raw
961
+ };
958
962
  }
959
963
  };
960
964
 
@@ -988,6 +992,163 @@ var OAUTH_CLIENT_SECRET = "d-FL95Q19q7MQmFpd7hHD0Ty";
988
992
  var OAUTH_SCOPES = [
989
993
  "https://www.googleapis.com/auth/cloud-platform"
990
994
  ].join(" ");
995
+ var OAUTH_REDIRECT_URI = "http://127.0.0.1:0/callback";
996
+
997
+ // src/sdk/accounts/storage.ts
998
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
999
+ import { join as join2, dirname } from "path";
1000
+
1001
+ // src/core/config.ts
1002
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1003
+ import { join } from "path";
1004
+ import { homedir } from "os";
1005
+ var CONFIG_DIR_NAME = "when-does-my-quota-refresh";
1006
+ var DEFAULT_CONFIG = {
1007
+ defaultMode: "local",
1008
+ cacheTtlMs: 5 * 60 * 1e3,
1009
+ // 5 minutes
1010
+ refreshIntervalMs: 5 * 60 * 1e3,
1011
+ wakeupModels: ["claude-sonnet-4-5", "gemini-3-flash", "gemini-3-pro-low"],
1012
+ wakeupAccounts: [],
1013
+ wakeupMode: "smart",
1014
+ wakeupIntervalHours: 6,
1015
+ wakeupDailyTimes: "9,17",
1016
+ notifications: true,
1017
+ plugins: [],
1018
+ daemon: {
1019
+ pollIntervalMs: 60 * 1e3,
1020
+ enabled: false
1021
+ }
1022
+ };
1023
+ function getConfigDir() {
1024
+ const home = homedir();
1025
+ switch (process.platform) {
1026
+ case "darwin":
1027
+ return join(home, "Library", "Application Support", CONFIG_DIR_NAME);
1028
+ case "win32":
1029
+ return join(process.env.APPDATA || join(home, "AppData", "Roaming"), CONFIG_DIR_NAME);
1030
+ default:
1031
+ return join(home, ".config", CONFIG_DIR_NAME);
1032
+ }
1033
+ }
1034
+ function getConfigPath() {
1035
+ return join(getConfigDir(), "config.json");
1036
+ }
1037
+ function getDataDir() {
1038
+ return join(getConfigDir(), "data");
1039
+ }
1040
+ function ensureDirs() {
1041
+ const configDir = getConfigDir();
1042
+ const dataDir = getDataDir();
1043
+ if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
1044
+ if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
1045
+ }
1046
+ function loadConfig() {
1047
+ try {
1048
+ const path = getConfigPath();
1049
+ if (!existsSync(path)) return DEFAULT_CONFIG;
1050
+ const raw = readFileSync(path, "utf-8");
1051
+ const parsed = JSON.parse(raw);
1052
+ debug("config", `Loaded config from ${path}`);
1053
+ return { ...DEFAULT_CONFIG, ...parsed };
1054
+ } catch (err) {
1055
+ debug("config", "Failed to load config, using defaults", err);
1056
+ return DEFAULT_CONFIG;
1057
+ }
1058
+ }
1059
+ function saveConfig(config) {
1060
+ ensureDirs();
1061
+ const path = getConfigPath();
1062
+ writeFileSync(path, JSON.stringify(config, null, 2));
1063
+ debug("config", `Saved config to ${path}`);
1064
+ }
1065
+ function getConfigDirPath() {
1066
+ return getConfigDir();
1067
+ }
1068
+ function getDataDirPath() {
1069
+ return getDataDir();
1070
+ }
1071
+
1072
+ // src/sdk/accounts/storage.ts
1073
+ var TOKENS_DIR = "accounts";
1074
+ function getStorePath() {
1075
+ return join2(getConfigDirPath(), TOKENS_DIR, "tokens.json");
1076
+ }
1077
+ function loadStore() {
1078
+ const path = getStorePath();
1079
+ if (!existsSync2(path)) return { accounts: [] };
1080
+ try {
1081
+ return JSON.parse(readFileSync2(path, "utf-8"));
1082
+ } catch {
1083
+ return { accounts: [] };
1084
+ }
1085
+ }
1086
+ function saveStore(store) {
1087
+ ensureDirs();
1088
+ const storePath = getStorePath();
1089
+ mkdirSync2(dirname(storePath), { recursive: true });
1090
+ writeFileSync2(storePath, JSON.stringify(store, null, 2));
1091
+ debug("account-storage", `Saved ${store.accounts.length} account(s)`);
1092
+ }
1093
+ function saveAccountTokens(email, tokens) {
1094
+ const store = loadStore();
1095
+ const existing = store.accounts.findIndex((a) => a.email === email);
1096
+ const account = {
1097
+ email,
1098
+ tokens,
1099
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
1100
+ };
1101
+ if (existing >= 0) {
1102
+ store.accounts[existing] = account;
1103
+ } else {
1104
+ store.accounts.push(account);
1105
+ }
1106
+ if (store.accounts.length === 1) {
1107
+ store.activeEmail = email;
1108
+ }
1109
+ saveStore(store);
1110
+ }
1111
+ function getAccountTokens(email) {
1112
+ const store = loadStore();
1113
+ const account = store.accounts.find((a) => a.email === email);
1114
+ return account?.tokens || null;
1115
+ }
1116
+ function getActiveAccountTokens() {
1117
+ const store = loadStore();
1118
+ if (!store.activeEmail) return null;
1119
+ const account = store.accounts.find((a) => a.email === store.activeEmail);
1120
+ return account?.tokens || null;
1121
+ }
1122
+ function setActiveAccount(email) {
1123
+ const store = loadStore();
1124
+ if (!store.accounts.find((a) => a.email === email)) return false;
1125
+ store.activeEmail = email;
1126
+ saveStore(store);
1127
+ return true;
1128
+ }
1129
+ function removeAccount(email) {
1130
+ const store = loadStore();
1131
+ const idx = store.accounts.findIndex((a) => a.email === email);
1132
+ if (idx < 0) return false;
1133
+ store.accounts.splice(idx, 1);
1134
+ if (store.activeEmail === email) {
1135
+ store.activeEmail = store.accounts[0]?.email;
1136
+ }
1137
+ saveStore(store);
1138
+ return true;
1139
+ }
1140
+ function listAccounts() {
1141
+ const store = loadStore();
1142
+ return store.accounts.map((a) => ({
1143
+ email: a.email,
1144
+ isActive: a.email === store.activeEmail,
1145
+ addedAt: a.addedAt
1146
+ }));
1147
+ }
1148
+ function getAllAccountTokens() {
1149
+ const store = loadStore();
1150
+ return store.accounts.map((a) => a.tokens);
1151
+ }
991
1152
 
992
1153
  // src/sources/oauth/client.ts
993
1154
  var OAuthClient = class {
@@ -998,10 +1159,10 @@ var OAuthClient = class {
998
1159
  /**
999
1160
  * Generate the OAuth authorization URL for user login
1000
1161
  */
1001
- getAuthUrl() {
1162
+ getAuthUrl(redirectUri) {
1002
1163
  const params = new URLSearchParams({
1003
1164
  client_id: OAUTH_CLIENT_ID,
1004
- redirect_uri: "http://127.0.0.1:0/callback",
1165
+ redirect_uri: redirectUri || OAUTH_REDIRECT_URI,
1005
1166
  response_type: "code",
1006
1167
  scope: OAUTH_SCOPES,
1007
1168
  access_type: "offline",
@@ -1012,12 +1173,12 @@ var OAuthClient = class {
1012
1173
  /**
1013
1174
  * Exchange an authorization code for tokens
1014
1175
  */
1015
- async exchangeCode(code) {
1176
+ async exchangeCode(code, redirectUri) {
1016
1177
  const body = new URLSearchParams({
1017
1178
  code,
1018
1179
  client_id: OAUTH_CLIENT_ID,
1019
1180
  client_secret: OAUTH_CLIENT_SECRET,
1020
- redirect_uri: "http://127.0.0.1:0/callback",
1181
+ redirect_uri: redirectUri || OAUTH_REDIRECT_URI,
1021
1182
  grant_type: "authorization_code"
1022
1183
  });
1023
1184
  const response = await this.postUrlEncoded("https://oauth2.googleapis.com/token", body);
@@ -1049,6 +1210,14 @@ var OAuthClient = class {
1049
1210
  accessToken: data.access_token,
1050
1211
  expiresAt: Date.now() + data.expires_in * 1e3
1051
1212
  };
1213
+ if (this.tokens.email) {
1214
+ try {
1215
+ saveAccountTokens(this.tokens.email, this.tokens);
1216
+ debug("oauth", `Persisted refreshed token for ${this.tokens.email}`);
1217
+ } catch (err) {
1218
+ debug("oauth", `Failed to persist refreshed token: ${err}`);
1219
+ }
1220
+ }
1052
1221
  return this.tokens;
1053
1222
  }
1054
1223
  /**
@@ -1094,6 +1263,43 @@ var OAuthClient = class {
1094
1263
  token
1095
1264
  );
1096
1265
  }
1266
+ /**
1267
+ * Fetch the authenticated user's profile (email, name, etc.)
1268
+ */
1269
+ async fetchUserInfo() {
1270
+ const token = await this.getAccessToken();
1271
+ return new Promise((resolve, reject) => {
1272
+ const options = {
1273
+ hostname: "www.googleapis.com",
1274
+ path: "/oauth2/v3/userinfo",
1275
+ method: "GET",
1276
+ headers: {
1277
+ "Authorization": `Bearer ${token}`
1278
+ },
1279
+ timeout: 1e4
1280
+ };
1281
+ const req = https2.request(options, (res) => {
1282
+ let data = "";
1283
+ res.on("data", (chunk) => {
1284
+ data += chunk;
1285
+ });
1286
+ res.on("end", () => {
1287
+ try {
1288
+ const info = JSON.parse(data);
1289
+ resolve({ email: info.email, name: info.name });
1290
+ } catch {
1291
+ resolve({});
1292
+ }
1293
+ });
1294
+ });
1295
+ req.on("error", () => resolve({}));
1296
+ req.on("timeout", () => {
1297
+ req.destroy();
1298
+ resolve({});
1299
+ });
1300
+ req.end();
1301
+ });
1302
+ }
1097
1303
  // ── HTTP Helpers ────────────────────────────────────────────────
1098
1304
  authenticatedPost(url, body, token) {
1099
1305
  return new Promise((resolve, reject) => {
@@ -1116,6 +1322,10 @@ var OAuthClient = class {
1116
1322
  responseData += chunk;
1117
1323
  });
1118
1324
  res.on("end", () => {
1325
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
1326
+ reject(new Error(`HTTP ${res.statusCode}: ${responseData.slice(0, 200)}`));
1327
+ return;
1328
+ }
1119
1329
  try {
1120
1330
  resolve(JSON.parse(responseData));
1121
1331
  } catch {
@@ -1152,6 +1362,10 @@ var OAuthClient = class {
1152
1362
  responseData += chunk;
1153
1363
  });
1154
1364
  res.on("end", () => {
1365
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
1366
+ reject(new Error(`HTTP ${res.statusCode}: ${responseData.slice(0, 200)}`));
1367
+ return;
1368
+ }
1155
1369
  try {
1156
1370
  resolve(JSON.parse(responseData));
1157
1371
  } catch {
@@ -1310,82 +1524,12 @@ var QuotaCache = class {
1310
1524
 
1311
1525
  // src/sdk/store.ts
1312
1526
  import Database from "better-sqlite3";
1313
- import { join as join2 } from "path";
1314
-
1315
- // src/core/config.ts
1316
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1317
- import { join } from "path";
1318
- import { homedir } from "os";
1319
- var CONFIG_DIR_NAME = "when-does-my-quota-refresh";
1320
- var DEFAULT_CONFIG = {
1321
- defaultMode: "local",
1322
- cacheTtlMs: 5 * 60 * 1e3,
1323
- // 5 minutes
1324
- refreshIntervalMs: 5 * 60 * 1e3,
1325
- wakeupModels: ["claude-sonnet-4-5", "gemini-3-flash", "gemini-3-pro-low"],
1326
- wakeupAccounts: [],
1327
- notifications: true,
1328
- plugins: [],
1329
- daemon: {
1330
- pollIntervalMs: 60 * 1e3,
1331
- enabled: false
1332
- }
1333
- };
1334
- function getConfigDir() {
1335
- const home = homedir();
1336
- switch (process.platform) {
1337
- case "darwin":
1338
- return join(home, "Library", "Application Support", CONFIG_DIR_NAME);
1339
- case "win32":
1340
- return join(process.env.APPDATA || join(home, "AppData", "Roaming"), CONFIG_DIR_NAME);
1341
- default:
1342
- return join(home, ".config", CONFIG_DIR_NAME);
1343
- }
1344
- }
1345
- function getConfigPath() {
1346
- return join(getConfigDir(), "config.json");
1347
- }
1348
- function getDataDir() {
1349
- return join(getConfigDir(), "data");
1350
- }
1351
- function ensureDirs() {
1352
- const configDir = getConfigDir();
1353
- const dataDir = getDataDir();
1354
- if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
1355
- if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
1356
- }
1357
- function loadConfig() {
1358
- try {
1359
- const path = getConfigPath();
1360
- if (!existsSync(path)) return DEFAULT_CONFIG;
1361
- const raw = readFileSync(path, "utf-8");
1362
- const parsed = JSON.parse(raw);
1363
- debug("config", `Loaded config from ${path}`);
1364
- return { ...DEFAULT_CONFIG, ...parsed };
1365
- } catch (err) {
1366
- debug("config", "Failed to load config, using defaults", err);
1367
- return DEFAULT_CONFIG;
1368
- }
1369
- }
1370
- function saveConfig(config) {
1371
- ensureDirs();
1372
- const path = getConfigPath();
1373
- writeFileSync(path, JSON.stringify(config, null, 2));
1374
- debug("config", `Saved config to ${path}`);
1375
- }
1376
- function getConfigDirPath() {
1377
- return getConfigDir();
1378
- }
1379
- function getDataDirPath() {
1380
- return getDataDir();
1381
- }
1382
-
1383
- // src/sdk/store.ts
1527
+ import { join as join3 } from "path";
1384
1528
  var db = null;
1385
1529
  function getDb() {
1386
1530
  if (db) return db;
1387
1531
  ensureDirs();
1388
- const dbPath = join2(getDataDirPath(), "history.db");
1532
+ const dbPath = join3(getDataDirPath(), "history.db");
1389
1533
  db = new Database(dbPath);
1390
1534
  db.pragma("journal_mode = WAL");
1391
1535
  initSchema(db);
@@ -1522,101 +1666,20 @@ function closeStore() {
1522
1666
  }
1523
1667
  }
1524
1668
 
1525
- // src/sdk/accounts/storage.ts
1526
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1527
- import { join as join3 } from "path";
1528
- var TOKENS_DIR = "accounts";
1529
- function getStorePath() {
1530
- return join3(getConfigDirPath(), TOKENS_DIR, "tokens.json");
1531
- }
1532
- function loadStore() {
1533
- const path = getStorePath();
1534
- if (!existsSync2(path)) return { accounts: [] };
1535
- try {
1536
- return JSON.parse(readFileSync2(path, "utf-8"));
1537
- } catch {
1538
- return { accounts: [] };
1539
- }
1540
- }
1541
- function saveStore(store) {
1542
- ensureDirs();
1543
- writeFileSync2(getStorePath(), JSON.stringify(store, null, 2));
1544
- debug("account-storage", `Saved ${store.accounts.length} account(s)`);
1545
- }
1546
- function saveAccountTokens(email, tokens) {
1547
- const store = loadStore();
1548
- const existing = store.accounts.findIndex((a) => a.email === email);
1549
- const account = {
1550
- email,
1551
- tokens,
1552
- addedAt: (/* @__PURE__ */ new Date()).toISOString()
1553
- };
1554
- if (existing >= 0) {
1555
- store.accounts[existing] = account;
1556
- } else {
1557
- store.accounts.push(account);
1558
- }
1559
- if (store.accounts.length === 1) {
1560
- store.activeEmail = email;
1561
- }
1562
- saveStore(store);
1563
- }
1564
- function getAccountTokens(email) {
1565
- const store = loadStore();
1566
- const account = store.accounts.find((a) => a.email === email);
1567
- return account?.tokens || null;
1568
- }
1569
- function getActiveAccountTokens() {
1570
- const store = loadStore();
1571
- if (!store.activeEmail) return null;
1572
- const account = store.accounts.find((a) => a.email === store.activeEmail);
1573
- return account?.tokens || null;
1574
- }
1575
- function setActiveAccount(email) {
1576
- const store = loadStore();
1577
- if (!store.accounts.find((a) => a.email === email)) return false;
1578
- store.activeEmail = email;
1579
- saveStore(store);
1580
- return true;
1581
- }
1582
- function removeAccount(email) {
1583
- const store = loadStore();
1584
- const idx = store.accounts.findIndex((a) => a.email === email);
1585
- if (idx < 0) return false;
1586
- store.accounts.splice(idx, 1);
1587
- if (store.activeEmail === email) {
1588
- store.activeEmail = store.accounts[0]?.email;
1589
- }
1590
- saveStore(store);
1591
- return true;
1592
- }
1593
- function listAccounts() {
1594
- const store = loadStore();
1595
- return store.accounts.map((a) => ({
1596
- email: a.email,
1597
- isActive: a.email === store.activeEmail,
1598
- addedAt: a.addedAt
1599
- }));
1600
- }
1601
- function getAllAccountTokens() {
1602
- const store = loadStore();
1603
- return store.accounts.map((a) => a.tokens);
1604
- }
1605
-
1606
1669
  // src/sdk/accounts/auth.ts
1607
1670
  import open from "open";
1608
1671
  import http2 from "http";
1609
1672
  import { URL as URL3 } from "url";
1610
1673
  async function loginAccount() {
1611
1674
  const client = new OAuthClient();
1612
- const authUrl = client.getAuthUrl();
1613
1675
  debug("auth", "Starting OAuth login flow");
1676
+ let capturedRedirectUri = "http://127.0.0.1:0/callback";
1614
1677
  const code = await new Promise((resolve, reject) => {
1615
1678
  const server = http2.createServer(async (req, res) => {
1616
1679
  const url = new URL3(req.url || "/", `http://127.0.0.1`);
1617
1680
  const code2 = url.searchParams.get("code");
1618
1681
  if (code2) {
1619
- res.writeHead(200, { "Content-Type": "text/html" });
1682
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1620
1683
  res.end(`
1621
1684
  <html><body style="font-family: sans-serif; text-align: center; padding: 50px;">
1622
1685
  <h1>\u2705 Login successful!</h1>
@@ -1628,7 +1691,7 @@ async function loginAccount() {
1628
1691
  } else {
1629
1692
  const error = url.searchParams.get("error");
1630
1693
  if (error) {
1631
- res.writeHead(400, { "Content-Type": "text/html" });
1694
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
1632
1695
  res.end(`<h1>Login failed: ${error}</h1>`);
1633
1696
  server.close();
1634
1697
  reject(new AuthError(`OAuth error: ${error}`));
@@ -1641,7 +1704,9 @@ async function loginAccount() {
1641
1704
  server.listen(0, "127.0.0.1", () => {
1642
1705
  const addr = server.address();
1643
1706
  if (typeof addr === "object" && addr) {
1707
+ capturedRedirectUri = `http://127.0.0.1:${addr.port}/callback`;
1644
1708
  debug("auth", `Callback server on port ${addr.port}`);
1709
+ const authUrl = client.getAuthUrl(capturedRedirectUri);
1645
1710
  open(authUrl).catch(() => {
1646
1711
  console.log(`
1647
1712
  Open this URL in your browser:
@@ -1656,8 +1721,19 @@ ${authUrl}
1656
1721
  reject(new AuthError("Login timed out after 5 minutes"));
1657
1722
  }, 5 * 60 * 1e3);
1658
1723
  });
1659
- const tokens = await client.exchangeCode(code);
1660
- const email = tokens.email || "unknown@gmail.com";
1724
+ const tokens = await client.exchangeCode(code, capturedRedirectUri);
1725
+ let email = tokens.email;
1726
+ if (!email) {
1727
+ try {
1728
+ const userInfo = await client.fetchUserInfo();
1729
+ email = userInfo.email;
1730
+ } catch (err) {
1731
+ debug("auth", `Failed to fetch user info: ${err}`);
1732
+ }
1733
+ }
1734
+ if (!email) {
1735
+ throw new AuthError("Could not determine user email from Google account");
1736
+ }
1661
1737
  tokens.email = email;
1662
1738
  saveAccountTokens(email, tokens);
1663
1739
  setActiveAccount(email);
@@ -1665,17 +1741,160 @@ ${authUrl}
1665
1741
  return email;
1666
1742
  }
1667
1743
 
1744
+ // src/plugins/builtin/notifications.ts
1745
+ var NotificationPlugin = class {
1746
+ name = "notifications";
1747
+ version = "1.0.0";
1748
+ notifier = null;
1749
+ notifiedLow = /* @__PURE__ */ new Set();
1750
+ async onInit(ctx) {
1751
+ try {
1752
+ const { default: notifier } = await import("node-notifier");
1753
+ this.notifier = notifier;
1754
+ debug("notifications", "Notification plugin initialized");
1755
+ } catch {
1756
+ debug("notifications", "node-notifier not available, notifications disabled");
1757
+ }
1758
+ }
1759
+ async onQuotaUpdate(snapshot, ctx) {
1760
+ for (const group of snapshot.groups) {
1761
+ for (const bucket of group.buckets) {
1762
+ if (bucket.remainingFraction > 0.8) {
1763
+ this.notifiedLow.delete(`${group.displayName}:${bucket.displayName}`);
1764
+ }
1765
+ }
1766
+ }
1767
+ }
1768
+ async onQuotaLow(group, bucket, remaining, ctx) {
1769
+ const key = `${group}:${bucket}`;
1770
+ if (this.notifiedLow.has(key)) return;
1771
+ const pct = Math.round(remaining * 100);
1772
+ const title = "when-does-my-quota-refresh";
1773
+ const message = `\u26A0\uFE0F Low quota: ${group} \u2014 ${bucket} at ${pct}%`;
1774
+ ctx.log(message);
1775
+ this.notify(title, message);
1776
+ this.notifiedLow.add(key);
1777
+ }
1778
+ async onReset(group, nextReset, ctx) {
1779
+ const title = "when-does-my-quota-refresh";
1780
+ const message = `\u2705 ${group} quota has reset!`;
1781
+ ctx.log(message);
1782
+ this.notify(title, message);
1783
+ }
1784
+ notify(title, message) {
1785
+ if (!this.notifier) return;
1786
+ try {
1787
+ this.notifier.notify({ title, message, timeout: 5 });
1788
+ } catch (err) {
1789
+ debug("notifications", `Notification failed: ${err}`);
1790
+ }
1791
+ }
1792
+ };
1793
+
1794
+ // src/plugins/index.ts
1795
+ var PluginRegistry = class {
1796
+ plugins = [];
1797
+ context;
1798
+ constructor(context) {
1799
+ this.context = {
1800
+ notify: context?.notify || (() => {
1801
+ }),
1802
+ log: context?.log || ((msg) => console.log(msg))
1803
+ };
1804
+ }
1805
+ /**
1806
+ * Register a plugin
1807
+ */
1808
+ async register(plugin) {
1809
+ this.plugins.push(plugin);
1810
+ if (plugin.onInit) {
1811
+ await plugin.onInit(this.context);
1812
+ }
1813
+ debug("plugins", `Registered: ${plugin.name}@${plugin.version}`);
1814
+ }
1815
+ /**
1816
+ * Load built-in plugins
1817
+ */
1818
+ async loadBuiltins() {
1819
+ await this.register(new NotificationPlugin());
1820
+ }
1821
+ /**
1822
+ * Notify all plugins of a quota update
1823
+ */
1824
+ async notifyQuotaUpdate(snapshot) {
1825
+ for (const plugin of this.plugins) {
1826
+ if (plugin.onQuotaUpdate) {
1827
+ try {
1828
+ await plugin.onQuotaUpdate(snapshot, this.context);
1829
+ } catch (err) {
1830
+ debug("plugins", `Error in ${plugin.name}.onQuotaUpdate: ${err}`);
1831
+ }
1832
+ }
1833
+ }
1834
+ for (const group of snapshot.groups) {
1835
+ for (const bucket of group.buckets) {
1836
+ if (bucket.remainingFraction < 0.15) {
1837
+ await this.notifyQuotaLow(group.displayName, bucket.displayName, bucket.remainingFraction);
1838
+ }
1839
+ }
1840
+ }
1841
+ }
1842
+ /**
1843
+ * Notify all plugins of low quota
1844
+ */
1845
+ async notifyQuotaLow(group, bucket, remaining) {
1846
+ for (const plugin of this.plugins) {
1847
+ if (plugin.onQuotaLow) {
1848
+ try {
1849
+ await plugin.onQuotaLow(group, bucket, remaining, this.context);
1850
+ } catch (err) {
1851
+ debug("plugins", `Error in ${plugin.name}.onQuotaLow: ${err}`);
1852
+ }
1853
+ }
1854
+ }
1855
+ }
1856
+ /**
1857
+ * Destroy all plugins
1858
+ */
1859
+ async destroy() {
1860
+ for (const plugin of this.plugins) {
1861
+ if (plugin.onDestroy) {
1862
+ try {
1863
+ await plugin.onDestroy();
1864
+ } catch (err) {
1865
+ debug("plugins", `Error in ${plugin.name}.onDestroy: ${err}`);
1866
+ }
1867
+ }
1868
+ }
1869
+ this.plugins = [];
1870
+ }
1871
+ /**
1872
+ * Get registered plugin names
1873
+ */
1874
+ getNames() {
1875
+ return this.plugins.map((p) => p.name);
1876
+ }
1877
+ };
1878
+
1668
1879
  // src/sdk/quota.ts
1669
1880
  var QuotaClient = class {
1670
1881
  registry;
1671
1882
  cache;
1883
+ plugins;
1672
1884
  verbose;
1673
1885
  constructor(options) {
1674
1886
  this.verbose = options?.verbose || false;
1675
- setVerbose(this.verbose);
1887
+ if (options?.verbose !== void 0) {
1888
+ setVerbose(this.verbose);
1889
+ }
1676
1890
  const config = loadConfig();
1677
1891
  this.registry = new SourceRegistry();
1678
1892
  this.cache = new QuotaCache(options?.cacheTtlMs || config.cacheTtlMs);
1893
+ this.plugins = new PluginRegistry();
1894
+ if (config.notifications) {
1895
+ this.plugins.loadBuiltins().catch(() => {
1896
+ });
1897
+ }
1679
1898
  this.registerOAuthSources();
1680
1899
  }
1681
1900
  /**
@@ -1686,11 +1905,19 @@ var QuotaClient = class {
1686
1905
  return this.fetchAllAccounts(options);
1687
1906
  }
1688
1907
  if (!options?.refresh) {
1908
+ const tokens = getAllAccountTokens();
1909
+ for (const token of tokens) {
1910
+ if (!token.email) continue;
1911
+ const cached = this.cache.get(token.email, options?.source || "auto");
1912
+ if (cached) return cached;
1913
+ }
1689
1914
  }
1690
1915
  const snapshot = await this.registry.fetchQuota(options?.source);
1691
- if (snapshot.email) {
1916
+ if (snapshot.email && snapshot.email !== "unknown") {
1692
1917
  this.cache.set(snapshot.email, snapshot.method, snapshot);
1693
1918
  storeSnapshot(snapshot);
1919
+ this.plugins.notifyQuotaUpdate(snapshot).catch(() => {
1920
+ });
1694
1921
  }
1695
1922
  return snapshot;
1696
1923
  }