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/accounts-FH6AKZT7.js +27 -0
- package/dist/{auth-K5ETZYGY.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-24ILGZJ2.js → chunk-VLP22VAR.js} +263 -175
- package/dist/chunk-VLP22VAR.js.map +1 -0
- package/dist/cli/index.js +219 -29
- package/dist/cli/index.js.map +1 -1
- package/dist/sdk/index.d.ts +16 -5
- package/dist/sdk/index.js +409 -182
- package/dist/sdk/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-24ILGZJ2.js.map +0 -1
- /package/dist/{auth-K5ETZYGY.js.map → accounts-FH6AKZT7.js.map} +0 -0
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
|
|
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
|
|
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
|
|
|
@@ -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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|