when-does-my-quota-refresh 1.1.0 → 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
@@ -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
 
@@ -990,6 +994,162 @@ var OAUTH_SCOPES = [
990
994
  ].join(" ");
991
995
  var OAUTH_REDIRECT_URI = "http://127.0.0.1:0/callback";
992
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
+ }
1152
+
993
1153
  // src/sources/oauth/client.ts
994
1154
  var OAuthClient = class {
995
1155
  tokens = null;
@@ -1050,6 +1210,14 @@ var OAuthClient = class {
1050
1210
  accessToken: data.access_token,
1051
1211
  expiresAt: Date.now() + data.expires_in * 1e3
1052
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
+ }
1053
1221
  return this.tokens;
1054
1222
  }
1055
1223
  /**
@@ -1095,6 +1263,43 @@ var OAuthClient = class {
1095
1263
  token
1096
1264
  );
1097
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
+ }
1098
1303
  // ── HTTP Helpers ────────────────────────────────────────────────
1099
1304
  authenticatedPost(url, body, token) {
1100
1305
  return new Promise((resolve, reject) => {
@@ -1117,6 +1322,10 @@ var OAuthClient = class {
1117
1322
  responseData += chunk;
1118
1323
  });
1119
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
+ }
1120
1329
  try {
1121
1330
  resolve(JSON.parse(responseData));
1122
1331
  } catch {
@@ -1153,6 +1362,10 @@ var OAuthClient = class {
1153
1362
  responseData += chunk;
1154
1363
  });
1155
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
+ }
1156
1369
  try {
1157
1370
  resolve(JSON.parse(responseData));
1158
1371
  } catch {
@@ -1311,82 +1524,12 @@ var QuotaCache = class {
1311
1524
 
1312
1525
  // src/sdk/store.ts
1313
1526
  import Database from "better-sqlite3";
1314
- import { join as join2 } from "path";
1315
-
1316
- // src/core/config.ts
1317
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1318
- import { join } from "path";
1319
- import { homedir } from "os";
1320
- var CONFIG_DIR_NAME = "when-does-my-quota-refresh";
1321
- var DEFAULT_CONFIG = {
1322
- defaultMode: "local",
1323
- cacheTtlMs: 5 * 60 * 1e3,
1324
- // 5 minutes
1325
- refreshIntervalMs: 5 * 60 * 1e3,
1326
- wakeupModels: ["claude-sonnet-4-5", "gemini-3-flash", "gemini-3-pro-low"],
1327
- wakeupAccounts: [],
1328
- notifications: true,
1329
- plugins: [],
1330
- daemon: {
1331
- pollIntervalMs: 60 * 1e3,
1332
- enabled: false
1333
- }
1334
- };
1335
- function getConfigDir() {
1336
- const home = homedir();
1337
- switch (process.platform) {
1338
- case "darwin":
1339
- return join(home, "Library", "Application Support", CONFIG_DIR_NAME);
1340
- case "win32":
1341
- return join(process.env.APPDATA || join(home, "AppData", "Roaming"), CONFIG_DIR_NAME);
1342
- default:
1343
- return join(home, ".config", CONFIG_DIR_NAME);
1344
- }
1345
- }
1346
- function getConfigPath() {
1347
- return join(getConfigDir(), "config.json");
1348
- }
1349
- function getDataDir() {
1350
- return join(getConfigDir(), "data");
1351
- }
1352
- function ensureDirs() {
1353
- const configDir = getConfigDir();
1354
- const dataDir = getDataDir();
1355
- if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
1356
- if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
1357
- }
1358
- function loadConfig() {
1359
- try {
1360
- const path = getConfigPath();
1361
- if (!existsSync(path)) return DEFAULT_CONFIG;
1362
- const raw = readFileSync(path, "utf-8");
1363
- const parsed = JSON.parse(raw);
1364
- debug("config", `Loaded config from ${path}`);
1365
- return { ...DEFAULT_CONFIG, ...parsed };
1366
- } catch (err) {
1367
- debug("config", "Failed to load config, using defaults", err);
1368
- return DEFAULT_CONFIG;
1369
- }
1370
- }
1371
- function saveConfig(config) {
1372
- ensureDirs();
1373
- const path = getConfigPath();
1374
- writeFileSync(path, JSON.stringify(config, null, 2));
1375
- debug("config", `Saved config to ${path}`);
1376
- }
1377
- function getConfigDirPath() {
1378
- return getConfigDir();
1379
- }
1380
- function getDataDirPath() {
1381
- return getDataDir();
1382
- }
1383
-
1384
- // src/sdk/store.ts
1527
+ import { join as join3 } from "path";
1385
1528
  var db = null;
1386
1529
  function getDb() {
1387
1530
  if (db) return db;
1388
1531
  ensureDirs();
1389
- const dbPath = join2(getDataDirPath(), "history.db");
1532
+ const dbPath = join3(getDataDirPath(), "history.db");
1390
1533
  db = new Database(dbPath);
1391
1534
  db.pragma("journal_mode = WAL");
1392
1535
  initSchema(db);
@@ -1523,87 +1666,6 @@ function closeStore() {
1523
1666
  }
1524
1667
  }
1525
1668
 
1526
- // src/sdk/accounts/storage.ts
1527
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1528
- import { join as join3 } from "path";
1529
- var TOKENS_DIR = "accounts";
1530
- function getStorePath() {
1531
- return join3(getConfigDirPath(), TOKENS_DIR, "tokens.json");
1532
- }
1533
- function loadStore() {
1534
- const path = getStorePath();
1535
- if (!existsSync2(path)) return { accounts: [] };
1536
- try {
1537
- return JSON.parse(readFileSync2(path, "utf-8"));
1538
- } catch {
1539
- return { accounts: [] };
1540
- }
1541
- }
1542
- function saveStore(store) {
1543
- ensureDirs();
1544
- writeFileSync2(getStorePath(), JSON.stringify(store, null, 2));
1545
- debug("account-storage", `Saved ${store.accounts.length} account(s)`);
1546
- }
1547
- function saveAccountTokens(email, tokens) {
1548
- const store = loadStore();
1549
- const existing = store.accounts.findIndex((a) => a.email === email);
1550
- const account = {
1551
- email,
1552
- tokens,
1553
- addedAt: (/* @__PURE__ */ new Date()).toISOString()
1554
- };
1555
- if (existing >= 0) {
1556
- store.accounts[existing] = account;
1557
- } else {
1558
- store.accounts.push(account);
1559
- }
1560
- if (store.accounts.length === 1) {
1561
- store.activeEmail = email;
1562
- }
1563
- saveStore(store);
1564
- }
1565
- function getAccountTokens(email) {
1566
- const store = loadStore();
1567
- const account = store.accounts.find((a) => a.email === email);
1568
- return account?.tokens || null;
1569
- }
1570
- function getActiveAccountTokens() {
1571
- const store = loadStore();
1572
- if (!store.activeEmail) return null;
1573
- const account = store.accounts.find((a) => a.email === store.activeEmail);
1574
- return account?.tokens || null;
1575
- }
1576
- function setActiveAccount(email) {
1577
- const store = loadStore();
1578
- if (!store.accounts.find((a) => a.email === email)) return false;
1579
- store.activeEmail = email;
1580
- saveStore(store);
1581
- return true;
1582
- }
1583
- function removeAccount(email) {
1584
- const store = loadStore();
1585
- const idx = store.accounts.findIndex((a) => a.email === email);
1586
- if (idx < 0) return false;
1587
- store.accounts.splice(idx, 1);
1588
- if (store.activeEmail === email) {
1589
- store.activeEmail = store.accounts[0]?.email;
1590
- }
1591
- saveStore(store);
1592
- return true;
1593
- }
1594
- function listAccounts() {
1595
- const store = loadStore();
1596
- return store.accounts.map((a) => ({
1597
- email: a.email,
1598
- isActive: a.email === store.activeEmail,
1599
- addedAt: a.addedAt
1600
- }));
1601
- }
1602
- function getAllAccountTokens() {
1603
- const store = loadStore();
1604
- return store.accounts.map((a) => a.tokens);
1605
- }
1606
-
1607
1669
  // src/sdk/accounts/auth.ts
