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/accounts-FH6AKZT7.js +27 -0
- package/dist/{auth-XYJBJA2Q.js → auth-GX4ZIG5O.js} +2 -2
- package/dist/auth-GX4ZIG5O.js.map +1 -0
- package/dist/chunk-H7BEO7IF.js +2 -0
- package/dist/chunk-H7BEO7IF.js.map +1 -0
- package/dist/{chunk-ST6OOHQI.js → chunk-VLP22VAR.js} +254 -169
- package/dist/chunk-VLP22VAR.js.map +1 -0
- package/dist/cli/index.js +218 -28
- package/dist/cli/index.js.map +1 -1
- package/dist/sdk/index.d.ts +4 -0
- package/dist/sdk/index.js +399 -175
- package/dist/sdk/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-ST6OOHQI.js.map +0 -1
- /package/dist/{auth-XYJBJA2Q.js.map → accounts-FH6AKZT7.js.map} +0 -0
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
|
|
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
|
|
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")
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|