just-usage 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -9
- package/README.md +5 -1
- package/dist/cli.js +1271 -101
- package/package.json +5 -3
package/dist/cli.js
CHANGED
|
@@ -15,8 +15,8 @@ import { mkdirSync } from "node:fs";
|
|
|
15
15
|
// package.json
|
|
16
16
|
var package_default = {
|
|
17
17
|
name: "just-usage",
|
|
18
|
-
version: "0.0.
|
|
19
|
-
description: "One local page for your coding-CLI subscription quotas: Claude, Codex, Cursor, OpenCode Go.",
|
|
18
|
+
version: "0.0.5",
|
|
19
|
+
description: "One local page for your coding-CLI subscription quotas: Claude, Codex, Cursor, Antigravity, Grok, OpenCode Go.",
|
|
20
20
|
type: "module",
|
|
21
21
|
license: "MIT",
|
|
22
22
|
author: "spheceo",
|
|
@@ -31,6 +31,8 @@ var package_default = {
|
|
|
31
31
|
"codex",
|
|
32
32
|
"cursor",
|
|
33
33
|
"opencode",
|
|
34
|
+
"antigravity",
|
|
35
|
+
"grok",
|
|
34
36
|
"quota",
|
|
35
37
|
"usage",
|
|
36
38
|
"rate-limit",
|
|
@@ -49,7 +51,7 @@ var package_default = {
|
|
|
49
51
|
node: ">=20"
|
|
50
52
|
},
|
|
51
53
|
scripts: {
|
|
52
|
-
dev: "bun
|
|
54
|
+
dev: "bun run src/cli.ts",
|
|
53
55
|
build: "bun run scripts/build.ts",
|
|
54
56
|
test: "bun test",
|
|
55
57
|
typecheck: "tsc --noEmit",
|
|
@@ -86,7 +88,9 @@ var paths = {
|
|
|
86
88
|
registry: () => join(configDir(), "accounts.json"),
|
|
87
89
|
secrets: () => join(configDir(), "secrets.json"),
|
|
88
90
|
updateCache: () => join(configDir(), "update-check.json"),
|
|
89
|
-
profiles: (provider) => join(configDir(), "profiles", provider)
|
|
91
|
+
profiles: (provider) => join(configDir(), "profiles", provider),
|
|
92
|
+
runDir: () => join(configDir(), "run"),
|
|
93
|
+
runRecord: (port) => join(configDir(), "run", `${port}.json`)
|
|
90
94
|
};
|
|
91
95
|
|
|
92
96
|
// src/format.ts
|
|
@@ -828,24 +832,379 @@ async function verifyClaudeToken(token) {
|
|
|
828
832
|
// src/accounts.ts
|
|
829
833
|
import { renameSync, rmSync as rmSync2 } from "node:fs";
|
|
830
834
|
|
|
831
|
-
// src/adapters/
|
|
835
|
+
// src/adapters/antigravity.ts
|
|
836
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
832
837
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
833
838
|
import { homedir as homedir3 } from "node:os";
|
|
834
839
|
import { join as join3 } from "node:path";
|
|
840
|
+
var AGY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
|
841
|
+
var AGY_CLIENT_SECRET = "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";
|
|
842
|
+
var AGY_REDIRECT_URI = "https://antigravity.google/oauth-callback";
|
|
843
|
+
var AGY_SCOPES = [
|
|
844
|
+
"openid",
|
|
845
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
846
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
847
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
848
|
+
"https://www.googleapis.com/auth/cclog",
|
|
849
|
+
"https://www.googleapis.com/auth/experimentsandconfigs"
|
|
850
|
+
];
|
|
851
|
+
var USAGE_HOST = "https://daily-cloudcode-pa.googleapis.com";
|
|
852
|
+
var KEYCHAIN_SERVICE = "gemini";
|
|
853
|
+
var KEYCHAIN_ACCOUNT = "antigravity";
|
|
854
|
+
var FALLBACK_CLI_VERSION2 = "1.1.26";
|
|
855
|
+
function settingsFile() {
|
|
856
|
+
return join3(homedir3(), ".gemini", "antigravity-cli", "settings.json");
|
|
857
|
+
}
|
|
858
|
+
function usesGeminiApiKey() {
|
|
859
|
+
try {
|
|
860
|
+
if (!existsSync3(settingsFile()))
|
|
861
|
+
return false;
|
|
862
|
+
const parsed = JSON.parse(readFileSync3(settingsFile(), "utf8"));
|
|
863
|
+
return parsed.modelProvider === "gemini" && Boolean(process.env.GEMINI_API_KEY?.trim());
|
|
864
|
+
} catch {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function parseAgyKeyringBlob(raw) {
|
|
869
|
+
const text = raw.replace(/\r?\n$/, "");
|
|
870
|
+
const prefix = "go-keyring-base64:";
|
|
871
|
+
const json = text.startsWith(prefix) ? Buffer.from(text.slice(prefix.length), "base64").toString("utf8") : text;
|
|
872
|
+
try {
|
|
873
|
+
const parsed = JSON.parse(json);
|
|
874
|
+
const token = isObject(parsed.token) ? parsed.token : parsed;
|
|
875
|
+
if (typeof token.access_token !== "string" || !token.access_token)
|
|
876
|
+
return null;
|
|
877
|
+
if (typeof token.refresh_token !== "string" || !token.refresh_token)
|
|
878
|
+
return null;
|
|
879
|
+
const expiry = typeof token.expiry === "string" ? Date.parse(token.expiry) : typeof token.expiry === "number" ? token.expiry : NaN;
|
|
880
|
+
return {
|
|
881
|
+
accessToken: token.access_token,
|
|
882
|
+
refreshToken: token.refresh_token,
|
|
883
|
+
expiry: Number.isFinite(expiry) ? expiry : null
|
|
884
|
+
};
|
|
885
|
+
} catch {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function parseStoredAgySecret(raw) {
|
|
890
|
+
try {
|
|
891
|
+
const parsed = JSON.parse(raw);
|
|
892
|
+
if (typeof parsed.refresh_token !== "string" || !parsed.refresh_token)
|
|
893
|
+
return parseAgyKeyringBlob(raw);
|
|
894
|
+
return {
|
|
895
|
+
accessToken: typeof parsed.access_token === "string" ? parsed.access_token : "",
|
|
896
|
+
refreshToken: parsed.refresh_token,
|
|
897
|
+
expiry: typeof parsed.expiry === "number" ? parsed.expiry : typeof parsed.expiry === "string" ? Date.parse(parsed.expiry) || null : null
|
|
898
|
+
};
|
|
899
|
+
} catch {
|
|
900
|
+
return parseAgyKeyringBlob(raw);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function serializeAgySecret(token) {
|
|
904
|
+
return JSON.stringify({
|
|
905
|
+
access_token: token.accessToken,
|
|
906
|
+
refresh_token: token.refreshToken,
|
|
907
|
+
expiry: token.expiry
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
async function tokenFromKeychain() {
|
|
911
|
+
if (process.platform !== "darwin")
|
|
912
|
+
return null;
|
|
913
|
+
const res = await run("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"], { timeoutMs: 20000 });
|
|
914
|
+
if (res.code !== 0)
|
|
915
|
+
return null;
|
|
916
|
+
return parseAgyKeyringBlob(res.stdout);
|
|
917
|
+
}
|
|
918
|
+
async function tokenFromSecretTool() {
|
|
919
|
+
if (process.platform === "darwin" || process.platform === "win32")
|
|
920
|
+
return null;
|
|
921
|
+
const res = await run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE, "username", KEYCHAIN_ACCOUNT], { timeoutMs: 20000 });
|
|
922
|
+
if (res.code !== 0)
|
|
923
|
+
return null;
|
|
924
|
+
return parseAgyKeyringBlob(res.stdout);
|
|
925
|
+
}
|
|
926
|
+
async function readDefaultAgyToken() {
|
|
927
|
+
return await tokenFromKeychain() ?? await tokenFromSecretTool();
|
|
928
|
+
}
|
|
929
|
+
function createPkce() {
|
|
930
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
931
|
+
const challenge = createHash2("sha256").update(verifier).digest("base64url");
|
|
932
|
+
return { verifier, challenge };
|
|
933
|
+
}
|
|
934
|
+
function antigravityAuthUrl(opts) {
|
|
935
|
+
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
|
|
936
|
+
url.searchParams.set("client_id", AGY_CLIENT_ID);
|
|
937
|
+
url.searchParams.set("redirect_uri", AGY_REDIRECT_URI);
|
|
938
|
+
url.searchParams.set("response_type", "code");
|
|
939
|
+
url.searchParams.set("scope", AGY_SCOPES.join(" "));
|
|
940
|
+
url.searchParams.set("access_type", "offline");
|
|
941
|
+
url.searchParams.set("prompt", "consent");
|
|
942
|
+
url.searchParams.set("state", opts.state);
|
|
943
|
+
url.searchParams.set("code_challenge", opts.challenge);
|
|
944
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
945
|
+
return url.toString();
|
|
946
|
+
}
|
|
947
|
+
function isAntigravityCallbackUrl(raw) {
|
|
948
|
+
let u;
|
|
949
|
+
try {
|
|
950
|
+
u = new URL(raw.trim());
|
|
951
|
+
} catch {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
return u.protocol === "https:" && u.hostname === "antigravity.google" && u.pathname === "/oauth-callback" && Boolean(u.searchParams.get("code"));
|
|
955
|
+
}
|
|
956
|
+
function parseAntigravityAuthInput(raw) {
|
|
957
|
+
const text = raw.trim().replace(/^["']|["']$/g, "");
|
|
958
|
+
if (!text)
|
|
959
|
+
return null;
|
|
960
|
+
if (isAntigravityCallbackUrl(text)) {
|
|
961
|
+
const u = new URL(text);
|
|
962
|
+
return { code: u.searchParams.get("code"), state: u.searchParams.get("state") };
|
|
963
|
+
}
|
|
964
|
+
if (/^https?:\/\//i.test(text))
|
|
965
|
+
return null;
|
|
966
|
+
if (/^4\/[A-Za-z0-9_\-/]+$/.test(text) && text.length >= 20)
|
|
967
|
+
return { code: text, state: null };
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
async function userAgent2() {
|
|
971
|
+
const v = await binVersion("agy") ?? FALLBACK_CLI_VERSION2;
|
|
972
|
+
return `antigravity-cli/${v}`;
|
|
973
|
+
}
|
|
974
|
+
function headers2(token, ua) {
|
|
975
|
+
return {
|
|
976
|
+
Authorization: `Bearer ${token}`,
|
|
977
|
+
"Content-Type": "application/json",
|
|
978
|
+
Accept: "application/json",
|
|
979
|
+
"User-Agent": ua
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
async function refreshAgyToken(refreshToken) {
|
|
983
|
+
const res = await fetchJson("https://oauth2.googleapis.com/token", {
|
|
984
|
+
method: "POST",
|
|
985
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
986
|
+
body: new URLSearchParams({
|
|
987
|
+
client_id: AGY_CLIENT_ID,
|
|
988
|
+
client_secret: AGY_CLIENT_SECRET,
|
|
989
|
+
refresh_token: refreshToken,
|
|
990
|
+
grant_type: "refresh_token"
|
|
991
|
+
}).toString()
|
|
992
|
+
});
|
|
993
|
+
if (res.status !== 200 || !isObject(res.body) || typeof res.body.access_token !== "string")
|
|
994
|
+
return null;
|
|
995
|
+
const expiresIn = typeof res.body.expires_in === "number" ? res.body.expires_in : 3600;
|
|
996
|
+
return {
|
|
997
|
+
accessToken: res.body.access_token,
|
|
998
|
+
refreshToken: typeof res.body.refresh_token === "string" && res.body.refresh_token ? res.body.refresh_token : refreshToken,
|
|
999
|
+
expiry: Date.now() + expiresIn * 1000
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
async function exchangeAntigravityCode(code, verifier) {
|
|
1003
|
+
const res = await fetchJson("https://oauth2.googleapis.com/token", {
|
|
1004
|
+
method: "POST",
|
|
1005
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1006
|
+
body: new URLSearchParams({
|
|
1007
|
+
client_id: AGY_CLIENT_ID,
|
|
1008
|
+
client_secret: AGY_CLIENT_SECRET,
|
|
1009
|
+
code,
|
|
1010
|
+
code_verifier: verifier,
|
|
1011
|
+
grant_type: "authorization_code",
|
|
1012
|
+
redirect_uri: AGY_REDIRECT_URI
|
|
1013
|
+
}).toString()
|
|
1014
|
+
});
|
|
1015
|
+
if (res.status !== 200 || !isObject(res.body) || typeof res.body.access_token !== "string" || typeof res.body.refresh_token !== "string") {
|
|
1016
|
+
throw new Error(isObject(res.body) && typeof res.body.error_description === "string" ? res.body.error_description : `Token exchange failed (HTTP ${res.status}).`);
|
|
1017
|
+
}
|
|
1018
|
+
const expiresIn = typeof res.body.expires_in === "number" ? res.body.expires_in : 3600;
|
|
1019
|
+
return {
|
|
1020
|
+
accessToken: res.body.access_token,
|
|
1021
|
+
refreshToken: res.body.refresh_token,
|
|
1022
|
+
expiry: Date.now() + expiresIn * 1000
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
async function fetchAgyEmail(accessToken) {
|
|
1026
|
+
const res = await fetchJson("https://www.googleapis.com/oauth2/v2/userinfo", {
|
|
1027
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
1028
|
+
});
|
|
1029
|
+
if (res.status !== 200 || !isObject(res.body) || typeof res.body.email !== "string")
|
|
1030
|
+
return null;
|
|
1031
|
+
return res.body.email;
|
|
1032
|
+
}
|
|
1033
|
+
var GOOGLE_AI_PLANS = {
|
|
1034
|
+
"g1-plus-tier": "Plus",
|
|
1035
|
+
"g1-pro-tier": "Pro",
|
|
1036
|
+
"g1-ultra-tier": "Ultra"
|
|
1037
|
+
};
|
|
1038
|
+
var PRODUCT_TIER_NAMES = /^(antigravity|gemini code assist)$/i;
|
|
1039
|
+
function planFromTier(tier) {
|
|
1040
|
+
if (!tier)
|
|
1041
|
+
return null;
|
|
1042
|
+
const id = typeof tier.id === "string" ? tier.id.trim().toLowerCase() : "";
|
|
1043
|
+
if (id && GOOGLE_AI_PLANS[id])
|
|
1044
|
+
return GOOGLE_AI_PLANS[id];
|
|
1045
|
+
if (id.includes("ultra"))
|
|
1046
|
+
return "Ultra";
|
|
1047
|
+
if (id.includes("pro"))
|
|
1048
|
+
return "Pro";
|
|
1049
|
+
if (id.includes("plus"))
|
|
1050
|
+
return "Plus";
|
|
1051
|
+
const name = typeof tier.name === "string" ? tier.name.trim() : "";
|
|
1052
|
+
if (name && !PRODUCT_TIER_NAMES.test(name))
|
|
1053
|
+
return name;
|
|
1054
|
+
if (id === "free-tier")
|
|
1055
|
+
return "Free";
|
|
1056
|
+
if (id === "standard-tier")
|
|
1057
|
+
return "Standard";
|
|
1058
|
+
if (id === "legacy-tier")
|
|
1059
|
+
return "Legacy";
|
|
1060
|
+
return name || null;
|
|
1061
|
+
}
|
|
1062
|
+
function planFromCodeAssist(body) {
|
|
1063
|
+
if (!isObject(body))
|
|
1064
|
+
return null;
|
|
1065
|
+
const current = isObject(body.currentTier) ? body.currentTier : null;
|
|
1066
|
+
const paid = isObject(body.paidTier) ? body.paidTier : null;
|
|
1067
|
+
return planFromTier(paid) ?? planFromTier(current);
|
|
1068
|
+
}
|
|
1069
|
+
function windowMinutes(window) {
|
|
1070
|
+
const key = (window ?? "").toLowerCase().replace(/[_-]/g, "");
|
|
1071
|
+
if (key === "5h" || key === "fivehour" || key === "fivehours")
|
|
1072
|
+
return 300;
|
|
1073
|
+
if (key === "weekly" || key === "week")
|
|
1074
|
+
return 10080;
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
function usageLabel(minutes, fallback) {
|
|
1078
|
+
if (minutes === 300)
|
|
1079
|
+
return "5h Usage";
|
|
1080
|
+
if (minutes === 10080)
|
|
1081
|
+
return "Weekly Usage";
|
|
1082
|
+
return fallback.replace(/\s+remaining$/i, "").trim() || "Usage";
|
|
1083
|
+
}
|
|
1084
|
+
function normalizeAntigravityQuota(body) {
|
|
1085
|
+
if (!isObject(body) || !Array.isArray(body.groups))
|
|
1086
|
+
return [];
|
|
1087
|
+
const out = [];
|
|
1088
|
+
for (const group of body.groups) {
|
|
1089
|
+
if (!isObject(group) || !Array.isArray(group.buckets))
|
|
1090
|
+
continue;
|
|
1091
|
+
const rawGroup = typeof group.displayName === "string" && group.displayName.trim() ? group.displayName.trim() : undefined;
|
|
1092
|
+
const groupName = rawGroup && !/^gemini models$/i.test(rawGroup) ? rawGroup : undefined;
|
|
1093
|
+
for (const bucket of group.buckets) {
|
|
1094
|
+
if (!isObject(bucket) || bucket.disabled === true)
|
|
1095
|
+
continue;
|
|
1096
|
+
const remaining = typeof bucket.remainingFraction === "number" ? bucket.remainingFraction : null;
|
|
1097
|
+
const used = remaining === null ? null : clampPercent((1 - remaining) * 100);
|
|
1098
|
+
if (used === null)
|
|
1099
|
+
continue;
|
|
1100
|
+
const id = typeof bucket.bucketId === "string" && bucket.bucketId ? bucket.bucketId : `${groupName ?? "quota"}:${out.length}`;
|
|
1101
|
+
const minutes = windowMinutes(typeof bucket.window === "string" ? bucket.window : undefined);
|
|
1102
|
+
const fallback = typeof bucket.displayName === "string" ? bucket.displayName : "Usage";
|
|
1103
|
+
out.push({
|
|
1104
|
+
id,
|
|
1105
|
+
label: usageLabel(minutes, fallback),
|
|
1106
|
+
group: groupName,
|
|
1107
|
+
usedPercent: used,
|
|
1108
|
+
resetsAt: isoOrNull(bucket.resetTime),
|
|
1109
|
+
windowMinutes: minutes,
|
|
1110
|
+
kind: "rolling"
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return out;
|
|
1115
|
+
}
|
|
1116
|
+
async function resolveToken2(account) {
|
|
1117
|
+
if (account.kind === "token") {
|
|
1118
|
+
const raw = await secretStore().get(account.id);
|
|
1119
|
+
if (!raw)
|
|
1120
|
+
return { fail: snapshot(account, "error", { message: "Stored Google session missing. Remove the account and sign in again." }) };
|
|
1121
|
+
const parsed = parseStoredAgySecret(raw);
|
|
1122
|
+
if (!parsed)
|
|
1123
|
+
return { fail: snapshot(account, "error", { message: "Stored Google session is unreadable. Remove the account and sign in again." }) };
|
|
1124
|
+
return { token: parsed };
|
|
1125
|
+
}
|
|
1126
|
+
const token = await readDefaultAgyToken();
|
|
1127
|
+
if (!token) {
|
|
1128
|
+
if (usesGeminiApiKey()) {
|
|
1129
|
+
return { fail: snapshot(account, "unsupported", { message: "This CLI is using a Gemini API key, which has no subscription quota." }) };
|
|
1130
|
+
}
|
|
1131
|
+
return { fail: snapshot(account, "signed_out", { message: "Not signed in. Run `agy` and complete Google sign-in." }) };
|
|
1132
|
+
}
|
|
1133
|
+
return { token };
|
|
1134
|
+
}
|
|
1135
|
+
async function liveToken(account, token) {
|
|
1136
|
+
if (token.expiry && token.expiry > Date.now() + 60000 && token.accessToken)
|
|
1137
|
+
return token;
|
|
1138
|
+
const refreshed = await refreshAgyToken(token.refreshToken);
|
|
1139
|
+
if (!refreshed) {
|
|
1140
|
+
return { fail: snapshot(account, "error", { message: "Google session expired. Sign in again with `agy`." }) };
|
|
1141
|
+
}
|
|
1142
|
+
if (account.kind === "token") {
|
|
1143
|
+
await secretStore().set(account.id, serializeAgySecret(refreshed));
|
|
1144
|
+
}
|
|
1145
|
+
return refreshed;
|
|
1146
|
+
}
|
|
1147
|
+
async function fetchAntigravity(account) {
|
|
1148
|
+
try {
|
|
1149
|
+
const resolved = await resolveToken2(account);
|
|
1150
|
+
if ("fail" in resolved)
|
|
1151
|
+
return resolved.fail;
|
|
1152
|
+
const live = await liveToken(account, resolved.token);
|
|
1153
|
+
if ("fail" in live)
|
|
1154
|
+
return live.fail;
|
|
1155
|
+
const ua = await userAgent2();
|
|
1156
|
+
const h = headers2(live.accessToken, ua);
|
|
1157
|
+
const [usage, assist, email] = await Promise.all([
|
|
1158
|
+
fetchJson(`${USAGE_HOST}/v1internal:retrieveUserQuotaSummary`, { method: "POST", headers: h, body: "{}" }),
|
|
1159
|
+
fetchJson(`${USAGE_HOST}/v1internal:loadCodeAssist`, {
|
|
1160
|
+
method: "POST",
|
|
1161
|
+
headers: h,
|
|
1162
|
+
body: JSON.stringify({ metadata: { ideType: "ANTIGRAVITY" } })
|
|
1163
|
+
}),
|
|
1164
|
+
account.email ? Promise.resolve(account.email) : fetchAgyEmail(live.accessToken)
|
|
1165
|
+
]);
|
|
1166
|
+
const plan = assist.status === 200 ? planFromCodeAssist(assist.body) : null;
|
|
1167
|
+
if (usage.status === 401) {
|
|
1168
|
+
return snapshot(account, "error", { email, plan, message: "Google session rejected (401). Sign in again." });
|
|
1169
|
+
}
|
|
1170
|
+
if (usage.status === 403) {
|
|
1171
|
+
return snapshot(account, "unsupported", {
|
|
1172
|
+
email,
|
|
1173
|
+
plan,
|
|
1174
|
+
message: "This Google account has no Antigravity quota. A Google AI Pro / Antigravity subscription is required."
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
if (usage.status !== 200) {
|
|
1178
|
+
return snapshot(account, "error", { email, plan, message: `Usage endpoint returned HTTP ${usage.status}.` });
|
|
1179
|
+
}
|
|
1180
|
+
const windows = normalizeAntigravityQuota(usage.body);
|
|
1181
|
+
if (windows.length === 0) {
|
|
1182
|
+
return snapshot(account, "unsupported", { email, plan, message: "Usage response had no recognizable windows (schema may have changed)." });
|
|
1183
|
+
}
|
|
1184
|
+
return snapshot(account, "ok", { email, plan, windows });
|
|
1185
|
+
} catch (e) {
|
|
1186
|
+
return snapshot(account, "error", { message: errorMessage(e) });
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/adapters/opencode.ts
|
|
1191
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
1192
|
+
import { homedir as homedir4 } from "node:os";
|
|
1193
|
+
import { join as join4 } from "node:path";
|
|
835
1194
|
var USAGE_URL2 = "https://opencode.ai/zen/go/v1/usage";
|
|
836
1195
|
function openCodeAuthFile() {
|
|
837
1196
|
if (process.env.OPENCODE_AUTH_FILE)
|
|
838
1197
|
return process.env.OPENCODE_AUTH_FILE;
|
|
839
1198
|
const xdg = process.env.XDG_DATA_HOME;
|
|
840
|
-
const base = xdg && xdg.trim() ? xdg :
|
|
841
|
-
return
|
|
1199
|
+
const base = xdg && xdg.trim() ? xdg : join4(homedir4(), ".local", "share");
|
|
1200
|
+
return join4(base, "opencode", "auth.json");
|
|
842
1201
|
}
|
|
843
1202
|
function readOpenCodeGoKey() {
|
|
844
1203
|
const file = openCodeAuthFile();
|
|
845
|
-
if (!
|
|
1204
|
+
if (!existsSync4(file))
|
|
846
1205
|
return null;
|
|
847
1206
|
try {
|
|
848
|
-
const parsed = JSON.parse(
|
|
1207
|
+
const parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
849
1208
|
const entry = parsed["opencode-go"];
|
|
850
1209
|
if (isObject(entry) && typeof entry.key === "string" && entry.key)
|
|
851
1210
|
return entry.key;
|
|
@@ -905,14 +1264,14 @@ async function fetchOpenCode(account) {
|
|
|
905
1264
|
}
|
|
906
1265
|
|
|
907
1266
|
// src/registry.ts
|
|
908
|
-
import { existsSync as
|
|
909
|
-
import { dirname as dirname2, join as
|
|
1267
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2, rmSync } from "node:fs";
|
|
1268
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
910
1269
|
function readRegistry() {
|
|
911
1270
|
const file = paths.registry();
|
|
912
|
-
if (!
|
|
1271
|
+
if (!existsSync5(file))
|
|
913
1272
|
return { version: 1, accounts: [] };
|
|
914
1273
|
try {
|
|
915
|
-
const parsed = JSON.parse(
|
|
1274
|
+
const parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
916
1275
|
return { version: 1, accounts: Array.isArray(parsed.accounts) ? parsed.accounts : [] };
|
|
917
1276
|
} catch {
|
|
918
1277
|
return { version: 1, accounts: [] };
|
|
@@ -945,7 +1304,7 @@ function newAccountId(provider, hint) {
|
|
|
945
1304
|
}
|
|
946
1305
|
}
|
|
947
1306
|
function profileDirFor(provider, id) {
|
|
948
|
-
return
|
|
1307
|
+
return join5(paths.profiles(provider), id.split(":")[1] ?? "account");
|
|
949
1308
|
}
|
|
950
1309
|
function saveAccount(record) {
|
|
951
1310
|
const reg = readRegistry();
|
|
@@ -1176,50 +1535,139 @@ async function submitCodexCallback(sessionId, url) {
|
|
|
1176
1535
|
return;
|
|
1177
1536
|
await submitLocalCallback(url);
|
|
1178
1537
|
}
|
|
1538
|
+
var agySessions = new Map;
|
|
1539
|
+
function dropAgySession(id) {
|
|
1540
|
+
const rec = agySessions.get(id);
|
|
1541
|
+
if (!rec)
|
|
1542
|
+
return;
|
|
1543
|
+
clearTimeout(rec.timer);
|
|
1544
|
+
agySessions.delete(id);
|
|
1545
|
+
}
|
|
1546
|
+
function startAgySession(opts) {
|
|
1547
|
+
const pkce = createPkce();
|
|
1548
|
+
const state = crypto.randomUUID();
|
|
1549
|
+
const id = crypto.randomUUID();
|
|
1550
|
+
const authUrl = antigravityAuthUrl({ state, challenge: pkce.challenge });
|
|
1551
|
+
const rec = {
|
|
1552
|
+
id,
|
|
1553
|
+
label: opts.label?.trim() || undefined,
|
|
1554
|
+
accountId: opts.accountId,
|
|
1555
|
+
state,
|
|
1556
|
+
verifier: pkce.verifier,
|
|
1557
|
+
authUrl,
|
|
1558
|
+
status: "waiting",
|
|
1559
|
+
timer: setTimeout(() => dropAgySession(id), SESSION_TTL_MS)
|
|
1560
|
+
};
|
|
1561
|
+
rec.timer.unref?.();
|
|
1562
|
+
agySessions.set(id, rec);
|
|
1563
|
+
return { sessionId: id, authUrl };
|
|
1564
|
+
}
|
|
1565
|
+
async function beginAntigravityAdd(label) {
|
|
1566
|
+
if (!await which("agy"))
|
|
1567
|
+
throw new AccountError("agy is not installed (https://antigravity.google/docs/cli/install).");
|
|
1568
|
+
return startAgySession({ label });
|
|
1569
|
+
}
|
|
1570
|
+
async function beginAntigravityRelogin(accountId) {
|
|
1571
|
+
const existing = getAccount(accountId);
|
|
1572
|
+
if (!existing)
|
|
1573
|
+
throw new AccountError(`Unknown account: ${accountId}`, 404);
|
|
1574
|
+
if (existing.provider !== "antigravity" || existing.kind !== "token") {
|
|
1575
|
+
throw new AccountError(`${accountId} cannot be re-authenticated this way.`);
|
|
1576
|
+
}
|
|
1577
|
+
return startAgySession({ accountId });
|
|
1578
|
+
}
|
|
1579
|
+
function antigravitySessionStatus(sessionId) {
|
|
1580
|
+
const rec = agySessions.get(sessionId);
|
|
1581
|
+
if (!rec)
|
|
1582
|
+
return { status: "error", error: "Login session expired." };
|
|
1583
|
+
return { status: rec.status, authUrl: rec.authUrl, error: rec.error, account: rec.account };
|
|
1584
|
+
}
|
|
1585
|
+
async function submitAntigravityCallback(sessionId, raw) {
|
|
1586
|
+
const parsed = parseAntigravityAuthInput(raw);
|
|
1587
|
+
if (!parsed) {
|
|
1588
|
+
throw new AccountError("Paste the code from the Antigravity page, or the antigravity.google/oauth-callback URL.");
|
|
1589
|
+
}
|
|
1590
|
+
const rec = agySessions.get(sessionId);
|
|
1591
|
+
if (!rec)
|
|
1592
|
+
throw new AccountError("Login session expired.", 404);
|
|
1593
|
+
if (rec.status === "done" && rec.account)
|
|
1594
|
+
return rec.account;
|
|
1595
|
+
const { code, state } = parsed;
|
|
1596
|
+
if (state && state !== rec.state)
|
|
1597
|
+
throw new AccountError("Sign-in state did not match. Start again.");
|
|
1598
|
+
try {
|
|
1599
|
+
const token = await exchangeAntigravityCode(code, rec.verifier);
|
|
1600
|
+
const email = await fetchAgyEmail(token.accessToken);
|
|
1601
|
+
const named = rec.label?.trim() || "";
|
|
1602
|
+
if (rec.accountId) {
|
|
1603
|
+
await secretStore().set(rec.accountId, serializeAgySecret(token));
|
|
1604
|
+
updateAccount(rec.accountId, { email });
|
|
1605
|
+
rec.account = getAccount(rec.accountId);
|
|
1606
|
+
} else {
|
|
1607
|
+
const id = newAccountId("antigravity", named || email || "account");
|
|
1608
|
+
await secretStore().set(id, serializeAgySecret(token));
|
|
1609
|
+
rec.account = {
|
|
1610
|
+
id,
|
|
1611
|
+
provider: "antigravity",
|
|
1612
|
+
label: named,
|
|
1613
|
+
kind: "token",
|
|
1614
|
+
email,
|
|
1615
|
+
createdAt: new Date().toISOString()
|
|
1616
|
+
};
|
|
1617
|
+
saveAccount(rec.account);
|
|
1618
|
+
}
|
|
1619
|
+
rec.status = "done";
|
|
1620
|
+
return rec.account;
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
rec.status = "error";
|
|
1623
|
+
rec.error = e instanceof Error ? e.message : String(e);
|
|
1624
|
+
throw new AccountError(rec.error);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1179
1627
|
|
|
1180
1628
|
// src/collect.ts
|
|
1181
1629
|
import { hostname } from "node:os";
|
|
1182
1630
|
|
|
1183
1631
|
// src/adapters/cursor.ts
|
|
1184
|
-
import { existsSync as
|
|
1185
|
-
import { homedir as
|
|
1186
|
-
import { join as
|
|
1632
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
|
|
1633
|
+
import { homedir as homedir5 } from "node:os";
|
|
1634
|
+
import { join as join6 } from "node:path";
|
|
1187
1635
|
var USAGE_URL3 = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage";
|
|
1188
1636
|
var GROK_BOT_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetSandUsageStatus";
|
|
1189
1637
|
var PLAN_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetPlanInfo";
|
|
1190
|
-
var
|
|
1191
|
-
var
|
|
1638
|
+
var KEYCHAIN_ACCOUNT2 = "cursor-user";
|
|
1639
|
+
var KEYCHAIN_SERVICE2 = "cursor-access-token";
|
|
1192
1640
|
function tokenFromAuthJson(parsed) {
|
|
1193
1641
|
if (!isObject(parsed))
|
|
1194
1642
|
return null;
|
|
1195
1643
|
return typeof parsed.accessToken === "string" && parsed.accessToken ? parsed.accessToken : null;
|
|
1196
1644
|
}
|
|
1197
1645
|
function tokenFromAuthFile(file) {
|
|
1198
|
-
if (!
|
|
1646
|
+
if (!existsSync6(file))
|
|
1199
1647
|
return null;
|
|
1200
1648
|
try {
|
|
1201
|
-
return tokenFromAuthJson(JSON.parse(
|
|
1649
|
+
return tokenFromAuthJson(JSON.parse(readFileSync6(file, "utf8")));
|
|
1202
1650
|
} catch {
|
|
1203
1651
|
return null;
|
|
1204
1652
|
}
|
|
1205
1653
|
}
|
|
1206
1654
|
function authFiles() {
|
|
1207
|
-
const home =
|
|
1655
|
+
const home = homedir5();
|
|
1208
1656
|
const out = [];
|
|
1209
1657
|
if (process.platform === "win32") {
|
|
1210
|
-
const roaming = process.env.APPDATA ||
|
|
1211
|
-
out.push(
|
|
1658
|
+
const roaming = process.env.APPDATA || join6(home, "AppData", "Roaming");
|
|
1659
|
+
out.push(join6(roaming, "Cursor", "auth.json"));
|
|
1212
1660
|
} else if (process.platform !== "darwin") {
|
|
1213
|
-
const xdg = process.env.XDG_CONFIG_HOME ||
|
|
1214
|
-
out.push(
|
|
1661
|
+
const xdg = process.env.XDG_CONFIG_HOME || join6(home, ".config");
|
|
1662
|
+
out.push(join6(xdg, "cursor", "auth.json"));
|
|
1215
1663
|
}
|
|
1216
|
-
out.push(
|
|
1664
|
+
out.push(join6(home, ".cursor", "auth.json"));
|
|
1217
1665
|
return out;
|
|
1218
1666
|
}
|
|
1219
|
-
async function
|
|
1667
|
+
async function tokenFromKeychain2() {
|
|
1220
1668
|
if (process.platform !== "darwin")
|
|
1221
1669
|
return null;
|
|
1222
|
-
const res = await run("security", ["find-generic-password", "-a",
|
|
1670
|
+
const res = await run("security", ["find-generic-password", "-a", KEYCHAIN_ACCOUNT2, "-s", KEYCHAIN_SERVICE2, "-w"], { timeoutMs: 20000 });
|
|
1223
1671
|
if (res.code !== 0)
|
|
1224
1672
|
return null;
|
|
1225
1673
|
const v = res.stdout.replace(/\r?\n$/, "");
|
|
@@ -1234,7 +1682,7 @@ async function readCursorAccessToken() {
|
|
|
1234
1682
|
if (fromFile)
|
|
1235
1683
|
return fromFile;
|
|
1236
1684
|
}
|
|
1237
|
-
const fromKeychain = await
|
|
1685
|
+
const fromKeychain = await tokenFromKeychain2();
|
|
1238
1686
|
if (fromKeychain)
|
|
1239
1687
|
return fromKeychain;
|
|
1240
1688
|
if (process.env.CURSOR_AUTH_FILE)
|
|
@@ -1392,6 +1840,236 @@ async function fetchCursor(account) {
|
|
|
1392
1840
|
}
|
|
1393
1841
|
}
|
|
1394
1842
|
|
|
1843
|
+
// src/adapters/grok.ts
|
|
1844
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
1845
|
+
import { homedir as homedir6 } from "node:os";
|
|
1846
|
+
import { join as join7 } from "node:path";
|
|
1847
|
+
var DEFAULT_PROXY = "https://cli-chat-proxy.grok.com/v1";
|
|
1848
|
+
var TOKEN_URL = "https://auth.x.ai/oauth2/token";
|
|
1849
|
+
var FALLBACK_CLI_VERSION3 = "1.0.13";
|
|
1850
|
+
var TIER_NAMES = {
|
|
1851
|
+
supergrok: "SuperGrok",
|
|
1852
|
+
supergrok_lite: "SuperGrok Lite",
|
|
1853
|
+
supergrok_plus: "SuperGrok Plus",
|
|
1854
|
+
supergrok_heavy: "SuperGrok Heavy",
|
|
1855
|
+
x_premium: "X Premium",
|
|
1856
|
+
x_premium_plus: "X Premium+"
|
|
1857
|
+
};
|
|
1858
|
+
function authFile() {
|
|
1859
|
+
if (process.env.GROK_AUTH_FILE)
|
|
1860
|
+
return process.env.GROK_AUTH_FILE;
|
|
1861
|
+
return join7(homedir6(), ".grok", "auth.json");
|
|
1862
|
+
}
|
|
1863
|
+
function proxyBase() {
|
|
1864
|
+
const raw = process.env.GROK_CLI_CHAT_PROXY_BASE_URL?.trim();
|
|
1865
|
+
if (!raw)
|
|
1866
|
+
return DEFAULT_PROXY;
|
|
1867
|
+
return raw.replace(/\/+$/, "");
|
|
1868
|
+
}
|
|
1869
|
+
function num(v) {
|
|
1870
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
1871
|
+
return v;
|
|
1872
|
+
if (isObject(v) && typeof v.val === "number" && Number.isFinite(v.val))
|
|
1873
|
+
return v.val;
|
|
1874
|
+
if (typeof v === "string" && v.trim()) {
|
|
1875
|
+
const n = Number(v);
|
|
1876
|
+
return Number.isFinite(n) ? n : null;
|
|
1877
|
+
}
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
function money2(n) {
|
|
1881
|
+
return `$${n.toFixed(n % 1 ? 2 : 0)}`;
|
|
1882
|
+
}
|
|
1883
|
+
function sessionFromEntry(entry) {
|
|
1884
|
+
const access = typeof entry.key === "string" && entry.key ? entry.key : null;
|
|
1885
|
+
if (!access)
|
|
1886
|
+
return null;
|
|
1887
|
+
const expiry = typeof entry.expires_at === "string" ? Date.parse(entry.expires_at) : NaN;
|
|
1888
|
+
return {
|
|
1889
|
+
accessToken: access,
|
|
1890
|
+
refreshToken: typeof entry.refresh_token === "string" && entry.refresh_token ? entry.refresh_token : null,
|
|
1891
|
+
clientId: typeof entry.oidc_client_id === "string" && entry.oidc_client_id ? entry.oidc_client_id : null,
|
|
1892
|
+
email: typeof entry.email === "string" && entry.email.trim() ? entry.email.trim() : null,
|
|
1893
|
+
expiresAt: Number.isFinite(expiry) ? expiry : null,
|
|
1894
|
+
authMode: typeof entry.auth_mode === "string" ? entry.auth_mode : null
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function sessionFromAuthJson(parsed) {
|
|
1898
|
+
if (!isObject(parsed))
|
|
1899
|
+
return null;
|
|
1900
|
+
if (typeof parsed.key === "string" && parsed.key)
|
|
1901
|
+
return sessionFromEntry(parsed);
|
|
1902
|
+
const entries = Object.values(parsed).filter(isObject);
|
|
1903
|
+
const oidc = entries.find((e) => e.auth_mode === "oidc" && typeof e.key === "string" && e.key);
|
|
1904
|
+
if (oidc)
|
|
1905
|
+
return sessionFromEntry(oidc);
|
|
1906
|
+
const any = entries.find((e) => typeof e.key === "string" && e.key);
|
|
1907
|
+
return any ? sessionFromEntry(any) : null;
|
|
1908
|
+
}
|
|
1909
|
+
function readGrokSession() {
|
|
1910
|
+
const file = authFile();
|
|
1911
|
+
if (!existsSync7(file))
|
|
1912
|
+
return null;
|
|
1913
|
+
try {
|
|
1914
|
+
return sessionFromAuthJson(JSON.parse(readFileSync7(file, "utf8")));
|
|
1915
|
+
} catch {
|
|
1916
|
+
return null;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
function grokUsesApiKey(session) {
|
|
1920
|
+
if (session?.authMode === "oidc" && session.accessToken)
|
|
1921
|
+
return false;
|
|
1922
|
+
return Boolean(process.env.XAI_API_KEY?.trim()) || session?.authMode === "api_key";
|
|
1923
|
+
}
|
|
1924
|
+
function planFromGrokSettings(body) {
|
|
1925
|
+
if (!isObject(body))
|
|
1926
|
+
return null;
|
|
1927
|
+
if (typeof body.subscription_tier_display === "string" && body.subscription_tier_display.trim()) {
|
|
1928
|
+
return body.subscription_tier_display.trim();
|
|
1929
|
+
}
|
|
1930
|
+
const raw = typeof body.subscription_tier === "string" ? body.subscription_tier.trim() : "";
|
|
1931
|
+
if (!raw)
|
|
1932
|
+
return null;
|
|
1933
|
+
return TIER_NAMES[raw] ?? raw.replace(/_/g, " ");
|
|
1934
|
+
}
|
|
1935
|
+
function creditsRoot(body) {
|
|
1936
|
+
if (!isObject(body))
|
|
1937
|
+
return null;
|
|
1938
|
+
return isObject(body.config) ? body.config : body;
|
|
1939
|
+
}
|
|
1940
|
+
function normalizeGrokCredits(body) {
|
|
1941
|
+
const cfg = creditsRoot(body);
|
|
1942
|
+
if (!cfg)
|
|
1943
|
+
return [];
|
|
1944
|
+
const out = [];
|
|
1945
|
+
const period = isObject(cfg.currentPeriod) ? cfg.currentPeriod : null;
|
|
1946
|
+
const periodType = typeof period?.type === "string" ? period.type : "";
|
|
1947
|
+
const weekly = periodType.includes("WEEKLY");
|
|
1948
|
+
const resetsAt = isoOrNull(period?.end) ?? isoOrNull(cfg.billingPeriodEnd);
|
|
1949
|
+
const used = clampPercent(num(cfg.creditUsagePercent));
|
|
1950
|
+
if (used !== null) {
|
|
1951
|
+
out.push({
|
|
1952
|
+
id: "weekly",
|
|
1953
|
+
label: weekly || !periodType ? "Weekly Usage" : "Usage",
|
|
1954
|
+
usedPercent: used,
|
|
1955
|
+
resetsAt,
|
|
1956
|
+
windowMinutes: weekly || !periodType ? 10080 : null,
|
|
1957
|
+
kind: weekly || !periodType ? "rolling" : "cycle"
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
const cap = num(cfg.onDemandCap);
|
|
1961
|
+
const spent = num(cfg.onDemandUsed);
|
|
1962
|
+
if (cap && cap > 0 && spent !== null) {
|
|
1963
|
+
out.push({
|
|
1964
|
+
id: "on_demand",
|
|
1965
|
+
label: "On-demand",
|
|
1966
|
+
usedPercent: clampPercent(spent / cap * 100),
|
|
1967
|
+
resetsAt,
|
|
1968
|
+
windowMinutes: weekly ? 10080 : null,
|
|
1969
|
+
kind: weekly ? "rolling" : "cycle",
|
|
1970
|
+
note: `${money2(spent)} of ${money2(cap)}`
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
return out;
|
|
1974
|
+
}
|
|
1975
|
+
async function clientVersion() {
|
|
1976
|
+
return await binVersion("grok") ?? FALLBACK_CLI_VERSION3;
|
|
1977
|
+
}
|
|
1978
|
+
function headers3(token, version, email) {
|
|
1979
|
+
const h = {
|
|
1980
|
+
Authorization: `Bearer ${token}`,
|
|
1981
|
+
Accept: "application/json",
|
|
1982
|
+
"User-Agent": "xai-grok-cli",
|
|
1983
|
+
"x-grok-client-version": version,
|
|
1984
|
+
"x-grok-client-mode": "cli"
|
|
1985
|
+
};
|
|
1986
|
+
if (email)
|
|
1987
|
+
h["x-email"] = email;
|
|
1988
|
+
return h;
|
|
1989
|
+
}
|
|
1990
|
+
async function refreshAccessToken(session) {
|
|
1991
|
+
if (!session.refreshToken || !session.clientId)
|
|
1992
|
+
return null;
|
|
1993
|
+
const res = await fetchJson(TOKEN_URL, {
|
|
1994
|
+
method: "POST",
|
|
1995
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
1996
|
+
body: new URLSearchParams({
|
|
1997
|
+
grant_type: "refresh_token",
|
|
1998
|
+
refresh_token: session.refreshToken,
|
|
1999
|
+
client_id: session.clientId
|
|
2000
|
+
}).toString()
|
|
2001
|
+
});
|
|
2002
|
+
if (res.status !== 200 || !isObject(res.body) || typeof res.body.access_token !== "string" || !res.body.access_token) {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
const expiresIn = typeof res.body.expires_in === "number" ? res.body.expires_in : 3600;
|
|
2006
|
+
return {
|
|
2007
|
+
...session,
|
|
2008
|
+
accessToken: res.body.access_token,
|
|
2009
|
+
refreshToken: typeof res.body.refresh_token === "string" && res.body.refresh_token ? res.body.refresh_token : session.refreshToken,
|
|
2010
|
+
expiresAt: Date.now() + expiresIn * 1000
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
async function liveSession(session) {
|
|
2014
|
+
if (session.expiresAt && session.expiresAt > Date.now() + 60000)
|
|
2015
|
+
return session;
|
|
2016
|
+
return await refreshAccessToken(session) ?? (session.expiresAt && session.expiresAt > Date.now() ? session : null);
|
|
2017
|
+
}
|
|
2018
|
+
async function fetchGrok(account) {
|
|
2019
|
+
try {
|
|
2020
|
+
const stored = readGrokSession();
|
|
2021
|
+
if (!stored) {
|
|
2022
|
+
if (grokUsesApiKey(null)) {
|
|
2023
|
+
return snapshot(account, "unsupported", { message: "This CLI is using an xAI API key, which has no SuperGrok quota." });
|
|
2024
|
+
}
|
|
2025
|
+
return snapshot(account, "signed_out", { message: "Not signed in. Run `grok login --oauth`." });
|
|
2026
|
+
}
|
|
2027
|
+
if (stored.authMode && stored.authMode !== "oidc") {
|
|
2028
|
+
return snapshot(account, "unsupported", { email: stored.email, message: "This CLI is using an xAI API key, which has no SuperGrok quota." });
|
|
2029
|
+
}
|
|
2030
|
+
let session = await liveSession(stored);
|
|
2031
|
+
if (!session) {
|
|
2032
|
+
return snapshot(account, "error", { email: stored.email, message: "Grok session expired. Run `grok login --oauth`." });
|
|
2033
|
+
}
|
|
2034
|
+
const version = await clientVersion();
|
|
2035
|
+
const base = proxyBase();
|
|
2036
|
+
const get = (token, path) => fetchJson(`${base}${path}`, { headers: headers3(token, version, session.email) });
|
|
2037
|
+
let [credits, settings] = await Promise.all([
|
|
2038
|
+
get(session.accessToken, "/billing?format=credits"),
|
|
2039
|
+
get(session.accessToken, "/settings").catch(() => ({ status: 0, body: null, text: "" }))
|
|
2040
|
+
]);
|
|
2041
|
+
if (credits.status === 401 || credits.status === 403) {
|
|
2042
|
+
const refreshed = await refreshAccessToken(session);
|
|
2043
|
+
if (refreshed) {
|
|
2044
|
+
session = refreshed;
|
|
2045
|
+
[credits, settings] = await Promise.all([
|
|
2046
|
+
get(session.accessToken, "/billing?format=credits"),
|
|
2047
|
+
get(session.accessToken, "/settings").catch(() => ({ status: 0, body: null, text: "" }))
|
|
2048
|
+
]);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
const email = session.email ?? account.email ?? null;
|
|
2052
|
+
const plan = settings.status === 200 ? planFromGrokSettings(settings.body) : null;
|
|
2053
|
+
if (credits.status === 401 || credits.status === 403) {
|
|
2054
|
+
return snapshot(account, "error", { email, plan, message: `Grok rejected the session (${credits.status}). Run \`grok login --oauth\`.` });
|
|
2055
|
+
}
|
|
2056
|
+
if (credits.status !== 200) {
|
|
2057
|
+
return snapshot(account, "error", { email, plan, message: `Usage endpoint returned HTTP ${credits.status}.` });
|
|
2058
|
+
}
|
|
2059
|
+
const windows = normalizeGrokCredits(credits.body);
|
|
2060
|
+
if (windows.length === 0) {
|
|
2061
|
+
return snapshot(account, "unsupported", {
|
|
2062
|
+
email,
|
|
2063
|
+
plan,
|
|
2064
|
+
message: "Signed in, but this account has no SuperGrok / Grok Build allowance."
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
return snapshot(account, "ok", { email, plan, windows });
|
|
2068
|
+
} catch (e) {
|
|
2069
|
+
return snapshot(account, "error", { message: errorMessage(e) });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1395
2073
|
// src/adapters/index.ts
|
|
1396
2074
|
function fetchSnapshot(account) {
|
|
1397
2075
|
switch (account.provider) {
|
|
@@ -1401,6 +2079,10 @@ function fetchSnapshot(account) {
|
|
|
1401
2079
|
return fetchCodex(account);
|
|
1402
2080
|
case "cursor":
|
|
1403
2081
|
return fetchCursor(account);
|
|
2082
|
+
case "antigravity":
|
|
2083
|
+
return fetchAntigravity(account);
|
|
2084
|
+
case "grok":
|
|
2085
|
+
return fetchGrok(account);
|
|
1404
2086
|
case "opencode":
|
|
1405
2087
|
return fetchOpenCode(account);
|
|
1406
2088
|
}
|
|
@@ -1411,6 +2093,8 @@ var PROVIDERS = [
|
|
|
1411
2093
|
{ id: "claude", name: "Claude", bin: "claude" },
|
|
1412
2094
|
{ id: "codex", name: "Codex", bin: "codex" },
|
|
1413
2095
|
{ id: "cursor", name: "Cursor", bin: "cursor-agent" },
|
|
2096
|
+
{ id: "antigravity", name: "Antigravity", bin: "agy" },
|
|
2097
|
+
{ id: "grok", name: "Grok", bin: "grok" },
|
|
1414
2098
|
{ id: "opencode", name: "OpenCode Go", bin: "opencode" }
|
|
1415
2099
|
];
|
|
1416
2100
|
function providerName(id) {
|
|
@@ -1495,10 +2179,231 @@ class ReportCache {
|
|
|
1495
2179
|
}
|
|
1496
2180
|
}
|
|
1497
2181
|
|
|
2182
|
+
// src/instance.ts
|
|
2183
|
+
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2184
|
+
function looksLikeJustUsageCommand(command) {
|
|
2185
|
+
const s = command.replace(/\\/g, "/").toLowerCase();
|
|
2186
|
+
return s.includes("just-usage") || /\/just-usage\/(?:src\/|dist\/)?cli\.(ts|js)\b/.test(s);
|
|
2187
|
+
}
|
|
2188
|
+
function parseRunRecord(raw) {
|
|
2189
|
+
try {
|
|
2190
|
+
const parsed = JSON.parse(raw);
|
|
2191
|
+
if (!isObject(parsed))
|
|
2192
|
+
return null;
|
|
2193
|
+
const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
|
|
2194
|
+
const port = typeof parsed.port === "number" ? parsed.port : Number(parsed.port);
|
|
2195
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
2196
|
+
return null;
|
|
2197
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
2198
|
+
return null;
|
|
2199
|
+
return {
|
|
2200
|
+
pid,
|
|
2201
|
+
port,
|
|
2202
|
+
host: typeof parsed.host === "string" && parsed.host ? parsed.host : "0.0.0.0",
|
|
2203
|
+
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : ""
|
|
2204
|
+
};
|
|
2205
|
+
} catch {
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
function writeRunRecord(opts) {
|
|
2210
|
+
ensureDir(paths.runDir());
|
|
2211
|
+
writeFileSync3(paths.runRecord(opts.port), JSON.stringify({
|
|
2212
|
+
pid: process.pid,
|
|
2213
|
+
port: opts.port,
|
|
2214
|
+
host: opts.host,
|
|
2215
|
+
startedAt: new Date().toISOString()
|
|
2216
|
+
}), { encoding: "utf8", mode: 384 });
|
|
2217
|
+
}
|
|
2218
|
+
function readRunRecord(port) {
|
|
2219
|
+
const file = paths.runRecord(port);
|
|
2220
|
+
if (!existsSync8(file))
|
|
2221
|
+
return null;
|
|
2222
|
+
return parseRunRecord(readFileSync8(file, "utf8"));
|
|
2223
|
+
}
|
|
2224
|
+
function removeRunRecord(port) {
|
|
2225
|
+
const file = paths.runRecord(port);
|
|
2226
|
+
try {
|
|
2227
|
+
unlinkSync(file);
|
|
2228
|
+
} catch {}
|
|
2229
|
+
}
|
|
2230
|
+
function listRunPorts() {
|
|
2231
|
+
const dir = paths.runDir();
|
|
2232
|
+
if (!existsSync8(dir))
|
|
2233
|
+
return [];
|
|
2234
|
+
const ports = [];
|
|
2235
|
+
for (const name of readdirSync(dir)) {
|
|
2236
|
+
const m = name.match(/^(\d+)\.json$/);
|
|
2237
|
+
if (!m)
|
|
2238
|
+
continue;
|
|
2239
|
+
const port = Number(m[1]);
|
|
2240
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535)
|
|
2241
|
+
ports.push(port);
|
|
2242
|
+
}
|
|
2243
|
+
return ports.sort((a, b) => a - b);
|
|
2244
|
+
}
|
|
2245
|
+
function processAlive(pid) {
|
|
2246
|
+
try {
|
|
2247
|
+
process.kill(pid, 0);
|
|
2248
|
+
return true;
|
|
2249
|
+
} catch {
|
|
2250
|
+
return false;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
async function processCommand(pid) {
|
|
2254
|
+
if (process.platform === "win32") {
|
|
2255
|
+
const res = await run("wmic", ["process", "where", `ProcessId=${pid}`, "get", "CommandLine", "/value"], { timeoutMs: 5000 });
|
|
2256
|
+
const line = res.stdout.split(/\r?\n/).map((s) => s.trim()).find((s) => s.toLowerCase().startsWith("commandline="));
|
|
2257
|
+
return line ? line.slice(line.indexOf("=") + 1).trim() : "";
|
|
2258
|
+
}
|
|
2259
|
+
const res = await run("ps", ["-p", String(pid), "-www", "-o", "command="], { timeoutMs: 5000 });
|
|
2260
|
+
return res.stdout.split(/\r?\n/).map((s) => s.trim()).find((s) => s && !/^command$/i.test(s)) ?? "";
|
|
2261
|
+
}
|
|
2262
|
+
async function pidsOnPort(port) {
|
|
2263
|
+
if (process.platform === "win32") {
|
|
2264
|
+
const res = await run("netstat", ["-ano"], { timeoutMs: 8000 });
|
|
2265
|
+
const re = new RegExp(`[:\\[]${port}\\]?(?:\\s+\\S+){1,2}\\s+LISTENING\\s+(\\d+)`, "gi");
|
|
2266
|
+
const pids = new Set;
|
|
2267
|
+
for (const m of res.stdout.matchAll(re)) {
|
|
2268
|
+
const pid = Number(m[1]);
|
|
2269
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
2270
|
+
pids.add(pid);
|
|
2271
|
+
}
|
|
2272
|
+
return [...pids];
|
|
2273
|
+
}
|
|
2274
|
+
const res = await run("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeoutMs: 8000 });
|
|
2275
|
+
const pids = new Set;
|
|
2276
|
+
for (const line of res.stdout.split(/\r?\n/)) {
|
|
2277
|
+
const pid = Number(line.trim());
|
|
2278
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
2279
|
+
pids.add(pid);
|
|
2280
|
+
}
|
|
2281
|
+
return [...pids];
|
|
2282
|
+
}
|
|
2283
|
+
function parseHealth(body) {
|
|
2284
|
+
if (!isObject(body) || body.ok !== true)
|
|
2285
|
+
return null;
|
|
2286
|
+
const pid = typeof body.pid === "number" && Number.isInteger(body.pid) && body.pid > 0 ? body.pid : null;
|
|
2287
|
+
return {
|
|
2288
|
+
ok: true,
|
|
2289
|
+
version: typeof body.version === "string" ? body.version : null,
|
|
2290
|
+
name: typeof body.name === "string" ? body.name : null,
|
|
2291
|
+
pid
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
function isJustUsageHealth(info) {
|
|
2295
|
+
if (!info?.ok)
|
|
2296
|
+
return false;
|
|
2297
|
+
if (info.name && info.name !== PACKAGE_NAME)
|
|
2298
|
+
return false;
|
|
2299
|
+
return info.name === PACKAGE_NAME || Boolean(info.version);
|
|
2300
|
+
}
|
|
2301
|
+
async function fetchHealth(port) {
|
|
2302
|
+
try {
|
|
2303
|
+
const res = await fetchJson(`http://127.0.0.1:${port}/api/health`, { timeoutMs: 2000 });
|
|
2304
|
+
if (res.status !== 200)
|
|
2305
|
+
return null;
|
|
2306
|
+
return parseHealth(res.body);
|
|
2307
|
+
} catch {
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
async function oursOnPort(port) {
|
|
2312
|
+
const record = readRunRecord(port);
|
|
2313
|
+
const listeners = await pidsOnPort(port);
|
|
2314
|
+
const health = await fetchHealth(port);
|
|
2315
|
+
const ours = new Set;
|
|
2316
|
+
const known = new Set(listeners);
|
|
2317
|
+
if (record && processAlive(record.pid))
|
|
2318
|
+
known.add(record.pid);
|
|
2319
|
+
if (health?.pid && processAlive(health.pid))
|
|
2320
|
+
known.add(health.pid);
|
|
2321
|
+
for (const pid of known) {
|
|
2322
|
+
if (pid === process.pid)
|
|
2323
|
+
continue;
|
|
2324
|
+
const command = await processCommand(pid);
|
|
2325
|
+
const listening = listeners.includes(pid);
|
|
2326
|
+
if (looksLikeJustUsageCommand(command) || isJustUsageHealth(health) && (listening || pid === record?.pid || pid === health?.pid)) {
|
|
2327
|
+
ours.add(pid);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return { pids: [...ours], health, listening: listeners.length > 0 };
|
|
2331
|
+
}
|
|
2332
|
+
async function terminate(pids) {
|
|
2333
|
+
for (const pid of pids) {
|
|
2334
|
+
try {
|
|
2335
|
+
process.kill(pid, "SIGTERM");
|
|
2336
|
+
} catch {}
|
|
2337
|
+
}
|
|
2338
|
+
const deadline = Date.now() + 2500;
|
|
2339
|
+
while (Date.now() < deadline && pids.some(processAlive)) {
|
|
2340
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
2341
|
+
}
|
|
2342
|
+
for (const pid of pids) {
|
|
2343
|
+
if (!processAlive(pid))
|
|
2344
|
+
continue;
|
|
2345
|
+
try {
|
|
2346
|
+
process.kill(pid, "SIGKILL");
|
|
2347
|
+
} catch {}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
async function stopPort(port) {
|
|
2351
|
+
const found = await oursOnPort(port);
|
|
2352
|
+
if (found.pids.length === 0) {
|
|
2353
|
+
if (found.listening && !isJustUsageHealth(found.health))
|
|
2354
|
+
return { status: "busy", port };
|
|
2355
|
+
removeRunRecord(port);
|
|
2356
|
+
return { status: "idle", port };
|
|
2357
|
+
}
|
|
2358
|
+
await terminate(found.pids);
|
|
2359
|
+
removeRunRecord(port);
|
|
2360
|
+
return { status: "stopped", port, pids: found.pids };
|
|
2361
|
+
}
|
|
2362
|
+
async function stopServers(opts) {
|
|
2363
|
+
const ports = opts.all ? [...new Set([DEFAULT_PORT, ...listRunPorts(), ...opts.port ? [opts.port] : []])] : [opts.port ?? DEFAULT_PORT];
|
|
2364
|
+
const out = [];
|
|
2365
|
+
for (const port of ports)
|
|
2366
|
+
out.push(await stopPort(port));
|
|
2367
|
+
return out;
|
|
2368
|
+
}
|
|
2369
|
+
function formatStopResults(results) {
|
|
2370
|
+
const stopped = results.filter((r) => r.status === "stopped");
|
|
2371
|
+
const busy = results.filter((r) => r.status === "busy");
|
|
2372
|
+
const lines = [];
|
|
2373
|
+
for (const r of stopped) {
|
|
2374
|
+
lines.push(`Stopped ${PACKAGE_NAME} on port ${r.port} (pid ${r.pids.join(", ")}).`);
|
|
2375
|
+
}
|
|
2376
|
+
for (const r of busy) {
|
|
2377
|
+
lines.push(`Port ${r.port} is in use, but it is not a ${PACKAGE_NAME} server.`);
|
|
2378
|
+
}
|
|
2379
|
+
if (stopped.length === 0 && busy.length === 0) {
|
|
2380
|
+
if (results.length === 1)
|
|
2381
|
+
lines.push(`No ${PACKAGE_NAME} server is running on port ${results[0].port}.`);
|
|
2382
|
+
else
|
|
2383
|
+
lines.push(`No ${PACKAGE_NAME} server is running.`);
|
|
2384
|
+
}
|
|
2385
|
+
const code = busy.length ? 1 : stopped.length ? 0 : 1;
|
|
2386
|
+
return { text: lines.join(`
|
|
2387
|
+
`), code };
|
|
2388
|
+
}
|
|
2389
|
+
|
|
1498
2390
|
// src/server.ts
|
|
1499
2391
|
import { createServer } from "node:http";
|
|
1500
2392
|
import { hostname as hostname2, networkInterfaces } from "node:os";
|
|
1501
2393
|
|
|
2394
|
+
// src/ui/favicon.svg
|
|
2395
|
+
var favicon_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2396
|
+
<rect width="32" height="32" rx="2.5" fill="#000"/>
|
|
2397
|
+
<defs>
|
|
2398
|
+
<mask id="card">
|
|
2399
|
+
<rect x="6" y="6" width="20" height="20" rx="4.5" fill="#fff"/>
|
|
2400
|
+
<rect x="20.5" y="20.5" width="5.5" height="5.5" fill="#000"/>
|
|
2401
|
+
</mask>
|
|
2402
|
+
</defs>
|
|
2403
|
+
<rect width="32" height="32" fill="#fff" mask="url(#card)"/>
|
|
2404
|
+
</svg>
|
|
2405
|
+
`;
|
|
2406
|
+
|
|
1502
2407
|
// src/ui/index.html
|
|
1503
2408
|
var ui_default = `<!doctype html>
|
|
1504
2409
|
<html lang="en">
|
|
@@ -1507,7 +2412,7 @@ var ui_default = `<!doctype html>
|
|
|
1507
2412
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1508
2413
|
<meta name="color-scheme" content="dark">
|
|
1509
2414
|
<title>just-usage</title>
|
|
1510
|
-
<link rel="icon" type="image/svg+xml" href="
|
|
2415
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
1511
2416
|
<style>
|
|
1512
2417
|
:root {
|
|
1513
2418
|
--bg: #000;
|
|
@@ -1613,24 +2518,25 @@ var ui_default = `<!doctype html>
|
|
|
1613
2518
|
.credits-avail:hover, .credits-avail:focus-visible { color: var(--fg); }
|
|
1614
2519
|
.credits-tip {
|
|
1615
2520
|
display: none; position: absolute; right: 0; bottom: calc(100% + 8px);
|
|
1616
|
-
min-width:
|
|
2521
|
+
min-width: 0; padding: 8px 10px;
|
|
1617
2522
|
background: var(--surface); border: 1px solid var(--line-2); border-radius: 8px;
|
|
1618
2523
|
color: var(--muted); font-size: 11px; line-height: 1.45; white-space: nowrap; z-index: 5;
|
|
1619
2524
|
}
|
|
1620
2525
|
.credits-avail:hover .credits-tip, .credits-avail:focus-visible .credits-tip { display: block; }
|
|
1621
|
-
.credits-tip-row { display: flex; align-items: baseline; gap:
|
|
2526
|
+
.credits-tip-row { display: flex; align-items: baseline; gap: 10px; width: max-content; }
|
|
1622
2527
|
.credits-tip-row + .credits-tip-row { margin-top: 6px; }
|
|
1623
2528
|
.credits-tip-n { color: var(--fg); font-variant-numeric: tabular-nums; min-width: 1em; }
|
|
1624
|
-
.credits-tip-date { color: var(--fg);
|
|
1625
|
-
.credits-tip-left { color: var(--dim); font-variant-numeric: tabular-nums;
|
|
2529
|
+
.credits-tip-date { color: var(--fg); }
|
|
2530
|
+
.credits-tip-left { color: var(--dim); font-variant-numeric: tabular-nums; }
|
|
1626
2531
|
|
|
1627
2532
|
#dash[hidden], #settings[hidden] { display: none; }
|
|
1628
2533
|
.settings { display: flex; flex-direction: column; gap: 22px; }
|
|
1629
2534
|
.opts-label { font-size: 11px; color: var(--dim); letter-spacing: .04em; text-transform: uppercase; margin-bottom: 8px; }
|
|
1630
|
-
|
|
1631
|
-
.opts-row.
|
|
1632
|
-
|
|
1633
|
-
}
|
|
2535
|
+
#opts-providers { position: relative; }
|
|
2536
|
+
.opts-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 0; user-select: none; transition: opacity .22s ease; }
|
|
2537
|
+
.opts-row.dragging { opacity: .62; cursor: grabbing; }
|
|
2538
|
+
.opts-row.dragging .grip { cursor: grabbing; }
|
|
2539
|
+
.opts-email { margin-top: 10px; padding-top: 16px; border-top: 1px solid var(--line); }
|
|
1634
2540
|
.opts-prov { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
1635
2541
|
.opts-row img.logo { width: 14px; height: 14px; }
|
|
1636
2542
|
.grip {
|
|
@@ -1662,11 +2568,11 @@ var ui_default = `<!doctype html>
|
|
|
1662
2568
|
.seg input { appearance: none; position: absolute; }
|
|
1663
2569
|
.seg label:has(input:checked) { background: #121212; color: var(--fg); }
|
|
1664
2570
|
.opts-actions { display: flex; justify-content: flex-end; align-items: center; gap: 12px; padding-top: 2px; }
|
|
1665
|
-
.opts-actions:has(#opts-signed-in:not([hidden])) { justify-content: space-between; }
|
|
1666
2571
|
.opts-link { background: none; border: none; color: var(--muted); font: inherit; font-size: 12px; cursor: pointer; padding: 0; }
|
|
1667
2572
|
.opts-link:hover { color: var(--fg); }
|
|
1668
2573
|
.opts-link[hidden] { display: none; }
|
|
1669
2574
|
.acct-group { padding-bottom: 12px; }
|
|
2575
|
+
.acct-group:last-child { padding-bottom: 0; }
|
|
1670
2576
|
.acct-group + .acct-group { border-top: 1px solid var(--line); padding-top: 12px; }
|
|
1671
2577
|
.acct-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 13px; }
|
|
1672
2578
|
.acct-head img.logo { width: 14px; height: 14px; }
|
|
@@ -1683,6 +2589,7 @@ var ui_default = `<!doctype html>
|
|
|
1683
2589
|
border: 1px solid var(--line); border-radius: 12px;
|
|
1684
2590
|
padding: 0;
|
|
1685
2591
|
}
|
|
2592
|
+
dialog.dlg form { margin: 0; }
|
|
1686
2593
|
dialog.dlg::backdrop { background: rgba(0, 0, 0, .9); }
|
|
1687
2594
|
.dlg-head {
|
|
1688
2595
|
display: flex; justify-content: space-between; align-items: center;
|
|
@@ -1751,19 +2658,18 @@ var ui_default = `<!doctype html>
|
|
|
1751
2658
|
<label><input type="radio" name="meter" value="used"> Used</label>
|
|
1752
2659
|
</div>
|
|
1753
2660
|
</div>
|
|
1754
|
-
<label class="opts-row" for="opts-email">
|
|
1755
|
-
<span>Show email on accounts</span>
|
|
1756
|
-
<input class="chk" id="opts-email" type="checkbox">
|
|
1757
|
-
</label>
|
|
1758
2661
|
<div>
|
|
1759
2662
|
<div class="opts-inline tight">
|
|
1760
2663
|
<div class="opts-label">Accounts</div>
|
|
1761
2664
|
<button class="refresh" id="acct-add" type="button">Add account</button>
|
|
1762
2665
|
</div>
|
|
1763
2666
|
<div id="opts-accounts"></div>
|
|
2667
|
+
<label class="opts-row opts-email" for="opts-email">
|
|
2668
|
+
<span>Show email on accounts</span>
|
|
2669
|
+
<input class="chk" id="opts-email" type="checkbox">
|
|
2670
|
+
</label>
|
|
1764
2671
|
</div>
|
|
1765
2672
|
<div class="opts-actions">
|
|
1766
|
-
<button class="opts-link" id="opts-signed-in" type="button">Signed-in only</button>
|
|
1767
2673
|
<button class="refresh" id="refresh" type="button">Refresh data</button>
|
|
1768
2674
|
</div>
|
|
1769
2675
|
</div>
|
|
@@ -1774,11 +2680,13 @@ var ui_default = `<!doctype html>
|
|
|
1774
2680
|
</div>
|
|
1775
2681
|
|
|
1776
2682
|
<dialog class="dlg" id="add-acct">
|
|
1777
|
-
<
|
|
1778
|
-
<
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2683
|
+
<form autocomplete="off" onsubmit="return false">
|
|
2684
|
+
<div class="dlg-head">
|
|
2685
|
+
<strong id="add-acct-title">Add account</strong>
|
|
2686
|
+
<button class="opts-link" id="add-acct-close" type="button">Close</button>
|
|
2687
|
+
</div>
|
|
2688
|
+
<div class="dlg-body" id="add-acct-body"></div>
|
|
2689
|
+
</form>
|
|
1782
2690
|
</dialog>
|
|
1783
2691
|
|
|
1784
2692
|
<script>
|
|
@@ -1787,13 +2695,17 @@ var ui_default = `<!doctype html>
|
|
|
1787
2695
|
["claude", "Claude"],
|
|
1788
2696
|
["codex", "Codex"],
|
|
1789
2697
|
["cursor", "Cursor"],
|
|
2698
|
+
["antigravity", "Antigravity"],
|
|
2699
|
+
["grok", "Grok"],
|
|
1790
2700
|
["opencode", "OpenCode Go"],
|
|
1791
2701
|
];
|
|
1792
2702
|
const SIGNIN_HINT = {
|
|
1793
2703
|
claude: "Run <code>claude</code>, then <code>/login</code>.",
|
|
1794
2704
|
codex: "Run <code>codex login</code>.",
|
|
1795
2705
|
cursor: "Run <code>cursor-agent login</code>.",
|
|
2706
|
+
grok: "Run <code>grok login --oauth</code>.",
|
|
1796
2707
|
opencode: "Run <code>opencode auth login</code> or <code>just-usage add opencode</code>.",
|
|
2708
|
+
antigravity: "Run <code>agy</code> and complete Google sign-in.",
|
|
1797
2709
|
};
|
|
1798
2710
|
const $ = (id) => document.getElementById(id);
|
|
1799
2711
|
let report = null;
|
|
@@ -1823,22 +2735,37 @@ var ui_default = `<!doctype html>
|
|
|
1823
2735
|
} catch {}
|
|
1824
2736
|
let dragging = false;
|
|
1825
2737
|
let showLeft = localStorage.getItem("ju.showLeft") !== "0";
|
|
1826
|
-
|
|
2738
|
+
const defaultOrder = PROVIDERS.map(([id]) => id);
|
|
2739
|
+
let order = defaultOrder.slice();
|
|
1827
2740
|
try {
|
|
1828
2741
|
const raw = JSON.parse(localStorage.getItem("ju.order"));
|
|
1829
2742
|
if (Array.isArray(raw)) {
|
|
1830
|
-
const known = new Set(
|
|
2743
|
+
const known = new Set(defaultOrder);
|
|
1831
2744
|
const next = raw.filter((id) => known.has(id));
|
|
1832
|
-
for (const id of
|
|
2745
|
+
for (const id of defaultOrder) {
|
|
2746
|
+
if (next.includes(id)) continue;
|
|
2747
|
+
const after = defaultOrder.slice(defaultOrder.indexOf(id) + 1).find((x) => next.includes(x));
|
|
2748
|
+
next.splice(after ? next.indexOf(after) : next.length, 0, id);
|
|
2749
|
+
}
|
|
2750
|
+
if (!localStorage.getItem("ju.orderGen") && next[next.length - 1] === "grok") {
|
|
2751
|
+
const i = next.indexOf("opencode");
|
|
2752
|
+
if (i !== -1) {
|
|
2753
|
+
next.pop();
|
|
2754
|
+
next.splice(i, 0, "grok");
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
1833
2757
|
order = next;
|
|
1834
2758
|
}
|
|
1835
2759
|
} catch {}
|
|
2760
|
+
localStorage.setItem("ju.orderGen", "2");
|
|
2761
|
+
localStorage.setItem("ju.order", JSON.stringify(order));
|
|
1836
2762
|
|
|
1837
2763
|
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
1838
2764
|
const rich = (s) => esc(s).replace(/\`([^\`]+)\`/g, "<code>$1</code>");
|
|
1839
2765
|
const sev = (p) => (p == null ? null : p >= 85 ? "crit" : p >= 60 ? "warn" : "ok");
|
|
1840
2766
|
const rank = { ok: 1, warn: 2, crit: 3 };
|
|
1841
|
-
const logo = (id) => \`<img class="logo" src="/logos/\${id}.svg?v=
|
|
2767
|
+
const logo = (id) => \`<img class="logo" src="/logos/\${id}.svg?v=5" alt="" width="14" height="14">\`;
|
|
2768
|
+
const noSuggest = \`autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"\`;
|
|
1842
2769
|
const formatPlan = (p) => String(p).replace(/\\S+/g, (w) => w.charAt(0).toUpperCase() + w.slice(1));
|
|
1843
2770
|
const GENERIC_LABELS = new Set(["", "default", "token", "go", "pending", "profile", "account", "codex"]);
|
|
1844
2771
|
function isGenericLabel(label, email) {
|
|
@@ -1893,14 +2820,24 @@ var ui_default = `<!doctype html>
|
|
|
1893
2820
|
return d === 1 ? "1 day ago" : \`\${d} days ago\`;
|
|
1894
2821
|
}
|
|
1895
2822
|
|
|
1896
|
-
function
|
|
1897
|
-
return !!p?.accounts.some((a) => a.status
|
|
2823
|
+
function isDefaultVisible(p) {
|
|
2824
|
+
return !!p?.accounts.some((a) => a.status === "ok" || a.status === "error");
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
/** true/false = user chose; missing = auto-show when the CLI is signed in. */
|
|
2828
|
+
function explicitEnabled(id) {
|
|
2829
|
+
if (!enabled || typeof enabled !== "object" || !Object.hasOwn(enabled, id)) return null;
|
|
2830
|
+
if (enabled[id] === true) return true;
|
|
2831
|
+
if (enabled[id] === false) return false;
|
|
2832
|
+
return null;
|
|
1898
2833
|
}
|
|
1899
2834
|
|
|
1900
2835
|
function visibleIds() {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2836
|
+
return order.filter((id) => {
|
|
2837
|
+
const choice = explicitEnabled(id);
|
|
2838
|
+
if (choice !== null) return choice;
|
|
2839
|
+
return isDefaultVisible(report?.providers.find((x) => x.id === id));
|
|
2840
|
+
});
|
|
1904
2841
|
}
|
|
1905
2842
|
|
|
1906
2843
|
function ensureActive() {
|
|
@@ -1915,10 +2852,8 @@ var ui_default = `<!doctype html>
|
|
|
1915
2852
|
if (next) localStorage.setItem("ju.providers", JSON.stringify(next));
|
|
1916
2853
|
else localStorage.removeItem("ju.providers");
|
|
1917
2854
|
ensureActive();
|
|
1918
|
-
if (settingsOpen)
|
|
1919
|
-
|
|
1920
|
-
$("opts-signed-in").hidden = !enabled;
|
|
1921
|
-
} else render();
|
|
2855
|
+
if (settingsOpen) renderTabs();
|
|
2856
|
+
else render();
|
|
1922
2857
|
}
|
|
1923
2858
|
|
|
1924
2859
|
function persistOrder(next) {
|
|
@@ -1989,9 +2924,16 @@ var ui_default = `<!doctype html>
|
|
|
1989
2924
|
return html;
|
|
1990
2925
|
}
|
|
1991
2926
|
|
|
2927
|
+
function planBadge(provider, plan) {
|
|
2928
|
+
if (!plan) return "";
|
|
2929
|
+
const name = PROVIDERS.find(([id]) => id === provider)?.[1] || provider;
|
|
2930
|
+
if (plan.toLowerCase() === name.toLowerCase() || plan.toLowerCase() === provider) return "";
|
|
2931
|
+
return \`<span class="plan">\${esc(formatPlan(plan))}</span>\`;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
1992
2934
|
function renderCard(a, provider, index) {
|
|
1993
2935
|
const title = accountTitle(a, index);
|
|
1994
|
-
const plan =
|
|
2936
|
+
const plan = planBadge(provider, a.account.plan);
|
|
1995
2937
|
let badge = "";
|
|
1996
2938
|
if (a.status === "ok") {
|
|
1997
2939
|
let worst = null;
|
|
@@ -2071,7 +3013,7 @@ var ui_default = `<!doctype html>
|
|
|
2071
3013
|
}).join("");
|
|
2072
3014
|
for (const input of $("opts-providers").querySelectorAll("input")) {
|
|
2073
3015
|
input.onchange = () => {
|
|
2074
|
-
const next =
|
|
3016
|
+
const next = { ...(enabled || {}) };
|
|
2075
3017
|
next[input.dataset.id] = input.checked;
|
|
2076
3018
|
persistEnabled(next);
|
|
2077
3019
|
};
|
|
@@ -2084,22 +3026,48 @@ var ui_default = `<!doctype html>
|
|
|
2084
3026
|
const row = grip.closest(".opts-row");
|
|
2085
3027
|
dragging = true;
|
|
2086
3028
|
row.classList.add("dragging");
|
|
3029
|
+
document.body.style.cursor = "grabbing";
|
|
3030
|
+
try { grip.setPointerCapture(e.pointerId); } catch {}
|
|
3031
|
+
const ease = "transform .36s cubic-bezier(.4, 0, .2, 1)";
|
|
2087
3032
|
const place = (clientY) => {
|
|
2088
|
-
const
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
3033
|
+
const rows = [...list.querySelectorAll(".opts-row")];
|
|
3034
|
+
const others = rows.filter((r) => r !== row);
|
|
3035
|
+
const y = clientY - list.getBoundingClientRect().top + list.scrollTop;
|
|
3036
|
+
const before = others.find((r) => y < r.offsetTop + r.offsetHeight / 2 - 6);
|
|
3037
|
+
if ((before && row.nextElementSibling === before) || (!before && list.lastElementChild === row)) return;
|
|
3038
|
+
const first = new Map(rows.map((r) => {
|
|
3039
|
+
r.style.transition = "none";
|
|
3040
|
+
return [r, r.getBoundingClientRect()];
|
|
3041
|
+
}));
|
|
2093
3042
|
if (before) list.insertBefore(row, before);
|
|
2094
3043
|
else list.appendChild(row);
|
|
3044
|
+
for (const r of rows) {
|
|
3045
|
+
r.style.transform = "";
|
|
3046
|
+
const from = first.get(r);
|
|
3047
|
+
const dy = from.top - r.getBoundingClientRect().top;
|
|
3048
|
+
r.style.transform = dy ? \`translateY(\${dy}px)\` : "";
|
|
3049
|
+
}
|
|
3050
|
+
list.getBoundingClientRect();
|
|
3051
|
+
for (const r of rows) {
|
|
3052
|
+
if (!r.style.transform) continue;
|
|
3053
|
+
r.style.transition = ease;
|
|
3054
|
+
r.style.transform = "";
|
|
3055
|
+
}
|
|
2095
3056
|
};
|
|
2096
3057
|
const move = (ev) => place(ev.clientY);
|
|
2097
3058
|
const stop = () => {
|
|
2098
3059
|
document.removeEventListener("pointermove", move);
|
|
2099
3060
|
document.removeEventListener("pointerup", stop);
|
|
2100
3061
|
document.removeEventListener("pointercancel", stop);
|
|
3062
|
+
document.body.style.cursor = "";
|
|
2101
3063
|
dragging = false;
|
|
2102
3064
|
row.classList.remove("dragging");
|
|
3065
|
+
window.setTimeout(() => {
|
|
3066
|
+
for (const r of list.querySelectorAll(".opts-row")) {
|
|
3067
|
+
r.style.transition = "";
|
|
3068
|
+
r.style.transform = "";
|
|
3069
|
+
}
|
|
3070
|
+
}, 380);
|
|
2103
3071
|
const next = [...list.querySelectorAll(".opts-row")].map((r) => r.dataset.id);
|
|
2104
3072
|
if (next.join() !== order.join()) persistOrder(next);
|
|
2105
3073
|
};
|
|
@@ -2108,7 +3076,6 @@ var ui_default = `<!doctype html>
|
|
|
2108
3076
|
document.addEventListener("pointercancel", stop);
|
|
2109
3077
|
};
|
|
2110
3078
|
}
|
|
2111
|
-
$("opts-signed-in").hidden = !enabled;
|
|
2112
3079
|
for (const input of $("opts-meter").querySelectorAll("input")) {
|
|
2113
3080
|
input.checked = (input.value === "left") === showLeft;
|
|
2114
3081
|
}
|
|
@@ -2139,7 +3106,7 @@ var ui_default = `<!doctype html>
|
|
|
2139
3106
|
const extra = a.account.kind === "default" ? "from CLI" : a.account.kind;
|
|
2140
3107
|
if (renaming === a.account.id) {
|
|
2141
3108
|
return \`<div class="acct-row">
|
|
2142
|
-
<span class="acct-rename"><input class="field acct-rename-input" data-id="\${esc(a.account.id)}" value="\${esc(title)}">
|
|
3109
|
+
<span class="acct-rename"><input class="field acct-rename-input" data-id="\${esc(a.account.id)}" value="\${esc(title)}" \${noSuggest}>
|
|
2143
3110
|
<button class="refresh acct-rename-save" type="button" data-id="\${esc(a.account.id)}">Save</button></span>
|
|
2144
3111
|
<button class="opts-link acct-rename-cancel" type="button">Cancel</button>
|
|
2145
3112
|
</div>\`;
|
|
@@ -2225,25 +3192,30 @@ var ui_default = `<!doctype html>
|
|
|
2225
3192
|
body = \`<div class="pick">
|
|
2226
3193
|
<button class="pick-btn" type="button" data-id="claude">\${logo("claude")} Claude</button>
|
|
2227
3194
|
<button class="pick-btn" type="button" data-id="codex">\${logo("codex")} Codex</button>
|
|
3195
|
+
<button class="pick-btn" type="button" data-id="antigravity">\${logo("antigravity")} Antigravity</button>
|
|
2228
3196
|
<button class="pick-btn" type="button" data-id="opencode">\${logo("opencode")} OpenCode Go</button>
|
|
2229
3197
|
</div>
|
|
2230
|
-
<div class="acct-note">One Cursor account for now — whatever <code>cursor-agent</code> is signed in as.</div
|
|
3198
|
+
<div class="acct-note">One Cursor account for now — whatever <code>cursor-agent</code> is signed in as.</div>
|
|
3199
|
+
<div class="acct-note">One Grok account for now — whatever <code>grok</code> is signed in as.</div>\`;
|
|
2231
3200
|
} else if (wizard.step === "label") {
|
|
2232
3201
|
body = \`<div class="acct-note">Optional. Leave blank to show as Account 2, Account 3, …</div>
|
|
2233
|
-
<input class="field" id="wiz-label" placeholder="Label (optional)" value="\${esc(wizard.label)}">
|
|
3202
|
+
<input class="field" id="wiz-label" placeholder="Label (optional)" value="\${esc(wizard.label)}" \${noSuggest}>
|
|
2234
3203
|
<div class="acct-actions"><button class="opts-link" id="wiz-back" type="button">Back</button>
|
|
2235
3204
|
<button class="refresh" id="wiz-next" type="button">Continue</button></div>\`;
|
|
2236
|
-
} else if (wizard.provider === "codex") {
|
|
2237
|
-
|
|
3205
|
+
} else if (wizard.provider === "codex" || wizard.provider === "antigravity") {
|
|
3206
|
+
const oauth = wizard.provider === "codex"
|
|
3207
|
+
? { note: "Sign in with ChatGPT. If you are not on this machine, paste the localhost callback after the redirect.", action: "Sign in with ChatGPT", ph: "http://localhost:… callback", start: "wiz-codex", submit: "Submit callback" }
|
|
3208
|
+
: { note: "Sign in with Google. Copy the code from the Antigravity page (or paste the callback URL). This extra login does not replace <code>agy</code>'s default account.", action: "Sign in with Google", ph: "4/0A… code from the page", start: "wiz-agy", submit: "Submit code" };
|
|
3209
|
+
body = \`<div class="acct-note">\${oauth.note}</div>
|
|
2238
3210
|
\${wizard.authUrl ? \`<div class="acct-url">Open <a href="\${esc(wizard.authUrl)}" target="_blank" rel="noreferrer">this sign-in URL</a></div>
|
|
2239
|
-
<input class="field" id="wiz-callback" placeholder="
|
|
2240
|
-
<button class="refresh" id="wiz-callback-go" type="button"
|
|
2241
|
-
: \`<button class="refresh" id="
|
|
3211
|
+
<input class="field" id="wiz-callback" placeholder="\${esc(oauth.ph)}" value="\${esc(wizard.callback)}" \${noSuggest}>
|
|
3212
|
+
<button class="refresh" id="wiz-callback-go" type="button">\${oauth.submit || "Submit callback"}</button>\`
|
|
3213
|
+
: \`<button class="refresh" id="\${oauth.start}" type="button">\${oauth.action}</button>\`}
|
|
2242
3214
|
<button class="opts-link" id="wiz-back" type="button">Back</button>\`;
|
|
2243
3215
|
} else {
|
|
2244
3216
|
const ph = wizard.provider === "claude" ? "Setup token from <code>claude setup-token</code>" : "OpenCode Go API key";
|
|
2245
3217
|
body = \`<div class="acct-note">\${ph}.</div>
|
|
2246
|
-
<input class="field" id="wiz-secret" type="password" placeholder="\${wizard.provider === "claude" ? "Setup token" : "API key"}" value="\${esc(wizard.secret)}"
|
|
3218
|
+
<input class="field" id="wiz-secret" type="password" placeholder="\${wizard.provider === "claude" ? "Setup token" : "API key"}" value="\${esc(wizard.secret)}" \${noSuggest}>
|
|
2247
3219
|
<div class="acct-actions"><button class="opts-link" id="wiz-back" type="button">Back</button>
|
|
2248
3220
|
<button class="refresh" id="wiz-save" type="button">Add</button></div>\`;
|
|
2249
3221
|
}
|
|
@@ -2271,8 +3243,10 @@ var ui_default = `<!doctype html>
|
|
|
2271
3243
|
if (save) save.onclick = addToken;
|
|
2272
3244
|
const start = $("wiz-codex");
|
|
2273
3245
|
if (start) start.onclick = startCodex;
|
|
3246
|
+
const startAgy = $("wiz-agy");
|
|
3247
|
+
if (startAgy) startAgy.onclick = startAntigravity;
|
|
2274
3248
|
const submit = $("wiz-callback-go");
|
|
2275
|
-
if (submit) submit.onclick = submitCodexCallback;
|
|
3249
|
+
if (submit) submit.onclick = wizard.provider === "antigravity" ? submitAntigravityCallback : submitCodexCallback;
|
|
2276
3250
|
}
|
|
2277
3251
|
|
|
2278
3252
|
async function addToken() {
|
|
@@ -2329,6 +3303,56 @@ var ui_default = `<!doctype html>
|
|
|
2329
3303
|
}
|
|
2330
3304
|
}
|
|
2331
3305
|
|
|
3306
|
+
async function startAntigravity() {
|
|
3307
|
+
if (busy) return;
|
|
3308
|
+
const popup = window.open("about:blank", "_blank");
|
|
3309
|
+
busy = true;
|
|
3310
|
+
wizardMsg("Starting sign-in…", false);
|
|
3311
|
+
renderWizard();
|
|
3312
|
+
try {
|
|
3313
|
+
const body = await api("/api/accounts/antigravity/start", {
|
|
3314
|
+
method: "POST",
|
|
3315
|
+
headers: { "Content-Type": "application/json" },
|
|
3316
|
+
body: JSON.stringify({ label: wizard.label }),
|
|
3317
|
+
});
|
|
3318
|
+
wizard.sessionId = body.sessionId;
|
|
3319
|
+
wizard.authUrl = body.authUrl;
|
|
3320
|
+
if (body.authUrl) {
|
|
3321
|
+
if (popup) popup.location.replace(body.authUrl);
|
|
3322
|
+
else window.location.assign(body.authUrl);
|
|
3323
|
+
} else {
|
|
3324
|
+
popup?.close();
|
|
3325
|
+
}
|
|
3326
|
+
wizardMsg("Copy the code from the Antigravity page and paste it here.", false);
|
|
3327
|
+
} catch (e) {
|
|
3328
|
+
popup?.close();
|
|
3329
|
+
wizardMsg(e.message, true);
|
|
3330
|
+
} finally {
|
|
3331
|
+
busy = false;
|
|
3332
|
+
renderWizard();
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
async function submitAntigravityCallback() {
|
|
3337
|
+
if (!wizard.sessionId) return;
|
|
3338
|
+
if (busy) return;
|
|
3339
|
+
busy = true;
|
|
3340
|
+
try {
|
|
3341
|
+
await api("/api/accounts/antigravity/callback", {
|
|
3342
|
+
method: "POST",
|
|
3343
|
+
headers: { "Content-Type": "application/json" },
|
|
3344
|
+
body: JSON.stringify({ sessionId: wizard.sessionId, url: wizard.callback }),
|
|
3345
|
+
});
|
|
3346
|
+
closeWizard();
|
|
3347
|
+
await load(true);
|
|
3348
|
+
} catch (e) {
|
|
3349
|
+
wizardMsg(e.message, true);
|
|
3350
|
+
renderWizard();
|
|
3351
|
+
} finally {
|
|
3352
|
+
busy = false;
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
|
|
2332
3356
|
async function submitCodexCallback() {
|
|
2333
3357
|
if (!wizard.sessionId) return;
|
|
2334
3358
|
try {
|
|
@@ -2422,7 +3446,10 @@ var ui_default = `<!doctype html>
|
|
|
2422
3446
|
for (const a of p.accounts || []) {
|
|
2423
3447
|
const old = prev?.accounts.find((x) => x.account.id === a.account.id);
|
|
2424
3448
|
if (!a.account.email && old?.account.email) a.account.email = old.account.email;
|
|
2425
|
-
if (!a.account.plan && old?.account.plan)
|
|
3449
|
+
if (!a.account.plan && old?.account.plan) {
|
|
3450
|
+
const name = PROVIDERS.find(([id]) => id === p.id)?.[1] || p.id;
|
|
3451
|
+
if (old.account.plan.toLowerCase() !== name.toLowerCase()) a.account.plan = old.account.plan;
|
|
3452
|
+
}
|
|
2426
3453
|
}
|
|
2427
3454
|
}
|
|
2428
3455
|
}
|
|
@@ -2465,7 +3492,6 @@ var ui_default = `<!doctype html>
|
|
|
2465
3492
|
|
|
2466
3493
|
$("opts-btn").onclick = () => setView(!settingsOpen);
|
|
2467
3494
|
$("refresh").onclick = () => load(true, true);
|
|
2468
|
-
$("opts-signed-in").onclick = () => persistEnabled(null);
|
|
2469
3495
|
$("opts-email").onchange = () => {
|
|
2470
3496
|
showEmail = $("opts-email").checked;
|
|
2471
3497
|
localStorage.setItem("ju.showEmail", showEmail ? "1" : "0");
|
|
@@ -2516,6 +3542,13 @@ var ui_default = `<!doctype html>
|
|
|
2516
3542
|
</html>
|
|
2517
3543
|
`;
|
|
2518
3544
|
|
|
3545
|
+
// src/ui/logos/antigravity.svg
|
|
3546
|
+
var antigravity_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ececec" role="img">
|
|
3547
|
+
<title>Antigravity</title>
|
|
3548
|
+
<path d="M21.218 22.023c1.273.954 3.182.318 1.432-1.432C17.4 15.5 18.513 1.5 11.99 1.5 5.468 1.5 6.581 15.5 1.331 20.591c-1.91 1.91.16 2.386 1.432 1.432C7.695 18.682 7.377 12.795 11.99 12.795c4.614 0 4.296 5.887 9.228 9.228Z"/>
|
|
3549
|
+
</svg>
|
|
3550
|
+
`;
|
|
3551
|
+
|
|
2519
3552
|
// src/ui/logos/claude.svg
|
|
2520
3553
|
var claude_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ececec" role="img">
|
|
2521
3554
|
<title>Claude</title>
|
|
@@ -2537,6 +3570,14 @@ var cursor_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24
|
|
|
2537
3570
|
</svg>
|
|
2538
3571
|
`;
|
|
2539
3572
|
|
|
3573
|
+
// src/ui/logos/grok.svg
|
|
3574
|
+
var grok_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#ececec" role="img">
|
|
3575
|
+
<title>Grok</title>
|
|
3576
|
+
<path d="M210.484 312.759 343.465 210.383c6.519-5.019 15.837-3.061 18.943 4.734 16.35 41.114 9.046 90.523-23.483 124.446-32.528 33.924-77.788 41.364-119.157 24.42l-45.191 21.82c64.817 46.205 143.527 34.778 192.712-16.552 39.014-40.687 51.097-96.147 39.799-146.16l.102.107c-16.383-73.472 4.028-102.839 45.84-162.891.99-1.424 1.98-2.848 2.97-4.307L400.978 113.382v-.178L210.45 312.794"/>
|
|
3577
|
+
<path d="M183.042 337.641c-46.523-46.347-38.502-118.074 1.194-159.438 29.354-30.613 77.447-43.107 119.43-24.739l45.089-21.714c-8.123-6.123-18.534-12.708-30.48-17.336-53.998-23.173-118.645-11.64-162.54 34.102-42.222 44.033-55.499 111.738-32.699 169.511 17.033 43.179-10.888 73.721-39.013 104.548C74.056 433.503 64.055 444.431 56 456l127.007-118.323"/>
|
|
3578
|
+
</svg>
|
|
3579
|
+
`;
|
|
3580
|
+
|
|
2540
3581
|
// src/ui/logos/opencode.svg
|
|
2541
3582
|
var opencode_default = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ececec" fill-rule="evenodd" role="img">
|
|
2542
3583
|
<title>OpenCode</title>
|
|
@@ -2549,7 +3590,9 @@ var LOGOS = {
|
|
|
2549
3590
|
claude: claude_default,
|
|
2550
3591
|
codex: codex_default,
|
|
2551
3592
|
cursor: cursor_default,
|
|
2552
|
-
opencode: opencode_default
|
|
3593
|
+
opencode: opencode_default,
|
|
3594
|
+
antigravity: antigravity_default,
|
|
3595
|
+
grok: grok_default
|
|
2553
3596
|
};
|
|
2554
3597
|
function json(res, status, body) {
|
|
2555
3598
|
const text = JSON.stringify(body);
|
|
@@ -2603,6 +3646,11 @@ async function startServer(opts) {
|
|
|
2603
3646
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
2604
3647
|
try {
|
|
2605
3648
|
const method = req.method ?? "GET";
|
|
3649
|
+
if ((url.pathname === "/favicon.svg" || url.pathname === "/favicon.ico") && (method === "GET" || method === "HEAD")) {
|
|
3650
|
+
res.writeHead(200, { "Content-Type": "image/svg+xml; charset=utf-8", "Cache-Control": "public, max-age=86400" });
|
|
3651
|
+
res.end(favicon_default);
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
2606
3654
|
if (url.pathname === "/" && (method === "GET" || method === "HEAD")) {
|
|
2607
3655
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
2608
3656
|
res.end(ui_default);
|
|
@@ -2625,7 +3673,7 @@ async function startServer(opts) {
|
|
|
2625
3673
|
return;
|
|
2626
3674
|
}
|
|
2627
3675
|
if (url.pathname === "/api/health" && (method === "GET" || method === "HEAD")) {
|
|
2628
|
-
json(res, 200, { ok: true, version: VERSION });
|
|
3676
|
+
json(res, 200, { ok: true, name: PACKAGE_NAME, version: VERSION, pid: process.pid });
|
|
2629
3677
|
return;
|
|
2630
3678
|
}
|
|
2631
3679
|
if (url.pathname === "/api/accounts" && method === "POST") {
|
|
@@ -2641,7 +3689,23 @@ async function startServer(opts) {
|
|
|
2641
3689
|
json(res, 200, await mutated(() => addOpenCodeKey(secret, label)));
|
|
2642
3690
|
return;
|
|
2643
3691
|
}
|
|
2644
|
-
json(res, 400, { error: "Add Claude with a setup-token, OpenCode with an API key, or start a Codex sign-in." });
|
|
3692
|
+
json(res, 400, { error: "Add Claude with a setup-token, OpenCode with an API key, or start a Codex / Antigravity sign-in." });
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
if (url.pathname === "/api/accounts/antigravity/start" && method === "POST") {
|
|
3696
|
+
const body = await readJson(req);
|
|
3697
|
+
const accountId = str(body.accountId);
|
|
3698
|
+
json(res, 200, accountId ? await beginAntigravityRelogin(accountId) : await beginAntigravityAdd(str(body.label) || undefined));
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
if (url.pathname === "/api/accounts/antigravity/callback" && method === "POST") {
|
|
3702
|
+
const body = await readJson(req);
|
|
3703
|
+
json(res, 200, { ok: true, account: await mutated(() => submitAntigravityCallback(str(body.sessionId), str(body.url))) });
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3706
|
+
const agySession = url.pathname.match(/^\/api\/accounts\/antigravity\/session\/([^/]+)$/);
|
|
3707
|
+
if (agySession && (method === "GET" || method === "HEAD")) {
|
|
3708
|
+
json(res, 200, antigravitySessionStatus(decodeURIComponent(agySession[1])));
|
|
2645
3709
|
return;
|
|
2646
3710
|
}
|
|
2647
3711
|
if (url.pathname === "/api/accounts/codex/start" && method === "POST") {
|
|
@@ -2695,7 +3759,14 @@ async function startServer(opts) {
|
|
|
2695
3759
|
return new Promise((resolve, reject) => {
|
|
2696
3760
|
server.once("error", reject);
|
|
2697
3761
|
server.listen(opts.port, opts.host, () => {
|
|
2698
|
-
|
|
3762
|
+
writeRunRecord({ port: opts.port, host: opts.host });
|
|
3763
|
+
resolve({
|
|
3764
|
+
close: () => {
|
|
3765
|
+
removeRunRecord(opts.port);
|
|
3766
|
+
server.close();
|
|
3767
|
+
},
|
|
3768
|
+
urls: reachableUrls(opts.host, opts.port, tailscaleIp)
|
|
3769
|
+
});
|
|
2699
3770
|
});
|
|
2700
3771
|
});
|
|
2701
3772
|
}
|
|
@@ -2703,7 +3774,6 @@ function isTailscale(ip) {
|
|
|
2703
3774
|
const [a, b] = ip.split(".").map(Number);
|
|
2704
3775
|
return a === 100 && b !== undefined && b >= 64 && b <= 127;
|
|
2705
3776
|
}
|
|
2706
|
-
var TAILSCALE_NOTE = "Tailscale only. Not same-Wi-Fi. Other devices need Tailscale too.";
|
|
2707
3777
|
async function detectTailscaleIp() {
|
|
2708
3778
|
const res = await run("tailscale", ["status", "--json"], { timeoutMs: 4000 });
|
|
2709
3779
|
if (res.code !== 0)
|
|
@@ -2744,7 +3814,7 @@ function reachableUrls(host, port, tailscaleIp = null) {
|
|
|
2744
3814
|
}
|
|
2745
3815
|
}
|
|
2746
3816
|
if (tailscaleIp) {
|
|
2747
|
-
urls.push({ kind: "tailscale", url: `http://${tailscaleIp}:${port}
|
|
3817
|
+
urls.push({ kind: "tailscale", url: `http://${tailscaleIp}:${port}` });
|
|
2748
3818
|
}
|
|
2749
3819
|
return urls;
|
|
2750
3820
|
}
|
|
@@ -2809,15 +3879,15 @@ function renderReport(report, now = Date.now()) {
|
|
|
2809
3879
|
}
|
|
2810
3880
|
|
|
2811
3881
|
// src/update.ts
|
|
2812
|
-
import { existsSync as
|
|
3882
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2813
3883
|
import { dirname as dirname3 } from "node:path";
|
|
2814
3884
|
var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
2815
3885
|
function readCache() {
|
|
2816
3886
|
const file = paths.updateCache();
|
|
2817
|
-
if (!
|
|
3887
|
+
if (!existsSync9(file))
|
|
2818
3888
|
return null;
|
|
2819
3889
|
try {
|
|
2820
|
-
const parsed = JSON.parse(
|
|
3890
|
+
const parsed = JSON.parse(readFileSync9(file, "utf8"));
|
|
2821
3891
|
if (typeof parsed.checkedAt === "string" && typeof parsed.latest === "string")
|
|
2822
3892
|
return parsed;
|
|
2823
3893
|
} catch {}
|
|
@@ -2826,7 +3896,7 @@ function readCache() {
|
|
|
2826
3896
|
function writeCache(c) {
|
|
2827
3897
|
try {
|
|
2828
3898
|
ensureDir(dirname3(paths.updateCache()));
|
|
2829
|
-
|
|
3899
|
+
writeFileSync4(paths.updateCache(), JSON.stringify(c) + `
|
|
2830
3900
|
`);
|
|
2831
3901
|
} catch {}
|
|
2832
3902
|
}
|
|
@@ -2919,18 +3989,69 @@ Usage
|
|
|
2919
3989
|
--port <n> Port (default ${DEFAULT_PORT})
|
|
2920
3990
|
--host <addr> Bind address (default ${DEFAULT_HOST}; use 127.0.0.1 for local only)
|
|
2921
3991
|
--no-open Don't open a browser
|
|
3992
|
+
just-usage stop [options] Stop a running just-usage server
|
|
3993
|
+
--port <n> Port (default ${DEFAULT_PORT})
|
|
3994
|
+
--all Stop every just-usage server this CLI started
|
|
2922
3995
|
just-usage status [--json] Print quotas in the terminal
|
|
2923
3996
|
just-usage accounts List accounts
|
|
2924
|
-
just-usage add <provider> Add an account (codex | claude | opencode)
|
|
3997
|
+
just-usage add <provider> Add an account (codex | claude | opencode | antigravity)
|
|
2925
3998
|
--label <name> Friendly name
|
|
2926
3999
|
--token Claude only: paste a \`claude setup-token\` instead of a profile login
|
|
2927
4000
|
just-usage login <account-id> Re-authenticate a profile account
|
|
2928
4001
|
just-usage remove <account-id> Remove an account and anything we stored for it
|
|
2929
4002
|
just-usage upgrade [--check] Update to the latest release
|
|
2930
4003
|
just-usage --version
|
|
4004
|
+
just-usage stop --help More on stopping a running server
|
|
2931
4005
|
|
|
2932
4006
|
Providers: ${PROVIDERS.map((p) => p.name).join(", ")}
|
|
2933
4007
|
Cursor uses whatever \`cursor-agent\` is logged in as (single account).
|
|
4008
|
+
Grok uses whatever \`grok login --oauth\` stored (single account).
|
|
4009
|
+
Antigravity extras are extra Google logins; they do not replace \`agy\`'s signed-in account.
|
|
4010
|
+
|
|
4011
|
+
Stop
|
|
4012
|
+
\`just-usage stop\` asks the server to exit — the same as Ctrl+C in the terminal
|
|
4013
|
+
that started it. Use this when that terminal is gone or another instance is
|
|
4014
|
+
still holding the port.
|
|
4015
|
+
|
|
4016
|
+
just-usage stop Stop the default server (port ${DEFAULT_PORT})
|
|
4017
|
+
just-usage stop --port 5758 Stop a server you started on another port
|
|
4018
|
+
just-usage stop --all Stop every just-usage server recorded here
|
|
4019
|
+
|
|
4020
|
+
It only stops a process it can confirm is just-usage (pid file, health route,
|
|
4021
|
+
or command line). An unrelated process on the same port is left alone.
|
|
4022
|
+
`;
|
|
4023
|
+
var STOP_HELP = `${PACKAGE_NAME} stop
|
|
4024
|
+
Stop a running just-usage server.
|
|
4025
|
+
|
|
4026
|
+
Usage
|
|
4027
|
+
just-usage stop
|
|
4028
|
+
just-usage stop --port <n>
|
|
4029
|
+
just-usage stop --all
|
|
4030
|
+
|
|
4031
|
+
What it does
|
|
4032
|
+
The server writes a small pid file under ~/.config/just-usage/run/ when it
|
|
4033
|
+
starts. \`stop\` reads that file and asks the process to exit (SIGTERM, then
|
|
4034
|
+
SIGKILL if it ignores the first signal).
|
|
4035
|
+
|
|
4036
|
+
If the pid file is missing or stale — for example after a crash — \`stop\`
|
|
4037
|
+
looks for a listener on the port and checks GET /api/health. Only a confirmed
|
|
4038
|
+
just-usage server is stopped.
|
|
4039
|
+
|
|
4040
|
+
Options
|
|
4041
|
+
--port <n> Port the server is bound to (default ${DEFAULT_PORT})
|
|
4042
|
+
--all Stop every just-usage server this CLI has a pid file for,
|
|
4043
|
+
plus the default port if that is still running
|
|
4044
|
+
--help, -h Show this help
|
|
4045
|
+
|
|
4046
|
+
Examples
|
|
4047
|
+
just-usage stop
|
|
4048
|
+
just-usage serve --port 5758
|
|
4049
|
+
just-usage stop --port 5758
|
|
4050
|
+
just-usage stop --all
|
|
4051
|
+
|
|
4052
|
+
See also
|
|
4053
|
+
just-usage serve Start the server
|
|
4054
|
+
just-usage --help All commands
|
|
2934
4055
|
`;
|
|
2935
4056
|
function fail(msg, code = 1) {
|
|
2936
4057
|
console.error(msg);
|
|
@@ -2994,17 +4115,21 @@ Update available: v${u.current} → v${u.latest}. Run: just-usage upgrade
|
|
|
2994
4115
|
server = await startServer({ host, port, getUpdate: () => update });
|
|
2995
4116
|
} catch (e) {
|
|
2996
4117
|
const code = e.code;
|
|
2997
|
-
if (code === "EADDRINUSE")
|
|
2998
|
-
|
|
4118
|
+
if (code === "EADDRINUSE") {
|
|
4119
|
+
const stop = port === DEFAULT_PORT ? "just-usage stop" : `just-usage stop --port ${port}`;
|
|
4120
|
+
fail(`Port ${port} is already in use. Stop it with: ${stop}
|
|
4121
|
+
Or start another: just-usage serve --port ${port + 1}`);
|
|
4122
|
+
}
|
|
2999
4123
|
throw e;
|
|
3000
4124
|
}
|
|
3001
4125
|
console.log(`${PACKAGE_NAME} v${VERSION}`);
|
|
3002
4126
|
for (const u of server.urls) {
|
|
3003
4127
|
const tag = u.kind === "local" ? "local " : u.kind === "tailscale" ? "tailscale" : "network ";
|
|
3004
|
-
console.log(` ${tag} ${u.url}
|
|
4128
|
+
console.log(` ${tag} ${u.url}`);
|
|
3005
4129
|
}
|
|
4130
|
+
const stopHint = port === DEFAULT_PORT ? "just-usage stop" : `just-usage stop --port ${port}`;
|
|
3006
4131
|
console.log(`
|
|
3007
|
-
Press Ctrl+C to stop
|
|
4132
|
+
Press Ctrl+C to stop. From another terminal: ${stopHint}`);
|
|
3008
4133
|
if (shouldOpen)
|
|
3009
4134
|
openInBrowser(server.urls[0].url);
|
|
3010
4135
|
const shutdown = () => {
|
|
@@ -3026,10 +4151,10 @@ async function cmdStatus(argv) {
|
|
|
3026
4151
|
}
|
|
3027
4152
|
function cmdAccounts() {
|
|
3028
4153
|
const rows = listAccounts();
|
|
3029
|
-
console.log("Default accounts come from each CLI's own login (codex login, claude /login, cursor-agent login, opencode auth login).");
|
|
4154
|
+
console.log("Default accounts come from each CLI's own login (codex login, claude /login, cursor-agent login, grok login --oauth, opencode auth login, agy).");
|
|
3030
4155
|
if (rows.length === 0) {
|
|
3031
4156
|
console.log(`
|
|
3032
|
-
No extra accounts. Add one with: just-usage add codex | claude | opencode`);
|
|
4157
|
+
No extra accounts. Add one with: just-usage add codex | claude | opencode | antigravity`);
|
|
3033
4158
|
return;
|
|
3034
4159
|
}
|
|
3035
4160
|
console.log("");
|
|
@@ -3103,7 +4228,7 @@ async function cmdAdd(argv) {
|
|
|
3103
4228
|
});
|
|
3104
4229
|
const provider = positionals[0];
|
|
3105
4230
|
if (!isProvider(provider))
|
|
3106
|
-
fail(`Usage: just-usage add <codex|claude|opencode> [--label name] [--token]`);
|
|
4231
|
+
fail(`Usage: just-usage add <codex|claude|opencode|antigravity> [--label name] [--token]`);
|
|
3107
4232
|
switch (provider) {
|
|
3108
4233
|
case "codex":
|
|
3109
4234
|
return addCodex(values.label);
|
|
@@ -3111,8 +4236,28 @@ async function cmdAdd(argv) {
|
|
|
3111
4236
|
return values.token ? addClaudeTokenCli(values.label) : addClaudeProfile(values.label);
|
|
3112
4237
|
case "opencode":
|
|
3113
4238
|
return addOpenCodeCli(values.label);
|
|
4239
|
+
case "antigravity":
|
|
4240
|
+
return addAntigravityCli(values.label);
|
|
3114
4241
|
case "cursor":
|
|
3115
4242
|
fail("Cursor is single-account: just-usage shows whatever `cursor-agent` is logged in as.");
|
|
4243
|
+
case "grok":
|
|
4244
|
+
fail("Grok is single-account: just-usage shows whatever `grok` is logged in as.");
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
async function addAntigravityCli(label) {
|
|
4248
|
+
const { sessionId, authUrl } = await beginAntigravityAdd(label);
|
|
4249
|
+
console.log(`
|
|
4250
|
+
Open this URL to sign in (opening your browser):
|
|
4251
|
+
${authUrl}
|
|
4252
|
+
`);
|
|
4253
|
+
openInBrowser(authUrl);
|
|
4254
|
+
console.log("After Google sign-in, copy the code from the Antigravity page (or paste the callback URL).");
|
|
4255
|
+
const callback = await prompt("Code: ");
|
|
4256
|
+
try {
|
|
4257
|
+
const account = await submitAntigravityCallback(sessionId, callback);
|
|
4258
|
+
console.log(`Added ${account.id}${account.email ? ` (${account.email})` : ""}.`);
|
|
4259
|
+
} catch (e) {
|
|
4260
|
+
fail(e instanceof Error ? e.message : String(e));
|
|
3116
4261
|
}
|
|
3117
4262
|
}
|
|
3118
4263
|
async function cmdLogin(argv) {
|
|
@@ -3155,6 +4300,28 @@ async function cmdRemove(argv) {
|
|
|
3155
4300
|
fail(e instanceof Error ? e.message : String(e));
|
|
3156
4301
|
}
|
|
3157
4302
|
}
|
|
4303
|
+
async function cmdStop(argv) {
|
|
4304
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
4305
|
+
console.log(STOP_HELP);
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
const { values } = parseArgs({
|
|
4309
|
+
args: argv,
|
|
4310
|
+
options: {
|
|
4311
|
+
port: { type: "string", short: "p" },
|
|
4312
|
+
all: { type: "boolean" }
|
|
4313
|
+
},
|
|
4314
|
+
allowPositionals: true,
|
|
4315
|
+
strict: false
|
|
4316
|
+
});
|
|
4317
|
+
const port = values.port ? Number(values.port) : undefined;
|
|
4318
|
+
if (values.port && (!Number.isInteger(port) || port <= 0 || port > 65535))
|
|
4319
|
+
fail(`invalid port: ${values.port}`);
|
|
4320
|
+
const { text, code } = formatStopResults(await stopServers({ port, all: values.all === true }));
|
|
4321
|
+
console.log(text);
|
|
4322
|
+
if (code !== 0)
|
|
4323
|
+
process.exit(code);
|
|
4324
|
+
}
|
|
3158
4325
|
async function cmdUpgrade(argv) {
|
|
3159
4326
|
const { values } = parseArgs({ args: argv, options: { check: { type: "boolean" }, yes: { type: "boolean", short: "y" } }, allowPositionals: true, strict: false });
|
|
3160
4327
|
const info = await checkForUpdate(true);
|
|
@@ -3194,6 +4361,9 @@ async function main() {
|
|
|
3194
4361
|
return cmdServe([]);
|
|
3195
4362
|
case "serve":
|
|
3196
4363
|
return cmdServe(argv.slice(1));
|
|
4364
|
+
case "stop":
|
|
4365
|
+
case "quit":
|
|
4366
|
+
return cmdStop(argv.slice(1));
|
|
3197
4367
|
case "status":
|
|
3198
4368
|
return cmdStatus(argv.slice(1));
|
|
3199
4369
|
case "accounts":
|