1608
1670
  import open from "open";
1609
1671
  import http2 from "http";
@@ -1617,7 +1679,7 @@ async function loginAccount() {
1617
1679
  const url = new URL3(req.url || "/", `http://127.0.0.1`);
1618
1680
  const code2 = url.searchParams.get("code");
1619
1681
  if (code2) {
1620
- res.writeHead(200, { "Content-Type": "text/html" });
1682
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1621
1683
  res.end(`
1622
1684
  <html><body style="font-family: sans-serif; text-align: center; padding: 50px;">
1623
1685
  <h1>\u2705 Login successful!</h1>
@@ -1629,7 +1691,7 @@ async function loginAccount() {
1629
1691
  } else {
1630
1692
  const error = url.searchParams.get("error");
1631
1693
  if (error) {
1632
- res.writeHead(400, { "Content-Type": "text/html" });
1694
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
1633
1695
  res.end(`<h1>Login failed: ${error}</h1>`);
1634
1696
  server.close();
1635
1697
  reject(new AuthError(`OAuth error: ${error}`));
@@ -1660,7 +1722,18 @@ ${authUrl}
1660
1722
  }, 5 * 60 * 1e3);
1661
1723
  });
1662
1724
  const tokens = await client.exchangeCode(code, capturedRedirectUri);
1663
- const email = tokens.email || "unknown@gmail.com";
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
+ }
1664
1737
  tokens.email = email;
1665
1738
  saveAccountTokens(email, tokens);
1666
1739
  setActiveAccount(email);
@@ -1668,17 +1741,160 @@ ${authUrl}
1668
1741
  return email;
1669
1742
  }
1670
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
+
1671
1879
  // src/sdk/quota.ts
1672
1880
  var QuotaClient = class {
1673
1881
  registry;
1674
1882
  cache;
1883
+ plugins;
1675
1884
  verbose;
1676
1885
  constructor(options) {
1677
1886
  this.verbose = options?.verbose || false;
1678
- setVerbose(this.verbose);
1887
+ if (options?.verbose !== void 0) {
1888
+ setVerbose(this.verbose);
1889
+ }
1679
1890
  const config = loadConfig();
1680
1891
  this.registry = new SourceRegistry();
1681
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
+ }
1682
1898
  this.registerOAuthSources();
1683
1899
  }
1684
1900
  /**
@@ -1689,11 +1905,19 @@ var QuotaClient = class {
1689
1905
  return this.fetchAllAccounts(options);
1690
1906
  }
1691
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
+ }
1692
1914
  }
1693
1915
  const snapshot = await this.registry.fetchQuota(options?.source);
1694
- if (snapshot.email) {
1916
+ if (snapshot.email && snapshot.email !== "unknown") {
1695
1917
  this.cache.set(snapshot.email, snapshot.method, snapshot);
1696
1918
  storeSnapshot(snapshot);
1919
+ this.plugins.notifyQuotaUpdate(snapshot).catch(() => {
1920
+ });
1697
1921
  }
1698
1922
  return snapshot;
1699
1923
  }